初识Java 18-4 泛型
目录
泛型存在的问题
在泛型中使用基本类型
实现参数化接口
类型转换和警告
无法实现的重载
基类会劫持接口
自限定类型
奇异递归类型
自限定
自限定提供的参数协变性
本笔记参考自: 《On Java 中文版》
泛型存在的问题
接下来讨论的,是在泛型中经常可能遇到的一些问题。
在泛型中使用基本类型
Java的泛型并不支持基本类型,因此我们无法将其用作泛型的类型参数。一个替代的方法是使用基本类型的包装类:
【例子:通过包装类使用泛型】
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;public class ListOfInt {public static void main(String[] args) {List<Integer> li = IntStream.range(38, 48).boxed() // 将基本类型转换成其对应的包装类.collect(Collectors.toList());System.out.println(li);}
}
程序执行的结果是:
这足以应付大部分的情况。但如果真的需要追求性能,可以使用专门适配基本类型的集合,例如org.apache.commons.collections.primitives。
或者,可以使用泛型集合来装载基本类型:
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;public class ByteSet {Byte[] possibles = {1, 2, 3, 4, 5, 6, 7, 8, 9};Set<Byte> mySet1 =new HashSet<>(Arrays.asList(possibles));// 不可行的方式:/* Set<Byte> mySet2 =new HashSet<>(Arrays.<Byte>asList(1, 2, 3, 4, 5, 6, 7, 8, 9)); */
}
在这里,自动装箱机制为我们解决了转换问题。但它不会总是有效,例如:
【例子:向数组中填充对象】
import java.util.*;
import java.util.function.*;interface FillArray {static <T> T[] fill(T[] a, Supplier<T> gen) {// 使用get()填充数组aArrays.setAll(a, n -> gen.get());return a;}static int[] fill(int[] a, IntSupplier gen) {Arrays.setAll(a, n -> gen.getAsInt());return a;}static long[] fill(long[] a, LongSupplier gen) {Arrays.setAll(a, n -> gen.getAsLong());return a;}static double[] fill(double[] a, DoubleSupplier gen) {Arrays.setAll(a, n -> gen.getAsDouble());return a;}
}interface Rand {// SplittableRandom也是用于生成随机数的类SplittableRandom r = new SplittableRandom(47);class StringGenerator implements Supplier<String> {int strlen;StringGenerator(int strlen) {this.strlen = strlen;}@Overridepublic String get() {return r.ints(strlen, 'a', 'z' + 1).collect(StringBuilder::new,StringBuilder::appendCodePoint,StringBuilder::append).toString();}}class IntegerGenerator implements IntSupplier {@Overridepublic int getAsInt() {return r.nextInt(10_000);}}
}public class PrimitiveGenericTest {public static void main(String[] args) {String[] strings = FillArray.fill(new String[5], new Rand.StringGenerator(7));System.out.println(Arrays.toString(strings));int[] integers = FillArray.fill(new int[9], new Rand.IntegerGenerator());System.out.println(Arrays.toString(integers));}
}
程序执行的结果是:
由于自动装箱对数组无效,因此需要我们手动重载FillArray.fill()方法,或者通过一个生成器来包装输出结果。
实现参数化接口
一个类无法实现同一个泛型接口的两种变体:
因为类型擦除,这两个变体实际上都表示着原生的Payable。换言之,上述代码中Hourly将同一个接口实现了两次。
类型转换和警告
因为类型擦除,我们无法对类型参数使用类型转换或instanceof。因此,有时会需要在边界处进行类型转换:
【例子:在泛型边界处进行类型转换】
import java.util.Arrays;
import java.util.stream.Stream;class FixedSizeStack<T> {private final int size;private Object[] storage;private int index = 0;FixedSizeStack(int size) {this.size = size;storage = new Object[size];}public void push(T item) {if (index < size)storage[index++] = item;}@SuppressWarnings("unchecked")public T pop() {return index == 0 ?null : (T) storage[--index];}@SuppressWarnings("unchecked")Stream<T> stream() {return (Stream<T>) Arrays.stream(storage);}
}public class GenericCast {static String[] letters ="ABCDEFGHIJKLMNOPQRST".split("");public static void main(String[] args) {FixedSizeStack<String> strings =new FixedSizeStack<>(letters.length);Arrays.stream(letters).forEach(strings::push);System.out.println(strings.pop());strings.stream().map(s -> s + " ").forEach(System.out::print);}
}
程序执行的结果是:
pop()和stram()会产生警告,因为编译器无法知道这种类型转换是否安全。在本例中,类型参数T会被擦除成Object。
虽然在泛型的边界处,类型转换会自动发生。但有时我们仍然需要手动进行类型转换,此时编译器会发出警告:
【例子:对泛型进行转型】
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.List;public class NeedCasting {@SuppressWarnings("unchecked")public void f(String[] args) throws Exception {ObjectInputStream in = new ObjectInputStream(new FileInputStream(args[0]));List<Integer> shapes = (List<Integer>) in.readObject();}
}
实际上,readObject()不会知道它正在读取什么,因此它会返回Object。
现在注释掉@SuppressWarnings("unchecked"),并且使用参数-Xlint:unchecked进行编译:
警告清楚地告诉了我们,readObject()会返回一个未经检查的Object。
Java 5还引入了一个转型方法,通过Class.cast(),可以将对象强制转换成目标类型。这个方法也适用于泛型:
【例子:尝试强制转换泛型】
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.List;public class ClassCasting {@SuppressWarnings("unchecked")public void f(String[] args) throws Exception {ObjectInputStream in = new ObjectInputStream(new FileInputStream(args[0]));// 无法编译的代码:// List<Integer> lw1 =// List<>.class.cast(in.readObject()); // 使用cast()进行强制类型转换// 会引发警告:List<Integer> lw2 = List.class.cast(in.readObject());// 无法编译:// List<Integer> lw3 = List<Integer>.class.cast(in.readObject());// 会引发警告List<Integer> lw4 = (List<Integer>) List.class.cast(in.readObject());}
}
然而,如代码所示。这些做法都会存在着这样那样的限制。
无法实现的重载
由于类型擦除,下面的这种写法是不被允许的:
【例子:无法实现的重载】
import java.util.List;public class UseList<W, T> {void f(List<T> v) {}void f(List<W> v) {}
}
因为被擦除的参数无法作为单独的参数列表,所以我们还需要为每一个相似的方法提高不同的方法名。
基类会劫持接口
假设我们想要创建一个类,这个类实现了Comparable接口,这样这个类的不同对象就能进行互相的比较:
【例子:实现了Comparable的父类】
public class ComparablePetimplements Comparable<ComparablePet> {@Overridepublic int compareTo(ComparablePet arg) {return 0;}
}
一个好的想法是,任何继承了这个类的子类,其对象之间应该也能进行比较(在这个例子中,父类是Pet,子类就是Cat)。然而事实并不会如我们所愿:
遗憾的是,若继承了父类的泛型接口,编译器不会再允许我们添加另一个Comparable接口。在这里,我们只能遵循父类的比较方式。
我们还可以在子类中重写compareTo()的行为,但这种行为是面向ComparablePet的(而不是限定在这个子类中)。
自限定类型
自限定类型来自于Java早期的泛型使用习惯:
class SelfBounded<T extends SelfBounded<T>> { // ...
在这里,类型参数的边界就是类本身:SelfBounded有一个类型参数T,而参数T的边界却又是SelfBounded。
这种写法更加强调extends在泛型参数中使用时的含义。
奇异递归类型
先看一个自限定类型的简化版本。尽管无法直接继承泛型参数,但我们可以继承一个使用了泛型参数的类。
【例子:继承泛型类】
class GenericType<T> {
}public class CuriouslyRecurringGenericextends GenericType<CuriouslyRecurringGeneric> {
}
这种方式被称为奇异递归泛型。其中,“奇异递归”是指子类奇怪地出现在了其基类中的现象、
要理解这一点,首先需要明确:Java泛型的重点在于参数和返回类型,因此可以生成将派生类型作为参数和返回值的基类。派生类型也可作为字段,不过此时它们会被擦除为Object。
【例子:用子类替换基类的参数】
首先定义一个简单的泛型:
public class BasicHolder<T> {T element;void set(T arg) {element = arg;}T get() {return element;}void f() {System.out.println(element.getClass().getSimpleName());}
}
在这个基类中,所有方法的接收或返回值(若有)都是T。接下来尝试使用这个类:
class Subtype extends BasicHolder<Subtype> {
}public class CRGWithBasicHolder {public static void main(String[] args) {// Subtype中的所有方法,其接收和返回的都是Subtype:Subtype st1 = new Subtype(),st2 = new Subtype();st1.set(st2);Subtype st3 = st1.get();st1.f();}
}
程序执行的结果是:
需要注意的是,Subtype类中,所有方法的接收和返回值都已经变成了Subtype。这就是一个奇异递归泛型:基类用子类替换了其参数。在这里,基类用于提供通用的方法模板,而子类使用的方法都会具有一个具体的类型,即子类自身。
自限定
上述的BasicHolder可以将任何类型作为其泛型参数:
【例子:BasicHolder的广泛应用】
class Other {
}// 将不相关的Other作为参数
class BasicOther extends BasicHolder<Other> {
}
自限定在这种操作的基础上更进一步,它强制地把泛型作为自身的边界参数进行使用:
// 自限定类型:
class SelfBounded<T extends SelfBounded<T>> {T element;SelfBounded<T> set(T arg) {element = arg;return this;}T get() {return element;}
}class A extends SelfBounded<A> {
}// 属于SelfBounding<>的类型也可以这样使用:
class B extends SelfBounded<A> {
}class C extends SelfBounded<C> {C setAndGet(C arg) {set(arg);return get();}
}class D {
}
// 但这种做法是不被允许的:
// class E extends SelfBounding<D> {
// }// 这样的可以(自限定的语法并非强制性的):
class F extends SelfBounded {
}public class SelfBounding {public static void main(String[] args) {A a = new A();a.set(new A());a = a.set(new A()).get();a = a.get();C c = new C();c = c.setAndGet(new C());}
}
需要注意的是,自限定类型会要求类处于继承关系中。因此像E这种并不处于继承关系中的类无法使用自限定。
除此之外,可以看到编译器并没有对F这种写法发出警告:
class F extends SelfBounded {}
由此可知,编译器对自限定的语法并不做强制要求,这需要程序员自己注意(或使用工具保证不会使用原生类型)。
注意:自限定类型只服务于强制继承关系。若使用自限定,这意味着该类使用的类型参数和使用该参数的类属于同一个基类。
对于普通的泛型类而言,像上例中的E这样的类型是可以作为泛型参数的。这种泛型类就没有对继承关系的强制性要求。
除此之外,自限定还可用于泛型方法:
【例子:使用了自限定的泛型方法】
public class SelfBoundingMethods {static <T extends SelfBounded<T>> T f(T arg) {return arg.set(arg).get();}public static void main(String[] args) {A a = f(new A());}
}
这种做法的特点是,方法f()无法应用于自限定参数规定范围之外的对象。
自限定提供的参数协变性
自限定类型的价值在于它可以生成协变参数类型,即方法参数的类型会随着子类而变化。现在先来看一个协变参数类型的例子,这种写法是Java 5引入的:
【例子:Java中的协变参数类型】
class Base {
}class Derived extends Base {
}interface OrdinaryGetter {Base get();
}interface DerivedGetter extends OrdinaryGetter {@OverrideDerived get();
}public class CovariantReturnTypes {void test(DerivedGetter d) {Derived d2 = d.get();}
}
这种做法有着自洽的逻辑:子类方法可以返回比其基类方法更加具体的类型(但这种写法在Java 5之前是行不通的)。
而自限定方法则可以直接返回精确的派生类型:
【例子:自限定的返回值】
interface GenericGetter<T extends GenericGetter<T>> {T get();
}interface Getter extends GenericGetter<Getter> {
}public class GenericsAndReturnTypes {void test(Getter g) {Getter result = g.get();// 因为返回的类型是子类,因此可以用基类来承接:GenericGetter gg = g.get();}
}
不过,这种做法只在引入了协变类型的Java 5之后有效。
与上述这两种形式不同,在普通的类中,参数的类型无法随子类型而变化。
【例子:普通类的返回值】
class OrdinarySetter {void set(Base base) {System.out.println("OrdinarySetter.set(Base)");}
}class DerivedSetter extends OrdinarySetter {void set(Derived derived) {System.out.println("DerivedSetter.set(Derived)");}
}public class OrdinaryArguments {public static void main(String[] args) {Base base = new Base();Derived derived = new Derived();DerivedSetter ds = new DerivedSetter();ds.set(derived);// 编译通过,但这里发生的不是重写,是重载:ds.set(base);}
}
程序执行的结果是:
尽管在main()中,ds.set(derived)和ds.set(base)都是合法的,但发生的并不是重写,而是重载。从输出可以看出,在子类DerivedSetter中存在着两个set()方法,一个参数是Base,另一个的是Derived。
若对DerivedSetter的set()方法使用@Override注释,就可以看出问题。
当使用自限定类型时,子类中来自基类的方法的参数会发生改变,因此会出现下面这种情况:
【例子:子类方法的参数会被重写】
interface SelfBoundSetter<T extends SelfBoundSetter<T>> {void set(T arg);
}interface Setter extends SelfBoundSetter<Setter> {// 未进行任何改动,但实际上set()已经被重写
}public class SelfBoundingAndCovariantArguments {void testA(Setter s1, Setter s2, SelfBoundSetter sbs) {s1.set(s2);// 不允许这么做:// s1.set(sbs);}
}
s1.set(sbs)存在问题:
编译器认为基类无法匹配当前set()的类型,尽管上述代码中并没有在Setter中显式地重写set()方法,但set()的参数确实已经被重写了。
若不使用自限定,那么普通的继承机制就会启动:
【例子:普通的继承机制】
// 非自限定的类型:
class OtherGenericSetter<T> {void set(T arg) {System.out.println("GenericSetter.set(Base)");}
}class DerivedGS extends OtherGenericSetter<Base> {void set(Derived derived) {System.out.println("DerivedGS.set(Derived)");}
}public class PlainGenericInheritance {public static void main(String[] args) {Base base = new Base();Derived derived = new Derived();DerivedGS dgs = new DerivedGS();dgs.set(derived);// 发生了重载:dgs.set(base);}
}
程序执行的结果是:
显然,这里发生的还是重载。若使用的是自限定,最后只会有一个接收确切类型参数的方法版本。
相关文章:

初识Java 18-4 泛型
目录 泛型存在的问题 在泛型中使用基本类型 实现参数化接口 类型转换和警告 无法实现的重载 基类会劫持接口 自限定类型 奇异递归类型 自限定 自限定提供的参数协变性 本笔记参考自: 《On Java 中文版》 泛型存在的问题 接下来讨论的,是在泛型…...

家政保洁预约小程序app开发特点有哪些?
家政预约服务小程序APP开发的特点介绍; 1. 低成本:用户通过手机APP下单,省去了中介费用,降低了雇主的雇佣成本。 2. 高收入:家政服务人员通过手机APP接单,省去了中介费用,从而提高了服务人员的…...

【JavaEE初阶】 HTTP响应报文
文章目录 🌲序言🎍200 OK🍀404 Not Found🎄403 Forbidden🌴405 Method Not Allowed🎋500 Internal Server Error🌳504 Gateway Timeout🌲302 Move temporarily🎍301 Move…...
PTA: 螺旋矩阵
题目 所谓“螺旋矩阵”,是指对任意给定的N,将1到NN的数字从左上角第1个格子开始,按顺时针螺旋方向顺序填入NN的方阵里。本题要求构造这样的螺旋方阵。 格式 输入格式: 输入在一行中给出一个正整数N(<10)。 输出…...

SparkSQL远程调试(IDEA)
启动Intellij IDEA,打开spark源码项目,配置远程调试 Run->Edit Configuration 启动远程spark-sql spark-sql --verbose --driver-java-options "-Xdebug -Xrunjdwp:transportdt_socket,servery,suspendy,address5005"运行远程调试…...
Vue2 Vue3 响应式实现原理
Vue2 和 Vue3 的响应式实现原理有所不同。 Vue2 响应式实现原理: Vue2 使用 Object.defineProperty() 方法来实现数据劫持,从而实现数据的响应式更新。具体步骤如下: 首先,在初始化阶段,遍历 data 对象的所有属性&a…...

Android Tombstone 与Debuggerd 原理浅谈
一、前言 Android系统类问题主要有stability、performance、power、security。tombstoned是android平台的一个守护进程,它注册成3个socket服务端,客户端封装在crash_dump和debuggerd_client。 crash_dump用于跟踪定位C crash, debuggerd_cli…...
Matlab 三维电力线重建
文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 之前曾经讨论过关于悬链线方程的曲线拟合点云最小二乘法拟合曲线,在这篇博客中其实拟合的是悬链线的一种近似形式,但对于大多数情况下已经够用了。方程如下所示: z = A ( x 2 + y 2 ) +...

GoLang Filepath.Walk遍历优化
原生标准库在文件量过大时效率和内存均表现不好 1400万文件遍历Filepath.Walk 1400万文件重写直接调用windows api并处理细节 结论 1400万文件遍历时对比 对比条目filepath.walkwindows api并触发黑科技运行时间710秒22秒内存占用480M38M 关键代码 //超级快的文件遍历 fun…...
Java面向对象第7天
精华笔记: 成员内部类:了解,应用率不高 类中套类,外面的称为外部类,里面的称为内部类 内部类只服务于外部类,对外不具备可见性 内部类对象通常在外部类中创建 内部类中可以直接访问外部类的成员(包括私有…...

网络安全如何自学?
1.网络安全是什么 网络安全可以基于攻击和防御视角来分类,我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术,而“蓝队”、“安全运营”、“安全运维”则研究防御技术。 2.网络安全市场 一、是市场需求量高; 二、则是发展相对成熟…...
Flink-时间窗口
在流数据处理应用中,一个很重要、也很常见的操作就是窗口计算。所谓的“窗口”,一 般就是划定的一段时间范围,也就是“时间窗”;对在这范围内的数据进行处理,就是所谓的 窗口计算。所以窗口和时间往往是分不开的。 时…...

软件设计模式原则(三)单一职责原则
单一职责原则(SRP)又称单一功能原则。它规定一个类应该只有一个发生变化的原因。所谓职责是指类变化的原因。如果一个类有多于一个的动机被改变,那么这个类就具有多于一个的职责。而单一职责原则就是指一个类或者模块应该有且只有一个改变的原…...

使用Postman创建Mock Server
这篇文章将教会大家如何利用 Postman,通过 Mock 的方式测试我们的 API。 什么是 Mock Mock 是一项特殊的测试技巧,可以在没有依赖项的情况下进行单元测试。通常情况下,Mock 与其他方法的主要区别就是,用于取代代码依赖项的模拟对…...

【古月居《ros入门21讲》学习笔记】15_ROS中的坐标系管理系统
目录 说明: 1. 机器人中的坐标变换 tf功能包能干什么? tf坐标变换如何实现 2. 小海龟跟随实验 安装 ros-melodic-turtle-tf 实验命令 运行效果 说明: 1. 本系列学习笔记基于B站:古月居《ROS入门21讲》课程,且使…...

初始linux:文件操作
目录 提示:以下指令均在Xshell 7 中进行 linux的理念 一、echo echo "字符串" 二、输出重定向 > > [文件] echo "字符串" > [文件] echo "字符串" > > [文件] 制作大文件 三、< 输入重定向与ca…...

iOS上传ipa使用可视化工具Transporter
文章目录 前言一、Transporter二、Appuploader三、iTMSTransporter总结 前言 最近为了让非开发人员上传IPA文件,特意找了一些方法,至于以前的ApplicationUploader已经不能用了,下面介绍两个工具可以上传IPA包。 一、Transporter 1、操作简单…...

解读《陆奇最新演讲实录—我的大模型世界观》
腾讯科技频道记者张小珺一篇《陆奇最新演讲实录—我的大模型世界观》刷爆朋友圈。文章知识点丰富、字里行间处处流淌着创业方法论和AI应用商机,含金量极高! PS:一家之言、不求苟同。如有不爽之处,欢迎来 找我。 腾讯新闻原文&am…...

ChatGPT到底是如何运作?
自从2022年11月30日发布以来,ChatGPT一直占据着科技届的头条位置,随着苹果的创新能力下降,ChatGPT不断给大家带来震撼,2023年11月7日,首届OpenAI开发者大会在洛杉矶举行,业界普遍认为,OpenAI的开…...
学习Java第57天,Servlet的基本使用步骤
步骤1 开发一个web类型的module 步骤2 开发一个UserServlet public class UserServlet extends HttpServlet {Overrideprotected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {// 获取请求中的参数String usern…...

Chapter03-Authentication vulnerabilities
文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...

简易版抽奖活动的设计技术方案
1.前言 本技术方案旨在设计一套完整且可靠的抽奖活动逻辑,确保抽奖活动能够公平、公正、公开地进行,同时满足高并发访问、数据安全存储与高效处理等需求,为用户提供流畅的抽奖体验,助力业务顺利开展。本方案将涵盖抽奖活动的整体架构设计、核心流程逻辑、关键功能实现以及…...

visual studio 2022更改主题为深色
visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中,选择 环境 -> 常规 ,将其中的颜色主题改成深色 点击确定,更改完成...

CentOS下的分布式内存计算Spark环境部署
一、Spark 核心架构与应用场景 1.1 分布式计算引擎的核心优势 Spark 是基于内存的分布式计算框架,相比 MapReduce 具有以下核心优势: 内存计算:数据可常驻内存,迭代计算性能提升 10-100 倍(文档段落:3-79…...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...
Spring Boot面试题精选汇总
🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉 📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...
Typeerror: cannot read properties of undefined (reading ‘XXX‘)
最近需要在离线机器上运行软件,所以得把软件用docker打包起来,大部分功能都没问题,出了一个奇怪的事情。同样的代码,在本机上用vscode可以运行起来,但是打包之后在docker里出现了问题。使用的是dialog组件,…...

保姆级教程:在无网络无显卡的Windows电脑的vscode本地部署deepseek
文章目录 1 前言2 部署流程2.1 准备工作2.2 Ollama2.2.1 使用有网络的电脑下载Ollama2.2.2 安装Ollama(有网络的电脑)2.2.3 安装Ollama(无网络的电脑)2.2.4 安装验证2.2.5 修改大模型安装位置2.2.6 下载Deepseek模型 2.3 将deepse…...

【分享】推荐一些办公小工具
1、PDF 在线转换 https://smallpdf.com/cn/pdf-tools 推荐理由:大部分的转换软件需要收费,要么功能不齐全,而开会员又用不了几次浪费钱,借用别人的又不安全。 这个网站它不需要登录或下载安装。而且提供的免费功能就能满足日常…...
【Nginx】使用 Nginx+Lua 实现基于 IP 的访问频率限制
使用 NginxLua 实现基于 IP 的访问频率限制 在高并发场景下,限制某个 IP 的访问频率是非常重要的,可以有效防止恶意攻击或错误配置导致的服务宕机。以下是一个详细的实现方案,使用 Nginx 和 Lua 脚本结合 Redis 来实现基于 IP 的访问频率限制…...