泛型
8 基本概念和原理
一个简单泛型类
基本概念
泛型就是类型参数化,处理的数据类型是不固定的,而是可以作为参数传入
Object<T>
- 其中的T就是传递的实际参数类型。
- 参数类型可以有多个,如
Object<U,V>
多个类型之间用逗号分隔
基本原理
Java有Java编译器和Java虚拟机,编译器将Java源代码转换为.class文件,虚拟机加载并运行.class文件。
对于泛型类,Java编译器会将泛型代码转换为普通的非泛型代码,将类型参数T擦除,替换为Object,插入必要的强制类型转换。
强调:Java泛型是通过擦除实现的,T会被替换为Object,程序运行时,并不知道泛型的实际类型参数。
泛型的好处
- 更好的安全性
- 更好的可读性
容器类
泛型方法
public static <T> int indexOf(T[] arr, T elm){
//具体方法
}
可以通过T,使用
indexOf(new String[]{"a","b"});
indexOf(new int[]{1,2});
泛型接口
如 Comparable、Comparator
类型参数的限定
- 参数必须为给定的上届类型或其子类型,这个限定是通过extends关键字来表示的。
- 这个上界可以是某个具体的类或者某个具体的接口,也可以是其他的类型参数。
上界为某个具体的类
public class NumberPair<U extends Number, V extends Number> extends Pair<U, V>{
public NumberPair(U first, V second){
super(first, second);
}
}
上界为某个接口
public static <T entends Comparable>T max(T[] arr){
T max = arr[0]
//函数业务逻辑
}
此时,Java会给一个警告,因为Comparable是一个泛型接口它也需要一个类型参数
public static <T entends Comparable<T>>T max(T[] arr){
//函数业务逻辑
}
<T extends Comparable>形式称为地柜类型限制,解读:
- T表示一种数据类型,必须实现Comparable接口,且必须可以与相同类型的元素进行比较。
上界为其他类型参数
Java支持一个类型参数以另一个类型参作为上界。
public void addAll(DynamicArray<E> c){
for(int i=0; i<c.size; i++){
add(c.get(i));
}
}
但存在局限性
DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> numbers = new DynamicArray<>();
ints.add(100);
inst.add(34);
numbers.addAll(ints);//会提示编译错误
虽然Integer是Number的子类,但DynamicArray并不是DynamicArray的子类,DynamicArray的对象也不能赋值给DynamicArray的变量。
可以使用类型限定来解决:
public <T extends E> void addAll(DynamicArray<T> c){
//业务逻辑
}
小结
泛型是计算机程序中一种重要的思维方式,它将数据结构和算法与数据类型相分离,使得同一套数据结构和算法能够应用于各种数据类型,而且可以保证类型安全,提高可读性。
在Java中,泛型是通过类型擦除来实现的,它是Java编译器的概念,Java虚拟机运行时对泛型基本一无所知。
解析通配符
更简洁的参数类型限定
为了将Integer对象添加到Number容器中,我们的类型参数使用了其他类型参数作为上界。
写法繁琐,它可以替换位:?
public void addAll(DynamicArray<? extends E> c){
}
这种方法没有定义类型参数,c的类型是DynamicArray<? extends E>,?表示通配符,<? extends E>表示有限定通配符,匹配E或E的某个子类型,具体什么子类型是未知的。
和 <? extends E>有什么关系?
- 用于定义类型参数,它声明了一个类型参数T,可放在泛型类定义中类名后面、泛型方法返回值前面
- <? extends E> 用于实例化类型参数,它用于实例化泛型变量中的类型参数,只是这个具体类型是未知的,只知道它是E或E的某个子类型。
- 虽然有所不同,但两种写法经常可以达成相同目标:
public void addAll(DynamicArray<? extends E> c)
public <T extends E> void addAll(DynamicArray<T> c)
理解通配符
除了有限定通配符,还有一种通配符,形成DynamicArray<?>,称为无限定通配符
通配符形式更为简洁,但是存在一个重要限制,只能读,不能写。
- 借助待类型参数的泛型方法,可以解决不能写入的问题。
private static <T> void swapIntegerNal(DynamicArray<T> arr, int i, int j){
//写入
}
public static void swap(DynamticArry<?> arr, int i, intj){
swapIntegerNal(arr, i, j);
}
除了这种需要写的场合,如果参数类型之间有依赖关系,也只能用类型参数。比如将src容器中的内容复制到dest中:
public static <D, S extends D> void copy(DynamicArray<D> dest, DynamicArray<S> src){
for(int i = 0; i < src.size(); i++){
dest.add(src.get(i))
}
}
S和D有依赖关系,要么相同,要么S是D的子类,否则类型不兼容,有编译错误。
上面的声明可以使用通配符简化,两个参数可以简化为一个。
public static <D> void copy(DynamicArray<D> dest, DynamicArray<? extends D> src){
//业务逻辑
}
注:如果返回值依赖于类型参数,也不能用通配符。
总结:
- 通配符形式都可以用类型参数的形式来替代,通配符能做的,用类型参数都能做
- 通配符形式可以减少类型参数,形式上往往更为简单,可读性也更好,所以,能用通配符的就用通配符。
- 如果类型参数之间有依赖关系,或者返回值依赖类型参数,或者需要写操作,则只能用类型参数
- 通配符形式和类型参数往往配合使用,比如,上面的copy方法,定义必要的类型参数,使用通配符表达依赖,并接受更广泛的数据类型。
超类型通配符。
还有一种通配符,与<? extends E>相反, 它的形式为<? super E>,称为超类型通配符
通配符比较
三种通配符形式:<?>、<? extends E>、<? super E>
- 它们的目的都是为了使方法接口更为灵活,可以接受更为广泛的类型。
- <? super E>用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象,它不能被类型参数形式替代。
- <?>和<? extends E>用于灵活读取,使得方法可以读取E或E的任意子类型的容器对象,它们可以用类型参数的形式替代,但通配符形式更为简介。
细节和局限性
使用泛型类、方法、接口
定义泛型类、方法、接口
泛型与数组
泛型与数组的关系
- Java不支持创建泛型数组
- 如果要存放泛型对象,可以使用原始类型的数组,或者使用泛型容器
- 泛型容器内部使用Object数组,如果要转换泛型容器为对应类型的数组,需要使用反射。
总结
泛型的局限性:
- 不能使用基本类型
- 没有运行时类型信息
- 类型擦除会引发一些冲突
- 不能通过类型参数创建对象
- 不能用于静态变量