You are on page 1of 38

Luca Pagani – Riassunto di informatica LB

0 – Ripasso
OPERATORI:
== test di uguaglianza
!= test di disuguaglianza
![boole] not logico
[boole] & [boole] and logico
[boole] | [boole] or logico
[boole] && [boole] and logico condizionale
[boole] || [boole] or logico condizionale
[boole] ^ [boole] xor logico
[int/long] + [int/long] somma
[int/long] - [int/long] sottrazione
[int/long] * [int/long] moltiplicazione
[int/long] / [int/long] divisione senza resto
[int/long] % [int/long] resto della divisione
[int/long] & [int/long] and bit-a-bit
[int/long] | [int/long] or bit-a-bit
[int/long] ^ [int/long] xor bit-a-bit
∼[int/long] not bit-a-bit
[int/long] << n shift a destra di n bit
[int/long] >>> n shift a sinistra di n bit
[int/long] < [int/long] minore
[int/long] <= [int/long] minore o uguale
[int/long] > [int/long] maggiore
[int/long] >= [int/long] maggiore o uguale
x++ aumenta la variabile x di 1
x-- decrementa la variabile x di 1

ARRAY:
int[] array (sequenza) di interi
new int[n] nome crea un nuovo array d’interi di lunghezza n (inizializzato a 0)
(int può essere sostituito con string, float, double, long, etc…)

CONVERSIONE:
<type>number converte il numero [number] nel tipo (compatibile) [type]
Esempio:
(float)5 converte il numero in uno stesso numero di tipo float
(float può essere sostituito con string, int, double, long, etc…)

SIGNATURE:
Descrivere la signature della funzione significa identificare:
- nome della funzione;
- nome dei parametri;
- tipo dei parametri.
Esempio: int mcd(int a, int b) Æ Funzione di nome mcd che dà come risultato un intero e vuole in pasto due
numeri interi a e b .

COSTRUTTO IF (con else, senza else):


Se è vera l’espressione booleana tra parentesi, esegue la funzione 1; se è falsa esegue la funzione 2.
if<espressione booleana>{
corpo della funzione 1
} else {
corpo della funzione 2
}

Se è vera l’espressione booleana tra parentesi, esegue la funzione 1; altrimenti prosegue.


if<espressione booleana>{
corpo della funzione 1
}
Luca Pagani – Riassunto di informatica LB

prosegue…

COSTRUTTO WHILE
Finché è vera l’espressione booleana tra parentesi, il programma esegue la funzione 1
while<espressione booleana>{
corpo della funzione 1
}

COSTRUTTO FOR
Ripete una certa funzione 1 finché è vera l’espressione booleana, ma non solo: risulta anche possibile
dichiarare una variabile caratteristica del ciclo e un’azione da compiere alla fine di ogni “passata” del for.
for<dichiarazione di una variabile, in genere contatore ; espressione booleana ; operazione, in genere
incremento di variabile contatore, da eseguire ad ogni ciclo for eseguito){
corpo della funzione 1
}
… prosegue

COSTRUTTO BREAK
Quando eseguita causa l’uscita immediata dal ciclo; la successiva istruzione eseguita è quella successiva al
ciclo.

COSTRUTTO CONTINUE
Quando eseguita causa il passaggio al prossimo passo del ciclo che è:
- valutazione della condizione (ciclo while)
- incremento (ciclo for)

COSTRUTTO DO
Esegue una certa istruzione finché è vera una certa condizione.
do {
istruzione
} while <espressione booleana>

COSTRUTTO SWITCH
Usato per fare una selezione multipla sulla base di un valore intero.
Se i = 0 esegue l’istruzione 1, se i = 1 esegue l’istruzione 2 etc. altrimenti esegue l’istruzione 4 (di default)
switch(i){
case 0: istruzione 1
case 1: istruzione 2
case 2: istruzione 3
default: istruzione 4
}

RICORSIONE VS. ITERAZIONE


Ricorsione:
- ad ogni passo invoca una funzione;
- è costosa dal punto di vista del tempo e della memoria;
- in caso di loop, satura la memoria a disposizione nello stack.
Iterazione:
- ad ogni passo aggiorna variabili locali o parametri (cambia valori sullo stack);
- molto meno costosa;
- in caso di loop non causa esaurimento di memoria e non si ha la terminazione.

TAIL VS. NON TAIL


Schema tail:
- es. fattoriale;
- le chiamate si aprono fino alla terminazione, che definisce il valore di partenza;
- a quel punto ogni invocazione, prima di ritornare, effettua una computazione.
Schema non tail:
- es. mcd;
- le funzioni computano mano a mano;
Luca Pagani – Riassunto di informatica LB

- arrivati alla terminazione il risultato è pronto;


- le invocazioni si chiudono tutte assieme.

GESTIONE DINAMICA DEI DATI


Gli elaboratori utilizzano uno schema particolare per la memorizzazione dei dati di tipi per riferimento. Un
valore di questi tipi non contiene l’informazione che codifica il dato in questione; in realtà è un riferimento
(o puntatore) ad un’area di memoria che contiene l’effettiva informazione (chiamata memoria heap).
Concettualmente, la memoria heap è una sequenza di caselle: ogni casella ha un indirizzo e un contenuto e,
dunque, è molto simile a una variabile.

MODULI IN JAVA
Un modulo rappresenta una collezione di funzionalità con uno scopo globale comune. I moduli sono stati
introdotti per fornire un’astrazione di lavoro più estesa della funzione, utile per organizzare opportunamente
codici di grandi dimensioni.
Un modulo è caratterizzato da un nome (identificatore): comincia in maiuscolo, ed è una sola parola.
È inoltre realizzato tramite il meccanismo delle classi di Java (v. oltre).
Un modulo è poi costituito da un insieme di definizione di proprietà [NOTA: siamo ancora nell’ottica di una
programmazione in-the-small]:
- funzioni (o metodi): usando le classi come modulo, tali funzioni devono riportare la parola chiave
static all’inizio;
- variabili globali: così come a volte è necessario memorizzare i risultati parziali delle funzioni dentro
a una delle loro variabili (locali), a volte può essere utile disporre di variabili globali a tutto il modulo.
Esse sono definite con la stessa sintassi già vista per i metodi (aggiungendo la parola chiave static);
- procedure: sono come le funzioni, ma non ritornano alcun risultato in uscita. Hanno come tipo di
ritorno il tipo speciale void, dato appunto che nel suo corpo non si ritorna alcun valore, e dunque la
sua invocazione è usata puramente come istruzione;
- inizializzatori: sono procedure speciali che vengono eseguite prima di qualunque accesso al modulo.
Due sono i possibili modificatori:
- public: la proprietà è visibile da fuori;
- private: la proprietà non è visibile da fuori.

JAVA VIRTUAL MACHINE (JVM)


Si tratta di un programma eseguibile che accetta un codice binario (file di estensione .class): la JVM
interpreta tale codice (ottenuto tramite compilazione), sostituendosi virtualmente all’H/W.
Per eseguirla bisogna fare riferimento al file java.exe .

ADT (Abstract Data Type)


Sono tipi di dato per i quali non si descrive in che modo preciso i valori sono codificati e gli operatori
lavorano e lo si fa, piuttosto, in termini di una rappresentazione astratta, detta simbolica.
Quali utilizzi?
- Per definire ad un elevato livello d’astrazione un tipo di dato relativo a strutture complesse (grafi,
alberi, liste…), definendo possibili modi di manipolarlo;
- certi linguaggi di programmazione supportano poi la possibilità di realizzare questi ADT, permettendo di
definire nuovi tipi (es. numeri complessi), specificando valori ed operatori.
Per definire un ADT significa specificare:
- il suo nome;
- i costruttori (che ne creano i valori);
- funzioni (insieme delle signature di funzioni usate per manipolare i valori);
- assiomi (regole per definire la semantica delle funzioni, per riscrivere e trasformare i dati).
Un ADT è una specifica per chiunque intenda progettare un nuovo tipo di dato:
- non descrive in che modo codificare i valori;
- non dice come realizzare gli operatori;
- descrive, bensì, quali caratteristiche deve avere una qualunque realizzazione del tipo di dato, in termini di
valori ed operatori, affinché sia considerabile come corretta.
Luca Pagani – Riassunto di informatica LB

1 – Programmazione, algoritmi e sistemi


La programmazione è l’attività con cui si risolvono problemi mediante lo sviluppo di algoritmi.
Un algoritmo è un metodo per la soluzione di un problema adatto ad essere implementato sotto forma di
programma.
I programmi possono avere una natura e una complessità notevolmente diversa in base alla natura del
problema: si va da problemi di pura natura algoritmica (dato un input, si riceve un output – programmazione
“in the small”) ai problemi complessi che richiedono interazioni dinamiche (con l’utente, con l’interfaccia
grafica, col file system – programmazione “in the large”).
Un sistema è costituito da più elementi interdipendenti e interagenti, uniti tra loro in modo organico;
l’ambiente di un sistema è tutto ciò che è fuori dal sistema e con cui il sistema interagisce dinamicamente
mediante I/O. Ogni sistema è caratterizzato da forme di interazione, sia fra le parti che lo costituiscono, sia
nei confronti dell’ambiente in cui è immerso. Un’applicazione software include sia aspetti sistemici, sia
aspetti computazionali / algoritmici.
Programmare un’applicazione implica affrontare il gap che sussiste fra problema e soluzione, secondo un
certo insieme di fasi:
- analisi del problema;
- ideazione della soluzione sulla base del programma;
- implementazione della soluzione utilizzando il linguaggio più opportuno;
- esecuzione del programma su un certo ambiente di esecuzione.
Ecco alcuni principi guida della programmazione:
- decomposizione: dato un problema complesso, lo si suddivide in un certo insieme di sotto-
problemi più semplici, che possono essere risolti in modo indipendente (approccio top-down). Nella
programmazione strutturata, le funzioni e le procedure promuovono la decomposizione di un
problema algoritmico in sottoproblemi, difendendo funzioni/procedure che a loro volta chiamano
altre funzioni/procedure; l’utilizzo sopra descritto non implica la conoscenza di come la funzione
operi o sia implementata, ma solamente della sua signature. Questa possibilità di usare le funzioni
come “scatole nere” prende il nome di procedural abstraction: la funzione diventa un componente
(ri)usabile semplicemente conoscendone la sola interfaccia, a prescindere dalla sua effettiva
implementazione. Nella programmazione strutturata viene introdotto il concetto di modulo per
aggregare un insieme di funzioni/procedure correlate, utili per la risoluzione di una determinata
classe di sottoproblemi. La caratteristica fondamentale dei moduli è la separazione fra interfaccia ed
implementazione, con l’implementazione nascosta all’utilizzatore (information hiding);
- astrazione: per astrazione intendiamo qui un’entità astratta (concettuale) definita e utilizzata per
rappresentare la soluzione al problema, ovvero per definirne un modello. L’obiettivo di un modello
è mettere in evidenza tutti gli aspetti interessanti tralasciando tutti gli aspetti ritenuti non
significativi. L’applicazione di tecniche di decomposizione/astrazione nel caso di strutture dati è utile
per la creazione di nuovi tipi di strutture dati, come composizione di strutture dati di tipo primitivo.
La metodologia al centro di questo approccio prende il nome di data abstraction: allo scopo, i tipi di
dati astratti (ADT) sono astrazioni utili per rappresentare e definire questi nuovi tipi;
- composizione e riuso: è un principio duale al primo, che porta a definire una soluzione ad un
problema a partire da soluzioni già a disposizione relative a altri problemi, in generale riusando e
componendo astrazioni già definite e pronte all’uso;
- separazione: porta ad individuare e a separare all’interno di un problema di natura informatica gli
aspetti puramente algoritmici dagli aspetti che coinvolgono forme di interazione, e quindi affrontarli
separatamente. A tal proposto, la programmazione orientata agli oggetti (OOP – Object Oriented
Programming) introduce astrazioni e meccanismi ritenuti efficaci per applicare i principi visti nella
programmazione di applicazioni e sistemi software di una certa complessità.
Se la programmazione strutturata si basa su astrazioni quali strutture dati + funzioni/procedure, la
programmazione orientata agli oggetti è basata sull’unica astrazione di oggetto, come entità di prima classe
per modellare qualsiasi entità della soluzione.
Luca Pagani – Riassunto di informatica LB

2 – Sistemi operativi e programmazione di sistema

L’architettura di un moderno e general-purpose computer è data da una CPU e da un insieme di device


controller e adapters connessi attraverso un bus comune, che ne abilita l’interazione con la memoria
centrale. Il modello astratto di questa architettura è la macchina di Von Neumann: la memoria contiene
dati e programma, la CPU carica ed esegue le istruzioni della memoria (fetch), le deposita nell’apposito
registro (instruction register) e, infine, decodifica l’istruzione dando il via ad ulteriori caricamenti o
aggiornamento dei registri.
- Una CPU è caratterizzata da un insieme di registri: tra essi il program counter (PC) contiene l’indirizzo
dell’istruzione da eseguire mentre, a supporto dell’esecuzione della CPU, c’è lo stack, che è utilizzato per
memorizzare/ripristinare il valore del PC quando il controllo della CPU viene in qualche modo trasferito da un
punto ad un altro del codice per il passaggio dei parametri.
- Ogni device controller controlla uno specifico tipo di device: la CPU e i device controller sono entità
autonome, che competono per accedere alla memoria condivisa, e che sono coordinate dal memory
controller.
- La memoria in generale è la risorsa con lo scopo di contenere dati, e che è possibile manipolare con
operazioni di lettura e scrittura. Esiste una completa gerarchia di tipi di memoria dalla più costosa/veloce alla
più capace/lenta (registri, cache, memoria principale, disco elettronico, disco magnetico, disco ottico, nastri
magnetici…). I programmi devono risiedere nella memoria principale (RAM, Random Access Memory) per
essere eseguiti; la memoria è costituita da insieme contiguo (array) di celle, dette parole (word) di memoria,
accessibili direttamente dalla CPU per essere lette o scritte e univocamente riferite da un indirizzo. La CPU
interagisce con la memoria mediante delle istruzioni di load, con cui carica nei registri una parola di un dato
utilizzo, e store, con cui si scrive il contenuto di un registro in una parola in memoria.
La memoria principale è volatile: il suo contenuto, cioè, non persiste allo spegnimento del computer. Per la
memorizzazione persistente delle informazioni, invece, si utilizza la memoria secondaria, il cui obiettivo è
mantenere in modo persistente grandi quantità di dati, e che è superiore (come capienza) di due/tre ordini
di grandezza rispetto la memoria principale.
Tipicamente gli hard disk (memoria secondaria) contengono migliaia di cilindri e ogni traccia può contenere
centinaia di settori. La velocità di rotazione varia da 60 a 250 giri per secondo ed è misurata in transfert rate
(quantità di dati trasferiti per unità di tempo) e random-access time (somma del tempo impiegato per
posizionare il braccio sul cilindro – seek time – e del tempo impiegato per il posizionamento sul settore
voluto – rotational latency).
Una parte fondamentale dei sistemi informatici moderni è data dalla rete (network): sistemi
U

software/hardware più o meno complessi sono realizzati da un insieme più o meno grande e strutturato di
computer connessi in rete, in grado di comunicare. Si parla di sistemi distribuiti (distributed systems). I due
tipi principali e più diffusi di rete sono due:
- LAN (Local Area Network): è data da un insieme relativamente piccolo di computer, localizzati nella
stessa rea geografica;
- WAN (Wide Area Network): una WAN (es. Internet) connette una moltitudine di computer,
sottoreti, LAN distribuite in ampie aree geografiche. Data la mole di computer, queste reti sono
strutturate in sottoreti, connesse tra loro da appositi link di comunicazione, dette communication
processor, responsabili di interfacciare fra loro reti divise. Nel caso di Internet i communication
processor prendono il nome di router, che connettono una sottorete ad un’altra.

Il sistema operativo (SO – OS in inglese) è quel programma che funge da intermediario fra utenti e
hardware del computer, permettendo l’esecuzione di programmi (applicazioni) e coordinandone l’accesso alle
risorse. Un SO ha l’obiettivo di: eseguire i programmi degli utenti, rendere agevole l’utilizzo delle risorse del
computer e sfruttare in modo efficiente le risorse dello stesso. Esistono poi due livelli: a livello hardware ci
sono le risorse computazionali e interattive di base: CPU, memoria, sottosistemi di I/O; a livello
applicazione ci sono le applicazioni e sistemi che risolvono problemi o offrono funzionalità utili per gli
utenti.
Il sistema operativo media l’interazione fra livello applicazione e livello hardware, controllando e coordinando
l’accesso e l’uso del livello hardware richiesto dal livello applicazione, e rendendo i due livelli il più possibile
indipendenti fra loro; inoltre, il sistema operativo fattorizza le esigenze comuni alle applicazioni in servizi,
che le applicazioni possono direttamente usare. Tra i sistemi operativi più diffusi: la famiglia UNIX (tra cui
Linux – open source e gratis), la famiglia Windows (tra cui Windows XP), Mac OS X, Solaris.
Molti dei sistemi operativi sviluppati in ambito accademico e di ricerca sono open-source: i sorgenti,
ovvero, sono disponibili liberamente in rete, e ciò assicura uno sviluppo cooperativo e libero.
Luca Pagani – Riassunto di informatica LB

A seconda del punto di vista adottato, un sistema operativo ha obiettivi diversi: da un punto di vista “utente”
i principali fattori sono usabilità, performance e ottimizzazione delle risorse; da un punto di vista del
“sistema”, l’OS ha il compito di gestire e allocare l’accesso alle risorse hardware.
Un sistema operativo è tipicamente organizzato in un insieme di componenti che interagiscono fra loro per
realizzare nel complesso le funzionalità del SO. Fra le varie componenti, il kernel (nucleo) costituisce il
cuore del sistema operativo, come componente responsabile di aspetti vitali del funzionamento di
quest’utlimo. Un’altra componente importante è il sottosistema grafico per l’interazione con gli utenti (GUI,
Graphical User Interface), componente fondamentale nei desktop.
Oggi la rete è un componente fondamentale dei sistemi informatici, al cuore dei sistemi distribuiti, ovvero
insiemi di computer collegati tra loro in grado di comunicare. I sistemi operativi forniscono
servizi/protocolli di base per la gestione della rete, ed in particolare la comunicazione fra applicazioni di
computer distinti. Un tipo che architettura ampiamente utilizzata è quella client-server, per cui un
computer server (potente e performante) ospita e fornisce servizi a disposizione per il computer client, che
ne richiede e sfrutta i servizi da remoto.
Il WWW (World Wide Web) è un tipico sistema client-server. Da una parte abbiamo il web-server, su cui è
installato e attivo un sito internet; dall’altra i client web accedono al sito mediante programmi come web
browser, interagendo opportunamente con il server web mediante protocollo HTTP. Una estensione del
WWW è data dai Web Services, con cui si utilizza il Web come protocollo di base per realizzare sistemi basati
sui servizi.
I sistemi operativi moderni sono multiutente, ovvero permettono l’accesso (simultaneo) al medesimo
computer da parte di più utenti. È dunque di fondamentale importanza allora fornire politiche di controllo
degli accessi alle risorse e più in generale di protezione, per evitare che gli utenti possano danneggiare
volontariamente o involontariamente le risorse degli altri utenti e del sistema operativo. A tale scopo, i
sistemi operativi moderni permettono di definire un insieme di ruoli con cui suddividere gli utenti che usano
il sistema (amministratore, utente etc….). Per ogni utente è possibile definire un account, ovvero un profilo
che identifica l’utente stesso dal punto di vista del sistema; un account è in generale caratterizzato da uno
user name e da una password . Ogni utente, infatti, prima di iniziare una sessione di lavoro su un sistema
deve autenticare la propria identità: ciò viene fatto nella fase di login.
I sistemi operativi – come già detto - sono anzitutto ambienti che supportano l’esecuzione di
programmi/applicazioni: l’astrazione con cui nei sistemi operativi si definisce e identifica un programma in
esecuzione prende il nome di processo (task); i sistemi operativi sono multitasking e permettono, ovvero,
l’esecuzione simultanea di più processi, fornendo meccanismi di base per la comunicazione e
sincronizzazione dei processi.
Il file-system è quella parte dell’SO che fornisce i meccanismi di accesso e memorizzazione delle
informazioni (programmi e dati) allocate nella memoria di massa. Il file system realizza i concetti astratti
di: file (come unità logica di memorizzazione), directory (come insieme di file e di directory stesse) e
partizione (come insieme di file associato ad un particolare dispositivo fisico). Le caratteristiche di file,
directory e partizione sono del tutto indipendenti dalla natura e dal tipo di dispositivo utilizzato.
L’interprete comandi o shell è un programma presente in tutti i SO che permette di interagire con il
sistema, in particolare con il file system, mediante dei comandi con funzionalità varie. Tale programma ha
nomi diversi a seconda del sistema operativo: command-line interpreter, shell, terminal, prompt comandi
etc... Il programma legge ed interpreta i comandi direttamente forniti in input dall’utente o specificati in un
file di testo nel linguaggio specifico. In generale si indica con il termine sessione l’intera fase in cui un
utente esegue l’interprete comandi, vi esegue comandi e quindi chiude l’applicazione.

Un file è un insieme di informazioni rappresentati come insieme di record logici (bit, byte, linee, record etc.).
Ogni file è individuato da almeno un nome simbolico mediante il quale esso può essere riferito (ad esempio,
nell’invocazione dei comandi) e da un insieme di attributi:
- tipo: stabilisce l’appartenenza a una classe;
- indirizzo: puntatore a una memoria secondaria;
- dimensione: numero dei byte contenuti nel file;
- utente proprietario;
- data di creazione, di modifica etc…
Gli attributi di un file sono generalmente incapsulati in una struttura dati chiamata descrittore del file.
Ogni descrittore di file deve essere memorizzato in modo persistente: il SO mantiene l’insieme dei descrittori
di tutti i file presenti nel file system in apposite strutture in memoria secondaria.
In UNIX ad esempio i descrittori di file sono chiamati i-node e la struttura che contiene la lista di tutti gli i-
node è chiamata i-list.
È compito dell’OS consentire l’accesso on-line dei file; le tipiche operazioni che caratterizzano tale accesso
sono:
- creazione: allocazione di un file in memoria secondaria;
Luca Pagani – Riassunto di informatica LB

- lettura: di record logici all’interno di file;


- scrittura: inserimento di nuovi record logici all’interno di file;
- cancellazione: eliminazione del file dal file system.
Siccome ogni operazione richiede la localizzazione di parecchie informazioni su disco, con un alto costo a
livello computazionale, si è escogitato di mantenere in memoria centrale una struttura – la tabella dei file
aperti – che tiene traccia dei file che sono attualmente in uso. Spesso, dunque, il contenuto dei file aperti
viene temporaneamente copiato in memoria centrale (memory mapping) in modo da ottenere accessi più
veloci. Nasce dunque un problema di consistenza fra le informazioni relative al contenuto del file in memoria
secondaria ed in memoria centrale: l’operazione di flushing è l’operazione con cui si forza l’aggiornamento
delle informazioni su memoria secondaria a partire da quelle in memoria centrale.
Per ogni file aperto, tipicamente i SO mantengono come informazioni necessarie per la gestione:
- file pointer: che punta all’ultima locazione in cui c’è stata lettura/scrittura;
- file-open count: contatore del numero di volte in cui è stato aperto un file;
- diritti d’accesso: per ogni processo, specifica la modalità d’accesso.
Dal punto di vista fisico, ogni dispositivo di memorizzazione secondaria viene partizionato in blocchi (record
fisici); dal punto di vista logico, l’utente vede un file come un insieme di record logici. Uno dei compiti
dell’SO è dunque quello di stabilire una corrispondenza fra record logici e blocchi.
L’accesso a un file può avvenire secondo varie modalità:
- accesso sequenziale: in questa modalità d’accesso, il file è una sequenza di record logici e, per
accedere ad un particolare record logico Ri, è necessario accedere prima agli (i-1) record che lo
precedono in sequenza;
- accesso diretto: in questa modalità d’accesso, il file è un insieme non ordinato di record logici
numerati: si può accedere direttamente ad un particolare record logico specificandone il numero.
Questa modalità è utilissima nei database;
- accesso ad indice: ad ogni file viene associata una struttura dati (ancora un file) contenente
l’indice delle informazioni contenute nel file.

La directory (direttorio) è lo strumento per organizzare i file all’interno del file system: una directory può
contenere più file, ed è realizzata mediante una struttura dati che associa al nome di ogni file la posizione
del file nel disco. Risulta utile per localizzare i file in maniera efficiente e per stabilire un raggruppamento
logico di questi ultimi. La struttura logica delle directory può variare a seconda dell’OS. Gli schemi più comuni
sono:
- struttura a un livello: c’è una sola directory per file system;
- struttura a due livelli: il file system è strutturato su due livelli: il primo livello contiene una
directory per ogni utente del sistema, il secondo livello contiene le directory degli utenti (a un
livello);
- struttura ad albero: la struttura ad albero è una generalizzazione della precedente soluzione,
consentendo una organizzazione gerarchica a N livelli. Ogni direttorio può contenere più file e altri
direttori. La soluzione ad albero permette di avere ricerche efficienti e di poter raggruppare in modo
flessibile i file: rimane – tuttavia - il problema per cui due utenti non possono condividere
direttamente un file. I file all’interno di strutture ad alberi sono riferiti mediante nomi simbolici detti
pathname;
- struttura a grafico aciclico: la generalizzazione del caso precedente consiste nell’avere una
struttura a grafo, quindi con nodi (directory) che possono condividere “figli” (file o directory).
In sistemi operativi come UNIX, un file deve essere aperto prima di essere usato; analogamente, un file
system deve essere montato (mounted) prima di poter essere disponibile ai processi del sistema. Un file
sistema non ancora montato viene montato in un cosiddetto punto di mount (mount point) e può essere
successivamente smontato (unmounting).

La condivisione di file è un aspetto importante per i sistemi multiutente; nei sistemi distribuiti i file
possono essere condivisi fra host remoti, mediante opportune architetture o protocolli (tra cui il Network File
System – NFS – è uno dei più diffusi); altrimenti si può utilizzare il modello client-server (abbiamo ancora
NFS e poi CIFS, il protocollo standard Windows). Infine, i distributed information system sono infrastrutture
che implementano un accesso unificato alle informazioni remote.

Per programmazione di sistema s’intende la programmazione del sistema operativo o di alcune sue parti,
oppure lo sviluppo di programmi che concettualmente estendono il sistema operativo con ulteriori
funzionalità.
Il linguaggio storico per la programmazione di sistemi operativi è il linguaggio C, dal quale sono poi derivati
altri linguaggi (Object Oriented): C++, Objective-C, C#, Java (indirettamente).
Luca Pagani – Riassunto di informatica LB

Il C è di per sé un linguaggio di pura elaborazione, senza meccanismi a livello di linguaggio per


l’interazione/comunicazione, di I/O; è anche un linguaggio imperativo e procedurale, in quanto i
programmi sono strutturati in funzioni/procedure con sintassi molto simile a quella di Java statico; infine, il
controllo sui tipi è debole: il compilatore non assicura le proprietà di appartenenza di una variabile ad un
unico tipo.
I punti forti del C sono:
- flessibilità: si usa per applicazioni di qualsiasi tipo;
- portabilità: il C si usa in tutte le piattaforme (Unix, Windows, MSDOS, AIX, Mac OS…);
- semplicità: il C si basa su pochi concetti elementari;
- efficienza: la sua natura imperativa/procedurale porta ad avere forme compilate molto efficienti.
I punti deboli del C sono:
- basso livello d’astrazione: mancanza di astrazioni utili a risolvere i programmi, mancanza di
controllo stretto sui tipi, gestione della memoria totalmente a carico del programmatore…
- complessità notevole nella creazione di programmi di grandi dimensioni.
Luca Pagani – Riassunto di informatica LB

3 – Programmazione orientata agli oggetti

Il paradigma ad oggetti ha avuto pieno sviluppo dai primi anni ’80 fino ad oggi. Ecco cinque caratteristiche
fondamentali di questa filosofia:

1) Ogni cosa è un oggetto.


Gli oggetti sono le uniche entità con cui modellare lo spazio del problema: un oggetto ci permette di
rappresentare qualsiasi entità dotata di uno stato, caratterizzata allo stesso tempo da un determinato
comportamento computazionale. Tale comportamento è definito da un insieme di metodi, ovvero operazioni
di proprietà dell’oggetto che fungono da servizi che l’oggetto offre ai suoi possibili utilizzatori. Un oggetto è
dunque un’entità persistente, ovvero con uno stato, che vive in un contesto spazio-temporale.

2) Un programma è un insieme di oggetti che interagiscono mediante lo scambio di


messaggi.
Un modo intuitivo ed elegante di interpretare l’interazione con un oggetto è basato sulla nozione di scambio
di messaggi: un utente interagisce con un oggetto inviandogli un messaggio, con cui “richiede” l’esecuzione
di un metodo. L’oggetto risponde all’invio del messaggio eseguendo il metodo corrispondente; tale
esecuzione può portare alla generazione di informazioni di ritorno, che vengono restituite al cliente. È utile
considerare l’oggetto come un’entità a cui un utilizzatore si rivolge direttamente, in prima persona: dunque i
nomi dei metodi sono spesso imperativi, in quanto richieste di esecuzione di determinati servizi.
Un’applicazione è per questo inquadrabile come un sistema composto da un insieme dinamico di oggetti
che interagiscono mediante lo scambio di messaggi (invocazione di metodi).

3) Ogni oggetto ha la propria memoria, composta da altri oggetti.


Adottando la visione per cui tutto è un oggetto, allora i dati interni che caratterizzano la struttura di un
oggetto (campi) saranno definiti in termini di altri oggetti. Tali oggetti non sono visibili al di fuori dell’oggetto
e sono acceduti e manipolati solamente dai metodi dell’oggetto stesso, sia pubblici che privati. Come “fondo”
di questa struttura “ricorsiva” i linguaggi OO mettono a disposizione oggetti elementari che rappresentano
valori di tipi di dati semplici (numeri, caratteri, stringhe…).

4) Ogni oggetto è istanza di una classe.


Nella programmazione ad oggetti emerge la necessità di avere più istanze del medesimo tipo d’oggetto.
Questo aspetto è catturato dalla nozione di classe, che identifica una categoria, famiglia, tipo di oggetti. Nei
linguaggi OO ogni oggetto è istanza (realizzazione concreta) di una classe, che è la descrizione della
struttura e del comportamento che caratterizza tutti gli oggetti istanza di tale classe.
La creazione dinamica di un oggetto avviene mediante uno speciale operatore (new) specificando quale sia
la classe che ne descrive la struttura e il comportamento. Attenzione: le classi non esistono come entità
concrete, ma sono solo pure descrizioni. Gli oggetti invece “esistono”, creati con struttura e comportamento
definiti dalla classe a cui appartengono.
La sintassi per definire una classe è:
public class nome classe {
campi
costruttori
metodi
}
Per convenzione il nome della classe deve avere iniziale maiuscola e il resto minuscolo; essa è caratterizzata
da un nome e dalla descrizione dei suoi attributi, ovvero:
- campi – definiti anche data member; costituiscono la struttura vera e propria della classe, struttura
che tipicamente vogliamo tenere privata (information hiding). La definizione dei campi consiste in
una linea tipo
private <tipo campo – es. int, double…> <nome campo>
Per ogni oggetto istanza di una classe i campi costituiscono lo stato dell’oggetto. Esso, tipicamente,
evolve man mano che l’oggetto interagisce con il mondo esterno, ovvero ne sono invocati
metodi/operazioni che ne cambiano tale stato. Quest’ultimo è nascosto all’utilizzatore: i campi
(privati) non sono infatti mai acceduti direttamente da chi interagisce con l’oggetto. In Java è
possibile definire campi pubblici, ma è da evitare, così come non è consigliabile definire campi
statici. NOTA: in Java le costanti si definiscono come campi statici pubblici o privati di una classe, col
descrittore final.
Esempio:
Luca Pagani – Riassunto di informatica LB

private static final <tipo costante> <nome costante>


- metodi – definiti anche funzioni membro. I metodi costituiscono il comportamento degli oggetti
della classe. La definizione di un metodo è molto simile alla definizione di una funzione: si
specificano parametri d’ingresso e tipo di parametro d’uscita.
Esempio:
public <tipo di parametro di ritorno> <nome metodo><variabili d’entrata>{
(…)
}
È possibile definire anche metodi privati: mentre quelli pubblici costituiscono l’interfaccia, questi
sono utilizzati come operazioni interne non richiamabili dall’esterno. Un metodo può avere anche
delle variabili locali.
Esempio:
private <tipo di parametro di ritorno> <nome metodo><lista dei parametri>{
(…)
}
Fra i metodi ve ne sono alcuni detti costruttori, che vengono invocati solo all’atto della creazione
dinamica di un oggetto. Un costruttore è una sorta di metodo speciale, invocato automaticamente
all’atto di creazione dell’oggetto, per poterne inizializzare lo stato.
Esempio:
Counter c = new Counter(15);
Se non si definiscono esplicitamente costruttori, in una classe viene definito automaticamente il
costruttore di default, che non ha parametri e corpo vuoto. I costruttori sono infine definiti come
metodi con lo stesso nome della classe e senza parametri di ritorno. Anch’essi possono essere sia
pubblici che privati.

5) Ogni oggetto ha un’interfaccia, che definisce la natura dei messaggi esso può ricevere,
ovvero il tipo dell’oggetto.
Una prima proprietà importante dell’astrazione di oggetto è l’incapsulamento, ovvero la proprietà di
definire nella medesima astrazione sia aspetti strutturali, relativi ai dati che definiscono la struttura di un
oggetto, sia le procedure/funzioni (metodi) che operano su tale struttura. L’insieme di questi metodi
costituisce l’interfaccia (o protocollo) dell’oggetto, ovvero l’insieme delle richieste che un cliente può fare
all’oggetto e quindi l’insieme delle possibili interazioni. Accanto ai metodi che costituiscono l’interfaccia,
cosiddetti metodi pubblici, un oggetto può avere un insieme di metodi privati, non visibili agli utilizzatori,
come servizi ausiliari utilizzati dai metodi pubblici. Nell’ottica cliente/servitore, l’interfaccia può essere
concepita come contratto che l’oggetto si impegna a rispettare nei confronti dei suoi utilizzatori, in termini
di servizi forniti e caratterizzazione dell’operazione da effettuarsi. Quando, in un mondo ad oggetti, il cliente
non ha la percezione dello stato dell’oggetto né di come sia implementato, allora viene rispettato il principio
base dell’information hiding e tale stato è osservabile solo nella misura in cui lo permettono i metodi
forniti dall’interfaccia.

Dato il sorgente di una classe (*.java), questo dev’essere compilato creando la versione binaria della
classe, affinché sia utilizzabile dalla JVM. La compilazione ha anche il vantaggio di individuare eventuali errori
(di sintassi, di tipo, etc.) presenti nel sorgente.
Esempio (in prompt, utilizzando il compilatore Javac):
javac <viene invocato il compilatore> –d <per specificare dove verrà creato il bytecode, ovvero
nella cartella bin> bin <cartella di destinazione> src/Counter.java <file da compilare>.

L’astrazione di classe vista ci permette di implementare concretamente la nozione di Abstract Data Type:
una classe definisce/implementa un tipo di dato, caratterizzandolo in termini sia strutturali che
comportamentali, ovvero definendo l’insieme delle operazioni con cui è possibile manipolare le entità di tale
tipo. È tuttavia importante aver presente una sottile ma fondamentale differenza fra la nozione di ADT e la
nozione di classe: tipicamente le operazioni definite negli ADT sono funzioni, nel caso di oggetti/classi sono
metodi che cambiano lo stato dell’oggetto su cui sono invocati. Questa differenza è ben evidenziabile
considerando l’esempio classico della lista; costruita con gli ADT, essa non è un’entità con stato ma con
un’entità con valore, e viene di conseguenza definito un insieme di operazioni di base per manipolare tali
valori. Nel caso di lista costruita come classe, trattasi essa stessa di un’entità con stato completa di metodi
che permettono di variarne lo stato aggiungendo, rimuovendo o acquisendo elementi.

In Java è possibile definire classi all’interno di classi (inner classes): la visibilità di tali classi è limitata alla
classe stessa. Si utilizza questa prassi quando occorre definire classi ausiliarie ad una specifica classe, la cui
Luca Pagani – Riassunto di informatica LB

presenza, realizzazione e utilizzo devono essere ignorati dalle altre classi. Sebbene ci siano vari modi per
definire le classi interne, l’idioma classico prevede la definizione delle classi interne come static private.
È poi possibile definire più classi nel medesimo sorgente Java (solo una dev’essere pubblica), ma tale
possibilità non è di frequente utilizzo: si preferisce includere ogni classe in un singolo file sorgente e
magari, in seguito, raggruppare tanti file in moduli o archivi jar.

Abbiamo già accennato alla funzione dell’operatore new nella creazione di un oggetto: ebbene, esso crea un
nuovo oggetto della classe specifica, invocando il costruttore per la sua inizializzazione, e poi restituisce un
riferimento all’oggetto stesso. Se per qualche motivo si perde questo riferimento, un componente della
macchina virtuale chiamato Garbage collector elimina l’oggetto non più riferito che, in quanto tale, è
considerato “spazzatura”.
I riferimenti sono il mezzo per interagire con gli oggetti, e possono essere contenuti in variabili. Queste
variabili seguono le regole viste per le variabili “normali” e, in particolare, possono essere assegnate con il
valore di altre variabili, di tipo compatibile: in questo modo è possibile ottenere più variabili che contengono
il riferimento al medesimo oggetto.

In Java, l’operatore ==, se utilizzato con variabili di tipo riferimento, testa l’uguaglianza dei riferimenti (non
degli oggetti!), ovvero valuta se due variabili puntano allo stesso oggetto.
Per specificare che una variabile non riferisce alcun oggetto si utilizza la costante null:
Esempio:
Counter c = null;

L’invocazione dei metodi avviene mediante il riferimento, che funge da “telecomando” per interagire con
l’oggetto (inviandogli messaggi, ovvero invocando metodi). Nel caso di Java per l’invocazione di metodi si
utilizza la notazione puntata, tipo
<riferimento oggetto> . <nome metodo> < parametri attuali >
In Java il passaggio dei parametri nei metodi è per valore: tutti i parametri sono copiati sullo stack, nel
record di attivazione associato al metodo invocato. Questo significa che un metodo non può modificare i
riferimenti e i valori passati come parametri.
Esempio:
base = v;
v = v + base;
Base vale ancora v!

NOTA: In Java è possibile definire più costruttori e più metodi con lo stesso nome (overloading): tuttavia,
essi devono essere distinguibili per il tipo di parametri forniti in ingresso!

I linguaggi OO forniscono in generale la possibilità di utilizzare le classi anche per definire moduli (statici),
in termini di collezioni (librerie) di procedure correlate, senza alcuna nozione di oggetto/stato. In Java una
classe modulo si realizza dichiarando come statici tutti i metodi che contiene, ovvero antecedendo il
descrittore static alla definizione del metodo.
Per invocare un metodo statico m di una classe C si utilizza sempre la notazione puntata; al posto di
utilizzare un riferimento ad un oggetto si utilizza direttamente il nome della classe:
es. double value = Math.sin(Math.PI/6).
È dunque importante distinguere:
- classi che descrivono la struttura di un oggetto e fungono da template: NO metodi statici;
- classi che fungono da moduli (es. modulo math): SOLO metodi statici.

In Java si utilizza la keyword this per denotare, all’interno di metodi (e costruttori), il riferimento
all’oggetto stesso su cui è stato invocato un metodo; è usato anche per riferire i campi dell’oggetto in caso di
ambiguità di nome (rispetto ai parametri passati al costruttore o al metodo). This è utilizzato in particolare
quando nel codice di un metodo M (di un oggetto O) si vuole invocare un metodo su un altro oggetto
passando esso stesso (oggetto O) come argomento.

Nei linguaggi function/PO (procedure oriented), il punto d’ingresso di un programma è tipicamente dato
dalla funzione main, che viene invocata automaticamente al momento in cui un programma è mandato in
esecuzione. Java mantiene un approccio simile: il punto d’ingresso di un programma è dato da un metodo
statico main, definita in una classe che diventa la main class del programma. Di norma, quest’ultima
contiene solo il metodo main (ed eventualmente altri metodi ausiliari statici).
Luca Pagani – Riassunto di informatica LB

Per stampare su video semplici output dati esiste una classe specifica, ConsoleIO, che è possibile trovare
nella libreria console.jar. Alcuni metodi di base, per l’output, sono ad esempio:
- void printString(String st);
- void printInteger(int i);
- void printReal(double d).
Mentre per l’input:
- String readString().

Java è un linguaggio OO quasi puro: oltre agli oggetti esistono delle entità che non sono oggetti, ma sono
valori di tipo semplici (primitivi), ovvero numeri (interi e reali), caratteri, boolean. Quindi, accanto a variabili
che contengono riferimenti, possiamo avere variabili il cui tipo è primitivo e che contengono direttamente
valori (e non riferimenti). In realtà, in Java, per ogni tipo di dato primitivo esiste una classe corrispondente,
definita classe wrapper, che rappresenta il medesimo tipo di dato in termini d’oggetti.

In Java le stringhe sono oggetti della classe String. Il linguaggio, in realtà, mette a disposizione degli
operatori che ne permettono la manipolazione e l’uso in modo più diretto: l’operatore +, ad esempio,
concatena due oggetti stringhe a tempo di compilazione. Le stringhe sono trattate come oggetti ma senza
stato, come puri valori.
Ecco alcuni metodi della classe String:
- char charAT(int index) Æ recupera il carattere di posizione index;
- int length() Æ ottiene la lunghezza della stringa;
- boolean equals(String st) Æ verifica se due stringhe sono uguali;
- int indexOf(char ch) Æ ottiene l’indice del carattere specificato, se presente;
- String substring(int beginIndex) Æ recupera la sottostringa a partire dalla posizione
specificata;
- s0.equals(s1) Æ restituisce se due stringhe s0 e s1 sono uguali;
- s0.compareTo(s1) Æ testa l’ordine lessicografico di due stringhe e restituisce un intero<0 se
s0<s1, 0 se s0 e s1 sono uguali, un intero>0 se s0>s1.

Anche gli array sono oggetti, istanze di una classe speciale denotata da []; anche per gli array, prima si
definisce un riferimento, poi si crea dinamicamente l’oggetto. La sintassi per la definizione di variabili di tipo
array è:
<tipo elemento>[] <nome variabile> .
La sintassi per la creazione dinamica è:
<nome variabile> = new <tipo elemento>[dimensione].
Nel caso di array di elementi di tipo primitivo, l’array effettivamente contiene direttamente dei valori.
Nel caso di array di oggetti la creazione di un array non implica la creazione degli oggetti contenuti, ma
solo delle celle pronte per contenere riferimenti ad oggetti.
Per accedere al contenuto di un array si utilizza la notazione classica,
Esempio: int[] v = new int[3];
v[2] = 13; (1)
int x = v[2]. (2)
Tuttavia questi non sono veri operatori, ma invocazioni mascherate di metodi: ecco la versione “non
mascherata” delle invocazioni di cui sopra:
v.setElementAt(2,13); (1)
int x = v.getElementAt(2); (2)
int size = v.length (restituisce la lunghezza dell’array)
Esiste anche una struttura ad ADT-stack (pila di stringhe). Ecco i metodi:
SimpleStack stack = new SimpleStack(5)
(crea una pila con 5 “posti”);
stack.push(“una stringa”)
(inserisce all’interno della pila un’oggetto-stringa);
stack.pop()
(estrae dalla pila l’oggetto-stringa più “in alto”, ovvero l’ultimo inserito).

Un qualsiasi sistema (applicazione) non banale comporta in generale lo sviluppo di più classi, che si
aggiungono a quelle già disponibili da librerie. Per facilitare l’organizzazione dell’insieme, che può divenir
corposo, i linguaggi OO forniscono principi/meccanismi di raggruppamento, utili in generale per definire
contesti di visibilità e protezione. Allo scopo in Java è presente la nozione di package.
Luca Pagani – Riassunto di informatica LB

Un package, in Java, rappresenta un insieme di classi correlate: come esiste una stretta corrispondenza
fra classi e file, esiste una stretta corrispondenza fra package e directory.
Per dichiarare l’appartenenza di una classe ad un determinato package si utilizza la dichiarazione:
package <nome package>
Tale dichiarazione va posta all’inizio del sorgente della classe.
Quando una classe C è dichiarata appartenere ad un package P, il nome completo della classe è P.C ; nel
caso di main class all’interno di package, è necessario specificare il nome completo della classe quando si
manda in esecuzione la relativa applicazione.
Esempio:
java –cp build acme.TestCounter
(build è il classpath, ovvero la directory che contiene le classi compilate; -cp sta per classpath;
acme è il package).
Per utilizzare/riferire le classi definite all’interno di un package è possibile utilizzare il nome completo della
classe, oppure importare il nome completo della classe e utilizzarne il nome relativo.
L’importazione del nome completo della classe avviene mediante la direttiva import posta all’inizio del
sorgente della classe:
import <nome completo della classe>;
Oppure, per importare tutti i nomi di classi di un package:
import <nome package>.
Data la corrispondenza fra package e directory, è possibile avere package con nomi innestati, che seguono
il modello gerarchico del file system. In realtà, tale strutturazione gerarchica vale solo per i nomi e
l’organizzazione fisica del file: per i package non valgono relazioni gerarchiche e non sono definite relazioni
di package e sub-package.
Una classe pubblica è visibile sia dalle classi del medesimo package, sia da classi esterne al package.
Omettendo il descrittore public, la classe diviene protetta, ovvero è visibile solamente dalle classi del
medesimo package. Si usano classi protette per definire classi ausiliarie delle classi pubbliche del package,
ovvero classi interne non pensate per essere utilizzate direttamente dagli utilizzatori.
Ecco alcuni package e classi già pronte in Java:
- java.lang Æ contiene le classi fondamentali di Java e della Jvm;
- java.util Æ contiene classi di varia utilità (liste, alberi, hashmap, funzioni matematiche…);
- java.io Æ contiene classi per supportare I/O;
- java.awt Æ contiene classi di base per grafica ed eventi.
Esistono poi numerosi package che vengono forniti come estensioni, ovvero che non fanno parte del nucleo
fondazionale di Java.
- javax.swing Æ per costruire interfacce grafiche (GUI);
- javax.jdbc Æ per accedere ed interagire con database secondo protocolli standard e mediante
interrogazioni SQL;
- javax.rmi Æpackage con classi per la programmazione distribuita.

Fino ad ora abbiamo parlato di interfaccia di un oggetto per identificare l’insieme dei suoi metodi pubblici e
l’insieme dei messaggi a cui l’oggetto può rispondere.
Java, e altri linguaggi OO moderni, introducono un costrutto di prima classe per identificare esplicitamente
questo concetto: il costrutto interface.
Tale costrutto permette di definire esplicitamente un’interfaccia, come dichiarazione di un insieme di metodi,
senza specificarne l’implementazione, ovvero specificandone unicamente le signature.
Esempio:
public interface <nome interfaccia>{
lista dichiarazione metodi;
}
Come per le classi, le interfacce devono essere descritte in file sorgenti Java con lo stesso nome
dell’interfaccia. Seguono inoltre le stesse regole di raggruppamento in package e visibilità già viste per le
classi.
Dunque una differenza fondamentale fra classi e interfacce è che le classi definiscono come sono
implementati gli oggetti (ovvero come sono implementati campi e metodi), mentre le interfacce definiscono
unicamente quali metodi sono definiti: in altre parole, le interfacce ci permettono di separare aspetti di
specifica da aspetti di implementazione. Le interfacce definiscono pure specifiche di interazione: i
messaggi, ovvero, tramite i quali è possibile interagire con gli oggetti che dichiarano supportare tali
interfacce.
Luca Pagani – Riassunto di informatica LB

Definite tali interfacce come costrutti di prima classe, ora volgiamo usarle per esplicitare il fatto che un
oggetto ne supporti una o più, ovvero che sia in grado di rispondere a messaggi descritti in esse. Ciò si
esprime esplicitamente dichiarando che la classe dell’oggetto implementa tali interfacce. In Java:
public class <nome classe> implements <nome interfaccia 1>,
<nome interfaccia 2>,
<nome interfaccia 3>, … {
}
La classe in questione deve definire tutti i metodi dichiarati nelle interfacce.
La dichiarazione che una classe implementa una o più interfacce permette di correlare esplicitamente agli
occhi del compilatore e della virtual machine aspetti di pura specifica (le interfacce) ad aspetti di
implementazione (le classi): è questo un principio fondamentale dell’ingegneria dei sistemi software, che ha
come beneficio quello di poter usare implementazioni diverse per la medesima specifica.
Una medesima classe può implementare più interfacce e, quindi, gli oggetti istanza della classe possono
supportare interfacce diverse. Tipicamente, interfacce diverse corrispondono a capacità interattive distinte,
non correlate: ad esempio, supponendo di definire l’interfaccia IPrintable (la I davanti al nome è
convenzione per le interfacce) pensata per oggetti in grado di visualizzarsi in standard output rispondendo al
messaggio print, allora possiamo definire un contatore “stampabile”, concependo un oggetto che
supporta sia l’interfaccia ICounter, sia l’interfaccia IPrintable.
In precedenza abbiamo illustrato come la classe identificasse il tipo di un oggetto: la nozione di tipo è
fondamentale in primo luogo in fase di compilazione per identificare eventuali errori
concettuali/semantici all’interno di un programma. In realtà le interfacce ci permettono di definire il tipo
di un oggetto a prescindere dalla sua implementazione, unicamente in termini della sua specifica: possiamo
ovvero definire il tipo di un oggetto a partire dalle interfacce che supporta, quindi dall’insieme dei
messaggi con i quali è possibile interagire con l’oggetto stesso, e non da come sono poi implementati i
metodi. Viene dunque naturale identificare il tipo di un oggetto con le interfacce che esso supporta.
In Java tale nozione di tipo si traduce nel fatto che è possibile definire il tipo di una variabile anche
specificando non una classe, ma un’interfaccia.
Possiamo quindi scrivere
ICounter c;
Ovvero c contiene il riferimento ad un oggetto che supporta l’interfaccia ICounter, ovvero il tipo ICounter
(anche senza scrivere Counter c).
In generale, data una variabile v di tipo T, allora a v può essere assegnato un qualsiasi riferimento ad un
oggetto che supporti il tipo T, ovvero la cui classe implementi l’interfaccia T, oppure che sia di classe T.
NOTA: non ha senso scrivere
c = new ICounter();
perché quando si crea un oggetto bisogna sempre specificare la classe che fa da template.
Creando dunque un oggetto riferito a una certa interfaccia, si potranno invocare sul riferimento solo i metodi
contemplati dal suo tipo!
Dunque quando una richiesta è inviata ad un oggetto mediante un riferimento di un certo tipo T, la specifica
operazione (metodo) che viene eseguita dipende sia dalla signature del messaggio inviato, sia dallo specifico
oggetto identificato dal riferimento.
L’associazione runtime della richiesta di esecuzione di un metodo su un oggetto e la specifica operazione
effettivamente eseguita prende il nome di dynamic binding. Supportare il dynamic binding (“collegamento
dinamico”) significa che l’effettivo codice eseguito in seguito all’invocazione di un metodo mediante un
riferimento è stabilito solo a runtime: staticamente il compilatore può solo accertarsi che la richiesta (il
messaggio) sia specificato nel tipo di riferimento.
La proprietà di dynamic binding ci permette, a tempo di esecuzione, di sostituire tra loro oggetti che hanno
la medesima interfaccia: questo principio prende il nome di polimorfismo. Un’entità polimorfica subisce
comportamenti diversi da un metodo (ad es. obj.update) in base al tipo di oggetto agganciato: questo tipo si
stabilisce solo a run-time.
Il polimorfismo è un concetto chiave dell’object-oriented ed in particolare dell’ingegneria ad oggetti:
- permette agli utilizzatori (client) di un oggetto di fare meno assunzioni circa gli oggetti utilizzati,
limitandosi a dover conoscere solo l’interfaccia da utilizzare;
- semplifica quindi la definizione dei client di un oggetto;
- disaccoppia fra loro gli oggetti, e permette di variarne le relazioni a runtime.
Le interfacce ci forniscono la possibilità di identificare il tipo di un riferimento a partire solo dal
comportamento osservabile, interattivo, senza dover necessariamente specificare o vincolare aspetti di
tipo implementativo; la forma di polimorfismo vista ci permette di avere oggetti (e classi) distinti, quindi
implementazioni differenti, che supportano la medesima interfaccia.
Luca Pagani – Riassunto di informatica LB

4 – Estensione, riuso, classificazione


Per riusabilità si intende la capacità di (ri)utilizzare elementi di sviluppo e progettazione già
disponibili (perché costruiti in passato o forniti da terze parti) per lo sviluppo di nuovi elementi: si
tratta di una risorsa significativa per migliorare l’efficienza (gestione di risorse) e la qualità (riuso
di elementi ben progettati) di ciò che si va a progettare. Un sistema moderno necessita infatti di
essere evoluto nel tempo, per far fronte a richieste sempre nuove: dal punto di vista ingegneristico
è fondamentale avere quindi strumenti che supportino forme di progettazione e sviluppo
incrementale dei sistemi, dato che l’estendibilità ci permette di riusare il più possibile l’esistente,
senza riscrivere tutto da capo.
Nell’ambito dell’Object-Oriented, riusare significa riuscire a definire nuove classi e interfacce
riusando completamente le classi e le interfacce esistenti, specificando però specializzazioni e
estensioni. Tale supporto, vedremo, è fornito direttamente a livello di linguaggio, il quale ci evita
di usare metodi molto sempliciotti (copia-incolla del vecchio codice in una classe su cui fare le
estensioni) fornendocene di più eleganti. Definiamo dunque:
- subclassing ed ereditarietà: permette di definire l’implementazione di una classe in
termini di un’altra, ereditandone struttura e comportamento; la nuova classe è definita
subclass, mentre la classe esistente è chiamata superclass. La nuova classe possiede
automaticamente – cioè eredita - tutte le strutture dati e le operazioni (interfacce) della
superclass, ed in più può definirne di nuove. I campi della superclass sono implicitamente
replicati nella subclass oppure esplicitamente ridefiniti (overriden) da metodi con signature
compatibile. I costruttori, invece, non si ereditano e dunque le classi derivate devono
definirne di propri. Tuttavia, i costruttori delle classi derivate possono richiamare i
costruttori delle classi base, al fine di costruire quella parte d’oggetto che loro compete: in
questo contesto, i costruttori della superclass dovranno costruire quella parte di oggetto
definita dalla stessa classe padre, mentre quelli della classe derivata dovranno inizializzare
le nuovi parti definite. Nel linguaggio Java si scrive che:
public class <classe derivata> extends <classe base> {

}
Per riferirsi all’oggetto appartenente alla classe padre si utilizza la keyword super,
seguita dai parametri dello specifico costruttore che si vuole invocare. Se non si invocano
esplicitamente costruttori della superclass, viene automaticamente chiamato quello di
default. In tal caso, se non è definito il costruttore di default, viene segnalato errore in fase
di compilazione.
In Java una classe derivata non ha la visibilità dei campi e dei metodi definiti come privati
nella superclass: tali campi e metodi vengono sì ereditati, tuttavia non sono accessibili nella
definizione dei metodi della classe derivata. Se allora vogliamo rendere accessibile un
campo o un metodo ad una classe derivata, dobbiamo necessariamente definirlo come
pubblico? In tal modo, infatti, violeremmo il principio di information hiding. Allo scopo, alcuni
linguaggi (fra cui Java stesso) hanno introdotto un ulteriore tipo di modificatore di visibilità,
di nome protected, usabile sia per campi che per metodi. Un campo/metodo definito come
protected è visibile sono dai metodi e dai costruttori della classe stessa e delle eventuali
future classi derivate. In realtà, per certi aspetti, anche l’utilizzo del modificatore di visibilità
protected porta ad una violazione del principio di information hiding, rendendo visibili
dettagli implementativi non a classi client, ma alle future classi derivate. Questo implica
che nel caso in cui tali aspetti implementativi vengano cambiati, tali cambiamenti si
ripercuotano sulle classi derivate, che devono essere aggiornate di conseguenza. Alla fine,
l’unica soluzione è quella di adottare una disciplina più rigida nella definizione di classi base
e derivate, evitando di utilizzare protected, e imponendo di conseguenza anche per le
classi derivate l’accesso a strutture definite nella superclass sempre e solo mediante
metodi pubblici.
Come per le classi normali, una subclass permette di descrivere comportamento e struttura
di un insieme d’oggetti; lo fa tuttavia incrementalmente, a partire dalla superclass,
descrivendone estensioni e cambianti. A livello formale, il subclassing permette di definire
una relazione d’ordine parziale fra classi, relazione che gode della proprietà transitiva: è
dunque usuale definire gerarchie di classi, legate fra loro dalla relazione di ereditarietà.
Luca Pagani – Riassunto di informatica LB

La relazione di subclassing può essere utilizzata non solo per estendere, ma anche per
specializzare il comportamento di una classe: una classe derivata può non solo estendere
il comportamento della superclass, ma anche ridefinire il comportamento di alcuni metodi
definiti nella stessa. Ciò avviene attraverso l’overriding dei metodi: in Java esso si ottiene
semplicemente ridefinendo, nella classe derivata, un metodo definito nella superclass.
Nella ridefinizione del metodo nella classe derivata è possibile invocare esplicitamente il
metodo originale della superclass, sempre tramite l’indicatore super.
Il metodo ridefinito nella classe derivata (overriding method) deve avere lo stesso nome
e tipo di quello definito nella classe base (overridden method).
Senza subclassing, this si riferisce, nella dichiarazione di una classe C, all’oggetto di tale
classe. Nelle classi derivate ciò non è più vero: nel metodo che una subclass C’ eredita
da una classe C, this si riferisce all’oggetto della classe C’, non ad un oggetto della
classe originale C. In particolare, this può accedere ai metodi ridefiniti in C’ e non può
accedere ai metodi originali definiti in C. Per far ciò è possibile utilizzare l’identificatore
speciale super, che può essere usato per invocare la versione vecchia del metodo della
superclass.
Come s’è detto, in Java una classe derivata eredita tutte le interfacce implementate nella
superclass, come se le avesse implementate lei stessa. Questo aspetto si può
generalizzare dicendo che la relazione di subclassing specifica indirettamente anche
relazioni fra i tipi relativi alle classi coinvolte. È possibile che una medesima classe
estenda una classe base ed implementi una o più interfacce. In tal caso, nella medesima
definizione della classe, si utilizza sia extends che implements.
L’ereditarietà è una relazione utile non solo per riusare implementazione, ma come
potente strumento modellistica per descrivere relazioni fra le entità del dominio da
modellare. In particolare, l’ereditarietà permette di operare forme di classificazione delle
entità del mondo (es: classe padre = poligono; classe figlia = quadrato), identificando
gerarchie e fattorizzando proprietà strutturali e comportamenti delle entità.
Una classe potrebbe estendere più di una superclasse (ereditarietà multipla): tale tipo di
ereditarietà porterebbe alcuni vantaggi (maggiore capacità espressiva della modellazione),
ma anche problemi seri (conflitti, cicli indesiderati…). Per questo, Java non implementa
direttamente tale artificio, ma permette di modellare scenari simili con implementazione di
interfacce multiple.
Possiamo identificare delle proprietà che è utile posseggano tutti gli oggetti di un sistema,
a prescindere dal loro specifico tipo. Ad esempio, il fatto di avere una rappresentazione
testuale. Oppure il fatto di conoscere il nome della classe di appartenenza. O, ancora, di
aver definito – con una propria semantica – la relazione di uguaglianza nei confronti di altri
oggetti. Per modellare questo aspetto a livello di linguaggio, alcuni linguaggi definiscono
una classe madre da cui tutte le classi implicitamente derivano. In tal caso, la gerarchia
delle classi si configura come un unico albero, in cui la classe madre è la radice. In Java la
classe madre si chiama Object.
Il subclassing, infine, ci permette di avere un’altra forma di polimorfismo, definita in questo
caso anche sussunzione (subsumption). Tale forma è riassunta nella regola: se C’ è una
subclass di C e O’ è un’istanza di C’, allora O’, è considerata un’istanza anche di C.
Dunque O’ può essere utilizzato ogni volta si richieda l’interazione con oggetti di classe C.
Per ciò che concerne il tipo di riferimenti, possiamo usare riferimenti il cui tipo è
determinato dalla classe C per riferire anche oggetti di classe C’.
Come nel caso di polimorfismo già incontrato per le interfacce, anche per questa forma,
con le classi, è possibile considerare un analogo principio di sostituibilità: in ogni contesto
in cui si richieda di interagire con oggetti di classe C e C’ è una subclass di C, allora è
possibile utilizzare oggetti di classe C’. La questione è però più delicata, perché il principio
di sostituibilità potrebbe essere violato, ridefinendo il comportamento di un metodo con una
nuova specifica non compatibile con la semantica del metodo “padre”.
Le classi astratte sono classi parzialmente implementate: alcuni metodi sono dichiarati,
ma non definiti perché meglio specificati dalla subclass della classe astratta. Si tratta di un
potente mezzo per modellare gerarchie di entità che non solo condividono comportamenti
interattivi, ma anche aspetti strutturali (esempio: classe padre AbstractShape – una figura
geometrica; classi figlie: Line, Triangle, Ellipse… con metodo draw esteso).
Luca Pagani – Riassunto di informatica LB

Quello appena visto è un idioma di progettazione e sviluppo classico e frequente, che


sfrutta polimorfismo, subclassing e classi astratte, caratterizzato dalla realizzazione di
gerarchie in cui la classe base astratta definisce alcuni aspetti strutturali e comportamenti
concreti e dichiara un certo insieme di metodi come astratti. Le classi che derivano dalla
classe base ereditano struttura e comportamento, concretizzando tali metodi astratti.
Avendo allora un riferimento b dichiarato di tipo B, dove B è la classe base astratta che
definisce un metodo astratto m, b può essere assegnato con riferimenti a istanza di classi
derivate diverse. Invocando il metodo m mediante il riferimenti b, avremo comportamenti
eterogenei a seconda dello specifico oggetto riferito da b in quel momento.
Attenzione: derivare una classe C da una classe astratta A e implementare un’interfaccia
T con gli stessi metodi astratti di A sono strategie concettualmente diverse: nel primo caso
C ha struttura e comportamento definiti da A, mentre nel secondo caso C semplicemente si
impegna a rispettare il contratto (la specifica) interattiva definita da T.
- subtyping: permette di definire una nuova interfaccia in termini di un’altra, ereditando
l’insieme delle operazioni dichiarate e quindi estendendo tale insieme con nuove
operazioni; è possibile dichiarare, per le interfacce, relazioni analoghe a quelle viste per
le classi. L’obiettivo non sarà più il riuso di un’implementazione, ma quello di specifiche
interattive e di protocolli. Diciamo che un tipo è un sottotipo (subtype) di un altro tipo –
definito supertipo (supertype) – se l’interfaccia relativa contiene l’interfaccia del supertype.
Anche questo è un tipo di ereditarietà. In Java, la sintassi per dichiarare una nuova
interfaccia estendendone una esistente è:
public interface <nuova interfaccia> extends <interfaccia esistente>{
dichiarazione di nuovi metodi
}
Il subtyping permette di avere riuso delle interfacce, estendendo quelle esistenti con nuove
funzionalità, e rendendo possibile il riuso di oggetti che supportano le nuove interfacce
anche nei contesti in cui si richiedono le interfacce di base.
Se C’ è una sottoclasse della classe C, allora C’ implementa necessariamente tutte le
interfacce implementate da C, e quindi il tipo di C’ è un sottotipo di C; da questa definizione
pare che subtyping e subclassing siano aspetti distinti: ma mentre nel primo caso stiamo
parlando di relazioni fra interfacce (specifiche), nel secondo prendiamo in considerazione
relazioni fra classi (implementazione). Se la relazione di subclassing permette di abilitare
forme di riuso delle classi (quindi di implementazioni), il subtyping concerne forme di riuso
delle interfacce (quindi delle specifiche).
Abbiamo visto nei primi moduli che il cast
Es. (float)4
è un operatore con cui si forza il tipo con cui il compilatore deve considerare una variabile.
Il casting funziona anche con gli oggetti:
Es. (<T>)<Obj>
Dove T è il nome di un tipo (interfaccia o classe) è Obj è il riferimento ad un oggetto. Il
linguaggio Java effettua controlli (statici o compile time) sulla correttezza del cast.
Considerando gerarchie di classi/interfacce, sono possibili due tipi di cast:
o downcasting (verso il basso): quando T è un tipo che appartiene al sotto albero di
ereditarietà che ha radice nella classe C dell’oggetto Obj (e delle relative
interfacce);
o upcasting (verso l’alto): quando T è un tipo supportato da qualsiasi parent della
classe C dell’oggetto Obj; in Java è automatico.
- composizione: approccio alternativo all’ereditarietà, permette di ottenere nuove
funzionalità ed entità assemblando o componendo oggetti al fine di ottenere entità
complesse o articolate; in generale, tale tecnica permette di definire nuovi oggetti (classi)
senza ricorrere al subclassing. Questa tecnica di riuso è anche definita black-box, dal
momento che si applica senza conoscere alcun dettaglio interno degli oggetti soggetti a
composizione. La composizione di oggetti può essere costruita dinamicamente, a runtime,
mediante campi con cui oggetti composti riferiscono oggetti componenti. Possiamo
identificare tre forme di composizione:
o associazione: generica relazione che associa un insieme di oggetti; es. “La flotta
ha un ammiraglio” (non è aggregazione perché la flotta non è aggregato di
ammiragli, non è composizione, perché la flotta non è composta da ammiragli). In
Luca Pagani – Riassunto di informatica LB

questo caso l’oggetto composto semplicemente conosce (ha il riferimento a) altri


