JDK-6785612 : Generifying a class breaks compatibility wrt >1.5 clients
  • Type: Enhancement
  • Component: specification
  • Sub-Component: language
  • Affected Version: 6u10
  • Priority: P4
  • Status: Closed
  • Resolution: Not an Issue
  • OS: windows_xp
  • CPU: x86
  • Submitted: 2008-12-16
  • Updated: 2011-02-16
  • Resolved: 2009-03-04
Related Reports
Relates :  
Relates :  
Relates :  
Relates :  
Description
FULL PRODUCT VERSION :
java version "1.6.0_10"
Java(TM) SE Runtime Environment (build 1.6.0_10-b33)
Java HotSpot(TM) Client VM (build 11.0-b15, mixed mode, sharing)

ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows XP [Version 5.1.2600]
(this is not an OS specific issue)

EXTRA RELEVANT SYSTEM CONFIGURATION :
n/a
(this is not a configuration specific issue)

A DESCRIPTION OF THE PROBLEM :
If a class that has optional generic types is instantiated using the raw type... all return types for methods throughout that class are stripped, and now return raw types.


Related bugs:
5073043
5074427
6545698

The problem is not as simple as the solution addressed in these discarded bugs.  The problem is one of passivity.  If a class has a method with a typesafe construct in the return type, no generic type can EVER add to that class, for it will be break any consumers of that existing method that rely on the typesafety.

This seems like a pretty large burden to put on developers.


STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Create a basic class.
2. Create a method within that class that returns a parametrized generic type. (e.g. Collection<String>)
3.  Use that method in a way that depends on that return type.  (e.g. String s = col.get(0))
4.  Verify everything compiles.
5.  Add a generic to the basic class declaration.  (e.g. <T>)
6.  A compiler error is now generated on the line of code from step 3.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
I would not expect a raw type returned from a method that does not depend on the class' generic type.  (i.e. Obviously a method returning type T would be stripped, but if it's returning an explicit type, there is no reason to strip it.)
ACTUAL -
Class does not compile.

ERROR MESSAGES/STACK TRACES THAT OCCUR :
GenericsBug.java:28: incompatible types
found   : java.lang.Object
required: java.lang.Number
                Number n2 = new Generic().getGenericNumbers().get(0);
                                                                 ^
1 error

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
import java.util.*;

public class GenericsBug
{
	public static void main(String[] args)
	{
		//Simple class without generics that has a method that returns a type-safe list
		class NoGeneric
		{
			public List<Number> getGenericNumbers()
			{
				return new ArrayList<Number>();
			}
		}
		//This compiles fine, as the typesafe list returns a Number
		Number n1 = new NoGeneric().getGenericNumbers().get(0);
	
		//Simple class with a generic type that has the EXACT SAME method as above that returns a type-safe list
		class Generic<T>
		{
			public List<Number> getGenericNumbers()
			{
				return new ArrayList<Number>();
			}
		}
		//This does not compile, because getGenericNumbers() is now returning a raw List,
		//simply because the generic not being declared with a type
		Number n2 = new Generic().getGenericNumbers().get(0);

		//This DOES compile, because types are not stripped
		//This is the recommended solution from previous bugs, but we can't
		//break existing consumers of our class, meaning we can't add generics at all
		Number n3 = new Generic<Object>().getGenericNumbers().get(0);
	}
}
---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
The only workaround is not exposing typesafe objects as return types, and forcing the consumers to infer the cast (effectively preventing the use of generics all together) until we can declare that the class will never have a generic type.

For classes and packages without unknown consumers, simply adding the unchecked 'cast' resolves the compiler error.

e.g.
Number n2 = ((List<Number>)new Generic().getGenericNumbers()).get(0);

Comments
EVALUATION Secretly, I wince at seeing class C whose method returns Foo<String> being called "non-generic". The JLS says it's non-generic but that misses the point, as C is clearly not a legacy class unaware of generics. C has been generified; it just so happens to not have any type variables in its class signature. The term 'new C()' is NOT using a raw type; it's just not using a generic type either! The submitter basically argues that 'new C()' from this point on should recognize that C has been generified and never erase method signatures. Then, given: class C { List<Number> m() ... } calling 'new C().m().get(0)' would return a Number. If you CONTINUE to generify: class C<T> { List<Number> m() ... } saying 'new C()' would give a warning, sure, but 'new C().m().get(0)' would CONTINUE return a Number. That it actually returns an Object does make it seem that the more generics you use, the less generified the overall program becomes. But this is really a false argument. Once class C becomes class C<T>, "new C()" IS using a raw type. Treating C as raw but NOT erasing its methods would get complicated, fast; what if m() was modified at the same time to use T in its return type? The deal is that the client gets type-safety if they use the generic library in a type-correct way. No-one would claim that having m() return List<String> instead of List<Number> is a change which clients should be able to ignore. So don't claim that adding formal type parameters to C, or changing the bounds of existing formal type parameters, should be a source-compatible change. Java Puzzler #88 "Raw Deal" is right: "avoid writing raw types in code intended for release 5.0 or later." To summarize, migration compatibility as introduced in JDK 1.5 - that legacy consumers can use and reuse (i.e. extend) generified providers, and generified consumers can use and reuse legacy providers - has its limits. Implicitly, it assumes that a legacy class is generified in one go. The workaround is: don't generify class members without generifying the class signature too. I don't see anything short of far-reaching changes to do what the submitter wants. Footnote: It is in some ways strange that Java decided its generics should be use-site - for the raw/generic determination, and for variance. Having the declaration site determine that a class is generic, based on its methods and its class signature, would be at least as useful as declaration-site variance in avoiding these endless taxes on client code.
03-03-2009

EVALUATION Reassigning to 'specification'; the compiler is behaving according to JLS 4.8 ("The type of a constructor (��8.8), instance method (��8.8, ��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 erasure of its type in the generic declaration corresponding to C. The type of a static member of a raw type C is the same as its type in the generic declaration corresponding to C."). However I see the problem: on the one hand raw types have been designed in order to make usage of a generic library by a non-generic client as simple as possible - on the other hand raw-types are useless for generic clients exploiting a generified API. In particular, the submitter is complaining because generifying a class (that is adding one or more type-parameters to an existing class declaration C so that it becomes C<X1, X2 ... Xn>) it's an incompatible change w.r.t. to generic clients using that class. Suppose that e.g. C defines a member 'm' of type Foo<String>, and a generic client contains the following code: Foo<String> fs = new C().m; This code compiles fine if C is non-generic; however after C has been generified, the above code will result in an unchecked warning, because the code is accessing a member of a raw-type, resulting in the erasure of the member type (which becomes Foo instead of Foo<String>). In the above example the only consequence of the generification process is an unchecked warning, which can be easily removed by e.g. creating a C<Object> instead of a raw C. On the other hand, if the generic client expolited some type-dependent feature of Foo, as in: String fs = new C().m.t; where 't' is a field of Foo<T> of type T; this code wouldn't compile anymore after C is generified - as C().m has type Foo, so that C().m.t has type Object (instead of the expected String). Example: class Foo<T> { T t; } class C {//adding a type parameter to C breaks compilation of Test! Foo<String> m; } class Test { String fs = new C().m.t; }
16-12-2008