1.一个简单的范型示例
在以前,你可能遇到过这样的代码:
1.
2. List list = new LinkedList();
3.
4. list.add(“zhangsan”);
5.
6. list.add(“lisi”);
7.
8. list.add(“wangwu”);
9.
10. String name = (String)list.get(0);
注意,第10行需要强制转换。而使用范型:
1.
2. List<String> list = new LinkedList<String>();
3.
4. list.add(“zhagnsan”);
5.
6. list.add(“lisi”);
7.
8. list.add(“wangwu”);
9.
10. String name = list.get(0);
11.
这里将list声明成String类型的List。List是有一个类型参数的范型接口。这个例子中类型参数是String。
2.定义简单的范型
看j2se5.0中List和Iterator接口的实现(片断):
1.
2. public interface List<E>
3.
4. {
5.
6. void add(E x);
7.
8. Iterator<E> iterator();
9.
10. }
11.
12. public interface Iterator<E>
13.
14. {
15.
16. E next();
17.
18. boolean hasNext();
19.
20. }
21.
22.
23.
上面的代码我们比较熟悉,但是其中增加了尖括号。尖括号中的内容定义了接口List和Iterator的形式类型参数。类型参数可以用在范型声明中,如类和接口的声明。
一旦声明了范型,你就可以使用它。在上面的例子中使用了List<String>。这里使用String是实参,代替了形参E。如果使用List<Integer>,则用实参Integer代替了形参E。
不管List<Integer>还是List<String>,它们的类只有一个。考虑下面的代码:
1.
2. List<String> list1 = new LinkedList<String>();
3.
4. List<Integer> list2 = new LinkedList<Integer>();
5.
6. System.out.println(list1.getClass()==list2.getClass());
输出结果为true。
一般来说,形式类型参数都是大写,尽量使用单个字母,许多容器类都使用E作为参数。
3.范型和继承
考虑下面的代码,你认为它会出错吗?
1.
2. String s = “smallnest@163.com”;
3.
4. Object o = s:
5.
当然,String类继承Object类,这样做不会出错。但下面的代码呢?
1.
2. List<String> s = new LinkedList<String>();
3.
4. List<Object>o=s;
5.
编译出错!
是的,List<Object>和List<String>没有继承关系。
4.通配符
考虑下面一个方法:
1.
2. public void printCollection(Collection<Object> c)
3.
4. {
5.
6. for(Object o:c)
7.
8. {
9.
10. System.out.printf(“%s%n”,o);
11.
12. }
13.
14. }
15.
事实上,上面这个方法并不通用,它只能打印Collection<Object>类型的集合,象其他的如Collection<String>、Collection<Integer>并不能被打印,因为对象类型不一致。
为了解决这个问题,可以使用通配符:
1.
2. public void printCollection(Collection<?> c)
3.
4. {
5.
6. for(Object o:c)
7.
8. {
9.
10. System.out.printf(“%s%n”,o);
11.
12. }
13.
14. }
15.
Collection<?>被称作未知类型的集合。问号代表各种类型。
上面的读取集合中的数据时,我们采用Object类型。这样做时可以的,因为不管未知类型最终代表何种类型,它的数据都继承Object类,那么再考虑一下下面的代码:
1.
2. Collection<?> c = new ArrayList<String>();
3.
4. c.add(new Object()); //!!!!
5.
1.
这样做时错误的,因为我们不知道?代表何种类型,所以我们不能直接将Object增加到集合中,这会出现类型不匹配的情况。
5.有限制的通配符
有时在还没有完全指定类型参数时,需要对类型参数指定附加的约束。
考虑例子 Matrix
类,它使用类型参数 V
,该参数由 Number
类来限制:
public class Matrix<V extends Number> { ... }
编译器允许您创建 Matrix<Integer>
或 Matrix<Float>
类型的变量,但是如果您试图定义 Matrix<String>
类型的变量,则会出现错误。类型参数 V
被判断为由 Number
限制 。在没有类型限制时,假设类型参数由 Object
限制。这就是为什么前一屏 泛型方法 中的例子,允许 List.get()
在 List<?>
上调用时返回 Object
,即使编译器不知道类型参数 V
的类型。
6.类型通配符的作用
您可以对这样的 List
做什么呢?非常方便,可以从中检索元素,但是不能添加元素。原因不是编译器知道哪些方法修改列表哪些方法不修改列表,而是(大多数)变化的方法比不变化的方法需要更多的类型信息。下面的代码则工作得很好:
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(42));
List<?> lu = li;
System.out.println(lu.get(0));
为什么该代码能工作呢?对于 lu
,编译器一点都不知道 List
的类型参数的值。但是编译器比较聪明,它可以做一些类型推理。在本例中,它推断未知的类型参数必须扩展 Object
。(这个特定的推理没有太大的跳跃,但是编译器可以作出一些非常令人佩服的类型推理,后面就会看到(在 底层细节 一节中)。所以它让您调用 List.get()
并推断返回类型为 Object
。
另一方面,下面的代码不能工作:
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(42));
List<?> lu = li;
lu.add(new Integer(43)); // error
在本例中,对于 lu
,编译器不能对 List
的类型参数作出足够严密的推理,以确定将 Integer
传递给 List.add()
是类型安全的。所以编译器将不允许您这么做。
以免您仍然认为编译器知道哪些方法更改列表的内容哪些不更改列表内容,请注意下面的代码将能工作,因为它不依赖于编译器必须知道关于 lu
的类型参数的任何信息:
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(42));
List<?> lu = li;
lu.clear();
7.范型方法
考虑下面的代码,我们将一个数组的内容加到一个集合中
1.
2. public void copyArrayToCollection(Man[] men, Collection<?>c)
3.
4. {
5.
6. for(Man man:men)
7.
8. {
9.
10. c.add(man);
11.
12. }
13.
14. }
15.
这段代码时错的!
因为我们并不知道集合C的类型,所以不能将Man类型的数据加到集合中。
可以使用范型方法解决:
1.
2. public <T> void copyArrayToCollection(T[] men, Collection<T>c)
3.
4. {
5.
6. for(T man:men)
7.
8. {
9.
10. c.add(man);
11.
12. }
13.
14. }
15.
16.
17.
这里T时一个形式类型参数。
何时该采用通用方法?何时该采用通配符?
考虑下面的例子:
1.
2. interface Collection<E>
3.
4. {
5.
6. public boolean containsAll(Collection<?> c);
7.
8. public boolean addAll(Collection<? extends E> c);
9.
10. }
11.
改写成通用方法
1.
2. interface Collection<E>
3.
4. {
5.
6. public <T> boolean containsAll(Collection<T> c);
7.
8. public <T extends E> boolean addAll(Collection<T> c);
9.
10. }
11.
然而,在这里每个方法T只使用了一次,返回值不依赖形式参数,其他参数也不依赖形式参数。这说明实参被用作多态,这种情况下就应该用通配符。