The context
I'm working on a project that is heavily dependent on generic types. One of its key components is the so-called TypeToken
, which provides a way of representing generic types at runtime and applying some utility functions on them. To avoid Java's Type Erasure, I'm using the curly brackets notation ({}
) to create an automatically generated subclass since this makes the type reifiable.
What TypeToken
basically does
This is a strongly simplified version of TypeToken
which is way more lenient than the original implementation. However, I'm using this approach so I can make sure that the real problem doesn't lie in one of those utility functions.
public class TypeToken<T> {
private final Type type;
private final Class<T> rawType;
private final int hashCode;
/* ==== Constructor ==== */
@SuppressWarnings("unchecked")
protected TypeToken() {
ParameterizedType paramType = (ParameterizedType) this.getClass().getGenericSuperclass();
this.type = paramType.getActualTypeArguments()[0];
// ...
}
When it works
Basically, this implementation works perfectly in almost every situation. It has no problem with handling most types. The following examples work perfectly:
TypeToken<List<String>> token = new TypeToken<List<String>>() {};
TypeToken<List<? extends CharSequence>> token = new TypeToken<List<? extends CharSequence>>() {};
As it doesn't check the types, the implementation above allows every type that the compiler permits, including TypeVariables.
<T> void test() {
TypeToken<T[]> token = new TypeToken<T[]>() {};
}
In this case, type
is a GenericArrayType
holding a TypeVariable
as its component type. This is perfectly fine.
The weird situation when using lambdas
However, when you initialize a TypeToken
inside a lambda expression, things start to change. (The type variable comes from the test
function above)
Supplier<TypeToken<T[]>> sup = () -> new TypeToken<T[]>() {};
In this case, type
is still a GenericArrayType
, but it holds null
as its component type.
But if you're creating an anonymous inner class, things start to change again:
Supplier<TypeToken<T[]>> sup = new Supplier<TypeToken<T[]>>() {
@Override
public TypeToken<T[]> get() {
return new TypeToken<T[]>() {};
}
};
In this case, the component type again holds the correct value (TypeVariable)
The resulting questions
- What happens to the TypeVariable in the lambda-example? Why does the type inference not respect the generic type?
- What is the difference between the explicitly-declared and the implicitly-declared example? Is type inference the only difference?
- How can I fix this without using the boilerplate explicit declaration? This becomes especially important in unit testing since I want to check whether the constructor throws exceptions or not.
To clarify it a bit: This is not a problem that's "relevant" for the program since I do NOT allow non-resolvable types at all, but it's still an interesting phenomenon I'd like to understand.
My research
Update 1
Meanwhile, I've done some research on this topic. In the Java Language Specification §15.12.2.2 I've found an expression that might have something to do with it - "pertinent to applicability", mentioning "implicitly typed lambda expression" as an exception. Obviously, it's the incorrect chapter, but the expression is used in other places, including the chapter about type inference.
But to be honest: I haven't really figured out yet what all of those operators like :=
or Fi0
mean what makes it really hard to understand it in detail. I'd be glad if someone could clarify this a bit and if this might be the explanation of the weird behavior.
Update 2
I've thought of that approach again and came to the conclusion, that even if the compiler would remove the type since it's not "pertinent to applicability", it doesn't justify to set the component type to null
instead of the most generous type, Object. I cannot think of a single reason why the language designers decided to do so.
Update 3
I've just retested the same code with the latest version of Java (I used 8u191
before). To my regret, this hasn't changed anything, although Java's type inference has been improved...
Update 4
I've requested an entry in the offical Java Bug Database/Tracker a few days ago and it just got accepted. Since the developers who reviewed my report assigned the priority P4 to the bug, it might take a while until it'll be fixed. You can find the report here.
A huge shoutout to Tom Hawtin - tackline for mentioning that this might be an essential bug in the Java SE itself. However, a report by Mike Strobel would probably be way more detailed than mine due to his impressive background knowledge. However, when I wrote the report, Strobel's answer wasn't yet available.