Java 泛型

689 阅读6分钟

前言

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。

泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

Java中的泛型类似于C ++中的模板。 这个想法允许(Integer, String...等类型以及用户自定义类型)成为method, class, interface 的参数。 例如,像HashSet, ArrayList, HashMap等类很好的使用了泛型。

为什么需要泛型

让我们设想一种场景,创建一个List,用于存储Integer

List list = new LinkedList<>();
list.add(new Integer(1));
Integer i = list.iterator().next();

但编译器会在最后一行报错:

编译器只能保证得到的是一个Object类型的对象,而我们需要的是一个Integer对象,因此需要显式的强制类型转换

Integer i = (Integer) list.iterator().next();

这种类型转换很烦人,因为我们明明知道list里面存的是一个Integer类型的对象,却还要去强转, 而且类型转换一旦出现错误,在编译期是无法发现的,但会在运行期出现java.lang.ClassCastException异常:

如果我们可以明确表达我们想要使用的类型,而编译器可以确保这种类型的正确性,则会容易很多。 这正是Java泛型背后的核心思想

让我们将第一行代码修改一下:

List<Integer> list = new LinkedList<>();

通过<Integer>我们将list的范围缩小到Integer类型,也就是说我们指定了list中的类型只能为Integer

通过型我们可以做到一下两点:

  • 使用泛型机制编写的程序代码要比那些杂乱的使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性
  • 泛型程序设计(Generic programming) 意味着编写的代码可以被很多不同类型的对象所重用

泛型介绍

泛型类

一个泛型类(generic class)就是具有一个或多个类型变量的类

public class Pairs<T> {
  private T first;
  private T second;
  
  public Paris() {}
  public Pairs(T first, T second) { this.first = first; this.second = second; }
  
  public T getFirst() { return first; }
  
  public void setFirst(T first) { this.first = first; }
  
  public T getSecond() { return second; }
  
  public void setSecond(T second) { this.second = second; }
}

Pair类引入了一个类型变量T, 用尖括号(<>)括起来,并放在类名的后面,泛型类可以有多个类型变量,例如,可以定义Paris类中第一个成员变量和第二个成员变量使用不同的类型参数

public class Paris<T, U> {
    private T first;
    private U second;
}

用具体的类型替换泛型类中的类型变量,就可以实例化泛型类型, 例如:

Paris<String> paris = new Paris<String>()
Paris<Integer> paris = new Paris<Integer>()
Paris<User> paris = new Paris<User>()
// ...传入你想使用的类

换句话说, 泛型类可看作普通类的工厂。

泛型方法

上面已经介绍了如何定义一个泛型类.实际上还可以定义一个带有类型变量的简单方法

public class Pairs {
  public static <T> T getMiddle(T... a) {
    return a[a.length/2];
  }
}

这个方法是在普通类中定义的, 而不是在泛型类中定义的。 然而, 这是一个泛型方法, 可以从尖括号(<>)和类型变量看出这一点。 注意, 类型变量T放在修饰符 (这里是 public static) 的 后面, 返回类型的前面

当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:

类型推断

在这种情况 (实际也是大多数情况)下, 方法调用中可以省略 <Integer> 类型参数。 编译 器有足够的信息能够推断出所调用的方法。也就是说,可以调用:

Pairs.getMiddle(1,2,3,4,5);

泛型限定(Bounded Generics)

有时, 类或方法需要对类型变量加以约束。下面是一个典型的例子。 我们要计算数组中 的最小元素:

但是这里有个问题? 变量smallest类型为T,这意味着它可以是任意类型的对象,怎么才能确定T所属的类有compareTo方法呢?

解决这个问题的办法是将T限制为实现了Comparable接口的类,可以通过给类型变量T设定限定(bounds)来实现这点:

  public static <T extends Comparable> T min(T... a) 

现在,泛型的min方法只能被实现了Comparable接口的类(如String, LocalDate等)的数组调用。

多重限定(Multiple Bounds)

一个类型变量或通配符可以有多个限定, 例如:

<T  extends Comparable &  Serializable>

限定类型用“&” 分隔, 而逗号","用来分隔类型变量(<T, U>)

类型变量可以有多个限定接口,但是只能有一个限定类,并且当有限定类存在时,限定类要在限定列表中的第一个

泛型擦除(Type Erasure)

我们下面看一个例子:

Class<?> class1=new ArrayList<String>().getClass();
Class<?> class2=new ArrayList<Integer>().getClass();
System.out.println(class1);		//class java.util.ArrayList
System.out.println(class2);		//class java.util.ArrayList
System.out.println(class1.equals(class2));	//true

我们看输出发现,class1和class2居然是同一个类型ArrayList,在运行时我们传入的类型变量String和Integer都被丢掉了。Java语言泛型在设计的时候为了兼容原来的旧代码,Java的泛型机制使用了“擦除”机制。

  • 如果类型变量没有限定, 擦除后用Object代替T
  • 如果类型变量有限定,查出后使用第一个限定的类型代替T
    如果切换限定: class Interval<T extends Serializable & Comparable>会发生什么?

如果这样做, 原始类型用 Serializable 替换 T, 而编译器在必要时要向 Comparable 插入强制类型转换。为了提高效率,应该将标签(tagging) 接口(即没有方 法的接口,Serializable接口就是个标签接口,没有任何方法)放在边界列表的末尾

通配符(Wildcards)

通配符用?问号表示,用于指代未知类型。

已知Object是Java中所有类型的超类型,但是List不是任何集合的超类型

例如,List<Object>不是List<String>的超类型,并且将List<Object>类型的变量分配给List<String>类型的变量将导致编译器错。 我们可以使用通配符来实现:

// 无界通配符
List<?>
// 上界通配符: 用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
List<? extends E>
// 下届通配符: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object
List<? super E>

这里分享两篇关于通配符的博客:

本文就不过多赘述