Creación de streams en Java
Crea streams en Java desde colecciones, arrays, Stream.of, Stream.iterate, Stream.generate y fuentes de I/O.
El capítulo introductorio mostró la forma del pipeline — fuente → intermedias → terminal — y trató la fuente como algo dado. Este capítulo es el catálogo de fuentes. Todo pipeline de stream que escribas comienza con una de ellas, y cada una tiene un pequeño conjunto de características que determinan si el pipeline resultante es correcto, perezoso, finito, ordenado o paralelizable.
La lista es más corta de lo que parece. Casi todos los streams que escribirás comienzan con coll.stream(), Stream.of(...), Arrays.stream(arr) o un IntStream.range. El resto de este capítulo es "y aquí están las pocas situaciones donde las otras opciones son la elección correcta."
Desde una Collection — coll.stream()
La fuente dominante. Collection<T> tiene un método stream() por defecto, por lo que cada List, Set, Queue y Deque lo expone de forma gratuita:
List<String> names = List.of("Alice", "Bob", "Carol");
long count = names.stream().filter(n -> n.length() > 3).count();El stream es secuencial, de tamaño conocido (la JVM sabe el número de elementos de antemano) y ordenado si la colección lo es. Un List produce un stream ordenado; un HashSet produce uno sin orden; un TreeSet produce uno ordenado según el comparador del conjunto.
También existe coll.parallelStream(), que planifica las tareas en el ForkJoinPool común. Misma fuente, diferente política de ejecución — se cubre en Java Parallel Streams.
Desde elementos explícitos — Stream.of(...)
Usa Stream.of cuando tengas una lista corta y conocida de elementos y no quieras crear un List desechable:
Stream<String> s = Stream.of("a", "b", "c");
Stream<Integer> n = Stream.of(1, 2, 3, 4, 5);
Stream<Object> one = Stream.of("just one");Es un método varargs, por lo que acepta cualquier número de argumentos (cero está permitido y produce un stream vacío). Con un único argumento T[], el compilador elige Stream.of(T...) en lugar de Stream.of(T) — útil cuando ya tienes un array:
String[] arr = {"x", "y", "z"};
Stream<String> fromArr = Stream.of(arr); // same as Arrays.stream(arr)Desde un array — Arrays.stream(...)
Arrays.stream tiene sobrecargas para T[], int[], long[], double[], además de variantes con rango:
int[] xs = {3, 1, 4, 1, 5, 9, 2, 6};
IntStream ix = Arrays.stream(xs); // primitive specialisation
IntStream tail = Arrays.stream(xs, 2, xs.length); // half-open [2, len)
String[] words = {"alpha", "beta", "gamma"};
Stream<String> ws = Arrays.stream(words);Las sobrecargas primitivas devuelven IntStream, LongStream, DoubleStream — no Stream<Integer>. Esto importa: los streams primitivos evitan el boxing, tienen sum, average, min, max directamente (sin collector) y funcionan bien con mapToInt/mapToObj para moverse entre mundos.
Rangos primitivos — IntStream.range / rangeClosed
La forma más rápida de iterar por índice sin un bucle for:
// 0, 1, 2, ..., 9
IntStream.range(0, 10).forEach(i -> System.out.println(i));
// 1..10 inclusive
int sum = IntStream.rangeClosed(1, 10).sum(); // 55range(a, b) es semiabierto [a, b). rangeClosed(a, b) es [a, b]. Ambos son acotados, ordenados, de tamaño conocido y más rápidos que Stream.iterate(0, i -> i + 1).limit(n) porque la JVM conoce el número de elementos de antemano. Úsalos siempre que el cuerpo de un bucle sea "hacer algo en el índice i."
Para asociar un índice a los elementos de un List se escribe:
List<String> names = List.of("Alice", "Bob", "Carol");
IntStream.range(0, names.size())
.mapToObj(i -> i + ": " + names.get(i))
.forEach(System.out::println);Streams infinitos generados — Stream.iterate y Stream.generate
Dos formas de producir un stream ilimitado. Se parecen; no son lo mismo.
Stream.iterate(seed, f) — comienza con seed, luego f(seed), luego f(f(seed)), …. Ordenado, determinista, secuencial. Casi siempre seguido de un cortocircuito:
Stream.iterate(1, n -> n * 2)
.limit(10)
.forEach(System.out::println); // 1, 2, 4, 8, ..., 512También existe una sobrecarga de 3 argumentos Stream.iterate(seed, hasNext, next) (Java 9+) que incorpora la condición de parada en la fuente — no se necesita limit:
Stream.iterate(1, n -> n < 1000, n -> n * 2).forEach(System.out::println);Stream.generate(supplier) — llama a un Supplier<T> repetidamente. Sin orden, sin relación entre elementos:
Stream.generate(Math::random).limit(5).forEach(System.out::println);
Stream.generate(() -> "ping").limit(3).forEach(System.out::println);Usa iterate para secuencias donde cada término depende del anterior (n -> n + 1, n -> n * 2, el par de Fibonacci arr -> {arr[1], arr[0] + arr[1]}). Usa generate para valores independientes de una fuente auxiliar — números aleatorios, constantes fijas, UUIDs.
En cualquier caso, siempre termínalos con una operación de cortocircuito: limit(n), el iterate de 3 argumentos, o un terminal como findFirst / anyMatch. Un toList() simple sobre un stream infinito bloquea la JVM.
Desde I/O — Files.lines, BufferedReader.lines
Files.lines(path) abre un archivo y devuelve un Stream<String> con sus líneas. Perezoso: las líneas se leen conforme el pipeline las necesita, no de antemano:
try (Stream<String> lines = Files.lines(Path.of("words.txt"))) {
long longWords = lines.filter(w -> w.length() > 8).count();
}El try-with-resources es obligatorio. El stream mantiene abierto un descriptor de archivo, y la única forma de liberarlo es llamar a close() — lo que try-with-resources hace por ti. Sin ello, el descriptor se filtra hasta que el stream sea recolectado por el garbage collector, lo que puede no ocurrir nunca bajo carga.
Misma forma para Readers mediante BufferedReader.lines(). Ambos son la manera canónica de recorrer un archivo de texto sin cargarlo en memoria.
String.chars() y String.codePoints()
Un String es una secuencia de unidades de código UTF-16; la API expone ambas vistas:
"hello".chars() // IntStream of UTF-16 code units
.filter(Character::isUpperCase)
.count();
"héllo".codePoints() // IntStream of Unicode code points
.mapToObj(Character::toString)
.forEach(System.out::println);Ambos devuelven IntStream. chars() está bien para ASCII; para cualquier cosa que pueda contener pares sustitutos (la mayoría de los emoji, muchos scripts), codePoints() es la opción segura.
Streams vacíos y de un solo elemento
Para casos por defecto y ramas de flatMap:
Stream<String> none = Stream.empty(); // 0 elements
Stream<String> one = Stream.of("x"); // exactly 1
Stream<String> opt = Optional.of("x").stream(); // 1 if present, else emptyOptional.stream() (Java 9+) es el puente entre Optional<T> y Stream<T> — útil cuando aplicas flatMap a un stream de Optionals para obtener un stream de valores presentes sin ningún manejo de null.
Stream.Builder — insertando elementos uno a uno
Cuando no puedes expresar la fuente como un literal, un array o un generador — normalmente porque los elementos provienen de distintas ramas de código imperativo — existe un builder:
Stream.Builder<String> b = Stream.builder();
b.add("first");
if (someCondition) b.add("second");
b.accept("third");
Stream<String> s = b.build();Tras build(), el builder queda sellado; un add posterior lanza una excepción. Es una herramienta poco frecuente pero legítima. La mayor parte del código que recurre a ella se escribe mejor con un ArrayList<String> seguido de list.stream(), pero el builder evita ese intermedio cuando los datos se construyen de forma fragmentada.
Streams de Map — no existe uno
Map<K, V> no tiene un método stream(). En su lugar, debes hacer stream de sus vistas:
Map<String, Integer> ages = Map.of("Alice", 30, "Bob", 25);
ages.entrySet().stream().filter(e -> e.getValue() >= 18).map(Map.Entry::getKey).toList();
ages.keySet().stream().sorted().toList();
ages.values().stream().mapToInt(Integer::intValue).sum();entrySet().stream() es lo que necesitarás la mayoría de las veces — ambas mitades de cada entrada están disponibles, y Map.Entry::getKey / ::getValue funcionan como referencias a métodos.
Elegir la fuente correcta
| Situación | Usar |
|---|---|
Ya tienes un List, Set, Queue | coll.stream() |
| Tienes unos pocos elementos fijos | Stream.of(a, b, c) |
Tienes un T[] | Arrays.stream(arr) |
Tienes int[], long[], double[] | Arrays.stream(arr) → stream primitivo |
| Quieres iterar por índice | IntStream.range(0, n) |
| Quieres cada término a partir del anterior | Stream.iterate |
| Quieres muestras independientes | Stream.generate |
| Quieres las líneas de un archivo de texto | Files.lines(path) dentro de try-with-resources |
Quieres los caracteres de un String | "...".chars() o .codePoints() |
| Quieres un stream vacío por defecto | Stream.empty() |
| Estás construyendo datos de forma fragmentada | Stream.builder() |
Quieres hacer stream de un Map | map.entrySet().stream() |
Esa tabla cubre todo lo del capítulo, y probablemente el 99% del código real.
Un ejemplo completo: diez fuentes, un programa
El programa a continuación construye un stream desde cada una de las fuentes principales, ejecuta una pequeña operación terminal sobre él para que la salida sea visible, e imprime tanto el resultado como el tipo de fuente utilizada.
Lo que se puede aprender de la ejecución:
- Cada línea de la salida provino de una fuente diferente, pero todas fluyen hacia el mismo vocabulario de intermedias/terminal. La elección de la fuente determina con qué puede comenzar el pipeline; no cambia lo que viene después.
Arrays.stream(int[])produjo unIntStream—sum()está directamente en el stream, sin boxing, sinCollectors.summingInt. Las especializaciones primitivas importan en pipelines numéricos.- Las dos llamadas a
Stream.iteratemuestran la diferencia entreiterate(seed, f)+limit(n)(tú eliges el número) y eliterate(seed, hasNext, next)de 3 argumentos (la fuente elige el número). Ambas son acotadas; uniteratesin límite y sin un terminal de cortocircuito es el clásico bug que bloquea la JVM para siempre. Stream.empty()yOptional.of(...).stream()son la manera en que los streams vacíos y de un solo elemento entran en un pipeline — típicamente dentro de una rama deflatMapdonde algunas entradas producen cero o un elemento descendiente.Stream.builder()es la salida de emergencia para el caso (poco frecuente) en que la fuente se construye imperativamente a través de ramas. La mayor parte del código real recurre primero acoll.stream().
Qué sigue
Ahora puedes construir cualquier stream que necesites desde cualquier fuente que tengas disponible. Los dos capítulos siguientes cubren las operaciones que se ejecutan entre la fuente y el resultado. Primero, Java Stream Intermediate Operations — filter, map, flatMap, distinct, sorted, peek, limit, skip — las transformaciones perezosas que dan forma al stream sin ejecutarlo. Luego los terminales que producen el valor.