这一文,关于Java泛型的点点滴滴 一

news2024/11/14 14:44:22

作为一个 Java 程序员,用到泛型最多的,我估计应该就是这一行代码:

List<String> list = new ArrayList<>();

这也是所有 Java 程序员的泛型之路开始的地方啊。

不过本文讲泛型,先不从这里开始讲,而是再往前看一下,看一看没有泛型的时候,Java 代码是怎么写的,然后我们才会知道为什么要加入泛型,泛型代码该怎么写。

这里插播一下我的微信公众号,希望大家能够多多关注,我会不定期更新优秀的技术文章:

接下来,开始我们的正文。

为什么要设计泛型

提高代码重用性

没有泛型之前,我们写一个两数相加的函数:

public static int add(int a, int b) {
    return a + b;
}

看似没问题,对吧。不过这个时候我们想计算 float 类型的加法,那这个函数就不行了,因为他只能计算 int 值。此时就只能再加入一个相同的函数了:

public static float add(float a, float b) {
    return a + b;
}

现在我们有两个方法能够计算 int 和 float 类型的加法。那现在如果要计算 String 类型的加法呢,这两个方法就又不够用了。面对这样的需求,在没有泛型的支持下,我们只能不断地增加逻辑基本相同的方法,代码重用性极低。
这就是泛型要解决的第一个问题:提高代码重用性。
那在泛型的加持下,我们如何编写这个函数呢?

public static <T extends Number> double add(T a, T b) {
    return a.doubleValue() + b.doubleValue();
}

这个方法使用了泛型,它能够处理任何类型的数字相加,不需要针对每个类型编写各自的加法方法。这就大大提高了代码的重用性,有了这个方法,那些固定类型的方法就都可以删了。
特别是一些逻辑相同的代码,使用泛型不仅能够提高代码重用性,还能够提高可读性。比如说下面这段代码,真是的是非常好用:

public static <T> void printArray(T[] array) {
    for (T element : array) {
        System.out.println(element);
    }
}

泛型的这个特性虽然很牛了,但是这还不是 Java 要设计泛型的全部原因。因为泛型还有一个作用,那就是保证类型安全。

保证类型安全

在说泛型的这个作用之前,先问大家一个问题,咱们常用的集合 ArrayList 是 Java 哪个版本加入的呢?泛型又是 Java 哪个版本加入的呢?

答案:ArrayList 是 Java 1.2 版本加入的,而泛型是 Java 1.5 加入的。

也就是说,有一段时时间,ArrayList 不是大家普遍认识的带泛型的 ArrayList<T> 这种形式,而是一个只能存放 Object 的列表。

在那一段泛型之光没有照耀到 Java 的日子里,保证类型安全成为了 Java 程序员在使用集合时不得不考虑的事情,考虑下面这一段代码:

ArrayList list = new ArrayList();
list.add("123");
// do some work......
Integer num = (Integer) list.get(0);

这段代码没有使用泛型来使用 ArrayList,我们加入了字符串 "123",但是在使用时,我们假定程序员忘记了加入的类型,他只记得好像应该是数字,于是在获取时就直接使用了 Integer 类强转。

这样的代码是能通过编译的,但是在运行的时候,会崩溃:

 Caused by: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer

这是一个典型的未使用泛型,而导致的类型安全无法保证,引发的崩溃。让程序员去保证类型安全,本身是不靠谱的做法,特别是在这种都是 Object 对象的列表中,鬼都不会知道存着的是个什么鬼。

这个时候就需要泛型出场了,泛型能够在编译时保证类型安全。例如上面的代码,我们加入泛型:

ArrayList<String> list = new ArrayList<>();
list.add("123");
Integer num = (Integer) list.get(0);

首先,ArrayList 加入泛型后,我们就知道这个列表是只能存入 String 类型的,也就不会将其转换为 Integer。那如果我非要转换呢,javac 编译器就会报错:

错误: 不兼容的类型: String无法转换为Integer
    Integer num = (Integer) list.get(0);

这样类型安全就可以在编译时得到保证,不会出现在运行时的崩溃。

例子的代码很简单,大家可能看不到这一点对于软件开发有多重要,在大型复杂的项目中,这种类型安全的保证,是能减少很多运行时的崩溃的。特别是,一般像这种类型不一致的崩溃很多都是偶现的,偶现的 BUG 是最恶心的,因此使用泛型保证类型安全是十分必要的。

消除强制类型转换

泛型的这个作用其实就是上面保证类型安全这一点带来的。没有用泛型时,需要我们使用强制类型转化,但是加入泛型后,编译器已经能够知道我们存入的是什么类型,因此也就不需要我们进行强制类型转换了。

既然泛型有那么大的作用,那我们就赶紧把泛型用起来吧。

使用泛型

这一节,我们来看看如何使用系统提供的泛型类,以及其中需要注意的事项。

最常用到泛型的地方便是集合了,使用这些泛型集合类时,只需要把具体泛型参数 <T> 替换为需要的类型即可,例如 ArrayList<String>ArrayList<Number>Map<String, Integer> 等。

如果在使用泛型类时不指定类型参数,编译器会给出警告,且只能将 <T> 视为 Object 类型。这个时候就需要程序员自己去保证类型安全了,因此强烈不建议这么做,因为这样容易将类型转换异常带到运行时中去。

使用泛型基本就需要注意以上两点,下面介绍一下在使用泛型时的注意事项,这也是大家很少关注到的向上转型的问题。

在 Java 中,ArrayList<T> 是实现了 List<T> 接口,也就是说它可以向上转型为 List<T>

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

那么问题就来了,当泛型参数不同时,还能向上转型么,说具体一点,ArrayList<String> 能转型为 List<Number> 么?

答案是不行的:

ArrayList List = new ArrayList<String>();    //Raw use of parameterized class 'ArrayList'
List<Integer> list = new ArrayList<String>();    //直接报错

为什么 Java 不允许这么转型呢?因为运行转型的话,那么对于一个 ArrayList<String> 的容器,我将其转型为 ArrayList<Integer> 就可以往里面加入 Integer 对象了,这明显会造成 ClassCastException。泛型的存在用于限定类型的,这么一搞,泛型就失去了其作用。

这里,大家可以简单理解为,当泛型参数不一样时,两个类就没有太大关系了。例如 ArrayList<Integer>List<Number> 两者完全没有继承关系。

编写泛型

知道怎么使用系统的泛型之后,我们现在就来看看如何编写自己的泛型类。

泛型作为对类型进行限制的一种方式,我们编写泛型代码,也就是对使用我们代码的人进行一种限制。在这种情况下,我们是作为其他程序员的底层,向上提供某种框架代码,让其他程序员能够在我们设定的框架中更容易地编写代码实现功能。这有点类似于库的开发者,或是框架开发者,作为这种角色,写好泛型代码就更显得尤为重要了。毕竟,你也不想让别人说,这代码写得就跟一坨屎一样吧。

编写泛型类

编写泛型类,是比普通类要复杂的。这里我们就用 Pair<F, S> 这个类作为目标,一步一步编写出一个合格的泛型类。Pair 类是 Android 开发中一个简单的使用工具类,用于存储一对相关联的对象。

我们的第一版 Pair 只能使用没有使用泛型:

public class Pair {
    public final String first;
    public final String second;
}

那这肯定是不行的,因为这个 Pair 只能存放 String 类型的 first 和 second,那了能够存放所有类型,我们就使用泛型 <T>

public class Pair<T> {
    public final T first;
    public final T second;
}

我们把 firstsecondT 来修饰,表示其这两个成员变量是 T 类型的。而这个 T 类型,Java 是不知道的,我们必须声明告诉 Java 这是一个类型,因此类名从 Pair 变成了 Pair<T>,后面的 <T> 就是我们的泛型类型声明。

上面的代码看上去没问题,但是这个 Pair<T> 只能存放的 firstsecond 必须是相同的类型 T,那不同类型的怎么办呢?这时候我们再加一个泛型不就行了:

public class Pair<F, S> {
    public final F first;
    public final S second;
}

在加入两个泛型之后,firstsecond 的类型对应不同的泛型,这样就可以表示不同的类型了,注意 FS 这两个不同的泛型都需要在类上进行声明。

我们在为 Pair<F, S> 添加个构造方法:

public class Pair<F, S> {
    public final F first;
    public final S second;

    public Pair(F first, S second) {
        this.first = first;
        this.second = second;
    }
}

这算是一个简单的泛型类,那接下来,我们再为它编写一个泛型方法。

编写泛型方法

此处的泛型方法是指静态方法,而不是成员方法。这两种方法在使用泛型时是有一些区别的,其中最重要的一点就是,静态方法是不能使用类上声明的泛型类型,必须得自己声明泛型类型。例如,下面的代码将编译错误:

public static class Pair<F, S> {
    public final F first;
    public final S second;
    
    //编译错误,F、S 类型不能在 static 方法上使用
    public static Pair<F, S> create(F a, S b) {
        return new Pair<F, S>(a, b);
    }
}

可以想一想,为什么静态方法不能使用类上已经声明的泛型类型呢?

在回答这个问题之前,我们可以先想一下,类上的泛型类型,是在什么时候确定下来的呢?是在类创建的时候,我们在 new 的时候是需要提供具体类型的,这个时候泛型就被具体化为某个特定类型。不同的对象可能被创建为不同的类型,而静态方法只跟类相关,跟具体对象无关,而这些泛型又是跟具体对象相关的。所以静态对象不能使用类上声明的泛型也就变得合理了。

那要想使静态方法使用泛型,那就必须这个静态方法自己声明泛型:

public static <F, S> Pair <F, S> create(F a, S b) {
    return new Pair<F, S>(a, b);
}

这个静态方法在函数名前使用 <F, S> 来声明了两个泛型,那么后续这两个泛型就可以在这个函数中使用了。此时注意,这里的 FS 虽然与 Pair 上的 FS 泛型看似相同,实际上是没有任何关系的。所以为了避免产生误会,一般都会使用不同的泛型名,例如将这个方法的 <F, S> 变成 <A, B>

public static class Pair<F, S> {
    public final F first;
    public final S second;

    public static <A, B> Pair <A, B> create(A a, B b) {
        return new Pair<A, B>(a, b);
    }
}

这样才能够清楚地将静态方法的泛型类型和实例类型的泛型类型区分开。

在使用时,我们可以使用如下代码创建一个 Pair<F, S> 实例:

Pair<String, Integer> pair = Pair.create("123", 123);

这里总结一下编写泛型需要注意的几点:

  • 编写泛型时,需要定义泛型类型 <T>
  • 静态方法不能引用类上的泛型类型 <T>,必须定义自己方法特有的泛型类型;
  • 泛型可以同时定义多个,例如 <F, S><F、S、T>

在这里我们需要注意泛型的一个限制,那就是不能使用泛型类型直接创建对象。这一点也好理解,T 是什么类型只有在使用时,指定了泛型的具体类型才能确定。T 类型是一个抽象的类型,它是无法直接 new 出来的,就像你无法直接 new 一个 interface 一样。例如下面的代码是错误的:

public static class Pair<F, S> {
    public final F first;
    public final S second;

    public Pair(F first, S second) {
        this.first = new F();        //错误
        this.second = new S();       //错误
    }
}

这里使用 F 类型的默认构造,设想一下假如这个类型被确定为一个没有默认构造方法的类型呢。所以使用泛型类型创建对象是不行的。

Java 的泛型实现方式:类型擦除

上面的几节介绍了泛型的好处,泛型的使用,那这一节我们就来看看 Java 是如何实现泛型技术的。

首先,泛型编程并非 Java 特有的,在其他语言 C++、C# 上都有类似的技术,只不过名称不同而已,例如 C++ 上叫模版。在这些技术的加持下,程序员可以编写与具体类型无关的代码,只需要在使用时指定具体类型,从而提高代码的复用性;并且在编译时进行类型检查,减少运行时错误。

Java 的泛型是通过类型擦除(Type Erasure)来实现的。也就是说在编译时将泛型类型擦除,替换为其上限类型(通常为 Object),并在必要时插入类型转换。这种机制在编译时处理泛型类型,而在运行时移除了所有的泛型信息,因此叫做类型擦除。

这也就意味着,Java 的泛型是由编译器实现的,在编译成 class 文件时类型信息已经被擦除了,因此运行时,Java 虚拟机是没有任何泛型信息的。

例如上面我们编写的 Pair 的这个类,在我们看来,它是这样的,在源代码阶段,里面是包含泛型信息的:

public static class Pair<F, S> {
    public final F first;
    public final S second;

    public Pair(F first, S second) {
        this.first = first;
        this.second = second;
    }

    public static <A, B> Pair<A, B> create(A a, B b) {
        return new Pair<A, B>(a, b);
    }
}

那么在虚拟机的视角,它是这样的:

public class Pair {
    private Object first;
    private Object last;
    public Pair(Object first, Object last) {
        this.first = first;
        this.last = last;
    }
}

从这里就能看到,这个 Pair 在运行时已经没有泛型信息了,所有的泛型类型都被替换为了 Object

那么既然我们定义的泛型类型最终都变成了 Object,那我们就知道了 Java 泛型的一个局限:泛型类型 <T> 不能是基本类型。
因为像 intfloat 这些基本类型不是 Object 的子类,所以我们必须使用包装类:

Pair<float, int> pair = Pair.create(3.15, 123);    //编译错误
Pair<Float, Integer> pair = Pair.create(3.15F, 123);    //编译通过

尽管 Java 的泛型在编译时通过类型擦除机制移除了泛型类型信息,但 Java 编译器会在 class 文件中保留一些泛型信息,以便工具和开发人员能够利用这些信息进行反射和调试。所以如果大家把这个类编译为 class 文件之后,再查看它的反编译的内容,会发现它是有一些泛型信息的。但这并不意味着 JVM 在运行时会携带这些类型信息,既然是类型擦除,也就是说泛型类型参数被擦除并替换为其边界类型,如果没有指定边界,则默认为 Object

这里又引入了边界类型这个概念,在下一篇文章中,我们就来详细聊聊这个边界类型,这也是泛型中比较重要和难的点。

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

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

相关文章

CVPR 2024最佳论文分享:Mip-Splatting: 无混叠3D高斯溅射

本推文详细介绍了CVPR 2024最佳论文提名《Mip-Splatting: Alias-free 3D Gaussian Splatting》。该论文的第一作者为 Zehao Yu&#xff08;图宾根大学在读博士&#xff0c;导师&#xff1a;Andreas Geiger &#xff09;。论文提出了一种名为Mip-Splatting的方法&#xff0c;用于…

树和二叉树(不用看课程)

1. 树 1.1 树的概念与结构 树是⼀种非线性的数据结构&#xff0c;它是由 n&#xff08;n>0&#xff09; 个有限结点组成⼀个具有层次关系的集合。把它叫做树是因为它看起来像⼀棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的。 • 有⼀个特殊的结点&am…

大语言模型赋能设施农业:透过“智慧大脑“看智能环境调控

&#xff08;文/ 于景鑫 北京市农林科学院&#xff09;在上一篇专栏文章中,我们从宏观视角探讨了大语言模型为设施农业插上腾飞之翼的广阔前景。而要真正实现这一愿景,还需要在微观层面深入剖析LLM的技术原理和应用路径。本文将聚焦设施农业的核心环节之一——环境调控,看看&qu…

【解决】ubuntu20.04 root用户无法SSH登陆问题

Ubuntu root用户无法登录的问题通常可以通过修改‌SSH配置文件和系统登录配置来解决。 修改SSH配置文件 sudo vim /etc/ssh/sshd_config 找到 PermitRootLogin 设置&#xff0c;并将其值更改为 yes 以允许root用户通过SSH登录 保存并关闭文件之后&#xff0c;需要重启SSH服务…

Xshell、XFTP的安装配置及其使用

Xshell、XFTP的安装配置及其使用 Xshell的优点 安全远程连接&#xff1a; Xshell 使用 SSH 协议等安全协议进行远程连接&#xff0c;确保数据传输的加密和安全性。多会话管理&#xff1a; 用户可以同时管理多个远程连接&#xff0c;方便在不同服务器之间切换和操作。终端仿真…

html+css+js 实现马赛克背景按钮

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享htmlcss 绚丽效果&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 文…

python爬虫-事件触发机制

今天想爬取一些政策&#xff0c;从政策服务 (smejs.cn) 这个网址爬取&#xff0c;html源码找不到链接地址&#xff0c;通过浏览器的开发者工具&#xff0c;点击以下红框 分析预览可知想要的链接地址的id有了&#xff0c;进行地址拼接就行 点击标头可以看到请求后端服务器的api地…

女人内裤怎么洗才是最干净?内衣裤洗衣机怎么样?哪个牌子更好?

最近刚好用到一款比较好用的洗内衣裤洗衣机&#xff01;如果你也和我一样有洗内衣裤烦恼的&#xff0c;或者可以看看&#xff01; 内衣裤作为贴身穿的衣服&#xff0c;我是不会把它和外衣一起清洗的&#xff0c;而家里面的大洗衣机已经担起了清洗外衣的工作&#xff01; 朋友们…

React Router-v6.25.1

