Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

Sequences of data are integral to any project and any programming language. In Java, there are two ways to represent a sequence of elements: Collections and arrays.

In this tutorial, we’ll learn how to convert an ArrayList of wrapper classes into an array of primitives. While this sounds like a trivial task, some quirks in the Java APIs make this process less straightforward.

2. Simple For Loop

The easiest way to make this conversion is to use a declarative style with a for loop:

@ParameterizedTest
@MethodSource("floatListProvider")
void givenListOfWrapperFloat_whenConvertToPrimitiveArray_thenGetCorrectResult(List<Float> floats) {
    float[] actual = new float[floats.size()];
    for (int i = 0; i < floats.size(); i++) {
        actual[i] = floats.get(i);
    }
    compareSequences(floats, actual);
}

The main benefit of this code is that it’s explicit and easy to follow. However, we must take care of too many things for such a trivial task.

3. Converting to an Array of Float

The Collection API provides a nice method to convert a List into an array but doesn’t handle unboxing. However, it’s useful enough to consider it in this article:

@ParameterizedTest
@MethodSource("floatListProvider")
void givenListOfWrapperFloat_whenConvertToWrapperArray_thenGetCorrectResult(List<Float> floats) {
    Float[] actual = floats.toArray(new Float[0]);
    assertSequences(floats, actual);
}

The List class has the toArray() method, which can help us with conversion. However, the API is a bit confusing. We need to pass an array to ensure the correct type. The result will be of the same type as the array we pass.

Because we need to pass an instance, it’s unclear what size we should use and if the resulting array would be cropped. In this case, we shouldn’t worry about the size at all, and toArray() will take care of and expand an array if necessary.

At the same time, it’s fine to pass an array of specific size straight away:

@ParameterizedTest
@MethodSource("floatListProvider")
void givenListOfWrapperFloat_whenConvertToWrapperArrayWithPreSizedArray_thenGetCorrectResult(List<Float> floats) {
    Float[] actual = floats.toArray(new Float[floats.size()]);
    assertSequences(floats, actual);
}

Although it seems to be an optimization over the previous version, it’s not necessarily true. Java compiler would take care of the size without any problems. Additionally, calling the size() while creating an array might create issues in a multithreaded environment. Thus, using an empty array is recommended, as shown previously.

4. Unboxing Arrays

While we have the concept of unboxing for numeric values and booleans, trying to unbox arrays would result in a compile-time error. Thus, we should unbox each element separately. Here’s the variation of an example we’ve seen before:

@ParameterizedTest
@MethodSource("floatListProvider")
void givenListOfWrapperFloat_whenUnboxToPrimitiveArray_thenGetCorrectResult(List<Float> floats) {
    float[] actual = new float[floats.size()];
    Float[] floatArray = floats.toArray(new Float[0]);
    for (int i = 0; i < floats.size(); i++) {
        actual[i] = floatArray[i];
    }
    assertSequences(floats, actual);
}

We have two issues here. First, we’re using additional space for temporary arrays; it doesn’t affect the time complexity as we have to use the space for the result anyway.

The second issue is that the for loop doesn’t do much, as we use implicit unboxing here. It would be a good idea to eliminate it. We can do this with the help of utility class from Apache Commons:

@ParameterizedTest
@MethodSource("floatListProvider")
void givenListOfWrapperFloat_whenConvertToPrimitiveArrayWithArrayUtils_thenGetCorrectResult(List<Float> floats) {
    float[] actual = ArrayUtils.toPrimitive(floats.toArray(new Float[]{}));
    assertSequences(floats, actual);
}

This way, we get a nice one-liner solution to our problem. The toPrimitive() method just encapsulated the logic we used previously, with additional checks:

public static float[] toPrimitive(final Float[] array) {
    if (array == null) {
        return null;
    }
    if (array.length == 0) {
        return EMPTY_FLOAT_ARRAY;
    }
    final float[] result = new float[array.length];
    for (int i = 0; i < array.length; i++) {
        result[i] = array[i].floatValue();
    }
    return result;
}

It’s a nice and clean solution but requires some additional libraries. Alternatively, we can implement and use a similar method in our code.

5. Streams

When working with Collections, we can use streams to replicate the logic we used in loops. The Stream API can help us to convert a List and unbox the values at the same time. However, there’s a caveat: Java doesn’t have FloatStream.

If we’re not too picky about the floating point numbers, we can use DoubleStream to convert ArrayList<Float> to double[]:

@ParameterizedTest
@MethodSource("floatListProvider")
void givenListOfWrapperFloat_whenConvertingToPrimitiveArrayUsingStreams_thenGetCorrectResult(List<Float> floats) {
    double[] actual = floats.stream().mapToDouble(Float::doubleValue).toArray();
    assertSequences(floats, actual);
}

We successfully converted the List but in a slightly different floating-point representation. This is because we have only IntStream, LongStream, and DoubleStream available.

6. Custom Collectors

At the same time, we can implement a custom Collector and have all the logic inside it:

public class FloatCollector implements Collector<Float, float[], float[]> {
    private final int size;
    private int index = 0;
    public FloatCollector(int size) {
        this.size = size;
    }

    @Override
    public Supplier<float[]> supplier() {
        return () -> new float[size];
    }

    @Override
    public BiConsumer<float[], Float> accumulator() {
        return (array, number) -> {
            array[index] = number;
            index++;
        };
    }

    // other non-important methods
}

Other non-important methods include some stubs to allow our code to run and a no-op finalizer:

public class FloatCollector implements Collector<Float, float[], float[]> {
    // important methods

    @Override
    public BinaryOperator<float[]> combiner() {
        return null;
    }

    @Override
    public Function<float[], float[]> finisher() {
        return Function.identity();
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.emptySet();
    }
}

And now we can showcase our new and a little bit hacky Collector:

@ParameterizedTest
@MethodSource("floatListProvider")
void givenListOfWrapperFloat_whenConvertingWithCollector_thenGetCorrectResult(List<Float> floats) {
    float[] actual = floats.stream().collect(new FloatCollector(floats.size()));
    assertSequences(floats, actual);
}

While playing with Stream API interfaces is interesting, this solution is overly complex and doesn’t provide any benefits in this particular case. Also, this collector might work in a multithreaded environment, and we should take thread-safety into account.

7. Conclusion

Working with arrays and Collections is usual for any application. While Lists provide a better interface, sometimes we need to convert them into simple arrays.

Additional unboxing during this process makes it more challenging than it should be. However, a couple of tricks, custom methods, or third-party libraries can help streamline it.

As usual, all the code from this tutorial is available over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments