Why Does Javac Complain About Generics Unrelated to the Class' Type Arguments

Why does javac complain about generics unrelated to the class' type arguments?

From JLS 4.8 Raw Types

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.

and

The type of a constructor (§8.8), instance method (§8.4, §9.4), or non-static field (§8.3) M 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.

Which - if you read it carefully - implies that all types are erased, not just the type you left out.

Explicit method type parameter ignored on a raw class type; compiler bug?

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.

Java generic methods in generics classes

'for backwards compatibility' seems a sufficient reason for the type erasure of class generic types - it is needed e.g. to allow you to return an untyped List and pass it to some legacy code. The extension of this to generic methods seems like a tricky sub-case.

The JLS snippet from 4.8 (which you quote) covers constructors, instance methods and member fields - generic methods are just a particular case of instance methods in general. So it seems your case is covered by this snippet.

Adapting JLS 4.8 to this specific case :

The type of a generic method is the raw type that corresponds to the
erasure of its type in the generic declaration corresponding to C.

(here the 'type' of the method would include all parameter and return types). If you interpret 'erasure' as 'erasing all generics', then this does seem to match the observed behaviour, although it is not very intuitive or even useful. It almost seems like an overzealous consistency, to erase all generics, rather than just generic class parameters (although who am I to second guess the designers).

Perhaps there could be problems where the class generic parameters interact with the method generic parameters - in your code they are fully independent, but you could imagine other cases where they are assigned / mixed together. I think it's worth pointing out that use of raw types are not recommended, as per the JLS :

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 genericity 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

Some of the thinking of the java developers is apparent here :

http://bugs.sun.com/view_bug.do?bug_id=6400189

(bug + fix showing that a method's return type is treated as part of the method's type for the purposes of this type erasure)

There is also this request, where someone appears to request the behaviour you describe - only erase the class generic parameters, not other generics - but it was rejected with this reasoning:

The request is to modify type erasure so that in the type declaration Foo<T>, erasure only removes T from parameterized types. Then, it so happens that within Map<K,V>'s declaration, Set<Map.Entry<K,V>> erases to Set<Map.Entry>.

But if Map<K,V> had a method that took type Map<String,V>, its erasure would just be Map<String>. For type erasure to change the number of type parameters is horrific, especially for compile-time method resolution. We are absolutely not going to accept this request.

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<Map.Entry>).

Why does the Streams API need a hint for generic type in this case?

This top part of this answer is basically what Radiodef said in comments above. I'm not wanting to steal those words, but the answer below the --- doesn't really work without the prior explanation.

As pointed out by Radiodef, the reason why this doesn't work in the first case is because it's using a raw type, Collection. Instead, use Collection<?>, and it will work:

        return ((Collection<?>) value).stream()
.map(MyClass::defaultFormatter)
.collect(Collectors.joining(eol));

The reason why it works with the explicit variable is because of unchecked conversion. Note that the following produces an unchecked conversion warning:

        Stream<String> stream = ((Collection) value).stream()
.map(MyClass::defaultFormatter);

The actual type of the expression on the RHS is Stream; you're allowed to coerce that to a Stream<String>, as described in JLS Sec 5.1.9:

There is an unchecked conversion from the raw class or interface type (§4.8) G to any parameterized type of the form G<T1,...,Tn>.


The reason why you can't do the same without the variable is a bit more subtle. This answer addresses the issue more directly: when you use a raw type, all generics are erased from the type, not just ones directly related to the omitted type.

So, the type of Stream.collect when the Stream is raw is the erasure of the type when it is generic:

  • Stream.collect(Collector<? super T,A,R> collector) returns an R;
  • The erasure of R is Object

so the return type of the collect call is Object, as you observe here. This can't be automatically coerced to a List<String> via unchecked conversion because it's not List.



Related Topics



Leave a reply



Submit