2011-02-02 11 views
8

Biorąc pod uwagę następujący szkielet kodu, czy możliwe jest stwierdzenie, że właściwość foo jest w rzeczywistości typu String?Generics i 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]); 
       } 
      } 
     } 
    } 
} 

Rzeczywista moc jest

foo klasy java.lang.Object
TypeVariable: Klasa T java.lang.Object

Edit: I powinien mieć mentionend że wiem o wymazaniu typu i że metoda zwraca obiekt na poziomie kodu bajtowego. Niemniej jednak metadane dotyczące typów ogólnych są dostępne w pliku klasy i mogą być sprawdzane przez odbicie, tak jak w przykładowym kodzie. Oto kolejny fragment, który pokazuje, że w rzeczywistości SubBean ma parametr typu typu String:

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

wyjściowa:

klasa java.lang.String

Pytanie pozostaje wtedy, jak powiązać ten faktyczny typ z zmienną typu? Jeśli wiem, że istnieje tylko jeden parametr typu, jest to proste, ale chciałbym, aby ten kod działał również dla komponentów o wielu ogólnych parametrach typu.

+1

Classmate biblioteki Java opisane w http://www.cowtowncoder.com/blog/archives/2012/04/entry_471.html wydaje się ładnie wspierać rozwiązywanie typów generycznych. Projekt jest dostępny pod adresem github: https://github.com/cowtowncoder/java-classmate –

+0

Od wersji 1.7.0_80 języka Java jest wynikiem 'foo klasy java.lang.String TypeVariable: T class java.lang.Object' –

Odpowiedz

5

Dopóki klasa wykonawcze obiektu determinuje wartość parametru type można wnioskować o jego rzeczywistej wartości rekurencyjnie zastępując formalne parametry typu przez rzeczywistymi uzyskanymi z Class.getGenericSuperClass(): kod

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

test:

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> { } 

Jeśli z drugiej strony klasa nie określa wartości parametru typu, należy rozszerzyć komponent bean, aby zachować obiekt klasy dla rzeczywistego parametru typu w czasie wykonywania w inny sposób, np. wykonując:

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

Typowe usuwanie języka Java podczas kompilacji. W czasie wykonywania nie można określić typu T, który był obecny podczas kompilacji.

Oto link: type erasure

+1

Obawiam się, że jest to jeden z przypadków, w których tutorial Java nie mówi całej prawdy. Chodzi o to, że parametry typu nie istnieją w bajtodzie. Zadeklarowane typy, nawet jeśli są ogólne, są obecne w pliku klasy i mogą być sprawdzane za pomocą refleksji. W próbce kodu z pytania informacje te są wystarczające do zrekonstruowania parametru typu w czasie wykonywania. – meriton

0

Niestety, nie:

Leki generyczne są realizowane według rodzaju skasowaniem: ogólne informacje typ występuje tylko w czasie kompilacji, po którym jest on usuwany przez kompilator. Główną zaletą tego podejścia jest to, że zapewnia on całkowitą interoperacyjność między kodem generycznym a starszym kodem, który wykorzystuje typy nieparametryczne (które są technicznie znane jako typy surowe). Główną wadą jest to, że informacje o typie parametrów nie są dostępne w czasie wykonywania, a automatyczne generowanie rzutów może się nie powieść podczas współpracy ze źle zachowanym starym kodem. Istnieje jednak sposób na uzyskanie gwarantowanego bezpieczeństwa typu run-time w przypadku ogólnych kolekcji, nawet w przypadku współpracy z niewłaściwie zachowanym starym kodem.

jako cytat z http://download.oracle.com/javase/1.5.0/docs/guide/language/generics.html

+2

Obawiam się, że jest to jeden z przypadków, w których tutorial Java nie mówi całej prawdy. Chodzi o to, że parametry typu nie istnieją w bajtodzie. Zadeklarowane typy, nawet jeśli są ogólne, są obecne w pliku klasy i mogą być sprawdzane za pomocą refleksji. W próbce kodu z pytania informacje te są wystarczające do zrekonstruowania parametru typu w czasie wykonywania. – meriton

2

można uzyskać typ rodzajowy runtime za pomocą this hack. Kod wyodrębniony z linku.

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

Oto kod bajtowy 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 
} 

Jak widać, zarówno getter i setter są typu java.lang.Object. Introspector używa Getters i Setters do generowania PropertyDescriptor (pola są ignorowane), więc nie ma możliwości, aby Właściwość znała ogólny typ T.

+0

Kontrolowany obiekt ma typ 'SubBean', a nie' SuperBean'. Plik klasy 'SubBean' zawiera pełną rozszerzoną klauzulę, która ujawnia, że' T' faktycznie oznacza String dla instancji 'SubBean'. Ta informacja jest dostępna w czasie wykonywania. – meriton

+0

@meriton Prawda (chyba nie spojrzałem wystarczająco dobrze na to pytanie). Mimo to metody są zdefiniowane w superklasie, a Introspector używa tych definicji i ignoruje ogólny rodzaj informacji dziecka. –

1

Niestety, wymazanie typu jest w pełni skuteczne.

Chociaż wydaje się, że SubBean powinien mieć stały typ String dla tego ivar i tych metod, ponieważ parametr type dla SuperBean jest znany w czasie kompilacji, niestety nie tak działa. Kompilator nie tworzy String -ified wersję SuperBean w czasie kompilacji dla SubBean czerpać z - istnieje tylko jeden (typu skasowane) SuperBean

Jeden ewentualnie brzydki obejście, które przychodzi mi do głowy jest jednak, że SubBean może być w stanie przesłonić metody nadklasy z wersją specyficzny dla danego typu, a następnie BeanInfo może powrócić do tego, co można oczekiwać metod:

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

Aktualizacja: Powyższe nie działa. Uwaga ta informacja, że ​​@ Jörn Horstmann posty w komentarzach:

To nie wydaje się działać, jak introspektor wciąż zwraca metodę odczytu typu Object. Dodatkowo wydaje się, że jest to metoda generowania mostu (http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ102), co oznacza, że ​​mogę uzyskać dostęp do bugs.sun.com/view_bug.do?bug_id=6788525, jeśli chcę uzyskać dostęp do adnotacji w tej metodzie.

Innym brzydki wariancie powyższej obejścia skojarzenie właściwości:

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 teraz ma wyraźne właściwości FooItem który jest alias dla pierwotnego SuperBean własności Foo.

+1

Nie wydaje się to działać, ponieważ introspektor wciąż zwraca metodę odczytu typu Object. Dodatkowo wydaje się, że jest to metoda generowania mostu (http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ102), co oznacza, że ​​mogę natknąć się na http://bugs.sun.com/view_bug.do ? bug_id = 6788525, jeśli chcę uzyskać dostęp do adnotacji w tej metodzie. –

+0

@ Jörn Horstmann - Dzięki za aktualizację. Przykro mi, że to nie działało dla ciebie - wydawało się, że powinno, ale biorąc pod uwagę informacje o generowanych przez kompilator metodach mostu, widzę zamieszanie. Jedyna inna sugestia, jaką mogę wymyślić, to zaimplementować właściwość aliasu, np. 'public String getFooItem() {return super.getFoo(); } 'daje' SubBean' wyraźnie właściwość 'FooItem', która jest tak naprawdę aliasem dla' Foo'. –

+1

Dzięki, przesłanianie metod było zdecydowanie warte spróbowania i pomogło w zrozumieniu innych aspektów implementacji generycznych. Ale moim celem jest użycie tego w bibliotece do introspekcji dowolnych ziaren bez konieczności ich wcześniejszego modyfikowania. –

3

Znalazłem rozwiązanie dla przypadku, w którym istnieje hierarchia z pojedynczą klasą super (oprócz obiektu), która działa również, gdy istnieje wiele parametrów typu na super klasie.

Nadal nie działałby na rzecz większej hierarchii lub podczas implementowania ogólnych interfejsów . Chciałbym również potwierdzenie, że jest to faktycznie udokumentowane i powinno działać.

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

wyjściowa:

bar klasy java.lang.Object
Partner: B: Klasa java.lang.Integer
foo klasy java.lang.Object
Dopasowanie: F: klasa java.lang.String
qux klasy java.lang.Obiekt
meczu: P: X

muszę napisać kilka testów, aby ustalić, co zrobić z ostatnim ostatnim przypadku :)