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

244

Angenommen, Sie haben zwei Klassen:

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

Ich habe eine Methode, die a zurückgibt, List<TestA>und ich möchte alle Objekte in dieser Liste TestBso umwandeln, dass ich am Ende a habe List<TestB>.

Jeremy Logan
quelle

Antworten:

578

Einfach zu List<TestB>fast funktioniert; 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 durchlaufen, und dies ist zulässig (da Sie nur mit einer nicht aktivierten Warnung zu und von Platzhaltertypen wechseln können):

List<TestB> variable = (List<TestB>)(List<?>) collectionOfListA;
newacct
quelle
88
Bei allem Respekt denke ich, dass dies eine Art Hacky-Lösung ist - Sie weichen der Typensicherheit aus, die Java Ihnen bieten möchte. Schauen Sie sich zumindest dieses Java-Tutorial an ( docs.oracle.com/javase/tutorial/java/generics/subtyping.html ) und überlegen Sie, warum Sie dieses Problem haben, bevor Sie es auf diese Weise beheben.
jfritz42
63
@ jfritz42 Ich würde es nicht als "Ausweich" -Sicherheit bezeichnen. In diesem Fall haben Sie Kenntnisse, die Java nicht hat. Dafür ist Casting da.
Planky
3
Damit kann ich schreiben: List <String> myList = (List <String>) (List <?>) (New ArrayList <Integer> ()); Dies würde zur Laufzeit abstürzen. Ich bevorzuge so etwas wie List <? erweitert Number> myList = new ArrayList <Integer> ();
Bentaye
1
Ich stimme @ jfritz42 ehrlich zu. Ich hatte das gleiche Problem und schaute auf die von ihm angegebene URL und konnte mein Problem ohne das Casting lösen.
Sam Orozco
@ jfritz42 Dies unterscheidet sich funktional nicht vom Umwandeln eines Objekts in eine Ganzzahl, was vom Compiler erlaubt wird
kcazllerraf
116

Das Casting von Generika ist nicht möglich. Wenn Sie die Liste jedoch auf andere Weise definieren, können Sie TestB darin speichern:

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

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

Salandur
quelle
14
Dies ist nicht einmal eine gültige Java-Syntax.
Newacct
2
Das ist wahr, aber es war eher illustrativ als sachlich korrekt
Salandur
Ich habe die folgende Methode: createSingleString (List <? Extended Object> -Objekte) Innerhalb dieser Methode rufe ich String.valueOf (Object) auf, um die Liste in eine Zeichenfolge zu reduzieren. Es funktioniert hervorragend, wenn die Eingabe List <Long>, List <Boolean> usw. Ist. Vielen Dank!
Chris Sprague
2
Was ist, wenn TestA eine Schnittstelle ist?
Suvitruf - Andrei Apanasik
8
Können Sie einem MCVE geben, wo dies tatsächlich funktioniert? Ich verstehe Cannot instantiate the type ArrayList<? extends TestA>.
Nateowami
42

Sie können wirklich nicht *:

Ein Beispiel stammt aus diesem Java-Tutorial

Angenommen, es gibt zwei Arten Aund Bsolche B extends A. Dann ist der folgende Code korrekt:

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

Der vorherige Code ist gültig, da Bes sich um eine Unterklasse von handelt A. Was passiert nun mit List<A>und List<B>?

Es stellt sich heraus, dass dies List<B>keine Unterklasse von ist, List<A>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 Elternteil für beide List<A>und List<B>: List<?>zum Beispiel. 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 es @SuppressWarnings("unchecked")zu Ihrer Methode hinzufügen .

Steve Kuo
quelle
2
Durch die Verwendung des Materials in diesem Link kann auch die ungeprüfte Konvertierung vermieden werden, da der Compiler die gesamte Konvertierung entschlüsseln kann.
Ryan H
29

Mit Java 8 können Sie tatsächlich

List<TestB> variable = collectionOfListA
    .stream()
    .map(e -> (TestB) e)
    .collect(Collectors.toList());
fracz
quelle
32
Wirft das tatsächlich die Liste? Weil es sich eher wie eine Reihe von Operationen liest, die die gesamte Liste durchlaufen, jedes Element umwandeln und sie dann einer neu instanziierten Liste des gewünschten Typs hinzufügen. Anders ausgedrückt, könnten Sie das nicht genau mit Java7 machen - mit einer new List<TestB>, einer Schleife und einer Besetzung?
Patrick M
5
Aus dem OP "und ich möchte alle Objekte in dieser Liste umwandeln" - dies ist also tatsächlich eine der wenigen Antworten, die die Frage wie angegeben beantworten, auch wenn es nicht die Frage ist, nach der die Leute hier suchen.
Luke Usherwood
17

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

Grundsätzlich bitten Sie den Compiler, TestBOperationen für einen Typ auszuführen TestA, der diese nicht unterstützt.

jerryjvl
quelle
11

Da dies eine weit verbreitete Frage ist und die aktuellen Antworten hauptsächlich erklären, warum sie nicht funktioniert (oder hackige, gefährliche Lösungen vorschlagen, die ich niemals im Produktionscode sehen würde), halte ich es für angebracht, eine weitere Antwort hinzuzufügen, die zeigt die Fallstricke und eine mögliche Lösung.


Der Grund, warum dies im Allgemeinen nicht funktioniert, wurde bereits in anderen Antworten aufgezeigt: Ob die Konvertierung tatsächlich gültig ist oder nicht, hängt von den Typen der Objekte ab, die in der ursprünglichen Liste enthalten sind. Wenn es Objekte in der Liste , dessen Typs nicht vom Typ TestB, aber von einer anderen Unterklasse TestA, dann ist die Besetzung nicht gültig.


Natürlich sind die Abgüsse können gültig sein. 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 erstellen, im Allgemeinen wird jedoch nicht empfohlen :

Man könnte entweder ...

  • ... die ganze Liste besetzen oder
  • ... alle Elemente der Liste umwandeln

Die Implikationen des ersten Ansatzes (der der aktuell akzeptierten Antwort entspricht) sind subtil. Auf den ersten Blick scheint es richtig zu funktionieren. Wenn die Eingabeliste jedoch falsche Typen enthält ClassCastException, wird a möglicherweise an einer völlig anderen Stelle im Code ausgegeben, und es kann schwierig sein, dies zu debuggen und herauszufinden, wo das falsche Element in die Liste aufgenommen wurde. Das schlimmste Problem ist, dass jemand möglicherweise sogar die ungültigen Elemente hinzufügt, nachdem die Liste umgewandelt wurde, was das Debuggen noch schwieriger macht.

Das Problem des Debuggens dieser Fehler ClassCastExceptionskann mit der Collections#checkedCollectionMethodenfamilie behoben werden.


Filtern der Liste nach Typ

Eine typsicherere Methode zum Konvertieren von a List<Supertype> nach a List<Subtype>besteht darin , die Liste tatsächlich zu filtern und eine neue Liste zu erstellen, die nur Elemente mit einem bestimmten Typ enthält. Es gibt einige Freiheitsgrade für die Implementierung einer solchen Methode (z. B. in Bezug auf die Behandlung von nullEinträgen), aber eine mögliche Implementierung kann folgendermaßen 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-Supertyp-Beziehung bezüglich der Typparameter), wie in diesem 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]
Marco13
quelle
7

Sie können nicht besetzen List<TestB>, List<TestA>wie Steve Kuo erwähnt, ABER Sie können den Inhalt von List<TestA>in ablegen List<TestB>. 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 und die Elemente (TestB-Objekte) zur Liste hinzufügt. Ich hoffe das funktioniert bei dir.

Martinatime
quelle
Warum wurde dies abgelehnt? Es war nützlich für mich und ich sehe keine Erklärung für die Abwahl.
Zod
7
Sicher ist dieser Beitrag nicht falsch. Aber die Person, die die Frage gestellt hat, braucht endlich eine Liste von TestB. In diesem Fall ist diese Antwort irreführend. Sie können alle Elemente in Liste <TestB> zu Liste <TestA> hinzufügen, indem Sie Liste <TestA> .addAll (Liste <TestB>) aufrufen. Sie können List <TestB> .addAll (List <TestA>) jedoch nicht verwenden.
Prageeth
6

Der beste sichere Weg besteht darin, AbstractListElemente zu implementieren und in die Implementierung umzuwandeln. Ich habe eine ListUtilHilfsklasse 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 castMethode verwenden, um Objekte in der Liste blind zu konvertieren, und die convertMethode zum sicheren Casting. 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
}
Igor Zubchenok
quelle
4

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

List<TestB> list = new ArrayList<TestB> (
    Arrays.asList (
        testAList.toArray(new TestB[0])
    )
);
Peter Heusch
quelle
2
Sehr seltsam finde ich, dass dies keine Compiler-Warnung gibt.
Simon Forsberg
Dies ist eine Seltsamkeit bei Arrays. Seufz, Java-Arrays sind so nutzlos. Aber ich denke, es ist sicherlich nicht schlechter und viel lesbarer als andere Antworten. Ich sehe das nicht unsicherer als eine Besetzung.
Thufir
3

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

Java hat keine impliziten Regeln zum Konvertieren von Objekttypen. (Im Gegensatz zu Primitiven)

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

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.

Peter Lawrey
quelle
2

Wenn Sie ein Objekt der Klasse haben TestA, können Sie es nicht umwandeln TestB. Jeder TestBist ein TestA, aber nicht der andere Weg.

im folgenden Code:

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

Die zweite Zeile würde a werfen ClassCastException.

Sie können eine TestAReferenz nur umwandeln, wenn das Objekt selbst ist TestB. beispielsweise:

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

Daher können Sie eine Liste von nicht immer in eine Liste von TestAumwandeln TestB.

cd1
quelle
1
Ich denke, er spricht von so etwas wie: Sie erhalten 'a' als Methodenargument, das erstellt wird als - TestA a = new TestB (), dann möchten Sie das TestB-Objekt erhalten und müssen es dann umwandeln - TestB b = ( TestB) a;
Samsamara
2

Sie können die selectInstancesMethode in Eclipse-Sammlungen verwenden . Dies beinhaltet die Erstellung einer neuen Kollektion, ist jedoch nicht so effizient wie die akzeptierte Lösung, die Casting verwendet.

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 StringBufferin das Beispiel aufgenommen, um zu zeigen, dass selectInstancesnicht nur der Typ heruntergespielt wird, sondern auch gefiltert wird, wenn die Sammlung gemischte Typen enthält.

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

Donald Raab
quelle
1

Dies ist aufgrund der Lö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 eins zum anderen werfen - es gibt nichts zu wirken.

n3rd
quelle
"Es gibt nichts zu besetzen" und "Man kann nicht eins zum anderen besetzen" ist ein kleiner Widerspruch. Integer j = 42; Integer i = (Integer) j;funktioniert gut, beide sind ganze Zahlen, beide haben dieselbe Klasse, also "gibt es nichts zu wirken".
Simon Forsberg
1

Das Problem ist, dass Ihre Methode KEINE Liste von TestA zurückgibt, wenn sie einen TestB enthält. Was ist also, 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 so gut, wie Sie es sich erhoffen können (Eclipse warnt Sie vor einer ungeprüften Besetzung, die genau das ist, was Sie tun, also meh). Können Sie dies nutzen, um Ihr Problem zu lösen? Eigentlich können Sie deshalb:

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, oder? oder um genau zu sein:

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

scheint für mich ganz gut zu kompilieren.

Bill K.
quelle
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);
}

Dieser Test zeigt , wie zu werfen List<Object>zu List<MyClass>. Sie müssen jedoch darauf achten, dass objectListInstanzen des gleichen Typs wie enthalten sind MyClass. Und dieses Beispiel kann berücksichtigt werden, wenn List<T>es verwendet wird. Holen Sie sich zu diesem Zweck das Feld Class<T> clazzin den Konstruktor und verwenden Sie es anstelle von MyClass.class.

Alex Po
quelle
0

Das sollte funktionieren

List<TestA> testAList = new ArrayList<>();
List<TestB> testBList = new ArrayList<>()
testAList.addAll(new ArrayList<>(testBList));
anonymouse
quelle
1
Dies macht eine unnötige Kopie.
Defnull
0

Es ist ziemlich seltsam, dass das manuelle Casting einer Liste immer noch nicht von einer Toolbox bereitgestellt wird, die Folgendes implementiert:

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

Natürlich werden die Elemente nicht einzeln überprüft, aber genau das möchten wir hier vermeiden, wenn wir wissen, dass unsere Implementierung nur den Untertyp bereitstellt.

Donatello
quelle