What is the new way of getting all methods of a cl

2019-03-23 16:23发布

I want to get all methods of a class, including public, protected, package and private methods, and including inherited methods.

Remember:

  • Class.getDeclaredMethods() gets public, protected, package and private methods, but excludes inherited methods.
  • Class.getMethods gets inherited methods, but only the public ones.

Before Java 8 we could do something along the lines of:

Collection<Method> found = new ArrayList<Method>();
while (clazz != null) {
    for (Method m1 : clazz.getDeclaredMethods()) {
        boolean overridden = false;

        for (Method m2 : found) {
            if (m2.getName().equals(m1.getName())
              && Arrays.deepEquals(m1.getParameterTypes(), m2
                  .getParameterTypes())) {
            overridden = true;
            break;
            }
        }
        if (!overridden) found.add(m1);
    }

    clazz = clazz.getSuperclass();
}
return found;

But now, if the class implements some interface with default methods which are not overridden by concrete superclasses, these methods will escape the above detection. Besides, there are now rules concerning default methods with the same name, and these rules must be taken into account as well.

Question: What is the current recommended way of getting all methods of a class:

The most common definition of "all" should be the methods that can be directly accessed inside an instance method of the class, without the use of super or class names:

  • Include public, protected, package and private methods declared in the class itself.
  • Include protected methods of its superclasses.
  • Include package methods of its superclasses of the same package.
  • Include default methods of its interfaces (those not overridden/hidden, see here and here).
  • Include static methods (class and superclasses) with the appropriate accessibility.
  • Don't include private methods of superclasses.
  • Don't include overridden methods.
  • Don't include hidden methods (in special, don't include hidden static methods).
  • Don't include synthetic/bridge methods.
  • Don't include methods not allowed by Java, even if the JVM allows them.

So, the above definition fits the following signature when both boolean flags are false:

public Collection<Method> getAllMethods(Class clazz,
                               boolean includeAllPackageAndPrivateMethodsOfSuperclasses,
                               boolean includeOverridenAndHidden)

The ideal, canonical answer, should allow for these boolean flags.

2条回答
男人必须洒脱
2楼-- · 2019-03-23 16:52

I was not able to compile Holger's answer in an Android environment since MethodType is added in API level 26 and Android Studio supports a subset of Java 8 Language Features. In addition to this, Holger's code contains to much lambdas and streams, I consider those as human unreadable. So I decided to write a more readable code that works in any Java environment. But it's not an ideal solution since I did not include the flags.

Below snippets work same as if you called getAllMethods(clazz, false, false)

private static Collection<Method> getAllMethods(Class<?> target) {
    Class<?> clazz = target;
    Collection<MethodSignature> methodSignatures = new ArrayList<>();
    for(Method method : clazz.getDeclaredMethods()) {
        addIfAbsentAndNonSynthetic(methodSignatures, method);
    }
    for(Method method : clazz.getMethods()) {
        addIfAbsentAndNonSynthetic(methodSignatures, method);
    }
    Package pkg = clazz.getPackage();
    clazz = clazz.getSuperclass();
    while(clazz != null) {
        for(Method method : clazz.getDeclaredMethods()) {
            int modifier = method.getModifiers();
            if(Modifier.isPrivate(modifier)) {
                continue;
            }
            if(Modifier.isPublic(modifier) || Modifier.isProtected(modifier)) {
                addIfAbsentAndNonSynthetic(methodSignatures, method);
            }
            else if((pkg != null && pkg.equals(clazz.getPackage())) || (pkg == null
                    && clazz.getPackage() == null)) {
                addIfAbsentAndNonSynthetic(methodSignatures, method);
            }
        }
        clazz = clazz.getSuperclass();
    }
    Collection<Method> allMethods = new ArrayList<>(methodSignatures.size());
    for(MethodSignature methodSignature : methodSignatures) {
        allMethods.add(methodSignature.getMethod());
    }
    return allMethods;
}

private static void addIfAbsentAndNonSynthetic(Collection<MethodSignature> collection,
        Method method) {
    MethodSignature methodSignature = new MethodSignature(method);
    if(!method.isSynthetic() && !collection.contains(methodSignature)) {
        collection.add(methodSignature);
    }
}

Two of the components of a method declaration comprise the method signature: The method's name and the parameter types. The compiler does not consider return type when differentiating methods, so you can not declare two methods with the same signature even if they have a different return type. So MethodSignature class does not hold any reference to the return type of it's method.

But when you invoke getDeclaredMethods or getMethods it is possible to get multiple declared methods with the same name and parameter types, but different return types. This means that the compiler created a synthetic method, called a bridge method. To solve this problem, invoke method.isSynthetic() on the method, if it returns true skip it. Since it is a synthetic method there will be a non synthetic one with the same signature but different return type.

public class MethodSignature {
    private final Method mMethod;
    private final String mName;
    private final Class<?>[] mParameterTypes;

    public MethodSignature(Method method) {
        mMethod = method;
        mName = mMethod.getName();
        mParameterTypes = mMethod.getParameterTypes();
    }

    public Method getMethod() {
        return mMethod;
    }

    public String getName() {
        return mName;
    }

    public Class<?>[] getParameterTypes() {
        return mParameterTypes;
    }

    @Override
    public boolean equals(Object object) {
        if(this == object) {
            return true;
        }
        if(object == null) {
            return false;
        }
        if(!getClass().equals(object.getClass())) {
            return false;
        }
        MethodSignature obj = (MethodSignature) object;
        if(hashCode() != obj.hashCode()) {
            return false;
        }
        return mName.equals(obj.getName()) && Arrays
                .equals(mParameterTypes, obj.getParameterTypes());
    }

    @Override
    public int hashCode() {
        int hash = 11;
        hash = 37 * hash + Objects.hash(mName, Arrays.hashCode(mParameterTypes));
        return hash;
    }
}
查看更多
淡お忘
3楼-- · 2019-03-23 16:59

Even for the “Before Java 8” scenario, your code snippet isn’t correct. But collecting all methods isn’t a usual scenario though, as you normally need methods regarding a certain context, e.g. you might want to know which methods are accessible for a given context, which doesn’t include all methods, even if you consider non-public methods. If you really want all methods, you have to recall that private and static methods are never overridden and package-private methods are only overridden when being declared within the same package. So it’s not correct to filter every encountered method signature.

What makes matters worse is that methods might get overridden with different modifiers. The latter can be solved by keeping the idea to start at the actual class and use Class.getMethods() to get all public method including default methods and traverse the superclass hierarchy towards java.lang.Object so the already encountered overrides have the least restrictive access modifiers.

As a side note, nesting linear search loops is never a good idea. You’ll soon end up with a quadratic or worse complexity.

You may collect methods using the following method:

public static Set<Method> getAllMethods(Class<?> cl) {
    Set<Method> methods=new LinkedHashSet<>();
    Collections.addAll(methods, cl.getMethods());
    Map<Object,Set<Package>> types=new HashMap<>();
    final Set<Package> pkgIndependent = Collections.emptySet();
    for(Method m: methods) types.put(methodKey(m), pkgIndependent);
    for(Class<?> current=cl; current!=null; current=current.getSuperclass()) {
        for(Method m: current.getDeclaredMethods()) {
            final int mod = m.getModifiers(),
                access=Modifier.PUBLIC|Modifier.PROTECTED|Modifier.PRIVATE;
            if(!Modifier.isStatic(mod)) switch(mod&access) {
                case Modifier.PUBLIC: continue;
                default:
                    Set<Package> pkg=
                        types.computeIfAbsent(methodKey(m), key -> new HashSet<>());
                    if(pkg!=pkgIndependent && pkg.add(current.getPackage())) break;
                    else continue;
                case Modifier.PROTECTED:
                    if(types.putIfAbsent(methodKey(m), pkgIndependent)!=null) continue;
                    // otherwise fall-through
                case Modifier.PRIVATE:
            }
            methods.add(m);
        }
    }
    return methods;
}

private static Object methodKey(Method m) {
    return Arrays.asList(m.getName(),
        MethodType.methodType(m.getReturnType(), m.getParameterTypes()));
}

But as said, it might be the case that it isn’t suitable for whatever you want to do. You should ask yourself the following questions first:

  • Are you looking for methods that make up the API (that’s usually public and protected only)?
  • Or do you want to actually see methods accessible for a certain class/package context?
  • Shall static methods be included?
  • Shall synthetic/bridge methods be included?
  • etc.

Here is the revised method adapted to your more specific request:

public static Collection<Method> getAllMethods(Class clazz,
                boolean includeAllPackageAndPrivateMethodsOfSuperclasses,
                boolean includeOverridenAndHidden) {

    Predicate<Method> include = m -> !m.isBridge() && !m.isSynthetic() &&
         Character.isJavaIdentifierStart(m.getName().charAt(0))
      && m.getName().chars().skip(1).allMatch(Character::isJavaIdentifierPart);

    Set<Method> methods = new LinkedHashSet<>();
    Collections.addAll(methods, clazz.getMethods());
    methods.removeIf(include.negate());
    Stream.of(clazz.getDeclaredMethods()).filter(include).forEach(methods::add);

    final int access=Modifier.PUBLIC|Modifier.PROTECTED|Modifier.PRIVATE;

    Package p = clazz.getPackage();
    if(!includeAllPackageAndPrivateMethodsOfSuperclasses) {
        int pass = includeOverridenAndHidden?
            Modifier.PUBLIC|Modifier.PROTECTED: Modifier.PROTECTED;
        include = include.and(m -> { int mod = m.getModifiers();
            return (mod&pass)!=0
                || (mod&access)==0 && m.getDeclaringClass().getPackage()==p;
        });
    }
    if(!includeOverridenAndHidden) {
        Map<Object,Set<Package>> types = new HashMap<>();
        final Set<Package> pkgIndependent = Collections.emptySet();
        for(Method m: methods) {
            int acc=m.getModifiers()&access;
            if(acc==Modifier.PRIVATE) continue;
            if(acc!=0) types.put(methodKey(m), pkgIndependent);
            else types.computeIfAbsent(methodKey(m),x->new HashSet<>()).add(p);
        }
        include = include.and(m -> { int acc = m.getModifiers()&access;
            return acc!=0? acc==Modifier.PRIVATE
                    || types.putIfAbsent(methodKey(m), pkgIndependent)==null:
                noPkgOverride(m, types, pkgIndependent);
        });
    }
    for(clazz=clazz.getSuperclass(); clazz!=null; clazz=clazz.getSuperclass())
        Stream.of(clazz.getDeclaredMethods()).filter(include).forEach(methods::add);
    return methods;
}
static boolean noPkgOverride(
        Method m, Map<Object,Set<Package>> types, Set<Package> pkgIndependent) {
    Set<Package> pkg = types.computeIfAbsent(methodKey(m), key -> new HashSet<>());
    return pkg!=pkgIndependent && pkg.add(m.getDeclaringClass().getPackage());
}
private static Object methodKey(Method m) {
    return Arrays.asList(m.getName(),
        MethodType.methodType(m.getReturnType(), m.getParameterTypes()));
}
查看更多
登录 后发表回答