Java泛型(Generics)是JDK5中引入的特性,允许在定义类和接口的时候使用类型参数(type parameter),声明的类型参数在Runtime时替换为真正的具体类型。
泛型的优点是可以帮助编译器更早的检查一些类型错误,解决一些运行时异常,但为了保证与旧版本的JDK兼容,Java泛型的实现存在着一些不够优雅的地方。
正确理解泛型首先要理解类型擦除(type erasure)。Java的泛型基本都是在编译器这一层工作的,在生成的Java字节码中式不包含泛型中的类型信息的,换言之,类型参数会在编译器编译的时候被去掉,这个过程称之为类型擦除。举例如代码中定义的List<Object>和List<String>在编译之后都会变成List,JVM只会看到List。但Java编译器会在编译过程中尽可能的发现出隐含可能出错的地方,但仍然无法尽善尽美。所以Java的类型擦除机制不像C++中模板机制实现泛型那么优雅。主要是由于向前兼容,Java泛型支持更多的发生在编译层,不需要对JVM做任何的修改来支持。
首先我们简单看一下,C++中模板机制的泛型应用如下:
既可以适用int max(int i,int j),又可以适用double max(double i,double j),在编译时产生相对应的类型(如例子中的int和double)函数,而不是会擦除掉任何类型参数,灵活性高。
相比之下,Java中泛型的一些奇特的地方如下:
- Java泛型没有自己的Class类,所以静态变量是被泛型类的所有实例所共享的。(MyClass<T>访问静态变量方法仍然是MyClass.staticVar)
- Java泛型的类型参数不能用在Java异常处理的catch语句中,因为异常处理是由JVM在运行时执行的,所以JVM无法区分Exception<String>和Exception<Integer>
Java泛型的类型擦除的过程大致是:首先找到替换的具体类型,一般是Object,如果指定了泛型上界则使用上界。把代码中的类型参数都替换成具体的类,同时去掉<>中的类型声明。
编译器为了确保类型的安全性,会禁止某些泛型的使用方式,例如以下代码:
编译时会发生错误: 无法将inspect(java.util.List<java.lang.Object>) 应用于(java.util.List<java.lang.String>) inspect(strs)
由于无法保证在inspect中对于list只加入String类型的元素,如可能会加入Integer或者任何非String类型,所以编译器不允许将List<String>当成List<Object>来使用。Java编译器会尽可能早的检查可能存在的类型安全问题,给出编译错误。(若编译器无法做出判断,则给出警告信息)
在Java泛型中还可以适用通配符,如List<?>声明了List中包含的元素类型是未知的,通配符所代表的是一组类型,只是具体类型不确定。不确定的元素只能用Object来引用,所以如果把上面例子中的List<Object>替换成List<?>则编译可以通过,但如果加上list.add(1)那么编译器会再次给出编译错误:
一般在使用通配符的时候建议加个上界,如List<? extends Number>,具体的类型可以根据上界来看,List<Integer>、List<Double>都会是合法的。
在使用泛型的时候一些经验:
1. 在代码中避免泛型类和原始类型的混用。比如List<String>和List不应该共同使用。这样会产生一些编译器警告和潜在的运行时异常。当需要利用JDK 5之前开发的遗留代码,而不得不这么做时,也尽可能的隔离相关的代码。
2. 在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念。由于具体的类型是未知的,很多操作是不允许的。
3. 泛型类最好不要同数组一块使用。你只能创建new List<?>[10]这样的数组,无法创建new List<String>[10]这样的。这限制了数组的使用能力,而且会带来很多费解的问题。因此,当需要类似数组的功能时候,使用集合类即可。
泛型的优点是可以帮助编译器更早的检查一些类型错误,解决一些运行时异常,但为了保证与旧版本的JDK兼容,Java泛型的实现存在着一些不够优雅的地方。
正确理解泛型首先要理解类型擦除(type erasure)。Java的泛型基本都是在编译器这一层工作的,在生成的Java字节码中式不包含泛型中的类型信息的,换言之,类型参数会在编译器编译的时候被去掉,这个过程称之为类型擦除。举例如代码中定义的List<Object>和List<String>在编译之后都会变成List,JVM只会看到List。但Java编译器会在编译过程中尽可能的发现出隐含可能出错的地方,但仍然无法尽善尽美。所以Java的类型擦除机制不像C++中模板机制实现泛型那么优雅。主要是由于向前兼容,Java泛型支持更多的发生在编译层,不需要对JVM做任何的修改来支持。
首先我们简单看一下,C++中模板机制的泛型应用如下:
template <class T>
T& max(T& t1, T& t2) {
return t1> t2? t1 : t2;
}
既可以适用int max(int i,int j),又可以适用double max(double i,double j),在编译时产生相对应的类型(如例子中的int和double)函数,而不是会擦除掉任何类型参数,灵活性高。
相比之下,Java中泛型的一些奇特的地方如下:
- Java泛型没有自己的Class类,所以静态变量是被泛型类的所有实例所共享的。(MyClass<T>访问静态变量方法仍然是MyClass.staticVar)
- Java泛型的类型参数不能用在Java异常处理的catch语句中,因为异常处理是由JVM在运行时执行的,所以JVM无法区分Exception<String>和Exception<Integer>
Java泛型的类型擦除的过程大致是:首先找到替换的具体类型,一般是Object,如果指定了泛型上界则使用上界。把代码中的类型参数都替换成具体的类,同时去掉<>中的类型声明。
编译器为了确保类型的安全性,会禁止某些泛型的使用方式,例如以下代码:
public void inspect(List<Object> list) {
for (Object obj : list)
System.out.println(obj);
//possible happened
//list.add(1);
}
public void test() {
List<String> strs = new ArrayList<String>();
inspect(strs); //Compile error
}
编译时会发生错误: 无法将inspect(java.util.List<java.lang.Object>) 应用于(java.util.List<java.lang.String>) inspect(strs)
由于无法保证在inspect中对于list只加入String类型的元素,如可能会加入Integer或者任何非String类型,所以编译器不允许将List<String>当成List<Object>来使用。Java编译器会尽可能早的检查可能存在的类型安全问题,给出编译错误。(若编译器无法做出判断,则给出警告信息)
在Java泛型中还可以适用通配符,如List<?>声明了List中包含的元素类型是未知的,通配符所代表的是一组类型,只是具体类型不确定。不确定的元素只能用Object来引用,所以如果把上面例子中的List<Object>替换成List<?>则编译可以通过,但如果加上list.add(1)那么编译器会再次给出编译错误:
找不到符号
符号: 方法 add(int)
一般在使用通配符的时候建议加个上界,如List<? extends Number>,具体的类型可以根据上界来看,List<Integer>、List<Double>都会是合法的。
在使用泛型的时候一些经验:
1. 在代码中避免泛型类和原始类型的混用。比如List<String>和List不应该共同使用。这样会产生一些编译器警告和潜在的运行时异常。当需要利用JDK 5之前开发的遗留代码,而不得不这么做时,也尽可能的隔离相关的代码。
2. 在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念。由于具体的类型是未知的,很多操作是不允许的。
3. 泛型类最好不要同数组一块使用。你只能创建new List<?>[10]这样的数组,无法创建new List<String>[10]这样的。这限制了数组的使用能力,而且会带来很多费解的问题。因此,当需要类似数组的功能时候,使用集合类即可。
4. 不要忽视编译器给出的警告信息。