5

I am getting a compiler error calling a generic method with explicit type parameters, as if the explicit type parameter had not been taken into account. Minimal example:

class CastExample {
    static class ThingProducer<S> {
        public <T> T getThing() { return null; }
    }

    static class ThingA {}

    public static void main(String... args) {
        ThingProducer thingProducer = new ThingProducer();
        ThingA thingA = thingProducer.<ThingA>getThing(); // compile error here
    }
}

ThingProducer is a raw type since the class has a type parameter, but in calling getThing we are not referencing the class type parameter, but instead providing the method type parameter. Per my understanding of the JLS, this should be legal, but it gives me this error:

incompatible types: Object cannot be converted to ThingA

The error disappears if I

  • remove the <S> from ThingProducer
  • or make getThing static
  • declare thingProducer ThingProducer<?> instead of the raw type ThingProducer

Is this a compiler bug? If not, what rule in the JLS defines this behavior?

Andrew Spencer
  • 15,164
  • 4
  • 29
  • 48
  • 1
    `ThingProducer thingProducer` in your main is a raw type. Bad bad bad. For example, with `ThingProducer> thingProducer`, your code compiles fine. – Tunaki Nov 16 '15 at 12:55
  • True but it should still compile as a raw type - in the event I am not referencing this type parameter. – Andrew Spencer Nov 16 '15 at 13:46
  • Where is the question of which this is a duplicate? I searched before asking. – Andrew Spencer Nov 16 '15 at 13:47
  • The link is at the top of this question. – Tunaki Nov 16 '15 at 13:56
  • That is not a duplicate of this question. Edited question to clarify. – Andrew Spencer Nov 16 '15 at 15:20
  • 2
    This is a question about raw-types. The point of the dupe question is to show that never, ever, should you use raw-types, read the accepted answer again. – Tunaki Nov 16 '15 at 15:35
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/95254/discussion-between-andrew-spencer-and-tunaki). – Andrew Spencer Nov 16 '15 at 16:04
  • 1
    You can’t perform a generic invocation on a raw type. So you already got it right, the explicit type arguments have not been taken into account. Because you are invoking a method on a *raw type*. There is nothing more to say about it, *don’t use raw types*. – Holger Nov 16 '15 at 16:06
  • 1
    I think you haven't read the code sample. The class does indeed have a type parameter, however we are not referencing the *class* type parameter, but instead providing the *method* type parameter. Per my understanding of the JLS, this should be legal. – Andrew Spencer Nov 16 '15 at 16:10
  • ...and if it isn't legal, still would be worth documenting in an answer as an edge case, since the compiler method is anything but explicit. – Andrew Spencer Nov 16 '15 at 16:25
  • 2
    Only after getting an answer to this, did I manage to find the questions of which it is a duplicate: http://stackoverflow.com/questions/18001550/java-generic-methods-in-generics-classes and http://stackoverflow.com/questions/27314649/why-does-javac-complain-about-generics-unrelated-to-the-class-type-arguments – Andrew Spencer Nov 17 '15 at 13:29

2 Answers2

3

Section 4.8 of the Java Language Specification answers your question:

The type of a constructor (§8.8), instance method (§8.4, §9.4), or non-static field (§8.3) of a raw type C that is not inherited from its superclasses or superinterfaces is the raw type that corresponds to the erasure of its type in the generic declaration corresponding to C.

In your example, getThing() is an "instance method ... of a raw type C [in this case, ThingProducer] which is not inherited". According to the JLS, its type is "the raw type that corresponds to the erasure of its type in the generic declaration". In the generic declaration of getThing() its type T is unbounded, which means its erasure is java.lang.Object.

Note that the spec does not say that getThing()'s type is the type constructed by erasing the raw type of which it is a member (that is, ThingProducer) -- it is actually the erasure of getThing() itself, which means that both type parameters (T and S) are erased.

[Aside: In my original answer, I quoted another sentence of the spec: "It is a compile-time error to pass type arguments to a non-static type member of a raw type that is not inherited from its superclasses or superinterfaces." My original reading of that sentence was that the compiler is required to emit a compile-time error for your syntax above, since I concluded that you were attempting to "pass type arguments to a non-static type member of a raw type". But I've changed my mind: I believe that last sentence is referring to a non-static type member (that is, a nested type), not merely a non-static generic member.]

Of course, no discussion of section 4.8 is complete without quoting this bit from the spec:

The use of raw types is allowed only as a concession to compatibility of legacy code. The use of raw types in code written after the introduction of generics into the Java programming language is strongly discouraged. It is possible that future versions of the Java programming language will disallow the use of raw types.

Daniel Pryden
  • 59,486
  • 16
  • 97
  • 135
  • Thankyou for that clarification. I agree with your 2nd reading, the bold paragraph does not apply here. However as you note, the spec as worded says that `getThing()` on a raw `ThingProducer` returns type `Object` (regardless of what type parameter I put in the method invocation). The compile error that we see reflects this. One might expect that the method type parameters should "survive" erasure of the class's type parameters, but the compiler implementers already rejected a bug request for that, see: http://stackoverflow.com/a/18082751/587365 – Andrew Spencer Nov 17 '15 at 13:28
  • The bolded sentence doesn't apply. The compile error disappears if `thingA` is declared as type `Object`, even though type parameters are still passed: `Object thingA = thingProducer.getThing();` The error also disappears if `` is changed to ``. So the error is not in the method call *itself*, but in the subsequent cast/assignment, as if the method's type is being erased. – Boann Nov 17 '15 at 14:47
  • @Boann: Agreed. That's what my edit was about (see the bit about "I've changed my mind"). The bolded part is referring to *type* members (that is, a nested class) not ordinary members (like a method here). That said, I don't think it's *wrong* to include it in my answer, do you? – Daniel Pryden Nov 17 '15 at 23:47
  • @AndrewSpencer: "One might expect" indeed -- as you did expect -- but it's not true, and never has been. :-) As [the comment on this bug](http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6256320) points out, "It is too much to expect to be able to use raw types (`Map`) while still getting some of the type-safety of generics (`Set`)." The correct thing to do (as you noted in your answer below) is to use a type wildcard (`ThingProducer>`) rather than a raw type. – Daniel Pryden Nov 17 '15 at 23:52
  • @DanielPryden Yep, I had just found that comment, and indeed when put like that it is perfectly reasonable. – Andrew Spencer Nov 18 '15 at 09:05
  • And @Boann thanks for your tidying up of the question – Andrew Spencer Nov 18 '15 at 09:06
2

As a complement to the accepted answer, if you're just looking to fix the compile error as simply as possible, and you're not using the class's type parameter <S>, the most appropriate fix (thanks @Tunaki) is

ThingProducer<?> thingProducer = new ThingProducer();

instead of

ThingProducer thingProducer = new ThingProducer();

which keeps us in the world of generics, whilst documenting that it doesn't matter what the type parameter is.

(In this cut-down example, it would make more sense to change ThingProducer, but I suspect in the real world anyone getting this error is probably dealing with a legacy class they can't change - that was my case at least.)

Andrew Spencer
  • 15,164
  • 4
  • 29
  • 48