二十、泛型(1)
本章概要
- 基本概念
- 与 C++ 的比较
- 简单泛型
- 一个元组类库
- 一个堆栈类
- RandomList
基本概念
普通的类和方法只能使用特定的类型:基本数据类型或类类型。如果编写的代码需要应用于多种类型,这种严苛的限制对代码的束缚就会很大。
多态是一种面向对象思想的泛化机制。你可以将方法的参数类型设为基类,这样的方法就可以接受任何派生类作为参数,包括暂时还不存在的类。这样的方法更通用,应用范围更广。在类内部也是如此,在任何使用特定类型的地方,基类意味着更大的灵活性。除了 final
类(或只提供私有构造函数的类)任何类型都可被扩展,所以大部分时候这种灵活性是自带的。
拘泥于单一的继承体系太过局限,因为只有继承体系中的对象才能适用基类作为参数的方法中。如果方法以接口而不是类作为参数,限制就宽松多了,只要实现了接口就可以。这给予调用方一种选项,通过调整现有的类来实现接口,满足方法参数要求。接口可以突破继承体系的限制。
即便是接口也还是有诸多限制。一旦指定了接口,它就要求你的代码必须使用特定的接口。而我们希望编写更通用的代码,能够适用“非特定的类型”,而不是一个具体的接口或类。
这就是泛型的概念,是 Java 5 的重大变化之一。泛型实现了_参数化类型_,这样你编写的组件(通常是集合)可以适用于多种类型。“泛型”这个术语的含义是“适用于很多类型”。编程语言中泛型出现的初衷是通过解耦类或方法与所使用的类型之间的约束,使得类或方法具备最宽泛的表达力。随后你会发现 Java 中泛型的实现并没有那么“泛”,你可能会质疑“泛型”这个词是否合适用来描述这一功能。
如果你从未接触过参数化类型机制,你会发现泛型对 Java 语言确实是个很有益的补充。在你实例化一个类型参数时,编译器会负责转型并确保类型的正确性。这是一大进步。
然而,如果你了解其他语言(例如 C++ )的参数化机制,你会发现,Java 泛型并不能满足所有的预期。使用别人创建好的泛型相对容易,但是创建自己的泛型时,就会遇到很多意料之外的麻烦。
这并不是说 Java 泛型毫无用处。在很多情况下,它可以使代码更直接更优雅。不过,如果你见识过那种实现了更纯粹的泛型的编程语言,那么,Java 可能会令你失望。本章会介绍 Java 泛型的优点与局限。我会解释 Java 的泛型是如何发展成现在这样的,希望能够帮助你更有效地使用这个特性。
与 C++ 的比较
Java 的设计者曾说过,这门语言的灵感主要来自 C++ 。尽管如此,学习 Java 时基本不用参考 C++ 。
但是,Java 中的泛型需要与 C++ 进行对比,理由有两个:首先,理解 C++ 模板(泛型的主要灵感来源,包括基本语法)的某些特性,有助于理解泛型的基础理念。同时,非常重要的一点是,你可以了解 Java 泛型的局限是什么,以及为什么会有这些局限。最终的目标是明确 Java 泛型的边界,让你成为一个程序高手。只有知道了某个技术不能做什么,你才能更好地做到所能做的(部分原因是,不必浪费时间在死胡同里)。
第二个原因是,在 Java 社区中,大家普遍对 C++ 模板有一种误解,而这种误解可能会令你在理解泛型的意图时产生偏差。
因此,本章中会介绍少量 C++ 模板的例子,仅当它们确实可以加深理解时才会引入。
简单泛型
促成泛型出现的最主要的动机之一是为了创建_集合类_,参见 集合 章节。集合用于存放要使用到的对象。数组也是如此,不过集合比数组更加灵活,功能更丰富。几乎所有程序在运行过程中都会涉及到一组对象,因此集合是可复用性最高的类库之一。
我们先看一个只能持有单个对象的类。这个类可以明确指定其持有的对象的类型:
class Automobile {
}public class Holder1 {private Automobile a;public Holder1(Automobile a) {this.a = a;}Automobile get() {return a;}
}
这个类的可复用性不高,它无法持有其他类型的对象。我们可不希望为碰到的每个类型都编写一个新的类。
在 Java 5 之前,我们可以让这个类直接持有 Object
类型的对象:
// generics/ObjectHolder.javapublic class ObjectHolder {private Object a;public ObjectHolder(Object a) { this.a = a; }public void set(Object a) { this.a = a; }public Object get() { return a; }public static void main(String[] args) {ObjectHolder h2 = new ObjectHolder(new Automobile());Automobile a = (Automobile)h2.get();h2.set("Not an Automobile");String s = (String)h2.get();h2.set(1); // 自动装箱为 IntegerInteger x = (Integer)h2.get();}
}
现在,ObjectHolder
可以持有任何类型的对象,在上面的示例中,一个 ObjectHolder
先后持有了三种不同类型的对象。
一个集合中存储多种不同类型的对象的情况很少见,通常而言,我们只会用集合存储同一种类型的对象。泛型的主要目的之一就是用来约定集合要存储什么类型的对象,并且通过编译器确保规约得以满足。
因此,与其使用 Object
,我们更希望先指定一个类型占位符,稍后再决定具体使用什么类型。要达到这个目的,需要使用_类型参数_,用尖括号括住,放在类名后面。然后在使用这个类时,再用实际的类型替换此类型参数。在下面的例子中,T
就是类型参数:
public class ObjectHolder {private Object a;public ObjectHolder(Object a) {this.a = a;}public void set(Object a) {this.a = a;}public Object get() {return a;}public static void main(String[] args) {ObjectHolder h2 = new ObjectHolder(new Automobile());Automobile a = (Automobile) h2.get();h2.set("Not an Automobile");String s = (String) h2.get();h2.set(1); // 自动装箱为 IntegerInteger x = (Integer) h2.get();}
}
创建 GenericHolder
对象时,必须指明要持有的对象的类型,将其置于尖括号内,就像 main()
中那样使用。然后,你就只能在 GenericHolder
中存储该类型(或其子类,因为多态与泛型不冲突)的对象了。当你调用 get()
取值时,直接就是正确的类型。
这就是 Java 泛型的核心概念:你只需告诉编译器要使用什么类型,剩下的细节交给它来处理。
你可能注意到 h3
的定义非常繁复。在 =
左边有 GenericHolder<Automobile>
, 右边又重复了一次。在 Java 5 中,这种写法被解释成“必要的”,但在 Java 7 中设计者修正了这个问题(新的简写语法随后成为备受欢迎的特性)。以下是简写的例子:
GenericHolder.java
public class GenericHolder<T> {private T a;public GenericHolder() {}public void set(T a) {this.a = a;}public T get() {return a;}public static void main(String[] args) {GenericHolder<Automobile> h3 =new GenericHolder<Automobile>();h3.set(new Automobile()); // type checkedAutomobile a = h3.get(); // No cast needed//- h3.set("Not an Automobile"); // Error//- h3.set(1); // Error}
}
Diamond.java
class Bob {
}public class Diamond<T> {public static void main(String[] args) {GenericHolder<Bob> h3 = new GenericHolder<>();h3.set(new Bob());}
}
注意,在 h3
的定义处,=
右边的尖括号是空的(称为“钻石语法”),而不是重复左边的类型信息。在本书剩余部分都会使用这种语法。
一般来说,你可以认为泛型和其他类型差不多,只不过它们碰巧有类型参数罢了。在使用泛型时,你只需要指定它们的名称和类型参数列表即可。
一个元组类库
有时一个方法需要能返回多个对象。而 return 语句只能返回单个对象,解决方法就是创建一个对象,用它打包想要返回的多个对象。当然,可以在每次需要的时候,专门创建一个类来完成这样的工作。但是有了泛型,我们就可以一劳永逸。同时,还获得了编译时的类型安全。
这个概念称为_元组_,它是将一组对象直接打包存储于单一对象中。可以从该对象读取其中的元素,但不允许向其中存储新对象(这个概念也称为 数据传输对象 或 信使 )。
通常,元组可以具有任意长度,元组中的对象可以是不同类型的。不过,我们希望能够为每个对象指明类型,并且从元组中读取出来时,能够得到正确的类型。要处理不同长度的问题,我们需要创建多个不同的元组。下面是一个可以存储两个对象的元组:
public class Tuple2<A, B> {public final A a1;public final B a2;public Tuple2(A a, B b) {a1 = a;a2 = b;}public String rep() {return a1 + ", " + a2;}@Overridepublic String toString() {return "(" + rep() + ")";}
}
构造函数传入要存储的对象。这个元组隐式地保持了其中元素的次序。
初次阅读上面的代码时,你可能认为这违反了 Java 编程的封装原则。a1
和 a2
应该声明为 private,然后提供 getFirst()
和 getSecond()
取值方法才对呀?考虑下这样做能提供的“安全性”是什么:元组的使用程序可以读取 a1
和 a2
然后对它们执行任何操作,但无法对 a1
和 a2
重新赋值。例子中的 final
可以实现同样的效果,并且更为简洁明了。
另一种设计思路是允许元组的用户给 a1
和 a2
重新赋值。然而,采用上例中的形式无疑更加安全,如果用户想存储不同的元素,就会强制他们创建新的 Tuple2
对象。
我们可以利用继承机制实现长度更长的元组。添加更多的类型参数就行了:
Tuple3.java
public class Tuple3<A, B, C> extends Tuple2<A, B> {public final C a3;public Tuple3(A a, B b, C c) {super(a, b);a3 = c;}@Overridepublic String rep() {return super.rep() + ", " + a3;}
}
Tuple4.java
public class Tuple4<A, B, C, D>extends Tuple3<A, B, C> {public final D a4;public Tuple4(A a, B b, C c, D d) {super(a, b, c);a4 = d;}@Overridepublic String rep() {return super.rep() + ", " + a4;}
}
Tuple5.java
public class Tuple5<A, B, C, D, E>extends Tuple4<A, B, C, D> {public final E a5;public Tuple5(A a, B b, C c, D d, E e) {super(a, b, c, d);a5 = e;}@Overridepublic String rep() {return super.rep() + ", " + a5;}
}
演示需要,再定义两个类:
Amphibian.java
// generics/Amphibian.java
public class Amphibian {}
Vehicle.java
public class Vehicle {
}
使用元组时,你只需要定义一个长度适合的元组,将其作为返回值即可。注意下面例子中方法的返回类型:
public class TupleTest {static Tuple2<String, Integer> f() {// 47 自动装箱为 Integerreturn new Tuple2<>("hi", 47);}static Tuple3<Amphibian, String, Integer> g() {return new Tuple3<>(new Amphibian(), "hi", 47);}static Tuple4<Vehicle, Amphibian, String, Integer> h() {return new Tuple4<>(new Vehicle(), new Amphibian(), "hi", 47);}static Tuple5<Vehicle, Amphibian, String, Integer, Double> k() {return new Tuple5<>(new Vehicle(), new Amphibian(), "hi", 47, 11.1);}public static void main(String[] args) {Tuple2<String, Integer> ttsi = f();System.out.println(ttsi);// ttsi.a1 = "there"; // 编译错误,因为 final 不能重新赋值System.out.println(g());System.out.println(h());System.out.println(k());}
}
有了泛型,你可以很容易地创建元组,令其返回一组任意类型的对象。
通过 ttsi.a1 = "there"
语句的报错,我们可以看出,final 声明确实可以确保 public 字段在对象被构造出来之后就不能重新赋值了。
在上面的程序中,new
表达式有些啰嗦。本章稍后会介绍,如何利用 泛型方法 简化它们。
一个堆栈类
接下来我们看一个稍微复杂一点的例子:堆栈。在 集合 一章中,我们用 LinkedList
实现了 onjava.Stack
类。在那个例子中,LinkedList
本身已经具备了创建堆栈所需的方法。Stack
是通过两个泛型类 Stack<T>
和 LinkedList<T>
的组合来创建。我们可以看出,泛型只不过是一种类型罢了(稍后我们会看到一些例外的情况)。
这次我们不用 LinkedList
来实现自己的内部链式存储机制。
// 用链式结构实现的堆栈public class LinkedStack<T> {private static class Node<U> {U item;Node<U> next;Node() {item = null;next = null;}Node(U item, Node<U> next) {this.item = item;this.next = next;}boolean end() {return item == null && next == null;}}private Node<T> top = new Node<>(); // 栈顶public void push(T item) {top = new Node<>(item, top);}public T pop() {T result = top.item;if (!top.end()) {top = top.next;}return result;}public static void main(String[] args) {LinkedStack<String> lss = new LinkedStack<>();for (String s : "Phasers on stun!".split(" ")) {lss.push(s);}String s;while ((s = lss.pop()) != null) {System.out.println(s);}}
}
输出结果:
内部类 Node
也是一个泛型,它拥有自己的类型参数。
这个例子使用了一个 末端标识 (end sentinel) 来判断栈何时为空。这个末端标识是在构造 LinkedStack
时创建的。然后,每次调用 push()
就会创建一个 Node<T>
对象,并将其链接到前一个 Node<T>
对象。当你调用 pop()
方法时,总是返回 top.item
,然后丢弃当前 top
所指向的 Node<T>
,并将 top
指向下一个 Node<T>
,除非到达末端标识,这时就不能再移动 top
了。如果已经到达末端,程序还继续调用 pop()
方法,它只能得到 null
,说明栈已经空了。
RandomList
作为容器的另一个例子,假设我们需要一个持有特定类型对象的列表,每次调用它的 select()
方法时都随机返回一个元素。如果希望这种列表可以适用于各种类型,就需要使用泛型:
import java.util.*;
import java.util.stream.*;public class RandomList<T> extends ArrayList<T> {private Random rand = new Random(47);public T select() {return get(rand.nextInt(size()));}public static void main(String[] args) {RandomList<String> rs = new RandomList<>();Arrays.stream("The quick brown fox jumped over the lazy brown dog".split(" ")).forEach(rs::add);IntStream.range(0, 11).forEach(i ->System.out.print(rs.select() + " "));}
}
输出结果:
RandomList
继承了 ArrayList
的所有方法。本例中只添加了 select()
这个方法。
相关文章:

二十、泛型(1)
本章概要 基本概念 与 C 的比较 简单泛型 一个元组类库一个堆栈类RandomList 基本概念 普通的类和方法只能使用特定的类型:基本数据类型或类类型。如果编写的代码需要应用于多种类型,这种严苛的限制对代码的束缚就会很大。 多态是一种面向对象思想的泛…...

【Unity数据交互】游戏中常用到的Json序列化
ˊˊ 👨💻个人主页:元宇宙-秩沅 👨💻 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 👨💻 本文由 秩沅 原创 👨💻 收录于专栏࿱…...

TCP的滑动窗口和拥塞控制
目录 滑动窗口 1.发送窗口和接收窗口 2.滑动窗口的分类 停止等待协议:发送窗口大小 1, 接收窗口大小 1 后退N帧协议(GBN):发送窗口大小 > 1,接收窗口大小 1 选择重传协议(SR…...

零信任网络:一种全新的网络安全架构
随着网络技术的不断发展,网络安全问题日益凸显。传统的网络安全策略往往基于信任和验证,但这种信任策略存在一定的局限性。为了解决这一问题,零信任网络作为一种全新的网络安全架构,逐渐受到人们的关注。本文将对零信任网络的概念…...

基于单片机的智能拐杖软件设计
欢迎大家点赞、收藏、关注、评论啦 ,由于篇幅有限,只展示了部分核心代码。 技术交流认准下方 CSDN 官方提供的联系方式 文章目录 概要 一、整体设计方案2.1本设计设计原理2.1.1单片机基本介绍 二、本设计方案选择三、软件设计AD原理图:原理图…...

小程序如何设置自动预约快递
小程序通过设置自动预约功能,可以实现自动将订单信息发送给快递公司,快递公司可以自动上门取件。下面具体介绍如何设置。 在小程序管理员后台->配送设置处,选择首选配送公司。为了能够支持自动预约快递,请选择正常的快递公司&…...

STM32-HAL库08-TIM的输出比较模式(输出PWM的另一种方式)
STM32-HAL库08-TIM的输出比较模式(输出PWM的另一种方式) 一、所用材料: STM32F103C6T6最小系统板 STM32CUBEMX(HAL库软件) MDK5 示波器或者逻辑分析仪 二、所学内容: 通过定时器TIM的输出比较模式得到预…...

【数据结构】深入浅出讲解计数排序【图文详解,搞懂计数排序这一篇就够了】
计数排序 前言一、计数排序算法核心思路映射 概念补充绝对映射相对映射 二、计数排序算法核心实现步骤三、码源详解四、效率分析(1)时间复杂度 — O(Max(N,range))(2)空间…...

Canvas制作喷泉效果示例
Canvas能制作出很多动画效果,下面是一个制作喷泉效果的示例 效果图 源代码 <!DOCTYPE html> <html> <head> <meta charset"utf-8"> <meta name"viewport" content"widthdevice-width, initial-scale1 ,user-…...

什么是NPM(Node Package Manager)?它的作用是什么?
聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅:探索Web开发的奇妙世界 欢迎来到前端入门之旅!感兴趣的可以订阅本专栏哦!这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…...

oracle如果不适用toad或者plsql工具如何获取索引建表语句
select dbms_lob.substr(dbms_metadata.get_ddl(INDEX,INDEX_NAME,DIXON))||; from dba_indexes where ownerDIXON这个语句可以获取dixon用户的所有索引创建语句,sql脚本形式呈现 点开一个语句查看 如果不使用dbms_lob.substr这个函数最后得到是一个clob selec…...

某大厂伺服驱动器量产方案
本文介一款大厂量产伺服驱动器方案!带2500线省线式编码器,17位增量编码器,20位绝对值编码器!标配CANopen、高精度运动控制,高速总线通讯,主芯片28335FPGA,已验证过,带can和485通讯&a…...

【计算机网络】网络层:数据平面
一.网络层概述 每台路由器的数据平面的主要功能时从其输入链路向其输出链路转发数据报,控制平面的主要功能是协调这些本地的每路由转发动作,使得数据报沿着源和目的地主机之间的路由器路径最终进行端到端传送。 网络层不运行运输层和应用层协议。 转发是…...

Path with “WEB-INF“ or “META-INF“: [webapp/WEB-INF/NewFile.html]
2023-11-04 01:03:14.523 WARN 10896 --- [nio-8072-exec-6] o.s.w.s.r.ResourceHttpRequestHandler : Path with "WEB-INF" or "META-INF": [webapp/WEB-INFNewFile.html] spring.mvc.view.prefix:/webapp/WEB-INF/...
百度OCR 接口调用 提示 216101:param image not exist 问题解决
百度提供的文档并没有描述如何解决,例子也是,用工具请求可以通 axios 请求 需要用FormData 传参 let token await getAccessToken() //官网案例那个 请求token// console.log(token, "token");var formData new FormData();// imageBase64 :Base64 图片数据formD…...
1-10 HTML中input属性
HTML中input属性 text:用于接受单行文本输入password:用于密码输入,输入字符会被掩盖radio:用于单选按钮,用户可以在一组选项中选择一个checkbox:用于复选框,用户可以选择多个选项number&#…...

共焦显微镜使用
x.1 细胞培养 x.2 样品制备 以细菌为例,我们使用荧光染色细菌,静置15分钟。 15分钟后我们使用实验室的专用培养皿,选择吸收100uL的溶液滴在在培养皿中心。 x.3 显微镜使用 我们按照1, 2, 3, 4的顺序打开显微镜, 打开电脑&…...

windows + Mingw32-make 编译 PoDoFo库,openssl, libjpeg, Msys2工具的使用
参考: https://blog.csdn.net/sspdfn/article/details/104244306 https://blog.csdn.net/yaoyuanyylyy/article/details/17436303 https://blog.csdn.net/wxlfreewind/article/details/106492253 前期进行了各种摸索,由于Podofo依赖库比较多,…...

C++中图的存储
文章目录 0. 实例图1. 邻接矩阵2. 邻接矩阵2.1 链表数组2.2 链式前向星 3. 参考 0. 实例图 考虑下面这样一个图 1. 邻接矩阵 vis[i][j] 表示从i 到j有一条边。直接用二维数组就可以了。 using namespace std; int vertex_num 5; vector<vector<int>> graph(v…...
西瓜书读书笔记整理(七)—— 第七章 贝叶斯分类器
第七章 贝叶斯分类器 7.1 贝叶斯决策论(Bayesian Decision Theory)7.1.1 先验概率(Prior Probability)7.1.2 后验概率(Posterior Probability)7.1.3 似然度(Likelihood)7.1.4 决策规…...

XML Group端口详解
在XML数据映射过程中,经常需要对数据进行分组聚合操作。例如,当处理包含多个物料明细的XML文件时,可能需要将相同物料号的明细归为一组,或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码,增加了开…...

循环冗余码校验CRC码 算法步骤+详细实例计算
通信过程:(白话解释) 我们将原始待发送的消息称为 M M M,依据发送接收消息双方约定的生成多项式 G ( x ) G(x) G(x)(意思就是 G ( x ) G(x) G(x) 是已知的)࿰…...

基于Flask实现的医疗保险欺诈识别监测模型
基于Flask实现的医疗保险欺诈识别监测模型 项目截图 项目简介 社会医疗保险是国家通过立法形式强制实施,由雇主和个人按一定比例缴纳保险费,建立社会医疗保险基金,支付雇员医疗费用的一种医疗保险制度, 它是促进社会文明和进步的…...

React19源码系列之 事件插件系统
事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...
css的定位(position)详解:相对定位 绝对定位 固定定位
在 CSS 中,元素的定位通过 position 属性控制,共有 5 种定位模式:static(静态定位)、relative(相对定位)、absolute(绝对定位)、fixed(固定定位)和…...
C++八股 —— 单例模式
文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全(Thread Safety) 线程安全是指在多线程环境下,某个函数、类或代码片段能够被多个线程同时调用时,仍能保证数据的一致性和逻辑的正确性…...

如何在最短时间内提升打ctf(web)的水平?
刚刚刷完2遍 bugku 的 web 题,前来答题。 每个人对刷题理解是不同,有的人是看了writeup就等于刷了,有的人是收藏了writeup就等于刷了,有的人是跟着writeup做了一遍就等于刷了,还有的人是独立思考做了一遍就等于刷了。…...

【开发技术】.Net使用FFmpeg视频特定帧上绘制内容
目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法,当前调用一个医疗行业的AI识别算法后返回…...

安宝特案例丨Vuzix AR智能眼镜集成专业软件,助力卢森堡医院药房转型,赢得辉瑞创新奖
在Vuzix M400 AR智能眼镜的助力下,卢森堡罗伯特舒曼医院(the Robert Schuman Hospitals, HRS)凭借在无菌制剂生产流程中引入增强现实技术(AR)创新项目,荣获了2024年6月7日由卢森堡医院药剂师协会࿰…...
比较数据迁移后MySQL数据库和OceanBase数据仓库中的表
设计一个MySQL数据库和OceanBase数据仓库的表数据比较的详细程序流程,两张表是相同的结构,都有整型主键id字段,需要每次从数据库分批取得2000条数据,用于比较,比较操作的同时可以再取2000条数据,等上一次比较完成之后,开始比较,直到比较完所有的数据。比较操作需要比较…...