Trouble 2: 標準APIの利用方法に関する問題
<Q2-1>Vectorクラスのcloneメソッドが、オブジェクトのコピーとはならず、参照のままとなってしまいます。
Cloneしたオブジェクトの内容を変更すると、オリジナルのオブジェクトの内容も変更されてしまいます。
| <発生環境> |
| OS |
Solaris2.5.1 |
| JDK |
JDK1.2.1 |
| Vender |
Sun |
- <A2-1>
- Vectorのcloneメソッドは、保持している要素オブジェクトのcloneの生成までは行わないので、cloneで生成されたVectorオブジェクトは、clone元のVectorと同じ要素オブジェクトを参照しています。
要素オブジェクトまでもコピーしたVectorを生成したい場合は、Vectorのcloneメソッドを使わず、要素オブジェクトをコピーして、新しいVectorオブジェクトに格納するプログラムを実装する必要があります。
これを不便だと思う人がいるかと思いますが、勝手にcloneを生成されてメモリを大量に使用する潜在バグの温床にもなりかねませんし、オブジェクトのコピーの深さ(シャローコピーなのかディープコピーなのか)をコントロール出来なくなるので、Vectorのcloneメソッドの振る舞いは正しいと考えます。
<Q2-2>Calenderクラスに、lenientを設定していても、異常な値をsetしても例外をthrowしません。
APIには例外をthrowするように書いてあるのですが、どうなのでしょうか?
| <発生環境> |
| OS |
Solaris2.5.1 |
| JDK |
JDK1.2.1 |
| Vender |
Sun |
- <A2-2>
- 異常な値をsetしても、直ちに例外がthrowされるのではなく、その後、何らかの操作を行った時点で例外がthrowされるため、set後に、例外補足処理を行ってください。
<Q2-3>ObjectOutputStream使用中にヒープ使用量が上がりつづけてしまいます。
| <発生環境> |
| OS |
Windows/Linux |
| JDK |
JDK1.3.1 |
| Vender |
Sun |
- <A2-3>
- ObjectOutputStream.writeObject()するとObjectOutputSream内に書き込まれたオブジェクトとそのオブジェクトが保持しているオブジェクトの参照がObjectOutputSream内にキャッシュとして保持されます。
この仕組みのため、自分のプログラムでは参照を開放したはずのオブジェクトがガベージコレクションの対象にならず、メモリを圧迫する場合があります。
ObjectOutputSreamを使用する場合はストリームを開いたままにせずに、適切なタイミングで閉じるか、ObjectOutputStream.reset()を呼び出してObjectOutputSreamが保持するオブジェクトの参照を開放する必要があります。
<Q2-4>Runtime.exec()でシェルスクリプトを繰り返し実行するとRuntime.exec()がブロックしたままになってしまいます。
| <発生環境> |
| OS |
RedHatLinux6.2J |
| JDK |
JDK1.3.1 |
| Vender |
Sun |
- <A2-4>
- 根本的な原因と対策方法は不明のまま。
発生パターンとして、小さなシェルスクリプトを連続して実行すると現象が発生しやすい傾向があります。
この発生パターンから、回避策として一回の処理を1つのシェルスクリプトにまとめてRuntime.exec()の呼び出し回数を減らしてみたところ、現象が発生しなくなりました。
ただしこの回避策は、根本的な原因を解決したものではないので、すべてのケースで有効である確証は取れていません。
<Q2-5>propertiesファイルに2バイト文字のプロパティを記述するとjava.util.Properties.load()が失敗してしまいます。またはプロパティが取得できません。
| <発生環境> |
| OS |
any |
| JDK |
JDK1.2以降 |
| Vender |
any |
- <A2-5>
- 基本的に、プロパティに2バイト文字を記述する場合はUTF8でなくてはなりません。ASCII文字に統一するかUTF8で記述してください。
ただし、Unicodeエスケープを利用しても記述可能です。その場合は、native2asciiを使用してネイティブ文字コードからUnicodeエスケープに変換してください。
<Q2-6>Javaからシェルスクリプトを起動することができません。
| <発生環境> |
| OS |
Solaris |
| JDK |
JDK1.3 |
| Vender |
Sun |
- <A2-6>
- 以下のように、シェルコマンドからシェルスクリプトを起動するようにすることで解決します。
String cmd[] = {"/bin/sh", "-exec", "hogehoge.sh"};
Process proc_ = Runtime.getRuntime().exec(cmd);
この方法でも、標準出力に対して出力を行うシェルスクリプトを起動するとうまくいかないようです。これはWindowsのバッチ起動でも同様の問題が起きています。
この問題を回避するため、標準出力に出力する代わりに、テンポラリファイルに出力するなどの工夫が必要です。
<Q2-7>ObjectStreamを用いて、ファイルのReadWriteを行う際に、Write時に、1オブジェクト毎に同一ファイルのオープン・クローズを行うと、Readする際に2オブジェクト目以降が読込めません。
オブジェクト単位のログ出力を、バイナリ形式で行おうとしたところ、問題が発生しました。
| <発生環境> |
| OS |
Solaris2.5.1 |
| JDK |
JDK1.2.1 |
| Vender |
Sun |
- <A2-7>
- ファイルをクローズすると、バイナリの終端情報がファイルに付加されます。
そのため、その後更にオープン・クローズを繰り返しても、終端情報以降を利用することができません。
1ファイルに連続して書き込むのであれば、ファイルをオープンし続けるか、テキスト形式への変更が必要です。
<Q2-8>配列から変換したListに要素を追加することができません。
Arrays.asList(Object[] a)で生成されたListに対して、add(Object a)をしようとしたところ、java.lang.UnsupportedOperationExceptionが発生します。
| <発生環境> |
| OS |
Any |
| JDK |
Any |
| Vender |
Any |
- <A2-8>
- java.util.Arrays#asList(Object[] a)で生成されるインスタンスは、java.util.Arraysのソースコードでprivateなクラスとして定義されているArrayListです。 このArrayListクラスはメソッドadd(Object a)を実装していないため要素を追加できません。
<Q2-9>ビットシフトを行ったところ、予期せぬ数値が返ってきます。
2つのbyte型(8bit)変数をそれぞれ上位バイト、下位バイトと見なします。
上位バイト(high=0x07) を8ビット左シフトし、下位バイト(low=0xb1) と足して 16進数表現を得ようとしました。
Integer.toHexString( (high << 8) + (low) );
0x07b1 という結果を期待したのですが、0x06b1 が返されました。
| <発生環境> |
| OS |
Any |
| JDK |
Any |
| Vender |
Any |
- <A2-9>
- この現象は シフト対象の値が負の場合のみ発生します。
負の値に正しくマスクをかけてから、計算する必要があります。
Javaのシフト演算は、オペランドが int 型より小さい場合(つまり、shortや byte 型の場合)、自動的に int 型に拡張されます。
この際の符号の取り扱いが原因。拡張の際は符号付(signed)の値として見なされるため、符号付表現で負の値は、intに拡張したときに拡張されたビッ トが全て 1 になってしまいます。
この例では、0x07 と 0xb1 は内部的には以下のように扱われます。
0x07 = 0000 0000 0000 0000 0000 0000 0000 0111
0xb1 = 1111 1111 1111 1111 1111 1111 1011 0001
よって、下位8ビットを抽出するマスクをかける必要があります。
Integer.toHexString( (high << 8) + (low & 0xff) );
<Q2-10>URLConnectionを利用してWebにアクセスしようとしたのですが、例外が発生し、InputStreamを取得することが出来ませんでした。
以下の例外が発生ました。
java.net.ConnectException: Connection refused: connect
at java.net.PlainSocketImpl.socketConnect(Native Method)
(略)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream
(HttpURLConnection.java:574)
| <発生環境> |
| OS |
Any |
| JDK |
Any |
| Vender |
Any |
- <A2-10>
- URLConnectionが使用するProxyの設定を行っていない可能性があります。
Proxyを利用してWebにアクセスする場合は、予め以下のシステムプロパティを設定してください。
(ソースで設定する方法)
設定は、System#setProperty(String, String)を使用して行います。設定値はネットワーク管理者にお問い合わせください。
http.proxyHost … HTTP-Proxyサーバのホスト名
http.proxyPort … HTTP-Proxyサーバのポート番号
(コマンド引数として設定する方法)
java -Dhttp.proxyHost=hostname -Dhttp.proxyPort=portnumber ・・・・
なお、FTPまたはHTTPSでProxyサーバを利用する場合は、別途プロパティを設定する必要があります。
詳細はJava(TM) 2 SDK, Standard Edition ドキュメント:ネットワークのプロパティを参照してください。
<Q2-11>DateFormat.getDateInstance()で取得したインスタンスで日付文字列をパースしたところ、ParseExceptionが発生します。
しかし、地域(またはLANG)の設定によっては発生しないこともあります。
| <発生環境> |
| OS |
Any |
| JDK |
Any |
| Vender |
Any |
- <A2-11>
- ロケールの設定が異なると、日付文字列のパースに失敗する可能性があります。
状況によって、以下のようにする必要があると考えられます。
(特定のロケールに依存した文字列をパースする場合)
DateFormat.getDateInstance(int, Locale) など、Localeを引数に持つメソッドを使用してロケールを明示的に指定する。
(特定のフォーマットに従った文字列をパースする場合)
DateFormatのサブクラスであるSimpleDateFormatクラスに対し、フォーマット文字列を明示的に指定して使用する。ミリ秒などを含む文字列をパースする場合などが該当します。
Localeクラス、SimpleDateFormatクラスの詳細はAPIリファレンスを参照してください。
(フォーマット文字列の詳細はSimpleDateFormatクラスの説明として記述されています)
<Q2-12>Listのclear()を呼んでも空きヒープ領域が増えません。
空きヒープ領域を増やすために不要なListをclear()メソッドで開放しても、すぐには空きヒープ領域が増えません。
| <発生環境> |
| OS |
Any |
| JDK |
Any |
| Vender |
Any |
- <A2-12>
- Listのclear()メソッドはListが持っている参照を削除するだけであり、参照先のオブジェクトを破棄するわけではありません。そのため空きヒープ領域はすぐには増えません。
ガベージコレクトの実行により、参照先オブジェクトが使用していたヒープ領域が開放されます。
以下のサンプルを参照して下さい。
import java.util.*;
public class ListSample
{
public static void main(String[] args)
{
//プログラム開始時の空きヒープ領域を調べる。
Runtime rt = Runtime.getRuntime();
System.out.println( "\n<空きヒープ領域>\n");
System.out.println( "プログラム開始時\t: " + rt.freeMemory() );
//リストに格納するオブジェクトを用意する。
String[] bigObj = new String[100000];
for(int i=0; i<bigObj.length; i++)
{
bigObj[i] = "Now making a BIG object!!";
}
//オブジェクト作成後の空きヒープ領域を調べる。
System.out.println( "オブジェクト作成後\t: " + rt.freeMemory() );
//リストにオブジェクトを格納する。
ArrayList testList = new ArrayList();
for(int i=0; i<bigObj.length; i++)
{
testList.add( bigObj[i] );
}
//リスト作成後の空きヒープ領域を調べる。
System.out.println( "リスト作成後\t\t: " + rt.freeMemory() );
//リストからオブジェクトを削除する(ここでは参照が削除されるのみ)。
testList.clear();
//clear()呼び出し後の空きヒープ領域を調べる。
System.out.println( "clear()呼び出し後\t: " + rt.freeMemory() );
//ガベージコレクトを実行する。
System.gc();
//gc()呼び出し後の空きヒープ領域を調べる。
System.out.println( "gc()呼び出し後\t\t: " + rt.freeMemory() );
}
}
以下に実行結果を示します。
>java ListSample
<空きヒープ領域>
プログラム開始時 : 1757600
オブジェクト作成後 : 1357176
リスト作成後 : 594456
clear()呼び出し後 : 594136
gc()呼び出し後 : 1316264
clear()メソッドではヒープ領域の状態はほとんど変わらず、ガベージコレクトの実行後、空きヒープ領域が増えたことが分かります。
ただしgc()メソッドはJavaVMによっては実行されないことがあるので注意が必要です。

