Java体系中的泛型

在这里插入图片描述


1. 泛型

一般的类和方法,只能够使用基本类型,要么是自定义的类,如果要编写可以应用于多种数据类型的代码,这种刻板的限制对代码的约束就会很大,那么如何实现可应用于多种数据类型的代码,而不局限于单一一种呢?

答:参数化数据类型,将我们需要的数据类型指定为一个参数。在Java中实现类型参数化的方法是利用泛型。

1.1 什么是泛型

#泛型(generics) 是JDK5中引入的一个新特性,泛型提供了编译时类型安全监测机制,该机制允许我们在编译时检测到非法的类型数据结构。

1.2 泛型的语法

//泛型类的定义语法
class 泛型类<泛型标识,泛型标识,...> {//修饰符 泛型标识 变量名;
}
//常用的泛型标识:T、E、K、V
  1. 类名后的<泛型标识>代表占位符,例如:< T>,代表当前类是一个泛型类

泛型标识一般使用一个大写字母表示,常用的名称有:

  1. E 表示Element(一般在集合中使用,因为集合中存放的是元素)
  2. K 表示 Key
  3. V 表示 Value
  4. T 表示 Type
  1. 泛型只能接受类,所有的基本数据类型必须使用包装类

1.3 为什么使用泛型

当我们实现一个类,类中包含一个可以存放任何类型信息的数组成员,那么这个数组类型势必是所有类的父类Object,代码如下:

//MyArray.java
public class MyArray {public Object[] array = new Object[10];public Object getPos(int pos) {return this.array[pos]; }public void setVal(int pos,Object val) {this.array[pos] = val;}
}//Test.java
public class Test {public static void main(String[] args) {MyArray myArray = new MyArray();myArray.setVal(0,10);//存放整形myArray.setVal(1,"Hello World!");//存放字符串myArray.setVal(2,true);//存放布尔类型String str = myArray.getPos(1);//1),ErrorSystem.out.println(str);}
}//1)处代码应修改为
//String str = (String)myArray.getPos(1);

我们发现自定义类中可以存放任何类型数据,但是当取出数据的时候1)处出现了编译报错,这是为什么呢?

我们可以发现,getPos()方法的返回值是Object,也就是当我们接受数据的时候需要将返回值强转成相应类型,这样才能通过编译。

虽然在这种情况下,当前数组可以存放任何类型的数据;但是当我们取某下标对应值的数据时,我们必须知道当前下标下存的是什么类型,这对于我们来说是不现实的,那么如何解决呢?

所以有了泛型,泛型的目的是:指定当前的容器持有什么类型的对象,交由编译器去检查

上述代码修改成泛型类后:

//MyArray.java
public class MyArray<T> {public Object[] array = new Object[10];public T getPos(int pos) {return (T)this.array[pos]; }public void setVal(int pos,T val) {this.array[pos] = val;}
}//Test.java
public class Test {public static void main(String[] args) {MyArray<String> myArray = new MyArray<>();myArray.setVal(0,"Hello ");//存放字符串myArray.setVal(1,"World!");//存放字符串String str = myArray.getPos(1);//自动类型转换System.out.println(str);}
}
  1. 在<>中加入String指定了当前数组中存放String类型的数据,此时编译器会帮你实现自动类型检查,也就是说当你执行myArray.setVal(0,true);时编译器会报错,这就确保了数据存放的确切性。
  2. 在上面代码中,我们发现取数据类型的时候不用进行数据的强转了,也就是我们在泛型类中已经进行了数据的转换,换句话说编译器帮我们进行了自动类型转换

1.4 泛型类和泛型方法

1.4.1 定义

//泛型类的定义语法:
class 泛型类名称 <泛型标识,...> {...//返回值和存放值可以由泛型标识代替
}//泛型方法的定义语法:
修饰符 <泛型标识,...> 返回值类型 方法名(形参列表) {方法体...
}

1.4.2 实例

//MyArray.java
//定义一个泛型类MyArray
public class MyArray<T> {public Object[] array = new Object[10];public T getPos(int pos) {return (T)this.array[pos]; }public void setVal(int pos,T val) {this.array[pos] = val;}//定义一个静态泛型方法public static<T> void swap(T[] array,int i,int j) {T t = array[i];array[i] = array[j];array[j] = t;}
}

当泛型类和泛型方法定义出来后,我们该如何使用呢?

//Test.java
public class Test {public static void main(String[] args) {//实例一个泛型类,指明类型参数MyArray<String> myArray = new MyArray<>();int[] array1 = {1,2};String[] array2 = {"HELLO","WORLD"};//调用泛型方法MyArray.swap(array1,0,1);MyArray.swap(array2,0,1);System.out.println("array1:" + Arrays.toString(array1));System.out.println("array2:" + Arrays.toString(array2));}
}
//输出结果:
array1: [2 ,1 ]
array2: [WORLD ,HELLO ]

从上述运行结果,我们发现了
对于泛型方法:

