Como você lança uma lista de supertipos para uma lista de subtipos?


244

Por exemplo, digamos que você tenha duas classes:

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

Eu tenho um método que retorna List<TestA>ae gostaria de converter todos os objetos nessa lista para TestBque eu acabe com a List<TestB>.

Respostas:


578

Simplesmente converter List<TestB>quase funciona; mas não funciona porque você não pode converter um tipo genérico de um parâmetro para outro. No entanto, você pode transmitir através de um tipo curinga intermediário e isso será permitido (já que você pode transmitir para e a partir de tipos curinga, apenas com um aviso desmarcado):

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

88
Com todo o respeito, acho que essa é uma solução hacky - você está se esquivando do tipo de segurança que Java está tentando fornecer. Pelo menos, veja este tutorial sobre Java ( docs.oracle.com/javase/tutorial/java/generics/subtyping.html ) e considere por que você tem esse problema antes de corrigi-lo dessa maneira.
Jritz42

63
@ jfritz42 Eu não chamaria isso de segurança do tipo "esquivando-se". Nesse caso, você sabe que o Java não tem. É para isso que serve o elenco.
Planky

3
Isso me permite escrever: List <String> myList = (List <String>) (List <?>) (New ArrayList <Integer> ()); Isso falharia em tempo de execução. Eu prefiro algo como Lista <? estende Number> myList = new ArrayList <Integer> ();
Bentaye

1
Concordo sinceramente com @ jfritz42, ​​eu estava tendo o mesmo problema e verifiquei o URL que ele forneceu e foi capaz de resolver meu problema sem a transmissão.
Sam Orozco

@ jfritz42 isso não é funcionalmente diferente de lançar um objeto para um número inteiro, o que é permitido pelo compilador
kcazllerraf

116

a conversão de genéricos não é possível, mas se você definir a lista de outra maneira, será possível armazenar o TestB nela:

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

Você ainda tem a verificação de tipo para fazer quando estiver usando os objetos na lista.


14
Isso nem é sintaxe Java válida.
New123

2
Isso é verdade, mas era mais ilustrativo do que factualmente correctas
Salandur

Eu tenho o seguinte método: createSingleString (List <? Extends Object> objects) Dentro desse método eu chamo String.valueOf (Object) para recolher a lista em uma string. Funciona muito bem quando a entrada é List <Long>, List <Boolean>, etc. Obrigado!
Chris Sprague

2
E se TestA for interface?
Suvitruf - Andrei Apanasik

8
Você pode dar um MCVE onde isso realmente funciona? Eu entendo Cannot instantiate the type ArrayList<? extends TestA>.
Nateowami #

42

Você realmente não pode *:

Exemplo é retirado deste tutorial Java

Suponha que existem dois tipos Ae Btal B extends A. O código a seguir está correto:

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

O código anterior é válido porque Bé uma subclasse de A. Agora, o que acontece com List<A>e List<B>?

Acontece que List<B>não é uma subclasse de, List<A>portanto, não podemos escrever

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

Além disso, nem podemos escrever

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

*: Para possibilitar a transmissão, precisamos de um pai comum para ambos List<A>e List<B>: List<?>por exemplo. O seguinte é válido:

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

Você receberá, no entanto, um aviso. Você pode suprimi-lo adicionando @SuppressWarnings("unchecked")ao seu método.


2
Além disso, o uso do material neste link pode evitar a conversão desmarcada, pois o compilador pode decifrar toda a conversão.
Ryan H.

29

Com o Java 8, você realmente pode

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

32
Isso está realmente lançando a lista? Porque ele se parece mais com uma série de operações para iterar a lista inteira, convertendo cada elemento e, em seguida, adicionando-os a uma lista recém-instanciada do tipo desejado. Dito de outra forma, você não poderia fazer exatamente isso com o Java7 - com um new List<TestB>, um loop e um elenco?
Patrick M

5
Do OP "e eu gostaria de lançar todos os objetos dessa lista" - então, na verdade, essa é uma das poucas respostas que responde à pergunta conforme indicado, mesmo que não seja a pergunta que as pessoas que navegam aqui estão atrás.
Luke Usherwood

17

Eu acho que você está lançando na direção errada ... se o método retornar uma lista de TestAobjetos, não será realmente seguro lançá-los TestB.

Basicamente, você está solicitando ao compilador que permita executar TestBoperações em um tipo TestAque não as suporta.


11

Como essa é uma pergunta amplamente referenciada e as respostas atuais explicam principalmente por que ela não funciona (ou propõe soluções perigosas e hacky que eu nunca gostaria de ver no código de produção), acho apropriado adicionar outra resposta, mostrando as armadilhas e uma possível solução.


A razão pela qual isso não funciona em geral já foi apontada em outras respostas: A validade ou não da conversão depende dos tipos de objetos contidos na lista original. Quando houver objetos na lista cujo tipo não seja do tipo TestB, mas de uma subclasse diferente de TestA, a conversão não será válida.


Obviamente, os lançamentos podem ser válidos. Às vezes, você tem informações sobre os tipos que não estão disponíveis para o compilador. Nesses casos, é possível lançar as listas, mas, em geral, não é recomendado :

Pode-se também ...

  • ... lançar a lista inteira ou
  • ... lança todos os elementos da lista

As implicações da primeira abordagem (que corresponde à resposta atualmente aceita) são sutis. Pode parecer funcionar corretamente à primeira vista. Mas se houver tipos errados na lista de entrada, a ClassCastExceptionserá lançada, talvez em um local completamente diferente no código, e pode ser difícil depurar isso e descobrir onde o elemento errado entrou na lista. O pior problema é que alguém pode até adicionar os elementos inválidos após a lista ser lançada, tornando a depuração ainda mais difícil.

O problema de depurar esses espúrios ClassCastExceptionspode ser aliviado com a Collections#checkedCollectionfamília de métodos.


Filtrando a lista com base no tipo

Uma maneira mais segura de se converter de um List<Supertype> para um List<Subtype>é filtrar a lista e criar uma nova lista que contenha apenas elementos que tenham determinado tipo. Existem alguns graus de liberdade para a implementação de um método desse tipo (por exemplo, com relação ao tratamento de nullentradas), mas uma possível implementação pode ser assim:

/**
 * 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 pode ser usado para filtrar listas arbitrárias (não apenas com um determinado relacionamento Subtype-Supertype em relação aos parâmetros de tipo), como neste exemplo:

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

Você não pode lançar List<TestB>a List<TestA>como Steve Kuo menciona, mas você pode despejar o conteúdo de List<TestA>dentro List<TestB>. Tente o seguinte:

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

Eu não tentei esse código, provavelmente há erros, mas a idéia é que ele itere através do objeto de dados, adicionando os elementos (objetos TestB) à Lista. Espero que funcione para você.


por que isso foi votado? Foi útil para mim, e não vejo explicação para a votação final.
Zod 28/03/11

7
Certamente este post não está incorreto. Mas a pessoa que fez a pergunta finalmente precisa de uma lista de TestB. Nesse caso, esta resposta é enganosa. Você pode adicionar todos os elementos da Lista <TestB> à Lista <TestA> chamando List <TestA> .addAll (Lista <TestB>). Mas você não pode usar List <TestB> .addAll (List <TestA>);
prageeth

6

A melhor maneira segura é implementar AbstractListe lançar itens na implementação. Eu criei a ListUtilclasse auxiliar:

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

Você pode usar o castmétodo para converter objetos às cegas na lista e no convertmétodo para transmitir com segurança. Exemplo:

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

A única maneira que eu sei é copiando:

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

2
Muito estranho, acho que isso não dá um aviso ao compilador.
Simon Forsberg

isso é uma singularidade com matrizes. Suspiro, matrizes java são tão inúteis. Mas acho que certamente não é pior e muito legível do que outras respostas. Não vejo isso mais perigoso do que um elenco.
Thufir

3

Quando você lança uma referência ao objeto, está apenas lançando o tipo da referência, não o tipo do objeto. a conversão não altera o tipo real do objeto.

Java não possui regras implícitas para converter tipos de objetos. (Ao contrário dos primitivos)

Em vez disso, você precisa fornecer como converter um tipo para outro e chamá-lo 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));
}

Isso é mais detalhado do que em um idioma com suporte direto, mas funciona e você não precisa fazer isso com muita frequência.


2

se você tiver um objeto da classe TestA, não poderá convertê-lo TestB. Todo TestBé um TestA, mas não o contrário.

no seguinte código:

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

a segunda linha lançaria a ClassCastException.

você só pode converter uma TestAreferência se o próprio objeto for TestB. por exemplo:

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

portanto, nem sempre você pode converter uma lista TestAem uma lista de TestB.


1
Eu acho que ele está falando sobre algo como, você obtém 'a' como um argumento de método criado como - TestA a = new TestB (), então você deseja obter o objeto TestB e, em seguida, precisa convertê-lo - TestB b = ( TesteB) a;
samsamara

2

Você pode usar o selectInstancesmétodo nas Coleções Eclipse . Isso envolverá a criação de uma nova coleção, no entanto, portanto, não será tão eficiente quanto a solução aceita que usa a conversão.

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

Eu incluí StringBufferno exemplo para mostrar que selectInstancesnão apenas o tipo de downcasts, mas também filtrará se a coleção contiver tipos mistos.

Nota: Sou um colaborador das Coleções Eclipse.


1

Isso é possível devido ao apagamento do tipo. Vai descobrir que

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

Internamente, ambas as listas são do tipo List<Object>. Por esse motivo, você não pode converter um para o outro - não há nada para transmitir.


"Não há nada para lançar" e "Você não pode lançar um para o outro" é um pouco contraditório. Integer j = 42; Integer i = (Integer) j;funciona bem, ambos são números inteiros, ambos têm a mesma classe, portanto "não há nada a transmitir".
Simon Forsberg

1

O problema é que seu método NÃO retorna uma lista de TestA se ele contém um TestB, e daí se ele foi digitado corretamente? Então este elenco:

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

funciona tão bem quanto você poderia esperar (Eclipse avisa sobre um elenco não verificado, que é exatamente o que você está fazendo, então meh). Então, você pode usar isso para resolver seu problema? Na verdade, você pode por causa disso:

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

Exatamente o que você pediu, certo? ou para ser realmente exato:

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

parece compilar muito bem para mim.


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

Este teste mostra como transmitir List<Object>para List<MyClass>. Mas você precisa prestar atenção, pois objectListdeve conter instâncias do mesmo tipo que MyClass. E este exemplo pode ser considerado quando List<T>é usado. Para esse fim, pegue o campo Class<T> clazzno construtor e use-o em vez de MyClass.class.


0

Isso deveria funcionar

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

1
Isso faz uma cópia desnecessária.
defnull 12/09/17

0

É bastante estranho que a transmissão manual de uma lista ainda não seja fornecida por alguma caixa de ferramentas que implementa algo como:

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

Obviamente, isso não verificará os itens um por um, mas é exatamente isso que queremos evitar aqui, se soubermos que nossa implementação fornece apenas o subtipo.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.