通俗易懂理解Java泛型

什么是泛型

参数化类型

Java泛型是J2 SE1.5中引入的一个新特性,其本质是参数化类型,也就是说所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

为什么会引入泛型

Java集合(Collection)中元素的类型是多种多样的。例如,有些集合中的元素是Byte类型的,而有些则可能是String类型的,等等。Java允许程序员构建一个元素类型为Object的Collection,其中的元素可以是任何类型在Java SE 1.5之前,没有泛型(Generics)的情况下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要作显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以在预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。因此,为了解决这一问题,J2SE 1.5引入泛型也是自然而然的了。

我们用一个简单的例子更形象的说明一下引入泛型的背景。既然它是由于集合引入的,我们就用没有泛型的集合来演示。

我们先观察Java标准库提供的ArrayList,它可以看作“可变长度”的数组,因为用起来比数组更方便。

实际上ArrayList内部就是一个Object[]数组,配合存储一个当前分配的长度,就可以充当“可变数组”:

public class ArrayList {private Object[] array;private int size;public void add(Object e) {...}public void remove(int index) {...}public Object get(int index) {...}
}

我们点开源码也可以验证这个:
在这里插入图片描述

如果用上述ArrayList存储String类型,会有这么几个缺点:

  • 需要强制转型;
  • 不方便,易出错。

例如,代码必须这么写:

ArrayList list = new ArrayList();
list.add("Hello");
// 获取到Object,必须强制转型为String:
String first = (String) list.get(0);

很容易出现ClassCastException,因为容易“误转型”:

list.add(new Integer(123));
// ERROR: ClassCastException:
String second = (String) list.get(1);

要解决上述问题,我们可以为String单独编写一种ArrayList:

public class StringArrayList {private String[] array;private int size;public void add(String e) {...}public void remove(int index) {...}public String get(int index) {...}
}

这样一来,存入的必须是String,取出的也一定是String,不需要强制转型,因为编译器会强制检查放入的类型:

StringArrayList list = new StringArrayList();
list.add("Hello");
String first = list.get(0);
// 编译错误: 不允许放入非String类型:
list.add(new Integer(123));

问题暂时解决。

然而,新的问题是,如果要存储Integer,还需要为Integer单独编写一种ArrayList:

public class IntegerArrayList {private Integer[] array;private int size;public void add(Integer e) {...}public void remove(int index) {...}public Integer get(int index) {...}
}

实际上,还需要为其他所有class单独编写一种ArrayList:

  • LongArrayList
  • DoubleArrayList
  • PersonArrayList

这是不可能的,JDK的class就有上千个,而且它还不知道其他人编写的class。

为了解决新的问题,我们必须把ArrayList变成一种模板:ArrayList,也就是需要把类型参数化。代码如下:

public class ArrayList<T> {private T[] array;private int size;public void add(T e) {...}public void remove(int index) {...}public T get(int index) {...}
}

T可以是任何class。这样一来,我们就实现了:编写一次模版,可以创建任意类型的ArrayList:

// 创建可以存储String的ArrayList:
ArrayList<String> strList = new ArrayList<String>();
// 创建可以存储Float的ArrayList:
ArrayList<Float> floatList = new ArrayList<Float>();
// 创建可以存储Person的ArrayList:
ArrayList<Person> personList = new ArrayList<Person>();

因此,泛型就是定义一种模板,例如ArrayList,然后在代码中为用到的类创建对应的ArrayList<类型>:

ArrayList<String> strList = new ArrayList<String>();

由编译器针对类型作检查:

strList.add("hello"); // OK
String s = strList.get(0); // OK
strList.add(new Integer(123)); // compile error!
Integer n = strList.get(0); // compile error!

这样一来,既实现了编写一次,万能匹配,又通过编译器保证了类型安全:这就是泛型。

泛型类型

  1. 泛型方法
    你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。

    下面是定义泛型方法的规则:

    • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的 )。
    • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
    • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
    • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。

java 中泛型标记符:

  • E - Element (在集合中使用,因为集合中存放的是元素)
  • T - Type(Java 类)
  • K - Key(键)
  • V - Value(值)
  • N - Number(数值类型)
  • ? - 表示不确定的 java 类型

泛型方法示例:

public class GenericMethodTest {// 泛型方法 printArraypublic static <E> void printArray(E[] inputArray) {// 输出数组元素for (E element : inputArray) {System.out.printf("%s ", element);}System.out.println();}public static void main(String[] args) {// 创建不同类型数组: Integer, Double 和 CharacterInteger[] intArray = {1, 2, 3, 4, 5};Double[] doubleArray = {1.1, 2.2, 3.3, 4.4};Character[] charArray = {'H', 'E', 'L', 'L', 'O'};System.out.println("整型数组元素为:");printArray(intArray); // 传递一个整型数组System.out.println("\n双精度型数组元素为:");printArray(doubleArray); // 传递一个双精度型数组System.out.println("\n字符型数组元素为:");printArray(charArray); // 传递一个字符型数组}
}
  1. 泛型类
    泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。

