You want intersection of the list while saving of first list type.

Get the common field values in the Set.

valuesToCheck=secondList.stream().map(SecondListObject::commonFiled).collect(Collectors.toList);

'''

Apply a stream on the first while filtering based on the matching common field value in the set built in the previous step.

firstList.stream().filter(x->valuesToCheck.contains(x.getCommonFiled)).collect(toList)

You got the gist.

Answer from Gajraj Tanwar on Stack Overflow
Top answer
1 of 2
1

Supposing that the class is named Foo and that the field to change is String valueToReplace with getter/setter, you could use listOne.replaceAll() in this way :

list1.replaceAll(one ->  list2.stream()
                              .filter(other -> other.getCode().equals(one.getCode())
                              .findAny()
                              .map(Foo::getValueToReplace)
                              .ifPresent( newValue -> one.setValueToReplace(newValue));
                        return one;
                )

The idea is for each elements of list1 you replace the valueToReplace field of it by the value of the first match of list2. Otherwise you do nothing.
This code is not as efficient as it could but for small lists it is perfect.
For bigger lists, using a Map to store code/valueToReplace is very welcome.

// supposing that the code are unique in each list
Map<Integer, String>  mapOfValueToReplaceByCode =
    list2.stream()
         .collect(toMap(Foo::getCode, Foo::getValueToReplace));

list1.replaceAll(one -> {
                 String newValue = mapOfValueToReplaceByCode.get(one.getCode());
                 if (newValue != null){ 
                     one.setValueToReplace(newValue);
                 }
                  return one;
                )
2 of 2
1

Just keep the replace values in a map (with code as key) and then iterate over list1 to modify where necessary.

Map<String, String> replaceValues = list2.stream()
    .collect(Collectors.toMap(x -> x.code, x -> x.tobereplace));
list1.stream
    .filter(x -> replaceValues.containsKey(x.code))
    .forEach(x -> x.tobereplace = replaceValues.get(x.code));

EDIT

As josejuan points out in the comments, the Collectors.toMap will throw an exception if list2 contains duplicate values. The OP doesn't really specify what to do in that case, but the solution is using a merge function in the Collectors.toMap.

This will use the first element it encounters with any given code:

Map<String, String> replaceValues = list2.stream()
    .collect(Collectors.toMap(x -> x.code, x -> x.tobereplace, (x1, x2) -> x1));

The merge policy could be anything, like using the first element with a non-empty value e.g.

If the list was known to not have duplicates, please use a Set instead of a List. It will make things clearer for anyone reading the code, and help you avoid unnecessary checks.

🌐
Stack Overflow
stackoverflow.com › questions › 59376939 › how-to-compare-two-lists-of-java-objects-by-property-values
testing - How to Compare Two Lists of Java Objects by Property Values - Stack Overflow
December 17, 2019 - import java.util.*; public class TestListConvert { public static void main(String[] args) { List<Obs> firstList = new ArrayList<>(); List<Obs> secondList = new ArrayList<>(); firstList.add(new Obs("1","2")); secondList.add(new Obs("1","2")); System.out.println(firstList.equals(secondList)); } private static class Obs{ private String x; private String y; public Obs(String x, String y) { this.x = x; this.y = y; } public String getX() { return x; } public void setX(String x) { this.x = x; } public String getY() { return y; } public void setY(String y) { this.y = y; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Obs obs = (Obs) o; return Objects.equals(x, obs.x) && Objects.equals(y, obs.y); } @Override public int hashCode() { return Objects.hash(x, y); } } }
🌐
Coderanch
coderanch.com › t › 673893 › java › Comparing-ArrayLists-objects-shared-properties
Comparing two ArrayLists containing different objects with (some) shared properties (Beginning Java forum at Coderanch)
December 15, 2016 - In short, what I'd like to code ... current solution I have in mind is to simply iterate through the two ArrayLists at the same time (after sorting them), comparing the shared properties, and if they match up, continue the loop....
Top answer
1 of 3
12

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.

2 of 3
3

Following code copies marks from ObjectTwo to score in ObjectOne, if both ids are equal, it doesn't have intermediate object List<ObjectOne> result

listOne.stream()
    .forEach(one -> {listTwo.stream()
        .filter(two -> {return two.getId().equals(one.getId());})
        .limit(1)
        .forEach(two -> {one.setScore(two.getMarks());});
    });
🌐
Quora
quora.com › How-can-two-lists-of-objects-be-compared-in-Java
How can two lists of objects be compared in Java? - Quora
Answer (1 of 4): A̲l̲r̲i̲g̲h̲t̲ ̲,̲ ̲s̲o̲ ̲y̲o̲u̲ ̲w̲a̲n̲n̲a̲ ̲c̲o̲m̲p̲ar̲e̲ ̲t̲w̲o̲ ̲l̲is̲t̲s̲ ̲o̲f̲ ̲o̲b̲j̲e̲c̲t̲s ̲i̲n̲ ̲J̲a̲v̲a̲—̲f̲i̲r̲s̲t̲ ̲t̲h̲i̲n̲g̲ ̲t̲o̲ ̲c̲h̲e̲c̲k̲ ̲i̲s̲ ̲i̲f̲ ̲t̲h̲e̲y̲’̲r̲e̲ ...
Find elsewhere
🌐
Backendless
support.backendless.com › server code › codeless
Best way to compare two lists of objects - Codeless - Backendless Support
January 8, 2022 - I am trying to figure out the most efficient way (lowest execution time) to filter two lists of objects, keeping only a sublist of list A whose objects are not in list B. One list is coming form an external API and the …
🌐
DZone
dzone.com › coding › languages › how to compare list objects in java 7 vs. java 8
How to Compare List Objects in Java 7 vs. Java 8 - DZone
June 1, 2018 - If any number from aList is present in List 2 :true If any number from aList is not present in List 2 :false If all numbers from aList are present in List 2 :true · If you enjoyed this article and want to learn more about Java Streams, check out this collection of tutorials and articles on all things Java Streams.
Top answer
1 of 4
19

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.

2 of 4
18

I propose solution using java 8 streams:

    ArrayList<ObjOne> list = new ArrayList<>(Arrays.asList(new ObjOne("1","1"),new ObjOne("3","3"),new ObjOne("2","2")));
    ArrayList<ObjTwo> list2 = new ArrayList<>(Arrays.asList(new ObjTwo("1","1"),new ObjTwo("3","3"),new ObjTwo("4","4")));

    List<ObjOne> removed = list.stream().filter(o1 -> list2.stream().noneMatch(o2 -> o2.getId().equals(o1.getId())))
            .collect(Collectors.toList());
    System.out.print("added ");
    removed.forEach(System.out::println);

    List<ObjTwo> added = list2.stream().filter(o1 -> list.stream().noneMatch(o2 -> o2.getId().equals(o1.getId())))
             .collect(Collectors.toList());

    System.out.print("removed ");
    added.forEach(System.out::println);

This is basically your solution but implemented using streams, which will make your code shorter and easer to read

🌐
Javaprogramto
javaprogramto.com › 2020 › 04 › how-to-compare-two-arraylist-for-equality-in-java.html
How to compare two ArrayList for equality in Java 8? ArrayList equals() or containsAll() methods works? JavaProgramTo.com
June 13, 2021 - how to compare two lists in java using equals() and containsAll() method. These two methods take List as an argument and compare each and every object are same in each list. equals() method is overridden in ArrayList class.
Top answer
1 of 3
7

You can make the code shorter with filter and count, but using loops is cleaner here. Streams are not always the solution.

return Arrays.asList(
    IntStream.range(0, Math.min(a.size(), b.size())).filter(i -> a.get(i) > b.get(i)).count(),
    IntStream.range(0, Math.min(a.size(), b.size())).filter(i -> a.get(i) < b.get(i)).count()
);
2 of 3
3

Java 8 - partitioningBy() & counting()

One of the way to solve this problem with streams performing only one iteration over the given set of data is to make use of the built Collectors partitioningBy() and counting():

public static List<Integer> compareTriplets(List<Integer> a, List<Integer> b) {
    
    return IntStream.range(0, a.size())
        .map(i -> a.get(i) - b.get(i))
        .filter(i -> i != 0)
        .boxed()
        .collect(Collectors.collectingAndThen(
            Collectors.partitioningBy(i -> i > 0, Collectors.counting()),
            map -> Arrays.asList(map.get(true).intValue(), map.get(false).intValue())
        ));
}

Java 8 - custom Collector

Another option would be to define a custom Collector using static factory method Collector.of(). As well as previous approach, it would allow to process the data using a single stream:

public static List<Integer> compareTriplets(List<Integer> a, List<Integer> b) {
    
    return IntStream.range(0, a.size())
        .boxed()
        .collect(Collector.of(
            () -> new int[]{0, 0},
            (int[] score, Integer i) -> {
                if (a.get(i) > b.get(i)) score[0]++;
                if (b.get(i) > a.get(i)) score[1]++;
            },
            (int[] left, int[] right) -> {
                Arrays.setAll(left, i -> left[i] + right[i]);
                return left;
            },
            arr -> Arrays.asList(arr[0], arr[1])
        ));
}

Java 12 - Collector teeing()

Another option that would allow to produce the result using a single stream is Java 12 Collector teeing(), which expects two downstream Collectors and a Function which performs a final transformation by merging the results they produced.

public static List<Integer> compareTriplets(List<Integer> a, List<Integer> b) {
    
    return IntStream.range(0, a.size())
        .boxed()
        .collect(Collectors.teeing(
            Collectors.filtering(i -> a.get(i) > b.get(i), Collectors.counting()),
            Collectors.filtering(i -> b.get(i) > a.get(i), Collectors.counting()),
            (alice, bob) -> List.of(alice.intValue(), bob.intValue())
        ));
}
Top answer
1 of 3
20

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
2 of 3
9

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

Top answer
1 of 1
1

I find the current algorithm of "adding and removing" to be a streamed implementation of non-functional thinking. Rather than adding and removing, I encourage you to think in terms of joining (akin to SQL outer-joining) and filtration, which are more functional concepts. In this interpretation, your "local" collection is on the left-hand side of a left outer join, and your "remote" collection is on the right. This reduces the number of iterations from your six down to two, and produces the same output:

Customer.java

package com.stackexchange.ConsentExample;

import java.util.Collection;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public record Customer(
    Long id,
    String name,
    Integer consent
) {
    @Override
    public String toString() {
        return String.format(
            "Customer{id=%d, name='%s', consent=%d}",
            id, name, consent);
    }

    public static Stream<Customer> join(
        Collection<Customer> locals,
        Collection<Customer> remotes
    ) {
        /* Take local consent if there is no matching remote consent.
           Take remote consent if there is a match. */
        Map<Integer, Customer> remoteByConsent =
            remotes.stream()
            .collect(Collectors.toMap(
                Customer::consent, Function.identity()
            ));

        return locals.stream()
            .map(local ->
                remoteByConsent.getOrDefault(local.consent, local)
            );
    }
}

JoinTest.java

import com.stackexchange.ConsentExample.Customer;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

public class JoinTest {
    @Test
    public void testJoin() {
        final List<Customer>
        remotes = List.of(
            new Customer(10L, "name1", 12),
            new Customer(11L, "name2", 11),
            new Customer(12L, "name3", 14),
            new Customer(13L, "name4", 16)
        ),
        locals = List.of(
            new Customer(1L, "name1", 12),
            new Customer(2L, "name2", 13),
            new Customer(3L, "name3", 14),
            new Customer(4L, "name4", 15)
        );
        Map<Long, Customer> result =
            Customer.join(locals, remotes)
            .collect(Collectors.toMap(
                Customer::id, Function.identity()
            ));

        assertEquals(4, result.size());

        assertEquals("name1", result.get(10L).name());
        assertEquals(12, result.get(10L).consent());
        assertEquals("name2", result.get(2L).name());
        assertEquals(13, result.get(2L).consent());
        assertEquals("name3", result.get(12L).name());
        assertEquals(14, result.get(12L).consent());
        assertEquals("name4", result.get(4L).name());
        assertEquals(15, result.get(4L).consent());
    }
}