Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Retrofitting void methods to return its argument to facilitate fluency: breaking change?

"API design is like sex: make one mistake and support it for the rest of your life" (Josh Bloch on twitter)

There are many design mistakes in the Java library. Stack extends Vector (discussion), and we can't fix that without causing breakage. We can try to deprecate Integer.getInteger (discussion), but it's probably going to stay around forever.

Nonetheless, certain kinds of retrofitting can be done without causing breakage.

Effective Java 2nd Edition, Item 18: Prefer interfaces to abstract classes: Existing classes can be easily retrofitted to implement a new interface".

Examples: String implements CharSequence, Vector implements List, etc.

Effective Java 2nd Edition, Item 42: Use varargs judiciously: You can retrofit an existing method that takes an array as its final parameter to take varags instead with no effect on existing clients.

An (in)famous example is Arrays.asList, which caused confusions (discussion), but not breakage.

This question is about a different kind of retrofitting:

Can you retrofit a void method to return something without breaking existing code?

My initial hunch points to yes, because:

  • Return type doesn't affect which method is chosen at compile time
    • See: JLS 15.12.2.11 - Return Type Not Considered
    • Thus, changing return type doesn't change which method is chosen by the compiler
    • Retrofitting from void to return something is legal (but not the other way around!)
  • Even when you use reflection, things like Class.getMethod doesn't distinguish on return type

However, I'd like to hear a more thorough analysis by others more experienced in Java/API design.


Appendix: The Motivation

As suggested in the title, one motivation is to facilitate fluent interface style programming.

Consider this simple snippet that prints a shuffled list of names:

    List<String> names = Arrays.asList("Eenie", "Meenie", "Miny", "Moe");
    Collections.shuffle(names);
    System.out.println(names);
    // prints e.g. [Miny, Moe, Meenie, Eenie]

Had Collections.shuffle(List) been declared to return the input list, we could have written:

    System.out.println(
        Collections.shuffle(Arrays.asList("Eenie", "Meenie", "Miny", "Moe"))
    );

There are other methods in Collections that would've been much more pleasant to use if they were to return the input list instead of void, e.g. reverse(List), sort(List), etc. In fact, having Collections.sort and Arrays.sort return void is especially unfortunate, because it deprives us from writing expressive code such as this:

// DOES NOT COMPILE!!!
//     unless Arrays.sort is retrofitted to return the input array

static boolean isAnagram(String s1, String s2) {
    return Arrays.equals(
        Arrays.sort(s1.toCharArray()),
        Arrays.sort(s2.toCharArray())
    );
}

This void return type preventing fluency isn't just restricted to these utility methods, of course. The java.util.BitSet methods could've also been written to return this (ala StringBuffer and StringBuilder) to facilitate fluency.

// we can write this:
    StringBuilder sb = new StringBuilder();
    sb.append("this");
    sb.append("that");
    sb.insert(4, " & ");
    System.out.println(sb); // this & that

// but we also have the option to write this:
    System.out.println(
        new StringBuilder()
            .append("this")
            .append("that")
            .insert(4, " & ")
    ); // this & that

// we can write this:
    BitSet bs1 = new BitSet();
    bs1.set(1);
    bs1.set(3);
    BitSet bs2 = new BitSet();
    bs2.flip(5, 8);
    bs1.or(bs2);
    System.out.println(bs1); // {1, 3, 5, 6, 7}

// but we can't write like this!
//  System.out.println(
//      new BitSet().set(1).set(3).or(
//          new BitSet().flip(5, 8)
//      )
//  );

Unfortunately, unlike StringBuilder/StringBuffer, ALL of BitSet's mutators return void.

Related topics

  • Method chaining - why is it a good practice, or not?
like image 707
polygenelubricants Avatar asked Aug 28 '10 07:08

polygenelubricants


3 Answers

Unfortunately, yes, changing a void method to return something is a breaking change. This change will not affect source code compatibility (i.e. the same Java source code would still compile just like it did before, with absolutely no discernible effect) but it breaks binary compatibility (i.e. bytecodes that was previously compiled against the old API will no longer run).

Here are the relevant excerpts from the Java Language Specification 3rd Edition:

13.2 What Binary Compatibility Is and Is Not

Binary compatibility is not the same as source compatibility.


13.4 Evolution of Classes

This section describes the effects of changes to the declaration of a class and its members and constructors on pre-existing binaries.

13.4.15 Method Result Type

Changing the result type of a method, 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.

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.

That is, while the return type of a method is ignored by the Java compiler at compile-time during the method resolution process, this information is significant at run-time at the JVM bytecode level.


On bytecode method descriptors

A method's signature does not include the return type, but its bytecode descriptor does.

8.4.2 Method Signature

Two methods have the same signature if they have the same name and argument types.


15.12 Method Invocation Expressions

15.12.2 Compile-Time Step 2: Determine Method Signature

The descriptor (signature plus return type) of the most specific method is one used at run time to perform the method dispatch.

15.12.2.12 Example: Compile-Time Resolution

The most applicable method is chosen at compile time; its descriptor determines what method is actually executed at run time.

If a new method is added to a class, then source code that was compiled with the old definition of the class might not use the new method, even if a recompilation would cause this method to be chosen.

Ideally, source code should be recompiled whenever code that it depends on is changed. However, in an environment where different classes are maintained by different organizations, this is not always feasible.

A little inspection of the bytecode will help clarify this. When javap -c is run on the name shuffling snippet, we see instructions like this:

invokestatic java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
             \______________/ \____/ \___________________/\______________/
                type name     method      parameters        return type

invokestatic java/util/Collections.shuffle:(Ljava/util/List;)V
             \___________________/ \_____/ \________________/|
                   type name        method     parameters    return type

Related questions

  • java: what is this: [Ljava.lang.Object;?

On non-breaking retrofitting

Let's now address why retrofitting a new interface or a vararg, as explained in Effective Java 2nd Edition, does not break binary compatibility.

13.4.4 Superclasses and Superinterfaces

Changing the direct superclass or the set of direct superinterfaces of a class type will not break compatibility with pre-existing binaries, provided that the total set of superclasses or superinterfaces, respectively, of the class type loses no members.

Retrofitting a new interface does not cause the type to lose any member, hence this does not break binary compatibility. Similarly, due to the fact that varargs is implemented using arrays, this kind of retrofitting also does not break binary compatibility.

8.4.1 Formal Parameters

If the last formal parameter is a variable arity parameter of type T, it is considered to define a formal parameter of type T[].

Related questions

  • Difference between double… and double[] in formal parameter type declaration

Is there absolutely no way to do this?

Actually, yes, there is a way to retrofit a return value on previously void methods. We can't have two methods with the same exact signatures at the Java source code level, but we CAN have that at the JVM level, provided that they have different descriptors (due to having different return types).

Thus we can provide a binary for e.g. java.util.BitSet that has methods with both the void and non-void return types simultaneously. We only need to publish the non-void version as the new API. In fact, that's the only thing we can publish at the API, since having two methods with the exact same signature is illegal in Java.

This solution is a terrible hack, requiring special (and spec-defying) processing to compile BitSet.java to BitSet.class, and thus it may not be worth the effort to do so.

like image 64
polygenelubricants Avatar answered Nov 04 '22 13:11

polygenelubricants


If you can't retrofit, you still can wrap your class into a new one which uses the same methods but with correct returns (MyClassFluent). Or you can add new methods but with different names, instead of Arrays.sort() we could have Arrays.getSorted().

I think that the solution isn't to force things, just deal with them.

EDIT: I know I didn't answer to the "retrofitting of void methods" question, but your answer is already really clear.

like image 26
Colin Hebert Avatar answered Nov 04 '22 14:11

Colin Hebert


If you have to deal with source compatability only, then just go ahead. Changing from void to a return type will not break.

But to address the thing you actually want to do: I consider the problem with fluent interfaces is that the lines tend to get rather long and - after formatting - somewhat unreadable. For builders it works fine, but I'd probably not use it for anything else.

Is this for playing with it, or because you've found that this is really great?

like image 1
Thorbjørn Ravn Andersen Avatar answered Nov 04 '22 12:11

Thorbjørn Ravn Andersen