Generics and Covariant Overriding breaks backward compatibility-- How to fix it?
Join the DZone community and get the full member experience.
Join For Freepublic 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 :
- Add the bridge-method-annotation maven dependency
- Add bridge-method-injector maven plugin which will do the byte-code post processing to inject the necessary bridge methods
- 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.
Opinions expressed by DZone contributors are their own.
Comments