SlideShare uma empresa Scribd logo
1 de 103
Baixar para ler offline
Introduzione alla Programmazione
Orientata agli Oggetti in Java
Versione 1.1

Eugenio Polìto
Stefania Iaffaldano
Ultimo aggiornamento: 1 Novembre 2003
 

Copyright c 2003 Eugenio Polìto, Stefania Iaffaldano. All rights reserved.
This document is free; you can redistribute it and/or modify it under the terms of
the GNU General Public License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This document is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this
document; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
Boston, MA 02111-1307 USA.

Sosteniamo la Cultura Libera:
sosteniamo il Free Software
Per la segnalazione di errori e suggerimenti, potete contattarci ai seguenti
indirizzi:
Web: http://www.eugeniopolito.it
E-Mail: eugeniopolito@eugeniopolito.it

A
Typeset with L TEX.
Ringraziamenti
Un grazie speciale ad OldDrake, Andy83 e Francesco Costa per il
prezioso aiuto che hanno offerto!
Indice
1

Introduzione

8

I Teoria della OOP
2

II
3

Le idee fondamentali
2.1 Una breve storia della programmazione . . . . . . . . . .
2.2 I princìpi della OOP . . . . . . . . . . . . . . . . . . . . .
2.3 ADT: creare nuovi tipi . . . . . . . . . . . . . . . . . . .
2.4 La classe: implementare gli ADT tramite l’incapsulamento
2.5 L’oggetto . . . . . . . . . . . . . . . . . . . . . . . . . .
2.6 Le relazioni fra le classi . . . . . . . . . . . . . . . . . . .
2.6.1 Uso . . . . . . . . . . . . . . . . . . . . . . . . .
2.6.2 Aggregazione . . . . . . . . . . . . . . . . . . . .
2.6.3 Ereditarietà . . . . . . . . . . . . . . . . . . . . .
2.6.4 Classi astratte . . . . . . . . . . . . . . . . . . . .
2.6.5 Ereditarietà multipla . . . . . . . . . . . . . . . .
2.7 Binding dinamico e Polimorfismo . . . . . . . . . . . . .

9
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

La OOP in Java
Classi e oggetti
3.1 Definire una classe . . . . . . . . . . . . . . . . . . . . . . .
3.2 Garantire l’incapsulamento: metodi pubblici e attributi privati
3.3 Metodi ed attributi statici . . . . . . . . . . . . . . . . . . . .
3.4 Costruire un oggetto . . . . . . . . . . . . . . . . . . . . . .
3.5 La classe Persona e l’oggetto eugenio . . . . . . . . . . . .
3.6 Realizzare le relazioni fra classi . . . . . . . . . . . . . . . .
3.6.1 Uso . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.6.2 Metodi static: un esempio . . . . . . . . . . . . . .
3.6.3 Aggregazione . . . . . . . . . . . . . . . . . . . . . .
3.6.4 Ereditarietà . . . . . . . . . . . . . . . . . . . . . . .
3.7 Classi astratte . . . . . . . . . . . . . . . . . . . . . . . . . .
3.8 Interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.9 Ereditarietà multipla . . . . . . . . . . . . . . . . . . . . . .

9
9
12
13
15
16
17
17
18
18
22
23
27

29
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

29
29
29
31
32
34
37
37
39
41
44
52
56
60
4

Le operazioni sugli oggetti
4.1 Copia e clonazione . .
4.2 Confronto . . . . . . .
4.3 Binding dinamico . . .
4.4 Serializzazione . . . .

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

III APPENDICI
5

6

Una panoramica sul linguaggio
5.1 Tipi primitivi . . . . . . . . . .
5.2 Variabili . . . . . . . . . . . . .
5.3 Operatori . . . . . . . . . . . .
5.3.1 Operatori Aritmetici . .
5.3.2 Operatori relazionali . .
5.3.3 Operatori booleani . . .
5.3.4 Operatori su bit . . . . .
5.4 Blocchi . . . . . . . . . . . . .
5.5 Controllo del flusso . . . . . . .
5.6 Operazioni (Metodi) . . . . . .
5.6.1 Il main . . . . . . . . .
5.6.2 I package . . . . . . . .
5.6.3 Gli stream . . . . . . . .
5.6.4 L’I/O a linea di comando
5.6.5 Le eccezioni . . . . . .
5.6.6 Installazione del JDK . .
La licenza GNU GPL

67
67
73
76
79

83
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

83
83
84
84
84
85
85
85
86
86
87
89
89
90
91
91
93
94
Elenco delle figure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

Una classe in UML. . . . . . . . . . . . . . . . . . . . . . . . . .
La classe Persona in UML. . . . . . . . . . . . . . . . . . . . .
La relazione d’uso in UML. . . . . . . . . . . . . . . . . . . . . .
La relazione di aggregazione in UML. . . . . . . . . . . . . . . .
La relazione di ereditarietà in UML. . . . . . . . . . . . . . . . .
La classe delle persone come sottoclasse della classe degli animali.
Una piccola gerarchia di Animali. . . . . . . . . . . . . . . . . .
Una piccola gerarchia di Animali in UML. . . . . . . . . . . . . .
Matrimonio fra classe concreta e astratta . . . . . . . . . . . . .
Composizione: la classe Studente Lavoratore . . . . . . . . .
Studente Lavoratore come aggregazione e specializzazione . .
Studente Lavoratore come aggregazione . . . . . . . . . . . .
L’ex Studente ed ex Lavoratore ora Disoccupato . . . . . . .
La classe Studente come sottoclasse di Persona . . . . . . . . .
L’oggetto primo appena creato . . . . . . . . . . . . . . . . . . .
L’oggetto primo non ancora creato . . . . . . . . . . . . . . . . .
L’oggetto eugenio dopo la costruzione . . . . . . . . . . . . . .
Esecuzione del programma Applicazione.java . . . . . . . . .
Esecuzione del programma Applicazione.java . . . . . . . . .
Diagramma UML per interface . . . . . . . . . . . . . . . . .
Diagramma UML per implements . . . . . . . . . . . . . . . . .
Diagramma UML per il matrimonio fra classe concreta ed interfaccia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Una Pila di numeri interi . . . . . . . . . . . . . . . . . . . . . .
Esecuzione di ProvaPila . . . . . . . . . . . . . . . . . . . . . .
Gli oggetti primo e secondo dopo la creazione . . . . . . . . . .
Gli oggetti primo e secondo dopo l’assegnamento secondo =
primo; . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Gli oggetti primo e secondo dopo la clonazione . . . . . . . . . .
Gli oggetti primo e secondo dopo l’istruzione secondo.set(16);
Un oggetto ha sempre un riferimento implicito ad Object . . . . .
L’oggetto bill istanza di Cane . . . . . . . . . . . . . . . . . . .
base = derivata; . . . . . . . . . . . . . . . . . . . . . . . . .
Passaggio dei parametri ad una funzione . . . . . . . . . . . . . .

15
16
17
18
19
20
21
21
23
23
24
25
25
27
33
33
35
51
55
56
59
60
60
64
69
69
71
72
74
75
78
88
Elenco delle tabelle
1
2

Pro e contro dell’uso di ereditarietà e aggregazione . . . . . . . .
Tipi primitivi di Java . . . . . . . . . . . . . . . . . . . . . . . .

20
83
1 INTRODUZIONE

1

8

Introduzione

Questa trattazione sulla Programmazione Orientata agli Oggetti o Object Oriented Programming (OOP in seguito) in Java nasce dall’osservazione che, molto
spesso, i manuali sono degli ottimi reference (cfr. [3]) del linguaggio ma non approfondiscono i pochi ma essenziali concetti della OOP; d’altra parte nei corsi
universitari si presta fin troppa attenzione alla teoria senza approfondire gli aspetti
implementativi.
Quindi si cercherà di trattare sia la teoria che la pratica della OOP perché entrambi gli aspetti sono fondamentali per poter scrivere un buon codice orientato agli
oggetti.
Purtroppo Java viene considerato (in modo errato!) un linguaggio per scrivere
soltanto applet o addirittura viene confuso con il linguaggio di script JavaScript:
con Java si possono scrivere delle applicazioni standalone che non hanno molto
da invidiare (a livello di prestazioni) ai programmi scritti con altri linguaggi OOP
più efficienti come C++. Infatti, attualmente, la Java Virtual Machine (cioé lo
strato software che si occupa di trasformare il codice intermedio o bytecode in
chiamate alle funzioni del Sistema Operativo - syscall) consente di avere delle
ottime prestazioni.
In [1] potete trovare la versione 1.4 del JDK.
È importante sottolineare che questo non è un manuale sul linguaggio, ma è una
trattazione sull’impostazione Object Oriented del linguaggio: comunque, se non
si conosce la sintassi di Java, è consigliabile dare uno sguardo alla sezione 5 dove
è disponibile un veloce riassunto sulla struttura del linguaggio.
9

Parte I

Teoria della OOP
In questa parte verranno introdotte e discusse le idee fondamentali della OOP. Se
si conoscono già i princìpi ed i meccanismi della OOP si può passare alla parte
successiva, dove si discute come tali concetti sono realizzati in Java.

2
2.1

Le idee fondamentali
Una breve storia della programmazione

La programmazione dei computer esiste ormai da circa 60 anni. Ovviamente in
un tempo così lungo, ha subìto notevoli cambiamenti: agli albori l’arte di programmare consisteva nella programmazione di ogni singolo bit di informazione
tramite degli interruttori che venivano accesi e spenti. Quindi il programma era in
realtà una sequenza di accensioni e spegnimenti di interruttori che producevano il
risultato che si voleva, anzi che si sperava di ottenere: pensate cosa poteva comportare lo scambio di una accensione con uno spegnimento o viceversa!
Solo intorno al 1960 venne scritto il primo Assembler: questo linguaggio era (ed
è) troppo legato alla struttura del microprocessore e soprattutto era (ed è) difficile scrivere un programma con un linguaggio troppo vicino alla macchina e poco
amichevole per l’uomo.
Tra il 1969 ed il 1971, Dennis Ritchie scrisse il primo linguaggio di programmazione ad alto livello: il C. Seppure è ad un gradino superiore all’assembler e
soprattutto il codice sorgente di un programma può essere ricompilato su qualsiasi piattaforma (con qualche modifica!), questo linguaggio risulta ancora troppo
vicino alla macchina (basti pensare che i Sistemi Operativi sono scritti in C e Assembler).
Nel 1983 Bijarne Stroustrup (programmatore presso la compagnia telefonica americana Bell Laboratories) ebbe la necessità di dover simulare un sistema telefonico:
i linguaggi allora disponibili si prestavano poco a programmare un sistema così
complesso, così ebbe l’idea di partire dal C per scrivere un nuovo linguaggio che
supportasse le classi. Nacque così il C con oggetti. L’idea di classe era già nota
ed utilizzata in altri linguaggi come Smalltalk.
Tuttavia il C con oggetti (in seguito rinominato C++) è una estensione del C e
quindi ha dei pregi e dei difetti:
pro fra i pregi possiamo sicuramente annoverare l’efficienza (propria di C) e la
portabilità del codice sorgente (con qualche modifica) da una archittettura ad un’altra: i compilatori C++ esistono per ogni tipo di piattaforma
2 LE IDEE FONDAMENTALI

10

hardware, pertanto è sufficiente qualche modifica al codice sorgente ed una
ricompilazione;
contro la gestione della memoria, per esempio, è completamente a carico del
programmatore e, come si sa, la gestione dei puntatori è una fabbrica di
errori.
È inoltre possibile mixare sia codice C che codice C++, ottenendo così un
codice non molto pulito dal punto di vista della OOP: supponiamo di volere
confrontare due variabili a e b. Il codice corretto è il seguente:
if (a == b) {...}

Ma basta omettere un ’=’ per ottenere un assegnamento:
if (a = b) {...}

se l’assegnamento va a buon fine, viene eseguito il blocco di istruzioni. Un
errore del genere può essere frequente: se non usato bene, il C++ rischia di
essere un “boomerang” per il programmatore.
Nei primi mesi del 1996 venne presentata la prima versione del linguaggio Java
che introduceva non poche novità:
macchina virtuale anche se non era un concetto nuovo nell’informatica (già IBM
aveva fatto delle sperimentazioni sulle Virtual Machine in un Sistema Operativo proprietario), l’idea di sfruttare una Macchina Virtuale o Java Virtual
Machine - JVM che si interpone fra il linguaggio intermedio bytecode ed
il linguaggio macchina della architettura sottostante era una novità assoluta.
Tale idea è stata ultimamente ripresa da una nota Software House con un
progetto denominato .NET . . .
portabilità grazie al concetto di Virtual Machine è sufficiente compilare una sola volta il programma per poi eseguire il programma .class in formato di
bytecode su qualsiasi altra piattaforma; per C++ vale solo una portabilità di
codice sorgente e non di programma eseguibile;
esecuzione di programmi nei browser (applet) si può scrivere una unica applicazione che venga eseguita sia in modo nativo che all’interno di un browser
web;
gestione automatica della memoria questo è sicuramente uno degli aspetti più
importanti di Java. Ci preoccupiamo solo della costruzione di un oggetto,
perchè la distruzione è completamente gestita dalla JVM tramite il Garbage
2 LE IDEE FONDAMENTALI

11

Collector: un oggetto non più utilizzato viene automaticamente distrutto.
Inoltre in Java esiste solo il concetto di riferimento ad un oggetto che
comporta una gestione semplice della notazione (si pensi alla notazione . o
-> di C++ a seconda che un metodo sia richiamato su una variabile oggetto
/ reference o su un puntatore). L’assenza della gestione diretta dei puntatori
consente di produrre un codice sicuro.
Per un confronto fra Java e C++ cfr. [4].
2 LE IDEE FONDAMENTALI

2.2

12

I princìpi della OOP

La OOP è una evoluzione naturale dei linguaggi di programmazione: essa nasce
con lo scopo preciso di simulare e modellare la realtà. I princìpi su cui si basa la
OOP sono semplici ma molto potenti:

¡

Definire nuovi tipi di dati.

¡

Incapsulare i valori e le operazioni.

¡

Riusare il codice esistente (ereditarietà).

¡

Fornire il polimorfismo.

Come vedremo, nella OOP non si fa differenza fra valori ed operazioni: semplicemente si parla di Tipo di dato che ingloba le due entità in un’unica struttura.
Quindi è necessario definire un nuovo Tipo di dato.
È altrettanto necessario accedere ad un valore di un tipo di dato: come vedremo
questo è fattibile tramite il meccanismo di incapsulamento.
Un altro cardine della OOP è il riuso del codice: cioé utilizzare del codice esistente per poterlo specializzare.
Il polimorfismo si rende necessario, come vedremo, in una gerarchia di ereditarietà.
2 LE IDEE FONDAMENTALI

2.3

13

ADT: creare nuovi tipi

Un Tipo di Dato Astratto o Abstract Data Type - ADT è, per definizione, un
nuovo tipo di dato che estende i tipi nativi forniti dal linguaggio di programmazione.
Un ADT è caratterizzato da un insieme di:

¡

dati;

¡

operazioni che agiscono sui dati, leggengoli/scrivendoli;

Fin qui niente di nuovo: anche i linguaggi procedurali, come per esempio C, consentono di definire un ADT. Ma, mentre per tali linguaggi chiunque può avere
accesso ai dati e modificarli, i linguaggi Object Oriented ne garantiscono la loro
riservatezza.
Supponiamo infatti di voler definire in C (non preoccuparsi della sintassi) un ADT
Persona cioé una struttura dati che mantenga le informazioni (dati) di una Persona, come, per esempio, il nome, il cognome e la data di nascita e che consenta
di creare e stampare le informazioni di una persona (operazioni):
/* Struttura dati per mantenere la data di nascita */
struct Data {
int giorno;
int mese;
int anno;
};
/* Struttura dati per mantenere le info. della persona */
struct Persona {
struct Data *data_di_nascita;
char *nome;
char *cognome;
};
/* Setta le info. della persona */
void creaPersona(struct Persona *persona)
{
persona->data_di_nascita->giorno = 31;
persona->data_di_nascita->mese = 12;
persona->data_di_nascita->anno = 1976;
persona->nome = "Eugenio";
persona->cognome = "Polito";
}
2 LE IDEE FONDAMENTALI
/* Stampa le info. della persona */
void stampaDati(struct Persona *persona)
{
printf("Mi chiamo %s %s e sono nato il %i-%i-%i n",
persona->nome,
persona->cognome,
persona->data_di_nascita->giorno,
persona->data_di_nascita->mese,
persona->data_di_nascita->anno);
}
/* crea un puntatore alla struttura e lo inizializza;
quindi stampa le info. */
int main()
{
struct Persona *io;
creaPersona(io);
stampaDati(io);
return 0;
}

Se eseguiamo questo programma, otteniamo il seguente output:
Mi chiamo Eugenio Polito e sono nato il 31-12-1976

Proviamo adesso a modificare il main nel seguente modo:
int main()
{
struct Persona *io;
creaPersona(io);
io->data_di_nascita->mese = 2;
stampaDati(io);
return 0;
}

Questa volta l’output è:
Mi chiamo Eugenio Polito e sono nato il 31-2-1976

Cioè le mie informazioni private sono state modificate con l’assegnamento:
io->data_di_nascita->mese = 2

14
2 LE IDEE FONDAMENTALI

2.4

15

La classe: implementare gli ADT tramite l’incapsulamento

La classe consente di implementare gli ADT attraverso il meccanismo di incapsulamento: i dati devono rimanere privati insieme all’implementazione e solo
l’interfaccia delle operazioni è resa pubblica all’esterno della classe.
Questo approccio è fondamentale per garantire che nessuno possa accedere alle
informazioni della classe e quindi, dal punto di vista del programmatore, è una
garanzia per non fare errori nella stesura del codice: basti pensare all’esempio
dell’ADT Persona visto prima. Se i dati fossero stati privati non avrei potuto
liberamente modificare la data di nascita nel main. Quindi, ricapitolando, una
classe implementa un ADT (un sinonimo di classe è proprio tipo) attraverso il
meccanismo di incapsulamento.
La descrizione di una classe deve elencare:
i dati (o attributi): contengono le informazioni di un oggetto;
le operazioni (o metodi): consentono di leggere/scrivere gli attributi di un oggetto;
Quando si scrive una applicazione è buona norma iniziare con la progettazione
dell’applicazione stessa; Grady Booch identifica i seguenti obiettivi in questa fase:

¡

identificare le classi;

¡

identificare le funzionalità di queste classi;

¡

trovare le relazioni fra le classi;

Questo processo non può che essere iterativo.
Nella fase di progettazione si usa un formalismo grafico per rappresentare le
classi e le relazioni fra di esse: l’UML - Unified Modeling Language.
In UML una classe si rappresenta così:
Qui va il nome
della classe
Qui vanno messi
gli attributi
Qui vanno messi
i metodi

Figura 1: Una classe in UML.
2 LE IDEE FONDAMENTALI

16

Quindi la classe Persona in UML è così rappresentata:

Figura 2: La classe Persona in UML.

2.5

L’oggetto

Che cos’è quindi un oggetto?
Per definizione, diciamo che un oggetto è una istanza di una classe. Quindi un
oggetto deve essere conforme alla descrizione di una classe.
Un oggetto pertanto è contraddistinto da:
1. attributi;
2. metodi;
3. identità;
Allora se abbiamo una classe Persona, possiamo creare l’oggetto eugenio che è
una istanza di Persona. Tale oggetto avrà degli attributi come, per esempio, nome,
cognome e data di nascita; avrà dei metodi come creaPersona(...) , stampaDati(...), etc. Inoltre avrà una identità che lo contraddistingue da un eventuale fratello gemello, diciamo pippo (anche lui ovviamente istanza di Persona).
Per il meccanismo di incapsulamento un oggetto non deve mai manipolare direttamente i dati di un altro oggetto: la comunicazione deve avvenire tramite
messaggi (cioé chiamate a metodi). I client devono inviare messaggi ai server!
Quindi nell’esempio di prima: se il fratello gemello pippo, in un momento di
amnesia, vuole sapere quando è nato eugenio deve inviargli un messaggio, cioé
deve richiamare un metodo ottieniDataDiNascita(...) .
Quindi, ricapitolando, possiamo dire che:

¡

la classe è una entità statica cioé a tempo di compilazione;

¡

l’oggetto è una entità dinamica cioé a tempo di esecuzione (run time);

Nella sezione 4 vedremo come gli oggetti vengono gestiti in Java.
2 LE IDEE FONDAMENTALI

2.6

17

Le relazioni fra le classi

Un aspetto importante della OOP è la possibilità di definire delle relazioni fra le
classi per riuscire a simulare e modellare il mondo che ci circonda :
uso: una classe può usare oggetti di un’altra classe;
aggregazione: una classe può avere oggetti di un’altra classe;
ereditarietà: una classe può estendere un’altra classe.
Vediamole in dettaglio singolarmente.
2.6.1

Uso

L’uso o associazione è la relazione più semplice che intercorre fra due classi.
Per definizione diciamo che una classe A usa una classe B se:
- un metodo della classe A invia messaggi agli oggetti della classe B, oppure
- un metodo della classe A crea, restituisce, riceve oggetti della classe B.
Per esempio l’oggetto eugenio (istanza di Persona) usa l’oggetto phobos (istanza di Computer) per programmare: quindi l’oggetto eugenio ha un metodo (diciamo programma(...)) che usa phobos (tale oggetto avrà per esempio
un metodo scrivi(...)). Osserviamo ancòra che in questo modo l’incapsulamento è garantito: infatti eugenio non può accedere direttamente agli attributi
privati di phobos, come ram o bus (è il Sistema Operativo che gestisce tali
risorse). Questo discorso può valere per Linux che nasconde bene le risorse,
ma non può valere per altri Sistemi Operativi che avvertono l’avvenuto accesso a
parti di memoria riservate al kernel ed invitano a resettare il computer. . .
In UML questa relazione si rappresenta così:

Figura 3: La relazione d’uso in UML.
Per la realizzazione di questa relazione in Java vedere la sezione 3.6.1.
2 LE IDEE FONDAMENTALI
2.6.2

18

Aggregazione

Per definizione si dice che una classe A aggrega (contiene) oggetti di una classe
B quando la classe A contiene oggetti della classe B. Pertanto tale relazione è un
caso speciale della relazione di uso. Sugli oggetti aggregati sarà possibile chiamare tutti i metodi, ma ovviamente non sarà possibile accedere agli attributi (l’incapsulamento continua a “regnare”!).
N.B.: la relazione di aggregazione viene anche chiamata relazione has-a o ha-un.
Ritorniamo al nostro esempio della classe Persona: come si è detto una persona
ha una data di nascita. Risulta pertanto immediato e spontaneo aggregare un
oggetto della classe Data nella classe Persona!
In UML la relazione A aggrega B si disegna così:

Figura 4: La relazione di aggregazione in UML.
Notare che il rombo è attaccato alla classe che contiene l’altra.
Un oggetto aggregato è semplicemente un attributo!
Vi rimando alla sezione 3.6.3 per la realizzazione in Java di tale relazione.
2.6.3

Ereditarietà

Questa relazione (anche detta inheritance o specializzazione) è sicuramente la più
importante perché rende possibile il riuso del codice.
Si dice che una classe D (detta la classe derivata o sottoclasse) eredita da una
classe B (detta la classe base o superclasse) se gli oggetti di D formano un sottoinsieme degli oggetti della classe base B. Tale relazione è anche detta relazione
is-a o è-un. Inoltre si dice che D è un sottotipo di B.
Da questa definizione possiamo osservare che la relazione di ereditarietà è la
relazione binaria di sottoinsieme , cioé:
A

¢

¢

è una relazione che gode della proprietà transitiva:

¢

B

A

C

£

¢

C

¢

¢

Sappiamo che

D

A

Pertanto la relazione di ereditarietà è transitiva!
Nasce spontaneo domandarsi perché vale e non vale .
Il motivo è presto detto: la relazione è una relazione d’ordine fra insiemi, quindi
gode di tre proprietà:

¤

¢

¤
2 LE IDEE FONDAMENTALI
A

£

A

A
C

¤

¥

¤

¤

B B

¤

¥

¤

3. transitiva: C

B B

£

2. antisimmetrica: A

A

¦

¤

1. riflessiva: A

19

B

A

Ma riflettendo sul concetto di ereditarietà, affinché si verifichi la 1. dovrebbe
succedere che una classe erediti da se stessa, cioé la classe dovrebbe essere una
classe derivata da se stessa: impossibile!
Analogamente la proprietà 2. dice che una classe è una classe base ed una classe
derivata allo stesso tempo: anche questo è impossibile! Quindi vale solo la 3.
D eredita da B, in UML, si disegna così:

Figura 5: La relazione di ereditarietà in UML.
Vediamo adesso perché con l’ereditarietà si ottiene il riuso del codice.
Consideriamo una classe base B che ha un metodo f(...) ed una classe derivata
D che eredita da B. La classe D può usare il metodo f(...) in tre modi:
lo eredita: quindi f(...) può essere usato come se fosse un metodo di D;
lo riscrive (override): cioé si da un nuovo significato al metodo riscrivendo la
sua implementazione nella classe derivata, in modo che tale metodo esegua
una azione diversa;
lo estende: cioé richiama il metodo f(...) della classe base ed aggiunge altre
operazioni.
È immediato, pertanto, osservare che la classe derivata può risultare più grande
della classe base relativamente alle operazioni ed agli attributi. La classe derivata
non potrà accedere agli attributi della classe base, anche se li eredita, proprio per
garantire l’incapsulamento. Tuttavia, come vedremo, è possibile avere un accesso
2 LE IDEE FONDAMENTALI

20

controllato agli attributi della classe base da una classe derivata.
È importante notare che l’ereditarietà può essere simulata con l’aggregazione
(cioé is-a diventa has-a)!
Ovviamente ci sono dei pro e dei contro, che possiamo riassumere così:
Ereditarietà
Pro
polimorfismo e binding dinamico
Contro
legame stretto fra classe
base e derivata

Aggregazione
Pro
chiusura dei moduli
Contro
riscrittura dei metodi
nella classe derivata

Tabella 1: Pro e contro dell’uso di ereditarietà e aggregazione
Java non supporta l’inheritance multiplo quindi è necessario ricorrere all’aggregazione (vedere la sottosezione successiva 2.6.5).
Riprendiamo la classe Persona: pensandoci bene tale classe deriva da una
classe molto più grande, cioé la classe degli Animali:
Animali
Persone

Cani

      
¨¨¨¨¨¨¨
¨¨¨¨¨¨¨
¨¨¨¨¨¨¨
¨¨¨¨¨¨¨
¨¨¨¨¨¨¨
¨¨¨¨¨¨¨
¨¨¨¨¨¨¨
¨¨¨¨¨¨¨
¨¨¨¨¨¨¨
¨¨¨¨¨¨¨

¨§¨§¨§¨§¨§¨
© © © © ©
§§©¨§¨§¨§¨§¨§¨
¨©¨©¨©¨©¨©¨
©©¨©¨©¨©¨©¨©¨©
§¨§¨§¨§¨§¨§¨
¨©¨©¨©¨©¨©¨§
¨§¨§¨§¨§¨§¨©
§©¨§©¨§©¨§©¨§©¨§©¨©§
§©¨©¨©¨©¨©¨©¨©§©§
¨§©¨§©¨§©¨§©¨§©¨§
©§©©¨©¨©¨©¨©¨©¨©
§¨§¨§¨§¨§¨§¨
¨§¨§¨§¨§¨§¨§
©§¨©¨©¨©¨©¨©¨©
§¨§¨§¨§¨§¨§¨§
©¨©¨©¨©¨©¨©¨©
§¨§¨§¨§¨§¨§¨
¨¨¨¨¨¨§©§©§

Figura 6: La classe delle persone come sottoclasse della classe degli animali.
Quindi ogni Persona è un Animale; un oggetto di tipo Persona, come eugenio, è anche un Animale. Così come il mio cane bill è un oggetto di tipo Cane
ed anche lui fa parte della classe Animale.
Riflettiamo adesso sulle operazioni (metodi) che può fare un Animale: un animale
può mangiare, dormire, cacciare, correre, etc.
Una Persona è un Animale: di conseguenza eredita tutte le operazioni che può
fare un Animale. Lo stesso vale per la classe Cane. Ma sorge a questo punto una
domanda: una Persona mangia come un Cane? La risposta è ovviamente No!
Infatti una Persona per poter mangiare usa le proprie mani, a differenza del Cane
che fa tutto con la bocca e le zampe: quindi l’operazione del mangiare deve essere
ridefinita nella classe Persona!
2 LE IDEE FONDAMENTALI

21

Inoltre possiamo pensare a cosa possa fare in più una Persona rispetto ad un
Animale: può parlare, costruire, studiare, fare le guerre, inquinare... etc.
Quindi nella classe derivata si possono aggiungere nuove operazioni!
Si è detto precedentemente che la relazione di ereditarietà è transitiva: verifichiamo quanto detto con un esempio.
Pensiamo ancora alle classe Animale: come ci insegna Quark (. . . e la Scuola
Elementare. . . ) il mondo Animale è composto da sottoclassi come la classe dei
Mammiferi, degli Anfìbi, degli Insetti, etc. La classe dei Mammiferi è a sua
volta composta dalla classe degli Esseri Umani, dei Cani, delle Balene, etc.:
Animali
Mammiferi
Esseri
Umani

Insetti
Alati

¨¨¨¨¨
   
     
¨¨¨¨¨
¨¨¨¨¨
¨¨¨¨¨
¨¨

¨¨
¨¨
¨¨
¨¨
¨¨
¨¨
¨¨
¨¨
¨¨
¨¨

¨¨¨
 
¨¨¨
¨¨¨
¨¨¨
¨¨¨
¨¨¨
¨¨¨
¨¨¨
¨¨¨
¨¨¨
¨¨¨
¨¨¨

Cani

Non Alati

¨¨¨¨
  
¨¨¨¨
¨¨¨¨
¨¨¨¨
¨¨¨¨
¨¨¨¨
¨¨¨¨
¨¨¨¨
¨¨¨¨

Figura 7: Una piccola gerarchia di Animali.
Ripensiamo adesso al Cane: tale classe è una sottoclasse di Mammifero che a
sua volta è una sottoclasse di Animale, in UML:

Figura 8: Una piccola gerarchia di Animali in UML.
Pertanto ogni Cane è un Mammifero e, poiché ogni Mammifero è un Animale,
concludiamo che un Cane è un Animale! Pertanto ogni Cane potrà fare ogni operazione definita nella classe Animale.
Con questo esempio abbiamo anche introdotto il concetto di gerarchia di classi,
che, per definzione, è un insieme di classi che estendono una classe base comune.
2 LE IDEE FONDAMENTALI
2.6.4

22

Classi astratte

Riprendiamo l’esempio di Figura 8 ed esaminiamo i metodi della classe base
Animale: consideriamo, per esempio, l’operazione comunica(...). Se pensiamo ad un Cane tale operazione viene eseguita attraverso le espressioni della
faccia, del corpo, della coda. Un Essere Umano può espletare tale operazionr in
modo diverso: attraverso i gesti, le espressioni facciali, la parola. Un Delfino,
invece, comunica attraverso le onde sonore.
Allora che cosa significa tutto questo? Semplicemente stiamo dicendo che l’operazione comunica(...) non sappiamo come può essere realizzata nella classe
base! Un discorso analogo può essere fatto per l’operazione mangia(...).
In sostanza sappiamo che questi metodi esistono per tutte le classi che derivano
da Animale e che sono proprio tali classi a sapere come realizzare (implementare)
questi metodi.
I metodi come comunica(...), mangia(...) etc., si dicono metodi astratti o
metodi differiti: cioé si dichiarano nella classe base, ma non vengono implementati; saranno le classi derivate a sapere come implementare tali operazioni.
Una classe che ha almeno un metodo astratto si dice classe astratta e deve essere
dichiarata tale. Una classe astratta può anche contenere dei metodi non astratti
(concreti)!
Nella sezione 3.7 vedremo come dichiararle e usarle in Java.
Attraverso delle considerazioni siamo arrivati a definire la classe Animale come
classe astratta.
Riflettiamo un momento sul significato di questa definizione: creare oggetti della
classe Animale serve a ben poco, proprio perché tale classe è fin troppo generica
per essere istanziata. Piuttosto può essere usata come un contenitore di comportamenti (operazioni) comuni che ogni classe derivata sa come implementare!
Questo è un altro punto fondamentale della OOP:
È bene individuare le operazioni comuni per poterle posizionare al livello
più alto nella gerarchia di ereditarietà.
La classe Cane, Delfino etc., implementeranno ogni operazione astratta, proprio
perché ognuna di queste classi sa come fare per svolgere le operazioni ereditate
dalla classe base.
2 LE IDEE FONDAMENTALI
2.6.5

23

Ereditarietà multipla

Nella sottosezione 2.6.3 si è parlato della relazione di ereditarietà fra due classi.
Questa relazione può essere estesa a più classi.
Esistono tre forme di ereditarietà multipla:
matrimonio fra una classe concreta ed una astratta: per esempio:

Figura 9: Matrimonio fra classe concreta e astratta
Quindi Stack è la classe astratta che definisce le operazioni push(...),
pop(...), etc. La classe array serve per la manipolazione degli array.
Pertanto la Pila implementa le operazioni dello Stack e le richiama su un
array.
duplice sottotipo: una classe implementa due interfacce filtrate.
composizione: una classe estende due o più classi concrete. È proprio questo
caso che genera alcuni problemi.
Consideriamo il classico esempio (cfr. [2]) della classe Studente Lavoratore: questa classe estende la classe Studente e Lavoratore (entrambe estendono la classe Persona):

Figura 10: Composizione: la classe Studente Lavoratore
2 LE IDEE FONDAMENTALI

24

La classe Persona ha degli attributi, come nome, cognome, data di nascita etc., e dei metodi, come, per esempio, mangia(...). Sia la classe
Studente che la classe Lavoratore estendono la classe Persona, quindi erediteranno sia attributi che metodi. Supponiamo di creare l’oggetto
eugenio come istanza di Studente Lavoratore e richiamiamo su di esso il metodo mangia(...). Purtroppo tale metodo esiste sia nella classe
Studente che nella classe Lavoratore: quale metodo sarà usato? Nessuno
dei due perché il compilatore riporterà un errore in fase di compilazione!
Analogamente i membri saranno duplicati perché saranno ereditati da entrambe le classi (Studente e Lavoratore): eugenio si ritroverà con due
nomi, due cognomi e due date di nascita.
I progettisti di Java, proprio per evitare simili problemi, hanno deciso di
non supportare la composione come forma di ereditarietà multipla.
Però, come si è detto nella sezione 2.6.3, l’ereditarietà può essere simulata
con l’aggregazione, pertanto il diagramma UML di Figura 10 può essere
così ridisegnato:

Figura 11: Studente Lavoratore come aggregazione e specializzazione
Adesso lo Studente Lavoratore eredita un solo metodo mangia(...),
dorme(...), etc., così come avrà un solo nome, cognome, etc.
Se eugenio deve lavorare(. . . ) richiamerà il metodo omonimo sull’oggetto
aggregato istanza di Lavoratore.
Questo esempio porta ad un’altra riflessione importante: ma eugenio sarà
sempre uno Studente? Si spera di no. . . Prima o poi finirà di studiare!
Come si è detto in Tabella 1, l’ereditarietà ha lo svantaggio di stabilire un
legame troppo forte tra classe base e derivata. Ciò significa che l’oggetto
eugenio (che magari continua a vivere nella società in qualità di Lavoratore), anche quando non sarà più uno Studente, potrà invocare il metodo
faiLaFilaInSegreteria(...) o ricopiaAppunti(...) , perché con-
2 LE IDEE FONDAMENTALI

25

tinua ad essere uno Studente, secondo la gerarchia di Figura 11!
Risulta immediato cambiare nuovamente l’ereditarietà con l’aggregazione:

Figura 12: Studente Lavoratore come aggregazione
In questo modo, ad esempio, il metodo faiLaFilaInSegreteria(...)
viene richiamato sull’oggetto aggregato istanza di Studente. Quando eugenio non sarà più Studente, l’oggetto aggregato istanza di Studente
verrà eliminato (tanto è un semplice attributo!).
Se poi malauguratamente eugenio perde il proprio lavoro, non aggrega più
la classe Lavoratore: può comunque aggregare una nuova classe, come
per esempio Disoccupato:

Figura 13: L’ex Studente ed ex Lavoratore ora Disoccupato
Perché abbiamo aggregato una nuova classe (Disoccupato)? Se guardiamo
la Figura 11 si ha (per la transitività della relazione di ereditarietà):
Studente Lavoratore Studente Persona
Studente Lavoratore Persona.
Quindi ogni Studente Lavoratore può invocare il metodo mangia(...)
della classe Persona (lo eredita).
Analogamente, in Figura 11, vediamo che sia Studente che Lavoratore
ereditano da Persona. Quindi uno Studente Lavoratore può invocare il
metodo mangia(...) sia sull’oggetto aggregato istanza di Studente che
sull’oggetto aggregato istanza di Lavoratore.

£

¢

¢

¢
2 LE IDEE FONDAMENTALI

26

Ma se un oggetto di classe Studente Lavoratore termina di studiare e
perde il lavoro (cioé eliminiamo gli attributi, oggetti di tipo Studente e
Lavoratore) potrà continuare a mangiare? Risposta: No! Ecco spiegato il motivo per cui è stata aggregata una nuova classe in Studente
Lavoratore.
Ricapitolando:

¡

L’ereditarietà multipla sottoforma di composizione può essere modellata
con l’aggregazione e con l’ereditarietà singola. È bene usare questa combinazione per non incorrere in problemi seri durante la stesura del codice.

¡

Usare l’ereditarietà solo quando il legame fra la classe base e la classe
derivata è per sempre, cioé dura per tutta la vita degli oggetti, istanze
della classe derivata. Se tale legame non è duraturo è meglio usare l’aggregazione al posto della specializzazione.
2 LE IDEE FONDAMENTALI

2.7

27

Binding dinamico e Polimorfismo

La parola polimorfismo deriva dal greco e significa letteralmente molte forme.
Nella OOP tale termine si riferisce ai metodi: per definizione, il polimorfismo è
la capacità di un oggetto, la cui classe fa parte di una gerarchia, di chiamare la
versione corretta di un metodo.
Quindi il polimorfismo è necessario quando si ha una gerarchia di classi.
Consideriamo il seguente esempio:

Figura 14: La classe Studente come sottoclasse di Persona
Nella classe base Persona è definito il metodo calcolaSomma(...) , che,
per esempio, esegue la somma sui naturali 2+2 e restituisce 5 (in 3 vedremo come
passare argomenti ad un metodo e restituire valori); la classe derivata Studente
invece riscrive il metodo calcolaSomma(...) ed esegue la somma sui naturali
2+2 in modo corretto, restituendo 4.
N.B. Il metodo deve avere lo stesso nome, parametri e tipo di ritorno in ogni
classe, altrimenti non ha senso parlare di polimorfismo.
Creiamo adesso l’oggetto eugenio come istanza di Studente ed applichiamo il
metodo calcolaSomma(...) . L’oggetto eugenio è istanza di Studente, quindi
verrà richiamato il metodo di tale classe ed il risulato sarà 4.
Supponiamo adesso di modificare il tipo di eugenio in Persona (non ci preoccupiamo del dettaglio del linguaggio, vedremo in 4.3 come è possibile farlo in
Java): cambiare il tipo di un oggetto, istanza di una classe derivata, in tipo della classe base è possibile ed è proprio per questo motivo che è necessario il
polimorfismo; tuttavia questa conversione o cast comporta una perdita di proprietà dell’oggetto perché una classe base ha meno informazioni (metodi ed attributi)
della classe derivata.
A questo punto richiamiamo il metodo calcolaSomma(...) sull’oggetto eugenio.
Stavolta verrà richiamato il metodo della classe base: il tipo di eugenio è Persona
e quindi il risultato è 5!
Ma come è possibile invocare un metodo sullo stesso oggetto in base al suo tipo?
Ovviamente questo non può essere fatto durante la compilazione del programma, perché il metodo da invocare deve dipendere dal tipo dell’oggetto durante
2 LE IDEE FONDAMENTALI

28

l’esecuzione del programma! Per rendere possibile questo il compilatore deve
fornire il binding dinamico, cioé il compilatore non genera il codice per chiamare un metodo durante la compilazione (binding statico), ma genera il codice
per calcolare quale metodo chiamare su un oggetto in base alle informazioni sul
tipo dell’oggetto stesso durante l’esecuzione (run-time) del programma. Questo
meccanismo rende possibile il polimorfismo puro (o per sottotipo): il messaggio che è stato inviato all’oggetto eugenio era lo stesso, però ciò che cambiava
era la selezione del metodo corretto da invocare che dipendeva quindi dal tipo a
run-time dell’oggetto.
Ecco come viene invocato correttamente un metodo in una gerarchia di ereditarietà (supponiamo che il metodo venga richiamato su una sottoclasse, p.e.
Studente):

¡

la sottoclasse controlla se ha un tale metodo; in caso affermativo lo usa,
altrimenti:

¡

la classe padre si assume la responsabilità e cerca il metodo. Se lo trova
lo usa, altrimenti sarà la sua classe padre a predendere la responsabilità di
gestirlo.

Questa catena si interrompe se il metodo viene trovato, e sarà tale classe ad invocarlo, altrimenti, se non viene trovato, il compilatore segnala l’errore in fase di
compilazione. Pertanto lo stesso metodo può esistere su più livelli della gerarchia
di ereditarietà.
Il polimofismo puro non è l’unica forma di polimorfismo:
polimorfismo ad hoc (overloading) un metodo può avere lo stesso nome ma parametri diversi: il compilatore sceglie la versione corretta del metodo in
base al numero ed al tipo dei parametri. Il tipo di ritorno non viene usato
per la risoluzione, cioé se si ha un metodo con gli stessi argomenti e diverso
tipo di ritorno, il compilatore segnala un errore durante la compilazione.
Tale meccanismo è quindi risolto a tempo di compilazione.
N.B. Il polimorfismo puro invece si applica a metodi con lo stesso nome,
numero e tipo di parametri e tipo di ritorno e viene risolto a run-time.
polimorfismo parametrico è la capacità di eseguire delle operazioni su un qualsiasi tipo: questa tipologia non esiste in Java (ma può essere simulato
cfr. 3.9), perché necessita del supporto di classi parametriche. Per la
realizzazione di questo meccanismo in C++ cfr. [2].
29

Parte II

La OOP in Java
In questa parte vedremo come vengono realizzati i concetti della OOP in Java.

3
3.1

Classi e oggetti
Definire una classe

La definizione di una classe in Java avviene tramite la parola chiave class seguita
dal nome della classe. Affinché una classe sia visibile ad altre classi e quindi
istanziabile è necessario definirla public:
public class Prima
{
}

N.B. In Java ogni classe deriva dalla classe base cosmica Object: quindi anche se
non lo scriviamo esplicitamente, il compilatore si occupa di stabilire la relazione
di ereditarietà fra la nostra classe e la classe Object! Le parentesi { e } individuano l’inzio e la fine della classe ed, in generale, un blocco di istruzioni.
È bene usare la lettera maiuscola iniziale per il nome della classe; inoltre il nome
della classe deve essere lo stesso del nome del file fisico, cioé in questo caso
avremmo Prima.java (vedere la sezione 5). Affinché una classe realizzi un ADT
(cfr. sezione 2.3) è necessario definire i dati e le operazioni.

3.2

Garantire l’incapsulamento: metodi pubblici e attributi privati

Come si è detto (cfr. sezione 2.4), uno dei princìpi della OOP è l’incapsulamento: quindi è necessario definire dati (membri nella terminologia Java) privati e le
operazioni (detti anche metodi in Java) pubbliche.
Definiamo l’ADT Persona della sezione 2.3 in Java; per adesso supponiamo che
la persona abbia tre attributi nome, cognome, anni e due metodi creaPersona(...) e stampaDati(...):
3 CLASSI E OGGETTI

30

public class Persona
{
/* questo metodo inizializza gli attributi nome,
cognome ed anni
*/
public void creaPersona(String n,String c,int a)
{
nome = n;
cognome = c;
anni = a;
}
// questo metodo stampa gli attributi
public void stampaDati()
{
System.out.println(Nome: +nome);
System.out.println(Cognome: +cognome);
System.out.println(Età: +anni);
}
// attributi
private String nome;
private String cognome;
private int anni;
}

Abbiamo definito quindi gli attributi private ed i metodi public: l’incapsulamento è garantito!
Riflettiamo un attimo sulla sintassi:
attributi: la dichiarazione di un attributo richiede il modificatore di accesso
(usare sempre private!), il tipo dell’attributo (String, int, etc. che può
essere sia un tipo primitivo che una classe - vedere la sezione 5 per i tipi
primitivi del linguaggio) ed il nome dell’attibuto (anni, nome, etc.);
metodi: la dichiarazione di un metodo richiede invece: il modificatore di accesso (può essere sia public che private, in questo caso però il metodo
non potrà essere invocato da un oggetto - è bene usarlo per “funzioni di
servizio”), il tipo di ritorno che può essere: o un tipo primitivo o una
classe o void: cioè il metodo non restituisce alcuna informazione. Se il
3 CLASSI E OGGETTI

31

tipo di ritorno è diverso da void, deve essere usata l’istruzione return valore_da_restituire; come ultima istruzione del metodo; il valore_da_restituire deve avere un tipo in match col tipo di ritorno (devono essere gli
stessi tipi). Segue quindi il nome del metodo e fra parentesi tonde si specificano gli argomenti (anche detti firma del metodo): anche essi avranno
un tipo (primitivo o una classe) ed un nome; più argomenti vanno separati
da una virgola. La lista degli argomenti può essere vuota.
Nel nostro caso gli argomenti passati al metodo creaPersona(...) servono per inizializzare gli attributi: con l’assegnamento nome = n; stiamo
dicendo che all’attributo nome stiamo assegnandogli la variabile n.
// viene usato per i commenti su una singola linea, mentre /* e */ vengono usati
per scrivere commenti su più linee (il compilatore ignora i commenti).
Il metodo println(...) della classe System è usato per scrivere l’output a video
e + serve per concatenare stringhe (vedere la sezione 5).
La classe, così come è stata definita, non serve ancora a molto: vogliamo creare
oggetti che siano istanze di questa classe, sui quali possiamo invocare dei metodi. Dove andremo ad istanziare un generico oggetto di tipo Persona? Prima
di rispondere a questa domanda affrontiamo un altro discorso importante che ci
servirà per capire “alcune cose”:

3.3

Metodi ed attributi statici

Gli attributi static sono utili quando è necessario condividerli fra più oggetti,
quindi anziché avere più copie di un attributo che dovrà essere lo stesso per tutti
gli oggetti istanza della stessa classe, esso viene inzializzato una volta per tutte
e posto nella memoria statica. Un simile attributo avrà la stessa vita del programma, per esempio possiamo immaginare che in una classe Data è utile avere
memorizzato un array (cfr. 5) dei mesi dell’anno:
private static String[] mesi = {Gen,Feb,Mar,
Apr,Mag,Giu,
Lug,Ago,Set,
Ott,Nov,Dic};

Tale array sarà condiviso fra tutti gli oggetti di tipo Data.
Siccome tale array, è in realtà costante, risulta comodo definirlo tale: in Java si
usa la parola final per definire un attributo costante:
private static final String[] mesi = {Gen,Feb,Mar,
Apr,Mag,Giu,
3 CLASSI E OGGETTI

32
Lug,Ago,Set,
Ott,Nov,Dic};

Quindi mesi non è modificabile!
Allo stesso modo è possibile definire un metodo static: un tale metodo può essere richiamato senza la necessità di istanziare la classe (vedere la sottosezione
3.6.2 per un esempio).
In Java esiste un punto di inizio per ogni programma, dove poter creare l’oggetto istanza della classe ed invocare i metodi: il metodo main(...). Esso viene
richiamato prima che qualsiasi oggetto è stato istanziato, pertanto è necessario
che sia un metodo statico. La sua dichiarazione, che deve comparire in una sola
classe, è la seguente:
public static void main(String args[])
{
}

Quindi è public per poter essere visto all’esterno, è static per il motivo che si diceva prima, non ha alcun tipo di ritorno, accetta degli argomenti di tipo String
che possono essere passati a linea di comando.

3.4

Costruire un oggetto

Possiamo adesso affrontare la costruzione di un oggetto.
In Java un oggetto viene costruito con il seguente assegnamento:
Prova primo = new Prova();

Analizziamo la sintassi: stiamo dicendo che il nostro oggetto di nome primo è
una istanza della classe Prova e che lo stiamo costruendo, con l’operatore new,
attraverso il costruttore Prova().
L’oggetto che viene così creato è posto nella memoria heap (o memoria dinamica), la quale cresce e dimunisce a run-time, ogni volta che un oggetto è creato e
distrutto.
N.B. Mentre la costruzione la controlliamo noi direttamente, la distruzione viene
gestita automaticamente dalla JVM: quando un oggetto non viene più usato, la
JVM si assume la responsabilità di eliminarlo, senza che noi ce ne possiamo accorgere, tramite il meccanismo di Garbage Collection!
L’assegnamento dice che la variabile oggetto primo è un riferimento ad un oggetto, istanza della classe Prova.
Il concetto di riferimento è importante: molti pensano che Java non abbia i puntatori: sbagliato! Java non ha la sintassi da puntatore ma ne ha il comportamento.
3 CLASSI E OGGETTI

33

Infatti una variabile oggetto serve per accedere all’oggetto e non per memorizzarne le sue informazioni!; pertanto un oggetto di Java si comporta come
una variabile puntatore di C++. La gestione dei puntatori viene completamente
nascosta al programmatore, il quale può solo usare riferimenti agli oggetti.
La situazione dopo la costruzione dell’oggetto primo è la seguente:
primo

Prova

Figura 15: L’oggetto primo appena creato
Sottolineiamo che con la seguente scrittura:
Prova primo;

non è stato creato alcun oggetto, infatti si sta semplicemente dicendo che l’oggetto
primo che verrà creato sarà una istanza di Prova o di una sua sottoclasse; si ha
questa situazione:
primo

Prova

Figura 16: L’oggetto primo non ancora creato
cioé primo non è ancora un oggetto in quanto non fa riferimento a niente!
La costruzione dovrà avvenire con l’istruzione:
primo = new Prova();

Come si è detto prima, il metodo Prova() è il costruttore dell’oggetto, cioé
è il metodo che si occupa di inizializzare gli attributi dell’oggetto.
Essendo un metodo può essere overloadato, cioé può essere usato con argomenti
diversi. Un costruttore privo di argomenti si dice costruttore di default: se non
se ne fornisce nessuno, Java si occupa di crearne uno di default automaticamente
che si occupa di inizializzare gli attributi.
Il costruttore ha lo stesso nome della classe e non ha alcun tipo di ritorno.
Inoltre esso è richiamato soltanto una volta, cioé quando l’oggetto viene creato e
non può essere più richimato durante la vita dell’oggetto.
3 CLASSI E OGGETTI

3.5

34

La classe Persona e l’oggetto eugenio

Vediamo allora come scrivere una versione migliore della classe Persona, in cui
forniamo un costruttore ed un main:
public class Persona{
// Costruttore: inizializza gli attributi nome, cognome, anni
public Persona(String nome,String cognome,int anni)
{
this.nome = nome;
this.cognome = cognome;
this.anni = anni;
}
// questo metodo stampa gli attributi
public void stampaDati()
{
System.out.println(Nome: +nome);
System.out.println(Cognome: +cognome);
System.out.println(Età: +anni);
}
// attributi
private String nome;
private String cognome;
private int anni;

// main
public static void main(String args[])
{
Persona eugenio = new Persona(Eugenio,Polito,26);
eugenio.stampaDati();
}
}

Commentiamo questa classe: abbiamo definito il costruttore Persona(String
nome, String cognome, int anni) che si occupa di ricevere in ingresso i tre
parametri nome, cognome ed anni e si occupa di inizializzare gli attributi nome,
cognome e anni con i valori dei parametri. Come si può notare è stata usata
3 CLASSI E OGGETTI

35

la parola chiave this: questo non è altro che un puntatore che fa riferimento all’oggetto attuale (o implicito). Quindi la sintassi this.nome significa “fai
riferimento all’attributo nome dell’oggetto corrente”. In questo caso è essenziale
perchè il nome dell’argomento ed il nome dell’attributo sono identici. Come vedremo, this è utile anche per richiamare altri costruttori.
Il metodo stampaDati() serve per stampare gli attributi dell’oggetto.
Il metodo main(...) contiene al suo interno due istruzioni:
Persona eugenio = new Persona(Eugenio,Polito,26); con tale istruzione stiamo creando l’oggetto eugenio: esso viene costruito con il costruttore che ha la firma (gli argomenti) String,String,int (l’unico che abbiamo definito). A run time la situazione, dopo questo assegnamento, sarà
la seguente:
eugenio

Persona
nome = Eugenio
cognome = Polito
anni = 26

stampaDati()

Figura 17: L’oggetto eugenio dopo la costruzione
eugenio.stampaDati(); richiama il metodo stampaDati() sull’oggetto eugenio; il “.” viene usato per accedere al metodo.

E se avessimo voluto costruire l’oggetto col costruttore di default? Avremmo
ottenuto un errore, perché nella classe non sarebbe stato trovato dal compilatore
alcun costruttore senza argomenti, quindi è bene fornirne uno:
public class Persona{
// Costruttore di default
public Persona()
{
this(,,0);
}
// Costruttore: inzializza gli attributi nome, cognome, anni
public Persona(String nome,String cognome,int anni)
{
this.nome = nome;
3 CLASSI E OGGETTI

36

this.cognome = cognome;
this.anni = anni;
}
// questo metodo stampa gli attributi
public void stampaDati()
{
System.out.println(Nome: +nome);
System.out.println(Cognome: +cognome);
System.out.println(Età: +anni);
}
// attributi
private String nome;
private String cognome;
private int anni;

// main
public static void main(String args[])
{
Persona eugenio = new Persona(Eugenio,Polito,26);
Persona anonimo = new Persona();
eugenio.stampaDati();
anonimo.stampaDati();
}
}

Il costruttore di default richiama il costruttore che ha come argomenti: String,
String, int, attraverso il riferimento all’argomento implicito this. In questo
esempio il costruttore è un metodo che usa l’overloading: in base al numero e
tipo di argomenti, il compilatore seleziona la versione corretta del metodo (cfr.
Sezione 2.7).
L’output del programma è il seguente:
Nome: Eugenio
Cognome: Polito
Età: 26
Nome:
Cognome:
Età: 0
3 CLASSI E OGGETTI

3.6
3.6.1

37

Realizzare le relazioni fra classi
Uso

Riprendiamo l’esempio della sezione 2.6.1: vediamo come si realizza la relazione
di uso. Supponiamo che la classe Persona usi la classe Computer per eseguire
il prodotto e la somma di 2 numeri, quindi definiamo la classe Computer e poi la
classe Persona:
public class Computer
{
// restituisce il prodotto di a * b
public int calcolaProdotto(int a, int b)
{
return a*b;
}
// restituisce la somma di a + b
public int calcolaSomma(int a, int b)
{
return a+b;
}
}

Tale classe ha il metodo calcolaProdotto(...) che si occupa di calcolare
il prodotto di due numeri, passati come argomento e di restituirne il risultato
(return a*b;). Il discorso è analogo per il metodo calcolaSomma(...) .
La classe Persona invece è:
public class Persona
{
// Costruttore di default
public Persona()
{
this(,,0);
}
// Costruttore: inizializza gli attributi nome, cognome, anni
public Persona(String nome,String cognome,int anni)
{
this.nome = nome;
this.cognome = cognome;
3 CLASSI E OGGETTI

38

this.anni = anni;
}
// questo metodo stampa gli attributi
public void stampaDati()
{
System.out.println(Nome: +nome);
System.out.println(Cognome: +cognome);
System.out.println(Età: +anni);
}
/* usa l’oggetto ’phobos’ istanza di Computer
per eseguire il prodotto e la somma degli
interi a e b passati come argomenti */
public void usaComputer(int a, int b)
{
Computer phobos = new Computer();
int res = phobos.calcolaProdotto(a,b);
System.out.println(Risultato del prodotto +a+
 * +b+: +res);
res = phobos.calcolaSomma(a,b);
System.out.println(Risultato della somma +a+
 + +b+: +res);
}
// attributi
private String nome;
private String cognome;
private int anni;

// main
public static void main(String args[])
{
Persona eugenio = new Persona(Eugenio,Polito,26);
eugenio.usaComputer(5,5);
}
}

Il metodo usaComputer(...) crea (quindi usa) un oggetto phobos, istanza della
classe Computer (Computer phobos = new Computer();) richiama il metodo
3 CLASSI E OGGETTI

39

calcolaProdotto(...) su phobos, passandogli gli argomenti a e b. Il risultato
del calcolo viene posto temporaneamente nella variabile locale res: all’uscita
dal metodo tale variabile verrà eliminata; ogni variabile locale deve essere inizializzata, altrimenti il compilatore riporta un errore!
L’ istruzione successiva System.out.println(...) stampa l’output a video.
res = phobos.calcolaSomma(a,b); richiama sull’oggetto phobos il metodo
calcolaSomma(...) ed il risultato viene posto in res (tale variabile è stata
già dichiarata quindi non si deve specificare di nuovo il tipo, inoltre il risultato del prodotto viene perso perché adesso res contiene il valore della somma!).
L’istruzione successiva stampa il risultato della somma.
Notiamo che così come la variabile locale res nasce, vive e muore in questo
metodo, anche l’oggetto phobos ha lo stesso ciclo di vita: quando il metodo termina, l’oggetto phobos viene distrutto automaticamente dal Garbage Collector
della JVM e la memoria da lui occupata viene liberata.
N.B. Gli oggetti costruiti nel main (così come le variabili) vivono per tutta la
durata del programma!
Nel main viene creato l’oggetto eugenio che invoca il metodo usaComputer(...)
per usare il computer.

3.6.2

Metodi static: un esempio

Riprendiamo la classe Computer: come possiamo notare, non ha degli attributi; in realtà, non ci importa istanziare tale classe perché, così come è stata definita, funge più da contenitore di metodi che da classe istanziabile. Pertanto i metodi
di tale classe li possiamo definire static:
public class Computer{
// restituisce il prodotto di a * b
public static int calcolaProdotto(int a, int b)
{
return a*b;
}
// restituisce la somma di a + b
public static int calcolaSomma(int a, int b)
{
return a+b;
}
}

Adesso dobbiamo rivedere il metodo usaComputer(...) della classe Persona:
3 CLASSI E OGGETTI

40

