程序员社区

Java泛型 详解——泛型方法、类、接口

Java泛型示例教程——泛型方法、类、接口

Java 泛型中引入的最重要的特性之一。

如果您一直在使用Java 集合并使用版本 5 或更高版本,我相信您已经使用过它。

Java 中带有集合类的泛型非常简单,但它提供了比创建集合类型更多的特性。

我们将在本文中尝试学习泛型的特性。如果我们使用行话,理解泛型有时会变得混乱,所以我会尽量让它简单易懂。

Java 中的泛型

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

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

for(Object obj : list){
    //type casting leading to ClassCastException at runtime
    String str=(String) obj; 
}

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

List<String> list1 = new ArrayList<String>(); // java 7 ? List<String> list1 = new ArrayList<>(); 
list1.add("abc");
//list1.add(new Integer(5)); //compiler error

for(String str : list1){
     //no type casting needed, avoids ClassCastException
}

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

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
    }
}

注意在 main 方法中使用 GenericsType 类。我们不需要进行类型转换,我们可以在运行时移除 ClassCastException。如果我们在创建时不提供类型,编译器会产生一个警告“GenericsType 是原始类型。

对泛型类型 GenericsType 的引用应该被参数化”。当我们不提供类型时,类型就会变成Object,因此它允许 String 和 Integer 对象。但是,我们应该始终尽量避免这种情况,因为在处理可能产生运行时错误的原始类型时,我们将不得不使用类型转换。

提示:我们可以使用@SuppressWarnings("rawtypes")注解来抑制编译器警告,

Java 通用接口

Comparable interface 是接口中泛型的一个很好的例子,它被写成:

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

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

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

Java 泛型类型

Java 通用类型命名约定有助于我们轻松理解代码,命名约定是 Java 编程语言的最佳实践之一。所以泛型也有自己的命名约定。通常,类型参数名称是单个大写字母,以使其易于与 java 变量区分开来。最常用的类型参数名称是:

  • E – 元素(被 Java 集合框架广泛使用,例如 ArrayList、Set 等)
  • K – 键(在Map中使用)
  • N – 数字
  • T – 类型
  • V – 值(用于地图)
  • S、U、V 等 – 第 2、第 3、第 4 种类型

Java泛型方法

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

这是一个显示 java 泛型方法示例的类。

package com.journaldev.generics;

public class GenericsMethods {

    //Java Generic Method
    public static <T> boolean isEqual(GenericsType<T> g1, GenericsType<T> g2){
        return g1.get().equals(g2.get());
    }

    public static void main(String args[]){
        GenericsType<String> g1 = new GenericsType<>();
        g1.set("Pankaj");

        GenericsType<String> g2 = new GenericsType<>();
        g2.set("Pankaj");

        boolean isEqual = GenericsMethods.<String>isEqual(g1, g2);
        //above statement can be written simply as
        isEqual = GenericsMethods.isEqual(g1, g2);
        //This feature, known as type inference, allows you to invoke a generic method as an ordinary method, without specifying a type between angle brackets.
        //Compiler will infer the type that is needed
    }
}

请注意isEqual方法签名显示在方法中使用泛型类型的语法。

另外,请注意如何在我们的 java 程序中使用这些方法。我们可以在调用这些方法时指定类型,也可以像普通方法一样调用它们。Java 编译器足够聪明,可以确定要使用的变量的类型,这种工具称为类型推断

Java 泛型有界类型参数

假设我们想要限制可以在参数化类型中使用的对象类型,例如在比较两个对象的方法中,我们想要确保接受的对象是 Comparables。要声明有界类型参数,请列出类型参数的名称,然后是 extends 关键字,然后是其上限,类似于下面的方法。

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

这些方法的调用类似于无界方法,只是如果我们尝试使用任何不是 Comparable 的类,它将抛出编译时错误。

有界类型参数可以与方法以及类和接口一起使用。

Java泛型也支持多个边界,即<T extends A & B & C>。在这种情况下,A 可以是接口或类。如果 A 是类,那么 B 和 C 应该是一个接口。我们不能在多个边界中拥有多个类。

Java 泛型和继承

我们知道如果 A 是 B 的子类,Java 继承允许我们将变量 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<String> myClass1 = new MyClass<String>();
        MyClass<Object> myClass2 = new MyClass<Object>();
        //myClass2=myClass1; // compilation error since MyClass<String> is not a MyClass<Object>
        obj = myClass1; // MyClass<T> parent is Object
    }

    public static class MyClass<T>{}

}

我们不允许将 MyClass 变量分配给 MyClass 变量,因为它们是不相关的,实际上 MyClass parent 是 Object。

Java 泛型类和子类型

我们可以通过扩展或实现泛型类或接口的子类型。一个类或接口的类型参数与另一个类或接口的类型参数之间的关系由 extends 和 implements 子句确定。

例如,ArrayList 实现了继承 Collection 的 List,因此 ArrayList 是 List 的子类型,而 List 是 Collection 的子类型。

只要我们不更改类型参数,子类型关系就会保留,下面显示了多个类型参数的示例。

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

List 的子类型可以是 MyList<String,Object>、MyList<String,Integer> 等。

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 和 List 不相关,这是上限通配符有用的时候。我们使用泛型通配符与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泛型无界通配符

有时我们希望我们的泛型方法适用于所有类型,在这种情况下,可以使用无界通配符。它与使用 <? 扩展对象>。

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

我们可以为printData方法提供 List 或 List 或任何其他类型的 Object 列表参数。与上限列表类似,我们不允许向列表添加任何内容。

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 中使用泛型?

泛型提供强大的编译时类型检查并降低 ClassCastException 和对象显式转换的风险。

12.2) 泛型中的 T 是什么?

我们使用 来创建泛型类、接口和方法。当我们使用它时,T 被替换为实际类型。

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

通用代码确保类型安全。编译器使用类型擦除在编译时删除所有类型参数,以减少运行时的重载。

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

  • 泛型不支持子类型,所以List<Number> numbers = new ArrayList<Integer>();不会编译
  • 我们不能创建泛型数组,所以List<Integer>[] array = new ArrayList<Integer>[10]不会编译,

这是所有的Java泛型,Java泛型是一个非常庞大的题目,需要大量的时间来了解和有效地使用它。这篇文章试图提供泛型的基本细节,以及我们如何使用它来扩展我们的程序的类型安全。

赞(0) 打赏
未经允许不得转载:IDEA激活码 » Java泛型 详解——泛型方法、类、接口

一个分享Java & Python知识的社区