算術演算の Tips and Tricks

Revised: Feb./7th/2005; Since: Feb./23rd/2003

コンピュータのことを計算機と呼ぶことがあります。科学技術計算の現場では「計算機を回す」と言いますし、企業や研究施設の「電算センター」、「計算機室」という名称にその名残があります。現在の分散系システムでは通信機能の方が注目される傾向にありますが、計算は今でもコンピュータの重要な仕事です。コンピュータ上では、限られた桁の2進数に対して、ビット反転を組み合わせることで四則演算を表現するので、特有の問題が生じることもあります。ここではJavaでの数値表現について紹介します。

プリミティブ型とラッパー・クラスの違いは?

数値、ブール代数、文字を表現するデータは、ラッパー・クラスと呼ばれる参照型のオブジェクトでも表現できます。しかし、これはオブジェクト生成を含むコストの高い方法です。

基本的なデータには参照型でない(オブジェクトでない)データ型が用意されています。プリミティブ型と呼ばれるこれらのデータ型には、対応するクラスも用意されており、ラッパー・クラスと呼ばれます。ラッパー・クラスは、プリミティブ型データを格納する不変オブジェクトを生成します(表1)。

表1. プリミティブ型と対応するラッパー・クラス
意味プリミティブ型ラッパー・クラス
8ビット符号付整数bytejava.lang.Byte
16ビット符号付整数shortjava.lang.Short
32ビット符号付整数intjava.lang.Integer
64ビット符号付整数longjava.lang.Long
16ビットUNICODE(文字)charjava.lang.Character
32ビット符号付浮動小数点数floatjava.lang.Float
64ビット符号付浮動小数点数doublejava.lang.Double
ブール代数(真偽値)booleanjava.lang.Boolean

Javaアプリケーションは、メモリ上の、スタックとヒープと呼ばれる領域で実行されます。プリミティブ型データはスタックに格納されます。オブジェクトは、インスタンス(メソッドや型、及びデータ)がヒープに格納され、そのポインタがスタックに格納されます。ラッパー・クラス型オブジェクトより、プリミティブ型の方がメモリ・コストもパフォーマンスも良くなります。ラッパー・クラスでなければならない事情として、次のことが挙げられます:

他のクラスのメソッドから、オブジェクトが要求されているなど、止むを得ないとき以外は、ラッパー・クラスを使わない方が良いでしょう。

浮動小数点の比較は危険?

浮動小数点の計算結果は、数学的に完全に同じになることは滅多にありません。ほとんどの場合、微小な誤差を含んでいます。例えば、1.0 - 0.9 と 1.0/10.0 は異なり、0.1 は 10 回足しても 1.0 にはなりません。次の単純な例を見てください。

class FloatingDemo {
	public static void main(String[] args) {
		double d = 0.0;
		// 0.1 を 10 回加算
		for (int i = 0; i < 10; i++) {
			d += 0.1;
		}
		System.out.println(d);
		System.out.println(1.0 - d);
		// 16進数表現
		System.out.println(Long.toHexString(Double.doubleToRawLongBits(d)));
	}
}
>javac FloatingDemo.java

>java FloatingDemo
0.9999999999999999
1.1102230246251565E-16
3fefffffffffffff

コンピュータは内部的には二進数で表現して演算しています。つまり、小数の場合は、1/2 + 1/22 + 1/23 + 1/24 という形式で表現しています。10 進数で考えた場合と、殆ど、概ね、正確に変換できますが、ほんのわずかだけ異なることがあります。

例えば、0.1 の 10 倍は 1 です。float 型でも double 型でも同じです。しかし、先ほどの例で見たとおり、0.1 を 10 回足しても、わずかに 1 に足りません。例えば、0.1 を 2 進数で表現すると次のようになります。

100110011001100110011001100110011001100110011001...

これを 2 の累乗の分数で表現すると、次のようになります。

1/10 = 1/16 + 1/32 + 1/256 + 1/512 + 1/4096 + 1/8192 + ...

これは無限級数であり、厳密な値を求めるためには、無限に演算する必要があります。

一般に、2 進数では、数を Σi ai · 2i (-∞ < i < ∞) という、2 の級数で表現するため、実際には途中で計算を打ち切ることになり、微小な誤差が生まれます。厳密な値との誤差を打切り誤差と呼びます。一般に、算術計算では切り捨て/四捨五入/切り上げが必要になることが殆どで、そのために生じる誤差を丸め誤差と呼びます。

よって、浮動小数点数同士を比較するときは、比較対照の絶対値に対して十分に大きな差による大小比較は安全ですが、== による等しいことの判断は危険です。簡単な計算の結果、等しくなるつもりでも、演算順序や値の受け取り方によって誤差が生じ、== の評価結果が変わってしまう可能性があるからです。

可能であれば、浮動小数点同士の比較は避けた方が無難です。一般には、次のような回避策が挙げられます。

もっと厳密に計算できる?

浮動小数点に関する誤差は、二進数と十進数の差に起因するもので、BigDecimalクラスを使うことで回避できます。

一般に、算術計算時には様々な誤差が発生します。古くから計算機の計算結果を正しく評価する試みが続けられてきました。注意が必要な代表的な誤差には次のものがあります。

オーバーフロー/アンダーフロー(桁あふれ)
大き過ぎたり小さ過ぎたりする数値が、表現できる桁からはみ出ること
丸め誤差
無限小数などを有効桁内に収めるための、切り捨て/四捨五入/切り上げなどによって生じる誤差
打切り誤差
無限に演算する必要があるものを、有限回数で打ち切ることによって生じる誤差
桁落ち
絶対値が微小な差しかない数値間での加減算で、結果が有効数字の桁より小さいために、無意味な出力が得られること
情報落ち
絶対値の大きな数と小さな数の間の加減算で、有効数値の桁より小さい数値が計算結果に含まれないために、出力が無意味となること

java.mathパッケージの BigIntegerBigDecimal は、これらの問題を解決する一つの方法です。

BigInteger と BigDecimal は、String やラッパー・クラスと同様、不変オブジェクトです。BigInteger は任意精度の整数を表現することができます。BigDecimalは任意精度の符号付小数点数を取り扱え、丸め誤差を制御できます。

Javaのプリミティブ型では、浮動小数点数はIEEE 754規格の2進数で表現されます。これは要するに、小数を2のn乗の級数で表現する方法の一つであり、float型の32ビットや倍精度のdouble型64ビットを、何のために何ビット使うかを指定したものです(図1)。

IEEE 754規格
図1. IEEE 754規格倍精度浮動小数点数(数値は光速度[m/s])

BigDecimal を使えば、任意精度の小数を厳密に表現できます。指定した精度の桁数内で、厳密な結果を保証し、計算時に丸めモードを定数として指定することで、丸め誤差を制御できます(表2)。

表 2. BigDecimal クラスで丸めモードを指定する定数
定数説明
ROUND_CEILING正の無限大に近づくように丸めます
ROUND_DOWN0に近づけるように丸めます
ROUND_FLOOR負の無限大に近づくように丸めます
ROUND_HALF_DOWN「もっとも近い数字」 に丸めます。両隣の数字が等距離の場合は切り捨てます。
ROUND_HALF_EVEN「もっとも近い数字」 に丸めます。両隣の数字が等距離の場合は偶数側に丸めます。
ROUND_HALF_UP「もっとも近い数字」に丸めます。両隣の数字が等距離の場合は切り上げます。
ROUND_UNNECESSARY十分な桁数が用意できて、丸める必要がない場合に指定します。
ROUND_UP0から離れるように丸めます

BigDecimal では四則演算をメソッドで実行します。例えば、BigDecimal の値 a を同じく b で割るとき、丸めモードを ROUND_DOWN で実行する場合は次のようになります。

// a/bの実行
a.divide(b, BigDecimal.ROUND_DOWN);

有効数字の指定は簡単で、コンストラクタに与える引数の桁数で揃えられます。例えば、リスト1を見てみましょう。ここでは、rate が 0.250、掛け算対象の unit は 1 で初期化しています。一方が小点数以下 3 桁、一方が 0 桁なので、この場合の計算結果は小点数以下 3 桁で出力されます。この結果にもう一度 unit を掛けると、両方とも 3 桁になりますので、結果は 6 桁になります。以下、乗算の都度 3 桁ずつ増えていき、無限に桁が出力されることになります。

▼リスト1

// java.math.BigDecimal の import
import java.math.*;
class BigDecimalDemo {
    public static void main(String[] args) {
        // インスタンス化
        BigDecimal rate = new BigDecimal("0.250");
        BigDecimal unit = new BigDecimal("1");
    	
        for (int i = 0; i < 10; i++) {
            // BigInteger 型オブジェクトの乗算
            unit = unit.multiply(rate);
        	System.out.println(unit);
        }
    }
}

リスト1の実行結果は次のようになります。

0.250
0.062500
0.015625000
0.003906250000
0.000976562500000
0.000244140625000000
0.000061035156250000000
0.000015258789062500000000
0.000003814697265625000000000
0.000000953674316406250000000000

桁数を指定できることは便利なのですが、逆に言うと、この桁数の指定が足りないと、ROUND_HALF_DOWN などでばっさりと数字が落とされるので、無意味な結果が得られることもあります。そのため、設計の時点で、有効数字が何桁あれば良いのか、計算結果が何桁になるのかを正しく把握しておくことが重要です。

プリミティブ型ではサイズが小さい、精度が低い、丸め誤差が許容できないという場合には、BigInteger、BigDecimal の利用を検討します。但し、不変オブジェクトであることに起因する、オブジェクトの生成とコピーの繰り返しが発生します。そのため、サイズも計算速度もプリミティブ型よりも格段に悪化します。パフォーマンスと精度のトレードオフであることを理解した上で使ってください。

最小の値はいくつ?

最小の正の整数は、プリミティブ型の浮動小数点数では、MIN_VALUE で指定できます。

最小の表現なのでビット列だと "1" です。32 ビットの float 型だと 1.4E-45 で、64 ビットの double 型だと 4.9E-324 です。これより小さい値は 0 と区別できません。

次のコードは、この考え方で、float 型と double 型の最小の正の整数(0 と区別できる最小の正の整数)を計算するものです。確かにビット 1 に対して、求める値が計算できることが確認できます。

class FloatingEpsilon {
	public static void main(String[] args) {
		float fe = 1.0F;
		while (fe / 2.0F > 0) {
			fe = fe / 2.0F;
		}
		String intBits = Integer.toBinaryString(Float.floatToRawIntBits(fe));
		System.out.println(fe);
		System.out.println(intBits);

		double de = 1.0;
		while (de / 2.0 > 0) {
			de = de / 2.0;
		}
		String longBits = Long.toBinaryString(Double.doubleToRawLongBits(de));
		System.out.println(de);
		System.out.println(longBits);
	}
}
C:\java>javac FloatingEpsilon.java

C:\java>java FloatingEpsilon
1.4E-45
1
4.9E-324
1

x > y のとき、x - y >= + ε である最小の正の数 ε は仮数の基数と桁数に依存して決定し、 MIN_VALUE とは異なります。 1 + ε > 1 である最小の ε は次のようになります。64 ビット以上の浮動小数点数を計算可能なマシンでは、CPU によらず同じ値が得られます。MIN_VALUE よりもだいぶ大きいことに注意してください。

class MachineEpsilon {
	public static void main(String[] args) {
		float fe = 1.0F;
		float f = 1.0F + fe;
		while (f > 1.0F) {
			fe = fe / 2.0F;
			f = 1.0F + fe;
		}
		String intBits = Integer.toBinaryString(Float.floatToRawIntBits(fe));
		System.out.println(fe);
		System.out.println(intBits);

		double de = 1.0;
		double d = 1.0 + de;
		while (d > 1.0) {
			de = de / 2.0;
			d = 1.0 + de;
		}
		String longBits = Long.toBinaryString(Double.doubleToRawLongBits(de));
		System.out.println(de);
		System.out.println(longBits);
	}
}
C:\java>javac MachineEpsilon.java

C:\java>java MachineEpsilon
5.9604645E-8
110011100000000000000000000000
1.1102230246251565E-16
11110010100000000000000000000000000000000000000000000000000000

1.1102230246251565E-16 という数は、このページの最初の方で一回出てきました。0.1 を 10 回足した値と 1.0 との差異が同じ値です。つまり、この値以下の値は 1.0 との差として認識できないという閾値になっていることが分かります。

浮動小数点数の比較では、2つの数が完全に同じになることはまずありえないので、相対誤差がある値以下だったら等しいものと考えます。「ある値」の最小値が ε です。

絶対誤差 = | x - y |,

           | x - y |
相対誤差 = ---------
               y

例えば、x = 3.1415, y = 3.14 の場合、絶対誤差 = 0.0015, 相対誤差 = 4.7747891134808212637275187012574e-4 (約0.0048)です。

J2SE 5.0 以上では、BigDecimal.ulp() を用いて、最終桁単位 (unit in the last place) のサイズを得られます。その桁内の最小刻み幅(この値と、次に大きい絶対値および同じ桁数を持つ BigDecimal 値の間の正の距離)を返してくれます。その桁内では、その戻り値よりも小さな値 0.5ulp 以下は区別できません。1なら刻み幅 ulp は1であり、1.0なら刻み幅は0.1です。

import java.math.BigDecimal;

class MachineEpsilonUlpDemo {
	public static void main(String[] args) {
		BigDecimal unit = new BigDecimal(Double.MIN_VALUE);
		System.out.println(Double.MIN_VALUE);
		System.out.println(unit.ulp());
	}
}
C:\java>javac MachineEpsilonUlpDemo.java

C:\java>java MachineEpsilonUlpDemo
4.9E-324
1E-1074

数値計算における相対誤差、最終桁単位 (ulp)、マシン・イプシロン (machine epsilon) については、詳しくないのでこれ以上説明しません。他のページを参照してください。

数値のフォーマットは指定できる?

BigDecimalなどで結果が厳密なことは良いことなのですが、帳票印刷などで出力枠の桁数が決まっている場合には、そのままでは使えません。このようなときは、java.text.NumberFormat とそのサブクラスである java.text.DecimalFormat を使ってみましょう。

リスト2は年間の利子0.000300から月ごとの利子を計算し、12ヵ月分の残高を計算したものです。月利を算出するときに丸め誤差をROUND_HALF_DOWNで計算しています。何も指定しなければ、計算するごとに6桁ずつ増えていくはずですが、それでは非常に不恰好です。ここではフォーマットを指定して、利率はx.xxxx%、残高は¥x,xxxと出力されるようにしています。

残高では、getCurrencyInstance() メソッドによって、実行コンピュータのロケールから貨幣の出力フォーマットを取得しています。一方、% の出力では、DecimalFormat クラスを使って、自分でフォーマットを指定しています。

▼リスト2

// java.math.BigDecimal の import
import java.math.*;
// java.text.NumberFormatとjava.text.DecimalFormat の import
import java.text.*;
class BigDecimalDemo2 {
    public static void main(String[] args) {
    // インスタンス化
    BigDecimal rate = new BigDecimal("0.000300");
    BigDecimal MONTH = new BigDecimal("12");
        BigDecimal balance = new BigDecimal(100000000);
    // 丸めモード ROUND_HALF_DOWN を指定した除算
    BigDecimal monthlyRate = rate.divide(MONTH, BigDecimal.ROUND_HALF_DOWN);
        
        // 小数点以下4桁のフォーマットを指定
        NumberFormat nf = new DecimalFormat("0.0000%");
        
        // BigDecimal値をdoubleに変換し、フォーマットを適用
        System.out.println("年利: " + nf.format(rate.doubleValue()));
        System.out.println("月利: " + nf.format(monthlyRate.doubleValue()));
        // 当該コンピュータのロケールに応じた通貨単位のフォーマットを指定
        nf = NumberFormat.getCurrencyInstance();
        for (int i=0; i < 12; i++) {
            // 乗算
            BigDecimal interest = balance.multiply(monthlyRate);
            // 加算
            balance = balance.add(interest);
            // BigDecimalをdouble値に変換し、フォーマットを適用
            System.out.println("残高: " + nf.format(balance.doubleValue()));
        }
    }
}

リスト2の実行結果は次のようになります。

年利: 0.0300%
月利: 0.0025%
残高: ¥100,002,500
残高: ¥100,005,000
残高: ¥100,007,500
残高: ¥100,010,000
残高: ¥100,012,501
残高: ¥100,015,001
残高: ¥100,017,501
残高: ¥100,020,002
残高: ¥100,022,502
残高: ¥100,025,003
残高: ¥100,027,503
残高: ¥100,030,004

ロケールなどで既存のフォーマットが利用できるときはNumberFormat、単位などを独自にカスタマイズしたい場合は DecimalFormat を使いましょう。

数値判定の方法は?

入力値などが数値であるかどうかを判定したいことがあります。

「例外を例外的事象の処理以外では使わない」というのは例外処理の鉄則です。しかし、isDigit() を使うためには小数の処理などが面倒です。ちょっとしたテスト・コードのときなどには、次のようにして例外をキャッチする手法が良く使われます。

class ExceptionPrintDemo {
	public static Integer formatInt(String value) {
		if (value == null) {
			return null;
		}
		try {
			return new Integer(value);
		} catch(NumberFormatException e) {
			e.printStackTrace();
			return null;
		}
	}
	public static void main(String[] args) {
		if (formatInt(args[0]) != null) {
			System.out.println(args[0] + "は整数です。");
		} else {
			System.out.println(args[0] + "は整数ではありません。");
		}
	}
}
>javac ExceptionPrintDemo.java

>java ExceptionPrintDemo 1
1は整数です。

>java ExceptionPrintDemo 1.1
java.lang.NumberFormatException: For input string: "1.1"
        at java.lang.NumberFormatException.forInputString(Unknown Source)
        at java.lang.Integer.parseInt(Unknown Source)
        at java.lang.Integer.(Unknown Source)
        at ExceptionPrintDemo.formatInt(ExceptionPrintDemo.java:7)
        at ExceptionPrintDemo.main(ExceptionPrintDemo.java:14)
1.1は整数ではありません。


Copyright © 2003,2005 SUGAI, Manabu. All Rights Reserved.