  1. 泛型方法的调用,类型可不依赖于泛型类本身,类型是通过调用方法的时候来指定的
  2. 泛型类中的使用了泛型的成员方法并不是泛型方法,只有声明了< T>的方法才是泛型方法
  3. public与返回值中间< T>非常重要,可以理解为声明此方法为泛型方法
  4. < T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
  5. 泛型方法可以不存在泛型类中,泛型方法是独立的

1.4.3 类型推导(Type Inference)

类型推导,就是编译器根据上下文推导出类型实参,从而省略类型实参的编写
也就是上述泛型方法和泛型类的编写可分为使用类型推导和不使用类型推导两种

当我们实例泛型类和使用泛型方法时
不使用类型推导:

MyArray<String> myArray = new MyArray<String>();
MyArray.<Integer>swap(array1,0,1);
MyArray.<String>swap(array2,0,1);

使用类型推导:

//泛型类使用类型推导会根据前面传入的类型参数进行推导
MyArray<String> myArray = new MyArray();
//泛型方法会根据后面传入数据类型进行推导
MyArray.swap(array1,0,1);
MyArray.swap(array2,0,1);

1.5 裸类型(Raw Type)

裸类型就是一个泛型类但没有带着类型参数的,例如:

MyArray myArray = new MyArray();

泛型是Java 1.5版本才引进的概念,在这之前是没有泛型的,但是,泛型代码能够很好地和之前版本的代码兼容。那是因为,泛型信息只存在于代码编译阶段,在进入JVM之前,与泛型相关的信息会被擦除掉,我们称之为–类型擦除。

1.5.1 擦除机制

#擦除机制: 在编译的过程当中,将所有的泛型标识(T等)替换为Object这种机制
Java的泛型机制是在编译期间实现的,编译器生成的字节码文件.class在运行期间并不包含泛型的类型信息,换句话说就是在进入JVM前,与泛型相关的信息会被擦除掉。

//Test.java
public class Test {public static void main(String[] args) {MyArray<String> arrString = new MyArray<>();MyArray<Integer> arrInteger = new MyArray<>();System.out.println(arrString.getClass() == arrInteger.getClass());//true}
}
  • 我们通过getClass()方法获取两个存放不同信息的MyArray类信息进行比较,输出结果为==true。我们在<>中传入不同的数据类型,他们的类型参数不同,那为什么比较类信息的时候得到的true==呢?
    这就不能提到我们的擦除机制了,在编译期间我们借助泛型来实现自动类型检测和自动类型转换,在编译后擦除机制会将所有的泛型信息擦除,也就是在编译后上述代码类型变为了MyArray
    我们定义一个内容简单的泛型类进行讲解:
//无限制类型擦除
public class Counter<T> {private T number;public T getNumber() {return number;}public void setNumber(T number) {this.number = number;}
}//有限制类型擦除
//<T extends Number>是什么意思呢?我们在下面讲解泛型边界的时候进行解释
public class Counter<T extends Number> {private T number;public T getNumber() {return number;}public void setNumber(T number) {this.number = number;}
}

大多数情况下我们面对的泛型类是没有边界约束的,这时候经过编译器==擦除,我们会将所有的泛型标识T的数据类型Object;而面对有边界限制的泛型类,我们通常会将数据类型擦除==为它的上届类型。

//无限制类型擦除
public class Counter {private Object number;public Object getNumber() {return number;}public void setNumber(Object number) {this.number = number;}
}//有限制类型擦除
public class Counter {private Number number;public Number getNumber() {return number;}public void setNumber(Number number) {this.number = number;}
}
#类型擦除是如何执行的呢?

在我们定义了一个 MyArray< Integer > 泛型集合,若向该集合中插入 String 类型的对象,不需要运行程序,编译器就会直接报错。这里就让我们对上述学习的擦除机制产生了一些疑问:

