2006/10/31

[Java]serialize and nested-class mix like oil and water

java.io.Serializableをimplementsする場合、serialVersionUIDの宣言を義務付けた方がいい。

serialVersionUIDは、JDKのソースなんかを見るとよく宣言されているフィールド。 private static final long serialVersionUID = 123456789L; どこからも使われていないのでパッと見は不要な宣言なんだけど、これはJavaのserializeの機構が、互換性の検証のために利用している。(ベリファイアも絡んでるのかな?)
serialVersionUIDを宣言していないときは、実行時に自動的に計算される。

Javaのオブジェクトをデシリアライズするときは、復元しようとしているオブジェクトと、システム内に存在しているクラスの間に互換性があるかどうかを調べる必要がある。クラスの構成からハッシュ値を計算して、その値が等しいかどうかで、復元しようとしたオブジェクトと、システム内にあるclassに互換性があるかどうかを判定している。

何で自動計算任せだと問題があるかというと、ポイントは以下の2点。

  • ハッシュ値は、クラス名・インターフェース名・フィールド・メソッド から計算される
  • ネストしたクラスを使用していると、コンパイラによって値が変わることがある

メソッド名などで計算されるので、後でメソッドを追加したりすると以前に出力したバイナリが使用できなくなったりする。DBにインスタンスを保存しているような場合には致命的だ。テストの最終段階や、下手すると本番環境で突然落ちることになってしまう。

ネストしたクラスの方の現象は、privateなフィールドやメソッドの参照で起きる。曲者なのが、プライベートメンバへのアクセス時にコンパイラが生成するメソッド名が、コンパイラの裁量に任されている点。(ネストしたクラスがコンパイラとVMでどうやって実現されているのかは、ネストしたクラスを参照。)ビルドする環境が違うと、メソッド名(と、参照を保持するためのフィールド名)が変わる。メソッド名が変わると、ハッシュ値が変わってしまう。

最初は「必要になってからで良いかな」と思った。JDKについてる仕様を見ると、serialVersionUIDを宣言するのは次版からでもいいことになっているし。
開発用のマシンからテスト用のサーバにあるEJBを呼び出そうとすると、「クラスに互換性がない」なんていわれて、困ったことがある。調べてみると、DTOに内部クラスを使った構成が含まれていて、開発環境のEclipseとサーバで自動ビルドされたクラスで相違が出てしまったらしい。
#「んなことすんなよ」といわれてしまうとその通りなんだけど。

ビルドする人間が別のマシンに配置するファイルを別個にビルドしてしまうかもしれない。
バージョンアップ一つを見ても、本番環境にあるclassファイルがないとserialVersionUIDに書くべき値が調べられない。しかも、対象になったクラスをロードするのに必要なCLASSPATHなんかも分かっていないと、serialverコマンドは使えない。

この手間とリスクを考えると、serialVersionUIDの宣言を必須にし、宣言の仕方を規約にでも書いておいた方がいい

ラベル:

2006/10/29

[Java]eclipse warn serialVersionUID isn't declare

Eclipseで開発しているとき、SerializableをimplementsしているのにserialVersionUIDを宣言していないと警告が表示されるようになった。
The serializable class Foo does not declare a static final serialVersionUID field of type long
#日本語版のメッセージは確認してない

この警告は、javacで-Xlintオプションを付けると出力されるものだ。 この警告に対処するには、言われたとおりserialVersionUIDを宣言しよう。JDKについているserialverコマンドを使うのが一番オーソドックスな対応法だ。

E:\hoge>serialver -classpath . Serial Serial: static final long serialVersionUID = -563646877436713083L;

出力されたSerial:より後ろを、ソースに貼り付ければ完了。
Eclpseを使っているのなら、Ctrl+1の後でadd generated serial version IDを選択すると、もっと簡単に宣言できる。(add default serial version ID1Lを設定するだけ。動くけど。)
親クラスがSerializeを実装しているけど、自前クラスはシリアライズできないように対処しているのなら、アノテーション@SuppressWarning("serial")でコンパイラの警告を出さないようにできる。(アノテーションは5.0以降だけ)

Eclipseで既存のソースを眺めているだけだったりするなら、コンパイラ設定の警告/エラーから無視するように設定することができる。これは、メニューのWindowからワークスペースのデフォルトとしても設定できるし、プロジェクトごとに個別に設定することもできる。

serialize関連のメモを書いて気づいたんだけど。Googleで検索して出てくる警告への対処方法に、すごいものがぞろぞろ…

  • 1Lを設定してください
  • Eclipseの設定を変えるといい
  • SuppressWarningを書いてください

おいおい、せっかく警告してくれてるのに、黙らせちゃうのかよ…。どこでインスタンスを参照されているか分かったもんじゃないのに。
1Lを宣言するのは意味が分からない。Eclipseで始めてこのメッセージを見たのなら、どうせEclipseを使ってるんだろう。同じ手間をかけるなら、Ctrl+1からちゃんとした値を生成した方が良いじゃないか。 SuppressWarningのアノテーションは、「ちゃんと別の方法で対処してるよ」とコンパイラに知らせるための手段であって、コンパイラを黙らせるための方法じゃない。根本的な対策をとって(この場合、writeObject()でException投げるなりしてシリアライズできないようにして)始めて使っていい方法だ。
Eclipseの設定に到っては、設定の箇所に「潜在的な問題」とまで書いてある。要するに「動いちゃいるけどバグだ」ってことだ。この辺の設定やserialVersionUIDを知らないのは、基本的に初心者だろう。何の注意書きもなく教えていい設定じゃない。さらに言えば、javacで-Xlintを使ってコンパイルしたら出力されるんだから解決になっていない。

初心者なら、serialVersionUIDが何者かを理解できなくても、serialverやCtrl+1で設定する方法を教えれば素直に従うだろう。
個人の趣味でプログラミングしているならともかく、人に「コンパイラを黙らせる方法」として教える人は何を考えているんだろう。

ラベル:

2006/10/28

[Java]nested class, private menber

Javaでは、ネストしたクラスを使用すると、囲んだクラスとの間でprivateなメンバ(フィールドやメソッド)を参照できることになっている。

こんなコードがあったとする。ソースコードを見る限りフィールドにアクセスしているように見えるが、実際にコンパイルした後は違う形に書き換えられている。

public class Declare {     private int foo;     public class Inner {         public int getFoo() {             return foo;         }     } }

以下が、コンパイル後の構成を擬似的にコードに置き換えた内容。

public class Declare {     private int foo;     int fooAccess() {         return foo;     } } public class Declare$Inner { private Declare declare;     public int getFoo() {         return declare.fooAccess();     } }

InnerクラスはDeclare$Innerというファイル名(匿名クラスであればDeclare$1のような名前にされる)で、完全に独立したクラスとして生成され、囲んだクラスのインスタンスを参照している。
こうなると、Innerからはプライベートメンバーにアクセスすることができなくなるので、DeclareクラスにはfooAccessというメソッドが勝手に追加されている。今回は分かりやすいようにfooAccessという名前のメソッドにしているが、実際にはコンパイラが好きなように付ける。ユーザが作成するメソッド名とかぶらないようにすることは義務付けられている。

ラベル:

2006/10/26

[Java]serialize

serializeはJavaでは直列化と訳されている。簡単にいえば、オブジェクトをバイト列に変換する仕組み。逆の操作はデシリアライズになる。バイト列に変換すれば、ファイルに保存したり、DBに格納したり、他のシステムと通信したりできる。

Javaでは、java.io.Serializableをimplementsするだけで、そのオブジェクトはシリアライズ可能になる。(java.io.Externalizableでもいいんだけど、使う機会は少ない)
java.io.ObjectOutputStream#writeObject(Object)に渡して書き出し、java.io.ObjectInputStream#readObject()で読み込む。

具体的な使い方は検索すればいくらでも出てくるので、いくつか注意点。

  • 出力対象のオブジェクト内に格納されたオブジェクトも芋づる式にシリアライズされる。フィールドに設定されたStringとかも一緒に出力されるんだから分かると思うけど。もちろん、自分で作成したクラスを保持していている場合も同じで、全部出力される。
  • 内部に格納されたオブジェクトも、implements Serializableである必要がある。StringやIntegerなど、Javaの標準ライブラリのほとんどがSerializableを実装している。自前のクラスも忘れず実装しておく。
  • implements Serializableしたら、serialiVersionUIDを宣言する。コンパイラの変更やクラスのバージョンアップでデシリアライズできなくなったりすることがある。
  • シリアライズは、オブジェクトの生成を制限するパターンとの相性が悪い。SingletonパターンやTypeSafeEnumパターンでは要注意
  • finalize()メソッドの実装も要注意。副作用があるfinalizeを実装するようなクラスをSerializableにする機会もそんなにないと思うけど。

細かいそれぞれの細かい内容は後で書いとく。

ラベル:

[Java]String.intern() を実験

String.intern()について、ちょっと実験をしてみた。
定数(つまり文字リテラル)として作成されたStringと、String(char[])で後から作成したStringを用意し、定数の内容をリフレクションで書き換えてみた。
static final String STR = "aaa"; String str = new String(new char[]{'a', 'a', 'a'}); 定数書き換え後に後から作成した文字列の方をinternすると、自分自身が返ってきた。

結果
  • 書き換え前の定数値は、VMのプールから削除されているらしい
  • 後から作成した文字列strをinternしても、定数のインスタンスと混同されたりしない
  • STRの値を"aaa"から"aab"に書き換えたとすると、STR.intern()は、他の場所にあった"aab"のインスタンスを返してくる

やっぱりVMはちゃんと考えて実装してある。intern()の処理が遅いのもちょっと納得。

ラベル:

2006/10/25

[Java]String.intern()

JavaのStringクラスに、intern()というメソッドがある。
APIドキュメントの説明を要約すると、StringのインスタンスはVMが管理していて、str.intern()とすることにより、内容が同じ文字列があった場合にはそのインスタンスを返してくれるらしい。
これがどんなところに使われているかというと、クラス内の文字リテラル("foo"のように記述している箇所)で使用され、文字リテラルのインスタンスはVM上で同一になるようになっている。

同一インスタンスになるとうれしいことは、メモリの節約になることと、String.equals()が高速になること。例として挙げられそうなのは、読み込んだプロパティファイルをSingletonで保持している場合など、そのインスタンスが頻繁に比較される箇所。
設定ファイルを読み込んだ後にキーと値にString.intern()をかけつつHashMapにコピーしておけば、設定値名の定数などと同じインスタンスになってくれる可能性がある。

サンプルコード。
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

public class Config {
    private static final Map<String, String> PROPERTIES;
    static {
        Properties prop = new Properties();
        InputStream input = Config.class.getResourceAsStream("foo");
        try {
            if (input != null) {
                prop.load(input);
                input.close();
            }
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }

        Map<String, String> copy = new HashMap<String, String>(prop.size());
        for (Object key : prop.keySet()) {
            String asStr = (String) key;
            copy.put(asStr.intern(), prop.getProperty(asStr));
        }
        PROPERTIES = Collections.unmodifiableMap(copy);
    }

    public static String get(String key) {

        return PROPERTIES.get(key);
    }
}

パフォーマンス上のポイントは以下の三点。エラー処理は適当。

  1. キーと値をintern
  2. PropertiesをHashMapにコピー(不要なsynchronizedをやめるため)
  3. 変更する予定がないので、変更不可のMapでラップ

定数をキーとして指定することが多い場面では、処理速度がだいぶ違う。
注意が必要なのは、intern自身はあまり早い処理ではないので、頻繁に作成されるStringのインスタンスに対して逐次internしていると、逆に遅くなること。そして、上記の定数としての文字列がinternより前に読み込まれていないと、その定数には効果がないことだ。

ラベル:

[DOS]expansion

DOSの変数展開の話。

DOSの変数展開は、他の多くのシェルと同様にいくつかに分かれる。
通常の変数、引数変数、forで宣言した変数、setlocal enabledelayedexpansionで展開され、最終的に他のコマンドやバッチファイル およびcallによる関数等として使用される。

展開される順序は以下の順序になる。

%var%形式の変数, 引数(%n)

引数変数nは0だとシェルのパス、1~9なら当該バッチ(または関数)の引数に展開される。コード読み込み時に展開されるため、for文などの中に入れるとループ処理が始まる前に解釈され、ループ処理中は固定値になってしまう。また、入れ子にできないため、%var[%index%]%のような表記もできない。

for文の変数

for文で宣言する変数。ループ毎に展開される。

遅延展開変数。

!var!形式の変数。コマンド呼び出しとして解釈される直前に展開される。setlocal enabledelayedexpansionを宣言すると、対応するendlocalまでの間で使用可能になる。通常の変数などを入れ子にできる

以下のコードを実行すると、 setlocal enabledelayedexpansion set var[1]=aaa for /l %I in (1, 1, 1) do (     set index=1     echo var[%index%]     echo var[%I]     echo !var[%index%]!     echo !var[%I]! ) endlocal echoの出力は、一つ目が`var[]'、二つ目が`var[1]'、三つ目は`ECHOは <ON>です。'、4つ目が`aaa'。意図するところは4つめだろう。

2006/10/24

[DOS]array

DOS上で配列や連想配列を使いたくなることがある。 for /l %I in (0,1,2) do (     echo %arrayVar[%I]% ) のように、ループをしたい場合でも、上記は動いてくれない。(バッチ上では%Iではなく%%Iで書くが、これはあまり関係ない。for文というよりは、do (~~)の展開の仕方が問題で、set /?のヘルプの最後の方に解説がある。

配列という概念がないため、変数名を動的に切り替える形で実現する。DOSの変数展開について知る必要があるが、とりあえず答えを書いてしまう。 setlocal enableexpansion rem べたにアクセスする場合 set key=keyvalue set var[%key%]=aaa echo !var[%key%]! rem forループでアクセスする場合 for /L %I in (0,1,2) do (     set var[%I]=value%I endlocal for /L %I in (0,1,2) do (     echo !var[%I]! ) バッチファイルの時にforの変数%I%%Iにする必要があるのは通常と同じだ。配列そのものではなく、var[0]という名前の変数だということだけ覚えておいて欲しい。[]の中は、変数名として使える文字列であればなんでもよく、配列はわかりやすく数値をそのまま使用しているだけ。

2006/10/23

[Java]String Concatnate

文字列結合の話。
Stringを結合するときには、この順に適用可能か考えていく。

  1. 演算子+を使う
  2. String#concat()
  3. StringBuffer

システム開発をやっていると文字列の取り扱いはいたる所で出てくる。当然結合処理も。数字だろうが日付だろうがStringなシステム(こういうところはDBのカラムも片っ端からCHARVARCHAR2になっている)もあるけど、それはちょっと別の話。

Stringの結合で+を使った結合は遅いという言葉をたまに聞くが、文面どおりにとってはいけない。完全に間違っているわけではないけど、略しすぎている。本当の内容は複数ステートメントにまたがる結合を行うなら、+を使った結合は遅くなるだ。

StringBufferStringクラスのAPI Documentに従うと、+結合はStringBuffer(Java5.0以降なら、場合によってStringBuilder)を使った結合に変換される。
コード str = stringA + stringB; 変換後 str = new StringBuffer().append(stringA).append(stringB).toString(); 当然、コンパイル後には2つに違いはない。これが複数ステートメントに分かれているとどうなるかというと、
コード str = stringA + stringB; str = str + stringC; str = new StringBuffer().append(stringA).append(stringB).toString(); str = new StringBuffer().append(str).append(stringC).toString(); となり、StringBufferの生成とStringへの変換が2回に分かれてしまう。こうなると、最初からStringBufferを生成しておいてappendしていく方が効率がいい。 for (int i = 0; i < strs.length; i++) {     str = str + strs[i]; ) なんかになるとnew StringBuffertoStringを配列の長さ分だけ実行することになってぼろぼろになる。これを+を使った結合は遅いといっているわけだ。