Functional Java con Lambda e Stream

Progettazione e Sviluppo del Software

C.D.L. Tecnologie dei Sistemi Informatici

Danilo Pianini — danilo.pianini@unibo.it

Gianluca Aguzzi — gianluca.aguzzi@unibo.it

Angelo Filaseta — angelo.filaseta@unibo.it

Compiled on: 2025-12-05 — versione stampabile

back

A partire da Java 8

  • Principale novità: lambda (ossia uno degli elementi fondamentali dello stile di programmazione funzionale)
    • Le lambda portano ad uno stile più elegante e astratto di programmazione
    • In Java, portano a codice più compatto e chiaro in certe situazioni
    • Impatta alcuni aspetti di linguaggio
    • Impatta varie librerie

Risorse

Comparatori definiti attraverso classi

public class FirstComparableBasic {
    static void main() {
        final List<Person> list = new ArrayList<>();
        list.add(new Person("Mario", 1960, true));
        list.add(new Person("Gino", 1970, false));
        list.add(new Person("Rino", 1951, true));
        System.out.println(list);
        list.sort(new AgeComparator());
        System.out.println(list);
        list.sort(new AgeComparator().reversed());
        System.out.println(list);
        list.sort(new NameComparator());
        System.out.println(list);
    }
}

class AgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person o1, Person o2) {
        return Integer.compare(o1.getYearOfBirth(), o2.getYearOfBirth());
    }
}

class NameComparator implements Comparator<Person> {
    @Override
    public int compare(Person o1, Person o2) {
        return o1.getName().compareTo(o2.getName());
    }
}

Comparatori definiti attraverso classi innestate

  • Una classe innestata statica B (static nested) è definita all’interno di un’altra classe A
    • Dunque ci si può riferire ad essa via A.B (secondo regole di visibilità), o direttamente via B da dentro la classe Aapprofondimento
public class FirstComparableNested {
    static void main() {
        final List<Person> list = new ArrayList<>();
        list.add(new Person("Mario", 1960, true));
        list.add(new Person("Gino", 1970, false));
        list.add(new Person("Rino", 1951, true));
        System.out.println(list);
        list.sort(new AgeComparator());
        System.out.println(list);
        list.sort(new AgeComparator().reversed());
        System.out.println(list);
        list.sort(new NameComparator());
        System.out.println(list);
    }
    private static class AgeComparator implements Comparator<Person> {
        @Override public int compare(Person o1, Person o2) {
            return Integer.compare(o1.getYearOfBirth(), o2.getYearOfBirth());
        }
    }
    private static class NameComparator implements Comparator<Person> {
        @Override public int compare(Person o1, Person o2) {
            return o1.getName().compareTo(o2.getName());
        }
    }
}

Comparatori definiti attraverso classi anonime

  • Una classe anonima è una classe definita “al volo” (senza fornirne dunque un nome) e immediatamente istanziata per evitare la proliferazione di classi – approfondimento
    • Tipicamente: per implementare “al volo” una interfaccia
public class FirstComparableWithAnonymousClasses {
    static void main() {
        final List<Person> list = new ArrayList<>();
        list.add(new Person("Mario", 1960, true));
        list.add(new Person("Gino", 1970, false));
        list.add(new Person("Rino", 1951, true));
        System.out.println(list);
        final Comparator<Person> ageComparator = new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return Integer.compare(o1.getYearOfBirth(), o2.getYearOfBirth());
            }
        };
        list.sort(ageComparator);
        System.out.println(list);
        list.sort(ageComparator.reversed());
        System.out.println(list);
        list.sort(new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getName().compareTo(o2.getName());
            }
        });
        System.out.println(list);
    }
}

Comparatori attraverso lambda

  • Una lambda è una funzione anonima, creata “al volo”
  • internamente implementata come istanza di un’interfaccia funzionale
    • un’interfaccia “SAM” (single abstract method), ossia che definisce un solo metodo astratto
package it.unibo.lambdas.intro;

import java.util.ArrayList;
import java.util.List;

