JavaからKotlinに変換する7つのテクニック Kotlinらしさを生かした簡潔なコードに置き換えよう
既存のJavaコードをKotlinに変換する場面を想定し、より簡潔でKotlinらしいコードに置き換えるテクニックを、ヤフー株式会社でYahoo!ニュースアプリを開発する池田惇さんが解説します。開発現場にまだ多く残るJavaコードを必要に応じてKotlinへ置き換えることで、開発の負担を減らすことができます。
アプリエンジニアの池田惇(@jun_ikd)です。
これまでYahoo!ニュースや映像配信サービスGYAO!のAndroidアプリにKotlinを導入して、Javaからの置き換えを行ってきました。その経験などをもとに、既存のJavaコードを変換する際にどのように書けば、Kotlinの長所を生かすことができるかを紹介していきます
本記事のコードは下記の環境で制作しています。
- Build #IC-183.5912.21, built on February 26, 2019
- JRE: 1.8.0_152-release-1343-b28 x86_64
- JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
- macOS 10.14.3
Kotlinの利用拡大とJavaからの変換
Kotlinは、Java仮想マシン上で動作するプログラミング言語として2011年7月に発表され、Google I/O 2017でAndroidの開発言語として正式に採用されました。
サーバサイド/クライアントサイドのいずれもKotlinで開発できますが、特にAndroidアプリ開発においてはJavaに代わってもはやスタンダードになったと言えるのではないでしょうか。Javaと比較して短く簡潔に書けるという特徴から、勉強会のコード例などは今やほぼ全てKotlinで書かれていると思います。
GitHub社のブログ記事によると、Kotlinは2018年に最も利用が拡大したプログラミング言語とされています。
The State of the Octoverse: top programming languages of 2018 - The GitHub Blog
JavaからKotlinへの自動変換機能
Kotlin開発を始める方の多くは、既にJavaで書かれたアプリケーションを担当されており、JavaからKotlinへの置き換えを進めることが増えていくと思います。
統合開発環境であるIntelliJ IDEAやAndroidStudioには、次の画像のようにJavaからKotlinへの自動変換機能が備わっています(メニューバーの「Code」→「Convert Java File to Kotlin File」メニュー)。
この自動変換はたいへん優秀なので、自動変換しただけで、動くKotlinコードが完成することもあります。しかし、言語の長所を生かしたKotlinらしいコードには、いま一歩及ばないことが多いと思います。
本記事では、JavaからKotlinに置き換える際のテクニックを紹介し、簡潔でメンテナンスしやすいコードを目指します。
Kotlinの特徴
実際にコードを紹介する前に、簡単にKotlinの特徴を見てみましょう。
Kotlinの公式サイトには、Kotlinを選ぶ理由として以下の4つが挙げられています。
- 簡潔さ
- 後述する
data class
やラムダ式など、コードを簡潔に記述できる仕組みがあります。 - 安全性
- null許容・非許容の型が区別されるため、想定外の
NullPointerException
を防止できます。var hoge: String hoge = null // コンパイルエラー
- Javaとの相互運用性
- Java向けに開発されたライブラリが、ほぼ全て利用できます。もちろん、Java・Kotlin双方からコードを呼び出すことができます。
- 開発ツールとの親和性
- KotlinはもともとJetBrains社で開発されていたことから、統合開発環境であるIntelliJ IDEAとその派生であるAndroidStudioでは、Kotlin開発のためのサポートが充実しています。
Kotlinについて詳しく学びたい方は、下記の書籍などを参考にされると良いと思います。
Javaからの変換テクニック集
ここからは具体的に、JavaからKotlinへ変換する際のテクニックを挙げていきます。
コードの記述方法に正解はありませんが、書き方を選択する際の目安にしていただけると良いと思います。
1. nullチェックを簡潔に書く
値が存在しないこと表すとき、Java・Kotlinのいずれでもnull
を使います。
null
の変数を誤って参照するとNullPointerException
が発生してアプリケーションが停止してしまいます。これはJavaアプリケーションにおける最も多いクラッシュの一つと言えると思います。
これを防止するため、Javaでは@Nullable
アノテーションを使って変数にnull
が入ることを明示したり、if文を使ってチェックをするなど、多くの箇所で煩雑な記述を行う必要がありました。
Kotlinにおいてもチェック自体は必要なのですが、下記のパターンに応じてそれぞれ簡潔に記述することができます。
- メソッドの引数が
null
だったら処理を抜ける - 変数が
null
でないときだけ処理をする
この2つを、まずはJavaで書いた例を見てみましょう。いずれのパターンもif文を使って書くことが一般的だと思います。
// メソッドの引数がNullだったら処理を抜ける void someMethod(@Nullable String s) { if (s == null) return; } // 変数がNullで無いときだけ処理をする if (hoge != null) { // 処理本体 }
Kotlinではそれぞれ、エルビス演算子?:
とスコープ関数let
を使って、このように記述できます。
// メソッドの引数がNullだったら処理を抜ける fun someFunction(s: String?) { s ?: return } // 変数がNullで無いときだけ処理をする hoge?.let { // 処理本体 }
2. データを表すオブジェクトにはdata classを使う
Javaでデータを表すオブジェクトを作る場合は、下記のようになると思います。
public class SomeObject { private String mValue; SomeObject(String value) { mValue = value; } public String getValue() { return mValue; } @Override public boolean equals(Object o) { // 省略 } @Override public int hashCode() { // 省略 } @Override public String toString() { // 省略 } @Override protected Object clone() throws CloneNotSupportedException { // 省略 } }
この例のSomeObject
はmValue
というString型の値を1つ持っているだけです。しかし、同値性を表すためにequals()
とhashCode()
、文字列として出力するためにはtoString()
、複製のためにclone()
を実装する必要があります。
同じコードをKotlinで書くと、次の1行になります。
data class SomeObject(val value: String)
値を得るためのgetterも書く必要はありませんし、equals()
等のメソッドは自動的に実装済みになります(独自のロジックを使いたい場合は自分で実装します)。
また、複製にはcopy()
関数を使うことができます。既存のインスタンスから一部の値を変更しつつ新しいインスタンスを作ることが簡単にできます。
// copy()関数を使った複製 val someObject = SomeObject("hoge") val newObject = someObject.copy("newValue")
3. 1つのファイルに複数のクラスを書ける
Javaでは、publicなクラスは1ファイルに一つしか記述できません(ファイル名=publicなクラス名になります)。
Kotlinでは、1ファイルに複数のクラスを書くことができます。前述の軽量なdata class
などは1ファイルにまとめておくことで、開発時に見やすくすることができると思います。
// 数行のクラスなどは1ファイルに並べて書いたほうが使いやすいことがある data class SomeObject(val value: String) data class User( val id: String, val name: String, val age: Int )
4. Listenerクラスは不要
ボタンのタップや非同期処理の完了などイベントを通知するため、JavaではListenerを使います。
下記の例では、JavaClass
のコンストラクタでSomeListener
を渡しています。検知したいイベントが起きたときに、onEvent()
メソッドが呼び出されます。
public class JavaClass { interface SomeListener { void onEvent(); } private SomeListener mListener; JavaClass(SomeListener listener) { mListener = listener; } void doEvent() { mListener.onEvent(); } }
Javaは、関数が第一級オブジェクトではありません(変数に格納できず、引数・戻り値として受け渡しできない)。そのため、上記のようにListenerクラスを渡す必要がありました。
しかし、Kotlinの関数は第一級オブジェクトなので、受け渡しをすることができます。同様のコードをKotlinで書くとこうなります。
class KotlinClass(private val callBack: () -> Unit) { fun doEvent() { callBack() } }
引数なし、戻り値Unit型(何も返さない)の関数callBack
を渡しており、JavaのようにInterfaceを定義する必要はありません。
5. objectでシングルトンを簡単に使える
アプリケーション内で単一のインスタンスを使いたい場合は、シングルトンパターンを使います。
下記のコードは、Javaでシングルトンを実装した例です。
public class JavaClass { private static final JavaClass INSTANCE = new JavaClass(); private JavaClass() {} public static JavaClass getInstance() { return INSTANCE; } }
static
なフィールドにインスタンスを持っておき、getInstance()
メソッドを呼び出すとそのインスタンスを返却します。コンストラクタはprivate
にして、新しいインスタンスを生成できないように実装しました。
Kotlinでは、次のようにobject
キーワードを使うだけでシングルトンを実装できます。
object KotlinSingleton
シングルトンにするための実装コードは全てなくなりました。KotlinSingleton
は、これだけで単一のインスタンスになることが保証されます。
6. Utilクラスは不要
軽量な処理を多くつくる際、Javaでの開発ではstatic
メソッドのみが記述されたUtilクラスを作ることが多かったと思います。
public class SomeUtilClass { public static String someMethod() { // 省略 } public static boolean someMethod2() { // 省略 } }
このようなUtilクラスは状態を持たないため、本来はクラスである必要がありません。しかし、Javaはクラス外に実装ができないので、仕方なくクラスを作ってその中にメソッドを並べていたと言えます。
一方、Kotlinでは、クラスの外に関数を直接記述できます(top-level functionと呼びます)。そのため、わざわざクラスを用いる必要はありません。
class SomeClass { // 省略 } // クラス外でも関数を記述できる fun someFunction(): String { // 省略 } fun someFunction2(): Boolean { // 省略 }
グルーピングしたい場合は、object
を使うのも良いでしょう。
// 呼び出すときはKotlinObject.someFunction()と書く object KotlinObject { fun someFunction(): String { // 省略 } fun someFunction2(): Boolean { // 省略 } }
7. 名前付き引数があればBuilderパターン不要
インスタンス生成時に引数が多い場合、JavaではBuilderパターンを使って扱いやすくすることが多いと思います。
下記の例では、String
型の引数a
・b
・c
を渡しています。Builderパターンを用いることで型が同じ引数を区別できるので、間違いを防止しています。
// 初期化に3つの変数が必要なクラス public class JavaClass { JavaClass(String a, String b, String c) { } } // JavaClassを生成するBuilderクラス public class Builder { private String a; private String b; private String c; public Builder setA(String a) { this.a = a; return this; } public Builder setB(String b) { this.b = b; return this; } public Builder setC(String c) { this.c = c; return this; } public JavaClass build() { return new JavaClass(a, b, c); } } // 利用時 // 3ついずれもString型の値だが、専用の名前がついた関数を使うことで間違いを防止できる new Builder() .setA("a") .setB("b") .setC("c") .build();
Kotlinでは、呼び出し時に名前付き引数を使えます。引数を区別しやすくできるので、多くの場合でBuilderパターンは不要だと思います。
// 初期化に3つの変数が必要なクラス class KotlinClass(a: String, b: String, c: String) // 利用時 // 名前 = 値 という書き方ができるので間違いを防止できる KotlinClass( a = "a", b = "b", c = "c" )
ただし、引数の過不足などバリデーション処理を要する場合は、Builderパターン等の仕組みを準備する必要があるでしょう。
実際にJavaをKotlinに変換してみよう
主なテクニックを7つ紹介しました。次に、Javaのコードを実際にKotlinに変換してみましょう。次の手順で作業を進めます。
- サンプルのJavaコードを、IntelliJ IDEAの自動変換を使ってKotlinに変換する
- 前セクションで紹介したテクニックのいくつかを使って、簡潔なコードに手直しする
変換する前のJavaコードはこちらです。このSampleClassは、3つの値と2つのメソッドを持っています。
public class SampleClass { interface SomeListener { void onEvent(); } private String a; private String b; private String c; SampleClass(String a, String b, String c) { this.a = a; this.b = b; this.c = c; } public String getA() { return a; } public String getB() { return b; } public String getC() { return c; } public boolean execA() { if (a == null) return false; // 処理本体 } public void execB(SomeListener listener) { if (b != null) { listener.onEvent(); } } @Override public boolean equals(Object o) { return super.equals(o); } @Override public int hashCode() { return super.hashCode(); } @Override public String toString() { return super.toString(); } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
自動変換機能でKotlinのコードを作成する
このファイルをIntelliJ IDEAで開き、メニューの「Code」→「Convert Java File to Kotlin File」を実行すると、自動変換によって下記のようなKotlinのコードになります。
class SampleClass internal constructor(val a: String?, val b: String?, val c: String) { internal interface SomeListener { fun onEvent() } fun execA(): Boolean { if (a == null) return false // 処理本体 } fun execB(listener: SomeListener) { if (b != null) { listener.onEvent() } } override fun equals(o: Any?): Boolean { return super.equals(o) } override fun hashCode(): Int { return super.hashCode() } override fun toString(): String { return super.toString() } @Throws(CloneNotSupportedException::class) protected fun clone(): Any { return super.clone() } }
これだけでも元のJavaより短いコードになりましたが、冗長な箇所がありますし、このままだとsuper.clone()
が見つからず、コンパイルエラーになります。
上記のテクニックをいくつか適用していきましょう。
データを表すオブジェクトにdata classを使う
まず、「データを表すオブジェクトにはdata classを使う」を適用し、SampleClass
を、data class
化します。
これによって、equals()
・hashCode()
・toString()
・clone()
が削除できるため、このようにより簡潔になります。
// 自動変換で付いたinternal constructorは削除 data class SampleClass( val a: String?, val b: String?, val c: String ) { // 自動変換で付いたinternalは削除 interface SomeListener { fun onEvent() } fun execA(): Boolean { if (a == null) return false // 処理本体 } fun execB(listener: SomeListener) { if (b != null) { listener.onEvent() } } // equals(), hashCode(), toString(), clone()を削除 }
nullチェックを簡潔に書き、Listenerクラスは不要
次に、「nullチェックを簡潔に書く」と「Listenerクラスは不要」で紹介した2つのテクニックを適用してみます。
data class SampleClass( val a: String?, val b: String?, val c: String ) { fun execA(): Boolean { // エルビス演算子で簡潔に記述 a ?: return false // 処理本体 } // Listenerクラスは削除して関数を受け取る fun execB(callback: () -> Unit) { // スコープ関数letで簡潔に記述 b.let { callback() } } }
execA()
では、nullチェックをエルビス演算子で記述し直しています。execB()
ではnullチェックをスコープ関数let
で簡潔に記述し、Listenerクラスを削除して関数を受け取ります。
このように3つのテクニックを適用するだけで、元のJavaコードよりもかなり短く、扱いやすいKotlinのコードに変換することができました。
まとめ
本稿では、既存のJavaコードをKotlinに変換する場面を想定し、より簡潔でKotlinらしいコードに置き換えるテクニックを紹介しました。冗長な記述を減らし、メンテナンス性を高めることができるでしょう。
Kotlinの利用は広がっていますが、開発現場にはまだまだ多くのJavaコードがあります。必要に応じてKotlinへ置き換えることで、開発の負担を減らすことができます。
池田 惇(いけだ・じゅん)@jun_ikd