走進 JDK 之 Byte

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

走進 JDK 之 Integer數組

走進 JDK 之 Long緩存

走進 JDK 之 Floatbash

今天來講說 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() 方法轉換爲 bytepost

方法

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 來處理,後面的分析中將會看到。this

這裏再提一個問題,做爲方法內部局部變量的 byte 在內存中佔幾個字節 ?spa

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 屬性:

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

max_locals 表明了局部變量表所需的存儲空間,以 slot 爲單位。Slot 是虛擬機爲局部變量分配內存所使用的最小單位。簡而言之,棧幀就是一個 Slot[],利用下標來訪問數組元素。那麼,對於不一樣的數據類型是如何處理的呢?這裏就是典型的以空間換時間。除了 longdouble 佔用兩個 Slot 之外,其餘基本類型 booleanbytecharshortintfloat 等都佔用一個 Slot。這樣就而已快速的利用下標索引來進行定位了。因此,在局部變量表中,byteint 佔用的內存是同樣的。

總結

Byte 源碼沒有說的不少,不少方法都是直接調用 Integer 類的方法。後面主要說了兩個知識點:

  • 補碼錶示法更加利用運算,把減法當加法算,且能夠多表示一個 -128,也就是 1000 0000
  • 基本類型做爲方法局部變量是存儲在棧幀上的,除了 longdouble 佔兩個 Slot,其餘都佔用一個 Slot

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

相關文章
相關標籤/搜索