簡述泛型的基本使用和作用

来源:https://www.cnblogs.com/qian-fen/archive/2023/06/02/17451002.html
-Advertisement-
Play Games

# 前言 在上一篇文章中,給大家講解了泛型的概念、作用、使用場景,以及泛型集合、泛型介面和泛型類的用法,但受限於篇幅,並沒有把泛型的內容講解完畢。所以今天我們會繼續學習泛型方法、泛型擦除,以及通配符等的內容,希望大家繼續做好學習的準備哦。 *** 全文大約【**4600】** 字,不說廢話,只講可以 ...


前言

在上一篇文章中,給大家講解了泛型的概念、作用、使用場景,以及泛型集合、泛型介面和泛型類的用法,但受限於篇幅,並沒有把泛型的內容講解完畢。所以今天我們會繼續學習泛型方法、泛型擦除,以及通配符等的內容,希望大家繼續做好學習的準備哦。


全文大約【4600】 字,不說廢話,只講可以讓你學到技術、明白原理的純乾貨!本文帶有豐富的案例及配圖視頻,讓你更好地理解和運用文中的技術概念,並可以給你帶來具有足夠啟迪的思考...

一. 泛型方法

1. 簡介

我們可以在定義介面和類時使用泛型,這樣該介面和類中的所有方法及成員變數等處,也都可以使用該泛型。但其實泛型可以應用在整個類上,也可以只應用在類中的某個方法上。也就是說,方法所在的類可以是泛型類,也可以不是泛型類。方法中是否帶有泛型,與其所在的類有沒有泛型沒有關係。

泛型方法是在調用方法時才確定類型的方法,泛型可以使得該方法獨立於類而產生變化。另外,static靜態方法無法訪問泛型類的類型參數,因此,如果想讓一個static方法具有泛型能力,就必須使該靜態方法成為泛型方法。

2. 語法

我們在定義泛型方法時,需要在方法名的前面添加類型參數。定義泛型方法的語法格式如下:

[訪問許可權修飾符] [static] [final] <類型參數列表> 返回值類型 方法名([形式參數列表])

例如:

public static <T> List showInfo(Class<T> clazz,int userId){}

一般情況下,我們編寫泛型方法時,必須在該方法的名稱前聲明要使用的泛型,並且可以同時聲明多個泛型,中間也是用逗號分割。接下來就定義一個泛型方法,給大傢具體介紹一下泛型方法的創建和使用。

3. 代碼案例

這裡我們定義一個泛型方法,用於對數組排序後再遍歷元素輸出,代碼如下:

import java.util.Arrays;

public class Demo04 {
	
	//定義了一個靜態的泛型方法,遍曆數組中的每個元素
	public static <T> void printArray(T[] arr) {
		//先對數組進行排序
		Arrays.sort(arr);
		//再遍曆數組元素
	    for (T t : arr) {
	        System.out.print(t + " ");
	    }
	    System.out.println();
	}
	
	public static void main(String[] args) {
		Integer[] nums= {100,39,8,200,65};
		//調用泛型方法
		printArray(nums);
	}
}

在上面的代碼中,printArray()就是一個泛型方法,該方法中使用了類型參數T。並且我們在方法的參數中,使用類型參數T定義了一個泛型數組T[],接著對該數組進行排序和遍歷。這樣以後無論我們傳入任何類型的數組,都可以在不進行類型轉換的前提下,輕鬆實現排序等功能了,這樣我們之前提的需求也就很容易實現了。

二. 通配符

除了以上這些用法之外,泛型中還有一個很重要的通配符功能,接下來我們就來看看它是怎麼回事。

1. 簡介

泛型中的通配符其實也是一種特殊的泛型類型,也稱為通配符類型參數。利用通配符類型參數,可以讓我們編寫出更通用的代碼,甚至可以在不知道實際類型的情況下使用它們。我們一般是使用 ? 來代替具體的類型參數,例如 List<?> 在邏輯上可以等同於 List、List 等所有 List<具體類型實參> 的類。

對此,有的小伙伴可能會很好奇,我們為什麼需要通配符呢?其實之所以會出現通配符,主要是在開發時,有時候我們需要一個泛型類型,但我們卻不知道該使用哪個具體的類型。在這種情況下,我們就可以使用通配符類型參數,讓代碼更加地通用。比如,我們想編寫一個可以接受任何類型的集合,並返回其中最大的元素時,此時我們可能並不確定到底該傳入哪個具體的集合,那使用通配符就會更好一些。

2. 通配符的形式

泛型通配符在具體使用時,有如下三種實現形式:

  • 未限定通配符(?)?表示未知類型的通配符
  • 上限通配符(? extends T)?表示類型上限的通配符,T是一個類或介面
  • 下限通配符(? super T)?表示類型下限的通配符,T是一個類或介面

接下來我們針對以上這三種形式,分別通過幾個案例來給大家講解其用法。

3. 未限定通配符(?)

未限定通配符(?)是一種表示未知類型的通配符,它可以在需要一個類型參數的情況下使用。但由於沒有限制,因此它只能用於簡單的情況,例如集合中的迭代器或者返回類型是泛型的方法等。下麵是一個簡單的例子:

import java.util.ArrayList;
import java.util.List;

public class Demo05 {

	public static void main(String[] args) {
		List<String> names = new ArrayList<String>();
		List<Integer> ages = new ArrayList<Integer>();
		List<Number> numbers = new ArrayList<Number>();

		names.add("一一哥");
		names.add("秦始皇");
		ages.add(28);
		ages.add(50);
		ages.add(28);
		numbers.add(100);
		numbers.add(800);

		printElement(names);
		printElement(ages);
		printElement(numbers);
	}

	//未限定通配符的使用
	public static void printElement(List<?> data) {
		for(int i=0;i<data.size();i++) {
			//data.getClass().getSimpleName():用於獲取某個類的類名
			System.out.println(data.getClass().getSimpleName()+"--data: " + data.get(i));
		}
	}

}

在這個例子中,printElement()方法就接受了一個未知類型的List集合,所以names,ages,numbers都可以作為這個方法的實參,這就是未限定通配符的作用。

4. PECS原則

PECS是Producer Extends Consumer Super的縮寫,這是關於Java泛型的一種設計原則。該原則表示,如果我們需要返回T,它是生產者(Producer),要使用extends通配符;如果需要寫入T,它就是消費者(Consumer),要使用super通配符。該原則可以指導我們在使用泛型時,該如何定義類型參數的上限和下限。

當我們使用泛型時,可能需要定義類型參數的上限和下限。

例如,我們想要編寫一個方法來處理一些集合類型,但我們不知道這些集合中到底有什麼類型的元素,此時我們就可以定義一個類型參數來處理所有的集合類型。一般我們可以利用extends來設置泛型上限,利用super來設置泛型下限。接下來會在下麵的第5和第6小結中,給大家講解泛型的上限和下限具體該如何實現,請大家繼續往下學習。

5. 上限通配符(? extends T)

上限通配符(?extends T)是一種表示類型上限的通配符,其中T是一個類或介面,泛型類的類型必須實現或繼承 T這個介面或類。它指定了可以使用的類型上限,主要是用於限制輸入的參數類型。

import java.util.ArrayList;
import java.util.List;

/**
 * @author 一一哥Sun 
 */
public class Demo06 {

	public static void main(String[] args) {
		List<String> names = new ArrayList<String>();
		List<Integer> ages = new ArrayList<Integer>();
		List<Number> numbers = new ArrayList<Number>();

		names.add("一一哥");
		names.add("秦始皇");
		ages.add(28);
		ages.add(50);
		ages.add(28);
		numbers.add(100);
		numbers.add(800);
		
		//String等非Number類型就不行
		//printElementUpbound(names);
		
		//泛型值只能是Number及其子類類型,所以Integer/Double/Float等類型都可以,但String就不行
		printElementUpbound(ages);
		printElementUpbound(numbers);
	}

	//上限通配符的使用,這裡的泛型值只能是Number及其子類類型
	public static void printElementUpbound(List<? extends Number> data) {
		for(int i=0;i<data.size();i++) {
			//data.getClass().getSimpleName():用於獲取某個類的類名
			System.out.println(data.getClass().getSimpleName()+"--data: " + data.get(i));
		}
	}
}

在這個例子中,printElementUpbound方法中的集合泛型,可以是Number類或其子類,除此之外的其他類型都不行。也就是說,我們只能使用Number或其子類作為類型參數,泛型類型的上限是Number,這就是上限通配符的含義。

6. 下限通配符(? super T)

下限通配符(?super T)是一種表示類型下限的通配符,其中T是一個類或介面。它指定了可以使用的類型下限,主要用於限制輸出的參數類型。下麵是一個簡單的例子:

import java.util.ArrayList;
import java.util.List;

public class Demo07 {

	public static void main(String[] args) {
		List<String> names = new ArrayList<String>();
		List<Integer> ages = new ArrayList<Integer>();
		List<Double> numbers = new ArrayList<Double>();

		names.add("一一哥");
		names.add("秦始皇");
		ages.add(28);
		ages.add(50);
		ages.add(28);
		numbers.add(100.0);
		numbers.add(800.9);
		
		//String等非Number類型就不行
		//printElementUpbound(names);
		
		//此時Double類型也不行
		//printElementDownbound(numbers);
		
		//泛型值只能是Integer及其父類類型,所以Double/Float/String等類型都不可以
		printElementDownbound(ages);
		
	}

	//下限通配符的使用,這裡的泛型值只能是Integer及其父類類型
	public static void printElementDownbound(List<? super Integer> data) {
		for(int i=0;i<data.size();i++) {
			System.out.println(data.getClass().getSimpleName()+"--data: " + data.get(i));
		}
	}
}

在這個例子中,printElementDownbound方法中的集合泛型,可以是Integer或其父類型,即類型下限是Integer,除此之外的其他類型都不行。也就是說,我們只能使用Integer或其父類作為類型參數,泛型類型的下限是Integer,這就是下限通配符的含義。

7. <? extends T>和<? super T>的區別

在這裡,要給大家再總結一下<? extends T>和<? super T>的區別:

  • <? extends T> 允許調用 T get()這樣的 讀方法來獲取 T對象 的引用,但不允許調用 set(T)這樣的 寫方法來傳入 T 的引用(傳入 null 除外);
  • <? super T> 允許調用 set(T)這樣的 寫方法傳入 T對象 的引用,但不允許調用 T get()這樣的 讀方法來獲取 T對象 的引用(獲取 Object 除外)。
  • <? extends T> 允許讀不允許寫, <? super T> 允許寫不允許讀。

大家註意,無論是未限定通配符、上限通配符還是下限通配符,我們既可以在方法中使用,也可以在類或介面中使用。

三. 泛型擦除

我們在學習泛型時,除了要掌握上面這些泛型類、泛型介面、泛型方法以及通配符等內容之外,還要學習泛型擦除的相關內容。那麼什麼是泛型擦除呢?我們繼續往下學習吧。

1. 簡介

所謂的泛型擦除(Type Erasure),就是指在編譯時,JVM編譯器會將所有的泛型信息都擦除掉,變成原始類型,一般是將泛型的類型參數替換成具體類型的上限或下限(如果沒有指定上界,則預設為Object)。

換句話說,雖然我們在代碼中使用了泛型,但在編譯後,所有的泛型類型都會被擦除掉,轉而使用其對應的原始類型。這就是Java泛型的底層實現原理。這樣設計的目的是為了相容舊的JDK版本,使得Java具有了較好的向後相容性,舊的非泛型代碼可以直接使用泛型類庫,而不需要進行任何修改。同時,Java也提供了反射機制來操作泛型類型,使得泛型類型在某些情況下還是可以被獲取到的,所以即使有泛型擦除,仍然也不會太影響Java虛擬機的運行時效率。

比如,在我們定義一個泛型類時,我們會使用泛型類型參數來代替具體的類型,好比下麵這個例子:
在這裡插入圖片描述

2. 泛型擦除帶來的限制

在編譯之後,這個泛型類的類型參數T就會被擦除,成為其對應的原始類型Object。

這也意味著,我們無法在運行時獲取到泛型的實際類型參數,所以泛型擦除的使用會有一些限制。首先由於泛型類型參數被擦除了,因此我們在運行時就無法獲得泛型類型參數的信息。例如,如果我們有一個List類型的變數,在運行時我們就無法獲得這個List集合中的元素類型是Integer。另一個限制是在使用泛型類型時,還需要註意類型安全性。在編譯階段,編譯器會檢查泛型類型的類型安全性;但在運行階段,由於泛型類型參數被擦除了,因此就無法保證類型安全性了。泛型擦除的限制,主要表現在以下幾個方面:

無法使用基本類型實例化類型參數;

無法在運行時獲取泛型類型信息;

