Skip to content

Streams en Java

Aquesta secció explica els Streams de Java fora del context del curs, amb exemples independents. Una vegada dominats els Streams, la seva aplicació als serveis i controladors del projecte resultarà molt natural.


1. Què és un Stream?

Un Stream és una seqüència d'elements que es pot processar de forma declarativa mitjançant una cadena d'operacions. Va ser introduït a Java 8 el 2014 i ha canviat completament la manera d'escriure codi de processament de col·leccions. En lloc de dir-li a Java COM fer-ho (estil imperatiu), li dius QUÈ vols obtenir (estil declaratiu). Canvia la manera de programar, evitant els bucles als recorreguts de col·leccions.

Analogia: Imagina una cadena de muntatge en una fàbrica. Les peces (elements de la col·lecció) entren per un extrem, passen per diverses estacions de treball (operacions a efectuar sobre elles), i surten transformades per l'altre extrem.

Agafem com a cas base molt simplee el convertir un llistat de noms a majúscules, però només per aquells noms que tinguin més de 4 lletres, i després ordenar-los alfabèticament.

Java
// Llista d'empleats
List<String> noms = Arrays.asList("Pau", "Marta", "Jordi", "Laia", "Marc");

List<String> resultado1 = new ArrayList<>();    // per al resultat
for (String nom : noms) {
    if (nom.length() > 4) {                 // Filtra noms llargs
        resultado1.add(nom.toUpperCase());  // Converteix a majúscules
    }
}
Collections.sort(resultado1);         // Ordena
Java
1
2
3
4
5
6
7
8
// Llista d'empleats
List<String> noms = Arrays.asList("Pau", "Marta", "Jordi", "Laia", "Marc");

List<String> resultado2 = noms.stream()
    .filter(nom -> nom.length() > 4)  // Filtra noms llargs
    .map(String::toUpperCase)          // Converteix a majúscules
    .sorted()                          // Ordena
    .collect(Collectors.toList());     // Recull el resultat

Als dos cassos la col·lecció final és la mateixa: ["MARTA", "JORDI"]. Però el codi amb Stream és més clar, més concís i més fàcil de mantenir. No cal preocupar-se per crear llista de resultat, ni per afegir elements, ni per ordenar al final. Simplement encadenem les operacions que volem aplicar a la col·lecció original i recollim el resultat al final.

2. Anatomia d'un Stream

Tots els Streams segueixen l'estructura: font → operacions intermèdies → operació terminal

Text Only
┌─────────────┐    ┌──────────────────────────────────┐    ┌────────────┐
│    FONT     │ →  │    OPERACIONS INTERMÈDIES        │ →  │  TERMINAL  │
│  (source)   │    │  (retornen un nou Stream)        │    │  (resultat)│
├─────────────┤    ├──────────────────────────────────┤    ├────────────┤
│ llista      │    │ filter()   → filtra elements     │    │ collect()  │
│ array       │    │ map()      → transforma elements │    │ toList()   │
│ Set         │    │ sorted()   → ordena              │    │ forEach()  │
│ Map.values()│    │ distinct() → elimina duplicats   │    │ count()    │
│ Stream.of() │    │ limit()    → limita quantitat    │    │ findFirst()│
│ ...         │    │ peek()     → per depurar         │    │ anyMatch() │
└─────────────┘    └──────────────────────────────────┘    └────────────┘

Important: Les operacions intermèdies són lazy (mandroses). No s'executen fins que arriba l'operació terminal. Això permet optimitzacions internes de Java.

3. Crear un Stream

Java
// Des d'una List
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream1 = numeros.stream();

// Des d'un Array
String[] paraules = {"hola", "mon"};
Stream<String> stream2 = Arrays.stream(paraules);

// Des de valors literals
Stream<String> stream3 = Stream.of("a", "b", "c");

// Stream d'enters primitius (IntStream)
IntStream stream4 = IntStream.range(1, 11);  // 1 fins 10

// Stream infinit (cal limitar-lo!)
Stream<Integer> infinit = Stream.iterate(0, n -> n + 2);
// .limit(5) → [0, 2, 4, 6, 8]

4. Operació: filter()

filter() rep un predicat (condició booleana) i manté només els elements que el compleixen. El predicat és una funció lambda: definirem una variable que representa l'element actual del Stream, una fletxa i una expressió que avalua si passa el filtre o no.

Java
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Filtra els nombres parells
List<Integer> parells = numeros.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());
// Resultat: [2, 4, 6, 8, 10]

// Filtra strings que comencen per "A"
List<String> noms = Arrays.asList("Anna", "Biel", "Alba", "Carles", "Ares");
List<String> ambA = noms.stream()
    .filter(nom -> nom.startsWith("A"))
    .collect(Collectors.toList());
// Resultat: ["Anna", "Alba", "Ares"]

// Pots encadenar múltiples filter()
List<Integer> resultat = numeros.stream()
    .filter(n -> n > 3)         // Majors que 3: [4,5,6,7,8,9,10]
    .filter(n -> n % 2 == 0)    // Parells: [4, 6, 8, 10]
    .collect(Collectors.toList());

5. Operació: map()

map() transforma cada element del Stream en un altre element (pot ser d'un tipus diferent). De nou és una funció lambda que rep un element i aplica una modificació per obtenir el nou element. És la operació més important per a transformar dades.

Java
// Exemple 1: Transforma integers en strings
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5);
List<String> textos = numeros.stream()
    .map(n -> "Número: " + n)
    .collect(Collectors.toList());
// Resultat: ["Número: 1", "Número: 2", ...]

// Exemple 2: Extreu un camp d'un objecte
record Persona(String nom, int edat) {}

List<Persona> persones = Arrays.asList(
    new Persona("Anna", 30),
    new Persona("Biel", 25),
    new Persona("Carla", 35)
);

List<String> noms = persones.stream()
    .map(Persona::nom)          // Extreu el nom de cada Persona
    .collect(Collectors.toList());
// Resultat: ["Anna", "Biel", "Carla"]

List<Integer> edats = persones.stream()
    .map(p -> p.edat() * 2)     // Dobla l'edat de cada Persona
    .collect(Collectors.toList());
// Resultat: [60, 50, 70]

// Exemple 3: Canvia el tipus completament (String → Integer)
List<String> paraules = Arrays.asList("hola", "món", "java");
List<Integer> longituds = paraules.stream()
    .map(String::length)        // Equivalent a: s -> s.length()
    .collect(Collectors.toList());
// Resultat: [4, 3, 4]

6. Referència a mètode (::)

De vegades, voldrem fer servir un mètode dins del filter() o el map(). Aquest mètode pot ser static d'una classe, com per exemple String::length o be, si els streams són d'objectes, podem fer servir una referència a mètode d'instància p.getEdat()

Java
1
2
3
4
5
6
7
8
9
// Aquestes expressions son equivalents:
.map(s -> s.toUpperCase())    // Lambda explícita
.map(String::toUpperCase)     // Referència a mètode d'instància

.map(n -> Math.abs(n))        // Lambda explícita
.map(Math::abs)               // Referència a mètode estàtic

.map(s -> new StringBuilder(s))  // Lambda explícita
.map(StringBuilder::new)         // Referència a constructor

7. Operació: sorted()

Molt senzill, permet ordenar els elements del Stream. Per defecte, ordena segons l'ordre natural (per exemple, numèric o alfabètic). Però també pots passar-li un Comparator per definir un ordre personalitzat.

Java
List<Integer> numeros = Arrays.asList(5, 3, 8, 1, 9, 2);

// Ordre natural (ascendent)
List<Integer> ordenats = numeros.stream()
    .sorted()
    .collect(Collectors.toList());
// Resultat: [1, 2, 3, 5, 8, 9]

// Ordre invers
List<Integer> invers = numeros.stream()
    .sorted(Comparator.reverseOrder())
    .collect(Collectors.toList());
// Resultat: [9, 8, 5, 3, 2, 1]

// Ordenar per un camp d'un objecte
record Empleat(String nom, double salari) {}

List<Empleat> empleats = Arrays.asList(
    new Empleat("Pau", 55000),
    new Empleat("Marta", 70000),
    new Empleat("Jordi", 45000)
);

// Per salari ascendent
List<Empleat> perSalari = empleats.stream()
    .sorted(Comparator.comparingDouble(Empleat::salari))
    .collect(Collectors.toList());
// Resultat: [Jordi(45000), Pau(55000), Marta(70000)]

// Per salari descendent
List<Empleat> perSalariDesc = empleats.stream()
    .sorted(Comparator.comparingDouble(Empleat::salari).reversed())
    .collect(Collectors.toList());

8. Operació terminal: collect()

collect() recull tots els elements del Stream i els posa en una nova col·lecció, donant per concluït el processament. És l'operació més comuna per a obtenir un resultat final després de les operacions intermèdies. El paràmetre que rep és un Collector que defineix com es recullen els elements (per exemple, en una List, Set, Map, etc).

Java
List<String> noms = Arrays.asList("Anna", "Biel", "Anna", "Carla");

// A List (permet duplicats, manté ordre)
List<String> llista = noms.stream().collect(Collectors.toList());
// O amb Java 16+:
List<String> llista2 = noms.stream().toList();  // Immutable!

// A Set (elimina duplicats, no garanteix ordre)
Set<String> conjunt = noms.stream().collect(Collectors.toSet());
// Resultat: {"Anna", "Biel", "Carla"}  (Anna apareix 1 sola vegada)

// A Map (clau → valor)
Map<String, Integer> mapaNoms = noms.stream()
    .distinct()
    .collect(Collectors.toMap(
        nom -> nom,           // La clau és el nom
        String::length        // El valor és la longitud
    ));
// Resultat: {"Anna"=4, "Biel"=4, "Carla"=5}

// Agrupant per condició
Map<Boolean, List<String>> agrupat = noms.stream()
    .distinct()
    .collect(Collectors.partitioningBy(nom -> nom.length() > 4));
// Resultat: {false=["Anna","Biel"], true=["Carla"]}

9. Altres operacions terminals útils

Java
List<Integer> numeros = Arrays.asList(3, 7, 1, 9, 4, 6);

// count() - compta elements
long quants = numeros.stream()
    .filter(n -> n > 5)
    .count();
// Resultat: 3  (7, 9, 6)

// findFirst() - retorna el primer número major a 5 (Optional)
Optional<Integer> primer = numeros.stream()
    .filter(n -> n > 5)
    .findFirst();
primer.ifPresent(System.out::println);  // Imprimeix: 7

// anyMatch() - algun element compleix la condició?
boolean hiHaGrans = numeros.stream().anyMatch(n -> n > 8);
// Resultat: true  (9 > 8)

// allMatch() - tots els elements compleixen la condició?
boolean tOtsPositius = numeros.stream().allMatch(n -> n > 0);
// Resultat: true

// noneMatch() - cap element compleix la condició?
boolean capNegatiu = numeros.stream().noneMatch(n -> n < 0);
// Resultat: true

// min() i max()
Optional<Integer> maxim = numeros.stream().max(Comparator.naturalOrder());
// Resultat: Optional[9]

// sum(), average() (amb IntStream)
int suma = numeros.stream().mapToInt(Integer::intValue).sum();
// Resultat: 30

OptionalDouble mitja = numeros.stream().mapToDouble(Integer::doubleValue).average();
// Resultat: OptionalDouble[5.0]

// forEach() - executa una acció per a cada element (no retorna res)
numeros.stream()
    .filter(n -> n > 5)
    .forEach(n -> System.out.println("Gran: " + n));
// Imprimeix: Gran: 7, Gran: 9, Gran: 6

10. Cadenes complexes: combinant operacions

Java
record Estudiant(String nom, String carrera, double nota) {}

List<Estudiant> estudiants = Arrays.asList(
    new Estudiant("Anna",   "Informàtica", 8.5),
    new Estudiant("Biel",   "Matemàtiques", 7.2),
    new Estudiant("Carla",  "Informàtica", 9.1),
    new Estudiant("David",  "Informàtica", 6.3),
    new Estudiant("Elena",  "Matemàtiques", 8.8),
    new Estudiant("Ferran", "Informàtica", 5.5)
);

// Pregunta: Quin és el nom (en majúscules) dels 3 millors estudiants
//           d'Informàtica que han aprovat (nota >= 5)?

List<String> top3 = estudiants.stream()
    .filter(e -> e.carrera().equals("Informàtica"))  // Només Informàtica
    .filter(e -> e.nota() >= 5.0)                    // Aprovats
    .sorted(Comparator.comparingDouble(Estudiant::nota).reversed()) // Millors primer
    .limit(3)                                         // Primers 3
    .map(e -> e.nom().toUpperCase())                  // Nom en majúscules
    .collect(Collectors.toList());                    // Recull

// Resultat: ["CARLA", "ANNA", "DAVID"]

// Llegit en veu alta:
// "De tots els estudiants,
//  filtra els d'Informàtica,
//  filtra els aprovats,
//  ordena per nota descendent,
//  agafa els 3 primers,
//  transforma el nom a majúscules,
//  i recull-ho en una llista."

11. Optional: el tipus de retorn segur

Moltes operacions de Stream retornen Optional<T> perquè pot ser que no hi hagi cap element. Optional evita el NullPointerException.

Java
List<String> noms = Arrays.asList("Anna", "Biel", "Carla");

// findFirst() retorna Optional<String>
Optional<String> resultat = noms.stream()
    .filter(nom -> nom.startsWith("Z"))
    .findFirst();

// Formes de treballar amb Optional:

// 1. isPresent() / get() (no recomanat, pot llençar excepció)
if (resultat.isPresent()) {
    System.out.println(resultat.get());
}

// 2. ifPresent() (executa si hi ha valor)
resultat.ifPresent(nom -> System.out.println("Trobat: " + nom));

// 3. orElse() (valor per defecte si buit)
String nom = resultat.orElse("No trobat");
// Retorna: "No trobat"

// 4. orElseThrow() (llença excepció si buit) ← usat al projecte!
String nomOExcepcio = resultat.orElseThrow(
    () -> new RuntimeException("No s'ha trobat cap nom amb Z")
);

Optional i els repositoris

En els nostres serveis i controladors, quan fem cerques a la base de dades, és molt comú que el resultat sigui un Optional<T>. Això ens obliga a gestionar el cas en què no es trobi l'entitat, evitant així errors i millorant la robustesa del codi.