The King's Museum

ソフトウェアエンジニアのブログ。

『人を動かす』を読んだ

なるべく読んだ本の感想は書いておこう。今までは書いたり書かなかったりだったから。

最近、『人を動かす』を読んだ。

人を動かす 文庫版

人を動かす 文庫版

相手のことを考える

自己啓発の元祖ともいえるこの本。もちろん存在は知っていた。ここ最近読んだ本で立て続けにオススメされていたので重い腰を上げて読む。Kindle なら1000円以下だし。社会人たるものカーネギー本の一冊くらいは読んでおかないと。

購入したのは8月末。時間が取れず、読み終わるのに数ヶ月かかってしまった。面白い本ならどんどん読み進むのでやはりそれほど面白くはなかったのだろう。

やたら成功事例ばかり挙げられているので飽きてくる。そしてそれがだんだん胡散臭さへと変わる。リンカーンやカーネギー(鋼鉄王の方)のエピソードが至るところで紹介される。それにくわえて筆者のセミナーの受講者の成功例。さすがに食傷気味になる。ほんとにこんなうまくいくのか?と言いたくなる事例が多々。

それでも得るものはあったと思う。この本の教えは『相手のことを考える』という点に尽きる。この教えは簡単なようでとても難しい。実践してみると、日々の生活でいかに自分が相手のことを考えず会話しているかを思い知らされた。この本で紹介されるテクニックを使ってみよう!しかし、普段からやってないのでとても難しい。脳にパターンがまったくインストールされていない。

How to win friends

過去に読んだ『インテル経営の秘密』や『あなたのチーム、機能してますか?』とは真逆の教えもある。この本は議論は徹底的に回避しろと説く。一方、『あなたのチーム、機能してますか?』では「徹底的に議論しろ。しかし、徹底的に持論しても関係が崩れない信頼をお互い持て」と説く。徹底的に議論することで問題に対する理解を深め、最適な方法を見つけようという姿勢が見て取れる。

hjm333.hatenablog.com

すこし斜めからこの本を眺めると『相手をおだてて動かそう』といってるように見えてくる。もしかしたら、そういう側面もあるのかもしれない。なぜなら、原題は How to win friends and influence people (友に勝ち、他人に影響を与える方法)なのだから。

嫁を動かす

僕が読んだのは『文庫版』。もう一つ『新装版』というエディションがあるようだ。『新装版』には「幸福な家庭をつくる七原則」という付録が含まれている。家庭を持つ僕にとってはこちらの方がよっぽど大事だったのでは…。

人を動かす 新装版

人を動かす 新装版

ぐぐると『嫁を動かす(HOW TO WIN WIFE AND INFLUENCE PEOPLE)』なんてブログもでてきて、ますます『新装版』を買わなければならない衝動にかられている。

www.move-wife.com

【Effective Java】項目78:シリアライズされたインスタンスの代わりに、シリアライズ・プロキシを検討する

Serializable を実装すると、バグやセキュリティ上の問題が発生する可能性が高くなります。 コンストラクタ以外でインスタンスが生成されるようになるからです。

これらの可能性を大幅に減らす技法が、シリアライズ・プロキシ・パターン(Serialization Proxy Pattern)です。

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

シリアライズ・プロキシ・パターンの実装について、項目39項目76で利用した Period クラスを例にして説明します。

ネストしたクラスを実装

プロキシパターンを実装するにはまず、対象となるクラス内にシリアライズ可能な private static ネストクラスを追加します。

ネストしたクラスには、シリアライズ対象のクラスのシリアライズに必要な論理的な情報を保持させます。 対象のクラスのキャッシュプロパティなどはこれに含めないでください。

このネストしたクラスは、パラメータがシリアライズ対象クラスであるコンストラクタを持つべきです。 この際、防御的コピーを用いる必要はありません。

そして、単に Serializable を実装するだけでかまいません。すなわち、デフォルトのシリアライズ形式となります。

これらを実装したものは次のコードになります。

// ネストしたクラス。シリアライズ用の専用。
private static class SerializationProxy implements Serializable {
    // フィールドは final にできる
    private final Date start;
    private final Date end;

    SerializationProxy(Period period) {
        // 防御的コピーは必要はない
        this.start = period.start;
        this.end = period.end;
    }

    // UID を設定(項目75)
    private static final long serialVersionUID = 234098243823485285L;
}

writeReplace() の実装

次に対象のクラスに writeReplace() メソッドを追加します。 writeReplace() で返したインスタンスが、実際にストリームに書き込む際のインスタンスになります。

シリアライズ・プロキシ・パターンでは writeReplace() で、さきほど宣言したネストしたクラスのインスタンスを返します。 シリアライズ時には writeReplace() が呼び出され、ネストしたクラスのインスタンスシリアライズ化されます。 すなわち、シリアライズ対象クラスが自体がシリアライズされることはなくなります。

しかし、攻撃者によって改ざんされたストリームを防ぐ必要があります。 そのため、シリアライズ対象クラスに readObject メソッドを実装し、例外が発生するようにしておきます。

// シリアライズ対象クラスの readObject メソッド
private void readObject(ObjectInputStream stream) throw InvalidObjectException {
    throw new InvalidObjectException("Proxy required");
}

readResolve() の実装

最後に、readResolve() メソッド(項目77)を実装し、コンストラクタを使ってシリアライズ対象クラスのインスタンスを返します。 これでデシリアライズの時には、シリアライズ対象クラスが正しく返却されるようになります。

シリアライズプロキシパターンの利点はまさにここにあります。 この readResolve() メソッドでは、言語外のインスタンス生成機能を利用していません。 通常のコンストラクタ(または static ファクトリーメソッド)を利用して、インスタンスを生成しています。 これによって、通常のコンストラクタに実装されている不変式チェックを利用することができます。

実際の readResolve メソッドのコードは次のようになります。

private Object readResolve() {
    return new Period(start, end);
}

シリアライズ・プロキシ・パターンは防御的方法(項目76)と同様に偽りのバイトストリーム攻撃を防ぐことができます。 項目77で紹介したような内部フィールドの Steal 攻撃も防ぎます。

また、この方法を用いると他の手法と異なり、Period クラスを不変にすることができます。 これはとても重要な違いです。

EnumSet の実装例

このパターンの利点はもう一つあります。 これは、デシリアライズされた際のインスタンスとは異なるクラスのインスタンスを生成することができる点です。

EnumSet (項目32)では、Enum の要素の数によって異なるクラスのインスタンスが生成されます。 要素の数が 64 以下の場合には RegularEnumSet のインスタンスが生成され、64 を超過した場合には JumboEnumSet のインスタンスが生成されます。

もし、シリアライズ・プロクシ・パターンを使わないとすると、RegularEnumSet としてシリアライズされたストリームは、たとえ要素が増えても RegularEnumSet としてしかデシリアライズできません。 しかし、プロクシ・パーターンを使えば readResolve メソッド内で通常の static ファクトリーメソッドを利用できるため、要素に応じて異なるインスタンスを生成することができます。

実際、Java の EnumSet は次のように実装されています。

public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable

...(省略)...

    // シリアライズプロキシ
    private static class SerializationProxy <E extends Enum<E>>
        implements java.io.Serializable
    {
        private final Class<E> elementType;

        private final Enum<?>[] elements;

        SerializationProxy(EnumSet<E> set) {
            elementType = set.elementType;
            elements = set.toArray(ZERO_LENGTH_ENUM_ARRAY);
        }

        @SuppressWarnings("unchecked")
        private Object readResolve() {
            // elementType の Enum の数に応じて noneOf は異なるインスタンスを返す
            EnumSet<E> result = EnumSet.noneOf(elementType);
            for (Enum<?> e : elements)
                result.add((E)e);
            return result;
        }

        private static final long serialVersionUID = 362491234563181265L;
    }

    Object writeReplace() {
        // 書き込む際には SerializationProxy を書き込む
        return new SerializationProxy<>(this);
    }

    // Enum 自体はデシリアライズさせない。
    private void readObject(java.io.ObjectInputStream stream)
        throws java.io.InvalidObjectException {
        throw new java.io.InvalidObjectException("Proxy required");
    }
}

制限

シリアライズ・プロキシ・パターンも万能ではありません。

まず、クライアントによって拡張可能なクラスとは互換性がありません。

また、オブジェクトグラフが循環しているようなクラスでは利用できません。 シリアライズプロキシの readResolve からオブジェクトのメソッドを呼び出そうとしても、シリアライズ対象のクラスのインスタンスが復元できていないので、メソッドを呼び出すことができません。

また、通常のシリアライズよりもパフォーマンスが劣化することに注意してください。

感想

ついに終わった…。

最初の記事が 2015 年の 7 月。

hjm333.hatenablog.com

1年5ヶ月かかった。

Effective Java を解説しているブログはけっこうあるけれど、最後までたどり着いているブログはほとんど見受けられないので、そういう意味ではがんばったほうかな。

次の課題図書は何にしようかな~。

子育てエンジニアの一日(朝)

いつもとは志向を変えて自分の一日について書いてみる。

自分はソフトウェアエンジニア。結婚していて子供がいる。子供は一歳で、奥さんは働いている。いわゆる共働き家庭だ。

そんな人間のある日の一日。

07:00

起床する。子供はまだ寝ている。顔を洗い、髭を剃り、寝癖を直す。このタイミングでお風呂は洗っておく。夜帰宅したらすぐにお湯を張りたい。独身時代は毎朝シャワーを浴びていたけれど、いつのまにか辞めてしまったな。

次は子供のご飯の準備。ご飯は準備が簡単なものだ。火を使って調理したりはしない。並行して子供の服を選び、保育園の荷物を準備する。荷物はオムツ3枚、その日の着替1セット、エプロンとお手拭きタオル2セット。

子供を起こす時間だ。7:30。テレビをつけて教育チャンネルにしておく。幸い子供の寝起きは悪くない。5分ほど声をかけていると目覚めて自分でベッドを抜け出してくる。機嫌よくテレビを見始めるので、その間に服を着替えさせる。

子供にご飯を食べさせる。最近は自分で食べてくれるのでだいぶ楽になった。しかし、遊ばないように監視はしておかなければならない。彼は遊びたい年頃だ。監視を継続しつつ自分もご飯を食べる。一緒に子供ともぐもぐする。

奥さんは家を出て仕事に向かう。

08:00

8時になる頃、子供がご飯を食べ終わる。テレビでは『お母さんといっしょ』が始まる。洗濯が終わるので洗濯物を干す。雨の日は浴室乾燥機を使わなければならない。風呂に干す必要があるのでやや手間がかかる。洗濯を干し終わったら皿洗い。食洗機が欲しいとも思うが、賃貸なので設置するのは難しい。

子供のトイレの時間だ。子供をトイレに連れて行く。5分くらいでばっちりトイレしてくれるので助かる。子供の手を洗い、リビングに帰らせ、トイレを片付ける。褒めることを忘れてはいけない。トイレが上手にできたことを褒めちぎる。同時に自分の歯磨きもしておく。

8時半過ぎには家を出なければならない。それまでに少し時間があるので、英語の勉強をする。実際にはこの表現は間違っている。英語の勉強をしたいので少しだけ早めに起きて時間を確保している。子供に邪魔されつつも、なるべく集中するように努力する。

08:30

8時半だ。子供に靴下を履くように言う。いつのまにか自分で靴下を履けるようになった。靴下入れから靴下を取り出し、思考錯誤して靴下を履いている。なんとか履き終わって嬉しそうにしている。かかとの位置があってないので直してあげる。

出かける準備が整った。Evernote にメモした朝の家事チェックリストを確認する。暖房が付けっ放し、生協の箱を出したり忘れ、連絡帳の置き忘れ。過去に犯したミスがメモしてある。二度とやらないようにとメモしているのだが、それでも忘れ物をすることがある。人間は不思議だ。

家を出た。駅前の保育園に連れて行く。都心生活者の例に漏れず、我が家も保活戦争に巻き込まれている。0歳の早い段階から会社の近くの無認可保育所に電車通園し、ポイントを稼いだ。その甲斐あって今年度から最寄駅の保育園に通わせている。しかし、認可保育所ではなく小規模保育である。3歳になるタイミングで退園しなければならない。我が家の保活に終わり見えない。

保育園に着いた。子供の靴と靴下を脱がせ、荷物をロッカーに入れる。子供の体温を測り、熱がないことを確認する。もちろん、家でも測っているが改めて測りなおす。ここで 37.5 度以上の熱があると預かってもらえない。こういう場合は自宅にトンボ帰りし、会社を休まなければならない。過去、トンボ帰りしたことは4〜5回ある。

