【Java】泛型

2023-09-20 16:55:37

简单泛型

促成泛型出现的最主要的动机之一是为了创建集合类,我们先看一个只能持有单个对象的类。这个类可以明确指定其持有的对象的类型:

// generics/Holder1.java
class Automobile {}
public class Holder1 {
    private Automobile a;
    public Holder1(Automobile a) { this.a = a; }
    Automobile get() { return a; }
}

这个类的可复用性不高,它无法持有其他类型的对象。我们可不希望为碰到的每个类型都编写一个新的类。在 Java 5 之前,我们可以让这个类直接持有 Object 类型的对象:

// generics/ObjectHolder.java
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); // 自动装箱为 Integer
        Integer x = (Integer)h2.get();
    }
}

现在,ObjectHolder 可以持有任何类型的对象,在上面的示例中,一个 ObjectHolder 先后持有了三种不同类型的对象。Object可以试用泛型T来进行替代

// generics/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()); // 此处有类型校验
        Automobile a = h3.get();  // 无需类型转换
        //- h3.set("Not an Automobile"); // 报错
        //- h3.set(1);  // 报错
    }
}

创建 GenericHolder 对象时,必须指明要持有的对象的类型,将其置于尖括号内,就像 main() 中那样使用。然后,你就只能在 GenericHolder 中存储该类型(或其子类,因为多态与泛型不冲突)的对象了。当你调用 get() 取值时,直接就是正确的类型。

这就是 Java 泛型的核心概念:你只需告诉编译器要使用什么类型,剩下的细节交给它来处理。

一个元组类库
有时一个方法需要能返回多个对象。而 return 语句只能返回单个对象,解决方法就是创建一个对象,用它打包想要返回的多个对象。当然,可以在每次需要的时候,专门创建一个类来完成这样的工作。但是有了泛型,我们就可以一劳永逸。同时,还获得了编译时的类型安全。

这个概念称为元组它是将一组对象直接打包存储于单一对象中。可以从该对象读取其中的元素,但不允许向其中存储新对象(这个概念也称为 数据传输对象信使 )。

通常,元组可以具有任意长度,元组中的对象可以是不同类型的。不过,我们希望能够为每个对象指明类型,并且从元组中读取出来时,能够得到正确的类型。要处理不同长度的问题,我们需要创建多个不同的元组。下面是一个可以存储两个对象的元组:

// onjava/Tuple2.java
package onjava;
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; }
    @Override
    public String toString() {
        return "(" + rep() + ")";
    }
}

构造函数传入要存储的对象。这个元组隐式地保持了其中元素的次序。

初次阅读上面的代码时,你可能认为这违反了 Java 编程的封装原则。a1a2 应该声明为 private,然后提供 getFirst()getSecond() 取值方法才对呀?考虑下这样做能提供的“安全性”是什么:元组的使用程序可以读取 a1a2 然后对它们执行任何操作,但无法对 a1a2 重新赋值。例子中的 final 可以实现同样的效果,并且更为简洁明了。

另一种设计思路是允许元组的用户给 a1a2 重新赋值。然而,采用上例中的形式无疑更加安全,如果用户想存储不同的元素,就会强制他们创建新的 Tuple2 对象。

我们可以利用继承机制实现长度更长的元组。添加更多的类型参数就行了:

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;
    }
    @Override
    public String rep() {
        return super.rep() + ", " + a3;
    }
}

使用元组时,你只需要定义一个长度适合的元组,将其作为返回值即可。注意下面例子中方法的返回类型:

public class TupleTest {
    static Tuple2<String, Integer> f() {
        // 47 自动装箱为 Integer
        return 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());
    }
}
/* 输出:
 (hi, 47)
 (Amphibian@1540e19d, hi, 47)
 (Vehicle@7f31245a, Amphibian@6d6f6e28, hi, 47)
 (Vehicle@330bedb4, Amphibian@2503dbd3, hi, 47, 11.1)
 */
// generics/Amphibian.java
public class Amphibian {}
// generics/Vehicle.java
public class Vehicle {}

泛型方法

到目前为止,我们已经研究了参数化整个类。其实还可以参数化类中的方法。类本身可能是泛型的,也可能不是,不过这与它的方法是否是泛型的并没有什么关系。

泛型方法独立于类而改变方法。作为准则,请“尽可能”使用泛型方法。通常将单个方法泛型化要比将整个类泛型化更清晰易懂。

如果方法是 static 的,则无法访问该类的泛型类型参数,因此,如果使用了泛型类型参数,则它必须是泛型方法。

要定义泛型方法,请将泛型参数列表放置在返回值之前,如下所示:

// generics/GenericMethods.java
public class GenericMethods {
    public <T> void f(T x) {
        System.out.println(x.getClass().getName());
    }
    public static void main(String[] args) {
        GenericMethods gm = new GenericMethods();
        gm.f("");
        gm.f(1);
        gm.f(1.0);
        gm.f(1.0F);
        gm.f('c');
        gm.f(gm);
    }
}
/* Output:
java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
java.lang.Character
GenericMethods
*/

尽管可以同时对类及其方法进行参数化,但这里未将 GenericMethods 类参数化。只有方法 f() 具有类型参数,该参数由方法返回类型之前的参数列表指示。