public class FirstComparableWithLambdas {
    static void main() {
        final List<Person> list = new ArrayList<>();
        list.add(new Person("Mario", 1960, true));
        list.add(new Person("Gino", 1970, false));
        list.add(new Person("Rino", 1951, true));
        System.out.println(list);

        list.sort((Person o1, Person o2) -> Integer.compare(o1.getYearOfBirth(), o2.getYearOfBirth()));
        System.out.println(list);

        list.sort((o1, o2) -> Integer.compare(o2.getYearOfBirth(), o1.getYearOfBirth()));
        System.out.println(list);

        list.sort((o1, o2) -> o1.getName().compareTo(o2.getName()));
        System.out.println(list);
    }
}

Elementi delle lambda expression

Che cos’è una lambda

  • è una funzione (anonima) con accesso a uno scope locale (cf. chiusura o closure)
  • è applicabile a certi input, e dà un risultato (oppure void)
    • per calcolare il risultato potrebbe usare qualche variabile nello scope in cui è definita
  • la lambda è usabile come “valore” (quindi, come dato), ossia è passabile a metodi, altre funzioni, o memorizzata in variabili/campi
    • ossia, consente di “passare” del “codice”

Caratteristica specifica di Java

  • come vedremo, una lambda è un oggetto, e il suo tipo è sempre quello di una interface detta “funzionale”
  • metodi statici o istanza possono essere usati a mo’ di lambda (chiamati “method reference”, perché sono interpretabili come funzioni)

Come si esprime una lambda

Sintassi possibili

  • (T1 x1, ..., Tn xn) -> { <body> }
  • (x1, ..., xn) -> { <body> }
  • x -> { <body> }
  • (T1 x1,..,Tn xn) -> <exp>
  • (x1,..,xn) -> <exp>
  • x -> <exp>
  • .. oppure un “method reference” (si veda in seguito)

Ossia:

  • Per gli argomenti si può esprimere un tipo o può essere inferito (type inference)
  • Con un argomento, le parentesi tonde sono omettibili
  • Il body può essere direttamente una singola espressione/istruzione

Esempi di sintassi di Lambda expressions

package it.unibo.lambdas.intro;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class FilteringWithLambdas {
    static void main() {
        final List<String> list = Arrays.asList("foo", "bar", "foobar!", "AAAAAAA", "!!!");
        var l1 = new ArrayList<>(list);
        l1.removeIf((String s) -> { return s.length() > 3; }); // Sintassi "completa"
        System.out.println(l1); // [foo, bar, !!!]

        var l2 = new ArrayList<>(list);
        l2.removeIf(s -> { return s.startsWith("f"); }); // Sintassi con tipo dell'argomento inferito
        System.out.println(l2); // [bar, AAAAAAA, !!!]

        var l3 = new ArrayList<>(list);
        l3.removeIf((String s) -> s.contains("!")); // Sintassi con corpo dell'espressione semplificato
        System.out.println(l3);

        var l4 = new ArrayList<>(list);
        l4.removeIf(s -> s.matches("(foo)|(bar)")); // Sintassi più concisa
        System.out.println(l4);
    }
}

Method references

Sintassi possibili

  • <class>::<static-method>
    • sta per (x1, ..., xn) -> <class>.<static-method>(x1, ..., xn)
  • <class>::<instance-method>
    • sta per (x1, ..., xn) -> x1.<instance-method>(x2, ..., xn)
  • <obj>::<method>
    • sta per (x1, ..., xn) -> <obj>.<method>(x1, ..., xn)
  • <class>::new
    • sta per (x1, ..., xn) -> new <class>(x1, ..., xn)

Ossia:

  • Descrivibile come metodo (statico o non), o costruttore..
  • Usabile “naturalmente” (e opzionalmente) quando la lambda non fa altro che chiamare un metodo usando “banalmente” i suoi input e restituendo il suo “output”

Esempi di Method Reference (casi 1,2,3)

package it.unibo.lambdas.first;

import java.util.Arrays;
import java.util.List;

public class AllLambdas2 {
    private static int staticMyCompare(final String a, final String b) { return a.compareTo(b); }

    private int instanceMyCompare(final String a, final String b) { return b.compareTo(a); }

