2

I have a problem understanding the behaviour of Java generics in the following case.

Having some parametrised interface, IFace<T>, and a method on some class that returns a class extending this interface, <C extends IFace<?>> Class<C> getClazz() a java compilation error is produced by gradle, 1.8 Oracle JDK, OSX and Linux, but not by the Eclipse compiler within the Eclipse IDE (it also happily runs under Eclipse RCP OSGi runtime), for the following implementation:

public class Whatever {
  public interface IFace<T> {}

  @SuppressWarnings("unchecked")
  protected <C extends IFace<?>> Class<C> getClazz() {
    return (Class<C>) IFace.class;
  }
}

➜ ./gradlew build

:compileJava
/Users/user/src/test/src/main/java/Whatever.java:6: error: incompatible types: Class<IFace> cannot be converted to Class<C>
    return (Class<C>) IFace.class;
                           ^
  where C is a type-variable:
    C extends IFace<?> declared in method <C>getClazz()
1 error
:compileJava FAILED

This implementation is not a very logical one, it is the default one that somebody thought was good, but I would like to understand why it is not compiling rather than question the logic of the code.

The easiest fix was to drop a part of the generic definition in the method signature. The following compiles without issues, but relies on a raw type:

protected Class<? extends IFace> getClazz() {
  return IFace.class;
}

Why would this compile and the above not? Is there a way to avoid using the raw type?

Oleg Sklyar
  • 9,834
  • 6
  • 39
  • 62
  • I noticed more then once that `javac` and the Eclipse compiler have different views on what is correct generics code. –  Apr 19 '17 at 06:05
  • 2
    The trivial change here is to cast via the raw class: `return (Class) (Class) IFace.class;`. But I strongly recommend against this: the compiler is complaining for a reason, and you're just punting a compile-time failure to be a runtime failure. – Andy Turner Apr 19 '17 at 07:56
  • FWIW, Intellij also compiles this code. – Andy Turner Apr 19 '17 at 08:17
  • Andy, I have already fixed that code so I do not need suggestions how to improve it. I am looking for a clear reason why the the compiler fails to compile it, purely for my understanding. And I know IntelliJ compiles it for some reason. – Oleg Sklyar Apr 19 '17 at 22:04

4 Answers4

4

It's not compiling because it's not type-correct.

Consider the following:

class Something implements IFace<String> {}
Class<Something> clazz = new Whatever().getClazz();
Something sth = clazz.newInstance();

This would fail with a InstantiationException, because clazz is IFace.class, and so it can't be instantiated; it's not Something.class, which could be instantied.

Ideone demo

But the non-instantiability isn't the relevant point here - it is fine for a Class to be non-instantiable - it is that this code has tried to instantiate it.

Class<T> has a method T newInstance(), which must either return a T, if it completes successfully, or throw an exception.

If the clazz.newInstance() call above did succeed (and the compiler doesn't know that it won't), the returned value would be an instance of IFace, not Something, and so the assignment would fail with a ClassCastException.

You can demonstrate this by changing IFace to be instantiable:

class IFace<T> {}
class Something extends IFace<String> {}
Class<Something> clazz = new Whatever().getClazz();
Something sth = clazz.newInstance();  // ClassCastException

Ideone demo

By raising an error like it does, the compiler is removing the potential for getting into this situation at all.

So, please don't try to fudge the compiler's errors away with raw types. It's telling you there is a problem, and you should fix it properly. Exactly what the fix looks like depends upon what you actually use the return value of Whatever.getClass() for.

