Guide de la classe java.util.Arrays

Guide de la classe java.util.Arrays

1. introduction

Dans ce tutoriel, nous allons jeter un œil àjava.util.Arrays, une classe utilitaire qui fait partie de Java depuis Java 1.2.

L'utilisation deArrays, we peut créer, comparer, trier, rechercher, diffuser et transformer des tableaux.

2. Créer

Voyons quelques-unes des façons dont nous pouvons créer des tableaux:copyOf,copyOfRange etfill.

2.1. copyOf etcopyOfRange

Pour utilisercopyOfRange, nous avons besoin de notre tableau d'origine et de l'index de début (inclus) et de l'index de fin (exclusif) que nous voulons copier:

String[] intro = new String[] { "once", "upon", "a", "time" };
String[] abridgement = Arrays.copyOfRange(storyIntro, 0, 3);

assertArrayEquals(new String[] { "once", "upon", "a" }, abridgement);
assertFalse(Arrays.equals(intro, abridgement));

Et pour utilisercopyOf, nous prendrionsintro able une taille de tableau cible et nous récupérerions un nouveau tableau de cette longueur:

String[] revised = Arrays.copyOf(intro, 3);
String[] expanded = Arrays.copyOf(intro, 5);

assertArrayEquals(Arrays.copyOfRange(intro, 0, 3), revised);
assertNull(expanded[4]);

Notez quecopyOf pads the array with nulls if our target size is bigger than the original size.

2.2. fill

Une autre façon, nous pouvons créer un tableau de longueur fixe, estfill, which qui est utile lorsque nous voulons un tableau où tous les éléments sont les mêmes:

String[] stutter = new String[3];
Arrays.fill(stutter, "once");

