|
Java Tutorial - Parte 1 0.1
Un tutorial per esempi
|
Il linguaggio Java, come tutti gli altri linguaggi di programmazione, ha una sintassi ben definita che deve essere rigorosamente rispettata dal programmatore.
Un errore di battitura di una parola chiave o di un operatore vengono rilevati dal compilatore che rifiuta il sorgente e non produce alcun programma eseguibile. Gli errori di sintassi sono i più facili da correggere perchè rilevati dal compilatore: gli errori logici, che saranno affrontati in un capitolo successivo, sono molto più difficili da scoprire.
Il linguaggio Java è un linguaggio semi-compilato: non produce un eseguibile vero e proprio ma un codice intermedio detto bytecode che deve essere tradotto in linguaggio macchina da una Java Virtual Machine (JVM) la quale deve essere caricata in memoria oltre, naturalmente, al bytecode stesso. Il vantaggio di un linguaggio semi-compilato è evidente: non è necessario riscrivere e/o ricompilare un sorgente se il programma viene eseguito su una piattaforma / architettura hardware diversa fintanto che esiste una JVM specifica per la piattaforma / architettura.
Lo svantaggio è che la velocità di esecuzione sarà inferiore rispetto ad un programma scritto in linguaggio nativo (di questo ne riparleremo!!) e che la JVM occupa una parte di memoria RAM.
Per prima cosa dirò che Java, contrariamente ad altri linguaggi come per esempio il BASIC, è case sensitive cioè è sensibile alle maiuscole/minuscole. Mentre in BASIC le seguenti parole:
pippo Pippo PIPPO PiPPo
si riferiscono allo stesso identificatore, in Java esse rappresentano quattro diversi identificatori.
Tutte le parole chiave del linguaggio Java devono essere scritte in caratteri minuscoli. Scrivere una parola chiave in maiuscolo (come per esempio SWITCH) provoca un errore da parte del compilatore.
I commenti sono annotazioni fatte dal programmatore nel codice sorgente del programma e possono contenere qualsiasi sequenza di caratteri. Essi sono utilissimi per leggere il codice sorgente specialmente da parte di un altra persona. I commenti sono totalmente ignorati dal compilatore.
Java utilizza per i commenti una sintassi uguale a molti linguaggi di programmazione tra cui C, C++, PHP e molti altri:
Esempi:
I commenti sono utilissimi per descrivere le azioni compiute dal programma in modo più comprensibile agli umani. Per essere davvero utili, i commenti dovrebbero descrivere le parti di codice complesse oppure istruzioni o gruppi di istruzioni che non sono ovvie. Per fare un esempio, il seguente commento è totalmente inutile poichè descrive una istruzione semplice da capire per il programmatore:
Ogni programma è costituito da istruzioni che sono le azioni che la CPU deve compiere e dai dati che sono gli oggetti sui quali compiere le azioni per ottenere i risultati desiderati. A loro volta i dati si distinguono in:
I dati di tipo costante sono quelli che, come dice il nome, rimangono costanti per tutta l'esecuzione del programma. Il valore di una costante viene assegnato dal programmatore in fase di scrittura del codice sorgente ed esso non cambia mai. Vi sono due tipi di costanti:
Una costante numerica viene scritta come un qualsiasi numero come per esempio: 123. Se il numero comincia con lo ZERO, il compilatore presume che esso sia in base otto (ottale). Se il numero è preceduto dai caratteri 0x il compilatore assume sia in base 16 (esadecimale).
Una stringa è una sequenza di zero o più caratteri racchiusa tra doppi apici. Ecco alcuni esempi di costanti numeriche e stringa:
Notare che vi è una enorme differenza tra il numero 2 e la stringa "2". Anche se a prima vista essi appaiono identici, il compilatore tratta i primi come numeri ed i secondi come una sequenza di caratteri. Per esempio, se provassimo ad usare l'operatore di somma (il carattere '+') sul numero 2 e la stringa "2" otterremo risultati diversi:
per il compilatore Java, la somma di due stringhe equivale ad una loro concatenazione. Tuttavia, non tutti i linguaggi permettono l'uso del operatore plus con le stringhe; per esempio il linguaggio C non lo permette. Per maggiori info sul operatore di somma vedi Operatori aritmetici.
Le variabili sono dati che, come suggerisce il nome, possono variare il loro valore nel corso del programma sia per effetto di una elaborazione oppure perchè si tratta di dati scelti o inseriti dall'utente che interagisce col programma. Le variabili sono parti importantissime di ogni programma: se non esistessero qualsiasi programma potrebbe solo dare gli stessi risultati ogni volta che viene eseguito.
Qualsiasi variabile, prima di essere usata, deve essere dichiarata assegnandole un identificativo ed un tipo. Non è possibile in un qualsiasi momento futuro nel corso del programma cambiare il tipo alla variabile nè è consentito assegnarle un valore di tipo diverso da quello stabilito nella dichiarazione. La forma generale di definizione di una variabile è la seguente:
dove:
Notate il punto-e-virgola alla fine della dichiarazione: esso è obbligatorio e, in mancanza, il compilatore emetterà un errore di sintassi. Di seguito alcuni esempi di dichiarazione di variabili:
Per attributi di una variabile si intendono alcune caratteristiche che la contraddistinguono come per esempio l'attributo private che indica al compilatore che una variabile è accessibile solo all'interno dell'oggetto che la contiene. Per il momento, non abbiamo alcun bisogno di imparare questi concetti che saranno invece affrontati in Gli oggetti Java.
L'identificatore permette al programmatore di eseguire le istruzioni su una variabile specifica. Per esempio se voglio sommare una certa quantità alla variabile alfa userò quel identificatore nella istruzione di somma. Gli identificatori sono sequenze di caratteri non racchiuse tra doppi apici e sono a libera scelta del programmatore ma devono sottostare alle seguenti regole:
I seguenti sono esempi validi di identificatori:
Mentre i seguenti non sono validi:
Infine, l'ultimo componente di una variabile è il suo tipo. Java è un linguaggio fortemente tipizzato; questo significa che il compilatore emette un errore se si tenta di assegnare ad una variabile un tipo di dato incompatibile con quello dichiarato. Per esempio, lo spezzone di codice seguente dichiara una variabile di tipo numerico intero e poi assegna ad essa un valore di tipo stringa:
il simbolo uguale è l'operatore di assegnamento ed è usato per assegnare un valore ad una variabile; questo concetto viene affrontato più avanti nel capitolo L'operatore di assegnamento. Tentando di compilare questo pezzo di codice otterremo un errore dal compilatore ed il processo di compilazione si arresta.
Nel linguaggio Java ci sono due grandi categorie di tipi di dato: i tipi primitivi e gli oggetti; vi è una grande differenza nel modo in cui la JVM gestisce la allocazione della memoria tra queste due categorie di tipi di dato. Oltre ad essere gestite diversamente in memoria, le due categorie differiscono profondamente anche nel modo in cui la JVM passa i dati ai metodi; di questo ne parleremo più avanti (vedi Funzioni, procedure e metodi).
I tipi di dato primitivi in Java sono suddivisi in tre categorie:
la seguente tabella mostra per ogni tipo di dato primitivo, la parola chiave che lo definisce, la ampiezza in bits ed il range di valori che il singolo dato può contenere:
| Tipo | Ampiezza | Range | Note |
|---|---|---|---|
| boolean | N/A | true oppure false | la ampiezza dipende dalla piattafroma |
| byte | 8 | da -128 a +127 | in complemento a due |
| short | 16 | da -32768 a +32767 | in complemento a due |
| int | 32 | da -2147483648 a +2147483647 | in complemento a due |
| long | 64 | da -9,22x1018 a +9,22x1018 | in complemento a due |
| float | 32 | da ±1.4×10-45 a ±3.4×1038 | Virgola mobile in precisione singola IEEE754 |
| double | 64 | da ±5.0×10-324 a ±1.8×10308 | Virgola mobile in precisione doppia IEEE754 |
| char | 16 | da 0x0000 a 0xFFFF | rappresenta il BMP di Unicode (vedi il tipo char) |
Per tipi di dato complessi si intendono le strutture composte da tipi di dato primitivi aggregate a formare un nuovo tipo di dato.
Supponiamo, per esempio, di voler gestire le date del calendario gregoriano - è quello che usiamo tutti i giorni, istituito da Papa Gregorio XIII nel 1582. Ci rendiamo conto che abbiamo bisogno di tre variabili di tipo primitivo per gestire una data:
ma sarebbe piuttosto difficile tenere traccia di tutte queste variabili in un programma, soprattutto se abbiamo bisogno di molti dati di questo tipo. Una struttura, invece, consente di aggregare le tre variabili in un nuovo tipo che può essere usato al pari di un tipo primitivo. Tutti i linguaggi di programmazione permettono di definire tali strutture; per esempio in C e C++ una struttura viene definita con la parola chiave struct:
Le variabili giorno, mese ed anno all'interno della struttura sono chiamate data members (=membri dati) e posso definire una variabile di questo nuovo tipo esattamente come farei con un tipo primitivo cioè indicando gli eventuali attributi, il tipo ed il suo identificativo:
L'accesso ai membri dati avviene attraverso l'operatore "." (il punto). Supponiamo di impostare oggi al 4 maggio 2025 e domani al giorno successivo:
L'uso delle strutture consente di mantenere il codice pulito e leggibile. In Java le strutture non esistono. I tipi di dato complessi vengono gestiti attraverso gli oggetti. Si tratta di una caratteristica che va ben oltre il concetto di pulizia del codice: attraverso gli oggetti, il linguaggio Java esprime tutta la sua potenza e flessibilità.
Agli oggetti Java saranno dedicati due interi capitoli di questa opera: avrete un primo assaggio in Gli oggetti Java in cui imparerete le tecniche di base come l'astrazione dei dati, mentre in La programmazione ad oggetti ci addentreremo nei concetti più complessi come l'ereditarietà ed il polimorfismo.
Il linguaggio Java possiede un gran numero di operatori come molti altri linguaggi (il C, il C++, il Perl, il PHP, etc.). Altri linguaggi, invece, sono molto più verbosi ed usano parole chiave per eseguire le operazioni. Uno dei linguaggi più verbosi in assoluto che ho studiato è il COBOL; in questo linguaggio eseguire una somma si scrive più o meno così:
che si traduce più o meno così: aggiungi 10 a variabile. Per fortuna in Java si usa quello che per noi umani è l'operatore di somma che conosciamo sin da bambini: il simbolo "+" (più).
Per assegnare un valore ad una variabile si usa l'operatore di assegnamento (il simbolo uguale) Esempio:
Notate che la varibile beta è stata inizializzata con un numero più grande del numero massimo ammissibile per un tipo int. Per questo motivo si è dovuto aggiungere il suffisso "L" alla fine della costante numerica: in assenza del suffisso, il compilatore presume che la costante sia di tipo int e considera un errore assegnare ad un intero un valore di dieci-miliardi dal momento che il limite per gli interi è di poco superiore ai due-miliardi. Il compilatore tollera il carattere _ (underscore) in una costante numerica; esso può essere usato per aumentare la leggibilità del numero da parte degli umani ma non ha alcun significato per il compilatore.
alfa = alfa + 1 non ha alcun senso poichè non esiste soluzione per alfa che possa soddisfare la equazione, in programmazione il costrutto alfa = alfa + 1 è perfettamente legale e significa: aggiungi una unità alla variabile alfa ed ASSEGNA il risultato alla variabile alfa; il risultato del costrutto alfa = alfa + 1 è che il valore dlela variabile alfa sarà aumentato di una unità. Nel codice di cui sopra:
vi è un apparente errore poichè alla variabile delta, che è di tipo numerico a virgola mobile, ho assegnato un valore di tipo diverso: numerico intero.
Anche se a prima vista potrebbe sembrare un errore, in realtà l'assegnazione è perfettamente legale poichè il compilatore converte la costante intera in una costante in virgola mobile prima della assegnazione. Questa conversione (chiamata promozione in gergo informatico) è eseguita dal compilatore in modo automatico: in gergo informatico la operazione eseguita automaticamente dal compilatore si chiama implicit cast (=conversione di tipo implicita).
Diverso è il caso opposto: non è possibile assegnare ad una variabile di tipo intero un valore numerico in virgola mobile, e nemmeno il valore di una variabile di tipo long poichè in questi due casi vi è perdita di precisione. Tutti i seguenti esempi vengono considerati errori dal compilatore:
Vi starere chiedendo perchè l'ultima assegnazione sia illegale: in fondo la variabile gamma contiene un valore intero (100), cioè non frazionario, che può essere convertito in intero senza alcuna perdita di precisione.
Non ha importanza il valore effettivo contenuto nella variabile, ciò che conta è il suo tipo: essendo una variabile di tipo float è potenzialmente in grado di assumere un valore frazionario che NON può essere memorizzato con precisione in una variabile di tipo intero: per il compilatore questo è un errore.
Nel paragrafo precedente abbiamo imparato che ad una variabile di un certo tipo può essere assegnato un tipo di dato diverso e che il compilatore esegue automaticamente la promozione di un tipo di dato in un altro tipo se quest'ultimo possiede una capacità di memorizzazione maggiore del tipo di partenza. Per questo motivo la seguente assegnazione è legale nonostante la costante numerica sia di tipo intero ma viene assegnata ad un long
Tutti gli esempi che nel paragrafo precedente erano considerati errori dal compilatore possono invece essere accettati da quest'ultimo attraverso un explicit cast (=conversione di tipo esplicita):
I risultati del cast, tuttavia, sono imprecisi quando non totalmente errati. E' responsabilità del programmatore assicurarsi che i risultati di un cast esplicito siano coerenti con quanto atteso: il compilatore non emette alcun warning (=avvertimento) al riguardo. Nei prossimi capitoli scriveremo alcune applicazioni reali e ci saranno occasioni in cui ci attendiamo un risultato intero da una operazione di divisione la quale può produrre un risultato frazionario. In quello specifico caso il cast esplicito è assolutamente necessario poichè, in assenza, il compilatore si rifiuterebbe di eseguire la compilazione del sorgente.
Questo tipo di dato può contenere solo due valori, vero o falso. Per assegnare un valore ad una variabile di tipo boolean si usa sempre l'operatore di assegnamento ma i soli valori ammessi sono le parole chiave true e false:
Il tipo di dato char è usato per memorizzare una lettera del alfabeto umano come per esempio la lettera 'A' (a maiuscola) ma, come abbiamo avuto modo di imparare (vedi Il mondo digitale), le lettere del alfabeto non sono altro che codici numerici e, in ultima analisi, anche il tipo char è un tipo numerico.
Avendo una ampiezza di 16 bits, il tipo char non è in grado di contenere tutti i codici degli alfabeti definiti da Unicode ma il solo BMP (Basic Multilingual Plane) cioè il piano di base di Unicode che, comunque, codifica la stragrande maggioranza degli alfabeti del mondo.
Essendo char un tipo numerico, i seguenti assegnamenti sono equivalenti:
La costante char può essere espressa racchiudendo la lettera del alfabeto tra due apici singoli.
char con la stringa di caratteri: Una stringa è racchiusa tra doppi apici come per esempio: "hello" e rappresenta una sequenza di caratteri mentre il tipo char rappresenta UN SINGOLO CARATTERE. char. Le stringhe devono essere assegnate ad un tipo di dato complesso, un oggetto di tipo String. Anche ad una variabile di tipo non-primitivo viene assegnato un valore per mezzo del operatore di assegnamento ma la sintassi è leggermente diversa a causa del modo in cui la JVM gestisce la memoria allocata per gli oggetti. Mentre le variabili di tipo primitivo vengono allocate sullo stack, la memoria usata per gli oggetti è il cosidetto heap, una zona di memoria che non fà parte dello spazio degli indirizzi della applicazione ma viene assegnata alla applicazione dal sistema operativo quando la applicazione ne fà richiesta. Questa è la sintassi:
Il codice suesposto crea un oggetto data di calendario gregoriano che potrebbe rappresentare la data odierna. La parola riservata new indica al compilatore di creare l'oggetto nello heap, chiedendo al sistema operativo di allocare la memoria necessaria. E quando la variabile non serve più? Chi ci pensa a liberare la memoria allocata e renderla disponibile ad altre applicazioni? Ci pensa la JVM automaticamente per mezzo del cosidetto garbage collector (=il collezionista di spazzatura, ma questa è la prima ed unica volta che ne tento la traduzione; non sentirete mai un programmatore italiano tradurre il garbage collector di Java).
Affronteremo questo argomento in modo approfondito quando tratteremo gli oggetti Java ed in particolare nella sezione dedicata a I costruttori.
Si chiamano operatori di relazione / comparazione quegli operatori che restituiscono un valore booleano confrontando due operandi tra di loro come per esempio:
'alfa' è maggiore di 'beta'?
La risposta alla domanda (che in questo caso è una comparazione) può solo essere true ('alfa' è effettivamente maggiore di 'beta') oppure false ('alfa' è minore o uguale a 'beta'). Nella tabella seguente sono riepilogati gli operatori di relazione/comparazione definiti in Java:
| Operatore | Significato | Esempio |
|---|---|---|
| < | minore di | boolean r = a < b |
| <= | minore/uguale | boolean r = a <= b |
| > | maggiore di | boolean r = a > b |
| >= | maggiore/uguale | boolean r = a >= b |
| == | uguale a | boolean r = a == b |
| != | diverso da | boolean r = a != b |
Appartengono a questa categoria gli operatori che prendeno come dati in ingresso uno o due operandi di tipo booleano e danno un risultato booleano. Vi sono tre operatori logici in Java:
| Operatore | Simbolo Java | Significato |
|---|---|---|
| AND | && | congiunzione logica |
| OR | ιι | disgiunzione logica |
| NOT | ! | negazione logica |
Prende come dati due operandi booleani e restituisce il risultato (anch'esso booleano) secondo la seguente regola: se entrambi gli operandi sono true allora il risultato è true. In tutti gli altri casi il risultato è false. Per esempio, supponiamo di avere un vassoio di frutta di vario genere e che voglio istruire il computer a scegliere una mela rossa. Quindi è necessario testare due condizioni:
Il computer, nel scegliere il frutto per me dovra testare tutte e due le condizioni e poi unirle logicamente in modo che il risultato finale sia true se il frutto in questione soddisfa le mie esigenze: solo se entrambe le condizioni sono true, mi darà il frutto. Possiamo paragonare l'operatore AND alla nostra congiunzione e quando usiamo una espressione condizionale. Esempio:
mangierò un frutto solo se esso è una mela ED è rosso
Il codice Java corrispondente è questo:
L'operatore OR prende come dati due operandi booleani e restituisce il risultato (anch'esso booleano) secondo la reguente regola: se almeno uno degli operandi è true allora il risultato è true. Se entrambi gli operandi sono false, il risultato è false.
Torniamo al vassoio di frutti di vario genere dell'esempio precedente e cambiamo l'operatore logico in OR che può essere paragonato alla nostra congiunzione OPPURE:
mangerò un frutto se esso è una mela OPPURE se è rosso.
Anche in questo caso il computer testerà le due condizioni singolarmente e poi le unirà secondo la disgiunzione logica:
in questo caso, però, il risultato finale è completamente diverso: mangierò una mela rossa, certo; ma anche una mela verde come pure una pera rossa (almeno una delle due condizione è soddisfatta); non mangierò una pera verde, perchè in questo caso entrambe le condizioni sono false (frutto è mela? false, frutto è rosso? false).
L'operatore logico NOT (il cui simbolo è il punto esclamativo) prende come dato un solo operando booleano e ne restituisce la negazione logica ossia se l'operando è true, restituisce false; se l'operando è false, restituisce true. Sempre restando in tema del vassoio di frutta, l'operatore NOT equivale alla nostra negazione non come per esempio:
mangerò un frutto se NON è una mela.
In questo caso, il test su uno dei frutti del vassoio restituirà, per esempio per una pera, il risultato false e quindi non verrebbe mangiato. Tuttavia, la negazione logica NOT ne cambia il risultato finale in true e, quindi, il frutto sarà selezionato per essere mangiato.
Per rendere più chiaro il concetto osservate i due operatori di confronto (vedi Operatori di relazione):
== (uguale a) != (diverso da)e seguite il ragionamento: l'operatore diverso da può essere anche chiamato: non uguale a; (not equal to) ed in effetti, il primo char di questo operatore è proprio il NOT.
Gli operatori aritmetici del Java sono la addizione (+), la sottrazione (-), la divisione (/), la moltiplicazione (*), il modulo (%) e i due operatori di incremento (++) e decremento (–). Sicuramente, i primi quattro non richiedono spiegazioni data la loro familiarità nell'uso quotidiano e vengono usati esattamente allo stesso modo col quale siamo abituati. Ecco alcuni esempi:
L'operatore modulo (il cui simbolo è il segno di percentuale) viene utilizzato per restituire il resto di una divisione intera come per esempio: dieci diviso tre. Il quoziente è 3 col resto di 1: Il modulo, in questo caso, è 1 (il resto). Questo è il codice corrispondente:
Infine, gli operatori di incremento e decremento vengono utilizzati per incrementare o decrementare di una unità una variabile intera. Esempio
Vi è da osservare che l'uso dell'operatore di incremento è particolarmente efficiente perchè il microprocessore ha nel suo vocabolario una istruzione dedicata appositamente all'incremento ed al decremento di una unità.
Gli operatori di bitwise sono molto simili agli operatori logici ed infatti ne condividono la semantica ma, invece di operare a livello di condizione, operano a livello di ogni singolo bit che compone una variabile di tipo numerico intero (quindi si, anche il tipo char!).
Riprendiamo il concetto del operatore logico AND e vediamo quali sono i risultati finali a seconda del verificarsi delle due condizioni che rappresentano i due operandi booleani:
| condizione-1 | condizione-2 | risultato finale |
|---|---|---|
| true | true | true |
| false | true | false |
| true | false | false |
| false | false | false |
Ora sostituite alla parola true la cifra binaria 1 ed alla parola false la cifra binaria ZERO ed otterrete la tabella dei risultati degli operatori di bitwise per ogni singolo bit di una variabile intera. Gli operatori di bitwise sono: AND (&), OR (|), e XOR (^) detto eXclusive OR (=OR esclusivo). Nella tabella seguente sono mostrati i risultati per ogni operatore di bitwise confrontando i singoli bit di due variabili:
| bit-1 | bit-2 | AND | OR | XOR |
|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 0 |
| 0 | 1 | 0 | 1 | 1 |
| 1 | 0 | 0 | 1 | 1 |
| 0 | 0 | 0 | 0 | 0 |
Facciamo un esempio:
Il pezzo di codice suesposto esegue un bitwise AND tra due interi, il 40 ed il 12. Il risultato di questa operazione è 8 e si ottiene con questo procedimento:
decimale binario
40 101000
AND
12 001100
-- ------
8 001000
Vi chiederete che utilità possa avere una simile operazione. Ebbene, più avanti in questo stesso tutorial avremo la necessità di elaborare i singoli bit delle variabili (vedi I flags di disegno e Miglioriamo il crivello).
Se ben ricordate, avevamo lasciato in sospeso l'argomento degli operatori di assegnamento. Abbiamo affermato che essi sono molti ma, in realtà, solo uno è stato descritto nei capitoli precedenti (vedi L'operatore di assegnamento). Ebbene, ora è arrivato il momento di affrontarli poichè essi sono strettamente correlati agli operatori aritmetici.
Facciamo un esempio concreto: supponiamo di avere una variabile numerica alfa e di voler aggiungere un certo valore, diciamo 100 unità. Il codice corrispondete che conosciamo finora sarebbe il seguente:
Un metodo più elegante è quello di usare l'operatore di somma-ed-assegnazione il cui simbolo è il +=:
Anche gli altri operatori aritmetici definiscono gli stessi operatori di operazione-ed-assegnamento:
| Simbolo | Esempio | Significato |
|---|---|---|
| += | alfa += 100 | alfa = alfa + 100 |
| -= | alfa -= 100 | alfa = alfa - 100 |
| *= | alfa *= 100 | alfa = alfa * 100 |
| /= | alfa /= 100 | alfa = alfa / 100 |
| %= | alfa %= 100 | alfa = alfa % 100 |
Gli operatori di scorrrimento lavorano a livello dei bit di una variabile e consentono di scorrere i bits verso sinistra (left shift) e verso destra (right shift). L'operatore prende due operandi:
Il seguente codice:
dà come risultato il valore 40 in alfa e 2 in beta. Tutti i bit dell'operando di sinistra (10) vengono fatti scorrere di due posizioni ed il risultato viene assegnato alle variabili alfa e beta. Per comprendere meglio il risultato usiamo il formato binario:
decimale binario left-shift right-shift
-------- ------- ---------- -----------
10 1010 1010-- --10
Come avrete notato, lo shift verso sinistra di due posizioni equivale alla moltiplicazione del operando per quattro, mentre lo shift verso destra di due posizioni equivale alla divisione per quattro. Vi sembra un fatto curioso? Lo scorrimento verrà utilizzato in una applicazione reale più avanti in questa stessa guida e capirete perchè non è assolutamente un fatto curioso. (vedi Miglioriamo il crivello ).
Le strutture di controllo permettono al computer di eseguire azioni diverse a seconda del verificarsi o meno di determinate condizioni oppure permettono di eseguire le stesse azioni in modo ricorsivo per un numero prederminato di volte oppure fino al verificarsi di una certa condizione. Divideremo perciò le strutture di controllo in strutture condizionali e strutture iterative.
Questa istruzione fà parte delle strutture condizionali e consente al programmatore di eseguire azioni diverse a seconda della condizione stabilita dal programmatore. La sintassi generale della istruzione if è la seguente:
Se dovessimo tradurre in italiano la sintassi generale della istruzione if essa apparirebbe più o meno così:
SE ( condizione-1 è vera ) allora esegui azione-1 ALTRIMENTI SE ( condizione-2 è vera ) allora esegui azione-2 ALTRIMENTI esegui azione-3
Vediamo un esempio: supponiamo di avere un negozio virtuale, il cliente acquista i prodotti e, alla fine, conferma l'ordine. A seconda del totale acquistato il programma calcola uno sconto in funzione dell'importo della spesa: lo sconto sarà del 15% per ordini superiori a 500,00 euro, del 10% per ordini tra i 200 ed i 500 euro e del 5% per ordini inferiori ai 200 euro. Il codice per il calcolo della percentuale di sconto sarà il seguente:
Un'altro esempio di uso della condizione if è il seguente, molto più semplice, che non richiede l'uso della else: prendiamo ancora l'esempio del carrello ma questa volta, poichè siamo in periodo di crisi economica, applichiamo uno sconto solo per ordini superiori a EUR 500,00 nella misura del 6 percento.
Il codice sopraesposto è errato: bentinteso, il codice è sintatticamente perfetto e compila senza alcun errore. Si tratta però di un errore logico, in termini informatici conosciuto con il nome di bug (=letteralmente: insetto, bacherozzo). Se analizziamo attentamente il flusso del programma scopriamo l'errore: la istruzione di attribuzione dello sconto viene eseguita solo se l'ordine è maggiore o uguale a 500. Nel caso l'ordine sia minore di 500, non esiste alcuna istruzione per assegnare un valore alla variabile sconto la quale, pertanto, risulta indeterminata. Essa potrebbe addirittura essere uguale al 100% per la felicità del compratore. Il codice corretto è questo:
Sulla istruzione if vi sono alcune note che il lettore deve sapere prima di continuare con le altre strutture:
else è facoltativa, essa contiene il blocco di istruzioni che vengono eseguite se la condizione della if viene valutata falsa. if ed else ma non è necessario che esse siano bilanciate; in parole povere è possibile che alcune if abbiano la corrispondente else ed altre no. Porre particolare attenzione a bilanciare le parentesi graffe: per ogni if ed else ci devono essere una coppia di parentesi graffe. if che per la else dovrebbe essere rientrato di un certo spazio rispetto al resto del corpo della funzione. Nel caso di if nidificate si dovrebbe rientrare ulteriormente il testo; benchè per il compilatore non abbia alcuna utilità, per gli esseri umani il codice risulta molto più leggibile.Ecco alcuni esempi di codice che usa la istruzione condizionale if:
Commentiamo quello che accade nel primo esempio: la prima if testa se alfa è minore di beta e, in caso affermativo viene testato se beta è minore di gamma; se anche questo test viene valutato true allora alfa viene incrementata di 10 unità. Se anche uno solo di questi test restituisce false, alfa manterrà il suo valore originale.
Avremmo potuto scrivere il costrutto in maniera diversa, forse più intuitiva? Certo, usando la congiunzione logica AND che restituisce true solo se TUTTE le condizioni restituiscono true.
L'ultimo esempio di if è esattamente uguale al numero 2 (perfino le indentazioni sono le stesse) ma senza le parentesi graffe: questo per far notare al lettore l'importanza di esse:
Sebbene a prima vista il costrutto è uguale identico, l'assenza delle parentesi graffe fà in modo che la else si riferisca all'ultima if, la più interna, e non a quella alla quale il programmatore intendeva riferirsi. Il risultato del codice sopraesposto è che l'incremento di 10 unità viene gestito correttamente come nel primo esempio ma la diminuzione di 10 unità avviene nel caso l'ultimo test (e non il primo) fallisca; cioè non se alfa è maggiore/uguale a beta ma se beta è maggiore/uguale a gamma.
La dimenticanza della coppia di parentesi graffe è uno degli errori in cui incorrono spesso i neofiti. Si raccomanda ancora una volta di prendere l'abitudine di scrivere SEMPRE l'istruzione if con la coppia di parentesi graffe anche se l'azione condizionale è composta da una sola istruzione. Questo non solo rende più leggibile il codice ma previene errori come quello illustrato poco sopra.
Anche questo costrutto fà parte delle strutture di controllo di tipo condizionale ed è usato quando la condizione da testare è sempre la stessa e viene confrontata con valori diversi che però devono essere costanti. La sintassi generale di questo costrutto è la seguente:
A titolo di esempio supponiamo di avere una variabile intera alfa che può assumere i valori da 1 a 7 con il valore "1" che indica lunedì ed il "7" che indica domenica.
Vogliamo visualizzare il giorno della settimana espresso in lettere anzichè in numeri pertanto definiamo una variabile di tipo String (un tipo complesso che rappresenta una sequenza di caratteri) che assumerà il valore in lettere del giorno della settimana.
Il codice è il seguente:
Il codice è di facile lettura: usiamo il costrutto if .. else per testare l'uguaglianza di alfa con i sette valori ammessi dalle specifiche del programma e, nel caso una di queste sette condizioni venga soddisfatta usiamo l'azione corrispondente per assegnare il valore a in lettere a weekday. Quando una delle sette condizioni è soddisfatta, nessuna altra verrà più valutata poichè la clausola else viene valutata solo nel caso la condizione della if risulta false.
Nel caso TUTTE e sette le condizioni risultino false, allora la variabile alfa contiene un valore fuori dall'intervallo ammesso (da 1 a 7) e perciò verrà eseguita l'azione dell'ultima else, la quale imposta la stringa ad un messaggio di errore.
Premesso che il codice suesposto è perfettamente legale e funzionante l'uso di un costrutto switch in questo caso è assolutamente preferibile rispetto ad una serie di istruzioni condizionali sia per motivi di efficienza - in quanto la condizione viene valutata una sola volta nella switch - che di leggibilità.
Riscriviamo pertanto lo stesso pezzo di programma ma, questa volta, usando il costrutto switch; noterete la migliore leggibilità del codice:
Il costrutto switch valuta la condizione solo una volta, all'inizio del ciclo. Successivamente, confronta il risultato con le costanti dichiarate nei blocchi case e, se il valore della condizione è uguale a quello del case esegue tutte le istruzioni fino alla parola chiave break. Alla fine del costrutto, è facoltativo ma raccomandato (diciamo FORTEMENTE RACCOMANDATO) di usare un blocco default che viene eseguito nel caso la condizione non si è verificata in nessun blocco case: nel nostro esempio la istruzione del blocco default imposta un messaggio di errore nella stringa che dovrebbe contenere il giorno della settimana in lettere.
A partire da Java versione 14 il costrutto switch può essere utilizzato come una espressione: questo significa che se il blocco case restituisce un valore, il costrutto può essere scritto in questo modo, eliminando la necessità delle istruzioni break:
Con il termine ciclo si indica la capacità di eseguire una o più istruzioni in modo ricorsivo al verificarsi o meno di una determinata condizione. Possiamo paragornarli a dei comandi tipo:
cammina per 100 passi
procedi fino alla fine della strada
Lo schema di un ciclo in Java è il seguente:
Il blocco_di_codice contiene le istruzioni da eseguire finchè la condizione risulta vera. Il blocco di codice da eseguire deve essere racchiuso tra una coppia di parentesi graffe a meno che il blocco non sia composto da una sola istruzione: in questo caso le parentesi graffe sono facoltative.
Il costrutto for fà parte delle strutture di controllo di tipo iterativo ed è usato per eseguire una istruzione o gruppo di istruzioni per un numero predeterminato di volte. La sintassi generale di questo costrutto è la seguente:
Il ciclo for provvede ad inizializzare una variabile e continua ad eseguire le istruzioni comprese nel blocco di codice racchiuso tra le parentesi graffe fino a che condizione risulta vera. Ad ogni ciclo, viene eseguita la istruzione step.
A titolo di esempio supponiamo di dover calcolare il fattoriale di un numero intero positivo num. Poichè il fattoriale di un numero n è il prodotto dei primi n numeri naturali,
si tratta di un ciclo con un numero di iterazioni ben definito, che si adatta quindi perfettamente al ciclo for
Anche se il codice non è particolarmente complesso, vale la pena spendere due parole di spiegazione.
Innanzitutto inizializziamo il risultato a 1; questo perchè usiamo questa variabile per contenere tutti i prodotti di tutti i numeri da 2 a num compreso. Sarebbe stato un errore inizializzarla a ZERO poichè il risultato sarebbe stato sempre ZERO.
Il ciclo for inizia con l'inizializzazione di una variabile di comodo intera di nome i che parte da 2; anche in questo caso partire da 1 non sarebbe stato un errore ma inutile poichè qualsiasi numero moltiplicato 1 dà se stesso. L'istruzione del ciclo moltiplica il risultato intermedio risultato per la variabile del ciclo i e ne memorizza il prodotto nel risultato stesso. Alla fine delle istruzioni, il ciclo for esegue l'istruzione step che incrementa di 1 la variabile di comodo i. Prima di proseguire con un altra iterazione viene valutata la condizione che i sia minore o uguale num.
Supponiamo di voler calcolare il fattoriale di 5 il cui risultato è 120 e analizziamo passo-passo tutte le iterazioni del ciclo for ed il valore delle variabili ad esso associate:
iterazione azione valore 'risultato' valore 'i'
----------- ------ ------------------ ----------
init 1 2
prima 2 * 1 2 3
seconda 2 * 3 6 4
terza 6 * 4 24 5
quarta 24 * 5 120 6
Alla fine della quarta iterazione, la variabile i vale 6 quindi la condizione che i sia minore o uguale a num non è più soddisfatta ed il ciclo si arresta.
Vi è da osservare che può accadere che il ciclo for non esegua nemmeno una volta le istruzioni del suo blocco: questo accade se la condizione risulta false già alla prima iterazione. In pratica, la condizione viene testata PRIMA di eseguire le istruzioni del blocco: se essa risulta false, il ciclo termina prima ancora di cominciare.
Nel nostro esempio del fattoriale, se il numero in ingresso è ZERO o 1, la condizione è subito false poichè i, inizializzata a 2, è già maggiore di num.
Tuttavia, il risultato è corretto perchè, per convenzione, il fattoriale di ZERO e di "1" è "1" esattamente il valore al quale è stato inizializzata la variabile risultato.
Questi due costrutti sono in realtà due versioni diverse della stessa istruzione. Si tratta di una struttura di controllo di tipo iterativo in quando consente di eseguire le istruzioni del blocco racchiuso tra due parentesi graffe per un numero indeterminato di volte, fino a quando la condizione viene valutata true. La sintassi generale del costrutto while è la seguente:
Mentre quella del costrutto do..while è la seguente:
La differenza tra i due costrutti è che nella prima versione la condizione viene valutata immediatamente, PRIMA di eseguire le istruzioni del blocco. Se la condizione è false, nessuna istruzione del blocco viene eseguita. Nella seconda versione, di contro, il ciclo esegue sempre ALMENO UNA VOLTA le istruzioni del blocco ed alla fine di ogni iterazione viene valutata la condizione.
Riprendiamo l'esempio del calcolo del fattoriale di un numero n per scrivere un esempio di ciclo while. Prima di tutto osserviamo che il costrutto do..while non si applica in questo caso perchè se il numero da fattorializzare è ZERO oppure 1, il ciclo non deve essere eseguito ma nella do..while, come riportato sopra, il ciclo viene eseguito almeno una volta.
Come possiamo osservare, abbiamo semplicemente scritto il ciclo for in un altro modo: abbiamo inizializzato la variabile i all'esterno del ciclo ed abbiamo scritto l'istruzione di incremento di essa alla fine del ciclo, esattamente come accade nella for.
Per concludere, abbiamo potuto notare che il programmatore può usare diversi costrutti per ottenere lo stesso risultato e che tutti questi metodi sono funzionanti. L'uso di un costrutto piuttosto che un altro dipenderà pertanto dalla scelta del programmatore che, tuttavia, dovrebbe tenere in considerazione i requisiti di efficienza e leggibilità del codice.
Nelle sezioni che seguiranno saranno sviluppati alcuni programmi reali e l'autore di questa guida userà costrutti diversi a seconda dei casi: saranno illustrate al lettore le ragioni che hanno determinato la scelta dei vari costrutti in modo che egli possa acquisire una certa familiarità con questo concetto.
Per quanto riguarda il fattoriale il ciclo for è più indicato: esso infatti è specifico quando il numero di iterazioni è conosciuto a priori mentre il ciclo while è specifico quando il numero di iterazioni è sconosciuto nel momento in cui si entra il ciclo.
Ogni programma per computer è composto da una serie di istruzioni che il computer esegue partendo dalla prima istruzione e proseguendo nell'ordine in cui le istruzioni sono state scritte. Abbiamo già visto che le strutture di controllo permettono però di cambiare l'ordine di esecuzione: nelle strutture condizionali alcune istruzioni vengono eseguite mentre altre no; nelle strutture iterative un blocco di istruzioni può essere eseguito due o più volte oppure anche nessuna.
Un problema in siffatta organizzazione nasce dal fatto che alcune azioni che il programma deve eseguire potrebbero ripetersi molte volte nel corso del programma e che queste azioni possono essere particolarmente complesse e composte da decine o addirittura centinaia di istruzioni: doverle riscrivere ogni volta che servono sarebbe un incubo. Usare il copia-e-incolla potrebbe essere una soluzione ma... ci deve essere qualcosa di meglio!
Le funzioni permettono di scrivere il codice per eseguire una data operazione una volta per tutte ed assegnare a questo blocco di codice un identificatore (vedi Gli identificatori). Quando è necessario ottenere il risultato di questo blocco di codice è sufficente richiamare la funzione per mezzo del suo identificatore. Questa operazione viene detta function call (=richiamo di funzione). Schematicamente, un richiamo di funzione opera nel modo seguente:
Il corpo principale del programma (a sinistra) esegue le istruzioni nell'ordine in cui si trovano, quando arriva all'istruzione di chiamata di funzione, il controllo salta alla prima istruzione della funzione e prosegue eseguendo tutte le istruzioni della funzione nell'ordine in cui sono state scritte fino alla fine della funzione che provoca il rientro. Il controllo passa perciò alla istruzione immediatamente successiva a quella che ha richiamato la funzione, nel corpo principale.
Poichè una funzione può richiamare una altra funzione e così via distingueremo due casi di funzioni:
Per fare un esempio pratico, visto che abbiamo scritto un piccolo algoritmo che calcola il fattoriale di un numero, perchè non inserirlo in una funzione che possiamo richiamare molto più facilmente?
Prima di proseguire è necessario puntualizzare che in Java le funzioni non esistono, o meglio, non è che non esista il concetto di richiamo del codice; il concetto esiste, semplicemente si chiama in modo diverso: il pezzo di codice richiamabile si chiama metodo e non funzione.
In altri linguaggi, come il "C" ed il "PHP" si chiamano effettivamente funzioni, mentre in "Pascal" si chiamano procedure. Per esempio, questa è la definizione di una procedura in "Pascal".
Mentre in "C" la sintassi è la seguente:
In "PHP" la sintassi è ancora diversa:
E in Java? Java è un linguaggio fortemente orientato agli oggetti ed è impossibile prescindere da essi: tutto in Java deve essere definito all'interno di un oggetto ed anche i metodi (l'equivalente Java delle funzioni) non sfuggono a questa regola. Non possiamo quindi avere funzioni o procedure in Java? Si, possiamo averle ma si chiamano metodi statici. La sintassi generale per definire un metodo in Java è la seguente:
Analizziamo i vari componenti:
static, con cui si definisce un metodo statico il quale assomiglia ad una funzione void fattoriale. Il programmatore è libero di scegliere qualunque identificativo per il nome del metodo purchè rispetti i vincoli stabiliti per gli identificatori (vedi Gli identificatori) fattoriale l'argomento è il numero intero del quale calcolare il fattoriale return.Quindi il metodo che calcola il fatotoriale di un qualsiasi numero intero potrebbe essere definito come segue:
Il nostro nuovo metodo fattoriale può essere richiamato da qualsiasi punto del nostro programma semplicemente richiamandone il nome e passandovi l'argomento reale di cui calcolare il fattoriale. Esempio:
Il concetto di array (=tabella) è quello di un insieme ordinato di elementi dello stesso tipo. Ogni elemento dell'array è indirizzato tramite un indice numerico che parte da ZERO fino al numero di elementi dell'array meno uno. Schematicamente, una array può essere rappresentata in questo modo:
indice => 0 1 2 3 4 5 6 7 8 9
__ __ __ __ __ __ __ __ __ __
elemento |__|__|__|__|__|__|__|__|__|__|
La rappresentazione si riferisce ad una array di 10 elementi che possono essere indirizzati attraverso il loro indice (un intero senza segno) il cui valore và da ZERO a NOVE.
Un array è una variabile a tutti gli effetti e si definisce in questo modo:
Le parentesi quadre dopo il tipo di dato contenuto nella array, informano il compilatore che identificativo non è una semplice variabile di quel tipo ma una collezione di variabili di quel tipo. La sola definizone di una array non vuol dire che essa contenga qualcosa. Prima di poter usare una array è necessario allocare lo spazio per essa informando il compilatore di quanti elementi è composta la array. Il seguente spezzone di ocdice alloca una array di interi di cento elementi:
La parola chiave new alloca lo spazio di memoria necessario a contenere cento elementi di tipo intero. Per assegnare un valore agli elementi della array si usa l'operatore di subscript (le parentesi quadre) con indicato il numero del elemento (detto indice) da impostare. Esempio:
Ho assegnato il valore 10 al undicesimo elemento della array interi. Ma come? Non al decimo elemento? NO, poichè gli indici degli array cominciano da ZERO, quindi l'indice 10 è l'undicesimo elemento. Probabilmente l'intenzione del programmatore era quella di inserire nella array tutti i numeri interi da ZERO a 99. In questo caso un ciclo for è quello che ci serve:
Notate che la condizione del ciclo for è che i sia MINORE di 100 pertanto il ciclo si fermerà a 99.
Ma cosa succede se, per sbaglio, sforo la capacità della array? Java, contrariamente ad altri linguaggi come C e C++, esegue un controllo sulla congruità degli indici quando si accede agli elementi di una array: se l'indice è fuori range, il programma termina con una eccezione, a meno che essa non venga intercettata - affronteremo questo argomento in Le eccezioni.
La capacità di una array viene stabilita nel momento in cui essa viene allocata per mezzo del operatore new. Java mantiene nei suoi dati interni la lunghezza di una array, dato indispensabile per eseguire il controllo di congruità degli indici negli accessi agli elementi. Per ogni array, Java definisce l'attributo length che può essere usato dal programmatore per conoscere in ogni momento la capacità di una qualsiasi array. Possiamo pertanto riscrivere il codice suesposto in modo diverso:
Anzichè ripetere il valore 100 nel ciclo for, usiamo la capacità della array stessa per stabilire la condizione di fine ciclo.
Una array può essere allocata ed inizializzata in un sol colpo, per mezzo di una lista di inizializzazione. Esempio:
Il codice suesposto alloca una array di sette elementi di tipo stringa ed inizializza ogni elemento in successione partendo dal primo elemento (indice ZERO) con le stringhe indicate nella lista di inizializzazione. La lista di inizializzazione deve contenere elementi del tipo consono al tipo di array e deve essere racchiusa tra una coppia di parentesi graffe. Gli elementi devono essere separati dal carattere virgola.
Potremmo usare le nostre nuove competenze per riscrivere il codice che ci restituisce il giorno della settimana in lettere partendo da un numero in cui 1 identifica lunedì e 7 identifica domenica. Scriveremo pertanto una funzione (un metodo statico, in Java) riusabile in qualsiasi punto del programma che restituisce la stringa:
Dopo aver allocato ed inizializzato la array delle stringhe che descrivono il giorno della settimana, inizializziamo la stringa da ritornare contenuta nella variabile giorno con il messaggio di errore.
Successivamente verifichiamo che il codice fornito come argomento abbia un valore compreso tra 1 e 7 e solo se questa condizione è vera assegnamo alla variabile da ritornare giorno il valore del elemento della array corrispondente al codice diminuito di UNO. E' necessario diminuire il valore del codice perchè il codice 1 corrisponde a "lunedì" che è il primo elemento della array; esso però è ad indice ZERO.
Una array può a sua volta contenere una altra array. Si tratta in questo caso di una array bidimensionale, cioè a due dimensioni. Poichè una array può contenere una altra array, anche quest'ultima può contenere una altra array, e così via. Le array in Java possono essere allocate con un numero di dimensioni n. La sintassi per allocare una array multidimensionale è la seguente:
dove dim-n è la capacità di ogni dimensione della array. Vogliamo migliorare il metodo che restituisce il giorno della settimana in lettere dando la possibilità al programmatore di scegliere la lingua che preferisce tra italiano, inglese e tedesco. Per questo scopo creeremo una array bidimensionale in cui:
In questo metodo useremo l'indice ZERO della seconda array per restituire il messaggio di errore. Il codice è il seguente:
Il codice è abbastanza facile da leggere, tuttavia spendo qualche parola. Dopo aver allocato ed inizializzato la array a due dimensioni, verifico che il parametro lingua sia nel range stabilito (da 0 a 2) e, se non lo è, imposto lo ZERO come default (italiano). Successivamente verifico che il parametro codice (del giorno) sia entro i limiti stabiliti (da 1 a 7) e, se non lo fosse, lo imposto a ZERO che è l'indice del messaggio di errore nella lingua selezionata.
Infine, ritorno il valore del elemento a indice lingua per la prima dimensione e codice per la seconda dimensione.
Se non vi è chiaro il concetto della array a due dimensioni potete immaginarla come una tabella in cui le colonne sono la prima dimensione (la lingua) e le righe la seconda dimensione (il codice del giorno). Così, per indirizzare il giorno 4 della settimana (giovedì) in inglese, gli indici sono [1][4].
| indici | 0 | 1 | 2 |
|---|---|---|---|
| 0 | ERRORE... | ERROR.... | FEHLER... |
| 1 | lunedì | monday | montag |
| 2 | martedì | tuesday | dienstag |
| 3 | mercoledì | wednesday | mittwoch |
| 4 | giovedì | thursday | donnerstag |
| 5 | venerdì | firday | freitag |
| 6 | sabato | saturday | samstags |
| 7 | domenica | sunday | sonntag |
Il lettore ha imparato in I tipi primitivi che i caratteri degli alfabeti umani vengono rappresentati dal tipo char il quale, però, può contenere uno, ed un solo carattere dell'alfabeto, per esempio la A maiuscola. Per memorizzare una parola avremmo bisogno di molti caratteri e, quindi, una array di char; per esempio:
In Java una stringa di caratteri viene rappresentata da una classe: la classe String; una classe è un tipo di dato complesso che ancora non abbiamo affrontato ma faccio una eccezione per la classe String. Per definire una parola, una frase o addirittura un testo intero non devo usare una array di caratteri come visto in precedenza (sarebbe un incubo) ma posso usare la classe String in modo naturale ed intiutivo:
Se il testo deve essere memorizzato in più righe, posso aggiungere il delimitatore di fine-riga alla stringa:
A partire da Java versione 13 è stato aggiunto al linguaggio una nuova feature (=funzionalità) denominato text-blocks (=blocchi di testo) che semplifica enormemente la definizione di stringhe che si estendono su più righe. La sintassi del blocco di testo è la seguente:
Si noti la presenza di due serie di delimitatori (3 caratteri doppio-apice) sulla prima e sull'ultima riga. Questi 3 caratteri doppio-apice consecutivi indicano al compilatore Java che si stà dichiarando un blocco di testo Java. Entrambi i delimitatori devono essere posizionati su righe separate, sopra e sotto il testo effettivo da includere nel blocco di testo. Solo il testo sulle righe comprese tra quelle con i caratteri di delimitazione viene incluso nella stringa Java risultante.
Nell'esempio mostrato in precedenza, il testo tra le due righe con i delimitatori è stato rientrato in modo da trovarsi nella stessa posizione orizzontale dei delimitatori. In altre parole, il testo tra i delimitatori inizia nella stessa posizione orizzontale dei delimitatori. Questo è stato fatto puramente per motivi di formattazione del codice. Potremmo non volere che tutti quei caratteri di rientro (spazi o tabulazioni) facciano parte della stringa effettivamente creata da questo blocco di testo.
Quello che accade in realtà è che il compilatore Java rimuove tutti i caratteri di rientro dalla stringa prodotta dalla dichiarazione del blocco di testo. Il compilatore Java sa quanti caratteri di rientro rimuovere esaminando l'ultima riga del blocco di testo, ovvero la riga che contiene gli ultimi 3 caratteri di delimitazione. Il rientro dei caratteri di virgolette su quest'ultima riga determina quanti caratteri di rientro il compilatore Java rimuove dal testo all'interno del blocco di testo. Ecco 3 esempi di blocchi di testo Java che utilizzano diversi livelli di indentazione, controllati dall'indentazione degli ultimi 3 caratteri di virgolette delimitatrici:
Risultato:
Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet
Argomento precedente - Argomento successivo - Indice Generale