IL LINGUAGGIO JAVA Dispense per il corso di laboratorio di sistemi – I.T.I.S. ABACUS – A.S. 2008/2009 Autore: Roberto Amadini Testo di riferimento: La programmazione ad oggetti C++ Java (Lorenzi, Moriggia, Rizzi – ATLAS) 2. IL LINGUAGGIO JAVA 2.1 Introduzione Java è un linguaggio di programmazione ad alto livello orientato agli oggetti. Il processo di sviluppo di Java cominciò agli inizi degli anni ’90 da un gruppo di ingegneri della Sun Microsystems, mentre la sua prima apparizione ufficiale avvenne nel maggio del 1995 . Le principali caratteristiche di Java sono: • • • • L’orientamento agli oggetti La portabilità delle sue applicazioni La possibilità di costruire GUI (Graphical User Interface) La gestione di multithreading e networking L’ORIENTAMENTO AGLI OGGETTI consiste nella presenza di costrutti che implementano concetti propri della OOP come ereditarietà, polimorfismo, incapsulamento, gestione delle eccezioni ecc… Java è un linguaggio OO di tipo ibrido in quanto prevede la possibilità di gestire tipi di dato primitivi oltre agli oggetti (ad esempio int, char, float, double ecc…). Tuttavia spesso Java viene considerato come linguaggio puro in quanto gli oggetti costituiscono la struttura portante del linguaggio, mentre i tipi primitivi ne rappresentano solo una parte marginale. Di fatto questo linguaggio deriva direttamente dal C++, per cui molte caratteristiche di Java sono del tutto simili (se non addirittura identiche) al C++. Le maggiori differenze a livello di programmazione tra Java e C++ riguardano: • L’EREDITARIETA’: In Java si utilizza solamente l’ereditarietà SINGOLA (cioè una sottoclasse può avere al massimo una classe padre) mentre il C++ permette l’ereditarietà MULTIPLA (una sottoclasse può derivare da più classi padre). In realtà anche Java implementa un concetto simile all’ereditarietà multipla, grazie all’ausilio di classi interfaccia (interfaces) opportunamente implementate (implements). • La gestione della MEMORIA DINAMICA: In Java, fatta eccezione per i tipi primitivi, ogni variabile (array e oggetti) è di tipo riferimento (reference): le variabili contengono cioè un riferimento all’indirizzo di memoria dove sono allocati i dati (e i metodi) contenuti dalle variabili. Al contrario del C++, non è possibile utilizzare esplicitamente i puntatori per manipolare l’allocazione e la de-allocazione dinamica di oggetti in memoria; in particolare la de-allocazione è gestita in modo automatico dal sistema di run-time mediante un apposito strumento chiamato garbage collector (letteralmente “raccoglitore di rifiuti”, si occupa di “ripulire” la memoria). • La forte TIPIZZAZIONE: In Java le conversioni di tipo (cast) devono essere tutte esplicitate (al contrario il C++ permette in taluni casi conversioni non esplicitate). Fanno eccezione le promozioni di tipo (conversioni senza perdita di dati, ad esempio da int a float). La PORTABILITA’ è una proprietà fondamentale di Java; essa consiste nella capacità di poter eseguire codice Java su qualsiasi piattaforma in modo indipendente dall’hardware e dal sistema operativo sul quale il programma è in esecuzione. La portabilità in Java è realizzata con un sistema misto di compilazione/interpretazione: • • • Il codice sorgente (estensione .java) viene compilato dal compilatore di java (comando javac) che trasforma il codice sorgente in un apposito codice detto BYTECODE (estensione .class) Il bytecode non può essere eseguito da una macchina reale ma solamente da una macchina virtuale: la JAVA VIRTUAL MACHINE (JVM). La JVM è uno strato software che si pone “sopra” la macchina fisica dove è in esecuzione il file; il suo compito consiste nel permettere l’esecuzione del bytecode anche sulla macchina fisica interpretando ogni sua riga, cioè traducendo il bytecode (comprensibile solo dalla JVM) in codice macchina eseguibile sulla particolare macchina fisica (comando java). In questo modo, posso eseguire un file .class di java su qualsiasi piattaforma senza bisogno di dover modificare e/o ricompilare il codice sorgente: la JVM della particolare macchina fisica si occuperà del processo di interpretazione. CODICE SORGENTE COMPILAZIONE BYTECODE JVM INTERPRETAZIONE MACCHINA FISICA MioFile.java javac MioFile.Java MioFile.class java MioFile Eseguo MioFile Questa soluzione tuttavia è poco efficiente, in quanto l’interpretazione ha tempi decisamente maggiori rispetto alla compilazione. Per questo motivo, tutte le implementazioni recenti di JVM hanno incorporato un compilatore just-in-time (JIT compiler), cioè un compilatore interno, che al momento del lancio traduce “al volo” il programma bytecode Java in un normale programma nel linguaggio macchina del computer ospite (cioè anziché compilare il sorgente ed interpretare il corrispondente bytecode, di fatto compilo il sorgente e poi grazie al JIT ri-compilo il corrispondente byte code). Questi accorgimenti, a prezzo di una piccola attesa in fase di lancio del programma, permettono di avere delle applicazioni Java decisamente più veloci e leggere. Tuttavia, anche così Java resta un linguaggio meno efficiente dei linguaggi compilati come il C++, scontando il fatto di possedere degli strati di astrazione in più (la JVM) e di implementare una serie di automatismi, come il garbage collector, che se da un lato fanno risparmiare tempo ed errori in fase di sviluppo dei programmi, dall'altro sprecano spazio di memoria e tempo di CPU in fase di esecuzione del programma. Per poter programmare ed eseguire codice Java gli strumenti necessari sono: • • • 2.2 Un EDITOR di testo per scrivere il codice (ad es. Blocco note, emacs…) Un COMPILATORE (javac) ed un INTERPRETE java (java). Tali strumenti fanno parte del JDK (Java Development Kit), che comprende inoltre altre funzionalità per lo sviluppo di codice java (ad es., strumenti per la documentazione automatica del codice). Ad ogni JDK si associa quindi una JVM per la traduzione del bytecode e un JRE (Java Run-Time Environment) che permette l’esecuzione effettiva delle applicazioni Java sulla macchina fisica. Esistono tuttavia AMBIENTI INTEGRATI (IDE, Integrated Development Environment) che integrano Editor, JDK, JVM e JRE. Tra i più diffusi attualmente citiamo NetBeans ed Eclipse. La struttura di un programma Java In Java, a differenza del C++, un applicazione può essere costituita da una o più classi. Tra tutte le classi che compongono un applicazione Java, una si differenzia dalle altre perché: • • Ha lo stesso nome del file sorgente che contiene la classe Contiene lo speciale metodo main. Questo metodo è fondamentale perché l’esecuzione dell’applicazione Java comincia a partire da questo metodo. Vediamo quindi lo scheletro di un generico programma prova.java costituito da una sola classe: /* Importo le librerie (package) necessarie */ import Package_1; ... import Package_n; /* Classe principale, contiene il main e deve avere lo stesso nome del file sorgente. */ class prova { /* Metodo principale, necessario per l’esecuzione */ public static void main(String args[]) { // dichiarazione variabili // istruzioni del programma } // fine main } // fine classe nomeFile I package Java I package di Java sono insiemi di classi che forniscono servizi simili. Ogni utente può creare un proprio package oppure importare un package pre-esistente. I package di base di java (J2SE 6.0) sono: • • • • • • • • java.lang java.util java.io, java.nio java.math java.net java.security java.sql java.awt, java.swing funzionalità di base del linguaggio e tipi di dato fondamentali classi di collezione (strutture dati quali Vector, Stack, List, ecc…) operazioni si input/output operazioni aritmetiche in multi precisione operazioni di networking gestione di sicurezza e crittografia operazioni su basi di dati gestione di interfaccie GUI Per importare un package predefinito si usa l’istruzione import nomePackage; in questo modo sarà possibile utilizzare tutte le funzionalità del package specificato. Di fatto quest’istruzione corrisponde alla direttiva di pre-processing #include che è possibile utilizzare in C++ per includere librerie o file esterni al sorgente. 2.3 Il metodo main Come per il C++, il metodo main è fondamentale per il programma, in quanto da esso parte l’esecuzione del codice. A differenza del C++, nel quale il main è una funzione, in Java il main è un metodo: in sostanza è una funzione propria della classe che lo contiene (nel nostro esempio la classe prova, che deve necessariamente avere lo stesso nome del sorgente in cui è contenuta: prova.java). In Java infatti non esistono funzioni (al contrario del C++, essendo tale linguaggio fortemente legato al linguaggio procedurale C) ma solamente metodi: ogni “funzione” quindi deve appartenere ad una certa classe. Per questo motivo Java viene spesso considerato linguaggio object-oriented puro. Nell’esempio inoltre il metodo main è preceduto da tre parole-chiave: • • public static • void indica che il metodo è pubblico, cioè visibile ed accessibile da chiunque. indica che il metodo è statico, cioè associato alla classe in cui è contenuto (prova) e non ad oggetti della classe. Per ora ci basti sapere solo questo… indica che non ritorna nulla (esattamente come in C++) Inoltre, il main possiede anche un parametro: String args[] Come si può intuire, tale parametro è un array di stringhe, che corrisponde ai parametri passati da riga di comando quando l’applicazione viene lanciata. Basandoci su quanto appena visto, scriviamo ora il nostro primo programma in Java: /* Questo semplice programma legge una stringa da riga di comando e la stampa a video. Non importo nessuna libreria, in quanto il programma non lo richiede (non utilizzo costrutti particolari) */ class primoProg { public static void main(String args[]) { String stringaLetta = args[0]; System.out.println( “Hai inserito la stringa: “ + stringaLetta); } // fine main } // fine classe primoProg Innanzitutto si noti che la sintassi e la semantica dei commenti ( /* … */ e // ) è identica al C++. Inoltre è importante sottolineare che come il C++ anche Java è case-sensitive: non è indifferente utilizzare le maiuscole anziché le minuscole e viceversa. Il sorgente java si chiamerà primoProg.java (stesso nome della classe contenente il main). Il nostro programma per prima cosa dichiara una variabile di tipo String (stringa di caratteri, cfr. tipo string in C++) di nome stringaLetta che inizializza con il valore di args[0]. Cosa significa? In pratica, quando l’utente da riga di comando esegue il programma, può aggiungere uno o più parametri di tipo stringa che vengono automaticamente inseriti nell’array args. Quindi, all’interno del main è possibile estrarre tali parametri; nel nostro esempio estraggo il valore di args[0](che corrisponderà al primo parametro inserito) e lo inserisco nella variabile stringaLetta. Dopodiché, stampo a video un messaggio contenente il valore di stringaLetta. Per fare ciò utilizzo il metodo println della classe System.out che permette di stampare su standard output (il video) tale messaggio. Ciò è l’equivalente del cout in C++; si noti tuttavia il mancato utilizzo dell’operatore <<: la concatenazione delle stringhe “Hai inserito la stringa: “ e stringaLetta avviene mediante l’operatore +. Vediamo ora come è possibile eseguire il nostro primo programma primoProg.java Per scrivere il programma è sufficiente un qualsiasi editor di testo (ad es. Blocco note); ovviamente bisogna ricordarsi di salvare il file con l’estensione .java Una volta scritto, il programma va compilato: apriamo quindi il prompt dei comandi di MS-DOS e, dopo esserci posizionati nella cartella dove è presente il file primoProg.java, digitiamo: javac primoProg.java A questo punto, se non ci sono errori, viene generato il bytecode del sorgente: primoProg.class Ora per eseguire il programma è sufficiente richiamare l’interprete Java che trasforma il bytecode in linguaggio macchina: java primoProg Il programma verrà quindi eseguito, stampando a video il messaggio: Hai inserito la stringa: Perché? Semplicemente perché non abbiamo inserito alcun parametro da riga di comando! Se invece provassimo a digitare (senza dover ricompilare!) java primoProg Ciao!!! Allora l’output del programma sarà: Hai inserito la stringa: Ciao!!! In questo caso infatti args[0]conterrà la stringa Ciao!!! E se invece scrivessimo da riga di comando: java primoProg Ciao mondo!!! In questo caso l’output sarà: Hai inserito la stringa: Ciao Questo perché i parametri passati da riga di comando sono due: Ciao e mondo!!! Tali parametri verranno quindi inseriti rispettivamente in args[0] e args[1], ma siccome il programma stampa solamente il primo parametro (Ciao), il secondo (mondo!!!) viene ignorato. 2.3 Gli identificatori Gli identificatori sono nomi (sequenze di lettere e numeri) che possono essere attribuiti a variabili, metodi, classi ed oggetti. Come per il C++ non tutti gli identificatori sono formalmente corretti: esistono delle regole ben precise (ad es. devono cominciare con una lettera, non possono contenere caratteri speciali, devono avere una lunghezza massima, ecc…). In particolare, un identificatore non può chiamarsi come una parola-chiave di Java, in quanto tali parole sono riservate per particolari scopi. Le parole-chiave di Java sono ad oggi (J2SE 6.0) cinquanta: abstract assert boolean break byte case catch char class const continue default do double else enum extends final finally float for goto if implements import instanceof int interface long native Le celle evidenziate sono parole chiave anche del C++. new package private protected public return short static strictfp super switch synchronized this throw throws transient try void volatile while 2.4 Variabili, costanti e tipi di dato La sintassi e la semantica delle variabili è molto simile al C++; una variabile è caratterizzata da un nome, un tipo, un valore e dalla sua visibilità (scope). Ad esempio: int num; float altezza = 1.68; boolean x = true; //equivalente al tipo bool in C++ byte temperatura; //intero a 8 byte (può contenere valori da -128 a 127) Come nel C++ esistono regole di scope per le variabili. In sintesi una variabile è visibile all’interno del blocco in cui è dichiarata ed invisibile all’esterno del blocco; inoltre a differenza del C++ non è possibile dichiarare variabili con lo stesso nome all’interno di blocchi annidati, cioè non è possibile avere una situazione di questo tipo: { int x; ... { int x; //ri-dichiarazione di x: NON PERMESSO IN JAVA! ... } //blocco interno } //blocco esterno Una costante è una variabile che deve assumere sempre lo stesso valore per tutta la durata del programma; in Java anziché la parola chiave const si utilizza final, ad esempio: final double PI_GRECO = 3.14; Tipicamente in Java le variabili sono scritte sempre in minuscolo, mentre la maiuscola viene usata al posto dello spazio (ad es. numeroPersone, minimoComuneMultiplo, valoreAssoluto). Al contrario, le costanti sono scritte in maiuscolo, mentre al posto dello spazio si usa il carattere di underscore (ad es. TEMP_MAX, NUM_DI_NEPERO, ALIQUOTA_IVA). I tipi di dato in Java si suddividono in: • • tipi primitivi: byte, short, int, long, float, double, char, boolean. tipi riferimento: sono gli array e le classi , ce ne occuperemo più avanti. I tipi primitivi sono gli stessi del C++ a differenza di boolean e byte; in Java tuttavia non esistono i numeri unsigned (cioè senza segno, sempre positivi). La differenza fondamentale col C++ sta nella forte tipizzazione di Java: le conversioni (cast) tra tipi diversi devono essere quasi sempre esplicitate (fanno eccezione per le promozioni di tipo, cioè conversioni senza perdita di dati, ad es. int float, byte double ecc…): float piGreco = 3.14; int pg = piGreco; //errore di compilazione! Le precedenti istruzioni generano un errore a tempo di compilazione in quanto sto convertendo una variabile float in una variabile int (ho una perdita di dati, perciò non ho una promozione) senza avere esplicitato il cast. La versione corretta dell’assegnamento è quindi: float piGreco = 3.14; int pg = (int) piGreco; //converto piGreco nell’intero pg: pg = 3 2.5 Operatori logici e matematici Anche in questo caso la sintassi e la semantica è molto simile al C++; gli operatori utilizzati da Java sono: = == + += > <= − −= < >= * *= ! != / /= ~ && & &= ?: || | |= ++ ^ ^= −− % %= << <<= >> >>= >>> >>>= Le celle evidenziate sono quelle di maggior interesse per i nostri scopi Vale la pena ricordare che gli operatori non evidenziati in tabella servono per operazioni bit-a-bit (bit-wise) tra numeri binari. Ad esempio, << e >> in Java non servono per l’input/output ma per shiftare bit a sinistra e a destra; ~ calcola il complemento ad uno, ^ l’or esclusivo (XOR). 2.6 La gestione dell’input/output Abbiamo già visto nel programma precedente un esempio di scrittura in output: System.out.println(“Hai inserito la stringa: “ + stringaLetta); System.out è un oggetto associato allo standard output (video); viene quindi richiamato il metodo println di tale classe per poter stampare il parametro contenuto tra parentesi (due stringhe di testo concatenate in questo caso)e poi andare a capo. Tale parametro può essere tuttavia anche un numero intero o reale, un carattere o un booleano. L’oggetto System.out possiede inoltre altri metodi; ad esempio il metodo print(x) stampa il valore di x senza poi andare a capo. Analogamente a System.out esistono gli oggetti System.err (associato allo standard error, il video) e System.in (associato allo standard input, tastiera). Tuttavia l’utilizzo diretto di quest’ultimo non è conveniente; si preferisce quindi combinarlo con oggetti più potenti quali InputStreamReader e BufferedReader nel seguente modo: InputStreamReader input = new InputStreamReader(System.in); BufferedReader tastiera = new BufferedReader(input); Con queste istruzioni definisco prima un oggetto input di classe inizializzato con System.in; quindi creo un altro oggetto tastiera di classe BufferedReader inizializzato con input. Questo mi serve per sfruttare al meglio le potenzialità della classe BufferedReader ; ad esempio il metodo readLine() di tale classe permette di leggere da standard input una riga per volta. Attenzione! Il metodo readLine() accetta solo stringhe: se si vogliono acquisire altri tipi di dato, occorre fare il casting. Inoltre, tale metodo deve poter gestire eventuali eccezioni. Un eccezione è una situazione anomala che si verifica durante l’esecuzione del programma, come ad esempio una divisione per zero. La gestione delle eccezioni verrà ripreso più avanti; per ora ci basti sapere che la lettura di un dato da tastiera deve avvenite all’interno di un apposito blocco try-catch. Vediamo un esempio: InputStreamReader input = new InputStreamReader(System.in); BufferedReader tastiera = new BufferedReader(input); String nome; System.out.println(“Come ti chiami?”); try { nome = tastiera.readLine(); } catch(Exception e) {} In questo caso il programma chiede semplicemente all’utente il proprio nome da standard input e lo legge nella variabile tastiera. L’istruzione di lettura readLine deve sempre essere compresa in un blocco try seguito da un blocco catch (anche vuoto, come in questo caso). Esempio: Scrivere un programma Java che chieda all’utente il proprio anno di nascita e, successivamente, gli comunichi se è nato in un anno bisestile oppure no. /* Questo programma chiede all’utente il proprio anno di nascita e, successivamente, gli comunica se è nato in un anno bisestile oppure no. */ class esBisestile { public static void main(String args[]) { InputStreamReader input = new InputStreamReader(System.in); BufferedReader tastiera = new BufferedReader(input); String annoStr; int annoInt; System.out.println(“In quale anno sei nato?”); try { annoStr = tastiera.readLine(); annoInt = Integer.valueOf(annoStr).intValue(); //cast } catch(Exception e) { System.out.println(“Si è verificata un’eccezione!”); } if(annoInt % 4 == 0) System.out.println(“Sei nato in un anno bisestile!”); else System.out.println(“Non sei nato in un anno bisestile!”); } // fine main } // fine classe esBisestile Questo programma legge l’anno inserito da utente nella variabile annoStr. Tuttavia, come detto, l’input da tastiera è sempre una sequenza di caratteri, per cui è necessario convertire annoStr in un intero per poi poter valutare se si tratta di un anno bisestile oppure no. Ciò è effettuato con l’istruzione: annoInt = Integer.valueOf(annoStr).intValue(); Questa espressione converte il valore di annoStr in un intero che poi assegna alla variabile annoInt . Si tratta di due metodi della classe Integer: valueOf e intValue. Al momento non ci interessa sapere nel dettaglio come viene effettuata la conversione; si ricordi solamente che ciò permette il casting. Ad esempio le istruzioni: float annoF = Float.valueOf(annoStr).floatValue(); double annoD = Double.valueOf(annoStr).doubleValue(); avrebbero permesso di convertire annoStr in variabili di tipo float e double rispettivamente. Infine, si può notare come nella gestione delle eccezioni il blocco catch non sia vuoto: ciò significa che, nel caso in cui si verifichi un eccezione, viene stampato a video il messaggio “Si è verificata un eccezione!”. Ad esempio, in questo caso vi è eccezione se il valore inserito da utente non è un numero intero (ad es. 1984.567) oppure se inserisco una stringa di testo (ad es. MCMLXXXIV) 2.7 Le strutture di controllo Le strutture di controllo in Java sono pressoché identiche al C++. Nell’esempio precedente abbiamo visto l’utilizzo della struttura di selezione singola (if-else); allo stesso modo del C++ è possibile utilizzare la selezione multipla attraverso il costrutto switch-case. Per valutare la condizione in una struttura di selezione si utilizzano gli operatori logici (!, ||, &&) e matematici (<, <=, >, >=, ==, !=, %) esattamente come in C++. Tra le funzioni matematiche di uso comune fa invece eccezione la generazione di numeri (pseudo)casuali: anziché utilizzare srand e rand, uso il metodo random() della classe Math: double x = Math.random(); double y = Math.random() * 10; int n = (int) ( Math.random() * 5 ); int m = (int) ( Math.random() * 4 + 6 ); Math.random() restituisce un numero casuale di tipo double tra 0.0 e 1.0 (ad es. 0.6633709….). Nell’esempio precedente quindi x sarà un numero reale tra 0 e 1 mentre y sarà un numero reale compreso tra 0 e 10 (moltiplico il numero estratto per 10). Ovviamente è anche possibile arrotondare (o meglio troncare in questo caso) il numero estratto per ottenere numeri interi; nell’esempio precedente n sarà un intero compreso tra 0 e 5 mentre m sarà compreso tra 6 e 10. Le strutture di ripetizione (cicli) non si differenziano dal C++, in particolare hanno la stessa sintassi e semantica i cicli do-while, while e for. Inoltre, come per il C++, è possibile modificare l’ordine di esecuzione dei cicli attraverso particolari istruzioni: break e continue. L’istruzione break forza l’uscita da un ciclo: se viene eseguita, l’esecuzione riprende dalla prima istruzione presente dopo il ciclo (cfr. struttura switch-case). L’istruzione continue invece forza il ritorno all’inizio del ciclo: se viene eseguita, l’esecuzione riprende dalla prima istruzione del ciclo (nel caso di for e while prima però valuto la condizione) Nel malaugurato caso che si venga a creare un loop infinito, in Java è possibile bloccare l’esecuzione del programma premendo simultaneamente i tasti Ctrl + C. Esempio: Modo altamente masochista per visualizzare il valore di π approssimato a 5 cifre decimali. int k = 0; double d, pi = 1; while( true ) { d = (double) 2*k + 1; if( k % 2 == 0 ) pi += 1/d; else pi -= 1/d; k++; if( k == 1000000 ) break; } System.out.println( 4 * pi ); //System.out.prinln(Math.PI)... Esercizi Implementare in codice Java i seguenti esercizi: 1. 2. 3. 4. Scrivere un programma che stampi a video l’area di un triangolo data la base e l’altezza. Inserita da utente una data (a partire dal 01/01/1900), verificare se è corretta o no. Visualizzare sullo schermo 10 numeri casuali compresi tra due interi a e b letti da tastiera. Inserire una sequenza di numeri terminata da 0. Al termine dell’inserimento visualizzare il massimo, il minimo e la media dei numeri inseriti 0 escluso. 5. Inserire da utente il nome, il cognome e l’età di 5 ragazzi. Al termine dell’inserimento stampare nome e cognome del ragazzo più giovane e il numero di ragazzi il cui cognome comincia con una vocale. 2.8 Gli array Un array è una collezione finita di elementi omogenei (stesso tipo). Gli array monodimensionali spesso prendono il nome di vettori, mentre quelli bidimensionali prendono il nome di matrici. Un array è realizzato mediante un puntatore che punta all'area di memoria dove si trovano i suoi elementi; ciò significa che se dichiaro un array di nome V, la variabile V non conterrà il valore dei suoi elementi ma l'indirizzo di memoria del suo primo elemento. Tuttavia, come già sottolineato, Java non permette l'utilizzo esplicito di puntatori (gestione automatica della memoria) che quindi vengono mascherati (hiding) dal linguaggio. Nonostante ciò è importante notare che un array non è un tipo primitivo, bensì un tipo reference: ciò significa che una variabile di tipo array non contiene un valore ma un riferimento ai valori contenuti nell'array (indirizzo di memoria del suo primo elemento). Per poter creare ed utilizzare un array si devono seguire tre passaggi fondamentali: • • • DICHIARAZIONE ALLOCAZIONE INIZIALIZZAZIONE La dichiarazione consiste nella specificazione del nome dell'array e del tipo di dati che esso contiene. Un array può contenere sia tipi primitivi (ad es. int, double, ecc...) che tipi riferimento (classi oppure array stessi). La sintassi è la seguente: tipoElementi nomeArray[]; Ad es. int voti[]; //oppure int[] voti; String nomi[]; //oppure String[] nomi; Come si può notare, l'operatore [ ] può essere inserito equivalentemente sia dopo il nome che dopo il tipo. Si noti inoltre che al momento della dichiarazione non viene specificata la dimensione dell'array. L'istruzione di dichiarazione infatti crea una variabile di tipo array ma ad esso non associa nessun valore; la variabile array conterrà quindi il valore speciale null: ciò significa che la variabile di tipo riferimento che non si riferisce proprio a nulla. L'allocazione di un array consente di specificare la dimensione dell'array, cioè di allocare la quantità di memoria necessaria per contenere gli elementi dell'array specificato. La sintassi è la seguente: nomeArray = new tipoElementi[dimensione]; Ad es. voti = new int[10]; nomi = new String[15]; Come si può notare l'allocazione viene eseguita attraverso l'operatore new. Una eseguita l'allocazione l'array non conterrà più il valore null ma l'indirizzo di memoria del primo elemento dell'array, mentre i successivi saranno memorizzati nelle celle adiacenti. Spesso comunque le operazioni di dichiarazione ed allocazione vengono effettuate contemporaneamente nella stessa istruzione: tipoElementi nomeArray[] = new tipoElementi[dimensione] Ad es. int voti[] = new int[10]; String nomi[] = new String[15]; L'inizializzazione permette di inserire valori iniziali nell'array; è possibile assegnare immediatamente valori all'array al momento della dichiarazione nel seuente modo: tipoElementi nomeArray[ ] = {valore1, valore2, ..., valoreN} Ad es. int voti[] = {4,5,6,5,7,6}; String nomi[] = {“Tizio”, “Caio”, “Sempronio”}; In questo modo, dichiarazione, allocazione e inizializzazione vengono fatte contemporaneamente. Tuttavia spesso l'inizializzazione viene eseguita in un momento successivo alla dichiarazione; in questo caso è necessario potersi riferire ai singoli elementi dell'array attraverso l'utilizzo di identificatori univoci: gli indici. Il primo elemento dell'array ha indice 0 Il secondo elemento dell'array ha indice 1 ... L'elemento n-esimo dell'array ha indice n – 1 Un errore comune è l'utilizzo di indici che escono dai limiti dell'array: se l'array ha dimensione n, posso usare solamente indici compresi tra 0 ed n – 1. Se invece utilizzo un indice i < 0 oppure i > n – 1 verrà generata l'apposita eccezione ArrayIndexOutOfBoundsException. Per accedere in lettura o in scrittura all'elemento di indice i si utilizza l'operatore []: /* leggo l'elemento di indice i e lo assegno ad elem */ tipoElementi elem = nomeArray[i]; /* assegno all'elemento di indice j il valore di elem */ nomeArray[j] = elem; Ad es. int voti[] = new int[10]; voti[0] = 4; voti[1] = 5; int x = voti[0]; x += voti[1]; String nomi[] = new String[15]; nomi[0] = “Tizio”; nomi[1] = “Caio”; nomi[2] = “Sempronio”; String s = nomi[0] + nomi[2]; s += nomi[1]; Un importante proprietà dell'array è l'attributo length che ne ritorna la dimensione; ad es. double temperature = new double[30]; int dim = temperature.length; //n assumerà il valore 30 Questa proprietà risulta molto utile quando si utilizzano cicli per iterare su tutti gli elementi dell'array; si veda ad esempio il seguente ciclo for: for(int i = 0; i < voti.lentgh; i++) { voti[i] = (int) ( Math.random()*9 + 1 ); System.out.println( voti[i] ); } Il ciclo scorre tutti gli elementi del vettore, assegnando ad ogni elemento un numero casuale intero tra 1 e 10 che successivamente stampa a video. Esempio: /* Programma che stampa a video tutti i parametri inseriti dall'utente da riga di comando */ class parametri { public static void main(String args[]) { if(args.length == 0) System.out.println(“Nessuno parametro inserito”); else { System.out.println(“Hai inserito i seguenti parametri: ”); for(int i=0; i < args.lenth; i++) System.out.println(args[i]); } //fine else } //fine main } //fine classe Questo programma estrae dal vettore args tutti gli argomenti passati da riga di comando (se ce ne sono) e li stampa a video. Dopo aver compilato (javac parametri.java) posso quindi eseguire il file da riga di comando; ad esempio il comando: java parametri Ciao Mondo!!! STOP produrrà il seguente output: Hai inserito i seguenti parametri: Ciao Mondo!!! STOP Analogamente al C++, è inoltre possibile anche definire array bidimensionali: le cosiddette matrici. Dichiarazione: tipoElementi nomeMatrice[][]; Allocazione: nomeMatrice = new tipoElementi[numRighe][numColonne]; Dichiarazione + Allocazione: tipoElementi nomeMatrice = new tipoElementi[numRighe][numColonne]; Accesso: /* leggo l'elemento di indice di riga i e indice di colonna j e lo assegno ad elem */ tipoElementi elem = nomeMatrice[i][j]; /* assegno all'elemento di indice di riga i e indice di colonna j il valore di elem */ nomeMatrice[i][j] = elem; Esempio: /* Programma che carica una matrice 4x5 con valori interi casuali tra 0 e 5 e la stampa a video. */ class matrice { public static void main(String args[]) { int mat[] = new int[4][5]; for(int i = 0; i < 4; i++) { for(int j = 0; j < 5; j++) { mat[i][j] = Math.random() * 5; System.out.print(mat[i][j]+’ ‘); } //fine for colonne System.out.println(); } //fine for righe } //fine main } //fine classe //vado a capo di una riga Esercizi Implementare in codice Java i seguenti esercizi. Per ogni esercizio si raccomanda di stampare a video i vettori utilizzati nel formato [elem_1, elem_2, …, elem_n] 1. Chiedere un numero n ≥ 0 da tastiera ed inserire in un vettore i primi n quadrati (1, 2, 4, 9, 16, 25,…,n2 ). Quindi, calcolare e visualizzare la somma, il prodotto e la media degli elementi del vettore. 2. Caricare in modo casuale due vettori di double x e y di uguale dimensione n (dove n è chiesto da utente). Quindi calcolare il prodotto scalare tra x ed y, cioè la somma dei prodotti di tutti gli elementi di indice uguale ( es. x = [1, 0, 2] e y = [2.3, 3.7, 3] 1*2.3 + 0*3.7 + 2*3 = 8.3 ). 3. Caricare in un vettore di 10 elementi una serie di stringhe inserite da utente; l’inserimento termina quando inserisco la stringa ‘STOP’ (che non va inserita nel vettore) oppure il vettore è pieno (cioè, ho già inserito 10 stringhe…). Al termine dell’inserimento, stampare a video (uno sotto all’altra) solo le stringhe del vettore che hanno lunghezza maggiore o uguale a 3. 4. Caricare in un primo vettore di 5 elementi numeri casuali interi tra 1 e 10, corrispondenti alla combinazione vincente di un gratta e vinci. Quindi leggere da tastiera 5 numeri tra 1 e 10 e inserirli in un secondo vettore, corrispondente alla vostra giocata. Infine, controllate e stampate a video quante sono le corrispondenze (cioè, stesso numero nella stessa posizione) tra la combinazione vincente e la vostra giocata. 5. Caricare un vettore vett con n numeri casuali tra 0 e 10 (n è un intero chiesto da tastiera). Visualizzare quindi: • • • 2.9 l’elemento più piccolo contenuto in vett. l’elemento più grande contenuto in vett. l’indice del primo elemento pari di vett (se non ci sono numeri pari devo visualizzare -1). Le eccezioni Come già accennato, un eccezione è un anomalia che si presenta durante l’esecuzione del programma. Esempi classici di situazione anomala sono la divisione per zero oppure l’utilizzo di un indice che esce dai limiti di un array. Come visto per la lettura di input da utente, la gestione delle eccezioni è possibile mediante l’utilizzo del costrutto try-catch: try { //istruzioni da controllare (potenzialmente “pericolose”) } catch(Exception e) { //operazioni da eseguire se si verifica un eccezione } Quando si verifica un eccezione, l’esecuzione del programma si interrompe per cercare di gestirla. Se tale eccezione si verifica all’interno di un blocco try , allora l’esecuzione riprende a partire dalla prima istruzione di tale blocco catch. Viceversa, il programma termina segnalando un errore. Esempio: /* Questo programma genera un indice intero casuale tra 0 e 10. Successivamente carica un vettore di double di 5 elementi con numeri casuali tra 0 ed 1. Quindi prova ad accedere all’elemento di indice i del vettore: se è possibile lo stampa, altrimenti gestisce l’eccezione stampando a video un messaggio di errore. */ class eccezione { public static void main (String args[]) { int i = (int)(Math.random()*10); double vett[] = new double[5]; for(int i = 0; i < 5; i++) vett[i] = Math.random(); try { System.out.println("indice: " + i + " elemento: " + vett[i]); } //fine try catch(Exception e) { System.out.println("Si è verificata un eccezione! Impossibile accedere all'elemento di indice " + i ); } //fine catch System.out.println("Fine del programma. Bye!" ); } //fine main } //fine classe E’ importante notare che: • • Se 0 ≤ i ≤ 4, il programma non esegue il blocco catch (non c’è eccezione) e viene stampato il valore di vett[i] Se invece i > 4, il programma esegue il blocco catch (ho eccezione: esco dai limiti dell’array) e stampo un messaggio di errore. In entrambi i casi comunque il programma termina normalmente; viceversa se non avessi avuto un blocco try-catch in caso di eccezione il programma sarebbe terminato immediatamente segnalando un errore.