17

Can someone explain the following behavior to me?

I have a list of X and use the addAll() method to add elements. These elements are returned by a method using generic types. Method getA() returns < T extends A > with A being a class. Method getI() returns < T extends I > with I being an interface (see code below).

Difference: with listX.addAll(getA()) I get a compile error (as expected), but listX.addAll(getI()) compiles (throws a runtime error when element is cast to X).

Simpified code:

interface I {}
class A implements I {}

class X {}

public void test() {   
    List<X> listX = new ArrayList<>();
    listX.addAll(getA());

    listX.addAll(getI());
    for (X x : listX) {}
}
public <T extends A> List<T> getA() {
    return new ArrayList<>();
}
public <T extends I> List<T> getI() {
    return new ArrayList<>();
}

Am I missing something? Shouldn't I get a compile error both times?

That behavior seems to be new with Java 8, with versions below I have gotten compiler errors in both cases.

sepp2k
  • 363,768
  • 54
  • 674
  • 675
firecat
  • 171
  • 3
  • 2
    Where does `B` come in? And which compiler are you using? – shmosel Jan 16 '17 at 09:28
  • Yes, you must have multiple error-messages! – Grim Jan 16 '17 at 09:29
  • I used B to test. I used the eclipse compiler, but the problem shows with other java 8 compilers as well. – firecat Jan 16 '17 at 09:31
  • @PeterRader : surprisingly, just one. – rkosegi Jan 16 '17 at 09:32
  • @PeterRader : I only get the one error with getA(). – firecat Jan 16 '17 at 09:34
  • 1
    You're iterating over an empty list. How are you getting a runtime error? – shmosel Jan 16 '17 at 09:34
  • @shmosel I stripped the code of unnesessary details to show the problem... – firecat Jan 16 '17 at 09:38
  • 3
    But you haven't shown the runtime error. I suspect it involved ignoring a warning. – shmosel Jan 16 '17 at 09:39
  • @shmosel : It's a class cast exception because I try to cast an object that has no relation to class X. The problem is, using addAll() I can add objects to a list of X, that have no relation to X. – firecat Jan 16 '17 at 09:44
  • 3
    This has nothing to do with addAll. You could simplify the example to Collection extends X>z = getI(); I would remove "addAll" from the name of the question, and from the given example. – Gonen I Jan 16 '17 at 09:46
  • 3
    Just [tested with Java 7](http://ideone.com/k1krUE) and it seems to show the same behavior. – shmosel Jan 16 '17 at 09:49
  • 1
    @shmosel: IDEone doesn’t count. It always uses the Java 8 compiler, regardless of whether you select “Java 7” or not. Just try inserting a lambda expression when “Java 7” has been selected. – Holger Jan 16 '17 at 14:30
  • 2
    See also [here](http://stackoverflow.com/q/36402646/2711488), [here](http://stackoverflow.com/q/28466925/2711488) and [here](http://stackoverflow.com/q/30521974/2711488)… – Holger Jan 16 '17 at 14:55
  • @Holger Strange... why does the option even exist? – shmosel Jan 16 '17 at 17:02
  • 1
    @shmosel: Well, I’m not affiliated with IDEone, so I don’t know. Maybe, it’s intended to do what we would expect, but nobody ever reported to them that it doesn’t work… – Holger Jan 16 '17 at 17:11
  • 1
    @Holger I submitted a bug report; let's see if anything comes out of it. – shmosel Jan 16 '17 at 17:16

2 Answers2

8

I'd like to simplify the question and Shmosel's answer as follows:

interface I {}
class A implements I {}

class X {}

public void test() {   
    X temp = getI();  // compiles
    X temp2 = getA();  // does not compile
}

public <T extends I> T getI() {  
    return null;
}
public <T extends A> T getA() {  
    return null;
}

getI() can potentially return something that extends X and implements I, which is why it compiles. Normally, the type it actually returns would depend on something, for example an argument passed into the function.

getA() cannot return something that is an X, since it returns something that extends A, which does not extend X.

Gonen I
  • 5,576
  • 1
  • 29
  • 60
7

listX.addAll(getA()); doesn't compile because there's no possible subclass of X that's also a subclass of A.

listX.addAll(getI()); does compile because there could be a subclass of X that also implements I.

shmosel
  • 49,289
  • 6
  • 73
  • 138
  • 4
    A subclass of `X` that also implements `I` should be declared as `` not simply ``. – Nicolas Filotto Jan 16 '17 at 09:48
  • 1
    Makes sense to me. – Gonen I Jan 16 '17 at 09:48
  • 2
    @NicolasFilotto `getI()` is not declaring that `T` extends `X`. It's just promising to return a list of `T`, and letting the caller determine what type that is. It's the caller's responsibility to ensure its type argument is compatible with `I`, which in the case of `X`, it is. – shmosel Jan 16 '17 at 09:55
  • shmosel - I was trying to locate the Java 8 documentation of this specific enhancement. I could not locate neither in release notes nor in Collections Java 8 API. If you have this feature Documentation details as part of Java 8, Please update answer with link – Srikanth Balaji Jan 16 '17 at 09:57
  • 2
    @SrikanthA I would expect it to be in the JLS, if anything. But [as I mentioned](http://stackoverflow.com/questions/41673016/arraylist-using-addall-compiler-shows-different-behavior-with-generics/41673357#comment70544429_41673016), I couldn't reproduce the discrepancy between Java 7 and Java 8. – shmosel Jan 16 '17 at 09:58
  • The main purpose of generics is to prevent as much as possible class cast exception at runtime by checking the compatibility of the types at compile time, in this case the compiler is supposed to raise an error as it does in Java 7 since extending I is not good enough to prevent the exception. – Nicolas Filotto Jan 16 '17 at 10:02
  • 4
    @NicolasFilotto The onus here is on the `getI()` method to ensure it can safely return a `List`, regardless of what `T` may be. A more plausible scenario would be where there was an input parameter using type `T` that could be used to produce a return value. OP's case is unusual in that it's not possible to return a list containing anything but null items without a compile error or warning. Since he's returning an empty list, it is perfectly safe. – shmosel Jan 16 '17 at 10:10
  • 2
    @Nicolas Filotto: there is no error here, as the code is correct. It won’t throw at runtime—as long as the lists stay empty like in the question’s code. As soon, as you try to do something meaningful with it, warnings or even errors will appear. – Holger Jan 16 '17 at 14:37
  • 2
    @Srikanth A: the rules haven’t changed. What has changed, is the [Improved Type Inference](https://docs.oracle.com/javase/8/docs/technotes/guides/language/enhancements.html#javase8), which enables the compiler to infer the type for the nested invocation use case. – Holger Jan 16 '17 at 14:44