Структуры данных в Java. Полезные методы вспомогательных классов +8



Привет, habr!

Я Software Engineer в EPAM. Более 8 лет я работаю с legacy-кодом, написанном на языке Java (предвосхищая комментарии, отмечу, что понимание и терпимость к legacy началась задолго до EPAM, в заключении вы найдёте ответ, почему). Часто в работе я сталкивался с одними и теми же повторяющимися недочетами. Это побудило меня написать заметку, и начать я хочу со структур данных и вспомогательных классов Collections и Arrays. Почему-то некоторые разработчики пренебрегают их использованием, и напрасно

Разработчику на Java часто приходится сталкиваться с различными структурами данных. Это могут быть массивы, всевозможные коллекции или реализации Map. Казалось бы, всё с ними ясно и понятно, но существует несколько мелочей, о которые легко споткнуться.

Эта заметка может оказаться полезной как новичкам, которые ещё не знают этих нюансов, так и опытным разработчикам, которые могли что-то из этого забыть.

image
Photo by ammiel jr on Unsplash

КАТ


Сразу хочу оговориться, что этот материал актуален для Java 8. Понятно, что какие-то вещи уже сделаны лучше в Java 9+, но в большинстве крупных проектов чаще всего используется версия Java 8 (а иногда и Java 6).

Как лучше получить коллекцию на базе массива?


Предлагаю начать с формирования коллекции на базе массива.

Чаще всего встречается такой способ:

Integer[] someArray = {9, 10, 11, 12}; 
List<Integer> list = Arrays.asList(someArray); 

Он безусловно работает, но так ли всё с ним хорошо? И есть ли альтернативны решения?

На ум приходят сразу два минуса этого подхода:

  • Во-первых, метод Arrays.asList возвращает List. Но что, если нам нужна другая реализация Collection? Arrays.asList не позволит этого сделать, но чуть дальше будет рассмотрена альтернатива.
  • Во-вторых, List, полученный в результате вызова Arrays.asList не поддерживает изменения размера. Думаю, многие сталкивались с исключением, возникающим в результате работы с таким списком.

У интерфейса Collections можно найти альтернативу методу Arrays.asList — метод Collections.addAll. Вот как можно его использовать:

// Тут может быть любая коллекция (List, Set, ...) 
Collection<Integer> collection = ...; 
Integer[] someArray = {9, 10, 8, 7}; 
Collections.addAll(collection, someArray); 

Или же просто:

Collections.addAll(collection, 11, 12, 13, 14); 

Метод Collections.addAll принимает на входе объект Collection и массив. Вместо массива также можно указать элементы через запятую.

Какие преимущества Collections.addAll перед Arrays.asList?

  • Начнём с того, что при создании коллекций на основе массива Collections.addAll работает намного быстрее, чем метод addAll коллекции с подачей на вход Arrays.asList. Об этом можно найти упоминание в JavaDoc этого метода:
    The behavior of this convenience method is identical to that of c.addAll(Arrays.asList(elements)), but this method is likely to run significantly faster under most
  • Кроме того, Collections.addAll работает не только с List, но и с любой другой коллекцией.
  • А ещё при использовании этого метода не возникает проблемы изменения размера.

Как проще всего напечатать массив, многомерный массив или коллекцию?


Давайте теперь перейдём к такому вопросу, как получение печатного представления массива и коллекций.

Если просто сделать System.out.println(someArray), то получим что-то вроде этого:
[Ljava.lang.Integer;@6d06d69c.
Аналогичный результат ждёт при использовании метода toString() у массива.
Для вывода массива на помощь приходит метод Arrays.toString(...).

Integer[] someArray = new Integer[]{1, 2, 3}; 
System.out.println(Arrays.toString(someArray)); 

Вывод у этой строки будет такой:

[1, 2, 3] 

Если речь идёт о многомерном массиве, то можно воспользоваться методом: Arrays.deeptoString.

int[][] a = { 
    {1, 2, 3}, 
       {4, 5, 6} 
}; 
System.out.println(Arrays.deepToString(a)); 

Выводом этого фрагмента будет:

[[1, 2, 3], [4, 5, 6]] 


Таким образом, не требуется перебирать массив через какой-нибудь цикл вручную, чтобы вывести его элементы, достаточно использовать этот метод.

Что касается коллекций или реализаций Map, то тут нет никаких проблем. Все структуры данных, кроме массива, нормально выводятся.

Допустим, есть такой пример:

Collection<Integer> someCollection = new HashSet<>(); 
someCollection.add(1); 
someCollection.add(2); 
System.out.println(someCollection); 

Map<Integer, String> someMap = new HashMap<>(); 
someMap.put(1, "Some 1"); 
someMap.put(2, "Some 2"); 
System.out.println(someMap); 

Обратите внимание в выводе ниже, что и множество, и Map были выведены в удобном для чтения виде:

[1, 2] 
{1=Some 1, 2=Some 2} 


Как легко можно сравнить массивы между собой?


Бывают ситуации, когда необходимо сравнить массивы. В классе Arrays есть метод, позволяющий провести такое сравнение. Метод Arrays.equals сравнивает количество элементов и проверяет эквивалентность соответствующих элементов.

Допустим, у нас есть класс Elements с одним полем и определённым equals

private class Element { 
    final String name; 
     private Element(String name) { this.name = name; } 

     @Override 
    public boolean equals(Object o) { 
        if (this == o) return true; 
        if (o == null || getClass() != o.getClass()) return false; 
        Element element = (Element) o; 
        return Objects.equals(name, element.name); 
    } 

    @Override 
    public int hashCode() { 
        return Objects.hash(name); 
    } 
}

Определим три массива:

Element[] firstElementArray = { new Element("a"), new Element("b"), new Element("c") }; 
Element[] secondElementArray = {new Element("c"), new Element("b"), new Element("a") }; 
Element[] thirdElementArray = { new Element("a"), new Element("b"), new Element("c") }; 

Обратите внимание, что у первого и третьего массива элементы в одинаковом порядке.
Теперь можно выполнить сравнение используя метод Arrays.equals.

System.out.println("first equals to second? " 
        + Arrays.equals(firstElementArray, secondElementArray)); 
System.out.println("second equals to third? " 
        + Arrays.equals(secondElementArray, thirdElementArray)); 
System.out.println("first equals to third? " 
        + Arrays.equals(firstElementArray, thirdElementArray)); 

Результат будет следующим:

first equals to second? false 
second equals to third? false 
first equals to third? true 


Как эффективно скопировать массив?


Часто можно встретить в коде ручное копирование массивов с использованием циклов. Однако существует метод System.arraycopy, который выполнит копирование гораздо быстрее.

Предлагаю взглянуть на такой простой пример:

Element[] elements = { 
        new Element("a"), 
        new Element("b"), 
        new Element("c") 
}; 
Element[] copyOfElements = new Element[elements.length]; 
System.arraycopy(elements, 0, copyOfElements, 0, elements.length); 
System.out.println(Arrays.toString(copyOfElements)); 

У нас есть массив элементов. Мы создаём пустой массив той же длинны и копируем все элементы из первого во второй. В результате получим такой вывод:

[Element{name='a'}, Element{name='b'}, Element{name='c'}] 


Как по-разному отсортировать массив или коллекцию?


Массивы могут быть отсортированы с помощью метода Arrays.sort(someArray). Если требуется отсортировать массив в обратном порядке, то можно передать на вход этому методу Collections.reverseOrder() как второй параметр.

К примеру, есть массив, который мы отсортируем в прямом, а потом в обратном порядке:

String[] someArray = new String[]{"b", "a", "c"}; 

Arrays.sort(someArray); 
System.out.println(Arrays.toString(someArray)); 

Arrays.sort(someArray, Collections.reverseOrder()); 
System.out.println(Arrays.toString(someArray)); 

Вывод будет следующий:

[a, b, c] 
[c, b, a] 


Кроме прямой и обратной сортировки, бывает, возникает необходимость отсортировать массив строк независимо от регистра. Это легко сделать, передав String.CASE_INSENSITIVE_ORDER как второй параметр в Arrays.sort.

Collections.sort, к сожалению, позволяет отсортировать только реализации List.

По какому алгоритму сортирует Java?


Последнее, о чём можно упомянуть, говоря о сортировке в Java, это то, что в Java для простейших типов используется “quick sort”, а для объектов — “stable merge”. Так что не стоит тратить ресурсы на разработку собственной реализации метода сортировки, пока профилировщик не покажет, что это необходимо.

Что делать, если у нас есть массив, а метод принимает Iterable?


Предлагаю теперь перейти к такому вопросу, как передача массива в метод, требующий Iterable. Напомню, что Iterable — это интерфейс, который содержит метод iterator(), который должен возвращать Iterator.

Если есть метод, который принимает на входе Iterable, то массив туда просто так передать не получится. Несмотря на то, что массив можно перебирать в цикле for, он не является Iterable.

String[] someArray = new String[]{"a", "b", "c"}; 
for (String currentString : someArray) { 
    ... 
}  

В этом примере всё хорошо. Но если есть метод:

private static void someIteration(Iterable<String> iterable) { 
    ... 
} 

То такая строка не скомпилируется:

someIteration(someArray); 

Единственный выход в этой ситуации — преобразовать массив в коллекцию и уже её подать на вход такому методу.

Коротко еще о нескольких полезных методах Collections


Метод Комментарий
max(Collection) и max(Collection, Comparator)
min(Collection) и min(Collection, Comparator)
Обратите внимание, что можно подавать на вход Comparator
indexOfSubList(List, List)
Находит индекс первого вхождения одного списка (второй аргумент) в другом (первый аргумент)
lastIndexOfSubList(List, List)
Находит индекс последнего вхождения одного списка (второй аргумент) в другом (первый аргумент)
reverse(List)
Переставляет элементы в обратном порядке

Что стоит почитать?


Это лишь небольшая часть средств, которые могут облегчить жизнь разработчику при работе со структурами данных. Многие интересные моменты самой работы коллекций и удобные средства для работы с ними можно найти в книге Брюса Эккеля «Философия Java» (4-е полное издание). Однако, стоит быть внимательным, так как в ней встречаются ситуации, которые уже не воспроизводятся на Java 7, Java 8 и выше. Хоть в этой книге и описана Java 6, её материал остается в большинстве своём актуален и сейчас.

Конечно, «Философией Java» ограничиваться не стоит. Любому разработчику Java не повредит прочтение таких книг:

  • «Java. Эффективное программирование», Джошуа Блох.
  • «Рефакторинг. Улучшение проекта существующего кода», Мартин Фаулер.
  • «Чистый код. Создание, анализ и рефакторинг», Роберт Мартин.
  • «Spring 5 для профессионалов», Юлиана Козмина и другие.
  • «Test-Driven Java Development», Viktor Farcic, Alex Garcia (на русском языке пока не вышла).

Что в итоге?


Если вам на ум пришли интересные идеи, которые могли бы дополнить написанное в этой заметке, поделитесь ими в комментариях.

Отдельно хочу пожелать удачи и терпения тем, кто трудится с унаследованным старым кодом. Большинство крупных проектов — это legacy. И их значимость для заказчика трудно переоценить. Да и чувство победы от устранения бага, на поиск причин которого ушла не одна неделя, ничуть не уступает ощущениям при окончании реализации новой фичи.

Благодарю за внимание. Буду рад, если что-нибудь из представленного окажется полезным.
Всем успехов!

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (17):

  1. Bonart
    /#20893778

    Шел 2019 год.
    В яве все еще принято явно указывать типы переменных.
    Методы расширения все еще ересь.
    Котлин? Не слышали.
    Так выпьем же за энтерпрайз ныне, присно и во веки веков!

    • alexkhitev
      /#20893892

      Обращаю внимание, что речь идет про legacy.
      Вы много проектов 10-15 летнего возраста видели на Java9+ или Kotlin?

    • Angmarets
      /#20894222

      хипстота скриптовая подтянулась

    • YuryB
      /#20896216 / -1

      плохому танцору…

  2. leventov
    /#20893790

    Начнём с того, что работает Collections.addAll намного быстрее. Об этом можно найти упоминание в JavaDoc этого метода:
    The behavior of this convenience method is identical to that of c.addAll(Arrays.asList(elements)), but this method is likely to run significantly faster under most

    Это очень вряд ли, на современной JDK. Не существует никакой магии, которая бы позволяла Collections.addAll быть гораздо быстрее. Скорее, наоборот, — c.addAll(Arrays.asList(elements)) может предаллоцировать структуры нужного размера у себя внутри.


    То, что вы перечислили как "минусы" Arrays.asList() — это просто случаи, когда он не применим. Когда он применим, однозначно, именно его и нужно использовать (либо ImmutableList.of() из Guava).


    Хоть статья про Java 8, можно было бы упомянуть что на новых версиях еще можно использовать List.of()/Set.of().

    • sshikov
      /#20894088

      >List.of()/Set.of().
      Можно и что-то типа vavr применить. И вообще если не ограничиваться только JDK, то вариантов много. И некоторые из них лучше описанных.

      • alexkhitev
        /#20895110

        Добрый день. Подскажите лучшие на Ваш взгляд не-JDK-шные средства если есть время.
        Я сконцентрировался на средствах JDK, потому что зачастую проекты могут пользоваться только своими репозиториями при сборке. Там может не быть какой нибудь хорошей библиотеки, вроде гуавы, а добавление библиотеки в какой нибудь nexus происходит не так просто.

        • sshikov
          /#20895118

          >зачастую проекты могут пользоваться только своими репозиториями при сборке
          Я бы не обобщал так. Ни разу за 20 лет не видел таких проектов. Чаще всего в моей практике нексус просто является миррором central, а остальные репозитории типа bintray добавляются по запросу без проблем. И это банк, например, где процесс добавления санкционируют безопасники.

          Из не JDK думаю что первое что приходит в голову это именно vavr. Там есть свой полноценный набор коллекций, не такой, как в JDK, и это как раз хорошо — есть выбор.

          Дальше по-идее все зависит от потребностей — например, persistent collections могут где-то пригодиться. Или trove для кого-то.

          • alexkhitev
            /#20895132

            Спасибо большое! Посмотрю.
            Я видел изолированные репозитории довольно часто. В банках, в некоторых зарубежных проектах.

          • leventov
            /#20895284

            Trove — нет, это неподдерживаемый проект с кучей багов. fastutil или Eclipse collections.

            • sshikov
              /#20895308

              Вы правы наверное. Я имел в виду, что не JDK коллекции для примитивных типов — это один из тех случаев, про которые спрашивали. Какой из них лучше — я реально не знаю.

    • Endeavour
      /#20894148

      Тоже очень удивился комментарию про скорость Collections.addAll, аж в джавадоке.
      Оказывается, addAll ArrayList, LinkedList и ArrayDeque копирует коллекцию в массив перед тем как добавить ее. Тогда действительно Collections будет лучше.
      Зачем так сделано — непонятно.

      • leventov
        /#20895274

        В случае ArrayList/ArrayDeque, и копии из другой ArrayList/ArrayDeque/другой array-based коллекции это сводит addAll() к двум операциям System.arraycopy, т. е. поточного копирования памяти. Оно в разы быстрее поэлементного копирования.


        Зачем в LinkedList — труднее сказать, но так как LinkedList в принципе никогда не надо использовать, это уже не важно

  3. puyol_dev2
    /#20893798

    Что это было?

    • trix
      /#20898460

      Подходит к тебе человек и просит написать статью для корпоративного блога. Ты такой: «Ну, ээ...»

  4. Maccimo
    /#20904872

    Благодаря этой статье я понял, что legacy-код это нечто, написанное студентами, бегло пролиставшими Эккеля в книжном.
    Спасибо!

    • alexkhitev
      /#20905270

      Legacy бывают разные.
      В одной компании на протяжении 14 лет код писали исключительно вчерашние студенты (из-за дешевизны). При этом уходили они в среднем через год. Представьте, что там было с кодом.
      Еще в одной, весь проект написан полностью на статике. ООП не существовало для его автора.
      С другой стороны встречается не мало нормальных проектов. С тестами, прозрачной архитектурой и без костылей с велосипедами