Javaでは全てのクラスやインタフェースはObjectクラスのサブタイプになります。スーパークラスを指定しない場合は暗黙的にObjectクラスがスーパークラスになります。
また、Javaではクラス自身もClassクラスのオブジェクトとなっていて、オブジェクトが属するクラスの情報を調べたり、仮想マシンを起動したまま新たなクラスをロードしてインスタンスを生成するようなこともできます。(リフレクション)
この章では クラス全般の内容についてまとめています。
クラス定義とインスタンス化
クラス定義
クラスはキーワード「class」を使って定義します。クラスはフィールドやメソッド、メンバクラスなどを持つことができます。1つのソースファイルにはpublicなトップレベルのクラスは1つしか定義することができません。また、publicなクラスとソースファイルの名前(拡張子を除いた名前)が一致している必要があります。
1つのソースファイルにpublicでないトップレベルのクラスは複数定義することはできます。しかし、1つのソースファイルに複数のトップレベルのクラスを定義すると、誤って複数のファイルに同じ名前のクラスを定義してしまう可能性があります。その場合、コンパイラによっては コンパイルエラーになったり、コンパイルエラーにはならないけれども 実行時にどちらのクラスが使われるか不定になったりします。後者の場合 開発者を悩ませる(原因を特定しづらい)バグの原因になり得ます。1つのソースファイルに複数のトップレベルのクラスを定義することはできますが、1つのソースファイルの中にはトップレベルクラスは1つだけにするのが無難です。
クラス定義の例は次のようになります。
package com.example; // パッケージ宣言
// クラス宣言
public class SomeClass
{
// フィールド
private int id;
private String name;
// コンストラクタ
public SomeClass(int id, String name) {
this.id = id;
this.name = name;
}
// メソッド
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
クラスの構成要素をメンバと呼び、メンバにはフィールド・メソッド・コンストラクタ・メンバクラスがあります。フィールドにはインスタンスフィールド・クラスフィールドがあり、メソッドにはインスタンスメソッド・クラスメソッドがあります。メンバクラスにはstaticなメンバクラス・非staticなメンバクラスがあります。
インスタンス化
クラスのインスタンスはnew演算子を使って生成します。
SomeClass c = new SomeClass(1, "foo");
特殊な別の方法として、リフレクションにより ConstrucorクラスのnewInstance()メソッドによってインスタンスを生成することもできます。
パッケージとモジュール
パッケージ
クラス名の衝突を避けるためにパッケージで名前空間を分けることができます。クラス定義の冒頭のpackageキーワードで所属するパッケージを指定します。クラスはいずれかのパッケージに所属する必要があり、パッケージを指定しない場合はデフォルトパッケージになります。
外部に公開するパッケージの場合、パッケージ名の一意性を確保するために 組織のドメインをTLD(トップレベルドメイン)から順番に並べた形で始まる名前にすることが推奨されています。(例:jp.co.somecompany.somepackage)
パッケージはアクセス制御の単位としても用いられ、パッケージ外部からのアクセスを制限することもできます。パッケージ名はピリオド区切りの階層構造になりますが、名前の階層構造とアクセス制御には何の関係も無いことに注意が必要です。例えばaaa.bbbパッケージとaaa.bbb.cccパッケージがあるとすると、aaa.bbb.cccパッケージはaaa.bbbパッケージのサブパッケージではありますが、aaa.bbb.cccのパッケージのクラスからaaa.bbbパッケージのパッケージprivateな要素にアクセスすることはできません。(逆も同様です。)
モジュール(Java SE9~)
Javaアプリケーションの多様化や Javaの仕様そのものの巨大化によって、従来のパッケージの仕組みだけでは クラスライブラリの適切な構造化や管理が難しくなってきました。これに対応する形で 複数パッケージを束ねるモジュールという枠組みが追加されました。モジュールによって モジュール間の依存関係を明確にしたり モジュールの公開範囲を設定することができます。
java.lang、java.utilといった根幹部分のクラスライブラリはjava.baseモジュールに所属しています。モジュールが導入されてから Java APIドキュメントもモジュール単位に整理されました。
クラスのインポート
他のパッケージのクラスを利用する際には importでクラスをインポートします。クラスを個別にインポートする場合はパッケージ名も含めたクラス名(クラスの完全修飾名)を指定します。パッケージのクラスをまとめてインポートする場合はパッケージ名.*の形で指定します。
尚、java.langパッケージのクラスは自動的にインポートされるため、明示的にインポートする必要はありません。
import java.io.File; // java.io.Fileクラスのみをインポート
import java.net.*; // java.netパッケージの全クラスをインポート
クラスをインポートしなくても パッケージ名を含む完全修飾名でクラスを参照することも可能ですが、ソースコードが無駄に長くなるため、通常はクラスをインポートしてクラス名だけで参照するようにします。
staticインポート(J2SE 5.0~)
staticインポート機能を使うと クラスメンバ(クラスフィールド、クラス定数、クラスメソッド)をクラス名の記述なしに利用することができます。staticインポートするには「import static」でメンバを指定します。
import java.lang.Math;
…
int result = Math.max(val1, val2);
import static java.lang.Math.max;
…
int result = max(val1, val2); // クラス名の記述が不要
フィールド
フィールドの基本的な説明は割愛し、特筆する部分のみを取り上げています。
final修飾子
クラスフィールドやインスタンスフィールドを不変にしたり、クラスフィールドを定数にする場合にfinal修飾子を用います。尚、不変クラスにするには 全てのフィールドを不変にする必要があります。(他にも条件があります。詳細は「マルチスレッド」の章の「不変クラスの条件」を参照してください。)一般的には不変クラスは可変クラスよりも設計・実装・使用が容易と言われます。関数型のプログラミング言語では不変性が重視されます。
クラスフィールドやインスタンスフィールドがオブジェクト参照の場合、finalを付けても参照先のオブジェクトの状態の変更を禁止することはできないことに注意が必要です。
また、final修飾子はスレッド安全性を実現するために重要な役割を果たします。
finalフィールドの初期化
クラスフィールドやインスタンスフィールドにfinalを付けて不変とする場合、クラスやインスタンスの初期化が終わるまでに そのフィールドが初期化されていないとコンパイルエラーになります。クラスフィールドの場合とインスタンスフィールドの場合をそれぞれ見てみます。
finalが付いたクラスフィールドの初期化
finalを付けたクラスフィールドは宣言時に初期値を指定するか、宣言時に初期値を指定しない場合は staticイニシャライザの中で初期化を行います。いずれでも初期化を行わない場合はコンパイルエラーになります。
public class DeclarationExample {
public static final int STATIC_FINAL_CONSTANT_A = 123; // ①初期値を与える
public static final int STATIC_FINAL_CONSTANT_B; // ②初期値を与えない
static {
// 初期値を与えない②の場合は static イニシャライザで初期化
STATIC_FINAL_CONSTANT_B = STATIC_FINAL_CONSTANT_A * 2;
}
}
finalが付いたインスタンスフィールドの初期化
インスタンスフィールドは宣言時に初期値を指定するか、宣言時に初期値を指定しない場合は コンストラクタかインスタンスイニシャライザの中で初期化を行います。いずれでも初期化を行わない場合はコンパイルエラーになります。
public class DeclarationExample {
private final int INSTANCE_FINAL_CONSTANT_A = 123; // ①初期値を与える
private final int INSTANCE_FINAL_CONSTANT_B; // ②初期値を与えない
private final int INSTANCE_FINAL_CONSTANT_C; // ③初期値を与えない
// private final int INSTANCE_FINAL_CONSTANT_D; // ④初期値を与えない
public DeclarationExample() {
// 初期値を与えない場合はコンストラクタで初期化②
INSTANCE_FINAL_CONSTANT_B = INSTANCE_FINAL_CONSTANT_A * 2;
}
{
// またはインスタンスイニシャライザで初期化③
INSTANCE_FINAL_CONSTANT_C = INSTANCE_FINAL_CONSTANT_A * 3;
}
// public void setConstant() {
// // メソッドで初期化をしようとしてもコンパイルエラーとなる④
// INSTANCE_FINAL_CONSTANT_D = 10;
// }
}
finalの付いたフィールドが定数でない場合は 循環しないように注意が必要
finalの付いたフィールドが定数(値や式)でない場合は、初期化が循環しないように注意が必要です。定数でない場合とは コンストラクタやメソッドを呼び出す場合になります。言葉では説明するよりも、具体的なコード例を見てもらった方が早いと思います。
public class InitializationOrderExample {
private final int numOfBytes; // 文字列に必要なバイト数
private final String str; // 文字列
private static final InitializationOrderExample obj = new InitializationOrderExample("sample");
public static final int bytesPerChar = getBytesPerChar(); // 1文字に必要なバイト数。固定とする。
InitializationOrderExample(String str) {
this.numOfBytes = str.length() * bytesPerChar;
this.str = str;
}
private static int getBytesPerChar() {
return 2;
}
public static void main(String[] args) {
System.out.println(obj.numOfBytes + "," + InitializationOrderExample.bytesPerChar); // 0, 2
}
}
上のコードでは、numOfBytesに 文字数(6)×2の12を設定したいのですが、実際には0になります。これは クラスフィールド objの初期化が bytesPerChar の初期化の前に行われるため、objの初期化であるコンストラクタの処理では bytesPerCharの値がデフォルト値(0)であるためです。
bytesPerCharの初期化がメソッド呼び出しを伴わず、定数値や定数式であれば このような問題は起こりません。また、初期化は記述された順番に行われるため、次のように bytesPerCharの初期化の文を objの初期化の文より前に書けば この問題を解決することはできます。
public static final int bytesPerChar = getBytesPerChar(); // 1文字に必要なバイト数。固定とする。
private static final InitializationOrderExample obj = new InitializationOrderExample("sample");
マルチスレッドにおける役割
フィールドに対するfinal修飾子が持つ「初期化後に値を変更することができない」という働きは、マルチスレッドにおいて 不変オブジェクトを生成したり 初期化安全を実現するために とても重要な役割を果たします。詳しくは「マルチスレッド」の章で説明します。
メソッド
メソッドの基本的な説明は割愛し、特筆する部分のみを取り上げています。
メソッドの設計とドキュメント化
メソッドシグニチャの設計
Effective Javaでは メソッドシグニチャの設計について 示唆に富んだ項目が挙げられています。その中のいくつかをかいつまんでまとめます。
- メソッド名は 標準的な命名規約に従い、長くなり過ぎないようにします。
メソッド名の標準的な命名規約については「変数とデータ型」の章の「変数名」を参照してください。 - 引数の個数が多くなり過ぎないようにします。目安としては4個以下。
特に同一の型の引数が続くと、順番間違いによるバグがコンパイル時に検出できず実行時に間違いに気付くことになります。引数が多くなった場合の対処方法としては3つの技法が挙げられます。- メソッドを分割して、それぞれのメソッドで必要とする引数の数を減らします。
- 複数の引数が一つのまとまりを表すのなら、そのまとまりを表すstaticなメンバクラスを作成します。例えば 色を引数に指定するメソッドであれば、red・blue・greenを渡すのではなく Colorというstaticなメンバクラスを定義することによって、引数の数を3つから1つに減らすことができます。
- Builderパターンを適用します。「Object」の章の「Builderパターン」を参照してください。
- 引数の型については クラスよりもインタフェースを選びます。実装クラスの置き換えが容易になり、柔軟性を上げることができます。
- 2値の選択肢を持つ引数でも booleanの意味がメソッド名から明らかでない場合は列挙型を使います。
2値の選択肢を持つ引数はboolean型にしてしまいがちですが、メソッド名からtrue/falseが何を表しているのか不明な場合は2値の列挙型を使います。例えば人を表すPersonクラスで性別を指定してインスタンスを生成する場合、Person.newInstance(true)では 男性を指定しているのか女性を指定しているのか分かりません。列挙型 Gender { MALE, FEMALE }を定義して、Person.newInstance(Gender.MALE)とする方が意味が明瞭になります。また、列挙型であれば MALE、FEMALE以外の選択肢を追加する際にも柔軟に対応することができます。
ドキュメントコメント
Javaでは ソースコードに記述したドキュメントコメントからJavaDocを作成することができます。ドキュメントコメントはクラス、インタフェース、コンストラクタ、メソッド、フィールド宣言の前に記述します。
ここではメソッドのドキュメントコメントの書き方についてまとめます。
- メソッドの事前条件と事後条件を列挙します。
事前条件と事後条件については後述しますが、多くの事前条件は引数に対する条件となり、条件に合わない場合は実行時例外が投げられます。一般的には@throwsタグで暗黙的に事前条件が列挙されることになります。 - メソッドに副作用があれば副作用の内容を文書化します。
副作用とはオブジェクトの状態を変更したり、ファイル・DBの状態を変更したり、ネットワークの通信先の状態を変更したり、バックグラウンドのスレッドを開始したりといった システム状態の観察可能な変化を指します。 - 引数を@paramタグ、戻り値を@returnタグ、例外を@throwsタグで列挙します。
特に総称型メソッドの場合、型引数を@paramタグで文書化します。 - 継承されることを前提とするクラスの場合は、自己利用がある場合は@implSpecタグを使って明示します。自己利用については この章の「スーパークラスにおける自己利用」を参照してください。
- 最初のピリオドまでが概要説明となり、JavaDocのメソッドの「メソッドのサマリー」の「説明」の箇所に表示される内容になります。概要説明にピリオドを含む場合は {@literal }でエスケープすることができますが、Java 10からは {@summary }によって概要説明を明示できるようになりました。
例:{@summary 概要説明… }
続いて JavaDoc全般の書き方についての補足をまとめます。
- ドキュメント内にソースコード例を含めたい場合は{@code }によって記述することができます。複数行に渡る場合は ソースコードの改行を維持するために{@code ソースコード… }をHTMLの**
**タグで囲みます。
- JavaDocはHTML形式です。「<」、「>」、「&」など HTMLのメタ文字を含む場合は {@literal }でエスケープします。
- Java 9からJavaDocの右上に検索窓が追加されました。JavaDoc中の用語を{@index }で囲むことによって、この検索窓から検索するすることができるようになります。
- {@inheritDoc }を使うことによって スーパークラスや実装するインタフェースのメソッドコメントを継承することができます。
最後に メソッドに限らず APIとして公開する要素に対するドキュメントコメントの補足をまとめます。
- 総称型のクラスや総称型のメソッドを文書化する際には型引数を@paramタグで文書化します。
- 列挙型を文書化する際には 定数も文書化することを忘れないようにします。
- アノテーションを文書化する際には 全てのメンバも文書化します。
- クラスの文書化をする際に マルチスレッドにおける安全性のレベルを記述するようにします。安全性のレベルは次の通りです。
- 不変:不変クラスのオブジェクトであれば 安全にマルチスレッドで共有できます。
- スレッドセーフ:ユーザが同期を行わなくても マルチスレッドで全てのメソッドを安全に呼び出すことができます。(但し、複数のメソッドを呼び出すときは同期が必要な場合もあります。)
- 条件付スレッドセーフ:基本的にはスレッドセーフですが、一部ユーザが同期を行う必要のあるメソッド等を含みます。例えば Collections.synchronizedList()で返されるListの各メソッドは スレッドセーフになりますが、このListを走査する場合には 明示的に同期を行う必要があります。
どの操作が同期が必要で、どのロックオブジェクトを使うのかを明記します。
- スレッドセーフではない:マルチスレッドで使うには、ユーザが明示的に同期を行う必要があります。
- クラスがシリアライズ可能な場合は シリアライズ形式を文書化します。
メソッドの事前条件と事後条件
メソッドの事前条件とは、そのメソッド呼び出しが成功するために満たすべき条件になります。代表的なものはメソッドの引数やオブジェクトの状態です。例えばList.get(int index)では 次の2つが事前条件になります。
- indexが0~(size()-1)であること。(引数)
- List.isEmpty()がfalseであること。(オブジェクトの状態)
事前条件を守らないとメソッドは失敗しますが、事前条件を満たせば 必ずメソッド呼び出しが成功するというわけでもありません。
メソッドの事後条件とは、メソッドが成功した場合に 成立する条件になります。純粋関数の場合は 引数に対して期待する戻り値が返ってくればメソッドが成功したと判断できますが、オブジェクトの状態の変更等の副作用を持つメソッドの場合は 戻り値だけではなく 事後条件もメソッド成否の判断材料になります。 例えばArrayList.add(E e)では 戻り値がtrueであることとは別に 次の2つが事後条件になります。
- ArrayListの要素数が メソッド呼び出し前に比べて1増えていること。
- ArrayListの最後の要素が引数のeになっていること。
純粋関数の場合は副作用がないため 事後条件は特にありません。強いて言うならメソッド呼び出し前後で状態の変化が無いことが事後条件になります。
事前条件を満たすべき責任はメソッドの呼び出し側にありますが、事前条件が成立していることを確認するための手段が提供されていることが前提です。例えばList.get(int index)で indexが範囲内であるかどうかは、List.isEmpty()とList.size()で呼び出し側が確認するとこができます。また、Iterator.next()で次の要素があるかどうかはIterator.hasNext()で呼び出し側が確認することができます。事前条件が成立しているかどうかの確認手段が提供されていれば、事前条件を満たすべき責任はメソッドの呼び出し側にあります。したがって、事前条件を確認せずに呼び出した場合、メソッド失敗に伴う例外を処理する責任は呼び出し側にあります。一方で、事前条件を満たして呼び出された場合、事後条件を満たすべき責任はメソッドの処理側にあります。
尚、事前条件が成立しているかどうかの確認手段が提供されていない場合は、責任の分解点は曖昧になってしまいます。
前述の通り 事前条件を満たしてもメソッドが成功するとは限りません。メソッドがメモリ操作以外の処理を行う場合は事前条件を満たしていても失敗する可能性があります。(厳密にはメモリ操作で失敗しない可能性は0ではありませんが、無視できるほど低いため プログラム全体の事前条件として 暗黙にメモリ操作は失敗しない前提で考えられます。)メモリ操作以外の処理とは、ファイル読み書きであったり、ネットワーク通信であったり、DB操作等の処理が挙げられます。そのような処理はファイルに書き込み権限が無かったり、HDDやDBサーバ、ネットワークに障害が発生しているといった理由で、事前条件に関わらず失敗する可能性があります。
引数チェック
publicやprotectedのパッケージ外に公開しているメソッドについては メソッドの始めで引数の正当性チェックを行います。引数が妥当でない場合は 大抵は実行時例外を投げます。一般的な実行時例外は IllegalArgumentException、IndexOutOfBoundsException、NullPointerExceptionです。実行時例外は例外処理を強制することができないため、JavaDocの@throwsタグで 引数が妥当でない場合に投げられる例外を列挙します。
引数が妥当でない場合に 空のOptionalを返すという方法を思いつくかも知れませんが、Optionalは値が無いことを表す物で エラーの内容を表すこともできないため 引数が妥当でない場合には使えません。
引数チェックのタイミングとエラーアトミック性
引数の正当性チェックはメソッドの始めで行います。これは メソッドの中でオブジェクトの状態を変更したり DBへの書き込み等の変更系の操作を行う場合は特に重要で、メソッドが失敗する場合は メソッドを呼び出す前の状態から変更が無いようにしないといけません(これをエラーアトミック性と言います)。失敗したら変更前の状態に戻す(ロールバック)という方法もありますが、ロールバックが失敗した場合も考慮しなくてはならず 複雑で不完全になりがちです。エラーアトミック性を確保するには、一般的には変更系の操作を行う前に失敗の可能性を排除する方法を採ることになります。そのため、引数の正当性チェックはメソッドの始めに行うようにします。
また、コンストラクタで受け取った引数を後で使うような場合、使う際にチェックを行うのではなく コンストラクタで受け取った時点でチェックを行うようにします。そうしないと エラーが発生した場合に どこでインスタンスが生成されたかを追わなくてはならず、大抵はデバッグを複雑にしてしまいます。
引数チェックユーティリティ
java.util.Objectsクラスには 引数チェック用のユーティリティメソッドが用意されています。主なユーティリティメソッドを次にまとめます。メソッドの処理自体は単純な物ではありますが、ユーザが同じようなユーティリティを書く手間を省いてくれます。
引数チェック用の主なユーティリティメソッド
メソッド
|
引数
|
内容
|
requireNonNull
|
T obj
|
引数がnullでないかチェックを行います。 nullの場合はNullPointerExceptionが投げられます。
|
requireNonNull
|
T obj String message
|
同上。messageでNullPointerExceptionのメッセージを指定できます。
|
checkIndex
|
int index int length
|
indexが0から(length-1)であるかどうかチェックを行います。 範囲外の場合はIndexOutOfBoundsExceptionが投げられます。 配列やコレクション等のインデックスを指定する場合に インデックスが範囲内かどうかのチェックを行う用途に利用できます。
|
checkFromIndexSize
|
int fromIndex int size int length
|
fromIndex~(fromIndex+size-1)が0~(length-1)の範囲内であるかどうかチェックを行います。配列やコレクション等のサブレンジを指定する場合に サブレンジが範囲内かどうかのチェックを行う用途に利用できます。
|
checkFromToIndexSize
|
int fromIndex int toIndex int length
|
上のメソッドと同じですが、サブレンジの指定方法が異なります。fromIndex~(toIndex-1)が0~(length-1)の範囲内であるかどうかのチェックを行います。
|
アサーション
publicやprotectedでない パッケージ外に公開しないメソッドについては アサーションによる引数チェックを行うこともできます。パッケージ外に公開していないメソッドであれば 呼び出し元は同じパッケージ内に限られ、大抵は同じ組織で開発を行うため、どのような状況で呼び出されるかを管理するのは容易です。もし誤った引数でメソッドを呼び出していても、網羅的な試験を行うことで検出することが可能です。また、アサーションの有効・無効はjavaコマンドの-eaオプションで簡単に切替えられ、無効にした場合はチェックに掛かるコストを省くことができるため、性能が必要なパッケージを開発するような場合に有効です。
しかし、試験漏れなどがあった場合に アサーションを無効にしていると 思わぬ箇所で問題が顕在化するため 原因が特定しづらくなります。そのため、基幹システムのような高い堅牢性や保守性を求められるシステムを開発する場合は パッケージ外に公開しないメソッドであっても アサーションを使わず引数チェックを行い ログに残した方が良い場合もあります。
その辺りについては 性能や開発期間、堅牢性や保守性など開発システムの特性に合わせて 開発現場ごとに方針を定めるのが良いと思います。
引数チェックの省略
引数チェックはメソッドの始めで行うべきではありますが、場合によっては省略できることもあります。それは引数の正当性チェックのコストが高くつき、かつメソッドのメインの処理の中で正当性チェックが暗黙的に行われる場合です。例えばCollections.sort(List)のようなメソッドを考えます。Listの要素を走査していき、2つの要素の比較を行って並べ替えていきます。その際に要素が互いに比較可能でない場合はClassCastExceptionが投げられます。
このメソッドで 始めに引数チェックを行おうとすると、一旦Listの要素を走査していき 2つの要素が比較可能かどうかを調べた後(事前チェック)、再度Listの要素を走査していき 2つの要素の比較を行って並べ替える(メインの処理)という処理になります。メインの並べ替え処理の中で2つの要素を比較する際に 暗黙的に2つの要素が相互比較可能かのチェックが行われるため、同じような走査を2回行うことになり、性能劣化に繋がるのは明らかです。
このような場合は、冗長な引数チェックを省略することができます。ただし、メソッドが失敗した場合に、受け取ったListがメソッドを呼び出す前から変更がないようにして エラーアトミック性を失われないように考慮する必要があります。
また、暗黙的に引数の正当性チェックが行われる場合、そのままの例外をメソッドの外に投げても 呼び出し側が理解できない場合があります。例えばAbstractSequentialListのget(int index)メソッドでは indexで指定されたListIteratorを生成して、ListIteratorインスタンスのnext()の結果を返しますが、indexが範囲外の場合はnext()がNoSuchElementExceptionを投げます。それをそのままget()メソッドの外に投げてしまうと 呼び出した側は意味が分からず混乱してしまいます。そのような場合は 適切な概念を表す例外に翻訳して投げるようにします(例外翻訳)。get()メソッドでは、ListIteratorのnext()がNoSuchElementExceptionを投げた場合は、IndexOutOfBoundsExceptionに変換してget()の呼び出し側に投げるような翻訳を行っています。
ディフェンシブコピー
メソッドで引数を渡す場合(や戻り値を返す場合)のデータ型はプリミティブ型かオブジェクト参照になります。オブジェクト参照を渡す場合は、メソッドの呼び出し側と受け取り側で参照を共有することになります。そのため、どんなに受け取ったメソッド側で オブジェクトの状態を変更しないように努めたり、複数のオブジェクト間の不変条件が崩れないように努めても、メソッド呼び出し側で簡単にそれを崩すようなことができてしまいます。
そのような例を次に挙げます。
public final class TimeSpan {
private final Date start;
private final Date end;
public TimeSpan(Date start, Date end) {
if(start.compareTo(end) > 0) {
throw new IllegalArgumentException("start > end");
}
this.start = start;
this.end = end;
}
public Date getStart() {
return start;
}
public Date getEnd() {
return end;
}
}
コンストラクタでstart<endの条件が成立するようにチェックを行っていますが、次のようにすることによって簡単に不変条件を崩すことができてしまいます。
Date start = new Date();
Date end = new Date();
TimeSpan timespan = new TimeSpan(start, end);
// 呼び出し側で不変式を崩すことができてしまう。
end.setTime(end.getTime() - 10000);
また、次のようにしても同様に不変条件を崩すことができてしまいます。
// 別の箇所でendへの参照を取得して不変式(start < end)を崩すことができてしまう。
Date e = timespan.getEnd();
e.setTime(e.getTime() - 10000);
これはいずれも TimeSpanオブジェクトと利用者側でstartとendという可変オブジェクトへのオブジェクト参照を共有してしまっていることが原因です。オブジェクト参照の共有を防ぐには、メソッドやコンストラクタでオブジェクト参照を受け取る場面や 戻り値でオブジェクト参照を返す場面でオブジェクトのコピーを生成して返すようにします。これをディフェンシブコピーと言います。TimeSpanクラスをディフェンシブコピーを行うように修正すると次のようになります。
public final class TimeSpan {
private final Date start;
private final Date end;
public TimeSpan(Date start, Date end) {
// ディフェンシブコピー
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
// コピーした後に コピーした引数に対して正当性チェックを行う。(TOCTOU攻撃対策)
if(this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException("start > end");
}
}
public Date getStart() {
// ディフェンシブコピー
return new Date(start.getTime());
}
public Date getEnd() {
// ディフェンシブコピー
return new Date(end.getTime());
}
}
オブジェクト参照を受け取るコンストラクタでは、引数のコピーを生成してインスタンスフィールドとして保持するように修正しています。これにより オブジェクト参照の共有を回避することができます。ここで2点注意事項があります。
- 引数チェックは引数のコピーを生成した後に、コピーした引数に対して行う。
これは マルチスレッドでTimeSpanの生成を行っている場合、コンストラクタが引数のコピーを行ってから引数チェックを行う間に 別のスレッドが引数のオブジェクト参照の状態を変更してしまう可能性があるためです。 - java.util.Dateのように finalでないクラスの場合は、コピーの際にclone()を使わない。
悪意のある攻撃者がDateのサブクラスのインスタンスを渡す可能性があり、clone()メソッドを通して 攻撃者が攻撃できるような細工を施すことが可能なためです。finalなクラスであれば サブクラスの可能性はないため、clone()でコピーを作成しても問題ありません。
また、オブジェクト参照を返すgetStart()、getEnd()では、インスタンスフィールドのコピーを生成して返すように修正しています。これにより オブジェクト参照の共有を防ぐことができます。
尚、ディフェンシブコピーが必要なのは可変オブジェクトへの参照を渡す場合です。不変オブジェクトの参照は共有しても問題ないため 不変オブジェクトの受け渡しをする場合は ディフェンシブコピーは必要ありません。
ディフェンシブコピーは少なからずも複製のためのコストが掛かるため、例えば同一パッケージ内で 不変条件を崩さないことが確証できれば 省略しても問題ないかも知れません。
メソッドやコンストラクタによっては、引数で渡されたオブジェクトの状態を変更する性質の物もあります。そのような オブジェクトの所有権を明け渡すメソッドやコンストラクタでは ディフェンシブコピーは行わず、代わりに メソッドやコンストラクタのJavaDocに 受け取ったオブジェクト状態を変更することを明記します。
変更不可能なビュー
可変オブジェクトのディフェンシブコピーを作成するのが不可能な場合や、コピーにコストが掛かるような場合に 代替手段をとることができます。可変オブジェクトの変更不可能なビューを返す方法です。変更不可能なビューは 可変オブジェクトのクラスを継承するか、可変オブジェクトの型を定義するインタフェースを実装し、オブジェクトの状態を変更するメソッドを全てオーバーライドして UnsupportedOperationException を投げるようにします。
class UnmodifiableDate extends Date {
private Date date;
public UnmodifiableDate(Date date) {
this.date = date;
}
// 状態変更を行うメソッドは 例外を投げる。
@Override
public void setTime(long time) {
throw new UnsupportedOperationException();
}
// 他の全ての状態変更を行うメソッドも同様。
}
class SomeClass {
private Date date;
public Date getDate() {
return new UnmodifiableDate(date);
}
}
可変オブジェクトのクラスが継承不可能で、型を定義するインタフェースもない場合には この方法は適用できません。
可変長引数(J2SE 5.0~)
J2SE5.0から可変長引数に対応できるようになりました。可変長引数は「型名…」の形で指定し、0個以上の引数を受け付けます。
public int calcSum(int... values) {
int sum = 0;
for (int value : values) {
result += value;
}
return result;
}
可変長引数を受け取った側では 引数を配列として扱うことができます。可変長引数は各メソッドで1つだけ指定することができ、複数引数がある場合は必ず可変長引数を最後にする必要があります。
void someMethod1(int i, String... str) {} // OK
void someMethod2(String... str, int i) {} // NG:可変長引数は最後でないとダメ
void someMethod3(int... i, String... str1) {} // NG:可変長引数は1つでないとダメ
可変長引数の実体やリフレクションなどについては、以下のサイトが参考になります。
1個以上の引数を渡す場合
可変長引数は0個以上の引数の受け渡しを行いますが、時には1個以上の引数の受け渡しを行いたい場合があります。その場合は、受け取りたい型の引数を1つと、同じ型の可変長引数を受け取るように定義します。可変長引数のみにしてしまうと 引数なしでもコンパイルが通ってしまいますが、こうすることによって 引数が少なくとも1個はあることをコンパイル時に強制することができます。
int min(int first, int... remain) {
int min = first;
for(int i : remain) {
if(i < min) {
min = i;
}
}
return min;
}
可変長配列の性能劣化対策
可変長配列は 呼び出しのたびに配列を割り当てるため、少なからず性能劣化が生じます。もし 引数の個数が決まった個数以内に収まることが大半であるような状況であれば、性能劣化に対する対策を施すことができます。0個から決まった個数までの引数のメソッドと、それ以上の個数に対応する可変長メソッドを1つオーバーロードする形にします。
public class VarArgsExample {
public static <E> void of() {
System.out.println("of()");
}
public static <E> void of(E e1) {
System.out.println("of(E)");
}
public static <E> void of(E e1, E e2) {
System.out.println("of(E1, E2)");
}
public static <E> void of(E e1, E e2, E...elements) {
System.out.println("of(E1, E2, E...)");
}
public static void main(String[] args) {
of(); // of()
of(1); // of(E)
of(1, 2); // of(E1, E2)
of(1, 2, 3); // of(E1, E2, E...)
of(1, 2, 3, 4); // of(E1, E2, E...)
}
}
固定長引数のメソッドと可変長引数のメソッドの両方が一致する場合は、固定長引数のメソッドが優先されます。そのため、大半のメソッド呼び出しは可変長引数を必要としないため、可変長の柔軟性を維持しつつ 性能劣化を抑えることができます。java.util.List等のCollectionのサブインタフェースでも 同じような仕組みを提供しています。
名前付き引数(未サポート)
JavaにはPython・Ruby・C#のような名前付き引数の機能はありません。
Effective Javaの中で コンストラクタで 名前付き引数と似たような仕組みを実現する手段が Builderパターンとして提唱されています。Builderパターンについては「Object」の章の「Builderパターン」を参照してください。Builderパターンは実装の手間が掛かりますが、例えばLomBokライブラリを使用すると@Builderアノテーションを記述するだけでBuilderパターンを実現することもできます。
BuilderパターンとLombokライブラリについては以下のサイトが参考になります。
引数のデフォルト値(未サポート、代替可)
JavaではPython・Rubyのように引数のデフォルト値を設定することができません。これについてはオーバーロードで代替することができます。
// 引数 c のデフォルト値を 10 に設定。
public int someMethod(int a, int b) {
return someMethod(a, b, 10); // c のデフォルト値は10
}
public int someMethod(int a, int b, int c) {
}
オーバーロード
オーバーロードにより 同じ名前で引数の異なるメソッドを定義することができます。オーバーロードの条件はメソッドシグネチャが異なることであり、戻り値だけ違うメソッドやアクセス指定子だけが違うメソッドは メソッドシグネチャが同じためオーバーロードすることができません。
public int someMethod(int i) {...}
public int someMethod(String str) {...} // OK
//public void someMethod(int i) {...} // NG:戻り値だけが異なるため
//private int someMethod(int i) {...} // NG:アクセス指定子だけが異なるため
総称型の場合は メソッドシグニチャにイレイジャが用いられます。詳しくは「総称型」の章の「メソッドシグニチャ」を参照してください。
混乱し易いオーバーロードを避ける
Javaでは 同じ名前のメソッドを多重定義する仕組みが2つあります。オーバーライドとオーバーロードです。
- オーバーライドは どのメソッドを呼び出すかを コンパイル時ではなく 実行時の レシーバの型で決定します(ダイナミックバインディング)。
- 一方で、オーバーロードは どのメソッドを呼び出すかを コンパイル時の引数の型で決定します(スタティックバインディング)。
コンパイル時の型なのか実行時の型なのか、またレシーバの型なのか引数の型なのかがごちゃまぜになって、オーバーロードは実行時の引数の型で決まると勘違いされることがたまにあります。
オーバーロードの場合は次のようになります。
class OverloadExample {
public static void someMethod(Object obj) {
System.out.println("someMethod(Object)");
}
public static void someMethod(String str) {
System.out.println("someMethod(String)");
}
public static void main(String[] args) {
String str = "test";
Object obj = str;
someMethod(str); // ① someMethod(String)が呼び出される
someMethod(obj); // ② someMethod(Object)が呼び出される
}
}
①も②も引数には同じString型のオブジェクト参照を渡していますが、呼び出されるメソッドはコンパイル時の引数の型で決まります。Javaは基本的にダイナミックバインディングを行う言語であるため、オーバーロードは特殊な形と言えます。それゆえに プログラマに混乱を与え易いようなオーバーロードは避けるべきです。
オーバーロードされたメソッドの引数の数が異なる場合は 混乱を与えることはないので問題ありません。また、引数の数が同じでも、同じ位置の引数の型が根本的に異なる場合も問題ありません。2つの型が根本的に異なる というのは、暗黙的なキャストまたは明示的なキャストで変換できない型同士のことを指します。具体的には次のようになります。
- 2つの型の間に継承関係がある場合は 型は根本的に異なりません。
- プリミティブ型同士の場合は 型は根本的に異なりません。
- 片方がプリミティブ型の場合は そのラッパークラスが もう片方の型と継承関係があると 型は根本的に異なりません。
いくつか例を挙げます。
- StringとThrowableは継承関係がないので 根本的に異なる型です。
- ListとCollectionは継承関係があるので 根本的に異なる型ではありません。
- intとdoubleはプリミティブ型同士なので 根本的に異なる型ではありません。
- intとObjectは intのラッパークラスがIntegerで IntegerとObjectは継承関係があるので 根本的に異なる型ではありません。
オーバーロードされたメソッドの引数の型が 根本的に異ならない型の場合、混乱を与えます。次に例を挙げます。
class OverloadExample2 {
public static void someMethod2(int i) {
System.out.println("someMethod2(int)");
}
public static void someMethod2(Object obj) {
System.out.println("someMethod2(Object)");
}
public static void main(String[] args) {
Integer integer = Integer.valueOf(456);
// オーバーロードされていないIntegerを渡す。
someMethod2(integer); // ① someMethod2(Object)
someMethod2((int) integer); // ② someMethod2(int):明示的にキャストをすると②を呼べる
}
}
someMethod2()は引数intとObjectでオーバーロードされていますが、Integer型の引数を渡すと エラーにならずにコンパイルすることができます。Integerはオートアンボクシングされるとintになり Objectのサブタイプでもあるため、どちらが呼び出されるかは直感的には分かりづらいと思います。結果は①のようにsomeMethod2(Object)の方が呼び出されます。someMethod(int)を明示的に呼び出すには呼び出し時に明示的なキャストを行います。
逆にIntegerとObjectの引数でオーバーロードされているメソッドに対してintの引数を渡すとどうなるでしょうか。
class OverloadExample3 {
public static void someMethod3(Integer i) {
System.out.println("someMethod3(Integer)");
}
public static void someMethod3(Object obj) {
System.out.println("someMethod3(Object)");
}
public static void main(String[] args) {
int i = 123;
// オーバーロードされていないIntegerを渡す。
someMethod3(i); // ① someMethod3(Integer)
someMethod3((Object) i); // ② someMethod3(Object):明示的にキャストをすると②を呼べる
}
}
①のようにオートボクシングされてsomeMethod3(Integer)が呼び出されます。これは直感的に理解し易い動作です。②のようにキャストをすることにより someMethod3(Object)を明示的に呼び出すこともできます。
オーバーロードされたメソッドの選択の法則を知っていれば問題ないかも知れませんが、混乱を与え易いので 根本的に異ならない型の引数でメソッドをオーバーロードすることは避けるべきです。
根本的に異ならない型の引数のメソッドをオーバーロードしたい場合は、メソッド名を変える(つまりオーバーロードをしない)方が賢明です。コンストラクタをオーバーロードしたい場合は名前を変えることはできないため、異なる名前のstaticファクトリメソッドを用意するという手段を選択することができます。
引数が関数型インタフェースの場合
オーバーロードされているメソッドの引数が関数型インタフェースで、更にその引数にオーバーロードされているメソッド参照を渡すと 曖昧な参照とみなされ コンパイルエラーになる場合があります。
public class OverloadFIExample {
static class SomeClass {
void overload() {
System.out.println("SomeClass.overload()");
}
void overload(int i) {
System.out.println("SomeClass.overload(int)");
}
}
public static void someMethod(Runnable r) {
System.out.println("someMethod(Runnable)");
}
public static <V> void someMethod(Callable<V> callable) {
System.out.println("someMethod(Callable)");
}
public static void main(String[] args) {
SomeClass sc = new SomeClass();
// someMethod(sc::overload); // あいまい、コンパイルエラー
someMethod((Runnable)sc::overload); // 明示的なキャストが必要
}
}
someMethod()は引数RunnableとCallableでオーバーロードされています。また、SomeClassのoverload()は引数voidとintでオーバーロードされています。消去法で考えるとsomeMethod(Runnable)に対してSomeClassのoverload(void)へのメソッド参照を渡していることは明らかなのですが、コンパイラはこれを曖昧とみなしてコンパイルエラーにしてしまいます。更に混乱を引き起こすことに、SomeClassのoverload(int)が定義されていないと コンパイルが成功するようになります。
引数の数が同じで、同じ位置の引数が関数型インタフェースのメソッドをオーバーロードすることは、このような混乱をもたらすので避けるのが無難です。
これは、関数型インタフェース同士は型が根本的に異ならないことに起因しています。異なる関数型インタフェースの変数同士は相互にキャスト可能で、次のようなコードはコンパイルエラーにはなりません。(実行時にClassCastExceptionが発生します。)
Runnable runnable = () -> {};
Callable<String> callable = (Callable<String>) runnable; // 実行時にClassCastException
ゲッター/セッター
インスタンスフィールドにアクセスするgetXXXメソッド、setXXXメソッドをゲッター/セッターと呼びます。XXX部分はインスタンスフィールドの先頭を大文字にした名前が入ります。ゲッター/セッターを提供することにより 直接インスタンスフィールドへのアクセスを防ぐことができ、インスタンスフィールドへのアクセスの局所化とアクセス時に付随する処理を追加することができます。
戻り値
Javaではreturnの後の値や式は省略することができません。(「return;」はコンパイルエラーとなります。)
また、Javaでは複数の型の戻り値を返すことができません。(同一のデータ型ならば戻り値を配列にすれば良いのですが、Pythonのようにタプルやリストで複数のデータ型の値を戻り値とすることができません。)複数の型の戻り値を返すには それぞれの戻り値をインスタンスフィールドとして持つクラスを定義することで対応します。
戻り値としてのnull
全てのオブジェクト参照型の戻り値を返すメソッドでは、返す値が無い場合にnullを返すことができます。しかし、メソッドがnullを返す可能性があると、呼び出し側でnullチェックを行わないといけません。それにも関わらず 呼び出し側にnullチェックを強制することができません。その結果 想定外にNullPointerExceptionを発生させることにつながります。そのため 返す値が無い場合は nullを返すのではなく次の値を返すようにする方が望ましいです。
- 一つの値を返す場合。
空のOptionalを返します。Optionalの詳細については「null安全」の章を参照してください。 - 配列やコレクション等のコンテナを返す場合。
空の配列や空のコレクション等を返します。
空の配列や空のコレクションを生成するコストが気になるような場合は、不変な空のコレクションや不変な空配列を使いまわすことによって 割当のコストを抑制することができます。不変な空コレクションはCollectionsクラスのクラスメソッドemptyList()、emptySet()、emptyMap()で取得することができます。不変な空コレクションや不変な空配列は不変オブジェクトであるため、問題なく使いまわすことができます。
コンストラクタ
デフォルトコンストラクタ
明示的にコンストラクタを定義しない場合、コンパイラによって 引数なしのデフォルトコンストラクタが追加されます。
インスタンスイニシャライザ
インスタンスイニシャライザは コンストラクタの前に呼び出されます。インスタンスイニシャライザはclassブロックの直下に名前なしのブロックとして記述します。
public class SomeClass {
int id;
String name;
// インスタンスイニシャライザ
{
this.id = 1;
this.name = "Bob";
}
}
インスタンスイニシャライザは複数記述することができ、その場合は記述した順番に呼び出されます。たいていはコンストラクタで事足りますが、コンストラクタを記述することができない場合、例えば匿名クラスを初期化するためにはインスタンスイニシャライザを使うことになります。
コンストラクタからオーバーライド可能なメソッドを呼び出さない
コンストラクタから オーバーライド可能なメソッドを呼び出さないようにするべきです。コンストラクタから オーバーライド可能なメソッドを呼び出すと サブクラスのコンストラクタの前にそのメソッドが呼び出されてしまいます。そのため、サブクラスのコンストラクタで初期化される筈のフィールドがまだ初期化されておらず、思わぬ挙動を引き起こすことがあります。
class Super {
public Super() {
someMethod(); // オーバーライド可能なメソッドを呼び出す。
}
public void someMethod() { }
}
public class Sub extends Super {
private String someField;
public Sub(String param) {
// 暗黙のうちにsuperのデフォルトコンストラクタが呼び出される。
someField = param;
}
// Subのコンストラクタより前に呼び出されてしまう
@Override public void someMethod() {
System.out.println(someField);
}
public static void main(String[] args) {
Sub s = new Sub("hello"); // null
s.someMethod(); // hello
}
}
隠れたコンストラクタ
通常 クラスのオブジェクトをインスタンス化するには コンストラクタを用います。他に特殊な方法としてリフレクションを利用してインスタンス化することもできます。
しかし、見落とされがちですが クラスのオブジェクトがインスタンス化される契機は他にもあります。それらは Effective Javaの中で「隠れたコンストラクタ」と呼ばれていて、次の2つになります。
- clone()でインスタンスの複製を作成する時。コンストラクタと同様、オーバーライド可能なメソッドは呼び出さないようにしなくてはいけません。
- Serializeされたインスタンスを復元する時。コンストラクタと同様、オーバーライド可能なメソッドは呼び出さないようにしなくてはいけません。また、Serializeされたインスタンスを外部から受け取る場合は 改竄されている可能性があります。そのため、コンストラクタで オブジェクトの不変条件のチェック、セキュリティに関するチェック、キャッシュや数制限などの制御を行っている場合は、復元する際にも同様の処理を実行する必要があります。
ネストしたクラス
メンバクラスはネストしたクラスの1つです。ネストしたクラスではないクラスはトップレベルのクラスになります。ネストしたクラスには次の4種類があります。
- staticなメンバクラス
- 非staticなメンバクラス
- 匿名クラス
- ローカルクラス
1と2はクラスのメンバであるクラスです。3と4はクラスのメンバではなく、利用時に定義できる一時的なクラスです。3は式を書けるところならどこでも定義できます。4はメソッドの中で定義できます。また、1を除く2~4は内部クラスと呼ばれます。
それぞれのネストしたクラスについて見ていきます。
メンバクラス
メンバクラスにはstaticなメンバクラスと非staticなメンバクラスがあります。メンバクラスはいずれかのクラスのメンバになりますが、メンバクラスが所属するクラスをエンクロージングクラスと呼びます。メンバクラスからはエンクロージングクラスのインスタンスの全てのメンバ(privateなメンバも含む)に直接アクセスすることができます。
staticなメンバクラスと非staticなメンバクラスの違いは、メンバクラスのインスタンスが エンクロージングクラスのインスタンスに暗黙に関連付けられるかどうかです。違う視点から見ると、メンバクラスのインスタンスがエンクロージングクラスのインスタンスが無くても存在できるかどうかです。
- staticなメンバクラスのインスタンスはエンクロージングクラスのインスタンスが無くても存在できます。
- 非staticなメンバクラスのインスタンスはエンクロージングクラスのインスタンスが無いと存在できません。
この違いを Mapインタフェースの実装クラスで見ることができます。多くのMapの実装クラスでは Map.Entryはstaticなメンバクラスで実現し、keySet()等が返すコレクションビューは非staticなメンバクラスで実現しています。
Mapの格納するキーと値のペアはMap.Entryインタフェースを実装したクラスになりますが、staticなメンバクラスとして定義されています。これは Mapのインスタンスが存在していようがいまいがMap.Entryのインスタンスは存在することができるからです。
一方でMapのkeySet()、entrySet()、values()等が返すコレクションビューは 非staticなメンバクラスとして定義されています。コレクションビューはMapのインスタンスの別の見え方であり、基となるMapのインスタンスが存在しないとコレクションビューが存在することはできないからです。
非staticなメンバクラスは、エンクロージングクラスへのインスタンスと暗黙に関連付けられます。暗黙な関連付けは エンクロージングクラスのインスタンスメソッドから 非staticなメンバクラスのインスタンスを生成する際に確立されます。非staticなメンバクラスは エンクロージングクラスのインスタンスへの参照フィールドを持ちます。リフレクションで調べると 該当フィールドの存在を確認することができます。これはコンパイラが自動的に付与するフィールドで、該当フィールドに対応するFieldのisSynthetic()はtrueを返します。このように コンパイラが自動的に付与したフィールドは合成フィールドと呼ばれ 合成フィールドはisSynthetic()の結果がtrueになります。
staticなメンバクラスと非staticなメンバクラスの違いは、両者のメンバクラスの生成の仕方の違いにも表れます。非staticなメンバクラスのインスタンスを生成する際には エンクロージングクラスが存在している必要があります。そのため、非staticなメンバクラスのインスタンスを生成する方法は次のいずれかになります。
- エンクロージングクラスのインスタンスメソッドの中から非staticなメンバクラスのインスタンスを生成する。
- エンクロージングクラスのインスタンスに続けて new演算子で非staticなメンバクラスのインスタンスを生成する。(例:enclosingClassInstance.new MemberClass(args))
一方でstaticなメンバクラスは エンクロージングクラスを必要としないため、文脈に依らず通常のクラスと同じようにnew演算子でインスタンスを生成することができます。具体例を次に挙げます。
public class MemberClassExample {
class NonStaticMemberClass {} // 非staticなメンバクラス
static class StaticMemberClass {} // staticなメンバクラス
public void someMethod() {
// エンクロージングクラスのインスタンスメソッドで生成する場合はどちらも同じ
NonStaticMemberClass nsmc = new NonStaticMemberClass();
StaticMemberClass smc = new StaticMemberClass();
}
public static void main(String[] args) {
// 非staticなメンバクラスの生成にはエンクロージングクラスのインスタンスが必要
NonStaticMemberClass nsmc1 = new ClassExample().new NonStaticMemberClass();
MemberClassExample outer = new MemberClassExample();
NonStaticMemberClass nsmc2 = outer.new NonStaticMemberClass();
// staticなメンバクラスの生成にはエンクロージングクラスのインスタンスは不要
StaticMemberClass smc = new StaticMemberClass();
}
}
staticなメンバクラスと非staticなメンバクラスでは エンクロージングクラスのインスタンスへの参照を持っているかどうかが異なるため、次のような違いがあります。
- 非staticなメンバクラスはエンクロージングクラスのインスタンスの全てのメンバに直接アクセスできます。通常アクセス可能なクラスメンバであれば、「エンクロージングクラスのインスタンス.メンバ」の形でアクセスできますが、「エンクロージングクラスのインスタンス.」の部分を省略することができます。一方で staticなメンバクラスはエンクロージングクラスのインスタンスに関連付けられているわけではないため、そのようなことはできません。
- 非staticなメンバクラスは「エンクロージングクラス.this」の形でエンクロージングクラスのインスタンスにアクセスすることができます。一方で staticなメンバクラスはエンクロージングクラスのインスタンスに関連付けられているわけではないため、そのようなことはできません。
具体的な例を次に挙げます。
package basic2.clazz;
public class MemberClassExample2 {
private int i; // エンクロージングクラスのインスタンスフィールド
private static int si; // エンクロージングクラスのクラスフィールド
static class StaticMemberClass { // staticなメンバクラス
void someMethod() {
// System.out.println(i); // NG:エンクロージングクラスのインスタンスへの参照は持っていない
System.out.println(si); // OK:エンクロージングクラスを指定する必要はない。
System.out.println(this); // 自身(StaticMemberClassのインスタンス)
// System.out.println(MemberClassExample2.this); // NG
}
}
class NonStaticMemberClass { // 非staticなメンバクラス
void someMethod() {
System.out.println(i); // OK:エンクロージングクラスのインスタンスを指定する必要がない。
System.out.println(si); // OK:同様。
System.out.println(this); // 自身(NonStaticMemberClassのインスタンス)
System.out.println(MemberClassExample2.this); // エンクロージングクラスのインスタンス
}
}
public static void main(String[] args) {
NonStaticMemberClass nsmc = new MemberClassExample2().new NonStaticMemberClass();
nsmc.someMethod();
StaticMemberClass smc = new StaticMemberClass();
smc.someMethod();
}
}
フィールドの場合 staticを付けるとクラスフィールドになるため、static修飾子は「クラスで1つ」の意味を連想してしまうかも知れません。しかし、メンバクラスにおけるstatic修飾子は エンクロージングクラスのインスタンスからの独立性を表しています。そのため、staticなメンバクラスは シングルトン(そのクラスでインスタンスが1つだけしか存在できない)というわけではなく複数インスタンスを生成することができます。フィールドにおけるstatic修飾子の役割から連想すると勘違いし易い点だと思いますので注意が必要です。
また、非staticなメンバクラスは 暗黙的にエンクロージングクラスのインスタンスへの参照を持つため、メモリリークの原因になり易いです。具体的な例として ファイナライザに代わるクリーナにおけるクリーニングアクションクラスは この理由から非staticではなく staticなメンバクラスにする必要があります。詳しくは「Object」の章の「ファイナライザの代替:セーフティネットとしてのクリーナ」を参照してください。
匿名クラス
匿名クラス(無名クラスとも呼ばれます)は一時的なクラスで 利用時に定義とインスタンス化を同時に行います。既存のインタフェースを実装するか、既存のクラスを継承するかのどちらかになります。匿名クラスは 式を書けるところであれば どこでも書くことができます。メソッド呼び出しの引数に匿名クラスを書くこともできますし、メソッドの戻り値に匿名クラスを書くこともできます。匿名クラスはアダプタを作成する場合に良く利用されます。そして、インスタンスメソッドなど非staticな文脈で使われると エンクロージングクラスのインスタンスへの参照を持ちます。
匿名クラスには次のような制限があります。
- 名前を持たないため、後からそのクラスのインスタンスを生成することができません。
- 名前を持たないため、instanceof等 クラスを指定するような場面では使うことができません。
- スーパータイプであるインタフェースやクラスは1つしか指定することができません。複数のインタフェースを実装したり、スーパークラスとインタフェースを同時に指定するようなことはできません。
- staticなメンバを持つことができません。例外的にfinalなフィールドを持つことはできます。
また、制限ではないのですが、クラス定義が長いとコードの可読性が落ちるため 注意が必要です。(Effective Javaではおおよその目安として10行以下としています。)
Java SE8でラムダ式が追加されるまでは 匿名クラスが関数オブジェクトを生成するための手段でしたが、ラムダ式が導入された後ではラムダ式を使う方が望ましいとされています。ラムダ式は匿名クラスと ほぼ同じですが 若干違いがあります。(違いについては「ラムダ式」の章の「thisが指し示す物」を参照してください。)
ローカルクラス
ローカルクラスは ネストしたクラスの中で 最も利用頻度の低いであろうクラスです。ローカル変数が定義できる場所であればローカルクラスを定義することができ、スコープもローカル変数と同じになります。
匿名クラスと同様、非staticな文脈で使われると エンクロージングクラスのインスタンスへの参照を持ちます。一方で 匿名クラスのような制限はほとんどなく、staticなメンバを持つことができない以外の制限はありません。
限られたスコープの範囲だけ有効なクラスが必要なケースはあまり多くないと思いますので、見かける機会は少ないかも知れません。
アクセス修飾子
アクセス修飾子のアクセス範囲
アクセス修飾子によりアクセス制御を行います。アクセス修飾子の種類とアクセス範囲を次の表にまとめます。
アクセス修飾子とアクセス範囲アクセス 可能範囲 | アクセス修飾子 | アクセス範囲 |
---|
↑ 狭い | private | 自クラスのみからアクセス可能。 |
| なし (パッケージプライベート) | 自クラスに加えて、同じパッケージのクラスからアクセス可能。 |
| protected | パッケージプライベートに加えて、別パッケージのサブクラスからアクセス可能。 |
↓ 広い | public | 全てのクラスからアクセス可能。 |
アクセス修飾子はクラス/インタフェース、メンバ(フィールド、メソッド、コンストラクタ、メンバクラス)に付与することができますが、クラス/インタフェースは指定できる修飾子が限られます。次の表にまとめています。
指定可能なアクセス修飾子 | private | パッケージプライベート | protected | public |
---|
クラス/インタフェース | × | ○ | × | ○ |
フィールド | ○ | ○ | ○ | ○ |
メソッド | ○ | ○ | ○ | ○ |
コンストラクタ | ○ | ○ | ○ | ○ |
メンバクラス | ○ | ○ | ○ | ○ |
クラス/インタフェースおよびクラスのメンバの場合はアクセス修飾子を記述しないとパッケージプライベートになりますが、インタフェースのメンバの場合はアクセス修飾子を記述しないとpublicになる点に留意が必要です。
パッケージプライベート
オブジェクト指向におけるカプセリングを実現するためには、フィールドを非公開にして、メソッドやコンストラクタを公開します。そして クラス自身は 他のクラスから使えるように公開します。この公開・非公開はクラスの外に対しての公開・非公開であり、Javaでは クラス外への非公開はprivateが対応し、クラス外への公開はpublicまたはパッケージプライベートが対応します。
publicとパッケージプライベートの違いは、パッケージの外に対して公開するかしないかの違いになります。パッケージ外への公開はpublicが対応し、パッケージ外への非公開はパッケージプライベートが対応します。公開範囲に制限のないpublicに対して パッケージプライベートは公開範囲をパッケージ内に限定することになります。
ライブラリのように 他から使われることを目的としたパッケージ、特に不特定多数の利用者に使われるようなパッケージにおいては、publicとパッケージプライベートの使い分けは重要になります。一旦publicとして公開してしまうと、それを変更する場合に 全ての利用者に影響が出るため、そう簡単には変更できなくなります。一方でパッケージプライベートな部分は 利用者に影響を与えることなく 柔軟に変更することができます。ライブラリのようなパッケージを開発する際には publicとパッケージプライベートの使い分けに注意を払う必要があります。基本的な方針としては パッケージ外に公開する必要のないものは publicにせずにパッケージプライベートにします。
一方、アプリケーションのように 他から使われず、将来的にも他から使われる予定もないような場合は、publicとパッケージプライベートの使い分けはあまり重要ではありません。アプリケーションに変更を加える必要が出てきた場合でも、ライブラリと違って利用者への影響を考慮する必要がなく、影響範囲はパッケージ内に限定されるからです。勿論、publicでないとコンパイルが通らないような箇所は それに従う必要がありますが、それ以外はpublicなのかパッケージプライベートなのかはそれほど重要ではありません。(publicでないとコンパイルが通らない箇所とは、アプリケーションが複数パッケージから構成されていて、パッケージを跨って利用されるクラスやメソッドなどです。)
フィールドをパッケージプライベートにして限定公開する
パッケージプライベートは 通常はクラス外へ公開する要素(クラス自身やメソッド・コンストラクタなど)を パッケージ外へ非公開とする用途で利用されます。しかし、時には通常はクラス外へ非公開とする要素(フィールドなど)をパッケージ内に限って公開する用途に利用することもできます。
フィールドをクラス外へ非公開とするのはカプセリングの一環であり、カプセリングのメリットは次のような点が挙げられます。
- 単純にフィールドに値を設定したりフィールドから値を取得する以外に、値範囲チェックを行ったり付随する処理を加えることができます。
- 複数のフィールド間で不変式がある場合(endがstartより小さくてはいけないなど)には、不変式が保たれるようなチェックを行うことができます。
- 現時点でフィールドに値を設定したりフィールドから値を取得する際に 付随する処理を行っていなくても、将来必要なときに追加することができます。また、将来的にフィールドを計算式などのフィールド以外の物に置き換えることもできます。
基本的に、カプセリングを実現するには フィールドをクラス外へ非公開のprivateにして、アクセッサメソッドを公開します。しかし、構造体のようなクラスで アクセッサメソッドで フィールドの設定と取得以外に付随する処理を行わないような場合は、フィールドごとにアクセッサメソッドを用意するのは 余計な手間な感じが否めず、コードの見た目も冗長になります。そのような場合に フィールドをクラス外に公開して クラス外からもアクセッサメソッドを通さず直接フィールドにアクセスできるような方法を採るケースも見受けられます。
このような場合にもJava特有のパッケージプライベートを活用することができます。クラス外にフィールドを公開するのにpublicにしてしまうと パッケージ外の利用者への影響も考慮する必要があり 影響範囲が想定できなくなってしまいますが、パッケージプライベートにすることにより 影響範囲を特定しつつ 利便性を向上することができます。
パッケージの階層とアクセスレベル
パッケージはドット区切りの階層的な名前が付けられますが、パッケージ名の階層とアクセスレベルの間には何の関係もないことに注意が必要です。
例えば firstというパッケージとfirst.secondというパッケージがあり、それぞれ次のようなクラスがあるとします。
- first.Sampleクラス:パッケージプライベートなクラス
- first.second.PublicClassクラス:publicなクラス
- first.second.PackagePrivateClassクラス:パッケージプライベートなクラス
firstパッケージとfirst.secondパッケージは 名前だけ見ると親子関係があるように見えてしまいますが、アクセスレベルには何の関係もありません。そのため、SampleクラスからPublicClassを利用することはできますが、PackagePrivateClassを利用することはできません。逆も同様で、PackagePrivateClassからSampleクラスにアクセスすることはできません。
継承とアクセスレベル
サブクラスでスーパークラスのメソッドをオーバーライドする場合、サブクラスのメソッドのアクセスレベルはスーパークラスのメソッドのアクセスレベルより狭めることはできません。反対に広めることはできます。
例えば publicなスーパークラスのメソッドをサブクラスでオーバーライドする場合、protected/パッケージプライベート/privateにすることはできません。一方で、パッケージプライベートなスーパークラスのメソッドをサブクラスでオーバーライドする場合は、privateにすることはできませんが、protected/publicにすることができます。
これは、リスコフの置換原則に基づいています。例えば もしpublicなスーパークラスのメソッドを サブクラスでオーバーライドしてprivateにできてしまうと、スーパークラスの型の変数にサブクラスのインスタンスを格納した場合に 該当メソッドが呼び出せなくなってしまうことになります。
各要素のアクセスレベル
各要素のアクセスレベルは概ね次のようになります。
クラス/インタフェースのアクセスレベル
クラス/インタフェースはpublicかパッケージプライベート以外は指定することができません。トップレベルのクラスやインタフェースは他のクラスで使われることが前提であり、クラス外へ非公開としても意味がないため、privateやprotectedは指定できないようになっています。
パッケージ外へ公開する場合はpublic、パッケージ外へ非公開の場合はパッケージプライベートにします。
フィールドのアクセスレベル
フィールドはカプセリングを実現するため、クラス外へ非公開(private)にすることが多くなります。
前述の通り 構造体のようなクラスで利便性を上げたい場合は クラス外へ公開することもできますが、その場合はpublicではなくパッケージプライベートにするのが望ましいです。
ただし、定数(プリミティブ型の定数や不変オブジェクトへのオブジェクト参照)の場合はpublicなクラスフィールドとするのが通例です。
ライブラリのように 他から使われることを目的としたパッケージで サブクラス化されることを意図しているクラスの場合、サブクラスからアクセスを許可したいフィールドはprotectedにします。
メソッドのアクセスレベル
メソッドはオブジェクト間のやり取りに利用されることが多いため、クラス外へ公開することが多くなります。そのクラスだけでしか利用しないメソッドはクラス外へ非公開(private)になります。
クラス外へ公開する場合、パッケージ外へ公開する場合はpublic、パッケージ外へ非公開の場合はパッケージプライベートにします。
ライブラリのように 他から使われることを目的としたパッケージで、サブクラス化されることを意図しているクラスの場合、サブクラスからのみアクセスを許可したいメソッドはprotectedにします。
コンストラクタのアクセスレベル
コンストラクタはオブジェクト生成の制御をしたいかどうかでアクセスレベルが異なってきます。オブジェクト生成の制御が特に必要なければ クラス外へ公開することが多くなります。オブジェクト生成の制御が必要な場合は コンストラクタをクラス外へ非公開(private)にします。
クラス外へ公開する場合、パッケージ外へ公開する場合はpublic、パッケージ外へ非公開の場合はパッケージプライベートにします。
ライブラリのように 他から使われることを目的としたパッケージで、サブクラス化されることを意図しているクラスの場合、サブクラスからのみアクセスを許可したいコンストラクタはprotectedにします。
メンバクラスのアクセスレベル
メンバクラスはトップレベルのクラスと違い、クラス外へ非公開(private)にすることもできます。
メンバクラスは 目的に応じてクラス外へ公開する場合もあれば、非公開(private)とする場合もあります。
クラス外へ公開する場合、パッケージ外へ公開する場合はpublic、パッケージ外へ非公開の場合はパッケージプライベートにします。
ライブラリのように 他から使われることを目的としたパッケージで、サブクラス化されることを意図しているクラスの場合、サブクラスからのみアクセスを許可したいメンバクラスはprotectedにします。
クラスフィールド、クラスメソッド
クラスフィールド
クラスのメンバであるフィールド・メソッドにstatic修飾子を付けるとクラスフィールド・クラスメソッドになります。
クラスフィールドは個々のインスタンスが持つフィールドではなく、そのクラスで1つのフィールドになり、そのクラスの全てのインスタンスが共有するフィールドになります。また、クラスフィールドはクラスをインスタンス化することなくアクセスすることができます。クラスフィールドにfinalを付けると そのクラス特有の定数として使うことができます。そのクラスで共有したいフィールドがある場合や、インスタンスを一つだけに限定したい場合(Singletonパターン)などにクラスフィールドが利用されます。
クラスフィールドは 初期値を与えずに定義するとデフォルト値で初期化されます。ただし、finalの付いたクラスフィールドは クラスの初期化が終了するまでに初期化をしないとコンパイルエラーになります。
インタフェースでもクラスフィールドを定義することができますが、変数ではなく定数として扱われます。(「public」、「static」、「final」の修飾子が暗黙のうちに付与されます。)
クラスメソッド
クラスメソッドは クラスをインスタンス化することなく呼び出せるメソッドになります。クラスメソッドは特定のインスタンスのメソッドを呼び出すわけではないので メソッド内でthisを参照することはできません。また、クラスメソッドにsynchronizedを付けた場合、ロックオブジェクトはクラスオブジェクトになります(例えば SampleというクラスのクラスオブジェクトはSample.classです)。
クラスフィールドやクラスメソッドへのアクセス
クラスフィールドやクラスメソッドにアクセスする場合は「クラス名.クラスフィールド」・「クラス名.クラスメソッド」の形でアクセスします。そのクラスのインスタンスにアクセスできる場合は「インスタンス変数名.クラスフィールド」・「インスタンス変数名.クラスメソッド」の形でアクセスすることもできますが、この形は推奨されません。(コンパイラが警告を発します)
クラスの初期化
クラスに初めてアクセスされる際に クラスが初期化されます。クラスの初期化で何か処理を行いたい場合には staticイニシャライザに処理を書くことができます。この章の「final修飾子」で説明した通り、finalの付いたクラスフィールドは宣言時かstaticイニシャライザのどちらかで初期化する必要があります。
クラスの初期化中のデッドロック
staticイニシャライザを実行しているスレッドとは 別のスレッドがクラスフィールドにアクセスする際には、staticイニシャライザが完了するまで待たされます。そのため、staticイニシャライザの中で 別スレッドを起動すると、思わぬデッドロックを引き起こす可能性があります。次のコードはデッドロックに陥ります。
public class StaticInitializeExample {
private static Integer integerValue;
static {
Thread th = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("run() in static initializer start.");
// staticイニシャライザが完了するまで
// integerValue へのアクセスは待たされる。
integerValue = 123;
System.out.println("run() in static initializer end.");
}
});
System.out.println("before thread start.");
// 別スレッド開始。
th.start();
try {
th.join(); // 別スレッドの終了を待つが、別スレッドは終わらない。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("static initializer done." + integerValue);
}
public static void main(String[] args) {
}
}
上のコードは、th.join()での待ち合わせをやめるか、別スレッドからintegerValueへのアクセスを行わないようにするかのどちらかで デッドロックを回避することができます。
継承
継承の基本事項
あるクラスのサブクラスを定義するには「extends」に続いてスーパークラスを指定します。
class SupserClass {
}
class SubClass extends SuperClass {
}
Javaではクラスの多重継承は行えないため、クラスのextendsで指定できるスーパークラスは1つだけになります。
一方でインタフェースは多重継承を行うことができます。複数のインタフェースをスーパークラスに指定する場合は、extendsに続いてカンマで区切ってスーパーインタフェースを列挙します。
interface SomeInterface extends SomeSuperInterface, OtherSuperInterface {
}
インタフェースの継承については「インタフェース」の章の「インタフェースの継承」で詳しく説明します。
暗黙的なコンストラクタ呼び出し
サブクラスのコンストラクタで明示的にスーパークラスのコンストラクタを呼び出さない場合は、暗黙的にスーパークラスのデフォルトコンストラクタが呼び出されます。したがって、次のようにスーパークラスにデフォルトコンストラクタがない場合は、明示的にスーパークラスのコンストラクタを呼び出さないと コンパイルエラーになります。
class SuperClass {
protected int id;
SuperClass(int id) { // コンストラクタを定義しているので、引数なしのデフォルトコンストラクタは存在しない。
…
}
}
class SubClass extends SuperClass {
String name;
SubClass(int id) {
// SuperClass にデフォルトコンストラクタがない旨のコンパイルエラー。
this.id = id;
}
}
メソッドのオーバーライド
スーパークラスのメソッドをサブクラスでオーバーライドすることができます。オーバーライドしたメソッドには @Overrideアノテーションを付けておくとコンパイラがチェックしてくれますし、コードの読み手に意図が伝わります。(IDEのコードアシスト機能でオーバーライドするメソッドの雛形を自動生成してくれます。これによって メソッドシグニチャを間違えて別のメソッドを定義してしまうようなミスを防ぐことができます。)
継承を制限するfinal修飾子
クラスやインタフェースの宣言にfinal修飾子を付けると、継承を禁止することができます。また、メソッドに final修飾子を付けると、サブクラスによる該当メソッドのオーバーライドを禁止することができます。
(フィールドの場合は継承のメカニズムとは関係なく、final修飾子を付けると初期化後は変更不可となります。)
アクセスレベルprotected
スーパークラスにて クラス外へ非公開(private)なメンバは、サブクラスに引き継がれますが サブクラスからアクセスすることはできません。C++では クラス外へ非公開(private)なメンバのアクセスレベルをprotectedにすることによって、サブクラスからアクセスできるようになります。
しかし、Javaの場合はprotectedの意味合いが少し異なります。Javaではパッケージ外へ非公開(パッケージprivate)なメンバのアクセスレベルをprotectedにすることによって、パッケージ外のサブクラスからアクセスできるようになります。同じパッケージ内であればprotectedでもパッケージprivateでも違いはありません。
あるクラスのメンバがprotectedである場合、別のパッケージから見るとC++のprotectedに相当しますが、同じパッケージのクラスから見るとC++のpublicに相当します。同じパッケージのクラスから見て C++のprotectedと同じアクセスレベルを実現できるアクセス修飾子はJavaにはありません。C++からJavaに移行した人は この点を誤解し易いので注意が必要です。
継承の詳細
継承によるコードの再利用における脆弱性
「オブジェクト指向プログラミング」の章の「継承」でも説明しましたが、継承の主な目的は サブタイピングとコードの再利用です。
サブタイピングによって スーパークラスとそのサブクラス群を同種の型として扱うことができたり、抽象的なコードを書くことによって サブクラスのインスタンスの置き換えを容易に行うことができます。また、スーパークラスに機能を追加したり 部分的に置き換えたりして、スーパークラスのコードを再利用しつつ サブクラスで拡張することができます。
継承はオブジェクト指向においてコードを再利用するための強力な方法ではありますが、サブクラスはスーパークラスの実装の詳細を知っている必要があります(理由は後述します)。また、一旦サブクラスが作成されるとスーパークラスは変更に対して慎重にならなければいけません。通常のクラス利用者に影響が出ない範囲でクラスを変更した場合でも、サブクラスには影響が出てしまうことがあります。このような サブクラスに影響の出る可能性のある変更を ここでは便宜上 有害可能性のある変更と呼ぶことにします。有害可能性のある変更は コンパイラで制限することはできません。コンパイルは通ってしまうので、実行して初めて不具合に気付くことになります。
有害可能性のある変更をしてしまうのは次のようなケースが挙げられます。
- 誤ってスーパークラスに有害可能性のある変更をしてしまう場合。(コンパイラで防げない)
- そもそも スーパークラスの実装者が 有害可能性のある変更であることを知らずにスーパークラスを変更してしまう場合。
- 継承を前提にしていないクラスからサブクラスを作成した場合。この場合は 有害可能性のある変更であるかどうかは全く意識せずに継承元であるクラスを変更してしまうでしょう。
このように 継承によるコードの再利用は、コンパイラで強制できない約束事の上で成り立っていて、場合によってはその約束事が意識すらされないため、とても脆弱な仕組みであると言えます。次の章では 有害可能性のある変更とはどのような物かを見ていきます。
有害可能性のある変更
有害可能性のある変更は、大きく2つ挙げられます。
- スーパークラスで自己利用の有無を変更する
- スーパークラスでメソッドを追加する
1番目の自己利用は、サブクラスの実装者はスーパークラスの実装の詳細を知っている必要があることの理由でもあります。それぞれについて詳しく見ていきます。
スーパークラスにおける自己利用
オブジェクト指向におけるカプセリングの特徴の一つは、メソッドを呼び出す側はメソッドの実装の詳細を知っている必要はないということです。しかし、サブクラスでオーバーライドする場合は スーパークラスのメソッドの実装の詳細を知らないと 期待する動作にならない場合があります。実装の詳細とは、具体的には自己利用の有無で 通常のクラス利用者は知る必要のない情報です。(これが継承がカプセリングを破ると言われる理由です)
自己利用の具体的な例を見てみます。HashSetを継承して 変更回数をカウントするクラスModCountHashSetを作成します。同じようにArrayListを継承して 変更回数をカウントするクラスModCountArrayListを作成します。
class ModCountHashSet<E> extends HashSet<E> {
private int modCount = 0;
@Override
public boolean add(E e) {
modCount ++;
return super.add(e);
}
@Override
public boolean addAll(Collection c) {
modCount += c.size();
return super.addAll(c);
}
public int getModCount() {
return modCount;
}
}
class ModCountArrayList<E> extends ArrayList<> {
private int modCount = 0;
@Override
public boolean add(E e) {
modCount ++;
return super.add(e);
}
@Override
public boolean addAll(Collection c) {
modCount += c.size();
return super.addAll(c);
}
public int getModCount() {
return modCount;
}
}
どちらも privateなmodCountフィールドを用意して、add()とaddAll()で modCountに追加する要素数を加算しています。この2つのクラスを利用すると 想定外の結果になります。
public static void main(String[] args) {
ModCountHashSet<String> set = new ModCountHashSet<>();
set.addAll(List.of("One", "Two", "Three"));
System.out.println("ModCountHashSet:" + set.getModCount()); // ModCountHashSet:6
ModCountArrayList<String> list = new ModCountArrayList<>();
list.addAll(List.of("One", "Two", "Three"));
System.out.println("ModCountArrayList:" + list.getModCount()); // ModCountArrayList:3
}
同じようにどちらにも3つの要素を追加したにも関わらず、ModCountHashSetの方は6、ModCountArrayListの方は3になってしまいます。これは、それぞれのスーパークラスであるHashSetとArrayListのaddAll()の実装が異なることによります。HashSetのaddAll()はHashSetのadd()を呼び出しています。そのため 要素数の2倍カウントされてしまいます。一方でArrayListのaddAll()は性能向上のためArrayListのadd()を呼び出していません。そのため、要素数だけカウントされています。
HashSetのaddAll()のように、メソッドの内部で 別のオーバーライド可能な自身のメソッドを呼び出すことを自己利用(self-use)と言います。HashSetのaddAll()は自己利用をしていて、ArrayListのaddAll()は自己利用をしていません。この例では自己利用をしているHashSetを継承したModCountHashSetが期待通りの動作になっていないのですが、だからと言って自己利用が悪いというわけではありません。自己利用をしていることが分かっていれば、それを前提にした実装にすれば 期待通りの動作にすることはできます。ここで大事なのは、オーバーライドする際には 自己利用をしているかしていないかを知らないと 期待通りの動作にならないことがあるということです。
繰り返しになりますが、自己利用はメソッドの実装の詳細であるため、メソッドの呼び出し側は知る必要のない情報です。しかし、オーバーライドを行う場合は 自己利用の有無を知っている必要があります。これが 継承がカプセリングを破ると言われる理由です。
また、カプセリングの特徴の一つは、メソッドの実装側はメソッドシグニチャやメソッドの機能が変更にならない範囲であれば メソッドの呼び出し側に影響を与えることなく 実装の詳細を変更する自由があるということです。しかし、スーパークラスが自己利用の有無を変更すると サブクラスに影響が出てしまうため、この自由が奪われることになります。
以上のことから、スーパークラスのメソッドでの自己利用の有無の変更は 有害可能性のある変更になります。
スーパークラスでメソッドを追加
もう一つの 有害可能性のある変更はスーパークラスでのメソッド追加です。通常のクラス利用者にとっては、メソッドシグニチャやメソッドの機能が変更されたり、メソッドが削除されるとなんらかの影響が出ますが、メソッドの追加に対しては基本的には影響を受けません。しかし、一旦サブクラスが作成されると メソッド追加によってサブクラスに影響が出る場合があります。
先ほどのArrayListのサブクラスModCountArrayListを例に挙げます。ModCountArrayListではadd()やaddAll()をオーバーライドして変更回数をカウントしています。例えば ArrayListにaddFirst()とaddLast()という 位置を指定して要素を追加するメソッドが追加されたとします。その場合には ModCountArrayListはaddFirst()とaddLast()をオーバーライドしないと変更回数を正しくカウントできません。しかし、ModCountArrayListの管理者がArrayListの変更を知らなければ、ModCountArrayListはいつまでも 期待通りに動作しないままになってしまいます。ArrayListとModCountArrayListの管理者が別であるため、ModCountArrayListは常にArrayListの変更に追随できるわけではありません。
ここでは 変更カウントという あまりセンシティブでない機能追加を例に挙げましたが、オーバーライドで排他制御を追加しているような場合は スレッドセーフを保証できなくなってしまいますし、オーバーライドでセキュリティチェックを追加しているような場合は セキュリティホールが生まれてしまいます。いずれも重大な問題を引き起こしてしまう可能性があります。
また、サブクラスで独自にメソッドを追加した後に、偶然にもスーパークラスで同じメソッドシグニチャのメソッドを追加してしまう場合も考えられます。そのような場合には メソッドの戻り値が異なれば サブクラスでコンパイルが通らなくなりますし、戻り値が同じであれば サブクラスのメソッドは意図せずオーバーライドすることになります。意図せずオーバーライドすることになった場合でも、スーパークラスが定めた一般契約に適合するとは限りません。いずれにしても 偶然が重ならない限りは 何らかの不具合が発生する可能性が高いです。
継承によるコードの再利用を安全に行える場合
継承によるコードの再利用では スーパークラスで有害可能性のある変更を行うと サブクラスに影響を与える危険性があることを見てきました。それでは、継承によるコードの再利用は危険で全く使い物にならないかというと 勿論そういう訳でもなく 安全に利用できる場合もあります。
次のようなケースでは 継承によるコードの再利用を安全に行うことができます。
- スーパークラスとサブクラスが同じ開発元の管理下にある場合。(基本的には同一パッケージ内)
スーパークラスで有害可能性のある変更を行う場合でも、同時にサブクラスに修正を加えることができるためです。 - 継承されることを前提に設計され、継承のために必要なドキュメントが整備されているクラス(抽象クラスであることも多い)から継承する場合。
継承を前提に設計されたクラスは 自己利用の影響と有害可能性のある変更を理解している筈です。そのため、自己利用を明記してあることが期待できますし、有害可能性のある変更が行われないことも期待できます。
継承に代わるコード再利用のパターン:コンポジション
継承によるコードの再利用には 脆弱性があることを見てきました。それらの脆弱性を持たない 継承に代わるコード再利用のパターンがあります。コンポジションとかDecoratorパターンなどと呼ばれるパターンです。
一般的なコンポジションでは、4つのクラスとインタフェースが登場します。Effective Javaの中では特に名付けられていないものもありますが、分かり易いように ここでは便宜上次のように名前を付けました。
- 再利用元クラス(便宜上名前を付けました)
- 再利用元機能インタフェース(便宜上名前を付けました)
- 転送クラス
- ラッパークラス(プリミティブ型をラッピングするInteger等のクラスとは別物です)
1番目の再利用元クラスは コードを再利用したいクラスで、前の例ではArrayListやHashSetなどが該当します。継承でコードを再利用する場合のスーパークラスが該当します。
2番目の再利用元機能インタフェースは 再利用したいメソッド群を表すインタフェースです。再利用元クラスがこのインタフェースを実装している場合もあります。前の例ではArrayListにおけるListインタフェースが該当します。一方で 再利用元クラスがこのインタフェースを実装していない場合もあります。例えばjava.util.Dateクラスのコードを再利用したい場合などです。その場合は再利用したいメソッド群を表すインタフェースを一つ用意します。
3番目の転送クラスは privateなフィールドで 1番目の再利用元クラスのオブジェクトへの参照を持ちます。そして、2番目の再利用元機能インタフェースを実装しますが、各メソッドでは再利用元クラスの対応するメソッドを呼び出し、その結果をそのまま返します。この仕組みは転送と呼ばれます。
最後のラッパークラスは3番目の転送クラスを継承して、適宜メソッドをオーバーライドして必要な機能(変更カウント、排他制御、セキュリティチェックなど)を追加します。継承でコードを再利用する場合のサブクラスが該当します。
前の例のModCountArrayListをコンポジションで書き換えてみます。再利用元クラスはArrayListです。再利用元機能インタフェースはListです。転送クラスForwardingListは次のようになります。メソッドが多いので必要な部分だけ抜粋します。
class ForwardingList<E> implements List<E> {
private List<E> list;
public ForwardingList(List<E> list) {
this.list = list;
}
@Override
public boolean add(E e) {
return list.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return list.addAll(c);
}
// 他も同様。
}
ラッパークラスModCountは次のようになります。転送クラスForwardingListを継承します。
class ModCountArrayList<E> extends ForwardingList<E> {
private int modCount = 0;
public ModCountArrayList(List<E> list) {
super(list);
}
@Override
public boolean add(E e) {
modCount ++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
modCount += c.size();
return super.addAll(c);
}
}
コンポジションは継承によるコードの再利用における脆弱性とは無縁です。ラッパークラスは再利用元クラスの自己利用を知っている必要もなく、再利用元クラスが自己利用の有無を変更してもラッパークラスは影響を受けません。また、再利用元クラスがメソッドを追加した場合でも、やはりラッパークラスは影響を受けません。
ただし、再利用元機能インタフェースにデフォルトメソッドを追加した場合は、継承時にスーパークラスにメソッドを追加した場合と同様の影響があるため注意が必要です。詳細は「インタフェース」の章の「デフォルトメソッド」で説明します。
コンポジションにも欠点があります。コンポジションはラッパークラスを中継して再利用元クラスを利用するため、ラッパークラスに追加機能を挟み込める仕組みですが、追加機能を挟み込めない場合もあります。それは再利用元クラスが コールバックフレームワークに自身を登録する場合です。再利用元クラスはラッパークラスの存在は知らないため、コールバックフレームワークには再利用元クラス自身を登録します。コールバックフレームワークでは登録された再利用元クラスのメソッドを直接呼び出すことになるため、ラッパークラスはこの間に入り込むことができません。これはSELF問題として知られています。
不適切な継承:is-aの関係がない
基本的には継承はis-aの関係が成立する場合に適用できます。しかし、is-aの関係がないにも関わらず、コードの再利用だけを目的に継承を行うと 綻びが生じがちです。is-aの関係がない不適切な継承として、java.utilパッケージのVectorを継承したStackと、Hashtableを継承したPropertiesが挙げられます。
StackはLIFOであり、pop()・push()・peek()と言ったスタック固有のメソッドを提供しています。しかし、Vectorを継承しているため add()・remove()などを使って任意の位置に要素を追加したり 任意の位置の要素を削除したりできてしまいます。そのため 使い方を間違えると適切に動作しなくなってしまう脆い仕組みであると言えます。
また、Propertiesはキーと値がStringであり、setProperty()・getProperty()などのメソッドを提供しています。しかし、Hashtableを継承しているためput()・putAll()などを使ってString以外のキーや値のプロパティを追加することができてしまいます。その状態でstore()メソッドを呼び出すとClassCastExceptionが発生して失敗します。こちらも 使い方を間違えると適切に動作しなくなってしまいます。
is-aの関係がなく コードの再利用だけを目的とした継承を行うと このような問題が出てきます。このような問題に対しても コンポジションは有効です。ただし、StackもPropertiesも 適切な再利用元機能インタフェースを実装していないため 別途定義する必要があります。StackはListインタフェースを実装していますが、Stack固有の機能を表すインタフェースが別途必要です。同様にPropertiesはMapインタフェースを実装していますが、Properties固有の機能を表すインタフェースが別途必要です。
継承を前提としたpublicなクラスの設計
継承を前提としたpublicなクラスを設計する際には 次のようなポイントを踏まえる必要があります。
- オーバーライド可能なメソッドの自己利用を明記する。
- 必要に応じて protectedなメンバを用意してサブクラスに公開する。
- コンストラクタ(及び隠れたコンストラクタ)においてオーバーライド可能なメソッドを呼び出さない。
また、継承を前提としたクラスを一旦公開した後は、前述したような有害可能性のある変更は行わないようにしないといけません。
3つのポイントについて それぞれ詳しく見ていきます。
オーバーライド可能なメソッドの自己利用を明記する
前の章で サブクラスがスーパークラスのメソッドをオーバーライドする場合には、スーパークラスのメソッドにおける自己利用の有無を知っている必要がある理由を説明しました。ここではスーパークラスでどのように自己利用を明記するかをまとめます。
ドキュメント化にはJavaDocを利用します。記述する対象は自己利用をされているメソッドではなく、自己利用をしているメソッドになります。つまり オーバーライド可能な自身のメソッドを呼び出しているメソッド全てに自己利用の説明を記述します。説明を記述する箇所はメソッドのJavaDocの@implSpecタグを付けたセクションになります。説明内容は どのメソッドをどのような順番で呼び出していて、それぞれの呼び出しが後の処理にどのように影響するかといった詳細を記載します。
Javaの標準ライブラリのJavaDocにも 実装要件として自己利用の内容を記載しているメソッドがありますので、記載内容はそれらを参考にすることができます。
スーパークラスの自己利用を排除する
スーパークラスでオーバーライド可能なメソッドの自己利用を明記する代わりに、自己利用を排除するという選択をすることもできます。自己利用されているメソッドは、privateな(オーバーライド不可能な)ヘルパメソッドを用意することによって 次のように機械的に取り除くことができます。
- まず 自己利用されているメソッドの処理をそのままヘルパメソッドへ移します。
- 自己利用されているメソッドではヘルパメソッドを呼び出すように変更します。
- 自己利用をしているメソッドでは呼び出し先をヘルパメソッドに変更します。
自己利用されていたメソッドが1クッション挟むようになりますが、とても簡単に置き換えることができます。自己利用を排除することにより 実装の詳細を明記する必要がなくなるため、自己利用の除去は有力な選択肢になります。
必要に応じて protectedなメンバを用意してサブクラスに公開する
サブクラスで効率的に拡張を行うために protectedなメンバを用意してサブクラスに公開する必要があるかも知れません。カプセル化の観点から フィールドを公開するのは望ましくなく、基本的にはメソッドを公開します。例えば スーパークラスのあるメソッドが一連の処理から成り立っていて、サブクラスではそのうち部分的に置き換えや拡張ができれば良いような場合であれば、局所化した部分をprotectedなメソッドとして公開するようなケースが挙げられます。TemplateMethodパターンのようなイメージで、内部動作へのフックを提供していると言えます。
protectedのメンバは 他のパッケージの不特定多数のサブクラスからアクセス可能になるため、むやみやたらに公開して良いわけではありません。例えば セキュリティチェックを行うようなメソッドをprotectedとして公開してしまうと セキュリティチェックを無効にするようにオーバーライドされてしまい セキュリティホールが生まれてしまうため注意が必要です。
何をprotectedなメンバとして公開するかの指針を示すのは難しいのですが、サブクラスでどのような拡張が行われそうかを推測して 必要最低限なメンバだけを公開するように 慎重な検討が必要です。
コンストラクタ(及び隠れたコンストラクタ)においてオーバーライド可能なメソッドを呼び出さない
これについては この章の「コンストラクタからオーバーライド可能なメソッドを呼び出さない」で説明しましたので そちらを参照してください。
また、隠れたコンストラクタでも同様にオーバーライド可能なメソッドを呼び出さないようにする必要があります。隠れたコンストラクタについては この章の「隠れたコンストラクタ」を参照してください。
継承を前提としないpublicなクラス
継承を前提としないpublicなクラスの中でも、サブクラスが作成されると困るクラスでは明示的に継承を禁止する必要があります。不変クラスなどがそのようなクラスに該当します。継承を禁止するには2つの方法があります。
- クラスをfinalとして定義する。または、コンストラクタをprivateにする。
パッケージ外は勿論、パッケージ内でも継承を禁止することができます。 - コンストラクタをパッケージprivateにする。
パッケージ内での継承は許可しつつ、パッケージ外での継承を禁止することができます。
また、サブクラスが作成されても困らないようなクラスでも、publicなクラスであれば サブクラスへの影響が少なくなるような設計をしておくのが無難です。サブクラスへの影響が少なくなるような設計とは、オーバーライド可能なメソッドの自己利用を行わないようにすることです。もしオーバーライド可能なメソッドの自己利用があれば、ヘルパメソッドで置き換えることによって自己利用を取り除くことができます。
ただし、ヘルパメソッドへの置き換えをしても 自己利用によるサブクラスへの影響は防げますが、継承元のクラスにメソッドを追加することによるサブクラスへの影響を防ぐことはできません。完全に影響が出ないようにできるわけではないことに注意が必要です。