泛型编程

来源:互联网 发布:真实的鲁迅很变态知乎 编辑:程序博客网 时间:2024/05/05 22:22

更多参见: http://dev.yesky.com/javagaoji/fanxing/

JDK1.5 令我们期待很久,可是当他发布的时候却更换版本号为5.0。这说明Java已经有大幅度的变化。本文将讲解JDK5.0支持的新功能-----Java的泛型.

  1、Java泛型

  其实Java的泛型就是创建一个用类型作为参数的类。就象我们写类的方法一样,方法是这样的method(String str1,String str2 ),方法中参数str1、str2的值是可变的。而泛型也是一样的,这样写class Java_Generics<K,V>,这里边的K和V就象方法中的参数str1和str2,也是可变。下面看看例子:

//code list 1
import Java.util.Hashtable;
class TestGen0<K,V>{
 public Hashtable<K,V> h=new Hashtable<K,V>();
 public void put(K k, V v) {
  h.put(k,v);
 }
 public V get(K k) {
  return h.get(k);
 }
 public static void main(String args[]){
  TestGen0<String,String> t=new TestGen0<String,String>();
  t.put("key", "value");
  String s=t.get("key");
  System.out.println(s);
 }
}


  正确输出:value

  这只是个例子(Java中集合框架都泛型化了,这里费了2遍事.),不过看看是不是创建一个用类型作为参数的类,参数是K,V,传入的“值”是String类型。这个类他没有特定的待处理型别,以前我们定义好了一个类,在输入输入参数有所固定,是什么型别的有要求,但是现在编写程序,完全可以不制定参数的类型,具体用的时候来确定,增加了程序的通用性,像是一个模板。

  呵呵,类似C++的模板(类似)。

  1.1. 泛型通配符

  下面我们先看看这些程序:

//Code list 2
void TestGen0Medthod1(List l) {
 for (Object o : l)
  System.out.println(o);
}


  看看这个方法有没有异议,这个方法会通过编译的,假如你传入String,就是这样List<String>。

  接着我们调用它,问题就出现了,我们将一个List<String>当作List传给了方法,JVM会给我们一个警告,说这个破坏了类型安全,因为从List中返回的都是Object类型的,而让我们再看看下面的方法。

//Code list 3
void TestGen0Medthod1(List<String> l) {
 for (Object o : l)
  System.out.println(o);
}


  因为这里的List<String>不是List<Object>的子类,不是String与Object的关系,就是说List<String>不隶属于list<Object>,他们不是继承关系,所以是不行的,这里的extends是表示限制的。

  类型通配符是很神奇的,List<?>这个你能为他做什么呢?怎么都是“?”,它似乎不确定,他总不能返回一个?作为类型的数据吧,是啊他是不会返回一个“?”来问程序员的?JVM会做简单的思考的,看看代码吧,更直观些。

//code list 4
List<String> l1 = new ArrayList<String>();
li.add(“String”);
List<?> l2 = l1;
System.out.println(l1.get(0));


  这段代码没问题的,l1.get(0)将返回一个Object。

  1.2. 编写泛型类要注意:

  1) 在定义一个泛型类的时候,在 “<>”之间定义形式类型参数,例如:“class TestGen<K,V>”,其中“K” , “V”不代表值,而是表示类型。

  2) 实例化泛型对象的时候,一定要在类名后面指定类型参数的值(类型),一共要有两次书写。例如:

TestGen<String,String> t=new TestGen<String,String>();

  3) 泛型中<K extends Object>,extends并不代表继承,它是类型范围限制。
JDK1.5 令我们期待很久,可是当他发布的时候却更换版本号为5.0。这说明Java已经有大幅度的变化。本文将讲解JDK5.0支持的新功能-----Java的泛型.

  1、Java泛型

  其实Java的泛型就是创建一个用类型作为参数的类。就象我们写类的方法一样,方法是这样的method(String str1,String str2 ),方法中参数str1、str2的值是可变的。而泛型也是一样的,这样写class Java_Generics<K,V>,这里边的K和V就象方法中的参数str1和str2,也是可变。下面看看例子:

//code list 1
import Java.util.Hashtable;
class TestGen0<K,V>{
 public Hashtable<K,V> h=new Hashtable<K,V>();
 public void put(K k, V v) {
  h.put(k,v);
 }
 public V get(K k) {
  return h.get(k);
 }
 public static void main(String args[]){
  TestGen0<String,String> t=new TestGen0<String,String>();
  t.put("key", "value");
  String s=t.get("key");
  System.out.println(s);
 }
}


  正确输出:value

  这只是个例子(Java中集合框架都泛型化了,这里费了2遍事.),不过看看是不是创建一个用类型作为参数的类,参数是K,V,传入的“值”是String类型。这个类他没有特定的待处理型别,以前我们定义好了一个类,在输入输入参数有所固定,是什么型别的有要求,但是现在编写程序,完全可以不制定参数的类型,具体用的时候来确定,增加了程序的通用性,像是一个模板。

  呵呵,类似C++的模板(类似)。

  1.1. 泛型通配符

  下面我们先看看这些程序:

