Help us understand the problem. What is going on with this article?

java の int と Integer と BigInteger の違いや float や double を理解する

先日プログラミングコンテストの過去問を haskell でやった時に、べき乗をなんやかんやするロジックを書いた時に最終結果が12340000で欲しかったのに12340000.0になってしまって通りませんでした。恥ずかしい。

恥ずかしいけど、聞くは一時の恥聞かぬは一生の恥の解説とも言うし、知らないことは素直に学んで修めればなかったことにできるんです。できんの?

よくわからんけど、要するに曖昧なまま使ってたintみたいなものをきっちりまとめるぜって、そんな話。

かるーく背景

普段は java / DDD で契約管理のシステムを作ってるんだけど、扱う数字なんてたかだか数万の整数くらいなんだよね。ちょっとした円だとか契約の数を数えたりだとか、その程度。
なんと月の請求に日割りがないしね。驚きだね。

DDD の value object とかのおかげで、生のintとかを触ることもあんまりないしね。

なのでこの記事を書く直前の例えば java の理解度はだいたいこんな感じ。

byte? よくわからんけど怖い」
long? 長そ〜」
BigInteger? でかそ〜」
float? ふわふわ〜」
double? 何が倍なの〜?とうおるるるるるるるる」

ってくらいの理解度。これ大マジ。

(double のイタリア語が doppio ってことだけは知ってたんだよォォォ)

なのでそれくらいの人がまとめたんだよってことだけ了承してね!

haskell 版もよろしく

この記事の確認言語には java を使っています。

もともと haskell で確認してたのですが、いくつかの言語を確認した方がより理解が進むかと思って java でも試してみました。

python でも少し確認したりしたのですが、記事は本懐の haskell と仕事で使ってる java にしようかと思います。

というわけで、こっちもよろしく。→ haskell の Int と Integer の違いや Float や Double や Rational を理解する

まずは概要

プログラミングに入る前に、数学の話です。

数学といっても代数とか圏論とかの怖いやつはでてこないです。僕も怖いので。
だいたいが中学校くらいまでの話。

まずはこれを見てください。

スクリーンショット 2020-03-15 23.42.35.png

ざっくり説明します。

実数と虚数

普段目にする数はだいたい実数です。

対して虚数は便利なので発明された数ですが、現実には存在しません。
代表的なのが√-1もしくはそれをiと表現したものですね。わかりやすいですね。僕はよくわかりません。

虚数二乗して 0 未満の実数になる数で、実数それ以外と定義されます。

ここを細かく考える気はないので、「だいたい実数」くらいで大丈夫です。

有理数と無理数

実数の分類は真剣に考えます。

実数有理数無理数に大別されますが、有理数整数の比で表現できる数です

対してそれ以外の整数の比で表現できない数無理数と言います。

整数と有限小数と循環小数

いずれも有理数です。

整数は説明するまでもありませんね。
33/1と表現できるので有理数です。

有限小数は0.5の様な終わりのある小数です。
1/2の様に整数の比で表現できます。

対して0.333...0.142857142857142857...の様に同じ数字の繰り返しが無限に続く小数循環小数と言います。
これも1/31/7の様に整数の比で表現できます。

負の整数と正の整数とゼロと自然数

整数は一番馴染みがあるのであまり問題ないと思いますが、一応。

負の整数-1-5のことで、-5/1の形で表現できます。

00/1ですね。

正の整数についても同様です。

また、正の整数自然数とも言います。(0を含めるかは本記事では問いません)

小数と分数

少数分数についての補足です。

分数数の比で表現される数であり、一見有理数と同じな気がします。
が、有理数整数の比なので、分数の方が広い概念です。

例えば1/√2なんてのもありです。これは整数の比ではないので無理数です。
(だいたい 0.7 なので二乗するとだいたい0.50より大きいので虚数ではないですね。)

また、例えば無限小数というものがありますが、先ほどの図で言うと、有理数循環小数無理数無限小数です。
循環しているかしていないかの違いですね。

押さえておきたい英語

以上の要点を押さえつつ、我々プログラマは英単語も知らないと困るので、ざっくり整理しておきます。
(と言っても絵には英語も入ってますが。)

実数real number虚数imaginary numberです、イメージつきやすいですね。

あまり馴染みはないですが、有理数rational numberです。コピペで書いてると稀にぶち当たります。

