JavaRush /Blog Java /Random-ES /Pausa para el café #130. Cómo trabajar correctamente con ...

Pausa para el café #130. Cómo trabajar correctamente con matrices Java: consejos de Oracle

Publicado en el grupo Random-ES
Fuente: Oracle Trabajar con matrices puede incluir expresiones reflexivas, genéricas y lambda. Hace poco estuve hablando con un colega que desarrolla en C. La conversación se centró en las matrices y cómo funcionan en Java en comparación con C. Esto me pareció un poco extraño, dado que Java se considera un lenguaje similar a C. En realidad tienen muchas similitudes, pero también diferencias. Empecemos de forma sencilla. Pausa para el café #130.  Cómo trabajar correctamente con matrices Java: consejos de Oracle - 1

Declaración de matriz

Si sigue el tutorial de Java, verá que hay dos formas de declarar una matriz. El primero es sencillo:
int[] array; // a Java array declaration
Puedes ver en qué se diferencia de C, donde la sintaxis es:
int array[]; // a C array declaration
Volvamos nuevamente a Java. Después de declarar una matriz, debes asignarla:
array = new int[10]; // Java array allocation
¿Es posible declarar e inicializar una matriz a la vez? En realidad no:
int[10] array; // NOPE, ERROR!
Sin embargo, puedes declarar e inicializar la matriz de inmediato si ya conoces los valores:
int[] array = { 0, 1, 1, 2, 3, 5, 8 };
¿Qué pasa si no sabes el significado? Este es el código que verá con más frecuencia para declarar, asignar y usar una matriz 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;
...
Tenga en cuenta que especifiqué una matriz int , que es una matriz de tipos de datos primitivos de Java . Veamos qué sucede si intentas el mismo proceso con una matriz de objetos Java en lugar 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;
Si ejecutamos el código anterior, obtendremos una excepción inmediatamente después de intentar utilizar el primer elemento de la matriz. ¿Por qué? Aunque la matriz está asignada, cada segmento de la matriz contiene referencias de objetos vacías. Si ingresa este código en su IDE, incluso completará automáticamente el .val, por lo que el error puede resultar confuso. Para corregir el error, siga estos pasos:
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;
Pero no es elegante. Me preguntaba por qué no podía asignar fácilmente una matriz y los objetos dentro de la matriz con menos código, tal vez incluso todos en una sola línea. Para encontrar la respuesta, realicé varios experimentos.

Encontrar el nirvana entre las matrices de Java

Nuestro objetivo es codificar con elegancia. Siguiendo las reglas del "código limpio", decidí crear código reutilizable para limpiar el patrón de asignación de matrices. Aquí está el primer intento:
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 línea de código marcada "ver esto" se ve exactamente como quería, gracias a la implementación de toArray . Este enfoque utiliza la reflexión para encontrar el constructor predeterminado para la clase proporcionada y luego llama a ese constructor para crear una instancia de un objeto de esa clase. El proceso llama al constructor una vez para cada elemento de la matriz. ¡Fabuloso! Es una pena que no funcione. El código se compila bien, pero genera un error ClassCastException cuando se ejecuta. Para usar este código, necesita crear una matriz de elementos Object y luego convertir cada elemento de la matriz en una clase SomeClass como esta:
Object[] objects = MyArray.toArray(SomeClass.class, 32);
SomeClass scObj = (SomeClass)objects[0];
...
¡Esto no es elegante! Después de más experimentación, desarrollé varias soluciones utilizando expresiones de reflexión, genéricas y lambda.

Solución 1: utilizar la reflexión

Aquí estamos usando la clase java.lang.reflect.Array para crear una instancia de una matriz de la clase que especifique en lugar de usar la clase base java.lang.Object . Esto es esencialmente un cambio de código de una línea:
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;
}
Puede utilizar este enfoque para obtener una matriz de la clase deseada y luego trabajar con ella de esta manera:
SomeClass[] array1 = (SomeClass[])MyArray.toArray(SomeClass.class, 32);
Aunque este no es un cambio obligatorio, la segunda línea se cambió para usar la clase de reflexión Array para establecer el contenido de cada elemento de la matriz. ¡Esto es increíble! Pero hay un detalle más que no parece del todo correcto: el elenco de SomeClass[] no se ve muy bien. Afortunadamente, existe una solución con los genéricos.

Solución 2: utilice genéricos

El marco de Colecciones utiliza genéricos para la vinculación de tipos y elimina las conversiones en muchas de sus operaciones. Aquí también se pueden utilizar genéricos. Tomemos como ejemplo java.util.List .
List list = new ArrayList();
list.add( new SomeClass() );
SomeClass sc = list.get(0); // Error, needs a cast unless...
La tercera línea del fragmento anterior generará un error a menos que actualice la primera línea de esta manera:
List<SomeClass> = new ArrayList();
Puede lograr el mismo resultado utilizando genéricos en la clase MyArray . Aquí está la nueva versión:
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();
Se ve bien. Al utilizar genéricos e incluir el tipo de destino en la declaración, el tipo se puede inferir en otras operaciones. Además, este código se puede reducir a una línea haciendo esto:
SomeClass[] array = new MyArray<SomeClass>(SomeClass.class, 32).toArray();
Misión cumplida, ¿verdad? Bueno, no del todo. Esto está bien si no le importa a qué constructor de clase llame, pero si desea llamar a un constructor específico, entonces esta solución no funciona. Puede continuar usando la reflexión para resolver este problema, pero luego el código se volverá complejo. Afortunadamente, existen expresiones lambda que ofrecen otra solución.

Solución 3: utilice expresiones lambda

Lo admito, antes no me entusiasmaban especialmente las expresiones lambda, pero he aprendido a apreciarlas. En particular, me gustó la interfaz java.util.stream.Stream , que maneja colecciones de objetos. Stream me ayudó a alcanzar el nirvana de las matrices Java. Aquí está mi primer intento de usar lambdas:
SomeClass[] array =
    Stream.generate(() -> new SomeClass())
    .toArray(SomeClass[]::new);
He dividido este código en tres líneas para facilitar la lectura. Puede ver que cumple todos los requisitos: es simple y elegante, crea una matriz completa de instancias de objetos y le permite llamar a un constructor específico. Preste atención al parámetro del método toArray : SomeClass[]::new . Esta es una función generadora que se utiliza para asignar una matriz del tipo especificado. Sin embargo, tal como está, este código tiene un pequeño problema: crea una matriz de tamaño infinito. Esto no es muy óptimo. Pero el problema se puede resolver llamando al método límite :
SomeClass[] array =
    Stream.generate(() -> new SomeClass())
    .limit(32)   // calling the limit method
    .toArray(SomeClass[]::new);
La matriz ahora está limitada a 32 elementos. Incluso puedes establecer valores de objeto específicos para cada elemento de la matriz, como se muestra a continuación:
SomeClass[] array = Stream.generate(() -> {
    SomeClass result = new SomeClass();
    result.val = 16;
    return result;
    })
    .limit(32)
    .toArray(SomeClass[]::new);
Este código demuestra el poder de las expresiones lambda, pero el código no es claro ni compacto. En mi opinión, sería mucho mejor llamar a otro constructor para establecer el valor.
SomeClass[] array6 = Stream.generate( () -> new SomeClass(16) )
    .limit(32)
    .toArray(SomeClass[]::new);
Me gusta la solución basada en expresiones lambda. Es ideal cuando necesitas llamar a un constructor específico o trabajar con cada elemento de una matriz. Cuando necesito algo más simple, normalmente uso una solución basada en genéricos porque es más simple. Sin embargo, puede comprobar usted mismo que las expresiones lambda proporcionan una solución elegante y flexible.

Conclusión

Hoy aprendimos cómo trabajar con la declaración y asignación de matrices de primitivas, la asignación de matrices de elementos de objeto y el uso de expresiones reflexivas, genéricas y lambda en Java.
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION