cerca
Dispense Tetty - Integrazione JAVA
modifica cronologia stampa login logout

Wiki

UniCrema


Materie per semestre

Materie per anno

Materie per laurea


Help

Dispense Tetty - Integrazione JAVA

Torna alla pagina di Programmazione degli Elaboratori


:: Integrazione su JAVA ::


Indice


JAVA

JAVA è un linguaggio di programmazione OOP sviluppato dalla SUN.
Deriva dal C++, che è derivato dal C, quindi ha una sintassi a noi nota, e soprattutto nota a praticamente tutti i programmatori del mondo.

Il C++ è un'estensione del C orientata agli oggetti. Presenta tutte le caratteristiche della OOP, anche se le implementa in un modo un po' complicato. La via del C pare essere sempre la più complicata. Rispetto al C++, il JAVA ha delle intuizioni piuttosto carine.

Il JAVA, almeno quello distribuito dalla SUN, è un linguaggio compilato, ma non in linguaggio macchina. Tra il JAVA e il nostro computer c'è uno strato aggiuntivo, detto JAVA VIRTUAL MACHINE, il quale è una astrazione dell'hardware sottostante (il nostro computer) e allo stesso tempo è un hardware virtuale, una sorta di simulatore LC2. I programmi in JAVA vengono compilati in bytecode, che è il particolare linguaggio macchina della macchina virtuale. La JavaVM quindi legge il bytecode, e lo interpreta al volo.

Questo sistema permette di definire un linguaggio che vada bene su tutti i computer con tutti i processori con i sistemi operativi più disparati, il che è un buon vantaggio. Lo svantaggio è che è lento, rispetto ad un programma compilato come il C o il C++.
Ma il vantaggio principale è che, essendo separata dall'hardware, la JavaVM mi permette di giocare finché voglio con memoria e cose simili, che tanto non potrò mai produrre danni perché proprio la JavaVM mi protegge dal fare casini a basso livello, cosa purtroppo simpaticamente possibile col C.

Torna su

Il Riferimento e gli Object

In JAVA, il concetto di puntatore è scomparso, ed è stato sostituito con il riferimento.
Il tipo riferimento può anche lui puntare a una classe, ad un vettore, ad un interfaccia, al NULL. Il suo tipo base è il tipo riferimento, ma poi assume il tipo dell'oggetto che sta maneggiando in quel momento.

Tutto ciò che esiste in JAVA deriva dalla classe primigenia detta Object: tutto è un'istanza di Object.
Quindi, un riferimento di tipo Object può riferirsi ad ogni cosa!

Rispetto ai puntatori, i riferimenti hanno perso alcune caratteristiche che li rendevano potenti ma anche pericolosi da usare.
Innanzitutto è scomparsa la pointer arithmetic: quella roba che ci permetteva di dichiarare un'array e di girarci dentro.

 char lista[20];
 char * gatto;
 gatto = lista;
 *(gatto + 10) = 'a'; // equivale a dire lista[10] = 'a';

Non è più possibile in Java prendere un riferimento e farlo puntare ad un indirizzo, perché si tratta di una cosa rischiosa. Infatti posso prendere un puntatore e dirgli di puntare a un indirizzo a caso, e mandare tutto il programma in SegFault, e nessuno me lo impedirebbe, e soprattutto è uno di quei bug che non si trovano mai.

Torna su

La classe String

La classe String è una sequenza di caratteri Unicode.
E' immutabile: se voglio modificarla, la copio in un'altra stringa, vi copio il suo contenuto modificato, e reimposto il riferimento alla prima stringa in modo che si riferisca a questa copia modificata. Niente paura tutto ciò lo fa il JAVA da solo.
Questa procedura risolve tanti problemi legati al fatto che le char[] sono puntatori, ed è legittimo fare così:

 char lista[20];
 *(lista + 50) = 'f';  //Dove vado a scrivere? Chi mi controlla? Paura!

I caratteri Unicode sono un modo per rappresentare in tabelle tutti i caratteri che non ci stanno nella misera e ristretta tabella ASCII. L'ASCII è nato in America, e lì usano pochi caratteri. E gli arabi? I cinesi? E noi europei con le lettere con strani accenti etc.? Grave problema, perché ognuno di costoro estendeva a suo modo la tabella ASCII, rendendola incompatibile con l'estensione sviluppata in un altro stato.
Così si è pensato all'Unicode, che rappresenta in modo univoco un carattere, tramite un sistema di tabelle indicizzate: la tabella dei caratteri greci, quella dei caratteri lituani (??) e così via.
Il problema dell'Unicode è che descrive in astratto il sistema delle tabelle, ma non dice niente sulla sua implementazione. Ecco perché sono nate le codifiche UTF: uno standard per implementare la definizione delle tabelle di caratteri inventata da quelli dell'Unicode. Si sa per esempio che in UTF il primo byte mi indica la tabella, il secondo il carattere, e così via. La specifica Unicode di per sé diceva solo: occorre indicare in modo diverso la tabella e il carattere, ma non diceva affatto come.

Torna su

Come faccio a dichiarare un riferimento?

E' semplice.

 nomeClasse riferimento_a_classe;
 tipo[] riferimento_a_vettore;
 nomeInterfaccia riferimento_a_interfaccia;

e così via. Proprio come in C.

Per accedere ai campi di una struttura di cui ho il riferimento, non uso più il -> del C, ma mi limto al semplice '.'. Ecco un esempio per rinfrescare la memoria:

 //La via del C
 struct gatto {
   int colore;
 };

 struct gatto * Cheope = (struct gatto *)malloc(sizeof(struct gatto));
 Cheope -> colore = 15;

 //La via del JAVA
 gatto Cheope;
 Cheope.colore = 15;

Torna su

Creare vettori

Semplice:

 int[] lista = new int[300];

Posso creare array con qualsiasi tipo, comprese classi che creo io.

Che cos'è questo new? È una sorta di malloc ma automatizzato. Ci pensa lui ad allocare la memoria necessaria all'interno della JavaVM, e io non devo preoccuparmene.

Torna su

Dichiarazione di una classe

Ecco come dichiarare una classe in modo formale

 <modificatore> class <nome classe> extends <super classe> 
                implements <interfaccia1> ... <interfaccia2>
 {
   <attributi>
   <costruttori>
   <metodi>
 }

 <modificatore> ::= public, protected, private, abstract, static, final

Per istanziare una classe, cioè creare un oggetto, uso ancora il new:

 tipoClasse classe = new tipoClasse(parametri del costruttore);

Il new pensa tutto a lui, alloca la memoria etc, e quando la classe non serve più decide lui come disfarsene. Il bello della JavaVM:)

Vediamo in dettaglio che cosa significano quei vari peciotti.

Torna su

Metodi, Costruttori e Attributi

I metodi sono funzioni, esattamente come quelle del C.

Il costruttore è una funzione particolare, che appartiene alla Classe e non all'Oggetto, e serve per costruire effettivamente la mia classe. Se costruisco una classe 126?, occorre obbligatoriamente sapere il numero di targa, e questo va passato come parametro del costruttore.

 class 126
 {
   126(string numero_di_targa) {
     targa = numero_di_targa;
   }
   string targa;
 }

Questo costruttore di classe 126 prende come parametro il numero di targa, e lo assegna all' attributo targa della mia classe.
Come potete vedere, è una funzione sì, ma non ritorna niente. Nemmeno il void. Proprio niente.

Torna su

Interfacce

Un' Interfaccia serve per stabilire solo l'interfaccia di una classe, ciò che è visibile all'esterno, il guscio.

 interface Automobile {
   int accendi();
   int spegni();
 }

Quando stabilische che la mia classe implementa un'interfaccia, faccio così:

 class 126 implements Automobile {
   int accendi() {...}
   int spegni() {...}
 }

