Java泛型示例教程 – 泛型方法、類、接口

Java泛型是Java 5中引入的最重要的功能之一。如果你一直在使用Java集合,并且版本是5或更高,我相信你一定使用过它。Java中的泛型与集合类一起非常简单,但它提供的功能远不止于创建集合的类型。在本文中,我们将尝试学习泛型的特性。有时候,理解泛型可能会变得令人困惑,如果我们使用行话词汇,所以我会尽量简单易懂。

我们将深入研究Java泛型的以下主题。

  1. Java中的泛型

  2. Java泛型类

  3. Java泛型接口

  4. Java泛型类型

  5. Java泛型方法

  6. Java泛型有界类型参数

  7. Java 泛型與繼承

  8. Java 泛型類別與子類型

  9. Java 泛型通配符

  10. Java 泛型上界通配符

  11. Java 泛型無界通配符

  12. Java 泛型下界通配符

  13. 使用泛型通配符的子類型

  14. Java 泛型型擦除

  15. 泛型常見問題

1. Java 中的泛型

泛型是在 Java 5 中添加的,旨在提供编译时类型检查,并消除在使用集合类时常见的 ClassCastException 风险。整个集合框架都被重写以使用泛型来确保类型安全。让我们看看泛型如何帮助我们安全地使用集合类。

List list = new ArrayList();
list.add("abc");
list.add(new Integer(5)); //OK

for(Object obj : list){
	// 类型转换导致运行时的 ClassCastException
    String str=(String) obj; 
}

上述代码编译正常,但在运行时会抛出 ClassCastException,因为我们试图将列表中的 Object 转换为 String,而其中一个元素的类型是 Integer。Java 5 之后,我们使用集合类如下所示。

List list1 = new ArrayList(); // java 7 ? List list1 = new ArrayList<>(); 
list1.add("abc");
//list1.add(new Integer(5)); // 编译错误

for(String str : list1){
     // 无需类型转换,避免了 ClassCastException
}

请注意,在创建列表时,我们指定了列表中元素的类型将为 String。因此,如果我们尝试向列表中添加任何其他类型的对象,程序将抛出编译时错误。还请注意,在 for 循环中,我们不需要对列表中的元素进行类型转换,从而消除了运行时的 ClassCastException。

2. Java 泛型类

我們可以使用泛型類型定義自己的類。泛型類型是參數化的類或接口。我們使用尖括號(<>)來指定類型參數。為了理解其好處,讓我們假設我們有一個簡單的類,如下所示:

package com.journaldev.generics;

public class GenericsTypeOld {

	private Object t;

	public Object get() {
		return t;
	}

	public void set(Object t) {
		this.t = t;
	}

        public static void main(String args[]){
		GenericsTypeOld type = new GenericsTypeOld();
		type.set("Pankaj"); 
		String str = (String) type.get(); //type casting, error prone and can cause ClassCastException
	}
}

請注意,在使用此類時,我們必須使用類型轉換,這可能會在運行時產生ClassCastException。現在,我們將使用Java泛型類來重寫相同的類,如下所示:

package com.journaldev.generics;

public class GenericsType<T> {

	private T t;
	
	public T get(){
		return this.t;
	}
	
	public void set(T t1){
		this.t=t1;
	}
	
	public static void main(String args[]){
		GenericsType<String> type = new GenericsType<>();
		type.set("Pankaj"); //valid
		
		GenericsType type1 = new GenericsType(); //raw type
		type1.set("Pankaj"); //valid
		type1.set(10); //valid and autoboxing support
	}
}

請注意在主方法中使用的GenericsType類。我們無需進行類型轉換,並且可以在運行時刪除ClassCastException。如果我們在創建時不提供類型,編譯器將產生警告“GenericsType是原始類型。參考泛型類型GenericsType<T>應該是參數化的”。當我們不提供類型時,類型變為Object,因此它允許使用String和Integer對象。但是,我們應該始終儘量避免這樣做,因為我們將不得不在處理原始類型時使用類型轉換,這可能會產生運行時錯誤。

提示:我們可以使用@SuppressWarnings("rawtypes")註釋來抑制編譯器警告,請查看Java註釋教程

還要注意它支持Java自動裝箱

3. Java 泛型接口

Comparable 接口是泛型在接口中的一个很好的例子,其写法如下:

package java.lang;
import java.util.*;

public interface Comparable<T> {
    public int compareTo(T o);
}

同样地,我们可以在 Java 中创建泛型接口。我们也可以像 Map 接口一样拥有多个类型参数。同样地,我们也可以为参数化类型提供参数化值,例如 new HashMap<String, List<String>>(); 是有效的。

4. Java 泛型类型

Java 泛型类型的命名约定帮助我们更容易理解代码,并且拥有命名约定是 Java 编程语言的最佳实践之一。因此,泛型也有其自己的命名约定。通常,类型参数的名称是单个大写字母,以便与 Java 变量清晰地区分开来。最常用的类型参数名称包括:

  • E – Element (used extensively by the Java Collections Framework, for example ArrayList, Set etc.)
  • K – Key (Used in Map)
  • N – Number
  • T – Type
  • V – Value (Used in Map)
  • S,U,V etc. – 2nd, 3rd, 4th types

5. Java 泛型方法

有时候我们不想整个类都被参数化,这种情况下,我们可以创建Java泛型方法。由于构造函数是一种特殊类型的方法,我们也可以在构造函数中使用泛型类型。这里是一个展示Java泛型方法示例的类。

package com.journaldev.generics;

public class GenericsMethods {

	//Java泛型方法
	public static  boolean isEqual(GenericsType g1, GenericsType g2){
		return g1.get().equals(g2.get());
	}
	
	public static void main(String args[]){
		GenericsType g1 = new GenericsType<>();
		g1.set("Pankaj");
		
		GenericsType g2 = new GenericsType<>();
		g2.set("Pankaj");
		
		boolean isEqual = GenericsMethods.isEqual(g1, g2);
		//以上语句可以简单地写成
		isEqual = GenericsMethods.isEqual(g1, g2);
		//这个特性被称为类型推断,它允许你调用一个泛型方法作为普通方法,而不需要在尖括号之间指定类型。
		//编译器会推断所需的类型
	}
}

请注意isEqual方法签名展示了在方法中使用泛型类型的语法。还要注意如何在我们的Java程序中使用这些方法。我们可以在调用这些方法时指定类型,或者我们可以像调用普通方法一样调用它们。Java编译器足够智能,可以确定要使用的变量类型,这种功能称为类型推断

6. Java泛型有界类型参数

假設我們想要限制可以在參數化類型中使用的對象類型,例如在比較兩個對象的方法中,我們希望確保接受的對象是可比較的。要聲明有界類型參數,請列出類型參數的名稱,後跟extends關鍵字,後跟其上界,類似於以下方法。

public static <T extends Comparable<T>> int compare(T t1, T t2){
		return t1.compareTo(t2);
	}

這些方法的調用方式與無界方法類似,只是如果我們試圖使用任何不可比較的類,它將拋出編譯時錯誤。有界類型參數既可以與方法一起使用,也可以與類和接口一起使用。Java泛型還支持多界限,即<T extends A&B&C>。在這種情況下,A可以是接口或類。如果A是類,則B和C應該是接口。我們不能在多界限中有超過一個類。

7. Java泛型與繼承

我們知道Java繼承允許我們將變量A分配給另一個變量B,如果A是B的子類。因此,我們可能會認為任何A的通用類型都可以分配給B的通用類型,但這不是事實。讓我們通過一個簡單的程序來看看這一點。

package com.journaldev.generics;

public class GenericsInheritance {

	public static void main(String[] args) {
		String str = "abc";
		Object obj = new Object();
		obj=str; // works because String is-a Object, inheritance in java
		
		MyClass myClass1 = new MyClass();
		MyClass myClass2 = new MyClass();
		//myClass2=myClass1; // 編譯錯誤,因為MyClass不是MyClass
		obj = myClass1; // MyClass parent is Object
	}
	
	public static class MyClass{}

}

我們不能將 MyClass 變數分配給 MyClass變數,因為它們之間沒有關係,實際上 MyClass 的父類是 Object。

8. Java 泛型類和子類化

我們可以通過擴展或實現來對泛型類或接口進行子類化。一個類或接口的類型參數與另一個的類型參數之間的關係是由 extends 和 implements 子句確定的。例如,ArrayList 實現了擴展 Collection 的 List,因此 ArrayList 是 List 的子類,而 List 是 Collection 的子類。只要我們不改變類型參數,子類化關係就會被保留,下面顯示了多類型參數的示例。

interface MyList<E,T> extends List<E>{
}

List 的子類可以是 MyList、MyList 等。

9. Java 泛型通配符

問號(?)是泛型中的萬用符號,代表未知類型。萬用符號可以用作參數、字段或局部變量的類型,有時也可以用作返回類型。我們無法在調用泛型方法或實例化泛型類時使用萬用符號。在接下來的部分中,我們將學習關於上界萬用符號、下界萬用符號和萬用符號捕獲。

9.1) Java 泛型上界萬用符號

上界萬用符號用於放寬方法中變量類型的限制。假設我們想要編寫一個方法,該方法將返回列表中數字的總和,那麼我們的實現將是這樣的。

public static double sum(List<Number> list){
		double sum = 0;
		for(Number n : list){
			sum += n.doubleValue();
		}
		return sum;
	}

現在以上實現的問題是,它無法處理整數或浮點數列表,因為我們知道 List<Integer> 和 List<Double> 沒有關聯,這時上界萬用符號就派上用場了。我們使用泛型萬用符號與extends關鍵字以及上界類或接口,這將允許我們傳遞上界或其子類型的參數。上面的實現可以修改為如下程式。

package com.journaldev.generics;

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

public class GenericsWildcards {

	public static void main(String[] args) {
		List<Integer> ints = new ArrayList<>();
		ints.add(3); ints.add(5); ints.add(10);
		double sum = sum(ints);
		System.out.println("Sum of ints="+sum);
	}

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

這類似於根據界面來撰寫我們的程式碼,在上述方法中,我們可以使用上界類別 Number 的所有方法。請注意,使用上界列表時,我們只能將 null 添加到列表中,不能添加任何對象。如果我們在 sum 方法內嘗試添加元素到列表中,程序將無法編譯。

9.2) Java 泛型未界定通配符

有時我們會遇到一種情況,希望我們的泛型方法能夠處理所有類型,在這種情況下,可以使用未界定通配符。這與使用 <?extends Object> 相同。

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

我們可以將 List 或 List 或任何其他類型的對象列表作為 printData 方法的參數。與上界列表類似,我們不允許向列表添加任何內容。

9.3) Java 泛型下界通配符

假設我們想要在一個方法中將整數添加到整數列表中,我們可以將參數類型保持為List,但這將與Integers綁定在一起,而List和List也可以容納整數,因此我們可以使用下界通配符來實現這一點。我們使用泛型通配符(?)與super關鍵字和下界類來實現這一點。我們可以將下界或任何下界的超類作為參數傳遞,此時,Java編譯器允許將下界對象類型添加到列表中。

public static void addIntegers(List<? super Integer> list){
		list.add(new Integer(50));
	}

10. 使用泛型通配符的子類化

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

11. Java泛型類型擦除

Java中的泛型是為了在編譯時提供類型檢查而添加的,在運行時沒有用處,因此Java編譯器使用類型擦除功能來刪除字節碼中的所有泛型類型檢查代碼,並在必要時插入類型轉換。類型擦除確保為參數化類型不創建新類;因此,泛型不會產生運行時開銷。例如,如果我們有一個如下的泛型類:

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

    private T data;
    private Test<T> next;

    public Test(T d, Test<T> n) {
        this.data = d;
        this.next = n;
    }

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

Java編譯器將有界類型參數T替換為第一個界限接口Comparable,如下所示的代碼:

public class Test {

    private Comparable data;
    private Test next;

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

    public Comparable getData() { return data; }
}

12. 通用問答

12.1) 我們為什麼在Java中使用通用(Generics)?

通用(Generics)提供強大的編譯時類型檢查,減少了ClassCastException和對象的顯式轉換的風險。

12.2) 通用中的T是什麼?

我們使用<T>來創建通用類、接口和方法。當我們使用時,T會被實際類型替換。

12.3) Java中的通用(Generics)是如何工作的?

通用代碼確保類型安全。編譯器使用類型擦除(type-erasure)在編譯時去除所有類型參數,以減少運行時的負載。

13. Java中的泛型 – 进一步阅读

这就是关于Java中的泛型的全部内容,Java泛型是一个非常广泛的话题,需要大量时间来理解和有效使用它。本文试图提供泛型的基本细节以及如何使用它来扩展我们的程序以实现类型安全。

Source:
https://www.digitalocean.com/community/tutorials/java-generics-example-method-class-interface