1. Introducción
En la biblioteca estándar de Java hay muchas colecciones, pero cuando se trata de rangos, de buscar «elementos más cercanos», de priorización y de trabajar con franjas horarias, entran en escena los interfaces NavigableSet y NavigableMap.
NavigableSet amplía SortedSet, añadiendo métodos para buscar elementos relativos a un valor dado (menor, mayor, el más cercano, etc.), así como para trabajar con rangos.
NavigableMap amplía SortedMap, añadiendo métodos similares para la búsqueda por claves y el trabajo con rangos.
Las implementaciones más conocidas: TreeSet y TreeMap.
¿Cuándo se necesitan NavigableSet/NavigableMap?
- Cuando importa no solo almacenar elementos/claves únicos en orden, sino también encontrar rápidamente «vecinos» (por ejemplo, el hueco libre más próximo, la tarea prioritaria, un rango de valores).
- Cuando hay que trabajar con rangos: «todos los elementos entre X e Y», «todas las claves mayores/menores que una dada», «encontrar la clave más cercana».
2. Rangos: subSet, headSet, tailSet
Métodos para trabajar con rangos
NavigableSet y NavigableMap permiten obtener representaciones «vivas» (vistas, view) de subconjuntos de la colección:
- subSet(fromElement, fromInclusive, toElement, toInclusive) — elementos en el rango [fromElement; toElement], con inclusión/exclusión.
- headSet(toElement, inclusive) — todos los elementos menores (o menores o iguales) que toElement.
- tailSet(fromElement, inclusive) — todos los elementos mayores (o mayores o iguales) que fromElement.
Ejemplo:
NavigableSet<Integer> set = new TreeSet<>(List.of(10, 20, 30, 40, 50));
// Rango de 15 (incluido) a 45 (excluido)
NavigableSet<Integer> sub = set.subSet(15, true, 45, false); // [20, 30, 40]
System.out.println(sub); // [20, 30, 40]
// Todos los elementos <= 30
NavigableSet<Integer> head = set.headSet(30, true); // [10, 20, 30]
// Todos los elementos > 20
NavigableSet<Integer> tail = set.tailSet(20, false); // [30, 40, 50]
Inclusión/exclusión:
- true — inclusivo (el elemento forma parte del rango)
- false — exclusivo (el elemento no forma parte)
Para NavigableMap los métodos son análogos, solo que operan sobre las claves.
3. Operaciones con valores cercanos: floor, ceiling, lower, higher; pollFirst/Last
Búsqueda de elementos más cercanos
NavigableSet:
- lower(E e) — el mayor elemento < e (estrictamente menor)
- floor(E e) — el mayor elemento ≤ e (menor o igual)
- ceiling(E e) — el menor elemento ≥ e (mayor o igual)
- higher(E e) — el menor elemento > e (estrictamente mayor)
Ejemplo:
NavigableSet<Integer> set = new TreeSet<>(List.of(10, 20, 30, 40, 50));
System.out.println(set.lower(30)); // 20
System.out.println(set.floor(30)); // 30
System.out.println(set.ceiling(25)); // 30
System.out.println(set.higher(40)); // 50
pollFirst() / pollLast()
- pollFirst() — elimina y devuelve el elemento más pequeño (o null si está vacío)
- pollLast() — elimina y devuelve el elemento más grande
Ejemplo:
System.out.println(set.pollFirst()); // 10
System.out.println(set.pollLast()); // 50
System.out.println(set); // [20, 30, 40]
NavigableMap: métodos análogos para claves — lowerKey, floorKey, ceilingKey, higherKey, así como pollFirstEntry, pollLastEntry.
4. Vistas: descendingSet/descendingMap; vistas (view) y su «vivacidad»
Orden inverso: descendingSet/descendingMap
- descendingSet() — devuelve una vista del conjunto en orden inverso.
- descendingMap() — devuelve una vista del mapa en orden inverso de claves.
Ejemplo:
NavigableSet<Integer> set = new TreeSet<>(List.of(10, 20, 30, 40, 50));
NavigableSet<Integer> desc = set.descendingSet();
System.out.println(desc); // [50, 40, 30, 20, 10]
Importante: no es una copia, sino una vista «viva» (view) sobre el mismo conjunto de datos. Los cambios en una se reflejan en la otra.
Vistas (view) y su «vivacidad»
- Los métodos subSet, headSet, tailSet, descendingSet, descendingMap devuelven una vista (view): una representación «viva» de la colección original.
- Cualquier cambio en la vista se refleja en la colección original y viceversa.
- Si eliminas un elemento desde subSet, desaparecerá también del conjunto original.
Ejemplo:
NavigableSet<Integer> set = new TreeSet<>(List.of(10, 20, 30, 40, 50));
NavigableSet<Integer> sub = set.subSet(20, true, 40, true); // [20, 30, 40]
sub.remove(30);
System.out.println(set); // [10, 20, 40, 50]
Cuidado: si modificas la colección original de forma que un elemento «caiga fuera» del rango de la vista, en el siguiente acceso a la vista obtendrás ConcurrentModificationException.
5. Casos de uso de NavigableSet/NavigableMap
1. Franjas horarias/agenda
Objetivo: encontrar la franja horaria libre más próxima para una reunión.
NavigableSet<LocalTime> slots = new TreeSet<>(List.of(
LocalTime.of(9, 0),
LocalTime.of(10, 0),
LocalTime.of(11, 0),
LocalTime.of(14, 0)
));
LocalTime requested = LocalTime.of(10, 30);
LocalTime nextSlot = slots.ceiling(requested); // 11:00
System.out.println("Hora más próxima: " + nextSlot);
2. Priorización (cola con prioridad)
Objetivo: obtener siempre rápidamente el elemento con la prioridad más alta/más baja.
NavigableSet<Task> tasks = new TreeSet<>(Comparator.comparingInt(Task::priority));
tasks.add(new Task("Correo", 2));
tasks.add(new Task("Llamada", 1));
tasks.add(new Task("Informe", 3));
Task next = tasks.pollFirst(); // Tarea con la prioridad más alta (1)
3. «La clave más cercana» (por ejemplo, para buscar un rango en Map)
Objetivo: encontrar el rango de valores por clave o la clave más cercana.
NavigableMap<Integer, String> grades = new TreeMap<>();
grades.put(50, "Insuficiente");
grades.put(65, "Satisfactorio");
grades.put(75, "Bien");
grades.put(85, "Excelente");
int score = 78;
int key = grades.floorKey(score); // 75
System.out.println("Calificación: " + grades.get(key)); // Bien
6. Práctica: miniejemplos
Ejemplo 1: Rango de fechas
NavigableSet<LocalDate> holidays = new TreeSet<>(List.of(
LocalDate.of(2024, 1, 1),
LocalDate.of(2024, 5, 1),
LocalDate.of(2024, 12, 31)
));
LocalDate from = LocalDate.of(2024, 1, 1);
LocalDate to = LocalDate.of(2024, 6, 1);
NavigableSet<LocalDate> spring = holidays.subSet(from, true, to, false);
System.out.println(spring); // [2024-01-01, 2024-05-01]
Ejemplo 2: Búsqueda del valor más cercano
NavigableSet<Integer> numbers = new TreeSet<>(List.of(10, 20, 30, 40, 50));
int x = 25;
System.out.println("Menor: " + numbers.lower(x)); // 20
System.out.println("Menor o igual: " + numbers.floor(x)); // 20
System.out.println("Mayor o igual: " + numbers.ceiling(x)); // 30
System.out.println("Mayor: " + numbers.higher(x)); // 30
7. Errores típicos y matices
Error n.º 1: confundir inclusión/exclusión en subSet/headSet/tailSet.
Fíjate siempre en los parámetros inclusive: por defecto, en versiones antiguas de Java los rangos eran exclusivos; en NavigableSet/NavigableMap se puede indicar explícitamente inclusión/exclusión.
Error n.º 2: esperar que una vista sea una copia.
Una vista es una representación «viva», no una copia. Los cambios en la vista se reflejan en la colección original.
Error n.º 3: ConcurrentModificationException al modificar la colección original fuera de la vista.
Si modificas la colección original de forma que un elemento «caiga fuera» del rango de la vista, en el siguiente acceso a la vista obtendrás una excepción.
Error n.º 4: usar NavigableSet/NavigableMap sin Comparable o Comparator.
Estas colecciones requieren que los elementos (o claves) sean comparables (Comparable) o que se proporcione un Comparator. En caso contrario obtendrás ClassCastException.
Error n.º 5: esperar que pollFirst/pollLast no modifiquen la colección.
¡Estos métodos eliminan elementos! Si solo necesitas consultar, usa first()/last().
GO TO FULL VERSION