走進 JDK 之 Byte

秉心說發表於2019-03-22

整理一下前面幾篇文章,按順序閱讀效果更好。

走進 JDK 之 Integer

走進 JDK 之 Long

走進 JDK 之 Float

今天來說說 Byte

類宣告

public final class Byte extends Number implements Comparable<Byte> 
複製程式碼

和之前的一模一樣,不可變類,繼承了抽象類 Number,實現了 Comparable 介面。

欄位

private final byte value; // 包裝的 byte 值
public static final byte   MIN_VALUE = -128; // 最小值是 -128
public static final byte   MAX_VALUE = 127; // 最大值是 127
public static final Class<Byte>     TYPE = (Class<Byte>) Class.getPrimitiveClass("byte");
public static final int SIZE = 8; // byte 佔 8 bits
public static final int BYTES = SIZE / Byte.SIZE; // byte 佔一個位元組
private static final long serialVersionUID = -7183698231559129828L;
複製程式碼

都是很熟悉的屬性,不再過多分析了。這裡提第一個問題,為什麼最大值是 127,最小值是 -128,最小值的絕對值可以比最大值的絕對值大 1 呢 ?這裡先不說,看完程式碼再來解答。

建構函式

public Byte(byte value) {
    this.value = value;
}

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

兩個建構函式。第一個傳入 byte 直接給 value 賦值,第二個傳入字串,呼叫 parseByte() 方法轉換為 byte

方法

parseByte()

    public static byte parseByte(String s, int radix)
        throws NumberFormatException {
        int i = Integer.parseInt(s, radix);
        if (i < MIN_VALUE || i > MAX_VALUE)
            throw new NumberFormatException(
                "Value out of range. Value:\"" + s + "\" Radix:" + radix);
        return (byte)i;
    }
複製程式碼

呼叫 Integer.parseInt() 方法轉換為 int,再強轉 byteInteger.parseInt() 詳細解析見 走進 JDK 之 Integer 。不光是 parseInt() 方法,Byte.java 中還有好幾個地方都是當做 int 來處理,後面的分析中將會看到。

這裡再提一個問題,作為方法內部區域性變數的 byte 在記憶體中佔幾個位元組 ?

valueOf()

public static Byte valueOf(String s, int radix)
    throws NumberFormatException {
    return valueOf(parseByte(s, radix));
}

public static Byte valueOf(byte b) {
    final int offset = 128;
    return ByteCache.cache[(int)b + offset];
}
複製程式碼

再來看一下 ByteCache :

private static class ByteCache {
    private ByteCache(){}

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

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

同樣也是快取了 -128127,也就是說快取了 byte 的所有可取值。

toString()

public String toString() {
    return Integer.toString((int)value);
}
複製程式碼

toString() 方法直接呼叫了 Integer.toString()

其他的確沒啥好說的了,Byte 類原始碼比較簡單,認真度過 Integer 原始碼的同學,大概瀏覽一下就有數了。後面來回答一下前面提問的兩個問題。

為什麼 Byte 最小值的絕對值比最大值的絕對值大 1 呢 ?

其實不光是 Byte,Java 的所以基本整數型別都是這樣(當然不包括 char) :

基本型別 最大值 最小值
byte 127 -128
short 215-1 - 215
int 231-1 231
long 263-1 -263

可以看到取值範圍都是不對稱的,負數的範圍比正數的範圍都大 1。解釋這個問題之前,先來看幾個基本概念:

  • 原碼:最高位是符號位,後面表示具體數值。
  • 反碼:原碼的符號位不變,其餘取反
  • 補碼:反碼加 1
  • 以上僅針對負數,正數的原碼、反碼、補碼都是其本身

例如 -8 ,其原碼是 1000 1000,反碼是 1111 0111,補碼是 1111 1000。那麼計算機中到底儲存的是哪種形式呢?這就要涉及到減法運算了。相比減法運算,計算機是更樂意做加法運算的,如果遇到 1 - 8 這道題目,它就會想我計算 1 + (-8) 不是一個道理嗎,最好我還能不把符號位當符號位,一起作加法,還能提高一點運算效率。那麼,負數的加法運算怎麼做的,我們來嘗試一下。

首先,我們按原碼計算:

1 + (-8) = (0000 0001)(原) + (1000 1000)(原) = (1000 1001)(原) = -9
複製程式碼

顯然不正確。再看反碼:

1 + (-8) = (0000 0001)(反) + (1111 0111)(反) = (1111 1000)(反) = (1000 0111)(原) = -7
複製程式碼

好像沒什麼毛病,計算結果很正確。再換個例子看看:

1 + (-1) = (0000 0001)(反) + (1111 1110)(反) = (1111 1111)(反) = (1000 0000)(原) = -0
複製程式碼

上篇文章解析 Float 時說過,浮點數是區分 +0.0-0.0 的。但是整數的 0 是沒有正負之分的,用反碼沒法解決 -0 的問題。最後來看一下補碼運算是否會存在 -0

1 + (-1) = (0000 0001)(補) + (1111 1111)(補) = (0000 0000)(補) = (0000 0000)(原) = 0
複製程式碼

通過進位把符號位的 1 給溢位了,從而避免產生了 -0

綜上所述,補碼是比較適合在計算機中來表示整數的,實際上大多數計算機也正是這麼做的。再回到這個 -0,二進位制表示為 1000 0000,總不能把它丟掉吧,多點表示範圍總是好的,就把它定為了 -128,並且它沒有反碼,也沒有補碼,它就是 -128

現在我們知道了 -128 其實就是替代了 -0 的存在。再來說一個知識點,你會更加直觀的瞭解 -128。如何快速的將補碼轉換為十進位制數?其實不論正數還是負數,補碼二進位制轉換為我們熟悉的十進位制都遵循相同的規律。看看下面幾個轉換:

