(E book ita) java   introduzione alla programmazione orientata ad oggetti in java
Upcoming SlideShare
Loading in...5
×
 

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

on

  • 428 views

 

Statistics

Views

Total Views
428
Views on SlideShare
428
Embed Views
0

Actions

Likes
0
Downloads
22
Comments
0

0 Embeds 0

No embeds

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

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

  • 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:
  • 4 LE OPERAZIONI SUGLI OGGETTI 77 public class Prova { public static void main(String args[]) { Base base = new Base(); Derivata derivata = new Derivata(); base.f(); derivata.f(); derivata.g(); } } Se eseguiamo il Prova, otteniamo il seguente output: Sono la classe base Sono la classe derivata Ciao dalla classe derivata Vengono creati i due oggetti base e derivata, istanze, rispettivamente, di Base e Derivata. base invoca il metodo f(): esso richiama il metodo stampa() che stampa il messaggio: Sono la classe base. derivata richiama il metodo f() che è stato ereditato da Base: a sua volta f() invoca il metodo stampa() (riscritto nella classe derivata). Grazie al meccanismo del binding dinamico, la versione di stampa richiamata su derivata è proprio quella che appartiene alla classe Derivata. In sostanza il metodo da selezionare a run-time dipende dal tipo dinamico dell’oggetto: siccome il tipo dinamico di derivata è Derivata, viene selezionato il metodo stampa() che stampa il messaggio Sono la classe derivata. L’istruzione derivata.g() stampa a video il messaggio Ciao dalla classe derivata. Adesso modifichiamo il main nel seguente modo: public class Prova { public static void main(String args[]) { Base base = new Base(); Derivata derivata = new Derivata(); base = derivata; base.f();
  • 4 LE OPERAZIONI SUGLI OGGETTI 78 derivata.f(); } } Stavolta vengono creati i due oggetti base e derivata, e viene eseguito l’assegnamento: base = derivata; cioé viene creato un secondo reference a derivata: base Base f() stampa() metodo ereditato derivata Derivata f() stampa() g() Figura 31: base = derivata; quindi non ci meravigliamo se otteniamo, come risultato, il seguente output: Sono la classe derivata Sono la classe derivata È del tutto normale perché abbiamo creato un nuovo reference alla classe derivata, il cui tipo dinamico è Derivata che contiene il metodo stampa() che stampa il messaggio Sono la classe derivata. Adesso proviamo ad eseguire una nuova istruzione nel main: base.g(); Se proviamo a compilare, stavolta otteniamo un bel messaggio di errore, che ci informa che g() non è un metodo di Base! E anche questo fatto è del tutto normale: quando il compilatore avvia la compilazione, si rende conto che il tipo statico di base è Base, pertanto quando trova l’istruzione base.g(), cerca il metodo g() e non lo trova nella classe Base. N.B. Il binding dinamico in Java è offerto per default: se lo si vuole disabilitare, è necessario dichiarare i metodi final. Tale keyword può essere applicata anche alle classi: però ciò impedisce la derivazione, quindi è bene riflettere prima di usarla!
  • 4 LE OPERAZIONI SUGLI OGGETTI 4.4 79 Serializzazione Una operazione interessante ed importante che può essere fatta su un oggetto è la serializzazione: cioé la possibilità di salvare lo stato (cioé l’insieme dei valori degli attributi in un certo istante di tempo) di un oggetto su un file. Successivamente il contenuto dell’oggetto può essere ripristinato. Per realizzare questo meccanismo su una classe, quest’ultima deve implementare l’interfaccia Serializable del package java.io: tale interfaccia contiene i metodi void writeObject(java.io.ObjectOutputStream out) throws IOException void readObject(java.io.ObjectInputStrem in) throws IOException, ClassNotFoundExcpetion che servono, rispettivamente, per salvare ed aprire un oggetto e devono, ovviamente, essere implementate nella classe che realizza la serializzazione. Facciamo un esempio: supponiamo di voler serializzare la classe Persona: import java.io.Serializable; public class Persona implements Serializable{ // 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) { this.nome = nome; this.cognome = cognome; this.dataDiNascita = new Data(giorno,mese,anno); } /* stampa gli attributi e richiama il metodo stampaData() sull’oggetto dataDiNascita
  • 4 LE OPERAZIONI SUGLI OGGETTI 80 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 protected String nome; protected String cognome; protected Data dataDiNascita; } Allo stesso modo, la classe Data deve essere serializzata e quindi deve implementare l’interfaccia Serializable: import java.io.Serializable; public class Data implements Serializable { /* 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; }
  • 4 LE OPERAZIONI SUGLI OGGETTI 81 A questo punto possiamo definire i due metodi che realizzano il salvataggio ed il ripristino degli oggetti: import java.io.*; public class Serial { // Scrive l’oggetto sul file "persona.dat" public void writeFile() { Persona pippo = new Persona("Tizio","Caio",1,1,2003); try{ FileOutputStream file = new FileOutputStream(fileName); ObjectOutputStream obj = new ObjectOutputStream(file); obj.writeObject(pippo); obj.close(); } catch (IOException ioe) {System.out.println(ioe);} } // legge l’oggetto dal file "persona.dat" e lo stampa a video public void readFile() { Persona pippo = null; try { FileInputStream file = new FileInputStream(fileName); ObjectInputStream obj = new ObjectInputStream(file); pippo = (Persona)obj.readObject(); obj.close(); pippo.stampaDati(); } catch (Exception e) {System.out.println(e);} } // attributo: nome del file in cui memorizzare gli oggetti private String fileName = "persona.dat"; public static void main(String args[]) { Serial mySerialization = new Serial(); mySerialization.writeFile(); mySerialization.readFile(); } }
  • 4 LE OPERAZIONI SUGLI OGGETTI 82 Il metodo writeFile() crea un oggetto pippo di tipo Persona, e prova ad aprire il file persona.dat: se la classe Persona, così come Data, non avessero implementato l’interfaccia Serializable, il blocco try {...} catch {...} avrebbe sollevato una eccezione a run time. Viene quindi aperto uno stream di output (vedere la sezione 5) di nome persona.dat, su cui vogliamo inviare le informazioni dell’oggetto (istruzione ObjectOutputStream obj = new ObjectOutputStream(file); ). La serializzazione dell’oggetto viene eseguita con l’istruzione obj.writeObject(pippo); il file viene quindi chiuso. Simmetricamente, readFile() apre il file persona.dat come stream di input e legge l’oggetto: notare il cast forzato in Persona, poiché il tipo restituito da readObject() è Object. Sull’oggetto appena letto viene invocato il metodo stampaDati();. Eseguendo il programma, otteniamo il seguente output: Nome: Tizio Cognome: Caio Nato il: 1/1/2003 Provare a modificare il programma, in modo da creare un array che contiene oggetti di tipo Persona e Studente (derivato da Persona) ed eseguire la serializzazione.
  • 83 Parte III APPENDICI 5 5.1 Una panoramica sul linguaggio Tipi primitivi Java fornisce i seguenti tipi primitivi: 28 1] 216 1] 232 1] 264 1] 232 1] 264 1] ( ( ( ( ( ' true o false n interi nell’intervallo [-28 n ri interi nell’invervallo [-216 n ri interi nell’invervallo [-232 n ri interi nell’invervallo [-264 n ri reali nell’invervallo [-232 n ri reali nell’invervallo [-264 caratteri ri ' ' & & & ' ' ' & & & boolean byte short int long float double char Valore ( Tipo Lunghezza (byte) 1 1 2 4 8 4 8 2 Tabella 2: Tipi primitivi di Java La memorizzazione dei tipi è fissa: cioé su ogni tipo di macchina la dimensione è quella riportata sull’ultima colonna della tabella 2. Il tipo boolean assume solo i valori di verità true e false: non possono essere usati i valori 1 e 0, come in C/C++! I tipi numerici sono tutti con segno! Il tipo char memorizza i caratteri con la codifica Unicode su 2 byte. Java supporta i seguenti caratteri speciali: ) b backspace; ) t tabulazione; ) n newline; ) r carriage return; ) " virgole doppie; ) ’ apice;
  • 5 UNA PANORAMICA SUL LINGUAGGIO 84 backslash. ) 0) Gli array si dichiarano con la seguente sintassi: String vect[] = new String[10]; Si sta dichiarando un array di stringhe di 10 elementi. Le stringhe in Java non sono un tipo primitivo: esiste la classe String preposta alla loro gestione. Quindi le stringhe sono oggetti e come tali devono essere costruiti (cfr. sezione 3.4). 5.2 Variabili Le variabili sono dichiarate elencando il tipo, il nome ed, eventualmente, un valore iniziale; le costanti hanno il prefisso final: char carattere = ’C’; char nuovaLinea = ’n’; int numero; final float pi = 3.14; boolean vero = true; Il simbolo tt= è chiamato operatore di assegnamento, perché assegna un valore (che può essere anche un’altra variabile) ad una variabile. 5.3 5.3.1 Operatori Operatori Aritmetici Sono supportate ovviamente le 4 operazioni fondamentali sui numeri: +, -, *, /. Il resto di una divisione si ottiene con %. Le operazioni quali radice quadrata, elevamento a potenza, coseno, etc. sono disponibili, come metodi statici nel package java.lang.Math. Gli operatori di incremento ed decremento sono e . Esempi: ( 3( 1 21 int a = 1, b = 3; int c = a * b; int c = ++a; /* c vale 2 (++a incrementa prima a di 1 e poi esegue l’assegnamento) */ c--; // decrementa c di 1 /* */ servono per commenti su più linee, mentre // è usato per commenti su una singola linea.
  • 5 UNA PANORAMICA SUL LINGUAGGIO 5.3.2 Operatori relazionali I valori delle variabili possono essere confrontati con gli operatori: == uguale; != diverso; < minore stretto; > maggiore stretto; <= minore o uguale; >= maggiore o uguale. 5.3.3 Operatori booleani Per testare più valori booleani vengono usati gli operatori: && and; || or; ! not. Le condizioni possono essere combinate con questi operatori. 5.3.4 Operatori su bit Tali operatori sono: & and logico; | or logico; 4 xor; not; 5 « shift a dx.; » shift a sx. 85
  • 5 UNA PANORAMICA SUL LINGUAGGIO 5.4 86 Blocchi I blocchi di istruzioni sono delimitati da { e }. Ogni variabile dichiarata in un blocco deve essere inizializzata. È possibile dichiarare le variabili in ogni punto del blocco. 5.5 Controllo del flusso Il controllo condizionale avviene con l’istruzione: if (condizione) {...} else {...}; Se il blocco è composto da una sola istruzione si possono omettere i delimitatori { e }. Esempio: if ((a > 1) && (b == 2)) a--; else { b++; } Controlla se a è maggiore di 1 e b è uguale a 2: in caso affermativo decrementa a di 1, altrimenti incrementa b di 1. N.B. Il controllo viene eseguito in corto circuito: se a è minore o uguale di 1, il controllo su b non viene neanche eseguito! I cicli iterativi (loop, di cui non si conosce il numero di volte che dorvranno essere eseguiti, si possono fare in 2 modi: while (condizione) {...} oppure do {...} while (condizione); Nel primo modo, si esegue subito il test sulla condizione e si itera il blocco di istruzioni finché la condizione è valutata true. Nel secondo modo, si esegue prima il blocco di istruzioni e poi si fa il test sulla condizione: la differenza, quindi, fra i due modi è che nella seconda forma il blocco di istruzioni viene eseguito almeno 1 volta. Per le iterazioni finite si usa il ciclo for:
  • 5 UNA PANORAMICA SUL LINGUAGGIO 87 int somma = 0; for (int i=0; i < 10; i++) somma += i; Questo blocco esegue 10 volte l’istruzione somma += i; che assegna a somma il suo vecchio valore più i: tale ciclo calcola la somma dei numeri che vanno da 0 a 9. Notare che il ciclo parte da i=0 e finisce a i<10. Inoltre è possibile dichiarare ed inizializzare una variabile nel ciclo, come si può vedere. Tale ciclo può essere trasformato in un while: int somma = 0; int i = 0; while (i < 10) somma += i; i++; Le selezioni multiple possono essere fatte tramite switch (evitando di avere degli if ... else ... a cascata: switch (condizione) { case ... : ...; break; case ... : ...; break; . . . // la seguente istruzione è opzionale default : ...; break; } break; è necessario per interrompere i controlli, quando la condizione è stata verificata. Esempio: switch (a) { case 1: b++; break; case 2: b--; break; case 3: b*2; break; case 4: b+=3; break; } 5.6 Operazioni (Metodi) Le operazioni si dichiarano nel seguente modo: tipo_di_ritorno nome_della_funzione(argomenti) { ... }
  • 5 UNA PANORAMICA SUL LINGUAGGIO 88 Il tipo_di_ritorno può essere un tipo primitivo e non (classe) o void: se il tipo_di_ritorno non è void, nel blocco si deve usare, come ultima istruzione, return. Gli argomenti possono essere sia tipi primitivi che classi: nel caso si abbiano più argomenti, essi devono essere separati da una virgola. Il nome_della_funzione non può essere una keyword di Java (class, super, this, etc.), né un numero, nè un segno di punteggiatura. Esempio: public int f(int a, int b) { int temp = 0; temp = a + b; temp %= 2; return temp; } Per la discussione sul modificatore di accesso public vedere la sezione 3.2. Questa funzione riceve in ingresso due interi a e b, ne fa la somma, calcola il modulo della divisione per 2 di tale somma e restituisce il risultato. N.B. I parametri in Java sono passati by value (per valore), ciò significa che la funzione chiamante riceve il valore delle variabili, facendone una copia temporanea, e non la locazione di memoria (riferimento) in cui tali variabili si trovano; questo vuol dire che una funzione non può modificare il contenuto dei suoi parametri: f(...) temp = 0 a copia di a copia di b b f(...) temp = (a + b) % 2 Figura 32: Passaggio dei parametri ad una funzione
  • 5 UNA PANORAMICA SUL LINGUAGGIO 5.6.1 89 Il main Ogni programma deve avere un punto di inizio: in Java un programma inizia e finisce nel main, cioé in una funzione particolare che identifica il ciclo di vita della applicazione che scriviamo. Il main di una applicazione deve risiedere all’interno di una sola classe: public class Prova { public static void main(String args[]) { System.out.println(’’Ciao’’); } } La semantica del main è discussa nella sezione 3. Questo programmino stampa la stringa “Ciao” a video. 5.6.2 I package In Java si possono mettere le classi che hanno qualcosa in comune nei package: tutte le classi di una applicazione possono essere raggruppate in una “raccolta”. Tale linguaggio mette a disposizione molti package: per esempio in java.lang sono definite le classi come String, System, Integer etc., in java.util si trovano le classi di utilità come Vector, Date, etc. etc. Per definire un package si usa la keyword package (Sun consiglia di usare i domini Internet invertiti, essendo unici), p.e: package com.ciao; public class A { ... } Si sta definendo il package com.ciao: è necessario creare la directory com che contiene la sottodirectory ciao e tutte le classi di tale package devono essere posti in quest’ultima directory. Per usare importare un package in un’altra classe si usa la keyword import: import com.ciao.A; ...
  • 5 UNA PANORAMICA SUL LINGUAGGIO 90 importa, nella classe corrente, la classe A del package com.ciao. Se si vogliono importare tutte le classi di un package si usa la sintassi: import com.ciao.*; ... 5.6.3 Gli stream Java mette a disposione ben 60 classi per gli stream, cioé i flussi di byte, i cui usi possono andare dalla gestione dei file alla gestione dei byte di una rete. Ovviamente non si discuteranno tutti gli stream: consultare la documentazione delle API del JDK, reperibile in [1] o il reference [4]. A titolo di esempio vediamo come leggere e scrivere un file di testo. Per la lettura di un file di testo si usa la classe BufferedReader del package java.io: try { BufferedReader file = new BufferedReader(new FileReader(‘‘pippo.txt’’)); } catch (IOException readExcp) { System.out.println(‘‘File non trovato’’); } è necessario cautelare le operazioni sui file in un blocco try {}...catch {}! A questo punto è sufficiente un ciclo per scorrere lo stream col metodo readLine(): String line; while ((line = file.readLine()) != null) { System.out.println(line); } Per la scrittura di un file di testo si usa la classe PrintWriter: try { PrintWriter file2 = new PrintWriter(new FileWriter(‘‘pluto.txt’’)); } catch (IOException writeExcp) { System.out.println(‘‘Non è stato possibile scrivere il file’’); } e per scriverlo si usano i metodi print(...) o println(...), a seconda se si voglia andare o meno su una nuova linea:
  • 5 UNA PANORAMICA SUL LINGUAGGIO 91 for (int i=0; i<10; i++) file2.println(i); Scrive sul file file2 i numeri 0,1,. . . ,9. 5.6.4 L’I/O a linea di comando La scrittura di output è semplice, infatti basta ricorrere alla classe System di java.lang (tale package è importato automaticamente dal compilatore): System.out.println(‘‘...’’); Per leggere le informazioni dalla console è necessario usare gli stream (cfr. [1]): nella trattazione si usa un metodo più semplice, cioé si legge l’input passato al programma a linea di comando. Supponendo che il programma X necessiti di input da tastiera, si usa: java X ciao,20,b Si stanno passando ad X i parametri ciao, 20, b che verranno elaborati dal main. 5.6.5 Le eccezioni Le eccezioni sono un meccanismo utile per evitare che il programma si arresti subito dopo l’esecuzione di una istruzione non valida. Il codice da salvaguardare da eventuali errori a run time deve essere posto in un blocco try {...} catch {...}. Eseguendo il seguente programma: public class Div { // calcola a/b e stampa il risultato void f(int a, int b) { int ris; ris = a/b; System.out.println("Il risultato è: "+ris); } // main public static void main(String args[]) {
  • 5 UNA PANORAMICA SUL LINGUAGGIO 92 Div m = new Div(); m.f(2,0); } } otteniamo il seguente output: Exception in thread "main" java.lang.ArithmeticException: / by zero at Div.f(Div.java:6) at Div.main(Div.java:14) Cioé la JVM ci informa che è stata eseguita una divisione per zero. Cauteliamo allora l’istruzione di divisione in un blocco try {...} catch {...}: public class Div { void f(int a, int b) { try { int ris = a / b; System.out.println("Il risultato è: "+ris); } catch (Exception e) { System.out.println("Divisione per 0."); } } public static void main(String args[]) { Div m = new Div(); m.f(2,0); } } Stavolta si ha come output: Divisione per 0. Occorre osservare che l’esecuzione di un blocco try {...} catch {...} è molto più lento di un if {...} else {...}, quindi è bene usare le eccezioni in situazioni in cui non è possibile usare un if, come p.e. nella gestione dei file.
  • 5 UNA PANORAMICA SUL LINGUAGGIO 5.6.6 93 Installazione del JDK Il JDK, cioé l’ambiente di sviluppo per i programmi Java può essere scaricato dal sito ufficiale di Sun Microsystem, all’URL: http://java.sun.com dalla sezione Download. La versione attuale del JDK è la 1.4.0. Una volta scaricato ed installato (è consigliabile installarlo su Linux nella directory /usr/local), è necessario settare le variabili di ambiente PATH, CLASSPATH e JAVA_HOME. Su Linux tali variabili vanno settate nel file di configurazione profile che si trova nella directory /etc; ovviamente è necessario essere utente root per poter scrivere tale file. È sufficiente aggiungere queste linee: export CLASSPATH=.:/usr/local/j2sdk1.4.0_01 export JAVA_HOME=/usr/local/j2sdk1.4.0_01 export PATH=$PATH:/usr/local/j2sdk1.4.0_01/bin A questo punto per poter compilare un programma Java è sufficiente scrivere il comando javac seguito dal nome del file. Per esempio, se abbiamo il file Ciao.java, per generare il bytecode basta scrivere a linea di comando: javac Ciao.java otteniamo così il file Ciao.class; per eseguirlo si lancia la JVM: java Ciao Notare che stavolta il nome del file Ciao è senza estensione.
  • 6 LA LICENZA GNU GPL 6 94 La licenza GNU GPL GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software–to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation’s software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author’s protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors’ reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this,
  • 6 LA LICENZA GNU GPL 95 we have made it clear that any patent must be licensed for everyone’s free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program’s source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright
  • 6 LA LICENZA GNU GPL 96 notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the
  • 6 LA LICENZA GNU GPL 97 major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients’ exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution
  • 6 LA LICENZA GNU GPL 98 system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM
  • 6 LA LICENZA GNU GPL 99 PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
  • 6 LA LICENZA GNU GPL 100 <one line to give the program’s name and a brief idea of what it does.> Copyright (C) <year> <name of author> This program is free software; 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 program 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 program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type ‘show w’. This is free software, and you are welcome to redistribute it under certain conditions; type ‘show c’ for details. The hypothetical commands ‘show w’ and ‘show c’ should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than ‘show w’ and ‘show c’; they could even be mouse-clicks or menu items–whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program ‘Gnomovision’ (which makes passes at compilers) written by James Hacker. <signature of Ty Coon>, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License.
  • Indice analitico accesso protetto, 67 ADT, 11 aggregazione, 16, 39 nella aggregazione, 16 nella relazione d’uso, 15 nella specializzazione, 18 interfacce, 64 interface, 64 binding dinamico, 26, 74 blocchi di istruzione, 84 JDK, 91 JVM, 6, 30 classe astratta, 20 base, 16 definizione, 13 derivata, 16 in Java, 27 clonazione, 67 collezioni eterogenee, 62 composizione, 21 confronto, 71 copia, 65 costruttore, 30 licenza GPL, 92 line di comando, 89 main, 30 matrimonio, 21 memoria dinamica, 30 metodi astratti, 20 modificatore di accesso, 28 oggetto costruzione in Java, 30 definizione, 14 operatori, 82 aritmetici, 82 booleani, 83 relazionali, 83 su bit, 83 operazioni, 11 astratte, 20 statiche, 29 overloading, 26 overriding, 17 dati, 11 downcasting, 73 duplice sottotipo, 21 eccezioni, 89 ereditarietà, 16, 42 per il riuso del codice, 17 ereditarietà multipla, 21, 58 flusso controllo del, 84 Garbage Collector, 9, 30 gerarchia di ereditarietà, 19 package, 87 passaggio parametri by value, 66 polimorfismo ad hoc, 26 parametrico, 26 heap, 30 incapsulamento, 13 in Java, 27 negli oggetti, 14 101
  • INDICE ANALITICO puro, 25 polimorfismo parametrico simulazione, 62 progettazione, 13 relazioni, 15 aggregazione, 16 ereditarietà, 16 ereditarietà multipla, 21 relazione “è un” in Java, 42 relazione “ha un” in Java, 39 relazione “usa” in Java, 35 uso, 15 riferimento, 30 serializzazione, 77 simulazione della ereditarietà, 22 sottoclasse, 16 sottotipo, 16 superclasse, 16 this, 33 tipi primitivi, 81 tipo, 10, 11 tipo a run-time, 26 UML, 13 aggregazione, 16 ereditarietà, 17 uso, 15 uso, 15, 35 variabile locale, 37 102
  • RIFERIMENTI BIBLIOGRAFICI 103 Riferimenti bibliografici [1] Sun Microsystem, http://java.sun.com [2] Ira Pohl, Object-Oriented Programming Using C++, The Benjamin/Cummings Publishing Company, Inc., 1993 [3] Cay Horstmann - Gary Cornell, Java 2: i fondamenti, McGraw-Hill - Sun Microsystems Inc., 1999 [4] Cay Horstmann, Practical Object Oriented Development in C++ and Java, Wiley Computer Publishing, 1997