ここでは、今さら誰にも聞けない(笑)Javaの基本クラスについてソースから解析を行います。
解析対象とするJDKは1.4.2です。
新しく書いたものが下(←他のページと逆)になるように並べてあります。
やはり最初にこのクラスを解析しておく必要があるでしょう。
最も使用頻度が高いクラスの一つであることは間違いないこのクラス、
使い方によっては思わぬパフォーマンス低下を招くこともあります。
まず、クラスの最初に定義されている一文はこれです。
private char value[];
char型はプリミティブ型であり、これ一つで全ての文字を表すことが可能です。
char型は常に Unicode 形式で扱われます。
よって、文字コードを意識する必要はありません。
これはほとんどの面で便利なのですが、唯一欠点があります。
文字コードを意識する必要が無い代わりに、文字以外の情報を扱えません。
つまり、char型にバイナリ値の格納は出来ないという事です。
よって String クラスのインスタンスにもバイナリ値の格納は出来ません。
当たり前のことかも知れませんが、文字列同士の比較をする場合
文字数が多い程そのコストは高くつきます。
equalsメソッドはHash系クラス等で頻繁に使われるので
マップに使うキー文字列は出来るだけ短い方が効率が上がります。
ハッシュの生成は、Stringインスタンスが示す文字列を一文字ずつループして
計算を行うことで実現されています。
よって、equalsメソッド同様文字数が少ない方がコストを下げることが出来ます。
substringメソッドは、思いの他コストが安いです。
例えば10000文字のStringオブジェクトに対して
substring(0, 8000) メソッドを実行したらどうなるでしょうか。
僕は以前まで、内部的には new char[8000]
が実行されているものだと
思っていました。Stringオブジェクトは固定長だという認識があったからです。
しかし実際にはこのような処理は行われません。
substringメソッドにより生成されたStringオブジェクトは
元の文字列とvalueフィールドを共有します。
その代わりに、offset と count という2つのフィールドに値を設定することで
valueが示す文字列の一部を別のStringオブジェクトとして表現できるのです。
これも内部的にsubstringメソッドを使用しているので、
無駄にオブジェクトを生成してしまうといった事はありません。
これらはどちらも日付を表すクラスですが、用途によって使い分ける必要があります。
単純に日付を扱うなら Date クラス、
時刻計算などを必要とするならば Calendar を使います。
正直、これを面倒だと思う人もいるかもしれません。
パフォーマンスを問題にしない場面なら、Calendar の方が高性能なので
全て Calendar で統一して使う手もあります。
Date は非常にシンプルなクラスです。
持つフィールドは long 型が一つだけで、生成も低コストで済みます。
シリアライズしてもたった4byte(integer型と同じ)の容量しか使いません。
Calendar は Date の派生クラスではありませんが、
Date にある機能は全て持っています。
しかし、このクラスは Date クラスと比べると「重い」という欠点を持っています。
保持しているフィールドの数も多く、生成にも時間が掛かります。
シリアライズすると1000byte以上の容量を消費します(ただし同一ストリーム内で初回のみ)。
ですから、このインスタンスをシリアライズするのは避けた方がいいでしょう。
Webアプリケーションのセッションオブジェクト等には含めないのが理想です。
Dateクラスには多数のdeprecatedメソッド、コンストラクタが存在します。
これは JDK1.0 の頃に存在していたもので、現在ではその使用が推奨されていません。
しかしこれらのメソッドを使いたくなる場面があるのも事実です。
時を返す getHours メソッド等がその例だと思います。
ただ、なぜこれらのメソッドが deprecated になったかを理解すれば
これらの使用を控える気になるはずです。
その理由とは、これらは同期(synchronized)メソッドだからです。
一見、getHours は同期メソッドでは無いように見えますが、
内部的にはstaticフィールドで同期を取っています。
つまり、複数のスレッドからこのメソッドを同時に呼び出すと
ロックが掛かってしまいパフォーマンスが落ちます。
Webアプリケーションは通常リクエスト毎にスレッドが分かれていますから
特にこれらのメソッドの使用は控えた方が良いでしょう。
Javaにはいくつもの入出力クラスがあります。
それらのパフォーマンスについて探ってみました。
出力系は OutputStream
または Writer
の派生クラスになります。
大まかに言えば前者はbyte出力、後者はString出力を行います。
一般的に前者の方が速いと言われていますが、果たしてどれ程の差があるのでしょうか。
試しに、FileOutputStream
と FileWriter
で出力テストを行いました。
10byte程の文字列を100000回ループさせます。
驚いたことに、結果は FileWriter の方が3倍以上も速かったのです。
理由は、ラッパクラスを通していなかったからでした。
FileWriter の内部を見ると、デフォルトで8Kbのバッファサイズが用意されています。
こんな事どこにも書いてないので今まで知りませんでした。
JDKのドキュメントを見ると、Writer は BufferedWriter でラップしないと
効率が非常に悪くなると記述されていますが、
実際には先程の理由からラップしなくても効率は悪くなりません。
そして、これは BufferedOutputStream にも言える事ですが
8Kb以上のバッファサイズを用意してもバッファリングの性能はそれ以上良くなりません。
推測ですが、物理的なセクター単位(今回の環境では4Kb)を超えるバッファサイズを指定しても
性能は上がらないような気がします。
ですから、FileWriterは事実上ラップする必要が無いという結論になります。
テストしたところ、ラップした方が遅くなる場合も多々ありました。
FileOutputStream を BufferedOutputStream でラップしたところ(バッファサイズは8Kb)、
その性能は FileWriter の倍以上になりました。
出力系以上に、ラップクラスを通すことが重要になります。
FileInputStream をラップしないで使用する事は止めましょう。
ただし、一度に全ての内容を読み込むのならばラップの必要はありません。
FileReader は FileWriter と同じようにデフォルトでバッファされているのですが
BufferedReader でラップすることでさらに速くなります。
出力ストリームと異なり、バッファサイズを8Kb以上にすることで
パフォーマンスはさらに良くなります。
BufferedInputStream でラップしたストリームからデータを読み込む場合、
ループして1byteずつ読み込むのと available()
メソッドで予めサイズを取得しておいて
byte配列にまとめて読み込んだ場合では、どのくらい速度が違うでしょうか。
1MB強のファイルで試してみたところ、
前者では約30ms、後者では約15msでした。
そう、思ったほど差が無いのです。
100万回のループによって掛かるコストは、最近のPCにとって
もはや無視できる位に小さなものになっていました。
出来る限り後者の方法を使った方が良いのは間違い無いですが
必要なときには前者の方法を取ってもスピード的には全く問題が無いという事です。
念のために。ラップクラスを通さずに100万回ループさせるような事は絶対に止めましょう。
JSPサーブレットは、デフォルトで8KBのバッファサイズを持っています。
これは、8KBの書き込み毎にクライアント側に出力を送ることを意味します。
大容量の出力を返すページ等では、この値を大きくすることで
パフォーマンスを上げることが出来る可能性があります。
ただし、この場合必要に応じて明示的な out.flush();
を使う必要があるかもしれません。
そうしないと、バッファが一杯になるまではクライアントに出力が送られないので
クライアント側のブラウザに何も表示されなくなる時間が長くなってしまいます。
どこかの記事でも書いてありましたが、
「5秒経った後で一気に画面が表示されるページ」と
「1秒ごとに画面が徐々に表示され、最終的には表示に8秒掛かるページ」では
後者の方が「レスポンスが良い」と感じられるそうです。
ですから、実際にブラウザからレスポンスを確認した上でバッファサイズの調整を行ってください。