首页风格
门户
博客

Java泛类型(Generics)快速入门

日期:2011-9-20  编译:Gbin1.com

泛类型(Generics)是Java SE5.0里的特性,在发布后的几年里,我相信每个java开发者都不仅仅听说过,而且实实在在的使用过。这里有很多的免费和付费资源可以用来学些泛类,如下:

尽管这里有很多的信息,但是很多程序员还是弄不清楚java泛类的使用。这就是为什么我这里总结了所以使用泛类所需要的基本知识。

泛类使用的初衷

最简单的方式来想象泛类是一种语法方式用来省去类型转换操作:

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

以上代码很好解释,box是一个Apple对象的列表,我们使用get方法取出一个苹果的实例而不需要做强制类型转换。如果我们不使用泛类特性,那么我们将需要这样编码:

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

毋庸置疑,泛类的主要优势是让编译器来跟踪参数类型,执行类型校验并且转换类型:编译器保证了转换的正确性。

替代程序员的人为类型跟踪和转换,编译器会负责帮助开发人员来强化类型检查及其编译时的错误验证,这样能够保证程序尽量少的出现运行环境错误和debug的成本。

泛类语法

泛类语法介绍了变量类型的概念。一个变量类型,根据Java的标准,是一个无限制条件的标示符并且由以下方式定义:

  • 泛类类声明
  • 泛类接口声明
  • 泛类方法声明
  • 泛类构造器声明

泛类型类和接口

一个类或者接口如何有一个或者多个类型变量那么他们就是泛类型类或者接口。类型变量在类名之后并且由尖括号分隔:

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

简单来说,类型变量作为一个参数提供信息给编译器用来执行校验。

许多的Java类库里的类,例如Collections,都被修改成为泛类型。我们用来演示的代码中的List接口就是一个泛类型。在那个代码片段中box是List<Apple>的参考,是一个使用类型变量Apple执行List接口的实例。类型变量是编译器用来调用get方法在执行自动类型转换的类型定义。

实际上,新的泛类型签名或者List接口的get方法是:

T get(int index);

这个方法返回一个真正的T类型对象,T是类型变量指定在List<T>声明中。

泛类型方法和构造器

非常类似,如果我们声明一个或者多个类型变量的话,方法和构造器都可以被泛类型化。

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

这个方法将会接受一个参考到List<T>并且返回T类型的对象。

例子

你可以在自己类或者Java一般类库中使用泛类型。

1. 数据写操作中的类型安全...

以下代码片段中, 例如,我们创建了一个List<String>实例,并且注入数据,如下:

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

如果我们尝试添加其它类型的对象到List<String>,我们会看到编译器报错:

str.add(1);

2. 数据读取中...

如果我们传递List<String>参考,我们会一直保证得到的是一个字符串类型的对象:

String myString = str.get(0);

3.迭代

许多类库中的类,例如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);
}

4.使用foreach

for each语法充分利用了泛类型。以上代码片段可以书写成如下:

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


5. Autoboxing/Auto-Unboxing

当我们使用泛类型时,autoboxing/auto-unboxing特性会自动支持,我们不需要手动将Integer转为int了,如下:

List<Integer> ints = new ArrayList<Integer>();
ints.add(0);
ints.add(1);

int sum = 0;
for (int i : ints) {
sum += i;
}

子类型

和其它的面向对象的变成语言类似,类型的层次也可以构造:

在Java中,类型T的子类型可以是T的扩展也可以是类型的执行(如果类型是接口的话)。因为子类型是一个传递关系,如果A类型是B的子类型,B是C的子类型,那么A也将会是C的子类型。在上面图片中:

  • FujiApple是Apple的子类型
  • Apple是Fruit的子类型
  • FujiApple也同样是Fruit的子类型

因此每一个B的子类型A可以被赋予类型B的参考:

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

泛类型类型的子类型

如果一个Apple实例的参考可以被赋予一个Fruit的参考,那么List<Apple>和List<Fruit>的关系呢?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());

如果上面成立,那么可以随便添加子类型了。

这究竟是不是是个问题?

实际应该不是个问题。对于Java开发人员来说最大的不理解的原因是数组和泛类型的行为不一致。然而后者的子类型关系是协变的(Covariant):如果一个类型A是类型B的一个子类型。那么A[]就是B[]的子类型。

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

但是注意,如果我们在前面部分中重复变量的展现,我们可能得结果是将Strawberries(草莓)添加到一个Apple的数据中:

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

代码可以被编译,但是运行环境时会报错ArrayStoreException。因为数组的行为,在保存的操作时,Java运行环境需要检查类型和兼容性。这个校验,很自然的,添加了一个性能的损失,这个需要大家注意。

再次,泛类型是更安全的使用方式并且能够修正Java数组类型安全的弱点。

对于这里例子你现在可能想知道为什么对于数组来说子类型关系是协变的(Covariant),我将使用Java Generics and Collections来给你答案。如果它是不变的,那么将没有方法去传送一个参考到一个不知类型的数组对象到一个如下方法:

void sort(Object[] o);

随着泛类型的出现,这个参数规格的数据将不再需要并且最好避免使用。

通配符

正如我们在前部分看到的,泛类的子类型关系是不变的。然后,有时候我们想和使用一般类型一样使用泛类型:

  • 缩窄参考范围(covariance)
  • 扩展参考范围(contravariance)

 

covariance

我们假设,例如,我有一套盒子,每一个有不同的水果(fruit)。我们想能写一个方法来接受任何一个盒子。更正式的来说,对于一个类型B的子类型A来说,我们想找到一个方法来使用参考(或者一个方法参数)类型C<B>,能够接受C<A>的实例。

为了完成这个任务我们需要使用通配符来扩展。如下:

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

?为泛类扩展引入了协变的子类型:Apple是一个Fruit的子类型并且List<Apple>是List<? extends Fruit>的子类型。

Contravariance

现在我们介绍另外一个通配符:? super。如果这里有一个类型A的父类型B,那么C<B>就是子类型C<? super A>:

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

通配符为什么能够被使用?

理论够多的了:我们如何利用这些新的构建器?

?extends.

我们看看前面第二部分介绍数组协变时使用的例子:

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

正如我们看到的,当我们尝试通过Fruit数组参考来添加一个Strawberry到一个Apple的数组代码编译产生了运行时的异常。

现在我们能使用通配符来翻译代码到类似的泛类。因为Apple是一个Fruit的子类型,我们使用?extends通配符就可以赋予一个List<Apple>的参考到一个List<? extends Fruit>参考中:

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

这次,代码编译都无法通过!Java编译器现在阻止我们添加一个Stawberry到Fruits的list中。 我们将在编译时发现这个错误,甚至都不需要再运行环境中做监测来保证我们添加的是一个兼容类型。即使我们添加一个Fruit的实例到列表里:

fruits.add(new Fruit());

没戏。看起来你无法添加任何东西到一个机构体中,一旦这个结构体使用? extends通配符。

原因很简单,如果我们想到? extends T通配符告诉编译器我们将处理一个T类型的子类型,但是我们不知道那一个。因为这里没有办法说明,我们需要保证类型安全,这样你就不允许添加任何东西到这一个结构体中。另一方面,因为我们知道无论它是那个类型,它都是T的subtype,只要保证它是T实例的话我们就能够将数据从结构中读出。

Fruit get = fruits.get(0);

? Super

那么使用? super 通配符的类型又如何呢?我们使用如下代码:

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

我们知道fruits是一个一系列定义的Apple类型的父类型参考。同时我们无法知道哪一个父类型,但是我们知道Apple和任何他自己的子类型将被兼容赋值。实际上,因为这样一个无知的类型将是Apple和GreenApple的父类,我们可以这样写:

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

如果我们添加任何Apple的父类,编译器会无法通过:

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

因为我们不能够知道父类型是那个,我们不允许去添加任何实例。

那么如何从这个类型中取出数据呢?看来我们唯一能够取出的就是Object实例:因为我们不能够知道它的父类是那个,编译器只能保证它是一个Object的参考,因为Object是任何Java类型的父类。

总结? extends和? super通配符,我们得出如下结论:

  • 如果你需要从数据结构中取出对象你可以使用? extends通配符
  • 如果你需要从数据结构中存放对象你可以使用? super通配符
  • 如果你都需要做,那么你不要使用任何通配符

这就是Maurice Naftalin在他的Java Generics and Collection里称之为存取原则的概念。同时也是Joshua Bloch在Effective Java中称之为PECS原则大的概念。

via javacodegeeks  

原文来自: GBin1推荐教程之Java泛类型(Generics)快速入门

喜欢我们的文章请您与朋友分享:

?ü?à

评论

友情提示: 本站博客不再支持访客留言,如果有问题或者留言,请发布到  GBtags.com


  1. SiriBen

    此评论在等待批准中

    2013-2-28 下午2:27
今日推荐