初识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

    若对DerivedSetterset()方法使用@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);}
}

        程序执行的结果是:

        显然,这里发生的还是重载。若使用的是自限定,最后只会有一个接收确切类型参数的方法版本。

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

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

相关文章

爱芯元智AX650N部署yolov8s 自定义模型

爱芯元智AX650N部署yolov8s 自定义模型 本博客将向你展示零基础一步步的部署好自己的yolov8s模型&#xff08;博主展示的是自己训练的手写数字识别模型&#xff09;&#xff0c;本博客教你从训练模型到转化成利于Pulsar2 工具量化部署到开发板上 训练自己的YOLOv8s模型 准备自…

神经网络:脑科学中功能MRI成像的应用及其一些相关概念

文章目录 一、MRI成像简介核磁共振成像&#xff08;MRI&#xff09;侵入式成像功能磁共振成像&#xff08;fMRI&#xff09;血氧水平依赖&#xff08;BOLD&#xff09;效应对比基线状态代理指标 二、fMRI具有延迟性及其解决方案原因解决方法 三、fMRI 数据处理1. 数据预处理2. …

《2023全球隐私计算报告》正式发布!

2023全球隐私计算报告 1、2023全球隐私计算图谱2、国内外隐私计算相关政策3、隐私计算技术的最新发展4、隐私计算技术的合规挑战5、隐私计算的应用市场动态6、隐私计算开源整体趋势7、隐私计算的未来趋势 11月23日&#xff0c;由浙江省人民政府、商务部共同主办&#xff0c;杭州…

unity3d NPC寻路时相互挤压、导致离目标越来越远

更改寻路代理 约束的大小&#xff0c;人物周围绿色圆柱范围线&#xff0c;尽量调小

npm管理发布包-创建与发布

创建与发布 我们可以将自己开发的工具包发布到 npm 服务上&#xff0c;方便自己和其他开发者使用&#xff0c;操作步骤如下 创建文件夹&#xff0c;并创建文件indexjs&#xff0c;在文件中声明函数&#xff0c;使用 module.exports 暴露npm初始化工具包&#xff0c;package.j…

学C的第十一天【查看汇编代码一步步了解 函数栈帧(栈区局部变量)的创建和销毁】

相关代码gitee自取&#xff1a;C语言学习日记: 加油努力 (gitee.com) 接上期&#xff1a;学C的第十天&#xff08;继续深入学习函数、函数递归、练习&#xff09;-CSDN博客 函数栈帧的创建和销毁 越高级的编译器&#xff0c;越不容易学习和观察该过程 同时在不同的编译器下&…

会声会影2024旗舰版系统配置要求及格式支持

会声会影2024旗舰版是一款广受欢迎的视频编辑软件&#xff0c;它的最新版本&#xff0c;会声会影2023&#xff0c;已经发布。在这篇文章中&#xff0c;我们将探讨会声会影2024旗舰版系统配置要求及格式支持 会声会影2024是一款专业的视频剪辑软件&#xff0c;能够帮助用户制作高…

java:jpa、Hibernate、Spring Data JPA、ORM以及和mybatis的区别

文章目录 Java连接数据库几种方式JPAHibernate和Spring Data JPAORM框架jpa和mybatis区别Spring Boot JPA使用例子1、创建库和表2、添加依赖3、配置数据源和Hibernate属性4、配置实体类5、创建一个继承JpaRepository的接口&#xff1a;6、创建一个控制器&#xff08;Controller…

java反射和注解3-仿照retrofit组装接口参数

本片文章将用反射和注解仿照retrofit只需要传入一个带有给定注解的接口&#xff0c;通过调用接口就能直接将传入的数据和注解进行结合&#xff0c;生成对应参数 1&#xff0c;自定义注解 对字段的修饰 Retention(RetentionPolicy.RUNTIME) Target(ElementType.PARAMETER) pu…

代码块02使用细节-Java

代码块02使用细节 四、使用细节1、static代码块/静态代码块&#xff0c;随着类的加载而执行&#xff0c;且只执行一次2、 类什么时候被加载 [重要 ! ]案例演示&#xff1a;static代码块 3、普通代码块&#xff0c;在创建对象实例时&#xff0c;会被隐式的调用。案例演示&#x…

【算法每日一练]-图论(保姆级教程篇7 最小生成树 ,并查集模板篇)#村村通 #最小生成树

目录 题目&#xff1a;村村通 并查集 题目&#xff1a;最小生成树 kruskal算法 prim算法 先引入问题&#xff1a; 要在n个城市之间铺设光缆&#xff0c;主要目标是要使这 n 个城市的任意两个之间都可以通信&#xff0c;但铺设光缆的费用很高&#xff0c;且各个城市之间铺…

C++ day37 贪心算法 单调递增的数字 监控二叉树

题目1&#xff1a;738 单调递增的数字 题目链接&#xff1a;单调递增的数字 对题目的理解 返回小于或等于n的最大数字&#xff0c;且数字是单调递增&#xff08;单调递增数字的定义&#xff1a;每个相邻位上的数字满足x<y&#xff09; 贪心算法 注意本题的遍历顺序是从…