Luca Pagani – Riassunto di informatica LB

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

corpo della funzione 1 } … prosegue

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){

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 { } 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;

istruzione

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 sottoproblemi 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 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.
U

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 inode è 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 {

} 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:

campi costruttori metodi

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: double value = Math.sin(Math.PI/6). es. È 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; verifica se due stringhe sono uguali; - boolean equals(String st) - 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) (restituisce la lunghezza dell’array) int size = v.length 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 oggettoeccezione; - 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. recupera l’elemento che si trova nella posizione specificata dall’indice. • E get(int index) • 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. rimuove l’elemento in posizione specificata dall’indice. • E remove(int index) • 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): Rappresenta l’espressione regolare. • Pattern • 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.

Sign up to vote on this title
UsefulNot useful