走進 JDK 之 Float

文中相關源碼:java

Float.javagit

Float.cgithub



0.3f - 0.2f = ?
複製代碼

相信不少人會不假思索的填上 0.1f,那麼,打開 IDEA,默默的執行一下:數組

0.10000001
複製代碼

若是你對這個答案抱有疑問,那麼在閱讀 Float 源碼以前,咱們先來看一下 Float 在內存中是如何表示的。bash

從熟悉的十進制浮點數提及,以 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.75ide

再看一個十進制小數 1.75 轉換爲二進制小數的例子:函數

1.75 = 1 + 3/4
     = 7/4
     = 7 * 2^-2
複製代碼

7 的二進制表示爲 111* 2^-2 表示將小數點左移兩位,獲得 1.11。因此,1.75 = 1.11bthis

下圖列舉一些常見小數的值:

二進制 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。先來看維基百科上的一張圖:

這張圖描述了單精度浮點數在內存中具體的二進制表示方法:

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

相關文章
相關標籤/搜索