前言:知人者智,自知者明;胜人者力,自胜者强。 ——《道德经》
一、概述
Java 中的泛型
在 Java 中,其实所谓的泛型就是类型的参数化。如果方法有入参,那么这些入参面前往往会有类型,这个类型就是修饰参数所用。假如我们在创建类型的时候,也为其指定了参数,这个参数又是个类型,这种我们就称为泛型。
public void addList(Collection<T> list) {//T表示可传入任何类型表示
//TODO
}
使用泛型能够像传递参数一样传递类型,同时保证运行时的类型安全。类型的传递能满足各种类型的调用,类型安全是指编译器在编译期间会对泛型信息进行检查,只有符合规范才能通过编译,可以有效避免了运行时异常。这就是使用 Object 相比(所有类型都可以使用基类 Object 表示)泛型的优势所在。
//未使用泛型
Map map = new HashMap();
map.put("key", 10);
Integer i = (Integer) map.get("key"); //正确,返回的就是Integer类型
//String str = (String) map.get("key"); //错误,运行时会报异常,map存放的值实际是Integer类型,不是String类型
//使用泛型
Map<String, Integer> strMap = new HashMap<>();
strMap.put("key", 10);
Integer i2 = strMap.get("key"); //正确,不用考虑类型转换
//String str2 = strMap.get("key"); //错误,编译不通过,strMap的值只能是Integer类型,其他类型会报错
Java 中支持类泛型,也支持方法泛型:
//类泛型
public class Dragon<T> {//创建类的时候,通过<T>为其指定了类型参数T
public void share(T t) {
//TODO
}
}
//方法泛型化
public class Tiger {
public <T> void invite(T t) {//声明方法的时候,为其指定了类型参数T
//TODO
}
}
上面的 T
可以传入任何类型表示,相当于一个入参,只不过这个入参是个类型而已。以确定下面使用的参数是具体那种类型。
泛型的限制
了解泛型的用法后,我们直接使用 JDK 提供的泛型库来演示下泛型的限制:
List<String> strings = new ArrayList<>();
List<Object> objects = strings;//报错,List<String>不是List<Object>的子类
error:(IntelliJ says)
Incompatible types.Required:List<java.lang.Object> Found:List<java.lang.String>
下面一步步推导,为什么出现上面的错误,以及背后的原因。
按道理来说,String 是 Object 的子类,一个集合中的元素都是 String 类型,该集合应该能被放到 Object 类型的集合中,然而 Java 却不允许我们这样做,为什么呢?
List<String> strings = new ArrayList<>();
List<Object> objects = strings;//假如允许这样赋值
objects.add(10);//将一个数值放入 objects 中
String string = (String) objects.get(0);//报错,运行时会抛出异常,不能整数转换为字符串
假如 List<Object> objects = strings;
成立,那么 objects.add(10)
也是成立的,但是 集合 objects 中取出的元素会被强转为 String 类型,实际上该元素是 Integer 类型的,这样在运行时就会报错。这就是 Java 不允许我们这样做的原因,为了保证运行时安全性,同时说明泛型不是可变的。
List<String> strings = new ArrayList<>();
List<Object> objects = new ArrayList<>();
objects.addAll(strings);//正确
上面最后一行的写法是正确的,将 List<String>
通过 addAll()
的方法添加到 List<Object>
中,按照上面的说法,这是不可能的。我们自定义一个例子来看看:
//定义一个泛型接口
interface IList<T> {
void addAll(IList<T> list);//定义addAll()方法,用于添加集合
}
//MyList提供IList的默认实现
class MyList<T> implements IList<T> {
@Override
public void addAll(IList<T> list) { … }
}
//调用
MyList<String> stringMyList = new MyList<>();
MyList<Object> objectMyList = new MyList<>();
//objectMyList.addAll(stringMyList); //编译错误
上面的最后一行代码居然报错了,依然是提示了类型冲突,那么为什么 Java 中 List 的 addAll()
可以,为什么呢?我们来看看 List 中 addAll()
的源码:
boolean addAll(Collection<? extends E> items);
可以看到 addAll()
中的泛型实际上是 <? extends E>
这个类型,而不是 <E>
,这就引出了Java 中的通配符 ?
和界限。
二、协变和逆变
看下面一段代码:
Number num = new Integer(10); // compile success
ArrayList<Number> list = new ArrayList<Integer>(); // compile error
error:(IntelliJ says)
Incompatible types.Required:List<java.lang.Number> Found:List<java.lang.Integer>
为什么 Number 对象可以有 Integer 对象实例化,而 ArrayList<Number>
不能由 ArrayList<Integer>
实例化?要解决这些问题,我们需要了解 Java 中的协变和逆变以及泛型的通配符用法。
说逆变协变之前先来了解一下 Liskov替换原则
(Liskov Substitution Principle, LSP)
Liskov替换原则
LSP由Barbara Liskov于1987年提出,其定义如下:
所有引用基类(父类)的地方必须透明地使用其子类对象。
LSP包含一下四层含义:
- 子类完全拥有父类的方法,且具体子类必须实现父类的抽象方法;
- 子类中可以增加自己的方法;
- 当子类覆盖或者实现父类方法时,方法的形参要比父类方法的更为宽松;
- 当子类覆盖或者实现父类方法时,方法的返回值要比父类的更严格。
前面的两层含义比较好理解,后面的两层含义后面会讲解到。
类型转换
协变和逆变主要用来描述类型转换后的继承关系,如果 A、B 表示参数类型,f(⋅) 表示类型转换,≤ 表示继承关系(比如 A ≤ B 表示 A 是 B派生出来的子类)
- 当 f(⋅) 是协变时,如果 A ≤ B,则 f(A) ≤ f(B);
- 当 f(⋅) 是逆变时,如果 A ≤ B,则 f(B) ≤ f(A);
- 当 f(⋅) 是不变时, A ≤ B 时上面两个式子均不成立,即 f(A) ≤ f(B) 相护之间没有继承关系。
(1)泛型
简单来说,Integer 是 Number 派生出来的子类,如果是协变,ArrayList<Integer>
是 ArrayList<Number>
的子类型;如果是逆变,ArrayList<Intege >
是 ArrayList<Number>
的父类型;如果是不变的,则 ArrayList<Integer>
和 ArrayList<Number>
两者相护没有继承关系。
上面的例子中 ArrayList<Number> list = new ArrayList<Integer>()
编译报错,说明泛型是不变的。
(2)数组
数组是协变的,如下:
Number[] numbers = new Integer[0];
(3)方法
static Number method(Number num) {
return 1;
}
//调用
Object result1 = method(new Integer(1)); // compile success
Number result2 = method(new Integer(1)); // compile success
Number result3 = method(new Object()); // compile error 传入参数必须为Number或其子类型
Integer result4 = method(new Integer(0)); // compile error 返回值类型必须为Number或其超类型
调用方法 result = method(number);
,根据Liskov替换原则,传入形参 number 的应为方法 method(Number num)
形参的Number 类型或者是Number的子类型,返回结果的类型 result 应为 Number method(Number num)
返回值类型 Number 类型或者 Number 的超类型。
class Base {
Number method(Number n) { ... }
}
class Child extends Base {
@Override
Number method(Number n) { ... }
}
在 Java1.4中,子类覆盖(Override)父类方法时,形参和返回值类型必须保持与父类的一致。
class Child extends Base {
@Override
Integer method(Number n) { ... }
}
从 Java1.5开始,子类覆盖(Override)父类方法时,允许协变返回更为具体的类型。
三、泛型中的通配符
Java 中的泛型是不变的,如果需要实现逆变和协变,怎么办呢?这时就需要用到 通配符 ?
了。
Java 的通配符有两大类:
- 子类限定通配符:<? extends E>
- 父类限定通配符: <? super E>
通配符方式不同于普通的类型变量声明,它不能进行类型推断,实际上就限制了数据的写入和读取。那么需要明确的告诉编译器数据的上界和下界,否则会报错。
实现泛型协变和逆变主要是 extends 和 super 关键字,例如:
< ? extends T>实现泛型协变
ArrayList<? extends Number> list = new ArrayList<Integer>();
<? super T>实现泛型逆变
ArrayList<? super Number> list2 = new ArrayList<Object>();
这里我们以下面的几个类的 super 和 extend 关系举个例子说明:
Student 是 Teacher 的子类,但是 List<Student>
不是 List<Teacher>
的子类,那么有没有办法使他们兼容,答案是有的,那就是协变和逆变。
Java 中所有类的顶级父类是 Object,同时可以认为 null
所有类的子类。
3.1 泛型协变 < ? extends T>
通配符 ?
的类型在 null 和 T 之间,编译器不能确定放入的是什么类型,null 可以转换为任意类型,也就是说,编译器不知道类型下界是什么,只清楚类型上界。如图:
类型的上界是 T ,参数化类型是 T 或 T 的子类。
public class Test {
private static class Person { }
private static class Teacher extends Person { }
private static class Student extends Teacher { }
public static void main(String[] args) {
List<? extends Teacher> list = new ArrayList<>();
//不能加人任何元素
list.add(new Person()); // compile error
list.add(new Teacher()); // compile error
list.add(new Student()); // compile error
Teacher teacher = teachers.get(0);// compile success
}
}
可以看到 List<? extends Teacher>
在添加 Person、Teacher、Student 都发生了编译错误,我们来看看 add()
方法的实现:
boolean add(E e)
存入数据:
在调用 add()
方法的时候,泛型 e 自动变成了<? extends Teacher>,其表示 list 所持有的类型为 Teacher 或 Teacher 的子类,但是并不知道这个类具体是什么类型,只好阻止向其中加人任何子类。为了类型安全,不能往使用了 <? extends E>
的数据结构中写入任何值(null除外)。
读取数据:
但是,由于编译器总是知道他是 Teacher 或 Teacher 的子类型,因此可以从中取出 Teacher 对象;
Teacher teacher = teachers.get(0);
对于<? extends T>来说,参数类型 ?
表示的是 T 以及 T 的子类型。如果允许我们向容器中添加元素,那么无法确定子类具体是什么类型,这样取出元素的时候就可能报类型转换异常,为了运行时安全,Java 禁止了元素写入。
为了能调用 add()
方法,可以使用 super 关键字实现,也就是逆变。
3.2 泛型逆变<? super T>
通配符 ?
的类型在 T 和 Object 之间,编译器很清楚放入类型的下界是 T,但是不知道上界具体是什么。
表示类型的下界限是 T,参数化类型可以是 T 或者 T 的超类。
public class Test {
private static class Person { }
private static class Teacher extends Person { }
private static class Student extends Teacher { }
public static void main(String[] args) {
List<? super Teacher> list = new ArrayList<>();
//Teacher 及其子类可以看做成 Teacher,从而添加成功
list.add(new Person()); // compile error
list.add(new Teacher()); // compile success
list.add(new Student()); // compile success
Object object = teachers.get(0);// compile success
}
}
存入数据:
其中 <? super T> 表示所持有的类型为 Teacher 或 Teacher 的超类。其中 Teacher 和 Student 必定为这某一类型的子类,因为编译器会自动向上转型,Teacher 以及子类可被认为是 Teacher 类型,所以添加 Teacher 以及子类元素成功;但是 Person 是 Teacher 的超类,由于编译器并不知道 List 的内容究竟是 Teacher 的哪个超类,因此不允许加入任何的超类型。
读取数据:
编译器不知道这个超类具体是什么类,只能返回 Object 对象,因为 Object 在 Java 中是任何类的祖先。
Object object = teachers.get(0);
对于<? super T>来说,参数类型 ?
表示的是 T 或者 T 的超类。如果是 T 的子类那么也是 T 超类的子类,所以将元素添加到容器是允许的,因为取出来一定符合 T 或者 T 的超类型。但是如果是 T 的超类型是不允许向容器添加元素的,因为无法确定 T 的超类具体是什么类型,取出元素的时候可能会引起类型转换错误。
从上面的例子可以看出,extends 确定了泛型的上界,super 确定了泛型的下界。
3.3 PECS原则
那么问题来了,究竟什么时候调用 extends 什么时候用 super ?
《Effective Java》给出了答案:
PECS 全称为 Producer-Extends-Consumer-Super。
这就是 PECS 原则,即描述了子类限定通配符和父类限定通配符的使用原则。
这里以类 Stack 为例:
//代码已简化
public class Stack<E> extends Vector<E> {
public Stack() {}
public E push(E item)
public E pop()
//遍历获取src中的元素
public void pushAll(ArrayList<E> src) {
for (E e : src)//获取
push(e);
}
//将Stack中的元素依次取出add到dst中
public void popAll(ArrayList<E> dst) {
while (!isEmpty())
dst.add(pop());//添加
}
}
(1)要实现 pushAll(Iterable<E> src)
方法,将 src 的元素逐一存入类 Stack 中:
Stack<Number> stack = new Stack<>();
stack.pushAll(new ArrayList<Float>()); // compile error,提示类型冲突
stack.pushAll(new ArrayList<Integer>()); // compile error,提示类型冲突
上面一个实例化 Stack<Number>
的对象 stack,所以类中的泛型 E 表示 Number。src 有 ArrayList<Float>
和 ArrayList<Integer>
,在调用 pushAll()
方法时提示类型冲突错误,因为 Java 中的泛型是不可变的,ArrayList<Float>
和 ArrayList<Integer>
都不是 ArrayList<Number> src
的子类,因此需要使用 extends :
public void pushAll(ArrayList<? extends E> src) {//作为E生成器的参数的通配符类型
for (E e : src)//获取
push(e);
}
泛型协变后,ArrayList<Float>
和 ArrayList<Integer>
都是 ArrayList<Number> src
的子类,编译成功。
(2)要实现 popAll(ArrayList<E> dst)
方法,将类 Stack 中的元素依次取出 add()
到 dst 集合中,如果不用通配符实现:
Stack<Number> stack = new Stack<>();
stack.popAll(new ArrayList<Object>()); // compile error,提示类型冲突
传入的集合 dst 为 ArrayList<Object>
类型,调用 popAll()
方法时会提示类型冲突错误,因为 ArrayList<Object>
不是 ArrayList<Number>
的超类,所以需要使用super:
public void popAll(ArrayList<? super E> dst) {//作为E使用者的参数的通配符类型
while (!isEmpty())
dst.add(pop());//添加
}
泛型逆变后,ArrayList<Object>
是 ArrayList<Number>
的超类,编译成功。
上面的例子类 Stack 中,在调用 pushAll()
方法时 src 产生 E 实例(生产者场景),在调用 popAll()
方法时 dst 消耗 E 实例(消费者场景),PECS被称为Get and Put Principle(取放原则)。
其实 java.util.Collections 的 copy()
方法(JDK1.7)完美地诠释了 PECS:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
PECS原则总结:
- <? extends E>子类限定通配符:从泛型类读取类型 T 的数据,并且不能写入,用于生产者环境(Producer);
- <? super E>父类限定通配符: 从集合中写入类型 T 的数据,并且不需要读取,用于消费者场景(Consumer);
- 如果既要存数据又要取数据,那么通配符无法满足需求。
注意:如果使用生产者对象,例如 List<? extends Foo>,你不允许再这个对象上 add() 或者 set() ,但并不意味着这个对象是不可变的,例如:没有什么可以阻止你调用 clear() 来从列表中删除所有元素,因为 clear() 根本不接受任何参数。通配符(或其他类型的型变)唯一能保证的是类型安全性。不变性则完全是另外一个回事。
点关注,不迷路
好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是人才。
我是suming,感谢各位的支持和认可,您的点赞、评论、收藏【一键三连】就是我创作的最大动力,我们下篇文章见!
如果本篇博客有任何错误,请批评指教,不胜感激 !
要想成为一个优秀的安卓开发者,这里有必须要掌握的知识架构,一步一步朝着自己的梦想前进!Keep Moving!