JavaRush /Java Blog /Random-IT /Pausa caffè #130. Come lavorare correttamente con gli arr...

Pausa caffè #130. Come lavorare correttamente con gli array Java: suggerimenti di Oracle

Pubblicato nel gruppo Random-IT
Fonte: Oracle Lavorare con gli array può includere espressioni di riflessione, generici ed lambda. Recentemente ho parlato con un collega che sviluppa in C. La conversazione si è concentrata sugli array e su come funzionano in Java rispetto al C. L'ho trovato un po' strano, dato che Java è considerato un linguaggio simile al C. In realtà hanno molte somiglianze, ma ci sono anche differenze. Cominciamo in modo semplice. Pausa caffè #130.  Come lavorare correttamente con gli array Java - suggerimenti da Oracle - 1

Dichiarazione di matrice

Se segui il tutorial Java, vedrai che ci sono due modi per dichiarare un array. Il primo è semplice:
int[] array; // a Java array declaration
Puoi vedere come differisce da C, dove la sintassi è:
int array[]; // a C array declaration
Torniamo di nuovo a Java. Dopo aver dichiarato un array, è necessario allocarlo:
array = new int[10]; // Java array allocation
È possibile dichiarare e inizializzare un array contemporaneamente? In realtà no:
int[10] array; // NOPE, ERROR!
Tuttavia, puoi dichiarare e inizializzare subito l'array se conosci già i valori:
int[] array = { 0, 1, 1, 2, 3, 5, 8 };
E se non ne conosci il significato? Ecco il codice che vedrai più spesso per dichiarare, allocare e utilizzare un array int :
int[] array;
array = new int[10];
array[0] = 0;
array[1] = 1;
array[2] = 1;
array[3] = 2;
array[4] = 3;
array[5] = 5;
array[6] = 8;
...
Tieni presente che ho specificato un array int , che è un array di tipi di dati primitivi Java . Vediamo cosa succede se provi lo stesso processo con un array di oggetti Java anziché primitivi:
class SomeClass {
    int val;
    // …
}
SomeClass[] array = new SomeClass[10];
array[0].val = 0;
array[1].val = 1;
array[2].val = 1;
array[3].val = 2;
array[4].val = 3;
array[5].val = 5;
array[6].val = 8;
Se eseguiamo il codice precedente, otterremo un'eccezione immediatamente dopo aver tentato di utilizzare il primo elemento dell'array. Perché? Anche se la matrice è allocata, ogni segmento della matrice contiene riferimenti a oggetti vuoti. Se inserisci questo codice nel tuo IDE, compilerà automaticamente anche il file .val per te, quindi l'errore potrebbe creare confusione. Per correggere il bug, attenersi alla seguente procedura:
SomeClass[] array = new SomeClass[10];
for ( int i = 0; i < array.length; i++ ) {  //new code
    array[i] = new SomeClass();             //new code
}                                           //new code
array[0].val = 0;
array[1].val = 1;
array[2].val = 1;
array[3].val = 2;
array[4].val = 3;
array[5].val = 5;
array[6].val = 8;
Ma non è elegante. Mi chiedevo perché non potessi allocare facilmente un array e gli oggetti all'interno dell'array con meno codice, magari anche tutti su una riga. Per trovare la risposta, ho condotto diversi esperimenti.

Trovare il nirvana tra gli array Java

Il nostro obiettivo è codificare in modo elegante. Seguendo le regole del "codice pulito", ho deciso di creare codice riutilizzabile per ripulire il modello di allocazione dell'array. Ecco il primo tentativo:
public class MyArray {

    public static Object[] toArray(Class cls, int size)
      throws Exception {
        Constructor ctor = cls.getConstructors()[0];
        Object[] objects = new Object[size];
        for ( int i = 0; i < size; i++ ) {
            objects[i] = ctor.newInstance();
        }

        return objects;
    }

    public static void main(String[] args) throws Exception {
        SomeClass[] array1 = (SomeClass[])MyArray.toArray(SomeClass.class, 32); // see this
        System.out.println(array1);
    }
}
La riga di codice contrassegnata con "vedi questo" appare esattamente come volevo, grazie all'implementazione toArray . Questo approccio utilizza la riflessione per trovare il costruttore predefinito per la classe fornita e quindi chiama quel costruttore per creare un'istanza di un oggetto di quella classe. Il processo chiama il costruttore una volta per ciascun elemento dell'array. Favoloso! È solo un peccato che non funzioni. Il codice viene compilato correttamente, ma genera un errore ClassCastException durante l'esecuzione. Per utilizzare questo codice, devi creare un array di elementi Object e quindi trasmettere ciascun elemento dell'array a una classe SomeClass come questa:
Object[] objects = MyArray.toArray(SomeClass.class, 32);
SomeClass scObj = (SomeClass)objects[0];
...
Questo non è elegante! Dopo ulteriori sperimentazioni, ho sviluppato diverse soluzioni utilizzando la riflessione, i generici e le espressioni lambda.

Soluzione 1: utilizzare la riflessione

