将List<SubClass>转换为List<BaseClass>的最有效方法是什么?

161
我有一个 List<SubClass>,我想将其视为 List<BaseClass>。 好像不应该是问题,因为将 SubClass 强制转换为 BaseClass 很容易,但我的编译器报错说无法进行强制转换。
那么,最好的方法是什么来获取与 List<BaseClass> 相同对象的引用?
现在我只是创建了一个新列表并复制了旧列表:
List<BaseClass> convertedList = new ArrayList<BaseClass>(listOfSubClass)

但据我所了解,这需要创建一个全新的列表。如果可能的话,我想要引用原始列表!


3
你可以在Java中使用泛型来转换列表。以下是一个示例代码:List intList = new ArrayList<>(); intList.add(1); intList.add(2); intList.add(3); // 转换为Object类型的List List objList = (List) (List) intList; // 转换为String类型的List List strList = intList.stream().map(Object::toString).collect(Collectors.toList());请注意,将List转换为不同类型的List可能会导致ClassCastException。因此,请确保您知道要转换的类型,并进行适当的检查和处理。 - lukastymo
10个回答

204

这种类型的赋值语法使用通配符:

List<SubClass> subs = ...;
List<? extends BaseClass> bases = subs;

需要明确的是,List<SubClass> 不能与 List<BaseClass> 相互替换。保留对 List<SubClass> 的引用的代码将期望列表中的每个项目都是 SubClass 类型。如果代码的另一部分将列表称为 List<BaseClass>,则编译器不会在插入 BaseClassAnotherSubClass 时发出警告。但这将导致第一部分代码中的 ClassCastException,因为它假设列表中的所有内容都是 SubClass

在 Java 中,泛型集合与数组的行为不同。数组是协变的;也就是说,可以这样做:

SubClass[] subs = ...;
BaseClass[] bases = subs;
这是允许的,因为数组“知道”其元素的类型。如果有人试图通过“bases”引用在数组中存储不是SubClass实例的内容,则会抛出运行时异常。
通用集合不“知道”它们的组件类型;这些信息在编译时被“擦除”。因此,当发生无效存储时,它们无法引发运行时异常。相反,在集合中读取值时,将在某个遥远且难以关联的代码点处引发ClassCastException。如果您遵循关于类型安全的编译器警告,您将避免这些类型错误在运行时发生。

需要注意的是,使用数组时,如果检索到的对象不是SubClass(或派生类)类型,则不会出现ClassCastException异常,而是在插入时会出现ArrayStoreException异常。 - Axel
特别感谢您解释为什么这两个列表不能被视为相同。 - Riley Lark
Java类型系统假装数组是协变的,但它们实际上不可替换,这由ArrayStoreException证明。 - Paŭlo Ebermann

45

erickson已经解释了为什么你不能这样做,但这里有一些解决方案:

如果你只想从你的基本列表中获取元素,则你的接收方法原则上应声明为List<? extends BaseClass>

但如果它不是这样的,而且你无法更改它,你可以使用Collections.unmodifiableList(...)来包装列表,它允许返回参数的超类型的列表。(通过在插入尝试时抛出UnsupportedOperationException来避免类型安全问题。)


PS:add(null)是合法的。 - Long

15

完成这个任务最有效且同时安全的方法如下:

List<S> supers = List.copyOf( descendants );
这个函数的文档在这里:oracle.com - Java SE 19 docs - List.copyOf() 文档中说明此函数“自从:10”存在。
使用此函数具有以下优点:
- 这是一个整洁的一行代码。 - 它不会产生任何警告。 - 它不需要任何类型转换。 - 它不需要笨拙的 List<? extends S> 构造。 - 它不一定会制作副本!!! - 最重要的是:它做正确的事情。(它是安全的。)
为什么这是正确的事情?
如果您查看 List.copyOf() 的源代码,您将看到它的工作方式如下:
- 如果您的列表是使用 List.of() 创建的,则它将进行强制转换并返回而没有复制它。 - 否则(例如,如果您的列表是一个 ArrayList()),它将创建一个副本并返回它。
如果原始的List<D> 是一个 ArrayList<D>,那么为了获得一个 List<S>,必须制作 ArrayList 的副本如果使用强制转换,则可能会意外地添加 SList<S> 中,导致您的原始 ArrayList<D> 包含一个 SD 中,这是一种灾难性的情况,称为堆污染Wikipedia):尝试迭代原始 ArrayList<D> 中的所有 D 将抛出 ClassCastException
另一方面,如果您使用 List.of() 创建了原始的 List<D>,则它是不可更改的(*1),因此可以简单地将其强制转换为 List<S>,因为实际上没有人能够在 D 中添加 SList.copyOf() 为您处理此决策逻辑。
(*1) 当引入这些列表时,它们被称为“不可变的”;后来他们意识到称呼它们为不可变是错误的,因为集合不能保证其中包含的元素是不可变的;因此,他们更改了文档以将它们称为“不可修改”的;但是,“不可修改”在这些列表引入之前已经有了意义,它意味着“您无法修改的我的列表的不可修改视图,但我仍然可以随意更改并且这些变化会非常明显地显示给您看”。因此,不可变或不可修改都不正确。我喜欢称它们为“表面上不可变”,因为它们并不是深度不可变的,但这可能会引起一些争议,所以我只称它们为“不可更改”的妥协。

