Hatena::ブログ(Diary)

IT戦記 このページをアンテナに追加 RSSフィード

2010-03-26

Effective Java 読書会 14 日目 「シリアライズ!シリアライズ!」

お前をシリアルにしてやろうか!

f:id:amachang:20100326160023j:image

this photo is licensed by Horia Varlan

はじめに

いよいよ最後のページになりました!!!

はりきっていきましょう!!

今回の範囲

279 ページ 〜 305 ページ

前回はこちら

Effective Java 読書会 13 日目 「Java の例外めんどくさい」 - IT戦記

シリアライズって何?

シリアライズとは、構造を持ったデータ(Java では、オブジェクトやプリミティブ)を、バイト列にすること。

たとえば、

  • オブジェクトをファイルに保存したい!
  • 通信先にこのオブジェクトを送りたい!
  • (具体的には、)ゲームデータをセーブしたい!(とか)

などなどの用途で使えます。

Javaシリアライズ

ObjectOutputStream にオブジェクトを書き込むと、 ObjectOutputStream がラップしているオブジェクトがバイト列に変換される。

たとえば、以下のようなことになる

package jp.amachang;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Arrays;
import java.util.Date;

public class App {
    public static void main( String[] args ) throws IOException {

        // バイト配列ストリームを作る
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        
        // バイト配列ストリームをオブジェクトストリームでラップ
        ObjectOutputStream oout = new ObjectOutputStream(bout);
 
        // Date オブジェクトをシリアライズ
        oout.writeObject(new Date());
        
        oout.close();
        bout.close();
        
        // バイト配列ストリームに書き込まれたバイト列を表示
        System.out.println(Arrays.toString(bout.toByteArray()));
        // => [-84, -19, 0, 5, 115, 114, 0, 14, 106, 97, 118, 97, 46, 117, 116, 105, 108, 46, 68, 97, 116, 101, 104, 106, -127, 1, 75, 89, 116, 25, 3, 0, 0, 120, 112, 119, 8, 0, 0, 1, 39, -104, 127, 40, 91, 120]
    }
}

ちゃんと、 Date オブジェクトがバイト列になりました!!すごい!

もちろん ByteArrayOutputStream を FileOutputStream にすればファイルに保存できるし、 Socket と一緒に使えば RPC 的に使える!

シリアライズって何?

シリアライズは、シリアライズの逆でシリアライズで生成されたバイト列をオブジェクトに戻すことを言う。

バイト列に出来るだけじゃ意味ないですからね!

Java のデシリアライズ

Java のデシリアライズは ObjectInputStream で InputStream をラップして、 readObject すれば出来る。

以下の例を見てください。

package jp.amachang;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Date;

public class App {
    public static void main( String[] args ) throws IOException, ClassNotFoundException {
        
        // さっきシリアライズした new Date() のバイト列
        byte[] bytes = {-84, -19, 0, 5, 115, 114, 0, 14, 106, 97, 118, 97, 46, 117, 116, 105, 108, 46, 68, 97, 116, 101, 104, 106, -127, 1, 75, 89, 116, 25, 3, 0, 0, 120, 112, 119, 8, 0, 0, 1, 39, -104, 127, 40, 91, 120};

        // バイト配列をストリームにする
        ByteArrayInputStream bin = new ByteArrayInputStream(bytes);
        
        // オブジェクトストリームでラップ
        ObjectInputStream oin = new ObjectInputStream(bin);
        
        // デシリアライズする
        Date date = (Date)oin.readObject();
        
        // 日付を表示
        System.out.println(date);
        // => Fri Mar 26 12:23:42 JST 2010
    }
}

自分の作ったクラスをシリアライズできるようにする

まず単純に

以下のようにしてみると

package jp.amachang;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

// 自分の作ったオブジェクト
class Foo {
    int bar = 1;
    String baz = "foo";
}

public class App {
    public static void main( String[] args ) throws IOException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream oout = new ObjectOutputStream(bout);
 
        // オブジェクトをシリアライズ
        oout.writeObject(new Foo());
        // ↑ ここで以下のようなエラーが発生してしまう
        // 
        // Exception in thread "main" java.io.NotSerializableException: jp.amachang.Foo
        //      at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1156)
        //      at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:326)
        //      at jp.amachang.App.main(App.java:19)
    }
}

このようにエラーになってしまう。

マーカーインタフェース Serializable を実装してあげる

Serializable でマーク付けすれば、シリアライズできるようになる。

package jp.amachang;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Arrays;

// Serializable を実装してやる
class Foo implements Serializable {
    int bar = 1;
    String baz = "foo";
}

public class App {
    public static void main( String[] args ) throws IOException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream oout = new ObjectOutputStream(bout);
 
        // オブジェクトをシリアライズ
        oout.writeObject(new Foo());

        oout.close();
        bout.close();
        
