章目次
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章 シリアライズ
第4章 クラスとインタフェース
項目13 クラスとメンバーへのアクセス可能性を最小限にする
概要
モジュール設計において、内部データと実装の詳細は、最大限に隠蔽(カプセル化)するべきである。
カプセル化により、システムを効果的に分離する事ができる。これは、以下の理由において有用である。
- 並行して開発可能になる
- テストが容易になる
- 効果的なパフォーマンス・チューニングを可能とする
- 大規模なシステム構築のリスクを低減させる
- たとえシステムが成功しなくても、個々のモジュールは成功するかもしれない
各クラスやメンバーはできる限りアクセスできないようにすべき
Kotlin で読む
Kotlin の 可視性修飾子(Visibility Modifiers)は全部で4種類。
private
protected
internal
public
詳細はドキュメントを参照。基本的には Java と一緒だが、同一モジュール内でのアクセスを許可する internal
が新しく追加された。Kotlin においてモジュールとは、一緒にコンパイルされる Kotlin ファイルのセットの事で、つまり IntelliJ のモジュールだったり、Maven のプロジェクトだったりのこと。
クラスとメンバーへのアクセス可能性を最小限にするため Kotlin が何か特殊な構文を提供する訳ではないので、開発者自身がこの項目を常に意識してクラス設計を行う必要がある。
また書籍では、配列を定数として public で公開するのはほとんど常に誤りであると注意している。これは、配列が不変でない事が原因。
object HogeUtility {
val VALUES = arrayOf(1, 2, 3)
}
HogeUtility.VALUES[0] = 111 // 書き換え可能
不変な List を使えば、この問題は発生しない。
object HogeUtility {
val VALUES = listOf(1, 2, 3)
}
// HogeUtility.VALUES[0] = 111 書き換え不可
項目14 public のクラスでは、public のフィールドではなく、アクセッサーメソッドを使う
概要
public なクラスにおいて public なフィールドを用いた場合、カプセル化の恩恵を得ることはできない。故に、private なフィールドと public のアクセッサーメソッドを提供するべきである。
これにより、以下が可能となる。
- 外部への表現形式(API)を変更する事なく、内部の形式を変更する
- 不変式を強制する
- フィールドが変更された際に、補助処理を行う
Kotlin で読む
Kotlin では、すべてがプロパティでありフィールドは宣言できない。よって、自然にカプセル化を行う事ができる。
APIを変更せずに、内部の表現形式を変更する場合は、カスタムゲッター/セッターを利用する事ができる。
class Before (val name: String)
class After (val firstName: String, val lastName: String) {
val name: String
get() = "$firstName $lastName"
}
// こう使える
println(Before("John").name)
println(After("John", "Smith").name)
項目15 可変性を最小限にする
概要
可変にする正当な理由が無い限り、クラスは不変であるべきである。
不変オブジェクトは、生成された時点の状態を常に保持するという、シンプルさを持つ。この特性により、処理の理解が容易になる。また、不変オブジェクトは本質的にスレッドセーフであるため、制限なく共有可能という利点を持つ。
不変クラスを作るためには、以下5つの規則に従う。
- オブジェクトの状態を変更するためのいかなるメソッドも提供しない
- クラスが拡張できないことを保証する
- すべてのフィールドを final にする
- すべてのフィールドを private にする
- 可変コンポーネントに対する独占的アクセスを保証する
Kotlin で読む
Kotlin には val
があるが、あくまでも再代入禁止(final)なだけであり、不変クラスのためには直接利用できない。Kotlin のコレクションにも mutable な物と immutable な物があるのもそのため。
不変クラスを Kotlin で作るには、あくまでも書籍の5つの規則に従う必要がある。
なお、mutable な要素のない val のみで宣言されたプロパティを持つ data class は、5つの規則を満たす。
data class Complex (val re: Double, val im: Double) {
fun add(c: Complex) = Complex(re + c.re, im + c.im)
fun sub(c: Complex) = Complex(re - c.re, im - c.im)
fun mul(c: Complex) = Complex(re * c.re - im * c.im, re * c.im + im * c.re)
fun div(c: Complex): Complex {
val tmp = c.re * c.re + c.im * c.im
return Complex((re * c.re + im * c.im) / tmp, (im * c.re - re * c.im) / tmp)
}
}
項目16 継承よりコンポジションを選ぶ
概要
継承はカプセル化を破る。スーパークラスの変更は、サブクラスにも影響を及ぼす。
継承は、サブクラスが本当にスーパークラスのサブタイプである(is-a 関係が存在する)場合だけ適切である。継承の代わりにコンポジションを用いる事で、スーパークラスの実装に依存しない設計を行うべきである。
Kotlin で読む
継承の問題点を避けながらコードの再利用をしたい場合、既存クラスを拡張する代わりに、既存クラスのインスタンスを参照する private フィールドを持たせる設計ができる。これをコンポジション(composition)という。
また、そのクラスは保持した既存クラスのメソッドを呼び出すだけの転送メソッドを持たせる、転送(forwarding)という設計ができる。
技術的には正確に言うと違うらしいが、大抵このコンポジションと転送の組み合わせの事は委譲(delegation)と呼ばれている。Kotlin では委譲の為の構文が用意されており、これを用いる事で簡単にこの項目に取り組める。
Delegation - Kotlin Programming language
class InstrumentedSet<E>(private val s: MutableSet<E>) : MutableSet<E> by s {
var addCount: Int = 0
private set
override fun add(element: E): Boolean {
addCount++
return s.add(element)
}
override fun addAll(elements: Collection<E>): Boolean {
addCount += elements.size
return s.addAll(elements)
}
}
// こう使える
val s = InstrumentedSet(TreeSet<Int>())
s.add(1)
s.addAll(listOf(2, 3))
println(s.addCount) // 3
println(s.contains(4)) // false: 委譲先の実装が呼ばれる
項目17 継承のために設計および文書化する、でなければ継承を禁止する
概要
継承を意図するクラスを書く場合、以下の制限が課される
- 自己利用(self-use)を文書化すること
- サブクラスを書くことでテストをすること
- コンストラクタは、オーバーライド可能なメソッドを呼び出してはいけないこと
- もし Cloneable や Serializable を継承する場合、発生する特殊な問題に対応すること
これら制限を回避する最善の解決策は、サブクラス化を禁止することである。
Kotlin で読む
Kotlin では Effective Java の教えを守った結果、デフォルトでクラスは final である。もし継承させたい場合のみ open
を指定し継承可能にすることができる。
The open annotation on a class is the opposite of Java's final: it allows others to inherit from this class. By default, all
classes in Kotlin are final, which corresponds to Effective Java, 3rd Edition, Item 19: Design and document for inheritance
or else prohibit it.
Classes and Inheritance - Kotlin Programming Language
項目18 抽象クラスよりインタフェースを選ぶ
概要
既存のクラスを、新たなインタフェースを実装するように変更することは容易である。しかし、Java では単一継承のみを許可しているため、抽象クラスを用いるには制約が多い。
また、インタフェースはミックスイン(mixin)を定義するための理想的なツールとなる。
ただし、インタフェースは実装を持てないため、インタフェースを実装するのは少し大変である。そこで、抽象クラスを骨格実装(skeletal implementation)としてインタフェースごとに外部に公開することで両者の利点を得る方法もある。
※ Java8 からはインタフェースも実装を持てるようになった
Kotlin で読む
Kotlin の interface
は Java8 のものとよく似ており、実装も持つことができる。
ただし、Kotlin 1.2 以前は相互運用性から JVM1.6 をターゲットとしていないため、kotlin の interface のデフォルト実装は Java8 のデフォルト実装に対応しない。
Kotlin 1.2 からは、@JvmDefault
アノテーションをつけ、コンパイルオプションを指定する事で、きちんと Java8 のデフォルト実装としてバイトコードを生成することができる。
interface Foo {
@JvmDefault
fun foo(): Int = 0
}
項目19 型を定義するためだけにインタフェースを使用する
概要
インタフェースは、クラスが何が出来るかを述べるためのみに使用されるべきである。
書籍では、良くない例として定数インタフェースが挙げられている。
public interface PhysicalConstants {
// アヴォガドロ定数 (1/mol)
static final double AVOGADROS_NUMBER = 6.02214199e23;
// ボルツマン定数 (J/K)
static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
// 電子の質量 (kg)
static final double ELECTRON_MASS = 9.10938188e-31;
}
この定数インタフェースを実装することで、修飾子なしに特定の定数を使えるようにする訳であるが、これだと実装の詳細を外部に漏らすことになってしまい良くない。
public class Hoge implements PhysicalConstants{
public static void main(String[] args) {
System.out.println("avo: " + AVOGADROS_NUMBER); // 修飾子無しでアクセスできるが…
}
}
代わりの選択肢は、以下のようになる。
- そのクラスやインタフェースに紐づく
→ そのクラス・インタフェースに定数を追加する - 列挙型とみなされるのが最善
→ enum 型を使う - それ以外
→ インスタンス化不可能なユーティリティクラスを使う
Kotlin で読む
Kotlin でも変わらず、インタフェースの使い方は開発者自身で気をつける必要がある。型の定義以外に使わないように心がけよう。
例に挙げられた定数に関していえば、 Kotlin ではコンパイル時定数として const 修飾子を用意しているので、これを使うのが正しそう。ただし、以下の条件を満たす必要がある。
- トップレベルまたは object のメンバ
- String 型の値またはプリミティブ型で初期化される
- カスタムゲッターが無い
const val AVOGADROS_NUMBER = 6.02214199e23
object Util {
const val BOLTZMANN_CONSTANT = 1.3806503e-23
}
class Hoge {
fun avo() = println(AVOGADROS_NUMBER)
fun bol() = println(Util.BOLTZMANN_CONSTANT)
}
Java からは以下のように呼べる。
System.out.println(Util.BOLTZMANN_CONSTANT);
System.out.println(Item19Kt.AVOGADROS_NUMBER); // トップレベル定数は、"ファイル名kt"という名のクラスの static final なメンバとなる
項目20 タグ付クラスよりクラス階層を選ぶ
概要
インスタンスが2つ以上の特性を持っている場合、その特性を示すタグフィールドを持たせるのは良くない。そのようなクラスは、冗長で、誤りやすく、非効率である。
代わりに、クラス階層を利用するべきである。
Kotlin で読む
Java 同様、タグ付クラスよりクラス階層を選ぼう。
- 悪い例
class Figure {
enum class Shape { CIRCLE, RECTANGLE }
private var shape: Shape = Shape.CIRCLE // タグフィールド
private var radius: Double = 0.0 // CIRCLE の為のフィールド
private var length: Double = 0.0 // RECTANGLE の為のフィールド
private var width: Double = 0.0 // RECTANGLE の為のフィールド
/** CIRCLE の為のコンストラクタ */
constructor(radius: Double) {
shape = Shape.CIRCLE
this.radius = radius
}
/** RECTANGLE の為のコンストラクタ */
constructor(length: Double, width: Double) {
shape = Shape.RECTANGLE
this.length = length
this.width = width
}
fun area() = when (shape) {
Shape.CIRCLE -> Math.PI * (radius * radius)
Shape.RECTANGLE -> length * width
}
}
- 良い例
abstract class Figure {
abstract fun area(): Double
}
class Circle(val radius: Double) : Figure() {
override fun area() = Math.PI * (radius * radius)
}
class Rectangle(val length: Double, val width: Double): Figure() {
override fun area() = length * width
}
Kotlin だと特にすっきりして見える。
(ただ個人的には、この例であれば 項目18 抽象クラスよりインタフェースを選ぶ に従ってインタフェースの方が良い気がするが…)
項目21 戦略を表現するために関数オブジェクトを使用する
概要
言語によっては、関数ポインタ・委譲・ラムダ式等の機構により、特定の関数を保存し、別の関数がそれを呼び出す事ができる。Java ではこの機構を持っていなかったため、メソッドを1つだけ持つクラスを用意し、そのクラスへの参照を実質的に関数ポインタと見なす事ができた。項目名の「関数オブジェクト」とは、そのようなクラスのインスタンスの事である。
書籍では戦略インタフェース(単一のメソッドのみ持つインタフェース)を定義し、無名クラス等でこれを実装する事を推奨している。
…が、Java 8 によりラムダ式が使えるようになったため、この項目は実質役目を終えたと言えそう。
実際、この項目は、Effective Java 第3版では削除されたらしい。
かわりに、「第3版 項目42 匿名クラスの代わりにラムダを使用する」に吸収されたとの事。
Kotlin で読む
第3版にもある通り、ラムダを使用すると良い。
SAM 変換によって、Java の関数オブジェクトを要求する関数へラムダ式を渡す事が可能。
val arrayList = arrayListOf("1ichi", "2ni", "3san")
Collections.sort(arrayList, { a, b -> a.length - b.length }) // 長さ順
println(arrayList) // [2ni, 3san, 1ichi]
ただし Kotlin のインターフェイスには SAM 変換が適用されないので注意。
これは Kotlin には適切な関数型があるので、そっちを使うべきとの考えによるもの。
Also note that this feature works only for Java interop; since Kotlin has proper function types, automatic conversion of functions into implementations of Kotlin interfaces is unnecessary and therefore unsupported.
Calling Java code from Kotlin#SAM Conversions
追記
Kotlin ではコレクションに便利なメソッドが多く用意されている。Kotlin の標準ライブラリを使うと、リストのソートは関数参照を使って次のように宣言的にスマートに書ける!
// Mutable なリストの場合
val list = mutableListOf("1ichi", "2ni", "3san")
list.sortBy(String::length)
// Immutable なリストの場合
val list = listOf("1ichi", "2ni", "3san")
val sorted = list.sortedBy(String::length)
項目22 非 static のメンバークラスより static のメンバークラスを選ぶ
概要
ネストしたクラス(他のクラス内に定義されたクラス)は、Java において 4 種類ある。
- static のメンバークラス
- 非 static のメンバークラス
- 無名クラス
- ローカルクラス
static のメンバークラス以外は、内部クラス(inner class)として知られている。
非 static のメンバークラスのインスタンスは、エンクロージングインスタンス(それを含んでいるクラスのインスタンス)への参照を持つ。そのため、インスタンス生成のコストが余分にかかり GC の妨げにもなる。よって、エンクロージングインスタンスへアクセスする必要がないのであれば、常に static をつけ static のメンバークラスとすべきである。
Kotlin で読む
Kotlin で修飾なしにネストしたクラスを宣言した場合、外部への参照を持たない。つまり基本は static のメンバークラスである。
外部クラスのメンバにアクセスする必要がある場合のみ、inner 修飾子をつける事でアクセス可能とする。
class Outer1 {
private val bar: Int = 1
class Nested {
fun foo() = 2 // Outer1.bar にはアクセス不可
}
}
class Outer2 {
private val bar: Int = 1
inner class Nested {
fun foo() = bar // 外部クラスへアクセスする
}
}
// こう使う
val o1 = Outer1.Nested().foo()
val o2 = Outer2().Nested().foo()
おわり
- 前: Effective Java を Kotlin で読む(2):第3章 すべてのオブジェクトに共通のメソッド
- 次: Effective Java を Kotlin で読む(4):第5章 ジェネリックス
参考資料等
- Kotlin Reference
- Kotlin Language Documentation
- Effective Java 第2版
- Kotlin イン・アクション
- [(Blog)Composition over inheritance in Kotlin way] (https://proandroiddev.com/composition-over-inheritance-in-kotlin-way-fe341159bf1c)
- (Blog)KotlinのClass Delegationについて
- (Qiita)Kotlinとlambda式とインターフェースとSAM変換
- (Qiita)Kotlin のコレクション使い方メモ
- (Qiita)Kotlinの関数参照 #Kotlin_Sansan