泛型
泛型就是泛化类型,又被称为参数化的类型,也就是将类型像是参数一样的进行传递,提高程序的复用性。
泛型在使用之间一定要提前声明,就像声明一个形参一样。
泛型常用于容器类中
,因为一般一个容器可以装很多种类型的东西,一个碗里可以装西红柿鸡蛋面
,也可以装宫保鸡丁
,所以使用泛型就可以让容器拥有这种匹配多种类型内容的能力,这也是java中多态的体现。当然泛型也可以用在很多其他的地方,在java中提供的函数式编程api中也大量使用了泛型,因为其根本目的
就是建立一个类型的模板
。
泛型类
泛型类在创建对象的时候才能确定泛型的具体类型
泛型类的格式:
public class 类名<泛型> {
}
例如:
// 表示一个Box里面可以装的东西的类型是 T,但是这个T是个形参,具体类型是个啥得等到创建这个Box的对象的时候才能知道
public class Box<T> {
}
泛型接口
泛型接口在其实现类创建对象时才能确定泛型的具体类型
泛型接口的格式:
public interface 接口名<泛型> {
}
例如:
// 泛型接口的定义形式上和泛型类差不多,这其实是java中为一个类提供比较能力的接口,也就是让实现了这个接口的类的对象之间可以互相比较大小,比如这个T为Man时,就可以依靠某种策略来比较这些Man的大小了。
public interface Comparable<T> {
}
泛型方法
泛型方法在创建对象时不需要泛型的具体数据类型,而是在调用方法时确定具体类型
修饰符 <泛型> 返回值类型 方法名 () {
}
例如:
// 泛型参数的声明列表位于修饰符public和返回值类型之间,这个泛型参数声明列表其实形式上是和泛型类或者泛型接口上的那个列表是一样的,都是 <泛型1, 泛型2, 泛型3>这种类型
public <T> T method (T t) () {
}
注:有一点需要注意的是,泛型类中的静态泛型方法是不能使用到泛型类上声明的泛型的,而是只能使用泛型方法上声明的泛型,原因就是泛型类上的泛型只有在这个泛型类实例化的时候才能确定,而很明显静态方法是不依赖于实例的,只是依赖于类,所以如果要使用静态泛型方法的话,其中用到的所有泛型都必须要声明在方法上。
例如:
public class ATool<T> {
// 在这个静态泛型方法中能够使用的泛型只有在方法泛型声明列表中声明的E,而在类泛型声明列表中声明的T是不能用的
public static <E> void method(E e) {
}
}
泛型通配符
java中的泛型是只存在在编译时期的,编译完成之后就会进行泛型信息的擦除。所以在java中泛型是不允许
协变
和逆变
的,也就是说不能以List<Animal>
类型来接收List<Cat>
类型的参数,这两种类型虽然都是List
,并且传入的具体泛型类型也具有继承
关系,但是两者是两个完全不同的类型。
如果不能解决这个问题会违反了java中多态的思想,所以引入了
泛型通配符
,来解决这个问题,让以List<Aniaml>
类型为形参的方法可以接受List<Cat>
类型的实参,做为参数,扩大方法参数的接收范围。
泛型通配符的符号是
?
,?
是一个实参而并不是一个形参,这是非常重要的,它其实代表的是一个未知的具体的参数,而不是一个形式参数。
泛型通配符的接收范围是不受限制的,比如
List<?>
可以接受List<任何类型>
的参数,这样接收范围就太大了,在大多数条件下,都需要对其加以限制,让这个参数只能接收和某种类型
有上下级
关系的参数,从而引入了泛型的上限
和下限
。
泛型通配符的上限:
- 格式:
类型名称 <? extends 某种类型> 对象名称
- 意义:只能接收该类型及其子类类型
- 助记:extends是继承的关键字,? 继承了 某种类型的范围
泛型通配符的下限:
- 格式:
类型名称 <? super 某种类型> 对象名称
- 意义:只能接收该类型及其父类类型
- 助记:super是超级的意思,在java中也表示的是父类,虽然super是一个形容词、副词、名词,就是没有动词的意思,但是我们可以稍微引申一下,表示 ? 超越了 某种类型的范围,是这种类型或者其父类
例如:
/**
* 当使用带上限的泛型通配符时,传入的对象不能对该类型进行输入操作, 只能进行输出操作。
* @param l 一个List,里面装的东西只能是萝卜或者其子类
*/
public void test (List<? extends Radish> l) {
// 这个add方法调用在编译时会报错
l.add(r);
// 这个get方法可以正常调用
Radish r = l.get(0);
}
/**
* 当使用带下限的泛型通配符时,传入的对象不能对该类型进行输出操作,只能进行输入操作(其实可以输出为Object,因为Object是所有类的父类,但是设计上一般不考虑)。
* @param l 一个List,里面装的东西只能是萝卜或者其父类
*/
public void test2 (List<? super Radish> l) {
// 这个add方法可以正常调用
l.add(1);
// 这个get方法只能返回Object类型的对象
Radish r = l.get(0);
}
/**
* 这个方法的参数使用了不加任何限制的通配符,可以传入装有任何类型对象的List
*/
public void test3 (List<?> l) {
}
从上面那个例子可以看出,使用泛型的
上限
和下限
在扩大了方法参数的接收范围之外又引入了新的问题,那就是test1中List的add方法不能用了,test2中List的get方法不能用了(因为只能返回Object类型,所以认为它不能用了)
这种现象被称为java泛型的
PECS法则
,意思是Producer Extends, Consumer Super
,也就是说把使用了通配符上限的那个对象当做了生产者
,把使用了通配符下限的那个对象当做了消费者
,很明显生产者的定义就是只能读不能写的,而消费者的定义就是只能写不能读的。所以造成了这种情况。
那么为啥会这样呢,来想一下。如果你在一个超市里面负责把东西摆放到正确的位置,你有一个任务是把所有的白萝卜和青萝卜摆放到萝卜区规定的位置,而萝卜区又位于蔬菜区里面,test1方法的作用是把白萝卜放到某个区域里面,而test1方法的参数就是你要放的区域,而这个区域表示里面能放的东西是一个萝卜或者其子类,而青萝卜白萝卜都是其子类。
而你是一个瞎子,只知道这块区域是放萝卜或者其子类的,别的一概不知
,如果你在test1方法中通过add方法往这块区域里怼白萝卜,但是其实这个区域是放青萝卜的地方,那么顾客们就会很迷惑,所以你的老板(编译器),不同意你这么干,就会揍你一顿(编译期报错,防止问题留到运行期)
。但是很明显如果你的工作是给顾客拿萝卜,顾客想随便买一个什么萝卜回家去炒一下吃了,test1方法的作用是从某个区域里拿出一个萝卜,而这块区域是一个萝卜或者其子类,也就是你从这块区域里拿到的不管是青萝卜还是白萝卜,它都是一个萝卜,顾客拿到了一个萝卜就开心的走了。
而看看另一个区域吧,也就是test2方法的参数,这个区域只能放萝卜或者萝卜的父类,如果它是一个蔬菜区,你的老板觉得里面放的是个菜就行,
而你依然不知道这块地方具体是啥区,只知道这个地方是放萝卜或者萝卜的父类的
你尽情的往里面通过add方法放你的白萝卜或者是青萝卜,都没有问题,只要它是萝卜的子类,那必定也是蔬菜的子类,反正我只放萝卜和萝卜的子类,这样保守的工作总没有错误。但是如果顾客让你拿一个萝卜,这可坏了,因为你不知道这块地方具体是放啥的,你觉得这个地方有可能一块蔬菜区,那要是你给客人拿东西,拿成了一个茄子,那客人将会把你投诉。你心里想着“我可干不了这活”,所以你装成聋子拒绝了(同样是编译器报错),但是这个时候如果客户让你随便拿个啥东西(Object)
,你一听,这我行啊,反正啥东西都行,于是从这块区域随便摸了个东西给了顾客,顾客满意的走了,并且将你投诉。
以上的这个过程可以抽象为:
算了,不会画图,就这样吧,总之,这个地方将多态体现的淋漓尽致,要记住这个地方需要符合
PECS法则
,这其实就是将在运行期可能发生的错误提前到了编译期,从而提前预防了这个错误。重点
是如果泛型参数传入? extends T
的类对象不能使用其中参数中包含T类型的方法
,如果泛型参数传入? super T
的类对象不能使用其中返回值中包含T类型的方法
,而在kotlin
中表述为out T
和in T
将这种思想表达的更清楚。
举一个具体的例子吧:
/**
* 这个方法是 Collections类 中的 sort方法
* 来看一下这个泛型方法的泛型声明列表。传入的类型T,需要是Comparable的实现类,这是为了保证List中的对象可以被比较。而Comparable中泛型需要是类型T或者其父类,其实这里和父不父类没有关系,只是为了让其中的方法可以输入T类型的值而已。
*/
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
/**
* 这是Comparable接口的定义,如上文所说 Comparable<? super T>,是为了上文是为了让compareTo方法可以输入。
*/
public interface Comparable<T> {
public int compareTo(T o);
}
所以通过上面的sort方法就可以实现List的排序了。先这样吧。