Java 泛型

Java 泛型是 Java 5 引入的一个重要特性,相信大多数 Java 开发者都对此不陌生,但是泛型背后的实现原理和类型擦除还是许多人依然不是很清楚。本文将介绍 Java 泛型的原理和使用,重点阐述容易产生困惑的通配符、类型擦除等问题。

1. Java 泛型

1.1 Java 泛型是什么?

Java 泛型,提供了参数化类型,并且提供了编译时强类型检查。泛型可以让我们很简单地支持不同类型,在 Java 集合框架中泛型广泛用以对类型的抽象。

1.2 Java 泛型的好处

  • 提供编译时的强类型检查。可以在编译时发现类型安全问题,不用等到运行时。

  • 避免类型转换。

看下面一段代码:

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // type cast to String

如果使用泛型的话,不需要类型转换:

List list = new ArrayList();
list.add("hello");
String s = list.get(0);   // no cast
  • 可以实现通用的算法。通用算法可以处理不同类型的集合,可以进行自定义,并且类型安全且易于阅读。

2. 泛型类型与泛型方法

2.1 泛型类型

泛型类型是指泛型类或泛型接口。为了理解泛型类型的概念,看下面这个例子。

先定义一个简单的 Box 类:

public class Box {
    private String object;

    public void set(String object) { this.object = object; }
    public String get() { return object; }
}

上面代码中的 Box 只能存放 String 类型的元素,如果想存放 Integer 等其他类型的元素,则必须重写另外一个 Box,代码不能复用。下面再看使用泛型后的 Box:

public class Box {
    // T stands for "Type"
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

现在 Box 可以存放除基本类型外的任何类型了。使用 T 类型代替 String 类型,按照惯例,类型参数名是一个大写字母,常见的类型参数名如下:

  • E - Element(在 Java 集合框架中广泛运用)

  • K - Key

  • N - Number

  • T - Type

  • V - Value

2.2 泛型类型的原始类型(Raw Types)

原始类型(Raw Types)是没有指定参数类型的泛型类或泛型接口。例如,对于上面提到的Box泛型类:

Box rawBox = new Box();

Box就是Box的原始类型,原始类型一般出现在旧版代码中,因为大量的 API 在 Java 5 之前不是通用的。原始类型和泛型类型也可以转换:

// generic type to raw type
Box stringBox = new Box<>();
Box rawBox = stringBox;               // OK
rawBox.set(8);  // warning: unchecked invocation to set(T)

// raw type to generic type
Box rawBox = new Box();           // rawBox is a raw type of Box
Box intBox = rawBox;     // warning: unchecked conversion

2.3 泛型方法

泛型方法是指有它们自己参数化类型的方法。类型参数在一对尖括号之间,并且在方法返回类型之前。

下面 Util 类有一个泛型方法:

public class Util {
    public static  boolean compare(Pair p1, Pair p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
}

public class Pair {

    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey()   { return key; }
    public V getValue() { return value; }
}

通常调用泛型方法的方式如下:

Pair p1 = new Pair<>(1, "apple");
Pair p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);

// 如果在 Java 7 以上版本,利用类型推断可以简写为
boolean same = Util.compare(p1, p2);

3. 有界类型参数 (Bounded Type Parameters)

很多时候我们都想限制参数类型的边界,例如在对比两个对象的方法中,想确保方法参数都是 Comparable 的。声明有界类型参数(Bounded Type Parameters
),格式为T extends Class & Interface1 & ... & InterfaceN

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

这样当我们传递的参数没有实现 Comparable 接口,会有编译时错误。有界类型参数同样可以用于泛型类和泛型接口中,而且支持多个边界,例如 ,只允许最多一个 Class 边界,而且如果有一个 Class 边界,Class 边界必须在最前面。

4. 通配符

Java 泛型中问号?是通配符,表示未知类型。通配符可以用于参数、属性、局部变量或返回值的类型,但是不能用于泛型方法调用或创建泛型类实例的类型参数。

4.1 无界通配符 (Unbounded Wildcards)

单独使用?表示无界通配符,例如List,表示未知类型的 list。下面两个场景适合使用无界通配符: