¿Cómo se transmite una Lista de supertipos a una Lista de subtipos?

244

Por ejemplo, supongamos que tiene dos clases:

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

Tengo un método que devuelve ay List<TestA>me gustaría convertir todos los objetos en esa lista para TestBque termine con a List<TestB>.

Jeremy Logan
fuente

Respuestas:

578

Simplemente echando a List<TestB>casi obras; pero no funciona porque no puede emitir un tipo genérico de un parámetro a otro. Sin embargo, puede emitir a través de un tipo de comodín intermedio y se permitirá (ya que puede emitir desde y hacia tipos de comodín, solo con una advertencia sin marcar):

List<TestB> variable = (List<TestB>)(List<?>) collectionOfListA;
newacct
fuente
88
Con el debido respeto, creo que esta es una especie de solución extravagante: está esquivando el tipo de seguridad que Java está tratando de proporcionarle. Al menos, mire este tutorial de Java ( docs.oracle.com/javase/tutorial/java/generics/subtyping.html ) y considere por qué tiene este problema antes de solucionarlo de esta manera.
jfritz42
63
@ jfritz42 No lo llamaría seguridad de tipo "esquiva". En este caso tienes conocimiento que Java no tiene. Para eso es el casting.
Planky
3
Esto me permite escribir: List <String> myList = (List <String>) (List <?>) (New ArrayList <Integer> ()); Esto se estrellaría en tiempo de ejecución. Prefiero algo como List <? extiende Number> myList = new ArrayList <Integer> ();
Bentaye
1
Sinceramente, estoy de acuerdo con @ jfritz42 Estaba teniendo el mismo problema y miré la URL que me proporcionó y pude resolver mi problema sin el casting.
Sam Orozco
@ jfritz42 esto no es funcionalmente diferente de lanzar un Objeto a un Entero, lo cual está permitido por el compilador
kcazllerraf
116

La conversión de genéricos no es posible, pero si define la lista de otra manera, es posible almacenar TestB en ella:

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

Todavía tiene que hacer una verificación de tipo cuando usa los objetos de la lista.

Salandur
fuente
14
Esto ni siquiera es una sintaxis Java válida.
newacct
2
Eso es cierto, pero fue más ilustrativo que correcto
Salandur
Tengo el siguiente método: createSingleString (Lista <? Extiende Objeto> objetos) Dentro de este método llamo a String.valueOf (Objeto) para colapsar la lista en una sola cadena. Funciona muy bien cuando la entrada es List <Long>, List <Boolean>, etc. ¡Gracias!
Chris Sprague
2
¿Qué pasa si TestA es la interfaz?
Suvitruf - Andrei Apanasik
8
¿Puedes dar un MCVE donde esto realmente funciona? Consigo Cannot instantiate the type ArrayList<? extends TestA>.
Nateowami
42

Realmente no puedes *:

Se toma un ejemplo de este tutorial de Java

Suponga que hay dos tipos Ay Btal que B extends A. Entonces el siguiente código es correcto:

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

El código anterior es válido porque Bes una subclase de A. Ahora, ¿qué pasa con List<A>y List<B>?

Resulta que List<B>no es una subclase de, List<A>por lo tanto, no podemos escribir

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

Además, ni siquiera podemos escribir

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

*: Para hacer posible el casting necesitamos un padre común para ambos List<A>y List<B>: List<?>por ejemplo. Lo siguiente es válido:

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

Sin embargo, recibirá una advertencia. Puede suprimirlo agregando @SuppressWarnings("unchecked")a su método.

Steve Kuo
fuente
2
Además, el uso del material en este enlace puede evitar la conversión no verificada, ya que el compilador puede descifrar la conversión completa.
Ryan H
29

Con Java 8, en realidad puedes

List<TestB> variable = collectionOfListA
    .stream()
    .map(e -> (TestB) e)
    .collect(Collectors.toList());
fracz
fuente
32
¿Es eso realmente echando la lista? Porque se lee más como una serie de operaciones para iterar sobre la lista completa, convertir cada elemento y luego agregarlos a una lista recién instanciada del tipo deseado. Dicho de otra manera, ¿no podrías hacer exactamente esto con Java7, con un new List<TestB>, un bucle y un elenco?
Patrick M
55
Desde el OP "y me gustaría emitir todos los objetos en esa lista", así que en realidad, esta es una de las pocas respuestas que responde a la pregunta como se indicó, incluso si no es la pregunta que buscan las personas que navegan aquí.
Luke Usherwood el
17

Sin embargo, creo que está lanzando en la dirección incorrecta ... si el método devuelve una lista de TestAobjetos, entonces no es seguro enviarlos TestB.

Básicamente, le está pidiendo al compilador que le permita realizar TestBoperaciones en un tipo TestAque no los admite.

jerryjvl
fuente
11

Dado que esta es una pregunta ampliamente referenciada, y las respuestas actuales explican principalmente por qué no funciona (o proponen soluciones extravagantes y peligrosas que nunca me gustaría ver en el código de producción), creo que es apropiado agregar otra respuesta, mostrando las trampas y una posible solución.


La razón por la cual esto no funciona en general ya se ha señalado en otras respuestas: si la conversión es realmente válida o no depende de los tipos de objetos que están contenidos en la lista original. Cuando hay objetos en la lista cuyo tipo no es de tipo TestB, sino de una subclase diferente de TestA, entonces la conversión no es válida.


Por supuesto, los moldes pueden ser válidos. A veces tiene información sobre los tipos que no está disponible para el compilador. En estos casos, es posible emitir las listas, pero en general, no se recomienda :

Uno podría ...

  • ... emitir toda la lista o
  • ... emitir todos los elementos de la lista

Las implicaciones del primer enfoque (que corresponde a la respuesta actualmente aceptada) son sutiles. Puede parecer que funciona correctamente a primera vista. Pero si hay tipos incorrectos en la lista de entrada, se ClassCastExceptionarrojará un , tal vez en una ubicación completamente diferente en el código, y puede ser difícil depurar esto y descubrir dónde se deslizó el elemento incorrecto en la lista. El peor problema es que alguien podría incluso agregar los elementos inválidos después de que la lista ha sido lanzada, lo que dificulta aún más la depuración.

El problema de depurar estos espurios ClassCastExceptionspuede aliviarse con la Collections#checkedCollectionfamilia de métodos.


Filtrar la lista según el tipo

Una forma más segura de convertir de una List<Supertype> a una List<Subtype>es filtrar la lista y crear una nueva lista que contenga solo elementos que tengan cierto tipo. Hay algunos grados de libertad para la implementación de dicho método (por ejemplo, con respecto al tratamiento de las nullentradas), pero una posible implementación puede tener este aspecto:

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

Este método se puede utilizar para filtrar listas arbitrarias (no solo con una relación Subtipo-Supertipo dada con respecto a los parámetros de tipo), como en este ejemplo:

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

No se puede echar List<TestB>a List<TestA>como lo menciona Steve Kuo pero usted puede volcar el contenido de List<TestA>dentro List<TestB>. Intenta lo siguiente:

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

No he probado este código, por lo que probablemente haya errores, pero la idea es que debería iterar a través del objeto de datos agregando los elementos (objetos TestB) a la Lista. Espero que eso funcione para usted.

martinatime
fuente
¿Por qué fue rechazado? Fue útil para mí y no veo ninguna explicación para el voto negativo.
zod
77
Seguramente esta publicación no es incorrecta. Pero la persona que hizo la pregunta finalmente necesita una lista de TestB. En ese caso, esta respuesta es engañosa. Puede agregar todos los elementos en List <TestB> a List <TestA> llamando a List <TestA> .addAll (List <TestB>). Pero no puede usar List <TestB> .addAll (List <TestA>);
prageeth
6

La mejor manera segura es implementar AbstractListy lanzar elementos en la implementación. He creado ListUtilclase de ayuda:

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

Puede usar el castmétodo para lanzar objetos a ciegas en la lista y el convertmétodo para el lanzamiento seguro. Ejemplo:

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
fuente
4

La única forma que sé es copiando:

List<TestB> list = new ArrayList<TestB> (
    Arrays.asList (
        testAList.toArray(new TestB[0])
    )
);
Peter Heusch
fuente
2
Bastante extraño, creo que esto no da una advertencia del compilador.
Simon Forsberg
Esto es una rareza con las matrices. Suspiro, las matrices de Java son tan inútiles. Pero, creo que ciertamente no es peor, y mucho más legible, que otras respuestas. No lo veo más inseguro que un elenco.
Thufir
3

Cuando emite una referencia de objeto, solo está emitiendo el tipo de referencia, no el tipo de objeto. el lanzamiento no cambiará el tipo real del objeto.

Java no tiene reglas implícitas para convertir tipos de objetos. (A diferencia de las primitivas)

En su lugar, debe proporcionar cómo convertir un tipo a otro y llamarlo 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));
}

Esto es más detallado que en un lenguaje con soporte directo, pero funciona y no debería necesitar hacer esto con mucha frecuencia.

Peter Lawrey
fuente
2

Si tiene un objeto de la clase TestA, no puede lanzarlo TestB. todo TestBes un TestA, pero no al revés.

en el siguiente código:

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

la segunda línea arrojaría un ClassCastException.

solo puede emitir una TestAreferencia si el objeto en sí lo es TestB. por ejemplo:

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

por lo tanto, no siempre puede emitir una lista TestAa una lista de TestB.

cd1
fuente
1
Creo que está hablando de algo así, obtienes 'a' como argumento de método que se crea como - TestA a = new TestB (), luego quieres obtener el objeto TestB y luego necesitas lanzarlo - TestB b = ( Prueba B) a;
samsamara
2

Puede usar el selectInstancesmétodo en Eclipse Collections . Sin embargo, esto implicará crear una nueva colección, por lo que no será tan eficiente como la solución aceptada que utiliza la conversión.

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

Incluí StringBufferen el ejemplo para mostrar que selectInstancesno solo rechaza el tipo, sino que también filtra si la colección contiene tipos mixtos.

Nota: Soy un committer para Eclipse Collections.

Donald Raab
fuente
1

Esto es posible debido al tipo de borrado. Encontrarás que

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

Internamente ambas listas son de tipo List<Object>. Por esa razón no puedes lanzar uno al otro, no hay nada que lanzar.

n3er
fuente
"No hay nada que lanzar" y "No se puede lanzar uno al otro" es un poco una contradicción. Integer j = 42; Integer i = (Integer) j;funciona bien, ambos son enteros, ambos tienen la misma clase, por lo que "no hay nada que emitir".
Simon Forsberg
1

El problema es que su método NO devuelve una lista de TestA si contiene un TestB, entonces, ¿qué sucede si se escribió correctamente? Entonces este elenco:

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

funciona tan bien como podrías esperar (Eclipse te advierte de un elenco sin control que es exactamente lo que estás haciendo, así que meh). Entonces, ¿puedes usar esto para resolver tu problema? En realidad puedes debido a esto:

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

Exactamente lo que pediste, ¿verdad? o para ser realmente exacto:

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

Parece compilar bien para mí.

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

Esta prueba muestra cómo lanzar List<Object>a List<MyClass>. Pero debe prestar atención a que objectListdebe contener instancias del mismo tipo que MyClass. Y este ejemplo puede considerarse cuando List<T>se usa. Para este propósito, obtenga el campo Class<T> clazzen el constructor y úselo en lugar de MyClass.class.

Alex Po
fuente
0

Esto debería funcionar

List<TestA> testAList = new ArrayList<>();
List<TestB> testBList = new ArrayList<>()
testAList.addAll(new ArrayList<>(testBList));
anonymouse
fuente
1
Esto hace una copia innecesaria.
desactivado el
0

Es bastante extraño que la transmisión manual de una lista todavía no sea proporcionada por alguna caja de herramientas que implemente algo como:

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

Por supuesto, esto no verificará los elementos uno por uno, pero eso es precisamente lo que queremos evitar aquí, si sabemos bien que nuestra implementación solo proporciona el subtipo.

Donatello
fuente