Java泛型快速教程

泛型是Java SE 5.0引入的一种Java功能,在其发布几年后,我发誓那里的每个Java程序员不仅听说过它,而且已经使用过它。 关于Java泛型,有很多免费和商业资源,而我使用的最佳资源是:

尽管有大量的信息,但在我看来,有时候许多开发人员仍然不了解Java泛型的含义和含义。 这就是为什么我试图以最简单的方式总结开发人员需要的有关泛型的基本信息。

泛型的动机

考虑Java泛型的最简单方法是考虑一种语法糖,它可能使您省去一些强制转换操作:

 List<Apple> box = ...;  Apple apple = box.get( 0 ); 

前面的代码是自说的:box是对Apple类型对象列表的引用。 get方法返回一个Apple实例,不需要强制转换。 没有泛型,此代码将是:

 List box = ...;  Apple apple = (Apple) box.get( 0 ); 

不用说,泛型的主要优点是让编译器跟踪类型参数,执行类型检查和强制转换操作:编译器保证强制转换永远不会失败。

编译器现在不再依赖程序员来跟踪对象类型并执行强制转换,这可能导致运行时失败,难以调试和解决,而现在可以帮助程序员执行更多类型检查并在编译时检测更多失败。

通用设施

泛型工具引入了类型变量的概念。 根据Java语言规范,类型变量是由以下项引入的不合格标识符:

  • 通用类声明
  • 通用接口声明
  • 通用方法声明
  • 通用构造函数声明。

通用类和接口

如果类或接口具有一个或多个类型变量,则它是通用的。 类型变量由尖括号分隔,并遵循类(或接口)的名称:

 public interface List<T> extends Collection<T> {  ...  } 

粗略地说,类型变量充当参数,并提供编译器进行检查所需的信息。

Java库中的许多类(例如整个Collections Framework)都被修改为通用的。 例如,我们在第一个代码段中使用的List接口现在是一个通用类。 在该代码段中,box是对List <Apple>对象的引用,该对象是使用一个类型变量:Apple实现List接口的类的实例。 类型变量是编译器在将get方法的结果自动转换为Apple引用时使用的参数。

实际上,新的通用签名或接口List的get方法是:

 T get( int index); 

方法get确实返回了一个T类型的对象,其中T是List <T>声明中指定的类型变量。

通用方法和构造函数

如果方法和构造函数声明一个或多个类型变量,它们的方式几乎相同。

 public static <t> T getFirst(List<T> list) 

此方法将接受对List <T>的引用,并将返回类型T的对象。

例子

您可以在自己的类或通用Java库类中利用通用类。

书写时输入安全性…

例如,在下面的代码片段中,我们创建了一个实例List <String>,其中填充了一些数据:

 List<String> str = new ArrayList<String>();  str.add( "Hello " );  str.add( "World." ); 

如果我们尝试将其他类型的对象放入List <String>,则编译器将引发错误:

 str.add( 1 ); // won't compile 

…以及阅读时

如果我们传递List <String>引用,则始终保证可以从中检索String对象:

 String myString = str.get( 0 ); 

反复进行

库中的许多类(例如Iterator <T>)已得到增强并变得通用。 接口List <T>的iterator()方法现在返回一个Iterator <T>,可以轻松使用它,而无需转换通过其T next()方法返回的对象。

 for (Iterator<String> iter = str.iterator(); iter.hasNext();) {  String s = iter.next();  System.out.print(s);  } 

使用foreach

for每种语法都利用了泛型。 先前的代码片段可以写成:

 for (String s: str) {  System.out.print(s);  } 

更容易阅读和维护。

自动装箱和自动拆箱

处理泛型时,将自动使用Java语言的自动装箱/自动拆箱功能,如以下代码片段所示:

 List<Integer> ints = new ArrayList<Integer>();  ints.add( 0 );  ints.add( 1 );  int sum = 0 ;  for ( int i : ints) {  sum += i;  } 

但是请注意,装箱和拆箱会降低性能,因此,通常会出现警告和警告。

亚型

与其他面向对象的类型化语言一样,在Java中,可以构建类型的层次结构:

Java泛型

在Java中,类型T的子类型可以是扩展T的类型,也可以是直接或间接实现T的类型(如果T是接口)。 由于“成为...的子类型”是传递关系,因此,如果类型A是B的子类型,而B是C的子类型,则A也将是C的子类型。 在上图中:

  • 富士苹果是苹果的子类型
  • 苹果是水果的一种
  • FujiApple是Fruit的子类型。