    static void main() {
        final List<String> list = Arrays.asList("a", "bb", "c", "ddd");
        final AllLambdas2 objAL = new AllLambdas2();

        list.sort((x, y) -> staticMyCompare(x, y));
        list.sort(AllLambdas2::staticMyCompare);  // same as above
        System.out.println(list); // [a, bb, c, ddd]

        list.sort((x, y) -> objAL.instanceMyCompare(x, y));
        list.sort(objAL::instanceMyCompare);  // same as above
        System.out.println(list); // [ddd, c, bb, a]

        list.sort((x, y) -> x.compareTo(y));
        list.sort(String::compareTo);  // same as above
        System.out.println(list); // [ddd, c, bb, a]
    }
}

Dove si può usare una lambda?

Definizione di interfaccia “funzionale”

  • È una interface con un singolo metodo astratto
    • Non c’è ambiguità circa quale metodo implementare

Quale tipo è compatibile con una lambda?

  • Una lambda può essere passata dove ci si attende un oggetto che implementi una interfaccia funzionale
    • C’è compatibilità se i tipi in input/output della lambda (inferiti o non) sono compatibili con quelli dell’unico metodo dell’interfaccia

Motivazione:

  • Di fatto, il compilatore traduce la lambda nella creazione di un oggetto di una classe anonima che implementa l’interfaccia funzionale
    • Uno specifico opcode a livello di bytecode evita di costruirsi effettivamente un .class per ogni lambda

Generazione automatica della classe anonima

package it.unibo.lambdas.first;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class FirstComparable2 {
    static void main() {
        final List<Person> list = new ArrayList<>();
        list.add(new Person("Mario", 1960, true));
        list.add(new Person("Gino", 1970, false));
        list.add(new Person("Rino", 1951, true));
        System.out.println(list);

        // Sorting with a lambda
        list.sort((p1, p2) -> Integer.compare(p2.getYear(), p1.getYear()));
        System.out.println(list);

        // Nota che sort richiede un Comparator<Persona>, che ha il solo metodo:
        // int compare(Persona p1, Persona p2)
        // Quindi il codice equivalente generato da javac è:
        list.sort(new Comparator<Person>() {
            public int compare(Person p1, Person p2) {
                return Integer.compare(p2.getYear(), p1.getYear());
            }
        });
        System.out.println(list);
    }
}

Esempio: funzione riusabile di filtraggio

package it.unibo.lambdas.first;

// Similar to java.util.functions.Predicate<T>
public interface Filter<X> {
    boolean applyFilter(X x); // Does element x pass the filter?
}
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

public class FilterUtility {
    public static <X> Set<X> filterAll(Collection<X> set, Filter<X> filter) {
        final Set<X> newSet = new LinkedHashSet<>();
        for (final X x : set) {
            if (filter.applyFilter(x)) {
                newSet.add(x);
            }
        }
        return newSet;
    }

    static void main() {
        final List<Integer> ls = List.of(10, 20, 30, 40, 50, 60);
        // Nota che il nome del metodo in Filter non è mai menzionato qui
        System.out.println(filterAll(ls, x -> x > 20)); // [30,40,50,60]
        System.out.println(filterAll(ls, x -> x > 20 && x < 60)); // [30,40,50]
        System.out.println(filterAll(ls, x -> x % 20 == 0)); // [20,40,60]
    }
}

Metodi default nelle interfacce

È possibile fornire implementazioni di default ai metodi delle interface

  • sintassi: interface I { ... default int m(){ ... } }
  • significato: non è necessario implementarli nelle sottoclassi

Utilità

  • consente di aggiungere metodi a interfacce senza rompere la compatibilità con classi esistenti che le implementano
  • fornire comportamento ereditabile in modalità multipla
  • costruire più facilmente interfacce funzionali: queste devono in effetti avere un unico metodo senza default
  • consente di realizzare il pattern template method solo con interfacce

Esempi di interfacce con metodi di default nella Java API:

  • Iterable, Iterator, Collection, Comparator

Annotazione @FunctionalInterface

  • da usare per interfacce funzionali, affinché il compilatore controlli che l’interfaccia sia funzionale
    • ossia che vi sia un solo metodo “astratto”

Interfacce funzionali di libreria – package java.util.function

Perché scriversi una nuova interfaccia funzionale all’occorrenza?

  • Lo si fa solo per rappresentare concetti specifici del dominio
  • Lo si fa se ha metodi default aggiuntivi

In java.util.function vengono fornite varie interfacce “general purpose”

  • Sono tutte funzionali
  • Hanno metodi aggiuntivi default di cui non ci occupiamo
  • Hanno un metodo “astratto” chiamato, a seconda: apply, accept, test o get

