欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

泛型的几个注意点!

程序员文章站 2022-03-13 12:04:12
...
[size=medium]上周代码里碰到了泛型,使用到了类型参数通配符,平时使用到不多,但是一用到还是会有些模糊,于是想再学习下泛型,网上的文章一抓一大把,但都是你抄我我抄你的,好不容易找到两篇好文,于是整理了些过来。

[color=red][b]未经处理的类型和不被检查的警告[/b][/color]

即使被重写的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);



[color=red][b]参数化类型的体系[/b][/color]

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


[color=red][b]运行时类型安全[/b][/color]

就像我们所见到的,一个List<X>不允许被转换为一个List<Y>,即使这个X能够被转换为Y。然而,一个List<X>能够被转换为一个List,这样您就可以通过继承的方法来做这样的事情。
这种将参数化类型转换为非参数化类型的能力对于向下兼容是必要的,但是它会在泛型所带来的类型安全体系上凿个漏洞:
[color=red]Java容器的插入方法并不检查类型,所以很可能会插入错误的类型:[/color]
List l_raw = new ArrayList<Integer>();
l_raw.add(new Double(3)); // success

[color=red]Collections提供了检查类型的方法,比如checkedList返回一个在插入时检查类型的List:[/color]
List<Integer> l = Collections.checkedList(new ArrayList<Integer>(), Integer.class);
List l_raw = l;
l_raw.add(new Double(3)); // throws exception!

[color=red]在checked容器里试图插入不合法的类型会抛出异常。[/color]
[color=red]运行了下,结果如下:
Exception in thread "main" java.lang.ClassCastException: Attempt to insert class java.lang.Double element into collection with element type class java.lang.Integer
at java.util.Collections$CheckedCollection.typeCheck(Collections.java:2202)
at java.util.Collections$CheckedCollection.add(Collections.java:2243)
at generic.TestType.main(TestType.java:12)


[/color]


[color=red][b]参数化类型的数组[/b][/color]

在使用泛型类型的时候,数组需要特别的考虑。回忆一下,如果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[]数组,非常正确。既然编译器不能阻止您通过这个方法来战胜类型安全,那么它转而阻止您创建一个参数化类型的数组。所以上述情节永远不会发生,编译器在第一行就开始拒绝编译了。
注意这并不是一个在使用数组时使用泛型的全部的约束,这仅仅是一个创建一个参数化类型数组的约束。我们将在学习如何写泛型方法时再来讨论这个话题。


[b][color=red]类型参数通配符[/color][/b]

假设我们需要写一个方法来显示一个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()这类的方法。


[color=red][b]界定通配符[/b][/color]

让我们在我们原来的例子上作些小小的稍微复杂一点的改动。假设我们希望写一个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中去。
[/size]
相关标签: 泛型