You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2022/02/26 11:25:04 UTC
[groovy] 02/02: GROOVY-7802: MapWithDefault should be able to be configured to not store its default value
This is an automated email from the ASF dual-hosted git repository.
paulk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 77039ed8288c0b3612a0126d4d49bb71e951ff8b
Author: Paul King <pa...@asert.com.au>
AuthorDate: Fri Feb 25 17:50:41 2022 +1000
GROOVY-7802: MapWithDefault should be able to be configured to not store its default value
---
src/main/java/groovy/lang/MapWithDefault.java | 83 +++++++++++++++++++---
.../groovy/runtime/DefaultGroovyMethods.java | 55 +++++++++++++-
2 files changed, 126 insertions(+), 12 deletions(-)
diff --git a/src/main/java/groovy/lang/MapWithDefault.java b/src/main/java/groovy/lang/MapWithDefault.java
index ecd135d..405f8ce 100644
--- a/src/main/java/groovy/lang/MapWithDefault.java
+++ b/src/main/java/groovy/lang/MapWithDefault.java
@@ -23,22 +23,51 @@ import java.util.Map;
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 final class MapWithDefault<K, V> implements Map<K, V> {
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 final class MapWithDefault<K, V> implements Map<K, V> {
@Override
public void putAll(Map<? extends K, ? extends V> m) {
- delegate.putAll(m);
+ m.entrySet().forEach(e -> put(e.getKey(), e.getValue()));
}
@Override
diff --git a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
index 38eb499..e56062d 100644
--- a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
+++ b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
@@ -8701,6 +8701,7 @@ public class DefaultGroovyMethods extends DefaultGroovyMethodsSupport {
* 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 class DefaultGroovyMethods extends DefaultGroovyMethodsSupport {
* @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);
}
/**