Documente online.
Zona de administrare documente. Fisierele tale
Am uitat parola x Creaza cont nou
 HomeExploreaza
upload
Upload




Corso di Java

Italiana


Corso di Java

https://www.softwareplanet.net

Introduzione all’Object Oriented Programming
Lezione 1

Cosa è Java (what)

Eccoci all'inizio di questo corso su Java, uno dei linguaggi di programmazione più discusso degli ultimi tempi. Il mondo di Java è apparso sin dagli inizi, nell'ormai lontano 1995, un mondo completamente nuovo: permetteva di essere eseguito su una pagina del World Wide Web e prometteva di scavalcare i confini posti da sistemi operativi e architetture differenti.
Sin dalla nascita Java ha destato la curiosità di tutti, programmatori per primi, provocando ondate di entusiasmo, da un lato, e di scetticismo dall'altro.
In ogni caso, negli anni successivi, Java ha tenuto fede alle promesse ed è ormai una realtà indiscutibilmente affermata.
Quello che faremo in questo corso è scoprire, direttamente sulla nostra pelle, le cause e le ragioni di tanto clamore e, soprattutto, del successo di Java.



Prima di affrontare direttamente lo studio di questo linguaggio, diamo un'occhiata ai concetti che ne stanno alla base, e poiché Java è espressamente un linguaggio orientato agli oggetti, vediamo di capire cosa significa "programmare per oggetti".
L'Object Oriented Programming (Programmazione Orientata agli Oggetti), in breve OOP, è una tecnica di programmazione sperimentata a partire dagli anni '70 coi linguaggi Simula67, SmallTalk, Lisp fino agli attuali C++ (dal quale Java eredita gran parte della sintassi), Delphi e Visual Basic.
Essa rappresenta una delle idee più interessanti introdotte ultimamente nel campo della programmazione, in grado di risolvere i problemi di complessità e ingovernabilità che si presentavano nei progetti di grandi dimensioni.

A differenza della programmazione "control-flow" (per intenderci, quella classica di C e Pascal), in cui un programma viene inteso come una sequenza di azioni, nell'OOP un programma è considerato come un insieme di oggetti che lavorano assieme in un modo prestabilito allo scopo di perseguire un determinato obiettivo.
Avete presente il LEGO?
Potete pensare agli oggetti di un linguaggio di programmazione come ai mattoncini del LEGO: piccoli pezzi di varia forma e colore 21121g69v che possono essere combinati tra loro per creare forme più grandi e più complesse.
Oppure pensate al motore della vostra auto (non importa che sia una 500 o una Ferrari Testarossa): perché tutto funzioni è necessario che ogni pezzo (oggetto) stia al suo posto, e non è inoltre possibile utilizzare un carburatore della 500 per la Ferrari (nemmeno il viceversa, in realtà).

Questo esempio ci mostra una condizione importante: sebbene un carburatore è tale sia che si tratti di quello di una 500, sia che si tratti di quello della Ferrari, è importante che esso si integri col resto del sistema, altrimenti non andremo da nessuna parte.
Dunque possiamo definire oggetto un elemento autonomo contenuto in un programma, dotato di proprie qualità (che chiameremo attributi) e di un proprio comportamento (attraverso i metodi).
Il resto del sistema, motore o programma che sia, deve solo sapere come interagire con quell'oggetto, ossia conoscerne il comportamento esterno, senza doversi preoccupare di come esso è fatto o di come funziona in dettaglio.
Di un carburatore mi interessa sapere solamente che, agendo su una particolare leva in un modo determinato, essa determinerà un aumento dell'afflusso di carburante al motore, e non mi importa sapere in che modo ciò viene realizzato al suo interno.

In questo modo posso anche in seguito modificare internamente un oggetto, ad es. per aumentarne le prestazioni o l'affidabilità, mantenendo inalterata la sua interfaccia di interazione con il sistema.
Non solo: se sono riuscito a costruire un oggetto con prestazioni straordinarie, posso riutilizzarlo in un altro sistema analogo, con la sicurezza che esso funzionerà esattamente come io mi aspetto.
Un vantaggio evidente della programmazione ad oggetti è che posso costruire componenti riutilizzabili, purché definisca in maniera chiara e precisa il suo comportamento esterno.
Per ottenere il massimo dall'OOP occorre progettare in maniera adeguata gli oggetti che si vogliono utilizzare, avendo cura di rispettare poche ma importanti regole.
Presto vedremo in che modo è possibile creare oggetti con Java e quali regole occorre tenere presenti nel farlo.
Nella prossima puntata conosceremo più da vicino questo favoloso linguaggio.

Introduzione a Java: caratteristiche fondamentali
Lezione 2

Java è un linguaggio di programmazione estremamente semplice e potente, che racchiude in sé tutti i vantaggi della programmazione orientata agli oggetti, assieme alla possibilità di creare facilmente applicazioni che funzionano con Intenet.
Con esso è infatti possibile scrivere due tipi di applicazioni: applet e programmi stand-alone.
Le applet sono una delle novità più interessanti introdotte da Java; la loro caratteristica peculiare è quella di poter essere eseguite all'interno di un browser web abilitato, in un punto qualsiasi di una pagina html.
I programmi stand-alone sono quelli che conosciamo comunemente e che non necessitano di un ambiente software esterno per poter girare.
La principale promessa di Java è l'indipendenza dalla piattaforma, ossia la capacità dello stesso programma di essere eseguito su piattaforme e sistemi operativi differenti, cosa del tutto nuova rispetto ai tradizionali linguaggi di programmazione.

Con questi ultimi, l'operazione di compilazione di un file sorgente porta ad un file binario specifico per la macchina sulla quale il codice sorgente è stato compilato (fig. 1).

Compilare un file sorgente Java significa generare un formato speciale, detto bytecode, che potrà essere interpretato da un apposito interprete della piattaforma utilizzata (fig. 2).



Il bytecode è simile al codice macchina prodotto da altri linguaggi, ma non è specifico per alcun processore, per cui, una volta compilato il programma, il file ottenuto (avente estensione .class) potrà essere spostato da una piattaforma all'altra senza dover essere ricompilato.
In questo modo si aggiunge un livello tra sorgente e codice macchina.

L'interprete di bytecode è noto col nome di Java Virtual Machine; per un'applet essa è fornita da un browser abilitato per Java, mentre per un'applicazione stand-alone occorre che ne sia installata una sul sistema che dovrà farla girare.
Ciò costituisce un enorme vantaggio per noi programmatori, che non dovremo preoccuparci di creare versioni differenti dei nostri programmi per ogni piattaforma :-)).
Tuttavia i piaceri si pagano, e questa interessante proprietà ha un costo: questo ulteriore livello intermedio introduce un ritardo che, soprattutto per grosse applicazioni, può risultare perlomeno sgradevole.
Infatti trattandosi di un linguaggio interpretato, Java necessita di più tempo per eseguire le proprie operazioni.

Diverse sono le soluzioni pensate per risolvere questo problema, tra le quali la realizzazione di un processore capace di eseguire direttamente il bytecode (una virtual machine tutt'altro che virtuale), vanificando però la tanto sbandierata indipendenza dalla piattaforma.
Java eredita dal C++ gran parte della sintassi e della struttura OOP, ma Sun Microsystems (l'azienda che ha sviluppato Java) ha ben pensato di escludere alcune caratteristiche ambigue del buon vecchio C++, cosicché chi già lo conosce e lavora con esso da tempo, sebbene non troverà difficoltà ad imparare a programmare in Java, tuttavia potrà trovarsi spiazzato dall'assenza di alcune funzionalità del vecchio linguaggio, come i puntatori e l'ereditarietà multipla (ne parleremo meglio nelle prossime lezioni).
Particolare attenzione è stata posta dai progettisti di Java al problema della sicurezza, così tanto sentito nell'era di Internet e dell'e-commerce, soprattutto nelle applet.

Queste ultime, infatti, hanno delle limitazioni intrinseche, appositamente volute per assicurare che nessun programmatore malizioso possa creare applet che eseguano operazioni dannose o indiscrete a danno dei client che aprano una determinata pagina web.
In particolare, un'applet non può:
Leggere o scrivere file sul sistema dell'utente (di conseguenza nemmeno eseguire programmi su di esso o prelevarne informazioni di alcun tipo)
Comunicare con un sito Internet diverso da quello su cui viene distribuita la pagina web che include l'applet
Nonostante gli sforzi compiuti da Sun in questo senso, non si può essere sicuri al 100%, e su Internet non è difficile trovare informazioni sulle cosiddette "hostile applets".

Ora che conosciamo Java più da vicino, passiamo ad esaminare lo strumento di sviluppo distribuito gratuitamente da Sun, il Java Development Kit.

Presentazione di Sun JDK
Lezione 3

Come per qualsiasi altro linguaggio di programmazione, per lavorare con Java occorre disporre di uno strumento di sviluppo appropriato.
Diversi sono i prodotti commerciali attualmente diffusi per questo scopo: Sun Java Workshop, Symantec Visual Café, Inprise JBuilder, Microsoft Visual J++ e tanti altri, meno complessi ma altrettanto efficienti.
Il pregio principale di questi ambienti è sicuramente la possibilità di progettare interfacce utente direttamente "disegnandole" sullo schermo, secondo una prassi di programmazione visuale intuitiva, pratica e veloce; d'altro canto, però, il loro utilizzo richiede una spesa non indifferente per l'acquisto.
Per il nostro corso, faremo riferimento ad un tool di sviluppo distribuito gratuitamente da Sun Microsystems (https://java.sun.com) e spesso incluso nei CD di varie riviste d'informatica.

Sun Java Development Kit (JDK) non è un ambiente di sviluppo vero e proprio, si tratta piuttosto di una serie di programmi per la compilazione, il debug e l'esecuzione di programmi scritti in Java; la versione del JDK cui ci riferiremo è la 1.2.2 (denominata Java 2 Source Development Kit), ma per gran parte del corso è sufficiente possedere una qualsiasi versione a partire dalla 1.1.4.
Da adesso in poi presupporremo di disporre già di una versione di JDK installata (per la versione Windows basta lanciare l'applicativo, come per qualsiasi altro programma).
Per prima cosa apriamo la directory in cui JDK è installato e diamo un'occhiata alla subdirectory bin.
Essa contiene una serie di programmi eseguibili: se provate a cliccare su uno di essi otterrete solo messaggi di errore, o tutt'al più vedrete una finestra DOS aprirsi e subito richiudersi! Cosa è successo?
Semplicemente sono programmi che funzionano solo da linea di comando; qui emerge la principale differenza tra JDK e gli altri ambienti di sviluppo commerciali: non include un'editor per il codice sorgente.

Non vi scandalizzate per questo: Sun ha deciso di fornire solo l'essenziale, ma JDK è completo e con esso è possibile fare tutto quello che serve per programmare in Java.
Comunque l'assenza di editor non è un handicap per noi, anche se gli utenti Linux hanno un vantaggio in più.
Infatti gli editor più semplici per Windows (Notepad o edit del DOS) non hanno la possibilità di evidenziare le parole chiave del codice sorgente, come invece fanno emacs o vi per Linux.
Ciò non costituisce un grosso problema per piccoli programmi, ma via via che si creano applicativi più corposi, la leggibilità del codice ne risente.
Creiamo da qualche parte nel nostro hard disk una directory dedicata a questo corso.
A questo punto copiate il piccolo programma Java riportato di seguito, utilizzando un editor a vostra scelta (non preoccupatevi se al momento non ci capite nulla, sarà tutto spiegato e chiarito in seguito):

class Ciao
}

Perché la compilazione sia possibile è necessario salvare il file col nome Ciao.java (se utilizzate un programma Windows mettete il nome del file tra doppi apici, altrimenti ad esso verrà aggiunta l'estensione di default dell'editor) nella directory che abbiamo prima creato.
Sin da adesso è consigliabile non utilizzare le operazioni Copia&Incolla per copiare il codice di esempio: gli errori di sintassi che inevitabilmente si compiono all'inizio, anche se irritanti, sono necessari per acquisire una maggiore padronanza del linguaggio!
Se stiamo lavorando in ambiente Windows apriamo una finestra DOS, accediamo alla directory del corso e digitiamo il seguente comando:

javac Ciao.java

Se stiamo lavorando con Linux da X Window, apriamo una consolle virtuale e facciamo altrettanto.
Se non avete prima consultato la documentazione del JDK e seguito le istruzioni in essa riportate otterrete sicuramente un errore; se lo avete già fatto e la compilazione ha avuto esito positivo potete listare il contenuto della directory e vedrete che è spuntato un file col nome Ciao.class.

Per vedere il risultato del programma di esempio digitate il comando:

java Ciao

Come avrete ben capito javac è il compilatore dei sorgenti e java l'interprete di bytecode.
Chi non ha ottenuto segnalazioni di errore nella fase precedente, può saltare quest'ultima parte della lezione e attendere con impazienza la prossima; per chi, invece, non è riuscito a compilare il codice sorgente sarà utile pazientare ancora un .
Se il messaggio di errore è:

Comando o nome file non valido

occorre aprire il file c:\autoexec.bat e inserire la riga:

SET PATH=%PATH%;C:\<JDKDIR>\bin

dove <JDKDIR> è la directory in cui JDK è stato installato.
Anche gli utenti Linux dovranno includere il percorso di installazione di JDK nella variabile d'ambiente PATH.
Salvate il file, riavviate il sistema operativo e riprovate a compilare.
Buona fortuna!

Oggetti e classi
Lezione 4

Entriamo nel vivo della programmazione cominciando a vedere in che modo è possibile lavorare con gli oggetti di Java e come implementarli tramite le classi.
Il concetto di classe non ci è sicuramente nuovo, ma nella vita reale questa parola ha un significato un sfumato, non ben definito (oggi va di moda il termine fuzzy): d'ora in poi vi sarà perdonato di non sapere il significato di classe sociale o di lotta di classe, ma quando parleremo di classi Java, non avrete altra scelta che essere preparati!
Infatti la classe è l'elemento base dei programmi scritti in Java, anzi, un programma Java non è altro che una collezione di classi.
Per nostra fortuna, il concetto di classe in Java è semplice e chiaro.
Se dovessimo darne una definizione, diremmo che:

una classe è un modello per diversi oggetti con caratteristiche simili

Facciamo un esempio che può chiarirci meglio le idee.

Quando utilizziamo la parola "uomo", sappiamo che indichiamo un essere vivente appartenente alla classe (!) dei mammiferi, dotato di intelligenza e avente caratteristiche fisiche ben delineate: una testa, un busto, due braccia, due gambe.
Ma se diciamo "uomo", non ci riferiamo a nessun uomo in particolare, bensì ad un modello, che riassume in sé tutte le caratteristiche per le quali un essere vivente può classificarsi come uomo.
Quando definiamo una classe Java, in realtà definiamo un modello per tutti gli oggetti appartenenti a quella classe; una volta definita una classe è possibile creare un numero illimitato di oggetti per essa.
Ogni oggetto appartenente ad una classe è detto istanza di quella classe: Bill Gates è un'istanza della classe "uomo", Linus Torvalds è un'altra istanza della stessa classe.
Lassie e Rin Tin Tin sono due istanze della classe "cane" ed hanno una cosa in comune con i nostri amici citati prima (non anticipate le mie conclusioni!): appartengono alla classe "esseri viventi".
Dunque il concetto di classe ha una sua relatività, che dipende dall'utilizzo che vogliamo fare degli oggetti.

Se volessimo confrontare esseri viventi e materia inanimata, potremmo includere Bill Gates, Linus Torvalds, Lassie e Rin Tin Tin nella stessa classe, ma se volessimo distinguere tra animali a quattro zampe e bipedi non potremmo più farlo.
Dunque, prima di iniziare a scrivere la prima riga di codice, il programmatore Java deve decidere quali classi appartengono al progetto che sta realizzando.
Questa prima fase è la più delicata di tutto il lavoro, ma è indispensabile per una buona riuscita; infatti, se le classi sono progettate in maniera chiara e coerente, tutto il lavoro successivo sarà più semplice da realizzare, ci saranno meno errori e si potrà generare codice riutilizzabile.
In Java la definizione di classe ha la seguente sintassi:

class MiaPrimaClasse

Con questa istruzione abbiamo, di fatto, già creato una classe: provate a copiare questa riga in un file, salvatelo col nome MiaPrimaClasse.java e compilatelo: otterrete il file MiaPrimaClasse.class e nessun messaggio di errore.

Come avrete notato, per definire una classe occorre creare un file con lo stesso nome e con estensione .java.
Fate attenzione alle lettere maiuscole e minuscole: Java è case-sensitive, per cui considera MiaPrimaClasse diversa da Miaprimaclasse, miaprimaClasse, miaprimaclasse...
Pur avendo creato una classe Java, essa non fa nulla e non ha alcuna caratteristica che la possa distinguere, ad es., dalla classe:

class MiaSecondaClasse

perché non ha né qualità, né comportamenti che la identificano.
Se vogliamo caratterizzare la nostra classe dovremo definire quali sono i suoi attributi (ossia le sua qualità) e i suoi metodi (ossia il suo comportamento).
Sappiamo bene che la classe "uomo" è caratterizzata da svariate qualità (sesso, colore della pelle, colore degli occhi, colore dei capelli, altezza, peso, etc.) e da una serie di azioni che può compiere (mangiare, camminare, correre, dormire, tifare per una squadra, insultare l'arbitro, etc.).
Analogamente possiamo definire nella nostra classe le caratteristiche e le azioni comuni a tutti gli oggetti ad essa appartenenti.
Nella prossima puntata vedremo come farlo.

Variabili e attributi I
Lezione 5

A partire da questa lezione cominceremo a costruire la nostra prima applicazione.
Supponiamo di dover lavorare con una classe "Automobile", della quale abbiamo rilevato le seguenti qualità:
- marca
- modello
- capacità del serbatoio
- quantità di benzina nel serbatoio
- livello della riserva
- consumo (numero di Km con un litro)
- presenza dell'autoradio
- presenza dell'antifurto
Ovviamente queste non sono le uniche qualità di un'automobile, anzi, non sono nemmeno le prime che ci verrebbero in mente se ci venisse chiesto di elencarne alcune.
La scelta degli attributi è un passo importante nella progettazione di classi Java e va fatta tenendo in considerazione l'utilizzo cui è destinata la classe; nel nostro caso creeremo un'applicazione nella quale non useremo attributi come il colore, la velocità massima, l'accelerazione, etc., per cui ci limiteremo (per ora) a considerare solo queste.

Ognuna di queste qualità può essere espressa con una variabile, ossia con un elemento in cui è possibile memorizzare informazioni di vario tipo durante l'esecuzione del programma.
Il contenuto di una variabile può essere modificato (ciò giustifica, appunto, il nome variabile) all'interno della sua regione di visibilità, detta scope.
Lo scope di una variabile è quella porzione di codice nella quale essa è raggiungibile, ossia può essere letta ed eventualmente modificata.
Questo concetto ci potrà essere più chiaro dopo aver fatto una serie di premesse.
In Java una variabile può essere:
variabile istanza, utilizzata per definire gli attributi di un oggetto;
variabile di classe, utilizzata per definire attributi di un'intera classe di oggetti;
variabile locale, utilizzata all'interno di metodi e blocchi di istruzioni.
Ogni variabile è caratterizzata da un nome e da un tipo, e il suo valore iniziale può essere assegnato nel momento stesso in cui viene creata.

Scriviamo il corpo della nostra classe:

class Automobile

E' possibile assegnare un valore ad una variabile tramite un segno di uguale (=) interposto tra il nome della variabile (a sinistra) e il valore da assegnare (a destra), ad es:

marca = "Ferrari";
consumo = 5;
autoradio = true;

Quando si definisce una variabile istanza o di classe, viene assegnato un valore iniziale di default a seconda del tipo di informazione in essa contenuta: 0 per le variabili numeriche; \0 per i caratteri; false per i booleani e null per gli oggetti.
All'interno di una classe è possibile riferirsi a tutti i suoi attributi, ossia a tutte le sue variabili istanza, mentre le variabili locali sono accessibili solamente all'interno del blocco in cui sono definite.
Un blocco è una porzione di istruzioni Java racchiuse tra parentesi graffe.

Supponiamo che da qualche parte, all'interno della nostra classe ci sia un blocco di questo tipo:


    int j = i;
}

...

A parte la banalità del codice, possiamo notare che abbiamo inserito un blocco all'interno di un altro blocco più grande, e che dentro il blocco interno sono state dichiarate quattro variabili, per ognuna delle quali è stata fatta contestualmente un'operazione di assegnamento.
Le variabili i, k, f1 e f2 sono variabili locali al blocco interno e sono visibili solo all'interno di esso.
j e f3 sono locali al blocco esterno, per cui non sono visibili al di fuori di esso, ma è possibile utilizzarli nei blocchi interni: ciò rende possibile l'assegnamento fatto ad f2.

Poiché le variabili istanza (gli attributi) di una classe sono visibili in tutto il corpo della classe, è possibile assegnare quantitaBenzina ad f1 e livelloRiserva ad f3.
Il codice di esempio contiene due errori, che è importante rilevare:
Sebbene j sia dichiarato nel blocco esterno, essa non è accessibile in quello interno, perché quando questo viene eseguito, essa non è stata ancora dichiarata (cioè non esiste), per cui l'assegnamento fatto a k non è possibile;
Anche l'assegnamento fatto a j è un errore che viene rilevato in fase di compilazione, perché al termine del blocco interno, i non è più visibile, anzi possiamo dire che in tal punto i non esiste più.
Ciò vuol dire che lo scope di una variabile ha inizio nel punto in cui essa è dichiarata e ha termine alla fine del blocco che contiene la sua dichiarazione.
Notiamo che poiché tutto il corpo di ogni classe è racchiuso entro parentesi graffe, anche le variabili istanza rientrano in questa definizione (il blocco che contiene la dichiarazione è la classe e quindi ogni blocco che la classe contiene può accedere a tali variabili).
Approfondiremo i modi con cui è possibile utilizzare le variabili nella prossima puntata.

Variabili e attributi II
Lezione 6

Rivedendo la dichiarazione della nostra classe "Automobile", possiamo notare che:

  • metodi e attributi di una classe vanno inseriti all'interno delle parentesi graffe;
  • la dichiarazione di una variabile è costituita dal tipo di dato che la variabile memorizza e dal nome della variabile;
  • ogni dichiarazione termina con un punto e virgola;

è possibile dichiarare più variabili dello stesso tipo scrivendo prima il tipo e poi i nomi delle variabili, separati da virgole;

Per i nomi delle variabili, occorre ricordare che:

  • devono iniziare con una lettera, un trattino di sottolineatura ( _ ), o un segno di dollaro ($);
  • non possono iniziare con un numero;
  • dopo il primo carattere è consentita qualsiasi combinazione (di qualsiasi lunghezza) di lettere e numeri, purché non sia una parola chiave di Java;
  • è possibile utilizzare i caratteri accentati (è comunque preferibile evitarli).

Per quanto riguarda i nomi delle variabili esistono, oltre che i limiti e gli obblighi del linguaggio, una serie di convenzioni, proposte dalla stessa Sun Microsystems, per semplificare e uniformare la stesura di programmi Java da parte di diversi sviluppatori.
Discuteremo in seguito dell'importanza di seguire queste convenzioni; per ora rileviamo solo che non si tratta di specifiche vere e proprie del linguaggio, ma di una serie di accorgimenti da adottare (se si vuole) quando si scrive un programma in Java.
Poiché il programmatore è un animale scorbutico e abitudinario, fortemente legato al proprio modo di scrivere, esso potrà trovare irritante questi ulteriori vincoli e decidere di non attenervisi, la qual cosa non pregiudica la qualità del codice scritto.
Per chi comincia adesso a programmare è utile seguire questi consigli, che possono essere utili anche per altri linguaggi al di là di Java, e che comunque sono dettati dal buon senso e da anni di esperienze nel campo della programmazione.
Per tutto il corso, dove possibile, cercheremo di adottare queste convenzioni; per i nostri progetti personali ognuno potrà adottate il criterio che più ritiene opportuno.

Per nostra fortuna le convenzioni per i nomi delle variabili non sono nulla di limitante o di clamorosamente nuovo.
Eccole:

  1. Tutte le variabili sono scritte in lettere sia maiuscole che minuscole con la prima lettera minuscola e le parole interne iniziano con una lettera maiuscola.
  2. I nomi di variabile non devono iniziare con caratteri di underscore _ o di segno di dollaro $, sebbene siano entrambi permessi.
  3. I nomi di variabile dovrebbero essere corti ma significativi.
  4. La scelta di un nome di variabile deve essere mnemonica - cioè pensata per indicare ad un casuale osservatore l'utilizzo che ne viene fatto.
  5. I nomi di variabile di un solo carattere dovrebbero essere evitati, eccetto per variabili di comodo. Nomi comuni per variabili temporanee sono i, j, k, m, ed n per gli interi; c, d, ed e per i caratteri.



Esempi di variabili che seguono queste convenzioni sono:

float quantitaBenzina;
char c;

Per queste ed altre convenzioni, possiamo fare riferimento al documento "Convenzioni per la codifica di programmi Java", distribuito parallelamente a questo corso.
Spesso nei nostri programmi abbiamo necessità di avere dei valori costanti, magari condivisi da certi metodi dell'oggetto.
In Java è possibile specificare una costante, ossia una variabile il cui contenuto non viene modificato durante l'esecuzione, tramite la parola chiave final.
Nel nostro caso, ad es., la capacità massima del serbatoio dell'automobile, il livello della riserva e il consumo non sono valori che variano una volta che sono stati assegnati.
Dovremo allora correggere le definizioni e porre:

final float capacitaSerbatoio, livelloRiserva, consumo;
float quantitaBenzina;

Nella lezione successiva vedremo quali sono i tipi che possiamo utilizzare per le nostre variabili.

Tipi di dati primitivi e casting
Lezione 7

Abbiamo già visto come dichiarare gli attributi di una classe tramite variabili e costanti, ora vedremo di che tipo possono essere.
Un tipo di dato definisce i valori ammessi per una variabile appartenente al tipo e può essere:
- uno degli otto tipi primitivi;
- il nome di una classe o di un'interfaccia;
- un array.
I tipi di dato primitivi comprendono:
- i tipi per i numeri interi e a virgola mobile;
- i caratteri;
- i valori booleani (true e false).
e sono detti primitivi perché sono stati incorporati nel linguaggio Java, anziché essere oggetti effettivi, allo scopo di renderne più efficiente l'impiego.

Per le variabili che possono assumere solo valori interi, esistono in Java 4 tipi, il cui utilizzo dipende più che altro dalla dimensione dell'intero, ossia dal range di valori che può assumere:

TIPO

DIMENSIONE

INTERVALLO DI VALORI

byte

8 bit

da -128 a 127

short

16 bit

da -32768 a 32767

int

32 bit

da -2147483648 a 2147483647

long

64 bit

da -9223372036854775808 a 9223372036854775807


Notiamo che non esistono tipi interi senza segno e che le loro dimensioni e caratteristiche sono indipendenti dalla piattaforma e dal sistema operativo.

Per le variabili che possono assumere valori a virgola mobile esistono 2 tipi:

TIPO

DIMENSIONE

INTERVALLO DI VALORI

float

32 bit

da 1.4E-45 a 3.45E+38

double

64 bit

da 4.9E-324 a 1.7E+308


Oltre a differire per il range di valori che possono assumere, questi due tipi differiscono anche per la precisione (infatti il tipo double ha una precisione maggiore del tipo float) e ovviamente anche per lo spazio di memoria occupato.
Riprendiamo il codice della classe "Automobile" e verifichiamo che la scelta di dichiarare gli attributi capacitaSerbatoio, livelloRiserva, consumo e quantitaBenzina di tipo float è una buona scelta, in quanto questo tipo è sufficiente a rappresentarli.
Il settimo dei tipi fondamentali è il tipo char, utilizzato per singoli caratteri quali lettere, numeri, segni di punteggiatura e altri simboli. Questo tipo di dato ha dimensione di 16 bit e utilizza la codifica dello standard Unicode, che permette di gestire migliaia di caratteri.

Un carattere deve essere racchiuso tra singoli apici e può essere anche un carattere non stampabile o non accessibile da tastiera; questi ultimi possono essere rappresentati per mezzo di codici speciali, elencati nella seguente tabella:

CODICE

SIGNIFICATO

\n

nuova riga

\t

tabulatore

\b

cancella il carattere a sinistra

\r

ritorno a capo

\f

avanzamento pagina

barra inversa

apice

doppio apice


Ad es. è possibile scrivere:

char nuovaRiga = '\n';
char letteraA = 'A';

Infine l'ottavo tipo fondamentale è boolean, che può contenere il valore true (vero) o il valore false (falso); è possibile assegnare:

boolean autoradio = true;
boolean antifurto = false;


Per ognuno di questi tipi di dati esistono delle classi omonime, ma scritte con l'iniziale maiuscola, che hanno funzionalità diverse (quindi non interscambiabili con essi), la cui corrispondenza è mostrata in tabella:

TIPO PRIMITIVO

OGGETTO CORRISPONDENTE

byte

Byte

short

Short

int

Integer

long

Long

float

Float

double

Double

char

Character

boolean

Boolean


E' possibile convertire un tipo primitivo in un altro tramite l'operazione di casting.

Il casting è un'operazione che può essere pensata come avente un'origine (il valore da convertire in un altro tipo) e una destinazione (la variabile cui viene assegnato il valore convertito) ed ha la seguente forma:

variabileDestinazione = (nuovoTipo) valoreOrigine;

Questa operazione è necessaria se vogliamo assegnare un valore ad un tipo più piccolo, ad es. un int ad un byte, perché può portare alla perdita di informazioni, ma possiamo benissimo assegnare un valore int ad una variabile di tipo float, perché ciò non comporta perdita di informazioni.
Ad es.

int x = 5;
double y = x;


è possibile e non richiede alcun casting.

Nel seguente esempio:

int z = (int) (x / y);

a z di tipo int è assegnato il valore del risultato della divisione di x per y.
Il casting non è ammesso per i valori booleani e può essere effettuato anche tra oggetti appartenenti a classi diverse (questo lo vedremo in seguito).
Nella prossima lezione vedremo come dichiarare i metodi per la classe "Automobile".

Metodi e costruttori I
Lezione 8

Dopo aver discusso sui tipi fondamentali con cui poter lavorare in Java, riprendiamo la definizione della nostra classe "Automobile", che adesso ha la seguente struttura:

class Automobile

Se siete stati attenti, noterete che non era questa la forma in cui avevamo lasciato la nostra classe.
Infatti sono comparse due ulteriori parole chiave, public e private, che sono dette modificatori per il controllo di accesso, il cui compito è - appunto - quello di modificare la definizione delle nostre variabili, al fine di controllarne la modalità con cui è possibile utilizzarle.
In pratica viene usato il modificatore public quando si vuole che l'attributo sia visibile all'esterno della classe, mentre il modificatore private è utilizzato per impedire l'accesso a quell'attributo dall'esterno della classe.

Per accedere ad un attributo di un oggetto occorre utilizzare una sintassi del tipo:

nomeOggetto.nomeAttributo

Supponiamo di definire una classe "Proprietario" in cui dichiariamo un oggetto di tipo "Automobile", ad es:

class Proprietario


all'interno di tale classe è possibile utilizzare un'istruzione del tipo:

auto.marca = "Ferrari";

mentre se scrivessimo:

auto.quantitaBenzina

otterremmo un errore in fase di compilazione.
In questo modo è possibile nascondere all'esterno alcuni dettagli, anche se, nei casi pratici, è spesso necessario poter leggere o scrivere queste variabili.
Ad es. nella classe "Proprietario" vorremmo rendere possibile la lettura della quantità di benzina nel serbatoio e dare la possibilità di fare rifornimento.

In entrambi i casi ci occorre poter accedere in qualche modo alla variabile auto.quantitaBenzina.
In questi casi si definiscono dei metodi interni alla classe "Automobile", che rappresentano l'unico modo che hanno le classi esterne per accedere alle variabili private.
Questa caratteristica di Java è molto potente, perché dà la possibilità a noi programmatori di controllare completamente il modo con cui le altre classi interagiscono con la nostra.
Oltre ai due precedenti, esiste un terzo modificatore per il controllo di accesso, protected, che verrà spiegato in seguito. I modificatori possono essere applicati non solo alle variabili, ma anche alle classi e ai loro metodi.
Un metodo Java ha la seguente struttura:

<modificatore di accesso> <tipo ritornato> <nome del metodo> (<elenco parametri>)

Il modificatore di accesso può essere uno di quelli già visti per le variabili: è dunque possibile anche definire dei metodi interni utilizzabili solo in altri metodi della classe.
Il tipo ritornato può essere uno qualsiasi dei tipi fondamentali o un oggetto Java che costituisce la classe del valore restituito dal metodo; se il metodo non restituisce alcun valore si utilizza la parola chiave void.

L'elenco dei parametri è una serie di dichiarazioni di variabili separate da virgola.
Le istruzioni, ossia le operazioni eseguite dal metodo, sono contenute all'interno di una coppia di parentesi graffe.
Supponiamo di voler scrivere per la nostra classe un metodo che ci consenta di leggere la variabile quantitaBenzina, potremmo scrivere:

public float getQuantitaBenzina()

Questo metodo è pubblico, ritorna un valore di tipo float e non ha parametri formali.
Adesso, se nella classe "Proprietario" utilizziamo l'istruzione:

float benzina = auto.getQuantitaBenzina();

il valore ritornato dalla funzione getQuantitaBenzina() verrà assegnato alla variabile benzina.

Ogni metodo dichiarato in modo da restituire un valore di un certo tipo deve restituire esplicitamente tale valore per mezzo dell'istruzione:

return <valore>;

Nel codice di esempio il nostro metodo non fa altro che restituire il valore della variabile quantitaBenzina al momento dell'invocazione del metodo.
Un metodo per fare rifornimento all'automobile può essere il seguente:

public void aggiungiBenzina(float litri)

Anche questo è un metodo pubblico, che non restituisce alcun valore (non ha bisogno dell'istruzione return) e che ha un solo parametro, litri, che indica quanti litri di benzina aggiungere a quella già esistente.

Metodi e costruttori II
Lezione 9

Nell'ultima lezione abbiamo visto alcuni metodi della classe "Automobile" che ci consentono di accedere in maniera controllata ad alcuni attributi privati.
L'accesso ad una variabile può essere in lettura o in scrittura, a seconda che si voglia conoscerne il valore o assegnargliene uno.
Una convenzione indicata da Sun è quella di creare, per una variabile nomeVariabile di tipo tipoVariabile, dei metodi di lettura aventi la seguente struttura:

public tipoVariabile getNomeVariabile()

e metodi di scrittura del tipo:

public void setNomeVariabile(tipoVariabile valore)

Supponiamo di voler definire due metodi, rispettivamente per leggere e scrivere la variabile autoradio della nostra classe "Automobile".
In accordo con tale convenzione, potremo scrivere:

public boolean getAutoradio()

e:

public void setAutoradio(boolean tf)


Analogamente potremmo operare per la variabile antifurto.
Per le costanti non possiamo definire un metodo di scrittura, perché ogni operazione di assegnamento al di fuori del metodo costruttore verrebbe rilevata come errore in fase di compilazione.

Nel nostro caso, aggiungiamo alla classe "Automobile" i seguenti metodi:

public float getCapacitaSerbatoio()
public float getLivelloRiserva()

Ovviamente è possibile definire metodi di lettura e scrittura anche per gli attributi non privati; ad es., potremmo definire un metodo che ci restituisca una stringa contenente la marca e il modello dell'automobile, il cui corpo sarà:

public String getMarcaModello()

In questo metodo abbiamo utilizzato la concatenazione di tre stringhe per ottenerne una; la seconda stringa (costituita solo da uno spazio) è necessaria perché altrimenti concateneremmo le variabili marca e modello attaccando l'una all'altra senza alcuno spazio in mezzo.

L'operazione di concatenazione di stringhe può essere effettuata per mezzo dell'operatore +, il cui risultato è un'unica stringa contenente tutte le stringhe collegate da tale operatore.
Una possibilità interessante offerta da Java è quella di concatenare un tipo primitivo con una stringa.
Se ad es. volessimo utilizzare in una stringa il valore di una variabile booleana, potremmo scrivere:

boolean tf = true;
String s = "Il valore di tf è: " + tf;


In questo esempio la variabile s contiene la stringa "Il valore di tf è: true".
Allo stesso modo potremmo utilizzare nel metodo getMarcaModello() il carattere spazio (' '), invece di una stringa composta da un solo spazio (" ").
Finora sappiamo come definire una classe, come dichiarare i suoi attributi e i suoi metodi, ma non abbiamo ancora visto in che modo possiamo creare oggetti a partire da una data classe.
La dichiarazione di un oggetto appartenente alla classe è semplice, ed è uguale alla dichiarazione di una qualsiasi altra variabile, ad es. con l'istruzione:

Automobile auto;

viene dichiarato un oggetto di tipo Automobile che si chiama auto.

 Tuttavia se subito dopo facessimo un qualsivoglia riferimento ad essa, ad es.:

boolean af = auto.getAntifurto();


ci verrebbe segnalato, in fase di compilazione, un errore di sintassi.
Infatti l'oggetto auto è stato dichiarato ma non esiste ancora, e per poterlo creare occorre utilizzare l'operatore new.
Per creare un oggetto nomeOggetto appartenente alla classe NomeClasse, si usa una istruzione del tipo:

nomeOggetto = new NomeClasse();


Così, per creare il nostro oggetto auto, potremo scrivere:

auto = new Automobile();

Le parentesi tonde dopo il nome della classe sono importanti e non possono essere omesse; il loro contenuto può essere vuoto oppure consistere di una lista di argomenti, il cui numero e tipo sono definiti da ogni classe per mezzo di un metodo speciale, detto costruttore.
Quando si invoca l'operatore new, Java provvede automaticamente a:
Creare una nuova istanza della classe indicata;
Allocare la memoria necessaria a contenere quella istanza;
Richiamare il metodo costruttore della classe.
Nella prossima puntata vedremo come dichiarare i costruttori per le nostre classi.

Metodi e costruttori III
Lezione 10

Abbiamo accennato più volte ad uno speciale metodo che può essere contenuto nelle classi Java, noto col nome di costruttore: oggi vedremo cos'è, a cosa serve e come sfruttarlo al meglio per i nostri scopi.
Innanzitutto occorre sapere che il costruttore è un metodo che viene richiamato automaticamente da Java su un oggetto, quando questo viene creato, e non può essere invocato direttamente come un metodo normale.
Nella scorsa lezione abbiamo visto che l'ultima cosa che fa l'operatore new, quando si crea una nuova istanza di una classe, è proprio quella di richiamare il costruttore della classe; tuttavia non è obbligatorio che ogni classe definisca metodi costruttori: se questi sono assenti, l'istruzione new su quella classe va comunque a buon fine e l'oggetto viene generato.
L'importanza dei metodi costruttori consiste nel fatto che, tramite essi, è possibile impostare i valori iniziali degli attributi, richiamare determinati metodi in funzione di tali valori, richiamare metodi di altri oggetti e impostare le proprietà iniziali dell'oggetto.
Le differenze principali tra costruttori e metodi normali sono due:
i costruttori hanno sempre lo stesso nome della classe;
i costruttori non restituiscono mai un valore.

Per definire un costruttore occorre semplicemente indicare il nome della classe e la lista degli eventuali argomenti; ad es. nel caso della classe "Automobile" potremmo così definirlo:

Automobile(String mrc, String mdl, float capSerb, float livRis, float cns,
           float qBenz, boolean ar, boolean af)


Questo costruttore ha otto argomenti, che rappresentano rispettivamente: la marca dell'automobile, il modello, la capacità del serbatoio, il livello della riserva, il consumo, la quantità di benzina nel serbatoio, la presenza dell'autoradio e la presenza dell'antifurto.

Con questo costruttore, per istanziare un oggetto di tipo Automobile occorre utilizzare la seguente istruzione:

Automobile auto = new Automobile("Fiat", "500", 40, 5, 17, 30, true, false);


Con essa viene creato un oggetto di tipo Automobile di marca Fiat, modello 500, con serbatoio da 40 litri, livello di riserva da 5 litri, in grado di percorrere 17 km con un litro, avente 30 litri di benzina nel serbatoio, dotata di autoradio, ma sprovvista di antifurto.
Notiamo che nel corpo del costruttore vengono effettuati degli assegnamenti agli attributi capacitaSerbatoio, livelloRiserva e consumo, che sono stati dichiarati final, cioè come valori costanti (v. lez. 6): il costruttore è infatti l'unico luogo in cui ciò è possibile; al di fuori di esso ogni assegnamento di questo tipo produrrà, in fase di compilazione, l'errore "Can't assign a second value to a blank final variable" seguito dal nome della costante che si è cercato di modificare.
Non solo: l'inizializzazione di valori costanti è obbligatoria.
Infatti se si omette di inizializzare il valore di una variabile la compilazione del codice sorgente va a buon fine, mentre il compilatore produrrà un errore per ogni variabile final non inizializzata.

Se ad es. dimenticassimo di inizializzare il valore di capacitaSerbatoio, otterremmo l'errore:

Blank final variable 'capacitaSerbatoio' may not have been initialized. It must be assigned a value in an initializer, or in every constructor

In ogni caso, è opportuno che il programmatore si occupi di inizializzare opportunamente TUTTI gli attributi della classe, al fine di evitare comportamenti erronei degli oggetti.
Per ogni classe Java è possibile specificare più di un costruttore, purché ognuno di essi abbia una serie di argomenti diversa per numero e tipo.
In realtà Java permette, più in generale, di definire metodi con lo stesso nome, ma diversi per segnatura.
Per segnatura di un metodo si intende il numero degli argomenti e il tipo di dati o gli oggetti di ciascun argomento.
Ad es. potremmo stabilire dei valori di default per alcune variabili, che consentano di utilizzare un costruttore più compatto.
Vedremo come sfruttare questa opportunità nella prossima puntata.

Metodi e costruttori IV
Lezione 11

Nella scorsa lezione abbiamo conosciuto una delle caratteristiche più interessanti ed eleganti di Java: l'overloading dei metodi, in particolare dei metodi costruttori.
Questo aspetto non appesantisce il programmatore con un ulteriore carico di lavoro, perché una volta invocato il metodo, anche se ne esistono diversi con lo stesso nome, il numero e il tipo degli argomenti permettono a Java di invocare automaticamente il metodo corrispondente, e questo vale ovviamente anche nel caso dei costruttori.
L'overloading dei metodi elimina la necessità di metodi completamente differenti che eseguono sostanzialmente lo stesso compito e consentono ai metodi di funzionare in modo differente in base agli argomenti che ricevono.
Occorre notare che Java non considera il tipo di valore restituito come un elemento di distinzione, ragion per cui se si cerca di creare due metodi con la stessa segnatura, ma tipi restituiti diversi, la classe non viene compilata e si ottiene l'errore "Methods can't be redefined with a different return type".
Una possibilità che nasce dall'overloading è quella di definire dei valori di default per certi attributi.

Potremmo ad es. decidere che sia possibile non specificare esplicitamente i valori relativi all'autoradio e all'antifurto, se entrambi sono presenti nell'oggetto della classe "Automobile" che stiamo istanziando.
Ciò significa che se scrivo l'istruzione:

Automobile auto = new Automobile("Ferrari", "Testarossa", 100, 20, 4.2, 90);


istanzio l'oggetto auto come una Ferrari Testarossa che ha un serbatoio da 100 litri, livello di riserva di 20 litri, percorre 4.2 km con un litro, ha 90 litri di benzina nel serbatoio e contiene sia l'autoradio che l'antifurto (non me ne vogliate se i dati tecnici non corrispondono a verità, ma non sono informato al riguardo: al momento non avevo intenzione di comprarne una!).
Potremmo pensare di implementare questa possibilità, inserendo un altro costruttore come il seguente:

Automobile(String mrc, String mdl, float capSerb, float livRis, float cns,
            float qBenz)


Questa soluzione è corretta e funziona, tuttavia è poco elegante.

Infatti, sia questo metodo che il costruttore che avevamo originariamente creato per la nostra classe Automobile, compiono esattamente le stesse operazioni.
Per venirci in aiuto in queste situazioni, Java fornisce una sintassi speciale con la quale è possibile richiamare un costruttore dal corpo di un altro costruttore della stessa classe.
Ciò è possibile grazie alla parola chiave this, che viene utilizzata ogni qualvolta ci si riferisce all'oggetto corrente.
Essa può trovarsi:
in notazione puntata per citare un attributo dell'oggetto;
come argomento di un metodo;
come valore restituito dal metodo presente;
nel corpo di un metodo costruttore per invocare un altro costruttore della stessa classe.
Tralasciamo per ora i primi tre casi e concentriamoci su quello che ci interessa, ovvero il quarto.
Il nostro scopo è quello di risparmiare di scrivere più volte le stesse righe di codice, sia per una questione di tempo (e, come sappiamo bene, il tempo è denaro), sia per una questione di manutenibilità e di chiarezza del programma che stiamo scrivendo.

Immaginate quale lavoro potrebbe richiedere rivedere il codice di una classe corposa, magari con decine di attributi e numerosi costruttori, nella quale ci siamo accorti di aver fatto degli errori di inizializzazione!
Sarebbe inoltre scomodo scorrere decine di righe di listato per andare a cercare proprio le righe che ci interessano. e se ce ne dimenticassimo qualcuna?
Come possiamo ben vedere, oltre che una questione di eleganza, è soprattutto una questione di efficienza.
Esaminiamo il corpo del costruttore precedente, modificato utilizzando la parola chiave this:

Automobile(String mrc, String mdl, float capSerb, float livRis, float cns,
            float qBenz)


Quello che facciamo all'interno di esso è semplicemente richiamare il costruttore completo (quello con 8 parametri), passandogli i 6 valori che abbiamo ricevuto dall'istruzione new, più i 2 che abbiamo assegnato di default.

Ereditarietà e gerarchie di classi I
Lezione 12

Dopo aver lavorato pazientemente alla creazione della nostra prima classe in Java, dobbiamo per un attimo fermarci e - ahinoi! - andare ad approfondire qualche altro piccolo concetto teorico.
Nella quarta lezione, abbiamo saputo che i nostri amici Bill Gates e Linus Torvalds appartengono alla classe "Uomo", ma appartengono anche alla classe "Esseri viventi", della quale fanno parte anche Rin Tin Tin e Lassie, che a loro volta sono istanze della classe "Cane".
Vista così, la questione sembra piuttosto ingarbugliata e confusa, e correremmo il rischio di fare assegnamenti sbagliati!
Tuttavia non possiamo fare a meno di notare che esiste una certa relazione tra i nostri quattro amici e che questa relazione ha una particolarità: infatti possiamo dire che un uomo è un essere vivente, che un cane è anch'esso un essere vivente, ma non è corretto affermare che un essere vivente è un cane.
La differenza tra "Essere vivente" e "Uomo" sta nel livello di astrazione.

La prima rappresenta, infatti, una generalizzazione della seconda e comprende altre classi oltre ad essa (come ad es. la classe "Cane"), mentre la seconda rappresenta una specializzazione della prima.


Sia il cane che l'uomo hanno tutte le caratteristiche dell'essere vivente, ovvero entrambi vivono, in qualche modo si nutrono, etc.; si dice che essi ereditano dalla classe "Essere vivente" gli attributi e i comportamenti.
E proprio l'ereditarietà è uno dei concetti base dell'OOP e, di conseguenza, anche di Java.
Non è difficile comprendere che l'ereditarietà si può sviluppare in una gerarchia molto complessa, che si sviluppa a partire da una radice (che rappresenta il massimo grado di astrazione), per poi diramarsi via via in una struttura ad albero, in cui ogni ramo rappresenta una specializzazione del nodo precedente.

La figura successiva ci mostra un esempio di quanto espresso sinora.


In Java, una classe che eredita le caratteristiche da un'altra classe viene chiamata sottoclasse, mentre la classe che fornisce l'eredità è detta superclasse.
Una classe può avere una sola superclasse e un numero illimitato di sottoclassi.
Una sottoclasse eredita tutti gli attributi e i metodi della relativa superclasse, e può definirne di propri in aggiunta o in sostituzione a quelli della propria superclasse.

 Vediamo di chiarire tutto con un esempio pratico.
Definiamo una classe "Uomo" molto semplice:

class Uomo

    public String getProfessione()

    public byte getEta()

    public void dice(String parole)
}


Per creare una sottoclasse a partire da questa, occorre utilizzare la parola chiave extends.

Se volessimo ad es. definire una classe "UomoDAffari", potremmo scrivere:

class UomoDAffari extends Uomo
   
    public long getLiquidita()

    public void dice(String parole)
}


Questa classe contiene gli attributi nome, eta, professione (ereditati dalla classe Uomo) e gli attributi titoloDiStudio e liquidita, che sono invece specifici di essa.

Inoltre su un oggetto di tipo UomoDAffari possiamo invocare i metodi getNome(), getEta(), getProfessione(), getLiquidita() e dice(String).
Quest'ultimo metodo è stato definito sia nella superclasse "Uomo" che nella sottoclasse "UomoDAffari".
Nella prima, esegue la stampa a video della variabile nome, seguita dalla stringa ": - " e da una stringa passatagli come parametro; nella seconda, stampa innanzitutto il titolo di studio, poi un carattere di spazio seguito dalla variabile nome, e dopo la stringa ": - " seguita da un'altra stringa passatagli come parametro.
Questa possibilità di ridefinizione dei metodi è molto interessante; più avanti vedremo in dettaglio come funziona.

Ereditarietà e gerarchie di classi II
Lezione 13

Se avete provato a compilare gli esempi della volta scorsa, ossia la classe "Uomo" e la classe "UomoDAffari", vi sarete trovati di fronte ad almeno due errori del compilatore di questo tipo:



Come si può vedere, il compilatore Java ci da diverse informazioni riguardo ad ogni errore commesso:
il file sorgente in cui l'errore è stato rilevato;
il numero della riga;
il tipo dell'errore;
viene stampata l'intera riga e messo un simbolo (^) nel punto in cui l'errore è stato rilevato.
La prima informazione sembrerebbe inutile, tuttavia ha un senso, perché prima di compilare il sorgente indicato alla linea di comando (nel nostro caso UomoDAffari.java), Java cerca tutti i bytecode relativi alle classi utilizzate nel sorgente specificato, se non li trova cerca i sorgenti e (se li trova) li compila prima di quello da noi indicato.

In questo caso, la compilazione del sorgente Uomo.java (se non era stata fatta in precedenza) è andata a buon fine e non ha dato luogo ad alcun errore.
Il primo errore rilevato al compilatore è "No constructor matching Uomo() found in class Uomo" con riferimento al sorgente UomoDAffari.java.
La cosa può, a prima vista, sembrare piuttosto strana; tuttavia è chiaro che il problema è legato ai costruttori.
Abbiamo detto che non è necessario specificare un costruttore per una classe.
Quando si omette di definirlo esplicitamente, il compilatore si occupa di crearne automaticamente uno di default senza argomenti, cosicché per creare un oggetto di quella classe bisogna indicare il nome della classe, seguito da due parentesi tonde (aperta e chiusa) del tipo:

NomeClasse oggetto = new NomeClasse();

Questa istruzione crea un nuovo oggetto e invoca il costruttore di default, anche se non ne è stato definito uno in maniera esplicita; senza di esso non avremmo alcun metodo da chiamare per istanziare il nostro oggetto.
Teniamo presente che, una volta definito almeno un costruttore, il compilatore non creerà automaticamente quello di default (ossia senza parametri), per cui non potremo creare un oggetto della classe "Uomo" semplicemente scrivendo:

Uomo macho = new Uomo();

Poiché non abbiamo indicato alcun costruttore per la classe "UomoDAffari", il compilatore ne costruisce uno di default e salta fuori un problema dovuto all'ereditarietà, che ci spiega il perché del primo errore.

Infatti quando si istanzia un oggetto di una sottoclasse, esso contiene quello che potremmo definire un "sottooggetto" della superclasse, che è a tutti gli effetti un oggetto della classe padre (parlare di superclasse o di classe padre è la stessa cosa).
E' essenziale che il "sottooggetto" della classe padre sia inizializzato correttamente e ciò lo si può fare solamente effettuando l'inizializzazione nel costruttore, per mezzo di un'invocazione del costruttore della superclasse.
Java inserisce automaticamente nel costruttore della classe figlio una chiamata al costruttore senza argomenti della classe padre.
Ecco la causa dell'errore: il costruttore di default creato dal compilatore deve eseguire al suo interno l'invocazione al costruttore Uomo() che non è presente nella superclasse, per cui la compilazione non può avere luogo.
Due sono le soluzioni possibili:
possiamo definire esplicitamente un costruttore senza argomenti per la classe Uomo();
possiamo definire esplicitamente un costruttore per la classe "UomoDAffari", che invochi esplicitamente il costruttore della superclasse.

La prima soluzione non ha senso per il nostro esempio (non sarebbe carino creare un uomo con un nome di default), per cui opteremo per la seconda e inseriremo nel codice della sottoclasse il seguente costruttore:

UomoDAffari (String nome, byte eta, String prof, String tds, long l)

Avete notato qualcosa di nuovo?
Ebbene, la parola chiave super viene utilizzata per riferirsi alla superclasse della classe attuale.
Con la prima istruzione non facciamo altro che invocare uno dei costruttori della classe "Uomo", passandogli come parametri i valori opportuni, mentre le due istruzioni successive servono ad inizializzare gli attributi specifici della nostra sottoclasse.
Se riproviamo a compilare il primo errore sarà sparito; vedremo come correggere il secondo nella prossima puntata.

Ereditarietà e gerarchie di classi III
Lezione 14

Se proviamo nuovamente a compilare il sorgente della classe "UomoDAffari", otterremo il seguente errore:



che non ci dovrebbe stupire più di tanto: l'attributo nome dichiarato all'interno della classe "Uomo" è ad accesso privato, il che vuol dire, come abbiamo visto in precedenza, che nessuna classe esterna la può utilizzare direttamente.
Potremmo pensare di dichiararla ad accesso pubblico o di omettere il modificatore di accesso.
Queste soluzioni funzionano ma non sono esattamente quello che vorremmo, perché è nostra intenzione proteggere questo attributo da azioni esterne indesiderate.
La soluzione adeguata per questo problema è il modificatore di accesso protected, che ha pressappoco la stessa funzione di private, ma permette alle classi derivate di accedere a quell'attributo: proprio quello che cercavamo!

Se modifichiamo il codice della classe "Uomo", sostituendo al modificatore private nella dichiarazione della variabile nome il modificatore protected, e proviamo a ricompilare, non sarà più segnalato alcun errore.
All'interno del corpo della classe "UomoDAffari" è stato definito un metodo già presente nella classe "Uomo", ossia:

public void dice(String parole)

Questa operazione prende il nome di ridefinizione di un metodo e serve per specializzare il comportamento di una classe figlio rispetto alla classe padre.
Infatti, quando viene invocato un metodo su una classe, l'interprete Java cerca la dichiarazione del metodo dapprima nel corpo della classe, se la trova la esegue, altrimenti risale la gerarchia di figlio in padre, finchè non trova una dichiarazione del metodo (ovviamente se la ricerca è infruttuosa viene rilevato un errore).
Tutto ciò è possibile grazie al fatto che ogni classe può avere al massimo una sola superclasse, per cui non esiste ambiguità nella ricerca del metodo.

 In questo Java differisce da C++, nel quale è permessa l'ereditarietà multipla, cioè la possibilità di derivare attributi e metodi da più superclassi: sebbene rappresenti una buona possibilità di programmazione, l'ereditarietà multipla può portare a situazioni di ambiguità.
Supponiamo di definire una classe "GiocatoreDiGolf" nel seguente modo:

class GiocatoreDiGolf {
    ...
    public void dice(String parole)
    ...
}


Se fosse possibile ereditare da più classi, potremmo decidere di far derivare "UomoDAffariRicco" sia da "Uomo" che da "GiocatoreDiGolf" e di non ridefinire il metodo dice.
Nel momento in cui invocassimo tale metodo su un oggetto di tipo "UomoDAffariRicco", non potremmo sapere con certezza se venisse eseguito il metodo definito nella classe "Uomo" o quello definito nella classe "GiocatoreDiGolf".

Per ovviare a casi di ambiguità di tale tipo Java permette che una classe erediti solo da un'altra classe.
Questa soluzione elimina ogni causa di ambiguità al riguardo, ma si rivela troppo limitativa per sviluppare gerarchie di una certa complessità, che sono utili in gran parte delle applicazioni più comuni.
Per ovviare a un limite troppo stretto, Java permette di utilizzare le interfacce, ossia delle classi i cui metodi non eseguono alcuna istruzione.
Lavorare con le interfacce è come lavorare con le classi, senonché non è possibile creare direttamente un'istanza di un'interfaccia mediante l'istruzione new.
E' possibile sia utilizzare interfacce nelle nostre classi, che definirne di nuove.
Per poter utilizzare un'interfaccia occorre utilizzare la parola chiave implements come parte della definizione di classe, ad es.

class UomoDAffari extends Uomo implements unaInterfaccia

In questo esempio la classe "UomoDAffari" eredita gli attributi e i metodi della classe "Uomo" e quelli dell'interfaccia "unaInterfaccia".
Poiché le interfacce forniscono solo le definizioni dei metodi, occorre implementare tali metodi nella propria classe, eliminando così la possibilità di casi ambigui, propria dell'ereditarietà multipla.
Inoltre non c'è limite al numero di interfacce che si possono utilizzare, ma esiste l'obbligo di implementare tutti i metodi di ognuna di esse.
L'argomento delle interfacce può essere compreso appieno dopo aver acquisito una maggiore esperienza con la programmazione orientata agli oggetti di Java, per cui ne rimandiamo la trattazione completa alle lezioni successive.

Variabili e metodi di classe
Lezione 15

Normalmente, quando creiamo una classe, descriviamo come saranno gli oggetti di quella classe e quale sarà il loro comportamento, ma perché gli attributi e i metodi di un oggetto diventino disponibili è necessario crearlo mediante l'istruzione new.
Esistono, tuttavia, due situazioni in cui questo approccio non è sufficiente:
1. quando si vogliono memorizzare delle informazioni indipendentemente dal numero di oggetti creati o anche se non è stato creato alcun oggetto;
2. quando si vuole utilizzare un metodo che non è associato con nessun oggetto particolare della classe.
Per risolvere questo problema è possibile utilizzare la parola chiave static, che permette di non collegare la variabile o il metodo ad una particolare istanza della classe, cosicché è possibile invocare quel metodo o accedere a quella variabile anche se non è stato mai creato un oggetto di quella classe.
Si utilizzano i termini variabili di classe e metodi di classe per indicare che tali variabili e metodi esistono per l'intera classe e non per il particolare oggetto della classe.

Per rendere statico un attributo o un metodo, occorre semplicemente anteporre la parola chiave static alla definizione.
L'esempio seguente è una classe con un attributo ed un metodo statici:

class EsempioStatic

    public void stampa()
}


Se adesso creiamo due oggetti di questa classe:

EsempioStatic es1 = new EsempioStatic();
EsempioStatic es2 = new EsempioStatic();


entrambi condivideranno la stessa variabile i e sia es1.i che es2.i avranno lo stesso valore.

Esistono due modi per riferire una variabile o un metodo di tipo static:
1. utilizzando il nome di un oggetto della classe (ad esempio es1.i);
2. utilizzando direttamente il nome della classe (ad esempio EsempioStatic.incrementa())
Quest'ultimo è preferibile, perché ne enfatizza la natura statica.
Aggiungiamo alla precedente classe il seguente metodo:

public static void main(String args[])

ed esaminiamone le operazioni compiute.
Per prima cosa vengono dichiarati e creati due oggetti della classe "EsempioStatic", poi viene invocato su ciascuno di essi il metodo stampa(), viene invocato il metodo statico incrementa() direttamente sulla classe e poi nuovamente il metodo stampa().

Dal prompt del Dos (per Windows) o dalla shell (per Linux), compiliamo la classe con il comando:

javac EsempioStatic.java

e richiamiamo la macchina virtuale Java con il comando:

java EsempioStatic

Otterremo in output le seguenti righe:

Il valore di i e': 15
Il valore di i e': 15
Il valore di i e': 16
Il valore di i e': 16

Come possiamo vedere, l'esecuzione dell'istruzione:

EsempioStatic.incrementa();

ha avuto ripercussioni su entrambi gli oggetti es1 ed es2.
Per capire pienamente come viene ottenuto questo risultato, occorre guardare più profondamente come lavora Java.
Abbiamo detto che una variabile è un elemento in cui è possibile memorizzare informazioni di vario tipo durante l'esecuzione di un programma; perché ciò possa realizzarsi, è necessario che tali informazioni vengano memorizzate da qualche parte nella memoria del computer.

Quando viene creato un oggetto, Java pensa automaticamente ad allocare per esso la memoria necessaria a contenere i suoi attributi, più altre informazioni per gestire l'oggetto a basso livello (nulla di cui si debba preoccupare il programmatore).
Se vengono creati due oggetti di una stessa classe, ad ognuno di essi sarà assegnata un'area di memoria dedicata, contente una copia personale di tutte le variabili non statiche dichiarate nella classe. Se si modifica una variabile istanza, il nuovo valore sarà assegnato solo alla copia dell'oggetto sul quale l'operazione è stata eseguita, senza influire sulle copie delle altre istanze.
Nel caso di una variabile statica, l'area di memoria in cui essa è memorizzata viene condivisa da tutte le istanze della classe, cosicché se una di esse ne modifica il valore, l'operazione avrà ripercussione anche sulle altre.

Usare le classi
Lezione 16

A questo punto siamo in grado di creare le nostre classi in maniera appropriata, con tutti gli attributi e i metodi che ci servono, ma per organizzare tutto a dovere dobbiamo ancora fare qualche precisazione.
Adesso sappiamo che le classi possono essere organizzate in strutture gerarchiche, grazie ai meccanismi propri dell'OOP quali l'ereditarietà, ma questo non è l'unico modo possibile.
Se diamo un'occhiata alla documentazione fornita con il Sun JDK, potremo notare che Java ci offre una nutrita libreria di classi predefinite, che implementano buona parte delle funzionalità di base necessarie e che sono più che sufficienti per le normali esigenze di programmazione, anche se per programmi più complessi può rendersi necessaria la creazione di una famiglia di nuove classi interagenti tra loro.

Java organizza classi e interfacce in package.
Possiamo considerare un package come un contenitore che raccoglie classi, interfacce ed altri package.
Le librerie di classi di Java, disponibili in ogni implementazione, sono contenute in un package denominato java, che a sua volta contiene package minori che definiscono sottoinsiemi specifici delle funzionalità del linguaggio, come la gestione dei file, le comunicazioni di rete e così via.
Con Java 2 sono state introdotti nuovi package, come javax.swing per GUI (Graphical User Interface) avanzate.
A priori le classi definite dall'utente hanno accesso solo alle classi contenute in java.lang, che contiene le funzionalità di base, mentre per utilizzare le classi di altri package occorre richiamarli esplicitamente per nome o importarli nel file sorgente.
Supponiamo di voler utilizzare la classe "Date", contenuta nel package java.util, per creare un metodo che stampi a video la data odierna.

Possiamo utilizzare indistintamente:

class miaData
}


oppure :

import java.util.Date;

class miaData
}

Nel primo caso abbiamo richiamato esplicitamente la classe "Date" includendo il nome del package che la contiene all'interno del codice, nel secondo caso invece abbiamo importato la classe direttamente nel file sorgente, mediante l'istruzione import.
La seconda soluzione ci permette di utilizzare istruzioni più compatte per riferirci alla classe (si noti la differenza nelle due dichiarazioni della variabile d), ed è quella che viene comunemente usata.
Quando si utilizzano più classi dello stesso package, si utilizza il nome del package, seguito da un punto e un asterisco.
Se ad es. dovessimo importare più classi dal package java.util, potremmo utilizzare l'istruzione:

import java.util.*;

All'interno di un codice sorgente si possono importare tutti i package che si vogliono, mediante più istruzioni import consecutive.

La notazione con asterisco non comporta, come si potrebbe pensare, l'importazione di tutte le classi e le interfacce del package, ma Java si occuperà di recuperare automaticamente da esso tutte le classi utilizzate nel codice sorgente.
Notiamo che in entrambi i codici di esempio precedenti, abbiamo dichiarato una variabile s di tipo "String" senza importare o riferire alcun package, come del resto abbiamo fatto negli esempi delle lezioni precedenti: ciò è possibile perché "String" è una classe del package java.lang.
Più avanti vedremo come definire i nostri package e come organizzare al loro interno le classi da noi create.
Per quanto riguarda la creazione di nuove classi, è opportuno fare una precisazione.
In Java ogni cosa è un oggetto, con l'unica eccezione dei tipi primitivi (anche se di questi, come abbiamo visto, esistono gli oggetti corrispondenti), quindi è logico pensare che ogni classe venga considerata come sottoclasse di un unico progenitore, la classe "Object", che è la radice di tutta la gerarchia di classi (per indicare questa caratteristica parla di single root hierarchy, cioè gerarchia a radice singola).

