Edited at

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

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章 シリアライズ


第8章 プログラミング一般


項目45 ローカル変数のスコープを最小限にする


概要

コードの可読性と保守性を上げるために、ローカル変数のスコープは最小限にすべきである。

関連: 項目13 クラスとメンバーへのアクセス可能性を最小限にする

スコープを最小限にするため、以下の点に気をつける


  • ローカル変数が初めて使用される時に宣言を行う

  • ローカル変数宣言は、初期化子を含ませる

  • while ループより for ループを選ぶ

  • メソッドを小さくして焦点をはっきりさせる


Kotlin で読む

Java の try-catch 文にて例外を投げるメソッドでローカル変数を初期化し、tryブロックの外でも変数を利用する場合、以下のように宣言と初期化を同時に行えなかった。


java

Hoge hoge = null;

try {
hoge = newInstance();
} catch (FugaException e) {
System.exit(1);
}
doSomething(hoge);

Kotlin では try-catch も式になったので、以下のように書くことができる。


kotlin

val hoge = try {

newInstance()
} catch (e: Exception) {
exitProcess(1)
}
doSomething(hoge)

また Kotlin ではスコープ関数を利用する事ができる。

let, with, run, apply, also などがあるが、これがスコープを狭めるのに特に便利。


  • 例: メソッドの戻り値が null でない場合のみ、戻り値のオブジェクトが持つメソッドを実行したい場合


java

Hoge hoge = getNullOrHoge();

if (hoge != null) {
hoge.fuga();
}


kotlin

getNullOrHoge()?.let { // hoge のスコープを let のブロックスコープ内に収められる

it.fuga()
}
getNullOrHoge()?.fuga() // ※1行のみならこれも可


  • 例: Bean を初期化してメソッドの引数として使う場合


java

Hoge hoge = new Hoge();

hoge.a = "a";
hoge.b = "b";
doSomething(hoge);


kotlin

doSomething(Hoge().apply {

a = "a"
b = "b"
})


項目46 従来の for ループより for-each ループを選ぶ


概要

リリース1.5より以前は、配列やコレクションをイテレートする好ましいイディオムは以下のようなものだった。

// コレクション

for (Iterator i = c.iterator(); i.hasNext(); ) {
doSomething((Element) i.next());
}
// 配列
for (int i = 0; i < a.length; i++) {
doSomething(a[i]);
}

リリース1.5より for-each ループが導入され、これらは以下のように書けるようになった。

for (Element e : elements) {

doSomething(e);
}

さらに for-each ループは Iterable インタフェースを実装したいかなるオブジェクトに対してもイテレートが可能である。

従来の for ループより優れており、利用できる場所ではどこでも使用すべきである。

ただし、for-each ループが使用できない場合もいくつか存在する。


  • フィルタリング


    • 選択された要素だけを削除する場合



  • 変換


    • 要素のいくつか、あるいは全部を置換する必要がある場合



  • 並列イテレーション


    • 複数のコレクションを並列にイテレートする場合



※ また Java 8 からは Iterable#forEach が追加されたため、コレクションに関しては以下のようにさらに簡潔に書くこともできるようになった。

elements.forEach(e -> doSomething(e));


Kotlin で読む

Kotlin ではそもそも Java の従来型の for ループ(初期化、継続条件、増分処理の3つ組)のような構文は存在せず、for-each 型しか利用できない。

val array = arrayOf(1, 2, 3, 4, 5)

for (e in array) { println(e) }

ただし Kotlin では Iterable が大幅に強化されているので、大抵の場合 Iterable<T>.forEach を使えば問題なさそう。(Kotlin でも Array は相互互換性の為 Iterable を実装していないが、同名のメソッドを持っている為実用上問題ない)

val array = arrayOf(1, 2, 3, 4, 5)

array.forEach { println(it) }

for-each ループで出来なかったフィルタリングや変換も Iterable のメソッドで可能。

val array = arrayOf(1, 2, 3, 4, 5)

array.filter { it%2 == 1 }.map { it*10 }.forEach { println(it) }
// output: 10, 30, 50


項目47 ライブラリーを知り、ライブラリーを使う


概要

ライブラリーを使用することで、それを書いた専門家の知識と、それをあなたよりも前に使用した人々の経験を利用することになる。無駄な努力はするべきでなく、共通な事をするように思われるときは、ライブラリが存在するか調べ、利用するようにすべきである。

また Java の主要リリース毎には数多くの機能が標準ライブラリに追加されるため、それらを知っておくことは重要である。

特に、すべての Java プログラマは java.langjava.util と、ある程度の java.io の内容については知っておくべきである。


Kotlin で読む

標準ライブラリについて

Kotlin の標準ライブラリのドキュメント の内容は、ざっと目を通しておくと良いと思う。

特に kotlin.collections 、中でも Iterable についてはしっかり読んでおくととても捗る。

参考: (Qiita)Kotlin のコレクション使い方メモ

リリース情報について

Kotlin のリリース毎の変更点については公式リファレンスでもまとめられるが(例: What's New in Kotlin 1.3)、JetBrains の Kotlin Blog や、公式 Twitter (@kotlin) なんかをフォローしておくと最新の情報を追えて良さそう。


項目48 正確な答えが必要ならば、float と double を避ける


概要

金銭計算等、正確な数値が必要な場合は float と double は避けるべきである。それらは2進浮動小数点数であり、近似を行うために設計されている。

正確な数値計算を行うためには BigDecimal や、int あるいは long を使うべきである。


Kotlin で読む

Kotlin でも Java 同様、正確な値が欲しい場合は int や BigDecimal 等を使う。

Kotlin の場合は演算子オーバーロードによって BigDecimal でも通常の演算子を利用でき、可読性が良いのが嬉しい。

val pointOneDouble: Double = 0.1

val pointOneBigDecimal: BigDecimal = pointOneDouble.toBigDecimal()

println(pointOneDouble + pointOneDouble + pointOneDouble)
// output: 0.30000000000000004
println(pointOneBigDecimal + pointOneBigDecimal + pointOneBigDecimal)
// output: 0.3


項目49 ボクシングされた基本データより基本データ型を選ぶ


概要

Java は int などの基本データ型 (primitive type)と、String や List などの参照型 (reference type) から構成される2部型システムを持っており、全ての基本データ型はボクシングされた基本データ (boxed primitive) と呼ばれる対応する参照型を持っている。

またリリース1.5では自動ボクシング (autoboxing) と自動アンボクシング (auto-unboxing) が言語に追加された。

これは項目5で意図しないオートボクシングについて記述したように、基本データ型とボクシングされた型の2つの違いを不明瞭にしたが、違いを消し去ったわけではない。

どちらの型を使っているのかを意識し、注意深くどちらかを選択する事が重要である。

基本的には安全面やパフォーマンス面から基本型を利用するべきである。型パラメータとして利用する場合(例: コレクションの要素)や、リフレクションを使ってメソッドを呼び出す場合など基本型が利用できない場合、ボクシングされた型を利用する。


Kotlin で読む

項目5でも触れたが、Kotlin の数値型は通常基本データ型、必要な場合のみ参照型として扱われるため、特に自分で基本データ型を選ぶという事はない。

使い分けを意識しなくても良くなったが、内部的には Nullable 型やジェネリクスの型引数の場合にオートボクシングが行われているため、この事を頭には入れておいた方が良さそうに思う。


項目50 他の型が適切な場所では、文字列を避ける


概要

文字列はテキストを表現するために設計されており、それ以外の目的で使うことは不適切である。


  • 他の値型の代替としては貧弱


    • 外部からの入力は大抵文字列なのでやりがち

    • int、 boolean 等適切な型にすぐ変換すべき



  • 列挙型の代替としては貧弱


    • enum を使おう



  • 集合型の代替としては貧弱


    • 適切なクラスを作成すべき



  • 一意の偽造できないキー(capability)の代替としては貧弱


    • 偽造できない事をコード上で示せない




Kotlin で読む

特に Kotlin 特有の何かはないので、Java 同様に意識して文字列ではなく適切な型を使う。


項目51 文字列結合のパフォーマンスに用心する


概要

Java の String は不変である。そのため ++= を使って結合を行う場合、無駄なインスタンスが生成される。

String result = "";

for (int i = 0; i < N; i++) {
result += HOGE_STRING; // String 結合
}

頻繁に結合を行う場合は、 StringBuilder を利用すべきである。

StringBuilder b = new StringBuilder();

for (int i = 0; i < N; i++) {
b.append(HOGE_STRING);
}
String result = b.toString();

また、 StringBuffer は同期化処理を含む分パフォーマンスが低く、もはや使うべきでない。


Kotlin で読む

標準ライブラリに StringBuilder の拡張がある(StringBuilder.kt)ので、さらに簡潔に書けて良い。

val ONE = "one"

val TWO = "two"
val result = buildString { // StringBuilder をレシーバとしたラムダを引数とするトップレベル関数
append(ONE)
appendln(TWO) // 文字列+改行を追加
append(ONE, TWO) // 複数の文字列を追加
}
println(result) // output: onetwo\nonetwo

また String Template を使って可読性を上げるという選択肢も取れる。

val result = "$ONE$TWO\n$ONE$TWO"

※ 余談だが、+ 演算子を使っても大抵コンパイル時の最適化で StringBuilder を使ったものに変換される。ループ内で append したりでなければそこまで気にしなくて良いのかもしれない。


項目52 インタフェースでオブジェクトを参照する


概要

適切なインタフェース型が存在するならば、パラメータ、戻り値、変数、およびフィールドはすべてインタフェース型を使用して宣言されるべきである。オブジェクトのクラスを参照する必要がある唯一の場合は、オブジェクトを生成するときのみである。

型としてインタフェースを使用する事で、オブジェクト生成を別のクラスにするだけで簡単に実装を切り替える事ができるようになる。


オブジェクト生成の変更で他の実装に修正可能

List<Hoge> hoges = new Vector<Hoge>();

// 修正 ↓
List<Hoge> hoges = new ArrayList<Hoge>();


オブジェクト生成の変更では他の実装に修正不可能

Vector<Hoge> hoges = new Vector<Hoge>();

// 修正 ↓
Vector<Hoge> hoges = new ArrayList<Hoge>(); // コンパイルエラー

また、もしインタフェースを持っていなければ、必要な機能を提供する最も上位のクラスを利用すること。


Kotlin で読む

この項目の本質は、プログラムの意図をコードに適切に反映することの重要性であろう。

インタフェースを利用することで実装を気にしていない事を表現できるし、最も上位のクラスを利用することで下位クラスの持つ不必要な機能を使わない事を表現できる。

書籍では、 インタフェース型でフィールドを宣言することで「あなたを誠実にしてくれます」 と述べられている。

概要の例が readonly の用途であれば、Kotlin であればさらに MutableList でなく List を使用することで追加で意図を表現できる。


項目53 リフレクションよりインタフェースを選ぶ


概要

リフレクション機構である java.lang.reflect を利用すると、ロードされたクラスに関する情報へプログラムからアクセスができる。コンパイルされた時点で存在さえしないクラスでも使用できる強力な機能であるが、これには代価が伴う。


  • コンパイル時の型検査の恩恵をすべて失う

  • リフレクションを使うコードは冗長

  • パフォーマンスが悪くなる

一般に、実行時に普通のアプリケーション内で、オブジェクトはリフレクションによりアクセスされるべきではない。コンパイル時に知られていないクラスと一緒に動作しなければならないプログラムを書くのであれば、可能な限りオブジェクトのインスタンス化のためだけにリフレクションを利用し、コンパイル時に分かっているインタフェースやスーパークラスを使用してオブジェクトへアクセスすべきである。


Kotlin で読む

Kotlin においてリフレクション機能は kotlin.reflect で提供される。

このパッケージを利用するには、別途 kotlin-reflect.jar をプロジェクトに追加する必要がある。(サイズがでかいため。v1.3.20 では 2.5MB になる。)

java.lang.Class に相当するものとして KClass があり、これは以下のように取得できる。


1.コンパイル時に取得

class Person(val name: String, val age: Int)

// Java でいう クラス名.class
val kClass1 = Person::class


2.実行時に取得

val person = Person("Tom", 20)

// Java でいう オブジェクト.getClass()
val kClass2 = person.javaClass.kotlin


3.実行時に取得v1.1~

val person = Person("Tom", 20)

// v1.1 以降なら Bound Class References が使える
val kClass3 = person::class

Reflection#Bound Class References (since 1.1)

簡単な利用例

kClass1.memberProperties.forEach { print(it.name) }

// output: agename

その他色々便利な機能が用意されている。詳細はドキュメントを参照。

ただし Java 同様にリフレクションを使うには代価が伴う。可能な限り利用せず、インタフェースを使うようにする。


項目54 ネイティブメソッドを注意して使用する


概要

Java Native Interface (JNI) を利用する事で、 C や C++ などのネイティブのプログラミング言語で書かれたメソッドを呼び出すことができる。

これには歴史的に3つの主な用途があった。


  1. レジストリやファイルロックなどのプラットフォーム固有の機構へのアクセス

  2. 古いコードのライブラリへのアクセス

  3. パフォーマンスの改善

しかし、これらはJavaプラットフォームの成熟に伴い機会は減ってきている。

例えば 1.4 の java.util.prefs でレジストリに、1.6 の java.awt.SystemTray でシステムトレイにアクセス可能になった。

パフォーマンスに関しても JVM はバージョンを重ねる毎に高速になってきたため、ネイティブメソッドを使用するメリットは薄くなっている。

結論としては、ネイティブメソッドを使う前にはもう一度考え直し、本当に必要な場合のみに限り、徹底的にテストした上で使うべきである。


Kotlin で読む

Kotlin/Native の進化次第ではどうなるかわからないが…

JVM 上で動かすならば Kotlin から ネイティブコードを呼ぶ場合 Java 同様 JNI を利用することになる。

Using JNI with Kotlin

Java の native 修飾子の代わりに、Kotlin の場合 external 修飾子を利用する。

それ以外は Java 同様になる。


項目55 注意して最適化する


概要

最適化に関しては様々な格言が知られている。

(例:ドナルド・クヌースの「早すぎる最適化は諸悪の根源である」等)

速いプログラムよりも良いプログラムを書く努力をすべきである。

パフォーマンスを制限するような設計上の決定を避けるように努めるべきである。

(例:public クラスを可変とすると防御的コピーが必要になる、インタフェース型でなく実装型を利用すると後からより速い実装に差し替えられなくなる、等)

実装した結果、パフォーマンスに満足できない場合に初めて最適化を検討すべきであり、その際には前後に必ず計測を行うべきである。

何故なら Java ではコードと実際に CPU で実行されるものには「意味的ギャップ(semantic gap)」が従来のコンパイル言語よりも大きく、最適化によるパフォーマンスの向上を事前に見積もる事は非常に困難だからである。

また、プログラムの時間の80%はコードの20%で費やされている事が一般的に知られており、効果的な箇所のみ最適化を行うためでもある。


Kotlin で読む

意味的ギャップについて言えば、Kotlin は Java への変換を1段階挟んでいるようなものであるため、Java 単体よりもギャップが大きいと言えるのかもしれない。

パフォーマンス計測については Java 同様 JVisualVM 等のプロファイラを用いたり、そもそもパフォーマンスが計測可能な設計としておく事が望まれるだろう。


項目56 一般的に受け入れられている命名規約を守る


概要

Java プラットフォームには確立された命名規則があり、これは Java 言語仕様(Java Language Specification Chapter 6. Names)に含まれている。大雑把に言えば、命名規約は活字的(typographical)と文法的(grammatical)の2つに分離される。

それぞれの例をいくつか簡単に示す。

活字的命名規約


  • パッケージ名はピリオドで区切られた要素を持ち、階層的であるべき

  • クラス名・インタフェース名は1つかそれ以上の単語から構成されるべきで、単語の最初の文字は大文字であるべき

  • メソッド名・フィールド名は、クラス等と同じ規則だが最初の単語は小文字にすべき

  • 型パラメータは通常1文字で、T(任意の型)・E(コレクションの要素の型)・K(Mapのkey)・V(MapのValue)・X(例外)のどれかになる。

活字的命名規約はめったに破るべきでない。

文法的命名規約


  • クラス名は、単数名詞あるいは名詞句

  • 何らかの処理を行うメソッドは、一般に動詞あるいは動詞句

  • boolean 値を返すメソッドは、大抵 is まれに has ではじまり、その後に名詞・名詞句等が続く

  • boolean でない機能や属性を返すメソッドは、大抵は名詞・名詞句・getで始まる動詞句で命名される

  • オブジェクトの型を、別の型のオブジェクトに変換するメソッドは、大抵 toType とされる(例: toString、toArray)

文法的命名規約は柔軟で議論の的とされる。


Kotlin で読む

Kotlin では基本的に Java の命名規約に従えばよい。

Kotlin では、クラスのプロパティを宣言すると自動的にアクセサが生成されるので、命名規約的にも気にする事が減る。

ただし、クラスのプロパティが Boolean の場合、以下の例外的な動作をするので少し気をつける。


getter と setter の命名規則には例外があり、プロパティ名が is で始まっている場合は getter には接頭辞は追加されず、 setter では is が set に置き換わることになっています。

(Kotlinイン・アクション 2.2.1 プロパティ, p.31)


class Person(

val name: String,
var isMarried: Boolean // Boolean プロパティは is〇〇 と命名すれば良い
)

// こう使う
val person = Person("hoge", false)
person.isMarried = true
println(person.isMarried) // true


Javaから呼ぶ

Person person = new Person("hoge", false);

person.setMarried(true);
System.out.println(person.isMarried()); // true


おわり


参考資料等