前言

大家好啊,我是汤圆,明天给大家带来的是《Java中的泛型 - 细节篇》,心愿对大家有帮忙,谢谢

仔细的观众敌人们可能发现了,当初的题目不再是入门篇,而是各种具体篇细节篇

是因为之前的几篇比较简单,所以叫做入门篇会适合点;

当初往后的都缓缓的开始复杂化了,所以叫入门就有点题目党了,所以改叫具体篇或者细节篇或者进阶篇等等

文章纯属原创,集体总结不免有过错,如果有,麻烦在评论区回复或后盾私信,谢啦

简介

泛型的作用就是把类型参数化,也就是咱们常说的类型参数

平时咱们接触的一般办法的参数,比方public void fun(String s);参数的类型是String,是固定的

当初泛型的作用就是再将String定义为可变的参数,即定义一个类型参数T,比方public static <T> void fun(T t);这时参数的类型就是T的类型,是不固定的

从下面的String和T来看,泛型有着浓浓的多态的滋味,但实际上泛型跟多态还是有区别的

从实质上来讲,多态是Java中的一个个性,一个概念,泛型是实在存在的一种类型;

目录

上面咱们具体说下Java中的泛型相干的知识点,目录如下:

  • 什么是类型参数
  • 为啥要有泛型
  • 泛型的演变史
  • 类型擦除
  • 泛型的利用场景
  • 通配符限定
  • 动静类型平安
  • 等等
注释中大部分示例都是以汇合中的泛型为例来做介绍,因为用的比拟多,大家都相熟

注释

什么是类型参数

类型参数就是参数的类型,它承受类作为理论的值

文言一点来说,就是你能够把类型参数看作形参,把理论传入的类看作实参

比方:ArrayList<E>中的类型参数E看做形参, ArrayList<String>中的类String看做实参

如果你学过工厂设计模式,那么就能够把这里的ArrayList<E>看做一个工厂类,而后你须要什么类型的ArrayList,就传入对应的类型参数即可

  • 比方,传入Integer则为ArrayList<Integer>
  • 比方,传入String则为ArrayList<String>

为啥要有泛型

次要是为了进步代码可读性和安全性

具体的要从泛型的演变史说起

泛型的演变史

从狭义上来说,泛型很早就有了,只是隐式存在的;

比方List list = new ArrayList(); //等价于List<Object> list = new ArrayList<>();

然而这个时候的泛型是很软弱的,可读性和安全性都很差(这个期间的汇合绝对于数组来说,劣势还不是很大)

首先,填充数据时,没有类型查看,那就有可能把Cat放到Dog汇合中

其次,取出时,须要类型转换,如果你很侥幸的把对象放错了汇合(有可能是成心的),那么运行时就会报错转换异样(然而编译却能够通过)

不过到了JDK1.5,呈现了真正意义上的泛型(类型参数,用尖括号<>示意);

比方List<E>汇合类,其中的E就是泛型的类型参数,因为汇合中都是存的元素Element,所以用E字母代替(相似还有T,S,K-key,V-value);

这个时候,程序的健壮性就进步了,可读性和安全性也都很高,看一眼就晓得放进去的是个啥货色(这个期间的汇合绝对于数组来说,劣势就很显著了

当初拿List<Dog> list = new ArrayList<>();来举例说明

首先,填充数据时,编译器本人会进行类型查看,避免将Cat放入Dog中

其次,取出数据时,不须要咱们手动进行类型转换,编译器本人会进行类型转换

仔细的你可能发现了,既然有了泛型,那我放进去的是Dog,取出的不应该也是Dog吗?为啥编译器还要类型转换呢?

这里就引出类型擦除的概念

类型擦除

什么是类型擦除?

类型擦除指的是,你在给类型参数<T>赋值时,编译器会将实参类型擦除为Object(这里假如没有限定符,限定符上面会讲到)

所以这里咱们要明确一个货色:虚拟机中没有泛型类型对象的概念,在它眼里所有对象都是一般对象

比方上面的代码

擦除前

public class EraseDemo<T> {    private T t;    public static void main(String[] args) {            }    public T getT(){        return t;    }    public void setT(T t){        this.t = t;    }}

擦除后

public class EraseDemo {    private Object t;    public static void main(String[] args) {            }    public Object getT(){        return t;    }    public void setT(Object t){        this.t = t;    }}

能够看到,T都变成了Object

泛型类被擦除后的类型,咱们个别叫它原始类型(raw type),比方EraseDemo<T>擦除后的原始类型就是EraseDemo

相应的,如果你有两个数组列表,ArrayList<String>ArrayList<Integer> ,编译器也会把两者都擦除为ArrayList

你能够通过代码来测试一下

ArrayList<String> list1 = new ArrayList<>();ArrayList<Integer> list2 = new ArrayList<>();System.out.println(list1.getClass() == list2.getClass());// 这里会打印true
下面提到的限定符是干嘛的?

限定符就是用来限定边界的,如果泛型有设置边界,比方<T extends Animal>,那么擦除时,会擦除到第一个边界Animal类,而不是Object

上面还是以下面的代码为例,展现下擦除前后的比照

擦除前:

public class EraseDemo<T extends Animal> {    private T t;    public static void main(String[] args) {            }    public T getT(){        return t;    }    public void setT(T t){        this.t = t;    }}

擦除后:

public class EraseDemo {    private Animal t;    public static void main(String[] args) {            }    public Animal getT(){        return t;    }    public void setT(Animal t){        this.t = t;    }}
这里的extends符号是示意继承的意思吗?

不是的,这里的extends只是示意前者是后者的一个子类,能够继承也能够实现

之所以用extends只是因为这个关键词曾经内置在Java中了,且比拟合乎情景

如果本人再造一个关键词,比方sub,可能会使得某些旧代码产生问题(比方应用sub作为变量的代码)

为什么要擦除呢?

这其实不是想不想擦除的问题,而是不得不擦除的问题

因为旧代码是没有泛型概念的,这里的擦除次要是为了兼容旧代码,使得旧代码和新代码能够相互调用

泛型的利用场景

  • 从大的方向来说:

    • 用在类中:叫做泛型类,类名前面紧跟<类型参数>,比方ArrayList<E>
    • 用在办法中:叫做泛型办法,办法的返回值后面增加<类型参数>,比方:public <T> void fun(T obj)
是不是想到了抽象类和形象办法?

还是有区别的,抽象类和形象办法是互相关联的,然而泛型类和泛型办法之间没有分割

  • 集中到类的方向来说:泛型多用在汇合类中,比方ArrayList<E>

如果是自定义泛型的话,举荐用泛型办法,起因有二:

  1. 脱离泛型类独自应用,使代码更加清晰(不必为了某个小性能而泛化整个类)
  2. 泛型类中,静态方法无奈应用类型参数;然而动态的泛型办法能够

通配符限定

这里次要介绍<T>, <? extends T>, <? super T>的区别

  • <T>:这个是最罕用的,就是一般的类型参数,在调用时传入理论的类来替换T即可,这个理论的类能够是T,也能够是T的子类
比方List<String> list = new ArrayList<>();,这里的String就是理论传入的类,用来替换类型参数T
  • <? extends T>:这个属于通配符限定中的子类型限定,即传入理论的类必须是T或者T子类

乍一看,这个有点像<T>类型参数,都是往里放T或者T的子类;

然而区别还是挺多的,前面会列出

  • <? super T>:这个属于通配符限定中的超类型限定,即传入理论的类必须是T或者T的父类
  • <?>:这个属于有限定通配符,即它也不晓得外面该放啥类型,所以罗唆就不让你往里增加,只能获取(这一点相似<? extends T>

上面用表格列出<T><? extends T>, <? super T>的几个比拟明细的区别

<T><? extends T><? super T>
类型擦除传入实参时,实参被擦为Object,然而在get时编译器会主动转为T擦到T擦到Object
援用对象不能将援用指向子类型或者父类型的对象,比方:List<Animal> list = new ArrayList<Cat>();//报错能将援用指向子类型的对象,比方:List<? extends Animal> list = new ArrayList<Cat>();能将援用指向父类型的对象,比方:List<? super Cat> list = new ArrayList<Animal>();
增加数据能够增加数据,T或者T的子类不能能,T或者T的子类

上面咱们用代码来演示下

类型擦除:

// <T>类型,传入实参时,擦除为Object,然而get时还是实参的类型List<Animal> list1 = new ArrayList<>();// 非法list1.add(new Dog());// 非法Animal animal = list1.get(0); // 这里不须要强转,尽管后面传入实参时被擦除为Object,然而get时编译器外部曾经做了强制类型转换// <? extends T> 子类型的通配符限定,擦除到T(整个过程不再变)List<? extends Animal> list2 = list1;// 非法Animal animal2 = list2.get(0); // 这里不须要强转,因为只擦除到T(即Animal)// <? super T> 超类型的通配符限定,擦除到ObjectList<? super Animal> list3 = list1; // 非法Animal animal3 = (Animal)list3.get(0); // 须要手动强制,因为被擦除到Object

将援用指向子类型或父类型的对象:

// <T>类型,不能指向子类型或父类型List<Animal> list = new ArrayList<Dog>();// 报错:须要的是List<Animal>,提供的是ArrayList<Dog>// <? extends T> 子类型的通配符限定,指向子类型List<? extends Animal> list2 = new ArrayList<Dog>();// 非法// <? super T> 超类型的通配符限定,指向父类型List<? super Dog> list3 = new ArrayList<Animal>(); // 非法

增加数据

// <T>类型,能够增加T或者T的子类型List<Animal> list1 = new ArrayList<>();list.add(new Dog());// 非法// <? extends T> 子类型的通配符限定,不能增加元素List<? extends Animal> list2 = new ArrayList<Dog>();// 正确list2.add(new Dog()); // 报错:不能往里增加元素// <? super T> 超类型的通配符限定,能够增加T或者T的子类型List<? super Dog> list3 = new ArrayList<Animal>();list3.add(new Dog()); // 非法,能够增加T类型的元素list3.add(new Animal());//报错,不能增加父类型的元素

上面针对下面的测试后果进行解惑

先从<T>的报错开始吧

为啥<T>类型的援用不能指向子类型,比方 List<Animal> list = new ArrayList<Dog>();

首先阐明一点,Animal和Dog尽管是父子关系(Dog继承Animal),然而List<Animal> List<Dog>之间是没有任何关系的(有点像Java和Javascript)

他们之间的关系如下图

之所以这样设计,次要是为了类型平安的思考

上面用代码演示,假如能够将List<Animal>指向子类List<Dog>

List<Animal> list = new ArrayList<Dog>();// 假如这里不报错list.add(new Cat()); //这里把猫放到狗外面了

第二行能够看到,很显著,把猫放到狗外面是不对的,这就又回到了泛型真正呈现之前的期间了(没有泛型,汇合存取数据时不平安)

那为啥<? extends T>就能指向子类型呢?比方List<? extends Animal> list = new ArrayList<Dog>();

说的浅一点,起因是:这个通配符限定呈现的目标就是为了解决下面的不能指向子类的问题

当然,这个起因说了跟没说一样。上面开始正经点解释吧

因为这个通配符限定不容许插入任何数据,所以当你指向子类型时,这个list就只能寄存指向的那个汇合里的数据了,而不能再往里增加;

天然的也就类型平安了,只能拜访,不能增加

为什么<? extends T>不容许插入数据呢?

其实这个的起因跟下面的批改援用对象是相辅相成的,合起来就是为了保障泛型的类型安全性

思考上面的代码

List<Animal> list = new ArrayList<>();list.add(new Cat());list.add(new Dog());Dog d = (Dog) list.get(0); // 报错,转换异样

能够看到,插入的子类很凌乱,导致提取时转型容易出错(这是泛型<T>的一个弊病,当然咱们写的时候多用点心可能就不会这个问题)

然而有了<? extends T>之后,就不一样了

首先你能够通过批改援用的对象来使得list指向不同的Animal子类

其次你增加数据,不能间接增加,然而能够通过指向的Animal子类对象来增加

这样就保障了类型的安全性

代码如下:

// 定义一个Dog汇合List<Dog> listDog = new ArrayList<>();listDog.add(new Dog());// 让<? extends Animal>通配符限定的泛型 指向下面的Dog汇合List<? extends Animal> list2 = listDog;// 这时如果想往里增加数据,只须要操作listDog即可,它能够保障类型平安listDog.add(new Dog());// 如果本人去增加,就会报错list2.add(new Dog());// 报错

<? extends T>个别用在形参,这样咱们须要哪个子类型,只须要传入对应子类的泛型对象就能够了,从而实现泛型中的多态

<? super T>为啥能够插入呢?

两个起因

  1. 它只能插入T或者T的子类
  2. 它的上限是T

也就是说你轻易插入,我曾经限度了你插入的类型为T或者T的子类

那么我在查问时,就能够释怀的转为T或者T的父类

代码如下:

List<? super Dog> listDog = new ArrayList<>();listDog.add(new Dog());listDog.add(new Cat()); // 报错listDog.add(new Animal()); // 报错Dog dog = (Dog) listDog.get(0); // 外部被擦除为Object,所以要手动强转
为啥<T>获取时,编译器会主动强转转换,到了这里<? super T>,就要手动转换了呢?

这个可能是因为编译器也不确定你的要返回的T的父类是什么类型,所以罗唆留给你本人来解决了

然而如果你把这个listDog指向一个父类的泛型对象,而后又在父类的泛型对象中,插入其余类型,那可就乱了(又回到<T>的问题了,要本人多留神)

比方:

List<Animal> list = new ArrayList<>();list.add(new Cat()); // 加了Cat// 指向AnimalList<? super Dog> listDog = list;listDog.add(new Dog());list.add(new Cat()); // 报错list.add(new Animal()); // 报错Dog dog = (Dog) listDog.get(0); //报错:转换异样Cat-》Dog

所以倡议<? super T>在增加数据的时候,尽量集中在一个中央,不要多个中央增加,像下面的,要么都在<? super Dog>里增加数据,要么都在<Animal>中增加

动静类型安全检查

这个次要是为了跟旧代码兼容,对旧代码进行的一种类型安全检查,避免将Cat插入Dog汇合中这种谬误

这种查看是产生在编译阶段,这样就能够提前发现问题

对应的类为Collections工具类,办法如下图

代码如下

// 动静类型安全检查,在与旧代码兼容时,避免将Dog放到Cat汇合中相似的问题// === 查看之前 ===List list = new ArrayList<Integer>();// 增加不报错list.add("a");list.add(1);// 只有用的时候,才会报错Integer i = (Integer) list.get(0); // 这里运行时报错// === 查看之后 ===List list2 = Collections.checkedList(new ArrayList<>(), Integer.class);// 插入时就会报错list2.add("a"); // 这里编译时就报错,提前发现错误list2.add(1);

总结

泛型的作用:

  1. 进步类型安全性:预防各种类型转换问题
  2. 减少程序可读性:所见即所得,看失去放进去的是啥,也晓得会取出啥
  3. 进步代码重用性:多种同类型的数据(比方Animal下的Dog,Cat)能够汇合到一处来解决,从而调高代码重用性

类型擦除:

泛型T在传入实参时,实参的类型会被擦除为限定类型(即<? extends T>中的T),如果没有限定类型,则默认为Object

通配符限定:

  1. <? extends T>:子类型的通配符限定,以查问为主,比方消费者汇合场景
  2. <? super T>:超类型的通配符限定,以增加为主,比方生产者汇合场景

后记

最初,感激大家的观看,谢谢