Dan Callaghan

Covariant return types for bean property accessors

This afternoon a colleague helped me to track down a nasty bug that I had been struggling with all day.

Our story begins with covariant return types, a new feature added in Java 5. Consider the following contrived example:

interface Currency {
    String getIsoCurrencyCode();
}

class CurrencyEntity implements Currency {
    @Override
    public String getIsoCurrencyCode() {
        return "AUD";
    }
}

interface CurrencyAware {
    Currency getCurrency();
}

class PriceEntity implements CurrencyAware {
    private CurrencyEntity currency;

    @Override
    public CurrencyEntity getCurrency() {
        return currency;
    }

    public void setCurrency(CurrencyEntity currency) {
        this.currency = currency;
    }
}

Notice that PriceEntity implements the getCurrency method declared on the CurrencyAware interface, but the return type is different: the return type is declared as the concrete class CurrencyEntity, rather than the interface Currency which CurrencyEntity implements. Intuitively this seems reasonable, because code which calls the getCurrency method through the CurrencyAware interface will be expecting a return type of Currency, but the actual return type of CurrencyEntity implements the Currency interface, so everything works nicely. Here Java is allowing an implicit “covariant conversion” from the narrower type CurrencyEntity to its broader supertype Currency.

Things are not quite so simple when we examine the disassembled bytecode for PriceEntity however:

class PriceEntity extends java.lang.Object implements CurrencyAware{
PriceEntity();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public CurrencyEntity getCurrency();
  Code:
   0:   aload_0
   1:   getfield    #2; //Field currency:LCurrencyEntity;
   4:   areturn

public void setCurrency(CurrencyEntity);
  Code:
   0:   aload_0
   1:   aload_1
   2:   putfield    #2; //Field currency:LCurrencyEntity;
   5:   return

public Currency getCurrency();
  Code:
   0:   aload_0
   1:   invokevirtual   #3; //Method getCurrency:()LCurrencyEntity;
   4:   areturn

}

The striking thing here is that the class defines two nullary methods named getCurrency. At the source level this would be a compile error, because Java dispatches only on method name and argument types. I'm still not sure how the JVM decides which method to invoke, although it would make no difference since the second version of the method indirects to the first. Clearly the second method exists only to satisfy the CurrencyAware interface.

These duplicate methods give rise to some unexpected results when reflection is applied to this class, as the debugger shows:

PriceEntity.class.getDeclaredMethods()
     (java.lang.reflect.Method[]) [public CurrencyEntity PriceEntity.getCurrency(),
     public Currency PriceEntity.getCurrency(),
     void PriceEntity.setCurrency(CurrencyEntity)]
PriceEntity.class.getMethod("getCurrency", new Class[] { })
     (java.lang.reflect.Method) public CurrencyEntity PriceEntity.getCurrency()

Although getMethod gives us the expected result (namely, a Method object with return type of CurrencyEntity), getDeclaredMethods returns an array with three elements; both of the duplicate getCurrency methods are included.

These duplicate methods can confuse any other code which uses reflection to examine the class with covariant return types. A prime example is java.beans.Introspector, which iterates across all methods of a class in order to find bean property getter-setter pairs. We would expect this process to find a bean property whose type is CurrencyEntity and which is accessed through getCurrency/setCurrency. But if the duplicate getCurrency method is encountered first, the Introspector implementation in Sun’s JDK6 will erroneously identify a read-only bean property of type Currency, accessed through getCurrency with no setter.

java.beans.Introspector.getBeanInfo(PriceEntity.class).getPropertyDescriptors()
     (java.beans.PropertyDescriptor[]) [java.beans.PropertyDescriptor@e858167c,
     java.beans.PropertyDescriptor@f76288f9]
java.beans.Introspector.getBeanInfo(PriceEntity.class).getPropertyDescriptors()[1].getName()
     (java.lang.String) currency
java.beans.Introspector.getBeanInfo(PriceEntity.class).getPropertyDescriptors()[1].getReadMethod()
     (java.lang.reflect.Method) public Currency PriceEntity.getCurrency()
java.beans.Introspector.getBeanInfo(PriceEntity.class).getPropertyDescriptors()[1].getWriteMethod()
     null

The behaviour of Introspector is even more problematic in OpenJDK6 (at least in RHEL version 1.6.0.0-1.7.b09.el5) — the PropertyDescriptor returned will have a pair of accessor methods whose types are incompatible:

java.beans.Introspector.getBeanInfo(PriceEntity.class).getPropertyDescriptors()[1].getName()
     (java.lang.String) currency
java.beans.Introspector.getBeanInfo(PriceEntity.class).getPropertyDescriptors()[1].getReadMethod()
     (java.lang.reflect.Method) public Currency PriceEntity.getCurrency()
java.beans.Introspector.getBeanInfo(PriceEntity.class).getPropertyDescriptors()[1].getWriteMethod()
     (java.lang.reflect.Method) public void PriceEntity.setCurrency(CurrencyEntity)

This can then lead to some very confusing errors in code which relies on Introspector. The first clue to our bug this afternoon was the following bizarre stacktrace from Freemarker:

java.beans.IntrospectionException: type mismatch between read and write methods
        at java.beans.PropertyDescriptor.findPropertyType(PropertyDescriptor.java:657)
        at java.beans.PropertyDescriptor.setWriteMethod(PropertyDescriptor.java:318)
        at java.beans.PropertyDescriptor.<init>(PropertyDescriptor.java:140)
        at freemarker.ext.beans.BeansWrapper.populateClassMapWithBeanInfo(BeansWrapper.java:1127)
        at freemarker.ext.beans.BeansWrapper.populateClassMap(BeansWrapper.java:1017)
        at freemarker.ext.beans.BeansWrapper.introspectClassInternal(BeansWrapper.java:955)
        at freemarker.ext.beans.BeansWrapper.introspectClass(BeansWrapper.java:928)
        at freemarker.ext.beans.BeanModel.<init>(BeanModel.java:139)

Interestingly, this problem with Introspector was reported as a bug against the Spring project several years ago. As I understand it, covariant return types on bean property accessors are only supported correctly in Spring today because that library no longer relies on JavaBeans for its bean property introspection.