Перейти к основному содержимому

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

· 8 мин. чтения

Задача: Медиана двух отсортированных массивов

Даны два отсортированных массива размерами n и m. Найдите медиану слияния этих двух массивов.
Временная сложность решения должна быть O(log(m + n)) ...

ANDROMEDA

1. Введение

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

Используя массивы, мы можем создавать, сравнивать, сортировать, искать, передавать и преобразовывать массивы.

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 , мы бы взяли ввод и размер целевого массива, и мы бы получили новый массив такой длины:

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

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

Обратите внимание, что copyOf дополняет массив нулями , если наш целевой размер больше исходного размера.

2.2. наполнять

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

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

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

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

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

3. Сравнение

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

3.1. равные и глубокие равные

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

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

Обратите внимание, как deepE quals проходит успешно, а equals терпит неудачу .

Это связано с тем, что deepEquals в конечном итоге вызывает сам себя каждый раз, когда сталкивается с массивом , а equals просто сравнивает ссылки на подмассивы.

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

3.2. hashCode и глубокийHashCode

Реализация 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 — это основной расчет, используемый при работе со структурами данных, такими как HashMap и HashSet , в массивах .

4. Сортировка и поиск

Далее рассмотрим сортировку и поиск массивов.

4.1. Сортировать

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

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

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

Позаботьтесь о том, чтобы sort изменила исходную ссылку , поэтому здесь мы делаем копию.

sort будет использовать другой алгоритм для разных типов элементов массива. Типы-примитивы используют быструю сортировку с двумя опорными точками, а типы объектов используют Timsort . Оба имеют средний случай O (n log (n)) для случайно отсортированного массива.

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

4.2. бинарныйПоиск

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

И снова обратите внимание, что если наш массив не отсортирован, то binarySearch не будет работать так, как мы ожидаем!

5. Стриминг

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

5.1. ручей

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

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

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

6.2. список

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

List<String> rets = Arrays.asList(storyIntro);

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

Однако возвращаемый список будет фиксированной длины, поэтому мы не сможем добавлять или удалять элементы .

Обратите также внимание на то, что, как ни странно, java.util.Arrays имеет свой собственный подкласс ArrayList , который asList возвращает . Это может быть очень обманчивым при отладке!

6.3. установить все

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

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

И, конечно же, обработка исключений — одна из самых рискованных частей использования лямбда-выражений. Поэтому помните, что здесь, если лямбда выдает исключение, Java не определяет конечное состояние массива.

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

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

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

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

Обратите внимание, что метод выполняется параллельно, поэтому кумулятивная операция должна быть без побочных эффектов и ассоциативной .

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

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 .

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