Javaについて徹底解説!

Javaのdoubleを基礎から解説 浮動小数点数の考え方を身に着けよう!

大石 英人

開発エンジニア/Java20年/Java GOLD/リーダー/ボールド歴2年

Javaのdoubleはプリミティブ型の一つで、小数点を持つ数字を64ビットの範囲で表現できます。doubleは浮動小数点と指数表記という考え方のおかげで、およそ10308乗というとても大きな数字から、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よりもずっと広い範囲の数字を表現できるのです。

「浮動」小数点数と呼ばれるのは、数字の整数部と小数部の長さが可変なので、小数点の位置が数字に合わせてふわふわ移動するからです。一方で、固定小数点数というものも数値計算の世界にはありますが、Javadouble/floatは浮動小数点数です。

doubleを他のプリミティブ型と簡単に比較すると、以下のとおりです。doubleでは、もっとも広い範囲の数字を扱えることが分かるかと思います。

データ型値の種類ビット数表現できる値の範囲接尾語備考
boolean真偽値1ビットtrue/falseのどちらか
byte整数8ビット(1バイト)-128~127
short整数16ビット(2バイト)-32,768~32,767
char文字16ビット(2バイト)0~65,535Unicode文字、\u0000\uffff
int整数32ビット(4バイト)-2,147,483,648~2,147,483,647±2147百万、Unicodeコードポイント
long整数64ビット(8バイト)-9,223,372,036,854,775,808~9,223,372,036,854,775,807Lまたはl±922
float浮動小数点数32ビット(4バイト)±3.40282347E+38~ 1.40239846E-45Fまたはf単精度
±3.4×1038乗~約±1.4×10-45
double浮動小数点数64ビット(8バイト)±1.79769313486231570E+308~±4.94065645841246544E-324Dまたはd倍精度
±1.8×10308乗~約±4.9×10-324

1-1.doubleは指数表記で広い範囲の数字を表現する

doubleは、ものすごく広い範囲の数字を表現できます。前述の表にもあるとおり、10308乗というちょっとよくわからない大きさの数字や、10のマイナス324乗というこれまたすごい小さな数字を表現できます。

// かなり大きい数字と小さい数字、でもdoubleではまったくもって余裕!!
double tooBig = 1000000000000000000000000000000D;
double tooSmall = 0.00000000000000000000000000001;

これほど広い範囲の数字を表現できるのは、doubleは内部で「指数表記」を使うからです。指数表記では符号、仮数、基数、指数の組み合わせで数字を表現しますが、double64ビットの範囲を符号、仮数、指数それぞれに割り当てています。

指数表記の考え方

例えば、123,000は「1.23×105乗」です。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で小数点以下の数字を扱うと、double64ビットもあるとはいえ限られたビット数しか持っていないため、誤差がどうしても発生します。

1-2-1.誤差の例:0.110回足してみる

例えば、実はdoubleでは0.1すら正確に表せません。以下のとおり、double0.110回足しても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

https://ja.wikipedia.org/wiki/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になります。もう少し正確に言えば、doubledoubleでないもので計算をすると、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除算)を行ってもArithmeticExceptionthrowされません。割られる数により結果は違い、InfinityNaNになります。これらの結果は、NaNInfinityへの計算も含め、浮動小数点数の計算ルールで決められています。

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以外のプリミティブ型への変換は、ビット数が少なくなるので値が変わることがあります。変換方法は、変換先がfloatlongか、それ以外かで少し異なります。

3-2-1.double→floatへのキャスト

キャスト先がfloatの場合は、doubleが持っている仮数と指数がfloatで表現できる範囲に縮小されます。floatで表現できる範囲を超えていると±Infinityになります。また、doubleNaN±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からそれぞれの型のビットサイズに応じたキャストが行われます。±InfinityNaNの扱いは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にすること、またdoubleStringにすることは、プログラムでは日常茶飯事です。ここではその方法をお伝えします。

4-1.String→doubleの変換

ファイルから読み込んだ文字列や、引数で受け取った文字列からdoubleを作りたい時があります。そういう時はDouble.parseDoubleを使って文字列をdoubleに変換しましょう。doubleと解釈できない文字列の場合はNumberFormatExceptionthrowされます。

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に変換した後、doubleValuedoubleに変換するといいでしょう。また、longdoubleにそのまま変換できますので、Long.parseLongの戻り値のlongdoubleにキャストしてもいいです。

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.DecimalFormatString.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

https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/lang/String.html#format(java.lang.String,java.lang.Object…)

 

【参考】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.formatreplaceを経由しているのは、符号が正なら先頭へ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ビットの浮動小数点数を表現するために、doubleDoubleの二つの方法があります。Javaプログラミングの初心者は、なぜ表し方が二つあるのか混乱すると思います。

この章ではその理由と、doubleDoubleの使い分けの方針などをお伝えします。

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かどうかで表現できますが、それをJavadoubleでは上手に表現できません。

ですから、プログラム上では0-1などの値に特別な意味を持たせたりするのですが、確実さには欠けます。そういう値のチェックを忘れるなどのミスもしがちです。

そういう時に参照型であるDoubleを使えば、値がないことをnullとして表現できるのです。Doubleをどういう時に使うか分からない方は、その変数でnullを表現する必要があるかを一つの指針にしてみてください。

5-3.オートボクシングでdoubleDoubleを自動変換する

Java 1.5でオートボクシング(auto boxing)という仕組みが導入されました。オートボクシングで、doubleDoubleをプログラム上でほぼ同じものとして扱えます。

プログラム上でdoubleを使う所ではDoubleを使えますし、Doubleを使う所ではdoubleが使えます。本当のプログラム上は相変わらずdoubleDoubleは別物なのですが、その違いを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の面倒な部分がある程度解消されました。ですが、前述のとおりDoublenullを表せますが、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のような浮動小数点を使った計算では、いろいろな誤差に気を付ける必要があります。誤差が出る理由は、double2進数であることと、数字に使えるビット数、特に仮数が有限であることが主な原因なのですが、ここでは具体例を挙げて説明してみます。

これらの内容を正確に理解するには、数値計算を行う時の「有効数字(有効桁数)」と浮動小数点数の「正規化」という考え方の知識が前提となります。有効数字や正規化の考え方は、以下などを参考にしてください。

【参考】有効数字(Wikipedia)

https://ja.wikipedia.org/wiki/有効数字

 

【参考】浮動小数点数(Wikipedia)

https://ja.wikipedia.org/wiki/浮動小数点数

6-1.丸め誤差/打切り誤差

本来表したい数字と実際の数字に、数字に使える桁数のせいで差が出ることを「丸め誤差」や「打切り誤差」と呼びます。先述のとおり、doubleでは1/10=0.1を正確に表せません。0.12進数だと循環小数ですが、doubleは有限なので、どこかで四捨五入などで丸めるか、打ち切らなければならないのです。

1/10(10進数)
        ↓
0.1(10進数)

        ↓

0.000(1100)(2進数、最後の1100が無限に続く)

        ↓

doubleだと途中で打ち切られ0.1000000000000000055511151231257827021181583404541015625になる

数字はコンピュータ上では2進数になります。2の倍数とその合計で表す2進数では、10進数できっちり表せる小数でもそのまま表せないものがほとんどです。正確な計算が必要なら、二進化十進数(BCDBinary-coded decimal)のクラスや、有理数のまま計算できるクラスなどを使う必要があるかもしれません。

なお、丸め誤差/打切り誤差は2進数だけではありません。例えば、1/310進数で0.333…ですが、有限の桁で表すなら無限に続く3をどこかで終わらせます。そして、1/3とどこかの桁で終わった0.333…は違うものです。これと理屈は同じで、コンピュータは2進数がベースなだけです。

6-2.桁落ち

近い数字同士を引き算した結果、数字の有効数字(有効桁数)が少なくなることを桁落ちと言います。誤差を考慮した計算では、このような計算が途中でされないようにしなければなりません。

以下の例では2つの数字両方で有効数字は15桁で、小数部15桁目に1だけ差があります。d1d2を引き算すると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.情報落ち

情報落ちとは、絶対値が大きく違う数字同士で計算をすると、絶対値が小さな方の数字がなくなってしまう現象です。桁落ちよりは計算誤差への影響は少ないですが、できるだけ避けるべきものなのは変わりません。

例えば、以下の足し算では、d1d2ともに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桁目で切り上げされる

なお、doublefloatなどの浮動小数点のプリミティブ型を使って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.まとめ

この記事では、Javadoubleをお伝えしてきました。Javadouble64ビットの浮動小数点数で、大きな整数から小さな小数点以下の数字まで、大変広い範囲の値を表現できます。doubleJavaで小数点以下の数字を使う時の標準ともいえるデータ型で、詳細は国際規格のIEEE 754で決められているものです。

Javaでは、doubleとして扱えるものにはプリミティブ型のdoubleと、クラス(参照型)としてのDoubleの二種類があり、それらは違うものであることには注意しましょう。ただ、オートボクシングにより違いが見えにくくはなっています。

doubleは、コンピュータで2進数を扱うことの限界・注意点がよくわかるデータ型でもあります。それらの注意点はどのプログラミング言語でも同じですので、Javadoubleの使い方をしっかり学んでおけば、他のプログラミング言語でもそう大きな違いなく使えるでしょう。

特に有効数字や誤差の概念、計算上で発生する問題への対応方法を身に着けておけば、いざという時に役に立つかもしれません。計算は苦手だなぁという方も、考え方だけはしっかり覚えておくと、違いの分かるプログラマとして一目置かれるかもしれませんよ。

私たちは、全てのエンジニアに市場価値を高め自身の望む理想のキャリアを歩んでいただきたいと考えています。もし、今あなたが転職を検討しているのであればこちらの記事をご一読ください。理想のキャリアを実現するためのヒントが見つかるはずです。

『技術力』と『人間力』を高め市場価値の高いエンジニアを目指しませんか?

私たちは「技術力」だけでなく「人間力」の向上をもって遙かに高い水準の成果を出し、関わる全ての人々に感動を与え続ける集団でありたいと考えています。

高い水準で仕事を進めていただくためにも、弊社では次のような環境を用意しています。

  • 定年までIT業界で働くためのスキル(技術力、人間力)が身につく支援
  • 「給与が上がらない」を解消する6ヶ月に1度の明確な人事評価制度
  • 平均残業時間17時間!毎週の稼動確認を徹底しているから実現できる働きやすい環境

現在、株式会社ボールドでは「キャリア採用」のエントリーを受付中です。

まずは以下のボタンより弊社の紹介をご覧いただき、あなたの望むキャリアビジョンをエントリーフォームより詳しくお聞かせください。

コメント

文系IT未経験歓迎!【23卒対象】新卒採用のエントリーを受付中!
文系IT未経験歓迎!
【23卒対象】新卒採用のエントリーを受付中!