W3docs

Metodi generici in Java

Definisci metodi con parametri di tipo propri in Java, indipendenti dalla classe che li contiene.

Un metodo generico è un metodo che introduce un proprio parametro di tipo nella sua firma, indipendentemente da qualsiasi parametro a livello di classe. È lo strumento giusto quando la relazione tra tipi appartiene a un solo metodo — un'utilità che scambia due elementi di un array, una factory che restituisce una lista di qualsiasi cosa il chiamante passi, un helper statico che non dispone di un'istanza a cui collegare una T. I metodi generici sono il modo in cui quasi ogni metodo statico di java.util.Collections e java.util.Arrays è scritto.

Dove va il parametro di tipo

Il parametro di tipo viene dichiarato prima del tipo di ritorno, tra i modificatori e il ritorno:

public static <T> T identity(T value) {
  return value;
}

Leggendo da sinistra a destra: "public, static, dichiara un parametro di tipo T, restituisce una T, chiamato identity, riceve una T." Il <T> è ciò che rende questo un metodo generico, anziché un metodo che usa per caso una T a livello di classe.

Chiamalo come un metodo normale — il compilatore inferisce il tipo dall'argomento passato:

String s = identity("hello");   // T inferred as String
Integer n = identity(42);       // T inferred as Integer

Se l'inferenza fallisce o vuoi sovrascriverla, puoi fornire il tipo esplicitamente con la sintassi del type witness, dopo il punto:

String s = MyUtil.<String>identity("hello");   // rarely needed

In dieci anni di Java, scriverai quella forma esplicita forse una dozzina di volte.

Perché usare un parametro a livello di metodo invece di uno a livello di classe

Un parametro a livello di classe dice "tutta questa classe riguarda un tipo." Un parametro a livello di metodo dice "questa singola operazione è polimorfica rispetto a un tipo che non ha bisogno di sopravvivere alla chiamata." Non sono intercambiabili — rispondono a domande diverse:

// Method-level: the class isn't generic; the method is.
public class Arrays {
  public static <T> void swap(T[] arr, int i, int j) { ... }
}

// Class-level: the class is parameterised; methods share that T.
public class Box<T> {
  public T get() { ... }
  public void set(T value) { ... }
}

Usa un parametro a livello di metodo quando:

  • Il metodo è static (non ha istanza, quindi non può prendere in prestito la T a livello di classe).
  • La relazione tra tipi è locale al metodo — input e output condividono un tipo, ma la classe no.
  • Vuoi che diverse chiamate dello stesso metodo usino tipi diversi: swap su un String[] e swap su un Integer[] devono funzionare entrambe, e la classe non deve impegnarsi su uno solo.

Più parametri di tipo in un metodo

La stessa regola si applica: dichiarali tra i modificatori e il tipo di ritorno, separati da virgole:

public static <K, V> Map.Entry<K, V> entry(K key, V value) {
  return new AbstractMap.SimpleImmutableEntry<>(key, value);
}

Map.Entry<String, Integer> e = entry("Ada", 100);

Sia K che V vengono inferiti dagli argomenti. Se i due parametri condividono un tipo, il tipo inferito è quello su cui entrambi gli argomenti concordano:

public static <T> T firstOf(T a, T b) { return a; }

firstOf("x", "y");   // T = String
firstOf("x", 42);    // T = Object — the closest common supertype

Quest'ultimo caso è talvolta un'insidia. Il compilatore non lo rifiuta; allarga semplicemente T a Object in silenzio. Se volevi "due argomenti dello stesso tipo esatto," i generici non possono imporlo oltre l'allargamento — dovresti rendere gli argomenti parametri di tipo separati.

Un parametro a livello di metodo su una classe generica

Una classe generica può avere metodi generici che introducono propri parametri, distinti da quelli della classe. I due parametri coesistono:

public class Box<T> {
  private T value;

  public Box(T value) { this.value = value; }

  public T get() { return value; }

  // U is local to this method — independent of T.
  public <U> Box<U> map(java.util.function.Function<T, U> fn) {
    return new Box<>(fn.apply(value));
  }
}

Box<String> name   = new Box<>("Ada");
Box<Integer> length = name.map(String::length);     // T=String, U=Integer

La <U> di map è in scope solo all'interno di map. Può usare T (perché si trova dentro un Box<T>) ma non può sostituirla.

L'inferenza di tipo in pratica

Il compilatore inferisce i parametri di tipo di un metodo da:

  1. I tipi degli argomenti espliciti.
  2. Il tipo di destinazione — ciò a cui assegni il risultato, o il tipo del parametro del metodo a cui passi il risultato.

La seconda fonte è il motivo per cui List.of(), Collections.emptyList(), e simili generici basati solo sul ritorno funzionano senza un type witness esplicito nella maggior parte dei casi:

List<String> empty = Collections.emptyList();           // T inferred from the left side
process(Collections.emptyList());                       // T inferred from `process`'s parameter

Quando nessuna delle due fonti è disponibile (nessun argomento, nessun tipo di destinazione), il compilatore ricade su Object. Questo non è quasi mai ciò che vuoi — scrivi il type witness o aggiungi un tipo di destinazione:

var x = Collections.emptyList();   // List<Object> — probably not what you meant
List<String> y = Collections.emptyList();   // List<String> ✓

Una forma concreta: metodi di utilità sulle collezioni

I metodi Collections.unmodifiableList, Collections.sort, Collections.shuffle e simili della libreria standard sono tutti metodi generici su una classe di utilità non generica. Prendi sort, in essenza:

public static <T extends Comparable<T>> void sort(List<T> list) {
  // ... sorts using natural order
}

Quella firma fa due cose contemporaneamente. Il <T> dichiara un parametro di tipo. Il extends Comparable<T> è un boundT deve essere un tipo capace di confrontarsi con se stesso. Dedicheremo un intero capitolo ai parametri con bound; per ora, osserva semplicemente che il bound è ciò che consente al metodo di chiamare compareTo sui suoi elementi.

Un esempio pratico: swap tipizzato, last tipizzato, map tipizzato

Una piccola classe di utilità con tre metodi generici — uno void, uno che restituisce lo stesso tipo ricevuto, uno che mappa gli elementi in un nuovo tipo. Insieme coprono le tre forme che scriverai più spesso.

java— editable, runs on the server

Tre cose da notare. swap funziona sia su String[] che su Integer[] perché T viene inferito per ogni chiamata. last restituisce il tipo di elemento che il chiamante ha passato — nessun cast dal lato ricevente. map introduce due parametri di tipo e li collega tramite il parametro Function<T, R> — il compilatore garantisce che la funzione prenda il tipo di elemento della lista e restituisca il tipo di elemento della lista risultante.

Cosa c'è dopo

Hai visto i due modi di dichiarare un parametro di tipo — su una classe e su un metodo. Il passo successivo è il terzo posto in cui un parametro di tipo può vivere: su un'interfaccia. È così che la libreria standard definisce List<E>, Comparator<T>, Function<T, R> e ogni altro contratto che implementi quando scrivi codice polimorfico. Continua con Interfacce generiche in Java.

Pratica

Pratica
Stai scrivendo un'utilità statica `public static <T> T firstNonNull(T a, T b) { return a != null ? a : b; }`. Un chiamante scrive `firstNonNull('hi', 42)`. Cosa inferisce il compilatore per `T`?
Stai scrivendo un'utilità statica `public static <T> T firstNonNull(T a, T b) { return a != null ? a : b; }`. Un chiamante scrive `firstNonNull('hi', 42)`. Cosa inferisce il compilatore per `T`?
Was this page helpful?