從JDK原始碼角度看Float

超人汪小建發表於2019-02-26

關於IEEE 754

在看Float前需要先了解IEEE 754標準,該標準定義了浮點數的格式還有一些特殊值,它規定了計算機中二進位制與十進位制浮點數轉換的格式及方法。規定了四種表示浮點數值的方法,單精確度(32位)、雙精確度(64位)、延伸單精確度(43位以上)與延伸雙精確度(79位以上)。多數程式語言支援單精確度和雙精確度,這裡討論的Float就是Java的單精確度的實現。

浮點數的表示

浮點數由三部分組成,如下圖,符號位s、指數e和尾數f。

這裡寫圖片描述
這裡寫圖片描述

對於求值我們是有一個公式對應的,根據該公式來看會更簡單點,某個浮點數的值為:

可以看到32位的最高位為符號識別符號,1表示負數,0表示正數。指數部分為8位,其實可以是0到255,但是為了可正可負,這裡需要減去127後才是真正的指數,而底數固定為2。剩下的23位表示尾數,但預設前面都會加上1.。所以通過上面就可以將一個浮點數表示出來了。

我們舉個例子來看,二進位制的“01000001001101100000000000000000”表示的浮點數是啥?

  1. 符號位為0,表示正數。
  2. 指數為“10000010”,減去127後為3。
  3. 尾數對應的值為“1.011011”。
  4. 於是最終得到浮點數為“1011.011”,轉成十進位制為“11.375”。

Float概況

Java的Float類主要的作用就是對基本型別float進行封裝,提供了一些處理float型別的方法,比如float到String型別的轉換方法或String型別到float型別的轉換方法,當然也包含與其他型別之間的轉換方法。

繼承結構

--java.lang.Object
  --java.lang.Number
    --java.lang.Float複製程式碼

主要屬性

public static final float POSITIVE_INFINITY = 1.0f / 0.0f;
public static final float NEGATIVE_INFINITY = -1.0f / 0.0f;
public static final float NaN = 0.0f / 0.0f;
public static final float MAX_VALUE = 0x1.fffffeP+127f;
public static final float MIN_NORMAL = 0x1.0p-126f;
public static final float MIN_VALUE = 0x0.000002P-126f;
public static final int MAX_EXPONENT = 127;
public static final int MIN_EXPONENT = -126;
public static final int SIZE = 32;
public static final int BYTES = SIZE / Byte.SIZE;
public static final Class<Float> TYPE = (Class<Float>) Class.getPrimitiveClass("float");複製程式碼
  • POSITIVE_INFINITY 用來表示正無窮大,按照IEEE 754浮點標準規定,任何有限正數除以0為正無窮大,正無窮的值為0x7f800000。
  • NEGATIVE_INFINITY 用來表示負無窮大,任何有限負數除以0為負無窮的,負無窮的值為0xff800000。
  • NaN 用來表示處理計算中出現的錯誤情況,比如0除以0或負數平方根。對於單精度浮點數,IEEE 標準規定 NaN 的指數域全為 1,且尾數域不等於零的浮點數。它並沒有要求具體的尾數域,所以 NaN 實際上不非是一個,而是一族。Java這裡定義的值為0x7fc00000。
  • MAX_VALUE 用來表示最大的浮點數值,它定義為0x1.fffffeP+127f,這裡0x表示十六進位制,1.fffffe表示十六進位制的小數,P表示2,+表示幾次方,這裡就是2的127次方,最後的f是轉成浮點型。所以最後最大值為3.4028235E38。
  • MIN_NORMAL 用來表示最小標準值,它定義為0x1.0p-126f,這裡其實就是2的-126次方的了,值為1.17549435E-38f。
  • MIN_VALUE 用來表示浮點數最小值,它定義為0x0.000002P-126f,最後的值為1.4e-45f。
  • MAX_EXPONENT 用來表示指數的最大值,這裡定為127,這個也是按照IEEE 754浮點標準的規定。
  • MIN_EXPONENT 用來表示指數的最小值,按照IEEE 754浮點標準的規定,它為-126。
  • SIZE 用來表示二進位制float值的位元數,值為32,靜態變數且不可變。
  • BYTES 用來表示二進位制float值的位元組數,值為SIZE除於Byte.SIZE,結果為4。
  • TYPE的toString的值是float
    Class的getPrimitiveClass是一個native方法,在Class.c中有個Java_java_lang_Class_getPrimitiveClass方法與之對應,所以JVM層面會通過JVM_FindPrimitiveClass函式根據”float”字串獲得jclass,最終到Java層則為Class<Float>
