Дженерики и java.beans.Introspector

Учитывая следующий скелет кода, можно ли определить, что свойство foo на самом деле относится к типу String?

public class TestIntrospection {
    public static class SuperBean<T> {
        private T foo;

        public T getFoo() { return foo; }
        public void setFoo(T foo) { this.foo = foo; }
    }

    public static class SubBean extends SuperBean<String> {
    }

    public static void main(String[] args) throws IntrospectionException {
        BeanInfo beanInfo = Introspector.getBeanInfo(SubBean.class);
        PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
        for (PropertyDescriptor prop : propertyDescriptors) {
            if ("foo".equals(prop.getName())) {
                System.out.printf("%s of %s\n", prop.getName(), prop.getPropertyType());

                Method readMethod = prop.getReadMethod();
                Type returnType = prop.getReadMethod().getGenericReturnType();
                if (returnType instanceof TypeVariable) {
                    TypeVariable t = (TypeVariable) returnType;
                    GenericDeclaration d = t.getGenericDeclaration();
                    System.out.println("TypeVariable : " + t.getName() + " " + t.getBounds()[0]);
                }
            }
        }
    }
}

Фактический результат

foo класса java.lang.Object
TypeVariable : T class java.lang.Object

Редактировать: я должен был упомянуть, что я знаю о стирании типов и что метод фактически возвращает объект на уровне байт-кода. Тем не менее, метаданные об универсальных типах доступны в файле класса и могут быть запрошены путем отражения, как в примере кода. Вот еще один фрагмент, который показывает, что SubBean на самом деле имеет параметр типа String:

                Type superClass = SubBean.class.getGenericSuperclass();
                ParameterizedType pt = (ParameterizedType) superClass;
                System.out.println(pt.getActualTypeArguments()[0]);

выход:

класс java.lang.String

Тогда остается вопрос, как мне связать этот фактический аргумент типа с переменной типа? Если я знаю, что есть только один параметр типа, это просто, но я хотел бы, чтобы этот код работал также для bean-компонентов, имеющих несколько параметров универсального типа.


person Jörn Horstmann    schedule 02.02.2011    source источник
comment
Java-библиотека ClassMate, описанная на странице cowtowncoder.com/blog/archives/2012/. 04/entry_471.html, похоже, хорошо поддерживает разрешение универсальных типов. Проект доступен на github: github.com/cowtowncoder/java-classmate   -  person Jörn Horstmann    schedule 08.04.2012
comment
Начиная с Java 1.7.0_80 вывод равен foo of class java.lang.String TypeVariable : T class java.lang.Object   -  person Vsevolod Golovanov    schedule 27.04.2015


Ответы (7)


Пока класс среды выполнения объекта определяет значение параметра типа, вы можете вывести его фактическое значение, рекурсивно заменив формальные параметры типа фактическими параметрами, полученными из Class.getGenericSuperClass():

class Substitution extends HashMap<String, TypeExpr> {
    Substitution(TypeVariable[] formals, TypeExpr[] actuals) {
        for (int i = 0; i < actuals.length; i++) {
            put(formals[i].getName(),actuals[i]);
        }
    }

}

abstract class TypeExpr {
    abstract TypeExpr apply(Substitution s);

    public abstract String toString(); 

    static TypeExpr from(Type type) {
        if (type instanceof TypeVariable) {
            return new TypeVar((TypeVariable) type);
        } else if (type instanceof Class) {
            return new ClassType((Class) type);
        } else if (type instanceof ParameterizedType) {
            return new ClassType((ParameterizedType) type);
        } else if (type instanceof GenericArrayType) {
            return new ArrayType((GenericArrayType) type);
        } else if (type instanceof WildcardType) {
            return new WildcardTypeExpr((WildcardType) type);
        }
        throw new IllegalArgumentException(type.toString());
    }

    static TypeExpr[] from(Type[] types) {
        TypeExpr[] t = new TypeExpr[types.length];
        for (int i = 0; i < types.length; i++) {
            t[i] = from(types[i]);
        }
        return t;
    }

    static TypeExpr[] apply(TypeExpr[] types, Substitution s) {
        TypeExpr[] t = new TypeExpr[types.length];
        for (int i = 0; i < types.length; i++) {
            t[i] = types[i].apply(s);
        }
        return t;
    }

    static void append(StringBuilder sb, String sep, Object[] os) {
        String s = "";
        for (Object o : os) {
            sb.append(s);
            s = sep;
            sb.append(o);
        }
    }
}

class TypeVar extends TypeExpr {
    final String name;

    public TypeVar(String name) {
        this.name = name;
    }

    public TypeVar(TypeVariable var) {
        name = var.getName();
    }

    @Override
    public String toString() {
        return name;
    }

    @Override
    TypeExpr apply(Substitution s) {
        TypeExpr e = s.get(name);
        return e == null ? this : e;
    }
}

class ClassType extends TypeExpr {
    final Class clazz;
    final TypeExpr[] arguments; // empty if the class is not generic

    public ClassType(Class clazz, TypeExpr[] arguments) {
        this.clazz = clazz;
        this.arguments = arguments;
    }

    public ClassType(Class clazz) {
        this.clazz = clazz;
        arguments = from(clazz.getTypeParameters());
    }

    @Override
    public String toString() {
        String name = clazz.getSimpleName();
        if (arguments.length == 0) {
            return name;
        }

        StringBuilder sb = new StringBuilder();
        sb.append(name);
        sb.append("<");
        append(sb, ", ", arguments);
        sb.append(">");
        return sb.toString();
    }

    public ClassType(ParameterizedType pt) {
        clazz = (Class) pt.getRawType();
        Type[] args = pt.getActualTypeArguments();
        arguments = TypeExpr.from(args);
    }

    @Override
    ClassType apply(Substitution s) {
        return new ClassType(clazz, apply(arguments, s));
    }
}

class ArrayType extends TypeExpr {
    final TypeExpr componentType;

    public ArrayType(TypeExpr componentType) {
        this.componentType = componentType;
    }

    public ArrayType(GenericArrayType gat) {
        this.componentType = TypeExpr.from(gat.getGenericComponentType());
    }

    @Override
    public String toString() {
        return componentType + "[]";
    }

    @Override
    TypeExpr apply(Substitution s) {
        return new ArrayType(componentType.apply(s));
    }
}

class WildcardTypeExpr extends TypeExpr {
    final TypeExpr[] lowerBounds;
    final TypeExpr[] upperBounds;

    public WildcardTypeExpr(TypeExpr[] lowerBounds, TypeExpr[] upperBounds) {
        this.lowerBounds = lowerBounds;
        this.upperBounds = upperBounds;
    }

    WildcardTypeExpr(WildcardType wct) {
        lowerBounds = from(wct.getLowerBounds());
        upperBounds = from(wct.getUpperBounds());
    }

    @Override
    TypeExpr apply(Substitution s) {
        return new WildcardTypeExpr(
            apply(lowerBounds, s), 
            apply(upperBounds, s)
        );
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("?");
        if (lowerBounds.length > 0) {
            sb.append(" super ");
            append(sb, " & ", lowerBounds);
        }
        if (upperBounds.length > 0) {
            sb.append(" extends ");
            append(sb, " & ", upperBounds);
        }
        return sb.toString();
    }
}

public class Test {

    /**
     * @return {@code superClazz}, with the replaced type parameters it has for
     *         instances of {@code ct}, or {@code null}, if {@code superClazz}
     *         is not a super class or interface of {@code ct}
     */
    static ClassType getSuperClassType(ClassType ct, Class superClazz) {
        if (ct.clazz == superClazz) {
            return ct;
        }

        Substitution sub = new Substitution(ct.clazz.getTypeParameters(), ct.arguments);

        Type gsc = ct.clazz.getGenericSuperclass();
        if (gsc != null) {
            ClassType sct = (ClassType) TypeExpr.from(gsc);
            sct = sct.apply(sub);
            ClassType result = getSuperClassType(sct, superClazz);
            if (result != null) {
                return result;
            }
        }

        for (Type gi : ct.clazz.getGenericInterfaces()) {
            ClassType st = (ClassType) TypeExpr.from(gi);
            st = st.apply(sub);
            ClassType result = getSuperClassType(st, superClazz);
            if (result != null) {
                return result;
            }

        }
        return null;
    }

    public static ClassType getSuperClassType(Class clazz, Class superClazz) {
        return getSuperClassType((ClassType) TypeExpr.from(clazz), superClazz);
    }

Тестовый код:

    public static void check(Class c, Class sc, String expected) {
        String actual = getSuperClassType(c, sc).toString();
        if (!actual.equals(expected)) {
            throw new AssertionError(actual + " != " + expected);
        }
    }

    public static void main(String[] args) {
        check(Substitution.class, Map.class, "Map<String, TypeExpr>");
        check(HashMap.class, Map.class, "Map<K, V>");
        check(Bar.class, Foo.class, "Foo<List<? extends String[]>>");
    }
}

interface Foo<X> {

}
class SuperBar<X, Y> implements Foo<List<? extends Y[]>> {

}

class Bar<X> extends SuperBar<X, String> { }

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

class Super<T> {
    final Class<T> clazz;

    T foo;

    Super(Class<T> clazz) {
        this.clazz = clazz;
    }

    public T getFoo() {
        return foo;
    }

    public T setFoo() {
        this.foo = foo;
    }
}
person meriton    schedule 03.02.2011

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

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

public static class SuperBean<F, B, Q> {
    // getters and setters
}

public static class SubBean<X> extends SuperBean<String, Integer, X> {
}

...

                Type returnType = readMethod.getGenericReturnType();

                Type superClass = SubBean.class.getGenericSuperclass();
                GenericDeclaration genericDecl = ((TypeVariable) returnType).getGenericDeclaration();
                TypeVariable[] parameters = genericDecl.getTypeParameters();
                Type[]         actualArgs = ((ParameterizedType) superClass).getActualTypeArguments();

                for (int i=0; i<parameters.length; i++) {
                    //System.out.println(parameters[i] + " " + actualArgs[i]);
                    if (returnType == parameters[i]) {
                        System.out.println("Match : " + parameters[i] + " : " + actualArgs[i]);
                    }
                }

Выход:

bar класса java.lang.Object
Соответствие: B: класс java.lang.Integer
foo класса java.lang.Object
Соответствие: F: класс java.lang.String
qux of класс java.lang.Object
Соответствие : Q : X

Мне нужно написать еще несколько тестов, чтобы определить, что делать с последним последним случаем :)

person Jörn Horstmann    schedule 02.02.2011

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

    public class Base<T> {

      private final Class<T> klazz;

      @SuppressWarnings("unchecked")
      public Base() {
        Class<? extends Base> actualClassOfSubclass = this.getClass();
        ParameterizedType parameterizedType = (ParameterizedType) actualClassOfSubclass.getGenericSuperclass();
        Type firstTypeParameter = parameterizedType.getActualTypeArguments()[0];
        this.klazz = (Class) firstTypeParameter;
      }

      public boolean accepts(Object obj) {
        return this.klazz.isInstance(obj);
      }

    } 

    class ExtendsBase extends Base<String> {

      // nothing else to do!

    }
public class ExtendsBaseTest {

  @Test
  public void testTypeDiscovery() {
    ExtendsBase eb = new ExtendsBase();
    assertTrue(eb.accepts("Foo"));
    assertFalse(eb.accepts(123));
  }
}
person Gursel Koca    schedule 02.02.2011

Генераторы Java испытывают стирание типов во время компиляции. Во время выполнения невозможно определить тип T, который присутствовал во время компиляции.

Вот ссылка: введите стирание

person DwB    schedule 02.02.2011
comment
Боюсь, это один из тех случаев, когда учебник по Java не говорит всей правды. Они хотят сказать, что параметры типа не существуют в байт-коде. Объявленные типы, даже если они являются универсальными, присутствуют в файле класса и могут быть проверены с помощью отражения. В примере кода из вопроса этой информации достаточно для восстановления параметра типа во время выполнения. - person meriton; 03.02.2011

К сожалению, стирание шрифтов идет полным ходом.

Хотя кажется, что SubBean должен иметь фиксированный тип String для этого ivar и этих методов, потому что параметр типа для SuperBean известен во время компиляции, к сожалению, это не так, как это работает. Компилятор не создает String модифицированную версию SuperBean во время компиляции, из которой SubBean будет производным - есть только одна (стертая) SuperBean

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

public static class SubBean
extends SuperBean<String> {
    // Unfortunate this is necessary for bean reflection ...
    public String getFoo()         { return super.getFoo(); }
    public void setFoo(String foo) { super.setFoo(foo); }
}

Обновление: вышеописанное не работает. Обратите внимание на эту информацию, которую @Jörn Horstmann публикует в комментариях:

Похоже, это не работает, поскольку Introspector по-прежнему возвращает метод чтения типа Object. Кроме того, похоже, что это сгенерированный метод моста (http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ102), что означает, что я могу столкнуться с bugs.sun.com/view_bug.do?bug_id=6788525, если я хочу получить доступ к аннотациям этого метода.

Другим уродливым вариантом вышеуказанного обходного пути является псевдоним свойства:

public static class SubBean
extends SuperBean<String> {
    // Unfortunate this is necessary for bean reflection ...
    public String getFooItem()         { return super.getFoo(); }
    public void setFooItem(String foo) { super.setFoo(foo); }
}

SubBean теперь имеет отдельное свойство FooItem, которое является псевдонимом исходного свойства SuperBean Foo.

person Bert F    schedule 02.02.2011
comment
Похоже, это не работает, поскольку Introspector по-прежнему возвращает метод чтения типа Object. Кроме того, похоже, что это сгенерированный метод моста (angelikalanger.com/GenericsFAQ/FAQSections /), что означает, что я могу столкнуться с bugs.sun.com/view_bug. do?bug_id=6788525, если я хочу получить доступ к аннотациям этого метода. - person Jörn Horstmann; 03.02.2011
comment
@Jörn Horstmann - Спасибо за обновление. Мне жаль, что это не сработало для вас - казалось, что должно, но, учитывая информацию о сгенерированных компилятором методах моста, я вижу путаницу. Единственное другое предложение, о котором я могу думать, - это реализовать свойство псевдонима, например. public String getFooItem() { return super.getFoo(); } дает SubBean отчетливое свойство FooItem, которое на самом деле является псевдонимом для Foo. - person Bert F; 03.02.2011
comment
Спасибо, переопределение методов определенно стоило попробовать и помогло понять некоторые другие аспекты реализации дженериков. Но моя цель на самом деле состоит в том, чтобы использовать это в библиотеке для самоанализа произвольных bean-компонентов без необходимости их предварительной модификации. - person Jörn Horstmann; 03.02.2011

К сожалению нет:

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

Цитата из http://download.oracle.com/javase/1.5.0/docs/guide/language/generics.html

person Eric Giguere    schedule 02.02.2011
comment
Боюсь, это один из тех случаев, когда учебник по Java не говорит всей правды. Они хотят сказать, что параметры типа не существуют в байт-коде. Объявленные типы, даже если они являются универсальными, присутствуют в файле класса и могут быть проверены с помощью отражения. В примере кода из вопроса этой информации достаточно для восстановления параметра типа во время выполнения. - person meriton; 03.02.2011

Вот байтовый код SuperBean:

public class foo.bar.SuperBean {

  // Field descriptor #6 Ljava/lang/Object;
  // Signature: TT;
  private java.lang.Object foo;

  // Method descriptor #10 ()V
  // Stack: 1, Locals: 1
  public SuperBean();
    0  aload_0 [this]
    1  invokespecial java.lang.Object() [12]
    4  return
      Line numbers:
        [pc: 0, line: 3]
      Local variable table:
        [pc: 0, pc: 5] local: this index: 0 type: foo.bar.SuperBean
      Local variable type table:
        [pc: 0, pc: 5] local: this index: 0 type: foo.bar.SuperBean<T>

  // Method descriptor #21 ()Ljava/lang/Object;
  // Signature: ()TT;
  // Stack: 1, Locals: 1
  public java.lang.Object getFoo();
    0  aload_0 [this]
    1  getfield foo.bar.SuperBean.foo : java.lang.Object [24]
    4  areturn
      Line numbers:
        [pc: 0, line: 8]
      Local variable table:
        [pc: 0, pc: 5] local: this index: 0 type: foo.bar.SuperBean
      Local variable type table:
        [pc: 0, pc: 5] local: this index: 0 type: foo.bar.SuperBean<T>

  // Method descriptor #27 (Ljava/lang/Object;)V
  // Signature: (TT;)V
  // Stack: 2, Locals: 2
  public void setFoo(java.lang.Object foo);
    0  aload_0 [this]
    1  aload_1 [foo]
    2  putfield foo.bar.SuperBean.foo : java.lang.Object [24]
    5  return
      Line numbers:
        [pc: 0, line: 12]
        [pc: 5, line: 13]
      Local variable table:
        [pc: 0, pc: 6] local: this index: 0 type: foo.bar.SuperBean
        [pc: 0, pc: 6] local: foo index: 1 type: java.lang.Object
      Local variable type table:
        [pc: 0, pc: 6] local: this index: 0 type: foo.bar.SuperBean<T>
        [pc: 0, pc: 6] local: foo index: 1 type: T
}

Как видите, и геттер, и сеттер имеют тип java.lang.Object. Introspector использует геттеры и сеттеры для создания дескриптора свойства (поля игнорируются), поэтому свойство никак не может знать универсальный тип T.

person Sean Patrick Floyd    schedule 02.02.2011
comment
Проверяемый объект имеет тип SubBean, а не SuperBean. Файл класса SubBean содержит предложение полного расширения, которое показывает, что T на самом деле означает String для экземпляров SubBean. Эта информация доступна во время выполнения. - person meriton; 03.02.2011
comment
@meriton Верно (думаю, я недостаточно внимательно рассмотрел вопрос). Тем не менее, методы определены в суперклассе, и Introspector использует эти определения и игнорирует информацию об универсальном типе дочернего элемента. - person Sean Patrick Floyd; 03.02.2011