Есть ли лучший (более безопасный) способ получить класс параметризованного типа?

#java #generics #reflection #guava

#java #дженерики #отражение #гуава

Вопрос:

Я хочу создать метод, который возвращает класс параметризованного типа.

Рассмотрим interface :

 private static interface MyInterface<T> {
    void run(T parameter);
}
  

И одна реализация:

 private static class MyInterfaceImplString implements MyInterface<String> {
    @Override
    public void run(String parameter) {     }
}
  

Теперь я хочу перейти MyInterfaceImplString.class к методу, и этот метод вернет String.class .

Я что-то печатаю, и я вижу, что информация там есть, но я просто не могу ее получить. Или, по крайней мере, более безопасным способом.

 public class TypesTest {

    public static void main(String[] args) {
        Class<?> genericParameter1 = getGenericParamaterType(MyInterfaceImplVoid.class);
        System.out.println(genericParameter1); //expect Void here

        Class<?> genericParameter2 = getGenericParamaterType(MyInterfaceImplString.class);
        System.out.println(genericParameter2); //expect String here
    }

    private static Class<?> getGenericParamaterType(Class<? extends MyInterface<?>> clazz) {
        for (Method m : clazz.getMethods()) {
            if ("run".equals(m.getName()) amp;amp; m.getParameterCount() == 1) {
                System.out.println(TypeLiteral.get(clazz).getParameterTypes(m));
            }
        }

        return null;
    }

    private static class MyInterfaceImplVoid implements MyInterface<Void> {

        @Override
        public void run(Void parameter) {           }
    }

    private static class MyInterfaceImplString implements MyInterface<String> {

        @Override
        public void run(String parameter) {         }

    }

    private static interface MyInterface<T> {
        void run(T parameter);
    }
}
  

Игнорируйте null возвращаемое значение в методе. Я просто хочу что-то напечатать, посмотреть, что я получу, а затем вернуть его. Однако моя текущая реализация кажется несколько неортодоксальной, потому что класс может иметь более одного run метода, который не имеет ничего общего с. MyInterface

В случае проблемы с XY я хочу иметь возможность распознавать, какая из этих реализаций содержит параметризованный тип, который extends Employee (скажем так). Я вызываю run метод интерфейса, косвенно заданный HourlyPaidEmployee или MonthlyPaidEmployee . Итак, если базовая реализация есть MyInterface<Employee> , я могу ввести своего фактического сотрудника (оплачивается ежемесячно или почасово). Но если реализация есть MyInterface<HourlyEmployee> , я не могу ввести ежемесячно оплачиваемый. Итак, получение класса параметризованного типа помогает мне узнать, какие типы сотрудников я могу безопасно вводить в run метод.

Я с java-8 и Guice зависимостью в classpath, которая содержит guava зависимость.

РЕДАКТИРОВАТЬ: @Glorfindel сделал хороший вывод в своем ответе. Но если реализация выглядит так:

 private static class MyInterfaceImplVoid implements MyInterface<Void> {

    @Override
    public void run(Void parameter) {
    }

    public void run(Integer par) {
    }
}
  

Я не могу знать, что это за метод, который ссылается на интерфейс, поэтому его ответ может вернуться ко мне Integer.cass .

Ответ №1:

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

 public class TypesTest {
    public static void main(String[] args) {
        Type genericParameter1 = getGenericParameterType(MyInterfaceImplVoid.class);
        System.out.println(genericParameter1 ", " (genericParameter1==Void.class));

        Type genericParameter2 = getGenericParameterType(MyInterfaceImplString.class);
        System.out.println(genericParameter2 ", " (genericParameter2==String.class));
    }

    private static Type getGenericParameterType(Class<? extends MyInterface<?>> clazz) {
        for(Type interfaceType: clazz.getGenericInterfaces()) {
            if(interfaceType == clazz)
                throw new IllegalArgumentException("raw implementation");
            if(interfaceType instanceof ParameterizedType) {
                ParameterizedType pt = (ParameterizedType)interfaceType;
                if(pt.getRawType() == MyInterface.class)
                    return pt.getActualTypeArguments()[0];
            }
        }
        throw new UnsupportedOperationException("not implemented directly");
    }
}
  
 class java.lang.Void, true
class java.lang.String, true
  

