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.