Java Tutorial - Parte 1 0.1
Un tutorial per esempi
Caricamento in corso...
Ricerca in corso...
Nessun risultato
Un programma utile (MCM / MCD)

E' arrivato il momento di scrivere un programma utile. E' un programma piuttosto semplice ma comunque vedremo alcuni aspetti della programmazione che passano di solito in secondo piano, specialmente tra i neofiti (ma anche tra i più esperti ... ):

  • scrivere una suite di test
  • scovare e correggere i bugs
  • gestire le eccezioni
  • documentare i sorgenti

Il programma usa solo metodi statici quindi essi possono essere paragonati alle funzioni di linguaggi procedurali come il "C". Usare Java con metodi statici significa non sfruttarne la caratteristica più potente: la programmazione orientata agli oggetti ma questo aspetto verrà approfondito nei prossimi capitoli.

Questo programma a riga di comando (CLI) calcola il Massimo Comune Divisore (MCD) ed il Minimo Comune Multiplo (MCM) tra due numeri interi positivi che chiameremo alfa e beta. Convenzionalmente, si indicano in questo modo:

  MCD(alfa,beta) = risultato
  MCM(alfa,beta) = risultato

L'algoritmo di Euclide

Sappiamo dalla scuola che per calcolare il MCD ed MCM è necessario ridurre i numeri a fattori primi e prendere:

  • per il MCD i soli fattori comuni col minor esponente
  • per il MCM tutti i fattori, presi una sola volta, col massimo esponente

La fattorizzazione dei numeri però è un compito lungo e complesso e per questo semplice programma non vogliamo qualcosa di difficile. Per fortuna, esiste un metodo semplice ed efficace per calcolare il MCD ed MCM tra due numeri: l'algoritmo di Euclide.
Lo svantaggio di questo metodo è che esso è in grado di calcolare MCD ed MCM tra soli due numeri e non di più. L'algoritmo per MCD è davvero semplice e si può sintetizzare nei seguenti passi:

  1. siano alfa e beta due numeri interi tali che 0 ≤ beta < alfa
  2. se beta è uguale a ZERO allora il MCD è alfa
  3. se beta è diverso da ZERO:
    • calcola il resto della divisione di alfa con beta
    • poni alfa uguale a beta
    • poni beta uguale a resto
    • ricomincia dal punto 2.
  4. alla fine del ciclo, ritorna alfa

Per quanto riguarda il MCM, esso è ancora più semplice; per calcolarlo secondo l'algoritmo di Euclide, è sufficente moltiplicare i due numeri tra di loro e poi dividere il risultato per il loro MCD. Detto in altro modo:

MCM(a,b) = a * b / MCD(a,b)

Al seguente link l'algoritmo di Euclide potete trovare degli esempi e la dimostrazione che l'algoritmo funziona.

I files sorgente

Nella sottocartella javatutor1/projects/mcmd troverete i files sorgente di tutto il codice descritto in questo capitolo. Di seguito l'elenco dei files ed una loro breve descrizione:

Nome del file Descrizione
mcmd01.java la prima stesura del programma
mcmd02.java una ottimizzazione del metodo 'mcm'
mcmd03.java viene implementata una suite di test
mcmd04.java input dei dati: invito all'utente a fornirli
mcmd05.java input dei dati: la command-line
mcmd06.java la gestione delle eccezioni ed i bugs
mcmd07.java la documentazione ed il tool 'javadoc'
mcmd08.java dati di input di tipo 'long'
mcmd08bis.java risolto overflow in MCM con input 'long'
docoptions.txt usato per la documentazione
overview.txt usato per la documentazione

Scriviamo i metodi mcm e mcd

Aprite nel vostro editor di testi preferito il file sorgente mcmd01.java ed analizziamolo iniseme. Esso si compone dei seguenti elementi:

/* mcmd01.java - calcola il MCD / MCM tra due numeri
* ... omissis ...
* questo è solo un blocco di commento in stile GNU GPL
*/
class mcmd
{
static int mcd( int alfa, int beta )
{
... il metodo che calcola il MCD ...
}
static int mcm( int alfa, int beta )
{
... il metodo che calcola il MCM ...
}
public static void main( String[] args )
{
... il metodo principale ovvero l'entry point del programma
}
}

La prima osservazione che noterete sicuramente è che il metodo main è stato dichiarato con l'attributo public mentre gli altri due metodi non lo sono. La causa di questo è che il metodo main deve avere l'attributo public mentre per qualsiasi altro metodo l'attributo public non è necessario a meno che esso non debba essere richiamato dall'esterno della classe. Poichè il nostro piccolo programmino si compone di una sola classe, non c'è alcun bisogno di dichiarare i metodi pubblici.

Il metodo mcd

L'algoritmo per il MCD è un algortimo ricorsivo: si tratta di dividere uno dei due numeri per l'altro continuamente, fino a che il quoziente della divisione non risulti ZERO. Per un ripasso della sintassi dei cicli iterativi vedi I cicli. Poichè il numero di cicli da eseguire non è conosciuto a priori, il ciclo for non è molto indicato per questo algoritmo e, quindi, opteremo per uno dei due cicli while oppure do..while.
L'algoritmo prevede che se beta è ZERO, allora il MCD è già calcolato ed corrisponde ad alfa quindi non è assolutamente necessario entrare nel ciclo delle divisioni: dal momento che il ciclo do..while esegue almeno UNA iterazione, lo scartiamo e usiamo il while.

Il codice è facile e tra l'altro già ampiamente discusso in precedenza:

static int mcd( int alfa, int beta )
{
// i due numeri devono essere positivi
alfa = Math.abs(alfa);
beta = Math.abs(beta);
// beta deve essere minore/uguale ad alfa
int b = Math.min( alfa, beta );
int a = Math.max( alfa, beta );
while( b != 0 ) {
int resto = a % b;
a = b;
b = resto;
}
return a;
}

Il metodo Math.abs(num) ritorna il valore assoluto del numero fornito come argomento e questo viene usato per rendere entrambi i numeri positivi.
Il metodo Math.min(a,b) ritorna il minimo tra i due argomenti forniti al metodo mentre Math.max(a,b) ritorna, come avrete intuito, il massimo tra i due argomenti forniti. Questo codice è necessario per fare in modo che alfa sia maggiore o uguale a beta come stabilito dalla regola del algoritmo.

Infine, spendo qualche parola sul fatto che abbiamo dovuto usare due variabili temporanee a e b per implementare l'algoritmo: questo è necessario per mantenere inalterato il valore di alfa e beta. Se avessimo usato il costrutto beta=Math.min(alfa,beta) e il valore minore sarebbe stato alfa, allora beta avrebbe avuto il valore di alfa mentre alfa avrebbe continuato a possedere il suo valore originale; la conseguenza sarebbe che entrambi i due numeri avrebbero avuto lo stesso valore.

Concluso il ciclo iterativo (sempre se è mai stato eseguito) il valore di a contiene il MCD che stiamo cercando e che viene restituito dal metodo con la istruzione return.

Il metodo mcm

Il metodo mcm è ancora più semplice: dopo aver calcolato il valore assoluto dei parametri al metodo, esso moltiplica i due numeri in ingresso e li divide per il loro MCD secondo la formula già illustrata in precedenza.

Il metodo principale

Come abbiamo imparato in Il primo (inutile) programma ogni applicazione deve avere un punto di ingresso (entry point, in gergo informatico) ed in Java questo punto di ingresso è il metodo main che ha una sintassi specifica che deve essere rispettata.
Cosa esegue il metodo principale? Beh, è semplice: impostiamo i due numeri di cui trovare il MCD ed il MCM e richiamiamo i metodi scritti allo scopo. Il risultato restituito dai metodi verrà visualizzato a terminale: Ecco il sorgente:

public static void main( String[] args )
{
System.out.println( "mcmd - calcola il MCD ed il MCM tra due numeri" );
int alfa = -256;
int beta = 48;
int m1 = mcd( alfa, beta );
int m2 = mcm( alfa, beta );
System.out.println( "MCD(" + alfa + "," + beta + ") = " + m1 );
System.out.println( "MCM(" + alfa + "," + beta + ") = " + m2 );
}
}

Il codice del main è abbastanza semplice. Unica nota degna di rilievo sono le due istruzioni finali con le quali vengono visualizzati a terminale i risultati del calcolo. Abbiamo già visto nella sezione Scriviamo il programma sorgente del capitolo Il primo (inutile) programma che il costrutto:

System.out.println( "argomento" );

visualizza a terminale tutto ciò che viene indicato in argomento e che questo argomento può essere qualsiasi cosa: un numero, una stringa, un boolean o, perchè no, persino un oggetto. Abbiamo anche accennato al fatto che il simbolo "+" (più) si comporta diversamente tra numeri e stringhe: nel caso di numeri il simbolo esegue una somma numerica mentre nel caso di stringe quel simbolo esegue la concatenazione delle stringhe.
Pertanto, è possibile fornire come argomento al metodo println uno o più argomenti usando l'operatore di concatenazione delle stringhe il cui risultato è una nuova stringa ottenuta dalla concatenazione di tutti gli argomenti concatenati, siano essi numeri od oggetti.

Aprite il terminale (il Prompt del DOS), spostatevi nella cartella dove è presente il listato sorgente, compilate e mandate in esecuzione il programma:

 > cd javatutor1\projects\mcmd
 > javac mcmd01.java 
 > java mcmd 

Se non avete commesso errori, l'output del programma è il seguente:

mcmd - calcola il MCD ed il MCM tra due numeri
MCD(-256,48) = 16
MCM(-256,48) = 768

Volendo verificare la bontà dei risultati, non dobbiamo fare altro che ricorrere al metodo classico della scomposizione in fattori primi:

  • 256 = 28
  • 48 = 24 * 3

Ne deriva che il MCD è 24 = 16 mentre il MCM è pari a 28 * 3 = 768. Possiamo pertanto affermare che il nostro piccolo programmino funziona. Ma ne siamo davvero sicuri? Nelle prossime sezioni imparerete a scovare e correggere i bugs.

Il passaggio degli argomenti ai metodi

Forse alcuni di voi saranno rimasti stupiti dal output del programma nel quale il numero alfa è rimasto negativo nonostante lo stesso sia stato convertito in positivo sia nel metodo mcd che in mcm:

static int mcd( int alfa, int beta )
{
// i due numeri devono essere positivi
alfa = Math.abs(alfa);
beta = Math.abs(beta);
...
}

Ebbene, è perfettamente normale che alfa sia rimasto negativo nonostante esso sia stato convertito in positivo nel metodo mcd poichè la variabile alfa che è stata modificata è il parametro al metodo mcd. Ci sono sostanzialmente tre modalità con cui gli argomenti possono essere passati ai metodi (o funzioni o procedure, che dir si voglia):

  • passaggio per copia: al metodo chiamato viene passata una copia della variabile definita nel chiamante; qualsiasi modifica alla variabile eseguita nel metodo chiamato, viene eseguita sulla COPIA della variabile; pertanto, il valore della variabile originale rimane inalterato
  • passaggio per riferimento: al metodo chiamato viene passato un reference (=riferimento) alla variabile definita nel chiamante; qualsiasi modifica eseguita dal metodo chiamato si riflette sul valore originale della variabile
  • passaggio per puntatore: al metodo viene passato l'indirizzo di memoria della variabile definita nel chiamante: qualunque modifica eseguita dal metodo chiamato sulla variabile passata come argomento viene eseguita sul puntatore e non sulla variabile stessa; è possibile modificare il valore della variabile originale dereferenziando il puntatore

Alcuni linguaggi, come il "C++", consentono al programmatore la scelta tra tutte e tre le modalità; altri linguaggi, come il "C", consentono la scelta tra due sole modalità: quella per copia e quella per puntatore. Il linguaggio Java non consente alcuna scelta al programmatore:

  • le variabili di tipo primitivo (vedi I tipi primitivi) vengono passate per COPIA
  • le variabili di tipo complesso (vedi Gli oggetti Java) vengono passate per RIFERIMENTO; anche Le arrays rientrano tra i tipi complessi

Poichè alfa e beta sono tipi primitivi, ai metodi mcd ed mcm vengono passate delle COPIE delle variabili quindi, anche se il nome della variabile è uguale a quello degli originali, la modifica eseguita all'interno dei due metodi, ha effetto solo sulle COPIE delle due variabili e non su quelle originali definite nel main.

Con ciò non pensiate che Java sia un linguaggio limitato perchè non consenta la scelta tra le tre modalità: la strategia di Java in questo contesto è la migliore poichè per i tipi primitivi è facile e veloce creare una copia della variabile; i tipi complessi, di contro, possono avere una dimensione molto grande (per esempio le arrays) e sarebbe uno spreco di tempo e spazio di memoria creare una loro copia da passare al metodo.

Il metodo sovraccaricato

Benchè il nostro programmino funzioni (apparentemente) bene c'è una piccola osservazione da fare: la logica del programma non è molto efficiente perchè stiamo sprecando tempo-macchina. Mi spiego meglio analizzando le due istruzioni nel main che calcolano i risultati:

int m1 = mcd( alfa, beta );
int m2 = mcm( alfa, beta );

Possiamo notare che nella variabile m1 abbiamo già il MCD tra i due numeri ma il metodo mcm, che si basa sul MCD, lo ricalcola, richiamando a sua volta il metodo mcd. Benchè il tempo impiegato dalla intera applicazione è una frazione di secondo e quindi non ce ne preoccupiamo affatto è buona norma cercare sempre la soluzione più efficente anche quando il tempo macchina sprecato sembra trascurabile.

Scriveremo pertanto una nuova versione del metodo mcm: non una versione che prende il posto della vecchia ma una versione che si affianca ad essa in modo da poter scegliere quale delle due versioni usare a seconda del fatto di possedere o meno il risultato intermedio MCD.

static int mcm( int alfa, int beta, int mcd )
{
alfa = Math.abs(alfa);
beta = Math.abs(beta);
int result = alfa * beta / mcd;
return result;
}

Copiate ed incollate il nuovo metodo nel listato sorgente, subito prima del metodo main oppure aprite il listato sorgente mcmd02.java che contiene questo nuovo metodo. Il nuovo metodo ha lo stesso nome di quello precedente: mcm ma la lista di argomenti è diversa: in questo nuovo metodo abbiamo tre argomenti anzichè due; i due numeri di cui calcolare il MCM più il loro MCD.
Questo costrutto si chiama method's overloading (=sovraccaricamento del metodo) ed è usato estensivamente in Java come in tutti i linguaggi orientati agli oggetti. Perchè un metodo sia sovraccaricato correttamente si devono rispettare le seguenti regole:

  • il numero degli argomenti deve essere diverso
  • in caso di numero di argomenti uguale, almeno uno di essi deve essere di tipo diverso da quello del metodo sovraccaricato

Se queste condizioni non sono soddisfatte, il compilatore emetterà un errore in quanto si sta cercando di definire un metodo con lo stesso nome e la stessa lista di argomenti di uno già definito in precedenza. Per esempio, questi sono tutti metodi overloaded:

void myMehtod( int a )
void myMethod( int a, String b );
void myMethod( String b, int a );

Il metodo main dovrà essere modificato per usare la nuova organizzazione, più efficiente di prima:

int m1 = mcd( alfa, beta );
int m2 = mcm( alfa, beta, m1 );

Compilate il sorgente ed eseguite:

> javac mcmd02.java 
> java mcmd 

mcmd - calcola il MCD ed il MCM tra due numeri
MCD(-256,48) = 16
MCM(-256,48) = 768

Come avrete notato, l'output è assolutamente uguale a quello della versione precedente ma il tempo di esecuzione del programma sarà sicuramente minore: poichè il guadagno sarà probabilmente di qualche millisecondo (o ferse addirittura nanosecondo) non ce ne siamo neppure accorti.

Scrivere una suite di test

Questo è un argomento mai affrontato nelle guide e nei tutorial che trovate sia online che nelle librerie. E' un argomento ostico, non propedeutico alla comprensione del linguaggio e pertanto trascurato. Ma questo non è un tutorial come gli altri; è diverso proprio perchè affronta temi ed argomenti che gli altri tralasciano.

Abbiamo provato la nostra applicazioncina (o programmino, che dir si voglia) con un paio di numeri scelti a caso e abbiamo appurato che funziona. Si certo, funziona con quei due numeri; ma con gli altri? Eppoi, se un domani ci venisse chiesto: "ma con quali e quante coppie di numeri è stato testato il programma?" Non avremo una risposta anche perchè chi si ricorda, magari a distanza di mesi, con quali numeri lo abbiamo provato?
Ecco perchè scrivere una test suite è importante. Che io sappia non esiste una traduzione corretta in italiano del termine inglese "test suite". Il vocabolo inglese suite può essere tradotto con "raccolta" oppure "sequenza".

Scriveremo pertanto un metodo che esegue la verifica dei risultati in modo automatico, senza neppure l'intervento del programmatore. Per fare ciò dobbiamo:

  • selezionare un congruo numero di numeri interi, diciamo una decina
  • fattorizzare i dieci numeri
  • calcolare a mano il MCD ed MCM di alcune coppie di questi numeri
  • calcolare i risultati dei metodi mcd e mcm
  • verificare che i risultati ritornati dai metodi coincidano con quelli calcolati a mano

Cominciamo con il fattorizzare alcuni numeri scelti a caso:

  126 = 2, 32, 7
  234 = 2, 32, 13
  768 = 28, 3
  876 = 22, 3, 73
 4657 = è un numero primo
 1245 = 3, 5, 83
 7653 = 3, 2551
 4090 = 2, 5, 409
 1024 = 210
 8756 = 22, 11, 199
 7645 = 5, 11, 139

ora prendiamo alcune coppie a caso e calcoliamo il MCD ed il MCM a mano:

coppia di numeri MCD MCM
126, 234 2*32 = 18 2*32*13*7 = 1638
4657,876 1 4657*22*3*73 = 4079532
4090,7645 5 2*5*409*11*139 = 6253610
1245,126 3 32*5*83*2*7 = 52290
1024,768 28 = 256 310*3 = 3072

Una volta calcolati i risultati attesi, scriviamo un metodo che verifichi la rispondenza di questi risultati con quelli ottenuti dai nostri metodi che implementano l'algoritmo di Euclide. Il metodo dovrà:

  • definire le coppie dei numeri da calcolare MCD e MCM
  • definire i risultati attesi
  • calcolare il MCD e MCM con i metodi della classe
  • verificare la rispondenza dei risultati calcolati con i risultati attesi

Quindi abbiamo quattro coppie di numeri e quattro coppie di risultati e poichè questi dati sono omogenei tra di loro li definiremo in quattro arrays di interi. Chiamaremo il metodo test1; esso non ha bisogno di argomenti dal momento che le coppie di numeri sono già definite nel metodo stesso e, come già detto molte volte, sarà statico in modo da poterlo usare come una funzione cioè senza dover "costruire" un oggetto per poterlo usare.
Aprite nel editor di testi il file sorgente mcmd03.java

static void test1()
{
System.out.println( "test1 - verifica risultati metodi mcd e mcm" );
// definisce la array del primo numero
int[] alfa = { 126, 4657, 4090, 1245 };
// definisce la array del secondo numero
int[] beta = { 234, 876, 7645, 126 };
// definisce la array del MCD atteso
int[] mcdExpected = {18, 1, 5, 3 };
// definisce la array del MCM atteso
int[] mcmExpected = { 1638 , 4079532, 6253610, 52290 };
// usiamo un ciclo per verificare i risultati
for ( int i = 0; i < alfa.length; i++ ) {
System.out.println( "Verifica numeri: " + alfa[i] + " e " + beta[i] );
// calcola i risultati con i metodi mcd() e mcm()
int resultMCD = mcd( alfa[i], beta[i] );
int resultMCM = mcm( alfa[i], beta[i] );
// visualizza i risultati ottenuti e quelli attesi
System.out.println( " MCD: " + resultMCD + ", atteso: " + mcdExpected[i] );
System.out.println( " MCM: " + resultMCM + ", atteso: " + mcmExpected[i] );
// verifica che i risultati coincidano
assert resultMCD == mcdExpected[i] : "Risultato inatteso per MCD";
assert (resultMCM == mcmExpected[i]) : "Risultato inatteso per MCM";
}
}

Il codice è di facile lettura anche grazie alle righe di commento. Per ripassare l'argomento della definizione ed inizializzazione di una array vedi Le arrays e Inizializzazione dell'array. Una nota deve comunque essere spesa per un costrutto che non abbiamo mai visto fino a questo momento e cioè la istruzione assert. Affronteremo questo argomento subito, nella prossima sotto-sezione.

Modificate il metodo main, inserendo un richiamo al metodo test1 prima della fine del metodo:

public static void main( String[] args )
{
...
// esegue la suite test
test1();
}

