看了这篇Java 泛型通关指南,再也不怵满屏尖括号了

8,198 阅读8分钟

本文为掘金社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

在前面介绍 Java 集合框架里的各种容器的时候,我们已经接触到泛型了,那时我们对泛型的简单理解是,类似这样 ArrayList 声明一个 ArrayList 实例,就给它做了个类型限制,让能让它其中只能放入 String 类型的元素。泛型在 Java 中的应用相当广泛,所以我们特地开辟一个章节专门讲讲 Java 泛型的工作原理和应用。

泛型的英文名叫做 generics,一系列和泛型相关的名词都是以 generic 为前缀的,比如我们马上要学习的 Generic Method,Generic Types 等。

本文的内容大纲如下:

初识泛型

其实在使用 ArrayList 的时候我们已经接触过泛型了。如果不使用泛型在创建 ArrayList 实例时指定其元素的具体类型,那么 ArrayList 可以放入任何对象(Object的实例),但是 Java 编译器只知道 List 中存放的是 Object 实例。因此,在使用时需要将它们转换为对象所属的具体类(或接口)。

List list = new ArrayList();   // 没有泛型类型约束,列表元素默认是Object类型的

list.add(new String("First MyObject"));

MyObject myObject = (String) list.get(0);  // 使用列表元素时需要进行类型转换

for(Object obj : list){
    // 使用前进行类型转换
    String theMyObject = (String) anObject;

   ......
}

使用 Java 泛型可以限制插入到 ArrayList 中的对象类型,且从 ArrayList 中检索对象时无需再做类型转换。

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

list.add(new String("First MyObject"));

String myObject = list.get(0);

for(String obj : list){
   ......
}

泛型的语法就是在支持泛型的类型上,给出类型的定义。List 接口是支持泛型的,类型就是List里允许的元素的类型。创建 List 实例和引用的时候,都可以指定泛型对应的类型,比如这个例程里告诉编译器 List 元素的类型是 String。如果不指定,那就是Object这个默认类型,所以第一个例程用的 List 的元素就是 Object 类型。

泛型定义

“Java 泛型”这个技术术语,表示一组与泛型类型和方法的定义和使用相关的语言特性。在 Java 中,泛型类型(Generic Typs)或泛型方法(Generic Method)与常规类型和方法的不同之处在于它们具有类型参数。

通过提供替换形式类型参数的实际类型参数,泛型类型被实例化,形成参数化类型。像 LinkedList 这样的类是一个泛型类型,它有一个类型参数 E 。实例,例如 LinkedList 或 LinkedList,被称为参数化类型,而 String 和 Integer 是各自的实际类型参数。

为什么使用泛型

与上面 ArrayList 的例程相似,如果仔细了解 Java 集合框架中的类,会发现他们的方法大多都支持泛型类型参数,如果不使用泛型这些类提供的方法的参数和返回值都会是 Object 类型的。现在,在泛型这种形式中,它们可以将任何 Java 类型作为参数并返回相同的值。

程序员经常想指定一个集合只包含某种类型的元素,例如 Integer 或 String 或自己定义的类。在引入泛型以前的集合框架中,如果不添加额外的类型检查,就不可能拥有同类集合,且使用集合元素时还需要把元素转换回实际类型。引入泛型后,会在编译时自动在代码中添加参数的类型检查,这节省了我们编写大量不必要的代码。

通俗地说,在Java中泛型强制约束了类型安全。

如果没有这种类型的安全性约束,代码中可能隐藏着各种仅在运行时才能发现的BUG。使用泛型,能使它们在编译时就能被发现。

简单地说,泛型通过在编译时检测到更多错误来增加代码的稳定性。

现在我们对在 Java 中为什么使用泛型有了一个清晰的认识。下面了解它们在 Java 中的工作方式。当在源代码中使用泛型时实际会发生什么。

泛型的工作方式

泛型的核心是“类型安全”,那么究竟什么是类型安全呢?类型安全是编译器的一个保证:“如果在正确的地方使用了正确的类型,那么在运行时就不应该有任何 ClassCastException”。比如在 Java 中声明一个整数列表 List,那么 Java 会保证在编译时检测并报告任何将非整数类型插入到上述列表中的尝试。

让我们通过一个例子来理解下。

List<Integer> list = new ArrayList<Integer>();
 
list.add(1000);
 
list.add("lokesh"); // 编译错误

当你编写上面的代码并编译它时,你会得到以下错误:“The method add(Integer) in the type List is not applicable for the arguments (String)” 编译器警告过你 List 类型中的方法 add(Integer) 不适用于参数 (String)”。这正是泛型的目的,即类型安全。

泛型类型

现在我们对泛型的含义有了一些了解。现在开始探索围绕泛型的其他重要概念。首先来看一下泛型应用于源代码的各种方式。

泛型类和接口

泛型类的语法形式:

// <> 指定的参数称为类型参数。
class name<T1, T2, ..., Tn> { /* ... */ }

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。由尖括号(<>)分隔的类型参数部分跟在类名后面。它指定类型参数(也称为类型变量)T1,T2,...和 Tn

如果一个类声明了一个或多个类型变量,那么它就是个泛型类。这些类型变量称为类的类型参数。下面通过一个例子来理解这句话。

DemoClass 是一个简单的Java类,它有一个属性 t,属性的类型是 Object。

class DemoClass {
   private Object t;
 
   public void set(Object t) { this.t = t; }
    
   public Object get() { return t; }
}

这里我们希望一旦用某种类型初始化了类,类就只能用于该特定类型。例如如果我们希望类的一个实例持有类型为 String 的属性 t ,那么这个实例就应该只能设置和获取 String 类型的属性 t。

上面例子中由于我们已将属性 t 的类型声明为 Object,因此无法强制执行此限制,类的实例可以设置任何对象 到属性 t 上,并且可以从 get 方法期望任何返回值类型(因为所有 Java 类型都是 Object 类的子类型)。

为了强制 DemoClass 实例化后执行这种类型限制,我们可以在 DemoClass 的定义中使用泛型类型 :

class DemoClass<T> {
   //T stands for "Type"
   private T t;
 
   public void set(T t) { this.t = t; }
    
   public T get() { return t; }
}

这样声明后,可以确保 DemoClass 一旦实例化后,就不会被错误的类型误用。

DemoClass<String> instance = new DemoClass<String>();
instance.set("lokesh");   //Correct usage
instance.set(1);        //This will raise compile time error

泛型类型也适用于接口。让我们快速看一个例子来理解,泛型类型如何在 Java 的接口中使用。

//Generic interface definition
interface DemoInterface<T1, T2> 
{
   T2 doSomeOperation(T1 t);
   T1 doReverseOperation(T2 t);
}
 
//A class implementing generic interface
class DemoClass implements DemoInterface<String, Integer>
{
   public Integer doSomeOperation(String t)
   {
      //some code
   }
   public String doReverseOperation(Integer t)
   {
      //some code
   }
}

泛型方法

泛型方法是引入其自己的类型参数的方法。泛型方法可以是普通方法、静态方法以及构造方法。 泛型方法语法形式如下:

public <T> T func(T obj) {}

是否拥有泛型方法,与其所在的类是否是泛型类没有关系。如果一个方法声明里使用了泛型类型参数,那么这个方法就是泛型方法。泛型方法与泛型类非常相似。它们的不同之处仅在于类型信息的范围仅在方法(或构造函数)内部。

下面是一个泛型方法的代码示例,该方法可用于仅在该类型的数组 T[] list 中查找类型参数 T item 的出现次数。

package com.example.learngenrics;

import java.util.ArrayList;

public class GenericMethodDemo{
    public static <T> int countAllOccurrences(ArrayList<T> list, T item) {
        int count = 0;
        if (item == null) {
            for ( T listItem : list )
                if (listItem == null)
                    count++;
        }
        else {
            for ( T listItem : list )
                if (item.equals(listItem))
                    count++;
        }
        return count;
    }

    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("123");
        list.add("abc");
        list.add("123");

        System.out.println(countAllOccurrences(list, "123"));

        //System.out.println(countAllOccurrences(list, 123)); // 会导致编译错误。
       // 如果 list 的类型是 T[] 则不会,这容易掉进坑里,最好不用
    }
}

使用泛型方法的时候,通常不必指明类型参数,因为编译器会为我们找出具体的类型。这称为类型参数推断(type argument inference)。类型推断只对赋值操作有效,其他时候并不起作用

关于泛型数组:在 Java 中,在运行时在数组中推送任何不兼容的类型将抛出 ArrayStoreException。这意味着数组在运行时保留其类型信息,而泛型在运行时使用类型擦除,由于上述冲突,不允许在 Java 中实例化泛型数组。

类型通配符

在泛型代码中,“?” 问号通配符表示未知类型。例如 List<?> 在逻辑上是 List ,List 等所有 List<类型实参> 的父类。

无界通配符

"<?>" 叫做无界通配符,意味着对类型实参没有任何限制。

ArrayList<?>  list = new ArrayList<Long>();  

ArrayList<?>  list = new ArrayList<String>();  

ArrayList<?>  list = new ArrayList<Employee>();  

可以使用上界通配符来缩小类型参数的类型范围。

上界通配符

假设您想编写一个适用于 List、List 和 List 的方法,就可以通过使用上限通配符来实现这一点,把方法的参数类型声明成 List<? extends Number>,Integer、Double 是 Number 类的子类。所以通俗地讲,如果希望泛型表达式接受特定类的所有子类,就使用上界通配符。

import java.util.Arrays;
import java.util.List;

public class GenericUpperWildcardDemo
{
    public static void main(String[] args)
    {
        // Integer 类型的列表
        List<Integer> ints = Arrays.asList(1,2,3,4,5);
        System.out.println(sum(ints));

        // Double 类型的列表
        List<Double> doubles = Arrays.asList(1.5d,2d,3d);
        System.out.println(sum(doubles));
		// String 类型的列表
        List<String> strings = Arrays.asList("1","2");
        // 会造成编译错误,因为sum 方法接受的参数类型是<? extends Number>
        // String 并不是 Number 的子类
//        System.out.println(sum(strings));

    }

    // 参数接收所有Number子类类型的列表
    private static Number sum (List<? extends Number> numbers){
        double s = 0.0;
        for (Number n : numbers)
            s += n.doubleValue();
        return s;
    }
}

下界通配符

如果想把泛型的参数类型限制为指定类型或者其超类类型,那么需要使用下界通配符。 它的语法形式为:<? super 指定类型>,比如 <? super Number> 就代表类型实参可以是 Number类或者其父类。

import java.util.List;

public class GenericsLowerBoundedWildcardDemo {
    public static void addNumbers(List<? super Integer> list) {
        for (int i = 1; i <= 5; i++) {
            list.add(i);
        }
    }

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        addNumbers(list);
        System.out.println(Arrays.deepToString(list.toArray()));
    }
}
// Output:
// [1, 2, 3, 4, 5]

泛型不被允许的那些事儿

类的静态属性不能使用泛型类型

不能在类中定义泛型参数化的静态属性。任何这样做的尝试都会产生编译时错误:Cannot make a static reference to the non-static type T.

public class GenericsExample<T>
{
   private static T member; //This is not allowed
}

不能创建类型参数的实例

任何创建 T 实例的尝试都将失败,抛出编译错误 Cannot instantiate the type T

public class GenericsExample<T>
{
   public GenericsExample(){
      new T();
   }
}

泛型的类型参数不能是值类型

泛型的类型参数不能使用 int double 这样值类型,需要使用对应的包装类 Integer, Double 等。

final List<int> ids = new ArrayList<>();    // 不允许
 
final List<Integer> ids = new ArrayList<>(); 

不能创建泛型的异常类

public class GenericException<T> extends Exception {}

总结

关于 Java 泛型,这篇文章把能用到的知识点差不多都梳理了一遍,相信学完这一章在看使用 Collection 的代码的时候,或者开发项目时看到代码里各种尖括号,甚至尖括号再给你套两层尖括号的代码时,就不会觉得很懵了。

前情提要

Java 集合框架里最常用的 Java 容器以及泛型已经梳理完毕,不过这大块的内容只能算刚刚完结上半部分,与各个容器操作密切相关的还有 Stream 和 Lambda 这些知识,后面两篇会对这些进行讲解,敬请期待下次更新。