//Code list 2
void TestGen0Medthod1(List l) {
 for (Object o : l)
  System.out.println(o);
}


  看看这个方法有没有异议,这个方法会通过编译的,假如你传入String,就是这样List<String>。

  接着我们调用它,问题就出现了,我们将一个List<String>当作List传给了方法,JVM会给我们一个警告,说这个破坏了类型安全,因为从List中返回的都是Object类型的,而让我们再看看下面的方法。

//Code list 3
void TestGen0Medthod1(List<String> l) {
 for (Object o : l)
  System.out.println(o);
}


  因为这里的List<String>不是List<Object>的子类,不是String与Object的关系,就是说List<String>不隶属于list<Object>,他们不是继承关系,所以是不行的,这里的extends是表示限制的。

  类型通配符是很神奇的,List<?>这个你能为他做什么呢?怎么都是“?”,它似乎不确定,他总不能返回一个?作为类型的数据吧,是啊他是不会返回一个“?”来问程序员的?JVM会做简单的思考的,看看代码吧,更直观些。

//code list 4
List<String> l1 = new ArrayList<String>();
li.add(“String”);
List<?> l2 = l1;
System.out.println(l1.get(0));


  这段代码没问题的,l1.get(0)将返回一个Object。

  1.2. 编写泛型类要注意:

  1) 在定义一个泛型类的时候,在 “<>”之间定义形式类型参数,例如:“class TestGen<K,V>”,其中“K” , “V”不代表值,而是表示类型。

  2) 实例化泛型对象的时候,一定要在类名后面指定类型参数的值(类型),一共要有两次书写。例如:

TestGen<String,String> t=new TestGen<String,String>();

  3) 泛型中<K extends Object>,extends并不代表继承,它是类型范围限制。
2、泛型与数据类型转换

  2.1. 消除类型转换

  上面的例子大家看到什么了,数据类型转换的代码不见了。在以前我们经常要书写以下代码,如:

//code list 5
import Java.util.Hashtable;
class Test {
 public static void main(String[] args) {
  Hashtable h = new Hashtable();
  h.put("key", "value");
  String s = (String)h.get("key");
  System.out.println(s);
 }
}


  这个我们做了类型转换,是不是感觉很烦的,并且强制类型转换会带来潜在的危险,系统可能会抛一个ClassCastException异常信息。在JDK5.0中我们完全可以这么做,如:

//code list 6
import Java.util.Hashtable;
class Test {
 public static void main(String[] args) {
  Hashtable<String,Integer> h = new Hashtable<String,Integer> ();
  h.put("key", new Integer(123));
  int s = h.get("key").intValue();
  System.out.println(s);
 }
}


  这里我们使用泛化版本的HashMap,这样就不用我们来编写类型转换的代码了,类型转换的过程交给编译器来处理,是不是很方便,而且很安全。上面是String映射到String,也可以将Integer映射为String,只要写成HashTable<Integer,String> h=new HashTable<Integer,String>();h.get(new Integer(0))返回value。果然很方便。

  2.2 自动解包装与自动包装的功能

  从上面有没有看到有点别扭啊,h.get(new Integer(123))这里的new Integer(123);好烦的,在JDK5.0之前我们只能忍着了,现在这种问题已经解决了,请看下面这个方法。我们传入一个int这一基本型别,然后再将i的值直接添加到List中,其实List是不能储存基本型别的,List中应该存储对象,这里编译器将int包装成Integer,然后添加到List中去。接着我们用List.get(0);来检索数据,并返回对象再将对象解包装成int。恩,JDK5.0给我们带来更多方便与安全。

//Code list 7
public void autoBoxingUnboxing(int i) {
 ArrayList<Integer> L= new ArrayList<Integer>();
 L.add(i);
 int a = L.get(0);
 System.out.println("The value of i is " + a);
}


  2.3 限制泛型中类型参数的范围

  也许你已经发现在code list 1中的TestGen<K,V>这个泛型类,其中K,V可以是任意的型别。也许你有时候呢想限定一下K和V当然范围,怎么做呢?看看如下的代码:

//Code list 8
class TestGen2<K extents String,V extends Number>
{
 private V v=null;
 private K k=null;
 public void setV(V v){
  this.v=v;
 }
 public V getV(){
  return this.v;
 }
 public void setK(K k){
  this.k=k;
 }
 public V getK(){
  return this.k;
 }
 public static void main(String[] args)
 {
  TestGen2<String,Integer> t2=new TestGen2<String,Integer>();
  t2.setK(new String("String"));
  t2.setV(new Integer(123));
  System.out.println(t2.getK());
  System.out.println(t2.getV());
 }
}


  上边K的范围是<=String ,V的范围是<=Number,注意是“<=”,对于K可以是String的,V当然也可以是Number,也可以是Integer,Float,Double,Byte等。看看下图也许能直观些请看上图A是上图类中的基类,A1,A2分别是A的子类,A2有2个子类分别是A2_1,A2_2。

  然后我们定义一个受限的泛型类class MyGen<E extends A2>,这个泛型的范围就是上图中兰色部分。

  这个是单一的限制,你也可以对型别多重限制,如下:

class C<T extends Comparable<? super T> & Serializable>


  我们来分析以下这句,T extends Comparable这个是对上限的限制,Comparable< super T>这个是下限的限制,Serializable是第2个上限。一个指定的类型参数可以具有一个或多个上限。具有多重限制的类型参数可以用于访问它的每个限制的方法和域。

  2.4. 多态方法

//Code list 9
class TestGen {
 <T extends Object> public static List<T> make(T first) {
  return new List<T>(first);
 }
}



用J2SE 5.0创建定制的泛型集合

J2SE 5.0引入了许多新型的集合API-你需要了解它们以便能够正确地实现泛型定制的集合-它可以无缝地与多种类型和新型的"for each"结构一起工作。本文将向你展示示怎样创建与J2SE最新特征相兼容的集合。

  
一、 创建支持泛型的类

  首先,你必须学习如何创建一个允许存在"泛型类型"的类。这意味着无论何时实例化你的类,你都能够指定一个或多个Java类型与该类相关联。为了说明这个问题,请考虑列表1中的一个简单示例类。

  注意,列表1中的类是如何声明的。它在尖括号之间指定三个泛型。这些泛型是真实类型的占位符。当你声明一个这种类型的类时,你可以指定一个类来代替
ONE,TWO和THREE。如果你不这样做,那么该类将使用Object的默认类型。

  这个类显示出怎样设计一个类来接收三个泛型类型。当你创建一个这种类型的类时你要支持准确的类型。

  列表1.泛型类:

 

package com.heatonresearch.examples.collections;
public class Example<ONE, TWO, THREE> {
 private ONE one;
 private TWO two;
 private THREE three;
 public ONE getOne() { return one; }
 public void setOne(ONE one) { this.one = one; }
 public THREE getThree() { return three; }
 public void setThree(THREE three) { this.three = three; }
 public TWO getTwo() { return two; }
 public void setTwo(TWO two) { this.two = two; }
 public static void main(String args[]) {
  Example<
Double, Integer, String> example = new
  Example<Double, Integer, String>();
  example.setOne(1.5);
  example.setTwo(2);
  example.setThree("Three");
 }
}


  下面是如何实例化一个Example类型的类的情形:

 

Example example=new Example();


  前面的代码将代替具体的Double,Integer和String类型-相当于在列表1中的"ONE"、"TWO"和"THREE"占位符。你可以看到这些变量都有这些类型,通过下面三行设置它们的值。

 

example.setOne(1.5);
example.setTwo(2);
example.setThree("Three");


  现在,既然你已经知道如何创建一个使用泛型的定制类,那么创建一个使用泛型的定制集合类则更为简单些。

  二、 创建一个Queue类

  一个队列是一个很有用的
数据结构。为了理解一个队列的功能,你可以想像在一个娱乐公园人们排队骑马的情形。人们从队的后面进入到队中。为此,他们等待而最后到达队伍的前端。其顺序不能改变。

  这种情形可以被应用到一个队列类上去。它共有两个方法,分别是"push"和"
pop"。你使用push方法来把对象放置到队列中,而使用pop方法从队列中删除一项。例如,如果你使用push方法把三个对象添加到队列上,那么连续调用pop三次将以同样顺序从队列中删除这三个元素。这正与娱乐公园的情形相一致。如果有三个人以一特定的顺序进入队中,他们将以相同的顺序得到骑马娱乐。

  下列代码显示出怎么实现一个使用泛型的Java队列。

 

package com.heatonresearch.examples.collections;
import java.util.*;
public class Queue {
private ArrayList list = new ArrayList();
public void push(T obj) { list.add(obj); }
public T pop() throws QueueException {
if (size() == 0)
throw new QueueException(
"Tried to pop something from the queue, " +
"
when it was empty");
T result = list.get(0);
list.remove(0);
return result;
}
public boolean isEmpty() { return list.isEmpty(); }
public int size() { return list.size(); }
public void clear() { list.clear(); }
}


  前面的代码声明了队列类,这样它可以接收一个泛型类型。

 

public class Queue


  泛型类型"T"是该类类型-它将被放入到该队列中去。为了把这些项存储到一个队列中,该类还要创建一个接收"T"类型的ArrayList。
push方法很简单的。它接收单一的类型为泛型"T"的对象,并且把它添加到ArrayList上。

  pop方法稍微复杂些。首先,如果你要从队列中弹出一个对象,并且如果在队列中没有对象,那么该类将抛出一个QueueException类型的异常。下面是QueueException类。

 

package com.heatonresearch.examples.collections;
public class QueueException extends Exception {
 public QueueException(String msg) {
  super(msg);
 }
}


  下面是抛出QueueException类型异常的代码:

 

if (size() == 0)
throw new QueueException("Tried to pop something from the queue, " +
"when it was empty");


  如果队列不空,该方法将从队列中检索最后一个元素,在一个名叫result的变量中存储它,然后从该列表中删除这个项。下面几行代码实现了这一功能:

 

T result = list.get(0);
list.remove(0);
return result;


  注意,该临时变量也是泛型类型"T"。当这个类与真实的代表泛型类型的Java类型一起使用时,为了实现最大程度上的兼容性,无论你何时存取这些变量,确保总是使用泛型类型是非常重要的。

三、 测试Queue类

