web-dev-qa-db-fra.com

Méthode astucieuse pour parcourir des tableaux parallèles en Java en utilisant foreach

J'ai hérité d'un tas de code qui utilise énormément les tableaux parallèles pour stocker les paires clé/valeur. En fait, il était logique de le faire de cette façon, mais il est plutôt délicat d’écrire des boucles qui parcourent ces valeurs. J'aime beaucoup la nouvelle construction Java foreach, mais il ne semble pas qu'il soit possible de parcourir des listes parallèles à l'aide de cette méthode.

Avec une boucle for normale, je peux le faire facilement:

for (int i = 0; i < list1.length; ++i) {
    doStuff(list1[i]);
    doStuff(list2[i]);
}

Mais à mon avis, cela n’est pas sémantiquement pur, car nous ne vérifions pas les limites de list2 pendant l’itération. Existe-t-il une syntaxe intelligente similaire à celle que je peux utiliser avec des listes parallèles?

31
Travis Webb

Je voudrais utiliser un Map moi-même. Mais en vous rendant à votre mot qu’une paire de tableaux a un sens dans votre cas, qu’en est-il d’une méthode utilitaire qui prend vos deux tableaux et retourne un wrapper Iterable?

Conceptuellement:

for (Pair<K,V> p : wrap(list1, list2)) {
    doStuff(p.getKey());
    doStuff(p.getValue());
}

Le wrapper Iterable<Pair<K,V>> masquerait la vérification des limites.

21
Isaac Truett

Sur la page officielle Oracle sur la boucle for améliorée:

Enfin, il n'est pas utilisable pour les boucles cela doit itérer sur plusieurs collections en parallèle. Celles-ci les lacunes étaient connues par le les concepteurs, qui ont fait une conscience décision d'aller avec un propre, simple construire qui couvrirait le grand majorité des cas.

En gros, il vaut mieux utiliser la boucle for normale.

Si vous utilisez ces paires de tableaux pour simuler une carte, vous pouvez toujours écrire une classe qui implémente l'interface de carte avec les deux tableaux. cela pourrait vous permettre d'abstraire une grande partie de la boucle. 

Sans regarder votre code, je ne peux pas vous dire si cette option est la meilleure voie à suivre, mais c'est quelque chose que vous pourriez envisager. 

10
Tikhon Jelvis

C'était un exercice amusant. J'ai créé un objet appelé ParallelList qui prend un nombre variable de listes typées et peut parcourir les valeurs à chaque index (renvoyé sous forme de liste de valeurs):

public class ParallelList<T> implements Iterable<List<T>> {

    private final List<List<T>> lists;

    public ParallelList(List<T>... lists) {
        this.lists = new ArrayList<List<T>>(lists.length);
        this.lists.addAll(Arrays.asList(lists));
    }

    public Iterator<List<T>> iterator() {
        return new Iterator<List<T>>() {
            private int loc = 0;

            public boolean hasNext() {
                boolean hasNext = false;
                for (List<T> list : lists) {
                    hasNext |= (loc < list.size());
                }
                return hasNext;
            }

            public List<T> next() {
                List<T> vals = new ArrayList<T>(lists.size());
                for (int i=0; i<lists.size(); i++) {
                    vals.add(loc < lists.get(i).size() ? lists.get(i).get(loc) : null);
                }
                loc++;
                return vals;
            }

            public void remove() {
                for (List<T> list : lists) {
                    if (loc < list.size()) {
                        list.remove(loc);
                    }
                }
            }
        };
    }
}

Exemple d'utilisation:

List<Integer> list1 = Arrays.asList(new Integer[] {1, 2, 3, 4, 5});
List<Integer> list2 = Arrays.asList(new Integer[] {6, 7, 8});
ParallelList<Integer> list = new ParallelList<Integer>(list1, list2);
for (List<Integer> ints : list) {
    System.out.println(String.format("%s, %s", ints.get(0), ints.get(1)));
}

