![]() |
Università
|
Diploma in Ingegneria Informatica
|
A.A. 2001/2002
Versione 1.0 27/09/2001
Le classi interne di Java, ovvero classi definite all'interno di altre classi, sono state introdotte a partire dal JDK 1.1.x per consentire un maggior controllo nella visibilità di classi ad uso molto ristretto (semplici helper o adattatori), inserendo queste ultime nello specifico contesto in cui debbono operare, spesso attraverso una singola istanza (oggetto). Limitando la visibilità della classe interna si ottiene un codice più chiaro, si riduce la quantità di nomi esportati dal package di appartenenza, e se ne fa un uso particolarmente vantaggioso nel caso dell'interfaccia grafica (ad esempio per difinire gli ascoltatori di eventi).
Esistono 4 tipi di classi interne:
static
e agisce come una classe o
interfaccia 'normale' a parte che il nome che la identifica include la classe
contenente (ad es., se nella classe 'normale' A del package P è definita
la classe top-level B, il suo nome completo è P.A.B).
final
visibile nel blocco; è utile come classe adattatore (adapter), in
particolare per gli ascoltatori di eventi (listener o callback).
La JVM non ha subito variazioni con l'introduzione delle classi interne: ciò comporta,
per esempio, che, mentre la classe A
di primo livello produce il compilato A.class
,
la classe A.B
produca A$B.class
, e similmente per la trasformazione
di altri nomi dovuta al nesting. In qualche caso il compilatore deve anche
generare codice aggiuntivo per consentire alcune particolari regole di
visibilità. Comunque il programmatore non è tenuto
a preoccuparsi di queste trasformazioni automaticamente operate dal compilatore.
static
(per le interfacce, è implicito). Possono essere richiamate sia dalla classe
contenente che da altre classi esterne, eventualmente con l'uso dell'istruzione
import
se da un file diverso, cosa che rende non più obbligatoria
la qualificazione con il nome della classe contenente, similmente a quanto accade con i
package.
Esempio di interfaccia interna top-level:
public class LinkedList { // interfaccia (statica) locale public interface Linkable { public Linkable getNext(); public void setNext(Linkable node); } // altri membri della classe contenente Linkable head; // testa della lista // metodi public void insert(Linkable node) { ... } } //{c} LinkedList // in altro file import LinkedList.*; class LinkableFloat implements Linkable { float f; public LinkableFloat(float f) { this.f = f; } // implementazione dell'interfaccia Linkable next; public Linkable getNext() { return next; } .... } //{c} LinkableFloatSi noti che con l'uso di
import
si può citare solo Linkable
anziché LinkedList.Linkable
.
Non sono dichiarate con l'attributo static
e la loro introduzione
aggiunge effettivamente una semantica nuova. Infatti ogni oggetto istanza della
classe interna è associato implicitamente ad uno specifico oggetto
della classe contenente, con una relazione che è in generale molti-a-uno. Quando chiamando
il metodo a.m()
sull'oggetto a
di classe A
al suo interno viene creato l'oggetto b
della classe B
membro interno di A
, l'oggetto b
è implicitamente
associato ad a
. Ogni successiva chiamata a.m()
crea
oggetti di B
che sono associati ad a
. L'oggetto b
attraverso i suoi metodi può quindi accedere ai membri anche privati dell'oggetto
associato a
(oltre naturalmente ai propri membri). Questa associazione automatica,
che evita di passare esplicitamente ai metodi dell'oggetto 'incluso' un riferimento
all'oggetto 'includente', rende elegante il codice e pratica la definizione di classi
helper o adattatori.
L'accesso, da parte dell'oggetto incluso, ai membri dell'oggetto includente
può dare qualche problema di sintassi. Anche se è consentito
che avvenga con la semplice citazione del nome del membro, così come
avviene normalmente con i membri propri, in caso di conflitto (lo stesso nome
per un membro proprio e uno dell'oggetto includente) non è possibile
utilizzare la qualificazione con this
che vale per i membri
propri. A tale scopo, la sintassi viene estesa: nell'esempio sopra, se entrambe le
classi A
e B
hanno un membro x
, nel metodo
b.f()
un riferimento a quello in B
è dato
da x
oppure da this.x
, mentre uno a quello di A
è dato da A.this.x
, cioè anteponendo anche il nome
della classe includente.
Un'altra estensione della sintassi riguarda la possibilità
di specificare esplicitamente l'associazione oggetto incluso-oggetto includente.
Nel caso della creazione di un oggetto incluso non da un metodo di un oggetto
includente, l'operatore new
di creazione va corredato dell'indicazione
dell'oggetto includente. Per l'esempio sopra la sintassi è a.new B()
(si noti, non a.new A.B()
). All'interno di un metodo di B
è invece possibile usare solo new
oppure, coerentemente, this.new
.
Se il nesting è multiplo, ci si riferisce sempre all'oggetto immediatamente
contenente. Se ad esempio la classe membro C
è definita in B
, si
può scrivere:
A a = new A(); A.B b = a.new B(); A.B.C c = b.new C();Vi sono anche alcuni vincoli: la classi membro non possono avere lo stesso nome di altre classi o package, e non possono contenere membri statici (come anche quelle locali ed anonime), questi ultimi avendo senso solo in classi di primo livello.
Esempio di enumeratore come classe membro:
public class LinkedList { .... // metodi public void insert(Linkable node) { ... } public Enumeration enums() { return new Enum(); } // creatore dell'enumeratore private class Enum implements Enumeration { Linkable current; public Enum() { current = head; } // equivalente a: // this.current = LinkedList.this.head; ..... } //{c} EnumSi noti che nell'esempio la classe membro
Enum
è dichiarata private
e pertanto non è possibile creare enumeratori al di fuori della classe
LinkedList
se non chiamando il metodo enums()
. Se invece
la visibilità fosse package, all'interno di quest'ultimo ma fuori
da LinkedList
si potrebbero utilizzare le istruzioni:
LinkedList li = new LinkedList(); Enumeration e = li.new Enum();
Una classe locale viene definita all'interno di un metodo, di un inizializzatore
statico e di un inizializzatore d'istanza di classe (vedi).
Esiste un principio di associazione oggetto incluso-oggetto includente del
tutto analogo ad una classe membro. Una classe locale è però utilizzabile
sono nel blocco di codice ove è definita. Per motivi di implementazione,
oltre ai membri della classe contenente, può utilizzare anche
variabili e parametri di chiamata locali ma solo se hanno l'attributo final
,
quest'ultima essendo un'ulteriore estensione di Java 1.1.
La regola di visibilità porta ad utilizzare questo tipo di classi
quando sono usate in un solo metodo o blocco di codice. Una classe locale può
utilizzare la sintassi di this
vista sopra per le classi membro
ma non quella di new
. Non puņ includere membri statici né interfacce
interne (che sono come visto implicitamente statiche) e non possono avere
alcun modificatore (public
, protected
, private
)
esattamente come accade per le variabili locali.
Esempio di ascoltatore di un evento come classe locale:
public class GUI extends Frame { SomeApplication someObject; .... MenuItem createMItem(final int command, ...) { MenuItem m = new MenuItem(...); // classe locale ascoltatore class MenuItemListener implements ActionListener { // implementa il metodo d'interfaccia public void actionPerformed(ActionEvent e) { someObject.someAction(command); } } //{c} MenuItemListener // registra l'ascoltatore m.addActionListener(new MenuItemListener()); return m; } //[m] createMItem } //{c} GUISi noti che
command
è un parametro final
del
metodo all'interno del quale la classe locale è definita, mentre
someObject
, essendo un membro della classe includente, può
non essere final
.
Una classe anonima differisce da una classe locale solo perché
non c'è una dichiarazione con nome della classe ma, in pratica,
solo un'istruzione con l'operatore new
seguito dal corpo della classe
anonima (tra parentesi graffe), eventualmente preceduto da un nome di classe
o di interfaccia, seguito da parentesi tonde, se rispettivamente la classe
anonima estende quella classe o implementa quell'interfaccia. Ad esempio la
scrittura new A(5) { ... }
indica la creazione di un oggetto
di classe anonima che estende la classe A
, passando come
parametro di un suo costruttore il valore 5; il corpo della classe anonima
è in parentesi graffe. Non potendo esplicitare le parole chiave
extends
o implements
, se la classe anonima implementa
un'interfaccia, può discendere solo da Object
, come
per esempio nell'istruzione new Sequence() { ... }
L'assenza di un nome per la classe ha come svantaggio quello di non poter definire
per la classe alcun costruttore; inoltre costringe il compilatore a nominare
il compilato di una classe anonima definita nella classe B
come
B$n
ove n è un numero progressivo assegnato alle classi
anonime. È conveniente che la scrittura di una classe anonima segua
semplici regole di indentazione utili alla leggibilità del codice.
Esempio di ascoltatore di un evento come classe anonima:
public class GUI extends Frame { SomeApplication someObject; .... MenuItem createMItem(final int command, ...) { MenuItem m = new MenuItem(...); // registra l'ascoltatore m.addActionListener(new ActionListener() { // classe anonima ascoltatore // implementa il metodo d'interfaccia public void actionPerformed(ActionEvent e) { someObject.someAction(command); } } /*{c} classe anonima ascoltatore */ ); return m; } //[m] createMItem } //{c} GUIL'assenza di costruttori nelle classi anonime viene compensata da una estensione di Java 1.1, utilizzabile in qualsiasi tipo di classe, data dall'inizializzatore d'istanza. Si tratta di un blocco di codice, inserito tra graffe, posto nel corpo di una classe e che viene eseguito alla creazione di ogni istanza della classe, dopo aver chiamato il costruttore della superclasse ma prima di eseguire quello della classe. Una classe può definire un numero arbitrario di questi blocchi, che vengono eseguiti nell'ordine in cui compaiono.
Esempio di uso di inizializzatori d'istanza:
public class Indici { // variabile d'istanza public int[] a1; // inizializzatore di istanza { a1 = new int[10]; for (int i=0; i<10; i++) a1[i] = i; } // variabile d'istanza public int[] a2; // inizializzatore di istanza { a2 = new int[10]; for (int i=0; i<10; i++) a2[i] = (i<<1); // i*2 } .... } //{c} Indici
Altre estensioni di Java 1.1 che vengono qui solo brevemente citate sono:
catch
possono avere
l'attributo final
, il vincolo di inserire per le variabili final
l'inizializzatore assieme alla dichiarazione è stato rimosso e sostituito
dal vincolo che la variabile final deve essere inizializzata prima del suo
primo utilizzo, ad esempio in un costruttore, e da allora non è più
modificabile.
new
che consente di
esplicitare una lista di espressioni di inizializzazione degli elementi di un array
non contestualmente alla dichiarazione dell'array. Ad esempio è ora consentito
scrivere entrambe queste forme:
int[] a = {1, 2, 3}; int[] b; b = new int[] {1, 2, 3}; // array anonimo
class
si può ottenere un oggetto Class
che fornisce la descrizione della
classe a cui l'operatore è applicato: per esempio, String.class
fornisce un oggetto che descrive la classe String
. In Java 1.1 questo è
possibile anche per i tipi primigenî, ad esempio int.class
. Poiché
per questi tipi le descrizioni sono predefinite, vengono rese disponibili alcune classi
literal che rappresentano queste descrizioni; ad esempio nel caso di int
la classe literal di descrizione è data da Integer.TYPE
,
nel caso float
da float.TYPE
, nel caso void
da
Void.TYPE
.
Ulteriori informazioni sono reperibili nella documentazione allagata al JDK ed in particolare in docs/guide/innerclasses/index.html.
In alcuni algoritmi di ordinamento si ottiene un miglioramento, più o meno sensibile, che si traduce normalmente nella eliminazione di un confronto ripetuto, inserendo ad uno dei capi della lista da ordinare un elemento aggiuntivo denominato sentinella. La caratteristica di questo elemento è che il confronto della sua chiave con quella di qualsiasi altro elemento della lista fa terminare il ciclo che è basato su tale confronto. Ad esempio, nell'algoritmo di insertion sort (vedi FI2.Sorting.ArraySort.insertionSort()) nel ciclo più interno:
while (i >= 0 && vec[i].greater(key))
il confronto i>=0 potrebbe essere eliminato se l'elemento vec[0] fosse una sentinella la cui chiave si sa per certo essere <= di quella di ogni altro elemento effettivo, in quanto ciò farebbe terminare il ciclo. Se il numero di elementi è elevato e il confronto tra chiavi di complessità confrontabile con quello tra interi (i>=0), l'eleminazione di quest'ultimo può comportare un miglioramento anche sensibile. Può però non essere agevole imporre la presenza della sentinella: intanto, deve occupare una posizione estrema dell'array; poi, per verificare la condizione attesa, o la sua chiave è posta ad ogni ciclo pari alla chiave di key, oppure è sempre pari ad un valore maggiore di qualsiasi chiave effettiva, valore che non è sempre possibile determinare.
La seguente tabella è è ricavata dal libro di N.Wilt "Classical Algorithms in C++", John Wiley & Sons, New York, 1995, ed è stata calcolata utilizzando un processore Digital Alpha con sistema operativo Windows NT e un real-time process cycle counter della stessa risoluzione del clock del processore (233 MHz corripondente a 4.3 ns) e fornisce il valore medio calcolato su un ampio ventaglio di casi (da 10 a 100000 elementi, inizialmente ordinati a caso) senza uso di memoria virtuale.
Algoritmo | Durata (in cicli di clock) |
---|---|
Insertion Sort | 1.93 N² |
Selection Sort | 4.84 N² |
Shell Sort | 38.1 N lg² N º |
Recursive Merge Sort | 38.2 N lg N |
Iterative Merge Sort | 44.4 N lg N |
Recursive Quicksort | 20.2 N lg N |
Full-blown Quicksort (cutoff=10) | 17.8 N lg N |
Heapsort | 65.4 N lg N |
º Il comportamento asintotico di shell sort non è noto ma la curva N lg² N sembra approssimare meglio i dati piuttosto che N1.25
In Java 1.1 il modello di gestione degli eventi è detto 'a delegazione' poiché si basa sull'uso di 'classi ascoltatore' che, attarverso uno o più dei loro metodi, fanno la funzione del meccanismo di callback presente in altri ambienti grafici e facilmente realizzabile in C/C++ con l'uso dei puntatori a funzione. In estrema sintesi l'oggetto che riceve l'evento, e che è tipicamente l'immagine di un elemento dell'interfaccia grafica (bottone, area di testo, ecc.), ha pre-registrato presso di sè un certo numero di oggetti ascoltatore: se fra questi ve n'è uno adatto al tipo di evento prodottosi, quest'ultimo viene passato all'ascoltatore attivando l'apposito metodo definito nell'interfaccia dell'ascoltatore stesso, altrimenti l'evento viene ignorato.
Nella seguente tabella sono elencati per ogni classe dell'interfaccia grafica gli eventi che possono essere generati e i relativi ascoltatori con la lista dei metodi da implementare; per alcuni ascoltatori sono anche citate le classi adattatore che consentono di non dover implementare tutti i metodi dell'interfaccia dell'ascoltatore se si interessa gestire solo un sottoinsieme degli eventi 'ascoltabili'. Gli eventi sono divisi tra eventi a basso livello (low-level) e eventi semantici.
Componente | Evento | Ascoltatore | Adattatore | Metodi | Commento |
---|---|---|---|---|---|
Component |
ComponentEvent |
ComponentListener |
ComponentAdapter |
componentHidden() |
comp. nascosto |
componentMoved() |
comp. mosso | ||||
componentResized() |
comp. modificato | ||||
componentShown() |
comp. mostrato | ||||
FocusEvent |
FocusListener |
FocusAdapter |
focusGained() |
comp. con focus | |
focusLost() |
comp. senza focus | ||||
KeyEvent |
KeyListener |
KeyAdapter |
keyPressed() |
tasto premuto | |
keyReleased() |
tasto rilasciato | ||||
keyTyped() |
tasto premuto e rilasciato | ||||
MouseEvent |
MouseListener |
MouseAdapter |
mouseClicked() |
tasto mouse premuto e rilasciato | |
mouseEntered() |
mouse entro un comp. | ||||
mouseExited() |
mouse fuori di un comp. | ||||
mousePressed() |
tasto mouse premuto | ||||
mouseReleased() |
tasto mouse rilasciato | MouseMotionListener |
MouseMotionAdapter |
mouseDragged() |
mouse trascinato |
mouseMoved() |
mouse mosso su un comp. | ||||
Container |
ContainerEvent |
ContainerListener |
ContainerAdapter |
componentAdded() |
comp. aggiunto |
componentRemoved() |
comp. eliminato | ||||
Window (Dialog and Frame) |
WindowEvent |
WindowListener |
WindowAdapter |
windowActivated() |
finestra attivata |
windowClosed() |
finestra chiusa | ||||
windowClosing() |
finestra in chiusura | ||||
windowDeactivated() |
finestra disattivata | ||||
windowDeiconified() |
finestra riaperta da icona | ||||
windowIconified() |
finestra iconizzata | ||||
windowOpened() |
finestra aperta |
Componente | Evento | Ascoltatore | Adattatore | Metodi | Commento |
---|---|---|---|---|---|
Button |
ActionEvent |
ActionListener |
--- |
actionPerformed() |
azione eseguita |
Choice |
ItemEvent |
ItemListener |
--- |
itemStateChanged() |
variazione su una voce |
Checkbox |
ItemEvent |
ItemListener |
--- |
itemStateChanged() |
variazione su una voce |
CheckboxMenuItem |
ItemEvent |
ItemListener |
--- |
itemStateChanged() |
variazione su una voce |
List |
ActionEvent |
ActionListener |
--- |
actionPerformed() |
azione eseguita | ItemEvent |
ItemListener |
--- |
itemStateChanged() |
variazione su una voce |
MenuItem |
ActionEvent |
ActionListener |
--- |
actionPerformed() |
azione eseguita |
ScrollBar |
AdjustmentEvent |
AdjustmentListener |
--- |
adjustmentValueChanged() |
modifica valore di impostazione |
TextComponent (TextArea e TextField) |
TextEvent |
TextListener |
--- |
textValueChanged() |
modifica contenuto di testo |
TextField |
ActionEvent |
ActionListener |
--- |
actionPerformed() |
azione eseguita |