1. 程式人生 > >Java表示式的陷阱——泛型引起的錯誤

Java表示式的陷阱——泛型引起的錯誤

4、泛型引起的錯誤

        泛型是Java5新增的知識點,它允許在使用Java類、呼叫方法時傳入一個型別引數,這樣就可以讓Java類、呼叫方法動態地改變型別。

4、1 原始型別變數的賦值

       在嚴格的泛型程式中,使用帶泛型宣告的類時應該總是為止指定型別實參,但為了與之前版本Java程式碼保持一致,Java也允許使用帶泛型宣告的類時不指定型別引數。如果使用帶泛型宣告的類時沒有傳入型別引數,那麼這個型別引數預設是宣告該引數時指定的第一個上限型別,這個型別引數也被稱為raw type(原始型別)。
import java.util.ArrayList;
import java.util.List;

public class RawTypeTest {
	public static void main(String[] args) {
		List list = new ArrayList();
		list.add("Java物件1");
		list.add("Java物件2");
		list.add("Java物件3");
		List<Integer> intList = list;
		for ( int i = 0 ; i < intList.size() ; i ++ ) {
			System.out.println(intList.get(i));
		}
	}
}
輸出結果為: Java物件1
Java物件2
Java物件3
       上面程式中先定義了一個不帶泛型資訊的List集合,其中所有的集合元素都是String型別。然後將List集合賦給List<Integer>變數,但是可以正常輸出intList集合的三個字串元素。        通過上面介紹可以看出,當程式把一個原始型別的變數賦給一個帶泛型資訊的變數時,只要它們的型別保持相容(例如:將List變數賦給List<integer>,無論List集合裡實際包含什麼型別的元素),系統都不會有任何問題。不過需要指出的是,當把一個原始型別的變數(如List變數)賦給帶泛型資訊的變數(如List<Integer>)時會有一個潛在的問題:JVM會把集合裡盛裝的所有元素都當做Integer來處理。上面程式遍歷List<Integer>集合時,只是簡單地輸出每個集合元素,並未涉及集合元素的型別,因此程式並沒有出現異常;否則,程式要麼在執行時出現ClassCastException異常,要麼在編譯時提示編譯錯誤。
import java.util.ArrayList;
import java.util.List;

public class RawTypeTest2 {
	public static void main(String[] args) {
		List list = new ArrayList();
		list.add("Java物件1");
		list.add("Java物件2");
		list.add("Java物件3");
		List<Integer> intList = list;
		for ( int i = 0 ; i < intList.size() ; i ++ ) {
			Integer i = intList.get(i);
			System.out.println(i);
		}
	}
}
       上面程式遍歷intList集合時,嘗試將每個集合元素都賦給Integer變數。由於intList集合的型別本身就是List<Integer>型別,因此編譯器會將每個集合元素都當成Integer處理。嘗試執行上面程式,將看到如下執行時異常。
Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
	Duplicate local variable i
       很明顯,intList所引用的集合裡包含的集合元素的型別是String,而不是Integer,因此程式在執行程式碼時將會引發ClassCastException異常。
import java.util.ArrayList;
import java.util.List;

public class RawTypeTest3 {
	public static void main(String[] args) {
		List list = new ArrayList();
		list.add("Java物件1");
		list.add("Java物件2");
		list.add("Java物件3");
		List<Integer> intList = list;
		for ( int i = 0 ; i < intList.size() ; i ++ ) {
			String str = intList.get(i);
			System.out.println(intList.get(i));
		}
	}
}
       嘗試編譯上面程式,將發現編譯器直接提示如下編譯資訊。對於程式中intList集合而言,它的型別是List<Integer>型別,因此編譯器會認為該集合的每個元素都是Integer型別,而上面程式嘗試將該集合元素賦給一個String型別的變數,因此會提示編譯錯誤。
Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
	Type mismatch: cannot convert from Integer to String
總結
  1. 當程式把一個原始型別的變數賦給一個帶泛型資訊的變數時,總是可以通過編譯,只是會提示一些警告資訊。
  2. 當程式試圖訪問帶有泛型宣告的集合的集合元素時,編譯器總是把集合元素當成泛型型別處理,並不關心集合裡元素的實際型別。
  3. 當程式試圖訪問帶有泛型宣告的集合的集合元素時,JVM會遍歷每個集合元素自動執行強制型別轉化,如果集合元素的實際型別與集合所帶的泛型資訊不匹配,執行時將引發ClassCastException異常。

4、2 原始型別的擦除

       當把一個具有泛型資訊的物件賦給另一個沒有泛型資訊的變數時,所有尖括號裡的型別資訊都將被拋棄。比如,將一個List<String>型別的物件轉型為List,則該List對集合元素的型別檢查變成了型別變數的上限(即Object)。
class Tree<T extends Number> {
	T size;
	
	public Tree(T size) {
		this.size = size;
	}

	public T getSize() {
		return size;
	}

	public void setSize(T size) {
		this.size = size;
	}
}

public class ErasureTest {
	public static void main(String[] args) {
		Tree<Integer> a = new Tree<Integer>(6);  //①
		Integer as = a.getSize();
		Tree b = a;
		Number size1 = b.getSize();
		//Integer size2 = b.getSize();  報錯:Type mismatch: cannot convert from Number to Integer  
	}
}
       上面程式裡定義了一個帶泛型宣告的Tree類,其型別形參的上限是Number,這個型別形參用來定義Tree類的size屬性。在①行程式碼建立了一個Tree<Integer>物件,所以呼叫a的getSize()方法時返回Integer型別的值。當把a賦給一個不帶泛型資訊的b變數時,編譯器就會丟失a物件的泛型資訊,但因為Tree型別形參的上限是Number類,所以編譯器依然知道b的getSize()方法返回Number型別,但具體是Number的哪個子類就不清楚了。        從上面程式可以看出,當把一個帶泛型資訊的Java物件賦給不帶泛型資訊的變數時,Java程式會發生擦除,這種擦除不僅會擦除使用該Java類時傳入的型別實參,而且會擦除所有泛型資訊,也就是擦除所有尖括號裡的資訊。
import java.util.ArrayList;
import java.util.List;

class AppleTree<T extends Number> {
	T size;
	
	public AppleTree() {}
	
	public AppleTree(T size) {
		this.size = size;
	}

	public T getSize() {
		return size;
	}

	public void setSize(T size) {
		this.size = size;
	}
	
	public List<String> getApple() {
		List<String> list = new ArrayList<>();
		for ( int i = 0 ; i < 3 ; i ++ ) {
			list.add(new AppleTree<Integer>(10 * i).toString());
		}
		return list;
	}
	
	public String toString() {
		return "AppleTree[size = " + size + "]";
	}
}

public class ErasureTest2 {
	public static void main(String[] args) {
		AppleTree<Integer> a = new AppleTree<Integer>(6);
		for ( String appleTree : a.getApple() ) {
			System.out.println(appleTree);
		}
		AppleTree b = a;
		for ( String appleTree : b.getApple() ) {
			System.out.println(appleTree);
		}
	}
}
       上面程式的main方法中先建立了一個AppleTree<Integer>物件,程式呼叫該方法的getApple()方法的返回值肯定是List<String>型別的值。將a變數賦給AppleTree型別的b變數時會發生擦除,該AppleTree<Integer>物件將丟失所有的泛型資訊,包括 getApple()方法的返回值型別List<Stirng>裡的尖括號資訊,因此最後會提示“Type mismatch: cannot convert from element type Object to String”。