Как вы приведете список супертипов к списку подтипов?

244

Например, допустим, у вас есть два класса:

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

У меня есть метод, который возвращает a, List<TestA>и я хотел бы привести все объекты в этом списке, TestBчтобы в итоге я получил List<TestB>.

Джереми Логан
источник

Ответы:

578

Просто приведение к List<TestB>почти работам; но это не работает, потому что вы не можете привести общий тип одного параметра к другому. Тем не менее, вы можете использовать промежуточный тип подстановочного знака, и это будет разрешено (поскольку вы можете приводить к типам подстановочных знаков и обратно, просто с непроверенным предупреждением):

List<TestB> variable = (List<TestB>)(List<?>) collectionOfListA;
newacct
источник
88
При всем уважении, я думаю, что это своего рода хакерское решение - вы уклоняетесь от безопасности типов, которую Java пытается вам предоставить. По крайней мере, посмотрите на этот учебник по Java ( docs.oracle.com/javase/tutorial/java/generics/subtyping.html ) и подумайте, почему у вас возникла эта проблема, прежде чем исправлять ее таким образом.
jfritz42
63
@ jfritz42 Я бы не назвал это безопасностью типа уклонения. В этом случае у вас есть знания, которых нет у Java. Вот для чего нужен кастинг.
Планки
3
Это позволяет мне писать: List <String> myList = (List <String>) (List <?>) (New ArrayList <Integer> ()); Это может произойти сбой во время выполнения. Я предпочитаю что-то вроде List <? extends Number> myList = new ArrayList <Integer> ();
Бентайе
1
Я честно согласен с @ jfritz42. У меня возникла та же проблема, я посмотрел на предоставленный им URL и смог решить мою проблему без кастинга.
Сэм Ороско,
@ jfritz42 это функционально не отличается от приведения объекта к
целому
116

приведение дженериков невозможно, но если вы определите список по-другому, в нем можно сохранить TestB:

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

У вас все еще есть проверка типов, когда вы используете объекты в списке.

Salandur
источник
14
Это даже не правильный синтаксис Java.
newacct
2
Это правда, но это было скорее наглядно, чем на самом деле правильно
Саландур
У меня есть следующий метод: createSingleString (List <? Extends Object> objects) Внутри этого метода я вызываю String.valueOf (Object), чтобы свернуть список в одну строку. Он отлично работает, когда вводим List <Long>, List <Boolean> и т. Д. Спасибо!
Крис Спраг
2
Что если TestA - это интерфейс?
Сувитруф - Андрей Апанасик
8
Можете ли вы дать MCVE, где это на самом деле работает? Я получаю Cannot instantiate the type ArrayList<? extends TestA>.
Nateowami
42

Вы действительно не можете *:

Пример взят из этого урока Java

Предположим, есть два типа Aи Bтакие B extends A. Тогда следующий код верен:

    B b = new B();
    A a = b;

Предыдущий код действителен, потому что Bявляется подклассом A. Теперь, что происходит с 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")в свой метод.

Стив Куо
источник
2
Кроме того, использование материалов по этой ссылке может избежать непроверенного преобразования, поскольку компилятор может расшифровать все преобразование.
Райан Х
29

С Java 8 вы действительно можете

List<TestB> variable = collectionOfListA
    .stream()
    .map(e -> (TestB) e)
    .collect(Collectors.toList());
fracz
источник
32
Это на самом деле составление списка? Потому что это больше похоже на серию операций для итерации по всему списку, приведения каждого элемента и последующего добавления их во вновь созданный список нужного типа. Иными словами, не могли бы вы сделать именно это с Java7 - с new List<TestB>помощью цикла, приведения и приведения?
Патрик М
5
Из ОП «и я хотел бы разыграть все объекты в этом списке» - так что на самом деле это один из немногих ответов, который отвечает на поставленный вопрос, даже если это не тот вопрос, который просматривают здесь люди после.
Люк Ашервуд
17

Я думаю, что вы приводите в неправильном направлении, хотя ... если метод возвращает список TestAобъектов, тогда действительно небезопасно приводить их к TestB.

По сути, вы просите компилятор позволить вам выполнять TestBоперации с типом, TestAкоторый их не поддерживает.

jerryjvl
источник
11

Поскольку на этот вопрос часто ссылаются, и текущие ответы в основном объясняют, почему он не работает (или предлагают хакерские, опасные решения, которые я никогда не хотел бы видеть в рабочем коде), я думаю, что было бы целесообразно добавить другой ответ, показывающий подводные камни и возможное решение.


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


Конечно, приведения могут быть действительными. Иногда у вас есть информация о типах, которые недоступны для компилятора. В этих случаях можно привести списки, но в общем случае это не рекомендуется :

Можно или ...

  • ... бросить весь список или
  • ... бросить все элементы списка

Последствия первого подхода (который соответствует принятому в настоящее время ответу) неуловимы. На первый взгляд может показаться, что он работает правильно. Но если в списке ввода есть неправильные типы, тогда ClassCastExceptionбудет выброшено a , возможно, в совершенно другом месте кода, и это может быть трудно отладить и выяснить, где неправильный элемент попал в список. Худшая проблема заключается в том, что кто-то может даже добавить недопустимые элементы после того, как список будет приведен, что еще больше затрудняет отладку.

Проблема отладки этих ложных ClassCastExceptionsможет быть решена с помощью Collections#checkedCollectionсемейства методов.


Фильтрация списка по типу

Более безопасный для преобразования тип из a List<Supertype> в a 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]
Marco13
источник
7

Вы не можете бросить , List<TestB>чтобы List<TestA>Стив Kuo упоминает , но вы можете сбросить содержимое List<TestA>в List<TestB>. Попробуйте следующее:

List<TestA> result = new List<TestA>();
List<TestB> data = new List<TestB>();
result.addAll(data);

Я не пробовал этот код, поэтому, вероятно, есть ошибки, но идея в том, что он должен перебирать объект данных, добавляя элементы (объекты TestB) в список. Я надеюсь, что это работает для вас.

martinatime
источник
почему за это проголосовали? Это было полезно для меня, и я не вижу объяснений для голосования против.
Зод
7
Конечно, этот пост не является неправильным. Но человеку, который задал вопрос, наконец-то нужен список TestB. В этом случае этот ответ вводит в заблуждение. Вы можете добавить все элементы из List <TestB> в List <TestA>, вызвав List <TestA> .addAll (List <TestB>). Но вы не можете использовать List <TestB> .addAll (List <TestA>);
пражет
6

Лучший безопасный способ - реализовать 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
}
Игорь Зубченок
источник
4

Единственный способ, которым я знаю, это скопировать:

List<TestB> list = new ArrayList<TestB> (
    Arrays.asList (
        testAList.toArray(new TestB[0])
    )
);
Питер Хойш
источник
2
Довольно странно, я думаю, что это не дает предупреждение компилятору.
Саймон Форсберг
это странность с массивами. Вздох, Java-массивы так бесполезны. Но я думаю, что это, конечно, не хуже и намного читабельнее, чем другие ответы. Я не вижу в этом ничего более опасного, чем актеры.
Thufir
3

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

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

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

Питер Лори
источник
2

если у вас есть объект класса TestA, вы не можете его привести TestB. каждый TestBесть TestA, но не наоборот.

в следующем коде:

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

вторая строка будет бросать ClassCastException.

Вы можете привести TestAссылку только если сам объект TestB. например:

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

Таким образом, вы не всегда можете привести список TestAк списку TestB.

CD1
источник
1
Я думаю, что он говорит о чем-то вроде: вы получаете «a» в качестве аргумента метода, который создается как - TestA a = new TestB (), затем вы хотите получить объект TestB, а затем вам нужно привести его - TestB b = ( TestB) a;
Самсамара
2

Вы можете использовать selectInstancesметод в Eclipse Collections . Однако это потребует создания новой коллекции, поэтому не будет столь же эффективным, как принятое решение, использующее кастинг.

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.

Дональд Рааб
источник
1

Это возможно из-за стирания типа. Вы найдете это

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

Внутренне оба списка имеют тип List<Object>. По этой причине вы не можете разыгрывать одно на другое - нечего разыгрывать.

N3rd
источник
«Нечего разыгрывать» и «Нельзя разыгрывать одно другому» - это противоречие. Integer j = 42; Integer i = (Integer) j;отлично работает, оба являются целыми числами, они оба имеют один и тот же класс, поэтому «нечего разыгрывать».
Саймон Форсберг
1

Проблема в том, что ваш метод НЕ возвращает список TestA, если он содержит TestB, так что, если он был правильно напечатан? Тогда этот актерский состав:

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;

похоже компилируется просто отлично для меня.

Билл К
источник
1
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.

Алекс По
источник
0

Это должно бы сработать

List<TestA> testAList = new ArrayList<>();
List<TestB> testBList = new ArrayList<>()
testAList.addAll(new ArrayList<>(testBList));
Anonymouse
источник
1
Это делает ненужную копию.
defnull
0

Довольно странно, что приведение списка вручную до сих пор не обеспечивается некоторым набором инструментов, реализующим что-то вроде:

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

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

Донателло
источник