JNIEXPORT jclass JNICALL
Java_java_lang_Class_getPrimitiveClass(JNIEnv *env,
                                       jclass cls,
                                       jstring name)
{
    const char *utfName;
    jclass result;

    if (name == NULL) {
        JNU_ThrowNullPointerException(env, 0);
        return NULL;
    }

    utfName = (*env)->GetStringUTFChars(env, name, 0);
    if (utfName == 0)
        return NULL;

    result = JVM_FindPrimitiveClass(env, utfName);

    (*env)->ReleaseStringUTFChars(env, name, utfName);

    return result;
}複製程式碼

TYPE執行toString時,邏輯如下,則其實是getName函式決定其值,getName通過native方法getName0從JVM層獲取名稱,

public String toString() {
        return (isInterface() ? "interface " : (isPrimitive() ? "" : "class "))
            + getName();
    }複製程式碼

getName0根據一個陣列獲得對應的名稱,JVM根據Java層的Class可得到對應型別的陣列下標,比如這裡下標為6,則名稱為”float”。

const char* type2name_tab[T_CONFLICT+1] = {
  NULL, NULL, NULL, NULL,
  "boolean",
  "char",
  "float",
  "double",
  "byte",
  "short",
  "int",
  "long",
  "object",
  "array",
  "void",
  "*address*",
  "*narrowoop*",
  "*conflict*"
};複製程式碼

主要方法

parseFloat

public static float parseFloat(String s) throws NumberFormatException {
        return FloatingDecimal.parseFloat(s);
    }複製程式碼

通過呼叫FloatingDecimal的parseFloat方法來實現對字串的轉換,FloatingDecimal類主要提供了對 IEEE-754,該方法的實現程式碼實在是太長,這裡不再貼出了,說下它的處理思想及步驟。

  1. 判斷開頭是否為“-”或“+”符號,即正數或負數。
  2. 判斷是不是一個NaN,如果是則返回NaN。
  3. 判斷是不是一個Infinity,如果是則返回Infinity。
  4. 判斷是不是一個0x開頭的十六進位制的Java浮點數,如果是則將該十六進位制浮點數按照IEEE-754標準轉換成十進位制浮點數。比如字串為 0x12.3512p+11f ,則轉換後為37288.562。
  5. 判斷字串中是否有包含了E字元,即是否是科學計數法,如果有則需要處理。比如字串為 10001.222E+2 ,則轉換後為1000122.2。
  6. 還要處理浮點數精度問題,這個處理比較複雜,我們知道浮點數的精度是7位有效數字,這裡為什麼是7呢?還要回到IEEE-754標準上,32位二進位制轉換成浮點數是根據公式$(-1)^s*(1.f)*2^{(e-127)}$轉換的,可以看到它的精度由尾數來決定,尾數有23位,那麼$2^{23}=8388608$,該值介於$10^{6}$$10^{7}$,所以它能保證6位精確的數,但是7位就不一定了,這裡是相對小數點來說的,所以對應整個浮點型的精確值為有效位數就是7位,8位的不一定能準確表示。這裡對比幾個例子,字串30.200019轉換後為30.20002,一共七位有效位;字串30.200001轉換後為30.2,一共七位有效位,但後面都為0,所以省略;字串30000.2196501轉換後為30000.219,一共八位有效位,剛好能準確表示八位。

建構函式

public Float(String s) throws NumberFormatException {
        value = parseFloat(s);
    }
public Float(float value) {
        this.value = value;
    }
public Float(double value) {
        this.value = (float)value;
    }複製程式碼

提供三種建構函式,都比較簡單,可傳入String、float和double型別值,其中String型別會呼叫parseFloat方法進行轉換,double則直接轉成float型別。

toString

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

兩個toString方法,主要看第二個,通過FloatingDecimal類的toJavaFormatString方法轉成字串。這個轉換過程也是比較複雜,這裡不再貼程式碼,它處理的過程是先將浮點數轉成IEEE-754標準的二進位制形式,並且還要判斷是否是正負無窮大,是否是NaN。然後再按照IEEE-754標準從二進位制轉換成十進位制,此過程十分複雜,需要考慮的點相當多。最後生成浮點數對應的字串。

valueOf方法

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

有兩個valueOf方法,對於float型的直接new一個Float物件返回,而對於字串則先呼叫parseFloat方法轉成float後再new一個Float物件返回。

xxxValue方法

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;
    }複製程式碼

包括byteValue、shortValue、intValue、longValue、floatValue和doubleValue等方法,其實就是轉換成對應的型別。

floatToRawIntBits方法

public static native int floatToRawIntBits(float value);

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;
}複製程式碼

floatToRawIntBits是一個本地方法,該方法主要是將一個浮點數轉成IEEE 754標準的二進位制形式對應的整型數。對應的本地方法的處理邏輯簡單而且有效,就是通過一個union實現了int和float的轉換,最後再轉成java的整型jint。

floatToIntBits方法

public static native float intBitsToFloat(int bits);

JNIEXPORT jfloat JNICALL
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;
}複製程式碼

該方法與floatToRawIntBits方法對應,floatToIntBits同樣是一個本地方法,該方法主要是將一個IEEE 754標準的二進位制形式對應的整型數轉成一個浮點數。可以看到其本地實現也是通過union來實現的,完成int轉成float,最後再轉成java的浮點型jfloat。

floatToIntBits方法

public static int floatToIntBits(float value) {
        int result = floatToRawIntBits(value);
        if ( ((result & FloatConsts.EXP_BIT_MASK) ==
              FloatConsts.EXP_BIT_MASK) &&
             (result & FloatConsts.SIGNIF_BIT_MASK) != 0)
            result = 0x7fc00000;
        return result;
    }複製程式碼

該方法主要先通過呼叫floatToRawIntBits獲取到IEEE 754標準對應的整型數,然後再分別用FloatConsts.EXP_BIT_MASK和FloatConsts.SIGNIF_BIT_MASK兩個掩碼去判斷是否為NaN,0x7fc00000對應的即為NaN。

hashCode方法

public int hashCode() {
        return Float.hashCode(value);
    }
public static int hashCode(float value) {
        return floatToIntBits(value);
    }複製程式碼

主要看第二個hashCode方法即可,它是通過呼叫floatToIntBits來實現的,所以它返回的雜湊碼其實就是某個浮點數的IEEE 754標準對應的整型數。

isFinite 和 isInfinite

public static boolean isFinite(float f) {
        return Math.abs(f) <= FloatConsts.MAX_VALUE;
    }
public static boolean isInfinite(float v) {
        return (v == POSITIVE_INFINITY) || (v == NEGATIVE_INFINITY);
    }複製程式碼

這兩個方法分別用於判斷一個浮點數是否為有窮數或無窮數。邏輯很簡單,絕對值小於FloatConsts.MAX_VALUE的數則為有窮數,FloatConsts.MAX_VALUE的值為3.4028235e+38f,它其實與前面Float類中定義的MAX_VALUE相同。而是否為無窮數則通過POSITIVE_INFINITY和NEGATIVE_INFINITY進行判斷。

isNaN方法

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

用於判斷是個浮點數是否為NaN,該方法邏輯很簡單,直接(v != v),為啥能這樣做?因為規定一個NaN與任何值都不相等,包括它自己。所以這部分邏輯在JVM或本地中會做,於是可以直接通過比較來判斷。

max 和 min方法

public static float max(float a, float b) {
        return Math.max(a, b);
    }
public static float min(float a, float b) {
        return Math.min(a, b);
    }複製程式碼

用於獲取兩者較大或較小值,直接交由Math類完成。

compare方法

public static int compare(float f1, float f2) {
        if (f1 < f2)
            return -1;           
        if (f1 > f2)
            return 1;            

        int thisBits    = Float.floatToIntBits(f1);
        int anotherBits = Float.floatToIntBits(f2);

        return (thisBits == anotherBits ?  0 : 
                (thisBits < anotherBits ? -1 : 
                 1));                          
    }複製程式碼

f1小於f2則返回-1,反之則返回1。無法通過上述直接比較時則使用floatToIntBits方法分別將f1和f2轉成IEEE 754標準對應的整型數,然後再比較。相等則返回0,否則返回-1或1。

以下是廣告相關閱讀

========廣告時間========

鄙人的新書《Tomcat核心設計剖析》已經在京東銷售了,有需要的朋友可以到 item.jd.com/12185360.ht… 進行預定。感謝各位朋友。

為什麼寫《Tomcat核心設計剖析》

=========================

相關閱讀:

從JDK原始碼角度看Object
從JDK原始碼角度看Long
從JDK原始碼角度看Integer
volatile足以保證資料同步嗎
談談Java基礎資料型別
從JDK原始碼角度看併發鎖的優化
從JDK原始碼角度看執行緒的阻塞和喚醒
從JDK原始碼角度看併發競爭的超時
從JDK原始碼角度看java併發執行緒的中斷
從JDK原始碼角度看Java併發的公平性
從JDK原始碼角度看java併發的原子性如何保證
從JDK原始碼角度看Byte
從JDK原始碼角度看Boolean
從JDK原始碼角度看Short

有幫助,可打賞:

歡迎關注:

相關文章