0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看威廉希尔官方网站 视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

深入了解Java泛型——从前世今生到PECS原则

OSC开源社区 来源:OSC开源社区 2024-11-21 11:45 次阅读

本文主要介绍泛型诞生的前世今生,特性,以及著名PECS原则的由来。

在日常开发中,必不可少的会使用到泛型,这个过程中经常会出现类似“为什么这样会编译报错?”,“为什么这个列表无法添加元素?”的问题,也会出现感叹Java的泛型限制太多了很难用的情况。

为了更好的使用泛型,就需要更深地了解它,因此本文主要介绍泛型诞生的前世今生,特性,以及著名PECS原则的由来。

泛型的诞生

背景

在没有泛型之前,必须使用Object编写适用于多种类型的代码,想想就令人头疼,并且非常的不安全。同时由于数组的存在,设计者为了让其可以比较通用的进行处理,也让数组允许协变,这又为程序添加了一些天然的不安全因素。为了解决这些情况,Java的设计者终于在Java5中引入泛型,然而,正是因为引入泛型的时机较晚,为了兼容先前的代码,设计者也不得不做出一些限制,来让使用者(也就是我们)以难受换来一些安全。

优点

简单来说,泛型的引入有以下好处:

程序更加易读

安全性有所保证

以ArrayList举例,在增加泛型类之前,其通用性是用继承来实现的,ArrayList类只维护一个Object引用的数组,当我们使用这个工具类时,想要获取指定类型的对象必须经过强转:

import java.util.ArrayList;
import java.util.Date;

public class Main {
  public static void main(String[] args) {
    ArrayList list = new ArrayList();
    //强制类型转换
    String res = (String) list.get(0);
    //十分不安全的行为
    list.add(new Date());
  }
}

这种写法在编译类型时不会报错,但一旦使用get获取结果并试图将Date转换为其他类型时,很有可能出现类型转换异常,为了解决这种情况,类型参数应用而生。

类型参数

类型参数(Type parameter)使得ArrayList以及其他可能用到的集合类能够方便的指示虚拟机其包含元素的类型:

import java.util.ArrayList;


public class Main {
  public static void main(String[] args) {
    ArrayList objects = new ArrayList<>();
    objects.add("Hello");
  }
}

这使得代码具有更好的可读性,并且在调用get()的时候,无需进行强转,最重要的是,编译器终于可以检查一个插入操作是否符合要求,运行时可能出现的各种类型转换错误得以在编译阶段就被阻止。

import java.util.ArrayList;
import java.util.Date;


public class Main {
  public static void main(String[] args) {
    ArrayList objects = new ArrayList<>();
    //we can do it like that
    objects.add("Hello");
    //wrong example
    objects.add(new Date());
  }
}

基本用法

一般来说,使用泛型工具类很容易,但是自己编写会相对困难很多,设计者必须考虑的相当周全才能使自己的泛型类库比较完善。

泛型类

泛型类是有一个或者多个类型变量的类,泛型类中的属性可以全都不是泛型,不过一般不会这样做,毕竟类型变量在整个类上定义就是用于指定方法的返回类型以及字段的类型,定义代码如下:

public class Animal {
  private String name;
  private T mouth;
  
  public T getMouth(){
    return mouth;
  }
}

泛型类可以有多个类型变量:

public class Animal {
  private String name;
  private T mouth;
  private U eyes;


  public T getMouth(){
    return mouth;
  }
}

泛型方法

泛型方法可以在普通类中定义,也可以在泛型类中定义,例如:

public class Animal {
  private T value;
  public static  T get(T... a){
    return a[a.length-1];
  }
  public T getFirst(){
    return value;
  }
}

类型擦除

虚拟机没有泛型类型对象,也就是说,所有对象在虚拟机中都属于普通类,这意味着在程序编译并运行后我们的类型变量会被擦除(erased)并替换为限定类型,擦掉类型参数后的类型就叫做原始类型(raw type),正是因为有类型参数,所以下面的比较结果会为true:

256ea9d8-9f6d-11ef-93f3-92fbcf53809c.jpg



这里的替换规则我个人理解为:“替换最近上界”,也就是无限定符修饰,则为顶级父类Object,如果有,则会替换为其指定的类型。最直观的示例如下,这就是类型擦除的体现:

257bed8c-9f6d-11ef-93f3-92fbcf53809c.jpg



259165e0-9f6d-11ef-93f3-92fbcf53809c.jpg



前面说过,泛型是在1.5才提出的,因此类型擦除的目的就是为了保证已有的代码和类文件依然合法,也就是向低版本兼容。这样做会带来几个问题:

1.类型参数不支持基本类型,只支持引用类型,这是因为泛型会被擦除为具体类型,而Object不能存储基本类型的值。

运行时你只能对原始类型进行类型检测

2598cc86-9f6d-11ef-93f3-92fbcf53809c.jpg



2.不能实例化类型参数

不能实例化泛型数组,因为类型擦除会将数组变为Object数组,如果允许实例化,极易造成类型转换异常。

强制转换

在编写泛型方法调用时,如果擦出了返回类型,编译器会插入强制类型转换。例如下面的代码:

public class Main {
  public static void main(String[] args) {
    Animal pair = new Animal<>();
    Integer first = pair.getFirst();
  }
}

getFirst擦除类型后的返回类型是Object,编译器自动插入转换到Integer的强制类型转换,也就是说,编译器把这个方法调用转换为两条虚拟机指令:

对原始方法的调用。

将返回的Object类型强制转换为Integer类型。

方法桥接

子类重写父类方法时,必须和父类保持相同的方法名称,参数列表和返回类型。那么问题来了,如果按照之前的思路来讲,当泛型父类或接口的类型参数被擦除了,那么子类岂不是不构成重写条件?(参数类型很可能变化):

擦除前:

25aec6c6-9f6d-11ef-93f3-92fbcf53809c.jpg



擦除后:

25b99844-9f6d-11ef-93f3-92fbcf53809c.jpg



为了解决这个事情,Java引入了桥接方法,为每个继承/实现泛型类/接口的子类服务,以此保持多态性,字节码如下:

25c50206-9f6d-11ef-93f3-92fbcf53809c.jpg



(图片来源:RudeCrab)

其实现原理,就是重写擦除后的父类方法,并在其内部委托了原始的子类方法,巧妙绕过了擦除带来的影响。不仅如此,就算不是泛型类,当子类方法重写父类方法的返回类型是父类返回类型的子类时,编译器也会生成桥接方法来满足重写的规则。

总结

Java核心威廉希尔官方网站 中总结的非常到位:

虚拟机中没有泛型,只有普通的类和方法。

所有的类型参数都会替换为他们的限定类型。

会合成桥接方法来保持多态。

为保持类型安全性,必要时会插入强制类型转换。

变型(Variant)与数组

变型是类型系统中很重要的概念,主要有三个规则协变,逆变,和不变:

25cecc46-9f6d-11ef-93f3-92fbcf53809c.jpg



这三个类型可以解释为:假设有一个类型构造器f,它可以将已知类型转换为另一种类型,那么,有Animal父类和Dog子类。

则f(Dog)是f(Animal)的子类,称为协变;

则f(Dog)是f(Animal)的父类,成为逆变;

则f(Dog)和f(Animal)没有任何关系;

而这个f(),可以是泛型,可以是数组,也可以是方法。

知道了以上概念,我们需要直接指出,泛型默认是不支持协变的,原因很简单,类型安全:如果允许协变,可能会造成类型转换异常。而数组支持协变,正如文章开头所说,就是设计者希望可以对数组进行比较通用的处理,防止方法为每一种类型编写重复逻辑,这样做也确实导致为数组赋值元素时可能会抛出运行时异常ArrayStoreException,这是一个很危险的坑。Effective Java中直接指出允许数组协变是Java的缺陷,我想这也是要多用列表而不用数组的原因之一。

泛型协变—PECS原则

为了让泛型也支持多态,让其支持协变是很必要的,最常用的场景:我们想让一个方法接受一个集合,并做统一的逻辑处理,如果泛型不支持协变,这种很基本的需求都会成为奢望。

上界

让泛型支持协变很简单,只需要使用? extends的组合即可实现,?称为通配符,这种组合方式声明了类型的上界,标识泛型可接受的类型只能是指定类型或是其子类。在这里,ElectricVehicle和Diesel均是继承自Car。

25d26144-9f6d-11ef-93f3-92fbcf53809c.jpg