Ce qui imprimerait:

1, 6
2, 7
3, 8
4, null
5, null

Cet objet supporte les listes de longueurs variables, mais il pourrait être modifié pour être plus strict.

Malheureusement, je ne pouvais pas me débarrasser d'un avertissement du compilateur sur le constructeur ParallelList: A generic array of List<Integer> is created for varargs parameters, alors si quelqu'un sait comment s'en débarrasser, faites le moi savoir :)

9
Sean Adkinson

Vous pouvez utiliser une deuxième contrainte dans votre boucle for:

    for (int i = 0; i < list1.length && i < list2.length; ++i) 
    {
      doStuff(list1[i]);
      doStuff(list2[i]);
    }//for

L'une de mes méthodes préférées pour parcourir les collections est la boucle for-each, mais comme le mentionne le tutoriel Oracle, lorsque vous utilisez des collections parallèles, utilisez le itérateur plutôt que le for-each .

Ce qui suit est une réponse de Martin v. Löwis dans un message similaire post :

it1 = list1.iterator();
it2 = list2.iterator();
while(it1.hasNext() && it2.hasNext()) 
{
   value1 = it1.next();
   value2 = it2.next();

   doStuff(value1);
   doStuff(value2);
}//while

L'avantage pour l'itérateur est qu'il est générique. Si vous ne connaissez pas les collections utilisées, utilisez-le, sinon, si vous savez quelles sont vos collections, vous connaissez les fonctions longueur/taille et les fonctions habituelles de la boucle for avec la contrainte supplémentaire peut être utilisé ici. (Notez que je suis très pluriel dans ce billet, car une possibilité intéressante serait dans laquelle les collections utilisées sont différentes, par exemple l'une pourrait être une liste et l'autre un tableau par exemple)

J'espère que cela a aidé.

6
Alexei Blue

Avec Java 8, je les utilise pour boucler de manière sexy:

//parallel loop
public static <A, B> void loop(Collection<A> a, Collection<B> b, IntPredicate intPredicate, BiConsumer<A, B> biConsumer) {
    Iterator<A> ait = a.iterator();
    Iterator<B> bit = b.iterator();
    if (ait.hasNext() && bit.hasNext()) {
        for (int i = 0; intPredicate.test(i); i++) {
            if (!ait.hasNext()) {
                ait = a.iterator();
            }
            if (!bit.hasNext()) {
                bit = b.iterator();
            }
            biConsumer.accept(ait.next(), bit.next());
        }
    }
}

//nest loop
public static <A, B> void loopNest(Collection<A> a, Collection<B> b, BiConsumer<A, B> biConsumer) {
    for (A ai : a) {
        for (B bi : b) {
            biConsumer.accept(ai, bi);
        }
    }
}

Quelques exemples, avec ces 2 listes:

List<Integer> a = Arrays.asList(1, 2, 3);
List<String> b = Arrays.asList("a", "b", "c", "d");

Boucle d'une taille minimale de a et b:

loop(a, b, i -> i < Math.min(a.size(), b.size()), (x, y) -> {
    System.out.println(x +  " -> " + y);
});

Sortie:

1 -> a
2 -> b
3 -> c

Boucle d'une taille maximale de a et b (les éléments de la liste plus courte seront cyclés):

loop(a, b, i -> i < Math.max(a.size(), b.size()), (x, y) -> {
    System.out.println(x +  " -> " + y);
});

Sortie:

1 -> a
2 -> b
3 -> c
1 -> d

Boucle n fois ((les éléments seront cyclés si n est plus grand que la taille des listes)):

loop(a, b, i -> i < 5, (x, y) -> {
    System.out.println(x +  " -> " + y);
});

Sortie:

1 -> a
2 -> b
3 -> c
1 -> d
2 -> a

Boucle pour toujours:

loop(a, b, i -> true, (x, y) -> {
    System.out.println(x +  " -> " + y);
});

Appliquez à votre situation:

