Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is javac not able to typecheck the call site of a static method with a bounded type parameter as return type? [duplicate]

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?

like image 355
Flow Avatar asked Jul 22 '18 06:07

Flow


1 Answers

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.

like image 150
2 revs Avatar answered Oct 25 '22 13:10

2 revs