Complexité temporelle des collections Java

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 HashSetLinkedHashSet, 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.