Q学習でOpen AI GymのPendulum V0を学習した

強化学習のQ学習を勉強したので、せっかくなので Open AI Gymの 「pendulum v0」の学習を実装した。

この記事では Q 学習の基礎は知っているという前提で、学習環境や実装で苦労した点を説明する。

gym のインストール

  • gym をインストール
$ pip install gym
  • pybox2d をインストール
$ brew install swig
$ git clone https://github.com/pybox2d/pybox2d
$ cd pybox2d
$ python setup.py build
$ python setup.py install

Pendulum v0

Open AI Gym にはいくつかの強化学習評価用の環境が用意されているが、 今回は以下の「Pendulum v0」を選んだ。

https://github.com/openai/gym/wiki/Pendulum-v0

タスクの目的

振り子へ適切にトルクを与えることで、垂直に振り上げた状態を維持すること。

環境

「Pendulum v0」では以下の3つの値を環境の状態として取得できる。

  • cos(theta): 振り子が角度 θ のときの cos の値
  • sin(theta): 振り子が角度 θ のときの sin の値
  • theta_dot: 振り子が角度 θ のときの 角速度

行動

エージェントは環境を観測し、振り子へ与えるトルクを決定し、振り子を回転させる。

報酬

エージェントは、行動の結果、以下の式で計算される報酬を受取る。

-(theta^2 + 0.1*theta_dt^2 + 0.001*action^2)

終了条件

実装者が適当に決めて良いということなので、 今回は1エピソードの中で1000 回の行動をし、その中で観測した sin の値の平均が 0.91 を上回ったら終了、とした。

Q学習による制御

実装したコードは以下。大体500エピソード前後で終了する。

https://gist.github.com/uu64/71529c63b374a9103486395811fc77bf

施行錯誤した点がいくつかあるので紹介する。

報酬の与え方

デフォルトの報酬に加えて、以下の条件で追加報酬を与えた。これによりできるだけ早い収束を目指した。

  • sin の値が 0.98 より大きい場合は、最大 100 点のボーナス
  • sin の値が -0.98 より小さい場合は、最大 100 点の罰則

行動の決定方法

行動の決定には epsilon-greedy 法を使ったが、以下の式で epsilon を決定した。これは 0 エピソード時点では 40% の確率でランダムに行動を選択するが、徐々にその確率を減少させ、251エピソード以降は Q が最大となるような行動を選択するようにしている。

epsilon = -0.0016*episode + 0.4

epsilon が小さすぎると、Q 値の学習が足りないのか、sin の値がなかなか向上しなかった。

状態の離散化の解像度

環境の状態を示す値はすべて連続値なので、適当な解像度で離散化する必要がある。

theta_dot が -8.0 から 8.0 の間で、最初はあまり荒くてもよくないかと思い、0.4刻み(40分割)で学習をしていたが、細かすぎるのか収束に 2000 エピソード近くかかった。

最終的には解像度を 0.8 刻み(20分割)まで落としたところ、500エピソード前後で収束するようになった。

感想

振り子が立ち始めるまでは簡単だったが、そこから収束のスピードや精度を向上させるための細かいパラメータ調整に時間がかかった。状態の属性数が増えるとなかなか厳しいのでは、という印象。

次は流行りの DQN でもう少し難しいタスクに挑戦したい。

mac にpyenv で python3 の環境を構築した

機械学習関係の勉強をしてみたくなったので、mac に python3 の環境を構築した。

pyenv のインストール

python2 が必要になることがあるかもしれないので、pyenv をインストールしてバージョン管理する

  • pyenv のインストール
$ git clone https://github.com/pyenv/pyenv.git ~/.pyenv
  • pyenv のパス設定
$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc
$ echo 'eval "$(pyenv init -)"' >> ~/.zshrc
  • 利用可能なバージョン確認
$ pyenv install --list

python3 のインストール

インストールした pyenv を使って python3 をインストールする

  • python3 のインストール
$ pyenv install 3.6.5
$ pyenv versions
$ pyenv global 3.6.5
$ pyenv rehash
  • python3 のバージョン確認
$ python --version

pip3 のインストール

python のパッケージマネージャ pip をインストールする。 が、python3 のインストール時に一緒にインストールされるようなので、バージョンだけ確認しておく。

  • pip のバージョン確認
$ pip3 --version

Effective Javaを勉強します【第10章】

項目66. 共有された可変データへのアクセスを同期する

synchronized 予約語

Java ではマルチスレッドを取り扱うことができる。しかし複数のスレッドが同じオブジェクトを 同時に操作すると、プログラムが意図しない動作をする可能性がある。この問題を解決するのが synchronized 予約語である。

synchronized 予約語を利用することで、メソッドやブロックがある時点で1つのスレッドのみで 実行されていることを保証することができる。

変数の読み書きの同期

Java の言語仕様は longdouble 型以外の変数については、アトミックであることを保証している。 すなわち、複数のスレッドにより同期なしでその変数が並行して変更されたとしても、どれかのスレッドにより その変数に保存された値を返すことが保証されている。

ここで注意しなければならないのは、あるスレッドが書き込んだ値が、他のスレッドからも見えることは 保証されていないことである。

例えば、以下のコードについて考えてみる。プログラムが約1秒動作し、その後メインスレッドが stopRequestedtrue に設定することで backgroundThread が終了するように思えるが、実際はスレッドが止まることはない。

public class StopThread {
    private static boolean stopRequested;

    public static void main(String[] args) 
            throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (!stopRequested) {
                    i++;
                }
            }
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

スレッドが止まらない理由は、JVMの最適化により run メソッド内部の while ループが 以下のように書き換えられるためである。

if (!stopRequested) {
    while (true) {
        i++;
    }
}

上記を防ぐためには、書き込みメソッドと読み込みメソッドの両方を同期する必要がある。 上記のコードは以下のように修正する。

public class StopThread {
    private static boolean stopRequested;
    private static synchronized void requestStop() {
        stopRequested = true;
    }
    private static synchronized boolean stopRequested() {
        return stopRequested;
    } 

    public static void main(String[] args) 
            throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (!stopRequested()) {
                    i++;
                }
            }
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}

まとめると、複数スレッドで可変データを共有する場合は、longdouble 以外であっても、 synchronized で読み書きするスレッドを同期しなければならない。

volitile 修飾子

「変数の読み書きの同期」に記載したコードでは synchronized が使われているものの、 その目的は相互排他のためではなく、スレッドが最新の値を見ることを指示するために使われている。 (stopRequested を2つのスレッドが更新するわけではない)

このような時は、synchronized ではなく volatile 修飾子を使うことも可能である。修正方法は以下。 volatile 修飾子は相互排他を行わないが、フィールドを読み込むスレッドが最後に書き込まれた値を見ることを保証している。

public class StopThread {
    private static volatile boolean stopRequested;

    public static void main(String[] args) 
            throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (!stopRequested) {
                    i++;
                }
            }
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

項目67. 過剰な同期は避ける

同期されたメソッドやブロック内で決して制御をクライアントに譲ってはいけない。

言い換えると、オーバーライドされるように設計されているメソッドや、関数オブジェクトの形式でクライアントから受け取ったメソッドを同期された領域内で呼び出してはいけない。そのようなメソッドはどのように動作するか分からないので制御することができず、場合によっては例外やデッドロック、データ破壊が発生する可能性があるため。

オープンコール

同期された領域から異質なメソッド(オープンコール)を呼び出してはいけない。 以下の ObservableSet は上記を守っていないために、多くの問題を抱えている。

public class ObservableSet<E> extends ForwardingSet<E> {
    interface SetObserver<E> {
        void added(ObservableSet set, E element);
    }

    public ObservableSet(Set<E> set) {
        super(set);
    }

    private final List<SetObserver<E>> observers = new ArrayList<>();

    public void addObserver(SetObserver<E> observer) {
        synchronized (observers) {
            observers.add(observer);
        }
    }

    public boolean removeObserver(SetObserver<E> observer) {
        synchronized (observers) {
            return observers.remove(observer);
        }
    }

    private void notifyElementAdded(E element) {
        synchronized (observers) {
            for (SetObserver<E> observer: observers) {
                observer.added(this, element);
            }
        }
    }

    @Override
    public boolean add(E element) {
        boolean added = super.add(element);
        if (added) {
            notifyElementAdded(element);
        }
        return added;
    }
}

上記のコードは要素がセットに追加された時にクライアントが通知を受け取ることを可能にしている。 オブザーバーは addObserver メソッドを呼び出すことで通知を受けるようになり、 removeObserver メソッドを呼び出すことで通知を受けることを解除する。

この時、下記のように値を順にセットに追加し。23になったら自分を取り除くことを意図するとどうなるだろうか。

public static void main(String[] args) {
    ObservableSet<Integer> set = new ObservableSet<Integer>(new HashSet<Integer>());

    set.addObserver(new SetObserver<Integer>() {
        public void added(ObservableSet<Integer> s, Integer e) {
            System.out.println(e);
            if (e == 23) {
                s.removeObserver();
            }
        }
    });
    for (int i = 0;i < 100;i++) {
        set.add(i);
    }
}

実際には数字0から23までを表示して、その後に ConcurrentModificationException がスローされる。 これは notifyElementAdded メソッドが synchronized ブロック内で SetObserver.added (オープンコール)を 呼び出すことで生じている。

SetObserver.added メソッドは removeObserver メソッドを呼び出し、 さらにその中で observers.remove メソッドを呼び出しているため、 observers のイテレート中にその要素を削除しようとすることになり例外が発生してしまう。

この場合は例外が発生しただけだが、デッドロックが発生する場合もある。このように、同期された メソッドやブロックの中でオープンコールを呼んでは行けない。

一般的に、同期された領域内ではできる限り少ない処理を行うべきであり、必要最小限の処理の間でのみロックを獲得し、 その後は速やかに解放すべきである。

スレッドセーフな設計

クラスが並行して使用されるのであれば、可変クラスをスレッドセーフにすべきである。 また内部的に同期を行うことで、高い並行生を達成することが可能となる。 内部的に同期するための技法としては、次のようなものがある。

クラスを内部的に同期させる場合は、そのことをドキュメント化すること。

項目68. スレッドよりエグゼキューターとタスクを選ぶ

java.util.concurrent パッケージにはエグゼキューターフレームワークという マルチスレッド処理を行うための仕組みが整っている。

まずは別スレッドにしたい処理を、Runnable インタフェースを実装したクラスの run メソッドに実装する。

public class TestRunnable implements Runnable {

    public void run() {
        // 実行したい処理
        ...
    }
}

あとは以下のようにスレッドを生成、実行すればよい。

ExecutorSerivice executor = Executors.newSingleThreadExecutor();
executor.execute(new TestRunnable());

処理が終わったら以下のコードでエグゼキューターを終了する。

executorService.shutdown();

スレッドプール

キューを実行するスレッドが2つ以上必要な場合は、スレッドプールを利用することが可能。

小さなプログラムや軽い負荷のサーバー処理には Executors.newCachedThreadPool を利用すると良い。 ただし Executors.newCachedThreadPool を利用すると、タスクはキューに入れられることなく直ちにスレッドに渡されるため、 利用可能なスレッドがなければ次々と新たなスレッドが生成されてしまう。

したがって高負荷のサーバーでは、固定数のスレッドを持つ Executors.newFixedThreadPool を利用したほうがよい。

タスク

エグゼキューターはスレッドを実行する機構であり、実行する機能はタスクと呼ばれる Runnable インタフェースや Callable インタフェースに記述する。従来の Thread は処理の単位と処理を実行する機構が混在していたが、エグゼキューターとタスクによりそれらが分離され、より柔軟にスレッドを扱うことができるようになった。

項目69. wait と notify よりコンカレンシーユーティリティを選ぶ

Java 5以降では、wait と notify を使用するよりも、用意されているコンカレンシーユーティリティ(java.util.concurrent)を利用したほうが良い。

java.util.concurrent 内の高レベルのユーティリティは、エグゼキューターフレームワーク、コンカレントコレクション、シンクロナイザーの3つに分類され、本項目では後者2項目を説明する。

コンカレントコレクション

List、Queue、Map などの標準コレクションインタフェースの高パフォーマンスな並行実装を提供している。これらには独自の同期管理の仕組みが実装されているため、並行な活動を排除することは不可能である。すなわち、コンカレントコレクションをロックしても効果はなく、プログラムを遅くするだけなので注意すること。

コンカレントコレクションをアトミックに操作するために、いくつかのメソッドが用意されている。例えば、ConcurrentMapputIfAbsent メソッドなど。

private static final ConcurrentMap<String, String> map = new ConcurrentHashMap<String, String>();

public static String intern(String s) {
    // 毎回 putIfAbsent を呼び出すより、get で判定したほうがパフォーマンスが良い
    String result = map.get(s);
    if (result == null) {
        result = map.putIfAbsent(s, s);
        if (result == null) {
            result = s;
        }
    }
    return result;
} 

コレクションインタフェースによっては、ブロックする(操作が完了するまで待つ)操作が用意されている。 例えば BlockingQueue は、キューが空なら待つ take メソッドが用意されている。

シンクロナイザー

シンクロナイザーは、スレッドが他のスレッドを待つことを可能にするオブジェクトである。 CountDownLatchSemaphoreCyclicBarrierExchanger の4種類がある。

以下は CountDownLatch を用いたアクションの並列実行処理時間を計測するプログラムの例である。 waitnotify を利用して同様の実装をするより簡単である。

// executor: アクションを実行するエグゼキューター
// concurrency: 同時実行スレッド数
// action: 計測対象のアクション
public static long time(Executor executor, int concurrency, final Runnable action)
        throws InterruptedException {
    final CountDownLatch ready = new CountDownLatch(concurrency);
    final CountDownLatch start = new CountDownLatch(1);
    final CountDownLatch done = new CountDownLatch(concurrency);
    for (int i = 0; i < concurrency; i++) {
        executor.execute(new Runnable() {
            @Override public void run() {
                // スレッドが実行可能になったら通知する
                ready.countDown();
                try {
                    // start.countDown()が1回実行されるまで待つ
                    start.await();
                    action.run();
                } catch(InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    // スレッドの処理が終了したら通知する
                    done.countDown();
                }
            }
        });
    }
    // すべてのスレッドが実行可能になるまで待つ
    // すなわち concurrency 回 ready.countDown() が呼ばれるまで先には進まない
    ready.await();
    long start = System.nanoTime();
    start.countDown();
    // すべてのスレッドの処理が終了するまで待つ
    // すなわち concurrency 回 done.countDown() が呼ばれるまで先には進まない
    done.await();
    return System.nanoTime() - start;
}

waitnotify の使い方については、今後は直接使用する機会は少ないと思うので割愛。

項目70. スレッド安全性を文書化する

クラスのインスタンスstatic のメソッドが並行して使用された場合に、そのクラスはどのように振る舞うか文書化しなければならない。

文書化するときの注意点としては、以下のようなものが挙げられる。

  • synchronized 修飾子は実装の詳細であるため、Javadoc に含むべきではない。
  • スレッド安全性に関しては、以下のサポートするスレッド安全性のレベルを明確に文書化しなければならない。
    • 不変(immutable): このクラスのインスタンスは定数のように振る舞い、外部の同期は必要ない
    • 無条件スレッドセーフ(unconditionally thread-safe): このクラスのインスタンスは可変だが、外部同期は不要であり、並列実行可能な内部同期の仕組みが実装されている
    • 条件付きスレッドセーフ(conditionally thread-safe): 安全に並列実行するために、いくつかのメソッドは外部動機が必要
    • スレッドセーフではない(not thread-safe): このクラスのインスタンスは可変であり、ここのメソッド呼び出しはクライアントにより外部同期する必要がある
    • スレッド敵対(thread-hostile): このクラスは、たとえすべてのメソッドが外部同期されたとしても、並列実行すべきではない
  • 条件付きスレッドセーフのクラスを文書化する際は、外部同期が必要なメソッドと、そのために獲得すべきロックを明記する必要がある。
  • 誰でもアクセス可能なロックをクラスが使用すると、ロックを長期間保持することによるサービス拒否攻撃を受ける可能性がある。これに対して、無条件スレッドセーフのクラスの場合、以下のようなプライベートロックオブジェクトを利用することができる。継承のために設計したクラスにおいては、サブクラスとスーパークラスの間の干渉を防ぐことができるため、特に有用。
    private final Object lock = new Object();
    public void foo() {
        synchronized(lock) {
            ...
        }
    }

項目71. 遅延初期化を注意して使用する

遅延初期化は、フィールドの値が必要となるまで、そのフィールドの初期化を遅らせること。 ほとんどの状況では遅延初期化よりは普通の初期化が望ましい。

特に複数スレッドで遅延初期化を使用する場合、フィールドの初期化時にロックが必要となるためアクセスコストが増加し、 また適切に同期が行われない場合深刻なバグとなる可能性があるため、注意しなければならない。

同期されたアクセッサー

複数スレッドから遅延初期化を使用する場合、一般的には以下のような同期されたアクセッサーを使用する。

    private FieldType field;
    synchronized FieldType getField() {
        if (field == null) {
            field = computeFieldValue();
        }
        return field;
    }

遅延初期化ホルダークラスイデオム

static フィールドに対するパフォーマンスのために遅延初期化を使用する場合は、遅延初期化ホルダークラスイデオムを使用する。 これは「クラスが利用されるまでクラスが初期化されないことが保証される」という言語仕様を利用している。

    private static class FieldHolder {
        static final FieldType field = computeFieldValue();
    }
    static public FieldType getField() {
        return FieldHolder.field;
    }

上記の場合、初めて getField メソッドが呼び出されるときに、初めて FieldHolder クラスが初期化される。

二重チェックイデオム

インスタンスフィールドに対するパフォーマンスのために遅延初期化を使用する場合は、二重チェックイデオムを使用する。 このイデオムは、フィールドの初期化が行われた後にアクセスされた場合のロックのコストを回避する。

    private volatile FieldType field;
    public FieldType getField() {
        FieldType result = field;
        if (result == null) { //1回目検査
            synchronized(this) {
                result = field;
                if (result == null) { //2回目検査
                    field = result = computeFieldValue();
                }
            }
        }
        return result;
    }

上記では1回目の検査で、初期化されているかをチェックしている。 すでに初期化されている場合そのまま result を返すため、ロックによるコストを回避することができる。

初期化されていない場合、synchronized でロックをして2回目の検査を実施した上で、初期化を実施する。 (このため二重チェックイデオムと呼ばれる)

変数 fieldsynchronized 修飾子の内外からアクセスされるため、volatile 宣言する必要があることに注意すること。

項目72. スレッドスケジューラに依存しない

複数スレッドが実行可能な場合、スレッドスケジューラにより各スレッドの実行時間が決定されるが、この決定方法の詳細はオペレーティングシステムにより異なる可能性がある。 複数スレッドを扱うプログラムを書く際は、パフォーマンスや振る舞いが、あるオペレーティングシステムのスレッドスケジューラに依存しないよう注意する必要がある。

スレッドスケジューラに依存しないようにするためには、実行可能なスレッドの平均数がプロセッサの数を大きく超えないことを保証すればよい。 そのためには、有益な処理をしていないスレッドは動作しないようにする必要がある。

エグゼキュータフレームワークであれば、スレッドプールを適切な大きさにして、タスクを適度に小さくし、各スレッドが独立するよう設計すること。

項目73. スレッドグループを避ける

スレッドグループは、もともとセキュリティ向上のためにアプレットを隔離する仕組みとして考案されたが、今日では利用する機会はない。 もしスレッドの論理的なグループを扱うクラスを設計するのであれば、スレッドプールエグゼキューターの利用などを検討すること。

Effective Javaを勉強します【第9章】

項目57. 例外的状態にだけ例外を使用する

例外は、例外的条件に対してのみ使用すべきであり、通常の制御フローに対しては、 決して使用すべきではない。

例えば、パフォーマンス改善を目的として以下のような例外を利用したループを書く人がいる。しかしパフォーマンスが改善されることはない上、可読性が低下し、本来キャッチしなければならない悪い例外を見逃す原因となるため、この書き方は決してしてはならない。

try {
    int i = 0;
    while(true) {
        range[i++].climb();
    }
} catch(ArrayIndexOutOfBoundsException e) {
}

項目58. 回復可能な状態にはチェックされる例外を、プログラミングエラーには実行時例外を使用する

Java には、次の通り3種類の例外がある。

  • チェックされる例外
  • 実行時例外
  • エラー

それぞれの使い方について、一般的なルールを紹介する。

チェックされる例外

呼び出し側が適切に回復できるような状況に対しては、チェックされる例外を使用する。 逆に言えば、チェックされる例外がAPIからスローされるということは、APIの設計者はその状況から回復することを要求している。 したがって、チェックされる例外を無視する(catch 節で何も処理しない)ことは、基本的には良いことではない。

実行時例外とエラー

どちらも一般に回復が不可能で、実行を継続すべきでないような場合に用いる。 このような時は、適切なエラーメッセージを表示し、スレッドを停止させる。

実行時例外

実行時例外は、ArrayIndexOutOfException など、事前条件違反などによるプログラミングエラーに用いる。

エラー

エラーはJVMが実行を継続できない資源不足、不変式エラーなど、JVMが使用するのに予約されているという慣例がある。 逆に言えば、実装するすべてのチェックされない例外は RuntimeException をサブクラス化すべき。(エラーは用いない)

項目59. チェックされる例外を不必要に使用するのを避ける

チェックされる例外は、プログラマに例外状態の処理を強制し信頼性を大きく向上させるが、その分プログラマの負荷にもなる。

例外処理の負荷が正当化されるのは、APIの適切な使用で例外状態を防ぐことができず、かつ、 そのAPIを使用しているプログラマが例外に対して有用な処理をすることができる場合である。

上記以外の場合では、チェックされない例外を用いる方が適切であると言える。

以下は、チェックされる例外の不適切な使用例である。(プログラマはこれ以上の有用な処理ができない)

try {
    obj.action(args);
} catch (TheCheckedException e) {
    e.printStackTrace();
    System.exit(1);
}

チェックされる例外をチェックされない例外に変更するための方法として、 例外をスローするメソッドを、例外がスローされるかを判断する boolean を返すメソッドと 正常系の処理をするメソッドの2つに分割する方法がある。

上記のチェックされる例外の不適切な使用例は、以下のように修正することになる。

if(obj.actionPermitted(args)) {
    obj.action(args);
} else {
    // 例外状態を処理する
    ...
}

ただし上記は、外部要因により actionPermittedaction の間にオブジェクトの状態が 変化しない場合に限られるため注意すること。

項目60. 標準例外を使用する

Java は汎用的ないくつかのチェックされない例外(標準例外)を提供している。

標準例外を利用することで、APIの学習コストが下がる、 見慣れない例外を利用する場合と比較してコードの可読性が向上する、 例外クラスが少なくなることでメモリの削減とクラスのロード時間の短縮が期待できる、 などのメリットがある。

次に示すのは、利用頻度の高い標準例外である。適切な場合に利用すると良い。

例外 使用する機会
IllegalArgumentException パラメータの値が不適切
IllegalStateException メソッドの呼び出しに対してオブジェクトの状態が不正
NullPointerException パラメータの値が禁止されている null
IndexOutOfBoundsException インデックスパラメータの値が範囲外
ConcurrentModificationException 禁止されているオブジェクトの並行した変更を検出
UnsupportedOperationException オブジェクトがメソッドをサポートしていない

項目61. 抽象概念に適した例外をスローする

下位レイヤからスローされた例外をそのまま上位レイヤに伝播すると、上位レイヤの概念と合わないことがあり、利用者を混乱させる可能性がある。

そのような場合、上位レイヤは下位レベルの例外をキャッチして、上位レイヤの中で上位レベルの概念から説明可能な例外をスローすべきである。これを例外翻訳と呼ぶ。

try{
    // 下位レイヤの処理呼び出し
    ...
} catch (LowerLevelException e) {
    throw new HigherLevelException(...);
}

また、上位レベルの例外のデバッグに、下位レベルの例外の情報が役立つ場合には、例外連鎖と呼ばれる例外翻訳の特別な形式を採用し、下位レベルの例外を上位レベルの例外に渡してやるとよい。受け取った下位レベルの例外は Throwable.getCause メソッドで取り出すことができる。

try{
    // 下位レイヤの処理呼び出し
    ...
} catch (LowerLevelException cause) {
    throw new HigherLevelException(cause);
}

項目62. 各メソッドがスローするすべての例外を文書化する

項目名の通り、各メソッドがスローするすべての例外を文書化すべきである。

  • チェックされる例外をここに宣言し、各例外がスローされる条件を Javadoc@throws タグを用いて文書化すること。
  • チェックされない例外も同様に @throws タグを用いて文書化する。チェックされない例外はメソッド実行に関する事前条件を示しているため、非常に重要である。
  • チェックされない例外には throws 予約語を用いない方がよい。チェックされる例外と区別したいため。
  • クラス内の多くのメソッドで、ある特定の例外がスローされる場合は、クラスの Javadoc にその例外について記述しても良い。

項目63. 詳細メッセージにエラー記録情報を含める

プログラムがキャッチされなかった例外により失敗すると、システムは自動的にその例外のスタックトレースを表示する。スタックトレースには、その例外の toString メソッドの結果である例外の詳細メッセージが含まれる。

スタックトレースは例外発生時の調査に必須であり、唯一の情報源となることも多い。したがって、例外の toString メソッドで必要な情報を出力するようにすることが大切である。

  • 例外の toString メソッドでは、その例外の原因となったすべてのパラメータとフィールドの値を出力すべき
  • 一方でスタックトレースには一般的にファイル名や行番号が含まれ、ソースコードを参照することで多くの情報を得ることができるため、冗長になりうる情報を含める必要はない
  • 例外の文字列表現に、適切なエラー記録情報を含めることを保証する方法の1つは、例外のコンストラクタでそれらを要求すること。以下は IndexOutOfBoundsException で実装した場合の例。
/**
 * IndexOutOfBoundsException を生成する
 *
 * @param lowerBound 最も小さな正当インデックス値
 * @param upperBound 最も大きな正当インデックス値に 1 を足した値
 * @param index 実際のインデックス値
 */
public IndexOutOfBoundsExcetion(int lowerBound, int upperBound, int index) {
    super("Lower bound: " + lowerBound + 
        ", Upper Bound: " + upperBound + 
        ", Index: " + index);

    this.lowerBound = lowerBound;
    this.upperBound = upperBound;
    this.index = index;
}

項目64. エラーアトミック性に努める

一般に、メソッド仕様の一部である例外は、オブジェクトをメソッド呼び出しの前と同じ状態にすべき。 呼び出し元が例外から回復することを期待されているチェックされる例外に関しては、特に重要。 (呼び出し元が回復した後、再度呼び出すことができるようにするため)

エラーアトミック性は一般に望ましいが、達成するためのコストや複雑性を考慮して検討する必要がある。 エラーアトミック性を達成するには次のような方法がある。

  • 不変オブジェクトを設計する

    もっとも簡単にエラーアトミック性を達成する方法は、オブジェクトを不変にすることである。 生成された時に整合性が取れていなければならず、その後変更されることはないため。

  • パラメータの検査をする

    メソッドの最初にパラメータの正当性を検査し、オブジェクトの変更が行われる前に例外をスローする。 例えば、次のコードでは最初の if 文で、パラメータの正当性を検査している。

    java public Object pop() { if (size == 0) { throw new EmptyStackException(); } Object result == elements[--size]; elements[size] = null; return result; }

  • 計算の順番を工夫する

    オブジェクトの変更が必要な計算より前に、失敗するかもしれない計算を行うよう、メソッド内の処理順を設計する。

  • 回復コードを書く

    操作の途中で発生するエラーを捉えて、操作が始まる前の時点までオブジェクトの状態を戻す回復コードを書く。

  • 一時的コピーを利用する

    オブジェクトの一時的コピーに対して操作を行い、操作完了時にオブジェクトの内容を一時的コピーの内容で置き換える。

項目65. 例外を無視しない

例外を無視せず、なぜその例外がスローされるのか設計者の意図についてよく考えること。どうしても空の catch ブロックを使うのであれば、なぜそうして例外を無視するのが適切なのかコメントを含めるべき。

Effective Javaを勉強します【第8章】

項目45. ローカル変数のスコープを最小限にする

コードの可読性と保守性を向上させ、誤りの可能性を減らすためにローカル変数のスコープは最小限にすべきである。

スコープを最小限にするには?

ローカル変数のスコープを最小限にする最も強力な技法は、ローカル変数が初めて使用される箇所で宣言すること。

ローカル変数を早めに宣言することで、以下のような問題が生じる。

  • 処理を理解しようとしているプログラマの読み手の注意を逸らしてしまう
  • いざ変数の使用箇所まで到達しても、その変数の型や初期値が思い出せない
  • 本来意図した使用場所以外で呼び出された場合に、誤った動作をする可能性がある

スコープを最小限にするもう一つの方法は、メソッドを小さくして焦点をはっきりさせること。 1つの処理に対して1つのメソッドとすることで、関係のない処理にスコープを広げない。

ローカル変数宣言と初期化子

全てのローカル変数宣言は初期化子を含むべきである。 変数を合理的に初期化するために十分な情報がないのであれば、初期化が可能になるまで宣言を先送りすべき。

try-catch文はこの規則の例外である。 例外をスローするメソッドの戻り値で変数が初期化されるのであれば、その変数はtryブロック内で初期化される必要がある。 しかしその変数がtryブロック外でも使用される場合、tryブロックの前で宣言しなければ使用することができない。 その場合は変数を合理的に初期化することはできない。

ループ変数とスコープ

forループではfor形式とfor-each形式の両方で、ループ変数の宣言が可能であり、このスコープはループ内に限定される。 したがってループ変数の内容がループ変数後に不要な場合は、whileループではなくforループを選択すべき。

例えば以下のwhile文には、コピー&ペーストミスによるバグが含まれているが、コンパイルエラーや例外は発生しない。

Iterator<Element> i = c2.iterator();
while (i.hasNext()) {
    doSomething(i.next());
}

Iterator<Element> i2 = c2.iterator();
while (i.hasNext()) { //バグ
    doSomething(i2.next());
}

一方で上記の処理をfor文で書き直した場合、コンパイルエラーとなるため、バグの発見が容易である。

for (Iterator<Element> i = c2.iterator(); i.hasNext());  {
    doSomething(i.next());
}

for (Iterator<Element> i2 = c2.iterator(); i.hasNext(); ) { //コンパイルエラー
    doSomething(i2.next());
}

項目46. 従来のforループよりfor-eachループを選ぶ

従来のforループに含まれるイテレータ変数とインデックス変数はコードを散らかしているだけであり、時にはエラーの原因となる。

for-eachループはコードを簡潔にし、余分なエラーの原因を排除してくれる。 従来のforループと比較した時に、パフォーマンス上のペナルティも存在しない。

// リリース1.6以降におけるコレクションと配列をイテレートするための好ましいイディオム
for (Elemetn e : elements) {
    doSomething(e);
}

for-eachループはコレクションと配列以外にも、Iterableインタフェースを実装したあらゆるオブジェクトをイテレートすることが可能。

Iterableは単一なメソッドから構成されるため、実装は難しくない。Collectionは実装しない場合であってもIterableを実装すれば、 ループ処理でfor-each文が利用可能となり、ユーザーから感謝されるだろう。

public interface Iterable<E> {
    iterator<E> iterator();
}

for-eachが利用できない場合

一般的にfor-each文は従来のfor文より優れているが、以下のケースではfor-each文は利用できないので注意すること。

  • フィルタリング:特定の要素のみを抽出し削除する必要がある場合
  • 変換:特定の要素に新しい値を設定する場合
  • 並列イテレーション:複数のコレクションを並列にイテレートする必要がある場合

項目47. ライブラリーを知り、ライブラリーを使う

車輪の再発明のために、無駄な努力をしないこと。

実装しようとしている処理がすでにクラス内やライブラリー内に存在するのであれば、それを利用すること。 存在の有無が分からなければ調べること。

特に、標準ライブラリーの利用には以下のメリットがある。

  • 実装した専門家の知識と、これまでの他の利用者の経験を活用することが可能
  • 本質的でない課題に時間を無駄にする必要がない(ランダム処理をさせたいがために擬似乱数発生法や整数論について勉強する必要があるのか?)
  • 標準ライブラリーを開発しているコミュニティにより、パフォーマンスやバグが勝手に修正されてゆく
  • アプリケーションのコアがコードの大部分を占めることで、可読性・保守性が向上する

標準ライブラリーを効率的に活用するためにも、主要リリースごとの新機能や改善点は把握しておくことが大切。

項目48. 正確な答えが必要ならばfloatdoubleを避ける

float型とdouble型は、主に科学計算と工学計算のために設計されている。 それらは2進浮動小数点算術を行うが、これは広い範囲の大きさに対して正確な近似を素早く行うために設計されており、正確な結果は提供しない。

したがってfloat型とdouble型は、特に金銭計算には適さない。

例) 1ドルで、10セント、20セント、30セント、・・・、1ドルと値段がついたキャンディを、安い順から1つづつ買う場合、いくつ買えるかを計算させる。 正しくは、10+20+30+40=100セント=1ドル購入可能。

double funds = 1.00;
int itemsBought = 0;
for (double price = .10; funds >= price; price += .10) {
    funds -= price;
    itemsBought++;
}

System.out.println(itemsBought + " items bought.");
// => 3 items bought.

System.out.println("Change: $" + funds);
// => Change: $0.3999999999999999

上記のように正しい結果にならない。これは以下のようにBigDecimalを利用すると解決可能。

final BigDecimal TEN_CENTS = new BigDecimal(".10");
int itemsBought = 0;
BigDecimal funds = new BigDecimal("1.00");
for (BigDecimal price = TEN_CENTS;
         funds.compareTo(price) >= 0;
         price = price.add(TEN_CENTS)) {
    itemsBought++;
    funds = funds.subtract(price);
}
System.out.println(itemsBought + " items bought.");
// => 4 items bought.

System.out.println("Change: $" + funds);
// => Change: $0.0

しかしBigDecimalには、計算が遅い、基本データの算術型より不便という欠点がある。

BigDecimalの欠点(主にパフォーマンス)が許容できない場合、intlongを代わりに用いることも可能。 しかしこの場合、プログラマが小数点の位置を把握しなければならない。またintは9桁、long18桁の精度でしか計算できない。

以下はintを用いて、セントで計算を行った例。

int itemsBought = 0;
int funds = 100;
for (int price = 10; funds >= price; price += 10) {
    itemsBought++;
    funds -= price;
}
System.out.println(itemsBought + " items bought.");
// => 4 items bought.

System.out.println("Change: $" + funds);
// => Change: $0

項目49. ボクシングされた基本データより基本データ型を選ぶ

Javaではすべての基本データ型は、ボクシングされた基本データ(参照型)を持っている。 例えば、intならIntegerdoubleならDoublebooleanならBooleanなど。

基本データ型を自動でボクシングされた基本データへ変換する自動ボクシング、参照型を自動で基本データ型へ変換する自動アンボクシングがあるが、 基本データ型とボクシングされた基本データの間には以下のような違いが存在する。

  • 基本データ型は値だが、ボクシングされた基本データはオブジェクトである
  • 基本データ型は機能する値しか持たないが、ボクシングされた基本データはnullという機能しない値を持つことが可能
  • 基本データ型は、ボクシングされた基本データと比較して、時間的・空間的に効率的

ボクシングされた基本データの比較

以下はボクシングされた基本データに対するコンパレータの例。

Comparator<Integer> comparator = new Comparator<Integer>() {
    @Override
    public int compare(Integer first, Integer second) {
        return first < second ? -1 : (first == second ? 0 : 1);
    }
};

一見正しそうに見えるが、compareメソッドにおけるfirstsecondの比較に欠陥がある。

first < secondでは引数は自動アンボクシングされる。 しかしこの検査がfalseだった場合、first == secondではボクシングされた基本データの同一性比較が行われる。 この時、2つの値が同じ値を示している異なるインスタンスであった場合に、falsesecondfirstより小さい)と判定されてしまう。

ボクシングされた基本データに対して==演算子を利用するのは大抵まちがいである。 上記の問題は、以下のように比較の前に基本データ型として引数を取り出すことで解決が可能。

Comparator<Integer> comparator = new Comparator<Integer>() {
    @Override
    public int compare(Integer first, Integer second) {
        int f = first;  // 自動アンボクシング
        int s = second; // 自動アンボクシング
        return f < s ? -1 : (f == s ? 0 : 1);
    }
};

null に対する自動ボクシング

ボクシングされた基本データ型を自動アンボクシングしようとすると、NullPointerExceptionが発生する。

Integer i = null;
if (i == 42) { // 自動アンボクシングにより、 NullPointerException が発生
    System.out.println("true");
}

自動ボクシング/アンボクシングによるパフォーマンス低下

以下のコードは、ローカル変数sumをボクシングされた基本データ型Longで宣言したことにより、 実行時にボクシングとアンボクシングが繰り返され、大幅にパフォーマンスが低下する。

public static void main(String[] args) {
    Long sum = 0L;
    for (long i = 0; i < Integer.MAX_VALUE; i++) {
        sum += i;
    }
}

ボクシングされた基本データの使い所

以下のケースではボクシングされた基本データを使用すべき。

  • コレクション内の要素、キー、値として。コレクションには基本データ型を入れることはできない。
  • パラメータ化された型の空パラメータとして。同様に基本データ型は利用不可。
  • リフレクションによりメソッドを呼び出す場合。(項目53にて)

項目50. 他の方が適切な場所では、文字列を避ける

文字列はテキストを表現する以外の目的で利用すべきではない。 以下は、文字列を使用すべきでないケース。

  • 他の値型に対する代替としての利用。
    データをファイルやキーボード入力から取得した場合、大抵は文字列の形式で受け取るが、内容が数値ならばintfloat
    「はい」や「いいえ」ならばbooleanといった具合に、適切な値型へ変換すべき。
  • 列挙型(enum)の代替としての利用。
  • 集合型の代替としての利用。不要な文字列解析の手間が生じるので、その集合を表すクラスを用意する。
  • ケイパビリティ(偽造できないキー)としての利用。

項目51. 文字列結合のパフォーマンスに用心する

n個の文字列を結合するのに、文字列結合演算子(+)を繰り返し利用すると、nに関して2次の時間を必要とするので注意すること。

代わりにStringBuilderappendメソッドを使用する。

public String statement(){
    String result = "";
    for (int i = 0; i < numItems(); i++){
        result += lineForItem(i);
    }
    return result;
}
// numItemsが100、lineForItemが80文字長の文字列を返す場合、上記よりパフォーマンスが85倍改善された
public String statement(){
    StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH);
    for (int i = 0; i < numItems(); i++){
        b.append(lineForItem(i));
    }
    return b.toString();
}

項目52. インタフェースでオブジェクトを参照する

適切なインタフェース型が存在するならば、パラメータ、戻り値、変数、およびフィールドはすべてインタフェース型で宣言されるべき。

// 良い - 型としてインタフェースを利用
List<Subscriber> subscribers = new Vector<Subscriber>();

// 悪い - 型としてクラスを利用
Vector<Subscriber> subscribers = new Vector<Subscriber>();

上記のように書くだけで、プログラムがかなり柔軟になる。 例えば実装を切り替えたい場合、コンストラクタで指定しているクラスを変更する、あるいは別のstaticファクトリーメソッドを使用するだけ。

ただし、元の実装がインタフェースの一般契約で要求されていない何らかの特別な機能を提供していて、コードがその機能に依存している場合、 新たな実装が同じ機能を提供するようにしなければならない。(下記の場合、Vectorクラス固有の同期機能に依存していないかどうか)

// VectorからArrayListへ切り替える
List<Subscriber> subscribers = new ArrayList<Subscriber>();

項目53. リフレクションよりインタフェースを選ぶ

コア・リフレクション機構(java.lang.reflect)を用いれば、Class インスタンスからコンストラクタを表す Constructor インスタンス、メソッドを表す Method インスタンス、フィールドを表す Field インスタンスを取得することができる。またこれらを用いて、実際のインスタンスを生成したり、メソッドを呼び出したり、フィールドにアクセスすることが可能である。

このようにコア・リフレクション機構は非常に強力であるが、次に示すようなデメリットが存在する。したがって、基本的にはアプリケーションの設計時(クラス内部の構造の調査など)のみに利用すべきであり、クラスへのアクセスにはインタフェースやスーパークラスを利用すべきである。

  • 例外の検査を含め、コンパイル時の型検査の恩恵を全て失う
  • リフレクションのコードは冗長であり、可読性が低い
  • 通常のメソッド呼び出しと比較して、パフォーマンスが悪い

項目54. ネイティブメソッドを注意して使用する

ネイティブメソッドとは、C や C++ などのネイティブプログラミング言語で書かれたメソッドのことであり、 Java Native Interface により Java からネイティブメソッドを呼び出すことが可能である。

その用途としては、レジストリやファイルロックなどのプラットフォーム固有の機構へのアクセスや、古いコードのライブラリへのアクセスが挙げられる。また、アプリケーションの中でもパフォーマンスが重要な箇所で、パフォーマンス改善のために用いられてきた。

上記は過去の話であり、現在では次のような理由から、パフォーマンス改善のためにネイティブメソッドを使用する必要はない。

  • 過去と比べて、JVMは高速になっており、ネイティブメソッドに引けを取らないパフォーマンスを得ることが可能である
  • ネイティブメソッドにはメモリ破壊エラーの危険性がある
  • ネイティブ言語はプラットフォーム依存であり、移植性が非常に低い

ネイティブメソッドは、低レベルのリソースへのアクセスや古いライブラリーへのアクセスなど、 どうしても必要な場合でのみ利用されるべきである。

項目55. 注意して最適化する

速いプログラムよりも良いプログラムを書くように努めること。 良いプログラムを書けばスピードは結果として得ることができる。具体的には次のような点について、よく考えること。

  • パフォーマンスを制限するような設計を避ける

  • APIの場合、以下のような点を考慮すること

    • public の型を可変にすると不要な防御的コピーが必要となる可能性がある
    • コンポジションが適切な public のクラスで継承を選択した場合、スーパークラスによりパフォーマンスが制限される可能性がある
    • インタフェースでなく実装型を使用した場合、特定の遅い実装に拘束される恐れがある
  • 上記に気をつけて実装を全て終え、それでもパフォーマンスが不十分であるときに、最適化を検討すべきである

    • 最適化の前後では、プロファイラを用いるなどしてパフォーマンスを測定する
    • 必要であればアルゴリズムの変更を検討する(低レベルな最適化をしても目標とするパフォーマンスに達しないことがしばしばあるため)

項目55. 一般的に受け入れられている命名規約を守る

以下の命名規約を守ること。(他人が混乱しないような命名をする)

活字的命名規約

  • パッケージ名は

    • ピリオドで区切られた階層的構造をしている
    • 区切られた要素は英小文字と数字から構成される
    • 要素は一般的に8文字以下
    • 組織外で使用されるパッケージ名は、その組織のインターネットドメイン名で始まるべき
  • クラス名、インタフェース名は(enumアノテーション含む)

    • 1つ以上の単語で構成され、最初の文字は大文字である
    • 一般的でない省略形は避ける
  • メソッド名とフィールド名は

    • 1つ以上の単語で構成され、最初の文字は小文字である
    • 一般的でない省略形は避ける
    • 定数フィールドは全て大文字の1つ以上の単語から構成され、区切り文字は「_」である
    • ローカル変数であれば省略形は許可される
    • 型パラメータは通常1文字である

文法的命名規約

活字的命名規約に比べて、曖昧かつ柔軟である。

  • クラスは、一般に単数名詞あるいは名詞句で命名される
  • インタフェースは able や ible で終わる形容詞で命名される
  • アノテーションは特に品詞に関する制限はない
  • boolean でない機能や属性を返すメソッドは、名詞、名詞句、get で始まる動詞句で命名されることが多い
  • 属性を設定するメソッドは setAttribute と名付けるべき(⇔ getAttribute)
  • オブジェクトの型を変換し、別の型の無関係なオブジェクトを返すメソッドは、大抵 toType と呼ばれる
  • レシーバーオブジェクトの型と異なる型を持つビューを返すメソッドは、大抵 asType と呼ばれる
  • 呼び出されたオブジェクトと同じ値を持つ基本データを返すメソッドは、大抵 typeValue と呼ばれる
  • static ファクトリーに対応する名前は、valueOf、of、getInstance、newInstance、getType、newType など
  • フィールド名に関してはあまり確立された規約はないが、boolean 型の場合、アクセッサ-メソッド名から is を除いた名前であることが多い

Effective Javaを勉強します【第7章】

項目38. パラメータの正当性を検査する

コンストラクタやメソッドの引数(パラメータ)に関して何らかの制約(参照がnullではいけないなど)がある場合は、 文書化し、メソッドの初めに検査をすることでその制約を強制すべきである。

上記を行わない場合、意図せぬ例外を引き起こしたり、オブジェクトが不正な状態のまま離れた場所でエラーを引き起こし、デバッグが困難になる恐れがある。

publicのメソッドの場合

スローされる例外を、Javadoc@throwsタグを使用して文書化する。

/**
 * 値が(this mod m)であるBigIntegerを返します。このメソッドは、
 * remainderメソッドとは異なり、常に負でないBigIntegerを返します。
 *
 * @param m 正でなければならないモジュラス
 * @return this mod
 * @throws ArithmeticException m <= 0の場合
 */
 public BigInteger mod(BigInteger m) {
     ...
 }

publicでないメソッドの場合

アサーションを用いてパラメータをチェックする。

private static void sort(long a[], int offset, int length) {
    assert a!= null;
    assert offset >= 0 && offset <= a.length;
    assert length >= 0 && length <= a.length - offset;
    ...
}

項目39. 必要な場合には、防御的にコピーする

クライアントからオブジェクトを受け取るメソッドやコンストラクタを書くときはいつでも、 クライアントが提供するオブジェクトが可変である可能性を考慮すべきである。

オブジェクトを受け取った後に変更があった場合に、クラスに問題が生じる可能性がある場合は、 受け取ったオブジェクトをそのまま利用するのではなく、防御的にコピーして利用しなければならない。

public class Period {
    private final Date start;
    private final Date end;

    /**
     * @param start 期間の開始。
     * @param end 期間の終わり。開始より前であってはならない。
     * @throws IllegalArgumentException start が end の後の場合。
     * @throws NullPointerException start か end が null の場合
     */
    public Period(Date start, Date end) {
        if (start.compareTo(end) > 0) {
            throw new IllegalArgumentException(start + " after " + end);
        }
        this.start = start;
        this.end = end;
    }

