Перейти к основному содержимому

Набор гуавы + функция = карта

· 6 мин. чтения

1. Обзор

В этом руководстве мы проиллюстрируем одну из многих полезных функций пакета сбора Guava: как применить функцию к набору Guava и получить карту .

Мы обсудим два подхода — создание неизменяемой карты и динамической карты на основе встроенных операций гуавы, а затем реализацию пользовательской реализации живой карты .

2. Настройка

Во-первых, мы добавим библиотеку Guava в качестве зависимости в pom.xml:

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>

Небольшое примечание — вы можете проверить, есть ли более новая версия, здесь .

3. Функция отображения

Давайте сначала определим функцию, которую мы будем применять к элементам наборов:

Function<Integer, String> function = new Function<Integer, String>() {
@Override
public String apply(Integer from) {
return Integer.toBinaryString(from.intValue());
}
};

Функция просто преобразует значение Integer в его двоичное строковое представление.

4. Гуава toMap ()

Guava предлагает статический служебный класс, относящийся к экземплярам Map . Среди прочего, у него есть две операции, которые можно использовать для преобразования набора в карту , применяя определенную функцию Гуавы .

В следующем фрагменте показано создание неизменяемой карты :

Map<Integer, String> immutableMap = Maps.toMap(set, function);

Следующие тесты подтверждают правильность преобразования набора:

@Test
public void givenStringSetAndSimpleMap_whenMapsToElementLength_thenCorrect() {
Set set = new TreeSet(Arrays.asList(32, 64, 128));
Map<Integer, String> immutableMap = Maps.toMap(set, function);
assertTrue(immutableMap.get(32).equals("100000")
&& immutableMap.get(64).equals("1000000")
&& immutableMap.get(128).equals("10000000"));
}

Проблема с созданной картой заключается в том, что при добавлении элемента в исходный набор производная карта не обновляется.

4. Гуава как карта ()

Если мы воспользуемся предыдущим примером и создадим карту с помощью метода Maps.asMap :

Map<Integer, String> liveMap = Maps.asMap(set, function);

Мы получим живое представление карты — это означает, что изменения в исходном наборе также будут отражены на карте:

@Test
public void givenStringSet_whenMapsToElementLength_thenCorrect() {
Set<Integer> set = new TreeSet<Integer>(Arrays.asList(32, 64, 128));
Map<Integer, String> liveMap = Maps.asMap(set, function);
assertTrue(liveMap.get(32).equals("100000")
&& liveMap.get(64).equals("1000000")
&& liveMap.get(128).equals("10000000"));

set.add(256);
assertTrue(liveMap.get(256).equals("100000000") && liveMap.size() == 4);
}

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

5. Создание пользовательской живой карты

Когда мы говорим о представлении карты набора , мы в основном расширяем возможности набора с помощью функции Guava . ``

В представлении « Карта в реальном времени » изменения в наборе должны обновлять набор записей карты в режиме реального времени. Мы создадим нашу собственную универсальную карту с подклассом AbstractMap<K,V > , например:

public class GuavaMapFromSet<K, V> extends AbstractMap<K, V> {
public GuavaMapFromSet(Set<K> keys,
Function<? super K, ? extends V> function) {
}
}

Стоит отметить, что основным контрактом всех подклассов AbstractMap является реализация метода entrySet , как мы это сделали. Затем мы рассмотрим 2 критические части кода в следующих подразделах.

5.1. Записи

Другим атрибутом в нашей карте будут записи , представляющие наш EntrySet:

private Set<Entry<K, V>> entries;

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

public GuavaMapFromSet(Set<K> keys,Function<? super K, ? extends V> function) {
this.entries=keys;
}

Небольшое примечание: чтобы сохранить представление в реальном времени, мы будем использовать тот же итератор во входном наборе для последующего набора элементов Map .

При выполнении контракта AbstractMap<K,V > мы реализуем метод entrySet , в котором затем возвращаем записи :

@Override
public Set<java.util.Map.Entry<K, V>> entrySet() {
return this.entries;
}

5.2. Кэш

Эта карта хранит значения, полученные путем применения функции к набору:

private WeakHashMap<K, V> cache;

6. Итератор множества

Мы будем использовать итератор входного набора для последующего набора записей карты . Для этого мы используем настраиваемый EntrySet , а также настраиваемый класс Entry . ``

6.1. Начальный класс _

Во-первых, давайте посмотрим, как будет выглядеть отдельная запись на карте :

private class SingleEntry implements Entry<K, V> {
private K key;
public SingleEntry( K key) {
this.key = key;
}
@Override
public K getKey() {
return this.key;
}
@Override
public V getValue() {
V value = GuavaMapFromSet.this.cache.get(this.key);
if (value == null) {
value = GuavaMapFromSet.this.function.apply(this.key);
GuavaMapFromSet.this.cache.put(this.key, value);
}
return value;
}
@Override
public V setValue( V value) {
throw new UnsupportedOperationException();
}
}

Понятно, что в этом коде мы не разрешаем изменять набор из представления карты , поскольку вызов setValue вызывает исключение UnsupportedOperationException.

Обратите особое внимание на getValue — это суть нашей функциональности просмотра в реальном времени . Мы проверяем кеш внутри нашей карты на наличие текущего ключа ( элемент Set ).

Если мы находим ключ, мы возвращаем его, иначе мы применяем нашу функцию к текущему ключу и получаем значение , а затем сохраняем его в кеше .

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

6.2. EntrySet _ ``

Теперь мы реализуем EntrySet :

private class MyEntrySet extends AbstractSet<Entry<K, V>> {
private Set<K> keys;
public MyEntrySet(Set<K> keys) {
this.keys = keys;
}
@Override
public Iterator<Map.Entry<K, V>> iterator() {
return new LiveViewIterator();
}
@Override
public int size() {
return this.keys.size();
}
}

Мы выполнили контракт на расширение AbstractSet , переопределив методы итератора и размера . Но есть еще кое-что.

Помните, что экземпляр этого EntrySet будет формировать записи в нашем представлении карты . Обычно EntrySet карты просто возвращает полную запись для каждой итерации.

Однако в нашем случае нам нужно использовать итератор из входного набора , чтобы сохранить наше живое представление . Мы хорошо знаем, что он будет возвращать только элементы Set , поэтому нам также нужен собственный итератор .

6.3. Итератор _ ``

Вот реализация нашего итератора для указанного выше EntrySet :

public class LiveViewIterator implements Iterator<Entry<K, V>> {
private Iterator<K> inner;

public LiveViewIterator () {
this.inner = MyEntrySet.this.keys.iterator();
}

@Override
public boolean hasNext() {
return this.inner.hasNext();
}
@Override
public Map.Entry<K, V> next() {
K key = this.inner.next();
return new SingleEntry(key);
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}

LiveViewIterator должен находиться внутри класса MyEntrySet , таким образом, мы можем совместно использовать итератор Set при инициализации. ``

При переборе записей GuavaMapFromSet с использованием итератора вызов next просто извлекает ключ из итератора Set и создает SingleEntry .

7. Собираем все вместе

После объединения того, что мы рассмотрели в этом руководстве, давайте заменим переменную liveMap из предыдущих примеров и заменим ее нашей пользовательской картой:

@Test
public void givenIntSet_whenMapsToElementBinaryValue_thenCorrect() {
Set<Integer> set = new TreeSet<>(Arrays.asList(32, 64, 128));
Map<Integer, String> customMap = new GuavaMapFromSet<Integer, String>(set, function);

assertTrue(customMap.get(32).equals("100000")
&& customMap.get(64).equals("1000000")
&& customMap.get(128).equals("10000000"));
}

Изменив содержимое input Set , мы увидим, что Карта обновляется в режиме реального времени:

@Test
public void givenStringSet_whenMapsToElementLength_thenCorrect() {
Set<Integer> set = new TreeSet<Integer>(Arrays.asList(32, 64, 128));
Map<Integer, String> customMap = Maps.asMap(set, function);

assertTrue(customMap.get(32).equals("100000")
&& customMap.get(64).equals("1000000")
&& customMap.get(128).equals("10000000"));

set.add(256);
assertTrue(customMap.get(256).equals("100000000") && customMap.size() == 4);
}

8. Заключение

В этом руководстве мы рассмотрели различные способы использования операций Guava и получения представления карты из набора с помощью применения функции .

Полную реализацию всех этих примеров и фрагментов кода можно найти в моем проекте Guava на github — это проект на основе Eclipse, поэтому его легко импортировать и запускать как есть.