Java泛型详解 原作者:火木棉 博客地址:网易博客
Java泛型 (generics)是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(
一、类型擦除
正确理解泛型概念的首要前提是理解 类型擦除(type erasure)。 Java中的 泛型基本上都是在编译器这个层次来实现的 。在生成的很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:
- 泛型类并没有自己独有的Class类对象。
比如并不存在List<String>. class或是List<Integer>.class, 而只有List.class。 - 静态变量是被泛型类的所有实例所共享的。
对于声明为MyClass<T>的类, 访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过new MyClass<String>还是new MyClass<Integer>创建的对象, 都是共享一个静态变量。 - 泛型的类型参数不能用在Java异常处理的catch语句中。
因为异常处理是由JVM在运行时刻来进行的。 由于类型信息被擦除, JVM是无法区分两个异常类型MyException< String>和MyException<Integer>的。 对于JVM来说,它们都是 MyException类型的。 也就无法执行与异常对应的catch语句。
类型擦除的基本过程也比较简单,
class MyString implements Comparable<String>{
public int compareTo(String str){
return 0;
}
}
当类型信息被擦除之后,上述类的声明变成了class MyString implements Comparable。但是这样的话,
二、实例分析
了解了类型擦除机制之后,
public void inspect(List<Object> list) {
for (Object obj : list) {
System.out.println(obj);
}
list.add(1); //这个操作在当前方法的上下文是合法的。
}
public void test() {
List<String> strs = new ArrayList<String>();
inspect(strs); //编译错误
}
这段代码中,inspect方法接受List<Object>
三、通配符(?)与上下界(extends ,super )
在使用泛型类的时候,既可以指定一个具体的类型,如List<
package com.codeleven.test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Main{
public static void test1(List<?> list){
//list.add(new Integer(10)); 不能进行编译,因为list这个引用是某个未知的具体类;如果?代表String,那怎么添加Integer?
//list.add("123"); 如果?代表Integer,那怎么添加String?
Integer aint= (Integer)list.get(0);
String astr = (String)list.get(1);
}
public static void test2(List<? extends Number> list){
//list.add(new Integer(1)); 因为list的具体类型是继承Number的一个子类,不过究竟是Integer还是Float还是...无从得知,在编译期就直接报错;
//list.add(new Float(1.3));
//假设没有编译期的报错,那么因为上界是Number的子类,所以我们可以自由添加Float,Integer,Double等等类型,但是到了运行期,真正的类型Integer被检测出来,此时依旧会报ClassCastException
Number temp1 = list.get(0); //可以发现获取到的类型直接是Number, 所以限定了上界,那么获取出来的类型必定是Number或者Number的子类
Number temp2 = list.get(1);
}
public static void test3(List<? super Number> list){
list.add(new Integer(567)); //因为限定了下界,所有再加入的类型都一定会是Number的子类或Number,所以用Number接受就可以了
//list.add(new String("123"));报错,因为不是Number的子类
//Number temp = list.get(0); 编译器不知道取出来的究竟是Number或者Number的某个具体父类
}
public static void main(String[] args){
//通配符的使用和解释
List<?> alist = new ArrayList<Object>(Arrays.asList(new Integer(1), new String("123")));
test1(alist);
//上界
List<? extends Number> nlist = new ArrayList<Integer>(Arrays.asList(new Integer(123), new Integer(111)));
test2(nlist);
//下界
List<? super Number> slist = new ArrayList<Object>(Arrays.asList(new Integer(123), new Integer(999), new String("哈哈")));
test3(slist);
}
}
如上所示,试图对一个带通配符的泛型类进行操作的时候,
因为对于List<?>中的元素只能用Object来引用,
四、类型系统
在Java中,引入泛型之后的类型系统增加了两个维度:
- 相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。
即List<String>是Collection< String> 的子类型,List<String> 可以替换Collection<String>。 这种情况也适用于带有上下界的类型声明。 - 当泛型类的类型声明中使用了通配符的时候, 其子类型可以在两个维度上分别展开。如对Collection<
? extends Number>来说, 其子类型可以在Collection这个维度上展开, 即List<? extends Number>和Set<? extends Number>等;也可以在Number这个层次上展开, 即Collection<Double>和 Collection<Integer>等。如此循环下去, ArrayList<Long>和 HashSet<Double> 等也都算是Collection<? extends Number>的子类型。 - 如果泛型类中包含多个类型参数,
则对于每个类型参数分别应用上面的规则。
五、开发自己的泛型类
泛型类与一般的Java类基本相同,只是在类和接口定义上多出来
class ClassTest<X extends Number, Y, Z> { private X x; private static Y y; //编译错误,不能用在静态变量中 public X getFirst() { //正确用法 return x; } public void wrong() { Z z = new Z(); //编译错误,不能创建对象 ,因为泛型类并没有自己独立的Class类对象 } }
End、最佳实践
在使用泛型的时候可以遵循一些基本的原则,- 在代码中避免泛型类和原始类型的混用。比如List<
String>和List不应该共同使用。 这样会产生一些编译器警告和潜在的运行时异常。当需要利用JDK 5之前开发的遗留代码,而不得不这么做时, 也尽可能的隔离相关的代码。 - 在使用带通配符的泛型类的时候,
需要明确通配符所代表的一组类型的概念。 由于具体的类型是未知的,很多操作是不允许的。 - 泛型类最好不要同数组一块使用。你只能创建new List<?>[10]这样的数组,无法创建new List<String>[10]这样的。
这限制了数组的使用能力,而且会带来很多费解的问题。因此, 当需要类似数组的功能时候,使用集合类即可。 - 不要忽视编译器给出的警告信息。