级别: 初级 周 晶 ([email protected]), 计算机硕士 2006 年 12 月 28 日 泛型是 Sun 公司发布的 JDK 5.0 中的一个重要特性,它的最大优点是提供了程序的类型安全同可以向后兼容。为了帮助读者更好地理解和使用泛型,本文通过一些示例从基本原理,重要概念,关键技术,以及相似技术比较等多个角度对 Java 语言中的泛型技术进行了介绍,重点强调了泛型中的一些基本但又不是很好理解的概 很多 Java 程序员都使用过集合(Collection),集合中元素的类型是多种多样的,例如,有些集合中的元素是 Byte 类型的,而有些则可能是 String 类型的,等等。Java 语言之所以支持这么多种类的集合,是因为它允许程序员构建一个元素类型为 Object 的 Collection,所以其中的元素可以是任何类型。 当使用 Collection 时,我们经常要做的一件事情就是要进行类型转换,当转换成所需的类型以后,再对它们进行处理。很明显,这种设计给编程人员带来了极大的不便,同时也容易引入错误。 在很多 Java 应用中,上述情况非常普遍,为了解决这个问题,使 Java 语言变得更加安全好用,近些年的一些编译器对 Java 语言进行了扩充,使 Java 语言支持了"泛型",特别是 Sun 公司发布的 JDK 5.0 更是将泛型作为其中一个重要的特性加以推广。 本文首先对泛型的基本概念和特点进行简单介绍,然后通过引入几个实例来讨论带有泛型的类,泛型中的子类型,以及范化方法和受限类型参数等重要概念。为了帮助读者更加深刻的理解并使用泛型,本文还介绍了泛型的转化,即,如何将带有泛型的 Java 程序转化成一般的没有泛型的 Java 程序。这样,读者对泛型的理解就不会仅仅局限在表面上了。考虑到多数读者仅仅是使用泛型,因此本文并未介绍泛型在编译器中的具体实现。Java 中的泛型和 C++ 中的模板表面上非常相似,但实际上二者还是有很大区别的,本文最后简单介绍了 Java 中的泛型与 C++ 模板的主要区别。 泛型本质上是提供类型的"类型参数",它们也被称为参数化类型(parameterized type)或参量多态(parametric polymorphism)。其实泛型思想并不是 Java 最先引入的,C++ 中的模板就是一个运用泛型的例子。 GJ(Generic Java)是对 Java 语言的一种扩展,是一种带有参数化类型的 Java 语言。用 GJ 编写的程序看起来和普通的 Java 程序基本相同,只不过多了一些参数化的类型同时少了一些类型转换。实际上,这些 GJ 程序也是首先被转化成一般的不带泛型的 Java 程序后再进行处理的,编译器自动完成了从 Generic Java 到普通 Java 的翻译。具体的转化过程大致分为以下几个部分:
转化后的程序和没有引入泛型时程序员不得不手工完成转换的程序是非常一致的,具体的转化过程会在后面介绍。GJ 保持了和 Java 语言以及 Java 虚拟机很好的兼容性,下面对 GJ 的特点做一个简要的总结。
以上是泛型的一些主要特点,下面通过几个相关的例子来对 Java 语言中的泛型进行说明。 为了帮助大家更好地理解 Java 语言中的泛型,我们在这里先来对比两段实现相同功能的 GJ 代码和 Java 代码。通过观察它们的不同点来对 Java 中的泛型有个总体的把握,首先来分析一下不带泛型的 Java 代码,程序如下:
1 interface Collection {
2 public void add (Object x); 3 public Iterator iterator (); 4 } 5 6 interface Iterator { 7 public Object next (); 8 public boolean hasNext (); 9 } 10 11 class NoSuchElementException extends RuntimeException {} 12 13 class LinkedList implements Collection { 14 15 protected class Node { 16 Object elt; 17 Node next = null; 18 Node (Object elt) { this.elt = elt; } 19 } 20 21 protected Node head = null, tail = null; 22 23 public LinkedList () {} 24 25 public void add (Object elt) { 26 if (head == null) { head = new Node(elt); tail = head; } 27 else { tail.next = new Node(elt); tail = tail.next; } 28 } 29 30 public Iterator iterator () { 31 32 return new Iterator () { 33 protected Node ptr = head; 34 public boolean hasNext () { return ptr != null; } 35 public Object next () { 36 if (ptr != null) { 37 Object elt = ptr.elt; ptr = ptr.next; return elt; 38 } else throw new NoSuchElementException (); 39 } 40 }; 41 } 42 } 接口 代码如下:
1 class Test {
2 public static void main (String[] args) { 3 // byte list 4 LinkedList xs = new LinkedList(); 5 xs.add(new Byte(0)); xs.add(new Byte(1)); 6 Byte x = (Byte)xs.iterator().next(); 7 // string list 8 LinkedList ys = new LinkedList(); 9 ys.add("zero"); ys.add("one"); 10 String y = (String)ys.iterator().next(); 11 // string list list 12 LinkedList zss = new LinkedList(); 13 zss.add(ys); 14 String z = (String)((LinkedList)zss.iterator().next()).iterator().next(); 15 // string list treated as byte list 16 Byte w = (Byte)ys.iterator().next(); // run-time exception 17 } 18 } 从上面的程序我们可以看出,当从一个链表中提取元素时需要进行类型转换,这些都要由程序员显式地完成。如果我们不小心从 String 类型的链表中试图提取一个 Byte 型的元素,见第 15 到第 16 行的代码,那么这将会抛出一个运行时的异常。请注意,上面这段程序可以顺利地经过编译,不会产生任何编译时的错误,因为编译器并不做类型检查,这种检查是在运行时进行的。不难发现,传统 Java 语言的这一缺陷推迟了发现程序中错误的时间,从软件工程的角度来看,这对软件的开发是非常不利的。接下来,我们讨论一下如何用 GJ 来实现同样功能的程序。源程序如下:
1 interface Collection<A> {
2 public void add(A x); 3 public Iterator<A> iterator(); 4 } 5 6 interface Iterator<A> { 7 public A next(); 8 public boolean hasNext(); 9 } 10 11 class NoSuchElementException extends RuntimeException {} 12 13 class LinkedList<A> implements Collection<A> { 14 protected class Node { 15 A elt; 16 Node next = null; 17 Node (A elt) { this.elt = elt; } 18 } 19 20 protected Node head = null, tail = null; 21 22 public LinkedList () {} 23 24 public void add (A elt) { 25 if (head == null) { head = new Node(elt); tail = head; } 26 else { tail.next = new Node(elt); tail = tail.next; } 27 } 28 29 public Iterator<A> iterator () { 30 return new Iterator<A> () { 31 protected Node ptr = head; 32 public boolean hasNext () { return ptr != null; } 33 public A next () { 34 if (ptr != null) { 35 A elt = ptr.elt; ptr = ptr.next; return elt; 36 } else throw new NoSuchElementException (); 37 } 38 }; 39 } 40 } 程序的功能并没有任何改变,只是在实现方式上使用了泛型技术。我们注意到上面程序的接口和类均带有一个类型参数 A,它被包含在一对尖括号(< >)中,见第 1,6 和 13 行,这种表示法遵循了 C++ 中模板的表示习惯。这部分程序和上面程序的主要区别就是在 下面再来分析一下在 GJ 中是如何对这个类进行操作的,程序如下:
1 class Test {
2 public static void main (String [] args) { 3 // byte list 4 LinkedList<Byte> xs = new LinkedList<Byte>(); 5 xs.add(new Byte(0)); xs.add(new Byte(1)); 6 Byte x = xs.iterator().next(); 7 // string list 8 LinkedList<String> ys = new LinkedList<String>(); 9 ys.add("zero"); ys.add("one"); 10 String y = ys.iterator().next(); 11 // string list list 12 LinkedList<LinkedList<String>>zss= newLinkedList<LinkedList<String>>(); 13 zss.add(ys); 14 String z = zss.iterator().next().iterator().next(); 15 // string list treated as byte list 16 Byte w = ys.iterator().next(); // compile-time error 17 } 18 } 在这里我们可以看到,有了泛型以后,程序员并不需要进行显式的类型转换,只要赋予一个参数化的类型即可,见第 4,8 和 12 行,这是非常方便的,同时也不会因为忘记进行类型转换而产生错误。另外需要注意的就是当试图从一个字符串类型的链表里提取出一个元素,然后将它赋值给一个 Byte 型的变量时,见第 16 行,编译器将会在编译时报出错误,而不是由虚拟机在运行时报错,这是因为编译器会在编译时刻对 GJ 代码进行类型检查,此种机制有利于尽早地发现并改正错误。 类型参数的作用域是定义这个类型参数的整个类,但是不包括静态成员函数。这是因为当访问同一个静态成员函数时,同一个类的不同实例可能有不同的类型参数,所以上述提到的那个作用域不应该包括这些静态函数,否则就会引起混乱。 在 Java 语言中,我们可以将某种类型的变量赋值给其父类型所对应的变量,例如,String 是 Object 的子类型,因此,我们可以将 String 类型的变量赋值给 Object 类型的变量,甚至可以将 String [ ] 类型的变量(数组)赋值给 Object [ ] 类型的变量,即 String [ ] 是 Object [ ] 的子类型。 上述情形恐怕已经深深地印在了广大读者的脑中,对于泛型来讲,上述情形有所变化,因此请广大读者务必引起注意。为了说明这种不同,我们还是先来分析一个小例子,代码如下所示:
1 List<String> ls = new ArrayList<String>();
2 List<Object> lo = ls; 3 lo.add(new Integer()); 4 String s = ls.get(0); 上述代码的第二行将 如果上述赋值是合理的,那么上面代码的第三行的操作将是可行的,因为 一般情况下,如果 A 是 B 的子类型,C 是某个泛型的声明,那么 在这一部分我们将讨论有关泛化方法(generic method )和受限类型参数(bounded type parameter)的内容,这是泛型中的两个重要概念,还是先来分析一下与此相关的代码。
1 interface Comparable<A> {
2 public int compareTo(A that); 3 } 4 5 class Byte implements Comparable<Byte> { 6 private byte value; 7 public Byte(byte value) {this.value = value;} 8 public byte byteValue() {return value;} 9 public int compareTo(Byte that) { 10 return this.value - that.value; 11 } 12 } 13 14 class Collections { 15 public static <A implements Comparable<A>> 16 A max (Collection<A> xs) { 17 Iterator<A> xi = xs.iterator(); 18 A w = xi.next(); 19 while (xi.hasNext()) { 20 A x = xi.next(); 21 if (w.compareTo(x) < 0) w = x; 22 } 23 return w; 24 } 25 } 这里定义了一个接口 第 14 行到第 25 行的代码定义了一个类 之所以说它是泛化了的方法,是因为这个方法可以应用到很多种类型上。当要将一个方法声明为泛化方法时,我们只需要在这个方法的返回类型(A)之前加上一个类型参数(A),并用尖括号(< >)将它括起来。这里的类型参数(A)是在方法被调用时自动实例化的。例如,假设对象 m 的类型是
除此之外,还需要注意的一点是,在某些情况下,擦除技术需要引入类型转换(cast),这些情况主要包括: 情况 1. 方法的返回类型是类型参数; 情况 2. 在访问数据域时,域的类型是一个类型参数。 例如在本文"带有泛型的类"一小节的最后,我们给出了一段测试程序,一个 Test 类。这个类包含以下几行代码:
8 LinkedList<String> ys = new LinkedList<String>();
9 ys.add("zero"); ys.add("one");
10 String y = ys.iterator().next();
这部分代码转换后就变成了如下的代码:
8 LinkedList ys = new LinkedList();
9 ys.add("zero"); ys.add("one");
10 String y = (String)ys.iterator().next();
第 10 行的代码进行了类型转换,这是因为在调用 上面介绍了泛型转化中的擦除技术,接下来,我们讨论一下泛型转化中的另外一个重要问题--桥方法(bridge method)。 Java 是一种面向对象的语言,因此覆盖(overridden)是其中的一项重要技术。覆盖能够正常"工作"的前提是方法名和方法的参数类型及个数完全匹配(参数的顺序也应一致),为了满足这项要求,编译器在泛型转化中引入了桥方法(bridge method)。接下来,我们通过一个例子来分析一下桥方法在泛型转化中所起的作用。在本文"泛化方法和受限类型参数"一小节所给出的代码中,第 9 行到第 11 行的程序如下所示:
public int compareTo(Byte that)
{ return this.value - that.value; }
这部分代码经过转化,就变成了下面的样子:
public int compareTo(Byte that)
{ return this.value - that.value; }
public int compareTo(Object that)
{ return this.compareTo((Byte)that); }
第 12 行的方法 根据面向对象的基本概念,我们知道,重载(overloading)允许桥方法和原来的方法共享同一个方法名,正如上面例子所显示的那样,因此桥方法的引入是完全合法的。一般情况下,当一个类实现了一个参数化的接口或是继承了一个参数化的类时,需要引入桥方法。 到此,我们对泛型中的子类型,带有泛型的类,泛化方法,受限类型参数以及泛型的转化进行了简要的介绍,下面部分将结合这些技术对前面提到的例子进行一下总结,以便能够帮助读者更深刻更全面地理解泛型。 首先来分析一下本文提到的那个
interface Collection
{
public void add (
Object x);
public Iterator iterator ();
}
interface Iterator
{
public
Object next ();
public boolean hasNext (); 9
}
class NoSuchElementException extends RuntimeException {}
class LinkedList implements Collection
{
protected class Node
{
Object elt;
Node next = null;
Node (
Object elt)
{ this.elt = elt; }
}
protected Node head = null, tail = null;
public LinkedList () {}
public void add (
Object elt)
{
if (head == null)
{
head = new Node(elt);
tail = head;
} else
{
tail.next = new Node(elt);
tail = tail.next;
}
}
public Iterator iterator ()
{
return new Iterator ()
{
protected Node ptr = head;
public boolean hasNext ()
{ return ptr != null; }
public
Object next ()
{
if (ptr != null)
{
Object elt = ptr.elt;
ptr = ptr.next;
return elt;
} else
{
throw new NoSuchElementException ();
}
}
};
}
}
通过分析上述代码,我们不难发现,所有参数化类型 Collection, Iterator 和 LinkedList 中的类型参数 "A" 全都被擦除了。另外,剩下的类型变量 "A" 都用其上限进行了替换,这里的上限是 Object,见黑体字标出的部分,这是转化的关键部分。 下面我们分析一下在介绍有关泛化方法(generic method)和受限类型参数(bounded type parameter)时举的那个例子,该段 GJ 代码经过转换后的等价 Java 程序如下所示:
interface Comparable
{
public int compareTo(
Object that);
}
class Byte implements Comparable
{
private byte value;
public Byte(byte value)
{this.value = value;}
public byte byteValue(){return value;}
public int compareTo(Byte that)
{ return this.value - that.value; }
public int compareTo(Object that)
{
return this.compareTo((Byte)that);
}
}
class Collections
{
public static
Comparable max(Collection xs)
{
Iterator xi = xs.iterator();
Comparable w = (Comparable)xi.next();
while (xi.hasNext())
{
Comparable x = (Comparable)xi.next();
if (w.compareTo(x) < 0) w = x;
}
return w;
}
}
同样请读者注意黑体字标出的部分,这些关键点我们在前面已经介绍过了,故不赘述。唯一需要注意的一点就是第 18,20,22 行出现的Comparable。在泛型转化中,类型变量应该用其上限来替换,一般情况下这些上限是 "Object",但是当遇到受限的类型参数时,这个上限就不再是 "Object" 了,编译器会用限制这些类型参数的类型来替换它,上述代码就用了对 A 进行限制的类型 "Comparable" 来替换 A。 桥方法的引入,为解决覆盖问题带来了方便,但是这种方法还存在一些问题,例如下面这段代码:
interface Iterator<A>
{
public boolean hasNext ();
public A next ();
}
class Interval implements Iterator<Integer>
{
private int i;
private int n;
public Interval (int l, int u)
{ i = l; n = u; }
public boolean hasNext ()
{ return (i <= n); }
public Integer next ()
{ return new Integer(i++); }
}
根据以上所讲的内容,这部分代码转换后的 Java 程序应该是如下这个样子:
interface Iterator {
public boolean hasNext ();
public Object next ();
}
class Interval implements Iterator
{
private int i;
private int n;
public Interval (int l, int u)
{ i = l; n = u; }
public boolean hasNext ()
{ return (i <= n); }
public Integer next%1% ()
{ return new Integer(i++); }
// bridge
public Object next%2%() { return next%1%(); }
}
相信有些读者已经发现了这里的问题,这不是一段合法的 Java 源程序,因为第 14 行和第 16 行的两个 next() 有相同的参数,无法加以区分。代码中的 %1% 和 %2% 是为了区分而人为加入的,并非 GJ 转化的结果。 不过,这并不是什么太大的问题,因为 Java 虚拟机可以区分这两个 next() 方法,也就是说,从 Java 源程序的角度来看,上述程序是不正确的,但是当编译成字节码时,JVM 可以对两个 next() 方法进行识别。这是因为,在 JVM 中,方法定义时所使用的方法签名包括方法的返回类型,这样一来,只要 GJ 编译出的字节码符合Java字节码的规范即可,这也正好说明了 GJ 和 JVM 中字节码规范要求的一致性! 最后,值得一提的是,JDK 5.0 除了在编译器层面对 Java 中的泛型进行了支持,Java 的类库为支持泛型也做了相应地调整,例如,集合框架中所有的标准集合接口都进行了泛型化,同时,集合接口的实现也都进行了相应地泛型化。 GJ 程序的语法在表面上与 C++ 中的模板非常类似,但是二者之间有着本质的区别。 首先,Java 语言中的泛型不能接受基本类型作为类型参数――它只能接受引用类型。这意味着可以定义 List<Integer>,但是不可以定义 List<int>。 其次,在 C++ 模板中,编译器使用提供的类型参数来扩充模板,因此,为 List<A> 生成的 C++ 代码不同于为 List<B> 生成的代码,List<A> 和 List<B> 实际上是两个不同的类。而 Java 中的泛型则以不同的方式实现,编译器仅仅对这些类型参数进行擦除和替换。类型 ArrayList<Integer> 和 ArrayList<String> 的对象共享相同的类,并且只存在一个 ArrayList 类。 本文通过一些示例从基本原理,重要概念,关键技术,以及相似技术比较等多个角度对 Java 语言中的泛型技术进行了介绍,希望这种介绍方法能够帮助读者更好地理解和使用泛型。本文主要针对广大的 Java 语言使用者,在介绍了泛型的基本概念后,重点介绍了比较底层的泛型转化技术,旨在帮助读者更加深刻地掌握泛型,笔者相信这部分内容可以使读者避免对泛型理解的表面化,也所谓知其然更知其所以然。
原文链接:http://www.ibm.com/developerworks/cn/java/j-lo-gj/index.html?S_TACT=105AGX52&S_CMP=techcsdn |