具有不同 class 继承级别的 getDeclaredMethod() 和 getDeclaredMethods() 之间的性能差异

Performance difference between getDeclaredMethod() and getDeclaredMethods() with different levels of class inheritance

希望在Android中找到一个方法的声明class。基本思想是查找目标方法是否在class中声明。如果没有,我会递归地在它的超 classes 中找到目标方法。出于特定原因,效率很重要。所以我有两个实现,我比较了它们的性能差异。

版本 1:使用 API Class.getDeclaredMethod 检查方法是否存在于 class 中。如果没有,将抛出 NoSuchMethodException

public Class getDeclaringClass1(Class cls, String mtdName, Class[] paraTypes) {
        Class declaringCls = null;
        try {
            declaringCls = cls.getDeclaredMethod(mtdName, paraTypes).getDeclaringClass();
        } catch (NoSuchMethodException | SecurityException e) {
            Class supr = cls.getSuperclass();
            if(supr != null) declaringCls = getDeclaringClass1(supr, mtdName, paraTypes);
        }
        return declaringCls;
}

版本 2:使用 Class.getDeclaredMethods 手动检查方法是否在 class 中声明。每个方法我们都会根据方法名和参数类型一一匹配。

public Class getDeclaringClass2(Class cls, String mtdName, Class[] paraTypes) {
        Class declaringCls = null;
        Method[] methods = cls.getDeclaredMethods();
        boolean containsMtd = false;
        for(Method method: methods) {
            if(method.getName().equals(mtdName)) {
                boolean allEqual = true;
                for(int i=0; i< method.getParameterTypes().length; i++) {
                    if(! method.getParameterTypes()[i].equals(paraTypes[i])) {
                       allEqual = false;
                       break;
                    }
                }
                if(allEqual) {
                    containsMtd = true;
                    declaringCls = cls;
                    break;
                }
            }
        }
        if(! containsMtd) {
           Class supr = cls.getSuperclass();
           if(supr != null) declaringCls = getDeclaringClass2(supr, mtdName, paraTypes);
        }
        return declaringCls;
}

我在测试这两个版本的效率时有一些有趣的观察。基本上我创建了几个空的 classes CCCCCCCCCC。他们的关系是

CC extends C
CCC extends CC
CCCC extends CCC

所有这些 classes 没有声明任何方法,我使用 toString 方法作为目标方法。我用 10000 次循环测试了两个 getDeclaringClass

start = System.currentTimeMillis();
for(long i=0; i<10000; i++) {
     getDeclaringClass(cls, "toString", new Class[0]).getName()); // getDeclaringClass will be set to version 1 or 2
}
end = System.currentTimeMillis();
System.out.println((end - start) + "ms");

结果如下:

cls Version 1 Version 2
C 1168ms 1632ms
CC 2599ms 1397ms
CCC 3495ms 1680ms
CCCC 4908ms 1559ms

我们可以看到,当我们拥有更多级别的 class 继承时,版本 1 的性能会显着下降。但是版本2的性能变化不大。

那么版本 1 和版本 2 之间有什么区别呢?是什么让版本 1 这么慢?

您的测量方法不是有效的基准。一些问题可以在 How do I write a correct micro-benchmark in Java?

中找到

但是,虽然结果可能存在错误或噪音,但它们并非不可信。

您创建了没有方法的测试 classes,因此 getDeclaredMethods() 将 return 一个空数组。这意味着对于这些 classes,第二个变体的循环体中发生的任何事情都无关紧要——它永远不会执行。遍历空数组几乎不花时间,所以实际时间总是花在最后 class、java.lang.Object 上,它声明了方法,包括您要搜索的方法。

相比之下,第一个变体每次都构造并传递一个新的NoSuchMethodException,该方法还没有在class中找到。只要这种开销没有得到优化(你不应该依赖它来实现),它就是一个相当昂贵的操作。

The Exceptional Performance of Lil' Exception 中所述,很大一部分成本是构建堆栈跟踪,这取决于调用堆栈的深度。由于您的方法以递归方式处理 class 层次结构,因此随着 class 层次结构的加深,这些成本比线性增长更糟。

这是一个实现细节,我不知道Android的环境是如何实现的,但是getDeclaredMethod不一定比线性搜索更好。对于 OpenJDK,它不会,因为它仍在后台执行线性搜索。构建允许 better-than-linear 查找的数据结构本身会花费时间,但只有在应用程序对同一个 class 执行大量反射查找时才会得到回报,这种情况很少发生。

因此,对于此类实现,您可以在任一变体中进行线性搜索。第二个具有克隆方法数组的成本,因为 getDeclaredMethods() return 是防御性副本,但这远低于第一个变体中创建异常的成本。

在这样的操作中,当您希望该方法在某些 class 层次结构中不存在时,您最好使用手动搜索,以防止出现异常。

不过,你可以简化操作:

public Class<?> getDeclaringClass2(Class<?> cls, String mtdName, Class<?>[] paraTypes) {
    for(; cls != null; cls = cls.getSuperclass()) {
        for(Method method: cls.getDeclaredMethods()) {
            if(method.getName().equals(mtdName)
            && method.getParameterCount() == paraTypes.length
            && Arrays.equals(method.getParameterTypes(), paraTypes)) 
                return cls;
        }
    }
    return null;
}

这使用循环而不是递归和已经存在的数组比较方法。但是它对数组长度做了一个 pre-test 。因此,当长度已经不同时,它避免了创建参数类型数组的防御性副本。这可能并不总是必要的,具体取决于运行时环境的优化能力,但如您所说,将其包含在对性能至关重要的代码中也无妨。