- Type: Design proposal
- Author: Jake Wharton
- Status: Under consideration
- Prototype: Partially implemented in Kotlin 1.1.60
Discussion of this proposal is held in this issue.
This proposal adds annotations which enable the use of more Kotlin language features like extension functions, extension properties, default values, and property names for Java code.
@ExtensionFunction
/@ExtensionProperty
- Turn a static method with at least one argument into an extension function or an extension property.@ParameterName
- An explicit name for parameters.@DefaultValue
- Default parameter values.
There is a large corpus of Java libraries which, for various reasons, can't or won't port to Kotlin for the foreseeable future. This fact does not render them as obsolete or inferior to a Kotlin library. In fact, a certain portion of Java library authors are sympathetic to Kotlin users and are willing to do more for compatibility.
Java libraries following Effective Java item 1 have static factory methods
for adapting arguments into instances of the enclosing type. For example,
to create a Guava ByteSource
from a File
:
val file = File("hello.txt")
val source = Files.asByteSource(file)
This calling convention is significantly different than what would be used
creating an InputStream
from a File
with the Kotlin standard library:
val file = File("hello.txt")
val input = file.inputStream()
A Kotlin-friendly version of Guava's ByteSource
factory could be changed
to:
@ExtensionFunction
public static ByteSource asByteSource(File file) { .. }
which would allow Kotlin callers to instead use it as an extension:
val file = File("hello.txt")
val source = file.asByteSource()
To complete the calling convention parity with the standard library, the function name can be adjusted solely on the Kotlin side:
@ExtensionFunction("byteSource")
public static ByteSource asByteSource(File file) { .. }
to enable:
val file = File("hello.txt")
val source = file.byteSource()
Changing the name from the Kotlin side is essential as the context of the class
name is lost when changing to an extension function. For example, if we naively
translate Guava's ImmutableSet
factory:
@ExtensionFunction
public static <T> ImmutableSet<T> copyOf(Collection<T> list) { .. }
the function lacks clarity in the operation it performs:
val list = listOf("a", "b", "c")
val immutableSet = list.copyOf()
By supplying a name we can again create parity with the calling convention of Kotlin:
@ExtensionFunction("toImmutableSet")
public static <T> ImmutableSet<T> copyOf(Collection<T> list) { .. }
val list = listOf("a", "b", "c")
val immutableSet = list.toImmutableSet()
Static methods annotated with @ExtensionProperty
will be turned into
extension properties. For example, to get the root cause of an exception with
Guava:
} catch (e: InvocationTargetException) {
// Original calling convention
val cause = Throwables.getRootCause(e)
}
// Updated Java signature with annotation
@ExtensionProperty
public static Throwable getRootCause(Throwable t) { .. }
} catch (e: InvocationTargetException) {
// New calling convention
val cause = e.rootCause
}
@ExtensionProperty
methods with names that match the "Getters and Setters"
Java-to-Kotlin interop rules will have the same renaming behavior as
members. Supplying an alternate name allows overriding this behavior, if
desired.
While the Java class file format does allow parameter names as of version 52
(corresponding to Java 8), they are opt-in and almost never included. Once
enabled, they also require committing to stable parameter names for the
entirety of the API surface. @ParameterName
can be used to define stable
parameter names. For example, Android's View.setPadding
has four parameters in an order that can be difficult to remember:
public void setPadding(int left, int top, int right, int bottom) { .. }
By adding @ParameterName
, Kotlin callers can specify the parameters in any
order:
public void setPadding(
@ParameterName("left") int left,
@ParameterName("top") int top,
@ParameterName("right") int right,
@ParameterName("bottom") int bottom) { .. }
val view = View(context)
view.setPadding(top = 5, bottom = 5, left = 10, right = 10)
Default values can also be specified with @DefaultValue
which are interpreted
as expressions in a similar fashion to ReplaceWith
. In
keeping with Android's View.setPadding
example, we can now add defaults that
look up the current value allowing a subset of values to be passed:
public void setPadding(
@ParameterName("left") @DefaultValue("paddingLeft") int left,
@ParameterName("top") @DefaultValue("paddingTop") int top,
@ParameterName("right") @DefaultValue("paddingRight") int right,
@ParameterName("bottom") @DefaultValue("paddingBottom") int bottom) { .. }
val view = View(context)
view.setPadding(left = 10, right = 10)
These annotations can all combine together to adapt a Java API which is otherwise unidiomatic to something which feels very natural.
@ExtensionFunction("toByteString")
public static ByteString of(
byte[] bytes,
@ParameterName("offset") @DefaultValue("0") int offset,
@ParameterName("count") @DefaultValue("bytes.size - offset") int count) { .. }
val bytes = byteArrayOf(1, 2, 3)
// without annotations:
val byteString1 = ByteString.of(bytes, 0, bytes.length)
// with annotations:
val byteString2 = bytes.toByteString()
At present, libraries written in Java that want to be Kotlin friendly can add nullness annotations to avoid platform types and can use well-known names to enable some operator use and property use. This is the limit of the Kotlin language features they have access to.
A Java library can do one of two things to go farther with its support:
- Publish Kotlin extensions inside their library or as a sibling artifact.
- Rewrite their public API (or the entire library) in Kotlin.
Since rewriting in Kotlin would obviously solve all of the interoperability problems, option #1 is the only real comparison. JUnit 5 and Project Reactor are two examples which include Kotlin extensions in their primary artifact but with an optional Kotlin dependency. RxKotlin and Android KTX are two examples of separate artifacts (for RxJava and the Android framework, respectively) to provide extensions.
The majority of features provided by these libraries are adapting Java
static methods into Kotlin instance extensions. Functionality-wise, there
should be no difference between an annotated Java method and a manually-written
Kotlin extension. For the Guava ByteSource
factory, a zero-overhead extension
can be written:
inline fun File.byteSource(): ByteSource = Files.asByteSource(this)
Adding parameter names and default values to manually-written Kotlin extensions works for adapting Java static method but does not fully work for instance methods. For the Android padding example, you can write an extension that can enables the use of parameter names for a subset of arguments, but fails when you try to include all four:
inline fun View.setPadding(
left: Int = paddingLeft,
top: Int = paddingTop,
right: Int = paddingRight,
bottom: Int = paddingBottom
) {
setPadding(left, top, right, bottom)
}
// works:
view.setPadding(left = 5, right = 5)
// error:
view.setPadding(left = 5, right = 5, top = 10, bottom = 10)
This is because Kotlin will always prefer calling a real member over an extension. When all four arguments are provided the member (and its lack of parameter name support) is resolved.
As outlined in the "Migration" section, the annotation-based approach enables Kotlin consumers to be gradually introduced to the Kotlin calling convention without having to know they're available and without having to seek out a sibling artifact.
Other advantages start to become subjective:
-
The annotation-based approach keeps a single source of truth for both Java and Kotlin callers. This is similar to what
@JvmName
and@JsName
provide to Kotlin API authors for controlling how Java and Javascript consumers see their API.A potential downside of this is that you are limited to the functionality that the annotations provide.
A single source of truth can be both an advantage and disadvantage for documentation. For usage samples, it can be challenging to cover the conventions of each language. The full documentation is always present, though, regardless of the calling language. Manually-written extensions tend to contain only a summary and a
@see
directive to the original Java method. -
No new compiler / toolchain has to be introduced for the annotation-based approach. Your compilation and distribution mechanism for Java does not have to be updated to include Kotlin in the main or a sibling artifact.
Since these annotations change the calling convention and/or the name of functions and properties, their addition is otherwise a source-incompatible change for Kotlin users. In an effort to mitigate the pain that this would cause and to guide the consumer to discoverability of the Kotlin-specific calling convention, both the Java-style and Kotlin-style invocations are allowed.
The previous example:
@ExtensionFunction("toByteString")
public static ByteString of(
byte[] bytes,
@ParameterName("offset") @DefaultValue("0") int offset,
@ParameterName("count") @DefaultValue("bytes.length") int count) { .. }
val bytes = byteArrayOf(1, 2, 3)
// Java-style:
val byteString1 = ByteString.of(bytes, 0, bytes.length)
// Kotlin-style:
val byteString2 = bytes.toByteString()
would compile as-is having both styles in use.
To gently migrate users to the Kotlin-style syntax, the Kotlin IDEA plugin would highlight Java-style call sites with a yellow underline. An intention action would automatically rewrite the Java-style to Kotlin-style, using default values where appropriate.
This migration behavior is designed to match what happens when other
Java-style conventions are used such as calling foo.equals(bar)
(which
should be foo == bar
) or foo.getBaz()
(which should be foo.baz
).
The compiler would not emit these as actual warnings.
If a library author chooses to ignore validation of any kind and creates an invalid configuration the Kotlin consumer shouldn't be punished.
For example, the ability to supply an alternate name for extensions and parameters opens up the possibility to create name collisions:
public static String one(
@ParameterName("one") int one,
@ParameterName("one") int uno) { .. }
In this case the entire one
method would be rejected from any annotation
enhancement. This is important as the intent can be ambiguous in certain
configurations. For example, both @ExtensionFunction
and @ExtensionProperty
may be present on a single method.
The Kotlin compiler, when consuming an annotated Java library, should emit a warning for each Java method with an invalid configuration. The Kotlin IDEA plugin should not show any warnings inline, not offer any migration assistance, and treat the method as otherwise being pure, vanilla Java.
Since these annotations are meant for consumption by Java libraries, they
should not be part of the Kotlin standard library. Since they are
Kotlin-specific, they should not be part of org.jetbrains:annotations
.
A new artifact, org.jetbrains.kotlin:kotlin-java-annotations
should be
created to house these annotations. Since the interpreter for these
annotations is the Kotlin compiler and Kotlin IDEA plugin, they should
live in the JetBrains/kotlin repository and be versioned
and released as part of the normal Kotlin process.
This artifact could also be the same for the annotations proposed in KEEP-99.
When adding platform-specific annotations such as @JvmName
or @JsName
to
Kotlin code the compiler validates you are not violating the target platform's
language rules. When Java code is being annotated, however, the Kotlin compiler
is not present to perform validation. An annotation processor will be provided
to perform validation.
The annotation processor artifact will be
org.jetbrains.kotlin:kotlin-java-annotations-validator
and live in the
JetBrains/kotlin repository and be versioned and released as
part of the normal Kotlin process.
In order to get real-time validation in the IDE, the Kotlin IDEA plugin will also include the validation rules which it will run on Java sources containing the annotations to show errors.
This proposal focuses on only 4 annotations: @ExtensionFunction
,
@ExtensionProperty
, @DefaultValue
, and @ParameterName
. These were chosen
because a sampling of Java libraries showed them to have the most impact to
Kotlin consumers if implemented.
There are certainly other annotations which could be proposed and implemented
to further interop. Ones that come to mind are denoting top-level functions,
lambda parameters becoming lambda-with-receiver, final classes with a static
getInstance()
method becoming an object, Class
-accepting generic methods
being automatically provided with reification, and destructing component
markers. While appealing, these are decidedly more rare and would only increase
the complexity of this proposal. If desired they can be pursued at a later
time.