对于泛型类,必须在实例化该类时指定类型参数。使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。 这称为 类型参数推断。因此,对 f() 的调用看起来像普通的方法调用,并且 f() 看起来像被重载了无数次一样。它甚至会接受 GenericMethods 类型的参数。

如果使用基本类型调用 f() ,自动装箱就开始起作用,自动将基本类型包装在它们对应的包装类型中。

节选自《On Java8》

更多推荐

自然语言处理技术之词向量:GloVe单词表示的全局向量(glove.840B.300d、glove.6B)

目录一、词向量介绍二、GloVe学习词向量的词嵌入模型三、词向量入门(代码下载)四、训练五、模型概述六、可视化七、发布历史一、词向量介绍自然语言处理(NLP)中的词向量是将文本中的词汇表示为数值向量的技术。词向量的主要作用是将文本数据转换成计算机可以理解和处理的形式,以便进行各种NLP任务。以下是词向量在NLP中的主要

nbcio-boot移植到若依ruoyi-nbcio平台里一formdesigner部分(三)

因为这个版本的若依plus不支持本地文件上传,所以需要增加这些本地上传文件的后端代码和前端代码修改。1、后端部分先配置跳过测试吧,平时编译也不需要这个<!--添加配置跳过测试--><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-s

配置Vite获取内网IP(Vue3项目​ts版本获取本机局域网IP地址)

参考文章:vue项目获取本机局域网IP地址(vue.config.js版本)在Vite中,没有vue.config.js文件,而是使用vite.config.js(或vite.config.ts,如果项目使用TypeScript)来配置项目;1.获取IP需要借助os模块,需要先安装依赖:npminstallos2.其次

睿趣科技:抖音开网店新手卖什么好

随着社交媒体和电子商务的迅猛发展,越来越多的人开始探索在抖音上开网店,希望能够通过这一平台实现创业梦想。但对于新手来说,选择什么产品进行销售可能会是一个困扰。在本文中,我们将探讨一些适合抖音新手的热门产品和一些成功经验,以帮助你在抖音上开设一家成功的网店。潮流服饰:服装和配饰一直是电子商务的热门领域之一。通过在抖音上展

轻量型服务器能支撑多少人访问?

一、服务器配置影响访问人数服务器的配置是影响轻量型服务器能够支撑的访问人数的关键因素之一。通常而言,轻量型服务器的配置普遍不高,适合小型团队或个人使用。如果服务器配置较低,那么支撑访问人数的能力也会受到限制。较为简单的应用程序对服务器性能要求不高,可以支持较多的访问人数。但是,如果应用程序较为复杂,对服务器性能要求较高

9月16日,每日信息差

今天是2023年09月16日,以下是为您准备的15条信息差第一、天猫超市首单“茅小凌”已由菜鸟送达,首单已由菜鸟供应链完成履约,18分钟送达消费者手中第二、软银考虑对OpenAI进行投资。此外,软银还初步拟收购英国人工智能芯片制造商Graphcore第三、我国共有327家网约车平台公司取得经营许可。各地共发放网约车驾驶

行业追踪,2023-09-20

自动复盘2023-09-20凡所有相,皆是虚妄。若见诸相非相,即见如来。k线图是最好的老师,每天持续发布板块的rps排名,追踪板块,板块来开仓,板块去清仓,丢弃自以为是的想法,板块去留让市场来告诉你跟踪板块总结:成交额超过100亿排名靠前,macd柱由绿转红成交量要大于均线有必要给每个行业加一个上级的归类,这样更能体现

C# Math.Round()四舍五入、四舍六入五成双

开发者为了实现小数点后2位的四舍五入,编写了如下代码,varnum=Math.Round(12.125,2);代码非常的简单,开发者实际得到的结果是12.12,这与其所预期的四舍五入结果12.13相悖。其实产生这个结果的原因是由于Math.Round默认使用的并非是四舍五入的原则,而是四舍六入五成双的原则。四舍六入五成

华为HCIA(三)

链路本地地址接口标识64bit当STP端口到了Forwarding状态后,会转发流量,也处理报文在TCP/IP模型中,会话层,表示层和应用层,都规划成了应用层路由表包含目的地址和掩码,优先级,cost,下一跳和出接口。Destination(目的地)protocol(协议)学习进制!!!在NCP协商完成后,PPP保持通

第37章_瑞萨MCU零基础入门系列教程之DAC数模转换模块

本教程基于韦东山百问网出的DShanMCU-RA6M5开发板进行编写,需要的同学可以在这里获取:https://item.taobao.com/item.htm?id=728461040949配套资料获取:https://renesas-docs.100ask.net瑞萨MCU零基础入门系列教程汇总:https://b

前端实现PDF预览:简单而高效的方法

前言PDF是一种常用的文件格式,但在网页中直接预览PDF文件可能会带来一些挑战。本文将介绍一种简单而高效的前端方法,以实现PDF文件的预览。使用iframe标签嵌入PDF文件最简单的方法是使用iframe标签来嵌入PDF文件。代码如下所示:<iframesrc="/path/to/pdf/file.pdf"width=

热文推荐