Руководство по классу java.util.Arrays

Руководство по классу java.util.Arrays

1. Вступление

В этом руководстве мы рассмотримjava.util.Arrays, служебный класс, который был частью Java со времен Java 1.2.

С помощьюArrays, we можно создавать, сравнивать, сортировать, искать, передавать и преобразовывать массивы.

2. Создание

Давайте рассмотрим некоторые способы создания массивов:copyOf,copyOfRange иfill..

2.1. copyOf иcopyOfRange

Чтобы использоватьcopyOfRange, нам нужен наш исходный массив и начальный индекс (включительно) и конечный индекс (исключая), которые мы хотим скопировать:

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));

А чтобы использоватьcopyOf, мы возьмемintro и размер целевого массива и вернем новый массив такой длины:

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

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

Обратите внимание, чтоcopyOf pads the array with nulls if our target size is bigger than the original size.

2.2. fillс

Другой способ, мы можем создать массив фиксированной длины,fill, w, который полезен, когда нам нужен массив, в котором все элементы одинаковы:

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

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

ПроверьтеsetAll to создать массив, в котором элементы разные.

Обратите внимание, что нам нужно создать экземпляр массива заранее - в отличие от чего-то вродеString[] filled = Arrays.fill(“once”, 3); - поскольку эта функция была введена до того, как в языке появились универсальные шаблоны.

3. Сравнение

Теперь перейдем к методам сравнения массивов.

3.1. equals песокdeepEquals

Мы можем использоватьequals для простого сравнения массивов по размеру и содержанию. Если мы добавим ноль в качестве одного из элементов, проверка содержимого завершится неудачно:

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

Когда у нас есть вложенные или многомерные массивы, мы можем использоватьdeepEquals не только для проверки элементов верхнего уровня, но и для рекурсивного выполнения проверки:

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));

Обратите внимание, какdeepEquals проходит, ноequals не проходит _._

This is because deepEquals ultimately calls itself each time it encounters an array, аequals просто сравнивает ссылки на подмассивы.

Кроме того, это делает опасным вызов массива со ссылкой на себя!

3.2. hashCode песокdeepHashCode

РеализацияhashCode даст нам другую часть контрактаequals /hashCode, которая рекомендуется для объектов Java. Мы используемhashCode для вычисления целого числа на основе содержимого массива:

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

Теперь мы устанавливаем элемент исходного массива в null и заново вычисляем значения хеша:

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

В качестве альтернативыdeepHashCode проверяет вложенные массивы на соответствие количества элементов и содержимого. Если пересчитать сdeepHashCode:

int deepHashAfter = Arrays.deepHashCode(looping);

Теперь мы можем увидеть разницу в двух методах:

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. Сортировка и поиск

Теперь давайте рассмотрим сортировку и поиск в массивах.

4.1. sort

Если наши элементы являются примитивами или реализуютComparable, мы можем использоватьsort to для выполнения встроенной сортировки:

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, поэтому мы выполняем здесь копирование.

sort will использует другой алгоритм для разных типов элементов массива. Primitive types use a dual-pivot quicksort иObject types use Timsort. Оба имеют средний случайO(n log(n))  для произвольно отсортированного массива.

Начиная с Java 8,parallelSort  доступен для параллельной сортировки-слияния. Он предлагает метод одновременной сортировки с использованием нескольких стилейArrays.sort .

4.2. binarySearchс

Поиск в несортированном массиве является линейным, но если у нас есть отсортированный массив, мы можем сделать это вO(log n), что мы можем сделать сbinarySearch:

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

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

Если мы не предоставляемComparator в качестве третьего параметра, тоbinarySearch считает, что наш тип элемента имеет типComparable.

И снова заметим, чтоif our array isn’t first sorted, then binarySearch won’t work as we expect!

5. Потоковый

Как мы видели ранее,Arrays was обновлен в Java 8, чтобы включить методы, использующие Stream API, такие какparallelSort (упомянутый выше),stream andsetAll.

5.1. streamс

stream дает нам полный доступ к Stream API для нашего массива:

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

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

Мы можем предоставить инклюзивные и исключительные индексы для потока, однако мы должны ожидатьArrayIndexOutOfBoundsException, если индексы неупорядочены, отрицательны или выходят за пределы допустимого диапазона.

6. Преобразуя

Наконец,toString,asList, иsetAll  дают нам несколько различных способов преобразования массивов.

6.1. toString иdeepToString

Отличный способ получить читаемую версию нашего исходного массива - использоватьtoString:

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

Опятьwe 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с

Самый удобный из всех методовArrays для нас - этоasList.. У нас есть простой способ превратить массив в список:

List rets = Arrays.asList(storyIntro);

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

Однакоthe returned List will be a fixed length so we won’t be able to add or remove elements.

Отметим также, что любопытно, чтоjava.util.Arrays has its own ArrayList subclass, which asList returns. Это может быть очень обманчиво при отладке!

6.3. setAllс

С помощьюsetAll мы можем установить все элементы массива с функциональным интерфейсом. Реализация генератора принимает позиционный индекс в качестве параметра:

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

И, конечно же, обработка исключений является одной из наиболее рискованных частей использования лямбд. Так что помните, что здесьif the lambda throws an exception, then Java doesn’t define the final state of the array.

7. Параллельный префикс

Еще один новый метод вArrays, представленный в Java 8, - этоparallelPrefix. С помощьюparallelPrefix мы можем работать с каждым элементом входного массива накопительным образом.

7.1. parallelPrefixс

Если оператор выполняет сложение, как в следующем примере,[1, 2, 3, 4] приведет к[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}));

Также мы можем указать поддиапазон для операции:

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 }));

Обратите внимание, что метод выполняется параллельно, поэтомуthe cumulative operation should be side-effect-free and associative.

Для неассоциативной функции:

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

использованиеparallelPrefix приведет к противоречивым результатам:

@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. Спектакль

Вычисление параллельных префиксов обычно более эффективно, чем последовательные циклы, особенно для больших массивов. При запуске микротеста на машине Intel Xeon (6 ядер) сJMH мы можем увидеть значительное улучшение производительности:

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

Вот эталонный код:

@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. Заключение

В этой статье мы узнали, как использовать некоторые методы создания, поиска, сортировки и преобразования массивов с помощью классаjava.util.Arrays.

Этот класс был расширен в более поздних выпусках Java с включением методов создания и потребления потоков вJava 8 и методов несоответствия вJava 9.

Источником этой статьи, как всегда, являетсяover on Github.