You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by bd...@apache.org on 2021/06/21 16:45:32 UTC

[sling-org-apache-sling-graphql-core] 01/01: SLING-10502 - lazy loading helpers

This is an automated email from the ASF dual-hosted git repository.

bdelacretaz pushed a commit to branch SLING-10502/lazy-loading
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-graphql-core.git

commit 7aea4ef95f4e1e79df341f04399f9c0123ab9519
Author: Bertrand Delacretaz <bd...@apache.org>
AuthorDate: Mon Jun 21 18:42:53 2021 +0200

    SLING-10502 - lazy loading helpers
---
 .../helpers/layzloading/LazyLoadingField.java      |  44 +++
 .../helpers/layzloading/LazyLoadingMap.java        | 171 ++++++++++++
 .../graphql/helpers/layzloading/package-info.java  |  26 ++
 .../helpers/lazyloading/LazyLoadingFieldTest.java  |  65 +++++
 .../helpers/lazyloading/LazyLoadingMapTest.java    | 300 +++++++++++++++++++++
 5 files changed, 606 insertions(+)

diff --git a/src/main/java/org/apache/sling/graphql/helpers/layzloading/LazyLoadingField.java b/src/main/java/org/apache/sling/graphql/helpers/layzloading/LazyLoadingField.java
new file mode 100644
index 0000000..83ce1f6
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/helpers/layzloading/LazyLoadingField.java
@@ -0,0 +1,44 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+
+package org.apache.sling.graphql.helpers.layzloading;
+
+import java.util.function.Supplier;
+
+/** Helper for a single lazy-loading value */
+public class LazyLoadingField<T> {
+    private Supplier<T> supplier;
+    private T value;
+
+    public LazyLoadingField(Supplier<T> supplier) {
+        this.supplier = supplier;
+    }
+
+    public T get() {
+        if(value == null) {
+            synchronized(this) {
+                if(value == null && supplier != null) {
+                    value = supplier.get();
+                }
+                supplier = null;
+            }
+        }
+        return value;
+    }
+}
diff --git a/src/main/java/org/apache/sling/graphql/helpers/layzloading/LazyLoadingMap.java b/src/main/java/org/apache/sling/graphql/helpers/layzloading/LazyLoadingMap.java
new file mode 100644
index 0000000..1a70e62
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/helpers/layzloading/LazyLoadingMap.java
@@ -0,0 +1,171 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+
+package org.apache.sling.graphql.helpers.layzloading;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LazyLoadingMap<K, T> extends HashMap<K, T> {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private final Map<K, Supplier<T>> suppliers = new HashMap<>();
+    private int suppliersCallCount;
+
+    /** Calls computeAll - should be avoided if possible */
+    @Override
+    public boolean equals(Object o) {
+        if(!(o instanceof LazyLoadingMap)) {
+            return false;
+        }
+        final LazyLoadingMap<?,?> other = (LazyLoadingMap<?,?>)o;
+
+        // Equality seems complicated to compute without this
+        computeAll();
+        return super.equals(other);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + suppliers.hashCode();
+    }
+
+    /** Adds a Supplier that provides a lazy loaded value 
+     *  @return this object, to be able to chain calls
+    */
+    public LazyLoadingMap<K, T> withSupplier(K key, Supplier<T> supplier) {
+        suppliers.put(key, supplier);
+        return this;
+    }
+
+    @Override
+    public T get(Object key) {
+        return lazyCompute(key);
+    }
+
+    @SuppressWarnings("unchecked")
+    private T lazyCompute(Object key) {
+        if(key == null) {
+            return null;
+        }
+        T value = super.get(key);
+        if(value == null) {
+            synchronized(this) {
+                if(value == null) {
+                    final Supplier<T> s = suppliers.remove(key);
+                    if(s != null) {
+                        suppliersCallCount++;
+                        value = s.get();
+                        super.put((K)key, value);
+                    }
+                }
+            }
+        }
+        return value;
+    }
+
+
+    /** This indicates how many Supplier calls have been made.
+     *  Can be useful in case of doubt, as several methods need
+     *  to call computeAll().
+     */
+    public int getSuppliersCallCount() {
+        return suppliersCallCount;
+    }
+
+    @Override
+    public T remove(Object key) {
+        synchronized(this) {
+            lazyCompute(key);
+            return super.remove(key);
+        }        
+    }
+
+    @Override
+    public void clear() {
+        synchronized(this) {
+            suppliers.clear();
+            super.clear();
+        }
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        return super.containsKey(key) || suppliers.containsKey(key);
+    }
+
+    @Override
+    public int size() {
+        return keySet().size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return super.isEmpty() && suppliers.isEmpty();
+    }
+
+    @Override
+    public Set<K> keySet() {
+        final Set<K> result = new HashSet<>();
+        result.addAll(super.keySet());
+        result.addAll(suppliers.keySet());
+        return result;
+    }
+
+    /** Required for some methods that need all our values
+     *  Calling those methods should be avoided if possible
+     */
+    private void computeAll() {
+        log.info("computeAll called, all remaining lazy values will be evaluated now");
+        suppliers.entrySet().forEach(e -> {
+            if(!super.containsKey(e.getKey())) {
+                suppliersCallCount++;
+                put(e.getKey(), e.getValue().get());
+            }
+        });
+    }
+
+    /** Calls computeAll - should be avoided if possible */
+    @Override
+    public Collection<T> values() {
+        computeAll();
+        return super.values();
+    }
+
+    /** Calls computeAll - should be avoided if possible */
+    @Override
+    public Set<Entry<K, T>> entrySet() {
+        computeAll();
+        return super.entrySet();
+    }
+
+    /** Calls computeAll - should be avoided if possible */
+    @Override
+    public boolean containsValue(Object value) {
+        computeAll();
+        return super.containsValue(value);
+    }
+}
diff --git a/src/main/java/org/apache/sling/graphql/helpers/layzloading/package-info.java b/src/main/java/org/apache/sling/graphql/helpers/layzloading/package-info.java
new file mode 100644
index 0000000..b43f77a
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/helpers/layzloading/package-info.java
@@ -0,0 +1,26 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+
+ /**
+  * This package contains helpers which are independent of
+  * a specific implementation of the underlying graphQL engine.
+  */
+@Version("0.0.1")
+package org.apache.sling.graphql.helpers.layzloading;
+import org.osgi.annotation.versioning.Version;
diff --git a/src/test/java/org/apache/sling/graphql/helpers/lazyloading/LazyLoadingFieldTest.java b/src/test/java/org/apache/sling/graphql/helpers/lazyloading/LazyLoadingFieldTest.java
new file mode 100644
index 0000000..52608fe
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/helpers/lazyloading/LazyLoadingFieldTest.java
@@ -0,0 +1,65 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.graphql.helpers.lazyloading;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+
+import org.apache.sling.graphql.helpers.layzloading.LazyLoadingField;
+import org.junit.Test;
+
+public class LazyLoadingFieldTest {
+    @Test
+    public void basicTest() {
+        final AtomicInteger nCalls = new AtomicInteger();
+
+        final Supplier<String> sup = () -> {
+            nCalls.incrementAndGet();
+            return UUID.randomUUID().toString();
+        };
+
+        final LazyLoadingField<String> f = new LazyLoadingField<>(sup);
+        assertEquals(0, nCalls.get());
+        final String firstValue = f.get();
+        assertEquals(1, nCalls.get());
+        for(int i=0; i < 42; i++) {
+            assertEquals(firstValue, f.get());
+        }
+        assertEquals(1, nCalls.get());
+    }
+
+    @Test
+    public void nullSupplier() {
+        final AtomicInteger nCalls = new AtomicInteger();
+        final Supplier<Double> nullSup = () -> {
+            nCalls.incrementAndGet();
+            return null;
+        };
+        final LazyLoadingField<Double> f = new LazyLoadingField<>(nullSup);
+        assertEquals(0, nCalls.get());
+        assertNull(f.get());
+        assertEquals(1, nCalls.get());
+        assertNull(f.get());
+        assertEquals(1, nCalls.get());
+    }
+}
diff --git a/src/test/java/org/apache/sling/graphql/helpers/lazyloading/LazyLoadingMapTest.java b/src/test/java/org/apache/sling/graphql/helpers/lazyloading/LazyLoadingMapTest.java
new file mode 100644
index 0000000..c886303
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/helpers/lazyloading/LazyLoadingMapTest.java
@@ -0,0 +1,300 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.graphql.helpers.lazyloading;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Supplier;
+
+import org.apache.sling.graphql.helpers.layzloading.LazyLoadingMap;
+import org.junit.Before;
+import org.junit.Test;
+
+public class LazyLoadingMapTest {
+    private static final String TEST_STRING = "Fritz Frisst etc. etc.";
+    private int counter;
+    private Supplier<String> lazyCounter = () -> "X" + String.valueOf(++counter);
+
+    @Before
+    public void setup() {
+        counter = 0;
+    }
+
+    @Test
+    public void basicTest() {
+        final LazyLoadingMap<Integer, String> map = new LazyLoadingMap<>();
+        assertNull(map.get(42));
+
+        map
+            .withSupplier(21, () -> UUID.randomUUID().toString())
+            .withSupplier(42, () -> TEST_STRING)
+        ;
+        assertEquals(0, map.getSuppliersCallCount());
+        assertEquals(TEST_STRING, map.get(42));
+        assertEquals(1, map.getSuppliersCallCount());
+        final String random = map.get(21);
+        assertNotNull(random);
+        assertEquals(random, map.get(21));
+        assertEquals(2, map.getSuppliersCallCount());
+    }
+
+    @Test
+    public void addAlsoWorks() {
+        final LazyLoadingMap<Integer, String> map = new LazyLoadingMap<>();
+        map.put(42, TEST_STRING);
+        assertEquals(TEST_STRING, map.get(42));
+        map.withSupplier(42, () -> "should not change");
+        assertEquals(TEST_STRING, map.get(42));
+        assertEquals(0, map.getSuppliersCallCount());
+    }
+
+    @Test
+    public void remove() {
+        final LazyLoadingMap<Integer, String> map = new LazyLoadingMap<>();
+        map.withSupplier(21, lazyCounter);
+        map.put(42, TEST_STRING);
+        assertEquals(2, map.size());
+        assertEquals(TEST_STRING, map.get(42));
+        assertEquals(TEST_STRING, map.remove(42));
+        assertNull(map.get(42));
+        assertEquals(1, map.size());
+        assertEquals(0, map.getSuppliersCallCount());
+
+        // Remove before and after computing
+        assertEquals(0, map.getSuppliersCallCount());
+        map.withSupplier(112, lazyCounter);
+        map.withSupplier(113, lazyCounter);
+        assertEquals("X1", map.get(113));
+        assertEquals("X1", map.remove(113));
+        assertEquals(1, map.getSuppliersCallCount());
+        assertEquals("X2", map.remove(112));
+        assertEquals(2, map.getSuppliersCallCount());
+    }
+
+    @Test
+    public void containsValueComputesEverything() {
+        final LazyLoadingMap<Integer, String> map = new LazyLoadingMap<>();
+        assertFalse(map.containsKey(42));
+        assertEquals(0, map.getSuppliersCallCount());
+
+        assertFalse(map.containsValue("X1"));
+        map.withSupplier(42, lazyCounter);
+        assertTrue(map.containsValue("X1"));
+
+        assertFalse(map.containsValue("X2"));
+        map.withSupplier(21, lazyCounter);
+        assertEquals(1, map.getSuppliersCallCount());
+        assertTrue(map.containsValue("X1"));
+        assertTrue(map.containsValue("X2"));
+        assertEquals(2, map.getSuppliersCallCount());
+
+        assertFalse(map.containsValue(TEST_STRING));
+        map.put(71, TEST_STRING);
+        map
+            .withSupplier(92, lazyCounter)
+            .withSupplier(93, lazyCounter)
+        ;
+        assertTrue(map.containsValue(TEST_STRING));
+        assertTrue(map.containsValue("X1"));
+        assertTrue(map.containsValue("X2"));
+        assertTrue(map.containsValue("X3"));
+        assertTrue(map.containsValue("X4"));
+        assertFalse(map.containsValue("X5"));
+
+        assertEquals(4, map.getSuppliersCallCount());
+    }
+
+    @Test
+    public void containsKey() {
+        final LazyLoadingMap<Integer, String> map = new LazyLoadingMap<>();
+        assertFalse(map.containsKey(42));
+        assertEquals(0, map.getSuppliersCallCount());
+
+        map.withSupplier(42, lazyCounter);
+        map.put(21, "nothing");
+
+        assertTrue(map.containsKey(42));
+        assertTrue(map.containsKey(21));
+        assertFalse(map.containsKey(22));
+
+        assertEquals(0, map.getSuppliersCallCount());
+    }
+
+    @Test
+    public void keySet() {
+        final LazyLoadingMap<Integer, String> map = new LazyLoadingMap<>();
+        assertEquals(0, map.keySet().size());
+        map.put(112, "nothing");
+        assertEquals(1, map.keySet().size());
+        map.withSupplier(21, lazyCounter);
+        map.withSupplier(42, lazyCounter);
+        map.withSupplier(110, lazyCounter);
+
+        assertEquals(0, map.getSuppliersCallCount());
+        map.get(42);
+        assertEquals(1, map.getSuppliersCallCount());
+
+        final Set<Integer> ks = map.keySet();
+        assertEquals(4, ks.size());
+        assertTrue(ks.contains(21));
+        assertTrue(ks.contains(42));
+        assertTrue(ks.contains(112));
+        assertTrue(ks.contains(110));
+        assertEquals(1, map.getSuppliersCallCount());
+    }
+
+    @Test
+    public void entrySet() {
+        final LazyLoadingMap<Integer, String> map = new LazyLoadingMap<>();
+        map.put(112, TEST_STRING);
+        map.withSupplier(21, lazyCounter);
+        map.withSupplier(42, lazyCounter);
+
+        final Set<String> toFind = new HashSet<>();
+        toFind.add(TEST_STRING);
+        toFind.add("X1");
+        toFind.add("X2");
+
+        assertEquals(3, toFind.size());
+        map.entrySet().forEach(e -> toFind.remove(e.getValue()));
+        assertEquals(0, toFind.size());
+        assertEquals(2, map.getSuppliersCallCount());
+    }
+
+    @Test
+    public void values() {
+        final LazyLoadingMap<Integer, String> map = new LazyLoadingMap<>();
+        map.put(112, TEST_STRING);
+        map.withSupplier(21, lazyCounter);
+        map.withSupplier(42, lazyCounter);
+
+        final Set<String> toFind = new HashSet<>();
+        toFind.add(TEST_STRING);
+        toFind.add("X1");
+        toFind.add("X2");
+
+        assertEquals(3, toFind.size());
+        assertEquals(0, map.getSuppliersCallCount());
+        map.values().forEach(v -> toFind.remove(v));
+        assertEquals(0, toFind.size());
+        assertEquals(2, map.getSuppliersCallCount());
+    }
+
+    @Test
+    public void isEmpty() {
+        final LazyLoadingMap<Integer, String> map = new LazyLoadingMap<>();
+        assertTrue(map.isEmpty());
+
+        map.put(112, TEST_STRING);
+        assertFalse(map.isEmpty());
+        map.withSupplier(42, lazyCounter);
+        assertFalse(map.isEmpty());
+
+        map.remove(112);
+        assertFalse(map.isEmpty());
+        map.remove(42);
+        assertTrue(map.isEmpty());
+
+        assertEquals(1, map.getSuppliersCallCount());
+    }
+
+    @Test
+    public void nullSupplier() {
+        final LazyLoadingMap<Integer, String> map = new LazyLoadingMap<>();
+        final Supplier<String> nullSup = () -> null;
+
+        map.withSupplier(42, nullSup);
+        assertEquals(0, map.getSuppliersCallCount());
+        assertEquals(1, map.size());
+
+        assertNull(map.get(42));
+        assertEquals(1, map.getSuppliersCallCount());
+        assertEquals(1, map.size());
+
+        assertNull(map.get(42));
+        assertEquals(1, map.getSuppliersCallCount());
+        assertEquals(1, map.size());
+    }
+
+    @Test
+    public void clear() {
+        final LazyLoadingMap<Integer, String> map = new LazyLoadingMap<>();
+        map.withSupplier(21, lazyCounter);
+        map.withSupplier(42, lazyCounter);
+        assertEquals("X1", map.get(42));
+        assertEquals(1, map.getSuppliersCallCount());
+        assertEquals(2, map.size());
+        map.clear();
+        assertEquals(0, map.size());
+        assertNull(map.get(42));
+        assertEquals(1, map.getSuppliersCallCount());
+    }
+
+    @Test
+    public void testHashCode() {
+        final LazyLoadingMap<Integer, String> map = new LazyLoadingMap<>();
+        int hc = map.hashCode();
+        map.put(21, TEST_STRING);
+        assertNotEquals(hc, hashCode());
+        hc = map.hashCode();
+        map.withSupplier(42, lazyCounter);
+        assertNotEquals(hc, hashCode());
+    }
+
+    @Test
+    public void testEquals() {
+        final Supplier<String> constant = () -> "Some String";
+        final LazyLoadingMap<Integer, String> A = new LazyLoadingMap<>();
+        final LazyLoadingMap<Integer, String> B = new LazyLoadingMap<>();
+        assertEquals(A, B);
+
+        A.withSupplier(42, constant);
+        A.put(21, TEST_STRING);
+        assertNotEquals(A, B);
+        assertEquals(1, A.getSuppliersCallCount());
+        assertEquals(0, B.getSuppliersCallCount());
+
+        B.withSupplier(42, constant);
+        assertNotEquals(A, B);
+        B.put(21, TEST_STRING);
+        assertEquals(B, A);
+        assertEquals(1, A.getSuppliersCallCount());
+        assertEquals(1, B.getSuppliersCallCount());
+    }
+
+    @Test
+    public void nullKey() {
+        final LazyLoadingMap<Integer, String> map = new LazyLoadingMap<>();
+        assertNull(map.get(null));
+    }
+
+    @Test
+    public void applesAndOranges() {
+        final LazyLoadingMap<Integer, String> map = new LazyLoadingMap<>();
+        assertNotEquals(map, "A string");
+    }
+}