Avoiding Returning Wildcard Types

风流意气都作罢 提交于 2019-12-09 10:41:35

问题


I have a class with a collection of Wildcard Types that is a singleton, something like:

public ObliviousClass{

    private static final ObliviousClass INSTANCE = new ObliviousClass();

    private Map<Key, Type<?>> map = new HashMap<Key, Type<?>>();

    public void putType(Key key, Type<?> type){
        map.put(type);
    }

    // returns the singleton
    public static ObliviousClass getInstance(){
        return INSTANCE;
    }

}

I'd like to be able to add different Parameterized types to this collection in client code:

void clientMethod(){
    ObliviousClass oc = ObliviousClass.getInstance();

    Type<Integer> intType = ...
    Type<String> stringType = ...

    oc.putType(new Key(0), intType);
    oc.putType(new Key(1), stringType);
}

Up to this point, as I understand it, everything is ok. But a client also needs to be able to get a Type<?> provided the Key. So a method something like the following would be added to ObliviousClass:

public Type<?> getType(Key key){
    return map.get(key);
}

But in my handy copy of Effective Java, I read:

Do not use wildcard types as return types.

I understand the issue, as the client would have to cast the returned Type<?>. But I really do not want to make ObliviousClass a generic type, ObliviousClass<T>, because then my client code above would not work...

Is there a better design for what I am trying to do? -My current solution is to provide a static method for the client; something along the lines of:

public static <T> void getType(ObliviousClass instance, Key key, Type<T> dest){
    dest = (Type<T>)instance.getType(key);
}

I searched around, but wasn't able to find an answer that totally cleared my confusion.


回答1:


Here's a type-safe way to store multiple instances of a given type in a map. The key is that you need to provide a Class instance when retrieving values in order to perform runtime type-checking, because static type information has been erased.

class ObliviousClass {

  private final Map<Key, Object> map = new HashMap<Key, Object>();

  public Object put(Key key, Object value)
  {
    return map.put(key, value);
  }

  public <T> T get(Key key, Class<? extends T> type)
  {
    return type.cast(map.get(key)); 
  }

}

Usage would look like this:

oc.put(k1, 42);
oc.put(k2, "Hello!");
...
Integer i = oc.get(k1, Integer.class);
String s = oc.get(k2, String.class);
Integer x = oc.get(k2, Integer.class); /* Throws ClassCastException */



回答2:


Simply type your class:

public ObliviousClass <T> {

    private Map<Key, Type<T>> map = new HashMap<Key, Type<T>>();

    public void putType(Key key, Type<T> type){
        map.put(type);
    }

    public Type<T> getType(Key key){
        map.get(key);
    }
}

FYI, at this point you have the delegation pattern in play.

Your example client code would need to declare two instances of ObliviousClass: ObliviousClass<String> and ObliviousClass<Integer>.

Edit:

If you must have a mixed bag of Types, you can impose a type on your method, but you'll get a compiler warning for an unsafe cast:

public class ObliviousClass {

    private final Map<Key, Type<?>> map = new HashMap<Key, Type<?>>();

    public void putType(Key key, Type<?> value) {
       map.put(key, value);
    }

    @SuppressWarnings("unchecked")
    public <T> Type<T> getType1(Key key, Class<T> typeClass) {
       return (Type<T>)map.get(key); 
    }

    @SuppressWarnings("unchecked")
    public <T> Type<T> getType2(Key key) {
        return (Type<T>) map.get(key);
    }
}

Clients can type the calls to these methods like this:

Type<Integer> x = obliviousClass.getType1(key, Integer.class);
Type<Integer> y = obliviousClass.<Integer>getType2(key);

Take your pick as to which one you prefer and use that.




回答3:


For those landing on this question these many years later, this is not how Java generics are designed to be used. (I was going to comment but had more to details.)

The generic pattern manages a single parent class per type ID rather than multiple different classes. If we consider the simpler List<T>, a list of strings OR integers (as List<String> or List<Integer>) is how generics are defined. One class per type. This way, there is a consistent type when the values are referenced. Storing unrelated types would be the same as List<Object>. Only the programmer can know when multiple types are stored and how to retrieve them with casting.

It would be ok to store subclasses to a parent class, but when accessed from the collection without casting, the parent class contact is all that is known. For instance, a generic collection defined with an interface like Map<String, Runnable>. However, only the run() method is visible even if other public methods are added to implementations (unless the programmer explicitly casts). To access additional methods, casting is necessary.

This is a limitation in Java. A language could be defined to know the L-Value type - even Java. But it wasn't. When new features are added, there are many backward compatible considerations [Sun and] Oracle take into account. Code compiled with generics was designed to run on older JVMs with type erasure. Java uses type erasure at compile time once it has determined that the generics are consistently reference. The bytecode uses Object as if the instance was (sort of) defined as List. If the choice was made to abandon backward compatibility, like Java 9 and 11, then multiple types might have been workable.



来源:https://stackoverflow.com/questions/10507466/avoiding-returning-wildcard-types

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!