Unboxing and Reflection in Java?
I have a method with the signature public void setFoo(int newFoo)
in a model I'm writing. I'm using the following code to call it from within my controller:
protected void setModelProperty(String propertyName, Object newValue) {
for (AbstractModel model: registeredModels) {
try {
Method method = model.getClass().
getMethod("set"+propertyName, new Class[] {
newValue.getClass()
}
);
method.invoke(model, newValue);
} catch (Exception ex) {
// Handle exception.
System.err.println(ex.toString());
}
}
}
Calling this method like controller.setModelProperty("Foo",5);
results in an exception being thrown: java.lang.NoSuchMethodException: foo.bar.models.FooModel.setFoo(java.lang.Integer)
-- it looks l开发者_如何学Goike the int
is being boxed as an Integer
, which doesn't match the signature of setFoo.
Is there any way to convince this reflection code to pass 5
(or whatever int I pass in) as an Integer, without the boxing? Or do I have to create public void setFoo(Integer newFoo)
in my model and unbox explicitly, then call the original setFoo?
You could specialise your setModelProperty
for any primitives you expect to be used with:
protected void setModelProperty(String propertyName, int newValue) { /* ... */ }
Alternatively, you could use instanceof on newValue to check for boxed primitives:
Class[] classes;
if (newValue instanceof Integer) {
classes = new Class[] { int.class };
} else if (newValue instanceof Double) {
/* etc...*/
} else {
classes = new Class[] {newValue.getClass() };
}
Or finally, if you have the source for setFoo, you could change it to take a boxed Integer instead of an int - the overhead is usually negligible.
Is there any way to convince this reflection code to pass 5 (or whatever int I pass in) as an Integer, without the boxing?
Not while your method signature says Object newValue
, because an int can never be an Object
. A way to keep the method generic would be to have callers pass in the type explicitly, i.e.:
protected void setModelProperty(String propertyName, Object newValue, Class type) {
Alternatively, you could test the type of newValue
to see if it's a primitive wrapper and in that case look for both the primitive and the wrapped version of the method. However, that won't work when the user passes in a null
. Actually, the method won't work at all in that case...
newValue is being boxed as an Integer when setModelProperty() is called. This is the only way it can be called; 'int' is not an instanceof Object. newValue.getClass() returns "Integer" and thus the call to getMethod() fails.
If you want to work with primitives here you'll need a special version of setModelProperty. Alternatively you could write a method setFoo(Integer).
Or more generally you could write:
if (newValue.getClass()==Integer.class) {
// code to look for a method with an int argument
}
Problem is not on invoking but on getting the method, as you are using Integer.class which is different from int.class.
You can fix it by doing a second call to get method if the first one fails:
protected void setModelProperty(String propertyName, Object newValue) {
for (AbstractModel model: registeredModels) {
Method method = null;
try {
method = model.getClass().
getMethod("set"+propertyName, new Class[] {
newValue.getClass()
}
);
} catch (Exception ex) {
try {
if (canBoxPrimitive(newValue.getClass()) {
method = model.getClass().
getMethod("set"+propertyName, new Class[] {
primitiveClass(newValue.getClass())
}
}
}
catch (Exception ex) {
// handle it
}
}
method.invoke(model, newValue);
}
}
For options to code the functions: boolean canBoxPrimitive(Class) and Class primitiveClass(Class) you can take a look here: Dynamically find the class that represents a primitive Java type
Outoboxing will be done, but you are looking for the wrong method. The parameter type array is not correct. For int the type would be Integer.TYPE instead of Integer.class.
Try some like the following to get it working regardless of the parameer type (outoboxing will do it):
protected void setModelProperty(String propertyName, Object newValue) {
for (AbstractModel model: registeredModels) {
try {
Method method = findSetter(model.getClass(), propertyName);
method.invoke(model, newValue);
} catch (Exception ex) {
// Handle exception.
System.err.println(ex.toString());
}
}
}
private Method findSetter(Class<?> type, String propertyName) {
for (Method methos : type.getMethods()) {
if (method.getName().equalsIgnoreCase("set" + propertyName) && method.getParameterTypes().length == 1) {
return method;
}
}
throw new NoSuchMethodError("No setter for " + propertyName);
}
Did you try
Integer.valueOf(5)
to pass 5 along as an Integer instead of an int?
精彩评论