谷本 心 in せろ部屋 このページをアンテナに追加 RSSフィード Twitter

2009-12-24

[]レビューで鍛えるJavaコーディング力 その1(日付バリデーション)

現場でJavaのソースをレビューしていると、一目で「問題だ」と気づくコードによく出会います。

しかも、同じような問題をアチコチで見かけることも、少なくありません。


FindBugsを導入したおかげで、そういう問題が多少減ったものの、

やはりゼロになるわけではありません。


、、、ということで、

そんな問題コードに即座に反応できるようになるために、

私が見てきた問題コードをクイズ形式で紹介していきます。


ぜひ皆さんも、脊髄反射でNG部分を見つけてください。

見つけられなければ、適当にブクマでもしてください(笑


では今回は、日付チェックに関する問題です。

問題

以下のコードの問題を指摘し、修正してください。

ただし、問題は複数あることもあれば、全くないこともあります。

/**
 * 日付が正しい形式であり、存在する日付であることを検証します。
 *
 * @param date 日付を示す文字列
 * @return 正しい日付の場合はtrue、そうでない場合はfalseを返します。
 */
private static final DateFormat DF = new SimpleDateFormat("yyyy/MM/dd hh:mm:ss");

public static boolean checkDate(String date)
{
	try
	{
		DF.parse(date);
	}
	catch (ParseException ex)
	{
		return false;
	}

	return true;
}

シンプルな日付のバリデーションメソッドです。

呼び出し元では、この値を使ってCSV出力などを行うという想定です。

解答1

真っ先に思いつくのは

「SimpleDateFormatはスレッドセーフじゃないから、クラス変数として持っちゃダメ」

「setLenientしていないから、暦のチェックがされない」

の、どちらかではなかったでしょうか。


SimpleDateFormatが日付のパースやフォーマットを行うクラスということしか知らず、

どこかで見たサンプルを丸写ししているレベルだと、この問題に引っかかります。


SimpleDateFormatは、複数スレッドから同時にアクセスされてしまうと、

formatした結果がおかしくなったり、例外が発生することがあります。


なので、毎回SimpleDateFormatのインスタンスを生成するか、

Jakarta Commons LangのFastDateFormatクラスを利用しましょう。


また、DateFormat#setLenient(false)を指定していない場合

「2009/12/32 10:89:50」のような存在しない日付もパースし、

「2010/01/01 11:29:50」として扱ってしまいます。


目的がバリデーションである以上は、

「2009/12/32 10:89:50」はエラーとすべきでしょう。


だから、DateFormatの宣言部分は、以下のように修正すべきです。

public static boolean checkDate(String date)
{
	DateFormat df = new SimpleDateFormat("yyyy/MM/dd hh:mm:ss");
	df.setLenient(false);

	try
	{
		df.parse(date);
	}
	// 後は同じ
}

楽勝でしたか?

解答2

もう一つ、大きな間違いがあります。

それは "yyyy/MM/dd hh:mm:ss" です。


hhは時間を示す表示ですが、AM/PMがつくことを前提とした「12時間表記」なのです。

これでは「23:00:00」をエラーとみなしてしまうか、「11:00:00」と扱ってしまいます。


24時間表記は「hh」ではなく「HH」なので

"yyyy/MM/dd hh:mm:ss" ではなく "yyyy/MM/dd HH:mm:ss" と記述する必要があるのです。


かなりうっかり度が高いミスですが、

Javaを始めて1〜2年経って、日付フォーマットやAPIを大体覚えてしまった人や、

特に問題コードを見てスレッドセーフやsetLenientの問題に脊髄反射した人ほど、

このミスを見落としやすかったのではないでしょうか。

解答3

まだ問題があります。

それは、SimpleDateFormat付フォーマットが前方一致であるため、

後ろに余計な文字列がついていても、パースできてしまう、という問題です。


たとえば "2010/01/01 11:29:50ですよ" とか

あるいは "2010/01/01 11:29:5a" なども、パースできてしまいます。


特に後者はJakartaCommons Validatorを使っていても

チェックができなかったりするので、注意が必要です。


「入力は寛容に、出力は厳格に」の原則に従えば

別にパースできてしまっても良いじゃないか、という想いもありますが、

これはCSV出力するためのフォーマットチェックと位置づけていますから

厳密にチェックする必要があるでしょう。


要するにDateFormatで行うのは「暦の妥当性」チェックであって、

形式チェックはきちんと別に行う必要がある、ということです。


正規表現で形式チェックを行ったり、

長さチェックとsubstringとparseIntでのチェックを行なっても良いでしょう。


私は時々妥当性チェックとして、

パースとフォーマットを行って、元の文字列を取得できるか」を確認する、

という方法を使うことがあります。


こんな感じです。

public static boolean checkDate(String date) {
	DateFormat df = new SimpleDateFormat("yyyy/MM/dd hh:mm:ss");
	String converted = null;
	try
	{
		Date convertedDate = df.parse(date);
		converted = df.format(convertedDate);
	} catch (ParseException ex) {
		return false;
	}

	return converted.equals(date);
}

パフォーマンスには疑問があるかも知れませんが、

setLenientの存在や、SimpleDateFormatが前方一致であることを知らなくても

この方法であれば、確実に日付をチェックすることができます。


この「変換と逆変換を行なって元の値を取得できるか」という可逆性チェックは

APIの細かい仕様を知らなくても、チェックできる、

つまり「安全性」というメリットがあるのです。

まとめ

それでは、今回の問題をおさらいしておきましょう。

  • SimpleDateFormatはスレッドセーフではない。毎回インスタンス生成するか、FastDateFormatを使う。
  • 暦のチェックをするなら、setLenient(false)は必須。
  • hh:mm:ssではなくHH:mm:ssが正しい。
  • SimpleDateFormatは前方一致。
  • parseとformatをして、元の値が取得できるかをチェックするのは良いアイデア。

こんな感じで、これからもちょくちょく問題コードを並べてみますね!

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/cero-t/20091224/1261672117