-
001292
とある資料を作るにあたって、Java で Generics を使った際のパフォーマンスについて調べてみました。
まず、java.lang.ArrayList をベースにして StringArrayList を作成しました。 このクラスは ArrayList 中で型パラメーターを使っている箇所をすべて String に置き換えたものです。 いわゆる String 専用の ArrayList です。 これと同じことは、Generics による ArrayList<String> でも実現できます。 型パラメーターを置き換えただけなので当然ですよね。
それを踏まえて、以下の二つのコードの処理時間を比較して見ましょう。
StringArrayList stringArrayList = new StringArrayList(ARRAY_LENGTH); ... for (int i = 0; i < ARRAY_LENGTH; ++i) { String str = stringArrayList.get(i); }ArrayList<String> arrayList = new ArrayList<String>(ARRAY_LENGTH); ... for (int i = 0; i < ARRAY_LENGTH; ++i) { String str = arrayList.get(i); }すると 1M 要素でおよそ 130-150% ほど StringArrayList のほうが速いという結果になりました。 この理由を探るためにコンパイル後の *.class を逆コンパイルしてみました。
for(int i = 0; i < 0xf4240; i++) { String s = stringArrayList.get(i); }for(int i = 0; i < 0xf4240; i++) { String s1 = (String)arrayList.get(i); }注目すべきは Generics を使用した場合のキャストです。 対照実験として次のコードを測定してみます。
for (int i = 0; i < ARRAY_LENGTH; ++i) { Object obj = arrayList.get(i); }この場合は stringArrayList と arrayList で速度に差はなく、逆コンパイルの結果は次のようになっていました。
for(int i = 0; i < 0xf4240; i++) { Object obj = arrayList.get(i); }キャストがありません。 このことから、自動的に実行されるキャストが、Generics を使った ArrayList のパフォーマンスを押し下げている可能性が高いと結論付けられます。
さすがに、いまさら特定型専用の Collections を再発明すべきだとは思いませんが、本当にカッツカツにパフォーマンスを求める場合はこのあたりも気にする必要があるんですね。
[ permalink ] [ 0 comment(s) ] [ 0 trackback(s) ] -
001291
酒の席の議論がなかなか面白かったのでこちらでも展開。 Java プログラミングを対象とした話です。
- null check は呼び側で行う?呼ばれ側で行う?
- null check を怠って NullPointerException を投げていいの?
つまり、
void methodA () { ... methodB (str); } void methodB (String str) { if (str != null) { return; } ... } (呼ばれ側主義)とするか、
void methodA () { ... if (str != null) { methodB (str); } } void methodB (String str) { ... } (呼び側主義)とするか。
呼ばれ側主義者の主張はこう。
- 呼び元で null check すると複数箇所に同じ check コードが分散し、コード量を増大させる
- null check をせずに NullPointerException が発生するのは恥ずかしい
- 事実これまで、呼ばれ側で null check しないと困ることが多かった(プラグマティズム)
一方、呼び側主義者の主張はこう。
- メソッド先頭に null check の if 文がずらずらと並ぶのは可読性を損ねる
- JDK のライブラリ群だって null を入れたら NullPointerException を投げている
この議論は平行線をたどり、お互い決定打が出ずなんとなくもやっとしたまま次回に持ち越されました。
ここから、その後にいろいろ考えた末の私見です。 ちなみに、ぼくは呼ばれ側主義穏健派の立場です。 呼ばれ側でもちろん行うべきであるが、さらに呼び元でもチェックするにやぶさかではない、と。
ぼくはこの話は、
- 前提条件をチェックすべきはどんなとき?チェックするならどこで?
- 前提条件を満たさなかった場合、呼び元にどう伝える?
という二つの話に分解すべきではなかろうかと考えています。 議題に現れる null check と NullPointerException という概念が、一見 null というキーワードでつながっているように見えますが、実は一方は処理の前提条件の話で、一方は処理失敗を伝える話なのではなかろうかと。
前提条件をチェックすべきはどんなとき?チェックするならどこで?処理失敗にも二種類あります。 実行前に発生が予見できないものと、できるものです。 前者の代表例は OutOfMemory など java.lang.Error のサブ・クラスとなっているものです。 後者は、前提条件が満たされていないまま処理が実行された場合が挙げられます。 例えば次のような処理の場合、
String name = item.getName();
item が非 null であることが処理の暗黙の前提条件となっています。 もし前提条件が破られた場合は例外が発生します。 この例外は、
if (item == null) { return; } String name = item.getName();のように、明確に前提条件を確認することで回避可能です。 特に、たとえば次のような場合、
- コストの高い処理を行う
- 副作用を伴う処理を行う
「やってみたけど途中でだめになりました」となった場合の影響が大きいため、処理の前に前提条件を確認し、条件が満たされていないのであれば、はじめから実行すべきではありません。 null check はこういった前提条件確認の典型で、他にも例えば値域や非零の確認なども考えられます。
次に、連続する処理の前提条件について考えてみます。 例えば次のような処理の場合を考えます。
... /* * アイテムを追加する */ // item の null check if (item == null) { return; } // 追加 ++ this.itemCount; // 副作用あり this.items.put(item.getId(), item); // null check のおかげで何事もなく完了 /* * 割引率を適用する */ // discount の値域チェック if (discount < 0 || 100 < discount) { // 割引率が 0-100 の範囲になければ例外 throw new IllegalArgumentException ("dicount should be between 0 and 100"); } // 適用 this.totalPrice += item.getPrice() * (1D - (double) discount / 100D); ...全体としては、アイテム追加と割引率適用の大きく二つの処理から構成されています。 アイテム追加の直前に item の null check が行われているので、アイテム追加処理は正常に完了します。 割引率の適用処理も直前に値域を判断しているので、定価を超えたりマイナスになったりすることはありません。 個々の処理は正常に完了します。 しかし、item が非 null かつ discount が負値の場合などには、アイテム追加処理の正常完了後に例外が発生してしまい、アイテム追加処理の副作用だけが残ってしまいます。 このように後続の処理が失敗すると、先行処理の取り消しが必要になるような場合があるため、前提条件の確認はできる限り早い段階で行うべきであると言えます。
... /* * 前提条件を確認する */ // item の null check if (item == null) { return; } // discount の値域チェック if (discount < 0 || 100 < discount) { // 割引率が 0-100 の範囲になければ例外 throw new IllegalArgumentException ("dicount should be between 0 and 100"); } /* * アイテムを追加する */ ++ this.itemCount; // 副作用あり this.items.put(item.getId(), item); // null check のおかげで何事もなく完了 /* * 割引率を適用する */ this.totalPrice += item.getPrice() * (1D - (double) discount / 100D); ...では、どこまで早い段階であれば良いのでしょうか。 これがまさに議論になった「呼び元側(より早い段階)で行うべき」か「呼ばれ側(それよりは遅い段階)で行うべき」かという論点に解を与える話かと思います。 先ほどの例に挙げた処理が、ひとつのメソッドであるとしましょう。
public void addItem(Item item, int discount) { /* * 前提条件を確認する */ // item の null check if (item == null) { return; } // discount の値域チェック if (discount < 0 || 100 < discount) { // 割引率が 0-100 の範囲になければ例外 throw new IllegalArgumentException ("dicount should be between 0 and 100"); } /* * アイテムを追加する */ ++ this.itemCount; // 副作用あり this.items.put(item.getId(), item); // null check のおかげで何事もなく完了 /* * 割引率を適用する */ this.totalPrice += item.getPrice() * (1D - (double) discount / 100D); }メソッドが呼ばれた時点で引数の値は確定しています。 前提条件の確認はできるだけ早く行うという原則に従い、確認は呼び出し直後よりも後に行うべきではありません。 では、それよりも早い時点ではどうでしょう。 呼び側が呼ぶ直前でも値は確定しています。 呼び側で前提条件を確認する場合を考えてみましょう。
public class Main { public void main (String [] args) { ... if (item == null) { ... } else if (discount < 0 || 100 < discount) { ... } else { cart.addItem(item, discount); } ... } } public class Cart { public void addItem(Item item, int discount) { /* * アイテムを追加する */ ++ this.itemCount; // 副作用あり this.items.put(item.getId(), item); /* * 割引率を適用する */ this.totalPrice += item.getPrice() * (1D - (double) discount / 100D); } }処理全体としては先ほどと変わりなく、前提条件が満たされた場合のみ処理が行われます。 しかし、クラスの堅牢性を考えると、この例は非常に大きな問題を孕んでいると言えます。 items と totalPrice の整合性に責任を持つべきは Cart(呼ばれ側)です。 しかしこの例では、Cart 自身は整合性を保つために必要な前提条件確認を行っていないため、悪意(もしくはミス)により不正な引数が与えられると壊れる、非常に脆弱なクラスとなっています。 引数としてどのような値を受け入れるかはクラスに属すべき情報であり、特に public メソッドはクラス内外の境界をなすため、引数のチェックは確実に行うべきです。
今回の議論の範囲からは少し外れますが、メソッドの先頭で前提条件を記述することで、そのメソッドが受け入れられる引数の範囲を "コードで" 明確化することができると考えています。 これは特にコードにアクセスできる開発者にとって、コードの可読性が高まるというメリットがあります。 もちろん、前提条件を確認すれば万事 OK なのではなく、別途明確に文書化もすべきですが。
ここまでをまとめると、
- 処理の前提条件はできる限り早くに確認すべし
- 堅牢性の観点から、前提条件の確認をクラスの外のみで行うべからず
- 上記二つより、特に public メソッドの引数は呼び出し直後に確認すべし
- 副次的に、読みやすくなるというメリットもあるよ
という主張になります。
前提条件を満たさなかった場合、呼び元にどう伝える?前提条件を満たさなかった場合には、呼び側にその旨を伝える必要があります。 その代表的な方法として以下のようなものが考えられます。
- 例外を投げる
- 戻り値として null を返す
- 戻り値としてエラーを示すオブジェクトを返す
- 引数として渡されたオブジェクトにエラーを示すオブジェクトを埋め込む
これらの方法は、伝えられる情報 / 呼び元への依存性 / コストなどがそれぞれ異なります。 こういったメリット / デメリットを考慮し、またはアプリケーションの設計方針に従って、前提条件が満たされていなかった旨を呼び元に戻す手段を決定します。
伝えられる情報 呼び元への依存性 コスト その他 例外を投げる ◎
デフォルトで発生場所 / スタック・トレースを戻し、さらに設計次第で柔軟に情報を戻せる◎
Java 言語使用で提供される機構であり、呼び元への依存性は低い×
new Exception() は比較的高コストJava っぽい 戻り値として null を返す ×
失敗した旨しか戻せない△
null の扱いについて、呼び元と共通理解をもつ必要がある◎
特に処理不要メソッドの正常な戻り値として null が返る可能性のあるメソッドでは使えない 戻り値としてエラーを示すオブジェクトを返す ○
設計次第で柔軟に情報を戻せる×
エラーを示すオブジェクトの設計について、呼び元と共通理解をもつ必要がある○
new Object() が必要引数として渡されたオブジェクトにエラーを示すオブジェクトを埋め込む ○
設計次第で柔軟に情報を戻せる×
エラーを示すオブジェクトの設計について、呼び元と共通理解をもつ必要がある○
new Object() が必要非常に Java っぽくない 議論に挙がった "NullPointerException" を投げるべきか否かも、この枠組みで考えるべきものです。 検討した結果 NullPointerException を投げるべき場合もあります。 しかし、NullPointerException は "null でした" という非常に汎用的な意味しか持ちません。 アプリケーション上では、それはもっと特化した意味を持つはずです(引数が不正であるとか、結果としてユーザー情報の取得が行えないとか)。 アプリケーション・コードは、それらの意味を適切に呼び側に伝える必要がある場合が殆どなので、NullPointerException をそのまま返すことはあまりないと言えるでしょう。 JDK のライブラリ群が NullPointerException を投げるのは、アプリケーションとは違って NullPointerException は文字通り NullPointerException の意味しかもてないためであると考えられます。 それに意味を加えるのは、JDK のライブラリを呼ぶ側のアプリケーション・コードの役割です。
ここまでをまとめると、
- 前提条件を満たさない場合は適切な方法でその旨を呼び側に戻すべし
- null check の結果を NullPointerException で戻すことも考えるべし
- しかし、アプリケーション・コードは NullPointerException ではなくより適切な情報を戻すべし
- 非常に汎用的なライブラリなどでは NullPointerException も辞さない覚悟で
といった主張です。
これまで、null check を呼ばれ側で行うことを当然のことと捉えていたため、それがなぜなのかを深く考えたことがありませんでした。 改めて分析したところ、やはり呼ばれ側で行うべきだとの思いを強くした次第です。 いかがでしょう。
-
000731
IBM の資料。勉強になります。以下、関連する資料。
これはBEA の資料。
[ permalink ] [ 0 comment(s) ] -
000427
2004-11-01 付で,ISID の Java コーディング規約が技術者コミュニティの「オブジェクト倶楽部」を通じて公開されました.
コーディングの工程を外出しするにあたって,品質を揃えるための布石という位置付けのようです. オブジェクト倶楽部がメンテナンスも行うようなのですが,社内資産と乖離してしまった場合はどうするのでしょうね?
コーディング規約は,コードを相互利用する範囲内で共通の認識があれば良くて,世界中のプログラマが単一のルールに従う必要はあまりないとぼくは思いますが,それらローカルルールを決めるにあたっての参考としてこのようなドキュメントが提供されていることはとても意義があると思います.
まだきちんと見ていないので中身に関するコメントは差し控えます.
コーディング規約の有名どころとして,他にこのようなものもあります.
[ permalink ] [ 0 comment(s) ] -
000373
久々の Java ネタ.
Java decompiler の Jad がサイト移転していました.
Jad - the fast JAva Decompiler以前までは Tripod に置いてあって,転送量制限に引っかかって DL できないこともしばしばでした. 今後はそんなことがなくなりそうかな?
ぼくは他の decompiler を使ったことはないけれど,これはなかなか優秀なツールだと思います. 難読化ツールにも対抗できると自ら宣言していますしね. ソースを公開していない Java アプリケーションの挙動がおかしい際に,これでソースを覗いて解決したことも数知れず (ただし,decompile 対象アプリケーションのライセンス条項には注意しましょう).
インストールはいたって簡単.上記リンク先の "Download Jad" から適当なバージョン/プラットフォーム用のバイナリを DL して展開するのみ. 実行形式のファイルが現れる筈です.
ぼくは Linux 用しか使ったことがありませんが,いずれのプラットフォームでもコンパイルは不要な筈です. ソースを公開していないと言っている以上,コンパイルしようがないですし.使い方もごく簡単で,
$ jad -lnc Test.class
こんな感じで Test.jad が生成されます."-lnc" は行番号を出力するオプションです. 複数ファイルを処理するのはこんな感じです.
$ jad -r -lnc **/*.class
ソースが見られないとどうも落ち着かない,という方は使ってみてはいかがでしょうか.
[ permalink ] [ 0 comment(s) ] -
000243
Java Technology Conference 2004
このイベントの案内メールが最近良く届くのだけれど,興味がある話は軒並み有料登録が必要で,しかも登録は 28,000円から.さすがに手が出ません.
- 高速なJavaアプリケーションのために:HotSpot 仮想マシンのチューニング方法
- Servlet2.4, JSP2.0 and beyond
- JavaServer Faces(予定)
- 虎の飼いならし方教えます - J2SE1.5 Tiger -
- JAIN テクノロジーとイベント駆動型コンテナ
魅力・・・Tiger 魅力.
ぼくの内定先も,Platinum Sponsor で名を連ねているのだけれど,お願いしたら招待券もらえないかな・・・w.無理かな・・・.
[ permalink ] [ 1 comment(s) ] -
000229
忘れないようにメモ書き.
java.net.URLConnection のタイムアウトを設定する方法.
Networking Properties
System.setProperty (
"sun.net.client.defaultConnectTimeout" ,
Integer.toString ( URLCONNECTION_TIMEOUT ) );
System.setProperty (
"sun.net.client.defaultReadTimeout" ,
Integer.toString( URLCONNECTION_TIMEOUT ) );
これをやらないとかなりの時間待ち続けます.せっかくマルチスレッドにしたのに,意味なし.最初は こんな記述 を見つけてげんなりしたのだけれど,一次情報源にあたると解がありました.ちょっと毛色は違うけれど,こういう問題もあるらしいので注意を.この場合は sun.net.client.defaultReadTimeout を設定することで getInputStream から抜けるまでの時間を制御できるようです.
[ permalink ] [ 0 comment(s) ] -
000209
呼制御まわりを,java で置き換えようという動きです.
この記事を読んだだけでは
"Java で SIP"
くらいの認識だったのですが,
本家に当たってみたところ,
もっと対象が広いようです.Jain Overview (Sun)
JAIN の紹介 (Sun)既存のキャリアが抱えている回線交換網が
これで置き換わるとはあまり思えませんが,
企業内の PBX のリプレースなどなら有効なのかな?
J2EE との連携というあたりも,
かなり興味をそそられます.SIP とか SIMPLE あたりなら,
ぼく個人のレベルでもなんとかいじれるレイヤだと思うので,
今後の動きに注目したいと思います.
[ permalink ] [ 4 comment(s) ] -
000170
今日の午後,
「丸山先生レクチャーシリーズ in 東京2003」の第二回,
「J2EE1.4とWebサービスの発展 - Enterprise Javaの射程」
に参加してきました.4つの話があったのですが,その中でも特に興味を引かれたのは,
J2SE1.5,コードネーム "TIGER" です.
"Ease of Development" をメインテーマに掲げ,
言語仕様にもかなり手を加えているようです.まずは,Generics.
これは,事前に型指定を行うことでキャストをなくし,
ランタイムに ClassCastException を投げるのではなく,
コンパイル時の型チェックで捉えられるようにするものです.[Sample]
Iterator<Integer> iter = list.iterator ();
while ( iter.hasNext () ) {
Integer i = iter.next ();
}
あとは,Autoboxing/Unboxing があったり,
メタデータという名前で
C# の custom attribute っぽいものがあったりと,
かなり C# に近い部分が増えた印象です.
その他,今日の話を聴いた中で気になった点を二つ.01. Generics の定義方法
Generics の指定はインスタンスに対して行う.
しかし,実際にそれが意味をもつのは
メソッド呼び出しのタイミング.
このあたりの対応関係は
きちんと定義できるのだろうか.02. Varargs と Overload の整合性
Varargs は,C言語の printf のように,
可変引数を取ることを可能にするもの.
しかし,これを不用意に使うと,
Overload とバッティングしてしまい,
意図しない動作が生じるのではないのか.以上二つ.
質問タイムがなかったので,
気になったまま持って帰ってきてしまいました.
調査し次第,報告します.[ permalink ] [ 3 comment(s) ]