Pull to refresh

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

Reading time9 min
Views111K
Если вы не очень часто программируете на 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: архив с исходниками и тестами.
Tags:
Hubs:
+32
Comments32

Articles