Innanzitutto non esistono classi super, in Java si usa la parola super
per indicare la classe da cui deriva quella che stiamo scrivendo. Mentre
l’ereditarietà è uno dei concetti fondamentali della programmazione ad
oggetti. Quando si decide di adottare questa metodologia, si dovrebbero
seguire determinati passaggi. La prima cosa da fare è l’analisi del problema
per trovare una soluzione. Una volta trovata, si rivede tutto il processo
risolutivo, cercando di individuare le entità fondamentali che intervengono
nel raggiungimento della soluzione. Inoltre è necessario capire con esattezza
come interagiscono tra di loro queste entità.
Quando si termina questa analisi spesso ci si accorge che molte di queste
entità o oggetti appartengono a delle categorie o classi che li raggruppano
a uno o più livelli determinando una gerarchia di classi, e proprio l’ereditarietà
permette lo sfruttamento delle capacità delle classi genitrici per lo
sviluppo di sottoclassi specializzate per un compito ben preciso.
Pensiamo ad una classe per la gestione di un serbatoio, questa classe
avrà una variabile interna che indicherà la quantità di liquido contenuto
al suo interno, e una lo stato del rubinetto che permette di riempirlo
o svuotarlo. Supponiamo di voler gestire un tipo particolare di serbatoio
in cui è di fondamentale importanza anche la temperatura e la pressione
del contenuto. Senza l’ereditarietà avremmo dovuto duplicare il codice
scritto per il primo tipo di serbatoio, e modificarlo per tenere conto
delle nuove esigenze.
A un certo punto ci accorgiamo che per tutti i serbatoi dovremmo gestire
un’altra informazione, per esempio quante volte è stato usato, magari
per determinare un indice di usura, superato il quale andrebbe verificato
lo stato generale del serbatoio.
Avendo duplicato il codice dovremo modificare entrambi le classi per i
due tipi di serbatoi. E in fin dei conti siamo stati fortunati che esistessero
solo due tipi di serbatoi, nel caso ce ne fossero stati di più avremmo
dovuto fare un lavoro molto maggiore, con un notevole aumento della probabilità
di introdurre un errore da qualche parte.
Per aggirare il problema ci viene in aiuto l’ereditarietà, con cui avremmo
definito il secondo serbatoio come un derivato dal primo, e questo ci
avrebbe evitato la duplicazione del codice e nel momento dell’introduzione
della gestione dell’indice di usura, la modifica l’avremmo apportata solo
alla prima classe e sarebbe stata ereditata da tutte le classi derivate.
Il nostro lavoro sarebbe stato molto minore e la possibilità di introdurre
errori sarebbe stata minima.
In Java avremmo scritto qualcosa del genere in cui si può vedere l’uso
della parola chiave super.
class Serbatoio {
private double capacita=100.0;
private double contenuto=0.0;
private bool rubinetto=false; // Variabile a true -> aperto
private int usura=0; // Variabile aggiunta per l'indice di usura
public bool apri()
{
if(rubinetto==false) {
rubinetto=true;
usura++;
}
}
public bool pericolo()
{
return usura>1000;
}
// Altri metodi di interfaccia
...
}
class SerbatoioPressurizzato extends Serbatoio {
private double temperatura;
private double pressione;
public bool pericolo()
{
return super.pericolo() // L'uso di super è fondamentale, senza si avrebbe
// una chiamata ricorsiva infinita.
|| temperatura>35.0
|| pressione>5;
}
// Altri metodi di interfaccia
...
}
Oltre a questi vantaggi l’ereditarietà permette la gestione e riutilizzo
di codice arbitrariamente complesso e di cui potremmo benissimo non sapere
nulla dei suoi processi interni. In realtà questo è possibile anche con
una programmazione non ad oggetti e quindi senza ereditarietà, ma non
in modo tanto pulito e trasparente.
Passiamo ora alla programmazione concorrente. Bisogna premettere che
in java la programmazione concorrente ha delle restrizioni imposte dall’essere
un linguaggio indipendente dal sistema operativo. Per cui parlare di semafori
è o altri metodi classici per gestire l’esecuzione concorrente di processi
è delicato. Iniziamo col dire che esistono due tipi di sincronizzazione,
quella tra processi distinti e quella all’interno di uno stesso processo
tra i suoi diversi thread. Tra processi la sincronizzazione può avvenire
solo se si dispone di meccanismi offerti dal sistema operativo per farli
comunicare. Tra questi possiamo ricordare i messaggi, i socket, la memoria
condivisa, le pipe, i semafori, o il semplice l’accesso esclusivo ad un
file. Di questi solo i semafori sono stati pensati appositamente per la
sincronizzazione, ma con le altre forme di comunicazione è possibile implementare
in modo piu o meno macchinoso sistemi equivalenti, magari con la definizione
di un processo server che offra semafori ai client e quindi con l’uso
di una comunicazione come i socket. In effetti le uniche IPC (Inter Process
Comunication) previste da Java sono i file, e i socket che quindi devono
essere supportati dal sistema operativo. Mentre per la sincronizzazione
dei thread, e quindi all’interno di uno stesso processo, la comunicazione
non è un problema, perché tutti i thread di un processo condividono la
stessa area di memoria, per cui “vedono” tutti gli stessi dati. Anche
in questo caso Java non fornisce oggetti per la sincronizzazione, perché
è stata inserita nel linguaggio una parola chiave per non permettere l’esecuzione
di una parte di codice da più thread contemporaneamente. Il termine in
questione è synchronized che inserito tra i modificatori di un metodo
lo rende thread-safe, ma è possibile usarlo per la definizione di una
sezione critica, che concettualmente è simile al metodo sincronizzato,
ma si riferisce solo ad una porzione di codice. Il funzionamento di synchronized
è molto interessante, e sinceramente ho scoperto alcuni particolari proprio
scrivendo questo esempio. Infatti la prima stesura non funzionava, mentre
la seconda che apparentemente è uguale, funziona perfettamente rivelando
un particolare molto importante.
Esercizio: Thread in competizione.