Package java.util.function, funzioni che lavorano su oggetti generici

Interface Method signature Lambda type
Consumer<T> void accept(T t) (T) -> void
BiConsumer<T,U> void accept(T t, U u) (T, U) -> void
Function<T,R> R apply(T t) (T) -> R
BiFunction<T,U,R> R apply(T t, U u) (T, U) -> R
UnaryOperator<T> T apply(T t) (T) -> T
BinaryOperator<T> T apply(T t1, T t2) (T, T) -> T
Predicate<T> boolean test(T t) (T) -> boolean
BiPredicate<T,U> boolean test(T t, U u) (T, U) -> boolean
Supplier<T> T get() () -> T
java.lang.Runnable void run() () -> void

Package java.util.function, funzioni che lavorano su int

Interface Method signature Lambda type
IntConsumer void accept(int value) (int) -> void
IntFunction<R> R apply(int value) (int) -> R
ToIntFunction<T> int applyAsInt(T value) (T) -> int
ToIntBiFunction<T,U> int applyAsInt(T t, U u) (T, U) -> int
IntSupplier int getAsInt() () -> int
IntUnaryOperator int applyAsInt(int operand) (int) -> int
IntBinaryOperator int applyAsInt(int left, int right) (int, int) -> int
IntPredicate boolean test(int value) (int) -> boolean
IntToLongFunction long applyAsLong(int value) (int) -> long
IntToDoubleFunction double applyAsDouble(int value) (int) -> double

Package java.util.function, funzioni che lavorano su long

Interface Method signature Lambda type
LongConsumer void accept(long value) (long) -> void
LongFunction<R> R apply(long value) (long) -> R
ToLongFunction<T> long applyAsLong(T value) (T) -> long
ToLongBiFunction<T,U> long applyAsLong(T t, U u) (T, U) -> long
LongSupplier long getAsLong() () -> long
LongUnaryOperator long applyAsLong(long operand) (long) -> long
LongBinaryOperator long applyAsLong(long left, long right) (long, long) -> long
LongPredicate boolean test(long value) (long) -> boolean
LongToIntFunction int applyAsInt(long value) (long) -> int
LongToDoubleFunction double applyAsDouble(long value) (long) -> double

Package java.util.function, funzioni che lavorano su double

Interface Method signature Lambda type
DoubleConsumer void accept(double value) (double) -> void
DoubleFunction<R> R apply(double value) (double) -> R
ToDoubleFunction<T> double applyAsDouble(T value) (T) -> double
ToDoubleBiFunction<T,U> double applyAsDouble(T t, U u) (T, U) -> double
DoubleSupplier double getAsDouble() () -> double
DoubleUnaryOperator double applyAsDouble(double operand) (double) -> double
DoubleBinaryOperator double applyAsDouble(double left, double right) (double, double) -> double
DoublePredicate boolean test(double value) (double) -> boolean
DoubleToIntFunction int applyAsInt(double value) (double) -> int
DoubleToLongFunction long applyAsLong(double value) (double) -> long

Esempio: funzione riusabile di filtraggio via Predicate

package it.unibo.lambdas.first;

import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;

public class FilterUtility2 {
    public static <X> Set<X> filterAll(Collection<X> set, Predicate<X> filter) {
        final Set<X> newSet = new LinkedHashSet<>();
        for (final X x : set) {
            if (filter.test(x)) {
                newSet.add(x);
            }
        }
        return newSet;
    }

    static void main() {
        final List<Integer> ls = List.of(10, 20, 30, 40, 50, 60);
        // Note that the name of the method in Filter is never mentioned here
        System.out.println(filterAll(ls, x -> x > 20)); // [30,40,50,60]
        System.out.println(filterAll(ls, x -> x > 20 && x < 60)); // [30,40,50]
        System.out.println(filterAll(ls, x -> x % 20 == 0)); // [20,40,60]
    }
}

Esempio: comandi “programmati” via Runnable

package it.unibo.lambdas.first;

import java.util.List;

public class RunnableUtility {
    private static void repeat(final int howMany, final Runnable r) {
        for (int i = 0; i < howMany; i++) {
            r.run();
        }
    }

    private static void batchExecution(final List<Runnable> list) {
        for (final Runnable r : list) {
            r.run();
        }
    }

    static void main() {
        repeat(10, () -> System.out.println("ok"));
        batchExecution(
            List.of(
                () -> System.out.println("a"),
                () -> System.out.println("b"),
                () -> System.out.println("c"),
                () -> System.exit(0)
            )
        );
    }
}

Funzioni di ordine superiore

  • Le funzioni di ordine superiore sono funzioni che accettano in ingresso funzioni e/o che restituiscono in output funzioni
final Function<Integer, Function<Integer, Integer>> multiplier = x -> (y -> x * y);
final Function<Integer, Integer> doubler = multiplier.apply(2);
assertEquals(10, doubler.apply(5));

final BiFunction<Predicate<String>, Predicate<String>, Predicate<String>> and = 
  (p1, p2) -> (s -> p1.test(s) && p2.test(s));
Predicate<String> p = and.apply(s -> s.length() < 5, s -> s.toUpperCase().equals(s));
assertFalse(p.test("abc"));
assertFalse(p.test("ABCDEFG"));
assertTrue(p.test("ABC"));

Chiusure lessicali

  • Una chiusura (lessicale) o (in inglese) closure è una funzione che accede a simboli visibili nello scope
    • Limitazione: le variabili nello scope referenziate in una lambda devono essere final
final int k = 10;
Predicate<String> p = (s) -> s.length() > k;
p.test("hello"); // false
p.test("hello world"); // true

Motivazioni e vantaggi nell’uso delle lambda in Java

Elementi di programmazione funzionale in Java

  • Le lambda consentono di aggiungere certe funzionalità della programmazione funzionale in Java, creando quindi una contaminazione OOP + FP
  • Il principale uso è quello che concerne la creazione di funzionalità (metodi) ad alto riuso – ad esempio filterAll
  • Tali metodi possono prendere in ingresso funzioni, passate con sintassi semplificata rispetto a quella delle classi anonime, rendendo più “naturale” e agevole l’uso di questo meccanismo

Miglioramento alle API di Java

  • Concetto di Stream<T> e sue manipolazioni, per lavorare su dati sequenziali (collezioni, file, …)
  • Supporto più diretto ad alcuni pattern: Command, Strategy, Observer
  • Alcune migliorie “varie” nelle API

Interfaccia java.util.Map – metodi aggiuntivi

public interface Map<K,V> {
    ...
    
    default V getOrDefault(Object key, V defaultValue) {..}

    default void forEach(BiConsumer<? super K, ? super V> action) {..}
    
    default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {..}
    
    default V putIfAbsent(K key, V value) {..}
    
    default boolean remove(Object key, Object value) {..}
    
    default boolean replace(K key, V oldValue, V newValue) {..}
    
    default V replace(K key, V value) {..}
    
    default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {..}
    
    default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {..}
    
    default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {..}
    
    default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {..}
}

Esempi su Map

package it.unibo.lambdas.first;

import java.util.LinkedHashMap;
import java.util.Map;

public class UseMap {
    static void main() {
        final Map<Integer, String> map = new LinkedHashMap<>();
        map.put(10, "a");
        map.put(20, "bb");
        map.put(30, "ccc");
        map.forEach((k, v) -> System.out.println(k + " " + v));

        map.replaceAll((k, v) -> v + k); // nuovi valori
        System.out.println(map);
        // {20=bb20, 10=a10, 30=ccc30}

        map.merge(5, ".", String::concat);
        map.merge(10, ".", String::concat);
        System.out.println(map);
        // {20=bb20, 5=., 10=a10., 30=ccc30}
    }
}

Il concetto di Stream

Idee

  • Uno Stream rappresenta un flusso sequenziale (anche infinito) di dati omogenei, usabile una volta sola
  • Assomiglia al concetto di Iterator, ma lo Stream è più dichiarativo, perché non indica passo-passo come l’informazione viene processata, e quindi è concettualmente più astratto
  • Ove possibile, uno Stream manipola i suoi elementi in modo “lazy” (ritardato): i dati vengono processati mano a mano che servono, non sono memorizzati tutti assieme come nelle Collection
  • È possibile creare pipeline (catene) di trasformazioni di Stream (implementate con decorazioni successive) in modo funzionale, per ottenere flessibilmente computazioni non banali dei loro elementi, con codice più compatto e leggibile
  • Questa modalità di lavoro rende alcune computazioni (automaticamente) parallelizzabili

Computazioni con gli Stream

Struttura a pipeline

  • Una sorgente o sink:
    • Una Collection/array, un dispositivo di I/O, una funzione generatrice
  • Una sequenza di trasformazioni:
    • trasformare (mappare), filtrare, raggruppare, ma non solo
  • Un terminatore, che aggrega i dati dello Stream:
    • una riduzione a un valore, una Collection/array, un Iteratore

Esempio: con persone con nome, città e reddito

  • Data una List<Persona> con proprietà reddito e città, ottenere la somma dei redditi di tutte le persone di Cesena
  • Come lo realizziamo tramite una pipeline?
    • Sorgente: la lista
    • Trasformazione 1: filtro sulle persone di Cesena
    • Trasformazione 2: si mappa ogni persona sul suo reddito
    • Terminazione: sommo
  • Aspetto cruciale: le fasi intermedie (dopo le trasformazioni), non generano collezioni temporanee

Classe Personequals, hashCode e toString omessi

public final class Person {
    private final String name;
    private final String city;
    private final double income;
    private final Set<String> jobs;
    public Person(String name, String city, double income, String... jobs) {
        this.name = Objects.requireNonNull(name);
        this.city = city; // null in ingresso indica città assente
        this.income = income;
        this.jobs = Set.copyOf(Arrays.asList(jobs)); // conversione a set
    }
    // Getters
    public String getName() { return this.name; }
    public String getCity() { return this.city; }
    public double getIncome() { return this.income; }
    public Set<String> getJobs() { return this.jobs; } // Set immutabile, non serve copia
    // Hashcode, equals, toString
    @Override public int hashCode() { return Objects.hash(name, city, income, jobs); }
    @Override public boolean equals(Object obj) {
        return this == obj
            || obj instanceof Person person
            && Objects.equals(name, person.name)
            && Objects.equals(city, person.city)
            && Double.compare(income, person.income) == 0
            && Objects.equals(jobs, person.jobs);
    }
    @Override public String toString() {
        return "Person [name=" + name + ", city=" + city + ", income=" + income + ", jobs=" + jobs + "]";
    }
}

Realizzazione dell’esempio con gli Stream

public class UseStreamsOnPerson {
    static void main() {
        final List<Person> list = List.of(
            new Person("Mario", "Cesena", 20000, "Teacher"),
            new Person("Rino", "Forlì", 50000, "Professor"),
            new Person("Lino", "Cesena", 110000, "Professor", "Dean"),
            new Person("Ugo", "Cesena", 20000, "Secretary"),
            new Person("Marco", null, 4000, "Contractor")
        );

        final double result = list.stream()
            .filter(it -> "Cesena".equals(it.getCity())) // Tieni solo i cesenati
            .mapToDouble(Person::getIncome) // Prendi il loro reddito
            .sum(); // Somma
        System.out.println(result);

        // alternativa con iteratore: qual è la più leggibile?
        double totalIncome = 0; // inizializza somma
        for (final Person p: list) { // per ogni persona nella lista
            if ("Cesena".equals(p.getCity())) { // se la città è cesena
                totalIncome = totalIncome + p.getIncome(); // allora la somma aumenta del suo reddito
            }
        }
        System.out.println(totalIncome);
    }
}

Questa è solo una introduzione alle lambda in Java. Nel blocco di approfondimento si trovano ulteriori informazioni su come:

  • creare stream
  • manipolazioni di stream (avanzate)
  • esempi di uso di stream

Lambda e funzioni first-class

Progettazione e Sviluppo del Software

C.D.L. Tecnologie dei Sistemi Informatici

Danilo Pianini — danilo.pianini@unibo.it

Gianluca Aguzzi — gianluca.aguzzi@unibo.it

Angelo Filaseta — angelo.filaseta@unibo.it

Compiled on: 2025-12-05 — versione stampabile

back

Riconoscimenti

  • Questo materiale è ampiamente basato su quello realizzato dai Prof. Mirko Viroli e Roberto Casadei, che ringrazio.

  • Ogni errore riscontratovi è esclusiva responsabilità degli autori di questo documento.