2

I found a strange inconsistence when implementing a Java interface that makes heavy use of generics and I can't find an explanation to why this happens.

I've stripped down the example as much as possible and I'm aware that the usage of generics makes not much sense in this example anymore.

public interface Service<T> {
    public List<T> thisCompiles();
    public List<T> andThisCompiles(List<Object> inputParam);
    public <S extends T> List<S> thisCompilesAswell();
    public <S extends T> List<S> evenThisCompiles(List inputParam);
    public <S extends T> List<S> butThisDoesnt(List<Object> inputParam);
}

public class ServiceImpl implements Service<Number> {

    @Override
    public List<Number> thisCompiles() {
        return null;
    }

    @Override
    public List<Number> andThisCompiles(List<Object> inputParam) {
        return null;
    }

    @Override
    public List<Number> thisCompilesAswell() {
        return null;
    }

    @Override
    public List<Number> evenThisCompiles(List inputParam) {
        return null;
    }

    @Override
    public List<Number> butThisDoesnt(List<Object> inputParam) {
        return null;
    }
}

Please note that in the implementation, all return types are List<Number>, although the interface is more generous.

When compiling this snippet (I tried Oracle JDK 8 u144 and OpenJDK 8 u121), I get the following error messages:

  • The method butThisDoesnt(List<Object>) of type ServiceImpl must override or implement a supertype method
  • The type ServiceImpl must implement the inherited abstract method Service<Number>.butThisDoesnt(List<Object>)
  • Name clash: The method butThisDoesnt(List<Object>) of type ServiceImpl has the same erasure as butThisDoesnt(List<Object>) of type Service<T> but does not override it

It seems that compilation fails as soon as the parameter that I pass to the function has some generic parameter itself.

Implementing the 5th function as

    @Override
    public <S extends Number> List<S> butThisDoesnt(List<Object> inputParam) {
        return null;
    }

works as expected.

Is there an explanation for this sort of behaviour or did I stumble upon a bug (although it happens in OpenJDK aswell)? Why is the 5th function behaving differently?

I am not interested in finding out how I can make this code compile (I fixed it for my codebase). I just stumbled upon this problem and am curious about the logical explanation why exactly the compilers accept the first four methods but reject the fifth.

sina
  • 1,817
  • 1
  • 18
  • 42
  • 1
    ` List` is different than `List` so the first version doesn't override the abstract method. So it is clear you haven't implemented all of the methods. It is also clear you haven't actually overridden anything, so your `@Override` is borked. Finally you have a name clash because `method(List a)` is the same signature as `method(List b)` as explained in the answer below. – matt Jul 27 '17 at 14:39
  • @matt Check methods 3 and 4 (`thisCompilesAswell` and `evenThisCompiles`). They also return `List` while being defined as ` List` in the interface, but they compile. The name clash is only a result of the `@Override` not working. – sina Jul 27 '17 at 14:44
  • Remove the @Override then, I think you still get the name clash. I'm really surprised the other methods compile. Type erasure says that they have the same signature. – matt Jul 27 '17 at 14:47
  • @matt same signature with different names? – user85421 Jul 27 '17 at 14:49
  • @CarlosHeuberger what? He says the name clash is because of the Override not working. It isn't, it is because he has a method `butThisDoesnt(List inputParam)` which is colliding the same method from the interface. Not the actual `@` annotation. – matt Jul 27 '17 at 14:53
  • Yeah, but the name clash is only because the method does not compile - if it would work the same as it does for methods 3 and 4, the name clash would go away aswell. – sina Jul 27 '17 at 14:58
  • @sina I am more surprised that the other ones compile. With the 'extends' there isn't a problem, but you could write signature, `public List thisCompilesAswell();` and it still compiles, but you can subvert type safety. – matt Jul 27 '17 at 15:03
  • I know how I can make it compile (see my edit). I want to know why the methods behave differently. – sina Jul 27 '17 at 15:03
  • I am not tell you how to make it compile, I am tell you your example can be less strict, then it is easy to see type errors will occur. Like here, http://ideone.com/1oexvR – matt Jul 27 '17 at 15:09
  • I'm suprised that the compile errors are different for the following two snippets: `public List butThisDoesnt( List inputParam) {return null;}` says: attempting to use incompatible return type. Where as this says: `public List butThisDoesnt( List inputParam) {return null;}` methods have same erasure – Lino Jul 27 '17 at 15:10
  • Does javac give you a warning for `evenThisCompiles` ? – matt Jul 27 '17 at 15:27
  • Yes, `return type requires unchecked conversion from List to List` (if compiled with `Xlint:unchecked`). Still, I think you miss the point of my question - I want to know, why the methods behave differently when it comes to compiling. This example is not about possible runtime exceptions, about how much sense the generics make or if there would be a better way to program the interface. – sina Jul 27 '17 at 15:32
  • @sina I get the point of your question. It should be pretty clear by what I am posting. Yes the behavior changes if you pass an object with a generic parameter as an argument. Which one is correct then. You could probably get away with filing a bug. You could definitely reduce this down to a two method one error example though. – matt Jul 27 '17 at 16:36
  • Okay, I have updated my answer. Essentially, the first way is allowed with just a warning because it is for transitioning from non-generic code. The method with an error cannot be for transitioning from non-generic code, because it takes a generic in the argument list. Hence why a raw list works, but a parameterized one does not. – matt Jul 27 '17 at 17:23

2 Answers2

2

I have included a simplified version of the issue. In the following example, the program will not compile because of the method broken.

import java.util.List;
import java.util.ArrayList;

public class Main{
    static interface Testing{

        <S> List<S> getAList();
        <S> List<S> broken(List<String> check);
    }

    static class Junk implements Testing{
        public List<Number> getAList(){
            return new ArrayList<>();
        }
        public List<Number> broken(List<String>check){
            return new ArrayList<>();
        }
    }

    public static void main (String[] args) throws java.lang.Exception
    {
        // your code goes here
    }
}

There are two errors and one warning.

  • Main.java:11: error: Junk is not abstract and does not override
    abstract method broken(List) in Testing static class Junk implements Testing{ ^ where S is a type-variable: S extends Object declared in method broken(List)

  • Main.java:12: warning: [unchecked] getAList() in Junk implements getAList() in Testing public List getAList(){ ^ return type requires unchecked conversion from List to List where S is a type-variable: S extends Object declared in method getAList()

  • Main.java:15: error: name clash: broken(List) in Junk and broken(List) in Testing have the same erasure, yet neither overrides the other public List broken(Listcheck){ ^ where S is a type-variable: S extends Object declared in method broken(List)

2 errors 1 warning

Why does the first method infer a cast and only give a warning?

"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."

https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.4.5

The other method has a Generic in the argument list, therefor it is not a migration from previously non-generic code, and it is unsound and should be considered an error.

matt
  • 10,892
  • 3
  • 22
  • 34
1

The Problem with your fifth method is that one might think that following snippet works:

List<Integer> ints = new ArrayList<>(); // this works as expected
List<Number> number = new ArrayList<>(); // this works too
number = ints; // this doesn't work

That is, because with generics there exists no such thing as hierarchy relation and so List<Number> is in no sense related to List<Integer>.

This is due to type erasure, generics are only used during compilation. At runtime you only have List-objects (type erasure = no type available anymore at runtime).

In the interface the method is defined as <S extends T, V> List<S> butThisDoesnt(List<V> inputParam); where S can be any subtype of Number. If we now apply the above mentioned to your implementation, then it should make sense.

public List<Number> butThisDoesnt(List<Object> inputParam) {
    return null;
}

List<Number> clashes with the definition in the interface, in that way, because List<Number> can contain any Number but can not be a List<Integer>.

Read more to type erasure in this other question. And on this page.

Note please correct me if i'm wrong or if I did miss something.

Community
  • 1
  • 1
Lino
  • 19,604
  • 6
  • 47
  • 65
  • 1
    Why should being able to distinguish between `List` and `List` at runtime help in this specific case (or why is not being able to causing a problem)? – sina Jul 27 '17 at 14:35
  • Did you have a look at the functions `thisCompilesAswell` and `evenThisCompiles`? They are defined the same way (just without using generics for the inputParam), but they compile. – sina Jul 27 '17 at 14:57
  • @sina I have to admit i can't give you a correct answer... i'm baffled too, guess i'll remove the answer again – Lino Jul 27 '17 at 15:07