Win a copy of Kotlin in Action this week in the Kotlin forum!
  • Post Reply Bookmark Topic Watch Topic
  • New Topic

ClassCastException when generic parameter made into varargs.  RSS feed

 
Stevens Miller
Bartender
Posts: 1444
30
C++ Java Netbeans IDE Windows
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
I'm working on passing instances to constructors, so the objects they construct can "learn" different behaviors at run-time (the "Strategy" pattern, I believe).

Here's my initial code, Version 1:



Everything seemed to be working fine, until I changed my Strategy interface so that its single method's signature went from single-argument to variable-argument (Lines 21 and 27), in Version 2:



Now, I get this at run-time:

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.awt.Dimension;
at classcast.ConcreteStrategy.execute(Main.java:24)
at classcast.Context.<init>(Main.java:15)
at classcast.Main.main(Main.java:7)

Thinking about erasure (something I don't yet quite grok), I thought the array of Objects the varargs version of execute now gets passed was a mismatch for its new signature. So, I made ConcreteStrategy generic (with changes at Lines 7, 24, and 27), allowing the compiler to infer its specific type from the type of the argument to which it is assigned at Line 7, in Version 3:



This solved the problem, but I'm still puzzled as to why there ever was one. I am guessing it's related to erasure, in that the Context instance's call to its Strategy instance's execute method doesn't know what data type the Strategy instance expects (in the first version), so data is just being passed as an object, which, somehow, ConcreteStrategy can handle as a single parameter, but not when it is given an array.

Can someone more familiar with generics and varargs help me understand why Version 1 works, but Version 2 doesn't?
 
Stephan van Hulst
Saloon Keeper
Posts: 7812
142
  • Likes 1
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
You're correct. Type erasure is the issue here in combination with the fact that arrays and generics don't mix well.

The problem is that Context is in charge of transforming data into something that can be passed to the execute() method. Since Context is generic, it will wrap data into an instance of Object[]. Why not an instance of D[]? Because you can't create generic arrays. Why not Dimension[]? Because Context doesn't know about Dimension at compile time because it's generic, and it doesn't know about Dimension at runtime because of type erasure.

Alright, fine... we're passing data to ConcreteStrategy.execute(), wrapped in an Object[]. Here's the rub: In version 2, ConcreteStrategy expects a Dimension[], while it's being passed an Object[].

The compiler could have rejected this program if you weren't allowed to declare generic varargs. Since you can, this program is perfectly valid. It just doesn't work.

Version 3 works because ConcreteStrategy expects an unbounded generic array, which at runtime translates to Object[]. Now you're passing an Object[] where an Object[] is expected: No ClassCastException!

You solve this problem elegantly by not mixing arrays and generics, and using convenience methods that translate varargs to collections.





One change I feel I should elaborate a bit more on:

Context is now just a holder for a strategy and data. You should not perform a lot of logic in the constructor of an object. Constructors should do the bare minimum to initialize the object so it has a consistent state. If you want to perform additional logic afterwards, put it in a separate method, in this case, Context.execute().

Whenever I use the new keyword, I expect to get an object that represents what I'm *about* to do. I don't expect it to actually perform that action yet.
 
Stevens Miller
Bartender
Posts: 1444
30
C++ Java Netbeans IDE Windows
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Stephan van Hulst wrote:In version 2, ConcreteStrategy expects a Dimension[], while it's being passed an Object[].


Yes! I get it. Erasure causes V2 to pass a reference to an incompatible array.

You solve this problem elegantly by not mixing arrays and generics, and using convenience methods that translate varargs to collections.


Collections! That is flippin' brilliant!

One change I feel I should elaborate a bit more on:

Context is now just a holder for a strategy and data. You should not perform a lot of logic in the constructor of an object. Constructors should do the bare minimum to initialize the object so it has a consistent state. If you want to perform additional logic afterwards, put it in a separate method, in this case, Context.execute().

Whenever I use the new keyword, I expect to get an object that represents what I'm *about* to do. I don't expect it to actually perform that action yet.


Guess I should elaborate back in my own defense : I agree completely. The presence of execute in my Contexts' constructors was strictly in the name of posting the smallest possible SSCCE. In actual practice, I am convinced that constructors should do as little as possible.
 
Stephan van Hulst
Saloon Keeper
Posts: 7812
142
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Glad it helped

Note that if you had compiled your code with -Xlint:all, it probably would have given you a warning about using generic varargs.

You can suppress this warning if you are certain your usage doesn't lead to situations like the one shown in Version 2 of your code:

We can guarantee this code is safe, because we don't care about the runtime type of the data array. It's not being passed to methods that require D to be of some specific type, we're only interested in the individual elements.

In general, iterating over generic varargs is safe, and passing the array to methods that are annotated with @SafeVarargs is also safe.
 
Don't get me started about those stupid light bulbs.
  • Post Reply Bookmark Topic Watch Topic
  • New Topic
Boost this thread!