Jak rzutujesz Listę nadtypów na Listę podtypów?

244

Załóżmy na przykład, że masz dwie klasy:

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

Mam metodę, która zwraca a List<TestA>i chciałbym rzutować wszystkie obiekty z tej listy na, TestBaby skończyć z List<TestB>.

Jeremy Logan
źródło

Odpowiedzi:

578

Po prostu casting do List<TestB>prawie działa; ale to nie działa, ponieważ nie można rzutować typu ogólnego jednego parametru na inny. Możesz jednak użyć pośredniego typu symbolu wieloznacznego i będzie to dozwolone (ponieważ możesz przesyłać do i z typów symboli wieloznacznych, tylko z niezaznaczonym ostrzeżeniem):

List<TestB> variable = (List<TestB>)(List<?>) collectionOfListA;
newacct
źródło
88
Z całym szacunkiem uważam, że jest to dość hackerskie rozwiązanie - unikasz bezpieczeństwa, które Java próbuje ci zapewnić. Przynajmniej spójrz na ten samouczek Java ( docs.oracle.com/javase/tutorial/java/generics/subtyping.html ) i zastanów się, dlaczego masz ten problem, zanim naprawisz go w ten sposób.
jfritz42
63
@ jfritz42 Nie nazwałbym tego „unikaniem” bezpieczeństwa. W tym przypadku masz wiedzę, której nie ma Java. Po to jest casting.
Planky
3
To pozwala mi pisać: List <String> myList = (List <String>) (List <?>) (New ArrayList <Integer> ()); Spowodowałoby to awarię w czasie wykonywania. Wolę coś takiego jak Lista <? rozszerza Number> myList = new ArrayList <Integer> ();
Bentaye,
1
Szczerze zgadzam się z @ jfritz42 Miałem ten sam problem i sprawdziłem podany przez niego adres URL i byłem w stanie rozwiązać mój problem bez przesyłania.
Sam Orozco,
@ jfritz42 nie różni się to funkcjonalnie od rzutowania obiektu na liczbę całkowitą, co jest dozwolone przez kompilator
kcazllerraf
116

rzutowanie ogólnych nie jest możliwe, ale jeśli zdefiniujesz listę w inny sposób, możesz w niej zapisać TestB:

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

Nadal masz sprawdzanie typu do zrobienia, gdy używasz obiektów z listy.

Salandur
źródło
14
To nawet nie jest poprawna składnia Java.
newacct
2
To prawda, ale było bardziej ilustracyjne niż zgodne z faktami
Salandur,
Mam następującą metodę: createSingleString (List <? Extends Object> objects) Wewnątrz tej metody wywołuję String.valueOf (Object), aby zwinąć listę w jeden ciąg. Działa świetnie, gdy dane wejściowe to List <Long>, List <Boolean> itp. Dzięki!
Chris Sprague,
2
Co jeśli TestA jest interfejsem?
Suvitruf - Andrei Apanasik
8
Czy możesz podać MCVE, gdzie to naprawdę działa? I dostać Cannot instantiate the type ArrayList<? extends TestA>.
Nateowami,
42

Naprawdę nie możesz *:

Przykład pochodzi z tego samouczka Java

Załóżmy, że istnieją dwa typy Ai Btakie, że B extends A. Następnie następujący kod jest poprawny:

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

Poprzedni kod jest prawidłowy, ponieważ Bjest podklasą A. Co się teraz dzieje z List<A>i List<B>?

Okazuje się, że List<B>nie jest to podklasa, List<A>dlatego nie możemy pisać

    List<B> b = new ArrayList<>();
    List<A> a = b; // error, List<B> is not of type List<A>

Co więcej, nie możemy nawet pisać

    List<B> b = new ArrayList<>();
    List<A> a = (List<A>)b; // error, List<B> is not of type List<A>

*: Aby umożliwić casting, potrzebujemy wspólnego rodzica dla obu List<A>i List<B>: List<?>na przykład. Poniższa jest ważny:

    List<B> b = new ArrayList<>();
    List<?> t = (List<B>)b;
    List<A> a = (List<A>)t;

Otrzymasz jednak ostrzeżenie. Możesz go wyłączyć, dodając @SuppressWarnings("unchecked")do swojej metody.

Steve Kuo
źródło
2
Ponadto użycie materiału w tym linku pozwala uniknąć niesprawdzonej konwersji, ponieważ kompilator może rozszyfrować całą konwersję.
Ryan H
29

Z Javą 8 możesz to zrobić

List<TestB> variable = collectionOfListA
    .stream()
    .map(e -> (TestB) e)
    .collect(Collectors.toList());
