Come si fa a lanciare un elenco di supertipi in un elenco di sottotipi?


244

Ad esempio, supponiamo che tu abbia due classi:

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

Ho un metodo che restituisce a List<TestA>e vorrei lanciare tutti gli oggetti in quell'elenco in TestBmodo da finire con a List<TestB>.

Risposte:


578

Semplicemente casting per List<TestB>quasi funzionare; ma non funziona perché non puoi trasmettere un tipo generico di un parametro a un altro. Tuttavia, è possibile eseguire il cast di un tipo di carattere jolly intermedio e sarà consentito (poiché è possibile eseguire il cast da e verso i tipi di carattere jolly, solo con un avviso non selezionato):

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

88
Con tutto il rispetto, penso che questa sia una specie di soluzione caotica - stai schivando il tipo di sicurezza che Java sta cercando di fornirti. Almeno guarda questo tutorial Java ( docs.oracle.com/javase/tutorial/java/generics/subtyping.html ) e considera perché hai questo problema prima di risolverlo in questo modo.
jfritz42,

63
@ jfritz42 Non la definirei una sicurezza di tipo "schivare". In questo caso hai conoscenza che Java non ha. Ecco a cosa serve il casting.
Planky,

3
Questo mi permette di scrivere: List <String> myList = (List <String>) (List <?>) (New ArrayList <Integer> ()); Ciò si arresterebbe in modo anomalo in fase di esecuzione. Preferisco qualcosa come Elenco <? extends Number> myList = new ArrayList <Integer> ();
Bentaye,

1
Sono sinceramente d'accordo con @ jfritz42 che stavo avendo lo stesso problema e ho guardato l'URL che ha fornito e sono stato in grado di risolvere il mio problema senza il casting.
Sam Orozco,

@ jfritz42 questo non è funzionalmente diverso dal lanciare un oggetto in un numero intero, cosa consentita dal compilatore
kcazllerraf,

116

il casting di generici non è possibile, ma se si definisce l'elenco in un altro modo è possibile memorizzare TestB in esso:

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

È ancora necessario eseguire il controllo del tipo quando si utilizzano gli oggetti nell'elenco.


14
Questa non è nemmeno una sintassi Java valida.
Newacct

2
È vero, ma era più illustrativo che effettivamente corretto
Salandur il

Ho il seguente metodo: createSingleString (List <? Extends Object> oggetti) All'interno di questo metodo chiamo String.valueOf (Object) per comprimere l'elenco in una stringa. Funziona alla grande quando l'input è List <Long>, List <Boolean>, ecc. Grazie!
Chris Sprague,

2
Cosa succede se TestA è interfaccia?
Suvitruf - Andrei Apanasik,

8
Puoi dare un MCVE dove questo funziona davvero? Ho capito Cannot instantiate the type ArrayList<? extends TestA>.
Nateowami,

42

Non puoi davvero *:

L'esempio è tratto da questo tutorial Java

Supponiamo che ci siano due tipi Ae Btale B extends A. Quindi il seguente codice è corretto:

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

Il codice precedente è valido perché Bè una sottoclasse di A. Ora, cosa succede con List<A>e List<B>?

Si scopre che List<B>non è una sottoclasse di List<A>quindi non possiamo scrivere

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

Inoltre, non possiamo nemmeno scrivere

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

*: Per rendere possibile il casting, abbiamo bisogno di un genitore comune per entrambi List<A>e List<B>: List<?>per esempio. È valido quanto segue :

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

Riceverai comunque un avviso. Puoi sopprimerlo aggiungendo @SuppressWarnings("unchecked")al tuo metodo.


2
Inoltre, l'utilizzo del materiale in questo collegamento può evitare la conversione non selezionata, poiché il compilatore può decifrare l'intera conversione.
Ryan H,

29

Con Java 8, in realtà è possibile

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

32
Sta effettivamente lanciando la lista? Perché legge più come una serie di operazioni per scorrere su tutto l'elenco, lanciare ogni elemento e quindi aggiungerli a un elenco appena istanziato del tipo desiderato. Detto in altro modo, non potresti fare esattamente questo con Java7- con a new List<TestB>, un loop e un cast?
Patrick M,

5
Dall'OP "e vorrei lanciare tutti gli oggetti in quell'elenco" - quindi in realtà, questa è una delle poche risposte che risponde alla domanda come indicato, anche se non è la domanda che le persone navigano qui.
Luke Usherwood,

17

Penso che stai lanciando nella direzione sbagliata però ... se il metodo restituisce un elenco di TestAoggetti, allora non è davvero sicuro lanciarli TestB.

Fondamentalmente stai chiedendo al compilatore di permetterti di eseguire TestBoperazioni su un tipo TestAche non li supporta.


11

Dato che questa è una domanda ampiamente referenziata e le risposte attuali spiegano principalmente perché non funziona (o propongono soluzioni pericolose, che non vorrei mai vedere nel codice di produzione), penso che sia opportuno aggiungere un'altra risposta, mostrando le insidie ​​e una possibile soluzione.


Il motivo per cui ciò non funziona in generale è già stato sottolineato in altre risposte: la validità effettiva della conversione dipende dai tipi di oggetti contenuti nell'elenco originale. Quando ci sono oggetti nella lista il cui tipo non è di tipo TestB, ma di una sottoclasse diversa di TestA, allora il cast non è valido.


Naturalmente, i cast possono essere validi. A volte hai informazioni sui tipi che non sono disponibili per il compilatore. In questi casi, è possibile eseguire il cast degli elenchi, ma in generale non è consigliabile :

Uno potrebbe o ...

  • ... cast tutta la lista o
  • ... cast tutti gli elementi dell'elenco

Le implicazioni del primo approccio (che corrisponde alla risposta attualmente accettata) sono sottili. A prima vista potrebbe sembrare che funzioni correttamente. Ma se ci sono tipi errati nell'elenco di input, allora ClassCastExceptionverrà lanciato un, forse in una posizione completamente diversa nel codice, e potrebbe essere difficile eseguire il debug di questo e scoprire dove l'elemento sbagliato è scivolato nell'elenco. Il problema peggiore è che qualcuno potrebbe persino aggiungere gli elementi non validi dopo che l'elenco è stato lanciato, rendendo ancora più difficile il debug.

Il problema del debug di questi spuri ClassCastExceptionspuò essere alleviato con la Collections#checkedCollectionfamiglia di metodi.


Filtro dell'elenco in base al tipo

Un modo più sicuro per la conversione da a List<Supertype> a List<Subtype>è in realtà filtrare l'elenco e creare un nuovo elenco che contenga solo elementi di un determinato tipo. Vi sono alcuni gradi di libertà per l'implementazione di un tale metodo (ad esempio per quanto riguarda il trattamento delle nullvoci), ma una possibile implementazione può apparire così:

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

Questo metodo può essere utilizzato per filtrare elenchi arbitrari (non solo con una data relazione Sottotipo-Supertipo relativa ai parametri del tipo), come in questo esempio:

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

7

Non puoi lanciare List<TestB>a List<TestA>come cita Steve Kuo ma è possibile scaricare il contenuto di List<TestA>in List<TestB>. Prova quanto segue:

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

Non ho provato questo codice, quindi probabilmente ci sono errori, ma l'idea è che dovrebbe iterare attraverso l'oggetto dati aggiungendo gli elementi (oggetti TestB) all'Elenco. Spero che funzioni per te.


perché questo è stato votato? Mi è stato utile e non vedo alcuna spiegazione per il voto negativo.
zod,

7
Sicuramente questo post non è errato. Ma la persona che ha posto la domanda ha finalmente bisogno di un elenco di TestB. In tal caso questa risposta è fuorviante. È possibile aggiungere tutti gli elementi in Elenco <TestB> a Elenco <TestA> chiamando Elenco <TestA> .addAll (Elenco <TestB>). Ma non puoi usare List <TestB> .addAll (List <TestA>);
Prageeth

6

Il modo più sicuro è implementare AbstractListe lanciare oggetti in fase di implementazione. Ho creato la ListUtilclasse helper:

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

Puoi usare il castmetodo per lanciare alla cieca oggetti nell'elenco e il convertmetodo per lanciare in sicurezza. Esempio:

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

L'unico modo che conosco è copiando:

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

2
Abbastanza strano, penso che questo non dia un avvertimento al compilatore.
Simon Forsberg,

questa è una stranezza con le matrici. Sospiro, gli array java sono così inutili. Ma penso che non sia certamente peggio, e molto leggibile, di altre risposte. Non lo vedo più pericoloso di un cast.
Giovedì

3

Quando lanci un riferimento a un oggetto, stai semplicemente trasmettendo il tipo di riferimento, non il tipo di oggetto. il casting non cambierà il tipo effettivo dell'oggetto.

Java non ha regole implicite per la conversione dei tipi di oggetto. (A differenza dei primitivi)

Invece è necessario fornire come convertire un tipo in un altro e chiamarlo manualmente.

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

Questo è più dettagliato che in una lingua con supporto diretto, ma funziona e non dovresti farlo molto spesso.


2

se hai un oggetto della classe TestA, non puoi lanciarlo TestB. ogni TestBè unTestA , ma non viceversa.

nel seguente codice:

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

la seconda linea lancerebbe a ClassCastException .

puoi lanciare un TestAriferimento solo se l'oggetto stesso è TestB. per esempio:

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

quindi, potresti non sempre trasmettere un elenco TestAa un elenco di TestB.


1
Penso che stia parlando di qualcosa del genere, ottieni 'a' come argomento del metodo che viene creato come - TestA a = new TestB (), quindi vuoi ottenere l'oggetto TestB e quindi devi lanciarlo - TestB b = ( TestB) a;
samsamara,

2

È possibile utilizzare il selectInstancesmetodo nelle raccolte Eclipse . Ciò ha comportato la creazione di una nuova collezione, tuttavia non sarà efficiente come la soluzione accettata che utilizza il casting.

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

Ho incluso StringBuffernell'esempio per dimostrarloselectInstances non solo esegue il downcast del tipo, ma filtrerà anche se la raccolta contiene tipi misti.

Nota: sono un committer per le raccolte Eclipse.


1

Ciò è possibile a causa della cancellazione del tipo. Lo troverai

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

Internamente entrambi gli elenchi sono di tipo List<Object>. Per questo motivo non puoi lanciare l'uno sull'altro: non c'è nulla da lanciare.


"Non c'è niente da lanciare" e "Non puoi lanciare l'uno all'altro" è un po 'una contraddizione. Integer j = 42; Integer i = (Integer) j;funziona bene, entrambi sono numeri interi, entrambi hanno la stessa classe, quindi "non c'è nulla da trasmettere".
Simon Forsberg,

1

Il problema è che il tuo metodo NON restituisce un elenco di TestA se contiene un TestB, quindi cosa succede se è stato digitato correttamente? Quindi questo cast:

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

funziona così come potresti sperare (Eclipse ti avverte di un cast non controllato che è esattamente quello che stai facendo, quindi meh). Quindi puoi usarlo per risolvere il tuo problema? In realtà è possibile per questo motivo:

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

Esattamente quello che hai chiesto, giusto? o per essere veramente esatti:

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

sembra compilarsi bene per me.


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

Questo test dimostra come lanciare List<Object>a List<MyClass>. Ma è necessario prestare attenzione a che objectListdeve contenere istanze dello stesso tipo di MyClass. E questo esempio può essere considerato quando List<T>viene utilizzato. A questo scopo, ottieni il campo Class<T> clazznel costruttore e usalo al posto di MyClass.class.


0

Questo dovrebbe funzionare

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

1
Questo rende una copia non necessaria.
defnull

0

Abbastanza strano che il casting manuale di un elenco non sia ancora fornito da alcuni toolbox che implementano qualcosa del tipo:

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

Naturalmente, questo non controllerà gli elementi uno per uno, ma questo è esattamente ciò che vogliamo evitare qui, se sappiamo bene che la nostra implementazione fornisce solo il sottotipo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.