关于java:如何将超类型列表转换为子类型列表?

How do you cast a List of supertypes to a List of subtypes?

例如,假设您有两个类:

1
2
public class TestA {}
public class TestB extends TestA{}

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


简单地强制转换到List几乎可以工作;但是它不工作,因为您不能将一个参数的泛型类型强制转换到另一个参数。但是,您可以通过中间通配符类型进行强制转换,这是允许的(因为您可以通过通配符类型进行强制转换,只需发出未选中的警告):

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


泛型的强制转换是不可能的,但是如果您以另一种方式定义列表,则可以在其中存储testb:

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

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


你真的不能:

从Java教程中获取示例

假设有两种类型的AB,这样B extends A。则以下代码正确:

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

前面的代码是有效的,因为BA的子类。现在,ListList发生了什么?

原来List不是List的一个子类,所以我们不能写

1
2
    List b = new ArrayList<>();
    List<A> a = b; // error, List is not of type List<A>

而且,我们甚至不能写

1
2
    List b = new ArrayList<>();
    List<A> a = (List<A>)b; // error, List is not of type List<A>

*:为了使铸造成为可能,我们需要一个ListList的共同母公司:例如List。以下内容有效:

1
2
3
    List b = new ArrayList<>();
    List<?> t = (List)b;
    List<A> a = (List<A>)t;

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


用Java 8,你实际上可以

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


我觉得你选错了方向…如果该方法返回TestA对象的列表,那么将它们强制转换为TestB对象是不安全的。

基本上,您要求编译器允许您对不支持它们的类型TestA执行TestB操作。


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

一般来说,这不起作用的原因已经在其他答案中指出:转换是否实际有效取决于原始列表中包含的对象类型。如果列表中有对象的类型不是TestB类型,而是TestA的不同子类,则强制转换无效。

当然,这些石膏可能是有效的。有时您会得到关于编译器不可用类型的信息。在这些情况下,可以铸造列表,但一般来说,不建议:

一个人可以…

  • …铸造整个清单或
  • …强制转换列表中的所有元素

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

这些伪ClassCastExceptions的调试问题可以用Collections#checkedCollection系列方法来解决。

根据类型筛选列表

List转换为List的一种更安全的类型方法是实际筛选列表,并创建一个只包含具有特定类型的元素的新列表。实施这种方法有一定的自由度(例如,关于处理null条目),但一种可能的实施方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * 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;
}

此方法可用于筛选任意列表(不仅具有与类型参数相关的给定子类型超类型关系),如本例中所示:

1
2
3
4
5
6
7
8
9
10
// 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]


你不能像Steve Kuo提到的那样将List投射到List上,但是你可以将List的内容转储到List中。请尝试以下操作:

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

我没有尝试过这段代码,所以可能会有错误,但我的想法是它应该迭代数据对象,将元素(testb对象)添加到列表中。我希望这对你有用。


最好的安全方法是在实现中实现AbstractList和强制转换项。我创建了ListUtil帮助类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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方法安全投射。例子:

1
2
3
4
5
6
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
}

我唯一知道的方法是复制:

1
2
3
4
5
List<TestB> list = new ArrayList<TestB> (
    Arrays.asList (
        testAList.toArray(new TestB[0])
    )
);


当您转换一个对象引用时,您只是转换引用的类型,而不是对象的类型。强制转换不会更改对象的实际类型。

Java不具有用于转换对象类型的隐式规则。(与原语不同)

相反,您需要提供如何将一种类型转换为另一种类型并手动调用它。

1
2
3
4
5
6
7
8
9
10
11
12
public class TestA {}
public class TestB extends TestA{
    TestB(TestA testA) {
        // build a TestB from a TestA
    }
}

List<TestA> result = ....
List<TestB> data = new List<TestB>();
for(TestA testA : result) {
   data.add(new TestB(testA));
}

这比直接支持的语言更冗长,但它是有效的,您不需要经常这样做。


如果有TestA类对象,则不能将其强制转换为TestB。每个TestB都是一个TestA,但不是相反的方式。

在以下代码中:

1
2
TestA a = new TestA();
TestB b = (TestB) a;

第二行将抛出一个ClassCastException

如果对象本身是TestB,则只能强制转换TestA引用。例如:

1
2
TestA a = new TestB();
TestB b = (TestB) a;

因此,您可能不会总是将EDOCX1的列表(13)强制转换为EDOCX1的列表(14)。


由于类型擦除,这是可能的。你会发现的

1
2
3
List<TestA> x = new ArrayList<TestA>();
List<TestB> y = new ArrayList<TestB>();
x.getClass().equals(y.getClass()); // true

在内部,这两个列表都是List类型。因为这个原因,你不能将一个投射到另一个-没有什么可以投射的。


您可以在Eclipse集合中使用selectInstances方法。这将涉及创建一个新的集合,然而,这样的效率将不如公认的解决方案使用铸造。

1
2
3
4
5
List<CharSequence> parent =
        Arrays.asList("1","2","3", new StringBuffer("4"));
List<String> strings =
        Lists.adapt(parent).selectInstancesOf(String.class);
Assert.assertEquals(Arrays.asList("1","2","3"), strings);

我在示例中加入了StringBuffer,以表明selectInstances不仅会降级类型,而且还会在集合包含混合类型时进行筛选。

注意:我是Eclipse集合的提交者。


问题是,如果您的方法包含一个testb,那么它不会返回一个testa列表,那么如果它是正确键入的呢?然后这个演员:

1
2
3
4
class TestA{};
class TestB extends TestA{};
List<? extends TestA> listA;
List<TestB> listB = (List<TestB>) listA;

工作和你希望的一样好(Eclipse警告你一个未检查的演员表,这正是你所做的,所以Meh)。你能用这个来解决你的问题吗?实际上,你可以这样做:

1
2
3
List<TestA> badlist = null; // Actually contains TestBs, as specified
List<? extends TestA> talist = badlist;  // Umm, works
List<TextB> tblist = (List<TestB>)talist; // TADA!

你到底要什么,对吗?或者更确切地说:

1
List<TestB> tblist = (List<TestB>)(List<? extends TestA>) badlist;

似乎对我来说编译得很好。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyClass {
  String field;

  MyClass(String field) {
    this.field = field;
  }
}

@Test
public void testTypeCast() {
  List<Object> objectList = Arrays.asList(new MyClass("1"), new MyClass("2"));

  Class<MyClass> clazz = MyClass.class;
  List<MyClass> myClassList = objectList.stream()
      .map(clazz::cast)
      .collect(Collectors.toList());

  assertEquals(objectList.size(), myClassList.size());
  assertEquals(objectList, myClassList);
}

这个测试演示了如何将List铸造成List。但您需要注意的是,objectList必须包含与MyClass相同类型的实例。当使用List时,可以考虑这个例子。为此,在构造函数中获取字段Class clazz,并使用它而不是MyClass.class


这应该管用

1
2
3
List<TestA> testAList = new ArrayList<>();
List<TestB> testBList = new ArrayList<>()
testAList.addAll(new ArrayList<>(testBList));