Vuol dire che la mia classe 126 implementa l'interfaccia Automobile, e cioè deve fornire tutti i metodi specificati dalla mia interfaccia.

Posso anche implementare più interfacce

 class 126 implements Automobile, FortunaDrago { ... }

e vuol dire che la mia 126 deve implementare tutti i metodi dell'interfaccia Automobile e tutti i metodi dell'interfaccia FortunaDrago.

Quando definisco un'interfaccia, posso definire solo le seguenti cose:

  • costanti
  • metodi astratti (cioè solo prototipi, devono essere le classi che implementano quell'interfaccia a scrivere l'effettivo codice: l'interfaccia mi dice solo che deve esserci un metodo con quel nome e quel valore di ritorno, ma poi non dice nulla di come vada implementato. OOP all'opera)
  • classi membro (includo altre classi)
  • interfacce membro (include altre interfacce)

Questi due ultimi punti meritano un paragrafo a sé.

Torna su

Extends e Implements

Parlando della OOP, dicevamo che è possibile estendere una classe o un'interfaccia.

Le interfacce le abbiamo appena viste: basta che le implementi, e scriva tutti i membri che devono esserci, e ho implementato un'interfaccia. Abbiamo visto anche che posso implementare in una stessa classe più interfacce. Questa possibilità si chiama ereditarietà.

Invece la parolina extends mi fa estendere una classe già esistente: recupero i suoi membri già implementati, e ne aggiungo di nuovi.
Per esempio, ho scritto la classe 126, e ora voglio una 126 particolare che ha anche l'autoradio:

 class 126_tamarra extends 126 {
   //tutti i metodi di 126 sono già presenti!
   //ne aggiungo tre:
   int accendi_autoradio();
   int alza_volume_autoradio();
   int spegni_autoradio();
 }

La differenza tra questo tipo di estensione e l'estensione di interfaccia è duplice:

  1. se estendo una classe eredito anche i suoi bei metodi già implementati. Se voglio posso anche riscriverli
  2. posso estendere 1 sola classe, mentre posso implementare diverse interfacce.

Si parla di ereditarietà multipla quando posso ereditare da più classi, e in JAVA ciò non è permesso: posso solo implementare più interfacce.

Qual'è il problema dell'estensione di Classe?
Il problema è che se eredito da due classi diverse che hanno implementato un metodo con lo stesso nome, quale devo utilizzare? Il C++ permette sta cosa, e il procedimento per distinguere quale dei due metodi usare necessita di un avvocato per districarsene. Il JAVA semplicemente non lo permette.
Ciò deriva dal fatto che il JAVA è stato pensato per il Web: se eredito da un programma scaricato automaticamente da un sito, come faccio a controllare il codice come fa il C++ per stabilire chi eredita che cosa e così via? Non posso. Quindi niente ereditarietà multipla. E' molto meglio così.

Torna su

Interfacce e incapsulamento: modificatori

Nella nostra definizione di Tipo di Dato Astratto, dicevamo che occorre dargli le precondizioni e gli assiomi.
Le interfacce si avvicinano al Tipo di Dato Astratto, nel senso che permettono di separare l'interfaccia dal tuorlo. Ma non permettono di dare precondizioni e assiomi. Quindi non sono veri Tipi di Dato Astratto(tm).

Prima parlavo dei modificatori. Servono proprio all'incapsulamento, perché mi dicono chi rimane nascosto e chi rimane visibile.

Public vuol dire che quel metodo è visibile: tutti quelli che hanno accesso alla mia classe lo possono vedere e quindi utilizzare.
Private è il contrario: solo gli altri metodi della mia classe possono vederlo, dall'esterno no: c'è un guscio impenetrabile sopra di esso. Nemmeno le classi che ereditano da me possono vederlo, è mio e basta.
Protected è una cosa diversa: vuol dire che al di fuori non lo può vedere nessuno, ma i miei eredi sì.

Torna su

Ereditarietà e Polimorfismo

Come avete visto sopra, posso far ereditare interfacce o classi ad altre classi: ho una classe super, cioè superiore, da cui gli altri ereditano.

Il principio di sostituibilità di Liskov si occupa di queste faccende legali relative all'ereditarietà.

Una classe definisce un tipo, le sue sottoclassi sono sottotipi.
Dire che A è sottotipo di B vuol dire che ogni programma che utilizza un oggetto di classe A può utilizzare indifferentemente un oggetto di classe B senza modificare il comportamento logico, proprio perché è un suo sottotipo.
Se la 126 e la Rover sono entrambi sottotipi di Automobile, il programma Pilota che prende come argomento un'Automobile può guidare indifferentemente la 126 o la Rover.

Questo principio ci dice che una sottoclasse non può restringere il comportamento della classe genitrice. Se tutte le mie Automobili vanno a carburante, la 126 non può andare a pedali.

Per seguire questo principio, in fase di progettazione si deve sudare un po' per capire chi eredita da chi. Ecco un bell'esempio: rettangoli o quadrati. Devo creare la classe Rettangolo e derivarne la sottoclasse Quadrato, o viceversa?

Seguiamo la prima strada: creo il Rettangolo, ed il Quadrato deriva dal rettangolo. L'interfaccia del Rettangolo è questa:

 double base, altezza;
 impostaBase(double);
 impostaAltezza(double);

Quindi il Quadrato erediterà questa interfaccia. Ma quando implemento la classe Quadrato?
Supponiamo che qualcuno dica al mio quadrato: impostaBase(23). Essendo un quadrato, anche l'Altezza deve essere impostata a 23. Posso quindi implementare il metodo impostaBase(double) in modo che se aggiorno una cosa mi aggiorni anche l'altra automaticamente.

Problema problema: quando ad un Rettangolo raddoppio la base, la sua area raddoppia. Quando ad un Quadrato raddoppio la base, la sua area quadruplica! Il comportamento logico non è lo stesso: il principio di Liskov non è soddisfatto!!

Allora devo fare così: l'interfaccia madre è Quadrato:

  double base;
  impostaBase(double);

e il Rettangolo eredita da Quadrato e lo estende:

  class Rettangolo implements Quadrato {
    //la base e impostaBase(double) le ho ereditate
    double altezza; //lo aggiungo io
    impostaAltezza(double); //anche questo lo aggiungo io
  }

e così tutto funziona.

Per dare un po' di termini, l'ereditarietà di interfaccia si chiama subtyping, mentre l'ereditarietà di realizzazione (di classe) si chiama subclassing.

Il subtyping permette la compatibilità dei tipi: il sottotipo è Liskovianamente accettabile, ed è compatibile all'indietro con tutti i suoi antenati. Questo è già polimorfismo, nello specifico polimorfismo per inclusione.
Il controllo del polimorfismo lo si può fare anche staticamente: il JAVA prende la lista degli antenati e vede se tutto quello che faccio è accettabile.

Come ho detto prima, invece, il subclassing prende dagli antenati anche il codice effettivamente scritto.

Torna su

Algebra dei tipi

Warning: i caratteri strani usati in questa sezione sono proprio strani. Qui sono arrotondati, ma dovete immaginarveli tutti squadrati

L'Algebra dei Tipi è una particolare Algebra usata per spiegare l'ereditarietà. I compilatori dei linguaggi OOP includono dei sistemi per verificare, in base a quest'Algebra, se le regole dell'ereditarietà sono state rispettate o meno. E valgono anche per le cose banali tipo l'assegnazione di un char ad un int.

T
T è il tipo Top, quello da cui tutti gli altri tipi derivano.


┴ è il tipo Assurdo, che cerca di riunire interfacce incompatibili

A B
Vuol dire che il tipo A è sottotipo (estende) il tipo B. Quindi A può avere qualche cosa in più di B.

E = A ᑎ B
Vuol dire che il tipo E è il tipo più generale, cioè quello con meno antenati, che estende sia A che B.
ᑎ si chiama meet.

In base a questa E = A ᑌ B
Vuol dire che E è il primo antenato che trovo, da cui sia A che B derivano.
ᑌ si chiama join.

L'assegnazione

 tipoX x;
 tipoY y;
 x = y;

è valida se ho tipoX tipoY.

Lo stesso vale per i metodi

 //prototipo:
 dario (tipoX);

 //chiamata:
 dario(tipoY);

è valida sempre se tipoX tipoY.

Torna su

Polimorfismo

Il polimorfismo è la capacità dello stesso oggetto di apparire in forme diverse in contesti diversi.
Posso anche dirla così: capacità di oggetti diversi di apparire nella stessa forma a seconda del contesto.

Nei linguaggi tradizionali non c'è polimorfismo, ma solo monomorfismo: per esempio, una funzione accetta come parametri solo oggetti di un certo tipo. I linguaggi polimorfi accettano invece tutti i tipi che si possono "trasformare" in un altro tipo, secondo le regole dell'Algebra dei Tipi spiegata qui sopra.

Ci sono diversi tipi di polimorfismo, secondo la classificazione Cardelli-Wegner:

  • Universale
* parametrico
* per inclusione
  • Ad Hoc
* overloading
* coercion

Il polimorfismo del C è del tipo 'ad hoc: vuol dire che è limitato a pochi tipi predefiniti, scelti, appunto, ad hoc.

La coercion si ha quando un tipo viene convertito automaticamente in un altro tipo, secondo certe precise regole di promozione. Si pensi ai char che vengono convertiti in int e così via nel C: avviene un cast automatico, lo fa il linguaggio.

Invece, l' overloading si ha quando fornisco un operatore con lo stesso nome, ma che ho definito in più modi, ed ognuno di questi modi prende in argomento un tipo diverso.
Ciò vuol dire che posso definire una funzione così:

 void moltiplica(int valore);
 void moltiplica(float valore);

Nei linguaggi che supportano l' overloading, è il compilatore scelto a stabilire quale delle due funzioni utilizzare, a seconda che io chiami moltiplica passandogli un intero oppure un float.

Il polimorfismo universale invece si applica a tutti i tipi indifferentemente, seguendo le care regolette dell'Algebra dei Tipi.

Quello per inclusione è quello della OOP: se implemento un'interfaccia, sono un suo sottotipo; se estendo una classe, sono un suo sottotipo.

Il polimorfismo parametrico è ben diverso. Vuol dire avere la capacità di stabilire il tipo che una funzione ritorna in base al tipo in ingresso. Questa funzione la dichiarerei così:

 Tipo somma(Tipo A, Tipo B);

ma qui Tipo non è un particolare tipo, bensì vuol dire: prendi il tipo che ti passano come parametro, ed utilizzalo come tipo per la variabile di ritorno.
Avendo definito così la funzione, posso fare con la stessa funzione queste cose:

  int gatto;
  gatto = somma(23, 45);

  float cane;
  cane = somma(23.5, 45.7);

  126 dario = new 126("bg669287");
  126 clara = new 126("cr708982");
  126 risultato;
  risultato = somma(dario, clara);

Capite? È follia! Eppure si può fare, volendo. Si chiama programmazione generica, o template programming. Il C++ so per certo che implementa queste cose, non che io sappia come farlo. Cmq chi ha usato un po' la STL del C++ ha usato implicitamente template, cioè questo tipo di polimorfismo, perché la STL vuol dire proprio Standard Template Library.

