Java 中的泛型 – 第 III 部分






4.11/5 (9投票s)
本文讨论了混合泛型和非泛型(原始类型)代码时遇到的问题,以及将非泛型遗留代码转换为泛型时遇到的问题。
摘要
在第一部分和第二部分中,我们讨论了 Java 泛型的优点和用法,以及其底层实现方式。在本第三部分中,我们将总结讨论混合泛型和非泛型(原始类型)代码时遇到的问题,以及将非泛型遗留代码转换为泛型时遇到的问题。
混合泛型和非泛型代码
我们来看下面的例子
import java.util.ArrayList;
public class Test
{
public static void addElements(Collection list)
{
list.add(3);
//list.add(1.2);
}
public static void main(String[] args)
{
ArrayList<Integer> lst = new ArrayList<Integer>();
addElements(lst);
//lst.add(3.2);
int total = 0;
for(int val : lst)
{
total += val;
}
System.out.println("Total is : " + total);
}
}
在上面的示例中,lst
指的是泛型 ArrayList
的一个实例。我将该实例传递给 addElements()
方法。在该方法中,我向 ArrayList
添加了 3。回到 main()
方法,我遍历 ArrayList
,一次从中提取一个整数值并计算总和。上述程序的输出是
Total is : 3
现在,在 main()
方法中,如果我取消注释语句 lst.add(3.2);
,我会得到一个编译错误,如下所示
Error: line (18) cannot find symbol method add(double)
另一方面,如果我将该语句注释掉,但在 addElements()
方法中取消注释语句 list.add(1.2);
,我不会得到任何编译错误。但是,当运行程序时,我会得到一个运行时异常,如下所示
Exception in thread "main" java.lang.ClassCastException: java.lang.Double
at Test.main(Test.java:21)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:78)
哪里出错了?在 main()
方法中,我假设 ArrayList<Integer>
包含整数值。然而,在运行时,由于在 addElements()
方法中添加了值 1.2,这个假设被证明是错误的。
您可能会同意,得到编译时错误比得到运行时错误更好。然而,泛型并未完全提供其预期的类型安全性。如果我们确实会得到运行时异常,那么最好是在 addElements()
方法中(我们在其中将值 1.2 添加到 ArrayList
)而不是在 main()
方法中(我们试图从中获取元素)得到它。这可以通过使用 Collections
类的 checkedList()
方法来实现,如下所示
//addElements(lst);
addElements(Collections.checkedList(lst, Integer.class));
checkedList()
方法将给定的 ArrayList
包装在一个对象中,该对象将确保通过它添加的元素是指定类型,在此例中为 Integer
类型。
当我执行此程序时,我得到以下运行时异常
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:2206)
at java.util.Collections$CheckedCollection.add(Collections.java:2240)
at Test.addElements(Test.java:11)
at Test.main(Test.java:19)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:78)
将此异常消息与之前的消息进行比较。在这种情况下,异常报告在 addElements()
方法内的第 11 行,而不是之前报告的 main()
方法内的第 21 行。
如果您必须将泛型类型传递给接受非泛型类型的那些方法,请考虑按照上面的示例进行对象包装。
将非泛型代码转换为泛型
在第二部分中,我们讨论了类型擦除技术,并看到了参数化类型是如何转换为 Object
类型或指定的边界类型之一的。如果我们必须从非泛型类型转换为泛型类型,这是否只是添加参数化类型 E
或将 Object
替换为 E
的问题?不幸的是,生活没那么简单。
考虑以下示例
import java.util.ArrayList;
import java.util.Collection;
public class MyList
{
private ArrayList list = new ArrayList();
public void add(Object anObject)
{
list.add(anObject);
}
public boolean contains(Object anObject)
{
if (list.contains(anObject))
return true;
return false;
}
public boolean containsAny(Collection objects)
{
for(Object anObject : objects)
{
if (contains(anObject))
return true;
}
return false;
}
public void addMany(Collection objects)
{
for(Object anObject : objects)
{
add(anObject);
}
}
public void copyTo(MyList destination)
{
for(Object anObject : list)
{
destination.list.add(anObject);
}
}
}
MyList
是一个表示我自己的集合的类。我们暂且不深入探讨 addMany()
方法或 containsAny()
方法是否应该真正属于 MyList
类。从设计角度来看,如果您认为这些不应该在这里,它们可能属于其他地方——在一个外观(facade)中——而我们将要讨论的问题将延伸到该类。现在,让我们看看使用此类的一个示例代码
class Animal {}
class Dog extends Animal { }
class Cat extends Animal { }
public class Test
{
public static void main(String[] args)
{
MyList lst = new MyList();
Dog snow = new Dog();
lst.add(snow);
System.out.println("Does list contain my snow? " + lst.contains(snow));
Cat tom = new Cat();
lst.add(tom);
System.out.println("Does list contain tom? " + lst.contains(tom));
}
}
上述程序产生如下所示的预期结果
Does list contain my snow? true
Does list contain tom? true
现在,让我们开始将 MyList
改为使用泛型。最简单的解决方案——用参数化类型 E
替换 Object
。这是代码更改的结果
import java.util.ArrayList;
import java.util.Collection;
public class MyList<E>
{
private ArrayList<E> list = new ArrayList<E>();
public void add(E anObject)
{
list.add(anObject);
}
public boolean contains(E anObject)
{
if (list.contains(anObject))
return true;
return false;
}
public boolean containsAny(Collection<E> objects)
{
for(E anObject : objects)
{
if (contains(anObject))
return true;
}
return false;
}
public void addMany(Collection<E> objects)
{
for(E anObject : objects)
{
add(anObject);
}
}
public void copyTo(MyList<E> destination)
{
for(E anObject : list)
{
destination.list.add(anObject);
}
}
}
我们修改 main()
方法以使用泛型(实际上,如果您继续使用非泛型风格,则无需更改 main()
)。
唯一修改的语句如下所示
MyList<Animal> lst = new MyList<Animal>();
程序可以编译而没有错误,并产生与之前相同的结果。那么,从原始类型到泛型的转换进行得非常顺利,对吧?我们发货吧?
嗯,这正好触及了测试代码的问题。如果没有良好的测试,我们最终会发布此代码,结果只会收到客户电话,他们会编写如下代码
Dog rover = new Dog();
ArrayList<Dog> dogs = new ArrayList<Dog>();
dogs.add(snow);
dogs.add(rover);
System.out.println("Does list contain snow or rover? " + lst.containsAny(dogs));
我们得到一个编译错误
Error: line (29)
containsAny(java.util.Collection<Animal>)
in MyList<Animal> cannot be applied to java.util.ArrayList<Dog>)
怎么修复?我们需要稍微调整一下 containsAny()
方法以适应这个合理的调用。更改如下所示
public boolean containsAny(Collection<? extends E> objects)
(有关下界、上界和通配符的详细信息,请参阅本文的第一部分和第二部分。)
现在,程序再次正常工作。但是,如果 main()
方法按如下方式修改,我们又会得到一个编译错误
lst.addMany(dogs);
再次,这需要调整代码,这次是 addMany()
方法。
public void addMany(Collection<? extends E> objects)
现在,让我们看看 copyTo()
方法。这里有一个使用该方法的示例
MyList<Dog> myDogs = new MyList<Dog>();
myDogs.add(new Dog());
myDogs.copyTo(new MyList<Dog>());
在上面的代码中,我们正在将 Dog
对象从一个 MyList<Dog>
复制到另一个 MyList<Dog>
。看起来合理吗?是的,并且它有效。将 Dog
对象从 MyList<Dog>
复制到 MyList<Animal>
也是合法的,不是吗?毕竟,一个 Animal
列表可以包含 Dog
对象。所以,让我们试试
MyList<Dog> myDogs = new MyList<Dog>();
myDogs.add(new Dog());
myDogs.copyTo(new MyList<Animal>());
然而,这段代码会导致编译错误,如下所示
Error: line (36) copyTo(MyList<Dog>) in MyList<Dog> cannot be applied to (MyList<Animal>)
在这种情况下,我们确实希望将一个基类集合发送到该方法。我们必须再次进行调整,这次是 copyTo()
方法。我们希望该方法接受 Dog
对象的 MyList
或 Dog
基类的 MyList
。总的来说,我们希望它接受参数化类型的 MyList
或参数化类型基类的 MyList
。所以,这是该代码
public void copyTo(MyList<? super E> destination)
根据具体情况,您可能需要使用参数化类型 E
、下界、上界或通配符。不幸的是,这需要仔细考虑。您可能很容易忽略这些细节(就像我们在上面的示例中看到的那样),并且问题可能直到有人实际编写了一段代码来测试您的代码,以至于暴露了问题。
结论
总而言之,泛型是为了提供类型安全性而开发的。它们在一定程度上实现了这一目标。然而,它们并未提供完全的类型安全性。这很大程度上是由于设计目标和实现限制。首先,学习 Java 泛型。然后,要有智慧地决定何时以及如何(在多大程度上)使用它。