
泛型
一、基本概念和原理
泛型将接口的概念进一步延申,“泛型”的字面意思是广泛的类型。
类、接口和方法都可以应用于非常广泛的类型,代码与它们能够操作
的数据类型不再绑定到一起,同一套代码可以应用到多种数据类型。
这样,不仅可以复用代码降低耦合,而且可以提高代码的可读性和安全性。
1.泛型类
public class Pair <T>{
private T first;
private T second; public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
}
T表示类型参数,泛型就是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入。
//类型参数可以有多个,用逗号分隔
public class Pair2<T, U> {
private T first;
private U second;
public Pair2(T first, U second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public U getSecond() {
return second;
}
}
Pair<Integer> pair = new Pair<>(99, 88); //Integer就是传递的实际类型参数
System.out.println(pair.getFirst() + " " + pair.getSecond()); //99 88
Pair2<String, Integer> tang = new Pair2<>("Tang", 22);
System.out.println(tang.getFirst() + " " + tang.getSecond());//Tang 22
2.泛型方法
除了泛型类,方法也可以是泛型的,而且,一个方法是不是泛型与它所在的类是不是泛型无关。
public class Alex {
//类型参数位E,放在返回值前面,与泛型类一样类型参数可以有多个
public static <E> int indexOf(E[] arr, E elm) {
for (int i = 0; i < arr.length; i++) {
if (arr[i].equals(elm)) {
return i;
}
}
return -1;
}
public static void main(String[] args) {
int index = indexOf(new Integer[]{1, 2, 3}, 2);
System.out.println(index);//
int tom = indexOf(new String[]{"Tom", "Jimmy", "Cat"}, "Tom");
System.out.println(tom);//
}
}
3.泛型接口
public interface Comparable<T> {
public int compareTo(T o);
}
4.基本原理
Java泛型的内部原理:
public class Pair {
Object first;
Object second;
public Pair(Object first, Object second){
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
}
对于泛型类,Java编译器会将泛型代码转换为非泛型代码(Object或者其上边界类型),
就如上面的Pair类代码,将类型参数T擦除,替换为Object,插入必要的强制类型转换。Java
虚拟机执行的时候不知道泛型这回事,只知道普通的类和代码。
Java是从Java5才引入泛型的,所以用类型擦除来实现泛型是一种不得已的选择。
泛型的好处:
编译器帮我们发现代码问题,因此更安全,还提升了代码的可读性。
5.类型参数的限定
Java支持给类型参数设定上界,类型参数必须为给定的上界类型或者其子类型。
//限定类型后如果类型使用错误编译器就会提醒
//并且擦除类型时就会转换为相应的边界类型
public class SonPair<U extends String, V extends Integer> extends Pair<U, V>{
public SonPair(U first, V second) {
super(first, second);
}
}
其中上界可以是某个具体类,也可以是接口或者其他类型参数。
总之,泛型是计算机程序中的一种重要思维方式,它将数据结构和算法与数据类型相分离,
使得同一套数据结构和算法能够应用于各种数据类型,而且可以保证类型安全,提高可读性。
二、通配符
1.更简洁的参数类型限定
1)有限定统配符
形如ClassName<? extends E>这种统配符,叫做有限定统配符匹配E或者E的某个子类,具体的类型未知。
public void addAll(DynamicArray<? extends E> c)
public <T extends E> void addAll(DynamicArray<T> c)
虽然与使用<T extends E>的效果一样,该方法与使用<T extends E>相比不用定义新的类型参数T,
因此也不必把add方法定义为泛型方法,也不用改动相关的源代码。
2)无限定通配符
形如ClassName<?>的统配符为无限定统配符。
比较:
public static int indexOf(DynamicArray<?> arr, Object elm)
public static <T> int indexOf(DynamicArray<T> arr, Object elm)
使用无限定统配符比添加一个泛型参数T更加简洁。
但是,不管是有限定统配符还是无限定统配符都有一个缺陷:只能读,不能写。
例如:
DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<? extends Number> numbers = ints;
Integer a = 200;
numbers.add(a); //报错
numbers.add((Number)a)Ҕ //报错
numbers.add((Object)a); //报错
为什么会报错?
其次,如果方法的返回值依赖于类型参数,也不能用通配符:
public static <T extends Comparable<T>> T max(DynamicArray<T> arr){
T max = arr.get(0);
for(int i=1; i<arr.size(); i++){if(arr.get(i).compareTo(max)>0){
max = arr.get(i);
}
}
return max;
}
//很难用统配符代替
总之:
1)统配符能做的类型参数都能做
2)因为更简洁,能用通配符就用通配符
3)如果参数类型之间有依赖关系,或者返回值依赖类型参数,或者需要写操作,只能由类型参数。
4)两者往往配合使用
2.超类型通配符
形如<? super E>的通配符被称为超类型通配符,表示E的某个父类型。
使用场景:
1)可以更灵活地写入:
public void copyTo(DynamicArray<E> dest){
for(int i=0; i<size; i++){
dest.add(get(i));
}
}
DynamicArray<Integer> ints = new DynamicArray<Integer>();
ints.add(100);
ints.add(34);
DynamicArray<Number> numbers = new DynamicArray<Number>();
ints.copyTo(numbers);//报错,因为期望的类型是DynamicArray<Integer>
解决办法:
public void copyTo(DynamicArray<? super E> dest){
for(int i=0; i<size; i++){
dest.add(get(i));
}
}
2)解决子类没有实现泛型接口的问题:
public static <T extends Comparable<T>> T max(DynamicArray<T> arr)
//注意Base实现了Comparable<Base>
class Base implements Comparable<Base>{
//code
}
//Child并没有实现Comparable<Child>它实现的是Comparable<Base>
class Child extends Base {
//code
}
DynamicArray<Child> childs = new DynamicArray<Child>();
childs.add(new Child(20));
childs.add(new Child(80));
Child maxChild = max(childs);//编译器报错,提示类型不匹配,
报错原因:在<T extends Comparable<T>> 中对T类型的要求是extends Comparable<T>
而Child并没有实现Comparable<Child>,其实Child也没有必要去重复实现Comparable接口。
解决办法:
public static <T extends Comparable<? super T>> T max(DynamicArray<T> arr)
<? super T>可以配匹Base所以整体是匹配的。
另外,请注意:参数类型限定只有extends形式没有super形式,比如:
<E super T>是有问题的,Java不支持这种语法。
总结:
1)<? super E>用于灵活写入与比较,使得使得父可以使用于子。
2)<?> <? extends E>用于灵活读取。
三、细节和局限性
我们将通过以下几方面介绍这些细节和局限性:
1.使用泛型类、方法和接口
1)因为类型参数会被替换为object或者一个类的父类,所以基本类型不能用来实例化类型参数,解决办法,使用包装类型
2)运行时类型信息不适用于泛型:
在内存中每个类都有一份类型信息,而每个对象也都保存着对应的类型信息的引用,
这个类型信息也是一个对象,它的类型为Class,Class本身也是一个泛型类。
每个类的类型对象可以通过<类名>.class的方式引用,可以通过getClass()方法获得。
这个类型只有一份与泛型无关,所以Java不支持如下写法:
Pair<Integer>.class
一个泛型对象的getClass()方法的返回值与其原始类型对象也是相同的。
同理也不支持:
if (p1 instanceof Pair<Integer>)
不过支持:
if (p2 instanceof Pair<?>)
3)由于类型擦除可能会引发一些冲突:
比如上一个Base与Child的例子,Child没有专门实现Comparable接口
可是如果Child想自己实现接口,重写compareTo()方法呢比如:
class Child extends Base implements Comparable<Child>{
//编译器报错
}
遗憾的是编译器会报错,Comparable接口不能如此被实现两次(一次是Comparable<Base>, 一次是Comparable<Child>)
因为类型擦除后,实际上只能有一个。因此只能通过重写compareTo()方法修改比较方法:
class Child extends Base {
@Override
public int compareTo(Base o) {
if(!(o instanceof Child)){
throw new IllegalArgumentException();
}
Child c = (Child)o;
return 0;
}
}
4)对重载方法的影响:
public static void test(DynamicArray<Integer> intArr)
public static void test(DynamicArray<String> strArr)//报错
2.定义泛型类、方法和接口
1)不能通过类型参数创建对象,解决方法:使用反射等。
2)泛型类型参数不能用于静态变量和方法,为什么????
3.泛型与数组
1)不能创建泛型数组,例如:
Pair<Object,Integer>[] options = new Pair<Object,Integer>[]{
new Pair("1