web-dev-qa-db-fra.com

Création d'une liste distincte de la liste existante dans Java 7 et 8?

Si j'ai:

List<Integer> listInts = { 1, 1, 3, 77, 2, 19, 77, 123, 14, 123... }

in Java ce qui est un moyen efficace de créer un List<Integer> listDistinctInts contenant uniquement les valeurs distinctes de listInts?

Ma pensée immédiate est de créer un Set<Integer> setInts Contenant toutes les valeurs de listInts puis d'appeler List<Integer> listDistinctInts = new ArrayList<>(setInts);

Mais cela semble potentiellement inefficace - existe-t-il une meilleure solution en utilisant Java 7?

Je n'utilise pas Java 8, mais je pense qu'en l'utilisant, je pourrais faire quelque chose comme ça (?):

List<Integer> listDistinctInts = listInts.stream().distinct().collect(Collectors.toList());

Serait-ce plus performant que l'approche ci-dessus et/ou existe-t-il un moyen plus efficace de le faire dans Java 8?

Enfin, (et je suis conscient que poser plusieurs questions peut être mal vu mais c'est directement lié) si je ne me souciais que du nombre d'éléments distincts dans listInts existe-t-il un moyen plus efficace d'obtenir cette valeur (en Java 7 et 8) - sans créer d'abord une liste ou un ensemble de tous les éléments distincts?

Je suis le plus intéressé par les moyens natifs Java d'accomplir cela et d'éviter de réinventer des roues, mais envisagerais le code ou les bibliothèques roulés à la main s'ils offrent une meilleure clarté ou de meilleures performances. J'ai lu ceci question connexe Java - Liste distincte d'objets mais ce n'est pas tout à fait clair sur les différences de performances entre les approches Java 7 et 8 ou s'il existe de meilleures techniques?

20
Matt Coubrough

J'ai maintenant MicroBenchmarké la plupart des options proposées parmi les excellentes réponses fournies. Comme la plupart des questions non triviales liées aux performances, la réponse à la meilleure est "cela dépend".

Tous mes tests ont été effectués avec [~ # ~] jmh [~ # ~] Java Microbenchmarking Harnais .

La plupart de ces tests ont été effectués avec JDK 1.8, bien que j'aie également effectué certains des tests avec JDK 1.7 juste pour m'assurer que ses performances n'étaient pas trop différentes (elles étaient presque identiques). J'ai testé les techniques suivantes tirées des réponses fournies jusqu'à présent:


1. Java 8 Stream - La solution utilisant stream() J'avais proposé comme possibilité si j'utilisais Java8:

public List<Integer> testJava8Stream(List<Integer> listInts) {
    return listInts.stream().distinct().collect(Collectors.toList());
}

pros modern Java 8, pas de dépendances tierces

contre Nécessite Java 8


2. Ajout à la liste - La solution proposée par Victor2748 où une nouvelle liste est construite et ajoutée, si et seulement si la liste ne contient pas déjà la valeur. Notez que je préalloue également la liste de destination à la taille de l'original (le maximum possible) pour éviter toute réallocation:

public List<Integer> testAddingToList(List<Integer> listInts) {
    List<Integer> listDistinctInts = new ArrayList<>(listInts.size());
    for(Integer i : listInts)
    {
        if( !listDistinctInts.contains(i) ) { listDistinctInts.add(i); }
    }
    return listDistinctInts;
}

pros Fonctionne dans toutes les versions Java, pas besoin de créer un ensemble puis de copier, pas de 3e) deps parti

contre Doit vérifier à plusieurs reprises la liste pour les valeurs existantes pendant que nous la construisons


3. GS Collections Fast (maintenant collections Eclipse) - La solution proposée par Craig P. Motlin utilisant le GS Collections bibliothèque et leur type de liste personnalisé FastList:

public List<Integer> testGsCollectionsFast(FastList listFast)
{
    return listFast.distinct();
}

pros Code expressif simple et très rapide, fonctionne en Java 7 et 8 =

contre Nécessite une bibliothèque tierce et un FastList plutôt qu'un List<Integer> régulier


4. GS Collections Adapted - La solution FastList ne comparait pas tout à fait comparable car elle nécessitait un FastList passé à la méthode plutôt qu'un bon vieux 'ArrayList<Integer> J'ai donc également testé la méthode de l'adaptateur proposée par Craig:

public List<Integer> testGsCollectionsAdapted(List<Integer> listInts)
{
    return listAdapter.adapt(listInts).distinct();
}

pros Ne nécessite pas de FastList, fonctionne en Java 7 et 8

contre Doit adapter List afin de ne pas fonctionner aussi bien, a besoin d'une bibliothèque tierce


5. Guava ImmutableSet - La méthode proposée par Louis Wasserman dans les commentaires, et par 卢 声 远 Shengyuan L dans leur réponse en utilisant Goyave :

public List<Integer> testGuavaImmutable(List<Integer> listInts)
{
    return ImmutableSet.copyOf(listInts).asList();
}

pros Apparemment très rapide, fonctionne en Java 7 ou 8

contre Renvoie un Immutable List, ne peut pas gérer les valeurs nulles dans la liste d'entrée et nécessite une bibliothèque tierce


7. HashSet - Mon idée originale (également recommandée par EverV0id , lix et Radiodef)

public List<Integer> testHashSet(List<Integer> listInts)
{
    return new ArrayList<Integer>(new HashSet<Integer>(listInts));
}

pros Fonctionne dans Java 7 et 8, pas de dépendances tierces

contre Ne conserve pas l'ordre d'origine de la liste, doit construire l'ensemble puis le copier dans la liste.


6. LinkedHashSet - Puisque la solution HashSet n'a pas conservé l'ordre des nombres entiers dans la liste d'origine, j'ai également testé une version qui utilise LinkedHashSet pour préserver l'ordre:

public List<Integer> testLinkedHashSet(List<Integer> listInts)
{
    return new ArrayList<Integer>(new LinkedHashSet<Integer>(listInts));
}

pros Conserve l'ordre d'origine, fonctionne en Java 7 et 8, pas de dépendances tierces

contre Peu probable d'être aussi rapide qu'une approche régulière HashSet


Résultats

Voici mes résultats pour différentes tailles de listInts (résultats classés du plus lent au plus rapide):

1. prenant distinct de ArrayList de 100 000 entrées aléatoires entre 0 et 50 000 (c'est-à-dire grande liste, quelques doublons)

Benchmark                Mode       Samples     Mean   Mean error    Units

AddingToList            thrpt        10        0.505        0.012    ops/s
Java8Stream             thrpt        10      234.932       31.959    ops/s
LinkedHashSet           thrpt        10      262.185       16.679    ops/s
HashSet                 thrpt        10      264.295       24.154    ops/s
GsCollectionsAdapted    thrpt        10      357.998       18.468    ops/s
GsCollectionsFast       thrpt        10      363.443       40.089    ops/s
GuavaImmutable          thrpt        10      469.423       26.056    ops/s

2. prenant distinct de ArrayList de 1000 entrées aléatoires entre 0 et 50 (c'est-à-dire liste moyenne, nombreux doublons)

Benchmark                Mode       Samples     Mean   Mean error    Units

AddingToList            thrpt        10    32794.698     1154.113    ops/s
HashSet                 thrpt        10    61622.073     2752.557    ops/s
LinkedHashSet           thrpt        10    67155.865     1690.119    ops/s
Java8Stream             thrpt        10    87440.902    13517.925    ops/s
GsCollectionsFast       thrpt        10   103490.738    35302.201    ops/s
GsCollectionsAdapted    thrpt        10   143135.973     4733.601    ops/s
GuavaImmutable          thrpt        10   186301.330    13421.850    ops/s

3. prenant distinct de ArrayList de 100 entes aléatoires entre 0-100 (c'est-à-dire petite liste, quelques doublons)

Benchmark                Mode       Samples     Mean   Mean error    Units

AddingToList            thrpt        10   278435.085    14229.285    ops/s
Java8Stream             thrpt        10   397664.052    24282.858    ops/s
LinkedHashSet           thrpt        10   462701.618    20098.435    ops/s
GsCollectionsAdapted    thrpt        10   477097.125    15212.580    ops/s
GsCollectionsFast       thrpt        10   511248.923    48155.211    ops/s
HashSet                 thrpt        10   512003.713    25886.696    ops/s
GuavaImmutable          thrpt        10  1082006.560    18716.012    ops/s

4. prenant distinct de ArrayList de 10 entrées aléatoires entre 0 et 50 (c.-à-d. petite liste, quelques doublons)

Benchmark                Mode       Samples     Mean   Mean error    Units

Java8Stream             thrpt        10  2739774.758   306124.297    ops/s
LinkedHashSet           thrpt        10  3607479.332   150331.918    ops/s
HashSet                 thrpt        10  4238393.657   185624.358    ops/s
GsCollectionsAdapted    thrpt        10  5919254.755   495444.800    ops/s
GsCollectionsFast       thrpt        10  7916079.963  1708778.450    ops/s
AddingToList            thrpt        10  7931479.667   966331.036    ops/s
GuavaImmutable          thrpt        10  9021621.880   845936.861    ops/s

Conclusions

  • Si vous ne prenez les éléments distincts d'une liste qu'une seule fois, et que la liste n'est pas très longue any de ces méthodes devraient être adéquates.

  • Les approches générales les plus efficaces sont venues des bibliothèques tierces: GS Collections et Guava se sont montrées admirablement.

  • Vous devrez peut-être tenir compte de la taille de votre liste et du nombre probable de doublons lors de la sélection de la méthode la plus performante.

  • L'approche naïve de l'ajout à une nouvelle liste uniquement si la valeur n'est pas déjà présente fonctionne très bien pour les petites listes, mais dès que vous avez plus d'une poignée de valeurs dans la liste d'entrée, elle exécute la pire des méthodes essayées.

  • La méthode Guava ImmutableSet.copyOf(listInts).asList() fonctionne le plus rapidement dans la plupart des situations. Mais prenez note des restrictions: la liste retournée est Immutable et la liste d'entrée ne peut pas contenir de null.

  • La méthode HashSet exécute le meilleur des approches non tierces et généralement mieux que Java 8 flux, mais réorganise les entiers (ce qui peut ou non être un problème en fonction de votre cas d'utilisation).

  • L'approche LinkedHashSet conserve l'ordre, mais sans surprise était généralement pire que la méthode HashSet.

  • Les méthodes HashSet et LinkedHashSet fonctionneront moins bien lors de l'utilisation de listes de types de données qui ont des calculs HashCode complexes, alors faites votre propre profilage si vous essayez de sélectionner des Foos distincts à partir d'un List<Foo>.

  • Si vous avez déjà GS Collections comme dépendance, cela fonctionne très bien et est plus flexible que l'approche ImmutableList Guava . Si vous ne l'avez pas en tant que dépendance, il convient d'envisager de l'ajouter si les performances de sélection d'éléments distincts sont essentielles aux performances de votre application.

  • Décevant Java 8 flux semblaient fonctionner assez mal. Il peut y avoir une meilleure façon de coder l'appel distinct() que la façon dont je l'ai utilisé, donc les commentaires ou autres réponses sont bien sûr Bienvenue.

NB. Je ne suis pas un expert en MicroBenchmarking, donc si quelqu'un trouve des défauts dans mes résultats ou ma méthodologie, veuillez m'en informer et je m'efforcerai de corriger la réponse.

30
Matt Coubrough

Si vous utilisez Collections Eclipse (anciennement Collections GS ), vous pouvez utiliser la méthode distinct().

ListIterable<Integer> listInts = FastList.newListWith(1, 1, 3, 77, 2, 19, 77, 123, 14, 123);
Assert.assertEquals(
        FastList.newListWith(1, 3, 77, 2, 19, 123, 14),
        listInts.distinct());

L'avantage d'utiliser distinct() au lieu de convertir en ensemble puis de revenir en liste est que distinct() préserve l'ordre de la liste d'origine, en conservant la première occurrence de chaque élément. Il est implémenté en utilisant à la fois un ensemble et une liste.

MutableSet<T> seenSoFar = UnifiedSet.newSet();
int size = list.size();
for (int i = 0; i < size; i++)
{
    T item = list.get(i);
    if (seenSoFar.add(item))
    {
        targetCollection.add(item);
    }
}
return targetCollection;

Si vous ne pouvez pas convertir votre liste d'origine en un type de collections GS, vous pouvez utiliser ListAdapter pour obtenir la même API.

MutableList<Integer> distinct = ListAdapter.adapt(integers).distinct();

Il n'y a aucun moyen d'éviter la création de l'ensemble. Pourtant, UnifiedSet est plus efficace que HashSet donc il y aura un certain avantage de vitesse.

Si tout ce que vous voulez, c'est le nombre d'éléments distincts, il est plus efficace de simplement créer un ensemble sans créer la liste.

Verify.assertSize(7, UnifiedSet.newSet(listInts));

Eclipse Collections 8.0 nécessite Java 8. Eclipse Collections 7.x fonctionne bien avec Java 8, mais ne nécessite que Java 5 .

Remarque: Je suis un committer pour les collections Eclipse.

3
Craig P. Motlin

Goyave peut être votre choix:

ImmutableSet<Integer> set = ImmutableSet.copyOf(listInts);

L'API est extrêmement optimisée.

Il est PLUS RAPIDE que listInts.stream().distinct() et new LinkedHashSet<>(listInts).

1

Vous devriez essayer new LinkedList(new HashSet(listInts)).

1
Everv0id

Lors de l'ajout d'une valeur à un listInts, vérifiez:

int valueToAdd;
//...
if (!listInts.contains(valueToAdd)) {listInts.add(valueToAdd)}

si vous avez une liste existante, utilisez une instruction for-each pour copier toutes les valeurs de cette liste, dans une nouvelle, que vous voulez être "distinctes":

List<Integer> listWithRepeatedValues;
List<Integer> distinctList;
//...
for (Integer i : listWithRepeatedValues) {
    if (!listInts.contains(valueToAdd)) {distinctList.add(i);}
}
0
Victor2748

Cela devrait fonctionner:

yourlist.stream (). map (votre wrapper qui remplace equals et hashchode method :: new) .distinct (). map (wrapper défini ci-dessus :: méthode qui retourne la sortie finale) .collect (Collectors.toList ());

0
Righto

Ne t'inquiète pas. L'utilisation d'un HashSet est un moyen assez simple et efficace d'éliminer les doublons:

    Set<Integer> uniqueList = new HashSet<>();
    uniqueList.addAll(listInts);   // Add all elements eliminating duplicates

    for (int n : uniqueList)       // Check the results (in no particular order)
        System.out.println(n);

    System.out.println("Number distinct values: " + uniqueList.size());

Dans un scénario plus spécifique, juste au cas où la plage de valeurs possibles est connue, elle n'est pas très grande, tandis que listInts est très grande.
La façon la plus efficace de compter le nombre d'entrées uniques dans la liste à laquelle je peux penser est:

    boolean[] counterTable = new boolean[124];
    int counter = 0;

    for (int n : listInts)
        if (!counterTable[n]) {
            counter++;
            counterTable[n] = true;
        }

    System.out.println("Number of distinct values: " + counter);
0
ulix