    public Date getStart() {
        return start;
    }

    public Date getEnd() {
        return end;
    }
}

上記には2つの問題がある。

1つ目はコンストラクタで、startendをそのまま受け取っている点である。 Dateクラスは可変であるため、以下のように操作することでクラスの制約を破ることが可能。

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); //endの時間を操作可能

上記はコンストラクタを以下のように修正することで解決する。 コピーに対して正当性検査を行うことで、検査までの間にパラメータを不正に変更されることから保護している。

    public Period(Date start, Date end) {
        // 防御的コピー
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());

        if (start.compareTo(end) > 0) {
            throw new IllegalArgumentException(start + " after " + end);
        }
    }

2つ目の問題は、アクセッサーで内部のstartendをそのまま返却している点である。 受け取ったstartendは可変なので、以下のように操作することでクラスの制約を破ることが可能。

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); //endの時間を操作可能

この点については、以下のようにアクセッサーで防御的コピーを返すように修正することで解決する。

    public Date getStart() {
        return new Date(start.getTime());
    }
    public Date getEnd() {
        return new Date(end.getTime());
    }

一番良いのは、可能な限り不変オブジェクトを使い、不必要に可変オブジェクトを使わないこと。

項目40. メソッドのシグニチャを注意深く設計する

API設計時は以下の点を考慮すること。

  • メソッド名を注意深く選ぶ

    • 理解可能で、パッケージ内の他のメソッド名やjavaの標準ライブラリと矛盾が生じないようなメソッド名を選ぶ
  • 便利なメソッドを提供しすぎない

    • 多くのメソッドを定義したインタフェース/クラスは学習、使用、文書化、テスト、保守などを困難にする
    • 多くの役割を与えすぎないこと
  • 長いパラメータのリストは避ける

    • 4個以下が望ましい(それ以上は大抵覚えられない)
    • パラメータのリストを短くする方法は例えば以下
      • メソッドを分割する
      • パラメータの集まりを保持するヘルパークラスを作成する(staticなメンバークラスなど)
      • ビルダーパターンをオブジェクト生成からメソッド呼び出しに適用する
  • パラメータ型に関しては、クラスよりインタフェースを選ぶ

    • 例えば、HashMapの代わりにMapを使用すれば、HashTableHashMapTreeMapなどどれでも渡すことができるため、柔軟かつ拡張性も高い
  • booleanパラメータより2つの要素を持つenum型を使用する

    • booleanよりもパラメータの意味が理解しやすく、可読性が向上するため

項目41. オーバーロードを注意して使用する

オーバーロードしたメソッドの選択は静的(コンパイル時に決定)であるため、ユーザーを混乱させる可能性があるため。

オーバーロードの利用

以下の例を考える。

public class CollectionClassifier {
    public static String classify(Set<?> a) {
        return "Set";
    }
    public static String classify(List<?> a) {
        return "List";
    }
    public static String classify(Collection<?> a) {
        return "Unknown Collection";
    }

    public static void main(String[] args) {
        Collection <?>[] collections = {
            new HashSet<String>(),
            new ArrayList<BigInteger>(),
            new HashMap<String, String>().values()
        };

        for (Collection<?> c : collections) {
            System.out.println(Overload.classify(c));
        }
    }
}

上記のプログラムを実行した場合、"Set" → "List" → "Unknown Collection" と表示すると期待するかもしれないが、 実際は "Unknown Collection" が3回表示される。

これは、オーバーロードされたメソッドの選択はコンパイル時に行われるためである。 コンパイル時には、3回のループでclassifyメソッドに渡されるパラメータの型は全てCollection<?>である。

したがって同じ数のパラメータを持つ複数のシグニチャでメソッドをオーバーロードすることは、基本的に控えるべきである。 例えばObjectOutputStreamクラスは複数のwriteメソッドを持っているが、 writeBoolean(boolean)writeInt(int)writeLong(long)といったように異なったメソッド名を用いているため、 ユーザーは混乱しない。

またコンストラクタの場合、異なる名前を使用することができないため、staticファクトリーメソッドの利用を検討する。

オーバーライドの利用

オーバーライドしたメソッドの選択は動的に行われる。

class Wine {
    String name() { return "wine"; }
}

class SparklingWine extends Wine {
    @Override String name() { return "sparkling wine"; }
}

class Champagne extends SparklingWine {
    @Override String name() { return "champagne"; }
}

public class Overriding {
    public static void main (String[] args) {
        Wine[] wines = {
            new Wine(), new SparklingWine(), new Champagne()
        };
        for (Wine wine: wines) {
            System.out.println(wine.name());
        }
    }
}

上記を実行すると、 "wine" → "sparkling wine" → "sparkling wine" と表示される。 オーバーライドされたメソッドの呼び出しには、オブジェクトのコンパイル時の型は影響がない。

項目42. 可変長引数を注意して使用する

可変長引数メソッド

指定された型の0個以上の引数を受け付けるメソッドを、可変長引数メソッドと呼ぶ。 可変長引数の仕組みは、呼び出した時点で渡された引数の数と同じ大きさの配列を最初に生成し、 その配列に引数値を入れて、最後のその配列をメソッドに渡す。

例えば、複数のint変数の合計を返す可変長引数メソッドは以下。

static int sum(int ... args) {
    int sum = 0;
    for (int arg: args) {
        sum += arg;
    }
    return sum;
}

ある型の引数を0個以上ではなく1個以上必要とする場合は、第1引数を通常の引数とし、第2引数を可変長引数とするとよい。

static int min(int firstParam, int... params) {
    int min = firstParam;
    for (int param: params) {
        if (param < min) {
            min = param;
        }
    }
    return min;
}

可変長引数メソッドへの変更時の注意

可変長引数は最終的に配列としてメソッドに渡されるため、最後の引数が配列である既存のメソッドを可変長引数メソッドに 変更することは容易である。しかし、むやみに可変長引数メソッドに変更すべきではない。

例えば Arrays.asList メソッドは、1.4までは Object 型の配列を受け取って、List 型に変換していた。具体的には、配列の各要素を取り出し、List に格納し返却している。下記のコードでは、配列を List に変換することで、配列の各要素を確認することができている(配列に対して直接 toString を呼び出しても、ハッシュ値が表示されて役に立たない)。

System.out.println(Arrays.asList(myArray));

上記myArrayint 型の配列の場合、コンパイルエラーとなる。なぜなら java.lang.Object[]int[] の間に継承関係はないためである。

しかし、1.5から Arrays.asList メソッドは、Object 型の配列ではなく、Object 型の可変長引数を受け取るようになった。この時 myArrayint 型の配列の場合、Object 型に int[] は渡すことができるため、コンパイルエラーとはならないが、int[]myArray のみを要素とする List<int[]> が返却される。すると、要素は int[] であるため、ユーザーの意図する各要素の値ではなく配列のハッシュ値が表示されてしまう。

このように、可変長引数メソッドを乱用するとユーザーを困惑させることになりかねないため、注意が必要である。

項目43. null ではなく、空配列か空コレクションを返す

配列やコレクションを返すメソッドが、空配列や空コレクションの代わりに、null を返すべきではない。 null を返却すると、クライアントで null を処理する余分なコードを書かなければならないため。 もしチェックを忘れると NullPointerException が発生する恐れもある。

null を返す場合と比較して、空配列を返す方が配列を割り当てるコストがかかるため、好ましくないと言われることがあるが、 これは正しくない。なぜなら、このレベルでパフォーマンスが問題となることはほとんどない。

また、どうしても空配列を返却したい場合は、次のように同一のオブジェクトを使いまわせば良い。 下記イディオムはコレクションが空の場合、空配列を新規に生成しないため、空配列生成のコストは問題とならない。

private final List<Cheese> cheeses = ...;

private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];

public Cheeses[] getCheeses() {
    return cheeses.toArray(EMPTY_CHEESE_ARRAY);
}

項目44. すべての公開API要素に対してドキュメントコメントを書く

Javadoc を利用することで、ソースコードから自動的にAPIドキュメンテーションを生成することができる。 APIを適切に文書化するために、全ての公開されているクラス、インタフェース、コンストラクタ、メソッド、フィールド宣言の前に ドキュメントコメント(Javadoc)を書くべきである。

Javadoc を書く際は以下の点に気をつける。

  • メソッドに関するドキュメントコメントには、メソッドがどのように処理を行なっているかではなく、何を行なっているかを記述すべき

    • 事前条件と事後条件をすべて列挙すること
    • すべての副作用を文書化すること
    • スレッド安全性について記述すること
    • 上記を記載するために @param タグ、@return タグ、@throws タグを記載すること
  • ジェネリック型やジェネリックメソッドを文書化する際には、すべての型パラメータを文書化するのを忘れない

/**
 * An object that maps keys to values. A map cannot contain
 * duplicate keys; each key can map to at most one value.
 *
 * @param <K> the type of keys maintained by this map
 * @param <V> the type of mapped values
 */
public interface Map<K, V> {
  ...
}
  • enum 型を文書化する際には、型とすべての public のメソッドだけでなく定数も文書化すること
/**
 * An instrument section of a symphony orchestra.
 */
public enum OrchestraSection {
    /** Woodwinds, such as flute, clarinet, and oboe. */
    WOODWIND,
    /** Brass instruments, such as french horn and trumpet. */
    BRASS,
    ...
}
  • アノテーションを文書化する際には、その型自身だけでなく、全てのメンバーを文書化する
/**
 * Indicates that the annotated method is a test method that
 * must throw the designated exception to succeed.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    /**
     * the exception that the annotated test method must throw
     * in order to pass. (The test is permitted to throw any 
     * subtype of the type described by this class object.)
     */
    Class<? extends Throwable> value();
}

Effective Javaを勉強します【第6章】

項目30. int定数の代わりにenumを使用する

一年での季節、太陽系の惑星、トランプの役など、固定数の定数からその値が成り立つ型を列挙型と呼ぶ。Java4以前で列挙型を表現する際には int enum パターンが用いられていたが、Java 5以降では enum 型を用いて列挙型を表現すべきである。

int enum パターン

以下は int enum パターンの例である。

public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;

public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLAE = 1;
public static final int ORANGE_BLOOD = 2;

上記は以下のような欠点を持つ。

  • 型安全性を提供しない。オレンジを期待するメソッドにアップルを渡したり、オレンジとアップルを==演算子で比較しても何のエラーにもならない。
  • 名前空間を提供しない。APPLE_等の接頭辞で名前の衝突を防ぐ手間がかかる。
  • int enum 定数を変更する度に再コンパイルの必要がある。再コンパイルされない場合、動作はするが結果がどうなるかはわからない。
  • 利便性が低い。デバッガ等で表示しても文字情報は取得することができず、定数をイテレートするなどの操作手段もない。

int 定数の代わりに String 定数が用いられる場合もあるが、文字列比較のコストやハードコートした際の誤字によるバグを考慮すると、さらに望ましくない。

enum

上記で説明した int enum 型の欠点を回避するのが enum 型である。以下はその例である。

public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }

enum 型は内部的にはクラスであり、クラス同様パッケージに属する。基本的には1ファイルに火取るの enum 型を定義し、型名と同じファイル名のファイルに記述する。内部クラスのように他のクラスの内部に定義することも可能。

上記Apple を定義した Apple クラスを javap コマンドで逆コンパイルすると、以下のように表示される。

Compiled from "Apple.java"
public final class Apple extends java.lang.Enum<Apple> {
  public static final Apple FUJI;
  public static final Apple PIPPIN;
  public static final Apple GRANNY_SMITH;
  public static Apple[] values();
  public static Apple valueOf(java.lang.String);
  static {};
}

これにより以下のようなことが分かる。

  • enum 宣言された型は java.lang.Enum を継承している
  • 個々の列挙定数は public static final であり、自身が宣言された型のインスタンスである
  • enum 型はアクセス可能なコンストラクタを持っていないため事実上 final であり、クライアントは enum 型のインスタンスの生成や拡張は不可能である(シングルトン)

enum は型安全性を提供している。たとえば型 Apple で引数を宣言した場合、null でないオブジェクト参照は必ず FUJI / PIPPIN / GRANNY_SMITHのいずれかの値であることが保証される。また、異なる enum 型の値を渡そうとしたり、異なる enum 型の値同士を == で比較しようとするとコンパイル時エラーになる。

また、各 enum 型は独自の名前空間を提供するため、同一名の定数が共存可能である。加えて toString メソッドにより文字列へ変換することも可能である。

enum型 へのフィールド・メソッドの追加

enum 型に任意のフィールド・メソッドを追加することにより、定数にデータを関連づけることが可能である。

例えば、太陽系の惑星について考えてみる。各惑星は、質量と半径を持ち、それらから表面重力や惑星表面上での物体の重さが計算可能である。

これらを enum で表現すると以下のようになる。

public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7);

    private final double mass;
    private final double radius;
    private final double surfaceGravity;

    public static final double G = 6.67300E-11;

    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        this.surfaceGravity = G * mass / (radius * radius);
    }

    public double mass() { return mass; }
    public double radius() { return radius; }
    public double surfaceGravity() { return  surfaceGravity; }
    public double surfaceWeight(double mass) { return mass * surfaceGravity; }
}

enum にデータを関連づけるためには、インスタンスフィールドを宣言し、データを受け取るコンストラクタを書き、そのフィールドにデータを保存すれば良い。enum は不変であるため、すべてのフィールドは final とすべきである。

enum の値セットを取得するにはvalues()メソッドを用いる。これにより、各惑星上での物体の重さを計算するプログラムを以下のように記述することができる。

double earthWeight = 1.0;
double mass = earthWeight / Planet.EARTH.surfaceGravity();
// 地球上で重さ1.0の物体の各惑星上での質量を計算する
for (Planet p : Planet.values())
    System.out.printf("Weight on %s is %f%n",
                      p, p.surfaceWeight(mass));

定数固有メソッド実装

Planet クラスの例では、定数ごとのフィールド値が異なってもメソッドの振る舞いは同じだったが、各定数にメソッドの振る舞いを変えたい場合がある。そのような時には定数固有メソッド実装を利用する。以下はその例。

public enum Operation {
    PLUS { double apply(double x, double y) { return x + y; } },
    MINUS { double apply(double x, double y) { return x - y; } },
    TIMES { double apply(double x, double y) { return x * y; } },
    DIVIDE { double apply(double x, double y) { return x / y; } };

    abstract double apply(double x, double y);
}

このように enum 内で抽象メソッドを宣言し、各定数ごとにオーバーライドすることで振る舞いを変化させることができる。仮に実装を忘れたとしても、コンパイル時エラーとなりそのまま動作することはない。

また、定数固有メソッドは定数固有データを同時に利用することができる。

public enum Operation {
    PLUS("+") { double apply(double x, double y) { return x + y; } },
    MINUS("-") { double apply(double x, double y) { return x - y; } },
    TIMES("*") { double apply(double x, double y) { return x * y; } },
    DIVIDE("/") { double apply(double x, double y) { return x / y; } };

    private final String symbol;
    Operation(String symbol) { this.symbol = symbol; }

    abstract double apply(double x, double y);
}

fromString メソッドの実装の検討

enum はデフォルトで、定数の名前を定数自身へ変換する valueOf(String) メソッドを持っている。(上記Operation の場合、返り値の型は Operation

もし enum 型で toString メソッドをオーバーライドする場合、それに対応する fromString メソッドの実装を検討すべきである。 fromString メソッドは、引数で渡されたカスタム文字列を定数自身へ変換する。

private static final Map<String, Operation> stringToEnum = new HashMap<>();
// toString() で定義したカスタム文字列を Map で記憶する
static {
   for (Operation op : values()) {
       stringToEnum.put(op.toString(), op);
   }
}
// toString() でカスタム文字列を定義
@Override
public String toString() {
    return symbol;
}
// toString に対応する fromString()
public static Operation fromString(String symbol) {
    return stringToEnum.get(symbol);
}

パフォーマンス

一般的に、パフォーマンスに関して enum は int 定数と比べても遜色ない。小さな差ではあるが、enum 型のロードと初期化には空間的・時間的コストが発生する。しかし資源制約の厳しいデバイス等でなければ、そのコストが顕著になることはない。

項目31. 序数の代わりにインスタンスフィールドを使用する

enumordinal() というメソッドを持っている。このメソッドは enum 型内の各 enum 定数の整数位置を返す。

public enum Ensemble {
    SOLO, DUET, TRIO, QUARTET, QUINTET,
    SEXTET, SEPTET, OCTET, NONET, DECTET;

    // Ensemble.SOLO.ordinal() であれば 1 を返す
    public int numberOfMusicians() { return ordinal() + 1; }
}

上記は動作するが、以下の理由より保守性が非常に低い。

  • 定数が並び替えられた場合、numberOfMusicians は動作しなくなる
  • 複数の定数の同じ整数値を割り当てることができない
    • 例えばダブルカルテット(4人×2)の場合、オクテット(8人)との区別ができない

そもそも ordinal メソッドは、EnumSet や EnumMap などの汎用の enum をベースとしたデータ構造の実装を手助けする手段として提供されている。したがって普通の開発者は ordinal メソッドを使うべきではない。

代わりに、下記のようにインスタンスフィールドを用いて、整数値を関連づける。これにより上記の保守性の問題はすべて解決する。

public enum Ensemble {
    SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
    SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
    NONET(9), DECTET(10), TRIPLE_QUARTET(12);

    private final int size;
    Ensemble(int size) { this.size = size; }
    public int numberOfMusicians() { return this.size; }
}

項目32. ビットフィールドの代わりにEnumSetを使用する

enum が集合で用いられる場合、従来はビットフィールドが利用されてきたが int enum パターンと同様の欠点を持つ。 代わりに EnumSet を用いることで、ビットフィールドの簡潔性とパフォーマンスを維持しつつ、欠点を解決することが可能である。

ビットフィールド

ビットフィールドとは、列挙型の各定数に異なる2の累乗を割り当てて、int enum パターンを使用する方法である。

public class Text {
    public static final int STYLE_BOLD   = 1 << 0; // 1
    public static final int STYLE_ITALIC = 1 << 1; // 2
    public static final int STYLE_UNDERLINE = 1 << 2; // 4
    public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8

    // パラメータは、0個以上のSTYLE_定数のビット
    public void applyStyles(int styles) {
        ...
    }
}

ビットフィールドとして知られている複数の定数を1つの集合にまとめるために、 以下のようにビット和操作を行う。

// 以下はtext変数がボールドかつイタリックであることを指す(引数は 0011)
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

ビットフィールドは和集合や共通集合などの操作を効率的に行うが、int enum 定数の短所を全て持っている。

EnumSet

ビットフィールドの代わりに、EnumSet が利用可能である。 EnumSet は Set インタゲースを実装しており、豊富な機能、安全性、他の Set 実装との相互運用性が提供されている。 またパフォーマンスも、ビットフィールドと比べても遜色ない。

public class Text {
    public enum Style {
        BOLD, ITALIC, UNDERLINE, STRIKETHROUGH
    }
    // どのようなSetでも渡せるが、EnumSet が明らかに最善
    public void applyStyles(Set<Style> styles) {...}
}

各属性は以下のように設定可能である。

// 以下はtext変数がボールドかつイタリックであることを指す
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

項目33. 序数インデックスの代わりにEnumMapを使用する

下記のようなハーブを表現したクラスがあるとする。 ハーブは3種に分類可能であり(一年生植物(annual)、多年生植物(perennial)、越年性植物(biennial))、 それらを enum で表現している。

public class Herb {
    enum Type {
        Annual, Perennial, Biennial
    }

    private String name;
    private String type;

    public Herb(String name, String type) {
        this.name = name;
        this.type = type;
    }
}

ここで、ハーブを種類ごとに Set などのコンテナに格納したいとする。 この時、種類の序数でインデックスされた配列で格納先の Set を区別してはいけない。 このような時は EnumMap を用いるのが適切である。

序数インデックスを用いた配列による分類

種類の序数でインデックスされた配列に各種ハーブを格納する場合、以下のようなコードになる。

// 種類の混在したハーブの集合
Herb[] all = new Herb[] { ... };

// 3種類のハーブに対応した3つの Set を配列にする
Set<Herb>[] herbsByType = (Set<Herb>[]) new Set[Herb.Type.values().length];
for (int i = 0; i < herbsByType.length; i++) {
    herbsByType[i] = new HashSet<Herb>();
}

// ハーブを種類ごとに分類
for (Herb h: all) {
    herbsByType[h.type.ordinal()].add(h);
}

この方法は以下の問題を抱える。

  • 配列はジェネリックスと互換性がないため、無検査キャストを必要とし、コンパイル時には警告が出る
  • 配列のインデックスからハーブの種類を読み取ることができない
  • 配列のアクセス時に、意図した種類と対応したインデックスが利用されるかはクライアント次第(型安全でない)

EnumSet を用いた分類

EnumSet を用いてハーブを分類する場合、以下のようなコードになる。

// 種類の混在したハーブの集合
Herb[] all = new Herb[] { ... };

// ハーブの種類をキー、値を Set としたMapを作成
Map<Herb.Type, Set<Herb>> map = new EnumMap<>(Herb.Type.class);
for (Herb.Type type: Herb.Type.values() {
    map.put(herb.type, new HashSet<Herb>());
}

// ハーブを種類ごとに分類
for (Herb herb: all) {
    map.get(herb.type).add(all);
}

このプログラムは序数インデックスを用いる方法と比べて

  • より短く、より明瞭、より安全
  • パフォーマンスも遜色ない
  • 型安全

多次元関係

2つの enum からの対応付けを表す時も同様であり、序数を用いるよりEnumSetを用いた方が良い。 例えば水の相転移を表現したいとする。(液体(liquid)から個体(solid)は凍結(fleezing)、液体から気体(gas)は沸騰(boiling))

以下は序数を用いる例。

public enum Phase {
    SOLID, LIQUID, GAS;
    
    public enum Transition {
        MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;

        // 状態遷移を配列で表現
        Transition[][] TRANSITIONS = {
            { null,    MELT,     SUBLIME},
            { FREEZE,  null,     BOIL},
            { DEPOSIT, CONDENSE, SUBLIME}
        };

        // 状態遷移時の現象を取得する
        public static Transition from(Phase src, Phase dst) {
            return TRANSITIONS[src.ordinal()][dst.ordinal()];
        }
    }
}

上記は、ハーブの分類を序数を用いて行った時と同様に、以下の欠点を持つ

  • 序数と配列のインデックスの関係をコンパイラは知らないため、転移表に誤りがあったりした場合、実行時エラーとなる
  • null が増えると空間が無駄になる

ハーブの例と同様に、EnumMap を用いることでこれらの問題は解決する。 新たにプラズマという相を追加する場合であっても、TransitionPhase に定数を追加するだけで良い。

public enum Transition {
    MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
    BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
    SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);

    private final Phase src;
    private final Phase dst;

    public Transition(Phase src, Phase dst) {
        this.src = src;
        this.dst = dst;
    }

    // 二重 Map
    private static final Map<Phase, Map<Phase, Transition>> map = 
        new EnumMap<Phase, Map<Phase, Transition>>(Phase.class);
    static {
        for (Phase phase: Phase.values()) {
            map.put(phase, new EnumMap<Phase, Transition>(Phase.class));
        }
        for (Transition transition: Transition.values()) {
            map.get(transition.src).put(transition.dst, transition);
        }
    }

    public static Transition from(Phase src, Phase dst) {
        return map.get(src).get(dst);
    }
}

項目34. 拡張可能なenumをインタフェースで模倣する

基本の enum 型に伴うインタフェースを書いて、そのインタフェースを実装することで拡張可能な enum 型を模倣することが可能。

オペコード

オペコードは拡張可能な列挙型を使わざるをえない場面の1例。enum 型が任意のインタフェースを実装できるという事実を利用して、 オペコードに関するインタフェースを定義し、それを実装した enum を定義してやればよい。

interface Operation {
    double apply(double x, double y);
}

public enum BasicOperation implements Operation {
    PLUS("+") {
        @Override public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-") {
        @Override public double apply(double x, double y) {
            return x - y;
        }
    };

    private final String symbol;
    BasicOperation(String symbol) {
        this.symbol = symbol;
    }
}

// Enumは拡張不可だが、共通インタフェース Operation を利用して拡張した操作を定義している
public enum ExtendedOperation implements Operation {
    EXP("^") {
        public double apply(double x, double y) {
            return Math.pow(x, y);
        }
    },
    REMAINDER("%") {
        public double apply(double x, double y) {
            return x  % y;
        }
    };

    private final String symbol;
    ExtendedOperation(String symbol) {
        this.symbol = symbol;
    }
}

項目35. 命名パターンよりアノテーションを選ぶ

命名規則とは、JUnitのように「テストケースメソッドの先頭には"test"をつける」といったルールのこと。

命名規則の代わりにアノテーションを利用することには以下のようなメリットがある。

  • 命名の際の誤字などをコンパイラでチェックできる
  • パラメータを関連づけることができる
  • クラスやメソッドなど、利用箇所を限定することができる

アノテーション

JavaにはOverrideアノテーションなど、いくつかのアノテーションが用意されているが、自分でアノテーションを定義することも可能。 その際は@interfaceを用いる。

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

上記ではTestアノテーションを定義している。

また@Target(ElementType.METHOD)のように、定義時に別のアノテーションを付与することもできる(メタアノテーション)。

@Retention(RetentionPolicy.RUNTIME)Testアノテーションがメソッド宣言にのみ付与できることを示している。 @Target(ElementType.METHOD)コンパイラによってクラスファイルに記録し、実行時にVMに保持することを示している。

マーカーアノテーション

Testアノテーションのように、パラメータを付与された要素を「マーク」するためのアノテーションを指す。

public class SampleTest {
    @Test public static void m1() {}
    public static void m2() {}
    @Test public static void m3() {}
}

上記の場合Testアノテーションが付与されたm1()m2()のみがテストとして実行される。

パラメータを持つアノテーション

ある特定の例外がスローされた場合のみ成功するテストを表すアノテーションを定義することを考える。 そのためには、アノテーションに特定の例外が何か教えなければならない。

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest{
    Class<? extends Exception> value();
}

上記のように定義することで、ExceptionTestアノテーションに、特定の例外を表すパラメータを与えることが可能となる。 以下のように例外を期待するテストに利用する。

public class SampleTest2 {
    @ExceptionTest(ArithmeticException.class)
    public static void m1() {
        int i = 0;
        i  = i / i;
    }
}

項目36. 常にOverrideアノテーションを使用する

スーパークラスの宣言をオーバーライドしている全てのメソッド宣言に対して Override アノテーションを使用すべき。

Override アノテーションを使用することで、例えば以下のようなオーバーライドミスを コンパイラが教えてくれる。

public class Bigram {
    private final char first;
    private final char second;
    public Bigram(char first, char second) {
        ...
    }
    // 以下は間違い。正しくは public boolean equals(Object o) {}
    public boolean equals(Bigram b) {
        ...
    }
    public int hasCode() {
        ...
    }
}

Bigram クラスでは equals` メソッドのオーバーライドに失敗し、誤ってオーバーロードしている。

このようなミスは見つけづらいが 以下のように Override アノテーションを利用することでコンパイラがエラーメッセージを出力してくれる。

    @Override public boolean equals(Bigram b) {
        ...
    }

項目37. 型を定義するためにマーカーインタフェースを使用する

マーカーインタフェースとは、メソッド宣言を含まないインタフェースのことであり、 そのインタフェースを実装しているクラスが何らかの特性を持っていることを示す。

例として Serializable インタフェースが挙げられる。これはメソッドを1つも持たないが、 ObjectOutputStream に利用できるという特性を示す。

マーカーインタフェースは型を定義できるため、コンパイル時のエラーチェックが可能であるという点で マーカーアノテーションより優れている。

ただし、クラスやインタフェース以外のプログラム要素をマークしたい場合は、マーカーアノテーションを使う必要がある。 (マーカーインタフェースはクラス以外をマークすることができない。)