Comparing Two List by Using Java8 Matching Methods

comparing two different type of List using Java8

It doesn't clear how uniqueIdentificationNumber of the Person and Company are related. It's worth to refine these classes to represent the relationship between them in a better way (maybe a company can hold a reference to a list of customers). And don't overuse setters, if id is unique there's no need to allow it to be changed.

Although it's not clear how these values are connected because of the drawbacks of your class design technically it's doable.

return the customer object which is matching with Id

For that, you need to create two maps that will associate these identifiers with companies and persons. Then create a stream over the keys of one of these maps and check for every key whether if contained in another map. And then retrieve the Person objects for filtered keys and collect the result into a list.

    Map<Integer, Person> personById =
persons.stream()
.collect(Collectors.toMap(Person::getUniqueIdentificationNumber,
Function.identity()));
Map<Integer, Company> companyById =
companies.stream()
.collect(Collectors.toMap(Company::getUniqueIdentificationNumber,
Function.identity()));

List<Person> customers =
personById.keySet().stream()
.filter(companyById::containsKey) // checking whether id is present in the company map
.map(personById::get) // retrieving customers
.collect(Collectors.toList());

Update

Let me rephrase the description of the problem.

There are two unrelated classes A and B. Both classes have two fields of type int, let's say val1 and val2 (and maybe a few more fields but we are not interested in them).

We have a list of objects A and a list of objects B. The goal is to find a single object A for which exists an object B with the same values for both val1 and val2 (in order to keep things simple I propose to stick with this example).

There are two approaches that could be used for that purpose:

  • create an auxiliary class with two fields val1 and val2, and associate every instance of A and B with instances of this class;
  • create a nested map Map<Integer, Map<Integer, *targetClass*>>, this solution is more complicated and less flexible, if you'll need to compare objects by three, four, etc. fields the code will quickly become incomprehensible.

So I'll stick with the first approach. We need to declare the ValueHolder class with two fields and implement the equals/hashCode contract based on these fields. For Java 16 and above we can utilize a record for that purpose and make use of equals(), hashCode, getters provided by the compiler. The option with the record will look like this:

public record ValueHolder(int val1, int val2) {} // equals/hashCode, constructor and getters provided by the compiler

Classes A and B

public class A {
private int val1;
private int val2;

// constructor and getters
}

public class B {
private int val1;
private int val2;

// constructor and getters
}

And a method that accepts two lists: List<A> and List<B>, and return a result as Optional<A>. Because the matching element may or may not be present and it's a good practice to return an optional object in such cases instead of returning null in case the result was not found. It provides more flexibility and that's precisely the case for which the optional was designed.

public Optional<A> getMatchingItem(List<A> listA, List<B> listB) {

Map<ValueHolder, A> aByValue = listA.stream()
.collect(Collectors.toMap(a -> new ValueHolder(a.getVal1(), a.getVal2()),
Function.identity()));

Map<ValueHolder, B> bByValue = listB.stream()
.collect(Collectors.toMap(b -> new ValueHolder(b.getVal1(), b.getVal2()),
Function.identity()));

return aByValue.keySet().stream()
.filter(bByValue::containsKey)
.findFirst()
.map(aByValue::get);
}

Comparing two lists and getting differences

If I understand correctly, this is the example scenario:

  • listOne [datab] items: [A, B, C, D]
  • listTwo [front] items: [B, C, D, E, F]

and what you need to get as an effect is:

  • added: [E, F]
  • deleted: [A]

First thing first, I would use some type adapter or extend the different types from one common class and override the equals method so you can match them by id and name

Secondly, this is very easy operations on sets (you could use set's but list are fine too). I recommend using a library: https://commons.apache.org/proper/commons-collections/apidocs/org/apache/commons/collections4/CollectionUtils.html

And now basically:

  • added is listTwo - listOne
  • deleted is listOne - listTwo

and using java code:

  • added: CollectionUtils.removeAll(listTwo, listOne)
  • deleted: CollectionUtils.removeAll(listOne, listTwo)

Otherwise, all collections implementing Collection (Java Docs) also has removeAll method, which you can use.

Most Effective Way to get matched and unmatched objects in 2 ArrayLists

You don't need to sort these lists for this task.

In terms of the Set theory, you need to find the set difference. I.e. to find all unique objects that appear only in the first or in the second list.

This task can be solved in a few lines of code with liner time complexity. But it is important to implement the equals/hashCode contract in the EmployeeDetails.

public List<EmployeeDetails> compareLists(List<EmployeeDetails> fileOneEmpList,
List<EmployeeDetails> fileTwoEmpList) {

Set<EmployeeDetails> emp1 = new HashSet<>(fileOneEmpList);
Set<EmployeeDetails> emp2 = new HashSet<>(fileTwoEmpList);

emp1.removeAll(emp2);
emp2.removeAll(emp1);
emp1.addAll(emp2);

return new ArrayList<>(emp1);
}

The approach above is both the most efficient and the simplest.

If you are comfortable with Streams API, you can try another approach and implement this method in the following way:

public List<EmployeeDetails> compareLists(List<EmployeeDetails> fileOneEmpList,
List<EmployeeDetails> fileTwoEmpList) {

return Stream.of(new HashSet<>(fileOneEmpList), new HashSet<>(fileTwoEmpList)) // wrapping with sets to ensure uniqueness (if objects in the list are guaranteed to be unique - use lists instead)
.flatMap(Collection::stream)
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet().stream()
.filter(entry -> entry.getValue() == 1) // i.e. object appear only once either in the first or in the second list
.map(Map.Entry::getKey)
.collect(Collectors.toList()); // .toList(); for Java 16+
}

Time complexity of the stream based solution would be linear as well. But as I've said, the first solution based on the Collections API is simpler and slightly more performant.

If for some reason, there's no proper implementation of equals() and hashCode() in the EmployeeDetails. And you have no control over this class and can't change it. Then you can declare a wrapper class and perform the same actions.

Below is an example of how to create the wrapper using Java 16 records.
Methods equals() and hashCode() will be generated by the compiler based on empId and empDob.

public record EmployeeWrapper(String empId, String empDob) {
public EmployeeWrapper(EmployeeDetails details) {
this(details.getEmpID(), details.empDOB);
}
}

The implementation of the equals/hashCode for the EmployeeDetails class based on the empID and empDOB might look like this (also, you can use the facilities of your IDE to generate these methods):

    @Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

EmployeeDetails that = (EmployeeDetails) o;
return empID.equals(that.empID) && empDOB.equals(that.empDOB);
}

@Override
public int hashCode() {
return Objects.hash(empID, empDOB);
}

Java8 Streams - Compare Two List's object values and add value to sub object of first list?

Let listOne.size() is N and listTwo.size() is M.
Then 2-for-loops solution has complexity of O(M*N).

We can reduce it to O(M+N) by indexing listTwo by ids.

Case 1 - assuming listTwo has no objects with the same id
// pair each id with its marks
Map<String, String> marksIndex = listTwo.stream().collect(Collectors.toMap(ObjectTwo::getId, ObjectTwo::getMarks));
// go through list of `ObjectOne`s and lookup marks in the index
listOne.forEach(o1 -> o1.setScore(marksIndex.get(o1.getId())));
Case 2 - assuming listTwo has objects with the same id
    final Map<String, List<ObjectTwo>> marksIndex = listTwo.stream()
.collect(Collectors.groupingBy(ObjectTwo::getId, Collectors.toList()));

final List<ObjectOne> result = listOne.stream()
.flatMap(o1 -> marksIndex.get(o1.getId()).stream().map(o2 -> {
// make a copy of ObjectOne instance to avoid overwriting scores
ObjectOne copy = copy(o1);
copy.setScore(o2.getMarks());
return copy;
}))
.collect(Collectors.toList());

To implement copy method you either need to create a new object and copy fields one by one, but in such cases I prefer to follow the Builder pattern. It also results in more "functional" code.

Java 8: More efficient way of comparing lists of different types?

Your question’s code does not reflect what you describe in the comments. In the comments you say that all names should be present and the size should match, in other words, only the order may be different.

Your code is

List<Person> people = getPeopleFromDatabasePseudoMethod();
List<String> expectedValues = Arrays.asList("john", "joe", "bill");

assertTrue(people.stream().map(person -> person.getName())
.collect(Collectors.toList()).containsAll(expectedValues));

which lacks a test for the size of people, in other words allows duplicates. Further, using containsAll combining two Lists in very inefficient. It’s much better if you use a collection type which reflects you intention, i.e. has no duplicates, does not care about an order and has an efficient lookup:

Set<String> expectedNames=new HashSet<>(expectedValues);
assertTrue(people.stream().map(Person::getName)
.collect(Collectors.toSet()).equals(expectedNames));

with this solution you don’t need to test for the size manually, it is already implied that the sets have the same size if they match, only the order may be different.

There is a solution which does not require collecting the names of persons:

Set<String> expectedNames=new HashSet<>(expectedValues);
assertTrue(people.stream().allMatch(p->expectedNames.remove(p.getName()))
&& expectedNames.isEmpty());

but it only works if expectedNames is a temporary set created out of the static collection of expected names. As soon as you decide to replace your static collection by a Set, the first solution doesn’t require a temporary set and the latter has no advantage over it.

Iterate two lists to find a match and return boolean using java8

You can use a stream pipeline on both lists and call anyMatch:

return CollectionUtils.isNotEmpty(pathNames) &&
newPaths.stream().anyMatch(np ->
pathNames.stream().anyMatch(pn -> FilenameUtils.wildcardMatch(np, pn)));


Related Topics



Leave a reply



Submit