Edited at

Effective Java を Kotlin で読む(6):第7章 メソッド

effectivekotlin.png


章目次

Effective Java を Kotlin で読む(1):第2章 オブジェクトの生成と消滅

Effective Java を Kotlin で読む(2):第3章 すべてのオブジェクトに共通のメソッド

Effective Java を Kotlin で読む(3):第4章 クラスとインタフェース

Effective Java を Kotlin で読む(4):第5章 ジェネリックス

Effective Java を Kotlin で読む(5):第6章 enum とアノテーション

Effective Java を Kotlin で読む(6):第7章 メソッド 👈この記事

Effective Java を Kotlin で読む(7):第8章 プログラミング一般

Effective Java を Kotlin で読む(8):第9章 例外

Effective Java を Kotlin で読む(9):第10章 並行性

Effective Java を Kotlin で読む(10):第11章 シリアライズ


第7章 メソッド


項目38 パラメータの正当性を検査する


概要

メソッドやコンストラクタのパラメータは何らかの制約を持っている場合がある(例:null であってはならない等)。その場合、明確に文書化した上で、処理の始めに正当性検査を行うべきである。

もし正当性検査を怠った場合、処理の途中で訳のわからない例外で失敗したり、誤った結果を返したりする。最悪のケースとして、メソッドは正常にリターンするが内部状態を不正にしてしまい、全く関係の無い箇所でエラーを発生させる事もある。

public のメソッド・コンストラクタに関しては、制約が守られていない場合にスローされる例外を Javadoc の @throw タグを使用して文書化すべきである。

また、公開されないメソッド・コンストラクタに関してはアサーションを用いることで、開発中のみ検査を行うことができる(-ea オプションを有効にしない限り検査を行わない)。


Kotlin で読む

Kotlin では標準ライブラリに正当性検査用のメソッドが用意されているので、これを使おう。



  • require


    • 引数が false の場合 IllegalArgumentException をスローする

    • メソッドの引数をテストする時に使う




  • check


    • 引数が false の場合 IllegalStateException をスローする

    • オブジェクトの状態をテストする時に使う




  • assert


    • 引数が false の場合 AssertionError をスローする

    • ただし -ea オプションを有効にしない限り検査を行わない



fun hoge(a: String) {

require(a == "hoge") { "hoge じゃない!" }
}

var state = "invalid"
fun fuga() {
check(state == "valid") { "状態が異常!" }
}

fun piyo() {
assert(false) { "テスト時にのみ失敗" }
}

Try Kotlin で確認


項目39 必要な場合には、防御的にコピーする


概要

クラスを設計する際は、利用者は不変式を破壊するために徹底した努力をすることを想定し防御的にプログラムするべきである。

例えば、不変な期間を表現するクラスを考える。

public final class Period {

private final Date start, end;

public Period(Date start, Date end) {
this.start = start;
this.end = end;
}
public Date start() { return start; }
public Date end() { return end; }
}

このクラスには問題があり、以下のように不変式を破壊する事ができる。

Date start = new Date();

Period period = new Period(start, new Date());

start.setTime(1000); // 不変式破壊!
period.end().setTime(2000); // 不変式破壊!

これは以下のように防御的にコピーを行うことで防ぐことができる。

public class FixedPeriod {

private final Date start, end;

public FixedPeriod(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
}
public Date start() { return new Date(start.getTime()); }
public Date end() { return new Date(start.getTime()); }
}

また書籍では、この本当の教訓は「可能であれば常に構成要素として不変オブジェクトを使うべき」であるとしている。


Kotlin で読む

素直に Java の例を Kotlin で書けばこのようになるが…

class FixedPeriod(start: Date, end: Date) {

var start: Date
private set
get() = Date(field.time)
var end: Date
private set
get() = Date(field.time)

init {
this.start = Date(start.time)
this.end = Date(end.time)
}
}

書籍でも述べられているように、構成要素としては不変オブジェクトを使うべきであろう。

data class ImmutablePeriod(val startMs: Long, val endMs: Long)

Try Kotlin で確認

data class であれば copy メソッドが使えたりと、Kotlin では Java に比べて不変なオブジェクトの扱いが簡単になっている。

積極的に不変オブジェクトを使っていこう。


項目40 メソッドのシグニチャを注意深く設計する


概要

この項目は、独立した項目を作るほどではないAPI設計のヒント集である。



  • メソッド名を注意深く選ぶ


    • 名前は常に標準命名規則(項目56)に従うべきである

    • 疑問があればJavaライブラリを参考にすると良い(ただし一部矛盾はある)




  • 便利なメソッドを提供し過ぎたりしないようにする


    • 個々のメソッドは「自分の役割を果たす」べき

    • メソッドが多いと、クラスの学習・使用・文書化・テスト・保守等を困難にする




  • 長いパラメータのリストは避ける


    • 4個以下を目標にすべき


    • 同じ型のパラメータが何個も続くのは特に有害である


      • 分かり辛い上に、順序を間違えてもコンパイルできてしまう






  • パラメータ型に関しては、クラスよりインタフェースを選ぶ


    • 柔軟性が増し、未来の型にも対応できる

    • 例: HashMap よりも Map を利用する




  • boolean パラメータより2つの要素を持つ enum 型を使用する


    • 後から3つめの要素が出てきても対応できる

    • 便利なメソッドを追加することもできる




Kotlin で読む

それぞれの項目について Kotlin 視点で考える。


  • メソッド名を注意深く選ぶ

"Kotlin follows the Java naming conventions."

との事なので Java の標準命名規則に従えば良さそう。


  • 便利なメソッドを提供し過ぎたりしないようにする

Java 同様の意識で良さそう。

また、必要なら拡張関数を使えば利用者側でユーティリティを用意もできる。


  • 長いパラメータのリストは避ける

名前付き引数が使えるため、そこまで神経質にならなくても良いかなとは思う。

が、そもそもパラメータが増えすぎない設計を目指すべきではある。


  • パラメータ型に関しては、クラスよりインタフェースを選ぶ

Java 同様の意識で良さそう。

また、特にパラメータに不変なものを使うと、項目39のようにTOCTOU攻撃を防げて良さそう。


  • boolean パラメータより2つの要素を持つ enum 型を使用する

Java 同様の意識で良さそう。


項目41 オーバーロードを注意して使用する


概要

Java でクラスのメソッド選択は、オーバーロードでは静的に、オーバーライドでは動的に行われる。

以下が具体例である。

public class Overload {

public static String classify(List<?> obj) {
return "List";
}
public static String classify(Collection<?> obj) {
return "Collection";
}
}

public class OverrideParent {

public String getClassName() {
return "OverrideParent";
}
}

public class OverrideChild extends OverrideParent {

@Override
public String getClassName() {
return "OverrideChild";
}
}

public class Main {

public static void main(String[] args) {
Collection<?> list = new ArrayList();
System.out.println(Overload.classify(list)); // Collection

OverrideParent child = new OverrideChild();
System.out.println(child.getClassName()); // OverrideChild
}
}

また、オートボクシングが絡むとどのメソッドが呼ばれるか分かり辛くなる。

List<String> strList = new ArrayList<>();

strList.add("1");
strList.add("2");
strList.remove("1"); //
System.out.println(strList); // 出力: [2]

List<Integer> intList = new ArrayList<>();
intList.add(1);
intList.add(2);
intList.remove(1);
System.out.println(intList); // 出力: [1]

これは、String の例では List#remove(Object o) が、 Integer の例では List#remove(int index) が呼ばれるためである。(リリース 1.5 までは基本データ型と参照型は根本的に異なっていたため問題にならなかった。)

どのオーバーロードされたメソッドが呼び出されるかは、Java言語仕様において33ページも占めるほど複雑である。

故に書籍では、保守的な方針として同じパラメータ数の2つのオーバーロードされたメソッドを決して提供しないべきであるとしている。

これを実現するにあたっては、メソッドに別名を利用したり、コンストラクタでは static ファクトリーメソッドを利用することができる。


Kotlin で読む

Java と一緒でメソッド選択は、オーバーロードでは静的に、オーバーライドでは動的に行われる。

fun classify(a: Collection<*>): String {

return "Collection"
}

fun classify(a: List<*>): String {
return "List"
}

open class OverrideParent {
open fun getClassName() = "OverrideParent"
}

class OverrideChild : OverrideParent() {
override fun getClassName() = "OverrideChild"
}

fun main(args: Array<String>) {
val list: Collection<*> = listOf("dummy")
println(classify(list)) // Collection

val child = OverrideChild()
println(child.getClassName()) // OverrideChild
}

Try Kotlin で確認

名前付き引数が使えるため、同じパラメータ数のメソッドでも Java よりは区別は付きやすいが、わかりにくい事には違いないので Java 同様避けるようにした方が良いだろう。

またオートボクシングの例については Kotlin から Java のメソッドを呼ぶ際は Integer として扱われるので、以下の通りより直感的になる。

val strList = ArrayList<String>()

strList.add("1")
strList.add("2")
strList.remove("1")
println(strList) // 出力: [2]

val intList = ArrayList<Int>()
intList.add(1)
intList.add(2)
intList.remove(1)
println(intList) // 出力: [2]

val intList2 = ArrayList<Int>()
intList2.add(1)
intList2.add(2)
intList2.removeAt(1) // Index 指定で削除するメソッドは別名!
println(intList2) // 出力: [1]

Try Kotlin で確認

ちなみに以下のように名前付き引数でメソッドが特定できない場合はコンパイルエラーになる。

fun test(a: String, b: Int) = println("a")

fun test(b: Int, a: String) = println("b")

// このように使う
test("abc", 123) // a
test(123, "abc") // b
test(a = "abc", b = 123) // コンパイルエラー


項目42 可変長引数を注意して使用する


概要

Java にはリリース1.5より可変引数メソッドが導入された。

可変長引数を利用する際はいくつかの場合に注意する必要がある。


  • N個以上の引数が必要な場合

以下のように実行時に検査する実装をしてしまうと、コンパイル時にエラーを検出できない。

// 1個以上の引数が必要なメソッド

void hoge(String... args) {
if (args.length == 0) throw new IllegalArgumentException("Too few arguments");
// 以下省略
}
// hoge(); のように呼ぶと実行時に IllegalArgumentException が投げられる

この場合、N個の引数に加えて可変長引数を持つメソッドとすべきである。

// 1個以上の引数が必要なメソッド(改)

void hoge2(String a, String... args) {
// 省略
}
// hoge2(); はコンパイルエラー


  • 最後のパラメータとして配列を持つメソッドを修正する場合

リリース1.4では以下のコードはエラーになる。

int[] digits = { 3, 1, 4, 1, 5, 9, 2 };

Arrays.asList(digits);

これは Arrays.asListObject[] を引数として受け取るよう宣言されていたためである。

が、リリース1.5で Arrays.asList不幸にも 可変長引数を受け取れるよう変更された。

リリース 1.4(javadoc)

public static List asList(Object[] a)

リリース 1.5(javadoc)

public static <T> List<T> asList(T... a)

この変更により、今ではエラーにならず意図しない結果が得られるようになってしまった。

int[] digits = { 3, 1, 4, 1, 5, 9, 2 };

Arrays.asList(digits); // List<Integer> では無く List<int[]> が返る…

このように型検査を失う結果となるため、最後のパラメータとして配列を持つメソッドをすべて修正してはならない


  • パフォーマンスが必要な場合

可変長引数のメソッド呼び出しには、配列生成と初期化のコストが余分にかかる。

この際、大抵のメソッド呼び出しで引数が3個以下とするならば

「0〜3個までのパラメータを持つメソッド + 可変長引数のメソッド」

を用意するというテクニックが使える。

実際 EnumSet ではこの技法を使用している。

※ EnumSet はビットフィールドに対するパフォーマンスを損なわない置き換えを意図しているので、こうするのが適切


Kotlin で読む

Kotlin の可変長引数は Java と若干記述が異なり vararg 修飾子を利用する

fun printNumbers(vararg numbers: Number) {

// numbers は Array<out Number> として扱える
println(numbers.joinToString())
}

// 使用例
printNumbers(1, 2.0, 3f) // 1, 2.0, 3.0

Java との違いとして Kotlin では可変長引数に配列をそのまま渡すことはできない。

Kotlin の配列は spread 演算子で引数展開できるため、他の可変長引数を持つメソッドは以下のように呼び出す必要がある。

fun printNumbers2(vararg numbers: Number) {

println(listOf(*numbers))
}

// 使用例
printNumbers(1, 2.0, 3f) // [1, 2.0, 3.0]

Try Kotlin で確認

そのため、元々配列を受け取るメソッドを、後から可変長引数を受け取るようにAPIを変えずに変更できない。よって Arrays.asList のような不幸な変更はそもそも起こりえないと言える。

また Java では可変長引数のメソッドに null を渡す際の挙動で以下のように直感に反する場合がある。

void hoge(String... args) {

System.out.println(args.length);
}

// 使用例
main.hoge(); // 0
String a = null;
main.hoge(a); // 1
main.hoge(null); // ぬるぽ

3番目で NullPointerException が発生する理由は String[] 型として null を渡しているためである。つまり以下の違い。

main.hoge((String) null); // String... として扱われる -> String[]{null}

main.hoge((String[]) null); // String[] として扱われる -> null

Kotlin であれば配列は渡せないのでこの辺も安心!


項目43 null ではなく、空配列か空コレクションを返す


概要

配列やコレクションを返すメソッドで、要素が存在しなかった時などに null を返すコードを見かける事がある。しかし、空配列か空コレクションの代わりに null を返す理由は決して無い。

null を返さなければ、呼び出し元で null の処理する必要も無い事に加え、予め用意されたサイズ0のコレクションを返すようにすればパフォーマンスでも差は無いためである。


Kotlin で読む

Kotlin も Java 同様 null を返さないようにすべきである。

標準ライブラリに空コレクションを返すメソッドがそれぞれ用意されている。

val list: List<Int> = emptyList()

val set: Set<String> = emptySet()
val map: Map<String, Int> = emptyMap()

ちなみに引数無しの listOf 等はそもそも空コレクションを返す。

public inline fun <T> listOf(): List<T> = emptyList()

よって、パフォーマンス的には以下でも問題ない。

val list: List<Int> = listOf()

val set: Set<String> = setOf()
val map: Map<String, Int> = mapOf()

また、そもそも Kotlin の場合 nullable はしっかり型で区別されるため、自然とコレクションを null で返そうとは思わないのかもしれない。

ちなみに、この項目で言及されているのは配列やコレクションに関してであり、オブジェクトに関する話ではない。オブジェクトとして null を返したくない場合 Null オブジェクトパターン(wikipedia) もあるが、ここでは扱わない。(個人的にはバグを見逃すデメリットの方が大きいので null としてきちんと扱った方が何かと良さそうに思っている。)


項目44 すべての公開 API 要素に対してドキュメントコメントを書く


概要

Java では Javadoc 形式のドキュメンテーションコメント(/** */)を書くことが一般的であり、これによりソースコードから API ドキュメントを生成できるようになる。

API を適切に文書化するため、すべての公開されているクラス・インタフェース・コンストラクタ・メソッド・フィールド宣言の前にドキュメンテーションコメントを書かなければならない。

書籍では細かく例が挙げられているが、ここではいくつかを簡潔に抜粋する。


  • メソッドに関するドキュメントコメント

    事前条件(呼び出し時に成立しているべき条件)、事後条件(呼び出し後に成立しているべき条件)に加えて、いかなる副作用も文書化すべき(例:スレッドを開始する場合)。

    また、スレッド安全性(項目70)についても記述すべきである。


  • 各ドキュメントコメント最初の1文(概要説明)

    概要説明は、それが要約しようとしている実体の機能を述べるために完結していなければならない。

    メソッドやコンストラクタでは、それ自身が行う処理を説明している動詞句であるべきである。

    クラス・インタフェース・フィールドに対しては、それ自身によって表される事柄を説明している名詞句であるべきである。


  • ジェネリック型・enum型・アノテーションの文書化

    公開要素はすべて文書化する必要がある。よって

    ジェネリック型では すべての型パラメータを文書化する ことを忘れてはならない。

    enum型では すべての定数を文書化する ことを忘れてはならない。

    アノテーション型では すべてのメンバーを文書化する ことを忘れてはならない。



Kotlin で読む

JavaDoc に相当するものとして、 Kotlin では KDoc があり、 Dokka というツールを使ってドキュメント化する事ができる。

基本的には JavaDoc と同じように記述できるが、Kotlin 独自の概念などを表現するためにブロックタグが追加されていたり、Markdown が使えるようになったりと細かい改良が加えられている。

詳細は Dokka の README.md を参照。

ドキュメントの内容に関しては Java 同様すべての公開 API 要素に対してドキュメントコメントを書く事を意識するようにしたい。


おわり


参考資料等