深入浅出Java泛型

公众号「稀有猿诉」        原文链接 深入浅出Java泛型

温故而知新,可以为师矣!

在前面的一篇文章中学习了Kotlin的泛型知识,但总感觉还不够深入,因为一些深入的话题和高级的特性并未有讲清楚。但在继续深入之前还是有必要重温一下Java的泛型知识,这是因为Kotlin是基于JVM的语言,并且与Java关系暧昧,它可以与Java混合使用,可以相互调用,在某种程度上讲Kotlin可以视为Java的一种『方言』。所以,我们先回顾Java的泛型,夯实基础,并弄清楚Java泛型遗留了哪些问题,然后再看看Kotlin是如何解决这些问题的。

基础使用方法

还是要从基本的使用方法来谈起。

泛型(Generics)就是在类或者方法定义的时候并不指定其操作数据的具体类型,而是用一个虚拟的名字**<T>代替,类的使用者或者方法的调用在使用时提供具体的类型,以达到类和方法能对所有的类型都能使用的目录。可以把泛型理解为参数化,也就是说定义的时候把其操作的数据类型视为一种参数,由使用者在使用时具体指定(创建对象时或者调用方法时),因此泛型也可以称为参数化类型**。有3个地方可以使用泛型,类,接口和方法,接下分别来看一下具体如何使用。

泛型类

泛型类,也即参数化类型的类,是最为常见的一种泛型的使用方式。这些类可以视为元类,它会操作另一个类型,比如存储或者加工,类本身的实现重点在于如何操作,而对于这个『另一个类型』具体是什么,并不关心。这时就可以用泛型,在定义类的时候并不指定具体的类型,而是用一个虚拟的类型来代替,由类的使用者在使用的时候来指定具体的类型:

class ArrayList<E> {
	public void add(E e) { ... }
	public E get(int index) { ... }
}

这里ArrayList是一个容器,可以以线性的方式来存储任意其他类型,具体是啥其实ArrayList并不关心,所以这里用泛型,E就是参数化类型,代指某一个类型。使用时需要提供具体的类型,可以Integer,String,或者定义好了的任何一种类型(Class):

ArrayList<String> players = new ArrayList<>();
players.add("James");
players.add("Kevin");
System.out.println("#1 is " + players.get(0));
System.out.println("#2 is " + players.get(1));
// #1 is James
// #2 is Kevin

小结 一下,泛型是为了增强代码的复用,定义时用尖括号<>表示的参数化类型Parameterized type,拼接在类名字的后面,使用时再指定具体的类型。并且,当编译器能推断出参数类型时,可以用钻石符号(Diamond operator)<>来省略参数类型名字。

泛型接口

泛型可以用于接口的声明,与类一样,把类型参数化即可:

interface Consumer<T> {
	void consume(T t);
}

泛型方法

除了类和接口,方法也可以使用泛型,把用尖括号表示的参数化类型<T>放在方法的返回类型之前就可以了:

public <T> ArrayList<T> fromArrayToList(T[] a) { ... }

String[] names = {"James", "Kevin", "Harden"};
ArrayList<String> players = fromArrayToList(names);

需要注意的是,因为Java的方法必须声明在类里面,但这并不意味着方法的泛型一定要与类的类型参数一致,当然了,方法可以直接使用类的类型参数,也可以自己再定义一个另外的类型参数,注意这是方法自定义的泛型与其所在的类的泛型没啥关系,如:

class ArrayList<E> {
	public <T> ArrayList<T> transfer(E e) { ... }
}

注意,为了可读性方法自定义的泛型最好不要与其所在类使用的泛型一样,比如类用T,方法也用T,虽然这是可以的,因为这个替代类型名字随便取为啥非要弄的容易混淆呢?

多元类型参数

类型参数可以有多个,用不同的代号名字并用逗号隔开就可以了,就比如哈希表:

class HashMap<K, V> { ... }

就是一个使用二元类型参数的类。

以上就是泛型的基础使用方法。

理解泛型的本质

通过以上的介绍可以得出泛型的根本目的是加强复用,让类和方法不受类型的限制,可以应用于任何类型,并且是以一种安全的方式,受到编译器的支持。

泛型的优势

如果不用泛型,想要让类或者方法通用,即对任何对象都能生效,那只能把其参数的类型声明为顶层基类Object,然后在某些地方做手动类型转换(type casting)。很明显,这非常容易出错,并且非常的不安全, 一旦某些地方忘记了检查,就会有运行时的类型转换异常(ClassCastException)。

使用了泛型后,编译器会帮助我们对类型对待检查和自动转换,在完成代码复用的同时,又能保证运行时的类型安全,减少运行时的类型转换错误,所以我们应该尽可能多的使用泛型。

命名规范

虽然说参数化类型可以用任何名字,但为了可读性还是要遵从比较流行的规范:

  • T 类型
  • E 集合里面元素的类型
  • K 哈希表,或者其他有键值的键的类型
  • V 哈希表中值的类型
  • N 数字类型
  • S, U, V等多元参数类型时使用

泛型高级特性

指定参数类型的界限

泛型在定义的时候用虚拟的类型表示参数化的类型,使用的时候传入具体的类型,但有些时候需要对可以传入的具体类型做限制,这时可以用类似<T extends Number>来限定可以使用的类型参数的界限(上界),这里的Number可以是任意已知的类型。并且与类的多继承规则一样,这里可以指定多个类型上限,但只能有一个类且要放在最前面后面的只能是接口,用&来连接,如<T extends ClassA & IfaceB & IfaceC>,比如:

class Calculator<T extends Number & Runnable & Closeable> {
    private T operand;
    
    public static <S extends Number & Runnable & Comparable> S plus(S a, S b) {
        //
    }
}

指定泛型中参数型的限制在实际项目中是很有用的,它可以加强代码复用,把一些公共的代码从子类中抽出来,比如像一个列表中的Item有不同的数据类型和不同的布局样式,常规的多态是说让每个子类去实现自己的布局样式,但如果共性太多,这时就可以在创建一个泛型的类或者方法来做,而这个类或者方法就可以指定基类作为泛型类型界限。这样可以加强代码的类型安全,避免调用者传入代码不认识和不能处理的参数类型。

界限通配符来实现协变与逆变

协变与逆变是用来描述对象的继承关系在使用这些对象为类型参数的泛型中的联系。比如说Dog是Animal的子类,那么使用这两个类型为参数的泛型对象之间的关系应该是会么呢?如List<Dog>是否也是List<Animal>的子类?Java中的泛型是不可变的Invariant,即泛型对象之间的关系与它们的类型参数之间的关系是没有联系的,即List<Dog>与List<Animal>之间没关系。

不可变Invariant是为了类型安全,编译器检查泛型类型参数必须严格匹配,但在有些时候会带来极大的不方便,因为面向对象的两大基本特性继承和多态保证了子类对象可以当作其基类使用,换句话说能用Animal的地方,放一个Dog对象应该完全合法。但因为泛型不可变,一个声明为addAll(List<Animal>)的方法,是没有办法传入List<Dog>的:

class Animal {}
class Dog extends Animal {}
class List<E> {
	private E[] items;
	private int size;
	public void addAll(List<E> b) {
		for (E x : b) {
			items[size++] = x;
		}
	}
	
	public void getAll(List<E> b) {
		for (E e : items) {
			b.add(e);
		}
	}
}

List<Animal> animals = new List<>();
List<Dog> dogs = new List<>();
animals.addAll(dogs); // compile error
dogs.getAll(animals); // compile error

但这其实是很安全的,因为我们把Dog从列表中取出,然后当作Animal使用,这是向上转型(Upcasting)是完全安全的。但因为泛型是不可变的,编译器必须要保证泛型的类型参数必须完全一致,因此会给出编译错误,但这显然不方便,会让泛型的作用大打折扣。再比如Object是所有对象的基类,但是当把Object作为类型参数时,这个泛型并不是其他泛型的父类,如List<String>并不是List<Object>的子类。

实际上这里需要的是协变(Covariance)与逆变(Contravariance),也就是让使用类型参数的泛型具有其类型参数一致的继承关系,就要用到界限通配符(Bounded Wildcards)。一共有三种:

  • 上界进行协变Covariant,参数化类型<? extends T>表示可以是以T为基类的任意子类类型,当然也包括T本身,泛型<S>会变成<? extends T>的子类,如果S是T的子类。
  • 下界进行逆变Contravariant,参数化类型<? super T>表示可以是T或者T的基类类型泛型<B>会变成<? super T>的基类,如果B是T的基类。
  • 无界,参数化类型<?>表示可以是任何类型,可以理解为泛型里的顶层基类(就像Object之于其他对象一样)。

使用界限通配符来修改上述🌰:

class List<E> {
	public void addAll(List<? extends E> b) { ... }
	
	public void getAll(List<? super E> b) { ... }
}

List<Animal> animals = new List<>();
List<Dog> dogs = new List<>();
animals.addAll(dogs); // 0 warnings, 0 errors!
dogs.getAll(animals); // 0 warnings, 0 errors!

需要特别注意的是界限通配符解决的问题是协变与逆变,也即让两个泛型之间的关系与其参数类型保持一致,但具体的这一对类型参数仍可以是任何类型。这与前一小节讨论的参数类型界限是完全不同的概念,不是同一码事儿,参数类型界限是限制使用泛型时可以传入的类型的限制。

界限通配符解决的是泛型之间的关系,每当需要进行协变与逆变的时候就需要用到通配符,以让代码更通用更合理。还需要特别注意的界限通配符只能用于方法的参数,大神Joshua Bloch在《Effective Java》中给出的建议是通配符要用于方法的输入泛型参数,如果参数是生产者用extends(即从里面读取对象),如果是消费者用super(即往里面写数据)

运行时的泛型擦除

泛型是为了以类型安全的方式实现代码复用,但是在Java 1.5版本时引入的,为了保持向后兼容性,编译器会对泛型的类型信息进行擦除(type erasure),使其变成常规的对象,这样运行时(JVM)就不用处理新增加的类型了,保持了字节码的兼容性。比如List<String>与List<Integer>在运行时都变成了List对象,JVM并不知道它们的参数类型。泛型的类型参数检查,以及类型的转换都是发生在编译时,是编译器做的事情。

泛型擦除带来的一个问题就是泛型不能使用类型判断符(instanceof),以及不能进行强制类型转换,比如这样写是不合法的:

// Compile error: Illegal 	generic type for instanceof
if (list instanceof List<Dog>) {
	List<Dog> ld = (List<Dog>) list;
}

很显然,反射(Reflect)是完全没有办法用泛型的,因为反射是在运行时,这时泛型都被擦除了。如果非要使用泛型,必须要把其类型参数的Class传入作为参数(也即把T的具体参数的class对象传入如String.class),以此来区分不同的泛型,可以参考泛型工厂方法的实现。

Java泛型的问题

泛型不支持基础类型

Java为了效率和兼容性保留了基础数据类型,如int, boolean, float,但它们并不是对象。而泛型的类型参数必须是对象,因此基础类型是不能用在泛型上面的,比如不能用List<int>,而只能用List<Integer>,好在有自动装箱autoboxinng和拆箱unboxing,所以List<Integer>也可以可以直接用于整数类型的。

泛型不支持数组

这里的意思是指不能用泛型去声明数组,比如List<String>[],这是不允许的。(不要搞混混淆了,数组当作泛型的类型参数是完全可以的,如List<int[]>,因为数组是一个类型。)

参考资料

  • The Basics of Java Generics
  • Understanding Java generics, Part 1: Principles and fundamentals
  • Understanding Java generics, Part 2: The hard part
  • Generics in Java
  • Java Generics Example Tutorial - Generic Method, Class, Interface
  • Java 基础 - 泛型机制详解
  • 一文搞懂 java 泛型,也有可能搞不懂,毕竟讲得太全面了
  • java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

原创不易,「打赏」「点赞」「在看」「收藏」「分享」 总要有一个吧!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

吴恩达深度学习笔记:神经网络的编程基础2.5-2.8

目录 第一门课&#xff1a;神经网络和深度学习 (Neural Networks and Deep Learning)第二周&#xff1a;神经网络的编程基础 (Basics of Neural Network programming)2.5 导数&#xff08;Derivatives&#xff09; 第一门课&#xff1a;神经网络和深度学习 (Neural Networks an…

04-微服务 面试题

目录 1.Spring Cloud 常见的组件有哪些? 2.服务注册和发现是什么意思?(Spring Cloud 如何实现服务注册发现) 3.你们项目负载均衡如何实现的 ? 4.什么是服务雪崩,怎么解决这个问题? 5.你们服务是怎么监控的? 6.微服务限流(漏桶算法、令牌桶算法) 7.解释一下CAP…

scrapy的基本使用介绍

创建项目 ### 1. 创建虚拟环境 conda create -n spiderScrapy python3.9 ### 2. 安装scrapy pip install scrapy2.8.0 -i https://pypi.tuna.tsinghua.edu.cn/simple### 3. 生成一个框架 scrapy startproject my_spider### 4. 生成项目 scrapy genspider baidu https://www.b…

RabbitMQ - 02 - 基本消息模型

目录 部署demo项目 什么是基本消息模型 实现基本消息模型 部署demo项目 首先配置好一个mq的练习demo,并配置好相关依赖 链接&#xff1a;https://pan.baidu.com/s/1oXAqgoz9Y_5V7YxC_rLa-Q?pwdv2sg 提取码&#xff1a;v2sg 如图 父xml文件已经配置好了 AMQP依赖了 什么…

重学SpringBoot3-集成Thymeleaf

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 重学SpringBoot3-集成Thymeleaf 1. 添加Thymeleaf依赖2. 配置Thymeleaf属性&#xff08;可选&#xff09;3. 创建Thymeleaf模板4. 创建一个Controller5. 运行应用并访问页面Thymeleaf基本语法小技巧 国际化步骤 …

Cassandra 安装部署

文章目录 一、概述1.官方文档2. 克隆服务器3.安装准备3.1.安装 JDK 113.2.安装 Python3.3.下载文件 二、安装部署1.配置 Cassandra2.启动 Cassandra3.关闭Cassandra4.查看状态5.客户端连接服务器6.服务运行脚本 开源中间件 # Cassandrahttps://iothub.org.cn/docs/middleware/…

15. UE5 RPG获取GE应用的回调,并根据Tag设置数据显示到窗口

在上一篇介绍了对标签如何在项目中设置&#xff0c;这一篇先讲解一下如何在GE里面使用GameplayTag标签。 之前我在第十一章节中 11. UE5 RPG使用GameplayEffect修改角色属性&#xff08;二&#xff09;介绍了一些GE的属性&#xff0c;在UE 5.3版本中&#xff0c;修改的配置方式…

SpringBoot中MD5使用

SpringBoot中MD5使用 新建md5类 public final class MD5 {public static String encrypt(String strSrc) {try {char[] hexChars {0, 1, 2, 3, 4, 5, 6, 7, 8,9, a, b, c, d, e, f};byte[] bytes strSrc.getBytes();MessageDigest md MessageDigest.getInstance("MD5…

云游戏发行是什么?云游戏发行的演进历程

云游戏发行是一系列基于云游戏技术的游戏发行策略或行为&#xff0c;融合云试玩、云微端、可玩广告、跨端移植等技术&#xff0c;从而在传统游戏发行生态的基础上实现更为卓越的发行效果。 云游戏发行出现的原因 近年来&#xff0c;游戏市场出现负增长。其原因一方面在于游戏版…

高颜值抓包工具Charles,实现Mac和IOS端抓取https请求

Hi&#xff0c;大家好。在进行测试的过程中&#xff0c;不可避免的会有程序报错&#xff0c;为了能更快修复掉Bug&#xff0c;我们作为测试人员需要给开发人员提供更准确的报错信息或者接口地址&#xff0c;这个时候就需要用到我们的抓包工具。 常见的抓包工具有Fiddler、Char…

LeetCode_Java_二叉搜索树系列(题目+思路+代码)

目录 108.将有序数组转化为二叉搜索树 109.有序链表转换二叉搜索树 876.链表的中间节点 108.将有序数组转化为二叉搜索树 给你一个整数数组 nums &#xff0c;其中元素已经按 升序 排列&#xff0c;请你将其转换为一棵 平衡二叉搜索树。 示例 1&#xff1a; 输入&#xf…

vscode使用svn

网上这种文章很多&#xff0c;但很多都实现不了&#xff0c;自己亲测安装有效的过程记录下来&#xff0c;分享给大家。 第一步&#xff1a;去官网下载svn.安装TortoiseSVN 下载地址 下载的地址&#xff1a; Apache Subversion Binary Packageshttps://subversion.apache.or…

OpenHarmony教程指南—ArkTS时钟

简单时钟 介绍 本示例通过使用ohos.display 接口以及Canvas组件来实现一个简单的时钟应用。 效果预览 使用说明 1.界面通过setInterval实现周期性实时刷新时间&#xff0c;使用Canvas绘制时钟&#xff0c;指针旋转角度通过计算得出。 例如&#xff1a;"2 * Math.PI /…

linux ,Windows部署

Linux部署 准备好虚拟机 连接好查看版本&#xff1a;java -version安装jdk 解压命令&#xff1a;tar -zxvf 加jdk的压缩文件名cd /etc 在编辑vim profile文件 在最底下写入&#xff1a; export JAVA_HOME/root/soft/jdk1.8.0_151&#xff08;跟自己的jdk保持一致&#xff0…

【网站项目】012医院住院管理系统

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

C++_异常

目录 1、异常的关键字 2、异常的写法 3、异常的使用规则 3.1 规则1 3.2 规则2 3.3 规则3 3.4 规则4 3.5 规则5 4、异常的重新抛出 5、异常的规范 5.1 C98的异常规范 5.2 C11的异常规范 6、C标准库的异常体系 7、异常的优缺点 结语 前言&#xff1a; C的异常…

Python从0到100(四):Python中的运算符介绍

前言&#xff1a; 零基础学Python&#xff1a;Python从0到100最新最全教程。 想做这件事情很久了&#xff0c;这次我更新了自己所写过的所有博客&#xff0c;汇集成了Python从0到100&#xff0c;共一百节课&#xff0c;帮助大家一个月时间里从零基础到学习Python基础语法、Pyth…

Java中的参数传递

程序设计语言将实参传递给方法&#xff08;或函数&#xff09;的方式分为两种&#xff1a; 值传递&#xff1a;方法接收的是实参值的拷贝&#xff0c;会创建副本。引用传递&#xff1a;方法接收的直接是实参所引用的对象在堆中的地址&#xff0c;不会创建副本&#xff0c;对形…

3.1_3 连续分配管理方式

3.1_3 连续分配管理方式 连续分配&#xff1a;指为用户进程分配的必须是一个连续的内存空间。 &#xff08;一&#xff09;单一连续分配 在单一连续分配方式中&#xff0c;内存被分为系统区和用户区。 系统区通常位于内存的低地址部分&#xff0c;用于存放操作系统相关数据&am…

11 vector的实现

注意 实现仿cplus官网的的string类&#xff0c;对部分主要功能实现 实现 文件 #pragma once #include <string> #include <assert.h>namespace myvector {template <class T>class vector{public://iteratortypedef T* iterator;typedef const T* const_…