技术开发 频道

多角度看 Java 中的泛型


IT168技术文档】 
    带有泛型的类 

    为了帮助大家更好地理解 Java 语言中的泛型,我们在这里先来对比两段实现相同功能的 GJ 代码和 Java 代码。通过观察它们的不同点来对 Java 中的泛型有个总体的把握,首先来分析一下不带泛型的 Java 代码,程序如下:
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 }
    接口 Collection 提供了两个方法,即添加元素的方法 add(Object x),见第 2 行,以及返回该 Collection 的 Iterator 实例的方法 iterator(),见第 3 行。Iterator 接口也提供了两个方法,其一就是判断是否有下一个元素的方法 hasNext(),见第 8 行,另外就是返回下一个元素的方法 next(),见第 7 行。LinkedList 类是对接口 Collection 的实现,它是一个含有一系列节点的链表,节点中的数据类型是 Object,这样就可以创建任意类型的节点了,比如 Byte, String 等等。上面这段程序就是用没有泛型的传统的 Java 语言编写的代码。接下来我们分析一下传统的 Java 语言是如何使用这个类的。

代码如下:
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 来实现同样功能的程序。源程序如下:
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++ 中模板的表示习惯。这部分程序和上面程序的主要区别就是在 Collection, Iterator, 或 LinkedList 出现的地方均用 Collection<A>, Iterator<A>, 或 LinkedList<A> 来代替,当然,第 22 行对构造函数的声明除外。 

    下面再来分析一下在 GJ 中是如何对这个类进行操作的,程序如下:
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 代码进行类型检查,此种机制有利于尽早地发现并改正错误。 

    类型参数的作用域是定义这个类型参数的整个类,但是不包括静态成员函数。这是因为当访问同一个静态成员函数时,同一个类的不同实例可能有不同的类型参数,所以上述提到的那个作用域不应该包括这些静态函数,否则就会引起混乱。
0
相关文章