KotlinにおけるJavaとの相互運用性を高めるための工夫

この記事はKotlin Advent Calendar 2017の6日目です。

KotlinはJavaとの相互運用性を重視していますが、JavaとKotlinは言語仕様的に異なる部分があり、Kotlinはその違いを吸収するために様々な工夫をしています。

この記事では、その工夫をいくつかピックアップして紹介したいと思います。

Jvm Annotation

@JvmStatic

KotlinのCompanion Objectで定義したコードをJavaから呼び出すと、Companionクラスを経由することになります。Companionクラス経由であっても動作上の問題はありませんが、出来れば普通にJavaで定義した場合と同様な呼び出し方をしたいですよね?

そういった場合はJvmStaticアノテーションを付けることで、staticとして定義されているかのように呼び出すことができます。

// Kotlin
class JvmStaticSampleForCompanionObject {
    companion object {
        @JvmStatic
        fun doSomething() { /* 省略 */ }
    }
}
// Java
JvmStaticSampleForCompanionObject.Companion.doSomething(); // JvmStaticなし
JvmStaticSampleForCompanionObject.doSomething(); // JvmStaticあり

また、objectクラスに対してJvmStaticアノテーションをつけた場合も同様の挙動になります。

// Kotlin
object JvmStaticSampleForObject {
    @JvmStatic
    fun doSomething() { /* 省略 */ }
}
// Java
JvmStaticSampleForObject.INSTANCE.doSomething(); // JvmStaticなし
JvmStaticSampleForObject.doSomething(); // JvmStaticあり

単純に呼び出し方の問題ならCompanionクラス経由でも動作上はまったく問題ありません。しかし、Javaのライブラリの中にはstaticクラス/メソッドとして定義されていることを期待して実装されているものもあり、そういったライブラリを使う場合はJvmStaticアノテーションが有用です。

例えば、AndroidにおけるData Bindingでカスタムバインディングを定義する場合、BindingAdapterアノテーションを付与したメソッドはstaticメソッドとして定義されていることを期待しているため、Kotlinでカスタムバインディングを実装する場合はJvmStaticアノテーションが必要になります。具体例に関しては、Android Data Binding Tipsをご覧ください。

@JvmField

Kotlinでプロパティとして定義したものをJavaから呼び出す場合は自動生成されたGetter/Setterを経由してアクセスすることになりますが、このプロパティにJvmFieldアノテーションを付与することでJavaのFieldとして定義されているかのように呼び出すことができます。

// Kotlin
class JvmFieldSample {
    @JvmField
    val foo = 0
}
// Java
JvmFieldSample jvmFieldSample = new JvmFieldSample();
jvmFieldSample.getFoo(); // JvmFieldなし
jvmFieldSample.foo; // JvmFieldあり

@JvmOverloads

Kotlinはデフォルト引数をサポートしており、メソッドのオーバーロードを簡潔に記述することができますが、そのままではJavaから呼び出した場合にオーバーロードされていることになりません。このような場合にはJvmOverloadsアノテーションをつけることでオーバーロードされているかのように呼び出すことができます。

// Kotlin
class JvmOverloadsSample @JvmOverloads constructor(val foo: Int, val bar: Int = 0)
// Java
new JvmOverloadsSample(0);
new JvmOverloadsSample(0, 0);

その他

Jvm Annotationは今回紹介した以外にもいくつかあります。

  • @JvmName
  • @JvmMultifileClass
  • @JvmWildcard
  • @JvmSuppressWildcards

これらに関してはこの辺に解説があるので、興味がある方は覗いてみてください。

Platform Type

KotlinはJavaとの相互運用性を重視しており、Kotlin/Javaが混在するプロジェクトではJavaからKotlinのコードを呼び出すこともあると思います。ここで疑問なのが、Kotlinは言語仕様としてNullableとNonNullを明確に区別している点です。Javaから呼び出す以上はこれらを区別することは不可能ですが、JavaからKotlinのコードを呼び出すこと自体は問題なくできます。

このような型の扱い方の違いを吸収するためにKotlinはPlatform Typeという特殊な型を持っています。

具体例を交えつつ説明すると、以下のようなJavaクラスを定義し、それをKotlinから呼び出す場合を考えてみます。

// Java
public class PlatformTypeSample {
    public static Integer getInteger() {
        return 0;
    }
}
// Kotlin
val int = PlatformTypeSample.getInteger()

さて、上記のintはどんな型として推論されるでしょうか? Intでしょうか、Int?でしょうか。

答えはそのどちらでもなく、Int!になります。 この最後に!の付いた型がPlatform Typeで、Intかもしれないし、Int?かもしれない型になります。 つまり、実行時にnullが渡された場合は実行時エラーが発生することになります。

これではせっかくKotlinで書いているメリットが半減してしまいますが、Nullableアノテーション、NonNullアノテーションを使うことでPlatform Typeとして推論されることを防ぐことができます。

// Java
public class PlatformTypeSample {
    @NonNull or @Nullable
    public static Integer getInteger() {
        return 0;
    }
}
// Kotlin
val nonNullInt: Int = PlatformTypeSample.getInteger() // NonNullアノテーション
val nullableInt: Int? = PlatformTypeSample.getInteger() // Nullableアノテーション

アノテーションをうまく活用することでJavaと共存しつつも型安全なコードを書くことができます。

Mapped Type

JavaとKotlinは言語仕様が異なるため、以下のようなマッピングが定義されており、相互運用する場合はこれに沿ってマッピングされることになります。

Primitive Type

Java Kotlin
byte kotlin.Byte
short kotlin.Short
int kotlin.Int
long kotlin.Long
char kotlin.Char
float kotlin.Float
double kotlin.Double
boolean kotlin.Boolean

Non-Primitive Built-in Class

Java Kotlin
java.lang.Object kotlin.Any!
java.lang.Cloneable kotlin.Cloneable!
java.lang.Comparable kotlin.Comparable!
java.lang.Enum kotlin.Enum!
java.lang.Annotation kotlin.Annotation!
java.lang.Deprecated kotlin.Deprecated!
java.lang.CharSequence kotlin.CharSequence!
java.lang.String kotlin.String!
java.lang.Number kotlin.Number!
java.lang.Throwable kotlin.Throwable!

Javaの参照型は基本的にPlatform Typeにマッピングされるようなので、積極的にNonNullアノテーションをつけておきたいところです。

Boxed Primitive Type

Java Kotlin
java.lang.Byte kotlin.Byte?
java.lang.Short kotlin.Short?
java.lang.Integer kotlin.Int?
java.lang.Long kotlin.Long?
java.lang.Character kotlin.Char?
java.lang.Float kotlin.Float?
java.lang.Double kotlin.Double?
java.lang.Boolean kotlin.Boolean?

まとめ

この記事ではKotlinがJavaとの互換性を保つために行っている工夫をいくつか紹介しました。

今回紹介した以外にも公式リファレンスには涙ぐましい?工夫が載っています。興味のある方は以下のページを覗いてみると面白いかもしれません。

それでは、Have a nice Kotin!