Comment convertir une liste de supertypes en une liste de sous-types?

244

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 pour TestBque je me retrouve avec un List<TestB>.

Jeremy Logan
la source

Réponses:

578

Il suffit de lancer pour List<TestB>presque travailler; 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 transtyper via un type de caractère générique intermédiaire et il sera autorisé (puisque vous pouvez convertir vers et depuis des types de caractère générique, juste avec un avertissement non coché):

List<TestB> variable = (List<TestB>)(List<?>) collectionOfListA;
newacct
la source
88
Avec tout le respect que je vous dois, je pense que c'est une sorte de solution hacky - vous esquivez le type de sécurité que Java essaie de vous fournir. Regardez au moins ce didacticiel Java ( docs.oracle.com/javase/tutorial/java/generics/subtyping.html ) et demandez-vous pourquoi vous avez ce problème avant de le résoudre de cette façon.
jfritz42
63
@ jfritz42 Je n'appellerais pas cela une sécurité de type "esquivant". Dans ce cas, vous savez que Java n'a pas. C'est à ça que sert le casting.
Planky
3
Cela me permet d'écrire: List <String> myList = (List <String>) (List <?>) (New ArrayList <Integer> ()); Cela se bloquerait lors de l'exécution. Je préfère quelque chose comme List <? étend Number> myList = new ArrayList <Integer> ();
Bentaye
1
Je suis honnêtement d'accord avec @ jfritz42. J'avais le même problème et j'ai regardé l'URL qu'il a fournie et j'ai pu résoudre mon problème sans le casting.
Sam Orozco
@ jfritz42 ce n'est pas fonctionnellement différent de la conversion d'un objet en un entier, ce qui est autorisé par le compilateur
kcazllerraf
116

la conversion 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.

Salandur
la source
14
Ce n'est même pas une syntaxe Java valide.
newacct
2
C'est vrai, mais c'était plus illustratif que factuellement correct
Salandur
J'ai la méthode suivante: createSingleString (Liste <? Étend l'objet> objets) À l'intérieur de cette méthode j'appelle String.valueOf (Object) pour réduire la liste en une chaîne. Cela fonctionne très bien lorsque l'entrée est List <Long>, List <Boolean>, etc. Merci!
Chris Sprague
2
Et si TestA est une interface?
Suvitruf - Andrei Apanasik
8
Pouvez-vous donner un MCVE où cela fonctionne réellement? Je comprends Cannot instantiate the type ArrayList<? extends TestA>.
Nateowami
42

Vous ne pouvez vraiment pas *:

Un exemple est tiré de ce tutoriel Java

Supposons qu'il existe deux types Aet Btels que B extends A. Ensuite, le code suivant est correct:

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

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

Il s'avère que ce 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>

De plus, nous ne pouvons même pas é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 les deux 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.

Steve Kuo
la source
2
En outre, l'utilisation du contenu de ce lien peut éviter la conversion non contrôlée, car le compilateur peut déchiffrer la conversion entière.
Ryan H
29

Avec Java 8, vous pouvez réellement

List<TestB> variable = collectionOfListA
    .stream()
    .map(e -> (TestB) e)
    .collect(Collectors.toList());
fracz
la source
32
Est-ce que cela fait réellement partie de la liste? Parce qu'il ressemble plus à une série d'opérations pour itérer sur toute la liste, en castant chaque élément, puis en les ajoutant à une liste nouvellement instanciée du type souhaité. Autrement dit, ne pourriez-vous pas faire exactement cela avec Java7 - avec un new List<TestB>, une boucle et un casting?
Patrick M
5
De l'OP "et je voudrais lancer tous les objets de cette liste" - donc en fait, c'est l'une des rares réponses qui répond à la question comme indiqué, même si ce n'est pas la question que les gens parcourant ici sont après.
Luke Usherwood
17

Je pense que vous lancez dans la mauvaise direction cependant ... si la méthode retourne une liste d' TestAobjets, alors il n'est vraiment pas sûr de les convertir enTestB .

Fondamentalement, vous demandez au compilateur de vous permettre d'effectuer des TestBopérations sur un type TestAqui ne les prend pas en charge.

jerryjvl
la source
11

Étant donné qu'il s'agit d'une question largement référencée et que les réponses actuelles expliquent principalement pourquoi cela ne fonctionne pas (ou proposent des solutions hacky et dangereuses que je n'aimerais jamais voir dans le code de production), je pense qu'il est approprié d'ajouter une autre réponse, 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é ou non 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 n'est pas valide.


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

On pourrait soit ...

  • ... lancer toute la liste ou
  • ... lancer 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 coup d'œil. Mais s'il y a des types incorrects dans la liste d'entrée, alors un ClassCastExceptionsera jeté, peut-être à un emplacement complètement différent dans le code, et il peut être difficile de le déboguer et de découvrir où le mauvais élément s'est glissé dans la liste. Le pire problème est que quelqu'un pourrait même ajouter les éléments invalides après la conversion de la liste, ce qui rend le débogage encore plus difficile.

Le problème du débogage de ces parasites ClassCastExceptionspeut être atténué avec la Collections#checkedCollectionfamille de méthodes.


Filtrage de la liste en fonction du type

Une façon plus sûre de convertir un de un List<Supertype> en un List<Subtype>consiste à filtrer la liste et à créer une nouvelle liste qui ne contient que des éléments qui ont un certain type. Il existe certains degrés de liberté pour la mise en œuvre d'une telle méthode (par exemple en ce qui concerne le traitement des nullentrées), mais une mise en œuvre possible peut 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 Subtype-Supertype donnée 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]
Marco13
la source
7

Vous ne pouvez pas lancer List<TestB>à List<TestA>comme Steve Kuo mentionne mais vous pouvez vider le contenu de List<TestA>dans 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 itérer à travers l'objet de données en ajoutant les éléments (objets TestB) dans la liste. J'espère que cela fonctionne pour vous.

martinatime
la source
pourquoi ce vote a-t-il été rejeté? Cela m'a été utile et je ne vois aucune explication au vote négatif.
2011
7
Certes, ce message n'est pas incorrect. Mais la personne qui a posé la question a enfin besoin d'une liste de TestB. Dans ce cas, cette réponse est trompeuse. Vous pouvez ajouter tous les éléments de List <TestB> à List <TestA> en appelant List <TestA> .addAll (List <TestB>). Mais vous ne pouvez pas utiliser List <TestB> .addAll (List <TestA>);
prageeth
6

Le meilleur moyen sûr consiste à implémenter un AbstractListélément et à le convertir en implémentation. J'ai créé une ListUtilclasse d'assistance:

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 une castméthode pour lancer aveuglément des objets dans la liste et une convertméthode 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
}
Igor Zubchenok
la source
4

La seule façon que je connaisse est de copier:

List<TestB> list = new ArrayList<TestB> (
    Arrays.asList (
        testAList.toArray(new TestB[0])
    )
);
Peter Heusch
la source
2
Assez étrange, je pense que cela ne donne pas d'avertissement au compilateur.
Simon Forsberg
c'est une bizarrerie avec les tableaux. Soupir, les tableaux Java sont tellement inutiles. Mais, je pense que ce n'est certainement pas pire et beaucoup plus lisible que les autres réponses. Je ne vois pas cela plus dangereux qu'un casting.
Thufir
3

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

Java n'a pas de règles implicites pour convertir les types d'objets. (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 verbeux que dans une langue avec un support direct, mais cela fonctionne et vous ne devriez pas avoir besoin de le faire très souvent.

Peter Lawrey
la source
2

si vous avez un objet de la classe TestA, vous ne pouvez pas le convertir TestB. tout TestBest 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 convertir une TestAréférence que si l'objet lui-même l'est TestB. par exemple:

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

ainsi, vous ne pouvez pas toujours lancer une liste de TestAdans une liste de TestB.

cd1
la source
1
Je pense qu'il parle de quelque chose comme, vous obtenez 'a' comme argument de méthode qui est créé comme - TestA a = nouveau TestB (), puis vous voulez obtenir l'objet TestB et ensuite vous devez le lancer - TestB b = ( TestB) a;
samsamara
2

Vous pouvez utiliser la selectInstancesméthode dans les collections Eclipse . Cela impliquera la création d'une nouvelle collection mais ne sera donc pas aussi efficace que la solution acceptée qui utilise le moulage.

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 StringBufferdans l'exemple pour montrer que selectInstancesnon seulement rétrograde le type, mais filtrera également si la collection contient des types mixtes.

Remarque: je suis un committer pour les collections Eclipse.

Donald Raab
la source
1

Cela est possible en raison de l'effacement du type. Vous constaterez 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.

n3rd
la source
"Il n'y a rien à lancer" et "Vous ne pouvez pas lancer l'un à l'autre" est un peu contradictoire. Integer j = 42; Integer i = (Integer) j;fonctionne très bien, les deux sont des entiers, ils ont tous les deux la même classe, donc "il n'y a rien à lancer".
Simon Forsberg
1

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

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

fonctionne aussi bien que vous pouvez l'espérer (Eclipse vous avertit d'une distribution non contrôlée qui est exactement ce que vous faites, alors meh). Pouvez-vous donc l'utiliser pour résoudre votre problème? En fait, vous pouvez à cause de cela:

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.

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

Ce test montre comment caster List<Object>vers List<MyClass>. Mais vous devez faire attention à ce qu'il objectListdoit contenir des instances du même type que MyClass. Et cet exemple peut être pris en compte lorsqu'il List<T>est utilisé. Pour cela, récupérez le champ Class<T> clazzdans le constructeur et utilisez-le à la place de MyClass.class.

Alex Po
la source
0

Cela devrait fonctionner

List<TestA> testAList = new ArrayList<>();
List<TestB> testBList = new ArrayList<>()
testAList.addAll(new ArrayList<>(testBList));
anonymouse
la source
1
Cela fait une copie inutile.
Defnull
0

Il est assez étrange que la conversion manuelle d'une liste ne soit toujours pas fournie par une boîte à outils implémentant quelque chose comme:

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

Bien sûr, cela ne vérifiera pas les éléments un par un, mais c'est précisément ce que nous voulons éviter ici, si nous savons bien que notre implémentation ne fournit que le sous-type.

Donatello
la source