Tips

【Java】Integer.parseIntのソースコードを読んでみた【後編】
2020.06.20

【Java】Integer.parseIntのソースコードを読んでみた【後編】

はじめに


前回の続き。
いよいよメインの変換処理について解説していきます。

数値変換


        int result = 0;
        int multmin,digit;
       
        multmin = limit / radix;
        while (i < len) {
            digit = Character.digit(s.charAt(i++),radix);
            //①Character.digitの戻り値が-1ならばエラー出力
            if (digit < 0) {
                throw NumberFormatException.forInputString(s);
            }
            //②??の場合、エラー出力
            if (result < multmin) {
                throw NumberFormatException.forInputString(s);
            }
            result *= radix;
            //③??の場合、エラー出力
            if (result < limit + digit) {
                throw NumberFormatException.forInputString(s);
            }
            result -= digit;
        }

変数宣言は(行が長くなるので)今回新しく登場したもの以外は省略しています。

今回はwhile文の中にif文が3つもあって複雑そうですね…。
ですが、if文の中身をよく見てみると全て例外処理の条件になっています。
ならば、最後まで正常に処理が行われるケース例外が発生するケースとで分けて考えれば良さそうですね!

数値変換(正常系)


        int result = 0;
        int i = 0, len = s.length();
        int digit;

        char firstChar = s.charAt(0);
        if (firstChar < '0') {

            i++;
        }

        while (i < len) {
            digit = Character.digit(s.charAt(i++),radix);
            result *= radix;
            result -= digit;
        }

前回解説した箇所も含めて、正常系の動作に関係する行のみ抽出しました(いくつか途中行を中略しています)。
while文の中身が本メソッドのメインとなる文字列⇒数値変換処理となります。

1行目 : digit = Character.digit(s.charAt(i++),radix);

Character.digit()は、第1引数に渡したchar型の文字を第2引数で渡した基数で解釈して、数値に変換してくれるメソッドです。
今回第1引数には、Integer.parseInt()の引数に渡した文字列(変数s)を先頭から1文字ずつ取り出して渡してあげています。
その際、文字列の先頭に”+”や”-“などの記号が付いている場合は、8行目でiに+1してあげることで2文字目から取り出せるようにしています。

2行目 : result *= radix;

この行の処理は、「桁が1つ大きくなったときに、値が基数倍される」ことを意味しています。

例)
2進数:111 → 1110
10進数: 7 → 14
で2倍に増えている

while文の中でどのように動作するかは、後で具体例を見てみましょう。

3行目 : result -= digit;

Character.digit()の結果を負の値に変換して、結果に追加します。
何故、負の値に変換するのかは後ほど解説します。


各行の処理についてわかったところで、具体的な数値を使って変換されるロジックを考えてみましょう。

例①:変換対象の文字列が"11"で基数が2進数の場合
《while文1週目》
 digit = Character.digit('1',2); //digit = 1
 result *= 2; //result = 0
 result -= 1; //result = -1
《while文2週目》
 digit = Character.digit('1',2); //digit = 1
 result *= 2; //result = -1 * 2 = -2
 result -= 1; //result = -2 - 1 = -3

例②:変換対象の文字列が"ff"で基数が16進数の場合
《while文1週目》
 digit = Character.digit('f',16); //digit = 15
 result *= 16; //result = 0
 result -= 15; //result = -15
《while文2週目》
 digit = Character.digit('f',16); //digit = 15
 result *= 16; //result = -15 * 16 = -240
 result -= 15; //result = -240 - 15 = -255

以上、文字列を10進数の(負の)数値に変換できていますね。

数値変換(異常系①)


