走進 JDK 之 Float

秉心說發表於2019-03-09

文中相關原始碼:

Float.java

Float.c



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.5f0.25f 都可以準確的表示為二進位制小數,分別是 0.1b0.01b

說到這裡,其實我們還是不瞭解 float 在記憶體中到底是什麼樣的?int 型的 1, 記憶體中就是 00000000000000000000000000000001,那麼 0.75f 呢?關於浮點數,有一個廣泛使用的運算標準,叫做 IEEE 754-1985,全稱 IEEE 二進位制浮點數算數標準, 由 IEEE(電氣和電子工程師協會)指定,所有的計算機都支援 IEEE 浮點數標準。

本文後面都只針對 32 位單精度浮點數,對應 Java 中的 Float。先來看維基百科上的一張圖:

走進 JDK 之 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,等價於將小數點左移三位。

對於雙精度浮點數來說,exponent11 位,fraction52 位。

關於浮點數的詳細介紹可以閱讀 《深入理解計算機系統》 第二章第四節的相關內容。下面就進入 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);
}
複製程式碼

呼叫了 FloatDecimalparseFloat(String) 方法。這個方法原始碼相當的長,邏輯也比較複雜,我也只是大概看了一下流程。我就不貼原始碼了,捋一下大致流程:

  • 首先取出符號位,判斷正數還是負數
  • 判斷是否為 NaN
  • 判斷是否為 Infinity
  • 判斷是否是以 0x0X 開頭的十六進位制浮點數。若是,呼叫 parseHexString() 方法處理
  • 跳過開頭的無效的 0
  • 迴圈取出各位數字。注意若包含 e 或者 E,需要注意科學計數法的處理
  • 根據取得的字元陣列等資訊構建 ASCIIToBinaryBuffer 物件,呼叫其 floatValue() 方法,獲取最終結果

這塊原始碼看的一知半解,有功夫再慢慢跟進。Stringfloat 的方法除此之外,還有 valueOf() 方法。

valueOf(String)

    public static Float valueOf(String s) throws NumberFormatException {
        return new Float(parseFloat(s));
    }
複製程式碼

沒啥好說的,還是呼叫 parseFloat() 方法。

下面看一下 floatString 的相關方法。

toString()

public String toString() {
    return Float.toString(value);
}

public static String toString(float f) {
    return FloatingDecimal.toJavaFormatString(f);
}
複製程式碼

最終呼叫了 FloatDecimaltoJavaFormatString() 方法。這個方法也是原始碼相當長,邏輯很複雜。首先會通過 floatToRawIntBits() 方法轉換成其符合 IEEE 754 標準的二進位制形式對應的 int 值,再轉換為相應的十進位制字串。

最後看一下 Float 中提供的其他一些方法。

isNaN()

public static boolean isNaN(float v) {
    return (v != v);
}
複製程式碼

這個判斷很有意思,v != v。據此我們可以推斷出,對於任意不是 NaNv ,必定滿足 v == v。對於為 NaNv,必定滿足 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 值。因為 floatint 都是佔 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。通過 unionfloat 轉換為其二進位制對應的 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.NaNNaN 值了。

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 題解,歡迎關注!

走進 JDK 之 Float

相關文章