走進 JDK 之 Long

秉心說發表於2019-03-04

文中相關原始碼:

Long.java

上一篇文章 走進 JDK 之 Integer 解析了 Integer.java,而 Long.javaInteger.java 的原始碼結構幾乎是一模一樣的,所以這篇文章會寫的比較簡略,沒有細讀過 Integer.java 原始碼的可以先看一下我的上一篇文章。這裡就簡單介紹一下 Long 以及原始碼中和 Integer 的細微區別。

類宣告

public final class Long extends Number implements Comparable<Long>{}
複製程式碼

Integer 一樣,不可變類,繼承了 Number 類,實現了 Comparable 介面。

欄位

// long 最小值,-2^63
public static final long MIN_VALUE = 0x8000000000000000L;
// long 最大值,-2^63-1
public static final long MAX_VALUE = 0x7fffffffffffffffL;
public static final Class<Long> TYPE = (Class<Long>) Class.getPrimitiveClass("long");
private final long value;
// 以二進位制補碼形式表示 long 值所需的位元數
public static final int SIZE = 64;
// 以二進位制補碼形式表示 long 值所需的位元組數,恆為 8
public static final int BYTES = SIZE / Byte.SIZE;
private static final long serialVersionUID = 4290774380558885855L;
複製程式碼

建構函式

public Long(long value) {
    this.value = value;
}

public Long(String s) throws NumberFormatException {
    this.value = parseLong(s, 10);
}
複製程式碼

建構函式和 Integer 一樣也是兩個。第一個引數為 long 值。第二個引數為 String 字串,通過 parseLong() 方法轉換為 long 值。

方法

Long.java 中的方法實現幾乎和 Integer.java 一致,這裡不再一一分析原始碼了,僅列舉一下出現的方法,也算對上一篇文章的總結。

String 轉 long

public static int parseLong(String s)
public static long parseLong(String s, int radix)
public static long parseUnsignedLong(String s)
public static long parseUnsignedLong(String s, int radix)
public static Long decode(String nm)
public static Long valueOf(String s)
public static Long valueOf(String s, int radix)
public static Long getLong(String nm)
public static Long getLong(String nm, long val)
public static Long getLong(String nm, Long val)
複製程式碼

如果你還記得 IntegerCache 的話,沒錯,同樣也有一個 LongCache :

private static class LongCache {
    private LongCache(){}

    static final Long cache[] = new Long[-(-128) + 127 + 1];

    static {
        for(int i = 0; i < cache.length; i++)
            cache[i] = new Long(i - 128);
    }
}
複製程式碼

IntegerCache 一樣,它也是快取了 -128127

int 轉 String

public static String toString(long i)
public static String toString(long i, int radix)
public static String toUnsignedString(long i)
public static String toUnsignedString(long i, int radix)
複製程式碼

toString()

首先看一下 toString(long i) 方法:

public static String toString(long i) {
    if (i == Long.MIN_VALUE)
        return "-9223372036854775808";
    int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
    char[] buf = new char[size];
    getChars(i, size, buf);
    return new String(buf, true);
}
複製程式碼

整體套路和 Integer 是一樣的,但在 stringSize()getChars() 的具體實現上有一些細微的不同。

static int stringSize(long x) {
    long p = 10;
    for (int i=1; i<19; i++) {
        if (x < p)
            return i;
        p = 10*p;
    }
    return 19;
}
複製程式碼

Long.MAX_VALUE 是 19 位數字,所以最多不會迴圈超過 19 次。原理和 IntegerstringSize() 方法是一樣的,小於 10 就是 1 位數,小於 100 就是 2 位數..... 。還記得 Integer 中是怎麼實現的嗎?

final static int [] sizeTable = { 9, 99, 999, 9999, 99999, 999999, 9999999,
                                  99999999, 999999999, Integer.MAX_VALUE };

// Requires positive x
static int stringSize(int x) {
    for (int i=0; ; i++)
        if (x <= sizeTable[i])
            return i+1;
}
複製程式碼

使用了一個 10 個元素的陣列。如果 Long 也使用同樣的方法,那麼就需要一個 20 個元素的陣列。JDK 開發者可能是站在考慮記憶體使用的角度,並沒有使用這種方法。

除了 stringSize() 外,getChars() 的實現也略有不同。Long 中的 getChars() 如下所示:

static void getChars(long i, int index, char[] buf) {
    long q;
    int r;
    int charPos = index;
    char sign = 0;

    if (i < 0) {
        sign = '-';
        i = -i;
    }

    // Get 2 digits/iteration using longs until quotient fits into an int
    while (i > Integer.MAX_VALUE) {
        q = i / 100;
        // really: r = i - (q * 100);
        r = (int)(i - ((q << 6) + (q << 5) + (q << 2)));
        i = q;
        buf[--charPos] = Integer.DigitOnes[r];
        buf[--charPos] = Integer.DigitTens[r];
    }

    // Get 2 digits/iteration using ints
    int q2;
    int i2 = (int)i;
    while (i2 >= 65536) {
        q2 = i2 / 100;
        // really: r = i2 - (q * 100);
        r = i2 - ((q2 << 6) + (q2 << 5) + (q2 << 2));
        i2 = q2;
        buf[--charPos] = Integer.DigitOnes[r];
        buf[--charPos] = Integer.DigitTens[r];
    }

    // Fall thru to fast mode for smaller numbers
    // assert(i2 <= 65536, i2);
    for (;;) {
        q2 = (i2 * 52429) >>> (16+3);
        r = i2 - ((q2 << 3) + (q2 << 1));  // r = i2-(q2*10) ...
        buf[--charPos] = Integer.digits[r];
        i2 = q2;
        if (i2 == 0) break;
    }
    if (sign != 0) {
        buf[--charPos] = sign;
    }
}
複製程式碼

這裡分隔了三段迴圈體,分別以 Integer.MAX_VALUE65536 為分界線,而 Integer 僅以 65536 為界分了兩段迴圈體。我們先不看這多出來的一段,來看一下 Long 裡面第二段迴圈體的實現:

// Get 2 digits/iteration using ints
int q2;
int i2 = (int)i;
while (i2 >= 65536) {
    q2 = i2 / 100;
    // really: r = i2 - (q * 100);
    r = i2 - ((q2 << 6) + (q2 << 5) + (q2 << 2));
    i2 = q2;
    buf[--charPos] = Integer.DigitOnes[r];
    buf[--charPos] = Integer.DigitTens[r];
}
複製程式碼

看似和 Integer 一樣,但別忘了這裡是 LongLong 佔用 8 個位元組,而 int 只佔用 4個位元組。所以,當 long 型的值小於 Integer.MAX_VALUE 時,可以強轉為 int, 從而節省記憶體。看到這,你應該明白第一個迴圈體的含義了,當 i > Integer.MAX_VALUE 時,強轉 int 會溢位,就只能當做 long 處理了。

toUnsignedString()

Integer.toUnsignedString(int,int)

public static String toUnsignedString(int i, int radix) {
    return Long.toUnsignedString(toUnsignedLong(i), radix);
}
複製程式碼

上面是 IntegertoUnsignedString() 的實現, 直接呼叫 Long 的相應方法。來看一下 Long 是如何實現這個方法的:

public static String toUnsignedString(long i, int radix) {
    if (i >= 0)
        return toString(i, radix);
    else {
        switch (radix) {
        case 2:
            return toBinaryString(i);

        case 4:
            return toUnsignedString0(i, 2);

        case 8:
            return toOctalString(i);

        case 10:
            /*
             * We can get the effect of an unsigned division by 10
             * on a long value by first shifting right, yielding a
             * positive value, and then dividing by 5.  This
             * allows the last digit and preceding digits to be
             * isolated more quickly than by an initial conversion
             * to BigInteger.
             */
            long quot = (i >>> 1) / 5;
            long rem = i - quot * 10;
            return toString(quot) + rem;

        case 16:
            return toHexString(i);

        case 32:
            return toUnsignedString0(i, 5);

        default:
            return toUnsignedBigInteger(i).toString(radix);
        }
    }
}
複製程式碼

