Introduce SharedSingletonMap 45/26545/8
authorRobert Varga <rovarga@cisco.com>
Fri, 4 Sep 2015 14:49:58 +0000 (16:49 +0200)
committerGerrit Code Review <gerrit@opendaylight.org>
Thu, 10 Sep 2015 07:13:02 +0000 (07:13 +0000)
ImmutableMap can be very expensive in terms of memory overhead when
storing a large number of singleton maps which have the same keySet(),
because it instantiates an ImmutableSet, which remains cached. That ends
up costing 24 bytes in local testing.

This patch implements a replacement class which retains the same
properties, but shares the keySet instances -- thus lowering the overall
overhead. For these instances we introduce an SingletonSet class, which
is memory-efficient and gives us direct access to the container entry.

This leaves entrySet() as the only method which allocates objects. We do
not cache returned objects in the expectation any caller users them as
pure DTOs, hence they end up being cheap.

Change-Id: I64e74d8e661d2689d32bea31e1cdf72e87ed64af
Signed-off-by: Robert Varga <rovarga@cisco.com>
common/util/src/main/java/org/opendaylight/yangtools/util/ImmutableOffsetMap.java
common/util/src/main/java/org/opendaylight/yangtools/util/MutableOffsetMap.java
common/util/src/main/java/org/opendaylight/yangtools/util/SharedSingletonMap.java [new file with mode: 0644]
common/util/src/main/java/org/opendaylight/yangtools/util/SingletonSet.java [new file with mode: 0644]
common/util/src/test/java/org/opendaylight/yangtools/util/OffsetMapTest.java
common/util/src/test/java/org/opendaylight/yangtools/util/SharedSingletonMapTest.java [new file with mode: 0644]
common/util/src/test/java/org/opendaylight/yangtools/util/SingletonSetTest.java [new file with mode: 0644]
yang/yang-data-api/src/main/java/org/opendaylight/yangtools/yang/data/api/YangInstanceIdentifier.java

index b9c374fbdf67592c7044e5a3a2cf621839c5c7eb..bbab15649c0a45b21ef3073dd3b1c874551ec2de 100644 (file)
@@ -80,23 +80,25 @@ public class ImmutableOffsetMap<K, V> extends AbstractLazyValueMap<K, V> impleme
      * @return An isolated, immutable copy of the input map
      */
     @Nonnull public static <K, V> Map<K, V> copyOf(@Nonnull final Map<K, V> m) {
-        // Prevent a copy
-        if (m instanceof ImmutableOffsetMap) {
+        // Prevent a copy. Note that ImmutableMap is not listed here because of its potentially larger keySet overhead.
+        if (m instanceof ImmutableOffsetMap || m instanceof SharedSingletonMap) {
             return m;
         }
 
-        // Better-packed
+        // Familiar and efficient to copy
+        if (m instanceof MutableOffsetMap) {
+            return ((MutableOffsetMap<K, V>) m).toUnmodifiableMap();
+        }
+
         final int size = m.size();
         if (size == 0) {
+            // Shares a single object
             return ImmutableMap.of();
         }
         if (size == 1) {
-            return ImmutableMap.copyOf(m);
-        }
-
-        // Familiar and efficient
-        if (m instanceof MutableOffsetMap) {
-            return ((MutableOffsetMap<K, V>) m).toUnmodifiableMap();
+            // Efficient single-entry implementation
+            final Entry<K, V> e = m.entrySet().iterator().next();
+            return SharedSingletonMap.of(e.getKey(), e.getValue());
         }
 
         final Map<K, Integer> offsets = OffsetMapCache.offsetsFor(m.keySet());
index 67eb8daddee28a163ad718387cb4691d9bb8473f..4be9ed99cc41f849d8f8e83515bb094c7d763f50 100644 (file)
@@ -51,7 +51,7 @@ public class MutableOffsetMap<K, V> extends AbstractLazyValueMap<K, V> implement
         this(Collections.<K>emptySet());
     }
 
-    public MutableOffsetMap(final Collection<K> keySet) {
+    protected MutableOffsetMap(final Collection<K> keySet) {
         if (!keySet.isEmpty()) {
             removed = keySet.size();
             offsets = OffsetMapCache.offsetsFor(keySet);
@@ -66,9 +66,11 @@ public class MutableOffsetMap<K, V> extends AbstractLazyValueMap<K, V> implement
     }
 
     protected MutableOffsetMap(final ImmutableOffsetMap<K, V> m) {
-        this.offsets = m.offsets();
-        this.objects = m.objects();
-        this.newKeys = new LinkedHashMap<>();
+        this(m.offsets(), m.objects());
+    }
+
+    protected MutableOffsetMap(final Map<K, V> m) {
+        this(OffsetMapCache.offsetsFor(m.keySet()), m.values().toArray());
     }
 
     protected MutableOffsetMap(final MutableOffsetMap<K, V> m) {
@@ -78,6 +80,34 @@ public class MutableOffsetMap<K, V> extends AbstractLazyValueMap<K, V> implement
         this.removed = m.removed;
     }
 
+    private MutableOffsetMap(final Map<K, Integer> offsets, final Object[] objects) {
+        this.offsets = Preconditions.checkNotNull(offsets);
+        this.objects = Preconditions.checkNotNull(objects);
+        this.newKeys = new LinkedHashMap<>();
+    }
+
+    public static <K, V> MutableOffsetMap<K, V> copyOf(final Map<K, V> m) {
+        if (m instanceof MutableOffsetMap) {
+            return ((MutableOffsetMap<K, V>) m).clone();
+        }
+        if (m instanceof ImmutableOffsetMap) {
+            return ((ImmutableOffsetMap<K, V>) m).toModifiableMap();
+        }
+
+        return new MutableOffsetMap<>(m);
+    }
+
+    public static <K, V> MutableOffsetMap<K, V> forOffsets(final Map<K, Integer> offsets) {
+        final Object[] objects = new Object[offsets.size()];
+        Arrays.fill(objects, NO_VALUE);
+
+        return new MutableOffsetMap<>(offsets, objects);
+    }
+
+    public static <K, V> MutableOffsetMap<K, V> forKeySet(final Collection<K> keySet) {
+        return forOffsets(OffsetMapCache.offsetsFor(keySet));
+    }
+
     @Override
     public final int size() {
         return offsets.size() + newKeys.size() - removed;
@@ -249,7 +279,7 @@ public class MutableOffsetMap<K, V> extends AbstractLazyValueMap<K, V> implement
     }
 
     @Override
-    public MutableOffsetMap<K, V> clone() throws CloneNotSupportedException {
+    public MutableOffsetMap<K, V> clone() {
         return new MutableOffsetMap<K, V>(this);
     }
 
diff --git a/common/util/src/main/java/org/opendaylight/yangtools/util/SharedSingletonMap.java b/common/util/src/main/java/org/opendaylight/yangtools/util/SharedSingletonMap.java
new file mode 100644 (file)
index 0000000..17ff0d9
--- /dev/null
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.util;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Preconditions;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import java.io.Serializable;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.Map;
+
+/**
+ * Implementation of the {@link Map} interface which stores a single mapping. The key set is shared among all instances
+ * which contain the same key. This implementation does not support null keys or values.
+ *
+ * @param <K> the type of keys maintained by this map
+ * @param <V> the type of mapped values
+ */
+@Beta
+public final class SharedSingletonMap<K, V> implements Serializable, UnmodifiableMapPhase<K, V> {
+    private static final long serialVersionUID = 1L;
+    private static final LoadingCache<Object, SingletonSet<Object>> CACHE = CacheBuilder.newBuilder().weakValues()
+            .build(new CacheLoader<Object, SingletonSet<Object>>() {
+                @Override
+                public SingletonSet<Object> load(final Object key) {
+                    return SingletonSet.of(key);
+                }
+            });
+    private final SingletonSet<K> keySet;
+    private final V value;
+    private int hashCode;
+
+    @SuppressWarnings("unchecked")
+    private SharedSingletonMap(final K key, final V value) {
+        this.keySet = (SingletonSet<K>) CACHE.getUnchecked(key);
+        this.value = Preconditions.checkNotNull(value);
+    }
+
+    public static <K, V> SharedSingletonMap<K, V> of(final K key, final V value) {
+        return new SharedSingletonMap<>(key, value);
+    }
+
+    public static <K, V> SharedSingletonMap<K, V> copyOf(final Map<K, V> m) {
+        Preconditions.checkArgument(m.size() == 1);
+
+        final Entry<K, V> e = m.entrySet().iterator().next();
+        return new SharedSingletonMap<>(e.getKey(), e.getValue());
+    }
+
+    @Override
+    public ModifiableMapPhase<K, V> toModifiableMap() {
+        return new MutableOffsetMap<K, V>(this);
+    }
+
+    @Override
+    public SingletonSet<Entry<K, V>> entrySet() {
+        return SingletonSet.<Entry<K, V>>of(new SimpleImmutableEntry<>(keySet.getElement(), value));
+    }
+
+    @Override
+    public SingletonSet<K> keySet() {
+        return keySet;
+    }
+
+    @Override
+    public SingletonSet<V> values() {
+        return SingletonSet.of(value);
+    }
+
+    @Override
+    public boolean containsKey(final Object key) {
+        return keySet.contains(key);
+    }
+
+    @Override
+    public boolean containsValue(final Object value) {
+        return this.value.equals(value);
+    }
+
+    @Override
+    public V get(final Object key) {
+        return keySet.contains(key) ? value : null;
+    }
+
+    @Override
+    public int size() {
+        return 1;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return false;
+    }
+
+    @Override
+    public V put(final K key, final V value) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public V remove(final Object key) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void putAll(final Map<? extends K, ? extends V> m) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void clear() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int hashCode() {
+        if (hashCode == 0) {
+            hashCode = keySet.getElement().hashCode() ^ value.hashCode();
+        }
+        return hashCode;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof Map)) {
+            return false;
+        }
+
+        final Map<?, ?> m = (Map<?, ?>)obj;
+        return m.size() == 1 && value.equals(m.get(keySet.getElement()));
+    }
+
+    @Override
+    public String toString() {
+        return "{" + keySet.getElement() + '=' + value + '}';
+    }
+}
diff --git a/common/util/src/main/java/org/opendaylight/yangtools/util/SingletonSet.java b/common/util/src/main/java/org/opendaylight/yangtools/util/SingletonSet.java
new file mode 100644 (file)
index 0000000..9587d11
--- /dev/null
@@ -0,0 +1,191 @@
+/*
+ * Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.util;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterators;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Set;
+import javax.annotation.Nonnull;
+import org.opendaylight.yangtools.concepts.Immutable;
+
+/**
+ * A {@link Set} containing a single value. For some reason neither Java nor Guava provide direct access to the retained
+ * element -- which is desirable in some situations, as is the case in {@link SharedSingletonMap#entrySet()}.
+ */
+@Beta
+public abstract class SingletonSet<E> implements Set<E>, Immutable, Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private static final SingletonSet<?> NULL_SINGLETON = new SingletonSet<Object>() {
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        public boolean contains(final Object o) {
+            return o == null;
+        }
+
+        @Override
+        public int hashCode() {
+            return 0;
+        }
+
+        @Override
+        public Object getElement() {
+            return null;
+        }
+
+        @Override
+        public String toString() {
+            return "[null]";
+        }
+
+        private Object readResolve() {
+            return NULL_SINGLETON;
+        }
+    };
+
+    @SuppressWarnings("unchecked")
+    public static <E> SingletonSet<E> of(@Nonnull final E element) {
+        if (element == null) {
+            return (SingletonSet<E>) NULL_SINGLETON;
+        }
+        return new RegularSingletonSet<E>(element);
+    }
+
+    public abstract E getElement();
+
+    @Override
+    public final int size() {
+        return 1;
+    }
+
+    @Override
+    public final boolean isEmpty() {
+        return false;
+    }
+
+    @Override
+    public final Iterator<E> iterator() {
+        return Iterators.singletonIterator(getElement());
+    }
+
+    @Override
+    public final Object[] toArray() {
+        return new Object[] { getElement() };
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public final <T> T[] toArray(final T[] a) {
+        if (a.length > 0) {
+            a[0] = (T)getElement();
+            return a;
+        }
+
+        return (T[]) new Object[] { (T) getElement() };
+    }
+
+    @Override
+    public final boolean add(final E e) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public final boolean remove(final Object o) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public final boolean containsAll(final Collection<?> c) {
+        if (c.isEmpty()) {
+            return true;
+        }
+        if (c.size() != 1) {
+            return false;
+        }
+
+        return otherContains(c);
+    }
+
+    @Override
+    public final boolean addAll(final Collection<? extends E> c) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public final boolean retainAll(final Collection<?> c) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public final boolean removeAll(final Collection<?> c) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public final void clear() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public abstract int hashCode();
+
+    @Override
+    public final boolean equals(final Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        if (!(obj instanceof Set)) {
+            return false;
+        }
+
+        final Set<?> s = (Set<?>)obj;
+        return s.size() == 1 && otherContains(s);
+    }
+
+    private boolean otherContains(final Collection<?> other) {
+        try {
+            return other.contains(getElement());
+        } catch (ClassCastException | NullPointerException e) {
+            return false;
+        }
+    }
+
+    private static final class RegularSingletonSet<E> extends SingletonSet<E> {
+        private static final long serialVersionUID = 1L;
+        private final E element;
+
+        RegularSingletonSet(final E element) {
+            this.element = Preconditions.checkNotNull(element);
+        }
+
+        @Override
+        public boolean contains(final Object o) {
+            return element.equals(o);
+        }
+
+        @Override
+        public E getElement() {
+            return element;
+        }
+
+        @Override
+        public int hashCode() {
+            return getElement().hashCode();
+        }
+
+        @Override
+        public String toString() {
+            return "[" + element + ']';
+        }
+    }
+}
index 9022228ad4956a446306270dcfbf0c0cab619bc9..5aef8d12c30f1b6f7495e346784d8f9425b34b44 100644 (file)
@@ -60,7 +60,7 @@ public class OffsetMapTest {
         final Map<String, String> result = ImmutableOffsetMap.copyOf(source);
 
         assertEquals(source, result);
-        assertTrue(result instanceof ImmutableMap);
+        assertTrue(result instanceof SharedSingletonMap);
     }
 
     @Test
diff --git a/common/util/src/test/java/org/opendaylight/yangtools/util/SharedSingletonMapTest.java b/common/util/src/test/java/org/opendaylight/yangtools/util/SharedSingletonMapTest.java
new file mode 100644 (file)
index 0000000..3e0bb75
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import org.junit.Test;
+
+public class SharedSingletonMapTest {
+    private static UnmodifiableMapPhase<String, String> create() {
+        return SharedSingletonMap.of("k1", "v1");
+    }
+
+    @Test
+    public void testSimpleOperations() {
+        final Map<String, String> m = create();
+
+        assertFalse(m.isEmpty());
+        assertEquals(1, m.size());
+
+        assertTrue(m.containsKey("k1"));
+        assertFalse(m.containsKey(null));
+        assertFalse(m.containsKey("v1"));
+
+        assertTrue(m.containsValue("v1"));
+        assertFalse(m.containsValue(null));
+        assertFalse(m.containsValue("k1"));
+
+        assertEquals("v1", m.get("k1"));
+        assertNull(m.get(null));
+        assertNull(m.get("v1"));
+
+        assertFalse(m.equals(null));
+        assertTrue(m.equals(m));
+        assertFalse(m.equals(""));
+
+        final Map<String, String> same = Collections.singletonMap("k1", "v1");
+        assertEquals(same.toString(), m.toString());
+        assertTrue(same.equals(m));
+        assertTrue(m.equals(same));
+        assertEquals(same.entrySet(), m.entrySet());
+        assertEquals(same.values(), m.values());
+
+        // Perform twice to exercise the cache
+        assertEquals(same.hashCode(), m.hashCode());
+        assertEquals(same.hashCode(), m.hashCode());
+
+        assertFalse(m.equals(Collections.singletonMap(null, null)));
+        assertFalse(m.equals(Collections.singletonMap("k1", null)));
+        assertFalse(m.equals(Collections.singletonMap(null, "v1")));
+        assertFalse(m.equals(Collections.singletonMap("k1", "v2")));
+
+        final Set<String> set = m.keySet();
+        assertTrue(set instanceof SingletonSet);
+        assertTrue(set.contains("k1"));
+    }
+
+    @Test(expected=UnsupportedOperationException.class)
+    public void testClear() {
+        create().clear();
+    }
+
+    @Test(expected=UnsupportedOperationException.class)
+    public void testPut() {
+        create().put(null, null);
+    }
+
+    @Test(expected=UnsupportedOperationException.class)
+    public void testPutAll() {
+        create().putAll(Collections.singletonMap("", ""));
+    }
+
+    @Test(expected=UnsupportedOperationException.class)
+    public void testRemove() {
+        create().remove(null);
+    }
+}
diff --git a/common/util/src/test/java/org/opendaylight/yangtools/util/SingletonSetTest.java b/common/util/src/test/java/org/opendaylight/yangtools/util/SingletonSetTest.java
new file mode 100644 (file)
index 0000000..9cd7f35
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import java.util.Collections;
+import java.util.Iterator;
+import org.junit.Test;
+
+public class SingletonSetTest {
+    private static final String ELEMENT = "element";
+
+    private static SingletonSet<?> nullSet() {
+        return SingletonSet.of(null);
+    }
+
+    @Test
+    public void testNullSingleton() {
+        final SingletonSet<?> s = nullSet();
+
+        assertFalse(s.isEmpty());
+        assertEquals(1, s.size());
+        assertFalse(s.contains(""));
+        assertTrue(s.contains(null));
+        assertNull(s.getElement());
+        assertEquals(0, s.hashCode());
+        assertTrue(s.equals(Collections.singleton(null)));
+        assertFalse(s.equals(Collections.singleton("")));
+        assertFalse(s.equals(""));
+        assertTrue(s.equals(s));
+        assertFalse(s.equals(null));
+        assertEquals(Collections.singleton(null).toString(), s.toString());
+    }
+
+    @Test
+    public void testRegularSingleton() {
+        final SingletonSet<?> s = SingletonSet.of(ELEMENT);
+
+        assertFalse(s.isEmpty());
+        assertEquals(1, s.size());
+        assertFalse(s.contains(""));
+        assertFalse(s.contains(null));
+        assertTrue(s.contains(ELEMENT));
+
+        assertSame(ELEMENT, s.getElement());
+        assertEquals(ELEMENT.hashCode(), s.hashCode());
+        assertTrue(s.equals(Collections.singleton(ELEMENT)));
+        assertFalse(s.equals(Collections.singleton("")));
+        assertFalse(s.equals(Collections.singleton(null)));
+        assertFalse(s.equals(""));
+        assertTrue(s.equals(s));
+        assertFalse(s.equals(null));
+        assertEquals(Collections.singleton(ELEMENT).toString(), s.toString());
+    }
+
+    @Test
+    public void testIterator() {
+        final SingletonSet<?> s = SingletonSet.of(ELEMENT);
+        final Iterator<?> it = s.iterator();
+
+        assertTrue(it.hasNext());
+        assertSame(ELEMENT, it.next());
+        assertFalse(it.hasNext());
+    }
+
+    @Test(expected=UnsupportedOperationException.class)
+    public void testRejectedAdd() {
+        final SingletonSet<?> s = nullSet();
+        s.add(null);
+    }
+
+    @Test(expected=UnsupportedOperationException.class)
+    public void testRejectedAddAll() {
+        final SingletonSet<?> s = nullSet();
+        s.addAll(null);
+    }
+
+    @Test(expected=UnsupportedOperationException.class)
+    public void testRejectedClear() {
+        final SingletonSet<?> s = nullSet();
+        s.clear();
+    }
+
+    @Test(expected=UnsupportedOperationException.class)
+    public void testRejectedRemove() {
+        final SingletonSet<?> s = nullSet();
+        s.remove(null);
+    }
+
+    @Test(expected=UnsupportedOperationException.class)
+    public void testRejectedRemoveAll() {
+        final SingletonSet<?> s = nullSet();
+        s.removeAll(null);
+    }
+
+    @Test(expected=UnsupportedOperationException.class)
+    public void testRejectedRetainAll() {
+        final SingletonSet<?> s = nullSet();
+        s.retainAll(null);
+    }
+}
index 81f474db58c1b0ded8d3383b1cc34effbf50c6d9..849f7f08432c6772d5f361419f0e141379f1c52a 100644 (file)
@@ -13,7 +13,6 @@ import com.google.common.base.Preconditions;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import java.io.Serializable;
@@ -34,6 +33,7 @@ import org.opendaylight.yangtools.concepts.Immutable;
 import org.opendaylight.yangtools.concepts.Path;
 import org.opendaylight.yangtools.util.HashCodeBuilder;
 import org.opendaylight.yangtools.util.ImmutableOffsetMap;
+import org.opendaylight.yangtools.util.SharedSingletonMap;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.common.QNameModule;
 import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode;
@@ -497,12 +497,12 @@ public abstract class YangInstanceIdentifier implements Path<YangInstanceIdentif
 
         public NodeIdentifierWithPredicates(final QName node, final Map<QName, Object> keyValues) {
             super(node);
-            // Retains ImmutableMap for maps with size() <= 1. For larger sizes uses a shared key set.
+            // Retains ImmutableMap for empty maps. For larger sizes uses a shared key set.
             this.keyValues = ImmutableOffsetMap.copyOf(keyValues);
         }
 
         public NodeIdentifierWithPredicates(final QName node, final QName key, final Object value) {
-            this(node, ImmutableMap.of(key, value));
+            this(node, SharedSingletonMap.of(key, value));
         }
 
         public Map<QName, Object> getKeyValues() {