邊界處的動作
- 正是因爲有了擦除,我們發現泛型最令人困惑的方面源自這樣一個事實,即可以表示沒有任何意義的事物。
public class ArrayMaker<T> {
private Class<T> tClass;
public ArrayMaker(Class<T> tClass) {
this.tClass = tClass;
}
public T[] create(int size){
return (T[])Array.newInstance(tClass,size);
}
public static void main(String[] args) {
ArrayMaker<String> arrayMaker=new ArrayMaker<>(String.class);
String[] strings = arrayMaker.create(9);
System.out.println(Arrays.toString(strings));
}
}
//運行結果爲
[null, null, null, null, null, null, null, null, null]
- 即使 tClass 被存儲爲 Class<T> ,擦除也意味着它實際將被存儲爲 Class,沒有任何參數。因此, 當你正在使用它時,例如在創建數組 Array.newInstance() 實際上並未擁有 tclass所蘊含的類型信息,因此這不會產生具體的結果,所以必須轉型,這將產生一條令你無法滿意的警告。
- 注意, 對於在泛型中創建數組,使用Array.newInstance()是推薦的方式。
- 如果我們要創建一個容器而不是數組,情況就有些不同了:
public class ArrayMaker1<T> {
List<T> create(){
return new ArrayList<T>();
}
public static void main(String[] args) {
ArrayMaker1<String> maker1 = new ArrayMaker1<>();
List<String> strings = maker1.create();
System.out.println(strings);
}
}
//運行結果
[]
- 編譯器不會給出任何警告,儘管我們(從擦除中)知道在 create() 內部的 new ArrayList<T> 中的 <T> 被移除了 ___在運行時,這個類的內部沒有任何<T> ,因此這看起來毫無意義。但是如果你遵從這種思路,並將這個表達式改爲 new ArrayList() , 編譯器就會給出警告。
- 在本例中,這是否真的毫無意義呢? 如果返回list之前,將某些東西放入其中,就像下面這樣,情況又會如何呢?
public class ArrayMark2<T> {
List<T> create(T t,int size){
List<T> list=new ArrayList<>();
for (int i = 0; i < size; i++) {
list.add(t);
}
return list;
}
public static void main(String[] args) {
ArrayMark2<String> arrayMark2=new ArrayMark2<>();
List<String> list = arrayMark2.create("hello", 3);
System.out.println(list);
}
}
//運行結果爲
[hello, hello, hello]
- 即使編譯器無法知道有關 create() 中的 T 的任何信息, 但是它仍舊可以在編譯期確保你放置到 list 中的對象具有 T 類型,使其適合 ArrayList<T> 。
- 因此, 即使擦除在方法或類內部移除了有關實際類型的信息,編譯器仍舊可以確保在方法或類中使用的類型的內部一致性。
- 因爲擦除在方法體中移除了類型信息,所以在運行時的問題就是邊界: 即對象進入和離開的地點。這些正是編譯器在編譯器執行類型檢查並插入轉型代碼的地點。
public class SimpleHolder {
private Object obj;
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
public static void main(String[] args) {
SimpleHolder holder = new SimpleHolder();
holder.setObj("Item");
String obj = (String) holder.getObj();
}
}
- 用 javap -c 反編譯 SimpleHolder 類,得到如下內容
public java.lang.Object getObj();
Code:
0: aload_0
1: getfield #2 // Field obj:Ljava/lang/Object;
4: areturn
public void setObj(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field obj:Ljava/lang/Object;
5: return
public static void main(java.lang.String[]);
Code:
0: new #3 // class generic/SimpleHolder
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String Item
11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #7 // Method getObj:()Ljava/lang/Object;
18: checkcast #8 // class java/lang/String
21: astore_2
22: return
- setObj() 和 getObj() 方法將直接存儲和產生值,而轉型是在調用 getObj() 的時候接受檢查的。
- 現在將泛型合併到上面的代碼中:
public class SimpleHolder <T>{
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
public static void main(String[] args) {
SimpleHolder<String> holder = new SimpleHolder<String>();
holder.setObj("Item");
String obj = holder.getObj();
}
}
//從 getObj() 返回之後的轉型消失了, 但是我們還知道傳遞給setObj() 的值在編譯器會接受檢查,下面是相關字節碼。
public T getObj();
Code:
0: aload_0
1: getfield #2 // Field obj:Ljava/lang/Object;
4: areturn
public void setObj(T);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field obj:Ljava/lang/Object;
5: return
public static void main(java.lang.String[]);
Code:
0: new #3 // class generic/GenericHolder
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String item
11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #7 // Method getObj:()Ljava/lang/Object;
18: checkcast #8 // class java/lang/String
21: astore_2
22: return
- 所產生的的字節碼是相同的。對進入setObj() 的類型檢查是不需要的,因爲這將由編譯器執行。
- 而對從 getObj() 返回的值進行轉型仍舊是需要的,但這與你自己必須執行的操作是一樣的___此處它將由編譯器自動插入,因此你寫入和讀取的代碼的噪聲將更小。
- 由於所產生的 getObj() 和 setObj() 的字節碼相同,所以在泛型中的所有動作都發生在邊界處___對傳遞進來的值進行額外的編譯期檢查,並插入對傳遞出去值的轉型。這有助於澄清對擦除的混淆,記住: 邊界就是發生動作的地方。
擦除的補償
- 正如我們看到的,擦除丟失了在泛型代碼中執行某些操作的能力。任何在運行時需要知道確切類型信息的操作都將無法工作。
public class Erased<T> {
private final int SIZE=100;
static void f(Object obj){
//下面這段代碼是錯誤的
if (obj instanceof T){
T t=new T();
T [] array=new T[SIZE];
T[] arrays=(T)Object[SIZE]; //未經檢查的警告
}
}
}
- 偶爾可以繞過這些問題來編程,但是有時必須通過引入類型標籤來對擦除進行補償。這意味着你需要顯式地傳遞你的類型的 Class 對象,以便你可以在類型的表達式中使用它。
- 例如,在前面實例中對使用 instanceof 的嘗試最終失敗了,因爲其類型信息已經被擦除了。如果引入類型標籤,就可以轉而使用動態的 isInstance();
class Building{}
class House extends Building{}
public class Erased<T> {
Class<T> tClass;
public Erased(Class<T> tClass) {
this.tClass = tClass;
}
boolean f(Object obj){
return tClass.isInstance(obj);
}
public static void main(String[] args) {
//1, 創建泛型 爲 Building類
Erased<Building> erased=new Erased<>(Building.class);
System.out.println(erased.f(new House()));
System.out.println(erased.f(new Building()));
//2,創建泛型爲 House類
Erased<House> erased1=new Erased<>(House.class);
System.out.println(erased1.f(new Building()));
System.out.println(erased1.f(new House()));
}
}
//運行結果爲
true
true
false
true
- 編譯器將確保類型標籤可以匹配泛型參數。
創建類型實例
- 在 Erased.java(編譯錯誤的版本中) 中對創建一個 new T() 的嘗試講將無法實現,部分原因是因爲擦除,而另一部分原因就是因爲編譯器不能驗證T具有默認(無參)構造器。
- Java中的解決方案是傳遞一個工廠對象,並用它來創建新的實例。最便利的工廠對象就是Class 對象,因此如果使用類型標籤,那麼你就可以使用 newInstance() 來創建類型的新對象。
public class ClassAsFactory<T> {
T t;
public ClassAsFactory(Class<T> tClass) {
try {
t = tClass.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
class Employee{}
class InstantiateGenericType{
public static void main(String[] args) {
ClassAsFactory<Employee> employeeClassAsFactory=new ClassAsFactory<>(Employee.class);
System.out.println("ClassAsFactory<Employee> succeeded");
try {
ClassAsFactory<Integer> classAsFactory=new ClassAsFactory<>(Integer.class);
}catch (Exception e){
System.out.println(" ClassAsFactory<Integer> failed");
}
}
}
//運行結果爲
ClassAsFactory<Employee> succeeded
ClassAsFactory<Integer> failed
- 這可以編譯,但是會因 ClassAsFactory<Integer> 而失敗,因爲Integer 沒有任何默認的構造器。因爲這個錯誤不是在編譯期間捕獲的,所以Sun 的夥計們對這種方式並不贊成,他們建議使用顯式的工廠,並將限制其類型,使得只能接受實現了這個工廠的類。
interface Factory<T>{
T create();
}
//創建對象工廠
class Foo2<T>{
private T t;
public <F extends Factory<T>> Foo2(F factory){
t=factory.create();
}
}
class IntegerFactory implements Factory<Integer>{
@Override
public Integer create() {
return new Integer(0);
}
}
class Widget{
static class TestFactory implements Factory<Widget>{
@Override
public Widget create() {
return new Widget();
}
}
}
public class FactoryConstraint {
public static void main(String[] args) {
new Foo2<>(new IntegerFactory());
new Foo2<>(new Widget.TestFactory());
}
}
- 注意,這確實只是傳遞 Class<T> 的一種變體。兩種方式都傳遞了工廠對象,Class<T> 碰巧是內建的工廠對象,而上面的方式創建了一個顯式的工廠對象,但是你卻獲得了編譯期檢查。
- 另一種方式是模板方法設計模式。在下面的示例中, get() 是模板方法,而create() 在子類中定義的,用來生成子類類型的對象。
public abstract class GenericWithCreate <T>{
final T element;
GenericWithCreate() {
this.element = create();
}
abstract T create();
}
class X{}
class Creator extends GenericWithCreate<X>{
@Override
X create() {
return new X();
}
void f(){
System.out.println(element.getClass().getSimpleName());
}
}
class CreatorGeneric{
public static void main(String[] args) {
Creator creator = new Creator();
creator.f();
}
}
//運行結果爲
X
泛型數組
- 正如你在 Erased.java中所見(Erased 類中出現錯誤的版本),不能創建泛型數組。一般的解決方案是在任何要創建泛型數組的地方都使用 ArrayList。
public class ListOfGenerics<T> {
private List<T> lists=new ArrayList<>();
void add(T t){
lists.add(t);
}
T get(int index){
return lists.get(index);
}
}
- 這裏你將獲取數組的行爲,以及由泛型提供的編譯器的類型安全。
- 有時,你仍舊希望創建泛型類型的數組(例如,ArrayList 內部使用的是數組)。有趣的是,可以按照編譯器喜歡的方式來定義一個引用,例如
class Generic<T>{}
class ArrayOfGenericReference{
static Generic<Integer> [] gia;
}
- 編譯器將接受這個程序,而不會產生任何警告。但是,永遠都不能創建這個確切類型的數組(包括類型參數),因此這有一點令人困惑。既然所有數組無論它們持有的類型如何,都具有相同的結構(每個數組槽位的尺寸和數組的佈局),那麼看起來你應該能夠創建一個 Object 數組,並將其轉型爲所希望的數組類型。事實上這可以編譯,但是不能運行,它將產ClassCaseException。
public class ArrayOfGeneric {
static final int SIZE = 100;
static Generic<Integer>[] gia;
public static void main(String[] args) {
//compiles produces classCastException
//編譯產生 classCastException
//gia=(Generic<Integer>[])new Object[SIZE];
//運行時類型是原始(擦除)類型
gia = (Generic<Integer>[]) new Generic[SIZE];
System.out.println(gia.getClass().getSimpleName());
gia[0] = new Generic<Integer>();
//編譯時錯誤
// gia[1]=new Object();
//在編譯時發現類型不匹配
//gia[2]=new Generic<Double>();
}
}
//運行結果爲
Generic[]
- 問題在意數組將跟蹤它們的實際類型,而這個類型是在數組被創建時確定的,因此,即使 gia 已經被轉型爲 Generic<Integer>[] ,而這個信息只存於編譯期。
- 在運行時,它仍舊是Object數組,而這將引發問題。成功創建泛型數組的唯一方式就是創建一個被擦除類型的新數組,然後對其轉型。
public class GenericArray<T> {
private T [] array;
public GenericArray(int size) {
array= (T[]) new Object[size];
}
public void put(int index,T item){
array[index] = item;
}
public T get(int index){
return array[index];
}
public T[] rep(){
return array;
}
public static void main(String[] args) {
GenericArray<Integer> genericArray =new GenericArray<>(5);
//ClassCastException
//Integer [] integers=genericArray.rep();
//下面這個是沒問題的
java.lang.Object [] objects=genericArray.rep();
}
}
- 與前面相同,我們並不能聲明 T[] array=new T[size],因此我們創建了一個對象數組,然後對其轉型。
- rep() 方法將返回 T[] ,它在 main() 中將用於 genericArray ,因此如果調用它,並嘗試着將結果作爲 Integer [] 引用來捕獲,就會得到 ClassCastException ,這還是因爲實際的運行時類型是 Object[]。
- 因爲有了擦除,數組在運行時類型就只能是 Object[] ,如果我們立即將其轉型爲 T[] ,那麼在編譯期該數組的實際類型就將丟失,而編譯器可能會錯過某些潛在的錯誤檢查。
- 正因爲這樣,最好是在集合內部使用 Object[] ,然後當你使用數組元素時,添加一個對T 的轉型。讓我們看看這是如何運作的如下。
public class GenericArray2<T> {
private Object[] array;
public GenericArray2(int size) {
array=new Object[size];
}
public void put(int index,T item){
array[index] =item;
}
public T get(int index){
return (T) array[index];
}
public T[] rep(){
return (T[]) array;
}
public static void main(String[] args) {
GenericArray2<Integer> integerGenericArray2=new GenericArray2<>(5);
for (int i = 0; i < 5; i++) {
integerGenericArray2.put(i,i);
}
for (int i = 0; i < 5; i++) {
Integer integer = integerGenericArray2.get(i);
System.out.println(integer);
}
try {
Integer[] integers = integerGenericArray2.rep();
}catch (Exception e){
System.out.println("異常了");
}
}
}
//運行結果爲
0
1
2
3
4
異常了
- 初看起來,這好像沒有多大變化,只是轉型挪了地方。但是現在的內部是Object[] 而不是 T[] ,當get() 方法被調用時,它將對象轉型爲 T ,這實際上是正確的類型,因此這是安全的。
- 然而,如果你調用 rep() ,它還是嘗試着將 Object[] 轉型爲 T[] ,這仍舊是不正確的,將在編譯器產生警告,在運行時產生異常。因此,沒有任何方式可以推翻底層的數據類型,它只能是 Object[] 。在內部將 array 當做Object[] 而不是T [] 處理的優勢是: 我們不太可能忘記這個數組的運行時類型,從而以外地引入缺陷(儘管大多數也可能是所有這類缺陷都可以在運行時快速地探測到)。
- 對於新代碼,應該傳遞一個類型標記。在這種情況下, GenericArray 看起來會像下面這樣。
public class GenericArrayWithTypeToken<T> {
private T[] array;
public GenericArrayWithTypeToken(Class<T> tClass,int size){
array= (T[]) Array.newInstance(tClass,size);
}
public void put(int index,T item){
array[index]=item;
}
public T get(int index){
return array[index];
}
public T[] rep(){
return array;
}
public static void main(String[] args) {
GenericArrayWithTypeToken<Integer> integers=new GenericArrayWithTypeToken<>(Integer.class,5);
Integer[] rep = integers.rep();
//這個是沒有錯誤的
}
}
- 類型Class<T> 被傳遞到構造器中,以便從擦除中恢復,使得我們可以創建需要的實際類型的數組。
- 一旦我們獲得了實際類型,就可以返回它,並獲得想要的結果,就像在 main() 中看到的那樣。該數組的運行時類型是確切類型T []。