Effective Javaは「一人前」のJavaエンジニアになるために避けては通れない書籍だと思います。特にアプリケーション基盤のコードや公開APIを作るような立場にいるエンジニアは、この書籍に書かれてあることを把握していないと、まともなものを作れません。
素晴らしい書籍だということは言うまでもありませんが、一方で「要するにこういうことだよね」という理解をしづらい書籍であるとも感じます。
その原因は、おそらく以下のとおりでしょう。
- 表現が回りくどいです。
- 全部で90の項目がありますが、項目内の構造が分かりづらいです。
- コード例が必要以上に複雑な場合があります。
内容の本質そのものはあまり難しくないのに、こういった理由でこの書籍の敷居が上がるのは、何と言うかもったいないと思います。
そこで、本記事では(ほぼ)全項目について、できるだけ「読みやすい日本語」で内容を説明してみたいと思います。
ただし、あまりにも当たり前の話だったり、読むのが簡単な項目などについては、解説を省略しました。また、私見も混ざっています。そういった前提でご覧いただければと思います。
なお、Effective Java 第3版はJava 9をターゲットに執筆されています。そこで、Java 9の公式ドキュメントのありかを念のため載せておきます(意外にたどり着きづらいですので)。
[Java 9] 公式ドキュメント トップ
https://docs.oracle.com/javase/jp/9/
[Java 9] JDKのJavadoc(トップ画面から辿れます)
https://docs.oracle.com/javase/jp/9/docs/toc.htm
[Java 9] Java言語仕様(トップ画面から辿れます)
https://docs.oracle.com/javase/specs/jls/se9/html/index.html
第1章 はじめに
書籍で使う用語の定義などが書いてあるだけです。読まなくても良いでしょう。(おわり)
第2章 オブジェクトの生成と消滅
項目1 コンストラクタの代わりにstaticファクトリメソッドを検討する。
例えば、以下のような感じです。
- NG: BigInteger(int, int, Random) ← コンストラクタ。分かりづらい。
- OK: BigInteger.probablePrime(int, Random) ←staticファクトリメソッド。分かりやすい。
まずはstaticファクトリメソッドを検討し、それが微妙ならコンストラクタを選びましょう。
◆長所
①分かりやすい名前をつけることができる。
コンストラクタだと以下の不都合があります。
-
コンストラクタの名前は1つ(クラス名)に限定されてしまいます。
-
コンストラクタ同士を区別するのは引数の違いだけです。この結果、利用者はコンストラクタ間の違いを理解しづらいです。
staticファクトリメソッドには、このような不都合はありません。
②新たなオブジェクトを生成する必要がない。同じオブジェクトを使いまわせる。
③戻り値の型そのものではなく、そのサブタイプのオブジェクトを返せる。
例えば、java.util.CollectionsにはemptyList()
というstaticなファクトリメソッドがあります。以下の特徴があります。
-
emptyList()
は、内部クラスEmptyListのインスタンスを返却します。内部クラスEmptyListは、publicではありませんから、外部から隠蔽できます。 -
emptyList()
の戻り値の型は、List
インターフェースです。
このおかげで、CollectionsのAPIはとても簡潔です。具体的には・・・
-
実装者にとっては、EmptyListの使い方(API)を利用者に説明する必要がありません。
-
利用者にとっては、EmptyListの存在を意識する必要はないですし、使い方(API)を理解する必要もありません。
List
インターフェースの使い方だけ知っておけば使えます。
④ 状況に応じて、返却するサブタイプを切り替えることができる。
③では、常に同じサブタイプを返却する状況を想定して解説されています。
④で言いたいのは、「複数のサブタイプの中から、状況に応じたものを選んで返却できる」ということです。
⑤ 返却するサブタイプは、実行時に決まってもOK。(staticファクトリメソッドの実装時点で決まっていなくてもOK。)
例えば、JDBCのDriverManager.getConnection()
がこれに該当します。
結果、APIとしての柔軟性が高まります。
◆短所
①利用者は、戻り値の型のサブクラスを作れない。
例えば、java.util.CollectionsのemptyList()
ではEmptyListが返却されますが、EmptyListはprivateなクラスのため、利用者はEmptyListのサブクラスを作れません。
この例に限らず、私にはサブクラスを作りたくなるようなケースを思いつけませんでした。
実務上は短所や制約には全く感じないと思います。
②利用者は、staticファクトリメソッドを見つけづらい。
これは確かにそうですよね。Javadocではコンストラクタは別のセクションとなるため目立ちますが、staticファクトリメソッドはメソッドの一覧に埋もれてしまいます。
以下のような一般的な命名パターンに沿うことで、利用者にとって分かりやすいAPIとするよう、心がけましょう。
命名パターン | 例 | 意味合い |
---|---|---|
from | Date d = Date.from(instant); |
型変換する。 |
of | Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING); |
集約する。 |
valueOf | BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE); |
from、 ofと同じ意味で使われる。 |
instance / getInstance | StackWalker luke = StackWalker.getInstance(options); |
パラメータに応じたインスタンスを返す。 |
create / newInstance | Object newArray = Array.newInstance(classObject, arrayLen); |
呼び出しごとに新たなインスタンスを返す。 |
get型名 | FileStore fs = Files.getFileStore(path); |
自身のクラスとは異なるクラスを返却する。 |
new型名 | BufferedReader br = Files.newBufferedReader(path); |
自身のクラスとは異なるクラスを返却する。呼び出しごとに新たなインスタンスを返す。 |
型名 | List<Complaint> litany = Collections.list(legacyLitany); |
get型名、new型名の短縮版。 |
参考
Collections Frameworkの概要
https://docs.oracle.com/javase/jp/9/docs/api/java/util/doc-files/coll-overview.html
項目2 多くのコンストラクタパラメータに直面したときにはビルダーを検討する
【NG】テレスコーピング・パターン
public class NutritionFacts {
// フィールド定義は省略
public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings, int calories) {
this(servingSize, servings, calories, 0);
}
// 上記のような流れが延々と続いていく・・・
}
NGな点
- 利用者にとってどうでも良いパラメータであっても、設定を強制させてしまいます。
- 利用者は、何番目のパラメータが何かを気にする必要があり、とても大変です。(実務上は、IDEで多少軽減されます)
- コードの読み手は、各パラメータの意味を理解しづらいです。(実務上は、IDEで多少軽減されます)
- 型さえ合っていればコンパイルエラーにはならないので、同じ型のパラメータを誤った順番で指定しても、利用者は間違いに気づけません。
【NG】JavaBeansパターン
// 何の変哲もない、ふつうのJavaBeansです。
public class NutritionFacts {
// フィールド定義は省略
public NutritionFats() {}
public void setServingSize(int val) { //略 }
public void setServings(int val) { //略 }
public void setCalories(int val) { //略 }
// まだまだsetterが続く・・・
}
NGな点
- インスタンス化した後に、パラメータ間で不整合が起こるかもしれません。本来は、インスタンス化する前にそういった不整合を検知すべきです。
- setterから内部の状態を変更できるわけなので、クラスを不変にできません。
【Builderパターン】
// 利用者側のコード
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
calories(100).sodium(35).carbohydrate(27).build();
長所
- 実装しやすいです。
- 読みやすいです。
-
build()
でインスタンス化する前に、パラメータ間の不整合を検知できます。この長所はとっても重要です。なぜなら、不整合がここで検知されず、遠い場所で不整合を起因とするエラーが起きてしまうと、原因の特定が困難になってしまうからです。 - クラスが階層構造になっている場合でも、このパターンを適用できます。
- APIが柔軟になります。例えば、1つのBuilder(のインスタンス)を使い回し、
build()
内でシリアル番号を採番することで、一意なオブジェクトをどんどん生成できます。
短所(些細なものです)
- Builderをインスタンス化する必要があり、性能要件がシビアな状況では気にする必要があります。しかし、実務上はそんな状況は稀だと思いますので、些細な短所です。
- テレスコーピングパターンより、利用者のコードが長くなってしまいます。なので、パラメータの数が多い時に使うのが良いかもしれません。ただ、パラメータの数は保守していくとどんどん増えていくこともありますし、途中からBuilderパターンに変更するのは非現実的なので、些細な短所です。
項目3 privateのコンストラクタかenum型でシングルトン特性を強制する
シングルトンを実現する方法は以下の3つです。状況に応じて最良のものを選びましょう。
- enum型でシングルトンを実装する。 ← 多くの場合これが最適
- privateコンストラクタでインスタンスを1つ作って・・・
- publicなフィールドに設定して公開する。
- staticファクトリメソッドで返却する。
enum型でシングルトンを実装すると、こんな感じになります。
public enum Elvis {
INSTANCE;
public void someMethod();
}
他の方法ではシングルトンでなくなるリスクがあり、それを回避する手間が発生してしまいますので、enumを使う方法がベストです。具体的には以下のとおりです。
- privateコンストラクタの場合では、利用者がリフレクションによって内部のprivateコンストラクタを呼び出せてしまいます。これを防ぐためのチェック処理を書かねばらなず、面倒です。
- シングルトンのクラスをシリアライズできるようにしたい場合、デシリアライズの度にインスタンスが生成されてしまう恐れがあります。これを防ぐための処理を書かねばらなず、面倒です。(シリアライズしたい状況は稀だと思いますが・・)
こういったリスクは、実務ではほぼ無視して良いのではと思ってしまいますが、きちんと考えるべきですね。
enum型を使う方法は、実務では見たことがありませんが、上記の点で合理的ですので積極的に使うべきでしょう。
なお、privateコンストラクタを使う2つの方法には、以下の長所があります。ただ、こういった長所を得たいケースは限られるような気がしますので、結局は多くの場合にenumが最適だと言えるでしょう。
方法 | 長所 |
---|---|
publicなフィールドに設定して公開する | ・シングルトンであることがAPIから明確に理解できる ・単純 |
staticファクトリメソッドで返却する | ・シングルトンにすべきかどうかを後から変更できる ・ジェネリックのシングルトンファクトリにできる ・メソッド参照を使える |
項目4 privateのコンストラクタでインスタンス化不可能を強制する
staticなメソッドとstaticなフィールドだけで構成されるクラスは、一般にユーティリティクラスと呼ばれます。
このようなクラスは、本来インスタンス化して使うべきではないですが、コンストラクタが使えてしまうと、誤ってインスタンス化されてしまう恐れがあります。実害はあまりないような気はしますが、そういった使われ方をすると非常に残念な気持ちになりますね・・。
そこで、以下のようにprivateなコンストラクタを実装しましょう。
public class UtilityClass {
// インスタンス化できないように、デフォルトコンストラクタを抑制する。
// このようにコメントを残しておくことで、このコンストラクタの実装意図を後世に伝えましょう。
private UtilityClass() {
throw new AssertionError();
}
}
こうしてけば、誤ってインスタンス化されたり、誤って継承されてサブクラスを作られるのを防げます。
項目5 資源を直接結びつけるよりも依存性注入を選ぶ
スペルチェッカーを例に説明します。
【NG】ユーティリティクラスとして実装
public class SpellChecker{
// スペクチェックに使う辞書
private static final Lexicon dictionary = ...;
// 項目4の作法に従い、インスタンス化を抑止
private SpellChecker() {}
public static boolean isValid(String word) { ... }
}
【NG】シングルトンとなるように実装
// SpellChecker.INSTANCE.isValid("some-word"); のように使います。
public class SpellChecker{
// スペクチェックに使う辞書
private final Lexicon dictionary = ...;
// 項目4の作法に従い、インスタンス化を抑止
private SpellChecker() {}
public static SpellChecker INSTANCE = new SpellChecker(...);
public static boolean isValid(String word) { ... }
}
これらのNG例では1つの辞書しか使えないため、状況に応じて辞書を切り替えることができません。この難点は、本番のコードではもちろんのこと、テストの時にも当てはまります。
setDictionary(lexicon)
のようなメソッドを用意して、後から変更できるようにする手段もありますが、利用者にとって分かりにくいです。スレッドセーフでもありません。
そもそも、辞書が状況によって変わるということは、辞書は「状態」に当たります。ですので、インスタンス化して利用されるようなクラスとして、スペルチェッカーを実装すべきでしょう。
具体的には以下のとおりです。
【OK】依存性注入のスタイルで実装
public class SpellChecker{
// スペクチェックに使う辞書
private final Lexicon dictionary;
// 辞書という「状態」を持つのでインスタンス化して利用してもらう。
// この時、辞書という依存性を注入する。
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public static boolean isValid(String word) { ... }
}
こうすれば、状況に応じて辞書を切り替えることができますし、スレッドセーフとなります。
項目6 不必要なオブジェクトの生成を避ける
オブジェクトを再利用すると、オブジェクトの生成コストを最小限にして、速度を高めることができます。
逆に、不要なオブジェクトを多数生成してしまうと、ものすごく遅くなります。
不変なオブジェクト(イミュータブルなオブジェクト)は常に安全に再利用できるため、イミュータブルである、という性質はとっても重要なのです。
【NG その1】
// new String()で、不要なオブジェクトを生成している。
// "bikini"で、不要なオブジェクトを生成している。
String s = new String("bikini");
【OK】
// 生成されるオブジェクトは、"bikini"のStringインスタンスのみ。
// 同じJVM内であれば、文字列リテラル"bikini"のインスタンスは常に再利用される。
String s = "bikini";
【NG その2】
// コンストラクタなので、常に新たなオブジェクトが生成されてしまう。
new Boolean(String);
【OK】
// staticファクトリメソッドでは、新たなオブジェクトを生成する必要はない。
// trueあるいはfalseのBooleanオブジェクトが、再利用される。
Boolean.valueOf(String);
【NG その3】
// matchesの内部でPatternオブジェクトが生成される。
// isRomanNumeral()が呼び出されるたび、Patternオブジェクトが生成されてしまう。
static boolean isRomanNumeral(String s){
return s.matches("正規表現。内容は省略します。");
}
【OK】
public class RomanNumerals {
// Patternオブジェクトを再利用している。
private static final Pattern ROMAN = Pattern.compile("正規表現。内容は省略します。");
static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}
【NG その4】
// オートボクシングでのNG例
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
// Longはイミュータブルなので、加算の度に新たなインスタンスが生成されてしまう。
sum += i;
return sum;
}
【OK】
private static long sum() {
// プリミティブ型に変更(Long -> long)
long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
ちなみに、例えばMap.keySet()はSetビュー(アダプター)を返しますが、何度keySet()を呼び出しても、同じSetインスタンスが返却されます。呼び出す度に新しいインスタンスが生成されていると思いがちですが、こういった所でも実はインスタンスが再利用され、効率化が図られています。自分たちがAPIを実装する場合も、「新たなインスタンスを生成する必要ってあるのかな?」という視点に立ち、最適な実装をすべきですね。
項目7 使われなくなったオブジェクト参照を取り除く
クラスの中で、ガベージコレクターの管理の及ばない独自のメモリを管理している場合、不要になったオブジェクトの参照をクリアする必要があります(変数にnull
を設定します)。
管理が及ばない、というのは実質的に未使用のオブジェクトであっても、ガベージコレクタはそのことを認識できない、という意味です。
普通は、スコープから外れた変数はガベージコレクションの対象になります。一方、クラス内でオブジェクトを管理していると、スコープから外れないわけですから、ガベージコレクションの対象になりません。ガベージコレクタから見ると、そのオブジェクトは使用されている、と見なされるわけですね。
書籍では、シンプルなスタック実装を例に、上記を解説しています。詳しくは書籍を参照ください。
独自のキャッシュを実装するケース
独自のキャッシュを実装する場合も、先述の「クラスの中で独自のメモリ管理をするケース」に該当します。
例えばHashMapで画像データをキャッシュする場合、HashMapは何らかのオブジェクトAのフィールドとして管理されるはずですから、以下のように参照が連なります。
利用元 -> オブジェクトA -> HashMap -> keyおよびvalue
HashMapで管理するデータ(keyおよびvalue)が実際には不要になったとしても、オブジェクトAやHashMapが参照され続ける限り、そのデータがガベージコレクションされることはありません。結果、メモリは膨れ上がっていきます。
この場合の対処方法は以下のとおりです。(自分でキャッシュを実装する機会は、あんまり無いと思いますが・・)
方法① WeakHashMapでキャッシュを実装します。
WeakHashMapは、キーがそのWeakHashMapオブジェクト以外から参照されなくなると、そのエントリー(キーと値のペア)が次のGCの対象になります。いわゆる弱参照という仕組みを利用しています。
「キーがそのWeakHashMapオブジェクト以外から参照されない」という状態をもって、キャッシュから削除したい場合に、採用を検討しましょう。
と、本書では説明されていますが、そんな簡単にキャッシュから削除されてしまっては、もはやキャッシュと呼べないですよね。詳しくは、後述します。
方法② ScheduledThreadPoolExecutorで、定期的にキャッシュ中の古いデータを削除します。
ScheduledThreadPoolExecutorの使い方をザッと理解するには、こちらの記事がおすすめです。
https://codechacha.com/ja/java-scheduled-thread-pool-executor/
キャッシュに登録してからある程度時間が経ったものを削除したい、という場合に採用を検討しましょう。
方法③ キャッシュに新たなエントリを追加する時に、古いものを削除する。
シンプルですね。これも②と同様、キャッシュに登録してからある程度時間が経ったものを削除したい、という場合に採用を検討しましょう。
リスナーやコールバックをメモリに登録するケース
クライアントからリスナーやコールバックを登録できるAPIを作る場合、きちんと考えてつくらないと、登録されたリスナーやコールバックがGCの対象になりません。
WeakHashMapのキーとして保存するなど、弱参照の仕組みを使うと良いでしょう。WeakHashMap以外の誰からも使われなくなったら、GCの対象になります。
参考:弱参照とは?
ふつうは参照元に到達できないオブジェクト(誰からも使われていないオブジェクト)は、GCの対象となりメモリから削除されます。
しかし、そのようにすぐ削除されては困る場合があります。参照元に到達できないオブジェクトであっても、すぐにGCの対象とならないようにする仕組みがあります。それがjava.lang.refパッケージです。
このパッケージの世界では、通常の参照のことを「強参照」と呼び、以下の通り独自の「参照」が定義されています。
java.lang.refにおける参照の種類 | GC対象へのなりにくさ(相対値) | 説明 | 用途 |
---|---|---|---|
弱参照 | ★ あっさり消される |
自分(WeakReferenceオブジェクト)だけが、あるオブジェクトAを参照している場合、次のGCの対象になります。 | オブジェクトAへの強参照が無くなったら、オブジェクトAをすぐにメモリから消したい場合に使います。WeakHashMapでは、正にこの用途でWeakReferenceが利用されています。本書ではキャッシュの用途で使えるみたいなことが書いてありますが、強参照が無くなったら消えてしまうなんて、それはもはやキャッシュではないと思いますので、使う機会はほぼないと思います。 |
ソフト参照 | ★★ けっこうしぶとい |
自分(SoftReferenceオブジェクト)だけが、あるオブジェクトAを参照している場合、参照先のオブジェクトが最近 作成/参照されたなら、次のGCの対象になりません。そうでない場合は、次のGCの対象になります。 | キャッシュの用途で使うならこれだと思いますが、JavaSEにはこれをサポートするMapはありません。実際に使う機会はほぼ無さそうです。 |
ファントム参照 | ★★★ 基本的に消えない |
自分(PhantomReferenceオブジェクト)だけが、あるオブジェクトAを参照している状態となっても、GCの対象になりません。 | (不明) |
項目8 ファイナライザとクリーナーを避ける
ここでいうファイナライザとは、java.lang.Object#finalize()、あるいはそれをサブクラスでオーバーライドしたメソッドのことです。
クリーナーとは、java.lang.ref.Cleanerのことです。
これがダメな理由が、書籍には色々と書いてありますが、中身を理解する必要はほぼ無いです。
とにかく危険なので絶対に使わない。それで良いでしょう。
項目9 try-finallyよりもtry-with-resourcesを選ぶ
try-finallyには以下の問題があります。
- クローズ漏れが起きやすい。
- 漏れなくクローズしようとすると、ネストが増える。結果、コードは読みづらく、修正しづらくなる。
- 発生する例外を握り潰しやすい。
- 例外を握り潰さないようにするには、catch節を書く必要がある。結果、コードは読みづらく、修正しづらくなる。
try-with-resourcesを使うと、これらの問題が解消されます。
なお、try中に例外が発生し、その後にcloseでも例外が発生した場合、前者の方を優先してthrowします。catfh節で、これをキャッチできます。
後者にアクセスするには、前者の例外オブジェクトにあるgetSuppressedメソッドを使います。ただ、多くの場合は前者を知りたい場合が多いはずなので、使う機会はあまりなさそうです。
第3章 すべてのオブジェクトに共通のメソッド
項目10 equalsをオーバーライドするときは一般契約に従う
そもそもequalsメソッドをオーバーライドする機会は限定的だと思います。
もしオーバーライドするのであれば、満たすべき要件があります。
もしequalsメソッドをオーバーライドすべき状況となったら、その要件を守りましょう。要件とは、具体的には以下のとおりです。
-
反射性:x.equals(x)はtrueを返す。
-
対称性:y.equals(x)がtrueの場合のみ、x.equals(y)はtrueを返す。
-
推移性:x.equals(y)とy.equals(z)がtrueなら、x.equals(z)はtrueを返す。
-
整合性:x.equals(y)を複数回呼び出した場合、その結果は同じとなる。
-
非null性:x.equals(null)はfalseを返す。
※x, y, zはnullでないとする。
書籍では、それぞれの要件について気を付けるべきことが書いてありますが、そもそもequalsをオーバーライドする機会はあまりありませんので、必要性がない時点で詳しく学ぶのは、コスパが低いでしょう。
このため、私は必要になった時に書籍を参照するに留め、本記事でも詳しくは解説しません。
項目11 equalsをオーバーライドするときは、常にhashCodeをオーバーライドする
項目10の通り、equalsをオーバーライドする機会はあまり無さそうなので、この項目もあまり重要性は高くなさそうです。概要を解説するに留めます。
hashCodeメソッドをオーバーライドする場合の要件は、以下のとおりです。
- hashCodeメソッドが複数回呼び出された場合、同じ値を返す。
- 2つのオブジェクトがequalsメソッドで等しいなら、2つのオブジェクトのhashCodeメソッドは、同じ値を返す。
- 2つのオブジェクトがequalsメソッドで異なるからといって、2つのオブジェクトのhashCodeメソッドも、別々の値を返す必要は無い。しかし、別々の値を返した方が、ハッシュテーブルのパフォーマンスが改善されやすい。
項目12 toStringを常にオーバーライドする
toStringメソッドをオーバーライドするメリットは、そのクラスの利用者がデバッグをしやすくなることです。
ただ、実務でつくるシステムは芸術作品ではなく、人的・期間的なリソースは限られています。ですので、必要に応じてオーバーライドの要否を決めれば良いと思います。
もしtoStringメソッドをオーバーライドするなら、以下に気をつけましょう。
- 利用者が知るべきと思われる、全ての情報を出力しましょう。toStringは利用者のために存在することを意識しましょう。
- 戻り値の形式(例えば電話番号なら090-1234-5678)を決めるかどうかは、状況に応じて判断しましょう。利用者にとっては形式が定まっている方が嬉しいかもしれませんが、一度定めると後から変更しづらいです。バランスをとりましょう。
項目13 cloneを注意してオーバーライドする
Object.clone()をオーバーライドするのは原則NGです。理由は以下の通りです。
- オーバーライドする時に満たすべき要件が、コンパイラで強制されないばかりか、Javadocに明記されてすらいません。
- その要件はかなり複雑です。詳細は書籍をご覧ください。
- オーバーライドしたメソッドでは、super.clone()という形でObject.clone()を呼び出す必要があります。その際、以下の煩雑さが生まれます。
- Object.clone()がthrowするかもしれない、CloneNotSupportedExceptionを処理する必要があります。
- Object.clone()の戻り型はObjectですが、実務上はこれを特定の型にキャストする必要があります。
- finalなフィールドは、変更が禁止されるためコピーできません。clone()をオーバーライドするため、という本質的でない理由でfinalを外す必要があります。
- 上記以外にも細々とした理由があります。詳細は書籍をご覧ください。
NGですので、既にObject.clone()をオーバーライドしたクラスが存在し、それを保守で修正せざるを得ない、などの特別な状況でもない限り、Object.clone()をオーバーライドするべきではありません。
代わりに、以下のような手段を採用しましょう。これらには、上記のような難点はありません。
// コピーコンストラクタ
public Yum(Yum yum) { ... }
// コピーファクトリ
public static Yum newInstance(Yum yum) { ... }
なお、コピーコンストラクタやコピーファクトリといった方法を採用する場合でも、Object.clone()を採用する場合でも、共通して以下の点に気をつけましょう。
-
フィールドをコピーする時は原則としてディープコピーとなるようにしましょう。
つまり、フィールドのコピーをする際は、コピー元のオブジェクト参照を、コピー先のフィールドに設定しただけではダメです。コピー元とコピー先で、同じオブジェクト参照を共有してしまうからです。これがいわゆるシャロー(浅い)コピーです。考え方としては当たり前ですが、実装するのはかなり面倒です。このルールには例外があり、不変なオブジェクトであれば参照のコピーでOKです。
-
コピー中にコピー元の状態が変わるのはもちろんNGですから、スレッドセーフに実装しましょう。
この項目を理解するには、以下について事前に把握しておくことをオススメします。
-
Object.clone()はnative修飾子がついているので、Java以外の言語で実装されています。つまり、処理の中身がある、ということです。
-
共変戻り値型
https://www.slideshare.net/ryotamurohoshi/ss-38240061
項目14 Comparableの実装を検討する
開発するクラスにComparable.compareTo()を実装すると、そのクラスのオブジェクトをコレクションに格納して、コレクションの便利なAPIを使えるようになります。例えば、良い感じにソートできるようになります。
この利点を得たい場合は、Comparableインターフェースを実装しましょう。
その場合、equalsメソッドのオーバーライドと似たような要件を満たす必要があります。
-
sgn(x.compareTo(y)) == -sgn(y.compareTo(x)) となる。つまり、順序を逆にして比較した場合、結果の符号も逆になる。
-
(x.compareTo(y) > 0 && y.compareTo(z) > 0)なら、x.compareTo(z) > 0となる。つまり、推移的な関係になる。
-
x.compareTo(y) == 0 なら、sgn(x.compareTo(z)) == sgn(y.compareTo(z)) となる。つまり、xとyの順序が等しいなら、他のオブジェクトzをx,yのそれぞれと比較した場合、結果は等しくなる。
-
x.compareTo(y) == 0 なら、x.equals(y)はtrueになるし、その逆も成立するのが必須ではないが好ましい。
※sgn(expression)は、expressionが負、ゼロ、正の時にそれぞれ-1, 0, 1を返す関数です。
はじめの3つの要件については注意事項があります。Comparableを実装している既存のクラスがあったとして、それをextendsして新たなフィールドを追加する場合、これらの要件を満たすことは実質的に不可能です。無理に実現すると、もはやオブジェクト指向なコードではなくなるからです。このような場合は、extendsではなくコンポジションで実現しましょう(項目18)。
4つ目の要件については、仮に違反するとどうなるでしょうか?違反の例として、BigDecimalがあります。new BigDecimal("1.0")とnew BigDecimal("1.00")は、equalsメソッドでは等しくなく、compareToメソッドでは等しいです。
これらを新しいHashSetに入れると、equqlsメソッドで比較されるため、要素数は2となります。一方、新しいTreeSetに入れると、compareToメソッドで比較されるため、要素数は1となります。こういった挙動になることを頭の片隅に入れておかないと、万が一問題が発生した場合に、原因の見当がつかなくなります。
4つの要件以外に、以下の点に注意しましょう。
- Comparableインターフェースはパラメータ化されているので、引数の型のチェックやキャストは不要です。
- 引数がnullなら、NullPointerExceptionが送出されるべきです。何もしなくても普通はそうなるはずです。
- 比較対象のフィールドがオブジェクトなら、それらの参照を大小比較するのではなく、compareToによって比較しましょう。必要に応じて、自分でcompareToを実装しても良いですし、既存のComparatorを使っても良いです(例:String.CASE_INSENSITIVE_ORDER)。
- 大小比較するにあたって、関係演算子(>, <, ==)を使うのはやめましょう。分かりづらくなるだけです。代わりに、Integer.compare()といったcompareメソッドを使うようにしましょう。
- Java 8では、コンパレータ構築メソッドを使って、流れるように実装できます。
private static final Comparator<PhoneNumber> COMPARATOR =
comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
return COMPARATOR.compare(this, pn);
}
- 差に基づくコンパレータにしてしまうと、推移性の要件を満たせないばかりか、整数のオーバーフローなどが発生するリスクが生まれてしまいます。代わりに、先述のInteger.compare()などのstaticメソッドや、コンパレータ構築メソッドを使いましょう。
// NG例
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
}
第4章 クラスとインタフェース
項目15 クラスとメンバーへのアクセス可能性を最小限にする
情報隠蔽、カプセル化とは(what)
コンポーネントの外部からアクセスできる部分(公開API)を最小限に留めることです。それ以外の部分(内部データ、実装の詳細)を外部からアクセスできないようにします。
平たくいうと、publicやprotectedで宣言するものを、最小限に留めることです。そうなるように、注意深くクラスを設計します。
情報隠蔽、カプセル化をする理由(why)
情報隠蔽、カプセル化によって、コンポーネントを個別に開発でき、個別に最適化できます。つまり、他のコンポーネントに害を及ぼさないか、という心配を大きく減らせます。
それによって以下を狙うことが、情報隠蔽やカプセル化の目的です。
- 各コンポーネントを並列して開発できるようにして、開発のスピードを速めます。
- 影響調査の手間を減らして、保守の負荷を低くします。
- ボトルネックを局所的に解消できるようにして、パフォーマンスチューニングをしやすくします。
- コンポーネントを再利用できるようにします。
なお、こういった目的を達成できなければ、情報隠蔽やカプセル化を狙う意味はありません。実務で作るプログラムは芸術作品ではなく、ビジネスを成功させるための手段です。ですので、「キレイなものを作った」と満足しているようではいけないのです(私はそうやって満足してしまいやすいので、すごく注意しています)。
どうやれば良いか(how)
具体的には、以下の点を考慮しましょう。
- 何も考えずにpublicをつけるのではなく、アクセス可能性が最小限となるものを選びましょう。
- Serializableを実装すると、実質的に公開APIとなってしまいます。
- protectedで宣言されたものは、利用者(サブクラス)からアクセスできてしまうので公開APIになってしまいます。
- テストのためにパッケージプライベートにするのは、外部に公開していないという点で許容されます。
- 定数をpublic static finalで公開する場合は、その定数がコンポーネントの抽象化の方針に沿っているかを確認すべきです。
- 可変なオブジェクトは、利用者に中身を変更されてしまう恐れがあります。ですので、定数として公開するのはNGです。代わりに、不変なオブジェクトに変換して公開するか(Collections.unmodifiableListなど)、コピーを返すべきです。
- Java 9で追加されたモジュールシステムは、まだ広く使われておらず、有用性が確認されていませんので、現時点で積極的に使う必要はありません。
項目16 publicのクラスでは、publicのフィールドではなく、アクセッサーメソッドを使う
setter/getterをつけましょう、という当たり前の話です。
実務では「こうするのが常識でしょ」という感じで、何も考えずにsetter/getterをつけてしまいがちだと思います。ただ、理由を把握しておかないと、良いクラス設計になりません。改めて、setter/getterをつけるべき理由を把握しておきましょう。
setter/getterをつけるべき理由は、情報隠蔽、カプセル化の恩恵を得るためです。具体的には以下のとおりです。
- 内部データの表現形式(どんな型にするか、など)を、後から変更できるようにするためです。
- フィールド間の整合性などをチェックできるようにするためです。
- 後から補助処理を追加できるようにするためです。
公開APIをできるだけ減らすことが本質的なポイントですので、パッケージプライベートのクラスや、privateの内部クラスについては、そのフィールドにsetter/getterを用意する必要性は薄いです。不必要にsetter/getterを設けようとすると、実装の手間が増え、読みづらいコードになってしまいます。「何も考えずにとにかくsetter/getterをつけるんだ」という感じにならないようにしましょう。
項目17 可変性を最小限にする
不変オブジェクト(イミュータブルなオブジェクト)には、スレッドセーフに利用できる、といった点をはじめ、いくつかの利点があります。JDKでの代表例は、String, Integer等のボクシングされた基本データクラス, BigDecimal, BigIntegerです。
値を持つようなクラスを自作する場合は、可変にすべき正当な理由がない限り、不変に作りましょう。完璧に不変にするのが現実的でない場合でも、フィールドをできるだけfinalにするなど、できるだけ不変に近づけるのが良いです。とりうる状態が減ると、不具合が起こりづらいなどのメリットがあるからです。
不変オブジェクトの利点
- スレッドセーフ。複数のスレッドから、よってたかってアクセスされても問題が起きません。実務的にはこれが一番のメリットだと思います。
- 利用者は、オブジェクト内部の状態遷移を意識しなくて良いです。
- インスタンスを共有できます。結果、オブジェクトを生成するコスト、メモリ消費量を抑えられます。
- インスタンス間で、内部状態を共有できます。例えば、内部に配列を持つクラスであれば、あるインスタンスで保持する配列への参照を、新たなインスタンスに渡して再利用させることができます。
- そのインスタンスをフィールドに保持する他のオブジェクトが、内部的に何らかの状態を保たなければいけない場合があります(フィールド間の大小関係など)。フィールドで保持するインスタンスが可変だと、そのインスタンスの状態遷移を意識しなければならないため、望ましい状態を保つための処理は、すごく複雑になります。あるいは、そもそも実現は不可能です。逆に、フィールドで保持するインスタンスが不変な場合は、そういった問題が一切ありません。
- エラーアトミック性があります。エラーアトミック性とは、失敗する前の状態にオブジェクトが戻る性質です。不変オブジェクトはそもそも変化しませんので、この性質を持つのは当たり前です。可変オブジェクトにエラーアトミック性を持たせるためには、そのための手間がかかります。不変オブジェクトはそういった手間を必要としないが良いわけですね。
不変オブジェクトの欠点
-
異なる値を持つインスタンスそれぞれで、システム資源を消費します。結果、メモリをたくさん消費したり、パフォーマンスが悪くなる可能性があります。例えば、複数の計算ステップそれぞれで、その場限りのインスタンスを生成しては破棄する、といったことがあります。対処方法は以下のとおりです。
- 複数の処理ステップを、1つの公開APIにまとめる。
- Stringに対するStringBuilderのような、publicな可変コンパニオンクラスを提供する。
不変オブジェクトの作り方(満たすべき要件)
- setterのような、オブジェクトの状態を変更できるメソッドを提供してはいけません。
- extendsされないようにします。クラスをfinalにするか、コンストラクタをprivateにしつつstaticファクトリメソッドを提供するか、のどちらかで対応しましょう。
- 全てのフィールドをfinalにします。こうすることで、そのフィールドは変更不可となりますし、スレッド間でインスタンスの参照を安全に受け渡しできます。これには例外があります。内部でのみ利用する計算結果をフィールドでキャッシュしておきたい、といった場合もあります。その場合は、finalにしないという選択肢もアリです。
- 全てのフィールドをprivateにします。こうすることで、仮にフィールドに可変オブジェクトを持っていたとしても、利用者にオブジェクトの中身を変更させないようにできます。また、利用者に影響を及ぼさずに、後からフィールドの内部表現を変更できます。
- フィールドに可変オブジェクトを持つ場合、その可変オブジェクトにアクセスできるのは、不変クラス(自分自身)だけにします。利用者から渡されたオブジェクトを元に、不変クラスのオブジェクトを生成する場合、その参照をそのまま使うのではなく、防御的コピー(項目50)をしましょう。
項目18 継承よりもコンポジションを選ぶ
継承はカプセル化を破ります。つまり、サブクラスはスーパークラスの実装の詳細に依存してしまいます。結果、スーパークラスの実装の詳細が変更されると、サブクラスが意図通りに動かなくなったり、セキュリティホールが生まれかねません。
サブクラスがスーパークラスのメソッドをオーバーライドしなければ大丈夫、と思ってしまうかもしれませんが、そんなことはありません。スーパークラスが後で追加したメソッドのシグネチャが、サブクラスで実装されたメソッドと競合するかもしれないのです。
こういった不都合があるので、いきなり継承に飛びつくのはやめましょう。コンポジションには、同様の不都合はありません。
ただ、継承は100%悪で、コンポジションが100%正というわけでもありません。状況に応じてコンポジションと継承、どちらが最適なのかを選べるようになりましょう。
コンポジションとは
以下のように、既存のクラスを継承するのではなく、privateのフィールドに既存のクラスを保持します。
その既存クラスのメソッドを呼び出すことで、既存クラスを拡張します。
// 転送クラスです。このクラスでコンポジションが適用されています。
// 再利用できるように、InstrumentedSetとは別にクラスを設けています。
public class ForwardingSet<E> implements Set<E> {
// 拡張したい既存のクラスのオブジェクトを、フィールドで保持します。
private final Set<E> s;
public ForwardingSet(Set<E> s) {
this.s = s;
}
// 拡張したい既存のクラスのオブジェクトに、処理を丸投げします。
public void clear() {
this.s.clear();
}
// 以降は省略。
}
/*
転送クラスを継承することで、独自のクラスを作ります。
「え?継承はダメなんでしょ?」と思うかもしれませんが、ここでの継承は、
ForwardingSetを他の用途で再利用できるようにするための妥当な判断です。
*/
public class InstrumentedSet<E> extends FowardingSet<E> {
// このクラスは、Setに累計で何回addされたかを管理する役割を持ちます。
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
// 以降は省略。
}
ちなみに、上記コード例の技法は、厳密には「委譲(delegation)」とは異なります。ご注意ください。
継承しても良い場合とは
以下の場合は継承しても良いでしょう。
- 継承先のクラス(スーパークラスにしたいクラス)が、継承されることを前提に作られている場合です。クラスのJavadocに、その旨が記載されているはずです。
- 継承先のクラス(スーパークラスにしたいクラス)がパッケージ内にのみ公開されている場合です(パッケージプライベート)。この場合、スーパークラスを変更するときに、サブクラスに何らかの悪影響があったとしても、その悪影響はあくまでもパッケージ内に留まるからです。サブクラスを修正することで事なきを得られます。
なお、継承を採用する場合の大前提として、クラス間に「is-a」関係が成立している必要があります。つまり、クラスBがクラスAを継承する場合、「全てのBはAであるか」の答えはyesである必要があります。継承とは、そういった関係性をコードで実現するための技術だからです。
項目19 継承のために設計および文書化する、でなければ継承を禁止する
継承される前提のクラスを作るのは、実はものすごく大変で難しいことです。具体的には、以下の点を考慮する必要があります。
-
サブクラスでオーバーライド可能なメソッドが、スーパークラスでどのようなタイミングで呼び出されるのかを、利用者に示す必要があります。この情報は、正にスーパークラスの「実装の詳細」なので、情報隠蔽/カプセル化を破ってしまいます・・。
-
スーパークラスに、protectedのメソッド(場合によってはフィールド)を設けて、スーパークラスの内部動作に対するフックを、サブクラスに提供する必要に迫られるでしょう。多すぎると、情報隠蔽/カプセル化の恩恵から遠ざかります。一方、少なすぎると、サブクラスにとってスーパークラスは使いにくくなってしまいます。このバランスをとって設計する必要があり、ものすごく大変です。
-
スーパークラスのコンストラクタは、オーバーライド可能なメソッドを呼びしてはいけません。スーパークラスのコンストラクタは、サブクラスのコンストラクタの冒頭で呼び出されますので、サブクラスの準備が整っていない状態で、オーバーライド可能なメソッドを呼び出すことになるからです。
-
スーパークラスがCloneable, Serializableを実装する場合、注意しなければならないことがあります。ただ、そういった状況は稀だと思いますので、解説を省略します。詳細は書籍をご覧ください。
このように、ものすごく大変です。どうしても継承される前提のクラスを作る場合、上記の点を引き受ける覚悟をしてください。それがプロというものです。
継承される前提のクラスを作るケースより、そうでないケースが多いと思います。その場合は、作ったクラスが誤って継承されないように、クラスをfinalにするか、コンストラクタをprivateにしてstaticファクトリメソッドを用意するか、のどちらかの工夫を施しましょう。
項目20 抽象クラスよりもインタフェースを選ぶ
この項目は、読み解くのがものすごく難しいです。分かりやすさ優先で解説していきます。
ある「型」に対して、その実装は複数あります。例えば、Comparableという「型」の実装は複数あります。このような「複数の実装を許す」型というものを実現する仕組みとして、Javaでは以下の2つが提供されています。
- インタフェース(先ほどのComparableの例は、こちらですね。)
- 抽象クラス
「複数の実装を許す」ような独自の型を新たに作りたいなら、基本的にはインタフェースで実現しましょう。インタフェースは以下の点で、抽象クラスより優れているからです。利用者にとって使いやすいのです。
- 利用者は、複数のインタフェースを実装できます。Javaでは1つのクラスしか継承できませんので、抽象クラスを利用する場合はこういった利点を得られません。
- 既存のクラスがすでに何かを継承していたとしても、利用者は、そのクラスにインタフェースを導入できます。Javaでは1つのクラスしか継承できませんので、抽象クラスを利用する場合はこういった利点を得られません。
- 利用者は、既存のインタフェースを組み合わせて、新たなインタフェースを作り出せます。抽象クラスでも同様のことができますが、とんでもなく煩雑になります。具体的には、こちらの記事がとても参考になります。
- 利用者が既存の「複数の実装を許す」型を利用して、新たな機能をつくりたいとします。その型が抽象クラスなら、利用者は抽象クラスを継承するしかありません。インタフェースであれば、コンポジションを使えます(項目18)。
込み入ったインタフェースを作る時のテクニック「実装補助」「骨格実装」
単純なインタフェースを自作する場合は、気にしなくても良いテクニックです。必要な方だけご覧ください。
インタフェースを自作するとき、そのインタフェースに複数のメソッドを定義したとします。例えば、メソッドAとメソッドBを定義するとします。普通、メソッドAではメソッドBを呼び出すのが分かりきっているなら、典型的なメソッドAのロジックをインタフェースに実装した方が、利用者にとって使いやすくなります。利用者にとっては、自分が実装する部分が少ない方が楽ですもんね。
これを実現する方法には、以下の2パターンがあります。
- 実装補助:インタフェースのメソッドをdefaultで宣言し、インタフェース内にロジックを実装します。先ほどの例だと、メソッドAの宣言部にdefaultをつけて、ロジックを実装します。defaultはJava 8から使えます。これで済む場合は、先ほどのインタフェースの利点をフルに得られますので、次に示す骨格実装ではなく、まずはこちらの選択肢を検討しましょう。
- 骨格実装:実装補助を施したインタフェースを、抽象クラスで実装します。インタフェースのdefaultではequalsなどのObjectクラスのメソッドを実装できないなど、いくつか制約があります。抽象クラスでそれらの部分をカバーできます(実装できます)。このパターンの具体例は、AbstractListなどです。なお、継承されることを前提とするわけですから、項目19に従ってJavadocをしっかり書きましょう。
項目21 将来のためにインタフェースを設計する
インタフェースは一度公開したら最後。そう簡単に変更できません。公開前に、しっかりと検証しましょう。
後からインタフェースにメソッドを追加すると、そのインタフェースを実装していたクラスはコンパイルエラーになります。その問題を回避する「裏技」としてdefaultを使えますが、これは以下の点でNGです。defaultに頼らず、しっかりと設計するのが重要です。
- defaultは、そういった用途のために存在するわけではありません。
- defaultメソッドで、インタフェースの既存のメソッドを呼び出す場合、インタフェースを実装していたクラスの内部状態を壊す恐れがあります。現場の状況が理解されずに理不尽なルールを一方的に押しつけられ、現場が混乱するような感じですね。
項目22 型を定義するためだけにインタフェースを使う
インタフェースに定数を定義して、そのインタフェースを実装することでクラスに定数を利用させる、という手法があります。これはNGです。JDKにはそういったインタフェースが実際にありますが、真似してはいけません。
なぜなら、クラスが他のコンポーネントの定数を利用する、というのは実装の詳細だからです。インタフェースを実装するということは、その部分を公開APIにするということです。実装の詳細を公開APIにしてはいけません。そもそも、定数を公開するというのは、インタフェースの本質からかけ離れているので、論外なのです。
定数を外部に提供するなら、以下のいずれかにしましょう。
- 定数が特定のクラスに強く結びついているなら、そのクラスの中に定数を追加しましょう。例えば、Integer.MAX_VALUEなどです。
- 定数が列挙型のメンバーとして見なされるなら、enum型で定義しましょう。
- その他の場合、インスタンス化不可能なユーティリティクラスに定義しましょう。
// インスタンス化不可能なユーティリティクラス
public class PhysicalConstatns(){
private PhysicalConstants() {} // インスタンス化を防ぐ
public static final double AVOGADROS_NUMBER = 6.022_140_857e23;
}
なお、利用者では定数クラスをstaticインポートすることで、定数を利用するたびにクラス名を書かなくても済みます。と書籍では解説されていますが、そうすると後から見返したときに「この定数ってどこで定義してるやつだっけ?」ということになり、読みづらくなることもあります。staticインポートが適切かどうかは、バランスを考慮して判断しましょう。
項目23 タグ付きクラスよりもクラス階層を選ぶ
1つのクラスの中で、円か長方形かを表現するようなクラスがあったとします。その中には、円の場合に使われるコンストラクタ、フィールド、メソッドがあり、同様に長方形の場合に使われるものがあります。
このように、1つのクラスで複数の概念を表現しようとするクラスを、書籍では「タグ付きクラス」と呼んでいます。
複数の概念を表現したいなら、素直にクラスを分けましょう。具体的には、継承/サブタイプ化を利用して、階層構造に整理しましょう。当たり前ですね。
項目24 非staticのメンバークラスよりもstaticのメンバークラスを選ぶ
時に、クラスの内部で別のクラスを宣言したくなることがあります。こういった文脈では、前者のクラスは「エンクロージングクラス」、後者のクラスは「ネストしたクラス」と呼ばれます。
ネストしたクラスを実装する方法はいくつかあります。具体的には以下の4つです。
- staticのメンバークラス
- 非staticのメンバークラス
- 無名クラス
- ローカルクラス(メソッド内で名前付きで宣言するクラスです)
これらを適切に使い分けましょう。以下の流れで考えると、間違えることは無いはずです。
検討ステップ0
作ろうとしている「ネストしたクラス」は、エンクロージングクラスとは無関係に、他のクラスから利用される可能性があるでしょうか?
もしYESなら、そもそもネストしたクラスとして作るべきではありません。エンクロージングクラスとは独立した、単なる普通のクラスとして作成すべきです。
検討ステップ1
作ろうとしている「ネストしたクラス」が、以下の全てに該当する場合、無名クラスを使いましょう。
- エンクロージングクラスの特定のメソッドでしか使わない。
- ネストしたクラスの宣言部が短い。長いと、エンクロージングクラスのメソッドが読みづらいものになってしまいます。
- 一箇所でのみインスタンス化する。
なお、エンクロージングクラスの非staticメソッド内で、無名クラスが宣言されると、その無名クラスのインスタンスはエンクロージングクラスのインスタンスを自動的に参照します。場合によってはこれがメモリリークを引き起こします。その危険性を認識した上で、利用しましょう。
非staticメソッド内で宣言される場合は、こういったことは起きません。
検討ステップ2
作ろうとしている「ネストしたクラス」が、以下の全てに該当する場合、ローカルクラスを使いましょう。
- エンクロージングクラスの特定のメソッドでしか使わない。
- ネストしたクラスの宣言部が短い。
- そのメソッド内の複数箇所でインスタンス化する。複数箇所で使われるなら、名前付きで宣言しておく必要がおく必要があるわけです。
検討ステップ3
作ろうとしている「ネストしたクラス」が、以下の全てに該当する場合、非staticのメンバークラスを使いましょう。
- 以下のどちらかに該当する。
- ネストしたクラスの宣言部が長い。
- エンクロージングクラスの、複数のメソッドで利用される。
- エンクロージングクラスのインスタンスにアクセスする必要がある。非staticのメンバークラスがインスタンス化されると、そのインスタンスからエンクロージングクラスのインスタンスを、自動的に参照します。場合によってはこれがメモリリークを引き起こします。その危険性を認識した上で、利用しましょう。
検討ステップ4
作ろうとしている「ネストしたクラス」が、以下の全てに該当する場合、staticのメンバークラスを使いましょう。
- 以下のどちらかに該当する。
- ネストしたクラスの宣言部が長い。
- エンクロージングクラスの、複数のメソッドで利用される。
- エンクロージングクラスのインスタンスにアクセスする必要が無い。非staticのメンバークラスのような危険性がありませんので、できるだけこちらを選びましょう。
項目25 ソースファイルを単一のトップレベルのクラスに限定する
普通は、1つのソースファイルに複数のトップレベルのクラスを実装することなんて、無いですよね。この項目では、それがNGな理由が解説されていますが、そもそも実務ではそんなことをしないわけですから、理由を知る必要などありません。(おわり)
第5章 ジェネリックス
項目26 原型を使わない
原型というのは、List<String>
ではなくList
のように、型パラメータを伴わない表現のことです。
原型を使ってはいけない、というのはもはや常識ですね。その理由は、実行時にClassCastExceptionが起こる恐れがあるからです。型パラメータを指定して実装すると、そういったリスクをコンパイルエラーやコンパイル時の警告という形で気づくことができます。原型を使わないようにしましょう。
ただ、うっかり原型を使ってしまいそうになる局面があります。具体的には以下のとおりです。
【NG】
// Setの型パラメータが分からないからといって、こんな実装をしてはいけません。
static int numElementsInCommon(Set s1, Set s2) {
int result = 0;
for (Object o1 : s1)
if(s2.contains(o1))
result++;
return result;
}
以下のように実装しましょう。
【OK】
// こうすると、s1やs2に要素を追加しようとした時に、コンパイルエラーとなります。
// 実行時に気づくより断然良いですね。(ただし、nullは追加できます)
static int numElementsInCommon(Set<?> s1, Set<?> s2) {
// 略
}
項目27 無検査警告を取り除く
ジェネリクスを使って実装すると、実行時のClassCastExceptionにつながるリスクを、コンパイルエラーやコンパイル時の警告によって気づくことができます。
警告は放っておいてもコンパイルできますが、原則として全ての警告に対応し、警告が出ないように修正しましょう。本当に問題がない場合のみ、@SuppressWarning("unchecked")
をつけて、警告を抑制しましょう。ちなみに、"unchecked"とは、「型をチェックしていないけど大丈夫?」という意味合いの警告です。
本当に問題がない場合に警告を抑制しないと、本当に問題がある警告に気づくことができません。そこも手を抜かないようにしましょう。
項目28 配列よりもリストを選ぶ
歴史的経緯によって、配列とジェネリクスは異なる性質を持ちます。それが原因で、両者を組み合わせて使うと、問題が起きます。コードの簡潔さやパフォーマンスに関して顕著な問題がない限り、ジェネリクスに統一して実装しましょう。
組み合わせて使うと、どんな問題が起こるの?
両者を組み合わせて実装すると、コンパイラが出すエラーや警告の意味を理解できず、無駄に混乱してしまいます。場合によっては、十分な検討をせずにその警告を抑制してしまい、結果的にClassCastExceptionを発生させたり、保守メンバーに「何でここで@SuppressWarning
してるんだろう・・?」と疑問を抱かせ、困惑させてしまうでしょう。
例えば・・・
-
List<String>[]
のような、ジェネリクスを要素に持つ配列、というものはコンパイルエラーになります。それを許してしまうと、ClassCastExceptionが起き得るからです。そんなことしないから関係ないよ、と思うかもしれませんが、そんなことはありません。例えば、可変長引数のメソッドでは可変長の引数を保持するために配列が生成されます。引数をジェネリクス型にすると、理解しづらい警告が出ます。舞台裏で何が起きているかを理解しないと、適切に解決できないでしょう。new E[]
もダメです。 -
コンパイルエラーや警告の意味を分からず、場当たり的に実装した結果、以下のようなNGなクラスが生まれます。
public class Chooserr<T> { private final T[] choiceArray; public Chooser(Collection<T> choices) { // choicesはTを保持する型かもしれないですが、 // 多くの場合、toArray()がT[]を返す保証など無いはずです。 // 実行時にClassCastExceptionが発生する恐れがあります。 // 意味も分からず@SuppressWarningsするのはNGです。 @SuppressWarning("unchecked") choiceArray = (T[]) choices.toArray(); } }
なぜこんな問題が起こるのか?
両者には、以下の違いがあるためです。(と書籍では説明されていますが、これらの違いがなぜ上記の問題につながるのか、といったことについては説明されていません…。暇があれば後で解説を追加したいと思います。)
違い① | 違い② | |
---|---|---|
配列 | 例えば、Object[] objectArray = new Long[1]; では、Long[] はObject[] のサブタイプとして扱われます。なので、こういった代入が許可されます。こういった性質をcovariantと呼びます。「相手に応じて柔軟に変化する」といったニュアンスで覚えると良いでしょう。 |
配列は、実行時に自身がどんな型を格納できるのか、ということを知っています。なので、不適切な型のオブジェクトを代入しようとすると、実行時に例外が起きます。こういった性質を、「具象化されている」と呼びます。 |
ジェネリクス | 例えば、List<Object> objectList = new ArrayList<Long>(); では、ArrayList<Long> はList<Object> のサブタイプとして扱われません。なので、コンパイルエラーになります。こういった性質をinvariantと呼びます。covariantのような柔軟性がない、ということです。 |
ジェネリクスはコンパイル時にのみ型制約を強制し、実行時には要素の型情報を廃棄(erase)します。こういったことを「イレイジャで実装されている」と表現します。これは、ジェネリクスが導入された時に、ジェネリクスを利用していない既存のコードと、利用する新しいコードが共存できるようにするための措置です。これが冒頭で触れた「歴史的経緯」です。 |
※covariantは共変、invariantは不変と訳されていますが、こういった日本語訳は混乱につながるだけです。覚えない方が良いです。
項目29 ジェネリック型を使う
クラスを自作するときは、できるだけジェネリック化すべきです。そうすることで、利用者は以下の利点を得られます。
- 実行時にClassCastExceptionが起きる危険性を、排除できます。
- 利用者は、戻り値をいちいちキャストしなくても済みます。
場合によっては、自作するクラスの内部では配列を使った方がいいかもしれません。ArrayListのような基本的なジェネリック型を自作する場合や、パフォーマンス上の理由がある場合などです。
こういった場合、クラス内部ではコンパイル時の警告を抑制するため、@SuppressWanings("unchecked")
することになります。もちろん、抑制するのが本当に妥当なのか、十分に検討すべきなのは言うまでもありません。
項目30 ジェネリックメソッドを使う
メソッドを自作するときも、できるだけジェネリック化すべきです。そうすることで、利用者は項目29と同じ利点を得られます。
まずは、ふつうのジェネリックメソッドを定義する場合について、解説します。
【NG】非ジェネリックなメソッド
// 保持するオブジェクトの型がs1とs2とで異なる場合、実行時にClassCastExceptionが発生します。
public static Set union(Set s1, Set s2) {
Set reslut = new HashSet(s1);
result.addAll(s2);
return result;
}
【OK】ジェネリックなメソッド
// メソッドで利用する型パラメータ(のリスト)を、修飾子と戻り値型の間に宣言する必要があります。
// この例では、staticの直後にある<E>のことです。
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> reslut = new HashSet<>(s1);
result.addAll(s2);
return result;
}
次に、応用的なジェネリックなメソッドを実装する場合です。次の2つのテクニックを紹介します。
- ジェネリック・シングルトン・ファクトリ
- 再帰型境界
テクニックその1:ジェネリック・シングルトン・ファクトリ
利用者の指定する型で動作するオブジェクトを返す、という役割のAPIが必要になったとしましょう。生成するオブジェクトが状態を持たないのならば、利用者が指定する型が違うからといって毎回オブジェクトを生成するのは無駄です。
利用者の指定する型で動作しつつも、そのオブジェクトをシングルトンにできる(つまりインスタンスの生成コストやメモリ使用量を抑えられる)テクニックが、ジェネッリック・シングルトン・ファクトリです。
書籍では、この例として恒等関数が挙げられています。ちなみに、恒等関数とはパラメータをそのまま返す関数です。何の役に立つの?と思うかもしれませんが、機械学習の界隈では活性化関数の一つとして登場します。活性化関数としてAPIに何かしら指定しないといけないんだけど、何もしたくないから、実質的に何もしない関数を指定する、といった用途があります。
// このテクニックのポイント①
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
// このテクニックのポイント②
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
return (UnaryOperator<T>) IDENTITY_FN;
}
このテクニックのポイント①
ジェネリクスがイレイジャで実装されているからこそ、利用者にどんな型を指定されたとしてもIDENTITY_FNという一つのインスタンスをひたすら返してもOKなのです。
一方、ジェネリクスが具象化されていたら、つまり実行時にもIDENTITY_FNが、利用者が指定した型パラメータを覚えていたら、IDENTITY_FNという一つのインスタンスだけでは対応できません。
例えば、利用者が型パラメータにStringを指定してidentityFunction()を呼び出したら、IDENTITY_FNはUnaryOperator<String>
とならざるを得ません(ジェネリクスが具象化される世界の話です)。これだと、型パラメータとしてLongを指定したい人に対応するには、IDENTITY_FNのLong版を用意しなければなりません。そのオブジェクトを返す、identityFunction()メソッドのLong版も必要でしょう。
このテクニックのポイント②
ジェネリック・シングルトン・ファクトリを実装しようとすると、無検査でキャストしているけど大丈夫?という警告が出ます。ですが、多くの場合問題ないです。その理由を認識した上で、警告を抑制しましょう。
まず、この例の場合、無検査キャストの警告は何を意味するのでしょうか?
UnaryOperator<Object>
は、Objectという「特定の型」で動作すると想定されています。なお、Objectは最も汎用的な型ですが、全ての型を表すTに含まれる一つの型という位置付けですので、「特定の型」と表現しました。
一方、UnaryOperator<T>
のTは、全ての型を意味するわけですが、利用者が使う局面では一つの型に定まります。利用者が指定する型に定まるわけです。
特定の型で動作することを想定したUnaryOperator<特定の型>を、利用者が指定する型のUnaryOperator<利用者が指定する型>として扱うと、内部で「利用者が指定する型」から「特定の型」にキャストできず、ClassCastExceptionが発生する可能性があります。事情を知らないコンパイラからすると、その可能性を否定しきれません。
警告には、こういったメッセージが込められているのです。
しかし、この場合、利用者から渡された引数を、何も変更せずそのまま返すだけです。正確には、実行時に内部で「利用者が指定する型」からObjectにキャストされますが、Objectというクラス階層の頂点に立つものにキャストするわけですので、ClassCastExceptionが起こる余地がありません。このため、「コンパイラさん、今回は大丈夫ですよ」という感じで、警告を抑制しても問題ありません。
参考:このキャストは何でコンパイルエラーにならないの?
return (UnaryOperator<T>) IDENTITY_FN;
の部分で、無検査キャストの警告が出るわけですが、そもそもなぜUnaryOperator<Object>
がUnaryOperator<T>
にキャストできるのでしょうか?つまり、なぜコンパイルエラーが起きないのでしょうか?
List<Object> objectList = new ArrayList<Long>();
がコンパイルエラーとなるように、ジェネリクスはinvariantです。ですから、一見こういったキャストはできなさそうです。
しかし、一方がTのような総称型を型パラメータとする場合、両者が全く別の型とは言い切れない、とコンパイラが判断するため、キャストが許可されます。ちなみに、双方向のキャストが許可されます。
これは、参照型の縮小変換(narrowing reference conversion)と呼ばれており、Java言語規約で定められています。詳しくは、こちらがとても参考になります。
テクニックその2:再帰型境界
利用者が指定できる型パラメータに、何らかの制限を設けるテクニックです。なぜ「再帰型」と呼ぶのかは、例を見た方が分かりやすいでしょう。
public static <E extends Comparable<E>> E max(Collection<E> c);
<E extends Comparable<E>>
の意味するところは、
利用者が型パラメータに指定する型は、同じ型の他のオブジェクトと比較できるものでないとダメだよ。
ということです。
もっと平たく言うと、利用者が引数で指定するコレクションは、その要素同士が相互に比較できるものじゃないとダメ、ということですね。
こういった感じで、利用者が指定できる型パラメータに制限を設けることができます。
項目31 APIの柔軟性向上のために境界ワイルドカードを使う
この項目もかなり読みづらいです。噛み砕いて説明します。
自作するAPIが、パラメータされた型を引数として受け取るとしましょう。例えばList<E>
やSet<E>
、Iterable<E>
、Collection<E>
などですね。
こういった場合、利用者にとって使いやすいAPIとするために、工夫すべきことがあります。
利用者にとっての「使いやすさ」とは?
まずは利用者の立場に立ってみましょう。誰かがStackというクラスをAPIとして公開し、あなたがそれを利用しているとします。
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ... ;
// IntegerはNumberのサブタイプなので、直感的にはこのように使えそうな気がしませんか?
numberStack.pushAll(integers);
利用者であるあなたは、暗黙的にこう考えているのです。「こちらがAPIにオブジェクトを提供する場合、より具体的な型のオブジェクトを渡しても、きっとうまく動くだろう」。
逆に、次の場合はどうでしょうか?
Stack<Number> numberStack = new Stack<>();
Collection<Object> objectsHolder = ... ;
// ObjectはNumberのスーパータイプなので、直感的にはこのように使えそうな気がしませんか?
numberStack.popAll(objectsHolder);
利用者であるあなたは、暗黙的にこう考えているのです。「こちらがAPIからオブジェクトを受け取る側なら、より抽象的な型のオブジェクトとして受け取れるだろう」。
利用者にとっては、APIにこういった柔軟性があると助かるわけですね。
そのためには、どうすれば良いか?
APIを作る側の立場に戻りましょう。
まずは、先ほどの1つ目のケースです。
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ... ;
// IntegerはNumberのサブタイプなので、直感的にはこのように使えそうな気がしませんか?
numberStack.pushAll(integers);
このような柔軟性をAPIに持たせるには、APIを以下のように実装します。
// ポイントは、<? extends E> の部分です。ロジック部分は省略。
public <E> void pushAll(Iterable<? extends E> src) {
...
}
利用者は暗黙的にこう考えます。「こちらがAPIにオブジェクトを提供する場合、より具体的な型のオブジェクトを渡しても、きっとうまく動くだろう」。
利用者の気持ちをAPIで実現するならば、「EのIterable」ではなく、「Eそのもの、あるいはEのサブタイプのIterable」とすべきなのです。
このようにパラメータがAPIにオブジェクトを提供する場合、そのパラメータはproducerである、と言います。「producerの場合は、extends」というわけです。
なお、<? extends E>
という感じにパラメータされた型を「境界ワイルドカード型」と呼びます。
次に、先ほどの2つ目のケースです。
Stack<Number> numberStack = new Stack<>();
Collection<Object> objectsHolder = ... ;
// ObjectはNumberのスーパータイプなので、直感的にはこのように使えそうな気がしませんか?
numberStack.popAll(objectsHolder);
このような柔軟性をAPIに持たせるには、APIを以下のように実装します。
// ポイントは、<? super E> の部分です。ロジック部分は省略。
public <E> void popAll(Collection<? super E> dst) {
...
}
利用者は暗黙的にこう考えます。「こちらがAPIからオブジェクトを受け取る側なら、より抽象的な型のオブジェクトとして受け取れるだろう」。
利用者の気持ちをAPIで実現するならば、「EのCollection」ではなく、「Eそのもの、あるいはEのスーパータイプのCollection」とすべきなのです。
このようにパラメータがAPIからオブジェクトを受け取る場合、そのパラメータはconsumerである、と言います。「consumerの場合は、super」というわけです。
まとめると、以下の通りです。
- パラメータがproducerの場合は、 **e**xtends E>
- パラメータがconsumerの場合は、 **s**uper E>
頭文字をとって、「PECS」と覚えましょう。
その他のアドバイス
ここでは、比較的こまかめなアドバイスを紹介します。
-
APIの戻り値の型に、境界ワイルドカード型を適用するのはNGです。利用者に柔軟性を与えるどころか、制約を強制してしまいます。
-
利用者にワイルドカード型を何かしら意識させるようであれば、利用者にとって使いにくいAPIということです。API設計を見直しましょう。
-
APIの引数には
T extends Comparable<T>
ではなく、T extends Comparable<? super T>
を常に使いましょう。<? super T>
は比較先であり、Tを受け取る側(consumer)ですのでsuper
を付けます。「Tそのものか、Tのスーパータイプと比較できるT」という意味です。こうすることで、T自身ではComparableを実装していないけど、TのスーパータイプがComparableを実装していれば、APIに引数として渡せます。以下に例を示します。// APIの例(とっても複雑ですね・・。柔軟にするための代償です。) public static <T extends Comparable<? super T>> T max(List<? extends T> list) { ... } // APIの利用例 List<ScheduledFuture<?>> scheduledFutures = ...; ScheduledFuture<?> max = max(scheduledFutures);
ScheduledFuture自身はComparableを実装していませんが、スーパータイプであるDelayedでは実装されています。スーパータイプであるDelayedの力を借りることができる、ということです。maxは、柔軟なAPIだと言えるでしょう。
項目32 ジェネリックスと可変長引数を注意して組み合わせる
List<String>[]
といった、「ジェネリクスを要素に持つ配列」を生成しようとするとコンパイルエラーになります。そういった配列の存在を許すと、実行時にClassCastExceptionが起きる恐れがあるからです(項目28)。
しかしこれには例外があります。可変長引数は配列によって実現されますが、可変長引数にジェネリクスを指定することで、ジェネリクスを要素に持つ配列が生まれてしまうのです。
static void dangerous(List<String>... stringLists) {
// stringListsの正体は、List<String>を要素にもつ「配列」です。
// これが原因で、どこかでClassCastExceptionが起きてしまうかもしれません。
List<String>[] array = stringLists;
...
}
こういった事情がありますから、APIを作るときには以下の点を考慮しましょう。
-
パフォーマンスやコードの冗長さといった点で問題がなければ、可変長引数にジェネリクスを指定するのではなく、Listで可変長の引数を受け取るようにしましょう。可変長引数とジェネリクスという相性の悪いものを積極的に組み合わせる必要はないのです。「利用者が引数のListを生成するのが面倒では?」と思うかもしれませんが、利用者に
List.of()
を使ってもらえば済む話です。 -
どうしても可変長引数にジェネリクスを指定したい場合は、以下の全てに対応しましょう。
- 実行時にClassCastExceptionが起こる危険性を取り除きましょう。そのためには・・・
- ジェネリクスの配列に、要素を保存してはいけません(上書きしてはいけません)。
- ジェネリクスの配列(の参照)を、信頼できないコードに公開してはいけません。
- ClassCastExceptionの危険がないことを、
@SafeVarargs
で示しましょう。メソッドにこのアノテーションを付けます。こうすることで、利用者があなたのAPIを呼び出すときに、不要なコンパイラ警告が表示されずに済みます。
- 実行時にClassCastExceptionが起こる危険性を取り除きましょう。そのためには・・・
項目33 型安全な異種コンテナを検討する
個人的には、かなり発展的な内容と思います。
そもそも「異種コンテナ」とは何か?どんなときに嬉しいのか?
具体例で説明します。
アプリケーションFW(フレームワーク)を作ってそれを他のメンバーに使ってもらう立場ですと、アノテーションを利用して個々のアプリケーションを制御することがあります。私がアノテーションを作り、メンバーの皆さんは自作するクラスにそのアノテーションをつけます。私はそのアノテーションを目印に、メンバーの作ったアプリ(クラス)を制御をするわけですね。
FWでは、メンバーの作るクラスからアノテーションを取得して、どんな風にメンバーのクラスを制御すべきかを判断します。
メンバーの作るクラスにどんなアノテーションが付けられているか、という情報はそのクラスのClassオブジェクトに「異種コンテナ」として保持されています。
FWは、メンバーがクラスにつけた@MyAnnotaion1
に、どんな値が設定されているかを知りたいわけです。ですので、ClassCreatedByMember.class.getAnnotation(MyAnnotation1.class)
を呼び出して、メンバーがそのクラスに付けた@MyAnnotation1
(を表すオブジェクト)を取得します。
同様に、@MyAnnotation2
の情報を知りたい場合は、FWはClassCreatedByMember.class.getAnnotation(MyAnnotation2.class)
sを呼び出します。
このように、特定のClassオブジェクト(この場合はMyAnnotation1.classや、MyAnnotation2.class)をキーにして、それに対応するオブジェクトを格納しておきたいことがあります。それが、この項目で紹介されている「異種コンテナ」です。
いつかそういった「異種コンテナ」を自作する機会が訪れるかもしれません。この項目の内容を、一つのテクニックとして覚えておきましょう。
優れた異種コンテナを作る方法
この項目では、優れた異種コンテナを作る方法が説明されています。具体的には、型安全に作る(ClassCastExceptionが起きないようにする)方法が説明されています。
ポイントは以下の通りです。
public class Favorites {
// ・Mapのキーにワイルドカード「?」を使うことで、色々な型をキーにできる柔軟性が生まれます。
// ・Mapの値はObject型です。これで型安全なの?と思うかもしれませんが、他のところで型安全性を確保しています。
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
// ・誤って原型を指定された時の対策として、type.cast()で型チェックします。
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
public <T> T getFavorite(Class<T> type) {
// ・引数typeで指定された型を、必ず返します。
// 例えば、String.classを指定したのにIntegerが返されて、
// ClassCastExceptionになる、といったことは起きません。
// ・favorites.get()の結果はObject型ですが、そのままで困るのでキャストしています。
return type.cast(favorites.get(type));
}
}
なお、Favoritesの例では、利用者は基本的にどんな型のオブジェクトでも格納できます。しかし、場合によっては格納できる型に何らかの制限を設けたい場合もあるでしょう。
例えば、Annotationのサブタイプじゃないとダメ、といった制限を設けたいなら
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
のように、<T extends Annotation>
で利用者に制限を課すことができます。
第6章 enumとアノテーション
項目34 int定数の代わりにenumを使う
enumを使わずに定数を宣言する場合の「難点」
enumを使わずにintで定数を宣言すると・・・
- 型さえ合えば、本来の用途とはかけ離れた用途に使えてしまいます。そういった事態に、コンパイルエラーで気づくことができません。
- 独自の名前空間がないので、他の関係のない定数と重複しない名前にする必要があります。
- 定数の名前が、ログやデバッガに表示されません。値だけ表示されても調査の役には立たないでしょう。
- 定数をイテレートしたり、定数の数を数えられません。
Stringで定数を宣言すると・・・
- その定数フィールドの利用を強制できません。ハードコーディングされてミスタイプされたとしてもコンパイルエラーにならないので、そういった事態に気づけません。
enum型の仕組みと特徴
enum型という機能は、定数をはじめとする「列挙されるもの」に「独自の型」を持たせることが、その本質だと言えるでしょう。独自の型であるため、先述した「enumを使わない場合の難点」がありません。
enum型はつまるところクラスです。クラスの特殊な形なのです。
自分のインスタンスを、自分のpublic static finalフィールドに(暗黙的に)持っていて、独自のフィールドやメソッドを定義することができます。こういった性質は普通のクラスと同じですが、裏で色々な細工がなされるところが、普通のクラスと異なります。
例えば以下のような感じです。
- enumで宣言した型は、自動的にjava.lang.Enumを継承するようになります。なお、java.lang.Enumは抽象クラスで、以下のように、列挙型が共通して備えるべき機能がしっかりと実装されています。
- Objectクラスのequals(), hashCode()を適切にオーバーライドしています。
- Comparableを実装しています。
- Serializableを実装しています。enum型がどう実装されても対応できるようになっています。
- public static finalフィールドを設け、enum型をインスタンス化してそのフィールドに設定する、といったことを裏で自動的にやってくれます。
enumには以下の特徴があります。
- enum型の中に定義されている列挙定数1つに対して、public static finalフィールドの1インスタンスが割り当てられます。各列挙定数はシングルトンということです。
- enum型には外部からアクセスできるコンストラクタが無いので、以下のような「嬉しい制限」が備わっています。
- enum型そのものを外部からインスタンス化できません。
- enum型を外部から継承することはできません。
- enum型は独自の名前空間を持っています。定数名が衝突することを気にしなくても良いのです。
- toString()によって、調査の手掛かりになる情報をログやデバッガに表示できます。
- enum型には、メソッドやフィールドを定義できます。
- enum型は、任意のインタフェースを実装できます。
個人的には、enum型というのは世の中で考えられている以上に、その仕組みが分かりづらいと思います。Java言語仕様をしっかりと理解しておかないと、enumを効果的に使うのはちょっと難しいかなと思います。
Java言語仕様 8.9. Enum Types
https://docs.oracle.com/javase/specs/jls/se9/html/jls-8.html#jls-8.9
Java言語仕様に書かれてあることで知っておくとよい点は、以下の通りかなと思います。
- 以下の結果、シングルトンであることが保証されます。
- enum型のcloneメソッドを呼び出しても、インスタンスは複製されません。
- リフレクションであっても、enum型のインスタンスを生成できません。
- enum型のインスタンスがデシリアライズされても、重複したインスタンスが生成されないようになっています。
- enum型には独自のコンストラクタを定義できますが、publicやprotectedをつけられません。アクセス修飾子をつけずに定義するわけですが、その場合は自動的にprivate扱いになります。
- enum型に独自のコンストラクタを定義しない場合、privateのデフォルトコンストラクタが自動的に設けられます。
- enumのstaticフィールドは、コンストラクタが実行された時点ではまだ初期化されていません。ですので、コンストラクタからstaticフィールドにはアクセスできません。
- enum型を定義すると、暗黙的に以下のメソッドが宣言されます。
public static E[] values();
public static E valueOf(String name);
enum型を作る時のポイント
独自のenum型を作る場合、以下の点を考慮しましょう。
-
enum型も普通のクラスと同じように、可視性を最小限にしましょう。
-
同じメソッド名だけど、定数ごとに異なる振る舞いをさせたい場合があります。ポリモーフィズムですね。enum型内部では自分自身を継承できます。具体的には、enum型の中で列挙定数を宣言するときに匿名クラスを定義できます。その匿名クラスからenum型自身を継承し、enum型自身に定義されているabstratメソッドをオーバーライドしたり、enum型がimplementsするインタフェースのメソッドを実装できます。
定数間でコードを共通化したい場合、定数ごとにabstractメソッドをオーバーライドさせ、共通部分をprivateメソッドにしても良いでしょう。書籍にあるように、戦略enumパターンを採用しても良いと思います。どちらにしても求められるのは「不適切な実装をしたときにコンパイルエラーで気付けるか」ということですし、どちらを選ぶかの判断基準は「簡潔さと柔軟性のバランスをどう取るか」ということです。
-
既存のenum型にswitch文をかますことで振る舞いを拡張することができます。以下のようなケースに役立ちます。
- 定数ごとに異なる振る舞いをする既存のenum型があるとしましょう。あなたは、そのenum型を修正できません。でもその既存のenum型の振る舞いを拡張しなければなりません。
- 既存のenum型にわざわざメソッドとして設けるほどではないけど、自分にとって何らかの拡張された振る舞いが必要です。
-
toString()をオーバーライドして独自の名前を返すようにした場合、valueOf(String)ではその独自の名前に対応できません。独自の名前に対応できるようなfromString(String)のようなメソッドを設けた方が良いです。
項目35 序数の代わりにインスタンスフィールドを使う
java.lang.Enumにはordinal()というメソッドがあります。enum定数のインスタンスに対してordinal()を呼び出すと、そのenum定数がenum型の中で何番目に宣言されているか、というintが返ります。
このメソッドを使って何かをしようとすると、多くの場合に破綻します。何番目に宣言されているかに依存するロジックは、いかにも変更に弱そうですよね。
なので、よほどの場合でない限り、ordinal()を使わないようにしましょう。
この項目はそういったレベルの理解をしておけば、実務上は問題ありません。
項目36 ビットフィールドの代わりにEnumSetを使う
定数の集合、というものを扱いたいときがあります。
例えば、書式という列挙型に「太字」「イタリック」「下線」といった要素があるとします。この場合、「太字」かつ「イタリック」といった、要素の組み合わせを表現できる必要があります。
enum型が登場する以前は、これをビットで表現していました。
// 現代ではNGです。
// 定数宣言
private static final int STYLE_BOLD = 1 << 0; // 0001
private static final int STYLE_ITALIC = 1 << 1; // 0010
private static final int STYLE_UNDERLINE = 1 << 2; // 0100
// 太字かつイタリック
int styles = STYLE_BOLD | STYLE_ITALIC // 0011
といった感じです。
この手法は、簡潔でパフォーマンスが良いという良さがある一方で、項目34に記載したint定数の欠点に加え、ビット数を初めの段階で確定させなければならない、という欠点があります。
現代では、こういった「定数の要素の組み合わせ」を良い感じに表現する方法があります。それがEnumSetです。
// 現代ではこちらが推奨されます。
// 定数宣言
enum Style {BOLD, ITALIC, UNDERLINE}
// 太字かつイタリック
Set<Style> styles = EnumSet.of(Style.BOLD, Style.ITALIC);
こちらは明らかに簡潔です。EnumSetの内部ではビット演算がなされますので、パフォーマンスも良いです。当然ながら、int定数が持つ欠点もありません。
項目37 序数インデックスの代わりにEnumMapを使う
列挙定数(enum型のインスタンス)をキーにして、他の何らかのデータを値にして、Mapを作りたくなることがあります。
その場合は、EnumMapを使いましょう。
項目35と同様に、間違ってもjava.lang.Enum.ordinary()を使わないようにしましょう。
(おわり)
項目38 拡張可能なenumをインタフェースで模倣する
あなたが公開するenum型の列挙定数だけでは、利用者にとって不足があるかもしれません。
例えば、四則演算を表すenum型を公開したとして、利用者は「累乗の演算を表す列挙定数も欲しい」と思うかもしれません。
そういった類の柔軟性をAPIに持たせるために、公開するenum型にインタフェースを実装させましょう。利用者には、独自のenum型を作ってもらい、そのインターフェースを実装してもらいましょう。そして、あなたのAPIでは、四則演算を表すenum型という実装クラスに対してではなく、インタフェースに対してロジックを書きましょう。こうすることで、利用者が拡張したenum型を動作させることができます。
項目39 命名パターンよりもアノテーションを選ぶ
ページ数の多い項目ですが、それはコード例が長いだけであって、学び取るべきことは少ないです。具体的には以下の通りです。
メソッドなどのプログラム要素の名前に、何らかのルールを設けて、そのルールに応じて付けられた「目印」を手掛かりに、ツールやフレームワークがプログラムを制御する、ということが昔は行われていました。例えば、JUnitではテストメソッド名がtestで始まること、というルールを設けていました。こういった技法は命名パターンと呼ばれます。
そういった技法は明らかに脆弱です。
ツールやフレームワークからプログラムを制御したい場合は、「手掛かり」をアノテーションでつけるようにしましょう。命名パターンの持つ脆弱さとは無縁になれます。
JDKには既に有用なアノテーションがたくさんあります。それらをしっかり活用しましょう。
(おわり)
項目40 常にOverrideアノテーションを使う
スーパータイプのメソッドをオーバーライドする場合は、必ず@Override
をつけましょう。オーバーライドしているつもりが実はできていなかった、といった間違いをコンパイラが教えてくれます。
(おわり)
項目41 型を定義するためにマーカーインタフェースを使う
この項目はかなり読みづらいです…。噛み砕いて説明していこうと思います。
例えばFWやツールを開発していて、それらを利用する個別のプログラムを制御したいとします。その場合、個別のプログラムのどんな部分をどんな風に制御するかを、FWやツールが判断するために、個別のプログラムに何らかの「目印」(マーカー)をつける必要があります。
こういったマーカーを実現する方法として、以下の2つがあります。
- マーカーインタフェース(Serializableなど)
- マーカーアノテーション(JUnitの
@Test
など)
この項目では、これらをどう使い分けるべきか?ということが解説されています。
マーカーインタフェースの長所
-
マーカーインタフェースは、型を定義できます。このため、コンパイル時に誤りに気づくことができます。一方、マーカーアノテーションではそういったことはできません。
-
マーカーインタフェースには、適用するための条件を付けることができます。
例えば、あるインタフェースAがあって、そのインタフェースAを実装したクラスにだけ、マーカーインタフェースが適用されるようにしたいとします。その場合、マーカーインタフェースにインタフェースAをextendsさせます。すると、そのマーカーインタフェースを実装するクラスは、自動的にインタフェースAも実装することになります。
「このマーカーインタフェースをつけるには、インタフェースAを実装する必要があるんですよ」という条件を付けられるのです。(こういった状況の具体例は思いつきませんが・・)
一方、マーカーアノテーションではそういったことはできません。
マーカーアノテーションの長所
- クラスやインタフェース以外にも、適用できます。一方、マーカーインタフェースはクラスとインタフェースにしか適用できません。
使い分けの考え方
この項目のメッセージは、「できるだけマーカーインタフェースを使いましょう。なぜならコンパイル時に誤りに気付けるからです。」という感じです。それを念頭において、以下をご覧ください。
-
クラスやインタフェース以外に適用する必要があるなら、マーカーアノテーションを使うしかありません。
-
「マークしたオブジェクトを引数にとるメソッド」が必要になりそうなら、コンパイル時の型チェックができるのでマーカーインタフェースを使いましょう。そうでないなら、マーカーアノテーションを使えば良いでしょう。
-
アノテーションが多用されるフレームワークなら、一貫性を重視する目的でマーカーアノテーションを使った方がいいかもしれません。バランスを見て判断すべきでしょう。
第7章 ラムダとストリーム
項目42 無名クラスよりもラムダを選ぶ
昔は関数オブジェクトを表現するために、無名クラスが使われていました。
Java 8から、関数オブジェクトを表現しやすくするために、関数型インタフェースが導入されました。併せて、関数型インタフェースのインスタンスを簡潔に表現できる仕組みとして、ラムダ式(あるいは単に「ラムダ」)が導入されました。
ラムダについては、以下を理解した上で使いましょう。
- ラムダの良さは簡潔さにありますから、出来る限り型を省略してコードを書くのが良いです。
- 型を省略できるのは、ラムダが型推論をしてくれるからです。その型推論は、ジェネリクスを手掛かりにして行われますから、ジェネリクスを最大限に活用することがラムダの良さを引き出す重要ポイントとなります。
- ラムダには名前とドキュメンテーションがありませんから、自明なロジックでなかったり、数行を超えたロジックなら、ラムダで実装すべきではありません。
- ラムダよりも無名クラスの方が適切な場合があります。状況に応じて選択しましょう。
- ラムダは関数型インタフェースしか実装できません。無名クラスは抽象クラスを実装することができます。
- 無名クラスは、複数の抽象メソッドを持つインタフェースを実装できません。
- ラムダにおける
this
はエンクロージングインスタンスを表し、無名クラスにおけるthis
は無名クラスのインスタンスを表します。
項目43 ラムダよりもメソッド参照を選ぶ
ラムダよりもメソッド参照の方が簡潔に実装できる場合があります。メソッド参照も、選択肢の一つに入れておきましょう。
ただし、以下の点を考慮すべきです。
- ラムダの場合、パラメータ名を書きます。そのパラメータ名が読みやすさのために必要なら、ラムダを選ぶべきでしょう。
- ラムダの場合、ロジックを書きます。決まり切ったロジックであっても、そのロジックが書いてあることで、すごく読みやすくなるのなら、ラムダを選ぶべきでしょう。
- ラムダの処理をメソッドに抽出し、そのメソッド参照を使う、という選択肢もあります。その場合、抽出したメソッドにドキュメントを書けます。
- クラス名がすごく長かったりすると、メソッド参照の方が冗長になります。
なお、メソッド参照には5つの種類があります。書籍の表がとても分かりやすいのでそのまま引用します。はじめは慣れないかもしれませんが、頑張って覚える価値はあると思います。
メソッド参照の種類 | 例 | 同等のラムダ |
---|---|---|
static | Integer::parseInt |
str -> Integer.parseInt(str) |
バウンド | Instant.now()::isAfter |
Instant then = Instant.now(); t -> then.isAfter(t)
|
アンバウンド | String::toLowerCase |
str -> str.toLowerCase() |
クラスコンストラクタ | TreeMap<K,V>::new |
() -> new TreeMap<K,V>() |
配列コンストラクタ | int[]::new |
len -> new int[len] |
項目44 標準の関数型インタフェースを使う
Javaに関数型インタフェースやラムダが備わったことで、APIを作る場合のベストプラクティスはかなり変わってきました。具体的には、関数オブジェクトを引数にとるコンストラクタやメソッドを作るのが当たり前になってきました。
例えば、このような感じです。
/**
* 関数オブジェクトを利用するAPIの一例です。
* @param funcSpecifiedByUser 第1引数に主語、第2引数に目的語を取り、何らかの文章を返す関数です。この結果が標準出力に表示されます。
*/
public static void apiUsingFuncObj(BinaryOperator<String> funcSpecifiedByUser) {
System.out.println(funcSpecifiedByUser.apply("I", "you"));
}
// APIの利用例です。分かりやすさのために+で文字連結しています。
public static void main(String[] args) {
apiUsingFuncObj((subjectWord, objectWord) -> subjectWord + " love " + objectWord + ".");
// I love you. と表示されます。
}
このように自作するAPIの引数として関数インタフェースを採用することができます。利用者はラムダを使って関数インタフェースを実装した関数オブジェクトをつくり、APIに渡すことができます。
この時、自作するAPIの引数の型には、何らかの関数インタフェースを指定するわけですが、多くの場合、Javaに標準で用意されている関数インタフェースで事足ります。APIの提供者としては、余計な関数インタフェースを定義する必要はないのです。
利用者からすると、APIが標準の関数インタフェースを採用してくれた方が楽です。独自の関数インタフェースが定義されていた場合、その仕様を理解しなければならないからです。標準の関数インタフェースなら「ああ、あれね」という感じで、既存の知識をそのまま使えるので楽なのです。
ということで、APIのパラメータとして関数インタフェースを採用する場合、まずはJava標準の関数インタフェースを使うことを考えましょう。
Java標準の関数インタフェースには、どんなものがあるか
Java標準の関数インタフェースを紹介する記事は、世の中にたくさんありますので、詳しくはそちらに譲ります。ここでは、基本の6個の関数インタフェースを紹介することで、全体像を把握してください。
基本の関数インタフェース
関数インタフェース | シグニチャ | 説明 | メソッド参照の例 |
---|---|---|---|
UnaryOperator<T> |
T apply(T t) |
引数の型と同じ型を返します。 | String::toLowerCase |
BinaryOperator<T> |
T apply(T t1, T t2) |
引数の型と同じ型を返します。2つの引数をとります。 | BigInteger::add |
Predicate<T> |
boolean test(T t) |
引数を受け取ってbooleanを返します。 | Collection::isEmpty |
Function<T,R> |
R apply(T t) |
引数とは異なる型を返します。 | Arrays::asList |
Supplier<T> |
T get() |
引数をとらずに値を返します。 | Instant::now |
Consumer<T> |
void accept(T t) |
引数を受け取りますが、何も返しません。 | System.out::println |
それぞれの機能は直交しているわけではありません。その辺りはあまり気にしない方が良いでしょう。
考慮すべき点
-
UnaryOperator<Integer>
のように、基本の関数インタフェースの型パラメータに、ボクシングされた基本データクラスを指定するのはNGです。ボクシング、アンボクシングにはコストがかかるからです。代わりにIntUnaryOperator
のような、基本データ型に対応した関数インタフェースを使いましょう。 -
場合によっては、独自の関数インタフェースを作った方が良いです。以下のどれかに当てはまるなら、自作した方が良いかもしれません。
- 広く使われて、説明的な名前から恩恵を得られる。
- インタフェースに関連付けられた強い契約を持っている。
- 特別なデフォルトメソッドから恩恵を得られる。
-
自作した関数インタフェースには
@FunctionalInterface
を付けましょう。- 関数インタフェースであり、ラムダに使えることを読み手に伝えることができます。
- 誤って抽象メソッドを複数定義した場合に、コンパイルエラーで気づかせてくれます。それは、関数インタフェースを自作するあなたにとっても、その面倒を見る他のメンバーにとっても有難いことです。
-
APIを自作する場合には、同じ引数の位置に異なる関数型インタフェースを受け取る、同じ名前のメソッドを設けてはいけません。利用者が困ります。例えば、ExecutorServiceのsubmitメソッドがこれに当てはまります。
項目45 ストリームを注意して使う
ストリームとは?ストリームAPIとは?
ストリームとは、データ要素の有限あるいは無限なシーケンスのことです。Java 8では、このストリームを扱いやすくするためにストリームAPIが追加されました。
ストリームAPIでは、「ストリームパイプライン」を利用することで、ストリームを操作できます。
ストリームパイプラインは以下から構成されます。
- ソースのストリーム
- 中間操作
- 終端操作
パイプラインは遅延評価されますので、無限なシーケンスを扱うことができます。
注意すべき点
ストリームAPIは「オシャレ」ですが、乱用すると可読性が下がります。ストリームAPIの目的は「コードを簡潔にすること」ですので、その目的が達成されないような使い方はNGです。
具体的には、以下の点を考慮しましょう。
-
極論すると、ストリームAPIとループのどちらを使って実装すべきかは、書いてみないと分かりません。チームメンバーがどれだけストリームAPIに慣れているか、にもよります。状況に応じて、どちらの方が読みやすいかを見極めましょう。
-
次のような場合には、ストリームAPIが適している可能性が高いです。
- 均一に要素のシーケンスを変換する
- 要素のシーケンスをフィルターする
- 単一操作(たとえば、加算する、結合する、最小値を計算する)を使って、シーケンス中の要素を一つにまとめる
- 共通の属性でグループ化するなどして、シーケンス中の要素をコレクションに蓄積する
- シーケンスの要素から、特定の上限に合致する要素を検索する
-
ストリームAPIに限った話ではありませんが、ラムダのパラメータ名は可読性に大きく影響します。注意深くパラメータ名を考えましょう。
-
ストリームAPIでは、ヘルパーメソッドが重要な役割を果たすことがあります。ストリームパイプラインの中で実行する処理をヘルパーメソッドに切り出すと、そのヘルパーメソッドに名前を付けられるわけです。ストリームパイプラインからヘルパーメソッドを呼び出すと、ヘルパーメソッドの名前を見ればそこで何をやっているのか分かりますから、可読性が上がります。
-
後の中間操作で、前の中間操作のスコープで有効だったデータにアクセスしたい時があります。その場合、そのデータを前の中間操作からずっと覚えておくために、中間操作間で引き回すような実装はやめましょう。読みづらいだけです。代わりに、後の中間操作のスコープでアクセスできるデータをもとに、お目当てのデータを逆算しましょう。
項目46 ストリームで副作用のない関数を選ぶ
この項目の趣旨は「コレクターAPIを使いましょう」ということです。
ストリームパイプラインから「外側」にアクセスするのはNG
ストリームAPIを使うことで、何を得るべきでしょうか?ストリームAPIで狙うものは「何となくのカッコ良さ」ではありません。
最も重要なものは「簡潔さ」です。その他、「効率性」(CPUやメモリ負荷を低くする)も大事です。場合によっては「並列性」(マルチスレッドで処理することで処理性能を高める)も狙うべきでしょう。ストリームAPIを使ったとしても、こういったものを得られないと意味がないのです。
適切にストリームAPIを使うためには、個々のステージでの変換では、前のステージの変換結果だけにアクセスできるべきです。
逆に、ストリームパイプラインの外にある変数などにアクセスしてはいけません。こういったことをしてしまうと、少なくとも「簡潔さ」が失われます。コードの読み手は、ストリームパイプラインの外にあるものまで気にしないと、処理内容を読み解けなくなるからです。不具合が混入されやすくもなるでしょう。
例えば、以下のようなコードはNGです。
Map<String, Long> freq = new HashMap<>();
try(Stream<String> words = new Scanner(file).tokens()) {
words.forEach(word -> {
freq.merge(word.toLowerCase(), 1L, Long::sum);
});
}
/*
【何がダメなのか?】
forEach終端操作は、最終的な変換結果をストリームパイプラインの外部に返せません。
それにも関わらず、forEach終端操作を使って、最終的な変換結果をストリームパイプラインの外側に連携しようとしています。
この結果、ストリームパイプラインの外にある変数freqにアクセスしてしまっており、「簡潔さ」が失われています。
平たく言うと、読みづらいのです。
*/
ストリームパイプラインは、全体として1つの変換処理を構成するわけですから、最終的な変換結果をストリームパイプラインの外部に返すのが本分と言えるでしょう。こういった意味では、forEach終端操作が役立つ機会は限定的です。デバッグ用途や、ログ出力くらいかなと思います。
どうすればいいのか?
では、各変換ステージを独立したものにしつつ、ストリームパイプラインの外側に最終的な変換結果を返すにはどうすれば良いのでしょうか?そのために、「コレクター」という役割が用意されています。
具体的には、終端処理としてStream.collect(collector)
を呼び出します。collectメソッドの引数にコレクターのオブジェクトを渡します。このコレクターが、ストリームの要素を集めてくれます。多くの場合、何らかのCollectionに要素を集めます。collectメソッドはコレクターが集めた結果を、ストリームパイプラインの外側に返します。
標準で様々なコレクターが用意されています。これらのコレクターオブジェクトを取得するには、java.util.stream.Collectorsのファクトリメソッドを呼び出します。
以下に、標準で用意されているコレクターのうち、代表的なものを紹介します。
用途 | 用途 | コレクターオブジェクトの取得方法 | 備考 |
---|---|---|---|
ストリーム要素をListに集める | - | Collectors.toList() | |
ストリーム要素をSetに集める | - | Collectors.toSet() | |
ストリーム要素を任意のCollectionに集める | - | Collectors.toCollection(collectionFactory) | |
ストリーム要素をMapに集める | 単純に集める | Collectors.toMap(keyMapper, valueMapper) | |
〃 | キーが重複した場合に適切にマージしつつ集める | Collectors.toMap(keyMapper, valueMapper, mergeFunction) | |
〃 | ↑に加え、特定のMap実装を使うように指定する | Collectors.toMap(keyMapper, valueMapper, mergeFunction, mapFactory) | |
〃 | ストリーム要素をグループに分け、グループごとの要素リストをMapの値として格納する | Collectors.groupingBy(classifier) | |
〃 | ↑とほぼ同じだが、Mapの値としてリスト以外のコレクションを指定する | Collectors.groupingBy(classifier, downstream) | downstream(ダウンストリームコレクター)とは、サブストリーム(グループに属する要素の集合)を入力にして、コレクションを生成するコレクター(関数オブジェクト)です。例えば、Collectors.counting()で得られるダウンストリームコレクターを使い、グループごとの件数を集計できます。 |
〃 | ↑に加え、特定のMap実装を使うように指定する | Collectors.groupingBy(classifier, mapFactory, downstream) | downstreamの順序が↑と異なります。注意しましょう。 |
ストリーム要素内の最大値要素を取得する | - | Collectors.maxBy(comparator) | 比較ルールを示すコンパレータを引数にとります |
ストリーム要素内の最小値要素を取得する | - | Collectors.minBy(comparator) | 〃 |
ストリーム要素の文字列を単純に連結する | - | Collectors.joining() | |
ストリーム要素の文字列をデリミターで連結する | - | Collectors.joining(delimiter) |
もちろん上記以外にもあります。
なお、表では説明のためにCollectors.toList()などと書きましたが、実際に使うときはCollectorsに定義されている全てのメンバーをstaticインポートし、Collectors.
を省略できるようにしましょう。格段に読みやすくなります。
項目47 戻り値型としてStreamよりもCollectionを選ぶ
自作するAPIが要素のシーケンスを返却する、ということはよくあると思います。その場合、利用者によっては、その返却値をStreamとして扱いたい場合もありますし、Iterableとして扱いたい場合もあるはずです。
ですので、両方に対応できる戻り値型がベストです。具体的には、Collectionかそのサブタイプが良いです。なぜなら、CollectionインタフェースはIterableのサブタイプであり、streamメソッドが備わっているからです。
-
戻り値型をCollectionかそのサブタイプにできる場合は、以下の点を考慮しましょう。
- 要素数がメモリに格納するのに十分に小さい場合、ArrayListなどのコレクションの標準的な実装で大丈夫です。
- そうでない場合、小さいメモリ領域で済む特殊なコレクションの実装が必要です。
-
戻り値型をCollectionかそのサブタイプにできできない場合は、以下の点を考慮しましょう。
- IterableかStreamのどちらか自然な方を選択するのが好ましいです。
- 時には実装のしやすさで、どちらを採用するかが決まってしまうこともあります。
- いずれにしても、一方から一方に変換するアダプターが必要になってしまいます。アダプターを使うと、実装が散らかってしまいますし、遅いです。
項目48 ストリームを並列化するときは注意を払う
ストリームパイプラインの中でStream.parallel()
を呼び出すと、パイプラインの処理がマルチスレッドで実行されるわけですが、多くの場合、ひどい結果につながります。つまり、速くならないどころが、シングルスレッドで実行する場合よりも壊滅的に遅くなります。その理由を理解するのはかなり難しいですし、速くなるように実装するのも大変難しいことです。
ですので、よほどの理由や確証がない限り、ストリームを並列化するのはやめましょう。
第8章 メソッド
項目49 パラメータの正当性を検査する
メソッドやコンストラクタで受け付けたパラメータについては、冒頭で正当性を検査しましょう。そうしないと、不正なパラメータが原因で訳のわからない例外が発生したり、コードと関係のないところで思わぬ異常事態が発生する恐れがあります。
以下の点を考慮しましょう。
- パラメータに何らかの制約がある場合、Javadocの
@throws
にそのことを書きましょう。 - Java 7で追加されたObjects.requireNonNullメソッドは、Null検査に便利です。ぜひ使いましょう。
- Java 9から、リストと配列のインデックスを検査する用途に、ObjectsにcheckFromIndexSize, checkFromToIndex, checkIndexメソッドが追加されました。用途に合うなら便利です。
- 正当性検査の処理コストが高く、かつ処理の途中で正当性検査が暗黙的に行われる場合、明示的に正当性検査を設けるべきではありません。
項目50 必要な場合、防御的にコピーする
APIとして公開するクラスは、たとえ利用者に悪意がなくても、利用者からひどい扱いを受けると考えた方が良いです。具体的には、そのクラスの不変式が壊されるような使い方をされるもの、と考えましょう。
ですから、利用者がどんなに不適切な使い方をしようとも、クラスの不変式が崩されないような工夫をしておくべきです。それが防御的コピーです。具体的には以下の対処をしましょう。
- 利用者から可変オブジェクトを受け取り、それを状態として保存するのであれば、そのオブジェクトのコピーを作り、その参照を保存しておきましょう。その場合、受け取ったオブジェクトのcloneメソッドを使うのは危険です。そのサブクラスを信頼できないからです。
- クラスが状態として持つ可変オブジェクトや配列を利用者に返却する場合、そのオブジェクトのコピーを作り、それを返却しましょう。
- そもそも、できるだけ不変クラスを使うようにしましょう。上記のようなことを気にしなくてもよくなります。特に、Java 8からはjava.util.Dateではなく、java.time.Instant(と同じパッケージの他のクラス)といった不変クラスを使うようにしましょう。
ただし、防御的コピーをしない、という判断をするときもあります。それは以下のような場合です。そういった場合は、Javadocにその旨の記載をするといった対処が最低限必要です。
- 防御的コピーの処理コストが許容できない場合。
- 何らかの理由で利用者を信頼できる場合。
- 不変式が崩されても、困るのは利用者だけである場合。
項目51 メソッドのシグニチャを注意深く設計する
この項目はTIPS集です。これらを守ると、自作するAPIが学習しやすく、使いやすいものになります。そして、エラーにつながりづらくもなります。
- メソッド名を注意深く選びましょう。
- 理解可能であること。
- 同じパッケージ内の他の名前と矛盾がないこと。
- 存在する広範囲のコンセンサスと矛盾がないこと。
- 短いこと。
- 便利なメソッドを提供しすぎないようにしましょう。
- メソッドが多すぎると、保守する側も使う側も大変です。
- メソッドを複数組み合わせて行える処理を、さらに一つの便利なメソッドとして提供するのは、頻繁に使われることが明らかな場合だけにしましょう。
- パラメータ数は4個以下にしましょう。
- 利用者は多くのパラメータを覚えられません。
- 同一の型のパラメータが何個も続くのはNGです。うっかり順序を間違えてもコンパイラは教えてくれません。
- パラメータを減らす方法は以下の通りです。
- 複数のメソッドに分割すること。ListのsubList, indexOf, lastIndexOfがそのお手本です。
- パラメータの集まりを保持するヘルパークラスを作りましょう。staticのメンバークラスになるでしょう。
- Builderパターンをメソッド呼び出しに適用しましょう。最後にexecuteしますが、その時にパラメータの正当性チェックをすると良いでしょう。
- パラメータの型にはできる限りインタフェースを採用しましょう。
- 異なる実装に切り替えられる柔軟性が生まれます。
- booleanパラメータよりも二つの要素をもつenum型を使いましょう。
- 読みやすいですし、拡張性もあります。
項目52 オーバーロードを注意して使う
APIを自作するとき、同じパラメータ数の複数のオーバーロードされたメソッドやコンストラクタを提供するのはNGです。利用者の意図する通りに動作せず、利用者を混乱させる恐れがあります。
そこで、以下のように対応しましょう。
- メソッドであれば、メソッド名を変えましょう。
- コンストラクタであれば名前を変えられませんので、staticファクトリメソッドで実装しましょう。
- どうしてもオーバーロードせざるを得ず、それらに同じ振る舞いをさせたい場合、両者が同じ振る舞いになることを保証するために、限定的な方から一般的な方に転送しましょう。
項目53 可変長引数を注意して使う
可変長引数メソッドは便利です。ただし、以下の点に注意しましょう。
- 可変長引数に必須パラメータが含まれると、可変長引数を利用者が誤って指定しなかった場合にコンパイルエラーになってくれません。必須パラメータを可変長引数に含めず、可変長引数とは別に定義しましょう。
- 可変長引数は、メソッドが呼び出される度に配列を生成することで実現されています。そういった配列生成のコストがあることを認識して、APIを定義しましょう。そのコストが受け入れられない場合は、よく使われる引数の数を調べて、その引数のメソッドを個別に定義しましょう。
項目54 nullではなく、空コレクションか空配列を返す
データのシーケンスを返却するメソッドで、場合によってnullを返却するものがありますが、これはNGです。
理由は以下のとおりです。
- 利用者はnullを扱うコードを書く手間が生じます。そもそも、nullへの対応が漏れてしまうかもしれません。
- APIを実装する側にとって、nullを返却する処理によって不要にコードが散らかります。
【NG】
public List<Cheese> getCheeses() {
return cheesesInStock.isEmpty() ? null
: new ArrayList<>(cheesesInStock);
}
【OK】
public List<Cheese> getCheeses() {
// わざわざ条件分岐させる必要はありません。
// これで空のリストを返せます。
return new ArrayList<>(cheesesInStock);
}
空のリストを毎回生成するコストは、無視できるほど小さい場合が多いです。万一それが問題になるのであれば、Collections.emptyList()などを使って、不変の空コレクションを返却するようにしましょう。ただし、そうすべき局面は滅多にありませんので無闇にそうしてはいけません。
項目55 オプショナルを注意して返す
Java 8より前では、値を返さないメソッドを書くために、以下の手段がとられていました。
- nullを返す。
- 例外をスローする。
これらの手段に問題があるのは明らかで、Java 8から良い手段が追加されました。それがOptionalです。
APIからOptionalを返すことで、利用者に戻り値が空である可能性を認識させ、空だった場合のハンドリングを利用者に強制させることができます。
Optionalオブジェクトを生成する方法は以下の通りです。
Optionalの生成方法 | 説明 |
---|---|
Optional.empty() | 空のオプショナルを返します。 |
Optional.of(value) | nullでないvalueを含むオプショナルを返します。nullが渡されるとNullPointerExceptionがスローされます。 |
Optional.ofNullable(value) | nullの可能性がある値を受け付けて、nullが渡されたら空オプショナルを返します。 |
APIの利用者は、次のようにOptionalをハンドリングします。
// 空だった場合、orElseで指定したデフォルト値を使うようにしています。
String lastWordInLexicon max(words).orElse("No words...");
// 空だった場合、orElseThrowで指定した例外ファクトリによって例外がスローさせるようにしています。
// 例外の生成コストが、実際に例外がスローされるときだけに発生するように、例外ファクトリを指定するようになっています。
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
// オプショナルが空でないと分かりきっている場合は、いきなり値を取得できます。
// 万が一オプショナルが空の場合は、NoSuchElementExceptionがスローされます。
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
Optionalには便利なメソッドが色々と定義されています。例えば、orElseGetメソッドはSupplier<T>
を受け取り、Optionalが空のときにそれを呼び出すことができます。このおうちisPresentメソッドは、他のメソッドでやりたいことができなかったために存在するものであって、これを積極的に使うべきではありません。
Optionalを要素に持つStreamを扱う場合、空でないものだけにフィルターしたい場合はよくあります。その場合、以下のように実装すると良いでしょう。
// Java 8以前
// Optional.isPresent()が活躍する数少ない局面です。
streamOfOptional
.filter(Optional::isPresent)
.map(Optional::get)
// Java 9以降
// Optionalに追加されたstream()メソッドでは、
// Optionalに値があればその要素を含み、値が無ければ何も含まないStreamを返します。
// この性質を利用して、以下のように実装できます。
streamOfOptional
.flatMap(Optional::stream)
Optionalについては、以下に注意しましょう。
- 当然ながら、コレクションなどをOptionalで包んで返却してはいけません。コレクションであれば、空のコレクションを返せば良いのです。
- ボクシングされた基本データ型のOptionalを返してはいけません。無駄な処理コストが発生し、それは無視できない場合が多いためです。
- メソッドの戻り値を返す以外の用途でOptionalを使うべきではありません。例えば、インスタンスフィールドにOptionalを保持するような使い方は、多くの場合で不適切です。他に手段があるはずです。
項目56 すべての公開API要素に対してドキュメントコメントを書く
ドキュメントコメント(Javadoc)はなぜ必要か?【why】
利用者に誤った使い方をさせないため、公開APIについてはJavadocを書くべきです。
更に、保守メンバーが誤った方向性で修正しないように、また、保守メンバーがもとの実装の意図や目的を理解できるようにして、APIを保守できるようにするために、公開されていないものについてもJavadocを書くべきです。
実務においては、こういった目的が果たせるような最小限の内容を書くべきでしょう。コードは芸術作品ではなく、利益を生み出す手段です。Javadocにかけるコストが実益に見合うか、という点も重要です。
どうすれば良いか?【how】
以下の通りです。
- APIと利用者で守るべき「契約」を明記しましょう。
- 事前条件:利用者がAPIを呼び出すために成立していなければならない事柄です。
- 事後条件:呼び出しが正常に完了した後に成立していなければならない事柄です。
- 副作用:システム状態の観察可能な変化です。バックグラウンドでスレッドを起動する、など。
- パラメータ:基本的には名詞句で書きましょう。
- 戻り値:基本的には名詞句で。APIの概要説明と同じなら省略可です。
- スローされる例外:チェック/非チェックに関わらず書きましょう。例外クラス名と、スローされる条件を書きましょう。
- コードは
{@code}
で表現しましょう。 - 例えば「this list」(このリスト)の「this(これ)」はメソッドが呼び出されたオブジェクトを表します。
- 継承して使ってもらうクラスの場合、自身の他のメソッドを呼び出していることを
@implSpec
に書きましょう。そうしないと利用者が誤った利用をしてしまいます。少なくともJava 9ではこのタグはデフォルトでは有効にならないので、注意すべきです。 -
< > &
といった文字を表現したい場合は、{@literal}
を使いましょう。 - ドキュメントコメントの冒頭には概要説明を書きましょう。
- 異なるAPIが同じ概要説明であってはならなりません。
- 概要説明はピリオド(.)によって終了してしまうので要注意です。適切に
{@literal}
などを使うべきです。 - 概要説明では、簡潔さのために主語は多くの場合省略されます。英語の場合は三人称現在形を使うべきです。
- クラス、インタフェース、フィールドについては、名詞句を使うべきです。
- Java 9からは、Javadoc画面の右上にある検索ボックスで、キーワード検索をできます。このとき検索にHITするのは、クラス名やメソッド名だけでなく
{@index}
で明示的に設定されたキーワードです。 - ジェネリック型やメソッドについては全ての型パラメータを文書化しましょう。
- enum型の場合は、全ての定数それぞれに対してドキュメントコメントをつけましょう。
- アノテーションについては、全てのメンバーを文書化しましょう。
- パッケージレベルのドキュメントコメントはpackage-info.javaに記載しましょう。
- マルチスレッドで動作させても大丈夫なのか、について記載しましょう。
- シリアライズ可能なのか、どのような形式でシリアライズされるか、について記載しましょう。
-
{@inheritDoc}
は利用しづらく、制約があると言われています。 - 個々のクラスなどだけではなく、それらの関係性を説明するドキュメントが必要な場合もあります。
- 以上のルールは自動的に検査されるべきです。Java 8以降ではデフォルトで検査されます。checkstyleなどでは更にしっかりと検査されます。
第9章 プログラミング一般
この章で書かれていることは「当たり前」のことです。特筆すべきことだけ解説していきます。
項目57 ローカル変数のスコープを最小限にする
特筆すべきことはありません。
項目58 従来のforループよりもfor-eachループを選ぶ
細かいですが、以下の点だけ覚えておきましょう。
- Collection.removeIf(filter)を使えば、従来のforループに頼らなくても、走査しつつ特定の要素を削除できます。
項目59 ライブラリを知り、ライブラリを使う
java.lang, java.util, java.io, java.util.concurrentをはじめ、主要なサブパッケージにどんな機能が備わっているのかを、知っておきましょう。知らなければ、使うことができません。リリースで追加される新機能は必ずチェックしましょう。
たとえば、Java 7の時点では、RandomよりThreadLocalRandomを使うべきです。こちらの方が速いです。
項目60 正確な答えが必要ならば、floatとdoubleを避ける
floatとdoubleは、正確な結果に近似する計算を素早くおこなうために存在します。正確な計算結果を得るために存在していません。ですので、正確な計算結果が欲しい場合は、これらを使うべきではありません。
代わりにBigDecimalを使いましょう。
ただ、BigDecimalには「遅い」という欠点があります。BigDecimalの遅さが許容できない場合は、intやlongといった整数を使って計算しましょう(ドルをセントに換算して計算するなど)。9桁までならint、18桁までならlongが使えます。それを超える場合はBigDecimalを使うしかありません。
項目61 ボクシングされた基本データよりも基本データ型を選ぶ
特筆すべきことは特にありません。
項目62 他の型が適切な場所では、文字列を避ける
例えば本質的に数値を表す文字列ならintなどで扱いましょう、といった感じの当たり前の内容です。
細かいですが、以下の点だけ覚えておきましょう。
- Stringは不変なので、同じ文字列を表すものであればJVM全体で同じインスタンスが使用されます。ですので、スレッド間で同じ名前空間を共有している場合のキーとしては、使えません。
項目63 文字列結合のパフォーマンスに用心する
Stringは不変なので+
で連結されるたびに新たなインスタンスが生成されてしまいます。
+
を何度も適用して文字列を結合するには、StringBuilderを使いましょう(スレッドセーフではないことに注意しましょう)。
だからといって、+
での連結は全て悪か、というとそうでもありません。1個や2個の+
であれば、簡潔でパフォーマンス上の問題は無いでしょう。
項目64 インタフェースでオブジェクトを参照する
インタフェースや抽象クラスで参照した方が柔軟性は高いです。後で実装クラスを差し替えられるからです。
項目65 リフレクションよりもインタフェースを選ぶ
リフレクションを使うべき局面は極めて限定的です。無闇に使わないようにしましょう。
項目66 ネイティブメソッドを注意して使う
CやC++で実装されたメソッドを呼び出せる機能がJNIですが、これを自分で実装すべき局面はおそらくありません。万が一それをやる場合は注意しましょう、という内容です。本当にそんな局面に直面してから、この項目を読めばよいと思います。
PJで何らかの製品を使う必要になり、それを使う唯一の手段がJNIである、ということが稀にあります。 JNIで呼び出す処理はJVMの管理外で動きますから、メモリの破壊といったリスクが伴います。そういった危険性を認識し、徹底的にテストしましょう。
項目67 注意して最適化する
最適化とは極論すると、「速く」するために低レイヤーに着目したコードを書くことです。最適化というのは、それが必要になってはじめて実施するものであって、はじめからやるものではありません。なぜなら、最適化をするとコードが散らかりますし、そもそも効力が無い恐れがあるからです。
やるべきことは以下の通りです。
- アーキテクチャレベルで、パフォーマンスの問題がないようにすべきです。アーキテクチャを注意深く設計しましょう。特に注意すべき箇所は、公開API、外部との通信部、データ永続化部です。これらはパフォーマンスに大きく影響するとともに、後から差し替えることがほぼ不可能です。
- 注意深く公開APIを設計しましょう。
- 公開する型を可変にすると防御的コピーが必要になってしまいます。
- 不適切に継承を採用すると、スーパークラスがサブクラスの足かせになり、サブクラスをチューニングできなくなります。
- インタフェースに対して利用してもらわないと、後から効率的な実装に差し替えられなくなります。
- より良いパフォーマンスを狙ってAPI設計をねじ曲げると、そのAPIをサポートをし続ける苦労の方が大きくなります。
- 最適化が必要そうな場合、プロファイリングツールを使ってボトルネックを特定しましょう。例えば、jmhは可視性に優れたマイクロベンチマーク・フレームワークです。
- どうしても最適化する場合は、本当に良くなったかを確認するため測定しましょう。バイトコードが動作するハードウェアによって、実際にどのようにプログラムが処理されるかが変わってきます。今日では様々なハードウェアがあるわけですから、測定しない限り、最適化が功を奏したかどうかは判定できません。
項目68 一般的に受け入れられている命名規約を守る
特筆すべきことは特にありません。当たり前の話です。
第10章 例外
項目69 例外的状態にだけ例外を使う
タイトルの通りです。理由は説明するまでも無いでしょう。
項目70 回復可能な状態にはチェックされる例外を、プログラミングエラーには実行時例外を使う
異常事態を表すクラスは、以下の構造(継承関係)をとっています。それぞれの使い分けと注意事項についても記載しておきました。
説明 | |||
---|---|---|---|
Throwable | |||
Errorとそのサブタイプ | JVMが使うものです。自作するのはやめましょう。 | ||
Exception | |||
RuntimeExceptionとそのサブタイプ | いわゆる「チェックされない例外」です。利用者が何らかの誤りを犯している状況であれば、これをスローすべきです。利用者にハンドリングされても害になるだけだからです。 | ||
RuntimeExceptionのサブタイプでないもの | いわゆる「チェックされる例外」です。呼び出し元が適切に回復できるような状況であれば、これをスローすべきです。利用者に回復処理を強制させることができるからです。利用者が適切なハンドリングをできるように、例外クラスに情報取得用のメソッドを用意しましょう。ただし、項目71の通り、まずはOptionalを返すことを検討しましょう。 |
項目71 チェックされる例外を不必要に使うのを避ける
APIからチェックされる例外をスローするのであれば、それが本当に必要かどうかを吟味すべきです。その例外を利用者が受け取ったとしても、実質的には何もできないのであれば、スローしてはいけません。
利用者に何らかの回復処理を強制する必要性とその価値がある場合、チェックされる例外をスローするAPIは、利用者にとっては鬱陶しくも感じます。以下の難点があるからです。
- try-catchを書かないといけません。面倒ですし、コードが散らかります。
- ストリーム内でそのAPIを使えません。ストリームではチェックされる例外をハンドリングできないからです。
こういった難点を解消・軽減する手段として、Optionalを返す、というものがあります。例外をスローする代わりに、空Optionalを返すということです。ただし、Optionalには例外クラスのように付加情報を持たせることができません。バランスをみて判断しましょう。
項目72 標準的な例外を使う
タイトルの通りです。理由は説明するまでも無いでしょう。
項目73 抽象概念に適した例外をスローする。
Javaでは例外を伝播させることができます。下位レイヤーでスローされた例外が、複数層を経て上位レイヤーに伝播していくと、下位レイヤーのコードと上位レイヤーのコードが結合してしまいます。つまり、両者を独立して修正するのが難しくなってしまいます。
このため、以下を心がけましょう。
- そもそも例外を発生させないようにしましょう。つまり、上位レイヤーは下位レイヤーを呼び出すにあたり、下位レイヤーを呼び出すにあたっての事前条件が成立していることをチェックしましょう。それによって伝播をできるだけ回避しましょう。
- どうしても伝播させるべきであれば、上位レイヤーと下位レイヤーを分離するために、適切な抽象レベルのレイヤーで、適切な抽象レベルの例外を新たにスローしましょう。その際、元の例外をcauseとして設定すべきです。例外が起きたときの調査がしやすくなるからです。
項目74 各メソッドがスローするすべての例外を文書化する
項目56で説明されていることとかなり重複します。そちらをしっかり理解すれば大丈夫でしょう。
項目75 詳細メッセージにエラー記録情報を含める
例外発生時の調査のために、例外の詳細メッセージはとても重要な役割を果たします。
- 例外の原因となった、全てのパラメータとフィールドの値を含めましょう。
- ログは一般のプログラマ等も見ますので、当然ながらセキュアな情報を含めてはいけません。
- 詳細メッセージに、Javadocとソースコードから容易に読み取れることを含めるのは無駄です。
- 例外を自作するなら、調査に必要な情報を利用者に必ず設定してもらうために、例外クラスのコンストラクタ引数でそういった情報を受け取れるようにすべきです。
項目76 エラーアトミック性に務める
状態を持つオブジェクトのメソッドが呼び出され、メソッド内部で異常事態が発生したとします。その後、オブジェクトがメソッド呼び出しの前の状態に戻る、あるいはその状態と変わらないのが望ましいです。この性質のことを「エラーアトミック性」と呼びます。
この性質は、チェックされる例外をスローする場合に重要です。チェック例外がスローされる、ということは利用者が何らかの回復処理をできる、ということだからです。元の状態に戻らなければ、利用者が回復処理をすることは不可能でしょう。
エラーアトミック性を実現するために、以下の方法をとりましょう。
- そもそもクラスを不変なものとして作りましょう。不変であれば、何も考える必要はありません。
- 以下の処理が、「オブジェクトの状態を変更する処理」の前に実行されるようにしましょう。
- パラメータの正当性チェック
- 失敗するかもしれない処理
- オブジェクトの一時的コピーに対して操作を行い、操作が完了したらオブジェクトの内容を一時的コピーの内容で置き換えましょう。
エラーアトミック性は望ましいですが、必ず達成できるわけでもないですし、場合によっては実現にコストがかかりすぎることもあります。もしもエラーアトミック性が達成できない場合は、メソッドが失敗した場合にオブジェクトがどのような状態に置かれるか、とういことをJavadocに明記しましょう。
項目77 例外を無視しない
タイトルのとおりです。理由は説明するまでも無いでしょう。
稀ですが、例外を無視するのが適切な場合もあります。その場合は、必ずコメントにその理由を残しましょう。
第11章
項目78 共有された可変データへのアクセスを同期する
同期では「相互排他」と「スレッド間通信」が実現される
複数のスレッドで一つのオブジェクトのデータを読み書きする場合、以下の2点に注意を払う必要があります。
- 相互排他:あるスレッドがオブジェクトを変更している間に、他のスレッドからそのオブジェクトが不整合な状態に見えることを防ぐ必要があります。
- スレッド間通信:あるスレッドが行った変更が、他のスレッドから確実に見える必要があります。
後者は忘れられがちなので、注意が必要です。適切に実装しなければ、あるスレッドが行った変更が他のスレッドから永遠に見えないように、コンパイラが勝手に最適化したりします。その結果、タイミングによって発生したり発生しなかったりするような厄介な不具合につながります。
後者に着目したものにvolatile修飾子があります。これをフィールドにつけると、スレッドごとのキャッシュが利用されなくなるため、最後に書き込まれた値が読み込みをするスレッドから見える、ということが保証されます。ただし、volatileでは相互排他は行われません。複数スレッドでオブジェクトを共有する局面では、上記2点の両方を満たす必要があることがほとんどなので、volatileで済む局面は少ないでしょう。
上記2点の両方を満たすためには、同期を行う必要があります。具体的には、メソッドにsynchronized修飾子をつけましょう(syncronizedブロックだけでは、他スレッドでの値の可視性は保証されません)。場合によってはjava.util.concurrent.atomicパッケージが適切かもしれません(AtomicLongなど)。
おまけ:「アトミック性」とは何か?
マルチスレッドの文脈では「アトミック性」という用語がよく出てきますので、理解しておくことをオススメします。アトミック性とは、データに対する複数の操作が、他のスレッドから単一の操作のように見える性質のことです。
具体的なこととしては、以下の点を理解しておけば良いと思います。
- ふつうlongとdoubleの変数はアトミック性は保証されません。32bitずつの2つの書き込みが別々に行われる結果、別のスレッドから中途半端な状態が見えてしまいます。longとdoubleであっても、変数にvolatileをつけることで、他のスレッドからは64bitの1つの書き込みとして見えるようになります。
- インクリメント演算子
i++
はアトミックではありません。変数の読み込みと書き込みという2つの操作が行われるわけですが、他のスレッドからはそれらの操作は別々の操作に見えます。その結果、あるスレッドでの読み込みと書き込みの間に、他のスレッドからの読み込みや書き込みが入り込めてしまいます。この問題に対応したライブラリがjava.util.concurrent.atomicパッケージです。
項目79 過剰な同期は避ける
過剰に同期を利用すると、悪いことが起こります。
過剰に同期を利用する、というのはどういうことか?
過剰に同期を利用する、とは以下を指します。
- 同期すべきでないところで同期を利用する。
- 同期すべきだけど、同期のスコープが大きすぎる。
- 単純にスコープが大きすぎる。
- 同期のスコープで、利用者に制御を譲ってしまう(例:利用者がオーバーライドするメソッドや、利用者から渡された関数オブジェクトを、同期のスコープ内で呼び出す)。
過剰に同期を利用すると、どうなるのか?
過剰に同期を利用すると、以下の問題が起こります。
-
正確性の問題(書籍で紹介されているコード例が、とても参考になります)
- 1つのスレッドで重複してロックをとってしまい、そのスレッド自身でクラスの不変式を破ってしまいます。Javaのロックは再入可能です。つまり、ロックを取得した後に再度同じロックを取得しようとしても、エラーになりません。
- デッドロックが起こります。つまり、複数のスレッドが互いのロック解放を待ち合ってしまいます。
-
パフォーマンスの問題
- ロックを取得したスレッド以外のスレッドが、必要以上に待たされます。
- 全てのCPUコアに、メモリの一貫したビューを持たせるために遅延が発生します。マルチコアの時代には、このコストが非常に大きくなります。
- JVMがコード実行を十分に最適化できなくなります。
どうすればいいのか?
以下につきます。
- できるだけ同期をしないようにしましょう。
- 可変クラスを作る場合、以下の選択肢があります。できるだけ前者を採用しましょう。
- 利用者に同期してもらうようにします。できるだけこちらを採用しましょう。
- クラス内部で同期をします。こちらを採用すると、利用者に同期をする/しないの選択肢を提供できません。まずは利用者に同期してもらうことを検討し、それだと問題がある場合のみクラス内部での同期を採用しましょう。
- 同期するならスコープを最小限にしましょう。特に、同期のスコープ内で、利用者に制御を任せるような処理を呼び出さないようにしましょう。
項目80 スレッドよりもエグゼキュータ、タスク、ストリームを選ぶ
独自にThreadクラスを使うのではなく、java.util.concurrentパッケージにある
エグゼキュータフレームワークを使いましょう。というのが、本項目のメッセージです。
この項目に書いてある内容は、何と言うか中途半端です。この項目で紹介されている「Java Concurrency in Practice(Java並行処理プログラミング ―その「基盤」と「最新API」を究める―)」を読んだ方が良いでしょう。
項目81 waitとnotifyよりも並行処理ユーティリティを選ぶ
以前にwaitとnotifyで実現していたことを、現在ではjava.util.concurrentパッケージの高レベルな並行処理ユーティリティで、簡単に実現できるようになっています。
高レベルな並行処理ユーティリティ
java.util.concurrentパッケージの高レベルな並行処理ユーティリティは、以下の3つに分類されます。
- エグゼキュータフレームワーク
- 項目81を参照ください。
- コンカレントコレクション
- List, Queue, Mapといった標準のインタフェースを実装しており、内部で適切な同期処理を行います。同期しつつも高パフォーマンスを実現してくれます。
- 外部から同期処理に介入することはできませんので、コンカレントコレクションに対する基本操作を複数組み合わせ、それらをアトミックにすることは、外部からはできません。これを実現するために、基本操作を複数組み合わせてアトミックな操作をするためのAPIが用意されています。(MapのputIfAbsentなど)。
- 基本操作を複数組み合わせた処理は、Mapなどのインタフェースにそのデフォルト実装として組み込まれていますが、アトミックになるのはコンカレントコレクションの実装のみです。Mapなどのインタフェースにデフォルト実装が組み込まれているのは、アトミックでなくても便利だからです。
- 同期されたコレクション(Collections.sysnchronizedMapなど)は過去の産物であり、遅いです。特段の理由がない限り、コンカレントコレクションを使いましょう。
- Queueなどの実装においては、操作が完了するまで待つ「ブロックする操作」をできるように拡張されています。例えばBlockingQueueのtakeメソッドでは、キューが空なら待ち、キューに登録されたら処理を行います。エグゼキュータフレームワークでは、この仕組みが利用されています。
- シンクロナイザ
- スレッド間の掲示板のような働きをして、スレッド間で足並みを揃えることを可能にします。
- よく使われるのはCountDownLatchです。たとえば親スレッドが
new CountDownLatch(3)
のオブジェクトを生成し、子スレッドA, B, Cを起動させて、CountDownLatchオブジェクトのawait()
を呼び出し、待ちに入ります。スレッドA, B, CがこのCountDownLatchオブジェクトのcountDown()
を呼び出すと、親スレッドの待ちが解除され、親スレッドの後続処理が実行されます。一連の処理の裏側ではwait, notifyが実行されていますが、ややこしい部分をCountDownLatchが全て担当してくれています。
※並行処理の文脈に限った話ではありませんが、時間間隔の測定には、System.currentTimeMillis()ではなく、System.nanoTime()を使いましょう。後者の方が正確かつ高精度で、システムのリアルタイムクロックの調整による影響を受けません。
waitとnotify
保守などでwaitとnotifyを使ったコードの面倒を見ることもあるでしょう。その場合は、waitとnotifyの定石を把握しておくべきです。
この部分はほぼ以下と同じですので、詳しい説明は省略します。
項目82 スレッド安全性を文書化する
書いてあること全てが重要です。この項目の内容を理解するのは簡単ですので、特に解説する必要は無いでしょう。
項目83 遅延初期化を注意して使う
稀に、フィールドの値が必要となるまで、そのフィールドの初期化を遅らせることがあります。これを遅延初期化と呼びます。
遅延初期化の目的は、以下のとおりです。
- 最適化のため(「速く」するため)
- 初期化に何らかの循環処理があり、その循環を断ち切るため
最適化を目的とする場合、「それをやって本当に意味があるのか?」ということを良く考えましょう。効果が極めて小さかったり、逆効果となる可能性もあります。そのためにコードを散らかすのは論外です。
通常の初期化で問題ない場合、以下のようにfinalをつけて初期化しましょう。
private final FieldType field = computeFieldValue();
staticフィールドであっても同じです。
遅延初期化の方法
フィールドの型がオブジェクトの参照の場合を例に説明します。
なお、基本データでもほぼ同じです。本データの場合はnullではなく、デフォルト値0に対して検査する、という差異があるのみです。
①初期化循環を断ち切る場合
以下のように「同期されたアクセッサー」を使いましょう。単純かつ明快な方法です。
private FieldType field;
// synchronizedメソッドにより、
// 「相互排他」と「スレッド間通信」の両方が実現されます。
private synchronized FieldType getField() {
if (field == null)
field = computeFieldValue();
return field;
}
staticフィールドであっても同じです。
②最適化のためにstaticフィールドを遅延初期化する場合
遅延初期化しない場合は以下のようにしますが・・・
private static final FieldType field = computeFieleValue();
最適化のためにstaticフィールドを遅延初期化する場合は、以下のように「遅延初期化ホルダー・クラス・イディオム」を使いましょう。
private static class FieldHolder {
// 遅延初期化したいフィールドを、staticなメンバークラスに持たせます。
static final FieldType field = computeFieleValue();
}
private static FieldType getField() {
// ・フィールドの値が必要になった時点で初めて、staticなメンバークラスをロードします。
// クラスがロードされた結果として、staticフィールドの初期化処理が実行されます。
// ・典型的なJVMでは、クラスを初期化する時にフィールドへのアクセスを同期するので、
// 明示的な同期処理を書く必要はありません。
return FieldHolder.field;
}
③最適化のためにインスタンスフィールドを遅延初期化する場合
以下のように、「二重チェックイディオム」を使いましょう。
// getFieldメソッド中のsyncronizedブロックだけでは、初期化されたフィールドが他スレッドから見えません。
// volatileをつけることで、初期化されたフィールドを他スレッドから即時に見えるようにします。
private volatile FieldType field;
private FieldType getField() {
// fieldの読み込みを1度だけにして性能を向上させるため
// ローカル変数resultにfieldの値を代入しています。
FieldType result = field;
// いったん初期化されたらロックは不要なので、
// 1回目の検査ではロックしません。
if (result != null)
return result;
// 2回目の検査のときに初めてロックします。
synchronized(this) {
if (field == null)
// ロックしないと、このタイミングで(ifの判定とcomputeFieldValueメソッドの呼び出しの間で)
// 他スレッドがフィールドを初期化してしまう恐れがあります。
field = computeFieldValue();
return field;
}
}
項目84 スレッドスケジューラに依存しない
スレッドスケジューラに依存するのはなぜNGか?
スレッドスケジューラとは、JVMの構成要素の一つで、その名の通りスレッドのスケジューリングをします。JVMの構成要素の一つということは、つまるところOSの機能を利用するわけで、その振る舞いはOSに強く依存します。
このため、私たちはスレッドスケジューラの振る舞いを正確に知ることはできませんし、制御することもできません。もしプログラムの正しさやパフォーマンスが、スレッドスケジューラに依存するものであれば、ある時はうまく動くけどある時はうまく動かない、ある時は速いけどある時は遅い、といった不安定な動きをするでしょう。また、違うOSのJVMに移植すると、移植前の環境と同じように動かないかもしれません。
こういった理由から、プログラムの正しさやパフォーマンスが、スレッドスケジューラに依存するものであってはいけません。
どうすればいいのか?
以下の点を考慮しましょう。
- 実行可能(RUNNABLE)なスレッドの平均数を少なく保ちましょう。具体的には、プロセッサの数よりはるかに大きくしないようにしましょう。実行可能(RUNNABLE)なスレッドの数が多いと、スケジューラに選択肢を与えることになり、結果、スケジューラに依存してしまいます。なお、スレッドの取り得る状態については、こちらの公式ドキュメントをご覧ください。実行可能(RUNNABLE)なスレッド数を少なく保つために、以下を考慮しましょう。
- スレッドの処理が終わったら、そのスレッドを待ちの状態(WAITING)にしましょう。
- エグゼキュータフレームワークでは
- スレッドプールを適切な大きさにしましょう。
- タスクの大きさを適度にしましょう。タスクの粒度が小さすぎると、タスクにスレッドをディスパッチするコストが大きくなり、遅くなります。
- while(true)の中で、ひたすら条件判定を繰り返し「待ち」を実現することを「ビジーウェイト」と呼びます。これをやってしまうと、以下の問題につながります。
- スレッドスケジューラの予想のつかない処理に対して、プログラムを脆弱にします。
- プロセッサへの負荷を増大させ、他のスレッドが処理されにくくなくなります。
- Thread.yieldを使うのはやめましょう。このメソッドは、「自スレッドに割り当てられているCPU使用量を、他のスレッドに譲っても良いよ」という意思をスケジューラに示すものです。これを受けたスケジューラがどのように振舞うのかは、予測がつきませんし不安定なので、Thread.yieldを使ってはいけません。
- スレッドの優先順位を調整するのはやめましょう。Thread.setPriority(int newPriority)でスレッドの優先順位を調整できるのですが、Thread.yieldと同様、これを受けたスケジューラがどう振舞るのかは、よく分かりません。
第12章 シリアライズ
項目85 Javaのシリアライズよりも代替手段を選ぶ
システム内で1箇所でもディシリアライズをしている場合、攻撃者によって作られた不正なオブジェクトをディシリアライズする危険性があります。ディシリアライズの過程で不正なコードが実行され、システムがハングする等、致命的な問題につながります。
こういったセキュリティ上の問題点があるので、シリアライズ/ディシリアライズを一切使うべきではありません。代わりに、JSONやprotobufといった技術を使いましょう。これらの技術を採用することで、先述のセキュリティ問題の多くを回避するとともに、クロスプラットフォームのサポート、高いパフォーマンス、ツールのエコシステム、コミュニティによるサポートといった利点を得られます。
既存システムの保守などで、どうしてもシリアライズ機構を利用せざるを得ない場合、以下のとおり対策しましょう。
- 信頼されないデータをディシリアライズしないようにしましょう。
- Java 9で追加されたjava.io.ObjectInputFilterを使って、ホワイトリストにあるクラスのみをディシリアライズするようにしましょう。ただし、これでは一般的なクラスだけで構成された攻撃コードには対応できません。
項目86 Serializableを最新の注意を払って実装する
どうしてもシリアライズ機構を採用する場合、それに伴う代償を認識しておくべきです。代償は以下のとおりです。
- いったんリリースされると、Serializableを実装したクラスの実装を変更する柔軟性が、ほぼ失われます。
- バグやセキュリティホールの可能性を増大させます。
- 新しいバージョンのクラスのリリースに関連したテストの負荷を増大させることです。
Serializableを実装する場合には、以下に注意しましょう。
- クラスにはシリアルバージョンUIDを明示的に設定しましょう。明示的に設定しないと、自動的にIDがふられ、互換性のある変更であるにも関わらず互換性が無いと判断されてしまいます。
- 継承のために設計されたクラスは、原則としてSerializableを実装すべきではありません。インタフェースは、原則としてSerializableを拡張すべきではありません。それらのクラスやインタフェースを利用する人が、Serializableを実装しなければならないからです。
- シリアライズ可能かつ拡張可能で、インスタンスフィールドを持つクラスを実装する場合、以下を考慮しましょう。
- サブクラスがfinalizeメソッドをオーバーライドできないようにすべきです。ファイナライザ攻撃を防ぎましょう。
- インスタンスフィールドがデフォルト値に初期化されると問題がある場合、readObjectNoDataメソッドを追加しましょう。
- staticのメンバークラスを除き、内部クラスはSerializableを実装すべきではありません。エンクロージングインスタンスへの参照など、シリアライズ形式が定まらない要素があるからです。
項目87〜90
ごめんなさい、記事を書く時間が無くなってしまったため、時間ができたら書きたいと思います。ただ、本当にSeirializableを実装する必要に迫られてから学び始めても、全く遅くは無い内容です、ということだけはお伝えしておきます。
おわりに
間違いなどありましたら、ぜひ教えてください!