简体   繁体   中英

Different return value types in implementation of generic methods

Today I stumbled upon some working Java code I wouldn't even have expected to compile. Reduced to its bare minimum, it looks like this:

import java.util.List;

interface A {
    <T> List<String> foo();
}

interface B {
    <T> List<Integer> foo();
}

class C implements A, B {
    @Override
    public List<?> foo()
    {
        return null;
    }
}

At first sight, the type parameter <T> of the foo methods in A and B look unnecessary since T is not used anywhere else. Anyway, I found out that this is playing a crucial role in allowing the conflicting return value types to coexist in the same implementation: if one or both of the <T> s are left out, the code doesn't compile. Here the non-working version:

import java.util.List;

interface A {
    List<String> foo();
}

interface B {
    List<Integer> foo();
}

class C implements A, B {
    @Override
    public List<?> foo()
    {
        return null;
    }
}

I don't need to fix the code snippets above as those are just examples I made up to explain my point. I'm only curious to understand why the compiler is behaving differently with them. Can someone explain what rules exactly are making the difference here?

While the first example does compile, it will give an unchecked conversion warning:

// Type safety: The return type List<?> for foo() from the type C needs
// unchecked conversion to conform to List<String>
public List<?> foo()
{
    return null;
}

What's happening here is that by declaring type parameters, A.foo() and B.foo() are generic methods . Then, the overriding C.foo() omits that type parameter. This is similar to using a raw type , essentially "opting out" of generic type checking for that method signature. That causes the compiler to use the inherited methods' erasures instead: List<String> foo() and List<Integer> foo() both become List foo() , which can therefore be implemented by C.foo() .

You can see that by keeping the type parameter in the C.foo() declaration, there will be the expected compiler error instead:

// The return type is incompatible with A.foo()
public <T> List<?> foo()
{
    return null;
}

Likewise, if either of the interface methods don't declare a type parameter, then omitting a type parameter from the override fails to "opt out" of generic type checking for that method, and the return type List<?> remains incompatible.

This behavior is covered in JLS §8.4.2 :

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.

Angelika Langer's generics FAQ expands on this behavior in her section Can a non-generic method override a generic one? :

Now, let us explore an example where non-generic subtype methods override generic supertype methods. Non-generic subtype methods are considered overriding versions of the generic supertype methods if the signatures' erasures are identical.

Example (of non-generic subtype methods overriding generic supertype methods):

 class Super { public <T> void set( T arg) { ... } public <T> T get() { ... } } class Sub extends Super { public void set( Object arg) { ... } // overrides public Object get() { ... } // overrides with unchecked warning } 

 warning: get() in Sub overrides <T>get() in Super; return type requires unchecked conversion found : Object required: T public Object get() { 

Here the subtype methods have signatures, namely set(Object) and get() , that are identical to the erasures of the supertype methods. These type-erased signatures are considered override-equivalent.

There is one blemish in the case of the get method: we receive an unchecked warning because the return types are not really compatible. The return type of the subtype method get is Object , the return type of the supertype method get is an unbounded type parameter. The subtype method's return type is neither identical to the supertype method's return type nor is it a subtype thereof; in both situations the compiler would happily accept the return types as compatible. Instead, the subtype method's return type Object is convertible to the supertype method's return type by means of an unchecked conversion. An unchecked warning indicates that a type check is necessary that neither the compiler nor the virtual machine can perform. In other words, the unchecked operation is not type-safe. In case of the convertible return types someone would have to make sure that the subtype method's return value is type-compatible to the supertype method's return type, but nobody except the programmer can ensure this.

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM