Java generic methods in generics classes
Asked Answered
F

6

31

If you create a generic class in Java (the class has generic type parameters), can you use generic methods (the method takes generic type parameters)?

Consider the following example:

public class MyClass {
  public <K> K doSomething(K k){
    return k;
  }
}

public class MyGenericClass<T> {
  public <K> K doSomething(K k){
    return k;
  }

  public <K> List<K> makeSingletonList(K k){
    return Collections.singletonList(k);
  }
}

As you would expect with a generic method, I can call doSomething(K) on instances of MyClass with any object:

MyClass clazz = new MyClass();
String string = clazz.doSomething("String");
Integer integer = clazz.doSomething(1);

However, if I try to use instances of MyGenericClass without specifying a generic type, I calling doSomething(K) returns an Object, regardless of what K was passed in:

MyGenericClass untyped = new MyGenericClass();
// this doesn't compile - "Incompatible types. Required: String, Found: Object"
String string = untyped.doSomething("String");

Oddly, it will compile if the return type is a generic class - e.g. List<K> (Actually, this can be explained - see answer below):

MyGenericClass untyped = new MyGenericClass();
List<String> list = untyped.makeSingletonList("String"); // this compiles

Also, it will compile if the generic class is typed, even if only with wildcards:

MyGenericClass<?> wildcard = new MyGenericClass();
String string = wildcard.doSomething("String"); // this compiles
  • Is there a good reason why calling a generic method in an untyped generic class shouldn't work?

  • Is there some clever trick relating to generic classes and generic methods that I am missing?

EDIT:

To clarify, I would expect an untyped or raw-typed generic class not to honour the generic class's type parameters (because they haven't been provided). However, it's not clear to my why an untyped or raw-typed generic class would mean that generic methods are not honoured.

It transpires that this issue has already been raised on SO, c.f. this question. The answers to this explain that when a class is untyped / in its raw-form, all generics are removed from the class - including typing of generic methods.

However, there isn't really an explanation as to why this is the case. So allow me to clarify my question:

  • Why does Java remove generic method typing on untyped or raw-type generic classes? Is there a good reason for this, or was it just an oversight?

EDIT - discussion of JLS:

It has been suggested (in answer to the previous SO question and to this question) that this is treated in JLS 4.8, which states:

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.

It is clear to me how this relates to an untyped class - the class generic types are replaced with the erasure types. If the class generics are bound, then the erasure type corresponds to those bounds. If the they are not bound, then the erasure type is Object - e.g.

// unbound class types
public class MyGenericClass<T> {
  public T doSomething(T t) { return t; }
}
MyGenericClass untyped = new MyGenericClass();
Object t = untyped.doSomething("String");

// bound class types
public class MyBoundedGenericClass<T extends Number> {
  public T doSomething(T t) { return t; }
}
MyBoundedGenericClass bounded = new MyBoundedGenericClass();
Object t1 = bounded.doSomething("String"); // does not compile
Number t2 = bounded.doSomething(1); // does compile

Whilst generic methods are instance methods, it is not clear to me that JLS 4.8 applies to generic methods. The generic method's type (<K> in earlier example) is not untyped, as it's type is determined by the method parameters - only the class is untyped / raw-typed.

Floppy answered 1/8, 2013 at 18:15 Comment(6)
possible duplicate of Combining Raw Types and Generic Methods. See also: Why won't this generic java code compile?Calculable
#14882503 :)Plainsman
@PaulBellora - thanks. The first is very much related - although my copious searching of SO didn't find it, so thanks for the reference. The second is less relevant, as it only covers generic classes - not generic methods.Floppy
Sorry, slightly OT. But I tried searching for it, is static class a new construction? AFAIK it will not compile in Java 6 (will it in Java 7?).Twocolor
@hajder - sorry, I originally wrote these as nested classes in a test class (hence making them static), and neglected to remove them when I C&Ped to the question. Now removed.Floppy
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." if you interpret 'erasure' as 'erasing all generics', then this does seem to match the observed behaviour, though not intuitive or very useful. It almost seems like an overzealous consistency, to erase all generics, not just generic class parameters (although who am I to second guess the designers)Barracks
B
8

'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 :

https://bugs.java.com/bugdatabase/view_bug?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>).

Barracks answered 6/8, 2013 at 14:17 Comment(4)
I would agree backwards compatability is a good reason - it's just that it's not clear why type erasure of all generics would be necessary to achieve this. +1 for the bug report, in which this issue is recognised as a bug, but that fixing it affects compatibility (which is not quite the same thing as saying it was needed for compatibility...). Unfortunately it seems that the fix hasn't yet been incorporated into a released version...Floppy
there is also this request : bugs.sun.com/bugdatabase/view_bug.do?bug_id=6256320 where someone appears to request the behaviour you describe - only erase the class generic parameters, not other generics - but it was rejectedBarracks
+1 Good research - consider quoting the evaluations of the linked bug/request in your answer. @Floppy I recommend this answer for the bounty because it's the closest you're going to get to knowing the intent of the language designers.Calculable
Indeed - a well deserved bounty, now awarded.Floppy
V
4

With Java Generics, if you use the raw form of a generic class, then all generics on the class, even unrelated generic methods such as your makeSingletonList and doSomething methods, become raw. The reason as I understand it is to provide backwards compatibility with Java code written pre-generics.

If there is no use for your generic type parameter T, then simply remove it from MyGenericClass, leaving your methods generic with K. Else you'll have to live with the fact that the class type parameter T must be given to your class to use generics on anything else in the class.

Volturno answered 1/8, 2013 at 18:20 Comment(3)
Thanks - in my use case, I need the generic class type parameters. They're not used in the example above as I wanted a succinct SSCCE. Also, it's not clear to me why erasing all generics in raw class ensures backwards compatibility.Floppy
@Floppy suppose you has this interface: interface C<T> { <K> K foo(); }. Behavior explained in the answer ensures that some pre-generic C can be safely automatically "generified" without old code changes. Otherwise users of C interface should call foo like this: c.<SomeType>foo()Repetitive
@Repetitive - you may want to post your answer as an answer... in any case, I think you've missed the point. If you have an untyped implementation of C, not only would the class generics (T) be treated as Object (which is to be expected), but also the generic method (<K> K foo()) would be treated as untyped, so the implementation of foo() would have to be public Object foo(){...}, which comes back to the original question.Floppy
R
2

I've found one reason to fully discard generics (however it's not quite good). Reason is: generics may be bounded. Consider this class:

public static class MyGenericClass<T> {
    public <K extends T> K doSomething(K k){
        return k;
    }

    public <K> List<K> makeSingletonList(K k){
        return Collections.singletonList(k);
    }
}

The compiler has to discard generics in doSomething, when you use the class without generics. And I think all generics are discarded to be consistent with this behavior.

makeSingletonList compiles because Java does an unchecked cast from List to List<K> (however compiler displays warning).

Repetitive answered 6/8, 2013 at 11:48 Comment(5)
No - the method types (<K>) are independent of the class types (<T>) (unless otherwise constrained, e.g. <K extends T> as per your example), and independent of each other. Thus we could have a class C<T extends Number> with method public <T> T doSomething(T t){ return t; }, and that could be called as new C<Integer>().doSomething("String"). This would compile (but would be not very clear for future developers...) as the method generic type is taken instead of the class generic type, and they are independent.Floppy
It doesn't contradict anything that I wrote. Yes, method types independent of the class types. But if method types constrained with class types, compiler has to discard it all when class used without generics.Repetitive
+1 one example for method type usage such that it depends on class type is reason enough for omitting generics away anywhere in the class, when raw type is used.Foretopgallant
I disagree - the compiler doesn't have to discard all the generic types (although, this seems to be what has been done). Rather, the compiler could treat only the untyped generics as Object. That's what I would have expected, but that's not what happens, and hence the question remains... why are all the generic types removed?Floppy
I think the full type erasure makes sense combined with the mentioned backwards compatability. When the object is created without generic type arguments it can be assumed that the usage of the object takes place in non generic code and so all the generic types are replaced with Object right away. I guess it's not possible for the compiler to guess on each call if it was intended to be generic. What happens if you make the methods static?Wilterdink
F
1

This doesn't answer the fundamental question, but does address the issue of why makeSingletonList(...) compiles whilst doSomething(...) does not:

In an untyped implementation of MyGenericClass:

MyGenericClass untyped = new MyGenericClass();
List<String> list = untyped.makeSingletonList("String");

...is equivalent to:

MyGenericClass untyped = new MyGenericClass();
List list = untyped.makeSingletonList("String");
List<String> typedList = list;

This will compile (with several warnings), but is then open to runtime errors.

Indeed, this is also analogous to:

MyGenericClass untyped = new MyGenericClass();
List<Integer> list = untyped.makeSingletonList("String"); // this compiles!!!
Integer a = list.get(0);

...which compiles but will throw a runtime ClassCastException when you try and get the String value out and cast it to an Integer.

Floppy answered 6/8, 2013 at 12:48 Comment(0)
V
0

The reason for this is backwards compatibility with pre-generics code. The pre-generics code did not use generic arguments, instead using what seems today to be a raw type. The pre-generics code would use Object references instead of references using the generic type, and raw types use type Object for all generic arguments, so the code was indeed backwards compatible.

As an example, consider this code:

List list = new ArrayList();

This is pre-generic code, and once generics were introduced, this was interpreted as a raw generic type equivalent to:

List<?> list = new ArrayList<>();

Because the ? doesn't have an extends or super keyword after it, it is transformed into this:

List<Object> list = new ArrayList<>();

The version of List that was used before generics used an Object to refer to a list element, and this version also uses an Object to refer to a list element, so backwards compatibility is retained.

Vatic answered 6/8, 2013 at 10:30 Comment(3)
Your answer provides a good explanation as to why Java ignores class generic types in an untyped class. However, the question is really about generic methods, for which the typing is also ignored on an untyped class, and which you haven't covered.Floppy
The JLS snippet from 4.8 (from the question you link) covers constructors, instance methods and member fields - generic methods are just a particular case of instance methods in general. So it seems to me tbodt's answer does cover your situation?Barracks
isn't 'for backwards compatibility' a sufficient reason? Some of the thinking is apparent here : 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)Barracks
R
0

From my comment on MAnyKeys answer:

I think the full type erasure makes sense combined with the mentioned backwards compatability. When the object is created without generic type arguments it can be (or shoudl be?) assumed that the usage of the object takes place in non generic code.

Consider this legacy code:

public class MyGenericClass {
    public Object doSomething(Object k){
        return k;
    }
}

called like this:

MyGenericClass foo = new MyGenricClass();
NotKType notKType = foo.doSomething(new NotKType());

Now the class and it's method are made generic:

public class MyGenericClass<T> {
    public <K extends KType> K doSomething(K k){
        return k;
    }
}

Now the above caller code wouldn't compile anymore as NotKType is not a suptype of KType. To avoid this the generic types are replaced with Object. Although there are cases where it wouldn't make any difference (like your example), it is at least very complex for the compiler to analyze when. Might even be impossible.

I know this szenario seems a little constructed but I'm sure it happens from time to time.

Rasbora answered 6/8, 2013 at 14:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.