Filter a List with Java8 only when other List has elements

Viewed 188

I am trying to write code to filter elements from a list if they exist in the another list.

The business logic requires me to return all elements from the first list if the second list is empty.

List<String> list1 = Arrays.asList("a","b","c","d");         
//List<String> list2 = Arrays.asList("a","d");       
List<String> list2 = Collections.emptyList();        

if (!list2.isEmpty()) {
  List<String> finalList = list1.stream()
      .map(x -> x)
      .filter(x -> list2.isEmpty() && list2.contains(x))
      .collect(toList());
  return finalList;
} else {
  return list1;
}

Is there a better way to do it? Preferably, in a single lambda function.

4 Answers

Firstly, condition list2.isEmpty() && list2.contains(x) is incorrect, and it'll never get evaluated to true. Because list2 can either be empty or contain a particular object, but not both.

Operation filter() will retain only those elements that match the predicate, hence your stream will always generate an empty list.

Since the possibility of list2 being empty is eliminated by preceding if statement, I assume that the correct filtering condition has to be list2.contains(x).

Is there a better way to do it? Preferably in a single lambda function.

You can achieve that with Java 8 method Collection.removeIf() that expects a Predicate and will remove every element from the source that matches the given predicate.

You don't need initial values in the list1 you can invoke removeIf() directly on it, otherwise you need to make a defensive copy.

For better performance, you need to wrap the second list with a Set, in this case contains() check will be done in a constant time.

Set<String> set2 = new HashSet<>(list2);
list1.removeIf(x -> !set2.contains(x); // element will get removed if not present in the set
return list1;

Or you can generate a new list like that:

Set<String> set2 = new HashSet<>(list2);
return list1.stream()
        .filter(set2::contains)        // element will be preserved if it's present in the set
        .collect(Collectors.toList()); // or `toList()` with Java 16+

You could try streaming the second list into a set to avoid traversing the entire list each time (O(n^2)) in your filter predicate.

public List<String> filterElementsPresentInBothLists(List<String> list1, List<String> list2) {
        assert list2 != null;
        return list2.stream()
                .collect(Collectors.collectingAndThen(Collectors.toSet(),
                        setView -> setView.isEmpty() ? list1 :
                                list1.stream().filter(setView::contains)
                                        .collect(Collectors.toList())));
    }

I see the mapping also is not required, and the below one satisfies your requirement,

        List<String> list1 = Arrays.asList("a","b","c","d");
        //List<String> list2 = Arrays.asList("a","d");
        List<String> list2 = Collections.emptyList();
        List<String> finalList = list1.stream()
                                    .filter(list2::contains)
                                    .toList(); // Java 16+
        List<String> list = toList.isEmpty() ? list2 : finalList;

        System.out.println(list);

Converting second list to HashMap can reduce the lookup to O(1) time complexity and hence overall time complexity will be list1.size() + list2.size().

        Map<String, String> map = list2.stream()
                .collect(Collectors.toMap(Function.identity(), Function.identity()));

        list1.stream()
                .filter( x -> list2.isEmpty() || map.containsKey(x))
                .collect(Collectors.toList());
Related