Why does the following code compile? The method IElement.getX(String)
returns an instance of the type IElement
or of subclasses thereof. The code in the Main
class invokes the getX(String)
method. The compiler allows to store the return value to a variable of the type Integer
(which obviously is not in the hierarchy of IElement
).
public interface IElement extends CharSequence { <T extends IElement> T getX(String value); } public class Main { public void example(IElement element) { Integer x = element.getX("x"); } }
Shouldn't the return type still be an instance of IElement
- even after the type erasure?
The bytecode of the getX(String)
method is:
public abstract <T extends IElement> T getX(java.lang.String); flags: ACC_PUBLIC, ACC_ABSTRACT Signature: #7 // <T::LIElement;>(Ljava/lang/String;)TT;
Edit: Replaced String
consistently with Integer
.
Whenever you want to restrict the type parameter to subtypes of a particular class you can use the bounded type parameter. If you just specify a type (class) as bounded parameter, only sub types of that particular class are accepted by the current generic class. These are known as bounded-types in generics in Java.
Multiple parametersYou can also use more than one type parameter in generics in Java, you just need to pass specify another type parameter in the angle brackets separated by comma.
To declare a bounded type parameter, list the type parameter's name, followed by the extends keyword, followed by its upper bound, which in this example is Number . Note that, in this context, extends is used in a general sense to mean either "extends" (as in classes) or "implements" (as in interfaces).
A bound is a constraint on the type of a type parameter. Bounds use the extends keyword and some new syntax to limit the parameter types that may be applied to a generic type. In the case of a generic class, the bounds simply limit the type that may be supplied to instantiate it.
This is actually a legitimate type inference*.
We can reduce this to the following example (Ideone):
interface Foo { <F extends Foo> F bar(); public static void main(String[] args) { Foo foo = null; String baz = foo.bar(); } }
The compiler is allowed to infer a (nonsensical, really) intersection type String & Foo
because Foo
is an interface. For the example in the question, Integer & IElement
is inferred.
It's nonsensical because the conversion is impossible. We can't do such a cast ourselves:
// won't compile because Integer is final Integer x = (Integer & IElement) element;
Type inference basically works with:
At the end of the algorithm, each variable is resolved to an intersection type based on the bound set, and if they're valid, the invocation compiles.
The process begins in 8.1.3:
When inference begins, a bound set is typically generated from a list of type parameter declarations
P1, ..., Pp
and associated inference variablesα1, ..., αp
. Such a bound set is constructed as follows. For each l (1 ≤ l ≤ p):
[…]
Otherwise, for each type
T
delimited by&
in a TypeBound, the boundαl <: T[P1:=α1, ..., Pp:=αp]
appears in the set […].
So, this means first the compiler starts with a bound of F <: Foo
(which means F
is a subtype of Foo
).
Moving to 18.5.2, the return target type gets considered:
If the invocation is a poly expression, […] let
R
be the return type ofm
, letT
be the invocation's target type, and then:
[…]
Otherwise, the constraint formula
‹R θ → T›
is reduced and incorporated with [the bound set].
The constraint formula ‹R θ → T›
gets reduced to another bound of R θ <: T
, so we have F <: String
.
Later on these get resolved according to 18.4:
[…] a candidate instantiation
Ti
is defined for eachαi
:
- Otherwise, where
αi
has proper upper boundsU1, ..., Uk
,Ti = glb(U1, ..., Uk)
.The bounds
α1 = T1, ..., αn = Tn
are incorporated with the current bound set.
Recall that our set of bounds is F <: Foo, F <: String
. glb(String, Foo)
is defined as String & Foo
. This is apparently a legitimate type for glb, which only requires that:
It is a compile-time error if, for any two classes (not interfaces)
Vi
andVj
,Vi
is not a subclass ofVj
or vice versa.
Finally:
If resolution succeeds with instantiations
T1, ..., Tp
for inference variablesα1, ..., αp
, letθ'
be the substitution[P1:=T1, ..., Pp:=Tp]
. Then:
- If unchecked conversion was not necessary for the method to be applicable, then the invocation type of
m
is obtained by applyingθ'
to the type ofm
.
The method is therefore invoked with String & Foo
as the type of F
. We can of course assign this to a String
, thus impossibly converting a Foo
to a String
.
The fact that String
/Integer
are final classes is apparently not considered.
* Note: type erasure is/was completely unrelated to the issue.
Also, while this compiles on Java 7 as well, I think it's reasonable to say we needn't worry about the specification there. Java 7's type inference was essentially a less sophisticated version of Java 8's. It compiles for similar reasons.
As an addendum, while strange, this will likely never cause a problem that was not already present. It's rarely useful to write a generic method whose return type is solely inferred from the return target, because only null
can be returned from such a method without casting.
Suppose for example we have some map analog which stores subtypes of a particular interface:
interface FooImplMap { void put(String key, Foo value); <F extends Foo> F get(String key); } class Bar implements Foo {} class Biz implements Foo {}
It's already perfectly valid to make an error such as the following:
FooImplMap m = ...; m.put("b", new Bar()); Biz b = m.get("b"); // casting Bar to Biz
So the fact that we can also do Integer i = m.get("b");
is not a new possibility for error. If we were programming code like this, it was already potentially unsound to begin with.
Generally, a type parameter should only be solely inferred from the target type if there is no reason to bound it, e.g. Collections.emptyList()
and Optional.empty()
:
private static final Optional<?> EMPTY = new Optional<>(); public static<T> Optional<T> empty() { @SuppressWarnings("unchecked") Optional<T> t = (Optional<T>) EMPTY; return t; }
This is A-OK because Optional.empty()
can neither produce nor consume a T
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With