Búsqueda en colecciones de Java
Busca elementos en colecciones de Java con contains, indexOf, binarySearch y búsquedas basadas en streams.
"¿Está este elemento en la colección?" parece una sola pregunta, pero Java la responde de media docena de formas distintas, con costes diferentes y tipos de retorno diferentes. Saber cuál usar puede convertir un bucle caliente de 50 milisegundos en uno de 50 microsegundos. Este capítulo es un recorrido por los métodos de búsqueda en colecciones, los mapas que las envuelven y los helpers estáticos de Collections.
El modelo mental orientado al coste
El coste de cada método de búsqueda lo determina la colección subyacente, no el lugar donde se llama. Elige la colección correcta desde el principio y tus búsquedas serán gratuitas; elige la equivocada y ninguna llamada ingeniosa te salvará.
| Colección | contains / consulta | Por qué |
|---|---|---|
HashSet, LinkedHashSet, HashMap.keySet() | O(1) esperado | Búsqueda por bucket hash |
TreeSet, TreeMap.keySet() | O(log n) | Árbol rojo-negro |
ArrayList, LinkedList, Vector | O(n) | Recorrido lineal |
ArrayList ordenado + Collections.binarySearch | O(log n) | Búsqueda binaria en lista indexada |
LinkedList + Collections.binarySearch | O(n) | La búsqueda binaria debe indexar — O(n) por paso |
Dos reglas prácticas:
- Si usas
containscon frecuencia, usa unSet. Construir unHashSeta partir de unaListy consultarlo casi siempre es más rápido quelist.containsen un bucle. - Si los datos están ordenados e indexados, usa
Collections.binarySearch. Vale la pena a partir de unos 30 elementos en la mayoría de JVMs.
Collection.contains(o)
Toda Collection lo tiene. La semántica se basa en igualdad:
boolean has = list.contains("alpha"); // uses .equalsPara una List, esto es un recorrido lineal — O(n). Para un HashSet, es una búsqueda por bucket hash — O(1) esperado. Para un TreeSet, es un recorrido de árbol de O(log n). La firma del método es la misma; el coste no lo es.
Se permite null (el método devuelve si la colección contiene algún elemento null), a menos que la colección rechace null directamente — TreeSet con ordenación natural, EnumSet, ConcurrentHashMap.keySet().
List.indexOf y lastIndexOf
Las listas soportan más que un simple "sí/no" — devuelven la posición:
int firstA = list.indexOf("alpha"); // -1 if absent
int lastA = list.lastIndexOf("alpha");Recorrido lineal desde el frente (o desde el final). Para un ArrayList<String> de mil elementos, esto está bien. Para un millón, construye un Map<String, Integer> una vez y consúltalo.
Map.containsKey, containsValue, get, getOrDefault
Los métodos de búsqueda específicos de los mapas se dividen claramente:
map.containsKey("alpha"); // O(1) for HashMap, O(log n) for TreeMap
map.get("alpha"); // returns the value or null
map.getOrDefault("alpha", 0); // returns the value or your default
map.containsValue("v"); // O(n) — scans every entrycontainsValue es la trampa. Recorre cada entrada cada vez. Si te encuentras llamándolo más de una vez, construye un mapa inverso (Map<V, K>) o un Set<V> de valores una vez y consúltalo.
getOrDefault es un pequeño pero importante cambio de patrón: reemplaza el antiguo idioma Integer n = map.get(k); if (n == null) n = 0; con una sola línea, y el valor por defecto solo se usa cuando la clave está ausente — no cuando el valor es null. (Para "ausente o null," usa Objects.requireNonNullElse(map.get(k), 0).)
Collections.binarySearch
Búsqueda binaria en una lista ordenada:
List<String> sorted = new ArrayList<>(...);
Collections.sort(sorted);
int hit = Collections.binarySearch(sorted, "delta"); // 2 (some index)
int miss = Collections.binarySearch(sorted, "zeta"); // negativeDos precondiciones:
- La lista debe estar ordenada en el orden que usará la búsqueda. Si la ordenaste con un comparador, pasa el mismo comparador a
binarySearch. Los órdenes incompatibles producen resultados sin sentido (silenciosamente — sin excepción). - La lista debería estar indexada (
ArrayList, noLinkedList). En una lista enlazada, la búsqueda binaria es O(n log n), peor que el recorrido lineal.
El valor de retorno codifica tanto "encontrado" como "dónde insertar":
int i = Collections.binarySearch(sorted, key);
if (i >= 0) {
// key is at index i
} else {
int insertAt = -i - 1;
sorted.add(insertAt, key); // keeps the list sorted
}Esa aritmética de -i - 1 es la forma en que toda rutina "buscar o insertar" en el JDK maneja un fallo. Vale la pena memorizarlo.
Collections.frequency y disjoint
Dos helpers que encapsulan patrones de búsqueda comunes:
int n = Collections.frequency(coll, "alpha"); // how many times "alpha" appears
boolean none = Collections.disjoint(a, b); // no element of a is in bfrequency es O(n). Para consultas repetidas con diferentes objetivos, cuenta una vez con un stream en un Map<T, Long> en su lugar.
disjoint está implementado de forma inteligente: itera la colección más pequeña y comprueba contains en la más grande si la más grande es un Set, intercambiando argumentos internamente. Así, Collections.disjoint(largeList, smallSet) es O(largeList) — y más rápido que implementarlo tú mismo.
Búsqueda basada en streams
Los streams manejan "encontrar el primer elemento que coincida" con findFirst / findAny, y "¿hay alguna coincidencia?" con anyMatch / allMatch / noneMatch:
Optional<Person> match = people.stream()
.filter(p -> p.age() >= 18 && p.name().startsWith("A"))
.findFirst();
boolean any = people.stream().anyMatch(p -> p.age() >= 65);
boolean all = people.stream().allMatch(p -> p.age() >= 0);
boolean non = people.stream().noneMatch(p -> p.age() < 0);Los streams realizan cortocircuito en findFirst y anyMatch — se detienen tan pronto como se encuentra una coincidencia. Son la respuesta más clara para la búsqueda basada en predicados. No son más rápidos que contains para búsqueda de igualdad en la estructura de datos correcta — un HashSet.contains siempre vencerá a stream().anyMatch(x -> x.equals(target)).
Optional<T> merece atención propia (tiene un capítulo en la parte de programación funcional). Por ahora: findFirst().isPresent() es la expresión más clara de "¿encontramos algo?" para un predicado.
LinkedHashSet para "contains y orden"
Un patrón común: necesitas contains rápido y iteración en orden de inserción. LinkedHashSet es la respuesta:
LinkedHashSet<String> seen = new LinkedHashSet<>();
for (String line : input) {
if (seen.add(line)) System.out.println(line); // print first occurrences only
}add devuelve true solo la primera vez. El conjunto rechaza duplicados en O(1) y preserva el orden de inserción para la iteración. Esa es la herramienta correcta para "deduplicar manteniendo el orden" — ni HashSet (pierde el orden) ni ArrayList (contains lento) es tan bueno.
Un ejemplo práctico: comparando cinco estrategias de búsqueda con los mismos datos
El programa siguiente inserta 100 000 strings en distintas colecciones y mide cinco estrategias de consulta para 1 000 coincidencias aleatorias: ArrayList.contains, HashSet.contains, TreeSet.contains, Collections.binarySearch en una lista ordenada y stream().anyMatch.
Qué extraer de la ejecución:
HashSet.containsyCollections.binarySearchen elArrayListordenado son dramáticamente más rápidos queArrayList.containspara consultas repetidas. La tabla hash gana para "cualquier igualdad," la búsqueda binaria gana cuando los datos deben mantenerse ordenados por otras razones también.TreeSet.containsva muy cerca, pero no es gratuito — cada consulta recorre un árbol de profundidad ~log₂(100 000) ≈ 17, con fallos de caché para los punteros del árbol.stream().anyMatchpara búsqueda de igualdad es la peor opción aquí: el mismo O(n) quelist.containspero con sobrecarga de asignación adicional por consulta. Úsalo para predicados, no para igualdad simple en una lista.- La llamada con "clave ausente" devolvió un valor negativo, y
-i - 1dio el índice donde se insertaría"zzz"para mantener la lista ordenada. Esa es la misma convención que usanTreeMap.subMapyArrays.binarySearch.
Qué viene a continuación
Ya has cubierto iteración, ordenación, clasificación y búsqueda — las cuatro operaciones mecánicas para las que existe el framework de colecciones. El capítulo final de esta parte es la historia moderna para lo único que ninguna de ellas tocó: la inmutabilidad. Java Unmodifiable Collections cubre List.of, Set.of, Map.of, y los wrappers Collections.unmodifiable* — cuándo cada uno es la elección correcta y por qué un patrón de "copia defensiva" que antes ocupaba cuatro líneas ahora cabe en una.