java基礎(七)-----深入剖析Java中的裝箱和拆箱
本文主要介紹Java中的自動拆箱與自動裝箱的有關知識。
基本資料型別
基本型別,或者叫做內建型別,是Java中不同於類(Class)的特殊型別。它們是我們程式設計中使用最頻繁的型別。
Java是一種強型別語言,第一次申明變數必須說明資料型別,第一次變數賦值稱為變數的初始化。
Java基本型別共有八種,基本型別可以分為三類:
字元型別char
布林型別boolean
數值型別byte
、short
、int
、long
、float
、double
。
數值型別又可以分為整數型別byte
、short
、int
、long
和浮點數型別float
、double
。
Java中的數值型別不存在無符號的,它們的取值範圍是固定的,不會隨著機器硬體環境或者作業系統的改變而改變。
基本資料型別有什麼好處
我們都知道在Java語言中,new
一個物件是儲存在堆裡的,我們通過棧中的引用來使用這些物件;所以,物件本身來說是比較消耗資源的。
對於經常用到的型別,如int等,如果我們每次使用這種變數的時候都需要new一個Java物件的話,就會比較笨重。所以,和C++一樣,Java提供了基本資料型別,這種資料的變數不需要使用new建立,他們不會在堆上建立,而是直接在棧記憶體中儲存,因此會更加高效。
整型的取值範圍
Java中的整型主要包含byte
、short
、int
和long
這四種,表示的數字範圍也是從小到大的,之所以表示範圍不同主要和他們儲存資料時所佔的位元組數有關。
先來個簡答的科普,1位元組=8位(bit)。java中的整型屬於有符號數。
先來看計算中8bit可以表示的數字:
最小值:10000000 (-128)(-2^7) 最大值:01111111(127)(2^7-1)
整型的這幾個型別中,
-
byte:byte用1個位元組來儲存,範圍為-128(-2^7)到127(2^7-1),在變數初始化的時候,byte型別的預設值為0。
-
short:short用2個位元組儲存,範圍為-32,768 (-2^15)到32,767 (2^15-1),在變數初始化的時候,short型別的預設值為0,一般情況下,因為Java本身轉型的原因,可以直接寫為0。
-
int:int用4個位元組儲存,範圍為-2,147,483,648 (-2^31)到2,147,483,647 (2^31-1),在變數初始化的時候,int型別的預設值為0。
-
long:long用8個位元組儲存,範圍為-9,223,372,036,854,775,808 (-2^63)到9,223,372,036, 854,775,807 (2^63-1),在變數初始化的時候,long型別的預設值為0L或0l,也可直接寫為0。
超出範圍怎麼辦
上面說過了,整型中,每個型別都有一定的表示範圍,但是,在程式中有些計算會導致超出表示範圍,即溢位。如以下程式碼:
int i = Integer.MAX_VALUE; int j = Integer.MAX_VALUE; int k = i + j; System.out.println("i (" + i + ") + j (" + j + ") = k (" + k + ")");
輸出結果:i (2147483647) + j (2147483647) = k (-2)
這就是發生了溢位,溢位的時候並不會拋異常,也沒有任何提示。所以,在程式中,使用同類型的資料進行運算的時候,一定要注意資料溢位的問題。
包裝型別
Java語言是一個面向物件的語言,但是Java中的基本資料型別卻是不面向物件的,這在實際使用時存在很多的不便,為了解決這個不足,在設計類時為每個基本資料型別設計了一個對應的類進行代表,這樣八個和基本資料型別對應的類統稱為包裝類(Wrapper Class)。
包裝類均位於java.lang包,包裝類和基本資料型別的對應關係如下表所示
基本資料型別 | 包裝類 |
---|---|
byte | Byte |
boolean | Boolean |
short | Short |
char | Character |
int | Integer |
long | Long |
float | Float |
double | Double |
在這八個類名中,除了Integer和Character類以後,其它六個類的類名和基本資料型別一致,只是類名的第一個字母大寫即可。
為什麼需要包裝類
很多人會有疑問,既然Java中為了提高效率,提供了八種基本資料型別,為什麼還要提供包裝類呢?
這個問題,其實前面已經有了答案,因為Java是一種面嚮物件語言,很多地方都需要使用物件而不是基本資料型別。比如,在集合類中,我們是無法將int 、double等型別放進去的。因為集合的容器要求元素是Object型別。
為了讓基本型別也具有物件的特徵,就出現了包裝型別,它相當於將基本型別“包裝起來”,使得它具有了物件的性質,並且為其添加了屬性和方法,豐富了基本型別的操作。
拆箱與裝箱
那麼,有了基本資料型別和包裝類,肯定有些時候要在他們之間進行轉換。比如把一個基本資料型別的int轉換成一個包裝型別的Integer物件。
我們認為包裝類是對基本型別的包裝,所以,把基本資料型別轉換成包裝類的過程就是打包裝,英文對應於boxing,中文翻譯為裝箱。
反之,把包裝類轉換成基本資料型別的過程就是拆包裝,英文對應於unboxing,中文翻譯為拆箱。
在Java SE5之前,要進行裝箱,可以通過以下程式碼:
Integer i = new Integer(10);
自動拆箱與自動裝箱
在Java SE5中,為了減少開發人員的工作,Java提供了自動拆箱與自動裝箱功能。
自動裝箱: 就是將基本資料型別自動轉換成對應的包裝類。
自動拆箱:就是將包裝類自動轉換成對應的基本資料型別。
Integer i =10;//自動裝箱 int b= i;//自動拆箱
Integer i=10
可以替代Integer i = new Integer(10);
,這就是因為Java幫我們提供了自動裝箱的功能,不需要開發者手動去new一個Integer物件。
自動裝箱與自動拆箱的實現原理
既然Java提供了自動拆裝箱的能力,那麼,我們就來看一下,到底是什麼原理,Java是如何實現的自動拆裝箱功能。
我們有以下自動拆裝箱的程式碼:
public staticvoid main(String[]args){ Integer integer=1; //裝箱 int i=integer; //拆箱 }
對以上程式碼進行反編譯後可以得到以下程式碼:
public staticvoid main(String[]args){ Integer integer=Integer.valueOf(1); int i=integer.intValue(); }
從上面反編譯後的程式碼可以看出,int的自動裝箱都是通過Integer.valueOf()
方法來實現的,Integer的自動拆箱都是通過integer.intValue
來實現的。如果讀者感興趣,可以試著將八種類型都反編譯一遍 ,你會發現以下規律:
自動裝箱都是通過包裝類的valueOf()
方法來實現的.自動拆箱都是通過包裝類物件的xxxValue()
來實現的。
哪些地方會自動拆裝箱
我們瞭解過原理之後,在來看一下,什麼情況下,Java會幫我們進行自動拆裝箱。前面提到的變數的初始化和賦值的場景就不介紹了,那是最簡單的也最容易理解的。
我們主要來看一下,那些可能被忽略的場景。
場景一、將基本資料型別放入集合類
我們知道,Java中的集合類只能接收物件型別,那麼以下程式碼為什麼會不報錯呢?
List<Integer> li = new ArrayList<>(); for (int i = 1; i < 50; i ++){ li.add(i); }
將上面程式碼進行反編譯,可以得到以下程式碼:
List<Integer> li = new ArrayList<>(); for (int i = 1; i < 50; i += 2){ li.add(Integer.valueOf(i)); }
以上,我們可以得出結論,當我們把基本資料型別放入集合類中的時候,會進行自動裝箱。
場景二、包裝型別和基本型別的大小比較
有沒有人想過,當我們對Integer物件與基本型別進行大小比較的時候,實際上比較的是什麼內容呢?看以下程式碼:
Integer a=1; System.out.println(a==1?"等於":"不等於"); Boolean bool=false; System.out.println(bool?"真":"假");
對以上程式碼進行反編譯,得到以下程式碼:
Integer a=1; System.out.println(a.intValue()==1?"等於":"不等於"); Boolean bool=false; System.out.println(bool.booleanValue?"真":"假");
可以看到,包裝類與基本資料型別進行比較運算,是先將包裝類進行拆箱成基本資料型別,然後進行比較的。
場景三、包裝型別的運算
有沒有人想過,當我們對Integer物件進行四則運算的時候,是如何進行的呢?看以下程式碼:
Integer i = 10; Integer j = 20; System.out.println(i+j);
反編譯後代碼如下:
Integer i = Integer.valueOf(10); Integer j = Integer.valueOf(20); System.out.println(i.intValue() + j.intValue());
我們發現,兩個包裝型別之間的運算,會被自動拆箱成基本型別進行。
場景四、三目運算子的使用
這是很多人不知道的一個場景,作者也是一次線上的血淋淋的Bug發生後才瞭解到的一種案例。看一個簡單的三目運算子的程式碼:
boolean flag = true; Integer i = 0; int j = 1; int k = flag ? i : j;
很多人不知道,其實在int k = flag ? i : j;
這一行,會發生自動拆箱。反編譯後代碼如下:
boolean flag = true; Integer i = Integer.valueOf(0); int j = 1; int k = flag ? i.intValue() : j; System.out.println(k);
這其實是三目運算子的語法規範。當第二,第三位運算元分別為基本型別和物件時,其中的物件就會拆箱為基本型別進行操作。
因為例子中,flag ? i : j;
片段中,第二段的i是一個包裝型別的物件,而第三段的j是一個基本型別,所以會對包裝類進行自動拆箱。如果這個時候i的值為null
,那麼久會發生NullPointerException。
場景五、函式引數與返回值
這個比較容易理解,直接上程式碼了:
//自動拆箱 public int getNum1(Integer num) { return num; } //自動裝箱 public Integer getNum2(int num) { return num; }
自動拆裝箱與快取
Java SE的自動拆裝箱還提供了一個和快取有關的功能,我們先來看以下程式碼,猜測一下輸出結果:
public static void main(String... strings) { Integer integer1 = 3; Integer integer2 = 3; if (integer1 == integer2) System.out.println("integer1 == integer2"); else System.out.println("integer1 != integer2"); Integer integer3 = 300; Integer integer4 = 300; if (integer3 == integer4) System.out.println("integer3 == integer4"); else System.out.println("integer3 != integer4"); }
我們普遍認為上面的兩個判斷的結果都是false。雖然比較的值是相等的,但是由於比較的是物件,而物件的引用不一樣,所以會認為兩個if判斷都是false的。在Java中,==比較的是物件應用,而equals比較的是值。所以,在這個例子中,不同的物件有不同的引用,所以在進行比較的時候都將返回false。奇怪的是,這裡兩個類似的if條件判斷返回不同的布林值。
上面這段程式碼真正的輸出結果:
integer1 == integer2 integer3 != integer4
原因就和Integer中的快取機制有關。在Java 5中,在Integer的操作上引入了一個新功能來節省記憶體和提高效能。整型物件通過使用相同的物件引用實現了快取和重用。
適用於整數值區間-128 至 +127。
只適用於自動裝箱。使用建構函式建立物件不適用。
我們只需要知道,當需要進行自動裝箱時,如果數字在-128至127之間時,會直接使用快取中的物件,而不是重新建立一個物件。
如果一個變數p的值是:
-128至127之間的整數 true 和 false的布林值 ‘\u0000’至 ‘\u007f’之間的字元
範圍內的時,將p包裝成a和b兩個物件時,可以直接使用a==b判斷a和b的值是否相等。
推薦部落格
https://www.cnblogs.com/chen-haozi/p/10227797.html
自動拆裝箱帶來的問題
當然,自動拆裝箱是一個很好的功能,大大節省了開發人員的精力,不再需要關心到底什麼時候需要拆裝箱。但是,他也會引入一些問題。
包裝物件的數值比較,不能簡單的使用==
,雖然-128到127之間的數字可以,但是這個範圍之外還是需要使用equals
比較。
前面提到,有些場景會進行自動拆裝箱,同時也說過,由於自動拆箱,如果包裝類物件為null,那麼自動拆箱時就有可能丟擲NPE。
如果一個for迴圈中有大量拆裝箱操作,會浪費很多資源。