public class Persona
{
// Costruttore di default
public Persona()
{
this(,,0);
}
// Costruttore: inizializza gli attributi nome, cognome, anni
public Persona(String nome,String cognome,int anni)
{
this.nome = nome;
this.cognome = cognome;
this.anni = anni;
}
// questo metodo stampa gli attributi
public void stampaDati()
{
System.out.println(Nome: +nome);
System.out.println(Cognome: +cognome);
System.out.println(Età: +anni);

/* usa i metodi della classe Computer per eseguire
il prodotto e la somma fra gli interi a e b
passati come argomenti */
public void usaComputer(int a, int b)
{
int res = Computer.calcolaProdotto(a,b);
System.out.println(Risultato del prodotto +a+
 * +b+: +res);
res = Computer.calcolaSomma(a,b);
System.out.println(Risultato della somma +a+
 + +b+: +res);
}
// attributi
private String nome;
private String cognome;
private int anni;
3 CLASSI E OGGETTI

41

// main
public static void main(String args[])
{
Persona eugenio = new Persona(Eugenio,Polito,26);
eugenio.usaComputer(5,5);
}
}

Come si può notare nel metodo usaComputer(...) , questa volta non viene creato
un oggetto istanza della classe Computer, ma si usa quest’ultima per accedere
ai metodi calcolaSomma(...) e calcolaProdotto(...) , essendo dei metodi
static.
3.6.3

Aggregazione

Riprendiamo l’esempio discusso nella sezione 2.6.2: si diceva che la classe Persona
aggrega la classe Data, perché ogni persona ha una data di nascita.
Definiamo la classe Data:
public class Data {
/* Costruttore: inizializza gli attributi giorno,
mese, anno con i valori passati come
argomenti */
public Data(int giorno, int mese, int anno)
{
this.giorno = giorno;
this.mese = mese;
this.anno = anno;
}
// stampa la Data
public void stampaData()
{
System.out.println(giorno+/+mese+/+anno);
}
// attributi
private int giorno, mese, anno;
}

Tale classe ha gli attributi giorno, mese e anno che vengono inizializzati col
costruttore e possono essere stampati a video col metodo stampaData().
La classe Persona:
3 CLASSI E OGGETTI

42

public class Persona {
/* Costruttore: inizializza gli attributi nome, cognome,
e data di nascita */
public Persona(String nome,
String cognome,
int giorno,
int mese,
int anno)
{
this.nome = nome;
this.cognome = cognome;
dataDiNascita = new Data(giorno,mese,anno);
}
/* stampa gli attributi e richiama il metodo
stampaData() sull’oggetto dataDiNascita
per la stampa della data di nascita */
public void stampaDati()
{
System.out.println(Nome: +nome);
System.out.println(Cognome: +cognome);
System.out.print(Nato il: );
dataDiNascita.stampaData();
}
// attributi
private String nome;
private String cognome;
private Data dataDiNascita;
// main
public static void main(String args[])
{
Persona eugenio = new Persona(Eugenio,Polito,31,12,1976);
eugenio.stampaDati();
}
}

contiene gli attributi nome, cognome e dataDiNascita (istanza di Data): quindi
l’aggregazione si realizza in Java come attributo.
Notiamo che l’oggetto dataDiNascita viene creato nel costruttore con gli argo-
3 CLASSI E OGGETTI

43

menti passati come parametri: l’oggetto viene costruito solo quando si sa come
farlo.
Osserviamo che l’incapsulamento è garantito: gli attributi dell’oggetto dataDiNascita possono essere letti solo col metodo stampaData().
N.B. Come si è detto il main deve comparire una sola volta in una sola classe;
per chiarezza, quando si ha più di una classe, è consigliabile porlo in un’altra
classe. Quindi, in questo caso, lo togliamo dalla classe Persona e lo poniamo in
una nuova classe, diciamo Applicazione:
public class Persona {
/* Costruttore: inizializza gli attributi nome, cognome,
e data di nascita */
public Persona(String nome,
String cognome,
int giorno,
int mese,
int anno)
{
this.nome = nome;
this.cognome = cognome;
dataDiNascita = new Data(giorno,mese,anno);
}
/* stampa gli attributi e richiama il metodo
stampaData() sull’oggetto dataDiNascita
per la stampa della data di nascita */
public void stampaDati()
{
System.out.println(Nome: +nome);
System.out.println(Cognome: +cognome);
System.out.print(Nato il: );
dataDiNascita.stampaData();
}
// attributi
private String nome;
private String cognome;
private Data dataDiNascita;
}

e la classe Applicazione conterrà il main:
public class Applicazione {
3 CLASSI E OGGETTI

44

// main
public static void main(String args[])
{
Persona eugenio = new Persona(Eugenio,Polito,31,12,1976);
eugenio.stampaDati();
}
}

In seguito verrà utilizzato questo modo di procedere.
3.6.4

Ereditarietà

Vogliamo estendere la classe Persona in modo da gestire la classe Studente,
cioé vogliamo che Studente erediti da Persona: questo è logicamente vero dal
momento che ogni Studente è una Persona.
Definiamo la classe Persona:
import java.util.Random;
public class Persona
{
/* Costruttore: inizializza gli attributi nome, cognome,
e data di nascita */
public Persona(String nome,
String cognome,
int giorno,
int mese,
int anno)
{
this.nome = nome;
this.cognome = cognome;
dataDiNascita = new Data(giorno,mese,anno);
}
/* stampa gli attributi e richiama il metodo
stampaData() sull’oggetto dataDiNascita
per la stampa della data di nascita */
public void stampaDati()
{
System.out.println(Nome: +nome);
System.out.println(Cognome: +cognome);
System.out.print(Nato il: );
3 CLASSI E OGGETTI

45

dataDiNascita.stampaData();
}
// autoesplicativo
public void mangia()
{
System.out.println(nMangio con forchetta e coltellon);
}
// stampa casualmente ’n’ numeri (magari da giocare al lotto:)
public void conta(int n)
{
System.out.print(Conto: );
Random r = new Random();
for (int i = 0; i  n; i++)
System.out.print(r.nextInt(n)+t);
System.out.println();
}
// attributi
private String nome;
private String cognome;
private Data dataDiNascita;
}

Con l’istruzione import java.util.Random si sta importando la classe Random
che è contenuta nel package java.util (per l’uso dei package vedere la sezione
5) e serve per generare dei numeri pseudo-casuali.
Il costruttore ed il metodo stampaDati() sono stati già discussi.
Il metodo mangia() stampa a video un messaggio molto eloquente; il simbolo
“ n” serve per andare a capo.
Il metodo conta(...) riceve come argomento un intero n, e stampa n numeri
casuali attraverso un ciclo iterativo (per i cicli vedere 5).
Adesso vogliamo definire la classe Studente come sottoclasse di Persona, in
cui:

¡

il metodo mangia() viene ereditato;

¡

il metodo conta(...) viene riscritto;

¡

il metodo stampaDati() viene esteso.

¡

viene aggiunto il metodo faiFilaInSegreteria() ;
3 CLASSI E OGGETTI

46

Supponiamo inoltre che la nuova classe abbia l’attributo anniDiScuola.
La classe Studente è dunque:
public class Studente extends Persona
{
/* Costruttore: richiama il costruttore della classe base
(inializzando gli attributi nome, cognome, dataDiNascita)
ed inializza il membro anniDiScuola */
public Studente(String nome,
String cognome,
int giorno,
int mese,
int anno,
int anniDiScuola)
{
super(nome,cognome,giorno,mese,anno);
this.anniDiScuola = anniDiScuola;
}
/* Riscrive il metodo omonimo della classe base:
Stampa i numeri 1,2,...,n */
public void conta(int n)
{
System.out.print(Conto: );
for (int i = 1; i = n; i++)
System.out.print(i+t);
System.out.println();
}
/* Estende il metodo omonimo della classe base:
richiama il metodo della classe base omonimo
ed in più stampa l’attributo anniDiScuola */
public void stampaDati()
{
super.stampaDati();
System.out.println(Anni di scuola: +anniDiScuola);
}
/* stampa il messaggio ... */
public void faiFilaInSegreteria()
{
3 CLASSI E OGGETTI

47

System.out.println(...Aspetto il mio turno
in segreteria...);
}
// attributo
private int anniDiScuola;
}

In Java l’ereditarietà è resa possibile con la parola chiave extends.
Il costruttore richiama il costruttore della classe base, che ha la firma String,
String, int ,int, int, attraverso la parola chiave super; inoltre inizializza il
membro anniDiScuola. Notiamo che per costruire gli attributi nome, cognome,
dataDiNascita è necessario ricorrere al costruttore della classe base perché hanno tutti campo di visibilità (o scope) private.
Il metodo conta(...) è stato riscritto: ora stampa “correttamente” i numeri
1,2,. . . ,n.
Il metodo stampaDati(...) è stato esteso: richiama il metodo omonimo della
classe base ed in più stampa l’attributo anniDiScuola.
Infine è stato aggiunto il metodo faiFilaInSegreteria che stampa un messaggio di attesa. . .
Come si è detto gli attributi della classe base non sono accessibili alla classe
derivata perché hanno scope private. Tuttavia è possibile consentire solo alle
classi derivate di avere un accesso protetto agli attributi, attraverso il modificatore di accesso protected. La classe Persona può essere pertanto riscritta nel
seguente modo:
import java.util.Random;
public class Persona{
// Costruttore di default
public Persona()
{
this(,,0,0,0);
}
/* Costruttore: inizializza gli attributi nome, cognome,
anni e data di nascita */
public Persona(String nome,
String cognome,
int giorno,
int mese,
int anno)
3 CLASSI E OGGETTI

48

{
this.nome = nome;
this.cognome = cognome;
this.dataDiNascita = new Data(giorno,mese,anno);
}
/* stampa gli attributi e richiama il metodo
stampaData() sull’oggetto dataDiNascita
per la stampa della data di nascita */
public void stampaDati()
{
System.out.println(Nome: +nome);
System.out.println(Cognome: +cognome);
System.out.print(Nato il: );
dataDiNascita.stampaData();
}
// autoesplicativo
public void mangia()
{
System.out.println(nMangio con forchetta e coltellon);
}
// stampa casualmente ’n’ numeri (magari da giocare al lotto:)
public void conta(int n)
{
System.out.print(Conto: );
Random r = new Random();
for (int i = 0; i  n; i++)
System.out.print(r.nextInt(n)+t);
System.out.println();
}

// attributi
protected String nome;
protected String cognome;
protected Data dataDiNascita;
// main
public static void main(String args[])
3 CLASSI E OGGETTI

49

{
Persona eugenio = new Persona(Eugenio,Polito,31,12,1976);
eugenio.stampaDati();
eugenio.mangia();
eugenio.conta(5);
}
}

Adesso possiamo avere accesso diretto agli attributi della classe base dalla classe
derivata Studente:
public class Studente extends Persona
{
/* Costruttore: inizializza gli attributi
public Studente(String nome,
String cognome,
int giorno,
int mese,
int anno,
int anniDiScuola)
{
this.nome = nome;
this.cognome = cognome;
this.dataDiNascita = new Data(giorno,mese,anno);
this.anniDiScuola = anniDiScuola;
}
/* Riscrive il metodo omonmio della classe base:
Stampa i numeri 1,2,...,n */
public void conta(int n)
{
System.out.print(Conto: );
for (int i = 1; i = n; i++)
System.out.print(i+t);
System.out.println();
}
/* Estende il metodo omonimo della classe base:
richiama il metodo della classe base omonimo
ed in più stampa l’attributo anniDiScuola */
public void stampaDati()
3 CLASSI E OGGETTI

50

{
super.stampaDati();
System.out.println(Anni di scuola: +anniDiScuola);
}
/* stampa il messaggio ... */
public void faiFilaInSegreteria()
{
System.out.println(...Aspetto il mio turno in segreteria...);
}
// attributo
private int anniDiScuola;
}

Notare come adesso nel costruttore si possa accedere direttamente agli attributi
della classe base (this.nome, this.cognome, this.dataDiNascita ).
Un’altra cosa da osservare è che si è reso necessario inserire un costruttore di
default nella classe base perché il costruttore della classe derivata va a cercare
subito il costruttore di default della superclasse e poi inizializza gli attributi!
Vediamo una applicazione di esempio:
public class Applicazione
{
public static void main(String args[])
{
Persona bill = new Persona(Bill,Cancelli,13,13,1984);
bill.stampaDati();
bill.conta(5);
bill.mangia();
Studente tizio = new Studente(Pinco,Pallino,1,1,1970,15);
tizio.stampaDati();
tizio.conta(5);
tizio.mangia();
tizio.faiFilaInSegreteria();
}
}

Una possibile esecuzione è la seguente:
Nel main abbiamo creato l’oggetto bill (istruzione Persona bill = new
Persona(Bill,Cancelli,13,13,1984);), il quale invoca i metodi stampaDati();, conta(5); e mangia();.
3 CLASSI E OGGETTI

51

Figura 18: Esecuzione del programma Applicazione.java
È stato quindi creato l’oggetto tizio come istanza della classe Studente. Richiama il metodo stampaDati: come si può vedere in Figura 18, oltre al nome,
cognome e data di nascita viene stampato l’attributo anniDiScuola (ricordiamoci che tale metodo è stato esteso, proprio per permettere di stampare tale
attributo).
Viene poi richiamato il metodo conta(5): siccome tale metodo è stato riscritto,
stampa a video la sequenza corretta dei numeri 1,2,. . . ,n.
L’oggetto tizio invoca poi il metodo mangia(), che essendo ereditato stampa lo
stesso messaggio che è stato stampato precedentemente dallo stesso metodo invocato da bill.
Infine tizio invoca il metodo aggiunto nella classe Studente faiFilaInSegreteria() che stampa un messaggio.
Osserviamo che se avessimo invocato faiLaFilaInSegreteria() sull’oggetto
bill avremmo ottenuto un messaggio di errore, perché tale metodo non è definito
nella classe Persona.
3 CLASSI E OGGETTI

3.7

52

Classi astratte

Nella sezione 2.6.4 abbiamo parlato del concetto di classe astratta: vediamo
adesso come si realizza in Java.
Come si è detto, un Animale può essere considerato un contenitore di operazioni (dal momento che non si sa come definire tali operazioni in generale:
come mangia() o comunica() un Animale?) per tutte le classi derivate, come
Persona, Cane etc.: cioé la classe Animale è una classe astratta.
Supponiamo che tale classe abbia due metodi astratti mangia() e comunica()
ed uno concreto dorme(), dal momento che tutti gli animali dormono allo stesso
modo:
public abstract class Animale
{
// metodo astratto per mangiare
public abstract void mangia();
// metodo astratto per comunicare
public abstract void comunica();
// metodo concreto per dormire
public void dorme()
{
System.out.println(Dormo...);
}
}

Quindi una classe astratta è definita tale con la keyword abstract: questo tipo
di classe può contenere sia metodi astratti (definiti ovviamente abstract), sia
metodi concreti.
Ogni classe derivata da una classe astratta deve implementare i metodi astratti
della classe base!
Per esempio una eventuale classe Cane potrebbe avere questa forma:
public class Cane extends Animale
{
// costruttore
public Cane(String nome)
{
this.nome = nome;
}
3 CLASSI E OGGETTI

53

// implementa il metodo della classe base
public void comunica()
{
System.out.println(Sono +nome+ e faccio Bau Bau);
}
// implementa il metodo della classe base
public void mangia()
{
System.out.println(Mangio con la bocca e le zampe);
}
// attributo
private String nome;
}

Pertanto Cane estende la classe Animale: realizza i metodi astratti mangia() e
comunica ed eredita il metodo dorme().
Analogamente una classe Persona potrebbe essere così:
public class Persona extends Animale
{
// costruttore
public Persona(String nome, String cognome)
{
this.nome = nome;
this.cognome = cognome;
}
// implementa il metodo della classe base
public void comunica()
{
System.out.println(...Salve mondo, sono +nome+ +cognome);
}
// implementa il metodo della classe base
public void mangia()
{
System.out.println(Mangio con forchetta e coltello);
}
// estende il metodo della classe base
3 CLASSI E OGGETTI

54

public void dorme()
{
super.dorme();
System.out.println(ed in più russo!);
}
// aggiunge il metodo:
public void faiGuerra()
{
System.out.println(...Sono un animale intelligente
perché faccio le guerre...);
}
private String nome,cognome;
}

Questa classe implementa i metodi astratti della classe base, estende il metodo
dorme() ed aggiunge il metodo faiGuerra().
Definiamo un main nella classe Applicazione:
public class Applicazione
{
public static void main(String args[])
{
Cane bill = new Cane(bill);
bill.comunica();
bill.mangia();
bill.dorme();
System.out.println(n-----------------------);
Persona george = new Persona(George,Fluff);
george.comunica();
george.mangia();
george.dorme();
george.faiGuerra();
}
}
3 CLASSI E OGGETTI

55

L’esecuzione del programma è la seguente:

Figura 19: Esecuzione del programma Applicazione.java
Nel main viene creato l’oggetto bill che è una istanza della classe Cane: su
di esso viene invocato il metodo comunica() che stampa l’attributo nome (inizializzato nel costruttore) ed il verso “Bau Bau”. bill invoca quindi il metodo
mangia(), che stampa una stringa che “ci spiega” come il cane riesca a mangiare.
Infine viene richiamato su bill il metodo dorme().
Allo stesso modo viene creato l’oggetto george che è un’istanza di Persona: esso
invoca gli stessi metodi che invoca l’oggetto bill ed in più richiama il metodo
faiGuerra().
3 CLASSI E OGGETTI

3.8

56

Interfacce

Le interfacce sono un meccanismo proprio di Java che, come vedremo nella
sezione successiva, consentono di avere un supporto parziale ma sufficiente per
l’ereditarietà multipla.
Attraverso una interfaccia si definisce un comportamento comune a classi che fra
di loro non sono in relazione. Come si è detto, anche una classe astratta definisce
un contenitore di metodi per le classi derivate, quindi in questo caso si usa la
relazione di ereditarietà. Quando si parla invece di interfaccia si definiscono i
metodi che le classi dovranno implementare, pertanto in una interfaccia non possono esistere metodi concreti!
In UML una interfaccia si disegna così:

Figura 20: Diagramma UML per interface
Considerimo, per esempio, i file HTML ed i file bytecode di Java: ovviamente
essi non hanno nulla in comune, se non il fatto di supportare le stesse operazioni,
come apri(...), chiudi(...), etc.
Vediamo allora come costruire una interfaccia comune di operazioni da usare su
diversi file, per aprirli, determinarne il tipo e chiuderli. Definiamo allora un
interfaccia FileType:
public interface FileType
{
// apre il file
public void open();
// verifica se il tipo di file è OK
public boolean fileTypeOk();
// chiude il file
public void close();
}

Si sta dicendo che l’interfaccia FileType (definita con la keyword interface)
definisce i tre metodi open(), fileTypeOk(), close() ed ogni classe che vuole
implementare questa interfaccia deve implementare i metodi in essa definiti.
Supponiamo adesso di voler aprire e verificare un file bytecode di Java (per la
struttura dei bytecode Java cfr. [1]):
3 CLASSI E OGGETTI

57

import java.io.*;
public class FileClass implements FileType
{
/* Costruttore: inizializza il nome del file
che si vuole leggere */
public FileClass(String nome)
{
nomeDelFile = nome;
}
// apre il file fisico il cui nome è nomeDelFile
public void open()
{
try {
fileClass = new DataInputStream(new
FileInputStream(nomeDelFile));
} catch (FileNotFoundException fne) {
System.out.println(File +nomeDelFile+ non trovato.);
}
}
/* verifica se il file è un file bytecode di Java:
legge i primi 4 byte (cioé 32 bit = int) e controlla
se tale intero è il numero esadecimale 0xcafebabe,
cioé è l’header del file .class */
public boolean fileTypeOk()
{
int cafe = 0;
try {
cafe = fileClass.readInt();
} catch (IOException ioe) {}
if ((cafe ^ 0xCAFEBABE) == 0) return true;
else return false;
}
// chiude il file fisico
public void close()
{
try {
3 CLASSI E OGGETTI

58

fileClass.close();
} catch (IOException ioe) {
System.out.println(Non posso chiudere il file);
}
}
// file fisico
private DataInputStream fileClass;
// nome del file
private String nomeDelFile;
// main
public static void main(String args[])
{
if (args.length != 0) {
FileClass myFile = new FileClass(args[0]);
myFile.open();
if (myFile.fileTypeOk())
System.out.println(Il file +args[0]+
 è un bytecode Java);
else System.out.println(Il file +args[0]+
 non è un file .class!);
myFile.close();
}
else
System.out.println(uso: java FileClass nome del file);
}
}

Provare a compilarlo e ad eseguirlo (sintassi: java FileClass “nome” dove
“nome” è un nome di file .class, per esempio provare con: java FileClass
FileClass.class. . . )
La classe FileClass implementa le operazioni dell’interfaccia attraverso la keyword implements. Quindi, come si vede dal codice, la classe deve implementare
tutti i metodi dell’interfaccia.
Il costruttore riceve come argomento il nome del file che usa per inizializzare
l’attributo nomeDelFile.
Il metodo open() implementa il metodo omonimo dell’interfaccia FileType:
quindi tenta di aprire il file come stream di byte e se non trova il file solleva una
eccezione (per i file e le eccezioni cfr. la sezione 5).
3 CLASSI E OGGETTI

59

Il metodo fileTypeOk verifica se il file in aperto è una bytecode Java: se l’header
o intestazione comincia con il numero esadecimale 0xCAFEBABE (0x significa che
ciò che segue è un numero in base 16) allora il file è un bytecode Java, altrimenti
non lo è. Notare che per il test si è usato l’operatore fra bit XOR - in Java ˆ, che
restituisce 0 se i bit sono uguali, 1 altrimenti.
close chiude lo stream: se non lo trova (. . . magari è stato cancellato o spostato. . . ) solleva una eccezione.
Il main richiama in ordine i tre metodi di cui sopra.
Dal momento che un bytecode è un file di byte, si sono usate le classi di accesso
ai file del package java.io.
In 5 verrà discusso come accedere ai file.
In UML implements si disegna così:

Figura 21: Diagramma UML per implements
Dunque FileClass implementa l’interfaccia FileType.
Analogamente se volessimo verificare un file HTML, un ELF di Linux (file esegubile) etc., non dobbiamo far altro che scrivere delle classi che implementano le
operazioni dell’interfaccia FileType.
L’utilizzo delle interfacce è conveniente, almeno per due motivi:

¡

si separa l’interfaccia dall’implementazione;

¡

si ha una garanzia per non fare errori: si modifica solo l’implementazione e
non l’interfaccia.
3 CLASSI E OGGETTI

3.9

60

Ereditarietà multipla

In 2.6.5 si è parlato della ereditarietà multipla in teoria: realizziamo adesso
qualche esempio pratico.
Come si è detto in 2.6.5 l’ereditarietà multipla ha tre forme, di cui solo due sono
supportate in Java.
Vediamo come realizzare il matrimonio fra una classe concreta ed una astratta: poiché in Java ogni classe ha un solo padre, non è possibile ereditare da due
o più classi contemporaneamente; sembrerebbe a prima vista che il matrimonio
“non possa essere celebrato”. In realtà è possibile farlo, perché una interfaccia è
una classe astratta senza metodi concreti, quindi possiamo fare un matrimonio fra
una classe concreta ed una interfaccia. L’esempio della Pila della sezione 2.6.5
diventa pertanto:

Figura 22: Diagramma UML per il matrimonio fra classe concreta ed interfaccia
La nostra Pila dovrà avere una struttura FIFO (First In First Out) anche detta
FCFS (First Come First Served) cioé il primo elemento che entra deve essere il
primo elemento ad uscire (pensate ad una pila di piatti. . . ); quindi la struttura che
dobbiamo implementare è questa:
pop()
top

push(20)

10
20
5
16
4
5

Figura 23: Una Pila di numeri interi
L’operazione push(...) inserisce un elemento sulla cima della pila, mentre
3 CLASSI E OGGETTI

61

pop() preleva l’elemento in cima. L’attributo top punta alla cima della struttura.
Scriviamo allora l’interfaccia Stack: essa definisce le operazioni in astratto che
verranno implementate da Pila sull’array.
public interface Stack
{
// inserisce un oggetto ’element’ nella pila
public void push(Object element);
// preleva l’oggetto dalla cima della pila
public Object pop();
// verifica se la pila è piena
public boolean isFull();
// verifica se la pila è vuota
public boolean isEmpty();
}
Pila deve implementare Stack ed estendere array: poiché Java fornisce un
buon supporto per gli array attraverso la classe Vector del package java.util,
useremo tale classe come array:
import java.util.Vector;
public class Pila extends Vector implements Stack
{
// alloca una Pila di numElements elementi
public Pila(int numElements)
{
super(numElements);
maxElements = numElements;
top = 0;
}
// inserisce un element nella Pila
public void push(Object element)
{
if (!isFull()) {
super.addElement(element);
3 CLASSI E OGGETTI

62

top++;
}
else System.out.println(Pila Piena!);
}
// preleva l’elemento in cima alla Pila
public Object pop()
{
if (!isEmpty())
return super.remove(--top);
else {
System.out.println(Pila Vuota!);
return null;
}
}
// restituisce ’true’ se la Pila è vuota, ’false’ altrimenti
public boolean isFull()
{
return (top == maxElements);
}
/* riscrive il metodo omonimo della superclasse :
restituisce ’true’ se la Pila è vuota, ’false’
altrimenti */
public boolean isEmpty()
{
return (top == 0);
}
// puntatore alla cima della Pila
private int top;
// contatore del numero degli elementi della Pila
private int maxElements;
}

Poiché Pila è un Vector, supporta tutti i metodi di tale classe, inoltre poiché implementa l’interfaccia Stack deve implementare tutti i metodi di tale interfaccia.
Il costruttore richiama il costruttore della classe base Vector, setta l’attributo
numElements (cioé il numero massimo di elementi che la pila può memorizzare)
al valore passatogli come argomento e inizializza il top a 0 (quindi la pila è vuo-
3 CLASSI E OGGETTI

63

ta).
I metodi isFull() ed isEmpty() restituiscono true se, rispettivamente la pila è
piena (quindi top è uguale al massimo valore di elementi che la pila può supportare) e se la pila è vuota (top è uguale a 0).
Il metodo push(...) inserisce un elemento passatogli come argomento in cima
alla pila: se la pila è piena viene segnalato un errore. Notiamo che in realtà l’inserimento avviene tramite la chiamata al metodo addElement(...) della classe
base Vector che si preoccupa di inserire l’elemento nel vettore fisico.
pop è l’operazione complementare a push(...).
Una applicazione d’esempio potrebbe essere la seguente:
public class ProvaPila
{
public static void main(String args[])
{
if (args.length != 0)
{
Pila pila = new Pila((new Integer(args[0])).intValue());
// inserisci elem. finché la pila non è piena
int i = 1;
while (!pila.isFull())
{
pila.push(new Integer(i++));
}
// preleva elem. finché la pila non è vuota
while (!pila.isEmpty())
{
System.out.println(elemento prelevato: 
+pila.pop());
}
}
else
System.out.println(uso: java ProvaPila num_elem);
}
}
3 CLASSI E OGGETTI

64

Una possibile esecuzione è la seguente:

Figura 24: Esecuzione di ProvaPila
Osserviamo adesso un fatto importante: riprendiamo l’iterfaccia Stack; consideriamo i due metodi

¡

public void push(Object element);

¡

public Object pop();

Come si può vedere, push(...) prende come argomento un elemento il cui tipo è
Object, mentre la funzione pop() restituisce un Object. Cosa vuol dire questo?
Semplicemente che tali funzioni operano su oggetti di tipo Object: ma come si è
detto nella sottosezione 3.6.4, ogni classe deriva da Object, quindi questi metodi
funzionano su qualunque tipo!
Pertanto la nostra Pila, che implementa l’interfaccia Stack, sarà una pila che
potrà contenere elementi di qualsiasi tipo: allora potrà contenere numeri interi,
numeri reali, oggetti Persona, etc. etc.
Utilizzando il tipo cosmico Object (come parametro di funzione e/o tipo di ritorno di un metodo), si può simulare il polimorfismo parametrico (cfr. sezione
2.7); tuttavia, mentre in C++ (cfr. [2]) è possibile istanziare oggetti dello stesso
tipo (cioé con un template di C++ si possono avere solo collezioni omogenee),
in Java è possibile mixare tipi diversi (ogni classe è-un Object), quindi si possono ottenere collezioni eterogenee.
Vediamo cosa significa questo fatto nel nostro caso:
3 CLASSI E OGGETTI

65

public class ProvaPila
{
public static void main(String args[])
{
if (args.length != 0)
{
Pila pila = new Pila((new Integer(args[0])).intValue());
// inserisci elem. finché la pila non è piena
int i = 1;
while (!pila.isFull())
{
// inserisce un intero
pila.push(new Integer(i));
// inserisce un reale
pila.push(new Float(i*Math.PI));
// inserisce una stringa
pila.push(new String(Sono il numero: +i));
i++;
}
// preleva elem. finché la pila non è vuota
while (!pila.isEmpty())
{
System.out.println(elemento prelevato: 
+pila.pop());
}
}
else
System.out.println(uso: java ProvaPila num_elem);
}
}

Stavolta nella pila vengono inseriti rispettivamente: un numero intero, un numero
reale ed una stringa: abbiamo così ottenuto una pila “universale” di oggetti.
Occorre osservare che una classe può implementare più interfacce: ad esempio,
se vogliamo che la nostra Pila salvi il contenuto della pila su un file, possiamo
scrivere Pila così:
public class Pila extends Vector implements Stack, FileStack
{
...
}
3 CLASSI E OGGETTI

66

dove Vector e Stack sono le stesse viste sopre, mentre FileStack è una interfaccia che definisce i metodi per l’accesso ai file fisici. Pertanto una classe può
estendere una sola classe base ma può implementare più interfacce.
La seconda forma di ereditarietà multipla è il duplice sottotipo: cioé una classe
implementa due interfacce filtrate. Se abbiamo una interfaccia A ed una interfaccia B, è possibile fare questo:
public interface A {...};
public interface B extends A {...};

Una interfaccia può estendere un’altra interfaccia: di più può estendere un numero illimitato di interfacce, cioé si può avere una cosa del genere:
public interface X extends A1 ,A2 ,...,An {...}

dove ogni Ai i 1 2
n, sono interfacce!
Una classe concreta Y implementerà X:

!
%$$#! !

¦ !

public class Y implements X {...}

L’ereditarietà multipla sottoforma di composizione di oggetti non è supportata (cfr. sezione 2.6.5) per questioni di sicurezza del codice e per non rendere
complessa la JVM.
4 LE OPERAZIONI SUGLI OGGETTI

4
4.1

67

Le operazioni sugli oggetti
Copia e clonazione

Supponiamo di avere la seguente classe:
public class A
{
// costruttore di default: richiama il costruttore A(int num)
public A()
{
this(0);
}
/* costruttore: setta l’attributo num al valore passato come
argomento */
public A(int num)
{
this.num = num;
}
// assegna un nuovo valore a num
public void set(int num)
{
this.num = num;
}
// stampa num
public void print()
{
System.out.println(num);
}
// attributo
private int num;
// main
public static void main(String args[])
{
A primo = new A(1453);
primo.print();
A secondo = new A();
4 LE OPERAZIONI SUGLI OGGETTI

68

secondo.print();
secondo = primo;
secondo.set(16);
primo.print();
secondo.print();
}
}

Se eseguiamo tale programma, otteniamo il seguente output:
1453
0
16
16

Esaminiamo il main: viene creato l’oggetto primo che inizializza il membro
num a 1453; quando si richiama il metodo print() sull’oggetto primo, si ottiene
a video 1453.
Viene poi creato l’oggetto secondo che viene costruito col costruttore di default
(quindi il membro num è 0) e ed è richiamato su questo oggetto print() che stampa 0.
Si esegue poi l’assegnamento secondo = primo.
Si richiama poi il metodo set(...) sull’oggetto secondo, passando come argomento l’intero 16.
Quando si esegue l’istruzione secondo.print(); , viene stampato a video il numero 16.
Invocando print su primo, invece di ottenere il numero 1453, otteniamo 16.
Che cosa è successo?
Come si è detto nella sezione 3.4, la variabile oggetto è un riferimento all’oggetto,
cioé essa serve per accedere alle informazioni dell’oggetto alla quale si riferisce
e non per memorizzarle.
Allora con l’assegnamento secondo = primo;, non si sta facendo una copia di
valori, ma si sta facendo una copia di riferimenti: sia primo che secondo puntano allo stesso oggetto. In sostanza si è creato un secondo riferimento all’oggetto
primo.
4 LE OPERAZIONI SUGLI OGGETTI

69

Quando gli oggetti primo e secondo sono stati costruiti, nello heap si ha una
situazione del genere:
primo

A

num = 1453

secondo

A

num = 0

Figura 25: Gli oggetti primo e secondo dopo la creazione
e dopo l’assegnamento secondo = primo; si ha:
primo

A

num = 1453

secondo

A

num = 0

Figura 26: Gli oggetti primo e secondo dopo l’assegnamento secondo =
primo;
Pertanto ogni modifica sullo stato (attributi) di un oggetto si ripercuote sullo
stato dell’altro.
Vogliamo evitare questa situazione: cioé vogliamo che il riferimento, dopo la
copia, rimanga intatto. Per fare questo Java mette a dispozione il metodo clone()
nella classe Object: quindi basterà invocare tale metodo e verrà eseguita una
copia totale dell’oggetto (ricordiamo ancora una volta che ogni oggetto deriva
da Object implicitamente). È necessario implementare l’interfaccia Cloneable
(già definita in Java) per indicare che la clonazione dell’oggetto è possibile: il
metodo clone() di Java è protected, quindi per poterlo invocare è necessario
implementare Cloneable. Inoltre poiché il tipo di ritorno di questo metodo è
Object, è necessario un cast nel tipo corrente dell’oggetto:
4 LE OPERAZIONI SUGLI OGGETTI

70

public class A implements Cloneable
{
// costruttore di default: richiama il costruttore A(int num)
public A()
{
this(0);
}
/* costruttore: setta l’attributo num al valore passato come
argomento */
public A(int num)
{
this.num = num;
}
/* implementa il metodo dell’interfaccia Cloneable:
richiama il metodo clone() di Object */
public Object clone()
{
try {
return super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
// assegna un nuovo valore a num
public void set(int num)
{
this.num = num;
}
// stampa num
public void print()
{
System.out.println(num);
}
// attributo
private int num;
4 LE OPERAZIONI SUGLI OGGETTI

71

// main
public static void main(String args[])
{
A primo = new A(1453);
A secondo = new A();
secondo = (A)primo.clone();
secondo.set(16);
primo.print();
secondo.print();
}
}

Se eseguiamo tale codice, otteniamo il seguente output:
1453
16

La situazione nello heap dopo la clonazione, cioé dopo l’istruzione
secondo = (A)primo.clone();

è la seguente:
primo

A

num = 1453

secondo

A

num = 1453

Figura 27: Gli oggetti primo e secondo dopo la clonazione
I due oggetti hanno adesso “vite indipendenti”.
4 LE OPERAZIONI SUGLI OGGETTI

72

Dopo l’istruzione:
secondo.set(16);

La situazione è la seguente:
primo

A

num = 1453

secondo

A

num = 16

Figura 28: Gli oggetti primo e secondo dopo l’istruzione secondo.set(16);
Occorre notare che se una classe aggrega un’altra classe, è necessario definire
il metodo clone() anche nella classe aggregata, altrimenti quest’ultima non verrebbe clonata se fosse eseguito un clone() su un oggetto dell’altra classe!
4 LE OPERAZIONI SUGLI OGGETTI

4.2

73

Confronto

Un’altra operazione importante, che può ricorrere spesso in una applicazione vera, è il confronto di oggetti della stessa classe.
Java mette a disposizione il metodo equals(...) nella classe Object che confronta due oggetti e restituisce

¡

true se i due oggetti sono identici (cioé sono lo stesso riferimento),

¡

false altrimenti

Tale metodo non è molto utile se, per esempio, vogliamo confrontare due persone:
in questo caso è necessario confrontare tutti gli attributi e restituire true se sono
uguali, false altrimenti.
Risulta allora conveniente riscrivere tale metodo:
public class Persona
{
//Costruttore: inizializza gli attributi
public Persona(String nome,String cognome,int anni)
{
this.nome = nome;
this.cognome = cognome;
this.anni = anni;
}
/* riscrive il metodo della classe base Object:
testa se due persone sono uguali dal punto di
vista degli attributi */
public boolean equals(Object object)
{
if (object instanceof Persona)
return ((this.nome.equals(((Persona)object).nome)) 
(this.cognome.equals(((Persona)object).cognome)) 
(this.anni == ((Persona)object).anni));
else
return false;
}
// attributi
private String nome,cognome;
private int anni;
4 LE OPERAZIONI SUGLI OGGETTI

74

// main
public static void main(String args[])
{
Persona pippo = new Persona(Pippo,Caio,2);
Persona pluto = new Persona(Pippo,Caio,2);
if (pippo.equals(pluto))
System.out.println(Sono la stessa persona);
else System.out.println(Sono persone diverse);
}
}

Il metodo public boolean equals(Object object) riscrive il metodo omonimo di Object: come si può notare, tale metodo inizia con un confronto e precisamente:
if (object instanceof Persona)

questo controllo è molto importante. Infatti, quando nel main viene eseguita
l’istruzione pippo.equals(pluto) , al metodo equals(...) si sta passando
come argomento l’oggetto pluto che è una istanza di Persona: possiamo immaginare una situazione del genere:
pluto

Persona
nome = Pippo
cognome = Caio
anni = 2

Object

Figura 29: Un oggetto ha sempre un riferimento implicito ad Object
Poiché ogni classe deriva da Object, allora ogni oggetto è anche un riferimento ad un Object (a tempo di esecuzione del programma), quindi pluto è un
oggetto di tipo Persona, ma anche di tipo Object!
4 LE OPERAZIONI SUGLI OGGETTI

75

Se avessimo una classe Cane (con un unico attributo nome) ed un oggetto
bill, istanza di tale classe, avremmo allora:
bill

Cane
nome = bill

Object

Figura 30: L’oggetto bill istanza di Cane
Riguardiamo adesso il metodo equals(...) e notiamo che accetta come
argomento un Object: nessuno ci vieta allora di scrivere nel main l’istruzione:
pluto.equals(bill);

Ma ha senso confrontare un oggetto di tipo Persona con un oggetto di tipo Cane?
Ovviamente no!
Abbiamo quindi bisogno di controllare a run time il tipo dinamico (cioé il tipo
dell’oggetto durante l’esecuzione del programma che può essere una istanza di
una qualunque classe della gerarchia di ereditarietà) dell’oggetto passato come
argomento al metodo equals(...): se il tipo di tale oggetto è Persona, allora
possiamo confrontare gli attributi dei due oggetti che sono sicuramente istanze di
Persona, altrimenti il confronto degli attributi non ha senso (sono ovviamente due
oggetti di tipo diverso). Questo controllo viene fatto con la keyword instanceof,
la cui sintassi è:
if (nome_dell’_oggetto instanceof Nome_della_Classe) {...}

il risultato sarà true se il tipo dinamico di nome_dell’_oggetto è esattamente
Nome_della_Classe , false altrimenti.
Nel nostro caso (sia pippo che pluto sono oggetti Persona), tale controllo (if
(object instanceof Persona)) sarà true, perché il tipo dinamico di pluto è
Persona.
Osserviamo però che prima e dopo questo controllo stiamo usando pippo come
istanza di Object e non di Persona! Pertanto se tentiamo di accedere ad un qualsiasi attributo di Persona, otteniamo un errore a tempo di compilazione. Quindi è
necessario specificare che se il controllo di instanceof andrà a buon fine durante
l’esecuzione del programma, il tipo di pluto dovrà essere ripristinato a Persona:
questa operazione è detta downcasting e viene eseguita con la sintassi:
4 LE OPERAZIONI SUGLI OGGETTI

76

(Nome_della_classe_derivata)nome_dell’_oggetto

Questo tipo di conversione è pericolosa: usarla solo quando necessario e soprattutto, prima di eseguire il cast verificare il tipo dinamico dell’oggetto con
instanceof. Come vedremo, questa conversione può essere spesso evitata se si
ricorre al polimorfismo!

4.3

Binding dinamico

Consideriamo due classi, per esempio Base da cui deriva Derivata:
public class Base
{
// richiama stampa()
public void f()
{
stampa();
}
// stampa un messaggio
public void stampa()
{
System.out.println(Sono la classe base);
}
}
public class Derivata extends Base
{
// riscrive il metodo della classe base
public void stampa()
{
System.out.println(Sono la classe derivata);
}
// stampa la stringa ciao
public void g()
{
System.out.println(Ciao dalla classe derivata);
}
}

Supponiamo inoltre di avere il seguente main:
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
(E book ita) java   introduzione alla programmazione orientata ad oggetti in java

Mais conteúdo relacionado

Destaque

Docslide.us bi apps-etl-4-informatica-powercenter-express
Docslide.us bi apps-etl-4-informatica-powercenter-expressDocslide.us bi apps-etl-4-informatica-powercenter-express
Docslide.us bi apps-etl-4-informatica-powercenter-express
Raffaella D'angelo
 
Lezione 11 - Vba E Excel
Lezione 11 - Vba E ExcelLezione 11 - Vba E Excel
Lezione 11 - Vba E Excel
Rice Cipriani
 
Programmaoggetti[1]
Programmaoggetti[1]Programmaoggetti[1]
Programmaoggetti[1]
Anna_1969
 
Digital Marketing
Digital MarketingDigital Marketing
Digital Marketing
Atul Sharma
 
Linguaggio Java
Linguaggio JavaLinguaggio Java
Linguaggio Java
GSamLo
 

Destaque (20)

Docslide.us bi apps-etl-4-informatica-powercenter-express
Docslide.us bi apps-etl-4-informatica-powercenter-expressDocslide.us bi apps-etl-4-informatica-powercenter-express
Docslide.us bi apps-etl-4-informatica-powercenter-express
 
Docslide.us dynamic lookup-cache
Docslide.us dynamic lookup-cacheDocslide.us dynamic lookup-cache
Docslide.us dynamic lookup-cache
 
Programmazione ad oggetti
Programmazione ad oggettiProgrammazione ad oggetti
Programmazione ad oggetti
 
La piattaforma web di CNA: Istanze Drupal replicabili integrate con Alfresco ...
La piattaforma web di CNA: Istanze Drupal replicabili integrate con Alfresco ...La piattaforma web di CNA: Istanze Drupal replicabili integrate con Alfresco ...
La piattaforma web di CNA: Istanze Drupal replicabili integrate con Alfresco ...
 
Lezione 11 - Vba E Excel
Lezione 11 - Vba E ExcelLezione 11 - Vba E Excel
Lezione 11 - Vba E Excel
 
Esercizio di excel
Esercizio di excelEsercizio di excel
Esercizio di excel
 
Activiti
ActivitiActiviti
Activiti
 
Office & VBA - Giorno 1
Office & VBA - Giorno 1Office & VBA - Giorno 1
Office & VBA - Giorno 1
 
Programmaoggetti[1]
Programmaoggetti[1]Programmaoggetti[1]
Programmaoggetti[1]
 
Digital Marketing
Digital MarketingDigital Marketing
Digital Marketing
 
Linguaggio Java
Linguaggio JavaLinguaggio Java
Linguaggio Java
 
Lezione android esercizi
Lezione android esercizi Lezione android esercizi
Lezione android esercizi
 
Autocad lezione 1
Autocad lezione 1Autocad lezione 1
Autocad lezione 1
 
Sviluppo Di Un Sito Web
Sviluppo Di Un Sito WebSviluppo Di Un Sito Web
Sviluppo Di Un Sito Web
 
Terza lezioneandroid
Terza lezioneandroidTerza lezioneandroid
Terza lezioneandroid
 
Corso Android
Corso AndroidCorso Android
Corso Android
 
Html5 appunti.0
Html5   appunti.0Html5   appunti.0
Html5 appunti.0
 
Tutorial su JMS (Java Message Service)
Tutorial su JMS (Java Message Service)Tutorial su JMS (Java Message Service)
Tutorial su JMS (Java Message Service)
 
Html e CSS ipertesti e siti web 4.5
Html e CSS   ipertesti e siti web 4.5Html e CSS   ipertesti e siti web 4.5
Html e CSS ipertesti e siti web 4.5
 
Becoming a Better Programmer
Becoming a Better ProgrammerBecoming a Better Programmer
Becoming a Better Programmer
 

Semelhante a (E book ita) java introduzione alla programmazione orientata ad oggetti in java

Il tutorial di Python
Il tutorial di PythonIl tutorial di Python
Il tutorial di Python
AmmLibera AL
 
Il Modello Pragmatico Elementare per lo sviluppo di Sistemi Adattivi - Tesi
Il Modello Pragmatico Elementare per lo sviluppo di Sistemi Adattivi - TesiIl Modello Pragmatico Elementare per lo sviluppo di Sistemi Adattivi - Tesi
Il Modello Pragmatico Elementare per lo sviluppo di Sistemi Adattivi - Tesi
Francesco Magagnino
 
Imparare c n.104
Imparare c  n.104Imparare c  n.104
Imparare c n.104
Pi Libri
 
Orchestrazione delle risorse umane nel BPM
Orchestrazione delle risorse umane nel BPMOrchestrazione delle risorse umane nel BPM
Orchestrazione delle risorse umane nel BPM
Michele Filannino
 
Progettazione e sviluppo di un'applicazione web per la gestione di dati di at...
Progettazione e sviluppo di un'applicazione web per la gestione di dati di at...Progettazione e sviluppo di un'applicazione web per la gestione di dati di at...
Progettazione e sviluppo di un'applicazione web per la gestione di dati di at...
daniel_zotti
 
24546913 progettazione-e-implementazione-del-sistema-di-controllo-per-un-pend...
24546913 progettazione-e-implementazione-del-sistema-di-controllo-per-un-pend...24546913 progettazione-e-implementazione-del-sistema-di-controllo-per-un-pend...
24546913 progettazione-e-implementazione-del-sistema-di-controllo-per-un-pend...
maaske
 
Tecniche di Test-driven development in ambito sicurezza informatica e rilevaz...
Tecniche di Test-driven development in ambito sicurezza informatica e rilevaz...Tecniche di Test-driven development in ambito sicurezza informatica e rilevaz...
Tecniche di Test-driven development in ambito sicurezza informatica e rilevaz...
fcecutti
 
Implementazione in Java di plugin Maven per algoritmi di addestramento per re...
Implementazione in Java di plugin Maven per algoritmi di addestramento per re...Implementazione in Java di plugin Maven per algoritmi di addestramento per re...
Implementazione in Java di plugin Maven per algoritmi di addestramento per re...
Francesco Komauli
 
Validation and analysis of mobility models
Validation and analysis of mobility modelsValidation and analysis of mobility models
Validation and analysis of mobility models
Umberto Griffo
 
Progetto e sviluppo di un'applicazionemobile multipiattaforma per il supporto...
Progetto e sviluppo di un'applicazionemobile multipiattaforma per il supporto...Progetto e sviluppo di un'applicazionemobile multipiattaforma per il supporto...
Progetto e sviluppo di un'applicazionemobile multipiattaforma per il supporto...
maik_o
 

Semelhante a (E book ita) java introduzione alla programmazione orientata ad oggetti in java (20)

Il tutorial di Python
Il tutorial di PythonIl tutorial di Python
Il tutorial di Python
 
Guida C# By Megahao
Guida C# By MegahaoGuida C# By Megahao
Guida C# By Megahao
 
Openfisca Managing Tool: a tool to manage fiscal sistems
Openfisca Managing Tool: a tool to manage fiscal sistemsOpenfisca Managing Tool: a tool to manage fiscal sistems
Openfisca Managing Tool: a tool to manage fiscal sistems
 
Il Modello Pragmatico Elementare per lo sviluppo di Sistemi Adattivi - Tesi
Il Modello Pragmatico Elementare per lo sviluppo di Sistemi Adattivi - TesiIl Modello Pragmatico Elementare per lo sviluppo di Sistemi Adattivi - Tesi
Il Modello Pragmatico Elementare per lo sviluppo di Sistemi Adattivi - Tesi
 
Imparare c n.104
Imparare c  n.104Imparare c  n.104
Imparare c n.104
 
Piano Nazionale Scuola Digitale (risorse integrative)
Piano Nazionale Scuola Digitale (risorse integrative)Piano Nazionale Scuola Digitale (risorse integrative)
Piano Nazionale Scuola Digitale (risorse integrative)
 
Tesi peiretti
Tesi peirettiTesi peiretti
Tesi peiretti
 
Applicazioni intelligenzaartificiale
Applicazioni intelligenzaartificialeApplicazioni intelligenzaartificiale
Applicazioni intelligenzaartificiale
 
Orchestrazione delle risorse umane nel BPM
Orchestrazione delle risorse umane nel BPMOrchestrazione delle risorse umane nel BPM
Orchestrazione delle risorse umane nel BPM
 
Relazione Analisi di Usabilità Apple itunes
Relazione Analisi di Usabilità Apple itunes Relazione Analisi di Usabilità Apple itunes
Relazione Analisi di Usabilità Apple itunes
 
Pattern Recognition Lecture Notes
Pattern Recognition Lecture NotesPattern Recognition Lecture Notes
Pattern Recognition Lecture Notes
 
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da u...
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da u...Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da u...
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da u...
 
LEARNING OBJECT MODELLO DI RIFERIMENTO SCORM E AUTHORING APPLICATIONS
LEARNING OBJECT MODELLO DI RIFERIMENTO SCORM E AUTHORING APPLICATIONSLEARNING OBJECT MODELLO DI RIFERIMENTO SCORM E AUTHORING APPLICATIONS
LEARNING OBJECT MODELLO DI RIFERIMENTO SCORM E AUTHORING APPLICATIONS
 
Progettazione e sviluppo di un'applicazione web per la gestione di dati di at...
Progettazione e sviluppo di un'applicazione web per la gestione di dati di at...Progettazione e sviluppo di un'applicazione web per la gestione di dati di at...
Progettazione e sviluppo di un'applicazione web per la gestione di dati di at...
 
24546913 progettazione-e-implementazione-del-sistema-di-controllo-per-un-pend...
24546913 progettazione-e-implementazione-del-sistema-di-controllo-per-un-pend...24546913 progettazione-e-implementazione-del-sistema-di-controllo-per-un-pend...
24546913 progettazione-e-implementazione-del-sistema-di-controllo-per-un-pend...
 
Tecniche di Test-driven development in ambito sicurezza informatica e rilevaz...
Tecniche di Test-driven development in ambito sicurezza informatica e rilevaz...Tecniche di Test-driven development in ambito sicurezza informatica e rilevaz...
Tecniche di Test-driven development in ambito sicurezza informatica e rilevaz...
 
Implementazione in Java di plugin Maven per algoritmi di addestramento per re...
Implementazione in Java di plugin Maven per algoritmi di addestramento per re...Implementazione in Java di plugin Maven per algoritmi di addestramento per re...
Implementazione in Java di plugin Maven per algoritmi di addestramento per re...
 
Validation and analysis of mobility models
Validation and analysis of mobility modelsValidation and analysis of mobility models
Validation and analysis of mobility models
 
Progetto e sviluppo di un'applicazionemobile multipiattaforma per il supporto...
Progetto e sviluppo di un'applicazionemobile multipiattaforma per il supporto...Progetto e sviluppo di un'applicazionemobile multipiattaforma per il supporto...
Progetto e sviluppo di un'applicazionemobile multipiattaforma per il supporto...
 
Segmentazione automatica di immagini di mosaici tramite tecniche di calcolo e...
Segmentazione automatica di immagini di mosaici tramite tecniche di calcolo e...Segmentazione automatica di immagini di mosaici tramite tecniche di calcolo e...
Segmentazione automatica di immagini di mosaici tramite tecniche di calcolo e...
 

(E book ita) java introduzione alla programmazione orientata ad oggetti in java

  • 1. Introduzione alla Programmazione Orientata agli Oggetti in Java Versione 1.1 Eugenio Polìto Stefania Iaffaldano Ultimo aggiornamento: 1 Novembre 2003
  • 2.   Copyright c 2003 Eugenio Polìto, Stefania Iaffaldano. All rights reserved. This document is free; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This document is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this document; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. Sosteniamo la Cultura Libera: sosteniamo il Free Software Per la segnalazione di errori e suggerimenti, potete contattarci ai seguenti indirizzi: Web: http://www.eugeniopolito.it E-Mail: eugeniopolito@eugeniopolito.it A Typeset with L TEX.
  • 3. Ringraziamenti Un grazie speciale ad OldDrake, Andy83 e Francesco Costa per il prezioso aiuto che hanno offerto!
  • 4. Indice 1 Introduzione 8 I Teoria della OOP 2 II 3 Le idee fondamentali 2.1 Una breve storia della programmazione . . . . . . . . . . 2.2 I princìpi della OOP . . . . . . . . . . . . . . . . . . . . . 2.3 ADT: creare nuovi tipi . . . . . . . . . . . . . . . . . . . 2.4 La classe: implementare gli ADT tramite l’incapsulamento 2.5 L’oggetto . . . . . . . . . . . . . . . . . . . . . . . . . . 2.6 Le relazioni fra le classi . . . . . . . . . . . . . . . . . . . 2.6.1 Uso . . . . . . . . . . . . . . . . . . . . . . . . . 2.6.2 Aggregazione . . . . . . . . . . . . . . . . . . . . 2.6.3 Ereditarietà . . . . . . . . . . . . . . . . . . . . . 2.6.4 Classi astratte . . . . . . . . . . . . . . . . . . . . 2.6.5 Ereditarietà multipla . . . . . . . . . . . . . . . . 2.7 Binding dinamico e Polimorfismo . . . . . . . . . . . . . 9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La OOP in Java Classi e oggetti 3.1 Definire una classe . . . . . . . . . . . . . . . . . . . . . . . 3.2 Garantire l’incapsulamento: metodi pubblici e attributi privati 3.3 Metodi ed attributi statici . . . . . . . . . . . . . . . . . . . . 3.4 Costruire un oggetto . . . . . . . . . . . . . . . . . . . . . . 3.5 La classe Persona e l’oggetto eugenio . . . . . . . . . . . . 3.6 Realizzare le relazioni fra classi . . . . . . . . . . . . . . . . 3.6.1 Uso . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6.2 Metodi static: un esempio . . . . . . . . . . . . . . 3.6.3 Aggregazione . . . . . . . . . . . . . . . . . . . . . . 3.6.4 Ereditarietà . . . . . . . . . . . . . . . . . . . . . . . 3.7 Classi astratte . . . . . . . . . . . . . . . . . . . . . . . . . . 3.8 Interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.9 Ereditarietà multipla . . . . . . . . . . . . . . . . . . . . . . 9 9 12 13 15 16 17 17 18 18 22 23 27 29 . . . . . . . . . . . . . . . . . . . . . . . . . . 29 29 29 31 32 34 37 37 39 41 44 52 56 60
  • 5. 4 Le operazioni sugli oggetti 4.1 Copia e clonazione . . 4.2 Confronto . . . . . . . 4.3 Binding dinamico . . . 4.4 Serializzazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . III APPENDICI 5 6 Una panoramica sul linguaggio 5.1 Tipi primitivi . . . . . . . . . . 5.2 Variabili . . . . . . . . . . . . . 5.3 Operatori . . . . . . . . . . . . 5.3.1 Operatori Aritmetici . . 5.3.2 Operatori relazionali . . 5.3.3 Operatori booleani . . . 5.3.4 Operatori su bit . . . . . 5.4 Blocchi . . . . . . . . . . . . . 5.5 Controllo del flusso . . . . . . . 5.6 Operazioni (Metodi) . . . . . . 5.6.1 Il main . . . . . . . . . 5.6.2 I package . . . . . . . . 5.6.3 Gli stream . . . . . . . . 5.6.4 L’I/O a linea di comando 5.6.5 Le eccezioni . . . . . . 5.6.6 Installazione del JDK . . La licenza GNU GPL 67 67 73 76 79 83 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 83 84 84 84 85 85 85 86 86 87 89 89 90 91 91 93 94
  • 6. Elenco delle figure 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 Una classe in UML. . . . . . . . . . . . . . . . . . . . . . . . . . La classe Persona in UML. . . . . . . . . . . . . . . . . . . . . La relazione d’uso in UML. . . . . . . . . . . . . . . . . . . . . . La relazione di aggregazione in UML. . . . . . . . . . . . . . . . La relazione di ereditarietà in UML. . . . . . . . . . . . . . . . . La classe delle persone come sottoclasse della classe degli animali. Una piccola gerarchia di Animali. . . . . . . . . . . . . . . . . . Una piccola gerarchia di Animali in UML. . . . . . . . . . . . . . Matrimonio fra classe concreta e astratta . . . . . . . . . . . . . Composizione: la classe Studente Lavoratore . . . . . . . . . Studente Lavoratore come aggregazione e specializzazione . . Studente Lavoratore come aggregazione . . . . . . . . . . . . L’ex Studente ed ex Lavoratore ora Disoccupato . . . . . . . La classe Studente come sottoclasse di Persona . . . . . . . . . L’oggetto primo appena creato . . . . . . . . . . . . . . . . . . . L’oggetto primo non ancora creato . . . . . . . . . . . . . . . . . L’oggetto eugenio dopo la costruzione . . . . . . . . . . . . . . Esecuzione del programma Applicazione.java . . . . . . . . . Esecuzione del programma Applicazione.java . . . . . . . . . Diagramma UML per interface . . . . . . . . . . . . . . . . . Diagramma UML per implements . . . . . . . . . . . . . . . . . Diagramma UML per il matrimonio fra classe concreta ed interfaccia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Una Pila di numeri interi . . . . . . . . . . . . . . . . . . . . . . Esecuzione di ProvaPila . . . . . . . . . . . . . . . . . . . . . . Gli oggetti primo e secondo dopo la creazione . . . . . . . . . . Gli oggetti primo e secondo dopo l’assegnamento secondo = primo; . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Gli oggetti primo e secondo dopo la clonazione . . . . . . . . . . Gli oggetti primo e secondo dopo l’istruzione secondo.set(16); Un oggetto ha sempre un riferimento implicito ad Object . . . . . L’oggetto bill istanza di Cane . . . . . . . . . . . . . . . . . . . base = derivata; . . . . . . . . . . . . . . . . . . . . . . . . . Passaggio dei parametri ad una funzione . . . . . . . . . . . . . . 15 16 17 18 19 20 21 21 23 23 24 25 25 27 33 33 35 51 55 56 59 60 60 64 69 69 71 72 74 75 78 88
  • 7. Elenco delle tabelle 1 2 Pro e contro dell’uso di ereditarietà e aggregazione . . . . . . . . Tipi primitivi di Java . . . . . . . . . . . . . . . . . . . . . . . . 20 83
  • 8. 1 INTRODUZIONE 1 8 Introduzione Questa trattazione sulla Programmazione Orientata agli Oggetti o Object Oriented Programming (OOP in seguito) in Java nasce dall’osservazione che, molto spesso, i manuali sono degli ottimi reference (cfr. [3]) del linguaggio ma non approfondiscono i pochi ma essenziali concetti della OOP; d’altra parte nei corsi universitari si presta fin troppa attenzione alla teoria senza approfondire gli aspetti implementativi. Quindi si cercherà di trattare sia la teoria che la pratica della OOP perché entrambi gli aspetti sono fondamentali per poter scrivere un buon codice orientato agli oggetti. Purtroppo Java viene considerato (in modo errato!) un linguaggio per scrivere soltanto applet o addirittura viene confuso con il linguaggio di script JavaScript: con Java si possono scrivere delle applicazioni standalone che non hanno molto da invidiare (a livello di prestazioni) ai programmi scritti con altri linguaggi OOP più efficienti come C++. Infatti, attualmente, la Java Virtual Machine (cioé lo strato software che si occupa di trasformare il codice intermedio o bytecode in chiamate alle funzioni del Sistema Operativo - syscall) consente di avere delle ottime prestazioni. In [1] potete trovare la versione 1.4 del JDK. È importante sottolineare che questo non è un manuale sul linguaggio, ma è una trattazione sull’impostazione Object Oriented del linguaggio: comunque, se non si conosce la sintassi di Java, è consigliabile dare uno sguardo alla sezione 5 dove è disponibile un veloce riassunto sulla struttura del linguaggio.
  • 9. 9 Parte I Teoria della OOP In questa parte verranno introdotte e discusse le idee fondamentali della OOP. Se si conoscono già i princìpi ed i meccanismi della OOP si può passare alla parte successiva, dove si discute come tali concetti sono realizzati in Java. 2 2.1 Le idee fondamentali Una breve storia della programmazione La programmazione dei computer esiste ormai da circa 60 anni. Ovviamente in un tempo così lungo, ha subìto notevoli cambiamenti: agli albori l’arte di programmare consisteva nella programmazione di ogni singolo bit di informazione tramite degli interruttori che venivano accesi e spenti. Quindi il programma era in realtà una sequenza di accensioni e spegnimenti di interruttori che producevano il risultato che si voleva, anzi che si sperava di ottenere: pensate cosa poteva comportare lo scambio di una accensione con uno spegnimento o viceversa! Solo intorno al 1960 venne scritto il primo Assembler: questo linguaggio era (ed è) troppo legato alla struttura del microprocessore e soprattutto era (ed è) difficile scrivere un programma con un linguaggio troppo vicino alla macchina e poco amichevole per l’uomo. Tra il 1969 ed il 1971, Dennis Ritchie scrisse il primo linguaggio di programmazione ad alto livello: il C. Seppure è ad un gradino superiore all’assembler e soprattutto il codice sorgente di un programma può essere ricompilato su qualsiasi piattaforma (con qualche modifica!), questo linguaggio risulta ancora troppo vicino alla macchina (basti pensare che i Sistemi Operativi sono scritti in C e Assembler). Nel 1983 Bijarne Stroustrup (programmatore presso la compagnia telefonica americana Bell Laboratories) ebbe la necessità di dover simulare un sistema telefonico: i linguaggi allora disponibili si prestavano poco a programmare un sistema così complesso, così ebbe l’idea di partire dal C per scrivere un nuovo linguaggio che supportasse le classi. Nacque così il C con oggetti. L’idea di classe era già nota ed utilizzata in altri linguaggi come Smalltalk. Tuttavia il C con oggetti (in seguito rinominato C++) è una estensione del C e quindi ha dei pregi e dei difetti: pro fra i pregi possiamo sicuramente annoverare l’efficienza (propria di C) e la portabilità del codice sorgente (con qualche modifica) da una archittettura ad un’altra: i compilatori C++ esistono per ogni tipo di piattaforma
  • 10. 2 LE IDEE FONDAMENTALI 10 hardware, pertanto è sufficiente qualche modifica al codice sorgente ed una ricompilazione; contro la gestione della memoria, per esempio, è completamente a carico del programmatore e, come si sa, la gestione dei puntatori è una fabbrica di errori. È inoltre possibile mixare sia codice C che codice C++, ottenendo così un codice non molto pulito dal punto di vista della OOP: supponiamo di volere confrontare due variabili a e b. Il codice corretto è il seguente: if (a == b) {...} Ma basta omettere un ’=’ per ottenere un assegnamento: if (a = b) {...} se l’assegnamento va a buon fine, viene eseguito il blocco di istruzioni. Un errore del genere può essere frequente: se non usato bene, il C++ rischia di essere un “boomerang” per il programmatore. Nei primi mesi del 1996 venne presentata la prima versione del linguaggio Java che introduceva non poche novità: macchina virtuale anche se non era un concetto nuovo nell’informatica (già IBM aveva fatto delle sperimentazioni sulle Virtual Machine in un Sistema Operativo proprietario), l’idea di sfruttare una Macchina Virtuale o Java Virtual Machine - JVM che si interpone fra il linguaggio intermedio bytecode ed il linguaggio macchina della architettura sottostante era una novità assoluta. Tale idea è stata ultimamente ripresa da una nota Software House con un progetto denominato .NET . . . portabilità grazie al concetto di Virtual Machine è sufficiente compilare una sola volta il programma per poi eseguire il programma .class in formato di bytecode su qualsiasi altra piattaforma; per C++ vale solo una portabilità di codice sorgente e non di programma eseguibile; esecuzione di programmi nei browser (applet) si può scrivere una unica applicazione che venga eseguita sia in modo nativo che all’interno di un browser web; gestione automatica della memoria questo è sicuramente uno degli aspetti più importanti di Java. Ci preoccupiamo solo della costruzione di un oggetto, perchè la distruzione è completamente gestita dalla JVM tramite il Garbage
  • 11. 2 LE IDEE FONDAMENTALI 11 Collector: un oggetto non più utilizzato viene automaticamente distrutto. Inoltre in Java esiste solo il concetto di riferimento ad un oggetto che comporta una gestione semplice della notazione (si pensi alla notazione . o -> di C++ a seconda che un metodo sia richiamato su una variabile oggetto / reference o su un puntatore). L’assenza della gestione diretta dei puntatori consente di produrre un codice sicuro. Per un confronto fra Java e C++ cfr. [4].
  • 12. 2 LE IDEE FONDAMENTALI 2.2 12 I princìpi della OOP La OOP è una evoluzione naturale dei linguaggi di programmazione: essa nasce con lo scopo preciso di simulare e modellare la realtà. I princìpi su cui si basa la OOP sono semplici ma molto potenti: ¡ Definire nuovi tipi di dati. ¡ Incapsulare i valori e le operazioni. ¡ Riusare il codice esistente (ereditarietà). ¡ Fornire il polimorfismo. Come vedremo, nella OOP non si fa differenza fra valori ed operazioni: semplicemente si parla di Tipo di dato che ingloba le due entità in un’unica struttura. Quindi è necessario definire un nuovo Tipo di dato. È altrettanto necessario accedere ad un valore di un tipo di dato: come vedremo questo è fattibile tramite il meccanismo di incapsulamento. Un altro cardine della OOP è il riuso del codice: cioé utilizzare del codice esistente per poterlo specializzare. Il polimorfismo si rende necessario, come vedremo, in una gerarchia di ereditarietà.
  • 13. 2 LE IDEE FONDAMENTALI 2.3 13 ADT: creare nuovi tipi Un Tipo di Dato Astratto o Abstract Data Type - ADT è, per definizione, un nuovo tipo di dato che estende i tipi nativi forniti dal linguaggio di programmazione. Un ADT è caratterizzato da un insieme di: ¡ dati; ¡ operazioni che agiscono sui dati, leggengoli/scrivendoli; Fin qui niente di nuovo: anche i linguaggi procedurali, come per esempio C, consentono di definire un ADT. Ma, mentre per tali linguaggi chiunque può avere accesso ai dati e modificarli, i linguaggi Object Oriented ne garantiscono la loro riservatezza. Supponiamo infatti di voler definire in C (non preoccuparsi della sintassi) un ADT Persona cioé una struttura dati che mantenga le informazioni (dati) di una Persona, come, per esempio, il nome, il cognome e la data di nascita e che consenta di creare e stampare le informazioni di una persona (operazioni): /* Struttura dati per mantenere la data di nascita */ struct Data { int giorno; int mese; int anno; }; /* Struttura dati per mantenere le info. della persona */ struct Persona { struct Data *data_di_nascita; char *nome; char *cognome; }; /* Setta le info. della persona */ void creaPersona(struct Persona *persona) { persona->data_di_nascita->giorno = 31; persona->data_di_nascita->mese = 12; persona->data_di_nascita->anno = 1976; persona->nome = "Eugenio"; persona->cognome = "Polito"; }
  • 14. 2 LE IDEE FONDAMENTALI /* Stampa le info. della persona */ void stampaDati(struct Persona *persona) { printf("Mi chiamo %s %s e sono nato il %i-%i-%i n", persona->nome, persona->cognome, persona->data_di_nascita->giorno, persona->data_di_nascita->mese, persona->data_di_nascita->anno); } /* crea un puntatore alla struttura e lo inizializza; quindi stampa le info. */ int main() { struct Persona *io; creaPersona(io); stampaDati(io); return 0; } Se eseguiamo questo programma, otteniamo il seguente output: Mi chiamo Eugenio Polito e sono nato il 31-12-1976 Proviamo adesso a modificare il main nel seguente modo: int main() { struct Persona *io; creaPersona(io); io->data_di_nascita->mese = 2; stampaDati(io); return 0; } Questa volta l’output è: Mi chiamo Eugenio Polito e sono nato il 31-2-1976 Cioè le mie informazioni private sono state modificate con l’assegnamento: io->data_di_nascita->mese = 2 14
  • 15. 2 LE IDEE FONDAMENTALI 2.4 15 La classe: implementare gli ADT tramite l’incapsulamento La classe consente di implementare gli ADT attraverso il meccanismo di incapsulamento: i dati devono rimanere privati insieme all’implementazione e solo l’interfaccia delle operazioni è resa pubblica all’esterno della classe. Questo approccio è fondamentale per garantire che nessuno possa accedere alle informazioni della classe e quindi, dal punto di vista del programmatore, è una garanzia per non fare errori nella stesura del codice: basti pensare all’esempio dell’ADT Persona visto prima. Se i dati fossero stati privati non avrei potuto liberamente modificare la data di nascita nel main. Quindi, ricapitolando, una classe implementa un ADT (un sinonimo di classe è proprio tipo) attraverso il meccanismo di incapsulamento. La descrizione di una classe deve elencare: i dati (o attributi): contengono le informazioni di un oggetto; le operazioni (o metodi): consentono di leggere/scrivere gli attributi di un oggetto; Quando si scrive una applicazione è buona norma iniziare con la progettazione dell’applicazione stessa; Grady Booch identifica i seguenti obiettivi in questa fase: ¡ identificare le classi; ¡ identificare le funzionalità di queste classi; ¡ trovare le relazioni fra le classi; Questo processo non può che essere iterativo. Nella fase di progettazione si usa un formalismo grafico per rappresentare le classi e le relazioni fra di esse: l’UML - Unified Modeling Language. In UML una classe si rappresenta così: Qui va il nome della classe Qui vanno messi gli attributi Qui vanno messi i metodi Figura 1: Una classe in UML.
  • 16. 2 LE IDEE FONDAMENTALI 16 Quindi la classe Persona in UML è così rappresentata: Figura 2: La classe Persona in UML. 2.5 L’oggetto Che cos’è quindi un oggetto? Per definizione, diciamo che un oggetto è una istanza di una classe. Quindi un oggetto deve essere conforme alla descrizione di una classe. Un oggetto pertanto è contraddistinto da: 1. attributi; 2. metodi; 3. identità; Allora se abbiamo una classe Persona, possiamo creare l’oggetto eugenio che è una istanza di Persona. Tale oggetto avrà degli attributi come, per esempio, nome, cognome e data di nascita; avrà dei metodi come creaPersona(...) , stampaDati(...), etc. Inoltre avrà una identità che lo contraddistingue da un eventuale fratello gemello, diciamo pippo (anche lui ovviamente istanza di Persona). Per il meccanismo di incapsulamento un oggetto non deve mai manipolare direttamente i dati di un altro oggetto: la comunicazione deve avvenire tramite messaggi (cioé chiamate a metodi). I client devono inviare messaggi ai server! Quindi nell’esempio di prima: se il fratello gemello pippo, in un momento di amnesia, vuole sapere quando è nato eugenio deve inviargli un messaggio, cioé deve richiamare un metodo ottieniDataDiNascita(...) . Quindi, ricapitolando, possiamo dire che: ¡ la classe è una entità statica cioé a tempo di compilazione; ¡ l’oggetto è una entità dinamica cioé a tempo di esecuzione (run time); Nella sezione 4 vedremo come gli oggetti vengono gestiti in Java.
  • 17. 2 LE IDEE FONDAMENTALI 2.6 17 Le relazioni fra le classi Un aspetto importante della OOP è la possibilità di definire delle relazioni fra le classi per riuscire a simulare e modellare il mondo che ci circonda : uso: una classe può usare oggetti di un’altra classe; aggregazione: una classe può avere oggetti di un’altra classe; ereditarietà: una classe può estendere un’altra classe. Vediamole in dettaglio singolarmente. 2.6.1 Uso L’uso o associazione è la relazione più semplice che intercorre fra due classi. Per definizione diciamo che una classe A usa una classe B se: - un metodo della classe A invia messaggi agli oggetti della classe B, oppure - un metodo della classe A crea, restituisce, riceve oggetti della classe B. Per esempio l’oggetto eugenio (istanza di Persona) usa l’oggetto phobos (istanza di Computer) per programmare: quindi l’oggetto eugenio ha un metodo (diciamo programma(...)) che usa phobos (tale oggetto avrà per esempio un metodo scrivi(...)). Osserviamo ancòra che in questo modo l’incapsulamento è garantito: infatti eugenio non può accedere direttamente agli attributi privati di phobos, come ram o bus (è il Sistema Operativo che gestisce tali risorse). Questo discorso può valere per Linux che nasconde bene le risorse, ma non può valere per altri Sistemi Operativi che avvertono l’avvenuto accesso a parti di memoria riservate al kernel ed invitano a resettare il computer. . . In UML questa relazione si rappresenta così: Figura 3: La relazione d’uso in UML. Per la realizzazione di questa relazione in Java vedere la sezione 3.6.1.
  • 18. 2 LE IDEE FONDAMENTALI 2.6.2 18 Aggregazione Per definizione si dice che una classe A aggrega (contiene) oggetti di una classe B quando la classe A contiene oggetti della classe B. Pertanto tale relazione è un caso speciale della relazione di uso. Sugli oggetti aggregati sarà possibile chiamare tutti i metodi, ma ovviamente non sarà possibile accedere agli attributi (l’incapsulamento continua a “regnare”!). N.B.: la relazione di aggregazione viene anche chiamata relazione has-a o ha-un. Ritorniamo al nostro esempio della classe Persona: come si è detto una persona ha una data di nascita. Risulta pertanto immediato e spontaneo aggregare un oggetto della classe Data nella classe Persona! In UML la relazione A aggrega B si disegna così: Figura 4: La relazione di aggregazione in UML. Notare che il rombo è attaccato alla classe che contiene l’altra. Un oggetto aggregato è semplicemente un attributo! Vi rimando alla sezione 3.6.3 per la realizzazione in Java di tale relazione. 2.6.3 Ereditarietà Questa relazione (anche detta inheritance o specializzazione) è sicuramente la più importante perché rende possibile il riuso del codice. Si dice che una classe D (detta la classe derivata o sottoclasse) eredita da una classe B (detta la classe base o superclasse) se gli oggetti di D formano un sottoinsieme degli oggetti della classe base B. Tale relazione è anche detta relazione is-a o è-un. Inoltre si dice che D è un sottotipo di B. Da questa definizione possiamo osservare che la relazione di ereditarietà è la relazione binaria di sottoinsieme , cioé: A ¢ ¢ è una relazione che gode della proprietà transitiva: ¢ B A C £ ¢ C ¢ ¢ Sappiamo che D A Pertanto la relazione di ereditarietà è transitiva! Nasce spontaneo domandarsi perché vale e non vale . Il motivo è presto detto: la relazione è una relazione d’ordine fra insiemi, quindi gode di tre proprietà: ¤ ¢ ¤
  • 19. 2 LE IDEE FONDAMENTALI A £ A A C ¤ ¥ ¤ ¤ B B ¤ ¥ ¤ 3. transitiva: C B B £ 2. antisimmetrica: A A ¦ ¤ 1. riflessiva: A 19 B A Ma riflettendo sul concetto di ereditarietà, affinché si verifichi la 1. dovrebbe succedere che una classe erediti da se stessa, cioé la classe dovrebbe essere una classe derivata da se stessa: impossibile! Analogamente la proprietà 2. dice che una classe è una classe base ed una classe derivata allo stesso tempo: anche questo è impossibile! Quindi vale solo la 3. D eredita da B, in UML, si disegna così: Figura 5: La relazione di ereditarietà in UML. Vediamo adesso perché con l’ereditarietà si ottiene il riuso del codice. Consideriamo una classe base B che ha un metodo f(...) ed una classe derivata D che eredita da B. La classe D può usare il metodo f(...) in tre modi: lo eredita: quindi f(...) può essere usato come se fosse un metodo di D; lo riscrive (override): cioé si da un nuovo significato al metodo riscrivendo la sua implementazione nella classe derivata, in modo che tale metodo esegua una azione diversa; lo estende: cioé richiama il metodo f(...) della classe base ed aggiunge altre operazioni. È immediato, pertanto, osservare che la classe derivata può risultare più grande della classe base relativamente alle operazioni ed agli attributi. La classe derivata non potrà accedere agli attributi della classe base, anche se li eredita, proprio per garantire l’incapsulamento. Tuttavia, come vedremo, è possibile avere un accesso
  • 20. 2 LE IDEE FONDAMENTALI 20 controllato agli attributi della classe base da una classe derivata. È importante notare che l’ereditarietà può essere simulata con l’aggregazione (cioé is-a diventa has-a)! Ovviamente ci sono dei pro e dei contro, che possiamo riassumere così: Ereditarietà Pro polimorfismo e binding dinamico Contro legame stretto fra classe base e derivata Aggregazione Pro chiusura dei moduli Contro riscrittura dei metodi nella classe derivata Tabella 1: Pro e contro dell’uso di ereditarietà e aggregazione Java non supporta l’inheritance multiplo quindi è necessario ricorrere all’aggregazione (vedere la sottosezione successiva 2.6.5). Riprendiamo la classe Persona: pensandoci bene tale classe deriva da una classe molto più grande, cioé la classe degli Animali: Animali Persone Cani ¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨ ¨§¨§¨§¨§¨§¨ © © © © © §§©¨§¨§¨§¨§¨§¨ ¨©¨©¨©¨©¨©¨ ©©¨©¨©¨©¨©¨©¨© §¨§¨§¨§¨§¨§¨ ¨©¨©¨©¨©¨©¨§ ¨§¨§¨§¨§¨§¨© §©¨§©¨§©¨§©¨§©¨§©¨©§ §©¨©¨©¨©¨©¨©¨©§©§ ¨§©¨§©¨§©¨§©¨§©¨§ ©§©©¨©¨©¨©¨©¨©¨© §¨§¨§¨§¨§¨§¨ ¨§¨§¨§¨§¨§¨§ ©§¨©¨©¨©¨©¨©¨© §¨§¨§¨§¨§¨§¨§ ©¨©¨©¨©¨©¨©¨© §¨§¨§¨§¨§¨§¨ ¨¨¨¨¨¨§©§©§ Figura 6: La classe delle persone come sottoclasse della classe degli animali. Quindi ogni Persona è un Animale; un oggetto di tipo Persona, come eugenio, è anche un Animale. Così come il mio cane bill è un oggetto di tipo Cane ed anche lui fa parte della classe Animale. Riflettiamo adesso sulle operazioni (metodi) che può fare un Animale: un animale può mangiare, dormire, cacciare, correre, etc. Una Persona è un Animale: di conseguenza eredita tutte le operazioni che può fare un Animale. Lo stesso vale per la classe Cane. Ma sorge a questo punto una domanda: una Persona mangia come un Cane? La risposta è ovviamente No! Infatti una Persona per poter mangiare usa le proprie mani, a differenza del Cane che fa tutto con la bocca e le zampe: quindi l’operazione del mangiare deve essere ridefinita nella classe Persona!
  • 21. 2 LE IDEE FONDAMENTALI 21 Inoltre possiamo pensare a cosa possa fare in più una Persona rispetto ad un Animale: può parlare, costruire, studiare, fare le guerre, inquinare... etc. Quindi nella classe derivata si possono aggiungere nuove operazioni! Si è detto precedentemente che la relazione di ereditarietà è transitiva: verifichiamo quanto detto con un esempio. Pensiamo ancora alle classe Animale: come ci insegna Quark (. . . e la Scuola Elementare. . . ) il mondo Animale è composto da sottoclassi come la classe dei Mammiferi, degli Anfìbi, degli Insetti, etc. La classe dei Mammiferi è a sua volta composta dalla classe degli Esseri Umani, dei Cani, delle Balene, etc.: Animali Mammiferi Esseri Umani Insetti Alati ¨¨¨¨¨ ¨¨¨¨¨ ¨¨¨¨¨ ¨¨¨¨¨ ¨¨ ¨¨ ¨¨ ¨¨ ¨¨ ¨¨ ¨¨ ¨¨ ¨¨ ¨¨ ¨¨ ¨¨¨ ¨¨¨ ¨¨¨ ¨¨¨ ¨¨¨ ¨¨¨ ¨¨¨ ¨¨¨ ¨¨¨ ¨¨¨ ¨¨¨ ¨¨¨ Cani Non Alati ¨¨¨¨ ¨¨¨¨ ¨¨¨¨ ¨¨¨¨ ¨¨¨¨ ¨¨¨¨ ¨¨¨¨ ¨¨¨¨ ¨¨¨¨ Figura 7: Una piccola gerarchia di Animali. Ripensiamo adesso al Cane: tale classe è una sottoclasse di Mammifero che a sua volta è una sottoclasse di Animale, in UML: Figura 8: Una piccola gerarchia di Animali in UML. Pertanto ogni Cane è un Mammifero e, poiché ogni Mammifero è un Animale, concludiamo che un Cane è un Animale! Pertanto ogni Cane potrà fare ogni operazione definita nella classe Animale. Con questo esempio abbiamo anche introdotto il concetto di gerarchia di classi, che, per definzione, è un insieme di classi che estendono una classe base comune.
  • 22. 2 LE IDEE FONDAMENTALI 2.6.4 22 Classi astratte Riprendiamo l’esempio di Figura 8 ed esaminiamo i metodi della classe base Animale: consideriamo, per esempio, l’operazione comunica(...). Se pensiamo ad un Cane tale operazione viene eseguita attraverso le espressioni della faccia, del corpo, della coda. Un Essere Umano può espletare tale operazionr in modo diverso: attraverso i gesti, le espressioni facciali, la parola. Un Delfino, invece, comunica attraverso le onde sonore. Allora che cosa significa tutto questo? Semplicemente stiamo dicendo che l’operazione comunica(...) non sappiamo come può essere realizzata nella classe base! Un discorso analogo può essere fatto per l’operazione mangia(...). In sostanza sappiamo che questi metodi esistono per tutte le classi che derivano da Animale e che sono proprio tali classi a sapere come realizzare (implementare) questi metodi. I metodi come comunica(...), mangia(...) etc., si dicono metodi astratti o metodi differiti: cioé si dichiarano nella classe base, ma non vengono implementati; saranno le classi derivate a sapere come implementare tali operazioni. Una classe che ha almeno un metodo astratto si dice classe astratta e deve essere dichiarata tale. Una classe astratta può anche contenere dei metodi non astratti (concreti)! Nella sezione 3.7 vedremo come dichiararle e usarle in Java. Attraverso delle considerazioni siamo arrivati a definire la classe Animale come classe astratta. Riflettiamo un momento sul significato di questa definizione: creare oggetti della classe Animale serve a ben poco, proprio perché tale classe è fin troppo generica per essere istanziata. Piuttosto può essere usata come un contenitore di comportamenti (operazioni) comuni che ogni classe derivata sa come implementare! Questo è un altro punto fondamentale della OOP: È bene individuare le operazioni comuni per poterle posizionare al livello più alto nella gerarchia di ereditarietà. La classe Cane, Delfino etc., implementeranno ogni operazione astratta, proprio perché ognuna di queste classi sa come fare per svolgere le operazioni ereditate dalla classe base.
  • 23. 2 LE IDEE FONDAMENTALI 2.6.5 23 Ereditarietà multipla Nella sottosezione 2.6.3 si è parlato della relazione di ereditarietà fra due classi. Questa relazione può essere estesa a più classi. Esistono tre forme di ereditarietà multipla: matrimonio fra una classe concreta ed una astratta: per esempio: Figura 9: Matrimonio fra classe concreta e astratta Quindi Stack è la classe astratta che definisce le operazioni push(...), pop(...), etc. La classe array serve per la manipolazione degli array. Pertanto la Pila implementa le operazioni dello Stack e le richiama su un array. duplice sottotipo: una classe implementa due interfacce filtrate. composizione: una classe estende due o più classi concrete. È proprio questo caso che genera alcuni problemi. Consideriamo il classico esempio (cfr. [2]) della classe Studente Lavoratore: questa classe estende la classe Studente e Lavoratore (entrambe estendono la classe Persona): Figura 10: Composizione: la classe Studente Lavoratore
  • 24. 2 LE IDEE FONDAMENTALI 24 La classe Persona ha degli attributi, come nome, cognome, data di nascita etc., e dei metodi, come, per esempio, mangia(...). Sia la classe Studente che la classe Lavoratore estendono la classe Persona, quindi erediteranno sia attributi che metodi. Supponiamo di creare l’oggetto eugenio come istanza di Studente Lavoratore e richiamiamo su di esso il metodo mangia(...). Purtroppo tale metodo esiste sia nella classe Studente che nella classe Lavoratore: quale metodo sarà usato? Nessuno dei due perché il compilatore riporterà un errore in fase di compilazione! Analogamente i membri saranno duplicati perché saranno ereditati da entrambe le classi (Studente e Lavoratore): eugenio si ritroverà con due nomi, due cognomi e due date di nascita. I progettisti di Java, proprio per evitare simili problemi, hanno deciso di non supportare la composione come forma di ereditarietà multipla. Però, come si è detto nella sezione 2.6.3, l’ereditarietà può essere simulata con l’aggregazione, pertanto il diagramma UML di Figura 10 può essere così ridisegnato: Figura 11: Studente Lavoratore come aggregazione e specializzazione Adesso lo Studente Lavoratore eredita un solo metodo mangia(...), dorme(...), etc., così come avrà un solo nome, cognome, etc. Se eugenio deve lavorare(. . . ) richiamerà il metodo omonimo sull’oggetto aggregato istanza di Lavoratore. Questo esempio porta ad un’altra riflessione importante: ma eugenio sarà sempre uno Studente? Si spera di no. . . Prima o poi finirà di studiare! Come si è detto in Tabella 1, l’ereditarietà ha lo svantaggio di stabilire un legame troppo forte tra classe base e derivata. Ciò significa che l’oggetto eugenio (che magari continua a vivere nella società in qualità di Lavoratore), anche quando non sarà più uno Studente, potrà invocare il metodo faiLaFilaInSegreteria(...) o ricopiaAppunti(...) , perché con-
  • 25. 2 LE IDEE FONDAMENTALI 25 tinua ad essere uno Studente, secondo la gerarchia di Figura 11! Risulta immediato cambiare nuovamente l’ereditarietà con l’aggregazione: Figura 12: Studente Lavoratore come aggregazione In questo modo, ad esempio, il metodo faiLaFilaInSegreteria(...) viene richiamato sull’oggetto aggregato istanza di Studente. Quando eugenio non sarà più Studente, l’oggetto aggregato istanza di Studente verrà eliminato (tanto è un semplice attributo!). Se poi malauguratamente eugenio perde il proprio lavoro, non aggrega più la classe Lavoratore: può comunque aggregare una nuova classe, come per esempio Disoccupato: Figura 13: L’ex Studente ed ex Lavoratore ora Disoccupato Perché abbiamo aggregato una nuova classe (Disoccupato)? Se guardiamo la Figura 11 si ha (per la transitività della relazione di ereditarietà): Studente Lavoratore Studente Persona Studente Lavoratore Persona. Quindi ogni Studente Lavoratore può invocare il metodo mangia(...) della classe Persona (lo eredita). Analogamente, in Figura 11, vediamo che sia Studente che Lavoratore ereditano da Persona. Quindi uno Studente Lavoratore può invocare il metodo mangia(...) sia sull’oggetto aggregato istanza di Studente che sull’oggetto aggregato istanza di Lavoratore. £ ¢ ¢ ¢
  • 26. 2 LE IDEE FONDAMENTALI 26 Ma se un oggetto di classe Studente Lavoratore termina di studiare e perde il lavoro (cioé eliminiamo gli attributi, oggetti di tipo Studente e Lavoratore) potrà continuare a mangiare? Risposta: No! Ecco spiegato il motivo per cui è stata aggregata una nuova classe in Studente Lavoratore. Ricapitolando: ¡ L’ereditarietà multipla sottoforma di composizione può essere modellata con l’aggregazione e con l’ereditarietà singola. È bene usare questa combinazione per non incorrere in problemi seri durante la stesura del codice. ¡ Usare l’ereditarietà solo quando il legame fra la classe base e la classe derivata è per sempre, cioé dura per tutta la vita degli oggetti, istanze della classe derivata. Se tale legame non è duraturo è meglio usare l’aggregazione al posto della specializzazione.
  • 27. 2 LE IDEE FONDAMENTALI 2.7 27 Binding dinamico e Polimorfismo La parola polimorfismo deriva dal greco e significa letteralmente molte forme. Nella OOP tale termine si riferisce ai metodi: per definizione, il polimorfismo è la capacità di un oggetto, la cui classe fa parte di una gerarchia, di chiamare la versione corretta di un metodo. Quindi il polimorfismo è necessario quando si ha una gerarchia di classi. Consideriamo il seguente esempio: Figura 14: La classe Studente come sottoclasse di Persona Nella classe base Persona è definito il metodo calcolaSomma(...) , che, per esempio, esegue la somma sui naturali 2+2 e restituisce 5 (in 3 vedremo come passare argomenti ad un metodo e restituire valori); la classe derivata Studente invece riscrive il metodo calcolaSomma(...) ed esegue la somma sui naturali 2+2 in modo corretto, restituendo 4. N.B. Il metodo deve avere lo stesso nome, parametri e tipo di ritorno in ogni classe, altrimenti non ha senso parlare di polimorfismo. Creiamo adesso l’oggetto eugenio come istanza di Studente ed applichiamo il metodo calcolaSomma(...) . L’oggetto eugenio è istanza di Studente, quindi verrà richiamato il metodo di tale classe ed il risulato sarà 4. Supponiamo adesso di modificare il tipo di eugenio in Persona (non ci preoccupiamo del dettaglio del linguaggio, vedremo in 4.3 come è possibile farlo in Java): cambiare il tipo di un oggetto, istanza di una classe derivata, in tipo della classe base è possibile ed è proprio per questo motivo che è necessario il polimorfismo; tuttavia questa conversione o cast comporta una perdita di proprietà dell’oggetto perché una classe base ha meno informazioni (metodi ed attributi) della classe derivata. A questo punto richiamiamo il metodo calcolaSomma(...) sull’oggetto eugenio. Stavolta verrà richiamato il metodo della classe base: il tipo di eugenio è Persona e quindi il risultato è 5! Ma come è possibile invocare un metodo sullo stesso oggetto in base al suo tipo? Ovviamente questo non può essere fatto durante la compilazione del programma, perché il metodo da invocare deve dipendere dal tipo dell’oggetto durante
  • 28. 2 LE IDEE FONDAMENTALI 28 l’esecuzione del programma! Per rendere possibile questo il compilatore deve fornire il binding dinamico, cioé il compilatore non genera il codice per chiamare un metodo durante la compilazione (binding statico), ma genera il codice per calcolare quale metodo chiamare su un oggetto in base alle informazioni sul tipo dell’oggetto stesso durante l’esecuzione (run-time) del programma. Questo meccanismo rende possibile il polimorfismo puro (o per sottotipo): il messaggio che è stato inviato all’oggetto eugenio era lo stesso, però ciò che cambiava era la selezione del metodo corretto da invocare che dipendeva quindi dal tipo a run-time dell’oggetto. Ecco come viene invocato correttamente un metodo in una gerarchia di ereditarietà (supponiamo che il metodo venga richiamato su una sottoclasse, p.e. Studente): ¡ la sottoclasse controlla se ha un tale metodo; in caso affermativo lo usa, altrimenti: ¡ la classe padre si assume la responsabilità e cerca il metodo. Se lo trova lo usa, altrimenti sarà la sua classe padre a predendere la responsabilità di gestirlo. Questa catena si interrompe se il metodo viene trovato, e sarà tale classe ad invocarlo, altrimenti, se non viene trovato, il compilatore segnala l’errore in fase di compilazione. Pertanto lo stesso metodo può esistere su più livelli della gerarchia di ereditarietà. Il polimofismo puro non è l’unica forma di polimorfismo: polimorfismo ad hoc (overloading) un metodo può avere lo stesso nome ma parametri diversi: il compilatore sceglie la versione corretta del metodo in base al numero ed al tipo dei parametri. Il tipo di ritorno non viene usato per la risoluzione, cioé se si ha un metodo con gli stessi argomenti e diverso tipo di ritorno, il compilatore segnala un errore durante la compilazione. Tale meccanismo è quindi risolto a tempo di compilazione. N.B. Il polimorfismo puro invece si applica a metodi con lo stesso nome, numero e tipo di parametri e tipo di ritorno e viene risolto a run-time. polimorfismo parametrico è la capacità di eseguire delle operazioni su un qualsiasi tipo: questa tipologia non esiste in Java (ma può essere simulato cfr. 3.9), perché necessita del supporto di classi parametriche. Per la realizzazione di questo meccanismo in C++ cfr. [2].
  • 29. 29 Parte II La OOP in Java In questa parte vedremo come vengono realizzati i concetti della OOP in Java. 3 3.1 Classi e oggetti Definire una classe La definizione di una classe in Java avviene tramite la parola chiave class seguita dal nome della classe. Affinché una classe sia visibile ad altre classi e quindi istanziabile è necessario definirla public: public class Prima { } N.B. In Java ogni classe deriva dalla classe base cosmica Object: quindi anche se non lo scriviamo esplicitamente, il compilatore si occupa di stabilire la relazione di ereditarietà fra la nostra classe e la classe Object! Le parentesi { e } individuano l’inzio e la fine della classe ed, in generale, un blocco di istruzioni. È bene usare la lettera maiuscola iniziale per il nome della classe; inoltre il nome della classe deve essere lo stesso del nome del file fisico, cioé in questo caso avremmo Prima.java (vedere la sezione 5). Affinché una classe realizzi un ADT (cfr. sezione 2.3) è necessario definire i dati e le operazioni. 3.2 Garantire l’incapsulamento: metodi pubblici e attributi privati Come si è detto (cfr. sezione 2.4), uno dei princìpi della OOP è l’incapsulamento: quindi è necessario definire dati (membri nella terminologia Java) privati e le operazioni (detti anche metodi in Java) pubbliche. Definiamo l’ADT Persona della sezione 2.3 in Java; per adesso supponiamo che la persona abbia tre attributi nome, cognome, anni e due metodi creaPersona(...) e stampaDati(...):
  • 30. 3 CLASSI E OGGETTI 30 public class Persona { /* questo metodo inizializza gli attributi nome, cognome ed anni */ public void creaPersona(String n,String c,int a) { nome = n; cognome = c; anni = a; } // questo metodo stampa gli attributi public void stampaDati() { System.out.println(Nome: +nome); System.out.println(Cognome: +cognome); System.out.println(Età: +anni); } // attributi private String nome; private String cognome; private int anni; } Abbiamo definito quindi gli attributi private ed i metodi public: l’incapsulamento è garantito! Riflettiamo un attimo sulla sintassi: attributi: la dichiarazione di un attributo richiede il modificatore di accesso (usare sempre private!), il tipo dell’attributo (String, int, etc. che può essere sia un tipo primitivo che una classe - vedere la sezione 5 per i tipi primitivi del linguaggio) ed il nome dell’attibuto (anni, nome, etc.); metodi: la dichiarazione di un metodo richiede invece: il modificatore di accesso (può essere sia public che private, in questo caso però il metodo non potrà essere invocato da un oggetto - è bene usarlo per “funzioni di servizio”), il tipo di ritorno che può essere: o un tipo primitivo o una classe o void: cioè il metodo non restituisce alcuna informazione. Se il
  • 31. 3 CLASSI E OGGETTI 31 tipo di ritorno è diverso da void, deve essere usata l’istruzione return valore_da_restituire; come ultima istruzione del metodo; il valore_da_restituire deve avere un tipo in match col tipo di ritorno (devono essere gli stessi tipi). Segue quindi il nome del metodo e fra parentesi tonde si specificano gli argomenti (anche detti firma del metodo): anche essi avranno un tipo (primitivo o una classe) ed un nome; più argomenti vanno separati da una virgola. La lista degli argomenti può essere vuota. Nel nostro caso gli argomenti passati al metodo creaPersona(...) servono per inizializzare gli attributi: con l’assegnamento nome = n; stiamo dicendo che all’attributo nome stiamo assegnandogli la variabile n. // viene usato per i commenti su una singola linea, mentre /* e */ vengono usati per scrivere commenti su più linee (il compilatore ignora i commenti). Il metodo println(...) della classe System è usato per scrivere l’output a video e + serve per concatenare stringhe (vedere la sezione 5). La classe, così come è stata definita, non serve ancora a molto: vogliamo creare oggetti che siano istanze di questa classe, sui quali possiamo invocare dei metodi. Dove andremo ad istanziare un generico oggetto di tipo Persona? Prima di rispondere a questa domanda affrontiamo un altro discorso importante che ci servirà per capire “alcune cose”: 3.3 Metodi ed attributi statici Gli attributi static sono utili quando è necessario condividerli fra più oggetti, quindi anziché avere più copie di un attributo che dovrà essere lo stesso per tutti gli oggetti istanza della stessa classe, esso viene inzializzato una volta per tutte e posto nella memoria statica. Un simile attributo avrà la stessa vita del programma, per esempio possiamo immaginare che in una classe Data è utile avere memorizzato un array (cfr. 5) dei mesi dell’anno: private static String[] mesi = {Gen,Feb,Mar, Apr,Mag,Giu, Lug,Ago,Set, Ott,Nov,Dic}; Tale array sarà condiviso fra tutti gli oggetti di tipo Data. Siccome tale array, è in realtà costante, risulta comodo definirlo tale: in Java si usa la parola final per definire un attributo costante: private static final String[] mesi = {Gen,Feb,Mar, Apr,Mag,Giu,
  • 32. 3 CLASSI E OGGETTI 32 Lug,Ago,Set, Ott,Nov,Dic}; Quindi mesi non è modificabile! Allo stesso modo è possibile definire un metodo static: un tale metodo può essere richiamato senza la necessità di istanziare la classe (vedere la sottosezione 3.6.2 per un esempio). In Java esiste un punto di inizio per ogni programma, dove poter creare l’oggetto istanza della classe ed invocare i metodi: il metodo main(...). Esso viene richiamato prima che qualsiasi oggetto è stato istanziato, pertanto è necessario che sia un metodo statico. La sua dichiarazione, che deve comparire in una sola classe, è la seguente: public static void main(String args[]) { } Quindi è public per poter essere visto all’esterno, è static per il motivo che si diceva prima, non ha alcun tipo di ritorno, accetta degli argomenti di tipo String che possono essere passati a linea di comando. 3.4 Costruire un oggetto Possiamo adesso affrontare la costruzione di un oggetto. In Java un oggetto viene costruito con il seguente assegnamento: Prova primo = new Prova(); Analizziamo la sintassi: stiamo dicendo che il nostro oggetto di nome primo è una istanza della classe Prova e che lo stiamo costruendo, con l’operatore new, attraverso il costruttore Prova(). L’oggetto che viene così creato è posto nella memoria heap (o memoria dinamica), la quale cresce e dimunisce a run-time, ogni volta che un oggetto è creato e distrutto. N.B. Mentre la costruzione la controlliamo noi direttamente, la distruzione viene gestita automaticamente dalla JVM: quando un oggetto non viene più usato, la JVM si assume la responsabilità di eliminarlo, senza che noi ce ne possiamo accorgere, tramite il meccanismo di Garbage Collection! L’assegnamento dice che la variabile oggetto primo è un riferimento ad un oggetto, istanza della classe Prova. Il concetto di riferimento è importante: molti pensano che Java non abbia i puntatori: sbagliato! Java non ha la sintassi da puntatore ma ne ha il comportamento.
  • 33. 3 CLASSI E OGGETTI 33 Infatti una variabile oggetto serve per accedere all’oggetto e non per memorizzarne le sue informazioni!; pertanto un oggetto di Java si comporta come una variabile puntatore di C++. La gestione dei puntatori viene completamente nascosta al programmatore, il quale può solo usare riferimenti agli oggetti. La situazione dopo la costruzione dell’oggetto primo è la seguente: primo Prova Figura 15: L’oggetto primo appena creato Sottolineiamo che con la seguente scrittura: Prova primo; non è stato creato alcun oggetto, infatti si sta semplicemente dicendo che l’oggetto primo che verrà creato sarà una istanza di Prova o di una sua sottoclasse; si ha questa situazione: primo Prova Figura 16: L’oggetto primo non ancora creato cioé primo non è ancora un oggetto in quanto non fa riferimento a niente! La costruzione dovrà avvenire con l’istruzione: primo = new Prova(); Come si è detto prima, il metodo Prova() è il costruttore dell’oggetto, cioé è il metodo che si occupa di inizializzare gli attributi dell’oggetto. Essendo un metodo può essere overloadato, cioé può essere usato con argomenti diversi. Un costruttore privo di argomenti si dice costruttore di default: se non se ne fornisce nessuno, Java si occupa di crearne uno di default automaticamente che si occupa di inizializzare gli attributi. Il costruttore ha lo stesso nome della classe e non ha alcun tipo di ritorno. Inoltre esso è richiamato soltanto una volta, cioé quando l’oggetto viene creato e non può essere più richimato durante la vita dell’oggetto.
  • 34. 3 CLASSI E OGGETTI 3.5 34 La classe Persona e l’oggetto eugenio Vediamo allora come scrivere una versione migliore della classe Persona, in cui forniamo un costruttore ed un main: public class Persona{ // Costruttore: inizializza gli attributi nome, cognome, anni public Persona(String nome,String cognome,int anni) { this.nome = nome; this.cognome = cognome; this.anni = anni; } // questo metodo stampa gli attributi public void stampaDati() { System.out.println(Nome: +nome); System.out.println(Cognome: +cognome); System.out.println(Età: +anni); } // attributi private String nome; private String cognome; private int anni; // main public static void main(String args[]) { Persona eugenio = new Persona(Eugenio,Polito,26); eugenio.stampaDati(); } } Commentiamo questa classe: abbiamo definito il costruttore Persona(String nome, String cognome, int anni) che si occupa di ricevere in ingresso i tre parametri nome, cognome ed anni e si occupa di inizializzare gli attributi nome, cognome e anni con i valori dei parametri. Come si può notare è stata usata
  • 35. 3 CLASSI E OGGETTI 35 la parola chiave this: questo non è altro che un puntatore che fa riferimento all’oggetto attuale (o implicito). Quindi la sintassi this.nome significa “fai riferimento all’attributo nome dell’oggetto corrente”. In questo caso è essenziale perchè il nome dell’argomento ed il nome dell’attributo sono identici. Come vedremo, this è utile anche per richiamare altri costruttori. Il metodo stampaDati() serve per stampare gli attributi dell’oggetto. Il metodo main(...) contiene al suo interno due istruzioni: Persona eugenio = new Persona(Eugenio,Polito,26); con tale istruzione stiamo creando l’oggetto eugenio: esso viene costruito con il costruttore che ha la firma (gli argomenti) String,String,int (l’unico che abbiamo definito). A run time la situazione, dopo questo assegnamento, sarà la seguente: eugenio Persona nome = Eugenio cognome = Polito anni = 26 stampaDati() Figura 17: L’oggetto eugenio dopo la costruzione eugenio.stampaDati(); richiama il metodo stampaDati() sull’oggetto eugenio; il “.” viene usato per accedere al metodo. E se avessimo voluto costruire l’oggetto col costruttore di default? Avremmo ottenuto un errore, perché nella classe non sarebbe stato trovato dal compilatore alcun costruttore senza argomenti, quindi è bene fornirne uno: public class Persona{ // Costruttore di default public Persona() { this(,,0); } // Costruttore: inzializza gli attributi nome, cognome, anni public Persona(String nome,String cognome,int anni) { this.nome = nome;
  • 36. 3 CLASSI E OGGETTI 36 this.cognome = cognome; this.anni = anni; } // questo metodo stampa gli attributi public void stampaDati() { System.out.println(Nome: +nome); System.out.println(Cognome: +cognome); System.out.println(Età: +anni); } // attributi private String nome; private String cognome; private int anni; // main public static void main(String args[]) { Persona eugenio = new Persona(Eugenio,Polito,26); Persona anonimo = new Persona(); eugenio.stampaDati(); anonimo.stampaDati(); } } Il costruttore di default richiama il costruttore che ha come argomenti: String, String, int, attraverso il riferimento all’argomento implicito this. In questo esempio il costruttore è un metodo che usa l’overloading: in base al numero e tipo di argomenti, il compilatore seleziona la versione corretta del metodo (cfr. Sezione 2.7). L’output del programma è il seguente: Nome: Eugenio Cognome: Polito Età: 26 Nome: Cognome: Età: 0
  • 37. 3 CLASSI E OGGETTI 3.6 3.6.1 37 Realizzare le relazioni fra classi Uso Riprendiamo l’esempio della sezione 2.6.1: vediamo come si realizza la relazione di uso. Supponiamo che la classe Persona usi la classe Computer per eseguire il prodotto e la somma di 2 numeri, quindi definiamo la classe Computer e poi la classe Persona: public class Computer { // restituisce il prodotto di a * b public int calcolaProdotto(int a, int b) { return a*b; } // restituisce la somma di a + b public int calcolaSomma(int a, int b) { return a+b; } } Tale classe ha il metodo calcolaProdotto(...) che si occupa di calcolare il prodotto di due numeri, passati come argomento e di restituirne il risultato (return a*b;). Il discorso è analogo per il metodo calcolaSomma(...) . La classe Persona invece è: public class Persona { // Costruttore di default public Persona() { this(,,0); } // Costruttore: inizializza gli attributi nome, cognome, anni public Persona(String nome,String cognome,int anni) { this.nome = nome; this.cognome = cognome;
  • 38. 3 CLASSI E OGGETTI 38 this.anni = anni; } // questo metodo stampa gli attributi public void stampaDati() { System.out.println(Nome: +nome); System.out.println(Cognome: +cognome); System.out.println(Età: +anni); } /* usa l’oggetto ’phobos’ istanza di Computer per eseguire il prodotto e la somma degli interi a e b passati come argomenti */ public void usaComputer(int a, int b) { Computer phobos = new Computer(); int res = phobos.calcolaProdotto(a,b); System.out.println(Risultato del prodotto +a+ * +b+: +res); res = phobos.calcolaSomma(a,b); System.out.println(Risultato della somma +a+ + +b+: +res); } // attributi private String nome; private String cognome; private int anni; // main public static void main(String args[]) { Persona eugenio = new Persona(Eugenio,Polito,26); eugenio.usaComputer(5,5); } } Il metodo usaComputer(...) crea (quindi usa) un oggetto phobos, istanza della classe Computer (Computer phobos = new Computer();) richiama il metodo
  • 39. 3 CLASSI E OGGETTI 39 calcolaProdotto(...) su phobos, passandogli gli argomenti a e b. Il risultato del calcolo viene posto temporaneamente nella variabile locale res: all’uscita dal metodo tale variabile verrà eliminata; ogni variabile locale deve essere inizializzata, altrimenti il compilatore riporta un errore! L’ istruzione successiva System.out.println(...) stampa l’output a video. res = phobos.calcolaSomma(a,b); richiama sull’oggetto phobos il metodo calcolaSomma(...) ed il risultato viene posto in res (tale variabile è stata già dichiarata quindi non si deve specificare di nuovo il tipo, inoltre il risultato del prodotto viene perso perché adesso res contiene il valore della somma!). L’istruzione successiva stampa il risultato della somma. Notiamo che così come la variabile locale res nasce, vive e muore in questo metodo, anche l’oggetto phobos ha lo stesso ciclo di vita: quando il metodo termina, l’oggetto phobos viene distrutto automaticamente dal Garbage Collector della JVM e la memoria da lui occupata viene liberata. N.B. Gli oggetti costruiti nel main (così come le variabili) vivono per tutta la durata del programma! Nel main viene creato l’oggetto eugenio che invoca il metodo usaComputer(...) per usare il computer. 3.6.2 Metodi static: un esempio Riprendiamo la classe Computer: come possiamo notare, non ha degli attributi; in realtà, non ci importa istanziare tale classe perché, così come è stata definita, funge più da contenitore di metodi che da classe istanziabile. Pertanto i metodi di tale classe li possiamo definire static: public class Computer{ // restituisce il prodotto di a * b public static int calcolaProdotto(int a, int b) { return a*b; } // restituisce la somma di a + b public static int calcolaSomma(int a, int b) { return a+b; } } Adesso dobbiamo rivedere il metodo usaComputer(...) della classe Persona:
  • 40. 3 CLASSI E OGGETTI 40 public class Persona { // Costruttore di default public Persona() { this(,,0); } // Costruttore: inizializza gli attributi nome, cognome, anni public Persona(String nome,String cognome,int anni) { this.nome = nome; this.cognome = cognome; this.anni = anni; } // questo metodo stampa gli attributi public void stampaDati() { System.out.println(Nome: +nome); System.out.println(Cognome: +cognome); System.out.println(Età: +anni); /* usa i metodi della classe Computer per eseguire il prodotto e la somma fra gli interi a e b passati come argomenti */ public void usaComputer(int a, int b) { int res = Computer.calcolaProdotto(a,b); System.out.println(Risultato del prodotto +a+ * +b+: +res); res = Computer.calcolaSomma(a,b); System.out.println(Risultato della somma +a+ + +b+: +res); } // attributi private String nome; private String cognome; private int anni;
  • 41. 3 CLASSI E OGGETTI 41 // main public static void main(String args[]) { Persona eugenio = new Persona(Eugenio,Polito,26); eugenio.usaComputer(5,5); } } Come si può notare nel metodo usaComputer(...) , questa volta non viene creato un oggetto istanza della classe Computer, ma si usa quest’ultima per accedere ai metodi calcolaSomma(...) e calcolaProdotto(...) , essendo dei metodi static. 3.6.3 Aggregazione Riprendiamo l’esempio discusso nella sezione 2.6.2: si diceva che la classe Persona aggrega la classe Data, perché ogni persona ha una data di nascita. Definiamo la classe Data: public class Data { /* Costruttore: inizializza gli attributi giorno, mese, anno con i valori passati come argomenti */ public Data(int giorno, int mese, int anno) { this.giorno = giorno; this.mese = mese; this.anno = anno; } // stampa la Data public void stampaData() { System.out.println(giorno+/+mese+/+anno); } // attributi private int giorno, mese, anno; } Tale classe ha gli attributi giorno, mese e anno che vengono inizializzati col costruttore e possono essere stampati a video col metodo stampaData(). La classe Persona:
  • 42. 3 CLASSI E OGGETTI 42 public class Persona { /* Costruttore: inizializza gli attributi nome, cognome, e data di nascita */ public Persona(String nome, String cognome, int giorno, int mese, int anno) { this.nome = nome; this.cognome = cognome; dataDiNascita = new Data(giorno,mese,anno); } /* stampa gli attributi e richiama il metodo stampaData() sull’oggetto dataDiNascita per la stampa della data di nascita */ public void stampaDati() { System.out.println(Nome: +nome); System.out.println(Cognome: +cognome); System.out.print(Nato il: ); dataDiNascita.stampaData(); } // attributi private String nome; private String cognome; private Data dataDiNascita; // main public static void main(String args[]) { Persona eugenio = new Persona(Eugenio,Polito,31,12,1976); eugenio.stampaDati(); } } contiene gli attributi nome, cognome e dataDiNascita (istanza di Data): quindi l’aggregazione si realizza in Java come attributo. Notiamo che l’oggetto dataDiNascita viene creato nel costruttore con gli argo-
  • 43. 3 CLASSI E OGGETTI 43 menti passati come parametri: l’oggetto viene costruito solo quando si sa come farlo. Osserviamo che l’incapsulamento è garantito: gli attributi dell’oggetto dataDiNascita possono essere letti solo col metodo stampaData(). N.B. Come si è detto il main deve comparire una sola volta in una sola classe; per chiarezza, quando si ha più di una classe, è consigliabile porlo in un’altra classe. Quindi, in questo caso, lo togliamo dalla classe Persona e lo poniamo in una nuova classe, diciamo Applicazione: public class Persona { /* Costruttore: inizializza gli attributi nome, cognome, e data di nascita */ public Persona(String nome, String cognome, int giorno, int mese, int anno) { this.nome = nome; this.cognome = cognome; dataDiNascita = new Data(giorno,mese,anno); } /* stampa gli attributi e richiama il metodo stampaData() sull’oggetto dataDiNascita per la stampa della data di nascita */ public void stampaDati() { System.out.println(Nome: +nome); System.out.println(Cognome: +cognome); System.out.print(Nato il: ); dataDiNascita.stampaData(); } // attributi private String nome; private String cognome; private Data dataDiNascita; } e la classe Applicazione conterrà il main: public class Applicazione {
  • 44. 3 CLASSI E OGGETTI 44 // main public static void main(String args[]) { Persona eugenio = new Persona(Eugenio,Polito,31,12,1976); eugenio.stampaDati(); } } In seguito verrà utilizzato questo modo di procedere. 3.6.4 Ereditarietà Vogliamo estendere la classe Persona in modo da gestire la classe Studente, cioé vogliamo che Studente erediti da Persona: questo è logicamente vero dal momento che ogni Studente è una Persona. Definiamo la classe Persona: import java.util.Random; public class Persona { /* Costruttore: inizializza gli attributi nome, cognome, e data di nascita */ public Persona(String nome, String cognome, int giorno, int mese, int anno) { this.nome = nome; this.cognome = cognome; dataDiNascita = new Data(giorno,mese,anno); } /* stampa gli attributi e richiama il metodo stampaData() sull’oggetto dataDiNascita per la stampa della data di nascita */ public void stampaDati() { System.out.println(Nome: +nome); System.out.println(Cognome: +cognome); System.out.print(Nato il: );
  • 45. 3 CLASSI E OGGETTI 45 dataDiNascita.stampaData(); } // autoesplicativo public void mangia() { System.out.println(nMangio con forchetta e coltellon); } // stampa casualmente ’n’ numeri (magari da giocare al lotto:) public void conta(int n) { System.out.print(Conto: ); Random r = new Random(); for (int i = 0; i n; i++) System.out.print(r.nextInt(n)+t); System.out.println(); } // attributi private String nome; private String cognome; private Data dataDiNascita; } Con l’istruzione import java.util.Random si sta importando la classe Random che è contenuta nel package java.util (per l’uso dei package vedere la sezione 5) e serve per generare dei numeri pseudo-casuali. Il costruttore ed il metodo stampaDati() sono stati già discussi. Il metodo mangia() stampa a video un messaggio molto eloquente; il simbolo “ n” serve per andare a capo. Il metodo conta(...) riceve come argomento un intero n, e stampa n numeri casuali attraverso un ciclo iterativo (per i cicli vedere 5). Adesso vogliamo definire la classe Studente come sottoclasse di Persona, in cui: ¡ il metodo mangia() viene ereditato; ¡ il metodo conta(...) viene riscritto; ¡ il metodo stampaDati() viene esteso. ¡ viene aggiunto il metodo faiFilaInSegreteria() ;
  • 46. 3 CLASSI E OGGETTI 46 Supponiamo inoltre che la nuova classe abbia l’attributo anniDiScuola. La classe Studente è dunque: public class Studente extends Persona { /* Costruttore: richiama il costruttore della classe base (inializzando gli attributi nome, cognome, dataDiNascita) ed inializza il membro anniDiScuola */ public Studente(String nome, String cognome, int giorno, int mese, int anno, int anniDiScuola) { super(nome,cognome,giorno,mese,anno); this.anniDiScuola = anniDiScuola; } /* Riscrive il metodo omonimo della classe base: Stampa i numeri 1,2,...,n */ public void conta(int n) { System.out.print(Conto: ); for (int i = 1; i = n; i++) System.out.print(i+t); System.out.println(); } /* Estende il metodo omonimo della classe base: richiama il metodo della classe base omonimo ed in più stampa l’attributo anniDiScuola */ public void stampaDati() { super.stampaDati(); System.out.println(Anni di scuola: +anniDiScuola); } /* stampa il messaggio ... */ public void faiFilaInSegreteria() {
  • 47. 3 CLASSI E OGGETTI 47 System.out.println(...Aspetto il mio turno in segreteria...); } // attributo private int anniDiScuola; } In Java l’ereditarietà è resa possibile con la parola chiave extends. Il costruttore richiama il costruttore della classe base, che ha la firma String, String, int ,int, int, attraverso la parola chiave super; inoltre inizializza il membro anniDiScuola. Notiamo che per costruire gli attributi nome, cognome, dataDiNascita è necessario ricorrere al costruttore della classe base perché hanno tutti campo di visibilità (o scope) private. Il metodo conta(...) è stato riscritto: ora stampa “correttamente” i numeri 1,2,. . . ,n. Il metodo stampaDati(...) è stato esteso: richiama il metodo omonimo della classe base ed in più stampa l’attributo anniDiScuola. Infine è stato aggiunto il metodo faiFilaInSegreteria che stampa un messaggio di attesa. . . Come si è detto gli attributi della classe base non sono accessibili alla classe derivata perché hanno scope private. Tuttavia è possibile consentire solo alle classi derivate di avere un accesso protetto agli attributi, attraverso il modificatore di accesso protected. La classe Persona può essere pertanto riscritta nel seguente modo: import java.util.Random; public class Persona{ // Costruttore di default public Persona() { this(,,0,0,0); } /* Costruttore: inizializza gli attributi nome, cognome, anni e data di nascita */ public Persona(String nome, String cognome, int giorno, int mese, int anno)
  • 48. 3 CLASSI E OGGETTI 48 { this.nome = nome; this.cognome = cognome; this.dataDiNascita = new Data(giorno,mese,anno); } /* stampa gli attributi e richiama il metodo stampaData() sull’oggetto dataDiNascita per la stampa della data di nascita */ public void stampaDati() { System.out.println(Nome: +nome); System.out.println(Cognome: +cognome); System.out.print(Nato il: ); dataDiNascita.stampaData(); } // autoesplicativo public void mangia() { System.out.println(nMangio con forchetta e coltellon); } // stampa casualmente ’n’ numeri (magari da giocare al lotto:) public void conta(int n) { System.out.print(Conto: ); Random r = new Random(); for (int i = 0; i n; i++) System.out.print(r.nextInt(n)+t); System.out.println(); } // attributi protected String nome; protected String cognome; protected Data dataDiNascita; // main public static void main(String args[])
  • 49. 3 CLASSI E OGGETTI 49 { Persona eugenio = new Persona(Eugenio,Polito,31,12,1976); eugenio.stampaDati(); eugenio.mangia(); eugenio.conta(5); } } Adesso possiamo avere accesso diretto agli attributi della classe base dalla classe derivata Studente: public class Studente extends Persona { /* Costruttore: inizializza gli attributi public Studente(String nome, String cognome, int giorno, int mese, int anno, int anniDiScuola) { this.nome = nome; this.cognome = cognome; this.dataDiNascita = new Data(giorno,mese,anno); this.anniDiScuola = anniDiScuola; } /* Riscrive il metodo omonmio della classe base: Stampa i numeri 1,2,...,n */ public void conta(int n) { System.out.print(Conto: ); for (int i = 1; i = n; i++) System.out.print(i+t); System.out.println(); } /* Estende il metodo omonimo della classe base: richiama il metodo della classe base omonimo ed in più stampa l’attributo anniDiScuola */ public void stampaDati()
  • 50. 3 CLASSI E OGGETTI 50 { super.stampaDati(); System.out.println(Anni di scuola: +anniDiScuola); } /* stampa il messaggio ... */ public void faiFilaInSegreteria() { System.out.println(...Aspetto il mio turno in segreteria...); } // attributo private int anniDiScuola; } Notare come adesso nel costruttore si possa accedere direttamente agli attributi della classe base (this.nome, this.cognome, this.dataDiNascita ). Un’altra cosa da osservare è che si è reso necessario inserire un costruttore di default nella classe base perché il costruttore della classe derivata va a cercare subito il costruttore di default della superclasse e poi inizializza gli attributi! Vediamo una applicazione di esempio: public class Applicazione { public static void main(String args[]) { Persona bill = new Persona(Bill,Cancelli,13,13,1984); bill.stampaDati(); bill.conta(5); bill.mangia(); Studente tizio = new Studente(Pinco,Pallino,1,1,1970,15); tizio.stampaDati(); tizio.conta(5); tizio.mangia(); tizio.faiFilaInSegreteria(); } } Una possibile esecuzione è la seguente: Nel main abbiamo creato l’oggetto bill (istruzione Persona bill = new Persona(Bill,Cancelli,13,13,1984);), il quale invoca i metodi stampaDati();, conta(5); e mangia();.
  • 51. 3 CLASSI E OGGETTI 51 Figura 18: Esecuzione del programma Applicazione.java È stato quindi creato l’oggetto tizio come istanza della classe Studente. Richiama il metodo stampaDati: come si può vedere in Figura 18, oltre al nome, cognome e data di nascita viene stampato l’attributo anniDiScuola (ricordiamoci che tale metodo è stato esteso, proprio per permettere di stampare tale attributo). Viene poi richiamato il metodo conta(5): siccome tale metodo è stato riscritto, stampa a video la sequenza corretta dei numeri 1,2,. . . ,n. L’oggetto tizio invoca poi il metodo mangia(), che essendo ereditato stampa lo stesso messaggio che è stato stampato precedentemente dallo stesso metodo invocato da bill. Infine tizio invoca il metodo aggiunto nella classe Studente faiFilaInSegreteria() che stampa un messaggio. Osserviamo che se avessimo invocato faiLaFilaInSegreteria() sull’oggetto bill avremmo ottenuto un messaggio di errore, perché tale metodo non è definito nella classe Persona.
  • 52. 3 CLASSI E OGGETTI 3.7 52 Classi astratte Nella sezione 2.6.4 abbiamo parlato del concetto di classe astratta: vediamo adesso come si realizza in Java. Come si è detto, un Animale può essere considerato un contenitore di operazioni (dal momento che non si sa come definire tali operazioni in generale: come mangia() o comunica() un Animale?) per tutte le classi derivate, come Persona, Cane etc.: cioé la classe Animale è una classe astratta. Supponiamo che tale classe abbia due metodi astratti mangia() e comunica() ed uno concreto dorme(), dal momento che tutti gli animali dormono allo stesso modo: public abstract class Animale { // metodo astratto per mangiare public abstract void mangia(); // metodo astratto per comunicare public abstract void comunica(); // metodo concreto per dormire public void dorme() { System.out.println(Dormo...); } } Quindi una classe astratta è definita tale con la keyword abstract: questo tipo di classe può contenere sia metodi astratti (definiti ovviamente abstract), sia metodi concreti. Ogni classe derivata da una classe astratta deve implementare i metodi astratti della classe base! Per esempio una eventuale classe Cane potrebbe avere questa forma: public class Cane extends Animale { // costruttore public Cane(String nome) { this.nome = nome; }
  • 53. 3 CLASSI E OGGETTI 53 // implementa il metodo della classe base public void comunica() { System.out.println(Sono +nome+ e faccio Bau Bau); } // implementa il metodo della classe base public void mangia() { System.out.println(Mangio con la bocca e le zampe); } // attributo private String nome; } Pertanto Cane estende la classe Animale: realizza i metodi astratti mangia() e comunica ed eredita il metodo dorme(). Analogamente una classe Persona potrebbe essere così: public class Persona extends Animale { // costruttore public Persona(String nome, String cognome) { this.nome = nome; this.cognome = cognome; } // implementa il metodo della classe base public void comunica() { System.out.println(...Salve mondo, sono +nome+ +cognome); } // implementa il metodo della classe base public void mangia() { System.out.println(Mangio con forchetta e coltello); } // estende il metodo della classe base
  • 54. 3 CLASSI E OGGETTI 54 public void dorme() { super.dorme(); System.out.println(ed in più russo!); } // aggiunge il metodo: public void faiGuerra() { System.out.println(...Sono un animale intelligente perché faccio le guerre...); } private String nome,cognome; } Questa classe implementa i metodi astratti della classe base, estende il metodo dorme() ed aggiunge il metodo faiGuerra(). Definiamo un main nella classe Applicazione: public class Applicazione { public static void main(String args[]) { Cane bill = new Cane(bill); bill.comunica(); bill.mangia(); bill.dorme(); System.out.println(n-----------------------); Persona george = new Persona(George,Fluff); george.comunica(); george.mangia(); george.dorme(); george.faiGuerra(); } }
  • 55. 3 CLASSI E OGGETTI 55 L’esecuzione del programma è la seguente: Figura 19: Esecuzione del programma Applicazione.java Nel main viene creato l’oggetto bill che è una istanza della classe Cane: su di esso viene invocato il metodo comunica() che stampa l’attributo nome (inizializzato nel costruttore) ed il verso “Bau Bau”. bill invoca quindi il metodo mangia(), che stampa una stringa che “ci spiega” come il cane riesca a mangiare. Infine viene richiamato su bill il metodo dorme(). Allo stesso modo viene creato l’oggetto george che è un’istanza di Persona: esso invoca gli stessi metodi che invoca l’oggetto bill ed in più richiama il metodo faiGuerra().
  • 56. 3 CLASSI E OGGETTI 3.8 56 Interfacce Le interfacce sono un meccanismo proprio di Java che, come vedremo nella sezione successiva, consentono di avere un supporto parziale ma sufficiente per l’ereditarietà multipla. Attraverso una interfaccia si definisce un comportamento comune a classi che fra di loro non sono in relazione. Come si è detto, anche una classe astratta definisce un contenitore di metodi per le classi derivate, quindi in questo caso si usa la relazione di ereditarietà. Quando si parla invece di interfaccia si definiscono i metodi che le classi dovranno implementare, pertanto in una interfaccia non possono esistere metodi concreti! In UML una interfaccia si disegna così: Figura 20: Diagramma UML per interface Considerimo, per esempio, i file HTML ed i file bytecode di Java: ovviamente essi non hanno nulla in comune, se non il fatto di supportare le stesse operazioni, come apri(...), chiudi(...), etc. Vediamo allora come costruire una interfaccia comune di operazioni da usare su diversi file, per aprirli, determinarne il tipo e chiuderli. Definiamo allora un interfaccia FileType: public interface FileType { // apre il file public void open(); // verifica se il tipo di file è OK public boolean fileTypeOk(); // chiude il file public void close(); } Si sta dicendo che l’interfaccia FileType (definita con la keyword interface) definisce i tre metodi open(), fileTypeOk(), close() ed ogni classe che vuole implementare questa interfaccia deve implementare i metodi in essa definiti. Supponiamo adesso di voler aprire e verificare un file bytecode di Java (per la struttura dei bytecode Java cfr. [1]):
  • 57. 3 CLASSI E OGGETTI 57 import java.io.*; public class FileClass implements FileType { /* Costruttore: inizializza il nome del file che si vuole leggere */ public FileClass(String nome) { nomeDelFile = nome; } // apre il file fisico il cui nome è nomeDelFile public void open() { try { fileClass = new DataInputStream(new FileInputStream(nomeDelFile)); } catch (FileNotFoundException fne) { System.out.println(File +nomeDelFile+ non trovato.); } } /* verifica se il file è un file bytecode di Java: legge i primi 4 byte (cioé 32 bit = int) e controlla se tale intero è il numero esadecimale 0xcafebabe, cioé è l’header del file .class */ public boolean fileTypeOk() { int cafe = 0; try { cafe = fileClass.readInt(); } catch (IOException ioe) {} if ((cafe ^ 0xCAFEBABE) == 0) return true; else return false; } // chiude il file fisico public void close() { try {
  • 58. 3 CLASSI E OGGETTI 58 fileClass.close(); } catch (IOException ioe) { System.out.println(Non posso chiudere il file); } } // file fisico private DataInputStream fileClass; // nome del file private String nomeDelFile; // main public static void main(String args[]) { if (args.length != 0) { FileClass myFile = new FileClass(args[0]); myFile.open(); if (myFile.fileTypeOk()) System.out.println(Il file +args[0]+ è un bytecode Java); else System.out.println(Il file +args[0]+ non è un file .class!); myFile.close(); } else System.out.println(uso: java FileClass nome del file); } } Provare a compilarlo e ad eseguirlo (sintassi: java FileClass “nome” dove “nome” è un nome di file .class, per esempio provare con: java FileClass FileClass.class. . . ) La classe FileClass implementa le operazioni dell’interfaccia attraverso la keyword implements. Quindi, come si vede dal codice, la classe deve implementare tutti i metodi dell’interfaccia. Il costruttore riceve come argomento il nome del file che usa per inizializzare l’attributo nomeDelFile. Il metodo open() implementa il metodo omonimo dell’interfaccia FileType: quindi tenta di aprire il file come stream di byte e se non trova il file solleva una eccezione (per i file e le eccezioni cfr. la sezione 5).
  • 59. 3 CLASSI E OGGETTI 59 Il metodo fileTypeOk verifica se il file in aperto è una bytecode Java: se l’header o intestazione comincia con il numero esadecimale 0xCAFEBABE (0x significa che ciò che segue è un numero in base 16) allora il file è un bytecode Java, altrimenti non lo è. Notare che per il test si è usato l’operatore fra bit XOR - in Java ˆ, che restituisce 0 se i bit sono uguali, 1 altrimenti. close chiude lo stream: se non lo trova (. . . magari è stato cancellato o spostato. . . ) solleva una eccezione. Il main richiama in ordine i tre metodi di cui sopra. Dal momento che un bytecode è un file di byte, si sono usate le classi di accesso ai file del package java.io. In 5 verrà discusso come accedere ai file. In UML implements si disegna così: Figura 21: Diagramma UML per implements Dunque FileClass implementa l’interfaccia FileType. Analogamente se volessimo verificare un file HTML, un ELF di Linux (file esegubile) etc., non dobbiamo far altro che scrivere delle classi che implementano le operazioni dell’interfaccia FileType. L’utilizzo delle interfacce è conveniente, almeno per due motivi: ¡ si separa l’interfaccia dall’implementazione; ¡ si ha una garanzia per non fare errori: si modifica solo l’implementazione e non l’interfaccia.
  • 60. 3 CLASSI E OGGETTI 3.9 60 Ereditarietà multipla In 2.6.5 si è parlato della ereditarietà multipla in teoria: realizziamo adesso qualche esempio pratico. Come si è detto in 2.6.5 l’ereditarietà multipla ha tre forme, di cui solo due sono supportate in Java. Vediamo come realizzare il matrimonio fra una classe concreta ed una astratta: poiché in Java ogni classe ha un solo padre, non è possibile ereditare da due o più classi contemporaneamente; sembrerebbe a prima vista che il matrimonio “non possa essere celebrato”. In realtà è possibile farlo, perché una interfaccia è una classe astratta senza metodi concreti, quindi possiamo fare un matrimonio fra una classe concreta ed una interfaccia. L’esempio della Pila della sezione 2.6.5 diventa pertanto: Figura 22: Diagramma UML per il matrimonio fra classe concreta ed interfaccia La nostra Pila dovrà avere una struttura FIFO (First In First Out) anche detta FCFS (First Come First Served) cioé il primo elemento che entra deve essere il primo elemento ad uscire (pensate ad una pila di piatti. . . ); quindi la struttura che dobbiamo implementare è questa: pop() top push(20) 10 20 5 16 4 5 Figura 23: Una Pila di numeri interi L’operazione push(...) inserisce un elemento sulla cima della pila, mentre
  • 61. 3 CLASSI E OGGETTI 61 pop() preleva l’elemento in cima. L’attributo top punta alla cima della struttura. Scriviamo allora l’interfaccia Stack: essa definisce le operazioni in astratto che verranno implementate da Pila sull’array. public interface Stack { // inserisce un oggetto ’element’ nella pila public void push(Object element); // preleva l’oggetto dalla cima della pila public Object pop(); // verifica se la pila è piena public boolean isFull(); // verifica se la pila è vuota public boolean isEmpty(); } Pila deve implementare Stack ed estendere array: poiché Java fornisce un buon supporto per gli array attraverso la classe Vector del package java.util, useremo tale classe come array: import java.util.Vector; public class Pila extends Vector implements Stack { // alloca una Pila di numElements elementi public Pila(int numElements) { super(numElements); maxElements = numElements; top = 0; } // inserisce un element nella Pila public void push(Object element) { if (!isFull()) { super.addElement(element);
  • 62. 3 CLASSI E OGGETTI 62 top++; } else System.out.println(Pila Piena!); } // preleva l’elemento in cima alla Pila public Object pop() { if (!isEmpty()) return super.remove(--top); else { System.out.println(Pila Vuota!); return null; } } // restituisce ’true’ se la Pila è vuota, ’false’ altrimenti public boolean isFull() { return (top == maxElements); } /* riscrive il metodo omonimo della superclasse : restituisce ’true’ se la Pila è vuota, ’false’ altrimenti */ public boolean isEmpty() { return (top == 0); } // puntatore alla cima della Pila private int top; // contatore del numero degli elementi della Pila private int maxElements; } Poiché Pila è un Vector, supporta tutti i metodi di tale classe, inoltre poiché implementa l’interfaccia Stack deve implementare tutti i metodi di tale interfaccia. Il costruttore richiama il costruttore della classe base Vector, setta l’attributo numElements (cioé il numero massimo di elementi che la pila può memorizzare) al valore passatogli come argomento e inizializza il top a 0 (quindi la pila è vuo-
  • 63. 3 CLASSI E OGGETTI 63 ta). I metodi isFull() ed isEmpty() restituiscono true se, rispettivamente la pila è piena (quindi top è uguale al massimo valore di elementi che la pila può supportare) e se la pila è vuota (top è uguale a 0). Il metodo push(...) inserisce un elemento passatogli come argomento in cima alla pila: se la pila è piena viene segnalato un errore. Notiamo che in realtà l’inserimento avviene tramite la chiamata al metodo addElement(...) della classe base Vector che si preoccupa di inserire l’elemento nel vettore fisico. pop è l’operazione complementare a push(...). Una applicazione d’esempio potrebbe essere la seguente: public class ProvaPila { public static void main(String args[]) { if (args.length != 0) { Pila pila = new Pila((new Integer(args[0])).intValue()); // inserisci elem. finché la pila non è piena int i = 1; while (!pila.isFull()) { pila.push(new Integer(i++)); } // preleva elem. finché la pila non è vuota while (!pila.isEmpty()) { System.out.println(elemento prelevato: +pila.pop()); } } else System.out.println(uso: java ProvaPila num_elem); } }
  • 64. 3 CLASSI E OGGETTI 64 Una possibile esecuzione è la seguente: Figura 24: Esecuzione di ProvaPila Osserviamo adesso un fatto importante: riprendiamo l’iterfaccia Stack; consideriamo i due metodi ¡ public void push(Object element); ¡ public Object pop(); Come si può vedere, push(...) prende come argomento un elemento il cui tipo è Object, mentre la funzione pop() restituisce un Object. Cosa vuol dire questo? Semplicemente che tali funzioni operano su oggetti di tipo Object: ma come si è detto nella sottosezione 3.6.4, ogni classe deriva da Object, quindi questi metodi funzionano su qualunque tipo! Pertanto la nostra Pila, che implementa l’interfaccia Stack, sarà una pila che potrà contenere elementi di qualsiasi tipo: allora potrà contenere numeri interi, numeri reali, oggetti Persona, etc. etc. Utilizzando il tipo cosmico Object (come parametro di funzione e/o tipo di ritorno di un metodo), si può simulare il polimorfismo parametrico (cfr. sezione 2.7); tuttavia, mentre in C++ (cfr. [2]) è possibile istanziare oggetti dello stesso tipo (cioé con un template di C++ si possono avere solo collezioni omogenee), in Java è possibile mixare tipi diversi (ogni classe è-un Object), quindi si possono ottenere collezioni eterogenee. Vediamo cosa significa questo fatto nel nostro caso:
  • 65. 3 CLASSI E OGGETTI 65 public class ProvaPila { public static void main(String args[]) { if (args.length != 0) { Pila pila = new Pila((new Integer(args[0])).intValue()); // inserisci elem. finché la pila non è piena int i = 1; while (!pila.isFull()) { // inserisce un intero pila.push(new Integer(i)); // inserisce un reale pila.push(new Float(i*Math.PI)); // inserisce una stringa pila.push(new String(Sono il numero: +i)); i++; } // preleva elem. finché la pila non è vuota while (!pila.isEmpty()) { System.out.println(elemento prelevato: +pila.pop()); } } else System.out.println(uso: java ProvaPila num_elem); } } Stavolta nella pila vengono inseriti rispettivamente: un numero intero, un numero reale ed una stringa: abbiamo così ottenuto una pila “universale” di oggetti. Occorre osservare che una classe può implementare più interfacce: ad esempio, se vogliamo che la nostra Pila salvi il contenuto della pila su un file, possiamo scrivere Pila così: public class Pila extends Vector implements Stack, FileStack { ... }
  • 66. 3 CLASSI E OGGETTI 66 dove Vector e Stack sono le stesse viste sopre, mentre FileStack è una interfaccia che definisce i metodi per l’accesso ai file fisici. Pertanto una classe può estendere una sola classe base ma può implementare più interfacce. La seconda forma di ereditarietà multipla è il duplice sottotipo: cioé una classe implementa due interfacce filtrate. Se abbiamo una interfaccia A ed una interfaccia B, è possibile fare questo: public interface A {...}; public interface B extends A {...}; Una interfaccia può estendere un’altra interfaccia: di più può estendere un numero illimitato di interfacce, cioé si può avere una cosa del genere: public interface X extends A1 ,A2 ,...,An {...} dove ogni Ai i 1 2 n, sono interfacce! Una classe concreta Y implementerà X: ! %$$#! ! ¦ ! public class Y implements X {...} L’ereditarietà multipla sottoforma di composizione di oggetti non è supportata (cfr. sezione 2.6.5) per questioni di sicurezza del codice e per non rendere complessa la JVM.
  • 67. 4 LE OPERAZIONI SUGLI OGGETTI 4 4.1 67 Le operazioni sugli oggetti Copia e clonazione Supponiamo di avere la seguente classe: public class A { // costruttore di default: richiama il costruttore A(int num) public A() { this(0); } /* costruttore: setta l’attributo num al valore passato come argomento */ public A(int num) { this.num = num; } // assegna un nuovo valore a num public void set(int num) { this.num = num; } // stampa num public void print() { System.out.println(num); } // attributo private int num; // main public static void main(String args[]) { A primo = new A(1453); primo.print(); A secondo = new A();
  • 68. 4 LE OPERAZIONI SUGLI OGGETTI 68 secondo.print(); secondo = primo; secondo.set(16); primo.print(); secondo.print(); } } Se eseguiamo tale programma, otteniamo il seguente output: 1453 0 16 16 Esaminiamo il main: viene creato l’oggetto primo che inizializza il membro num a 1453; quando si richiama il metodo print() sull’oggetto primo, si ottiene a video 1453. Viene poi creato l’oggetto secondo che viene costruito col costruttore di default (quindi il membro num è 0) e ed è richiamato su questo oggetto print() che stampa 0. Si esegue poi l’assegnamento secondo = primo. Si richiama poi il metodo set(...) sull’oggetto secondo, passando come argomento l’intero 16. Quando si esegue l’istruzione secondo.print(); , viene stampato a video il numero 16. Invocando print su primo, invece di ottenere il numero 1453, otteniamo 16. Che cosa è successo? Come si è detto nella sezione 3.4, la variabile oggetto è un riferimento all’oggetto, cioé essa serve per accedere alle informazioni dell’oggetto alla quale si riferisce e non per memorizzarle. Allora con l’assegnamento secondo = primo;, non si sta facendo una copia di valori, ma si sta facendo una copia di riferimenti: sia primo che secondo puntano allo stesso oggetto. In sostanza si è creato un secondo riferimento all’oggetto primo.
  • 69. 4 LE OPERAZIONI SUGLI OGGETTI 69 Quando gli oggetti primo e secondo sono stati costruiti, nello heap si ha una situazione del genere: primo A num = 1453 secondo A num = 0 Figura 25: Gli oggetti primo e secondo dopo la creazione e dopo l’assegnamento secondo = primo; si ha: primo A num = 1453 secondo A num = 0 Figura 26: Gli oggetti primo e secondo dopo l’assegnamento secondo = primo; Pertanto ogni modifica sullo stato (attributi) di un oggetto si ripercuote sullo stato dell’altro. Vogliamo evitare questa situazione: cioé vogliamo che il riferimento, dopo la copia, rimanga intatto. Per fare questo Java mette a dispozione il metodo clone() nella classe Object: quindi basterà invocare tale metodo e verrà eseguita una copia totale dell’oggetto (ricordiamo ancora una volta che ogni oggetto deriva da Object implicitamente). È necessario implementare l’interfaccia Cloneable (già definita in Java) per indicare che la clonazione dell’oggetto è possibile: il metodo clone() di Java è protected, quindi per poterlo invocare è necessario implementare Cloneable. Inoltre poiché il tipo di ritorno di questo metodo è Object, è necessario un cast nel tipo corrente dell’oggetto:
  • 70. 4 LE OPERAZIONI SUGLI OGGETTI 70 public class A implements Cloneable { // costruttore di default: richiama il costruttore A(int num) public A() { this(0); } /* costruttore: setta l’attributo num al valore passato come argomento */ public A(int num) { this.num = num; } /* implementa il metodo dell’interfaccia Cloneable: richiama il metodo clone() di Object */ public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { return null; } } // assegna un nuovo valore a num public void set(int num) { this.num = num; } // stampa num public void print() { System.out.println(num); } // attributo private int num;
  • 71. 4 LE OPERAZIONI SUGLI OGGETTI 71 // main public static void main(String args[]) { A primo = new A(1453); A secondo = new A(); secondo = (A)primo.clone(); secondo.set(16); primo.print(); secondo.print(); } } Se eseguiamo tale codice, otteniamo il seguente output: 1453 16 La situazione nello heap dopo la clonazione, cioé dopo l’istruzione secondo = (A)primo.clone(); è la seguente: primo A num = 1453 secondo A num = 1453 Figura 27: Gli oggetti primo e secondo dopo la clonazione I due oggetti hanno adesso “vite indipendenti”.
  • 72. 4 LE OPERAZIONI SUGLI OGGETTI 72 Dopo l’istruzione: secondo.set(16); La situazione è la seguente: primo A num = 1453 secondo A num = 16 Figura 28: Gli oggetti primo e secondo dopo l’istruzione secondo.set(16); Occorre notare che se una classe aggrega un’altra classe, è necessario definire il metodo clone() anche nella classe aggregata, altrimenti quest’ultima non verrebbe clonata se fosse eseguito un clone() su un oggetto dell’altra classe!
  • 73. 4 LE OPERAZIONI SUGLI OGGETTI 4.2 73 Confronto Un’altra operazione importante, che può ricorrere spesso in una applicazione vera, è il confronto di oggetti della stessa classe. Java mette a disposizione il metodo equals(...) nella classe Object che confronta due oggetti e restituisce ¡ true se i due oggetti sono identici (cioé sono lo stesso riferimento), ¡ false altrimenti Tale metodo non è molto utile se, per esempio, vogliamo confrontare due persone: in questo caso è necessario confrontare tutti gli attributi e restituire true se sono uguali, false altrimenti. Risulta allora conveniente riscrivere tale metodo: public class Persona { //Costruttore: inizializza gli attributi public Persona(String nome,String cognome,int anni) { this.nome = nome; this.cognome = cognome; this.anni = anni; } /* riscrive il metodo della classe base Object: testa se due persone sono uguali dal punto di vista degli attributi */ public boolean equals(Object object) { if (object instanceof Persona) return ((this.nome.equals(((Persona)object).nome)) (this.cognome.equals(((Persona)object).cognome)) (this.anni == ((Persona)object).anni)); else return false; } // attributi private String nome,cognome; private int anni;
  • 74. 4 LE OPERAZIONI SUGLI OGGETTI 74 // main public static void main(String args[]) { Persona pippo = new Persona(Pippo,Caio,2); Persona pluto = new Persona(Pippo,Caio,2); if (pippo.equals(pluto)) System.out.println(Sono la stessa persona); else System.out.println(Sono persone diverse); } } Il metodo public boolean equals(Object object) riscrive il metodo omonimo di Object: come si può notare, tale metodo inizia con un confronto e precisamente: if (object instanceof Persona) questo controllo è molto importante. Infatti, quando nel main viene eseguita l’istruzione pippo.equals(pluto) , al metodo equals(...) si sta passando come argomento l’oggetto pluto che è una istanza di Persona: possiamo immaginare una situazione del genere: pluto Persona nome = Pippo cognome = Caio anni = 2 Object Figura 29: Un oggetto ha sempre un riferimento implicito ad Object Poiché ogni classe deriva da Object, allora ogni oggetto è anche un riferimento ad un Object (a tempo di esecuzione del programma), quindi pluto è un oggetto di tipo Persona, ma anche di tipo Object!
  • 75. 4 LE OPERAZIONI SUGLI OGGETTI 75 Se avessimo una classe Cane (con un unico attributo nome) ed un oggetto bill, istanza di tale classe, avremmo allora: bill Cane nome = bill Object Figura 30: L’oggetto bill istanza di Cane Riguardiamo adesso il metodo equals(...) e notiamo che accetta come argomento un Object: nessuno ci vieta allora di scrivere nel main l’istruzione: pluto.equals(bill); Ma ha senso confrontare un oggetto di tipo Persona con un oggetto di tipo Cane? Ovviamente no! Abbiamo quindi bisogno di controllare a run time il tipo dinamico (cioé il tipo dell’oggetto durante l’esecuzione del programma che può essere una istanza di una qualunque classe della gerarchia di ereditarietà) dell’oggetto passato come argomento al metodo equals(...): se il tipo di tale oggetto è Persona, allora possiamo confrontare gli attributi dei due oggetti che sono sicuramente istanze di Persona, altrimenti il confronto degli attributi non ha senso (sono ovviamente due oggetti di tipo diverso). Questo controllo viene fatto con la keyword instanceof, la cui sintassi è: if (nome_dell’_oggetto instanceof Nome_della_Classe) {...} il risultato sarà true se il tipo dinamico di nome_dell’_oggetto è esattamente Nome_della_Classe , false altrimenti. Nel nostro caso (sia pippo che pluto sono oggetti Persona), tale controllo (if (object instanceof Persona)) sarà true, perché il tipo dinamico di pluto è Persona. Osserviamo però che prima e dopo questo controllo stiamo usando pippo come istanza di Object e non di Persona! Pertanto se tentiamo di accedere ad un qualsiasi attributo di Persona, otteniamo un errore a tempo di compilazione. Quindi è necessario specificare che se il controllo di instanceof andrà a buon fine durante l’esecuzione del programma, il tipo di pluto dovrà essere ripristinato a Persona: questa operazione è detta downcasting e viene eseguita con la sintassi:
  • 76. 4 LE OPERAZIONI SUGLI OGGETTI 76 (Nome_della_classe_derivata)nome_dell’_oggetto Questo tipo di conversione è pericolosa: usarla solo quando necessario e soprattutto, prima di eseguire il cast verificare il tipo dinamico dell’oggetto con instanceof. Come vedremo, questa conversione può essere spesso evitata se si ricorre al polimorfismo! 4.3 Binding dinamico Consideriamo due classi, per esempio Base da cui deriva Derivata: public class Base { // richiama stampa() public void f() { stampa(); } // stampa un messaggio public void stampa() { System.out.println(Sono la classe base); } } public class Derivata extends Base { // riscrive il metodo della classe base public void stampa() { System.out.println(Sono la classe derivata); } // stampa la stringa ciao public void g() { System.out.println(Ciao dalla classe derivata); } } Supponiamo inoltre di avere il seguente main: