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);
     }
 
     /**