Utilizzare le Interfacce in Java
- Per comprendere la potenza delle interfacce, analizziamo un esempio pratico.
- Creiamo un'interfaccia per una pila di interi e implementiamo due versioni: una a dimensione fissa e una espandibile.
Applicare le Interfacce
Per comprendere la potenza delle interfacce, analizziamo un esempio più pratico.
Nelle lezioni precedenti, è stata sviluppata una classe chiamata Stack
che implementava una semplice pila a dimensione fissa.
Tuttavia, ci sono molti modi per implementare una pila. Ad esempio, la pila può avere una dimensione fissa oppure può essere espandibile. La pila può anche essere contenuta in un array, una lista collegata, un albero binario e così via.
Indipendentemente dal modo in cui la pila è implementata, l'interfaccia alla pila rimane la stessa. Vale a dire, i metodi push()
e pop()
definiscono l'interfaccia alla pila indipendentemente dai dettagli dell'implementazione.
Poiché l'interfaccia a una pila è separata dalla sua implementazione, è facile definire un'interfaccia per la pila, lasciando a ciascuna implementazione il compito di definirne gli aspetti specifici. Analizziamo due esempi.
Per prima cosa, ecco l'interfaccia che definisce una pila di interi. Inserire questo codice in un file chiamato StackInteri.java
. Questa interfaccia sarà utilizzata da entrambe le implementazioni della pila.
// Definire un'interfaccia per una pila di interi.
interface StackInteri {
void push(int elemento); // memorizza un elemento
int pop(); // recupera un elemento
}
Il programma seguente crea una classe chiamata StackFisso
che implementa una versione a lunghezza fissa di una pila di interi:
// Un'implementazione di StackInteri che usa una memoria fissa.
class StackFisso implements StackInteri {
private int[] pila;
private int cima;
// alloca e inizializza la pila
StackFisso(int dimensione) {
pila = new int[dimensione];
cima = -1;
}
// Inserisce un elemento nella pila
public void push(int elemento) {
if(cima == pila.length - 1) // utilizza il membro length
System.out.println("La pila è piena.");
else
pila[++cima] = elemento;
}
// Estrae un elemento dalla pila
public int pop() {
if(cima < 0) {
System.out.println("Stack vuota.");
return 0;
} else
return pila[cima--];
}
}
class TestInterfaccia {
public static void main(String[] args) {
StackFisso pila1 = new StackFisso(5);
StackFisso pila2 = new StackFisso(8);
// inserisce alcuni numeri nella pila
for(int i = 0; i < 5; i++) pila1.push(i);
for(int i = 0; i < 8; i++) pila2.push(i);
// estrae quei numeri dalla pila
System.out.println("Contenuto della pila1:");
for(int i = 0; i < 5; i++)
System.out.println(pila1.pop());
System.out.println("Contenuto della pila2:");
for(int i = 0; i < 8; i++)
System.out.println(pila2.pop());
}
}
Segue ora un'altra implementazione di StackInteri
che crea una pila dinamica utilizzando la stessa definizione di interfaccia.
In questa implementazione, ogni pila è costruita con una lunghezza iniziale. Se questa lunghezza iniziale viene superata, la pila viene aumentata di dimensione. Ogni volta che serve più spazio, la dimensione della pila viene raddoppiata.
// Implementare una pila "espandibile".
class StackDinamico implements StackInteri {
private int[] pila;
private int cima;
// alloca e inizializza la pila
StackDinamico(int dimensione) {
pila = new int[dimensione];
cima = -1;
}
// Inserisce un elemento nella pila
public void push(int elemento) {
// se la pila è piena, allocare una pila più grande
if(cima == pila.length - 1) {
int[] temp = new int[pila.length * 2]; // raddoppia la dimensione
for(int i = 0; i < pila.length; i++) temp[i] = pila[i];
pila = temp;
}
pila[++cima] = elemento;
}
// Estrae un elemento dalla pila
public int pop() {
if(cima < 0) {
System.out.println("Stack vuota.");
return 0;
} else
return pila[cima--];
}
}
class TestInterfaccia2 {
public static void main(String[] args) {
StackDinamico pila1 = new StackDinamico(5);
StackDinamico pila2 = new StackDinamico(8);
// questi cicli causano la crescita della pila
for(int i = 0; i < 12; i++) pila1.push(i);
for(int i = 0; i < 20; i++) pila2.push(i);
System.out.println("Contenuto della pila1:");
for(int i = 0; i < 12; i++)
System.out.println(pila1.pop());
System.out.println("Contenuto della pila2:");
for(int i = 0; i < 20; i++)
System.out.println(pila2.pop());
}
}
La classe seguente utilizza entrambe le implementazioni StackFisso
e StackDinamico
. Lo fa tramite un riferimento a interfaccia. Ciò significa che le chiamate a push()
e pop()
vengono risolte a tempo di esecuzione invece che a tempo di compilazione.
/* Creare una variabile di interfaccia e
accedere alle pile tramite essa.
*/
class TestInterfaccia3 {
public static void main(String[] args) {
StackInteri pila; // creare una variabile di riferimento a interfaccia
StackDinamico dinamica = new StackDinamico(5);
StackFisso fissa = new StackFisso(8);
pila = dinamica; // caricare la pila dinamica
// inserire alcuni numeri nella pila
for(int i = 0; i < 12; i++) pila.push(i);
pila = fissa; // caricare la pila fissa
for(int i = 0; i < 8; i++) pila.push(i);
pila = dinamica;
System.out.println("Valori nella pila dinamica:");
for(int i = 0; i < 12; i++)
System.out.println(pila.pop());
pila = fissa;
System.out.println("Valori nella pila fissa:");
for(int i = 0; i < 8; i++)
System.out.println(pila.pop());
}
}
In questo programma, pila
è un riferimento all'interfaccia StackInteri
. Dunque, quando si riferisce a dinamica
, utilizza le versioni di push()
e pop()
definite dall'implementazione StackDinamico
. Quando si riferisce a fissa
, utilizza le versioni di push()
e pop()
definite da StackFisso
. Come spiegato, queste determinazioni vengono fatte a tempo di esecuzione. Accedere a implementazioni multiple di un'interfaccia tramite un riferimento a interfaccia è il modo più potente con cui Java realizza il meccanismo del polimorfismo a tempo di esecuzione.