it-swarm.com.de

Wie wandelt man eine Liste von Supertypen in eine Liste von Subtypen um?

Angenommen, Sie haben zwei Klassen:

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

Ich habe eine Methode, die ein List<TestA> und ich möchte alle Objekte in dieser Liste in TestB umwandeln, sodass ich am Ende ein List<TestB>.

224
Jeremy Logan

Einfach Casting zu List<TestB> funktioniert fast; Dies funktioniert jedoch nicht, da Sie einen generischen Typ eines Parameters nicht in einen anderen umwandeln können. Sie können jedoch einen Zwischen-Platzhaltertyp durchgehen, und dieser ist zulässig (da Sie nur mit einer nicht aktivierten Warnung auf Platzhaltertypen und von diesen wechseln können):

List<TestB> variable = (List<TestB>)(List<?>) collectionOfListA;
548
newacct

das Casting von Generika ist nicht möglich, aber wenn Sie die Liste auf andere Weise definieren, ist es möglich, TestB darin zu speichern:

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

Sie müssen noch die Typüberprüfung durchführen, wenn Sie die Objekte in der Liste verwenden.

112
Salandur

Sie können wirklich nicht *:

Das Beispiel stammt aus dieses Java Tutorial

Angenommen, es gibt zwei Typen A und B, so dass B extends A. Dann ist der folgende Code richtig:

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

Der vorherige Code ist gültig, da B eine Unterklasse von A ist. Was passiert nun mit List<A> Und List<B>?

Es stellt sich heraus, dass List<B> Keine Unterklasse von List<A> Ist, daher können wir nicht schreiben

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

Außerdem können wir nicht einmal schreiben

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

*: Um das Casting zu ermöglichen, benötigen wir ein gemeinsames übergeordnetes Element für List<A> Und List<B>: List<?>. Folgendes ist gültig:

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

Sie erhalten jedoch eine Warnung. Sie können es unterdrücken, indem Sie Ihrer Methode @SuppressWarnings("unchecked") hinzufügen.

40
Steve Kuo

Mit Java 8 können Sie tatsächlich

List<TestB> variable = collectionOfListA
    .stream()
    .map(e -> (TestB) e)
    .collect(Collectors.toList());
27
fracz

Ich denke, Sie wandeln in die falsche Richtung ... Wenn die Methode eine Liste von TestA Objekten zurückgibt, ist es wirklich nicht sicher, sie in TestB zu wandeln.

Grundsätzlich bitten Sie den Compiler, TestB Operationen auf einem Typ TestA ausführen zu lassen, der diese nicht unterstützt.

18
jerryjvl

Da es sich um eine häufig gestellte Frage handelt und die aktuellen Antworten hauptsächlich erklären, warum sie nicht funktionieren (oder hackige, gefährliche Lösungen vorschlagen, die ich niemals im Produktionscode sehen möchte), halte ich es für angebracht, eine weitere Antwort hinzuzufügen die Fallstricke und eine mögliche Lösung.


Der Grund, warum dies im Allgemeinen nicht funktioniert, wurde bereits in anderen Antworten erläutert: Ob die Konvertierung tatsächlich gültig ist oder nicht, hängt von den Objekttypen ab, die in der ursprünglichen Liste enthalten sind. Wenn die Liste Objekte enthält, deren Typ nicht vom Typ TestB ist, sondern von einer anderen Unterklasse von TestA, ist die Besetzung not gültig.


Natürlich sind die Darstellungen darf gültig. Manchmal haben Sie Informationen zu den Typen, die für den Compiler nicht verfügbar sind. In diesen Fällen ist es möglich, die Listen zu besetzen, aber im Allgemeinen wird nicht empfohlen :

Man könnte entweder ...

  • ... die ganze Liste oder
  • ... wirke alle Elemente der Liste

Die Implikationen des ersten Ansatzes (der der aktuell akzeptierten Antwort entspricht) sind subtil. Es scheint auf den ersten Blick richtig zu funktionieren. Wenn sich jedoch falsche Typen in der Eingabeliste befinden, wird ein ClassCastException ausgegeben, möglicherweise an einer völlig anderen Stelle im Code, und es kann schwierig sein, dies zu debuggen und herauszufinden, wo das falsche Element abhanden gekommen ist in die Liste. Das schlimmste Problem ist, dass jemand die ungültigen Elemente hinzufügen könnte after die Liste wurde umgewandelt, was das Debuggen noch schwieriger macht.

Das Problem des Debuggens dieser falschen ClassCastExceptions kann mit Collections#checkedCollection Methodenfamilie.


Filtern der Liste nach Typ

Eine typsichere Methode zum Konvertieren von List<Supertype> zu einem List<Subtype> soll eigentlich filtern die Liste und eine neue Liste erstellen, die nur Elemente enthält, die einen bestimmten Typ haben. Es gibt einige Freiheitsgrade für die Implementierung einer solchen Methode (z. B. in Bezug auf die Behandlung von null Einträgen), aber eine mögliche Implementierung könnte so aussehen:

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