oggetti componenti, che però sono entità indipendenti. È la relazione più debole,
spesso è di tipo “usa”. In Java è pervasivo l’utilizzo di associazioni;
o aggregazione: esprime che un oggetto (“tutto”) è un aggregato di altri oggetti
(“parti”); non implica però che il tutto sia un organismo in cui le parti siano
essenziali per la sua essenza o il suo funzionamento; es. “Una classe ha degli
studenti” (la classe è fatta di studenti, ma nessuno è essenziale). In questo caso un
oggetto è parte di un altro oggetto: tuttavia il medesimo componente può essere
parte di più aggregazioni o di più parti composte. È una relazione più forte
dell’associazione: il tipo di relazione è di tipo “ha” o “è parte di”. In Java, per
realizzare aggregazioni si possono utilizzare oggetti-contenitori come array o
classi liste, mappe, etc.;
o composizione vera e propria: esprime che l’oggetto “tutto” è composto da tante
“parti”, tutte essenziali e indispensabili; es. “La linea ha dei punti” (una linea è fatta
di punti, se ne togliamo uno non c’è più la linea). È il caso di aggregazione in cui
l’oggetto composto possiede ed è responsabile dell’oggetto componente.
Tipicamente oggetto composto e componente hanno lo stesso tempo di vita:
l’oggetto componente non sopravvive all’oggetto composto. I componenti,
inoltre, non sono generalmente condivisi con altri oggetti. La relazione fra oggetti è
di tipo “ha” o “è parte di”, con la parte non condivisa.
Ereditarietà e forme di composizione hanno vantaggi e svantaggi: l’ereditarietà,
essendo una relazione fra classi, definita a compile-time, si applica in modo semplice e
immediato, sfruttando direttamente il supporto del linguaggio, e quindi rendendo più
semplice modificare e raffinare l’implementazione ereditata. Nel subclassing, tuttavia,
non si può cambiare, a run-time, l’implementazione ereditata dalla classe parent a
compile-time. Inoltre, spesso le classi parent espongono una parte della propria
implementazione fisica alle subclass, rompendo il principio dell’incapsulamento. La
composizione di oggetti, invece, può essere definita dinamicamente a run-time.
L’interazione fra oggetti componenti e composti avviene sempre solo attraverso
interfacce ben definite, quindi non sussistono le violazioni di incapsulamento presenti
nel caso dell’ereditarietà: ogni oggetto può essere rimpiazzato dinamicamente da un
altro, dal momento che è dello stesso tipo.
La delegazione è una tecnica che permette di rendere la composizione potente per ciò
che concerne il riuso quanto l’ereditarietà. Nel modello OO due sono gli oggetti coinvolti
nell’esecuzione di una richiesta: l’oggetto che richiede il servizio (client, ovvero colui
che manda il messaggio = invoca il metodo), l’oggetto che fornisce il servizio (ovvero
l’oggetto destinatario del messaggio). Con la delegazione, gli oggetti sono tre: un
client, l’oggetto che riceve la richiesta e un suo delegato che svolge l’operazione.
Attraverso la delegazione è possibile ottenere forme di riuso analoghe a quelle che
otteniamo con il subclassing. Nel caso di subclassing si deferisce la richiesta alla
superclass; nel caso della delegazione è come se si avesse un esplicito riferimento
ad un oggetto di classe parent (oggetto delegato), a cui l’oggetto ricevente manda lo
stesso messaggio ricevuto (delega l’operazione).
- polimorfismo parametrico: approccio con cui è possibile definire classi parametrizzate
rispetto a tipi, catturando comportamenti comuni a prescindere dallo specifico insieme di
tipi utilizzato. Tale strategia deriva dalla necessità di utilizzare un tipo d’oggetto con
variabili diverse da quelle con cui ordinariamente si lavora (ad esempio: lista di stringhe al
posto di lista di interi, una volta creato l’oggetto lista). Come risolvere questo problema?
Anzitutto dobbiamo perdere il tipo effettivo dell’operatore, con un casting oppure
ricorrendo l’uso di operatori come istanceof per determinare il tipo reale dell’oggetto. Poi
c’è il problema dell’impossibilità a forzare il fatto che, ad esempio in una lista, gli elementi
che andiamo inserire siano tutti dello stesso tipo. Il problema è, in generale, definire
interfacce e classi parametrizzate rispetto al tipo di elementi gestiti nell’interfaccia e nelle
classi stesse, riusando completamente la loro struttura a prescindere dal tipo specifico. La
soluzione adottata nei linguaggi OO più evoluti prende il nome di generici: la soluzione ci
permette di definire classi parametrizzate rispetto a tipi che compaiono nella definizione
della classe stessa, sia nella definizione dei campi, sia dei metodi. La sintassi generale per
definire una classe parametrizzata rispetto ai tipi T1, T2, ….TN è la seguente:
Luca Pagani – Riassunto di informatica LB

public class <nome classe><T1, T2, …, TN>{


uso tipi T1, …, TN, per definire il tipo di campi, di parametri di ingresso e uscita dei
metodi, di variabili locali ai metodi
}
Le interfacce generiche possono estendere altre interfacce, che possono essere a loro
volta generiche, parametrizzate sugli stessi tipi o con parametri specificati.
Attenzione: se A è un sottotipo (subclass o subinterface) di B, e G è una dichiarazione di
classe generica, allora G<A> non è in generale un sottotipo G<B>: renderlo tale creerebbe
problemi. Per affrontare problemi come questo, definendo gerarchie di classi/interfacce
generiche, è stato introdotto il tipo wildcard (<?>), con cui è possibile definire classi
generiche che fungono da supertipo per tutte le altre (Es: AbstractList<?> è un
supertipo di AbstractList<String>). È possibile vincolare il tipo wildcard ad
appartenere ad una determinata gerarchia di ereditarietà (bounded wildcard), specificando
<? extends T> dove T è la classe/tipo base della gerarchia.
Luca Pagani – Riassunto di informatica LB

5 – Eccezioni
Il tempo ideale per la rilevazione degli errori, nell’ambito della programmazione, è la
compilazione. Tuttavia, non tutti gli errori possono essere rilevati a compile-time: in particolare,
quelli che dipendono dalle dinamiche del sistema, ovvero dalle interazioni a cui sono soggetti gli
oggetti a tempo di esecuzione (runtime). Più in generale, errori a runtime sorgono dal momento
che nell’esecuzione di un metodo o di un costruttore si creano situazioni di errore che non
permettono il completamento della costruzione dell’oggetto o dell’esecuzione del metodo stesso,
che devono quindi essere interrotte. Nella maggior parte dei casi, tali errori sono interpretabili
come una sorta di violazione del contratto fra l’utilizzatore dell’oggetto (client) e l’oggetto
utilizzato (ad es. violazione di semantica). Il termine per indicare gli errori runtime è eccezione; in
generale, un’eccezione è un evento anomalo che non permette la continuazione del metodo o del
contesto di esecuzione in cui si verifica: non è infatti possibile continuare la computazione dal
momento che non ci sono informazioni sufficienti per far fronte al problema corrente.
I linguaggi moderni forniscono costrutti per gestire le eccezioni: l’occorrenza di un’eccezione non
provoca la terminazione del programma in esecuzione, ma può essere opportunamente gestita
all’interno del programma stesso. Le parti del programma che si occupano della gestione delle
eccezioni prendono il nome di exception handler: esse catturano l’eccezione specificando le
azioni da fare in seguito e sono definite direttamente dal chiamante, nel punto in cui viene invocato
il metodo o costruttore che può portare alla generazione di eccezioni.
Nei linguaggi ad oggetti moderni le eccezioni sono rappresentate da oggetti di opportune classi. In
Java sono fornite classi base che descrivono vari tipi di eccezione, da cui è possibile derivare
classi per crearsi i propri tipi di eccezione. La classe base principale che rappresenta le eccezioni
è Exception; nuove eccezioni si definiscono estendendo la classe Exception:

public <nome dell’eccezione> extends Exception{


}

È tipico definire eccezioni con costruttori a cui si passano informazioni specifiche dell’eccezione
avvenuta, ad esempio una stringa descrittiva del problema.
I due aspetti fondamentali che concernono le eccezioni sono:
- la generazione (o lancio) di eccezioni: la generazione di eccezioni concerne la
manifestazione esplicita dell’errore, con la creazione e la propagazione di un oggetto-
eccezione;
- la cattura e gestione delle eccezioni: concerne la specifica delle azioni da fare lato-cliente
per gestire le eccezioni generate da oggetti con cui il client abbia interagito.
Per lanciare un’eccezione in Java si crea, per prima cosa, l’oggetto che rappresenta l’eccezione
(come normale oggetto Java); poi, si utilizza l’istruzione throw, con cui si genera l’oggetto
eccezione specificato. Esempio:

throw new <Nome della classe dell’eccezione>(…);

Throw accetta come parametro un qualsiasi oggetto di classe che derivi direttamente o
indirettamente dalla classe java.lang.Exception.
Il tipo delle eccezioni eventualmente generate in un metodo o costruttore deve essere dichiarato
nella signature del metodo costruttore mediante la dichiarazione throws:

<Parametro> <Nome del metodo> (<Lista dei parametri>) throws <Lista delle eccezioni>{

}

È possibile descrivere – in termini di documentazione javadoc – le eccezioni di un metodo o di un


costruttore sfruttando il tag @throws oppure @exception.
In Java, per gestire le eccezioni, si definiscono lato-chiamante dei blocchi controllati (guarded
region), ovvero delle regioni di codice in cui possono essere generate eccezioni seguite dal codice
appropriato che definisce come le eccezioni eventualmente generate debbano essere gestite.
Ecco la sintassi per definire tali regioni:
Luca Pagani – Riassunto di informatica LB

try {
<blocco di codice che può generare eccezioni – guarded region>

} catch (<Tipo Eccezione E1> e1){
<codice per gestire eccezione e1>
} catch (<Tipo Eccezione En> en){
<codice per gestire eccezione en>
}
<Se tutto va bene si prosegue qui>

Se non sono generate eccezioni, nessun blocco catch è eseguito. Nel caso di generazione di
eccezioni, l’eccezione è catturata da un blocco catch, le cui istruzioni sono eseguite, quindi si
prosegue con il codice che segue il blocco try-catch.
Se nel blocco catch si specifica una eccezione di tipo X, vengono catturate e gestite da quel blocco
tutte le eccezioni di tipo X e di qualsiasi sottotipo (ovvero classi derivate) di X.
In Java, nella ridefinizione di metodi in caso di subclassing, il metodo overriding (classe derivata)
deve avere la signature che coincide con quella del metodo overridden (classi base), anche per
ciò che concerne la generazione di eccezioni: in sintesi, un metodo overridden non può né
dichiarare di poter generare più eccezioni di quelle del metodo originario, né poterne generare
meno.
S’è detto che, quando un’eccezione viene generata, il sistema di exception handling cerca
l’handler (blocco catch in Java) più “vicino” – in termini di contesto di esecuzione in atto – a cui
cedere il controllo. Tale ricerca comporta una forma di match fra il tipo E di eccezione generata e
il tipo E’ di eccezione che l’handler è in grado di catturare: in realtà, tale match non richiede
necessariamente che i tipi coincidano, ma è sufficiente che E sia una subclass di E’: in altre parole,
richiede che E sia un sottotipo di E’. In virtù del matching basato su tipi, allora è possibile
catturare qualsiasi tipo di eccezione (a meno di quelle runtime) specificando in catch un’eccezione
di tipo Exception, ovvero il tipo più generale:

try {

} catch (Exception ex){

}

Viceversa, è possibile dichiarare che un metodo può generare qualsiasi tipo di eccezione
specificando nella sua signature throws Exception: ciò è possibile anche se il metodo in sé
internamente genera eccezioni specifiche, che derivano da Exception.
Nella gestione delle eccezioni capita frequentemente di avere operazioni che devono essere
eseguite sia in caso di generazione, sia in caso di non generazione di eccezioni. Tipicamente,
concernono qualche forma di chiusura, di finalizzazione. In Java questa possibilità è supportata e
codificata con il blocco finally.

try {

} catch (<Tipo Eccezione E1> e1){
<codice per gestire eccezione e1>
} catch (<Tipo Eccezione E2> e2){
<codice per gestire eccezione e2>
} finally {
<operazioni conclusive>

Nel caso in cui l’eccezione non sia catturata da nessun blocco try-catch, arrivi al metodo main e
anche quest’ultimo non la catturi, allora il programma termina e in standard error viene
visualizzato il messaggio relativo all’eccezione, con indicato lo stack trace, ovvero tutti i contesti di
esecuzione (metodi) avversati, a partire da quello in cui è stata generata l’eccezione fino al main e
alla terminazione. In ogni caso, per far ciò, il metodo main deve dichiarare esplicitamente di
generare (eventualmente) eccezioni.
Luca Pagani – Riassunto di informatica LB

6 – Errori
Gli errori si dividono in:
• errori run-time: gli errori a tempo di esecuzione sono segnalati dalla Java Virtual Machine e si
manifestano con la generazione di eccezioni;
• errori compile-time: questi tipi di errori vengono segnalati a tempo di compilazione direttamente
dal compilatore (javac). Si dividono a loro volta in errori di sintassi (violazione delle regole
grammaticali del linguaggio) ed errori semantici (violazione delle regole semantiche del
linguaggio, tipicamente relative ai tipi).

TIPI PRINCIPALI D’ERRORE COMPILE-TIME:


• “### expected”: le regole richiedono l’elemento sintattico ### nel punto specificato. Si risolve
semplicemente aggiungendo tale elemento dove segnalato;
• “cannot resolve symbol”: viene riportato quando si utilizza un simbolo non precedentemente
definito (es. si invoca un metodo su un oggetto non definito nella classe; si accede a un campo di
un oggetto non definito; si utilizza un costruttore, una variabile non precedentemente dichiarati;
si riferiscono classi/interfacce sconosciute…);
• “### has private access in §§§”: riportato quando si invoca un metodo (o si accede un campo)
non pubblico di un oggetto;
• “missing return statement”: riportato quando, in un metodo con parametro di ritorno, esiste un
percorso computazionale per cui il metodo termina senza specificare tale valore di ritorno;
• “incompatibile types”: riportato quando si cerca di riferire un oggetto di tipo T con un
riferimento non compatibile col tipo T;
• “unreported exception”: riportato quando si invoca un metodo (o si utilizza un costruttore) che
può generare eccezioni, senza specificare come gestirle (ovvero manca un blocco try/catch
oppure la dichiarazione throws);
• implementazione mancante di metodi: riportato quando in una classe non si implementano tutti
i metodi di un’interfaccia, oppure non si implementano metodi astratti di una classe base astratta
(e la classe derivata è concreta);

TIPI PRINCIPALI D’ERRORE RUN-TIME:


• “null pointer exception”: riportato quando si invoca un metodo (o si accede un campo) usando
un riferimento che contiene null, ovvero non riferisce alcun oggetto concreto (es. array non
inizializzati);
• “class cast exception”: riportato quando si tenta di convertire il tipo di un oggetto in un tipo non
compatibile;
• “array index out of bounds exception”: riportato quando si accede ad un elemento di un array
fuori dagli indici consentiti;
• “no class defound error”: riportato quando non si trova una classe perché il tentativo *.class è
stato rimosso o collocato in una posizione errata;
• “no such method error”: riportato quando si invoca un metodo di cui non si ritrova la versione
compilata nel *.class. In particolare, quest’errore viene generato quando si cerca di eseguire
un’applicazione la cui main class non definisce in modo corretto il metodo main.
Luca Pagani – Riassunto di informatica LB

7 – Strutture dati e classi di utilità


Nella progettazione/sviluppo di applicazioni, è frequente la necessità di definire forme di
composizione/aggregazione di oggetti, utilizzando quindi strutture dati (oggetti) atti a contenere e
gestire insieme di oggetti. Java mette a disposizione gli array come forme semplici di oggetti contenitori,
supportati direttamente a livello di linguaggio.
Gli array sono utili per gestire collezioni di cardinalità prefissata, costante, con oggetti tutti dello stesso
tipo; nella libreria java.util è presente una classe (un modulo) di nome Arrays, che fornisce le
funzionalità (statiche) per la gestione degli array:
• equals: testa l’uguaglianza;
• fill: riempie il contenuto dell’array con un dato valore;
• sort: ordinamento;
• binarySearch: ricerca binaria;
• asList: trasforma un array in una lista.

Nello sviluppo di applicazioni, tuttavia, è frequente dover gestire insiemi, collezioni di oggetti la cui
cardinalità non è nota a priori e, soprattutto, varia nel tempo; con ciò si intende collezioni che
richiedono l’aggiunta e rimozione dinamica di elementi. In questi casi gli array di base non sono adatti e,
a supporto di questi aspetti, il JDK mette a disposizione a disposizione – nella libreria delle utility
(java.util) – un insieme di classi chiamate container classes, che rappresentano strutture dati di base
quali liste, insiemi, mappe.

Le classi fornite per la gestione di insiemi di oggetti possono essere suddivise concettualmente in due
categorie:
• Collection: per collection s’intende una collezione (gruppo) di oggetti individuali. Nelle collection
abbiamo due sotto-categorie:
o list: astrazione di lista, caratterizzata da elementi in una specifica sequenza;
o set: gruppi di elementi non duplicati.
• Map: per map s’intende una collezione (gruppo) di coppie (chiave, valore): gli elementi vengono
inseriti specificando una chiave, che serve per la loro successiva ricerca o recupero; una map è
dunque una struttura dati associativa.

Le funzionalità fornite da un qualsiasi oggetto Collection<E> (ovvero che supporta l’interfaccia


Collection<E>) sono:
• Boolean add(E obj) Æ Aggiunge l’oggetto alla collezione. Ritorna false se l’operazione non
riesce.
• Void clear() Æ Rimuove tutti gli elementi di una collezione.
• Boolean contains(E obj) Æ Verifica la presenza di un oggetto nella collezione.
• Boolean isEmpty() Æ Testa se la collezione è vuota.
• Boolean remove(E obj) Æ Rimuove un elemento dalla collezione.
• Int size() Æ Restituisce il numero degli elementi della collezione.
• Obejct[] toArray() Æ Restituisce un array contenente gli oggetti della collezione.
• Iterator<E> iterator() Æ Ottiene un iteratore per la collezione.
Esistono poi funzionalità che operano su una collezione a partire da altre:
• Void addAll(Collection<? Extends E> c) Æ Aggiunge tutti gli elementi della collezione
specificata al container.
• Boolean containsAll(Collection<? Extends E> c) Æ Verifica la presenza di elementi
specificati in una collezione.
• Boolean removeAll(Collection<? Extends E> c) Æ Rimuove tutti gli elementi
specificati in una collezione.
• Boolean retainAll(Collection<? Extends E> c) Æ Mantiene solo gli elementi
specificati nella collezione.
NOTA: la sintassi <? Extends E> indica una collezione di elementi parametrizzati su un qualsiasi tipo
che sia un sottotipo rispetto ad E.

Le liste sono collezioni per le quali è stabilito un ordine (sequenza) degli elementi contenuti.
java.util.List<E> è l’interfaccia di riferimento; i metodi principali (in aggiunta a quelli di
Collection) sono:
Luca Pagani – Riassunto di informatica LB

• Void add(int index, E elem) Æ aggiunge un elemento nella posizione specificata


dall’indice.
• E get(int index) Æ recupera l’elemento che si trova nella posizione specificata dall’indice.
• Int indexOf(Object obj) Æ verifica se un oggetto sia presente nella lista e ne restituisce
l’indice (-1 se non presente) – usa equals per il confronto degli elementi.
• E remove(int index) Æ rimuove l’elemento in posizione specificata dall’indice.
• Void set(int index, E elem) Æ sostituisce l’elemento in posizione specificata dall’indice
con un nuovo oggetto.
• List<E> subList(int from, int to) Æ ottiene una sottolista con tutti gli elementi
compresi nel range di indici specificato.
Implementazioni concrete delle liste sono fornite dalle classi java.util.ArrayList e
java.util.LinkedList; la prima implementa la lista tramite un array (molto performante nell’accesso
diretto, lento nell’aggiunta e rimozione), la seconda tramite una concatenazione (lenta nell’accesso
diretto, possiede metodi specifici per aggiungere/rimuovere, cioè addFirst, addLast, getFirst,
getLast, removeFirst, removeLast).

Uno stack (o pila) è una struttura dati di tipo LIFO (Last-In-First-Out). Si realizzano facilmente con le
LinkedList (utilizzando i metodi addFirst, addLast, getFirst, getLast, removeFirst,
removeLast, isEmpty…).

Una coda è una struttura dati di tipo FIFO (First-In-First-Out). Vale lo stesso discorso fatto per gli stack.

Il concetto di iteratore è stato introdotto per definire l’attraversamento (vista, scorrimento) degli
elementi di una collezione, astraendo dalla specifica struttura della collezione stessa. Un iteratore è un
oggetto che permette di visitare elemento dopo elemento tutto il contenuto di una collezione, a
prescindere che sia una lista, un insieme o una mappa.
L’interfaccia rappresentante è Iterator<E> ed è caratterizzata dai metodi:
- boolean hasNext() Æ testa se ci sono ancora elementi da visitare;
- E next() Æ visita il prossimo elemento;
- Void remove() Æ nel caso si voglia rimuovere l’elemento appena visitato dalla lista.
Le classi Java che derivano da Collection, forniscono un metodo con cui recuperare un iteratore dagli
elementi della collezione (metodo Iterator<E> iterator()).
La classe Iterator è utilizzata in modo consistente nelle Collections API; allo scopo, è stata introdotta
nella versione 1.5 di Java un’estensione del costrutto di iterazione for che funge direttamente da
iteratore, facendo le veci della classe Iterator.
Sintassi:
for (<Tipo di elemento> element: <Collezione di elementi di quel tipo>){
<Utilizza Elementi>
}

Gli insiemi (set) sono collezioni di elementi non ripetuti; l’interfaccia Set ha gli stessi metodi di
Collection. Ovviamente, ogni elemento aggiunto dev’essere unico (non già presente nell’insieme):
per stabilire se un oggetto è già presente nell’insieme ne viene testata l’uguaglianza con gli elementi già
presenti, utilizzando il metodo equals.
Specifiche implementazioni (classi) sono:
- HashSet<E>: insiemi con metodo veloce per la ricerca dell’elemento. Gli oggetti devono
definire un proprio codice hash, ritornato dal metodo (da ridefinire) hashCode definito in
Object.
- TreeSet<E>: insieme ordinato da una struttura ad albero. Gli elementi sono mantenuti ordinati
secondo una relazione di ordine definita degli oggetti stessi inseriti, che per questo devono
implementare l’interfaccia Comparable<E>.

Le mappe servono per memorizzare collezioni di oggetti permettendone il recupero/la ricerca in termini
associativi: ogni elemento viene memorizzato specificando una chiave, utilizzata successivamente per la
ricerca. Quindi, a differenza di un array, l’accesso ad un elemento (oggetto) non avviene attraverso un
indice, ma attraverso una chiave.
Il tipo mappa è rappresentato dall’interfaccia parametrica Map<K, V>; i metodi:
- V put(K key, V value) Æ inserimento di un nuovo elemento nella mappa, di chiave
specificata;
- V get(Object key) Æ recupera l’elemento associato alla chiave specificata;
Luca Pagani – Riassunto di informatica LB

- boolean containsKey(Object key) Æ verifica la presenza di elementi associati alla chiave


specificata;
- boolean containsValue(Object value) Æ verifica la presenza di elementi (associati ad
una qualsiasi chiave);
- int size() Æ ottiene il numero degli elementi correntemente inseriti nella mappa;
- collection<V> values() Æ ottiene l’insieme dei valori inseriti in una collezione.
Implementazioni concrete dell’interfaccia Map sono le classi HashMap e TreeMap.
HashMap è molto efficiente, basata su tecniche di memorizzazione hash: ad ogni chiave viene associato
un valore numerico (codice hash), che funge da identificatore univoco della chiave; l’identificatore viene
utilizzato per inserire/recuperare la chiave (e anche l’oggetto associato come valore) nella collezione in
modo diretto, con complessità costante, senza dover scorrere elementi. Ogni oggetto Java ha predefinito
un proprio codice hash, recuperabile mediante un metodo hashCode definito nella classe Object. È
possibile (e in alcuni casi necessario) ridefinire tale metodo per le proprie classi, in particolare qualora si
voglia definire uno specifico sistema di calcolo del codice hash (ad esempio per aumentarne l’efficienza
rispetto al caso generale.
TreeMap mantiene la mappa ordinata secondo strutture ad albero.

Oltre alle classi per la gestione di strutture dati note, il package java.util contiene molte altre classi
d’utilità generale:
- StringTokeneizer: permette di estrarre da una stringa una serie di parole, dette token,
separate da specifici caratteri (separatori). Un oggetto StringTokeneizer si costruisce
fornendo la stringa sorgente ed eventualmente una stringa che contiene i caratteri che fungono da
separatori: se non si forniscono separatori, vengono utilizzati gli spazi come default; se si vuole
specificare quali separatori usare, si fornisce una stringa come lista di caratteri che fungono da
separatori. Dopo che è stato costruito il tokenizer è possibile interagirvi per estrarre mano a mano
tutti i token dalla stringa, finché ce ne sono. Per estrarre un token si usa il metodo nextToken; si
può estrarre un token considerando come separatori dei caratteri diversi da prima: allo scopo, è
sufficiente fornire a nextToken i nuovi separatori. Il metodo hasMoreTokens – infine -
permette di sapere se ci sono token disponibili (vedere meglio la documentazione Java);
o Split: si trova nella classe String, e realizza esattamente le medesime funzionalità di
StringTokenizer, estraendo tuttavia tutti i token in un colpo solo, in un array (vedere
Javadoc per i dettagli).

Il package java.util.regex fornisce classi per fare il matching di sequenze di caratteri, specificando
pattern definiti da espressioni regolari (costruite con grammatiche regolari). Le classi sono (vedi
documentazione Javadoc):
• Pattern Æ Rappresenta l’espressione regolare.
• Matcher Æ Entità che esegue le operazioni di matching su una specifica sequenza di caratteri,
guidato da uno specifico pattern.

Altre importanti classi contenenti in java.util sono:


• Date: rappresenta uno specifico istante temporale, con la precisione dei millisecondi, utilizzando
come sistema di riferimento temporale il sistema UTC (n° di millisecondi trascorsi dal primo
gennaio 1970 GMT).
o DateFormat: fornisce servizi per manipolare il formato di una data.
• Calendar: classe astratta che fornisce metodi per convertire date in termini di giorni, mesi,
anni.
• Random: rappresenta un generatore di numeri pseudo-casuali.
Luca Pagani – Riassunto di informatica LB

8 – Architetture ad eventi
Un evento è un accadimento che avviene in un preciso istante temporale, che ci interessa esplicitamente
modellare, e alla cui occorrenza vogliamo eseguire un determinato insieme d’azioni.
In Java possiamo utilizzare, descrivere, rappresentare eventi di tipo molto eterogeneo; possiamo inoltre
descrivere gli eventi usando granularità diverse (es. “il valore della temperatura è cambiato” oppure “ il
valore della temperatura ha superato la soglia X”).
Gli eventi possono essere semplici oppure composti (detti anche logici), ovvero essere l’aggregazione di
eventi indipendenti; gli eventi possono inoltre essere in relazione fra loro, per cui possiamo
progettare/definire gerarchie di eventi.
Legate alla nozione di evento ci sono due tipi di entità fondamentali:
• la sorgente (o generatore): è l’entità ove l’evento si verifica, accade, e dove si genera la notifica
dello stesso;
• gli ascoltatori (o osservatori): entità interessata all’evento, ovvero interessata ad essere
informata quando l’evento accade.
Il caso più frequente riguarda più osservatori per la medesima sorgente; tuttavia possiamo avere anche
una medesima entità osservatrice di più sorgenti. Un’entità, infine, può essere al tempo stesso
osservatore e generatore di eventi.

Ci sono alcune caratteristiche chiave che caratterizzano sistemi progettati ad eventi e che rendono le
forme di interazione che lega i componenti del sistema concettualmente diverse rispetto al modello
cliente-servitore proprio dell’OO:
• disaccoppiamento: la sorgente non conosce a priori gli osservatori a cui notificare gli eventi che
accadono. Tali osservatori possono aggiungersi (e togliersi) dinamicamente;
• reattività: un osservatore non deve fare il polling della sorgente. Non deve inoltre essere lui
stesso a richiedere alla sorgente dell’avvenuto accadimento di un evento: quando l’evento
succede, l’osservatore riceve una notifica di tale evento. Nel paradigma ad oggetti, invece, un
cliente interessato ad un servizio di un servitore invia un messaggio di richiesta di tale servizio,
dopodichè il server esegue l’operazione ed eventualmente fornisce informazioni di ritorno. In
questo tipo di interazione tutto parte dal cliente, che stimola l’oggetto servitore; è inoltre il
client che richiede e che deve conoscere esplicitamente il servitore.

Dettaglio concettuale: un evento non è un messaggio; non viene infatti inviato da un mittente ad un
destinatario. Un evento, poi, “accade” e viene solo in seguito notificato dagli ascoltatori tramite invio di
messaggi.

Le fasi fondamentali che caratterizzano le interazioni fra sorgenti e osservatori sono due:
• registrazione/deregistrazione: in questa fase, che caratterizza gli aspetti statici/strutturali del
sistema, un osservatore manifesta ad una sorgente il proprio interesse a ricevere la notifica di
eventi di un determinato tipo; in altre parole, si registra per ricevere la notifica di eventi di un
determinato tipo proprio della sorgente. Allo stesso modo, un osservatore non più interessato a
determinati eventi può deregistrarsi dalla sorgente;
• notifica: in questa fase – che caratterizza la fase dinamica del sistema – l’accadimento di un
evento nella sorgente comporta la notifica di tale evento a tutti gli osservatori interessati,
mediante, ad esempio, l’invio di appositi messaggi.

In letteratura esistono alcuni pattern che realizzano architetture ad eventi in termini object-oriented:
• pattern observer: viene definita una relazione uno-a-molti fra un insieme di oggetti tale per cui
quando un determinato oggetto cambia stato, gli oggetti ad esso dipendenti vengono notificati e
aggiornati automaticamente;
• event-listener: è la generalizzazione del caso precedente, in cui si definiscono eventi di tipo
eterogeneo (non solo il cambiamento di stato) relativi ad una sorgente;
• publish/subscrive: è la forma più generale di architettura ad eventi, in cui le sorgenti degli eventi
(publish) e gli osservatori degli eventi (subscriver) non interagiscono direttamente ma mediante
delle entità mediatrici (event-service) che offrono un insieme di servizi più o meno articolato, al
di là della semplice notifica degli eventi.

Soffermiamoci sul pattern event-listener: in esso gli eventi si suppongono esplicitamente modellati come
oggetti, in istanze della classe Event. Il comportamento visibile da un osservatore è dato da una
interfaccia (eventListener del modello), caratterizzata da un metodo con cui viene notificato un
evento (eventNotified). Il metodo ha come parametro di ingresso un oggetto di tipo Event, che porta
Luca Pagani – Riassunto di informatica LB

con sé informazioni circa l’evento accaduto. La sorgente è caratterizzata da un’interfaccia


(EventSource del modello) che mette a disposizione metodi per registrare e deregistrare nuovi
osservatori (metodi addListener e removeListener). La sorgente può tener traccia dell’insieme degli
ascoltatori registrati mediante una lista. All’accadere di un evento, quest’ultimo dev’essere notificato agli
osservatori: ciò avviene, ad esempio, scorrendo elemento per elemento la lista e quindi inviando a
ciascuno il messaggio eventNotified con, come argomento, informazioni sull’evento accaduto.

Per realizzare, in Java, un’architettura ad eventi, dobbiamo dunque definire:


• una gerarchia di classi per rappresentare gli eventi;
• l’interfaccia per ascoltare l’evento, che i listener specifici implementeranno (tale interfaccia
avrà un insieme di metodi che rappresentano le varie forme con cui può essere generato il
medesimo evento);
• la sorgente dovrà mettere a disposizione un’interfaccia opportuna per registrare/deregistrare i
listener.

Esistono precise convenzioni in base alla quali assegnare i nomi delle parti di un sistema ad
eventi/componenti:
• tipicamente il nome della classe relativa a un evento è ###Event (es. MouseEvent,
PatientEvent, ExamListEvent);
• di conseguenza, il nome dell’interfaccia listener è ###Listener (es. MouseListener,
PatientListener etc…);
• i metodi di tale interfaccia avranno come argomenti un parametro di tipo ###Event. I nomi dei
metodi tipicamente descrivono il particolare tipo di evento successo (es: mouseMoved(Mouse
Event ev), temperatureRaised(PatientEvent ev) etc...);
• i metodi con cui si registrano i listener sulla sorgente avranno come nomi:
public void add###Listener(###Listener e);
public void remove###Listener(###Listener e).

Accanto al paradigma Object-Oriented si è sviluppato, attorno agli anni ’90, un programma strettamente
correlato denominato Component-Oriented.
In questa sede, un componente può essere inteso come un oggetto (anche se non tutti in realtà lo sono),
che “vive” all’interno di un ben definito “contenitore” (component container) che svolge da contesto,
ambiente del componente:
• componenti possono essere aggiunti/rimossi dinamicamente (runtime) da un contenitore;
• contenitore e componenti interagiscono mediante interfacce esplicitate: un componente può
offrire e fruire servizi al/del contenitore mentre il contenitore tipicamente funge da coordinatore
dei componenti.
L’interazione ad eventi è una delle principali forme di interazione fra container e componenti; esistono
spesso delle convenzioni che regolano le interfacce di questi ultimi, per renderli in grado di esser parte di
un container, di essere sorgenti e/o ascoltatori di eventi.

NOTE: possiamo progettare gerarchie di eventi, di granularità diversa. Realizzare l’osservazione di eventi
composti, si possono definire ascoltatori registrati su sorgenti multiple (ed anche eterogenee): quindi, in
questo caso il medesimo listener viene sollecitato con la notifica di eventi di sorgenti diverse. È possibile
infine concatenare fra loro sorgenti e ascoltatori, creando catene di notifiche di eventi, definendo un
ascoltatore che a sua volta sia sorgente di eventi.
Luca Pagani – Riassunto di informatica LB

