W3docs

Riferimenti a metodi in Java

Usa l'operatore :: in Java per riferire metodi statici, di istanza, legati e costruttori come lambda.

Un riferimento a metodo è una sintassi abbreviata per una lambda il cui corpo non fa altro che chiamare un metodo esistente. Quando una lambda è letteralmente x -> SomeClass.foo(x) o (a, b) -> a.bar(b), l'operatore :: permette di scriverla come SomeClass::foo o Some::bar. Il compilatore produce lo stesso valore in entrambi i casi — un'istanza dell'interfaccia funzionale appropriata — quindi ovunque si adatti una lambda, si adatta anche un riferimento a metodo corrispondente.

Function<String, Integer> len1 = s -> s.length();
Function<String, Integer> len2 = String::length;            // identical at runtime

List<String> names = List.of("Bob", "Alice");
names.forEach(s -> System.out.println(s));                   // lambda
names.forEach(System.out::println);                          // method reference

Le quattro forme seguenti coprono tutti i riferimenti a metodo che scriverai. L'unica competenza richiesta è riconoscere quale forma si adatta a un dato punto di chiamata.

Forma 1: riferimento a metodo statico — ClassName::staticMethod

Il metodo è un metodo static di una classe. Il riferimento diventa una lambda i cui parametri sono i parametri del metodo statico:

Function<String, Integer> parse  = Integer::parseInt;        // s -> Integer.parseInt(s)
BinaryOperator<Integer>   max    = Math::max;                 // (a, b) -> Math.max(a, b)
Function<Object, String>  toStr  = String::valueOf;            // o -> String.valueOf(o)

Questa è la forma che compare nel codice con gli stream come nums.stream().reduce(0, Integer::sum)Integer.sum(int, int) è statico, quindi Integer::sum è un BinaryOperator<Integer> (un BiFunction<Integer, Integer, Integer>).

Forma 2: riferimento a metodo di istanza legato — instance::method

Il metodo è un metodo di istanza su un oggetto specifico e nominato. Il riferimento diventa una lambda i cui parametri sono i parametri del metodo (l'istanza viene catturata):

PrintStream out = System.out;
Consumer<String> print = out::println;                  // s -> out.println(s)

String prefix = "Hello, ";
Function<String, String> greet = prefix::concat;        // name -> prefix.concat(name)

List<String> log = new ArrayList<>();
Consumer<String> record = log::add;                     // msg -> log.add(msg)

Il ricevitore legato occupa lo slot senza argomenti: poiché prefix è già catturato, greet ha bisogno solo dell'argomento di concat, quindi è una Function<String, String> anziché una BiFunction. Analogamente, log::add mantiene log fisso ed espone solo l'elemento da aggiungere, fornendo un Consumer<String>.

L'instance catturata viene mantenuta dall'oggetto risultante, in modo simile a come una lambda cattura le variabili locali effectively final. I riferimenti legati sono il modo per dire "usa il metodo di questo oggetto specifico come callback" — Logger::info su un logger particolare, event::handle su un handler particolare.

Forma 3: riferimento a metodo di istanza non legato — ClassName::method

Il metodo è un metodo di istanza, ma lo si referenzia tramite la classe anziché un'istanza specifica. Il riferimento diventa una lambda il cui primo parametro è il ricevitore, mentre i restanti sono i parametri propri del metodo:

Function<String, Integer>   len    = String::length;             // s -> s.length()        — first param is the receiver
Function<String, String>    upper  = String::toUpperCase;         // s -> s.toUpperCase()
BiPredicate<String, String> starts = String::startsWith;          // (s, prefix) -> s.startsWith(prefix)

Questa è la forma su cui le persone inciampano. String::length sembra che possa significare "il metodo length sulla classe String" — ma non esiste un tale metodo statico. Significa davvero "dato qualsiasi String, chiama il suo metodo di istanza length() — il ricevitore è il primo parametro della lambda." Ecco perché String::length è una Function<String, Integer> (un input, un output) e String::startsWith è un BiPredicate<String, String> (il secondo input è il prefisso che il ricevitore testa).

Questa forma è il motore alla base di quasi ogni pipeline di stream:

people.stream()
    .map(Person::name)               // unbound: p -> p.name()
    .filter(s -> s.startsWith("A"))
    .map(String::toUpperCase)        // unbound: s -> s.toUpperCase()
    .forEach(System.out::println);   // bound: s -> out.println(s)

Forma 4: riferimento a costruttore — ClassName::new

Fa riferimento a un costruttore come funzione. La lambda risultante prende i parametri del costruttore e restituisce una nuova istanza:

Supplier<List<String>>           listOf  = ArrayList::new;                  // () -> new ArrayList<>()
Function<Integer, ArrayList<?>>  sized   = ArrayList::new;                   // n -> new ArrayList<>(n)
Function<String, BigDecimal>     toBig   = BigDecimal::new;                  // s -> new BigDecimal(s)
BiFunction<String, Integer, AbstractMap.SimpleEntry<String, Integer>> entry =
    AbstractMap.SimpleEntry::new;

I riferimenti a costruttore sono il modo in cui Collectors.toCollection(TreeSet::new) permette di scegliere un tipo di destinazione, e come Stream.generate(Random::new) produce oggetti Random indipendenti ad ogni chiamata di get().

Gli array hanno una forma speciale: String[]::new è una IntFunction<String[]>n -> new String[n]. È quella che usa stream.toArray(String[]::new).

Riferimento a metodo vs lambda — quando ognuno vince

Un riferimento a metodo è la scelta giusta quando il corpo della lambda è esattamente una singola chiamata a metodo con i parametri passati in ordine:

LambdaRiferimento a metodo
s -> s.length()String::length
s -> System.out.println(s)System.out::println
(a, b) -> a.compareTo(b)String::compareTo
() -> new ArrayList<>()ArrayList::new

Una lambda è la scelta giusta quando il corpo fa qualsiasi altra cosa:

  • Chiama più di un metodo: s -> s.trim().toUpperCase() (nessun riferimento per la catena).
  • Contiene una qualsiasi trasformazione degli argomenti: s -> System.out.println("[" + s + "]").
  • Contiene qualsiasi flusso di controllo: n -> n < 0 ? 0 : n.
  • Riordina o duplica gli argomenti: (a, b) -> b.compareTo(a) (comparatore invertito).

L'ottimizzazione non riguarda davvero la velocità a runtime — entrambi vengono compilati nello stesso bootstrap invokedynamic. Si tratta di leggibilità. Person::name salta all'occhio come "il campo name," mentre p -> p.name() richiede di leggere tre token. Quando il riferimento si adatta, preferiscilo; quando non si adatta, non contorcere il codice per farlo adattare.

Un trabocchetto con i riferimenti a costruttore: overload ambigui

ClassName::new funziona bene quando c'è un solo costruttore che corrisponde all'interfaccia target. Quando ce ne sono diversi, il compilatore sceglie in base al numero di parametri e ai tipi del tipo target. Nella maggior parte dei casi funziona; occasionalmente no, e bisogna disambiguare tipizzando la variabile esplicitamente o ricorrendo a una lambda:

// ArrayList has constructors: (), (int), (Collection)
Supplier<ArrayList<String>>           a = ArrayList::new;     // picks the no-arg
Function<Integer, ArrayList<String>>  b = ArrayList::new;     // picks the (int) one
Function<List<String>, ArrayList<String>> c = ArrayList::new; // picks the (Collection) one

// var inference can't disambiguate — this would not compile:
// var ambiguous = ArrayList::new;

La soluzione è mantenere il tipo target esplicito, come in a, b, c sopra.

Un esempio pratico: tutte e quattro le forme in un unico programma

Il programma qui sotto costruisce e usa un riferimento a metodo di ciascuna forma, dimostra come String::length (non legato) diventi una Function<String, Integer>, e mostra il trucco del riferimento a costruttore che guida stream().toArray(T[]::new).

java— editable, runs on the server

Cosa trarre dall'esecuzione:

  • Tutte e quattro le forme si compilano in istanze di normali interfacce funzionali — parse è una Function<String, Integer> che tu abbia scritto s -> Integer.parseInt(s) o Integer::parseInt. La forma abbreviata è puramente sintattica.
  • I riferimenti non legati String::length e String::toUpperCase hanno entrambi un ricevitore come primo parametro. Ecco perché String::length è una Function<String, Integer> e String::startsWith è un BiPredicate<String, String> — il ricevitore occupa uno slot, il parametro esplicito l'altro.
  • Il riferimento a costruttore String[]::new ha prodotto una IntFunction<String[]> — la forma che stream().toArray(...) richiede. I riferimenti a costruttore sono il modo per dire a uno stream "ecco il tipo di destinazione."
  • Il comparatore di lunghezza invertito non poteva essere scritto come riferimento a metodo: il ricevitore e il parametro si scambiano, e i riferimenti a metodo non possono riordinare gli argomenti. Questo è esattamente il tipo di caso in cui una lambda è ancora la scelta giusta.

Cosa viene dopo

Ora puoi scrivere una pipeline di stream quasi interamente con riferimenti a metodi e lasciare le poche trasformazioni che necessitano davvero di modellazione in piccole lambda. Quello stile è l'introduzione naturale al pezzo centrale della parte: gli stream. Il prossimo capitolo, Introduzione agli Stream Java, presenta l'API Stream<T> — cos'è, come appare una pipeline di stream, perché è lazy, perché può essere usata una sola volta, e come si integra con le lambda, le interfacce funzionali e i riferimenti a metodo che hai appena imparato.

Pratica

Pratica
`String::length` è che tipo di riferimento a metodo, e quale interfaccia funzionale corrisponde?
`String::length` è che tipo di riferimento a metodo, e quale interfaccia funzionale corrisponde?
Was this page helpful?