Java - generics, reflection e passaggio dei parametri
In questa pagina 5
Questa nota raccoglie le lezioni "Un po' di Java" del corso: tre argomenti che servono per tutto il resto (pattern, test, concorrenza). Chi viene dal C trova le differenze di fondo in Puntatori in CUn puntatore contiene un indirizzo di memoria; operatori & e *, NULL, puntatori come parametri per modificare variabili del chiamante, aritmetica dei puntatori, legame tra array e puntatori, const, puntatori a puntatori.Puntatori in C → e Gestione della memoria in CSegmenti di memoria di un processo (codice, dati statici, stack, heap); durata delle variabili; allocazione dinamica con malloc, calloc, realloc e free; errori classici: memory leak, dangling pointer, double free, buffer overflow; strumenti di controllo.Gestione della memoria in C →; chi viene dal C++ confronti con C++ - funzioni, reference e passaggio dei parametriuna funzione ha parametri formali (0 o più, input) e un valore di ritorno (0 o 1, output); in C++ gli argomenti si passano per valore, cioè il parametro è una variabile locale inizializzata con una copia dell'argomento, e modificarla non cambia l'originale; per oggetti grandi (vector, immagini) la copia costa, e si passa per reference (T&), un alias dell'oggetto del chiamante che permette lettura e scrittura, oppure per const reference (const T&), alias in sola lettura verificata dal compilatore, che accetta anche literal (oggetto temporaneo); regola: valore per oggetti piccoli, const T& per grandi da non modificare, T& solo se la funzione deve davvero modificare o produrre più di un output; il passaggio converte l'argomento nel tipo del parametro come in un'inizializzazione e un cast esplicito (static_cast) rende visibile l'intenzione.C++ - funzioni, reference e passaggio dei parametri → e C++ - template e programmazione genericaun template permette di usare un tipo (o un intero noto in compilazione) come parametro di una classe o di una funzione; il compilatore genera il codice per ogni tipo per cui il template è specializzato (template instantiation, a tempo di compilazione): il codice è efficiente ma gli errori sono lunghi e oscuri, e il compilatore controlla sia il template sia il suo argomento; la notazione è template
Java e C: identità dei dati e cast
Definizione (identità dei dati). In Java ogni oggetto conosce la propria classe a tempo di esecuzione. In C un cast cambia solo l'interpretazione dei byte da parte del compilatore.
- Il cast
(Integer) odi Java viene controllato a tempo di esecuzione: seonon è unIntegersi haClassCastException. Il cast di C non controlla niente. - Un array di byte resta un array di byte (in un driver di rete i 4 byte di un indirizzo IPv4 non diventano un
intper magia): meno efficiente a livello di hardware, ma sicuro. - Linking e caricamento. Il C è dynamically linked (le librerie sono collegate prima dell'esecuzione; con
-staticsi includono nell'eseguibile, che cresce molto di dimensione). Java usa il lazy binding: una classe viene cercata e caricata solo al primo uso, "all'ultimo momento". È la base della riflessione (vedi sotto). - La documentazione del JDK (
docs.oracle.com/en/java/javase/21/docs/api) va consultata da subito: i nomi di classi e metodi si cercano lì.
Passaggio dei parametri
Definizione. In Java il passaggio dei parametri è sempre per valore. Per i tipi primitivi si copia il valore; per gli oggetti si copia il riferimento (l'indirizzo dell'oggetto), quindi il chiamante e il metodo vedono lo stesso oggetto ma hanno due variabili diverse che lo riferiscono.
Conseguenze:
| che cosa fa il metodo | effetto fuori dal metodo |
|---|---|
| modifica un parametro primitivo | nessuno (è una copia) |
riassegna il parametro oggetto (s = new ...) |
nessuno (cambia solo la copia locale del riferimento) |
modifica lo stato dell'oggetto (s.copie = 99) |
visibile al chiamante |
| modifica il contenuto di un array | visibile (gli array sono oggetti) |
"modifica" una String |
nessuno: le stringhe sono immutabili, s + "!" crea un nuovo oggetto |
| scambia due parametri oggetto | nessuno: non si può scrivere swap(a, b) in Java |
Stato in memoria di riassegna(s): prima della chiamata s e il parametro s del metodo puntano entrambi a Scheda(1) sull'heap; dopo s = new Scheda(99) il parametro punta a un nuovo oggetto, la variabile del chiamante no.
| passo | variabile s del chiamante (stack) |
parametro s del metodo (stack) |
heap |
|---|---|---|---|
| ingresso nel metodo | → A | → A | A = Scheda(1) |
dopo s = new Scheda(99) |
→ A | → B | A = Scheda(1), B = Scheda(99) |
| uscita dal metodo | → A | (scompare) | A = Scheda(1); B senza riferimenti |
import java.util.Arrays;
class Scheda {
int copie;
Scheda(int copie) { this.copie = copie; }
@Override public String toString() { return "Scheda(" + copie + ")"; }
}
public class PassaggioParametri {
static void incrementaPrimitivo(int x) { x = x + 1; }
static void riassegna(Scheda s) { s = new Scheda(99); } // cambia la copia del riferimento
static void modifica(Scheda s) { s.copie = 99; } // cambia l'oggetto condiviso
static void scambia(Scheda a, Scheda b) { Scheda t = a; a = b; b = t; } // non scambia nulla fuori
static void riempi(int[] v) { Arrays.fill(v, 7); }
static String allunga(String s) { s = s + "!"; return s; } // le stringhe sono immutabili
public static void main(String[] args) {
int n = 5;
incrementaPrimitivo(n);
System.out.println("n dopo incrementaPrimitivo = " + n);
Scheda s = new Scheda(1);
riassegna(s);
System.out.println("dopo riassegna: " + s);
modifica(s);
System.out.println("dopo modifica: " + s);
Scheda a = new Scheda(1), b = new Scheda(2);
scambia(a, b);
System.out.println("dopo scambia: a = " + a + ", b = " + b);
int[] v = new int[3];
riempi(v);
System.out.println("array dopo riempi: " + Arrays.toString(v));
String t = "ciao";
allunga(t);
System.out.println("t dopo allunga (valore scartato) = " + t);
t = allunga(t);
System.out.println("t dopo t = allunga(t) = " + t);
}
}Uscita:
n dopo incrementaPrimitivo = 5
dopo riassegna: Scheda(1)
dopo modifica: Scheda(99)
dopo scambia: a = Scheda(1), b = Scheda(2)
array dopo riempi: [7, 7, 7]
t dopo allunga (valore scartato) = ciao
t dopo t = allunga(t) = ciao!A differenza del C++ (C++ - funzioni, reference e passaggio dei parametriuna funzione ha parametri formali (0 o più, input) e un valore di ritorno (0 o 1, output); in C++ gli argomenti si passano per valore, cioè il parametro è una variabile locale inizializzata con una copia dell'argomento, e modificarla non cambia l'originale; per oggetti grandi (vector, immagini) la copia costa, e si passa per reference (T&), un alias dell'oggetto del chiamante che permette lettura e scrittura, oppure per const reference (const T&), alias in sola lettura verificata dal compilatore, che accetta anche literal (oggetto temporaneo); regola: valore per oggetti piccoli, const T& per grandi da non modificare, T& solo se la funzione deve davvero modificare o produrre più di un output; il passaggio converte l'argomento nel tipo del parametro come in un'inizializzazione e un cast esplicito (static_cast) rende visibile l'intenzione.C++ - funzioni, reference e passaggio dei parametri →) non esistono parametri per riferimento (int&): per restituire due valori si restituisce un oggetto o un array.
Esempio dal corso (
EsempioPassaggioParametri). Il metodoint func1(String a)dichiaraint zz;e poi faif(zz < 0). Il compilatore rifiuta il programma:error: variable zz might not have been initialized. Le variabili locali non hanno valore iniziale (il compilatore impone l'assegnamento prima dell'uso); invece i campi (comeString s;nella stessa classe) ricevono un valore di default (null,0,false), per cuitoString()restituiscenulleprintlnstampa "null". Il codice del corso va quindi corretto (int zz = 0;) prima di eseguirlo.
Generics
Il problema: contenitori di Object
Una coda che contiene Object accetta qualsiasi cosa, ma ogni elemento letto va riconvertito con un cast, e l'errore di tipo compare solo a tempo di esecuzione. Gli esempi del corso Coda/Main2 inseriscono per errore una Coda dentro la coda di stringhe e al prelievo il programma lancia ClassCastException. I generics spostano il controllo a tempo di compilazione.
Definizione (generics). Una classe o un metodo con uno o più parametri di tipo (
class CodaG<T>): il tipo degli elementi è deciso da chi usa la classe (CodaG<Integer>), e il compilatore rifiuta gli usi incoerenti (ci.aggiungi("30")non compila).
import java.util.NoSuchElementException;
/** Coda circolare senza generics: contiene Object. */
class Coda {
private final Object[] dati;
private int testa, dim;
Coda(int capacita) { dati = new Object[capacita]; }
void aggiungi(Object o) {
if (dim == dati.length) throw new IllegalStateException("coda piena");
dati[(testa + dim) % dati.length] = o;
dim++;
}
Object preleva() {
if (dim == 0) throw new NoSuchElementException("coda vuota");
Object r = dati[testa];
dati[testa] = null; // niente riferimenti rimasti
testa = (testa + 1) % dati.length;
dim--;
return r;
}
int dimensione() { return dim; }
}
/** La stessa coda con un parametro di tipo T. */
class CodaG<T> {
private final T[] dati;
private int testa, dim;
@SuppressWarnings("unchecked")
CodaG(int capacita) {
dati = (T[]) new Object[capacita]; // cast non controllato: vedi la nota sulla type erasure
}
void aggiungi(T o) {
if (dim == dati.length) throw new IllegalStateException("coda piena");
dati[(testa + dim) % dati.length] = o;
dim++;
}
T preleva() {
if (dim == 0) throw new NoSuchElementException("coda vuota");
T r = dati[testa];
dati[testa] = null;
testa = (testa + 1) % dati.length;
dim--;
return r;
}
int dimensione() { return dim; }
}
public class Contenitori {
public static void main(String[] args) {
// 1) senza generics: l'errore compare solo a tempo di esecuzione
Coda c = new Coda(4);
c.aggiungi("10");
c.aggiungi("20");
c.aggiungi(new Coda(1)); // compila senza problemi...
int somma = 0;
try {
while (true) somma += Integer.parseInt((String) c.preleva());
} catch (ClassCastException e) { // ...ma esplode qui, al terzo prelievo
System.out.println("Coda: ClassCastException dopo somma = " + somma);
}
// 2) con i generics: lo stesso errore e' un errore di compilazione
CodaG<Integer> ci = new CodaG<>(4);
ci.aggiungi(10);
ci.aggiungi(20);
// ci.aggiungi("30"); // errore: String non e' convertibile in Integer
int s2 = 0;
while (ci.dimensione() > 0) s2 += ci.preleva(); // niente cast, unboxing automatico
System.out.println("CodaG<Integer>: somma = " + s2);
// 3) type erasure: a tempo di esecuzione il parametro di tipo non esiste piu'
CodaG<String> cs = new CodaG<>(1);
System.out.println("stessa classe a runtime? " + (cs.getClass() == ci.getClass()));
// 4) coda vuota: eccezione non controllata
try {
cs.preleva();
} catch (NoSuchElementException e) {
System.out.println("preleva() su coda vuota: " + e.getMessage());
}
}
}Uscita:
Coda: ClassCastException dopo somma = 30
CodaG<Integer>: somma = 30
stessa classe a runtime? true
preleva() su coda vuota: coda vuotaPunti da ricordare:
- Niente primitivi:
CodaG<int>è illegale, si usano le classi involucro (Integer,Double...). Il passaggio trainteIntegerè automatico (autoboxing e unboxing), ma==tra dueIntegerconfronta i riferimenti:Integer a = 127, b = 127dàtrue(valori in una cache), con128dàfalse. Si usaequals. Il vecchionew Integer(0)è deprecato "for removal": si usaInteger.valueOf(0)o l'autoboxing. - Type erasure. Il compilatore controlla i tipi e poi cancella i parametri: a runtime
CodaG<String>eCodaG<Integer>sono la stessa classe (l'uscita sopra stampatrue). Conseguenze: non si può scriverenew T[n](da qui il cast(T[]) new Object[n]con un avviso unchecked, che si sopprime con@SuppressWarnings("unchecked")perché sappiamo che l'array resta privato), néx instanceof CodaG<String>, né sovraccaricare due metodi che differiscono solo per il parametro di tipo. - Differenza dai template C++ (C++ - template e programmazione genericaun template permette di usare un tipo (o un intero noto in compilazione) come parametro di una classe o di una funzione; il compilatore genera il codice per ogni tipo per cui il template è specializzato (template instantiation, a tempo di compilazione): il codice è efficiente ma gli errori sono lunghi e oscuri, e il compilatore controlla sia il template sia il suo argomento; la notazione è template
(o class T, equivalenti) davanti alla classe o alla funzione, e le funzioni membro fuori dalla classe si scrivono template ): in C++ il compilatore genera una copia di codice per ogni tipo istanziato (e può specializzarlo), in Java esiste un solo codice per tutti i tipi, con controlli statici e cast inseriti dal compilatore.tipo Classe ::nome(...); nelle function template il compilatore deduce gli argomenti dai parametri della chiamata (myMax(i, j) invece di myMax (i, j)), ma due argomenti di tipo diverso per lo stesso T sono un errore; una specializzazione esplicita (template<> ...) fornisce il codice per un tipo particolare; i parametri interi (template<typename T, int N> struct array) danno array di dimensione fissa senza free store, più efficienti e che esprimono concetti come la tripletta RGB; poiché il compilatore deve vedere la definizione completa al punto d'uso, le definizioni si mettono nell'header (spesso in un file .hpp incluso in coda al .h).C++ - template e programmazione generica → - Si può comunque "inquinare" l'heap con un cast non controllato: un elemento di tipo sbagliato entra in una
List<String>tramite un tipo grezzo (List), e l'errore compare alla lettura comeClassCastException. Per questo i tipi grezzi (raw types) vanno evitati:Class<?>, nonClass. - Vincoli e wildcard:
<T extends Comparable<T>>richiede cheTsi possa confrontare con se stesso (permettex.compareTo(y));CodaG<? extends Number>accetta una coda di qualunque sottotipo diNumber, in sola lettura. Un uso completo è nell'esercizio Esercizio - generics e riflessione con la coda e il caricatore di classi (esempi del corso). - Le classi delle liste (
List<String>,ArrayList,ListIterator, come inEsempioLista1del corso) sono tutte generiche; per le strutture dati in sé vedi Liste, pile e codeRipasso degli ADT elementari: lista index-based (array) e position-based (lista doppiamente concatenata con sentinelle), pila LIFO, coda FIFO, deque, iteratori; interfacce, implementazioni e costi; array estendibile e coda circolare.Liste, pile e code → e Realizzare contenitori su array e listeCome si realizza un ADT contenitore partendo da un array: lunghezza logica e capacità con raddoppio (costo ammortizzato), dizionario su array ordinato con ricerca binaria, coda doppia su array circolare, coda con priorità a livelli, ADT costruiti sopra altri ADT (pila di code, pila reversibile); tabella dei costi.Realizzare contenitori su array e liste →.
Riflessione (reflection)
Definizione (riflessione). L'insieme di classi di
java.langejava.lang.reflectcon cui un programma ispeziona e manipola classi, metodi e campi a tempo di esecuzione, anche senza conoscerli a tempo di compilazione.
Mappa delle classi:
| classe | rappresenta | operazioni principali |
|---|---|---|
Class<?> |
un tipo (classe, interfaccia, array) | Class.forName("pacchetto.Nome"), getDeclaredConstructors(), getDeclaredMethods(), getDeclaredFields(), isArray(), componentType() |
Constructor<?> |
un costruttore | getParameterTypes(), newInstance(args) |
Method |
un metodo | getName(), getParameterTypes(), invoke(oggetto, args) (primo argomento null per i metodi statici) |
Field |
un campo | get(oggetto), set(oggetto, valore) |
Punti chiave:
Class.forName("Libro")carica e inizializza la classe: il bloccostatic { ... }viene eseguito a quel punto (nel corso,Pippostampa "Yuk yuk!!!" appena caricata). Si usa nei driver JDBC, nei plugin, nei framework che scoprono classi dal nome in un file di configurazione.Class.newInstance()è deprecato (da Java 9): si ottiene unConstructore si chiamanewInstance, che incapsula le eccezioni del costruttore in unaInvocationTargetException.- Accessibilità. Un costruttore o un campo
privatesi trova (getDeclared...) ma non si usa:IllegalAccessException. ConsetAccessible(true)la riflessione aggiraprivate: è la ragione per cui un Singleton con costruttore privato si può rompere (vedi Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e PrototypeI pattern creazionali isolano la creazione degli oggetti dal codice che li usa: Singleton garantisce un'unica istanza (costruttore privato, metodo statico getInstance; la versione lazy ingenua non è thread-safe, si correggono con synchronized, inizializzazione eager, double-checked locking con volatile, holder idiom o enum); Factory Method lascia alle sottoclassi la scelta della classe da istanziare; Abstract Factory crea famiglie coerenti di prodotti correlati tramite un'interfaccia di fabbrica; Builder costruisce passo passo oggetti complessi con campi facoltativi; Prototype crea oggetti clonando un prototipo (copia profonda delle parti mutabili); la Simple Factory non è un pattern GoF ma sposta il codice di creazione in una classe sola.Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e Prototype →); i costruttori deglienumfanno eccezione. Una classe senzapublicnon è accessibile da un altro pacchetto (Ciccianel corso). - I metodi sono identificati da nome e tipi dei parametri:
getDeclaredMethod("maiuscolo", String.class, int.class);int.classè il tipo primitivo,Integer.classquello involucro, e sono diversi (nel corsoCoccodrillohasomma(Integer, Integer)esomma(int, int)).
Eccezioni più frequenti:
| eccezione | quando |
|---|---|
ClassNotFoundException |
il nome non corrisponde a nessuna classe nel classpath (il nome va completo di pacchetto: altro.Ciccia2) |
NoSuchMethodException |
non esiste il costruttore o metodo con quei tipi di parametro (per esempio manca il costruttore senza parametri) |
InstantiationException |
la classe è astratta o un'interfaccia |
IllegalAccessException |
membro non accessibile (private, classe non pubblica) |
InvocationTargetException |
il costruttore o il metodo invocato ha lanciato un'eccezione: la causa è in getCause() |
IllegalArgumentException |
argomenti di numero o tipo sbagliati, oppure valore non assegnabile al campo |
import java.io.IOException;
import java.lang.reflect.*;
import java.util.Arrays;
class Libro {
static int istanze = 0;
String titolo;
static {
System.out.println(" [static] la classe Libro e' stata caricata");
}
public Libro() { this("senza titolo"); }
public Libro(String titolo) {
this.titolo = titolo;
istanze++;
}
public int lunghezzaTitolo() { return titolo.length(); }
public String maiuscolo(String prefisso, int volte) {
return (prefisso + titolo).repeat(volte).toUpperCase();
}
@Override public String toString() { return "Libro<" + titolo + ">"; }
}
class Segreto {
private Segreto() { System.out.println(" [Segreto] costruttore privato eseguito"); }
}
class Fragile {
public Fragile() throws IOException { throw new IOException("disco non pronto"); }
}
public class Riflessione {
public static void main(String[] args) throws Exception {
// 1) Class.forName carica la classe e ne esegue l'inizializzatore statico
System.out.println("prima di forName");
Class<?> c = Class.forName("Libro");
System.out.println("dopo forName: " + c);
// 2) elenco dei costruttori
Constructor<?>[] costr = c.getDeclaredConstructors();
Arrays.sort(costr, (a, b) -> a.getParameterCount() - b.getParameterCount());
for (int i = 0; i < costr.length; i++) System.out.println(" #" + i + " " + costr[i]);
// 3) creazione dinamica con il costruttore a un parametro String
Constructor<?> k = c.getDeclaredConstructor(String.class);
Object o = k.newInstance("Il nome della rosa");
System.out.println("creato: " + o);
// 4) invocazione dinamica di metodi (anche con parametri)
Method m1 = c.getDeclaredMethod("lunghezzaTitolo");
System.out.println("lunghezzaTitolo() = " + m1.invoke(o));
Method m2 = c.getDeclaredMethod("maiuscolo", String.class, int.class);
System.out.println("maiuscolo(\"x\",2) = " + m2.invoke(o, "x", 2));
// 5) lettura e scrittura di un campo (stringa) e di uno statico
Field f = c.getDeclaredField("titolo");
f.set(o, "Il pendolo di Foucault");
System.out.println("dopo f.set: " + o);
Field n = c.getDeclaredField("istanze");
System.out.println("istanze (campo statico, obj = null) = " + n.get(null));
// 6) costruttore privato: lo trovo, ma non posso usarlo...
Constructor<?> p = Class.forName("Segreto").getDeclaredConstructor();
try {
p.newInstance();
} catch (IllegalAccessException e) {
System.out.println("IllegalAccessException: " + e.getMessage());
}
// ...a meno di forzare l'accesso (la riflessione puo' aggirare private)
p.setAccessible(true);
p.newInstance();
// 7) il costruttore lancia: l'eccezione arriva incapsulata
try {
Class.forName("Fragile").getDeclaredConstructor().newInstance();
} catch (InvocationTargetException e) {
System.out.println("InvocationTargetException, causa: " + e.getCause());
}
// 8) classe inesistente
try {
Class.forName("Inesistente");
} catch (ClassNotFoundException e) {
System.out.println("ClassNotFoundException: " + e.getMessage());
}
}
}Uscita (l'ordine di getDeclaredConstructors non è garantito dalla JVM: il programma lo ordina per numero di parametri):
prima di forName
[static] la classe Libro e' stata caricata
dopo forName: class Libro
#0 public Libro()
#1 public Libro(java.lang.String)
creato: Libro<Il nome della rosa>
lunghezzaTitolo() = 18
maiuscolo("x",2) = XIL NOME DELLA ROSAXIL NOME DELLA ROSA
dopo f.set: Libro<Il pendolo di Foucault>
istanze (campo statico, obj = null) = 1
IllegalAccessException: class Riflessione cannot access a member of class Segreto with modifiers "private"
[Segreto] costruttore privato eseguito
InvocationTargetException, causa: java.io.IOException: disco non pronto
ClassNotFoundException: InesistenteI caricatori interattivi del corso
Gli esempi Loader, Loader2... Loader7 (e quelli della dispensa) sono la stessa idea sviluppata per gradi: leggono da tastiera il nome di una classe e fanno qualcosa con essa.
| programma | cosa aggiunge |
|---|---|
Loader |
Class.forName sul nome passato come argomento e stampa della classe |
Loader2/Loader3 |
il nome si legge da tastiera in un ciclo; Loader3 crea l'oggetto (newInstance) e stampa toString() |
Loader4 |
usa getDeclaredConstructor() e gestisce NoSuchMethodException (classe senza costruttore senza parametri) e InvocationTargetException |
Loader5 |
elenca tutti i costruttori e fa scegliere; per ogni parametro chiama se stesso ricorsivamente (myInstantiate) per creare l'oggetto da passare |
Loader6 |
dopo la creazione elenca i metodi (getDeclaredMethods) e ne esegue uno con invoke |
Loader7 |
stampa il valore restituito; per i parametri array usa Array.newInstance(formals[i].componentType(), 0) |
| versioni della dispensa | cambiare un campo con Field.set; scelta del metodo con nome e tipi dei parametri letti da tastiera |
Le classi di prova mostrano i casi limite: Ciccia2 ha il costruttore private e un blocco statico che stampa al caricamento; Ciccia3 ha un costruttore che lancia IOException; Insta1 ha un costruttore che richiede un Insta2 (da qui la ricorsione); ArrayDentro ha un costruttore con Integer[]; Coccodrillo ha tre costruttori, di cui uno che lancia sempre.
Costi e usi
- Sicurezza di tipo: quello che il compilatore controllava ora si scopre a runtime (le eccezioni sopra); errori di battitura nei nomi di metodo diventano
NoSuchMethodException. - Prestazioni: più lenta della chiamata normale.
- Incapsulamento:
setAccessibleaggiraprivate. - Dove si usa: framework che trovano classi e metodi dal nome (JUnit trova i metodi con
@Testcosì, vedi Software testing - progettazione dei casi di test e JUnitIl testing esegue il programma con dati artificiali per mostrare che soddisfa i requisiti (validation testing) e per scoprirne i difetti (defect testing), ma può rivelare la presenza di errori e non la loro assenza; il test di sviluppo comprende unit test (singole classi e metodi), test dei componenti (le interfacce) e test di sistema (le interazioni); i casi di test si ricavano con le classi di equivalenza (una per partizione) e i valori limite (ai bordi delle partizioni), più linee guida per sequenze e input anomali; JUnit automatizza i test con @Test, fixture @Before/@After (@BeforeClass/@AfterClass), le asserzioni (assertEquals con atteso per primo), eccezioni attese, timeout e @Ignore; nel TDD il test si scrive prima del codice (red, green, refactor) e i test automatici diventano la suite di regressione; seguono i test di rilascio (black box sui requisiti, prestazioni) e dell'utente (alpha, beta, accettazione).Software testing - progettazione dei casi di test e JUnit →), i driver JDBC, i proxy dinamici (java.lang.reflect.Proxy, nel pattern Proxy di Pattern strutturali - Adapter, Decorator, Composite, Facade e ProxyI pattern strutturali compongono classi e oggetti in strutture più grandi: Adapter converte l'interfaccia di una classe in quella attesa dal client (di oggetto, per composizione, o di classe, per ereditarietà multipla di classe e interfaccia); Decorator aggiunge responsabilità a un oggetto con un involucro che ha la stessa interfaccia (l'ordine dei decoratori conta); Composite tratta in modo uniforme foglie e contenitori in una struttura ad albero; Facade offre un'interfaccia semplice a un sottosistema (non cerca il polimorfismo, a differenza dell'Adapter); Proxy è un sostituto con la stessa interfaccia che controlla l'accesso (virtuale, di protezione, di cache, dinamico); Bridge e Flyweight completano le sette forme del catalogo.Pattern strutturali - Adapter, Decorator, Composite, Facade e Proxy →), la serializzazione, i contenitori di dependency injection.
Domande d'esame
1. Un metodo Java riceve un oggetto e fa p = new Persona("X");. Il chiamante vede il cambiamento? E se il metodo fa p.setNome("X")?
Traccia. Il passaggio è per valore del riferimento: la riassegnazione cambia solo la copia locale e il chiamante non vede nulla; p.setNome("X") modifica l'oggetto condiviso e il chiamante lo vede. Per le String (immutabili) anche l'equivalente di setNome non esiste: occorre restituire il nuovo valore.
2. Perché CodaG<String> e CodaG<Integer> hanno la stessa classe a runtime? Che cosa non si può fare per questo?
Traccia. Type erasure: il compilatore verifica i tipi poi li cancella (un solo codice, a differenza dei template C++). Non si possono creare array di T (new T[n] si aggira con (T[]) new Object[n] e un cast non controllato), usare instanceof CodaG<String>, né avere parametri di tipo primitivo.
3. Scrivere in poche righe come creare un oggetto di classe Libro dal nome, con il costruttore che prende una String, e invocarne un metodo String titolo(). Quali eccezioni vanno gestite?
Traccia. Class<?> c = Class.forName("Libro"); Object o = c.getDeclaredConstructor(String.class).newInstance("X"); Object r = c.getDeclaredMethod("titolo").invoke(o);. Eccezioni: ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException (la causa è l'eccezione lanciata dal costruttore o dal metodo).
Versione ripasso
- Java e C: in Java ogni dato ha identità a runtime; il cast è controllato (
ClassCastException), in C cambia solo il tipo visto dal compilatore. Lazy binding: le classi si caricano al primo uso (C: collegamento dinamico prima dell'esecuzione,-staticper includerle). - Parametri: sempre per valore. Primitivi: copia, nessun effetto fuori. Oggetti: si copia il riferimento: riassegnare
s = new ...non si vede fuori;s.campo = ...si vede; array: contenuto visibile;Stringimmutabile (servet = allunga(t)); non esisteswap(a, b)né parametro per riferimento (C++:int&). Variabili locali senza valore iniziale (erroremight not have been initialized), i campi hanno il default.
passo di riassegna(s) |
chiamante | metodo | heap |
|---|---|---|---|
| ingresso | → A | → A | A = Scheda(1) |
dopo s = new Scheda(99) |
→ A | → B | A e B |
static void riassegna(Scheda s) { s = new Scheda(99); } // il chiamante NON vede
static void modifica(Scheda s) { s.copie = 99; } // il chiamante vede
static void scambia(Scheda a, Scheda b) { Scheda t = a; a = b; b = t; } // non scambia nulla fuoriUscita del programma completo: n dopo incrementaPrimitivo = 5; dopo riassegna: Scheda(1); dopo modifica: Scheda(99); dopo scambia: a = Scheda(1), b = Scheda(2); array dopo riempi: [7, 7, 7]; t dopo allunga (valore scartato) = ciao; t dopo t = allunga(t) = ciao!.
- Generics:
class CodaG<T>; l'errore di tipo diventa errore di compilazione; niente cast. Erasure: a runtimeCodaG<String>eCodaG<Integer>sono la stessa classe; niente primitivi (Integer, autoboxing;==traIntegerconfronta riferimenti, 127 sì, 128 no); nientenew T[n]((T[]) new Object[n]+@SuppressWarnings("unchecked")); nienteinstanceof CodaG<String>. Non sono template C++ (un solo codice contro una copia per tipo). Evitare i tipi grezzi (heap pollution, errore alla lettura).<T extends Comparable<T>>: vincolo;? extends Number: sola lettura di qualunque sottotipo.
CodaG<Integer> ci = new CodaG<>(4);
ci.aggiungi(10); // ci.aggiungi("30") non compila
int s = ci.preleva(); // niente cast: unboxing automatico- Riflessione:
Class.forName("pacchetto.Nome")carica e inizializza (eseguestatic { });getDeclaredConstructors/Methods/Fields;Constructor.newInstance(args);Method.invoke(obj, args)(nullper gli statici);Field.get/set.Class.newInstance()è deprecato. Metodi identificati da nome + tipi (int.classdiverso daInteger.class).getDeclaredConstructorsnon ha ordine garantito.
Class<?> c = Class.forName("Libro");
Object o = c.getDeclaredConstructor(String.class).newInstance("Il nome della rosa");
Object n = c.getDeclaredMethod("lunghezzaTitolo").invoke(o);| eccezione | quando |
|---|---|
ClassNotFoundException |
nome inesistente (serve il nome completo di pacchetto) |
NoSuchMethodException |
costruttore/metodo con quei tipi non c'è |
InstantiationException |
classe astratta o interfaccia |
IllegalAccessException |
private o classe non pubblica |
InvocationTargetException |
il codice invocato ha lanciato: causa in getCause() |
Cosa stampa il programma di riflessione (nell'ordine): prima di forName, poi [static] la classe Libro e' stata caricata (l'inizializzatore statico gira dentro forName), i due costruttori, creato: Libro<Il nome della rosa>, lunghezzaTitolo() = 18, maiuscolo("x",2) = XIL NOME DELLA ROSAXIL NOME DELLA ROSA, istanze (campo statico, obj = null) = 1, IllegalAccessException sul costruttore privato, il costruttore privato eseguito dopo setAccessible(true), InvocationTargetException, causa: java.io.IOException: disco non pronto, ClassNotFoundException: Inesistente.
- Coda di
Objectcontro coda generica (programmaContenitori):Codaaccettanew Coda(1)e fallisce conClassCastExceptional terzo prelievo (somma 30);CodaG<Integer>rifiuta"30"in compilazione e somma 30 senza cast;cs.getClass() == ci.getClass()valetrue;preleva()su coda vuota lanciaNoSuchElementException. - Cosa compilare e cosa no:
CodaG<int>,new T[n],x instanceof CodaG<String>,ci.aggiungi("30")suCodaG<Integer>non compilano;(T[]) new Object[n]compila con avviso (unchecked);CodaG.somma(cs)conCodaG<String>non compila (Stringnon estendeNumber). - Aggirare
private:setAccessible(true). Rompe il Singleton con costruttore privato, non l'enum(Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e PrototypeI pattern creazionali isolano la creazione degli oggetti dal codice che li usa: Singleton garantisce un'unica istanza (costruttore privato, metodo statico getInstance; la versione lazy ingenua non è thread-safe, si correggono con synchronized, inizializzazione eager, double-checked locking con volatile, holder idiom o enum); Factory Method lascia alle sottoclassi la scelta della classe da istanziare; Abstract Factory crea famiglie coerenti di prodotti correlati tramite un'interfaccia di fabbrica; Builder costruisce passo passo oggetti complessi con campi facoltativi; Prototype crea oggetti clonando un prototipo (copia profonda delle parti mutabili); la Simple Factory non è un pattern GoF ma sposta il codice di creazione in una classe sola.Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e Prototype →). - Loader del corso: da
Loader(stampa la classe) aLoader7(costruttori scelti, creazione ricorsiva dei parametri, invocazione di metodi, parametri array conArray.newInstance).Loader/Loader2/Loader3: nome da argomento o da tastiera,newInstance.Loader4:getDeclaredConstructor()e gestione diNoSuchMethodExceptioneInvocationTargetException.Loader5: elenco dei costruttori; per ogni parametro richiamamyInstantiate(ricorsione).Loader6/Loader7: elenco dei metodi,invoke, stampa del risultato, array.- Classi di prova:
Ciccia2(costruttore privato),Ciccia3(costruttore che lancia),Insta1(Insta2),ArrayDentro(Integer[]),Coccodrillo(tre costruttori,somma(Integer, Integer)esomma(int, int)).
- Dove serve: JUnit (cerca
@Test), JDBC (Class.forNamedel driver), proxy dinamici, dependency injection. Costi: errori a runtime, più lenta, incapsulamento aggirato. - Errori tipici: credere che
s = new ...nel metodo cambisdel chiamante; confrontareIntegercon==; aspettarsi codice diverso perCodaG<String>eCodaG<Integer>; dimenticare cheforNameesegue il blocco statico; non scartareInvocationTargetExceptionper leggerne la causa; usareClassgrezzo invece diClass<?>.
Esercizi su questo argomento
Teoria collegata
- C++ - template e programmazione generica
- Ingegneria del software - prodotto software e processo di sviluppo
- Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e Prototype
- Pattern strutturali - Adapter, Decorator, Composite, Facade e Proxy
- Software testing - progettazione dei casi di test e JUnit