Search code examples
javaarraysreferenceobserver-patternconsumer

ArrayStoreException in array of Consumers<X>


I’m storing references to BiConsumers<Integer, X> adapted to Consumer<Integer>:

public void setConsumer(BiConsumer<Integer, X> consumer) {
    fieldConsumer = integer -> consumer.accept(integer, fieldSubject);
}

But I need 2 of them, so I changed the code to use an array:

private Consumer<Integer>[] fieldConsumers;

public MyClass(int numberOfConsumers) {
    Consumer<Integer> consumer = integer -> {};
    fieldConsumers= (Consumer<Integer>[]) Array.newInstance(consumer.getClass(), numberOfObservers);
}

public void addConsumer(int consumerIndex, BiConsumer<Integer, X> consumer) {
    // Offending line
    fieldConsumers[consumerIndex] = responseType-> consumer.accept(responseType, fieldSubject);

}

So that the callback can be triggered with a:

for (Consumer<Integer> consumer: fieldConsumers) {
    consumer.accept(responseType);
}

I got the error:

java.lang.ArrayStoreException:

on this line:

fieldConsumers[consumerIndex] = responseType-> consumer.accept(responseType, fieldSubject);

Now, If you are still reading this, I have one more question:

Am I still holding reference to outside Consumers if I do it this way, as opposed to using the old fieldConsumers.add(consumer) where fieldConsumers is a List<BiConsumer<Integer, X>> ?


Solution

  • You used Array.newInstance(consumer.getClass(), numberOfObservers) to create the Consumer<Integer>[] array. But consumer.getClass() returns the actual class of the object you’re invoking the method on, which is always an implementation class of the interface. An array of this element type can only hold objects of the same concrete class, not arbitrary implementations of the interface.

    This is not different to, e.g.

    CharSequence cs = "hello";
    CharSequence[] array = (CharSequence[]) Array.newInstance(cs.getClass(), 1);
    array[0] = new StringBuilder();
    

    Here, cs has the type CharSequence and the reflective array creation appears to create an array of type CharSequence[], so storing a StringBuilder should be possible. But since cs.getClass() returns the actual implementation class String, the array is actually of type String[], hence, the attempt to store a StringBuilder produces an ArrayStoreException.

    In case of lambda expressions, things get slightly more complicated, as the actual implementation classes of the functional interface are provided at runtime and intentionally unspecified. You used the lambda expression integer -> {} for the array creation in the constructor, which evaluated to a different implementation class than the responseType-> consumer.accept(responseType, fieldSubject) within the addConsumer method, in this particular runtime.

    This behavior is in line with this answer describing the behavior of the most commonly used environment. Still, other implementations could exhibit different behavior, e.g. evaluate to the same implementation class for a particular functional interface for all lambda expressions. But it’s also possible that multiple evaluations of the same lambda expression produce different classes.

    So the fix is to use the intended interface element type, e.g.

    fieldConsumers=(Consumer<Integer>[])Array.newInstance(Consumer.class, numberOfObservers);
    

    But there is no need for a reflective array creation at all. You can use:

    fieldConsumers = new Consumer[numberOfObservers];
    

    You can not write new Consumer<Integer>[numberOfObservers], as generic array creation is not allowed. That’s why the code above uses a raw type. Using Reflection instead wouldn’t improve the situation, as it is an unchecked operation in either case. You might have to add @SuppressWarnings for it. The cleaner alternative is to use a List<Consumer<Integer>>, as it shields you from the oddities of arrays and generics.

    It’s not clear what you mean with “reference to outside Consumers” here. In either case, you have references to Consumer implementations capturing references to BiConsumer implementations you received as arguments to addConsumer.