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.

Java

Linguaggio Java
Linguaggio Java

categoria · sviluppo · modifica
Come posso contribuire?

→ Vai su Wikiversity

Java:  Guida alla programmazioneJava/Introduzione - Specifica di linguaggio - Libreria standardJava/Comunicazione seriale

Tipi di datoJava/Tipi di datoEspressioniJava/OperatoriVariabiliJava/Variabili localiIstruzioniJava/IstruzioniStrutture di controlloJava/Strutture di controlloPackageJava/Package
Classi e oggettiJava/Classi e oggettiOggettiJava/OggettiMetodiJava/MetodiMetodi (cont.)Java/Metodi/2ArrayJava/Array
Modificatori di accessoJava/Modificatori di accessoEreditarietàJava/Ereditarietà
Gestione delle eccezioniJava/Gestione delle eccezioniGenericsJava/Generics
Thread e parallelismoJava/MultithreadingInterblocco ricontrollatoJava/Interblocco ricontrollato
Alcune differenze con il CJava/Concetti fondamentaliLessicoJava/LessicoGrammaticaJava/Grammatica

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 modifica

Le 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 tipo array di int
  • Object[] indica il tipo array di Object
  • int[][] indica il tipo array il cui component type è int[],[1] quindi, in definitiva, indica il tipo array 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 variabile i.
i[0] = 5; assegna il valore intero 5 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()(→ javadoc).

Creazione e distruzione modifica

Gli 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 modifica

Gli array come oggetti modifica

In 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(→ javadoc) e java.io.Serializable(→ javadoc). 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 modifica

Il 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 modifica

Per 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 modifica

Un 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 modifica

Non è 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(→ javadoc).

    int[] array = new int[3];
    array[-1] = 10; // lancia un'eccezione

Utilizzi tipici modifica

Iterazione 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 modifica

A 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 modifica

  Per 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()(→ javadoc).
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()(→ javadoc).
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? modifica

Un array ha la stessa identica funzionalità di una java.util.List(→ javadoc) 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(→ javadoc) 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(→ javadoc) è 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 modifica

Gli 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 modifica

Negli 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++ modifica

Il 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

Note modifica

Note esplicative
  1. In altre parole: indica il tipo di un array le cui celle referenziano degli array di int.
  2. 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.
  3. The Java Language Specification, Third Edition, Arrays, su java.sun.com. URL consultato il 21 aprile 2011.
  4. The Java Language Specification, Third Edition, Types, Values, and Variables, su java.sun.com. URL consultato il 6 maggio 2011.
  5. The Java Language Specification, Second Edition - Arrays, su java.sun.com. URL consultato il 23 marzo 2011.
Approfondimenti
  1. Vedi anche: documentazione del metodo java.util.Collections.nCopies()