InfApplicata-04-ProgrammazioneAdOggetti

Programmazione ad oggetti: classi ed oggetti
La programmazione ad oggetti è un modo di organizzare i dati ed il codice dei programmi. Un
oggetto è una struttura in memoria che rappresenta una certa entità logica o fisica. Ogni oggetto è
composto da un insieme di attributi e metodi. Una classe è la descrizione di tutti gli oggetti che
appartengono ad una stessa classe.
Una classe si dichiara specificandone il nome seguito dalla dichiarazionbe dei suoi attributi, il/i
costruttore/i ed i metodi. Ognuno di questi elementi è opzionale.
nome_classe {
//attributi
//costruttori
//metodi
}
Attributi
Gli attributi sono come delle variabili, visibili all'interno di tutta la classe. Quindi, un attributo si
può usare inizializzando altri attributi, nei costruttori e in tutti i metodi della classe. Inoltre, è usato
da chi fa uso di un oggetto di quella classe.
Costruttori
Un costruttore si dichiara con la sintassi:
<nome_classe> ( <parametro1>, <parametro2>, ...) { <codice> }
Serve ad "inizializzare" un oggetto. Generalmente si mette nel costruttore tutto il codice che serve
per "preparare" un oggetto al suo uso; ad esempio, inizializzare i suoi attributi.
Istanziare
Data una descrizione di una classe, si possono istanziare oggetti di quella classe. In pratica, viene
allocata della memoria (ram) per contenere i valori degli attributi di un oggetto. La sintassi per
istanziare un oggetto è:
<nome classe> <nome_variabile> = new <costruttore> ;
Come per i tipi primitivi, la variabile è un nome che vogliamo noi.
Usare gli oggetti
Per accedere (leggere o scrivere) gli attributi di un oggetto si usa la sintassi:
<variabile>.<attributo>
Per usare i metodi di un oggetto si usa la sintassi:
<variabile>.<metodo>
Le stringhe sono oggetti
Le variabili di tipo String che abbiamo usato fino ad ora sono riferimenti ad oggetti. Quando
scriviamo:
String s = "ciao" ;
in realtà viene tradotto in:
String s = new String("ciao") ;
La lunghezza della stringa si ottiene invocando un metodo della classe String:
int l = s.lenght() ;
Esempio: biblioteca
Ad esempio, vogliamo scrivere un programma per gestire i libri di una biblioteca. Programmando
ad oggetti "libro" e "biblioteca" sono degli ottimi candidati per diventare delle classi.
Un libro può essere descritto da una classe con i seguenti attributi: autore, titolo, editore, anno,
descrizione.
Vedere biblioteca/Libro.java
Una biblioteca invece contiene una collezione di libri ed i metodi per elencarli tutti e per cercare gli
autori.
Vedere biblioteca/Biblioteca.java
Notare che nessuna delle due classi contiene un main. Per provare le classi creo una classe apposita
con un main per provare le classi.
Vedere biblioteca/ProvaBiblioteca.java
Uso della memoria
Ci sono due cose fondamentali da sapere sulla gestione della memoria in relazione agli oggetti.
Il valore NULL
Attenzione. Diversamente dai tipi primitivi, una variabile che rappresenta un riferimento ad un
oggetto (dunque anche String) non ha un valore di default ma vale "NULL". Se dichiaro soltanto
una variabile per un oggetto, ma non lo istanzio, non posso usarne gli attributi ed i metodi. Ad
esempio, se eseguo il seguente codice:
Libro l ;
l.titolo = "Eva Luna" ;
ottengo un errore di esecuzione: "Null Pointer Exception".
Il Garbage Collector
Come già detto, gli oggetti vanno istanziati, cioè bisogna allocare al memoria che usano. Ma come
faccio a "deallocarli". In Java non esiste un'istruzione esplicita per fare questo (come il "distruttore"
in C++); la Java Virtual Machine si occupa automaticamente di riconoscere quali sono gli oggetti
che non vengono più usati da nessuno e di liberare la memoria che occupano. Questo processo si
chiama "Garbage Collection".
I membri statici
Le classi possono contenere attributi o metodi statici. Questi sono legati alla descrizione della
classe, non agli oggetti di quella classe, duque possono essere usati anche senza istanziare oggetti.
Infatti, i metodi che abbiamo usato fino ad ora erano tutti statici. Per dichiarare attirubit o metodi
statici basta inserire la parola static prima del loro tipo.
Gli attibuti statici vengono spesso usati per dichiarare valori costanti. Ad esempio:
Math.PI ;
e' un attributo statico della classe Math (nativa di Java) che rappresenta il valore PI greco.
Un altro esempio è tenere conto delle istanze di una classe che vengono create. Ad esempio:
public class StaticTest {
/** Attributo statico usato per contare il numero di istanze.
*/
static int numOfInstances = 0 ;
/** Costruttore che aumenta il contatore di istanze
*/
public StaticTest() {
StaticTest.numOfInstances++ ;
// oppure semplicemente
// numOfInstances++ ;
}
/** Restituisce il numero di istanze fatte.
*/
public static int getNumOfInstances() {
return numOfInstances ;
}
}
public void aNormalMethod() {
// quello che volete ...
}
In un altro metodo posso farmi dire quante istanze ci sono. Ad esempio:
// Istanzio un oggetto
StaticTest s = new StaticTest() ;
// mi faccio dire quanti ce ne sono
System.out.println("Ci sono in giro "+StaticTest.getNumOfInstances()+" oggetti
della classe StaticTest") ;
// Uso l'oggetto come tutti gli altri
s.aNormalMethod() ;
Overloading
Fare overloading vuol dire: "dichiarare metodi con lo stesso nome ma parametri diversi". Serve a
poter dichiarare metodi con lo stesso nome. Questo è utile perchè generalmente il nome di un
metodo è associato al significato dell'operazione che esegue. L'overloading può essere fatto sia sui
metodi che sui costruttori.
Esempio
La classe Libro può essere estesa aggiungendo un altro costruttore:
public Libro(String autore, String titolo) {
this.autore = autore ;
this.titolo = titolo ;
}
public Libro(String autore, String titolo, String editore, int anno, String
descrizione) {
this(autore, titolo) ;
this.editore = editore ;
this.descrizione = descrizione ;
}
Si possono aggiungere altri metodi per impostare i dati rimanenti:
public void impostaDati(String editore, int anno) {
this.editore = editore ;
this.anno = anno ;
}
public void impostaDati(String editore, int anno, String descrizione) {
this.editore = editore ;
this.anno = anno ;
this.descrizione = descrizione ;
}
Oppure, si può sfruttare un metodo già esistente ed aggiungere funzionalità. Il metodo precedente si
può scrivere come:
public void impostaDati(String editore, int anno, String descrizione) {
impostaDati(editore, anno) ;
this.descrizione = descrizione ;
}
Estensioni ed Ereditarietà
L'ereditarietà ' il metodo più diffuso di riutilizzare codice già fatto nella programmazione ad oggetti.
Estendere una classe vuol dire creare una nuova classe aggiungendo attributi e metodi ad una classe
già esistente. Si dice che la nuova classe eredita le funzionalità della prima.
Attenzione. I costruttori NON vengono ereditati; bisogna ridefinirli tutti.
Super
La parola chiave super serve a fare riferimento a membri della sovraclasse o sopraclasse o
superclasse.
Esempio
Vedere LibroCatalogato.java e VolumeLibro.java
Rappresentazione grafica
Spesso si vedono documentazioni su programmi ad oggetti che visualizzano l'organizzazione tra le
classi in questo modo:
Libro
LibroCatalogato
VolumeLibro
La classi vengono rappresentate con rettangoli. Le estensioni vengono rappresentate con frecce che
vanno dalla sottoclasse alla classe estesa. Viene a crearsi una gerarchia di classi.
Polimorfismo
Il polimorfismo è una conseguenza dell'estensione delle classi. Ci troviamo ad evere un insieme di
tipi (ex Libro, LibroCatalogato, VolumeLibro) che sono tra loro in qualche modo relazionati.
Gli oggetti di una sottoclasse possono essere trattati come se fossero oggetti della superclasse.
Questo si può fare perchè ho la certezza di trattare un oggetto che ha ereditato tutti gli attributi e
metodi di una superclasse. In pratica ho la possibilità di trattare indistintamente diversi tipi di
oggetti, appartenenti a diverse sottoclassi, come se fossero oggetti della superclasse.
Polimorfismo significa che gli oggetti cambiano il loro comportamento pur mantenendo la stessa
forma.
Esempio
La dichiarazione di un oggetto di una sottoclasse di libro, ex:
VolumeLibro inferno = new VolumeLibro("Dante", "Divina Commedia (Inferno)", 1) ;
sipotrebbe senza problemi scrivere così:
Libro inferno = new VolumeLibro("Dante", "Divina Commedia (Inferno)", 1) ;
Da ora in poi l'oggetto "inferno" può essere trattato come se fosse un'istanza di Libro, tanto ne
possiede sicuramente tutti gli attributi e i metodi.
Upcasting
Il tipo di assegnameneto appena visto si chiama "upcasting", cioè assegnare un oggetto ad una
variabile di tipo di una classe superiore. L'upcasting è fatto in automatico, come quando si assegna
un float ad un double.
Attenzione però. Una volta fatto l'assegnamento ad un tipo superiore, non posso più accedere ad un
membro della sottoclasse.
Downcasting
Il downcasting è l'inverso dell'upcasting. Ex:
VolumeLibro vol1 = (VolumeLibro)inferno ;
Attenzione: è un assegnamento che potrebbe dare errori a run-time. Ad esempio se faccio:
LibroCatalogato libro_22 = (LibroCatalogato)inferno ;
La compilazione funziona, ma ottengo una ClassCastException durante l'esecuzione.
Esempio
Nell'esempio della biblioteca, posso continuare ad usare la biblioteca per contenere libri di sottotipi
diversi.
Vedere ProvaBiblioteca2.java
Membri final
La parola chiave "final" impedisce ad un attributo o ad un metodo di essere ridefiniti nelle
sottoclassi.
Overriding e Binding dinamico
I meccanismi di overriding e binding dinamico caratterizzano la programmazionead oggetti.
Rispetto ad una programmazione tradizionale, questi meccanismi eliminano la necessità di
"tempestare" i programmi di costrutti "switch" o "else-if". Questo facilita l'ampliamento dei
programmi.
Overriding
Le sottoclassi possono ridefinire i metodi della loro superclasse. Questo si chiama overriding. In
genere viene fatto perchè estendendo una classe, alcuni metodi hanno bisogno di essere modificati,
o riscritti affinchè il loro significato rimanga coerente con il significato delle informazioni
contenutte nella sottoclasse.
Esempio
Nelle classi LibroCatalogato.java e VolumeLibro.java il metodo getInformazioni() è soggetto ad
overriding sia nella classe LibroCatalogato che in VolumeLibro. La sua estensione è stata necessaria
per stampare le informazioni aggiuntive dei due sottotipi di Libro.
Binding Dinamico
Quando si invoca un metodo di una classe, il motore di esecuzione di un programma ad oggetti
controlla di quale tipo è esattamente un oggetto. Infatti, come detto prima, l'oggetto potrebbe essere
anche di un sottotipo di quello dichiarato. Se il metodo è stato ridefinito nelle sottoclassi
(overriding), viene eseguita la versione "più in basso" nella gerarchia delle classi.
Esempio
Nella classe Biblioteca non è stato necessario modificare niente!!! Quando vengono richieste le
informazioni sui libri contenuti nella biblioteca, il meccanismo di binding dinamico individua
automaticamente che un oggetto del vettore non è della classe Libri, ma di una sottoclasse, ed
invoca il metodo della sottoclasse appropriata.
Vedere ProvaBiblioteca2.java
Questo funziona perchè l'associazione (binding) tra il nome di un metodo e la classe a cui appartiene
viene fatto a run-time (dinamicamente). Da cui il nome binding dinamico.
Classi astratte
Le classi astratte sono classi contenenti metodi astratti. I metodi astratti sono metodi di cui viene
dichiarata sltanto l'intestazione, senza il corpo. Le classi astratte sono in genere create per preparare
del codice ed impostarne l'ordine di esecuzione. Le classi astratte sono pensate per essere estese da
chi ha bisogno di sfruttare tale codice.
Si usa la parola chiave abstract per dichiarare una classe ed un metodo astratti.
Esempio
Per fornirvi il codice base che apre una finestra vi abbiamo dato il codice di una classe dentro il
quale scrivere i vostri programmi. Invece sarebbe stato più opportuno firnirvi una classe astratta con
il codice già fatto.
Vedere FinestraBaseAstratta.java e MiaFinestra.java
I vostri programmi grafici possono essere scritti estendendo la classe astratta ed implementando il
metodo astratto "disegna()" opportunamente preparato.
Notare che rispetto all'approccio precedente, in questo caso voi potete sfruttare del codice già fatto
senza bisogno del sorgente, basterebbe allegare una documentazione ben fatta.
In realtà i commenti ai metodi ed alla classe sono già una documentazione esaustiva all'uso della
classe. Più avanti vedremo che esistono dei toos per generare automaticamente la documentazione a
partire dai commenti.
Organizzare le classi in packages
Quando si fanno grosse applicazioni, che possono contenere migliaia di classi, di solito le si
organizza in packages. Un package è una collezioni di classi. I packages sono organizzati
gerarchicamente, come i files e le directory.
Infatti, ad ogni package cossisponde una directory che contiene i files .java delle classi di quel
package.
Le classi appartenenti ad un package devono avere la dichiarazione del package di appartenenza
come prima linea del codice sorgente (commenti esclusi):
package <nomePackage> ;
Le classi che fanno uso di classi di altri packages devono importare la classi esterne con l'istruzione
import:
import <nomepackage>.<nomeclasse> ;
se ci sono più packages innestati, si nominano separati dal punto:
import <nomepackage1>.<nomepackage2>....<nomepackageN>.<nomeclasse> ;
Si possono importare tutte le classi di un package usando l'asterisco:
import <nomepackage>.* ;
Esempio
Le classi Rettangolo e Cerchio sono state messe nel package "forme".
Vedere directory forme ed i files Retangolo.java e Cerchio.java
La classe Graphics è definita nel package java.awt:
import java.awt.Graphics ;
Oppure, posso importare il Graphics insieme a tutte le classi del suo package con:
import java.awt.* ;
Sub-packages
I packages, come le directories, posso essere infinitamente innestati. Tuttavia, l'import non è
ricorsivo. Importare un package non vuol dire che importo tutti i sub-packages. Facendo:
import java.* ;
l'import NON va a cercare nel package awt, dunque NON importa la classe Graphics.
Compilazione
Per compilare le classi di un package devo posizionarmi nella directory base/root, fuori da tutti i
packages. Dunque se nell'esempio della biblioteca io dovevo entrare nella directory "biblioteca" per
compilare ed eseguire le classi, nel caso delle forme io DEVO stare fuori dalla directory "forme":
> javac forme\Forma.java
> javac forme\Rettangolo.java
> javac forme\ProvaForme.java
oppure
> javac forme\*.java
In Unix e Mac invece delle "\" (backslash) si usano le "/" (slash).
Esecuzione
Sempre posizionato fuori dalla directory del package, invoco una classe spacificandone il nome
come negli import, cioè usando il punto (".") per separare i nomi dei package e delle classi.
> java forme.ProvaForme
Interfacce
Le interfacce descrivono un elenco di metodi senza codice. Si possono considerare come delle
classe astratte con tutti i metodi astratti e senza costruttori.
Una classe implementa un'interfaccia se definisce tutti imetodi dichiarati nell'interfaccia. La sintassi
è:
interface <nomeInterfaccia> {
// ... elenco metodi
}
Interfacce e polimorfismo
In riferimento al polimorfismo, anche le interfacce dichiarano dei tipi nuovi. Dunque valgono tutti i
fenomeni di polimorfismo, upcasting e downcasting descritti prima.
Vedere il package "forme"
Rappresentazione grafica
Anche le interfacce vengono rappresentate graficamente con dei rettangoli. Per distinguere le
interfacce dalle classi si può scrivere il nome dell'interfaccia in "italic" invece di testo normale.
L'esempio delle forme può essere rappresentato così.
Forma
Rettangolo
Cerchio
Implementare più interfacce
La cosa si fa interessante considerando che una classe può estendere un'altra classe ed implementare
più interfacce contemporaneamente.
class <nomeClasse> extends <superClasse> implements <interfaccia1>,
<interfaccia2>, ... , <interfacciaN> { ... }
Dunque si possono creare classi i cui oggetti sono contemporaneamente di più tipi.
Estendere interfacce
Anche le interfacce si possono estendere tra di loro:
interface <nomeInterfaccia> extends <superInterfaccia> {
// ... elenco nuovi metodi
}
Visibilità o "Controllo di Accesso"
Quando si cominciano a sviluppare applicazioni con molte classi è necessario decidere per ogni
classe, e per ogni metodo di una classe, chi può farne uso.
Per esempio, una classe con molte funzionalità potrebbe essere molto complessa, e contenere molti
attributi e metodi. Tuttavia, solo alcuni di questi attributi e metodi servono a chi vuole fare uso della
classe. Dunque, alcuni metodi devono essere cisibili all'esterno, mentre altri devono restare
invisibili se non a chi mantiene il codice della classe.
Per controllare l'accesso ai membri di una classe si usano le parole chiave public, protected, private
e package (oppure nulla, è il default):
Specifier
class subclass package
private
X
protected
X
X
X
public
X
X
X
package (o niente)
X
world
X
X
Queste parole chiave si mettono davanti al tipo dell'attributo o davanti al tipo di ritorno di un
metodo o davanti alla dichiarazione di una classe.
– Public significa che la classe o il membro è visibile a chiunque.
–
Protected significa che la classe o il membro è visibile da classi dello stesso package o sottoclassi
nello stesso package.
–
Package significa che la classe o il membro sono visibili solo da classi dello stesso package.
–
Private significa che i membri sono visibili solo dalla classe stessa.
Esempio
Nella classe Libro, la descrizione può essere molto lunga. Dunque, vale la pena di comprimerla con
qualche algoritmo (tipo Zip). Però, non voglio che chi usa la classe libro debba preoccuparsene.
Per fare questo nego la visibilità dell'attributo "descrizione", e metto disposizione dei metodi che
nascondono i dettagli della compressione
public class Libro {
...
// Vettore di bytes che contiene la descrizione compressa
private byte[] descrizione ;
...
public void setDescrizione(String descrizione) {
// algoritmo di compressione che trasforma la stringa in bytes
}
public String getDescrizione() {
// algoritmo di decompressione che restituisce il testo originario
}
Getters e setters
Quello precedente è il tipico esempio del perchè è meglio non lasciare mai visibili all'esterno gli
attributi di una classe. (Molto) Spesso si nascondono tutti gli attributi di una classe e si creano dei
metodi getAttributo() e setAttributo(valore) per leggerne ed impostarne i valori. I metodi di get e set
vengono chiamati getters e setters. Come abbiamo visto può capitare che concettualmente settiamo
qualcosa che in realtà può non essere un attributo.