年后跑路第一战,从 Java 泛型学起!
概述
JDK 5.0 引入了 Java 泛型,允许设计者详细地描述变量和方法的类型要如何变化,使得代码具有更好的可读性。本文章是对 Java 中泛型的快速介绍,包含泛型背后的目标以及使用泛型如何提高我们代码的质量。
为什么要引入泛型?
在没有泛型的背景下,让我们想象一个场景,我们要在 Java 中创建一个 List 来存储 Integer。
代码如下:
果不其然,IDEA 会直接提醒需要强制转换
我们对代码进行修改,如下所示:
在没有泛型的前提下,定义的 List 可以保存任何对象,当我们遍历时候,根据上下文进行判断,只能保证它是一个 Object,所以需要我们显示转换。
我们知道 List 中的数据类型是 Integer,可以直接强制转换,如果我们不知道或者强制转换时候写错类型,就会导致报错,一场灾难就这样发生了。
这时候,就有人想了,我能不能在使用 List 时候就指定保存的类型,编译阶段来帮我保证类型的正确性,那就可以完全避免让人讨厌的强制转换,所以,泛型就因运而生了。
让我们修改前面代码片段的第一行:
通过添加包含类型的菱形运算符 <>,我们将 List 能保存的类型限制到只有 Integer 类型,编译器可以在编译时强制执行类型。
泛型方法
对于泛型方法,我们可以用不同类型的参数调用它们。编译器将确保我们使用的任何类型的正确性。
泛型方法属性:
泛型方法在方法声明的返回类型之前有一个类型参数(包含类型的菱形运算符)。
类型参数可以是有界的(我们将在本文后面解释边界)。
泛型方法可以在方法签名中具有用逗号分隔的不同类型参数。
泛型方法的方法体就像普通方法一样。
这是定义将数组转换为 List 的泛型方法的示例:
方法签名中的*<T>表明该方法将处理泛型类型 T*。即使该方法返回 void,这也是必需的。
如前所述,该方法可以处理多个泛型类型。在这种情况下,我们必须将所有泛型类型添加到方法签名中。
以下是我们如何修改上述方法以处理类型 T 和类型 G:
我们正在传递一个函数,该函数将具有 T 类型元素的数组转换为具有 G 类型元素的列表。
一个例子是将 Integer 转换为它的 String 表示:
请注意,Oracle 建议使用大写字母来表示泛型类型,并选择更具描述性的字母来表示正式类型。在 Java 集合中,我们使用 T 表示类型,K 表示键,V 表示值。
有界泛型
类型参数可以有界,我们可以限制方法接受的类型。例如,我们可以指定一个方法接受一个类型及其所有子类(上限)或一个类型及其所有超类(下限)。要声明上界类型,我们在类型后使用关键字 extends,要声明下界类型,我们在类型后使用关键字 super
例子:
我们在这里使用关键字 extends 表示类型 T 在类的情况下扩展上限或在接口的情况下实现上限。
多重边界
一个类型也可以有多个上限:
如果 T 扩展的类型之一是一个类(例如 Number),我们必须将它放在边界列表中的第一个。否则会导致编译时错误。
在泛型中使用通配符
在 Java 中,通配符由?表示,我们使用它们来指代未知类型。通配符对泛型特别有用,可以用作参数类型。
首先,**我们知道 Object 是所有 Java 类的超类型。但是,Object 的集合不是任何集合的超类型。所以,一个 List<Object>不是 List<String>**的超类型,二者直接没有任何关系
例子:
假如现在有一个 Building 的子类型,叫 House,我们不能将这个方法用于 House 的列表,即使 House 是 Building 的一个子类型。
如果我们需要将此方法与类型 Building 及其所有子类型一起使用,则有界通配符可以发挥作用:
现在此方法将适用于类型 Building 及其所有子类型。 这称为上限通配符,其中类型 Building 是上限。
我们还可以指定具有下限的通配符,其中未知类型必须是指定类型的超类型。 可以使用 super 关键字后跟特定类型来指定下限。 例如,<? super T> 表示未知类型,它是 T 的超类(= T 及其所有父类)。
类型擦除
Java 中添加了泛型以确保类型安全。 并且为了确保泛型不会在运行时造成开销,编译器在编译时对泛型应用了一个称为类型擦除的过程。
如果类型参数是无界的,则类型擦除会删除所有类型参数并用它们的边界或 Object 替换它们。 这样,编译后的字节码只包含正常的类、接口和方法,确保不会产生新的类型。 在编译时也将正确的转换应用于 Object 类型。
这是类型擦除的示例:
使用类型擦除,无界类型 T 被替换为 Object:
如果类型是有界的,则在编译时该类型将被边界替换:
编译后:
泛型和原始数据类型
Java 中泛型的一个限制是类型参数不能是基本类型。
例如,以下不能编译:
要理解基本类型为什么不起作用,让我们记住泛型是一个编译时特性,这意味着类型参数被删除并且所有泛型类型都实现为类型 Object。
我们来看 一个列表的 add 方法:
add 方法的签名是:
并将被编译为:
因此,类型参数必须可转换为 Object。由于基本类型不扩展 Object,我们不能将它们用作类型参数。
然而,Java 为原语提供了装箱类型,以及自动装箱和拆箱来解包它们:
所以,如果我们想创建一个可以容纳整数的列表,我们可以使用这个包装器:
编译后的代码将等效于以下内容:
结论
Java 泛型是对 Java 语言的强大补充,因为它使程序员的工作更轻松且不易出错。泛型在编译时强制类型正确,最重要的是,可以实现泛型算法而不会对我们的应用程序造成任何额外开销。
版权声明: 本文为 InfoQ 作者【麦洛】的原创文章。
原文链接:【http://xie.infoq.cn/article/145f6ff17e0f60e85ef4e2a52】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论