泛型概念
1、java 中的泛型是在JDK1.5 版本中引入的概念;所谓泛型,就是“适用于许许多多”类型的意思,它是"参数化类型"概念的具体实现办法。
2、“参数化类型”:有时候我们在编写工具类的时候,可能希望这个类能够兼容很多的类型,而不是具体的某一个类。这样的话我们的代码就会更加的简单和强大。特别是容器类,它可能即需要接收String类型,又需要接收Integer类型。所以参数化类型才会被引入到编程语言中,如C++、JAVA等都有参数化类型的具体实现。
3、在JAVA中,或许你会存在这样的疑问,那就是,如果单纯只是需要接收各种各样的类型,为什么不使用Object了;就如容器类的实现,如果我们使用Object,虽然它可以接收任何类型,但是这并不符合我们对某一个单一容器的预期,我们希望的是在某一个单一容器中只能保存一种数据类型(如String容器中不能接收Integer类型的对象),否则可能会增加程序出错的可能性。而且Object还涉及到一个类型强转的问题。
4、在JAVA中,泛型诞生的一个重要原因就是为了设计简洁的容器类;它也是多态特性的一个种要的体现。
5、并不是所有的代码都需要使用到泛型,只有当你的方法或者类足够复杂时,才推荐你去使用泛型特性。
泛型简单应用举例
为了体会泛型的强大之处,下面我们使用其实现一个元组。
所谓元组就是将许许多多不同的对象打包成一个单一的对象,可以打包2个对象的称为二元组,可以打包3个对象的称为3元组…;我们对元组的要求是初始化之后不能修改。
有时候,我们的方法需要返回多个不同的对象,这个时候我们就可以使用元组。
class TwoTuple<A,B>{
public final A first;
// 使用 final ,防止修改
public final B second;
public TwoTuple(A a, B b){
this.first = a;
this.second = b;
}
}
/**
* 通过继承实现三元组
* */
class ThreeTuple<A,B,C> extends TwoTuple<A,B>{
public final C third;
public ThreeTuple(A a, B b, C c){
super(a,b);
this.third = c;
}
}
通过上面的例子我们可以观察到,使用泛型就是使用字母表示类型暂时未指定(在使用的时候指定具体类型)的类型。
泛型接口
在JAVA中,泛型是可以应用于接口的。
下面我们通过泛型编写一个List集合的接口抽象,如下:
/**
* 1、构建集合
* */
interface ListTest<T>{
//往集合中增加对象
void add(int index, T element);
//根据集合下标返回对象
T get(int index);
}
我们可以看到,泛型接口的语法格式和泛型类是一模一样的。
泛型方法
到目前为止,我们看到的泛型,都是应用到整个类上面的;同样的,它也是可以应用到方法上面;
如果我们可以使用泛型方法解决问题,那么我们就应该尽可能的避免使用泛型类。泛型方法案例如下:
package com.java.basic.generic;
/**
*
* 泛型方法
* */
public class GenericDemo003 {
public static void main(String[] args) {
f("AAA");
f(2);
f(3.9);
}
/**
* 构造一个用于输出实例类名称的方法
* 1、只需要将泛型参数列表置于返回值之前
* */
public static <T> void f(T x){
System.out.println(x.getClass().getName());
}
}
泛型的实现原理(擦除)
首先我们来观察一下下面的这个案例:
package com.java.basic.generic;
import java.util.ArrayList;
public class GenericDemo010 {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
//输出为true ,表明他们的类型相同
//无法获取泛型参数类型的信息
System.out.println(c1==c2);
ArrayList<Integer> list1 = new ArrayList<>();
//编译错误,不能将字符串类型放入
// list1.add("223");
}
}
我们可以观察到 new ArrayList().getClass() 和 new ArrayList().getClass() 它们的结果居然是相等的,这个因为JAVA泛型是通过擦除来实现的。这就意味这在你使用泛型的时候,任何具体的类型信息都是被擦除的。诸如List都会被擦除为List,T 都会被擦除为Object。
那么为什么JAVA 要使用这种方式了?因为JAVA泛型是在JDK1.5之后引入的,它必须要兼容1.5之前的大部分代码(如果改写1.5版本之前的所有代码,这个代价将会是非常昂贵的)。这就要求当某个类库改写为泛型的时候,以前使用它的位置也必须是合法的。所以JAVA选择了类型擦除这种折中的方案。
那么通过擦除来实现泛型,会带来那些问题了?请观察下面两个编译错误:
class GenericLimit<T>{
//编译错误
// T t = new T();
public boolean instanceTest(Object obj){
//编译错误
// return obj instanceof T;
}
}
因为类型擦除的原因,泛型是不能应用于 new instanceof的,因为它们的类型被擦除掉了。
边界
因为擦除移除了类型信息,所有泛型参数只能调用Object的方法。那么,有没有什么办法让泛型参数调用Object之外的方法了?java提供了extends关键字,限制泛型参数可以调用某个类型的子集。这种方法在一定程度上是对擦除的一种补偿方式。
具体使用方式如下:
package com.java.basic.generic;
public class GenericDemo011 {
public static void main(String[] args) {
Solid solid = new Solid(new Iron());
//输出铁的参数
solid.print();
}
}
//颜色
interface HasColor{
String color();
}
//重量
interface Weight {
int weight();
}
//铁
class Iron implements HasColor,Weight{
@Override
public String color() {
return "白色";
}
@Override
public int weight() {
return 30;
}
}
//固体
//泛型擦除到HasColor 和 Weight
class Solid<T extends HasColor & Weight>{
T item;
public Solid(T item){
this.item = item;
}
// 输出立方图形参数
public void print(){
System.out.println("颜色: " + item.color());
System.out.println("重量: " + item.weight() + " KG!!");
}
}
通配符
Java泛型通配符(?)是Java中一种非常有用的技术,它可以用来指定泛型参数的类型,使程序员可以更容易地实现面向对象编程。
通配符(?) 可以结合 extends 和 super 搭配使用,以便更加精确的控制泛型参数的类型。
上边界extends:协变
下边界super:逆变
协变、逆变和不变
首先我们要搞明白什么是协变、逆变和不变;
Cat继承于Animal,Cat是Animal的子类型。Cat是对Animal功能的拓展,也就是说需要传入Animal的地方,也同样能够传入Cat。(里氏替换原则)
Cat与Animal如果单独拿出来用,继承关系非常明确。
但如果换一个环境,Cat[]与Animal[]是何关系? List与List是何关系?
协变和逆变就是用来描述这种本来满足继承关系的两个实体换一种更复杂的环境是否还满足继承关系。
如果依然满足原有继承关系,则称之为协变。
如果继承关系(准确点说是兼容方向)发生了反转,则称之为逆变。
如果转换后的两种结构没有任何关系,比如List与List互不兼容,则称之为不变。
协变数组
Java中的数组是协变的。
Object数组类型的引用可以指向Animal数组,Animal类型的数组可以存放Cat。
但如果尝试用Cat[] 类型的数组接收Animal对象,则会抛出异常ArrayStoreException。
class Animal{} //动物
class Cat extends Animal{} //猫
public class GenericDemo012 {
public static void main(String[] args) {
Animal[] arrAnimal = new Animal[2];
arrAnimal[0] = new Animal();
arrAnimal[1] = new Cat();
Object[] arrCat = new Cat[2];
arrCat[0] = new Animal(); // 发生ArrayStoreException 异常
arrCat[1] = new Cat();
}
}
普通泛型
普通泛型是不可变的
List<Animal> animals = new ArrayList<Cat>(); //编译错误
Java中的泛型信息在编译期间会被擦除,上述的写法无法通过语法检查。
通配符和extends、super
泛型看似好像将协变与逆变挡在了门外,但其实并不是一刀切,我们依然可以通过上下边界来拓展继承关系。
上边界extends:协变
如果我们需要保持继承关系,即List可以接收List之类的子类型集合。
public class GenericDemo012 {
public static void main(String[] args) {
List<? extends Animal> animals = new ArrayList<Cat>();
animals.add(new Animal()); //编译失败,不能添加
animals.add(new Cat()); //编译失败,不能添加
animals = new ArrayList<Cat>(){{ //ok
add(new Cat()) ;
add(new Cat()) ;
}};
}
}
class Animal{} //动物
class Cat extends Animal{} //猫
class SmallCat extends Cat{} // 小猫
可以看到,我们尝试向集合里添加数据的时候,都失败了,哪怕是最基本的Animal对象也不可以。这是为啥?
Animal的子类可能有Cat、Dog。Cat可以兼容Animal,Dog可以兼容Animal,但是问题在于Dog与Cat之间互不兼容,一个集合中可能出现两种甚至更多种类型的对象,是不安全的。所以干脆都不让添加元素了。
那这个上边界还有意义吗?
有,只要你能证明你传递的集合都是同一种类型的对象就可以了。看上图下方的两个例子,一个是单纯的赋值,另一个是方法传参。共同点在于我传递的是Cat类型的集合,确保集合里是同一种类型的对象,且Cat继承于Animal。
? extends Animal 表达的是,能从这个集合里面获取到的,都是Animal的子类。
下边界super:逆变
super有一个地方不是很好理解,网上很多的博客在这个地方都写的有点矛盾。
首先,<?super Cat >作为下边界, 表示的是只能接收Cat以及Cat父类的拓展数据类型的引用。
特地强调是拓展数据类型的引用,就拿集合来说:
List<? super Cat> cats = new ArrayList<>();
cats.add(new Animal()); //编译失败,不能添加
cats.add(new Cat()); //ok
cats.add(new SmallCat()); //ok
我向集合里添加了Cat的父类Animal,以及子类SmallCat,结果添加父类的时候报语法错误,子类却没事,说好的下边界呢?说好的只能添加Cat及Cat父类呢?
其实协变和逆变都只是针对于List、List这些拓展类型来说的,Animal与Cat本身的继承关系不受影响,依然是Cat继承于Animal。
List<? super Cat> cats1 = new ArrayList<Animal>(); //ok
List<? super Cat> cats = new ArrayList<>(); //ok
List<? super Cat> cats2 = new ArrayList<SmallCat>(); //编译错误
看上面的引用、参数传递,就知道super的具体作用了。
List<? super Cat> 类型的引用,只能指向 List<? super Animal>以及 List<? super Cat>类型的数据结构,无法指向 List<? super SmallCat>类型。
这才所谓的下边界。
SmallCat类型是Cat的子类型,所以Cat类型的引用可以指向SmallCat对象。
但是使用泛型后,List<? super Cat>类型的引用,就不能指向List类型的对象,但是可以指向List集合,这就是继承关系的倒置,也就是 “逆变”。
另外,使用super时,尝试获取容器内部元素时,默认的类型是Object,因为Cat最保险最顶层的基类就是Object。所以说使用super时,放便添加,但是不利于读取。
使用场合
总结一下上边界与下边界的优缺点
使用上边界(extends)时,无法添加元素,但是可以轻易的获取元素。(适合 读取)
使用下边界(super)时,可以很容易的添加元素,但是获取元素的时候只能用Object类型的引用接收。(适合写入)
其实也就是PECS原则(Producer Extends Consumer Super),生产者适合用extends,消费者适合用super。
关于PECS我们将在其它的文章中专门说明。
无边界通配符
//无边界通配符 <?>
List<?> arr1 = new ArrayList<>();
List arr2 = new ArrayList<>();
无边界通配符意味着可以接受"任何事物",其实大多数情况下编译器并不关心是无边界通配符还是原生类型;你可能认为无边界通配符只是一种装饰;但是它其实想表达的是这里使用的是某个具体的类型,而不是使用原生类型,只不过此具体类型未指定而已。
参考
<<java编程思想(第四版)>>