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

例如,假设你有两个类:

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

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

327873 次浏览

你真的不能*:

示例取自Java教程

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

    B b = new B();
A a = b;
上面的代码是有效的,因为BA的子类。 现在,List<A>List<B>发生了什么?< / p >

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")来抑制它。

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

基本上,你是在要求编译器让你对不支持的TestA类型执行TestB操作。

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

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

在内部,两个列表都是List<Object>类型。出于这个原因,你不能将一个转换为另一个——没有什么可以转换的。

如果你有一个TestA类的对象,你不能将它强制转换为TestB。每个TestB都是TestA,而不是相反。

在以下代码中:

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

第二行将抛出ClassCastException

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

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

因此,你不能总是将TestA的列表转换为TestB的列表。

你不能像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对象)添加到List中。我希望这对你有用。

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

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

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

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

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

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

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

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

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

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

最安全的方法是实现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方法来安全强制转换。 例子:< / p >
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
}

在Java 8中,实际上可以做到

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

由于这是一个被广泛引用的问题,目前的答案主要解释了为什么它不起作用(或者提出了我永远不希望在生产代码中看到的俗套、危险的解决方案),我认为应该添加另一个答案,展示陷阱和可能的解决方案。


在其他回答中已经指出了一般情况下这不能工作的原因:转换是否实际有效取决于原始列表中包含的对象的类型。当列表中有对象的类型不是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]

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

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;

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

这应该是可行的

List<TestA> testAList = new ArrayList<>();
List<TestB> testBList = new ArrayList<>()
testAList.addAll(new ArrayList<>(testBList));
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

更新

实际上,在强类型Java AFAIK中,你不能从超类型转换为子类型。但是可以引入超类型实现的接口。

interface ITest {
}


class TestA implements ITest {
}


class TestB extends TestA {
}


public class Main {


public static void main(String[] args) {
List<ITest> testAList = Arrays.asList(new TestA(), new TestA());


Class<ITest> clazz = ITest.class;
List<ITest> testBList = testAList.stream()
.map(clazz::cast)
.collect(Collectors.toList());


System.out.println(testBList.size());
System.out.println(testAList);
}
}

或者可以从子类型转换为超类型。是这样的:

class TestA {
}


class TestB extends TestA {
}


public class Main {


public static void main(String[] args) {
var a = new TestA();
var b = new TestB();
System.out.println(((TestA) b));


List<TestB> testBList = Arrays.asList(new TestB(), new TestB());


Class<TestA> clazz = TestA.class;
List<TestA> testAList = testBList.stream()
.map(clazz::cast)
.collect(Collectors.toList());


System.out.println(testAList.size());
System.out.println(testAList);
}
}

供参考,我最初的答案指出了与generics一起使用它的可能性。事实上,我从Hibernate DAO代码中获取了它。

你可以在Eclipse集合中使用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的提交者。

很奇怪,手动强制转换列表仍然没有提供一些实现如下功能的工具箱:

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

当然,这不会逐个检查项,但如果我们清楚地知道我们的实现只提供子类型,那么这正是我们在这里要避免的。

2022年回答

将一组超类型的List转换为一组子类型的List是没有意义的,任何人都不应该尝试或甚至考虑这样做。如果你认为你的代码需要这样做,你需要重写你的代码,使它不需要这样做。

对于这个问题,大多数访问者可能想要做相反的事情,这实际上是有道理的:

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

我发现的最好的方法是:

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

这样做有以下优点:

  • 这是一个简洁的一行代码
  • 它不会产生警告
  • 它不会复制如果你的列表是用List.of()创建的!!
  • 最重要的是:它做了正确的事情。

为什么这是对的?

如果你看一下List.copyOf()的源代码,你会发现它的工作原理如下:

  • 如果你的列表是用List.of()创建的,那么它将执行强制转换并返回,而不复制它。
  • 否则,(例如,如果你的列表是ArrayList(),)它将创建一个副本并返回它。

如果你的List<TestB>是一个ArrayList<TestB>,那么ArrayList的副本ArrayList<TestB>3如果你要将ArrayList<TestB>转换为List<TestA>,你将有可能无意中将TestA添加到那个List<TestA>中,这将导致你的原始ArrayList<TestB>在__abc9中包含一个TestA,这是内存破坏:试图迭代原始ArrayList<TestB>中的所有__abc9将抛出一个ArrayList<TestB>2。

另一方面,如果你的List<TestB>是用List.of()创建的,那么它是不可更改的__abc4,所以没有人会无意中向它添加TestA,所以将它强制转换为List<TestA>是可以的。


(*1)当这些名单第一次出现时,它们被称为“不可改变的”;后来他们意识到称它们为不可变是错误的,因为一个集合不可能是不可变的,因为它不能保证它所包含的元素是不可变的;所以他们修改了文档,称其为“不可修改”。相反;然而,“unmodifiable"在这些列表被介绍之前就已经有了一个含义,它意味着“一个不可修改的对你我的列表,我仍然可以随心所欲地突变,并且突变将对你非常明显”。所以,不可变和不可修改都不是正确的。我喜欢称它们为“表面上不可改变”。从某种意义上说,它们并不是完全不可改变的,但这可能会激怒一些人,所以我只是称它们为“不可改变的”。作为妥协。

简单的答案

你不能直接类型转换。

解决方案

 public <T> List<T> getSubItemList(List<IAdapterImage> superList, Class<T> clazz) {
return superList.stream()
.map(item -> clazz.isInstance(item) ? clazz.cast(item) : null)
.collect(Collectors.toList());
}

使用

private final List<IAdapterImage> myList = new ArrayList<>();


List<SubType> subTypeList = getSubItemList(myList,SubType.class);

所以这是一个简单的工作,我用来转换我的列表与超级类型到列表与子类型。我们使用的是java 8中引入的流api,在我们的超列表上使用map,我们只是检查传递的参数是否是我们的超类型的实例,返回项目。最后,我们收集成一个新的列表。当然,我们需要把result放到一个新的列表中。

我必须用公司系统的方法来实现:

interface MyInterface{}
Class MyClass implements MyInterface{}

该方法接收一个接口列表,也就是说,我已经实例化了一个接口列表

private get(List<MyInterface> interface) {
List<MyClass> myClasses = interface.stream()
.filter(MyClass.class::isInstance)
.map(MyClass.class::cast)
.collect(Collectors.toList());
}