Tell compiler an <Object> is equivalent to the <?> it wants

Viewed 140

I have objects that pre-generate some configuration once so they can process computation later on faster (possibly several times). I'm trying to genericize it to avoid passing the configuration as an Object and casting it each time.

interface IComputable<T> {
    T configure(); // Generate configuration object
    int compute(T conf); // Run the computation based on the pre-generated configuration
    float precision(T conf); // Make the compute() computation finer-grained
    ...
}
class ComputableInfo {
    IComputable<?> computable;
    Object config; // Real type is <?>
    int result;

    ComputableInfo(String id) {
        computable = ComputableFactory.createFrom(id);
        config = computable.configure();
        result = computable.compute(config); // <<<--- The method compute(capture#3-of ?) in the type TestInterface.IComputable<capture#3-of ?> is not applicable for the arguments (Object)
    }
}

I'm getting a compilation error:

The method compute(capture#3-of ?) in the type TestInterface.IComputable<capture#3-of ?> is not applicable for the arguments (Object)

Of course, I can replace the int compute(T conf) by int compute(Object conf) but I would have to explicitly cast it to the appropriate T. It's not that big a problem, but it makes the code less obvious.

I could also make the ComputableInfo generic with

interface ComputableInfo<T> {
    IComputable<T> computable;
    T config;
    ...

but that would create compilation problems in some other places (mostly "raw types" warning) that I'd like to avoid more than the previous workaround (using Object instead of T).

Is there a way to achieve that? I'm even open to turning such problem from error to warning in the compiler settings, or maybe have an extra private method that would return both config and result in a single object?

EDIT: to add up on the "further compilation problems" if I make ComputableInfo generic: I have another method in the interface (see edited) that is called through the ComputableInfo:

ComputableInfo<?> info = getInfo(id);
info.computable.precision(info.config); // <<<--- (same kind of error)

The problem is that the ComputableInfo has no way to know the T type of Computable<T> (or no way I know), as it comes from a Factory that builds it from a configuration file.

2 Answers

Getting an object from a wildcarded type and passing it back to the same object, is a known limitation of the generic type system. E.g., when you have

List<?> list = …

you might want to copy an element from one index to another like

Object o = list.get(0);
list.set(1, o);

but it doesn’t work, even when you avoid the local variable of a non-denotable type. In other words, even the following does not compile:

list.set(1, list.get(0));

But you can add a generic helper method that does the operation by allowing to capture the wildcard type in a type parameter for the duration of the operation:

static <T> void copyFromTo(List<T> l, int from, int to) {
    l.set(to, l.get(from));
}
List<?> list = …
copyFromTo(list, 0, 1); // now works

You can apply this pattern to your case as well:

class ComputableInfo {
    IComputable<?> computable;
    Object config; // Real type is <?>
    int result;

    ComputableInfo(String id) {
        computable = ComputableFactory.createFrom(id);
        configureAndCompute(computable);
    }

    private <T> void configureAndCompute(IComputable<T> computable) {
        T typedConfig = computable.configure();
        this.config = typedConfig;
        this.result = computable.compute(typedConfig);
    }
}

This works and does not require making ComputableInfo generic.

If you need to capture the type for longer than a single method, e.g. if you want to use the created config multiple times, you can use encapsulation:

class ComputableInfo {
    static final class CompState<T> {
        IComputable<T> computable;
        T config;

        CompState(IComputable<T> c) {
            computable = c;
        }
        private void configure() {
            config = computable.configure();
        }
        private int compute() {
            return computable.compute(config);
        }
    }
    CompState<?> state;
    int result;

    ComputableInfo(String id) {
        state = new CompState<>(ComputableFactory.createFrom(id));
        state.configure();
        result = state.compute();
    }
}

That way, you still avoid exporting the type parameter to users of ComputableInfo.

You need to use a lower-bounded wildcard. The Object is not compliant with a wildcard ? itself.

class ComputableInfo {
    IComputable<? super Object> computable;
    Object config;
    int result;

    ComputableInfo(String id) {
         computable = null;
         config = computable.configure();
         result = computable.compute(config);
    }
}

A lower bound states that IComputable will be an instance of Object or an instance of some object being a superclass of Object (which actually the object is a parent of all Objects). For the better understanding, let's use rather Number:

IComputable<Integer> computableInteger = ...;
IComputable<Number> computableNumber = ...;
IComputable<Object> computableObject = ...;

IComputable<? super Number> computableSuperNumber = ...;
computableSuperNumber = computableInteger;                  // doesn't compile
computableSuperNumber = computableNumber;                   // ok
computableSuperNumber = computableObject;                   // ok

However, it is safe to pass an Integer or Double into a method of that instance through. In a snippet below computableSuperObject references an IComputable that could be one of:

  • IComputable<Number>
  • IComputable<Object>.

Since the reference might be IComputable<Number>, computing with Object is illegal as long it doesn't fit there as long as Object can be ex. String.

IComputable<? super Number> computableSuperNumber = ...;

Integer integer = 1;
Double d = 1d;
Number number = 1;
Object object = 1;                     // the Object can be also "string", see below
Object objectString = "string";
String string = "string";

computableSuperNumber.compute(integer);                     // ok
computableSuperNumber.compute(d);                           // ok
computableSuperNumber.compute(number);                      // ok
computableSuperNumber.compute(object);                      // doesn't compile
computableSuperNumber.compute(objectString);                // doesn't compile
computableSuperNumber.compute(string);                      // doesn't compile
Related