        // ちゃんとシリアライズされている
        System.out.println(Arrays.toString(bout.toByteArray()));
        // => [-84, -19, 0, 5, 115, 114, 0, 15, 106, 112, 46, 97, 109, 97, 99, 104, 97, 110, 103, 46, 70, 111, 111, -104, -48, -77, 46, -35, 69, 25, 40, 2, 0, 2, 73, 0, 3, 98, 97, 114, 76, 0, 3, 98, 97, 122, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 120, 112, 0, 0, 0, 1, 116, 0, 3, 102, 111, 111]
    }
}
シリアライズできるかな?
package jp.amachang;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

class Foo implements Serializable {
    int bar = 1;
    String baz = "foo";
}

public class App {
    public static void main( String[] args ) throws IOException, ClassNotFoundException {

        // さっき、シリアライズしたバイト列を
        byte[] bytes = {-84, -19, 0, 5, 115, 114, 0, 15, 106, 112, 46, 97, 109, 97, 99, 104, 97, 110, 103, 46, 70, 111, 111, -104, -48, -77, 46, -35, 69, 25, 40, 2, 0, 2, 73, 0, 3, 98, 97, 114, 76, 0, 3, 98, 97, 122, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 120, 112, 0, 0, 0, 1, 116, 0, 3, 102, 111, 111};

        ByteArrayInputStream bin = new ByteArrayInputStream(bytes);
        ObjectInputStream oin = new ObjectInputStream(bin);
        
        // デシリアライズ
        Foo foo = (Foo) oin.readObject();
        
        // ちゃんと出来てるかな?
        System.out.println(foo.bar); // => 1
        System.out.println(foo.baz); // => foo
        // やったね!!
    }
}

こんな感じで、 Serializable を実装するだけで、シリアライズできるようになるんですね!

シリアライズした時とデシリアライズした時で、オブジェクトの形が変わっちゃったらどうなるん?

package jp.amachang;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Date;

// さっきのオブジェクトを拡張
class Foo implements Serializable {
    int bar = 1;
    String baz = "foo";
    
    // このフィールドが追加された
    Date createdAt;
}

public class App {
    public static void main( String[] args ) throws IOException, ClassNotFoundException {

        // さっき、シリアライズしたバイト列を
        byte[] bytes = {-84, -19, 0, 5, 115, 114, 0, 15, 106, 112, 46, 97, 109, 97, 99, 104, 97, 110, 103, 46, 70, 111, 111, -104, -48, -77, 46, -35, 69, 25, 40, 2, 0, 2, 73, 0, 3, 98, 97, 114, 76, 0, 3, 98, 97, 122, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 120, 112, 0, 0, 0, 1, 116, 0, 3, 102, 111, 111};

        ByteArrayInputStream bin = new ByteArrayInputStream(bytes);
        ObjectInputStream oin = new ObjectInputStream(bin);
        
        // デシリアライズ
        Foo foo = (Foo) oin.readObject();
        // ↑ ここで以下のようなエラーが発生!!!!
        // Exception in thread "main" java.io.InvalidClassException: jp.amachang.Foo; local class incompatible: stream classdesc serialVersionUID = -7435245970926528216, local class serialVersionUID = 911504535012214353
        //      at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:562)
        //      at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1583)
        //      at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1496)
        //      at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1732)
        //      at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1329)
        //      at java.io.ObjectInputStream.readObject(ObjectInputStream.java:351)
        //      at jp.amachang.App.main(App.java:28)
    }
}

節子「…にいちゃん…なんで、オブジェクト…死んでしまうん…?」

修造「大丈夫大丈夫!死んでない!死んでない!Serial Version UID で出来る出来る!出来る!」

というわけで、シリアライズするときに Sereal Version UID をつけましょう

シリアライズ
package jp.amachang;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Arrays;

class Foo implements Serializable {

    // Serial Version UID を付ける
    private static final long serialVersionUID = 1L;

    int bar = 1;
    String baz = "foo";
}

public class App {
    public static void main( String[] args ) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream oout = new ObjectOutputStream(bout);
        oout.writeObject(new Foo());

        oout.close();
        bout.close();
        
        // ちゃんとシリアライズされる(バージョン番号(1L)が埋め込まれている)
        System.out.println(Arrays.toString(bout.toByteArray()));
        // => [-84, -19, 0, 5, 115, 114, 0, 15, 106, 112, 46, 97, 109, 97, 99, 104, 97, 110, 103, 46, 70, 111, 111, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 2, 73, 0, 3, 98, 97, 114, 76, 0, 3, 98, 97, 122, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 120, 112, 0, 0, 0, 1, 116, 0, 3, 102, 111, 111]
    }
}
シリアライズ
package jp.amachang;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Date;

class Foo implements Serializable {

    // Serial Version UID は同じ
    private static final long serialVersionUID = 1L;

    int bar = 1;
    String baz = "foo";
    
    // 新しくフィールドを追加
    Date createdAt = new Date();
}

public class App {
    public static void main( String[] args ) throws IOException, ClassNotFoundException {

        // さっきシリアライズしたバイト列
        byte[] bytes = {-84, -19, 0, 5, 115, 114, 0, 15, 106, 112, 46, 97, 109, 97, 99, 104, 97, 110, 103, 46, 70, 111, 111, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 2, 73, 0, 3, 98, 97, 114, 76, 0, 3, 98, 97, 122, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 120, 112, 0, 0, 0, 1, 116, 0, 3, 102, 111, 111};

        ByteArrayInputStream bin = new ByteArrayInputStream(bytes);
        ObjectInputStream oin = new ObjectInputStream(bin);
        
        // Serial Version UID が同じならちゃんとデシリアライズできる
        Foo foo = (Foo)oin.readObject();
        
        System.out.println(foo.bar); // => 1
        System.out.println(foo.baz); // => foo
        
        // 新しく追加されたフィールドは null になっている
        System.out.println(foo.createdAt); // => null
    }
}

やったね!

Serial Version UID を付けない場合は

メソッドの追加などにも影響を受けるので、自分でちゃんと付けたほうがいいです。

シリアライズのとき、コンストラクタは呼ばれません!!!!

先ほどの例で、 foo.createdAt が null になっていたことからも分かるように、シリアライズ、デシリアライズは完全に言語外の仕組み、コンストラクタによって初期化が行われないことに注意しましょう!

シリアライズ形式は公開 API

シリアライズ形式とは、シリアライズされるバイト列の形式のこと。

シリアライズ形式は、一度決まるといたるところで使われ、それを変更するのは難しい。

たとえば、ソフトウェアアップデートされても、前のバージョンで保存されたデータが PC に残っているかもしれない。

よって、シリアライズ形式は公開 API と認識すべし。

自分でシリアライズ形式をカスタマイズする

シリアライズ形式を自分で制御することもできる。

package jp.amachang;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class Foo implements Serializable {

    private static final long serialVersionUID = 1L;

    int bar = 1;
    String baz = "foo";

    // 自分でシリアライズ
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.writeInt(bar);
        out.writeObject(baz);
    }

    // 自分でデシリアライズ
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        bar = in.readInt();
        baz = (String) in.readObject();
    }
}

public class App {
    public static void main( String[] args ) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream oout = new ObjectOutputStream(bout);
        
        // シリアライズ
        oout.writeObject(new Foo());
        
        oout.close();
        bout.close();
        
        ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
        ObjectInputStream oin = new ObjectInputStream(bin);
        
        // デシリアライズ
        Foo foo = (Foo) oin.readObject();
        
        // ちゃんと出来てる???
        System.out.println(foo.bar); // => 1
        System.out.println(foo.baz); // => foo
    }
}

transient と default(Read|Write)Object を使う

また、一部だけカスタマイズした場合は、 transient と default(Read|Write)Object を使うといい

class Foo implements Serializable {

    private static final long serialVersionUID = 1L;

    // baz のシリアライズの仕方だけカスタマイズする
    int bar = 1;
    transient String baz = "foo";

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject(); // transient が付いていないフィールドを全部シリアライズ
        out.writeObject(baz); // baz だけ手動でシリアライズ
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        out.defaultReadObject(); // transient が付いていないフィールドを全部デシリアライズ
        baz = (String) in.readObject(); // baz を手動でデシリアライズ
    }
}

バイナリ互換性とセマンティック互換性

シリアライズ形式が偶然同じでも、フィールドが持つ意味(セマンティック)が変わっているかもしれない。

フィールドの意味も考えて、シリアライズ、デシリアライズを考えないといけない!

Serializable じゃなくて、デフォルトコンストラクタを持たないクラスを継承した場合、デシリアライズできない

package jp.amachang;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

// デフォルトコンストラクタを持たず、
// Serializable な親を持つ場合は
abstract class Bar {
    Bar(int i) {} 
}

class Foo extends Bar implements Serializable {

    private static final long serialVersionUID = 1L;

    Foo() {
        super(1);
    }
    
    int bar = 1;
    String baz = "foo";
}

public class App {
    public static void main( String[] args ) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream oout = new ObjectOutputStream(bout);
        
        oout.writeObject(new Foo());
        
        oout.close();
        bout.close();
        
        ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
        ObjectInputStream oin = new ObjectInputStream(bin);
        
        // デシリアライズ
        Foo foo = (Foo) oin.readObject();
        // ここで例外が発生
        // Exception in thread "main" java.io.InvalidClassException: jp.amachang.Foo; jp.amachang.Foo; no valid
        // constructor
        // at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:713)
        // at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1733)
        // at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1329)
        // at java.io.ObjectInputStream.readObject(ObjectInputStream.java:351)
        // at jp.amachang.App.main(App.java:42)
        // Caused by: java.io.InvalidClassException: jp.amachang.Foo; no valid constructor
        // at java.io.ObjectStreamClass.<init>(ObjectStreamClass.java:471)
        // at java.io.ObjectStreamClass.lookup(ObjectStreamClass.java:310)
        // at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1106)
        // at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:326)
        // at jp.amachang.App.main(App.java:33)
    }
}

原因は、デシリアライズ時にデシリアライズされるオブジェクトのもっとも近い Serializable じゃない親のデフォルトコンストラクタが呼ばれるから。

そのオブジェクトコンストラクタは決して呼ばれないので注意せよ!!!

なので、 abstract クラスにはなるべくデフォルトコンストラクタを用意しましょう

デフォルトコンストラクタ以外の処理がある場合、コンストラクタの処理を protected な初期化メソッドとして切り出して置いて、 readObject の中で呼び出すといいよ!

enum は、難しいこと考えなくてもシリアライズ可能

自前でのシングルトンやインスタンス制御より enum のほうが楽ちん!

readObject を呼び出すときの注意

オーバーライドされた可能性のあるメソッドを呼び出さない!(サブクラスで、親クラスに存在しないフィールドを参照する可能性があるため)

コンストラクタで防御的なコピーをしているなら、 readObject でもやる!

シリアライズプロキシーパターン

意味的にシリアライズすべき、フィールドだけを持ったプロキシークラスを生成して、そのクラスがシリアライズされる

public class Cereal implements Serializable {
    private String  name;
    private int     calorie;
   
    public Cereal(String name, int calorie) {
        this.name = name;
        this.calorie = calorie;
    }
   
    /**
     *  このクラスの代わりにシリアライズする代替オブジェクトを返す
     */
    private Object writeReplace() {
        //  Cereal の代わりに CerealProxy をシリアライズする
        return new CerealProxy(this);
    }
     
    /**
     *  このクラスのデシリアライズの実装。
     */
    private void readObject(ObjectInputStream in) throws InvalidObjectException {
        //  このクラスはデシリアライズさせない!
        throw new InvalidObjectException("Proxy required.");
    }
     
    /**
     *  Cereal のシリアライズプロキシクラス。
     *  Cereal とは論理的に等価
     */
    private static class CerealProxy implements Serializable {
        private static final long serialVersionUID = 1L;
   
        private final String    name;
        private final int       calorie;
         
        public CerealProxy(Cereal cereal) {
            this.name = cereal.name;
            this.calorie = cereal.calorie;
        }
         
        /**
         *  このクラスの代わりにデシリアライズする代替オブジェクトを返す
         */
        private Object readResolve() {
            //  CerealProxy の代わりに Cereal をデシリアライズする
            return new Cereal(name, calorie);
        }
    }
}

シリアライズプロキシーパターンのコードは、同僚の K.Nishina (Twitter とかブログやってないのかな><)さんが書いてくれたものを使わせてもらいました!

シリアル

まとめ

これで、すべてのページが終了しました!!!

やったぜええええええええええ!

2010-03-25

Effective Java 読書会 13 日目 「Java の例外めんどくさい」

IOException の catch に何を書いていいか分かりません><!

IOExcep tion... - mmr - はてなセリフ

はじめに

順番が前後しますが、今回は Java の特徴のひとつである例外機構についてです。

今回の範囲

223 ページ 〜 250 ページ

前回はこちら

Effective Java 読書会 12 日目 「スレッド・セーフってなによ!!」 - IT戦記

Java の例外

  • throw 可能なオブジェクト
  • チェック例外
    • メソッドの実装者が「呼び出し元が回復可能」だと考えている例外
    • ちゃんと「なぜ、例外だったのか」理由が提供されるべき
    • 呼び出し元は try catch で囲むか throws 宣言を書く必要がある
    • Exception を継承していて RuntimeException を継承していないクラス
  • 実行時例外
    • 事前条件違反(仕様で明確に定義された条件が揃わない状態でメソッドが呼び出されたこと)を通知する手段として使われる
    • 実行時にこれが発生する場合は、メソッドの呼び出し元がコードを直すべき
    • 呼び出し元が try catch も throws も書く必要がない
    • RutimeException を継承したクラス
  • エラー
    • JVM が使う

ちょっとイメージが沸きやすいように台詞で言い換えてみる

チェック例外

男子「好きです!付き合ってください!」(メソッドの実行)

女子「別に好きな男の子がいるんです!」(回復可能例外、理由付き)

男子「じゃあ、友達になって!相談とかのるよ!」(回復)

実行時例外

男子「好きです!付き合ってください!」(メソッドの実行)

女子「え、あなた誰ですか!?」(事前条件違反)

修正

男子「はじめまして、以前文化祭でチョコバナナを売ってましたよね」

女子「はい」

男子「あ、僕そのとき客として行ったの覚えてます?」

女子「あー!あのときの!」(事前条件)

時は流れて…

男子「好きです!付き合ってください!」(メソッドの実行)

女子「こちらこそ!」(成功)

エラー

男子「好きです!付き合ってください!」(メソッドの実行)

女子「…」

女子「…」

女子(神)「私だ」(エラー)

男子(神)「お前だったのか」

女子(神)「暇を持て余した、神々の」

男子(神)「遊び」

-- プログラム終了 --

チェック例外は負荷が高い

try catch は使う側のコードが非常に煩雑になる

チェック例外が生みだす煩雑さが正当かを考える

チェック例外は使う側のコードを煩雑にするけど、以下の二つの条件が成立する場合、その煩雑さは正当化される。

  • その例外が発生する事前条件を定義できない
  • 呼び出し元が、 catch したときに有用な処理を書ける(例外が、なんらかの情報を持っている)

この二つの条件を満たさない場合は、実行時例外にするべき

たとえば、呼び出し元が e.printStackTrace() くらいしかすることがないんだったら、実行時例外にしてしまおう。

標準例外を使おう

  • IllegalArgumentException
  • IllegalStateException
  • NullPointerException
  • IndexOutOfBoundsException
  • ConcurrentModificationException
  • UnsupportedOperationExcption

など、標準例外で用意されているものは積極的に使おう!

抽象概念に的した例外を投げる

適切に例外翻訳をしよう

たとえば、 findUser とかっていうメソッドから SQLException が投げられちゃだめで、 UserNotFoundException とかにしたほうがいい。

ただ、「何でも例外翻訳すればいい」はダメ。可能なら、そのメソッド内で例外が発生しないことを保証しよう。

例外を文書化しよう

  • チェック例外は @throws に書く
  • 実行時例外は javadoc の @throws には書くけどメソッドの throws には書かない

例外のメッセージに詳細な情報を含める

例外の文字列表現は、その例外の原因となった全ての情報を含んでいるべき。

以下のようなイディオムで書くといいよ

// コンストラクタで必要な情報をすべて受け取る
public FooException(int foo, String bar, long baz) {

    // 詳細メッセージを作る
    super("foo: " + foo + ", bar: " + bar + ", baz: " + baz);

    // フィールドに記録する
    this.foo = foo;
    this.bar = bar;
    this.baz = baz;
}

エラーアトミック性

すごい重要。

エラー、例外が発生した場合は、オブジェクトの状態をメソッド呼び出し前の状態に戻してあげる。

これは JavaScript をやっててすごく思うこと

たとえば複雑な DOM を構築するようなメソッドでエラーが発生した場合に DOM にゴミがいっぱいくっついた状態になってしまうことが多い

エラー発生時に内部で一回 catch して綺麗にするなどエラーアトミックにしておくことが重要。

例外を無視しない

チェック例外は、握りつぶさない。

例外を無視してもいい場合は、その理由を catch ブロックに記述する。

事前条件などで、例外が発生しないことが確実なら AssertionError を投げるなどする。

例外以外のこと

例外以外の項目に書いてあったこと

リフレクション
  • コンパイル時の検査がない
  • ぎこちなくて、冗長
  • パフォーマンスが悪い

あんまりメリットない。

フレームワークとか作るときには使うかもねー

ネイティブメソッド

JNI で C/C++ で書かれたメソッドを呼べる。

結構、めんどくさい。

速度のためにやるのは、オススメしない。

最近の Java は速いらしい

注意して最適化する

または、最適化なぞするな

最適化する前にプロファイリングなどをちゃんとやろう。

命名規約

空気を読んで、名前付けしましょう。

まとめ

うん! Java のチェック例外メッチャめんどくさい!

次回はこちら

Effective Java 読書会 14 日目 「シリアライズ!シリアライズ!」 - IT戦記

2010-03-24

Effective Java 読書会 12 日目 「スレッド・セーフってなによ!!」

スレッドセーフスレッドセーフって何なのよ!本当の気持ち聞かせてよ!

/**   お前への愛は、... - 消しゴム - はてなセリフ

はじめに

皆様おひさしぶりです。

久しぶりに Effective Java 読書会のまとめを上げていきたいと思います!!!

今回の範囲

269 ページ 〜 278 ページ

前回はこちら

Effective Java 読書会 11 日目 「Java マルチスレッド難しいいい」 - IT戦記

synchronized を付ければスレッド・セーフってわけではない

synchronized はスレッド・セーフを実現するための一つの方法(実装の詳細)であって、

  • synchronized を使っているからスレッド・セーフというわけではないし、
  • synchronized を使っていないからスレッド・セーフじゃないというわけではない

そもそも、スレッド・セーフとは何か

  • そのクラスのインスタンスに対するどんな順番で行われる操作も仕様的に正しく振る舞い続ける
  • 複数のスレッドから、どんな順番で操作が行われても同様に正しく振る舞い続ける

みたいなこと

そもそも「正しく振る舞い続ける」とは何か?というのは、そのクラスの設計者が決めることなので

「正しい振る舞い」とは何なのかという仕様がないと「スレッド・セーフ」は語れないよね

つまり、「このコードはスレッドセーフだ!!」というには

文書化が必要

スレッド・セーフのレベル 

遅延初期化を注意して使用する

基本的には、遅延初期化しなくてもいい。

初期化チェーンを切りたいときとかに使いましょう。

普通の初期化
private final FieldType field = computeFieldValue();
FieldType getField() {
    return field;
}
同期化されたメソッドで遅延初期化
private FieldType field;

// 毎回、同期化されたコードを実行することになる。
// (パフォーマンスが気になる場合は、二重チェックイディオムを使う)
synchronized FieldType getField() {
    if (field == null) {
        field = computeFieldValue();
    }
    return field;
}
二重チェックイデオム

前の例より、複雑だけど速い

private volatile FieldType field;

FieldType getField() {
    FieldType result = field; // フィールドのスナップショットを取る(volatile によって可視性が保証される)
    if (result == null) { // 同期ブロックのの外からチェック(一度初期化されるとロックされることなく、値が返る)
        synchronized (this) { // ここで同期
            result = field; // もう一回チェック(複数のスレッドがここに入ってくる可能性があるため)
            if (result == null) {
                field = result = computeFieldValue(); //初期化
            }
        }
    }
    return result;
}
単一チェックイデオム

初期化が何回も走っても良い場合

private volatile FieldType field;

FieldType getField() {
    FieldType result = field; // フィールドのスナップショットを取る(volatile によって可視性が保証される)
    if (result == null) {
        field = result = computeFieldValue(); // 初期化、何回か実行される可能性はある
    }
    return result;
}

スレッドスケジューラに依存したコードを書かない

一時的なパフォーマンス改善などにはなるかもしれないが、移植可能じゃなくなる><

そもそも、スレッドが有益なことをしていない状況になる場合は、スレッドにすることがダメ><

ThreadGroup

使っちゃダメよ><

まとめ

マルチスレッドに関する仕様を javadoc に書こう!

次回はこちら

Effective Java 読書会 13 日目 「Java の例外めんどくさい」 - IT戦記

2010-03-09

Effective Java 読書会 11 日目 「Java マルチスレッド難しいいい」

はじめに

順番が前後しますが、都合上「並行性」の章を先に書きたいと思います。

読んだところ

251 ページ 〜 268 ページ

前回はこちら

Effective Java 読書会 10 日目 「Java の基本テクニック集」 - IT戦記

同期とは何か

同期 = 原子性 + 可視性

  • 原子性(アトミック性)
    • データの状態遷移の過渡的な不整合な状態が(どのスレッドからも)見えないという性質。
    • 適切に相互排他することでデータの原子性を保証できる。(保護されたコードを実行できるスレッドは一つだけ。)
  • 可視性(ビジビリティ)

同期するというのは、原子性を保証することだけではなく、可視性も保証することだということを忘れてはいけない!!!

long, double 以外の変数への読み書き

  • 原子性は保証されている!
  • しかし、可視性の保証はない!
    • メモリモデルで定義されている

同期されないフィールドへの値の設定、参照

したがって、以下のような例では stopRequested の可視性の保証はされない。

import java.util.concurrent.TimeUnit;

public class Main {

    private static boolean stopRequested;
    
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            public void run() {
                int i = 0;
                
                // 値の参照
                while (!stopRequested) {
                    i++;
                }
            }
        }).start();
        TimeUnit.SECONDS.sleep(1);
        
        // 値の設定
        // (この値の変更が、いつもう一つのスレッドに反映されるかは分からない)
        // (もう一つのスレッドのコードが巻き上げの最適化をされてしまうかもしれない!!)
        stopRequested = true;
    }
}

ということになる。

で、実際試してみるとスレッドは止まらない。

活性エラー

上の例のように、プログラムが先に進めなくなるエラー

ちなみに:Thread.stop は使ってはいけない

データを破壊する可能性がある。

スレッドを停止する推奨される方法は、同期されたフラグのポーリングだそうです。

同期させてみる

以下のように、値の設定と参照を synchronized メソッドで囲めばいい。

import java.util.concurrent.TimeUnit;

public class Main {

    private static boolean stopRequested;
    
    private static synchronized void requestStop() {
        stopRequested = true;
    }

    private static synchronized boolean stopRequested() {
        // synchronized に入るときにそれまでの変更がこのスレッドに確実に反映される。
        // (stopRequested への可視性が保証される)
        return stopRequested;
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            public void run() {
                int i = 0;
                    
                // 値の参照
                while (!stopRequested()) {
                    i++;
                }   
            }   
        }).start();
        TimeUnit.SECONDS.sleep(1);
            
        // 値の設定
        requestStop()
    }   
}

volatile

synchronized は原子性と可視性を保証する。

boolean への読み書きは、元々原子性は保証されているので、可視性だけを保証すればいい。

で、変数の可視性を保証する方法が「volatile」。

volatile を使ってさっきの例を書き換えると

import java.util.concurrent.TimeUnit;

public class Main {

    private static volatile boolean stopRequested;
        
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            public void run() {
                int i = 0;
                    
                // 値の参照
                while (!stopRequested) {
                    i++;
                }   
            }   
        }).start();
        TimeUnit.SECONDS.sleep(1);
            
        // 値の設定
        stopRequested = true;
    }   
}

すっきり!

volatile だけじゃだめな場合

public class Main {

    // count の原子性、可視性は保証されているが
    private static volatile int count = 0;
    
    public static int getCount() {
        // ++ は原子性がないのでダメ
        return count++;
    };
}

count++ は

  • 値の読み出し
  • 加算
  • 書き戻し

の 3 つの操作を行う

たとえば、以下のように二つのスレッドが走った場合、不整合な値が返ってしまう。

  • Thread1: 値の読み出し
  • Thread2: 値の読み出し
  • Thread1: 加算
  • Thread2: 加算
  • Thread1: 書き戻し
  • Thread2: 書き戻し

安全性エラー

上の例のようにプログラムが誤った結果を返すエラー

synchronized で書き直し

こんな感じですかね。 synchronized なので、 long も使えるよ!

public class Main {

    private static long count = 0;
    
    public static synchronized long getCount() {
        if (count == Long.MAX_VALUE) {
            throw new ArithmeticException("too large to increment");
        }
        return count++;
    };
}

もっといい例は

java.util.concurrent.atomic を使う!

import java.util.concurrent.atomic.AtomicLong;

public class Main {

    private static AtomicLong count = new AtomicLong();
    
    public static long getCount() {
        return count.getAndIncrement();
    };
}

わおわお!

すっきりですね

可変データはなるべく共有しない

同期しなくていい最善の方法は、可変データを共有しないこと。

不変データ最強伝説。

可変データでも、変更しなけれ

事実上不変!(effetively immutable)

オブジェクトの参照を他のスレッドに転送する

安全な公開!(safe publication)

過剰な同期

過剰に同期しすぎると、以下のような振る舞いをする可能性がある

デッドロック

お互いがお互いのスレッドを待っている状態。

二つの synchronized メソッドを持つオブジェクト A, B があって

この二つのパターンがあるような場合で、複数スレッドから A, B を扱うとデッドロックが発生する

オブザーバーパターン

オブザーバーパターンや、イベントリスナーのように、オブジェクトの呼び出し階層が上下逆になるような場合にデッドロックが発生しやすい。

その本質的な原因は「異質なメソッド」(コールバックのように中で何が行われるかわからないメソッド)を synchronized から呼ぶことである

class Observable {
    private List<Observer> observers = new ArrayList<Observer>();
    
    public void addOvserver(Observer o) {
        synchronized (observers) {
            observers.add(o);
        }
    }

    private void notifyUpdate(String e) {

        // ロックの中で!!
        synchronized (observers) {
            for (Observer observer : observers) {

                // 異質なメソッド!!
                // この update はどのような処理が行われるか分からない(Observer の実装は、この class を使う側が決めるから!!)
                // 中でデッドロックするかも!!!!
                observer.update(this, e);
            }
        }
    }

    // ...
}

interface Observer {
    public void update(Observable o, String e);
}

オープンコールで解決

ロックの中で呼ばないように、配列を一旦コピーすれば解決する

class Observable {
    private List<Observer> observers = new ArrayList<Observer>();
    
    public void addOvserver(Observer o) {
        synchronized (observers) {
            observers.add(o);
        }
    }

    private void notifyUpdate(String e) {
        
        // 状態をコピーする
        List<Observer> snapshot = null;
        synchronized (observers) {
            snapshot = new ArrayList<Observer>(observers);
        }

        for (Observer observer : snapshot) {
            // 異質なメソッド!
            // でも安心
            observer.update(this, e); 
        }
    }

    // ...
}

interface Observer {
    public void update(Observable o, String e);
}

このような、ロックの外からの呼び出しをオープンコールという。

オープンコール重要。

スナップショットを取るのもいいけど、変更したときに丸々コピーする方が効率いい

イテレーションの度にコピーするより、オブジェクトに変更があったときに内部の状態をコピーするという方法もある。

Observer が追加されたら、今まで使ってたリストは破棄して一個追加された状態のリストを新たに作るような感じ。

この操作を抽象化したリストが CopyOnWriteArrayList 、オブザーバーパターンのときは使おう!

class Observable {
    private List<Observer> observers = new CopyOnWriteArrayList<Observer>();
    
    public void addOvserver(Observer o) {
        // ここで内部の配列がコピーされるので
        // (前の状態が破壊されずに残っているので)
        observers.add(o);
    }

    private void notifyUpdate(String e) {
        for (Observer observer : observers) {
            // ここでは、ロックもスナップショットもいらずに
            // オープンコールできる
            observer.update(this, e);
        }
    }

    // ...
}

interface Observer {
    public void update(Observable o, String e);
}

Thread より

Executor を使おう!

wait, notify より

コンカレンシーユーティリティを使おう!

まとめ

スレッド難しい><

次回はこちら

Effective Java 読書会 12 日目 「スレッド・セーフってなによ!!」 - IT戦記

2010-03-03

Effective Java 読書会 10 日目 「Java の基本テクニック集」

はじめに

読書会に参加していないところがあるので、そこは議事録を読みながら、なるべく自分の言葉で書いていきます!

読んだところ

175 ページ〜 222 ページ

前回はこちら

Effective Java 読書会 9 日目 「Enum の拡張とアノテーション」 - IT戦記

引数の検査をきちんとして javadoc の @throws に書く

  • IllegalArgumentException
  • IndexOutOfBoundsException
  • NullPointerException

などは、事前に引数チェックして出す。たとえば、 OpenJDK の String(byte[], int, int, String) では、以下のような実装になっている、自分で引数チェックをして、その内容を明確に @throws に記述している。

    // チェック関数
    private static void checkBounds(byte[] bytes, int offset, int length) {
        if (length < 0)
            throw new StringIndexOutOfBoundsException(length);
        if (offset < 0)
            throw new StringIndexOutOfBoundsException(offset);
        if (offset > bytes.length - length)
            throw new StringIndexOutOfBoundsException(offset + length);
    }

    /**
     * (snip)
     * 
     * (以下のように javadoc に書く)
     *
     * @throws  IndexOutOfBoundsException
     *          If the {@code offset} and {@code length} arguments index
     *          characters outside the bounds of the {@code bytes} array
     *
     * @since  JDK1.1
     */
    public String(byte bytes[], int offset, int length, String charsetName)
        throws UnsupportedEncodingException
    {
        if (charsetName == null)
            throw new NullPointerException("charsetName");

        checkBounds(bytes, offset, length); // ここで引数チェック

        // (snip)
    }

受け取ったらすぐにチェック!

以下の場合は引数チェックしない

  • コストがデカい
  • 引数のチェックが、自体が元々やりたい処理の一部の場合は 2 回チェックすることになるので、やらなくていい

防御的コピー

不変オブジェクトが理想的だけど、可変オブジェクトを自分以外から破壊されないようにコピーしてしまうことも重要。

これも簡単な話で、OpenJDK の java.lang.String の実装では、以下のように char 配列を防御的にコピーしている。他のオブジェクトからの変更可能性を排除して、自分は不変オブジェクトでいられる!

    public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(count);
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.offset = 0;
        this.count = count;

        // 防御的コピー!!!!
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

メソッド

Java の標準ライブラリのメソッド名などを参考に、いい名前にしましょう

便利なメソッドは要望が出てから

最初から、便利なメソッドを作り過ぎない><

シンプルなものだけ提供して、使われる箇所を一通り見てから追加したければする。

引数多すぎ自重

4 つまで。

引数の減らし方

  • メソッド分割
  • ヘルパークラス
  • ビルダーパターン
    • かこいいやつ!
  • 引数に HashMap や TreeMap などのコンクリートクラスを使うより、 Map とかインタフェースを使う
  • boolean より 2 値の enum
    • enum! enum! みんな腕をふれー!

