目录
一、泛型概述
泛型是JDK5引入的一个新特性,其提供了编译时类型安全检测机制。
其本质是参数化类型,即给类型指定一个参数,在使用时指定此参数具体的值,那么这个类型就可以在这时确定了。这种参数类型可以应用在类,接口和方法中,分别被称为泛型类,泛型接口和泛型方法。
二、为什么使用泛型
1. 保证了类型的安全性
想象一下,在日常开发中对List进行创建,如果没有使用泛型确定参数数据类型,那么代表我们可以写入任意类型的数据,在读取时无法保证参数类型,倘若插入了错误的数据类型对象,运行时会导致数据类型转换异常。
2.消除强制转换
3.避免了不必要的装箱、拆箱操作,提高程序的性能
Boxing(装箱)和UnBoxing(拆箱)操作会带来一定开销,特别在对集合操作非常频繁的程序中,引入泛型后,会带来性能提升。
4.扩展性强,符合面向对象的软件编程宗旨
5.提高程序可读性
类型显式的编写,阅读即可知所需参数类型。
//*********不使用泛型*********
List list = new ArrayList();
list.add(1);
list.add("1");
int data = (int) list.get(1); //运行时报错
//*********使用泛型*********
List<Integer> list = new ArrayList<>();
list.add(1);
list.add("1"); // 编译时报错
int data = list.get(1); //不需要强转
三、泛型的应用
泛型有三种使用方式,分别为:泛型类,泛型接口和泛型方法。
泛型标识
泛型标识可任意设置,Java常见的泛型标识及其含义:
- T: 代表任意类型
- E: 代表集合中的元素类型Element
- K: 代表key (key-value)
- V: 代表value,通常与K配合使用
1)泛型类
格式:
public class 类名<泛型类型1, 泛型类型2, ...> {
private 泛型标识 变量名;
}
使用:
public class GenericClass<T> {
private T data;
public GenericClass(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
注意:泛型类中的静态方法和静态变量不可以使用泛型类所声明的类型参数。
why?
泛型类中的类型参数的确定是在创建泛型对象的时候,静态变量和静态方法在类加载时已经初始化,静态成员有可能在泛型类的类型参数未确定前使用类名直接调用。
2)泛型接口
格式:
public interface 接口名<泛型类型1, 泛型类型2, ...> {
}
使用:
public interface IPerson<T> {
T eat();
}
/**
*若继承或实现了泛型接口,但是没有确定泛型接口中的类型参数,也可将当前接口/类定义为泛型接口/类
*/
public interface IMan<T> extends IPerson<T>{
void play();
}
public class Student implement IPerson<String> { //不给定类型则默认为Object
@Override
public String eat() {
return "食堂";
}
}
注意:在泛型接口中,静态成员也不能使用泛型接口定义的类型参数。
3)泛型方法
当在一个方法的返回值前声明了泛型标识时,该方法就被声明为一个泛型方法。此泛型标识只能在当前方法中使用。当然,泛型方法也可以使用泛型类中定义的泛型参数,但是如果仅仅是使用了泛型类中定义的泛型参数,却并没有声明泛型标识时,此方法就不是泛型方法,如果泛型方法和泛型类的泛型标识重复,泛型方法始终以自己声明的类型参数为准,当然尽量避免同名。
格式:
public <T> void 泛型方法(T data) {
}
使用:
public class TestClass<E> {
private E testData;
//不是泛型方法
public E getTestData() {
return testData;
}
//泛型方法
public <T> T genericMethod(T data) {
return data;
}
}
public class Test {
public static <T> T add(T x, T y) {
return y;
}
public static void main(String[] args) {
//一、不显式的指定类型参数
//传入的数据类型都为Integer,所以add中的<T> == <Integer>
int i = Test.add(1, 2);
/**
* 类型推断
* 传入的数据类型一个Integer,一个Double,所以add中的<T>取共同父类的最小级 <Number>
*/
Number i = Test.add(1, 2.8);
//传入的数据类型一个Integer,一个String,所以add中的<T>取共同父类的最小级 <Object>
Object i = Test.add(1, "2.8");
//二、显示的指定类型参数
int i = Test.<Integer>add(1, 2.2); //编译错误
Number i = Test.<Number>add(1, 2.2);
}
}
四、类型擦除
1.什么是类型擦除
编译器在编译期间会擦除代码中的所有泛型语法并相应的做出一些类型转换动作。
换而言之,泛型信息只存在于代码编译阶段,编译结束后,与泛型相关的信息会被擦除,专业术语称为类型擦除。也就是说,成功编译后的class文件不包含任何泛型信息,泛型信息不会进入到运行时阶段。
public class TypesErasure {
public static void main(String[] args) {
List<String> sList = new ArrayList<>();
List<Integer> iList = new ArrayList<>();
Log.d("TypesErasure", "result:" + (sList.getClass() == iiList.getClass())); //true
}
}
public class Caculate<T> {
private T num;
}
|
| 反编译
|
v
public class Caculate {
public Caculate() {} //默认构造器
private Object num;
}
上方的例子中,声明了两个List,一个类型参数为String,一个类型参数为Integer,按规则两个List中的泛型类型参数被确定为不同的两种数据类型,为什么对比它们的类信息却是相同的呢?
就是因为编译期间进行了类型擦除。List<String>和List<Integer>在编译后都变为List<Object>。
也可以对一个泛型类或泛型方法进行反编译,发现:泛型标识被擦除,参数num的数据类型被替换为Object,替换了泛型数据类型的数据类型称之为原始数据类型。
那么是否所有的泛型数据类型在编译后都会被替换为Object呢?
答案是否定的。大部分情况是这样,有一种特殊情况:使用了extends和super语法的有界类型参数(泛型通配符)。
public class Caculate<T extends Number> {
private T num;
}
|
| 反编译
|
v
public class Caculate<Number> {
public Caculate() {} //默认构造器
private Number num;
}
2.类型擦除的原理
按前文的逻辑,编译(类型擦除)后List<Integer>变为List<Object>,这时向数组插入一个字符串,同时思考两个问题:
- 编译器会报错吗
- 如果会,编译器又是怎么在不用运行程序的情况下进行类型检查并报错的呢
Java是如何解决这个问题的呢。其实在创建一个泛型类对象时,Java编译器会先检查代码中传入的泛型数据类型并记录下来,然后再对代码进行编译,编译的同时进行类型擦除。当需要对被擦除了泛型信息的对象进行操作时,编译器会自动将对象进行类型转换。也就是说,在调用list.get()时,执行了两条字节码指令:
- 对原始的方法get()的调用,返回的是Object()类型
- 将返回的Object()类型强制转换为Integer类型
五、泛型通配符
1.什么是泛型通配符
在日常的开发中,确实有这样的需求,希望泛型能够处理某一类型范围内的类型参数,比如某个泛型类和它的子类,为此Java引入了泛型通配符这个概念。
泛型通配符有三种形式:
- <?>:无限定的通配符
- <? extends T>:上界通配符
- <? super T>:下界通配符
2.上界通配符
2.1 定义
上界通配符<? extends T>:T代表类型参数的上界,<? extends T>表示类型参数的范围是T和T的子类。需要注意的是它也是一个数据类型实参,和Number、String、Integer一样都是一种实际的数据类型。
上图中的例子将ArrayList<Integer>向上转型,逻辑上可将<? extends Number>视为ArrayList<Integer>的父类。
ArrayList<? extends Number>可以代表ArrayList<Integer>、ArrayList<Double>、……ArrayList<Number>中的某一个集合,但是不能指定 ArrayList<? extends Number>的数据类型,(有点难理解)
举个例子:
ArrayList<? extends Number>并不能往其中添加Integer或Float对象。假设可以会发生什么情况,这个list中有Integer,Float等类型对象,在读数据时,我们无法知道是什么类型,声明为Integer或Float可能出现ClassCastException异常。而因为null表示任何类型,所以向列表中添加。
那么为什么要引入上界通配符这个概念呢?
2.2 用法
public static int addInt(List<Integer> list) {
int data = 0;
for (Integer integer : list) {
data += integer;
}
return data;
}
public static int addFloat(List<Double> list) {
int data = 0;
for (Double d : list) {
data += d.intValue();
}
return data;
}
|
|
v
public static int add(List<? extends Number> list) {
int data = 0;
for (Number integer : list) {
data += integer.intValue();
}
return data;
}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2);
List<Double> fList = Arrays.asList(1.2, 2.4);
Log.d("Test", "list:" + add(list) + ",fList:" + add(fList));
}
此时,就不需要针对add方法添加多个方法。
3.下界通配符
3.1 定义
下界通配符<? super T>:T代表类型参数的下界,<? super T>表示类型参数的范围是T和T的超类。它也是一个数据类型实参,和Number、String、Integer一样都是一种实际的数据类型。
(水印不能可选择去掉……)
ArrayList<? super Integer>的下界是ArrayList<Integer>。因此可以确定Integer类或其子类(有的话) 可以加入列表;而Number类的父类就不能加入了,因为不能确定ArrayList<? super Integer>的数据类型。
3.2 用法
public static void main(String[] args) {
ArrayList<Number> iList = new ArrayList<>();
iList.add(1);
iList.add(1.1);
fillData(iList);
}
public static void fillData(ArrayList<? super Number> list) {
list.add(2);
list.add(2.2);
}
4.无限定通配符<?>
无限通配符<?>:?代表了任何一种数据类型,能代表任何一种数据类型的只有null。<?>也是一个实际的数据类型。
六、PESC原则
什么时候用extends,什么时候用super呢?PESC原则:Producer Extends Consumer Super。
如果需要返回泛型,那么它是生产者(Producer),使用extends通配符;如果需要写入泛型,那么它是消费者(Consumer),使用super通配符。
以Collection.copy()为例:
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");
} else {
if (srcSize < 10 || 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());
}
}
}
}
原理就是遍历src集合,数据写入dest集合。因此src是生产者,声明为<? extends T>;dest是消费者,声明为<? super T>。