Jump to: navigation, search

Difference between revisions of "Evolving Java-based APIs 2"

m (New page: = Evolving Java-based APIs (part 2) = == Achieving API Binary Compatibility == <blockquote><i>"[A]n object-oriented model must be carefully designed so that class-library transformations...)
 
m (fixed links to examples)
(9 intermediate revisions by 3 users not shown)
Line 1: Line 1:
= Evolving Java-based APIs (part 2) =
+
Part 2 of [[Evolving Java-based APIs | Evolving_Java-based_APIs]].
  
 
== Achieving API Binary Compatibility ==
 
== Achieving API Binary Compatibility ==
Line 164: Line 164:
  
 
<td><b><font color="#FF0000">Breaks compatibility</font></b></td>
 
<td><b><font color="#FF0000">Breaks compatibility</font></b></td>
 +
</tr>
 +
 +
<tr>
 +
<td ROWSPAN="2">Move API method up type hierarchy</td>
 +
 +
    <td width="35%">If method in supertype need not be implemented by Client</td>
 +
 +
    <td width="25%">Binary compatible</td>
 +
</tr>
 +
 +
<tr>
 +
<td>If method in supertype must be implemented by Client</td>
 +
 +
<td><b><font color="#FF0000">Breaks compatibility</font></b> (7)</td>
 +
</tr>
 +
 +
<tr>
 +
<td>Move API method down type hierarchy</td>
 +
 +
<td>-</td>
 +
 +
<td><b><font color="#FF0000">Breaks compatibility (7)</font></b></td>
 
</tr>
 
</tr>
  
Line 299: Line 321:
 
</table>
 
</table>
  
(0) Although adding a new method to an API interface which need not be reimplemented by Clients does not break binary compatibility, a pre-existing Client subclass of an existing implementation might still provide a pre-existing implementation of a method by this name. See [[#Example 4 - Adding an API method]] in the preceding section for why this breaks API contract compatibility.  
+
(0) Although adding a new method to an API interface which need not be reimplemented by Clients does not break binary compatibility, a pre-existing Client subclass of an existing implementation might still provide a pre-existing implementation of a method by this name. See [[Evolving Java-based APIs#Example 4 - Adding an API method]] in the preceding section for why this breaks API contract compatibility.  
  
 
(1) Adding a new method to an API interface that is implemented by Clients (e.g., a callback, listener, or visitor interface) breaks compatibility because hypothetical pre-existing implementations do not implement the new method.
 
(1) Adding a new method to an API interface that is implemented by Clients (e.g., a callback, listener, or visitor interface) breaks compatibility because hypothetical pre-existing implementations do not implement the new method.
Line 312: Line 334:
  
 
(6) Existing annotations that mention the deleted element will cause an exception (AnnotationTypeMismatchException) to be thrown when the annotation is read.
 
(6) Existing annotations that mention the deleted element will cause an exception (AnnotationTypeMismatchException) to be thrown when the annotation is read.
 +
 +
(7) Moving methods (and other members) up and down the type hierarchy are composite changes that must always be analyzed in terms of the net effect on the types involved plus any types laying along the path. For instance, the net effect of moving an API method to a supertype is an API method being added to the supertype; the subtype is not normally affected because the API method would still be inherited.
  
 
==== Evolving API interfaces - API methods ====
 
==== Evolving API interfaces - API methods ====
Line 541: Line 565:
  
 
<td><b><font color="#FF0000">Breaks compatibility</font></b></td>
 
<td><b><font color="#FF0000">Breaks compatibility</font></b></td>
 +
</tr>
 +
 +
<tr>
 +
<td ROWSPAN="2">Move API method up type hierarchy</td>
 +
 +
    <td width="35%">If method in supertype need not be reimplemented by Client</td>
 +
 +
    <td width="25%">Binary compatible</td>
 +
</tr>
 +
 +
<tr>
 +
<td>If method in supertype must be reimplemented by Client</td>
 +
 +
<td><b><font color="#FF0000">Breaks compatibility</font></b> (9)</td>
 +
</tr>
 +
 +
<tr>
 +
<td>Move API method down type hierarchy</td>
 +
 +
<td>-</td>
 +
 +
<td><b><font color="#FF0000">Breaks compatibility</font></b> (9)</td>
 
</tr>
 
</tr>
  
Line 766: Line 812:
 
</table>
 
</table>
  
(0) Although adding a new method to an API class which need not be reimplemented by Clients does not break binary compatibility, a pre-existing subclass might still provide a pre-existing implementation of a method by this name. See [[#Example 4 - Adding an API method]] in the preceding section for why this breaks API contract compatibility.  
+
(0) Although adding a new method to an API class which need not be reimplemented by Clients does not break binary compatibility, a pre-existing subclass might still provide a pre-existing implementation of a method by this name. See [[Evolving Java-based APIs#Example 4 - Adding an API method]] in the preceding section for why this breaks API contract compatibility.  
  
 
(1) Adding a new method to an API class that must be reimplemented by Clients breaks compatibility because pre-existing subclasses would not provide any such implementation.
 
(1) Adding a new method to an API class that must be reimplemented by Clients breaks compatibility because pre-existing subclasses would not provide any such implementation.
Line 783: Line 829:
  
 
(8) Client code can use the values() method to determine the ordinal positions of the enum constants. So although this is a binary compatible change, it may break contractual compatibility.
 
(8) Client code can use the values() method to determine the ordinal positions of the enum constants. So although this is a binary compatible change, it may break contractual compatibility.
 +
 +
(9) Moving methods (and other members) up and down the type hierarchy are composite changes that must always be analyzed in terms of the net effect on the types involved plus any types laying along the path. For instance, the net effect of moving an API method to a supertype is an API method being added to the supertype; the subtype is not normally affected because the API method would still be inherited.
  
 
==== Evolving API classes - API methods and constructors ====
 
==== Evolving API classes - API methods and constructors ====
Line 1,158: Line 1,206:
 
(2) Java compilers always inline the value of constant fields (ones with compile-time computable values, whether primitive or <code>String</code> type). As a consequence, changing the value of an API constant field does not affect pre-existing Clients. Invariably, this does not meet the objective for changing the API field's value.  
 
(2) Java compilers always inline the value of constant fields (ones with compile-time computable values, whether primitive or <code>String</code> type). As a consequence, changing the value of an API constant field does not affect pre-existing Clients. Invariably, this does not meet the objective for changing the API field's value.  
  
(3) Java compilers always inline the value of constant fields (ones with a compile-time computable values, whether primitive or <code>String</code>type). As a consequence, changing an API constant field into a non-<code>final</code> one does not propagate to pre-existing Clients. Invariably, this does not meet the objective for making the API field non-<code>final</code>.  
+
(3) Java compilers always inline the value of constant fields (ones with compile-time computable values, whether primitive or <code>String</code> type). As a consequence, changing an API constant field into a non-<code>final</code> one does not propagate to pre-existing Clients. Invariably, this does not meet the objective for making the API field non-<code>final</code>.  
  
 
(4) Making an API field final breaks compatibility with pre-existing binaries that attempt to assign new values to the field.
 
(4) Making an API field final breaks compatibility with pre-existing binaries that attempt to assign new values to the field.
Line 1,270: Line 1,318:
 
an example of a method that became both generic and variable arity in 1.5.
 
an example of a method that became both generic and variable arity in 1.5.
  
Two final notes. Regarding whether existing APIs ought to embrace generics,
+
<p>Two final notes. Regarding whether existing APIs ought to embrace generics,
 
the Java Language Specification says only this:</p>
 
the Java Language Specification says only this:</p>
 
<blockquote>
 
<blockquote>
Line 1,300: Line 1,348:
  
 
Parties that declare annotation types should try to provide helpful guidance for their customers.
 
Parties that declare annotation types should try to provide helpful guidance for their customers.
 
== Data Compatibility ==
 
The Component implementation may need to store and retrieve its internal
 
data from a file. For example, Microsoft Word stores a document in a file. When one
 
of these files may live from release to release, clients would break if
 
the format or interpretation of that data changed in an incompatible way.
 
<br><b>Data compatibility</b> is an additional issue for components with
 
persistent data.
 
<p>The standard technique is to tag all stored data with its format version
 
number. The format version number is increased when the format is changed
 
from one release to the next. The Component implementation contains readers
 
for the current format version and for all past versions, but usually only
 
the writer for the current format version (unless for some reason there
 
is an ongoing need to write older versions).
 
 
== Standard Workarounds ==
 
 
When evolving APIs, the prime directive places serious constraints on how
 
this can be done.
 
 
Here are some standard techniques that come in handy when you're caught
 
between a rock and a hard place. They're not necessarily pretty, but they
 
get the job done.
 
 
=== Deprecate and Forward ===
 
 
When some part of the Component API is made obsolete by some new and improved
 
Component API, the old API should be marked as deprecated using the <code>@deprecated</code>
 
Javadoc tag (the comment directing the reader attention to the replacement
 
API). When feasible, the implementation of the old API should forward the
 
message to the corresponding method in the replacement API; doing so will
 
mean that any performance improvements or bug fixes made to the implementation
 
of the new API will automatically be of benefit to clients of the old API.
 
 
=== Start over in a New Package ===
 
 
Even simpler than Deprecate and Forward, the Component API and implementation
 
can be redone in new packages. The old API and implementation are left
 
in the old location untouched, except to mark them as deprecated. Old and
 
new API and implementations co-exist independent of one another.
 
 
=== Adding an argument ===
 
 
Here is a simple technique for adding an argument to a method that is intended to be overridden by subclasses. For example the <code>Viewer.inputChanged(Object input)</code> method should get an additional argument <code>Object oldInput</code>. Adding the argument results in pre-existing clients overridding the wrong method. The workaround is to call the old method as the default implementation
 
of the new method:
 
 
<pre>
 
public void inputChanged(Object input, Object oldInput) {
 
  inputChanged(input);
 
}
 
</pre>
 
 
Pre-existing clients which override the old method continue to work; and all calls to the old method continue to work New or upgraded clients will override the new method; and all calls to the new method will work, even if they happen to invoke an old implementation.
 
 
=== "2" Convention ===
 
The first release of an API callback-style interface didn't work as well as hoped. For example, the first release contained:
 
 
<pre>
 
public interface IProgressMonitor {
 
  void start();
 
  void stop();
 
}
 
</pre>
 
 
You now wish you had something like:
 
 
<pre>
 
public interface IProgressMonitor {
 
  void start(int total);
 
  void worked(int units);
 
  void stop();
 
}
 
</pre>
 
 
But it's too late to change <code>IProgressMonitor</code> to be that API. So you mark <code>IProgressMonitor</code> as deprecated and introduce the new and improved one under the name <code>IProgressMonitor2</code> (a name everyone recognizes as the second attempt):
 
 
<pre>
 
public interface IProgressMonitor2 extends IProgressMonitor {
 
  void start(int total);
 
  void worked(int units);
 
  void stop();
 
}
 
</pre>
 
 
By declaring the new interface to extend the old one, any object of type <code>IProgressMonitor2</code> can be passed to a method expecting an old <code>IProgressMonitor</code>.
 
 
=== COM Style ===
 
 
The "COM style" is to not implement interfaces directly but to ask for an interface
 
by using <code>getAdapter(someInterfaceID)</code>. This allows adding new interfaces
 
in the implementation without breaking existing classes.
 
 
=== Making Obsolete Hook Methods Final ===
 
 
As a framework evolves, it may sometimes be necessary to break compatibility.
 
When compatibility is being broken knowingly, there are some tricks that
 
make it easier for broken clients to find and fix the breakage.
 
 
A common situation occurs when the signature of a framework hook method
 
is changed. Overridding a hook method that is no longer called by the base
 
class can be tough to track down, especially if the base class contains
 
a default implementation of the hook method. In order to make this jump
 
out, the obsolete method should be marked as <code>final</code> in addition
 
to being deprecated. This ensures that existing subclasses which override
 
the obsolete method will no longer compile or link.
 
 
== Defective API Specifications ==
 
 
As hard as one might try, achieving perfect APIs is difficult. The harsh
 
reality is that some parts of large Component API will be specified better
 
than others.
 
 
One problem is specification bugs---when the API spec actually says
 
the wrong thing. Every effort should be made to catch these prior to release.
 
 
Another problem is underspecification---when the API spec does not specify
 
enough. In some cases, the implementor will notice this before the API
 
is ever released. In other cases, the specification will be adequate for
 
the implementor's needs but inadequate for clients. When an API is released
 
in advance of serious usage from real clients, it may be discovered too
 
late that the specification should have been tighter or, even worse, that
 
the API should have been designed differently.
 
 
When you find out that you're saddled with a defective API specification,
 
these points are worth bearing in mind:
 
<ul>
 
<li>
 
APIs are not sacrosanct; it's just that breaking compatibility is usually
 
very costly. For a truly unusable feature, the cost is likely much lower.</li>
 
 
<li>
 
Tightening up a seriously weak specification can often be achieved without
 
breaking compatibility by changing the specification in a way consistent
 
with the existing implementation. That is, codify more of how it actually
 
works to ensure that clients that currently work continue to work in subsequent
 
releases.</li>
 
 
<li>
 
Breaking compatibility in a limited way may be cheaper in the long run
 
that leaving a bad patch of API as it is.</li>
 
 
<li>
 
If you break compatibility between releases, do it in a controlled way
 
that only breaks those Clients that actually utilize of the bad parts of
 
the API. This localizes the pain to affected Clients (and their downstream
 
customers), rather than foisting a "Big Bang" release on everyone.</li>
 
 
<li>
 
Document all breaking API changes in the release notes. Clients appreciate this
 
much more than discovering for themselves that you knowingly broke them.</li>
 
</ul>
 
 
== A Word about Source Code Incompatibilities ==
 
 
While the idea that the Java source code for existing Clients should continue to compile without errors against the revised Component API, this is not strictly necessary (and not always achievable). API contract and binary compatibility are the only hard requirements. Source code incompatibilities are not worth losing sleep over because the Client's owner can easily correct these problems if they do arise with only localized editing of the source code.
 
 
The following is a list of known kinds of Java source code incompatibilities that can arise as APIs evolve:
 
<ul>
 
<li>
 
Ambiguities involving type-import-on-demand declarations.</li>
 
 
<ul>
 
 
<li>
 
Triggered by: adding an API class or interface.</li>
 
 
<li>
 
Remedy: add single type import declaration to disambiguate.</li>
 
 
<li>
 
Avoidance strategy: use at most one type-import-on-demand declaration per
 
compilation unit.</li>
 
</ul>
 
 
<li>
 
Ambiguities involving overloaded methods.</li>
 
 
<ul>
 
<li>
 
Triggered by: adding an overloaded API method or constructor.</li>
 
 
<li>
 
Remedy: add casts to disambiguate ambiguously typed arguments.</li>
 
 
<li>
 
Avoidance strategy: put casts on null arguments.</li>
 
</ul>
 
 
<li>
 
Ambiguities involving field and type member hiding.</li>
 
 
<ul>
 
<li>
 
Triggered by: adding an API field.</li>
 
 
<li>
 
Remedy: add qualification to disambiguate ambiguous field references.</li>
 
 
<li>
 
Avoidance strategy: none.</li>
 
</ul>
 
 
<li>
 
Ambiguities involving fields and local variables.</li>
 
 
<ul>
 
<li>
 
Triggered by: adding an API field.</li>
 
 
<li>
 
Remedy: rename conflicting local variables to avoid new field name.</li>
 
 
<li>
 
Avoidance strategy: don't declared API fields in classes and interfaces
 
that Clients implement.</li>
 
</ul>
 
 
<li>
 
Problems involving checked exceptions thrown by methods.</li>
 
<ul>
 
<li>
 
Triggered by: removing checked exceptions from a method's <code>throws</code>
 
clause.</li>
 
<li>
 
Remedy: add or remove exception handlers as required.</li>
 
<li>
 
Avoidance strategy: none.</li>
 
</ul>
 
</ul>
 
  
 
Copyright © 2000, 2007 IBM Corporation
 
Copyright © 2000, 2007 IBM Corporation
  
 
[[Category:API]]
 
[[Category:API]]

Revision as of 04:08, 24 November 2008

Part 2 of Evolving_Java-based_APIs.

Achieving API Binary Compatibility

"[A]n object-oriented model must be carefully designed so

that class-library transformations that should not break already compiled applications, indeed, do not break such applications."
---Ira Forman, Michael Conner, Scott Danforth, and Larry Raper, "Release-to-Release Binary Compatibility in SOM", in Proceedings of OOPSLA '95.

Achieving API binary compatibility depends in part on the Java language's notion of binary compatibility:

"A change to a type is binary compatible with (equivalently, does not break binary compatibility with) preexisting binaries if preexisting binaries that previously linked without error will continue to link without error." (JLS3, 13.2)

Reference: Gosling, Joy, Steele, and Bracha, The Java Language Specification, Third Edition, Addison-Wesley, 2005; chapter 13 Binary Compatibility.

The tables in the following sections summarize which kinds of changes break API binary compatibility.

Bear in mind that many changes will have effects in several places. For example, defining a new public interface with one public method and adding that interface as the superinterface of an existing interface has the following ramifications:

  • a new public API interface is added to an API package
  • the superinterface set of the existing API interface has expanded
  • a new public API method is added to the existing API interface

Each of these individual net effects could break binary compatibility. Use the tables to determine whether the net effects preserve or break compatibility.

Evolving API packages

It is always possible to evolve the Component API to include a new API package. However, once introduced in a release, an API package cannot easily be withdrawn from service. When an API package becomes obsolete, its API classes and API interfaces should continue to work but be marked as deprecated. After a couple of releases, it may be possible to phase out an obsolete API package.

The names of non-public (non-API) types in API packages do not appear in Client source code or binaries. Non-API types can be added or deleted without jeopardizing binary compatibility. However, once made public in a release, these types are part of the API and cannot easily be withdrawn from service without breaking existing Clients. When an API type becomes obsolete, it should continue to work but be marked as deprecated.
 

Add API package - Binary compatible
Delete API package - Breaks compatibility
Add API type to API package - Binary compatible
Delete API type from API package - Breaks compatibility
Add non-public (non-API) type to API package - Binary compatible
Delete non-public (non-API) type from API package - Binary compatible
Change non-public (non-API) type in API package to make public (API) - Binary compatible
Change public type in API package to make non-public - Breaks compatibility
Change kind of API type (class, interface, enum, or annotation type) - Breaks compatibility (1)

(1) API class-interface gender changes break binary compatibility, even in cases where the class/interface is used by, but not implemented by, Clients. This is because the Java VM bytecodes for invoking a method declared in an interface are different from the ones used for invoking a method declared in a class. More generally, all gender changes involving classes, enums, interfaces, and annotation types break binary compatibility for one reason or another.

Evolving API Interfaces

Evolving API interfaces is somewhat more straightforward than API classes since all methods are public and abstract, all fields are public static and final, all type members are public and static, and there are no constructors. Annotation types (@interface) , which are a form of interface, are also covered.

Add API method If method need not be implemented by Client Binary compatible (0)
If method must be implemented by Client Breaks compatibility (1)
Delete API method  - Breaks compatibility
Move API method up type hierarchy If method in supertype need not be implemented by Client Binary compatible
If method in supertype must be implemented by Client Breaks compatibility (7)
Move API method down type hierarchy - Breaks compatibility (7)
Add API field If interface not implementable by Clients Binary compatible
If interface implementable by Clients Breaks compatibility (2)
Delete API field - Breaks compatibility
Expand superinterface set (direct or inherited) - Binary compatible
Contract superinterface set (direct or inherited) - Breaks compatibility (3)
Add, delete, or change static initializers - Binary compatible
Add API type member If interface not implementable by Clients Binary compatible
If interface implementable by Clients Breaks compatibility (2)
Delete API type member - Breaks compatibility
Re-order field, method, and type member declarations - Binary compatible
Add type parameter If interface has no type parameters Binary compatible (4)
If interface has type parameters Breaks compatibility
Delete type parameter - Breaks compatibility
Re-order type parameters - Breaks compatibility
Rename type parameter - Binary compatible
Add, delete, or change type bounds of type parameter - Breaks compatibility
Add element to annotation type If element has a default value Binary compatible
If element has no default value Breaks compatibility (5)
Delete element from annotation type - Breaks compatibility (6)

(0) Although adding a new method to an API interface which need not be reimplemented by Clients does not break binary compatibility, a pre-existing Client subclass of an existing implementation might still provide a pre-existing implementation of a method by this name. See Evolving Java-based APIs#Example 4 - Adding an API method in the preceding section for why this breaks API contract compatibility.

(1) Adding a new method to an API interface that is implemented by Clients (e.g., a callback, listener, or visitor interface) breaks compatibility because hypothetical pre-existing implementations do not implement the new method.

(2) Adding an API field to an API interface that is implemented by Clients (e.g., a callback, listener, or visitor interface) breaks binary compatibility in a different way. A field added to a superinterface of C may hide an instance field inherited from a superclass of C, causing linking errors to be detected. Because of this fact, it is important to distinguish between API interfaces that Clients should implement from those that Clients should merely use. API interfaces that Clients should implement should not include fields.

(3) Shrinking the set of API interfaces that a given API interfaces extends (either directly or inherited) breaks compatibility because some casts between API interfaces in hypothetical pre-existing Client code between will no longer work. However, non-API superinterfaces can be removed without breaking binary compatibility.

(4) Altering the type parameters of a parameterized type breaks compatibility. However, adding type parameters to a previously unparameterized type retains compatibility because of Java's special treatment of legacy references (raw types).

(5) Existing annotations would not have a value for the new element, causing an exception (IncompleteAnnotationException) to be thrown when the annotation is read.

(6) Existing annotations that mention the deleted element will cause an exception (AnnotationTypeMismatchException) to be thrown when the annotation is read.

(7) Moving methods (and other members) up and down the type hierarchy are composite changes that must always be analyzed in terms of the net effect on the types involved plus any types laying along the path. For instance, the net effect of moving an API method to a supertype is an API method being added to the supertype; the subtype is not normally affected because the API method would still be inherited.

Evolving API interfaces - API methods

All methods in an API interface are implicitly public and abstract, and are therefore all considered API methods. The same is true for method declarations defining the elements of an API annotation type.

Change formal parameter name - Binary compatible
Change method name - Breaks compatibility
Add or delete formal parameter - Breaks compatibility
Change type of a formal parameter - Breaks compatibility
Change result type (including void) - Breaks compatibility
Add checked exceptions thrown - Breaks compatibility (1)
Add unchecked exceptions thrown - Binary compatible
Delete checked exceptions thrown - Breaks compatibility (1)
Delete unchecked exceptions thrown - Binary compatible
Re-order list of exceptions thrown - Binary compatible
Add type parameter If method has no type parameters Binary compatible (2)
If method has type parameters Breaks compatibility
Delete type parameter - Breaks compatibility
Re-order type parameters - Breaks compatibility
Rename type parameter - Binary compatible
Add, delete, or change type bounds of type parameter - Breaks compatibility
Change last parameter from array type T[] to variable arity T... - Binary compatible (3)
Change last parameter from variable arity T... to array type T[] - Breaks compatibility (4)
Add default clause to annotation type element - Binary compatible
Change default clause on annotation type element - Binary compatible (5)
Delete default clause from annotation type element - Breaks compatibility

(1) Adding and deleting checked exceptions declared as thrown by an API method does not break binary compatibility; however, it breaks contract compatibility (and source code compatibility).

(2) Adding type parameters to an unparameterized method is a compatible change owing to Java's story for interfacing with non-generic legacy code.

(3) A variable arity method declaration such as "void foo(String... y)" is compiled as if it had been written "void foo(String[] y)".

(4) Although existing binaries will continue to work, existing invocations in source code may not compile because the compiler no longer automatically bundles up the extra arguments into an array.

(5) Defaults are applied dynamically at the time annotations are read. Changing the default value may affect annotations in all classes, including ones compiled before the change was made.

Evolving API interfaces - API fields

All fields in an API interface are implicitly public, static, and final; they are therefore all considered API fields.

Because of binary compatibility problems with fields, the Java Language Specification recommends against using API fields. However, this is not always possible; in particular, enumeration constants to be used in switch statements must be defined as API fields.

Change type of API field - Breaks compatibility (1)
Change value of API field If field is compile-time constant value Breaks compatibility (2)
If field is not compile-time constant value Binary compatible

(1) All field type changes break binary compatibility, even seemingly innocuous primitive type widenings like turning a short into an int.

(2) Java compilers always inline the value of constant fields (ones with compile-time computable values, whether primitive or String type). As a consequence, changing the value of an API constant field does not affect pre-existing Clients. Invariably, this fails to meet the objective for changing the API field's value in the first place.

Evolving API interfaces - API type members

All type members in an API interface are implicitly public and static; they are therefore considered API type members. The rules for evolving an API type member are basically the same as for API classes and interfaces declared at the package level.

Evolving API Classes

Evolving API classes is somewhat more complex than API interfaces due to the wider variety of modifiers, including protected API members. Enums, which are a form of class, are also covered.

Add API method If method need not be reimplemented by Client Binary compatible (0)
If method must be reimplemented by Client Breaks compatibility (1)
Delete API method  - Breaks compatibility
Move API method up type hierarchy If method in supertype need not be reimplemented by Client Binary compatible
If method in supertype must be reimplemented by Client Breaks compatibility (9)
Move API method down type hierarchy - Breaks compatibility (9)
Add API constructor If there are other constructors Binary compatible
If this is only constructor Breaks compatibility (2)
Delete API constructor - Breaks compatibility
Add API field If class is not subclassable by Client Binary compatible
If class is subclassable by Client Breaks compatibility (3)
Delete API field - Breaks compatibility
Expand superinterface set (direct or inherited) - Binary compatible
Contract superinterface set (direct or inherited) - Breaks compatibility (4)
Expand superclass set (direct or inherited) - Binary compatible
Contract superclass set (direct or inherited) - Breaks compatibility (4)
Add, delete, or change static or instance initializers - Binary compatible
Add API type member If class is not subclassable by Client Binary compatible
If class is subclassable by Client Breaks compatibility (3)
Delete API type member - Breaks compatibility
Re-order field, method, constructor, and type member declarations - Binary compatible
Add or delete non-API members; that is, private or default access fields, methods, constructors, and type members - Binary compatible
Change abstract to non-abstract - Binary compatible
Change non-abstract to abstract - Breaks compatibility (5)
Change final to non-final - Binary compatible
Change non-final to final - Breaks compatibility (6)
Add type parameter If class has no type parameters Binary compatible (7)
If class has type parameters Breaks compatibility
Delete type parameter - Breaks compatibility
Re-order type parameters - Breaks compatibility
Rename type parameter - Binary compatible
Add, delete, or change type bounds of type parameter - Breaks compatibility
Rename enum constant - Breaks compatibility
Add, change, or delete enum constant arguments - Binary compatible
Add, change, or delete enum constant class body - Binary compatible
Add enum constant - Binary compatible (8)
Delete enum constant - Breaks compatibility
Re-order enum constants - Binary compatible (8)

(0) Although adding a new method to an API class which need not be reimplemented by Clients does not break binary compatibility, a pre-existing subclass might still provide a pre-existing implementation of a method by this name. See Evolving Java-based APIs#Example 4 - Adding an API method in the preceding section for why this breaks API contract compatibility.

(1) Adding a new method to an API class that must be reimplemented by Clients breaks compatibility because pre-existing subclasses would not provide any such implementation.

(2) Adding the first constructor to an API class causes the compiler to no longer generate a default (public, 0 argument) constructor, thereby breaking compatibility with pre-existing code that invoked this API constructor. To avoid this pitfall, API classes should always explicitly declare at least one constructor.

(3) Adding a new field to an API class that is subclassed by Clients breaks binary compatibility. A field in a superinterface of C may hide an added field inherited from a superclass of C, causing linking errors to be detected when a static field hides an instance field. Apart from the binary compatibility issues, it is generally good software engineering practice that API classes should not expose any fields.

(4) Shrinking an API class's set of API superclasses and superinterfaces (either directly or inherited) breaks compatibility because some casts in pre-existing Client code will now longer work. However, non-API superclasses and superinterfaces can be removed without breaking binary compatibility.

(5) Pre-existing binaries that attempt to create new instances of the API class will fail with a link-time or runtime error.

(6) Pre-existing binaries that subclass the API class will fail with a link-time error.

(7) Adding type parameters to an unparameterized type is a compatible change owing to Java's story for interfacing with non-generic legacy code.

(8) Client code can use the values() method to determine the ordinal positions of the enum constants. So although this is a binary compatible change, it may break contractual compatibility.

(9) Moving methods (and other members) up and down the type hierarchy are composite changes that must always be analyzed in terms of the net effect on the types involved plus any types laying along the path. For instance, the net effect of moving an API method to a supertype is an API method being added to the supertype; the subtype is not normally affected because the API method would still be inherited.

Evolving API classes - API methods and constructors

Change body of method or constructor - Binary compatible
Change formal parameter name - Binary compatible
Change method name - Breaks compatibility
Add or delete formal parameter - Breaks compatibility
Change type of a formal parameter - Breaks compatibility
Change result type (including void) - Breaks compatibility
Add checked exceptions thrown - Breaks compatibility (1)
Add unchecked exceptions thrown - Binary compatible
Delete checked exceptions thrown - Breaks compatibility (1)
Delete unchecked exceptions thrown - Binary compatible
Re-order list of exceptions thrown - Binary compatible
Decrease access; that is, from protected access to default or private access; or from public access to protected, default, or private access - Breaks compatibility
Increase access; that is, from protected access to public access - Binary compatible (2)
Change abstract to non-abstract - Binary compatible
Change non-abstract to abstract - Breaks compatibility (3)
Change final to non-final - Binary compatible
Change non-final to final If method not reimplementable by Clients Binary compatible
If method reimplementable by Clients Breaks compatibility (4)
Change static to non-static - Breaks compatibility
Change non-static to static - Breaks compatibility
Change native to non-native - Binary compatible
Change non-native to native - Binary compatible
Change synchronized to non-synchronized - Binary compatible (5)
Change non-synchronized to synchronized - Binary compatible (5)
Add type parameter If method has no type parameters Binary compatible (6)
If method has type parameters Breaks compatibility
Delete type parameter - Breaks compatibility
Re-order type parameters - Breaks compatibility
Rename type parameter - Binary compatible
Add, delete, or change type bounds of type parameter - Breaks compatibility
Change last parameter from array type T[] to variable arity T... - Binary compatible (7)
Change last parameter from variable arity T... to array type T[] - Breaks compatibility (8)

(1) Adding and deleting checked exceptions declared as thrown by an API method does not break binary compatibility; however, it breaks contract compatibility (and source code compatibility).

(2) Perhaps surprisingly, the binary format is defined so that changing a member or constructor to be more accessible does not cause a linkage error when a subclass (already) defines a method to have less access.

(3) Pre-existing binaries that invoke the method will fail with a runtime error.

(4) Pre-existing binaries that reimplement the method will fail with a link-time error.

(5) Adding or removing the synchronized modifier also has a bearing on the method's behavior in a multi-threaded world, and may therefore raise a question of contract compatibility.

(6) Adding type parameters to an unparameterized type is a compatible change owing to Java's story for interfacing with non-generic legacy code.

(7) A variable arity method declaration such as "void foo(int x, String... y)" is compiled as if it had been written "void foo(int x, String[] y)".

(8) Although existing binaries will continue to work, existing invocations in source code may not compile because the compiler no longer automatically bundles up the extra arguments into an array.

Evolving API classes - API fields

Because of binary compatibility problems with fields, the Java Language Specification recommends against using API fields. However, this is not always possible; in particular, enumeration constants to be used in switch statements must be defined as API constant fields.
 

Change type of API field - Breaks compatibility (1)
Change value of API field If field is compile-time constant Breaks compatibility (2)
If field is not compile-time constant Binary compatible
Decrease access; that is, from protected access to default or private access; or from public access to protected, default, or private access - Breaks compatibility
Increase access; that is, from protected access to public access - Binary compatible
Change final to non-final If field is non-static Binary compatible
If field is static with compile-time constant value Breaks compatibility (3)
If field is static with non-compile-time constant value Binary compatible
Change non-final to final - Breaks compatibility (4)
Change static to non-static - Breaks compatibility (5)
Change non-static to static - Breaks compatibility (5)
Change transient to non-transient - Binary compatible
Change non-transient to transient - Binary compatible

(1) All field type changes break binary compatibility, even seemingly innocuous primitive type widenings link turning a short into an int.

(2) Java compilers always inline the value of constant fields (ones with compile-time computable values, whether primitive or String type). As a consequence, changing the value of an API constant field does not affect pre-existing Clients. Invariably, this does not meet the objective for changing the API field's value.

(3) Java compilers always inline the value of constant fields (ones with compile-time computable values, whether primitive or String type). As a consequence, changing an API constant field into a non-final one does not propagate to pre-existing Clients. Invariably, this does not meet the objective for making the API field non-final.

(4) Making an API field final breaks compatibility with pre-existing binaries that attempt to assign new values to the field.

(5) Changing whether an API field is declared static or not results in link-time errors where the field is used by a pre-existing binary which expected a field of the other kind.

Evolving API classes - API type members

The rules for evolving an API type member are basically the same as for API classes and interfaces declared at the package level, with these additional rules for changing access modifiers:
 

Decrease access; that is, from protected access to default or private access; or from public access to protected, default, or private access - Breaks compatibility
Increase access; that is, from protected access to public access - Binary compatible

Evolving non-API packages

The names of non-API packages, classes, and interfaces do not appear in Client source code or binaries. Consequently, non-API packages, classes, and interfaces can be added or deleted without jeopardizing binary compatibility. However, when non-API classes and interfaces containing public or protected members are among the superclass or superinterface sets of API classes and interfaces, non-API changes may have ramifications to API methods, fields, and constructors.

Add non-API package - Binary compatible
Delete non-API package - Binary compatible
Add class or interface to non-API package - Binary compatible
Delete class or interface in a non-API package - Binary compatible
Change existing class or interface in non-API package - Binary compatible

Turning non-generic types and methods into generic ones

Generic types and methods were added to the Java language in Java SE 5 (aka JDK 1.5) along with a special story for how legacy non-generic code can continue to use types and methods that have been "generified". The prime example of this is the java.util Collections Framework, which was upgraded to make use of generics while remaining compatible with code that uses collections in the old way.

The key concepts behind Java's special compatibility mechanism are raw types and erasures.

A raw type is a use of a generic type without the normal type arguments. For example, "List" in the declaration statement "List x = null;" is a raw type since List is a generic type declared "public interface List<E> ..." in JDK 1.5. Contrast this to a normal use of List which looks like "List<String> x = null;" or "List<?> x = null;" where a type augument ("String") or wildcard is specified.

The term erasure is suggestive. Imagine going through your code and literally erasing the type parameters from the generic type declaration (e.g., erasing the "<E>" in "public interface List<E> ...") to get a non-generic type declaration, and replacing all occurrence of the deleted type variable with Object. For type parameters with type bounds (e.g., "<E extends T1 & T2 & T3 & ...>"), the leftmost type bound ("T1"), rather than Object, is substituted for the type variable. The resulting declaration is known as the erasure.

According to the special compatibility story, the Java compiler treats a raw type as a reference to the type's erasure. An existing type can be evolved into a generic type by adding type parameters to the type declaration and judiciously introducing uses of the type variables into the signatures of its existing methods and fields. As long as the erasure looks like the corresponding declaration prior to generification, the change is binary compatible with existing code.

As a case study of how to generify an existing API, carefully compare the Java 1.4 Collections Framework[1] classes with their counterparts in the Java 1.5 Collections Framework[2]. You will see that the erasures of the 1.5 versions looks just like the 1.4 versions.

Variable arity methods were also introduced in 1.5, also with a special story for how legacy code can continue to invoke or override a method that has been upgraded. A variable arity method declaration such as "void foo(int x, String... y)" is compiled as if it had been written "void foo(int x, String[] y)". This provides a consistent interpretation for old-style invocations of the form "foo(int 5, new String[]{"a","b"})". The class Arrays.asList[3] is an example of a method that became both generic and variable arity in 1.5.

Two final notes. Regarding whether existing APIs ought to embrace generics, the Java Language Specification says only this:

"The use of raw types is allowed only as a concession to compatibility of legacy code. The use of raw types in code written after the introduction of genericity into the Java programming language is strongly discouraged. It is possible that future versions of the Java programming language will disallow the use of raw types." (JLS3, 4.8[4])

The implication is that code that uses the Collections Framework should use types like "List<?>" instead of the raw type "List".

But, also bear in mind that there are severe constraints on how a type or method that already is generic can be compatibly evolved with respect to its type parameters (see the tables above). So if you plan to generify an API, remember that you only get one chance (release), to get it right. In particular, if you change a type in an API signature from the raw type "List" to "List<?>" or "List<Object>", you will be locked into that decision. The moral is that generifying an existing API is something that should be considered from the perspective of the API as a whole rather than piecemeal on a method-by-method or class-by-class basis.

Evolving annotations on API elements

Annotations were added to the Java language in Java SE 5 (aka JDK 1.5). Annotation can be used on most any named Java element, including packages, types, methods, and fields. It's a natural question to ask whether adding, deleting, or changing an annotation on a API element is a compatible or a breaking change.

On one hand, adding or removing annotations has no effect on the correct linkage of class files by the Java virtual machine. On the other hand, annotations exist to be read via reflective APIs for manipulating annotations. So there is no uniform answer as to what will happen if a given annotation is or is not present on an API element (or non-API element, for that matter). It depends entirely on the specifics of the annotation and the mechanisms that are processing those annotations.

Parties that declare annotation types should try to provide helpful guidance for their customers.

Copyright © 2000, 2007 IBM Corporation