Java に parseInt() はあるが、isInt() が無い理由


Java には Integer.parseInt() という文字列を int に変換するメソッドが存在する。isInt() というメソッドは存在しない。なので、その文字列が数字かどうかの判断だけを行いたい場合でも、とりあえず parseInt() してみる、という実装になる。

try {
	Integer.parseInt(intstring);
} catch (NumberFormatException e) {
	return false;
}
return true;

という、よく見る実装となる。どうして、Java には isInt() が存在しないのか、例外オブジェクトの生成は重いと聞くし、int文字列かどうかの判定をしてから parseInt() したいのに・・・。


・・・答えは、


「isInt() をもし実装したとすると、その内部処理は parseInt() とほぼ同一になるから」


違うのは、parseInt() の返却型が int から boolean に変わるだけである。NumberFormatException の代わりに false を返すメソッドとなるだろう。そりゃー無駄である。しかしそれでも NumberFormatException を返さないのだから意味がある、なにしろ例外オブジェクトの生成は重いのだから!・・・そうなのだろうか。実際に文字列をひたすら parseInt() する処理を 100 万回繰り返すロジックを作って試してみたところ、JDK1.6 では以下のようになった。

parseInt() 失敗時 --> 約 2000 ns
parseInt() 成功時 --> 約   80 ns

参考:
String オブジェクト生成するだけ --> 約 20 ns
Object 生成するだけ --> 約 4 ns

の時間を要することがわかった。例外オブジェクトの生成は確かに重いことがわかる。


前述のように、isInt() を追加する場合、その処理時間は、ほぼ parseInt() 成功時と同じになると考えて良い。となると、isInt() を parseInt() の前に常に挟むようにすることで、成功時の処理時間は 160 ns に伸びることになる。その代わり parseInt() の失敗は無くなる。中学生のころを思い出すがこういう式になるんで、、、

80x + 2000 = 160x

x=25。25回に1回以上、数字では無いデータがきそうであれば事前に isInt() を走らせた方がいいことになる。数字しかこないのにこのようなチェックを事前に入れると単にパフォーマンスが半分になるだけの結果になる。


25回に一回以上はエラーになるよ!という場合、現状存在しない isInt() を新たに作ることを決心することになるが、単に parseInt() の内容をコピーするだけではシャクである。何かもうちょっと良い工夫は無いものか。


そこで、Object オブジェクトの生成が 4ns であることに着目し、BooleanValuePair という boolean と int 値だけをフィールド変数に持つオブジェクトを新しく作成した。実際作成して生成時間を計測したところ 6ns であった。


parseInt() を単純にコピーして isInt() を作成する代わりに、返却値が、BooleanValuePair である parseInt() を作成する。数字では無いデータがきた場合、NumberFormatException の代わりに、BooleanValuePair の boolean 値を false にして返すのだ。こんな感じになろうかと思う。

/**
 * boolean と int がペアになったオブジェクト。parseInt の返却値として使われる。
 * BooleanValuePair pair = parseInt(i);
 * if (pair.result) {
 *     成功時処理(pair.value);
 * } else {
 *     失敗時処理();
 * }
 * という感じで使える。
 * 
 * @author kamei
 *
 */
public static class BooleanValuePair {
	public static final BooleanValuePair FALSE = new BooleanValuePair(false, 0);
	public BooleanValuePair(final boolean bool, final int value) {
		this.result = bool;
		this.value = value;
	}
	public final boolean result;
	public final int value;
}

/**
* 返却値以外は Integer.parseInt と一緒。基数 は 10 進数固定なので、
* radix チェック部分は取り払った。
* 
* @param 文字列
* @return BooleanValuePair(result=true時、変換成功。value より変換値が取れる)
*/
public static BooleanValuePair parseInt(String s) {
	if (s == null) {
		return BooleanValuePair.FALSE;
	}
	int i = 0, length = s.length();
	int result = 0;
	boolean negative = false;
	int limit;
	int multmin;
	int digit;

	if (length > 0) {
		if (s.charAt(0) == '-') {
			negative = true;
			limit = Integer.MIN_VALUE;
			i++;
		} else {
			limit = -Integer.MAX_VALUE;
		}
		multmin = limit / 10;
		if (i < length) {
			digit = Character.digit(s.charAt(i++), 10);
			if (digit < 0) {
				return BooleanValuePair.FALSE;
			} else {
				result = -digit;
			}
		}
		while (i < length) {
			digit = Character.digit(s.charAt(i++), 10);
			if (digit < 0) {
				return BooleanValuePair.FALSE;
			}
			if (result < multmin) {
				return BooleanValuePair.FALSE;
			}
			result *= 10;
			if (result < limit + digit) {
				return BooleanValuePair.FALSE;
			}
			result -= digit;
		}
	} else {
		return BooleanValuePair.FALSE;
	}
	if (negative) {
		if (i > 1) {
			return new BooleanValuePair(true, result);
		} else {
			return BooleanValuePair.FALSE;
		}
	} else {
		return new BooleanValuePair(true, -result);
	}
}


さて、このバージョンでは、常に 80 ns + 6 ns = 86 ns をかけることで失敗時のコストをなくしている。失敗時には常に同じオブジェクトを返却し、オブジェクト生成を行わないので失敗時の処理時間は 80 ns ということになるかと思う。


この処理を Integer.parseInt() と置き換えた場合は、

80x + 2000 = 86x

なので、約 333 回となる。333 回に一回以上失敗が発生するのであれば、こちらの自作 parseInt() を使用した方が良いということになるだろう。


ただし、今回作成したものは、オリジナルから基数判定処理を取り除いているため、処理時間がオブジェクトの生成を含めても 10ns ほど短縮されてしまった。

80x + 2000 = 70x(答えはマイナス値になっちゃう)

常に自作 parseInt() を使用したほうが良いということになる。


この方法の最大の欠点はレスポンスがオブジェクト値になることだろう。どうも変な感じがしてしまう。また、JDK1.6 では例外オブジェクトの生成は重いが、将来においてはそれが改善される可能性は十分にある。こんなロジックをわざわざ作ったのも良い思い出話になってしまうことだろう。


まあもちろんもっと言えば、そもそものところ変換失敗時においてすら 2000 ns 程度の処理時間であり、100 万回失敗したときに要する時間が 2 秒である。100万件のデータを1時間かけて処理するバッチ処理の中に、parseInt() が 一件あたり 50 回、合計で 5000 万回含まれていたとしても、そのほとんどが成功データであることも加味すると、ここを自作版に置き換えたところで処理時間の改善は良くて 10 秒といったところだろう。そんなのは誤差である。


その他にこの処理が大量にコールされるケースを真面目に考えてみたが、一つも思い浮かばなかった。意外にそんなに多回数呼ばれるような性質の処理ではないのである。(画像処理等は計算回数は多いが、文字列から数字にパースするなんて処理が入る余地はなさそうだし)


結論としては、この部分の処理時間を気にしたって無駄なんで、Integer.parseInt() を常にコールして、例外が飛んできたら失敗扱いとするやりかたがいいのではないかと。


・・・なにこの結論w


追記:
日付関係のパースやチェック処理も事情は似たようなもの。
文字列--> int
文字列--> Date
生成するのが Date だって違いしかない。