彻底讲清 Java 的泛型(上)

简介: 彻底讲清 Java 的泛型(上)

普通的类和方法只能使用特定的类型:基本数据类型或类类型。

如果编写的代码需要应用于多种类型,这种严苛的限制对代码的束缚就会很大。


多态是一种面向对象思想的泛化机制。可以将方法的参数类型设为基类,这样的方法就可以接受任何派生类作为参数,包括暂时还不存在的类。

这样的方法更通用,应用范围更广。在类内部也是如此,在任何使用特定类型的地方,基类意味着更大的灵活性。

除了 final 类(或只提供私有构造函数的类)任何类型都可被扩展,所以大部分时候这种灵活性是自带的。


接口可以突破继承体系的限制

单一的继承体系太过局限,因为只有继承体系中的对象才能适用基类作为参数的方法中。如果方法以接口而不是类作为参数,限制就宽松多了,只要实现了接口就可以。这给予调用方一种选项,通过调整现有的类来实现接口,满足方法参数要求。


接口的限制

一旦指定了接口,它就要求你的代码必须使用特定的接口。而我们希望编写更通用的代码,能够适用“非特定的类型”,而不是一个具体的接口或类。


这就是泛型的概念,是 Java 5 的重大变化。泛型实现了参数化类型,这样你编写的组件(比如集合)可以适用于多种类型。“泛型”这个术语的含义是“适用于很多类型”。

编程语言中泛型出现的初衷是通过解耦类或方法与所使用的类型之间的约束,使得类或方法具备最宽泛的表达力。

随后你会发现 Java 中泛型的实现并没有那么“泛”,你可能会质疑“泛型”这个词是否合适用来描述这一功能。


实例化一个类型参数时,编译器会负责转型并确保类型的正确性。使用别人创建好的泛型相对容易,但是创建自己的泛型时,就会遇到很多意料之外的麻烦。

在很多情况下,它可以使代码更直接更优雅。不过,如果你见识过那种实现了更纯粹的泛型的编程语言,那么,Java 可能会令你失望。


本章会介绍 Java 泛型的优点与局限。我会解释 Java 的泛型是如何发展成现在这样的,希望能够帮助你更有效地使用这个特性。[^1]

1 与 C++ 的比较

Java 的设计者曾说过,这门语言的灵感主要来自 C++ 。尽管如此,学习 Java 时基本不用参考 C++ 。

但是,Java 中的泛型需要与 C++ 进行对比,理由有两个

1.1 理解 C++ 模板

泛型的主要灵感来源,包括基本语法的某些特性,有助于理解泛型的基础理念。

同时可以理解

  • Java 泛型的局限是什么
  • 为什么会有这些局限
  • 最终明确 Java 泛型的边界

只有知道了某个技术不能做什么,你才能更好地做到所能做的(不必浪费时间在死胡同)。

1.2 误解 C++ 模板

在 Java 社区中,大家普遍对 C++ 模板有一种误解,而这种误解可能会令你在理解泛型的意图时产生偏差。

因此,本章中会介绍少量 C++ 模板的例子,仅当它们确实可以加深理解时才会引入。

2 简单泛型

促成泛型出现的最主要的动机之一是创建集合类:几乎所有程序在运行过程中都会涉及到一组对象

持有单个对象的类

明确指定其持有的对象的类型

image.png

可复用性不高,无法持有其他类型的对象。不希望为碰到的每个类型都编写一个新的类。

Java 5 前,可以让这个类

直接持有 Object 对象

  • 一个 ObjectHolder 先后持有了三种不同类型的对象:
  • image.png
  • 现在,ObjectHolder 可以持有任何类型的对象


通常只会用集合存储同一种类型的对象。


泛型的主要目的之一:约定集合要存储什么类型对象,并且通过编译器保证


因此与其使用 Object ,我们更希望先指定一个类型占位符,稍后决定具体使用什么类型。

要达到这个目的,需要使用类型参数,用尖括号括住,放在类名后面。

然后在使用类时,再用实际类型替换此类型参数。


在下面的例子中,T 就是类型参数:

image.png

创建 GenericHolder 对象时,必须指明要持有的对象的类型,置于尖括号

然后,就只能在 GenericHolder 中存储该类型(或其子类,多态与泛型不冲突)的对象。

当你调用 get() 取值时,直接就是正确的类型。


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


h3 的定义非常繁复。在 = 左边有 GenericHolder<Automobile>, 右边又重复了一次。在 Java 5 中,这种写法被解释成“必要的”,Java 7 修正了这个问题。


一般来说,你可以认为泛型和其他类型差不多,只不过它们碰巧有类型参数。

在使用泛型时,只需要指定它们的名称和类型参数列表。

3 一个元组类库

有时一个方法需要能返回多个对象。而 return 语句只能返回单个对象,解决方法就是创建一个对象,用它打包想要返回的多个对象。

当然,可以在每次需要的时候,专门创建一个类来完成这样的工作。

有了泛型,我们就可以一劳永逸。同时,还获得了编译时的类型安全。

这称为

元组

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


元组可以具有任意长度,元组中对象可以不同类型。

不过,我们希望能够为每个对象指明类型,并且从元组中读取出来时,能够得到正确的类型。

要处理不同长度的问题,我们需要创建多个不同的元组。


下面是一个可以存储两个对象的元组:

image.png

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


初次阅读你可能认为这违反了 Java 编程的封装原则:a1 和 a2 应该声明为 private,然后提供 getFirst() 和 getSecond() 取值方法

这样做能提供的“安全性”:元组的使用程序可以读取 a1 和 a2 对它们执行任何操作,但无法对 a1 和 a2 重新赋值。final 可以实现同样效果,更简洁。


而这里是另一种设计思路:

允许用户给 a1 和 a2 重新赋值。然而更加安全,如果用户想存储不同的元素,就会强制他们创建新的 Tuple2 对象。

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

image.png

演示需要,再定义两个类:

// generics/Amphibian.java
public class Amphibian {}
// generics/Vehicle.java
public class Vehicle {}

使用元组时,只需要定义一个长度适合的元组,将其作为返回值即可

image.png

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

通过 ttsi.a1 = "there" 语句的报错,我们可以看出,final 声明确实可以确保 public 字段在对象被构造出来之后就不能重新赋值了。

new 表达式有些啰嗦。

泛型方法 简化元组

使用类型参数推断和静态导入,把早期的元组重写为更通用的库。

重载静态方法创建元组:

image.png

我们修改 TupleTest.java 来测试 Tuple.java :

image.png

f() 返回参数化 Tuple2, f2() 返回未参数化的 Tuple2。编译器不会在这里警告 f2() ,因为返回值未以参数化方式使用。从某种意义上说,它被“向上转型”为一个未参数化的 Tuple2 。 但是,如果尝试将 f2() 的结果放入到参数化的 Tuple2 中,则编译器将发出警告。


目录
相关文章
|
2月前
|
安全 Java
Java之泛型使用教程
Java之泛型使用教程
238 10
|
4月前
|
安全 Java API
在Java中识别泛型信息
以上步骤和示例代码展示了怎样在Java中获取泛型类、泛型方法和泛型字段的类型参数信息。这些方法利用Java的反射API来绕过类型擦除的限制并访问运行时的类型信息。这对于在运行时进行类型安全的操作是很有帮助的,比如在创建类型安全的集合或者其他复杂数据结构时处理泛型。注意,过度使用反射可能会导致代码难以理解和维护,因此应该在确有必要时才使用反射来获取泛型信息。
216 11
|
安全 Java 编译器
揭秘JAVA深渊:那些让你头大的最晦涩知识点,从泛型迷思到并发陷阱,你敢挑战吗?
【8月更文挑战第22天】Java中的难点常隐藏在其高级特性中,如泛型与类型擦除、并发编程中的内存可见性及指令重排,以及反射与动态代理等。这些特性虽强大却也晦涩,要求开发者深入理解JVM运作机制及计算机底层细节。例如,泛型在编译时检查类型以增强安全性,但在运行时因类型擦除而丢失类型信息,可能导致类型安全问题。并发编程中,内存可见性和指令重排对同步机制提出更高要求,不当处理会导致数据不一致。反射与动态代理虽提供运行时行为定制能力,但也增加了复杂度和性能开销。掌握这些知识需深厚的技术底蕴和实践经验。
278 2
|
Java 编译器 容器
Java——包装类和泛型
包装类是Java中一种特殊类,用于将基本数据类型(如 `int`、`double`、`char` 等)封装成对象。这样做可以利用对象的特性和方法。Java 提供了八种基本数据类型的包装类:`Integer` (`int`)、`Double` (`double`)、`Byte` (`byte`)、`Short` (`short`)、`Long` (`long`)、`Float` (`float`)、`Character` (`char`) 和 `Boolean` (`boolean`)。包装类可以通过 `valueOf()` 方法或自动装箱/拆箱机制创建。
179 9
Java——包装类和泛型
|
安全 Java API
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
String常量池、String、StringBuffer、Stringbuilder有什么区别、List与Set的区别、ArrayList和LinkedList的区别、HashMap底层原理、ConcurrentHashMap、HashMap和Hashtable的区别、泛型擦除、ABA问题、IO多路复用、BIO、NIO、O、异常处理机制、反射
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
|
存储 安全 Java
🌱Java零基础 - 泛型详解
【10月更文挑战第7天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
151 1
|
Java 语音技术 容器
java数据结构泛型
java数据结构泛型
116 5
|
存储 Java 编译器
Java集合定义其泛型
Java集合定义其泛型
96 1
|
存储 Java 编译器
【用Java学习数据结构系列】初识泛型
【用Java学习数据结构系列】初识泛型
113 2
|
存储 安全 搜索推荐
Java中的泛型
【9月更文挑战第15天】在 Java 中,泛型是一种编译时类型检查机制,通过使用类型参数提升代码的安全性和重用性。其主要作用包括类型安全,避免运行时类型转换错误,以及代码重用,允许编写通用逻辑。泛型通过尖括号 `&lt;&gt;` 定义类型参数,并支持上界和下界限定,以及无界和有界通配符。使用泛型需注意类型擦除、无法创建泛型数组及基本数据类型的限制。泛型显著提高了代码的安全性和灵活性。
198 8