1

Here's the code. The errors and warnings for both Java 7 and 8 are included in comments.

It is possible to change the signature of takeCollection(...) so that callers have a way to call it and pass SubRoot/Foo/Bar collections and compile this both against Java 7 and 8, while NOT having to refer in the method signature specifically to SubRoot/Foo/Bar types via overloading (there may many of these, some in dependent projects not visible to this one)? If so, how?

package generics;

import java.util.Collection;

// -------------------------------------------------------------------------------
// Cannot change this (start)
//-------------------------------------------------------------------------------
interface Root<F extends Foo<F,B>, B extends Bar<F,B>> {}
class     Foo <F extends Foo<F,B>, B extends Bar<F,B>> implements Root<F,B> {}
class     Bar <F extends Foo<F,B>, B extends Bar<F,B>> implements Root<F,B> {}

interface SubRoot extends Root<SubFoo, SubBar> {}
class     SubFoo  extends Foo <SubFoo, SubBar> implements SubRoot {}
class     SubBar  extends Bar <SubFoo, SubBar> implements SubRoot {}
//-------------------------------------------------------------------------------
// End of cannot change block.
//-------------------------------------------------------------------------------

public class GenericsTest {

  <R extends Root<F,B>, F extends Foo<F,B>, B extends Bar<F,B>>
  void takeOne(R one) {}

  // The following method signature can be changed a bit, but it has to accept a collection
  // of Roots, or ANY of its subtypes.
  <R extends Root<F,B>, F extends Foo<F,B>, B extends Bar<F,B>, C extends Collection<R>>
  void takeCollection(C collection) {}

  // ------------------------------------------
  // Test/illustration code that compiles well
  // ------------------------------------------

  @SuppressWarnings("rawtypes") // Warnings understood
  void testOneRoot1(Root root, Foo foo, Bar bar) {
    takeOne(root);
    // Java 1.7 and 1.8 agree about the warning, as expected.
    //
    // generics\GenericsTest.java:35: warning: [unchecked] unchecked method invocation: method takeOne in class GenericsTest is applied to given types
    //    takeOne(root);
    //           ^
    //  required: R
    //  found: Root
    //  where R,F,B are type-variables:
    //    R extends Root<F,B> declared in method <R,F,B>takeOne(R)
    //    F extends Foo<F,B> declared in method <R,F,B>takeOne(R)
    //    B extends Bar<F,B> declared in method <R,F,B>takeOne(R)

    takeOne(foo);
    // Java 1.7 and 1.8 agree about the warning, as expected.
    //
    // generics\GenericsTest.java:48: warning: [unchecked] unchecked method invocation: method takeOne in class GenericsTest is applied to given types
    //    takeOne(foo);
    //           ^
    //  required: R
    //  found: Foo
    //  where R,F,B are type-variables:
    //    R extends Root<F,B> declared in method <R,F,B>takeOne(R)
    //    F extends Foo<F,B> declared in method <R,F,B>takeOne(R)
    //    B extends Bar<F,B> declared in method <R,F,B>takeOne(R)

    takeOne(bar);
    // Java 1.7 and 1.8 agree about the warning, as expected.
    //
    // generics\GenericsTest.java:61: warning: [unchecked] unchecked method invocation: method takeOne in class GenericsTest is applied to given types
    //    takeOne(bar);
    //           ^
    //  required: R
    //  found: Bar
    //  where R,F,B are type-variables:
    //    R extends Root<F,B> declared in method <R,F,B>takeOne(R)
    //    F extends Foo<F,B> declared in method <R,F,B>takeOne(R)
    //    B extends Bar<F,B> declared in method <R,F,B>takeOne(R)
  }

  <R extends Root<F,B>, F extends Foo<F,B>, B extends Bar<F,B>>
  void testOneRoot2(R root, F foo, B bar) {
    takeOne(root); // All fine
    takeOne(foo);  // All fine
    takeOne(bar);  // All fine
  }

  void testOneSubRoot(SubRoot subRoot, SubFoo subFoo, SubBar subBar) {
    takeOne(subRoot); // All fine
    takeOne(subFoo);  // All fine
    takeOne(subBar);  // All fine
  }

  void testRootCollectionRaw(@SuppressWarnings("rawtypes") Collection<? extends Root> collection) {
    takeCollection(collection);
    // Java 1.7 quiet (not expected). Java 1.8 produces a warning, as expected:
    //
    //  generics\GenericsTest.java:89: warning: [unchecked] unchecked method invocation: method takeCollection in class GenericsTest is applied to given types
    //      takeCollection(collection);
    //                    ^
    //    required: C
    //    found: Collection<CAP#1>
    //    where C,R,F,B are type-variables:
    //      C extends Collection<R> declared in method <R,F,B,C>takeCollection(C)
    //      R extends Root<F,B> declared in method <R,F,B,C>takeCollection(C)
    //      F extends Foo<F,B> declared in method <R,F,B,C>takeCollection(C)
    //      B extends Bar<F,B> declared in method <R,F,B,C>takeCollection(C)
    //    where CAP#1 is a fresh type-variable:
    //      CAP#1 extends Root from capture of ? extends Root
 }

  // --------------------------------------------
  // Test/illustration code that does NOT compile
  // --------------------------------------------

  <R extends Root<F,B>, F extends Foo<F,B>, B extends Bar<F,B>>
  void testRootCollection1(Collection<? extends R> collection) {
    takeCollection(collection);
    //  JDK 1.8 quiet (as expected). JDK 1.7 yields an error (not expected):
    //
    //  generics\GenericsTest.java:112: error: invalid inferred types for R#1,F#1,B#1; inferred type does not conform to declared bound(s)
    //      takeCollection(collection);
    //                    ^
    //      inferred: CAP#1
    //      bound(s): Root<CAP#2,CAP#3>
    //    where R#1,F#1,B#1,C,R#2,F#2,B#2 are type-variables:
    //      R#1 extends Root<F#1,B#1> declared in method <R#1,F#1,B#1,C>takeCollection(C)
    //      F#1 extends Foo<F#1,B#1> declared in method <R#1,F#1,B#1,C>takeCollection(C)
    //      B#1 extends Bar<F#1,B#1> declared in method <R#1,F#1,B#1,C>takeCollection(C)
    //      C extends Collection<R#1> declared in method <R#1,F#1,B#1,C>takeCollection(C)
    //      R#2 extends Root<F#2,B#2> declared in method <R#2,F#2,B#2>testRootCollection1(Collection<? extends R#2>)
    //      F#2 extends Foo<F#2,B#2> declared in method <R#2,F#2,B#2>testRootCollection1(Collection<? extends R#2>)
    //      B#2 extends Bar<F#2,B#2> declared in method <R#2,F#2,B#2>testRootCollection1(Collection<? extends R#2>)
    //    where CAP#1,CAP#2,CAP#3 are fresh type-variables:
    //      CAP#1 extends R#2 from capture of ? extends R#2
    //      CAP#2 extends Foo<CAP#2,CAP#3> from capture of ?
    //      CAP#3 extends Bar<CAP#2,CAP#3> from capture of ?
  }