fracz
źródło
32
Czy to naprawdę rzuca listę? Ponieważ bardziej przypomina serię operacji w celu iteracji po całej liście, rzutowania każdego elementu, a następnie dodawania ich do nowo utworzonej listy żądanego typu. Innymi słowy, czy nie można tego dokładnie zrobić przy pomocy Java7- z new List<TestB>pętlą i obsadą?
Patrick M
5
Z PO „i chciałbym rzucić wszystkie obiekty z tej listy” - tak naprawdę jest to jedna z niewielu odpowiedzi, która odpowiada na zadane pytanie, nawet jeśli nie jest to pytanie, którego szukają ludzie przeglądający tutaj.
Luke Usherwood
17

Myślę, że rzucasz w złym kierunku ... jeśli metoda zwróci listę TestAobiektów, to naprawdę nie jest bezpiecznie ich rzucić TestB.

Zasadniczo prosisz kompilator o pozwolenie na wykonywanie TestBoperacji na typach, TestAktóre ich nie obsługują.

jerryjvl
źródło
11

Ponieważ jest to pytanie, na które często się powołuje, a obecne odpowiedzi głównie wyjaśniają, dlaczego to nie działa (lub proponują hackerskie, niebezpieczne rozwiązania, których nigdy nie chciałbym widzieć w kodzie produkcyjnym), uważam, że należy dodać kolejną odpowiedź, pokazującą pułapki i możliwe rozwiązanie.


Powód, dla którego to ogólnie nie działa, został już wskazany w innych odpowiedziach: To, czy konwersja jest rzeczywiście ważna, zależy od typów obiektów zawartych na oryginalnej liście. Gdy istnieją obiekty w liście, którego typ nie jest typu TestB, ale o innej podklasy TestA, następnie obsada jest nie ważna.


Oczywiście, obsady mogą być ważne. Czasami masz informacje o typach, które nie są dostępne dla kompilatora. W takich przypadkach możliwe jest przesyłanie list, ale ogólnie nie jest zalecane :

Można albo ...

  • ... obsadzić całą listę lub
  • ... obsadzić wszystkie elementy listy

Implikacje pierwszego podejścia (które odpowiadają obecnie przyjętej odpowiedzi) są subtelne. Na pierwszy rzut oka może się wydawać, że działa poprawnie. Ale jeśli na liście wejściowej znajdują się niewłaściwe typy, to ClassCastExceptionzostanie wyrzucony, być może w zupełnie innym miejscu w kodzie, i może być trudno go debugować i dowiedzieć się, gdzie niewłaściwy element wślizgnął się na listę. Najgorszym problemem jest to, że ktoś może nawet dodać nieprawidłowe elementy po rzutowaniu listy, co jeszcze bardziej utrudnia debugowanie.

Problem debugowania tych fałszywych ClassCastExceptionsmożna złagodzić dzięki Collections#checkedCollectionrodzinie metod.


Filtrowanie listy na podstawie typu

Bardziej bezpiecznym sposobem na konwersję z a List<Supertype> na a List<Subtype>jest przefiltrowanie listy i utworzenie nowej listy zawierającej tylko elementy określonego typu. Istnieją pewne stopnie swobody we wdrażaniu takiej metody (np. W odniesieniu do traktowania nullwpisów), ale jedna możliwa implementacja może wyglądać następująco:

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

Metodę tę można zastosować do filtrowania dowolnych list (nie tylko z daną relacją podtypu i typu pod względem parametrów typu), jak w tym przykładzie:

// 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
źródło
7

Nie można rzucać List<TestB>się List<TestA>jak wspomina Steve Kuo ale można zrzucić zawartość List<TestA>do List<TestB>. Spróbuj wykonać następujące czynności:

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

Nie próbowałem tego kodu, więc prawdopodobnie są to błędy, ale pomysł polega na tym, że powinien on iterować przez obiekt danych dodając elementy (obiekty TestB) do listy. Mam nadzieję, że ci się uda.

martinatime
źródło
dlaczego zostało to przegłosowane? Przydało mi się to i nie widzę wytłumaczenia dla głosowania w dół.
zod
7
Z pewnością ten post nie jest nieprawidłowy. Ale osoba, która zadała pytanie, w końcu potrzebuje listy TestB. W takim przypadku ta odpowiedź jest myląca. Możesz dodać wszystkie elementy z Listy <TestB> do Listy <TestA>, wywołując List <TestA> .addAll (Lista <TestB>). Ale nie możesz używać List <TestB> .addAll (List <TestA>);
modli się
6

Najlepszym bezpiecznym sposobem jest zaimplementowanie AbstractListi rzucenie przedmiotów w trakcie implementacji. Stworzyłem ListUtilklasę pomocnika:

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

Możesz użyć castmetody ślepego rzutowania obiektów na liście oraz convertmetody bezpiecznego rzutowania. Przykład:

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
}
Igor Zubchenok
źródło
4

Jedyny sposób, jaki znam to kopiowanie:

List<TestB> list = new ArrayList<TestB> (
    Arrays.asList (
        testAList.toArray(new TestB[0])
    )
);
Peter Heusch
źródło
2
Całkiem dziwne, myślę, że to nie daje ostrzeżenia kompilatorowi.
Simon Forsberg
jest to osobliwość z tablicami. Westchnienie, tablice Java są tak bezużyteczne. Ale myślę, że z pewnością nie jest to gorsze i bardziej czytelne niż inne odpowiedzi. Nie widzę w tym nic bardziej niebezpiecznego niż obsada.
Thufir
3

Kiedy rzutujesz odwołanie do obiektu, po prostu rzutujesz typ odwołania, a nie typ obiektu. rzutowanie nie zmieni faktycznego typu obiektu.

Java nie ma niejawnych reguł konwersji typów obiektów. (W przeciwieństwie do prymitywów)

Zamiast tego musisz podać, jak przekonwertować jeden typ na inny i wywołać go ręcznie.

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

Jest to bardziej szczegółowe niż w języku z bezpośrednim wsparciem, ale działa i nie powinieneś robić tego zbyt często.

Peter Lawrey
źródło
2

jeśli masz obiekt klasy TestA, nie możesz go rzucić TestB. każdy TestBjest TestA, ale nie na odwrót.

w następującym kodzie:

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

druga linia rzuciłaby ClassCastException.

możesz rzucić TestAodwołanie tylko wtedy, gdy sam obiekt jest TestB. na przykład:

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

więc nie zawsze możesz rzucić listę TestAna listę TestB.

cd1
źródło
1
Myślę, że mówi o czymś takim: dostajesz „a” jako argument metody, który jest tworzony jako - TestA a = nowy TestB (), następnie chcesz uzyskać obiekt TestB, a następnie musisz go rzutować - TestB b = ( TestB) a;
samsamara,
2

Możesz użyć tej selectInstancesmetody w kolekcji Eclipse . Będzie to wiązało się z utworzeniem nowej kolekcji, jednak nie będzie tak wydajne, jak zaakceptowane rozwiązanie wykorzystujące rzutowanie.

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

Podałem StringBufferw przykładzie, aby pokazać, że selectInstancesnie tylko obniża typ, ale także filtruje, jeśli kolekcja zawiera typy mieszane.

Uwaga: jestem osobą odpowiedzialną za kolekcje Eclipse.

Donald Raab
źródło
1

Jest to możliwe z powodu usunięcia typu. Znajdziesz to

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

Wewnętrznie obie listy są typu List<Object>. Z tego powodu nie możesz rzucać jednego na drugi - nie ma nic do rzucenia.

n3
źródło
„Nie ma nic do rzucenia” i „Nie możesz rzucić jednego na drugiego” to trochę sprzeczność. Integer j = 42; Integer i = (Integer) j;działa dobrze, oba są liczbami całkowitymi, oba mają tę samą klasę, więc „nie ma co rzucać”.
Simon Forsberg
1

Problem polega na tym, że twoja metoda NIE zwraca listy TestA, jeśli zawiera TestB, więc co, jeśli została poprawnie wpisana? Następnie ta obsada:

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

działa tak dobrze, jak można się spodziewać (Eclipse ostrzega przed niesprawdzoną obsadą, która jest dokładnie tym, co robisz, więc meh). Czy możesz to wykorzystać do rozwiązania swojego problemu? W rzeczywistości możesz z tego powodu:

List<TestA> badlist = null; // Actually contains TestBs, as specified
List<? extends TestA> talist = badlist;  // Umm, works
List<TextB> tblist = (List<TestB>)talist; // TADA!

Dokładnie o co prosiłeś, prawda? lub być naprawdę dokładnym:

List<TestB> tblist = (List<TestB>)(List<? extends TestA>) badlist;

wydaje się, że dobrze się kompiluje.

Bill K.
źródło
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);
}

Ten test pokazuje, jak przesyłać List<Object>na List<MyClass>. Ale musisz zwrócić uwagę na to, że objectListmusi zawierać instancje tego samego typu co MyClass. I ten przykład można rozważyć, gdy List<T>jest używany. W tym celu pobierz pole Class<T> clazzw konstruktorze i użyj go zamiast MyClass.class.

Alex Po
źródło
0

To powinno zadziałać

List<TestA> testAList = new ArrayList<>();
List<TestB> testBList = new ArrayList<>()
testAList.addAll(new ArrayList<>(testBList));
anonimowa
źródło
1
To tworzy niepotrzebną kopię.
defnull
0

Dość dziwne, że ręczne przesyłanie listy wciąż nie jest dostępne w niektórych narzędziach implementujących coś takiego:

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

Oczywiście nie będzie to sprawdzać pozycji jeden po drugim, ale właśnie tego chcemy tutaj uniknąć, jeśli dobrze wiemy, że nasza implementacja zapewnia tylko podtyp.

Donatello
źródło