DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
The Latest "Software Integration: The Intersection of APIs, Microservices, and Cloud-Based Systems" Trend Report
Get the report
  1. DZone
  2. Data Engineering
  3. Data
  4. Generics and Covariant Overriding breaks backward compatibility-- How to fix it?

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

Shekhar Gulati user avatar by
Shekhar Gulati
·
Feb. 09, 11 · Interview
Like (0)
Save
Tweet
Share
7.32K Views

Join the DZone community and get the full member experience.

Join For Free
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.

Compatibility (chemical) Data Types Java (programming language)

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • Configure Kubernetes Health Checks
  • Important Takeaways for PostgreSQL Indexes
  • What Is QA as a Service?
  • Reconciling Java and DevOps with JeKa

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: