Giunti alla sesta puntata del nostro viaggio all’interno di Java, è ora di occuparci di una serie di concetti che renderanno le potenzialità del linguaggio estremamente più elevate rispetto a quelle che conosciamo adesso. Si tratta di concetti strettamente legati al mondo della programmazione ad oggetti. Per introdurli dobbiamo però fare un passo indietro, e parlare dei modificatori di visibilità.
Modificatori di visibilità e modificatore static
In tutti i linguaggi di programmazione ad oggetti esiste un concetto, chiamato scope delle variabili, che per i programmatori non esperti può rivelarsi ostico. Cercheremo pertanto ora di fare un minimo di chiarezza.
Abbiamo imparato fino ad ora a definire variabili e costanti di vario tipo, e abbiamo visto che in Java questo avviene essenzialmente in due modi:
– variabili dichiarate all’interno di metodi
– variabili dichiarate come dati di istanza
Abbiamo anche detto che se abbiamo una classe MyClass
che definisce un certo tipo di oggetti, tutti gli oggetti di quella classe avranno “al loro interno” le variabili dichiarate come dati di istanza, mentre le variabili dichiarate nei metodi sono le variabili che utilizziamo per poter implementare le funzionalità degli oggetti, ma in realtà non hanno nessuna particolare pretesa di far parte degli oggetti stessi, e quindi di descriverne in un certo senso lo status. Su tutti questi concetti possono intervenire due tipi di modificatori, i modificatori di visibilità e il modificatore static
.
Occupiamoci in primo luogo del modificatore static
. La sua funzione differisce a seconda che ci sia posto davanti ad una variabile o ad un metodo, come in questo codice:
public class MyClass{
// dato di istanza NON static
int numero1;
//dato di istanza static
static numero2;
// Il costruttore NON può essere static
public MyClass(){
}
// metodo static
public static void method(){}
}
Occupiamoci in primo luogo del modificatore static posto dinanzi ai dati di istanza. Esso fa in modo che la variabile sia una cosiddetta variabile di classe cioè che venga allocata una sola volta quando si alloca lo spazio per la classe MyClass e non per ogni oggetto, in parole povere non descrive lo stato dell’oggetto ma di una classe. Questo fa sì che il modo per accedervi sia il seguente:
MyClass myObject = MyClass();
// Accesso sbagliato alla variabile numero2, restituisce errore
myObject.numero2 = 3;
// Accesso corretto alla variabile numero2
MyClass.numero2 = 3;
Notate che gli stessi concetti possono essere applicati alle costanti (che ricordiamo essere espresse con il modificatore final
in Java) e in effetti questo è uno degli usi più comuni del modificatore static, la definizione di costanti che possano essere quindi accessibili in tutte le parti del programma senza dover dichiarare un oggetto atto a contenerle (operazione che, evidentemente, richiede l’uso di risorse e i programmatori sono in genere restii a utilizzare risorse quando questo si può evitare).
Per quanto riguarda i dati di istanza inoltre, in Java esistono tre diversi modificatori di visibilità e si tratta dei modificatori public
, private
, protected
. Si tratta di tre modifiche effettive, poiché se omettiamo modificatori di visibilità il compilatore assume che esso abbia una visibilità ancora differente da questi. Pur tuttavia per i nostri scopi non ci occuperemo delle piccole differenze che intercorrono tra questa visibilità e la visibilità protected (la più vicina delle tre).
La caratteristica fondamentale che dobbiamo considerare per questi modificatori di visibilità è che i dati di istanza dichiarati public sono direttamente accessibili con la sintassi
o MyClass.dato
(nel caso siano static).
Anche dall’esterno della classe medesima. Ciò significa che se scriviamo
public class MyClass{
private int dato;
...
}
// altra classe public, quindi altro file
public class MySecondClass{
public MySecondClass(){
MyClass myObject = new MyClass();
myObject.dato = 4;
}
}
il compilatore ci restituirà un errore dicendo che “dato” non ha visibilità public. Questo sistema garantisce quindi che solo ed esclusivamente un metodo della classe MyClass possa agire sulla variabile dato. Si tratta di un meccanismo fondamentale che permette ad una classe di rispettare i principi detti dell’incapsulamento secondo i quali è buona programmazione non permettere l’accesso diretto alle variabili da parte di altre classi quando questo non è fortemente necessario. Noterete che programmando avrete un irrefrenabile istinto a dichiarare public un gran numero di variabili, tutte quelle cui volete accedere in fase di esecuzione. Si tratta di una pratica fortemente sconsigliata, soprattutto perché rende meno portabile il codice. La soluzione standard per queste situazioni, in Java, è creare due metodi getXXXX
e setXXXX
che accedano alla variabile in questione, la quale la possiamo dichiarare private o protected (la differenza consiste nel fatto che le variabili protected sono accessibili direttamente da classi dello stesso package, quelle private no). Ad esempio, se abbiamo una variabile
potremmo scrivere i due metodi seguenti
public int getNumeroSoci(){
return numeroSoci;
}
public void setNumeroSoci(int value){
numeroSoci = value;
}
Si tratta di una metodologia di programmazione molto comune in Java, ampiamente usata anche per costruire il package delle classi standard (ricordo che quando scaricate il j2se c’è un file chiamato src.zip che contiene i sorgenti delle classi java del package standard e che potete rendervi conto di come sono frequenti questi metodi visionando la documentazione). Notate come i metodi sono stati dichiarati public, poiché in questo caso desidero che siano accessibili a tutte le classi, ma avrei potuto dichiararli protected se avessi voluto che fossero usati solo all’interno del package.
Ereditarietà e polimorfismo
Giunti a questo punto abbiamo i mezzi per comprendere due concetti fondamentali della programmazione Java, che arricchiscono a dismisura le possibilità che ci offre il linguaggio. Molto spesso infatti ci troviamo di fronte alla modellazione di un mondo per il quale è sconveniente scrivere una classe per ogni oggetto che contenga tutte le caratteristiche, poiché gli oggetti possono avere caratteristiche comuni o possono rispecchiare un naturale ordine gerarchico. Ereditarietà e polimorfismo ci aiutano molto in questo senso e si definiscono come segue:
1. ereditarietà è il meccanismo tramite il quale un oggetto è in una relazione logica con un altro nel tipo “è un”. Ad esempio, se modelliamo la classe cavallo
e la classe mammifero
, vien naturale sostenere “il cavallo è un mammifero” e quindi un oggetto cavallo ha tutte le caratteristiche del mammifero (notate che non è vero il contrario, non tutti i mammiferi son cavalli). In questo caso in Java dopo aver costruito la classe mammifero potremmo dichiarare la classe cavallo come segue
per indicare al compilatore che la classe che modella il generico cavallo estende le proprietà del generico mammifero. In questo caso si dice che Cavallo eredità da Mammifero. Più precisamente cavallo erediterà tutti i metodi e gli attributi non dichiarati private della classe mammifero, anche quelli che eventualmente mammifero ha a sua volta ereditato!
2. polimorfismo è il meccanismo tramite il quale una variabile può puntare a più di un tipo di oggetto e si ottiene essenzialmente in due modi
– tramite l’ereditarietà, cioè se dichiariamo (tornando all’esempio precedente)
Mammifero variabile1;
Cavallo variabile2 = new Cavallo()
variabile2 = variabile1;
stiamo compiendo operazioni valide. Siccome, inoltre, tutte le classi implicatamente ereditano da Object le variabili object possono puntare qualunque oggetto desideriamo.
– Tramite l’uso delle interfaccie. In Java una interfaccia è un modulo (motivo per il quale le classi non sono l’unico modulo in Java, vedi prima lezione) che fornisce solo delle costanti e dei metodi pubblici non ancora implementati che obbligatoriamente devono essere implementati da tutte le classi che implementano una determinata interfaccia. Una classe che dichiari di voler implementare un’interfaccia si presenta in questo modo
public class MyClass implements MyInterface
dove MyInterface ha questa struttura
public interface MyInterface{
//notate come il metodo non ha implementazione e che è seguito solo dal punto e virgola
public void myMethod();
}
Java inoltre fa largo uso di questa tecnica poiché nella libreria standard esistono un gran numero di interface delle quali ci occuperemmo diffusamente nella prossima lezione parlando di interfaccie grafiche utente (GUI: graphics user interface)
Deve essere immediatamente chiaro che l’uso corretto di questi due capisaldi della programmazione a oggetti (quindi non solo in Java) permette al programmatore non solo di risparmiare tempo, ma anche e soprattutto di rendere il suo codice più leggibile e facilmente modificabile. Se ad esempio abbiamo un certo numero n di classi che ereditano da una classe che chiamiamo Padre per fissare le idee ed esse hanno tutte un metodo in comune che vogliamo modificare, se esse non lo ereditano da Padre saremmo immancabilmente costretti a modificare n metodi. In caso contrario con una sola modifca potremmo propagare il nuovo codice a tutte le n classi! Immaginate che n valga 10, o 20 (cosa che può tranquillamente verificarsi perché come abbiamo detto anche i nipoti ereditano i metodi public) e avrete una misura del tempo che risparmiate anche per un solo metodo.
Questo inoltre ci consente di ampliare notevolmente l’uso che potevamo fare della libreria standard in Java. Con questi nuovi strumenti siamo in grado, all’occorrenza, di scrivere delle classi che estendono le già notevoli potenzialità di quelle standard. Tutto questo sarà lampante alla scrittura della prima Gui.
Eccezioni
L’ultima importante disquisizione teorica che dobbiamo affrontare, seppur sommariamente, per poter scrivere programmi Java sfruttandone le potenzialità riguarda la gestione degli errori. In Java dobbiamo distinguere due tipi di situazioni “anormali”: errori ed eccezioni (particolare non da poco, sono entrambi oggetti, poiché tutto in Java è un oggetto). Gli errori si verificano normalmente in fase di compilazione, e impediscono la terminazione della stessa. Fanno parte di questa categoria tutti gli errori di battitura del programmatore, ad esempio. Può inoltre verificarsi un errore in fase di esecuzione, per fare un altro esempio, se cerchiamo un file che non esiste o cose di questo genere. Le eccezioni rappresentano invece tutte le situazioni “anormali” in cui un determinato programma si può trovare che dovrebbero sempre essere gestite dal programmatore per evitare che l’applicazione crashi miseramente (tipicamente questa è la prima cosa da evitare, perché causa in genere la perdita dei dati su cui si sta lavorando). Se ad esempio richiediamo all’utente di inserire un intero e questi inserisce una stringa, è inammissibile che l’applicazione termini in maniera imprevista e dobbiamo preventivamente occuparcene.
Il meccanismo che ci mette a disposizione Java si compone di questi capi saldi
1. inseriamo all’interno all’interno di un blocco try{}
tutte le istruzioni che potrebbero generare un’eccezione
2. ogni blocco try
deve avere almeno un blocco catch immediatamente dopo di esso che gestisce una eccezione (e tutte le sue sottoclassi, per la gerarchia delle eccezioni si veda la documentazione ufficiale). Ad esempio questo blocco intercetta tutte le eccezioni
try{
//istruzioni che possono generare eccezioni
..
}
catch(Exception e){
System.out.println("Si è verificata un'eccezione:" +e)
}
poiché Exception
è la classe radice della gerarchia delle eccezioni Java. All’interno del blocco catch
inseriamo l’azione corrispondente che vogliamo il programma compia se si solleva l’eccezione. In questo caso stampiamo banalmente l’errore riportato, per puri fini di debug. Tornando all’esempio precedente potremmo chiedere all’utente di inserire nuovamente un dato, sottolineando che deve essere un intero.
3. Un metodo che possa lanciare un’eccezione si dichiara con throws
in questo modo
Public void myDangerousMethod() throws Exception {
}
4. Quando usiamo un metodo che lancia un’eccezione siamo obbligati ad intercettarla o a propagarla a nostra volta al livello superiore (se esiste, cioè il metodo a sua volta chiamante) fino al punto di ingresso del programma (main) ma prima o poi siamo comunque obbligati a gestirla pena la non compilazione del codice.
Non ci occuperemmo in questa sede di ulteriori risvolti teorici che non ci interessano, e nella prossima lezione cominceremo a cimentarci con la realizzazione di un semplice “caso di studio” su tutti i concetti fin qui espressi, il primo esempio davvero “realistico” di ciò che Java ci permette di fare.