为了杜绝可协变后出现类似于数组一样的安全隐患,泛型设计采用了“一刀切”的方式,即:只要声明了上界,除了null之外,一律不准传入给泛型。说白了,就是只读不写,这样当然可以保证安全性。

25f06be4-9f6d-11ef-93f3-92fbcf53809c.jpg



到这里可以顺便说一下集合的设计,可以注意到集合中只有add方法是泛型参数,而其余方法并不是,为何要这样设计,为何不把其余方法的参数类型也改为E?其原因就是在于,如果将contains和remove改为E,那么声明上界之后,调用这两个方法会引发编译错误,然而这两个方法均为类型安全方法,自然不可声明为E,add作为很明显的写方法,自然也需要用E作为参数类型,到这里,不得不感叹类库设计者的想法独到。

25fa262a-9f6d-11ef-93f3-92fbcf53809c.jpg



下界

对应协变的上界,自然有逆变的下界,很自然的,我们使用? super的组合来声明一个泛型的下界,来表示可以接收本类型或者其父类型。

2608e958-9f6d-11ef-93f3-92fbcf53809c.jpg



而且相对应的,正是由于最多只能接收父类型泛型,所以不会有类型转换失败的风险,因此逆变可以添加元素,不过添加的元素类型只能是指定类型和其子类,切记不要把添加元素和接收泛型类参数给弄混了。

有利有弊,虽然逆变没有了协变只读不写的限制,但是读取元素时将不能确定具体的类型,只能用Object来接收:

261b1fba-9f6d-11ef-93f3-92fbcf53809c.jpg



PECS

正如上面对上下界的描述,我们已经明白了大致的应用场景,当我们需要只读不写时,就用协变,只写不读,就用逆变。又想读又想写,我们应该指明准确的泛型类型。

注明的PECS原则就总结了这一点,PECS(Prodcuer extends Consumer super),也就是说,作为元素的生产者Prodcuer,要用协变,支持元素的读取,而作为消费者Consumer,要支持逆变,支持元素的写入。

2621182a-9f6d-11ef-93f3-92fbcf53809c.jpg



Collections的copy方法就非常好的印证了这一点:

262c6c66-9f6d-11ef-93f3-92fbcf53809c.jpg

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • JAVA
    +关注

    关注

    19

    文章

    2966

    浏览量

    104704
  • 程序
    +关注

    关注

    117

    文章

    3785

    浏览量

    81006
  • specs
    +关注

    关注

    0

    文章

    5

    浏览量

    1583

原文标题:深入了解Java泛型——从前世今生到PECS原则

文章出处:【微信号:OSC开源社区,微信公众号:OSC开源社区】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    如何为不同的电机选择合适的驱动芯片?纳芯微带你深入了解

    在现代生活中,电机广泛使用在家电产品、汽车电子、工业控制等众多应用领域,每一个电机的运转都离不开合适的驱动芯片。纳芯微提供丰富的电机驱动产品选择,本期威廉希尔官方网站 分享将重点介绍常见电机种类与感性负载应用,帮助大家更深入了解如何选择合适的电机驱动芯片。
    的头像 发表于 12-23 09:58 99次阅读
    如何为不同的电机选择合适的驱动芯片?纳芯微带你<b class='flag-5'>深入了解</b>!

    深入了解光伏逆变器测试系统

    的交流电(AC)。随着太阳能发电的广泛应用,确保光伏逆变器的可靠性和效率变得至关重要,这也使得光伏逆变器测试系统的作用愈发凸显。 一、光伏逆变器的工作原理 要深入理解光伏逆变器测试系统的必要性,首先需要了解逆变器的
    的头像 发表于 10-24 14:59 188次阅读
    <b class='flag-5'>深入了解</b>光伏逆变器测试系统

    深入了解PCI转XMC载板转接卡

    电子发烧友网站提供《深入了解PCI转XMC载板转接卡.docx》资料免费下载
    发表于 09-06 14:35 0次下载

    深入了解表面贴装晶体滤波器:DSF753S 2POLE、DSF753S 3POLE 和 DSF753S 4POLE

    深入了解表面贴装晶体滤波器:DSF753S 2POLE、DSF753S 3POLE 和 DSF753S 4POLE
    的头像 发表于 07-31 11:05 423次阅读
    <b class='flag-5'>深入了解</b>表面贴装<b class='flag-5'>型</b>晶体滤波器:DSF753S 2POLE、DSF753S 3POLE 和 DSF753S 4POLE

    深入了解 MEMS 振荡器 温度补偿 MEMS 振荡器 TC-MO

    深入了解 MEMS 振荡器/温度补偿 MEMS 振荡器(TC-MO)-μPower MO1534/MO1569/MO1576/MO8021
    的头像 发表于 07-30 16:38 500次阅读
    <b class='flag-5'>深入了解</b> MEMS 振荡器 温度补偿 MEMS 振荡器 TC-MO

    深入了解表面贴装晶体谐振器DSX1210A

    深入了解表面贴装晶体谐振器DSX1210A
    的头像 发表于 07-25 14:27 392次阅读
    <b class='flag-5'>深入了解</b>表面贴装晶体谐振器DSX1210A

    深入了解恒温晶体振荡器DC5032AS

    深入了解恒温晶体振荡器DC5032AS
    的头像 发表于 07-25 10:37 290次阅读
    <b class='flag-5'>深入了解</b>恒温晶体振荡器DC5032AS

    小熊派官网正式上线 可深入了解小熊派的各款开发套件

    我们为每一款开发板打造一个开源社区,让每一位开发者都能在这里找到归属感和灵感。通过官网对硬件、软件、案例和教程的开源,大家可以深入了解小熊派的各款开发套件。
    的头像 发表于 06-13 08:42 874次阅读
    小熊派官网正式上线 可<b class='flag-5'>深入了解</b>小熊派的各款开发套件

    深入了解IEEE协会:设备MAC地址申请指南

    在数字化浪潮中,设备之间的通信变得日益频繁和重要。而在这个通信网络中,每一台设备都需要一个独特的身份标识来帮助大家有效识别设备信息,那就是MAC地址。本篇内容,英利检测将带大家深入了解IEEE协会
    的头像 发表于 05-09 17:20 551次阅读
    <b class='flag-5'>深入了解</b>IEEE协会:设备MAC地址申请指南

    拆解FPGA芯片,带你深入了解其原理

    拆解FPGA芯片,带你深入了解其原理 现场可编程门阵列(FPGA)可以实现任意数字逻辑,从微处理器视频生成器或加密矿机,一应俱全。FPGA由许多逻辑模块组成,每个逻辑模块通常由触发器和逻辑功能
    发表于 04-17 11:07

    鸿蒙语言TypeScript学习第18天:【

    (Generics)是一种编程语言特性,允许在定义函数、类、接口等时使用占位符来表示类型,而不是具体的类型。
    的头像 发表于 04-16 14:56 369次阅读
    鸿蒙语言TypeScript学习第18天:【<b class='flag-5'>泛</b><b class='flag-5'>型</b>】

    深入了解影响ZR执行器性能的关键因素

    深入了解影响ZR执行器性能的关键因素-速程精密 在工业自动化领域,ZR执行器作为关键的终端设备,其性能的稳定性对于整个自动化系统的运行至关重要。了解影响ZR执行器性能的因素有助于更好地维护和优化其
    的头像 发表于 03-20 15:04 532次阅读
    <b class='flag-5'>深入了解</b>影响ZR执行器性能的关键因素

    S参数:深入了解与实际应用

    以一个无源二端口网络为例,深入介绍S参数。信号在传输过程中会产生入射波和反射波,既有进入端口的信号也有从端口中出来的信号。
    的头像 发表于 01-23 11:20 899次阅读
    S参数:<b class='flag-5'>深入了解</b>与实际应用

    深入了解RAG威廉希尔官方网站

    这是任何RAG流程的最后一步——基于我们仔细检索的所有上下文和初始用户查询生成答案。最简单的方法可能是将所有获取到的上下文(超过某个相关性阈值的)连同查询一起一次性输入给LLM。
    的头像 发表于 01-17 11:36 3188次阅读
    <b class='flag-5'>深入了解</b>RAG威廉希尔官方网站

    深入了解Linux中vi命令的使用

    深入了解Linux中vi命令的使用 VI是一款在Linux系统中使用的文本编辑器,它是一款功能强大、灵活性高的编辑器。VI编辑器具有非常高效的命令行操作方式,并且在各个版本的Linux中都得到了广泛
    的头像 发表于 12-25 11:15 456次阅读