文章目录
一、泛型是什么?
泛型,即类型参数化,处理的数据类型是不固定的,可以作为参数传入。
二、关键概念
1.泛型方法
方法访问修饰符 之后声明了<T>标识该方法为泛型方法
特点:泛型方法能独立于类而产生变化,比普通方法更加通用、灵活
//泛型方法
public static <T extends Number> T testGeneric(T number) {
return number;
}
2.类型通配符
描述:使用 ?代替具体的类型实参
为了描述上界通配符和下解通配符的具体含义,这里定义三个java类:Animal.java、Cat.java、MiniCat.java
public class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public Animal() {
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Animal{" +
"name='" + name + '\'' +
'}';
}
}
public class Cat extends Animal{
private int age;
public Cat() {
}
public Cat(String name, int age) {
super(name);
this.age = age;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Cat{" +
"age=" + age +
"} " + super.toString();
}
}
public class MiniCat extends Cat{
private Integer level;
public MiniCat() {
}
public Integer getLevel() {
return level;
}
@Override
public String toString() {
return "MiniCat{" +
"level=" + level +
"} " + super.toString();
}
}
2.1 上界通配符 <? extends E>
要求填充的参数是 E 或者 E的子类类型,采用上限通配符的方法中不能写入 E 或者 E的子类元素
只能在泛型方法中读容器list,不能写入
/**
* @author zhaoyuqi start
* @create 2022-10-17 - 16:15
*/
public class TestUP {
public static void main(String[] args) {
ArrayList<Animal> animals = new ArrayList<>();
ArrayList<Cat> cats = new ArrayList<>();
ArrayList<MiniCat> miniCats = new ArrayList<>();
//showAnimal(animals);//报错,只能接受Cat或者Cat子类类型的容器
showAnimal(cats);
showAnimal(miniCats);
}
/**
* 泛型上限通配符 ? extends Cat 只能是Cat或者Cat的子类类型
* @param list
*/
public static void showAnimal(ArrayList<? extends Cat> list){ //在方法里不可写入,在外面可以写入
//list.add(new MiniCat());//报错,只能遍历,不可写入
/*
因为Number 的子类类型有很多个避免出现 在Integer容器里面放Double,
这样会导致类型不安全,所以干脆不准向里面写入。
*/
//list.add(new Cat());
for (int i = 0; i < list.size(); i++) {
System.out.println("list.get(i) = " + list.get(i).getClass().getSimpleName());
}
}
}
2.2 下界通配符 <? super E>
要求填充的参数是 E 或者 E的父类类型,采用下限通配符的方法中可以添加 E 或者 E的父类元素
既能在泛型方法中读容器内容,又能写入元素
/**
* @author zhaoyuqi start
* @create 2022-10-17 - 16:33
*/
public class TestDOWN {
public static void main(String[] args) {
ArrayList<Animal> animals = new ArrayList<>();
ArrayList<Cat> cats = new ArrayList<>();
ArrayList<MiniCat> miniCats = new ArrayList<>();
showAnimal(animals);
showAnimal(cats);
//showAnimal(miniCats);//报错,只接受Cat 或者Cat父类类型的容器
}
/**
* 泛型下限通配符 ? super Cat 只能是Cat或者Cat的父类类型
* @param list
*/
public static void showAnimal(ArrayList<? super Cat> list){
/*
可以写入。
因为对象初始化的时候是从父类开始初始化的,可以传入父类类型因为
父类类型先于自己存在,而且父类是确定的。
*/
list.add(new MiniCat());
list.add(new Cat());
for (int i = 0; i < list.size(); i++) {
System.out.println("list.get(i) = " + list.get(i).getClass().getSimpleName());
}
}
}
2.3 上界和下界通配符何时使用
何时使用extends,何时使用super?为了便于记忆,我们可以用PECS原则:Producer Extends Consumer Super。
即:如果需要返回T,它是生产者(Producer),要使用extends通配符;如果需要写入T,它是消费者(Consumer),要使用super通配符。
例如copy()方法
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++) {
T t = src.get(i); // src是producer
dest.add(t); // dest是consumer
}
}
}
需要返回T的src是生产者,因此声明为List<? extends T>,需要写入T的dest是消费者,因此声明为List<? super T>。
3. 类型擦除
泛型的信息只存在于代码编译阶段,在进入JVM之前,与泛型相关的信息会被擦除称之为—类型擦除
public static void main(String[] args){
ArrayList<Integer> list1 = new ArrayList<>();
ArrayList<String> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass());//true
}
以上代码结果为true,两个ArrayList虽然声明为两个装载两个不同类型的容器,但是经过Java编译之后对应的Class都是java.util.ArrayList,也就是说,虽然ArrayList 和 ArrayList 在编译时是不同的类型,但是在编译完成之后都被会编译器简化为ArrayList。
无限制类型的擦除
当类定义中的类型参数没有任何限制时,在类型擦除后,会被直接替换为Object
反编译前:
public class Erasure1<T> {
private T key;
public T getKey() {
return key;
}
}
反编译后:
public class Erasure1
{
public Erasure1()
{
}
//返回类型是Object
public Object getKey()
{
return key;
}
private Object key;
}
有限制类型的擦除
当类定义中的类型参数存在限制时,在类型擦除中替换为类型参数的上界或者下界
反编译前:
//限制泛型的类型只能是Number以及Number的子类类型
public class Erasure2<T extends Number> {
private T key;
public T getKey() {
return key;
}
}
反编译后:
public class Erasure2
{
public Erasure2()
{
}
//替换为泛型类型的上界Number
public Number getKey()
{
return key;
}
private Number key;
}
擦除方法中的类型参数
在擦除方法中的类型参数时,和擦除类定义中的类型参数一致,无限制时直接擦除
为Object,有限制时则会被擦除为上界类型或下界类型
反编译前:
public class Erasure3<T> {
private T key;
//非泛型方法
public T getKey() {
return key;
}
//泛型方法
public static <T extends Number> T testGeneric(T number) {
return number;
}
}
反编译后:
public class Erasure3
{
public Erasure3()
{
}
//非泛型方法:由于没有类型限制,所以是Object
public Object getKey()
{
return key;
}
//泛型方法有<? extends Number> 限制 故为上界 Number
public static Number testGeneric(Number number)
{
return number;
}
private Object key;
}
类型擦除会引起什么问题?
首先我们创建一个接口,以及一个接口的实现类。
反编译前:
public interface Info<T> {
T getInfo(T param);
}
class InfoImpl implements Info<String>{
@Override
public String getInfo(String param) {
return param;
}
}
反编译前的代码,
反编译后:
class InfoImpl implements Info{
InfoImpl()
{
}
public String getInfo(String param)
{
return param;
}
/*
一般用实现类去调用方法,会先找到接口中的同名方法,再到实现类的方法。
为了不破坏这中关系编译器帮生成了此桥接方法:为了保持实现类与接口的实现关系
*/
public volatile Object getInfo(Object obj)
{
return getInfo((String)obj);
}
}
可以看到,编译后的代码中生成了两个getInfo方法。参数为Object的get方法负责实现Info接口中的同名方法,然后在实现类中又额外添加了一个参数为String的getInfo方法,这个方法也就是理论上应该生成的带参数类型的方法。最终用接口方法调用额外添加的方法(多态),通过这种方式构建了接口和实现类的关系,类似于起到了桥接的作用,因此也被称为桥接方法,最终,通过这种机制保证了泛型情况下的Java多态性。
桥接方法出自 链接:https://juejin.cn/post/6999797611146248222
为什么要使用类型擦除?
主要目的:避免创建过多的运行时类,导致运行时的过度消耗。
如果每一个泛型容器都要创建各自的运行时类,那无疑是很浪费资源的。
总结
我理解为泛型是容器的标签,规定了容器里面能存放什么类型的数据,它将数据结构和数据类型分离,可以使得同一套数据结构可以适用不同的数据类型,而且可以很好的保证数据类型的安全性和可读性。