続いては異常系3パターンを見て行きます。
まずは1つ目の異常系から。

    char firstChar = s.charAt(0);
    //1文字目が数字でないならばtrue
    if (firstChar < '0') {
        //1文字目が'-'(マイナス)ならばtrue
        if (firstChar == '-') {
        //1文字目が'+'でないならばエラー出力
        } else if (firstChar != '+')
            throw NumberFormatException.forInputString(s);
            
        i++;
    }
    multmin = limit / radix;
    while (i < len) {
        //①Character.digitの戻り値が-1ならばエラー出力
        digit = Character.digit(s.charAt(i++),radix);
        if (digit < 0) {
            throw NumberFormatException.forInputString(s);
        }

前編の記事で説明した通り、1文字目に’*’とか’=’などの記号が含まれている場合は8行目で処理できます。
しかし、’?’などのようにUnicode上で’0’よりも大きな数字が割り当てられている記号に関しては、3行目のif文でfalseとなるため、8行目で排除することはできませんでした。
あるいは、2文字目以降に記号が紛れ込んでいる場合も、当然8行目で排除することができません。

それらのケースに対応する処理が16行目のif文になります。
Character.digit()は引数に渡されたchar型の値が数値に変換できない記号であった場合は戻り値として-1を返します。
そのため、8行目で撃ち漏らした記号が文字列に含まれていた場合、変数digitに-1が代入され、

        if (digit < 0) {
            throw NumberFormatException.forInputString(s);
        }

で処理される訳です。

数値変換(異常系②)


続いて2つ目の異常系です。

int multmin;

multmin = limit / radix;
while (i < len) {
    digit = Character.digit(s.charAt(i++),radix);
    //②??の場合、エラー出力
    if (result < multmin) {
        throw NumberFormatException.forInputString(s);
    }
    result *= radix;
    result -= digit;
}

ここでは、resultがmultminよりも低い値であれば例外を発生させていますが、これにはどのような意味があるのでしょうか?
multminの定義を見てみると、

multmin = limit / radix;

となっていますが、これにどのような数学的意味があるのかはよくわかりません。
ひとまずは、条件式を変換してみましょう。

if (result < multmin)
→ if (result < limit / radix)
→ if (result * radix < limit)

ここまで変換してみて、気が付きましたでしょうか。
この条件式の左辺result * radixは、この後の10行目で行う処理そのままです。
その処理の結果得られる値(負の値)とlimitを比較しているということはつまり…
10行目の処理をしてもintの範囲超えないよね!(10行目の処理をしても大丈夫!)
ということを意味しているんですね。

※limit…変換対象の文字列が正の場合は-Integer.MAX_VALUE(-2147483647)
負の場合はInteger.MIN_VALUE(-2147483648)が代入される。

数値変換(異常系②)


最後、3つ目の異常系です。

while (i < len) {
    digit = Character.digit(s.charAt(i++),radix);
    result *= radix;
    //③??の場合、エラー出力
    if (result < limit + digit) {
        throw NumberFormatException.forInputString(s);
    }
    result -= digit;
}

こちらも条件式を変換してみましょう。

if (result < limit)
→ if (result - digit < limit)

もうお分かりですね。
この条件式の左辺result – digitは、この後の8行目で行う処理そのままとなっています。
つまり、このif文で引っ掛からなければ、
8行目の処理を行ってもintの範囲を逸脱しないよ!
ということを表しているんですね。


という訳で、例外処理の部分をまとめると以下のようになります。

while (i < len) {
    digit = Character.digit(s.charAt(i++),radix);
    //①記号が混じっていたら例外発生
    if (digit < 0) {
        throw NumberFormatException.forInputString(s);
    }
    //②11行目の処理をしたときに、intの範囲を逸脱したら例外発生
    if (result < multmin) {
        throw NumberFormatException.forInputString(s);
    }
    result *= radix;
    //③16行目の処理をしたときに、intの範囲を逸脱したら例外発生
    if (result < limit + digit) {
        throw NumberFormatException.forInputString(s);
    }
    result -= digit;
}

戻り値


ここまで長かったですね…。
いよいよ最後、変換した数値をreturnしてInteger.parseInt()の全処理は終了です。
return文ではオシャレに三項演算子を使っています。

boolean negative = false;

char firstChar = s.charAt(0);
if (firstChar < '0') { // Possible leading "+" or "-"
    if (firstChar == '-') {
        negative = true;
    }
}

return negative ? result : -result;

negativeがtrueの場合(文字列の一文字目が”-“の場合)は、resultの値をそのままreturnし、
negativeがfalseの場合(文字列の一文字目が”-“でない場合)は、resultの値を正負逆転させてreturnしています。
変数resultは、ずっと負の値を扱ってきたので、これにて無事returnが完了しました。

resultやlimitに負の値を使用する理由


以上、Integer.parseInt()の内部処理について簡単に解説してきましたが、いくつか説明を後回しにした箇所がありましたね。

limitはint型に代入できる値の最大値・最小値を保持する変数です。
初期値は-Integer.MAX_VALUE(-2147483647)で、1文字目が’-‘(マイナス)の場合はInteger.MIN_VALUE
(-2147483648)が代入されます。
なぜ、Integer.MAX_VALUEに’-‘をつけるのか?その理由は後ほど説明します

3行目 : result -= digit;

Character.digit()の結果を負の値に変換して、結果に追加します。
何故、負の値に変換するのかは後ほど解説します

最後にこれらについて考えてみましょう。
ポイントはint型の範囲です。

int型の範囲は-2147483648~2147483647です。
ここで、Integer.parseInt()の引数に渡された文字列が”-2147483648″であった場合、resultやlimitに正の値を使用して変換処理が行えるでしょうか?
答えはです。(int型である)resultにもlimitにも値は最大で2147483647までしか入らないので、少なくとも既存のロジックでは処理が行えません。

つまり、resultやlimitに負の値を使用する理由は、引数に渡された文字列が”-2147483648″であった場合でも、他の数値が渡された場合と同じロジック(コード)を使用して処理が行えるようにするため、なのですね。

おわりに


Integer.parseInt()の仕組みについて見てきました。
こんな面倒な処理が、先人たちの努力によって簡単に利用できるなんて、感謝しなければなりませんね。

また、int型の範囲チェックをするときは負の値に変換するとコンパクトな処理が書けるという、使いどころのわからない教訓が得られました。
役立てられる日は来るのでしょうか…。

新連載はじまりました!新Java基礎 連載リンク

はじめてのJAVA 連載

Recent News

Recent Tips

Tag Search