和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

实战示例:

public class Box<T> {private T t;public void add(T t) {this.t = t;}public T get() {return t;}public static void main(String[] args) {Box<Integer> integerBox = new Box<>();Box<String> stringBox = new Box<>();integerBox.add(10);stringBox.add("Hello");System.out.printf("整型值为 :%d\n\n", integerBox.get());System.out.printf("字符串为 :%s\n", stringBox.get());}
}

通过上面两个简单的泛型类型案例,我们对泛型有了一个比较直观的理解,但是这个时候会有人问:我开发了很多年了,为什么从来没有自己写过泛型类或方法?
首先我们上面也讲解了引入泛型的背景,通常来说,泛型类一般用在集合类中,例如ArrayList,因此我们很少需要编写泛型类。

正因为我们写的少,但是有的时候需要开发一些底层的架构,依然是避免不了使用泛型,这个时候我们可以整理一个通用套路来更好的编写泛型类。

编写泛型

可以按照以下步骤来编写一个泛型类。

  1. 首先,按照某种类型,例如:String,来编写类:
public class Pair {private String first;private String last;public Pair(String first, String last) {this.first = first;this.last = last;}public String getFirst() {return first;}public String getLast() {return last;}
}
  1. 然后,标记所有的特定类型,这里是String,把特定类型String替换为T,并申明:
public class Pair<T> {private T first;private T last;public Pair(T first, T last) {this.first = first;this.last = last;}public T getFirst() {return first;}public T getLast() {return last;}
}

熟练后即可直接从T开始编写。

这么看编写一个泛型类其实还是比较简单的,如果我们还想进一步了解Java泛型的实现方式,那么需要对擦拭法进行了解。

擦拭法

泛型是一种类似”模板代码“的技术,不同语言的泛型实现方式不一定相同。

Java语言的泛型实现方式是擦拭法(Type Erasure)。

所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。

例如,我们编写了一个泛型类Pair,这是编译器看到的代码:

public class Pair<T> {private T first;private T last;public Pair(T first, T last) {this.first = first;this.last = last;}public T getFirst() {return first;}public T getLast() {return last;}
}

而虚拟机根本不知道泛型。这是虚拟机执行的代码:

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

因此,Java使用擦拭法实现泛型,导致了:

  • 编译器把类型视为Object;
  • 编译器根据实现安全的强制转型。

使用泛型的时候,我们编写的代码也是编译器看到的代码:

Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();

而虚拟机执行的代码并没有泛型:

Pair p = new Pair("Hello", "world");
String first = (String) p.getFirst();
String last = (String) p.getLast();

所以,Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。

了解了Java泛型的实现方式——擦拭法,我们就知道了Java泛型的局限:

局限一:不能是基本类型,例如int,因为实际类型是Object,Object类型无法持有基本类型:

Pair<int> p = new Pair<>(1, 2); // compile error!

局限二:无法取得带泛型的Class。观察以下代码:

public class Main {public static void main(String[] args) {Pair<String> p1 = new Pair<>("Hello", "world");Pair<Integer> p2 = new Pair<>(123, 456);Class c1 = p1.getClass();Class c2 = p2.getClass();System.out.println(c1==c2); // trueSystem.out.println(c1==Pair.class); // true}
}class Pair<T> {private T first;private T last;public Pair(T first, T last) {this.first = first;this.last = last;}public T getFirst() {return first;}public T getLast() {return last;}
}

因为T是Object,我们对Pair和Pair类型获取Class时,获取到的是同一个Class,也就是Pair类的Class。

换句话说,所有泛型实例,无论T的类型是什么,getClass()返回同一个Class实例,因为编译后它们全部都是Pair。

局限三:无法判断带泛型的类型:

Pair<Integer> p = new Pair<>(123, 456);
// Compile error:
if (p instanceof Pair<String>) {
}

原因和前面一样,并不存在Pair.class,而是只有唯一的Pair.class。

局限四:不能实例化T类型:

public class Pair<T> {private T first;private T last;public Pair() {// Compile error:first = new T();last = new T();}
}

上述代码无法通过编译,因为构造方法的两行语句:

first = new T();
last = new T();

擦拭后实际上变成了:

first = new Object();
last = new Object();

这样一来,创建new Pair()和创建new Pair()就全部成了Object,显然编译器要阻止这种类型不对的代码。

要实例化T类型,我们必须借助额外的Class参数:

public class Pair<T> {private T first;private T last;public Pair(Class<T> clazz) {first = clazz.newInstance();last = clazz.newInstance();}
}

上述代码借助Class参数并通过反射来实例化T类型,使用的时候,也必须传入Class。例如:

Pair<String> pair = new Pair<>(String.class);

因为传入了Class的实例,所以我们借助String.class就可以实例化String类型。

类型通配符

