Reflection on methods with a primitive numeric return type

maasg picture maasg · Aug 1, 2012 · Viewed 9.2k times · Source

I'm currently working on a small framework to collect metrics in an OSGi system. The core of it is an annotation @Metric that indicates that a given method of a service can deliver a metric (e.g. numeric value) when asked.

Those methods would look like:

@Metric
public int getQueueSize() {...}

or

@Metric
public double getAvgTaskTime() {...}

I'm using reflection to inspect the service implementation class and register the methods that are annotated with @Metric. As a sanity check, I'm checking that the method indeed delivers a numeric value. I tried this and failed:

for (Method method: metricSource.getClass().getMethods()) {
    if (method.isAnnotationPresent(Metric.class) &&  Number.class.isAssignableFrom(method.getReturnType()) { 
        // add method for later process
    }

Then, later on the processor, I'd do:

Number value = (Number) method.invoke(target, obj[]);

It turns out that for a primitive type you would get e.g. int.class and that's not assignable to Number.class whereas the boxed type Integer.class would be. Damn. Move on.

Then I created a Map<Class<?>, Extractor> where Extractor takes a class and casts a parameter into that class:

public NumberExtractor(Class<? extends Number> clazz) {
    this.clazz = clazz;
    }       
    public double extract(Object val) {
        Number nbr = clazz.cast(val);
        return nbr.doubleValue();
    }
}

In face of the previous observation, I added an entry like this:

extractorMap.put(int.class, new NumberExtractor(int.class));

But that didn't work either. It gave a runtime class cast exception saying that Integer.class could not be cast to int. Note also that the compiler does not complain on new NumberExtractor(int.class), letting the boundary check Class<? extends Number> pass on int.class

At the end, this combination worked:

extractorMap.put(int.class, new NumberExtractor(Integer.class));

What is going on here? Reflection says that the return type of the object (int.class) is not assignable to Number.class, but when I go on with the method invoke, I actually get a Integer.class? WhiskeyTangoFoxtrot?!

I'm left wondering whether there is another way to address this issue other than maintaining the Map of int -> Integer, Integer -> Integer, float -> Float, Float -> Float? (which is now done, so this is for the sake of learning)

The behaviour of the boxing and type information doesn't seem coherent here.

Could anybody shine some light on this?

Answer

Jon Skeet picture Jon Skeet · Aug 1, 2012

Reflection says that the return type of the object (int.class) is not assignable to Number.class, but when I go on with the method invoke, I actually get a Integer.class?

Absolutely. The reflection API can't possibly return you an actual int, so it boxes the value in an Integer. What else could it do?

The fact that it has to box does not change the assignability from int to Number. Look at the docs for isAssignableFrom:

If this Class object represents a primitive type, this method returns true if the specified Class parameter is exactly this Class object; otherwise it returns false.

So it's behaving exactly according to the documentation.

I'm left wondering whether there is another way to address this issue other than maintaining the Map of int -> Integer, Integer -> Integer, float -> Float, Float -> Float? (which is now done, so this is for the sake of learning)

Yup, that sounds right to me. Or just have a Set<Class> for primitive numeric types rather than explicitly mapping.