Elegant array development might encompass reflection, generics, and lambdas.
I was recently chatting with a colleague who develops in C. The conversation came to arrays and how very different they work in Java compared to C—which he found surprising given that Java is considered a C-like language.
This exploration of Java arrays starts simple but quickly gets interesting, especially if you studied or use C.
Declaring an array
If you follow a tutorial on Java, you’ll see there are two ways to declare an array. The first is straightforward.
int[] array; // a Java array declaration
You can see how it differs from C, where the following is the proper syntax:
int array[]; // a C array declaration
Focusing now on Java, after declaring an array you need to allocate it.
array = new int[10]; // Java array allocation
Can you declare and initialize an array in one step? Well, you cannot take a shortcut and do the following:
int[10] array; // NOPE, ERROR!
However, you can declare and initialize an array in one step if you already know the values.
int[] array = { 0, 1, 1, 2, 3, 5, 8 };
What if you don’t know the values? Here’s the code you’ll encounter more often to declare, allocate, and use 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;
...
Notice 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 you run the code above, you’ll get an exception as soon as you try to use the first array element. Why? Although the array is allocated, the array buckets each contain null object references. If you type this code into your IDE, it will even autocomplete the .val for you, so the error can be confusing. To resolve the error, do the following:
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;
That is not elegant. Indeed, it has often frustrated me that I cannot more easily allocate the array, and the objects within the array, by writing less code, maybe even all in one line.
Thus, I did some experimenting.
Finding Java array nirvana
The goal here is coding elegance, not to be a purist. Smells like “clean code” spirit! And in that spirit, I set out to create some reusable code to clean up the array allocation pattern. Here’s a first attempt.
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 one line of code marked as “see this” is elegant and looks just the way I wanted, thanks to the implementation of toArray. This approach uses reflection to find the default constructor for the provided class, and then it calls that constructor to instantiate an object of this class. The process calls the constructor once for each element of the array. Brilliant!
Too bad it doesn’t work.
The code compiles fine but results in a ClassCastException when you run it. To use this code, you need to create an array of Object elements and then cast each array element to class SomeClass, as follows:
Object[] objects = MyArray.toArray(SomeClass.class, 32);
SomeClass scObj = (SomeClass)objects[0];
...
That isn’t elegant! After more experimentation, I evolved several solutions that use reflection, generics, and lambdas.
Solution 1: Use reflection
The answer to the issues above is to use 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 single-line code change that gets closer to the goal.
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 class you desire, and then operate on it as follows:
SomeClass[] array1 = (SomeClass[])MyArray.toArray(SomeClass.class, 32);
Although it’s not a necessary change, the second line was modified to use reflection’s Array class to set each array element’s content. This is great! But there’s one more detail that does not feel quite right: That cast to SomeClass[] isn’t elegant. Fortunately, there is a solution with generics.
Solution 2: Use generics
The Collections framework uses generics to be type-specific and eliminate casts in many of their operations. You can use generics here as well. 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 result in an error, unless you update the first line as follows:
List<SomeClass> = new ArrayList();
You can achieve the same result 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();
This looks good. By using generics and including the target type in the declaration, the type can be inferred in other operations. In fact, this code can be reduced to a single line, as follows, if you choose:
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’re calling, but if you want to call a specific constructor, this solution falls short. You can continue to use reflection to solve this problem, but the code may get complex. Fortunately, lambdas offer another solution.
Solution 3: Use lambdas
I’ll admit, I was slow to adopt lambdas, but I’ve grown to appreciate their value. In particular, I’ve come to appreciate the java.util.stream.Stream interface, which processes collections of objects. Stream helped me achieve Java array nirvana.
Here’s my first attempt to use lambdas.
SomeClass[] array =
Stream.generate(() -> new SomeClass())
.toArray(SomeClass[]::new);
I broke this code across three lines for readability. You can see it checks all the boxes: It is simple and elegant, creates a populated array of instantiated objects, and allows you to call a specific constructor.
Notice the parameter to the toArray method: SomeClass[]::new. This is a generator function used to allocate an array of the specified type.
However, as it stands, this code has a minor issue: It creates an array of infinite size. That is suboptimal. Fortunately, this can be resolved 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 array element, as in the following:
SomeClass[] array = Stream.generate(() -> {
SomeClass result = new SomeClass();
result.val = 16;
return result;
})
.limit(32)
.toArray(SomeClass[]::new);
This code demonstrates the power of lambdas, but the code is not neat and compact. Calling a different constructor to set the value is much cleaner, in my opinion.
SomeClass[] array6 = Stream.generate( () -> new SomeClass(16) )
.limit(32)
.toArray(SomeClass[]::new);
I like the lambda-based solution, which is ideal when you need to call a specific constructor or operate on each array element. When I need something more basic, I tend to use the solution based on generics since it’s simpler. However, you can see that lambdas provide an elegant and flexible solution.
Source: oracle.com
0 comments:
Post a Comment