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 pò.
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 pò 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.
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:
è
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:
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:
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: >>=, <<=, >>>=.
|