程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 計算機程序的思維邏輯 (37),思維37

計算機程序的思維邏輯 (37),思維37

編輯:JAVA綜合教程

計算機程序的思維邏輯 (37),思維37


35節介紹了泛型的基本概念和原理,上節介紹了泛型中的通配符,本節來介紹泛型中的一些細節和局限性。

這些局限性主要與Java的實現機制有關,Java中,泛型是通過類型擦除來實現的,類型參數在編譯時會被替換為Object,運行時Java虛擬機不知道泛型這回事,這帶來了很多局限性,其中有的部分是比較容易理解的,有的則是非常違反直覺的。

一項技術,往往只有理解了其局限性,我們才算是真正理解了它,才能更好的應用它。

下面,我們將從以下幾個方面來介紹這些細節和局限性:

  • 使用泛型類、方法和接口
  • 定義泛型類、方法和接口
  • 泛型與數組

使用泛型類、方法和接口

在使用泛型類、方法和接口時,有一些值得注意的地方,比如:

  • 基本類型不能用於實例化類型參數
  • 運行時類型信息不適用於泛型
  • 類型擦除可能會引發一些沖突

我們逐個來看下。

基本類型不能用於實例化類型參數

Java中,因為類型參數會被替換為Object,所以Java泛型中不能使用基本數據類型,也就是說,類似下面寫法是不合法的:

Pair<int> minmax = new Pair<int>(1,100);

解決方法就是使用基本類型對應的包裝類。

運行時類型信息不適用於泛型

在介紹繼承的實現原理時,我們提到,在內存中,每個類都有一份類型信息,而每個對象也都保存著其對應類型信息的引用。關於運行時信息,後續文章我們會進一步詳細介紹,這裡簡要說明一下。

在Java中,這個類型信息也是一個對象,它的類型為Class,Class本身也是一個泛型類,每個類的類型對象可以通過<類名>.class的方式引用,比如String.class,Integer.class。

這個類型對象也可以通過對象的getClass()方法獲得,比如:

Class<?> cls = "hello".getClass();

這個類型對象只有一份,與泛型無關,所以Java不支持類似如下寫法:

Pair<Integer>.class

一個泛型對象的getClass方法的返回值與原始類型對象也是相同的,比如說,下面代碼的輸出都是true:

Pair<Integer> p1 = new Pair<Integer>(1,100);
Pair<String> p2 = new Pair<String>("hello","world");
System.out.println(Pair.class==p1.getClass());
System.out.println(Pair.class==p2.getClass());

在第16節,我們介紹過instanceof關鍵字,instanceof後面是接口或類名,instanceof是運行時判斷,也與泛型無關,所以,Java也不支持類似如下寫法:

if(p1 instanceof Pair<Integer>)

不過,Java支持這麼寫:

if(p1 instanceof Pair<?>)

類型擦除可能會引發一些沖突

由於類型擦除,可能會引發一些編譯沖突,這些沖突初看上去並不容易理解,我們通過一些例子看一下。

上節我們介紹過一個例子,有兩個類Base和Child,Base的聲明為:

class Base implements Comparable<Base>

Child的聲明為:

class Child extends Base

Child沒有專門實現Comparable接口,上節我們說Base類已經有了比較所需的全部信息,所以Child沒有必要實現,可是如果Child希望自定義這個比較方法呢?直覺上,可以這樣修改Child類:

class Child extends Base implements Comparable<Child>{
    @Override
    public int compareTo(Child o) {
        
    }
    //...
}

遺憾的是,Java編譯器會提示錯誤,Comparable接口不能被實現兩次,且兩次實現的類型參數還不同,一次是Comparable<Base>,一次是Comparable<Child>。為什麼不允許呢?因為類型擦除後,實際上只能有一個。

那Child有什麼辦法修改比較方法呢?只能是重寫Base類的實現,如下所示:

class Child extends Base {
    @Override
    public int compareTo(Base o) {
        if(!(o instanceof Child)){
            throw new IllegalArgumentException();
        }
        Child c = (Child)o;
        //...
        return 0;
    }
    //...
}

還有,你可能認為可以這麼定義重載方法:

public static void test(DynamicArray<Integer> intArr)
public static void test(DynamicArray<String> strArr)

雖然參數都是DynamicArray,但實例化類型不同,一個是DynamicArray<Integer>,另一個是DynamicArray<String>,同樣,遺憾的是,Java不允許這種寫法,理由同樣是,類型擦除後,它們的聲明是一樣的。

