2011-02-02 2 views
8

С учетом следующего кода скелета, можно ли определить, что свойство foo действительно имеет тип String?Generics and java.beans.Introspector

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 класс java.lang.Object

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

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

выход:

класс java.lang.String

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

+1

одноклассник библиотека Java описана в http://www.cowtowncoder.com/blog/archives/2012/04/entry_471.html, кажется, прекрасно поддерживает решения общих типов. Проект доступен в github: https://github.com/cowtowncoder/java-classmate –

+0

Начиная с версии Java 1.7.0_80, выдается 'foo класса java.lang.String TypeVariable: T class java.lang.Object' –

ответ

5

Пока класс выполнения объекта определяет значение параметра типа , вы можете определить его фактическое значение рекурсивно замены формальных параметров типа фактическими, полученными из 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; 
    } 
} 
1

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

Вот ссылка: type erasure

+1

Я боюсь, что это один из случаев, когда Java Tutorial не говорит правду. То, что они хотят сказать, заключается в том, что параметры типа не существуют в байт-коде. Объявленные типы, даже если они являются общими, присутствуют в файле класса и могут быть проверены с помощью отражения. В примере кода из вопроса эта информация достаточна для восстановления параметра типа во время выполнения. – meriton

0

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

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

Как цитирует http://download.oracle.com/javase/1.5.0/docs/guide/language/generics.html

+2

Я боюсь, что это один из случаев, когда Java Tutorial не говорит правду. То, что они хотят сказать, заключается в том, что параметры типа не существуют в байт-коде. Объявленные типы, даже если они являются общими, присутствуют в файле класса и могут быть проверены с помощью отражения. В примере кода из вопроса эта информация достаточна для восстановления параметра типа во время выполнения. – meriton

2

Вы можете получить тип во время выполнения родовым с помощью this hack. Код извлечен из ссылки.

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

Вот байт код 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 использует Getters и Setters для генерации PropertyDescriptor (поля игнорируются), поэтому Property не может знать общий тип T.

+0

Проверяемый объект имеет тип 'SubBean', а не' SuperBean'. Файл класса 'SubBean' содержит полное предложение extends, которое показывает, что' T' фактически означает String для экземпляров 'SubBean'. Эта информация доступна во время выполнения. – meriton

+0

@meriton True (думаю, я не очень хорошо рассмотрел вопрос). Тем не менее, методы определены в суперклассе, и Introspector использует эти определения и игнорирует информацию об общем типе ребенка. –

1

К сожалению, тип стирания в полном объеме.

Хотя кажется, что SubBean должен иметь фиксированный тип String для этого ivar и тех методов, поскольку параметр типа для SuperBean известен во время компиляции, к сожалению, не так, как он работает. Компилятор не создает String -ified версии 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.

+1

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

+0

@ Jörn Horstmann - Спасибо за обновление. Я сожалею, что это не сработало для вас - похоже, должно быть, но, учитывая информацию о компиляторе, созданный метод моста, я вижу замешательство. Единственное другое предложение, о котором я могу думать, это реализовать свойство alias, например. 'public String getFooItem() {return super.getFoo(); } 'дает' SubBean' явно свойство 'FooItem', которое действительно является псевдонимом для' Foo'. –

+1

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

3

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

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

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

Выход:

бар класса java.lang.Object
Совпадение: B: класс java.lang.Integer
Foo класса java.lang.Object
Матч: F: класс java.lang.String
qux класса java.lang.Объект
Match: Q: X

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