Docsity
Docsity

Prepara i tuoi esami
Prepara i tuoi esami

Studia grazie alle numerose risorse presenti su Docsity


Ottieni i punti per scaricare
Ottieni i punti per scaricare

Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium


Guide e consigli
Guide e consigli


Dal linguaggio C al linguaggio Java, Dispense di Programmazione Java

introduzione alla programmazione orientata agli oggetti e al linguaggio Java. Si assume che il lettore conosca il linguaggio C cosicchè le caratteristiche di Java che derivano direttamente dal C sono trattate il più rapidamente possibile

Tipologia: Dispense

2019/2020

Caricato il 09/03/2020

libertar
libertar 🇮🇹

4.5

(15)

18 documenti

1 / 35

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
Dal linguaggio C al linguaggio Java
(Terza parte)
Riccardo Silvestri
30-3-2009 10- 3-2010
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
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18
pf19
pf1a
pf1b
pf1c
pf1d
pf1e
pf1f
pf20
pf21
pf22
pf23

Anteprima parziale del testo

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;