loading...
Cover image for Matt's Tidbits #74 - Resolving an interesting Kotlin  Java interop issue

Matt's Tidbits #74 - Resolving an interesting Kotlin Java interop issue

mpeng3 profile image Matthew Groves Originally published at Medium ・3 min read

Last time, I shared a quick tidbit and some resources on Kotlin scope functions. This week, I have a story about resolving an interesting Kotlin <-> Java interop issue.

Kotlin has many useful features - let's consider two of them:

  1. Default arguments - allowing you to specify a default value for any method parameter, simplifying the appearance at the call site.
  2. Delegates - a language feature that lets you automatically delegate functionality to a particular object (giving you the benefits of the Inheritance "is-a" structure via the more favorable Composition "has-a" relationship).

It's simple to set up something like the following:

interface Base {
  fun foo(bar: Int = 42)
}

class BaseImpl() : Base {
  override fun foo(bar: Int) {
    // do something with bar
  }
}

class Derived(b: Base) : Base by b

In this setup, Derived has all of the capabilities of Base (since it implements its interface), but it does not have to redefine any of the behavior, since it is all delegated to the supplied instance of Base, b . You can use this in Kotlin like this:
Derived(BaseImpl()).foo() - and the value of bar inside the foo method would use the default value of 42.

There's one problem with this though - if you try to call this from Java, you have to specify a value for bar, since Java doesn't know that it has a default value. The most common workaround for this is to add the [@JvmOverloads](https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#overloads-generation) annotation to the method, which will automatically generate methods with all of the possible default argument combinations for use by Java. So, in our case, the following methods would be visible in Java:

  • void foo()
  • void foo(int bar)

Calling foo() would use the default value of 42, whereas the other version lets you specify what value to use for bar.

There's another interesting question here - where should you put @JvmOverloads? On the Base interface, or on the BaseImpl class? Kotlin makes this decision easy - you cannot use this annotation on an interface, so it will have to exist on the BaseImpl class.

However, this presents an issue - if you add this to the BaseImpl class, you'll find that it won't achieve anything - because the default argument for bar is present on the interface. If you try to move the default argument from the Base interface to the BaseImpl class, you'll find that you can't - default arguments have to be declared on the top-most method declaration when you have overridden methods.

How do we solve this? One suggestion I saw was to set up Base as an abstract open class. This does allow you to specify @JvmOverloads on Base, but this is a little bit of a code smell (other classes can extend your behavior), and it also exposes a new problem - you can only use an interface for Delegation.

So... we're pretty stuck for our use case. Or are we?

We have the following options:

  1. Define separate copies of the method (both of the variations of foo()) - I'm not crazy about this idea though, because it unnecessarily pollutes your Base interface with extra methods (which are only needed for Java).
  2. ... Which is why I much prefer to use Extension Functions instead! You can set one up like this: fun Base.foo() = this.foo()

By writing it this way, it's now possible to make it clear that this is something that should only be used by Java code, for example, with this neat trick (by giving the method an obviously undesirable name for Kotlin, but an ok name for Java):

@file:JvmName("BaseUtils")

@JvmName("foo")
fun Base.dont_use_in_kotlin_foo() = this.foo()

This might look slightly worse at a call site:
BaseUtils.foo(new Derived(new BaseImpl()));

But, in my opinion, this makes it more clear that this is a temporary bridge/addition to the interface, and once you've converted any Java code that uses it to Kotlin, you can safely remove the whole thing!

Have you encountered this issue before? If so, how did you work around it? I'd love to hear your story in the comments! And, please follow me on Medium if you're interested in being notified of future tidbits.

This tidbit was discovered on June 22, 2020.

Posted on by:

mpeng3 profile

Matthew Groves

@mpeng3

Software engineer with 10+ years of professional experience in C++, C#, Java, and Kotlin.

Discussion

markdown guide