Java 8 Streams - Compare two Lists' object values and add value to new List?

71,364

Solution 1

Let's run through each part of the code. First, createSharedListViaStream:

public static List<SchoolObj> createSharedListViaStream(List<SchoolObj> listOne, List<SchoolObj> listTwo)
{
    // We create a stream of elements from the first list.
    List<SchoolObj> listOneList = listOne.stream()
    // We select any elements such that in the stream of elements from the second list
    .filter(two -> listTwo.stream()
    // there is an element that has the same name and school as this element,
        .anyMatch(one -> one.getName().equals(two.getName()) 
            && two.getSchool().equals(one.getSchool())))
    // and collect all matching elements from the first list into a new list.
    .collect(Collectors.toList());
    // We return the collected list.
    return listOneList;
}

After running through the code, it does exactly what you want it to do. Now, let's run through createSharedListViaLoop:

public static List<SchoolObj> createSharedListViaLoop(List<SchoolObj> listOne, List<SchoolObj> listTwo)
{
    // We build up a result by...
    List<SchoolObj> result = new ArrayList<SchoolObj>();
    // going through each element in the first list,
    for (SchoolObj one : listOne)
    {
    // going through each element in the second list,
        for (SchoolObj two : listTwo)
        {
    // and collecting the first list's element if it matches the second list's element.
            if (one.getName().equals(two.getName()) && one.getSchool().equals(two.getSchool()))
            {
                result.add(one);
            }
        }
    }
    // We return the collected list
    return result;
}

So far, so good... right? In fact, your code in createSharedListViaStream is fundamentally correct; instead, it is your createSharedListViaLoop that may be causing discrepancies in output.

Think about the following set of inputs:
List1 = [SchoolObj("nameA","SchoolX"), SchoolObj("nameC","SchoolZ")]
List2 = [SchoolObj("nameA","SchoolX"), SchoolObj("nameA","SchoolX"), SchoolObj("nameB","SchoolY")]

Here, createSharedListViaStream will return the only element of the first list that appears in both lists: SchoolObj("nameA","SchoolX"). However, createSharedListViaLoop will return the following list: [SchoolObj("nameA","SchoolX"),SchoolObj("nameA","SchoolX")]. More precisely, createSharedListViaLoop will collect the correct object, but it will do so twice. I suspect this to be the reason for the output of createSharedListViaStream to be "incorrect" based on comparison to the output of createSharedListViaLoop.

The reason that createSharedListViaLoop does this duplication is based on the lack of termination of its inner for loop. Although we iterate over all elements of the first list to check if they are present in the second, finding a single match will suffice to add the element to the result. We can avoid redundant element addition by changing the inner loop to the following:

for (SchoolObj one : listOne)
    {
    for (SchoolObj two : listTwo)
    {
        if (one.getName().equals(two.getName()) && one.getSchool().equals(two.getSchool()))
        {
            result.add(one);
            break;
        }
    }
}

Additionally, if you don't want duplicate Objects in your list (by location in memory), you can use distinct like so:

List<SchoolObj> result = ...;
result = result.stream().distinct().collect(Collectors.toList());

As a final caution, the above will keep the results distinct in the following scenario:

List<SchoolObj> list = new ArrayList<>();
SchoolObj duplicate = new SchoolObj("nameC", "schoolD");
listOne.add(duplicate);
listOne.add(duplicate);
list.stream().distinct().forEach(System.out::println); 
// prints:
// nameC schoolD

However, it will not work in the following scenario, unless you override the equals method for SchoolObj:

List<SchoolObj> list = new ArrayList<>();
listOne.add(new SchoolObj("nameC", "schoolD"));
listOne.add(new SchoolObj("nameC", "schoolD"));
list.stream().distinct().forEach(System.out::println); 
// prints (unless Object::equals overridden)
// nameC schoolD
// nameC schoolD

Solution 2

You can filter in one list if contains in another list then collect.

List<SchoolObj> listCommon = listTwo.stream()
                                         .filter(e -> listOne.contains(e)) 
                                         .collect(Collectors.toList());

You need to override equals() method in SchoolObj class. contains() method you will uses the equals() method to evaluate if two objects are the same.

@Override
public boolean equals(Object o) {
    if (!(o instanceof SchoolObj))
        return false;
    SchoolObj n = (SchoolObj) o;
    return n.name.equals(name) && n.school.equals(school);
}

But better solution is to use Set for one list and filter in another list to collect if contains in Set. Set#contains takes O(1) which is faster.

Set<SchoolObj> setOne = new HashSet<>(listOne);
List<SchoolObj> listCommon = listTwo.stream()
                                     .filter(e -> setOne.contains(e)) 
                                     .collect(Collectors.toList());

You need to override hashCode() method also along with equals() in SchoolObj class for Set#contains.(assuming name and school can't be null)

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + name.hashCode();
    result = prime * result + school.hashCode();
    return result;
}

Here you will get details how to override equals and hashCode in a better way

Share:
71,364
Admin
Author by

Admin

Updated on July 05, 2022

Comments

  • Admin
    Admin almost 2 years

    I have two Lists containing objects of this class:

    public class SchoolObj
    {
        private String name;
        private String school;
    
        public SchoolObj()
        {
            this(null, null);
        }
    
        public SchoolObj(String nameStr, String schoolStr)
        {
            this.setName(nameStr);
            this.setSchool(schoolStr);
        }
    
        public String getName()
        {
            return this.name;
        }
    
        public void setName(String name)
        {
            this.name = name;
        }
    
        public String getSchool()
        {
            return this.school;
        }
    
        public void setSchool(String school)
        {
            this.school = school;
        }
    
        @Override
        public String toString()
        {
            return this.getName() + ' ' + this.getSchool();
        }
    }
    

    I want to compare the objects in those two lists by name and school. If they are equal I need to create a new List containing those SchoolObj objects which are found in both Lists.

    I know we can use two for loops and do it is in the createSharedListViaLoop method below.

    My question is, how can I accomplish the same thing with Java streams?

    I tried with createSharedListViaStream below, but it is not working as expected.

    import java.util.ArrayList;
    import java.util.List;
    import java.util.stream.Collectors;
    
    public class StreamTest
    {
        public static void main(String[] args)
        {
            List<SchoolObj> listOne = new ArrayList<SchoolObj>();
            // TODO: Add sample data to listOne.
            listOne.add(new SchoolObj("nameA", "schoolX"));
            listOne.add(new SchoolObj("nameC", "schoolZ"));
    
            List<SchoolObj> listTwo = new ArrayList<SchoolObj>();
            // TODO: Add sample data to listTwo.
            listTwo.add(new SchoolObj("nameA", "schoolX"));
            listTwo.add(new SchoolObj("nameB", "schoolY"));
    
            // Print results from loop method.
            System.out.println("Results from loop method:");
            List<SchoolObj> resultsViaLoop = StreamTest.createSharedListViaLoop(listOne, listTwo);
            for (SchoolObj obj : resultsViaLoop)
            {
                System.out.println(obj);
            }
    
            // Print results from stream method.
            System.out.println("Results from stream method:");
            List<SchoolObj> resultsViaStream = StreamTest.createSharedListViaStream(listOne, listTwo);
            for (SchoolObj obj : resultsViaStream)
            {
                System.out.println(obj);
            }
        }
    
        public static List<SchoolObj> createSharedListViaLoop(List<SchoolObj> listOne, List<SchoolObj> listTwo)
        {
            List<SchoolObj> result = new ArrayList<SchoolObj>();
    
            for (SchoolObj one : listOne)
            {
                for (SchoolObj two : listTwo)
                {
                    if (one.getName().equals(two.getName()) && one.getSchool().equals(two.getSchool()))
                    {
                        result.add(one);
                    }
                }
            }
    
            return result;
        }
    
        public static List<SchoolObj> createSharedListViaStream(List<SchoolObj> listOne, List<SchoolObj> listTwo)
        {
            List<SchoolObj> listOneList = listOne.stream().filter(two -> listTwo.stream()
                  .anyMatch(one -> one.getName().equals(two.getName()) && two.getSchool().equals(one.getSchool()))) 
                  .collect(Collectors.toList());
    
            return listOneList;
        }
    }