<Q2-13>FileクラスのgetAbsolutePath()メソッドを実行しても、絶対パスが返って来ません。
カレントディレクトリを
C:\eclipse\workspace\JTS_Test
として、以下の親ディレクトリを出力するコードを実行しました。
File file = new File("..\\");
String absolutePath = file.getAbsolutePath();
System.out.println(absolutePath);
親ディレクトリとして
C:\eclipse\workspace
が出力されることを期待したのですが、
C:\eclipse\workspace\JTS_Test\..
と表示されます。
| <発生環境> |
| OS |
Any |
| JDK |
Any |
| Vender |
Any |
- <A2-13>
- "絶対パス"という言葉の意味の食い違いによると考えられます。
getAbsolutePath()メソッドで返って来るのは、
・UNIX :"/"で始まるパス
・Windows:ドライブ指示子で始まり、その後に"\\" が続くパス、または"\\"から始まるパス
となります。"."や".."などの変換は行いません。
"."や".."などの変換を含んだ一般的な意味での絶対パスを取得するには、getCanonicalPath()メソッドを
使用します。
<Q2-14>java.util.Timerクラスに登録したTimerTaskの1つを、Thread.sleep()メソッドで停止させたところ、他のTimerTaskが実行されなくなってしまいました。
| <発生環境> |
| OS |
Any |
| JDK |
JDK1.3.0 |
| Vender |
Any |
- <A2-14>
- Timerクラスが、TimerTaskを順に処理しているためです。
Timerクラスは、登録されたTimerTaskを連続して実行するための唯一のスレッドを持ちます。
よって、現在実行中のTimerTaskの処理が終わらない限り、次のTimerTaskが実行されることはありません。
このため1つのTimerTaskがスリープしている間は、その後に実行される予定のTimerTaskは永遠に待たされることになります。
よって、TimerTaskでは停止もしくは長い処理を行うべきではありません。
もし必要な場合は、TimerTaskから別のスレッドを起動してその中で実行する様にすることで問題を回避できます。
<Q2-15>java.util.TimerTaskをcancel()していないのにIllegalStateException("Timer already cancelled.")が発生してしまいます。
TimerTaskを定期的に登録するようなシステムで、あるとき以下の例外が発生しました。
IllegalStateException : Timer already cancelled.
(以下、java.util.Timer#schedule() で終わるスタックトレース)
java.util.Timer#cancel()を呼び出した後にjava.util.Timer#schedule()を呼び出すとこの例外が発生するらしいのですが、そもそもcancel()を呼び出すコードは実装していません。
| <発生環境> |
| OS |
Any |
| JDK |
1.4.2 |
| Vender |
Any |
- <A2-15>
- java.util.TimerTask#run()内でキャッチされない例外が発生している可能性があります。
TimerはTimerTaskを実行するためにTimerThreadというスレッドを持っています。
このTimerThreadが登録されたTimerTaskを実行するのですが、TimerTask#run()内でキャッチされない例外が発生すると、TimerThreadが例外をキャッチし、Timerに対してcancel()処理を行ないます。この結果、全てのTimerTaskがキャンセルされます。
よって覚えのないIllegalStateExceptionが発生する場合、TimerTask#run()内で例外が発生している可能性が考えられます。
対処方法としては、以下の方法が考えられます。
1.TimerTaskのrun()メソッドを、例外が発生しないようなコードに書き換える。
2.TimerTaskのrun()メソッド内の例外を正しくキャッチ/処理する。
3.schedule()を呼び出す箇所でIllegalStateExceptionをキャッチし、Timerを再生成するようにする。
<Q2-16>Runtime#exec(String) で IOException が発生して、外部コマンドが実行できません。
| <発生環境> |
| OS |
Solaris、Linux |
| JDK |
1.4.1、1.4.2 |
| Vender |
Sun |
- <A2-16>
- コマンドを指定するパスに空白文字が含まれていませんか?
Runtime#exec(String)では空白文字は引数との区切り文字に解釈されてしまいます。
代わりに Runtime#exec(String[]) を使って下さい。
以下は"/my bin"というディレクトリの"touch"コマンドを引数"test.txt"で実行した例です。
失敗例:
String command = "/my bin/touch test.txt";
Runtime.getRuntime().exec(command);
(実行すると、「java.io.IOException: /my: not found」が発生します)
成功例:
String[] command = {"/my bin/touch", "test.txt"};
Runtime.getRuntime().exec(command);
<Q2-17>Runtime#exec()を使用すると、OutOfMemoryErrorが発生することがあります。起動しようとしている外部プログラムはごく小さなもので、それに対して十分な空きメモリはあります。
Runtime#exec()を使用すると、OutOfMemoryErrorが発生することがあります。起動しようとしている外部プログラムはごく小さなもので、それに対して十分な空きメモリはあります。
| <発生環境> |
| OS |
Solaris |
| JDK |
1.4.1 |
| Vender |
Sun |
- <A2-17>
- 起動元JavaVMが“太っている”可能性があります。
JavaVMのUNIX実装(Solaris、Linux)では外部プロセスの起動にfork()、exec()システムコールを使用します(※)。従って大量のメモリを使用しているなど、起動元プロセスが太った状態にあると、外部プログラムのサイズによらずforkに失敗することがあります。
ps、prstatコマンドなどで起動元プロセスのメモリサイズを確認してください。
起動元プロセスのメモリ大量消費が確認できた場合、
(1)swap領域を増やす
(2)物理メモリを増設する
(3)起動元プロセスをシェイプアップする
などの対処を行なう必要があります。
※実際にはfork1()を使用するため、単純にプロセス全体のコピーが作成されるわけではありません。
<Q2-18>java.nioを使用していると、ネットワーク切断によってCPU使用率が100%になることがあります。
java.nioを使用して、ネットワークを使用するシステムを構築しています。
そこで、CPU使用率が100%になるという問題が発生しました。
調査した結果、ネットワーク切断と相関関係があるようです。
どのような原因が考えられますか?
| <発生環境> |
| OS |
Windows XP SP2 |
| JDK |
1.4.1_07 |
| Vender |
Sun |
- <A2-18>
- Javaのバグである可能性があります。
システム内に、Selector#select()を使用した無限ループ構造がありませんか?
JDK1.4.1_04以前のバージョンでは、
ネットワーク切断後に数十秒経過すると、選択できるキーがないにもかかわらず、Selector#select()
がブロックされずそのままreturnしてしまう状態になる、という不具合があります。
その結果、ループ中のどこでもブロックされず、処理が繰り返されることになり、
CPU使用率が100%になってしまいます。
根本的な解決策は、このバグが解決したバージョンである、JDK1.4.2_05以降
を使用することです(Solaris、Linuxでは1.4.2_06で解決されています)。
運用上の問題などでバージョンを上げることができない場合、Selector#select()を呼ぶ前
にThread.sleep()を実行する等して100ms程度待つことで、問題の発生を回避することができます。
<Q2-19>pingは通るのに、java.net.InetAddressのisReachable()がfalseになります。
あるホストに対して、java.net.InetAddressのisReachable()がfalseを返しました。
しかし、同じホストに対してのpingは通ります。
java.net.InetAddressのisReachable()は、pingとして使用できると考えていたのですが、どうなっているのでしょうか?
| <発生環境> |
| OS |
Windows XP SP2 |
| JDK |
5.0 Update4 |
| Vender |
Sun |
- <A2-19>
- java.net.InetAddressのisReachable()は、pingそのものではありません。
APIリファレンスによると、一般的な実装では以下のような方法で到達可能かどうかを判定します。
(1)権限が取得できる場合はICMP ECHO REQUEST(ping)を使用
(2)そうでない場合、目的のホストのポート7(Echo)でTCP接続の確立を試行
しかし、Windowsでは常に(2)の方法で判定を行う実装になっています。
従って、Windowsでpingが成功するのにisReachable()がfalseになる場合、ポート7(Echo)への接続に反応がない可能性があります。
その場合、Echoの失敗の原因を取り除く(ファイアーウォールの7番ポートの接続を許可する等)ことにより、解決されます。
※参考
Solaris及びlinuxでは、APIリファレンスの通りに実装されているため、
権限を取得することができない可能性も考えられます。
対処として、root権限で実行しICMPが選択される様にすることが考えられます。
<Q2-20>MappedByteBufferを使用していると、メモリが足りなくなります。
ファイルへの高速アクセスのために、java.nio.MappedByteBufferを使用してアプリケーションを作成しています。
必要な時にMappedByteBufferを初期化し、不要になった時点でcloseしていますが、実行中に以下の例外が発生してしまいます。
java.io.IOException: このコマンドを実行するのに十分な記憶域がありません。
at sun.nio.ch.FileChannelImpl.map0(Native Method)
at sun.nio.ch.FileChannelImpl.map(FileChannelImpl.java:742)
at mappedbytebuffer.TestMemoryMapping.main(TestMemoryMapping.java:22)
どんな原因が考えられますか?
| <発生環境> |
| OS |
Windows XP SP2 |
| JDK |
5.0 Update4 |
| Vender |
Sun |
- <A2-20>
- MappedByteBufferを使用すると、JavaVMプロセスのアドレス空間中に連続したメモリ領域を確保します。
このメモリの確保に失敗すると、Qの例外が発生します。
MappedByteBufferは、ガベージコレクトされるまで確保したメモリ領域を解放しない仕様になっています。
つまり、仮にバッファをcloseしたとしても、その時点ではメモリ領域の解放は行われません。
ガベージコレクトが実行されればメモリ領域は解放されますが、MappedByteBufferの使用するメモリ領域は通常のJavaオブジェクトとは別の所に確保されるので、ガベージコレクトの契機にはならない場合があります。
その結果、Javaオブジェクトを保存するメモリ領域(heap)は十分でも、MappedByteBufferの使用するメモリ領域が不十分になり、Qの例外が発生することがあります。
MappedByteBufferを使用するのは、システム終了までリソースを解放しなくても良い場合に限るべきです。
例えば、システムで使用する大量のデータを、リングバッファとして保持する場合に使用するなどはMappedByteBufferの好例でしょう。
<Q2-21>Stringのmatches()メソッドを呼び出すとハング(hang)してしまったようで、反応が返ってこなくなってしまいました。
以下のような単純な処理です。
string.matches("(.*\n*)*")
- <A2-21>
- String#matchesメソッドに改行(\n)を利用するとハングしてしまう(処理が進まなくなる)のは、Java Bug Databaseにも載っている、Javaのバグです。
参考URL:
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6328855
回避策としては、改行(\n)をそのまま利用せずに、復帰改行(\r\n)を使えば良いようです。
例)
string.matches("(.*\r*\n*)*"));
他の回避策はない模様です。
また、Java Bug Databaseに修正の報告はあがっていませんので、改行(\n)を使う必要がある場合は、バージョンアップを待つしかないようです。
(2007/2/28現在)
<Q2-22>System.getPropertyが再帰的に呼び出され、StackOverFlowになります。
以下のように、Propertyを繰り返し生成し、getPropertyで存在しないキーを指定すると、StackOverFlowが発生する可能性があります。
for(int num=0; num > 10000&; num++)
{
Properties p = new Properties(System.getProperties()); ・・・①
Properties System.setProperties(p); ・・・②
}
System.getProperty("test");
- <A2-22>
- 上記の①、②のコードを1回実行するたびに、getPropertyで再帰的に呼ばれる回数が
1回増えます。
そのため、①、②を繰り返し実行し、getPropertyで存在しないキーを指定して、
getPropertyを呼び出すと、プロパティの呼び出しが多数行われる状態になってしまいます。
これにより、CPU使用率も高くなってしまい、最終的にはStackOverFlowします。
参考URL:
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4906193
回避策としては、①、②を以下のように書き換えます。
new Properties()せず、直接操作するようにすることで、
この問題の発生を防ぐことができます。
Properties p = System.getProperties();
System.setProperties(p);
参考)
以下は、問題発生時のスタックトレースの出力例です。
Exception in thread "main" java.lang.StackOverflowError
at java.util.Hashtable.get(Hashtable.java:334)
at java.util.Properties.getProperty(Properties.java:932)
at java.util.Properties.getProperty(Properties.java:934)
at java.util.Properties.getProperty(Properties.java:934)
at java.util.Properties.getProperty(Properties.java:934)
at java.util.Properties.getProperty(Properties.java:934)
at java.util.Properties.getProperty(Properties.java:934)
at java.util.Properties.getProperty(Properties.java:934)
at java.util.Properties.getProperty(Properties.java:934)
==== 以下省略 ====

<Q2-23>HashSetで取得した値を変更すると、HashSetで値が正しく取得できなくなります。
HashSetで取得した値を以下のように変更すると、HashSetが正しく動作しなくなります。
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
class Foo {
public int id;
public Foo() {}
public int hashCode() {
return id;
}
public boolean equals(Object obj) {
if (obj instanceof Foo) {
return (this.id == ((Foo) obj).id);
}
return false;
}
}
class HashSetError {
public static void main(String[] args) {
Foo foo = new Foo();
foo.id = 1;
// HashSetにデータを入れる
Set<Foo> barList = new HashSet<Foo>();
barList.add(foo);
// trueと表示
System.out.println("foo contains? " + barList.contains(foo));
Iterator<Foo> it = barList.iterator();
while (it.hasNext()) {
Foo tmp = it.next();
tmp.id = 2;
}
// falseと表示
System.out.println("foo contains? " + barList.contains(foo));
}
}
※JDK1.5.0で動作確認を行いました。
- <A2-23>
- これはHashSetが各要素と紐付けているハッシュ値と、
要素のhashCode()を呼び出したときのハッシュ値が異なるために発生する問題です。
HashSetは要素を管理するために、要素と同時にそれに対応するハッシュ値を保存しています。
各要素のハッシュ値は、HashSet#add()を呼ばれた際にhashCode()を呼び出した結果となります。
上の例ではHashSetに追加されたfooはハッシュ値1とともに保存されます。
しかしHashSetが管理している要素に対してハッシュ値に影響を与える操作を行うとしても
HashSetはその操作によって要素のハッシュ値を変更することはありません。
つまり要素の値を変更すると、要素のハッシュ値とHashSetが管理するハッシュ値が
不一致になるのです。
上の例ではHashSetに追加されたfooは 「tmp.id=2;」 によってハッシュ値が2になりますが、
HashSetが登録されているハッシュ値は1のままです。
このため、ハッシュ値が変更になった要素をHashSetで検索すると、
HashSetはハッシュ値で検索を行うために登録されていないと判断されてしまいます。
これを解決するには、値を変更した要素を新しいHashSetに対してaddする必要があります。
また本来、HashSetに格納した内容を変更することは避けるべきです。
java.util.SetのJavaDocにも、以下の通りの記述があります。
注: 可変オブジェクトがセット要素として使用される場合は、
細心の注意が必要です。
オブジェクトがセット内の要素であるうちに equals 比較に影響する
方式でその値が変更された場合、セットの動作は保証されません。
この禁止事項の特例により、セットがそれ自体を要素として
持つことは許可されません。
参考URL:
http://java.sun.com/j2se/1.5.0/ja/docs/ja/api/java/util/Set.html

<Q2-24>getStackTrace()を頻繁に実行するとOutOfMemoryErrorが発生します。
正常動作していたアプリケーションを処理解析用に修正し、
定期的にスタックトレースを表示するようにしました。
その結果、長時間運用中にOutOfMemoryErrorが発生して、
アプリケーションが強制終了してしまいました。
表示されるエラーメッセージは以下の通りです。
Exception java.lang.OutOfMemoryError: requested 128 bytes for GrET* in
/BUILD_AREA/jdk1.5.0_14/hotspot/src/share/vm/utilities/growableArray.
cpp. Out of swap space?
| <発生環境> |
| OS |
Any |
| JDK |
JDK 5.0 |
- <A2-24>
- この現象は、Java標準APIのjava.lang.Thread.getStackTrace()を
頻繁に呼び出した際に発生します。
JDK5.0のjava.lang.Thread.getStackTrace()の実装には、
C言語のソースコードレベルにおいてメモリ解放忘れのバグが存在します。
このバグが原因となり、java.lang.Thread.getStackTrace()を呼び出すたびに、
Cヒープと呼ばれるメモリ領域でメモリリークが発生します。
同様のメモリリークは、java.lang.Thread.getStackTrace()と同じC言語
のソースを利用している以下のJava APIでも発生します。
- java.lang.Thread.getAllStackTraces()
- java.lang.management.ThreadMXBean.getThreadInfo()
この件については以下のJavaのbugdatabaseで報告されています。
参考URL:
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6469701
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6434648
この問題はJDK6.0では修正されています。
そのためJDK5.0を利用したアプリケーションの場合には、
JDK6.0へ以降するか、以下の3つのメソッドの利用を避ける必要があります。
- java.lang.Thread.getStackTrace()
- java.lang.Thread.getAllStackTraces()
- java.lang.management.ThreadMXBean.getThreadInfo()
このうち1のjava.lang.Thread.getStackTrace()については、
java.lang.Throwable.getStackTrace()を使うことで回避することができます。
しかし2, 3についてはJDK5.0で現在回避する方法はありません。
JDK6.0への移行をお勧めします。
この記事の詳細な情報をJTSメールマガジンにて配信しています。
情報を取得したい方は、メールマガジンのバックナンバーを参照して下さい。

<Q2-26>オブジェクトの重複チェックに時間がかかってしまいます。
処理が非常に遅くなるときがあります。
原因を調査してみたところ、containsメソッドを利用したオブジェクトの重複チェックに
時間がかかっているようです。
| <発生環境> |
| OS |
Any |
| JDK |
JDK5.0以降 |
| Vender |
Sun |
- <A2-26>
- 線形検索を行っている可能性が高いです。
Listのようなオブジェクトとは、containsメソッドで重複チェックを行うときに、
格納された要素を先頭から比較します。
そのため、格納している要素の数に応じて、時間がかかってしまってしまいます。
対策:
HashSetを利用して、重複チェックすることをお勧めします。
ただし、HashSetを利用する場合には、検索対象となるオブジェクトのhashCodeメソッドと、
equalsメソッドを実装する必要があるので、注意しましょう。
参考:
1万件のListとSetに対して、以下のような方法で1万回の検索を行ったところ、
それぞれ以下のような結果になりました。
List Search:828(ミリ秒)
Set Search :83(ミリ秒)
List list = search.createListData();
Set set = search.createSetData();
long start = System.currentTimeMillis();
search(list);
long end = System.currentTimeMillis();
System.out.println("List Search:"+ (end-start) + "(ミリ秒)");
start = System.currentTimeMillis();
search(set);
end = System.currentTimeMillis();
System.out.println("Set Search :"+ (end-start) + "(ミリ秒)");
/** Listデータの作成 */
public List createListData(){
List list = new ArrayList();
for (int num = 0; num < DATA_MAX; num++){
list.add(String.valueOf(num));
}
return list;
}
/** Setデータの作成 */
public Set createSetData(){
Set set = new HashSet();
for (int num = 0; num < DATA_MAX; num++){
set.add(String.valueOf(num));
}
return set;
}
/** containsによる重複チェック */
public void search(Collection col){
Random random = new Random();
for (int num = 0; num < REPEAT_TIME; num++){
int searchData = random.nextInt(DATA_MAX);
col.contains(String.valueOf(searchData));
}
}

注意:本文書の内容に誤りがあり、またこの文書によって不利益を被っても、
Acroquest Technology 株式会社は一切関知いたしません。