Java Tutorial - Parte 1 0.1
Un tutorial per esempi
Caricamento in corso...
Ricerca in corso...
Nessun risultato
La programmazione ad oggetti

Introduzione

In questo capitolo imparerete le altre due caratteristiche della programmazione ad oggetti:

  • la ereditarietà
  • il polimorfismo

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:

  • rettangoli
  • quadrati
  • triangoli isosceli
  • triangoli equilateri
  • cerchi
  • ellissi
  • poligoni regolari con un numero di lati a piacere

In una versione futura (se mai ci sarà) saranno gestiti anche:

  • rombi
  • parallelogrammi
  • trapezi

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.

I files del capitolo

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

La applicazione Geometry

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).

La sintassi della app Geometry

java -ea Geometry [OPTIONS] SHAPE base [height | sides]

Per il momento, l'unica opzione accettata dal programma è la:

  • -h[SHAPE]: visualizza help

l'argomento alla opzione -h può essere la stringa "SHAPE" che, se specificata, visualizza l'elenco delle forme geometriche gestite dal programma. Esse sono:

  • RECT rettangolo: gli argomenti base ed height sono obbligatori
  • QUAD quadrato; l'argomento height, se specificato, viene ignorato poichè per un quadrato la altezza è uguale alla base
  • TRIA triangolo isoscele: gli argomenti base ed height sono obbligatori
  • EQUI triangolo equilatero: l'argomento base rappresenta il lato del triangolo, l'argomento height, se specificato, viene ignorato
  • PENTA pentagono regolare: l'argomento base è la dimensione del raggio del cerchio circoscritto nel poligono
  • ESA esagono regolare: l'argomento base è la dimensione del raggio del cerchio circoscritto nel poligono
  • ETTA ettagono regolare (7 lati): l'argomento base è la dimensione del raggio del cerchio circoscritto nel poligono
  • OTTA ottagono regolare (8 lati): l'argomento base è la dimensione del raggio del cerchio circoscritto nel poligono
  • ENNA ennagono regolare (9 lati): l'argomento base è la dimensione del raggio del cerchio circoscritto nel poligono
  • DECA decagono regolare (10 lati): l'argomento base è la dimensione del raggio del cerchio circoscritto nel poligono
  • CIRC cerchio: l'argomento 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
  • POLI: un poligono regolare con 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.

Un primo approccio

Aprite il file sorgente rect01.java nel vostro editor preferito e commentiamo il listato. La organizzazione della classe Rectangle è la seguente:

class Rectangle
{
private float base, height;
public Rectangle( float b, float h ) throws IllegalArgumentException
public float getPerimeter()
public float getArea()
public String getFigureType()
public void printInfo()

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.

I metodi

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'area

La classe Square

Come già accennato, possiamo facilmente notare che il quadrato altro non è che una particolare forma di rettangolo:

  • il parametro base coincide con il parametro altezza
  • le formule per calcolare area e perimetro sono le stesse di quelle del 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.

class Square extends Rectangle
{
// costruttore
public Square( float side ) { ... }
// metodo sovrascritto
@Override
public String getFigureType()
{
return "Square";
}
}

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 nelle derivate

public Square( float side )
{
super( side, side );
}

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 ).

Da notare che la istruzione super() DEVE essere la prima istruzione nel corpo del costruttore di una derivata: non è possibile anteporre nulla a questa istruzione.

I metodi sovrascritti

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:

  • il tipo di figura; esso viene restituito dal metodo getFigureType
  • la base e l'altezza della figura: qui si potrebbe obiettare che esse coincidono e che si potrebbe semplicemente stampare la misura del lato
  • la misura di area e perimetro: che viene restituita dai relativi metodi i quali, come abbiamo notato, sono corretti

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":

@Override
public String getFigureType()
{
return "Square";
}

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:

  • avere lo stesso nome
  • avere lo stesso numero e tipo di argomenti
  • avere lo stesso attributo di accesso o uno più permissivo

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.

Il metodo main

Ora scriviamo il metodo principale main in modo che il programma possa accettare uno o due argomenti sulla cmdline:

  • se viene specificato un solo argomento esso è il lato di un quadrato e quindi istanzieremo un oggetto di classe Square
  • se vengono specificati almeno due argomenti essi sono la base e l'altezza di un rettangolo e quindi istanzieremo un oggetto di classe Rectangle
public static void main( String[] args )
{
try {
float b = 0, h = 0;
if ( args.length >= 2 ) {
h = Float.parseFloat( args[1] );
}
if ( args.length >= 1 ) {
b = Float.parseFloat( args[0] );
}
Rectangle fig = null;
if ( h > 0 ) {
fig = new Rectangle( b, h );
}
else {
fig = new Square( b );
}
fig.printInfo();
}
catch( ... )
}

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:

Rectangle fig = null;

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.

Affinchè si inneschi il meccanismo del polimorfismo è necessario che la variabile di riferimento sia di tipo classe base. Il polimorfismo si innesca solo per gli oggetti di tipo derivato dalla classe della variabile e solo per i metodi sovrascritti.

