JavaRush /Java Blog /Random EN /Coffee break #130. How to work with Java arrays correctly...

Coffee break #130. How to work with Java arrays correctly - tips from Oracle

Published in the Random EN group
Source: Oracle Working with arrays can include reflection, generics, and lambda expressions. I was recently talking with a colleague who develops in C. The conversation turned to arrays and how they work in Java compared to C. I found this a little strange, given that Java is considered a C-like language. They actually have a lot of similarities, but there are also differences. Let's start simple. Coffee break #130.  How to work correctly with Java arrays - tips from Oracle - 1

Array Declaration

If you follow the Java tutorial, you will see that there are two ways to declare an array. The first one is straightforward:
int[] array; // a Java array declaration
You can see how it differs from C, where the syntax is:
int array[]; // a C array declaration
Let's return again to Java. After declaring an array, you need to allocate it:
array = new int[10]; // Java array allocation
Is it possible to declare and initialize an array at once? Actually no:
int[10] array; // NOPE, ERROR!
However, you can declare and initialize the array right away if you already know the values:
int[] array = { 0, 1, 1, 2, 3, 5, 8 };
What if you don't know the meaning? Here's the code you'll see most often for declaring, allocating, and using an int array :
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;
...
Note that I specified an int array , which is an array of Java primitive data types . Let's see what happens if you try the same process with an array of Java objects instead of primitives:
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;
If we run the above code, we will get an exception immediately after trying to use the first element of the array. Why? Even though the array is allocated, each segment of the array contains empty object references. If you enter this code into your IDE, it will even automatically fill in the .val for you, so the error may be confusing. To fix the bug, follow these steps:
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;
But it's not elegant. I wondered why I couldn't easily allocate an array and the objects within the array with less code, maybe even all on one line. To find the answer, I conducted several experiments.

Finding nirvana among Java arrays

Our goal is to code elegantly. Following the rules of “clean code”, I decided to create reusable code to clean up the array allocation pattern. Here's the first try:
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);
    }
}
The line of code marked “see this” looks exactly the way I wanted, thanks to the toArray implementation . This approach uses reflection to find the default constructor for the provided class and then calls that constructor to instantiate an object of that class. The process calls the constructor once for each array element. Fabulous! It's just a pity that it doesn't work. The code compiles fine, but throws a ClassCastException error when run. To use this code, you need to create an array of Object elements, and then cast each element of the array to a SomeClass class like this:
Object[] objects = MyArray.toArray(SomeClass.class, 32);
SomeClass scObj = (SomeClass)objects[0];
...
This is not elegant! After more experimentation, I developed several solutions using reflection, generics, and lambda expressions.

Solution 1: Use reflection

Here we are using the java.lang.reflect.Array class to instantiate an array of the class you specify instead of using the base java.lang.Object class . This is essentially a one-line code change:
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;
}
You can use this approach to get an array of the desired class and then work with it like this:
SomeClass[] array1 = (SomeClass[])MyArray.toArray(SomeClass.class, 32);
Although this is not a required change, the second line has been changed to use the Array reflection class to set the contents of each array element. This is amazing! But there is one more detail that doesn't seem quite right: the cast to SomeClass[] doesn't look very nice. Fortunately, there is a solution with generics.

Solution 2: Use generics

The Collections framework uses generics for type binding and eliminates casts to them in many of its operations. Generics can also be used here. Let's take java.util.List for example .
List list = new ArrayList();
list.add( new SomeClass() );
SomeClass sc = list.get(0); // Error, needs a cast unless...
The third line in the above snippet will throw an error unless you update the first line like this:
List<SomeClass> = new ArrayList();
You can achieve the same result by using generics in the MyArray class . Here's the new version:
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();
It looks good. By using generics and including the target type in the declaration, the type can be inferred in other operations. Additionally, this code can be reduced to one line by doing this:
SomeClass[] array = new MyArray<SomeClass>(SomeClass.class, 32).toArray();
Mission accomplished, right? Well, not quite. This is fine if you don't care which class constructor you call, but if you want to call a specific constructor, then this solution doesn't work. You can continue to use reflection to solve this problem, but then the code will become complex. Luckily, there are lambda expressions that offer another solution.

Solution 3: Use lambda expressions

I'll admit, I wasn't particularly excited about lambda expressions before, but I've learned to appreciate them. In particular, I liked the java.util.stream.Stream interface , which handles collections of objects. Stream helped me reach Java array nirvana. Here's my first attempt at using lambdas:
SomeClass[] array =
    Stream.generate(() -> new SomeClass())
    .toArray(SomeClass[]::new);
I've broken this code into three lines for easier reading. You can see that it ticks all the boxes: it's simple and elegant, creates a populated array of object instances, and allows you to call a specific constructor. Pay attention to the toArray method parameter : SomeClass[]::new . This is a generator function used to allocate an array of the specified type. However, as it stands, this code has a small problem: it creates an array of infinite size. This is not very optimal. But the problem can be solved by calling the limit method :
SomeClass[] array =
    Stream.generate(() -> new SomeClass())
    .limit(32)   // calling the limit method
    .toArray(SomeClass[]::new);
The array is now limited to 32 elements. You can even set specific object values ​​for each element of the array, as shown below:
SomeClass[] array = Stream.generate(() -> {
    SomeClass result = new SomeClass();
    result.val = 16;
    return result;
    })
    .limit(32)
    .toArray(SomeClass[]::new);
This code demonstrates the power of lambda expressions, but the code is not neat or compact. In my opinion, calling another constructor to set the value would be much better.
SomeClass[] array6 = Stream.generate( () -> new SomeClass(16) )
    .limit(32)
    .toArray(SomeClass[]::new);
I like the lambda expression based solution. It's ideal when you need to call a specific constructor or work with each element of an array. When I need something simpler, I usually use a generics-based solution because it's simpler. However, you can see for yourself that lambda expressions provide an elegant and flexible solution.

Conclusion

Today we learned how to work with declaring and allocating arrays of primitives, allocating arrays of Object elements , using reflection, generics, and lambda expressions in Java.
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION