问题
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
回答1:
This has been directly addressed in The Java® Language Specification, §13. Binary Compatibility, §13.4.15. Method Result Type:
Changing the result type of a method, or replacing a result type with
void
, or replacingvoid
with a result type, has the combined effect of deleting the old method and adding a new method with the new result type or newlyvoid
result (see §13.4.12).
The referenced §13.4.12 saying:
…
Deleting a method or constructor from a class may break compatibility with any pre-existing binary that referenced this method or constructor; a
NoSuchMethodError
may be thrown when such a reference from a pre-existing binary is linked. Such an error will occur only if no method with a matching signature and return type is declared in a superclass.
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:
interface LegacyProcess<C extends ProcessExecutionContext> {
Future<?> performAsync(C context);
}
public interface Process<C extends ProcessExecutionContext> extends LegacyProcess<C> {
@Override ListenableFuture<?> performAsync(C context);
}
Now, Process
inherits a matching method from LegacyProcess
, 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.
来源:https://stackoverflow.com/questions/43493452/can-i-recompile-a-public-api-with-a-sub-interface-and-keep-binary-compatibility