Classi astratte
La classificazione di oggetti software permette la realizzazione
incrementale di entità software con gradi crescenti
di specializzazione. Ogni elemento può essere
sviluppato e testato in modo indipendente: le migliorie
apportate ad una classe in cima alla una gerarchia verranno
trasmes-se automaticamente a tutte le sottoclassi.
Durante
il lavoro di classificazione, tuttavia, capitano anche
situazioni in cui emergono entità che pur avendo
attributi e dei metodi ben precisi, non corrispondono
ad entità concrete. Nella classificazione degli
esseri viventi, ad esempio, esiste la categoria dei
mammiferi, che racchiu-de un insieme di attributi comuni
a diverse specie viventi (cani, gatti, mucche
),
ma che di per sé non rappresenta nessun animale.
Procedendo
con la classificazione è possibile trovare ulteriori
esempi. In Figura 11.1, le catego-rie Erbivoro, Carnivoro,
Felino, Ovino e Bovino non corrispondono ad alcun animale
concreto, ma rappresentano comunque passaggi fondamentali
nella classificazione.
Figura 1 - Classificazione delle specie viventi
Nella
progettazione di software si presenta spesso un problema
simile: le gerarchie molto arti-colate presentano spesso
dei nodi che corrispondono a categorie astratte di oggetti,
indispensa-bili come passaggi logici, ma di fatto non
istanziabili. Tali classi vengono dette Astratte, ed
of-frono la possibilità di definire degli speciali
metodi abstract privi di implementazione, che delegano
la proipria concretizzazione a chi implementa le sottoclassi.
Per
definire una classe astratta, è necessario aggiungere
il modificatore abstract sia nella di-chiarazione della
classe, sia in quella dei metodi privi di implementazione.
L'esempio seguente riporta il caso della classificazione
degli esseri viventi. La classe astratta mammifero dichiara
il metodo concreto mangia(), che è definito come
l'esecuzione in sequenza di tre passaggi: ingerisci(),
digerisci() ed evacua():
public
abstract class Mammifero {
public void mangia(Cibo c) {
ingerisci(Cibo c);
digerisci();
evacua();
}
public abstract void ingerisci(Cibo c);
public abstract void digerisci();
public abstract void evacua();
}
Le
modalità di ingestione, digestione ed espulsione
sono differenti in ognuna delle sottocatego-rie, tuttavia
esse sono sempre presenti. Ogni sottoclasse concreta
di Mammifero sarà tenuta a fornire una implementazione
dei metodi astratti, che rifletta la natura particolare
dell'entità rappresentata (la digestione nei
mammiferi carnivori è sostanzialmente differente
da quella dei mammiferi erbivori ruminanti).
Classi interne
Il linguaggio Java permette di definire classi all'interno
di altre classi, con un livello di nidifi-cazione arbitrario:
public
class MyClass {
public method() {
}
public
class MyInnerClass {
public method() {
...
}
}
}
La
classe interna può essere richiamata all'interno
della classe di livello superiore utilizzando il suo
nome senza particolari differenze rispetto a come si
utilizza qualunque altra classe presente nel name space
del sorgente; di contro, all'esterno del sorgente in
cui viene dichiarata essa è accessibile solamente
utilizzando il percorso completo:
MyClass.MyInnerClass
m = new MyClass.MyInnerClass();
Nel codice di esempio, si può notare che la classe
interna definisce un metodo utilizzando lo stesso nome
di un metodo della classe di livello superiore. In casi
come questo, qualora la clas-se interna desideri chiamare
il metodo omonimo della classe di livello superiore,
dovrà accede-re all'istanza della classe contenitrice
attraverso un uso particolare di this:
MyClass.this.metodo();
lo stesso nome Uso di this e nomeclasse.this
Le
classi interne sono state introdotte a partire dal JDK
1.1, principalmente per supportare il modello di eventi
tipico delle interfacce grafiche. Pertanto esse verranno
trattate in modo ap-profondito solamente in tale contesto.
Il contesto statico: Variabili e metodi di classe
Gli attributi visti fino ad ora sono associati alle
istanze di una classe: ogni singola istanza pos-siede
una copia privata di tali attributi, e i metodi della
classe lavorano sul tale copia privata. Esiste la possibilità
di definire attributi statici, ossia variabili legate
alla classe, presenti in co-pia unica e accessibili
da tutte le istanze. Allo stesso modo è possibile
definire metodi statici, ossia metodi non legati alle
singole istanze, che possono operare esclusivamente
su variabili statiche. Attributi e metodi statici vengono
chiamati anche "attributi e metodi di classe",
per distinguerli dagli attributi e dai metodi visti
fino ad ora, che vengono detti "di istanza".
Metodi
e variabili di classe costituiscono il contesto statico
di una classe. Per accedere al con-testo statico di
una classe non è necessario crearne un'istanza:
ogni metodo statico è accessibile direttamente
attraverso la sintassi:
NomeClasse.metodoStatico();
All'interno di un metodo di classe è possibile
accedere solamente a metodi ed attributi statici; inoltre
in tale contesto gli identificatori this e super sono
privi di significato.
Attributi
e i metodi di classe sono utili per rappresentare caratteristiche
comuni a tutto l'insieme di oggetti appartenenti ad
una classe. Ad esempio, in un'ipotetica classe che denoti
le finestre grafiche di un desktop compariranno sia
attributi di istanza (tipo la posizione e la di-mensione,
che sono caratteristiche di ogni singola finestra) sia
attributi di classe (come il colore della barra del
titolo, uguale per tutte le finestre presenti sul sistema).
Questa situazione può essere rappresentata dal
seguente esempio:
public
class Window {
private int x;
private int y;
private int height ;
private int width;
private static Color titleBarColor;
public void setX(int x) {
this.x = x;
}
...
public static void setTitleBarColor(Color
c) {
titleBarColor = c;
}
}
In questo esempio, se si possiede un oggetto di tipo
Window e si desidera impostarne le dimen-sioni, si provvederà
ad effettuare una chiamata a metodo come di consueto:
w.setWidth(400);
w.setHeight(300);
Queste
chiamate, come ormai dovrebbe essere chiaro, operano
sull'istanza referenziata dalla variabile w. Al contrario,
la chiamata:
Window.setTitleBarColor(new
Color("Red"));
va
a modificare l'attributo statico titleBarColor, con
la conseguenza di modificare tutti gli og-getti di tipo
Window presenti in memoria.
Nota:
le variabili e i metodi statici possono essere richiamati
anche attraverso l'identificatore di una va-riabile;
pertanto, riferendosi all'esempio precedente, le seguenti
istruzioni
Window.
setTitleBarColor (new Color("Red"));
w.setTitleBarColor (new Color("Red"));
sortiranno
il medesimo effetto. Dal momento che metodi e variabili
di classe operano esclusivamente sul contesto statico,
si preferisce seguire la convenzione di richiamarli
unicamente attraverso l'identificatore della classe,
in modo da sottolineare la differenza.
I package
Il linguaggio Java offre la possibilità di organizzare
le classi in package, uno strumento di clas-sificazione
gerarchico simile alle directory di un file system.
Un package è un contenitore che può raccogliere
al suo interno sia classi che altri package, secondo
una struttura gerarchica. Grazie ai package è
possibile suddividere un software in moduli, raggruppando
insiemi di clas-si che svolgono un determinato compito.
Una simile forma di organizzazione diventa indispen-sabile
nei moderni sistemi software, composti di solito da
centinaia o migliaia di classi.
L'organizzazione
delle classi in package permette inoltre di risolvere
il problema del conflitto di nomi (in inglese name clash).
Il conflitto di nomi e' un problema molto sentito nella
comuni-tà dei programmatori ad oggetti: all'interno
di progetti di grandi dimensioni, la necessità
di da-re un nome ad ogni cosa conduce a dover riutilizzare
più volte nomi comuni tipo "Persona",
"Cliente" o "Studente". La divisione
in package risolve il problema del conflitto di nomi,
dal momento che in questo caso e' sufficiente che i
nomi siano unici all'interno di un package.
Dichiarazione di Package
Per includere una classe in un package è necessario
inserire in testa al sorgente la dichiarazio-ne:
package
nomepackage;
Se
si desidera inserire la classe in un sottopackage, bisogna
utilizzare come separatore il carat-tere ".":
package
pakage1.package2.myPackage;
Oltre
alla dichiarazione di appartenenza, è necessario
che il file sorgente venga posto fisica-mente all'interno
di una struttura di directory analoga a quella dei package
sottostanti, a partire da una locazione che prenderà
il nome di radice. Si osservi la figura 11.2:
Figura 2 - Esempio di organizzazione in package
all'interno di un file system
Se
si desidera creare una classe "userInterface"
all'interno del package it.mokabyte.sampleApplication.userInterface,
è necessario creare nella directory c:/progetto/it/mokabyte/sampleApplication/userInterface
un sorgente "MainFrame.java" do-tato di un'intestazione
di questo tipo:
package
it.mokabyte.sampleProject.userInterface;
public
class MainFrame {
...
}
In
uno scenario come questo, la directory "progetto"
svolge il ruolo di radice: la posizione dei package
all'interno della gerarchia viene considerata in relazione
a quest'ultima.
Compilazione ed esecuzione
L'organizzazione di un progetto in package richiede
una certa attenzione in fase di compilazio-ne ed esecuzione,
al fine di evitare errori comuni che generano una grossa
frustrazione nei principianti.
In
primo luogo, per identificare univocamente una classe
e' necessario specificarne per esteso il nome assoluto,
comprensivo di identificatori di package:
it.mokabyte.sampleApplication.userInterface.MainFrame
In
secondo luogo, prima di invocare i comandi bisogna posizionarsi
sulla radice dell'albero di package; in caso contrario
il compilatore e la JVM non riusciranno a trovare i
files. Riprenden-do l'esempio precedente, prima di compilare
o eseguire la classe MainFrame sarà necessario
effettuare una chiamata di questo tipo:
cd
c:/progetto
Per
compilare la classe MainFrame.java bisogna digitare:
javac
it\mokabyte\sampleApplication\userInterface\MainFrame.java
su
piattaforma windows e:
javac
it/mokabyte/sampleApplication/userInterface/MainFrame.java
sotto
Unix-Linux.
Per
eseguire il main della classe invece bisogna invocare
il comando java specificando il nome assoluto:
java
it.mokabyte.sampleApplication.userInterface.MainFrame
Quando
si comincia a lavorare su progetti di grandi dimensioni
organizzati a package, e' bene prendere l'abitudine
di separare i file sorgenti dalle classi in uscita dal
compilatore. Il flag -d del compilatore javac permette
di specificare la directory di destinazione del compilatore;
per-tanto, se si desidera compilare la classe MainFrame
in modo tale da piazzare i file .class nella directory
C:\classes, si dovrai invocare il comando:
javac
-d C:\classes it\mokabyte\sampleApplication\userInterface\MainFrame.java
All'interno
della directory C:\classes verranno automaticamente
generate le cartelle corri-spondenti ai package, con
i file .class nelle posizioni corrette.
Dichiarazione Import
Per accedere alle classi contenute in un package è
necessario utilizzare il nome della classe per esteso,
sia in fase di dichiarazione che di assegnamento:
it.mokabyte.sampleApplication.userInterface.MainFrame
m =
new it.mokabyte.sampleApplication.userInterface.MainFrame();
La scomodità di un simile approccio e' evidente:
l'utilizzo di nomi composti così lunghi può
alla fine generare altrettanti problemi del conflitto
di nomi. Per non essere costretti a specifica-re ogni
volta tutto il percorso verso la classe MainFrame, e'
possibile importare la classe all'interno del name space
del sorgente su cui si sta lavorando, aggiungendo un'apposita
clau-sola import all'intestazione del file:
import
it.mokabyte.sampleApplication.userInterface.MainFrame;
public
class MyClass {
public static void main(String argv[]) {
MainFrame f = new MainFrame();
}
}
Grazie
alla import, l'identificatore MainFrame entra a far
parte del name space del sorgente: sarà compito
del compilatore associare il nome
MainFrame
con
la classe
it.mokabyte.sampleApplication.userInterface.MainFrame
La
import viene spesso utilizzata per importare interi
package, utilizzando il carattere "*" al posto
del nome della classe:
import
it.mokabyte.sampleApplication.userInterface.*;
In
questo caso, il name space del sorgente comprenderà
I nomi di tutte le classi contenute nel package importato.
Ovviamente l'import di interi package può a sua
volta generare conflitti sui nomi, dal momento che package
differenti possono contenere nomi uguali. Queste eventualità
verranno in ogni caso segnalate in fase di compilazione,
e potranno essere risolte ricorrendo, solo in quei casi,
all'uso di nomi completi.
Convenzioni di Naming dei Package
I nomi di package seguono la consueta convenzione camel
case, con iniziale minuscola. Per l'organizzazione in
sottopackage, le specifiche Sun consigliano di seguire
la regola del nome di dominio invertito: ad esempio
la IBM (dominio ibm.com) inserisce le proprie classi
in package che hanno come prefisso com.ibm; allo stesso
modo la Sun Microsystem (sun.com) utilizza il prefisso
com.sun. Grazie a questa convenzione è possibile
raggiungere l'obiettivo di garantire l'unicità
dei nomi di classe senza il bisogno di ricorrere ad
una qualche autorità centralizzata.
Principali package del JDK
A differenza di altri linguaggi di programmazione, le
classi di sistema di Java non sono riunite in librerie,
ma in package. Ogni package contiene classi che permettono
di svolgere un deter-minato compito: grafica, comunicazione
in rete, gestione del file system
Di seguito,
vengono presentati i package più importanti del
linguaggio:
- java.lang:
classi base del linguaggio, tra le quali si trovano
Object, String, le wrapper class (Integer, Boolean
)
e la classe Math (che contiene le più importanti
funzioni matematiche). A differenza degli altri package,
java.lang non necessita di essere im-portato, dato
che esso fa già parte della dotazione standard
del linguaggio.
-
java.io: contiene tutte le classi necessarie a gestire
il file system e l'Input Output
- java.util:
classi di utilità, come Vector, Hashtable o
Date
- java.net:
qui si trovano tutte le classi di supporto alla comunicazione
via rete
- java.awt:
toolkit grafico
- javax.swing:
classi per la gestione della grafica a finestre
L'uso
delle classi presente in questi ed altri package verrà
chiarito nei prossimi capitoli.
Modificatori
I modificatori sono parole riservate del linguaggio
che permettono di impostare determinate caratteristiche
di classi, metodi e attributi. Alcuni di questi modificatori,
come abstract e sta-tic, sono già stati trattati
ampiamente nei precedenti paragrafi; altri, come public,
private e protected, verranno finalmente illustrati
in modo completo e formale.
Modificatori di Accesso
I modificatori di accesso permettono di impostare il
livello di visibilità dei principali elementi
di un sorgente Java, ossia di classi, metodi e attributi.
Ad ognuno di questi elementi è possibile assegnare
uno dei seguenti livelli di protezione:
private:
l'elemento è visibile solo all'interno di una
classe
nessun modificatore (package protection): l'elemento
è visibile a tutte le classi che fanno parte
dello stesso package
protected: l'elemento è accessibile a
tutte le classi del package e a tutte le sottoclassi,
anche se dichiarate in package differenti
public: l'elemento è visibile ovunque.
Il
modificatore private è caratteristico degli attributi.
I principi base della programmazione ad oggetti suggeriscono
di rendere tutti gli attributi private, e di permetterne
l'accesso in modo programmatico attraverso metodi getter.
Ci sono comunque diverse occasioni in cui conviene dichiarare
private anche un metodo, qualora esso venga chiamato
solamente all'interno della classe.
La
package protection viene utilizzata soprattutto per
classi di appoggio che hanno una loro utilità
all'interno di un package, ma che si desidera restino
nascoste al resto del sistema.
Si
ricorre a protected in tutte le occasioni in cui si
voglia rendere un metodo visibile solo alle sottoclassi:
tipicamente vengono dichiarati protected i metodi getter
relativi ad attributi che si desidera restino inaccessibili
al di fuori del dominio della classe e delle sue sottoclassi.
Infine
il modificatore public rende un elemento visibile ovunque:
si raccomanda di utilizzare tale modificatore con parsimonia
e di limitarne l'uso a classi e metodi. Si ricordi che
l'insieme dei metodi pubblici di una classe è
l'interfaccia attraverso la quale una classe comunica
con il mondo esterno, e pertanto si raccomanda di scegliere
con cura quali metodi rendere pubblici e quali no, avendo
la stessa accortezza che il progettista di un elettrodomestico
ha nel non lascia-re fili o ingranaggi scoperti.
Modificatore final
Il modificatore final assume un significato diverso
a seconda del contesto di utilizzo. Se abbinato ad un
attributo, esso lo rende immutabile; tale circostanza
impone che l'assegnamento di una variabile final venga
effettuata nello stesso momento della dichiarazione.
Spesso final viene utilizzato in abbinamento a static
per definire delle costanti;
public
static final float pigreco = 3.14;
Bisogna
fare attenzione al fatto che quando si dichiara final
un reference ad un oggetto, a risul-tare immutabile
è il reference, non l'oggetto in se. Se si definisce
un attributo del tipo:
public
static final Vector list = new Vector();
L'uso
di final vieta l'operazione di assegnamento sulla variabile:
list
= new Vector() // errore: la variabile list è
final
Al
contrario è assolutamente legale chiamare i metodi
che modificano lo stato dell'oggetto:
list.add("Nuovo
Elemento");
L'uso
di final in abbinamento ad un metodo ha invece l'effetto
di vietarne l'overridding nelle sottoclassi; se associato
ad una classe ha l'effetto di proibire del tutto la
creazione di sottoclassi (in una classe final, tutti
i metodi sono final a loro volta). L'uso di final su
metodi e classi, oltre ad avere dei ben precisi contesti
di utilizzo, presenta l'ulteriore vantaggio di permettere
al compilatore di effettuare delle ottimizzazioni, rendendo
l'esecuzione più efficiente. Per questa ragione
alcune classi di sistema, come String e StringBuffer,
sono state definite final.
native
Il modificatore native serve a dichiarare metodi privi
di implementazione; a differenza dei me-todi abstract,
i metodi native vanno implementati in un qualche linguaggio
nativo (tipicamente in C o C++). La filosofia di Java
non incoraggia la dichiarazione di metodi nativi, ossia
dipen-denti dalla macchina. Per questa ragione l'effettiva
creazione di metodi nativi risulta piuttosto macchinosa.
strictfp
Il modificatore strictfp, usabile sia su metodi che
su classi, ha lo scopo di forzare la JVM ad attenersi
strettamente allo standard IEEE 754 nel calcolo di espressioni
in virgola mobile. Il linguaggio Java infatti utilizza
di norma una variante di tale standard che offre una
maggior precisione; esistono tuttavia delle situazioni
in cui è necessario fare in modo che i risultati
delle operazioni siano assolutamente conformi allo standard
industriale, anche se questo implica una minor precisione.
transient, volatile e synchronized
Il modificatore transient permette di specificare che
un determinato attributo non concorre a definire lo
stato di un oggetto. Esso verrà studiato in profondità
nei capitoli relativi ad IO e Ja-vaBeans. L'uso di volatile
e synchronized, due modificatori dotati di una semantica
piuttosto complessa, verrà invece illustrato
nel capitolo sui Thread e sulla concorrenza.
Conclusioni
In questo articolo sono stati approfonditi i costrutti
delle classi astratte, di contesto statico e l'uso dei
package. Il mese prossimo studieremo il costrutto delle
interfacce.
|