I have a public API, used several times across several projects:
public interface Process<C extends ProcessExecutionContext> {
Future<?> performAsync(C context);
}
And an abstract class that takes care of implementing the Future mechanism (not shown). I know that all projects subclass the corresponding abstract class (for which performAsync is final) and no single class implements the abstract interface without subclassing the abstract implementor. This is by design and because this "public" API is "public" within our company.
Finding that Future
is too limitative compared to Spring's ListenableFuture
I decided to extend the interface to
public interface Process<C extends ProcessExecutionContext> {
ListenableFuture<?> performAsync(C context);
}
And I already implemented the ListenableFuture in the single abstract superclass not shown in the example. No other implementation exists, by design.
Every caller so far uses Future
which is a superinterface of ListenableFuture
. Code compiles well if you use Future<?> future = processReturningListenable.performAsync(context)
.
Question is: if I deploy an up-to-date JAR of the public API, containing both the interface and the abstract superclass with ListenableFuture
implementation to existing environments, without recompiling all the projects, does the performAsync
call still work?
I.e. does Java grant binary compatibility of interfaces when they are replaced with a method that return a subtype of the original type?
I am asking this because 1) I find no one available for doing a simple test with an existing JAR file and 2) having to recompile all projects is a red alert.
I assume what I ask is possible because Java method names are identified by a signature that counts the method name and the input parameters. Changing the output parameters doesn't change the method's name
This has been directly addressed in The Java® Language Specification, §13. Binary Compatibility, §13.4.15. Method Result Type:
The referenced §13.4.12 saying:
So the answer is, no, you can’t do that without potentially breaking binary compatibility with existing code.
Technically, it’s simply wrong to assume that methods are identified by name and parameter types only, on the byte code level, they are always identified by name, parameter types and return type.
But note that the cite above states “Such an error will occur only if no method with a matching signature and return type is declared in a superclass”. This guides to a possible work-around:
Now,
Process
inherits a matching method fromLegacyProcess
, a type that doesn’t need to be exported, then overrides it with the more specific return type, as you wish. This is called “co-variant return type”. On the byte code level, there will be a “Future performAsync(…)
” method which delegates to the actual implementation method “ListenableFuture performAsync(…)
”. This automatically generated delegation method is known as bridge method.That way, existing compiled client code continues to work, while every recompiled code will start using the new method directly without the bridge method.