以下例子是根据vitereactts构建的&#xff0c;使用路由前先安装好这些环境&#xff01;&#xff01;&#xff01;&#xff01; 1、路由的简单使用 首先要创建一个浏览器路由器并配置我们的第一个路由。这将为我们的 Web 应用启用客户端路由。 该main.jsx文件是入口点。打开它…

【杰理蓝牙开发】AC695x 音频部分

本文主要记录 杰理蓝牙audio接口的使用&#xff0c;包括ADC和DAC原理的介绍和API接口的使用。 【杰理蓝牙开发】AC695x 音频部分 0. 个人简介 && 授权须知1. ADC【音频数据采集】硬件部分1.1 单片机引脚1.2 硬件电路设计1.3 MIC 输入通路解释 2. 【DAC】音频信号编解码…

GLSL教程 第9章:计算着色器

目录 9.1 计算着色器的基本概念 计算着色器的主要特点&#xff1a; 9.2 计算着色器的基础知识 1. 创建计算着色器 计算着色器代码&#xff1a; 2. 编译和链接计算着色器 示例代码&#xff1a; 3. 执行计算着色器 示例代码&#xff1a; 9.3 实现并行计算和数据并行处理…

51单片机-第五节-串口通信

1.什么是串口&#xff1f; 串口是通讯接口&#xff0c;实现两个设备的互相通信。 单片机自带UART&#xff0c;其中引脚有TXD发送端&#xff0c;RXD接收端。且电平标准为TTL&#xff08;5V为1,0V为0&#xff09;。 2.常见电平标准&#xff1a; &#xff08;1&#xff09;TTL电…

Mysql中如何实现两列的值互换?给你提供些思路。

文章目录 Mysql中如何实现两列的值互换1、第一感觉此sql应该能处理问题了2、需要一个地方存要替换的值&#xff0c;不然两列搞不定。2.1 加第三列&#xff1f;&#xff08;能解决&#xff0c;但是看起来呆呆&#xff09;2.2 上临时表&#xff08;搞点弯路走走&#xff09; 示例…

C语言画蜡烛图

GPT-4o (OpenAI) 在C语言中&#xff0c;绘制蜡烛图&#xff08;Candlestick Chart&#xff09;不是直接的任务&#xff0c;因为C语言本身不包含高级图形绘制库。然而&#xff0c;可以通过某些图形库来完成这项任务&#xff0c;例如使用GTK、SDL、OpenGL等。 以下是通过GTK库绘…

【iOS】—— retain\release实现原理和属性关键字

【iOS】—— retain\release实现原理和属性关键字 1. retain\reelase实现原理1.1 retain实现原理1.2 release实现原理 2. 属性关键字2.1 属性关键字的分类2.2 内存管理关键字2.2.1 weak2.2.2 assgin2.3.3 strong和copy 2.4 线程安全的关键字2.5 修饰变量的关键字2.5.1常量const…

北京率先建设AI原生城市,力争明年推出百个优秀行业大模型产品

7月26日&#xff0c;《北京市推动“人工智能”行动计划&#xff08;2024-2025年&#xff09;》&#xff08;简称《行动计划》&#xff09;正式向社会发布&#xff0c;新京报记者在北京市发展和改革委员会举行的新闻发布会上获悉&#xff0c;北京将率先建设AI原生城市&#xff0…

基于JSP的班级同学录网站

你好呀&#xff0c;我是计算机学姐码农小野&#xff01;如果有相关需求&#xff0c;可以私信联系我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;JSPB/S架构 工具&#xff1a;Eclipse、Mysql 系统展示 首页 管理员功能界面 用户功能界面 论坛管…

ubuntu上部署vue项目到ngixn中+SpringBoot项目+postgresql数据库

文章目录 前提1.Ubuntu上安装ngix2.部署Vue项目2.1上传vue项目2.2.配置 3.Ubuntu上安装Postgres4.部署springboot项目 前提 记一次在ubuntu部署前端vue和后端springboot项目&#xff0c;以及数据库postgresql的安装以及启动、停止等常用的命令。 1.Ubuntu上安装ngix 1、检查…

探索 Python 的色彩世界:Colorama 库深度解析

文章目录 &#x1f308; 探索 Python 的色彩世界&#xff1a;Colorama 库深度解析背景&#xff1a;为何选择 Colorama&#xff1f;Colorama 是什么&#xff1f;如何安装 Colorama&#xff1f;简单库函数使用方法场景应用示例常见问题及解决方案总结 &#x1f308; 探索 Python …