21

The following program compiles in Java 7 and in Eclipse Mars RC2 for Java 8:

import java.util.List;

public class Test {

    static final void a(Class<? extends List<?>> type) {
        b(newList(type));
    }

    static final <T> List<T> b(List<T> list) {
        return list;
    }

    static final <L extends List<?>> L newList(Class<L> type) {
        try {
            return type.newInstance();
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Using the javac 1.8.0_45 compiler, the following compilation error is reported:

Test.java:6: error: method b in class Test cannot be applied to given types;
        b(newList(type));
        ^
  required: List<T>
  found: CAP#1
  reason: inference variable L has incompatible bounds
    equality constraints: CAP#2
    upper bounds: List<CAP#3>,List<?>
  where T,L are type-variables:
    T extends Object declared in method <T>b(List<T>)
    L extends List<?> declared in method <L>newList(Class<L>)
  where CAP#1,CAP#2,CAP#3 are fresh type-variables:
    CAP#1 extends List<?> from capture of ? extends List<?>
    CAP#2 extends List<?> from capture of ? extends List<?>
    CAP#3 extends Object from capture of ?

A workaround is to locally assign a variable:

import java.util.List;

public class Test {

    static final void a(Class<? extends List<?>> type) {

        // Workaround here
        List<?> variable = newList(type);
        b(variable);
    }

    static final <T> List<T> b(List<T> list) {
        return list;
    }

    static final <L extends List<?>> L newList(Class<L> type) {
        try {
            return type.newInstance();
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

I know that type inference has changed a lot in Java 8 (e.g. due to JEP 101 "generalized target-type inference"). So, is this a bug or a new language "feature"?

EDIT: I have also reported this to Oracle as JI-9021550, but just in case this is a "feature" in Java 8, I've reported the issue also to Eclipse:

Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509

3 Answers3

7

Disclaimer - I don't know enough about the subject, and the following is an informal reasoning of mine to try to justify javac's behavior.


We can reduce the problem to

<X extends List<?>> void a(Class<X> type) throws Exception
{
    X instance = type.newInstance();
    b(instance);  // error
}

<T> List<T> b(List<T> list) { ... }

To infer T, we have constraints

      X <: List<?>
      X <: List<T>

Essentially, this is unsolvable. For example, no T exists if X=List<?>.

Not sure how Java7 infers this case. But javac8 (and IntelliJ) behaves "reasonably", I'd say.


Now, how come this workaround works?

    List<?> instance = type.newInstance();
    b(instance);  // ok!

It works due to wildcard capture, which introduces more type info, "narrowing" the type of instance

    instance is List<?>  =>  exist W, where instance is List<W>  =>  T=W

Unfortunately, this is not done when instance is X, thus there is less type info to work with.

Conceivably, the language could be "improved" to do wildcard capture for X too:

    instance is X, X is List<?>  =>  exist W, where instance is List<W>
ZhongYu
  • 19,446
  • 5
  • 33
  • 61
  • Interesting... In fact, Eclipse also rejects your example, so perhaps this reduction is not exactly equivalent? Your `void a()` method is generic, for instance. Why is this unsolvable? If `X=List=CAP#1 extends String>` then `T=CAP#1` would be a valid `T`, no? What am I missing? Why does assigning a local variable work as a workaround? In your case, I could also assign `List> instance = type.newInstance()` and it would work again. – Lukas Eder Jun 03 '15 at 15:00
  • 1
    there's no wildcard capture during inferrence. `X<:List>` does not imply there exists `W where X<:List`; actually such `W` must not exist. – ZhongYu Jun 03 '15 at 15:10
  • 1
    the type of object `type` must undergo wildcard capture (becoming `Class`) before inference. I remember Eclipse might be a little different on that. – ZhongYu Jun 03 '15 at 15:14
  • 3
    The problem simply is the compiler going mad as soon as a wildcard appears. Just replace your `>` with `>` and the problem entirely disappears. There is no semantic difference between the `Y` and the `?` here, both denote an entirely unknown type which adds no information to the scenario, especially as we don’t use `Y` anywhere else. Still, using `Y` instead of `?` makes the compiler happy. – Holger Jun 03 '15 at 15:18
  • 1
    @Holger - It's true `List>` cannot be a super interface, it must be some concrete `List`. However this information is not utilized by other parts of the type system. Your solution explicitly expresses this info to compiler. – ZhongYu Jun 03 '15 at 15:23
  • @Holger: Oh yes, good point. I remember having run into this before: http://stackoverflow.com/a/11500038/521799. The problem with adding `` is simply the fact that it needlessly changes a possibly public API, which has implications on the call-site, should the call-site decide to bind generic types explicitly... – Lukas Eder Jun 03 '15 at 15:26
  • 1
    @LukasEder - `Y` is added to achieve the effect of wildcard capture; since we can do that too inside method body, as the workaround shows, `Y` is probably not necessary on the interface. – ZhongYu Jun 03 '15 at 15:31
  • @bayou.io: We can do it in the method body *in this case*, although I think that the mechanism is a bit different... – Lukas Eder Jun 03 '15 at 15:32
  • 1
    @LukasEder - if `Y` serves no purpose other than capturing one wildcard in another type parameter, it's likely unnecessary. – ZhongYu Jun 03 '15 at 15:35
  • The weird thing is the error we get for your example: `argument mismatch; X cannot be converted to List`. Inference seems to do the capture conversion on `X` but for some reason chokes trying to pass `instance` as that type. I'm beginning to think it's not intentional. – Radiodef Jun 03 '15 at 15:36
  • 2
    Assigning an instance of `X extends List>` to a variable of type `List>` is not narrowing the type, it’s *widening* the type. And I don’t see how this “introduces more type info”… – Holger Jun 03 '15 at 16:54
  • 1
    @Holger - I meant `List> => List` is narrowing. That is not done for `X`. – ZhongYu Jun 03 '15 at 17:15
  • The answer looks mostly reasonable, but unfortunately the premise "We can reduce the problem to ..." is wrong. Java 8 type inference is (to some degree) able to combine inner inference and outer inference. Assigning intermediate results to a variable changes the game. This is witnessed by the fact that Eclipse reports an error against the reduced variant but accepts the original variant. IOW: the reduced variant is not indicative of the correct solution for the original. – Stephan Herrmann Jun 03 '15 at 21:37
  • 1
    @StephanHerrmann - Your objection is correct on general principle - a better, more precise analysis should be based on the complete procedure of type inference. Indeed, in this case, the inner method invocation `newList(type)` is a poly expression, subject to outer context. However, there's an obvious equality `L=X` here, so I think it can be reduced, and we can analyze `b(X)` on its own merit. Even if that is not exactly how inference procedure goes, it at least shows informally why the constraints are unsatisfiable. – ZhongYu Jun 03 '15 at 22:23
  • The only thing obvious to me is: there is more to this case than meets the eye. FYI, see the linked eclipse bugzilla for the type bounds collected by ecj during inference. Note that all equality bounds involve some captures. – Stephan Herrmann Jun 03 '15 at 23:29
  • 1
    @StephanHerrmann - Thanks. I don't know enough so I shouldn't call it an eclipse bug; especially when this code is perfectly safe at runtime. The other bug in Holger's answer though does look like an eclipse bug:) – ZhongYu Jun 04 '15 at 00:01
  • @bayou.io: note that the Eclipse bug I found was discovered while I tried to develop the question’s code into two directions— an obviously correct code and an obviously incorrect code. That’s why I assumed a relationship between these behaviors. – Holger Jun 04 '15 at 08:05
6

Thanks for the bug report, and thanks, Holger, for the example in your answer. These and several others finally made me question one small change made in the Eclipse compiler 11 years ago. The point was: Eclipse had illegally extended the capture algorithm to apply recursively to wildcard bounds.

There was one example where this illegal change perfectly aligned Eclipse behavior with javac. Generations of Eclipse developers have trusted this old decision more than what we could clearly see in JLS. Today I believe that previous deviation must have had a different reason.

Today I took the courage to align ecj with JLS in this regard and voila 5 bugs that appeared to be extremely hard to crack, have essentially been solved just like that (plus a little tweak here and there as compensation).

Ergo: Yes, Eclipse had a bug, but that bug has been fixed as of 4.7 milestone 2 :)

Here's what ecj will report henceforth:

The method b(List<T>) in the type Test is not applicable for the arguments (capture#1-of ? extends List<?>)

It's the wildcard inside a capture bound that doesn't find a rule to detect compatibility. More precisely, some time during inference (incorporation to be precise) we encounter the following constraint (T#0 representing an inference variable):

⟨T#0 = ?⟩

Naively, we could just resolve the type variable to the wildcard, but -- presumably because wildcards are not considered types -- the reduction rules define the above as reducing to FALSE, thus letting inference fail.

Stephan Herrmann
  • 7,963
  • 2
  • 27
  • 38
5

Thanks to bayou.io’s answer we can narrow the problem to the fact that

<X extends List<?>> void a(X instance) {
    b(instance);  // error
}
static final <T> List<T> b(List<T> list) {
    return list;
}

produces an error while

<X extends List<?>> void a(X instance) {
    List<?> instance2=instance;
    b(instance2);
}
static final <T> List<T> b(List<T> list) {
    return list;
}

can be compiled without problems. The assignment of instance2=instance is a widening conversion which should also happen for method invocation arguments. So the difference to the pattern of this answer is the additional subtype relationship.


Note that while I’m not sure whether this specific case is in line with the Java Language Specification, some tests revealed that Eclipse accepting the code is likely due to the fact that it is more sloppy regarding Generic types in general, as the following, definitely incorrect, code could be compiled without any error nor warning:

public static void main(String... arg) {
    List<Integer> l1=Arrays.asList(0, 1, 2);
    List<String>  l2=Arrays.asList("0", "1", "2");
    a(Arrays.asList(l1, l2));
}
static final void a(List<? extends List<?>> type) {
    test(type);
}
static final <Y,L extends List<Y>> void test(List<L> type) {
    L l1=type.get(0), l2=type.get(1);
    l2.set(0, l1.get(0));
}
Community
  • 1
  • 1
Holger
  • 285,553
  • 42
  • 434
  • 765
  • 1
    For the records: **both** examples were manifestations of the same wrong fix in https://bugs.eclipse.org/111208 which has been re-fixed and better aligned with JLS meanwhile (see also https://bugs.eclipse.org/472851). – Stephan Herrmann Jan 06 '19 at 17:35