Java泛型,这一篇就够了

news2024/10/5 22:22:25

1. 为什么我们需要泛型

现实世界中我们经常遇到这样一种情况,同一个算法/数据结构适用于多种数据类型,我们不想为每一种类型单独写一个实现。举个例子来说,我们有一个Pair类型,存储key、value两个字段,代码如下。如果有一天,我想存储Integer类型的key, Date类型的Value,这个时候,我需要重新定义一个Pair类型。

public class Pair {
    private String key;
    private String value;
}
public class PairIntDate {
    private Integer key;
    private Date value;
}

存储的需求是多种多样的,慢慢地我们会有一大堆PairXxxYyy类型,重复定义的Class会大量冗余。于是人们想到了第二个方案,用Object引用存储,使用的时候再做强制类型转换。

public static class Pair {
    public Object key;
    public Object value;
}

使用的时候,写入可以直接赋值,读取的时候强制类型转换。Java的ArrayList就是这么做的,通过维护一个Object[] elementData实现数据的存储。

Pair p = new Pair();
p.key = "stringKey";
p.key = Integer.valueOf(1);
String key = (String) p.key;

这种方法带来了两个问题:

  1. 读取时要强制类型转换
  2. 没有编译检查,能写入任意类型数据,导致读取时失败

Java 5开始引入的泛型就是为了解决这个问题的,使用泛型时,我们可以这样定义Pair并使用,完美的解决了上面两个问题。

public class RawArray {



    public static class Pair<K, V> {
        private K key;
        private V value;

        public K getKey() {
            return key;
        }

        public void setKey(K key) {
            this.key = key;
        }

        public V getValue() {
            return value;
        }

        public void setValue(V value) {
            this.value = value;
        }
    }
    public static void main(String[] args) {
        Pair<String, Integer> pair = new Pair<>();
        pair.setKey("pairKey");
        pair.setValue(Integer.valueOf(1));
        Integer localValue = pair.getValue();
    }      

}

2. 泛型的实现原理

2.1 类型擦除

JVM内并没有泛型这个概念,所有的类型都是普通的类。以上面的RawArray为例,我们看看Java内部是怎么做的。通过javap命令查看class文件的字节码,命令如下:

javap -verbose RawArray$Pair.class
javap -verbose RawArray.class

Pair类的字节码如图,实际存在的字段key、value都是Object类型,getKey方法实际返回值类型是Object,setKey的入参实际类型也是Object类型,这就是我们经常说的类型擦除,泛型会擦除到定义的上界。

下面我们看看main方法里是如何使用Pair对象的,调用setKey方法时的类型检测是编译器行为,在字节码中并没有体现。getValue方法返回的是Object类型对象,编译器插入了checkcast命令替我们完成类类型转换。

2.2 桥接方法

从上一节学习的Pair字节码我们知道,Pair类的setKey方法的参数、getKey方法的返回值都被擦除为Object类型。如果我们继承Pair, 并重写setKey、getKey方法。

public static class ExtendPair extends Pair<String, Integer> {

    public String getKey() {
        return super.getKey();
    }

    public void setKey(String key) {
        super.setKey(key);
    }
}

回忆一下Java的基础知识, Java的方法签名指的是方法名和参数列表,父类Pair有两个方法:

  1. Object getKey()
  2. void setKey(Object)

子类ExtendPair重写/新增了两个方法

  1. String getKey()
  2. void setKey(String key)

两个setKey的方法签名不同,实际是两个重载方法,而getKey方法签名相同,返回值却不同,Java语言规范内是不允许的。如果我将ExtendPair向上转型为Pair并调用getKey和getValue方法会怎么样呢?通过阅读ExtendPair的字节码,我们找到了答案。

Java语言规范中不允许同时定义String getKey()、Object getKey()两个签名相同的方法,但是字节码层面是允许的,通过ExtendPair的字节码,生成的Object getKey()会调用String getKey()方法,这个方法被称为桥接方法,向上转型为Pair后,实际调用的是Object getKey()方法,签名和父类完全一致,然后由它转发个String getKey()方法。

setKey的桥接方法实现和getKey如出一辙,这里就不详细讲解了,有兴趣可以看看对应的字节码。值得一提的是,在类继承时提到的协变返回类型也是通过桥接方法实现的。

3. Java泛型的定义

3.1 泛型方法

之前的案例里我们已经看到泛型类( RawArray$Pair)的定义了,泛型类的类型参数不能用到静态方法上。我们来看一下泛型方法的定义,类型参数放到修饰词之后,返回值之前,看示例的of方法。通常调用泛型方法时我们不需要明确地知道类型参数,编译器自己能通过入参/返回值接收对象的引用推断出类型参数。

public static class Pair<K, V> {
    private K key;
    private V value;

    public static <T,S> Pair<T,S> of(T k, S v) {
        Pair<T,S> r = new Pair<>();
        r.key = k;
        r.value = v;
        return r;
    }
}

通过这么使用我们定义的泛型方法

var aPair = Pair.of("pairKey",1);
System.out.println(aPair.getKey());

3.2 类型限定

讲Java泛型的原理的时候,我们讲过Java会将泛型的类型擦除,最后存储的是Object类型的引用。Java并不支持动态语言里的Duking Type,这个时候如果我们想调用Pair对象里key、value对象上的方法,我们只能调用Object上定义的方法。如果我们想调用指定类型下的方法,就需要用到类型限定。

我们通过一个例子来看,假设我们有一个Range类,接收两个值来表示一段区间,这两个值必须支持比较(Comparable),同时我们希望在较小的实例上执行某些操作(包装在Runnbale中),代码如下

public static class Range<T extends Comparable<T> & Runnable> {
    private T min;
    private T max;
    

    public static <S extends Comparable<S> & Runnable> Range<S> of(S s1, S s2) {
        if (s1.compareTo(s2) > 0) {
            S temp = s1;
            s1 = s2;
            s2 = temp;
        }
        s1.run();
        Range<S> range = new Range<>();
        range.min = s1;
        range.max = s2;
        return range;
    }
}

示例中的S extends Comparable<S> & Runnable就是我们说的类型限定,如果有多个限定类型用&号分隔,Java允许类继承一个父类多个接口,指定类型限定的时候,通过extends关键字,后面接一个0或1个父类,0或N个接口,如果有的话,父类限定要放到最前面。

可以看到Comparable.compareTo方法、Runnable.run方法都可以直接在泛型方法内部调用,虽然还是要求用接口做类型限定,比起Duking Type还略有缺憾,但应该说已经不错了。

3.3 通配符类型

假设我们有3个类,Animal表示动物,Dog是Animal的子类,Cage是一个泛型类,可以持有一个对象的引用,代码如下

public static class Animal {
}

public static class Dog extends Animal {
}

public static class Cage<T> {
    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}

各个类直接的关系如下图,Animal和Dog有继承关系,Cage和Cage并没有,最右侧的一列我们稍后再讲。

如果我们一个方法delivery用于投递Cage,如果我们使用Cage调用这个方法,编译器提示Required Cage, Provide Cage

private static void delivery(Cage<Animal> cage)

使用通配符可以解决这个问题,Cage表示持有某个Animal的子类,这个类型可以是Animal,也可以是Dog

private static void delivery(Cage<? extends Animal> cage)

除了子类型限定以外,还有超类型限定,无限定通配符

类型

语法

简记

描述

子类限定

Cage

能调Cage.getT方法,用Animal接受返回值;不能调Cage.setT,T是继承Animal的类,不知道确切类型

超类限定

Cage

能调Cage.setT方法,用Animal子类做入参;不能调Cage.getT,T是Animal的超类,不知道确切类型

无限定通配符

Cage

读Object

能调Cage.getT方法,用Object接收返回值;不能调Cage.setT,不知道T的确切类型

4. Java泛型的局限

4.1 类型检查只能用于原始类型

由于类型擦除的原因Pair和Pair都会将类型参数擦除到Object,所以这两个泛型类型的实例变量的Class实际是相同的

Pair<Integer,Integer> pi = new Pair<>();
Pair<Long,Long> pl = new Pair<>();
System.out.println(pi.getClass() == pl.getClass());

调用instanceof时,下面两个判断都会发true,而对pi instanceof Pair检查时,会报编译错误。

if (pi instanceof Pair<Integer, Integer>) {
    System.out.println("Pair<Integer,Integer>");
}
if (pi instanceof Pair) {
    System.out.println("Pair");
}

4.2 不能参数化类型数组

不能创建泛型类型的数组,数组会存储元素的类型,通过Class.getComponentType()获取数组元素的类型,Class.getComponentType()只能元素的原始类型,运行时无法做到阻止往Pair[]的数组中添加Pair的元素。但是编译器允许定义泛型数组的变量,定义一个Pair的数组后强制类型转换为泛型数组。

Java不允许我们创建泛型对象的数组,可变参数数组的时候这个限制略有放松,允许我们定义泛型的可变参数列表,下面这段代码是允许的

private void varArgs(Pair<String, Integer>... ps) {
    // code goes here
}

4.3 无法实例化类型变量

因为类型擦除的作用,我们无法通过T.class引用到Class对象,当然更无法通过T.class创建对象的实例。早期我们通过传入Class clazz来完成实例的创建。Java 8之后可以通过函数式接口来创建。

public static class Holder<T> {
    public T makeT() {
        return T.class.newInstance(); // 无法正常允许,不能通过T.class因为到Class对象
    }
    public T makeT(Class<T> clazz) throws Exception {
        return clazz.getConstructor().newInstance();
    }
    public T makeT(Supplier<T> supplier) {
        return supplier.get();
    }
}

泛型数组的创建也可以采用类似的方法,传入Class clazz或者传入一个函数式接口来创建

public static class Holder<T> {
    public T[] makeT(Class<T> clazz) {
        return (T[]) Array.newInstance(clazz, 10);
    }
    public T[] makeT(IntFunction<T[]> make) {
        return make.apply(10);
    }
}
// 调用方式
Holder<String> ss = new Holder<>();
System.out.println(Arrays.toString(ss.makeT(String.class)));
System.out.println(Arrays.toString(ss.makeT(String[]::new)));

4.4 不能定义/抛出/捕获泛型异常

继承自Throwable的类不能是泛型类型,catch的括号里不能用类型变量。下面的throwsAs是CoreJava里的一个示例,通过欺骗编译器允许方法不声明检查型异常。

public static <T extends Throwable> void throwsAs(Throwable t) throws T {
    throw (T) t;
}


public T makeT(Class<T> clazz) {
    try {
        return clazz.getConstructor().newInstance();
    } catch (Throwable t) {
        GenInstance.<RuntimeException>throwsAs(t);
        return null;
    }
}

5. Java泛型的反射

Java反射的信息最终来自.class文件,而在编译的时候我们并不知道类会怎么被使用。假设我们定义了Company>类,使用时Company、Company引用的是同一个Class对象。为了方便讲解,我们定义如下的测试类

public static class KeyNiuTech extends Company<Date> {
}

public static class Company<T extends Comparable<? super T>> {
    private T[] staff;
    private T aStaff;
    public static <O extends Comparable<? super O>> Company<O> of(O o) {
        Company<O> c = new Company<>();
        c.aStaff = o;
        return c;
    }
}

通过查看这两个的字节码可以看到,这两个能拿到的信息的上限,Company能拿到类型参数T以及T的上限Comparable,以及Comparable的类型参数。KeyNiuTech里更有意思一点,KeyNiuTech继承自Company,这里的这个类型Date也被保存了,记不记得我们解析JSON时经常需要传递一个TypeReference的匿名子类?

Java提供了5种类型来支持泛型的泛型,类型之间的关系如下图

我们来看一下每种实现类分别代表着哪种类型的数据

类型

说明

举例

Class

具体类型

KeyNiuTech.class、Company.class

TypeVariable

类型变量

T extends Comparable

WildcardType

通配符

? super T

ParameterizedType

泛型类

Company、Comparable

GenericeArrayType

泛型数组

Company.staff通过Field获取后,读getGenericeType()返回的就是这个类型

最后一个示例结尾,这段代码用于打印类的定义信息

private static StringBuilder classDetail(Class clazz) {
    StringBuilder sb = new StringBuilder();

    int modifier = clazz.getModifiers();
    sb.append(Modifier.toString(modifier)).append(" ");
    sb.append(clazz.getName());

    TypeVariable[] tvs = clazz.getTypeParameters();
    if (tvs != null && tvs.length > 0) {
        sb.append("<");
        for (TypeVariable tv : tvs) {
            sb.append(tv.getName());
            Type[] bounds = tv.getBounds();
            if (bounds != null && bounds.length > 0) {
                sb.append(" extends ");
                for (Type bound : bounds) {
                    sb.append(bound.getTypeName());
                }
            }
        }
        sb.append(">");
    }

    Type superClass = clazz.getGenericSuperclass();
    if (superClass instanceof Class) {
        sb.append(" extends ");
        sb.append(superClass.getTypeName());
    } else if (superClass instanceof ParameterizedType) {
        ParameterizedType pt = (ParameterizedType) superClass;
        sb.append(" extends ");
        sb.append(pt.getRawType().getTypeName());
        sb.append("<");
        Type[] actualTypes = pt.getActualTypeArguments();
        for (Type at : actualTypes) {
            sb.append(at.getTypeName());
        }
        sb.append(">");
    }

    Type[] superInterfaces = clazz.getGenericInterfaces();
    for (Type ifs : superInterfaces) {
    }

    return sb;
}

输出示例

public static com.company.generic.GenericReflect$KeyNiuTech extends com.company.generic.GenericReflect$Company<java.util.Date>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1669345.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

机器视觉技术精准测量点胶高度与宽度:提升生产质量的新利器

在现代化生产线中&#xff0c;点胶工艺是许多产品制造过程中的重要环节。点胶的高度和宽度直接影响到产品的质量和性能。传统的测量方法往往效率低下、精度不高&#xff0c;而机器视觉技术的引入&#xff0c;为点胶高度和宽度的测量带来了革命性的变革。本文将探讨机器视觉如何…

如何解决pycharm在HTML文件中注释快捷键出错的问题(HTML注释规则出错)

文章目录 💢 问题 💢🏡 演示环境 🏡💯 解决方案 💯⚓️ 相关链接 ⚓️💢 问题 💢 你是否在编程时遇到过这样的烦恼?当你正专注地编写HTML代码,想要快速注释掉某部分内容时,却发现PyCharm的注释快捷键失灵了(没有使用正确的注释格式)。这不仅打断了你的工作…

目标检测——DAGM2007纹理背景缺陷数据集

引言 亲爱的读者们&#xff0c;您是否在寻找某个特定的数据集&#xff0c;用于研究或项目实践&#xff1f;欢迎您在评论区留言&#xff0c;或者通过公众号私信告诉我&#xff0c;您想要的数据集的类型主题。小编会竭尽全力为您寻找&#xff0c;并在找到后第一时间与您分享。 …

Puppeteer的基本使用及多目标同时访问

文章目录 一、安装 puppeteer 并更改默认缓存路径1、更改 Puppeteer 用于安装浏览器的默认缓存目录2、安装 puppeteer3、项目结构目录 二、基本使用1、启动浏览器并访问目标网站2、生成截图3、生成 PDF 文件4、获取目标网站 html 结构并解析5、拦截请求6、执行 JavaScript7、同…

(Java)心得:LeetCode——18.四数之和

一、原题 给你一个由 n 个整数组成的数组 nums &#xff0c;和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] &#xff08;若两个四元组元素一一对应&#xff0c;则认为两个四元组重复&#xff09;&#xff1a; …

【CTF Web】QSNCTF 文章管理系统 Writeup(SQL注入+Linux命令+RCE)

文章管理系统 题目描述 这是我们的文章管理系统&#xff0c;快来看看有什么漏洞可以拿到FLAG吧&#xff1f;注意&#xff1a;可能有个假FLAG哦 解法 SQL 注入。 ?id1 or 11 --取得假 flag。 爆库名。 ?id1 union select 1,group_concat(schema_name) from information_sch…

反调试 - ptrace占坑

ptrace占坑 这是ptrace占坑的标志。 ptrace可以让一个进程监视和控制另一个进程的执行,并且修改被监视进程的内存、寄存器等,主要应用于调试器的断点调试、系统调用跟踪等。 在Android app保护中,ptrace被广泛用于反调试。一个进程只能被ptrace一次,如果先调用了ptrace方法,那…

AI办公自动化-用kimi把PDF文档按照章节自动拆分成多个docx文档

一个PDF文档很长&#xff0c;希望按照章节分拆成小文档。 可以在kimichat中输入提示词&#xff1a; 你是一个Python编程专家&#xff0c;要完成一个编写拆分PDF文档的Python脚本的任务&#xff0c;具体步骤如下&#xff1a; 打开文件夹&#xff1a;D:\chatgpt图书\图书1&…

爬虫工作量由小到大的思维转变---<第七十三章 > Scrapy爬虫详解一下HTTPERROE的问题

前言&#xff1a; 在我们的日常工作中&#xff0c;有时会忽略一些工具或组件的重要性&#xff0c;直到它们引起一连串的问题&#xff0c;我们才意识到它们的价值。正如在Scrapy框架中的HttpErrorMiddleware&#xff08;HTTP错误中间件&#xff09;一样&#xff0c;在开始时&…

JVM调优:JVM中的垃圾收集器详解

JVM&#xff08;Java Virtual Machine&#xff09;垃圾收集器是Java虚拟机中的一个重要组件&#xff0c;负责自动管理Java堆内存中的对象。垃圾收集器的主要任务是找出那些不再被程序使用的对象&#xff0c;并释放它们占用的内存&#xff0c;以便为新的对象分配空间。这个过程被…

ES6 笔记02

目录 01 对象的扩展 02 链判断运算符 03 属性名表达式 04 Symbol 类型 05 set集合的使用 06 Map集合的使用 07 Set集合和Map集合的遍历方式 08 iterator迭代器 01 对象的扩展 对象的属性和方法的简洁表示: es6允许在字面量对象里面直接写变量名 let 变量名变量值; let …

Hexo博客重新部署与Git配置

由于电脑重装了一次&#xff0c;发现之前Hexo与NexT主题版本过于落后&#xff0c;重新部署了下。 1 Node.js与git安装 这一块安装就不赘述了。去两个官网找安装文件安装即可。 node.js git 打开git以后配置的几个关键命令行。 git config --global user.name "你的gi…

langchain 自定义模型使用

目录 背景 参考 实现 调用 背景 在公司有大模型可以通过 api 方式调用&#xff0c;想使用 langchain 框架调用&#xff0c;langchina 已经封装好大部分模型了&#xff0c;但自己公司的模型不支持&#xff0c;想使用&#xff0c;相当于自定义模型 参考 Custom Chat Model …

基于Springboot的家教管理系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的家教管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&…

Idea插件Easy-Code模板文件

目录 需要引入的依赖application.yml.vmapplication-dev.yml.vmresult.java.vm (统一返回集)resultCodeEnum.java.vm &#xff08;统一返回集需要的枚举类&#xff09;globalCorsConfig.java.vm &#xff08;全局跨域处理&#xff09;entity.java.vm &#xff08;实体类&#x…

【科研绘图 基础版】01 使用Python绘制时间序列折线图

下面这段代码绘制了一个折线图&#xff0c;其中包含了实际平均温度数据和使用线性回归模型预测的平均温度数据&#xff08;用来近似地表示数据的整体趋势&#xff09;。 具体来说&#xff0c;图中的横轴表示年份&#xff0c;纵轴表示平均温度。蓝色的实心线代表了实际的平均温度…

【Mac】Perfectly Clear Workbench(智能图像清晰修复软件)安装教程

软件介绍 Perfectly Clear Workbench是由Athentech Imaging开发的一款图像处理软件&#xff0c;旨在帮助用户快速、轻松地优化和改善数字照片的质量。以下是Perfectly Clear Workbench的一些主要特点和功能&#xff1a; 1.自动图像优化 该软件采用先进的图像处理算法&#xf…

StarRocks 【新一代MPP数据库】

1、StarRocks 1.1、StarRocks 简介 StarRocks 是新一代极速全场景 MPP (Massively Parallel Processing&#xff0c;MPP数据库是一种基于大规模并行处理技术的数据库系统&#xff0c;旨在高效处理大量数据。) 数据库。StarRocks 的愿景是能够让用户的数据分析变得更加简单和敏…

设计模式:迭代器模式(Iterator)

设计模式&#xff1a;迭代器模式&#xff08;Iterator&#xff09; 设计模式&#xff1a;迭代器模式&#xff08;Iterator&#xff09;模式动机模式定义模式结构时序图模式实现在单线程环境下的测试在多线程环境下的测试模式分析优缺点适用场景应用场景参考 设计模式&#xff1…

第五百回 Get路由管理

文章目录 1. 概念介绍2. 使用方法2.1 普通路由2.2 命名路由 3. 示例代码4. 内容总结 我们在上一章回中介绍了"使用get显示Dialog"相关的内容&#xff0c;本章回中将介绍使用get进行路由管理.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在本章…