困惑させるオーバーロードは禁止

同じ引数の数のオーバーロードは避けたらいいんじゃね。

自動ボクシングで話がややこしくなります。

可変長引数

引数がひとつ必須の場合は

static int sub(int ... args) { /* ... */ }

じゃなくて

static int sub(int first, int ... args) { /* ... */ }

配列を表示する

System.out.println(Arrays.toString(array)); // これが正解

System.out.println(Arrays.asList(array)); // こうじゃないよ、これは可変長引数だからね

可変長引数はあまり乱用しない

不適切に使用すると、困惑させちゃうから

static void foo(Object ... args) とかはダメよ☆

配列やコレクションを返すメソッドで null を返すな

配列や空コレクションを返すように!

ちなみに空配列は不変オブジェクトなので、使いまわせるので軽い。

javadoc

書け

変数スコープはちっちゃくね

  • 変数宣言は変数が使われるところで書く
  • 宣言時初期化
  • while より for、 for より for-each
    • Iterable を実装すれば、なんでも for-each!
  • メソッドもちっちゃくね

とにかく変数の使われるところをはっきりさせる

ライブラリを使え

ライブラリを使え

車輪の再発明するなー!!

float, double は 0.1 を正確に表現できない

BigDecimal 使えと

丸め方まで制御可能

ボクシングされたデータ型に == はダメ

あたりまえだけど、よくある間違い

文字列結合して意味を持たせるのはダメ

面倒でもクラス作る

文字列結合

StringBuilder 使いましょう StringBuffer は無駄に synchronized なので遅い

あるならインタフェース使う

以下より

ArrayList<Foo> = new ArrayList<Foo>();

以下のほうがいい。

List<Foo> = new ArrayList<Foo>();

あとから修正が楽

まとめ

今回は、多めですが内容が簡単な章でした!

次回はこちら

Effective Java 読書会 11 日目 「Java マルチスレッド難しいいい」 - IT戦記