Но обратите внимание, что возвращаемый тип — это Type , а не class , поскольку аргумент type не гарантируется как a Class (повторяемый тип или необработанный тип). Это может быть параметризованный тип сам по себе, но также и переменная типа.

Рассмотрим случай, подобный

 abstract class InTheMiddle<T> implements MyInterface<T> {}
class Implementation extends InTheMiddle<String> {
    @Override
    public void run(String parameter) {
    }
}
  

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

Но имейте в виду, что также возможно следующее:

 abstract class Outer<T> {
    class Inner implements MyInterface<T> {
        @Override
        public void run(T parameter) {
        }
    }
}
class SubclassOfOuter extends Outer<Integer> {
}
  
 SubclassOfOuter outer = new SubclassOfOuter();
MyInterface<Integer> object = outer.new Inner();
  

что потребует обхода иерархии типов и артефактов лексической области видимости (помимо внешних классов, классы реализации могут быть локальными классами универсального метода, размещенного внутри универсального класса, который может быть локальным для другого метода …).

Затем есть конструкции, которые вообще невозможно запросить во время выполнения из-за стирания типа:

 class GenericImpl<X> implements MyInterface<X> {
    @Override
    public void run(X parameter) {
    }
}
GenericImpl<String> a = new GenericImpl<>();
GenericImpl<Integer> b = new GenericImpl<>();
  

или

 MyInterface<Thread> another = t -> {};
  

Поэтому дважды подумайте, прежде чем делать свой код зависимым от возможности определения фактического аргумента типа во время выполнения.

Комментарии:

1. (1 ). Ваш метод удовлетворяет мои потребности. Кроме того, это только java. (Не говоря уже о моем хаотичном). Спасибо. И чтобы ответить на ваш комментарий в другом ответе, я никогда не утверждал, что Guava отменяет стирание типа. Я просто подумал, что Guava может каким-то образом найти общий параметризованный тип, в то время как чистая java не может. Вероятно, вызывающий туземцев. Хотя я действительно пересмотрел его. Поскольку они удалены, он, вероятно, не сможет этого сделать. Спасибо за ваше время и усилия.

Ответ №2:

Если я заменю

 System.out.println(TypeLiteral.get(clazz).getParameterTypes(m));
  

с помощью

 return m.getParameterTypes()[0];
  

Я получаю следующий вывод:

 class java.lang.Void  
class java.lang.String
  

Нет необходимости в Guice / Guava, обычная старая Java, похоже, работает нормально. Я не знаю хорошего решения для другой проблемы; кажется, reflection даже не может определить, что один метод является переопределением, а другой — нет. Если у вас есть достаточный контроль над классами реализации, я бы посоветовал просто использовать другое имя вместо run другого метода.

Комментарии:

1. Отредактировал мой вопрос. Проверьте это.

2. Да, я действительно не знаю решения для этого случая, кроме как не использовать потенциально конфликтующие имена методов…

3. Я опубликовал ответ, который работает, если вам интересно. Спасибо за время и усилия.

Ответ №3:

Ваш T не привязан.

Это означает, что каждый impl этого интерфейса, даже ваш MyInterfaceImplVoid , будет иметь run(Object) метод, даже если вы его вообще не писали — и в этом случае он приведет аргумент к Void и передаст вызов дальше run(Void) . Итак, если вы хотите быть уверены, что вызываете метод интерфейса, всегда вызывайте run(Object) и отправляйте объект правильного типа (как выяснено в ответе @Glorfindel).

Обратите внимание, что этот «трюк» с определением границы обычно является плохим планом. Что вы собираетесь делать, когда я напишу этот класс:

 class DynamicMyInterface<Z> implements MyInterface<Z> {
    private Class<Z> forcedType;

    public DynamicMyInterface(Class<Z> type) {
        this.forcedType = type;
    }

    public void run(Z in) {
        forcedType.cast(in);
        ....
    }
}
  

Ваша прогулка в .getGenericType() и тому подобное никогда не сможет понять, что это такое, из-за стирания. Вы не продвинетесь дальше Z extends Object .

Комментарии:

1. Я знаю об удалении во время выполнения. Вот почему я упоминаю и пытаюсь сделать это с помощью Guava. Я также знаю, что запрос типа работает и в чистой java. Однако запрос типа в конструкторе не подходит для моего варианта использования. Спасибо за ваше время. Если вам интересно, я опубликовал ниже кое-что, что работает.

