Узнаем параметр Generic-класса в Java

    Если вы не очень часто программируете на Java, то этот топик скорее всего будет для вас бесполезен. Не читайте его :)

    Недавно понадобилось решить следующую задачу: определить класс, которым параметризован generic-класс.

    Если кто-то сталкивался с подобной задачей, то наверное также сразу попробовал написать что-то вроде этого:
    public class AbstractEntityFactory<E extends Entity> {
      public Class getEntityClass() {
        return E.class;
      }
    }

    Увы, IDE либо компилятор сразу укажут вам на ошибку («cannot select from a type variable» в стандартном компиляторе): " E.class" — не является допустимой конструкцией. Дело в том, что в общем случае во время исполнения программы информации о реальных параметрах нашего generic-класса может уже и не быть. Поэтому такая конструкция в Java не может работать.

    Если мы напишем
    ArrayList<Float> listOfNumbers = new ArrayList<Float>();
    то из-за стирания типов мы не можем анализируя listOfNumbers узнать, что это — ArrayList параметризованный именно Float, а не чем-то еще. К сожалению Java Generics работают именно так :(

    Неужели информации о параметрах generic-классов при компиляции всегда теряется бесследно и не существует во время выполнения? Нет, существует. Но только в информации о классе, который явно определяет значение параметра в его generic-родителе. Выделим исследуемый класс:
    public class FloatList extends ArrayList<Float>{}
    ArrayList<Float> listOfNumbers = new FloatList();

    Теперь, если мы будем анализировать через отражения listOfNumbers, мы сможем узнать, что это объект класса FloatList, для которого предком является ArrayList и этот ArrayList внутри FloatList был параметризован классом Float. Узнать всё это нам поможет метод Class.getGenericSuperclass().
    Class actualClass = listOfNumbers.getClass();
    ParameterizedType type = (ParameterizedType)actualClass.getGenericSuperclass();
    System.out.println(type); // java.util.ArrayList<java.lang.Float>
    Class parameter = (Class)type.getActualTypeArguments()[0];
    System.out.println(parameter); // class java.lang.Float

    Таким образом, теперь мы можем узнать актуальный параметр generic-класса, если этот параметр был задан явным образом (то есть параметр определен внутри секции extends одного из наследников). Пусть мы не можем решить проблему определения типа параметра в общем виде, но во многих случаях даже того, что мы получили — достаточно.

    Вынесем всё в отдельный метод:
    public class ReflectionUtils {
      public static Class getGenericParameterClass(Class actualClass, int parameterIndex) {
        return (Class) ((ParameterizedType) actualClass.getGenericSuperclass()).getActualTypeArguments()[parameterIndex];
      }
    }

    Перепишем наш исходный класс:
    public class AbstractEntityFactory<E extends Entity> {
      public Class getEntityClass() {
        return ReflectionUtils.getGenericParameterClass(this.getClass(), 0);
      }
    }

    Всё, проблема решена! Или нет?..

    Предположим, что от FloatList будет унаследован класс ExtendedFloatList? Очевидно, что actualClass.getGenericSuperclass() вернет нам уже не тот класс, который надо (FloatList вместо ExtendedFloatList). А если иерархия будет еще сложнее? Наш метод оказывается никуда не годным. Обобщим нашу задачу. Пркдставим, что у нас есть такая иерархия классов:
    public class ReflectionUtilsTest extends TestCase {
      // В комментариях приведены "реальные" параметры

      static class A<K, L> {
        // String, Integer
      }

      static class B<P, Q, R extends Collection> extends A<Q, P> {
        // Integer, String, Set
      }

      static class C<X extends Comparable<String>, Y, Z> extends B<Z, X, Set<Long>> {
        // String, Double, Integer
      }

      static class D<M, N extends Comparable<Double>> extends C<String, N, M> {
        // Integer, Double
      }

      static class E extends D<Integer, Double> {
        //
      }
    }

    Пусть теперь нам нужно из экземпляра класса E достать информацию о том, что его предок B в качестве второго параметра (Q) получил класс String.

    Итак, что изменилось? Во-первых, теперь нам нужно анализировать не непосредственного родителя, а «подняться» по иерархии классов до определенного предка. Во-вторых, нам нужно учитывать, что параметры могут быть заданы не в ближайшем наследнике анализируемого класса, а «ниже». В-третьих, простой каст параметра к Class может не пройти — сам параметр может быть параметризованным классом. Попробуем всё это учесть…

    import java.lang.reflect.GenericDeclaration;
    import java.lang.reflect.ParameterizedType;
    import java.lang.reflect.Type;
    import java.lang.reflect.TypeVariable;
    import java.util.Stack;

    /**
    * Alex Tracer (c) 2009
    */
    public class ReflectionUtils {

      /**
       * Для некоторого класса определяет каким классом был параметризован один из его предков с generic-параметрами.
       *
       * @param actualClass   анализируемый класс
       * @param genericClass  класс, для которого определяется значение параметра
       * @param parameterIndex номер параметра
       * @return        класс, являющийся параметром с индексом parameterIndex в genericClass
       */
      public static Class getGenericParameterClass(final Class actualClass, final Class genericClass, final int parameterIndex) {
        // Прекращаем работу если genericClass не является предком actualClass.
        if (!genericClass.isAssignableFrom(actualClass.getSuperclass())) {
          throw new IllegalArgumentException("Class " + genericClass.getName() + " is not a superclass of "
              + actualClass.getName() + ".");
        }

        // Нам нужно найти класс, для которого непосредственным родителем будет genericClass.
        // Мы будем подниматься вверх по иерархии, пока не найдем интересующий нас класс.
        // В процессе поднятия мы будем сохранять в genericClasses все классы - они нам понадобятся при спуске вниз.

        // Проейденные классы - используются для спуска вниз.
        Stack<ParameterizedType> genericClasses = new Stack<ParameterizedType>();

        // clazz - текущий рассматриваемый класс
        Class clazz = actualClass;

        while (true) {
          Type genericSuperclass = clazz.getGenericSuperclass();
          boolean isParameterizedType = genericSuperclass instanceof ParameterizedType;
          if (isParameterizedType) {
            // Если предок - параметризованный класс, то запоминаем его - возможно он пригодится при спуске вниз.
            genericClasses.push((ParameterizedType) genericSuperclass);
          } else {
            // В иерархии встретился непараметризованный класс. Все ранее сохраненные параметризованные классы будут бесполезны.
            genericClasses.clear();
          }
          // Проверяем, дошли мы до нужного предка или нет.
          Type rawType = isParameterizedType ? ((ParameterizedType) genericSuperclass).getRawType() : genericSuperclass;
          if (!rawType.equals(genericClass)) {
            // genericClass не является непосредственным родителем для текущего класса.
            // Поднимаемся по иерархии дальше.
            clazz = clazz.getSuperclass();
          } else {
            // Мы поднялись до нужного класса. Останавливаемся.
            break;
          }
        }

        // Нужный класс найден. Теперь мы можем узнать, какими типами он параметризован.
        Type result = genericClasses.pop().getActualTypeArguments()[parameterIndex];

        while (result instanceof TypeVariable && !genericClasses.empty()) {
          // Похоже наш параметр задан где-то ниже по иерархии, спускаемся вниз.

          // Получаем индекс параметра в том классе, в котором он задан.
          int actualArgumentIndex = getParameterTypeDeclarationIndex((TypeVariable) result);
          // Берем соответствующий класс, содержащий метаинформацию о нашем параметре.
          ParameterizedType type = genericClasses.pop();
          // Получаем информацию о значении параметра.
          result = type.getActualTypeArguments()[actualArgumentIndex];
        }

        if (result instanceof TypeVariable) {
          // Мы спустились до самого низа, но даже там нужный параметр не имеет явного задания.
          // Следовательно из-за "Type erasure" узнать класс для параметра невозможно.
          throw new IllegalStateException("Unable to resolve type variable " + result + "."
              + " Try to replace instances of parametrized class with its non-parameterized subtype.");
        }

        if (result instanceof ParameterizedType) {
          // Сам параметр оказался параметризованным.
          // Отбросим информацию о его параметрах, она нам не нужна.
          result = ((ParameterizedType) result).getRawType();
        }

        if (result == null) {
          // Should never happen. :)
          throw new IllegalStateException("Unable to determine actual parameter type for "
              + actualClass.getName() + ".");
        }

        if (!(result instanceof Class)) {
          // Похоже, что параметр - массив или что-то еще, что не является классом.
          throw new IllegalStateException("Actual parameter type for " + actualClass.getName() + " is not a Class.");
        }

        return (Class) result;
      }

      public static int getParameterTypeDeclarationIndex(final TypeVariable typeVariable) {
        GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration();

        // Ищем наш параметр среди всех параметров того класса, где определен нужный нам параметр.
        TypeVariable[] typeVariables = genericDeclaration.getTypeParameters();
        Integer actualArgumentIndex = null;
        for (int i = 0; i < typeVariables.length; i++) {
          if (typeVariables[i].equals(typeVariable)) {
            actualArgumentIndex = i;
            break;
          }
        }
        if (actualArgumentIndex != null) {
          return actualArgumentIndex;
        } else {
          throw new IllegalStateException("Argument " + typeVariable.toString() + " is not found in "
              + genericDeclaration.toString() + ".");
        }
      }
    }

    Ухх, наш метод «в одну строчку» превратился в громоздкого монстра! :)
    Надеюсь комментариев достаточно, чтобы понять происходящее ;)

    Итак, перепишем наш начальный класс:
    public class AbstractEntityFactory<E extends Entity> {
      public Class getEntityClass() {
        return ReflectionUtils.getGenericParameterClass(this.getClass(), AbstractEntityFactory.class, 0);
      }
    }

    Теперь такой код отработает корректно:
    public class Topic extends Entity {
    }

    public class TopicFactory extends AbstractEntityFactory<Topic> {
      public void doSomething() {
        Class entityClass = getEntityClass(); // Вернет Topic
      }
    }

    На этом пожалуй всё. Спасибо что дочитали до конца :)

    Это мой первый пост на Хабре. Буду благодарен за критику, замечания и указания на ошибки.

    Upd: код исправлен для корректного учета ситуации, когда где-то в иерархии присутствует непараметризованный класс.
    Upd2: спасибо пользователю Power за указание на ошибки.

    Upd3: архив с исходниками и тестами.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 32

      –2
      Полезно, благодарен
        0
        public class FloatList extends ArrayList{}

        наверно стоит заменить на
        public class FloatList extends ArrayList{};

        правильно?
          +3
          Скорее на
          public class FloatList extends ArrayList<Float> {}
            0
            Да, верно. Потерялось в процессе «раскрашивания кода».
              0
              ыы
              именно это и хотел сказать.

              любимый Хабр зачем-то покрошил [Float], видимо как html-тэг.
            +3
            А зачем это понадобилось? Если не секрет?
              0
              Пример ситуации: в слое представления есть формы с таблицами. Столбцы в таблицах — соотносятся с полями некоторых сущностей в модели данных (в одной таблице могут присутствовать поля из разных связанных сущностей). Задача: организовать фильтрацию и сортировку по столбцам таблиц средствами базы данных. То есть нужно преобразовать информацию о том, какие столбцы таблицы выбраны для фильтрации/сортировки в информацию о полях сущностей, потом по этой информации построить Criteria для выборки данных, получить данные и развернуть цепочку в обратном направлении. Вот для всего этого на некоторых этапах понадобилась возможность анализировать параметры (хотя есть обходные пути).
                +1
                например, для написания Generic DAO.
                В Hibernate / Seam так делают

                вполне удобно
                  0
                  Бинго! :)
                  0
                  Думаю Автор бывший .NET-чик, потому что в .NET-е Generic-и работают на runtime-е.

                  В JAVA изза обратной совместимости Generic — и работают только в compile time, в отличие от .NET-а, в котором существует две копии библиотек колекций: с generic-ами и без.

                  На JAVA такого рода задачи лучше решать.

                  abstract class Base<X>{
                      public abstract Class<X> getXType();
                  }
                  
                  class MyClass extends Base<String>{
                      public Class<String> getXType(){
                          return String.class;
                      }
                  }
                  


                  Хочу также обратить внимание на парамертрезированый тип Class метода getXType, так как это являеться хороши тоном в JAVA.

                  Это также являеться хорошим примером Strong Typed программировния, потому что конструкция abstract/Class всегда будет следить за тем что getXType возвращает именно тот тип который указан в качестве generic-а.
                    +1
                    К сожалению этот метод применим только в ситуации, когда есть возможность менять код тех классов, для которых нужно определить тип. Если нужно получить информацию о параметризованном классе, опреденном в стороннем коде, то всё же понадобится метод вроде того, что в топике.
                      –1
                      Пишите код проще, да прибудет вам.
                  +3
                  Вы догадываетесь, что будет, если в иерархии классов встретится непараметризуемый/непараметризованный класс?
                  Например:

                  public class Test
                  {
                  	private class A<T> {}
                  
                  	private class B extends A<String> {}
                  
                  	private class C<T> extends B {}
                  
                  	private class D extends C<Object> {}
                  
                  	public static void main(String[] args) throws Exception
                  	{
                  		Class<?> clazz = C.class;
                  		System.out.println(clazz.getGenericSuperclass() instanceof ParameterizedType);
                  
                  		ReflectionUtils.getGenericParameterClass(D.class, A.class, 0); // ClassCastException, а должно было вернуть String.class
                  	}
                  }
                  
                    0
                    Спасибо, за ценное замечание. Плюс вам в карму. Действительно, забыл учесть эту ситуацию, хотя и хотел :)

                    Внес исправления в код.

                    0
                    Полезно! Если честно, то даже не задумывался о попытке вытfщить класс генерика, применял конструкции типа
                    class Test{
                    Classt;
                    public Test(Classgen){t = gen}
                    }
                      0
                      По-хорошему — так и нужно делать. То есть передавать в конструктор реальный класс, а не искать его так как описано в топике. Это просто, надёжно и полностью корректно.

                      Но иногда — это не очень удобно: довешивать в конструкторы дополнительный параметр. Особенно — если классов сотни, а ты можешь гарантировать, что все они будут параметризованы при наследовании. Собственно это и стало причиной топика.
                        0
                        это понятно — не всегда получалось красиво — помогала либо строгая иерархия классов (чтобы по имени вытащить генерализацию), либо аннотирование. Попробуем применить в жизни — как знать, может, пригодится :)
                      0
                      Хотелось бы уточнить ещё одну вещь.
                        +1
                        Чёрт, комментарий отправился раньше, чем нужно.
                        Так вот
                            if (!(result instanceof Class)) {
                              // Похоже, что параметр - массив, примитивный типи, интерфейс или еще-что-то, что не является классом.
                              throw new IllegalStateException("Actual parameter type for " + actualClass.getName() + " is not a Class.");
                            }
                        

                        Вообще говоря, массив, примитивный тип и интерфейс являются классами (см. Class#isArray(), Class#isPrimitive(), Class#isInterface(); если точнее, то примитивы — не классы, но для них есть соответствующие классы).

                        Правда, если параметризовать класс-родитель массивом, то параметр действительно вернётся не как Class, а как GenericArrayType — и это единственный возможный случай попасть в этот блок кода. Потому что примитивным типом вообще нельзя параметризовать класс-родитель.
                        Ещё один наследник (кроме, собственно, GenericArrayType, ParameterizedType, TypeVariable и Class) интерфейса Type — WildcardType (представляющий wildcard'ы) — и им тоже, как и примитивом, нельзя параметризовать класс-родитель при наследовании.
                          +1
                          Уточнение к «примитивным типом вообще нельзя параметризовать класс-родитель»: примитивным типом нельзя вообще ничего параметризовать, в том числе и класс-родитель :) А вот массивом примитивных типов (например, int[]) — можно.
                            0
                            Да, верно — этот комментарий был не корректен. Действительно, попасть в данный блок кода можно только параметризовав класс массивом (неважно каким): примитивным классом параметризовать класс нельзя, а с интерфейсами проблемы нет — основная ветка кода с ними прекрасно справляется. В частности у меня в примере иерархии встречается интерфейс Set, который обрабатывается без проблем.

                            Поправил комментарий в коде.
                          0
                          А тесты для ReflectionUtils есть?
                          Мне как раз такое понадобилось, но без тестов использовать страшно. ;-)
                            0
                            Да, есть. Добавил архив к статье.

                            Правда там более поздняя версия утилиты — теперь поддерживается извлечение информации из интерфейсов, а не только из классов. Если нужны будут пояснения — могу набросать отдельную небольшую статью.
                            0
                            Аналогичная реализация от гугла:
                            code.google.com/p/google-gson/source/browse/trunk/gson/src/main/java/com/google/gson/reflect/TypeToken.java
                              0
                              Поясните, чем она аналогична? Не наблюдаю в ней решения проблемы, описанной в топике. Если вы про метод getSuperclassTypeParameter(...), то он представляет собой как раз то, от чего я отказался в начале статьи по причине ограниченности применения.
                                0
                                Проблема описанная в топике: определить класс, которым параметризован generic-класс. В простом случае подходит. Аналогичный не значит лучший.
                                  0
                                  Простите, но решение в виде
                                  type.getActualTypeArguments()[0]
                                    0
                                    * отправилось раньше времени.

                                    Простите, но решение в виде:
                                    type.getActualTypeArguments()[0]
                                    используемое в приведенном коде от гугла 1 в 1 присутствует в статье. И не решает основную задачу в полном объеме. А этот тривиальный случай с одним параметром находится поиском того же Гугла на сотне сайтов.

                                    В итоге всё еще не понятна цель вашего первого комментария.
                                      0
                                      повесить закладку на сорц, чтобы просто было найти при необходимости
                                        0
                                        Хотите повесить на что-то закладку — вешайте. Но для этого вовсе не обязательно засорять топики на Хабре.
                                          0
                                          релевантность налицо. имхо, дискуссия неконструктивна
                                            0
                                            Релевантность? Конечно информация релевантна сама себе. Но в данном случае от повторения информации нет никакого толку. Вы добавили в топик то же самое, что уже было в нем описано.

                                            Насчет неконструктивности — согласен :)

                            Only users with full accounts can post comments. Log in, please.