find the matching objects from two array lists? list can contain same multiple objects

542 Views Asked by At

I have two list ListA listA = new ArrayList() and ListB listB = new ArrayList() both contain object of type Position object and Position contain these variables.

Position {
    String account;
    String Date;
    String Cycle;
    String Status;
} 

and if for example my lists has values like this

ListA = ["ACC1","20-Jan-23","1","open"],
        ["ACC1","20-Jan-23","2","closing"],
        ["ACC2","20-Jan-23","1","open"],
        ["ACC2","20-Jan-23","2","closing"],
        ["ACC3","20-Jan-23","1","open"],
        ["ACC3","20-Jan-23","2","closing"]

ListB = ["ACC1","20-Jan-23","1","open"],
        ["ACC1","20-Jan-23","2","closing"],
        ["ACC2","20-Jan-23","1","open"],
        ["ACC2","20-Jan-23","2","closed"],
        ["ACC3","20-Jan-23","1","open"]

now my requirement is from the above both lists, I need to find out and extract all accounts that exactly matches in the other list but uniquely, meaning

"ACC1" having two objects in listA and same exists in ListB so this the right candidate that i needed to extract

"ACC2" having two objects in both lists but only one matching exactly same with listB, but other record doesnt match because the status values differs ('closing' and 'closed') so i need to exclude ACC2

"ACC3" having two objects in listA but not in list B, so i need to exclude this ACC3 as well

so ACC1 is what i'm interested in

Is there any way we can achieve this efficiently using java streams or usual standard way

Thanks

3

There are 3 best solutions below

6
Eritrean On BEST ANSWER

Assuming you have all the necessary getters and have overriden the equals and hashCode methods, for example like this:

class Position {
    String account;
    String Date;
    String Cycle;
    String Status;

    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        final Position position = (Position) o;
        return Objects.equals(account, position.account) && Objects.equals(Date, position.Date)
               && Objects.equals(Cycle, position.Cycle) && Objects.equals(Status, position.Status);
    }

    @Override
    public int hashCode() {
        return Objects.hash(account, Date, Cycle, Status);
    }
}

You could stream over both lists, order them in an identical way and group them by account and use the resulting maps to filter accounts having the same list of Position objects. Example code:

import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;

public class Example2 {
    public static void main(String[] args) {

        List<Position> listA = List.of(new Position("ACC1", "20-Jan-23", "1", "open"),
                                       new Position("ACC1", "20-Jan-23", "2", "closing"),
                                       new Position("ACC2", "20-Jan-23", "1", "open"),
                                       new Position("ACC2", "20-Jan-23", "2", "closing"),
                                       new Position("ACC3", "20-Jan-23", "1", "open"),
                                       new Position("ACC3", "20-Jan-23", "2", "closing"));

        List<Position> listB = List.of(new Position("ACC1", "20-Jan-23", "1", "open"),
                                       new Position("ACC1", "20-Jan-23", "2", "closing"),    
                                       new Position("ACC2", "20-Jan-23", "1", "open"),
                                       new Position("ACC2", "20-Jan-23", "2", "closed"),
                                       new Position("ACC3", "20-Jan-23", "1", "open"));

        Comparator<Position> comparator = Comparator.comparing(Position::getAccount)
                                                    .thenComparing(Position::getDate)
                                                    .thenComparing(Position::getCycle)
                                                    .thenComparing(Position::getStatus);
        Map<String, List<Position>> mapA = listA.stream().sorted(comparator).collect(Collectors.groupingBy(Position::getAccount));
        Map<String, List<Position>> mapB = listB.stream().sorted(comparator).collect(Collectors.groupingBy(Position::getAccount));

        List<String> result = mapA.keySet()
                                  .stream()
                                  .filter(key -> mapA.get(key).equals(mapB.get(key)))
                                  .toList();

        System.out.println(result);

    }

    @AllArgsConstructor
    @Getter
    @ToString
    static class Position {
        String account;
        String Date;
        String Cycle;
        String Status;

        @Override
        public boolean equals(final Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            final Position position = (Position) o;
            return Objects.equals(account, position.account) && Objects.equals(Date, position.Date)
                   && Objects.equals(Cycle, position.Cycle) && Objects.equals(Status, position.Status);
        }

        @Override
        public int hashCode() {
            return Objects.hash(account, Date, Cycle, Status);
        }
    }
}

UPDATE

..been told not to override equal and hascode method in PositionEntity... is there any other way to compare the equality of the 2 list of objects without using equals method?

you can do the field by field comparison manually. I have added an example using BiPredicates. May be there are some eleganter ways to do this with some 3rd party libraries. But without changing the first approach too much the below should give you the same result without the need to override equals and hashCode.

