站内搜索: 请输入搜索关键词

当前页面: 开发资料首页Java 专题J2SE 5.0中的泛型

J2SE 5.0中的泛型

摘要: J2SE 5.0中的泛型
内容:
J2SE 5.0中的泛型

作者:Budi Kurniawan

翻译:RR00

email:di_feng_ro@hotmail.com


版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明
英文原文地址:
http://www.onjava.com/pub/a/onjava/2005/07/06/generics.html
中文地址:
http://www.matrix.org.cn/resource/article/43/43634_java_generics.html
关键词: java generics java5

摘要
泛型是J2SE 5.0最重要的特性。他们让你写一个type(类或接口)和创建一个实例通过传递一个或多个引用类型。这个实例受限于只能作用于这些类型。比如,在java 5,java.util.List 已经被泛化。当建立一个list对象时,你通过传递一个java类型建立一个List实例,此list实例只能作用于所传递的类型。这意味着如果你传递一个String ,此List实例只能拥有String对象;如果你传递一个Integer,此实例只能存贮Integer对象。除了创建参数化的类型,你还能创建参数化的函数。
泛型的第一个好处是编译时的严格类型检查。这是集合框架最重要的特点。此外,泛型消除了绝大多数的类型转换。在JDK 5.0之前,当你使用集合框架时,你不得不进行类型转换。
本文将教你如何操作泛型。它的第一部分是“没有泛型的日子”,先让我们回忆老版本JDK的不便。然后,举一些泛型的例子。在讨论完语法以及有界泛型的使用之后,文章最后一章将解释如何写泛型。


没有泛型的日子
所有的java类都源自java.lang.Object,这意味着所有的JAVA对象能转换成Object。因此,在之前的JDK的版本中,很多集合框架的函数接受一个Object参数。所以,collections是一个能持有任何对象的多用途工具,但带来了不良的后果。

举个简单的例子,在JDK 5.0的之前版本中,类List的函数add接受一个Object参数:

public boolean add(java.lang.Object element)


所以你能传递任何类型给add。这是故意这么设计的。否则,它只能传递某种特定的对象,这样就会出现各种List类型,如,StringList, EmployeeList, AddressList等。
add通过Object传递能带来好处,现在我们考虑get函数(返回List中的一个元素).如下是JDK 5之前版本的定义:

public java.lang.Object get(int index) throws IndexOutOfBoundsException


get返回一个Object.不幸的事情从此开始了.假如你储存了两个String对象在一个List中:

List stringList1 = new ArrayList();
stringList1.add("Java 5");
stringList1.add("with generics");


当你想从stringList1取得一个元素时,你得到了一个Object.为了操作原来的类型元素,你不得不把它转换为String。

String s1 = (String) stringList1.get(0);


但是,假如你曾经把一个非String对象加入stringList1中,上面的代码会抛出一个ClassCastException. 有了泛型,你能创建一个单一用途的List实例.比如,你能创建一个只接受String对象的List实例,另外一个实例只能接受Employee对象.这同样适用于集合框架中的其他类型.


泛型入门

像一个函数能接受参数一样,一个泛型也能接受参数.这就是一个泛型经常被称为一个参数化类型的原因.但是不像函数用()传递参数,泛型是用<>传递参数的.声明一个泛型和声明一个普通类没有什么区别,只不过你把泛型的变量放在<>中.
比如,在JDK 5中,你可以这样声明一个java.util.List : List myList;
E 称为类型变量.意味着一个变量将被一个类型替代.替代类型变量的值将被当作参数或返回类型.对于List接口来说,当一个实例被创建以后,E 将被当作一个add或别的函数的参数.E 也会使get或别的参数的返回值.下面是add和get的定义:

boolean add
E get(int index)


NOTE:一个泛型在声明或例示时允许你传递特定的类型变量: E.除此之外,如果E是个类,你可以传递子类;如果E是个接口,你可以传递实现接口的类;

-----------------------------译者添加--------------------
List numberList= new ArrayList();
numberList.add(2.0);
numberList.add(2);
-----------------------------译者添加--------------------

如果你传递一个String给一个List,比如:

List myList;


那么mylist的add函数将接受一个String作为他的参数,而get函数将返回一个String.因为返回了一个特定的类型,所以不用类型转化了。

NOTE:根据惯例,我们使用一个唯一的大写字目表示一个类型变量。为了创建一个泛型,你需在声明时传递同样的参数列表。比如,你要想创建一个ArrayList来操作String ,你必须把String放在<>中。如:

List myList = new ArrayList();


再比如,java.util.Map 是这么定义的:

public interface Map


K用来声明map键(KEY)的类型而V用来表示值(VALUE)的类型。put和values是这么定义的:

V put(K key, V value)
Collection values()


NOTE:一个泛型不准直接的或间接的是java.lang.Throwable的子类。因为异常是在运行时抛出的,所以它不可能预言什么类型的异常将在编译时抛出.

列表1的例子将比较List在JDK 1.4 和JDK1.5的不同

package com.brainysoftware.jdk5.app16;
import java.util.List;
import java.util.ArrayList;

public class GenericListTest {
public static void main(String[] args) {
// in JDK 1.4
List stringList1 = new ArrayList();
stringList1.add("Java 1.0 - 5.0");
stringList1.add("without generics");
// cast to java.lang.String
String s1 = (String) stringList1.get(0);
System.out.println(s1.toUpperCase());

// now with generics in JDK 5
List stringList2 = new ArrayList();
stringList2.add("Java 5.0");
stringList2.add("with generics");
// no need for type casting
String s2 = stringList2.get(0);
System.out.println(s2.toUpperCase());
}
}


在列表1中,stringList2是个泛型。声明List告诉编译器List的实例能接受一个String对象。当然,在另外的情况中,你能新建能接受各种对象的List实例。注意,当从List实例中返回成员元素时,不需要对象转化,因为他返回的了你想要的类型,也就是String.

NOTE:泛型的类型检查(type checking)是在编译时完成的.

最让人感兴趣的事情是,一个泛型是个类型并且能被当作一个类型变量。比如,你想你的List储存lists of Strings,你能通过把List作为他的类型变量来声明List。比如:

List> myListOfListsOfStrings;


要从myList中的第一个List重新取得String,你可以这么用:

String s = myListOfListsOfStrings.get(0).get(0);


下一个列表中的ListOfListsTest类示范了一个List(命名为listOfLists)接受一个String List作为参数。

package com.brainysoftware.jdk5.app16;
import java.util.ArrayList;
import java.util.List;
public class ListOfListsTest {
public static void main(String[] args) {
List listOfStrings = new ArrayList();
listOfStrings.add("Hello again");
List> listOfLists = new ArrayList>();
listOfLists.add(listOfStrings);
String s = listOfLists.get(0).get(0);
System.out.println(s); // prints "Hello again"
}
}


另外,一个泛型接受一个或多个类型变量。比如,java.util.Map有两个类型变量s。第一个定义了键(key)的类型,第二个定义了值(value)的类型。下面的例子讲教我们如何使用个一个泛型Map.

package com.brainysoftware.jdk5.app16;
import java.util.HashMap;
import java.util.Map;
public class MapTest {
public static void main(String[] args) {
Map map = new HashMap();
map.put("key1", "value1");
map.put("key2", "value2");
String value1 = map.get("key1");
}
}


在这个例子中,重新得到一个key1代表的String值,我们不需要任何类型转换。

没有参数的情况下使用泛型

既然在J2SE 5.0中收集类型已经泛型化,那么,原来的使用这些类型的代码将如何呢?很幸运,他们在JAVA 5中将继续工作,因为你能使用没有参数的泛型。比如,你能继续像原来一样使用List接口,正如下面的例子一样。

List stringList1 = new ArrayList();
stringList1.add("Java 1.0 - 5.0");
stringList1.add("without generics");
String s1 = (String) stringList1.get(0);


一个没有任何参数的泛型被称为原型(raw type)。它意味着这些为JDK1.4或更早的版本而写的代码将继续在java 5中工作。

尽管如此,一个需要注意的事情是,JDK5编译器希望你使用带参数的泛型。否则,编译器将提示警告,因为他认为你可能忘了定义类型变量s。比如,编译上面的代码的时候你会看到下面这些警告,因为第一个List被认为是原型。

Note: com/brainysoftware/jdk5/app16/GenericListTest.java
uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

当你使用原型时,如果你不想看到这些警告,你有几个选择来达到目的:
1.编译时带上参数-source 1.4
2.使用@SupressWarnings("unchecked")注释
3.更新你的代码,使用List. List的实例能接受任何类型的对象,就像是一个原型List。然而,编译器不会报错。

使用 ? 通配符
前面提过,如果你声明了一个List, 那么这个List对aType起作用,所以你能储存下面这些类型的对象:
1.一个aType的实例
2.它的子类的实例(如果aType是个类)
3.实现aType接口的类实例(如果aType是个接口)
但是,请注意,一个泛型本身是个JAVA类型,就像java.lang.String或java.io.File一样。传递不同的类型变量给泛型可以创建不同的JAVA类型。比如,下面例子中list1和list2引用了不同的类型对象。

List list1 = new ArrayList();
List list2 = new ArrayList();

list1指向了一个类型变量s为java.lang.Objects 的List而list2指向了一个类型变量s为String 的List。所以传递一个List给一个参数为List的函数将导致compile time错误。下面列表可以说明:
package com.brainysoftware.jdk5.app16;
import java.util.ArrayList;
import java.util.List;

public class AllowedTypeTest {
public static void doIt(List l) {
}
public static void main(String[] args) {
List myList = new ArrayList();
// 这里将产生一个错误
doIt(myList);
}
}
上面的代码无法编译,因为你试图传递一个错误的类型给函数doIt。doIt的参数是List二你传递的参数是List
可以使用 ? 通配符解决这个难题。List<?> 意味着一个对任何对象起作用的List。所以,doIt可以改为:

public static void doIt(List<?> l) {}


在某些情况下你会考虑使用 ? 通配符。比如,你有一个printList函数,这个函数打印一个List的所有成员,你想让这个函数对任何类型的List起作用时。否则,你只能累死累活的写很多printList的重载函数。下面的列表引用了使用 ? 通配符的printList函数。
package com.brainysoftware.jdk5.app16;
import java.util.ArrayList;
import java.util.List;

public class WildCardTest {

public static void printList(List<?> list) {
for (Object element : list) {
System.out.println(element);
}
}
public static void main(String[] args) {
List list1 = new ArrayList();
list1.add("Hello");
list1.add("World");
printList(list1);

List list2 = new ArrayList();
list2.add(100);
list2.add(200);
printList(list2);
}
}


这些代码说明了在printList函数中,List<?>表示各种类型的List对象。然而,请注意,在声明的时候使用 ? 通配符是不合法的,像这样:

List<?> myList = new ArrayList<?>(); // 不合法


如果你想创建一个接收任何类型对象的List,你可以使用Object作为类型变量,就像这样:

List myList = new ArrayList();

在函数中使用界限通配符
在之前的章节中,你学会了通过传递不同的类型变量s来创建不同JAVA类型的泛型,但并不考虑类型变量s之间的继承关系。在很多情况下,你想一个函数有不同的List参数。比如,你有一个函数getAverage,他返回了一个List中成员的平均值。然而,如果你把List作为getAverage的参数,你就没法传递List 或List参数,因为List和List 和List不是同样的类型。

你能使用原型或使用通配符,但这样无法在编译时进行安全类型检查,因为你能传递任何类型的List,比如List的实例。你可以使用List作为参数,但是你就只能传递List给函数。但这样就使你的函数功能减少,因为你可能更多的时候要操作List或List,而不是List

J2SE5.0增加了一个规则来解决了这种约束,这个规则就是允许你定义一个上界(upper bound) 类型变量.在这种方式中,你能传递一个类型或它的子类。在上面getAverage函数的例子中,你能传递一个List或它的子类的实例,比如List or List

使用上界规则的语法这么定义的:GenericType<? extends upperBoundType>. 比如,对getAverage函数的参数,你可以这么写List<? extends Number>. 下面例子说明了如何使用这种规则。
package com.brainysoftware.jdk5.app16;
import java.util.ArrayList;
import java.util.List;
public class BoundedWildcardTest {
public static double getAverage(List<? extends Number> numberList)
{
double total = 0.0;
for (Number number : numberList)
total += number.doubleValue();
return total/numberList.size();
}

public static void main(String[] args) {
List integerList = new ArrayList();
integerList.add(3);
integerList.add(30);
integerList.add(300);
System.out.println(getAverage(integerList)); // 111.0
List doubleList = new ArrayList();
doubleList.add(3.0);
doubleList.add(33.0);
System.out.println(getAverage(doubleList)); // 18.0
}
}

由于有了上界规则,上面例子中的getAverage函数允许你传递一个List 或一个类型变量是任何java.lang.Number子类的List。

下界规则
关键字extends定义了一个类型变量的上界。通过使用super关键字,我们可以定义一个类型变量的下界,尽管使用的情况不多。比如,如果一个函数的参数是List<? super Integer>,那么意味着你可以传递一个List的实例或者任何java.lang.Integer的超类(superclass)。

创建泛型

前面的章节主要说明了如何使使用泛型,特别是集合框架中的类。现在我们开始学习如何写自己的泛型。

基本上,除了声明一些你想要使用的类型变量s外,一个泛型和别的类没有什么区别。这些类型变量s位于类型后面的<>中。比如,下面的Point就是个泛型。一个Point对象代表了一个系统中的点,它有横坐标和纵坐标。通过使Point泛型化,你能定义一个点实例的精确程度。比如,如果一个Point对象需要非常精确,你就把Double作为类型变量。否则,Integer 就够了。
package com.brainysoftware.jdk5.app16;
public class Point {
T x;
T y;
public Point(T x, T y) {
this.x = x;
this.y = y;
}
public T getX() {
return x;
}
public T getY() {
return y;
}
public void setX(T x) {
this.x = x;
}
public void setY(T y) {
this.y = y;
}
}


在这个例子中,T是Point的类型变量 。T是getX和getY的返回值类型,也是setX和setY的参数类型。此外,构造函数结合两个T参数。
使用point类就像使用别的类一样。比如,下面的例子创建了两个Point对象:ponint1和point2。前者把Integer作为类型变量,而后者把Double作为类型变量。

Point point1 = new Point(4, 2);
point1.setX(7);
Point point2 = new Point(1.3, 2.6);
point2.setX(109.91);


总结
泛型使代码在编译时有了更严格的类型检查。特别是在集合框架中,泛型有两个作用。第一,他们增加了对集合类型在编译时的类型检查,所以集合类所能持有的类型对传递给它的参数类型起了限制作用。比如你创建了一个持有strings的java.util.List实例,那么他就将不能接受Integers或别的类型。其次,当你从一个集合中取得一个元素时,泛型消除了类型转换的必要。
泛型能够在没有类型变量的情况下使用,比如,作为原型。这些措施让Java 5之前的代码能够运行在JRE 5中。但是,对新的应用程序,你最好不要使用原型,因为以后Java可能不支持他们。

你已经知道通过传递不同类型的类型变量给泛型可以产生不同的JAVA类型。就是说List和List的类型是不同的。尽管String是java.lang.Object。但是传递一个List给一个参数是List的函数会参数会产生编译错误(compile error)。函数能用 ? 通配符使其接受任何类型的参数。List<?> 意味着任何类型的对象。
最后,你已经看到了写一个泛型和别的一般JAVA类没有什么区别。你只需要在类型名称后面的<>中声明一系列的类型变量s就行了。这些类型变量s就是返回值类型或者参数类型。根据惯例,一个类型变量用一个大写字母表示。


Budi Kurniawan是一个高级 J2EE 架构师和作家。
Java, java, J2SE, j2se, J2EE, j2ee, J2ME, j2me, ejb, ejb3, JBOSS, jboss, spring, hibernate, jdo, struts, webwork, ajax, AJAX, mysql, MySQL, Oracle, Weblogic, Websphere, scjp, scjd
J2SE 5.0中的泛型

作者:Budi Kurniawan

翻译:RR00

email:di_feng_ro@hotmail.com


版权声明:可以任意转载,转载时请务必以超链接?
↑返回目录
前一篇: Java FTP libraries 比较
后一篇: Java中的泛型 第一部分