当前位置:网站首页 > Java基础 > 正文

java泛型基础教程



Doker官网:Doker 多克

一、介绍

JDK5.0为Java编程语言引入了几个新的扩展。其中之一就是泛型的引入。

这条线索是对泛型的介绍。您可能熟悉其他语言中的类似结构,尤其是C++模板。如果是这样的话,你会发现两者既有相似之处,也有重要的区别。如果你不熟悉其他地方的相似结构,那就更好了;你可以重新开始,而不必忘记任何误解。

泛型允许对类型进行抽象。最常见的例子是容器类型,例如“集合”层次结构中的容器类型。

二、为什么要使用泛型?

简而言之,泛型使类型(类和接口)在定义类、接口和方法时成为参数。与方法声明中使用的更熟悉的形式参数非常相似,类型参数为您提供了一种使用不同输入重用相同代码的方法。不同之处在于,形式参数的输入是值,而类型参数的输入则是类型。

使用泛型的代码比非泛型代码有很多好处:

  • 在编译时进行更强的类型检查。

Java编译器对泛型代码应用强类型检查,如果代码违反类型安全,则会发出错误。修复编译时错误比修复运行时错误更容易,因为运行时错误很难找到。

  • 减少转型

以下没有泛型的代码段需要强制转换:

 

当重写为使用泛型时,代码不需要强制转换:

 
  • 使程序员能够实现通用算法。
    通过使用泛型,程序员可以实现泛型算法,这些算法适用于不同类型的集合,可以自定义,并且类型安全且更易于阅读。

三、泛型类型

泛型类型是在类型上参数化的泛型类或接口。将修改以下 Box 类以演示该概念。

1、一个简单的盒子类

首先检查一个对任何类型的对象进行操作的非泛型Box类。它只需要提供两种方法:set和get,前者向框中添加对象,后者检索对象:

 

由于它的方法接受或返回Object,所以只要它不是基元类型之一,就可以自由地传入任何您想要的内容。在编译时,无法验证类是如何使用的。代码的一部分可能会将Integer放在框中,并期望从中取出Integers,而代码的另一部分可能错误地传入String,从而导致运行时错误。

2、盒子类的泛型版本

泛型类使用以下格式定义:

 

类型参数部分由尖括号(<>)分隔,位于类名之后。它指定类型参数(也称为类型变量)T1、T2、…、。。。,和Tn。

要更新Box类以使用泛型,您可以通过将代码“public class Box”更改为“public class Box<T>”来创建泛型类型声明。这引入了类型变量T,它可以在类内的任何地方使用。

通过此更改,Box类变为:

 

正如您所看到的,Object的所有出现都被T替换。类型变量可以是您指定的任何非基元类型:任何类类型、任何接口类型、任何数组类型,甚至其他类型变量。

这种相同的技术可以应用于创建通用接口。

3、类型参数命名约定

按照约定,类型参数名称是单个大写字母。这与你已经知道的变量命名约定形成鲜明对比,并且有充分的理由:如果没有这个约定,就很难区分类型变量和普通类或接口名称之间的区别。

最常用的类型参数名称包括:

  • E - Element(被Java集合框架广泛使用)
  • K - 键
  • N - 数字
  • T - 类型
  • V - 值
  • S,U,V等- 第2、3、4类

您将看到这些名称在整个 Java SE API 和本课程的其余部分使用。

4、调用和实例化泛型类型

要从代码中引用泛型Box类,必须执行泛型类型调用,该调用将T替换为一些具体值,例如Integer:

 

您可以将泛型类型调用视为类似于普通方法调用,但不是将参数传递给方法,而是将类型参数(在本例中为Integer)传递给Box类本身。

与任何其他变量声明一样,此代码实际上并没有创建新的Box对象。它只是简单地声明integerBox将保存对“Box of Integer”的引用,这就是读取Box<Integer>的方式。

泛型类型的调用通常被称为参数化类型。

要实例化这个类,请像往常一样使用new关键字,但将<Integer>放在类名和括号之间:

 

5、Diamond

在Java SE 7及更高版本中,只要编译器能够从上下文中确定或推断类型参数,就可以将调用泛型类的构造函数所需的类型参数替换为一组空的类型参数(<>)。这对尖括号,<>,被非正式地称为钻石。例如,您可以使用以下语句创建Box<Integer>的实例:

 

6、多种类型参数

如前所述,泛型类可以有多个类型参数。例如,泛型 OrderedPair 类,它实现了泛型 Pair 接口:

 

以下语句创建 OrderedPair 类的两个实例化:

 

代码 new OrderedPair<String, Integer> 将 K 实例化为 String,将 V 实例化为 Integer。因此,OrderedPair 的构造函数的参数类型分别为字符串和整数。由于自动装箱,将字符串和 int 传递给类是有效的。

如 The Diamond 中所述,由于 Java 编译器可以从声明 OrderedPair<String, Integer> 推断出 K 和 V 类型,因此可以使用菱形表示法缩短这些语句:

 

若要创建泛型接口,请遵循与创建泛型类相同的约定。

7、参数化类型

还可以将类型参数(即 K 或 V)替换为参数化类型(即 List<String>)。例如,使用 OrderedPair<K, V> 示例:

 

四、泛型方法

泛型方法是引入自己的类型参数的方法。这类似于声明泛型类型,但类型参数的作用域仅限于声明它的方法。允许使用静态和非静态泛型方法以及泛型类构造函数。

泛型方法的语法包括一个类型参数列表,位于尖括号内,显示在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。

Util类包括一个通用方法compare,用于比较两个Pair对象:

 

调用此方法的完整语法为:

 

类型已显式提供,如粗体所示。通常,这可以省略,编译器将推断所需的类型:

 

此功能称为类型推断,允许您将泛型方法作为普通方法调用,而无需在尖括号之间指定类型

五、有界类型参数

有时可能需要限制可以用作参数化类型中的类型参数的类型。例如,一个对数字进行操作的方法可能只想接受Number或其子类的实例。这就是有界类型参数的作用。

要声明有界类型参数,请列出类型参数的名称,后跟extends关键字,然后是其上界,在本例中为Number。请注意,在本文中,extends在一般意义上用于表示“扩展”(如在类中)或“实现”(如接口中)。

 

除了限制可用于实例化泛型类型的类型外,有界类型参数还允许您调用在边界中定义的方法:

 

isEven方法通过n调用Integer类中定义的intValue方法。

多个边界

前面的示例说明了使用具有单个边界的类型参数,但类型参数可以具有多个边界:

 

具有多个边界的类型变量是边界中列出的所有类型的子类型。如果其中一个边界是类,则必须首先指定它。例如:

 

如果未首先指定绑定A,则会出现编译时错误:

 

六、泛型、继承和子类型

正如您已经知道的,只要类型兼容,就可以将一种类型的对象分配给另一种类型。例如,您可以将Integer指定给Object,因为Object是Integer的超类型之一:

 

java泛型基础教程在面向对象的术语中,这被称为“是一种”关系。由于Integer是Object的一种,因此可以进行赋值。但Integer也是一种数字,因此以下代码也是有效的:

 

泛型也是如此。您可以执行泛型类型调用,将Number作为其类型参数传递,如果该参数与Number兼容,则将允许任何后续的add调用:

 

现在考虑以下方法:

 

它接受什么类型的论点?通过查看其签名,您可以看到它接受一个类型为Box<Number>的参数。但这意味着什么?您是否可以像您预期的那样,在Box<Integer>或Box<Double>中传递?答案是“否”,因为Box<Integer>和Box<Double>不是Box<Number>的子类型。

当涉及到使用泛型编程时,这是一个常见的误解,但这是需要学习的一个重要概念。

泛型类和子类型

您可以通过扩展或实现泛型类或接口来对其进行子类型划分。一个类或接口的类型参数与另一个类的类型参数之间的关系由extends和implements子句决定。

以Collections类为例,ArrayList<E>实现了List<E〉,List<E<扩展了Collection<E>。所以ArrayList<String>是List<String<的一个子类型,它是Collection<String>的子类型。只要不改变类型参数,类型之间的子类型关系就会保留下来。

现在想象一下,我们想要定义自己的列表接口PayloadList,它将泛型类型P的可选值与每个元素相关联。它的声明可能看起来像:

 

PayloadList的以下参数化是List<String>的子类型:

  • PayloadList<String,String>
  • PayloadList<String,Integer>
  • PayloadList<String,Exception>

七、类型推断

类型推断是Java编译器查看每个方法调用和相应声明的能力,以确定使调用适用的类型参数。推理算法确定参数的类型,如果可用,还确定分配或返回结果的类型。最后,推理算法试图找到适用于所有参数的最具体的类型。

为了说明最后一点,在以下示例中,推断确定传递给pick方法的第二个参数的类型为Serializable:

 

类型推断和泛型方法

泛型方法向您介绍了类型推理,它使您能够像调用普通方法一样调用泛型方法,而无需在尖括号之间指定类型。考虑以下示例BoxDemo,它需要Box类:

 

以下是此示例的输出:

 

泛型方法addBox定义了一个名为U的类型参数。通常,Java编译器可以推断泛型方法调用的类型参数。因此,在大多数情况下,您不必指定它们。例如,要调用泛型方法addBox,可以指定具有类型见证的类型参数,如下所示:

 

或者,如果省略类型见证,Java编译器会自动推断(从方法的参数)类型参数是Integer:

 

泛型类的类型推断和实例化

只要编译器可以从上下文中推断类型参数,就可以用一组空的类型parameters (<>)替换调用泛型类的构造函数所需的类型参数。这对尖括号被非正式地称为diamond。

例如,考虑以下变量声明:

 

您可以将构造函数的参数化类型替换为一组空的类型parameters(<>):

 

请注意,要在泛型类实例化期间利用类型推断,必须使用菱形。在以下示例中,编译器生成未检查的转换警告,因为HashMap()构造函数引用的是HashMap原始类型,而不是Map<String,List<String>>类型:

 

类型推理与泛型类和非泛型类的泛型构造函数

请注意,构造函数在泛型类和非泛型类中都可以是泛型的(换句话说,声明它们自己的形式类型参数)。考虑以下示例:

 

考虑MyClass类的以下实例化:

 

此语句创建参数化类型MyClass<Integer>的实例;该语句为泛型类MyClass<X>的形式类型参数X显式指定Integer类型。请注意,该泛型类的构造函数包含一个形式类型参数T。编译器为该泛型类构造函数的形式类型参数T推断类型String(因为该构造函数的实际参数是String对象)。

Java SE 7之前版本的编译器能够推断泛型构造函数的实际类型参数,类似于泛型方法。然而,如果使用菱形(<>),Java SE 7及更高版本中的编译器可以推断出正在实例化的泛型类的实际类型参数。考虑以下示例:

 

在本例中,编译器推断泛型类MyClass<X>的形式类型参数X的类型Integer。它为这个泛型类的构造函数的形式类型参数T推断类型String。

目标类型

Java编译器利用目标类型来推断泛型方法调用的类型参数。表达式的目标类型是Java编译器所期望的数据类型,具体取决于表达式的出现位置。考虑Collections.emptyList方法,该方法声明如下:

 

请考虑以下赋值语句:

 

此语句需要List<String>的一个实例;此数据类型是目标类型。由于方法emptyList返回List<T>类型的值,因此编译器推断类型参数T必须是值String。这在Java SE 7和8中都有效。或者,您可以使用类型见证并指定T的值,如下所示:

 

然而,在这种情况下,这是不必要的。不过,在其他情况下,这是必要的。考虑以下方法:

 

假设您想用一个空列表调用方法processStringList。在Java SE 7中,以下语句不会编译:

 

Java SE 7编译器生成类似于以下内容的错误消息:

 

编译器需要类型参数T的值,因此它以值Object开头。因此,Collections.emptyList的调用返回类型为List<Object>的值,该值与方法processStringList不兼容。因此,在Java SE 7中,必须指定类型参数的值,如下所示:

 

在Java SE 8中不再需要这样做。目标类型的概念已经扩展到包括方法参数,例如方法processStringList的参数。在这种情况下,processStringList需要List<String>类型的参数。方法Collections.emptyList返回值List<T>,因此使用目标类型List<String>,编译器推断类型参数T的值为String。因此,在Java SE 8中,编译以下语句:

 

八、通配符

在泛型代码中,被称为通配符的问号(?)表示未知类型。通配符可以用于各种情况:作为参数、字段或局部变量的类型;有时作为返回类型(尽管更具体一些是更好的编程实践)。通配符永远不会用作泛型方法调用、泛型类实例创建或超类型的类型参数。

以下部分将更详细地讨论通配符,包括上界通配符、下界通配符和通配符捕获。

1、上限通配符

可以使用上限通配符来放宽对变量的限制。例如,假设您想编写一个在List<Integer>、List<Double>和List<Number>上工作的方法;您可以通过使用上限通配符来实现这一点。

要声明一个上界通配符,请使用character (“?”),后跟extends关键字,再后跟其上界。请注意,在本文中,extends在一般意义上用于表示“扩展”(如在类中)或“实现”(如接口中)。

要编写适用于Number列表和Number子类型(如Integer、Double和Float)的方法,您需要指定List<?extends Number>。术语 List<Number>比List<?extends Number>限制性更强,因为前者只匹配Number类型的列表,而后者匹配Number类型或其任何子类的列表。

请考虑以下处理方法:

 

上限通配符<?extends Foo>,其中Foo是任何类型,与Foo和Foo的任何子类型匹配。进程方法可以访问列表元素,类型为 Foo:

 

在 foreach 子句中,elem 变量遍历列表中的每个元素。Foo 类中定义的任何方法现在都可以在 elem 上使用。

sumOfList 方法返回列表中数字的总和:

 

以下代码使用 Integer 对象列表打印 sum = 6.0:

 

双精度值列表可以使用相同的 sumOfList 方法。以下代码打印 sum = 7.0:

 

2、无限通配符

无界通配符类型是使用通配符(?)指定的,例如List<?>。这被称为未知类型的列表。有两种情况下,无界通配符是一种有用的方法:

如果您正在编写一个可以使用Object类中提供的功能实现的方法。

当代码在泛型类中使用不依赖于类型参数的方法时。例如,List.size或List.clear。实际上,Class<?>之所以经常使用,是因为类<T>中的大多数方法都不依赖于T。

考虑以下方法printList:

 

printList的目标是打印任何类型的列表,但它无法实现这一目标——它只打印对象实例的列表;它无法打印List<Integer>、List<String>、List>Double>等,因为它们不是List<Object>的子类型。要编写通用的printList方法,请使用List<?>:

 

因为对于任何具体类型A,List<A>都是List<?>的子类型,您可以使用printList打印任何类型的列表:

 

需要注意的是,List<Object>和List<?>不一样。您可以将Object或Object的任何子类型插入到List<Object>中。但您只能在列表<?>中插入null。通配符使用指南部分提供了有关如何确定在给定情况下应使用哪种通配符(如果有的话)的更多信息。

九、类型擦除

泛型被引入Java语言,以在编译时提供更严格的类型检查,并支持泛型编程。为了实现泛型,Java编译器将类型擦除应用于:

将泛型类型中的所有类型参数替换为其边界,如果类型参数是无边界的,则替换为Object。因此,生成的字节码只包含普通的类、接口和方法。

如有必要,请插入类型强制转换以保持类型安全。

生成桥接方法以保留扩展泛型类型中的多态性。

类型擦除确保不会为参数化类型创建新的类;因此,泛型不会产生运行时开销。

1、通用类型的擦除

在类型擦除过程中,如果类型参数是有界的,Java编译器将擦除所有类型参数,并将每个类型参数替换为其第一个绑定;如果类型参数为无界的,则替换为Object。

考虑以下泛型类,该类表示单链列表中的节点:

 

由于类型参数T是无界的,Java编译器将其替换为Object:

 

在以下示例中,泛型Node类使用有界类型参数:

 

Java编译器将有界类型参数T替换为第一个有界类Comparable:

 

2、通用方法的擦除

Java 编译器还会擦除泛型方法参数中的类型参数。请考虑以下通用方法:

 

因为 T 是无界的,所以 Java 编译器将其替换为 Object:

 

假设定义了以下类:

 

您可以编写一个泛型方法来绘制不同的形状:

 

Java 编译器将 T 替换为 Shape:

 

官网:泛型

版权声明


相关文章:

  • java基础入门 当当2024-10-24 17:10:03
  • JAVA语言基础王维虎2024-10-24 17:10:03
  • 没有java基础能学大数据吗2024-10-24 17:10:03
  • 北风java0基础教程2024-10-24 17:10:03
  • b站java基础推荐2024-10-24 17:10:03
  • 容器的使用基础JAVA2024-10-24 17:10:03
  • 动力节点的java基础2024-10-24 17:10:03
  • java基础程序例题2024-10-24 17:10:03
  • 招收java学徒0基础2024-10-24 17:10:03
  • java 基础练习答案2024-10-24 17:10:03