Skip to content
Permalink
Browse files
GROOVY-7802: MapWithDefault should be able to be configured to not st…
…ore its default value
  • Loading branch information
paulk-asert committed Feb 26, 2022
1 parent 00729e7 commit 77039ed8288c0b3612a0126d4d49bb71e951ff8b
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 12 deletions.
@@ -23,22 +23,51 @@
import java.util.Set;

/**
* A wrapper for Map which allows a default value to be specified.
* A wrapper for Map which allows a default value to be specified using a closure.
* Normally not instantiated directly but used via the DGM <code>withDefault</code> method.
*
* @since 1.7.1
*/
public final class MapWithDefault<K, V> implements Map<K, V> {

private final Map<K, V> delegate;
private final Closure initClosure;
private final Closure<V> initClosure;
private final boolean autoGrow;
private final boolean autoShrink;

private MapWithDefault(Map<K, V> m, Closure initClosure) {
private MapWithDefault(Map<K, V> m, Closure<V> initClosure, boolean autoGrow, boolean autoShrink) {
delegate = m;
this.initClosure = initClosure;
}

public static <K, V> Map<K, V> newInstance(Map<K, V> m, Closure initClosure) {
return new MapWithDefault<K, V>(m, initClosure);
this.autoGrow = autoGrow;
this.autoShrink = autoShrink;
}

/**
* Decorates the given Map allowing a default value to be specified.
*
* @param m a Map to wrap
* @param initClosure the closure which when passed the <code>key</code> returns the default value
* @return the wrapped Map
*/
public static <K, V> Map<K, V> newInstance(Map<K, V> m, Closure<V> initClosure) {
return new MapWithDefault<>(m, initClosure, true, false);
}

/**
* Decorates the given Map allowing a default value to be specified.
* Allows the behavior to be configured using {@code autoGrow} and {@code autoShrink} parameters.
* The value of {@code autoShrink} doesn't alter any values in the initial wrapped map, but you
* can start with an empty map and use {@code putAll} if you really need the minimal backing map value.
*
* @param m a Map to wrap
* @param autoGrow when true, also mutate the map adding in this value; otherwise, don't mutate the map, just return to calculated value
* @param autoShrink when true, ensure the key will be removed if attempting to store the default value using put or putAll
* @param initClosure the closure which when passed the <code>key</code> returns the default value
* @return the wrapped Map
* @since 4.0.1
*/
public static <K, V> Map<K, V> newInstance(Map<K, V> m, boolean autoGrow, boolean autoShrink, Closure<V> initClosure) {
return new MapWithDefault<>(m, initClosure, autoGrow, autoShrink);
}

@Override
@@ -61,16 +90,48 @@ public boolean containsValue(Object value) {
return delegate.containsValue(value);
}

/**
* Returns the value to which the specified key is mapped,
* or the default value as specified by the initializing closure
* if this map contains no mapping for the key.
*
* If <code>autoGrow</code> is true and the initializing closure is called,
* the map is modified to contain the new key and value so that the calculated
* value is effectively cached if needed again.
* Otherwise, the map will be unchanged.
*/
@Override
public V get(Object key) {
if (!delegate.containsKey(key)) {
delegate.put((K)key, (V)initClosure.call(new Object[]{key}));
if (delegate.containsKey(key)) {
return delegate.get(key);
}
V value = getDefaultValue(key);
if (autoGrow) {
delegate.put((K)key, value);
}
return delegate.get(key);
return value;
}

private V getDefaultValue(Object key) {
return initClosure.call(new Object[]{key});
}

/**
* Associates the specified value with the specified key in this map.
*
* If <code>autoShrink</code> is true, the initializing closure is called
* and if it evaluates to the value being stored, the value will not be stored
* and indeed any existing value will be removed. This can be useful when trying
* to keep the memory requirements small for large key sets where only a spare
* number of entries differ from the default.
*
* @return the previous value associated with {@code key} if any, otherwise {@code null}.
*/
@Override
public V put(K key, V value) {
if (autoShrink && value.equals(getDefaultValue(key))) {
return remove(key);
}
return delegate.put(key, value);
}

@@ -81,7 +142,7 @@ public V remove(Object key) {

@Override
public void putAll(Map<? extends K, ? extends V> m) {
delegate.putAll(m);
m.entrySet().forEach(e -> put(e.getKey(), e.getValue()));
}

@Override
@@ -8701,6 +8701,7 @@ public static SpreadMap toSpreadMap(Iterable self) {
* to <code>get(key)</code>. If an unknown key is found, a default value will be
* stored into the Map before being returned. The default value stored will be the
* result of calling the supplied Closure with the key as the parameter to the Closure.
*
* Example usage:
* <pre class="groovyTestCase">
* def map = [a:1, b:2].withDefault{ k {@code ->} k.toCharacter().isLowerCase() ? 10 : -10 }
@@ -8716,9 +8717,61 @@ public static SpreadMap toSpreadMap(Iterable self) {
* @param init a Closure which is passed the unknown key
* @return the wrapped Map
* @since 1.7.1
* @see #withDefault(Map, boolean, boolean, Closure)
*/
public static <K, V> Map<K, V> withDefault(Map<K, V> self, @ClosureParams(FirstParam.FirstGenericType.class) Closure<V> init) {
return MapWithDefault.newInstance(self, init);
return MapWithDefault.newInstance(self, true, false, init);
}

/**
* Wraps a map using the decorator pattern with a wrapper that intercepts all calls
* to <code>get(key)</code> and <code>put(key, value)</code>.
* If an unknown key is found for <code>get</code>, a default value will be returned.
* The default value will be the result of calling the supplied Closure with the key
* as the parameter to the Closure.
* If <code>autoGrow</code> is set, the value will be stored into the map.
*
* If <code>autoShrink</code> is set, then an attempt to <code>put</code> the default value
* into the map is ignored and indeed any existing value would be removed.
*
* If you wish the backing map to be as small as possible, consider setting <code>autoGrow</code>
* to <code>false</code> and <code>autoShrink</code> to <code>true</code>.
* This keeps the backing map as small as possible, i.e. sparse, but also means that
* <code>containsKey</code>, <code>keySet</code>, <code>size</code>, and other methods
* will only reflect the sparse values.
*
* If you are wrapping an immutable map, you should set <code>autoGrow</code>
* and <code>autoShrink</code> to <code>false</code>.
* In this scenario, the <code>get</code> method is essentially a shorthand
* for calling <code>getOrDefault</code> with the default value supplied once as a Closure.
*
* Example usage:
* <pre class="groovyTestCase">
* // sparse map example
* def answers = [life: 100].withDefault(false, true){ 42 }
* assert answers.size() == 1
* assert answers.foo == 42
* assert answers.size() == 1
* answers.life = 42
* answers.putAll(universe: 42, everything: 42)
* assert answers.size() == 0
*
* // immutable map example
* def certainties = [death: true, taxes: true].asImmutable().withDefault(false, false){ false }
* assert certainties.size() == 2
* assert certainties.wealth == false
* assert certainties.size() == 2
* </pre>
*
* @param self a Map
* @param autoGrow whether calling get could potentially grow the map if the key isn't found
* @param autoShrink whether calling put with the default value could potentially shrink the map
* @param init a Closure which is passed the unknown key
* @return the wrapped Map
* @since 4.0.1
*/
public static <K, V> Map<K, V> withDefault(Map<K, V> self, boolean autoGrow, boolean autoShrink, @ClosureParams(FirstParam.FirstGenericType.class) Closure<V> init) {
return MapWithDefault.newInstance(self, autoGrow, autoShrink, init);
}

/**

0 comments on commit 77039ed

Please sign in to comment.