  下列类用于测试"泛型"队列。

package com.heatonresearch.examples.collections;
 public class TestQueue {
  public static void main(String args[]) {
   Queue<Integer> queue = new Queue<Integer>();
   queue.push(1);
   queue.push(2);
   queue.push(3);
   try {
    System.out.println("Pop 1:" + queue.pop());
    System.out.println("Pop 2:" + queue.pop());
    System.out.println("Pop 3:" + queue.pop());
   }
   catch (QueueException e) { e.printStackTrace(); }
  }
 }


  前面的代码中创建的队列仅接收整型对象。

Queue<Integer> queue = new Queue<Integer>();


  接下来的测试把三个整数添加到该队列上。

queue.push(1);
queue.push(2);
queue.push(3);


  注意,添加到该队列中的这些数字都是原始的类型。因为J2SE的自动装箱特性,这些原始的int类型被自动地转变成Integer对象。

  接下来,该测试使用pop方法检索对象。在该队列为空的情况下,该测试捕获到QueueException异常。从队列中弹出三个数字的结果是:

1
2
3


  尽管在这里作为一接收的整数队列显示,但是因为泛型,所以队列类对于任何Java对象情况都能正常工作。

  四、 创建一个可预知的Stack集合


  这里是一个更复杂的集合类型-它实现了一个堆栈以使你在实际删除一个对象之前能够预知或"可偷看"。你可以或者通过使用一个迭代算子或使用J2SE 5.0的新的"for each"结构语句来进行预知。

  这个PeekableStack类是一个先进后出(FILO)栈-让你遍历当前栈中的内容。它的实现使用了两个类。首先,PeekableStack类实现实际的栈部分。其次,PeekableStackIterator类实现一个"Java标准的"Iterator类-你可以用它来遍历整个栈。列表2(见所附源代码文件)显示出PeekableStack类的具体编码。

  注意,列表2中的PeekableStack类实现了Iterable接口。这对于支持新型的J2SE 5.0"for-each"结构语句是必要的。该Iterable接口用于指定你的集合支持"iterator"方法-它返回一个迭代算子。如果没有这个接口,你的类将无法与新型的"for-each"结构语句相兼容。

  这个可预知的栈包含push和pop方法,就象队列一样。该push方法仅仅是比队列稍微复杂些。而push方法负责把对象添加到栈上去并增加版本数(version)。

  这个version变量允许PeekableStackIterator类保证没有修改操作发生。在迭代算子创建时,这个算子保留一份当前版本数。如果栈上通过调用push方法发生任何变化,那么这个版本数就不会匹配;此不匹配将导致算子抛出一个ConcurrentModificationException异常。

  pop方法稍微复杂些。首先,它必须决定在该列表中的最后一个元素,这是通过获得列表的大小并且减去1而得到的。

int last = list.size() - 1;


  如果这个结果是一个小于零的数字,那么该栈就是空的,因此pop方法就返回null。

if (last < 0) return null;


  如果在栈中存在最后一个元素,那么就从列表中检索它。在从列表中成功地检索这个项后,你可以把它删除。

T result = list.get(last);
list.remove(last);


  最后,返回从列表中检索的对象。

return result;


  为支持"for each"迭代,PeekableStack类的iterator方法返回一个"Java标准的"Iterator类-你可以用它来遍历包含在栈中的所有对象。iterator方法创建一个新的iterator并且返回之。

PeekableStackIterator peekableStackIterator=new
PeekableStackIterator(this, list);


  如你所见,该iterator类接收当前栈和栈的项目列表作为构造器参数。这些值将为PeekableStackIterator所用-下一节将讨论之。

  五、 创建一个可预知的Stack迭代算子

  如果PeekableStack类将要同Java中新的"for each"结构语句一起使用,那么你必须创建一个"Java标准的"Iterator。列表3显示出一个PeekableStackIterator类的实现。

  在列表3中,迭代子实际上并没有以任何方式改变栈的值;代之的是,该迭代子追踪它在元素列表中的当前位置并且总是返回下一个元素。因为这个信息被存储在iteration类本身,所以有可能存在多个算子运行于相同的栈上。

  下列程序用于测试可预知的栈。

package com.heatonresearch.examples.collections;
 import java.util.*;
 public class TestPeekableStack {
  public static void main(String args[]) {
   PeekableStack<Integer> stack = new
   PeekableStack<Integer>();
   stack.push(1);
   stack.push(2);
   stack.push(3);
   for (int i : stack) { System.out.println(i); }
   System.out.println("Pop 1:" + stack.pop());
   System.out.println("Pop 2:" + stack.pop());
   System.out.println("Pop 3:" + stack.pop());
  }
}


  如你所见,有三个项被添加到栈上去。然后,这三个项被使用新的"for each"结构语句显示出来。

for( int i: stack)
{
 System.out.println( i );
}


  因此,你看到怎样成功地实现一集合-它支持新型的J2SE惯例-既有泛型也有"for each"结构语句。如你所见,创建与J2SE 5.0中新型的结构相兼容的集合是相当容易的-这只需要利用泛型并且实现恰当的接口即可。你会发现这样的集合类被无缝地集成到J2SE 5.0中。

Java 5.0泛型编程之泛型类型