long 型數值轉換為無符號字串。long 的最大值是 2^63-1,而 unsigned long 最大值為 2^64-1。所以正整數對於 toUnsignedString() 方法來說,仍可以當做有符號數處理,直接呼叫 toString(long i,int radix) 處理。

二、四、八、十六進位制

對於負數來說,根據進位制的不同,採取不同的處理。radix2481632時,分別呼叫了 toBinaryString()toUnsignedString0()toOctalString()toHexString()toUnsignedString0() 方法,其實這些方法最後都呼叫了 toUnsignedString0(long i,int shift) 方法,其中 raidx = 1 << shift

static String toUnsignedString0(long val, int shift) {
    // assert shift > 0 && shift <=5 : "Illegal shift value";
    int mag = Long.SIZE - Long.numberOfLeadingZeros(val);
    int chars = Math.max(((mag + (shift - 1)) / shift), 1);
    char[] buf = new char[chars];

    formatUnsignedLong(val, shift, buf, 0, chars);
    return new String(buf, true);
}
複製程式碼

逐行分析一下:

int mag = Long.SIZE - Long.numberOfLeadingZeros(val);
複製程式碼

mag 表示該數字表示為二進位制實際需要的位數(去除高位的 0)。numberOfLeadingZeros() 方法之前說過,表示該數字以二進位制補碼形式表示時最高位 1 前面的位數。比如 16L,二進位制為 0001 0000, 最高位 1 在第 4 位,則前面有 59 個 0,表示 16L 實際需要的位數是 Long.SIZE - 59 = 5,即 10000。當然,對於負數來說,符號位為 1,所以永遠需要 64 位來表示。

int chars = Math.max(((mag + (shift - 1)) / shift), 1);
複製程式碼

根據 magshift 獲取要表示的字串的字元數。仍以上面的 16L 為例,mag 等於 5,若 shift1,計算出 chars 等於 5。若 shift 等於 4,即以十六進位制表示,計算出 chars 等於 2,因為 16 對應的十六進位制是 0x10,表示為字元的話只需要兩個字元。

static int formatUnsignedLong(long val, int shift, char[] buf, int offset, int len) {
   int charPos = len;
   int radix = 1 << shift; // 進位制
   int mask = radix - 1; // 掩碼,二進位制都為 1
   do {
       buf[offset + --charPos] = Integer.digits[((int) val) & mask]; // 取出 val 中對應進位制的最後一位
       val >>>= shift; // 無符號右移 shift,高位補 0,移走上一步已經取出的最後一位資料
   } while (val != 0 && charPos > 0);

   return charPos;
}
複製程式碼

逐行分析:

int charPos = len;
複製程式碼

charPos 表示填充字元在陣列中的位置。初始值為 len ,不難想到還是從陣列尾部開始填充的,所以迴圈體中的內容肯定是取出 unsigned val 的最後一位數字。

int radix = 1 << shift; // 進位制
int mask = radix - 1; // 掩碼,二進位制都為 1
複製程式碼

這裡先記住一點,掩碼 mask 的二進位制有效數字都是 1

二進位制   : mask = 1 = 0b0001
八進位制   : mask = 7 = 0b0111
十六進位制  : mask = 15 = 0b1111
複製程式碼

最後看迴圈體中的內容:

do {
    buf[offset + --charPos] = Integer.digits[((int) val) & mask]; // 取出 val 中對應進位制的最後一位
    val >>>= shift; // 無符號右移 shift,高位補 0,移走上一步已經取出的最後一位資料
} while (val != 0 && charPos > 0);
複製程式碼

