65.9K
CodeProject 正在变化。 阅读更多。
Home

Java 中的泛型 – 第 III 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.11/5 (9投票s)

2008年7月9日

CPOL

5分钟阅读

viewsIcon

23670

本文讨论了混合泛型和非泛型(原始类型)代码时遇到的问题,以及将非泛型遗留代码转换为泛型时遇到的问题。

摘要

第一部分第二部分中,我们讨论了 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 对象的 MyListDog 基类的 MyList。总的来说,我们希望它接受参数化类型的 MyList 或参数化类型基类的 MyList。所以,这是该代码

public void copyTo(MyList<? super E> destination)

根据具体情况,您可能需要使用参数化类型 E、下界、上界或通配符。不幸的是,这需要仔细考虑。您可能很容易忽略这些细节(就像我们在上面的示例中看到的那样),并且问题可能直到有人实际编写了一段代码来测试您的代码,以至于暴露了问题。

结论

总而言之,泛型是为了提供类型安全性而开发的。它们在一定程度上实现了这一目标。然而,它们并未提供完全的类型安全性。这很大程度上是由于设计目标和实现限制。首先,学习 Java 泛型。然后,要有智慧地决定何时以及如何(在多大程度上)使用它。

参考文献

  1. Java 中的泛型,第一部分
  2. Java 中的泛型,第二部分
© . All rights reserved.