目录
一、概念:
普通的类和方法只能使用特定的类型:基本数据类型或类类型。如果编写的代码需要应用于多种类型,这种严苛的限制对代码的束缚就会很大。
- 多态是一种面向对象思想的泛化机制。你可以将方法的参数类型设为基类,这样的方法就可以接受任何派生类作为参数,包括暂时还不存在的类。这样的方法更通用,应用范围更广。在类内部也是如此,在任何使用特定类型的地方,基类意味着更大的灵活性。除了
final
类(或只提供私有构造函数的类)任何类型都可被扩展,所以大部分时候这种灵活性是自带的。 - 拘泥于单一的继承体系太过局限,因为只有继承体系中的对象才能适用基类作为参数的方法中。如果方法以接口而不是类作为参数,限制就宽松多了,只要实现了接口就可以。这给予调用方一种选项,通过调整现有的类来实现接口,满足方法参数要求。接口可以突破继承体系的限制。
- 即便是接口也还是有诸多限制。一旦指定了接口,它就要求你的代码必须使用特定的接口。而我们希望编写更通用的代码,能够适用“非特定的类型”,而不是一个具体的接口或类。
- 这就是泛型的概念,是 Java 5 的重大变化之一。泛型实现了参数化类型,这样你编写的组件(通常是集合)可以适用于多种类型。“泛型”这个术语的含义是“适用于很多类型”。编程语言中泛型出现的初衷是通过解耦类或方法与所使用的类型之间的约束,使得类或方法具备最宽泛的表达力。
二、泛型使用:
1、泛型方法(<E>):
你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
反例:
public static Set union(Set s1, Set s2){
Set result=new HashSet(s1);
result.addAll(s2);
return result;
}
正例:
public static<E> Set<E> union(Set<E> s1,Set<E> s2){
Set<E>result= new HashSet<>(s1);
result.addAll(s2);
return result;
}
2、泛型类<T>:
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
public class Box<T>{
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
}
三、编写泛型
参数化声明并使用JDK提供的泛型类型和方法通常不会太困难。但编写自己的泛型类型有点困难,通常来说,泛型类一般用在集合类中,例如ArrayList<T>
,我们很少需要编写泛型类。如果我们确实需要编写一个泛型类,那么,应该如何编写它?
首先,按照某种类型,例如:String
,来编写类:
public class Stack<String> {
private String[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack(){
elements = (String[])new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(String e){
ensureCapacity();
elements[size++] = e;
}
public String pop(){
if(size==0) {
throw new EmptyStackException();
}
String result=elements[--size];
elements[size] = null; // Eliminateobsoletereference
return result;
}
private void ensureCapacity(){
if(elements.length==size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
然后,把特定类型String
替换为T
,并申明<T>
:
public class Stack<T> {
private T[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack(){
elements = (T[])new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(T e){
ensureCapacity();
elements[size++]=e;
}
public T pop(){
if(size==0) {
throw new EmptyStackException();
}
T result=elements[--size];
elements[size] = null; // Eliminateobsoletereference
return result;
}
private void ensureCapacity(){
if(elements.length==size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
熟练后即可直接从T开始编写。
小结:
- 编写泛型时,需要定义泛型类型
<T>
; - 静态方法不能引用泛型类型
<T>
,必须定义其他类型(例如<K>
)来实现静态泛型方法; - 泛型可以同时定义多种类型,例如
Map<K, V>
。
四、擦拭法
泛型是一种类似”模板代码“的技术,不同语言的泛型实现方式不一定相同。Java语言的泛型实现方式是擦拭法(Type Erasure)。所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。
我们编写了一个泛型类Stack<T> ,这是编译器看到的代码:
public class Stack<T> {
private T[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack(){
elements = (T[])new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(T e){
ensureCapacity();
elements[size++]=e;
}
public T pop(){
if(size==0) {
throw new EmptyStackException();
}
T result=elements[--size];
elements[size] = null; // Eliminateobsoletereference
return result;
}
private void ensureCapacity(){
if(elements.length==size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
而虚拟机根本不知道泛型。这是虚拟机执行的代码:
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack(){
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e){
ensureCapacity();
elements[size++]=e;
}
public Object pop(){
if(size==0) {
throw new EmptyStackException();
}
Object result=elements[--size];
elements[size] = null; // Eliminateobsoletereference
return result;
}
private void ensureCapacity(){
if(elements.length==size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
因此,Java使用擦拭法实现泛型,导致了:
- 编译器把类型
<T>
视为Object
; - 编译器根据
<T>
实现安全的强制转型。
使用泛型的时候,我们编写的代码也是编译器看到的代码,而虚拟机执行的代码并没有泛型,所以,Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T
视为Object
处理,但是,在需要转型的时候,编译器会根据T
的类型自动为我们实行安全地强制转型。
了解了Java泛型的实现方式——擦拭法,我们就知道了Java泛型的局限:
- 局限一:
<T>
不能是基本类型,例如int
,因为实际类型是Object
,Object
类型无法持有基本类型:
局限二:无法取得带泛型的Class,
换句话说,所有泛型实例,无论T
的类型是什么,getClass()
返回同一个Class
实例,因为编译后它们全部都是Stack<Object>
。
局限三:无法判断带泛型的类型:
局限四:不能实例化T
类型:
要实例化T
类型,我们必须借助额外的Class<T>
参数:
public class Tuple<T> {
private T first;
private T last;
public Pair(Class<T> clazz) {
first = clazz.newInstance();
last = clazz.newInstance();
}
}
五、通配符
1、<? extends T>
表示该通配符所代表的类型是T类型的子类,使用extends通配符表示可以读,不能写
public void pushAll(Iterable<? extends T> src){
for(E e : src)
push(e);
}
2、<? super T>
表示该通配符所代表的类型是T类型的父类,即使用super通配符表示只能写不能读。
public void popAll(Collection<? super T> dst){
while(!isEmpty())
dst.add(pop());
}
3、无限定通配符<?>
很少使用,可以用<T>替换,同时它是所有<T>类型的超类。既不能读,也不能写,那只能做一些null
判断。
static boolean isNull(Pair<?> p) {
return p.getFirst() == null || p.getLast() == null;
}
大多数情况下,可以引入泛型参数<T>
消除<?>
通配符:
static <T> boolean isNull(Pair<T> p) {
return p.getFirst() == null || p.getLast() == null;
}
使用extends
和super
通配符要遵循PECS原则。
- 如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)
- 如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)
- 如果既要存又要取,那么就不要使用任何通配符。