简体   繁体   中英

Proper way of overriding generic methods in Java

Consider the following code:

import java.util.Arrays;
import java.util.List;

class Person {}
class OtherPerson {}
class SomeDummyClass {}

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

class B implements A {
    @Override
    public List<Object> foo() {
        return Arrays.asList(Integer.valueOf(3), new Person(), new OtherPerson());
    }
}

public class Main {
    public static void main(String[] args) {
        A a = new B();
        List<SomeDummyClass> someDummyClasses = a.foo();

        System.out.println(someDummyClasses.get(0)); // prints 3
    }
}

I'm confused why we can override the foo() method without any compile time errors. This particularly became less intuitive to reason about when I wrote List<SomeDummyClass> someDummyClasses = a.foo(); and this is perfectly valid (no compile time error).

A real time example of this case is with mybatis-3 .

Questions

  • I understand that why is this statement: List<SomeDummyClass> someDummyClasses = a.foo(); not giving any compile time error. But at runtime, I thought it should have given a ClassCastException . What's actuall happening here?
  • When I do System.out.println(someDummyClasses.get(0)); I get a ClassCastException . Why is this so?
  • Is there a better way to re-write these so that the code becomes less fragile?

I'm confused why we can override the foo() method with any compile time errros.

Yes but you have a compilation warning since the return type in the overrided method List<Object> is more specific that List<E> in the interface method :

Unchecked overriding: return type requires unchecked conversion. Found java.util.List<java.lang.Object> , required java.util.List<E> .

To not handle this kind of warning may lead to an inconsistent use of the generics. And there you have a very good example.

About :

This particularly became less intuitive to reason about when I wrote List someDummyClasses = a.foo(); and this is perfectly valid (no compile time error).

And why would you get a compilation error ?

Here you declare a as a A :

A a = new B();  

So here :

List<SomeDummyClass> someDummyClasses = a.foo();

at compile time, foo() refers to the <E> List<E> foo() method declared in the interface and a method scoped generic with E that is not bounded (so is Object ) is designed to adapt the declared return type to the target : here that is the type of the variable declared by the client of the method to assign the method return.

Change your declaration to :

B b = new B();  
List<SomeDummyClass> someDummyClasses = b.foo();

And the compilation would not even pass.

Note that your issue is not specific to overriding method. Declare a method (that also produces a warning about unchecked cast) :

public static <T> List<T> getListOfAny(){
    return (List<T>) new ArrayList<Integer>();
}

And you could use it in the same inconsistent way :

 List<String> listOfAny = getListOfAny();

The moral of this story : don't consider unchecked conversions warnings produced by the compiler like cosmetics/details but handle them correctly and you could benefit as much as possible from the compilation check brought by generics.

But at runtime, I thought it should have given a ClassCastException. What's actually happening here?

Remember that generics in Java are purely a compile-time thing. When your code is compiled, the compiler removes all the generic parameters, and adds casts where necessary. Your code at runtime looks like this:

interface A {
    List foo();
}

class B implements A {
    @Override
    public List foo() {
        return Arrays.asList(Integer.valueOf(3), new Person(), new OtherPerson());
    }
}

// ...

List someDummyClasses = a.foo();

System.out.println(someDummyClasses.get(0));

Look! Nothing is wrong here. There are no casts, let alone ClassCastException s.

There would be an ClassCastException if you did this:

SomeDummyClass obj = someDummyClasses.get(0);

Because the above code will become this at runtime:

SomeDummyClass obj = (SomeDummyClass)someDummyClasses.get(0);

When I do System.out.println(someDummyClasses.get(0)); I get a ClassCastException . Why is this so?

No it does not .

Is there a better way to re-write these so that the code becomes less fragile?

(Note that I don't know much about mybatis)

I think what you should do here is to make A a generic interface:

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

And make B implements A<Object> . This way, at least it's more type-safe.

1st) The signature of A#foo is return a <E> List<E> that will return a list of the type being captured as E on asssignment, List<X> = new A().foo() (if A was a concrete class) compiles since it captures the type, while List<X> = new B().foo() return a List<Object> and cannot be assigned.

2nd) You get a ClassCastException since you assigned Integers, and Strings to a List

3rd) Your interface could have like <E extends SummyDummyClass> List<E> foo(); turning the declaration on B class invalid and so on, also you will need less casts since you know in advance the type of the superclass of E

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