この記事はKotlin Advent Calendar
の2日目の記事です。
前書き
この記事はJavaでKotlinと同じくらい安全なコードを書く(前編)の続きです。
前編ではJava
で安全性を高めるための工夫として、以下の4点を書きました。
-
null
を安全に取り扱う - メソッド・クラス・変数を基本的に変更できないようにする
-
Collection
を読み取り専用にする - 一致を安全に判定する
後編では、これらがKotlin
でどのような表現になるかと、Java
で実現した場合と比べた簡単さについて書きます。
nullを安全に取り扱う
Kotlin
では、nullable
な場合型に?
を付けます。
// fooはnon-null, barはnullable、戻り値はnon-null
fun func(foo: String, bar: String?): String { /* 略 */ }
このコードは、前回の記事で紹介した下記のコードと等価です。
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@Nonnull
public String func(@Nonnull String foo, @Nullable String bar) { /* 略 */ }
比較すると、Kotlin
のコードでは特にアノテーションを付ける必要が無く、シンプルなことが分かります。
また、Kotlin
では指定されたnullability
に違反するコードを書いた場合コンパイルエラーになります。
メソッド・クラス・変数を基本的に変更できないようにする
メソッド/クラスを変更できないようにする
クラスを継承するためにはopen
で修飾する必要が有り、何もしなかった場合、継承するとコンパイルエラーになります。
class Closed
class ClosedEx : Closed() // コンパイルエラーになる
open class Open
class OpenEx : Open() // コンパイルエラーにならない
以下はJava
でクラスを継承不可にするコードです。
実際のコードでは継承を利用することの方が少ないため、Kotlin
の方がシンプルかつ安全です。
public final class Temp {}
// コンパイルエラーになる
public class TempEx extends Temp {}
open
クラスに定義した関数に関しては自由にオーバーライドできるため、これを継承不可にする場合はJava
同様にfinal
修飾する必要が有ります。
open class Open {
final fun closed() {}
open fun open() {}
}
class OpenEx : Open() {
override fun closed() { super.closed() } // コンパイルエラーになる
override fun open() { super.open() } // コンパイルエラーにならない
}
public class Temp {
public final void closed() {}
}
public class TempEx extends Temp {
@Override
public void closed() { super.func(); } // コンパイルエラーになる
}
こちらはKotlin
とJava
であまり差は有りません。
変数を変更できないようにする
Kotlin
では、変数をval
もしくはvar
で宣言します。
val
で宣言した場合は再代入不可、var
で宣言した場合は再代入可能となります。
val value: Int = 0
var variable: Int = 0
value = 1 // コンパイルエラーになる
variable = 1 // コンパイルエラーにならない
一々final
修飾する必要が無くなっているため、lombok
を導入していないJava
に比べるとこちらの方がシンプルです。
また、Kotlin
では、引数、拡張for
文の変数、ラムダ式のレシーバーに再代入するとコンパイルエラーになります。
こちらも一々final
修飾する必要が無くなっており、シンプルかつ安全です。
引数への再代入不可
// コンパイルエラーになる
fun f1(arg: Int?) { arg = 0 }
// コンパイルエラーになる
public void f1(@Nonnull final Integer arg) { arg = 0; }
拡張for文の変数への再代入不可
// コンパイルエラーになる
for (i in list) { i = 0 }
// コンパイルエラーになる
for (@Nonnull final Integer i : list) { i = 0; }
ラムダ式のレシーバーへの再代入不可
// コンパイルエラーになる
list.forEach { i -> i = 0 }
// コンパイルエラーになる
list.forEach((@Nonnull final Integer i) -> { i = 0; });
Collectionを読み取り専用にする
Kotlin
では、mutable
なCollection
とread only
なCollection
が区別されています。
val readOnly: List<Int> = listOf(1, 2, 3)
val mutable: MutableList<Int> = mutableListOf(1, 2, 3)
readOnly[0] = 1 // コンパイルエラーになる
mutable[0] = 1 // コンパイルエラーにならない
Java
で同様の安全性を確保する場合、変更操作で実行時エラーが発生することを承知でCollections.unmodifiable...
でラップするか、以下のようなラッパークラスをそれぞれ定義する必要が有ります。
import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.Iterator;
/**
* 変更できないCollection
*/
public abstract class ImCollection<E> implements Iterable<E> {
@Nonnull private final Collection<E> rawValue;
protected ImCollection(@Nonnull Collection<E> rawValue) {this.rawValue = rawValue;}
@Override
@Nonnull
public final Iterator<E> iterator() { return rawValue.iterator(); }
/**
* @return 変更するためのCollection
*/
// 継承先で具体的な型を指定するためにabstractで定義している
@Nonnull
public abstract Collection<E> getUnsafeValue();
public int size() { return rawValue.size(); }
public boolean isEmpty() { return rawValue.isEmpty(); }
public boolean contains(E e) { return rawValue.contains(e); }
// 以降Collectionに定義された変更しない操作
}
import javax.annotation.Nonnull;
import java.util.List;
/**
* 変更できないList
*/
public final class ReadOnlyList<E> extends ImCollection<E> {
@Nonnull private final List<E> value;
public ReadOnlyList(@Nonnull List<E> value) {
super(value);
this.value = value;
}
@Override
@Nonnull
public List<E> getUnsafeValue() {return value;}
public E get(int index) { return value.get(index); }
public int indexOf(E e) { return value.indexOf(e); }
public int lastIndexOf(E e) { return value.lastIndexOf(e); }
// 以降Listに定義された変更しない操作
}
Collection
の内容を生成時以外に変更することは少ないため、Kotlin
の方がシンプルかつ安全です。
一致を安全に判定する
Kotlin
のコード上では、プリミティブ型とオブジェクト型、及びnullability
の違いを意識する必要はなく、ただ単に==
を使うだけで比較が行えます。
// Kotlin上のIntは、non-nullならプリミティブ型、nullableならラッパー型としてJVM上で扱われるが、
// Kotlin上では全く同じように比較できる
fun isEquals(i: Int, j: Int): Boolean { return i == j }
fun isEquals(i: Int, j: Int?): Boolean { return i == j }
fun isEquals(i: Int?, j: Int?): Boolean { return i == j }
Java
の場合、以下のように型ごとに比較方法を意識する必要が有ります。
また、サンプルコードには出していませんが、null
からequals
を呼び出すと実行時エラーになるためnull
チェックも適切に行う必要が有ります。
それらを統一的に扱うため、Objects.equals
に統一する手段も有りますが、Kotlin
の方がシンプルに書くことができます。
// primitive型同士
boolean isEquals(int i, int j) { return i == j; }
// primitive型とオブジェクト型
boolean isEquals(int i, @Nonnull Integer j) { return i == j; }
// オブジェクト型同士
boolean isEquals(@Nonnull Integer i, @Nonnull Integer j) { return i.equals(j); }
補足: 同一インスタンスかの比較について
Java
でオブジェクト型を==
で比較した場合同一インスタンスかの比較になりますが、Kotlin
では===
でこの比較が行えます。
否定形は!==
です。
後書き
このシリーズではJava
で安全なコードを書く大変さと、Kotlin
で安全なコードを書く簡単さについて書きました。
Java
で安全性を突き詰めていくと、ライブラリ・フレームワークの導入やコード・CI
の複雑化が避けられず、最後には人の目を入れる必要性もあるため、100%徹底することも難しいという課題に当たります。
一方Kotlin
では、ただ文法に沿って書くだけで、コンパイル時の機械的なチェックによって多くの安全性を確保できます。
ライブラリ・フレームワークを導入したり、コードやCI
を複雑にすることなく安全性を確保できることは、Kotlin
を使う大きな利点です。
また、シンプルかつ安全にコードを書けるということは、コード内に全ての意図が分かりやすく表現される = ドキュメント性の高いコードが書かれるということにもつながります。
Kotlin
の書きやすい・読みやすいという特徴は、ソフトウェア開発における生産性の向上につながります。
Java
に限らず、安全性が気になっていたり、安全に書くために生産性を犠牲にしている感の有る方は、是非Kotlin
に触れてみて下さい。