主頁 >  其他 > Java 泛型詳解

Java 泛型詳解

2020-11-22 15:56:05 其他


本文部分摘自 On Java 8


概述

在 Java5 以前,普通的類和方法只能使用特定的型別:基本資料型別或型別別,如果撰寫的代碼需要應用于多種型別,這種嚴苛的限制對代碼的束縛就會很大

Java5 的一個重大變化就是引入泛型,泛型實作了引數化型別,使得你撰寫的組件(通常是集合)可以適用于多種型別,泛型的初衷是通過解耦類或方法與所使用的型別之間的約束,使得類或方法具備最寬泛的表達力,然而很快你就會發現,Java 中的泛型并沒有你想的那么完美,甚至存在一些令人迷惑的實作


泛型類

促成泛型出現的最主要動機之一就是為了創建集合類,集合用于存放要使用到的物件,現有一個只能持有單個物件的類:

class Automobile {}

public class Holder1 {
    private Automobile a;
    public Holder1(Automobile a) { this.a = a; }
    Automobile get() { return a; }
}

如果沒有泛型,那么就必須明確指定其持有的物件的型別,會導致該復用性不高,它無法持有其他型別的物件,我們當然不希望為每個型別都撰寫一個新類

在 Java5 以前,為了解決這個問題,我們可以讓這個類直接持有 Object 型別的物件,這樣就可以持有多種不同型別的物件了,但通常而言,我們只會用集合存盤同一型別的物件,泛型的主要目的之一就是用來約定集合要存盤什么型別的物件,并且通過編譯器確保規約得以滿足

所以,與其使用 Object,我們更希望先指定一個型別占位符,稍后再決定具體使用什么型別,由此我們需要使用型別引數,用尖括號括住,放在類名后面,然后在使用這個類時,再用實際的型別替換此型別引數

public class GenericHolder<T> {
    private T a;
    public GenericHolder() {}
    public void set(T a) { this.a = a; }
    public T get() { return a; }

    public static void main(String[] args) {
        // 在 Java7 中右邊的尖括號可以為空
        GenericHolder<Automobile> h2 = new GenericHolder<Automobile>();
        GenericHolder<Automobile> h3 = new GenericHolder<>();
        h3.set(new Automobile()); // 此處有型別校驗
        Automobile a = h3.get();  // 無需型別轉換
        //- h3.set("Not an Automobile"); // 報錯
    }
}

元組類別庫

有時一個方法需要能回傳多個物件,而 return陳述句只能回傳單個物件,解決的方法就是創建一個物件,用它來打包想要回傳的多個物件,元組的概念正是基于此,元組將一組物件直接打包存盤于單一物件中,可以從該物件讀取其中元素,卻不允許向其中存盤新物件(這個概念也稱資料傳輸物件或信使)

元組可以具有任意長度,元組中的物件可以是不同型別的,我們希望能為每個物件指明型別,這時泛型就派上用場了,例如下面是一個可以存盤兩個物件的元組:

public class Tuple<A, B> {
    public final A a1;
    public final B a2;
    public Tuple(A a, B b) { a1 = a; a2 = b; }
    public String rep() { return a1 + ", " + a2; }

    @Override
    public String toString() {
        return "(" + rep() + ")";
    }
}

使用 final 修飾成員變數可以保證其不被修改,如果用戶想存盤不同的元素,那么就必須創建新的 Tuple 物件,當然也可以允許用戶重新對 a1、a2 賦值,但無疑前一種形式會更加安全

利用繼承機制可以實作長度更長的元組:

public class Tuple3<A, B, C> extends Tuple2<A, B> {
    public final C a3;
    public Tuple3(A a, B b, C c) {
        super(a, b);
        a3 = c;
    }

    @Override
    public String rep() {
        return super.rep() + ", " + a3;
    }
}

泛型方法

到目前為止,我們已經研究了引數化整個類,其實還可以引數化類中的方法,類本身是否是泛型,與它的方法是否是泛型并沒有什么直接關系,我們應該盡可能使用泛型方法,通常將單個方法泛型化要比將整個類泛型化要更加清晰易懂

要定義泛型方法,請將泛型引數串列放置在回傳值之前:

public class GenericMethods {
    public <T> void f(T x) {
        System.out.println(x.getClass().getName());
    }

    public static void main(String[] args) {
        GenericMethods gm = new GenericMethods();
        gm.f("");
        gm.f(1);
        gm.f(1.0);
        gm.f(1.0F);
        gm.f('c');
        gm.f(gm);
    }
}

使用泛型方法時,通常不需要指定引數型別,因為編譯器會找出這些型別,這稱為型別引數推斷,因此,對 f() 的呼叫看起來像普通的方法呼叫,而且像是被多載了無數次一樣


泛型擦除

當你開始深入研究泛型時,你會發現一個殘酷的現實:在泛型代碼內部,無法獲取任何有關泛型引數型別的資訊

class Frob {}
class Fnorkle {}
class Quark<Q> {}
class Particle<POSITION, MOMENTUM> {}

public class LostInformation {

    public static void main(String[] args) {
        List<Frob> list = new ArrayList<>();
        Map<Frob, Fnorkle> map = new HashMap<>();
        Quark<Fnorkle> quark = new Quark<>();
        Particle<Long, Double> p = new Particle<>();
        System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(p.getClass().getTypeParameters()));
    }
}

/* Output:
[E]
[K,V]
[Q]
[POSITION,MOMENTUM]
*/

正如上例中輸出所示,你只能看到用作引數占位符的識別符號,這并非有用的資訊,Java 泛型是使用擦除實作的,這意味著當你在使用泛型時,任何具體的型別資訊都被擦除了,你唯一知道的就是你在使用一個物件,因此 List<String> 和 List 在運行時實際上是相同的型別,它們都被擦除成原生型別 List

再來看一個例子:

class Manipulator<T> {
    private T obj;

    Manipulator(T x) {
        obj = x;
    }

    // Error: cannot find symbol: method f():
    public void manipulate() {
        obj.f();
    }
}

public class Manipulation {
    public static void main(String[] args) {
        HasF hf = new HasF();
        Manipulator<HasF> manipulator = new Manipulator<>(hf);
        manipulator.manipulate();
    }
}

因為擦除,Java 編譯器無法將 manipulate() 方法能呼叫 obj 的 f() 方法這一需求映射到 HasF 具有 f() 方法這個事實上,為了呼叫 f(),我們必須協助泛型類,為泛型類給定一個邊界,以此告訴編譯器只能接受遵循這個邊界的型別,這里重用了 extends 關鍵字,由于有了邊界,下面的代碼就能通過編譯:

public class Manipulator2<T extends HasF> {
    private T obj;

    Manipulator2(T x) {
        obj = x;
    }

    public void manipulate() {
        obj.f();
    }
}

邊界 <T extends HasF> 宣告 T 必須是 HasF 型別或其子類,如果情況確實如此,就可以安全地在 obj 上呼叫 f() 方法,泛型型別引數會擦除到它的第一個邊界(可能有多個邊界,稍后你將看到),我們還提到了型別引數的擦除,編譯器實際上會把型別引數替換為它的擦除,就像上面的示例,T 擦除到了 HasF,就像在類的宣告中用 HasF 替換了 T 一樣,如果我們愿意,完全可以把上例的 T 替換成 HashF,效果也是一樣的,那么泛型的意義又何在呢?

這提出了很重要的一點:泛型只有在型別引數比某個具體型別(以及其子類)更加“泛化”,代碼能跨多個類作業時才有用,因此,使用型別引數通常比簡單的宣告類更加復雜,但是,不能因此認為使用 <T extends HasF> 形式就是有缺陷的,你必須查看所有的代碼,從而確定代碼是否復雜到必須使用泛型的程度

有關泛型擦除的困惑,其實是 Java 為實作泛型的一種妥協,因為泛型并不是 Java 語言出現時就有的,擦除減少了泛型的泛化性,泛型型別只有在靜態型別檢測期間才出現,在此之后,程式中的所有泛型型別都將被擦除,替換為它們的非泛型上界,例如, List<T> 這樣的型別注解會被擦除為 List,普通的型別變數在未指定邊界的情況下會被擦除為 Object

在 Java5 以前撰寫的類別庫是沒有使用泛型的,而作者可能打算重新用泛型撰寫,或者根本不打算這樣做,Java 設計者們既要保證舊代碼和類檔案依然合法,還得考慮當某個類別庫變為泛型時,不會破壞依賴于它的代碼和應用,Java 設計者們最終認為泛型是唯一可行的解決方案,擦除使得向泛型的遷移成為可能,為了實作非泛型的代碼和泛型代碼共存,必須將某個類別庫使用了泛型這樣的“證據”擦除

基于上述觀點,當你在撰寫泛型代碼時,必須時刻提醒自己,你只是看起來擁有有關引數的型別資訊而言,因為擦除,我們無法在運行時知道確切的型別,為了補償擦除帶來的弊端,我們可以為所需的型別顯示傳遞一個 Class 物件,以在型別運算式中使用它

class Building {
}

class House extends Building {
}

public class ClassTypeCapture<T> {
    Class<T> kind;

    public ClassTypeCapture(Class<T> kind) {
        this.kind = kind;
    }

    public boolean f(Object arg) {
        return kind.isInstance(arg);
    }

    public static void main(String[] args) {
        ClassTypeCapture<Building> ctt1 =
                new ClassTypeCapture<>(Building.class);
        System.out.println(ctt1.f(new Building()));
        System.out.println(ctt1.f(new House()));
        ClassTypeCapture<House> ctt2 =
                new ClassTypeCapture<>(House.class);
        System.out.println(ctt2.f(new Building()));
        System.out.println(ctt2.f(new House()));
    }
}

邊界和通配符

由于擦除會洗掉型別資訊,因此唯一可用于無限制泛型引數的方法是那些 Object 可用的方法,邊界允許我們對泛型使用的引數型別施以型別,將引數限制為某型別的子集,那么就可以呼叫該子集中的方法,為了應用約束,Java 泛型使用了 extends 關鍵字

class Coord {
    public int x, y, z;
}

interface Weight {
    int weight();
}

class Solid<T extends Coord & Weight> {
    T item;

    Solid(T item) {
        this.item = item;
    }

    T getItem() {
        return item;
    }

    int getX() {
        return item.x;
    }

    int getY() {
        return item.y;
    }

    int getZ() {
        return item.z;
    }

    int weight() {
        return item.weight();
    }
}

class Bounded
        extends Coord implements Weight {

    @Override
    public int weight() {
        return 0;
    }
}

public class BasicBounds {
    public static void main(String[] args) {
        Solid<Bounded> solid =
                new Solid<>(new Bounded());
        solid.getY();
        solid.weight();
    }
}

引入通配符可以在泛型實體化時更加靈活地控制,也可以在方法中控制方法的引數,具體語法如下:

  • ? extends T:表示 T 或 T 的子類
  • ? super T:表示 T 或 T 的父類
  • ?:表示可以是任意型別

值得注意的問題

在這里主要闡述在使用 Java 泛型時會出現的各類問題

1. 任何基本資料型別不能作為型別引數

Java 泛型的限制之一是不能將基本型別用作型別引數,因此,不能創建 ArrayList<int> 之類的東西, 解決方法是使用基本型別的包裝器類以及自動裝箱機制,如果創建一個 ArrayList<Integer>,并將基本型別 int 應用于這個集合,那么你將發現自動裝箱機制將自動地實作 int 到 Integer 的雙向轉換,這幾乎就像是有一個 ArrayList<int> 一樣

2. 實作引數化介面

一個類不能實作同一個泛型介面的兩種變體,由于擦除的原因,這兩個變體會成為相同的介面,下面是產生這種沖突的情況:

interface Payable<T> {}

class Employee implements Payable<Employee> {}

class Hourly extends Employee implements Payable<Hourly> {}

Hourly 不能編譯,因為擦除會將 Payable<Employe> 和 Payable<Hourly> 簡化為相同的類 Payable,這樣,上面的代碼就意味著在重復兩次地實作相同的介面,十分有趣的是,如果從 Payable 的兩種用法中都移除掉泛型引數(就像編譯器在擦除階段所做的那樣)這段代碼就可以編譯

3. 轉型和警告

使用帶有泛型型別引數的轉型不會有任何效果,例如:

class Storage<T> {
    
    private Object obj;

    Storage() {
        obj = new Object();
    }

    @SuppressWarnings("unchecked")
    public T pop() {
        return (T)obj;
    }
}

public class GenericCast {

    public static void main(String[] args) {
        Storage<String> storage = new Storage<>();
        System.out.println(storage.pop());
    }
}

如果沒有 @SuppressWarnings 注解,編譯器將對 pop() 產生 “unchecked cast” 警告,由于擦除的原因,編譯器無法知道這個轉型是否是安全的,并且 pop() 方法實際上并沒有執行任何轉型, 這是因為,T 被擦除到它的第一個邊界,默認情況下是 Object,因此 pop() 實際上只是將 Object 轉型為 Object

4. 多載

下面的程式是不能編譯的,因為擦除,所以多載方法產生了相同的型別簽名

public class UseList<W, T> {
    void f(List<T> v) {}
    void f(List<W> v) {}
}

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/226387.html

標籤:其他

上一篇:E1. Weights Division (easy version) 決議(思維、優先佇列、樹狀DP)

下一篇:Unity 2D Light (4) - 平行光

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more