2. @GeorgeZ. Guava не может отменить удаление типа. Он не содержит магии.

Ответ №4:

Я нашел способ получить класс параметризованного типа с помощью этого «уродливого» — «какого черта» метода:

 private static Class<?> getGenericParamaterType(Class<? extends MyInterface<?>> clazz) {
    for (TypeToken<?> typeToken : TypeToken.of(clazz).getTypes()) {
        Class<?> rawType = typeToken.getRawType();
        if (rawType == MyInterface.class) {
            TypeLiteral<?> typeLiteral = TypeLiteral.get(typeToken.getType());
            for (Method method : rawType.getMethods()) {
                if ("run".equals(method.getName())) {
                    TypeLiteral<?> typeLiteral2 = typeLiteral.getParameterTypes(method).get(0);
                    return typeLiteral2.getRawType();
                }
            }
        }
    }
    return null;
}
  

Он работает именно так, как я хочу. Однако я не уверен в его производительности. Кроме того, я действительно думаю, что Guava предоставляет более краткий способ достижения того, чего я хочу. С другой стороны, я не смог его найти.

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

 public class TypesTest {

    public static void main(String[] args) {
        Class<?> genericParameter1 = getGenericParamaterType(MyInterfaceImplVoid.class);
        System.out.println(genericParameter1); //expect Void here

        Class<?> genericParameter2 = getGenericParamaterType(MyInterfaceImplString.class);
        System.out.println(genericParameter2); //expect String here

        Class<?> genericParameter3 = getGenericParamaterType(AbstractImpl.class);
        System.out.println(genericParameter3); //expect Integer here
    }

    private static Class<?> getGenericParamaterType(Class<? extends MyInterface<?>> clazz) {
        for (TypeToken<?> typeToken : TypeToken.of(clazz).getTypes()) {
            Class<?> rawType = typeToken.getRawType();
            if (rawType == MyInterface.class) {
                TypeLiteral<?> typeLiteral = TypeLiteral.get(typeToken.getType());
                for (Method method : rawType.getMethods()) {
                    if ("run".equals(method.getName())) {
                        TypeLiteral<?> typeLiteral2 = typeLiteral.getParameterTypes(method).get(0);
                        return typeLiteral2.getRawType();
                    }
                }
            }
        }
        return null;
    }

    private static class MyInterfaceImplVoid extends JPanel implements MyInterface<Void>, Runnable {

        @Override
        public void run(Void parameter) { 
        }

        public void run(Integer par) {          }

        public void run(String s) {         }

        @Override
        public void someOtherMethod() {         }

        @Override
        public void run() {         }

    }

    private static abstract class AbstractImpl implements MyInterface<Integer> {

    }

    private static class MyInterfaceImplString implements MyInterface<String> {

        @Override
        public void run(String parameter) {         }

        @Override
        public void someOtherMethod() {         }

    }

    private static interface MyInterface<T> {
        void run(T parameter);

        void someOtherMethod();
    }
}
  

Я не буду отмечать этот ответ как принятый, поскольку я совершенно уверен, что есть более удобный способ сделать это.

Комментарии:

1. Нет, это не работает. запустите его против этого: class Broken implements MyInterface<Integer> { public void run(String s) {} @Override public void run(Integer i) {} — это случайным образом вернет целое число или строку. Кроме того, ваш метод getGenericParamater выполняет весь этот TypeToken voodoo и полностью игнорирует его ( typeLiteral никогда не возвращается) — зачем все эти усилия? В более общем плане «поиск метода run для определения типа» не может работать. Однако вы можете поискать метод run(Object) для его вызова.

2. @rzwitserloot Независимо от того, что я делаю, данный класс Broken я всегда получаю Integer.class в результате. Однако я понимаю, что когда дело доходит до отражения, все может быть случайным. Что касается усилий, вы читали раздел проблемы XY в моем вопросе? Я хочу знать, что вводить в качестве параметра в неявный вызов. Есть ли какой-либо другой подход, который я могу получить?

3. @rzwitserloot Кроме того, внутри моего метода, если System.out.println(typeToken); вы получите такой результат TypesTest$MyInterface<java.lang.Integer> . Эта строка содержит информацию, которую я ищу. Может быть, лучше (безопаснее) извлечь его из строки?

4. Нет, извлекать его из строки небезопасно. Вы можете получить это целое число из объекта typetoken .