Каков новый способ получения всех методов класса, включая унаследованные методы по умолчанию Java 8?

Я хочу получить все методы класса, включая общедоступные, защищенные, пакетные и частные методы и включая унаследованные методы.

Помните:

  • Class.getDeclaredMethods() становится общедоступным, защищенным, пакетным и частным методы , но исключает унаследованные методы.
  • Class.getMethods получает унаследованные методы, , но только публичные.

До Java 8 мы могли бы что-то сделать в соответствии с:

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;

Но теперь, если класс реализует некоторый интерфейс с стандартными методами, которые не переопределяются конкретными суперклассами, эти методы избегают описанного выше обнаружения. Кроме того, в настоящее время существуют правила, касающиеся методов по умолчанию с тем же именем, и эти правила также должны быть приняты во внимание.

Вопрос: Каков текущий рекомендуемый способ получения всех методов класса:

Наиболее распространенным определением "все" должны быть методы, к которым можно напрямую обращаться внутри метода экземпляра класса, без использования super или имен классов:

  • Включить общедоступные, защищенные, пакетные и частные методы, объявленные в самом классе.
  • Включить защищенные методы своих суперклассов.
  • Включить методы пакета своих суперклассов того же пакета.
  • Включить методы по умолчанию для своих интерфейсов (те, которые не переопределены/скрыты, см. здесь и здесь).
  • Включить статические методы (класс и суперклассы) с соответствующей доступностью.
  • Не включайте частные методы суперклассов.
  • Не включать переопределенные методы.
  • Не включайте скрытые методы (в специальные, не включайте скрытые статические методы).
  • Не включать синтетические/мостовые методы.
  • Не включать методы, не разрешенные Java, даже если JVM позволяет им.

Итак, указанное выше определение соответствует следующей сигнатуре, когда оба булевых флага false:

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

Идеальный канонический ответ должен учитывать эти булевы флаги.

Ответы

Ответ 1

Даже для сценария "Before Java 8" ваш фрагмент кода неверен. Но сбор всех методов не является обычным сценарием, поскольку, как правило, вам нужны методы, относящиеся к определенному контексту, например. вы можете узнать, какие методы доступны для данного контекста, который не включает все методы, даже если вы рассматриваете методы не public. Если вам действительно нужны все методы, вы должны вспомнить, что методы private и static никогда не переопределяются, а методы private-private переопределяются только при объявлении в одном и том же package. Таким образом, он не подходит для фильтрации каждой встреченной сигнатуры метода.

Что еще хуже, так это то, что методы могут быть переопределены с помощью разных модификаторов. Последнее можно решить, сохранив идею, чтобы начать с фактического класса и использовать Class.getMethods() для получения всего метода public, включая методы default, и пройти иерархию суперкласса по направлению к java.lang.Object, так что уже встречающиеся переопределения имеют наименьшее ограничение модификаторы доступа.

В качестве побочного примечания, встраивание линейных циклов поиска никогда не является хорошей идеей. Youll скоро закончится с квадратичной или худшей сложностью.

Вы можете собирать методы, используя следующий метод:

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

Но, как сказано, может быть, это не подходит для того, что вы хотите сделать. Сначала вы должны задать себе следующие вопросы:

  • Вы ищете методы, которые составляют API (обычно это public и protected)?
  • Или вы действительно хотите видеть методы, доступные для определенного контекста class/package?
  • Включены методы static?
  • Включены ли синтетические/мостиковые методы?
  • и др.

Вот пересмотренный метод, адаптированный к вашему более конкретному запросу:

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

Ответ 2

Я не смог скомпилировать ответ Holger в среде Android, так как MethodType добавлен в уровень API 26, а Android Studio поддерживает подмножество Java 8 Language Features. В дополнение к этому, код Хольгера содержит много лямбда и потоков, я считаю их нечитаемыми. Поэтому я решил написать более читаемый код, который работает в любой среде Java. Но это не идеальное решение, так как я не включил флаги.

Ниже фрагменты работают так же, как если бы вы звонили 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);
    }
}

Два из компонентов объявления метода содержат подпись метода: имя метода и типы параметров. Компилятор не рассматривает тип возврата при дифференцировании методов, поэтому вы не можете объявить два метода с одной и той же сигнатурой, даже если они имеют другой тип возвращаемого значения. Поэтому класс MethodSignature не содержит ссылки на возвращаемый тип его метода.

Но когда вы вызываете getDeclaredMethods или getMethods, можно получить несколько объявленных методов с тем же именем и типами параметров, но с разными типами возвращаемых данных. Это означает, что компилятор создал синтетический метод, называемый мостовым методом. Чтобы решить эту проблему, вызовите method.isSynthetic() в методе, если он возвращает true, пропустив его. Поскольку это синтетический метод, будет существовать не синтетический, имеющий одну и ту же подпись, но другой тип возврата.

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