1
很好的描述,为什么有人不能将ArrayList<S>强制转换为ArrayList<D>,但List.of()可以被转换。 - Lubo
类似的情况也适用于 Collections.unmodifiableList(),原因相似。 - Johannes Kuhn

14

正如@erickson所解释的那样,如果你真的想要一个对原始列表的引用,确保没有任何代码向该列表插入了任何内容,如果你将来想在其原始声明下再次使用它。最简单的方法是将其转换为一个普通的非泛型列表:

List<BaseClass> baseList = (List)new ArrayList<SubClass>();

如果您不知道List发生了什么事情,我不建议这样做,并建议您更改需要接受所拥有的List的代码。


6

我错过了你只是使用双重转换来强制转换原始列表的答案。因此,以下是完整的答案:

List<BaseClass> baseList = (List<BaseClass>)(List<?>)subList;

没有复制,操作速度快。然而,你在这里欺骗编译器,所以你必须确保绝对不要以某种方式修改列表,使得subList开始包含不同子类型的项目。当处理不可变列表时,通常不会出现这个问题。


3
以下是一个有效的代码片段,它可以构建一个新的数组列表,但JVM对象创建的开销很小。
我看到其他回答过于复杂。
List<BaseClass> baselist = new ArrayList<>(sublist);

1
谢谢您提供这段代码片段,它可能会立即提供一些帮助。通过展示为什么这是一个好的问题解决方案,适当的解释将极大地提高其教育价值,并使其对未来具有类似但不完全相同的问题的读者更有用。请编辑您的答案以添加解释,并指出适用的限制和假设。特别是,这与创建新列表的问题中的代码有何不同? - Toby Speight
开销并不小,因为它还必须(浅)复制每个引用。因此,它随着列表的大小而扩展,因此它是O(n)操作。 - john16384

2
你尝试的做法非常有用,我发现在我编写代码时经常需要这样做。
大多数Java程序员实现 getConvertedList() 时会毫不犹豫地分配一个新的 ArrayList<>(),将其填充为原始列表中的所有元素,并返回它。令人欣慰的是,全球数百万台机器上运行的Java代码所消耗的约30%的时钟周期仅用于创建此类无用的ArrayList副本,这些副本在它们创建后的微秒内被垃圾回收。
当然,解决此问题的方法是向下转换集合。以下是如何执行此操作:
static <T,U extends T> List<T> downCastList( List<U> list )
{
    @SuppressWarnings( "unchecked" )
    List<T> result = (List<T>)list;
    return result;
}

由于Java语言的一种扭曲,中间变量result是必需的:
  • return (List<T>)list;会产生一个“未经检查的转换”警告;

  • 为了抑制警告,需要使用@SuppressWarnings("unchecked")注释,并且良好的编程实践要求它必须放置在最小可能的范围内,即单个语句而不是方法。

  • 在Java中,注释不能放在任何代码行上;它必须放在某个实体上,例如类、字段、方法等。

  • 幸运的是,一个这样的可注释实体是局部变量声明。

  • 因此,我们必须声明一个新的局部变量来使用@SuppressWarnings注释,并返回该变量的值。(无论如何,它都应该被JIT优化掉。)

注意:这个答案刚刚被点赞了,很酷,但如果你正在阅读这篇文章,请确保也阅读我对同一问题的第二个、更近期的答案:https://dev59.com/1G445IYBdhLWcg3wBVrz#72195980


这个方法不应该叫做 upCastList() 吗?毕竟,你是从子类向上转型到父类。 否则,这个方法非常有用。我将它作为扩展方法(使用 Lombok)添加到了 List 中,现在我可以简单地使用 superList = subList.upCast(); - marc.guenther

1
如何将所有元素转换?它将创建一个新列表,但将引用旧列表中的原始对象。
List<BaseClass> convertedList = listOfSubClass.stream().map(x -> (BaseClass)x).collect(Collectors.toList());

这是完整的代码片段,可用于将子类列表转换为超类。 - kanaparthikiran
1
在调用.map()之前,是否有必要在列表上调用.stream()呢? - giulp
是的,Giulp。已修复(添加了stream())。 - drordk

-2

这样应该也可以:

public static <T> List<T> convertListWithExtendableClasses(
    final List< ? extends T> originalList,
    final Class<T> clazz )
{
    final List<T> newList = new ArrayList<>();
    for ( final T item : originalList )
    {
        newList.add( item );
    }// for
    return newList;
}

其实不太清楚 Eclipse 为什么需要 clazz..


-2

这是使用泛型的完整工作代码,将子类列表转换为超类。

调用者方法传递子类类型

List<SubClass> subClassParam = new ArrayList<>();    
getElementDefinitionStatuses(subClassParam);

接受基类任何子类型的被调用方法

private static List<String> getElementDefinitionStatuses(List<? extends 
    BaseClass> baseClassVariableName) {
     return allElementStatuses;
    }
}

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