定義泛型類、方法和接口

在定義泛型類、方法和接口時,也有一些需要注意的地方,比如:

  • 不能通過類型參數創建對象
  • 泛型類類型參數不能用於靜態變量和方法
  • 了解多個類型限定的語法 

我們逐個來看下。

不能通過類型參數創建對象

不能通過類型參數創建對象,比如,T是類型參數,下面寫法都是非法的:

T elm = new T();
T[] arr = new T[10];

為什麼非法呢?因為如果允許,那你以為創建的就是對應類型的對象,但由於類型擦除,Java只能創建Object類型的對象,而無法創建T類型的對象,容易引起誤解,所以Java干脆禁止這麼做。

那如果確實希望根據類型創建對象呢?需要設計API接受類型對象,即Class對象,並使用Java中的反射機制,後續文章我們再詳細介紹反射,這裡簡要說明一下,如果類型有默認構造方法,可以調用Class的newInstance方法構建對象,類似這樣:

public static <T> T create(Class<T> type){
    try {
        return type.newInstance();
    } catch (Exception e) {
        return null;
    }
}

比如:

Date date = create(Date.class);
StringBuilder sb = create(StringBuilder.class);

泛型類類型參數不能用於靜態變量和方法

對於泛型類聲明的類型參數,可以在實例變量和方法中使用,但在靜態變量和靜態方法中是不能使用的。類似下面這種寫法是非法的:

public class Singleton<T> {

    private static T instance;
    
    public synchronized static T getInstance(){
        if(instance==null){
             // 創建實例
        }
        return instance;
    }
}    

如果合法的話,那麼對於每種實例化類型,都需要有一個對應的靜態變量和方法。但由於類型擦除,Singleton類型只有一份,靜態變量和方法都是類型的屬性,且與類型參數無關,所以不能使用泛型類類型參數。

不過,對於靜態方法,它可以是泛型方法,可以聲明自己的類型參數,這個參數與泛型類的類型參數是沒有關系的。

了解多個類型限定的語法

之前介紹類型參數限定的時候,我們介紹,上界可以為某個類、某個接口或者其他類型參數,但上界都是只有一個,Java中還支持多個上界,多個上界之間以&分隔,類似這樣:

T extends Base & Comparable & Serializable

Base為上界類,Comparable和Serializable為上界接口,如果有上界類,類應該放在第一個,類型擦除時,會用第一個上界替換。

泛型與數組

泛型與數組的關系稍微復雜一些,我們單獨討論一下。

為什麼不能創建泛型數組?

引入泛型後,一個令人驚訝的事實是,你不能創建泛型數組。比如說,我們可能想這樣創建一個Pair的泛型數組,以表示隨機一節中介紹的獎勵面額和權重。

Pair<Object,Integer>[] options = new Pair<Object,Integer>[]{
        new Pair("1元",7),
        new Pair("2元", 2),
        new Pair("10元", 1)
};

Java會提示編譯錯誤,不能創建泛型數組。這是為什麼呢?我們先來進一步理解一下數組。

前面我們解釋過,類型參數之間有繼承關系的容器之間是沒有關系的,比如,一個DynamicArray<Integer>對象不能賦值給一個DynamicArray<Number>變量。不過,數組是可以的,看代碼:

Integer[] ints = new Integer[10];
Number[] numbers = ints;
Object[] objs = ints;

後面兩種賦值都是允許的。數組為什麼可以呢?數組是Java直接支持的概念,它知道數組元素的實際類型,它知道Object和Number都是Integer的父類型,所以這個操作是允許的。

雖然Java允許這種轉換,但如果使用不當,可能會引起運行時異常,比如:

Integer[] ints = new Integer[10];
Object[] objs = ints;
objs[0] = "hello";

編譯是沒有問題的,運行時會拋出ArrayStoreException,因為Java知道實際的類型是Integer,所以寫入String會拋出異常。

理解了數組的這個行為,我們再來看泛型數組。如果Java允許創建泛型數組,則會發生非常嚴重的問題,我們看看具體會發生什麼:

Pair<Object,Integer>[] options = new Pair<Object,Integer>[3];
Object[] objs = options;
objs[0] = new Pair<Double,String>(12.34,"hello");

如果可以創建泛型數組options,那它就可以賦值給其他類型的數組objs,而最後一行明顯錯誤的賦值操作,則既不會引起編譯錯誤,也不會觸發運行時異常,因為Pair<Double,String>的運行時類型是Pair,和objs的運行時類型Pair[]是匹配的。但我們知道,它的實際類型是不匹配的,在程序的其他地方,當把objs[0]當做Pair<Object,Integer>進行處理的時候,一定會觸發異常。

也就是說,如果允許創建泛型數組,那就可能會有上面這種錯誤操作,它既不會引起編譯錯誤,也不會立即觸發運行時異常,卻相當於埋下了一顆炸彈,不定什麼時候爆發,為避免這種情況,Java干脆就禁止創建泛型數組。

如何存放泛型對象?

但,現實需要能夠存放泛型對象的容器啊,怎麼辦呢?可以使用原始類型的數組,比如:

Pair[] options = new Pair[]{
      new Pair<String,Integer>("1元",7),
      new Pair<String,Integer>("2元", 2),
      new Pair<String,Integer>("10元", 1)};

更好的選擇是,使用後續章節介紹的泛型容器。目前,可以使用我們自己實現的DynamicArray,比如:

DynamicArray<Pair<String,Integer>> options = new DynamicArray<>();
options.add(new Pair<String,Integer>("1元",7));
options.add(new Pair<String,Integer>("2元",2));
options.add(new Pair<String,Integer>("10元",1));

DynamicArray內部的數組為Object類型,一些操作插入了強制類型轉換,外部接口是類型安全的,對數組的訪問都是內部代碼,可以避免誤用和類型異常。

如何轉換容器為數組?

有時,我們希望轉換泛型容器為一個數組,比如說,對於DynamicArray,我們可能希望它有這麼一個方法:

public E[] toArray()

而我們希望可以這麼用:

DynamicArray<Integer> ints = new DynamicArray<Integer>();
ints.add(100);
ints.add(34);
Integer[] arr = ints.toArray();

先使用動態容器收集一些數據,然後轉換為一個固定數組,這也是一個常見合理的需求,怎麼來實現這個toArray方法呢?

可能想先這樣:

E[] arr = new E[size];

遺憾的是,如之前所述,這是不合法的。Java運行時根本不知道E是什麼,也就無法做到創建E類型的數組。

另一種想法是這樣:

public E[] toArray(){
    Object[] copy = new Object[size];
    System.arraycopy(elementData, 0, copy, 0, size);
    return (E[])copy;
}

或者使用之前介紹的Arrays方法:

public E[] toArray(){
    return (E[])Arrays.copyOf(elementData, size);
}

結果都是一樣的,沒有編譯錯誤了,但運行時,會拋出ClassCastException異常,原因是,Object類型的數組不能轉換為Integer類型的數組。

那怎麼辦呢?可以利用Java中的運行時類型信息和反射機制,這些概念我們後續章節再介紹。這裡,我們簡要介紹下。

Java必須在運行時知道你要轉換成的數組類型,類型可以作為參數傳遞給toArray方法,比如:

public E[] toArray(Class<E> type){
    Object copy = Array.newInstance(type, size);
    System.arraycopy(elementData, 0, copy, 0, size);
    return (E[])copy;
}

Class<E>表示要轉換成的數組類型信息,有了這個類型信息,Array類的newInstance方法就可以創建出真正類型的數組對象。

調用toArray方法時,需要傳遞需要的類型,比如,可以這樣:

Integer[] arr = ints.toArray(Integer.class);

泛型與數組小結

我們來稍微總結下泛型與數組的關系:

  • Java不支持創建泛型數組。
  • 如果要存放泛型對象,可以使用原始類型的數組,或者使用泛型容器。
  • 泛型容器內部使用Object數組,如果要轉換泛型容器為對應類型的數組,需要使用反射。

小結

本節介紹了泛型的一些細節和局限性,這些局限性主要是由於Java泛型的實現機制引起的,這些局限性包括,不能使用基本類型,沒有運行時類型信息,類型擦除會引發一些沖突,不能通過類型參數創建對象,不能用於靜態變量等,我們還單獨討論了泛型與數組的關系。

我們需要理解這些局限性,但,幸運的是,一般並不需要特別去記憶,因為用錯的時候,Java開發環境和編譯器會提示你,當被提示時,你需要能夠理解,並可以從容應對。

至此,關於泛型的介紹就結束了,泛型是Java容器類的基礎,理解了泛型,接下來,就讓我們開始探索Java中的容器類。

 

----------------

未完待續,查看最新文章,敬請關注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術的本質。用心寫作,原創文章,保留所有版權。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved