最近在寫一個專案,里面需要頻繁使用反射操作,由于Java的反射API使用起來比較復雜,所以我決定把常用的反射操作封裝成一個工具類:ReflectUtils,
在ReflectUtils中,有這么一個call方法:
public static <T> T call(Object obj, String methodName, Object... params);
這個方法利用反射呼叫某個實體物件的某個方法,obj是物件實體,methodName是方法名,params是傳遞給方法的引數,
最初這個方法是這么來實作的:
public static <T> T call(Object obj, String methodName, Object... params)
{
try
{
Method method = obj.getClass().getMethod(methodName, getTypes(params));
return (T) method.invoke(obj, params);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
這個實作看起來很簡單,只是把反射獲取方法和呼叫方法的程序簡單封裝了一下,其中getTypes方法用于獲取引數型別串列:
private static Class<?>[] getTypes(Object... params)
{
return Arrays.stream(params).map(Object::getClass).toArray(Class<?>[]::new);
}
不過很快就發現了問題,假設我要呼叫某個String物件的substring方法:
String s1 = "hello";
String s2 = ReflectUtils.call(s1, "substring", 1, 4);
預期s2的值應該為"ell",但是上面的代碼執行中卻拋出了例外:
Exception in thread "main" java.lang.RuntimeException: java.lang.NoSuchMethodException: java.lang.String.substring(java.lang.Integer,java.lang.Integer)
從例外資訊不難推斷,String的substring方法的兩個引數都是int型別,但是當我們把兩個基本型別int通過Object...傳入call時,int被包裝成了Integer,與substring的引數型別不匹配,也就是說,凡是呼叫含有基本型別引數的函式,call函式都會失敗,
這可怎么辦呢?我想到了下面這個看起來十分“暴力”但是有用的解決方案:
public static <T> T call(Object obj, String methodName, Object... params)
{
try
{
Method method = obj.getClass().getMethod(methodName, getTypes(params));
return (T) method.invoke(obj, params);
}
catch (Exception e)
{
// 遍歷obj中的每一個方法
for (Method method : obj.getClass().getMethods())
{
try
{
// 篩選方法名和引數數量相同的方法
if (method.getName().equals(methodName) &&
method.getParameterCount() == params.length)
return (T) method.invoke(obj, params);
}
catch (Exception ignored) {}
}
// 找不到方法
throw new RuntimeException(e);
}
}
簡單地說,如果getMethod找不到匹配的方法,那么就直接遍歷物件中所有方法名等于methodName且引數數量等于params長度的方法,并依次呼叫這些方法,如果呼叫成功就直接回傳,
這個實作雖然看起來效率有點低,但是好歹能湊合使用,所以我使用了很長一段時間,直到遇到下面這個需求:
提前獲取方法呼叫的回傳值型別,而不實際呼叫這個方法,
例如,我想要知道將引數1和4(兩個int型別的實參)傳入String的substring方法后,方法回傳值的型別:
Class<?> returnType = ReflectUtils.getReturnType(String.class, "substring", 1, 2);
上面代碼的預期輸出是String.class,
可以想象,這個getReturnType方法的簽名一定是下面這樣的:
public static Class<?> getReturnType(Class<?> type, String methodName, Object... params);
一個很容易想到的實作:
public static Class<?> getReturnType(Class<?> type, String methodName, Object... params)
{
try
{
Method method = type.getMethod(methodName, getTypes(params));
return method.getReturnType();
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
事實上,這個實作是錯誤的,原因與上面的call方法類似,因為Java的自動裝箱機制,當我們把兩個int通過Object...傳入時,int會被包裝成Integer,getReturnType內部使用getMethod來查找方法,它實際執行的是下面這條陳述句:
type.getMethod("substring", Integer.class, Integer.class);
而不是我們期望的:
type.getMethod("substring", int.class, int.class);
當然什么也找不到了,萬惡的自動裝箱!
而且,在這種情況下,也不可能像call一樣依次嘗試呼叫type中的所有方法,因為我們僅僅只是想獲取方法的回傳值,而不希望真正呼叫這個方法,
下面記錄一下我的解決方案,
首先在一個Map中存盤基本型別與包裝型別的對應關系:
private static final Map<Class<?>, Class<?>> primitiveAndWrap = new HashMap<>();
static
{
primitiveAndWrap.put(byte.class, Byte.class);
primitiveAndWrap.put(short.class, Short.class);
primitiveAndWrap.put(int.class, Integer.class);
primitiveAndWrap.put(long.class, Long.class);
primitiveAndWrap.put(float.class, Float.class);
primitiveAndWrap.put(double.class, Double.class);
primitiveAndWrap.put(char.class, Character.class);
primitiveAndWrap.put(boolean.class, Boolean.class);
}
isPrimitive方法用于判斷一個型別是不是基本型別:
public static boolean isPrimitive(Class<?> type)
{
return primitiveAndWrap.containsKey(type);
}
getWrap方法用于將基本型別轉換為對應的包裝型別:
public static Class<?> getWrap(Class<?> type)
{
if (!isPrimitive(type)) return type;
return primitiveAndWrap.get(type);
}
match方法用于判斷actualType是否能被賦值給declaredType,注意,在進行isAssignableFrom判斷前,使用getWrap抹平了基本型別與包裝型別之間的差距:
private static boolean match(Class<?> declaredType, Class<?> actualType)
{
return getWrap(declaredType).isAssignableFrom(getWrap(actualType));
}
進一步實作一個判斷型別陣列的match方法:
private static boolean match(Class<?>[]c1, Class<?>[] c2)
{
if (c1.length == c2.length)
{
for (int i = 0; i < c1.length; ++i)
{
if (!match(c1[i], c2[i])) return false;
}
return true;
}
return false;
}
接著實作一個getMethod方法:
private static Method getMethod(Class<?> type, String name, Class<?>[] parameterTypes)
{
try
{
return type.getMethod(name, parameterTypes);
}
catch (Exception e)
{
for (Method method : type.getMethods())
{
if (method.getName().equals(name) && method.getParameterCount() == parameterTypes.length)
if (match(method.getParameterTypes(), parameterTypes))
return method;
}
throw new RuntimeException(e);
}
}
這個方法十分關鍵,它用來從某個型別中獲取滿足條件的方法,在getMethod內部,首先嘗試用Class的getMethod來獲取方法,如果獲取不到,則遍歷type中所有具有指定方法名和指定引數個數的方法,并判斷該方法的引數型別是否與parameterTypes匹配(使用上面的match方法),即實參型別能否賦值給形參型別,
有了上面這些方法,就可以來實作getReturnType了:
public static Class<?> getReturnType(Class<?> type, String methodName, Object... params)
{
return getMethod(type, methodName, getTypes(params)).getReturnType();
}
call的實作也可改寫如下:
public static <T> T call(Object obj, String methodName, Object... params)
{
try
{
return (T) getMethod(obj.getClass(), methodName, getTypes(params)).invoke(obj, params);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
ReflectUtils的完整代碼:https://github.com/byx2000/ReflectUtils
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/263636.html
標籤:Java
