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 Generics 上界通配符

上界通配符用于放宽方法中变量类型的限制。假设我们想编写一个方法,该方法将返回列表中数字的总和,那么我们的实现将是这样的。

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 + "::");
		}
	}

我们可以为printData方法提供List<String>或List<Integer>或任何其他类型的对象列表参数。与上界列表类似,我们不允许向列表中添加任何内容。

9.3) Java泛型下界通配符

假设我们想要在一个方法中将整数添加到整数列表中,我们可以保持参数类型为List,但这将与整数绑定,而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中使用泛型?

泛型提供了强大的编译时类型检查,减少了ClassCastException的风险以及对象的显式转换。

12.2) 泛型中的T是什么意思?

我们使用<T>来创建泛型类、接口和方法。在使用时,T会被实际类型替换。

12.3) Java中的泛型是如何工作的?

泛型代码确保类型安全。编译器使用类型擦除,在编译时移除所有类型参数,以减少运行时的负担。

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

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

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