Java体系中的泛型

news2024/10/24 9:34:00

在这里插入图片描述


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),Error
		System.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.coloradmin.cn/o/2210926.html

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

相关文章

第5篇:DDOS病毒----应急响应之Linux实战篇

现象描述 某服务器网络资源异常,感染该木马病毒的服务器会占用网络带宽&#xff0c;甚至影响网络业务正常应用。 系统分析 针对日志服务器病毒事件排查情况&#xff1a; 在开机启动项/etc/rc.d/rc.local发现可疑的sh.sh脚本&#xff0c;进一步跟踪sh.sh脚本,这是一个检测病毒…

C++从入门到起飞之——AVL树 全方位剖析!

&#x1f308;个人主页&#xff1a;秋风起&#xff0c;再归来~&#x1f525;系列专栏&#xff1a;C从入门到起飞 &#x1f516;克心守己&#xff0c;律己则安 目录 1. AVL的概念 2. AVL树的实现 2.1 AVL树的结构 2.2 AVL树的插⼊ >AVL树插⼊⼀个值的⼤概过程 &…

Rocky linux 修改ip地址, rocky服务器修改静态地址, rocky虚拟机修改ip

1. 更新yum yum update 2. 安装ifconfig yum install net-tools 3. 修改配置 vi /etc/NetworkManager/system-connections/ens33.nmconnection 将ipv4内容修改如下&#xff1a; # 自动改为手动 methodmanual # 网关为vm ware 查看网关地址 address你想改为的ip/24,网关 #dns不…

Qml 分组动画(二) 动画嵌套(自学笔记)

分组动画嵌套示例&#xff0c;直接看效果&#xff0c; 做一个踢足球的示例 下面两个Rectangle 制作渐变的天空和大地 下面这个Rectangle 用于放置足球图片&#xff0c; 由于足球图片直接从网上下载的 没有找到合适大小的图片 &#xff0c;所以用 一个矩形框作限制&#xff0c;…

闲谈Promise

预备知识 回调函数&#xff1a;当一个函数作为参数传入另一个函数中&#xff0c;并且它不会立刻执行&#xff0c;当满足一定条件之后&#xff0c;才会执行&#xff0c;这种函数称为回调函数。比如&#xff1a;定时器。异步任务&#xff1a;与之对应的概念是同步任务&#xff0…

【JVM】面试篇

1 什么是JVM&#xff1f; 1.1 定义 JVM 指的是Java虚拟机&#xff08; Java Virtual Machine &#xff09;。JVM 本质上是一个运行在计算机上的程序&#xff0c;他的职责是运行Java字节码文件&#xff0c;Java虚拟机上可以运行Java、Kotlin、Scala、Groovy等语言。 启动这个程…

电子电气架构---软件定义汽车的新兴生态系统

我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 屏蔽力是信息过载时代一个人的特殊竞争力&#xff0c;任何消耗你的人和事&#xff0c;多看一眼都是你的不…

【含文档】基于Springboot+Vue的校园体育器材管理系统(含源码+数据库+lw)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 系统定…

C++ : STL容器之vector剖析

STL容器之vector剖析 一、构造函数与赋值&#xff08;一&#xff09;默认构造&#xff08;二&#xff09;拷贝构造&#xff08;三&#xff09;几个相同值构造&#xff08;四&#xff09;迭代器构造&#xff08;五&#xff09;initializer_list 构造&#xff08;六&#xff09;赋…

网络编程(19)——C++使用asio协程实现并发服务器

十九、day19 上一节学习了如果通过asio协程实现一个简单的并发服务器demo&#xff08;官方案例&#xff09;&#xff0c;今天学习如何通过asio协程搭建一个比较完整的并发服务器。 主要实现了AsioIOServicePool线程池、逻辑层LogicSystem、粘包处理、接收协程、发送队列、网络…

C语言入门:打开编程世界的大门

