Shekhar Gulati is a Java Consultant with over 5 years experience.He is currently working with Xebia India an Agile Software Development company.The postings on this site and on his blog are his own and do not necessarily represent opinion of his employer. His own blog is at http://whyjava.wordpress.com/ and you can follow him on twitter here. Shekhar has posted 25 posts at DZone. View Full User Profile

Generics and Covariant Overriding breaks backward compatibility-- How to fix it?

02.09.2011
| 5854 views |
  • submit to reddit
Generics and Covariant Overriding are very useful features which were added to Java 5. Generics allows a type or method to operate on objects of various types while providing compile-time type safety whereas Covariant overriding allows changing the method return type to subtype of the return type of the super class method when overriding it in a sub-class. Both of these features help in designing type safe clean API's. Whenever you are designing a new API or redesigning your legacy code which is not currently used by anyone, you do not have to worry about binary compatibility as no one is using it and changing it will not break the binary compatibility. But usually there are number of clients or consumers of your API who have to be recompiled to work with the new code otherwise binary compatibility will break. For example, in legacy code you have the following code
public class MyService{
public A getA(){
return new A();
}
}

and in the new redesigned code you changed the code so that it return subtype of A called ASubtype.

public class MyService{
public ASubtype getA(){
return new ASubtype();
}
}

In the above code snippet if the client were using the legacy version of getA() method which was returning A then they have to be recompiled in order for them to work with new getA() method which is returning subtype of A . The same is true when we generify our code. For example, suppose we have a class MyService which implements an interface Service as shown below

public interface Service {

String getMessage(Object request);

}

public class ExampleService implements Service {

public String getMessage(Object request) {
return "Hello world!";
}

}

When we generify the code(as shown below) i.e. we add type parameter T to Service interface and the ExampleService which implements the generic Service interface now have getMessage() method which takes String argument instead of Object . Now all the clients of the ExampleService API will need to be recompiled against the new signature otherwise binary compatibility will break.

public interface Service<T> {
String getMessage(T request);
}

public class ExampleService implements Service {
public String getMessage(String request) {
return "Hello world!";
}
}

This leads to an interesting question how it works in standard Java code. The same problem should have occurred when interfaces like Comparable or Comparator and many others were generified because they also used to take Object as arguments and they were generified to take T type parameter.  But the classes like Integer, String, etc. which implement these interfaces still remain binary compatible. I found the answer how binary compatibility is maintained in Java SDK while reading Java Generics and Collections Book. Java Generics and Collections book is a great reference for learning Generics. In Java SDK this problem is solved by adding additional methods to the class files. These methods are generated automatically by compiler and are called bridges. So, the compiled class file will contain two version of the method one that takes type parameter specified by implementing class i.e., Integer or String and other that take Object as argument.The one that takes Object as argument is added by the compiler. You can find the same by de-compiling the Integer or String class. De-compiled version of Integer class is as shown below contains two version of compareTo method as shown below

public int compareTo(Integer integer){
int i = value;
int j = integer.value;
return i >= j ? ((int) (i != j ? 1 : 0)) : -1;
}

public volatile int compareTo(Object obj){
return compareTo((Integer)obj);
}

As you can see above the decompiled version of Integer class has two version of compareTo() method. The first compareTo(Integer integer) is the one which exists in the source code of Integer class but the second method compareTo(Object obj) is a bridge method which is added by compiler. This is how binary compatibility is maintained by Java.

But how can we maintain binary compatibility of our code?

It is great that JDK maintains binary compatibility but how can we maintain binary compatibility of the code that we write. Is there a way to generate bridge methods for the code that we have written?

Yes we can maintain binary compatibility of our code by using a small library called Bridge Method Injection. This will generate the required bridge methods for the client to work without recompiling their code.

Before we apply this to our Covariant overriding example we need to integrate this library in our build system. There are three things that we need to do for its integration :

  1. Add the bridge-method-annotation maven dependency
  2. Add bridge-method-injector maven plugin which will do the byte-code post processing to inject the necessary bridge methods
  3. Add repositories from where plugins and dependencies can be downloaded.

All the above three steps are shown below in the sample pom.xml

<?xml version="1.0" encoding="UTF-8"?>
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>com.shekhar.jl</groupId>
<artifactId>bridge-method-injection-example</artifactId>
<version>1.0.0.CI-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.test.failure.ignore>true</maven.test.failure.ignore>
</properties>
<profiles>
<profile>
<id>strict</id>
<properties>
<maven.test.failure.ignore>false</maven.test.failure.ignore>
</properties>
</profile>
</profiles>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.infradna.tool</groupId>
<artifactId>bridge-method-annotation</artifactId>
<version>1.4</version>
<optional>true</optional>
</dependency>
</dependencies>

<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<inherited>false</inherited>
<configuration>
<descriptorRefs>
<descriptorRef>project</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
<plugin>
<groupId>com.infradna.tool</groupId>
<artifactId>bridge-method-injector</artifactId>
<version>1.4</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

<repositories>
<repository>
<id>bridgeMethodInjection</id>
<name>bridgeMethodInjection</name>
<url>http://maven.dyndns.org/2/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>bridgeMethodInjection</id>
<name>bridgeMethodInjection</name>
<url>http://maven.dyndns.org/2/</url>
</pluginRepository>
</pluginRepositories>
</project>

 Now lets apply this to our Covariant example. This is done by applying a @WithBridgeMethod annotation as shown below. This annotation tells the byte code processor to add the bridge method to your class file with return type as A.

public class MyService{
@WithBridgeMethods(A.class)
public ASubtype getA(){
return new ASubtype();
}
}

 The same can be done in case of Generics and you can refer to project documentation for more.

Published at DZone with permission of its author, Shekhar Gulati.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Rémi Forax replied on Wed, 2011/02/09 - 1:03pm

I don't understand why you don't use a bridge method generated by javac. 

class CompatBridgeService {
    public abstract A getA();
}

public class MyService extends CompatBridgeService {
    public ASubtype getA() { ... }   
}

Rémi

 

Ricky Clarkson replied on Wed, 2011/02/09 - 1:32pm

There's nothing special about how the compiler treats JDK classes; your own classes get the exact same bridge methods. As far as I can tell, whatever the XML is you pasted above is entirely unnecessary.

Christian Schli... replied on Wed, 2011/02/09 - 9:59pm

The suggestion in the headline is wrong: Generics and Coviant overriding do not break binary compatibility. To the contrary, they were designed so that binary compatibility is maintained.

So what's wrong here? Consider your first two examples. You are not using inheritance and hence no covariant overriding. Rather than that, you've changed the method signature for getA(). You could have subclassed the MyService class of the first example instead and then applied covariant overriding to the method.

Likewise for the generics examples...

Mustafa Asif replied on Wed, 2013/11/27 - 11:27pm

Why would you need to recompile client code if a service starts returning subtype instead of supertype for a method call like described in the initial part of your blog for method getA()?

Can you please give an example?

Thanks

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.