Help with Java Generics: Cannot use “Object” as ar

2019-05-10 13:28发布

问题:

I have the following code:

import java.util.*;

public class SellTransaction extends Transaction {
    private Map<String,? extends Object> origValueMap;
    public SellTransaction(Map<String,? extends Object> valueMap) {
        super(Transaction.Type.Sell);
        assignValues(valueMap);
        this.origValueMap=valueMap;
    }
    public SellTransaction[] splitTransaction(double splitAtQuantity) {
        Map<String,? extends Object> valueMapPart1=origValueMap;
        valueMapPart1.put(nameMappings[3],(Object)new Double(splitAtQuantity));
        Map<String,? extends Object> valueMapPart2=origValueMap;
        valueMapPart2.put(nameMappings[3],((Double)origValueMap.get(nameMappings[3]))-splitAtQuantity);
        return new SellTransaction[] {new SellTransaction(valueMapPart1),new SellTransaction(valueMapPart2)};
    }
}

The code fails to compile when I call valueMapPart1.put and valueMapPart2.put, with the error:

The method put(String, capture#5-of ? extends Object) in the type Map is not applicable for the arguments (String, Object)

I have read on the Internet about generics and wildcards and captures, but I still don't understand what is going wrong. My understanding is that the value of the Map's can be any class that extends Object, which I think might be redundant, because all classes extend Object. And I cannot change the generics to something like ? super Object, because the Map is supplied by some library.

So why is this not compiling? Also, if I try to cast valueMap to Map<String,Object>, the compiler gives me that 'Unchecked conversion' warning.

Thanks!

回答1:

If the library specifies extends then they are explicitly disallowing put. You should defensively copy before modifying, since they can quite legitimately change their return type to be immutable in a new version. If copying is expensive, then you can try creating a map type that is of type <String, Object> that first queries their map, and then queries some map you create that has your local modifications.

If you do know that their return type is immutable and that you solely own it, then the @SuppressWarnings("unchecked") annotations is a legitimate way to work around the warning, but I would double check those assumptions and comment extensively.

To understand extends vs super, look at it this way. Since the value can be any type that extends Object, the following is valid.

Map<String, Number> strToNum = new HashMap<String, Number>();
strToNum.put("one", Integer.valueOf(1));  // OK

Map<String, String> strToStr = new HashMap<String, String>();
strToStr.put("one", "1");  // OK

Map<String, ? extends Object> strToUnk = randomBoolean() ? strToNum : strToStr;
strToUnk.put("null", null);  // OK.  null is an instance of every reference type.
strToUnk.put("two", Integer.valueOf(2));  // NOT OK.  strToUnk might be a string to string map
strToUnk.put("two", "2");  // NOT OK.  strToUnk might be a string to number map

So put doesn't really work with the extends boundary types. But it works perfectly well with reading operations like get:

Object value = strToUnk.get("one");  // We don't know whether value is Integer or String, but it is an object (or null).

If you want a map to primarily use with "put" instead of "get", then you can use "super" instead of extends as in:

Map<String, Number> strToNum = new HashMap<String, Number>();
Map<String, Object> strToObj = new HashMap<String, Object>();

Map<String, ? super Number> strToNumBase;
if (randomBoolean()) {
  strToNumBase = strToNum;
} else {
  strToNumBase = strToObj;
}

// OK.  We know that any subclass of Number can be used as values.
strToNumBase.put("two", Double.valueOf(2.0d));

// But now, gets don't work as well.
Number n = strToNumBase.get("one");  // NOT OK. 


回答2:

As far as I know, bounded widecards, i.e. ? extends Number, is not used for variables or fileds. It is commonly used for arguments of method.

Let's first consider a case without generic type.

public void method(List<Number> list) {
}

Example usages:

method(new List<Double>()); // <-- Java compiler complains about this
method(new List<Number>()); // <-- Java compiler is happy with this.

You can only pass a List of Number but not the List of Double to this method even if Double is subclass of Number.

The widecard generic can be used here to tell java compiler that this method can accept any list of subclass of Number.

public void method(List<? extends Number> list) {
}

Example usages:

method(new List<Double>()); // <-- Java compiler is happy with this.
method(new List<Number>()); // <-- Java compiler is happy with this.

However, you will no longer be able to modify the list object, e.g.

public void method(List<? extends Number> list) {
    list.add(new Double()); // this is not allowed
}

The above list now have type of "unknown subtype of Number" which can be List, List, List, etc. Adding a Double object to the list of unknown type is certainly unsafe. To illustrate this point, a call to method is

method(new ArrayList<Integer>());

...
public void method(List<? extends Number> list) {
    // adding Double to Integer list does not make sense.
    list.add(new Double()); // compiler error
}

For variables and fields, you normally don't use bounded widecards, you can do

private Map<String, Object> origValueMap;

...

Map<String, Object> valueMapPart1 = origValueMap;
valueMapPart1.put(nameMappings[3], new Double(splitAtQuantity));

Note: there is no need to cast new Double(splitAtQuantity) to its super type, e.g Number or Object.



回答3:

This really goes to an old object-oriented gotcha. At first glance, it would seem that a "bag of apples" is a subclass of a "bag of fruit" but it is not. With object-oriented code, you can always use a subclass in place of a superclass (which is called the Liskov Substitution Principle). A bag of apples breaks this because it will not accept an orange whereas a bag of fruit would accept an orange.

In the terms of the question, Collection<?> could be a Collection<Object> (which would accept your Double) or it could be a Collection<Integer> (which would not).