'Java編程的邏輯:小數的二進制表示,小數計算為什麼會出錯?'

Java 電腦 編程語言 硬件 IT脫口秀 2019-08-06
"
"
Java編程的邏輯:小數的二進制表示,小數計算為什麼會出錯?

小數計算為什麼會出錯?

簡要答案

實際上,不是運算本身會出錯,而是計算機根本就不能精確的表示很多數,比如0.1這個數。

計算機是用一種二進制格式存儲小數的,這個二進制格式不能精確表示0.1,它只能表示一個非常接近0.1但又不等於0.1的一個數。

數字都不能精確表示,在不精確數字上的運算結果不精確也就不足為奇了。

0.1怎麼會不能精確表示呢?在十進制的世界裡是可以的,但在二進制的世界裡不行。在說二進制之前,我們先來看下熟悉的十進制。

實際上,十進制也只能表示那些可以表述為10的多少次方和的數,比如12.345,實際上表示的:110+21+30.1+40.01+5*0.001,與整數的表示類似,小數點後面的每個位置也都有一個位權,從左到右,依次為 0.1,0.01,0.001,...即10^(-1), 10^(-2), 10^(-3)。

很多數,十進制也是不能精確表示的,比如1/3, 保留三位小數的話,十進制表示是0.333,但無論後面保留多少位小數,都是不精確的,用0.333進行運算,比如乘以3,期望結果是1,但實際上卻是0.999。

二進制是類似的,但二進制只能表示哪些可以表述為2的多少次方和的數,來看下2的次方的一些例子:

"
Java編程的邏輯:小數的二進制表示,小數計算為什麼會出錯?

小數計算為什麼會出錯?

簡要答案

實際上,不是運算本身會出錯,而是計算機根本就不能精確的表示很多數,比如0.1這個數。

計算機是用一種二進制格式存儲小數的,這個二進制格式不能精確表示0.1,它只能表示一個非常接近0.1但又不等於0.1的一個數。

數字都不能精確表示,在不精確數字上的運算結果不精確也就不足為奇了。

0.1怎麼會不能精確表示呢?在十進制的世界裡是可以的,但在二進制的世界裡不行。在說二進制之前,我們先來看下熟悉的十進制。

實際上,十進制也只能表示那些可以表述為10的多少次方和的數,比如12.345,實際上表示的:110+21+30.1+40.01+5*0.001,與整數的表示類似,小數點後面的每個位置也都有一個位權,從左到右,依次為 0.1,0.01,0.001,...即10^(-1), 10^(-2), 10^(-3)。

很多數,十進制也是不能精確表示的,比如1/3, 保留三位小數的話,十進制表示是0.333,但無論後面保留多少位小數,都是不精確的,用0.333進行運算,比如乘以3,期望結果是1,但實際上卻是0.999。

二進制是類似的,但二進制只能表示哪些可以表述為2的多少次方和的數,來看下2的次方的一些例子:

Java編程的邏輯:小數的二進制表示,小數計算為什麼會出錯?

可以精確表示為2的某次方之和的數可以精確表示,其他數則不能精確表示。

"
Java編程的邏輯:小數的二進制表示,小數計算為什麼會出錯?

小數計算為什麼會出錯?

簡要答案

實際上,不是運算本身會出錯,而是計算機根本就不能精確的表示很多數,比如0.1這個數。

計算機是用一種二進制格式存儲小數的,這個二進制格式不能精確表示0.1,它只能表示一個非常接近0.1但又不等於0.1的一個數。

數字都不能精確表示,在不精確數字上的運算結果不精確也就不足為奇了。

0.1怎麼會不能精確表示呢?在十進制的世界裡是可以的,但在二進制的世界裡不行。在說二進制之前,我們先來看下熟悉的十進制。

實際上,十進制也只能表示那些可以表述為10的多少次方和的數,比如12.345,實際上表示的:110+21+30.1+40.01+5*0.001,與整數的表示類似,小數點後面的每個位置也都有一個位權,從左到右,依次為 0.1,0.01,0.001,...即10^(-1), 10^(-2), 10^(-3)。

很多數,十進制也是不能精確表示的,比如1/3, 保留三位小數的話,十進制表示是0.333,但無論後面保留多少位小數,都是不精確的,用0.333進行運算,比如乘以3,期望結果是1,但實際上卻是0.999。

二進制是類似的,但二進制只能表示哪些可以表述為2的多少次方和的數,來看下2的次方的一些例子:

Java編程的邏輯:小數的二進制表示,小數計算為什麼會出錯?

可以精確表示為2的某次方之和的數可以精確表示,其他數則不能精確表示。

Java編程的邏輯:小數的二進制表示,小數計算為什麼會出錯?

為什麼一定要用二進制呢?

為什麼就不能用我們熟悉的十進制呢?在最最底層,計算機使用的電子元器件只能表示兩個狀態,通常是低壓和高壓,對應0和1,使用二進制容易基於這些電子器件構建硬件設備和進行運算。如果非要使用十進制,則這些硬件就會複雜很多,並且效率低下。

為什麼有的小數計算是準確的

如果你編寫程序進行試驗,你會發現有的計算結果是準確的。比如,我用Java寫:

System.out.println(0.1f+0.1f); 
System.out.println(0.1f*0.1f);

第一行輸出0.2,第二行輸出0.010000001。按照上面的說法,第一行的結果應該也不對啊?

其實,這只是Java語言給我們造成的假象,計算結果其實也是不精確的,但是由於結果和0.2足夠接近,在輸出的時候,Java選擇了輸出0.2這個看上去非常精簡的數字,而不是一箇中間有很多0的小數。

在誤差足夠小的時候,結果看上去是精確的,但不精確其實才是常態。

怎麼處理計算不精確

計算不精確,怎麼辦呢?大部分情況下,我們不需要那麼高的精度,可以四捨五入,或者在輸出的時候只保留固定個數的小數位。

如果真的需要比較高的精度,一種方法是將小數轉化為整數進行運算,運算結束後再轉化為小數,另外的方法一般是使用十進制的數據類型,這個沒有統一的規範,在Java中是BigDecimal,運算更準確,但效率比較低,本節就不詳細說了。

"
Java編程的邏輯:小數的二進制表示,小數計算為什麼會出錯?

小數計算為什麼會出錯?

簡要答案

實際上,不是運算本身會出錯,而是計算機根本就不能精確的表示很多數,比如0.1這個數。

計算機是用一種二進制格式存儲小數的,這個二進制格式不能精確表示0.1,它只能表示一個非常接近0.1但又不等於0.1的一個數。

數字都不能精確表示,在不精確數字上的運算結果不精確也就不足為奇了。

0.1怎麼會不能精確表示呢?在十進制的世界裡是可以的,但在二進制的世界裡不行。在說二進制之前,我們先來看下熟悉的十進制。

實際上,十進制也只能表示那些可以表述為10的多少次方和的數,比如12.345,實際上表示的:110+21+30.1+40.01+5*0.001,與整數的表示類似,小數點後面的每個位置也都有一個位權,從左到右,依次為 0.1,0.01,0.001,...即10^(-1), 10^(-2), 10^(-3)。

很多數,十進制也是不能精確表示的,比如1/3, 保留三位小數的話,十進制表示是0.333,但無論後面保留多少位小數,都是不精確的,用0.333進行運算,比如乘以3,期望結果是1,但實際上卻是0.999。

二進制是類似的,但二進制只能表示哪些可以表述為2的多少次方和的數,來看下2的次方的一些例子:

Java編程的邏輯:小數的二進制表示,小數計算為什麼會出錯?

可以精確表示為2的某次方之和的數可以精確表示,其他數則不能精確表示。

Java編程的邏輯:小數的二進制表示,小數計算為什麼會出錯?

為什麼一定要用二進制呢?

