4、泛型引起的错误
泛型是Java5新增的知识点,它允许在使用Java类、调用方法时传入一个类型参数,这样就可以让Java类、调用方法动态地改变类型。
4、1 原始类型变量的赋值
在严格的泛型程序中,使用带泛型声明的类时应该总是为止指定类型实参,但为了与之前版本Java代码保持一致,Java也允许使用带泛型声明的类时不指定类型参数。如果使用带泛型声明的类时没有传入类型参数,那么这个类型参数默认是声明该参数时指定的第一个上限类型,这个类型参数也被称为raw type(原始类型)。
import java.util.ArrayList;
import java.util.List;
public class RawTypeTest {
public static void main(String[] args) {
List list = new ArrayList();
list.add("Java对象1");
list.add("Java对象2");
list.add("Java对象3");
List<Integer> intList = list;
for ( int i = 0 ; i < intList.size() ; i ++ ) {
System.out.println(intList.get(i));
}
}
}
输出结果为:
Java对象1
Java对象2
Java对象3
上面程序中先定义了一个不带泛型信息的List集合,其中所有的集合元素都是String类型。然后将List集合赋给List<Integer>变量,但是可以正常输出intList集合的三个字符串元素。
Java对象2
Java对象3
通过上面介绍可以看出,当程序把一个原始类型的变量赋给一个带泛型信息的变量时,只要它们的类型保持兼容(例如:将List变量赋给List<integer>,无论List集合里实际包含什么类型的元素),系统都不会有任何问题。不过需要指出的是,当把一个原始类型的变量(如List变量)赋给带泛型信息的变量(如List<Integer>)时会有一个潜在的问题:JVM会把集合里盛装的所有元素都当做Integer来处理。上面程序遍历List<Integer>集合时,只是简单地输出每个集合元素,并未涉及集合元素的类型,因此程序并没有出现异常;否则,程序要么在运行时出现ClassCastException异常,要么在编译时提示编译错误。
import java.util.ArrayList;
import java.util.List;
public class RawTypeTest2 {
public static void main(String[] args) {
List list = new ArrayList();
list.add("Java对象1");
list.add("Java对象2");
list.add("Java对象3");
List<Integer> intList = list;
for ( int i = 0 ; i < intList.size() ; i ++ ) {
Integer i = intList.get(i);
System.out.println(i);
}
}
}
上面程序遍历intList集合时,尝试将每个集合元素都赋给Integer变量。由于intList集合的类型本身就是List<Integer>类型,因此编译器会将每个集合元素都当成Integer处理。尝试运行上面程序,将看到如下运行时异常。
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
Duplicate local variable i
很明显,intList所引用的集合里包含的集合元素的类型是String,而不是Integer,因此程序在运行代码时将会引发ClassCastException异常。
import java.util.ArrayList;
import java.util.List;
public class RawTypeTest3 {
public static void main(String[] args) {
List list = new ArrayList();
list.add("Java对象1");
list.add("Java对象2");
list.add("Java对象3");
List<Integer> intList = list;
for ( int i = 0 ; i < intList.size() ; i ++ ) {
String str = intList.get(i);
System.out.println(intList.get(i));
}
}
}
尝试编译上面程序,将发现编译器直接提示如下编译信息。对于程序中intList集合而言,它的类型是List<Integer>类型,因此编译器会认为该集合的每个元素都是Integer类型,而上面程序尝试将该集合元素赋给一个String类型的变量,因此会提示编译错误。
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
Type mismatch: cannot convert from Integer to String
总结:
- 当程序把一个原始类型的变量赋给一个带泛型信息的变量时,总是可以通过编译,只是会提示一些警告信息。
- 当程序试图访问带有泛型声明的集合的集合元素时,编译器总是把集合元素当成泛型类型处理,并不关心集合里元素的实际类型。
- 当程序试图访问带有泛型声明的集合的集合元素时,JVM会遍历每个集合元素自动执行强制类型转化,如果集合元素的实际类型与集合所带的泛型信息不匹配,运行时将引发ClassCastException异常。
4、2 原始类型的擦除
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有尖括号里的类型信息都将被抛弃。比如,将一个List<String>类型的对象转型为List,则该List对集合元素的类型检查变成了类型变量的上限(即Object)。
class Tree<T extends Number> {
T size;
public Tree(T size) {
this.size = size;
}
public T getSize() {
return size;
}
public void setSize(T size) {
this.size = size;
}
}
public class ErasureTest {
public static void main(String[] args) {
Tree<Integer> a = new Tree<Integer>(6); //①
Integer as = a.getSize();
Tree b = a;
Number size1 = b.getSize();
//Integer size2 = b.getSize(); 报错:Type mismatch: cannot convert from Number to Integer
}
}
上面程序里定义了一个带泛型声明的Tree类,其类型形参的上限是Number,这个类型形参用来定义Tree类的size属性。在①行代码创建了一个Tree<Integer>对象,所以调用a的getSize()方法时返回Integer类型的值。当把a赋给一个不带泛型信息的b变量时,编译器就会丢失a对象的泛型信息,但因为Tree类型形参的上限是Number类,所以编译器依然知道b的getSize()方法返回Number类型,但具体是Number的哪个子类就不清楚了。
从上面程序可以看出,当把一个带泛型信息的Java对象赋给不带泛型信息的变量时,Java程序会发生擦除,这种擦除不仅会擦除使用该Java类时传入的类型实参,而且会擦除所有泛型信息,也就是擦除所有尖括号里的信息。
import java.util.ArrayList;
import java.util.List;
class AppleTree<T extends Number> {
T size;
public AppleTree() {}
public AppleTree(T size) {
this.size = size;
}
public T getSize() {
return size;
}
public void setSize(T size) {
this.size = size;
}
public List<String> getApple() {
List<String> list = new ArrayList<>();
for ( int i = 0 ; i < 3 ; i ++ ) {
list.add(new AppleTree<Integer>(10 * i).toString());
}
return list;
}
public String toString() {
return "AppleTree[size = " + size + "]";
}
}
public class ErasureTest2 {
public static void main(String[] args) {
AppleTree<Integer> a = new AppleTree<Integer>(6);
for ( String appleTree : a.getApple() ) {
System.out.println(appleTree);
}
AppleTree b = a;
for ( String appleTree : b.getApple() ) {
System.out.println(appleTree);
}
}
}
上面程序的main方法中先创建了一个AppleTree<Integer>对象,程序调用该方法的getApple()方法的返回值肯定是List<String>类型的值。将a变量赋给AppleTree类型的b变量时会发生擦除,该AppleTree<Integer>对象将丢失所有的泛型信息,包括
getApple()方法的返回值类型List<Stirng>里的尖括号信息,因此最后会提示“Type mismatch: cannot convert from element type Object to String”。