ratio比率という意味なので変数名で使ったことがある人もいるのではないでしょうか。

また、絵にはないですが小数decimal分数fractionです。

プログラミングの世界へ ( java )

さっそく java でサンプルコードを見たいところですが、人間の世界とコンピュータの世界では大きく違うことがあります。

それは「メモリが有限」ということです。

どこにそれが関係するかと言うと、例えば「すげーでけー数」と「無限小数」です。

固定長整数

例えば java のintは 32bit 固定の整数です。

コンピュータのメモリには限界があるので、数値を 32 の0|1の範囲に限定して表現します。

多倍長整数

対して多倍長整数扱う数に応じて動的にメモリを確保する数値の表現方法です。

理論上は無限の数を扱うことができます。(もちろんコンピュータのメモリの許す限りですが。)

固定長整数と多倍長整数

みんな大好きオーバーフローはこの固定長整数が引き起こします。

たとえば java のbyteは 8bit 固定の整数です。
頭の 1bit を正負の符号に、残りを値の表現に使います。

0000|0000から1ずつ増加を始め、0111|1111から1000|0000になるところでオーバーフローし、
1111|1111から1|0000|0000になるところで 9bit 目が範囲外になり0000|0000として扱われます。
(見やすくするために 4 桁ごとに|を入れています。)

スクリーンショット 2020-03-16 0.36.23.png

対してBigInteger多倍長整数です。
こいつは桁あふれが起きそうになると、動的にメモリを確保するのでオーバーフローしません。

スクリーンショット 2020-03-16 2.43.29.png

(符号や値の保持については実装方法によるので、上図はイメージです。)

固定長整数はメモリ効率や性能に優れ、多倍長整数は精度に優れます。
これらは適材適所です。

浮動小数点

整数と同じく小数においても同様の考え方があります。

浮動小数点とは数値の表現方法の一つで、固定長仮数部指数部を持つ表現方法です。

ざっくり仮数部は値で指数部は桁を表していると考えれば大丈夫。

例えば二進数の0.00000101101 * 2^-8の様に表されます。

ただこれだと10.1 * 2^-7とかでも表現できちゃうので、IEEE754と言う規格で仮数部1.xにすると決まってます。なので1.01 * 2^-6です。
1.01e-6なんて書いたりもします。

コード書いていてたまに出るe入ってるやつはこれだね。怖かったけど克服したぞ。

仮数部指数部によって小数点を打つ位置が変わってくるので浮動小数点と言うのかな。
一方で対になる単語は固定小数点で、例えば整数がこれに含まれます。

java で確認

前置きが長くなりました。ここからはガシガシ java で確認していきます。

type 説明
byte, Byte 8bit 固定長整数
short, Short 16bit 固定長整数
int, Integer 32bit 固定長整数
long, Long 64bit 固定長整数
float, Float 単精度浮動小数点 ( 32bit )
double, Double 倍精度浮動小数点 ( 64bit )
BigInteger 多倍長整数
BigDeciaml 多倍長小数

以下のコードはSystem.out.printlnに相当するものは省略し、その行のコメントがその結果とします。

byte, short, int, long

たくさんあるけど恐れることはありません。

こいつらは全部固定長整数で、違いは表現できる精度しかありません。

Byte.MAX_VALUE;       // 127
Short.MAX_VALUE;      // 32767
Integer.MAX_VALUE;    // 2147483647
Long.MAX_VALUE;       // 9223372036854775807

例えばIntegerの上限値に+1すると、オーバーフローします。

Integer.MAX_VALUE + 1;    // -2147483648

相互変換

また当然ですが、精度の低い方から大きい方へのキャストは問題ありませんが、逆は正しく行えません。

short s = 20000;

(int) s;    // 20000
int i = 40000;

(short) i;    // -25536

ところで int と Integer の違い

もともとの趣旨とは離れるのですが、案外面白いのでせっかくと言うことで。

java のintはプリミティブ型で、Integerはクラス型と言います。

主な違いはすんごいざっくり言うと「intnullが許容されない」のと、「intList<T>とかのTになれない」くらいです。
精度とかについてはintIntegerに違いはありません。これ大事。

また java にはコンパイラがよしなに相互変換してくれる仕組みがあるので、大体の場合はあんまりどちらかを気にしなくても大丈夫です。

相互変換、の前にメモリの話

普段あんまり考えることはないかもしれませんが、スタック領域とヒープ領域について超ざっくり説明します。

例えばこの様なコードを書いた場合。
intIntegerの変数を区別しやすくするため、本記事では変数名の先頭に大文字を使います。)

Integer Ia = new Integer(1);

この場合、メモリはこんな感じになってます。

スクリーンショット 2020-03-16 1.31.34.png

newをするとスタック領域のIaという変数に何かが入ります。
なんとなくIaにはインスタンス自体が入ってる気がしますが、入ってるのは矢印だけです。恐ろしい言い方をするとポインタです。

作られたインスタンスはヒープ領域に入っています。

対してプリミティブ型のintはスタック領域にそのまま確保されます。

Integer Ia = new Integer(1);
Integer Ib = new Integer(1);
int ia = 1;
int ib = 1;

なのでこんなコードを書いた場合の絵は下のようになります。

スクリーンショット 2020-03-16 1.36.04.png

同一性と同値性

「java で比較に==を使うんじゃあねぇ」と怖い人に怒られたことがある人はいっぱいいると思いますが、せっかくなのでなんでなのか見てみましょう。

クラス型における同一性同じインスタンスかを、同値性同じ値かを比較することです。
前者は==で、後者はequalsによって行われます。また同値性は実装に依存します。
(例えば DDD の entity の比較では identity の一致のみで同値とみなす場合もあります。)

プリミティブ型の==はシンプルに値を比較します。

スクリーンショット 2020-03-16 1.40.54.png

なのでIa == Ib宛先の違う矢印なので false です。Ia.equals(Ib)宛先の値が同じなので true です。

例えるなら「A さんも B さんも 500 円玉を持っていて、物理的には違う硬貨だけど価値は同じ」と言った感じです。

auto boxing と auto unboxing

スタック領域とヒープ領域、比較について理解したところで、相互変換についてです。

int -> Integerboxing , 逆を unboxing と言います。
ラッパークラスの箱に入れるイメージかな。

以下のコードが実行できるのは auto boxing | auto unboxing によるものです。

Integer Ia = new Integer(1);
int ia = Ia;                   // unboxing

スクリーンショット 2020-03-16 1.47.07.png

int ib = 1;
Integer Ib = ib;               // boxing

スクリーンショット 2020-03-16 1.47.15.png

内部的にはスタック領域に値を持ってきたり、ヒープ領域にインスタンスを作って参照を得たりしています。
(実際には元の値は消えませんが、イメージしやすいので薄くしています。)

余談 落とし穴

さて、以下のコードはtruefalseどちらになるでしょうか。

int ia = 1;
int ib = 1;
Integer Ia = ia;
Integer Ib = ib;

Ia == Ib;    // true or false ?

auto boxing によってnewされるのでIaIbの矢印は違うはずです。上の絵でもそうなってます。

が、これtrueになります。

どうやら auto boxingInteger#valueOfで、auto unboxingInteger#intValueによって実現される様です。

Integer Ia = Integer.valueOf(ia);
Integer Ib = Integer.valueOf(ib);

で、肝心のInteger#valueOfですが、こんな実装になっています。

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

どうやらよく使う-128~127はキャッシュされているみたいですね。なので上のコード例だとnewされないです。

こんなコードだとちゃんとfalseになります、理解は間違ってなかった様で安心だ。

int ia = 1000;
int ib = 1000;
Integer Ia = ia;
Integer Ib = ib;

Ia == Ib;    // false

あ、ちなみに内部では auto unboxingInteger#intValueを使うと言うことは、Ianullの場合に auto unboxing をするとNullPointerExceptionがでますよ。

int と Integer の違いまとめ

  • 精度は同じ
  • 比較はちょっと気をつけろよ
  • 相互変換は便利だけど完全に置き換えてくれるわけではないから気をつけろよ

ってことですね。

本記事においては以後intIntegerfloatFloatは精度の違いがないため、特に断りなくサンプルコードで都合が良い方を用います。

float, double

整数は押さえましたね。次は小数です。

doubleの何がなんだって思ってましたが、勉強すれば明瞭ですね。
floatは 32bit を使って、doubleは 64bit を使って値を表現するということでした。だから倍精度。

コンピュータのメモリが有限である以上無限小数を完全に表現することは不可能なので、誤差が出る前提で扱わなければなりません。

例えば十進数の0.01は二進数だと有限で表現することができません。
有限で表現できない以上どこかで諦めなければいけず、それを繰り返せば誤差が大きくなるのはなんとなく感覚で理解できますね。

で、どんな誤差が出るか、です。試してみましょう。

float f = 0;
for (int i = 0; i < 100; i++) {
    f += 0.01f;
}

double d = 0;
for (int i = 0; i < 100; i++) {
    d += 0.01d;
}

f;    // 0.99999934
d;    // 1.0000000000000007

doubleの方が1.0に近いですね。

相互変換

floatdoubleの変換も、shortintと同様に精度の高い方から低い方へ変換すると壊れます。

f;             // 0.99999934
d;             // 1.0000000000000007

(double) f;    // 0.9999993443489075
(float) d;     // 1.0

doubleからfloatにした場合は欠けてしまっていますね。

またそもそも有限なので、単純に以下の様な値で誤差が出ます。

10d / 3d;       // 3.3333333333333335
1.00000001f;    // 1.0

BigInteger, BigDecimal

お待たせしました、多倍長の奴らです。

こいつらは桁に応じて動的にメモリを確保するので、オーバーフローしないし誤差も出ません。なんかすごい。

さっそく試してみましょう。

BigDecimal

小数BigDecimalから試してみます。初っ端から気前よく巨大な整数を扱ってみましょう。

BigDecimal bd = new BigDecimal(Long.MAX_VALUE);

bd;                           // 9223372036854775807

bd.add(new BigDecimal(1));    // 9223372036854775808

longの上限に加算してもオーバーフローしてません。
もっと思い切りよく足しても全然大丈夫。

bd.add(bd);                   // 18446744073709551614

小数も加算できる。

bd.add(new BigDecimal(0.5));  // 9223372036854775807.5

本命?の小数の誤差はどうでしょうか。

BigDecimal bd = BigDecimal.ZERO;
BigDecimal x = new BigDecimal(0.01);
for (int i = 0; i < 100; i++) {
    bd = bd.add(x);
}

bd;    // 1.00000000000000002081668171172168513294309377670288085937500

double1.0000000000000007より精度が良いですね。(toString出来ているのはすげぇ頑張ってるからです。)

doubleで誤差が出た10d / 3dはどうでしょうか。

BigDecimal bd10 = new BigDecimal(10);
BigDecimal bd3  = new BigDecimal(3);

bd10.divide(bd3);    // ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

terminatingって単語は冒頭のベン図っぽいもので見ましたね、有限小数じゃあないって怒られてます。

誤差のでる値は誤差のあるまま持たせてくれないみたいですね。
切り捨てたり切り上げたりを明示しないとだめみたいです。

bd10.divide(bd3, RoundingMode.FLOOR) // 3

bd10.divide(bd3, RoundingMode.CEILING) // 4

BigInteger

こいつは簡単です。小数の扱えないBigDecimalです。

BigInteger bi = BigInteger.valueOf(Long.MAX_VALUE);

bi;            // 9223372036854775807

bi.add(bi);    // 18446744073709551614

BigIntegerには0.5みたいな小数を渡せる生成メソッドがないので、BigDecimalと比べると「これだけ」です。

もう大丈夫。怖くない。

余談 破壊/非破壊

ところでなんとなく java の感覚だとaddすると破壊する気がしませんか?
List#addとかそうじゃん。

けど、addするたびにメモリを確保し直す可能性があることを理解していると、非破壊で毎度違うインスタンスを作ってるって考えやすいよね。
(実装方法によるので immutable の場合もあるけど mutable の場合もあるらしい。)

まとめ

ながーい記事になったけど、やってみて感じた java における数値表現の要点は3つだけだ!

  • byte, short, int, longの違いは精度だけ、それぞれ限界があるぜ
  • float, doubleも違いは精度だけ、小数は有限のメモリでは表現できないので誤差が前提なんだぜ
  • BigIntegerBigDecimalは(メモリがある限り)限界がない整数小数だぜ

これだけだ!intIntegerの違いは数値表現ってより java のお勉強としてがんばるんだ!

いやーそれにしても勉強になった。普段どれだけ適当にやってきたかを痛感した。

そしてこれを理解したらどうするかと言うと、やっぱドメインロジックとは切り離したいので value object を作って隠蔽するわけだ!
きっちり理解したので普段の業務(ドメイン実装)ではやっぱり使わないわけだ!なんというパラドクス!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away