Scarica Dal linguaggio C al linguaggio Java e più Dispense in PDF di Programmazione Java solo su Docsity!
Dal linguaggio C al linguaggio Java
(Terza parte)
Riccardo Silvestri
Sommario della terza parte
Estendere e condividere
Ereditarietà e polimorfismo Superclasse/sottoclasse Costruttori e la parola chiave super Ridefinire (override)
Polimorfismo Sottotipi e array
Esercizi Errori_tipi Titoli_incorniciati Titoli_verticali
Gerarchie di classi
Esercizi Errori_prodotti Estensione_prodotti Estensione_prodotti+ File_system Regioni Figure_geometriche
Biblioteca
La classe Object L'operatore instanceof Il metodo equals() Il metodo toString()
Esercizi Errori_O_1 Errori_O_2 Errori_O_3 Titoli_Object Prodotti_Object Stampa_array
Stampa_multiarray
Classi astratte La prima classe - versione 3 Il Template design pattern
Esercizi Piramidi_obligue Cornici Ellissi Strisce_verticali Collisione2 Menu File_app
Interfacce Ereditarietà multipla Liste
Esercizi Liste_ordinate Ricerca_prefissi Insiemi_di_interi Insiemi_di_oggetti Sequenze Code_stringhe
Code_oggetti Pile_oggetti Parole_connesse Cammini_di_parole Cammini_di_parole+
Estendere e condividere
A volte accade che una classe implementi delle funzionalità che vorremmo poter riusare in un'altra
classe. Ma non vorremmo dover implementare di nuovo queste funzionalità nella nuova classe. Si pensi,
ad esempio, a una classe Persona che permette di gestire tramite opportuni metodi le generalità di una
persona (nome, cognome, data di nascita), l'indirizzo e magari altro ancora. Dovendo definire una o più
classi per gestire i dati relativi ai dipendenti di una azienda sarebbe conveniente poter riusare la classe
Persona. Un dipendente è anche una persona e i dati gestiti dalla classe Persona devono essere gestiti
dall'archivio. Si potrebbe definire una classe Dipendente che relativamente ai dati in comune con quelli
gestiti dalla classe Persona contiene una copia della corrispondente implementazione e interfaccia
(campi e metodi). Ma se non abbiamo il codice sorgente della classe Persona? E anche se avessimo il
codice sorgente, nella maggior parte delle situazioni, non è conveniente replicare il codice.
Sempre continuando con il nostro esempio dell'archivio dei dipendenti, sicuramente avremo bisogno di
gestire i dati di dipendenti che rivestono ruoli diversi. Ad esempio, potrebbero esserci dei dipendenti nel
ruolo di dirigenti. In relazione ad un dirigente si dovranno gestire delle informazioni ulteriori, ad
esempio, la denominazione del reparto diretto, eventuali responsabilità di progetto, ecc. Allora diventa
naturale definire una nuova classe chiamata appunto Dirigente. Però un dirigente è anche un dipendente
e così tutti i dati gestiti dalla classe Dipendente dovranno essere gestiti anche dalla classe Dirigente.
Cosa facciamo? Replichiamo il codice della classe Dipendente nella classe Dirigente? Chiaramente
questa non è la soluzione ottimale.
Per fortuna Java, al pari di quasi tutti i lunguaggi orientati agli oggetti, fornisce un meccanismo che
permette di definire una nuova classe estendendo una classe esistente. La nuova classe, così definita,
eredita tutti i campi e i metodi (accessibili) della classe originale senza bisogno di replicarne il codice.
Quindi la classe Dirigente può essere definita come una estensione della classe Dipendente (e la classe
Dipendente può, a sua volta, estendere la classe Persona). La classe Dirigente necessiterà solamente
dell'implementazione dei campi e metodi che servono per gestire i dati che sono di esclusiva pertinenza
di un dirigente ma non di un dipendente generico. Così la classe Dirigente eredita, e quindi condivide ,
l'implementazione e l'interfaccia della classe Dipendente. È anche vero che la classe Dirigente estende
la classe Dipendente perchè definisce metodi e campi che la classe Dipendente non possiede. Inoltre, il
tipo della classe che estende un'altra classe diventa un sottotipo di quest'ultima. Così il tipo della classe
Dirigente diventa un sottotipo della classe Dipendente. Questo significa che ovunque si può usare un
oggetto di tipo Dipendente si può anche usare un oggetto di tipo Dirigente.
Una classe che ne estende un'altra non solo eredita l'interfaccia e l'implementazione ma può anche
modificare il comportamento dei metodi che eredita. Se, ad esempio, la classe Dipendente ha un metodo
salario() che ritorna appunto il salario del dipendente, la classe Dirigente può ridefinire il metodo in
modo che ritorni il salario del dirigente, mantenendo la stessa interfaccia. Questo, insieme al fatto che il
tipo della classe Dirigente è trattato come un sottotipo della classe Dipendente, è un esempio di ciò
che viene chiamato polimorfismo (sarà spiegato tra poco).
Ereditarietà e polimorfismo
La sintassi di Java per definire una classe che ne estende un'altra è molto semplice e consiste nell'uso
della parola chiave extends nell'intestazione della classe. Consideriamo come primo esempio una classe
Title che serve a rappresentare dei titoli, cioè, delle stringhe che possono venir stampate in una varietà
di modi. Per rendere l'esempio semplice ci limiteremo a definire la classe così che permetta di stampare
la stringa variando solamente la spaziatura tra i caratteri.
import static java.lang.System.; public class Title { private String title; private int spacing; public Title(String t) { title = t; spacing = 0; // per default lo spazio tra i caratteri è zero } public void setSpacing( int space) { spacing = space; } public int printLength() { // ritorna il numero di caratteri stampati return title.length()(1 + spacing) - spacing; } public void print() { // stampa il titolo int nc = title.length(); for ( int i = 0 ; i < nc ; i++) { out.print(title.charAt(i)); if (i < nc - 1) for (int j = 0 ; j < spacing ; j++) out.print(' '); } } }
Consideriamo ora una classe AlignTitle che estende la classe Title aggiungendo la possibilità di
rappresentare titoli con allineamento a sinistra, a destra o centrale rispetto ad un campo di lunghezza
specificata. In questo e nei prossimi esempi supporremo che la definizione della classe sia sempre
preceduta dalla direttiva import static java.lang.System.*;.
public class AlignTitle extends Title { public static final int LEFT = 0, CENTER = 1, RIGHT = 2; private int alignment, fSize;
definito dalla sottoclasse è considerato un sottotipo ( subtype ) del tipo della superclasse. Ciò significa che
ovunque si può usare un oggetto della superclasse si può anche usare un oggetto della sottoclasse. Nel
nostro esempio, il tipo TitleAlign è un sottotipo di Title e ovunque si può usare un oggetto di tipo
Title si può anche usare un oggetto di tipo AlignTitle. Ad esempio, se un metodo richiede come
parametro un oggetto di tipo Title allora gli si può passare anche un oggetto di tipo AlignTitle (il
viceversa non è possibile perchè, ad esempio, il metodo setAlign() appartiene all'interfaccia del
sottotipo AlignTitle ma non appartiene all'interfaccia del supertipo Title). Questa caratteristica è detta
polimorfismo. Un oggetto di tipo AlignTitle può assumere diverse "forme" potendo essere usato sia
come un oggetto AlignTitle sia come un oggetto Title. Il seguente semplice programma usa le classi
Title e AlignTitle e mostra il polimorfismo degli oggetti di tipo AlignTitle.
public class Test { public static void stampaTitoli(Title[] array) { for ( int i = 0 ; i < array.length ; i++) { array[i].print(); out.println(); } out.println(); } public static void main(String[] args) { Title t = new Title("Classe Base"); AlignTitle ta = new AlignTitle("Ereditarietà", AlignTitle.RIGHT, 50); Title[] array = new Title[3]; array[0] = t; array[1] = ta; array[2] = new AlignTitle("Polimorfismo", AlignTitle.CENTER, 50); stampaTitoli(array); for ( int i = 0 ; i < array.length ; i++) array[i].setSpacing(1); stampaTitoli(array); ta.setAlign(AlignTitle.CENTER); ((AlignTitle)array[2]).setAlign(AlignTitle.RIGHT); stampaTitoli(array); } }
Si noti come è possibile creare un oggetto di tipo AlignTitle e assegnarlo ad una variabile di tipo Title
(senza un cast esplicito). Il metodo stampaTitoli() stampa un array di oggetti di tipo Title però alcuni
di questi sono di tipo AlignTitle. Quando il metodo print() è invocato, l'implementazione che deve
essere usata (quella della classe Title o quella della classe AlignTitle) è determinata in base
all'identità (il riferimento) dell'oggetto su cui è invocato il metodo. L'identità di un oggetto contiene
infatti anche il nome della classe a cui l'oggetto appartiene. La tecnica che permette di determinare
l'implementazione di un metodo durante l'esecuzione è detta dynamic binding (cioè, legame dinamico).
Questa è in contrapposizione allo static binding (legame statico) che invece determina l'implementazione
staticamente, al momento della compilazione. Chiaramente, il polimorfismo richiede necessariamente
l'uso del dynamic binding. Si osservi che quando un oggetto del sottotipo AlignTitle è mantenuto in
una variabile del supertipo Title si può usare un cast per invocare su di esso metodi che appartengono
alla sottoclasse ma non alla superclasse (ad esempio il metodo setAlign()).
Già da questo semplicissimo esempio si può notare come il polimorfismo aiuti a trattare in modo
uniforme oggetti di tipo diverso. Il metodo stampaTitoli() tratta senza distinzioni oggetti di tipo Title
e di tipo AlignTitle senza però sacrificarne le differenze. Infatti la stampa degli oggetti, grazie al
polimorfismo, usa automaticamente per ogni oggetto l'implementazione appropriata. Ed ecco, infine, il
risultato dell'esecuzione del programma:
Classe Base Ereditarietà Polimorfismo C l a s s e B a s e E r e d i t a r i e t à P o l i m o r f i s m o C l a s s e B a s e E r e d i t a r i e t à P o l i m o r f i s m o
Sottotipi e array In questo primo esempio abbiamo visto una classe (AlignTitle) che ne estende
un'altra (Title). Dovrebbe essere chiaro che la classe che estende può a sua volta essere estesa da una
ulteriore classe. Invero, non c'è nessun limite sul numero di classi che possono esserci in una catena in
cui ogni classe estende quella che la precede. Consideriamo, ad esempio, la seguente catena con tre
classi:
class A { ... } class B extends A { ... } class C extends B { ... }
Quindi la classe C estende la classe B che a sua volta estende la classe A. La terminologia delle
superclassi/sottoclassi è usata anche in relazione a classi che non sono direttamente l'una l'estensione
dell'altra. Così si dice che C è una sottoclasse di A (oltre a essere una sottoclasse di B) e che A è una
superclasse di C (oltre a essere anche una superclasse di B). L'importante concetto di sottotipo si applica
parimenti a tutte le sottoclassi dirette o indirette di una classe. Quindi C è, al pari di B, un sottotipo di A.
Ovunque si può usare un oggetto di tipo A si può anche usare un oggetto di tipo C.
La relazione di sottotipo si estende anche agli array. Se Type è un qualsiasi tipo e Subtype è un
sottotipo di Type allora Subtype [] è un sottotipo di Type []. Così B[] e C[] sono sottotipi di A[] e C[]
è un sottotipo di B[]. Ecco alcuni esempi:
A[] arrayA; B[] arrayB; C[] arrayC = new C[10]; arrayA = arrayC; // OK perché C[] è un sottotipo di A[] arrayB = arrayC; // OK perché C[] è un sottotipo di B[] arrayB = arrayA; // ERRORE (in compilazione) A[] non è un sottotipo di B[] arrayA = (B[])arrayC; // OK C[] è un sottotipo di B[] che è un sottotipo di A[]
La relazione di sottotipo si estende naturalmente anche agli array di array:
A[][] matrixA; B[][] matrixB; C[][] matrixC = new C[5][10]; matrixA = matrixC; // OK C[][] è un sottotipo di A[][] matrixB = matrixC; // OK C[][] è un sottotipo di B[][] matrixB = matrixA; // ERRORE (in compilazione) A[][] non è un sottotipo di B[][] matrixA = (B[][])matrixC; // OK C[][] è un sottotipo di B[][] che è un sottotipo di A[][]
Si noti che il fatto che C[][] è un sottotipo di A[][] deriva da: C è un sottotipo di A e questo implica che
C[] è un sottotipo di A[] che a sua volta implica che C[][] è un sottotipo di A[][].
Essenzialmente la relazione di sottotipo (tra tipi classe o tipi array) corrisponde alle conversioni cast
che sono corrette in compilazione (cioè, il compilatore non segnala alcun errore). Più precisamente se
Type1 e Type2 sono due tipi classe o array e var è una variabile di tipo Type2 , allora la conversione cast
( Type1 )var è corretta in compilazione se e solo se Type2 è un sottotipo di Type.
Esercizi
[Errori_tipi] Il seguente programma contiene 4 errori (uno di questi si verifica solamente in
esecuzione), trovarli e spiegarli.
class Point { public final double x, y; public Point( double x, double y) { this .x = x; this .y = y; } } class LPoint extends Point { public final String label; public LPoint( double x, double y, String l) {
| Frigorifero | | Televisore |
Come si vede le relazioni di estensione tra classi producono una gerarchia di classi. Le classi che si
trovano più in alto sono quelle più generali e via via che si scende si trovano classi sempre più
specializzate. La relazione di estensione può quindi anche essere vista come una relazione di
specializzazione. Ad esempio, la classe Frigorifero specializza AppElettr che a sua volta specializza
Prodotto. Ovviamente, le relazioni possono anche essere viste nel verso opposto e quindi, ad esempio,
la classe Prodotto generalizza AppElettr che a sua volta generalizza sia Frigorifero che Televisore.
Iniziamo col definire la classe base Prodotto.
public class Prodotto { private float prezzo; private String produttore; public Prodotto( float p, String prod) { prezzo = p; produttore = prod; } public String getProduttore() { return produttore; } public float prezzoAlConsumo() { return prezzo; } public void stampa() { out.println(" Produttore: "+produttore); out.println(" Prezzo: "+prezzo+" euro"); } }
Il metodo stampa() produce una stampa degli attributi del prodotto. Questo comportamento deve essere
rispettato da tutte le classi nella gerarchia che essendo più specializzate hanno attributi aggiuntivi e
dovranno quindi necessariamente ridefinire il metodo. Passiamo ora alla definizione della classe
Abbigliamento che deriva direttamente da Prodotto.
public class Abbigliamento extends Prodotto { private String categoria; // il tipo del capo (camicia, pantaloni, ecc.) private int taglia; private String colore = null ; public Abbigliamento( float p, String prod, String cat, int t) { super (p, prod); // costruttore della classe Prodotto categoria = cat; taglia = t; } public void setColore(String c) { colore = c; } // ridefinisce il metodo della classe Prodotto public void stampa() { out.println(categoria); super .stampa(); // invoca il metodo della classe Prodotto out.println(" Taglia: "+taglia); if (colore != null ) out.println(" Colore: "+colore); } }
Si noti come il costruttore estenda il costruttore della classe Prodotto aggiungendo altri attributi e come
il costruttore della superclasse è invocato. Il metodo stampa() è ridefinito per stampare anche gli
attributi propri dei capi di abbigliamento. Il metodo della superclasse è però comunque invocato
(super.stampa()) per la stampa degli attributi comuni. Passiamo ora alla definizione della classe
AppElettr che deriva anch'essa direttamente da Prodotto.
public class AppElettr extends Prodotto { private float contributoRAEE; // per il riciclo degli app. elettrici e elettronici private String modello; private int consumoWatt = 0; public AppElettr( float p, String prod, float raee, String mod) { super (p, prod); // costruttore della classe Prodotto contributoRAEE = raee; modello = mod; } // ridefinisce il metodo della classe Prodotto public float prezzoAlConsumo() { return super .prezzoAlConsumo() + contributoRAEE; } public String getModello() { return modello; } public void setConsumoWatt( int watt) { consumoWatt = watt; } // ridefinisce il metodo della classe Prodotto public void stampa() { out.println(" Produttore: "+getProduttore()); out.println(" Prezzo (compreso contributo RAEE): "+prezzoAlConsumo()+" euro"); if (consumoWatt > 0) out.println(" Consumo: "+consumoWatt+" watt"); } }
La classe AppElettr ridefinisce il metodo prezzoAlConsumo() perchè deve aggiungere il contributo
RAEE (Riciclo Apparecchi Elettrici e Elettronici) che è comune a tutti i prodotti di questa classe
(l'importo però varia a seconda della tipologia). Si noti come anche in questo caso si usa la parola chiave
super per accedere al metodo della superclasse. Il metodo stampa() è ridefinito senza poter sfruttare il
metodo della classe Prodotto perché la dicitura dell'attributo prezzo è differente. Inoltre, si noti che
l'attributo modello non è stampato perché questo è stampato in modo particolare dalle sottoclassi di
AppElettr (sempre per questa ragione c'è il metodo getModello()). Infine definiamo le classi
Frigorifero e Televisore che derivano direttamente da AppElettr.
public class Frigorifero extends AppElettr { private int capacita; // capacità in litri del frigorifero public Frigorifero( float p, String prod, float raee, String mod, int cap) { super (p, prod, raee, mod); // costruttore della classe AppElettr capacita = cap; } // ridefinisce il metodo della classe AppElettr public void stampa() { out.println("FRIGORIFERO: "+getModello()); super .stampa(); // invoca il metodo della classe AppElettr out.println(" Capacità: "+capacita+" litri"); } } public class Televisore extends AppElettr { private int pollici; public Televisore( float p, String prod, float raee, String mod, int pol) { super (p, prod, raee, mod); // costruttore della classe AppElettr pollici = pol; } // ridefinisce il metodo della classe AppElettr public void stampa() { out.println("TELEVISORE: "+getModello()); super .stampa(); // invoca il metodo della classe AppElettr out.println(" Dimensione: "+pollici+" pollici"); } }
PANTALONI
Produttore: Levit Prezzo: 70.0 euro Taglia: 48 Colore: Marrone
Che cosa abbiamo imparato da questi primi esempi? Una cosa che, sopratutto da quest'ultimo esempio,
si può intuire è che l'ereditarietà, se ben usata, permette di strutturare il codice in modi che rispecchiano
la natura delle informazioni reali che si vogliono rappresentare. La gerarchia delle classi rispecchia in
modo fedele la gerarchia di concetti che sorge naturalmente nel momento in cui si vogliono organizzare
le informazioni relative ai diversi prodotti. La gerarchia permette anche di raccogliere facilmente a
fattore comune tutto ciò che si può condividere. Così che le classi si differenziano solamente dove è
veramente necessario. Questo elimina alla radice le possibili duplicazioni di codice rendendo più facile il
controllo della correttezza e la manutenzione. Inoltre, la struttura gerarchica, raccogliendo a fattor
comune tutto ciò che è condivisibile, facilita l'estensione delle funzionalità del sistema. Ad esempio, se si
vuole aggiungere una classe per rappresentare le lavatrici, basterà introdurre una sottoclasse di
AppElettr e gestire in essa solo ciò che differenzia le lavatrici dagli altri apparecchi elettrici/elettronici
(quello che è in comune come il prezzo, il produttore, il consumo in watt, ecc. è già gestito dalle
superclassi).
Un'altro importante vantaggio offerto dalla ereditarietà e in particolar modo dal polimorfismo sta nella
possibilità di trattare in modo uniforme oggetti di natura diversa. Il metodo stampaProdotti() tratta in
modo uniforme oggetti che rappresentano frigoriferi, televisori e capi di abbigliamento, mantenendo al
contempo la specificità di ogni oggetto. Questo, in ultima analisi, aiuta a scrivere codice più snello, più
leggibile e meno soggetto ad errori.
Esercizi
[Errori_prodotti] Il seguente programma usa le definizioni della gerarchia precedentemente definita e
contiene 3 errori (uno solo dei quali si verifica in compilazione), trovarli e spiegarli.
public class Test { public static void main(String[] args) { Prodotto[][] pM = new AppElettr[10][]; pM[0] = new Televisore[5]; pM[1] = new Frigorifero[8]; pM[2] = new Abbigliamento[10]; System.out.println(pM[0][0].getProduttore()); pM[1][0] = new Frigorifero(1000, "AAA", 8, "FF", 300); System.out.println(pM[1][0].getModello()); ((Frigorifero)pM[1][0]).setConsumoWatt(500); } }
[Estensione_prodotti] Definire una classe Lavatrice estendendo la classe AppElettr per gestire
attributi specifici come capacità di carico (in Kg) e tipo caricamento (frontale o superiore). Ovviamente
la stampa effettuata dal metodo stampa() deve essere coerente con quella degli altri apparecchi elettrici.
[Estensione_prodotti+] Definire una piccola gerarchia di classi che estende la classe AppElettr per
gestire i dati relativi a computer. Si consideri la possibilità di definire una classe Computer che raccoglie
gli attributi comuni a tutti i tipi di computer e poi delle sottoclassi per desktop , portatili (notebook), ed
eventualmente anche per ultraportatili.
[File_system] Definire una piccola gerarchia di classi per gestire le informazioni relative ai file e alle
directory di un file system.
Suggerimento: Definire una classe base FS_Item per rappresentare le informazioni comuni ai file e alle directory (nome, path assoluto, diritti di accesso, ecc.) e poi una sottoclasse per i file e una sottoclasse per le directory. Prevedere anche un metodo isDir() della classe base che ritorna true solo se l'oggetto è una directory.
[Regioni] Si vuole realizzare un archivio per mantenere i dati relativi alle regioni , provincie e
capoluoghi di provincia. Per ogni regione si vuole gestire il nome, l'estensione (in Km quadrati), la
popolazione e i collegamenti alle provincie. Per ogni provincia si vuole gestire il nome, l'estensione, la
popolazione, il numero di comuni e il collegamento al capoluogo di provincia. Per ogni capoluogo di
provincia si vuole gestire il nome, la popolazione, l'estensione e l'elenco dei nomi di tutte le
circoscrizioni. Definire una gerarchia di classi per la rappresentazione dell'archivio secondo le seguenti
indicazioni. Definire una classe base ElemGeo che gestisce i dati comuni ai diversi elementi (regioni,
provincie e capoluoghi) e un codice numerico che identifica univocamente ogni elemento. Definire poi le
sottoclassi Regione, Provincia e Capoluogo per gestire i dati specifici. Definire opportuni metodi per
impostare i dati ed eventualmente leggerli. Definire un metodo stampa che stampa tutti i dati di un
elemento.
[Figure_geometriche] Definire una classe Punto per rappresentare punti del piano a coordinate intere.
Definire una gerarchia di classi per gestire figure geometriche del piano (cerchi, rettangoli e triangoli) a
coordinate intere secondo le indicazioni seguenti.
a. Definire una classe base Figura2D e le sottoclassi Cerchio, Rettangolo e Triangolo. Ogni
oggetto di tipo Cerchio è determinato da un centro di tipo Punto e un raggio (intero). Ogni oggetto
Rettangolo è determinato da due punti (di tipo Punto) rappresentanti lo spigolo in alto a sinistra e
quello in basso a destra (il rettangolo ha i lati paralleli agli assi). Ogni oggetto Triangolo è
determinato da tre punti (i vertici del triangolo). Definire un metodo area che ritorna l'area della
figura geometrica. Definire un metodo minR che ritorna un oggetto Rettangolo che è il più
piccolo rettangolo che contiene la figura geometrica. Definire inoltre un metodo isIn che prende
in input un punto (di tipo Punto) e ritorna true o false a seconda che il punto cada all'interno o
all'esterno della figura geometrica.
b. Definire un metodo statico maxArea della classe Figura2D che prende in input un array di
Figura2D e ritorna la massima area delle figure geometriche dell'array.
c. Definire un metodo statico minRettangolo della classe Figura2D che prende in input un array di
Figura2D e ritorna un oggetto Rettangolo che è il più piccolo rettangolo che contiene tutte le
figure geometriche dell'array.
[Biblioteca] Si vuole gestire un archivio dei documenti (libri e DVD) di una biblioteca. Ogni
documento ha una collocazione. Prima di tutto definire quindi una classe Collocazione per gestire
appunto le collocazioni. Una collocazione è determinata da una stringa che specifica il nome di un
reparto della biblioteca, un numero di scaffale che identifica uno scaffale del reparto e da un numero che
indica una posizione nello scaffale. Poi, definire una gerarchia di classi secondo le seguenti indicazioni.
a. Definire una classe base Documento e poi le sottoclassi Libro, DVD_Video e DVD_Audio. Un
oggetto di tipo Libro consiste in una collocazione (un oggetto di tipo Collocazione), una stringa
contenente l'autore o gli autori del libro, una stringa contenente il titolo e un intero contenente il
numero di pagine. Un oggetto DVD_Video consiste in una collocazione, una stringa che contiene il
titolo del film, una stringa che contiene il regista o i registi e un intero che contiene la durata in
minuti del film. Un oggetto DVD_Audio consiste in una collocazione, una stringa che contiene il
nome della casa discografica, una stringa che contiene il titolo del DVD e per ogni brano una
stringa contenente il titolo del brano. Definire un metodo stampa che stampa le informazioni
relative ad un documento. Definire un metodo cercaInTitolo che prende in input una stringa str
e ritorna true o false a seconda che str sia contenuta o meno nel titolo del documento.
b. Definire un metodo statico cercaTitoli della classe Documento che prende in input un array di
oggetti Documento arrD e una stringa str e ritorna in un array di oggetti Documento tutti i
documenti dell'array arrD il cui titolo contiene la stringa str.
La classe Object
Nel linguaggio Java tutte le classi estendono automaticamente, direttamente o indirettamente, una
classe speciale chiamata Object. Quindi tutte le classi sono sottoclassi dirette o indirette di questa classe.
Quando una qualsiasi classe è definita, anche se non estende esplicitamente nessuna classe,
implicitamente estende la classe Object. Se ad esempio definiamo una classe NomeClasse:
class NomeClasse { ... }
Ciò è equivalente a scrivere:
class NomeClasse extends Object {
per copiare array di String, array di Title array di Point, ecc. Come nei seguenti esempi:
String[] a = {"primo", "secondo", "terzo"}; String[] b = new String[5]; copiaArray(a, b); Title[] t = { new Title("Classe"), new Title("Oggetto")}; Title[] tt = new Title[10]; copiaArray(t, tt); AlignTitle[] at = { new AlignTitle("A", AlignTitle.LEFT, 8), new AlignTitle("B", AlignTitle.LEFT, 8)}; copiaArray(at, t); // OK perché il tipo di at è un sottotipo del tipo di t int [][] mat = {{1, 2, 3}, {4, 5, 6}}; int [][] mat2 = new int [4][5]; copiaArray(mat, mat2);
Ovviamente non può essere usato per copiare array di tipi primitivi. Se il tipo effettivo (cioè, il tipo al
run-time) di src non è un sottotipo del tipo effettivo di dst allora accade un errore al run-time che
produce una eccezione di tipo ArrayStoreException. Ad esempio
copiaArray(a, mat);
non produce alcun errore in compilazione ma produrrà un errore in esecuzione.
L'operatore instanceof Se si vuole definire un metodo che permette di fare anche la copia di array di
tipi primitivi è necessario scrivere un metodo con la seguente intestazione:
public static void copiaArray(Object src, Object dst)
Infatti questo metodo può accettare come argomenti array di int o di un qualsiasi tipo primitivo. Però per
poterlo implementare è necessario avere la possibilità di riconoscere il tipo effettivo degli argomenti. Per
questo Java mette a disposizione l'operatore instanceof. Per un qualsiasi tipo riferimento (classe o
array) Type e un qualsiasi riferimento ad un oggetto (istanza di una classe o di un array) ref ,
l'espressione
ref instanceof Type
è true se e solo se il tipo al run-time di ref è un sottotipo di (o è uguale a) Type. In altre parole,
l'espressione ha valore true se e solo se l'oggetto ref o è una istanza del tipo Type o è una istanza di
qualche sottotipo di Type. Vediamo subito alcuni esempi chiarificatori:
Object obj = new Object(); if (obj instanceof Object) {...} // VERO if (obj instanceof Object[]) {...} // FALSO String str = "A"; if (str instanceof String) {...} // VERO if (str instanceof Object) {...} // VERO if (str instanceof Object[]) {...} // ERRORE in compilazione if (obj instanceof String) {...} // FALSO obj = str; if (obj instanceof String) {...} // VERO Title titolo = new Title("A"); if (titolo instanceof AlignTitle) {...} // FALSO titolo = new AlignTitle("B", AlignTitle.LEFT, 8); if (titolo instanceof AlignTitle) {...} // VERO if (titolo instanceof Title) {...} // VERO int [] interi = new int [10]; if (interi instanceof Object[]) {...} // ERRORE in compilazione obj = interi; if (obj instanceof Object[]) {...} // FALSO if (obj instanceof int []) {...} // VERO if (obj instanceof long []) {...} // FALSO if (interi instanceof long []) {...} // ERRORE in compilazione
Si osservi che l'espressione str instanceof Object[] produce immediatamente un errore in
compilazione perchè il compilatore può determinare che ha sempre valore false. Invece l'espressione
obj instanceof String può avere, a seconda del valore al run-time della variabile obj, sia valore true
che false.
Vediamo ora come l'operatore instanceof può essere usato per implementare il metodo
copiaArray():
public static void copiaArray(Object src, Object dst) { if ((src instanceof Object[]) && (dst instanceof Object[])) { Object[] s = (Object[])src; Object[] d = (Object[])dst; int n = (s.length <= d.length? s.length : d.length); for ( int i = 0 ; i < n ; i++) d[i] = s[i]; } else if ((src instanceof boolean []) && (dst instanceof boolean [])) { boolean [] s = ( boolean [])src; boolean [] d = ( boolean [])dst; int n = (s.length <= d.length? s.length : d.length); for ( int i = 0 ; i < n ; i++) d[i] = s[i]; } else if ((src instanceof byte []) && (dst instanceof byte [])) { byte [] s = ( byte [])src; byte [] d = ( byte [])dst; int n = (s.length <= d.length? s.length : d.length); for ( int i = 0 ; i < n ; i++) d[i] = s[i]; } else if ((src instanceof short []) && (dst instanceof short [])) { short [] s = ( short [])src; short [] d = ( short [])dst; int n = (s.length <= d.length? s.length : d.length); for ( int i = 0 ; i < n ; i++) d[i] = s[i]; } else if ((src instanceof int []) && (dst instanceof int [])) { int [] s = ( int [])src; int [] d = ( int [])dst; int n = (s.length <= d.length? s.length : d.length); for ( int i = 0 ; i < n ; i++) d[i] = s[i]; } else if ((src instanceof long []) && (dst instanceof long [])) { long [] s = ( long [])src; long [] d = ( long [])dst; int n = (s.length <= d.length? s.length : d.length); for ( int i = 0 ; i < n ; i++) d[i] = s[i]; } else if ((src instanceof char []) && (dst instanceof char [])) { char [] s = ( char [])src; char [] d = ( char [])dst; int n = (s.length <= d.length? s.length : d.length); for ( int i = 0 ; i < n ; i++) d[i] = s[i]; } else if ((src instanceof float []) && (dst instanceof float [])) { float [] s = ( float [])src; float [] d = ( float [])dst; int n = (s.length <= d.length? s.length : d.length); for ( int i = 0 ; i < n ; i++) d[i] = s[i]; } else if ((src instanceof double []) && (dst instanceof double [])) { double [] s = ( double [])src; double [] d = ( double [])dst; int n = (s.length <= d.length? s.length : d.length); for ( int i = 0 ; i < n ; i++) d[i] = s[i]; } else { throw new IllegalArgumentException("Array di tipo diverso"); } }
Ed ecco alcuni esempi di invocazione di tale metodo:
int [] intA = {0,1,2,3,4}; int [] intB = new int [10]; copiaArray(intA, intB); double [] dA = {2.897, 0.0067, 2.3459}; double [] dB = {0, 2, 6.7, 5.78986}; copiaArray(dA, dB); String[] strA = {"cane", "gatto", "topo"}; String[] strB = new String[6]; copiaArray(strA, strB); AlignTitle[] atA = { new AlignTitle("A", AlignTitle.LEFT, 8), new AlignTitle("B", AlignTitle.LEFT, 8)}; Title[] tB = new Title[4]; copiaArray(atA, tB); copiaArray(intA, dB); // ERRORE in esecuzione: IllegalArgumentException
Se adesso rifacciamo un test con la nuova classe IntPoint otteniamo:
public class Test { public static main(String[] args) { IntPoint p1 = new IntPoint(1, 1); intPoint p2 = new IntPoint(1, 1); if (p1.equals(p2)) { // VERO perchè sono oggetti dello stesso tipo ... // IntPoint e hanno lo stesso valore (anche se } // sono oggetti diversi) p2.x = 2; if (p1.equals(p2)) {...} // FALSO oggetti dello stesso tipo IntPoint } // ma con valori differenti }
La ridefinizione del metodo equals() che abbiamo dato non controlla che l'oggetto obj sia un'instanza
della classe IntPoint ma solamente che sia un'istanza di una qualche sottoclasse (tramite l'operatore
instanceof). Avremmo dovuto invece controllare che era proprio della stessa classe? Questo è un punto
piuttosto delicato e non c'è una risposta univoca. In alcuni casi è più conveniente definirlo come sopra e
in altri conviene controllare l'uguaglianza della classe (e c'è un modo di farlo). Non approfondiremo oltre
l'argomento perché al momento sarebbe prematuro.
Il metodo toString() L'intestazione del metodo toString() è la seguente:
public String toString()
Il metodo ritorna una rappresentazione tramite stringa dell'oggetto su cui è invocato. Come al solito
l'implementazione di default non è molto utile. Infatti, ritorna una stringa contenente il nome della classe
dell'oggetto seguita dal carattere '@' e poi la rappresentazione in esadecimale di un codice hash
dell'oggetto (per ora non approfondiremo da dove proviene e a cosa serve questo codice). Ecco alcuni
esempi:
public class Test { public static main(String[] args) { IntPoint p1 = new IntPoint(1, 1); intPoint p2 = new IntPoint(1, 1); out.println(p1.toString()); out.println(p2.toString()); p2.x = 2; out.println(p2.toString()); Title t = new Title("Titolo"); out.println(t.toString()); } }
Il risultato dell'esecuzione è il seguente (assumendo che le classi IntPoint e Title siano nel package
metodologie):
metodologie.IntPoint@dbe metodologie.IntPoint@af9e metodologie.IntPoint@af9e metodologie.Title@b6ece
Quindi, come nel caso del metodo equals(), se si vuole che tale metodo sia utile è necessario
ridefinirlo. Tutte le classi della piattaforma Java per cui il metodo toString() è utile lo ridefiniscono.
Ad esempio la classe String (ritorna la stringa stessa). Il metodo toString() è importante anche perché
è automaticamente invocato (dal compilatore) tutte le volte che il riferimento ad un oggetto è usato in
una espressione di concatenazione di stringhe come operando dell'operatore +. Ad esempio l'espressione
"punto: "+p1 è automaticamente trasformata dal compilatore nell'espressione "punto:
"+p1.toString(). Inoltre il metodo println() quando riceve come argomento il riferimento ad oggetto
invoca il metodo toString() su quell'oggetto. Infatti nel programma precedente avremmo potuto
scrivere out.println(p1) invece di out.println(p1.toString()). Vediamo ora come si può
ridefinire il metodo toString(). Per semplicità consideriamo le classi IntPoint e Title:
class IntPoint { ... // la parte che rimane invariata è omessa public String toString() {
return "("+x+", "+y+")"; } } class Title { ... // la parte che rimane invariata è omessa public String toString() { return title; } }
Se ora eseguiamo di nuovo il programma di test precedente otteniamo il seguente risultato:
Titolo
Tutti i metodi della classe Object sono anche ereditati dagli oggetti di tipo array. Però, a differenza degli
oggetti di tipo classe, per gli oggetti array i metodi non possono essere ridefiniti. Per questa ragione la
classe Arrays ha metodi statici che sono dei validi sostituti per gli array di gran parte dei metodi della
classe Object, come toString() e equals().
Esercizi
[Errori_O_1] Il seguente programma contiene uno o più errori. Trovare gli errori e spiegarli. In
particolare, dire per ogni errore se si verifica in compilazione o durante l'esecuzione.
class Pair { private String key, value; public Pair(String k, String v) { key = k; value = v; } public String getKey() { return key; } } public class Test { public static void main(String[] args) { Pair[] pp = new Pair[] { new Pair("K", "V"), new Pair("KK", "VV")}; System.out.println(pp[0].toString()); System.out.println(pp.toString()); Object[] oA = pp; String k = oA[0].getKey(); Object[] oB = new int [4]; } }
[Errori_O_2] Il seguente programma contiene uno o più errori. Trovare gli errori e spiegarli. In
particolare, dire per ogni errore se si verifica in compilazione o durante l'esecuzione.
public class Test { public static void main(String[] args) { String[] sA = new String[] {"A", "B", "C"}; double [] dA = new double [] {0.9, 1.2}; System.out.println(sA.toString()+dA.toString()); Object[] oA = dA; Object obj = sA; Object obj2 = dA; boolean [][] tab = new boolean [4][4]; Object[] oB = tab; Object[][] oT = tab; } }
[Errori_O_3] Il seguente programma contiene uno o più errori. Trovare gli errori e spiegarli. In
particolare, dire per ogni errore se si verifica in compilazione o durante l'esecuzione.
public class Test {
nascondere il fatto che la classe base (nel nostro esempio Shape) non è una classe concreta. Nel senso
che gli oggetti di tale classe non possono essere usati direttamente. Gli oggetti della classe Shape non
rappresentano alcuna figura specifica e quindi non possono in nessun modo essere usati direttamente,
solamente gli oggetti delle sottoclassi possono essere usati direttamente. In altre parole, non ha senso
istanziare oggetti della classe Shape.
Proprio allo scopo di fornire strumenti per risolvere in modo soddisfacente situazioni come quella
appena descritta, il linguaggio Java permette di definire classi astratte ( abstract classes ). Una classe
astratta è come una classe normale (concreta) con però uno o più metodi senza implementazione. Un
metodo senza implementazione è un metodo astratto ( abstract method ) per il quale è definita solamente
l'intestazione (ovvero l'interfaccia). La sintassi per definire metodi astratti e classi astratte è molto
semplice. È sufficiente usare il modificatore abstract e terminare l'intestazione dei metodi astratti con
";" che sostituisce il corpo del metodo. Ecco un breve elenco delle caratteristiche principali di una classe
astratta.
Una classe con un metodo astratto deve essere dichiarata astratta.
Una classe astratta non può essere istanziata.
Una sottoclasse di una classe astratta può essere istanziata solo se implementa tutti i metodi astratti
della superclasse.
Se una sottoclasse di una classe astratta non implementa tutti i metodi astratti che eredita è essa
stessa astratta e deve essere esplicitamente dichiarata astratta.
Metodi static o private non possono essere astratti (perché tali metodi non sono ereditati dalle
sottoclassi e quindi non sarebbero mai implementati).
Oltre a queste caratteristiche che la differenziano da una classe concreta, una classe astratta è del tutto
simile ad una classe normale.
La prima classe - versione 3 Grazie alle classi astratte possiamo ristrutturare le classi CharRect e
PrintMedium. Prima di tutto introdurremo una classe astratta, che chiameremo CharShape, che
rappresenta una generica figura di caratteri. La classe CharRect sarà una delle sottoclassi concrete di
CharShape. Un'altra sarà CharPyramid. Ovviamente se ne possono aggiungere altre a piacimento.
D'altronde uno degli scopi della nuova struttura è proprio quello di facilitare l'estensione delle
funzionalità del sistema. Inoltre, le classi astratte risultano utili anche per migliorare la classe
PrintMedium. La classe PrintMedium diventerà una classe astratta e per ogni mezzo di stampa specifico
si introdurrà una corrispondente sottoclasse concreta di PrintMedium.
Per mantenere le classi semplici in modo da focalizzare l'attenzione sulle relazioni tra le classi,
implementeremo una versione semplificata della classe CharRect. Rispetto all'ultima versione
prevediamo un solo carattere e un solo metodo di stampa. Iniziamo dalla definizione della classe
CharShape:
// package in cui sono definite tutte le classi della gerarchia di CharShape package charshape; import printmedium.; // il package in cui è definita la classe PrintMedium public abstract class CharShape { private static final char DEF_FILLCHAR = ''; private char fillChar = DEF_FILLCHAR; private int left, top; private PrintMedium pMedium; public CharShape(PrintMedium pm, int l, int t) { left = l; top = t; pMedium = pm; } public void setChar( char c) { fillChar = c; } public void setPM(PrintMedium pm) { pMedium = pm; } public abstract void draw(); // metodi astratti che saranno implementati public abstract int area(); // nelle sottoclassi concrete // metodo di utilità che stampa una linea di caratteri nella riga r con lo
void drawRow( int r, int offset, int length) { // specificato offset rispetto a for ( int k = 0 ; k < length ; k++) // left e di lunghezza length pMedium.printChar(top + r, left + offset + k, fillChar); } void end() { pMedium.end(); } }
Essenzialmente la classe si occupa di gestire la stampa a "basso livello" fornendo un metodo di utilità
drawRow() che stampa una linea di caratteri che inizia in una specificata posizione di una riga e ha una
certa lunghezza. Si noti che i metodi drawRow() e end() hanno accesso limitato al package charshape
perché tali metodi servono solamente per l'implementazione delle sottoclassi. Passsiamo ora alla
definizione delle sottoclassi.
package charshape; import printmedium.; // il package in cui è definita la classe PrintMedium public class CharRect extends CharShape { private int width, height; public CharRect(PrintMedium pm, int l, int t, int w, int h) { super (pm, l, t); // invoca il costruttore di CharShape width = w; height = h; } public void draw() { // implementa il metodo astratto for ( int r = 0 ; r < height ; r++) drawRow(r, 0, width); end(); } public int area() { // implementa il metodo astratto return widthheight; } }
Ed ecco anche la definizione della classe CharPyramid:
package charshape; import printmedium.; // il package in cui è definita la classe PrintMedium public class CharPyramid extends CharShape { private int height; public CharPyramid(PrintMedium pm, int l, int t, int h) { super (pm, l, t); // invoca il costruttore di CharShape height = h; } public void draw() { // implementa il metodo astratto for ( int r = 0 ; r < height ; r++) drawRow(r, height - r - 1, 2r + 1); end(); } public int area() { // implementa il metodo astratto return height*height; } }
Grazie al metodo drawRow(), le implementazioni dei metodi draw() delle due sottoclassi sono
particolarmente semplici ed evitano duplicazioni di codice. Veniamo ora alla classe PrintMedium.
// il package in cui sono definite tutte le classi della gerarchia PrintMedium package printmedium;