Andy Turner
  • 137,514
  • 11
  • 162
  • 243
  • This answer is plain wrong: it proves the obvious and irrelevant matter, namely, that one cannot instantiate an interface. It has nothing to do with the original question, which does not try to instantiate any class received in the result. – Oleg Sklyar Apr 19 '17 at 00:15
  • @Oleg the compiler doesn't care - or even know - that you never try to reflectively instantiate the return value. It knows that *if you did*, it would not be safe, so it stops you doing it. From the type system's perspective, what you're doing no different from `List list = (List) Arrays.asList(new Object());`: if you invoke `list.toString()`, there would be no type issue; but if you invoked `String s = list.get(0);`, it would fail at runtime. So, the compiler stops you writing that assignment. – Andy Turner Apr 19 '17 at 06:46
  • You write yourself that your first code example causes `InstantiationException`, so it compiles through the definition of `Whatever`. My problem is that the class definition won't compile! So your answer cannot be correct, at least it is incorrectly formulated. – Oleg Sklyar Apr 19 '17 at 07:05
  • 2
    "My problem is that the class definition won't compile!" Yes, because it's not type correct. There is no difference between asking why you can't cast a `Class` to `Class` vs why you can't cast a `List` to a `List`. – Andy Turner Apr 19 '17 at 07:41
  • Your argument that "it's not type correct" does not hold water: write a dummy implementation of that interface typed with Object and return that class instead -- the compiler will happily take it! It is just as type incorrect as that interface, yet it just types it with totally irrelevant type... – Oleg Sklyar Apr 19 '17 at 22:13
  • @OlegSklyar please can you post an ideone link showing what you mean; I am not sure what you are saying to try. – Andy Turner Apr 19 '17 at 22:54
  • I am sort of tired with this discussion as it has not brought me a step closer to the actual answer. But what I mean is that while the above fails to compile, the following compiles happily (while it is still "not type correct"): `public static class Foo implements IFace {}` followed by `protected Class extends IFace>> getClazz() { return Foo.class; }` – Oleg Sklyar Apr 19 '17 at 23:24
  • @Oleg Debate is almost academic as we don't know what real world problem you are trying to solve. Correct answer then would be: Grab specification for Java language / compiler or / and compiler sources to see why this is happening. Otherwise we will be throwing out some non correct workarounds like `public class Whatever { public interface IFace {} @SuppressWarnings("unchecked") protected > Class getClazz() throws ClassNotFoundException { return (Class) Class.forName("IFace"); } }` which is compilable, but it is dirty hack IMHO. – Martin Polak Apr 20 '17 at 05:45
  • I don't see why this answer would be "plain wrong"; the only thing that appears "wrong" to me is the attitude that I sense in some of the comments to the answer. – GhostCat Apr 20 '17 at 07:04
  • @OlegSklyar your example compiles because it is type safe: by [PECS](http://stackoverflow.com/questions/2723397/what-is-pecs-producer-extends-consumer-super), a `Class extends IFace>>` produces `IFace>` instances (which `Foo.class.newInstance()` produces, since `Foo` is a subtype of `IFace>`; and you can't invoke any consumer methods on it. It's also completely different to the code which you're asking why it won't compile. – Andy Turner Apr 20 '17 at 07:08
0

It is kind of funny, that the Eclipse compiler does compile the code, but Oracle Java Compiler will not compile it. You can use the Eclipse Compiler during the gradle build to make sure, gradle is compiling the same way the IDE does. Add the following snippet to your build.gradle file

    configurations {
      ecj
    }

    dependencies {
      ecj 'org.eclipse.jdt.core.compiler:ecj:4.4.2'
    }

    compileJava {
        options.fork = true
        options.forkOptions.with {
        executable = 'java'
        jvmArgs = ['-classpath', project.configurations.ecj.asPath, 'org.eclipse.jdt.internal.compiler.batch.Main', '-nowarn']
      }
    }
  • I think the question refer to understand why is that error not how to bypass it. – Teocci Apr 19 '17 at 06:34
  • @Erich I will upvote for a good tip, but the question is really why the code won'd compile. What makes compiler think it is wrong. – Oleg Sklyar Apr 19 '17 at 07:02
0

It fails to compile because C could possibly be anything, where the compiler can be sure that IFace.class does not fulfill that requirement:

class X implements IFace<String> {
}

Class<X> myX = myWhatever.getClass(); // would be bad because IFace.class is not a Class<X>.

Andy just demonstrated why this assignment would be bad (e.g. when trying to instantiate that class), so my answer is not very different from his, but perhaps a little easier to understand...

This is all about the nice Java compiler feature of the type parameters for methods implied by calling context. You surely know the method

Collections.emptyList();

Which is declared as

public static <T> List<T> emptyList() {
  // ...
}

An implementation returning (List<T>)new ArrayList<String>(); would obviously be illegal, even with SuppressWarnings, as the T may be anything the caller assigns (or uses) the method's result to (type inference). But this is very similar to what you try when returning IFace.class where another class would be required by the caller.

Oh, and for the ones really enjoying Generics, here is the possibly worst solution to your problem:

public <C extends IFace<?>> Class<? super C> getClazz() {
  return IFace.class;
}
Florian Albrecht
  • 2,266
  • 1
  • 20
  • 25
  • If it was like you describe then returning a class of a dummy implementation typed with Object would also fail to compile. It does not. On a side note, I already have a solution, but the one you suggest would only permit to return that interface, which would be even worse than it is now :) – Oleg Sklyar Apr 19 '17 at 22:09
  • 1
    So you have a solution, which you don't share here, and still go ahead telling everybody that it is wrong or does not help what they say? Did you really understand what this site is about? – Florian Albrecht Apr 20 '17 at 08:25
  • The "solution" I have is presented in the question, please read carefully! I am, however, interested in understanding the reasons for the compilation problem, not in how to fix that code. – Oleg Sklyar Apr 20 '17 at 14:24
-1

Following will probably work:

public class Whatever {
    public interface IFace<T> {}

    @SuppressWarnings("unchecked")
    protected <C extends IFace> Class<C> getClazz() {
        return (Class<C>) IFace.class;
    }
}

In your former code, problem is that C has to extend IFace<?>>, but you provided only IFace. And for type system Class<IFace> != Class<IFace<?>>, therefore Class<IFace> can not be cast to Class<C extends IFace<?>>.

Maybe some better solution exists, as I am not a generics expert.

Martin Polak
  • 124
  • 6
  • Going to the raw type is exactly the workaround I described in my question. It still does not answer the question why it does not compile as it is. – Oleg Sklyar Apr 19 '17 at 07:06
  • I also described, where the problem is...'problem is that C has to extend IFace>>, but you provided only IFace. And for type system Class != Class>, therefore Class can not be cast to Class>.' This is why it will not compile. And probably Eclipse compiler is not that strict. BTW, thanks for downvote ;) – Martin Polak Apr 19 '17 at 07:11