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

时间:2009-06-01 03:25:18

标签: java list generics collections casting

例如,假设您有两个类:

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

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

17 个答案:

答案 0 :(得分:533)

简单地投射到List<TestB>几乎可以工作;但它不起作用,因为你不能将一个参数的泛型类型转换为另一个参数。但是,您可以通过中间通配符类型进行强制转换并允许它(因为您可以使用未经检查的警告来强制转换为通配符类型和转换通配符类型):

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

答案 1 :(得分:109)

无法转换泛型,但如果您以其他方式定义列表,则可以将TestB存储在其中:

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

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

答案 2 :(得分:39)

你真的不能*:

示例取自this Java tutorial

假设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")来禁止它。

答案 3 :(得分:27)

使用Java 8,您实际上可以

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

答案 4 :(得分:18)

我认为你向错误的方向投射......如果方法返回TestA个对象的列表,那么将它们转换为TestB是不安全的。

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

答案 5 :(得分:8)

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

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

当然,演员可能有效。您有时会获得有关编译器不可用的类型的信息。在这些情况下,可以投射列表,但一般来说,不推荐

一个人可以......

  • ...投下整个清单或
  • ...投射列表中的所有元素

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

使用Collections#checkedCollection系列方法可以缓解调试这些虚假ClassCastExceptions的问题。

根据类型

过滤列表

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;
}

此方法可用于过滤任意列表(不仅具有关于类型参数的给定Subtype-Supertype关系),如下例所示:

// 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]

答案 6 :(得分:6)

您无法将List<TestB>转发给List<TestA>,因为Steve Kuo提及但您可以将List<TestA>的内容转储到List<TestB>。请尝试以下方法:

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

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

答案 7 :(得分:6)

最安全的方法是在实施中实施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
}

答案 8 :(得分:3)

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

List<TestB> list = new ArrayList<TestB> (
    Arrays.asList (
        testAList.toArray(new TestB[0])
    )
);

答案 9 :(得分:3)

当您转换对象引用时,您只是转换引用的类型,而不是对象的类型。 cast不会改变对象的实际类型。

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

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

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));
}

这比直接支持的语言更冗长,但它有效,你不应该经常这样做。

答案 10 :(得分:2)

如果您有班级TestA的对象,则无法将其投射到TestB。每个TestB都是TestA,但不是相反。

在以下代码中:

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

第二行会抛出ClassCastException

如果对象本身为TestA,则只能投射TestB引用。例如:

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

因此,您可能无法始终将TestA列表添加到TestB列表中。

答案 11 :(得分:2)

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

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

内部两个列表都是List<Object>类型。出于这个原因,你不能把一个投射到另一个 - 没有什么可以施展。

答案 12 :(得分:1)

问题是如果你的方法包含TestB,你的方法不返回TestA列表,那么如果它被正确输入了怎么办?然后这个演员:

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

和你所希望的一样有效(Eclipse警告你一个未经检查的演员,这正是你在做的事情,所以meh)。那么你可以用它来解决你的问题吗?其实你可以因为这个:

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

你究竟要求什么,对吧?或者确切地说:

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

似乎对我来说很好。

答案 13 :(得分:1)

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<Object>投射到List<MyClass>。但您需要注意objectList必须包含与MyClass相同类型的实例。当使用List<T>时,可以考虑此示例。为此,请在构造函数中获取字段Class<T> clazz,并使用它而不是MyClass.class

答案 14 :(得分:1)

您可以在Eclipse Collections中使用selectInstances方法。这将涉及创建一个新的集合,但是不会像使用铸造的公认解决方案那样有效。

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 Collections的提交者。

答案 15 :(得分:1)

奇怪的是,某些工具箱仍无法提供手动转换列表的功能,例如:

@SuppressWarnings({ "unchecked", "rawtypes" })
public static <T extends E, E> List<T> cast(List<E> list) {
    return (List) list;
}

当然,这不会一个接一个地检查项目,但是如果我们完全知道我们的实现仅提供子类型,那正是我们要避免的事情。

答案 16 :(得分:0)

这应该可行

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