web-dev-qa-db-fra.com

Comment transtypez-vous une liste de super-types en une liste de sous-types?

Par exemple, supposons que vous ayez deux classes:

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

J'ai une méthode qui retourne un List<TestA> et je voudrais convertir tous les objets de cette liste en TestB pour que je me retrouve avec un List<TestB>.

224
Jeremy Logan

Le simple fait d'utiliser List<TestB> fonctionne presque; mais cela ne fonctionne pas car vous ne pouvez pas convertir un type générique d'un paramètre en un autre. Cependant, vous pouvez utiliser un type générique intermédiaire, ce qui sera autorisé (puisque vous pouvez utiliser un type de caractère générique, avec juste un avertissement non vérifié):

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

la génération de génériques n'est pas possible, mais si vous définissez la liste d'une autre manière, il est possible d'y stocker TestB:

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

Vous devez toujours vérifier le type lorsque vous utilisez les objets de la liste.

112
Salandur

Vous ne pouvez vraiment pas *:

Exemple tiré de ce tutoriel Java

Supposons qu'il existe deux types A et B tels que B extends A. Alors le code suivant est correct:

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

Le code précédent est valide car B est une sous-classe de A. Maintenant, que se passe-t-il avec List<A> et List<B>?

Il s'avère que List<B> n'est pas une sous-classe de List<A> donc nous ne pouvons pas écrire

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

En outre, nous ne pouvons pas même écrire

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

*: Pour rendre le casting possible, nous avons besoin d'un parent commun pour List<A> et List<B>: List<?> par exemple. Ce qui suit est valide:

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

Vous recevrez cependant un avertissement. Vous pouvez le supprimer en ajoutant @SuppressWarnings("unchecked") à votre méthode.

40
Steve Kuo

Avec Java 8, vous pouvez réellement

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

Je pense cependant que vous lancez dans la mauvaise direction ... si la méthode renvoie une liste d'objets TestA, il n'est alors pas prudent de les convertir en TestB.

En gros, vous demandez au compilateur de vous permettre d'effectuer TestB opérations sur un type TestA qui ne les prend pas en charge.

18
jerryjvl

Comme cette question est largement référencée et que les réponses actuelles expliquent principalement pourquoi elle ne fonctionne pas (ou ne propose pas de solutions dangereuses, que je n’aimerais jamais voir dans le code de production), je pense qu’il est approprié d’ajouter une autre réponse, en montrant: les pièges, et une solution possible.


La raison pour laquelle cela ne fonctionne pas en général a déjà été signalée dans d'autres réponses: La validité de la conversion dépend des types d'objets contenus dans la liste d'origine. Lorsqu'il y a des objets dans la liste dont le type n'est pas de type TestB, mais d'une sous-classe différente de TestA, la conversion est pas valide.


Bien sûr, les conversions peuvent être valides. Vous avez parfois des informations sur les types qui ne sont pas disponibles pour le compilateur. Dans ces cas, il est possible de convertir les listes, mais en général, il est non recommandé :

On pourrait soit ...

  • ... jeter toute la liste ou
  • ... Cast tous les éléments de la liste

Les implications de la première approche (qui correspond à la réponse actuellement acceptée) sont subtiles. Cela peut sembler fonctionner correctement au premier abord. Mais s'il y a des types incorrects dans la liste d'entrée, alors un ClassCastException sera jeté, peut-être à un endroit complètement différent du code, et il sera peut-être difficile de le déboguer et de savoir où le mauvais élément s'est glissé. la liste. Le pire problème est que quelqu'un pourrait même ajouter les éléments non valides après que la liste ait été lancée, ce qui rend le débogage encore plus difficile.

Le problème du débogage de ces parasites ClassCastExceptions peut être résolu avec la famille Collections#checkedCollection .


Filtrer la liste en fonction du type

Un moyen plus sûr de taper de la conversion d'un _List<Supertype>_ à un _List<Subtype>_ consiste à filtrer la liste et à créer une nouvelle liste contenant uniquement des éléments qui ont un certain type. Il existe quelques degrés de liberté pour la mise en œuvre d'une telle méthode (par exemple, en ce qui concerne le traitement des entrées null), mais une mise en œuvre possible pourrait ressembler à ceci:

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

Cette méthode peut être utilisée afin de filtrer des listes arbitraires (pas seulement avec une relation donnée Subtype-Supertype concernant les paramètres de type), comme dans cet exemple:

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

Vous ne pouvez pas convertir List<TestB> en List<TestA> comme le mentionne Steve Kuo MAIS vous pouvez vider le contenu de List<TestA> en List<TestB>. Essayez ce qui suit:

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

Je n'ai pas essayé ce code, donc il y a probablement des erreurs, mais l'idée est qu'il devrait parcourir l'objet de données en ajoutant les éléments (objets TestB) à la liste. J'espère que cela fonctionne pour vous.

6
martinatime

La meilleure méthode consiste à implémenter un AbstractList et à transtyper les éléments. J'ai créé la classe d'assistance ListUtil:

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

Vous pouvez utiliser la méthode cast pour convertir en aveugle des objets dans la liste et la méthode convert pour une diffusion en toute sécurité. Exemple:

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

Lorsque vous transformez une référence d'objet, vous ne faites que transtyper le type de la référence, pas le type de l'objet. casting ne changera pas le type réel de l'objet.

Java n'a pas de règles implicites pour convertir les types d'objet. (Contrairement aux primitives)

Au lieu de cela, vous devez indiquer comment convertir un type en un autre et l’appeler manuellement.

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

C'est plus bavard que dans une langue avec support direct, mais cela fonctionne et vous ne devriez pas avoir à le faire très souvent.

3
Peter Lawrey

Le seul moyen que je connaisse est de copier:

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

Ceci est possible en raison de la suppression du type. Vous trouverez que

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

En interne, les deux listes sont de type List<Object>. Pour cette raison, vous ne pouvez pas lancer l'un sur l'autre - il n'y a rien à lancer.

2
n3rd

si vous avez un objet de la classe TestA, vous ne pouvez pas le convertir en TestB. chaque TestB est un TestA, mais pas l'inverse.

dans le code suivant:

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

la deuxième ligne lancerait un ClassCastException.

vous ne pouvez transtyper une référence TestA que si l'objet lui-même est TestB. par exemple:

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

ainsi, vous ne pouvez pas toujours convertir une liste de TestA en une liste de TestB.

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

Ce test montre comment convertir List<Object> en List<MyClass>. Mais vous devez faire attention à ce que objectList doit contenir des instances du même type que MyClass. Et cet exemple peut être considéré lorsque List<T> est utilisé. A cette fin, récupérez le champ Class<T> clazz dans le constructeur et utilisez-le à la place de MyClass.class.

1
Alex Po

Le problème est que votre méthode ne renvoie PAS une liste de TestA si elle contient un TestB, alors que si elle a été correctement tapée? Puis ce casting:

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

travaille à peu près aussi bien que vous pouvez l’espérer (Eclipse vous avertit d’un casting non contrôlé qui correspond exactement à ce que vous faites, alors me voilà). Alors pouvez-vous utiliser cela pour résoudre votre problème? En fait, vous pouvez pour cette raison:

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

Exactement ce que vous avez demandé, non? ou pour être vraiment exact:

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

semble compiler très bien pour moi.

1
Bill K

Vous pouvez utiliser la méthode selectInstances dans Eclipse Collections . Cela impliquera la création d'une nouvelle collection, mais ne sera donc pas aussi efficace que la solution acceptée qui utilise le 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);

J'ai inclus StringBuffer dans l'exemple pour montrer que selectInstances non seulement rétrograder le type, mais également filtrer si la collection contient des types mélangés.

Remarque: je suis un partisan des collections Eclipse.

1
Donald Raab

Cela devrait fonctionner

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