JavaRush/Java блог/Random/Кофе-брейк #226. Подробное руководство по классу java.uti...

Кофе-брейк #226. Подробное руководство по классу java.util.Arrays

Статья из группы Random
участников
Источник: Baeldung В этом руководстве подробно рассмотрены возможности служебного класса java.util.Arrays, который является частью языка Java, начиная с версии Java 1.2. Кофе-брейк #226. Подробное руководство по классу java.util.Arrays - 1При необходимости сравнить, скопировать или отсортировать массивы, многие разработчики прибегают к таким операциям, как написание циклов, операторов if и другой логики, необходимой для такой обработки. В большинстве случаев это ошибочное решение, поскольку такие операции дублируют поведение, доступное в служебном классе java.util.Arrays. Давайте посмотрим, как использовать класс Arrays для обработки повседневных операций с массивами.

1. Создание массивов

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

1.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 дополняет массив нулями, если наш целевой размер больше исходного размера.

1.2. fill

Еще один способ, которым мы можем создать массив фиксированной длины, это fill (заполнение). Его полезно применять, когда нам нужен массив, в котором все элементы одинаковы:
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); – поскольку эта функция была введена до того, как в языке стали доступны дженерики.

2. Сравнение

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

2.1 equals and deepEquals

Для простого сравнения массивов по размеру и содержимому можно использовать 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));
Обратите внимание, как deepEquals выполняется успешно, а equals терпит неудачу. Это связано с тем, что deepEquals в конечном итоге вызывает сам себя каждый раз, когда сталкивается с массивом, а equals просто сравнивает ссылки на подмассивы. Учтите, что довольно опасно обращаться к массиву с самоссылкой!

2.2. hashCode and deepHashCode

Реализация hashCode даст нам другую часть контракта equals/hashCode, которая рекомендуется для объектов Java. Мы используем hashCode для вычисления целого числа (integer) на основе содержимого массива:
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.

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

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

3.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.

3.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. И снова обратите внимание, что если наш массив не отсортирован, то binarySearch не будет работать так, как мы ожидаем!

4. Потоковые методы

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

4.1. stream

stream дает нам для нашего массива полный доступ к Stream API:
Assert.assertEquals(Arrays.stream(intro).count(), 4);

exception.expect(ArrayIndexOutOfBoundsException.class);
Arrays.stream(intro, 2, 1).count();
Мы можем предоставить инклюзивные и эксклюзивные индексы для потока, однако мы должны ожидать появления ошибки ArrayIndexOutOfBoundsException, если индексы out of order (не в порядке), negative (отрицательные) или out of range (вне диапазона).

5. Преобразование массивов

Используя toString, asList и setAll, мы имеем несколько способов преобразования массивов.

5.1. toString и DeepToString

Отличный способ получить удобочитаемую версию исходного массива — использовать toString:
assertEquals("[once, upon, a, time]", Arrays.toString(storyIntro));
Для печати содержимого вложенных массивов мы снова должны использовать “глубокую” версию — DeepToString:
assertEquals(
  "[[once, upon, a, time], [chapter one, chapter two], [the, end]]",
  Arrays.deepToString(story));

5.2. asList

Наиболее удобным из всех методов массивов для нас является asList. Он дает нам простой способ превратить массив в список (List):
List<String> rets = Arrays.asList(storyIntro);

assertTrue(rets.contains("upon"));
assertTrue(rets.contains("time"));
assertEquals(rets.size(), 4);
Однако возвращаемый список будет фиксированной длины, поэтому мы не сможем добавлять или удалять элементы. Обратите также внимание на то, что, как ни странно, java.util.Arrays имеет свой собственный подкласс ArrayList, который возвращает asList. Это может выглядеть очень обманчиво при отладке!

5.3. setAll

С помощью setAll мы можем установить все элементы массива с функциональным интерфейсом. Реализация генератора в качестве параметра принимает позиционный индекс:
String[] longAgo = new String[4];
Arrays.setAll(longAgo, i -> this.getWord(i));
assertArrayEquals(longAgo, new String[]{"a","long","time","ago"});
Конечно же, обработка исключений — одна из самых рискованных частей использования лямбда-выражений. Поэтому помните, что здесь, если лямбда выдает исключение, Java не определяет конечное состояние массива.

6. Parallel Prefix

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

6.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 }));
Заметьте, что метод выполняется параллельно, поэтому кумулятивная операция должна быть без побочных эффектов и ассоциативной. Для неассоциативной функции:
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);
}

Заключение

В этой статье мы узнали, как использовать методы создания, поиска, сортировки и преобразования массивов с помощью класса java.util.Arrays. Этот класс был расширен в более поздних версиях Java за счет включения методов создания и потребления потока в Java 8 и методов несоответствия в Java 9.
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
У этой страницы еще нет ни одного комментария