6

I was recently caught flat-footed by the following Java code:

interface Common {}

interface A extends Common {}

static class B implements Common {}

static class Impl {
  private A a;

  public <T extends A> T translate() {
    return (T) a;
  }
}

static class Usage {
  public void use() {
    Impl impl = new Impl();
    B b = impl.translate(); // Why does this compile?
  }
}

I would have expected that the type constraint on Impl.translate would not allow storing the result in type B to be accepted by the compiler, considering that B does not extend A. Instead of a compiler error, the code throws an UncheckedCastException at runtime.

This only happens when the method returns the type T; if it is the method parameter instead:

  public <T extends A> void translate(T t) {}

Then instances of B are not allowed as parameters to translate, as expected.

What's going on here? Why is this allowed by Java's type system?

moople
  • 71
  • 1
  • 4
  • 1
    I'm also curious how you compile `static class B implements Common {}`. You can't make a class `static` if it's not inner of other class. Same for `static class Impl` and `static class Usage` – KunLun Jul 16 '20 at 15:52
  • 2
    The description of [the Error Prone checker for this pattern](http://errorprone.info/bugpattern/TypeParameterUnusedInFormals) may help to understand it. Note that it compiles, but *with an unchecked cast warning*. – Andy Turner Jul 16 '20 at 15:53
  • 1
    I think this is what's actually going on: https://stackoverflow.com/questions/38092697/why-does-this-generic-code-compile-in-java-8 – jacobm Jul 16 '20 at 18:27
  • 1
    Does this answer your question? [Why does this generic code compile in java 8?](https://stackoverflow.com/questions/38092697/why-does-this-generic-code-compile-in-java-8) – user Jul 16 '20 at 18:40
  • 2
    „*...please can you un-accept my answer so I can delete it?...*“ @AndyTurner — I respect a man who doesn't sell-out his integrity for a few hundred thousand internet points :) – deduper Jul 16 '20 at 21:23

3 Answers3

5

This compiles because it is, in principle, possible for an object to be both a B and an A. For example, an instance of this class:

static class C extends B implements A {}

It doesn't matter to the compiler that no such class exists. It might be possible for someone else to import this code from a dependency and define C themselves, and that is valid and must be allowed to work. It doesn't matter that the actual class of the object is trivial to find at compile time because the compiler does not do that kind of analysis. Modifiers that forbid the existence of such a class, such as adding final to B, are also not considered, though I'm not sure about why. It could be just to reduce the complexity of compiler logic.

Changing A to also be a class makes this a compile error because Java does not allow a class to extend multiple classes.

Douglas
  • 5,017
  • 1
  • 14
  • 28
  • Agree with this answer. If the Impl has a setter method of its “a” then it will be even easier to understand. Consider somewhere else calls ### impl.setA(new C()) and then later there’s another call of ### C c=impl.translate(); – Bee Chow Jul 16 '20 at 18:44
1

The T of your generic isn't getting assigned. Since it is possible to have a value that is both B and A the compiler assumes you're ok.

If you assign the type to B it you get the error.

B b = impl.<B>translate(); 

Testy.java:16: error: method translate in class Impl cannot be applied to given types; B b = impl.translate(); ^
required: no arguments
found: no arguments
reason: explicit type argument B does not conform to declared bound(s) A

That is a similar error you get when the type is determined by the argument.

public <T extends A> T translate(T t) {
    return (T) a;
  }

B b = impl.translate(new B());

The type is being assigned by the argument, and the argument is a B this will not compile.

As mentioned in the other answer, if both A and B are classes then there will not be a T that can satisfy both.

Testy.java:15: error: incompatible types: inference variable T has incompatible upper bounds B,A
B b = impl.translate(); // Why does this compile?
^
where T is a type-variable:
T extends A declared in method translate()

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

TL;DR Answer

The point is that there could be a class that extends [B] and implements [A], and that theoretical type is what T is inferred to.“ — @JornVernee Jul 3 '17 at 14:43

The Long Answer

Jorn Vernee actually works at Oracle on the Java team. His comment quoted in the TL;DR is the most concise, easiest to grok distillation of his employer's Chapter 18. Type Inference you'll ever find.

If you know all about type systems and lambda calculus, and want the complete nitty-gritty, then Chapter 18 will be right down your alley.

In between the expert concision of Vernee's TL;DR and the headache-inducing rigor of the JLS, is user @Radiodef's point-by-point digest of the JLS' Reduction, Incorporation and Resolution processes.

@JornVernee's and @Radiodef's explanations of the JLS' reasoning behind this puzzler, are (to me) the clearest, easiest to grok of any of the dozens of other answers to the seven or eight — at least — duplicates of this question:

  1. Why does this generic code compile in java 8?
  2. Generic return type upper bound - interface vs. class - surprisingly valid code
  3. Apparent type violation, but compiles
  4. Why can this generic method with a bound return any type?
  5. String gets assigned to a List without a compilation error
  6. Is this a Java generic type inference compiler bug?
  7. Java type inference: reference is ambiguous in Java 8, but not Java 7
deduper
  • 1,944
  • 9
  • 22