Академический Документы
Профессиональный Документы
Культура Документы
ad oggetti in Java
Introduzione allo sviluppo orientato ad
oggetti
Vincenzo De Notaris
8/1/2008
LA PROGRAMMAZIONE AD OGGETTI IN JAVA
Paradigma ad oggetti
L’oggetto
Tra oggetti diversi sussistono delle relazioni, catalogate in diversi tipi: strutturali, non
strutturali, di ereditarietà, di realizzazione, di associazione e di composizione.
L’information hiding
Una delle caratteristiche più importanti dell’OOP è l’information hiding, che permette
di creare una netta separazione tra colui che utilizza oggetti e colui che li definisce
attraverso delle regole di visibilità associate ad attributi e metodi di una classe. In Java
vengono utilizzate tre keyword per esplicitare tali regole. La parola chiave public
stabilisce che le definizioni che seguono sono disponibili a tutti, private che esse siano
visibili sono all’interno della classe, protected che esse non siano visibili all’esterno, ma
accessibili alle classi derivate (concetto che verrà chiarito in seguito).
// costruttore
public console(){
// inizializza gli attributi
}
// attributi
public int giocatori;
protected Joypad controller = new Joypad();
private Chip piastra = new Chip();
// metodi
public void setGiocatori(int giocatori){
this.giocatori = giocatori;
}
Progettare e programmare in questo modo rende possibile a tutte le altre parti del
sistema, cioè agli altri oggetti che collaborano ed utilizzano i servizi come dei client,
di ignorare il meccanismo interno di implementazione, permettendo, tra le altre cose,
di realizzare con maggiore libertà i servizi. Quest’approccio aumenta la modularità,
diminuisce l’accoppiamento tra le parti del sistema e promuove il riutilizzo del
software in contesti diversi da quello in cui è stato inizialmente progettato.
Come visto finora è possibile raggruppare gli oggetti del nostro dominio del problema
in classi.
Si è accennato al fatto che una classe è un utile meccanismo di astrazione, che, come
tale, ci permette di gestire la complessità del problema trascurando i particolari
accessori degli oggetti e rappresentando solo le caratteristiche importanti per la
nostra applicazione.
Una classe, quindi, non è corretta in sé, come un’astrazione non è corretta in sé, ma
solo in relazione al contesto in cui la facciamo: la domanda che ciò conduce nel
processo di astrazione è quali sono i comportamenti e gli attributi che regolano
l’andamento del sistema che vogliamo progettare e quali invece sono del tutto
accessori. Quindi una classe è un ‘insieme di comportamenti e attributi comuni ad un
insieme di oggetti del nostro sistema, che rappresentino una responsabilità univoca
(semantica comune).
I package
L’impiego dei package risulta essere uno strumento assai utile in quanto consente di
decomporre il problema in sottoproblemi, che possono essere assegnati ad un package
o ad insiemi di package. Le classi contenute nel package decomporranno e
risolveranno il sottoproblema seguendo l’approccio divide et impera.
E possibile con essi gestire lo sviluppo parallelo del codice, poiché package diversi
possono essere assegnati a diversi team di sviluppo, favorendo il rispetto dei tempi di
consegna, tracciare le dipendenze tra diversi moduli, realizzati con i package; in
questo modo è possibile tenere sotto controllo l’accoppiamento dell’applicazione.
Il meccanismo dei package consente anche di evitare conflitti sui nomi delle classi
quando queste vengono caricate da web. In questo caso, è possibile che esitano classi
con lo stesso nome residenti su siti differenti, e che , nel corso dell’esecuzione del
programma, vengano riferite. Il meccanismo dei package si dimostra quindi
insostituibile per evitare pericolose ambiguità: a ciascuna classe corrisponderà uno
spazio dei nomi differente, cui potremo riferirsi senza possibilità di errore.
Il meccanismo dei package fa parte integrante del linguaggio, anche quando non
vengono esplicitamente creati, infatti, compilando una classe, questa farà parte del
cosiddetto “package di default”, che corrisponde alla directory in cui risiede il .class.
Con i package è possibile organizzare anche le librerie Java, ovvero le API. Le classi
fondamentali per ogni programma sono nel package java.lang, l’unico ad essere
visibile automaticamente.
Il modificatore static è usato per metodi e variabili di classe, ovvero variabili non
legate ad alcuna istanza ma direttamente alla classe in cui sono dichiarate.
Una variabile static è condivisa tra tutti gli oggetti istanza della stessa classe.
In particolare è possibile rappresentare i campi static come appartenenti ad un’area di
memoria comune a tutte le classi, mentre l’area di memoria delle variabili e d’istanza
viene copiata e ripetuta per ciascuna istanza della classe. Poiché è possibile creare
anche variabili reference di tipo static, ovvero variabili che puntino ad oggetti e
statiche, in Java esistono dei costrutti detti inizializzatori statici. Si tratta di una coppia
di parentesi, che si possono scrivere in qualunque punto all’interno di una classe,
precedute dal modificatore static, che viene eseguita non appena la classe viene
referenziata la prima volta, e prima di ogni suo costruttore. È possibile inserire quanti
blocchi stati si vuole, questi vengono eseguiti nell’ordine in cui appaiono nella classe
dall’alto verso il basso.
I problemi legati alla condivisione del valore delle variabili statiche divengono molto
forti nel caso di programmi concorrenti. In questo caso, è necessario utilizzare le
tecniche di sincronizzazione di accesso alle variabili della classe.
I metodi dichiarati statici sono anche detti metodi di classe. I metodi pubblici dichiarati
statici possono essere richiamati indipendentemente dall’esistenza di un istanza della
classe. I metodi che operano su uno specifico oggetto non devono essere dichiarati
static, mentre quelli che sono di utilità generale e che non agiscono sulle singole
istanze, devono essere dichiarati static. I metodi statici non possono accedere alle
variabili della classe che non sono dichiarate anch’esse statiche.
Il modificatore abstract è utilizzato per creare metodi e classi astratte. Questo concetto
verrà chiarito in seguito.
Le possibilità offerte dalle librerie di Java sono molte , ma non infinite: possono
esistere casi in cui si vuole accedere a codice già scritto o a dispositivi fisici attraverso
un’applicazione scritta in Java.
In questi casi è possibile usare il meccanismo dei metodi native (ovvero nativi) scritti in
C e C++.
L’ereditarietà
Il riuso del codice implementato per strutturare un oggetto è uno dei maggiori
vantaggi offerto dai linguaggi di programmazione ad oggetti. In tale contesto trova
perfetta collocazione il concetto di ereditarietà, uno dei meccanismi di
rappresentazione ed astrazione più potenti dei linguaggi object oriented. In
particolare, permette di generalizzare ed astrarre, rappresentando in una sola classe
(detta superclasse) attributi e metodi comuni a più classi (dette sottoclassi o classi
derivate). Quando si eredita da un tipo esistente, ne vien creato uno nuovo contenente
non soltanto gli elementi del tipo esistente (sebbene quelli private siano inaccessibili e
nascosti), ma anche la duplicazione dell’interfaccia della classe. Avendo superclasse e
classe derivata la stessa interfaccia, per differenziare le due vengono utilizzati due
procedimenti: aggiungere metodi alla classe derivata o ridefinirne l’implementazione.
E’ opportuno notare come Java una classe può ereditare al massimo da una sola
classe.
Il polimorfismo
Per rendere l’idea del concetto è utile vedere il codice, rifacendoci alle classi sopra
descritte.
Il metodo gioca utilizza come parametro un oggetto c di tipo Console.
Con il polimorfismo possiamo utilizzare questo metodo per classi derivate diverse tra
loro senza implementazioni aggiuntive.
gioca(wii);
gioca(xbox360);
gioca(ps3);
Il metodo visto di trattare un tipo derivate come se fosse un tipo base è detto
upcasting (conversione di tipo verso l’alto).
Uno dei maggiori benefici del polimorfismo, come in effetti di un po' tutti gli altri
principi della programmazione ad oggetti, è la facilità di manutenzione del codice.
Classi e metodi astratti
Il corpo del metodo configura vien rimandato alle classi derivate e può essere scritto in
maniera differente l’un dall’altro.
In Java non esiste come in altri linguaggi (ad esempio il C++) il concetto di ereditarietà
multipla secondo il quale una classe può essere una estensione di più superclassi: una
classe può essere la specializzazione di una sola classe. Questo rende il linguaggio più
semplice da usare, ma più restrittivo nella implementazioni di modelli che utilizzano
l’ereditarietà multipla.
Le interfacce non fanno parte della gerarchia delle normali classi: al vertice della
gerarchia delle interfacce non c’è la classe Object.
I metodi possono essere dichiarati public e abstract e non possono essere protected o
private.
Con la parola chiave implements una classe si impegna a implementare i metodi definiti
nell’interfaccia.
Se una classe implementa un’interfaccia le sue sottoclassi ne ereditano i metodi.
Una classe può implementare un numero qualsiasi di interfacce.
Se due interfacce hanno lo stesso metodo con lo stesso profilo basta implementarne
uno.
Se i metodi con lo stesso nome hanno diverso profilo vanno implementati entrambi.
Se i metodi con lo stesso nome hanno lo stesso profilo, ma restituiscono tipi diversi il
compilatore segnala un errore.
Con gestione delle eccezioni si suole intendere un meccanismo di gestione degli errori
integrando metodi di controllo direttamente nel linguaggio di programmazione; il Java
si rifà esattamente a questo modello.
Il programmatore definisce dei blocchi di codice (detti handler) per la gestione delle
eccezioni dei tipi interessati. Nel caso in cui venga rilevata una condizione anomala, il
metodo costruisce un ‘eccezione della classe che rappresenta e la “lancia”, per far sì
che i meccanismi preposti alla “cattura” possano gestirla.
try{
// istruzioni che lanciano eccezioni
}catch(Esempio1Exception e){
// istruzioni associate all'eccezione di tipo Esempio1Exception
}catch(Esempio2Exception e){
// istruzioni associate all'eccezione di tipo Esempio2Exception
}finally{
// istruzioni "finali"
}
Le istruzioni del blocco che segue try vengono eseguite sotto il controllo dell’handler;
nel suo interno va inserito codice grado di lanciare un eccezione.
Le istruzioni di un blocco catch sono eseguite quando nell’esecuzione del blocco try
viene lanciata un eccezione del tipo corrispondente (o di una sua classe derivata) non
gestita da un handler più interno; la corrispondente variabile è associata all’oggetto
che è stato lanciato.
Le istruzioni del blocco finally vengono sempre eseguite, alla fine del blocco try o dei
blocchi catch eventualmente lanciati; esso è usato tipicamente per rilascio di risorse
od operazioni di ripristino.
Per lanciare un’eccezione, il programmatore deve creare un oggetto della classe che
rappresenta quella determinata eccezione, usando la seguente sintassi:
Bisogna tener conto che ad un eccezione possono essere associati anche dei
parametri, a differenza dell’esempio riportato.
La classe Throwable in Java descrive tutto ciò che può essere generato come un
eccezione.
In generale esistono due oggetti di tipo Throwable, ossia due gerarchie di ereditarietà:
Error, che rappresenta gli errori in fase di compilazione; Exception, il tipo base che può
essere generato da qualsiasi metodo delle classi della libreria standard e che
rappresenta le eccezioni controllate.
Esiste, inoltre, un’ulteriore classe di eccezioni denominata RuntimeException e che
rappresenta le eccezioni non controllate (unchecked excepetion), ossia gli errori che
possono verificarsi a tempo di esecuzione.
Per ricapitolare, le eccezioni vengono utilizzate per: gestire i problemi nel punto
adeguato, risolverli e chiamare nuovamente il metodo che ha generato l’eccezione;
risolvere il problema e continuare senza richiamare il metodo generatore; calcolare
risultati alternativi a quelli previsti; terminare il programma; semplificare il codice e la
sua manutenzione; rendere il software più sicuro.
I contenitori
Le classi Contenitors sono fra gli strumenti più potenti per lo sviluppo, ed assumono il
compito di “collezionare” gli oggetti.
Set setEsempio;
setEsempio = new HashSet();
In tal modo è stato salvaguardato il polimorfismo, ma ciò nonostante è possibile creare
un Set utilizzando direttamente il costruttore di HashSet.
Gli iteratori
Durante l’utilizzo dei contenitori risulta molto utile ricorrere ad oggetti denominati
Iterator, ossia iteratori.
Essi possono essere intesi come una sorta di “segnaposto” attraverso il quale scorrere
gli elementi immagazzinati.
La vera potenza degli iteratori va ricercata nel fatto che essi danno la possibilità do
separare l’operazione di scorrimento di una sequenza dalla sottostanza a tale
sequenza.
La gestione dell’input/output
Per OutputStream:
Con l’avvento della versione 1.1 del linguaggio Java sono state introdotte le classi
Reader e Writer che, di fatto, per alcune operazioni vanno a sostituire le classi in
precedenza analizzate; quest’ultime, comunque, mantengono intatta la loro efficienza.
Qui di seguito riporto blocchi di codice nel quale vengono utilizzate le classi di cui
discusso.
Nei calcolatori dotati di più processori o cpu multi-core, ogni processore esegue
istruzioni di un diverso thread.
Nelle macchine single-core il processore lavora su più thread, con una tempistica
stabilita dal SO.
In tal caso il parallelismo non è reale, bensì simulato e si parla di esecuzione
concorrente.
I thread che fanno parte della stessa esecuzione condividono le risorse in gioco. Dal
momento che il sistema usa uno stack dei record di attivazione per gestire la
sequenza dinamica, ogni thread ha il proprio stack; con ciò si salvaguarda la
saturazione della memoria nel caso di un elevato numero di concorrenze.
Per attivare un thread in Java occorre creare un oggetto della classe Thread a cui
devono essere associate operazioni da eseguire. Per attivare l’esecuzione del nuovo
thread è necessario ricorrere al metodo start() dell’oggetto di tipo Thread; per fermarla
è possibile utilizzare il metodo stop().
Per effettuare quest’associazione esistono due metodi: creare una classe derivata da
Thread o creare una classe che implementa l’interfaccia Runnable, passando un
oggetto di questa classe al costruttore di Thread.
Nel primo caso, la classe derivata deve ridefinire il metodo public void run() della
classe Thread; il codice del metodo verrà eseguito nel nuovo thread.
/*
* viene avviato il thread t contenente le istuzioni
* implementate nel metodo run dell'oggetto NuovoThread
*/
t.start();
/*
* in simultanea al thread t del tipo NuovoThread
* possono essere definite istruzioni eseguite
* nel thread del main
*/
}
}
Nel secondo caso, la classe deve implementare l’unico metodo definito in Runnable,
che ha lo stesso prototipo precedente: public void run().
Analogamente a prima:
/*
* viene avviato il thread t contenente le istuzioni
* implementate nel metodo run dell'oggetto NuovoThread
*/
t.start();
/*
* in simultanea al thread t del tipo NuovoThread
* possono essere definite istruzioni eseguite
* nel thread del main
*/
}
synchronized (this) {
// istruzioni contenenti risorse comuni
}
try {
// il thread viene “addormentato”
Thread.sleep(30000);
} catch (InterruptedException e) {
// attesa terminate attraverso un’eccezione
}
Il metodo sleep() è static, quindi si richiama usando direttamente il nome della classe
Thread; esso sospende il thread corrente per un tempo espresso in millisecondi
(msec).
Un thread può mettersi in attesa della terminazione di un altro thread usando uno sei
seguenti metodi della classe Thread: join(), che sospende il thread corrente fino a
quando non termina il thread destinatario del metodo; join(long msec), nel quale viene
specificato anche un tempo di attesa massimo.
Il metodo join va applicato al thread di cui vogliamo aspettare la terminazione.
try {
// viene stabilita l’attesa del thread t
t.join();
} catch (InterruptedException e) { }
Anche il tal caso è necessario un blocco composto da try e catch per gestire
l’eccezione di interruzione.
Nel caso in cui si vuol gestire l’attesa che una struttura di dati si trovi in una situazione
particolare, l’evento che fa risvegliare un thread non è prefissato ma dipende
dall’applicazione; è necessario, indi, un meccanismo più generale. Per realizzare
questo tipo di attesa Java mette a disposizione i metodi wait() e notifyAll(); il primo dei
due mette un thread in attesa che si sia un cambiamento nell’oggetto. Il secondo
avvisa tutti i thread in attesa che è avvenuto un cambiamento.
Il thread che richiama il wait deve aver acquisito il mutex dell’oggetto a cui viene
applicato il metodo. Il mutex viene rilasciato automaticamente ed il thread viene
sospeso fino a quando un altro thread non richiama notifyAll sullo stesso oggeto. Al
suo risveglio il thread riacquisisce il mutex sullo stesso oggetto.
L’acquisizione del mutex è necessaria per consentire al thread di esaminare lo stato di
una struttura di dati condivisa; il rilascio automatico del mutex, che riguarda solo
quello dell’oggetto a cui è applicato il metodo, consente agli altri thread di modificare
la risorsa condivisa. Il thread che richiama wait utilizza, tipicamente, un ciclo di
controllo.
Per quanto riguarda il metodo notifyAll, il thread che lo richiama deve aver acquisito il
mutex.
Tutti i thread in attesa vengono risvegliati, ma vengono eseguiti uno alla volta.
In una classe ben progettata, il codice che gestisce l’attesa della condizione è
incapsulato nei metodi dell’oggetto che contiene la struttura di dati.
L’interfaccia grafica
I metodi principali messi a disposizione per la gestione degli applet sono init(), start(),
stop() e destroy().
Oltre agli applet è possibile utilizzare anche delle finestre grafiche chiamate Frame (il
cui significato, dall’inglese, è cornice).