import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.BiPredicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;

public class Example2 {
    public static void main(String[] args) {

        List<Position> listA = List.of(new Position("ACC1", "20-Jan-23", "1", "open"),
                                       new Position("ACC1", "20-Jan-23", "2", "closing"),
                                       new Position("ACC2", "20-Jan-23", "1", "open"),
                                       new Position("ACC2", "20-Jan-23", "2", "closing"),
                                       new Position("ACC3", "20-Jan-23", "1", "open"),
                                       new Position("ACC3", "20-Jan-23", "2", "closing"));

        List<Position> listB = List.of(new Position("ACC1", "20-Jan-23", "1", "open"),
                                       new Position("ACC1", "20-Jan-23", "2", "closing"),
                                       new Position("ACC2", "20-Jan-23", "1", "open"),
                                       new Position("ACC2", "20-Jan-23", "2", "closed"),
                                       new Position("ACC3", "20-Jan-23", "1", "open"));

        Comparator<Position> comparator = Comparator.comparing(Position::getAccount)
                                                    .thenComparing(Position::getDate)
                                                    .thenComparing(Position::getCycle)
                                                    .thenComparing(Position::getStatus);
        Map<String, List<Position>> mapA = listA.stream().sorted(comparator).collect(Collectors.groupingBy(Position::getAccount));
        Map<String, List<Position>> mapB = listB.stream().sorted(comparator).collect(Collectors.groupingBy(Position::getAccount));

        //Since you cannot modify equals and hashCode you can do the field by field comparison manually
        //there are maybe some 3rd party libraries which might do this elegantly,
        // but something like below should work fine
        BiPredicate<Position,Position> positionsEqual = (position1, position2) ->
                position1.getAccount().equals(position2.getAccount()) &&
                position1.getDate().equals(position2.getDate()) &&
                position1.getCycle().equals(position2.getCycle()) &&
                position1.getStatus().equals(position2.getStatus());

        //Same approach to test the equality of two lists index by index using above predicate
        BiPredicate<List<Position>,List<Position>> listsEqual = (list1, list2) -> {
            if (list1.size() != list2.size()){
                return false;
            }
            return IntStream.range(0, list1.size()).allMatch(i -> positionsEqual.test(list1.get(i), list2.get(i)));
        };

        //using the predicate to filter
        List<String> result = mapA.keySet()
                                  .stream()
                                  .filter(key -> listsEqual.test(mapA.get(key),(mapB.get(key))))
                                  .toList();

        System.out.println(result);

    }

    @AllArgsConstructor
    @Getter
    @ToString
    static class Position {
        String account;
        String Date;
        String Cycle;
        String Status;

        //Constructor, getter, toString..
    }
}
0
antonio_s87 On

As Eritrean stated above, you need to implement hashCode() and equals() methods. Instead of streams you could use HashSet:

    Set<Position> positionSet = new HashSet<>();
    listA.stream().forEach(position -> positionSet.add(position));
    Set<String> result = new ArrayList<>();
    for(Position position : listB){
        if(positionSet.contains(position)){
            result.add(position.getAccount());
        }
    }

So you find all Positions that are in both lists and then just add their accounts to the result set.

0
WJS On

This solution presumes you have overridden equals and hashCode which is a requirement. This is required for the map, and set data structures. I also made use of getters.

Putting the objects in sets would also make the lookups faster.

  • This puts the objects in Map<Boolean, Set<Position>>. I used two loops to avoid accessing external state in a stream. I also didn't feel it was warranted to combine the lists to use a single loop.
Map<Boolean, Set<Position>> map = new HashMap<>();

for (Position p : setA) {   
    map.computeIfAbsent(setA.contains(p) && setB.contains(p),
            v -> new HashSet<>()).add(p);
}
for (Position p : setB) {   
    map.computeIfAbsent(listA.contains(p) && setB.contains(p),
            v -> new HashSet<>()).add(p);
}
  • Once the values are separated into two sets, those accounts that were not in both sets are retrieved from the map. This is done by filtering on the boolean in the streamed entrySet.

Set<Position> partial = map.entrySet().stream().filter(Entry::getKey)
        .flatMap(e -> e.getValue().stream())
        .collect(Collectors.toCollection(HashSet::new));
  • Now that a partial list of possible successes has been retrieved, the Accounts that were not in both lists must be removed from the partial list.
for(Position p : map.get(false)) {
     partial.removeIf(pos -> pos.getAccount().equals(p.getAccount()));
}

results.forEach(System.out::println);

prints

Position [ACC1, 20-Jan-23, 1, open]
Position [ACC1, 20-Jan-23, 2, closing]