Trigger a specific behavior implementation when a Map changes

1. Background Usually during development, we ...
1. Background

Usually during development, we choose to use Guava's Cache class as the local cache.

Sometimes, however, we don't need expiration control, we don't want to introduce Guava packages, and we also choose to use Map for caching simply and roughly.

However, in some business scenarios, special handling is required when Map properties change.

For example, when a Map is unchanged, an immutable List is generated based on the value of the Map.

2. Methods

Attempting to use Guava's Cache class, we found that when CacheBuilder constructed the Cache, only RemovalListener provided listening for removing or replacing elements, not for adding new elements, emptying map s, and so on.

Therefore, we can transform the Map by drawing on the idea of SynchronizedList, which is the reference of SynchronizedList as a list held inside the wrapper.

static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> { private static final long serialVersionUID = -7754090372962971524L; final List<E> list; SynchronizedList(List<E> list) { super(list); this.list = list; } SynchronizedList(List<E> list, Object mutex) { super(list, mutex); this.list = list; } public boolean equals(Object o) { if (this == o) return true; synchronized (mutex) } public int hashCode() { synchronized (mutex) } public E get(int index) { synchronized (mutex) } public E set(int index, E element) { synchronized (mutex) } public void add(int index, E element) { synchronized (mutex) } public E remove(int index) { synchronized (mutex) } public int indexOf(Object o) { synchronized (mutex) } // Omit Others }

The reference code is as follows:

Construct a ModifyHookMap class that passes through Builder to the underlying Map and callback functions that need to be aware.

import org.apache.commons.math3.util.Pair; import java.util.Collection; import java.util.Map; import java.util.Set; import java.util.function.Consumer; public class ModifyHookMap<K, V> implements Map<K, V> { private Map<K, V> map; private Consumer<ModifyHookMap<K, V>> onModified; private Consumer<Pair<K, V>> onPut; private Consumer<Pair<K, V>> onRemove; private Consumer<Map<? extends K, ? extends V>> onPutAll; private Consumer<Void> onClear; private ModifyHookMap(Builder<K, V> builder) { map = builder.map; onModified = builder.onModified; onPut = builder.onPut; onRemove = builder.onRemove; onPutAll = builder.onPutAll; onClear = builder.onClear; } @Override public int size() { return map.size(); } @Override public boolean isEmpty() { return map.isEmpty(); } @Override public boolean containsKey(Object key) { return map.containsKey(key); } @Override public boolean containsValue(Object value) { return map.containsValue(value); } @Override public V get(Object key) { return map.get(key); } @Override public V put(K key, V value) { V put = map.put(key, value); if (onPut != null) { onPut.accept(new Pair<>(key, value)); } if (onModified != null) { onModified.accept(this); } return put; } @Override public V remove(Object key) { V remove = map.remove(key); if (onRemove != null) { onRemove.accept(new Pair<>((K) key, remove)); } if (onModified != null) { onModified.accept(this); } return remove; } @Override public void putAll(Map<? extends K, ? extends V> m) { if (onPutAll != null) { onPutAll.accept(m); } map.putAll(m); if (onModified != null) { onModified.accept(this); } } @Override public void clear() { map.clear(); if (onClear != null) { onClear.accept(null); } if (onModified != null) { onModified.accept(this); } } @Override public Set<K> keySet() { return map.keySet(); } @Override public Collection<V> values() { return map.values(); } @Override public Set<Entry<K, V>> entrySet() { return map.entrySet(); } public static final class Builder<K, V> { private Map<K, V> map; private Consumer<ModifyHookMap<K, V>> onModified; private Consumer<Pair<K, V>> onPut; private Consumer<Pair<K, V>> onRemove; private Consumer<Map<? extends K, ? extends V>> onPutAll; private Consumer<Void> onClear; public Builder() { } public Builder<K, V> map(Map<K, V> val) { map = val; return this; } public Builder<K, V> onModified(Consumer<ModifyHookMap<K, V>> val) { onModified = val; return this; } public Builder<K, V> onPut(Consumer<Pair<K, V>> val) { onPut = val; return this; } public Builder<K, V> onRemove(Consumer<Pair<K, V>> val) { onRemove = val; return this; } public Builder<K, V> onPutAll(Consumer<Map<? extends K, ? extends V>> val) { onPutAll = val; return this; } public Builder<K, V> onClear(Consumer<Void> val) { onClear = val; return this; } public ModifyHookMap<K, V> build() { return new ModifyHookMap<K, V>(this); } } }

The test code is as follows (in a hurry, no JUnit single test is used here, simply use the main method):

import com.alibaba.fastjson.JSON; import com.google.common.collect.ImmutableList; import org.apache.commons.math3.util.Pair; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class Demo { private static List<Integer> cache = new ArrayList<Integer>(); private static ModifyHookMap<String, Integer> mapWrapper = new ModifyHookMap .Builder<String, Integer>() .map(new HashMap<>()) .onClear((v) -> { System.out.println("implement clear"); }) .onRemove((Pair<String, Integer> pair) -> { System.out.println("implement remove,k:" + pair.getKey() + ";v" + pair.getValue()); }) .onPut((Pair<String, Integer> pair) -> { System.out.println("implement put,k:" + pair.getKey() + ";v" + pair.getValue()); }) .onPutAll((Map<? extends String, ? extends Integer> map) -> { System.out.println("implement putAll:" + JSON.toJSONString(map)); }) .onModified((modifyInvokeMap -> { cache = ImmutableList.copyOf(modifyInvokeMap.values()); System.out.println("Perform modifications" + JSON.toJSONString(cache)); })) .build(); public static void main(String[] args) { mapWrapper.put("1", 1); Map<String, Integer> otherMap = new HashMap<>(); otherMap.put("2", 2); mapWrapper.putAll(otherMap); mapWrapper.remove("2"); mapWrapper.clear(); } }

Output:

Execute put, k:1;v1
Perform Modification [1]
Execute putAll:{"2":2}
Perform Modification [1,2]
Execute remove, k:2;v2
Perform Modification [1]
Execute clear
Perform Modification []

It is important to note that if the key s or value s passed in from Consumer are modified, the underlying Map's data will be affected.

Therefore, only read operations are recommended in Consumer, not modify operations.

3. Summary

This article implements its own Map with callbacks by referring to SynchronizedList.

What I want to say is that open source code is not everything. When open source code is not enough, sometimes we need to write our own specific logic to meet business needs.

In addition, the value of learning lies in making use of what you have learned. We should try to use the necessary knowledge flexibly to solve business problems, so that we can truly bring the value of reading source code into play.

If you have a better way to handle it, you are welcome to comment and discuss with me.

9 November 2021, 12:37 | Views: 9218

Add new comment

For adding a comment, please log in
or create account

0 comments