文中相關原始碼:
0.3f - 0.2f = ?
複製程式碼
相信很多人會不假思索的填上 0.1f
,那麼,開啟 IDEA
,默默的執行一下:
0.10000001
複製程式碼
如果你對這個答案抱有疑問,那麼在閱讀 Float
原始碼之前,我們先來看一下 Float
在記憶體中是如何表示的。
從熟悉的十進位制浮點數說起,以 12.34
為例,顯然下面這個等式是成立的:
12.34 = 1 * 10^1 + 2 * 10^0 + 3 * 10^-1 + 4 * 10^-2
複製程式碼
同樣的,對於二進位制浮點數,也有如下等式,這裡以 10.11b
(程式碼塊裡面好像打不了下標,本文中以 b
結尾的均表示二進位制浮點數)為例:
10.11 b = 1 * 2^1 + 0 * 2^0 + 1 * 2^-1 + 1 * 2^-2
= 2 + 1/2 + 1/4
= 2.75
複製程式碼
這樣,二進位制浮點數 10.11b
就轉換成了十進位制浮點數 2.75
。
再看一個十進位制小數 1.75
轉換為二進位制小數的例子:
1.75 = 1 + 3/4
= 7/4
= 7 * 2^-2
複製程式碼
7
的二進位制表示為 111
,* 2^-2
表示將小數點左移兩位,得到 1.11
。所以,1.75 = 1.11b
。
下圖列舉一些常見小數的值:
二進位制 | x^y | 十進位制 |
---|---|---|
0.1 | 2 ^ -1 | 0.5 |
0.01 | 2 ^ -2 | 0.25 |
0.001 | 2 ^ -3 | 0.125 |
0.0001 | 2 ^ -4 | 0.0625 |
0.00001 | 2 ^ -5 | 0.03125 |
你發現問題的所在了嗎?我們再回到 0.3f - 0.2f
的問題上。不管是整數還是浮點數,最終在記憶體中都是以二進位制形式存在的,那麼 0.3f
如何以二進位制表示呢?顯而易見,沒有辦法以 x * 2^y
的形式來準確表示 0.3f
,也就是說,我們並不能將 0.3f
準確的表示為一個二進位制小數,只能近似的表示它,增加二進位制的長度可以提高精確度。同樣,對於 0.2f
,我們也沒法準確的表示為二進位制小數,所以最後的計算結果才不是 0.1f
。
最後再看一個減法,0.5f - 0.25f = ?
。答案是 0.25f
,我想這時候你應該不會再答錯了。因為 0.5f
和 0.25f
都可以準確的表示為二進位制小數,分別是 0.1b
和 0.01b
。
說到這裡,其實我們還是不瞭解 float
在記憶體中到底是什麼樣的?int
型的 1
, 記憶體中就是 00000000000000000000000000000001
,那麼 0.75f
呢?關於浮點數,有一個廣泛使用的運算標準,叫做 IEEE 754-1985,全稱 IEEE 二進位制浮點數算數標準, 由 IEEE(電氣和電子工程師協會)指定,所有的計算機都支援 IEEE 浮點數標準。
本文後面都只針對 32 位單精度浮點數,對應 Java 中的 Float
。先來看維基百科上的一張圖:
這張圖描述了單精度浮點數在記憶體中具體的二進位制表示方法:
- sign : 符號位,
1
位 。0
表示正數,1
表示負數。用s
表示 - exponent : 階碼域,
8
位。用E
表示,通常E = exponent - 127
,exponent
為無符號數 - fraction : 尾數域,
23
位。用M
表示,通常M = 1.fraction
通常情況下,一個浮點數可以表示如下:
V = (-1)^s * M * 2^E
複製程式碼
以上圖中的 0.15625f
為例。符號位為 0
,表示為正數。階碼域為 1111100
,等於十進位制 124
,則 階碼 E = 124 - 127 = -3
。尾數域為 01
,則 M = 1.01
。代入公式得:
V = (-1)^0 * 1.01 * 2^-3 = 0.00101 b = 0.15625
複製程式碼
注意,* 2^-3
,等價於將小數點左移三位。
對於雙精度浮點數來說,exponent
是 11
位,fraction
是 52
位。
關於浮點數的詳細介紹可以閱讀 《深入理解計算機系統》
第二章第四節的相關內容。下面就進入 Float
的原始碼部分。
類宣告
public final class Float extends Number implements Comparable<Float>{}
複製程式碼
不可變類,繼承了 Number
類,實現了 Comparable
介面。
欄位
private final float value;
private static final long serialVersionUID = -2671257302660747028L;
public static final Class<Float> TYPE = (Class<Float>) Class.getPrimitiveClass("float");
複製程式碼
final
修飾的 value
欄位保證其不可變性,value
也是 Float
類所包裝的浮點數。
// 0 11111111 00000000000000000000000
public static final float POSITIVE_INFINITY = 1.0f / 0.0f;
// 1 11111111 00000000000000000000000
public static final float NEGATIVE_INFINITY = -1.0f / 0.0f;
複製程式碼
正無窮和負無窮。階碼域都為 1
,尾數域都為 0
。
// 0 11111111 10000000000000000000000
public static final float NaN = 0.0f / 0.0f;
複製程式碼
Not a number
,非數字。階碼域都為 1
,尾數域不全為 0
。
// 0 11111110 11111111111111111111111
public static final float MAX_VALUE = 0x1.fffffeP+127f;
複製程式碼
最大值。階碼域為 11111110
,即 127
。按公式計算,V = 1.11...1 * 2^127
。
/*
* 0 00000001 00000000000000000000000
* 最小的規格化數(正數)
*/
public static final float MIN_NORMAL = 0x1.0p-126f; // 1.17549435E-38f
/*
* 0 00000000 00000000000000000000001
* 最小的非規格化數(正數)
*/
public static final float MIN_VALUE = 0x0.000002P-126f;
複製程式碼
這裡出現了兩個新名詞,規格化數
和 非規格化數
。上文中一直在說 通常情況下
,這個通常情況指的就是 規格化數
。那麼什麼是規格化數呢?階碼域 exponent != 0 && exponent != 255
,即階碼域即不全為 0
,也不全為 1
,這樣的浮點數就成為規格化數。對於規格化數,有如下規則:
E = exponent - 127
M = 1.fraction
V = (-1)^s * M * 2^E
複製程式碼
階碼域全為 0
的浮點數是 非規格化數
。對於非規格化數,對應規則也發生了改變:
E = 1 - 127 = -126
M = 0.fraction
V = (-1)^s * M * 2^E
複製程式碼
浮點數的計算方法並沒有發生改變,階碼 E
和尾數 M
的計算方法與規格化數不同了。非規格化數有兩個用途,第一,它可以表示 0
。由於規格化的尾數域 M = 1.fraction
,所以規格化數是沒法表示零值的。除了符號位外,其他域全為 0
,就表示 0.0f
。根據符號位的不同,還有 +0.0f
和 -0.0f
,它們被認為是不同的。第二,非規格數的存在使得浮點數可能表示的數值分佈更加均勻的接近於 0.0
,它可以表示那些非常接近於 0 的數。
public static final int MAX_EXPONENT = 127; // 指數域(階碼)最大值
public static final int MIN_EXPONENT = -126; // 指數域(階碼)最小值
public static final int SIZE = 32; // float 佔 32 bits
public static final int BYTES = SIZE / Byte.SIZE; // float 佔 4 bytes
複製程式碼
建構函式
public Float(float value) {
this.value = value;
}
public Float(double value) {
this.value = (float)value;
}
public Float(String s) throws NumberFormatException {
value = parseFloat(s);
}
複製程式碼
Float
有三個建構函式。第一個直接傳入 float
值。第二個傳入 double
值,再強轉 float
。第三個傳入 String
,呼叫 parseFloat()
函式轉換成 float
。下面就來看看這個 parseFloat
函式。
方法
parseFloat(String)
public static float parseFloat(String s) throws NumberFormatException {
return FloatingDecimal.parseFloat(s);
}
複製程式碼
呼叫了 FloatDecimal
的 parseFloat(String)
方法。這個方法原始碼相當的長,邏輯也比較複雜,我也只是大概看了一下流程。我就不貼原始碼了,捋一下大致流程:
- 首先取出符號位,判斷正數還是負數
- 判斷是否為
NaN
- 判斷是否為
Infinity
- 判斷是否是以
0x
或0X
開頭的十六進位制浮點數。若是,呼叫parseHexString()
方法處理 - 跳過開頭的無效的
0
- 迴圈取出各位數字。注意若包含
e
或者E
,需要注意科學計數法的處理 - 根據取得的字元陣列等資訊構建
ASCIIToBinaryBuffer
物件,呼叫其floatValue()
方法,獲取最終結果
這塊原始碼看的一知半解,有功夫再慢慢跟進。String
轉 float
的方法除此之外,還有 valueOf()
方法。
valueOf(String)
public static Float valueOf(String s) throws NumberFormatException {
return new Float(parseFloat(s));
}
複製程式碼
沒啥好說的,還是呼叫 parseFloat()
方法。
下面看一下 float
轉 String
的相關方法。
toString()
public String toString() {
return Float.toString(value);
}
public static String toString(float f) {
return FloatingDecimal.toJavaFormatString(f);
}
複製程式碼
最終呼叫了 FloatDecimal
的 toJavaFormatString()
方法。這個方法也是原始碼相當長,邏輯很複雜。首先會通過 floatToRawIntBits()
方法轉換成其符合 IEEE 754
標準的二進位制形式對應的 int
值,再轉換為相應的十進位制字串。
最後看一下 Float
中提供的其他一些方法。
isNaN()
public static boolean isNaN(float v) {
return (v != v);
}
複製程式碼
這個判斷很有意思,v != v
。據此我們可以推斷出,對於任意不是 NaN
的 v
,必定滿足 v == v
。對於為 NaN
的 v
,必定滿足 v != v
。
isInfinite()
public boolean isInfinite() {
return isInfinite(value);
}
public static boolean isInfinite(float v) {
return (v == POSITIVE_INFINITY) || (v == NEGATIVE_INFINITY);
}
複製程式碼
判斷是否為正無窮或負無窮。
isFinite()
public static boolean isFinite(float f) {
return Math.abs(f) <= FloatConsts.MAX_VALUE;
}
複製程式碼
判斷浮點數是否是一個有限值。
Number 介面方法
public byte byteValue() { return (byte)value; }
public short shortValue() { return (short)value; }
public int intValue() { return (int)value; }
public long longValue() { return (long)value; }
public float floatValue() { return value; }
public double doubleValue() { return (double)value; }
複製程式碼
floatToRawIntBits(float)
public static native int floatToRawIntBits(float value);
複製程式碼
這是一個 native
方法,將 float
浮點數轉換為其 IEEE 754
標準二進位制形式對應的 int
值。因為 float
和 int
都是佔 32 位,所以每一個 float 總有對應的 int 值。具體實現在 native/java/lang/Float.c
中:
JNIEXPORT jint JNICALL
Java_java_lang_Float_floatToRawIntBits(JNIEnv *env, jclass unused, jfloat v)
{
union {
int i;
float f;
} u;
u.f = (float)v;
return (jint)u.i;
}
複製程式碼
union
是一種資料結構,它能在同一個記憶體空間中儲存不同的資料型別,也就是說同一塊記憶體,它可以表示 float
, 也可以表示 int
。通過 union
將 float
轉換為其二進位制對應的 int
值。
floatToIntBits(float)
public static int floatToIntBits(float value) {
int result = floatToRawIntBits(value);
// Check for NaN based on values of bit fields, maximum
// exponent and nonzero significand.
if ( ((result & FloatConsts.EXP_BIT_MASK) ==
FloatConsts.EXP_BIT_MASK) &&
(result & FloatConsts.SIGNIF_BIT_MASK) != 0)
result = 0x7fc00000;
return result;
}
複製程式碼
基本等同於 floatToRawIntBits()
方法,區別在於這裡對於 NaN
作了檢測,如果結果為 NaN
, 直接返回 0x7fc00000
,也就是 Java 中的 Float.NaN
。乍看一下,這不是在多此一舉嗎?如果是 NaN
就直接返回 NaN
。還記得前面對 NaN
的說明嗎,階碼域都為 1
,尾數域不全為 0
,所以 IEEE 754
中的 NaN
並不是一個固定的值,而是一個值域,但是在 Java 中將 Float.NaN
定義為了 0x7fc00000
,相應二進位制為 0 11111111 10000000000000000000000
。所以方法引數中的 NaN
值並不一定就是 0x7fc00000
。從檢測 NaN
的條件中也可以看出一二:
((result & FloatConsts.EXP_BIT_MASK) == FloatConsts.EXP_BIT_MASK) &&
(result & FloatConsts.SIGNIF_BIT_MASK) != 0
複製程式碼
前半段是檢測階碼域的。FloatConsts.EXP_BIT_MASK
值為 0x7F800000
, 二進位制為 0 11111111 00000000000000000000000
,若滿足 (result & FloatConsts.EXP_BIT_MASK) == FloatConsts.EXP_BIT_MASK
,則 result
階碼域必定全為 1
。
後半段是檢測尾數域的。FloatConsts.SIGNIF_BIT_MASK
值為 0x007FFFFF
,二進位制為 0 00000000 11111111111111111111111
,若要滿足 (result & FloatConsts.SIGNIF_BIT_MASK) != 0
, 則 result
尾數域不全為 0
即可。
根據這裡兩個檢測條件也可以知道這裡的 NaN
並不是一個固定的值。但是 Float.NaN
又是一個固定的值,那麼如何獲取其他不同的 NaN
呢?答案就是 intBitsToFloat(int)
方法。
intBitsToFloat(int)
public static native float intBitsToFloat(int bits);
Java_java_lang_Float_intBitsToFloat(JNIEnv *env, jclass unused, jint v)
{
union {
int i;
float f;
} u;
u.i = (long)v;
return (jfloat)u.f;
}
複製程式碼
native
方法,也是通過聯合體 union
來實現的。只要引數中的 int
值滿足 IEEE 754
對於 NaN
的標準,就可以產生值不為 Float.NaN
的 NaN
值了。
hashCode()
@Override
public int hashCode() {
return Float.hashCode(value);
}
public static int hashCode(float value) {
return floatToIntBits(value);
}
複製程式碼
hashCode()
函式直接呼叫 floatToIntBits()
方法,返回其二進位制對應的 int
值。
equals()
public boolean equals(Object obj) {
return (obj instanceof Float)
&& (floatToIntBits(((Float)obj).value) == floatToIntBits(value));
}
複製程式碼
equals
的條件是其 IEEE 754
標準的二進位制形式相等。
總結
Float
就說到這裡了。這篇原始碼解釋不是很多,主要說明了 Float
在記憶體中的二進位制形式,也就是 IEEE 754
標準。瞭解了 IEEE 754
,對於浮點數也就瞭然於心了。最後再推薦一下 《深入理解計算機系統》 中 2.4
節關於浮點數的介紹。
文章同步更新於微信公眾號:
秉心說
, 專注 Java 、 Android 原創知識分享,LeetCode 題解,歡迎關注!