Quando viene creata una nuova classe che non estende esplicitamente nessuna classe padre, ad es.:

class nuovaClasse

automaticamente viene definita come sottoclasse della classe "Object", come se la definizione fosse la seguente:

class nuovaClasse extends java.lang.Object

Con la prossima lezione inizieremo un nuovo capitolo, in cui vedremo come realizzare applicazioni Java.

Scrivere un'applicazione Java
Lezione 17

A partire da questa lezione cominciamo lo studio del modo in cui Java ci permette di sviluppare applicazioni.
Per chi sviluppa software già da tempo, utilizzando uno dei linguaggi "classici", quali C, C++, Pascal e altri, il concetto di applicazione non è nuovo.
Per applicazione si intende un programma eseguibile autonomamente.
Tralasciando i dettagli teorici, ci concentreremo sulle possibilità che ci vengono offerte da Java per creare delle applicazioni che girino su qualsiasi piattaforma.
Nel caso abbiate dimenticato quanto detto a proposito nella lezione 2, vi consiglio di rileggerla per rinfrescare tutte quelle nozioni che adesso metteremo in pratica.

Qualcuno di noi avrà provato, in passato, a richiamare la macchina virtuale di Java, passandogli direttamente come parametro il nome di una delle classi create nelle precedenti lezioni, ottenendo un errore del tipo:

In class Automobile: void main(String argv[]) is not defined

Ciò avviene perché l'interprete Java presuppone che, se il programma viene lanciato dalla riga di comando, si tratti di un'applicazione. Un'applicazione, infatti, parte da un metodo particolare, chiamato main(), e poiché la classe "Automobile" non possiede tale metodo, l'interprete non sa come comportarsi.

Per utilizzare questa classe possiamo:
1. creare un'applicazione o un'applet che ne faccia uso;
2. aggiungere un metodo main() alla classe stessa.
La segnatura del metodo main() ha sempre questa forma:

public static void main(String arg[])

Infatti deve:
- essere pubblico, perché deve rendersi disponibile al mondo esterno (in particolare all'interprete Java);
- essere un metodo di classe, perché deve essere usato senza prima aver creato un oggetto;
- non restituire alcun valore.

Notiamo inoltre che esso accetta un parametro particolare, il cui uso e significato vedremo in seguito.
Il corpo del metodo main() contiene il codice di avvio dell'applicazione, che provvede all'inizializzazione di variabili e alla creazione di istanze delle classi usate.
Essendo inoltre un metodo di classe, la classe che lo contiene non viene creata automaticamente quando si avvia l'esecuzione, per cui se si vuole trattare la classe come un oggetto, occorre crearne esplicitamente un'istanza all'interno di tale metodo.
Aggiungiamo alla classe "Automobile" il seguente metodo main() (solitamente viene aggiunto come ultimo metodo):

public static void main(String arg[])

Ricompiliamo la classe e mandiamola in esecuzione (ormai sappiamo come fare).

Otterremo a video la seguente riga:

La mia auto e' una Vera Carriola

Abbiamo così creato un'applicazione molto semplice, ma possiamo sbizzarrirci ad utilizzare i metodi della classe per creare diverse applicazioni.
Poiché lo abbiamo già visto diverse volte, è il caso di notare che per stampare a video una stringa, si utilizza un metodo particolare, println(), che accetta come parametro la stringa da stampare.
Questo metodo è invocato sulla variabile out, che è un attributo statico della classe System fornita dal JDK stesso: ciò spiega perché è possibile utilizzare tale metodo senza aver prima istanziato un oggetto della classe System.
Un'applicazione Java può avere una sola classe, oppure, come avviene nella maggior parte dei casi, essere composta da diverse classi, delle quali vengono create e utilizzate una o più istanze durante l'esecuzione. E' possibile creare tutte le classi di cui si voglia disporre, senza limiti sul numero o la dimensione.

Tra tutte le classi che compongono l'applicazione, solo quella d'avvio necessita di avere un metodo main(); tuttavia, se le classi ausiliarie includono dei propri metodi main(), questi vengono ignorati quando il programma viene eseguito.
Una buona abitudine di programmazione è quella di corredare ogni nuova classe di un metodo main(), che può essere impiegato per testare la classe e per dare, a chi vuole usare la classe da noi creata, un esempio di possibile utilizzo.
Adesso andate a riguardare il codice di esempio della lezione 3 e non dovreste avere difficoltà a comprenderne il significato.
Alla prossima!

Espressioni e operatori
Lezione 18

Ora che sappiamo come creare applicazioni Java, dobbiamo conoscere che cosa è possibile fare con esse, ossia quali sono i costrutti che il linguaggio ci mette a disposizione per l'implementazione degli algoritmi.
In Java è possibile manipolare oggetti e dati utilizzando gli operatori, e prendere delle decisioni per mezzo di istruzioni di controllo dell'esecuzione.
Poiché Java è stato derivato in gran parte dal C++, molte istruzioni e gli operatori che vedremo saranno familiari ai programmatori di C e C++, anche se sono stati aggiunti miglioramenti e semplificazioni.
Infatti uno degli obiettivi dei "genitori" di Java era quello di semplicare l'apprendimento della sintassi e della logica del nuovo linguaggio anche da parte degli sviluppatori esperti e poiché i programmatori C++ erano (e lo sono ancora) in maggioranza la scelta era obbligata.

Nei linguaggi di programmazione, un operatore elabora uno o più argomenti (detti operandi) per produrre un nuovo valore.
In Java quasi tutti gli operatori lavorano solo con i tipi primitivi, ad eccezione di '=', '==' e '!=' che funzionano con tutti gli oggetti.
Inoltre la classe String supporta anche '+' e '+='.
Esistono delle regole di precedenza che definiscono il modo in cui viene valutata un'espressione con più operatori, ovvero l'ordine con cui gli operatori vengono eseguiti: ad es. possiamo pensare alla moltiplicazione e alla divisione, che vengono eseguite prima dell'addizione e della sottrazione.
Per modificare le regole di precedenza, si devono usare delle parentesi per rendere esplicito l'ordine di valutazione.
Ad es. la seguente istruzione:

a = x - y + 18*3 + z;

ha un significato completamente diverso dalla stessa istruzione con un particolare raggruppamento delle parentesi:

a = x - (y + 18)*(3 + z);

L'operazione di assegnamento viene eseguita con l'operatore =, che prende il valore dell'espressione alla sua destra e lo assegna alla variabile alla sua sinistra.
A destra dell'operatore = possiamo avere una costante, una variabile o una qualsiasi espressione che produce un valore, ma a sinistra deve esserci necessariamente una variabile (ossia uno spazio fisico in cui memorizzare il valore).
E' possibile assegnare una valore costante ad una variabile:

x = 4;

ma non si può assegnare nulla ad un valore costante, cioè non si può scrivere:

4 = x;

L'assegnamento tra tipi primitivi è molto semplice, in quanto non si fa altro che copiare il valore di una variabile nell'altra, ad es:

x = y;

copia il valore di y in x, cosicché se in seguito si modifica y, l'operazione non avrà alcun effetto su x, il che è quello che tutti ci aspettiamo che avvenga.

Le cose cambiano quando applichiamo l'operatore di assegnamento tra due oggetti.
Infatti lavorare con gli oggetti significa operare con il loro riferimento, per cui se si assegna un oggetto ad un altro, non si fa altro che copiare il riferimento a quell'oggetto.
Per comprendere meglio la differenza sostituiamo il precedente metodo main() della classe "Automobile" con il seguente:

public static void main(String arg[])

Una volta compilato il sorgente ed eseguito il binario, otterremo il seguente output:

L'auto 1 e' una Vera Carriola
L'auto 2 e' una Vera Bomba
Sull'auto 2 c'e' l'autoradio? true
Adesso l'auto 2 e' una Vera Carriola
Sull'auto 2 c'e' l'autoradio? false


Inizialmente auto1 e auto2 sono due oggetti distinti della stessa classe e le prime due stampe a video lo evidenziano; con l'assegnamento:

auto2 = auto1;

la variabile auto2 punta allo stesso oggetto di auto1, per cui la modifica alla variabile autoradio dell'oggetto auto1 si riflette sull'oggetto auto2.

Vedete la differenza con il seguente programma:

class TestPrimitivi
}

Operatori aritmetici
Lezione 19

Java comprende 5 operatori per l'aritmetica di base, come mostrato nella tabella seguente:

OPERATORE

SIGNIFICATO

ESEMPIO

Somma

Sottrazione

Moltiplicazione

Divisione

Modulo


Tutti gli operatori richiedono due operandi, uno a destra e uno a sinistra, e permettono di realizzare le istruzioni aritmetiche fondamentali nella forma che tutti conosciamo.
Un'attenzione particolare merita l'operazione di divisione, che genera un intero se entrambi gli operandi appartengono ad uno dei tipi interi, o un numero in virgola mobile se gli operandi sono float o double; inoltre la divisione tra interi genera un numero intero, arrotondando il risultato se necessario.

Ad es. il programma:

class TestDivisione
}


stampa a video il numero 2, in quanto l'operazione di divisione produce un valore non intero (2.33) che viene arrotondato troncando la parte decimale.

Il modulo, che utilizza l'operatore %, restituisce il resto di una divisione, ad es. 33 % 17 restituisce 16, perché il numero 17 sta una volta nel numero 33 con resto di 16.
E' bene ricordare che la maggior parte delle operazioni che coinvolgono interi produce un tipo int indipendentemente dal tipo di operandi originari, cosicché spesso è necessario ricorrere al casting per effettuare assegnamenti a variabili di tipo più ristretto (ad es. byte o short).

Riguardo all'assegnamento, Java consente di legarne più di uno in un'unica istruzione del tipo:

x = y = z = 7;


Nel codice di esempio viene assegnato a z il valore 7, a y il nuovo valore di z (quindi 7) e a x il nuovo valore di y (sempre 7); ciò avviene perché la parte destra di un'espressione di assegnamento viene valutata prima di eseguire l'assegnamento stesso.
Sebbene questa rappresenti una buona possibilità per rendere il codice più compatto, ne viene comunque sconsigliato l'uso, perché può rendere più difficile la comprensione del programma.

Utilizzare un'espressione per modificare il valore di una variabile è un'operazione molto comune alla quale si deve ricorrere e in Java esistono degli operatori utilizzati appositamente per questo scopo, come mostrato nella tabella seguente:

ESPRESSIONE

SIGNIFICATO

x += y

x = x + y

x -= y

x = x - y

x *= y

x = x * y

x /= y

x = x / y

x %= y

x = x % y


Queste forme abbreviate sono molto comode ed equivalgono funzionalmente alle istruzioni di assegnamento, ma se si utilizzano espressioni complesse, possono verificarsi casi in cui gli operatori non sono equivalenti, per cui è opportuno, in tali casi, non ricorrere alle forme abbreviate.
Ad es. le due istruzioni:

x = x / y + 5 ;
x /= y + 5 ;


non producono lo stesso risultato: infatti nel primo caso viene prima eseguita la divisione e poi la somma, mentre nel secondo caso viene prima eseguita la somma e poi la divisione

Altri due operatori sono il minus unario e il plus unario, che vengono utilizzati premettendo, rispettivamente, il segno - e il segno + al nome della variabile.
Il minus unario restituisce il valore della variabile che precede con segno opposto; il plus unario fornisce l'operazione simmetrica a quella precedente e non sortisce, a conti fatti, nessun effetto, poiché restituisce inalterato il valore della variabile che lo segue.
Il codice:

int x = 5;
int y = -x;
int z = +x;


inizializza x al valore 5, assegna il valore -5 a y e il valore 5 (identico a quello di x) a z.

Infine sono molto utilizzati gli operatori di incremento (++) e decremento (--), che permettono, rispettivamente, di aggiungere e sottrarre 1 al valore della variabile.

Possono essere usati in notazione prefissa o suffissa, cioè precedere o seguire il nome della variabile che modificano.
Questa differenza ha importanza implementativa, perché la notazione prefissa incrementa il valore della variabile prima di valutare l'espressione alla quale la variabile appartiene, mentre la notazione suffissa esegue prima la valutazione
dell'espressione e dopo incrementa la variabile.
Ad es. il codice:

int x, y ,z;     
x = 18;
y = x++;
z = ++x;

inizializza x, assegna a y il valore 18 e incrementa x, dopodiché incrementa x e assegna a z il suo nuovo valore, cioè 20. Nella prossima puntata analizzeremo gli operatori logici.

Operatori relazionali e logici I
Lezione 20

Java offre 6 operatori per le operazioni di confronto, come mostra la tabella seguente:

OPERATORE

SIGNIFICATO

ESEMPIO

Uguale a

x == 3

Diverso da

x != 3

<

Minore di

x < 3

>

Maggiore di

x > 3

<=

Minore o uguale a

x <= 3

>=

Maggiore o uguale a

x >= 3


Con tali operatori è possibile effettuare confronti tra variabili, tra variabili e valori letterali o altri tipi di informazione, ottenendo come risultato un valore booleano true o false, a seconda che il confronto abbia dato esito positivo o negativo.
Nel codice di es.

int soldiInTasca = 10000;
int prezzoBigliettoConcerto = 40000;
boolean ciPossoAndare = soldiInTasca >= prezzoBigliettoConcerto;

la variabile ciPossoAndare assume il valore false, perché la variabile soldiInTasca ha un valore minore di prezzoBigliettoConcerto.

Le espressioni che restituiscono valori booleani si possono combinare in espressioni più complesse tramite gli operatori logici, che sono riassunti in tabella:

OPERATORE

SIGNIFICATO

NOT logico

&

AND

OR

XOR

&&

AND logico

OR logico


Il programma riportato qui di seguito mostra degli esempi di utilizzo degli operatori relazionali e logici:

import java.util.Random;

public class TestOperatoriLogici
}

Un possibile risultato d'esecuzione è il seguente:

----- ----- ------
Numeri generati:
----- ----- ------
 x = 536896264
 y = -349864761
----- ----- ------

 1: x == y vale false
 2: x != y vale true
 3: x  < y vale false
 4: x  > y vale true
 5: x <= y vale false
 6: x >= y vale true
----- ----- ------
 7: !(x == y) vale true
 8: (x != y) &  (x  < y) vale false
 9: (x <= y) |  (x >= 0) vale true
10: !(x < y) ^  (x != y) vale false
11: (x != y) && (x  < y) vale false
12: (x <= y) || (x >= 0) vale true
----- ----- ------


Vediamo di capire cosa avviene.

Attraverso un'istanza della classe "Random" del package java.util vengono generati due numeri interi pseudocasuali x e y, che vengono mostrati a video.
Nelle istruzioni successive (volutamente contrassegnate con un numero) vengono stampate delle possibili espressioni logiche seguite dal loro valore booleano.
Le prime 6 espressioni non dovrebbero esserci oscure:
la 1 confronta x e y e ci dice se sono due numeri uguali;
la 2 ci dice se x e y sono diversi;
la 3 è falsa perché x non è minore di y;
la 4 è vera persché x è maggiore di y;
la 5 è uguale alla 3 e differisce da essa solo nel caso in cui x e y sono uguali;
la 6 è uguale alla 4 e differisce da essa solo nel caso in cui x e y sono uguali.
Le espressioni dalla 7 alla 12 sono un po' più complesse e interessanti:
la 7 ci restituisce il valore opposto alla 1 ed equivale alla 2;

la 8 verifica che siano contemporaneamente vere le due condizioni, se una delle due è falsa, il risultato è falso;
la 9 verifica che almeno una delle due condizioni sia vera, se sono entrambe false, il risultato è falso;
la 10 mostra il funzionamento dello XOR logico, detto anche or esclusivo, che vale true se le due condizioni hanno valore logico opposto, false se le condizioni hanno lo stesso valore logico;
la 11 ha la stessa funzionalità della 8, ma differisce da essa, perché se la prima condizione è falsa, la seconda non viene valutata;
la 12 è uguale alla 9 con la differenza che se la prima condizione è vera, la seconda non viene valutata.

Operatori relazionali e logici II
Lezione 21

L'utilizzo degli operatori && e || può dare luogo al cosiddetto fenomeno di "circuito corto" (short circuiting), che consiste nel fatto che la valutazione dell'espressione continuerà solo finché il suo valore booleano non potrà essere valutata in maniera non ambigua, cosicché potranno non essere valutate tutte le parti dell'espressione logica.
L'esempio seguente ci potrà rendere le cose più chiare:

class CircuitoCorto
     
    public static void main(String arg[])
}

Questa classe fornisce un metodo pubblico che riceve in ingresso due valori e restituisce un valore booleano che vale true se il primo parametro è minore del secondo, false altrimenti; inoltre strampa a video delle informazioni sull'espressione che sta valutando.
Nel metodo main() abbiamo istanziato un oggetto della classe "CircuitoCorto" e dichiarato una variabile locale booleana in cui memorizziamo il valore dell'espressione:

cc.minoreDi(0,1) && cc.minoreDi(2,2) && cc.minoreDi(2,3)

che equivale alla seguente espressione espressa in linguaggio matematico:

0 < 1 AND 2 < 2 AND 2 < 3

poiché la seconda relazione è falsa, l'intera espressione è falsa.
Poiché basta, in una serie di AND consecutivi, che una sola relazione sia falsa perché sia falsa l'intera espressione, il test viene fermato non appena viene trovata una relazione non vera.

Così l'output del programma di esempio sarà:

Domanda:  0 e' minore di 1?
Risposta: true
Domanda:  2 e' minore di 2?
Risposta: false
Il valore dell'espressione e': false


cioè non viene valutata la terza relazione, perché ormai il valore dell'intera espressione sarà comunque falso.
Se provate a sostituire nell'espressione l'operatore & al posto dell'operatore && otterrete il output dal programma il seguente risultato:

Domanda:  0 e' minore di 1?
Risposta: true
Domanda:  2 e' minore di 2?
Risposta: false
Domanda:  2 e' minore di 3?
Risposta: true
Il valore dell'espressione e': false


che mostra che sono state valutate tutte e tre le relazioni.

E' intuitivo comprendere che la prima scelta è più efficiene della seconda, perché valuta l'espressione in minor tempo, dovendo eseguire meno istruzioni.
Analogamente l'operatore || è più efficiente dell'operatore |.

Come avviene per gli operatori aritmetici, anche gli operatori logici hanno delle istruzioni di assegnamento abbreviate, che sono riassunti nella tabella seguente e il cui significato dovrebbe essere abbastanza chiaro:

ESPRESSIONE

SIGNIFICATO

x &= y

x = x & y

x |= y

x = x | y

x ^= y

x = x ^ y


Notiamo che non sono previsti assegnamenti abbreviati per gli operatori && e ||.

Un altro operatore logico, molto usato, è l'operatore ternario, che ha la seguente forma:

test ? risultatovero : risultatofalso


Questo operatore esegue l'espressione logica (test) posta prima del '?'; se il risultato dell'espressione è true ritorna il valore risultatovero, altrimenti ritorna il valore risultatofalso.

Ad es. proviamo ad eseguire il seguente programma:

import java.util.Random;

class TestTernario
}


Vengono generati due numeri interi pseudocasuali e l'espressione ternaria restituisce il valore maggiore; un possibile risultato è il seguente:

i vale 2042297107
j vale 405272159
Il maggiore tra i due e' 2042297107


Nella prossima lezione esamineremo gli operatori che ci consentono di operare a livello dei bit.

Operatori sui bit
Lezione 22

Gli operatori sui bit permettono di manipolare i singoli bit di un tipo di dato primitivo, eseguendo operazioni di algebra booleana sui bit corrispondenti dei due argomenti.
Innanzitutto dobbiamo ricordarci che tutte le informazioni circolano, all'interno di sistemi digitali come il computer, sotto forma di valori 0 (zero) e 1 (uno): ogni oggetto del linguaggio, sia esso un tipo di dato primitivo o l'istanza di una classe, è rappresentato all'interno del computer mediante opportune combinazioni di queste due cifre, dette bit (binary Digit).
Per operare con le cifre binarie 0 e 1 ci si avvale della cosiddetta Algebra di Boole, che fornisce un insieme di regole con le quali è possibile combinare queste cifre per effettuare operazioni aritmetiche elementari.
Le operazioni più complesse e le cosiddette funzioni booleane possono essere realizzate per mezzo di appropriate sequenze di queste operazioni elementari.

L'operatore binario AND (&) produce un 1 se entrambi gli operandi sono 1, 0 altrimenti:

OPERATORE AND BINARIO

Operando 1

Operando 2

Risultato


L'operatore binario OR (|) produce un 1 se almeno uno degli operandi è 1, 0 altrimenti:

OPERATORE OR BINARIO

Operando 1

Operando 2

Risultato

L'operatore binario OR esclusivo (^), brevemente indicato come XOR, produce un 1 se gli operandi hanno valori diversi, 0 se hanno lo stesso valore:

OPERATORE XOR BINARIO

Operando 1

Operando 2

Risultato


L'operatore logico NOT (~), chiamato anche complemento a uno, è un operatore unario (opera cioè con un solo operando) e produce il valore opposto del suo operando:

OPERATORE NOT

Operando

Risultato

Gli operatori binari utilizzano gli stessi caratteri dei corrispondenti operatori logici, cosicché è più facile avere un metodo mnemonico per ricordarne l'utilizzo e il significato.
Anche gli operatori binari possono essere combinati col segno = per istruzioni di assegnamento abbreviate: &=, |= e ^= sono forme permesse.
Naturalmente, poiché ~ è un operatore unario, non può essere combinato con il segno =.

Il tipo booleano è trattato come un valore ad un bit, con una differenza: è possibile eseguire operazioni binarie di tipo AND, OR e XOR, ma non si può eseguire il NOT binario (probabilmente per evitare confusione con il NOT logico).
Per i booleani gli operatori binari hanno lo stesso effetto degli operatori logici, eccezion fatta per l'effetto "short circuiting" (a tal proposito rivedete la lezione precedente).

Vediamo un esempio di applicazione di AND binario a due interi, 12 e 15:

12 & 15

il risultato che si ottiene è 12.
Nonostante l'apparente stranezza del risultato, la spiegazione è molto semplice: la rappresentazione binaria di 12 è 1100, mentre la rappresentazione binaria di 15 è 1111.
L'operatore AND esegue l'and logico bit a bit tra le due stringhe, cosicché, se mettiamo in colonna le due rappresentazioni binarie ed eseguiamo l'operazione, otterremo:

    1100
  & 1111
    ----
    1100


che è ancora la rappresentazione binaria del numero 12.

Un altro tipo di operazione che si può effettuare sui bit è lo shift, ossia lo slittamento delle cifre binarie di un certo numero di posizioni verso destra o verso sinistra.
In Java esistono 3 operatori di shift:

OPERATORE

USO

>>

op1 >> op2

<<

op1 << op2

>>>

op1 >>> op2


Questi operatori possono essere utilizzati solo con i tipi primitivi.

L'operatore >> sposta verso destra le cifre dell'operando op1 di un numero di bit pari al valore specificato da op2, inserendo, al posto dei bit di ordine più alto, degli zero se op2 è positivo o degli uno se op2 è negativo.
Questa caratteristica è detta estensione in segno e non è utilizzata dall'operatore >>>, che ha la stessa funzione del precedente (cioè spostare verso destra i bit di op1 secondo il valore di op2), ma che inserisce sempre al posto dei bit più segnificativi degli zero, qualsiasi sia il segno di op2.

Ad es.:

13 >> 1

sposta i bit che rappresentano il numero 13 (1101) verso destra e aggiunge un 1 nella cifra più significativa, originando il valore 6 (0110).
L'operatore << sposta verso sinistra le cifre dell'operando op1 di un numero di bit pari al valore specificato da op2, inserendo degli zero al posto dei bit di ordine più basso.

Se questi operatori sono appplicati a variabili di tipo char, byte o short, esse vengono automaticamente convertite in int prima dell'operazione di shift, e il risultato prodotto è di tipo int.
Al fine di prevenire abusi nel numero di posizioni di cui viene shiftato un int, vengono presi in considerazione solo i 5 bit di ordine più basso di op2.
Se si opera con una variabile di tipo long, il risultato dello shift è ancora un long, ma vengono considerati solo gli ultimi 6 bit di op2.
Anche per gli operatori di shift esistono delle istruzioni di assegnamento abbreviate: >>=, <<=, >>>=.


Document Info


Accesari: 3462
Apreciat: hand-up

Comenteaza documentul:

Nu esti inregistrat
Trebuie sa fii utilizator inregistrat pentru a putea comenta


Creaza cont nou

A fost util?

Daca documentul a fost util si crezi ca merita
sa adaugi un link catre el la tine in site


in pagina web a site-ului tau.




eCoduri.com - coduri postale, contabile, CAEN sau bancare

Politica de confidentialitate | Termenii si conditii de utilizare




Copyright © Contact (SCRIGROUP Int. 2024 )