edutecnica
 


Classi e oggetti in Java

E' noto che Java è un linguaggio orientato agli oggetti, nel suo uso viene, dunque, usato un approccio diverso ai problemi di programmazione standard come nel caso di C o Pascal; in quest'ultimo caso viene usato un approccio funzionale o procedurale:un problema, viene decomposto in sottoproblemi; viene così prodotto un insieme di procedure o funzioni che comunicano tramite parametri o variabili globali.

L'impostazione della OOP, prevede, diversamente, l'identificazione di una serie di entita indicati come oggetti, stabilendone delle reciproche relazioni che vanno a costituire una gerarchia fra questi oggetti.

Il concetto di oggetto in informatica è legato alla nozione di incapsulamento di dati indicati col nome di attributi e di procedure per elaborarli indicate col nome di metodi.

Un oggetto può dunque essere considerato un'entità software composta da :
attributi=variabili che descrivono lo stato dell'oggetto e in tal modo lo rappresentano.
Poi abbiamo i metodi=sottoprogrammi che operano implicitamente sugli attributi.

Il comportamento di un oggetto è descritto dalla sua classe. La classe viene considerata come il 'calco', la 'matrice' da cui gli oggetti possono essere riprodotti (istanziati). La sintassi generale usata per dichiarare ed istanziare un oggetto l'abbiamo già intravista con l'uso di vettori e matrici:

nomeClasse nomeOggetto //dichiarazione
nomeOggetto=new nomeClasse(parametri) //allocazione


o in forma più sintetica

nomeClasse nomeOggetto= new nomeClasse(parametri)

la forma più generale un programma in Java (che prevede obbligatoriamente la presenza di almeno una classe) ha la seguente struttura:

eventuali dichiarazioni import
class nomeclasse {
public static void main(String[] args) {
attributi;
.. istruzioni;
}//fine main
eventuali metodi costruttori;
altri eventuali metodi;

}//fine classe
atre eventuali classi;


Si nota la presenza di eventuali metodi costruttori che hanno il compito di inizializzare lo stato dell'oggetto al momento della sua creazione.

La forma generale per l'invocazione di un metodo è la seguente:

nomeOggetto.nomeMetodo(parametri)

L'esempio più semplice potrebbe essere un programma che istanzia una classe A passando due interi che devono essere moltiplicati. L'oggetto creato deve restituire il prodotto dei due operandi.

class PRO {
public static void main (String args []) {
A a = new A(5, 2);
System.out.println(a.getA());
}//fine main
}// fine classe

class A {
private int x,y,i;
A(int a, int b) {
     x=a;y=b;
     mul();
}//costruttore
private void mul(){i=x*y;}
public int getA(){return i;}
}//fine classe A


Come si vede associamo la classe A alla classe PRO che è dotata del metodo main che istanzia e fa uso di un oggetto a di classe A.

Si usa dire in questo caso che la classe PRO è cliente della classe A , la quale è a sua volta, servitrice della classe PRO.

La definizione della classe A secondo la definizione UML è la seguente:

Possiamo constatare come sia impossibile agire direttamente sugli attributi (privati) dell'oggetto a della classe A, direttamente dal main() tramite ad es. l'istruzione:
System.out.println(a.i);
Questa operazione sarebbe possibile solo se l'attributo i fosse public.

class PRO {
public static void main (String args []) {
     A a = new A(1, 2);
     System.out.println(a.getA());
     System.out.println(a.i);
}//fine main
}// fine classe
class A {
private int x,y;
public int i;
A(int a, int b) {x=a;y=b;mul();}//costruttore
private void mul(){i=x*y;}
public int getA(){return i;}
}//fine classe A


ma questo contraddice uno dei principi della OOP, l' information hiding (occultamento dell'informazione) che consiste nel tenere il più possibile nascosto lo stato dell'oggetto.
Infatti, se per consuetudine, consentiamo l'accesso diretto alla stato dell'oggetto dovremo riscrivere tutti i fruitori (i clienti) di quell'oggetto, qualora in futuro decidessimo di cambiare la rappresentazione dell'oggetto stesso.
Ad. es. se decidessimo che l'attributo i diventa di tipo boolean, ogni volta che ne nel programma troviamo a.i=6; dovremmo scrivere a.i=true; se invece l'attributo è privato ed è accessibile solo tramite i metodi dell'oggetto come a.getA(); il problema non si pone.

La regola dell'information hiding afferma che l'unico modo per modificare o interrogare lo stato di un oggetto è quello di servirsi dei suoi metodi. Ma bisogna prestare attenzione; ad es.

private void mul(){i=x*y;}

Come si può notare in Java possono essere definiti privati anche i metodi.

Possiamo perciò dire:

un attributo (variabile,metodo,o classe) public è visibile indistintamente in tutte le parti del programma, esempio se nella classe C hai una variabile 'a' contrassegnata con public allora puoi accedere ad essa facendo C.a .

private: l'attributo è visibile SOLO all'interno della classe in cui è dichiarato, quindi NON puoi accedervi dall'esterno direttamente con la "dot form" ovvero con il punto C.a .

protected: è come private, con la differenza che se dichiari un attributo 'a' protected nella classe C esso sarà visibile anche nelle classi che estendono C (extends C). Cioè nelle classi che ereditano C (classi derivate).

Ereditarietà

Nell'esempio precedente, viene restituito il prodotto fra due interi; idealmente possiamo pensare al calcolo dell'area di un rettangolo. Ovviamente, se aggiungiamo una seconda classe V che deve eseguire la moltiplicazione di 3 interi possiamo pensare al calcolo del volume di un parallelepipedo. Il volume del parallelepipedo si misura facendo volume =larghezza x lunghezza x altezza .
Se i primi 2 numeri della serie costituiscono i lati della base, affideremo alla classe A il calcolo dell'area che una volta restituita verrà elaborata dalla classe V per restituire il volume del parallelepipedo, secondo la formula: volume= area x altezza

class PRO{
public static void main (String args []) {
     V v = new V(5, 2, 3);
     System.out.println(v.getV());
}//fine main
}// fine classe
class A {
private int x,y,i;
A(int a, int b) {x=a;y=b;mul();}//costruttore
private void mul(){i=x*y;}
public int getA(){return i;}
}//fine classe A
class V extends A {
private int j;
V(int a, int b, int c) { //invocazione costruttore superclasse
     super(a,b);
     j = getA()*c;
}//fine costruttore
public int getV() {return j;}
}//fine classe V

Lo schema UML dell'operazione di derivazione effettuata è qui rappresentata.



La sottoclasse V eredita dalla superclasse genitrice A attributi e metodi, ma per farsi restituire l'area di base dalla superclasse il costruttore V(int a, int b, int c) deve invocare il costruttore A(int a, int b) tramite la chiamata:

super(a,b);

In modo tale che possa poi essere eseguito il metodo mul() e l'operazione i=x*y; quest'ultimo attributo verrà poi restituito all'interno del costruttore V tramite l'istruzione:

j = getA()*c;

Se dovesse essere omessa la chiamata super(a,b) o se super fosse invocato senza parametri, il compilatore genera un errore, perché A(int a, int b) si aspetta dei valori in ingresso.
Da qui si desume che la classe V, eredita effettivamente metodi ed attributi di A.

Questo approccio non è l'unico. Nell'esempio seguente, A, non ha un costruttore:

class PRO{
public static void main (String args []) {
V v = new V(5, 2, 3);
System.out.println(v.getV());
}//fine main
}// fine classe
class A {
private int x,y,i;
public void setA(int a, int b){x=a;y=b;mul();}
private void mul(){i=x*y;}
public int getA(){return i;}
}//fine classe A
class V extends A {
private int j;
V(int a, int b, int c) { setA(a,b); j = getA()*c; }//fine costruttore
public int getV() {return j;}
}//fine classe V


Se all'interno di V togliessimo la chiamata al metodo pubblico setA(a,b); il programma funzionerebbe ugualmente restituendo 0 (zero).

I costruttori, infatti, chiedono lo spazio in memoria per l'oggetto da creare e lo inizializzano.
La loro importanza sta nel fatto che vengono coinvolti nell'operazione di istanziazione:

new nomeClasse(eventuali parametri);

Implicitamente viene sempre effettuata una chiamata super() al costruttore della classe genitrice, quindi, in questo secondo caso, l'istruzione:

A();//costruttore di A

viene effettivamente eseguita.
La prima cosa che viene effettuata, quando si crea un oggetto è la chiamata super() al costruttore della classe genitrice, questo, provoca a catena le chiamate ai costruttori di tutte le superclassi di quella istanziata.
Se in una classe non vi è costruttore, viene comunque, allocato spazio in memoria per l'oggetto e per i suoi attributi inizializzandoli ai valori di default ( 0 per i numeri, false per i boolean e null per i riferimenti).
Se esiste, viene prima eseguito il costruttore della classe genitrice e poi quello della classe discendente.

Attenzione! Se il costruttore si aspetta dei parametri, bisogna farglieli arrivare, se no come si è detto, il compilatore genera un errore.

Alla fine, viene restituito al programma chiamante un puntatore che identifica l'oggetto creato.

Nel nostro ultimo caso se omettiamo l'istruzione setA(..) il costruttore di A viene attivato ugualmente ma gli attributi x,y,i vengono posti a 0; ecco perché il programma restituisce 0.

Si deduce che: non siamo obbligati ad usare i costruttori, ma il fatto di usarli, favorisce l'azione di portare da subito l'oggetto ad uno stato valido.

Polimorfismo

In Java esistono due forme di polimorfismo:

Prima forma: implica l'uso dell'ereditarietà; e consente ad oggetti appartenenti a sottoclassi differenti ereditino lo steso metodo dalla superclasse.

Nel seguente schema le classi derivate I ed S ereditano i metodi della superclasse T.



La classe T è dotata del metodo isT(..) che verifica se i lati di un triangolo soddisfano alla condizione di esistenza del triangolo stesso.
La classe peri(..) restituisce il perimetro del triangolo.
Possono essere valutati solo triangoli scaleni (S) ed isosceli (I).
Una volta istanziato un oggetto di classe S o di classe I se il triangolo esiste viene restituito il perimetro.
Se il triangolo non esiste, viene restituito 0.

class triangolo{
public static void main (String args []) {
     S s=new S(2,1,3);//scaleno
     I i=new I(3,4);//isoscele lati=3,3,4
     System.out.println(s.getS());
     System.out.println(i.getI());
}//fine main
}// fine classe
class T{
protected boolean isT(int x,int y,int z) {
     if (((x+y)>z) && ((x+z)>y) && ((y+z)>x))return true;
     else return false;
}
protected int peri(int h,int k,int w){return h+k+w;}
}//fine classe T
class S extends T {//scaleno
private int a,b,c,p;
S(int A,int B,int C){//costruttore
     a=A;b=B;c=C;
     if(isT(a,b,c)==true)p=peri(a,b,c);
     else p=0;
}//fine costruttore
public int getS() {return p;}
}//fine classe S
class I extends T {//scaleno
private int a,b,p;
I(int A,int B){//costruttore
     a=A;b=B;
     if(isT(a,a,b)==true)p=peri(a,a,b);
     else p=0;
}//fine costruttore
public int getI() {return p;}
}//fine classe I


L'output di questo listato sarà:
0
10

questo perché il primo triangolo (quello scaleno) non esiste.

Si nota come i costruttori di S ed I facciano uso dei metodi ereditati. L'uso:

protected boolean isT(int x,int y,int z)
protected int peri(int h,int k,int w)


della clausola protected, permette solo alle due sottoclassi di fare uso dei due metodi.

Seconda forma:  lo stesso nome per un metodo rappresenta elaborazioni differenti dichiarate in sottoclassi e quindi durante l'esecuzione del programma, in oggetti diversi.
Metodi contenuti in diverse classi della gerarchia possono avere lo stesso identificatore, gli stessi parametri di ingresso (per numero e tipo di dato) e restituire un risultato dello stesso tipo di dato, ma possono differire per l'elaborazione eseguita.

Se un metodo di una sottoclasse ridefinisce un metodo con la stessa firma dichiarato nella superclasse si dice che i due metodi sono sovrapposti (overriding).

Se due metodi di una classe restituiscono lo stesso tipo, hanno lo stesso identificatore ma hanno segnatura (dei parametri diversa) si parla di metodi sovraccarichi (overload).

In questa rielaborazione del programma precedente i due costruttori della classe SI sono overloaded:

class triangolo{
public static void main (String args []) {
     SI s=new SI(2,1,3);//scaleno
     SI i=new SI(3,4);//isoscele lati=3,3,4
     System.out.println(s.getSI());
     System.out.println(i.getSI());
}//fine main
}// fine classe
class T{//classe Triangolo
protected boolean isT(int x,int y,int z) {
     if (((x+y)>z) && ((x+z)>y) && ((y+z)>x))return true;
     else return false;
}
protected int peri(int h,int k,int w){return h+k+w;}
}//fine classe T
class SI extends T {
private int a,b,c,p;
SI(int A,int B,int C){//costruttore scaleno
a=A;b=B;c=C;
     if(isT(a,b,c)==true)p=peri(a,b,c);
     else p=0;
}//fine costruttore
SI(int A,int B){//costruttore isoscele
a=A;b=B;
     if(isT(a,a,b)==true)p=peri(a,a,b);
     else p=0;
}//fine costruttore
int getSI() {return p;}
}//fine classe SI