  <R extends Root<F,B>, F extends Foo<F,B>, B extends Bar<F,B>>
  void testRootCollection2(Collection<R> collection) {
    takeCollection(collection);
    //  JDK 1.8 quiet (as expected). JDK 1.7 yields an error (not expected):
    //
    //  generics\GenericsTest.java:136: error: invalid inferred types for R#1,F#1,B#1; inferred type does not conform to declared bound(s)
    //      takeCollection(collection);
    //                    ^
    //      inferred: R#2
    //      bound(s): Root<CAP#1,CAP#2>
    //    where R#1,F#1,B#1,C,R#2,F#2,B#2 are type-variables:
    //      R#1 extends Root<F#1,B#1> declared in method <R#1,F#1,B#1,C>takeCollection(C)
    //      F#1 extends Foo<F#1,B#1> declared in method <R#1,F#1,B#1,C>takeCollection(C)
    //      B#1 extends Bar<F#1,B#1> declared in method <R#1,F#1,B#1,C>takeCollection(C)
    //      C extends Collection<R#1> declared in method <R#1,F#1,B#1,C>takeCollection(C)
    //      R#2 extends Root<F#2,B#2> declared in method <R#2,F#2,B#2>testRootCollection2(Collection<R#2>)
    //      F#2 extends Foo<F#2,B#2> declared in method <R#2,F#2,B#2>testRootCollection2(Collection<R#2>)
    //      B#2 extends Bar<F#2,B#2> declared in method <R#2,F#2,B#2>testRootCollection2(Collection<R#2>)
    //    where CAP#1,CAP#2 are fresh type-variables:
    //      CAP#1 extends Foo<CAP#1,CAP#2> from capture of ?
    //      CAP#2 extends Bar<CAP#1,CAP#2> from capture of ?
  }

  void testSubRootCollection1(Collection<SubRoot> collection) {
    takeCollection(collection);
    //  JDK 1.8 quiet (as expected). JDK 1.7 yields an error (not expected):
    //
    //  generics\GenericsTest.java:158: error: invalid inferred types for R,F,B; inferred type does not conform to declared bound(s)
    //      takeCollection(collection);
    //                    ^
    //      inferred: SubRoot
    //      bound(s): Root<CAP#1,CAP#2>
    //    where R,F,B,C are type-variables:
    //      R extends Root<F,B> declared in method <R,F,B,C>takeCollection(C)
    //      F extends Foo<F,B> declared in method <R,F,B,C>takeCollection(C)
    //      B extends Bar<F,B> declared in method <R,F,B,C>takeCollection(C)
    //      C extends Collection<R> declared in method <R,F,B,C>takeCollection(C)
    //    where CAP#1,CAP#2 are fresh type-variables:
    //      CAP#1 extends Foo<CAP#1,CAP#2> from capture of ?
    //      CAP#2 extends Bar<CAP#1,CAP#2> from capture of ?
  }

  void testSubRootCollection2(Collection<? extends SubRoot> collection) {
    takeCollection(collection);
    //  JDK 1.8 quiet. JDK 1.7 yields an error:
    //
    //  generics\GenericsTest.java:177: error: invalid inferred types for R,F,B; inferred type does not conform to declared bound(s)
    //      takeCollection(collection);
    //                    ^
    //      inferred: CAP#1
    //      bound(s): Root<CAP#2,CAP#3>
    //    where R,F,B,C are type-variables:
    //      R extends Root<F,B> declared in method <R,F,B,C>takeCollection(C)
    //      F extends Foo<F,B> declared in method <R,F,B,C>takeCollection(C)
    //      B extends Bar<F,B> declared in method <R,F,B,C>takeCollection(C)
    //      C extends Collection<R> declared in method <R,F,B,C>takeCollection(C)
    //    where CAP#1,CAP#2,CAP#3 are fresh type-variables:
    //      CAP#1 extends SubRoot from capture of ? extends SubRoot
    //      CAP#2 extends Foo<CAP#2,CAP#3> from capture of ?
    //      CAP#3 extends Bar<CAP#2,CAP#3> from capture of ?
  }
}
Makoto
  • 104,088
  • 27
  • 192
  • 230
Learner
  • 1,215
  • 1
  • 11
  • 26
  • 5
    Yikes, that's some scary-looking generics. Could you maybe cut it down to just ask about one of these walls of error text, rather than all of these? – Andy Turner Mar 03 '17 at 15:44
  • 2
    I also think that you are expecting quite a lot ... – GhostCat Mar 03 '17 at 15:52
  • Most errors are the same. I added them here for completeness as some people complained they didn't see them when I originally asked them. I have reduced the problem down to the minimum - top 27 lines are all that matters really, the rest is just illustrating the problem in various ways. – Learner Mar 03 '17 at 15:54
  • 1
    Replacing all rawtypes in your code with wildcarded types (eg. `Foo` with `Foo,?>`) removes all warnings, at least in Java 8. – Calculator Mar 03 '17 at 15:58
  • That is fine, I am not concerned about these - I noted them in comments as expected. What was unexpected is that Java 7 does not issue all the warnings. – Learner Mar 03 '17 at 16:02

