Docsity
Docsity

Prepara i tuoi esami
Prepara i tuoi esami

Studia grazie alle numerose risorse presenti su Docsity


Ottieni i punti per scaricare
Ottieni i punti per scaricare

Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium


Guide e consigli
Guide e consigli


Appunti di OOP Programmazione Ad Oggetti Java, Appunti di Elementi di Informatica

Questi appunti contengono tutti i riferimenti necessari per affrontare un corso di Programmazione ad Oggetti in Java. Contenuti principali: information hiding,relazioni fra classi, riusabilità del codice, classi, interfacce, upcasting, downcasting, composizione, ereditarietà, polimorfismo, java, eccezioni, input output, collection

Tipologia: Appunti

2012/2013

In vendita dal 30/09/2013

tanux87
tanux87 🇮🇹

11 documenti

1 / 41

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
Java maton aropp’ maton:
Appunti
di
Programmazione ad Oggetti
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18
pf19
pf1a
pf1b
pf1c
pf1d
pf1e
pf1f
pf20
pf21
pf22
pf23
pf24
pf25
pf26
pf27
pf28
pf29

Anteprima parziale del testo

Scarica Appunti di OOP Programmazione Ad Oggetti Java e più Appunti in PDF di Elementi di Informatica solo su Docsity!

Java maton aropp’ maton:

Appunti

di

Programmazione ad Oggetti

Nota Introduttiva

Il presente documento, è un riassunto di riflessioni, appunti, slides del

corso di OOP, letture dal libro Thinking in Java:

non vuole sostituire nè le slides, nè libri ufficiali e consigliamo in ogni

caso di non studiare esclusivamente da questo pdf, ma di seguire

comunque slides presentate dai professori e di approfondire ulteriormente

da libri di testo ufficiali.

L’Information Hiding Concetto , conosciuto anche con il nome di incapsulamento, che si riassume con una frase: nascondere l’implementazione,separando l’interfaccia (aspetto esterno di un oggetto), dall’implementazione (aspetto interno) dell’oggetto stesso. Per spiegare questo concetto, immaginiamo che 2 entità si ripartiscono un’applicazione: i creatori di classe(che implementa i dati nelle classi) e i programmatori client (manco a dirlo:gli oggetti, che utilizzano tramite l’interfaccia i dati presenti nelle classi, per le loro applicazioni). Questo sorta di condivisione può portare all’uso improprio dei dati da parte del programmatore client (ad esempio alterandoli in maniera impropria ), se questi non sono in qualche modo protetti; allora il creatore della classe deve limitare gli accessi , o meglio controllare gli accessi ai dati , fornendo cosi l’accesso solo ai dati di cui realmente si abbisogna, e nascondendo di conseguenza le informazioni (più precisamente l’implementazione) non necessarie a quel programma client. Questo meccanismo comporta, nella progettazione di un’applicazione, pregi dell’applicazione stessa molto importanti: si limitano i bug nei programmi,consente la riusabilità del codice dando la possibilità di modifiche di particolari classi senza curarsi di dover modificare il resto delle classi (perchè si modifica l’implementazione, ma non l’interfaccia),e inoltre aumenta la modularità dell’applicazione. In generale ciò che si rende protetto o nascosto, sono proprio gli attributi (appunto i dati interni) della classe, e si rendono pubblici i metodi della classe ( e i metodi saranno gli unici a poter agire sugli attributi della classe).

Le relazioni fra le classi Tra le classi esistono delle relazioni logiche che correlano le classi fra di loro:

  • Generalizzazione : si definisce una classe astratta dal contesto che poi sarà specializzata(Superclasse).
  • Specializzazione : si specializza una classe con le caratteristiche di una classe generalizzata(Sottoclasse); Un esempio di classe generalizzata e che poi sarà specializzata è la Classe Astratta , che contiene metodi astratti che non contengono implementazione, e si lascia alle classi specializzande di implementare il codice di quei metodi.
  • Associazione: quando non esiste nessun tipo di gerarchia tra due classi,e le istanze delle due classi devono scambiarsi comunque dei messaggi, esse possono considerarsi associate.
  • Aggregazione: La relazione di aggregazione è un caso speciale di associazione che viene utilizzata per mettere in evidenza le situazioni in cui un oggetto è una parte di un oggetto più grande.
  • Composizione: La relazione di composizione viene utilizzata per indicare che un oggetto è composto da altri oggetti, quindi se non esistono questi oggetti,non esiste nemmeno l’oggetto composto.
  • Ereditarietà: è il meccanismo con il quale una classe ha implicitamente definiti metodi e attributi di altre classi.

La Riusabilità del codice: riusare l’implementazione VS riusare l’interfaccia Innanzitutto c’è da fare una precisazione: la prima forma e quella più evidente di riusabilità del codice, sta proprio nelle classi; infatti l’elemento di codice “più riusabile” è la classe stessa, che può essere riusata proprio per sviluppare i concetti come Ereditarietà, Composizione,Aggregazione,Polimorfismo. Per riusabilità del codice, si intende quella peculiarità (raggiungibile con intuizione o esperienza) che si da al progetto di un’applicazione, e che prevede la possibilità di effettuare delle modiche al codice in maniera semplice, utilizzando proprio gli oggetti gia presenti in quell’applicazione e comporre nuove classi grazie all’instanziamento di quelle già esistenti (concetti di Composizione , Aggregazione ) riusando quindi l’implementazione. Quando invece creiamo una classe,e magari poi siamo costretti a crearne una molto simile e che faccia quasi le stesse operazioni, ciò produrrebbe uno spreco, e sarebbe più gradevole se invece creassimo un clone della classe a poi aggiungere le modifiche che occorrono al clone, sfruttando di conseguenza l’interfaccia di quella classe.