Qui stiamo utilizzando la classe java.lang.reflect.Array per creare un'istanza di un array della classe specificata invece di utilizzare la classe base java.lang.Object . Si tratta essenzialmente di una modifica del codice di una riga:
public static Object[] toArray(Class cls, int size) throws Exception {
    Constructor ctor = cls.getConstructors()[0];
    Object array = Array.newInstance(cls, size);  // new code
    for ( int i = 0; i < size; i++ ) {
        Array.set(array, i, ctor.newInstance());  // new code
    }
    return (Object[])array;
}
Puoi utilizzare questo approccio per ottenere un array della classe desiderata e quindi lavorarci in questo modo:
SomeClass[] array1 = (SomeClass[])MyArray.toArray(SomeClass.class, 32);
Sebbene questa non sia una modifica necessaria, la seconda riga è stata modificata per utilizzare la classe di riflessione Array per impostare il contenuto di ciascun elemento dell'array. Questo è fantastico! Ma c'è un altro dettaglio che non sembra del tutto corretto: il cast di SomeClass[] non sembra molto carino. Fortunatamente esiste una soluzione con i farmaci generici.

Soluzione 2: utilizzare farmaci generici

Il framework Collections utilizza i generics per l'associazione dei tipi ed elimina i cast in molte delle sue operazioni. In questo caso è possibile utilizzare anche i generici. Prendiamo ad esempio java.util.List .
List list = new ArrayList();
list.add( new SomeClass() );
SomeClass sc = list.get(0); // Error, needs a cast unless...
La terza riga nello snippet sopra genererà un errore a meno che non aggiorni la prima riga in questo modo:
List<SomeClass> = new ArrayList();
È possibile ottenere lo stesso risultato utilizzando i generici nella classe MyArray . Ecco la nuova versione:
public class MyArray<E> {
    public <E> E[] toArray(Class cls, int size) throws Exception {
        E[] array = (E[])Array.newInstance(cls, size);
        Constructor ctor = cls.getConstructors()[0];
        for ( int element = 0; element < array.length; element++ ) {
            Array.set(array, element, ctor.newInstance());
        }
        return arrayOfGenericType;
    }
}
// ...
MyArray<SomeClass> a1 = new MyArray(SomeClass.class, 32);
SomeClass[] array1 = a1.toArray();
Sembra buona. Utilizzando i generics e includendo il tipo di destinazione nella dichiarazione, il tipo può essere dedotto in altre operazioni. Inoltre, questo codice può essere ridotto a una riga in questo modo:
SomeClass[] array = new MyArray<SomeClass>(SomeClass.class, 32).toArray();
Missione compiuta, vero? Beh, non proprio. Questo va bene se non ti interessa quale costruttore di classe chiami, ma se vuoi chiamare un costruttore specifico, questa soluzione non funziona. Puoi continuare a utilizzare la riflessione per risolvere questo problema, ma in questo caso il codice diventerà complesso. Fortunatamente, esistono espressioni lambda che offrono un'altra soluzione.

Soluzione 3: utilizzare le espressioni lambda

Lo ammetto, prima non ero particolarmente entusiasta delle espressioni lambda, ma ho imparato ad apprezzarle. In particolare, mi è piaciuta l' interfaccia java.util.stream.Stream , che gestisce raccolte di oggetti. Stream mi ha aiutato a raggiungere il nirvana dell'array Java. Ecco il mio primo tentativo di utilizzare lambda:
SomeClass[] array =
    Stream.generate(() -> new SomeClass())
    .toArray(SomeClass[]::new);
Ho suddiviso questo codice in tre righe per facilitarne la lettura. Puoi vedere che soddisfa tutte le esigenze: è semplice ed elegante, crea un array popolato di istanze di oggetti e ti consente di chiamare un costruttore specifico. Presta attenzione al parametro del metodo toArray : SomeClass[]::new . Questa è una funzione generatrice utilizzata per allocare un array del tipo specificato. Tuttavia, così com'è, questo codice presenta un piccolo problema: crea un array di dimensione infinita. Questo non è molto ottimale. Ma il problema può essere risolto chiamando il metodo limite :
SomeClass[] array =
    Stream.generate(() -> new SomeClass())
    .limit(32)   // calling the limit method
    .toArray(SomeClass[]::new);
L'array è ora limitato a 32 elementi. Puoi anche impostare valori oggetto specifici per ciascun elemento dell'array, come mostrato di seguito:
SomeClass[] array = Stream.generate(() -> {
    SomeClass result = new SomeClass();
    result.val = 16;
    return result;
    })
    .limit(32)
    .toArray(SomeClass[]::new);
Questo codice dimostra la potenza delle espressioni lambda, ma il codice non è pulito o compatto. Secondo me, chiamare un altro costruttore per impostare il valore sarebbe molto meglio.
SomeClass[] array6 = Stream.generate( () -> new SomeClass(16) )
    .limit(32)
    .toArray(SomeClass[]::new);
Mi piace la soluzione basata sull'espressione lambda. È ideale quando è necessario chiamare un costruttore specifico o lavorare con ciascun elemento di un array. Quando ho bisogno di qualcosa di più semplice, di solito utilizzo una soluzione basata sui farmaci generici perché è più semplice. Tuttavia, puoi vedere tu stesso che le espressioni lambda forniscono una soluzione elegante e flessibile.

Conclusione

Oggi abbiamo imparato come lavorare con la dichiarazione e l'allocazione di array di primitive, l'allocazione di array di elementi Object , utilizzando la riflessione, i generici e le espressioni lambda in Java.
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION