gianluca.aguzzi@unibo.it
angelo.filaseta@unibo.it
Questo materiale è ampiamente basato su quello realizzato dai Prof. Mirko Viroli e Roberto Casadei, che ringrazio.
Ogni errore riscontratovi è esclusiva responsabilità degli autori di questo documento.
interface
in Java e meccanismi collegatiL’incapsulamento ci fornisce i meccanismi per ben progettare le classi
$\Rightarrow$ ma le dipendenze fra classi non sono evitabili del tutto
class A {
private B b;
}
// Composizione
class A {
private B b;
public A() {
b = new B();
}
}
// Aggregazione
class A {
private B b;
public A(B b) {
b = this.b;
}
}
Introdurremo la composizione (che è una versione più forte della associazione), mostrando la sua relazione con le interface di Java
A
è ottenuto componendo un insieme di altri oggetti, delle classi B1
, B2
, …, Bn
A
contiene, o si compone di, o aggrega, oggetti delle classi B1
, B2
, …, Bn
A
include un oggetto di B1
, uno di B2
, …, uno di Bn
B1
, B2
, …, Bn
non sono tipi primitivi, ma classiclass A {
private B1 component1;
private B2 component2;
public A(B1 b1, ...) { ... }
}
A
, gli oggetti composti hanno vita propria e indipendente da A
A
, passati ad A
all’atto di costruzione o via setter, e possono sopravvivere ad A
A
, gli oggetti composti hanno vita vincolata da A
A
ClasseScolastica
aggrega un insieme di oggetti Studente
; una Automobile
ha un Motore
, quattro oggetti Ruota
etc.Casa
si compone di un insieme di oggetti Stanza
(una stanza non ha vita indipendente dalla casa); un Libro
si compone di più oggetti Capitolo
(è vero, si potrebbe considerare un capitolo in isolamento, ma questo rimarrebbe comunque un capitolo di quel libro)Un oggetto interfaccia grafica si compone di oggetti di tipo Button
, TextField
, Label
, eccetera
Un oggetto ateneo si compone di oggetti di tipo Facoltà
, Studenti
, Docenti
, eccetera
Un oggetto controllore domotica si compone di oggetti di tipo Lamp
, TV
, Radio
, eccetera
A
si compone esattamente di 1 oggetto di B
A
avrà un campo (privato) di tipo B
A
) è sempre presenteA
si compone opzionalmente di 1 oggetto di B
A
avrà un campo (privato) di tipo B
null
(oggetto di B
assente)A
si compone di un numero noto $n$ di oggetti di B
A
avrà $n$ campi (privati) di tipo B
– se “n” piccoloA
si compone di una moltitudine non nota di oggetti di B
A
avrà un campo (privato) di tipo B[]
(o altro container)public class Lamp {
/* Costanti luminosità */
private final static int LEVELS = 10;
private final static double DELTA = 0.1;
/* Campi della classe */
private int intensity;
private boolean switchedOn;
/* Costruttore */
public Lamp() {
this.switchedOn = false;
this.intensity = 0;
}
/* Gestione switching */
public void switchOn() {
this.switchedOn = true;
}
public void switchOff() {
this.switchedOn = false;
}
public boolean isSwitchedOn() {
return this.switchedOn;
}
/* Gestione intensita' */
private void correctIntensity() { // A solo uso interno
if (this.intensity < 0) {
this.intensity = 0;
} else if (this.intensity > LEVELS) {
this.intensity = LEVELS;
}
}
public void setIntensity(final double value) {
this.intensity = Math.round((float) (value / DELTA));
this.correctIntensity();
}
public void dim() {
this.intensity--;
this.correctIntensity();
}
public void brighten() {
this.intensity++;
this.correctIntensity();
}
public double getIntensity() {
return this.intensity * DELTA;
}
public String toString() {
return "Ac: " + this.isSwitchedOn() + ", Int: " + this.getIntensity();
}
}
TwoLampsDevice
Lamp
e sfrutto la composizioneTwoLampsDevice
public class TwoLampsDevice {
/* Composizione di due Lamp! */
private Lamp l1; // Potrei realizzare lo stato con un array
private Lamp l2; // I clienti non ne sarebbero influenzati!
/* Composizione inizializzata al momento della costruzione */
public TwoLampsDevice(){
this.l1 = new Lamp();
this.l2 = new Lamp();
}
/* Metodi getter */
public Lamp getFirst(){
return this.l1;
}
public Lamp getSecond(){
return this.l2;
}
/* Altri metodi che lavorano sulla "composizione" */
public void switchOnBoth(){
this.l1.switchOn(); // Nota la concatenazione di "."
this.l2.switchOn(); // .. è tipico della composizione
}
public void switchOffBoth(){
this.l1.switchOff();
this.l2.switchOff();
}
public void ecoMode(){
this.l1.switchOff();
this.l2.switchOn();
this.l2.setIntensity(0.5);
}
}
Un box rettangolare per classe, diviso in tre aree:
Su campi e metodi
-
se privati, +
se pubblicistatic
nome(arg1: tipo1, arg2: tipo2, ..): tipo_ritorno
archi fra classi indicano relazioni speciali:
A seconda dello scopo per cui si usa il diagramma, non è necessario riportare tutte le informazioni, ad esempio spesso si omettono le proprietà, le signature complete, ed alcune relazioni
Lamp
:Lamp
e TwoLampsDevice
LampsRow
public class LampsRow {
private final Lamp[] row; // Campo
public LampsRow(final int size) {
this.row = new Lamp[size]; // Tutti i valori a null
}
public void installLamp(final int position, final Lamp lamp) {
this.row[position] = lamp;
}
public void removeLamp(final int position) {
this.row[position] = null;
}
public void switchAll(final boolean on) {
for (Lamp lamp: this.row) {
if (lamp != null) { // Previene il NullPointException
if (on){
lamp.switchOn();
} else {
lamp.switchOff();
}
}
}
}
public Lamp getLamp(final int position) { // Selettore
return this.row[position];
}
public boolean isInstalled(final int position) { // Selettore
return this.row[position] != null;
}
}
Lamp
e LampsRow
DomusController
switchAll
in modo riusabile, e possibilmente rimandendo aperti all’introduzione di nuovi tipi di dispositivi?public class DomusController {
private Lamp[] lamps;
private TV[] tvs;
private AirConditioner[] airs;
private Radio[] radios;
//...
public void switchAll(boolean on) {
for (Lamp lamp: this.lamps) {
if (lamp != null) {
if (on){ lamp.switchOn(); } else { lamp.switchOff(); } // mal formattato!
}
}
for (TV tv: this.tvs) {
if (tv != null) {
if (on) { tv.switchOn(); } else { tv.switchOff(); } // mal formattato!
}
}
... // e così via per tutti i dispositivi
}
}
for
sia essenzialmente identica!
class Lamp {
private final static double DELTA = 0.1;
private int intensity;
private boolean switchedOn;
// ...
public Lamp() {
this.switchedOn = false;
this.intensity = 0;
}
public void switchOn() {
this.switchedOn = true;
}
public boolean isSwitchedOn() {
return this.switchedOn;
}
public double getIntensity() {
return this.intensity * DELTA;
}
// ...
}
Estrazione del “contratto d’uso” della lampadina
interface Lamp {
void switchOn();
Boolean isSwitchedOn();
double getIntensity()
// ...
}
DomusController
:
DomusController
gestirà un unico array di “dispositivi”interface
javac
come una classe, e produce un .class
interface
I
può essere “implementata” da una classeC
che lo dichiara esplicitamente (class C implements I { ... }
)C
dovrà definire (il corpo di) tutti i metodi dichiarati in I
C
, avrà come tipo C
al solito, ma anche I
!!DomusController
Lamp
, TV
, Radio
, AirConditioner
hanno una caratteristica comune: sono dispositivi e come tali possono come minimo essere accesi o spenti.Device
che tutti e 4 implementano.
Device
package it.unibo.interfaces.domo;
/* Interfaccia per dispositivi
Definisce un contratto:
- si può accendere
- si può spegnere
- si può verificare se acceso/spento
Nota: nessuna indicazione public/private nei metodi!
*/
public interface Device {
void switchOn();
void switchOff();
boolean isSwitchedOn();
}
Device
public class Lamp implements Device {
// ...
/* NOTA: Nessun cambiamento necessario rispetto a prima!
* Di seguito si riporta la parte di `Lamp` che
* permette di rispettare il contratto di `Device`,
* ovvero di implementare l'interfaccia
*/
private boolean switchedOn;
public void switchOn(){
this.switchedOn = true;
}
public void switchOff(){
this.switchedOn = false;
}
public boolean isSwitchedOn(){
return this.switchedOn;
}
// ...
}
public class TV implements Device {
// Nessun cambiamento necessario rispetto a prima!
}
public class Radio implements Device {
// Nessun cambiamento necessario rispetto a prima!
}
<< interface >> Nome
”
<<interface>>
implements
)
I
, in che senso I
è un tipo?I
è un tipo come gli altri (int
, float
, String
, Lamp
, Lamp[]
)/* Su Lamp effettuo le usuali operazioni */
Lamp lamp = new Lamp();
lamp.switchOn();
boolean b = lamp.isSwitchedOn();
lamp.dim();
/* Una variabile di tipo Device può contenere un Lamp,
e su essa posso eseguire le operazioni definite da Device */
Device dev; // creo la variabile
dev = new Lamp(); // assegnamento ok
dev.switchOn(); // operazione di Device
boolean b2 = dev.isSwitchedOn(); // operazioni di Device
/* Attenzione: non si può invocare un metodo specifico di Lamp
su una variabile di tipo Device! */
dev.dim(); // NO!!!!
Device dev2 = new Lamp(); // Altro assegnamento
/* Attenzione, le interfacce non sono istanziabili */
Device dev3 = new Device(); // NO!!!!
class DeviceUtilities {
/* Senza interfacce, non resta altro che... */
public static void switchOnIfCurrentlyOff(Lamp lamp) {
if (!lamp.isSwitchedOn()){
lamp.switchOn();
}
}
public static void switchOnIfCurrentlyOff(TV tv) {
if (!tv.isSwitchedOn()) {
tv.switchOn();
}
}
public static void switchOnIfCurrentlyOff(Radio radio) {
if (!radio.isSwitchedOn()) {
radio.switchOn();
}
}
..
}
class DeviceUtilities {
// ...
/* Grazie alle interfacce, fattorizzo in un solo metodo */
public static void switchOnIfCurrentlyOff(Device device) {
if (!device.isSwitchedOn()){
device.switchOn();
}
}
}
/* Codice cliente */
Lamp lamp = new Lamp();
TV tv = new TV();
Radio radio = new Radio();
switchOnIfCurrentlyOff(lamp); // OK, un Lamp è un Device
switchOnIfCurrentlyOff(tv); // OK, una TV è un Device
switchOnIfCurrentlyOff(radio); // OK, una Radio è un Device
quando si ritiene utile separare contratto da implementazione (sempre vero per i concetti cardine in applicazioni complesse)
quando si prevede la possibilità che varie classi possano voler implementare un medesimo contratto
quando si vogliono costruire funzionalità che possano lavorare con qualunque oggetto che implementi il contratto
caso particolare: Quando si vuole comporre (“has-a”) un qualunque oggetto che implementi il contratto
$\Rightarrow$ l’esperienza mostra che classi riusabili di norma hanno sempre una loro interface
DomusController
rivisitatoDomusController
package it.unibo.interfaces.domo;
public class DomusController {
/* Compongo n oggetti che implementano Device */
private final Device[] devices;
public DomusController(final int size) {
this.devices = new Device[size];
}
/* Aggiungo un device */
public void installDevice(final int position, final Device dev) {
this.devices[position] = dev;
}
/* Rimuovo un device */
public void removeDevice(final int position) {
this.devices[position] = null;
}
public Device getDevice(final int position) {
return this.devices[position];
}
/* Spengo/accendo tutti i device */
public void switchAll(final boolean on) {
for (Device dev : this.devices) {
if (dev != null) { // Prevengo il NullPointerException
if (on) {
dev.switchOn();
} else {
dev.switchOff();
}
}
}
}
/* Verifico se sono tutti accesi */
public boolean isCompletelySwitchedOn() {
for (Device dev : this.devices) {
if (dev != null && !dev.isSwitchedOn()) {
return false;
}
}
return true;
}
}
TV
public class TV implements Device {
/* Campi della classe */
private boolean switchedOn;
/* Costruttore */
public TV() {
this.switchedOn = false;
}
/* Metodi per accendere e spegnere */
public void switchOn() {
this.switchedOn = true;
}
public void switchOff() {
this.switchedOn = false;
}
public boolean isSwitchedOn() {
return this.switchedOn;
}
public String toString() {
return "I'm a TV..";
}
}
DomusController
public class UseDomusController {
public static void main(String[] s) {
// Creo un DomusController
final DomusController dc = new DomusController(10);
// Installo dei dispositivi
dc.installDevice(0, new Lamp());
dc.installDevice(1, new Lamp());
dc.installDevice(2, new Lamp());
dc.installDevice(3, new TV());
dc.installDevice(4, new TV());
dc.installDevice(5, new Radio());
// Li accendo tutti
dc.switchAll(true);
// Verifico l'accensione
final boolean b = dc.isCompletelySwitchedOn(); // true
System.out.println("Completely switched on: " + b);
}
}
implements
come relazione di “sottotipo”Lamp
è un sottotipo di Device
!Lamp
è anche del tipo Device
Lamp <: Device
Ogni classe è sottotipo delle interfacce che implementa!
Se B
è un sottotipo di A
allora ogni oggetto (o valore) di B
può(/deve) essere utilizzato dove un programma si attende un oggetto (o valore) di A
Se la classe C
implementa l’interfaccia I
, allora ogni istanza di C
può essere passata dove il programma si attende un elemento del tipo I
.
class Lamp implements Device { ... }
public void workOnDevice(Device d) { if(d.isSwitchedOn()) { ... } }
Lamp l = new Lamp();
Device d = l; // ok, ci si attende un Device
workOnDevice(l); // ok, il parametro dev'essere un Device
No. Il programma può manipolare gli elementi del tipo I
solo mandando loro i messaggi dichiarati in I
, che sono sicuramente “accettati” dagli oggetti di C
. Il viceversa non è vero.
Nota: I
è più generale di C
, ma fornisce meno funzionalità!
Ve ne sono di diversi tipi nei linguaggi OO
Il polimorfismo inclusivo precisamente l’applicazione del principio di sostituibilità di Liskov
B
è una specializzazione di A
(lo implementa)…B
dove se ne attende uno di A
C
deve “usare” uno o più oggetti di un tipo non predeterminato
I
cattura il contratto comune di tali oggettiC1
, C2
, C3
(e altre in futuro) implementano I
C
allora può focalizzarsi nel solo uso di oggetti di tipo I
(astraendo da eventuali altri dettagli)
C
non avrebbe dipendenze rispetto C1
, C2
, C3
public static void switchOnIfCurrentlyOff(Device device){
// Collegamento dinamico con i metodi da invocare..
if (!device.isSwitchedOn()){
device.switchOn();
}
}
/* Codice cliente */
Lamp lamp = new Lamp();
switchOnIfCurrentlyOff(lamp); // OK, un Lamp è un Device
Accade con le chiamate a metodi non-statici
switchOnIfCurrentlyOff()
mandiamo a device
due messaggi (isSwitchedOn
e switchOn
), ma il codice da eseguire viene scelto dinamicamente (ossia “late”), dipende dalla classe dell’oggetto device
(Lamp
, TV
, …)device
ha tipo Device
(tipo statico), ma a tempo di esecuzione è un Lamp
(tipo run-time)interface I {
void m();
}
class C1 implements I {
void m(){ System.out.println("I'm istance of C1");}
}
class C2 implements I {
void m(){ System.out.println("I'm istance of C2");}
static void m2(){ System.out.println("I'm a static method of C2");}
}
// Codice cliente
I i = Math.random() > 0.5 ? new C1() : new C2();
i.m(); // collegamento al body da eseguire è late, ossia dinamico
C2.m2(); // collegamento al body da eseguire è early, ossia statico
interface
?interface
?public interface I4 extends I1, I2, I3 {
void doSomething(String s);
// da Java 8
double E = 2.718282; // implicitamente public, static, final
default void doSomethingTwice(String s) { doSomething(s); doSomething(s); }
static double PI() { return Math.PI; }
}
Le interface
includano solo intestazioni di metodi!
Dichiarazione possibile: class C implements I1,I2,I3 { ... }
C
implementa sia I1
che I2
che I3
C
deve fornire un corpo per tutti i metodi di I1
, tutti quelli di I2
, tutti quelli di I3
I1
, I2
, I3
avessero metodi in comune non ci sarebbe problema, ognuno andrebbe implementato una volta solaC
hanno tipo C
, ma anche i tipi I1
, I2
e I3
Device
e Luminous
/* Interfaccia per dispositivi */
public interface Device { // Contratto:
void switchOn(); // - si può accendere
void switchOff(); // - si può spegnere
boolean isSwitchedOn(); // - si può verificare se acceso/spento
}
/* Interfaccia per entità luminose */
public interface Luminous { // Contratto:
void dim(); // - si può ridurre la luminosità
void bright(); // - si può aumentare la luminosità
}
public class Lamp implements Device, Luminous {
// ...
public void switchOn(){ ... }
public void switchOff(){ ... }
public boolean isSwitchedOn(){ ... }
public void dim(){ ... }
public void bright(){ ... }
}
Dichiarazione possibile: interface I extends I1,I2,I3 { ... }
I
definisce certi metodi, oltre a quelli di I1
, I2
, I3
C
che deve implementare I
deve fornire un corpo per tutti i metodi indicati in I
, più tutti quelli di I1
, tutti quelli di I2
, e tutti quelli di I3
C
hanno tipo C
, ma anche i tipi I
, I1
, I2
e I3
LuminousDevice
public interface Device {
void switchOn();
void switchOff();
boolean isSwitchedOn();
}
public interface Luminous {
void dim();
void brighten();
}
/* Interfaccia per dispositivi luminosi */
public interface LuminousDevice extends Device, Luminous {
// non si aggiungono ulteriori metodi
}
public class Lamp implements LuminousDevice {
// ...
public void switchOn(){ ... }
public void switchOff(){ ... }
public boolean isSwitchedOn(){ ... }
public void dim(){ ... }
public void brighten(){ ... }
}
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/module-summary.html
java.lang.CharSequence
: contratto per oggetti rappresentanti “sequenze di caratteri” leggibili
String
e StringBuffer
(stringhe mutabili)java.lang.Appendable
: contratto per oggetti su cui “appendere” (ovvero, aggiungere in coda) sequenze di caratteri (CharSequence
)
StringBuffer
java.io.DataInput
: lettura di oggetti di tipi primitivi a partire da un flusso di dati binariojava.io.Serializable
: interfaccia “tag” (vuota, senza metodi) per oggetti “serializzabili”javax.swing.Icon
: interfaccia per icone in interfacce grafichefinal class String implements CharSequence, Serializable, ... { ... }
class ImageIcon implements Icon, Serializable, ... { ... }
interface ObjectInput extends DataInput { ... }
Familiarizzare con: