Why does javac
not abort with a type error on this code example
import java.util.List;
public class StaticMethodWithBoundedReturnType {
static class Foo {
}
static class Bar extends Foo {
}
static <F extends Foo> F getFoo(String string) {
…
}
public static void main(String[] args) {
// Compiles without error, even though List does not extend Foo.
List<Integer> list = getFoo("baz");
}
}
Obviously List
can never be a subtype of Foo
. And even if there would exists a subtype of List
which would somehow extend Foo
, then the assigning of list
at the call site of getFoo()
should be invalid. I'm aware of the existence of type erasure. But shouldn't javac
be able see that the type of list
does not satisfy the bounded type constraint extends Foo
and thus fail compilation with a type error?
Why is javac
not able to typecheck the call site of a static method with a bounded type parameter as return type?
It appears I could get type safety with the following slight modification:
import java.util.List;
public class StaticMethodWithBoundedReturnType {
static class Foo {
}
static class Bar extends Foo {
}
static <F extends Foo> F getFoo(String string, Class<F> clazz) {
…
}
public static void main(String[] args) {
// Does not compile \o/
List<Integer> list = getFoo("baz", List.class);
}
}
But this requires adding the Class
parameter to getFoo() which isn't used in the method's body at all. Is there a better way to achieve type safety?
To understand this, we need to understand what the following actually means:
static <F extends Foo> F getFoo(String string) {
return null;
}
That says that getFoo
returns a value of some type that must be inferred from the context in which the call is made. Furthermore, it makes the constraint, that the inferred type must be a subtype of Foo
.
Since null
is assignable to all possible reference types, it is suitable as a return value. In fact, it is the only possible that may be returned.
To illustrate, try the following variation:
import java.util.List;
public class StaticMethodWithBoundedReturnType {
static class Foo {
}
static class Bar extends Foo {
}
static <F extends Foo> F getFoo(String string) {
return new Bar();
}
public static void main(String[] args) {
// Compiles without error, even though List does not extend Foo.
List<Integer> list = getFoo("baz");
}
}
This gives a compilation error
StaticMethodWithBoundedReturnType.java:11: error: incompatible types:
Bar cannot be converted to F
return new Bar();
^
You may ask: Why isn't Bar compatible with F?.
Answer: because the F
stands for ( R & ? extends Foo )
where R
is the type that the result of getFoo
is assigned to. And, within getFoo
re cannot know what R
will be. (Indeed, it can be lots of different types!)
In short, the type signature
<F extends Foo> F getFoo(String string)
is problematic. However, consider this:
static <F extends Foo> F getFoo(Class<F> clazz) {
return class.newInstance();
}
That is legal, and will return a value that satisfies runtime type safety (locally). But you will then get the expected compilation error if you try to assign it to a List<Integer>
:
StaticMethodWithBoundedReturnType.java:16: error: incompatible types:
inference variable F has incompatible bounds
List<Integer> list = getFoo(Bar.class);
^
equality constraints: Bar
upper bounds: List<Integer>,Foo
where F is a type-variable:
F extends Foo declared in method <F>getFoo(Class<F>)
1 error
Returning to the example, consider the call:
List<Integer> list = getFoo("baz");
That is legal because the inferred type for the result is the intersection type (List<Integer> & ? extends Foo)
. Indeed, that intersection type is implementable; e.g. as
class Baz extends Bar implements List<Integer> { /* list methods */ }
(The fact that there is no implementation in this Baz
class in our program is immaterial. There could be one.)
So, we can compile the program. And when we execute it, list
will be assigned null
, which is not a runtime type violation.
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