二十、泛型(1)

news2024/11/25 22:31:51

本章概要

  • 基本概念
    • 与 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.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 就是类型参数:

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();
    }
}

创建 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 checked
        Automobile 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;
    }

    @Override
    public String toString() {
        return "(" + rep() + ")";
    }
}

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

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

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

    @Override
    public 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;
    }
    
    @Override
    public 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;
    }

    @Override
    public 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 自动装箱为 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());
    }
}

在这里插入图片描述

有了泛型,你可以很容易地创建元组,令其返回一组任意类型的对象。

通过 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() 这个方法。

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

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

相关文章

预制构建生产管理系统

在建筑施工领域&#xff0c;预制构件的使用越来越广泛&#xff0c;它能够提高工程质量和施工效率&#xff0c;减少施工现场对环境的影响。通过凡尔码的二维码管理平台&#xff0c;系统从&#xff1a;生产过程、生产数据、生产计划、生产管理、成品交付等多个环节、工序和流程进…

mermaid学习第一天/更改主题颜色和边框颜色/《需求解释流程图》

mermaid 在线官网&#xff1a; https://mermaid-js.github.io/ 在线学习文件&#xff1a; https://mermaid.js.org/syntax/quadrantChart.html 1、今天主要是想做需求解释的流程图&#xff0c;又不想自己画&#xff0c;就用了&#xff0c;框框不能直接进行全局配置&#xff0…

如何助力机构单位提升运营效率?有什么靠谱的大数据管理平台?

随着数字化时代的到来&#xff0c;大数据管理平台已成为机构单位提高效率、优化资源配置的关键工具。其中&#xff0c;工单管理系统作为大数据管理平台的重要组成部分&#xff0c;对于机构单位运营的优化和提升具有举足轻重的地位。本文小编将为您介绍一款优秀的工单管理系统—…

Zygote进程通信为什么用Socket而不是Binder?

Zygote进程是Android系统中的一个特殊进程&#xff0c;它在系统启动时被创建&#xff0c;并负责孵化其他应用进程。它的主要作用是预加载和共享应用进程的资源&#xff0c;以提高应用启动的速度。 在Android系统中&#xff0c;常用的进程通信方式有以下几种&#xff1a; Intent…

概率论和数理统计(一)基本概念

前言 生活中对于事件的发生,可以概括为 确定现象&#xff1a;在一定条件下必然发生&#xff0c;如日出随机现象&#xff1a;在个别试验中其结果呈现出不确定性&#xff0c;在大量重复试验中其结果又具有统计规律的现象&#xff0c;称之为随机现象。 随机现象的特点&#xff…

独家分享 | BI零售数据分析模板,可视化,更易懂

“人、货、场”是零售数据分析的三大关键&#xff0c;只要能又快又透彻地掌握这三大关键的数据情况&#xff0c;即可为零售运营决策提供关键的数据支持&#xff0c;提高盈利、降低成本、优化采购库存结构等。奥威BI软件这套BI零售数据分析模板套装围绕“人、货、场”预设了数十…

低代码平台如火如荼,告诉我它具体能做什么?

目录 一、前言 二、低代码平台 三、低代码平台的优劣 四、低代码能解决哪些问题&#xff1f; 五、好用且强大的低代码平台 六、结语 一、前言 目前低代码平台如火如荼。这一新兴技术为企业提供了一种高效、灵活、快速开发应用程序的方法&#xff0c;并在短时间内取得了巨大成功…

游戏中找不到d3dx9_43.dll怎么办,教你快速解决方法

在计算机的世界里&#xff0c;我们经常会遇到一些让人头疼的问题。比如&#xff0c;有一天&#xff0c;小明正在玩他最喜欢的游戏&#xff0c;突然弹出了一个错误提示&#xff1a;“由于找不到d3dx9_43.dll,无法继续执行代码”。小明感到非常困惑&#xff0c;不知道这是什么意思…

【python VS vba】(5) 在python中使用xlwt操作Excel(待完善ing)

目录 1 什么是xlwt 2 导入xlwt 3 相关语法 3.1 创建新的workbook 3.2 创建新的sheet 3.3 保存workbook 4 python里表格的形式 4.1 矩阵 4.2 EXCEL的数据形式 完全等于矩阵的数字结构 4.3 python里矩阵 5 具体代码 5.1 代码 5.2 结果 5.3 要注意的问题 5.3.1 不能…

PostgreSQL manual

set path D:\DB\PostgreSQL\16\binconnect to database –h is host name -p is port number -d is database name -U is for user name psql -h localhost -p 5432 -d postgres -U postgresexport dump file 參考 pg_dump -h 192.168.1.1 -U uesername dbname > dbna…

项目实现思路-建设工程监理现场用表管理系统

项目实现思路-建设工程监理现场用表管理系统 文章目录 项目实现思路-建设工程监理现场用表管理系统1.架构&#xff08;B/S&#xff09;2.主要功能3.**技术细节**3.1 总体概要3.2 **技术细节之流程审批**3.3 **技术细节之电子签名和公章** 1.架构&#xff08;B/S&#xff09; 系…

期中成绩怎么发布?

作为一名老师&#xff0c;期中考试结束后&#xff0c;你可能正在为如何发布成绩而烦恼。传统的纸质方式不仅耗时而且容易出错&#xff0c;那么有没有一种方式可以让学生自助查询成绩呢&#xff1f;答案是肯定的。下面就为你介绍几种实用的方法&#xff0c;让成绩发布变得轻松又…

2024天津理工大学中环信息学院专升本机械设计制造自动化专业考纲

2024年天津理工大学中环信息学院高职升本科《机械设计制造及其自动化》专业课考试大纲《机械设计》《机械制图》 《机械设计》考试大纲 教 材&#xff1a;《机械设计》&#xff08;第十版&#xff09;&#xff0c;高等教育出版社&#xff0c;濮良贵、陈国定、吴立言主编&#…

代码随想录算法训练营第四十二天 | LeetCode 1049. 最后一块石头的重量 II、494. 目标和、474. 一和零

代码随想录算法训练营第四十二天 | LeetCode 1049. 最后一块石头的重量 II、494. 目标和、474. 一和零 文章链接&#xff1a;最后一块石头的重量 II 目标和 一和零 视频链接&#xff1a;最后一块石头的重量 II 目标和 一和零 1. LeetCode 1049. 最后一块石头的重量 II 1.1 思路…

四川天蝶电子商务有限公司:短视频运营怎么样?

短视频是一种以短小精悍的内容来吸引用户的新型媒体形式&#xff0c;近年来在社交网络平台上迅速走红&#xff0c;成为当今互联网世界的新宠。然而&#xff0c;要想成功运营短视频&#xff0c;需要借助一系列的策略和技巧&#xff0c;通过精心的规划和执行&#xff0c;才能够吸…

实用-----七牛云绑定自定义域名 配置 HTTPS

实用-----七牛云绑定自定义域名 配置 HTTPS&#xff08;无废话 无尿点&#xff09; 1.访问SSL证书购买页面 https://portal.qiniu.com/certificate/ssl 2.购买免费证书 3.补全信息 注意&#xff1a; 域名直接输入 无需 www座机号随意填 区号需要搜索 下面为示例 4. 直接确认…

2019年408真题复盘

紫色标记是认为有一定的思维难度或重点总结 红色标记是这次刷真题做错的 记录自己对题目的一些想法与联系&#xff0c;可能并不太关注题目本身。 分数用时 选择部分 80/8036min大题部分41/7094min总分121130min 摘自知乎老哥&#xff1a;“我做历年真题时&#xff0c;绝大部分…

为什么要强调AI技术与边缘智能结合?应用场景有哪些?

随着城市规模扩大所带来的公共安全问题越来越受到重视。传统城市安全视频监控系统前端摄像机内置计算能力较低&#xff0c;以边缘计算和万物互联技术为基础的新型视频监控系统是未来发展趋势。在移动计算和物联网进步的推动下&#xff0c;数十亿移动和物联网设备连接到互联网&a…

linux入门到精通-第五章-动态库和静态库

目录 参考概述1、静态链接2 、动态链接3 、静态、动态编译对比 静态库和动态库简介传统编译 静态库制作和使用1、创建静态库的过程2、使用静态库 动态库制作和使用1、创建动态库的过程1&#xff09;、生成目标文件&#xff0c;此时要加编译选项&#xff1a;-fPIC &#xff08;f…

PMP含金量怎么样?

是高的&#xff0c;就单看持证人数吧&#xff0c;今年就已经有50万人获得了PMP证书&#xff0c;全球160多万&#xff0c;占了大概三分之一。 PMP的含金量高在哪里&#xff1f; 1、系统的项目管理知识 PMP主要针对PMBOK指南内容展开&#xff0c;结合实际项目案例&#xff0c;系…