Most efficient way to cast List<SubClass> to List<BaseClass>
Asked Answered
G

10

166

I have a List<SubClass> that I want to treat as a List<BaseClass>. It seems like it shouldn't be a problem since casting a SubClass to a BaseClass is a snap, but my compiler complains that the cast is impossible.

So, what's the best way to get a reference to the same objects as a List<BaseClass>?

Right now I'm just making a new list and copying the old list:

List<BaseClass> convertedList = new ArrayList<BaseClass>(listOfSubClass)

But as I understand it that has to create an entirely new list. I'd like a reference to the original list, if possible!

Graduate answered 22/2, 2011 at 18:8 Comment(1)
the answer you have here: #663008Chondroma
H
206

The syntax for this sort of assignment uses a wildcard:

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

It's important to realize that a List<SubClass> is not interchangeable with a List<BaseClass>. Code that retains a reference to the List<SubClass> will expect every item in the list to be a SubClass. If another part of code referred to the list as a List<BaseClass>, the compiler will not complain when a BaseClass or AnotherSubClass is inserted. But this will cause a ClassCastException for the first piece of code, which assumes that everything in the list is a SubClass.

Generic collections do not behave the same as arrays in Java. Arrays are covariant; that is, it is allowed to do this:

SubClass[] subs = ...;
BaseClass[] bases = subs;

This is allowed, because the array "knows" the type of its elements. If someone attempts to store something that isn't an instance of SubClass in the array (via the bases reference), a runtime exception will be thrown.

Generic collections do not "know" their component type; this information is "erased" at compile time. Therefore, they can't raise a runtime exception when an invalid store occurs. Instead, a ClassCastException will be raised at some far distant, hard-to-associate point in code when a value is read from the collection. If you heed compiler warnings about type safety, you will avoid these type errors at runtime.

Havard answered 22/2, 2011 at 18:12 Comment(3)
Should note that with Arrays, instead of ClassCastException when retrieving an object that is not of type SubClass (or derived), you will get an ArrayStoreException when inserting.Steelworker
Thanks especially for the explanation of why the two lists cannot be considered the same.Graduate
The Java type system pretends that arrays are covariant, but they aren't really substitutable, proven by the ArrayStoreException.Tetanus
C
45

erickson already explained why you can't do this, but here some solutions:

If you only want to take elements out of your base list, in principle your receiving method should be declared as taking a List<? extends BaseClass>.

But if it isn't and you can't change it, you can wrap the list with Collections.unmodifiableList(...), which allows returning a List of a supertype of the argument's parameter. (It avoids the typesafety problem by throwing UnsupportedOperationException on insertion tries.)

Carven answered 22/2, 2011 at 21:5 Comment(1)
PS: add(null) is legalTwofaced
I
16

The most efficient and at the same time safe way of accomplishing this is as follows:

List<S> supers = List.copyOf( descendants );

The documentation of this function is here: oracle.com - Java SE 19 docs - List.copyOf() The documentation states that this function exists "Since: 10".

The use of this function has the following advantages:

  • It is a neat one-liner.
  • It produces no warnings.
  • It does not require any typecast.
  • It does not require the cumbersome List<? extends S> construct.
  • It does not necessarily make a copy !!!
  • Most importantly: it does the right thing. (It is safe.)

Why is this the right thing?

If you look at the source code of List.copyOf() you will see that it works as follows:

  • If your list was created with List.of(), then it will do the cast and return it without copying it.
  • Otherwise, (e.g. if your list is an ArrayList(),) it will create a copy and return it.

If your original List<D> is an ArrayList<D>, then in order to obtain a List<S>, a copy of the ArrayList must be made. If a cast was made instead, it would be opening up the possibility of inadvertently adding an S into that List<S>, causing your original ArrayList<D> to contain an S among the Ds, which is a disastrous situation known as Heap Pollution (Wikipedia): attempting to iterate all the Ds in the original ArrayList<D> would throw a ClassCastException.

On the other hand, if your original List<D> has been created using List.of(), then it is unchangeable(*1), so it is okay to simply cast it to List<S>, because nobody can actually add an S among the Ds.

List.copyOf() takes care of this decision logic for you.


(*1) when these lists were first introduced they were called "immutable"; later they realized that it is wrong to call them immutable, because a collection cannot be immutable, since it cannot vouch for the immutability of the elements that it contains; so they changed the documentation to call them "unmodifiable" instead; however, "unmodifiable" already had a meaning before these lists were introduced, and it meant "an unmodifiable to you view of my list which I am still free to mutate as I please, and the mutations will be very visible to you". So, neither immutable or unmodifiable is correct. I like to call them "superficially immutable" in the sense that they are not deeply immutable, but that may ruffle some feathers, so I just called them "unchangeable" as a compromise.

Illgotten answered 11/5, 2022 at 5:47 Comment(2)
Nice description, why someone cannot cast ArrayList<S> to ArrayList<D>, but List.of() can be casted.Vampirism
A similar thing applies to Collections.unmodifiableList(), for similar reasons.Restricted
N
14

As @erickson explained, if you really want a reference to the original list, make sure no code inserts anything to that list if you ever want to use it again under its original declaration. The simplest way to get it is to just cast it to a plain old ungeneric list:

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

I would not recommend this if you don't know what happens to the List and would suggest you change whatever code needs the List to accept the List you have.

Neurasthenia answered 22/2, 2011 at 18:45 Comment(0)
B
6

I missed the answer where you just cast the original list, using a double cast. So here it is for completeness:

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

Nothing is copied, and the operation is fast. However, you are tricking the compiler here so you must make absolutely sure to not modify the list in such a way that the subList starts containing items of a different sub type. When dealing with immutable lists this is usually not an issue.

Beadsman answered 22/2, 2020 at 18:50 Comment(0)
L
3

Below is a useful snippet that works. It constructs a new array list but JVM object creation over head is in-significant.

I saw other answers are un-necessarily complicated.

List<BaseClass> baselist = new ArrayList<>(sublist);
Libration answered 19/7, 2017 at 7:3 Comment(2)
Thank you for this code snippet, which may provide some immediate help. A proper explanation would greatly improve its educational value by showing why this is a good solution to the problem, and would make it more useful to future readers with similar, but not identical, questions. Please edit your answer to add explanation, and give an indication of what limitations and assumptions apply. In particular, how does this differ from the code in the question that makes a new list?Garmaise
The overhead is not insignifcant, as it also has to (shallow) copy every reference. As such it scales with the size of the list, so it is an O(n) operation.Beadsman
I
2

What you are trying to do is very useful and I find that I need to do it very often in code that I write.

Most java programmers would not think twice before implementing getConvertedList() by allocating a new ArrayList<>(), populating it with all the elements from the original list, and returning it. I enjoy entertaining the thought that about 30% of all clock cycles consumed by java code running on millions of machines all over the planet is doing nothing but creating such useless copies of ArrayLists which are garbage-collected microseconds after their creation.

The solution to this problem is, of course, down-casting the collection. Here is how to do it:

static <T,U extends T> List<T> downCastList( List<U> list )
{
    @SuppressWarnings( "unchecked" )
    List<T> result = (List<T>)list;
    return result;
}

The intermediate result variable is necessary due to a perversion of the java language:

  • return (List<T>)list; would produce an "unchecked cast" warning;

  • in order to suppress the warning, you need a @SuppressWarnings( "unchecked" ) annotation, and good programming practices mandate that it must be placed in the smallest possible scope, which is the individual statement, not the method.

  • in java an annotation cannot be placed on just any line of code; it must be placed on some entity, like a class, a field, a method, etc.

  • luckily, one such annotatable entity is a local variable declaration.

  • therefore, we have to declare a new local variable to use the @SuppressWarnings annotation on it, and then return the value of that variable. (It should not matter anyway, because it should be optimized away by the JIT.)

Note: this answer was just upvoted, which is cool, but if you are reading this, please be sure to also read the second, more recent answer of mine to this same question: https://mcmap.net/q/54704/-most-efficient-way-to-cast-list-lt-subclass-gt-to-list-lt-baseclass-gt

Illgotten answered 6/4, 2017 at 18:30 Comment(1)
Shouldn't this be called upCastList()? After all, you are casting from a sub class to a super class? Otherwise, this is super helpful. I added this as an extension method (using Lombok) to List, so now I can simply superList = subList.upCast();Harrar
N
1

How about casting all elements. It will create a new list, but will reference the original objects from the old list.

List<BaseClass> convertedList = listOfSubClass.stream().map(x -> (BaseClass)x).collect(Collectors.toList());
Nomadize answered 27/5, 2018 at 15:39 Comment(3)
This is the complete piece of code that works to cast sub class list to super class.Esthete
wouldn't be necessary to call .stream() on the list before calling .map() ?Largo
Yes giulp. Fixed (added stream()).Nomadize
U
-2

Something like this should work too:

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

Don't really know why clazz is needed in Eclipse..

Unveil answered 25/3, 2015 at 23:6 Comment(0)
E
-2

This is the complete working piece of code using Generics, to cast sub class list to super class.

Caller method that passes subclass type

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

Callee method that accepts any subtype of the base class

private static List<String> getElementDefinitionStatuses(List<? extends 
    BaseClass> baseClassVariableName) {
     return allElementStatuses;
    }
}
Esthete answered 5/9, 2018 at 20:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.