一.C语言是什么 在我们生活中&#xff0c;我们在交流时候使用的就是语言&#xff0c;在这个世界上有许多的国家、民族&#xff0c;自然也有很多语言如&#xff1a;汉语、英语、法语等等&#xff0c;这种人与人交流使用的语言我们称为自然语言。然而计算机并不能理解我们的语言…

github下载文件的两种方式(非git形式)

1.以下面的图为例 &#xff0c;可以直接点击右上方的绿色Code按键&#xff0c;在弹出的列表中选择Download Zip选项&#xff0c;即可下载。 2.如果下载的是单独的某一个文件&#xff0c;则可以按照下图的格式点击下图所示的那个下载的图标即可。

【Linux系统编程】第三十一弹---深入理解静态库:从零开始制作与高效使用的完全指南

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】 目录 1、静态库 1.1、怎么做静态库 1.2、怎么使用静态库 1、静态库 1.1、怎么做静态库 在Linux环境下&#xff0c;通常使用GCC&am…

【2024最新】基于springboot+vue的实验室管理系统lw+ppt

作者&#xff1a;计算机搬砖家 开发技术&#xff1a;SpringBoot、php、Python、小程序、SSM、Vue、MySQL、JSP、ElementUI等&#xff0c;“文末源码”。 专栏推荐&#xff1a;SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;Java精选实战项…

PAT甲级-1034 Head of a Gang

题目 题目大意 一个犯罪团伙满足条件&#xff1a;人数 > 2&#xff1b;团伙内的总通话时长 > k。团伙首领就是该团伙中通话时长最多的。先给定一组通话&#xff0c;格式为 A B time&#xff0c;要求输出犯罪团伙的数目&#xff0c;并输出每个团伙的首领名字和该团伙的人…

一文详解数据库范式

背景 在开发中&#xff0c;我们经常需要考虑如何设计合适的表结构&#xff0c;而则往往需要考虑数据库的范式。数据库的三范式&#xff08;3NF&#xff09;是数据库设计过程中用来减少数据冗余和提高数据一致性的重要规则。它们分别是第一范式&#xff08;1NF&#xff09;、第二…

【PR小技巧】PR技术分享 一 PR关键帧小技巧介绍

在Adobe Premiere Pro (简称PR) 中&#xff0c;关键帧是用于控制视频剪辑、音频轨道、效果动画等随时间变化的重要工具。通过合理使用关键帧&#xff0c;可以实现各种复杂的动画效果和精确的时间控制。今天我们就来学习一些关于关键帧的小技巧&#xff0c;以及具体的例子来说明…

算法专题五: 位运算

目录 常见位运算总结1. 位1的个数2. 比特位计数3. 汉明距离4. 只出现一次的数字5. 只出现一次的数字Ⅲ6. 判定字符是否唯一7. 丢失的数字8. 两正数之和9. 只出现一次的数字Ⅲ10. 消失的两个数字 常见位运算总结 重点 : 1. 位1的个数 算法思路: 这道题就用到了我们总结的那个第…

全新YOLOv11美化版检测界面 涵盖超多功能 支持百种模型改进训练

文章目录 前言视频效果必要环境一、界面功能概述1. 运行方法2. 图像选择图像:表格信息:统计信息:IOU和NMS调节:目标框显示: 3. 文件夹选择文件夹:进度显示:推理结果: 4. 视频、摄像头进度显示:实时检测:帧状态回溯: 5. 替换界面中的模型5. 鼠标悬浮 二、训练改进模型运行方法假…

力扣周赛:第419场周赛

&#x1f468;‍&#x1f393;作者简介&#xff1a;爱好技术和算法的研究生 &#x1f30c;上期文章&#xff1a;力扣周赛&#xff1a;第415场周赛 &#x1f4da;订阅专栏&#xff1a;力扣周赛 希望文章对你们有所帮助 因为一些特殊原因&#xff0c;这场比赛就打了1h&#xff0c…