著作一覧 |
public class Foo implements Serializable { transient Bar barbar; private void writeObject(java.io.ObjectOutputStream out) throws IOException { out.writeBoolean(barbar == null); if (barbar != null) { ByteArrayOutputStream bs = new ByteArrayOutputStream(); barbar.dump(bs); byte[] ba = bs.toByteArray(); out.writeInt(ba.length); out.write(ba); } } private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { boolean noBarbar = in.readBoolean(); if (!noBarbar) { int len = in.readInt(); byte[] ba = new byte[len]; in.read(ba, 0, len); barbar = Bar.undump(ba); } } private void readObjectNoData() throws ObjectStreamException { } static final long serialVersionUID = 0x121212121212L; }
クラスBarのundumpはファクトリメソッドで、与えられたダンプデータからBarのインスタンスを復元するものとする。だったら、Serializableにしておけよと思うが、そういうような仕組みになっている既存のやつだからしょうがない。
これが死ぬ。barbarのNullPointerExceptionだ。つまり、readObjectで読み込んだbaがおかしいらしい。
ところが、この後にデシリアライズするオブジェクトは正しく復元されている。つまり、ObjectInputStreamの読み込みポインタが途中でずれているという問題ではない。
正直、困りまくった。
結局、現場検証しかない。で、順番にいろいろログを仕込んで最終的に、次のがビンゴだった。
int resultLen = in.read(ba, 0, len); Logger.log(DEBUG, "unmarshal Bar", "size=" + len + ", result=" + resultLen);
読み込んだサイズが読むべきサイズより小さい。
????
で、あらためてObjectInputStreamのリファレンスを見て、以下のように修正。
in.readFully(ba);
これで正しく処理できるようになった。
でも、これってすさまじく愚かな仕様に見える。writeFullyがあるのならともかく、readだけなぜこんな仕様なんだ? (読み込み時間の短さがオブジェクトの復元よりも重要な場合に、早めに切り上げられるように小出しに読めるようにしたという理由はあり得るかも知れないけど、ならばwriteFullyがあっても良さそうに思えるのだが)
ジェズイットを見習え |
端末とかネットワークからの入力では、partial readしたいことが普通にあるからじゃないでしょうか。たとえば、改行までくるまで処理したいという場合には、何バイト入力待てば良いか分からないわけですが、だからといって1バイトずつ入力するのはシステムコールオーバーヘッドが大きいわけで。<br>writeについてもpartial writeが欲しいことは厳密に言うとあるんですが、こちらは、カーネル内にバッファがあり、バッファサイズまでは止まらずに書けてしまいますから、、readに比べるとやや必要性は低いです。対称性という意味ではあった方がいいんでしょうけど。でも、その場合はread/writeの動作をfullyにして、parital読み書きを長いメソッド名にした方が良さそうです。
確かにディスクはともかくネットワークまで含めるとありな気はするのですが、ものがオブジェクト復元用だからなぁ。byte[]を使った時点で負けなのでしょう。