Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does this program involving a generic interface method compile?

The following code throws a ClassCastException at runtime, and the line public String foo() { return "bar"; } generates a warning "found 'java.lang.String', required 'T'". I understand the ClassCastException (the interface method is called with T equal to Integer yet foo returns a String) and I understand the warning (it's trying to warn us about exactly this problem). But what I don't understand is why the program compiles at all. Why is a method returning a String allowed to override a method returning a T?

public class Main {

    interface MyInterface {

        <T> T foo();
    }

    static class MyClass implements MyInterface {

        @Override
        public String foo() { return "bar"; }
    }

    public static void main(String[] args) {
        int a = ((MyInterface) new MyClass()).<Integer>foo();
    }
}
like image 304
Paul Boddington Avatar asked Feb 11 '23 06:02

Paul Boddington


2 Answers

When naively declaring <T> T foo(); the compiler will try to infer the result type of foo from the variable where it will be assigned. This is the reason why this compiles. It can be easily tested:

interface MyInterface {
    <T> T foo();
}

class MyClass implements MyInterface {
    @Override
    public String foo() { return "bar"; }
}

public class Main {
    public static void main(String[] args) {
        MyInterface myInterface = new MyClass();
        //the result of foo will be String
        String bar = myInterface.foo();
        System.out.println(bar); //prints "bar"
        try {
            //the result of foo at compile time will be Integer
            Integer fail = myInterface.foo();
            System.out.println(fail); //won't be executed
        } catch (ClassCastException e) {
            //for test purposes only. Exceptions should be managed better
            System.out.println(e.getMessage()); //prints "java.lang.String cannot be cast to java.lang.Integer"
        }
    }
}

And the result at compile time cannot be Object. If it were object, then you would have to add a manual type cast, which is not the case.

In short, declaring a method like that is useless and can only bring confusion and chaos to the programmers.

This method declaration becomes useful in one of these cases:

  • When declaring the generic <T> at top level of the interface/class:

    interface MyInterface<T> {
        T foo();
    }
    
    class MyClass implements MyInterface<String> {
        @Override
        //can only return String here. Compiler can check this
        public String foo() { return "bar"; }
    }
    
  • When passing Class<T> as argument, which enables the compiler to infer the result type and raise a proper compiler error when this condition is not met:

    interface MyInterface {
        <T> T foo(Class<T> clazz);
    }
    
    class MyClass implements MyInterface {
        @Override
        public <T> T foo(Class<T> clazz) {
            try {
                return clazz.newInstance();
            } catch (InstantiationException e) {
                e.printStackTrace(System.out);
            } catch (IllegalAccessException e) {
                e.printStackTrace(System.out);
            }
            return null;
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            MyInterface myInterface = new MyClass();
            //uncomment line below to see the compiler error
            //Integer bar = myInterface.foo(String.class);
    
            //line below compiles and runs with no problem
            String bar = myInterface.foo(String.class);
            System.out.println(bar);
        }
    }
    
like image 166
Luiggi Mendoza Avatar answered Feb 13 '23 20:02

Luiggi Mendoza


This is a surprisingly deep question. The Java Language Specification writes:

An instance method mC declared in or inherited by class C, overrides from C another method mA declared in class A, iff all of the following are true:

  • The signature of mC is a subsignature (§8.4.2) of the signature of mA.
  • ...

and:

Two methods or constructors, M and N, have the same signature if they have the same name, the same type parameters (if any) (§8.4.4), and, after adapting the formal parameter types of N to the the type parameters of M, the same formal parameter types.

This is clearly not true in our case, as MyInterface.foo declares a type parameter, but MyClass.foo does not.

The signature of a method m1 is a subsignature of the signature of a method m2 if either:

  • m2 has the same signature as m1, or
  • the signature of m1 is the same as the erasure (§4.6) of the signature of m2.

The spec explains the need for that second condition as follows:

The notion of subsignature is designed to express a relationship between two methods whose signatures are not identical, but in which one may override the other. Specifically, it allows a method whose signature does not use generic types to override any generified version of that method. This is important so that library designers may freely generify methods independently of clients that define subclasses or subinterfaces of the library.

and indeed, that second condition is met in our case, as MyClass.foo has the signature foo(), which also is the erasure of the signature of MyInterface.foo.

That leaves the matter of the different return type. The spec writes:

If a method declaration d1 with return type R1 overrides or hides the declaration of another method d2 with return type R2, then d1 must be return-type-substitutable (§8.4.5) for d2, or a compile-time error occurs.

and:

A method declaration d1 with return type R1 is return-type-substitutable for another method d2 with return type R2 iff any of the following is true:

  • ...

  • If R1 is a reference type then one of the following is true:

    • R1, adapted to the type parameters of d2 (§8.4.4), is a subtype of R2.

    • R1 can be converted to a subtype of R2 by unchecked conversion (§5.1.9).

    • d1 does not have the same signature as d2 (§8.4.2), and R1 = |R2|.

In our case, R1 = String and R2 = T. The first condition is therefore false, as String is not a subtype of T. However, String may be converted to T by unchecked conversion, making the second condition true.

The spec explains the need for the second and third conditions as follows:

An unchecked conversion is allowed in the definition, despite being unsound, as a special allowance to allow smooth migration from non-generic to generic code. If an unchecked conversion is used to determine that R1 is return-type-substitutable for R2, then R1 is necessarily not a subtype of R2 and the rules for overriding (§8.4.8.3, §9.4.1) will require a compile-time unchecked warning.

That is, your code is accepted by the compiler because you inadvertently use two compiler features introduced to ease transition to generics by permitting a step by step generification of existing code. These features open a loop hole in the compile time type system, which may cause heap pollution and weird ClassCastExceptions in lines that may not even feature a cast in the source code. To alert you to this danger, the compiler is required to emit an unchecked warning. These features should therefore only be used for their intended purpose (compatiblity with non-generic legacy code), and otherwise avoided.

like image 40
meriton Avatar answered Feb 13 '23 20:02

meriton