Привет, habr!
Я Software Engineer в EPAM. Более 8 лет я работаю с legacy-кодом, написанном на языке Java (предвосхищая комментарии, отмечу, что понимание и терпимость к legacy началась задолго до EPAM, в заключении вы найдёте ответ, почему). Часто в работе я сталкивался с одними и теми же повторяющимися недочетами. Это побудило меня написать заметку, и начать я хочу со структур данных и вспомогательных классов Collections и Arrays. Почему-то некоторые разработчики пренебрегают их использованием, и напрасно
Разработчику на Java часто приходится сталкиваться с различными структурами данных. Это могут быть массивы, всевозможные коллекции или реализации Map. Казалось бы, всё с ними ясно и понятно, но существует несколько мелочей, о которые легко споткнуться.
Эта заметка может оказаться полезной как новичкам, которые ещё не знают этих нюансов, так и опытным разработчикам, которые могли что-то из этого забыть.
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.addAllArrays.asList?
- Начнём с того, что работает 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
- Кроме того, 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) и max(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. И их значимость для заказчика трудно переоценить. Да и чувство победы от устранения бага, на поиск причин которого ушла не одна неделя, ничуть не уступает ощущениям при окончании реализации новой фичи.
Благодарю за внимание. Буду рад, если что-нибудь из представленного окажется полезным.
Всем успехов!
Автор: Алексей Хитёв