assertTrue(Stream.of(stutter)
  .allMatch(el -> "once".equals(el));

VérifiezsetAll pour créer un tableau où les éléments sont différents.

Notez que nous devons instancier le tableau nous-mêmes au préalable - par opposition à quelque chose commeString[] filled = Arrays.fill(“once”, 3); - puisque cette fonctionnalité a été introduite avant que les génériques ne soient disponibles dans le langage.

3. Comparant

Passons maintenant aux méthodes de comparaison de tableaux.

3.1. equals abledeepEquals

Nous pouvons utiliserequals pour une simple comparaison de tableaux par taille et contenu. Si nous ajoutons un null à l'un des éléments, la vérification du contenu échoue:

assertTrue(
  Arrays.equals(new String[] { "once", "upon", "a", "time" }, intro));
assertFalse(
  Arrays.equals(new String[] { "once", "upon", "a", null }, intro));

Lorsque nous avons des tableaux imbriqués ou multidimensionnels, nous pouvons utiliserdeepEquals pour non seulement vérifier les éléments de niveau supérieur, mais également effectuer la vérification de manière récursive:

Object[] story = new Object[]
  { intro, new String[] { "chapter one", "chapter two" }, end };
Object[] copy = new Object[]
  { intro, new String[] { "chapter one", "chapter two" }, end };

assertTrue(Arrays.deepEquals(story, copy));
assertFalse(Arrays.equals(story, copy));

Notez commentdeepEquals réussit maisequals  échoue _._

This is because deepEquals ultimately calls itself each time it encounters an array, tandis queequals will compare simplement les références des sous-tableaux.

De plus, cela rend dangereux d'appeler un tableau avec une auto-référence!

3.2. hashCode abledeepHashCode

L'implémentation dehashCode nous donnera l'autre partie du contratequals /hashCode qui est recommandé pour les objets Java. Nous utilisonshashCode pour calculer un entier basé sur le contenu du tableau:

Object[] looping = new Object[]{ intro, intro };
int hashBefore = Arrays.hashCode(looping);
int deepHashBefore = Arrays.deepHashCode(looping);

Maintenant, définissons un élément du tableau d'origine sur null et recalculons les valeurs de hachage:

intro[3] = null;
int hashAfter = Arrays.hashCode(looping);

Alternativement,deepHashCode vérifie les tableaux imbriqués pour les numéros correspondants d'éléments et de contenus. Si nous recalculons avecdeepHashCode:

int deepHashAfter = Arrays.deepHashCode(looping);

Maintenant, nous pouvons voir la différence entre les deux méthodes:

assertEquals(hashAfter, hashBefore);
assertNotEquals(deepHashAfter, deepHashBefore);

deepHashCode is the underlying calculation used when we are working with data structures like HashMap and HashSet on arrays.

4. Tri et recherche

Jetons ensuite un œil au tri et à la recherche de tableaux.

4.1. sort

Si nos éléments sont des primitives ou implémententComparable, nous pouvons utilisersort pour effectuer un tri en ligne:

String[] sorted = Arrays.copyOf(intro, 4);
Arrays.sort(sorted);

assertArrayEquals(
  new String[]{ "a", "once", "time", "upon" },
  sorted);

Take care that sort mutates the original reference, c'est pourquoi nous effectuons une copie ici.

sort will utilise un algorithme différent pour différents types d'éléments de tableau. Primitive types use a dual-pivot quicksort etObject types use Timsort. Les deux ont le cas moyen deO(n log(n))  pour un tableau trié aléatoirement.

Depuis Java 8,parallelSort is est disponible pour un tri-fusion parallèle. Il offre une méthode de tri simultané utilisant plusieurs stasArrays.sort .

4.2. binarySearch

La recherche dans un tableau non trié est linéaire, mais si nous avons un tableau trié, nous pouvons le faire enO(log n), ce que nous pouvons faire avecbinarySearch:

int exact = Arrays.binarySearch(sorted, "time");
int caseInsensitive = Arrays.binarySearch(sorted, "TiMe", String::compareToIgnoreCase);

assertEquals("time", sorted[exact]);
assertEquals(2, exact);
assertEquals(exact, caseInsensitive);

Si nous ne fournissons pas deComparator comme troisième paramètre, alorsbinarySearch compte sur notre type d'élément étant de typeComparable.

Et encore une fois, notez queif our array isn’t first sorted, then binarySearch won’t work as we expect!

5. Diffusion

Comme nous l'avons vu précédemment,Arrays was mis à jour dans Java 8 pour inclure des méthodes utilisant l'API Stream telles queparallelSort (mentionné ci-dessus),stream andsetAll.

5.1. stream

stream nous donne un accès complet à l'API Stream pour notre tableau:

Assert.assertEquals(Arrays.stream(intro).count(), 4);

exception.expect(ArrayIndexOutOfBoundsException.class);
Arrays.stream(intro, 2, 1).count();

Nous pouvons fournir des indices inclusifs et exclusifs pour le flux, mais nous devrions nous attendre à unArrayIndexOutOfBoundsException si les indices sont dans le désordre, négatifs ou hors plage.

6. Transformer

Enfin,toString,asList, etsetAll nous donnent deux manières différentes de transformer des tableaux.

6.1. toString etdeepToString

Un excellent moyen d'obtenir une version lisible de notre tableau d'origine est avectoString:

assertEquals("[once, upon, a, time]", Arrays.toString(storyIntro));

Encore une foiswe must use the deep version to print the contents of nested arrays:

assertEquals(
  "[[once, upon, a, time], [chapter one, chapter two], [the, end]]",
  Arrays.deepToString(story));

6.2. asList

La plus pratique de toutes les méthodesArrays que nous pouvons utiliser est la méthodeasList..Nous avons un moyen simple de transformer un tableau en liste:

List rets = Arrays.asList(storyIntro);

assertTrue(rets.contains("upon"));
assertTrue(rets.contains("time"));
assertEquals(rets.size(), 4);

Cependant,the returned List will be a fixed length so we won’t be able to add or remove elements.

Notez également que, curieusement,java.util.Arrays has its own ArrayList subclass, which asList returns. Cela peut être très trompeur lors du débogage!

6.3. setAll

AvecsetAll, nous pouvons définir tous les éléments d'un tableau avec une interface fonctionnelle. L'implémentation du générateur prend l'indice de position en tant que paramètre:

String[] longAgo = new String[4];
Arrays.setAll(longAgo, i -> this.getWord(i));
assertArrayEquals(longAgo, new String[]{"a","long","time","ago"});

Et, bien sûr, la gestion des exceptions est l’un des aspects les plus délicats de l’utilisation de lambdas. Alors rappelez-vous qu'ici,if the lambda throws an exception, then Java doesn’t define the final state of the array.

7. Préfixe parallèle

Une autre nouvelle méthode dansArrays introduite depuis Java 8 estparallelPrefix. AvecparallelPrefix, nous pouvons opérer sur chaque élément du tableau d'entrée de manière cumulative.

7.1. parallelPrefix

Si l'opérateur effectue une addition comme dans l'exemple suivant,[1, 2, 3, 4] donnera[1, 3, 6, 10]:

int[] arr = new int[] { 1, 2, 3, 4};
Arrays.parallelPrefix(arr, (left, right) -> left + right);
assertThat(arr, is(new int[] { 1, 3, 6, 10}));

De plus, nous pouvons spécifier une sous-gamme pour l'opération:

int[] arri = new int[] { 1, 2, 3, 4, 5 };
Arrays.parallelPrefix(arri, 1, 4, (left, right) -> left + right);
assertThat(arri, is(new int[] { 1, 2, 5, 9, 5 }));

Notez que la méthode est exécutée en parallèle, doncthe cumulative operation should be side-effect-free and associative.

Pour une fonction non associative:

int nonassociativeFunc(int left, int right) {
    return left + right*left;
}

l'utilisation deparallelPrefix donnerait des résultats incohérents:

@Test
public void whenPrefixNonAssociative_thenError() {
    boolean consistent = true;
    Random r = new Random();
    for (int k = 0; k < 100_000; k++) {
        int[] arrA = r.ints(100, 1, 5).toArray();
        int[] arrB = Arrays.copyOf(arrA, arrA.length);

        Arrays.parallelPrefix(arrA, this::nonassociativeFunc);

        for (int i = 1; i < arrB.length; i++) {
            arrB[i] = nonassociativeFunc(arrB[i - 1], arrB[i]);
        }

        consistent = Arrays.equals(arrA, arrB);
        if(!consistent) break;
    }
    assertFalse(consistent);
}

7.2. Performance

Le calcul du préfixe parallèle est généralement plus efficace que les boucles séquentielles, en particulier pour les tableaux de grande taille. Lors de l'exécution de micro-benchmark sur une machine Intel Xeon (6 cœurs) avecJMH, nous pouvons constater une grande amélioration des performances:

Benchmark                      Mode        Cnt       Score   Error        Units
largeArrayLoopSum             thrpt         5        9.428 ± 0.075        ops/s
largeArrayParallelPrefixSum   thrpt         5       15.235 ± 0.075        ops/s

Benchmark                     Mode         Cnt       Score   Error        Units
largeArrayLoopSum             avgt          5      105.825 ± 0.846        ops/s
largeArrayParallelPrefixSum   avgt          5       65.676 ± 0.828        ops/s

Voici le code de référence:

@Benchmark
public void largeArrayLoopSum(BigArray bigArray, Blackhole blackhole) {
  for (int i = 0; i < ARRAY_SIZE - 1; i++) {
    bigArray.data[i + 1] += bigArray.data[i];
  }
  blackhole.consume(bigArray.data);
}

@Benchmark
public void largeArrayParallelPrefixSum(BigArray bigArray, Blackhole blackhole) {
  Arrays.parallelPrefix(bigArray.data, (left, right) -> left + right);
  blackhole.consume(bigArray.data);
}

7. Conclusion

Dans cet article, nous avons appris comment créer, rechercher, trier et transformer des tableaux à l'aide de la classejava.util.Arrays.

Cette classe a été étendue dans les versions Java plus récentes avec l'inclusion de méthodes de production et de consommation de flux dansJava 8 et de méthodes d'incompatibilité dansJava 9.

La source de cet article est, comme toujours,over on Github.