繁体   English   中英

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

[英]How do you cast a List of supertypes to a List of subtypes?

例如,假设您有两个类:

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

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

简单地转换为List<TestB>几乎可以工作; 但它不起作用,因为您不能将一个参数的泛型类型转换为另一个参数。 但是,您可以通过中间通配符类型进行转换,并且它是被允许的(因为您可以在通配符类型之间进行转换,只是带有未经检查的警告):

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

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

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

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

使用 Java 8,您实际上可以

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

你真的不能*:

示例取自本 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")添加到您的方法来抑制它。

我认为您的方向是错误的……如果该方法返回TestA对象的列表,那么将它们转换为TestB确实不安全。

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

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


这通常不起作用的原因已经在其他答案中指出:转换是否实际有效取决于原始列表中包含的对象的类型。 如果列表中有对象的类型不是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;
}

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

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

最好的安全方法是实现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
}

您不能将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<TestB> list = new ArrayList<TestB> (
    Arrays.asList (
        testAList.toArray(new TestB[0])
    )
);

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

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

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

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

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

您可以使用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 的提交者。

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 TestB ,这是内存损坏:尝试迭代原始ArrayList<TestB>中的所有TestB会抛出ClassCastException

另一方面,如果您的List<TestB>是使用List.of()创建的,那么它是不可更改的(*1) ,所以没有人可以无意中添加一个TestA到它,所以可以将它转换为List<TestA>


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

如果您有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列表。

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

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

效果与您希望的一样好(Eclipse 会警告您未经检查的演员阵容,这正是您正在做的事情,所以,嗯)。 那么你能用它来解决你的问题吗? 实际上你可以因为这个:

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;

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

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 代码中获取的。

很奇怪,一些工具箱仍然没有提供手动转换列表,例如:

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

当然,这不会一一检查项目,但这正是我们想要避免的,如果我们很清楚我们的实现只提供子类型。

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

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

在内部,两个列表都是List<Object>类型。 出于这个原因,您不能将一个投射到另一个 - 没有什么可投射的。

简单的答案

您不能直接键入 cast。

解决方法

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

所以这是我用来将具有超类型的列表转换为具有子类型的列表的简单解决方法。 We are using stream api which was introduced in java 8 here, using map on our super list we are simply checking if passed argument is instance of our super type the returning the item. 最后,我们正在收集到一个新的列表。 当然,我们必须在这里将结果放入一个新列表中。

这应该可行

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM