Package,
Interfacce,
Ereditarietà
Dott. Ing. Leonardo Rigutini
Dipartimento Ingegneria dell’Informazione
Università di Siena
Via Roma 56 – 53100 – SIENA
Uff. 0577233606
[email protected]
www.dii.unisi.it/~rigutini/
Package
I package

Un programma JAVA è costituito da una raccolta di classi. Finora i
nostri programmi erano composti da un piccolo numero di classi
Quando queste tuttavia aumentano di numero, limitarsi a distribuire
le classi su più file non basta ed occorre un meccanismo per
organizzare le collezioni di classi in modo chiaro e semplice

In JAVA questo meccanismo è fornito dai pacchetti:



un pacchetto (package) è costituito da una serie di clssi correlate
La libreria JDK è costituita da centinaia di classi organizzate in
dozzine di pacchetti:

java.lang , java.awt, java.io , ecc…
Definire il package
Per inserire le classi in un pacchetto si usa la seguente sintassi
all’inizio del file:
package NOME_PACCHETTO;


Il nome del pacchetto può essere costituito da una serie di
identificatori, separati da punti:


L’idea è quella di organizzare i package in maniera gerarchica e quindi
dare la possibilità di creare un albero di pacchetti
Ogni punto individua un “ramo” dell’albero
Importare pacchetti


Se vogliamo utilizzare una classe di un pacchetto è necessario
specificare al compilatore di quale pacchetto abbiamo bisogno
E’ possibile dereferenziare direttamente la classe utilizzando il
percorso completo della classe in fase di dichiarazione di una
variabile oggetto:
java.awt.Rectangle R= new java.awt.Rectangle(5,10,20,30)

Questo sistema è evidentemente molto scomodo, quindi in
alternativa è possibile importare all’inizio della definizione della
classe i pacchetti che poi verranno utilizzati nel codice tramite la
parola import:
import java.awt.Rectangle;
…
Rectangle R= new Rectangle(5,10,20,30)
Importare package

Quando di un package sono utilizzate molte classi, è possibile
collassare le molte righe relative a quel package in una riga
import java.awt.Rectangle;
import java.awt.Color;
import java.awt.*;

In questo modo viene specificato di importare tutte le classi presenti
nel package

Nel caso si utilizzi package omonimi o classi omonime in package
diversi, è necessario utilizzare comunque la notazione estesa per
risolvere l’eventuale ambiguità
Come vengono organizzate le classi

I package hanno una corrispondenza “fisica” sul progetto che si sta
sviluppando:


Ovvero se una classe fa parte di un package, allora deve risiedere in
una cartella il cui percorso di directory coincide con la gerarchia del
package
Es
package prova.bankaccount;
La classe deve essere salvata in
[CLASSPATH]/prova/bankaccount/BankAccount.class
Come vengono organizzate le classi

Notare come la root deve essere una directory nel CLASSPATH del
JCompiler e della JVM

Se vogliamo che la classe BankAccount o tutto il package
bankaccount.prova.* siano visibili in una classe terza, è necessario
inserire la directory da cui classes.bankaccount.* nasce:
C:\Programmi\JBuilder2005\jdk1.4\bin\javaw -classpath
“D:\Documenti\Activities\Teaching\LinguaggiProgrammazione storing2005\Examples\04.Package\01.BankAccount-Package\classes;”
Directory root per i package prova.* e prova.bankaccount.*
Pacchetti compressi

Usualmente il pacchetto viene fornito in formato compresso (jar):

All’interno del file .jar vengono memorizzate le classi del pacchetto in
una struttura a directory che rispecchia i package di ogni classe

Nella creazione del jar è possibile specificare se includere anche le
dipendenze, ovvero tutti quelle classi (con i relativi package) che
sono utilizzate nel pacchetto che stiamo costruendo

Inoltre è possibile inserire nel file .jar anche la documentazione, in
modo da avere un pacchetto completo memorizzato in un singolo
file e compresso
Pacchetti compressi

L’utilizzo dei file jar segue quello deli pacchetti normali:


Nel CLASSPATH è possibile specificare come path di ricerca classi
anche i files .jar in cui sono memorizzati i package
Il JCompiler e la JVM entrano all’interno dei file jar per utilizzare tutte le
classi necessarie
…\04.Package\01.BankAccount-Package>java TestBankAccount
Exception in thread "main" java.lang.NoClassDefFoundError: TestBankAccount
…\04.Package\01.BankAccount-Package>java –cp BankAccount.jar
prova.TestBankAccount
1500.0
Chiamata della classe
TestBankAccount
specificando il package
File jar per la classe
prova.TestBankAccount e per la classe
prova.bankaccount.BankAccount
Interfacce e polimorfismo
Riutilizzo

Supponiamo di avere una classe DataSet che calcola alcune
statistiche di un insieme di valori in ingresso (media, massimo,
varianza ecc…).

Vorremmo una classe che calcoli le stesse informazioni su oggetti
BankAccount (diversi dai semplici numeri).


La cosa che dobbiamo fare è modificare la classe Dataset aggiungendo
metodi che ricevono come parametro oggetti BankAccount
In questo modo però se vogliamo estendere le funzionalità della
classe DataSet ad altre classi (Coin per esempio) rimane necessario
ogni volta aggiungere metodi per la classe desiderata
Riutilizzo
public class DataSet {
public void add(int n) {
sum+=n;
cc++;
}
public double avg() {
return sum/cc;
}
}
public class DataSet {
private int sum;
private int cc=0;
public void add(BankAccount B) {
sum+=B.getBalance();
cc++;
}
public void add(Coin C) {
sum+=C.getValue();
cc++;
}
public double avg() {
return sum/cc;
}
Riutilizzo

Possiamo notare però che il meccanismo per l’analisi dei dati è il
solito in tutti i casi:


viene letto il valore dell’oggetto passato e viene calcolata l’informazione
richiesta
Se tutte le classi che noi vogliamo passare alle funzioni di DataSet
potessero accordarsi su un unico metodo getValue() che ritorna il
valore su cui la classe DataSet calcola le statistiche, potremmo
definire una sola funzione add()
Riutilizzo

Ma che tipo di oggetto passiamo come parametro per add() ?
Definiamo una classe fittizia Measurable che descrive solamente
l’interfaccia comune e poi specifichiamo le diverse classi che
“implementano” l’interfaccia
public interface Measurable {
public double getValue();
}
public class DataSet {
public void add(Measurable x) {
sum+=x.getValue();
cc++;
}
public double avg() {
return sum/cc;
}
}
Riutilizzo

Le classi che dovranno essere passate come parametri di
add(Measurable x) dovranno essere specificate come
implementazioni della classe Measurable e dovranno implementare
il metodo specificato nell’interfaccia:
public class BankAccount implements Measurable {
public double getValue() {
return balance;
}
}
public class Coin implements Measurable {
public double getValue() {
return value;
}
}
Interfaccia

I metodi di una interfaccia non sono dichiarati public perché lo sono
per impostazione predefinita. Tuttavia, i metodi di una classe non
sono pubblici se ciò non viene specificato:


Quindi nell’ implementazione di una interfaccia è necessario specificare i
metodi come public
La classe Measurable, è una descrizione di più alto livello delle
classi BankAccount e Coin. E’ possibile quindi utilizzare una
variabile oggetto di tipo Measurable per memorizzare un oggetto di
tipo BankAccount o Coin
Measurable x=new Coin();
Operatore instanceof

L’operazione inversa, invece, non è immediata come la precedente:


Se siamo sicuri che un oggetti di tipo Measurable sia di tipo Coin
possiamo “forzare” la conversione tramite un cast
Nel caso però in cui la forzatura non sia valida, ossia a run-time l’oggetto
forzato non è di tipo Coin, viene ritornato errore
Measurable x= …;
Coin y=(Coin)x;

Per verificare a run-time la corretta appartenenza ad una classe di
un oggetto è possibile utilizzare l’operatore instanceof:
Measurable x= …;
Coin y;
If (x instanceof Coin) {
y=(Coin)x;
}
Interfacce

Le interfacce non possono avere variabili, ma è possibile dichiarare
constanti che verranno “ereditate” da tutte le classi che
implementano l’interfaccia:

Quando vengono dichiarate le costanti in una interfaccia dovrebbero
essere omesse le parole chiave public, static e final poiché tutte le
variabili in una interfaccia sono definite automaticamente public static
final
public interface Move{
int NORTH=0;
int EAST=3;
int SOUTH=6;
int WEST=9;
}
Polimorfismo

La riga di codice
Measurable x;
è molto utilizzata in pratica e permette di avere la variabile x
disponibile per memorizzare oggetti di classi diverse:
x=new BankAccount();
x=new Coin();

Occorre ricordare però che in realtà non esiste alcun oggetto di tipo
Measurable. Il tipo dell’oggetto sarà sempre una classe che
implementa l’interfaccia
Polimorfismo


Pensiamo adesso a cosa accade quando viene chiamata una
funzione dell’interfaccia:
la JVM risolve il tipo dell’oggetto su cui è chiamato il metodo
d’interfaccia ed invoca quello relativo alla classe “corretta”
Ciò significa che l’invocazione di un metodo d’interfaccia può
chiamare metodi diversi:


Il principio secondo cui il tipo dell’oggetto determinato a run-time
determina il metodo da chiamare è detto polimorfismo
In realtà in JAVA tutti i metodi sono polimorfi, poiché la scelta del
metodo “giusto” da eseguire può dipendere dalla valutazione delle
classi passate come parametro:

overloading
Inner Class
Inner Class

Le Inner class sono classi definite all’interno di altre classi.
class Homer {
class Son {
public void Speak() {
System.out.println(“Son: Eat my socks!");
}
}
Son Bart = new Son();
}
public void Speak() {
System.out.println("Homer: DOH!");
Bart.Speak();
}
Inner Class

Le Inner class non possono essere istanziate direttamente con new
da classi esterne:
Homer.son Bart = new Homer.Son();

ERRORE
Un modo per rendere visibile la classe è creare un metodo che
ritorni una istanza della Inner Class:
class Homer {
…
public Son CreateSon() {
return new Son();
}
public static void main(String[] args) {
Homer Homer1 = new Homer();
Homer.Son Bart = Homer1.CreateSon();
}
}
OK
Inner Class

Le Inner class che implementano interfacce pubbliche possono
essere visibili all’esterno pur avendo l’implementazione nascosta
all’interno della classe contenitore
interface Simpson {
public void Speak();
}
class Homer {
}
pricate class Son implements Simpson {
public void Speak() {
System.out.println(“Son: Eat my socks!");
}
}
:
:
Inner Class definite nei metodi


Le Inner class possono essere definite anche nei metodi,
nascondendole del tutto al resto del programma
È possibile utilizzare queste classi all’esterno solo se posseggono
una interfaccia pubblica o una classe base
class Homer {
public void Speak() {
class Son {
}
}
:
:
}
System.out.println(“Son: Eat my socks!");
Inner Class anonime

interface Point {
public void setXY(int x, int y)
public int getX();
public int getY();
}
È possibile costruire
classi innestate senza
specificare il tipo, ma
solo specificando
l’interfaccia che
implementano
Inner Class anonima
che implementa
l’interfaccia Point
}
public class Prova {
static public Point getPoint(final int x, final int y) {
return new Point() {
private int X, Y;
{X = x; Y = y; }
public void setXY(int x, int y)
{X = x; Y = y;}
public int getX() { return X; }
public int getY() { return Y; }
}
}
public static void main(String[] args) {
Point p = getPoint(0, 0);
System.out.println(“P(”+p.getX()+“,” +p.getY()+“)”);
}
Ereditarietà
Organizzazione gerarchica

Abbiamo visto che i package forniscono una struttura gerarchica per
l’organizzazione delle classi

Una organizzazione gerarchica può esistere però anche tra le classi:


una classe può essere un tipo particolare di un’altra classe più generica:
Veicolo  Auto  Ferrari
È possibile definire una struttura tra classe in cui da una classe
generale vengono derivate classi sempre più specifiche che
ereditano le proprietà ed i metodi delle classi genitore
Eredità

Come detto un problema può essere descritto in maniera più chiara
se decomposto concettualmente in classi

Molte volte può essere necessario individuare classi di classi:


in molte situazioni infatti è possibile individuare oggetti generali da cui
discendono una serie di classi “del tipo di”
In tale immaginario, le classi derivate “ereditano” lo stato delle classi
genitore, aggiungendo funzionalità proprie della classe specifica:
Ereditarietà

Se pensiamo ad un veicolo, esso avrà delle funzioni (muovi, frena, gira
ecc…) e delle proprietà (peso, lunghezza, larghezza ecc…)

Da una classe di oggetti veicolo possono poi essere derivati altri tipi di
oggetti:

bicicletta, Autoveicolo, Motoveicolo
ognuno con delle funzionalità proprie:


ad esempio la funzione accendi() non dovrebbe appartenere alla classe bicicletta
Inoltre la classe autoveicolo potrebbe a sua volta essere suddivisa in base
al tipo di autoveicolo:

Auto, camion, van, ecc…
ognuna con proprietà e funzioni proprie e diverse dall’altro
Ereditarietà
Veicolo
Bicicletta
Autoveicolo
Autocarro
Utilitaria
Motoveicolo
Automobile
Berlina
Jeep
Ereditarietà

In Java è possibile dichiarare una classe come figlia di un’altra
classe tramite la parola extends
public class Auto extends Veicolo {
}


La classe genitore viene riferita come superclasse, mentre la classe
più specifica (figlia) viene chiamata sottoclasse
Nei diagrammi, l’ereditarietà si
indica con una freccia a vuoto
diretta dal figlio verso il padre
Veicolo
Auto
Eredità o interfaccia ?

A questo punto potremmo chiederci cosa ci sia di diverso tra
l’ereditarietà e l’implementazione di una interfaccia?
Un’interfaccia non è una classe, non ha uno stato né un
comportamento: indica solamente quali metodi sono da
implementare.
Una superclasse ha uno stato ed un comportamaento e questi
vengono ereditati dalla classe derivata.

L’ereditarietà realizza il concetto di riutilizzo del codice:

grazie all’ereditarietà, infatti, non siamo costretti a rifare il lavoro di
progettazione della classe e di implementazione delle funzioni più
generali
Ereditare variabili e metodi

Quando definiamo una classe come estensione di una superclasse,
ereditiamo tutte le variabili e le funzioni della classe genitore ed in
più possiamo aggiungere nuove variabili o funzioni

Cosa succede se ridefiniamo un metodo che già esiste nella
superclasse?
Il metodo viene “sovrascritto” (override) ovvero il metodo della
superclasse è sostituito dalla nuova implementazione del metodo
nella sottoclasse. Ogni chiamata a quel metodo da oggetti della
sottoclasse, viene risolta con la nuova implementazione
Ereditare variabili e metodi

Così come è possibile ridefinire le funzioni, è possibile ridefinire
anche le variabili membro, mettendo in ombra così le variabili della
superclasse omonime:


in questo caso però, nella sottoclasse esiste comunque una istanza
della variabile della superclasse
In entrambi i casi è sempre possibile fare riferimento ad una
variabile o un metodo della classe genitore tramite l’indicatore di
oggetto super:
…
super.drive();
…
int y = super.carburante;
Chiama esplicitamente la
funzione drive() della
classe genitore
Legge il valore della
variabile membro
carburante della classe
genitore
Costruttore

Tramite la parola super, è possibile fare riferimento anche al
costruttore della superclasse:
class Rettangolo {
float h, l;
public Rettangolo(float alt, float lung){
h=alt;
costruttore super-class
l=lung;
}
class Quadrato extends Rettangolo {
public Quadrato(float lung){
super(lung,lung);
}
override
public float diag(){
return Math.sqrt((h*h)+(l*l));
}
}
}
public float diag(){
return Math.sqrt(2) * l;
}
....
Quadrato q1=new Quadrato(2);
System.out.println(q1.area());
Visibilità


L’ereditarietà implica alcuni problemi di visibilità dei metodi e delle
variabili

Che diritti può avere una classe derivata su un metodo o una variabile
membro della superclasse che è stata definita privata?

È possibile definire variabili private che siano private ma utilizzabili per le
classi derivate?
Sono state definite le seguenti regole tra classi e sottoclassi:

La visibilità di una variabile o di un metodo di una superclasse è
specificata da “specificatori di accesso”
Specificatori di accesso




Public – rende visibile la variabile o il metodo a tutte le classi
Private – nasconde la variabile o il metodo a tutte le classi,
comprese le classi derivate
Protected – nasconde le variabili ed i metodi a tutte le classi tranne
quelle nello stesso package
Nessun specificatore – utilizza Protected come visibilità di default
Visibilità
Stesso Packa ge
Packa ge diversi
Ereditabile
Accessibile EreditabileAccessibile
Public
sモ
sモ
sモ
sモ
Protected
sモ
sモ
sモ
no
Private
sモ
sモ
no
no
Upcasting

È la proprietà più importante dell’ereditarietà

Il concetto è molto semplice: tutti i metodi che hanno una classe
come parametro accettano anche tutti le classi derivate

Il termine upcasting deriva dalla
direzione con cui ci si muove sugli
alberi di ereditarietà:
Stato
Veicolo
Metodi
Stato
Metodi
Auto
Upcasting
Inserimento di un
oggetto Veicolo: può
essere un oggetto
Auto, Autocarro,
ecc…  upcast
public class Concessionario {
protected Veicolo[] parcheggio;
// costruttore: specifica la dimensione del parcheggio
public Concessionario(int dim) {
parcheggio = new Veicolo[dim];
}
// mette un oggetto nel parcheggio senza controllare lo spazio
public void add (Veicolo v) {
parcheggio[parcheggio.length]=v;
}
}
Downcasting

Anche qui il concetto è semplice:
se siamo sicuri che un handle, pur essendo di una classe, si
riferisca ad un oggetto discendente, allora è possibile effettuare la
conversione

La conversione è forzata: cast esplicito
// preleva un veicolo dal parcheggio: se è un oggetto Auto,
// lo restituisce come oggetto Auto, altrimenti ritorna null
public Auto getAuto(int x) {
if (parcheggio[x] instanceof Auto) return (Auto) parcheggio[x];
return null;
}
downcast
RTTI (Run-Time Type Identification)

Per risolvere un riferimento ad una superclasse si può utilizzare la
risoluzione a run-time del tipo di oggetto

Avevamo già visto nel caso di interface come risolvere a
run-time il tipo di oggetto: instanceof

L’RTTI (Run-Time Type Identification) è il sistema che permette di
capire a che classe appartiene un oggetto.
Downcasting
Inserimento di un oggetto
Veicolo: può essere un
oggetto Auto, Autocarro,
ecc…  upcast
public class Concessionario {
protected Veicolo[ ] parcheggio;
// costruttore: specifica la dimensione del parcheggio
public Concessionario(int dim) {
parcheggio = new Veicolo[dim];
}
// mette un oggetto nel parcheggio senza controllare lo spazio
public void add (Veicolo v) {
parcheggio[parcheggio.length]=v;
}
// preleva un veicolo dal parcheggio: se è un oggetto Auto,
// lo restituisce come oggetto Auto, altrimenti ritorna null
public Auto getAuto(int x) {
if (parcheggio[x] instanceof Auto) return (Auto) parcheggio[x];
return null;
}
}
Prelievo di un Oggetto
Auto: cast da Veicolo ad
Auto  downcast
Lo specificatore final

Avevamo visto che final era utilizzato all’interno di una classe per
definire una costante:


Una variabile inizializzata in fase di dichiarazione e non più modificabile
È possibile utilizzare final anche per i metodi:
in tal caso il metodo non può più essere sovrascritto in una classe
derivata
Lo specificatore abstract

Il JAVA permette di definire classi i cui metodi non hanno
implementazione, queste classi si dicono astratte (abstract):
abstract class Shape {
}

Una classe astratta non può essere istanziata ma solamente estesa
da classi derivate
Lo specificatore abstract

Una classe astratta può avere dei metodi astratti che quindi non
devono essere implementati:
abstract class Poligono {
…
abstract double getArea();
…
}

Se una classe ha almeno un metodo astratto, deve essere
dichiarata abstract
Lo specificatore abstract

I metodi astratti però devono essere implementati obbligatoriamente
nelle classi derivate, a meno che anch’esse siano dichiarate
abstract
abstract class Poligono {
…
abstract double getArea();
double nLati() {
return n;
}
}
class Quadrato extends Poligono {
…
double getArea() {
return l*l;
}
…
}
Classi astratte

I vantaggi nella definizione di classi abstract sono:

definire una classe abstract significa definire una interfaccia
comune a tutte le classi derivate

una classe abstract non può essere istanziata: se ciò viene fatto
il compilatore produce un errore. Questo permette al progettista
della classe di costringere l’utente a definire il comportamento
dei metodi astratti
abstract ed interface

Ma cosa distingue una classe astratta da un interfaccia?

Nelle classi astratte è possibile definire proprietà utilizzando tutti gli
specificatori di visibilità (public, private, ecc …) , mentre nelle interfacce
questo non è possibile: in questo caso infatti le variabili divengono
automaticamente STATIC and FINAL

Nella interface il concetto di implementazione è assolutamente
inesistente. In pratica si stabilisce un protocollo di accesso, che poi verrà
utilizzato da tutte le classi che utilizzano la medesima interfaccia

L’ interface permette l’ereditarietà multipla
La classe Object


L’ereditarietà in Java prevede una classe ROOT da cui discende
qualsiasi altra classe: la classe Object
Ogni classe, che rappresenti qualsiasi entità, è sicuramente una
derivazione della classe Object e quindi mette a disposizione i
metodi di tale superclasse:





Dovuti al fatto che JAVA
protected Object clone() – copia
lavora con i riferimenti
Boolean equals(Object obj) – uguaglianza
protected void finalize() – dealloca l’oggetto (Garbage Collector)
Class getClass() – ritorna il tipo dell’oggetto a run-time
int hashCode() – ritorna un hash per l’oggetto
Risoluzione
del tipo a
run-time
La classe Object






void notify() – riprende un thread fermo su questo oggetto
void notifyAll() – sveglia tutti i thread fermi su questo oggetto
String toString() – ritorna una rappresentazione in stringa dell’oggetto
void wait() – ferma l’oggetto fino a che non viene chiamato il notify() o
notifyAll()
void wait(long timeout) – come sopra però con un timeout massimo
void wait(long timeout, int nanos) – come sopra però permette di specificare
il timeout in nanosecondi
Ritorna una
versione
stampabile
dell’oggetto
Garbage Collector
Quanto “vivono” gli oggetti?

Il programmatore si occupa di costruire gli oggetti, ma non di
distruggerli

In JAVA la deallocazione degli oggetti è gestita automaticamente:
quando un oggetto non è più utilizzato, la memoria utilizzata viene
liberata automaticamente


L’entità che si occupa di liberare memoria non più utile è il Garbage
Collector
public void xxx(){
Float W = new Float(0.5);
}
L’handle W scompare, ma non
l’oggetto associato!
Il Garbage Collector

Il garbage collector è avviato automaticamente dalla JVM, ma è
possibile forzare la chiamata al Garbage Collector tramite una
funzione ereditata da tutte le classe dalla classe Object:
System.gc();

È possibile capire se un oggetto è ancora referenziato (vivo)
utilizzando due possibili tecniche:


Conteggio
Ricostruzione
Conteggio

Questa tecnica prevede di contare i riferimenti agli oggetti.

Lo svantaggio è che molte volte gli oggetti si auto-referenziano:
1
A
non utilizzato
da variabile
1
B
1
non utilizzato
C
Ricostruzione

Un’altra tecnica è quella di seguire a ritroso i riferimenti fino ad una
variabile static oppure presente nello stack ed ancora attiva:
Memoria
A:
B:
B
B:
C:
A
non utilizzato
C
Memoria
A
C
B
C:
D
D
Strategie di GC

Ma cosa fare quando vengono trovati gli oggetti non più utilizzati?

Stop-and-Copy: una volta trovati gli oggetti da rimuovere, vengono
copiati in un altro heap gli oggetti ancora referenziati, e modificati tutti i
riferimenti a loro collegati. L’heap originario è sostituito dal nuovo heap.

Mark-and-Sweep: una volta trovati gli oggetti da rimuovere, questi
vengono eliminati senza copiare quelli ancora referenziati. In seguito
l’heap può essere riorganizzato spostando solo alcuni oggetti ancora
vivi, più piccoli degli spazi rilasciati.
Adaptive Garbace Collection

La tendenza attuale è quella di utilizzare entrambe le tecniche
(Adaptive Garbace Collection), ma in momenti diversi:


la tecnica Stop-and-Copy viene utilizzata solo quando l’heap è troppo
frammentato, e quindi è necessaria una riorganizzazione
la tecnica Mark-and-Sweep è più veloce, ma l’heap viene frammentato
Scarica

4. ereditarietà