zoukankan      html  css  js  c++  java
  • Java反射中与自动装箱有关的坑及其解决方案

    最近在写一个项目,里面需要频繁使用反射操作。由于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)
    

    从异常信息不难推断,Stringsubstring方法的两个参数都是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长度的方法,并依次调用这些方法,如果调用成功就直接返回。

    这个实现虽然看起来效率有点低,但是好歹能凑合使用,所以我使用了很长一段时间,直到遇到下面这个需求:

    提前获取方法调用的返回值类型,而不实际调用这个方法。

    例如,我想要知道将参数14(两个int类型的实参)传入Stringsubstring方法后,方法返回值的类型:

    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会被包装成IntegergetReturnType内部使用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内部,首先尝试用ClassgetMethod来获取方法。如果获取不到,则遍历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

  • 相关阅读:
    成功熬了四年还没死?一个IT屌丝创业者的深刻反思
    史氏语录
    WEB安全攻防学习内容
    从程序员的角度谈创业三年
    Windows2008 R2修改3389端口教程
    Win2008R2 zip格式mysql 安装与配置
    制作支持UEFI PC的Server2008 R2系统安装U盘
    郎科U208(主控 PS2251-50 HYNIX H27UCG8T2MYR)量产还原
    自用有线IP切换
    自动配置IP地址.bat
  • 原文地址:https://www.cnblogs.com/baiyuxuan/p/14446356.html
Copyright © 2011-2022 走看看