9 – Interfacce GUI
I modelli di interazione ad eventi, uniti al paradigma OO, sono ideali per la progettazione e lo sviluppo delle
interfacce grafiche. Le interfacce grafiche sono caratterizzate da elementi detti componenti (finestre,
pulsanti, campi di testo, combo-box, menu, fonts…); tali componenti possono essere facilmente modellati in
termini di oggetti e sono caratterizzati da forme di interazione tipicamente ad eventi.
In realtà l’introduzione delle interfacce grafiche nella progettazione e realizzazione di un sistema altera
completamente il modello tradizionale classico utilizzato per strutturare i programmi. Nell’approccio
classico, un programma è tipicamente strutturato in:
• read: acquisizione dati;
• eval: trasformazione ed elaborazione dei dati;
• print: emissione dei risultati in uscita.
L’elaborazione avviene quindi in un mondo chiuso (es. macchina di Turing), poco interattivo e poco aperto.
Questo schema viene radicalmente modificato dal concetto di applicazione dotata di interfaccia grafica
(GUI, Graphical User Interface), attraverso cui l’utente possa interagire durante l’elaborazione e determinarne
il flusso in modo non prevedibile a priori. Dunque, si svolgono azioni non più in conseguenza del proprio
interno flusso di controllo, ma in risposta ad eventi generati dall’esterno.
Il concetto di evento a questo livello introduce un notevole cambiamento nell’organizzazione di un sistema
software, in quanto implica l’idea di un sistema che reagisce a stimoli esterni anziché di un sistema che
decide per conto suo il momento di inviare comandi ai dispositivi.
Questo modello di organizzazione del software interattivo è nato con l’obiettivo di fornire una netta
separazione fra aspetti di presentazione e visualizzazione, da aspetti legati alla logica dell’applicazione.
Secondo questo pattern, un’applicazione interattiva si struttura separando (modello MVC):
• il modello dei dati (model): il modello rappresenta la struttura dei dati nell’applicazione e le relative
operazioni;
• la presentazione dei dati (view): è la responsabile delle interazioni con l’utente e ne raccoglie gli
input;
• il comportamento/logica dell’applicazione (controller): è sensibile alle azioni dell’utente e può
recuperare i dati forniti, traslando tutto ciò in chiamate di opportuni metodi del modello e
selezionando la vista appropriata. Funge da colla (coordinatore) fra view e model.
Il modello MVC può essere applicato al di là delle interfacce grafiche, ogni qualvolta ci siano forme di
interazione del nostro sistema con l’esterno (utenti, risorse o altri sistemi), sia in termini di acquisizione di
informazioni, sia in termini di presentazione.
Il punto fondamentale è la separazione fra i tre aspetti: tale separazione ci permette di avere (riusare)
molteplici viste per il medesimo modello di dati e, altresì, di poter cambiare il comportamento del sistema
(controllo) mantenendo le medesime viste e il medesimo modello dati.

In Java, la libreria standard per la programmazione di interfacce grafiche è javax.swing caratterizzata


principalmente da:
• utilizzo pervasivo del pattern “event-listener”: ogni entità grafica (finestre, pulsanti…) è un
componente generatore di eventi;
• un numero molto ampio di componenti.
Swing definisce una gerarchia di classi che forniscono ogni tipo di componente grafico:
• pulsanti Æ JButton, JRadioButton, JCheckBox;
• etichette ed icone Æ JLabel;
• campi ed aree di testo Æ JTextFields, JTextArea;
• aree “scrollabili” Æ JScrollPane;
• list boxes Æ JList;
• menu Æ JMenu, JMenuBar, JMenuItem, JPopMenu;
• combo boxes Æ JComboBox;
• tabbed panes Æ JTabbedPane;
• tool tips Æ vedere JComponent;
• message box Æ JOptionPane;
• dialoghi Æ JDialog, JFileChooser;
• slider e progress bar Æ JSlider, JProgressBar;
• alberi Æ JTree;
• tabelle Æ JTable.
Luca Pagani – Riassunto di informatica LB

La più semplice applicazione grafica consiste in una classe il cui main crea un JFRAME e lo rende visibile:
JFrame f = new JFrame(“Esempio”);
f.setVisible(true);
Per impostare le dimensioni di un qualunque contenitore:
f.setSize(new Dimension(300,150));
Per impostare la posizione:
f.setLocation(200,100);
L’impostazione predefinita di JFrame è che chiudere il frame non significhi terminare l’applicazione, ma
soltanto chiudere la finestra; c’è però un modo semplice per cambiare questa impostazione predefinita,
utilizzando il metodo della classe JFrame setDefaultCloseOperation e specificando una opportuna
costante (come nell’esempio):
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
In Swing non si possono aggiungere nuovi componenti direttamente al JFrame: dentro a ogni JFrame c’è
un Container, recuperabile col metodo getContentPane(): è a lui che vanno aggiunti nuovi
componenti, tipicamente mediante il metodo add():
Container c = getContentPane();
JPanel panel = new JPanel();
c.add(panel);

Una JLABEL serve per rappresentare una semplice etichetta o icona. Ecco come si crea:
JPanel panel = new JPanel();
JLabel lbl = new JLabel(“Etichetta”);
panel.add(lbl);
c.add(Panel);
pack(); Æ metodo che dimensiona il frame in modo da contenere esattamente il pannello dato.
Per visualizzare un’immagine come etichetta:
ImageIcon icon = new ImageIcon(“image.jpg”) Æ immagine nella cartella del codice sorgente.

Ogni componente grafico, quando si opera su di esso, genera un evento che descrive cosa è accaduto;
tipicamente, ogni componente può generare molti tipi diversi di eventi, in relazione a ciò che sta accadendo.
In Swing, un evento è un oggetto, istanza di una sottoclasse di java.util.EventObject: l’evento Event
creato è un componente che ha tutte le informazioni sull’evento e che viene inviato al Listener che reagisce di
conseguenza. Fra i più tipici generatori d’eventi vi sono i pulsanti (JBUTTON): quando viene premuto, un
bottone genera un evento di classe ActionEvent; questo evento viene inviato dal risistema allo specifico
ascoltatore di tipo ActionListener, registrato per quel bottone. Tale ascoltatore degli eventi deve
implementare il metodo:
void actionPerformed(ActionEvent ev);
Il frame ha un pannello che contiene etichetta e pulsante, creati nel costruttore del frame; quest’ultimo fa da
ascoltatore degli eventi per il pulsante Æ il costruttore del frame imposta il frame stesso come ascoltatore
degli eventi del pulsante.
Esempio:

class FrameWithButton extends JFrame implements ActionListener{


private JLabel l;

public FrameWithButton(){

Container c = getContentPane();
JPanel panel = new JPanel();
l = new JLabel(“Tizio”);
panel.add(l);
JButton b = new JButton(“Tizio/Caio”);
panel.add(b);
b.addActionListener(this);
c.add(panel);
pack();
}

Gli eventi di finestra sono gestiti dal sistema, che attua comportamenti predefiniti e irrevocabili. In più il
sistema può invocare i metodi dichiarati dall’interfaccia WindowListener; comunque sia, il
Luca Pagani – Riassunto di informatica LB

comportamento predefinito che hanno le finestre va già bene, tranne per l’evento “chiusura della finestra”,
che è gestito nascondendo la finestra anziché chiudendo l’applicazione.
Per far sì che, chiudendo la finestra del frame, l’applicazione venga chiusa davvero, il frame deve
implementare l’interfaccia WindowListener: in particolare deve ridefinire WindowClosing in modo che
invochi System.exit().

Il JTEXTFIELD è un componente “campo di testo”, usabile per scrivere e visualizzare una riga di testo: il
campo di testo può essere editabile o no, e il testo è accessibile con getText() / setText().
Se interessa solo come puro dispositivo di I/O, non occorre preoccuparsi di gestire i suoi eventi; altrimenti,
se si vuole reagire ogni volta che il testo contenuto cambia, occorre gestire un DocumentEvent (nel
documento che contiene il campo di testo). Infine, se invece si desidera notare i cambiamenti del testo solo
quando si preme invio sulla tastiera, allora basta gestire semplicemente il solito ActionEvent.

L’interfaccia DocumentListener dichiara tre metodi:


void insertUpdate(DocumentEvent e);
void removeUpdate(DocumentEvent e);
in caso di inserimento o rimozione di caratteri, l’azione è identica e quindi questi due metodi saranno
identici (ma vanno comunque implementati entrambi).
void changedUpdate(DocumentEvent e).

JCHECKBOX è una casella di opzione, che può essere selezionata o deselezionata [verifica: isSelected();
settaggio: setSelected()]. Ogni volta che lo stato della casella cambia, si generano:
• un ActionEvent, come per ogni pulsante;
• un ItemEvent, gestito da un ItemListener.
L’ItemListener dichiara il metodo:
public void itemStateChanged(ItemEvent e);
che deve essere implementato dalla classe che realizza l’ascoltatore degli eventi. In caso di più caselle gestite
dallo stesso listener, il metodo e.getItemSelectable() restituisce un riferimento all’oggetto sorgente
dell’evento.

Il JRADIOBUTTON è una casella di opzione che fa parte di un gruppo: in ogni istante può essere attiva una sola
casella del gruppo. Si agisce in questo modo:
• si creano tanti JRadioButton quanti ne servono, e si aggiungono come sempre al pannello;
• si crea un oggetto ButtonGroup e si aggiungono i JRadioButton al gruppo, tramite add.
Quando si seleziona uno dei bottoni, si generano tre eventi:
- un ItemEvent per il bottone selezionato;
- un ItemEvent per il bottone deselezionato;
- un ActionEvent da parte del bottone selezionato (anche se era già selezionato).
Solitamente conviene gestire l’ActionEvent (più che l’ItemEvent) perché ogni cambio di selezione ne
genera uno solo (invece che due), per cui si ottiene una gestione più semplice.

Una lista JLIST è una lista di valori fra cui si può sceglierne uno o più: quando si sceglie una voce si genera
un evento ListSelectionEvent, gestito da un ListSelectionListener; quest’ultimo deve
implementare il metodo void valueChanged(ListSelectionEvent), mentre per recuperare le voci scelte
si usano getSelectedValue() e getSelectedValues().

Una JCOMBOBOX è una lista di valori a discesa, in cui si può o sceglierne uno, o scrivere un valore diverso. Per
configurare l’elenco delle voci proposte si usa il metodo addItem(), per recuperare una voce già scelta
getSelectedItem(). Quando si sceglie una voce o se ne scrive una nuova, si genera un ActionEvent.

Quando si aggiungono componenti ad un contenitore, la loro posizione è decisa dal gestore di layout: il
gestore predefinito per un pannello è FlowLayout, che dispone i componenti in fila (da sinistra a destra,
dall’alto al basso). Ne esistono però altri:
- Border Layout: dispone i componenti lungo i bordi;
- GridLayout: dispone i componenti in una griglia m x n;
- GridBagLayour: dispone i componenti in una griglia m x n flessibile;
- BoxLayout: dispone i componenti o in orizzontale o in verticale, in un’unica casella.
Luca Pagani – Riassunto di informatica LB

Per disporre i componenti con ordine, è utile un’organizzazione a blocchi. Il componente box serve allo
scopo. Un box è un componente invisibile, che organizza i suoi componenti:
- o in orizzontale (uno a fianco all’altro);
- o in verticale (uno sotto l’altro).

Sfruttando la modellazione ad oggetti, le librerie Swing forniscono la possibilità di ridefinire o estendere il


modo con cui i componenti vengono disegnati sullo schermo (rendering). È possibile fare ciò ridefinendo il
metodo paintComponent definito da JComponent: tale metodo non è mai invocato esplicitamente
dall’utente o dal programmatore, ma internamente dall’insieme degli oggetti che implementano i
meccanismi interni di Swing. Per esempio, per disegnare un pannello occorre:
- definire una propria classe (MyPanel) che estenda il JPanel originale;
- in tale classe, ridefinire paintComponent(), che è il metodo (ereditato da JComponent) che si
occupa di disegnare il componente;
- il nuovo paintComponent() da noi definito deve sempre richiamare il metodo
paintComponent() originale, tramite super().

I MENU sono elementi della GUI navigabili mediante il puntatore del mouse, che permettono di scatenare
l’esecuzione di determinate azioni associate alle voci stesse dei menu. Ogni componente in grado di avere
menu è caratterizzato da una barra menu (menu bar) che contiene uno o più menu. Ogni menu si compone
poi di una o più voci ad opzioni (menu item), che possono essere rappresentate sia in forma di testo, sia in
forma di immagine. Un’opzione può essere a sua volta un menu (per avere menu in cascata).
In Swing abbiamo a disposizione i seguenti componenti (associabili a JFrame, JDialog o JApplet):
- barra del menu (JMenuBar);
- un menu dalla classe (JMenu);
- un menu item della classe (JMenuItem), che deriva da AbstractButton e quindi ne eredita il
comportamento. Viene a sua volta derivata in JCheckBoxMenuItem, JRadioButtonMenuItem.
Infine, è sorgente di eventi di tipo ActionEvent, quindi mette a disposizione metodi per registrare
ascoltatori di tipo ActionListener.
Ad un oggetto istanza di JMenuBar si aggiungono oggetti JMenu mediante metodo add, e in modo analogo
ad ogni menu si aggiungono item (oggetti istanze di JMenuItem) sempre mediante un metodo add.
È possibile aggiungere separatori fra i vari item mediante il metodo addSeparator.
Ad ogni menu item può essere associata anche una particolare combinazione di tasti che ne permettono la
scelta immediata senza l’utilizzo del mouse; è possibile cambiare sia la barra menu, sia i menu, sia i menu
item dinamicamente. Occorre in tal caso chiamare il metodo validate() del componente che contiene tali
componenti (il frame o dialog). Esiste infine la possibilità di creare e gestire pop-up menu (classe
JPopupMenu), ovvero menu contestuali creati all’interno di finestre, in posizioni non fisse.

Una FINESTRA DI DIALOGO (dialog box) è una finestra che compare (pop-up) nel contesto di un’altra finestra:
l’obiettivo è quello di gestire alcune interazioni specifiche, evitando di doverle gestire a livello della finestra
principale. Per creare una finestra di dialogo è necessario estendere la classe JDialog che, come JFrame,
ha un proprio layout manager (default: BorderLayout); per chiudere una finestra di dialogo si deve
invocare il metodo dispose().

Esistono poi un insieme di FINESTRE DI DIALOGO predefinite, direttamente da utilizzare (o da estendere),


fornenti funzionalità classiche che si trovano nei moderni sistemi operativi, come la selezione di un file da
aprire o da salvare, la selezione di una stampante su cui stampare etc.
La classe JFileChooser fornisce le funzionalità di finestra di dialogo sia per la selezione di file da aprire,
sia per la selezione di file su cui salvare informazioni:
- il metodo showOpenDialog visualizza una finestra di dialogo per selezionare un file da aprire;
- il metodo showSaveDialog visualizza una finestra di dialogo per selezionare un file su cui salvare
informazioni.

Il mouse è sorgente di eventi di tipo MouseEvent, che includono sia eventi generati in seguito al movimento
del mouse (ascoltatore MouseMotionListener), sia eventi relativi alla pressione dei tasti del mouse
(ascoltatore generico MouseListener); l’interfaccia MouseListner dichiara metodi per ascoltare come
eventi:
- il click di un tasto del mouse (mouseClicked);
- pressione di un tasto del mouse (mousePressed);
Luca Pagani – Riassunto di informatica LB

- rilascio del pulsante del mouse (mouseReleased);


- ingresso del puntatore nella zona di un componente (mouseEntered);
- uscita del puntatore dalla zona di un componente (mouseExited).
L’interfaccia MouseMotionListener dichiara metodi per ascoltare come eventi:
- spostamento del mouse (mouseMoved);
- spostamento del mouse con pulsanti premuti (mouseDragged).

Altri aspetti modellabili in Java:


- gestione di eventi avanzati del mouse (es. MouseWheel);
- gestione di aree di testo (JTextArea);
- gestione di icone (Icon);
- gestione di alberi di opzioni (JTree);
- gestione di tabelle (JTable);
- gestione di tool-tip (setToolTipText in JComponent);
- slider e progress bar (classi JSlider, JProgressBar);
- gestione della clipboard (java.awt.datatransfer);
- possibilità di cambiare il look&feel globale della GUI (UIManager, setLookAndFeel).
Luca Pagani – Riassunto di informatica LB

10 – I/O in Java

Il package java.io definisce i concetti base per gestire l’I/O da qualsiasi sorgente e verso qualsiasi
destinazione.
Il canale di comunicazione è detto stream; esso è monodirezionale (o di input, o di output), di uso generale,
adatto a trasferire byte (o caratteri), e rappresenta l’astrazione in grado di incapsulare tutti i dettagli di una
sorgente dati o di un dispositivo di output, fornendo un modo semplice e flessibile per aggiungere ulteriori
funzionalità a quelle fornite dal canale “base”.
Il package java.io distingue fra:
- stream di byte (implementati nelle interfacce InputStream e OutputStream);
- stream di caratteri (interfacce Reader e Writer).
L’approccio che lo stream adotta è detto “a cipolla”, in quanto altri tipi di stream sono pensati per avvolgere
quelli già esistenti e per aggiungere ulteriori funzionalità: è così possibile configurare il canale di
comunicazione con tutte e sole le funzionalità che servono senza doverle replicare e re-implementare più
volte. Dalle classi base astratte si derivano infatti varie classi concrete, specializzate per fungere da:
- sorgenti per input da file;
- dispositivi di output su file;
- stream di incapsulamento, cioè pensati per aggiungere a un altro stream nuove funzionalità (I/O
bufferizzato, filtrato, di numeri, di oggetti…).
Gli stream di incapsulamento hanno così come scopo quello di avvolgere un altro stream per creare un’entità
con funzionalità più evolute: il loro costruttore ha quindi come parametro un InputStream o un
OutputStream già esistente.

La classe base InputStream (astratta) definisce il concetto generale di “canale di input” operante a byte: il
costruttore apre lo stream, read() (deve essere ridefinito nelle classi derivate) legge uno o più byte,
close() chiude lo stream.
Similmente, la classe base OutputStream definisce il concetto generale di “canale di output” operante a
byte: il costruttore apre lo stream, write() (ridefinito) scrive uno o più byte, flush() svuota il buffer
d’uscita, close() chiude lo stream.

Queste due classi “base” vengono in seguito incapsulate. Vediamo il primo tipo di incapsulamento
(lavorando coi File):
- FileInputStream è la classe derivata che rappresenta il concetto di sorgente di byte agganciata
a un file: il nome del file da aprire è passato come parametro al costruttore FileInputStream (in
alternativa, si può passare al costruttore un oggetto File costruito in precedenza). Per aprire un file
binario in lettura si crea un oggetto di classe FileInputStream, specificando il nome del file
all’atto della creazione; per leggere dal file si usa poi il metodo read() che permette di leggere uno o
più byte (che la macchina restituisce poi come intero da 0 a 255, oppure restituisce -1 quando lo
stream è finito).
- FileOutputStream è la classe derivata che rappresenta il concetto di dispositivo d’uscita
agganciato a un file. Il nome del file da aprire è passato come parametro al costruttore di
FileOutputStream (in alternativa, si può fissare al costruttore un oggetto File costruito in
precedenza). Per aprire un file binario in scrittura si crea un oggetto di classe FileOutputStream,
specificando il nome del file all’atto della creazione; per scrivere sul file si usa il metodo write(),
che permette di scrivere uno o più byte e non restituisce nulla.

Altri stream di incapsulamento per input sono:


- BufferedInputStream: aggiunge un buffer e ridefinisce read() in modo da avere una lettura
bufferizzata;
- DataInputStream: definisce metodi per leggere i tipi di dati standard in forma binaria:
readInteger(), readFloat(),…
- ObjectInputStream: definisce un metodo per leggere oggetti “serializzati” (salvati) da uno
stream; offre anche metodi per leggere i tipi primitivi e gli oggetti delle classi wrapper di Java.
Luca Pagani – Riassunto di informatica LB

Gli stream di incapsulamento per output sono:


- BufferedOutputStream: aggiunge un buffer e ridefinisce write() in modo da avere una
scrittura bufferizzata;
- DataOutputStream: definisce metodi per scrivere i tipi di dati standard in forma binaria:
writeInteger();
- PrintStream: definisce metodi per stampare come stringa i valori primitivi (con print () ) e le
classi standard (con toString() );
- ObjectOutputStream: definisce un metodo per scrivere oggetti “serializzati” e offre anche metodi
per scrivere i tipi primitivi e gli oggetti delle classi wrapper di Java.

Per scrivere su un file binario occorre un FileOtuputStream, che però consente solo di scrivere un byte o
un array di bytes. Volendo scrivere dei float, int, double, boolean, … è molto più pratico un
DataOutputStream, che ha metodi idonei.
Stesso discorso per l’InputStream, incapsulato dal DataInputStream.
Le classi per l’I/O da stream di caratteri (Reader e Writer) sono più efficienti di quelle a byte: hanno nomi
analoghi e struttura analoga, e convertono correttamente la codifica UNICODE di Java in quella locale.
Cosa cambia rispetto agli stream binari? I file di testo si aprono costruendo un oggetto FileReader o
FileWriter; read() e write() leggono/scrivono un int che rappresenta un carattere UNICODE (2 byte).
Occorre dunque un cast esplicito per convertire il carattere UNICODE in int e viceversa.

Gli stream di byte sono di livello più basso rispetto agli stream di caratteri, ma a volte può rivelarsi utile
reinterpretare uno stream di byte come reader/writer, se esso trasmette caratteri. Esistono due classi
“incapsulanti” progettate proprio per questo scopo:
- InputStreamReader che reinterpreta un InputStream come un Reader,
- OutputStreamWriter che reinterpreta un OutputStream come un Writer.

Video e tastiera sono rappresentati dai due oggetti statici System.in e System.out; per ragioni “storiche”
di Java, il primo è “formalmente” un InputStream (incapsulato dentro un InputStreamReader) mentre
il secondo è “formalmente” un OutputStream (incapsulato dentro a un OutputStreamWriter);
nonostante ciò, essi sono in realtà stream di caratteri. Esempi tipici:
InputStreamReader tastiera = new InputStreamReader(System.in);
OutputStreamWriter video = new OutputStreamWriter(System.out).

Esiste poi un problema legato ai caratteri speciali (es. & ò à é ! # @): quando vengono stampati su file di
testo tramite FileWriter, essi vengono convertiti nell’encoding di default della piattaforma in uso. La
stampa su video via System.out usa sempre e comunque tale encoding, mentre le finestre prompt dei
comandi usano un encoding diverso. Occorre dunque effettuare la stampa a video tramite un Writer
configurato per usare il character encoding corretto.
Esempio:
new OutputStreamWriter(System.out, “CP850” [Å encoding diverso]);

Per scrivere su un file di testo occorre un FileWriter, che però consente solo di scrivere un carattere o una
stringa. Per scrivere float, int, double, boolean… occorre convertirli a priori in stringhe con il metodo
toSring() della classe wrapper corrispondente.
Per leggere un file di testo occorre un FileReader, che però consente solo di leggere un carattere o una
stringa. Per leggere fino alla fine del file:
- o si usa il metodo read() come prima;
- o si sfrutta il metodo ready(), che restituisce true fintanto che ci sono altri caratteri da leggere.

Serializzare un oggetto significa salvare un oggetto scrivendo una sua rappresentazione binaria su uno
stream di byte. Analogamente, deserializzare un oggetto significa ricostruire un oggetto a partire dalla sua
rappresentazione binaria letta da uno stream di byte. Le classi ObjectOutputStream e
ObjectInputStream offrono questa funzionalità per qualunque tipo di oggetto; la prima scrive un oggetto
serializzato su stream, tramite il metodo writeObject(), la seconda legge un oggetto serializzato salvato su
uno stream, tramite il metodo readObject().
Luca Pagani – Riassunto di informatica LB

Una classe che voglia essere “serializzabile” deve implementare l’interfaccia Serializable: è
un’interfaccia vuota, usata come marcatore. Il compilatore, infatti, si rifiuta di compilare una classe che usi
la serializzazione senza implementare tale interfaccia.
Nel file che si viene a creare con questa operazione, vengono scritti tutti i dati dell’istanza, inclusi quelli
ereditati e pure privati o protetti; non vengono scritti i dati statici e quelli qualificati con la keyword
transient (= il contenuto è destinato a non essere mantenuto).

Oltre all’interfaccia Serializable esiste anche l’interfaccia Externalizable; anche un oggetto


Externalizable può essere scritto o letto tramite uno stream binario, ma cambia l’approccio: con
Serializable, è lo stream che si occupa di scrivere / leggere un’istanza, mentre – con Externalizable
– è la classe dell’oggetto a dover occuparsi di come le proprie istanze debbano essere scritte / lette. Nel
primo caso, i metodi writeObject() e readObject() sono forniti già implementati da
ObjectOutputStream e ObjectInputStream, rispettivamente; al contrario, per gli oggetti di tipo
Externalizable, non sono forniti i metodi writeExternal() e readExternal() che devono quindi
essere implementati dalla classe stessa.

La classe File (package java.io) fornisce una rappresentazione astratta di pathname di file o directory (Es:
C:\Documenti). Un oggetto File si costruisce specificando il path name nel costruttore:
File MyFile = new File(“C:\Documenti”)
Creato l’oggetto che denota lo specifico file o directory, è possibile eseguire su di esso operazioni relative
alla creazione, alla rimozione, alla rinomina etc… :
- creazione di directory (mkdir, mkdirs);
- rinomina di file e directory (renameTo);
- rimozione di file e directory (delete);
- recuperare informazioni sul file relativo: lunghezza (length), data dell’ultima modifica
(lastModified);
- verificare l’esistenza di un file o di una directory (exists);
- verificare se si tratta di un file (isFile);
- verificare se si tratta di un directory (isDirectory);
- cambiare gli attributi (setLastModified e setReadOnly);
- in caso di directory: ottenere l’elenco dei nomi di file/directory contenuti (list).
Luca Pagani – Riassunto di informatica LB

11 – MULTITHREADING E TIMERS
A livello di sistema operativo, un programma in esecuzione prende il nome di processo. Finora
abbiamo associato ad un processo un unico flusso d’esecuzione: nel caso di Java, tale flusso di
controllo è quello che la JVM esegue nelle istruzioni che trova nel main della Main class, dalla
prima all’ultima. In realtà i sistemi moderni permettono di avere più flussi di esecuzione – chiamati
threads – all’interno del medesimo processo: ogni flusso rappresenta un’attività indipendente
dalle altre e concorre parallelamente agli altri (multithreading).
Java è uno dei pochi linguaggi che fornisce supporto per i thread direttamente a livello di
linguaggio, cercando di modellare tale nozione in termini di oggetto (classe). La JVM si occupa
quindi della creazione e della gestione dei thread: come vengano poi mappati dipende dal kernel
del sistema operativo.
Un thread è rappresentato dalla classe astratta Thread (package java.lang), caratterizzata dal
metodo astratto run, che definisce il comportamento della classe stessa. Un thread concreto si
definisce estendendo la classe Thread e specificando il comportamento del metodo run. A
runtime, un thread viene creato come un normale oggetto Java, e mandato in esecuzione
invocando il metodo start. Costruttori/metodi significativi della classe Thread sono:
• Thread(String name): costruisce il thread di nome name;
• void Thread.sleep(long ms): metodo statico per addormentare il thread corrente di ms
millisecondi;
• void destroy(): distrugge il thread;
• void setPriority(int priority): cambia la priorità di esecuzione del thread;
• String getName(): ottiene il nome del thread;
• boolean isAlive(): verifica se il thread è “vivo”;
• void interrupt(): interrompe l’attesa del thread (nel caso fosse sleep o wait);
• Thread Thread.currentThread(): metodo statico per recuperare il riferimento al thread
corrente.

Esiste anche un secondo modo per definire un thread, basato su interfacce (interfaccia
Runnable), utile quando la classe che funge da thread è già parte di una gerarchia di ereditarietà e
non può derivare da Thread. Il principio è lo stesso: la nuova classe deve implementare Runnable
e quindi ridefinire il metodo run().

Un thread, in Java, può trovarsi in uno dei seguenti stati:


- new: appena creato;
- runnable: potenzialmente eseguibile dalla JVM o direttamente in esecuzione;
- blocked: se esegue un’operazione bloccante o sospensiva (es. Sleep);
- dead: quando termina il corpo del metodo run().

La terminazione di un thread (thread cancellation) consiste nella terminazione della sua attività
prima del suo completamento. Il thread da terminare prende in genere il nome di target thread.
Tipicamente, si considerano due tipi di terminazioni:
- asychronous cancellation (deprecato): un thread ne elimina immediatamente il target
thread invocando il metodo stop();
- deferred cancellation: il thread controlla periodicamente se deve terminare, controllando
nel metodo run() stesso che non si siano verificate le condizioni per la terminazione.

Per gestire tutti gli eventi associati ai componenti attivi dell’interfaccia grafica, la libreria Swing
utilizza un unico thread, creato inizializzato alla prima creazione della finestra che si esegue.
È facile verificare che tutta l’attività concernente tutti i componenti Swing (anche di finestre
distinte) è a carico del medesimo thread: è sufficiente creare due finestre, con due pulsanti
ciascuna, e registrare come ascoltatore di una un listener dal comportamento opportunamente
errato (un loop), e si potrà verificare che non è più possibile interagire con nessuno dei
componenti, né della medesima finestra, né di finestre distinte. Esse possono essere soltanto
chiuse, ma perché tale comportamento è gestito direttamente dal sistema operativo.

L’utilizzo di thread è indispensabile per la realizzazione di applicazioni interattive, in cui


l’interfaccia grafica deve rispondere con una certa prontezza all’input dell’utente. Come pattern
Luca Pagani – Riassunto di informatica LB

generale, si deve fare in modo di non utilizzare mai il thread di Swing per svolgere compiti
pesanti, pena lo stallo di tutta l’interfaccia.

Dichiarando un metodo synchronized si vincola l’esecuzione del metodo ad un solo thread per
volta. I thread che ne richiedono l’esecuzione mentre già uno sta eseguendo vengono
automaticamente sospesi dalla JVM, in attesa che il thread in esecuzione esca dal metodo (in
coda). Dichiarando più metodi synchronized il vincolo viene esteso a tutti i metodi in questione: se
un thread sta eseguendo un metodo synchronized, ogni thread che richiede l’esecuzione di un
qualsiasi altro metodo synchronized viene sospeso e messo in attesa. Da notare che tale vincolo
non vale nei confronti dei metodi non synchronized: il fatto che un thread stia eseguendo un
metodo synchronized non vieta ad altri thread di eseguire concorrentemente eventuali metodi non
synchronized dell’oggetto stesso.

È possibile “proteggere” con synchronized non solo metodi interi, ma solo porzioni di metodi,
definendo dei blocchi synchronized:
synchronized(obj){
< synchronized block >
}
Il codice all’interno di tale blocco viene eseguito dal thread corrente solo dopo aver ottenuto il
lock dell’oggetto Obj specificato come argomento. Un metodo dichiarato synchronized non è altro
che un metodo il cui corpo è racchiuso dentro un blocco del tipo appena descritto; un metodo
synchronized non viene ereditato da classi che estendono la classe base in cui compare.

L’altro meccanismo fondamentale di sincronizzazione fornito in Java è dato dalle operazioni wait,
notify e notifyAll, fornite come metodi pubblici della classe Object, classe base di qualsiasi
nuova classe definita (vedi documentazione).
Ogni thread che esegue una wait su un oggetto O, viene sospeso fin quando un qualsiasi altro
thread non esegua una notify o notifyAll sul medesimo oggetto O.
Un aspetto importante e sottile che lega di per sé i due meccanismi precedenti concerne il fatto
che per poter eseguire una wait su un oggetto, un thread deve prima avere ottenuto il lock.
Questo implica che i metodi wait / signal su un oggetto X possono essere invocati solamente:
- o all’interno di un metodo synchronized dell’oggetto X stesso;
- o all’interno di un blocco synchronized.
synchronized(Obj){
< synchronized block >
Obj.wait(); [oppure Obj.notify(); ]

}
Se un metodo wait o signal viene eseguito da un thread su un oggetto senza possederne il lock,
viene generata un’eccezione dalla JVM.
L’esecuzione del metodo wait comporta il rilascio del lock: ciò è fondamentale per permettere ad
altri thread in attesi di non poter invocare metodi synchronized dell’oggetto stesso o porzioni di
codice synchronized sull’oggetto stesso.

Un timer è un’entità che funge da sorgente di eventi temporali (es. timeout), all’occorrenza dei
quali, in genere, si vogliono eseguire determinate azioni.
Nella libreria java.util è presente la classe Timer che funge da timer; in particolare, su un
oggetto Timer è possibile registrare attività (rappresentate dalla classe TimerTask) affinché siano
eseguite ad un certo istante temporale assoluto, oppure ad intervalli regolari. Da un punto di vista
terminologico si dice che le attività sono “schedulate” per essere eseguito ad un certo tempo o ad
un certo insieme di istanti temporali.
La classe TimerTask è la classe base astratta per definire le attività che il timer deve mettere in
esecuzione quando scatta l’evento temporale. Il metodo principale è il metodo astratto run che
deve essere implementato nelle classi derivate, specificando l’insieme delle azioni che devono
essere eseguite. Gli altri metodi (non astratti):
- boolean cancel() Æ cancella l’esecuzione del task;
- long scheduledExecutionTime() Æ per ottenere il tempo a cui è stato schedulato il
task.

Per utilizzare un timer bisogna prima di tutto creare l’oggetto Timer:


Timer timer = new Timer();
Luca Pagani – Riassunto di informatica LB

Poi si devono schedulare le attività da eseguire, specificando a quale evento temporale devono
essere eseguite e se l’evento è periodico o meno.
Ecco i metodi vari di utilità:
- void schedule(TimerTask task, Date date): il task viene eseguito al tempo
assoluto specificato da date;
- void schedule(TimerTask task, Date date, long period): il task viene eseguito
peridocamente ogni period millisecondi, a partire dal tempo assoluto specificato da date;
- void schedule(TimerTask task, long delay): il task viene eseguito dopo delay
millisecondi;
- void schedule (TimerTask task, lond delay, long period): il task viene eseguito
dopo delay millisecondi e poi periodicamente ogni period millisecondi.

You might also like