目录
泛型
1. 什么是泛型
2.泛型方法
3.通配符上界(泛型的协变)
4.通配符下界(泛型的逆变)
5.泛型的编译(擦除机制)
泛型
泛型:就是让一个类能适用于多个类型,就是在封装数据结构时能让封装的类型被各种类型使用所以引入了泛型的概念,虽然有了泛型,什么数据都可以放,但是更多情况下我们还是希望他只能持有一种数据类型。所以,泛型的主要目的:指定当前的容器,要持有什么类型的对象,让编译器去做检查。
1. 什么是泛型
语法格式如下:
泛型类<类型实参>变量名;//定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参);//实例化一个泛型类对象
一般用<T>作为占位符 ,表示当前类是一个泛型类。Java中的泛型参数只能是引用类型,不能是基本类型,这与Java的泛型擦出机制有关。
实例:
MyArray<Integer> list = new MyArray<Integer>();
*裸类型(Raw Type) (这是一个泛型类但没有带着类型实参)
MyArray list = new MyArray();
裸类型是为了兼容老版本的API保留机制,我们不要轻易使用。
2.泛型方法
泛型方法:定义一个泛型方法,我们需要在方法返回值前使用尖括号声明一个或多个泛型参数然在方法中就可以用到声明的泛型参数了,调用泛型方法时,我们不需要手动写出类型,编译器会根据你的调用,自动推导出具体类型。
静态泛型方法:泛型类有一个局限,静态方法和静态属性访问不了类上定义的泛型参数,静态泛型方法的定义和使用与普通泛型方法一致。
泛型类和泛型方法的使用场景:
当泛型参数需要在多个方法或成员属性间扭转,就使用泛型类,比如:集合。
当泛型参数只需要作用于某个方法,那就使用泛型方法。
3.通配符上界(泛型的协变)
泛型类型是具有不变性的,比如下面代码就是错误的:
Arraylist<Object> objectList;
ArrayList<String> stringList = new ArrayList<>();
objectList=stringList//这里会报错
objectList.add(new Shit());
String str = stringList.get(0);
//因为我们无法将一个object对象转化为string对象,所以在编译层面上面的赋值就会直接报错
为了让泛型变得更灵活,Java引入了通配符:?,通过下面的代码来给大家介绍一下通配符的作用:
在不使用通配符时,因为泛型的不变性,下面这段代码会出现问题,就使代码非常不灵活。
public static double sum(List<Number> list){
double result =0;
for(Number number : list){
result += number.doubleValue();
}
return result;
}
List<Double> doubleList = new ArrayList<>();
sum(doubleList)//这里会报错
我们可以使用通配符上界(?:extends T)来使代码更灵活
public static double sum(List<? extends Number> list){
double result =0;
for(Number number : list){
result += number.doubleValue();
}
return result;
}
List<Double> doubleList = new ArrayList<>();
sum(doubleList)
这种写法也被叫做泛型的协变 。
4.通配符下界(泛型的逆变)
我们还可以使用通配符下界(?:super T)来使代码变得灵活,代码实例如下:
class Food {
}
class Fruit extends Food {
}
class Apple extends Fruit {
}
class Plate<T> {
private T plate ;
public T getPlate() {
return plate;
}
public void setPlate(T plate) {
this.plate = plate;
}
}
public class TestDemo {
public static void main(String[] args) {
Plate<Fruit> plate1 = new Plate<>();
plate1.setPlate(new Fruit());
fun(plate1);
Plate<Food> plate2 = new Plate<>();
plate2.setPlate(new Food());
fun(plate2);
}
public static void fun(Plate<? super Fruit> temp){
// 此时可以修改!!添加的是Fruit 或者Fruit的子类
temp.setPlate(new Apple());//这个是Fruit的子类
temp.setPlate(new Fruit());//这个是Fruit的本身
//Fruit fruit = temp.getPlate(); 不能接收,这里无法确定是哪个父类
System.out.println(temp.getPlate());//只能直接输出
}
}
通配符的优缺点:
协变:放宽了对子类类型的泛型约束,但是缺点是不能对调用的参数进行写入数据只能进行读取数据。
逆变:放宽了对父类类型的泛型约束,但是缺点是不能对参数进行读取数据,只能写入数据。
5.泛型的编译(擦除机制)
擦除机制的实质就是,在编译阶段,Java的泛型类型可能是ArrayList<Integer>但是在java文件编译成字节码的过程中,泛型参数部分就被擦出了(泛型类,泛型方法的参数全部被替换成它的第一个上界或者顶级父类Object),在class文件中,无论参数是什么,JVM实际执行的代码类型其实是ArrayList<Object>类型,这也就引出了很多问题如下:
- 泛型参数只能是引用类型而不能是基本数据类型,因为基本数据类型无法被擦除成Object。
- 不能使用instanceof关键字进行泛型类型检测,因为在运行时所以的泛型类型都是裸类型。
- 泛型类型无法实例化类型参数T a=new T(),因为在运行时无法确定T的具体类型,也不知道T是否存在无参构造器。
- 无法实例化泛型数组T[] arry =new T[2];因为泛型最后都被擦除成Object数组,在使用时很容易发生类型转化异常,比如object转化不成string。
擦除机制是Java为了引入泛型这个语法而不得不做出的妥协之举,泛型语法是JDK5之后引入的,为了兼容老版本,不得不在编译阶段将泛型擦除成裸类型。但是在其他语言中,泛型的使用会非常自然且简单安全,在编写代码是我们要了解泛型擦除机制,否则可能会引发很多不必要的异常。
类型擦除是指在运行时对于JVM而言泛型参数被擦除掉了,并不代表泛型信息消失了,才class文件中泛型信息被以其他方式进行保存,我们依然可以在运行时通过反射的手段进行泛型类型检测。