I'm currently prepping for my Java SE 11 Developer certificate and I can't seem to wrap my head around the concept of type erasure. I have the following classes:
public class BaseClass {
public List<? extends CharSequence> transform(Set<? extends CharSequence> set) {
return null;
}
}
public class SubClass extends BaseClass {
@Override
public List<String> transform(Set<String> set) {
return null;
}
}
As far as I understand, type erasure will turn the method signatures into the following:
public List<? extends CharSequence> transform(Set set) {
return null;
}
public List<String> transform(Set set) {
return null;
}
Which to me seems like a valid override. Yet, when I compile the program, I get the following error:
name clash: transform(java.util.Set<java.lang.String>) in covariant.car.SubClass and transform(java.util.Set<? extends java.lang.CharSequence>) in covariant.car.BaseClass have the same erasure, yet neither overrides the other
What am I missing here?
If your code compiled, then you could upcast an instance of SubClass
to BaseClass
and then pass some other kind of CharSequence
than String
to the transform
method.
This shows how it would break type safety:
class NotString implements CharSequence {
public char charAt(int index) {
return 'A';
}
public int length() {
return 1;
}
public CharSequence subSequence(int start, int end) {
return this;
}
public String toString() {
return "A";
}
}
BaseClass base = new SubClass();
// Oops, passing Set<NotString> to transform(Set<String> set).
base.transform(Set.of(new NotString()));
Type erasure makes sure that generic types do not exist at runtime but they exist at compile time.
This results in the following:
The methods would be equivalent at runtime
They are not the same at compile time and the conpiler detects that difference.
As the purpose of generics is to cause a compiler error (or unchecked warning) instead of getting behaviour you very likely do not want, it just gives you that informative error:
name clash: transform(java.util.Set<java.lang.String>) in covariant.car.SubClass and transform(java.util.Set<? extends java.lang.CharSequence>) in covariant.car.BaseClass have the same erasure, yet neither overrides the other
It shows you the signatures of both methods and tells you that the result would be the same after applying Type Erasure but the (generic) signature is different.
After all, the transform
method of the BaseClass
allows caller to pass a Set
of anything that is a CharSequence
while the transform
method of SubClass
only allows a Set
of String
s.
I believe it's the method parameter that causes the error. When you make a subclass, your method can be more specific in what it returns (in your case, String
is more specific than <? extends CharSequence>
. However, method parameters can only be more general.
For example, this snippet should work:
List<? extends CharSequence> out = baseClass.transform(
Collections.singleton(new StringBuilder().append("Hi")));
If baseClass
happens to be an instance of SubClass
it should still work, so the method can't assume the parameter is List<String>
Your
public List<? extends CharSequence> transform(Set<? extends CharSequence> set)
under the compiler hood is similar to
public <T1 extends CharSequence, T2 extends CharSequence> List<T1> transform(Set<T2> set)
except instead of T1
and T2
it has names like capture#1 of?
and capture#2 of?
.
As you can see, this is a generic method that has bounds on parameters supplied by the caller, but the method you introduced in the subclass isn't generic anymore -- it doesn't receive type parameters, it has concrete types!
Now compiler sees that you try to override method(signatures of methods in JVM know nothing about generics in types supplied to them, so it would be just (Ljava/util/Set;)Ljava/util/List;
in both cases), and will try to ensure, that you class still can be used in place of superclass, but it's not possible, because super class could be used as
BaseClass bc = getBaseClassFromSomewhere();
List<CharBuffer> result = bc.<CharBuffer, CharBuffer>transform(someSetOfCharBuffers);
And if your code ever tried to read String
s from the supplied Set
it would fail dramatically in runtime with ClassCastException
, and same would happen with consumer that tried to read CharBuffer
s from the list you returned.
Meanwhile, you could capture type invariants of this code, if you captured parameters of transform
method explicitly in the class definition, like
public static class BaseClass<T1 extends CharSequence, T2 extends CharSequence> {
public List<T1> transform(Set<T2> set) {
return null;
}
}
public static class SubClass extends BaseClass<String, String> {
@Override
public List<String> transform(Set<String> set) {
return null;
}
}
then it would be a valid overload, because methods aren't generic anymore, and upcast of SubClass
is to BaseClass<String, String>
, so instances of your class could be used only at places where BaseClass<String, String>
are required, but not, for example, BaseClass<CharBuffer String>
.
JVM method signature stays the same, but actual types are captured in class definition, therefore compiler is assured, that it won't cause problems with casting at runtime.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.