Programmazione generica in Java
La programmazione generica in Java èstata resa possibiledai cosiddetti generici a partire da Java 1.5. Il termine è sinonimo di "tipi parametrizzati". L'idea alla base di questo è di introdurre variabili aggiuntive per i tipi. Queste variabili di tipo rappresentano tipi sconosciuti al momento dell'implementazione. Queste variabili di tipo vengono sostituite da tipi specifici solo quando vengono utilizzate classi, interfacce e metodi. In questo modo è generalmente possibile garantire una programmazione indipendente dai tipi. Ma non sempre.
Il concetto
Dalla versione 5.0 ("Tiger", pubblicata nel 2004), il linguaggio di programmazione Java fornisce anche i generici, un mezzo sintattico per la programmazione generica . Ciò consente di parametrizzare classi e metodi (metodi anche indipendenti dalle loro classi) con i tipi . Questo apre alcune possibilità simili per il linguaggio che sono paragonabili ai modelli in C ++ .
In linea di principio, tuttavia, esistono differenze significative. Mentre in Java il parametro di tipo è parametrizzato tramite l' interfaccia , in C ++ la parametrizzazione viene eseguita direttamente tramite il tipo del parametro di tipo stesso. Il codice sorgente di un template C ++ deve essere a disposizione dell'utente (ovvero quando viene inserito il parametro type), mentre un tipo Java generico può anche essere pubblicato come bytecode tradotto. Il compilatore produce codice di destinazione duplicato per vari parametri di tipo utilizzati in modo specifico.
Ad esempio, la funzione std :: sort in C ++ offre la possibilità di ordinare tutti i contenitori che offrono determinati metodi (qui specificatamente begin () e end (), ognuno dei quali restituisce un iteratore) e i cui parametri di tipo implementano l'operatore <'(o un'altra funzione di confronto è stata esplicitamente specificata). Uno svantaggio di questo sistema è che è più difficile (per il programmatore!) Tradurre. Il compilatore non ha altra opzione che sostituire il parametro di tipo in ogni caso con il tipo specifico richiesto e ricompilare l'intero codice.
In caso di parametri di tipo inappropriati e altri problemi, possono facilmente sorgere messaggi del compilatore complicati e incomprensibili, semplicemente perché i requisiti specifici per i parametri di tipo sono sconosciuti. Lavorare con i modelli C ++ richiede quindi una documentazione completa dei requisiti per un parametro di tipo. Con la metaprogrammazione del modello , la maggior parte dei requisiti (classe di base, disponibilità di metodi, copiabilità, assegnabilità, ecc.) Possono anche essere interrogati in costrutti speciali, il che si traduce in messaggi di errore più leggibili. Sebbene siano conformi agli standard, questi costrutti non sono supportati da tutti i compilatori .
Al contrario, le classi e i metodi generici in Java sono consapevoli dei vincoli sui propri parametri di tipo. Per ordinare una raccolta (senza un comparatore), gli elementi che contiene devono essere di tipo Comparable, ovvero aver implementato questa interfaccia. Il compilatore deve solo verificare se il parametro di tipo è un sottotipo di Comparable e può quindi assicurarsi che il codice sia corretto (cioè il metodo compareTo richiesto è disponibile). Inoltre, lo stesso codice viene utilizzato per tutti i tipi specifici e non viene duplicato ogni volta.
Esempi utili
Un programma ne utilizza uno per memorizzare ArrayListun elenco di messaggi di posta JButtonelettronica.
Finora ArrayList è stato corretto sul tipo Object :
List list = new ArrayList();
list.add(new JButton("Button 1"));
list.add(new JButton("Button 2"));
list.add(new JButton("Button 3"));
list.add(new JButton("Button 4"));
list.add(new JButton("Button 5"));
for (int i = 0; i < list.size(); i++) {
JButton button = (JButton) list.get(i);
button.setBackground(Color.white);
}
Notare la necessaria conversione esplicita del tipo (chiamata anche "cast") e l'incertezza del tipo ad essa associata. Potresti inavvertitamente ArrayListmemorizzare un oggetto nel file che non è un'istanza della classe JButton. Le informazioni sul tipo esatto vengono perse quando vengono inserite nell'elenco, quindi il compilatore non può impedire in fase di esecuzione quando JButtonsi ClassCastExceptionverifica la conversione esplicita del tipo di uno .
Con i tipi generici, puoi eseguire le seguenti operazioni in Java:
List<JButton> list = new ArrayList<JButton>();
list.add(new JButton("Button 1"));
list.add(new JButton("Button 2"));
list.add(new JButton("Button 3"));
list.add(new JButton("Button 4"));
list.add(new JButton("Button 5"));
for (int i = 0; i < list.size(); i++)
list.get(i).setBackground(Color.white);
Non è più necessaria una conversione di tipo esplicita durante la lettura, durante il salvataggio è possibile solo memorizzare JButtons ArrayList nell'elenco .
A partire da Java7, la creazione di istanze di tipi generici è stata semplificata. La prima riga nell'esempio sopra può essere scritta come segue da Java 7:
List<JButton> list = new ArrayList<>();
Combinando tipi generici con i cicli for estesi, l'esempio sopra può essere reso più breve:
List<JButton> list = new ArrayList<>();
list.add(new JButton("Button 1"));
list.add(new JButton("Button 2"));
list.add(new JButton("Button 3"));
list.add(new JButton("Button 4"));
list.add(new JButton("Button 5"));
for (JButton b: list)
b.setBackground(Color.white);
Il codice di esempio seguente fornisce un esempio di una classe generica che contiene due oggetti di qualsiasi tipo ma dello stesso tipo:
public class DoubleObject<T> {
private T object1;
private T object2;
public DoubleObject(T object1, T object2) {
this.object1 = object1;
this.object2 = object2;
}
public String toString() {
return this.object1 + ", " + this.object2;
}
public static void main(String[] args) {
DoubleObject<String> s = new DoubleObject<>("abc", "def");
DoubleObject<Integer> i = new DoubleObject<>(123, 456);
System.out.println("DoubleObject<String> s=" + s.toString());
System.out.println("DoubleObject<Integer> i=" + i.toString());
}
}
Casi di varianza
I seguenti casi di varianza possono essere distinti in Java. Ognuno di essi offre una flessibilità completamente indipendente quando si tratta di tipi generici e sono tutti assolutamente staticamente sicuri.
Invarianza
In caso di invarianza, il parametro type è univoco. In questo modo, l'invarianza offre la massima libertà possibile quando si utilizza il parametro type. Ad esempio, tutte le azioni sono consentite per gli elementi di un ArrayList <Integer> che sono consentite anche quando si utilizza direttamente un singolo intero (incluso l' autoboxing ). Esempio:
List<Integer> list = new ArrayList<Integer>();
// ...
Integer x = list.get(index);
list.get(index).methodeVonInteger();
list.set(index, 98347); // Autoboxing, entspricht Integer.valueOf(98347)
int y = list.get(index); // Auto-Unboxing
Queste possibilità vengono acquistate con poca flessibilità quando si assegnano oggetti della stessa classe generica. Ad esempio, non è consentito quanto segue:
List<Number> list = new ArrayList<Integer>();
anche se Integer è derivato da Number. Il motivo è che il compilatore non può più garantire che non si verifichino errori di tipo. Abbiamo avuto brutte esperienze con gli array che consentono tale assegnazione:
// OK, Integer[] ist abgeleitet von Number[]
Number[] array = new Integer[10];
// ArrayStoreException zur Laufzeit: Double -> Integer sind nicht
// zuweisungskompatibel
array[0] = new Double(5.0);
Covarianza
Gli array sono chiamati covarianti , il che significa:
o più in generale:
Il tipo di matrice si comporta allo stesso modo del parametro di tipo rispetto alla gerarchia di ereditarietà. La covarianza è possibile anche con tipi generici, ma solo con limitazioni in modo che gli errori di tipo possano essere esclusi in fase di compilazione.
Riferimenti necessari con la sintassi ? extends T deve essere contrassegnato esplicitamente come covariante. T è chiamato upper typebound , che è il parametro di tipo più generale consentito.
List<? extends Number> list;
list = new ArrayList<Double>();
list = new ArrayList<Long>();
list = new ArrayList<Integer>();
// Typfehler vom Compiler
list.set(index, myInteger);
// OK aber Warnung vom Compiler: unchecked cast
((List<Integer>) list).set(index, myInteger);
Non è possibile memorizzare elementi in questi elenchi perché, come descritto sopra, questo non è indipendente dai tipi (eccezione: è possibile memorizzare zero ). Si verifica un errore in fase di compilazione. Più in generale, è l'incarico
non autorizzato.
Tuttavia, è possibile leggere ad alta voce elementi:
Number n = list.get(index); // OK
Integer i = list.get(index); // Typfehler: Es muss sich bei '? extends Number'
// nicht um ein Integer handeln.
Integer j = (Integer) list.get(index); // OK
L'incarico
è quindi consentito, ma non l'assegnazione
I generici, come gli array, offrono un comportamento covariante, ma vietano tutte le operazioni non sicure per il tipo.
Contravarianza
La controvarianza descrive il comportamento della gerarchia di ereditarietà del tipo generico rispetto alla gerarchia del relativo parametro di tipo. Applicato all'esempio precedente, ciò significherebbe: un elenco <Numero> sarebbe compatibile con l'assegnazione di un elenco <Doppio>. Questo viene fatto come segue:
List<? super Double> list;
list = new ArrayList<Number>();
list = new ArrayList<Double>();
list = new ArrayList<Object>();
Un oggetto che si comporta in modo controvariante non deve fare ipotesi sulla misura in cui un elemento di tipo V è derivato da T, dove T è il tipo inferiore associato (nell'esempio di ? super Doubleè T Double). Pertanto, gli elenchi di cui sopra non possono essere letti:
// Fehler: 'list' könnte vom Typ List<Object> sein
Number x = list.get(index);
// Fehler: 'list' könnte List<Object> oder List<Number> sein
Double x = list.get(index);
// Die einzige Ausnahme: Objects sind auf jeden Fall in der Liste
Object x = list.get(index);
Non consentito, perché non indipendente dai tipi, lo è anche l'assegnazione ? super T → (derivato da Object)
Non difficile da indovinare: in cambio, un elemento può essere inserito in tale elenco:
List<? super Number> list;
list.add(new Double(3.0)); // OK: 'list' hat immer den Typ List<Number>
// oder List<Basisklasse von Number>. Damit
// ist die Zuweisung Double -> T immer erlaubt.
Polimorfismo parametrico non vincolato
Ultimo ma non meno importante, i generici offrono un comportamento completamente polimorfico. Nessuna dichiarazione sui parametri di tipo può essere fatta qui, perché nessun limite è specificato in nessuna delle due direzioni. Il carattere jolly è stato definito per questo. È rappresentato da un punto interrogativo.
List<?> list;
list = new ArrayList<Integer>();
list = new ArrayList<Object>();
list = new ArrayList<String>();
// ...
Il parametro type stesso non può essere utilizzato qui, poiché non è possibile alcuna istruzione. È consentito solo l'assegnazione T → Oggetto, poiché T è sicuramente un oggetto. In cambio, è garantito che il codice possa funzionare con tutti i Ts.
Questo può essere utile se lavori solo con il tipo generico:
// Keine Informationen über den Typparameter nötig, kann ''beliebige'' Listen
// aufnehmen.
int readSize(List<?> list) {
return list.size();
}
Per chiarire che i caratteri jolly qui non sono necessari e che in realtà non si tratta di alcuna varianza, viene fornita la seguente implementazione della funzione precedente:
<T> int readSize(List<T> list) {
return list.size();
}
link internet
- Ina Brenner: Tutorial sui generici in Java dell'autore di un libro sulla certificazione Java SCJP (tedesco)
- Klaus Kreft, Angelika Langer: Serie di articoli sui generici in Java nello spettro Java (tedesco)
- Capitolo sui tipi di dati generici da Galileo Openbook Java è anche un'isola (tedesco)