Compilate ed eseguite il programma (d'ora in poi non ve lo ricorderò più, presumo che oramai lo abbiate assimilato):

 > javac mcmd03.java 
 > java -ea mcmd 

otterrete l'output già visto nella versione precedente più un altro di questo tipo:

test1 - verifica risultati metodi mcd e mcm
Verifica numeri: 126 e 234
   MCD: 18, atteso: 18
   MCM: 1638, atteso: 1638
Verifica numeri: 4657 e 876
   MCD: 1, atteso: 1
   MCM: 4079532, atteso: 4079532
Verifica numeri: 4090 e 7645
   MCD: 5, atteso: 5
   MCM: 6253610, atteso: 6253610
Verifica numeri: 1245 e 126
   MCD: 3, atteso: 3
   MCM: 52290, atteso: 52290
Verifica numeri: 1024 e 768
   MCD: 256, atteso: 256
   MCM: 3072, atteso: 3072

L'output è facile da leggere. Non ci resta che verificare che il risultato riportato nel output sia uguale a quello atteso, anch'esso riportato nel output ma ... perchè devo farlo io personalmente?

La keyword assert

La parola chiave assert di Java è usata per verificare che una certa espressione o variabile o condizione, specificata subito dopo la parola chiave, sia valutata come true (=VERO). Nel nostro caso la condizione da verificare che sia VERA è che il risultato ottenuto dai metodi mcd e mcm sia UGUALE a quello atteso cioè che:

resultMCD == mcdExpected[i]
resultMCM == mcmExpected[i]

(per un breve ripasso del operatore di relazione uguale a (==) vedi Operatori di relazione).

Se la condizione viene valutata false, viene "sollevata una eccezione" di tipo AssertionError che provoca, di norma, la terminazione immediata del programma. L'argomento eccezioni sarà discusso in modo più approfondito nella sezione Le eccezioni.
Dopo la condizione da verificare è possibile specificare un messaggio di errore che verrà visualizzato dalla JVM come causa del sollevamento della eccezione; nel nostro caso il messaggio è "Risultato inatteso per MCD/MCM" a seconda di quale assert è fallita.
La condizione che viene specificata subito dopo la assert può essere racchiusa tra parentesi tonde anche se non è necessario. Le parentesi possono aiutare la leggibilità del codice. A titolo di esempio, modificate uno dei risultati attesi, per esempio il primo MCM ed al posto del 1638, che è il risultato corretto, scrivete il valore 654. Salvate il sorgente, compilate ed eseguite il programma ma, questa volta, dovrete eseguire la JVM con una opzione speciale:

 > javac mcmd03.java 
 > java -ea mcmd 

L'opzione -ea indicata subito prima della classe che contiene il metodo main sta per enable assertions ed istruisce la JVM di abilitare le assert. Se l'opzione non viene indicata la assert non ha alcun effetto ed il programma termina in modo regolare, ma senza la verifica sui risultati. L'output del programma con il risultato errato è il seguente:

test1 - verifica risultati metodi mcd e mcm
Verifica numeri: 126 e 234
   MCD: 18, atteso: 18
   MCM: 1638, atteso: 654
Exception in thread "main" java.lang.AssertionError: Risultato inatteso per MCM
        at mcmd.test1(mcmd03.java:93)
        at mcmd.main(mcmd03.java:67)

Direi che l'output è, come si suol dire in gergo, self-explanatory o, per dirla in italiano: "si commenta da se".

Come passare l'input al programma

Abbiamo un programma funzionante e testato (davvero? vi anticipo già che avremo delle sorprese al riguardo) ma, che noia dover modificare i sorgenti se dobbiamo calcolare il MCD e MCM di due numeri che non siano quelli scritti nel main o nel metodo di test. Si dice in gergo che tutti i dati scritti nel sorgente di un programma sono hardcoded (=codificati duramente) e sono immutabili a meno che non si modifichi il listato sorgente.
Ci deve essere un modo migliore per dare la possibilità al utente di specificare i due numeri che egli preferisce. I dati che l'utente inserisce nel programma sono conosciuti come i dati di input e, per una applicazione CLI (Command Line Interface) esistono sostanzialmente due metodi per ottenerli:

  • invitare l'utente a fornirli tramite la tastiera
  • specificarli sulla command line (=riga di comando)

Invitare l'utente a fornire i dati

Questo è un metodo poco usato nei programmi CLI poichè interrompe il flusso del programma e, mediante la visualizzazione di un messaggio, aspetta che l'utente digiti il dato tramite la tastiera. Non appena l'utente ha inserito il dato (normalmente premendo il tasto ENTER (=INVIO) sulla tastiera, il flusso del programma riprende.

Per avere questo metodo di input è necessario introdurre alcuni concetti che saranno affrontati nei prossimi capitoli. Per il momento, mi limito ad una breve panoramica dal momento che i concetti classe, istanza ed oggetto non sono ancora stati affrontati:

  • la tastiera del computer è gestita in Java dal "oggetto" System.in che è di classe InputStream (=flusso in input)
  • per interpretare i dati provenienti da un qualsiasi flusso in input si usa la classe Scanner, che fà parte del "package" java.util; si costruisce una "istanza" di uno scanner specificando nel "costruttore" quale è il flusso da interpretare

Comprendo che le parole "oggetto", "package", "istanza" e "costruttore" vi sono, al momento, sconosciute ma per il momento non sono propedeutiche alla comprensione del listato che scriveremo. Ci torneremo nei prossimi capitoli.

Aprite il listato sorgente mcmd04.java in cui troverete alcune modifiche. Aall'inizio del listato, subito dopo il blocco di commento, è stata inserita la seguente riga:

import java.util.Scanner;

Questa riga è necessaria perchè la classe Scanner non è definita nel nostro programma ma viene usata: la JVM deve sapere dove trovarne la definizione altrimenti non potrebbe mai eseguirne il bytecode. Come accennato, la classe Scanner fà parte del package java.util il quale a sua volta fà parte della libreria standard di Java; questa libreria viene installata automaticamente con il JRE (Java Runtime Environment) di Java il quale fa parte del OpenJDK.

Conosco già la vostra domanda: nel programma HelloWorld (vedi Scriviamo il programma sorgente) abbiamo usato la classe System senza definirla nè importarla, come è stato possibile localizzarla da parte della JVM quando abbiamo mandato in esecuzione il programma?
La risposta è semplice: la classe System fà parte del package java.lang il quale viene automaticamente importato dalla JVM!

Le altre modifiche al sorgente sono state apportate al metodo main:

// commentiamo le righe che impostano i valori hardcoded
//int alfa = -256;
//int beta = 48;
// invita l'utente a fornire i dati di input
Scanner scanner = new Scanner( System.in );
System.out.print( "Immetti il primo numero: " );
int alfa = scanner.nextInt();
System.out.print( "Immetti il secondo numero: " );
int beta = scanner.nextInt();

Prendete il codice come un "si fa così e basta" per ora anche perchè non useremo mai più l'input dell'utente attraverso la tastiera come invito.

Il metodo nextInt() del oggetto scanner attende che l'utente digiti una serie di caratteri sulla tastiera e poi prema ENTER. Tutti i caratteri saranno interpretati come un numero intero e restituiti nelle variabili alfa e beta. Cosa succede se l'utente digita caratteri alfabetici come per esempio "pippo"? Affronteremo anche questa eventualità nelle prossime sezioni quando impareremo a scovare i bugs del programma e a correggerli.

Dal momento che i risultati del test non ci interessano più commentiamo la riga nel main che richiama il metodo test1. Anche se commentata, la riga rimane ma non viene più eseguita però abbiamo ottenuto due vantaggi scrivendo il metodo test1:

  • abbiamo verificato la applicazione
  • abbiamo documentato i test eseguiti

Compiliamo ed eseguiamo mcmd04.java:

mcmd - calcola il MCD ed il MCM tra due numeri
Immetti il primo numero: 126
Immetti il secondo numero: 234
MCD(126,234) = 18
MCM(126,234) = 1638

La riga di comando

Come accennato poc'anzi l'input dell'utente tramite invito è poco usato nei programmi CLI professionali (anzi, direi che non è usato per nulla!). Nei programmi CLI è di gran lunga preferito ottenere i dati di input tramite la riga di comando (la cosidetta command line che d'ora in poi abbrevierò in cmdline).
La cmdline rappresenta tutti i caratteri che l'utente digita dopo il comando che manda in esecuzione un programma. Esempio:

> comando tutto quello che segue il 'comando' è la cmdline

Resta il problema che se l'utente deve specificare i dati direttamente sulla linea di comando non saprebbe cosa scrivere, ovviamente, mancando l'invito con un messaggio esplicativo. Questo problema viene risolto specificando una sintassi del comando che è una descrizione rigida e rigorosa di quanti e quali dati il programma si aspetta sulla cmdline.

Normalmente, la sintassi del comando (o del programma) viene descritta nella documentazione del programma stesso ma, di norma, viene anche riportata nel programma ma in una forma piuttosto sintetica. La sintassi di ogni programma CLI si può ottenere, di norma, digitando la parola --help oppure -h sulla riga di comando. Provate ad esempio a digitare:

> javac --help

Otterrete una lunga serie di informazioni sulla sintassi del comando javac che è il compilatore Java con interfaccia CLI. L'output del programma comincia più o meno così:

Usage: javac <options> <source files>
where possible options include:
...

Benchè ogni programmatore è libero di stabilire la sua sintassi personale, è buona norma seguire le regole di quella già in uso a cui gli utenti sono abituati.
Essa affonda le sue radici nel sistemi Unix™ fin dagli albori della elaborazione elettronica e tuttora in uso nei sistemi Unix-like come per esempio Linux. La sintassi generale di ogni comando CLI è la seguente:

  command [OPTIONS] ARGUMENT(S)

dove OPTIONS sono le opzioni ed ARGUMENT(S) sono zero o più argomenti non-opzione. Le regole generali della sintassi Unix per le opzioni e gli argomenti sono le seguenti:

  • se un argomento comincia col carattere "-" (trattino) esso è considerato una opzione; il trattino DEVE essere seguito da un carattere che rappresenta la opzione; per esempio -h
  • una opzione non dovrebbe MAI essere obbligatoria; può essere specificata oppure no
  • una opzione può avere un argomento alla opzione; esso deve seguire immediatamente il carattere della opzione; esempio "-hmcm". La opzione è la "-h" mentre l'argomento alla opzione è "mcm". Potrebbe significare che l'utente vuole aiuto sul tema "mcm"
  • tutto ciò che non inizia col trattino è considerato un argomento non-opzione. Se uno o più argomenti non sono obbligatori essi vanno racchiusi tra parentesi quadre
  • un comando potrebbe prevedere uno o più argomenti obbligatori: spesso gli argomenti obbligatori vengono racchiusi tra una coppia di parentesi angolate ma non è necessario dal momento che se un argomento non è obbligatori viene racchiuso tra parentesi quadre
  • un argomento uguale a "--" (doppio trattino) viene considerato il marcatore di fine opzioni; qualunque stringa successiva viene considerata argomento non-opzione anche se comincia col trattino

Queste sono le regole basilari; ce ne sono molte altre ma non sono interessanti dal momento che noi useremo solo quelle elencate sopra. Esempio:

comando -x -y765 hello -- -123
  • "-x" è considerata una opzione
  • "-y765" è considerata una opzione con argomento "765"
  • "hello" è considerato un argomento non-opzione
  • "--" è il marcatore di fine opzioni, non ha altro significato
  • "-123" è considerato un argomento non-opzione anche se comincia col carattere trattino

Per chi vuole approfondire questo argomento è disponibile il manuale della libreria standard del C pubblicato dalla Free Software Foundation. Il capitolo che tratta la sintassi dei comandi è: Program Argument Syntax Conventions. Che io sappia è disponibile solo in lingua inglese.

Dobbiamo pertanto stabilire quale sarà la sintassi del nostro programma. Dal momento che esso calcola il MCD ed il MCM tra due numeri, è ovvio che il programma si aspetta almeno due argomenti: i due numeri. Come opzioni possiamo averne due:

  • -h che visualizza una breve descrizione della sintassi
  • -t che esegue la suite di test

Ecco come apparirà la sintassi del programma mcmd e che definiremo in una stringa come text-block (per ripassare l'argomento dei blocchi di testo vedi Il blocco di testo):

        String usage = """
                    Usage: java -ea mcmd [OPTIONS] number number
                    where OPTIONS can be:
                        -h    display this help
                        -t    execute the test suite
                    """;

Aprite il file sorgente mcmd05.java che contiene il nuovo metodo parseCmdLine il quale interpreta la cmdline e ci restituisca i due numeri da elaborare. Purtroppo qualsiasi metodo non può restituire che UNO ed UN SOLO dato ma a noi ne servono due: alfa e beta. Siamo costretti a trovare una soluzione diversa.

La soluzione a questo problema è piuttosto facile: un metodo può effettivamente restituire un solo dato ma questo dato puà essere di qualsiasi tipo: può essere un tipo primitivo, certo, ma può anche essere una classe e, perchenno, anche una array! E poichè a noi servono due numeri interi la soluzione è quella di restituire una array di interi che contenga due elementi. L'argomento al metodo è una array di stringhe che sarà passata al metodo dal main. Questa array è fornita al main dalla JVM quando caricherà in memoria il programma per la sua esecuzione.

Per quanto riguarda l'uso delle minuscole e maiuscole nel nome dei metodi vi sono diverse convenzioni:

  • i metodi dovrebbero essere scritti in minuscolo con la inziale minuscola e, se composti da più parole, usare la maiuscola per separare le parole.
  • i metodi dovrebbero essere sempre scritti in minuscolo e, se composti da più parole, esse devono essere separate dal carattere underscore.
  • i metodi dovrebbero essere sempre scritti in minuscolo con la lettera iniziale di ogni parola maiuscola

Esempi:

parseCmdLine
parse_cmd_line
ParseCmdLine

In Java si usa la prima convenzione, quella in cui i metodi sono scritti sempre in minuscolo e, se composti da più parole, si scrive in maiuscolo la iniziale di tutte le parole successive alla prima. Non scrivete i metodi tutti in maiuscolo: nessun programmatore lo farebbe mai, in nessun linguaggio di programmazione. Le parole scritte tutte in maiuscolo hanno un significato speciale, che affronteremo in una sezione successiva (vedi Le costanti mnemoniche).

Ora descriverò il corpo del metodo che interpreta la command-line che trovate nel sorgente mcmd05.java:

static int[] parseCmdLine( String[] args )
{
String usage = """
Usage: java -ea McMd [OPTIONS] number number
where OPTIONS can be:
-h display this help
-t execute the test suite
""";
int numbers[] = new int[2];
boolean forceArgument = false;
int numArgs = 0;
for ( String item : args ) {
char ch = item.charAt(0); // estrae il primo carattere del argomento
if ( ch == '-' && !forceArgument ) {
// il primo carattere è un trattino
// valuta il secondo carattere
switch( item.charAt(1) ) {
case 'h' :
System.out.println( usage );
System.exit(0);
break;
case 't' :
test1();
System.exit(0);
break;
case '-' :
forceArgument = true;
break;
default :
break;
}
}
else {
// se non è una opzione allora è un argomento non-opzione
// e lo memorizziamo nella array da restituire al main
if ( numArgs < 2 ) {
numbers[numArgs] = Integer.parseInt( item );
numArgs++;
}
}
}
return numbers;
}

Il codice sorgente del metodo parseCmdLine non è difficile da leggere a parte forse un paio di costrutti che non sono mai stati affrontati. Uno di questi costrutti è il seguente:

for ( String item : args ) {
...
}

Si tratta di una versione semplificata (o per meglio dire migliorata) del ciclo for. Se il ciclo for è usato per iterare su tutti gli elementi di una array o di una collezione (argomento che affronteremo in Arrays e Collections) allora è possibile semplificare la sintassi del ciclo in questo modo:

for( element_type variabile : collection ) { .... }

dove element_type è il tipo di elemento (un intero, una stringa, etc), variabile è il nome che vogliamo dare al elemento i-esimo e collection è il nome della array o della collezione. I seguenti due costrutti sono, pertanto, equivalenti:

for ( String item : args ) {
...
}
for ( int i = 0; i < args.length; i++ ) {
String item = args[i];
...
}

Il metodo String.charAt(0) estrae il primo carattere della stringa; il metodo è definito nella classe String che fa parte della libreria standard di Java. Per approfondire questo concetto vedi: La API di Java, version 22.

Il booleano forceArgument viene usato per forzare l'assegnamento agli argomenti non-opzione di una eventuale stringa che comincia col "-" (trattino). Questo è necessario per poter fornire al programma numeri negativi come argomenti.
Sia MCD che MCM possono essere calcolati per i numeri negativi ma, poichè un multiplo o un divisore esiste sia nella versione negativa che positiva il programma visualizza solo la versione positiva: in effetti, uno dei divisori di +6 e di -6 è sia +3 che -3.

Due parole di commento sul codice che memorizza l'argomento non-opzione nella array di due elementi da restituire al main:

// se non è una opzione allora è un argomento non-opzione
// e lo memorizziamo nella array da restituire al main
if ( numArgs < 2 ) {
numbers[numArgs] = Integer.parseInt( item );
numArgs++;

la variabile numArgs tiene il conto di quanti argomenti non-opzione sono stati digitati sulla cmdline: ne devono essere restituiti due (i due numeri alfa e beta) ma non più di due. Se l'utente ne specifica più di due, solo i primi due vengono memorizzati nella array numbers; tutti gli altri vengono semplicemente ignorati.

Infine, un breve commento sul costrutto:

int Integer.parseInt( String s );

Il metodo statico Integer.parseInt(String) consente di convertire una stringa di caratteri in un numero intero. Come già accennato in precedenza, la stringa "1234" benchè appaia come un numero non lo è affatto ed il metodo di cui sopra consente la conversione. Non dimentichiamo, infatti, che gli argomenti alla cmdline sono memorizzati in una array di stringhe. Per maggiori informazioni consulta la documentazione ufficiale della libreria Java: Integer.parseInt.

Come ultima cosa dobbiamo modificare il metodo main che, ora, risulta molto più semplice:

public static void main( String[] args )
{
System.out.println( "mcmd - calcola il MCD ed il MCM tra due numeri" );
int[] numbers = parseCmdLine( args );
int m1 = mcd( numbers[0], numbers[1] );
int m2 = mcm( numbers[0], numbers[1], m1 );
System.out.println( "MCD(" + numbers[0] + "," + numbers[1] + ") = " + m1 );
System.out.println( "MCM(" + numbers[0] + "," + numbers[1] + ") = " + m2 );
}

Scovare e correggere i bugs

Anche se a prima vista il nostro programma funziona bene, ci sono diversi problemi da risolvere. Abbiamo testato il programma con una serie di numeri e ci ha dato risultati corretti ma non abbiamo testato le situazioni cosidette limite:

  • non forniamo alcun argomento sulla command-line
  • forniamo come argomento lo ZERO per entrambi i numeri
  • forniamo come argomento numeri reali (con la virgola)
  • forniamo come argomento stringhe di caratteri non numerici
  • forniamo come argomenti almeno un numero molto grande
  • forniamo come argomenti due numeri abbastanza grandi

Tutte queste situazioni limite vanno esplorate e, se il programma non si comporta correttamente, è necessario correggere gli errori logici, i cosidetti bugs che letteralmente significa "insetti" o "bacherozzi". Al contrario degli errori di sintassi, segnalati dal compilatore, gli errori logici sono difficili da scoprire.

Nessun argomento sulla command-line

Poichè il programma calcola il MCD ed MCM di due numeri forniti come argomenti alla command-line, il fatto che l'utente non li fornisca và contro le regole di sintassi della applicazione e quindi dovremmo segnalare l'errore.
Per controllare che l'utente fornisca esattamente due argomenti sulla command-line verifichiamo che la variabile numArgs, usata come contatore degli argomenti non-opzione, sia esattamente uguale a 2 all'uscita del metodo parseCmdLine:

private static void parseCmdLine( String[] args )
{
...
if ( numArgs != 2 ) {
"segnala un errore"
}
}

Per il momento teniamo in sospeso il codice che "segnala l'errore"; ci arriveremo tra poco.

ZERO per entrambi gli argomenti

Se specifichiamo lo ZERO per entrambi i numeri sulla command-line la app non visualizza l'output che ci aspettiamo (il MCD ed il MCM) ma un messaggio astruso, incomprensibile per qualsiasi utente. Si dice in gergo che la app è andata in crash:

javatutor1\projects\mcmd>java -ea mcmd 0 0
mcmd - calcola il MCD ed il MCM tra due numeri
Exception in thread "main" java.lang.ArithmeticException: / by zero
        at mcmd.mcm(mcmd05.java:53)
        at mcmd.main(mcmd05.java:63)

Quello che è successo è che uno dei metodi richiamati dal main ha sollevato una eccezione e possiamo notare dal messaggio che il metodo incriminato è il mcm, alla riga 53 del sorgente mcmd05.java (per il lettore la riga potrebbe essere leggermente diversa):

int result = alfa * beta / mcd; // la riga 53

Oltre alla riga del listato sorgente che ha causato la eccezione, l'output del programma ci informa anche che la eccezione sollevata è di tipo ArithmeticException il cui messaggio di errore è il seguente:

ArithmeticException: / by zero

che significa che abbiamo eseguito una divisione per ZERO, un assurdo matematico. Ciò è dovuto al fatto che il metodo mcm divide il risultato della moltiplicazione dei due numeri alfa e beta per il loro MCD ma, poichè MCD(0,0) = 0 ecco che andiamo incontro alla eccezione di divisione per zero.

Se uno qualsiasi dei metodi richiamati dal main solleva una eccezione, la JVM cerca, a ritroso, un handler (=gestore) che gestisca la eccezione.
Se nessun gestore viene trovato, la JVM termina il programma, visualizza il tipo di eccezione occorsa (nel nostro caso una ArithmeticException ), il messaggio di errore contenuto nella eccezione e il cosidetto stack frame cioè tutti i metodi nei quali ha cercato un gestore a ritroso fino al main.
Dopo la visualizzazione dell'errore, la JVM termina immediatamente il programma.

Questo però è da considerare un bug (=errore logico) del programma poichè le regole del MCD e del MCM stabiliscono che:

  • il MCD(0,0) è uguale a ZERO
  • il MCM(0,0) è uguale a ZERO

Pertanto, dobbiamo prevedere questa situazione nel metodo mcm:

public static void mcm( int alfa, int beta )
{
if ( alfa == 0 && beta == 0 ) {
return 0;
}
...
}

Non è necessario prevedere questa situazione nel metodo mcd poichè se beta è uguale a ZERO, viene restituito come MCD il valore di alfa che è anch'esso ZERO: quindi il valore restituito è corretto.

Numeri reali come argomenti

Se forniamo numeri reali (con la virgola) come argomenti otteniamo un crash della applicazione causato dal metodo statico di libreria Integer.parseInt il quale non è in grado di interpretare la stringa di caratteri come un numero intero. Esempio:

javatutor1\projects\mcmd>java -ea mcmd 10 65.4
mcmd - calcola il MCD ed il MCM tra due numeri
Exception in thread "main" java.lang.NumberFormatException: For input string: "65.4"
        at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
        at java.base/java.lang.Integer.parseInt(Integer.java:588)
        at java.base/java.lang.Integer.parseInt(Integer.java:685)
        at mcmd.parseCmdLine(mcmd06.java:137)
        at mcmd.main(mcmd06.java:63)

Anche in questo caso, il crash è causato da una eccezione sollevata dal metodo parseInt il quale non riesce ad interpretare la stringa "65.4" come numero intero. La eccezione sollevata in questo caso è di tipo NumberFormatException.

Questo comportamento non è di per sè da considerare un bug vero e proprio poichè il programma è progettato per calcolare MCD e MCM di due numeri interi: se l'user fornisce input non consono alla sintassi del programma, non può certo aspettarsi l'output previsto.

Tuttavia, terminare il programma con un messaggio criptico come quello visto più sopra non è professionale; sarebbe più opportuno informare l'user che uno degli argomenti da egli fornito non è valido; il messaggio di errore dovrebbe essere più amichevole, come per esempio il seguente:

ERRORE: l'argomento "65.4" è invalido; deve essere fornito un numero intero

Stringhe di caratteri non numerici

Se forniamo come argomento una stringa di caratteri che contiene caratteri alfabetici come per esempio "pippo", otteniamo una eccezione di tipo NumberFormatException nel metodo parseInt. Questa casistica è uguale alla precedente e quindi sarà trattata allo stesso modo.

Numeri molto grandi

Se forniamo come argomento un numero molto grande, per esempio 4 miliardi otteremo ancora una eccezione NumberFormatException perchè è stato superato il limite di capacità del tipo int (per un ripasso dei tipi di dati primitivi e dei loro limiti di capienza vedi I tipi primitivi). Il superamento di capacità dei dati numerici si chiama overflow.
Anche questo caso è molto simile ai due precedenti ma possiamo però migliorare il nostro messaggio di errore in caso di eccezione di tipo NumberFormatException che diventerà:

ERRORE: l'argomento "4000000000" è invalido; deve essere fornito un numero
            intero compreso tra -2147483648 e +2147483647

Numeri abbastanza grandi

Di tutti i bugs visti finora, questo è il più subdolo. Forniamo ora come argomenti due numeri non così grandi da provocare un overflow nel metodo parseInt ma abbastanza grandi da provocare overflow nei calcoli. Prendiamo ad esempio un numero primo come il 19.999.843 ed un altro numero, più piccolo, diciamo il 117.
Poichè uno dei due numeri è un numero primo, il MCD è ovviamente pari a 1. Pertanto il MCM è per forza di cose il prodotto dei due numeri:

MCM(19.999.843,117) = 2.339.981.631

il risultato del MCM è maggiore del numero più grande memorizzabile in un int. Ci aspetteremo quindi che la JVM sollevi una eccezione di qualche tipo ma, invece, nulla accade ed il programma termina normalmente visualizzando un risultato palesemente errato:

>java -ea mcmd 117 19999843
mcmd - calcola il MCD ed il MCM tra due numeri
MCD(117,19999843) = 1
MCM(117,19999843) = -1954985665

Come è possibile che il MCM sia un numero negativo? Ebbene, il problema nasce dal fatto che nei computer i numeri interi sono memorizzati in complemento a due; questo modo di memorizzare i numeri garantisce diversi vantaggi tra cui una semplificazione delle operazioni, risparmio di memoria e un incremento di velocità.
Lo svantaggio è che, per funzionare, è necessario ignorare i riporti nelle operazioni aritmetiche e questo porta a risultati errati nel caso di condizione di overflow. Per coloro che vogliono approfondire la tecnica del complemento a due c'è una sezione dedicata a questo tema in appendice, vedi Il complemento a due.

Dobbiamo quindi lasciare che il nostro programma possa fornire risultati errati? Certo che no, non sarebbe professionale.
Una prima soluzione a questo problema è davvero semplice: considerato che il MCM di due numeri primi di 32 bits (un tipo int) non può essere più grande di un numero di 64 bits (un tipo long) è sufficiente che i due metodi mcm restituiscano un long anzichè un int:

static long mcm( int alfa, int beta, int mcd )
{
if ( alfa == 0 && beta == 0 ) {
return 0;
}
alfa = Math.abs(alfa);
beta = Math.abs(beta);
long result = alfa * beta / mcd;
return result;
}
// e nel metodo main:
long m2 = mcm( numbers[0], numbers[1], m1 );

Si dovra modificare anche il tipo delle variabili mcmExpected e resultMCM da int long nel metodo test1. Trovate queste modifiche nel sorgente mcmd06.java; compilate ed eseguite: Con nostra grande sorpresa nulla è cambiato: il programma dà ancora il risultato errato per MCM nonostante la capienza di un intero di tipo long è assolutamente in grado di contenere il risultato corretto.
La causa di ciò che accade è piuttosto semplice analizzando con calma le istruzioni contenute nel metodo mcm ed in particolare:

long result = alfa * beta / mcd;

Nei calcoli aritmetici, il compilatore esegue le operazioni su tipi di dato omogenei: gli interi con gli interi, gli interi lunghi con gli interi lunghi e così via; il risultato è, ovviamente, dello stesso tipo degli operandi.
Tenendo presente quanto scritto sopra, avremo che alfa sarà moltiplicato per beta ed essendo entrambi di tipo int, il risultato sarà un int. E qui sorge un problema: il risultato di questa moltiplicazione potrebbe non trovare capienza in un int e per questo motivo sarà inevitabilmente troncato. Poco importa che, successivamente, sarà assegnato ad un tipo long; oramai il risultato è stato troncato ad int poichè entrambi gli operandi sono di tipo int.

Se gli operandi non sono dello stesso tipo il compilatore converte uno dei due nel tipo dell'altro operando, quello con la precisione maggiore: ne consegue che, in una operazione tra un int ed un long, il compilatore convertirà il valore del tipo int in un tipo long; questa conversione viene comunemente chiamata promozione. Dopo la promozione, i due operandi sono dello stesso tipo e la operazione aritmetica può essere eseguita.
Per risolvere il problema del risultato errato sarà sufficiente forzare il compilatore a promuovere tutte le variabili coinvolte in variabili di tipo long; per ottenere questo risultato è sufficiente promuovere esplicitamente anche uno solo degli operandi in modo da costringere il compilatore a promuovere anche tutti gli altri.
E' possibile forzare la promozione di un tipo di dato attraverso il explicit cast (=conversione di tipo esplicita, vedi Il cast esplicito):

long result = (long)alfa * beta / mcd;

Dopo questa modifica otterremo il risultato corretto.
Per chi vuole approfondire l'aspetto del cast esplicito (consiglio i neofiti di farlo davvero) ho scritto una dettagliata analisi di questo concetto in una sezione in appendice e, precisamente, in Quando usare il cast esplicito.

Le eccezioni

Con le nuove modifiche il nostro programma si comporta in maniera corretta: solleva eccezioni nei casi in cui l'input è errato, la condizione di overflow è stata risolta ed il caso limite nel quale l'utente fornisca come argomenti due numeri pari a ZERO è stato gestito. Rimane in sospeso la situazione in cui l'utente non fornisce alcun argomento alla command line (vedi Nessun argomento sulla command-line). Oppure la situazione in cui ne fornisca uno solo. Dobbiamo segnalare questa eventualità.
Inoltre, i messaggi di errore visualizzati automaticamente dalla JRE in caso di eccezioni sono assolutamente incomprensibili per l'utente medio; dobbiamo fare di più.

Come segnaliamo l'errore di sintassi se l'user non fornisce almeno due argomenti sulla command-line? Beh, nello stesso modo in cui lo stesso linguaggio Java™ segnala un errore: SOLLEVANDO UNA ECCEZIONE.

Una eccezione viene sollevata con la keyword throw che letteralmente significa "lanciare", "buttare" o "gettare" ma che io preferisco tradurre con "sollevare" poichè in italiano questo è il verbo corretto da associare al concetto di eccezione.
Riprendendo il caso visto in Nessun argomento sulla command-line modifichiamo la fine del metodo parseCmdLine in questo modo:

private static void parseCmdLine( String[] args )
{
...
if ( numArgs < 2 ) {
throw new IllegalArgumentException( "devi specificare almeno due argomenti"
+ " sulla riga di comando" );
}
}

Il codice solleva (throw) una eccezione di tipo IllegalArgumentException, un tipo di eccezione già definita nella libreria di base di Java, e precisamete nel package java.lang. Uno dei costruttori di questa classe prende come argomento una stringa che rappresenta il messaggio di errore associato alla eccezione.
Il messaggio fornito al costruttore viene restituito dal metodo getMessage() da usare quando si deve fornire all'utente il motivo del fallimento del programma.

Si distinguono due tipi fondamentali di eccezioni:

  • le eccezioni unchecked (dette anche runtime exceptions)
  • le eccezioni checked

La differenza tra i due tipi di eccezione è assolutamente sostanziale: per le eccezioni checked è OBBLIGATORIO fornire un handler che gestisca la eccezione all'interno del metodo in cui la eccezione può verificarsi. In mancanza di un handler nello stesso metodo, è necessario specificare che il metodo può sollevare una eccezione di quel tipo nella dichiarazione del metodo stesso.
Il compilatore Java verifica in fase di compilazione che tutte le eccezioni checked abbiamo un handler o che abbiamo la firma della eccezione nella dichiarazione del metodo: il programma NON compila se questa condizione non viene soddisfatta.

Nel nostro caso, un gestore della eccezione nel metodo parseCmdLine non ha alcun senso: il gestore serve a correggere, se possibile, la condizione eccezionale (o l'anomalia) e ripristinare il corretto funzionamente del programma.
Ma un input errato da parte dell'utente non può essere corretto se non terminando il programma e mandandolo nuovamente in esecuzione con gli argomenti corretti; pertanto la eccezione IllegalArgumentException deve essere propagata al main il quale la gestirà terminando la app.
Pertanto, se le eccezioni sollevate dal metodo parseCmdLine fossero checked, saremmo costretti a specificarle entrambe nella dichiarazione del metodo. La specifica delle eccezioni che un metodo può sollevare si scrive così:

static void parseCmdLine( String[] args ) throws IllegalArgumentException
{
...
}

Ma come distinguo una eccezione di tipo checked da quella di tipo unchecked? Ebbene, la risposta sta nel design stesso dell'albero genealogico delle eccezioni. Il concetto di albero genealogico al momento sfugge ancora alla comprensione del lettore di questa guida; esso verrà affrontato in un capitolo successivo in cui sarà spiegata estensivamente la La programmazione ad oggetti e, nello specifico, nella sezione le classi derivate.
Per il momento basterà sapere che una classe può derivare da una altra classe e che la derivata si chiama child class (classe figlia) della classe da cui deriva la quale si chiama parent class (classe genitore). Poichè anche la classe figlia può avere delle derivate, si crea una specie di albero genealogico con classi figlie, genitori, nonni e bisnonni.
L'albero genealogico delle classi eccezione in Java comincia col capostipite che si chiama Throwable (=sollevabile) e si articola in questo modo:

               Throwable 
                   |
            ---------------------
           |                     |
         Error               Exception              
           |                     | 
    AssertionError        RuntimeException
                                 |
                    --------------------------
                   |                          |
           ArithmeticException     IllegalArgumentException   
                                              |
                                    NumberFormatException

Quella di cui sopra è solo una piccolissima parte dell'albero; ci sono decine e decine di altre classi eccezione ma quelle riportate sono quelle che ci interessano da vicino.

Le classi derivate da Error sono quelle che segnalano errori gravi, normalmente non ripristinabili. Una di queste derivate è la AssertionError che abbiamo incontrato in Scrivere una suite di test. Quando viene sollevata una eccezione derivata da Error solitamente non c'è altra cosa da fare che terminare il programma, come per esempio nel caso della assert.

Le classi derivate da Exception, di contro, vengono sollevate quando è ancora possibile il recover (= recupero) della situazione, almeno in determinate circostanze. Per esempio, nel caso di un programma interattivo, quello che abbiamo scritto in Invitare l'utente a fornire i dati, con l'invito a fornire i dati da parte dell'utente, se viene sollevata una NumberFormatException è possibile recuperare invitando di nuovo l'utente a fornire il dato corretto.
Nel design delle eccezioni in Java, tutte quelle che derivano da RuntimeException sono eccezioni unchecked e pertanto non hanno alcun bisogno di essere gestite nè di avere la specifica nella dichiarazione del metodo, la cosidetta signature (=firma) del metodo.

Avrete pure notato che tutte le eccezioni sollevate dal metodo parseCmdLine sono unchecked e che la NumberFormatException è figlia della IllegalArgumentException. Non solo, anche la eccezione ArithmeticException sollevata dai metodi overloaded mcm è una eccezione uncheckd: ecco perchè non siamo stati costretti a specificare queste eccezioni nella dichiarazione del metodo parseCmdLine, tuttavia se vengono specificate NON è considerato un errore.
Ma se specifichiamo le eccezioni unchecked nella dichiarazione del metodo esse diventano checked? NO, esse rimangono unchecked poichè figlie della RuntimeException.

Rimane da risolvere il problema della eccezione NumberFormatException il cui messaggio di errore non è molto user friendly. Ve lo ricordate?

Exception in thread "main" NumberFormatException: For input string: "65.4"

Noi avevamo in mente qualcosa di molto più esplicativo, come riportato in Numeri abbastanza grandi.
Come risolviamo la questione? Ebbene, non dobbiamo fare altro che intercettare la eccezione NumberFormatException e sollevarne un'altra, specificando nel costruttore il messaggio che vogliamo noi.

Per intercettare una eccezione si usa il blocco try..catch. La sintassi generale di questo costrutto è la seguente:

try {
... istruzioni che possono sollevare una eccezione ...
... metodi chiamati che possono sollevare una eccezione ...
}
catch( tipo_eccezione identificativo )
{
... istruzioni da eseguire se la eccezione si verifica
}

Analizziamo brevemente il codice:

  • try e catch sono keywords (=parole riservate) del linguaggio Java. Quindi non possiamo usarle come identificatori di variabili o metodi
  • il blocco di istruzioni e i metodi chiamati di cui vogliamo intercettare la eventuale eccezione vanno scritti entro una coppia di parentesi graffe (aperta e chiusa) subito dopo la keyword try
  • il blocco try deve essere immediatamente seguito dal blocco catch
  • le istruzioni e i metodi richiamati nel blocco catch devono essere racchiusi in una coppia di parentesi graffe (aperta e chiusa)
  • le istruzioni e i metodi richiamati nel blocco catch vengono eseguiti solo se la eccezione si verifica
  • il tipo_eccezione specifica quale eccezione deve essere intercettata, per esempio NumberFormatEception. Il blocco catch non sarà eseguito se si verifica un altro tipo di eccezione a meno che essa non sia figlia del tipo intercettato
  • si possono intercettare più di una eccezione nello stesso blocco catch usando l'operatore di bitwise OR (vedi Operatori di bitwise)
  • si possono specificare due o più blocchi catch

Esempio:

try {
// apre un canale di comunicazione sulla rete
SocketChannel channel = SocketChannel.open(
new InetSocketAddress( "www.acme.com", 18862 ));
}
catch( SecurityException | UnsupportedAddressTypeException ex )
{
... errori non ripristinabili, chiudi il programma ...
}
catch( IOException | UnresolvedAddressException ex )
{
... errori recuperabili ripristinando la connessione alla rete
}

Lo spezzone di codice suesposto tenta (try) di stabilire una connessione sulla rete Internet con l'host "www.acme.com", sulla porta 18862. aprendo un canale di comunicazione di classe SocketChannel.
Il metodo statico SocketChannel.open, se eseguito con successo, restituisce il canale di comunicazione; altrimenti solleva diversi tipi di eccezione.
I due tipi di eccezione intercettati nel primo blocco catch non sono recuperabili poichè uno ha a che vedere con la sicurezza e l'altro perchè il tipo di indirizzo fornito non è supportato. Non ci resta che chiudere il programma. Notate l'operatore di bitwise OR (il carattere pipe) scritto tra i due tipi di eccezione SecurityException e UnsupportedAddressTypeException.
I due tipi di eccezione intercettati nel secondo blocco catch sono recuperabili poichè entrambi sono sintomo di una mancata connessione ad Internet. In questo caso si potrebbe visualizzare un messaggio in cui si invita l'utente a verificare la connessione e ritentare. Notate anche in questo caso l'operatore di bitwise OR (il carattere pipe) scritto tra i due tipi di eccezione IOException e UnresolvedAddressException.

Ritornando al nostro metodo parseCmdLine inseriremo un blocco try..catch nello spezzone di codice dove viene richiamato il metodo statico Integer.parseInt ed intercetteremo la eccezione NumberFormatException. Nel blocco catch solleveremo una nuova eccezione di tipo IllegalArgumentException alla quale passeremo un nostro messaggio personalizzato come descritto in Numeri reali come argomenti.

static void parseCmdLine( String[] args ) throws IllegalArgumentException
{
... omissis ...
else {
try {
if ( numArgs == 1 ) {
alfa = Integer.parseInt( item );
}
else if ( numArgs == 2 ) {
beta = Integer.parseInt( item );
}
}
catch( NumberFormatException ex ) {
throw new IllegalArgumentException( "argomento \'" + item
+ "\' è invalido: deve essere un intero compreso tra "
+ Integer.MIN_VALUE + " e " + Integer.MAX_VALUE);
}
}
}
... omissis ...
}

Forse siete rimasti sorpresi dal fatto che nel messaggio di errore della nuova eccezione non abbia fatto riferimento ai due valori numerici minimo e massimo che possono essere memorizzati in un intero ma io abbia usato dei nomi di variabili: Integer.MIN_VALUE e Integer.MAX_VALUE.
Ebbene, queste due "variabili" sono definite come membri dati pubblici statici costanti nel corpo della classe Integer e si comportano come costanti: il loro valore NON può essere modificato. In Java, ma questo accade in tutti i linguaggi di programmazione, si usa assegnare nomi mnemonici ad alcune costanti, specialmente quelle matematiche.
Nel caso della classe Integer vi sono due membri dati che sono definiti in questo modo:

public static final int MAX_VALUE 2147483647
public static final int MIN_VALUE -2147483648

il cui significato mi sembra alquanto self explanatory. Altro esempio di costante matematica è il seguente, definito in java.lang.Math:

public static final double PI 3.141592653589793

e rappresenta, come ben potete immaginare, il pigreco. La keyword final scritta come attributo ad una variabile indica che il valore della variabile stessa non può essere modificato rendendola, di fatto, una cosidetta costante mnemonica.

L'ultimo tocco di raffinatezza per il nostro programmino riguarda il blocco switch in cui analizziamo il carattere che rappresenta l'opzione alla riga di comando. Nel blocco valutiamo le opzioni riconosciute e cioè la -h e la -t ma ci disinteressiamo del caso in cui il carattere non è riconosciuto. Cosa succede se l'utente specifica la opzione -x? Nel blocco switch questa situazione ricadrebbe nel case default: che, però, non esegue alcunchè. La opzione sarebbe, di fatto, ignorata il che non sarebbe proprio un errore ma non è in linea con la sintassi del comando.
Se l'utente non usa la sintassi corretta, cosa dobbiamo fare? Ma è ovvio, sollevare una eccezione!

switch( item.charAt(1) ) {
case 'h' :
printUsage();
System.exit(0);
break;
case 't' :
test1();
System.exit(0);
break;
case '-' :
forceArgument = true;
break;
default :
// solleva una eccezione in caso di opzione non riconosciuta
throw new IllegalArgumentException( "l\'opzione: \'" + item + "\' è sconosciuta" );
Infine, dobbiamo modificare il comportamento della JVM che, in caso di
eccezioni visualizza un messaggio alquanto incompresibile per l'utente.
Per le eccezioni che noi prevediamo il messaggio sarà personalizzato.
Eccovi due esempi:
\verbatim
>java -ea McMd 65 pippo
McMd calcola il MCD ed il MCM tra due numeri
ERRORE: argomento 'pippo' è invalido: deve essere fornito un numero intero
compreso tra -2147483648 e 2147483647
\endverbatim
Modifichiamo il \c main intercettando le eccezioni:
\code
public static void main( String[] args )
{
System.out.println( "McMd calcola il MCD ed il MCM tra due numeri" );
try {
parseCmdLine( args );
int m1 = mcd( alfa, beta );
int m2 = mcm( alfa, beta, m1 );
System.out.println( "MCD(" + alfa + "," + beta + ") = " + m1 );
System.out.println( "MCM(" + alfa + "," + beta + ") = " + m2 );
}
catch( IllegalArgumentException ex )
{
System.out.println( "ERROR: " + ex.getMessage());
}
catch( Throwable ex )
{
System.out.println( "UNEXPECTED EXCEPTION: " + ex.getClass().getName());
ex.printStackTrace();
}
}

Se la eccezione sollevata dal nostro programma è la sola IllegalArgumentException, che senso ha intercettare anche Throwable?
Ebbene, cosa ci garantisce che il codice non sollevi qualche altra eccezione di tipo unchecked che noi non abbiamo previsto, o di cui non siamo a conoscenza? Nessuno ce lo garantisce: la JVM può sollevare una pletora di eccezioni non previste.

Supponiamo che in certe particolari circostanze possa accadere qualcosa di imprevisto come l'esaurimento della memoria dedicata alla JVM: in questo caso verrebbe sollevata una eccezione di tipo OutOfMemoryError. Ecco perchè è sempre buona abitudine intercettare nella ultima catch la classe base di tutte le eccezioni: Throwable nel cui blocco catch troviamo le seguenti istruzioni:

System.out.println( "UNEXPECTED EXCEPTION: " + ex.getClass().getName());
ex.printStackTrace();

che ci danno le informazioni sul nome della classe eccezione effettivamente sollevata, vedi ex.getClass().getName(), e la visualizzazione dello stack con i nomi di tutti i metodi richiamati, a ritroso, fino al main.

Lo schema di versioning

Ridendo e scherzando siamo già giunti alla versione 06 del nostro semplice programmino. Uno degli aspetti più ostici e trascurati nelle guide e nei tutorial di ogni linguaggio di programmazione è quello del tracciamento delle versioni del programma.
Se ora vi chiedessi: vi ricordate cosa abbiamo implementato nelle diverse versioni del programma? Scommetto che la risposta è NO. Ma è del tutto comprensibile: nessuno si ricorda che cosa è stato fatto nelle varie versioni a meno che non si prenda nota di tutto.

Eppure, è di fondamentale importanza tenere traccia delle modifiche apportate ad un listato sorgente. Premetto che ogni programmatore è libero di usare qualsiasi strategia egli desidera per il versioning (=versionamento? che brutta parola) delle applicazioni ma ci sono diverse linee guida che si dovrebbero seguire.
Per programmini cosidetti on-the-fly (=scritti al volo) potrebbe essere una buona strategia quella di tenere traccia delle varie versioni nel listato sorgente stesso, per esempio nel commento iniziale:

/*
  * mcmd06.java - calcola MCD / MCM di due numeri interi
  *
  * ... omissis ...
  *
  * CHANGELOG
  *     01 input hardcoded
  *     02 ottimizzazione MCM - metodo 'mcm' overloaded
  *     03 implementata test suite nel metodo 'test1'
  *     04 implementato invito tramite 'Scanner' per dati di input
  *     05 dati di input specificati nella command-line
  *     06 implementate le eccezioni e risolti i bugs, inserito un eastern egg
 */

il changelog (=diario delle modifiche) è uno dei componenti essenziali di ogni programma professionale. Per un programmino che si compone di un unico file sorgente il changelog può benissimo essere inserito nel commento iniziale del sorgente.
Ma i sorgenti sono tanti mentre il file oggetto, quello che contiene il bytecode è sempre uno ed uno solo: il file mcmd.class. Come riconosco a quale versione corrisponde? In altre parole, quale è il file sorgente che ho compilato per ultimo?

Digitate il seguente comando:

> javac --version

Il lettore avrà già capito dove voglio andare a parare: ebbene, ogni programma CLI (a riga di comando) che si rispetti possiede almeno due opzioni:

  • -h oppure --help che visualizza la sintassi del comando
  • -v oppure --version che visualizza la versione del programma

Pertanto, implementeremo la opzione -v anche nel nostro semplice programmino a partire dalla versione 06. Non ha molto senso implementarla nelle versioni precedenti dal momento che sono versioni di prova, non destinate al rilascio poichè piene di bugs e inadatte all'utilizzo da parte di un utente.

static int[] parseCmdLine( String[] args )
{
... omissis ...
case 'v' :
System.out.println( "mcmd versione 06" );
System.exit(0);
break;
... omissis ...
}

La documentazione

Un altro aspetto sempre trascurato dai tutorial (e non solo, anche dai programmatori, specialmente i neofiti) è quello della documentazione: un programma, per quanto utile, non sarà mai utilizzato senza la documentazione tecnica o un manuale d'uso.

In questa sezione impareremo ad usare il tool di documentazione che fa parte del Java Development Kit (JDK): si tratta, manco a dirlo, di un tool a riga di comano (CLI) il cui nome è javadoc. Questo tool crea una documentazione in formato HTML e prende l'input dai files sorgente stessi (cioè i files .java). Il programmatore deve semplicemente formattare le righe di commento nei files sorgente in un modo particolare, comprensibile al tool di documentazione. Divideremo pertanto i commenti in due categorie:

  • i commenti javadoc sono quelli elaborati dal tool di documentazione
  • tutti gli altri commenti che chiameremo non-javadoc

Un commento javadoc parte con la sequenza di caratteri tipica del inizio commento ma con due asterischi anzichè uno: ⁄∗∗ e si conclude con la sequenza classica della fine del commento: ∗⁄. Quindi il commento javadoc è un commento a tutti gli effetti, in stile "C", ed è assolutamente ignorato dal compilatore dal momento che è un commento:

/** Questo è un commento javadoc */

/* questo NON è un commento javadoc */

// questo NON è un commento javadoc

La regola generale del commento javadoc è che il commento deve precedere il codice che si vuole documentare che può essere una classe, un membro dati o un metodo.
Normalmente, tutti i metodi ed i membri dati di una classe dovrebbero essere documentati oltre alla classe stessa:

/** descrione breve della classe.
 * descrizione approfondita della classe
*/
class MyClass 
{
    /** descrizione breve del membro dati.
     * descrizione approfondita del membro dati
    */
    int myDataMember 
    
    /** descrizione breve del metodo.
     * descrizione approfondita del metodo
    */
    void myMethod()
    {
    }
}

Da notare che la descrizione breve di una classe, metodo o membro dati termina con il simbolo di punteggiatura punto. La descrizione approfondita è detta anche descrizione lunga.

La documentazione della classe mcmd

Aprite il file sorgente mcmd07.java che contiene tutte le modifiche descritte in questa sezione. Il commento della classe mcmd è il seguente:

/** Calcola il MCD ed il MCM di due numeri interi.
 * Questo è un programma con interfaccia CLI che calcola il 
 * Massimo Comune Divisore (MCD) ed il Minimo Comune Multiplo (MCM)
 * di due numeri interi usando
 * <a href="https://it.wikipedia.org/wiki/Algoritmo_di_Euclide"
 * target=_blank>l'algoritmo di Euclide</a>
 * <br />
 * I due metodi statici che calcolano il MCD ed il MCM sono:
 * {@code mcd} e {@code mcm}.
 *
 * @see mcd 
 * @see mcm 
*/

Poichè il risultato del tool javadoc sono pagine HTML è possibile inserire nei commenti qualsiasi tag HTML. Per esempio, nella descrizione lunga della classe mcmd è stato inserito un link alla pagina di Wikipedia che tratta l'algoritmo di Euclide.

Oltre ai tag HTML, javadoc riconosce alcuni speciali comandi come quelli visti sopra:

  • @see questo comando speciale introduce il paragrafo detto See Also (=vedi anche) nella descrizione lunga del costrutto da documentare.
  • {@code testo}: che scrive testo in caratteri monospaced.

La documentazione dei membri dati

Normalmente, per i membri dati si scrive solo la descrizione breve, a meno che non si voglia inserire i blocchi seealso che rimandano a uno o più metodi:

    /** il primo numero da elaborare */
    static int alfa = 0;
    
    /** il secondo numero da elaborare */
    static int beta = 0;

La documentazione del metodo mcm

I metodi vanno documentati in un modo piuttosto esaustivo:

  • dovrebbero avere sia una descrizione breve che una descrizione lunga a meno che non siano talmente banali per cui basta quella breve
  • se hanno degli argomenti, ogni argomento deve essere documentato con un comando speciale @param
  • il valore ritornato dal metodo, se non è void, deve essere documentato con un comando speciale @return
  • se possono sollevare eccezioni ognuna di esse va documentata con un comando speciale @throws
  • per quanto riguarda le eccezioni, il comando speciale @throws và usato anche per le eccezioni unchecked

Prendiamo ad esempio il metodo mcd, il commento javadoc per questo metodo sarà il seguente:

    /** Calcola il MCD.
     * Il metodo implementa l'algoritmo ricorsivo di Euclide per il calcolo 
     * del MCD.
     *
     * @param alfa il primo numero da elaborare
     * @param beta il secondo numero da elaborare
     * @return il MCD dei due numeri 
     * @throws OutOfMemoryError se {@code alfa} è pari a 18862 e {@code beta} è pari a 3963
    */

Il file overview.txt

Come accennato, il tool javadoc produce pagine HTML organizzate in moduli, packages, classi, membri dati e metodi (affronteremo l'argomento packages e moduli in un capitolo futuro). La pagina principale della documentazione prodotta da javadoc (la pagina index.html, per intenderci) è la pagina overview (=panoramica) che dovrebbe dare una panoramica generale del modulo o del package che stiamo documentando.

Di default, la pagina overview viene prodotta dal tool automaticamente e contiene l'elenco dei packages che compongono il modulo. Tuttavia, è buona abitudine personalizzare la pagina overview scrivendo una breve panoramica del programma. Per esempio, una tipica pagina overview dovrebbe indicare:

  • lo scopo principale del programma o applicazione
  • la sintassi del comando, se è un programma CLI
  • i limiti entro i quali il programma può operare.

Per creare una pagina overview si deve scrivere un file esterno in formato HTML. Questo file è già presente nella sottocartella javatutor1/projects/mcmd/.

Il file docoptions.txt

Per produrre la documentazione, utilizziamo il tool a riga di comando (CLI) javadoc la cui sintassi è piuttosto complessa. Provate a digitare il seguente comando:

> javadoc --help

ed otterrete una lunga lista di opzioni che possono essere specificate per ottenere un output personalizzato. Comunque, non è necessario studiarsi tutte le opzioni possibili poichè le opzioni principali e da usare quasi sempre sono sempre le stesse. Non è nemmeno necessario segnarsele da qualche parte: il tool permette di memorizzarle in un file separato e di eseguire il comando javadoc specificando il file che contiene le opzioni preceduto dal carattere @.

Predisporremo quindi un file di testo che chiameremo docoptions.txt che avrà il seguente contenuto:

-d docs
-use
-private
-splitindex
-windowtitle 'mcmd'
-doctitle 'Calcola il MCD ed il MCM'
-header '<b>Version 0.7</b>'
-bottom 'Copyleft Luciano Cattani.'
-overview overview.html
mcmd07.java 

Analizziamo alcune opzioni:

  • -d specifica la directory dove saranno memorizzati i files HTML che compongono la documentazione
  • -bottom specifica il testo da visualizzare in fondo ad ogni pagina HTML
  • -header specifica il testo da visualizzare in alto a destra in ogni pagina HTML
  • -overview specifica quale file deve essere la pagina principale della documentazione (la pagina overview)

Alla fine delle opzioni, vanno specificati tutti i files sorgente che contengono commenti javadoc e che vanno documentati; nel nostro caso, l'unico file sorgente è il mcmd07.java che contiene i commenti javadoc

Non è assolutamente necessario documentare tutte le classi, tutti i metodi e tutti i data membri: è possibile lasciare senza commento qualunque costrutto nei files sorgente. Ovviamente nessuna documentazione verrà prodotta per quel costrutto che non contiene commenti javadoc ma il tool vi avverte se avete dimenticato qualche metodo o membro dati da documentare: se non era una dimenticanza e lo avete lasciato appositamente senza commento, ignorate il warning
Per creare la documentazione del programma mcmd basta digitare:

> javadoc @docoptions.txt

Al seguente link potete vedere il risultato del comando di cui sopra.

La documentazione online

Per maggiori informazioni sul tool di documentazione Java seguite questo link: javadoc version 22

La versione 08 di mcmd

Benchè assolutamente funzionante, la versione 07 di mcmd ci lascia piuttosto insoddisfatti considerato che il nostro programmino potrebbe gestire dati in input di tipo long anzichè limitarsi al tipo int. Ricordate cosa è accaduto in occasione del bug Numeri molto grandi?
Possiamo considerare quattromiliardi un numero "molto grande"? Beh, non proprio. Ma se usassimo i tipi long per i numeri in input, allora si che la grandezza dei numeri necessari ad avere un overflow sarebbe davvero "molto grande", dell'ordine dei "miliardi di miliardi".

Aprite il file mcmd08.java che contiene la versione 08 del programma. Per gestire numeri di tipo long nel programma mcmd non dobbiamo fare altro che sostiuire tutte le variabili di tipo int in variabili di tipo long ad eccezione di una: quella che, nel metodo parseCmdLine tiene il conteggio degli argomenti non-opzione digitati sulla cmdline dall'utente.

Compilate ed eseguite il programma con i dati di input di cui al bug Numeri molto grandi nel quale un numero in input di quattromiliardi aveva provocato overflow:

> javac mcmd08.java
> java -ea mcmd 4000000000 9300

Adesso il programma funziona ed i due risultati vengono visualizzati:

mcmd - calcola il MCD ed il MCM tra due numeri
MCD(9300,4000000000) = 100
MCM(9300,4000000000) = 372000000000

Ma abbiamo un problema ... e scommetto che il lettore lo avrà già individuato: implementando il tipo long come dato in input avremo nuovamente il problema del overflow nel calcolo del MCM; provate a calcolare MCD ed MCM di due numeri primi che non trovano capienza nei 32 bits come per esempio 4.000.000.019 e 4.000.000.063:

>java -ea mcmd 4000000063 4000000019
mcmd - calcola il MCD ed il MCM tra due numeri
MCD(4000000063,4000000019) = 1
MCM(4000000063,4000000019) = -2446743745709550419

Ancora una volta otteniamo un numero negativo come MCM! Appare ovvio che il MCM di questi due numeri, essendo numeri primi, è il loro prodotto e che esso è sicuramente maggiore di 64 bits che è la capienza massima di un long. Ci servirebbe un tipo di dato intero da 128 bits per evitare l'overflow ma un simile tipo di dato intero al momento non esiste in Java. Però, c'è una altra soluzione.

In Java, come in tutti i linguaggi, gli operatori aritmetici (vedi Operatori aritmetici) eseguono le operazioni ignorando la condizione di overflow per ottenere la maggior velocità di esecuzione possibile. Ma esistono metodi dedicati che consentono al programmatore di essere avvisato se una qualsiasi operazione aritmetica provoca overflow.
Nel nostro caso, l'operazione che dobbiamo monitorare è quella del metodo mcm, quello che calcola il MCM dei due numeri. Aprite il file sorgente mcmd08bis.java che contiene le modifiche per correggere questo bug.

Dobbiamo modificare il sorgente in questo modo:

static long mcm( long alfa, long beta, long mcd )
{
if ( alfa == 0 && beta == 0 ) {
return 0;
}
alfa = Math.abs(alfa);
beta = Math.abs(beta);
// uso il metodo statico 'Math.multiplyExact' per calcolare il
// MCM e non il semplice operatore di moltiplicazione
long result = Math.multiplyExact( alfa / mcd, beta ) ;
return result;
}

Il metodo statico Math.multiplyExact moltiplica i due interi lunghi forniti come argomenti e restituisce il loro prodotto come un intero lungo. Nel caso di overflow il metodo solleva una eccezione di tipo ArithmeticException.

Ricompiliamo il programma con la modifica al metodo mcm e verifichiamo l'output del programma in caso di overflow nei calcoli:

>javac mcmmd08bis.java
>java -ea mcmd 4000000063 4000000019
mcmd - calcola il MCD ed il MCM tra due numeri
UNEXPECTED EXCEPTION: java.lang.ArithmeticException
java.lang.ArithmeticException: long overflow
        at java.base/java.lang.Math.multiplyExact(Math.java:1033)
        at mcmd.mcm(mcmd08bis.java:113)
        at mcmd.main(mcmd08bis.java:133)

il messaggio riportato dalla eccezione: "long overflow" è alquanto eloquente, almeno per noi programmatori. Non lo è molto per l'utente medio, certo, ma a questo si rimedia subito, per giove!

public static void main( String[] args )
{
System.out.println( "mcmd - calcola il MCD ed il MCM tra due numeri" );
try {
... omissis ...
}
// intercetto la 'ArithmeticException' e fornisco all'utente un
// messaggio più comprensibile
catch( ArithmeticException ex )
{
System.out.println( "ERRORE: impossibile calcolare MCM; il risultato è troppo grande" );
}
catch( ... omissis ...
}

Aggiorneremo anche il file overview.txt in modo da documentare i nuovi limiti del programma:

  • Fino alla versione 07 compresa, il programma è in grado di gestire in input solo quantità numeriche nell'intervallo del tipo intero di Java (32 bits) il cui range va da -2147483648 a +2147483647.
  • In versione 08, il programma è in grado di gestire in input solo quantità numeriche nell'intervallo del tipo long di Java (64 bits) il cui range va da circa da -9,22x1018 a circa +9,22x1018.

Lo stesso limite si applica al risultato del calcolo del Minimo Comune Multiplo.

La versione 09 di mcmd

Non siamo ancora soddisfatti? I limiti del programma non sono abbastanza grandi?
In effetti, di programmini che calcolano MCM/MCD ce ne sono a centinaia, o forse anche a migliaia in giro per Internet. Quindi il nostro programma dovrebbe dare qualcosa in più, quel qualcosa che gli altri non possiedono e questo qualcosa potrebbe essere la possibilità di specificare in input numeri grandi anzi, moooolto grandi, numeri che possono essere composti da milioni di cifre decimali!

La libreria standard di Java mette a disposizione del programmatore una classe il cui identificatore è BigInteger che rappresenta un intero con segno a precisione arbitraria; in altre parole che può contenere un numero di cifre a piacere, virtualmente infinito. In realtà, i limiti esistono anche per la classe BigInteger dal momento che, comunque, ci sarebbero limiti fisici: il grande intero deve essere memorizzato nella RAM del computer la cui capienza è un valore finito, anche se molto grande.
La classe BigInteger è in grado di contenere interi nel range che va da -2Integer.MAX_VALUE a +2Integer.MAX_VALUE (esclusi i limiti) il che corrisponde a numeri decimali composti da oltre mezzo miliardo di cifre! Si tratta di numeri talmente grandi che solo per digitarli alla tastiera, ipotizzando di rituscire a digitare 2 caratteri al secondo, impiegheremmo 15 anni.
E' pur vero che la precisione di BigInteger non è proprio arbitraria nel senso di infinita ma è talmente grande da essere, di fatto, irrangiungibile.

Per il resto, la classe BigInteger rappresenta un intero con segno in formato binario in complemento a due, esattamente come lo sono i tipi primitivi int e long. Tutte le operazioni che possono essere eseguite con un int, possono essere eseguite anche con BigInteger: le operazioni aritmetiche, il bitwise, i confronti, lo scorrimento verso destra e sinistra, il modulo, etc.
L'unico neo della classe BigInteger (delle classi in generale) è che non è possibile usare gli operatori a cui siamo abituati (vedi Gli operatori Java) ma è necessario richiamare i metodi della classe che ne simulano il comportamento. Così, per ottenere il modulo di due "interi grandi" non possiamo scrivere:

BigInteger alfa, beta, gamma;
gamma = alfa % beta;

ma invece:

BigInteger alfa, beta, gamma;
gamma = alfa.mod( beta );

Il file sorgente mcmd09.java contiene la versione di mcmd che usa la classe BigInteger per l'input dei dati e per i risultati il che consente all'user di inserire numeri grandi a piacere (salvo i limiti sopraesposti) e che rende questo piccolo programmino diverso da molti altri.
Al momento attuale, il lettore non ha alcuna familiarità con il concetto di classe e, pertanto, potrebbe non comprendere fino in fondo il sorgente mcmd09.java ma il prossimo capitolo tratterà proprio questi temi: le classi, gli oggetti, e le istanze.

Argomento precedente - Argomento successivo - - Indice Generale