W3docs

Interfacce Generiche in Java

Impara a progettare interfacce generiche in Java che parametrizzano le firme dei metodi su un tipo.

Un'interfaccia generica è un'interfaccia la cui dichiarazione accetta uno o più parametri di tipo, proprio come una classe generica. È il terzo posto in cui i parametri di tipo possono vivere in Java, insieme alle classi generiche e ai metodi generici, ed è quello più significativo — perché quasi ogni contratto riutilizzabile nella libreria standard è un'interfaccia generica. List<E>, Map<K, V>, Comparator<T>, Function<T, R>, Supplier<T>, Iterable<T>, Iterator<T> — sono la spina dorsale del Java moderno.

La sintassi

La lista dei parametri di tipo si trova tra il nome dell'interfaccia e il corpo:

public interface Container<T> {
  void add(T item);
  T   get(int index);
  int size();
}

Leggilo come "un Container parametrizzato su un certo tipo di elemento T." All'interno dell'interfaccia, T può comparire nei parametri dei metodi, nei tipi di ritorno e in qualsiasi altra posizione in cui può andare un tipo. I metodi default (Java 8+) e i metodi privati di interfaccia (Java 9+) possono usare T anche loro.

Quando implementi l'interfaccia, devi fare una scelta — e la scelta è l'intera decisione architetturale:

// 1. Pick a concrete type — the implementation is specialised.
public class StringContainer implements Container<String> {
  private final List<String> items = new ArrayList<>();
  public void   add(String s)  { items.add(s); }
  public String get(int i)     { return items.get(i); }
  public int    size()         { return items.size(); }
}

// 2. Stay generic — pass the parameter through to the class.
public class ListContainer<E> implements Container<E> {
  private final List<E> items = new ArrayList<>();
  public void add(E e)    { items.add(e); }
  public E    get(int i)  { return items.get(i); }
  public int  size()      { return items.size(); }
}

Entrambi sono validi. Il primo è "un Container che contiene String, specificamente." Il secondo è "un Container parametrizzato sullo stesso E che sceglie il chiamante." La maggior parte dei container riutilizzabili usa la seconda forma; quelli specializzati (un JsonObject è un "container di JsonValues, nient'altro") usano la prima.

Parametri di tipo multipli

La struttura si generalizza direttamente a due o più parametri. Guarda java.util.Map:

public interface Map<K, V> {
  V    put(K key, V value);
  V    get(Object key);          // Object on purpose — see below
  Set<K> keySet();
  Collection<V> values();
  ...
}

La dichiarazione Map<K, V> dice "due parametri: K per le chiavi, V per i valori." Le implementazioni li fissano o li passano attraverso:

public class StringIntMap implements Map<String, Integer> { ... }   // pinned
public class HashMap<K, V> implements Map<K, V>           { ... }   // passed through

Il get(Object key) nella firma di Map è una scelta deliberata di design dell'API — accetta qualsiasi object come chiave di ricerca per ragioni storiche. Torneremo su questo nella parte Collections; non è una regola dei generics, solo un compromesso specifico di Map.

Le interfacce funzionali sono interfacce generiche

Le interfacce in java.util.functionFunction, Predicate, Consumer, Supplier, BiFunction, e così via — sono tutte interfacce generiche con un unico metodo astratto, il che le rende obiettivi per le lambda:

public interface Function<T, R> {
  R apply(T t);
}

public interface Predicate<T> {
  boolean test(T t);
}

public interface Comparator<T> {
  int compare(T a, T b);
}

Quando scrivi s -> s.length(), il compilatore deduce un Function<String, Integer> dal contesto. I due parametri di tipo di Function<T, R> vengono riempiti dal codice circostante — di solito un'operazione su stream o un parametro di metodo:

List<String> names = List.of("Ada", "Grace", "Linus");
List<Integer> lengths = names.stream()
    .map(s -> s.length())          // Function<String, Integer> — both inferred
    .toList();

Questo è un'interfaccia generica e un metodo generico (Stream.map) che cooperano. La firma del metodo è approssimativamente <R> Stream<R> map(Function<? super T, ? extends R> mapper) — wildcard che incontreremo in Wildcards, e un parametro di tipo che seleziona R in base alla funzione passata.

