『Effective Java (3rd Edition)』に学ぶベストプラクティス
Joshua Bloch著『Effective Java (3rd Edition) 』からの抜粋
1. nullではなく、空のコレクションか空の配列を返す
メソッドがSetを返すなら、結果が空のときにreturn null;とせず、代わりにreturn Set.of();としましょう。
2. コンストラクタの代わりにstaticファクトリメソッドを検討する
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
常に優れているわけではありませんが、用途によっては多くの利点があります:
- staticファクトリメソッドの利点のひとつは、コンストラクタと違って名前を持てることです。
- 呼び出されるたびに新しいオブジェクトを生成する必要がありません。
- 戻り値の型の、任意のサブタイプのオブジェクトを返せます。
- 返すオブジェクトのクラスを、入力パラメーターに応じて呼び出しごとに変えられます。
- 返すオブジェクトのクラスは、そのメソッドを含むクラスを書く時点で存在していなくてもかまいません。
欠点:
- staticファクトリメソッドだけを提供する場合の主な制約は、publicまたはprotectedのコンストラクタを持たないクラスはサブクラス化できないことです。
- staticファクトリメソッドの2つ目の欠点は、プログラマーが見つけにくいことです。
3. コンストラクタのパラメーターが多いときはビルダーを検討する
staticファクトリとコンストラクタには共通の制約があります。省略可能なパラメーターが多くなると、うまく対応できないのです。私が関わったいくつかのプロジェクトは、この問題にひどく苦しめられました。
4. privateコンストラクタかenum型でシングルトンを強制する
複数のインスタンスを必要としないものは、一度だけインスタンス化すべきです。@ApplicationScopedを使うにせよ、別のシングルトンパターンを使うにせよ、です。
5. privateコンストラクタでインスタンス化不可能にする
staticメソッドとstaticフィールドをまとめただけのクラスを書きたくなることがあります。オブジェクトの観点で考えるのを避けるためにこうしたクラスを乱用する人がいるせいで評判は良くありませんが、正当な用途も確かにあります。
private MyStaticClass() {
// private constructor to prevent instantiation
}
6. 不要なオブジェクトの生成を避ける
String s = new String("bikini"); // DON'T DO THIS!
あるいは:
// Hideously slow! Can you spot the object creation?
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
解答:このプログラムは正しい答えを出しますが、1文字のタイプミスのせいで、本来よりもはるかに遅くなっています。変数sumがlongではなくLongとして宣言されているため、プログラムは約2^31個もの不要なLongインスタンスを生成してしまいます(long iをLong sumに加算するたびに、ほぼ1個ずつ)。
7. ファイナライザとクリーナーを避ける
ファイナライザは予測不能で、しばしば危険であり、たいていは不要です。使うと動作が不安定になったり、パフォーマンスが落ちたり、移植性の問題が生じたりします。
8. try-finallyよりもtry-with-resourcesを使う
// try-with-resources - the best way to close resources!
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
9. Comparableの実装を検討する
Comparableを実装すれば、このインターフェイスに依存する数多くのジェネリックなアルゴリズムやコレクション実装と、クラスを連携させられるようになります。
10. クラスとメンバーのアクセス範囲を最小限にする
よく設計されたコンポーネントとそうでないコンポーネントを分ける最も重要な要素は、そのコンポーネントが内部データやその他の実装の詳細を、ほかのコンポーネントからどれだけ隠しているかです。よく設計されたコンポーネントは実装の詳細をすべて隠し、APIと実装をきれいに分離しています。コンポーネント同士はAPIを通じてのみやり取りし、互いの内部の仕組みには関知しません。情報隠蔽またはカプセル化と呼ばれるこの考え方は、ソフトウェア設計の基本原則です。
11. publicクラスでは、publicフィールドではなくアクセサメソッドを使う
インスタンスフィールドをまとめる以外に何の役目もない、退化したクラスを書きたくなることがあるかもしれません:
// Degenerate classes like this should not be public!
class Point {
public double x;
public double y;
}
こうしたクラスのデータフィールドには直接アクセスされるため、カプセル化の恩恵を受けられません。APIを変えずに内部表現を変えることはできず、不変条件を強制することもできず、フィールドにアクセスされたときに補助的な処理を行うこともできません。オブジェクト指向に厳格なプログラマーは、こうしたクラスを忌まわしいものとみなし、privateフィールドとpublicなアクセサメソッド(ゲッター)、さらに可変クラスであればミューテーター(セッター)を備えたクラスに必ず置き換えるべきだと考えます:
// Encapsulation of data by accessor methods and mutators
class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public void setX(double x) { this.x = x; }
public void setY(double y) { this.y = y; }
}
publicクラスに関しては、確かに厳格派の言い分が正しいと言えます。クラスがパッケージの外からアクセスできるなら、クラスの内部表現を変更できる柔軟性を保つために、アクセサメソッドを用意しましょう。publicクラスがデータフィールドを公開してしまうと、クライアントのコードがあちこちに広まっている可能性があるため、内部表現を変える望みは完全に絶たれます。
しかし、クラスがパッケージプライベートであるか、privateのネストしたクラスであれば、データフィールドを公開すること自体に本質的な問題はありません。ただし、それらのフィールドがクラスの提供する抽象化をきちんと表していることが前提です。
12. 可変性を最小限にする
不変クラスとは、単にインスタンスを変更できないクラスのことです。各インスタンスが持つ情報はオブジェクトの生存期間中ずっと固定されているので、変化が観測されることはありません。Javaプラットフォームのライブラリには、String、ボクシングされた基本データ型のクラス、BigInteger、BigDecimalなど、多くの不変クラスが含まれています。これには十分な理由があります。不変クラスは可変クラスよりも設計、実装、利用が簡単で、エラーが起きにくく、より安全なのです。クラスを不変にするには、次の5つのルールに従います:
- オブジェクトの状態を変更するメソッドを提供しない(このようなメソッドはミューテーターと呼ばれます)。
-
クラスを拡張できないようにする。これにより、不注意な、あるいは悪意のあるサブクラスが、あたかもオブジェクトの状態が変わったかのように振る舞って、クラスの不変性を損なうことを防げます。サブクラス化を防ぐには、一般にクラスを
finalにするなどの方法をとります。 - すべてのフィールドをfinalにする。こうすれば、システムによって強制される形で意図をはっきり示せます。また、メモリモデルで説明されているとおり、新しく生成されたインスタンスへの参照が同期なしにスレッド間で受け渡される場合に、正しい動作を保証するためにも必要です。
- すべてのフィールドをprivateにする。これにより、フィールドが参照する可変オブジェクトにクライアントがアクセスして直接変更することを防げます。基本データ型の値や不変オブジェクトへの参照を持つpublic finalフィールドを不変クラスに持たせることは、技術的には許されますが、推奨されません。後のリリースで内部表現を変更できなくなるからです。
-
可変のコンポーネントへのアクセスを排他的にする。クラスに可変オブジェクトを参照するフィールドがある場合は、クラスのクライアントがそのオブジェクトへの参照を取得できないようにします。こうしたフィールドを、クライアントから渡されたオブジェクト参照で初期化したり、アクセサからそのまま返したりしてはいけません。コンストラクタ、アクセサ、
readObjectメソッドでは防御的コピーを作りましょう。
13. 継承よりコンポジションを選ぶ
継承はコードを再利用するための強力な手段ですが、常に最適な道具とは限りません。不適切に使うと、壊れやすいソフトウェアにつながります。サブクラスとスーパークラスの実装が同じプログラマーの管理下にあるパッケージ内であれば、継承を使っても安全です。拡張のために設計され、文書化されたクラスを拡張する場合も安全です。しかし、パッケージの境界を越えて普通の具象クラスを継承するのは危険です。
メソッド呼び出しとは異なり、継承はカプセル化を破ります。新しいメソッドを追加するだけで既存のメソッドをオーバーライドしなければ、クラスを拡張しても安全だと思うかもしれません。この種の拡張ははるかに安全ではありますが、リスクがないわけではありません。後のリリースでスーパークラスに新しいメソッドが加わり、運悪くサブクラスに同じシグネチャで戻り値の型だけが異なるメソッドを定義していた場合、サブクラスはもうコンパイルできなくなります。サブクラスに、新しいスーパークラスのメソッドとシグネチャも戻り値の型も同じメソッドを定義していた場合は、それをオーバーライドしていることになり、先に述べた問題にさらされます。さらに、そのメソッドが新しいスーパークラスのメソッドの契約を満たしているかどうかも疑わしいでしょう。サブクラスのメソッドを書いた時点では、その契約はまだ書かれていなかったのですから。
幸い、上で述べた問題をすべて回避する方法があります。既存のクラスを拡張する代わりに、既存クラスのインスタンスを参照するprivateフィールドを新しいクラスに持たせるのです。既存のクラスが新しいクラスの構成要素(コンポーネント)になるため、この設計はコンポジションと呼ばれます。新しいクラスの各インスタンスメソッドは、保持している既存クラスのインスタンスの対応するメソッドを呼び出し、その結果を返します。これは転送と呼ばれ、新しいクラスのメソッドは転送メソッドと呼ばれます。こうしてできたクラスは、既存クラスの実装の詳細に依存しない、きわめて堅牢なものになります。既存のクラスに新しいメソッドが追加されても、新しいクラスには何の影響もありません。
14. 抽象クラスよりもインターフェイスを選ぶ
Javaには、複数の実装を許す型を定義する仕組みが2つあります。インターフェイスと抽象クラスです。Java 8でインターフェイスにデフォルトメソッドが導入されて以来、どちらの仕組みでも一部のインスタンスメソッドの実装を提供できるようになりました。
大きな違いは、抽象クラスが定義する型を実装するには、クラスがその抽象クラスのサブクラスでなければならないことです。Javaは単一継承しか認めていないため、この制約によって抽象クラスを型定義として使う場面は大きく制限されます。一方インターフェイスは、必要なメソッドをすべて定義し、一般契約に従っていれば、クラス階層のどこにあるクラスでも実装できます。既存のクラスに新しいインターフェイスを実装させるのも簡単です。
- インターフェイスはミックスインの定義に最適です。
- インターフェイスを使えば、階層構造を持たない型のフレームワークを構築できます。
- インターフェイスは、安全で強力な機能拡張を可能にします。ラッパークラスのイディオムを使えばよいのです。抽象クラスで型を定義してしまうと、機能を追加したいプログラマーには継承以外の選択肢が残りません。
15. ジェネリクス:raw型を使わない
エラーは実行時まで発見されません。エラーが起きてからずっと後になり、しかもエラーの原因となったコードから遠く離れた場所で発覚することもあります。ClassCastExceptionが出たら、切手のコレクションに硬貨を紛れ込ませたメソッド呼び出しを探して、コードベースをくまなく調べるはめになります。コンパイラは助けてくれません。
16. ジェネリクス:未検査警告をなくす
ジェネリクスを使ってプログラミングすると、多くのコンパイラ警告に出くわします:
- 未検査キャスト警告
- 未検査メソッド呼び出し警告
- 未検査のパラメーター化された可変長引数型の警告
- 未検査変換警告
ジェネリクスの経験を積むほど警告は減っていきますが、新しく書いたコードが警告なしにきれいにコンパイルされるとは期待しないでください。未検査警告の多くは簡単に取り除けます。例えば、うっかり次のような宣言を書いてしまったとします:
Set<Lark> exaltation = new HashSet();
これを次のように直します:
Set<Lark> exaltation = new HashSet<>();
警告の中には、取り除くのがはるかに難しいものもあります。少し考える必要がある警告に出会っても、粘り強く取り組みましょう!取り除ける未検査警告は、すべて取り除きましょう。
警告をすべて取り除けば、コードが型安全であることが保証されます。これはとても良いことです。実行時にClassCastExceptionが発生しないということであり、プログラムが意図どおりに動くという確信も高まります。警告を取り除けないが、警告の原因となったコードが型安全であると証明できる場合に限り(その場合に限り)、@SuppressWarnings("unchecked")アノテーションで警告を抑制しましょう。コードが型安全であることを証明せずに警告を抑制すると、誤った安心感を抱くことになります。コードは警告を出さずにコンパイルされるかもしれませんが、それでも実行時にClassCastExceptionをスローする可能性があります。
@SuppressWarnings("unchecked")アノテーションを使うときは、毎回、なぜそうしても安全なのかをコメントに書きましょう。
17. ジェネリクス:意味のある場面で使う
痛い――ジェネリクスがどうしても必要なクラス:
こういうものは、遅くともマージリクエストの段階で見つけなければなりません!
18. ジェネリクス:やってはいけない使い方
すみません、言葉もありません。これは絶対にマージしないでください:
public abstract class JpaLearningCardBaseContentService<LC extends LearningCardBaseContent<L, LC, AC>, J extends JpaLearningCardBaseContent<L, LC, AC>, L extends
LearningCardBase<LC, A>, A extends AnswerOptionBase<L, AC>, AC extends AnswerOptionBaseContent<LC, AC, A>, AS extends AnswerOptionBaseService<A, AC, L, LC>, ACS extends
AnswerOptionBaseContentService<AC, A, L, LC>, LS extends LearningCardBaseService<L>>
extends JpaLightPersistenceService<LC, J, Long> implements LearningCardBaseContentService<LC, L, A, AC> {
// ...
}
クラスの宣言ひとつを理解するのに5分もかかるようではいけません。
19. 配列よりもリストを選ぶ
配列とジェネリック型には、重要な違いが2つあります。第一に、配列は共変です。この恐ろしげな言葉が意味するのは、単にSubがSuperのサブタイプであれば、配列型Sub[]も配列型Super[]のサブタイプになる、ということです。一方、ジェネリクスは不変(invariant)です。異なる2つの型Type1とType2について、List<Type1>はList<Type2>のサブタイプでもスーパータイプでもありません。
20. 匿名クラスよりもラムダを選ぶ
// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
代わりに、こうしましょう:
// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
21. ローカル変数のスコープを最小限にする
ローカル変数のスコープを最小限にする最も強力な方法は、最初に使う場所で宣言することです……
22. 従来のforループよりもfor-eachループを選ぶ
// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
... // Do something with e
}
23. ライブラリを知り、使う
0からある上限までのランダムな整数を生成したいとします。このよくある課題を前にすると、多くのプログラマーは次のような小さなメソッドを書くでしょう:
// Common but deeply flawed!
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}
このメソッドは良さそうに見えますが、3つの欠陥があります。1つ目は、nが小さな2のべき乗の場合、乱数の並びがかなり短い周期で繰り返されることです。2つ目の欠陥は、nが2のべき乗でない場合、平均すると一部の数がほかの数よりも頻繁に返されることです。nが大きいと、この影響はかなり顕著になります。
randomメソッドの3つ目の欠陥は、まれに致命的な失敗をして、指定した範囲外の数を返すことがある点です。これは、このメソッドがrnd.nextInt()の戻り値を、Math.absを呼び出して負でないintに対応させようとしているためです。nextInt()がInteger.MIN_VALUEを返すと、Math.absもInteger.MIN_VALUEを返し、nが2のべき乗でなければ、剰余演算子(%)は負の数を返します。これはほぼ確実にプログラムの失敗を招き、しかもその失敗は再現が難しいかもしれません。
24. 正確な答えが必要なら、floatとdoubleを避ける
金額にはBigDecimalを使いましょう。floatとdoubleでは丸め誤差が生じます。
例えば、ポケットに1.03ドルあって、42セント使ったとします。残りはいくらでしょうか?この問いに答えようとする素朴なプログラムの断片がこれです:
System.out.println(1.03 - 0.42);
残念ながら、出力は0.6100000000000001になります。これは特殊な例ではありません。ポケットに1ドルあって、1個10セントの座金を9個買ったとします。おつりはいくらでしょうか?
System.out.println(1.00 - 9 * 0.10);
このプログラムの断片によると、おつりは$0.09999999999999998です。
出力する前に結果を丸めれば問題は解決すると思うかもしれませんが、残念ながら、それでいつもうまくいくとは限りません。例えば、ポケットに1ドルあり、棚に10セント、20セント、30セント……と1ドルまでの値段のおいしそうなキャンディーが並んでいるとします。10セントのものから順に1個ずつ買っていき、棚の次のキャンディーが買えなくなるまで続けます。
25. ボクシングされた基本データ型よりも基本データ型を選ぶ
// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
26. オブジェクトはインターフェイスで参照する
良い例:
Set<Son> sonSet = new LinkedHashSet<>();
悪い例:
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
27. リフレクションよりもインターフェイスを選ぶ
そうしないと:
- 例外のチェックも含め、コンパイル時の型チェックの恩恵をすべて失います。存在しないメソッドやアクセスできないメソッドをリフレクションで呼び出そうとすると、特別な対策を講じていない限り、実行時に失敗します。
- リフレクションによるアクセスに必要なコードは、ぎこちなく冗長です。書くのは面倒で、読むのも大変です。
- パフォーマンスが落ちます。リフレクションによるメソッド呼び出しは、通常のメソッド呼び出しよりもはるかに遅いのです。
28. 一般に受け入れられている命名規則に従う
Javaプラットフォームには確立された命名規則があり、その多くはJava言語仕様(The Java Language Specification)[JLS, 6.1]に記されています。
29. 例外は例外的な状況にのみ使う
// Horrible abuse of exceptions. Don't ever do this!
try {
int i = 0;
while (true) {
range[i++].climb();
}
} catch (ArrayIndexOutOfBoundsException e) {
// catch it
}
このコードは何をしているのでしょうか?ぱっと見てもまったくわかりません。それだけでも、使わない理由として十分です。実はこれは、配列の要素をループで処理するための、ひどく見当違いなイディオムなのです。この無限ループは、配列の範囲外にある最初の要素にアクセスしようとしたときにArrayIndexOutOfBoundsExceptionをスローし、それをキャッチして無視することで終了します。本来は、配列をループで処理する標準的なイディオムと同じことをするはずのものです。標準的なイディオムなら、Javaプログラマーなら誰でもすぐに見てわかります:
for (Mountain m : range) {
m.climb();
}
例外は、その名のとおり例外的な状況にのみ使うべきです。通常の制御フローに使ってはいけません。
30. 回復可能な状況にはチェック例外を、プログラミングエラーには実行時例外を使う
呼び出し側が回復できると合理的に期待できる状況には、チェック例外を使いましょう。チェック例外をユーザーに突きつけることで、API設計者はその状況から回復するよう求めていることになります。ユーザーは例外をキャッチして無視することでこの要求を無視することもできますが、たいていは良くない考えです。
プログラミングエラーを示すには、実行時例外を使いましょう。実行時例外の大半は、事前条件違反を示しています。事前条件違反とは、APIのクライアントが、API仕様で定められた契約を守らなかったというだけのことです。例えば、配列アクセスの契約では、配列のインデックスは0以上、配列の長さ−1以下でなければならないと定められています。ArrayIndexOutOfBoundsExceptionは、この事前条件が破られたことを示します。
31. チェック例外の不必要な使用を避ける
まとめると、控えめに使えば、チェック例外はプログラムの信頼性を高めます。使いすぎると、APIは使うのが苦痛なものになります。
Pair<Boolean, LearningCardGroup> answerLearningCard(@NotNull LearningCardGroup learningCardGroup,
LearningCardAssignment learningCardAssignment,
@NotNull Collection<AnswerOption> answers)
throws InvalidTrainingStateException, PersistenceException, LearningCardGroupAlreadyCompletedException,
ConsecutiveCorrectCountExceededException,
PreviousLearningCardGroupUncompletedException, LearningCardGroupCooldownException,
LearningCardGroupHasUncompletedLearningCardsException, InvalidParticipantException, TrainingNotSignedException; // but why ???
// this is a mistake seen a lot with junior devs
// exceptions are exceptionally slow and should be avoided for mere validation
// the performance gets even worse if a stacktrace is printed, since logging and printing is one of the slowest possible actions
32. 標準の例外を使う
熟練したプログラマーとそれほど経験のないプログラマーを分ける特徴のひとつは、熟練者が高いレベルのコード再利用を目指し、たいていそれを実現していることです。
コードの再利用は良いことだ、というルールに、例外も例外ではありません。Javaのライブラリには、ほとんどのAPIが必要とする例外のほとんどをカバーする一連の例外が用意されています。標準の例外を再利用することには、いくつかの利点があります。何より大きいのは、プログラマーがすでに慣れ親しんでいる確立された慣習に合うため、APIが学びやすく使いやすくなることです。それに次ぐのは、見慣れない例外で散らかっていないため、APIを使うプログラムが読みやすくなることです。最後に(そして最も小さな利点として)、例外クラスが少なければ、メモリ使用量が減り、クラスのロードにかかる時間も短くなります。
最もよく再利用される例外の型はIllegalArgumentExceptionです。これは一般に、呼び出し側が不適切な値の引数を渡したときにスローする例外です。例えば、ある処理を繰り返す回数を表すパラメーターに、呼び出し側が負の数を渡した場合にスローするのがこの例外です。もうひとつよく再利用される例外がIllegalStateExceptionです。これは一般に、受け取る側のオブジェクトの状態のせいで呼び出しが不正である場合にスローする例外です。例えば、呼び出し側が、オブジェクトが適切に初期化される前にそれを使おうとした場合にスローするのがこの例外です。
誤ったメソッド呼び出しは、突き詰めればすべて不正な引数か不正な状態に帰着すると言えますが、ある種の不正な引数や状態に対しては、別の例外を使うのが標準になっています。null値が禁止されているパラメーターに呼び出し側がnullを渡した場合、慣例としてIllegalArgumentExceptionではなくNullPointerExceptionをスローします。同様に、シーケンスへのインデックスを表すパラメーターに範囲外の値が渡された場合は、IllegalArgumentExceptionではなくIndexOutOfBoundsExceptionをスローすべきです。
Exception、RuntimeException、Throwable、Errorを直接再利用してはいけません。これらのクラスは、抽象クラスであるかのように扱いましょう。ただし、チェック例外を実行時例外に変換したい場合で、自分が何をしているかを本当にわかっているなら、RuntimeExceptionを使ってもかまいません。
33. 例外を無視しない
// Empty catch block ignores exception - Highly suspect!
try {
...
} catch (SomeException e) {
}
空のcatchブロックは、例外の目的を台なしにします。例外の目的とは、例外的な状況に対処することを強いることです。例外を無視するのは、火災報知器を無視するのと同じです。しかも報知器を止めてしまうので、ほかの誰も本当に火事かどうかを確かめられなくなります。