1

I hope to find the declaring class of a method in Android. The basic idea is to find if the target method is declared in a class. If not, I will recursively find the target method in its super classes. For a specific reason, efficiency matters. So I have two implementations and I compared their performance differences.

Version 1: use the API Class.getDeclaredMethod to check if a method exists in a class. If not, NoSuchMethodException will be thrown.

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;
}

Version 2: Manually check if a method is declared in a class using Class.getDeclaredMethods. We will match each method one by one with the method name and parameter types.

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;
}

I made some interesting observations when testing the efficiency of these two versions. Basically I created several empty classes C, CC, CCC, CCCC. Their relationships are

CC extends C
CCC extends CC
CCCC extends CCC

All these classes does not declare any methods and I use the toString method as the target method. I testing two getDeclaringClass with a loop of 10000 times:

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");

Here are the results:

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

We can see that version 1's performance drops significantly when we have more levels of class inheritance. But version 2's performance does not change a lot.

So what makes the difference between version 1 and 2? What makes version 1 so slow?

Richard Hu
  • 811
  • 5
  • 18
  • 2
    https://stackoverflow.com/questions/504103/how-do-i-write-a-correct-micro-benchmark-in-java – shmosel Feb 16 '22 at 08:16
  • 1
    What’s the point of the manual superclass traversal? Use `cls.getMethod("toString").getDeclaringClass()` and you have the declaring class. – Holger Feb 17 '22 at 13:16
  • @Holger because `getMethod` does not return protected or private methods – Richard Hu Feb 17 '22 at 14:09
  • 1
    Private methods of different classes have no relationship at all, even if their defining classes happen to have a superclass relationship, so finding “the declaring class of a method” is pointless for private methods. Protected methods do indeed override other methods, but does you task really include processing protected methods? – Holger Feb 17 '22 at 14:23
  • @Holger Unfortunately yes... – Richard Hu Feb 17 '22 at 14:52

1 Answers1

1

Your method of measuring is not a valid benchmark. Some of the issues can be found in How do I write a correct micro-benchmark in Java?

But while the results are subject to errors or noise, they are not implausible.

You have created test classes with no methods, so getDeclaredMethods() will return an empty array. This implies that nothing happening in the loop body of your second variant matters at all for these classes—it’s never executed. Iterating over an empty array takes almost no time at all, so the actual time is always spend in the last class, java.lang.Object, which has declared methods, including the one you’re searching for.

In contrast, the first variant constructs and delivers a new NoSuchMethodException every time, the method has not been found in a class. As long as this overhead does not get optimized away (and you should not rely on this to happen), it’s a rather expensive operation.

As explained in The Exceptional Performance of Lil' Exception, a significant portion of the costs is the construction of the stack trace, which depends on the depth of the call stack. Since your method processes the class hierarchy recursively, these costs grow worse than linear with deeper class hierarchies.

It’s an implementation detail and I don’t know how Android’s environment has implemented it, but getDeclaredMethod does not necessarily work better than a linear search. In case of OpenJDK it does not, as it is still performing a linear search under the hood. Building a data structure allowing a better-than-linear lookup would cost time on its own but only pay off, if an application is performing a lot of reflective lookups on the same class, which rarely happens.

So for such implementations, you have a linear search in either variant. The second has the costs of cloning the array of methods, as getDeclaredMethods() returns a defensive copy, but this is far less than the costs of the creation of exceptions in the first variant.

In an operation like this, when you expect the method to be absent in some classes of the hierarchy, you are better of with the manual search, preventing exceptions.

Still, you can simplify the operation:

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;
}

This uses a loop rather than recursion and the already existing array comparison method. But it does a pre-test for the array length. So when the lengths already differ, it avoids the creation of a defensive copy of the parameter types array. This might not always be necessary, depending on the optimization capabilities of the runtime environment, but it won’t hurt to be included in code which, as you said, is performance critical.

Holger
  • 285,553
  • 42
  • 434
  • 765