Java/Array
Nel linguaggio di programmazione ad oggetti Java, gli array sono strutture dati costituite dall'aggregazione di un prefissato numero di variabili, individualmente accessibili tramite un indice numerico.
Un array può essere immaginato come un casellario costituito da un certo numero di celle. Le celle si comportano individualmente come variabili tradizionali (ovvero possono essere assegnate, lette, e così via). Tutte le celle di un array sono variabili dello stesso tipo, detto tipo degli elementi dell'array (element type in inglese). In Java, gli array sono oggetti.
Caratteristiche
modificaLe caratteristiche degli array sono definite dalla specifica di linguaggio.
Un array è un oggetto dotato di un numero fisso di celle (in inglese, slot), il tipo delle quali è chiamato component type dell'array. Esse si comportano esattamente come se fossero dei campi non final
, con le seguenti differenze:
- invece che da un nome alfanumerico, ogni cella è identificata da un numero intero positivo, chiamato "indice";
- invece del punto, per indicare una cella si usa una notazione diversa.
Il numero esatto di celle viene specificato nella creazione dell'array, e non può essere cambiato.
Il tipo dell'array è sempre un tipo riferimento, e si indica apponendo una coppia di parentesi quadre al component type. Esempi:
int[]
indica il tipoarray di int
Object[]
indica il tipoarray di Object
int[][]
indica il tipo array il cui component type èint[]
,[1] quindi, in definitiva, indica il tipoarray di array di int
Si noti che il tipo di un array non include mai il numero delle celle. Per conoscere questo numero, si usa il campo campo length
, il quale si comporta come un campo public final int
.
- Celle
L'indice della prima cella è sempre 0
[2] e, pertanto, l'indice dell'ultima cella è sempre n - 1
, dove n
è il numero di celle totali.
Ad esempio:
System.out.println(i[0]);
stampa a video il valore della prima cella dell'array referenziato dalla variabilei
.i[0] = 5;
assegna il valore intero5
alla prima cella dell'array.
Più precisamente, l'accesso a una cella, in lettura o in scrittura, si identifica apponendo una coppia di parentesi quadre ad un'espressione il cui compile-time type sia un tipo array, e inserendo fra queste parentesi un'espressione il cui compile-time type sia convertibile verso il tipo int
tramite cast implicito.
- Clonazione
Gli array esibiscono un metodo pubblico clone()
.
Creazione e distruzione
modificaGli array sono creati tramite una sintassi specifica.
- Inizializzando le celle con il valore di default
È possibile allocare un array in memoria servendosi della parola-chiave new
:
int[] i = new int[10];
Il numero di elementi tra parentesi quadre è obbligatorio.
Tutte le celle sono automaticamente inizializzate con il valore di default del component type. Nell'esempio appena mostrato, tutti gli slot dell'array referenziato dalla variabile i
valgono inizialmente 0
.
Ad esempio, la scrittura new byte[1024]
crea un array di 1024 celle, inizializzate al valore 0; new Object[1024]
crea un array di 1024 celle, inizializzate con il valore null.
- Elencando gli elementi
È disponibile una sintassi alternativa che permette di indicare direttamente i valori delle singole celle:
int[] numeri = new int[] { -1, 0, 0 };
La lunghezza dell'array viene calcolata in automatico in base al contenuto delle parentesi graffe; quindi l'esempio precedente equivale a:
int[] numeri = new int[3];
numeri[0] = -1;
numeri[1] = 0;
numeri[2] = 0;
Un'altra forma equivalente e più concisa è
int[] numeri = { -1, 0, 0 };
dove new int[]
viene inserito in automatico dal compilatore. Si tratta di un caso speciale, che funziona solo se l'array viene creato insieme alla dichiarazione della variabile, quindi il codice seguente non compila:
int[] numeri;
numeri = { -1, 0, 0 };
perché la dichiarazione e l'inizializzazione della variabile numeri
ora sono due istruzioni diverse.
- Deallocazione
Il programmatore non deve occuparsi di deallocare gli array, in quanto il Java è dotato di garbage collector.
Tipi array
modificaGli array come oggetti
modificaIn Java, gli array sono oggetti[3][4].
La prima implicazione di questo fatto è che gli array si considerano dotati di tutti i metodi e gli attributi della classe Object
. Per esempio, è possibile richiamare i metodi wait()
o notify()
su un array:
unArray.wait();
La seconda implicazione è che è possibile eseguire un'operazione di casting da un tipo array ad Object
dovunque sia necessario: assegnare ad una variabile di tipo Object un reference ad un array, passare un array come argomento a un metodo o costruttore che accetta argomenti di tipo Object
, ecc.
In aggiunta, gli array possono essere convertiti ai tipi Serializable
e Cloneable
[5].
Così, per esempio, si può serializzare un array allo scopo di salvarlo su file nello stesso modo in cui si salva su file un qualsiasi oggetto serializzabile:
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("unfile"));
oos.writeObject(unArray);
Gli array sono anche dotati di un attributo specifico, denominato length
, che indica il numero di celle che esso contiene. La presenza di questo attributo permette di passare un array tra diverse parti del programma senza dover indicare anche la sua dimensione (cosa che in altri linguaggi, come il C, è invece obbligatoria). Una variante più elegante del ciclo for
riportato sopra potrebbe dunque essere:
for(int i=2; i<unArray.length; i++)
unArray[i] = unArray[i-1]+unArray[i-2];
Di conseguenza, eventuali modifiche alla dimensione dell'array (specificata nella riga che contiene l'operatore new
) non costringeranno il programmatore a modificare ogni altro punto del programma in cui si fa uso dell'array.
Gerarchia delle classi
modificaÈ sempre possibile convertire un array verso il tipo Object
tramite cast implicito, in quanto tutti gli array sono oggetti.
In aggiunta, tutti gli array possono essere convertiti verso i tipi java.lang.Cloneable
e java.io.Serializable
.
In particolare,
- gli array supportano il metodo
clone()
; - è possibile serializzare gli array come qualunque altro oggetto:
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("un_file.ser"));
oos.writeObject(unArray);
Gerarchia fra tipi array
modificaIl tipo di un array creato con new T[...]
è T[]
. Se S
è un supertipo di T
, allora ogni array di tipo T[]
può essere convertito verso il tipo S[]
tramite cast implicito; la conversione inversa richiede il cast esplicito.
Questo significa che tutti gli array di oggetti possono essere convertiti verso Object[]
.
Utilizzo degli array
modificaPer accedere a una cella di un array, come in altri linguaggi, si utilizza una sintassi che consente di specificare l'indice (ovvero la posizione nell'array) di tale cella. Gli indici sono numeri interi consecutivi a partire da 0 (indice della prima cella). La seguente istruzione assegna il valore intero 1 alle prime due celle dell'array:
unArray[0] = 1;
unArray[1] = 1;
L'indice può essere specificato da un valore costante (come i valori 0 e 1 nell'esempio) oppure, più in generale, da una espressione di valore intero. Spesso tale espressione è ricavata a partire dal valore di una variabile contatore di un ciclo for
. L'esempio che segue prosegue la valorizzazione dell'array unArray
riempiendolo con i primi numeri della sequenza di Fibonacci (il primo e il secondo valore sono già stati assegnati sopra):
for(int i=2; i<45; i++)
unArray[i] = unArray[i-1]+unArray[i-2];
Tradotto in italiano, il frammento di codice riportato suonerebbe così: per tutti i valori crescenti di i da 2 a 44, assegna all'i-esima cella di unArray
la somma dei contenuti delle due celle precedenti.
Errori di indicizzazione
modificaUn tipico errore di programmazione consiste nell'uso di indici scorretti, quindi inferiori a 0 o superiori all'indice massimo (quindi superiori a unArray.length-1
). Nei linguaggi compilati come il C, questo può portare a errori di accesso alla memoria. In Java, un errore di indicizzazione viene sempre rilevato a run-time dalla macchina virtuale e segnalato tramite una eccezione.
Per esempio, il seguente frammento di codice contiene un errore comune: l'uso dell'operatore di confronto minore o uguale, invece del minore, per stabilire l'ultimo valore assunto dal contatore del ciclo.
for(int i=2; i<=45; i++)
unArray[i] = unArray[i-1]+unArray[i-2];
Questo ciclo esegue l'ultima iterazione per i
uguale a 45. Poiché gli indici degli array Java partono da 0, l'ultima cella (cioè la centesima) ha indice unArray.length-1=44
; il valore 45 è quindi scorretto e causerà una eccezione. L'eccezione sollevata è di classe ArrayIndexOutOfBoundsException
(indice dell'array fuori dagli estremi).
Secondo il convenzionale meccanismo di gestione delle eccezioni in Java, a meno che questa eccezione non sia catturata da una clausola try-catch
, essa causerà la terminazione improvvisa del metodo corrente e dei metodi che lo hanno invocato, e così via, fino a terminare il thread sul quale l'istruzione è stata eseguita. Questo comportamento è decisamente preferibile a quello del C, in cui l'errore di cui sopra non viene rilevato e comporta la scrittura di un valore in una cella di memoria esterna all'array (cella che potrebbe appartenere alla memoria allocata per un'altra variabile, che quindi muterà valore in modo imprevisto, causando un potenziale malfunzionamento molto difficile da scoprire e da correggere).
ArrayIndexOutOfBoundsException
modificaNon è permesso leggere o scrivere al di fuori della memoria che è stata allocata per un array. In altre parole, non è possibile accedere ad una cella che ha un indice inferiore all'indice minimo (che è sempre 0) o superiore all'indice massimo (che è sempre pari ad array.length - 1
).
Se si tenta di compiere una operazione del genere, la macchina virtuale lancerà una eccezione a run-time, in particolare una
java.lang.ArrayIndexOutOfBoundsException
.
int[] array = new int[3];
array[-1] = 10; // lancia un'eccezione
Utilizzi tipici
modificaIterazione classica
modifica // Riempie l'array con i numeri progressivi da 0 ad array.length - 1:
for(int i = 0; i < array.length; i++)
array[i] = i;
Questo è il costrutto standard per iterare lungo l'array. Vale la pena di esaminarlo nel dettaglio:
- si vuole iterare lungo tutto l'array, dalla prima all'ultima cella. Come già osservato, la prima cella ha indice 0, mentre l'ultima ha indice
array.length - 1
. - Quindi, l'indice del ciclo deve partire da 0, essere incrementato ogni volta di un'unità, e tutto questo finché non sarà raggiunto il valore massimo, che è
array.length - 1
; - quindi, la condizione da applicare al ciclo è
i <= array.length-1
, oppure, equivalentemente,i < array.length
.
- Un errore comune
Molti programmatori alle prime armi con il Java commettono il seguente errore:
for(int i = 0; i <= array.length; i++)
... // istruzioni
Questo ciclo fa sì che le istruzioni che costituiscono il corpo del for siano eseguite una volta in più: nell'ultima iterazione, i
è pari al valore di array.length
, ovvero un indice fuori dai limiti dell'array (cioè 0
e array.length - 1
). Un simile errore viene scoperto solo a run-time, quando il programma genera una ArrayIndexOutOfBoundsException non prevista.
For each
modificaA volte si può usare un costrutto alternativo:
// Stampo a video i valori contenuti nelle celle dell'array:
for(int slot : array)
System.out.println( slot );
che è una forma del tutto equivalente a
for(int i = 0; i < array.length; i++) {
int slot = array[i];
System.out.println( slot );
}
ovvero scorre l'array, dall'indice 0 all'indice array.length - 1
, assegnando automaticamente il valore array[i]
alla variabile slot
, e ad ogni ciclo esegue l'istruzione System.out.println(slot)
.
È chiamato for each o enhanced for ed è un costrutto introdotto dalla versione 1.5 di Java, che risparmia al programmatore il calcolo a mente degli indici se questi non sono realmente necessari (calcolo che viene svolto in automatico dal compilatore). Lo svantaggio è che non si può usare sempre, ad esempio nei seguenti casi:
- se bisogna accedere alle celle dell'array in scrittura;
- se è necessario accedere a più di una cella alla volta.
Array e collezioni
modificaPer approfondire, vedi Java Collections Framework. |
Le librerie standard della piattaforma Java forniscono un insieme di classi e interfacce che definiscono collezioni, e che si chiama Java Collections Framework.
Conversione
modifica- Da collezione ad array
Invocare sulla collezione uno dei due metodi toArray()
.
Nota: questo metodo restituisce una copia degli elementi della collezione: se quest'ultima viene modificata in seguito, le modifiche non interesseranno anche l'array.
- Da array a collezione
Invocare il metodo
java.util.Arrays.asList()
.
Nota: questo metodo restituisce una List che opera sull'array passato come argomento: se quest'ultimo verrà modificato in seguito, le modifiche potrebbero ripercuotersi sulla List.
Array o Collection?
modificaUn array ha la stessa identica funzionalità di una java.util.List
di dimensioni costanti e che fornisca accesso in lettura e scrittura.
La scelta fra un tipo array e una List (o altra Collection) è arbitraria e lasciata al programmatore.
Se si sta progettando l'implementazione privata di una classe o il corpo di un metodo, non c'è motivo di porsi il problema, perché, in futuro, si potrà tornare indietro sulla decisione con modifiche che resteranno sempre circoscritte.
Negli altri casi, in genere, non vale la pena di scegliere un tipo array al posto di una collezione, se l'unica ragione è ottimizzare il codice che accede alle celle, cioè per usare l'accesso diretto alla memoria come "scorciatoia" che eviti le invocazioni dei metodi get
e set
. Infatti, le macchine virtuali odierne adottano delle tecniche di ottimizzazione (come l'inlining dei metodi di dimensioni ridotte) che rendono superfluo un accorgimento come questo. Inoltre, la classe java.util.ArrayList
esibisce buone prestazioni per la maggior parte delle applicazioni in cui sia necessario disporre di una lista ad accesso casuale, e la classe java.util.LinkedList
è sicuramente più facile da usare di un array nel caso sia necessario uno stack o una coda.
Nella scelta si deve tenere conto anche del fatto che lo strato di astrazione aggiuntivo permette ai client di fornire una implementazione personalizzata, e ciò rende la struttura a oggetti più resistente nei confronti delle eventuali modifiche future che potrà essere necessario implementare nel programma.
In certi casi, una collezione personalizzata permette di risparmiare memoria, in quanto non tutto il contenuto deve risiedere in memoria. Si pensi ad una lunga sequenza di elementi uguali tra loro: in alternativa, si può conservare una sola copia del valore, e simulare una collezione di grandi dimensioni manipolando gli indici.
[A 1]
Un'altra implementazione può accedere direttamente alle proprietà di un oggetto o ai campi di un database, mascherandolo dietro l'interfaccia della collezione, invece di utilizzare la memoria come passaggio intermedio (e obbligatorio) per la memorizzazione dei dati.
Inoltre, per manipolare il contenuto di un array, riordinandolo o trasformando i singoli elementi, è necessario portare il programma ad eseguire costose iterazioni, ed eventualmente duplicarne il contenuto in memoria finché la trasformazione non è completa. Una collezione che sia stata implementata come wrapper, al contrario, può svolgere queste operazioni "al volo" e solo sugli elementi per i quali è effettivamente richiesta a tempo di esecuzione, lasciando inalterata la collezione iniziale.
Array di oggetti
modificaGli esempi di codice riportati fin qui creano e manipolano un array del tipo primitivo int. È possibile creare array di qualsiasi altro tipo, inclusi ovviamente i tipi riferimento (quindi le classi, le interfacce e perfino gli array). Il seguente frammento di codice dichiara un reference a un array con tipo base String e crea l'array con l'operatore new:
String altroArray[] = new String[100];
Il funzionamento dell'array è identico rispetto a quello di unArray
. Si deve comunque ricordare che un array può essere equiparato a un casellario costituito da N celle del tipo base. Ne consegue che le celle di altroArray
si comportano come variabili di tipo String
. Di conseguenza, come avviene per una variabile che ha tipo riferimento, tecnicamente le celle contengono solo reference a oggetti e non gli oggetti stessi.
Un esempio di codice che riempie le celle dell'array:
for(int i = 0; i < altroArray.length; i++)
altroArray[i] = "Stringa numero " + i;
Matrici e array multidimensionali
modificaNegli array multidimensionali, una cella non è identificata da un singolo valore di indice ma da una N-upla di indici. Per visualizzare il concetto, si pensi al caso di un array bidimensionale (detto anche matrice), che può essere immaginato come una tabella costituita da righe e colonne. In tal caso le celle saranno identificate da due indici (numero di riga e numero di colonna).
In Java, come nella tradizione C, gli array multidimensionali non sono altro che array di array. La sintassi e la semantica seguono da questa equivalenza (sebbene questa visione non sia necessariamente la più semplice da afferrare per un neofita).
Per dichiarare un array a più dimensioni (per esempio 2), la sintassi è come segue:
int[][] tabelline;
La sintassi, in effetti, dichiara tabelline
come un array i cui elementi sono array di tipo int[]
.
Si può allocare la matrice con una istruzione new della seguente forma:
tabelline = new int[10][10];
Questa istruzione crea una "matrice rettangolare" NxM (nella fattispecie, quadrata, 10x10). Così come gli array sono spesso manipolati tramite un "ciclo for", per le matrici N-dimensionali è frequente l'accesso tramite N cicli for "annidati". Due cicli annidati si comportano come le lancette dell'orologio: a ogni "step" (o iterazione) del ciclo più esterno (lancetta delle ore) il ciclo più interno esegue interamente il proprio percorso (lancetta dei minuti). Il seguente frammento di codice valorizza l'array "tabelline" inserendoci la tavola pitagorica:
for(int i=0; i<10; i++)
for(int j=0; j<10; j++)
tabelline[i][j] = (i+1)*(j+1);
La creazione di una matrice di interi come tabelline
può essere anche spezzata in due passaggi: prima si crea un array di array di interi, dopodiché si assegna ad ogni cella di detto array il reference a un array di interi:
tabelline = new int[10][];
tabelline[0] = new int[10];
tabelline[1] = new int[10];
...
Questo secondo approccio si può "leggere" in questi termini: la prima istruzione stabilisce quante saranno le righe della matrice; le successive specificano il numero di colonne di ogni riga. Quest'ultimo approccio è più flessibile del precedente, perché permette di avere righe di diversa lunghezza. Una "matrice triangolare" potrebbe per esempio essere creata come segue:
tabelline = new int[10][];
for(int i=0; i<tabelline.length; i++)
tabelline[i] = new int[i+1];
Differenze con il C++
modificaIl C++ e il Java adottano due approcci completamente differenti su questo tema.
In C++, una variabile array è un puntatore costante, il quale, a run-time, punta ad un'area di memoria dei dati del programma, configurata dal compilatore. Questa zona di memoria può essere a livello di programma (nel caso di una variabile globale o statica), oppure si trova nel call stack (nel caso di una variabile locale).
In Java, i tipi array sono tipi riferimento, quindi una variabile di tipo array referenzia un oggetto che viene allocato sempre e solo sullo heap.
L'allocazione statica e quella automatica richiedono che, a tempo di compilazione, sia nota la quantità massima di memoria che l'array occuperà a run-time. Il compilatore del C++ ha bisogno di conoscere non solo il tipo, ma anche il numero delle celle, che infatti va indicato obbligatoriamente nell'istruzione che dichiara l'array. Quest'ultimo può essere utilizzato subito dopo l'istruzione che lo dichiara, mentre in Java bisogna allocarlo esplicitamente.
Oltre agli array, anche i puntatori del C++ supportano l'operatore "parentesi quadre", e l'allocazione dinamica di un array tramite new
restituisce appunto un puntatore. Tuttavia, esso andrà memorizzato in una variabile di tipo puntatore, e non in una variabile array, perché quest'ultima è un puntatore costante. In effetti, in C++, la sintassi degli array "maschera" operazioni di accesso e manipolazione di puntatori (aritmetica, dereferenziazione).
Poiché il C++ non è dotato di garbage-collector, un array allocato dinamicamente va deallocato dopo essere stato utilizzato (l'istruzione che permette di fare questo è delete[]
, ed eventualmente può essere necessario iterare lungo l'array per deallocarne a loro volta i singoli elementi). Il Java ha garbage-collector, quindi gli array non vanno deallocati esplicitamente.
Per massimizzare le prestazioni del codice eseguibile, in C++ è consentito leggere o scrivere in una cella il cui indice è fuori dai limiti dell'array. Di fatto, questa operazione consiste nell'accesso ad una locazione di memoria non occupata da quell'array, che pertanto potrebbe non essere allocata affatto, oppure allocata dal programma o dal sistema per altri usi (variabili, istruzioni del programma, ecc.). L'ambiente di esecuzione non è tenuto a segnalare questa situazione, che è evidentemente non voluta dal programmatore, e i cui risultati non possono essere previsti a priori. In Java, la VM lancia una eccezione se si tenta di accedere ad una cella che è fuori dai limiti dell'array.
Infine, le regole del Java impongono che le celle risultino sempre inizializzate prima che sia possibile accedervi. In C++ non è così, in quanto le celle assumono inizialmente valori casuali o imprevedibili.
Bibliografia
modifica- Capitolo 10 della specifica di linguaggio, con particolare riferimento alla sezione 10.3, Array Creation, e alla sezione 10.7, Array Members.
- Capitolo 15 della specifica di linguaggio, sezione 15.13 (Array Access Expressions)
Note
modifica- Note esplicative
- ↑ In altre parole: indica il tipo di un array le cui celle referenziano degli array di int.
- ↑ In gergo, con terminologia inglese, si dice che gli indici sono 0-based. Esistono linguaggi che hanno array 1-based o che permettono di scegliere indici arbitrari.
- ↑ The Java Language Specification, Third Edition, Arrays, su java.sun.com. URL consultato il 21 aprile 2011.
- ↑ The Java Language Specification, Third Edition, Types, Values, and Variables, su java.sun.com. URL consultato il 6 maggio 2011.
- ↑ The Java Language Specification, Second Edition - Arrays, su java.sun.com. URL consultato il 23 marzo 2011.
- Approfondimenti