Esistono due costruttori SI(..) uno con 3 parametri in ingresso per il triangolo scaleno e uno con 2 parametri per il triangolo isoscele, quindi con diversa segnatura. Il programma eseguirà il metodo invocato da una chiamata con i parametri corrispondenti:questo è un esempio di overload di metodi.

Nel seguente esempio, possiamo invece, constatare un override di metodi:

class triangolo{
public static void main (String args []) {
     E e=new E(2);//equilatero
     System.out.println(e.getE());
}//fine main
}// fine classe
class T{
protected boolean isT(int x,int y,int z) {
     if (((x+y)>z) && ((x+z)>y) && ((y+z)>x))return true;
     else return false;
}
protected int peri(int h,int k,int w){return h+k+w;}
}//fine classe T
class E extends T {//scaleno
private int a,p; E(int A){//costruttore
     a=A;
     if(isT(a,a,a)==true)p=peri(a,a,a);
     else p=0;
}//fine costruttore
public int peri(int z,int w,int h){ return z*3; }
public int getE() {return p;}
}//fine classe E


In questo esempio viene creato un triangolo equilatero 'e'

Attenzione! La visibilità non può diminuire nelle sottoclassi. Quindi nell'eventualità che nell'ultima classe derivata E ci si dimenticassi il modificatore public davanti al metodo peri(..):

class T{
. . .
protected int peri(int h,int k,int w){return h+k+w;}
}//fine classe T
class E extends T {//scaleno
. . .
int peri(int z,int w,int h){ return z*3; }
. . .
}//fine classe E


il compilatore genera un errore del tipo:
attempting to assign weaker access privileges
cioè, si sta cercando di assegnare al metodo ridefinito una visibilità minore di quello del metodo padre. Quando si ridefinisce un metodo, non è lecito diminuirne la visibilità. Il contrario si può sempre fare.

Riassumendo, diremo che il polimorfismo è un principio della programmazione ad oggetti che interviene in presenza di una gerarchia di classi offrendo la possibilità di usare lo stesso metodo (di una sopraClasse) applicandolo ad oggetti diversi nella gerarchia (delle sottoClassi).


oppure può ammettere lo stesso nome per indicare metodi diversi (nell'elaborazione) incapsulati in classi differenti.


Qui vengono schematizzate le differenze fra overload e override di metodi.


Ancora sui metodi statici

I metodi statici non hanno bisogno di fare nessun riferimento ad alcun oggetto di quella classe al contrario dei metodi non-statici.
La differenza può essere resa evidente dal seguente listato: dove il programma chiamante main non ha bisogno di istanziare alcun oggetto della classe C per far intervenire il metodoStatico() contenuto nella classe C stessa.

class main {
     public static void main (String[] args){
     C.metodoStatico();
     }//fine main
}//fine classe main
//----------------
class C{
     void metodoNonStatico(){//metodo
     System.out.println("metodoNONStatico:eseguito");
     metodoStatico();
     }//fine metodo metodoNonStatico
static void metodoStatico(){
System.out.println("metodoStatico:eseguito");
}//fine metodo metodoStatico
}//fine classe C


In altri termini per invocare un metodo m non-statico appartenente ad una classe C bisogna usare la notazione C.m() . L'istruzione:
C.metodoNonStatico();
genera un errore, perché per chiamare un metodo-non statico da un contesto statico (static void main) bisogna istanziare un oggetto. La versione corretta sarà:

class main {
     public static void main (String[] args){
         C.metodoStatico(); //B.metodoNonStatico();//ERRORE!!
         C c=new C();
         c.metodoNonStatico();
     }//fine main
}//fine classe main


l'output corrispondente è

metodoStatico:eseguito
metodoNONStatico:eseguito
metodoStatico:eseguito


il metodoNonStatico viene eseguito 2 volte la prima sulla chiamata C.metodoStatico() senza istanziazione dell'oggetto c la seconda sulla chiamata c.metodoNonStatico() dopo la creazione dell'oggetto c dato che il metodoNonStatico, invoca il metodoStatico contenuto nella classe C.




edutecnica