 15 = (0000 1111)(補)
    = - 0*2^8 + (1*2^3 + 1*2^2 + 1*2^1 +1*2^0)
    = 0 + 8 + 4 + 2 + 1
   
-15 = (1111 0001)(補)
    = - 1*2^8 + (1*2^6 + 1*2^5 + 1*2^4 +1*2^0)
    = -128 + 64 + 32 + 16 + 1

複製程式碼

不需要轉換為原始碼,直接按補碼計算。正數符號位表示為 0,負數符號位表示為 -128。顯而易見,最小的負數肯定是 10000 0000 = -128 + 0 + 0 + ... 。現在你應該對個問題很清楚了吧。下面看第二個問題:

作為方法內部區域性變數的 byte 在記憶體中佔幾個位元組 ?

乍看之下我在問一個廢話,byte 那不肯定是 1 個位元組嗎 !沒錯,byte 是一個位元組,但是我這個問題有特定的條件,作為方法內部區域性變數的 byte。我們通常所說的 byte 佔一個位元組,指的是如果在 java 堆上分配一個 byte,那麼就是一個位元組。同理,int 就是四個位元組。那麼,方法內的區域性變數 是儲存在堆上的嗎?顯然不是的,它是儲存在棧中的。如果不理解的話,我們先來回顧一下 Java 的執行時資料區域。

Java 的執行時資料區包含一下幾塊:

  • 程式計數器:當前執行緒所執行的位元組碼的行號指示器
  • Java 虛擬機器棧:描述的是 Java 方法執行的記憶體模型
  • 本地方法棧:為 native 方法服務
  • Java 堆:所有的物件例項以及陣列都在這裡分配
  • 方法區:儲存已被虛擬機器載入的類資訊、常量、靜態常量、即時編譯器編譯後的程式碼等資料
  • 執行時常量池:方法區的一部分,存放編譯期生成的各種字面量和符號引用

我們通常所說的棧就是指 Java 虛擬機器棧。每一個執行緒都有自己的 Java 虛擬機器棧,用於儲存棧幀。棧幀是用於支援虛擬機器進行方法呼叫和方法執行的資料結構。每個方法在執行的同時都會建立一個棧幀,用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫直至執行完成的過程,就對應一個棧幀在虛擬機器棧中入棧到出棧的過程。所以,方法內的區域性變數 byte 不出意外應該就是儲存在區域性變數中了。那麼,區域性變數表的結構又是怎麼樣的呢?

區域性變數表是一組變數值儲存空間,用於存放方法引數和方法內部定義的變數。在 Java 程式編譯 Class 檔案時,就在方法的 Code 屬性的 max_locals 資料項中確定了該方法所需分配的區域性變數表的最大容量。在我之前一篇文章 Class 檔案格式詳解 中,詳細解析了 Class 檔案結構,我們再來回顧一下它的 main() 方法的 Code 屬性:

走進 JDK 之 Byte

max_stack 代表了運算元棧深度的最大值。在方法執行的任意時刻,運算元棧都不會超過這個深度。虛擬機器執行的時候需要根據這個值來分配棧幀中的操作棧深度。

max_locals 代表了區域性變數表所需的儲存空間,以 slot 為單位。Slot 是虛擬機器為區域性變數分配記憶體所使用的最小單位。簡而言之,棧幀就是一個 Slot[],利用下標來訪問陣列元素。那麼,對於不同的資料型別是如何處理的呢?這裡就是典型的以空間換時間。除了 longdouble 佔用兩個 Slot 以外,其他基本型別 booleanbytecharshortintfloat 等都佔用一個 Slot。這樣就而已快速的利用下標索引來進行定位了。所以,在區域性變數表中,byteint 佔用的記憶體是一樣的。

總結

Byte 原始碼沒有說的很多,很多方法都是直接呼叫 Integer 類的方法。後面主要說了兩個知識點:

  • 補碼錶示法更加利用運算,把減法當加法算,且可以多表示一個 -128,也就是 1000 0000
  • 基本型別作為方法區域性變數是儲存在棧幀上的,除了 longdouble 佔兩個 Slot,其他都佔用一個 Slot

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

走進 JDK 之 Byte

相關文章