泛型類型參數不能用於靜態變數或靜態方法;

不能實例化T類型。

接下來再給大傢具體分析一下這些限制

2.1 無法使用基本類型實例化類型參數

Java泛型中的類型參數不能是基本類型,只能是類或介面類型。例如,以下代碼在編譯階段會出錯,無法通過編譯:

List<int> list = new ArrayList<int>();

正確的寫法是使用基本類型對應的包裝類型,如下所示:

List<Integer> list = new ArrayList<Integer>();

2.2 無法在運行時獲取泛型類型信息

由於泛型擦除的存在,導致我們在程式運行時無法獲取泛型類型的信息。例如,以下代碼在運行時就無法獲取List的元素類型:

List<String> list = new ArrayList<String>(); 
Class<?> clazz = list.getClass(); 
Type type = clazz.getGenericSuperclass(); 
// 輸出:class java.util.ArrayList<E>
System.out.println(type); 

在輸出的結果中,我們只能得到ArrayList的類型信息,而無法獲取到集合中具體的泛型類型信息,也就是獲取不到String的信息。但如果我們就是想在運行時獲取到泛型的實際類型參數,其實可以參考以下方式進行實現:

public class Box<T> {
    private T content;

    public Box(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }

    public void setContent(T content) {
        this.content = content;
    }

    public Class<?> getContentType() {
        return content.getClass();
    }
}

在上面的代碼中,我們新增了一個方法 getContentType(),該方法用於返回泛型類型參數的實際位元組碼類型,以後我們通過調用這個方法,就可以間接地獲取到泛型類型的信息了。

2.3 泛型類型參數不能用於靜態變數或靜態方法

由於泛型類型參數是在實例化對象時才被確定的,因此不能在靜態變數或靜態方法中使用泛型類型參數。例如,以下代碼是無法編譯通過的:

public class MyClass<T> {   
    //這樣的代碼編譯不通過
    private static T value;   
    
    public static void setValue(T value) {        
        MyClass.value = value;     
    } 
}

正確的寫法是使用一個實際類型來代替泛型類型參數:

public class MyClass {     
    private static String value;          
    public static void setValue(String value) {         
        MyClass.value = value;     
    } 
}

2.4 不能實例化T類型

比如在下麵的案例中:

public class MyClass<T> {
    private T first;
    private T last;
    
    public MyClass() {
        first = new T();
        last = new T();
    }
}

上述代碼無法通過編譯,因為構造方法的兩行語句:

first = new T(); 
last = new T(); 

擦拭後實際上變成了:

first = new Object(); 
last = new Object(); 

這樣一來,創建new MyClass<String>()和創建new MyClass<Integer>()就變成了Object,編譯器會阻止這種類型不對的代碼。如果我們想對泛型T進行實例化,需要藉助Class< T >參數並集合反射技術來實現,且在使用時也必須傳入Class< T >。


四. 結語

不過,儘管泛型擦除有一些限制,但泛型仍然不失為一種強大的編程工具,它可以提高代碼的可讀性和可維護性。通過合理地使用泛型,我們可以在編譯時進行類型檢查,避免類型轉換的錯誤和運行時異常,從而提高了代碼的安全性和可靠性。同時,我們也需要瞭解Java泛型擦除的限制,以便在實際應用中做出正確的決策。

視頻教程:視頻教程,戳我直達


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前端組件 <hd-flex> <el-dialog v-model="isUploadDialog" width="50%" lock-scroll=false> <el-upload class="upload-demo" drag :action="url" :on-success="succe ...
  • 基於java的酒店管理系統設計與實現,酒店訂票系統,酒店預訂系統,酒店信息管理系統,app訂房系統設計與實現; ...
  • 本章將繼續探索內核中解析PE文件的相關內容,PE文件中FOA與VA,RVA之間的轉換也是很重要的,所謂的FOA是文件中的地址,VA則是記憶體裝入後的虛擬地址,RVA是記憶體基址與當前地址的相對偏移,本章還是需要用到`《驅動開發:內核解析PE結構導出表》`中所封裝的`KernelMapFile()`映射函... ...
  • 一致性哈希演算法是1997年由麻省理工的幾位學者提出的用於解決分散式緩存中的熱點問題。大家有沒有發現,我們之前介紹的例如快排之類的演算法是更早的六七十年代,此時分散式還沒有發展起來,大家往往還在提高單機性能。但是九十年代開始,逐漸需要用分散式集群來解決大型問題,相應的演算法研究也就應運而生。在說到一致性哈 ...
  • # 1.數值列表 列表非常適合用於存儲數字集合,而python提供了很多工具,可幫助我們高速地處理數字列表。 ## 1.1 range函數 python的range函數能夠輕鬆的生成連續一系列數字。 其語法格式:range(第一個數值,第二個數值) 編寫程式如下所示 ![image](https:/ ...
  • 時間序列數據是數據分析中一類常見且重要的數據。 它們按照時間順序記錄,通常是從某些現象的觀察中收集的,比如經濟指標、氣象數據、股票價格、銷售數據等等。 時間序列數據的特點是有規律地隨著時間變化而變化,它們的變化趨勢可以被分析和預測。時間序列分析是一種用於預測未來值或評估過去值的統計方法,常常被用於預 ...
  • 基於java的線上商城設計與實現,線上購物平臺,校園購物商城,商品銷售平臺,基於Java的電商平臺;電商平臺,買家和賣家可以在此平臺上進行銷售和交易,節約了大量的線下時間成本,購物車的功能,校園交易平臺等等; ...
  • 昨天遇到參數key大小寫不一致導致校驗簽名失敗的問題,查了很長時間才找到原因。看了一下FastJson源碼,發現JSON.toObject中轉換成對象的時候會忽略大小寫。 所以,當使用了JSON.toObject將json轉成Java對象後,再用JSON.toObject轉成json,key值就變了 ...
一周排行
    -Advertisement-
    Play Games
  • 1、預覽地址:http://139.155.137.144:9012 2、qq群:801913255 一、前言 隨著網路的發展,企業對於信息系統數據的保密工作愈發重視,不同身份、角色對於數據的訪問許可權都應該大相徑庭。 列如 1、不同登錄人員對一個數據列表的可見度是不一樣的,如數據列、數據行、數據按鈕 ...
  • 前言 上一篇文章寫瞭如何使用RabbitMQ做個簡單的發送郵件項目,然後評論也是比較多,也是準備去學習一下如何確保RabbitMQ的消息可靠性,但是由於時間原因,先來說說設計模式中的簡單工廠模式吧! 在瞭解簡單工廠模式之前,我們要知道C#是一款面向對象的高級程式語言。它有3大特性,封裝、繼承、多態。 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 介紹 Nodify是一個WPF基於節點的編輯器控制項,其中包含一系列節點、連接和連接器組件,旨在簡化構建基於節點的工具的過程 ...
  • 創建一個webapi項目做測試使用。 創建新控制器,搭建一個基礎框架,包括獲取當天日期、wiki的請求地址等 創建一個Http請求幫助類以及方法,用於獲取指定URL的信息 使用http請求訪問指定url,先運行一下,看看返回的內容。內容如圖右邊所示,實際上是一個Json數據。我們主要解析 大事記 部 ...
  • 最近在不少自媒體上看到有關.NET與C#的資訊與評價,感覺大家對.NET與C#還是不太瞭解,尤其是對2016年6月發佈的跨平臺.NET Core 1.0,更是知之甚少。在考慮一番之後,還是決定寫點東西總結一下,也回顧一下.NET的發展歷史。 首先,你沒看錯,.NET是跨平臺的,可以在Windows、 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 添加節點(nodes) 通過上一篇我們已經創建好了編輯器實例現在我們為編輯器添加一個節點 添加model和viewmode ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...
  • 類型檢查和轉換:當你需要檢查對象是否為特定類型,並且希望在同一時間內將其轉換為那個類型時,模式匹配提供了一種更簡潔的方式來完成這一任務,避免了使用傳統的as和is操作符後還需要進行額外的null檢查。 複雜條件邏輯:在處理複雜的條件邏輯時,特別是涉及到多個條件和類型的情況下,使用模式匹配可以使代碼更 ...
  • 在日常開發中,我們經常需要和文件打交道,特別是桌面開發,有時候就會需要載入大批量的文件,而且可能還會存在部分文件缺失的情況,那麼如何才能快速的判斷文件是否存在呢?如果處理不當的,且文件數量比較多的時候,可能會造成卡頓等情況,進而影響程式的使用體驗。今天就以一個簡單的小例子,簡述兩種不同的判斷文件是否... ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...