I have heard many times that the design of Generic types in Java is unsuccessful. For the most part, the claims are reduced to the lack of support for primitive types (which they plan to add) to erasing types, and more specifically, the inability to get the actual type of the parameter in runtime. Personally, I do not consider erasing types a problem, as well as the design of Generics is bad. But there are moments that annoy me with order, but are not so often mentioned.
For example, we know that the Class#getAnnotation
parameterized and has the following signature: public <A extends Annotation> A getAnnotation(Class<A> annotationClass)
. So you can write this code:
Deprecated d = Object.class.getAnnotation(Deprecated.class);
Here I decide to put Object.class
into a separate variable and the code stops compiling:
Class clazz = Object.class; // incompatible types: // java.lang.annotation.Annotation cannot be converted to java.lang.Deprecated Deprecated d = clazz.getAnnotation(Deprecated.class);
Where did I go wrong?
I was mistaken in not parameterizing the clazz
variable clazz
. It turns out that erasing a Class
type also erases types in all its methods! Why did it do so - I have no idea. We make a minimal correction to the code and everything works as it should.
Class<Object> clazz = Object.class; Deprecated d = clazz.getAnnotation(Deprecated.class);
The second example is a bit contrived, but indicative. Imagine you have such a simple class:
class Ref<T> { private T value = null; public T getValue() { return value; } public void setValue(T value) { this.value = value; } }
Having a ref
variable I decide to write such code. What can happen to him bad?
ref.setValue(ref.getValue());
It would be reasonable to assume that it will always compile, but this is not so! You just have to declare the ref
variable with Ref<?>
type Ref<?>
And you get an error incompatible types: java.lang.Object cannot be converted to capture#1 of ?
That is, the wildcard in the getter is automatically expanded in Object
. We cannot rely on the fact that in our expressions it will be equal to itself. Shoot yourself up this, albeit difficult, but definitely possible.
Further, let there be such a very simple class hierarchy:
class HasList<T extends List> { protected final T list; public HasList(T list) { this.list = list; } public T getList() { return list; } } class HasArrayList<T extends ArrayList> extends HasList<T> { public HasArrayList(T list) { super(list); } }
We write the following code:
HasArrayList<?> h = new HasArrayList<>(new ArrayList<>()); ArrayList list = h.getList();
The T
parameter of the HasArrayList
class has an upper limit equal to ArrayList
, which means that when erasing types, the code still needs to be compiled.
HasArrayList h = new HasArrayList<>(new ArrayList<>()); // incompatible types: java.util.List cannot be converted to java.util.ArrayList ArrayList list = h.getList();
Well, again, does not work. Now what is wrong?
Not so that in the getList
method signature, the return type is List
, and the compiler is simply too lazy to arrange explicit type conversions. Everything is fixed very simply - it is necessary to redefine this method in a subclass.
class HasArrayList<T extends ArrayList> extends HasList<T> { public HasArrayList(T list) { super(list); } @Override public T getList() { return super.getList(); } }
In this case, the compiler will generate a synthetic bridge method that returns an ArrayList
, and that is what will be called. Obviously ...
In general, if a class has a type-parameter, then it is better not to ignore it in the code, as a last resort you can specify <?>
. Otherwise, the compiler will do this nonsense, and then we will be nervous.
The last situation is the most tricky. I decided to write a similar class:
class MyArrayList<T extends Cloneable & BooleanSupplier> extends ArrayList<T> { public void removeTrueElements() { this.removeIf(BooleanSupplier::getAsBoolean); } }
Do you think there will be any problems when calling this method?
new MyArrayList<>().removeTrueElements();
It may seem ridiculous, but this code will throw an exception at startup:
Exception in thread "main" java.lang.BootstrapMethodError: call site initialization exception ... Caused by: java.lang.invoke.LambdaConversionException: Invalid receiver type interface java.lang.Cloneable; not a subtype of implementation type interface java.util.function.BooleanSupplier ...
No, I deceived you. This code will work if you compile it from JDK9, but the JDK8 compiler makes a mistake on it.
Yes, everything is just like that, this is not a runtime error, but a compiler error. Why you can not warn when compiling that the generation of this lambda will fall, I do not understand.
How to fix code without switching to Java 9? Like this:
public void removeTrueElements() { this.removeIf(t -> t.getAsBoolean()); }
Well, commenting, of course, so that your colleague does not convert everything back to method reference.
I am sure many of you have also encountered situations where the behavior of generics caused bewilderment. Share in comments, discuss.
Source: https://habr.com/ru/post/329550/
All Articles