Наиболее эффективный способ приведения List<SubClass> к List<BaseClass>

У меня есть List<SubClass> что я хочу рассматривать как List<BaseClass>, Кажется, что это не должно быть проблемой, так как SubClass к BaseClass совсем несложно, но мой компилятор жалуется, что приведение невозможно.

Итак, как лучше всего получить ссылку на те же объекты, что и List<BaseClass>?

Сейчас я просто создаю новый список и копирую старый список:

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

Но, насколько я понимаю, это должно создать совершенно новый список. Я хотел бы ссылку на оригинальный список, если это возможно!

11 ответов

Решение

Синтаксис для этого вида назначения использует подстановочный знак:

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

Важно понимать, что List<SubClass> не взаимозаменяемы с List<BaseClass>, Код, который сохраняет ссылку на List<SubClass> будет ожидать, что каждый элемент в списке будет SubClass, Если другая часть кода упоминается в списке как List<BaseClass>, компилятор не будет жаловаться, когда BaseClass или же AnotherSubClass вставлен. Но это вызовет ClassCastException для первого куска кода, который предполагает, что все в списке SubClass,

Общие коллекции не ведут себя так же, как массивы в Java. Массивы ковариантны; то есть разрешено делать это:

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

Это разрешено, потому что массив "знает" тип своих элементов. Если кто-то пытается сохранить что-то, что не является экземпляром SubClass в массиве (через bases ссылка), будет исключение во время выполнения.

Общие коллекции не "знают" свой тип компонента; эта информация "стирается" во время компиляции. Поэтому они не могут вызвать исключение времени выполнения, когда происходит неправильное хранилище. Вместо этого ClassCastException будет возникать в какой-то далекой, трудносвязываемой точке кода, когда значение будет считано из коллекции. Если вы учитываете предупреждения компилятора о безопасности типов, вы избежите этих ошибок типов во время выполнения.

Эриксон уже объяснил, почему вы не можете сделать это, но вот некоторые решения:

Если вы хотите только удалить элементы из своего базового списка, в принципе, ваш метод получения должен быть объявлен как получение List<? extends BaseClass>,

Но если это не так, и вы не можете изменить его, вы можете обернуть список с помощью Collections.unmodifiableList(...), что позволяет вернуть список супертипа параметра аргумента. (Это позволяет избежать проблемы безопасности типов, создавая исключение UnsupportedOperationException при попытках вставки.)

Еще в 2017 году я опубликовал еще один ответ на этот вопрос , который является хорошим ответом, но вчера я обнаружил лучший способ сделать это.

Лучший способ, который я нашел, заключается в следующем:

(Примечание: S = суперкласс, D = потомок)

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

Это имеет следующие преимущества:

  • Это аккуратный однострочник.
  • Это не требует уродливого List<? extends S>построить.
  • Он не выдает никаких предупреждений.
  • Он не делает копию , если ваш список был создан с помощью !!!
  • Самое главное: делает правильно.

Почему это правильно?

Если вы посмотрите на исходный код List.copyOf()вы увидите, что это работает следующим образом:

  • Если ваш список был создан с помощью , то он выполнит приведение и вернет его без копирования.
  • В противном случае (например, если ваш список ArrayList(),) он создаст копию и вернет ее.

Если ваш оригинал является копией ArrayList должны быть сделаны. Если бы вы привели as , вы бы открыли возможность непреднамеренного добавления a в this , что затем привело бы к тому, что ваш оригинал содержал a среди s , что является повреждением памяти: попытка повторить все Dс в оригинале ArrayList<D>бросил бы ClassCastException.

С другой стороны, если ваш исходный List<D>был создан с использованием List.of(), то он неизменяемый (*1) , поэтому никто не может случайно добавить Sк нему, так что можно просто применить его к List<S>.


(*1) когда эти списки были впервые введены, они назывались «неизменяемыми»; позднее они поняли, что неправильно называть их неизменяемыми, потому что коллекция не может быть неизменяемой, поскольку не может ручаться за неизменность элементов, которые в нее входят; поэтому они изменили документацию, назвав их «неизменяемыми»; однако «неизменяемый» уже имел значение до того, как эти списки были введены, и означал «неизменяемый для вас взгляд на мой список, который я все еще могу изменять по своему усмотрению, и мутации будут очень заметны для вас.". Таким образом, ни неизменяемый, ни немодифицируемый не являются правильными. Мне нравится называть их "поверхностно неизменяемыми" в том смысле, что они не являются глубоко неизменяемыми, но это может вызвать у некоторых раздражение, поэтому я просто назвал их "неизменяемыми" в качестве компромисса.

Как объяснил @erickson, если вы действительно хотите ссылку на исходный список, убедитесь, что никакой код не вставляет ничего в этот список, если вы когда-нибудь захотите использовать его снова в соответствии с его первоначальным объявлением. Самый простой способ получить его - просто привести его к простому старому не родовому списку:

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

Я не рекомендовал бы это, если вы не знаете, что происходит со списком, и предложил бы вам изменить любой код, который нужен списку, чтобы принять список, который у вас есть.

Я пропустил ответ, в котором вы просто приводили исходный список, используя двойное приведение. Итак, для полноты:

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

Ничего не копируется, операция выполняется быстро. Однако здесь вы обманываете компилятор, поэтому вы должны быть абсолютно уверены, что не изменили список таким образом, чтобыsubListначинает содержать элементы другого подтипа. При работе с неизменяемыми списками это обычно не проблема.

Ниже приведен полезный фрагмент, который работает. Он создает новый список массивов, но создание JVM-объекта незначительно.

Я видел, что другие ответы не обязательно сложны.

List<BaseClass> baselist = new ArrayList<>(sublist);

То, что вы пытаетесь сделать, очень полезно, и я считаю, что мне нужно делать это очень часто в коде, который я пишу. Пример использования:

Скажем, у нас есть интерфейс Foo и у нас есть zorking пакет, который имеет ZorkingFooManager который создает и управляет экземплярами package-private ZorkingFoo implements Foo, (Очень распространенный сценарий.)

Так, ZorkingFooManager должен содержать private Collection<ZorkingFoo> zorkingFoos но это должно выставить public Collection<Foo> getAllFoos(),

Большинство программистов Java не будут думать дважды перед реализацией getAllFoos() как выделение нового ArrayList<Foo>, заполняя его всеми элементами из zorkingFoos и вернуть его. Мне нравится развлекать мысль о том, что около 30% всех тактов, используемых Java-кодом, работающим на миллионах машин по всей планете, ничего не делают, кроме создания таких бесполезных копий списков ArrayList, которые представляют собой микросекунды, собираемые мусором после их создания.

Решением этой проблемы, конечно же, является уменьшение коллекции. Вот лучший способ сделать это:

static <T,U extends T> List<T> downCastList( List<U> list )
{
    return castList( list );
}

Что приводит нас к castList() функция:

static <T,E> List<T> castList( List<E> list )
{
    @SuppressWarnings( "unchecked" )
    List<T> result = (List<T>)list;
    return result;
}

Промежуточный result переменная необходима из-за извращения языка Java:

  • return (List<T>)list; создает исключение "непроверенный актерский состав"; Все идет нормально; но потом:

  • @SuppressWarnings( "unchecked" ) return (List<T>)list; является незаконным использованием аннотации подавления предупреждений.

Таким образом, хотя это не кошерно использовать @SuppressWarnings на return С другой стороны, его можно использовать в присваивании, поэтому дополнительная переменная "result" решает эту проблему. (В любом случае он должен быть оптимизирован либо компилятором, либо JIT.)

List<BaseClass> convertedList = Collections.checkedList(listOfSubClass, BaseClass.class)

Как насчет приведения всех элементов. Это создаст новый список, но будет ссылаться на оригинальные объекты из старого списка.

List<BaseClass> convertedList = listOfSubClass.map(x -> (BaseClass)x).collect(Collectors.toList());

Это полный рабочий кусок кода с использованием Generics для преобразования списка подклассов в суперкласс.

Метод вызывающего, который передает тип подкласса

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

Метод Callee, который принимает любой подтип базового класса

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

Нечто подобное должно работать тоже:

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 нужен клац.

Другие вопросы по тегам