1. Overview
Filtering a Collection by a List is a common business logic scenario. There are plenty of ways to achieve this. However, some may lead to under-performing solutions if not done properly.
In this tutorial, we’ll compare some filtering implementations and discuss their advantages and drawbacks.
2. Using a For-Each Loop
We’ll begin with the most classic syntax, a for-each loop.
For this and all other examples in this article, we’ll use the following class:
public class Employee { private Integer employeeNumber; private String name; private Integer departmentId; //Standard constructor, getters and setters. }
We’ll also use the following methods for all examples, for simplicity’s sake:
private List<Employee> buildEmployeeList() { return Arrays.asList( new Employee(1, "Mike", 1), new Employee(2, "John", 1), new Employee(3, "Mary", 1), new Employee(4, "Joe", 2), new Employee(5, "Nicole", 2), new Employee(6, "Alice", 2), new Employee(7, "Bob", 3), new Employee(8, "Scarlett", 3)); } private List<String> employeeNameFilter() { return Arrays.asList("Alice", "Mike", "Bob"); }
For our example, we’ll filter the first list of Employees based on the second list with Employee names to find only the Employees with those specific names.
Now, let’s see the traditional approach – looping through both lists looking for matches:
@Test public void givenEmployeeList_andNameFilterList_thenObtainFilteredEmployeeList_usingForEachLoop() { List<Employee> filteredList = new ArrayList<>(); List<Employee> originalList = buildEmployeeList(); List<String> nameFilter = employeeNameFilter(); for (Employee employee : originalList) { for (String name : nameFilter) { if (employee.getName().equalsIgnoreCase(name)) { filteredList.add(employee); } } } assertThat(filteredList.size(), is(nameFilter.size())); }
This is a simple syntax, but it’s quite verbose and not very efficient. Nested looping increases execution time and resource usage, because it is effectively performing a Cartesian Product of the two sets.
3. Using Lambda and List.contains()
We’ll now refactor the previous method by using Lambdas to simplify syntax and improve readability. Let’s also use the List.contains() method as the Lambda filter:
@Test public void givenEmployeeList_andNameFilterList_thenObtainFilteredEmployeeList_usingLambda() { List<Employee> filteredList; List<Employee> originalList = buildEmployeeList(); List<String> nameFilter = employeeNameFilter(); filteredList = originalList.stream() .filter(employee -> nameFilter.contains(employee.getName())) .collect(Collectors.toList()); assertThat(filteredList.size(), is(nameFilter.size())); }
By using the Stream API, readability has been greatly improved, but our code remains as inefficient as our previous method because it’s still performing the Cartesian Product internally.
4. Using Lambda with HashSet
To improve performance, we must use the HashSet.contains() method. This method differs from the List.contains() one because it computes the hash code and traverses to it if there’s a match, instead of checking every list member as in the previous examples:
@Test public void givenEmployeeList_andNameFilterList_thenObtainFilteredEmployeeList_usingLambdaAndHashSet() { List<Employee> filteredList; List<Employee> originalList = buildEmployeeList(); Set<String> nameFilterSet = employeeNameFilter().stream().collect(Collectors.toSet()); filteredList = originalList.stream() .filter(employee -> nameFilterSet.contains(employee.getName())) .collect(Collectors.toList()); assertThat(filteredList.size(), is(nameFilterSet.size())); }
By using HashSet, our code efficiency has vastly improved while not affecting readability.
5. Conclusion
In this quick tutorial, we learned how to filter a Collection by a List of values and the drawbacks of using what may seem like the most straightforward method.
We must always consider efficiency because our code might end up running in huge data sets, and performance issues could have catastrophic consequences in such environments.
All code presented in this article is available over on GitHub.