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?
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
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
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 Foo
s 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.
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.
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)
.
Vous devriez essayer new LinkedList(new HashSet(listInts))
.
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);}
}
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 ());
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);