Complexité temporelle des collections Java
1. Vue d'ensemble
Dans ce didacticiel,we’ll talk about the performance of different collections from the Java Collection API. Lorsque nous parlons de collections, nous pensons généralement aux structures de donnéesList, Map, etSet et à leurs implémentations communes.
Tout d'abord, nous examinerons les informations sur la complexité Big-O pour les opérations courantes, puis nous montrerons les nombres réels de certaines opérations de collecte.
2. La complexité du temps
Habituellement,when we talk about time complexity, we refer to Big-O notation. En termes simples, la notation décrit comment le temps d'exécution de l'algorithme augmente avec la taille de l'entrée.
Des articles utiles sont disponibles pour en savoir plus sur la notation Big-Otheory ou lesJava examples pratiques.
3. List
Commençons par une liste simple - qui est une collection ordonnée.
Ici, nous allons jeter un œil à un aperçu des performances des implémentations deArrayList, LinkedList, etCopyOnWriteArrayList.
3.1. ArrayList
The ArrayList in Java is backed by an array. Cela aide à comprendre la logique interne de sa mise en œuvre. Un guide plus complet pour lesArrayList est disponiblein this article.
Alors, concentrons-nous d'abord sur la complexité temporelle des opérations courantes, à un niveau élevé:
-
add() - prendO(1) temps
-
add(index, element) - en cycles moyens en temps deO(n)
-
get() - est toujours une opération à temps constantO(1)
-
remove() - s'exécute en temps linéaireO(n). Nous devons itérer le tableau entier pour trouver l'élément pouvant être supprimé
-
*indexOf()* – s'exécute également en temps linéaire. Il parcourt le tableau interne et vérifie chaque élément un à un. Ainsi, la complexité temporelle de cette opération nécessite toujoursO(n) temps
-
contains() - l'implémentation est basée surindexOf(). Donc, il fonctionnera également dans le tempsO(n)
3.2. CopyOnWriteArrayList
Cette implémentation de l'interfaceList estvery useful when working with multi-threaded applications. C'est thread-safe et bien expliqué dansthis guide here.
Voici l'aperçu de la notation Big-O des performances pourCopyOnWriteArrayList:
-
add() - dépend de la position que nous ajoutons de la valeur, donc la complexité estO(n)
-
get() - estO(1) cOpération à temps constant
-
remove() - prendO(n) temps
-
contains() - de même, la complexité estO(n)
Comme nous pouvons le voir, l'utilisation de cette collection est très coûteuse en raison des caractéristiques de performance de la méthodeadd().
3.3. LinkedList
LinkedList is a linear data structure which consists of nodes holding a data field and a reference to another node. Pour plus de fonctionnalités et de capacités deLinkedList, jetez un œil àthis article here.
Présentons l'estimation moyenne du temps dont nous avons besoin pour effectuer certaines opérations de base:
-
add() - prend en charge l'insertion à temps constant deO(1) à n'importe quelle position
-
get() - la recherche d'un élément prendO(n) temps
-
remove() - la suppression d'un élément nécessite également l'opérationO(1), car nous fournissons la position de l'élément
-
contains() - a également une complexité de tempsO(n)
3.4. Préchauffage de la JVM
Maintenant, pour prouver la théorie, jouons avec les données réelles. To be more precise, we’ll present the JMH (Java Microbenchmark Harness) test results of the most common collection operations.
Si vous n'êtes pas familier avec l'outil JMH, consultez ceuseful guide.
Dans un premier temps, nous présentons les principaux paramètres de nos tests de référence:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 10)
public class ArrayListBenchmark {
}
Ensuite, nous définissons le nombre d'itérations de préchauffage sur10. Nous souhaitons également voir la durée moyenne de nos résultats affichée en microsecondes.
3.5. Tests de référence
Il est maintenant temps d’exécuter nos tests de performances. Tout d'abord, nous commençons par lesArrayList:
@State(Scope.Thread)
public static class MyState {
List employeeList = new ArrayList<>();
long iterations = 100000;
Employee employee = new Employee(100L, "Harry");
int employeeIndex = -1;
@Setup(Level.Trial)
public void setUp() {
for (long i = 0; i < iterations; i++) {
employeeList.add(new Employee(i, "John"));
}
employeeList.add(employee);
employeeIndex = employeeList.indexOf(employee);
}
}
À l'intérieur de nosArrayListBenchmark, nous ajoutons la classeState pour contenir les données initiales.
Ici, nous créons unArrayList d'objetsEmployee. Après,we initialize it with 100.000 items inside of the setUp() method. The @State indicates that the @Benchmark tests have full access to the variables declared in it within the same thread.
Enfin, il est temps d’ajouter les tests de référence pour les méthodesadd(), contains(), indexOf(), remove(), etget():
@Benchmark
public void testAdd(ArrayListBenchmark.MyState state) {
state.employeeList.add(new Employee(state.iterations + 1, "John"));
}
@Benchmark
public void testAddAt(ArrayListBenchmark.MyState state) {
state.employeeList.add((int) (state.iterations), new Employee(state.iterations, "John"));
}
@Benchmark
public boolean testContains(ArrayListBenchmark.MyState state) {
return state.employeeList.contains(state.employee);
}
@Benchmark
public int testIndexOf(ArrayListBenchmark.MyState state) {
return state.employeeList.indexOf(state.employee);
}
@Benchmark
public Employee testGet(ArrayListBenchmark.MyState state) {
return state.employeeList.get(state.employeeIndex);
}
@Benchmark
public boolean testRemove(ArrayListBenchmark.MyState state) {
return state.employeeList.remove(state.employee);
}
3.6. Résultats de test
Tous les résultats sont présentés en microsecondes:
Benchmark Mode Cnt Score Error
ArrayListBenchmark.testAdd avgt 20 2.296 ± 0.007
ArrayListBenchmark.testAddAt avgt 20 101.092 ± 14.145
ArrayListBenchmark.testContains avgt 20 709.404 ± 64.331
ArrayListBenchmark.testGet avgt 20 0.007 ± 0.001
ArrayListBenchmark.testIndexOf avgt 20 717.158 ± 58.782
ArrayListBenchmark.testRemove avgt 20 624.856 ± 51.101
From the results we can learn, that testContains() and testIndexOf() methods run in approximately the same time. Nous pouvons également clairement voir l'énorme différence entre les scores de la méthodetestAdd(), testGet() du reste des résultats. L'ajout d'un élément prend 2.296microsecondes et en obtenir un est une opération de 0,007 microseconde.
La recherche ou la suppression d'un élément coûte environ700 microsecondes. Ces nombres sont la preuve de la partie théorique, où nous avons appris queadd(), etget() ont une complexité temporelle deO(1) et les autres méthodes sontO(n). élémentsn=10.000 dans notre exemple.
De même, nous pouvons écrire les mêmes tests pour la collectionCopyOnWriteArrayList. Tout ce dont nous avons besoin est de remplacer lesArrayList dans employeeList par l'instanceCopyOnWriteArrayList.
Voici les résultats du test de référence:
Benchmark Mode Cnt Score Error
CopyOnWriteBenchmark.testAdd avgt 20 652.189 ± 36.641
CopyOnWriteBenchmark.testAddAt avgt 20 897.258 ± 35.363
CopyOnWriteBenchmark.testContains avgt 20 537.098 ± 54.235
CopyOnWriteBenchmark.testGet avgt 20 0.006 ± 0.001
CopyOnWriteBenchmark.testIndexOf avgt 20 547.207 ± 48.904
CopyOnWriteBenchmark.testRemove avgt 20 648.162 ± 138.379
Ici encore, les chiffres confirment la théorie. Comme nous pouvons le voir,testGet() s'exécute en moyenne en 0,006 ms que nous pouvons considérer commeO(1). Comparing to ArrayList, we also notice the significant difference between testAdd() method results. As we have here O(n) complexity for the add() method versus ArrayList’s O(1).
We can clearly see the linear growth of the time, as performance numbers are 878.166 compared to 0.051.
Maintenant, il est temps deLinkedList:
Benchmark Cnt Score Error
testAdd 20 2.580 ± 0.003
testContains 20 1808.102 ± 68.155
testGet 20 1561.831 ± 70.876
testRemove 20 0.006 ± 0.001
Nous pouvons voir d'après les scores, que l'ajout et la suppression d'éléments dansLinkedList sont assez rapides.
En outre, il existe un écart de performances important entre les opérations d'ajout / suppression et d'obtention / contenance.
4. Map
Avec les dernières versions de JDK, nous assistons à une amélioration significative des performances pour les implémentations deMap, comme le remplacement desLinkedList par la structure de nœuds d'arborescence équilibrée dans les implémentations internesHashMap, LinkedHashMap . This shortens the element lookup worst-case scenario from O(n) to O(log(n)) time during the HashMap collisions.
Cependant, si nous implémentons les méthodes.equals() et.hashcode() appropriées, les collisions sont peu probables.
Pour en savoir plus sur les collisions deHashMap, consultezthis write-up. From the write-up, we can also learn, that storing and retrieving elements from the HashMap takes constant O(1) time.
4.1. Test des opérationsO(1)
Montrons quelques chiffres réels. Tout d'abord, pour lesHashMap:
Benchmark Mode Cnt Score Error
HashMapBenchmark.testContainsKey avgt 20 0.009 ± 0.002
HashMapBenchmark.testGet avgt 20 0.011 ± 0.001
HashMapBenchmark.testPut avgt 20 0.019 ± 0.002
HashMapBenchmark.testRemove avgt 20 0.010 ± 0.001
As we see, the numbers prove the O(1) constant time for running the methods listed above. Maintenant, faisons une comparaison des résultats du testHashMap avec les autres scores de l'instanceMap.
Pour toutes les méthodes répertoriées,we have O(1) for HashMap, LinkedHashMap, IdentityHashMap, WeakHashMap, EnumMap and ConcurrentHashMap.
Présentons les résultats des résultats des tests restants sous la forme d'un tableau:
Benchmark LinkedHashMap IdentityHashMap WeakHashMap ConcurrentHashMap
testContainsKey 0.008 0.009 0.014 0.011
testGet 0.011 0.109 0.019 0.012
testPut 0.020 0.013 0.020 0.031
testRemove 0.011 0.115 0.021 0.019
À partir des nombres de sortie, nous pouvons confirmer les affirmations de complexité temporelle deO(1).
4.2. Test des opérationsO(log(n))
Pour l'arborescenceTreeMap and ConcurrentSkipListMap the put(), get(), remove(), containsKey() operations time is O(log(n)).
[.pl-smi] # Ici,we want to make sure that our performance tests will run approximately in logarithmic time. Pour cette raison, nous initialisons les cartes avec n=1000, 10,000, 100,000, 1,000,000éléments en continu. #
Dans ce cas, nous nous intéressons au temps total d'exécution:
items count (n) 1000 10,000 100,000 1,000,000
all tests total score 00:03:17 00:03:17 00:03:30 00:05:27
[.pl-smi] #Quandn=1000 nous avons le total du temps d'exécution de00:03:17 millisecondes. n=10,000 le temps est presque inchangé00:03:18 ms. n=100,000 a une augmentation mineure00:03:30. Et enfin, lorsquen=1,000,000, l'exécution se termine en00:05:27 ms. #
Après avoir comparé les nombres d'exécution avec la fonctionlog(n) de chaquen, nous pouvons confirmer que la corrélation des deux fonctions correspond.
5. Set
Généralement,Set est une collection d'éléments uniques. Ici, nous allons examiner les implémentationsHashSet,LinkedHashSet,EnumSet, TreeSet, CopyOnWriteArraySet, etConcurrentSkipListSet de l'interfaceSet.
Pour mieux comprendre les éléments internes desHashSet,this guide est là pour vous aider.
Maintenant, allons de l'avant pour présenter les nombres de complexité temporelle. For HashSet, LinkedHashSet, and EnumSet the add(), remove() and contains() operations cost constant O(1) time. Thanks to the internal HashMap implementation.
De même, le TreeSet has O(log(n)) time complexity pour les opérations répertoriées pour le groupe précédent. C'est à cause de l'implémentation deTreeMap. La complexité temporelle pourConcurrentSkipListSet est égalementO(log(n)) temps, car elle est basée sur la structure de données de la liste à sauter.
PourCopyOnWriteArraySet,, les méthodesadd(), remove() ablecontains() ont une complexité temporelle moyenne O (n).
5.1. Méthodes d'essai
Passons maintenant à nos tests de référence:
@Benchmark
public boolean testAdd(SetBenchMark.MyState state) {
return state.employeeSet.add(state.employee);
}
@Benchmark
public Boolean testContains(SetBenchMark.MyState state) {
return state.employeeSet.contains(state.employee);
}
@Benchmark
public boolean testRemove(SetBenchMark.MyState state) {
return state.employeeSet.remove(state.employee);
}
De plus, nous laissons les configurations de référence restantes en l'état.
5.2. Comparaison des chiffres
Voyons le comportement du score d'exécution à l'exécution pour les élémentsHashSet etLinkedHashSet havingn = 1000; 10,000; 100,000.
Pour lesHashSet, , les nombres sont:
Benchmark 1000 10,000 100,000
.add() 0.026 0.023 0.024
.remove() 0.009 0.009 0.009
.contains() 0.009 0.009 0.010
De même, les résultats pourLinkedHashSet sont:
Benchmark 1000 10,000 100,000
.add() 0.022 0.026 0.027
.remove() 0.008 0.012 0.009
.contains() 0.008 0.013 0.009
Comme on le voit, les scores restent presque les mêmes pour chaque opération. De plus, lorsque nous les comparons avec les sorties de test deHashMap, elles se ressemblent également.
En conséquence, nous confirmons que toutes les méthodes testées fonctionnent en temps constantO(1).
6. Conclusion
Dans cet article,we present the time complexity of the most common implementations of the Java data structures.
Séparément, nous montrons les performances d'exécution réelles de chaque type de collection via les tests d'évaluation de la machine virtuelle Java. Nous avons également comparé les performances des mêmes opérations dans différentes collections. En conséquence, nous apprenons à choisir la bonne collection qui correspond à nos besoins.
Comme d'habitude, le code complet de cet article est disponibleover on GitHub.