|
Java Tutorial - Parte 1 0.1
Un tutorial per esempi
|
In questo capitolo imparerete le altre due caratteristiche della programmazione ad oggetti:
A questo scopo avvieremo un nuovo progetto costituito da due applicazioni CLI (Command Line Interface=interfaccia a riga di comando) che gestiscono figure geometriche piane e ne disegnano la forma. Il disegno viene salvato in un file immagine, in formato PNG. Le figure gestite dal progetto sono:
In una versione futura (se mai ci sarà) saranno gestiti anche:
Il primo programma è chiamato Geometry e fornisce le info su una qualsiasi figura geometrica come base, altezza, perimetro, area e altre info specifiche per il tipo di figura come, per esempio, l'apotema per i poligoni regolari.
Il secondo programma è chiamato Geomart e crea un quadro di arte moderna di dimensioni a piacere mescolando in modo casuale le forme geometriche, le dimensioni ed i loro colori. Se volete una anteprima di questi "quadri" clikkate questo link.
Nella sottocartella javatutor1/projects/geometry troverete i files sorgente di tutto il codice descritto in questo capitolo. Di seguito l'elenco dei files ed una loro breve descrizione:
| Nome del file | Descrizione |
|---|---|
| rect01.java | un primo tentativo, inutile e non usato nella applicazione |
| figure.java | le classi di base delle figure geometriche |
| rectangle.java | le classi Rectangle e Square |
| triangle.java | le classi Triangle ed EquiTriangle |
| ellipse.java | le classi Ellipse e Circle |
| polygon.java | la classe Polygon, un poligono regolare |
| geometry.java | la classe della applicazione principale |
| docoptions.txt | usato per la documentazione |
| overview.txt | usato per la documentazione |
Questa applicazione consente all'utente di specificare i dati essenziali di una figura geometrica come il tipo di figura (triangolo, rettagolo, etc), la lunghezza della base e della altezza ed il programma calcolerà area e perimetro della figura oltre ad altri dati specifici per il tipo di figura. Opzionalmente, il programma disegnerà la figura in un file PNG ma questo aspetto verrà affrontato nel prossimo capitolo (vedi Il disegno dei poligoni).
Per il momento, l'unica opzione accettata dal programma è la:
-h[SHAPE]: visualizza helpl'argomento alla opzione -h può essere la stringa "SHAPE" che, se specificata, visualizza l'elenco delle forme geometriche gestite dal programma. Esse sono:
base ed height sono obbligatori height, se specificato, viene ignorato poichè per un quadrato la altezza è uguale alla base base ed height sono obbligatori base rappresenta il lato del triangolo, l'argomento height, se specificato, viene ignorato base è la dimensione del raggio del cerchio circoscritto nel poligono base è la dimensione del raggio del cerchio circoscritto nel poligono base è la dimensione del raggio del cerchio circoscritto nel poligono base è la dimensione del raggio del cerchio circoscritto nel poligono base è la dimensione del raggio del cerchio circoscritto nel poligono base è la dimensione del raggio del cerchio circoscritto nel poligono base è la dimensione del diametro del cerchio; se viene specificato l'argomento height allora si ha una ellisse; questo ultimo argomento è la dimensione dell'asse verticale n lati; l'argomento base è la lunghezza del raggio del cerchio circoscritto mentre sides è il numero di lati del poligono (minimo 3).Esempi:
> java -ea Geometry RECT 234 128
fornisce le informazioni (area e perimetro) su un rettangolo con base 234 ed altezza 128 pixels
> java -ea Geometry OTTA 124 > java -ea Geometry POLI 124 8
I due comandi sono equivalenti: forniscono le informazioni su un ottagono regolare con raggio del cerchio circoscritto pari a 124 pixels.
Aprite il file sorgente rect01.java nel vostro editor preferito e commentiamo il listato. La organizzazione della classe Rectangle è la seguente:
Definiamo due membri dati privati: base (la base) ed height (la altezza) che sono i due dati essenziali di un rettangolo. Poichè la loro lunghezza deve essere maggiore di ZERO, dichiariamo nel costruttore che se i dati non sono coerenti, sarà sollevata una eccezione.
Richiamando i metodi possiamo ottenere le informazioni sulla figura:
getFigureType(): ritorna il tipo di figura; nel nostro caso la stringa "Rettangolo" getPerimeter(): ritorna la dimensione del perimetro; nel caso di un rettangolo la formula è: 2*base+2*height getArea(): ritorna la dimensione dell'area del rettangolo; nel caso di un rettangolo la formula è: base*height printInfo(): stampa a terminale le informazioni sulla figura implementata da questa classe: il tipo, la base e l'altezza, il perimetro e l'areaCome già accennato, possiamo facilmente notare che il quadrato altro non è che una particolare forma di rettangolo:
Pertanto, sarebbe assurdo riscrivere i metodi che restituiscono la area ed il perimetro per il quadrato: possiamo ereditarli dalla classe Rectangle derivando la classe Square (=quadrato) dalla classe Rectangle. Per derivare una classe (che chiameremo figlia o derivata) da un altra classe (che chiameremo genitore o base) si usa la parola chiave extends seguita dal nome della classe base.
La classe derivata eredita tutti i metodi ed i membri dati dalla classe base. Essa può, comunque, definire nuovi membri dati e nuovi metodi, specifici per la classe derivata. Se uno o più metodi della classe base non sono appropriati per la derivata, essa può sovrascriverli cioè definire metodi che hanno la stessa firma di quelli della classe base.
Nel caso della classe Square il metodo getFigureType della classe base NON è corretto dal momento che per la classe Square la stringa da restituire non è "Rettangolo" bensì "Quadrato".
Il costruttore della classe Square ha un solo argomento: il lato del quadrato (side).
Quando viene istanziata una classa derivata la JVM richiama tutti i costruttori dell'intero albero genealogico a partire DAL INIZIO dell'albero, cioè dal trisavolo. Nel nostro caso, il costruttore che viene chiamato per primo è quello di Rectangle e, successivamente, quello di Square.
Se la/le classe/i genitrici possiedono costruttori di default essi vengono richiamati automaticamente dalla JVM; se invece non esistono costruttori di default è necessario richiamarne uno passandovi i necessari argomenti usando la parola chiave super.
Poichè la classe Rectangle non ha costruttori di default, si rende necessario richiamare il suo unico costruttore passando come argomenti la base e l'altezza del rettangolo: poichè la classe Square gestisce un quadrato, la base e l'altezza coincidono col lato del quadrato. Ecco perchè la istruzione è super(side,side ).
super() DEVE essere la prima istruzione nel corpo del costruttore di una derivata: non è possibile anteporre nulla a questa istruzione. Passiamo ora a scrivere i metodi della classe Square: dal momento che la classe deriva da Rectangle, tutti i metodi ed i membri dati di quest'ultima sono ereditati dalla classe figlia. In particolare, i metodi getPerimeter e getArea sono corretti anche per il quadrato poichè sia il perimetro che l'area di un quadrato possono essere calcolari esattamente come quelli del rettangolo.
Anche il metodo printInfo può andare bene dal momento che esso stampa:
getFigureType L'unico metodo che dobbiamo cambiare (si dice in gergo sovrascrivere) è il metodo getFigureType: quello della classe genitore (detta la classe base) non và bene poichè esso deve restituire la stringa "Quadrato":
Il metodo in sè non richiede commenti; una nota però la merita la annotation @Override. Perchè un metodo venga correttamente sovrascritto deve avere la stessa signature (=firma), cioè deve:
E' piuttosto improbabile sbagliare la sovrascrittura del metodo getFigureType dal momento che non ha argomenti ma, se un metodo è particolarmente complesso un errore è possibile ed invece di avere un metodo sovrascritto avremo un altro metodo, totalmente diverso da quello della classe base.
La annotazione @Override istruisce il compilatore di verificare che il metodo definito in seguito sia effettivamente una sovrascrittura di un metodo definito nella classe base; in caso contrario il compilatore segnala un errore. Questo può aiutarci a prevenire errori logici: se intendevamo sovrascrivere un metodo, il messaggio di errore del compilatore ci segnala che la firma del metodo della derivata è errata.
La annotazione non è obbligatoria: non fà parte del linguaggio Java in senso stretto e, se non indicata, il sorgente compila comunque correttamente.
Ora scriviamo il metodo principale main in modo che il programma possa accettare uno o due argomenti sulla cmdline:
Square Rectangle Compiliamo ed eseguiamo il programma con alcuni argomenti:
..\javatutor1\projects\geometry>java -ea Rectangle 124 Figure type: Square b=124.0 h=124.0 A=15376.0 P=496.0 ..\javatutor1\projects\geometry>java -ea Rectangle 124 88 Figure type: Rectangle b=124.0 h=88.0 A=10912.0 P=424.0
Come previsto, il metodo getFigureType ritorna la stringa corretta a seconda del tipo di oggetto istanziato (un oggetto di classe Square o di classe Rectangle) e questo nonostante il tipo referenziato fig sia comunque di classe base:
Questo meccanismo è detto polimorfismo: i metodi sovrascritti che vengono effettivamente richiamati non dipendono dal tipo della variabile (la variabile fig è di tipo Rectangle) ma dipendono dal tipo di oggetto istanziato con l'operatore new.
L'output del metodo printInfo per un quadrato, benchè assolutamente corretto, non è, come si suol dire, professionale:
>java -ea Rectangle 124 Figure type: Square b=124.0 h=124.0 A=15376.0 P=496.0
E' pur vero che un quadrato ha una base ed una altezza ma, poichè essi coincidono, sarebbe più professionale stampare la misura del lato del quadrato: sappiamo bene che essa è più che sufficente per identificare il quadrato.
Pertanto, sovrascriveremo anche il metodo printInfo in modo che esso sia personalizzato per la figura del quadrato. Questo metodo non è stato inserito nel sorgente rect01.java quindi dovete copiarlo-e-incollarlo nel sorgente che avete aperto nel vostro editor di testi:
Compilate il sorgente e vi accorgerete che il compilatore rifiuta di compilare a causa del seguente messaggio di errore:
>javac rect01.java
rect01.java:96: error: base has private access in Rectangle
System.out.println( " l=" + base );
^
1 error
Il messaggio di errore è piuttosto eloquente: si è tentato di accedere al membro dati base che è privato della classe Rectangle da parte di un metodo esterno alla classe Rectangle. In effetti, il metodo printInfo da cui si è tentato l'accesso fà parte della classe Square e non della classe Rectangle.
Un membro dati o un metodo con attributo di accesso private è accessibile solo e soltanto dai metodi della classe in cui è stato definito e da nessun altro metodo esterno alla classe stessa. Tuttavia, l'accesso ai dati membro ed ai metodi di una classe base dovrebbe essere garantito proprio perchè la ereditarietà è quella caratteristica della programmazione ad oggetti che consente di condividere dati e metodi tra classi che appartengono allo stesso albero genealogico.
Ovviamente, questi membri dati e metodi non dovrebbero essere pubblici: l'accesso può essere consentito ai metodi delle classi derivate ma deve essere precluso all'esterno dell'albero. Questo attributo di accesso è il: protected. Modificate quindi l'attributo di accesso ai membri dati della classe Rectangle da private a protected:
ricompilate ed eseguite il programma:
>java -ea Rectangle 123 Figure Type: Square l=123.0 P=492.0 A=15129.0
Per quanto riguarda i triangoli possiamo osservare che essi si comportano come i rettangoli: hanno gli stessi dati essenziali (base ed altezza) e sono uno (il triangolo equilatero) un caso speciale dell'altro (il triangolo isoscele). Però, nessuno dei metodi del rettangolo può essere utilizzato per un triangolo nemmeno i due metodi che restituiscono il perimetro e l'area dal momento che le formule per il loro calcolo sono diverse.
Finora abbiamo gestito solo due figure delle quali una era derivata dall'altra. Ma che dire delle altre figure che non c'azzeccano con un rettangolo. Torniamo un momento all'inizio del capitolo e analizziamo le varie figure:
Circle potrebbe derivare da Ellipse EquiTriangle potrebbe derivare da Triangle; questa classe condivide anche i dati del rettangolo (base e altezza) ma le formule per il calcolo di area e perimetro sono diverse Sembrerebbe quindi che nel main ci servano almeno quattro variabili per gestire figure così diverse tra loro:
Assolutamente inaccettabile! La soluzione è quella di definire una classe base per tutte le figure geometriche: questa classe, che chiameremo Figure sarà il capostipite di tutte le altre e ci basterà una variabile di tipo Figure per gestire qualsiasi forma geometrica che deriva da essa.
L'albero genealogico delle figure geometriche sarà il seguente:
Figure
|
-----------------------------------------------
| | |
Ellipse FigureBH Polygon
| |
Circle --------------------------
| |
Triangle Rectangle
| |
EquiTriangle Square
Esaminiamo tutte le classi:
Figure è il capostipite, quello che ci consente di usare una sola variabile per riferirci a qualsiasi figura geometrica Ellipse: è la classe che descrive una ellissi; i dati essenziali di questa classe sono la dimensione dei due assi: quello orizzontale e quello verticale Circle: è un tipo speciale di ellissi in cui i due semidiametri coincidono e rappresentano il raggio del cerchio FigureBH: è la classe che rappresenta quelle figure geometriche in cui i dati essenziali sono la base e l'altezza: il triangolo ed il rettangolo Rectangle: rappresenta il rettangolo da cui deriva il caso particolare Square (il quadrato) Triangle: rappresenta il triangolo isoscele da cui deriva il caso particolare EquiTriangle (il triangolo equilatero) Polygon: rappresenta un poligono regolare i cui dati essenziali sono il raggio del cerchio circoscritto ed il numero di latiMa quali sono i dati specifici della classe Figure? Cosa memorizziamo nei suoi membri dati? Come possiamo costruire e/o istanziare un oggetto di tipo Figure?
Se rispondiamo a queste domande ci accorgiamo che:
Figure perchè che tipo di figura sarebbe? di nessun tipo, solo una derivata rappresenta effettivamente una figura geometricaEcco perchè la classe Figure è una classe base astratta: il compilatore impedisce di istanziare oggetti di tipo astratto e non solo: una classe astratta può definire dei metodi astratti: essi non hanno corpo ma qualsiasi derivata è obbligata a sovrascrivere tutti i metodi astratti. Questa è lo schema generale:
La parola chiave abstract definisce la classe MyClass come una classe base astratta. La stessa parola chiave usata come attributo ad un metodo lo definisce come metodo astratto che deve essere implementato nelle derivate.
Una classe base astratta può avere uno o più costruttori? Certo che si, dipende da cosa quella particolare classe rappresenta.
La classe base Figure non ha costruttori di default ma ne possiede uno che accetta come argomento il numero di lati della figura. Abbiamo anche stabilito che se la figura non ha lati (come la ellissi ed il cerchio) il numero di lati da passare al costruttore vale -1:
Notate che il membro dati numSides è stato dichiarato protected anzichè privato della classe base astratta in modo da consentire l'accesso alle derivate. Normalmente, tutti i membri dati definiti in una classe di cui si prevede la definizione di derivate dovrebbero essere dichiarati protected.
Ci possono essere casi, peraltro rari, in cui un dato di una classe base debba rimanere privato o perchè non è di alcuna utilità alle derivate o perchè la modifica di esso da parte delle derivate potrebbe essere critica per il funzionamento dell'intero albero genealogico.
In questi casi si potrebbe definire il membro dati privato e fornire uno o più metodi protected per il suo reperimento e/o la sua modifica. In ogni caso, se il membro dati della classe base non deve essere modificato ma deve comunque essere accessibile alle derivate è preferibile dichiararlo protected final piuttosto che private.
La classe capostipite deve definire (o meglio dichiarare ) tutti i metodi che possono essere richiamati dall'esterno della classe stessa (o dal main, se interno alla classe) come per esempio getArea e getPerimeter dal momento che essi possono essere richiamati usando una variabile di tipo Figure. Ma la classe base capostipite non sà nulla di
come si calcolano area e perimetro dal momento che le formule sono diverse a seconda del tipo di figura geometrica. Quindi la classe base dichiarerà questi metodi astratti:
Vi chiederete perchè tra la lista dei metodi astratti non compaia il metodo printInfo. In fondo, la classe base nulla sà di quali informazioni stampare a terminale: la base e l'altezza? il raggio? il numero di lati? le due assi di simmetria? Solo una derivata è a conoscenza di quali informazioni stampare: la classe Circle stamperà la misura del raggio mentre la classe Rectangle stamperà la misura della base e dell'altezza.
Ciononostante, il metodo printInfo sarà un metodo concreto: questo perchè ci sono alcune informazioni comuni a tutte le figure: il tipo di figura, il perimetro e l'area:
FigureType: Xxxxxx ... P=180.0 A=2025.0
Dal momento che queste informazioni sono restituite dai metodi sovrascritti nelle classi derivate (e tra l'altro astratti, quindi obbligatoriamente sovrascritti) è possibile definire come concreto il metodo printInfo nella classe capostipite:
Per stampare le informazioni che dipendono dal tipo di figura geometrica, il metodo printInfo richiama il metodo astratto getInfo che restituisce una array di stringhe; il metodo printInfo itera sulla array restituita e stampa tutte le stringhe di informazione.
Una classe base astratta può certamente avere metodi statici. Poichè un metodo statico non ha bisogno di un oggetto reale per essere richiamato, i metodi statici non sono mai astratti, nè possono essere sovrascritti e non innescheranno mai il polimorfismo. Una classe derivata può avere metodi statici con la stessa firma di un metedo della classe base ma sono metodi totalmente diversi, ognuno con la propria identità:
Nella classe base astratta Figure vengono al momento definiti due metodi statici:
realToString(double,int) che restituisce una stringa con un numero fisso di decimali da un valore di tipo double. Il numero di decimali da restituire è specificato nel secondo argomento al metodo realToString(double) che richiama il metodo precedente passandovi come secondo argomento una costante definita nella stasse stessa: NUMBER_OF_DECIMALS che viene inizializzato a 1.Una ellissi è una figura geometrica nella quale la somma delle distanze tra i punti della ellisse ed i due fuochi è costante. Una proprietà della ellissi è che possiede due assi di simmetria che passano entrambe dal centro della elissi. Per costruire una ellissi, pertanto, è sufficente fornire la lunghezza dei due assi di simmetria, l'asse orizzontale (ha) e l'asse verticale (va):
Passiamo ora a definire i metodi della classe Ellipse: quali metodi dobbiamo definire? Come abbiamo imparato poc'anzi, tutti quelli che nella classe base sono dichiarati astratti e cioè:
getFigureType() getPerimeter() getArea() getInfo() Il listato sorgente è davvero di facile lettura; notate l'uso della costante Math.PI che rappresenta il pi-greco. La libreria standard di Java definisce decine se non centinaia di costanti simili a pi-greco; vedi Constant Field Values.
Il cerchio è una figura geometrica piana in cui tutti i suoi punti giacciono ad una distanza costante da un punto detto centro del cerchio. Questa distanza è conosciuta col nome di raggio del cerchio e si indica con la lettera r. Per costruire un cerchio è pertanto sufficente specificare la misura del suo raggio:
Possiamo notare che anche il cerchio, come la ellisse, possiede gli assi di simmetria sia orizzontale che verticale ma essi sono coincidenti e valgono il doppio esatto della misura del raggio. Nel cerchio il suo asse di simmetria si chiama diametro.
Essendo il cerchio una forma particolare di ellisse, la classe che lo descrive deriva da Ellipse. La classe è definita nello stesso listato sorgente della sua classe base. Il suo costruttore, tuttavia, accetta un solo argomento: il raggio del cerchio:
Dal momento che questa è una classe derivata e la classe base non possiede un costruttore di default siamo costretti a richiamare il costruttore della classe base con l'istruzione super a cui dobbiamo fornire i due argomenti: asse orizzontale e asse verticale.
La misura dei due assi potrebbe essere calcolata facilmente: è il doppio del raggio del cerchio e, pertanto, avremmo potuto, molto più semplicemente, richiamare il costruttore con una istruzione come questa:
invece di passare due dati fittizi (il valore UNO) e poi calcolare i due assi in un secondo momento. Tuttavia, questo approccio non è professionale. Cosa accade se l'user costruisce un cerchio con raggio ZERO?
Ebbene, poichè il costruttore delle classi base viene eseguito per primo, avremmo passato al costruttore di Ellipse due valori a ZERO e questo avrebbe sollevato la eccezione:
ERROR: invalid arguments for \'ellipse\': axis cannot be ZERO or negative
che però non ha molto senso: l'user voleva costruire un cerchio e quindi il messaggio di errore propedeutico è il seguente:
ERROR: invalid arguments for \'circle\': radius cannot be ZERO or negative
Passiamo ora a definire i metodi astratti i quali, al contrario di quanto accaduto nella classe Ellipse, non sono obbligatori. Perchè mai non dovrebbero essere obbligatori se sono metodi astratti? Semplice, perchè la classe Circle li eredita dalla classe Eclipse, da cui deriva.
Per questo motivo specifichiamo per tutti essi la annotazione @Override; questo ci garantisce che i quattro metodi che abbiamo definito sovrascrivono altrettanti metodi della classe base:
Il codice dei metodi è facile, non sono necessari commenti. Sicuramente vi starete chiedendo per quale motivo si è scelto di derivare la classe Circle dalla classe Ellipse: nessuno dei membri dati e dei metodi della classe base è stato usato minimamente nelle classe derivata.
Persino il calcolo di area e perimetro è diverso. In effetti al momento non abbiamo avuto alcun vantaggio nella derivata ma, più avanti, quando scriveremo i metodi per il disegno delle figure geometriche, ci renderemo conto che un cerchio è effettivamente un caso particolare di ellisse (vedi Il disegno della ellisse)
Vi sono due figure geometriche che, pur sostanzialmente diverse tra loro, condividono i loro dati essenziali come la base e l'altezza: sono il rettangolo ed il triangolo isoscele.
Pertanto, definiremo una classe, derivata da Figure, che li accomuna in modo da poter condividere i suoi due dati essenziali: base e altezza. Dal momento che una simile figura non esiste nella realtà, la chiameremo FigureBH dove la "B" sta per base e la "H" stà per height.
Il costruttore è davvero semplice; non fà altro che memorizzare i due dati essenziali nei propri dati membro, dichiarati protected in modo da consentirne l'accesso alle derivate. Prima di qualsiasi operazione, però, richiama il costruttore della classe base passanvi il numero di lati della figura: argomento che viene passato al costrutore di FigureBH stessa dal momento che questo valore può variare.
Passiamo poi a definire i metodi astratti che, ricordiamolo, sono OBBLIGATORI.
Prendiamo ad esempio il metodo astratto getFigureType. Che tipo di figura dovrebbe restituire la classe FigureBH? Beh, che ne sa questa classe di che tipo di figura si tratta? Solo le derivate lo possono conoscere: Rectangle e Triangle. Quindi non possiamo definire questo metodo e nemmeno gli altri; le formule per calcolare area e perimetro sono diverse tra il triangolo ed il rettangolo.
Ma non è forse OBBLIGATORIO per una derivata sovrascrivere i metodi astratti? Si, lo è! A meno che la classe derivata non sia essa stessa una classe base astratta:
Dite la verità. Non vi eravate accorti della parola chiave abstract nella dichiarazione della classe FigureBH.
Dal momento che non dobbiamo (nè possiamo) definire i metodi astratti, la definizione della classe FigureBH potrebbe terminare qui. Cionostante, definiremo due metodi concreti che restituiscono i due dati essenziali della figura geometrica: la base e l'altezza:
A cosa servono questi due metodi? A nulla in particolare ma ne vedremo l'utilità più avanti in questo capitolo quando impareremo l'uso di un importante operatore del linguaggio Java; vedi L'operatore instanceof.
Questa classe descrive un triangolo isoscele, una figura geometrica con tre lati dei quali due uguali. Un triangolo isoscele è definito con due argomenti: la base (il lato diverso dagli altri due) e l'altezza. L'altezza del triangolo isoscele divide il triangolo in due triangoli rettangoli uguali; è possibile quindi calcolare la lunghezza dei due lati uguali applicando il teorema di pitagora. La misura dei due lati uguali coincide con l'ipotenusa dei due triangoli rettangoli:
Il costruttore del triangolo iscoscele è di facile lettura: utilizziamo la istruzione super per costruire la classe base FigureBH passandovi come argomenti il numero di lati, la base e l'altezza del triangolo. Per finire, calcoliamo la lunghezza dei due lati uguali: ci serve per calcolare il perimetro del triangolo. Questa misura viene memorizzata nel membro dati protetto hypo (sta per hypotenuse).
I metodi statici Math.sqrt e Math.pow che fanno parte della libreria standard di Java restituiscono, rispettivamente, la radice quadrata e l'elevamento a potenza di un numero reale di tipo double. Il valore restituito è anch'esso di tipo double; questo è il motivo della necessità di un cast esplicito per ridurlo a float. Per un ripasso sul concetto di cast esplicito vedi Il cast esplicito.
Questa classe descrive una figura geometrica reale, quindi non è astratta: dobbiamo necessariamente definire i metodi che nella classe base Figure sono stati dichiarati astratti:
Non li commento nemmeno. Cosa succede se ci dimentichiamo di definire il metodo getFigureType? Cosa succede se provo a compilare?
>javac triangle.java
triangle.java:5: error: Triangle is not abstract and does not override abstract
method getFigureType() in Figure
public class Triangle extends FigureBH
^
1 error
Il messaggio di errore del compilatore è alquanto eloquente: "Triangle non è una classe astratta e non sovrascrive il metodo astratto getFigureType() in Figure"
La classe EquiTriangle rappresenta un triangolo equilatero. Non commento il sorgente ad eccezione del fatto che il costruttore di questa classe accetta un solo argomento: la misura del lato del triangolo. Esso viene passato al costruttore della classe base assieme al secondo argomento che la classe base si aspetta: la altezza del triangolo.
Questo valore viene determinato col teorema di Pitagora.
Questa classe rappresenta un poligono regolare con n lati con n che può variare da un minimo di tre fino all'infinito, almeno teoricamente. In realtà, poichè il numero di lati viene memorizzato in una variabile di tipo int il limite superiore è di 231-1.
Se il numero di lati è pari a tre otteniamo un triangolo equilatero mentre se il numero di lati è pari a quattro otteniamo un quadrato. Queste due figure sono già descritte dalle classi specializzate viste in precedenza ma possono essere rappresentate anche dalla classe Polygon poichè i dati da passare al costruttore per descriverli sono diversi dalle classe specifiche.
Un poligono regolare viene descritto con due argomenti al costruttore:
r) n)Il costruttore prende come argomenti i due dati visti più sopra; uno di essi viene passato al cosruttore della classe base mentre l'altro viene memorizzato in un membro dati privato:
I quattro metodi astratti devono obbligatoriamente essere definiti essendo questa una classe non astratta.
Benchè questo metodo possa semplicemente ritornare la stringa "poligono
regolare con n lati", i poligoni regolari fino a 20 lati hanno un nome specifico; per esempio se n=5 il poligono si chiama pentagono.
Pertanto, questo metodo viene scritto in modo da ritornare il nome specifico del poligono: tutti i nomi specifici vengono memorizzati in una array a partire dal indice ZERO che ritorna la stringa del triangolo equilatero. Il numero di lati diminuito di tre unità rappresenta quindi l'indice della array di nomi da restituire.
Se il numero di lati è maggiore di 20, verrà restituita la stringa "poligono
regolare con n lati":
La formula per calcolare il perimetro di un poligono regolare è: Pn = 2nπ sin( 180°/n) e possiamo usare il metodo statico Math.sin per ottenere il seno di un angolo; unica accortezza è che il metodo sin vuole come argomento un angolo espresso in radianti e non in gradi sessagesimali. Poichè π=180° il metodo sarà scritto come segue:
Per calcolare l'area di un poligono regolare è sufficiente moltiplicare per n l'area dei triangoli isosceli che lo compongono. Quindi, dato che tali triangoli hanno come base un lato e come altezza l'apotema, si può anche assumere che la base di tutti i triangoli isosceli è il perimetro stesso e la loro altezza è l'apotema:
Il metodo getInfo restituisce una array di quattro elementi che contengono:
Vi sono altri metodi concreti definiti nella classe Polygon; uno di essi, il metodo getApotema, dobbiamo assolutamente definirlo poichè usato nel calcolo del perimetro.
Gli altri metodi non sono obbligatori ma sono definiti per convenienza e restituiscono:
I metodi sono di facile lettura; si tratta semplicemente di applicare le formule matematiche per calcolarne la misura.
Questa è la classe principale della applicazione, quella che contiene il metodo main. La classe principale si occupa di poche operazioni: di norma questa classe interpreta la command-line ed istanzia la classe specializzata che esegue l'operazione richiesta. Proprio questi sono i compiti della classe Geometry.
Non commenterò ogni singolo metodo della classe Geometry in quanto sono piuttosto facili. Come ricorderete, il comando ha la seguente sintassi;
> java -ea Geometry [OPTIONS] SHAPE base [height | sides]
ed un breve riassunto dei metodi della classe, comunque è doveroso:
main interpreta la command line ed istanzia la classe specializzata che gestisce la figura geometrica a seconda del argomento SHAPE. Figure, la classe base astratta printInfo che stampa le info sulla figura geometrica specificata parseCmdLine esegue la effettiva interpretazione della cmdline e restituisce la stringa che descrive la forma geometrica da elaborare printUsage stampa la sintassi del comandoInfine, compiliamo tutti i sorgenti e proviamo il programmino:
javatutor1\projects\geometry>javac *.java javatutor1\projects\geometry>java -ea Geometry -h Usage: java -ea Geometry [OPTIONS] SHAPE base [height | sides] -h[ITEM]: display help, ITEM can be: SHAPE or COLOR javatutor1\projects\geometry>java -ea Geometry -hSHAPE SHAPE is one of: RECT a rectangle QUAD a square TRIA a triangle EQUI an equilateral triangle PENTA a pentagon ESA a hexagon ETTA a ectagon OTTA a octagon ENNA a ennagon DECA a decagon ELLI a ellipse CIRC a circle POLI a regular polygon javatutor1\projects\geometry>java -ea Geometry RECT 96.3 41.4 Geometric figure is: Rectangle (96.3,41.4) Figure type: Rectangle b=96,3 h=41,4 P=275,4 A=3986,8 javatutor1\projects\geometry>java -ea Geometry RECT 233 144 Geometric figure is: Rectangle (233.0,144.0) Figure type: Rectangle b=233,0 h=144,0 P=754,0 A=33552,0 The figure is a rectangle with golden proportions javatutor1\projects\geometry>java -ea Geometry ELLI 123 78 Geometric figure is: Ellipse(123.0,78.0) Figure type: Ellipse ha=123,0 va=78,0 P=647,1 A=7535,1 javatutor1\projects\geometry>java -ea Geometry PENTA 69.26 Geometric figure is: Polygon (69.2,5) Figure type: Pentagon r=69,2 n=5 a=56,0 l=81,3 P=406,7 A=11385,7
Notate che tutti i valori vengono arrotondati ad un solo decimale: questo perchè i metodi printInfo e getInfo restituiscono stringhe di valori ottenuti con i metodi statici realToString che, come visto in I metodi statici, arrotondano il risultato ad un solo decimale.
Se io vi dicessi: "prendete carta e matita e disegnate un rettangolo" voi come lo disegnereste? Immagino simile al rettangolo verde che vedete qui sotto:
| | |
Ma anche quello giallo è un rettangolo ed anche quello rosso lo è. Eppure, inconsciamente lo disegnamo con le proporzioni di quello verde.
Questo è perfettamente normale poichè il nostro cervello considera il rettangolo verde ben proporzionato. Ed è proprio così: le dimensioni della base e della altezza del rettangolo verde sono, rispettivamente, 233 e 144 ed il loro rapporto è di circa 1,618 che corrisponde alla cosidetta Sezione aurea.
Ora noi vogliamo sapere, nel nostro programmino, se l'immagine da elaborare sia un rettangolo e, se lo è, se le sue proporzioni rientrano nella sezione aurea il cui valore è circa 1,6180339887... (si tratta di un numero irrazionale). Pertanto, nel metodo main, dopo aver stampato le informazioni sulla figura geometrica inseriremo due condizioni:
Ma come facciamo a stabilire se la variabile fig che è di tipo Figure (cioè di classe base astratta) è una istanza di Rectangle? Risposta: usando l'operatore instanceof!
Il codice che esegue questa verifica è il seguente:
L'operatore instanceof restituisce TRUE se l'istanza dell'oggetto il cui riferimento è di tipo classe base è del tipo derivato specificato a destra dell'operatore. Con tipo derivato non si intende soltanto la classe che implementa effettivamente l'istanza ma anche tutte le sue classi genitrici. Quindi Rectangle è una istanza di FigureBH? Assolutamente SI!
Notate anche che i metodi richiamati per ottenere la base e l'altezza della figura (rispettivamente, getBase e getHeight) non sono definiti in Figure e quindi è necessario eseguire un cast esplicito della variabile fig dalla classe base a Rectangle (andrebbe bene anche in FigureBH):
Cosa succede se eseguo un cast esplicito da una figura che non è del tipo consono? Per esempio, da un cerchio ad un triangolo?
Come potete ben immaginare, un costrutto come quello di cui sopra non può funzionare dal momento che la classe Circle non potrà mai essere considerata un triangolo. In effetti il costrutto sopra menzionato non funziona: tuttavia, il compilatore non emette alcun errore. Solo il runtime è a conoscenza del contenuto effettivo della variabile fig2 nel momento in cui avviene il cast pertanto la applicazione viene terminata a causa di una eccezione non prevista:
UNEXPECTED EXCEPTION: java.lang.ClassCastException java.lang.ClassCastException: class Circle cannot be cast to class Triangle ...
Tutte le classi che noi definiamo con la parola chiave class derivano automaticamente dalla classe Object che è il capostipite di tutte le classi Java, anche se non è necessario derivare esplicitamente da Object.
La classe Object è il capostipite di tutte le altre classi, non solo quelle definite da noi ma anche di tutte quelle della libreria standard di Java e di tutte le librerie di terze parti.
La classe Object definisce diversi metodi tra cui il metodo getClass che restituisce la effettiva classe di una specifica istanza. Abbiamo già usato questo metodo insieme al getName dell'oggetto restituito da getClass; possiamo stampare le stringa della classe di un qualsiasi oggetto mediante questo costrutto:
Un altro metodo molto utile della classe capostipite è il toString di cui tratterò qui sotto.
Uno dei metodi implementati in tutte le classi di questo piccolo programmino è il metodo toString. Esso è definito dalla classe Object e, nella sua versione base, restituisce una stringa composta dal nome della classe, il carattere at (la chiocciolina) ed una stringa esadecimale che rappresenta il codice hash dell'oggetto.
Benchè esso non sia mai stato utilizzato nè nella app Geometry nè nella app Geomart, il metodo toString si utilizza molto spesso nei messaggi di log che sono una tecnica di debugging molto usata. Il vantaggio del metodo toString consiste nel fatto che il metodo println lo richiama automaticamente per gli oggetti quando al metodo stesso viene passato come argomento la variabile stessa. Se il riferimento alla variabile è null, il metodo println stampa la stringa "null" anzichè sollevare una eccezione NullPointerException:
La implementazione del metodo toString nelle classi che gestiscono le figure geometriche è semplice: esso restituisce una stringa di caratteri composta dal nome della classe e dai suoi dati essenziali. Pe esempio, per un triangolo isoscele stampa la base e l'altezza del triangolo racchiuse tra due parentesi tonde:
Triangle(123.2, 76.4)
La documentazione completa in formato javadoc di questo progetto è disponibile al seguente link: Il progetto Geometry
Argomento precedente - Argomento successivo - Indice Generale