Java Class Generics and Method Generics conflicts

2019-07-10 02:39发布

问题:

I have a Generic Class Factory class that has two methods one utilizes the Class generic T value and the other only uses its own method generic definitions.

public class GenericClassFactory<T extends ClassMatchable> {
    public <E, K> E newObject(ClassMatcher<E, K> matcher, K key, String packageName){...}
    public <K> T newObject(K key, String packageName){...}
}

The method that utilizes the T generic works fine but when I want to use the other method that doesn't care what the T generic is it won't use the Generic E it will just return an Object and then I have to type cast it.

Data data = new GenericClassFactory().newObject(new ClassMatcher<Data, String>(){...}, "key1", "my.package.name.impl");

This has compile errors because it wants me to typecast it to (Data). If I pass the GenericClassFactory a valid Class Generic it will work. Its like it doesn't recognize method generics if you have a Class Generic defined but not used.

Data data = new GenericClassFactory<ClassMatchable>().newObject(new ClassMatcher<Data, String>(){...}, "key1", "my.package.name.impl");

That works fine. But it's dumb that I would have to define a class generic like that when it isn't needed for my purposes. I could do this:

public class GenericClassFactory {
    public <E, K> E newObject(ClassMatcher<E, K> matcher, K key, String packageName){...}
    public <T extends ClassMatchable, K> T newObject(K key, String packageName){...}
}

But now my second method seems like its too broad or something...maybe not. I mean it will still give a compile error if the object you are assigning to the return type doesn't implement ClassMatchable. Is that the way I should go? So that I don't have to typecast?

回答1:

That's right, if you don't type a class reference, then even generic methods that only use method type parameters will not be generified. It's one of the weirder nuances of Java Generics. As you say, you can put in some arbitrary type for T:

Data data = new GenericClassFactory<ClassMatchable>().newObject(new ClassMatcher<Data, String>(){...}, "key1", "my.package.name.impl");

But more likely this shouldn't even be an instance method. Can't it be a static method? If so you could just invoke it like this:

Data data =  GenericClassFactory.newObject(new ClassMatcher<Data, String>(){...}, "key1", "my.package.name.impl");

Edit

Note that this extends to all instance members, not just generic instance methods. Thus, there are simpler cases that demonstrate this odd nuance. This code compiles with only warnings:

public class Scratchpad<T> {
   List<String> list;
   public static void main(String[] args) {
      Scratchpad sp = new Scratchpad();
      List<Integer> list = sp.list;
   }
}

And that's because sp.list is resolved as a List, not a List<String>, even though Scratchpad.list has nothing to do with T.

This is verbosely documented in the JLS, Section 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.



回答2:

You should tell the actual types of E and K when calling the method:

  new GenericClassFactory<ClassMatchable>().<TypeforE, TypeforK>newObject(...)

It appears that Java can't infer it from the argument.

And, of course:

Its like it doesn't recognize method generics if you have a Class Generic defined but not used.

is exactly correct.



回答3:

Its like it doesn't recognize method generics if you have a Class Generic defined but not used.

Exactly right. If you define a generic constraint on a class, and then instantiate the class without providing any generic constraint (that is, you leave off the <> completely), then you've just stepped into the realm of Raw Types, where nothing is the same anymore.

Raw Types only exist for backwards compatibility. According to Angelika Langer's excellent Java Generics FAQ,

The use of raw types in code written after the introduction of genericity into the Java programming language is discouraged. According to the Java Language Specification, it is possible that future versions of the Java programming language will disallow the use of raw types.

It also states:

Methods or constructors of a raw type have the signature that they would have after type erasure. A method or constructor call to a raw type generates an unchecked warning if the erasure changes the argument types.

If the newObject() method doesn't make use of the type parameter T of the class that it belongs to, then something is wrong with your design: most likely newObject() should be made a static method.

However, if it really must be an instance method for some reason, you may be able to get it to work by using the wildcard type GenericClassFactory<?>:

GenericClassFactory<?> gcf = new GenericClassFactory();
Data data = gcf.newObject(new ClassMatcher<Data, String>(){...}, "key1", "my.package.name.impl"); 


回答4:

Consider if T really is a type parameter of the class or the method. For example, is there something in the class that restricts created types to T (class-level), or perhaps it is being used as a convenience to avoid casting result values (method-level).

From what you've posted here, it seems to me that T should be a type on the method and that your last example is the answer. The method definition doesn't seem too wide, if the class implementation is generic and can give out different types with each method invocation.