每个Java类型也将是Object的子类型。

类型B的每个子类型A都可以分配给类型B的引用:

 Apple a = ...;  Fruit f = a; 

通用类型的子类型化

如果可以将Apple实例的引用分配给Fruit的引用,如上所示,那么List <Apple>和List <Fruit>之间是什么关系? 哪一个是子类型? 更一般而言,如果类型A是类型B的子类型,则C <A>和C <B>如何相互关联?

出乎意料的是,答案是:绝对没有。 用更正式的词来说,泛型类型之间的子类型关系是不变的。

这意味着以下代码段无效:

 List<Apple> apples = ...;  List<Fruit> fruits = apples; 

以下内容也是如此:

 List<Apple> apples;  List<Fruit> fruits = ...;  apples = fruits; 

但为什么? 是一个苹果是一种水果,一盒苹果(一个清单)也是一盒水果。

从某种意义上讲是这样,但是类型(类)封装了状态和操作。 如果一盒苹果是一盒水果会怎样?

 List<Apple> apples = ...;  List<Fruit> fruits = apples;  fruits.add( new Strawberry()); 

如果是这样,我们可以在列表中添加Fruit的其他不同子类型,并且必须禁止这样做。

相反,更直观:一盒水果不是一盒苹果,因为它可能是其他种类(子类型)水果(水果)(例如草莓)的盒子(列表)。

真的有问题吗?

不应该这样。 Java开发人员感到惊讶的最强烈原因是数组的行为与泛型类型之间的不一致。 后者的子类型关系是不变的,而前者的子类型关系是协变的:如果类型A是类型B的子类型,则A []是B []的子类型:

 Apple[] apples = ...;  Fruit[] fruits = apples; 

可是等等! 如果我们重复上一节中公开的参数,最终可能会在一系列苹果中添加草莓:

 Apple[] apples = new Apple[ 1 ];  Fruit[] fruits = apples;  fruits[ 0 ] = new Strawberry(); 

该代码确实可以编译,但是在运行时会以ArrayStoreException的形式引发错误。 由于数组的这种行为,在存储操作期间,Java运行时需要检查类型是否兼容。 显然,该检查还会增加您应该意识到的性能损失。

同样,泛型更安全地使用,并且可以“纠正” Java数组的这种类型的安全性弱点。

在这种情况下,您现在想知道为什么数组的子类型关系是协变的,我将为您提供Java Generics和Collections给出的答案:如果它是不变的,则无法将引用传递给对象数组类型未知(无需每次都复制到Object [])的方法,例如:

 void sort(Object[] o); 

随着泛型的出现,数组的这种特性不再是必需的(我们将在本文的下一部分中看到),并且确实应该避免。

通配符

正如我们在本文前面的部分中所看到的,泛型类型的子类型关系是不变的。 不过,有时我们还是希望以与普通类型相同的方式使用通用类型:

  • 缩小参考(协方差)
  • 扩大参考(差异)

协方差

例如,假设我们有一组盒子,每个盒子都有不同种类的水果。 我们希望能够编写可以接受任何方法的方法。 更正式地说,给定类型B的子类型A,我们想找到一种方法来使用类型C <B>的引用(或方法参数),该引用可以接受C <A>的实例。

为了完成此任务,我们可以使用带有扩展名的通配符,例如以下示例:

 List<Apple> apples = new ArrayList<Apple>();  List<? extends Fruit> fruits = apples; 

? 扩展重新引入了泛型类型的协变子类型:Apple是Fruit的子类型,而List <Apple>是List <?的子类型。 延伸水果>。

逆差

现在让我们介绍另一个通配符: 超。 给定类型A的超类型B,则C <B>是C <?的子类型。 超级A>:

 List<Fruit> fruits = new ArrayList<Fruit>();  List<? super Apple> = fruits; 

如何使用通配符?

现在有足够的理论:我们如何利用这些新结构?

延伸

让我们回到第二部分中介绍Java数组协方差的示例:

 Apple[] apples = new Apple[ 1 ];  Fruit[] fruits = apples;  fruits[ 0 ] = new Strawberry(); 

如我们所见,当尝试通过对Fruit数组的引用将Strawberry添加到Apple数组时,此代码可以编译,但会导致运行时异常。

现在,我们可以使用通配符将此代码转换为与之对应的通用代码:由于Apple是Fruit的子类型,因此我们将使用? 扩展通配符,以便能够将List <Apple>的引用分配给List <?的引用 延伸水果>:

 List<Apple> apples = new ArrayList<Apple>();  List<? extends Fruit> fruits = apples;  fruits.add( new Strawberry()); 

这次,代码将无法编译! Java编译器现在阻止我们将草莓添加到水果列表中。 我们将在编译时检测到错误,甚至不需要进行任何运行时检查(例如在数组存储的情况下),以确保将兼容类型添加到列表中。 即使我们尝试将Fruit实例添加到列表中,代码也不会编译:

 fruits.add( new Fruit()); 

没门。 结果是,实际上,您不能将任何东西放入其类型使用?的结构中。 扩展通配符。

如果我们考虑一下,原因很简单: 扩展T通配符告诉编译器我们正在处理类型T的子类型,但是我们不知道是哪一个。 由于没有办法说出来,而且我们需要保证类型安全,因此不允许您在此类结构内放置任何内容。 另一方面,由于我们知道它可能是T的子类型,因此我们可以从结构中获取数据,并保证它是T实例:

 Fruit get = fruits.get( 0 ); 

使用类型的行为是什么? 超级通配符? 让我们从这个开始:

 List<Fruit> fruits = new ArrayList<Fruit>();  List<? super Apple> = fruits; 

我们知道水果是对Apple超类商品列表的引用。 同样,我们不知道它是哪个超类型,但是我们知道Apple及其任何子类型都将与其分配兼容。 确实,由于这种未知类型将同时是Apple和GreenApple超类型,因此我们可以这样写:

 fruits.add( new Apple());  fruits.add( new fruits.add( GreenApple()); 

如果我们尝试添加任何Apple超类型,编译器都会抱怨:

 fruits.add( new Fruit());  fruits.add( new Object()); 

由于我们不知道它是哪个超类型,因此不允许添加任何实例。

如何从这种类型的数据中获取数据呢? 事实证明,您唯一可以摆脱的是对象实例:由于我们无法知道它是哪个超类型,因此编译器只能保证它将是对对象的引用,因为对象是任何对象的超类型。 Java类型。

获取和放置原则或PECS规则

总结一下行为? 延伸和? 超级通配符,我们得出以下结论:

  • 使用 ? 如果需要从数据结构中检索对象,则扩展通配符
  • 使用 ? 如果需要将对象放入数据结构,则使用超级通配符
  • 如果您需要同时做这两个事情,请不要使用任何通配符。

这就是Maurice Naftalin在他的Java泛型和集合中称为“获取和放置原理”,在Joshua Bloch的“ 有效Java ”中称为PECS规则。

Bloch的助记符PECS来自“ Producer Extends,Consumer Super”,可能更容易记住和使用。

方法签名中的通配符

如本系列第二部分中所见,在Java中(与许多其他类型化语言一样),Substitution原则是:可以将子类型分配给其任何超类型的引用。

这适用于分配任何引用的过程,即,即使将参数传递给函数或存储其结果也是如此。 因此,该原理的优点之一是,在定义类层次结构时,可以编写“通用”方法来处理整个子层次结构,而与要处理特定对象实例的类无关。 到目前为止,在Fruit类的层次结构中,接受Fruit作为参数的函数将接受其任何子类型(例如Apple或Strawberry)。

从上一篇文章中可以看出,通配符可还原泛型类型的协变量和逆变量子类型:然后,使用通配符,使开发人员编写可以利用到目前为止所展示的优点的函数。

例如,如果开发人员想要定义一个方法eat,该方法接受任何水果的列表,则可以使用以下签名:

 void eat(List<? extends Fruit> fruits); 

由于水果类的任何子类型的列表都是List <? 扩展Fruit>,先前的方法将接受任何此类列表作为参数。 请注意,如上一节所述,“获取和放置原则”(或PECS规则)将允许您从此类列表中检索对象并将其分配给Fruit引用。

另一方面,如果要将实例放在作为参数传递的列表上,则应使用?。 超级通配符:

 void store(List<? super Fruit> container); 

这样,可以将任何水果超类的列表传递给存储功能,并且可以安全地将任何水果子类型放入其中。

有界类型变量

但是,泛型的灵活性比这更大。 类型变量可以有界,几乎与通配符可以有界(就像我们在第二部分中看到的一样)。 但是,类型变量不能以super为边界,而只能以extends为边界。 查看以下签名:

 public static <T extends I<T>> void name(Collection<T> t); 

它接受类型受限制的对象的集合:它必须满足T扩展I <T>条件。 起初,使用有界类型变量似乎并不比通配符更强大,但是稍后我们将详细介绍这些差异。

让我们假设层次结构中的一些(但不是全部)结果可能是多汁的,如下所示:

 public interface Juicy<T> { 
     Juice<T> squeeze();  } 

多汁的水果将实现此接口并发布挤压方法。

现在,您编写一个使用一堆水果并将其全部榨干的库方法。 您可以写的第一个签名可能是:

 <T> List<Juice<T>> squeeze(List<Juicy<T>> fruits); 

使用有界类型变量,您将编写以下内容(实际上,它与以前的方法具有相同的擦除作用):

 <T extends Juicy<T>> List<Juice<T>> squeeze(List<T> fruits); 

到目前为止,一切都很好。 但有限。 我们可以使用相同帖子中使用的相同参数,然后发现squeeze方法不起作用,例如,在以下情况下使用红色橘子列表:

 class Orange extends Fruit implements Juicy<Orange>;  RedOrange class extends Orange; 

由于我们已经了解了PECS原理,因此我们将通过以下方式更改方法:

 <T extends Juicy<? super T>> List<Juice<? super T>> squeezeSuperExtends(List<? extends T> fruits); 

此方法接受类型扩展为Juicy <?的对象列表。 super T>,换句话说,必须存在类型S,使得T扩展Juicy <S> S superT。

递归界

也许您想放松T延伸多汁<? 超级T>绑定。 这种绑定称为递归绑定,因为类型T必须满足的绑定取决于T。您可以在需要时使用递归绑定,也可以将它们与其他种类的绑定进行混合匹配。

因此,例如,您可以编写具有以下界限的通用方法:

 <A extends B<A,C>, C extends D<T>> 

请记住,这些示例仅用于说明泛型可以做什么。 您将要使用的界限始终取决于要放入类型层次结构中的约束。

使用多个类型变量

假设您想放宽在最后一个squeeze方法上设置的递归范围。 然后,让我们假设类型T可以扩展Juicy <S>,尽管T本身不扩展S。方法签名可以是:

 <T extends Juicy<S>, S> List<Juice<S>> squeezeSuperExtendsWithFruit(List<? extends T> fruits); 

此签名与上一个签名相当(因为我们仅在方法参数中使用T),但有一个小优点:由于我们声明了通用类型S,因此该方法可以返回List <Juice <S>而不是List <? 超级T>,在某些情况下很有用,因为编译器将根据您传递的方法参数帮助您确定S类型是哪种。 由于要返回列表,因此您可能希望调用者能够从中获取某些信息,并且如上一部分所述,您只能从列表中获取Object实例,例如List <?。 超级T>。

如果需要,显然可以为S添加更多界限,例如:

 <T extends Juicy<S>, S extends Fruit> List<Juice<S>> squeezeSuperExtendsWithFruit(List<? extends T> fruits); 

多界

如果要对同一类型变量应用多个范围怎么办? 事实证明,您只能为每个泛型类型变量编写一个绑定。 因此,以下界限是非法的:

 <T extends A, T extends B> // illegal 

编译器将失败,并显示以下消息:

T已在…中定义

必须使用不同的语法来表示多个界限,这是一个非常熟悉的表示法:

 <T extends A & B> 

先前的边界意味着T扩展 A和B。请注意,根据Java语言规范 第4.4章的规定,边界是:

  • 类型变量。
  • 一类。
  • 接口类型,然后是其他接口类型。

这意味着只能使用接口类型来表达多个界限。 无法在多重绑定中使用类型变量,并且编译器将失败,并显示以下消息:

类型变量不能后面跟随其他界限。

在我阅读的文档中,这并不总是很清楚。

参考文献:

编码愉快! 不要忘记分享!

拜伦

相关文章:


翻译自: https://www.javacodegeeks.com/2011/04/java-generics-quick-tutorial.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值