The Cat Ate My Source Code【The Pragmatic Programmer】
明けましておめでとうございます。 2017年もすでに10日ほど経過してますが…。
一年半くらい書いてきた Effective Java シリーズが終わってしまって、イマイチ継続的にブログを書く感じにならない。 やっぱり、何かをシリーズ化してノルマにすることがブログを継続するコツですね。習慣付いてるとシリーズ以外のネタを書くことへの心理的障壁も下がるし。
The Pragmatic Programmer
以前から The Pragmatic Programmer は一度読んでみたいと思っていた。ちょうど rebuild で話題になってたし、これを読んで1章ずつまとめてみようと思う。rebuild では「古い部分が多い」という話だったので、なるべく「今ならどーするか」みたいな話を入れていきたい。
The Pragmatic Programmer は日本語版が pdf で読めるようになってるんだけど、残念ながら Kindle にはなかった。今回は、英語の勉強も兼ね原著を読もうと思う。(しかし、Kindleで4000円は高いと感じてしまうな…)
The Pragmatic Programmer: From Journeyman to Master
- 作者: Andrew Hunt,David Thomas
- 出版社/メーカー: Addison-Wesley Professional
- 発売日: 1999/10/20
- メディア: ペーパーバック
- 購入: 1人 クリック: 18回
- この商品を含むブログ (7件) を見る
The Cat Ate My Source Code
1章は Take Responsibility について述べてる。簡単にまとめると、
- Take Responsibility しろ
- 失敗と無知を認めることを恐れるな
- クソな言い訳をするな
- 失敗した時のために対案をもっておけ
- 無理なら断れ
というところか。言ってることは至極真っ当である。うんうん、そうだよね。誠実さ。大事だよね。
でも、ここにたどり着くために実践するべき日々の行動をもう少し書いてくれれば、と感じた。あるべき姿だけ書かれていると大抵の人が「うんうん、そうだよね。誠実さ。大事だよね。」で終わってしまうわけだ。たとえば、「アクション:明日から自分のミスを毎日一つ書いて同僚と共有してみよう!」のように書いてあると少し違うのではないか。
こういう点は『Soft Skills』の方が手厚い印象を受ける(同じく『情熱プログラマ』も)。『Soft Skills』だと、"Chapter 10. Being a professional" でまさに同じことが書かれているし、取るべきアクションについても書かれているし。
- 作者: ジョン・ソンメズ,まつもとゆきひろ(解説),長尾高弘
- 出版社/メーカー: 日経BP社
- 発売日: 2016/05/20
- メディア: 単行本
- この商品を含むブログ (5件) を見る
- 作者: Chad Fowler,でびあんぐる
- 出版社/メーカー: オーム社
- 発売日: 2010/02/26
- メディア: 単行本(ソフトカバー)
- 購入: 24人 クリック: 683回
- この商品を含むブログ (126件) を見る
もちろん、『Soft Skills』や『情熱プログラマ』は The Pragmatic Programmer に多くの影響を受けている、と明確に書かれているので、元ネタを基にしてさらによい方法で書かれているのは当然なのだろうけれど。
そして、まだ一つしか読んでないのでこう判断するのは早いのかもしれない。もしかしたらこの後に出てくるのかもしれないし。
次章は "Software Entropy"。
見積もりのずれ
最近、見積もりをしてから仕事に取り掛かるようにしている。プロのソフトウェアエンジニアとして見積もり能力は重要だと思う(たぶん)。「それ、簡単にできますよ(どやぁ」と放言して、できたのは半年後なんてのはプロとはいえない。
ちゃんと見積もって実績を評価してみたら、見積もりのずれは作業の遅れではなく、作業時間の不足から起こりがちだなと気づいたというお話。
見積もりの仕方
自分は次のように見積もりをする。
- 直感で全体の見積もり:「まあ、この機能なら1ヶ月くらいで出せるでしょ」
- 作業項目への分割:「作業的には、既存実装の調査、モデル側実装、ビュー側実装、テスト、バグ修正、リリース作業、かな。」
- 時間単位で見積もり:「これは2時間くらい、これは6時間くらい、あーこれは重そうだから16時間くらいかかりそう…」
- 足し算:「全部で120時間か…」
- 遅延係数1.5をかける:「1.5 をかけると180時間か…1ヶ月じゃおわんねーな」
脇道にそれるが、何を持って「作業終了」とするかは事前にマネジメント側と認識をあわせておくべき。しかし、「自分の作業物に一切手を加えないでプロダクション環境にリリースできる状況」を作業終了としておけばほとんどの場合でマネジメント側と認識がずれることはない。すなわち、設計、実装、コードレビュー、再修正、テスト、テストバグ修正、リリース準備とリリース、をすべて作業に含んでおく。
遅延係数
ほとんどのソフトウエアエンジニアは楽観的で自分の能力を過大評価し、作業の難易度を甘く見る。例に漏れず自分も楽観的である。「金曜までに終わるな」と思った作業は、大抵、次の週の半ばにリリースされる(or マージされる)。さすがに5年くらい仕事してると、こういう自分の性質を理解しているので、最終的な見積もりを出す時には直感の値に係数1.5をかける。
ちなみに 1.5 をかけると自分の直感と比較してだいぶスケジュールが伸びることになる。直感と反するので不安になる。こんなにも時間がかかるなんて無能だと評価されないだろうか…。しかし、仕事は信頼感の方が大事。見積もりには不安を抑える力も必要だ。
仕事は早いに越したことはない。しかし、できると言った期間にできてないのは困る。もし、早く終わらせたいのであれば、作業量を調整するかより腕の立つエンジニアを連れて来てもらうしかない。これはマネジメント層の問題であり、ソフトウェアエンジニア個人にはどうしようもない。
遅延係数の正体
さて、ようやく本題。
この遅延係数 1.5 の正体は何だろうか。この 1.5 は自分の能力の過信率だと考えていた。すなわち、自分は自分の能力を 1.5 倍過信している。直感による見積もりでは、作業をその分だけ過小評価している。と。
しかし、そうではなかった。
自分の作業にはポモドーロを導入している。これによって明確に集中した作業時間を計測することができるようになった。週のポモドーロ総数は大抵 40〜45 だ。調子のいい日は 12 程度確保できるが、調子が悪かったり打ち合わせが入ると 5〜6 だったりする。
この実績ポモドーロ数と作業見積もりの値を比較すると、実は見積もりの値は正確であることがわかった。すなわち、5〜6 時間を見積もったタスクは 10 ポモドーロ程度かかっている。ということは、この遅延係数は自分の作業自体の遅延ではない。
実は、見積もった値を日にちに変換する際に問題があった。8 時間勤務で、オーバーヘッド含めても7時間程度は作業できると考え、7時間で一日に変換していた。しかし、7時間の作業といえば14ポモドーロである。こんなにも集中して作業するのはほぼ無理だ。実測は平均で8~9である。実際、4~5時間がいい線である。
これが遅延係数の正体だ。
まとめ
結局のところ、ちゃんと集中して作業する時間を確保することこそが、遅延を回避する一番の方法だということだ(自分にとってね)
【Effective Java】各項目のまとめ
Effective Java シリーズの各項目の一覧。
第1章:はじめに
第2章:オブジェクトの生成と消滅
- 項目1:コンストラクタの代わりに static ファクトリーメソッドを検討する
- 項目2:数多くのコンストラクタパラメータに直面した時にはビルダーを検討する
- 項目3:private のコンストラクタか enum 型でシングルトン特性を強制する
- 項目4:private のコンストラクタでインスタンス化不可能を強制する
- 項目5:不必要なオブジェクトの生成を避ける
- 項目6:廃れたオブジェクト参照を取り除く
- 項目7:ファイナライザを避ける
第3章:すべてのオブジェクトに共通のメソッド
- 項目8:equals をオーバーライドするときは一般契約に従う
- 項目9:equals をオーバーライドする時は、常に hashCode をオーバーライドする
- 項目10:toString を常にオーバーライドする
- 項目11:clone を注意してオーバーライドする
- 項目12:Comparable の実装を検討する
第4章:クラスとインタフェース
- 項目13:クラスとメンバーへのアクセス可能性を最小限にする
- 項目14:public のクラスでは、public のフィールドではなく、アクセッサーメソッドを使う
- 項目15:可変性を最小限にする
- 項目16:継承よりコンポジションを選ぶ
- 項目17:継承のための設計および文書化する、でなければ継承を禁止する
- 項目18:抽象クラスよりインタフェースを選ぶ
- 項目19:型を定義するためだけにインタフェースを利用する
- 項目20:タグ付クラスよりクラス階層を選ぶ
- 項目21:戦略を表現するために関数オブジェクトを利用する
- 項目22:非 static のメンバークラスより static のメンバークラスを選ぶ
第5章:ジェネリックス
- 項目23:新たなコードで原型を使用しない
- 項目24:無検査警告を取り除く
- 項目25:配列よりリストを選ぶ
- 項目26:ジェネリック型を使用する
- 項目27:ジェネリックメソッドを利用する
- 項目28:API の柔軟性向上のために境界ワイルドカードを使用する(その1)
- 項目28:API の柔軟性向上のために境界ワイルドカードを使用する(その2)
- 項目29:型安全な異種コンテナーを検討する
(以下、ジェネリックスの補足記事)
第6章:enum とアノテーション
- 項目30:int 定数の代わりに enum を使用する
- 項目31:序数の代わりにインスタンスフィールドを利用する
- 項目32:ビットフィールドの代わりに EnumSet を使用する
- 項目33:序数インデックスの代わりに EnumMap を使用する
- 項目34:拡張可能な enum をインタフェースで模倣する
- 項目35:命名パターンよりアノテーションを選ぶ
- 項目36:常に Override アノテーションを利用する
- 項目37:型を定義するためにマーカーインタフェースを使用する
第7章:メソッド
- 項目38:パラメータの正当性を検査する
- 項目39:必要な場合は、防御的にコピーする
- 項目40:メソッドのシグニチャを注意深く設計する
- 項目41:オーバーロードを注意して利用する
- 項目42:可変長引数を注意して使用する
- 項目43:null ではなく空配列か空コレクションを返す
- 項目44:すべての公開 API 要素に対してドキュメントコメントを書く
第8章:プログラミング一般
- 項目45:ローカル変数のスコープを最小限にする
- 項目46:従来の for ループより for-each を選ぶ
- 項目47:ライブラリーを知り、ライブラリーを使う
- 項目48:正確な答えが必要ならば、float と double を避ける
- 項目49:ボクシングされた基本データより基本データを選ぶ
- 項目50:他の型が適切な場所では、文字列を避ける
- 項目51:文字列結合のパフォーマンスに用心する
- 項目52:インタフェースでオブジェクトを参照する
- 項目53:リフレクションよりインタフェースを選ぶ
- 項目54:ネイティブメソッドを注意して使用する
- 項目55:注意して最適化する
- 項目56:一般的に受け入れられている命名規約を守る
第9章:例外
- 項目57:例外的状態にだけ例外を使用する
- 項目58:回復可能な状態にはチェックされる例外を、プログラミングエラーには実行時例外を使用する
- 項目59:チェックされる例外を不必要に使用するのを避ける
- 項目60:標準例外を使用する
- 項目61:抽象概念に適した例外をスローする
- 項目62:各メソッドがスローするすべての例外を文書化する
- 項目63:詳細メッセージにエラー記録情報を含める
- 項目64:エラーアトミック性につとめる
- 項目65:例外を無視しない
第10章:平行性
- 項目66:共有された可変データへのアクセスを同期する(前半)
- 項目66:共有された可変データへのアクセスを同期する(後半)
- 項目67:過剰な同期は避ける
- 項目68:スレッドよりエグゼキューターとタスクを選ぶ
- 項目69:wait と notify よりコンカレンシーユティリティを選ぶ(前半)
- 項目69:wait と notify よりコンカレンシーユティリティを選ぶ(後半)
- 項目70:スレッド安全性を文書化する
- 項目71:遅延初期化を注意して使用する
- 項目72:スレッドスケジューラに依存しない
- 項目73:スレッドグループを避ける
第11章:シリアライズ
- 項目74:Serializable を注意して実装する
- 項目75:カスタムシリアライズ形式の使用を検討する(前半)
- 項目75:カスタムシリアライズ形式の使用を検討する(後半)
- 項目76:防御的に readObject を書く
- 項目77:インスタンス制御に対しては、readResolve より enum 型を選ぶ
- 項目78:シリアライズされたインスタンスの代わりに、シリアライズ・プロキシを検討する
EFFECTIVE JAVA 第2版 (The Java Series)
- 作者: Joshua Bloch,柴田芳樹
- 出版社/メーカー: 丸善出版
- 発売日: 2014/03/11
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (11件) を見る
Kindle 版はこちら(ただし英語)
Effective Java: A Programming Language Guide (Java Series)
- 作者: Joshua Bloch
- 出版社/メーカー: Addison-Wesley Professional
- 発売日: 2008/05/08
- メディア: Kindle版
- この商品を含むブログを見る
『人を動かす』を読んだ
なるべく読んだ本の感想は書いておこう。今までは書いたり書かなかったりだったから。
最近、『人を動かす』を読んだ。
- 作者: D・カーネギー,山口博
- 出版社/メーカー: 創元社
- 発売日: 2016/01/26
- メディア: 単行本
- この商品を含むブログ (8件) を見る
相手のことを考える
自己啓発の元祖ともいえるこの本。もちろん存在は知っていた。ここ最近読んだ本で立て続けにオススメされていたので重い腰を上げて読む。Kindle なら1000円以下だし。社会人たるものカーネギー本の一冊くらいは読んでおかないと。
購入したのは8月末。時間が取れず、読み終わるのに数ヶ月かかってしまった。面白い本ならどんどん読み進むのでやはりそれほど面白くはなかったのだろう。
やたら成功事例ばかり挙げられているので飽きてくる。そしてそれがだんだん胡散臭さへと変わる。リンカーンやカーネギー(鋼鉄王の方)のエピソードが至るところで紹介される。それにくわえて筆者のセミナーの受講者の成功例。さすがに食傷気味になる。ほんとにこんなうまくいくのか?と言いたくなる事例が多々。
それでも得るものはあったと思う。この本の教えは『相手のことを考える』という点に尽きる。この教えは簡単なようでとても難しい。実践してみると、日々の生活でいかに自分が相手のことを考えず会話しているかを思い知らされた。この本で紹介されるテクニックを使ってみよう!しかし、普段からやってないのでとても難しい。脳にパターンがまったくインストールされていない。
How to win friends
過去に読んだ『インテル経営の秘密』や『あなたのチーム、機能してますか?』とは真逆の教えもある。この本は議論は徹底的に回避しろと説く。一方、『あなたのチーム、機能してますか?』では「徹底的に議論しろ。しかし、徹底的に持論しても関係が崩れない信頼をお互い持て」と説く。徹底的に議論することで問題に対する理解を深め、最適な方法を見つけようという姿勢が見て取れる。
すこし斜めからこの本を眺めると『相手をおだてて動かそう』といってるように見えてくる。もしかしたら、そういう側面もあるのかもしれない。なぜなら、原題は How to win friends and influence people (友に勝ち、他人に影響を与える方法)なのだから。
嫁を動かす
僕が読んだのは『文庫版』。もう一つ『新装版』というエディションがあるようだ。『新装版』には「幸福な家庭をつくる七原則」という付録が含まれている。家庭を持つ僕にとってはこちらの方がよっぽど大事だったのでは…。
- 作者: デールカーネギー,Dale Carnegie,山口博
- 出版社/メーカー: 創元社
- 発売日: 1999/10/31
- メディア: 単行本
- 購入: 174人 クリック: 3,319回
- この商品を含むブログ (618件) を見る
ぐぐると『嫁を動かす(HOW TO WIN WIFE AND INFLUENCE PEOPLE)』なんてブログもでてきて、ますます『新装版』を買わなければならない衝動にかられている。
【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 月。
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項目。 もっと感慨深いかと思ったけど、特にそんなこともなかった。。。