Javaのdoubleとは?プリミティブ型の一つ「double」を解説
Javaのdoubleはプリミティブ型の一つで、小数点を持つ数字を64ビットの範囲で表現できます。doubleは浮動小数点と指数表記という考え方のおかげで、およそ10の308乗というとても大きな数字から、10のマイナス324乗というとても小さい数字まで、大変広い範囲の数字を扱えます。
doubleを使えば簡単・高速に計算できますが、きちんと理解をしないままdoubleを使うと、「どうしてこの計算結果になるの?」となることもあります。特に、64ビットもあるとはいえサイズに限りがあることや、2進数によるいろいろな制限や計算上起きる誤差からは逃れられません。
だからこそ、doubleの特徴や使い方をしっかり知っているということは、数値計算がしっかりと行えるスキルがあるということです。どのような計算誤差がどのような仕組みで発生するかあらかじめ分かっていれば対策はしっかりとできますし、結果として実用上求められる精度での計算ができるのです。
この記事ではdoubleについて、そもそもdoubleとはどういうものか、doubleはどうやって使うのか、気を付けたい所などを初心者向けにお伝えします。
※この記事のサンプルは、Java 11の環境で動作確認しています
目次
1.doubleとは64ビットの浮動小数点数
double(ダブル)は、小数点以下を含む幅広い範囲の数字を扱えるデータ型で、プリミティブ型(primitive、基本データ型)の一つです。一つのdoubleのサイズは64ビット(8バイト)です。doubleは「倍精度浮動小数点数」とも呼ばれます。
名前が“double”なのは、何かの「二倍」だからです。その何かとは、同じ基本データ型である32ビット(4バイト)のfloatです。doubleではfloatの倍のビット数を使って、floatよりもずっと広い範囲の数字を表現できるのです。
「浮動」小数点数と呼ばれるのは、数字の整数部と小数部の長さが可変なので、小数点の位置が数字に合わせてふわふわ移動するからです。一方で、固定小数点数というものも数値計算の世界にはありますが、Javaのdouble/floatは浮動小数点数です。
doubleを他のプリミティブ型と簡単に比較すると、以下のとおりです。doubleでは、もっとも広い範囲の数字を扱えることが分かるかと思います。
データ型 | 値の種類 | ビット数 | 表現できる値の範囲 | 接尾語 | 備考 |
boolean | 真偽値 | 1ビット | true/falseのどちらか | – | |
byte | 整数 | 8ビット(1バイト) | -128~127 | – | |
short | 整数 | 16ビット(2バイト) | -32,768~32,767 | – | |
char | 文字 | 16ビット(2バイト) | 0~65,535 | – | Unicode文字、\u0000~\uffff |
int | 整数 | 32ビット(4バイト) | -2,147,483,648~2,147,483,647 | – | 約±21億4千7百万、Unicodeコードポイント |
long | 整数 | 64ビット(8バイト) | -9,223,372,036,854,775,808~9,223,372,036,854,775,807 | Lまたはl | 約±922京 |
float | 浮動小数点数 | 32ビット(4バイト) | ±3.40282347E+38~ 1.40239846E-45 | Fまたはf | 単精度 約±3.4×10の38乗~約±1.4×10の-45乗 |
double | 浮動小数点数 | 64ビット(8バイト) | ±1.79769313486231570E+308~±4.94065645841246544E-324 | Dまたはd | 倍精度 約±1.8×10の308乗~約±4.9×10の-324乗 |
1-1.doubleは指数表記で広い範囲の数字を表現する
doubleは、ものすごく広い範囲の数字を表現できます。前述の表にもあるとおり、10の308乗というちょっとよくわからない大きさの数字や、10のマイナス324乗というこれまたすごい小さな数字を表現できます。
// かなり大きい数字と小さい数字、でもdoubleではまったくもって余裕!! double tooBig = 1000000000000000000000000000000D; double tooSmall = 0.00000000000000000000000000001;
これほど広い範囲の数字を表現できるのは、doubleは内部で「指数表記」を使うからです。指数表記では符号、仮数、基数、指数の組み合わせで数字を表現しますが、doubleの64ビットの範囲を符号、仮数、指数それぞれに割り当てています。
例えば、123,000は「1.23×10の5乗」です。0.0000123も同様に「1.23×10の-5乗」です。両方とも1.23×10の何乗と言っているだけで、違うのは10の乗数だけです。doubleでは大きな数字も小さな数字もこのように言い換えて、ビットを効率的に使っています。
double moreTooBig = 1.0e308; // 1.0×10の308乗 double moreTooSmall = 5.0e-324; // 5.0×10の-324乗
1-2.doubleは誤差と一蓮托生
doubleを使うと、広い範囲の数字の計算を高速に行えます。ですが、その高速さは常に誤差と引き換えです。コンピュータでの数値計算は、昔も今もこれからも、誤差との戦いです。ですから、誤差はdoubleを使う上ではいつも意識しておきたいことです。
doubleのような浮動小数点数が苦手とするのは、小数点以下の数字を正確に表現することです。これは大事なことなので必ず覚えておきましょう。doubleで小数点以下の数字を扱うと、doubleが64ビットもあるとはいえ限られたビット数しか持っていないため、誤差がどうしても発生します。
1-2-1.誤差の例:0.1を10回足してみる
例えば、実はdoubleでは0.1すら正確に表せません。以下のとおり、doubleの0.1を10回足しても1にはなりません。プログラム上では“0.1”と明確に書いていますが、実際のdoubleが表している数字は、0.1とはほんのちょっとだけ違う数字なのです。
double diff = 0.1; double sum = 0; for (int i = 0; i < 10; i++) { sum += diff; } System.out.println(sum); // → 0.9999999999999999 System.out.println(sum == 1); // → false
この0.1との誤差は、丸め誤差というものが影響しています。doubleでは、この他にもいろいろな計算上の誤差が発生します。誤差の種類はあとで簡単に紹介しますが、どうにも計算結果が式と合わないな、という時は計算誤差を疑いましょう。計算誤差を回避するためのテクニックもいろいろあります。
1-3.Javaのdoubleは国際標準規格準拠
Javaのdoubleは、国際的な浮動小数点数の標準規格「IEEE 754」に準拠しています。他のプログラミング言語でもIEEE 754に準拠していることが普通なので、doubleでの計算結果は他のプログラミング言語での実行結果と概ね一致します。
【参考】IEEE 754
IEEE 754では、64ビット中のどの部分を符号、仮数、指数に使うかや、計算ルールが決められています。いろいろな理由や経緯があり、ビットパターンを作るにも少々計算が必要ですが、doubleを使う側はそれを強く意識しなくてもOKです。なお、doubleの基数は2固定なので、この64ビット中に基数はありません。
2.doubleの基本的な使い方
ここでは、doubleの使い方のサンプルをお伝えします。そして、doubleを使う上では注意すべきことがいくつかありますので、それらも順番にお伝えします。
2-1.doubleの宣言の仕方、初期値、リテラル
2-2-1.doubleの変数・配列の宣言、初期値の指定
doubleは型の一つですから、以下のように変数や配列変数の型として使えます。もちろんメソッドの引数や、戻り値としても使えます。
// double型の変数の宣言と、初期値の代入 double d = 12345.789; // double型の配列の宣言と、各インデックスへの値の代入 double[] darr = new double[3]; darr[0] = 1.1; darr[1] = -2.2; darr[2] = 3.3;
2-1-2.doubleのリテラルの書き方
doubleの変数へ代入できる数字のリテラル(文字どおりの、という意味)は、以下のいずれかの形式で書けます。小数点以下の数字を書けるのが、intなどの整数とは違いますね。Javaでは小数点がある数字を書くと、自動的にdouble扱いになります。“_”での桁区切りもできます(Java 7以降)。
- 10進数(整数、小数)
- 10進数+指数表記
- 2進数(0b始まり、整数)、Java 7以降
- 8進数(0始まり、整数)
- 16進数(0x始まり、整数)
- 16進数+指数表記
// 10進数の小数点ありの数字と、2進数・8進数・16進数の整数 double d1 = 12345; // 10進数(整数) double d2 = 1.2345; // 10進数(小数点以下あり) double d3 = -123_456.789_012; // 10進数、区切り文字あり double d4 = 0b0101; // 2進数の整数 double d5 = 012345; // 8進数の整数 double d6 = 0x12345; // 16進数の整数
とても大きい、または小さい数字を表すなら、指数表記も使えます。指数表記にすると自動的にdoubleになります。指数表記は前述のとおり符号、仮数、指数の組み合わせなので、リテラルでも同様に書きます。指数表記でのdoubleの基数は、10(e)と2(p)を選べて、10進数と16進数へだけ使えます。
// 10進数と16進数の指数表記 double d1 = 1.23e4; // 10進数 指数表記(1.23×10の4乗) → 12300 double d2 = -1.23e-4; // 10進数 指数表記(-1.23×10の-4乗) → -0.000123 double d3 = 0x1.23p4; // 16進数 指数表記(0x1.23×2の4乗) → 18.1875 double d4 = -0x1.23p-4; // 16進数 指数表記(-0x1.23×2の-4乗) → -0.071044921875
リテラルがdoubleだと明示的に表現したいなら、接尾語としてDあるいはdを付けて「これはdoubleだよ」とJavaに教えます。小数点以下がない数値リテラルは、Javaでは自動的にintになるので注意しましょう。
// 接尾語としてd/Dを付ければ、そのリテラルはdouble扱いになる double d1 = 5 / 10; // → 0、intの5をintの10で割るので、計算結果はintの0になる double d2 = 5 / 10d; // → 0.5、intの5をdoubleの10で割るので、5もdoubleになり、計算結果はdoubleの0.5になる
なお、doubleには正負の無限大(Inifinity)や非数(NaN、Not a Number)などの、特殊なものがあります。また、0にも正負の0(+0.0、-0.0)があって区別されます。これらはIEEE 754で決められているもので、普通のプログラムで意識することはあまりありませんが、あることは知っておきましょう。
double d1 = Double.POSITIVE_INFINITY; double d2 = Double.NEGATIVE_INFINITY; double d3 = Double.NaN; double d4 = +0.0; double d5 = -0.0;
2-1-3.doubleのフィールド、配列の初期値は0.0
フィールドとしてdouble型の変数を使ったり、double型の配列を使う場合は、未初期化だと0.0になります。他の数値型のプリミティブと同じです。
class DoubleTest { double d; // doubleをフィールドとして宣言したが、初期値は未設定 public static void main(String[] args) { DoubleTest doubleTest = new DoubleTest(); System.out.println(doubleTest.d); // → 0.0、変数宣言時に初期化されていないのでデフォルト値が設定される double[] array = new double[5]; System.out.prdoubleln(array[0]); // → 0.0、newした時点で配列全体が0.0で初期化される } }
2-2.doubleを使った四則演算
2-2-1.doubleが絡むと計算結果はdoubleになる
doubleを使った四則演算(加減乗除)には、四則演算用の算術演算子、つまり+-*/を使います。余りを求めるなら%です。ですので、ごく直感的に計算できるかと思います。
なお、doubleと計算した結果はdoubleになります。もう少し正確に言えば、doubleとdoubleでないもので計算をすると、doubleでないものの方が自動的にdoubleに変換(キャスト)されて計算されます。doubleが関係するキャストのルールは後述します。
double i = 10; double j = 3; double plus = i + j; double minus = i - j; double multiply = i * j; double divide = i / j; double surplus = i % j; System.out.println(plus); // → 13.0 System.out.println(minus); // → 7.0 System.out.println(multiply); // → 30.0 System.out.println(divide); // → 3.3333333333333335(計算上は10 / 3 = 3.333…だが、途中で打ち切られて丸められる) System.out.println(surplus); // → 1.0
2-2-2.doubleには無限大と非数がある
計算した結果、doubleで表現できる範囲よりも絶対値が大きくなったなら無限大(Infinity)になります(いわゆるオーバーフロー)。doubleで表現できる範囲よりも絶対値が小さくなったなら0.0になります(いわゆるアンダーフロー)。
double d1 = Double.MAX_VALUE; double d2 = -Double.MAX_VALUE; double d3 = Double.MIN_VALUE; double d4 = -Double.MIN_VALUE; double d5 = d1 * 2; // doubleの最大値を2倍してみる double d6 = d2 * 2; double d7 = d3 / 1000; // doubleで絶対値が最も小さな値を1000分の1にしてみる double d8 = d4 / 1000; System.out.println(d5); // → Infinty System.out.println(d6); // → -Infinty System.out.println(d7); // → 0.0 System.out.println(d8); // → -0.0
非数(NaN)はどんな数字とどんな計算をしてもNaNになります。この動きは、SQLでのNULLと似ていますね。
double d1 = Double.NaN + 1; double d2 = Double.NaN * 100; double d3 = Double.NaN + Double.POSITIVE_INFINITY; double d4 = Double.NaN + Double.NaN; System.out.println(d1); // → NaN System.out.println(d2); // → NaN System.out.println(d3); // → NaN System.out.println(d4); // → NaN
doubleでは、0での割り算(いわゆる0除算)を行ってもArithmeticExceptionはthrowされません。割られる数により結果は違い、InfinityやNaNになります。これらの結果は、NaNやInfinityへの計算も含め、浮動小数点数の計算ルールで決められています。
double d1 = 1.0 / 0.0; double d2 = 0.0 / 0.0; double d3 = Double.POSITIVE_INFINITY - 1; double d4 = Double.POSITIVE_INFINITY * 0; System.out.println(d1); // → Infinity System.out.println(d2); // → NaN System.out.println(d3); // → Infinity System.out.println(d4); // → NaN
2-3.doubleとビット演算
doubleへは直接ビット演算を行えません。Javaでのビット演算は、intなどの整数型プリミティブかbooleanにだけ定義されている計算だからです。
double d1 = 1.0; double d2 = -1.0; long l = d1 & d2; // コンパイルエラー!! 「演算子 & は引数の型 double, double で未定義です」
どうしてもdoubleが持つビットパターンへビット演算を行いたいなら、Double.doubleToLongBitsあるいはdoubleToRawLongBitsを使ってlongに変換してから行います。ちなみに、longのビットパターンからdoubleに変換するには、Double.longBitsToDoubleを使います。
double d1 = 1.0; double d2 = -1.0; long l1 = Double.doubleToLongBits(d1); // → 0x3ff0000000000000 long l2 = Double.doubleToLongBits(d2); // → 0xbff0000000000000 long l = l1 & l2; // → 4607182418800017408(0x3ff0000000000000) double d3 = Double.longBitsToDouble(l); // → 1.0
2-4.double同士の比較には気を付けよう
double同士を比較する時は、比較演算子==を使います。==での比較では、比べる数字同士が完全に一致していなければtrueにはならず、「大体同じ」とは判断してはくれません。==が正確に比較するのは当たり前のように思えますが、これがdoubleを使う上では思わぬ罠になることもあります。
double d1 = 1.23456789; double d2 = 1.23456789; double d3 = 1.2345678900001; double d4 = 1.2345678900000; System.out.println(d1 == d2); // → true System.out.println(d1 == d3); // → false System.out.println(d1 == d4); // → true
以下の例では、本当にごくわずかしか数字が違わないdouble同士ですが、それでも==の結果はfalseです。実際の数値計算では、計算式の上では値が一致するはずだけれども、いろいろな誤差により完全に一致しないケースは普通にあり得ます。doubleの計算結果は非常に細かい数字になりうるからです。
double d1 = 0.0e-300; // 0そのもの double d2 = 0.1e-300; // 0にものすごく近いが、0ではない System.out.println(d1 == d2); // → false System.out.println(0.0 == d2); // → false
そういうケースでは、ある程度の範囲なら数字のぶれは許容することにして、その範囲内にあれば等しいとみなすこともあります。そうしないと、実用上の計算に支障をきたすこともあるからです。
double d1 = 0.0e-300; // 0そのもの double d2 = 0.1e-300; // 0にものすごく近いが、0ではない double d3 = 0.1e-299; // ±この範囲に収まっていれば、同じと見なす if ((d1 - d3) <= d2 && d2 <= (d1 + d3)) { System.out.println("等しいとみなす"); // こちら!! } else { System.out.println("等しいとみなせない"); }
3.doubleと他のプリミティブ型との変換
Javaでは数字のプリミティブ型として、整数(byte/short/char/long)と、小数点が扱えるもの(float/double)があります。doubleとそれらとの変換や演算の時には注意することがいくつかあります。
3-1.double以外のプリミティブ型→doubleへのキャスト
double以外のプリミティブ型からdoubleへのキャストでは、元のプリミティブ型が持っていた数字がdoubleへそのまま設定されます。doubleは、Javaのプリミティブ型でもっとも表現できる数字の範囲が広いので、どのプリミティブ型の数字でもdoubleで表現できるからです。
整数型プリミティブ同士のキャストのように、キャスト先の型のサイズの分だけビットパターンが同じになるわけではありません。
byte b = Byte.MIN_VALUE; char c = Character.MAX_VALUE; short s = Short.MIN_VALUE; int i = Integer.MIN_VALUE; long l = Long.MIN_VALUE; float f = Float.MIN_VALUE; double d1 = b; // → -128.0 double d2 = c; // → 65535.0 double d3 = s; // → -32768.0 double d4 = i; // → -2.147483648E9 double d5 = l; // → -9.223372036854776E18 double d6 = f; // → 1.401298464324817E-45
3-2.double→double以外のプリミティブ型へのキャスト
doubleからdouble以外のプリミティブ型への変換は、ビット数が少なくなるので値が変わることがあります。変換方法は、変換先がfloatかlongか、それ以外かで少し異なります。
3-2-1.double→floatへのキャスト
キャスト先がfloatの場合は、doubleが持っている仮数と指数がfloatで表現できる範囲に縮小されます。floatで表現できる範囲を超えていると±Infinityになります。また、doubleがNaNや±Infinityだった場合はそのままです。
double d1 = Double.MAX_VALUE; float f1 = (float)d1; System.out.println(f1); // → Infinity double d2 = Double.POSITIVE_INFINITY; float f2 = (float)d2; System.out.println(f2); // → Infinity double d3 = Double.NaN; float f3 = (float)d3; System.out.println(f3); // → NaN
3-2-2.double→longへのキャスト
キャスト先がlongの場合は、doubleで表されている数字の小数部は切り捨てられ、整数部だけをlongで表現できる範囲内で変換します。longで表現できる範囲を超えている場合は、longの±の最大値です。doubleが±Inifinityならlongの最大値・最小値になり、NaNの場合は0です。
double d1 = 1.234e100; // longの範囲を大きく超える数字 long l1 = (long) d1; System.out.println(l1); // → 9223372036854775807、Longの最大値 double d2 = Double.MAX_VALUE; long l2 = (long) d2; System.out.println(l2); // → 9223372036854775807、Longの最大値 double d3 = Double.NaN; long l3 = (long) d3; System.out.println(l3); // → 0 double d4 = Double.POSITIVE_INFINITY; long l4 = (long) d4; System.out.println(l4); // → 9223372036854775807、Longの最大値 double d5 = Double.NEGATIVE_INFINITY; long l5 = (long) d5; System.out.println(l5); // → -9223372036854775808、Longの最小値
3-2-3.double→int/short/char/byteへのキャスト
キャスト先がlong以外の整数型プリミティブ(int/short/char/byte)の場合はlongの場合とほぼ同じですが、変換先の範囲がintになります。さらにそのintからそれぞれの型のビットサイズに応じたキャストが行われます。±InfinityとNaNの扱いはlongと同じです。以下ではintを例としています。
double d1 = 1.234e100; // intの範囲を大きく超える数字 int i1 = (int) d1; System.out.println(i1); // → 2147483647、intの最大値 double d2 = Double.MAX_VALUE; int i2 = (int) d2; System.out.println(i2); // → 2147483647、intの最大値 double d3 = Double.NaN; int i3 = (int) d3; System.out.println(i3); // → 0 double d4 = Double.POSITIVE_INFINITY; int i4 = (int) d4; System.out.println(i4); // → 2147483647、intの最大値 double d5 = Double.NEGATIVE_INFINITY; int i5 = (int) d5; System.out.println(i5); // → -2147483648、intの最小値
4.doubleとStringとの変換
Stringもdoubleと並んでJavaでは重要なクラスです。Stringをdoubleにすること、またdoubleをStringにすることは、プログラムでは日常茶飯事です。ここではその方法をお伝えします。
4-1.String→doubleの変換
ファイルから読み込んだ文字列や、引数で受け取った文字列からdoubleを作りたい時があります。そういう時はDouble.parseDoubleを使って文字列をdoubleに変換しましょう。doubleと解釈できない文字列の場合はNumberFormatExceptionがthrowされます。
String s = "12345.56"; double d1 = Double.parseDouble(s); // doubleに変換する時はparseDoubleを使う Double d2 = Double.valueOf(s); // Doubleに変換する時はvalueOfを使う System.out.println(d1); // → 12345.56 System.out.println(d2); // → 12345.56
Double.parseDoubleやvalueOfが受け付ける文字列は、リテラルのところでもお伝えした、10進数、10進数の指数表記、16進数の指数表記、非数(NaN)、±の無限大(Infinity)です。±0もきちんと区別されます。
System.out.println(Double.parseDouble("-123.456")); // 10進数 System.out.println(Double.parseDouble("123.456e2")); // 10進数(指数表記) System.out.println(Double.parseDouble("0x123.456p2")); // 16進数(指数表記) System.out.println(Double.parseDouble("NaN")); // NaN、"nan"ではエラーになる!! System.out.println(Double.parseDouble("Infinity")); // 正の無限 System.out.println(Double.parseDouble("-Infinity")); // 負の無限 System.out.println(Double.parseDouble("+0")); // 正の0 System.out.println(Double.parseDouble("-0")); // 負の0
なお、整数の2進数、8進数、16進数はDouble.parseDoubleは受け付けません。Long.valueOfの基数を指定できるメソッドでLongに変換した後、doubleValueでdoubleに変換するといいでしょう。また、longはdoubleにそのまま変換できますので、Long.parseLongの戻り値のlongをdoubleにキャストしてもいいです。
System.out.println(Long.valueOf("1010", 2).doubleValue()); // 2進数 System.out.println(Long.valueOf("12345670", 8).doubleValue()); // 8進数 System.out.println(Long.valueOf("123456789ABCDEF", 16).doubleValue()); // 16進数
4-2.double→Stringの変換
4-2-1.Double.toString(double)・String.valueOf(double)で変換する
doubleをStringにしたい時も頻繁にあります。簡単に行うなら、Double.toString(double)か、String.valueOf(double)を使いましょう。ちなみに、どちらを使っても結果は同じです。
double d = 123.456; String s = String.valueOf(d); System.out.println(s); // → "123.456"
4-2-2.BigDecimal.toPlainStringで変換する
絶対値が大きかったり、小数点以下の桁が多いdoubleを、Double.toStringやString.valueOfで文字列にすると指数表記になります。普通の書き方の10進数文字列にしたいなら、BigDecimalに変換した後、BigDecimal.toPlainStringを使うのが簡単です。
double d = 123456789012345e-20; String s1 = String.valueOf(d); System.out.println(s1); // → 1.23456789012345E-6 String s2 = BigDecimal.valueOf(d).toPlainString(); System.out.println(s2); // → 0.00000123456789012345
4-2-3.String.formatやDecimalFormatで書式を付けて変換する
doubleを3桁区切りなどでフォーマットしたり、0埋めをしたり、小数部の四捨五入をしたい場合もあるでしょう。その場合は、java.text.DecimalFormatやString.formatなどを使います。それぞれの書式の詳細は、Javadocを参照してください。
DecimalFormat df1 = new DecimalFormat("#,###.###"); // 3桁区切り、小数4桁目で四捨五入 DecimalFormat df2 = new DecimalFormat("000,000,000.000"); // 3桁区切り、0埋めあり、小数4桁目で四捨五入 DecimalFormat df3 = new DecimalFormat("#,###.00000"); // 3桁区切り、小数5桁目まで0埋めして表示 String s1 = df1.format(d); String s2 = df2.format(d); String s3 = df3.format(d); System.out.println(s1); // → -1,234,567 System.out.println(s2); // → -001,234,567 System.out.println(s3); // → -1,234,567.98760
double d = -1234567.9876; String s1 = String.format("%f", d); // 浮動小数点としてのデフォルト表示 String s2 = String.format("%,.3f", d); // 区切り文字あり、小数部は4桁目で四捨五入 String s3 = String.format("%,018.3f", d); // 符号や区切り文字も含めて全体で18文字、0埋めあり、小数部は4桁目で四捨五入 String s4 = String.format("%e", d); // 指数表記(Eだと大文字) System.out.println(s1); // → -1234567.987600 System.out.println(s2); // → -1,234,567.988 System.out.println(s3); // → -00001,234,567.988 System.out.println(s4); // → -1.234568e+06
【参考】java.text.DecimalFormat
https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/text/DecimalFormat.html
【参考】java.lang.String.format
【参考】java.util.Formatter
https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/util/Formatter.html
4-2-4.Double.doubleToRawLongBits・doubleToLongBitsでビットパターンに変換する
doubleが持つビットパターンをそのまま文字列にしたい場合は、Double.doubleToRawLongBitsまたはdoubleToLongBitsでlongに変換した後、Long.toBinaryStringで01の文字列にします。さらにString.formatとreplaceを経由しているのは、符号が正なら先頭へ0を付けたいからです。
double d = 123.456; String s = String.format("%64s", Long.toBinaryString(Double.doubleToRawLongBits(d))).replace(" ", "0"); System.out.println(s); // → 0100000001011110110111010010111100011010100111111011111001110111
このビットパターンはIEEE 754の倍精度浮動小数点の仕様そのままなので、IEEE 754の勉強をするのにも使えたりします。
なお、Double.doubleToRawLongBits・doubleToLongBitsの違いは非数(NaN)の扱いで、doubleToRawLongBitsはビットパターンそのまま、doubleToLongBitsは正規化された一つのNaNに変換します。
5.doubleとDouble
Javaでは64ビットの浮動小数点数を表現するために、doubleとDoubleの二つの方法があります。Javaプログラミングの初心者は、なぜ表し方が二つあるのか混乱すると思います。
この章ではその理由と、doubleとDoubleの使い分けの方針などをお伝えします。
5-1.二種類の表現方法は性能確保のため
Javaではプリミティブ型のdoubleと、クラスのDoubleは別物です。C#などではこういう区別がないのに、なぜJavaではあるのか。これは、Javaが生まれた当時にプログラムの実行速度を確保するためでした。
Javaは1995年に登場したプログラミング言語です。当時のCPUのクロック周波数は今とは桁が違い、一般向けのCPUでようやく100MHzを超えたくらい。メモリの量も全体で数MB~数10MBと非常に乏しかったものです。
doubleは64ビットの浮動小数点数そのものですし、コンピュータには浮動小数点数を専用に計算するハードウェアが搭載されているので、楽に速く扱えます。しかし、doubleをクラスとすると、一つのdoubleの数字に64ビット以上のメモリを使いますし、計算上でも余分なオーバーヘッドが発生します。
5-2.クラスのDoubleならnullを表現できる
JavaでDoubleを使うのは、Doubleが持つメソッドを使いたい時と、値がない場合…すなわちnullを表現したい時です。例えば、SQLでは値の有り無しをNULLかどうかで表現できますが、それをJavaのdoubleでは上手に表現できません。
ですから、プログラム上では0や-1などの値に特別な意味を持たせたりするのですが、確実さには欠けます。そういう値のチェックを忘れるなどのミスもしがちです。
そういう時に参照型であるDoubleを使えば、値がないことをnullとして表現できるのです。Doubleをどういう時に使うか分からない方は、その変数でnullを表現する必要があるかを一つの指針にしてみてください。
5-3.オートボクシングでdoubleとDoubleを自動変換する
Java 1.5でオートボクシング(auto boxing)という仕組みが導入されました。オートボクシングで、doubleとDoubleをプログラム上でほぼ同じものとして扱えます。
プログラム上でdoubleを使う所ではDoubleを使えますし、Doubleを使う所ではdoubleが使えます。本当のプログラム上は相変わらずdoubleとDoubleは別物なのですが、その違いをJavaが裏で自動的に変換をしてくれるのです。
Double doubleObj = Double.valueOf("12345"); // Double double doublePri = 65535; // double doublePri = doubleObj; // → 12345、doubleにDoubleを代入できる doublePri = new Double("65535"); // → 65535、同上 doubleObj = 12345; // → 12345、Doubleにdoubleを代入できる
これでJavaの面倒な部分がある程度解消されました。ですが、前述のとおりDoubleはnullを表せますが、doubleは必ず何かの整数なので、nullに相当するものがありません。
ですので、以下のように予期せぬところでNullPodoubleerExceptionが発生したりします。これは2019年のJava 11の時点でも変わっていません。プログラマが注意するか、Optionalを使う必要があります。
class IntTest { static Double returnDouble() { return null; } public static void main(String[] args) { double i = returnDouble(); // → nullをdoubleに変換できないので、実行するとNullPointerExceptionが発生する!! } }
6.【発展】doubleと誤差
doubleのような浮動小数点を使った計算では、いろいろな誤差に気を付ける必要があります。誤差が出る理由は、doubleが2進数であることと、数字に使えるビット数、特に仮数が有限であることが主な原因なのですが、ここでは具体例を挙げて説明してみます。
これらの内容を正確に理解するには、数値計算を行う時の「有効数字(有効桁数)」と浮動小数点数の「正規化」という考え方の知識が前提となります。有効数字や正規化の考え方は、以下などを参考にしてください。
【参考】有効数字(Wikipedia)
https://ja.wikipedia.org/wiki/有効数字
【参考】浮動小数点数(Wikipedia)
6-1.丸め誤差/打切り誤差
本来表したい数字と実際の数字に、数字に使える桁数のせいで差が出ることを「丸め誤差」や「打切り誤差」と呼びます。先述のとおり、doubleでは1/10=0.1を正確に表せません。0.1は2進数だと循環小数ですが、doubleは有限なので、どこかで四捨五入などで丸めるか、打ち切らなければならないのです。
1/10(10進数)
↓
0.1(10進数)↓
0.000(1100)(2進数、最後の1100が無限に続く)
↓
doubleだと途中で打ち切られ0.1000000000000000055511151231257827021181583404541015625になる
数字はコンピュータ上では2進数になります。2の倍数とその合計で表す2進数では、10進数できっちり表せる小数でもそのまま表せないものがほとんどです。正確な計算が必要なら、二進化十進数(BCD、Binary-coded decimal)のクラスや、有理数のまま計算できるクラスなどを使う必要があるかもしれません。
なお、丸め誤差/打切り誤差は2進数だけではありません。例えば、1/3は10進数で0.333…ですが、有限の桁で表すなら無限に続く3をどこかで終わらせます。そして、1/3とどこかの桁で終わった0.333…は違うものです。これと理屈は同じで、コンピュータは2進数がベースなだけです。
6-2.桁落ち
近い数字同士を引き算した結果、数字の有効数字(有効桁数)が少なくなることを桁落ちと言います。誤差を考慮した計算では、このような計算が途中でされないようにしなければなりません。
以下の例では2つの数字両方で有効数字は15桁で、小数部15桁目に1だけ差があります。d1とd2を引き算すると10進数では0.000000000000001で、正規化すると1e-15となり、有効数字は15桁から1桁へ一気に減ります。
double d1 = 0.123456789012345; // → 1.23456789012345e-1 double d2 = 0.123456789012344; // → 1.23456789012344e-1 double d3 = d1 - d2; System.out.println(d3); // → 9.992007221626409E-16(0.0000000000000009992007221626409)
実際のdoubleでの計算結果は、2進数への変換誤差のため1e-15ちょうどにはならず、ごくわずかに小さな数字です。そして、計算結果の有効数字は16桁あるように見えますが、計算に使ったdoubleの有効数字より小さい数字なので、計算上で意味がある数字ではありません。
6-2-1.桁落ちは誤差を考慮した計算では致命的
桁落ちが問題なのは、不確かな数字が計算途中に出現することにより、以後の計算結果に大きな影響を与えるからです。
例えば、途中の計算結果で1.000e-3という数字が出たとして、仮数の0の部分が本当に0か、桁落ちによる正規化で埋められた0かはわかりません。もし、計算上でこの数字を使うなら、この不確かな0がずっと付きまとうのです。
doubleで計算をすれば何らかの数字は出て来ます。でも、その数字にどれだけ意味があるかの観点は、プログラマが強く意識すべきことです。そして、桁落ちも2進数特有の事象ではないことは知っておきましょう。
6-3.情報落ち
情報落ちとは、絶対値が大きく違う数字同士で計算をすると、絶対値が小さな方の数字がなくなってしまう現象です。桁落ちよりは計算誤差への影響は少ないですが、できるだけ避けるべきものなのは変わりません。
例えば、以下の足し算では、d1とd2ともに15桁分の有効桁数があります。この二つの数字を足し合わせても、紙で計算したとおりの123456789012345.123456789012345とはならず、123456789012345.12になりました。ということは、d2の方の小数点第3位以下の数字がなくなっていますよね。
double d1 = 123456789012345.0; // → 1.23456789012345e15 double d2 = 0.123456789012345; // → 1.23456789012345e-1 double d3 = d1 + d2; System.out.println(d1); // → 1.23456789012345E14 System.out.println(d2); // → 0.123456789012345 System.out.println(d3); // → 1.2345678901234512E14(123456789012345.12)
こうなる理由は、浮動小数点数での仮数には桁数の制限があるからです。doubleの仮数には、この例での紙で計算した場合の30桁におよぶ有効数字を表せる桁数がありません。ですから、1.2345678901234512までが、この計算結果をdoubleの仮数で表現できる限界だったということですね。
6-3-1.情報落ちには計算順序が影響する
情報落ちが問題になるのは、大きな数字と小さな数字を連続して計算する時です。以下の例では、前者では0.001×10=0.01が計算結果に(誤差は出つつも)反映されていますが、後者では未反映です。基本的には、前者のように小さな値をまとめて先に計算して、その後に大きな値と計算して回避します。
double d1 = 123456789000000.0; double d2 = 0.001; double d3 = 0; for (int i = 0; i < 10; i++) { d3 += d2; // d2を10回足し合わせた数字を先に作って、 } double d4 = d1 + d3; // その結果を足し合わせる System.out.println(d3); // → 0.010000000000000002 System.out.println(d4); // → 1.2345678901234502E14(123456789012345.02)
double d1 = 123456789000000.0; double d2 = 0.001; double d3 = d1; for (int i = 0; i < 10; i++) { d3 += d2; // d2を直接足し合わせる } System.out.println(d3); // → 1.23456789012345E14(123456789012345)
6-4.BigDecimalによる正確な計算の例
正確な数字の表現や計算を行うなら、Javaではjava.math.BigDecimalを使います。ここまでに例として挙げてきた誤差が出る計算も、正確に計算できます。ただ、計算速度はdouble/floatを使う場合よりもどうしても遅くなりますので、要件に応じて使い分けましょう。
// BigDecimalなら0.1を10回足すと1になる!! BigDecimal diff = new BigDecimal("0.1"); BigDecimal sum = BigDecimal.ZERO; for (int i = 0; i < 10; i++) { sum = sum.add(diff); } System.out.println(sum); // → 1.0、0.1を10回足したのと同じ結果になる // BigDecimalを使った桁落ちと情報落ちの確認 BigDecimal bd1 = new BigDecimal("123456789012345"); BigDecimal bd2 = new BigDecimal("0.123456789012345"); BigDecimal bd3 = new BigDecimal("0.123456789012344"); // BigDecimalでも桁落ちはするが、数字は正確 BigDecimal bd4 = bd2.subtract(bd3); // 0.123456789012345 - 0.123456789012344 System.out.println(bd4.toPlainString()); // → 正確に0.000000000000001 System.out.println(bd4.precision()); // → 精度は1なので、桁落ちはしている // doubleでは情報落ちしていた計算でも大丈夫 BigDecimal bd5 = bd1.add(bd2); // 123456789012345 + 0.123456789012345 System.out.println(bd5); // → 123456789012345.123456789012345、情報落ちしていない!!
正確な計算に必要なスケール(小数点以下の桁数)は自動的に判断されますが、任意のスケールや丸め方法を指定することもできます。特に、割り算をする時はどこまでのスケールが必要か指定しないと、割り切ることが出来ない場合などに計算を終えられず、エラーになります。
BigDecimal bd1 = new BigDecimal("1"); BigDecimal bd2 = new BigDecimal("3"); // BigDecimal bd3 = bd1.divide(bd2); // → 1/3は割り切れないので、java.lang.ArithmeticExceptionが発生する BigDecimal bd4 = bd1.divide(bd2, 10, RoundingMode.HALF_EVEN); // 小数点10桁まで、丸めは四捨五入 BigDecimal bd5 = bd1.divide(bd2, 10, RoundingMode.CEILING); // 小数点10桁まで、丸めは切り上げ System.out.println(bd4); // → 0.3333333333、小数点10桁まで計算、11桁目で四捨五入されている System.out.println(bd5); // → 0.3333333334、小数点10桁まで計算、11桁目で切り上げされる
なお、doubleやfloatなどの浮動小数点のプリミティブ型を使ってBigDecimalを生成すると、以下のように浮動小数点数が実際に持っている数字になってしまいます。BigDecimalで正確に数字を表現したいなら、Stringのコンストラクタを使うのが無難です。
BigDecimal bd1 = new BigDecimal(0.1f); // floatの0.1でBigDecimalを生成すると、 System.out.println(bd1); // → 0.1ではない!! 0.100000001490116119384765625 BigDecimal bd2 = new BigDecimal(0.1d); // doubleの0.1でBigDecimalを生成すると、 System.out.println(bd2); // → 0.1ではない!! 0.1000000000000000055511151231257827021181583404541015625
7.まとめ
この記事では、Javaのdoubleをお伝えしてきました。Javaのdoubleは64ビットの浮動小数点数で、大きな整数から小さな小数点以下の数字まで、大変広い範囲の値を表現できます。doubleはJavaで小数点以下の数字を使う時の標準ともいえるデータ型で、詳細は国際規格のIEEE 754で決められているものです。
Javaでは、doubleとして扱えるものにはプリミティブ型のdoubleと、クラス(参照型)としてのDoubleの二種類があり、それらは違うものであることには注意しましょう。ただ、オートボクシングにより違いが見えにくくはなっています。
doubleは、コンピュータで2進数を扱うことの限界・注意点がよくわかるデータ型でもあります。それらの注意点はどのプログラミング言語でも同じですので、Javaでdoubleの使い方をしっかり学んでおけば、他のプログラミング言語でもそう大きな違いなく使えるでしょう。
特に有効数字や誤差の概念、計算上で発生する問題への対応方法を身に着けておけば、いざという時に役に立つかもしれません。計算は苦手だなぁという方も、考え方だけはしっかり覚えておくと、違いの分かるプログラマとして一目置かれるかもしれませんよ。
コメント