Diese Methode kann verwendet werden, um beliebige Listen zu filtern (nicht nur mit einer bestimmten Subtyp-Supertype-Beziehung in Bezug auf die Typparameter), wie im folgenden Beispiel:

// 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]
8
Marco13

Sie können List<TestB> Nicht in List<TestA> Umwandeln, wie Steve Kuo erwähnt, ABER Sie können den Inhalt von List<TestA> In List<TestB> Ablegen. Versuche Folgendes:

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

Ich habe diesen Code nicht ausprobiert, daher gibt es wahrscheinlich Fehler, aber die Idee ist, dass er durch das Datenobjekt iteriert werden sollte, indem die Elemente (TestB-Objekte) zur Liste hinzugefügt werden. Ich hoffe das klappt bei dir.

6
martinatime

Am sichersten ist es, ein AbstractList zu implementieren und Elemente in der Implementierung umzusetzen. Ich habe die Hilfsklasse ListUtil erstellt:

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

Sie können die Methode cast verwenden, um Objekte in der Liste blind zu gießen, und die Methode convert, um sie sicher zu gießen. Beispiel:

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
}
6
Igor Zubchenok

Wenn Sie eine Objektreferenz übertragen, übertragen Sie nur den Typ der Referenz, nicht den Typ des Objekts. Durch das Casting wird der tatsächliche Typ des Objekts nicht geändert.

Java hat keine impliziten Regeln für die Konvertierung von Objekttypen. (Im Gegensatz zu Primitiven)

Stattdessen müssen Sie angeben, wie ein Typ in einen anderen konvertiert und manuell aufgerufen werden kann.

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

Dies ist ausführlicher als in einer Sprache mit direkter Unterstützung, aber es funktioniert und Sie sollten dies nicht sehr oft tun müssen.

3
Peter Lawrey

Der einzige Weg, den ich kenne, ist das Kopieren von:

List<TestB> list = new ArrayList<TestB> (
    Arrays.asList (
        testAList.toArray(new TestB[0])
    )
);
3
Peter Heusch

Dies ist aufgrund der Typlöschung möglich. Sie werden das finden

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

Intern sind beide Listen vom Typ List<Object>. Aus diesem Grund kann man nicht aufeinander einwirken - es gibt nichts zu wirken.

2
n3rd

wenn Sie ein Objekt der Klasse TestA haben, können Sie es nicht in TestB umwandeln. jedes TestB ist ein TestA, aber nicht andersherum.

im folgenden Code:

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

die zweite Zeile würde einen ClassCastException auslösen.

sie können eine TestA -Referenz nur setzen, wenn das Objekt selbst TestB ist. zum Beispiel:

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

daher können Sie eine Liste von TestA nicht immer in eine Liste von TestB umwandeln.

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

Dieser Test zeigt, wie man List<Object> bis List<MyClass>. Beachten Sie jedoch, dass objectList Instanzen desselben Typs wie MyClass enthalten muss. Und dieses Beispiel kann in Betracht gezogen werden, wenn List<T> wird genutzt. Zu diesem Zweck erhalten Sie das Feld Class<T> clazz im Konstruktor und benutze es anstelle von MyClass.class.

1
Alex Po

Das Problem ist, dass Ihre Methode KEINE Liste von TestA zurückgibt, wenn sie ein TestB enthält. Was ist, wenn sie korrekt eingegeben wurde? Dann diese Besetzung:

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

funktioniert ungefähr so ​​gut, wie Sie es sich erhoffen könnten (Eclipse warnt Sie vor einer ungeprüften Besetzung, die genau das ist, was Sie tun, also meh). Können Sie dies also verwenden, um Ihr Problem zu lösen? Eigentlich können Sie aus diesem Grund:

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

Genau das, wonach Sie gefragt haben, richtig? oder um genau zu sein:

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

scheint ganz gut für mich zu kompilieren.

1
Bill K

Sie können die selectInstances -Methode in Eclipse Collections verwenden. Dies erfordert die Erstellung einer neuen Sammlung, ist jedoch nicht so effizient wie die akzeptierte Lösung, bei der Casting verwendet wird.

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

Ich habe StringBuffer in das Beispiel aufgenommen, um zu zeigen, dass selectInstances nicht nur den Typ herunterwirft, sondern auch filtert, wenn die Sammlung gemischte Typen enthält.

Hinweis: Ich bin ein Committer für Eclipse-Sammlungen.

1
Donald Raab

Das sollte funktionieren

List<TestA> testAList = new ArrayList<>();
List<TestB> testBList = new ArrayList<>()
testAList.addAll(new ArrayList<>(testBList));
0
anonymouse