@JvmDefault and delegates

Kotlin Docs → Calling Kotlin from Java → Default methods in interfaces states

Note that if an interface with @JvmDefault methods is used as a delegate, the default method implementations are called even if the actual delegate type provides its own implementations.

and gives the following example:

interface Producer {
    @JvmDefault fun produce() {
        println("interface method")
    }
}

class ProducerImpl: Producer {
    override fun produce() {
        println("class method")
    }
}

class DelegatedProducer(val p: Producer): Producer by p {
}

fun main() {
    val prod = ProducerImpl()
    DelegatedProducer(prod).produce() // prints "interface method"
}

I find this behavior very surprising and undesirable, and not using @JvmDefault is no good option if one wants good support of Java 8+ client code. (I would even argue that @JvmDefault should be the default when targetting JDK 8+.)

I see two issues with this behavior:

A) The main issue is that I think that @Jvm* annotations should not change semantics, particularly not inside the Kotlin world.

There seems to be no need for this issue. I can easily hand-craft a delegating implementation that is immune to @JvmDefault, so the compiler should be able to do this, too:

interface Producer {

    fun nonDefault() {
        println("interface method")
    }

    @JvmDefault
    fun jvmDefault() {
        println("interface method")
    }
}

class ProducerImpl : Producer {

    override fun nonDefault() {
        println("class method")
    }

    override fun jvmDefault() {
        println("class method")
    }
}

class ManuallyDelegatingProducer(val p: Producer) : Producer {
    override fun nonDefault() {
        p.nonDefault()
    }

    override fun jvmDefault() {
        p.jvmDefault()
    }
}

val p = ProducerImpl()
ManuallyDelegatingProducer(p).nonDefault() // prints "class method"
ManuallyDelegatingProducer(p).jvmDefault() // prints "class method"

B) The second issue is about the delegation behavior itself, not the difference in behavior with or without @JvmDefault annotation. It is merely theoretical, since given a non-experimental and thus unchangeable behavior without the annotation and my case for unchanged semantics with the annotation, retaining the established behavior necessarily follows. But I include it for completeness of discussion.

One might argue (though I wouldn’t) that when an interface method is a default method, an implementing class does not have to delegate, and so calling the default implementation is correct. But even then, there would be no need for a difference with or without annotation, since delegation could be implemented like this:

class InheritingProducer(val p: Producer) : Producer

class RedundantlyInheritingProducer(val p: Producer) : Producer {
    override fun nonDefault() {
        super.nonDefault()
    }

    override fun jvmDefault() {
        super.jvmDefault()
    }
}

val p = ProducerImpl()
InheritingProducer(p).nonDefault() // prints "interface method"
InheritingProducer(p).jvmDefault() // prints "interface method"
RedundantlyInheritingProducer(p).nonDefault() // prints "interface method"
RedundantlyInheritingProducer(p).jvmDefault() // prints "interface method"

What is the rationale behind the current behavior? Is there any chance of changing this behavior before @JvmDefault becomes non-experimental?

2 Likes

See also

and linked issue

2 Likes

Thanks. I did not understand all the details, but it seems that your article and the linked issue are about related but orthogonal issues.

In hope of getting attention from JetBrains, I have created issue KT-34612 with almost identical content.