クラスとメンバーのアクセス指定(public、protected、private、デフォルト)について記載しています。
記載内容
情報隠蔽、カプセル化
情報隠蔽により多くのメリットを得ることができる。
メリットの多くは、システムのコンポーネントを効果的に分離することで個別に開発、テストなどを行えることに起因する。
具体的なメリットとしては以下がある。
- 並行開発による開発速度向上
- 理解しやすさ、デバッグのしやすさによる保守負荷軽減
- コンポーネントごとのチューニングによるパフォーマンス向上
Javaのアクセス制御
クラス、インタフェース、メンバーがアクセス可能かは、宣言された場所とアクセス修飾子で決定される。
情報隠蔽には、これらの修飾子の適切な使用が不可欠となる。
指針は、各クラス、メンバーへのアクセシビリティを可能な限り狭くすることである。
クラス、インタフェースのアクセスレベル
トップレベルのクラス、インタフェースに対するアクセスレベルはpublicとパッケージプライベートしかない。
トップレベルのクラス、インタフェースをパッケージプライベートにできる場合、そうするべきである。
パッケージプライベートのクラス、インタフェースはパッケージの公開APIではなく実装の詳細の一部となり、
パッケージ外のプログラムに対する影響を心配せずに修正、削除などを行うことができる。
対してpublicとした場合は、パッケージ外のプログラムへの影響を考慮する必要がある。
もしトップレベルのパッケージプライベートなクラスを使っているクラスが1つだけであれば、
privateなネストしたクラスへの変更を検討するべきである。
ただし、このアクセスレベルの変更よりも、publicである必要が無いクラスのアクセスレベルを下げる方がはるかに重要となる。
メンバーのアクセスレベル
メンバー(フィールド、メソッド、ネストしたクラス/インタフェース)に関しては、private、パッケージプライベート、protected、publicの4種類のアクセスレベルがある。
クラスのpublicのAPIを設計した後、まずは全てのメンバーをprivateにするべきである。
同じパッケージ内の他のクラスがあるメンバーへアクセスする必要がある場合のみ、パッケージプライベートに変更するべきである。
この変更を頻繁に行っている場合、システムの設計を見直して互いにうまく分離されたクラスに再配置できないか考えるべきである。
ただし、privateとパッケージプライベートのメンバーはクラスの実装の一部であり、公開APIではないので大きな問題ではない(Serializableの場合を除く)。
publicのクラスに対してメンバーをprotectedにした場合、アクセシビリティが大幅に増加する。
protectedのメンバーはクラスの公開APIとなる。protectedの使用頻度はまれとなるべきである。
オーバーライドに対するアクセスレベル
メソッドをオーバーライドする場合、オーバーライドするサブクラスのメソッドは、スーパークラスのメソッドより高い(アクセス範囲が広い)アクセスレベルにする必要がある。
これはリスコフの置換原則に従い、スーパークラスを使用可能な場所でサブクラスを使用可能にするためである。
テストのためのアクセスレベルの緩和
自動ユニットテストを容易にするためにprivateメンバーをパッケージプライベートにすることは問題ない。
しかし、それ以上の緩和を行うべきではない。
つまり、テストを容易にするために公開APIを増やすべきではない。
通常、同一パッケージ内にユニットテストを作成すればパッケージプライベートのメンバーにアクセスできるため、それ以上の緩和は不要である。
フィールドのアクセスレベル
インスタンスフィールド
インスタンスフィールドはpublicにするべきではない。
もしpublicなインスタンスフィールドがfinalでないか、可変なオブジェクトへの参照であれば、
そのフィールドに設定する値を制限することができなくなるためである。
publicで可変なフィールド(finalでないか可変オブジェクトへの参照)をもつクラスは、一般にスレッドセーフではない。
また、フィールドが不変(finalかつ不変オブジェクトへの参照)であったとしても、publicにすることで内部データの変更に対する柔軟性がなくなる。
staticフィールド
staticフィールドもインスタンスフィールドと同等だが、例外としてpublic static finalのフィールドとして定数を公開できる。
このフィールドは、基本データ型の値か不変オブジェクトへの参照である必要がある。
長さが1以上の配列は各要素を変更可能であることに注意する必要がある。
そのため、public static finalな配列フィールドや、static finalなフィールドを返すアクセッサーを持つのは誤りである。
このような配列をクラス外のコードで変更できてしまうためである。
モジュール
Java9で導入されたモジュールシステムによって、モジュール内のパッケージを、公開/非公開に分けることができる。
そのため、公開APIとなるpublic、protectedのメンバーに対して以下に分類される。
- 公開パッケージのpublic、protectedなメンバー
- 非公開パッケージのpublic、protectedなメンバー
非公開パッケージのpublic、protectedなメンバーはモジュール内の他パッケージからはアクセス可能だが、
モジュール外からはアクセス不可になる。
ただし、モジュールのJarファイルをモジュールパスでなくクラスパスに配置するとモジュール内のパッケージはアクセス可能となる。
EffectiveJava執筆時ではモジュールが広く使われるかは不透明であり、明確になるまでは自身でモジュールを定義するのは避けるべきである。
(Effective Java執筆から時間がたっているため考察内で補足する)
考察
アクセス可能な範囲をなるべく狭める、という指針については大きな問題は無いと思います。
いくつか分かりづらい部分、執筆時からの時間経過による補足を記載します。
protectedフィールドのアクセスに関する例外
protectedフィールドのサブクラスからのアクセスについて、「例外が少しあります[JLS,6.6.2]」と記載されていますが、具体的な内容が記載されていません
これは、サブクラスSub1のprotectedフィールドを他のサブクラスSub2が参照する場合の規定と思われます。
(明確な記載が無いため予想となりますが、旧版ではこのケースの記載がありました)
以下にコード例を記載します。
package parent;
public class Parent {
protected int p;
}
package sub1;
public class Sub1 extends Parent {
/** 常にpの値の倍になることを保証する */
protected int s1;
public void setValue(int newP) {
p = newP;
s1 = p * 2; // s1は常にpの2倍となる
}
}
package sub2;
public class Sub2 extends Parent {
public void copyValue(Parent target) {
// ここでtarget.pへのアクセスでコンパイルエラーになる。
// これを許すとSub1のpとs1の関係性を破壊できてしまうため
target.p = this.p;
}
}
このような不変条件の破壊を防ぐための例外となります。
ただ、このようなケースはあまり無いと思われ、さほど気にする必要は無いかと思います。
(そもそも継承を避けるべきではありますが)
モジュールの使用について
Effective Java第3版の記載から大分経ちますが、通常の開発であればモジュールを導入する必要は無いかと思います。
例外として、不特定多数に公開するライブラリの実装している場合が考えられますが、
そのほかの開発では不要です。
また、リフレクションへの影響もあるため、Spring等のフレームワークでは適用が難しくなります。
protectedを使用するケース
誤解している開発者が多いのではないかと思いますが、不特定多数が継承可能なクラスのprotectedメンバーの公開範囲はpublicと同じだと考えるべきです。
今後の機能追加のためにフィールドをprotectedにするようなことは絶対に避けるべきです。
ただし、Java17でsealedが導入されたことにより、状況が変わりました。
sealedと組み合わせて正しくprotectedを使用すれば、堅牢かつ柔軟な設計につながる可能性がありそうです。
Effective Java第4版での記載に期待しましょう。