前言
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)
有时, 类或方法需要对类型变量加以约束。下面是一个典型的例子。 我们要计算数组中 的最小元素:
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>
这里分享两篇关于通配符的博客:
本文就不过多赘述