Java 包裝類型及易錯陷阱詳解

一、預備知識

1、Java把內存劃分成兩種:一種是棧內存,另一種是堆內存。

2、int是基本類型,直接存數值;而 Integer是類,產生對象時用一個引用指向這個對象。

3、包裝器(wrapper)——這是《JAVA核心技術》一書中對Integer這類對象的稱呼。

4、包裝器位於java.lang包中。

5、包裝類是引用傳遞而基本類型是值傳遞(下面的內存管理給予解釋)

6、基本類型的變量和對象的引用變量都是在函數的棧內存中分配 ,而實際的對象是在存儲堆內存中

int i = 5;//直接在棧中分配空間
Integer i = new Integr(5);//對象是在堆內存中,而i(引用變量)是在棧內存中

1.1 Java內存管理

在函數中定義的一些基本類型的變量和對象的引用變量都是在函數的棧內存中分配。當在一段代碼塊中定義一個變量時,Java就在棧中為這個變量分配內存空間,當超過變量的作用域後,Java會自動釋放掉為該變量分配的內存空間,該內存空間可以立刻被另作他用。

堆內存用於存放由new創建的對象和數組。在堆中分配的內存,由Java虛擬機自動垃圾回收器來管理。在堆中產生瞭一個數組或者對象後,還可以在棧中定義一個特殊的變量,這個變量的取值等於數組或者對象在堆內存中的首地址,在棧中的這個特殊的變量就變成瞭數組或者對象的引用變量,以後就可以在程序中使用棧內存中的引用變量來訪問堆中的數組或者對象,引用變量相當於為數組或者對象起的一個別名,或者代號。

引用變量是普通變量,定義時在棧中分配內存,引用變量在程序運行到作用域外釋放。而數組&對象本身在堆中分配,即使程序運行到使用new產生數組和對象的語句所在地代碼塊之外,數組和對象本身占用的堆內存也不會被釋放,數組和對象在沒有引用變量指向它的時候,才變成垃圾,不能再被使用,但是仍然占著內存,在隨後的一個不確定的時間被垃圾回收器釋放掉。這也就是Java比較占內存的主要原因,實際上棧中的變量指向堆內存中的變量,這就是 Java 中的指針!

1.2 基本數據類型的包裝類

基本類型 包裝類
boolean Boolean
char Character
byte Byte
int integer
long Long
float Float
double Double
short Short

1.3 包裝類的構造方法

1、所有包裝類都可將與之對應的基本數據類型作為參數,來構造它們的實例

2、除Character類外,其他包裝類可將一個字符串作為參數構造它們的實例

註意事項:

  • Boolean類構造方法參數為String類型時,若該字符串內容為true(不考慮大小寫),則該Boolean對象表示true,否則表示false。
  • 當Number包裝類構造方法參數為String類型時,字符串不能為null,且該字符串必須可解析為相應的基本數據類型的數據,否則編譯通過,運行時報NumberFormatException異常。

1.4 包裝類的優缺點

包裝類優點:

1、提供瞭一系列實用的方法

2、集合不允許存放基本數據類型數據,存放數字時,要用包裝類型

包裝類缺點:

  • 由於每個值分別包裝在對象中,所以ArrayList<Integer>的效率遠遠低於int[]數組。(應該用其構造小型集合,其原因是程序員操作的方便性要比執行效率更加重要)

1.5 包裝類易錯點

  • 對象包裝器類是不可變的,即一旦構造瞭包裝器,就不允許更改包裝在其中的值。
  • 對象包裝器類是不可變的,因此不能定義他們的子類。
Integer i = new Integer(20);
i = 50;
System.out.println(i); // 50

疑問:為什麼變瞭,前面說是不可變的咋變瞭,前後不是矛盾嗎?

想想前面介紹的Java內存管理方式,也許你已經明白瞭,如果還不明白就看看我的解釋:

Integer i 中的 i 隻是棧中指向對象的一個引用,後來 i = 50 又將i指向瞭50(此處運用到瞭自動裝箱技術),這就是變化的原因,但是原來堆中創建的對象還是不變的。

除瞭包裝器類型:Integer、Long、Short、Byte、Character、Boolean、Float和Double之外,還有BigInteger(java.math包)實例是不可變的,String、BigDecimal也是如此,不能修改它的值。不能修改現有實例的值,對這些類型的操作將返回新的實例。起先,不可變類型看起來可能很不自然,但是它具有很多勝過與其向對應的可變類型的優勢。不可變類型更容易設計、實現和使用;它出錯的可能性更小,並且更加安全

為瞭在一個包含對不可變對象引用的變量上執行計算,需要將計算的結果賦值給該變量。如下面的示例:

BigInteger fiveThousand = new BigInteger("5000");
BigInteger fiftyThousand = new BigInteger("50000");
BigInteger fiveHundredThousand = new BigInteger("500000");
BigInteger total = BigInteger.ZERO;
total = total.add(fiveThousand);
total = total.add(fiftyThousand);
total = total.add(fiveHundredThousand);
System.out.println(total);

二、自動拆/裝箱

基本數據(Primitive)類型的自動裝箱(autoboxing)、拆箱(unboxing)是自J2SE 5.0開始提供的功能。

Java語言規范中說道:在許多情況下包裝與解包裝是由編譯器自行完成的(在這種情況下包裝稱為裝箱,解包裝稱為拆箱)。

//聲明一個Integer對象
Integer num = 10;
/*以上的聲明就是用到瞭自動的裝箱,解析為:
Integer num = new Integer(10);
以上就是一個很好的體現,因為10是屬於基本數據類型的,原則上它是不能直接賦值給一個對象Integer的,但jdk1.5後你就可以進行這樣的聲明,這就是自動裝箱的魅力,自動將基本數據類型轉化為對應的封裝類型。成為一個對象以後就可以調用對象所聲明的所有的方法
自動拆箱:故名思議就是將對象重新轉化為基本數據類型:*/
 
//裝箱
Integer num_1 = 10;
//拆箱
int num_2 = num_1;
/*自動拆箱有個很典型的用法就是在進行運算的時候:因為對象時不能直接進行運算的,而是要轉化為基本數據類型後才能進行加減乘除*/
 
Integer num_3 = 10;
//進行計算時隱含的有自動拆箱
System.out.print(num_3--);
/*哈哈 應該感覺很簡單吧,下面我再來講點稍微難點的.*/
 
//在-128~127 之外的數
Integer num_4 = 297; Integer num_5 = 297;
System.out.println("num_4==num_5: "+(num_4==num_5));
// 在-128~127 之內的數
Integer num_6 = 97; Integer num_7 = 97;
System.out.println("num_6==num_7: "+(num_6==num_7));
/*打印的結果是:
    num_4==num_5: false 
    num_6==num_7: true 
*/
//此處的解釋在下方

註意事項:

1、裝箱和拆箱是編譯器認可的,而不是虛擬機。編譯器在生成類的字節碼時,插入必要的方法調用。虛擬機隻是執行字節碼。

2、包裝對象和拆箱對象可以自由轉換,但是要剔除NULL值,因為null值並不能轉化為基本類型。

import java.util.ArrayList;
import java.util.List;
public class Ceshi {
    // 計算list中所有元素之和
    public static int f(List<Integer> list) {
        int count = 0;
        for (int i : list) {
            count += i;
        }
        return count;
    }
 
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(2);
        list.add(null);
        System.out.println(f(list));
    }
}

運行結果:Exception in thread “main” java.lang.NullPointerException

運行失敗,報空指針異常,我們稍稍思考一下很快就知道原因瞭:在程序的for循環中,隱含瞭一個拆箱過程,再此過程中包裝類型轉換為瞭基本類型。我們知道拆箱過程是通過調用包裝對象的intValue方法來實現的,由於包裝對象是null值,訪問其intValue方法報空指針異常也就在所難免瞭。問題找到瞭,那就解決。(即加入null值檢查即可)

// 計算list中所有元素之和
public static int f(List<Integer> list) {
    int count = 0;
    for (Integer i : list) {
        count += (null != i) ? i : 0;
    }
    return count;
}

針對此類問題:謹記包裝類型參與運算時,要做null值校驗。

三、整形池

@SuppressWarnings("resource")
Scanner input = new Scanner(System.in);
while (input.hasNextInt()) {
    int i = input.nextInt();
    System.out.println("\n*********" + i + "的相等判斷**********");
    // 兩個通過new產生的integer對象
    Integer temp1 = new Integer(i);
    Integer temp2 = new Integer(i);
    System.out.println("new產生的對象:" + (temp1 == temp2));
 
    // 基本類型轉為包裝類型後比較
    temp1 = i;
    temp2 = i;
    System.out.println("基本類型轉換的對象:" + (temp1 == temp2));
 
    // 通過靜態方法生成一個實例
    temp1 = Integer.valueOf(i);
    temp2 = Integer.valueOf(i);
    System.out.println("valueOf產生的對象:" + (temp1 == temp2));
}

運行結果:

127 128 258

*********127的相等判斷**********
new產生的對象:false
基本類型轉換的對象:true
valueOf產生的對象:true
*********128的相等判斷**********
new產生的對象:false
基本類型轉換的對象:false
valueOf產生的對象:false
*********258的相等判斷**********
new產生的對象:false
基本類型轉換的對象:false
valueOf產生的對象:false

很不可思議,數字127的比較結果與另外兩個竟然不一樣,原因在哪裡?

  • new產生的Integer對象:new聲明的就是要生成一個新的對象,因為是兩個對象,地址肯定不一樣,所以比較結果為false毫無疑問。
  • 裝箱生成的對象:首先說明一點,自動裝箱的動作是通過valueOf方法實現的,也就是說後兩個算法是相同的,所以他們的結果一樣,產生上面現象的原因是什麼呢?

我們來看一下valueOf的源代碼:

/***
 * Cache to support the object identity semantics of autoboxing for values
 * between*-128 and 127(inclusive)as required by JLS.**The cache is initialized
 * on first usage.The size of the cache*may be controlled by the{
 * 
 * @code -XX:AutoBoxCacheMax=<size>}option.*During VM
 *       initialization,java.lang.Integer.IntegerCache.high property*may be set
 *       and saved in the private system properties in the*sun.misc.VM class.
 */
 
private static class IntegerCache {
	static final int low = -128;
	static final int high;
	static final Integer cache[];
 
	static {
// high value may be configured by property
		int h = 127;
		String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
		if (integerCacheHighPropValue != null) {
			try {
				int i = parseInt(integerCacheHighPropValue);
				i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
				h = Math.min(i, Integer.MAX_VALUE - (-low) - 1);
			} catch (NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
			}
		}
		high = h;
 
		cache = new Integer[(high - low) + 1];
		int j = low;
		for (int k = 0; k < cache.length; k++)
			cache[k] = new Integer(j++);
 
// range [-128, 127] must be interned (JLS7 5.1.7)
		assert IntegerCache.high >= 127;
	}
 
	private IntegerCache() {
	}
 
}
 
/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #Integer(int)}, as this method is likely
* to yield significantly better space and time performance by
* caching frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.5
*/
public static Integer valueOf(int i) {
	if (i >= IntegerCache.low && i <= IntegerCache.high)
		return IntegerCache.cache[i + (-IntegerCache.low)];
	return new Integer(i);
}

如果不是-128到127之間的 int 類型轉換為 Integer 對象,則直接返回一個新的對象。否則直接從cache數組中獲得。

cache是IntegerCache內部類的一個靜態數組,容納的是-128到127之間的Integer對象。通過valueOf產生包裝對象時,如果int參數在-128到127之間,則直接從整型池中獲得對象,不在該范圍的int類型則通過new生成包裝對象。

這就是整形池,其存在不僅提高瞭系統性能,同時節約瞭內存空間。

所以在聲明包裝對象的時候使用valueOf生成,而不是通過構造函數來生成的原因,在判斷對象是否相等的時候,最好是用equals方法,避免使用==產生非預期結果。

註意:通過包裝類的valueOf生成包裝實例可以顯著提高空間和時間性能。

四、優先選擇基本數據類型

包裝類型是一個類,它提供瞭諸如構造方法、類型轉換、比較等非常實用的功能,而且自動裝箱(拆箱)更是如虎添翼,但是無論是從安全性、性能方面來說,還是從穩定性方面來說,基本類型是首選方案。

public class Ceshi {
    public static void main(String[] args) {
        Ceshi temp=new Ceshi();
        int a=500;
        //分別傳遞int類型和Integer類型
        temp.f(a);
        temp.f(Integer.valueOf(a));
    }
    public void f(long i){
        System.out.println("基本類型參數的方法被調用");
    }
    public void f(Long i){
        System.out.println("包裝類型參數的方法被調用");
    }
}

上面程序的運行結果是:

基本類型參數的方法被調用
基本類型參數的方法被調用

很詫異是吧!感覺應該輸出不一樣的,第一個輸出毫無疑問,系統進行瞭自動的類型轉換,這種轉換隻能往高提升。第二個為什麼沒有調用包裝類參數的函數呢?

原因是自動裝箱有一個重要的原則:基本類型可以先加寬,再轉成寬類型的包裝類型,但不能直接轉變成寬類型的包裝類型。換句話說int可以加寬轉變成long,然後在轉變成Long對象,但不能直接轉變成包裝類型,註意這裡指的都是自動轉換,不是通過構造函數生成。

temp.f(Integer.valueOf(a));

這段代碼的執行過程為

1、a 通過valueOf方法包裝成一個Integer對象。

2、由於沒有f(Integer i)方法,編譯器“聰明”地把 Integer 對象又轉換成 int。

3、int 自動拓寬為 long,編譯結束。

註意:基本數據類型優先考慮。

public class Ceshi {
    public static void main(String[] args) {
        Ceshi temp=new Ceshi();
        int a=500;
        //分別傳遞int類型和Long類型
        temp.f(a);
        temp.f(Long.valueOf(a));
    }
    public void f(long i){
        System.out.println("基本類型參數的方法被調用");
    }
    public void f(Long i){
        System.out.println("包裝類型參數的方法被調用");
    }
}

這段程序的輸出結果為:

基本類型參數的方法被調用
包裝類型參數的方法被調用

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: