小数点を含む数値
- 解説
- 1. 小数とは
- 2. 誤差と精度
- 3. 浮動小数点数型:double型
- 4. 浮動小数点数型の問題点
- 5. BigDecimalを使おう
- 6. float型
- 7. このページで学習した型
解説
1. 小数とは
小数とは小数点を含む数値のことです。身近な小数には、円周率「3.14」や税込計算をする際の「1.08」などがあります。
割り算の結果では、小数点以下に同じ数字が無限に続く無限小数や、同じパターンが無限に続く循環小数という割り切れない小数が発生することがあります。その場合、どこかの桁で切り捨て/切り上げ/四捨五入を行う必要があり、これを丸めると言います。
この際、丸める桁までの桁数が多ければ多いほど本当の結果に近づくことになります。では、どのくらいの桁数を確保すればよいのかを考えてみましょう。
2. 誤差と精度
例えば長さ20cmのケーキを3等分する場合を考えてみましょう。
計算上は「20cm ÷ 3 = 6.66666…cm」ですが、友だちと食べるだけなので、小数点のすぐ右の位(小数第一位)で四捨五入をして整数に丸め、両端から「7cm」ずつ切ることにました。
ここで、本来の値との差分である「7cm - 6.66666…cm = 0.33333…cm」を誤差と呼びます。また、「四捨五入した桁までの桁数」のことを精度と言います。
最初に行った計算の精度が低かったために大きな誤差を生み、それが積み上がった結果、真ん中のケーキだけ6cmとなってしまいました!とはいっても、このケースでは切った人が我慢すれば良い程度のことであり、許容できる誤差であったと言えます。
お店で出すケーキならば、もっと高い精度で計算する必要があります。小数第二位で四捨五入を行うと、誤差は「6.7cm - 6.66666…cm = 0.03333…cm」となり、10分の1に抑えることができました。この誤差であれば、お店もお客さんも満足できそうです。
しかし、この精度で200cmのケーキを30個に切り分ける場合には、6.7cmで29個切った後、結局最後の1個は5.7cmとなってしまいます。
精密機器で誤差「6.6667cm - 6.66666…cm = 0.00003…cm」という高い精度で切り分けることも可能ですが、今度はそのコスト(金額や切り分けるスピード)を許容できません。
このように、許容できる誤差=必要な精度は目的によって異なり、かつコスト(データ量と計算速度)とのバランスも考慮する必要があるのです。
小数と、その計算において発生し得る誤差と、それをコントロールする精度について少し理解を深めたところで、Javaにおける小数の管理について学習しましょう。
3. 浮動小数点数型:double型
変数に「小数」を代入して使う場合には、変数を「double」型として宣言します。
値は小数点にピリオド(.)を使用し、クォーテーションなどでは括りません。
static void practiceDouble() {
double doubleValue = 3.14;
Logger.out(doubleValue);
}
「double」の語源は「Double precision floating point number.(倍精度浮動小数点数)」であり、ダブル型・浮動小数点数型・倍精度浮動小数点数型と呼びます。
浮動小数点数とは、その名の通り小数点の位置を浮動させてデータを管理するということを示しています。その仕組みは難解ですが、整数型と比べながら理解を深めてみましょう。
整数型は「0か1のビットの並び」である2進数でデータを管理していましたが、浮動小数点数型も同様に2進数で管理します。これがコンピューターにとって一番小さなデータ量で高速に扱えるからです。
10進数では数字を0~9までの10個使えるため、9の次で繰り上がりとなり、「1の位」「10の位」「100の位」と10倍毎に桁が増える・・・ということは、言うまでもありませんね。
2進数では数字を0と1の2個しか使えないため、1の次にもう繰り上がりとなり、「1の位」「2の位」「4の位」「8の位」と2倍毎に桁が増えるのでした。わからなければ、前のページに戻ってみましょう!
小数の場合も同じで、10進数で10分の1毎に小数点以下の桁が増えるように、2進数も2分の1毎に小数点以下の桁が増えます。
2進数 | 10進数 |
---|---|
1 | 1 |
0.1(1の1/2) | 0.5(1の1/2) |
0.01(0.1の1/2) | 0.25(0.5の1/2) |
0.001(0.01の1/2) | 0.125(0.25の1/2) |
「10進数の小数を表現できないから2進数に変換」したのにまた小数・・・と混乱しますが、一旦このルールで「10進数の11.625」を変換してみましょう。
このようにしてしまえば、小数点の位置さえどうにかすれば、整数と同じようにビットのみで表せそうです。小数点の位置は一番左の「1」の右になるように移動させます。「1011.101」であれば「1.011101」のように左へ3回移動(+3)します。10進数の0.625は「0.101」となりますので、その場合には「1.01」のように右へ1回移動(-1)します。
その時に移動した回数もまた整数でありビット変換できますので、これで、すべてビットのみで表せるようになりました。小数点の位置を浮動させてデータを管理するという意味がなんとなく分かったでしょうか。
下図が実際のdoubleのデータレイアウトです。64ビットの中を符号部/指数部/仮数部に分け、
変換と小数点の移動が済んだ2進数部分を仮数部で、小数点の移動回数を指数部で管理することで、ビットのみでの管理を実現しているのです。
4. 浮動小数点数型の問題点
問題点は2つあります。ただしこれは、コンピューターにとっては正しい動きなので、使う側から見て直感的でない、という問題点です。
- 代入時の変換だけで誤差が発生する場合がある
- 精度が一定でない
代入時の変換だけで誤差が発生する場合がある
簡単な例で確認しましょう。
static void practiceDouble() {
double doubleValue1 = 0.3;
double doubleValue2 = 0.2;
Logger.out(doubleValue1);
Logger.out(doubleValue2);
Logger.out(doubleValue1 + doubleValue2);
Logger.out(doubleValue1 - doubleValue2);
}
[2020/12/31 12:34:56.789] 0.3 // 間違いなく0.3(に見える)
[2020/12/31 12:34:56.789] 0.2 // 間違いなく0.2(に見える)
[2020/12/31 12:34:56.789] 0.5 // 予測通りの加算結果(に見える)
[2020/12/31 12:34:56.789] 0.09999999999999998 // 減算で突然誤差が表面化
「10進数の0.5」の2進数変換結果は「0.1」であり問題ありませんが、「10進数の0.3」の場合は「0.01001100110011001…」、「10進数の0.2」も「0.00110011001100110…」となり、結局割り切れない循環小数になってしまうのです。
この無限に続く小数を仮数部に入るだけ入れて、入らずに切り捨てられた分が誤差となります。そしてこの誤差は非常に小さいため、10進数に復元する際に切り捨てられて代入時と同じように見えたり、計算後に突然誤差が見えるようになったりします。
無限小数や循環小数だけでなく、小数点以下がとても長いケースも含めると、かなりの確率となります。また、この例では減算時に誤差が表面化しましたが、0.2と0.1でやってみると、加算やその他の計算でも同様であることが確認できます。
精度が一定でない
「10進数の0.1」の2進数変換結果は「0.00011001100110011…」となります。double型のルールに従うと、小数点を右に4回移動して先頭の1を含めないため、仮数部には6桁目から52桁分を格納することとなり、その結果小数第56位までの精度となります。
一方「10進数の8.1」の2進数変換結果は「1000.00011001100110011…」となります。同様にdouble型のルールに従うと、小数点を左に3回移動して先頭の1を含めないため、仮数部には2桁目から52桁分格納することとなり、その結果小数第49位までの精度となります。
私たちは小数第1位までの精度で0.1と8.1を代入したつもりでも、実際には精度は同じではなく、誤差の差は2の7乗=128倍にもなってしまったのです。
浮動小数点数型は、データ用に確保された領域内で可能な限り、小数点以下の情報を正確に残そうとしてくれます。この仕組みによって、扱う値によって精度が変わってしまうことがあるのです。この特性を正しく理解してコントロールできれば、高度な計算を高速に行うといった用途に適しているのですが、通常の用途では思いがけない計算結果となってしまうなどのミスにつながります。
5. BigDecimalを使おう
例えばゲームを作っていて「ジャンプしているときの0.1秒ごとの高さ」のような計算をする場合には、精度よりもリアルタイムに計算できることを優先し、doubleを使用しましょう。
しかしそれ以外の場合は、基本的にはBigDecimalというクラスを使用して計算するようにしましょう。まだクラスやメソッドの使い方を詳しく学習していませんが、使い方とポイントだけ確認しておきましょう。
※クイックフィックスから「java.math.BigDecimal」「java.math.RoundingMode」をインポートしましょう!
static void practiceBigDecimal() {
// (1)doubleの問題回避確認
BigDecimal bigDec1 = new BigDecimal("0.3");
BigDecimal bigDec2 = new BigDecimal("0.2");
Logger.out(bigDec1.add(bigDec2));
Logger.out(bigDec1.subtract(bigDec2));
// (2)掛け算の結果の丸め処理
BigDecimal bigDec3 = new BigDecimal("198");
BigDecimal bigDec4 = new BigDecimal("1.08");
Logger.out(bigDec3.multiply(bigDec4));
Logger.out(bigDec3.multiply(bigDec4)
.setScale(0, RoundingMode.FLOOR));
Logger.out(bigDec3.multiply(bigDec4)
.setScale(0, RoundingMode.HALF_UP));
// (3)割り算の結果の丸め処理
BigDecimal bigDec5 = new BigDecimal("20");
BigDecimal bigDec6 = new BigDecimal("3");
Logger.out(bigDec5.divide(bigDec6, 4, RoundingMode.HALF_UP));
Logger.out(bigDec5.divide(bigDec6, 3, RoundingMode.HALF_UP));
Logger.out(bigDec5.divide(bigDec6, 2, RoundingMode.HALF_UP));
Logger.out(bigDec5.divide(bigDec6, 1, RoundingMode.HALF_UP));
Logger.out(bigDec5.divide(bigDec6, 0, RoundingMode.HALF_UP));
}
[2020/12/31 12:34:56.789] 0.5 // doubleと異なり誤差無し
[2020/12/31 12:34:56.789] 0.1 // doubleと異なり誤差無し
[2020/12/31 12:34:56.789] 213.84 // 198円の税込価格計算結果
[2020/12/31 12:34:56.789] 213 // 小数点以下切り捨て
[2020/12/31 12:34:56.789] 214 // 小数第1位で四捨五入
[2020/12/31 12:34:56.789] 6.6667 // 20cm のケーキ3等分 精度高
[2020/12/31 12:34:56.789] 6.667 // 20cm のケーキ3等分 ↓
[2020/12/31 12:34:56.789] 6.67 // 20cm のケーキ3等分 ↓
[2020/12/31 12:34:56.789] 6.7 // 20cm のケーキ3等分 ↓
[2020/12/31 12:34:56.789] 7 // 20cm のケーキ3等分 精度低
「new BigDecimal()」の括弧の中には、文字列で小数を指定することが一番のポイントです。ここでダブルクォーテーションを忘れてしまうと、処理の途中がdouble型になってしまうため、この型を使っても誤差が出てしまいます。
丸め処理の指定方法が、割り算とそれ以外で異なります。割り算の場合には「divide」メソッドに丸める桁位置と、その方法を指定します。それ以外の場合には、「setScale」メソッドで同じように指定します。切り捨てるときは「RoundingMode.FLOOR」を、四捨五入は「RoundingMode.HALF_UP」を指定します。
BigDecimalを使ったからと言って、無限小数や循環小数、小数点以下の桁数が多い小数が発生しないわけではありません。しかし、誤差が発生する場合にはエラー終了して使う側に処理方法を決めさせる仕組みとなっており、どの桁でどのように丸めるかを完全に使う側が制御できるようにして、計算結果を保証しているのです。
6. float型
もうひとつ、浮動小数点数型として「float」型があります。語源は「Single precision floating point number.(単精度浮動小数点数)」の中の「floating=変動する・浮動的な」であり、フロート型・単精度浮動小数点数型と呼びます。
floatへの代入の際は必ず値の末尾に「f」もしくは「F」を付与します。名前からするとfloatの方が優先されそうですが、末尾に何もつけない小数はdoubleとして判定されます。
static void practiceFloat() {
float floatValue = 3.14f;
Logger.out(floatValue);
}
doubleにも「d」もしくは「D」を付与できますが、これはdoubleであることを明示したい場合以外は省略して構いません。
double型は64ビットで、float型は32ビットです。floatに比べて精度が倍だからdoubleです。このことからも、浮動小数点数型を使用する場合にはdoubleを優先して検討しましょう。
また、doubleと仕組みは同じですが、仮数部の桁数が異なる=精度が異なりますので、見た目上同じ値を代入してもその時点で異なる値となりますので、混在させないようにしましょう。
7. このページで学習した型
型 | 読み方 呼び方 | 分類 | 用途 | データ 範囲 |
---|---|---|---|---|
float | フロート型・ 単精度 浮動小数点数型 | 基本型 | 32bitで 足りる 小数計算 | ±1.40239846×10の-45乗 ~ ±3.40282347×10の38乗 |
double | ダブル型・ 倍精度 浮動小数点数型 | 基本型 | 性能を 求める 小数計算 | ±4.94065645841246544×10の-324乗 ~ ±1.79769313486231570×10の308乗 |
問題
確認問題
確認問題1
この中で小数とその計算に関する説明として正しいものはどれか?