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 .