はじめに
対象読者は以下の想定。
- Java, Kotlin をなんとなく使える
- Effective Java の内容をなんとなく知っている
勉強中なので、つっこみ大歓迎です。
概要
- 良い Kotlin コードが書きたい!
- Kotlin は "100% interoperable with Java"
- Javaの良いコード = Kotlinの良いコード?
- Javaエンジニアの必読書 Effective Java
- Kotlin Language Documentation でも度々言及されてる
- https://kotlinlang.org/docs/kotlin-docs.pdf
Kotlin を知った上で Effective Java の各項目を総復習する👆
基本方針
Effective Java 第2版(目次)を元にする。
- 全11章78項目一通り触れる(第1章「はじめに」は除く)
- 項目毎に概要をまとめる
- Kotlin で良い手段が提供されていれば紹介
- 特になければ Kotlin でのサンプルコードを載せる
章目次
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章 シリアライズ
第2章 オブジェクトの生成と消滅
項目1 コンストラクタの代わりに static ファクトリーメソッドを検討する
概要
インスタンス生成に static ファクトリーメソッドを使えないか検討すると良い。
なぜなら以下のような長所があるから。
- 名前をつけられる為可読性が良くなる
- オブジェクトを新たに生成しなくて良い場合がある
- 任意のサブタイプのオブジェクトを返せる
Kotlin で読む
Java の static メソッドは、 Kotlin において top level function や object 宣言 などに相当する。
ファクトリーメソッドである、標準ライブラリの listOf の実装を見てみる。
public fun <T> listOf(vararg elements: T): List<T>
= if (elements.size > 0) elements.asList() else emptyList()
top level function として実装されており、サイズ 0 以下の時は生成済みの空のリストを使いまわす事でオブジェクトを不要に生成しない工夫が見て取れる。
任意のサブタイプのオブジェクトを返すクラスは Kotlin ならこのように書ける。
interface Service {
val name: String
}
class HogeService : Service {
override val name = "hoge"
}
class FugaService : Service {
override val name = "fuga"
}
object Services {
fun newHogeInstance(): Service = HogeService()
fun newFugaInstance(): Service = FugaService()
}
Kotlin はインタフェースがプロパティ持てて良いですね。
項目2 数多くのコンストラクタパラメータに直面した時にはビルダーを検討する
概要
Javaでのインスタンス作成では、伝統的に テレスコーピングコンストラクタ・パターン や JavaBeansパターン が使われるが、パラメータ数が多くなると問題が出てくる。そこで、ビルダーパターンを使うか検討すると良い。
それぞれのパターンについて Java での例を挙げる。
【テレスコーピングコンストラクタ・パターン】
public class Hoge {
private int a, b, c;
Hoge(int a) {
this(a, 222, 333);
}
Hoge(int a, int b) {
this(a, b, 333);
}
Hoge(int a, int b, int c) {
this.a = a; this.b = b; this.c = c;
}
// getter 省略
}
- コードが冗長
- コンストラクタの意味が分かり辛い
【JavaBeans パターン】
public class Hoge {
private int a;
private int b = 222;
private int c = 333;
public setA(int a) { this.a = a; }
public setB(int b) { this.b = b; }
public setC(int c) { this.c = c; }
// getter 省略
}
- 生成途中で不整合な状態になる場合がある
- イミュータブル(不変)である可能性を排除してしまう
【ビルダーパターン】
public class Hoge {
private int a, b, c;
public static class Builder {
private int a;
private int b = 222;
private int c = 333;
public Builder(int a) {
this.a = a;
}
public Builder B(int b) {
this.b = b; return this;
}
public Builder C(int c) {
this.c = c; return this;
}
public Hoge build() {
return new Hoge(this);
}
}
private Hoge(Builder builder) {
a = builder.a;
b = builder.b;
c = builder.c;
}
// getter 省略
}
// こう使う
Hoge hoge = new Hoge.Builder(1).B(2).build();
hoge.getB() // 2
hoge.getC() // 333
ビルダーパターンにより
- 可読性の向上
- 中間状態の排除
のような利点が得られるが
- コードが冗長
- ビルダー生成にコストがかかる
のような欠点も持っている。
Java では、これらのパターンを場合によって使い分けていた。
Kotlin で読む
Kotlin の場合、
- デフォルト引数
- テレスコーピングパターンの冗長性を排除
- 名前付き変数
- 可読性向上!
以上の構文を用いて、簡潔に可読性の高いインスタンス生成が可能になっている。
class Hoge(
val a: Int,
val b: Int = 222,
val c: Int = 333
)
// 例1: Hoge(1, 2, 3)
// 例2: Hoge(1)
// 例3: Hoge(a = 1, b = 22)
Java から呼び出す場合も @JvmOverloads
を利用することで、コードの冗長性を排除しつつテレスコーピングパターンを実現可能。
class Hoge @JvmOverloads constructor(
val a: Int,
val b: Int = 222,
val c: Int = 333
)
// java からはこう呼べる
Hoge h1 = new Hoge(1);
Hoge h2 = new Hoge(11, 22);
Hoge h3 = new Hoge(111, 222, 333);
また、既存のJavaBeans パターンのクラスに対しても apply
を用いることで、Builder パターンのように利用することができる。
val hoge = Hoge().apply {
a = 1
b = 2
c = 3
}
Kotlin では、旧来の Builder パターンのクラスを自分で書く事は無さそうに思う。
項目3 private のコンストラクタか enum 型でシングルトン特性を強制する
概要
シングルトン:一度しかインスタンスが作成されないクラス
Java では言語機能としてシングルトンが提供されている訳ではないので、工夫して実装する事が求められる。
書籍では、単一要素の enum 型を利用する事が最善の実装であると結論付けている。
Kotlin で読む
Kotlin では object を使おう。
object Singleton {
fun hello() {
println("hello")
}
}
// こう使う
Singleton.hello()
// java から呼ぶ場合
Singleton.INSTANCE.hello();
(@JvmStatic
を付ければ INSTANCE
は省略可能)
Calling Java code from Kotlin#Static Methods
項目4 private のコンストラクタでインスタンス化不可能を強制する
概要
時折、ユーティリティクラスのような static なメソッドとフィールドのみのクラスを作りたくなる事がある。
このようなクラスはインスタンス化する必要はないが、Java ではコンストラクタを明示しない場合デフォルトコンストラクタが作成されインスタンス化可能になってしまう。
private コンストラクタを明示的に書き、インスタンス不可能を強制すべし。
Kotlin で読む
object 宣言 でシングルトンにするか
top level function を使おう。
object UtilityClass {
fun hoge() = "hoge"
}
fun fugaUtilityFunc() = "fuga"
// こう使う
UtilityClass.hoge()
fugaUtilityFunc()
Kotlinの関数はクラスに属する必要はない
また、拡張関数を用いて特定のクラスのインスタンスに紐づくユーティリティを表現するのも良さそう。
fun String.print() = print(this)
// こう使う
"hoge".print()
項目5 不必要なオブジェクトの生成を避ける
概要
オブジェクト生成にはコストがかかるので、可能であれば同じオブジェクトを使い回すべき。
大きく以下の点を意識する。
- 不変オブジェクトの再利用
- 変更されない可変オブジェクトの再利用
- (意図しない)オートボクシング
Kotlin で読む
【不変オブジェクトの再利用】
項目 1 コンストラクタの代わりに static ファクトリーメソッドを検討する
で行ったように、不変オブジェクトを再利用する事ができる。
public fun <T> listOf(vararg elements: T): List<T>
= if (elements.size > 0) elements.asList() else emptyList()
【変更されない可変オブジェクトの再利用】
メソッド呼び出し毎に可変オブジェクトを利用する場合、再利用できるか検討する。
- 悪い例
メソッド呼び出し毎にオブジェクトが生成される
/** date が2000年以降であるか判定する */
fun isAfterMillennium(date: Date): Boolean {
val gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")).apply {
set(2000, Calendar.JANUARY, 1, 0, 0, 0)
}
return date > gmtCal.time
}
- 良い例
private val gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")).apply {
set(2000, Calendar.JANUARY, 1, 0, 0, 0)
}
/** date が2000年以降であるか判定する */
fun isAfterMillennium2(date: Date): Boolean {
return date > gmtCal.time
}
【(意図しない)オートボクシング】
Kotlin で数値型は基本的に原型、必要な場合のみ参照型として扱われる為、意図しないオートボクシングによる不要なオブジェクトの生成は行われない!
- Java の場合
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i // ここでオートボクシングが発生!
}
System.out.println(sum);
- Kotlin の場合
var sum: Long = 0L
for (i in 0..Integer.MAX_VALUE.toLong()) {
sum += i // primitive 型同士
}
println(sum)
ところで具体的には
- Nullable 型
- ジェネリクスの型引数
等のように、原型が利用できない場合にのみ参照型として扱われる。
そのため以下のようにすると Kotlin でもボクシングを起こす事が可能。
var sum: Long? = 0L
for (i in 0..Integer.MAX_VALUE.toLong()) {
sum = sum?.plus(i)
}
ちなみに Try Kotlin 上でボクシングしない場合と比較してみた所、こちらが5倍ほど遅かった。
項目6 廃れたオブジェクト参照を取り除く
概要
メモリリークを回避するため、廃れたオブジェクト参照を取り除く事で GC が適切に働くようにすべき。
- 不要な参照に null を設定する
- 弱参照を利用する
等を意識する必要がある。
Kotlin で読む
【不要な参照に null を設定する】
Kotlin でも Java 同様メモリリークは起こる。独自のメモリ管理機構を作る場合は要注意。
class StringStack {
val elements = Array<String?>(1024) { null }
var size = 0
fun push(e: String?) {
elements[size++] = e
}
fun pop() = elements[--size]
}
例えばこれだと、 pop()
してもメモリは開放されない。
不要な参照に null を設定する事で回避できる。
fun pop(): String? {
val e = elements[--size]
elements[size] = null
return e
}
【弱参照を利用する】
弱参照 is 何?
-> 参照先のオブジェクトをGCから守れない参照
Java では java.lang.ref
パッケージで実装されている。
Kotlin で WeakReference
を使った例 ↓
class HeavyObject
var strongRef: HeavyObject? = HeavyObject()
val weakRef = WeakReference<HeavyObject>(strongRef)
fun printWeakRef() = println(weakRef.get())
fun main(args: Array<String>) {
printWeakRef()
strongRef = null // 弱参照のみ存在している状態にする
printWeakRef()
System.gc() // 弱参照のみなのでGCされる
printWeakRef() // ここで null
}
書籍では、コールバックやリスナー等で WeakHashMap
を利用することを推奨している。
項目7 ファイナライザを避ける
概要
- ファイナライザは基本的に使うべきでない
- パフォーマンス低下、移植性の問題等の為
- 代わりに明示的終了メソッドを提供する
- 例:
InputStream.close()
書籍では、ファイナライザを利用する場合の注意点等も述べられている。
Kotlin で読む
Java では Object.finalize()
をオーバーライドする事で実装できた。
が、kotlin.Any
は finalize()
を持っていない。
故に、単に override
修飾子なしに宣言するだけで良い。
Calling Java code from Kotlin#finalize
class C {
protected fun finalize() {
// finalization logic
}
}
一応、明示的終了メソッドが呼ばれなかった際のセーフティなどには使える。
が、繰り返しだが基本的に finalize()
は使う必要ない。
おわり
参考資料等
- Kotlin Reference
- Kotlin Language Documentation
- Effective Java 第2版
- Kotlin イン・アクション
- (Blog)The King's Museum / Effective Java
- [(Qiita)弱い参照とな] (https://qiita.com/yyyske/items/daa5c844647604e27e4f)
- (Slide)How "Effective Java" influenced Kotlin