為什麼就不能用我們熟悉的十進制呢?在最最底層,計算機使用的電子元器件只能表示兩個狀態,通常是低壓和高壓,對應0和1,使用二進制容易基於這些電子器件構建硬件設備和進行運算。如果非要使用十進制,則這些硬件就會複雜很多,並且效率低下。

為什麼有的小數計算是準確的

如果你編寫程序進行試驗,你會發現有的計算結果是準確的。比如,我用Java寫:

System.out.println(0.1f+0.1f); 
System.out.println(0.1f*0.1f);

第一行輸出0.2,第二行輸出0.010000001。按照上面的說法,第一行的結果應該也不對啊?

其實,這只是Java語言給我們造成的假象,計算結果其實也是不精確的,但是由於結果和0.2足夠接近,在輸出的時候,Java選擇了輸出0.2這個看上去非常精簡的數字,而不是一箇中間有很多0的小數。

在誤差足夠小的時候,結果看上去是精確的,但不精確其實才是常態。

怎麼處理計算不精確

計算不精確,怎麼辦呢?大部分情況下,我們不需要那麼高的精度,可以四捨五入,或者在輸出的時候只保留固定個數的小數位。

如果真的需要比較高的精度,一種方法是將小數轉化為整數進行運算,運算結束後再轉化為小數,另外的方法一般是使用十進制的數據類型,這個沒有統一的規範,在Java中是BigDecimal,運算更準確,但效率比較低,本節就不詳細說了。

Java編程的邏輯:小數的二進制表示,小數計算為什麼會出錯?

二進制表示

我們之前一直在用"小數"這個詞表示float和double類型,其實,這是不嚴謹的,"小數"是在數學中用的詞,在計算機中,我們一般說的是"浮點數"。float和double被稱為浮點數據類型,小數運算被稱為浮點運算。

為什麼要叫浮點數呢?這是由於小數的二進制表示中,表示那個小數點的時候,點不是固定的,而是浮動的。

我們還是用10進制類比,10進制有科學表示法,比如123.45這個數,直接這麼寫,就是固定表示法,如果用科學表示法,在小數點前只保留一位數字,可以寫為1.2345E2即1.2345*(10^2),即在科學表示法中,小數點向左浮動了兩位。

二進制中為表示小數,也採用類似的科學表示法,形如 m*(2^e)。m稱為尾數,e稱為指數。指數可以為真,也可以為負,負的指數表示哪些接近0的比較小的數。在二進制中,單獨表示尾數部分和指數部分,另外還有一個符號位表示正負。

幾乎所有的硬件和編程語言表示小數的二進制格式都是一樣的,這種格式是一個標準,叫做IEEE 754標準,它定義了兩種格式,一種是32位的,對應於Java的float,另一種是64位的,對應於Java的double。

"
Java編程的邏輯:小數的二進制表示,小數計算為什麼會出錯?

小數計算為什麼會出錯?

簡要答案

實際上,不是運算本身會出錯,而是計算機根本就不能精確的表示很多數,比如0.1這個數。

計算機是用一種二進制格式存儲小數的,這個二進制格式不能精確表示0.1,它只能表示一個非常接近0.1但又不等於0.1的一個數。

數字都不能精確表示,在不精確數字上的運算結果不精確也就不足為奇了。

0.1怎麼會不能精確表示呢?在十進制的世界裡是可以的,但在二進制的世界裡不行。在說二進制之前,我們先來看下熟悉的十進制。

實際上,十進制也只能表示那些可以表述為10的多少次方和的數,比如12.345,實際上表示的:110+21+30.1+40.01+5*0.001,與整數的表示類似,小數點後面的每個位置也都有一個位權,從左到右,依次為 0.1,0.01,0.001,...即10^(-1), 10^(-2), 10^(-3)。

很多數,十進制也是不能精確表示的,比如1/3, 保留三位小數的話,十進制表示是0.333,但無論後面保留多少位小數,都是不精確的,用0.333進行運算,比如乘以3,期望結果是1,但實際上卻是0.999。

二進制是類似的,但二進制只能表示哪些可以表述為2的多少次方和的數,來看下2的次方的一些例子:

Java編程的邏輯:小數的二進制表示,小數計算為什麼會出錯?

可以精確表示為2的某次方之和的數可以精確表示,其他數則不能精確表示。

Java編程的邏輯:小數的二進制表示,小數計算為什麼會出錯?

為什麼一定要用二進制呢?

為什麼就不能用我們熟悉的十進制呢?在最最底層,計算機使用的電子元器件只能表示兩個狀態,通常是低壓和高壓,對應0和1,使用二進制容易基於這些電子器件構建硬件設備和進行運算。如果非要使用十進制,則這些硬件就會複雜很多,並且效率低下。

為什麼有的小數計算是準確的

如果你編寫程序進行試驗,你會發現有的計算結果是準確的。比如,我用Java寫:

System.out.println(0.1f+0.1f); 
System.out.println(0.1f*0.1f);

第一行輸出0.2,第二行輸出0.010000001。按照上面的說法,第一行的結果應該也不對啊?

其實,這只是Java語言給我們造成的假象,計算結果其實也是不精確的,但是由於結果和0.2足夠接近,在輸出的時候,Java選擇了輸出0.2這個看上去非常精簡的數字,而不是一箇中間有很多0的小數。

在誤差足夠小的時候,結果看上去是精確的,但不精確其實才是常態。

怎麼處理計算不精確

計算不精確,怎麼辦呢?大部分情況下,我們不需要那麼高的精度,可以四捨五入,或者在輸出的時候只保留固定個數的小數位。

如果真的需要比較高的精度,一種方法是將小數轉化為整數進行運算,運算結束後再轉化為小數,另外的方法一般是使用十進制的數據類型,這個沒有統一的規範,在Java中是BigDecimal,運算更準確,但效率比較低,本節就不詳細說了。

Java編程的邏輯:小數的二進制表示,小數計算為什麼會出錯?

二進制表示

我們之前一直在用"小數"這個詞表示float和double類型,其實,這是不嚴謹的,"小數"是在數學中用的詞,在計算機中,我們一般說的是"浮點數"。float和double被稱為浮點數據類型,小數運算被稱為浮點運算。

為什麼要叫浮點數呢?這是由於小數的二進制表示中,表示那個小數點的時候,點不是固定的,而是浮動的。

我們還是用10進制類比,10進制有科學表示法,比如123.45這個數,直接這麼寫,就是固定表示法,如果用科學表示法,在小數點前只保留一位數字,可以寫為1.2345E2即1.2345*(10^2),即在科學表示法中,小數點向左浮動了兩位。

二進制中為表示小數,也採用類似的科學表示法,形如 m*(2^e)。m稱為尾數,e稱為指數。指數可以為真,也可以為負,負的指數表示哪些接近0的比較小的數。在二進制中,單獨表示尾數部分和指數部分,另外還有一個符號位表示正負。

幾乎所有的硬件和編程語言表示小數的二進制格式都是一樣的,這種格式是一個標準,叫做IEEE 754標準,它定義了兩種格式,一種是32位的,對應於Java的float,另一種是64位的,對應於Java的double。

Java編程的邏輯:小數的二進制表示,小數計算為什麼會出錯?

32位格式中,1位表示符號,23位表示尾數,8位表示指數。64位格式中,1位表示符號,52位表示尾數,11位表示指數。

在兩種格式中,除了表示正常的數,標準還規定了一些特殊的二進制形式表示一些特殊的值,比如負無窮,正無窮,0,NaN (非數值,比如0乘以無窮大)。

IEEE 754標準有一些複雜的細節,初次看上去難以理解,對於日常應用也不常用,本文就不介紹了。

如果你想查看浮點數的具體二進制形式,在Java中,可以使用如下代碼:

Integer.toBinaryString(Float.floatToIntBits(value))
Long.toBinaryString(Double.doubleToLongBits(value));
"

相關推薦

推薦中...