Collision between Kotlin class delegation and Java 8 default methods


#1

TLDR; Class delegation prevents Java 8 default interface methods to apply IF the code was built with later versions of said Java 8 interfaces.


Here’s a weird case I’ve encountered.

Let’s say there exist a simple java library interface and class:

public interface Reference<T> {
    T get();
    T set(T value);
}

public class SimpleReference<T> implements Reference<T> {
    private T _ref;

    public SimpleReference(T initialValue) {
        Objects.requireNonNull(initialValue);
        _ref = initialValue;
    }

    @Override public T get() {
        return _ref;
    }

    @Override public T set(T newValue) {
        Objects.requireNonNull(newValue);
        T oldValue = _ref;
        _ref = newValue;
        return oldValue;
    }
}

Also, I’m using a Kotlin library that allows me to observe a reference:

class ObservableReference<T>(private val _ref: Reference<T>, private val _onChange: (T, T) -> Unit): Reference<T> by _ref {
    override fun set(newValue: T): T {
        val oldValue = _ref.set(newValue)
        _onChange(oldValue, newValue)
        return oldValue
    }
}

From all that, I wrote an amazing program:

fun main(args: Array<String>) {
    val r = ObservableReference(SimpleReference("One")) { old, new -> println("Changing from $old to $new") }
    r.set("Two")
}

OK so what do we have ?

  • A Java interface for a simple reference holder (written by the Java library author).
  • A Java class that implements this interface the most simple way (also written by the Java library author).
  • A Kotlin class that implements the same interface and delegates everything to a Reference property except that it “hooks” itself into the reference change method so it can call an observator function (written by the Kotlin library author).
  • A Kotlin program (written by me).

At this point, everything works as expected: the program prints “Changing from One to Two”. Yay!

BTW, sorry for the convoluted code, it’s the simplest way I found to explain my use case.

So, let’s continue : the Java library is updated. Now, it also supports atomic get and set. Because the Java library author is a nice guy, he uses default interface methods to maximize forward compatibility:

public interface Reference<T> {
    T get();
    T set(T value);

    default T getAtomic() {
        synchronized (this) {
            return get();
        }
    }

    default T setAtomic(T value) {
        synchronized (this) {
            return set(value);
        }
    }
}

Because I like cuting edge technology, As soon as it goes out, I decide to update my program:

fun main(args: Array<String>) {
    val r = ObservableReference(SimpleReference("One")) { old, new -> println("Changing from $old to $new") }

    r.setAtomic("Two")
}

Badaboom! The program stops working as it should and my observator function is not called.

What just happened ?

When calling setAtomic, because it does not have one AND because of delegation, the kotlin ObservableReference class delegates the call to it’s delegate.
From this, of course, its own set function is never called.

The Kotlin library maintainer being a good friend of mine, decides to update the library. He removes delegation so that his library has to implement all delegation methods by hand BUT will enjoy the “default” methods of the Java library:

class ObservableReference<T>(private val _ref: Reference<T>, private val _onChange: (T, T) -> Unit): Reference<T> {
    override fun get(): T = _ref.get()
    override fun set(newValue: T): T {
        val oldValue = _ref.set(newValue)
        _onChange(oldValue, newValue)
        return oldValue
    }
}

… and now it re-works :smiley:.


My real use case is that I’m trying to wrap a full collection, that can be watched with RxJava. The problem came when I investigated those new Java 8 methods whose default implementations are supposed to call the java 7 existing methods.
I realized my wrapper was not intercepting mutability when using those methods. So I changed from auto delegation to manual delegation, and was happy with the result ^^


The conclusion is simply that using Kotlin’s class delegation prevents Java 8 methods to apply.
I do realize that this is often a good thing: the wrapped class often has specialized optimized implementations of these methods.
There are cases, however, like this one, where not applying Java 8 default methods instead of delegation precisely breaks the very purpose of those default interface methods.

More alarming, the way I see it, is that this behaviour is inconsistent regarding the order of build! The program did break because (you guessed it) all the code is in fact in one project :slight_smile:.

If I understand this right (please correct me if I don’t):

  • If the Kotlin library is built against the old Java library, the wrapper WILL be applied the default Java methods when running with the new Java library.
  • If the Kotlin library is built against the new Java library the wrapper WILL NOT be applied the default Java methods and instead generate delegation methods.

This, to my understanding, breaks the very purpose of the Java 8 default interface methods.

So, I’d like to ask Jetbrains if they have thought about this. Is this behavior a conscious decision or just the way things happen to play out ?
If the answer is the former, could you maybe write about the rationale of this decision ?
If it’s the latter, do you think something can be done about this (without breaking Kotlin 1.0 compatibility of course) ?

Thanks for reading this far :wink:

Salomon.


#2

Default methods are a java 8 feature. Kotlin still targets Java 6 (and Java 6 class file format). When you compiled against the old interface this was fine as the default methods are not present for the Kotlin class to clobber. What is most surprising to me is that it compiles correctly, although that probably has to do with the fact that it has to be able to compile against java 8 class files (as source of symbols) for practical reasons.


#3

As the Java 8 target is being added in 1.1, we’ll be looking into this issue. Thanks fro the report!


#4

@salomonbrys we decided to restrict delegation to java default methods, fix will be avaliable since 1.1-M04


#5

That’s great news.
Could you elaborate a bit more on what that exactly means ?