子供との別れを惜しむ。が、子供が別れを惜しむことはほとんどない。登園してすぐおもちゃで遊び始めている。こちらのことなど気にしていない。我が子ながらたくましい。0歳の早い頃から保育園に預けているからだろうか?「〇〇くん、ほらお父さんがバイバイって言ってるよ〜。〇〇くんはクールですねー。」と先生にフォローされながら、保育園を後にする。

09:00

さぁ準備は全て終わった。レディトゥーゴー。しかし、すでに一仕事を終えたような達成感に包まれている。そのままどこか遠くにいってしまいたくなる。しかし、そんな度胸は私にはない。そそくさと会社に向かう。今日も電車は遅延している。

長くなったので次回に続く。

【Effective Java】項目77:インスタンス制御に対しては、readResolve より enum 型を選ぶ

シングルトンのクラスをシリアライズする場合、readResolve メソッドを使ってインスタンス制御するよりも enum 型による実装を選ぶべきです。

readResolve メソッド

まず readResolve メソッドについて説明します。

例えば、次のようなシングルトンクラスを考えます。

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {
    }
}

この Elvis クラスをシリアライズできるようにしたいとします。

この時、単に implements Serializable としただけではシングルトンとして不完全です。 デフォルトシリアライズ形式かカスタムシリアライズ形式かに関係なく、シングルトンとして不適切な実装です。

なぜなら、Serializable を implements したクラスの readObject メソッドは常に新しいインスタンスを生成するからです。 このインスタンスpublic static final Elvis INSTANCE = new Elvis(); で生成されているインスタンスとは異なるため、シングルトンにおけるインスタンスの唯一性を満たすことができません。

これに対し readResolve メソッドを使うと、readObject によって生成されたインスタンスを交換することができます。

シリアライズされたクラスが readResolve を持っている場合、次のように機能します。

  • readObject() によって新たなインスタンスが生成される
  • そのインスタンスに対して readResolve() が呼び出される
  • このメソッドが返すオブジェクトが、新たに生成されたオブジェクトの代わりとなる
    • この際、元々のオブジェクトへの参照は保持されません。

Elvis クラスの場合、次のようにしてシングルトン特性を持つ readResolve を実装できます。

private Object readResolve() {
    // 唯一の正しいインスタンスを返す
    return INSTANCE;
}

ここで注意が必要です。

インスタンス制御を行うクラスではすべてのプロパティは transient と宣言する必要があります。 そうしない場合、次の項目で説明する攻撃を用いると、シングルトンの特性である「インスタンスが一つしかない」という不変性を破ることができてしまうからです。

Stealer 攻撃

Serializable を実装しているシングルトンが非 transient のプロパティを含んでいる場合、シングルトン特性を破壊する攻撃が可能です。 この攻撃は少し複雑です。

シングルトンが非 transient のプロパティを含んでいる場合、シングルトンの readResolve() が実行される前に、その非 transient のプロパティがデシリアライズされます。 プロパティがデシリアライズされる時、そのプロパティの readObject() が呼び出されます。

この時、シングルトンのダミーのインスタンスを保持しておけば、INSTANCE とは別のインスタンスを保持したままにできます。

サンプルコード

具体的なコードを見ていきます。 まず、非 transient なプロパティを含む不完全なシングルトン Elvis クラスです。

// 非 transient プロパティを含む不完全なシングルトン実装
public class Elvis implements Serializable {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {
    }
 
    // 非 transient なプロパティ
    private String[] favoriteSongs = {"Hound Dog", "Heartbreak Hotel"};

    public void printFavorites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }

    private Object readResolve() {
        return INSTANCE;
    }
}

次にこの不完全なシングルトンコードを攻撃するコードです。

// Elvis クラスを攻撃するクラス
public class ElvisStealer implements Serializable {
    // シングルトン以外のインスタンスを保持しておくための static フィールド
    static Elvis impersonator;

    // もう一つの Elvis インスタンス
    private Elvis payload;

    private Object readResolve() {
        // payload に保持されている Elvis インスタンスを impersonator に保持しておく
        impersonator = payload;

        // このプロパティを文字列配列として偽装するので文字列配列を返す
        return new String[]{"A Fool Such as I"};
    }

    private static final long serialVersionUID = 0;
}

そして、Elvis をシリアライズしたバイトストリームを改変します。 favoriteStrings のプロパティ領域には本来、String[] 型のインスタンスが含まれていますが、これを ElvisStealer に変更します。

こうすると、Elvis クラスは favoriteStrings をデシリアライズしているつもりで、ElvisStaler をディシリアライズしてしまうのです。

改変したバイトストリームと、それを利用してインスタンスを二つ得るコードは次の通りです。

public class ElvisImpersonator {
    // 改変した Elvis ストリーム
    private static final byte[] serializedForm = new byte[]{
            (byte) 0xac, (byte) 0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x05,
            0x45, 0x6c, 0x76, 0x69, 0x73, (byte) 0x84, (byte) 0xe6,
            (byte) 0x93, 0x33, (byte) 0xc3, (byte) 0xf4, (byte) 0x8b,
            0x32, 0x02, 0x00, 0x01, 0x4c, 0x00, 0x0d, 0x66, 0x61, 0x76,
            0x6f, 0x72, 0x69, 0x74, 0x65, 0x53, 0x6f, 0x6e, 0x67, 0x73,
            0x74, 0x00, 0x12, 0x4c, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c,
            0x61, 0x6e, 0x67, 0x2f, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74,
            0x3b, 0x78, 0x70, 0x73, 0x72, 0x00, 0x0c, 0x45, 0x6c, 0x76,
            0x69, 0x73, 0x53, 0x74, 0x65, 0x61, 0x6c, 0x65, 0x72, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01,
            0x4c, 0x00, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
            0x74, 0x00, 0x07, 0x4c, 0x45, 0x6c, 0x76, 0x69, 0x73, 0x3b,
            0x78, 0x70, 0x71, 0x00, 0x7e, 0x00, 0x02
    };

    public static void main(String[] args) {
        Elvis elvis = (Elvis) deserialize(serializedForm);
        Elvis impersonator = ElvisStealer.impersonator;
        elvis.printFavorites();
        impersonator.printFavorites();
    }

    private static Object deserialize(byte[] stream) {
        try {
            InputStream inputStream = new ByteArrayInputStream(stream);
            ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
            return objectInputStream.readObject();
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }

    }
}

このコードを実行すると、2つの Elvis インスタンスが存在していることが分かります。

[Hound Dog, Heartbreak Hotel]
[A Fool Such as I]

enum シングルトン

すべてを transient なプロパティに変更することで上記の攻撃は回避することができます。 しかし、よりよい方法は enum のシングルトンを使うことです。

enum のシングルトンについてはすでに項目3で述べました。 enum シングルトンは、通常のシングルトンパターンと違い、JVM によってインスタンスの唯一性が保証されるため非常に有利です。 また、enum はデフォルトでシリアライズ可能になっていることも重要です。

Elvis クラスを enum にすると次のようになります。

public enum Elvis {
    INSTANCE;
    private String[] favoriteSongs =
            {"Hound Dog", "Heartbreak Hotel"};

    public void printFavorites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }
}

しかし、コンパイル時にインスタンスが分からないようなシリアライズ可能なシングルトンを書くためにはenum 型は使えません。

継承

final のクラスに readResolve() を書く際は private であるべきです。 一方、final でないクラスに readResolve() を書くときはそのアクセス可能性を検討する必要があります。

readResolve() が protected か public でサブクラスが存在してオーバーライドしていない場合、readResolve() がスーパークラスを返すことになるため ClassCastException を起こしやすいので注意が必要です。

感想

ついにあと1項目。 もっと感慨深いかと思ったけど、特にそんなこともなかった。。。

【Effective Java】項目76:防御的に readObject を書く

ストリームからオブジェクトをデシリアライズする readObject メソッドは実質的に public コンストラクタとして機能します。 そのため、クラスのコンストラクタで検査している正当性や不変式を readObject にも実装する必要があります。

Period クラスの例

項目39では、開始日時と終了日時の組み合わせを示すための Period クラスを実装しました。 Period インスタンスは次の不変性があり、どんな Period インスタンスも次の条件を満たすことが保証されています。

start プロパティは end プロパティ以前の日時を示している

public class Period {
    private final Date start;
    private final Date end;

    /**
     * @param start 期間の開始
     * @param end   期間の終わり。開始より前であってはならない。
     * @throws IllegalArgumentException start が end の後の場合。
     * @throws NullPointerException     start か end が null の場合
     */
    public Period(Date start, Date end) {
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());

        if (this.start.compareTo(this.end) > 0) {
            throw new IllegalArgumentException(this.start + " after " + this.end);
        }
    }

    public Date getStart() {
        return new Date(start.getTime());
    }

    public Date getEnd() {
        return new Date(end.getTime());
    }
}

Period クラスをシリアライズするためには単に Serializable インタフェースを実装するだけです。 しかし、その方法では Period クラスの不変性を破る不正な Period インスタンスを生成することが可能になってしまいます。

項目39 ではコンストラクタとアクセッサにおいて防御的コピーを実装し、クラスの不変式が破られないように努力しました。 この努力が readObject メソッドにも必要になります。

不変式のチェック

単に Serializable を implements しただけではバイトストリームを改変することによって、簡単に Period クラスの不変性を突破することができます。 シリアライズによってバイトストリーム化された Date のバイナリに直接手を加え、Date インスタンスの値を改変します。

この攻撃を防ぐためには readObject メソッドで不変式をチェックし、InvalidObjectException をスローするようにします。

// 不変式をチェックする readObject メソッド
private void readObject(ObjectInputStream stream)
        throws IOException, ClassNotFoundExcpetion {
    stream.defaultReadObject();

    if (start.compareTo(end) > 0) {
        throw new InvalidObjectException(start + " after " + end);
    }
}

防御的コピー

不変式のチェックによって不正な Period インスタンスの生成を防ぐことができます しかし、これでは防御が不完全です。

正当な Period インスタンスを含むバイトストリームのあとに、Period インスタンスの private な Date フィールドへの参照を追加したバイトストリームを作ると可変な Period インスタンスを生成できてしまいます。

具体的には次のようなコードになります。

public class MutablePeriod {
    public final Period period;

    public final Date start;
    public final Date end;

    public MutablePeriod() {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bos);
            // Period インスタンスの書き込み
            out.writeObject(new Period(new Date(), new Date()));
            // Period インスタンスの特定のプロパティへの参照を作成
            byte[] ref = {0x71, 0, 0x7e, 0, 5};
            bos.write(ref);
            ref[4] = 4;
            bos.write(ref);

            // 可変な Period インスタンスの生成
            ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
            period = (Period) in.readObject();
            start = (Date) in.readObject();
            end = (Date) in.readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new AssertionError(e);
        }
    }
}

この MutablePeriod に対して、次のようなコードを実行すると簡単に Period の start と end を書き換えることができます。

MutablePeriod period = new MutablePeriod();
period.start.setTime(1);
period.end.setTime(0);

これを防ぐため項目39で用いた防御的コピーを利用します。

private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
        stream.defaultReadObject();

        start = new Date(this.start.getTime());
        end = new Date(this.end.getTime());

        if (this.start.compareTo(this.end) > 0) {
            throw new InvalidObjectException(this.start + " after " + this.end);
        }
    }

正当性検査を行う前に防御的コピーが行われることに注意してください。

また、防御的コピーを行うと、フィールドを final にすることはできません。 これは望ましいものではないかもしれませんが、攻撃を許す状態にしておくよりはだいぶましです。

writeUnshared, readUnshared

このようなオブジェクト参照による攻撃への対策として Java 4 で writeUnshared と readUnshared が追加されています。 しかし、これらは項目77で解説する、高度な攻撃に対して脆弱性を持っているので利用しないでください。

基準

防御的に readObject を書くかどうかは、同様のコンストラクタを不変式のチェックなしで公開するかどうか?という視点で考えてください。 もし、そのようなコンストラクタを書くことに不安があるならば、防御的に readObject を書く必要があります。

readObject を書くことはパブリックなコンストラクタを追加していると考えてください。 引数のバイトストリームは正当な物とは限りませんし、実際にシリアライズされたインスタンスである保証はありません。

【Coursera】Learning How To Learn を受講した

ひさしぶりに Coursera を受講した。

Learning How To Learn という講義。

https://www.coursera.org/learn/learning-how-to-learn

脳科学や心理学をもとにした学習のやり方を教えてくれる講義。

具体的には、

  • 毎日少しづつ勉強した方が長期記憶に残りやすい
  • 脳には集中モードと発散モードがあり、集中して勉強するだけではなく発散モードもうまく使うべき
  • 集中するためにはポモドーロを使おう
  • 学習の結果を意識をすると脳の痛みの分野が反応するので、学習のプロセスに意識を置こう
  • イメージと結びつけると長期に記憶しやすくなる。部屋に配置するようなイメージで記憶しよう

など、なんとなくどこかで聞いたことあるようなテクニックがいくつか紹介されている。

特に脳の発散モード(diffuse mode)という考え方が自分にとって画期的で、最近は仕事で意識的に取り入れてる。

集中モードと発散モード

脳には集中モード(focused mode)と発散モード(diffuse mode)があり、学習においてどちらも重要な側面を担っている。 集中モードでは脳は局所的にニューロンを発火させる。 発散モードでは脳は遠くの離れたニューロン同士を発火させる。

集中モードでは特定の箇所のニューロン同士が激しく反応するため、特定の物事を深く考え理解するのに向いている。 ちょうどピンボールで釘が敷き詰められた箇所をボールが通って激しく動きまわっているようなイメージ。 一方、発散モードでは遠くのニューロン同士が緩く反応するので柔軟で新しい発想を生むのに向いている。 これはピンボールで釘が互いに離れた場所でピンボールがゆるく通過していくイメージ。

コーディングやデバッグでは基本的に脳は集中モードになっている。 しかし、一度詰まってしまうと、集中モードでいる限り新たな視点は得られないので時間の無駄になってしまう。 そこで、そうなってしまったら脳を発散モードにうつす努力をする。 具体的にはコンビニに出かけてジュースを買ったり10分ほど外を散歩したりする。

これが実際かなり効果がある。 画期的なアイディアで劇的に解決!ということはほとんどないのだが、「ああ、こっちの条件で挙動確認すればいいのか」と違う視点で問題を見ることができるようになる。そうして自席に戻って30分くらい作業しているとすすっと問題が解決していたりする。

最近はこの方法を積極的に使うことでコーディング、デバッグ、少し大きめのタスクであっても大ハマリすることなく仕事ができている。 まあ、人によっては勤務中に外に散歩したりできないかもしれないが、席を立ったりするだけでだいぶ違うと思う。

思い返せば CODE COMPLETE でも同じようなことが書いてあったし、大学時代の教授も同じことを言っていた。 確かバートランド・ラッセルも何かの著書で無意識の能力について言及していた(気がする)。 きっと、気づいている人は積極的に使っているテクニックなのだろう。

感想

というわけで、たった4週しかない講義だったけど、使った時間の割には得たものは大きかった。

本来は学部生向けの講義なのだろうけど、社会人でも十分に役立つと思う。 現代で仕事をしていくには、どんな分野であれ素早く新しいことを学習していくことが求められているだろうから。

しかし、Coursera の講義を受講するたびに『10年前の学生時代にこういう講義を受講したかったなぁ』と強く思うなぁ。

【Effective Java】項目75:カスタムシリアライズ形式の使用を検討する(後半)

前回の記事ではデフォルトシリアライズとカスタムシリアライズの概要について説明しました。

hjm333.hatenablog.com

本記事では、カスタムシリアライズ実装の注意事項について説明します。

StringList のカスタムシリアライズ実装を再掲します。

public class StringList implements Serializable {
    private transient int size = 0;
    private transient Entry head = null;

    private static class Entry implements Serializable {
        String data;
        Entry next;
        Entry previous;
    }

    /**
     * この{@code StringList}インスタンスをシリアライズする。
     *
     * @serialData リストのサイズ({@code int})を書き出して、
     * 適切な順番にすべての要素({@code String})が続くようにする
     */
    private void writeObject(ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        s.writeInt(size);
        for (Entry e = head; e != null; e = e.next) {
            s.writeObject(e.data);
        }
    }

    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        int num = s.readInt();
        for (int i = 0; i < num; i++) {
            add((String) s.readObject());
        }

    }
}

注意点

transient フィールド

StringList のカスタムシリアライズ実装では private フィールドに transient 修飾子が指定されています。

transient 修飾子は、デフォルトのシリアライズ対象からそのフィールドを除外することを指定します。 すなわち、カスタムシリアライズ実装では StringList のすべての private フィールドは自動的にはシリアライズされなくなります。

デフォルトシリアライズを利用するかどうかに関わらず、defaultWriteObject を呼び出すと transient 指定されたフィールド以外のフィールドはすべてシリアライズされます。 無用なフィールドのシリアライズを避けるために、可能な限りフィールドは transient 指定するべきです。

特に transient を指定するべきフィールドとして、

  • データフィールドから値を計算できるフィールド(キャッシュなど)
  • ネイティブのデータ構造へのポインタを表すフィールド
  • 特定の JVM の1回の実行にひもづくフィールド

があります。

基本的にすべてのフィールドを transient 指定しようと考えてから、『このフィールドは transient を外そう』と考えるようにします。 この時、本当に transient でなくてよいかはよく確認してください。

transient フィールドのデフォルト値

transient 修飾子を指定した場合、デシリアライズされた際のフィールド値は、その型のデフォルト値になります。 オブジェクトならば null、boolean なら false、int ならば 0 です。

これらの値が受け入れられない場合、値を適切に設定する readObject メソッドを実装しなければなりません。 デシリアライズは言語外の仕組みを使うので、コンストラクタによる不変式の強制は利用できません。

defaultReadObject

StringList のフィールドはすべて transient 修飾子が指定されています。

すなわち、デフォルトシリアライズシリアライズされるフィールドはないはずです。 それにも関わらず writeObject 内で defaultWriteObject() を呼び出し、さらに readObject で defultReadObject() を呼んでいることに注意していください。

この呼び出しをすることで、後のリリースで StringList に transient ではないフィールドを追加することを可能にします。

もし、新しいバージョンで transient ではないプロパティが追加されて古いバージョンでデシリアライズした際、仮に defaultReadObject() を呼んでいないとしたら StreamCorruptedException が発生してしまいます。

@serialData

writeObject() は private メソッドにも関わらず、ドキュメンテーションコメントがつけられていることに注意してください。

writeObject() はシリアライズ形式を示しています。そして、シリアライズ形式は公開 API の一部です。 そのため、@serialData タグを用いてシリアライズされるデータの定義を書かなければなりません。

シリアライズが不適切な場合

前回述べたように StringList をデフォルトシリアライズにすることはいくつかの点で不適切です。 しかし、StringList のデフォルトシリアライズは『元のオブジェクトを正しくシリアライズし、デシリアライズによって正しく復元できる』という意味では正しいものです。

一方、デフォルトシリアライズを選ぶと、明確にエラーを引き起こすクラスがあります。

たとえばハッシュテーブルでは、デフォルトのシリアライズは機能しません。 なぜなら、キーとなるハッシュ値が、異なる JVM 間では違う可能性があります。 さらにいうとプログラムの実行ごとにハッシュ値が異なる可能性すらあります。

そのため、ハッシュテーブルにおけるデフォルトシリアライズによってハッシュ値シリアライズされると、デシリアライズした際に不変式が破られている可能性があります。

synchronized

もし、オブジェクトが同期によってスレッドセーフを実現しているならば、 writeObject メソッドは synchronized にする必要があります。

シリアルバージョン UID

Serializable を実装する場合、すべてのクラスで明示的なシリアルバージョン UID を定義してください。

シリアルバージョン UID は次のように宣言します。

private static final long serialVersionUID = randomLongValue;

明示的にシリアルバージョン UID を宣言すると、クラスのシリアライズに関する互換性を保つことができます。 これを宣言しない場合、シリアルバージョン UID はクラスの名前やフィールドを元にして自動生成されます。 すなわち、フィールド一つ追加しただけで、シリアルバージョン UID が変わってしまい、互換性が失われてしまいます。

また、明示的にシリアルバージョン UID を宣言しないと、実行時に UID を生成するためコストの高い計算が必要となります。

randomLongValue の値はどのような値でも問題ありませんが、serialvar ユティリティを実行すると値を生成できます。 最近の IDE には seiralVersionUID 生成機能が含まれています。

もし、クラスの古いバージョンにシリアルバージョン UID がついていない場合、新しいバージョンには古いバージョンのシリアルバージョン UID 自分で計算して、付け加える必要があります。

もし、異なるシリアルバージョン UID を持つクラスをデシリアライズしようとすると InvalidClassException が発生します。

(c) The King's Museum