java泛型

本文详细介绍了Java中的泛型,包括泛型的概念、泛型方法、类型通配符(上界和下界通配符)及其使用场景,以及类型擦除的原理和影响。泛型提高了代码的类型安全性和可读性,而类型擦除则是为了减少运行时的资源消耗。
摘要由CSDN通过智能技术生成


一、泛型是什么?

泛型,即类型参数化,处理的数据类型是不固定的,可以作为参数传入。

二、关键概念

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

为什么要使用类型擦除?

主要目的:避免创建过多的运行时类,导致运行时的过度消耗。
如果每一个泛型容器都要创建各自的运行时类,那无疑是很浪费资源的。

总结

我理解为泛型是容器的标签,规定了容器里面能存放什么类型的数据,它将数据结构和数据类型分离,可以使得同一套数据结构可以适用不同的数据类型,而且可以很好的保证数据类型的安全性和可读性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值