 Java5.0的新特性之一是引入了泛型类型和泛型方法。一个泛型类型通过使用一个或多个类型变量来定义,并拥有一个或多个使用一个类型变量作为一个参数或者返回值的占位符。例如,类型java.util.List<E>是一个泛型类型:一个list,其元素的类型被占位符E描述。这个类型有一个名为add()的方法,被声明为有一个类型为E的参数,同时,有一个get()方法,返回值被声明为E类型。

  为了使用泛型类型,你应该为类型变量详细指明实际的类型,形成一个就像List<String>类似的参数化类型。[1]指明这些额外的类型信息的原因是编译器据此能够在编译期为您提供很强的类型检查,增强您的程序的类型安全性。举个例子来说,您有一个只能保持String对象的List,那么这种类型检查就能够阻止您往里面加入String[]对象。同样的,增加的类型信息使编译器能够为您做一些类型转换的事情。比如,编译器知道了一个List<String>有个get()方法,其返回值是一个String对象,因此您不再需要去将返回值由一个Object强制转换为String。

  Java.util包中的集合类在java5.0中已经被做成了泛型,也许您将会在您的程序中频繁的使用到他们。类型安全的集合类就是一个泛型类型的典型案例。即便您从没有定义过您自己的泛型类型甚至从未用过除了java.util中的集合类以外的泛型类型,类型安全的集合类的好处也是极有意义的一个标志——他们证明了这个主要的新语言特性的复杂性。

  我们从探索类型安全的集合类中的基本的泛型用法开始,进而研究更多使用泛型类型的复杂细节。然后我们讨论类型参数通配符和有界通配符。描绘了如何使用泛型以后,我们阐明如何编写自己的泛型类型和泛型方法。我们对于泛型的讨论将结束于一趟对于JavaAPI的核心中重要的泛型类型的旅行。这趟旅程将探索这些类型以及他们的用法,旅程的目的是为了让您对泛型如何工作这个问题有个深入的理解。

  类型安全集合类

  Java.util类包包含了Java集合框架(Java Collections Framework),这是一批包含对象的set、对象的list以及基于key-value的map。第五章将谈到集合类。这里,我们讨论的是在java5.0中集合类使用类型参数来界定集合中的对象的类型。这个讨论并不适合java1.4或更早期版本。如果没有泛型,对于集合类的使用需要程序员记住每个集合中元素的类型。当您在java1.4种创建了一个集合,您知道您放入到集合中的对象的类型,但是编译器不知道。您必须小心地往其中加入一个合适类型的元素,当需要从集合中获取元素时,您必须显式的写强制类型转换以将他们从Object转换为他们真是的类型。考察下边的java1.4的代码。

public static void main(String[] args) {
    // This list is intended to hold only strings.
    // The compiler doesn't know that so we have to remember ourselves.
    List wordlist = new ArrayList();  

    // Oops! We added a String[] instead of a String.
    // The compiler doesn't know that this is an error.
    wordlist.add(args);

    // Since List can hold arbitrary
objects, the get() method returns
    // Object.  Since the list is intended to hold strings, we cast the
    // return value to String but get a ClassCastException because of
    // the error above.
    String word = (String)wordlist.get(0);
}

  泛型类型解决了这段代码中的显示的类型安全问题。Java.util中的List或是其他集合类已经使用泛型重写过了。就像前面提到的, List被重新定义为一个list,它中间的元素类型被一个类型可变的名称为E的占位符描述。Add()方法被重新定义为期望一个类型为E的参数,用于替换以前的Object,get()方法被重新定义为返回一个E,替换了以前的Object。

  在java5.0中,当我们申明一个List或者创建一个ArrayList的实例的时候,我们需要在泛型类型的名字后面紧跟一对“<>”,尖括号中写入我们需要的实际的类型。比如,一个保持String的List应该写成“List<String>”。需要注意的是,这非常象给一个方法传一个参数,区别是我们使用类型而不是值,同时使用尖括号而不是圆括号

  Java.util的集合类中的元素必须是对象化的,他们不能是基本类型。泛型的引入并没有改变这点。泛型不能使用基本类型:我们不能这样来申明——Set<char>或者List<int>。记住,无论如何,java5.0中的自动打包和自动解包特性使得使用Set<Character>或者List<Integer>和直接使用char和int值一样方便。(查看第二章以了解更多关于自动打包和自动解包的细节)。

  在Java5.0中,上面的例子将被重写为如下方式:

public static void main(String[] args) {
    // This list can only hold String objects
    List<String> wordlist = new ArrayList<String>();

    // args is a String[], not String, so the compiler won't let us do this
    wordlist.add(args);  // Compilation error!

    // We can do this, though.  
    // Notice the use of the new for/in looping statement
    for(String arg : args) wordlist.add(arg);

    // No cast is required.  List<String>.get() returns a String.
    String word = wordlist.get(0);
}