  1. 类型通配符一般是使用 ? 代替具体的类型参数。例如 List<?> 在逻辑上是 List,List 等所有 List<具体类型实参> 的父类。
import java.util.*;public class GenericTest {public static void main(String[] args) {List<String> name = new ArrayList<String>();List<Integer> age = new ArrayList<Integer>();List<Number> number = new ArrayList<Number>();name.add("icon");age.add(18);number.add(314);getData(name);getData(age);getData(number);}public static void getData(List<?> data) {System.out.println("data :" + data.get(0));}
}

因为 getData() 方法的参数是 List<?> 类型的,所以 name,age,number 都可以作为这个方法的实参,这就是通配符的作用。

  1. 类型通配符上限通过形如List来定义,如此定义就是通配符泛型值接受Number及其下层子类类型。
import java.util.*;public class GenericTest {public static void main(String[] args) {List<String> name = new ArrayList<String>();List<Integer> age = new ArrayList<Integer>();List<Number> number = new ArrayList<Number>();name.add("icon");age.add(18);number.add(314);//getUperNumber(name);//1getUperNumber(age);//2getUperNumber(number);//3}public static void getData(List<?> data) {System.out.println("data :" + data.get(0));}public static void getUperNumber(List<? extends Number> data) {System.out.println("data :" + data.get(0));}
}

在 //1 处会出现错误,因为 getUperNumber() 方法中的参数已经限定了参数泛型上限为 Number,所以泛型为 String 是不在这个范围之内,所以会报错。

  1. 类型通配符下限通过形如 List<? super Number> 来定义,表示类型只能接受 Number 及其上层父类类型,如 Object 类型的实例。

参考文献

  1. https://www.geeksforgeeks.org/generics-in-java/
  2. https://www.runoob.com/java/java-generics.html
  3. https://liaoxuefeng.com/books/java/generics/index.html

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

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

相关文章

c++ websocket简单讲解

只做简单讲解。 一.定义和原理 WebSocket 是从 HTML5 开始⽀持的⼀种⽹⻚端和服务端保持⻓连接的消息推送机制&#xff0c;传统的 web 程序都是属于 "⼀问⼀答" 的形式&#xff0c;即客⼾端给服务器发送了⼀个 HTTP 请求&#xff0c;服务器给客⼾端返回⼀个 HTTP 响…

JS【详解】内置构造函数/对象 URL(可用于创建和解析 URL,操作搜索参数,编码 url)

用途 用于创建和解析 URL 语法 new URL(url, [base])第1个参数 url &#xff1a;可为完整 url 或路径&#xff08;如果设置了 base&#xff09;第2个参数 base&#xff1a;可选的 base URL&#xff0c;若第1个参数是路径&#xff0c;则会根据这个 base 生成 URL new URL(https…

【内网渗透】最保姆级的春秋云镜Certify打靶笔记

目录 flag1 flag2 flag3 flag4 flag1 fscan扫外网 访问8983端口&#xff0c;发现配置项有log4j 【vulhub】Log4j2&#xff1a;CVE-2021-44228漏洞复现_vulhub cve-2021-44228-CSDN博客 /solr/admin/collections?action${jndi:ldap://z5o5w8.dnslog.cn} dnslog测出可…

C++string类相关OJ练习(2)

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 Cstring类相关OJ练习(2) 收录于专栏【C语法基础】 本专栏旨在分享学习C的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 1.反转字符串 …

windows安装composer

windows安装composer 1.介绍 Composer 是一个用于PHP的依赖管理工具。Composer允许你声明你的PHP项目所依赖的库&#xff0c;并管理它们。它会安装和更新你项目所需要的库。 Composer为我们的项目提供了两个功能&#xff1a; 帮助PHP进行依赖管理。在项目中实现文件自动加载…

紫光同创——PLL IP 的使用(Logos2)

本文档主要针对 Logos2 系列的 PLL 配置&#xff0c;至于 Logos 系列的 PLL&#xff0c;可以参考《PLLIP 的使用(Logos)》的文档。 一、PLL IP 介绍 1、PLL 基本配置模式 Basic Configurations PLL IP 是紫光同创基于 PLL 及时钟网络资源设计的 IP&#xff0c;通过不同的参数配…

电量提醒工具(充电提醒)

可提醒50%到100%之间的整电量&#xff0c;女声提醒&#xff0c;不占资源&#xff0c;防止设备未充上电。 https://download.csdn.net/download/zzmzzff/89713499

input系统之InputDispatcher

往期文章&#xff1a; Input系统之IMS的启动流程 input系统之InputReader 1.概述 InputReader和InputDispatcher是Input系统的重要组成部分&#xff0c;InputReader主要负责从设备节点获取原始输入事件&#xff0c;并将封装好的事件交给InputDispatcher&#xff1b;InputDis…

快速构建 AI 应用的利器:Python 库 Mesop

在当今这个 AI 技术飞速发展的时代&#xff0c;开发者们总是希望能够更快、更便捷地构建 AI 应用程序。今天&#xff0c;我要给大家介绍一个由 Google 推出的 Python 库——Mesop。它的出现&#xff0c;让我们能够轻松地搭建高效的 AI 应用。 Mesop 是什么&#xff1f; Mesop …

MATLAB生成COE文件

MATLAB代码 % 参数设置 N 4096; % 数据点数量 t linspace(0, 2*pi, N); % 时间向量 width 12; % 位宽% 正弦波&#xff0c;幅度在0到5之间 sine_wave 2.5 * sin(t) 2.5;% 三角波&#xff0c;幅度在0到5之间 tri_wave 5 * (1 - abs(mod(t/(2*pi)*4, 2) - 1));% 方波&…

echarts--Y轴名称超宽换行显示行高问题处理

设置yAxis.axisLabel.overflow:break为超宽换行 yAxis: [{type: category,inverse: true, //y轴坐标轴向下position: left, // 设置 y 轴的位置在左边offset: 65, // 设置 y 轴距离左边的偏移量axisLine: {show: false,},axisTick: {show: false},axisLabel: {show: true,inter…

集成电路学习:什么是MOSFET(MOS管)

一、MOSFET&#xff1a;MOS管 MOSFET&#xff0c;全称Metal-Oxide-Semiconductor Field-Effect Transistor&#xff0c;即金属-氧化物半导体场效应晶体管&#xff0c;也常被称为MOS管或金氧半场效晶体管。它是一种可以广泛使用在模拟电路与数字电路的场效应晶体管&#xff08;f…

day07-集合-MapStream递归

一、Collections 1.1 可变参数 可变参数就是一种特殊形参&#xff0c;定义在方法、构造器的形参列表里&#xff0c;格式是&#xff1a;数据类型... 参数名称 ​ 优点特点&#xff1a;可以不传数据给它&#xff1b;可以传一个或者同时传多个数据给它&#xff1b;也可以传一个数…

【系统架构设计师-2022年】综合知识-答案及详解

文章目录 【第1题】【第2题】【第3题】【第4题】【第5题】【第6~7题】【第8题】【第9题】【第10题】【第11~12题】【第13题】【第14题】【第15题】【第16题】【第17~18题】【第19题】【第20题】【第21题】【第22题】【第23题】【第24题】【第25题】【第26题】【第27题】【第28题…

【AQS源码】深入理解AQS的工作原理

【AQS源码】深入理解AQS的工作原理-CSDN博客

叉车专用AI防撞预警系统,带行车记录功能,守护人车安全!

AI防撞预警系统是一款为工业车辆等工程设备专门设计的智能视频监控装置。该系统通过三个独立的摄像头和深度学习算法实现机器视觉识别。 本系统的一个重要功能是能够能够实时侦测工作区域的危险状态并提供警示&#xff0c;一旦有人员进入危险区域&#xff0c;驾驶员即可得到动态…

SpringBoot2:RESTFUL风格接口开发及源码解读

一、RESTFUL简介 Rest风格支持&#xff08;使用HTTP请求方式&#xff0c;动词来表示对资源的操作&#xff09; 以前&#xff1a;/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户 现在&#xff1a; /user GET-获取用户 DELETE-删除用户 PUT-修改…

海外合规|新加坡网络安全认证计划简介(三)-Cyber Trust

一、 认证简介&#xff1a; Cyber Trust标志是针对数字化业务运营更为广泛的组织的网络安全认证。该标志针对的是规模较大或数字化程度较高的组织&#xff0c;因为这些组织可能具有更高的风险水平&#xff0c;需要他们投资专业知识和资源来管理和保护其 IT 基础设施和系统。Cy…

ES6语法详解

以下是ES6常用的一些语法和特性&#xff1a; 声明变量的关键字变化&#xff1a;使用let和const、var来声明变量。 箭头函数&#xff1a;使用箭头&#xff08;>&#xff09;定义函数&#xff0c;简化函数的写法。 模板字符串&#xff1a;使用反引号&#xff08;&#xff0…

【python】socket 入门以及多线程tcp链接

Socket 入门 及 多线程tcp链接 网络基础知识三要素 Socket是套接字的意思,是网络编程的核心对象,通信两端都独有自己的Socket对象, 数据在两个Socket之间通过 字节流(TCP协议) 或者 数据报包(UDP协议)的形式进行传输. 本文主要针对tcp流程进行讲解 socket-tcp流程图 1.创建服…