先日プログラミングコンテストの過去問を 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 を理解する
まずは概要
プログラミングに入る前に、数学の話です。
数学といっても代数とか圏論とかの怖いやつはでてこないです。僕も怖いので。
だいたいが中学校くらいまでの話。
まずはこれを見てください。
ざっくり説明します。
実数と虚数
普段目にする数はだいたい実数
です。
対して虚数
は便利なので発明された数ですが、現実には存在しません。
代表的なのが√-1
もしくはそれをi
と表現したものですね。わかりやすいですね。僕はよくわかりません。
虚数
は二乗して 0 未満の実数になる数で、実数
はそれ以外と定義されます。
ここを細かく考える気はないので、「だいたい実数
」くらいで大丈夫です。
有理数と無理数
実数
の分類は真剣に考えます。
実数
は有理数
と無理数
に大別されますが、有理数
は整数の比で表現できる数です。
対してそれ以外の整数の比で表現できない数を無理数
と言います。
整数と有限小数と循環小数
いずれも有理数
です。
整数
は説明するまでもありませんね。
3
は3/1
と表現できるので有理数
です。
有限小数は0.5
の様な終わりのある小数
です。
1/2
の様に整数
の比で表現できます。
対して0.333...
や0.142857142857142857...
の様に同じ数字の繰り返しが無限に続く小数
を循環小数
と言います。
これも1/3
や1/7
の様に整数
の比で表現できます。
負の整数と正の整数とゼロと自然数
整数
は一番馴染みがあるのであまり問題ないと思いますが、一応。
負の整数
は-1
や-5
のことで、-5/1
の形で表現できます。
0
は0/1
ですね。
正の整数
についても同様です。
また、正の整数
を自然数
とも言います。(0
を含めるかは本記事では問いません)
小数と分数
少数
と分数
についての補足です。
分数
は数の比で表現される数であり、一見有理数
と同じな気がします。
が、有理数
は整数の比なので、分数の方が広い概念です。
例えば1/√2
なんてのもありです。これは整数の比ではないので無理数
です。
(だいたい 0.7 なので二乗するとだいたい0.5
で0
より大きいので虚数
ではないですね。)
また、例えば無限小数
というものがありますが、先ほどの図で言うと、有理数
の循環小数
も無理数
も無限小数
です。
循環しているかしていないかの違いですね。
押さえておきたい英語
以上の要点を押さえつつ、我々プログラマは英単語も知らないと困るので、ざっくり整理しておきます。
(と言っても絵には英語も入ってますが。)
実数
は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 桁ごとに|
を入れています。)
対してBigInteger
は多倍長整数
です。
こいつは桁あふれが起きそうになると、動的にメモリを確保するのでオーバーフローしません。
(符号や値の保持については実装方法によるので、上図はイメージです。)
固定長整数
はメモリ効率や性能に優れ、多倍長整数
は精度に優れます。
これらは適材適所です。
浮動小数点
整数
と同じく小数
においても同様の考え方があります。
浮動小数点
とは数値の表現方法の一つで、固定長
の仮数部
と指数部
を持つ表現方法です。
ざっくり仮数部
は値で指数部
は桁を表していると考えれば大丈夫。
例えば二進数の0.00000101
は101 * 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
はクラス型と言います。
主な違いはすんごいざっくり言うと「int
はnull
が許容されない」のと、「int
はList<T>
とかのT
になれない」くらいです。
精度とかについてはint
とInteger
に違いはありません。これ大事。
また java にはコンパイラがよしなに相互変換してくれる仕組みがあるので、大体の場合はあんまりどちらかを気にしなくても大丈夫です。
相互変換、の前にメモリの話
普段あんまり考えることはないかもしれませんが、スタック領域とヒープ領域について超ざっくり説明します。
例えばこの様なコードを書いた場合。
(int
とInteger
の変数を区別しやすくするため、本記事では変数名の先頭に大文字を使います。)
Integer Ia = new Integer(1);
この場合、メモリはこんな感じになってます。
new
をするとスタック領域のIa
という変数に何かが入ります。
なんとなくIa
にはインスタンス自体が入ってる気がしますが、入ってるのは矢印だけです。恐ろしい言い方をするとポインタです。
作られたインスタンスはヒープ領域に入っています。
対してプリミティブ型のint
はスタック領域にそのまま確保されます。
Integer Ia = new Integer(1);
Integer Ib = new Integer(1);
int ia = 1;
int ib = 1;
なのでこんなコードを書いた場合の絵は下のようになります。
同一性と同値性
「java で比較に==
を使うんじゃあねぇ」と怖い人に怒られたことがある人はいっぱいいると思いますが、せっかくなのでなんでなのか見てみましょう。
クラス型における同一性
は同じインスタンスかを、同値性
は同じ値かを比較することです。
前者は==
で、後者はequals
によって行われます。また同値性は実装に依存します。
(例えば DDD の entity の比較では identity の一致のみで同値とみなす場合もあります。)
プリミティブ型の==
はシンプルに値を比較します。
なのでIa == Ib
は宛先の違う矢印なので false です。Ia.equals(Ib)
は宛先の値が同じなので true です。
例えるなら「A さんも B さんも 500 円玉を持っていて、物理的には違う硬貨だけど価値は同じ」と言った感じです。
auto boxing と auto unboxing
スタック領域とヒープ領域、比較について理解したところで、相互変換についてです。
int
-> Integer
を boxing , 逆を unboxing と言います。
ラッパークラスの箱に入れるイメージかな。
以下のコードが実行できるのは auto boxing | auto unboxing によるものです。
Integer Ia = new Integer(1);
int ia = Ia; // unboxing
int ib = 1;
Integer Ib = ib; // boxing
内部的にはスタック領域に値を持ってきたり、ヒープ領域にインスタンスを作って参照を得たりしています。
(実際には元の値は消えませんが、イメージしやすいので薄くしています。)
余談 落とし穴
さて、以下のコードはtrue
とfalse
どちらになるでしょうか。
int ia = 1;
int ib = 1;
Integer Ia = ia;
Integer Ib = ib;
Ia == Ib; // true or false ?
auto boxing によってnew
されるのでIa
とIb
の矢印は違うはずです。上の絵でもそうなってます。
が、これtrue
になります。
どうやら auto boxing はInteger#valueOf
で、auto unboxing はInteger#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 unboxing にInteger#intValue
を使うと言うことは、Ia
がnull
の場合に auto unboxing をするとNullPointerException
がでますよ。
int と Integer の違いまとめ
- 精度は同じ
- 比較はちょっと気をつけろよ
- 相互変換は便利だけど完全に置き換えてくれるわけではないから気をつけろよ
ってことですね。
本記事においては以後int
とInteger
やfloat
とFloat
は精度の違いがないため、特に断りなくサンプルコードで都合が良い方を用います。
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
に近いですね。
相互変換
float
とdouble
の変換も、short
とint
と同様に精度の高い方から低い方へ変換すると壊れます。
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
double
の1.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
も違いは精度だけ、小数
は有限のメモリでは表現できないので誤差が前提なんだぜ -
BigInteger
とBigDecimal
は(メモリがある限り)限界がない整数
と小数
だぜ
これだけだ!int
とInteger
の違いは数値表現ってより java のお勉強としてがんばるんだ!
いやーそれにしても勉強になった。普段どれだけ適当にやってきたかを痛感した。
そしてこれを理解したらどうするかと言うと、やっぱドメインロジックとは切り離したいので value object を作って隠蔽するわけだ!
きっちり理解したので普段の業務(ドメイン実装)ではやっぱり使わないわけだ!なんというパラドクス!