1. Información general
Filtrar una colección por una lista es un escenario de lógica empresarial común. Hay muchas formas de lograrlo. Sin embargo, algunos pueden dar lugar a soluciones de bajo rendimiento si no se hacen correctamente.
En este tutorial, compararemos algunas implementaciones de filtrado y discutiremos sus ventajas e inconvenientes .
2. Uso de un bucle para cada uno
Comenzaremos con la sintaxis más clásica, un bucle para cada uno.
Para este y todos los demás ejemplos de este artículo, usaremos la siguiente clase:
public class Employee { private Integer employeeNumber; private String name; private Integer departmentId; //Standard constructor, getters and setters. }
También usaremos los siguientes métodos para todos los ejemplos, por simplicidad:
private List 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 employeeNameFilter() { return Arrays.asList("Alice", "Mike", "Bob"); }
Para nuestro ejemplo, filtraremos la primera lista de empleados en función de la segunda lista con los nombres de los empleados para encontrar solo los empleados con esos nombres específicos.
Ahora, veamos el enfoque tradicional: recorrer ambas listas en busca de coincidencias:
@Test public void givenEmployeeList_andNameFilterList_thenObtainFilteredEmployeeList_usingForEachLoop() { List filteredList = new ArrayList(); List originalList = buildEmployeeList(); List nameFilter = employeeNameFilter(); for (Employee employee : originalList) { for (String name : nameFilter) { if (employee.getName().equals(name)) { filteredList.add(employee); // break; } } } assertThat(filteredList.size(), is(nameFilter.size())); }
Esta es una sintaxis simple, pero es bastante detallada y, en realidad, bastante ineficiente. En pocas palabras, itera a través del producto cartesiano de los dos conjuntos para obtener nuestra respuesta.
Incluso agregar un descanso para salir temprano seguirá iterando en el mismo orden que un producto cartesiano en el caso promedio.
Si llamamos al tamaño de la lista de empleados n, entonces nameFilter estará en el orden igual de grande, dándonos una clasificación O (n2) .
3. El uso de Streams y List # contiene
Ahora refactorizaremos el método anterior usando lambdas para simplificar la sintaxis y mejorar la legibilidad . Usemos también el método List # contains como filtro lambda :
@Test public void givenEmployeeList_andNameFilterList_thenObtainFilteredEmployeeList_usingLambda() { List filteredList; List originalList = buildEmployeeList(); List nameFilter = employeeNameFilter(); filteredList = originalList.stream() .filter(employee -> nameFilter.contains(employee.getName())) .collect(Collectors.toList()); assertThat(filteredList.size(), is(nameFilter.size())); }
Al usar la API Stream , la legibilidad se ha mejorado enormemente, pero nuestro código sigue siendo tan ineficiente como nuestro método anterior porque todavía está iterando a través del producto cartesiano internamente . Por tanto, tenemos la misma clasificación O (n2) .
4. Uso de Streams con HashSet
Para mejorar el rendimiento, debemos usar el método HashSet # contains . Este método difiere de List # contains porque realiza una búsqueda de código hash , lo que nos da un número de operaciones en tiempo constante:
@Test public void givenEmployeeList_andNameFilterList_thenObtainFilteredEmployeeList_usingLambdaAndHashSet() { List filteredList; List originalList = buildEmployeeList(); Set nameFilterSet = employeeNameFilter().stream().collect(Collectors.toSet()); filteredList = originalList.stream() .filter(employee -> nameFilterSet.contains(employee.getName())) .collect(Collectors.toList()); assertThat(filteredList.size(), is(nameFilterSet.size())); }
Al usar HashSet, la eficiencia de nuestro código ha mejorado enormemente sin afectar la legibilidad. Dado que HashSet # contiene ejecuciones en tiempo constante, hemos mejorado nuestra clasificación a O (n).
5. Conclusión
En este tutorial rápido, aprendimos cómo filtrar una colección por una lista de valores y los inconvenientes de usar lo que puede parecer el método más sencillo.
Siempre debemos considerar la eficiencia porque nuestro código podría terminar ejecutándose en grandes conjuntos de datos y los problemas de rendimiento podrían tener consecuencias catastróficas en dichos entornos.
Todo el código presentado en este artículo está disponible en GitHub.