  值得注意的是代码量其实并没有比原来那个没有泛型的例子少多少。使用“(String)”这样的类型转换被替换成了类型参数“<String>”。 不同的是类型参数需要且仅需要声明一次,而list能够被使用任何多次,不需要类型转换。在更长点的例子代码中,这一点将更加明显。即使在那些看上去泛型语法比非泛型语法要冗长的例子里,使用泛型依然是非常有价值的——额外的类型信息允许编译器在您的代码里执行更强的错误检查。以前只能在运行起才能发现的错误现在能够在编译时就被发现。此外,以前为了处理类型转换的异常,我们需要添加额外的代码行。如果没有泛型,那么当发生类型转换异常的时候,一个ClassCastException异常就会被从实际代码中抛出。

  就像一个方法可以使用任意数量的参数一样,类允许使用多个类型变量。接口Java.util.Map就是一个例子。一个Map体现了从一个key的对象到一个value的对象的映射关系。接口Map申明了一个类型变量来描述key的类型而另一个类型变量来描述value的类型。举个例子来说,假设您希望做一个String对象到Integer对象的映射关系:

public static void main(String[] args) {
    // A map from strings to their position in the args[] array
    Map<String,Integer> map = new HashMap<String,Integer>();

    // Note that we use autoboxing to wrap i in an Integer object.
    for(int i=0; i < args.length; i++) map.put(args[i], i);  

    //
Find the array index of a word.  Note no cast is required!
    Integer position = map.get("hello");

    // We can also rely on autounboxing to convert directly to an int,
    // but this throws a NullPointerException if the key does not exist
    // in the map
    int pos = map.get("world");
}

  象List<String>这个一个参数类型其本身也是也一个类型,也能够被用于当作其他类型的一个类型变量值。您可能会看到这样的代码:

// Look at all those nested angle brackets!
Map<String, List<List<int[]>>> map = getWeirdMap();

// The compiler knows all the types and we can write expressions
// like this without casting.  We might still get NullPointerException
// or ArrayIndexOutOfBounds at runtime, of course.
int value = map.get(key).get(0).get(0)[0];

// Here's how we break that expression down step by step.
List<List<int[]>> listOfLists = map.get(key);
List<int[]> listOfIntArrays = listOfLists.get(0);
int[] array = listOfIntArrays.get(0);
int element = array[0];

  在上面的代码里,java.util.List<E>和java.util.Map<K,V>的get()方法返回一个类型为E的list元素或者一个类型为V的map元素。注意,无论如何,泛型类型能够更精密的使用他们的变量。在本书中的参考章节查看List<E>,您将会看到它的iterator( )方法被声明为返回一个Iterator<E>。这意味着,这个方法返回一个跟list的实际的参数类型一样的一个参数类型的实例。为了具体的说明这点,下面的例子提供了不使用get(0)方法来获取一个List<String>的第一个元素的方法。

List<String> words = // ...initialized elsewhere...
Iterator<String> iterator = words.iterator();
String firstword = iterator.next();

理解泛型类型

  本段将对泛型类型的使用细节做进一步的探讨,以尝试说明下列问题:

  不带类型参数的使用泛型的后果

  参数化类型的体系

  一个关于编译期泛型类型的类型安全的漏洞和一个用于确保运行期类型安全的补丁

  为什么参数化类型的数组不是类型安全的

  未经处理的类型和不被检查的警告

  即使被重写的Java集合类带来了泛型的好处,在使用他们的时候您也不被要求说明类型变量。一个不带类型变量的泛型类型被认为是一个未经处理的类型(raw type)。这样,5.0版本以前的java代码仍然能够运行:您显式的编写所有类型转换就像您已经这样写的一样,您可能会被一些来自编译器的麻烦所困扰。查看下列存储不同类型的对象到一个未经处理的List:

List l = new ArrayList();
l.add("hello");  
l.add(new Integer(123));
Object o = l.get(0);

  这段代码在java1.4下运行得很好。如果您用java5.0来编译它,javac编译了,但是会打印出这样的“抱怨”:

Note: Test.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.


  如果我们加入-Xlint参数后重新编译,我们会看到这些警告:

Test.java:6: warning: [unchecked]
    unchecked call to add(E) as a member of the raw type java.util.List
        l.add("hello");  
         ^
Test.java:7: warning: [unchecked]
    unchecked call to add(E) as a member of the raw type java.util.List
        l.add(new Integer(123));

         ^

  编译在add()方法的调用上给出了警告,因为它不能够确信加入到list中的值具有正确的类型。它告诉我们说我们使用了一个未经处理的类型,它不能验证我们的代码是类型安全的。注意,get()方法的调用是没有问题的,因为能够被获得的元素已经安全的存在于list中了。

  如果您不想使用任何的java5.0的新特性,您可以简单的通过带-source1.4标记来编译他们,这样编译器就不会再“抱怨”了。如果您不能这样做,您可以忽略这些警告,通过使用一个
“@SuppressWarnings("unchecked")”注解(查看本章的4.3节)隐瞒这些警告信息或者升级您的代码,加入类型变量描述。[2]下列示例代码,编译的时候不再会有警告但仍然允许您往list中放入不同的类型的对象。

List<Object> l = new ArrayList<Object>();
l.add("hello");  
l.add(123);              // autoboxing
Object o = l.get(0);

  参数化类型的体系

  参数化类型有类型体系,就像一般的类型一样。这个体系基于对象的类型,而不是变量的类型。这里有些例子您可以尝试:

ArrayList<Integer> l = new ArrayList<Integer>();
List<Integer> m = l;                            // okay
Collection<Integer> n = l;                      // okay
ArrayList<Number> o = l;                        // error
Collection<Object> p = (Collection<Object>)l;   // error, even with cast

  一个List<Integer>是一个Collection<Integer>,但不是一个List<Object>。这句话不容易理解,如果您想理解为什么泛型这样做,这段值得看一下。考察这段代码:

List<Integer> li = new ArrayList<Integer>();
li.add(123);

// The line below will not compile.  But for the purposes of this
// thought-experiment, assume that it does compile and see how much
// trouble we get ourselves into.
List<Object> lo = li;  

// Now we can retrieve elements of the list as Object instead of Integer
Object number = lo.get(0);

// But what about this?
lo.add("hello world");

// If the line above is allowed then the line below throws ClassCastException
Integer i = li.get(1);  // Can't cast a String to Integer!

  这就是为什么List<Integer>不是一个List<Object>的原因,虽然List<Integer>中所有的元素事实上是一个Object的实例。如果允许转换成List<Object>,那么转换后,理论上非整型的对象也将被允许添加到list中。
运行时类型安全

  就像我们所见到的,一个List<X>不允许被转换为一个List<Y>,即使这个X能够被转换为Y。然而,一个List<X>能够被转换为一个List,这样您就可以通过继承的方法来做这样的事情。

  这种将参数化类型转换为非参数化类型的能力对于向下兼容是必要的,但是它会在泛型所带来的类型安全体系上凿个漏洞:

// Here's a basic parameterized list.
List<Integer> li = new ArrayList<Integer>();

// It is legal to assign a parameterized type to a nonparameterized variable
List l = li;  

// This line is a bug, but it compiles and runs.
// The Java 5.0 compiler will issue an unchecked warning about it.
// If it appeared as part of a legacy class compiled with Java 1.4, however,
// then we'd never even get the warning.  
l.add("hello");

// This line compiles without warning but throws ClassCastException at runtime.
// Note that the failure can occur far away from the actual bug.
Integer i = li.get(0);

  泛型仅提供了编译期的类型安全。如果您使用java5.0的编译器来编译您的代码并且没有得到任何警告,这些编译器的检查能够确保您的代码在运行期也是类型安全的。如果您获得了警告或者使用了像未经处理的类型那样修改您的集合的代码,那么您需要增加一些步骤来确保运行期的类型安全。您可以通过使用java.util.Collections中的checkedList()和checkedMap( )方法来做到这一步。这些方法将把您的集合打包成一个wrapper集合,从而在运行时检查确认只有正确类型的值能够被置入集合众。下面是一个能够补上类型安全漏洞的一个例子:

// Here's a basic parameterized list.
List<Integer> li = new ArrayList<Integer>();

// Wrap it for runtime type safety
List<Integer> cli = Collections.checkedList(li, Integer.class);

// Now widen the checked list to the raw type
List l = cli;  

// This line compiles but fails at runtime with a ClassCastException.
// The exception occurs exactly where the bug is, rather than far away
l.add("hello");

  参数化类型的数组

  在使用泛型类型的时候,数组需要特别的考虑。回忆一下,如果T是S的父类(或者接口),那么类型为S的数组S[],同时又是类型为T的数组T[]。正因为如此,每次您存放一个对象到数组中时,Java解释器都必须进行检查以确保您放入的对象类型与要存放的数组所允许的类型是匹对的。例如,下列代码在运行期会检查失败,抛出一个ArrayStoreException异常:

String[] words = new String[10];
Object[] objs = words;
objs[0] = 1;  // 1 autoboxed to an Integer, throws ArrayStoreException

  虽然编译时obj是一个Object[],但是在运行时它是一个String[],它不允许被用于存放一个Integer。

  当我们使用泛型类型的时候,仅仅依靠运行时的数组存放异常检查是不够的,因为一个运行时进行的检查并不能够获取编译时的类型参数信息。查看下列代码:

List<String>[] wordlists = new ArrayList<String>[10];
ArrayList<Integer> ali = new ArrayList<Integer>();
ali.add(123);
Object[] objs = wordlists;
objs[0] = ali;                       // No ArrayStoreException
String s = wordlists[0].get(0);      // ClassCastException!

  如果上面的代码被允许,那么运行时的数组存储检查将会成功:没有编译时的类型参数,代码简单地存储一个ArrayList到一个ArrayList[]数组,非常正确。既然编译器不能阻止您通过这个方法来战胜类型安全,那么它转而阻止您创建一个参数化类型的数组。所以上述情节永远不会发生,编译器在第一行就开始拒绝编译了。

  注意这并不是一个在使用数组时使用泛型的全部的约束,这仅仅是一个创建一个参数化类型数组的约束。我们将在学习如何写泛型方法时再来讨论这个话题。

  类型参数通配符

  假设我们需要写一个方法来显示一个List中的元素。[3]在以前,我们只需要象这样写段代码:

public static void printList(PrintWriter out, List list) {
    for(int i=0, n=list.size(); i < n; i++) {
        if (i > 0) out.print(", ");
        out.print(list.get(i).toString());
    }
}



在Java5.0中,List是一个泛型类型,如果我们试图编译这个方法,我们将会得到unchecked警告。为了解决这些警告,您可能需要这样来修改这个方法:

public static void printList(PrintWriter out, List<Object> list) {
    for(int i=0, n=list.size(); i < n; i++) {
        if (i > 0) out.print(", ");
        out.print(list.get(i).toString());
    }
}

  这段代码能够编译通过同时不会有警告,但是它并不是非常地有效,因为只有那些被声明为List<Object>的list才会被允许使用这个方法。还记得么,类似于List<String>和List<Integer>这样的List并不能被转型为List<Object>。事实上我们需要一个类型安全的printList()方法,它能够接受我们传入的任何List,而不关心它被参数化为什么。解决办法是使用类型参数通配符。方法可以被修改成这样:

public static void printList(PrintWriter out, List<?> list) {
    for(int i=0, n=list.size(); i < n; i++) {
        if (i > 0) out.print(", ");
        Object o = list.get(i);
        out.print(o.toString());
    }
}

  这个版本的方法能够被编译过,没有警告,而且能够在任何我们希望使用的地方使用。通配符“?”表示一个未知类型,类型List<?>被读作“List of unknown”
作为一般原则,如果类型是泛型的,同时您并不知道或者并不关心值的类型,您应该使用“?”通配符来代替一个未经处理的类型。未经处理的类型被允许仅是为了向下兼容,而且应该只能够被允许出现在老的代码中。注意,无论如何,您不能在调用构造器时使用通配符。下面的代码是非法的:

List<?> l = new ArrayList<?>();

  创建一个不知道类型的List是毫无道理的。如果您创建了它,那么您必须知道它将保持的元素是什么类型的。您可以在随后的方法中不关心元素类型而去遍历这里list,但是您需要在您创建它的时候描述元素的类型。如果你确实需要一个List来保持任何类型,那么您只能这么写:

List<Object> l = new ArrayList<Object>();

  从上面的printList()例子中,必须要搞清楚List<?>既不是List<Object>也不是一个未经处理的List。一个使用通配符的List<?>有两个重要的特性。第一,考察类似于get()的方法,他们被声明返回一个值,这个值的类型是类型参数中指定的。在这个例子中,类型是“unknown”,所以这些方法返回一个Object。既然我们期望的是调用这个object的toString()方法,程序能够很好的满足我们的意愿。

  第二,考察List的类似add()的方法,他们被声明为接受一个参数,这个参数被类型参数所定义。出人意料的是,当类型参数是未确定的,编译器不允许您调用任何有不确定参数类型的方法——因为它不能确认您传入了一个恰当的值。一个List(?)实际上是只读的——既然编译器不允许我们调用类似于add(),set(),addAll()这类的方法。

  界定通配符

  让我们在我们原来的例子上作些小小的稍微复杂一点的改动。假设我们希望写一个sumList()方法来计算list中Number类型的值的合计。在以前,我们使用未经处理的List,但是我们不想放弃类型安全,同时不得不处理来自编译器的unchecked警告。或者我们可以使用List<Number>,那样的话我们就不能调用List<Integer>、List<Double>中的方法了,而事实上我们需要调用。如果我们使用通配符,那么我们实际上不能得到我们期望的类型安全,我们不能确定我们的方法被什么样的List所调用,Number?还是Number的子类?甚至,String?这样的一个方法也许会被写成这样:

public static double sumList(List<?> list) {
    double total = 0.0;
    for(Object o : list) {
        Number n = (Number) o;  // A cast is required and may fail
        total += n.doubleValue();
    }
    return total;
}

  要修改这个方法让它变得真正的类型安全,我们需要使用界定通配符(bounded wildcard),能够确保List的类型参数是未知的,但又是Number或者Number的子类。下面的代码才是我们想要的:

public static double sumList(List<? extends Number> list) {
    double total = 0.0;
    for(Number n : list) total += n.doubleValue();
    return total;
}

  类型List<? extends Number>可以被理解为“Number未知子类的List”。理解这点非常重要,在这段文字中,Number被认为是其自身的子类。

  注意,这样的话,那些类型转换已经不再需要了。我们并不知道list中元素的具体类型,但是我们知道他们能够向上转型为Number,因此我们可以把他们从list中把他们当作一个Number对象取出。使用一个for/in循环能够稍微封装一下从list中取出元素的过程。普遍性的原则是当您使用一个界定通配符时,类似于List中的get()方法的那些方法将返回一个类型为上界的值。因此如果我们在for/in循环中调用list.get(),我们将得到一个Number。在前一节说到使用通配符时类似于list.add()这种方法中的限制依然有效:举个例子来说,如果编译器允许我们调用这类方法,我们就可以将一个Integer放到一个声明为仅保持Short值的list中去。

  同样可行的是使用下界通配符,不同的是用super替换extends。这个技巧在被调用的方法上有一点不同的作用。在实际应用中,下界通配符要比上界通配符用得少。我们将在后面的章节里讨论这个问题。

 

原创粉丝点击