Can I change a return type to be a strict subtype and retain binary compatibility?

Viewed 583

If I have some class:

import java.util.Date;
public final class Foo {
    private Date date;
    public Date getDate(){ return date; }
}

If I have compiled this as a binary and other people have built code against it, can I subsequently do this without breaking binary compatibility?

import java.sql.Date;
public final class Foo {
    private Date date;
    public Date getDate(){ return date; }
}

Note that java.sql.Date is a subclass of java.util.Date.

It seems obvious to me that, had I not declared the class final, then I would have broken source compatibility (i.e. someone could previously have compiled against my library a subclass of Foo which overrides the getDate method to return a java.util.Date; that code would no longer compile against my latest version). But does breaking source compatibility imply that binary compatibility is broken too? (that doesn't hold in other languages, such as scala)

2 Answers

I tested this.

Unfortunately there is a NoSuchMethodError exception at the point of the call when a method's return type is changed to a subtype of the original type.

Making this change is thus not binary backwards compatible.

This is too bad...

Test setup

test
├── c
├── c_int
│   └── C.java
├── c_num
│   └── C.java
└── Test.java

test/c_num/C.java:

package test.c;

public class C {
    public Number f() {
        System.out.println("f Number");
        return 0.0;
    }
}

test/c_int/C.java:

package test.c;

public class C {
    public Integer f() {
        System.out.println("f Integer");
        return 1;
    }
}

test/Test.java:

package test;

import test.c.C;

public class Test {
    public static void main(String[] args) throws Exception {
        C b = new C();
        Number n = b.f();
        System.out.println(n);
    }
}

Test

Compile the two C classes with different return type:

$ javac test/c_int/C.java
$ javac test/c_num/C.java

Compile Test against C with return type Number:

$ cp test/c_num/C.class test/c/
$ javac test/Test.java

Run Test against C with return type Number:

$ java test.Test
f Number
0.0

Run Test against C with return type Integer, without recompiling Test:

$ cp test/c_int/C.class test/c/
$ java test.Test
Exception in thread "main" java.lang.NoSuchMethodError: 'java.lang.Number test.c.C.f()'
    at test.Test.main(Test.java:8)

Bytecode

We can also see in the byte code that the method call to f contains the return type of the method:

$ javap -c test/Test.class
Compiled from "Test.java"
public class test.Test {
  public static void main(java.lang.String[]);
    Code:
       ...
       9: invokevirtual #4                  // Method test/c/C.f:()Ljava/lang/Number;
       ...
}

Eclipse document

The Eclipse foundation maintain a document called Evolving Java-based APIs.

The document states this about binary compatibility:

Evolving API classes - API methods and constructors
....
Change result type (including void)     -   Breaks compatibility

This does not make an exception about changes that only narrow the return type. All changes to the return type breaks binary compatibility.

I think the best way is to just test it yourself. Here is some snippet from official documentation:

13.4.15. Method Result Type

Changing the result type of a method, or replacing a result type with void, or replacing void with a result type, has the combined effect of deleting the old method and adding a new method with the new result type or newly void result (see §13.4.12).

and

13.4.12. Method and Constructor Declarations

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.

The way I read this is that if you are changing return type, no matter if it is a subtype of old type, means that you are deleting the old method and adding a new method and according to 13.4.12 it can break compatibility with any pre-existing binary that referenced this method or constructor.

Related