loop(list1, list2, i -> i < Math.min(a.size(), b.size()), (e1, e2) -> {
    doStuff(e1);
    doStuff(e2);
});
1
yelliver

Réponse simple: n ° 

Vous voulez une itération sexy et du code byte Java? Découvrez Scala: Scala pour une boucle sur deux listes simultanément

Déni de responsabilité: Il s'agit bien d'une réponse "utiliser une autre langue". Croyez-moi, j'aimerais que Java ait une itération parallèle sexy, mais personne n'a commencé à développer en Java parce qu'ils veulent du code sexy.

1
Joseph Lust

ArrayIterator vous permet d’éviter l’indexation, mais vous ne pouvez pas utiliser une boucle for-each sans écrire une classe ou au moins une fonction séparée. Comme le fait remarquer @Alexei Blue, la recommandation officielle (dans L'interface Collection ) est la suivante: «Utilisez Iterator au lieu de la construction for-each lorsque vous devez:… parcourir plusieurs collections en parallèle.":

import static com.google.common.base.Preconditions.checkArgument;
import org.Apache.commons.collections.iterators.ArrayIterator;

// …

  checkArgument(array1.length == array2.length);
  Iterator it1 = ArrayIterator(array1);
  Iterator it2 = ArrayIterator(array2);
  while (it1.hasNext()) {
      doStuff(it1.next());
      doOtherStuff(it2.next());
  }

Toutefois:

  • L'indexation est naturelle pour les tableaux - un tableau est par définition quelque chose que vous indexez, et une boucle numérique, comme dans votre code d'origine, est parfaitement naturelle et plus directe.
  • Les paires clé-valeur forment naturellement une variable Map, comme le remarque @Isaac Truett. Ainsi, la plus propre serait de créer des cartes pour tous vos tableaux parallèles (cette boucle ne serait donc que dans la fonction usine qui crée les cartes), bien que cela ne soit pas efficace si vous voulez juste les parcourir. (Utilisez Multimap si vous devez prendre en charge les doublons.)
  • Si vous en avez beaucoup, vous pouvez (partiellement) implémenter ParallelArrayMap<> (c'est-à-dire une carte supportée par des tableaux parallèles), ou peut-être ParallelArrayHashMap<> (pour ajouter une HashMap si vous voulez une recherche efficace par clé), et l'utiliser, ce qui permet l'itération dans l'ordre d'origine. C'est probablement exagéré, mais permet une réponse sexy.

C'est:

Map<T, U> map = new ParallelArrayMap<>(array1, array2);
for (Map.Entry<T, U> entry : map.entrySet()) {
  doStuff(entry.getKey());
  doOtherStuff(entry.getValue());
}

Philosophiquement, le style Java doit avoir les types explicit, nommés, implémentés par les classes. Ainsi, lorsque vous dites «[J'ai] des tableaux parallèles [qui] stockent des paires clé/valeur.», Java répond: «Écrivez une classe ParallelArrayMap qui implémente Map (paires clé/valeur) et qui possède un constructeur prenant des tableaux parallèles, puis vous pouvez utiliser entrySet pour renvoyer une Set sur laquelle vous pouvez effectuer une nouvelle itération, puisque Set implémente Collection. ”- crée la structure explicit dans un type, implémenté par une classe.

Pour itérer sur deux collections ou matrices parallèles, vous voulez itérer sur un Iterable<Pair<T, U>>, que les langages moins explicites vous permettent de créer avec Zip (que @Isaac Truett appelle wrap). Ce n'est pas Java idiomatique, cependant - quels sont les éléments de la paire? Voir Java: Comment écrire une fonction Zip? Quel devrait être le type de retour? pour une discussion approfondie sur la façon d’écrire cela en Java et sur la raison pour laquelle cela est découragé.

C'est exactement le compromis stylistique que Java fait: vous savez exactement quel type est tout, et vous devez pour le spécifier et le mettre en œuvre.

0
Nils von Barth