2 Answers2

3

All of the compilation errors go away for me if I change the signature of takeCollection to:

<R extends Root<F,B>, F extends Foo<F,B>, B extends Bar<F,B>>
void takeCollection(Collection<? extends R> collection) {}

There are unchecked/unsafe operations warnings, though, all related to raw types.

Andy Turner
  • 137,514
  • 11
  • 162
  • 243
  • That actually works! Can anyone explain why is that different? – Learner Mar 03 '17 at 16:01
  • 1
    "That actually works!" Don't sound so surprised. – Andy Turner Mar 03 '17 at 16:02
  • :) Well, I tried many things but not quite that variant. Why would this mean anything different to a compiler than what is shown above? Isn't void accept(P p) supposed to act the same as void accept(Q q)? – Learner Mar 03 '17 at 16:04
  • 1
    Not sure. I can imagine it has something to do with the compiler not liking that it can't calculate an actual bound for `C`, because it's being passed an upper-bounded collection. Whilst not an intellectually satisfying response, "just accept that it works" is a pragmatic response; add it to your repertoire of tricks to try next time you're in generics hell. – Andy Turner Mar 03 '17 at 16:16
  • Oh, I already applied it pragmatically :) .... But I would like to learn why this is an issue so that I can prevent it, deal better with situations like this. As it is I can't be sure whether I just moved the problem elsewhere (this is a library that unknown developers do and will use for unknown code - I don't want to break their code). It means that I may have to create method variants (overload if possible) to cover all the cases. – Learner Mar 03 '17 at 16:23
2

I would change the signatures like follows:

void takeOne(Root<?,?> one) {}

void takeCollection(Collection<? extends Root<?,?>> collection) {}

void testOneRoot1(Root<?,?> root, Foo<?,?> foo, Bar<?,?> bar) {}

<R extends Root<F,B>, F extends Foo<F,B>, B extends Bar<F,B>>
void testOneRoot2(R root, F foo, B bar) {}

void testOneSubRoot(SubRoot subRoot, SubFoo subFoo, SubBar subBar) {}

@SuppressWarnings("unchecked")
void testRootCollectionRaw(@SuppressWarnings("rawtypes") Collection<? extends Root> collection) {
   takeCollection((Collection<? extends Root<?,?>>)collection);    
}      

void testRootCollection1(Collection<? extends Root<?,?>> collection) {}

void testRootCollection2(Collection<? extends Root<?,?>> collection) {}

Your code should compile using this signatures for both Java 7 and Java 8 without errors or warnings. There are several places in your code where it makes sense to replace generic type parameters with wildcards. For example, <R extends Root<F,B>, F extends Foo<F,B>, B extends Bar<F,B>> void takeOne(R one) {} is almost equivalent to void takeOne(Root<?,?> one) {}, because how to bind F and B is already defined by the signature of Root. testRootCollectionRaw is a special case, because it features a rawtype as upperbound. I would rather solve this case with an unchecked cast in the testRootCollectionRaw method than by adapting the takeCollection signature for it to work.

The reason why your original code does not compile for Java 7 is that type inference in Java 7 is not as advanced as in Java 8. Java 8 will consider the target type of an expression on type inference. Generally, it is a good idea to replace explicit type parameters with wildcards where it is possible. A side effect of practicing this is that Java 7 has an easier time dealing with wildcards than with explicit type parameters in generic methods.

Calculator
  • 2,769
  • 1
  • 13
  • 18
  • testRootCollectionRaw is only an illustration, I don't really care about raw types. Wildcards aren't entirely equivalent, though. – Learner Mar 03 '17 at 17:15