JavaRush /Blogue Java /Random-PT /Pausa para café #130. Como trabalhar corretamente com arr...

Pausa para café #130. Como trabalhar corretamente com arrays Java - dicas da Oracle

Publicado no grupo Random-PT
Fonte: Oracle Trabalhar com arrays pode incluir expressões de reflexão, genéricas e lambda. Recentemente estive conversando com um colega que desenvolve em C. A conversa girou em torno de arrays e como eles funcionam em Java em comparação com C. Achei isso um pouco estranho, visto que Java é considerada uma linguagem semelhante a C. Na verdade, eles têm muitas semelhanças, mas também existem diferenças. Vamos começar de forma simples. Pausa para café #130.  Como trabalhar corretamente com arrays Java – dicas da Oracle – 1

Declaração de matriz

Se você seguir o tutorial Java, verá que existem duas maneiras de declarar um array. O primeiro é direto:
int[] array; // a Java array declaration
Você pode ver como ele difere de C, onde a sintaxe é:
int array[]; // a C array declaration
Voltemos novamente ao Java. Depois de declarar um array, você precisa alocá-lo:
array = new int[10]; // Java array allocation
É possível declarar e inicializar um array de uma só vez? Na verdade não:
int[10] array; // NOPE, ERROR!
No entanto, você pode declarar e inicializar o array imediatamente se já souber os valores:
int[] array = { 0, 1, 1, 2, 3, 5, 8 };
E se você não souber o significado? Aqui está o código que você verá com mais frequência para declarar, alocar e usar um 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;
...
Observe que especifiquei um array int , que é um array de tipos de dados primitivos Java . Vamos ver o que acontece se você tentar o mesmo processo com um array de objetos Java em vez de primitivos:
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 executarmos o código acima, obteremos uma exceção imediatamente após tentar usar o primeiro elemento do array. Por que? Mesmo que a matriz esteja alocada, cada segmento da matriz contém referências de objetos vazias. Se você inserir esse código em seu IDE, ele preencherá automaticamente o .val para você, portanto, o erro pode ser confuso. Para corrigir o bug, siga estas etapas:
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;
Mas não é elegante. Eu me perguntei por que não conseguia alocar facilmente um array e os objetos dentro do array com menos código, talvez até mesmo todos em uma linha. Para encontrar a resposta, conduzi vários experimentos.

Encontrando o nirvana entre arrays Java

Nosso objetivo é codificar com elegância. Seguindo as regras do “código limpo”, decidi criar código reutilizável para limpar o padrão de alocação do array. Aqui está a primeira tentativa:
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);
    }
}
A linha de código marcada como “veja isto” está exatamente como eu queria, graças à implementação toArray . Esta abordagem usa reflexão para encontrar o construtor padrão para a classe fornecida e depois chama esse construtor para instanciar um objeto dessa classe. O processo chama o construtor uma vez para cada elemento do array. Fabuloso! É uma pena que não funcione. O código compila bem, mas gera um erro ClassCastException quando executado. Para usar este código, você precisa criar um array de elementos Object e, em seguida, converter cada elemento do array em uma classe SomeClass como esta:
Object[] objects = MyArray.toArray(SomeClass.class, 32);
SomeClass scObj = (SomeClass)objects[0];
...
Isso não é elegante! Após mais experimentações, desenvolvi diversas soluções usando expressões de reflexão, genéricas e lambda.

Solução 1: use reflexão

Aqui estamos usando a classe java.lang.reflect.Array para instanciar um array da classe que você especifica em vez de usar a classe base java.lang.Object . Esta é essencialmente uma alteração de código de uma linha:
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;
}
Você pode usar esta abordagem para obter um array da classe desejada e então trabalhar com ele assim:
SomeClass[] array1 = (SomeClass[])MyArray.toArray(SomeClass.class, 32);
Embora esta não seja uma alteração obrigatória, a segunda linha foi alterada para usar a classe de reflexão Array para definir o conteúdo de cada elemento do array. Isso é incrível! Mas há mais um detalhe que não parece muito certo: a conversão para SomeClass[] não parece muito boa. Felizmente, existe uma solução com genéricos.

Solução 2: use genéricos

A estrutura Collections usa genéricos para vinculação de tipos e elimina conversões para eles em muitas de suas operações. Genéricos também podem ser usados ​​aqui. Vamos pegar java.util.List por exemplo .
List list = new ArrayList();
list.add( new SomeClass() );
SomeClass sc = list.get(0); // Error, needs a cast unless...
A terceira linha no snippet acima gerará um erro, a menos que você atualize a primeira linha assim:
List<SomeClass> = new ArrayList();
Você pode obter o mesmo resultado usando genéricos na classe MyArray . Aqui está a nova versão:
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();
Isso parece bom. Ao usar genéricos e incluir o tipo de destino na declaração, o tipo pode ser inferido em outras operações. Além disso, este código pode ser reduzido a uma linha fazendo o seguinte:
SomeClass[] array = new MyArray<SomeClass>(SomeClass.class, 32).toArray();
Missão cumprida, certo? Bem, não exatamente. Isso é bom se você não se importa com qual construtor de classe você chama, mas se quiser chamar um construtor específico, esta solução não funciona. Você pode continuar a usar a reflexão para resolver esse problema, mas o código se tornará complexo. Felizmente, existem expressões lambda que oferecem outra solução.

Solução 3: use expressões lambda

Admito que não estava particularmente entusiasmado com as expressões lambda antes, mas aprendi a apreciá-las. Em particular, gostei da interface java.util.stream.Stream , que lida com coleções de objetos. Stream me ajudou a alcançar o nirvana do array Java. Aqui está minha primeira tentativa de usar lambdas:
SomeClass[] array =
    Stream.generate(() -> new SomeClass())
    .toArray(SomeClass[]::new);
Dividi esse código em três linhas para facilitar a leitura. Você pode ver que ele preenche todos os requisitos: é simples e elegante, cria um array preenchido de instâncias de objetos e permite chamar um construtor específico. Preste atenção ao parâmetro do método toArray : SomeClass[]::new . Esta é uma função geradora usada para alocar um array do tipo especificado. Porém, do jeito que está, esse código tem um pequeno problema: ele cria um array de tamanho infinito. Isso não é muito ideal. Mas o problema pode ser resolvido chamando o método limit :
SomeClass[] array =
    Stream.generate(() -> new SomeClass())
    .limit(32)   // calling the limit method
    .toArray(SomeClass[]::new);
A matriz agora está limitada a 32 elementos. Você pode até definir valores de objetos específicos para cada elemento do array, conforme mostrado abaixo:
SomeClass[] array = Stream.generate(() -> {
    SomeClass result = new SomeClass();
    result.val = 16;
    return result;
    })
    .limit(32)
    .toArray(SomeClass[]::new);
Este código demonstra o poder das expressões lambda, mas o código não é simples nem compacto. Na minha opinião, chamar outro construtor para definir o valor seria muito melhor.
SomeClass[] array6 = Stream.generate( () -> new SomeClass(16) )
    .limit(32)
    .toArray(SomeClass[]::new);
Gosto da solução baseada na expressão lambda. É ideal quando você precisa chamar um construtor específico ou trabalhar com cada elemento de um array. Quando preciso de algo mais simples, costumo usar uma solução baseada em genéricos porque é mais simples. No entanto, você pode ver por si mesmo que as expressões lambda fornecem uma solução elegante e flexível.

Conclusão

Hoje aprendemos como trabalhar com declaração e alocação de arrays de primitivos, alocação de arrays de elementos Object , usando reflexão, genéricos e expressões lambda em Java.
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION