1. Обзор
В Java 9 появился долгожданный синтаксический сахар для создания небольших неизменяемых экземпляров Collection
с использованием лаконичного однострочного кода. Согласно JEP 269 , в JDK 9 будут включены новые удобные фабричные методы.
В этой статье мы рассмотрим его использование вместе с деталями реализации.
2. История и мотивация
Создание небольшой неизменяемой коллекции
в Java традиционным способом очень многословно.
Возьмем пример Set
:
Set<String> set = new HashSet<>();
set.add("foo");
set.add("bar");
set.add("baz");
set = Collections.unmodifiableSet(set);
Это слишком много кода для простой задачи, и это должно быть возможно сделать в одном выражении.
Вышесказанное справедливо и для карты.
Однако для List
есть фабричный метод:
List<String> list = Arrays.asList("foo", "bar", "baz");
Хотя это создание списка
лучше, чем инициализация конструктора, это менее очевидно , поскольку общая интуиция не будет заключаться в том, чтобы искать в классе Arrays
методы для создания списка
:
Существуют и другие способы уменьшения многословия, такие как метод инициализации с двойными фигурными скобками :
Set<String> set = Collections.unmodifiableSet(new HashSet<String>() {{
add("foo"); add("bar"); add("baz");
}});
или с помощью потоков
Java 8 :
Stream.of("foo", "bar", "baz")
.collect(collectingAndThen(toSet(), Collections::unmodifiableSet));
Техника двойной скобки лишь немного менее подробна, но значительно снижает удобочитаемость (и считается анти-шаблоном).
Однако версия Java 8 представляет собой однострочное выражение, и у нее тоже есть некоторые проблемы. Во-первых, это не очевидно и интуитивно понятно. Во-вторых, это все еще многословно. В-третьих, это связано с созданием ненужных объектов. И в-четвертых, этот метод нельзя использовать для создания Карты
.
Подводя итог недостаткам, можно сказать, что ни один из вышеперечисленных подходов не рассматривает конкретный вариант использования, создавая небольшую немодифицируемую проблему первого класса Коллекции .
3. Описание и использование
Статические методы были предоставлены для интерфейсов List
, Set
и Map
, которые принимают элементы в качестве аргументов и возвращают экземпляры List
, Set
и Map
соответственно.
Этот метод называется of(…)
для всех трех интерфейсов.
3.1. Список
и набор
Сигнатура и характеристики фабричных методов List
и Set
одинаковы:
static <E> List<E> of(E e1, E e2, E e3)
static <E> Set<E> of(E e1, E e2, E e3)
использование методов:
List<String> list = List.of("foo", "bar", "baz");
Set<String> set = Set.of("foo", "bar", "baz");
Как мы видим, это очень просто, коротко и лаконично.
В примере мы использовали метод, который принимает ровно три элемента в качестве параметров и возвращает список
/ набор
размера 3.
Но существует 12 перегруженных версий этого метода — одиннадцать с параметрами от 0 до 10 и одна с var-args:
static <E> List<E> of()
static <E> List<E> of(E e1)
static <E> List<E> of(E e1, E e2)
// ....and so on
static <E> List<E> of(E... elems)
Для большинства практических целей будет достаточно 10 элементов, но если требуется больше, можно использовать версию с переменными аргументами.
Теперь мы можем спросить, какой смысл иметь 11 дополнительных методов, если есть версия с переменными аргументами, которая может работать для любого количества элементов.
Ответ на это — производительность. Каждый вызов метода var-args неявно создает массив. Наличие перегруженных методов позволяет избежать ненужного создания объектов и накладных расходов на сборку мусора. Напротив, Arrays.asList
всегда создает этот неявный массив и, следовательно, менее эффективен, когда количество элементов невелико.
Если во время создания Set
с использованием фабричного метода в качестве параметров передаются повторяющиеся элементы, то во время выполнения выбрасывается исключение IllegalArgumentException :
@Test(expected = IllegalArgumentException.class)
public void onDuplicateElem_IfIllegalArgExp_thenSuccess() {
Set.of("foo", "bar", "baz", "foo");
}
Здесь важно отметить, что, поскольку фабричные методы используют дженерики, примитивные типы упаковываются автоматически.
Если передается массив примитивного типа, возвращается список
массивов
этого примитивного типа.
Например:
int[] arr = { 1, 2, 3, 4, 5 };
List<int[]> list = List.of(arr);
В этом случае возвращается List<int[]>
размера 1, а элемент с индексом 0 содержит массив.
3.2. карта
Сигнатура метода Map
factory:
static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3)
и использование:
Map<String, String> map = Map.of("foo", "a", "bar", "b", "baz", "c");
Подобно List
и Set
, метод of(…)
перегружен, чтобы иметь от 0 до 10 пар ключ-значение.
В случае Map
существует другой метод для более чем 10 пар ключ-значение:
static <K,V> Map<K,V> ofEntries(Map.Entry<? extends K,? extends V>... entries)
и его использование:
Map<String, String> map = Map.ofEntries(
new AbstractMap.SimpleEntry<>("foo", "a"),
new AbstractMap.SimpleEntry<>("bar", "b"),
new AbstractMap.SimpleEntry<>("baz", "c"));
Передача повторяющихся значений для Key вызовет исключение IllegalArgumentException
:
@Test(expected = IllegalArgumentException.class)
public void givenDuplicateKeys_ifIllegalArgExp_thenSuccess() {
Map.of("foo", "a", "foo", "b");
}
Опять же, и в случае с Map
примитивные типы упаковываются автоматически.
4. Замечания по реализации
Коллекции, созданные с использованием фабричных методов, не являются широко используемыми реализациями.
Например, List
не является ArrayList
, а Map
не является HashMap
. Это разные реализации, представленные в Java 9. Эти реализации являются внутренними, а их конструкторы имеют ограниченный доступ.
В этом разделе мы увидим некоторые важные различия в реализации, общие для всех трех типов коллекций.
4.1. Неизменный
Коллекции, созданные с использованием фабричных методов, являются неизменяемыми, и изменение элемента, добавление новых элементов или удаление элемента вызывает исключение UnsupportedOperationException
:
@Test(expected = UnsupportedOperationException.class)
public void onElemAdd_ifUnSupportedOpExpnThrown_thenSuccess() {
Set<String> set = Set.of("foo", "bar");
set.add("baz");
}
@Test(expected = UnsupportedOperationException.class)
public void onElemModify_ifUnSupportedOpExpnThrown_thenSuccess() {
List<String> list = List.of("foo", "bar");
list.set(0, "baz");
}
@Test(expected = UnsupportedOperationException.class)
public void onElemRemove_ifUnSupportedOpExpnThrown_thenSuccess() {
Map<String, String> map = Map.of("foo", "a", "bar", "b");
map.remove("foo");
}
4.2. Нулевой
элемент не разрешен
В случае List
и Set
никакие элементы не могут быть нулевыми
. В случае Map
ни ключи, ни значения не могут быть нулевыми
. Передача нулевого
аргумента вызывает исключение NullPointerException
:
@Test(expected = NullPointerException.class)
public void onNullElem_ifNullPtrExpnThrown_thenSuccess() {
List.of("foo", "bar", null);
}
В отличие от List.of
метод Arrays.asList
принимает нулевые
значения.
4.3. Экземпляры на основе ценности
Экземпляры, созданные фабричными методами, основаны на значениях. Это означает, что фабрики могут создавать новые экземпляры или возвращать существующие экземпляры.
Следовательно, если мы создадим списки с одинаковыми значениями, они могут ссылаться или не ссылаться на один и тот же объект в куче:
List<String> list1 = List.of("foo", "bar");
List<String> list2 = List.of("foo", "bar");
В этом случае list1 == list2
может быть или не быть истинным
в зависимости от JVM.
4.4. Сериализация
Коллекции, созданные из фабричных методов, являются сериализуемыми
, если элементы коллекции являются сериализуемыми.
5. Вывод
В этой статье мы представили новые фабричные методы для коллекций, представленные в Java 9.
Мы пришли к выводу, почему эта функция является долгожданным изменением, изучив некоторые прошлые методы создания неизменяемых коллекций. Мы рассмотрели его использование и выделили ключевые моменты, которые следует учитывать при их использовании.
Наконец, мы уточнили, что эти коллекции отличаются от широко используемых реализаций, и указали на ключевые отличия.
Полный исходный код и модульные тесты для этой статьи доступны на GitHub .