由於掩碼 mask 的特殊性,((int) val) & mask 必定是 val 轉換成對應進位制中的最後一位數字,將其塞入字元陣列中。然後執行 val >>>=shift,無符號右移都是高位填 0,移動 shift 位,正好移除了對應進位制的最後一位數。以此迴圈,直到全部填 0,字元陣列也就填滿了。

31L 為例,其二進位制表示為 0001 1111,十六進位制表示為 0x1f,將其轉換為十六進位制字串應該為 1f,其中 radix16shift4mask15

  • 第一次迴圈,((int) val) & mask 等於 0001 1111 & 0000 1111, 值為 1111,對應 Integer.digits 中字元為 'f'。然後將 val 無符號右移四位,得到新的值為 0000 0001,進入第二次迴圈。

  • 第二次迴圈,valmask 與運算,0000 0001 & 0000 1111,值為 0001,對應字元為 '1'。再將 val 無符號右移四位,其值為 0,結束迴圈。

這樣就得到最後的無符號十六進位制字串為 '1f'

十進位制

toUnsignedString(long value,int radix) 對十進位制進行了單獨處理:

case 10:
    /*
     * We can get the effect of an unsigned division by 10
     * on a long value by first shifting right, yielding a
     * positive value, and then dividing by 5.  This
     * allows the last digit and preceding digits to be
     * isolated more quickly than by an initial conversion
     * to BigInteger.
     */
    long quot = (i >>> 1) / 5;
    long rem = i - quot * 10;
    return toString(quot) + rem;
複製程式碼

註釋的大概意思是,我們可以將數字先無符號右移一位得到一個正值,再除以 5, 以此來代替用無符號數除以 10。這樣可以比通過初始化 BigInteger 更快的分離出最後一個數字。粗看程式碼,大概步驟應該是將 i 分成一個 longquot 和最後一個數字 rem,對於 quot 其實是有符號的,直接呼叫 toString() 方法,最後和 rem 拼接起來即可。大致思路是這樣,其中具體的位運算還沒有看的太懂。

其他進位制

default:
    return toUnsignedBigInteger(i).toString(radix);
複製程式碼

對於其他進位制,一律通過 BigInteger 進行處理,這裡不展開分析,後面說道 BigInteger 時再說。

位運算

位運算和 Integer 是完全一致的。

long highestOneBit(long i) : 返回以二進位制補碼形式,取左邊最高位 1,後面全部填 0 表示的 longlong lowestOneBit(long i)  : 與 highestOneBit() 相反,取其二進位制補碼的右邊最低位 1,其餘填 0
int numberOfLeadingZeros(long i) : 返回左邊最高位 1 之前的 0 的個數
int numberOfTrailingZeros(long i): 返回右邊最低位 1 之後的 0 的個數
int bitCount(long i) : 二進位制補碼中 1 的個數
long rotateRight(long i, int distance) : 將 i 的二進位制補碼迴圈右移 distance(注意與普通右移不同的是,右移的數字會移到最左邊)
long rotateLeft(long i, int distance)  : 與 rotateRight 相反
long reverse(long i) : 反轉二進位制補碼
int signum(long i)  : 正數返回 1,負數返回 -1,0 返回 0
long reverseBytes(long i) : 以位元組為單位反轉二進位制補碼
複製程式碼

總結

複習了 Intger 中的內容,補充了 Integer 中沒提到的 toUnsignedString() 方法。關於整數差不多就說到這了,後面有機會再分析一下 BigInteger。下篇文章來說說 Float,現在不妨思考這樣一個問題,0.3f - 0.2f 等於多少,寫下你的答案,再開啟 IDEA 執行一下吧!

文章同步更新於微信公眾號: 秉心說 , 專注 Java 、 Android 原創知識分享,LeetCode 題解,歡迎關注!

走進 JDK 之 Long

相關文章