O'REILLY JavaScript 第5版を飛ばし読む(13章 Webブラウザに組み込まれたJavaScript)

ここではDOMの構造やJavaアプレットの話が出てくるが、今回は割愛。

セキュリティ関係の話だけ少し取り出す。

JavaScript のセキュリティ

JavaScript ではできないこと

ユーザーの Web ブラウザ上で悪意のあるコードが実行されないよう、スクリプトの動作には以下のような制限がかけられている。

  • クライアントのPC上のファイルやディレクトリに対して、読み出し、書き込み、作成、削除、表示ができない
  • ソケットを開く、他のホストからの接続を受け付ける、といったネットワーク機能はサポートしていない
  • クリック等のイベントなしに、ブラウザをオープンすることはできない
  • そのプログラムがオープンしたウィンドウ以外は、ユーザーの確認なしにクローズできない
  • リンクをマウスオーバーした際に、飛び先がステータス行に表示されるが、その内容をスクリプトで変更することはできない
  • 1辺が100ピクセル以下のウィンドウ、画面サイズを超える大きさのウィンドウ、タイトルバーやステータス行のないウィンドウは作成できない
  • 画面外にウィンドウを移動することはできない
  • HTML の FileUpload 要素の Value プロバティには値を設定できない
  • 同一出身ポリシー(後述)

同一出身ポリシー

同一出身ポリシーとは、以下のような内容である。

  • あるスクリプトは、そのスクリプトを含むドキュメントと同じ出身のウィンドウやドキュメントのプロパティしか読み出せない
  • 同様に、XMLHttpRequest オブジェクトで HTTP を制御する際も、リクエストを送信できるのはそのスクリプトを含むドキュメントがロードされた Web サーバーに対してのみ
  • ドキュメントの「出身」は、そのドキュメントをロードした URL のプロトコルとホストとポート番号の組み合わせで決まる

これは、スクリプトを使って機密情報を盗めないようにする機構である。

例) 企業のイントラネット中のブラウザにロードされた悪意のあるスクリプトが空のウィンドウを開いたとする。  ユーザーがそのウィンドウからイントラネットのファイルを閲覧したとしても、同一出身ポリシーにより悪意のあるスクリプトはその内容を読み出すことはできない。

ただし、複数のサーバーを使用するような大規模な Web サービスでは、異なるサーバーからロードされたドキュメント間でプロパティを読み出さなければならない場合がある。 このような場合は各 Document オブジェクトの domain プロパティに同じ値を設定してやればよい。

document.domain プロパティが同一である場合、2つのドキュメントは同一の出身とみなされ、相互にドキュメントのプロパティを読み出せるようになる。 デフォルトではドキュメントをロードしたサーバーのホスト名が設定されているが、この値は書き換えることができる。

例) orders.example.com と catalog.example.com からロードしたドキュメントの domain プロパティを「example.com」に変更することで、同一出身ポリシーを回避できる。

クロスサイトスクリプティング

ある Web ページが、ユーザの入力したデータから HTML タグを削除せずに、ユーザの入力をそのまま利用してドキュメントのコンテンツを動的に生成していれば、クロスサイトスクリプト脆弱性が存在する。 (HTML タグを削除する処理のことを「サニタイズ」と呼ぶ)

クロスサイトスクリプティングの流れは以下(一例)。

  • クロスサイトスクリプト脆弱性が含まれるサイトAがある。
  • 悪意のあるユーザーがサイトAへのリンクを含むサイトBを用意する。サイトAへのリンクのクエリパラメータには、悪意のあるスクリプトをロードする script タグを含む値がセットされている。
  • ユーザーが上記リンクからサイトAへアクセスすると、サイトAでクエリパラメータを用いてコンテンツが動的に生成され、悪意のあるスクリプトがロードされる。
  • 悪意のあるスクリプトはクッキーの値等を読み出し、ユーザー情報等を取得することができてしまう。

感想

セキュリティの話はやっぱり変わらず大切。クロスサイトスクリプティングっていつも名前だけ覚えていて、内容なんだっけ。。。ってなる。

Java アプレットはもう殆ど見ないなあ。

O'REILLY JavaScript 第5版を飛ばし読む(9章 クラスとコンストラクタとタイプ)

仕事で JavaScript 結構書くので、基礎的なところは抑えときたいと思って、O'REILLY JavaScript 第5版を飛ばし読みしています。まずは JavaScript でのクラスの実現方法から。

JavaScript におけるコンストラクタ、メソッド

JavaScriptのオブジェクトは、new 演算子またはオブジェクトリテラル{} を使用することで生成できる。 new 演算子と一緒に使う関数をコンストラクタと呼ぶ。

また、オブジェクトのプロパティとして呼び出される関数をメソッドと呼ぶ。 この形式で関数を呼び出すと、関数を呼び出したときに使われたオブジェクトが this キーワードの値になる。

// コンストラクタ
funtion Rectangle(w, h) {
    this.width = w;
    this.height = h;
    // メソッド
    this.area = fucntion() {
        return this.width * this.height;
    }
}

// オブジェクトの生成
var r = new Rectangle(2, 4); //または rect = {width: 2, height: 4}
// 面積の計算
var a = rect.area;

プロトタイプと継承

上記の書き方には問題がある。それは生成されたすべての Rectangle オブジェクトが、3つのプロパティを持つ点である。 widthheight はオブジェクトごとに値が違うが、area は共通の関数を参照するため、オブジェクトごとに持つ必要はない。

この問題を解決するのがプロトタイプである。 実はすべての javascript オブジェクトには、プロトタイプオブジェクトというオブジェクトへの参照が含まれる。(prototype プロパティ)

new 演算子を使用してオブジェクトを生成すると、コンストラクタ関数の prototype プロパティの値が、生成されたオブジェクトのプロトタイプになる。 生成されたオブジェクトは自身のプロトタイプのプロパティを参照することができる。

例えば、上記の例であればコンストラクRectangleprototype にメソッド area を設定すれば、 すべての Rectangle オブジェクトから共通の area を参照できるようになる。これは Java でいう継承にあたる。

// コンストラクタ
funtion Rectangle(w, h) {
    this.width = w;
    this.height = h;
}

// プロトタイプ
Rectangle.prototype.area = fucntion() {
    return this.width * this.height;
}

プロトタイプを適切に使用することで、使用するメモリを大幅に節約することができる。 また、あるオブジェクトを生成した後に、そのオブジェクトのプロトタイプにプロパティを追加しても、その値を継承することができる。

継承プロパティへのアクセス

オブジェクト o のプロパティ p を読み出す時は、まずオブジェクト o にプロパティ p があるかをチェックし、 なければオブジェクト o のプロトタイプにプロパティ p があるかをチェックする。

一方で、オブジェクト o にプロパティ p を書き込む時は、オブジェクト o がプロパティ p を持たない場合、 自動でオブジェクト o に新しいプロパティ p が追加される。 これは、仮にプロトタイプのプロパティ p を書き換えると、同じプロトタイプを継承しているすべてのオブジェクトに影響が出るためである。

JavaScriptにおけるクラス

JavaScript にはクラスという正式の概念はない。ただし、上述したようにプロトタイプを用いて継承を実現しており、 Java などクラスベースのオブジェクト指向言語の長所をかなり実現している。

java などの言語と大きく異なる点は、プロパティやその型が予め決められておらず、動的に追加可能である点である。

※ これらの点について、最近のフロントエンド開発では ES6 以降の新しい構文や flow などのツールを用いることで改善が図られている。詳細はまたどこかで。。。

JavaScript における継承の例

以下に前述した Rectangle クラスを継承したサブクラスである PositionedRectangle クラスの例を示す。 サブクラスを作成する場合は、そのプロトタイプオブジェクトにスーパークラスインスタンスを指定すればよい。

// Rectangle クラスに位置情報を追加した PositionedRectangle クラスのコンストラクタ
funtion PositionedRectangle(x, y, w, h) {
    // call メソッドでスーパークラスのコンストラクタを呼び出す
    Rectangle.call(this, w, h);

    this.x = x;
    this.y = y;
}

// スーパークラスのコンストラクタを指定することで、サブクラスからスーパークラスのプロパティが参照可能になる
PositionedRectangle.prototype = new Rectangle();

// スーパークラスの area メソッドのみを継承し、 width や heightの値自体は継承したくない場合
// プロトタイプから削除する
delete PositionedRectangle.prototype.width;
delete PositionedRectangle.prototype.height;

// コンストラクタプロパティは、自身を参照するよう書き換える
PositionedRectangle.prototype.constructor = PositionedRectangle;

上記のようにサブクラスを生成せずに、 別のクラスのプロトタイプをあるクラスのプロトタイプへコピーして別のクラスのメソッドを継承する事もできる。(詳細な説明は割愛)

感想

プロトタイプを用いたクラスと継承の仕組みが理解できたのは良かった。 種々の JavaScript ライブラリ使ってても、デベロッパーツールとか使ってデバッグするときは生の JavaScript 読むことになるし、そういうときに上記を知っていれば捗る気がする。

本には JavaScript におけるオブジェクトクラスの判定法とかいろいろ書いてあるけど、最近は ES6 使って Babel とかで変換して・・・ってのが普通だし、そこらのテクニックはそんなに意識しなくてよいのかなと思うので割愛。

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 を除いた名前であることが多い