1. 程式人生 > >Java 之路 (十六) -- 泛型下(萬用字元、型別擦除、泛型的限制)

Java 之路 (十六) -- 泛型下(萬用字元、型別擦除、泛型的限制)

7. 萬用字元

萬用字元,即 “?”,用來表示未知型別。

萬用字元可用作各種情況:作為引數,欄位或區域性變數的型別;有時也作為返回型別;萬用字元從不用作泛型方法呼叫、泛型類例項建立或超型別的型別引數。

7.1 上限有界的萬用字元

使用上限萬用字元來放寬對變數的限制。

宣告上限萬用字元的語法:<? extends 上限>

舉個例子:

public static double sumOfList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list)
        s += n.doubleValue();
    return
s; }

這裡 上線萬用字元為

7.2 無界萬用字元

使用萬用字元 “?” 指定無界萬用字元型別,例如 List<?>,稱為未知型別的列表。

無界萬用字元有兩種適用場景:

  1. 當前正在編寫可以藉由 Object 類中的方法來實現的方法。
  2. 使用泛型類中不依賴於型別引數的方法時,如 List.size()、list.clear() 等;實際上,經常使用 Class
public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + " "
); System.out.println(); }

如果只是希望列印 Object 列表,那這個方法可行;但是如果目標是列印任何型別的列表,那這個方法就不行了,它無法輸出 List<Integer>List<Double> 等等,因為這些都不是 List<Object> 的子型別。

解決上述問題,就需要使用 List

public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " "
); System.out.println(); }

這樣,對於任何具體型別 A,List<A> 都是 List<?> 的子型別,於是可以用該方法列印任何型別的列表。

7.3 下限有界萬用字元

上限有界萬用字元將未知型別限制為該型別的特定型別或子型別,並使用 extends 關鍵字表示;類似的,下限有界萬用字元將位置型別限制為該型別的特定型別或超型別;

下限有界萬用字元的語法:<? super 下限>

注意:上限有界 和 下限有界 不能同時指定。

舉個例子:

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

在這個例子中,下限有界萬用字元是

7.4 萬用字元和子型別

在 5.1 泛型類和子型別 小節中,我們講到泛型類或介面並不僅僅因為它們的型別之間存在關係而相互關聯。

比如 Integer 是 Number 的子型別,但是 List<Integer>List<Number> 卻沒什麼關係,二者不過是有一個公共符類 List

List<? extends Integer> intList = new ArrayList<>();
List<? extends Number>  numList = intList;  // OK. List<? extends Integer> is a subtype of List<? extends Number>

因為 Integer 是 Number 的子型別,並且 numList 是一個 Number 物件的列表,所以現在 intList 和 numList 之間存在關聯了。下圖展示了使用上限和下限有界萬用字元宣告的多個 List 類之間的關係:(箭頭指向的是父型別)

7.5 萬用字元捕獲和幫助方法

某些情況下,編譯期會推斷出萬用字元的型別。例如,列表定義為 List

import java.util.List;

public class WildcardError {

    void foo(List<?> i) {
        i.set(0, i.get(0));
    }
}

此例中,編譯期將輸入引數 i 處理為 Object 型別,當 foo 方法呼叫 list.set(int,E) 時,編譯期無法確認插入到列表中的物件的型別,於是產生錯誤。發生此類錯誤時,通常意味著編譯器認為你為變數分配了錯誤的型別。也是出於這個原因,Java 新增泛型機制用來保證編譯時型別安全。

那麼發生錯誤時,如果解決編譯器錯誤呢?通常通過編寫捕獲萬用字元的私有的 Helper 方法,如下所示:

public class WildcardFixed {

    void foo(List<?> i) {
        fooHelper(i);
    }

    // Helper method created so that the wildcard can be captured
    // through type inference.
    private <T> void fooHelper(List<T> l) {
        l.set(0, l.get(0));
    }

}

由於輔助方法,編譯器使用推斷來確定 T 是呼叫中的捕獲的變數

7.6 萬用字元使用指南

實際開發中,通常關於何時使用上限有界萬用字元以及何時使用下限有界萬用字元存在很大疑惑。本節就介紹設計程式碼時要遵循的一些原則。

首先假設兩種變數:

  1. “in” 變數:向程式碼提供資料。
  2. “out” 變數:儲存資料供其他地方使用。

萬用字元指南

  1. 用上限萬用字元定義“in”變數。(使用extends關鍵字)
  2. 用下限萬用字元定義“out”變數。(使用super關鍵字)
  3. 在可以使用Object類中定義的方法訪問“in”變數的情況下,使用無界萬用字元。
  4. 在程式碼需要通過“in”和“out”變數訪問其他變數的情況下,不要使用萬用字元。

避免使用萬用字元作為返回型別。

8. 型別擦除

Java 中並沒有實現真正的泛型。為了實現泛型,Java 編譯器將型別擦除應用於:

  1. 如果型別引數是無界的,則將泛型型別中的所有型別引數替換為其邊界或物件 。 因此,生成的位元組碼僅包含普通的類,介面和方法。
  2. 如有必要,插入型別強制轉換以保持型別安全。
  3. 生成橋接方法以保留擴充套件泛型型別中的多型性。

8.1 泛型類的擦除

在型別擦除過程中,Java 編譯器將擦除所有型別引數,並在型別引數有界時將其替換為第一個邊界;如果型別引數無界,則替換為 Object。

如下:

public class Node<T> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

因為 T 無界,所以Java 編譯器會用 Object 替換它:

public class Node {

    private Object data;
    private Node next;

    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Object getData() { return data; }
    // ...
}

再舉個上限有界萬用字元的例子:

public class Node<T extends Comparable<T>> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

Java 編譯器會用第一個邊界 Comparable 替換有界型別引數 T:

public class Node {

    private Comparable data;
    private Node next;

    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Comparable getData() { return data; }
    // ...
}

8.2 泛型方法的擦除

Java 編譯器還會擦除泛型方法形參中的型別引數

1.舉一個無界型別引數的例子

public static <T> int count(T[] anArray, T elem) {
    int cnt = 0;
    for (T e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

由於T無界,因此 Java 編譯器會用 Object 替換之:

public static int count(Object[] anArray, Object elem) {
    int cnt = 0;
    for (Object e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

2.再舉一個有界型別引數的例子:

class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }

public static <T extends Shape> void draw(T shape) { /* ... */ }

上面編寫了一個繪製不同形狀的泛型方法,Java 編譯器會將 T 替換為 Shape:

public static void draw(Shape shape) { /* ... */ }

8.3 不可具體化型別

8.3.1 什麼是不可具體化型別

可具體化型別是指:型別資訊在執行時完全可用,這v澳闊基元、非泛型類、原生型別和為繫結萬用字元的引用。

不可具體化的型別是指:執行時沒有提供所有資訊,會通過型別擦除刪除部分編譯時的資訊。不可具體化型別的示例是 List<String>List<Number>,執行時 JVM 無法區分這兩個型別,它們都會程式設計 List 原生型別。

8.3.2 堆汙染

當引數化型別的變數引用不是該引數化型別的物件時,會發生堆汙染 。 如果程式執行某些操作,在編譯時產生未經檢查的警告,則會出現這種情況。 如果在編譯時(在編譯時型別檢查規則的限制內)或在執行時,生成涉及引數化型別(例如,強制轉換或方法呼叫)的操作的正確性,則會生成未經檢查的警告。驗證。 例如,在混合原始型別和引數化型別時,或者在執行未經檢查的強制轉換時,會發生堆汙染。

在正常情況下,當所有程式碼同時編譯時,編譯器會發出未經檢查的警告,以引起您對潛在堆汙染的注意。 如果單獨編譯程式碼的各個部分,則很難檢測到堆汙染的潛在風險。 如果確保程式碼在沒有警告的情況下編譯,則不會發生堆汙染。

8.3.3 可變引數的潛在漏洞

帶有可變引數的泛型方法可能導致堆汙染。

考慮以下例子:

public class ArrayBuilder {

  public static <T> void addToList (List<T> listArg, T... elements) {
    for (T x : elements) {
      listArg.add(x);
    }
  }

  public static void faultyMethod(List<String>... l) {
    Object[] objectArray = l;     // Valid
    objectArray[0] = Arrays.asList(42);
    String s = l[0].get(0);       // ClassCastException thrown here
  }

}

下面的類使用 ArrayBuilder:

public class HeapPollutionExample {

  public static void main(String[] args) {

    List<String> stringListA = new ArrayList<String>();
    List<String> stringListB = new ArrayList<String>();

    ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");
    ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve");
    List<List<String>> listOfStringLists =
      new ArrayList<List<String>>();
    ArrayBuilder.addToList(listOfStringLists,
      stringListA, stringListB);

    ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
  }
}

當編譯器遇到可變引數的方法時,它會將可變引數轉換為陣列。 但是,Java程式語言不允許建立引數化型別的陣列。在方法ArrayBuilder.addToList ,編譯器將可變引數T... elements轉換為形式引數T[] elements ,即陣列。 但是,由於型別擦除,編譯器會將可變引數轉換為Object[] elements 。 因此,存在堆汙染的可能性。

9. 泛型的限制

  1. 無法使用基本資料型別例項化泛型類

    class Pair<K, V> {
    
       private K key;
       private V value;
    
       public Pair(K key, V value) {
           this.key = key;
           this.value = value;
       }
    
       // ...
    }
    
    

    呼叫以下語句會報錯:

    Pair<int, char> p = new Pair<>(8, 'a');  // compile-time error

    應該如下呼叫:

    Pair<Integer, Character> p = new Pair<>(8, 'a');

    傳入的基本型別引數如8 會自動裝箱。

  2. *無法建立型別引數的例項

    //以下程式碼錯誤 
    public static <E> void append(List<E> list) {
       E elem = new E();  // compile-time error
       list.add(elem);
    }
    
    //解決方法:反射
    public static <E> void append(List<E> list, Class<E> cls) throws Exception {
       E elem = cls.newInstance();   // OK
       list.add(elem);
    }
    
    
    //通過以下方式呼叫 append方法
    List <String> ls = new ArrayList <>();
    append(ls,String.class);
  3. 無法宣告型別為型別引數的靜態欄位

    //不允許將 靜態欄位型別設定為型別引數的型別
    public class MobileDevice<T> {
       private static T os;
    
       // ...
    }
  4. 無法強制轉換或使用 instanceof

    1. 因為Java編譯器會擦除通用程式碼中的所有型別引數,所以無法驗證在執行時使用泛型型別的引數化型別。

    2. 特殊的,可以通過無界萬用字元驗證是否屬於某個基型別

      public static void rtti(List<?> list) {
        if (list instanceof ArrayList<?>) {  // OK; instanceof requires a reifiable type
            // ...
        }
      }
    3. 更特殊的,某些情況下,編譯器知道型別引數始終有效並允許強制轉換:

      List<String> l1 = ...;
      ArrayList<String> l2 = (ArrayList<String>)l1;  // OK
  5. 無法建立引數化型別的陣列

  6. 無法直接或間接擴充套件 Throwable 類,無法捕獲型別引數的例項

    // Extends Throwable indirectly
    class MathException<T> extends Exception { /* ... */ }    // compile-time error
    
    // Extends Throwable directly
    class QueueFullException<T> extends Throwable { /* ... */ // compile-time error
    
       public static <T extends Exception, J> void execute(List<J> jobs) {
       try {
           for (J job : jobs)
               // ...
       } catch (T e) {   // compile-time error
           // ...
       }
    }
  7. 型別擦除到原生型別的方法無法過載

    public class Example {
       public void print(Set<String> strSet) { }
       public void print(Set<Integer> intSet) { }
    }

    以上方法在型別擦除後具有相同的簽名,會在編譯時報錯。

相關推薦

Java () -- 字元型別限制

7. 萬用字元 萬用字元,即 “?”,用來表示未知型別。 萬用字元可用作各種情況:作為引數,欄位或區域性變數的型別;有時也作為返回型別;萬用字元從不用作泛型方法呼叫、泛型類例項建立或超型別的型別引數。 7.1 上限有界的萬用字元 使用上限萬用字元來放

Java (五) -- 方法有界型別引數與繼承型別推斷

Thinking in Java 中關於泛型的講解篇幅實在過長,前後嘗試閱讀這一章,但總是覺得找不到要點,很迷。於是放棄 Thinking in Java 泛型一章的閱讀,轉而官方教程,本章可以算作官方教程的中文版。 1.為什麼使用泛型 簡單來說

Java基礎系列繼承,字元反射

泛型型別的繼承規則 首先,我們來看一個類和它的子類,比如 Fruit 和 Apple。但是Pair<Apple>是Pair<Fruit>的一個子類麼?並不是。比如下面的這段程式碼就會編譯失敗: Apple[] apples = ...; Pair<F

Java (一) -- 持有物件CollectionListSetQueueMapIteratorforeach

本章將簡單介紹一下常用的集合類的特點,同時並不會深入原始碼分析原理,本文目的僅僅在於對 Java 集合類有一個整體認識 關於 API,本文不涉及過多,建議直接檢視 Java 官方文件 1. 容器概述 1.1 引入原因 Java 中,陣列用

Java--上界字元字元

轉自:Java泛型中extends和super的區別? 另,問題來源:Java 泛型 <? super T> 中 super 怎麼 理解?與 extends 有何不同?   <? extends T>和<? super T>是Java泛型中的

Java () -- 內部類概念分類特性意義"多重繼承"繼承

1. 內部類基礎 1.1 什麼是內部類 內部類的定義如下: 可以將一個類的定義放在另一個類的定義內部,這就是內部類 更具體一點,對於程式設計思想而言: 內部類允許將邏輯相關的類組織在一起,並控制位於內部的類的可視性。 內部類就像是一種程式碼隱

Java (九) -- 註解語法預定義註解元註解重複註解註解與反射

前言 官方註解的定義如下: 註解(一種元資料形式)提供有關不屬於程式本身的程式的資料。註解對它們註解的程式碼的操作沒有直接影響。 註解有許多用途,其中包括: 編譯器的資訊 - 編譯器可以使用註解來檢測錯誤或抑制警告。 編譯

QT學習QWebView實現簡易瀏覽器

QtWebkit 模組介紹   QtWebkit 模組提供了一個在Qt中使用web browser的engine,這使得我們在QT的應用程式中使用全球資訊網上的內容變得很容易,而且對其網頁內容的控制也可以通過native controls 實現  。   QtWebkit具

SDL遊戲()--中文輸入法

 中文輸入法 要實現中文拼音輸入,需要有 中文拼音對照表。這個可以到網上找到。 把檔案整理成如下格式: 'a'=>'啊阿呵吖嗄醃錒錒', 'ai'=>'愛矮挨哎礙癌艾唉哀藹隘埃皚呆嗌嬡璦曖捱砈噯鑀靄乂乃伌僾儗凒剴剴靉呃呝啀嘊噫噯堨塧壒奇娭娾嬡嵦愛懓懝敱敳昹

Java之上字元的理解(適合初學)

泛型的由來 為什麼需要泛型   Java的資料型別一般都是在定義時就需要確定,這種強制的好處就是型別安全,不會出現像弄一個ClassCastException的資料給jvm,資料安全那麼執行的class就會很穩定。但是假如說我不知道這個引數要傳什麼型別的,因為公司需求在變,如果寫死的那就只能便以此需求就改一次

網絡編程中的常見陷阱 0x進制數C++字面值常量

十六進制 aid word 網絡編程 情況 技術分享 fill 截斷 常見 十六進制數相等的推斷 請問例如以下程序的輸出是神馬? #include <iostream> #include <string> using namespace std

java中的字元?問題

本文是經過網上查詢的資料整合而來,給自己做個筆記,也分享給大家!需要你對泛型有一定的基礎瞭解。 package Test8_8; import java.util.ArrayList; import java.util.List; class Animal { privat

java】---型別與多的衝突和解決方法

型別擦除與多型的衝突和解決方法 現在有這樣一個泛型類: [java] view plain copy print ? class Pair<T>&

java 中 T 和 問號字元的區別

型別本來有:簡單型別和複雜型別,引入泛型後把複雜型別分的更細了; 現在List<Object>, List<String>是兩種不同的型別;且無繼承關係; 泛型的好處如: 開始版本 public void write(Integer

php原始碼章第一節 記憶體管理概述

記憶體是計算機非常關鍵的部件之一,是暫時儲存程式以及資料的空間,CPU只有有限的暫存器可以用於儲存計算資料,而大部分的資料都是儲存在記憶體中的,程式執行都是在記憶體中進行的。和CPU計算能力一樣,記憶體也是決定計算效率的一個關鍵部分。 計算中的資源中主要包含:

Java中T和問號字元的區別

型別本來有:簡單型別和複雜型別,引入泛型後把複雜型別分的更細了. 概述 泛型是Java SE 1.5的新特性,泛型的本質是引數化型別,也就是說所操作的資料型別被指定為一個引數。這種引數型別可以用在類、介面和方法的建立中,分別稱為泛型類、泛型介面、泛型方法。 Java語

Java三:字元詳解extends super

在java泛型中,? 表示萬用字元,代表未知型別,< ? extends Object>表示上邊界限定萬用字元,< ? super Object>表示下邊界限定萬用字元。 萬用字元 與 T 的區別 T:作用於模板上,用於將資料型別

Java-程式設計-使用字元? extends 和 ? super

泛型中使用萬用字元有兩種形式:子型別限定<? extends xxx>和超型別限定<? super xxx>。 (1)子型別限定 下面的程式碼定義了一個Pair<T>類,以及Employee,Manager和President類。 pub

java基礎(28)--型別與繼承

【泛型與型別擦除】 泛型是JDK 1.5的一項新特性,它的本質是引數化型別(Parameterized Type)的應用,也就是說所操作的資料型別被指定為一個引數。這種引數型別可以用在類、介面和方法的建立中,分別稱為泛型類、泛型介面和泛型方法。 泛

Java中的字元

1、上界萬用字元 首先,需要知道的是,Java語言中的陣列是支援協變的,什麼意思呢?看下面的程式碼: static class A extends Base{ void f() { System.out.pri