Rüdiger
Rüdiger

Reputation: 943

Java: Proper way of creating a list containing all not-in-intersect elements from two given lists based on a specific attribute?

Given two Lists of Objects, I'd be able to tell which items are not in their intersect based on one of their attributes. Let's look at the following example:

I have a class Foo that has two attributes: boo and placeholder

class Foo {
    private int boo;
    private int placeholder = 1;

    public Foo(int boo) {
        this.boo = boo;
    }

    public int getBoo() {
        return boo;
    }
}

Now I am creating two Lists from that (let's say this is my input)

    List<Foo> list1 = new ArrayList<Foo>();
    list1.add(new Foo(1));
    list1.add(new Foo(2));
    list1.add(new Foo(3));

    List<Foo> list2 = new ArrayList<Foo>();
    list2.add(new Foo(0));
    list2.add(new Foo(1));
    list2.add(new Foo(2));

And now I'd like to say which Items are in list1 and not in list2 or in list2 and not in list1 based on their attribute boo. So in the above example I want a List<Foo> notInIntersectList that contains one Foo(0) and one Foo(3).

    List<Foo> notInIntersectList = new ArrayList<Foo>();
    list1.forEach(li1foo -> {
        boolean inBothLists = false;
        list2.forEach(li2foo -> {
            if (li1foo.getBoo() == li2foo.getBoo()) {
                inBothLists = true;
            }
        });
        if (!inBothLists) {
            notInIntersectList.add(li1foo);
        }
    });
    //now I covered all items in list1 but not in list2. Now do this again with lists swapped, so I can also cover those.
    //...

Sadly I am getting Local variable inBothLists defined in an enclosing scope must be final or effectively final as an error. How is this issue solved properly, since this seems not to be the "right" solution?

Upvotes: 5

Views: 1154

Answers (6)

Carlos Cavero
Carlos Cavero

Reputation: 3176

I see here four possible solutions:

1) Avoid lambda expression following @Anony-Mousse's response

2) Include the variable at the class level (not recommended because this boolean is meant for local use):

public class Testing {
 boolean inBothLists = true;

 public static void main(String[] args) {
  List<Foo> notInIntersectList = new ArrayList<Foo>();
  list1.forEach(li1foo -> {
    inBothLists = false;
    list2.forEach(li2foo -> {
        if (li1foo.getBoo() == li2foo.getBoo()) {
            inBothLists = true;
        }
    });
    if (!inBothLists) {
        notInIntersectList.add(li1foo);
    }
  });

  System.out.println("Intersected values:");
  notInIntersectList.forEach(liInFoo -> {
        System.out.println(liInFoo);
  });

 }

3) Using contains method from List (avoiding boolean):

List<Foo> notInIntersectList = new ArrayList<Foo>();
list1.forEach(li1foo -> {
    if (!list2.contains(li1foo)) 
        notInIntersectList.add(li1foo);
});

4) Java8 Stream API (already mentioned in another answer and avoiding boolean):

List<Foo> notInIntersectList = new ArrayList<Foo>();
list1.forEach(li1foo -> {
        Foo result = list2.stream()
                  .filter(li2foo -> li1foo.getBoo() == li2foo.getBoo() )
                  .findAny()
                  .orElse(null);
        if (result == null)
            notInIntersectList.add(li1foo);
});

I would go for option 1), 3) or 4). I only kept 2.) to show an example using variables inside lambda functions.

UPDATE: I found 3) and 4) options in a recent Baeldung post on how to find an Element in a List

Upvotes: 0

Has QUIT--Anony-Mousse
Has QUIT--Anony-Mousse

Reputation: 77454

avoid Lambda expressions

They make code harder to read, and less efficient.

If you simply use traditional for loops, your code should work... plus, you can use break to stop searching for a second match.

    List<Foo> notInIntersectList = new ArrayList<Foo>();
    for(Foo li1foo : list1) {
        boolean inBothLists = false;
        for(Foo li2foo : list2) {
            if (li1foo.getBoo() == li2foo.getBoo()) {
                inBothLists = true;
            }
        }
        if (!inBothLists) {
            notInIntersectList.add(li1foo);
        }
    }

You probably still recognize your code there... Now here's the pro version with imperative coding:

    List<Foo> notInIntersectList = new ArrayList<Foo>();
    nextfoo: for(Foo li1foo : list1) {
        for(Foo li2foo : list2)
            if (li1foo.getBoo() == li2foo.getBoo())
                continue nextfoo;
        notInIntersectList.add(li1foo);
    }

This actually has a very clear and explicit logic (and it will make functional fans puke because of the effective "goto"). It's still slow if list2 is huge though, I didn't want to change your algorithm.

Upvotes: 1

Dorian Gray
Dorian Gray

Reputation: 2981

First of all, you shall add the methods equals and hashCode to your class Foo (see Why do I need to override the equals and hashCode methods in Java?)

class Foo {
    private int boo;
    private int placeholder = 1;

    public Foo(int boo) {
        this.boo = boo;
    }

    public int getBoo() {
        return boo;
    }

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

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (!(obj instanceof Foo))
            return false;
        Foo other = (Foo) obj;
        return boo == other.boo;
    }

}

Now you can use the removeAll method of List:

Removes all of this collection's elements that are also contained in the specified collection (optional operation). After this call returns, this collection will contain no elements in common with the specified collection.

You will have to build a new List notInIntersectList like that:

    List<Foo> listIntersection = new ArrayList<>(list1);
    listIntersection.removeAll(list2);

    List<Foo> notInIntersectList = new ArrayList<>(list1);
    notInIntersectList.addAll(list2);
    notInIntersectList.removeAll(listIntersection);

Upvotes: 3

Charlie
Charlie

Reputation: 1179

There is already a library for that:

Set<String> wordsWithPrimeLength = ImmutableSet.of("one", "two", "three", "six", "seven", "eight");
Set<String> primes = ImmutableSet.of("two", "three", "five", "seven");

SetView<String> intersection = Sets.intersection(primes, wordsWithPrimeLength); // contains "two", "three", "seven"
// I can use intersection as a Set directly, but copying it can be more efficient if I use it a lot.
return intersection.immutableCopy();

If you use difference(Set<E> set1, Set<?> set2) instead of intersection(Set<E> set1, Set<?> set2), you will get the difference of the two.

With ImmutableSet.copyOf(Collection<? extends E> elements) you can create a Set.

It is called Guava and provides many collection operations: https://github.com/google/guava/wiki/CollectionUtilitiesExplained

The API: https://google.github.io/guava/releases/19.0/api/docs/com/google/common/collect/ImmutableSet.html

Upvotes: 0

Thiyagu
Thiyagu

Reputation: 17890

You cannot mutate variables inside a lambda expression (See: Variable used in lambda expression should be final or effectively final)

Here's a way to fix your code (fun with Streams)

List<Foo> notInIntersectList = list1.stream()
        .filter(fooElementFromList1 -> list2
                .stream()
                .noneMatch(fooElementFromList2 -> fooElementFromList2.getBoo() == fooElementFromList1.getBoo()))
        .collect(Collectors.toCollection(ArrayList::new));

list2.stream()
        .filter(fooElementFromList2 -> list1
            .stream()
            .noneMatch(fooElementFromList1 -> fooElementFromList1.getBoo() == fooElementFromList2.getBoo()))
        .forEach(notInIntersectList::add);

The complexity of this is O(n*m) (where n and m are the number of elements in list1 and list2 respectively).

To do this in O(n+m), you can use a Set. For this, you need a equals and hashcode method on the Foo class. This considers two Foo instances as equal only based on the value of the instance variable boo.

class Foo {
    ....

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Foo other = (Foo) obj;
        return boo == other.boo;
    }

    @Override
    public int hashCode() {
        return boo;
    }
}

And use a Set for this as

Set<Foo> fooSet1 = new HashSet<>(list1);
Set<Foo> fooSet2 = new HashSet<>(list2);

fooSet1.removeAll(list2);
fooSet2.removeAll(list1);

List<Foo> notInIntersectList = Stream.concat(fooSet1.stream(), fooSet2.stream())
            .collect(Collectors.toList());

Upvotes: 6

RealSkeptic
RealSkeptic

Reputation: 34628

If you cannot create an equals and hashCode on your class (maybe they have them already but not based on boo), what I would do is:

  • Create a Set<Integer> (or a BitSet) containing all the boo values in list 1. Call it set1
  • Create a Set<Integer> (or a BitSet) containing all the boo values in list 2. Call it set2
  • Use set1.retainAll(set2) to get the intersection of the two sets.
  • Use the following to create my list:

    Stream.concat(list1.stream(),list2.stream())
          .filter(item-> ! set1.contains(item.getBoo()))
          .collect(Collectors.toList);
    

This is O(m+n) and also ensures the order of the items in the original lists is maintained, as well as any duplicates (items having the same boo).

Upvotes: 3

Related Questions