Il metodo printInfo

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:

public void printInfo()
{
System.out.println( ""Figure type: " + getFigureType());
System.out.println( " l=" + base );
System.out.println( " P=" + getPerimeter());
System.out.println( " A=" + getArea());
}

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:

class Rectangle
{
//private float base, height;
protected float base, height;

ricompilate ed eseguite il programma:

>java -ea Rectangle 123
Figure Type: Square
   l=123.0
   P=492.0
   A=15129.0

Le classi per i triangoli

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.

La classe base astratta

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:

  • il cerchio potrebbe essere una forma speciale di ellisse quindi la classe Circle potrebbe derivare da Ellipse
  • il triangolo equilatero potrebbe essere una forma speciale di triangolo isoscele quindi la classe 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
  • i poligoni regolari non hanno nulla in comune con le altre forme: i dati da fornire per gestire un poligono regolare sono il raggio del cerchio circoscritto (o inscritto) ed il numero di lati del poligono

Sembrerebbe quindi che nel main ci servano almeno quattro variabili per gestire figure così diverse tra loro:

Rectangle fig1; // gestisce anche la derivata Square
Ellipse fig2; // gestisce anche la derivata Circle
Triangle fig3; // gestisce anche la derivata EquiTriangle
Polygon fig4;

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 lati

Ma 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:

  • l'unico dato condiviso tra tutte le figure è il numero di lati dei poligoni; per la ellissi ed il cerchio, che non hanno lati, impostiamo convenzionalmente questo dato a -1
  • non possiamo assolutamente istanziare oggetti di tipo Figure perchè che tipo di figura sarebbe? di nessun tipo, solo una derivata rappresenta effettivamente una figura geometrica

Ecco 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:

abstract class MyClass
{
// metodo astratto: non ha corpo e termina con punto-e-virgola
public abstract method_1();
// metodo concreto
public void method_2()
{ ... }
// metodo statico
public static void method_3()
{ ... }
}

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.

Il costruttore astratto

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:

public abstract class Figure
{
// il numero di lati della figura
protected int numSides;
public Figure( int numSides )
{
this.numSides = numSides;
}
...

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.

I metodi astratti

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:

public abstract String getFigureType();
public abstract float getPerimeter();
public abstract float getArea();
public abstract String[] getInfo();

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.

I metodi concreti (non astratti)

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:

public void printInfo()
{
System.out.println( "Figure type: " + getFigureType());
String[] info = getInfo();
for ( String item : info ) {
System.out.println( item );
}
System.out.println( " P=" + getPerimeter());
System.out.println( " A=" + getArea());
}

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.

I metodi statici

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à:

abstract class Base
{
public static void staticMethod()
{ System.out.println("base"); }
}
class Derived extends Base
{
public static void staticMethod()
{ System.out.println("derived"); }
}
class Main
{
public static void main( String[] args )
{
Base var = new Derived();
Base.staticMethod(); // prints "base"
Derived.staticMethod(); // prints "derived"
var.staticMethod(); // prints "base"
}
}

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.

La classe Ellipse

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):

class Ellipse extends Figure
{
protected float horizAxis, vertAxis;
public Ellipse( float ha, float va ) throws IllegalArgumentException
{
super( -1 );
if ( ha <= 0 || va <= 0 ) {
throw new IllegalArgumentException( "invalid arguments for \'ellipse\': "
+ "axis cannot be ZERO or negative" );
}
horizAxis = ha;
vertAxis = 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.

La classe Circle

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:

class Circle extends Ellipse
{
private float radius;
public Circle( float radius )
{
super( 1, 1 );
if ( radius <= 0 ) {
throw new IllegalArgumentException( "invalid arguments for \'circle\' "
+ "radius cannot be ZERO or negative" );
}
this.radius = radius;
horizAxis = radius * 2;
vertAxis = radius * 2;
}

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:

super( radius*2, radius*2 );

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:

@Override
public String getFigureType()
... omissis ...
@Override
public float getPerimeter()
... omissis ...
@Override
public float getArea()
... omissis ...
@Override
public String[] getInfo()
... omissis ...

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)

La classe FigureBH

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.

abstract class FigureBH extends Figure
{
protected float base, height;
public FigureBH( int numSides, float base, float height )
{
super( numSides );
this.base = base;
this.height = height;
if ( base <= 0 || height <= 0 ) {
throw new IllegalArgumentException( "invalid data for a \'"
+ getClass().getName() + "\' figure type: base and height cannot be negative" );
}
}

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:

abstract class FigureBH extends Figure

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:

public float getBase()
{
return base;
}
public float getHeight()
{
return height;
}

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.

La classe Triangle

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

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).

class Triangle extends FigureBH
{
protected float hypo;
public Triangle( float b, float h ) throws IllegalArgumentException
{
super( 3, b, h );
// determina la lunghezza dei lati uguali
hypo = (float) Math.sqrt( Math.pow( base/2, 2.0 ) + Math.pow( height, 2.0));
}

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.

I metodi astratti

Questa classe descrive una figura geometrica reale, quindi non è astratta: dobbiamo necessariamente definire i metodi che nella classe base Figure sono stati dichiarati astratti:

public float getPerimeter()
... omissis ...
public float getArea()
... omissis ...
public String getFigureType()
... omissis ...
public String[] getInfo()
{
String[] info = new String[3];
info[0] = new String( " b=" + base );
info[1] = new String( " h=" + height );
info[2] = new String( " i=" + hypo );
return info;
}

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.

La classe Polygon

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:

  • la misura del raggio del cerchio circoscritto (r)
  • il numero di lati del poligono (n)

Il costruttore

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:

class Polygon extends Figure
{
private float radius;
public Polygon( float radius, int sides )
{
super( sides );
this.radius = radius;
if ( radius <= 0 || sides < 3 ) {
throw new IllegalArgumentException( "invalid arguments for \'Poligon\' "
+ "figure type: radius must be greater than ZERO and num sides greater than 2" );
}
}

I metodi astratti

I quattro metodi astratti devono obbligatoriamente essere definiti essendo questa una classe non astratta.

Il metodo getFigureType

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":

public String getFigureType()
{
String[] types = { "Equilateral triangle", "Square", "Pentagon",
"Hexagon", "Heptagon", "Octagon", "Ennagon", "Decagon",
"hendecagon", "Dodecagon", "Tridecago", "Tetradecagon",
"Pentadecagon", "Hexadecagon", "Heptadecagon", "Octadecagon",
"Ennedecagon", "Icosagon" };
String s = new String( "Polygon with " + numSides + " sides" );
if ( numSides <= 20 ) {
s = types[numSides - 3];
}
return s;
}

Il metodo getPerimeter

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:

public float getPerimeter()
{
// P = 2nr sin( PI / n )
double p = 2 * numSides * radius * Math.sin( Math.PI / numSides );
return (float) p;
}

Il metodo getArea

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:

public float getArea()
{
// A = nla / 2 = Pa / 2
float a = getPerimeter() * getApotema() / 2;
return a;
}

Il metodo getInfo

Il metodo getInfo restituisce una array di quattro elementi che contengono:

  • la misura del raggio del cerchio inscritto (r)
  • la misura dell'apotema (a)
  • la misura del lato del poligono (l)
  • il numero di lati del poligono (n)
public String[] getInfo()
{
String[] info = new String[4];
info[0] = new String( " r=" + radius );
info[1] = new String( " n=" + numSides );
info[2] = new String( " a=" + getApotema());
info[3] = new String( " l=" + getSide());
return info;
}

Altri metodi concreti

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:

  • la misura dell raggio del cerchio inscritto
  • la misura del lato del poligono
  • il numero di lati del poligono

I metodi sono di facile lettura; si tratta semplicemente di applicare le formule matematiche per calcolarne la misura.

La classe Geometry

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:

  • il metodo main interpreta la command line ed istanzia la classe specializzata che gestisce la figura geometrica a seconda del argomento SHAPE.
  • la variabile che contiene l'oggetto della figura geometrica è di tipo Figure, la classe base astratta
  • dopo l'instanziazione viene richiamato il metodo printInfo che stampa le info sulla figura geometrica specificata
  • il metodo parseCmdLine esegue la effettiva interpretazione della cmdline e restituisce la stringa che descrive la forma geometrica da elaborare
  • il metodo printUsage stampa la sintassi del comando

Infine, 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.

L'operatore instanceof

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:

  • se la figura è un rettangolo
  • se il rapporto tra la base e l'altezza è compreso tra 1,617 e 1.619

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:

public class Geometry
{
... omissis ...
public static void main( String[] args )
{
... omissis ...
fig.printInfo();
// dimostrazione del operatore 'instanceof'
if ( fig instanceof Rectangle ) {
Rectangle rect = (Rectangle) fig;
float b = Math.max( rect.getBase(), rect.getHeight());
float h = Math.min( rect.getBase(), rect.getHeight());
if ( b / h > 1.617 && b / h < 1.619 ) {
System.out.println( "The figure is a rectangle with golden proportions" );
}
}
... omissis ...

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):

Rectangle rect = (Rectangle) fig;

Cosa succede se eseguo un cast esplicito da una figura che non è del tipo consono? Per esempio, da un cerchio ad un triangolo?

Figure fig2 = new Circle( 120 );
Triangle tria = (Triangle) fig2;

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 ...

La classe Object

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:

Figure fig = new Rectangle( 212, 198 );
String name = fig.getClass().getName(); // restituisce "Rectangle"

Un altro metodo molto utile della classe capostipite è il toString di cui tratterò qui sotto.

Il metodo toString

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:

class Main
{
... data members ...
public void main( String[] args )
{
Figure fig; // non istanziato, è 'null'
// solleva una NullPointerException a runtime
fig.printInfo();
// il seguente NON solleva una eccezione ma stampa la stringa "null"
System.out.println( "Figure=" + fig );
}
}

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