Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ClassCastException when using custom map supplier in grouping-by

While I was doing a little programming exercise i stumbled upon a ClassCastException. As background i'm giving a simplified version of the exercise to demonstrate the problem:

Given a string which contains only the characters A or B compute a map with the characters as keys and the number of occurrences as values. Additionally the map should always contain both characters as key (with value zero if a character is missing in the input string).

Examples:

  • "A" => {A=1, B=0}
  • "AAB" => {A=2, B=1}

My first solution was the following:

import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.groupingBy;

public Map<Character, Long> createResult(String input) {
    Map<Character, Long> map = input.chars()
        .mapToObj(c -> (char) c)
        .collect(groupingBy(c -> c, counting()));

    map.putIfAbsent('A', 0L);
    map.putIfAbsent('B', 0L);
    return map;
}

This solution worked but i wanted to try if it was possible to supply a map with default values to the groupingBy function:

public HashMap<Character, Long> createResult2(String input) {
    return input.chars()
        .mapToObj(c -> (char) c)
        .collect(groupingBy(c -> c, this::mapFactory, counting()));
}

private HashMap<Character, Long> mapFactory() {
    HashMap<Character, Long> map = new HashMap<>();
    map.put('A', 0L);
    map.put('B', 0L);
    return map;
}

When calling createResult2 with input A a ClassCastException is thrown at runtime:

java.lang.ClassCastException: java.lang.Long cannot be cast to [Ljava.lang.Object;
    at java.util.stream.Collectors.lambda$groupingBy$45(Collectors.java:909)
    at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
    at java.util.stream.IntPipeline$4$1.accept(IntPipeline.java:250)
    at java.lang.CharSequence$1CharIterator.forEachRemaining(CharSequence.java:149)
    at java.util.Spliterators$IntIteratorSpliterator.forEachRemaining(Spliterators.java:1908)
    at java.util.Spliterator$OfInt.forEachRemaining(Spliterator.java:693)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
    at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)

Can anyone explain why this is happening?

like image 459
eee Avatar asked Nov 02 '16 17:11

eee


1 Answers

There is type inference magic involved, but if you want solution here it is:

Replace

map.put('A', 0L);
map.put('B', 0L);

by

map.put('A', new Object[]{0L});
map.put('B', new Object[]{0L});

But I strongly discourage you of using this solution in practice. Implementation details can be changed in any update and this hack stop works.

Here more explanation about "why" from groupingBy javadoc

mapFactory - a function which, when called, produces a new empty Map of the desired type

mapFactory is second parameter. Word "empty" is very important. Implementation use created map to store arrays of long while iterating and then convert them to long. It works because of massive amount of casting inside.

like image 157
talex Avatar answered Sep 28 '22 02:09

talex