Articles

Filtrar una Colección Java por una Lista

Información general

Filtrar una colección por una Lista es un escenario lógico de negocio común. Hay muchas maneras de lograrlo. Sin embargo, algunos pueden llevar a soluciones de bajo rendimiento si no se hacen correctamente.

En este tutorial, compararemos algunas implementaciones de filtrado y analizaremos sus ventajas e inconvenientes.

Usando un bucle For-Each

Comenzaremos con la sintaxis más clásica, un bucle for-each.

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<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");}

Para nuestro ejemplo, filtraremos la primera lista de Empleados en función de la segunda lista con nombres de Empleados para encontrar solo los Empleados con esos nombres específicos.

Ahora, veamos el enfoque tradicional: recorrer ambas listas en busca de coincidencias:

@Testpublic 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().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 antes de tiempo 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, el filtro de nombres estará en el orden igual de grande, lo que nos da una clasificación O(n2).

Usando Flujos y Lista#contiene

Ahora refactorizaremos el método anterior usando lambdas para simplificar la sintaxis y mejorar la legibilidad. También usemos el método List#contains como filtro lambda:

@Testpublic 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()));}

Al usar la API de flujo, la legibilidad se ha mejorado mucho, pero nuestro código sigue siendo tan ineficiente como nuestro método anterior porque todavía itera a través del producto cartesiano internamente. Por lo tanto, tenemos la misma clasificación O(n2).

Usando secuencias con HashSet

Para mejorar el rendimiento, debemos usar el método HashSet # contains. Este método se diferencia de List#contains porque realiza una búsqueda de código hash, lo que nos da un número de operaciones en tiempo constante:

@Testpublic 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()));}

Mediante el uso de HashSet, nuestra eficiencia de código ha mejorado enormemente sin afectar la legibilidad. Dado que el HashSet # contiene ejecuciones en tiempo constante, hemos mejorado nuestra clasificación a O(n).

Conclusión

En este tutorial rápido, aprendimos a 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 tales entornos.

Todo el código presentado en este artículo está disponible en GitHub.

empezar con el Muelle 5 y el Resorte de Arranque 2, a través del Aprender de Primavera del curso:

>> COMPRUEBE EL CURSO