  1. 不是说泛型信息在编译后就会被擦除掉吗?为什么泛型信息在擦除后能够保证我们添加数据类型对象的准确性,即如何保证我们只添加指定类型的数据类型呢?
  2. 泛型信息被擦除后我们又如何能够实现自动类型转换?

这里我们就要了解一下Java内部是如何解决这个问题的了:

  • 其实在创建一个泛型类的对象时, Java 编译器是先检查代码中传入 < T > 的数据类型,并保存下来,然后再对代码进行编译,编译的同时将会进行类型擦除;如果需要对被擦除了泛型信息的对象进行操作,编译器会自动将对象进行类型转换
public class Test {public static void main(String[] args) {MyArray<String> myArray = new MyArray<>();myArray.setVal(0,"Hello ");//存放字符串String str = myArray.getPos(0);//自动类型转换System.out.println(str);}
}
//也就是在编译后,当MyArray<String>的泛型信息被擦除后,getPos()方法会返回Object类型,但是编译器会自动插入String的强制类型转换
  • 泛型类型被擦除后,当需要使用相关的泛型信息时,编译器底层会自动实现类型转换

1.6 泛型的上界

//语法
class 泛型类名称<泛型标识 extends 类型边界> {...
}
//这对传入的类型参数有了一定要求,要求传入类型参数必须时类型边界的子类或类型边界

例如:

//MyArray.java
public class MyArray<T extends Number> {...
}//Test.java
public class Test {public static void main(String[] args) {MyArray<Integer> i1;//Integer是Number子类,正确MyArray<String> i2;//编译错误,String不是Number子类型}
}
//Tip:没有指定类型边界时,可以视为T extends Object

1.7 通配符

将<>中的泛型标识替换为?,这就是通配符。
以下述代码引入:

class Print<T> {private T message ;public T getMessage() {return message;}public void setMessage(T message) {this.message = message;}}public class TestDemo {public static void main(String[] args) {Print<String> message1 = new Print<>() ;message.setMessage("我是字符串...");fun(message1);Print<Integer> message2 = new Print<>() ;message.setMessage(100);fun(message2);//Error}public static void fun(Print<String> temp){System.out.println(temp.getMessage());}
}
//观察发现,fun()方法指定了Print<String>类型,如果我们实例一个对象泛型参数设置的是Integer,这时候当我们调用fun()方法的时候,编译器就报错了

这时候我们需要一个可以接收所有泛型类型的标识符<?>

public static void fun(Print<?> temp) {System.out.println(temp.getMessage());
}
//这时候调用fun()方法就可以传递,任何指定的泛型数据类型了

1.7.1 通配符的上界

//语法
<extends>//设置通配符上限

传入的参数类型必须是该类或该类的子类
我们定义以下几个类:

//Animal.java
public class Animal {...
}//Cat.java
public class Cat extends Animal {...
}//Dog.java
public class Dog extends Animal {...
}//PetDog.java
public class PetDog extends Dog {...
}

我们在测试类中设计一个新的方法:

public Test {public static void funUpper(Print<? extends Animal> temp) {System.out.println(temp.getMessage()):}
}

需要注意的是,此时通配符描述的是他可以接受任意属于或继承于Animal的类,但是我们无法在funUpper方法内部去设置任何类型的元素,因为我们无法确认类型
如:

public static void main(String[] args) {Print<Cat> message1 = new Print<>();message1.setMessage(new Cat());funUpper(message1);Print<Dog> message2 = new Print<>();message2.setMessage(new Dog());funUpper(message2);//temp可以接收任意属于或继承于`Animal`的类
}public static void funUpper(Print<? extends Animal> temp){//能够接受数据,明确数据上界Animal animal = temp.getMessage()System.out.println(animal);//不能写入数据//temp.setMessage(new Animal()); //Error//temp.setMessage(new Dog()); //Error//temp.setMessage(new PetDog()); //Error//temp.setMessage(new Cat()); //Error}

解释:

此时无法在funUpper函数中对temp进行添加元素,因为temp接收的是Animal和他的子类,此时存储的元素应该是哪个子类无法确定。所以添加会报错!但是可以获取元素。

  • <? extends T>小结:通配符的上界,不能进行数据的写入,只能进行数据的读取

1.7.2 通配符的下界

//语法
<super>//设置通配符上限

传入的参数类型必须是该类或该类的父类
我们在测试类中设计一个新的方法:

public static void funDown(Print<? super Dog> temp) {//可以修改数据temp.setMessage(new Dog());//Dog类本身temp.setMessage(new PetDog());//Dog类的子类}

此时通配符描述的是他可以接受任意属于DogDog的父类,在funDown中不可以接收,我们无法确认是哪个父类
错误用法:

public static void funDown(Print<? super Dog> temp) {//可以修改数据temp.setMessage(new Dog());//Dog类本身temp.setMessage(new PetDog());//Dog类的子类//Dog dog = temp.getMessage();//Error,不能接收,无法确定是那个父类System.out.println(temp.getMessage());//可以直接输出
}
  • <? super T>小结:通配符的下界,不能进行读取数据,只能写入数据。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

服务器数据恢复—EMC存储RAID5磁盘阵列数据恢复案例

服务器数据恢复环境&#xff1a; 一台EMC某型号存储设备&#xff0c;该存储中有一组由12块&#xff08;包括2块热备盘&#xff09;STAT硬盘组建的raid5阵列。 服务器故障&#xff1a; 该存储在运行过程中突然崩溃&#xff0c;raid瘫痪。数据恢复工程师到达现场对故障存储设备进…

肺结节分割与提取系统(基于传统图像处理方法)

Matlab肺结节分割(肺结节提取)源程序&#xff0c;GUI人机界面版本。使用传统图像分割方法&#xff0c;非深度学习方法。使用LIDC-IDRI数据集。 工作如下&#xff1a; 1、读取图像。读取原始dicom格式的CT图像&#xff0c;并显示&#xff0c;绘制灰度直方图&#xff1b; 2、图像…

欧科云链研究院深掘链上数据:洞察未来Web3的隐秘价值

目前链上数据正处于迈向下一个爆发的重要时刻。 随着Web3行业发展&#xff0c;公链数量呈现爆发式的增长&#xff0c;链上积聚的财富效应&#xff0c;特别是由行业热点话题引领的链上交互行为爆发式增长带来了巨量的链上数据&#xff0c;这些数据构筑了一个行为透明但与物理世…

extern “C“ 的作用、C++ 和 C 编译的不同、C++ 编译过程的五个主要阶段

在 C 中&#xff0c;如果需要从 C 语言导入函数或与 C 代码交互&#xff0c;需要使用 extern "C" 关键字。这是因为 C 和 C 在编译过程中的 符号命名机制&#xff08;即 "名称修饰" 或 "name mangling"&#xff09;不同。 1. extern "C&qu…

MokeJs使用实例

文章目录 MokeJs使用实例介绍使用安装配置文件导入配置到main.js使用 axios 发送网络请求测试&#xff08;如果不会axios&#xff0c;具体可以见上篇文章axios&#xff09;启动示例 MokeJs使用实例 介绍 使用 安装 npm install mockjs --save-dev # 或者 yarn add mockj…

【超详细】基于YOLOv11的PCB缺陷检测

主要内容如下&#xff1a; 1、数据集介绍 2、下载PCB数据集 3、不同格式数据集预处理&#xff08;Json/xml&#xff09;&#xff0c;制作YOLO格式训练集 4、模型训练及可视化 5、Onnxruntime推理 运行环境&#xff1a;Python3.8&#xff08;要求>3.8&#xff09;&#xff…

matlab不小心删除怎么撤回

预设项——>删除文件——>移动至临时文件夹 tem临时文件夹下

【RabbitMQ】初识 RabbitMQ

&#x1f970;&#x1f970;&#x1f970;来都来了&#xff0c;不妨点个关注叭&#xff01; &#x1f449;博客主页&#xff1a;欢迎各位大佬!&#x1f448; 文章目录 1. MQ 是什么&#xff1f;1.1 MQ 本质1.2 系统间通信 2. MQ的作用是什么&#xff1f;2.1 异步解耦2.2 流量削…

【ProtoBuf】ProtoBuf基础与安装

本篇文章介绍 C 使用方向 文章目录 ProtoBuf简介ProtoBuf安装WindowsLinux ProtoBuf简介 ProtoBuf(全称为 Protocol Buffer)是一种序列化结构数据的方法 序列化是将对象转换为可存储的或传输的格式的过程&#xff0c;通常用于数据交换或持久化存储。我们在C/Java中编写的类不…

2.13寸电子墨水屏HINK-E0213+esp8266

记录好数字 2.13寸电子墨水屏HINK-E0213esp8266 声明:大部分资料来源于微雪电子http://微雪电子-官网 https://www.waveshare.net/ 前言 很久以前买的一块电子墨水屏,运气很好,这个型号HINK-E0213资料很全,而且微雪官网也有相关电路资料http://2.13inch e-Paper HAT - Waves…

【GaussDB】产品简介

产品定位 GaussDB 200是一款具备分析及混合负载能力的分布式数据库&#xff0c;支持x86和Kunpeng硬件架构&#xff0c;支持行存储与列存储&#xff0c;提供PB(Petabyte)级数据分析能力、多模分析能力和实时处理能力&#xff0c;用于数据仓库、数据集市、实时分析、实时决策和混…

【UI】将 naive ui 的 message 封装进axios 中,关于naiveui的message相关的用法

文章目录 前言在setup外进行使用直接包裹使用vue 单文件中使用 参考文章&#xff1a; 关于naiveui的message相关的用法 前言 最近新建了一个vite vu3 的项目&#xff0c;完全是从0 到1 &#xff0c;封装到request 的时候 想对axios 请求做一个全局的处理&#xff0c;但发现…

【尚硅谷】FreeRTOS学笔记(更新中更新时间2024.10.12)

在网上看到的一段很形象的描述&#xff0c;放在这里给大家娱乐一下。 裸机开发&#xff1a;n个人拉屎&#xff0c;先进去一个拉完&#xff0c;下一个再来。看门狗&#xff1a;如果有人拉完屎还占着&#xff0c;茅坑刷视频&#xff0c;把他拖出去中断系统&#xff1a;n个人拉屎&…

两三年没涨薪了

前几天到上海见合伙人&#xff0c;有好几位合伙人也都是中型或者是大厂的骨干&#xff0c;基本上是在P8这个级别&#xff0c;大家谈到了几个共同点。 几个典型的现象说一下&#xff0c;既是新闻&#xff0c;也是旧故事。天下的事都雷同。第一个&#xff0c;老板换了&#xff0c…

LVS-DR+Keepalived 高可用群集部署

LVS-DRKeepalived 高可用群集部署 Keepalived 的工作原理LVSKeepalived 高可用群集部署配置负载调度器&#xff08;主、备相同&#xff09;关闭防火墙和核心防护及准备IPVS模块配置keeplived&#xff08;主、备DR 服务器上都要设置&#xff09;启动 ipvsadm 服务调整 proc 响应…

机器学习中的模型设计与训练流程详解

目录 前言1. 模型设计1.1 数据特性分析1.2 计算资源限制1.3 应用场景需求 2. 模型训练2.1 训练集与验证集的划分2.2 损失函数的选择2.3 模型参数更新 3. 优化方法3.1 梯度下降法3.2 正则化方法 4. 模型测试4.1 性能评估指标4.2 模型的泛化能力 5. 模型选择5.1 数据规模与模型复…

集合框架06:Vector集合使用

1.视频链接&#xff1a;13.13 Vector使用_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1zD4y1Q7Fw/?p13&spm_id_from333.1007.top_right_bar_window_history.content.click&vd_sourceb5775c3a4ea16a5306db9c7c1c1486b52.代码示例&#xff1a; package com.yu…

Fetch 与 Axios:JavaScript HTTP 请求库的详细比较

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐&#xff1a;「storm…

初知C++:AVL树

文章目录 初知C&#xff1a;AVL树1.AVL树的概念2.AVL树的是实现2.1.AVL树的结构2.2.AVL树的插入2.3.旋转2.4.AVL树的查找2.5.AVL树平衡检测 初知C&#xff1a;AVL树 1.AVL树的概念 • AVL树是最先发明的自平衡⼆叉查找树&#xff0c;AVL是⼀颗空树&#xff0c;或者具备下列性…

python如何对变量赋值

Python 中的变量赋值不需要类型声明。 每个变量在内存中创建&#xff0c;都包括变量的标识&#xff0c;名称和数据这些信息。 每个变量在使用前都必须赋值&#xff0c;变量赋值以后该变量才会被创建。 等号&#xff08;&#xff09;用来给变量赋值。 等号&#xff08;&…