Interfacce auto-referenziali — Comparable<T>

Uno dei pattern più utili nella libreria standard è l'interfaccia generica auto-referenziale, in cui l'argomento di tipo è la stessa classe che implementa l'interfaccia:

public interface Comparable<T> {
  int compareTo(T other);
}

public class Money implements Comparable<Money> {
  private final long cents;
  // ...
  @Override public int compareTo(Money other) {
    return Long.compare(this.cents, other.cents);
  }
}

Leggi class Money implements Comparable<Money> come "Money sa come confrontarsi con altri Money." Questo è ciò che fa funzionare Collections.sort(List<Money> list) senza un Comparator — ogni elemento porta già un compareTo(Money) ereditato dal contratto dell'interfaccia, e il sistema di tipi assicura che l'argomento abbia lo stesso tipo del ricevitore.

Comparable<T> è l'esempio canonico di questa struttura — ogni tipo di valore nel JDK che ha un ordine naturale lo implementa: Integer implements Comparable<Integer>, String implements Comparable<String>, LocalDate implements Comparable<LocalDate>, e così via.

Ereditare da un'interfaccia generica

Le stesse tre scelte compaiono per l'ereditarietà interfaccia-interfaccia — extends invece di implements, ma le regole sono le stesse:

// Pin the parameter.
public interface StringList extends List<String> { ... }

// Pass it through.
public interface MyList<E> extends List<E> { ... }

// Add new ones.
public interface IndexedList<E, I> extends List<E> { I indexOf(E e); }

Stessa idea che per le classi — il parametro del genitore deve essere fornito (con un tipo reale o uno inoltrato), e il figlio può aggiungere i propri parametri in aggiunta.

I metodi default possono usare il parametro di tipo

Java 8 ha aggiunto i metodi default alle interfacce. Possono usare il parametro di tipo dell'interfaccia esattamente come qualsiasi metodo astratto:

public interface Container<T> {
  void add(T item);
  T    get(int index);
  int  size();

  default boolean isEmpty()        { return size() == 0; }
  default void addAll(Iterable<T> items) {
    for (T item : items) add(item);
  }
}

Il metodo default addAll funziona per ogni implementatore, indipendentemente da quale T ha scelto. È così che Collection<E> fornisce forEach, removeIf, stream e simili — un unico corpo default, ogni implementazione lo ottiene.

Un esempio pratico: un'interfaccia Repository generica

Una piccola astrazione repository — interfaccia e due implementazioni. La prima implementazione fissa il tipo di entità (UserRepo contiene solo utenti); la seconda resta generica (InMemoryRepo<E> contiene qualsiasi cosa il chiamante richieda). Entrambe soddisfano lo stesso contratto dal lato del chiamante.

java— editable, runs on the server

InMemoryRepo<E> è la forma riutilizzabile — il parametro di tipo viene inoltrato dall'interfaccia alla classe, quindi lo stesso corpo funziona per User, String o qualsiasi altra cosa. UserRepo è la forma specializzata — fissa E a User e poi aggiunge metodi che hanno senso solo per gli utenti. Entrambi rispettano lo stesso contratto Repository<E>, e entrambi ereditano isEmpty() gratuitamente dal metodo default.

Cosa c'è dopo

Finora ogni parametro di tipo è stato completamente illimitato — T poteva essere qualsiasi cosa. In pratica spesso vuoi dire "T deve essere un Number" o "T deve implementare Comparable," in modo da poter chiamare effettivamente dei metodi su di esso nel corpo. È a questo che servono i parametri di tipo limitati, e sono il capitolo successivo. Continua con Java Bounded Type Parameters.

Pratica

Pratica
Hai `interface Repository<E> { E find(int id); }` e `class UserRepo implements Repository<User>`. Un chiamante scrive `Repository r = new UserRepo();` (senza argomento di tipo) e poi `User u = r.find(1);`. Qual è il problema?
Hai `interface Repository<E> { E find(int id); }` e `class UserRepo implements Repository<User>`. Un chiamante scrive `Repository r = new UserRepo();` (senza argomento di tipo) e poi `User u = r.find(1);`. Qual è il problema?
Was this page helpful?