La Stream API, introducida en Java 8, representa una evolución significativa en la forma en que procesamos colecciones de datos. En lugar de iteraciones imperativas manuales, los Streams permiten un enfoque declarativo y funcional para manipular conjuntos de datos de manera eficietne.
Fundamentos de Stream API
Un Stream no es una estructura de datos que almacena elementos, sino un conducto que transfiere datos desde una fuente (como una lista o un arreglo) a través de un pipeline de operaciones. El siguiente ejemplo ilustra cómo filtrar y transformar una lista de nombres de forma concisa:
import java.util.*;
import java.util.stream.Collectors;
public class AnalisisNombres {
public static void main(String[] args) {
List<String> empleados = List.of("Roberto", "Ana", "Ricardo", "Beatriz", "Raúl");
// Filtrar nombres que inician con 'R' y tienen más de 4 caracteres
List<String> filtrados = empleados.stream()
.filter(nombre -> nombre.startsWith("R"))
.filter(nombre -> nombre.length() > 4)
.collect(Collectors.toList());
System.out.println("Resultados: " + filtrados);
}
}
Generación de Streams
Dependiendo de la fuente de datos, existen diversas formas de inicializar un flujo:
- Colecciones: Cualquier objeto que implemente
Collectionpuede invocar.stream(). - Arreglos: Se utiliza el método estático
Arrays.stream(miArreglo). - Datos Dispersos: Mediante
Stream.of(val1, val2, ...). - Mapas: Requieren una conversión intermedia a través de
keySet(),values()oentrySet().
public class CreacionStreams {
public static void main(String[] args) {
// Desde arreglo
Integer[] puntajes = {10, 20, 30, 40};
Arrays.stream(puntajes).forEach(p -> System.out.print(p + " "));
// Desde Map
Map<Integer, String> usuarios = Map.of(1, "Admin", 2, "Editor");
usuarios.entrySet().stream()
.forEach(entry -> System.out.println("ID: " + entry.getKey() + " Rol: " + entry.getValue()));
}
}
Operaciones Intermedias
Estas operaciones transforman un Stream en otro, permiteindo el encadenamiento de métodos:
filter: Descarta elementos basados en un predicado.sorted: Ordena los elementos (natural o mediante comparador).limityskip: Controlan el tamaño del flujo y el punto de inicio.map: Transforma cada elemento aplicando una función.distinct: Elimina duplicados basándose enequals().
public class OperacionesFiltro {
public static void main(String[] args) {
List<Integer> valores = List.of(85, 40, 75, 90, 60, 100, 55);
valores.stream()
.filter(v -> v >= 60) // Solo aprobados
.sorted(Comparator.reverseOrder()) // De mayor a menor
.skip(1) // Saltar el primero
.limit(3) // Tomar los 3 siguientes
.map(v -> v + 5) // Bonus de puntos
.forEach(System.out::println);
}
}
Operaciones Terminales y Recolección
Una operación terminal cierra el Stream y produce un resultado o un efecto colateral. Ejemplos comunes son count(), forEach(), max(), min() y collect().
public class GestionInventario {
public static void main(String[] args) {
List<Producto> stock = List.of(
new Producto("Laptop", 1200.0),
new Producto("Mouse", 25.0),
new Producto("Teclado", 45.0)
);
// Obtener el producto más caro
Optional<Producto> caro = stock.stream()
.max(Comparator.comparingDouble(Producto::getPrecio));
caro.ifPresent(p -> System.out.println("Más costoso: " + p.getNombre()));
// Convertir a un Map: Nombre -> Precio
Map<String, Double> mapaPrecios = stock.stream()
.collect(Collectors.toMap(Producto::getNombre, Producto::getPrecio));
}
}
class Producto {
private String nombre;
private double precio;
public Producto(String nombre, double precio) {
this.nombre = nombre;
this.precio = precio;
}
public String getNombre() { return nombre; }
public double getPrecio() { return precio; }
}
Estructuras de Datos: Mapas
La interfaz Map<K, V> permite almacenar pares clave-valor. Es fundamental elegir la implementación correcta según la necesidad de rendimiento u ordenamiento:
- HashMap: Ofrece el mejor rendimiento (O(1) promedio), pero no garantiza orden.
- LinkedHashMap: Mantiane el orden de inserción de las claves.
- TreeMap: Mantiene las claves ordenadas según su orden natural o un comparador.
public class EjemploMapas {
public static void main(String[] args) {
Map<String, String> paises = new HashMap<>();
paises.put("ES", "España");
paises.put("MX", "México");
paises.put("AR", "Argentina");
// Recorrido eficiente con BiConsumer
paises.forEach((codigo, nombre) -> {
System.out.println("Código: " + codigo + ", País: " + nombre);
});
}
}
Parámetros Variables y la Clase Collections
Los Varargs (parámetros variables) permiten que un método acepte un número arbitrario de argumentos utilizando la sintaxis tipo... nombre. Internamente, el compilador los trata como un arreglo.
Por otro lado, la clase de utilidad java.util.Collections proporciona métodos estáticos para operar sobre colecciones, como desordenar (shuffle), ordenar (sort) o agregar múltiples elementos simultáneamente.
public class UtilidadesJava {
public static void main(String[] args) {
List<String> ciudades = new ArrayList<>();
// Uso de Collections.addAll con varargs
Collections.addAll(ciudades, "Madrid", "Bogotá", "Lima", "Quito");
// Desordenar la lista
Collections.shuffle(ciudades);
System.out.println("Aleatorio: " + ciudades);
// Ordenamiento personalizado
Collections.sort(ciudades, (a, b) -> a.length() - b.length());
System.out.println("Por longitud: " + ciudades);
}
// Método propio con varargs
public static int sumar(int... numeros) {
int total = 0;
for (int n : numeros) total += n;
return total;
}
}