Questo concetto è detto Ereditarietà , e comporta uno svantaggio nel confronto con la Composizione: il clone di una classe, subisce le stesse modifiche apportate alla classe da cui è stata clonata, chiamata Superclasse, perchè appunto riutilizzo l’interfaccia. Ma ciò non è sempre visto come uno svantaggio: infatti è possibile vederlo come facile applicazione di manutenibilità del codice , infatti se voglio applicare un nuovo metodo comune a tutte le classi, mi basta inserirlo nella superclasse, e si riverserà sulle classi derivate, accelerando di fatto la manutenibilità.Altro vantaggio introdotto dall’ereditarietà è il fatto che si può testare molto più velocemente il codice implementato in una classe derivata. Infatti se la classe derivata appena modificata farà sorgere un errore, “sappiamo gia dove mettere le mani”.

Nota1: In java, l’ereditarietà è garantita già appena dichiariamo un oggetto. Infatti tutto deriva dalla classe Object, che è la classe a gerarchia più elevata; infatti quando scriviamo: public class UnaClasse il compilatore lo vedrà come public class UnaClasse extends Object.

Nota2 : Se in una classe derivata volessimo in qualche modo richiamare il costruttore della superclasse nel costruttore della classe derivata, java ci mette a disposizione la parola chiave super( ); che è appunto un metodo che si riferisce al costruttore della superclasse. Analogamente ciò puo essere fatto per gli attributi,cioè quando ci si vuole riferire agli attributi della superclasse, con la seguente dicitura: super.nomeAttributo; Ciò ci serve in un contesto in cui vogliamo richiamare il costruttore della superclasse e passargli parametri definiti nella classe derivata.

public class ClasseDerivata extends ClassePadre{

public ClasseDerivata (String msg){ super (msg); //riferito al costruttore della classe padre }

}

Nota3 : In Java non è permessa l’ereditarietà multipla, cioè non è possibile fare ad esempio:

public class ClasseDerivata extends Classe1,Classe2 {

}

L'ereditarietà multipla può essere causa, in alcuni contesti, di qualche confusione. Una possibile causa di ambiguità è la seguente: se due classi base hanno ciascuna la propria implementazione indipendente dello stesso metodo o della stessa funzione, quale delle due è ereditata dalla classe derivata?Però questo inconveniente può essere aggirato, e spiegato in seguito.

Class VS Interface Abbiamo ben esplicitato il concetto di classe che è la descrizione degli attributi e dei metodi di un oggetto e che contiene implementazione di quei metodi, e sappiamo che per creare una classe utilizziamo la parola chiave class. Ma abbiamo notato che non è consentita l’ereditarietà multipla da più classi contemporaneamente. Per aggirare il problema, è stato introdotto in concetto di interfaccia. In pratica, invece di dichiarare una classe, dichiariamo proprio un’interfaccia nel senso pratico del codice, all’interno della quale definiamo solo le firme dei metodi, e non la loro implementazione. L’implementazione sarà poi ridefinita nella classe che utilizzerà l’interfaccia. Nella pratica invece di mettere

Upcasting Prima di parlare di upcasting, definiamo cos’è il casting. Il casting riguarda i tipi: in pratica è il processo per il quale da un’informazione in un certo tipo, si può ottenere l’informazione di un altro tipo. Ciò non avviene modificando l’informazione, ma si crea una nuova informazione da quella di base, ed è possibile effettuare il casting sia degli oggetti sia dei tipi primitivi. Durante il casting però può avvenire una perdita di informazioni: infatti se da un tipo int voglio passare ad un tipo float , posso farlo tranquillamente perchè qui non vi è perdita dell’informazione in quanto il tipo float offre una dimensione più ampia del tipo int, e in questo caso il casting non è richiesto. Viceversa, se da un tipo float voglio passare ad un int, ho perdita dell’informazione e quindi è necessario un operazione di casting. Un operazione di casting esplicito a livello di codice è cosi eseguita:

int a=4; float b=2.5;

a=( int )b; //casting esplicito b=a; //casting implicito

Lo stesso discorso avviene per gli oggetti(cioè si perdono informazioni), cambia solo il contesto: infatti ricordiamoci che gli oggetti hanno il riferimento da una parte, e il valore in un’altra. Quindi se definisco un oggetto di un certo tipo, e poi lo voglio assegnare ad un oggetto di un altro tipo, ciò non mi è consentito perchè non assegno direttamente il valore, ma il riferimento; con la conseguenza che l’oggetto a cui ho assegnato un oggetto di diverso tipo, punterebbe a quest’ultimo (e quindi ai suoi metodi) ma non può farlo, perchè punta a metodi ed attributi che sono solo quelli definiti nella sua classe. Riassumendo con un esempio: La classe Impiegato è la superclasse, e la classe Capo è una classe derivata;

non posso fare: Capo c = new Impiegato();

posso fare invece: Impiegato c = new Capo(); //upcasting.

Ovviamente ciò può produrre perdita delle informazioni, in quanto è possibile solo accedere ai metodi pubblici della classe padre (che ovviamente possono essere ridefiniti in quella figlia), ma non posso accedere agli eventuali metodi aggiunti nella classe figlio.

Downcasting E’ possibile vedere un oggetto della superclasse, come un oggetto della classe derivata?Questo non è assolutamente possibile, perché è un errore logico, rilevato dal compilatore. Per cui si ha bisogno di un casting esplicito per effettuare un operazione del tipo: Capo c = new Impiegato();

e quindi correggere come Capo c = (Capo)new Impiegato();

Il compilatore tradurrà come Capo capo = new Capo();

Esempio di Composizione: L’Automobile ha un motore. Nella realtà sappiamo che un automobile ha un motore, quindi se dovessimo scrivere un applicazione per la gestione di un automobile, possiamo pensare di creare una classe Automobile(con le proprie caratteristiche) il cui istanziamento prevede, ovviamente, un oggetto di tipo Automobile. Poi possiamo riusare l’implementazione fatta, per aggiungere un componente comune a tutti gli oggetti di tipo Automobile, quale ad esempio può essere il motore, e quindi creare una classe Motore(con le proprie caratteristiche sicuramente differenti dalla classe Automobile) che istanziata ci da l’oggetto di tipo Motore.

La modifica apportata va ad ampliare si la nostra applicazione, ma non comporta modifiche o conseguenza alla classe Automobile, nonostante,idealmente, il motore sia una parte essenziale di un automobile. Tutto ciò perché abbiamo sfruttato bene la riusabilità del codice.

Esempio di Ereditarietà: Il cardiologo/neurologo è un medico. Immaginiamo di progettare una classe Medico(la superclasse) che ha le proprie caratteristiche e i propri metodi. Supponiamo poi, di dover ampliare il nostro progetto aggiungendo delle classi che sono la specializzazione della classe Medico, e quindi di scrivere classi Cardiologo, Neurologo(classi derivate). Queste classi, definite in termini della classe Medico, ereditano le caratteristiche della classe Medico, ma allo stesso tempo è possibile ampliarle con nuovi metodi propri di quelle classi, e tutto ciò non comporta conseguenze o modifiche alla classe Medico. Il rovescio della medaglia pero, è che se apporto modifiche alla classe Medico, ne risentiranno tutte le classi definite in termini di quella. Inoltre, se abbiamo aggiunto dei nuovi metodi nelle classi derivate (e non specializzato quelli della superclasse, questi nuovi tipi creati non si legano alla superclasse con la relazione è un ma con la relazione è come un ) e la superclasse non potrà accedere ai membri della classe derivata. Da notare una cosa importante (cioè come l’information hiding si intreccia con l’ereditarietà): abbiamo detto che quando ereditiamo da un tipo esistente, ne creiamo uno nuovo, e questo nuovo tipo può avere sia i membri di quella da cui si eredita,sia i propri membri; c’è da notare pero, che se la superclasse ha dei propri membri nascosti (o privati), non saranno sicuramente visibili da questo nuovo tipo creato. Composizione VS Aggregazione Nelle relazioni tra le classi, abbiamo definito i concetti di aggregazione e composizione, affermando che essi sono molto simili tra di loro, ma non uguali. Ciò che li differenzia è la forza della relazione. L'aggregazione è una relazione puramente logica. Entrambe rispondono alla relazione “ha un”, e in entrambi i casi posso ampliare l’applicazione con una classe che contiene oggetti o istanziamenti di altre classi. Nell’aggregazione, creo gli oggetti nella classe che sto creando, poi li associo ai componenti dell’oggetto, ma ciò lo posso fare anche al di fuori dell’istanziamento della classe che sto creando. Quindi si tratta di effettuare questo processo: creo una nuova classe e al suo interno istanzio altre classi che servono(quindi metto dentro degli oggetti). Poi al di fuori della classe, alloco prima gli oggetti che compongono la classe, e istanzio la classe creata e alloco il relativo oggetto e lo uso.

public class A { /si supponga che B e C siano due classi gia create con le quali instanziamo gli oggetti aggreganti/ private B oggettoB; private C oggettoC;

public void usoB(B oB){ oggettoB = oB; }

public void usoC(C oC){ oggettoC = oC; }

public static void main(String[] args) { /alloco al difuori della classe A gli oggetti che ho aggregato/ B b = new B(); C c = new C();

A a = new A(); a.usoB(b); a.usoC(c); }

}

In questi termini si potrebbe pensare di dichiarare i metodi static e final, per aumentare l’efficienza dei programmi, invece ciò nella maggior parte dei casi non influisce in termini positivi sulla complessità dell’intero programma. E quindi, è meglio dichiarare metodi final, solo se non si vuole modificarne i comportamenti. Riassumendo: Nell’early binding, la chiamata ad metodo/funzione viene gestita cercando il riferimento della funzione da eseguire a tempo di compilazione, nel late binding la chiamata viene risolta a tempo di esecuzione.

Polimorfismo Il termine polimorfismo, indica che un qualcosa può avere più forme. Questo concetto, può essere sfruttato ad esempio per permettere ad un metodo di una certa classe, di eseguire funzioni diverse in base all’oggetto che lo richiama Sappiamo che quando inviamo un messaggio ad un oggetto (e quindi richiamiamo un metodo), l’oggetto eseguirà una certa richiesta in base al metodo che ascolterà il messaggio. Quindi siamo indotti a pensare che l’associazione messaggio-metodo sia univoca, cioè uno stesso messaggio sarà ascoltato sempre dallo stesso metodo. Con il polimorfismo ci si astrae (e il termine astrazione non è detto a caso, e vedremo perché)dalla relazione univoca prima citata, e si può associare lo stesso messaggio anche a metodi che eseguono funzioni diverse.

Ma come è possibile ciò? Sembrerebbe infatti un paradosso assurdo;e infatti lo è per i linguaggi procedurali (come il C), ma non per i linguaggi orientati agli oggetti (come il Java).Il fatto che sia possibile una cosa del genere, è dato dal fatto che il Java basa i collegamenti della chiamata ad un metodo con la relativa implementazione sulla tecnica del late binding (prima descritto). Grazie a questa tecnica, “si decide” a tempo di esecuzione quale oggetto determinare e chiamare il metodo appropriato. In sostanza, si invia un messaggio all’oggetto, e l’oggetto deciderà qual è la cosa giusta da fare; richiamando un’implementazione differente a fronte della stessa invocazione. La tecnica del polimorfismo, consiste nel creare una classe base generale che contiene i metodi e gli attributi comuni a tutte le classi derivata che poi creeremo ,quindi poi creare (quando si vuole) sottoclassi derivanti che potranno anche svolgere azioni diverse tra loro, creando cosi nuovi tipi, e il tutto rende estensibile la nostra applicazione, tutto a vantaggio di manutenibilità del codice, in quanto i nuovi tipi creati,ed eventuali metodi aggiuntivi, non si rifletteranno sul codice gia implementato. Nel gergo tecnico, la classe generale viene chiamata Classe Astratta , e in java per evidenziare che una classe è astratta si pone la parola chiave abstract prima della parola class. Ogni metodo di una classe astratta implementerà soltanto l’interfaccia dei metodi ( che saranno quindi anch’essi metodi astratti),e questi metodi saranno poi specializzati nelle classi derivate,e specializzati in base alla funzione che dovranno svolgere. Specializzare i metodi, non è del tutto preciso:infatti qui si esegue una riscrittura del codice dei metodi in ogni tipo che creiamo:questa tecnica è detta in gergo Override, in quanto aggiungiamo personalmente dei metodi alla classe. Importante da dire, che si comunicherà solo con l’interfaccia della classe base.

Aspetto molto importante è la chiamata ai costruttori. Difatti non basta richiamare il costruttore della classe derivata, bensì è necessario chiamare anche il costruttore della classe generica. Ed è per questo motivo che all’interno del costruttore della classe derivata bisogna aggiungere la chiamata al costruttore della classe generale (il che può avvenire attraverso super).

La Manutenibilità del codice La manutenibilità del codice rappresenta uno dei punti più critici nel ciclo di vita del software ed il suo costo è di norma superiore a quello dello sviluppo iniziale. Infatti è stimato che apportare modifiche al progetto occorre uno sforzo doppio, che per sviluppare inizialmente il progetto stesso. La manutenibilità è una metrica di qualità del software che comprende diverse proprietà interne ed esterne di un prodotto e del suo processo di sviluppo. E’ un obbiettivo di qualità del software a lungo termine.

Una delle regole fondamentali che è bene tenere a mente, è che nel tempo un software può essere modificato per adattarlo a nuove specifiche, e queste modifiche del codice devono essere apportate facilmente e in tempi rapidi, ed ovviamente queste modifiche devono comunque mantenere alta la qualità del software.

La manutenibilità si ottiene osservando alcune “regole speciali”:

  • Curare l’organizzazione del codice (con ruoli e responsabilità);
  • Curare la leggibilità del codice, adottando una convenzione di stile;
  • Automatizzare i test;
  • Semplificare il debbuging;

Note Aggiuntive : Refactoring del Codice. Il refactoring è una pratica di sviluppo, il cui scopo fondamentale è di migliorare la struttura del codice, per favorire il riutilizzo di porzioni di esso, per migliorarne la leggibilità e prepararne l’evoluzione. Il principio fondamentale sul quale si basa il refactoring, è riassunto molto bene dalla definizione che ne da Martin Fowler, primo fra tutti a teorizzare tale pratica. In altre parole: il refactoring rappresenta la necessità di modificare una porzione di codice, possibilmente di dimensioni limitate, senza alterare il compito che essa svolge e quindi senza alterarne il risultato.

Riassumendo:la programmazione OOP è un tipo di approccio,dal quale scaturiscono enormi peculiarità in termini di produzione di applicazioni: implementare facilmente nuovi oggetti nel nostro programma,e creare pacchetti di oggetti in modo tale da rendere sì complesso il nostro programma, ma nascondere questa complessità dietro al concetto stesso di oggetto con cui il nostro modo di pensare ha molta familiarità; far comunicare gli oggetti fra di loro, creare una categoria oggetto principale, del quale fanno parte oggetti della stessa tipologia, ma che possono agire per conto loro.

JAVA Le caratteristiche della piattaforma Java nasce come un linguaggio per lo sviluppo di software con le seguenti caratteristiche:

  • Leggero ( programmi di piccole dimensioni);
  • Sicuro (applicazioni orientate al web, gli Applet)
  • Portabile ( indipendente dalla piattaforma o dal S.O. su cui gira)

Peculiarità della piattaforma Java:

  • Scalabilità (capacità di un sistema di "crescere" o "decrescere" (aumentare o diminuire di scala) in funzione delle necessità e delle disponibilità. L'uso più tradizionale si riferisce alla scalabilità di carico, ovvero la capacità di un sistema di incrementare le proprie prestazioni. se a tale sistema vengono fornite nuove risorse (per esempio, nel caso del software, maggiore potenza di processore o processori aggiuntivi). Quanto un determinato sistema sia scalabile dipende dalla sua architettura; per esempio, la presenza di uno o più colli di bottiglia potrebbe rendere ininfluente l'aumento della potenza di calcolo complessiva.
  • Applicabilità (campi in cui può svariare,grazie alle migliaia di classi presenti in ogni versione della piattaforma; ad esempio in ambito della sicurezza,grafica ,web…);
  • Portabilità (garantisce l’ indipendenza sia dall’architettura hardware che software su cui gira. Ben noto è il fatto che se vogliamo riutilizzare lo stesso programma su una piattaforma diversa da quella di sviluppo, dobbiamo ricompilare il file sorgente sulla nuova piattaforma e trovare le librerie equivalenti sulla nuova piattaforma e collegarle al file precedente compilato, e in casi estremi occorre anche modificare il codice sorgente se per caso abbiamo usato istruzioni relativa ad una certa architettura. In java esiste una speciale macchina virtuale che ci garantisce l’indipendenza dalla piattaforma di esecuzione del programma:

E se invece di un oggetto, lavoro con un tipo di dato primitivi (int,char,boolean)? I tipi di dato primitivi ( in gergo detti tipi atomici ) vengono trattati come nel linguaggio C: cioè vengono allocati nell’area stack, e vengono allocati i valori, non i loro riferimenti. In java è possibile anche definire i tipi atomici come se fossero degli oggetti:cioè definire una “variabile” come Integer valore, invece di int valore; ciò perchè java possiede le classi in gergo dette wrapper ( che sono appunto Integer,Char,Boolean). Ovviamente essendo degli oggetti, il riferimento viene allocato in stack, l’oggetto allocato nell’area heap.Le classi wrapper incapsulano al loro interno un tipo atomico,cioe una classe Integre incapsula il tipo int. Essendo delle classi quindi, essi avranno dei metodi interni utilizzabili dall’oggetto che la classe istanzia.In realtà, le classi wrapper contengono anche dei metodi prettamente statici come ad esempio la conversione stringa/numero, e che operano direttamente sul tipo primitivo che è possibile passare al costruttore in fase di allocazione di un oggetto di classe wrapper.Questi metodo statici pero, lavorano solo sui tipi primitivi, le relative versioni non statiche lavorano sull’oggetto che noi abbiamo dichiarato come classe wrapper.

Dopo aver allocato,è necessario deallocare? Come avviene la deallocazione? In java la deallocazione avviene automaticamente grazie al garbage collector; esso è proprio un thread della Java Virtual Machine, che si avvia parallelamente all’avvio di un programma e il suo compito è proprio quello di esaminare tutti gli oggetti creati con new, e determinare quello ai quali non si fa più riferimento (eliminando cosi dalla mente del programmatore, il pensiero di dover deallocare gli oggetti non più referenziati per liberare spazio in memoria).Ovviamente il garbage collector segue particolari algoritmi per eseguire il suo compito. Nota: Non bisogna sempre fare affidamento sul garbage collector per eliminare oggetti non più referenziati, in quanto non sappiamo quando esso entrerà realmente in azione, e potrebbe anche non entrare mai in azione. Quindi bisogna contare sul garbage collector solo per il recupero di spazio in memoria.

E come vengono gestite invece le stringhe? In java è possibile gestire le stringhe attraverso delle classi. Quella più usata è la classe String, e una stringa si dichiara con la seguente sintassi: String stringa; Ovviamente in memoria la dichiarazione di una stringa produce lo stesso effetto di dichiarazione di un oggetto: cioe String str = new String();, nello stack va a finire il riferimento str all’oggetto di tipo String che va allocato nell’heap. Per comodità, java fa sembrare un oggetto di tipo stringa come un tipo atomico ( quando invece non lo è ed è un oggetto). Ed essendo un oggetto, incapsula nella sua implementazione dei metodi per la sua gestione, come la concatenazione, la verifica della lunghezza di una stringa, la comparazione con un'altra stringa. Da segnalare una cosa molto importante: la classe String rappresenta una classe non modificabile , cioè non si modifica direttamente il valore (ma si agisce attraverso il riferimento), mentre un altro esempio di classe per la gestione delle stringhe, StringBuffer , rende modificabile la stringa. Prima di descrivere la differenza, di particolare interesse, è osservare come viene gestita la concatenazione; essa può avvenire in due modi:

  • per mezzo dell’operatore + ( cioè facendo String str=”Ciao”; e poi str=str+” Mondo” avremmo come risultato che la stringa Mondo è stata concatenata alla stringa Ciao.);
  • per mezzo di un metodo ben definito della classe String chiamato concat(); (Es: String str=”Ciao”, str.concat(“ Mondo”); ). Nota1: Se scrivo String str = “pippo” e poi str =”pluto”, non modifico direttamente la stringa str ma si crea un nuovo oggetto di tipo String e ovviamente si sposta il riferimento a questo nuovo oggetto. Nota2: Assai differente è invece se uso la classe StringBuffer: infatti se dichiaro una oggetto di tipo StringBuffer str = “pippo” e poi faccio str.append(“pluto”); ( dove append() è un metodo della classe StringBuffer che serve appunto per concatenare) in questo caso avviene direttamente la modifica sull’oggetto referenziato da str; Nota1 e Nota2 sono state scritte per dire semplicemente che la classe String rappresenta stringhe non modificabili, la classe StringBuffer invece si.

E come vengono trattati gli array in java? In java gli array vengono dichiarati come in un linguaggio procedurale, ma sono pur sempre degli oggetti ( e per quanto tali, sono una collezione di riferimenti) e non si necessita di una classe Array per definirli: infatti posso liberamente dichiarare array di interi, di char, di oggetti di un certo tipo. Essendo comunque degli oggetti, essi godono comunque della peculiarità dell’information hiding in quanto si nasconde la loro implementazione interna mostrando solo l’interfaccia, il che può proteggere l’accesso a celle non lecite dell’array. Ciò è garantito dal fatto che in java l’array non è struttura di memorizzazione sequenziale (come in C), ma una struttura complessa ( infatti ricordiamo che è comunque un oggetto). Ovviamente come tutti gli oggetti, anche gli array oltre ad essere dichiarati, devono essere anche allocati ed inizializzati.

Ma come vengono passati i parametri ai metodi invocati in java? Per valore o per riferimento? Se il parametro in questione è definito come un tipo atomico, allora esso viene passato direttamente per valore, effettuando una copia del suo valore nella variabile definita nei parametri del metodo a cui lo stiamo passando; Se il parametro in questione è un oggetto invece, allora avviene la copia del riferimento all’oggetto all’interno del metodo, che in ogni caso corrisponde ad aver passato un valore, quindi in java “il passaggio dei parametri non avviene per riferimento ma per valore”.

Come vengono implementati in java concetti come:Information Hiding, Ereditarietà? Grazie a quelli che in gergo si chiamano Specificatori è possibile applicare quei concetti con l’utilizzo di queste parole chiavi:

  • private – rende non visibile un dato al di fuori della classe di appartenenza (information hiding);
  • protected – rende possibile l’accesso al dato, solo alle classi ereditarie (ereditarietà);
  • extends - una classe eredita le caratteristiche dalla classe che segue questo specificatore;

Ma allora,se incapsuliamo attributi di una classe (quindi rendendoli private) non esiste un modo per modificare direttamente i parametri di una classe, al di fuori della classe stessa? Si, implementando dei metodi pubblici nella classe stessa dove sono presenti gli attributi privati: infatti solo quei metodi della classe potranno modificare gli attributi, ed essendo pubblici è possibile richiamarli al di fuori del contesto della classe (e cioè dopo averla istanziata). Quei metodi sono in gergo detti Accessori/Mutatori : metodi set( che definiscono una sorta di accesso in lettura/scrittura) per impostare gli attributi, metodi get( che definiscono una sorta di accesso in sola lettura) per ottenerli.

Un particolare metodo presente in ogni classe che inizializza gli attributi della classe è detto Costruttore. Esso servirà al di fuori del contesto della classe e precisamente, dovendo inizializzare gli attributi della classe, esso sarà richiamato in fase di allocazione dell’oggetto,e per tale motivo è di norma dichiarato come public. Esso è particolare, perchè adotta lo stesso nome della classe, e il compilatore lo individuerà per tale motivo. Es: Oggetto obj = new Oggetto (); Oggetto(); è un metodo definito nella classe Oggetto, ed associandolo a new capiamo che è un costruttore. Nota1 :Se si dovesse omettere il costruttore, il compilatore ne creerà uno di default.

Ma cosa succede se rendo un costruttore privato invece che pubblico? Innanzitutto facciamo questa premessa: può sembrare un paradosso implementare un costruttore privato, in quanto quando andremo ad utilizzare l’istruzione che lo richiama al di fuori della classe (Oggetto obj = new Oggetto (); ) il compilatore ci dirà che non è possibile richiamarlo in quanto è un metodo non visibile. Possiamo pero trovare una soluzione: sfruttare lo static context e lo specificatore Static.

Nota1 : il parametro String[] args serve per passare argomenti al main da riga di comando. Nota2: le variabili con il prefisso static vengono dette attributi della classe, quelle senza invece attributi di istanza. Ovviamente allo stesso modo ci sono i metodi di classe, e i metodi di istanza.

Ma dichiarando cosi un attributo, mi nego la possibilità di dichiararmi una costante all’interno della classe, visto che devo necessariamente inizializzare un attributo statico?Non si può confondere la dichiarazione di un attributo statico con la dichiarazione della costante? Java ci mette a disposizione lo specificatore final per la dichiarazioni di costanti; infatti a differenza di static , un attributo dichiarato come final non potrà essere modificato dopo la sua definizione, nemmeno dai metodi della classe in cui è definito.

La tecnica dell’Overloading Nell’implementazione dei metodi delle classi, una cosa molto importante sia per stile,sia per manutenibilità, è buona norma scrivere i nomi dei metodi in maniera tale che identifichino ciò che essi fanno; In java è possibile anche dichiarare più metodi con lo stesso nome,facendo variare al più i parametri che gli passiamo (vedi la differenza tra i Costruttori di Default e i Costruttori a cui passo parametri, hanno lo stesso nome,variano nei parametri); un metodo overloaded può variare anche dall’ordine in cui gli si passano i parametri (cioè è possibile implementare metodi public toDo(int a,int b) e public toDo(int b,int a) nella stessa classe, ma ciò è sconsigliato in termini di leggibilità del codice).

Un’altra osservazione importante da fare è questa: ma è possibile distinguere i metodi per i valori che ritornano?Cioè è possibile l’overloading per metodi che ad esempio hanno gli stessi parametri in ingresso, ma che si differenziano per il valore che ritornano? In teoria, java consente questa cosa; cioè il compilatore riesce a distinguere quando si implementa public void toDo(); e public int toDo(); questa cosa però a differenza dell’overloading eseguito attraverso i parametri che passiamo, ha uno svantaggio: siccome in java posso comunque richiamare un metodo che ritorna un valore senza che questo valore sia assegnato ad una variabile ( cioè posso fare sia int x=toDo(); che toDo(); assumendo che toDo(); sia un metodo che ritorni un intero); in virtù di ciò, come si comporta java quando se ad esempio devo effettuare un’operazione del genere: public void toDo(); e public int toDo(); e poi quando mi occorre richiamo il metodo toDo(); , java chi prende in considerazione? Qui viene segnalato un errore di compilazione,perchè non si riesce a distinguere chi dei due azionare. Nota: la tecnica che permette di richiamare un metodo che ritorna un valore, senza che esso sia necessariamente assegnato ad una variabile dello stesso tipo si chiama “chiamata di un metodo per effetto collaterale”, in quanto ci si può trovare di fronte a contesti in cui si è comunque interessati a ciò che succede all’interno del metodo, ma non al valore che essi restituiscono.

I Package I package sono dei particolari contenitori di classi ( o di altri package o di librerie), e sono importanti per ottenere il pieno controllo sullo spazio dei nomi, dove per spazio dei nomi intendiamo i nomi delle classi, dei metodi ecc... Il meccanismo per lo più, riguarda soprattutto lo spazio dei nomi delle classi; infatti in un progetto posso avere due metodi con lo stesso nome e presenti in due classi differenti, ma non posso avere due classi con lo stesso nome in un unico package. Questo problema sorge soprattutto quando vogliamo integrare un progetto gia esistente con delle librerie e magari succede una coincidenza di due classi uguali. Grazie ai package è possibile separale. Per il loro utilizzo poi, si dovranno importare nella classe d’interesse, java ci consente ciò grazie ad import una parola chiave che serve proprio ad importare le classi da altri package, ed utilizzarle nella classe che stiamo ad esempio implementando. Tutto ciò a vantaggio della riusabilità del codice.

Le Eccezioni. Innanzitutto definiamo una grande differenza: le eccezioni non sono degli errori, ma bensì servono per gestire gli errori che si possono verificare. Il termine eccezione stesso indica “un caso eccezionale” che potrebbe far terminare l’esecuzione di un programma.

Infatti esistono errori durante l’esecuzione di un programma che possono verificarsi, ma un’applicazione di qualità, deve poter gestire questi errori, senza terminare la sua esecuzione, anzi mostrando all’utente cosa ha generato errore, e consentendo all’utente di continuare comunque ad utilizzare il programma.Quindi un’eccezione corrisponde alla segnalazione del verificarsi di una condizione anomala. In Java un'eccezione è rappresentata attraverso un oggetto, la cui classe determina il tipo di anomalia che si è verificata, e i cui attributi possono contenere informazioni di dettaglio sull'anomalia. Essendo una classe passiamo a farne un rapido esempio chiarificatore: public class ProvaException extends Exception{

public ProvaException (String msg){ }

public ProvaException (String msg){ super (msg); } } Notiamo subito una cosa: extends. Pensiamo subito all’ereditarietà, infatti è proprio cosi: la classe che gestirà l’eccezione eredita le caratteristiche della classe Exception che contiene metodi per la gestione appunto degli errori, e quindi dotiamo la nostra classe di queste caratteristiche.Quando scriviamo un pezzo di codice, e ci accorgiamo che in un certo punto potrebbe essere generato un errore, è in quel punto che dovremmo istanziare la nostra classe che cattura l’errore nel caso in cui esso realmente si verifichi. public void metodo (String a) throws ProvaException{ if (// !condizione di errore) //tutto ok else throw new ProvaException (“C’è un errore!”); } Notiamo la parola chiave throw , che significa “getta”. Infatti noi gettiamo l’ancora a cui aggrapparci per gestire la situazione di errore; ovviamente è facile interpretare new ProvaException (“C’è un errore!”); che istanzia la classe che abbiamo creato, creando appunto l’oggetto che gestisce l’eccezione, richiamandone il costruttore, e quindi un ovvia allocazione in memoria; notiamo infatti nell’implementazione della classe che abbiamo definito un costruttore che riceve come parametro di ingresso una stringa, che è quella che vogliamo far stampare a video nel caso in cui si verifichi l’errore. Ovviamente l’oggetto costruito avrà caratteristiche come ad esempio gli stessi metodi della classe padre, ereditati, come ad esempio per la restituzione del messaggio stesso. Ciò non basta pero per gestire gli errori con le eccezioni.Infatti non basta creare solo l’oggetto, ma anche che il metodo in cui pensiamo possa avvenire un errore deve essere in qualche modo protetto: infatti dobbiamo mettere quel metodo all’interno di quello che nel gergo si chiama handler. Un handler è il famoso blocco try-catch ( ) { }. try { o.metodo (a); } catch ( ProvaException exc ) { //altre istruzioni che saranno eseguite se quando catturata l’eccezione } Mettendo il metodo probabilmente soggetto ad errore nel blocco di protezione, diciamo: prova (try) ad eseguire metodo(a ) e se succede l’errore previsto , catturalo (catch).E in caso succeda l’errore previsto? Viene appunto catturato e istanziata la classe ProvaException, creando l’oggetto exc. Il blocco dove è presente //altre istruzioni, servirà a far continuare l’esecuzione del programma nel caso in cui succeda l’errore. Ad esempio in quel blocco potremmo richiamare un metodo che ci stampi a video il messaggio usato per istanziare la classe ProvaException (quello che in sostanza abbiamo passato al costruttore). public void metodoPrincipale ( ) { try { metodo ( a ); } catch ( ProvaException exc ) { JOptionPane.showMessageDialog( null , exc.getMessage() ); } }

Le operazioni di I/O

File – classe il cui istanziamento produce un oggetto di tipo File rappresentante il nome di un file, la sua posizione all’interno di una certa directory o meglio il suo percorso detto FilePath. Ma è possibile grazie ad un’entità che chiamiamo stream , aprire un flusso di comunicazione con un oggetto di tipo File, al fine di leggerne i contenuti e di scriverne dei nuovi.

I/O Stream – è l’entità astratta o tecnicamente detto il canale di comunicazione che le librerie I/O di java usano per leggere sequenzialmente (in caso di operazioni di lettura) il flusso di informazioni da una certa sorgente (file,connesione di rete), o per inviare (in caso di operazioni di scrittura) delle informazioni che verranno scritte sequenzialmente. Uno stream gestisce questi flussi di informazioni secondo una politica FIFO. Le classi per l’I/O sono quindi divise in classi di Input ( InputStream o Reader ) e classi di Output( OutputStream o Writer ) le quali dispongono grazie all’ereditarietà dei metodi di read( ) e write( ) che a seconda dei propri scopi possono scrivere un singolo byte o un array di byte nel caso degli stream, caratteri Unicode a 16 bit nel caso dei reader e writer.

InputStream – classe astratta usata per rappresentare quelle classi che incapsulano gli input generati da varie sorgenti (array di byte, String, file) e in generale serve per leggere flussi di byte (file binari, immagini ecc). Contiene i seguenti metodi:

  • public abstract int read() throws IOException , per leggere singoli byte, che vengono restituiti come interi (da 0 a 255);
  • public int read(byte[] buf, int offset, int count) throws IOException , per leggere una sequenza di byte memorizzandola in una array;

FileInputStream – classe derivata da InputStream usata per leggere informazioni da un file. Il costruttore prende una Stringa che è il nome del file, oppure un oggetto File. Si connette ad un FilterInputStream per fornirla di un’interfaccia più efficace per la manipolazione dei dati.

FilterInputSteram – Classe astratta che è un’interfaccia per i decoratori che forniscono funzionalità più evolute per la gestione dei file.

OutputStream – classe astratta che fornisce un’astrazione per scrivere i byte in una certa destinazione, che può essere di varie tipologie ma sempre in formatobyte (array di byte, file binari); Fornisce i seguenti metodi:

  • public abstract void write(int b) throws IOException , scrive b sotto forma di byte;
  • public void write(byte[] buf, int offset, int count) throws IOException, scrive una parte di array di byte, a partire da buff[offset] fino a un numero pari a count.

FileOutputStream – classe la cui funzione è quella di inviare i dati su file. Il costruttore prende in input un tipo Stringa che è il nome del file, oppure un oggetto File si connette ad un FilterOutputStream per fornirla di un’interfaccia più efficace per la manipolazione dei dati.

FilterOutputStream – classe astratta che costituisce un’interfaccia per fornire funzionalità aggiuntive ad altre classi OutputStream;

Reader e Writer Essi non sono sostituti delle classi Input e Output Stream. La differenza sostanziale è che Reader e Writer lavorano su caratteri Unicode a 16 bit.

Reader – classe astratta che fornisce i metodi per la lettura dei caratteri, metodo del tipo:

  • public int read() throws IOException, che legge un singolo carattere e lo restituisce sotto forma di intero (tra 0 e 65535).
  • public abstract int read(char[] buf, int offset, int count) throws IOException, che legge una sequenza di caratteri e la memorizza in un array di char.

FileReader - Classe derivata di InputStreamReader di utilità per leggere file di caratteri. Il costruttore prende in ingresso una stringa con nome del percorso o un oggetto di tipo File, ed apre il flusso di comunicazione con il file.