4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

KotlinAdvent Calendar 2021

Day 2

JavaでKotlinと同じくらい安全なコードを書く(後編)

Last updated at Posted at 2021-12-01

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

前書き

この記事はJavaでKotlinと同じくらい安全なコードを書く(前編)の続きです。

前編ではJavaで安全性を高めるための工夫として、以下の4点を書きました。

  • nullを安全に取り扱う
  • メソッド・クラス・変数を基本的に変更できないようにする
  • Collectionを読み取り専用にする
  • 一致を安全に判定する

後編では、これらがKotlinでどのような表現になるかと、Javaで実現した場合と比べた簡単さについて書きます。

nullを安全に取り扱う

Kotlinでは、nullableな場合型に?を付けます。

Kotlin
// fooはnon-null, barはnullable、戻り値はnon-null
fun func(foo: String, bar: String?): String { /* 略 */ }

このコードは、前回の記事で紹介した下記のコードと等価です。

Java
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

@Nonnull
public String func(@Nonnull String foo, @Nullable String bar) { /* 略 */ }

比較すると、Kotlinのコードでは特にアノテーションを付ける必要が無く、シンプルなことが分かります。
また、Kotlinでは指定されたnullabilityに違反するコードを書いた場合コンパイルエラーになります。

メソッド・クラス・変数を基本的に変更できないようにする

メソッド/クラスを変更できないようにする

クラスを継承するためにはopenで修飾する必要が有り、何もしなかった場合、継承するとコンパイルエラーになります。

Kotlin
class Closed
class ClosedEx : Closed() // コンパイルエラーになる

open class Open
class OpenEx : Open() // コンパイルエラーにならない

以下はJavaでクラスを継承不可にするコードです。
実際のコードでは継承を利用することの方が少ないため、Kotlinの方がシンプルかつ安全です。

Java
public final class Temp {}
// コンパイルエラーになる
public class TempEx extends Temp {}

openクラスに定義した関数に関しては自由にオーバーライドできるため、これを継承不可にする場合はJava同様にfinal修飾する必要が有ります。

Kotlin
open class Open {
    final fun closed() {}
    open fun open() {}
}
class OpenEx : Open() {
    override fun closed() { super.closed() } // コンパイルエラーになる
    override fun open() { super.open() } // コンパイルエラーにならない
}
Java
public class Temp {
    public final void closed() {}
}
public class TempEx extends Temp {
    @Override
    public void closed() { super.func(); } // コンパイルエラーになる
}

こちらはKotlinJavaであまり差は有りません。

変数を変更できないようにする

Kotlinでは、変数をvalもしくはvarで宣言します。
valで宣言した場合は再代入不可、varで宣言した場合は再代入可能となります。

Kotlin
val value: Int = 0
var variable: Int = 0

value = 1 // コンパイルエラーになる
variable = 1 // コンパイルエラーにならない

一々final修飾する必要が無くなっているため、lombokを導入していないJavaに比べるとこちらの方がシンプルです。

また、Kotlinでは、引数、拡張for文の変数、ラムダ式のレシーバーに再代入するとコンパイルエラーになります。
こちらも一々final修飾する必要が無くなっており、シンプルかつ安全です。

引数への再代入不可

Kotlin
// コンパイルエラーになる
fun f1(arg: Int?) { arg = 0 }
Java
// コンパイルエラーになる
public void f1(@Nonnull final Integer arg) { arg = 0; }

拡張for文の変数への再代入不可

Kotlin
// コンパイルエラーになる
for (i in list) { i = 0 }
Java
// コンパイルエラーになる
for (@Nonnull final Integer i : list) { i = 0; }

ラムダ式のレシーバーへの再代入不可

Kotlin
// コンパイルエラーになる
list.forEach { i -> i = 0 }
Java
// コンパイルエラーになる
list.forEach((@Nonnull final Integer i) -> { i = 0; });

Collectionを読み取り専用にする

Kotlinでは、mutableCollectionread onlyCollectionが区別されています。

Listの例
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...でラップするか、以下のようなラッパークラスをそれぞれ定義する必要が有ります。

Collectionのラッパークラス
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に定義された変更しない操作
}
Listのラッパークラス
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
// 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に触れてみて下さい。

4
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?