关于java:Java中的泛型-细节篇

46次阅读

共计 7415 个字符,预计需要花费 19 分钟才能阅读完成。

前言

大家好啊,我是汤圆,明天给大家带来的是《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> 超类型的通配符限定,擦除到 Object
List<? 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
// 指向 Animal
List<? 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>:超类型的通配符限定,以增加为主,比方生产者汇合场景

后记

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

正文完
 0