~saiya/hatenablog

No Code, No Life.

2つの封筒問題(パラドックス)の無限や極限値の問題としての考察

確率系のパラドックスで有名らしい "2つの封筒問題" なるものを知ったが、これは本質的には確率の問題というよりも無限や極限値にまつわる問題ではないか?ということに気づいたので、ぐぐって出て来る色々な解説とは少し違う観点でそのパラドックスのトリックを考察してみた。

Disclaimer: 筆者は数学のプロではないので、論理の曖昧さや穴がある可能性は大いにありえます。無限の扱い・理論はとてもむずかしい...。

2つの封筒問題とは

ぐぐると色々なページが出てきますが、いずれも以下のような内容:

2つの封筒がある。片方の封筒には、もう一方の2倍のお金が入っている(たとえば1万円と2万円など)。

プレイヤーはどちらかの片方だけの封筒をもらえる。

さらにプレイヤーはどちらか片方の封筒をあけて中身を見て、その上でどちらの封筒を取るか決めることができる。

片方の封筒をあけて中身を見たとき、封筒を変えた方が得か?損か?変わらないか?

 

Aさんは次のように考えた。

開けた封筒に1万円が入っていたとすると、もう一方の封筒には5千円か2万円がそれぞれ確率1/2で入っている。

よってもう一方の封筒に入っている金額の期待値は12,500円なので、封筒を変えた方が得。封筒にいくら入っていようと同じ計算ができるので、結局封筒を変えた方が得である。

 

一方Bさんは、どちらの封筒も区別できないのだから、金額が大きいほうの封筒を引く確率は1/2で変わらないと考えた。

 

AさんとBさんの言い分はどっちが正しいのか?それとも2人とも間違っているのか?

一見すると確率についてのパラドックスのように見えるが、実はこのパラドックスには無限を使ったトリック(と読者に無意識に仮定を持たせる叙述トリック)がある、というのが本稿の内容。

( 上記http://tasusu.hatenablog.com/entry/20100606/1275832157 より引用 )

TL;DR

  1. 出題者側が封筒に  +\infty のお金を詰めるのが期待値、という結果を招く暗黙の前提がこのパラドックスのキモである。
    • 「出題者側が封筒に詰める金額の期待値」が  +\infty となるロジックになっている
    • 上記を是とするならば、実は常に封筒を変えた方が得という結論は正しい
  2. かといって金額に上限を設けると、それを元に封筒を替えるべきかどうかが推測できてしまう
  3. 金額上限を設けずかつ金額の期待値が有限の値となるようにしようとすると、どこかしらでギャンブルとして破綻する

このように、問題設定をどのように解釈したり補ったりしてもギャンブルとして成立しないのではないか、という話。

問題文の穴

上記パラドックスには、以下の部分に論理の飛躍が含まれている:

開けた封筒に1万円が入っていたとすると、もう一方の封筒には5千円か2万円がそれぞれ確率1/2で入っている。

ここには以下の点で論理の飛躍がある:

  • 出題者は金に糸目を付けずにあらゆる金額を均等な確率で選ぶはず、という仮定(決めつけ)を置いている
    • 例えば「出題者は高い金額ほど出したがらない」といったバイアスがあるならば 1/2 とは断言できなくなる
    • あるいは、もしかしたら逆に「高い金額ほど出したがる」というバイアスがかかっているかもしれない

ぐぐって出てくる多くの記事などでもこの飛躍に言及されていることが多いが、しかし問題文では上記の仮定(決めつけ)を否定もしていない。なので、本稿では上記の仮定が誤まりであるとは決めつけずに考察してみるとする。

お金を出す側(出題者)の気持ちになって考えてみよう

出題者がどのような基準で金額を選んだ場合に、2つの封筒問題は成立するのかあるいはどのような矛盾をきたすのか、問題文で曖昧になっている部分について考察してゆく。

出題者が金に糸目を付けずにあらゆる金額を均等な確率で選ぶとどうなってしまうのか

開けた封筒に1万円が入っていたとすると、もう一方の封筒には5千円か2万円がそれぞれ確率1/2で入っている。

まず、↑の論理が成り立つような出題者を想定してみる。つまり、以下のような出題者を仮定する:

  • 出題者は出す金額に限界を設けない
    • 出題者がどこかしらに限界額を設けてしまうと、回答者にとっては開けた封筒の金額の2倍が限界を超える可能性が常にあるということになるため、確率 1/2 とは言えなくなってしまう *1
    • 限界額が存在するとしたらどうなるのか?は後述
  • 高い金額ほど滅多に出さない、といったバイアスも掛けない
    • バイアスを掛けていると、やはり確率 1/2 とは言えなくなる

上記を前提とした場合、「出題者側が封筒に詰める金額の期待値」が  +\infty になってしまう。出題をするためにはまず1つの封筒の金額を決めるはずだが、なにしろ 0円から  +\infty 円 の範囲内で均等にランダムに金額を選んで出題する*2ことになってしまうので、ランダムに選んだ結果の金額も無限に大きな数値になるということになってしまう。

そのようなトンデモナイ出題者がいると仮定した場合、実は "封筒を変えた方が得" ではある。なぜならば手元の開封済みの封筒に 1 万円が入っていようが 1000 億円が入っていようが 5000 兆円が入っていようが、この出題者が封筒に入れる金額の期待値  +\infty よりは小さいからである *3。よって、封筒を交換したほうが高い金額を得られることが期待できる *4

一見不思議な結論だが、以下のように捉えると直感的かもしれない:

  • 無限の金額を突っ込む出題者などというものを前提に置く以上は、手元の封筒の金額より大きい金額が出現やすくなる
  • 手元の封筒の金額を x 円とするとき、 [0, x) の区間と (x,  +\infty) の区間では後者のほうが大きい、よって x より小さい数値より大きい数値の方が出やすい

出題者の出せる金額に限界があるとするとどうなるか

では、もう少し現実的に考えて、以下のように仮定するとどうなるだろうか:

  • 出題者には実は封筒一つあたりの金額の限界があり、その範囲内で金額を選んで封筒を出す
  • ただし、回答者には上記の限界は知らされておらず、推測することも全くできない

おそらく多くの人が2つの封筒問題を見たときに暗黙のうちに想定しているのも上記のような前提ではなかろうか。

しかし実は上記の前提だけでは先述の  +\infty を封筒に入れてくる出題者と同じ状況なってしまう。なぜならば金額の限界が「まったくわからない」ということは、出題者の限界は実は 1000 億円かもしれないし 5000 兆円かもしれないし 1 無量大数円かもしれないし....となってしまうためである *5

ここで、さらに常識を働かせて「現実的に考えて限界はせいぜい 10 万円とかだろう」「1000 円では賭けが成立しないだろう」といった前提を置くと、今度はそれによって封筒の金額が絞り込まれてしまう。せいぜい最大 10 万円・最小 1000 円と考えているならば、手元の開封済み封筒が 49,500 円以上か未満かで封筒を替えるべきかどうかは自明である。

つまり、常識的な仮定によって金額の範囲に前提を置いてしまうのであれば、封筒を交換すべきかどうかは封筒の金額によって定まる、といえる。

金額には限界を設けない代わりに、高い金額ほど出る確率を下げる、という出し方ならどうか

上記の通り、金額に上限を設けたり上限が推測されたりしてしまうと、封筒を交換すべきかどうかは計算で判定できてしまう。

では、金額に上限を設けない代わりに、高い金額ほど出る確率を下げる方式はどうか。そうすれば、回答者が金額の上限を元に判断することができなくなり、同時に金額の期待値が  +\infty になることで破綻する問題も回避できるのではないか、という考え方である。

2つの封筒問題について言及している記事の複数で見られた例として以下のようなものがある:

封筒に入る金額は(2n円,2n+1円) (n=0,1,2,…) のどれかで、ペア(2n円,2n+1円)が選ばれる確率は  \frac{2^n}{3^{n+1}} である。

2n円(n>0)が入っていた場合、ありうるのは(2n-1円,2n円)か(2n円,2n+1円)で、それぞれ(事後)確率は3/5, 2/5である。これで期待値を計算すると、  \frac{11}{10}\cdot2^n 円になり、やはり変えた方が得である!

( 出展: たのしい確率 〜2つの封筒問題〜 - むしゃくしゃしてやった,今は反省している日記)

今回の前提では、たしかに以下の好ましい性質が満たされているかのように見える:

  • 金額の期待値が  +\infty になってしまい、常に封筒の交換が正解になってしまう問題が回避されている
    • ついでに、出題者が破産するリスクも 0 ではないが大分下がっていそうに見えるが...?
  • 金額の上限が定まらないので、回答者が 上限/2 と見比べて封筒を替えることができない

では検証として、この例において「出題者側が片方の封筒に詰める金額の期待値」を計算してみるとどうなるだろうか。金額を 2n と置くと、その金額を選ぶ確率は  \frac{2^n}{3^{n+1}} であるとのことである。期待値は、選びうるすべての金額にそれぞれの金額が選ばれる確率を乗じた結果の和であるので、

{\displaystyle
  {\sum_{n=0}^{\infty} 2^n \frac{2^n}{3^{n+1}}} = {\frac{1}{3} \sum_{n=0}^{\infty} (\frac{4}{3})^n} = +\infty
}

となり、結局「出題者側が片方の封筒に詰める金額の期待値」が  +\infty になる*6ため、先に述べた無限の予算のある出題者と同じ状況となってしまっている。そのため、この場合でも封筒を交換するのが常に良い選択肢となるのは数学的に必然的である (出題者の破産リスクも無限に高いままである)。

封筒に詰める金額の期待値を有限の値に収束させるとどうなるか

ここまでの考察から、ギャンブルとしての予測不能性を持たせて成立させるためには以下が必要であることが分かる:

  • 封筒に詰める金額に上限は設けない
    • 上限を設けると、上限/2 と見比べることで封筒を替えるかどうかが定まってしまう
  • 出題者側が片方の封筒に詰める金額の期待値が  +\infty に発散しないこと
    •  +\infty に発散してしまうと、封筒を必ず替えるのが妥当となってしまう
    • こうすることで、出題者が破産するリスクも低くはなる *7

例えば以下のように設定することで上記の条件をみたすことが出来る:

封筒に入る金額は(2n万円,2n+1万円) (n=0,1,2,…) のどれかで、ペア(2n万円,2n+1万円)が選ばれる確率は  \frac{1}{2^{2n+1}} である。

この条件であれば、「出題者側が小さい方の封筒に詰める金額の期待値」は

{\displaystyle
\sum_{n=0}^{\infty} 2^n \frac{1}{2^{2n+1}} = \sum_{n=0}^{\infty} \frac{1}{2^{n+1}} = 1
}

より、1/2n の和が 1 に収束するため、1 万円に収束する。

しかし、封筒に入る金額ペアの種類がそもそも (1万円,2万円), (2万円,3万円), (3万円,4万円), ... なのに期待値が 1 万円というのは明らかに矛盾するように思われる、2 万円以降がなかったことになっているかのような結果になってしまっている。

この矛盾の原因は確率を  \frac{1}{2^{2n+1}} と定義したことに起因する。このギャンブルを成立させるためには、出題者は必ず何かしらのペアを出さなければならない(資金を出さないという選択肢がない)はずである。よって、全てのペアの出現確率の和はちょうど 1 になって然るべきである、が、ペアの出現確率  \frac{1}{2^{2n+1}} の全ての和を計算すると:

{\displaystyle
\sum_{n=0}^{\infty} \frac{1}{2^{2n+1}}
= \sum_{n=0}^{\infty} \frac{1}{2} \frac{1 - (\frac{1}{4})^n}{1 - \frac{1}{4}}
= \frac{1}{2} \frac{1}{1 - \frac{1}{4}}
= \frac{2}{3}
}

より 2/3 となってしまい 1 に満たない。よって、出題者が 1/3 の確率で何も出さないケースが生じてしまっている (それによって賭けの期待値が 1 万円になっている)。

即ち、

封筒に入る金額は(2n万円,2n+1万円) (n=0,1,2,…) のどれかで、ペア(2n万円,2n+1万円)が選ばれる確率は  \frac{1}{2^{2n+1}} である。

この前提をおいた場合、以下の条件は満たしているが、

  • 封筒に詰める金額に上限は設けない
    • 上限を設けると、上限/2 と見比べることで封筒を替えるかどうかが定まってしまう
  • 出題者側が片方の封筒に詰める金額の期待値が  +\infty に発散しないこと
    •  +\infty に発散してしまうと、封筒を必ず替えるのが妥当となってしまう

以下の条件を満たせていない、

  • 出題者は必ず 0 より大きい金額の封筒のペアを提示すること
    • 掛け金を出すのを拒否する行為(やマイナスの金額提示)は NG ということ
    • 即ち、金額の各種ペアを出す確率の総合計がちょうど 1 であること

これは考えてみれば致し方がない話であり、以下の理由でこのギャンブルを成立させるように数式を設計することが困難なのである:

  • 確率の総合計がちょうど 1 になるには、ペアの出現確率は単調に減少する数列*8でなければならない
    • 金額に上限は設けない → 金額のペアの種類は無数にある → 単調に減少する数列でないと、合計が 1 を超える
  • 確率はマイナスになりえないし、ペアを出さないのも NG なので、ペアの出現確率の数列は 0 より大でなければならない
  • ペアの出現確率の数列の合計値は 1 に収束しなければならない (1 未満は駄目)
  • 封筒に詰める金額に上限は設けてはならないため、当然、金額は  +\infty に発散する数列でなければならない
  • しかし、封筒に詰める期待値は  +\infty に発散してはならない (一定の値にならなければならない)
    • 期待値は ペアの金額 * ペアの出現確率 の全ペアについての和

これらの条件を満たす都合の良い数列 *9 は、筆者の考えうる限りでは作れそうにない。

それゆえ、(これらの条件を満たす都合の良い数列ない限り) 2 つの封筒問題はここまでに論じたいずれかの問題が発生してしまうためにギャンブルとして成立させることが出来ないのではないか、というのが本稿の結論である。

*1:開けた封筒の2倍の金額を出題者が出せないかもしれない可能性はあるのに、1/2 は常に出せることになってしまうので

*2:そもそも上限のない無限長の区間から金額を「選ぶ」という作業が可能なのか?という疑問はあるが。正しくは、0円〜n円 の区間から一様にランダムな金額を選ぶ期待値の Lim n → ∞ の極限値が発散する、と考えるべきであろう。

*3:あらゆる値より大きいというのが無限の定義だ、とも

*4: なお、(確率に関するパラドックス (その1))http://www.yoshizoe-stat.jp/stat/sinf9307.pdf によると、この状況で封筒を交換した結果より高い金額が得られる可能性は 2/3 であるとのことである。上記論文における「 θ の事前分布 p(θ) を 0 < θ < ∞ の範囲での一様分布として」仮定している計算が、ここで議論している状況と同じ状況を述べているはず

*5: このように、幾らでも大きい数を置くことが出来る、という考え方が無限の数学的定義そのもの...のうちの一つの基本的な考え方である

*6:手計算で期待値を計算してみても、期待値が際限なく大きくなることが分かる。単調増加する式なのでわかりやすいはず。

*7:とはいえ、金額上限を設けないため、運が悪いと駄目だが...。リスクが低くなるだけである

*8:1/2, 1/4, 1/8 ... のように常に値が小さくなる数列

*9:合計値が 1 に収束する正の単調減少数列なのに、その数列を  +\infty に発散する数列の各項に掛けると和が定数に収束する数列

Logger を Spring に DI させれば同じようなコードを毎回書かなくて済むしテスト記述性も改善できる

SLF4J などの Logger を使うときに、毎回以下のようなボイラープレートを書くのにうんざりしていませんか? 少なくとも私は大変うんざりしています:

class TestController {   // Kotlin Example
    companion object {
        private val log = LoggerFactory.getLogger(TestController::class.java)
    }   // 長くてつらい...
}
class TestController {   // Java Example
    private static final Logger LOG = LoggerFactory.getLogger(TestController.class);
    // これは 1 行だが、とはいえ長いし class 名を二度書くのが辛い & コピペ間違いも起こしやすい
}

夜中に寝苦しくて眠れない時などに趣味で Kotlin + spring-boot 2.0 + その他諸々 にチャレンジしているのですが、その際に上記の悩みを Spring の DI で解消してみました (その結果としてテスト記述性も良くなりました) という久々の記事です。

なお、この記事は上記の通りの趣味的な都合により Kotlin, Spring メインの記述となっていますが、Java, Scala や Spring 以外の DI ツールでも類似・同様のことはおそらく可能ではないでしょうか (未検証)。

調べた限り世の中でも知られている苦労らしい

KotlinでLoggerの生成を少し楽にした - Qiita, Best practices for loggers - Kotlin Discussions などにおいて記述を多少省略する試みは広く行われているようです (Kotlin)。

しかし、数行にわたる記述を毎回書くという点は変わらない様子です。

では、どうするか

Spring の DI の一般的手法として、constructor injection (DI してほしいものをコンストラクタ引数で宣言する)という手法があります。

それを使い、かつ Kotlin の primary constructor で property を宣言する構文を用いることで、こんな風にできました:

@Controller
class TestController (val log : Logger) {   // ここで Logger をもらってしまう
    fun example() {
        log.info("It works")   // ↑の記述だけで Logger を使える
    }
}

Java では出来るもののトレードオフがあります、詳しくは後述。

Logger を DI するための下準備

上記のように Logger を受け取るコードをいきなり書くと「 Loggerインスタンスなんて見つからんぞ!」と Spring に怒られるはずです。 そのため「DI 先の class に応じた Logger インスタンスを作って DI する」ための仕込みが必要であり、それは以下のようにして実現できます:

@Configuration
class LoggerInjectionConfig {     // この class を component-scan で見える場所に置いておくか @Import で明示的に読み込む

    @Bean
    @Scope("prototype")
    fun logger(ip : InjectionPoint): Logger {    // Logger を DI する都度に呼ばれる
        return LoggerFactory.getLogger(
                ip.methodParameter?.containingClass
                        ?: ip.field?.declaringClass
                        ?: throw BeanCreationException("Cannot find type for Logger")
        )
    }
}

// Java 版は(めんどいので)書いてないですが、上記のコードの雰囲気で伝わるのではないかなぁ...

上記のように InjectionPoint なるものに DI 先の情報が渡ってくるので、それを使って Logger を生成する実装をしておいてやれば、先のように Logger を DI することが可能になります。

何がうれしいか

Logger を使う記述が簡潔になる (※Javaでは微妙な点あり)

@Controller
class TestController {   // 本手法の導入前
    companion object {
        private val log = LoggerFactory.getLogger(TestController::class.java)
    }
}

@Controller
class TestController (val log : Logger) {    // 本手法
}

と、大変簡潔になります。

Logger を使う処理というのは少なからず書くことがあるので、これによる頭の負荷の軽減は、上記の InjectionPoint 云々の実装を入れるには十分見合うものではないでしょうか。

ただし、Java の場合、コンストラクタ引数で受け取った Logger をフィールド変数に格納して...という一連の記述が冗長になってしまいます。そのため、Java で記述の簡潔さを優先する場合、field injection (フィールドに @Autowired を付けてフィールドに直接 DI する)を用いることになりますが、field injection は後述するテスト上のメリットが得られないといった負の側面があり近年の Spring の潮流としてもあまり推奨されていない、というトレードオフがあります。いっそ Lombok を使ったほうが良いかもしれませんね...。

public class TestController {
     
     @Autowired    // field injection すればこの記述だけで良いが、後述するテスト上のメリットが得られない点が微妙
     private Logger log; 


     public TestController(Logger log){
          this.log = log;
     }
     // かといって constructor injection すると、Kotlin と違って簡潔に記述できない
     // ( 記述の短縮どころか、伝統的な Logger の初期化方法より長くなっている )
}

ログ出力に対するテストがしやすくなる

そもそもの Spring などの DI を利用する一般的なメリットとして「DI するオブジェクトを差し替えることで、コードの再利用やテストがしやすくなる」というメリットがあるはずです。

しかし、一般的な Logger のイディオムは全くもって DI していないので、上記のメリットが得られなくなってしまっていないでしょうか?

例えば、以下のようなニーズを想定してみます:

  • 大事なログを出す処理なので「ログが期待通りに出ていること」をテストで assert したい
  • パラメタに渡した機密の情報(例えばクレジットカード番号とか)が「あらゆるログに一切出ていないこと」をテストで assert したい
  • ログに興味がないテストケースなので、ログは単に破棄したい
  • 上記のような制御を、設定ファイル決め打ちなどではなく、テストケースごとにプログラム記述で制御したい

一般的な Logger のイディオムを使っている場合、 Loggercompanion object / static final で決め打ちされてしまっているため、上記のようなニーズを満たすことが難しくなります。

リフレクションを用いて強引に Logger を差し替えるのは明らかに保守性も実装効率も悪いですし、かといって SLF4J や Log4j のバックエンドにまるまる自作の実装を差し込んで...といった手法では実装やメンテナンスに苦労が伴うことでしょう。

一方で、今回の手法を用いて Logger を DI するように実装していれば、spring-boot の @MockBean といった DI・モックツールの機能をフルに活用して何とでもできます。

上記の事例で示したようにログについてテストをしたい局面がいつ来るかは分からないことなので、当該手法を予め仕込んでおくことも個人的にはおすすめです。仕込みが無い状態で「クレジットカード番号があらゆるログに一切出ていないこと」というテストを後から実装するのは、概して苦労が伴うものと思われます。

重箱の隅な想定議論

ここから先は細かいことが気になる人向け。

続きを読む

英字配列キーボードな Mac でのキーボード設定 (with Windows VM)

Macbook を買う際には英字配列キーボードにしているのだが、その際のキーボード設定のやり方を毎回忘れてしまうのでメモがてら記事にした。

この設定をすることで、英字キーボードでありながら、日本語入力の On/Off を素早く切り替えることが可能になる。

以下の手順は Mac OS XWindows VM (VMWare Fusion) 環境で実践済みの手順だが、Mac/Windows 単独といった別の環境でも有用なはず。

この設定で実現されること

  1. 左 Command を「英数」、右 Command を「かな」キーとして使う
    • ただし、Command + 何かのキー の場合には普通の Command キーとして機能する
    • また、VM 系のソフトに対してのみはこのマッピングを適用しない
  2. Windows VM 側では右 Alt を「全角/半角」キーとして使う
    • VM 利用中は Command キーをそのまま使える (Windows キー扱いになる)

なお、もっと凝った設定をすることでより改善することもできるが、ここでは比較的手間のかからない方法を採用している。

1. 左 Command を「英数」、右 Command を「かな」キーとして使う

Karabiner (旧名 KeyRemap4MacBook) を使う。

Karabiner には「左右のコマンドキー(⌘)を「英数/かな」としても使う」という大変便利なキーマッピング設定がデフォルトで用意されており、VM を使わないならばこれで十分である。

しかし、Windows VM を使っている際にこの設定が適用されると残念なことになる*1ため、以下のようにして VM 系のソフト以外のみに上記設定を適用する:

  1. Karabiner (旧名 KeyRemap4MacBook) をインストール
  2. Karabiner の設定画面を開く
  3. "Misc & Uninstall" タブを開く
  4. "Open private.xml" し何らかのテキストエディタxml を開く
  5. 後述の XML をペーストし保存
  6. "Change Key" タブを開く
  7. "Reload XML" ボタンを押す (エラーになった場合 4 での XML 編集からやり直す)
  8. 検索ボックスに "コマンドキー" と入れて検索
  9. "コマンドキーの動作を優先モード v1 (VM 対策版)" または "コマンドキーの動作を優先モード v2 (VM 対策版)" を有効にする

v1 と v2 どちらにするかはお好みで。 個人的には、v1 の方がさくさく操作できるのだが、稀に誤爆して辛いので v2 のほうが無難*2という印象。

以下 private.xml の中身:

<?xml version="1.0"?>
<root>
  <item>
    <name>For Japanese</name>
    <item>
      <name>左右のコマンドキー(⌘)を「英数/かな」としても使う (VM 対策版)</name>
      <appendix>(左コマンドキーを英数キーにする)</appendix>
      <appendix>(右コマンドキーをかなキーにする)</appendix>

      <item>
        <name>コマンドキーの動作を優先モード v2 (VM 対策版)</name>
        <not>VIRTUALMACHINE</not>    <!-- ここが増えてるだけ -->
        <appendix>(コマンドキーの空打ちで「英数/かな」)</appendix>
        <appendix>(コマンドキーを押している間に他のキーを打つと通常のコマンドキーとして動作。</appendix>
        <appendix>ただし、他のキーを離す前にコマンドキーを離した場合は「英数/かな」として扱う。)</appendix>
        <identifier>remap.japanese_command2eisuukana_prefer_command_v2.nonvm</identifier>
        <autogen>
          __KeyOverlaidModifier__
          KeyCode::COMMAND_L,
          KeyCode::VK_CONFIG_SYNC_KEYDOWNUP_notsave_japanese_command2eisuukana_prefer_command_v2, ModifierFlag::COMMAND_L,
          KeyCode::JIS_EISUU,
        </autogen>
        <autogen>
          __KeyOverlaidModifier__
          KeyCode::COMMAND_R,
          KeyCode::VK_CONFIG_SYNC_KEYDOWNUP_notsave_japanese_command2eisuukana_prefer_command_v2, ModifierFlag::COMMAND_R,
          KeyCode::JIS_KANA,
        </autogen>
      </item>

      <item><name>──────────────────────────────</name></item>
      <item>
        <name>コマンドキーの動作を優先モード v1 (VM 対策版)</name>
        <not>VIRTUALMACHINE</not>    <!-- ここが増えてるだけ -->
        <appendix>(コマンドキーの空打ちで「英数/かな」)</appendix>
        <appendix>(コマンドキーを押している間に他のキーを打つと通常のコマンドキーとして動作)</appendix>
        <identifier>remap.jis_command2eisuukana_prefer_command.nonvm</identifier>
        <autogen>__KeyOverlaidModifier__ KeyCode::COMMAND_L, KeyCode::COMMAND_L, KeyCode::JIS_EISUU</autogen>
        <autogen>__KeyOverlaidModifier__ KeyCode::COMMAND_R, KeyCode::COMMAND_R, KeyCode::JIS_KANA</autogen>
      </item>
    </item>
  </item>
</root>

2. Windows VM 側では右 Alt を「全角/半角」キーとして使う

AutoHotkeyを使う。

これによって右 Alt で 全角/半角 操作が可能になる。

先述の設定で Mac 側では左右の Command で英数/かなを切り替えており、それと同じキーにならない点は多少気持ち悪い設定ではあるが、

  • スペースバーの横のキー(Command)は Windows キーにしておきたい
  • "全角/半角" と "英数", "かな" は別物でもいいのではないか (キーの数からして違うし) *3

という理由で筆者は VM では右 Alt にしている。

AutoHotkey の設定方法は以下:

  1. AutoHotkey をインストール
  2. スタートメニュー > すべてのプログラム > スタートアップ
  3. スタートアップを右クリックし「開く (O)」
  4. メモ帳等で "RAlt::vk19" (引用符は不要)とだけ書いたファイルを書く
  5. "右Altを全角半角にする.alt" といった名前で上記フォルダに保存
  6. ダブルクリックで "右Altを全角半角にする.alt" を実行

これで、Windows VM 側では右 Alt で IME を切り替えることができるようになる。

*1:Windows キーと IME の切り替え操作がバッティングする。例えば VMWare Fusion の場合なんと Windows キーと全角/半角キーをを同時に押した扱いになる。

*2:例えば「かな」キーの直後に W を押す操作をした場合、タイミング次第では v1 だと Command + W 扱いになってしまい作業中のウインドウを閉じることになる

*3:IME 側の設定も含めて頑張れば 英数, かな キー操作を Windows 側でも出来る。が、めんどいので最近そこまではやってない。

ENV.fetch のすすめ

Ruby にて環境変数の値を使う場合には ENV.fetch を使うと良いのではないか、という小ネタ。

そうすることで環境変数が存在しないエラーを早期に検知できる上、エラーの根本原因が例外に反映されるので分かりやすくなる。

この2行だけでほぼ言い尽くしてしまっているが、以下詳細説明:

ENV を使う背景

環境変数を読む理由は色々ありうるが、特に Heroku や Docker などの近代的な PaaS 環境・コンテナ環境において環境変数を読むことが多い。

例えば Ruby on Rails アプリで DB に接続する場合、接続先の DB のホストやユーザー情報を database.yml に決め打ちするのではなく、Heroku デプロイ時や Docker コンテナ起動時に指定したいということが多いためである。

また、Ruby on Rails では database.yml などの各種設定ファイルにて erb を使えるため、設定ファイル内で気軽に ENV を読めるという背景もある *1

ENV[...] より ENV.fetch

上記のような場合に

mysql:
  host: <%= ENV['MYSQL_HOST'] %>

のような書き方がよく見られるが、これは

mysql:
  host: <%= ENV.fetch('MYSQL_HOST') %>

の方がベターではないかというのが本エントリの趣旨。

読もうとしている環境変数が存在する場合は ENV[...] と ENV.fetch(...) は全く同じ振る舞いである。

しかし環境変数が存在しない場合、ENV[...] は nil になる*2が、fetch であれば設定ファイルの読み込み時点で以下のように例外が上がる。

Cannot load `Rails.application.database_configuration`: key not found: "MYSQL_HOST"

このように、あるべき環境変数が無いというエラーを早期に検知でき、かつエラー内容も分かりやすくなるのがこの手法のメリットである。

デフォルト値

なお、環境変数が無かった場合のデフォルト値を指定したい場合

mysql:
  host: <%= ENV.fetch('MYSQL_HOST', 'localhost') %>

のようにすれば良い。

このようにすると、環境変数が存在しなければ第二引数(上の例では localhost)が使われ、例外は発生しない。

*1:ERB.new(ファイル内容).result という風にすれば自前の設定ファイルに対しても erb を使える。Rails も内部ではこのようにしている。

*2:そのため、設定値を使う側でエラーになってしまい、エラー原因をたどる面倒が発生する

MySQL Connector/J (JDBC ドライバ)の罠まとめ

MySQL JDBC ドライバ(MySQL Connector/J)、JavaMySQL といえばまずコレだが、これまた地味に罠が多い(そして多くの人が踏んで苦しむ)のでまとめてみた。

(2015/03/19) こちら のコメント欄でご指摘ただいた wait_timeout の件について記事修正いたしました。

Summary

以下、いずれもプログラム設計時に理解しておかないと、開発中は大丈夫そうでも実用した途端に苦しまされれてしかも設計から治す羽目になる要注意な罠である:

  • SELECT 結果は全部メモリに載ってしまう (デフォルト設定で)

    • 大量 SELECT する場合は FetchSize, ResultSetType を要設定
      • 利用時には制約があるので、設計段階から考慮しなければならない (後述)
  • idle 時間の「合計で」コネクションが切られる 前回のクエリ処理から一定時間以上経過するとコネクションが切られる

    • Connector/J 限定ではないが, これも設計時点で要考慮
  • クライアントサイド プリペアドステートメントになっている (デフォルト設定で)

    • パフォーマンス向上目的に使えるものではない
    • ただし SQL インジェクション対策というセキュリティ目的では使うべき
  • 裏で勝手にスレッドが走っている -> そしてメモリリーク

    • tomcat など Servlet 環境で、デプロイ時のリソースリークの原因に

SELECT 結果は全部メモリに載ってしまう

デフォルトにより、ResultSets は完全に摘出され、メモリに保存されます。 ほとんどの場合において、これは最も効果的な操作方法であり、MySQL の設計により、ネットワーク プロトコルはより簡単に実装できます。 http://dev.mysql.com/doc/refman/5.1/ja/connector-j-reference-implementation-notes.html

MySQL Connector/J は、デフォルトで SELECT 結果をすべてメモリに格納してしまう。

そのため、大量のデータを処理するために SELECT するような実装を普通に書くと、確実に OutOfMemory の憂き目に・・・。

これを防ぐためには、

stmt = conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_READ_ONLY); stmt.setFetchSize(Integer.MIN_VALUE); フェッチのサイズが Integer.MIN_VALUE の、前進専用、読み取り専用のコンビネーションは、行ごとに結果セットをストリームするようドライバに指示する信号として機能します。この後、このステートメントで作成された結果セットは行ごとに摘出されます。

の通りに ResultSetType や FetchSize を設定してやる必要がある。

さらに、追加の罠として、

接続にクエリを発行する前に、結果セットのすべての行を読まなければならず、さもなければ例外が投入されます。

すなわち「SELECT 結果を読み終わるか ResultSet を閉じるまでは、そのコネクションでは別のクエリを発行できないよ」制限もあるという・・・。

なので、

  • 大量データを SELECT する
  • かつ、同一トランザクションで SELECT 中に INSERT なり UPDATE なりしたい

みたいな設計は MySQL Connector/J では実現不能なので、設計段階から要注意。

このことを知らずに実装すると、全件オンメモリで SELECT して 1 件 1 件を読み取りながら INSERT/UPDATE するような実装をしてしまい、後から ResultSetType や FetchSize を直すと今度は上記制約で設計自体が破綻し...という憂き目に遭って修正者の胃が痛くなる (実体験)。

もし上記のような設計をしたくなった場合、テーブル構造を見なおすなり、一時ファイルを使うなり、あるいはヒープ容量が十分であることを確かめて使うなり、考えなおさなければならない。

前回のクエリ処理から一定時間以上経過するとコネクションが切られる

誤: idle 時間の「合計で」コネクションが切られる (少なくとも最新 MySQL 5.6 ではこうでない)

問題を発生させていたプログラムにもはや触れないため上記の誤解の理由は不明だが、少なくとも MySQL 5.6 では見出しの通りである*1

サーバー側の実装としては sql/sql_parse.cc にて以下のようになっている様子:

/**
  Read one command from connection and execute it (query or simple command).
  This function is called in loop from thread function.

  For profiling to work, it must never be called recursively.

  @retval
    0  success
  @retval
    1  request of thread shutdown (see dispatch_command() description)
*/

bool do_command(THD *thd)
{

   ( 中略 )

  /*
    This thread will do a blocking read from the client which
    will be interrupted when the next command is received from
    the client, the connection is closed or "net_wait_timeout"
    number of seconds has passed.
  */
  my_net_set_read_timeout(net, thd->variables.net_wait_timeout);

このような実装のため、「最後に何らかのクエリ処理をしてから」の経過時間でコネクション切断判定がなされている。

以下古い記述:

Connector/J 限定ではなく MySQL そのものの仕様だが、コネクションが何もクエリ処理していない時間の合計が一定時間を超えると MySQL サーバー側から接続を一方的に切られてしまう。

"mysql wait_timeout" を日本語 google でぐぐって真っ先に出てくるページが

この設定、接続のアイドル状態が一定時間続くと MySQL 側から自動的に接続を切る設定らしい。 MySQL の wait_timeout を変更。 - Qiita

と書いているため誤解されていることが多いが、接続のアイドル時間の合計であり、一定時間続くと、ではない。

よくある例として、「このバッチは断続的にクエリを発行し続けるから何時間走っても大丈夫!」とか思い込んで実装 -> 本番で wait_timeout を食らって死亡! という流れをとても良く見かけるが、実際には死んでしまう。そして、そういった実装は得てしてコネクション 1 本であること前提(トランザクションをずっと貼ってたりとか)であるため、コネクション再接続に耐えうる実装に治すためには大手術が必要になり修正者の怒りと憎しみを駆り立てる (実体験)。

以下は wait_timeout の正しい理解でも言えるはずの事柄:

また別のパターンとして、tomcat などで C3PO コネクションプールを普通に使っているとこの wait_timeout による断が検知されず、「本番稼働し始めて半日ほどすると謎の JDBC エラーが時々出る」みたいな事態になってしまい、コネクション死活監視の設定とテストを休日返上で羽目になったりする (実体験*2 )。

このような事態にならぬよう、サーバー側 wait_timeout を大きくする*3か、アプリ側でコネクションの死活チェックと再接続を前提とした設計にするよう考えておくべきである。

なお、普通は wait_timeout グローバル変数値が使われるが、interactive 扱いの接続では interactive_timeout 値が使われるので注意。どちらにせよ、コネクションはいつか切られる前提で設計・テストしたほうが良いが...。

クライアントサイド プリペアドステートメント

MySQL Connector/J 3.1.0 からは、サーバ側プリペアド ステートメントおよびバイナリ エンコードされた結果セットは、サーバがそれらをサポートする場合に使用されます。 http://dev.mysql.com/doc/refman/5.1/ja/connector-j-reference-implementation-notes.html

公式の記述が罠だが、最新の Connector/J (少なくとも 5.x) は、デフォルトでクライアントサイド プリペアドステートメントを使う設定になっている。

Connector/J 5.0/5.1のデフォルト設定では、server-sideではなくclient-sideのPrepared Statementが使用されるようになっている。 Connector/J 3.1.0にてデフォルト値はtrueで登場したものの、Connector/J 5.0.5/5.1にてfalseとなった。 Connector/J 5.1とServer Side Prepared Statement - mir the developer

そもそも、プリペアドステートメントの元々のメリットは、DB サーバー側で SQL を予め解析しておいてクエリ実行時に毎度 SQL の解析処理を行わないようにすることでクエリ速度の顕著な向上が得られる点である。しかし、クライアントサイド プリペアドステートメントは言ってしまえば Connector/J がクライアント側で SQL を組み立てているだけなので、この性能上のメリットは全くない。

なので、例えばコネクションプール機構で頑張って PreparedStatement インスタンスをキャッシュ・再利用する実装をしても速度は向上しないどころかキャッシュ処理している分だけメモリの無駄かつ低速になる(実体験)。

なお、useServerPrepStmts なるパラメタを使うことでサーバーサイド プリペアドステートメントにすることもできるが、ある程度の負荷を掛けると謎の例外が頻発するなどの有り様で中々不安定である*4。Connector/J デフォルトでサーバーサイド プリペアドステートメントを使わない設定になっているのもむべなるかな。

このような事情であるので、MySQL Connector/J では PreparedStatement を性能向上のために使おうとは思わないほうが良い。

ただし、これは重要だが、PreparedStatement を使うことはセキュリティ面では大変好ましい。SQL を自力の文字列処理で生成することは SQL インジェクション脆弱性の温床になるので、PreparedStatement のパラメタ機能を使うことはセキュリティのための至極基本的な処置である (性能面ではメリットがなくても)。

裏で勝手にスレッドが走っている -> そしてメモリリーク

この件: http://bugs.mysql.com/bug.php?id=68556

本件は日本語情報があまりないのだが、実は MySQL Connector/J は裏で "AbandonedConnectionCleanupThread" なるスレッドを勝手に走らせている。このスレッドは(ぱっとソースを見た限り)放置された Connection を close してくれる。

この親切機能の何が問題であるかというと、tomcat 環境で

  1. 勝手に AbandonedConnectionCleanupThread が起動する (WebApp クラスローダーで)
  2. アプリの war なりをデプロイし直す (別の WebApp クラスローダーが作られる)
  3. 1 で起動した AbandonedConnectionCleanupThread は元気に走り続ける
  4. 古い WebApp クラスローダーは GC されない (1 のスレッドから参照されるため)
  5. デプロイ解除(配備解除)前のアプリの全ての static オブジェクトも GC されない (クラスローダーから参照)
  6. 激しくメモリリーク

という事態を招くためである。

表面的に見える事象としては、

  • アプリの再デプロイ後すぐに or しばらくして OutOfMemoryError
  • war を配備解除するたびにメモリが減っている
  • 大量のオブジェクトがリークしている

という厄介な状態であり、しかも上記の理由でクラスローダーまるごとリークしているため、真犯人の Connector/J 以外のオブジェクトが目立ってしまい、原因究明もしづらい (特にヒープのダンプだけ見ていると原因究明がとてもむずかしい)。

tomcat への war デプロイ後に「とりあえず tomcat 再起動しないとメモリが足りなくなる」現象が起きている場合、本件を疑う価値があるであろう。

なお、MySQL 公式のバグチケットは Tomcat 7 の JreMemoryLeakPreventionListener を使えということで close されているが*5、この解決策を使うと MySQL JDBC ドライバが Common クラスローダーでロードされるため、tomcat 上の全アプリが同じ Connector/J を使うことになる (Connector/J の持っている static 変数もアプリをまたいで共有することになる)。

公式のバグチケット: http://bugs.mysql.com/bug.php?id=68556

そのため、JreMemoryLeakPreventionListener で使うということは、tomcat 上の Connector/J を使う全アプリが運命共同体になることを意味する*6

それを許容できる、あるいはそもそも tomcat 上に war が 1 つしかないなら良いが、そうでないならば JreMemoryLeakPreventionListener を使うのではなく、

class MyServlet extends Servlet {
    @Override public void destroy(){
        // このアプリの classloader で走っている MySQL Connector/J のスレッドを停止させる
        // これによってこのアプリの classloader がまるごとメモリリークすることを防ぐ
        // 注: これを使う場合はこのアプリ自身で Connector/J をロードしていること、tomcat 全体で Connector/J をロードしている場合ここで止めてはいけない
        // http://saiya-moebius.hatenablog.com/entry/2014/08/20/230445
        AbandonedConnectionCleanupThread.shutdown();
    }
}

するべきであろう *7

*1:http://qiita.com/satococoa/items/e3396d9d75b9cf7e6214

*2:しかも C3PO はコネクションの生存判定の実装が賢くないので自力で頑張らないといけない部分があったりもする

*3:切断し忘れているコネクションや TCP レベルで断してしまったゴミコネクションが生き残る時間が長くなる副作用アリ

*4:MySQL Server 5.6 + Connector/J 5.1

*5:いまいち解決になってない気がするが・・・

*6:先述のサーバーサイド プリペアドステートメントの件では Connector/J の static 状態がどこかおかしくなるようで、全滅させられたことが・・・

*7:AbandonedConnectionCleanupThread#shutdown は古い Connector/J には存在しないが...

Objects#requireNonNull による null チェックのススメ

コンストラクタメソッドを書く際、引数が null になっていないかのチェックを書くのがめんどくさくて省略してしまうことはないだろうか? ...少なくとも私はめんどくさくて多くの場合省略してしまっていた。

しかし、Java 7 以降に入った Objects クラスなるものを使うと楽に書けるということを知ったので共有までに。

Usage

Objects (Java Platform SE 7)

公式の説明を見ればわかる通り大変シンプル。

Objects.requireNonNull メソッドは引数が null なら NullPointerException を投げ、そうでないなら引数をそのまま返す。

class Hoge {
    private final String huga;

    public Hoge(String huga){
        this.huga = Objects.requireNonNull(huga);
    }
}

これは Objects を使わない以下のコードと等価である:

class Hoge {
    private final String huga;

    public Hoge(String huga){
        if(huga == null) throw new NullPointerException();
        this.huga = huga;
    }
}

実際に Eclipse 上などで入力してみると、"Objects", ".", "requireNonNull" の 3 トークンだけで済む前者の方が、後者の if 文パターンよりも大分少ない手間で入力できる。

また、null チェックのためだけのコードが縦に長くならない点は可読性に良い (1 メソッドが長くなればなるほど読みづらい)。

加えて、requireNonNull はメソッドなので、文中のどこにでも挟めるのも地味に良い点である

public void doSomething(String huga){
    this.hogehoge(Objects.requireNonNull(piyo));     // メソッド呼び出し前に null チェックされる
}

ところで、null チェックって必要?

基本的に、エラーの原因とエラーの発生側の距離(コード上の場所や、実行タイミング)は近ければ近いほどよい。

距離が近くない場合、エラーが出てからその原因を特定するまでに時間を要してしまう。

Null チェックについてで言えば、フィールドに値を持つ場合、「フィールドに null を入れてしまった側」と、「そのフィールドの値を実際に使って NPE になってしまった側」のコード上の場所も実行タイミングも離れる。

それゆえに、NPE が発生してから、コードを辿ったり、あるいはもう一回プログラムを再実行してデバッガで追いかけなおすという手間*1が発生し開発効率が下がる。

よって、null チェックをしておくことは後々に自分や他人のためになる。

Objects#requireNonNull によって少ない負担で null チェックが出来るようになったことで、上記の null チェックのメリットがより得やすくなった(手間に対してメリットがペイするようになった)ので、これを機に null チェックする習慣をつけることを検討してはいかがだろうか。

*1:大概コードを追いにくいようなシロモノに限って作りもレガシーなので、プログラムの再実行のためにファイルやら何やらを整えなおすのに手間がかかり、実行してからエラーになるまで時間がかかり、例外トレースも汚くて...ということが多くつらい

SimpleDateFormat の罠まとめ & 対策コード例

公式ドキュメント:SimpleDateFormat (Java Platform SE 8)

便利かつ頻繁に利用される SimpleDateFormat クラスだが、実際のところ罠が多い。

知らずに罠を踏んでいる事例を身の回りで何度も目にした上に、罠を網羅的にまとめた記事が少ないことに気がついたので、書いてみた。

まとめ

  1. マルチスレッドで使うと壊れる : 下手なことはせずインスタンスは毎回作るべし
  2. 実在しない日時が通る : setLenient(false) すべし
  3. 文字の不足や過剰があっても通る : format 結果と突き合わせるべし

全対策を盛り込んだコード

License: 以下 DateTimeUtil クラスのライセンスは Public Domain とする

import java.util.Date;
import java.text.ParseException;
import java.text.SimpleDateFormat;

class DateTimeUtil {
    /** (Thread safe) {@link SimpleDateFormat} を用いて日時文字列をパースする。<br />
    * SimpleDateFormat とは異なり、実在しない日時(ex. "2014/02/29")や
    * 桁数の不一致(ex. "yyyy/MM/dd" で "14/02/28")は許容しないし、先頭一致ではなく全体一致である。<br />
    * 参考: http://saiya-moebius.hatenablog.com/entry/2014/08/17/165322
    * 
    * @param format {@link SimpleDateFormat} 形式のフォーマット文字列
    * @param source パース対象の文字列
    * @return パース結果の日時
    * @throws ParseException    与えられた文字列をパースできなかった。
    */
    public static Date parseExact(String format, String source) throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat(format);
        sdf.setLenient(false);
        Date result = sdf.parse(source);
        if(sdf.format(result).equalsIgnoreCase(source)) return result;
        throw new ParseException("Unparseable date: \"" + source + "\"", 0);
    }
}

マルチスレッドで使うと壊れる

Advice: 下手なことはせずインスタンスは毎回作るべし

日付フォーマットは同期化されません。スレッドごとに別のフォーマット・インスタンスを作成することをお薦めします。 SimpleDateFormat (Java Platform SE 8)

この公式ドキュメントの記載は脅しでもなんでもなく、SimpleDateFormat はパース中の内部状態をフィールド変数に持っている。

そのため、マルチスレッド動作するプログラムでインスタンスを使いまわすと高確率で死ぬ。

つい static final フィールドで持ちたくなる誘惑に駆られるが、インスタンスを毎回 new する習慣をつけたほうが良い。

なお、近年の JVMJIT, GC の最適化*1は賢いため、下手に synchronized で同期化したり ThreadLocal で使いまわすのはクヌース先生のいうところの「早すぎる最適化」になりがちであるし逆効果であることも多い*2。そういうことは本当に必要になってから考えよう。

実在しない日時が通る

Advice : setLenient(false) すべし

非厳密性 Calendarは、カレンダ・フィールドを解釈する際、厳密および非厳密の2つのモードを使用します。非厳密モードの場合、Calendarはそれ自身が生成する値よりも広範なカレンダ・フィールド値を受け入れます。Calendarが、get()の値を返すためにカレンダ・フィールド値を再計算する際、すべてのカレンダ・フィールドが正規化されます。たとえば、非厳密なGregorianCalendarは、MONTH == JANUARY、DAY_OF_MONTH == 32を2月1日として解釈します。 Calendar (Java Platform SE 8)

SImpleDateFormat はデフォルトでは setLenient(true) な状態(非厳密)であり、以下の JUnit 例が示す通り実在しない日付は自動的に繰り上げ・繰り下げが行われる。

@Test()
public void testLenient_OutOfRange() throws ParseException {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
    Assert.assertEquals("2014/03/01", sdf.format(sdf.parse("2014/02/29")));
}

多くの場合これは意図している挙動ではないので、SimpleDateFormat を使う場合は setLenient(false) にして使う習慣にするほうが良い。

@Test(expected = ParseException.class)
public void testNonLenient_OutOfRange() throws ParseException {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
    sdf.setLenient(false);
    sdf.parse("2014/02/29"); // 29 日は存在しない, ParseException
}

桁数不足や末尾が余計でも通る

Advice : ParsePosition を見るか format 結果と突き合わせるべし

Notice : これらの挙動は setLenient(false) しても止められない

この件については公式の記述を読むより下記コードを見た方がわかりやすいだろう:

@Test
public void testNonLenient_ExOrMissingChars() throws ParseException {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
    sdf.setLenient(false);
    Assert.assertEquals("2014/03/01", sdf.format(sdf.parse("2014/03/01ほげ"))); // 余計な文字
    Assert.assertEquals("2014/03/12", sdf.format(sdf.parse("2014/03/012")));  // 余計な文字
    Assert.assertEquals("2014/03/01", sdf.format(sdf.parse("2014/3/1"))); // 桁数不一致
    Assert.assertEquals("0014/03/01", sdf.format(sdf.parse("14/3/1")));   // 桁数不一致
}

setLenient(false) しているにもかかわらず、"yyyy/MM/dd" を書く側としては期待していない文字列が通ってしまっている。

特に後者の "14/3/1" が西暦 14 年になってしまうのは痛い。

公式ドキュメントでも実は説明されている

public Date parse(String source) throws ParseException 指定された文字列の先頭からテキストを解析して日付を生成します。メソッドは指定された文字列のテキスト全体に使用されない場合もあります。 DateFormat (Java Platform SE 8)

公式ドキュメントの言い回しはいまいち非直感的だが、要するに「末尾に余計な文字が付いていても無視するぞ」という意味である。

数値: フォーマット時に、パターン文字の数は最小桁数です。これより短い数値は、この桁数までゼロ埋めされます。解析には、2つの隣接するフィールドを区切る必要がないかぎり、パターン文字の数は無視されます。 SimpleDateFormat (Java Platform SE 8)

この公式ドキュメントも分かりづらいが、例えば "2014/03/01" であれば "/" で区切りが明確なので、"2014/3/012" のように桁数が余計でも "2014/3/1" のように足りない部分があっても許容されますよ、という意味である。

ParsePosition なるものもあるが...

このような問題への対策として ParsePosition なるものを利用することを対策としている記述もネット上に存在する。

ParsePosition オブジェクトを parse メソッドに渡すことで、parse 対象文字列の何文字目までがパースされたかを知ることができるので、パースされた文字数が文字列の長さと一致するのかを見る、という対策である。

しかしながらも、上記の通り日付の各部分の文字数が「足りなくても過剰でも」通ってしまう性質上、parse された文字数のチェックだけでは不十分である (例えば年が短くて日が長すぎるとか)。

また、ParsePosition オブジェクトを渡す場合 ParseException が発生しなくなってしまい、代わりに ParsePosition#getErrorIndex() を毎回調べなければいけない。これは、多くの場合エラーのチェック漏れバグの原因となる。

そのため、ParsePosition を用いるよりも、素直に parse で得られた Date を format して元の文字列と一致するかをチェックした方が早い。

*1:Escape Analysis によってインスタンスをスタックに割り当てる、スレッド固有のヒープに割り当てる、など

*2:かえって同期化のコストが大きくなったり ThreadLocal のクリーンナップが問題になったりすることも多い