Generics and java.beans.Introspector

Given the following code skeleton, is it possible to determine that the foo property is really of type 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]); } } } } } 

Actual conclusion

foo class java.lang.Object
TypeVariable: T class java.lang.Object

Edit: I should mention that I know about erasing styles and that the method actually returns an object at the bytecode level. However, metadata about common types is available in the class file and can be requested by reflection, as in the code example. Here is another snippet that shows that there is actually a parameter of type String on SubBean:

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

exit:

class java.lang.String

The question remains, how to bind this actual type argument to a type variable? If I know that there is only one type parameter, it is simple, but I would like this code to work for beans too, which has several typical type parameters.

+8
java generics javabeans introspection
source share
7 answers

While the runtime class of the object determines the value of the type parameter, you can infer its actual value by recursively replacing the parameters of the formal type with the actual ones obtained from 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); } 

Test code:

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

If, on the other hand, the class does not determine the value of the type parameter, you need to extend the bean to save the class object for the actual type parameter at runtime in other ways, for example. by doing:

 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; } } 
+5
source share

I found a solution for the case when there is a hierarchy with one super class (except for Object), which also works when there are several type parameters in the superclass.

Still will not work for deeper hierarchies or when implementing common interfaces. I would also like to receive confirmation that this is actually documented and should work.

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

Output:

bar class java.lang.Object
Match: B: class java.lang.Integer
foo class java.lang.Object
Match: F: class java.lang.String
qux class java.lang.Object
Match: Q: X

I need to write some more tests to determine what to do with the latter case :)

+3
source share

You can get the runtime type with this hack . The code is extracted from the link.

  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)); } } 
+2
source share

Java generics experience style erasure at compile time. At run time, it is not possible to determine the type of T that was present at compile time.

Here is the link: erase type

+1
source share

Unfortunately, type erasure is in full force.

Although it seems that SubBean should have a fixed String type for this ivar and those methods, because the type parameter for SuperBean is known at compile time, unfortunately, is not how it works. The compiler does not create a String -typed version of SuperBean at compile time for SubBean to output from it - there is only one (type-erased) SuperBean

One possible ugly workaround that comes up for me, however, is that SubBean can override the superclass method with a specific version, and then BeanInfo can return what you expect from the methods:

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

Strike>

Update: The above does not work. Pay attention to this information that @ Jörn Horstmann posts in the comments:

This does not work, since the Introspector still returns a read method of type Object. Also, this is apparently a generated bridge method ( http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ102 ), which means that I can run bugs.sun.com/view_bug.do ? bug_id = 6788525 if I want to access the annotations of this method.

Another ugly variation of the above workaround is the property property:

 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 now has the excellent FooItem property, which is an alias for the original SuperBean Foo property.

+1
source share

Unfortunately not:

Generics are implemented by erasing the type: information about the general type is present only at compile time, after which it is deleted by the compiler. The main advantage of this approach is that it provides full compatibility between common code and legacy code that uses unparameterized types (which are technically known as raw types). The main disadvantages are that information about the type of parameters is not available at runtime, and automatically generated drives may not work when interacting with unreliable legacy code. However, it is possible to provide guaranteed runtime security for shared collections, even when interacting with untrusted legacy code.

As stated from http://download.oracle.com/javase/1.5.0/docs/guide/language/generics.html

0
source share

Here's the SuperBean byte code:

 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 } 

As you can see, both getter and setter are of type java.lang.Object. Introspector uses Getters and Setters to generate PropertyDescriptor (fields are ignored), so Property cannot know the general type T.

0
source share

All Articles