Nota: da qualche parte deve essere specificato come fare la somma tra due oggetti di tipo 126, e anche se sembra una cosa assurda sommare due 126, al compilatore non gliene frega niente: se definisco l'operatore '+' che prende come parametri due oggetti di tipo 126, a lui va benissimo.
A livello ancora più generale, dovrebbe essere possibile definire un'operazione dal nome compareTo, e tutti i tipi che la implementano sono comparabili l'un l'altro. Ciò varia da linguaggio a linguaggio, ma vi assicuro che è cosa fattibile.

Ecco quindi spiegato come l'ereditarietà porti al polimorfismo.

Torna su

Errori ed Eccezioni

Eccezione = anomalia recuperabile.
Errore = anomalia irrecuperabile.

Ho un'eccezione quando cerco di aprire un file non esistente: errore qua e là ma posso continuare il programma, magari insultando l'utente

 if (Fp1 == NULL) printf("Imbecille! Devi specificare il nome file!\n");

Ho un errore quando invece tutto quanto va a puttane, come per esempio vado a scrivere su una porzione di memoria che dovrei lasciare in pace, e il sistema operativo va in SegFault e tutto s'impalla. Gli errori sono quelli che producono gli Schermi Blu della Morte di Windows, e non c'è altro da fare se non riavviare il computer.

Come fare a gestire le eccezioni? Nel passato si sono usati diversi modi.

Il sistema più scarabottolesco è quello delle TRAP e degli INTERRUPT. Se qualcuno si ricorda il divertimento della LC2, qui ritrova terminologia nota. Per esempio, mi arriva una divisione ma il registro mi contiene tutti 0, e la CPU si lamenta e lancia un'INT. Sta poi a me con una TRAP gestirmi l'INT. Se non avete ancora letto del MIX, la TRAP è una trappola che intercetta vari avvenimenti hardware, in questo esempio una Division by Zero.

Il sistema del C è quello dei valori di ritorno. Se tutto va bene ritorno 0, se no ritorno altri numeri, e sta poi a chi ha chiamato la mia funzione capire che cosa è successo.

 #define ERR_FILE_NOT_FOUND 32

 if (Fp1 == NULL) {
   printf("File inesistente\n");
   return ERR_FILE_NOT_FOUND;
 }

Un altro espediente è quello delle variabili di stato, cioè le variabili che vengono dichiarate prima del main, e che quindi sono visibili a tutti. Da ricordare che le variabili globali sono il male e sono da evitare, perché chiunque le può scrivere a caso e/o a piacimento. Si potrebbero usare così:

 int ERRORE = 0,

 int main() {
   ...
   if (Fp1 == NULL) ERRORE = 1;
   ...

   return ERRORE;
 }

Nel mondo delle interfacce grafiche, invece, dove è l'utente a decidere che cosa far fare all'applicazione, i vari clic del mouse o apertura di menu etc. sono detti eventi. Un errore è semplicemente un altro evento, e sta poi alla routine che gestisce gli eventi arrabattarsi per gestire l'errore. Non faccio esempi che qui si dilungherebbero troppo.

Torna su

La via OOP alla gestione di Errori ed Eccezioni

I linguaggi OOP (tutti) gestiscono le Eccezioni in questo modo: un'Eccezione è un Oggetto, né più né meno, che viene istanziato nel momento in cui si verifica un'anomalia.
Questa creazione dell'oggetto si chiama "lancio", in inglese "throw".

La teoria sottostante è quella della patata bollente: io lancio la patata bollente (la mia eccezione) e qualcuno dovrà afferrarla ("catch"), e quel qualcuno è la parte del mio codice che si preoccupa delle eccezioni.

In JAVA esiste la classe Throwable, cioè "gettabile", ed Errori ed Eccezioni sono tutti derivati di questa. Esiste naturalmente una monumentale gerarchia di errori, l'uno figlio dell'altro, per rappresentare tutto ciò che può andare male in un programma, ed è tanta roba...
Le varie eccezioni ereditano da Exception, gli errori da Error, i due figli di primo letto di Throwable.

Quindi, ho un mio bel metodo, e lo dichiaro così:

 tipo mioMetodo(parametri) throws BruttaEccezione { ... }

E poi chiamerò questo mioMetodo all'interno di un costrutto try ... catch:

 try
 {
   mioMetodo(valore);
 }
 catch (BruttaEccezione ahiahi)
 {
   ... // gestione dell'eccezione
 }

Che cosa succede in queste linee di codice?

Innanzitutto, ho dichiarto che il metodo mioMetodo, se va storto, lancia l'eccezione detta BruttaEccezione (posso averla inventata io, oppure esistere già in JAVA).

Poi, dentro il try { ... } chiamo il mioMetodo. Chiamarlo da dentro il costrutto try { ... } mi assicura che, nel caso generi un errore, esso sarà catturato dal seguente costrutto catch { ... }.
Infatti, il mio catch (BruttaEccezione ahiahi) riceve l'eccezione gettata da mioMetodo, e poi se la gestisce come vuole.

Posso anche non avere nessuna catch: l'errore arriva ma non reagisco. Oppure avere più di una catch, una per ogni eccezione che mi aspetto di ricevere.

Esiste infine la clausola finally {...} in cui si finisce anche se c'è qualcosa che il catch non ha preso. Quindi, il costrutto definitivo è

 try
 {
   ...
 }
 catch (tipoEccezione nome)
 {
   ...
 }
 finally
 {
   ... // qui si viene comunque, errore o non errore
 }

Quando un metodo getta un'eccezione, il metodo si interrompe e ritorna a chi lo ha chiamato. Se questo chiamante non gestisce l'eccezione, si torna a chi ha chiamato il chiamante e così via. Si dice che l'eccezione si propaga. Se nessuno la prende, arriva alla JavaVM e si blocca. Di solito cmq c'è qualcuno che prende l'eccezione.

Torna su

I/O in JAVA

JAVA gira su una Virtual Machine, cioè una macchina virtuale. Quindi, può girare su diversi sistemi operativi, come Linux, e anche su sistemi inoperativi come windows. Ognuno di questi sistemi, operativi o inoperativi, può gestire in modo diverso l'I/O. La JavaVM deve quindi astrarre dalle implementazioni specifiche, ed offrire al programmatore JAVA un'interfaccia comune a tutti.

Innanzitutto, i file sono sequenze di dati di un certo tipo. Possono essere di tipo disparato, ma tutti i file hanno in comune certe cose:

  • sono identificati da nome e percorso
  • si aprono e si chiudono
  • posso accedere ad essi in lettura o in scrittura
  • si può posizionare la testina all'interno di essi

In JAVA ho la libreria JAVA.IO che incapsula il concetto di stream, a noi già noto dal C. Lo stream è un flusso di dati, in arrivo o in partenza.
L' InputStream è il flusso in arrivo, mentre OutputStream è il flusso in partenza.

Tutti gli stream hanno un'interfaccia comune, come è lecito aspettarsi:

  • InputStream
* read(); legge
* skip(); salta un certo numero di dati
* mark(); marca una posizione per poi poterci ritornare
* reset(); azzera la testina
* close(); chiude il mio stream



* OutputStream

* write(); scrive
* flush(); svuota il buffer e lo scrive tutto (sincronizzazione)
* close(); chiude il mio stream

E naturalmente ci sono tutte le classi che ereditano da ciò e sono specializzate ad esempio nella lettura di file di char e così via.

Torna su

Serializzazione

È un meccanismo JAVA che mi permette di salvare gli oggetti su disco, in un file, anche quelli creati da me, in un certo formato, per poi recuperarli così come sono.
È JAVA stesso che decide come rappresentare i miei oggetti, basta che essi implementino l'interfaccia Serializable.

Se invece voglio essere io a decidere come rappresentare i miei oggetti quando li salvo, devo implementare l'interfaccia Externalizable.

Torna su


Torna alla pagina di Programmazione degli Elaboratori