Java基礎系列1:深入理解Java數據類型 當初學習電腦的時候,教科書中對程式的定義是:程式=數據結構+演算法,Java基礎系列第一篇就聊聊Java中的數據類型。 本篇聊Java數據類型主要包括四個內容: Java基本類型 Java封裝類型 自動裝箱和拆箱 封裝類型緩存機制 Java基本類型 Ja ...
Java基礎系列1:深入理解Java數據類型
當初學習電腦的時候,教科書中對程式的定義是:程式=數據結構+演算法,Java基礎系列第一篇就聊聊Java中的數據類型。
本篇聊Java數據類型主要包括四個內容:
- Java基本類型
- Java封裝類型
- 自動裝箱和拆箱
- 封裝類型緩存機制
Java基本類型
Java基本類型分類、大小及表示範圍
Java的基本數據類型總共有8種,包括三類:數值型,字元型,布爾型,其中
- 數值型:
- 整數類型:byte、short、int、long
- 浮點類型:float、double
- 字元型:char
- 布爾型:boolean
字元類型在記憶體中占有2個位元組,可以用來保存英文字母等字元。電腦處理字元類型時,是把這些字元當成不同的整數來看待,即ASKII碼,因此,嚴格來說,字元類型也算是整數類型的一種。
Java的這8種基本類型的大小,即所占用的存儲位元組數,以及可以表示的數據範圍如下表所示:
Java基本類型之間的轉換
Java是強類型的編程語言,其數據類型在定義時就已經確定了,因此不能隨意轉換成其他的數據類型,但是Java允許將一種類型賦值給另一種類型。
在Java中,boolean類型與其他7種類型的數據都不能進行轉換,這一點很明確。
但對於其他7種數據類型,它們之間都可以進行轉換,只是可能會存在精度損失或其他一些變化。轉換分為自動轉換和強制轉換:
- 自動類型轉換(隱式):無需任何操作
- 強制類型轉換(顯式):需使用轉換操作符
自動類型轉換需要滿足如下兩個條件:
- 轉換前的數據類型與轉換後的數據類型相容;
- 轉換後的數據類型的表示範圍比轉換前的類型大。
如果將6種數值類型作如下排序:
1
|
double > float > long > int > short > byte
|
那麼從小轉換到大,那麼可以直接轉換,而從大到小,或char或其他6種數據類型轉換,則必須使用強制轉換,且可能會發生精度損失。
Java基本數據類型的預設值
在某些場景下,比如在Restful API介面中,如果在dto中使用了基本類型的參數,那麼即使請求體中沒有傳該參數,伺服器在做反序列化的時候也會將該參數以預設值來處理。所以在實際開發的dto中務必不要使用基本類型。
以下是Java基本數據類型的預設值:
Java封裝類型
對於上面的8種基本類型,Java都有對應的封裝類型:
基本類型 | 封裝類型 |
---|---|
byte | Byte |
int | Integer |
short | Short |
float | Float |
double | Double |
long | Long |
boolean | Boolean |
char | Character |
基本類型 vs 封裝類型
Java封裝類型與基本類型相比,有如下區別:
- 從參數傳遞上來說,基本類型只能按值傳遞,而每個封裝類都是按引用傳遞的;
- 從存儲的位置上來說,基本類型是存儲在棧中的,而所有的對象都是在堆上創建和存儲的,所以基本類型的存取速度要快於在堆中的封裝類型的實例對象;JDK5.0開始可以自動封包了 ,也就是基本數據可以自動封裝成封裝類,基本數據類型的好處就是速度快(不涉及到對象的構造和回收),封裝類的目的主要是更好的處理數據之間的轉換,方法很多,用起來也方便。
- 基本類型的優勢是:數據存儲相對簡單,運算效率比較高;
- 封裝類型的優勢是:類型轉換的api更好用了,比如Integer.parseInt(*)等的,每個封裝類型都提供了parseXXX方法和toString方法。而且在集合當中,也只能使用封裝類型。封裝類型滿足了Java中一切皆對象的原則。
自動裝箱和拆箱
什麼是自動裝箱和拆箱
1
|
// 自動裝箱
|
簡單地說,裝箱就是自動將基本數據類型轉換為封裝類型;拆箱就是自動將封裝類型轉換為基本類型。
自動裝箱和拆箱的執行過程
我們就以上面的Integer的簡單例子來研究執行過程,具體代碼如下:
1
|
public class Main {
|
先編譯,執行:javac Main.java
,
再反編譯,執行:javap -c Main
,
執行後得到如下內容:
可以看到,
在執行Integer numInteger = 66;
的時候,系統為我們執行了Integer numInteger = Integer.valueOf(66)
;
在執行int numInt = numInteger;
的時候,系統為我們執行了int numInt = numInteger.intValue();
我們再來看一下Integer中valueOf方法的源碼:
1
|
public static Integer valueOf(int i) {
|
其中IntegerCache.low=-128,IntegerCache.high=127。
也即,在執行Integer.valueOf(num)方法時,會先判斷num的大小,如果小於-128或者大於127,就創建一個Integer對象,否則就從IntegerCache中來獲取。這裡涉及到了Integer的緩存機制,下一小節詳細討論。
1
|
private final int value;
|
這是Integer的構造函數,裡面定義了一個value變數,創建一個Integer對象,就會給這個變數初始化。
再來簡單看看IntegerCache是什麼東西,IntegerCache類時Integer類的一個內部類,其包含了三個屬性,如下:
1
|
private static class IntegerCache {
|
在valueOf方法中用到的cache數組,是一個靜態的Integer數組對象,而這個數組對象在Integer第一次使用的時候就會創建好。
總之,valueOf返回的都是一個Integer對象。所以我們這裡可以總結一點:裝箱的過程會創建對應的對象,這個會消耗記憶體,所以裝箱的過程會增加記憶體的消耗,影響性能。
封裝類型緩存機制
Integer緩存機制源碼分析
我們仍舊以Integer的例子來說明封裝類型的緩存機制,看一下完整的IntegerCache類的代碼:
1
|
private static class IntegerCache {
|
代碼很簡單,JVM在初始化的時候可以配置值java.lang.Integer.IntegerCache.high
,預設為127,然後在第一次使用Integer的時候,不是只創建需要的那一個Integer對象,而是創建值在-128到java.lang.Integer.IntegerCache.high
範圍內的所有的Integer對象,然後將其放入到cache數組中。
然後在每次自動裝箱的時候,如果值落在該範圍內,則自動從cache數組中去拿出已經實例化的對象來用,而不用再次去實例化這樣一個Integer對象。
每一個整數類型和字元類型、bool類型的封裝類型都有類似的緩存機制,這也是為了減輕封裝類型相比於基本類型的性能消耗。
Integer緩存機制實例
我們再舉一個例子來說明緩存機制。
1
|
public class Main {
|
後面的執行結果大家可能會很吃驚,原因是什麼呢,結合Integer緩存機制的說明,可以明白這個過程如下:
- i1和i2會進行自動裝箱,執行了valueOf方法,它們的值落在[-128,128),所以它們取到的IntegerCache.cache中的是同一個對象,所以它們是相等的;
- i3和i4也會進行自動裝箱,執行valueOf方法時,它們的值都大於128,所以會執行new Integer(200),也即它們分別創建了兩個不同的對象,所以它們肯定不相等。
浮點類型無緩存機制
上面介紹的緩存機制僅針對整數類型、字元類型、布爾類型,因為這幾種數據類型在一定區間的值的數量是固定,但是浮點類型如Float和Double卻在任意區間都有無數個值。
來看看Double.valueOf的源碼就知道了:
1
|
public static Double valueOf(String s) throws NumberFormatException {
|
可以看到Double.valueOf是直接返回一個新的Double對象,並沒有緩存機制。
使用緩存機制的封裝類型
進行一個歸類,使用了緩存機制的封裝類型有這樣幾種:
類型 | 預設緩存對象範圍 |
---|---|
Integer | [-128,127] |
Short | [-128,127] |
Long | [-128,127) |
Character | [0,127] |
總結
- 當一個基本數據類型與封裝類型進行==、+、-、*、/運算時,會將封裝類進行拆箱,對基本數據類型進行運算;
- 拆箱完成運算之後,如果返回的結果需要是封裝類型,則需要進行自動裝箱,返回封裝對象;
- equals(Object o) 因為原equals方法中的參數類型是封裝類型,所傳入的參數類型(a)是原始數據類型,所以會自動對其裝箱,反之,會對其進行拆箱;
- 當兩種不同類型用==比較時,包裝器類的需要拆箱, 當同種類型用==比較時,會自動拆箱或者裝箱。