如何将超类型列表转换为子类型列表?

309
例如,假设你有两个类:

For example, lets say you have two classes:

public class TestA {}
public class TestB extends TestA{}

我有一个方法返回一个List<TestA>,我想将列表中的所有对象转换为TestB,以便最终得到一个List<TestB>

20个回答

667

直接将类型转换为List<TestB>几乎可以工作;但是它不起作用,因为您不能将一个参数的泛型类型强制转换为另一个参数。但是,您可以通过中间的通配符类型进行转换,这样就允许了(因为您可以将通配符类型强制转换为其他类型,并带有未检查的警告):

List<TestB> variable = (List<TestB>)(List<?>) collectionOfListA;

98
恕我直言,我认为这种做法有些取巧——您正在规避Java试图提供的类型安全性。请至少查看一下此Java教程(http://docs.oracle.com/javase/tutorial/java/generics/subtyping.html),并考虑为什么会出现这个问题,然后再采用这种方式进行修复。 - jfritz42
84
@jfritz42 我不会称之为“躲避”类型安全。在这种情况下,你拥有Java没有的知识。这就是强制类型转换的作用。 - Planky
6
这让我可以写出以下代码: List<String> myList = (List<String>)(List<?>)(new ArrayList<Integer>()); 然而,这会在运行时崩溃。我更倾向于使用以下代码: List<? extends Number> myList = new ArrayList<Integer>(); - Bentaye
2
我完全同意 @jfritz42 的观点。我也遇到了同样的问题,看了他提供的URL后,成功解决了我的问题,而不需要进行强制转换。 - Sam Orozco
1
@jfritz42 这与将对象转换为整数没有功能上的区别,编译器允许这样做。 - kcazllerraf
这与List<TestB> variable = (List<TestB>)(Object) collectionOfListA;有什么不同吗? - wlnirvana

126

泛型类型的强制转换是不可能的,但如果你用另一种方式定义列表,就可以将TestB存储在其中:

List<? extends TestA> myList = new ArrayList<TestA>();

当您使用列表中的对象时,仍然需要进行类型检查。


23
这甚至不是有效的Java语法。 - newacct
2
这是正确的,但它更多地是为了说明而不是事实上的正确。 - Salandur
我有以下方法:createSingleString(List<? extends Object> objects)。在这个方法中,我调用String.valueOf(Object)将列表合并为一个字符串。当输入是List<Long>、List<Boolean>等时,它的效果非常好。谢谢! - Chris Sprague
2
如果TestA是接口,会怎样? - Suvitruf - Andrei Apanasik
10
你能提供一个 MCVE,以便演示这个问题的实际工作情况吗?我得到了 Cannot instantiate the type ArrayList<? extends TestA> 的错误。 - Nateowami

50

使用Java 8,您实际上可以

List<TestB> variable = collectionOfListA
    .stream()
    .map(e -> (TestB) e)
    .collect(Collectors.toList());

37
这实际上是在转换列表吗?因为它更像是一系列操作,用于迭代整个列表,将每个元素转换,然后将它们添加到所需类型的新实例化列表中。换句话说,你不可以使用Java7、一个new List<TestB>、循环和转换来做到这一点吗? - Patrick M
9
根据 OP 的说法,“我想将列表中的所有对象都转换一下”,实际上,这是为数不多的几个回答之一,回答了按照问题陈述的要求,即使它不是人们在这里浏览时想要的问题。 - Luke Usherwood
在我的情况下,我需要进行一些筛选,因此循环遍历列表不是我的问题。 - Thanh-Nhon Nguyen

49

你真的不能*:

示例来自这个Java教程

假设有两种类型AB,使得B extends A。 那么以下代码是正确的:

    B b = new B();
    A a = b;

上述代码有效是因为BA的子类。那么对于List<A>List<B>会发生什么呢?
事实证明,List<B>不是List<A>的子类,因此我们不能编写下面这样的代码:
    List<B> b = new ArrayList<>();
    List<A> a = b; // error, List<B> is not of type List<A>

此外,我们甚至不能编写代码。
    List<B> b = new ArrayList<>();
    List<A> a = (List<A>)b; // error, List<B> is not of type List<A>

为了实现强制类型转换,我们需要找到一个通用的父类来表示 List<A>List<B>,比如 List<?>。以下是有效的示例:
    List<B> b = new ArrayList<>();
    List<?> t = (List<B>)b;
    List<A> a = (List<A>)t;

然而,你会收到一个警告。你可以通过在方法中添加 @SuppressWarnings("unchecked") 来抑制它。


2
此外,使用此链接中的材料可以避免未经检查的转换,因为编译器可以解密整个转换过程。 - Ryan H

20

我认为您的转换方向不正确......如果方法返回一个TestA对象列表,那么将它们强制转换为TestB是不安全的。

基本上,您正在请求编译器让您对不支持TestB操作的类型TestA执行它们。


12

由于这个问题被广泛引用,目前的答案主要解释了为什么它不起作用(或提出了我永远不想在生产代码中看到的hacky,危险的解决方案),因此我认为添加另一个答案以显示陷阱和可能的解决方法是合适的。


为什么通常情况下这种方法不起作用已经在其他答案中指出:是否可以进行转换实际上取决于原始列表中包含的对象的类型。当列表中存在其类型不属于TestB类型,而是其它TestA字类时,强制类型转换就无效


当然,强制类型转换可能有效。有时您拥有有关类型的信息,但编译器无法获取该信息。在这些情况下,可以进行列表转换,但通常不建议这样做:

可以选择以下两种方式之一...

  • ... 转换整个列表或
  • ... 转换列表中的每个元素

第一种方法(当前接受的答案)的影响是微妙的。乍一看似乎可以正常工作。但是如果输入列表中有错误的类型,则会抛出ClassCastException,可能在代码中完全不同的位置,并且很难调试和找出哪个错误元素滑入了列表。最糟糕的问题是有人甚至可能在列表被强制转换后添加无效元素,使得调试变得更加困难。

解决这些虚假ClassCastExceptions的问题可以通过使用集合框架中的Collections#checkedCollection方法系列来缓解。


根据类型筛选列表

List<Supertype> 转换为 List<Subtype> 的更安全的方式是实际上过滤列表,并创建一个包含仅具有特定类型元素的新列表。对于此类方法的实现(例如,关于null条目的处理),存在一些灵活性,但是可能的一种实现如下:

/**
 * Filter the given list, and create a new list that only contains
 * the elements that are (subtypes) of the class c
 * 
 * @param listA The input list
 * @param c The class to filter for
 * @return The filtered list
 */
private static <T> List<T> filter(List<?> listA, Class<T> c)
{
    List<T> listB = new ArrayList<T>();
    for (Object a : listA)
    {
        if (c.isInstance(a))
        {
            listB.add(c.cast(a));
        }
    }
    return listB;
}

该方法可用于过滤任意列表(不仅限于具有有关类型参数的子类型-超类型关系),例如:

// A list of type "List<Number>" that actually 
// contains Integer, Double and Float values
List<Number> mixedNumbers = 
    new ArrayList<Number>(Arrays.asList(12, 3.4, 5.6f, 78));

// Filter the list, and create a list that contains
// only the Integer values:
List<Integer> integers = filter(mixedNumbers, Integer.class);

System.out.println(integers); // Prints [12, 78]

7

正如Steve Kuo所提到的,您无法将List<TestB>转换为List<TestA>,但是您可以将List<TestA>的内容转储到List<TestB>中。请尝试以下操作:

List<TestA> result = new List<TestA>();
List<TestB> data = new List<TestB>();
result.addAll(data);

我没有尝试过这段代码,所以可能会有错误,但是它的思路是遍历数据对象,并将元素(TestB 对象)添加到列表中。希望对你有用。


为什么这个被踩了?它对我很有用,而且我看不到任何关于踩的解释。 - zod
8
这篇文章肯定没有错误。但是最终询问问题的人需要TestB列表。在这种情况下,这篇回答会导致误解。你可以通过调用List<TestA>.addAll(List<TestB>)将List<TestB>中的所有元素添加到List<TestA>中。但你不能使用List<TestB>.addAll(List<TestA>)。 - prageeth

7

最安全的方法是实现一个 AbstractList 并在实现中转换项目。我创建了一个 ListUtil 帮助类:

public class ListUtil
{
    public static <TCastTo, TCastFrom extends TCastTo> List<TCastTo> convert(final List<TCastFrom> list)
    {
        return new AbstractList<TCastTo>() {
            @Override
            public TCastTo get(int i)
            {
                return list.get(i);
            }

            @Override
            public int size()
            {
                return list.size();
            }
        };
    }

    public static <TCastTo, TCastFrom> List<TCastTo> cast(final List<TCastFrom> list)
    {
        return new AbstractList<TCastTo>() {
            @Override
            public TCastTo get(int i)
            {
                return (TCastTo)list.get(i);
            }

            @Override
            public int size()
            {
                return list.size();
            }
        };
    }
}

您可以使用cast方法来盲目地将列表中的对象转换,而使用convert方法进行安全转换。 例如:
void test(List<TestA> listA, List<TestB> listB)
{
    List<TestB> castedB = ListUtil.cast(listA); // all items are blindly casted
    List<TestB> convertedB = ListUtil.<TestB, TestA>convert(listA); // wrong cause TestA does not extend TestB
    List<TestA> convertedA = ListUtil.<TestA, TestB>convert(listB); // OK all items are safely casted
}

5
我唯一知道的方法就是复制:
List<TestB> list = new ArrayList<TestB> (
    Arrays.asList (
        testAList.toArray(new TestB[0])
    )
);

2
我认为这个不会给编译器警告,相当奇怪。 - Simon Forsberg
1
这是数组的一个怪异问题。唉,Java 数组太没用了。但我认为它肯定不会比其他答案更糟糕,而且更易读。我认为这并不比强制转换更不安全。 - Thufir
我会这样做。虽然有点令人惊讶,但我也会加上注释来解释为什么它能够工作。 - Ole V.V.

4

2022年的回答

将一个超类型的列表转换为子类型的列表是毫无意义的,没有人应该尝试或者考虑这样做。如果你认为你的代码需要这样做,你需要重写你的代码,让它不需要这样做。

大多数访问这个问题的人可能希望做相反的事情,这实际上是有意义的:

将子类型的列表转换为超类型的列表。

我发现最好的方法如下:

List<TestA> testAs = List.copyOf( testBs );

这样做有以下优点:

  • 它是一个整洁的单行代码。
  • 它不会产生任何警告。
  • 如果您的列表是使用 List.of() 创建的,则不会复制 !!!
  • 最重要的是:它做了正确的事情。

为什么这是正确的做法?

如果您查看 List.copyOf() 的源代码,您会发现它的工作方式如下:

  • 如果您的列表是使用 List.of() 创建的,则进行类型转换并返回而不复制它。
  • 否则(例如,如果您的列表是一个 ArrayList(),)它将创建一个副本并返回它。

如果您的 List<TestB> 是一个 ArrayList<TestB>,那么必须制作 ArrayList 的副本。如果您将 ArrayList<TestB> 强制转换为 List<TestA>,则可能会无意中将 TestA 添加到该 List<TestA> 中,然后导致您的原始 ArrayList<TestB> 包含一个 TestA,这是内存破坏:尝试迭代原始 ArrayList<TestB> 中的所有 TestB 将抛出一个 ClassCastException

另一方面,如果您的 List<TestB> 是使用 List.of() 创建的,则它是不可更改的(*1),因此没有人会无意中向其中添加 TestA,因此只需将其强制转换为 List<TestA> 即可。


(*1) 当首次引入这些列表时,它们被称为“不可变的”;后来他们意识到称它们为不可变是错误的,因为集合不能被视为不可变,因为它不能保证其包含的元素的不可变性;因此,他们更改了文档以将它们称为“不可修改的”。然而,“不可修改的”在这些列表引入之前就已经有了一种含义,它意味着“我的列表的一种对你不可修改的视图,但我仍然可以随意进行修改,并且这些修改对你非常明显”。因此,既不是不可变的也不是不可修改的都是正确的。我喜欢称它们为“表面上不可变”,意思是它们不是深度不可变的,但这可能会引起一些麻烦,所以我将它们称为“unchangeable”,作为一个妥协。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接