整理一下前面幾篇文章,按順序閱讀效果更好。
今天來說說 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
,再強轉 byte
。Integer.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));
}
}
複製程式碼
同樣也是快取了 -128
到 127
,也就是說快取了 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[]
,利用下標來訪問陣列元素。那麼,對於不同的資料型別是如何處理的呢?這裡就是典型的以空間換時間。除了 long
和 double
佔用兩個 Slot
以外,其他基本型別 boolean
、byte
、char
、short
、int
、float
等都佔用一個 Slot
。這樣就而已快速的利用下標索引來進行定位了。所以,在區域性變數表中,byte
和 int
佔用的記憶體是一樣的。
總結
Byte
原始碼沒有說的很多,很多方法都是直接呼叫 Integer
類的方法。後面主要說了兩個知識點:
- 補碼錶示法更加利用運算,把減法當加法算,且可以多表示一個
-128
,也就是1000 0000
- 基本型別作為方法區域性變數是儲存在棧幀上的,除了
long
和double
佔兩個Slot
,其他都佔用一個Slot
文章同步更新於微信公眾號:
秉心說
, 專注 Java 、 Android 原創知識分享,LeetCode 題解,歡迎關注!