test/java/util/Map/InPlaceOpsCollisions.java

Print this page

        

@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.

@@ -21,21 +21,20 @@
  * questions.
  */
 
 /*
  * @test
- * @bug 7126277
+ * @bug 8005698
  * @run main Collisions -shortrun
- * @run main/othervm -Djdk.map.althashing.threshold=0 Collisions -shortrun
- * @summary Ensure Maps behave well with lots of hashCode() collisions.
- * @author Mike Duigou
+ * @run main/othervm -Djdk.map.randomseed=true Collisions -shortrun
+ * @summary Ensure overrides of in-place operations in Maps behave well with lots of collisions.
+ * @author Brent Christian
  */
 import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentSkipListMap;
+import java.util.function.*;
 
-public class Collisions {
+public class InPlaceOpsCollisions {
 
     /**
      * Number of elements per map.
      */
     private static final int TEST_SIZE = 5000;

@@ -75,10 +74,13 @@
         public String toString() {
             return Integer.toString(value);
         }
     }
 
+    static HashableInteger EXTRA_INT_VAL;
+    static String EXTRA_STRING_VAL;
+    
     private static Object[][] makeTestData(int size) {
         HashableInteger UNIQUE_OBJECTS[] = new HashableInteger[size];
         HashableInteger COLLIDING_OBJECTS[] = new HashableInteger[size];
         String UNIQUE_STRINGS[] = new String[size];
         String COLLIDING_STRINGS[] = new String[size];

@@ -89,10 +91,12 @@
             UNIQUE_STRINGS[i] = unhash(i);
             COLLIDING_STRINGS[i] = (0 == i % 2)
                     ? UNIQUE_STRINGS[i / 2]
                     : "\u0000\u0000\u0000\u0000\u0000" + COLLIDING_STRINGS[i - 1];
         }
+        EXTRA_INT_VAL = new HashableInteger(size, Integer.MAX_VALUE);
+        EXTRA_STRING_VAL = new String ("Extra Value");
 
      return new Object[][] {
             new Object[]{"Unique Objects", UNIQUE_OBJECTS},
             new Object[]{"Colliding Objects", COLLIDING_OBJECTS},
             new Object[]{"Unique Strings", UNIQUE_STRINGS},

@@ -144,209 +148,445 @@
 
         // loop through data sets
         for (Object[] keys_desc : mapKeys) {
             Map<Object, Object>[] maps = (Map<Object, Object>[]) new Map[]{
                         new HashMap<>(),
-                        new Hashtable<>(),
-                        new IdentityHashMap<>(),
                         new LinkedHashMap<>(),
-                        new TreeMap<>(),
-                        new WeakHashMap<>(),
-                        new ConcurrentHashMap<>(),
-                        new ConcurrentSkipListMap<>()
                     };
 
             // for each map type.
             for (Map<Object, Object> map : maps) {
                 String desc = (String) keys_desc[0];
                 Object[] keys = (Object[]) keys_desc[1];
                 try {
-                    testMap(map, desc, keys);
+                    testInPlaceOps(map, desc, keys);
                 } catch(Exception all) {
                     unexpected("Failed for " + map.getClass().getName() + " with " + desc, all);
                 }
             }
         }
     }
 
-    private static <T> void testMap(Map<T, T> map, String keys_desc, T[] keys) {
-        System.out.println(map.getClass() + " : " + keys_desc);
-        System.out.flush();
-        testInsertion(map, keys_desc, keys);
+    private static <T> void testInsertion(Map<T, T> map, String keys_desc, T[] keys) {
+        check("map empty", (map.size() == 0) && map.isEmpty());
 
-        if (keys[0] instanceof HashableInteger) {
-            testIntegerIteration((Map<HashableInteger, HashableInteger>) map, (HashableInteger[]) keys);
-        } else {
-            testStringIteration((Map<String, String>) map, (String[]) keys);
+        for (int i = 0; i < keys.length; i++) {
+            check(String.format("insertion: map expected size m%d != i%d", map.size(), i),
+                    map.size() == i);
+            check(String.format("insertion: put(%s[%d])", keys_desc, i), null == map.put(keys[i], keys[i]));
+            check(String.format("insertion: containsKey(%s[%d])", keys_desc, i), map.containsKey(keys[i]));
+            check(String.format("insertion: containsValue(%s[%d])", keys_desc, i), map.containsValue(keys[i]));
+        }
+
+        check(String.format("map expected size m%d != k%d", map.size(), keys.length),
+                map.size() == keys.length);
         }
 
-        testContainsKey(map, keys_desc, keys);
 
-        testRemove(map, keys_desc, keys);
+    private static <T> void testInPlaceOps(Map<T, T> map, String keys_desc, T[] keys) {
+        System.out.println(map.getClass() + " : " + keys_desc + ", testInPlaceOps");
+        System.out.flush();
+
+        testInsertion(map, keys_desc, keys);
+        testPutIfAbsent(map, keys_desc, keys);
 
         map.clear();
         testInsertion(map, keys_desc, keys);
-        testKeysIteratorRemove(map, keys_desc, keys);
+        testRemoveMapping(map, keys_desc, keys);
 
         map.clear();
         testInsertion(map, keys_desc, keys);
-        testValuesIteratorRemove(map, keys_desc, keys);
+        testReplaceOldValue(map, keys_desc, keys);
 
         map.clear();
         testInsertion(map, keys_desc, keys);
-        testEntriesIteratorRemove(map, keys_desc, keys);
+        testReplaceIfMapped(map, keys_desc, keys);    
 
-        check(map.isEmpty());
-    }
+        map.clear();
+        testInsertion(map, keys_desc, keys);
+        testComputeIfAbsent(map, keys_desc, keys, (k) -> getExtraVal(keys[0]));
 
-    private static <T> void testInsertion(Map<T, T> map, String keys_desc, T[] keys) {
-        check("map empty", (map.size() == 0) && map.isEmpty());
+        map.clear();
+        testInsertion(map, keys_desc, keys);
+        testComputeIfAbsent(map, keys_desc, keys, (k) -> null);
 
-        for (int i = 0; i < keys.length; i++) {
-            check(String.format("insertion: map expected size m%d != i%d", map.size(), i),
-                    map.size() == i);
-            check(String.format("insertion: put(%s[%d])", keys_desc, i), null == map.put(keys[i], keys[i]));
-            check(String.format("insertion: containsKey(%s[%d])", keys_desc, i), map.containsKey(keys[i]));
-            check(String.format("insertion: containsValue(%s[%d])", keys_desc, i), map.containsValue(keys[i]));
-        }
+        map.clear();
+        testInsertion(map, keys_desc, keys);
+        testComputeIfPresent(map, keys_desc, keys, (k, v) -> getExtraVal(keys[0])); 
 
-        check(String.format("map expected size m%d != k%d", map.size(), keys.length),
-                map.size() == keys.length);
+        map.clear();
+        testInsertion(map, keys_desc, keys);
+        testComputeIfPresent(map, keys_desc, keys, (k, v) -> null); 
+
+        if (!keys_desc.contains("Strings")) { // avoid parseInt() number format error
+            map.clear();
+            testInsertion(map, keys_desc, keys);
+            testComputeNonNull(map, keys_desc, keys);
     }
 
-    private static void testIntegerIteration(Map<HashableInteger, HashableInteger> map, HashableInteger[] keys) {
-        check(String.format("map expected size m%d != k%d", map.size(), keys.length),
-                map.size() == keys.length);
+        map.clear();
+        testInsertion(map, keys_desc, keys);
+        testComputeNull(map, keys_desc, keys);
 
-        BitSet all = new BitSet(keys.length);
-        for (Map.Entry<HashableInteger, HashableInteger> each : map.entrySet()) {
-            check("Iteration: key already seen", !all.get(each.getKey().value));
-            all.set(each.getKey().value);
+        if (!keys_desc.contains("Strings")) { // avoid parseInt() number format error
+            map.clear();
+            testInsertion(map, keys_desc, keys);
+            testMergeNonNull(map, keys_desc, keys);
         }
 
-        all.flip(0, keys.length);
-        check("Iteration: some keys not visited", all.isEmpty());
-
-        for (HashableInteger each : map.keySet()) {
-            check("Iteration: key already seen", !all.get(each.value));
-            all.set(each.value);
+        map.clear();
+        testInsertion(map, keys_desc, keys);
+        testMergeNull(map, keys_desc, keys);
         }
 
-        all.flip(0, keys.length);
-        check("Iteration: some keys not visited", all.isEmpty());
 
-        int count = 0;
-        for (HashableInteger each : map.values()) {
-            count++;
-        }
 
-        check(String.format("Iteration: value count matches size m%d != c%d", map.size(), count),
-                map.size() == count);
+    private static <T> void testPutIfAbsent(Map<T, T> map, String keys_desc, T[] keys) {
+        T extraVal = getExtraVal(keys[0]);
+        T retVal;
+        removeOddKeys(map, keys);
+        for (int i = 0; i < keys.length; i++) {
+            retVal = map.putIfAbsent(keys[i], extraVal);
+            if (i % 2 == 0) { // even: not absent, not put
+                check(String.format("putIfAbsent: (%s[%d]) retVal", keys_desc, i), retVal == keys[i]);
+                check(String.format("putIfAbsent: get(%s[%d])", keys_desc, i), keys[i] == map.get(keys[i]));
+                check(String.format("putIfAbsent: containsValue(%s[%d])", keys_desc, i), map.containsValue(keys[i]));
+            } else { // odd: absent, was put
+                check(String.format("putIfAbsent: (%s[%d]) retVal", keys_desc, i), retVal == null);                
+                check(String.format("putIfAbsent: get(%s[%d])", keys_desc, i), extraVal == map.get(keys[i]));                
+                check(String.format("putIfAbsent: !containsValue(%s[%d])", keys_desc, i), !map.containsValue(keys[i]));
+            }
+            check(String.format("insertion: containsKey(%s[%d])", keys_desc, i), map.containsKey(keys[i]));            
     }
-
-    private static void testStringIteration(Map<String, String> map, String[] keys) {
         check(String.format("map expected size m%d != k%d", map.size(), keys.length),
                 map.size() == keys.length);
-
-        BitSet all = new BitSet(keys.length);
-        for (Map.Entry<String, String> each : map.entrySet()) {
-            String key = each.getKey();
-            boolean longKey = key.length() > 5;
-            int index = key.hashCode() + (longKey ? keys.length / 2 : 0);
-            check("key already seen", !all.get(index));
-            all.set(index);
         }
 
-        all.flip(0, keys.length);
-        check("some keys not visited", all.isEmpty());
+    private static <T> void testRemoveMapping(Map<T, T> map, String keys_desc, T[] keys) {
+        T extraVal = getExtraVal(keys[0]);
+        boolean removed;
+        int removes = 0;
+        remapOddKeys(map, keys);
+        for (int i = 0; i < keys.length; i++) {
+            removed = map.remove(keys[i], keys[i]);
+            if (i % 2 == 0) { // even: original mapping, should be removed
+                check(String.format("removeMapping: retVal(%s[%d])", keys_desc, i), removed);
+                check(String.format("removeMapping: get(%s[%d])", keys_desc, i), null == map.get(keys[i]));
+                check(String.format("removeMapping: !containsKey(%s[%d])", keys_desc, i), !map.containsKey(keys[i]));
+                check(String.format("removeMapping: !containsValue(%s[%d])", keys_desc, i), !map.containsValue(keys[i]));
+                removes++;
+            } else { // odd: new mapping, not removed
+                check(String.format("removeMapping: retVal(%s[%d])", keys_desc, i), !removed);
+                check(String.format("removeMapping: get(%s[%d])", keys_desc, i), extraVal == map.get(keys[i]));
+                check(String.format("removeMapping: containsKey(%s[%d])", keys_desc, i), map.containsKey(keys[i]));
+                check(String.format("removeMapping: containsValue(%s[%d])", keys_desc, i), map.containsValue(extraVal));
+            }
+        }
+        check(String.format("map expected size m%d != k%d", map.size(), keys.length - removes),
+                map.size() == keys.length - removes); 
+    }
+    
+    private static <T> void testReplaceOldValue(Map<T, T> map, String keys_desc, T[] keys) {
+        // remap odds to extraVal
+        // call replace to replace for extraVal, for all keys
+        // check that all keys map to value from keys array        
+        T extraVal = getExtraVal(keys[0]);
+        boolean replaced;        
+        remapOddKeys(map, keys);
 
-        for (String each : map.keySet()) {
-            boolean longKey = each.length() > 5;
-            int index = each.hashCode() + (longKey ? keys.length / 2 : 0);
-            check("key already seen", !all.get(index));
-            all.set(index);
+        for (int i = 0; i < keys.length; i++) {
+            replaced = map.replace(keys[i], extraVal, keys[i]);
+            if (i % 2 == 0) { // even: original mapping, should not be replaced
+                check(String.format("replaceOldValue: retVal(%s[%d])", keys_desc, i), !replaced);
+            } else { // odd: new mapping, should be replaced
+                check(String.format("replaceOldValue: get(%s[%d])", keys_desc, i), replaced);
+            }
+            check(String.format("replaceOldValue: get(%s[%d])", keys_desc, i), keys[i] == map.get(keys[i]));
+            check(String.format("replaceOldValue: containsKey(%s[%d])", keys_desc, i), map.containsKey(keys[i]));
+            check(String.format("replaceOldValue: containsValue(%s[%d])", keys_desc, i), map.containsValue(keys[i]));
+//            removes++;            
+        }
+        check(String.format("replaceOldValue: !containsValue(%s[%s])", keys_desc, extraVal.toString()), !map.containsValue(extraVal));        
+        check(String.format("map expected size m%d != k%d", map.size(), keys.length),
+                map.size() == keys.length);                 
         }
 
-        all.flip(0, keys.length);
-        check("some keys not visited", all.isEmpty());
+    // TODO: Test case for key mapped to null value
+    private static <T> void testReplaceIfMapped(Map<T, T> map, String keys_desc, T[] keys) {
+        // remove odd keys
+        // call replace for all keys[]
+        // odd keys should remain absent, even keys should be mapped to EXTRA, no value from keys[] should be in map        
+        T extraVal = getExtraVal(keys[0]);
+        int expectedSize1 = 0;
+        removeOddKeys(map, keys);
+        int expectedSize2 = map.size();
 
-        int count = 0;
-        for (String each : map.values()) {
-            count++;
-        }
+        for (int i = 0; i < keys.length; i++) {
+            T retVal = map.replace(keys[i], extraVal);            
+            if (i % 2 == 0) { // even: still in map, should be replaced
+                check(String.format("replaceIfMapped: retVal(%s[%d])", keys_desc, i), retVal == keys[i]);
+                check(String.format("replaceIfMapped: get(%s[%d])", keys_desc, i), extraVal == map.get(keys[i]));
+                check(String.format("replaceIfMapped: containsKey(%s[%d])", keys_desc, i), map.containsKey(keys[i]));
+                expectedSize1++;
+            } else { // odd: was removed, should not be replaced
+                check(String.format("replaceIfMapped: retVal(%s[%d])", keys_desc, i), retVal == null);
+                check(String.format("replaceIfMapped: get(%s[%d])", keys_desc, i), null == map.get(keys[i]));
+                check(String.format("replaceIfMapped: containsKey(%s[%d])", keys_desc, i), !map.containsKey(keys[i]));
+            }
+            check(String.format("replaceIfMapped: !containsValue(%s[%d])", keys_desc, i), !map.containsValue(keys[i]));
+        }
+        check(String.format("replaceIfMapped: containsValue(%s[%s])", keys_desc, extraVal.toString()), map.containsValue(extraVal));
+        check(String.format("map expected size#1 m%d != k%d", map.size(), expectedSize1),
+                map.size() == expectedSize1);         
+        check(String.format("map expected size#2 m%d != k%d", map.size(), expectedSize2),
+                map.size() == expectedSize2);         
 
-        check(String.format("value count matches size m%d != k%d", map.size(), keys.length),
-                map.size() == keys.length);
     }
 
-    private static <T> void testContainsKey(Map<T, T> map, String keys_desc, T[] keys) {
+    private static <T> void testComputeIfAbsent(Map<T, T> map, String keys_desc, T[] keys,
+                                                Function<T,T> mappingFunction) {
+        // remove a third of the keys
+        // call computeIfAbsent for all keys, func returns EXTRA
+        // check that removed keys now -> EXTRA, other keys -> original val    
+        T expectedVal = mappingFunction.apply(keys[0]);
+        T retVal;
+        int expectedSize = 0;
+        removeThirdKeys(map, keys);
+        for (int i = 0; i < keys.length; i++) {
+            retVal = map.computeIfAbsent(keys[i], mappingFunction);
+            if (i % 3 != 2) { // key present, not computed
+                check(String.format("computeIfAbsent: (%s[%d]) retVal", keys_desc, i), retVal == keys[i]);
+                check(String.format("computeIfAbsent: get(%s[%d])", keys_desc, i), keys[i] == map.get(keys[i]));
+                check(String.format("computeIfAbsent: containsValue(%s[%d])", keys_desc, i), map.containsValue(keys[i]));
+                check(String.format("insertion: containsKey(%s[%d])", keys_desc, i), map.containsKey(keys[i]));            
+                expectedSize++;
+            } else { // key absent, computed unless function return null
+                check(String.format("computeIfAbsent: (%s[%d]) retVal", keys_desc, i), retVal == expectedVal);
+                check(String.format("computeIfAbsent: get(%s[%d])", keys_desc, i), expectedVal == map.get(keys[i]));                
+                check(String.format("computeIfAbsent: !containsValue(%s[%d])", keys_desc, i), !map.containsValue(keys[i]));
+                // mapping should not be added if function returns null
+                check(String.format("insertion: containsKey(%s[%d])", keys_desc, i), map.containsKey(keys[i]) != (expectedVal == null));
+                if (expectedVal != null) { expectedSize++; }
+            }
+        }
+        if (expectedVal != null) {
+            check(String.format("computeIfAbsent: containsValue(%s[%s])", keys_desc, expectedVal), map.containsValue(expectedVal));
+        }
+        check(String.format("map expected size m%d != k%d", map.size(), expectedSize),
+                map.size() == expectedSize);
+    }
+    
+    private static <T> void testComputeIfPresent(Map<T, T> map, String keys_desc, T[] keys,
+                                                BiFunction<T,T,T> mappingFunction) {
+        // remove a third of the keys
+        // call testComputeIfPresent for all keys[]
+        // removed keys should remain absent, even keys should be mapped to $RESULT
+        // no value from keys[] should be in map        
+        T funcResult = mappingFunction.apply(keys[0], keys[0]);
+        int expectedSize1 = 0;
+        removeThirdKeys(map, keys);
+        
         for (int i = 0; i < keys.length; i++) {
-            T each = keys[i];
-            check("containsKey: " + keys_desc + "[" + i + "]" + each, map.containsKey(each));
+            T retVal = map.computeIfPresent(keys[i], mappingFunction);
+            if (i % 3 != 2) { // key present
+                if (funcResult == null) { // was removed
+                    check(String.format("replaceIfMapped: containsKey(%s[%d])", keys_desc, i), !map.containsKey(keys[i]));                    
+                } else { // value was replaced
+                    check(String.format("replaceIfMapped: containsKey(%s[%d])", keys_desc, i), map.containsKey(keys[i]));
+                    expectedSize1++;                    
+                }
+                check(String.format("computeIfPresent: retVal(%s[%s])", keys_desc, i), retVal == funcResult);
+                check(String.format("replaceIfMapped: get(%s[%d])", keys_desc, i), funcResult == map.get(keys[i]));
+                
+            } else { // odd: was removed, should not be replaced
+                check(String.format("replaceIfMapped: retVal(%s[%d])", keys_desc, i), retVal == null);
+                check(String.format("replaceIfMapped: get(%s[%d])", keys_desc, i), null == map.get(keys[i]));
+                check(String.format("replaceIfMapped: containsKey(%s[%d])", keys_desc, i), !map.containsKey(keys[i]));
+            }
+            check(String.format("replaceIfMapped: !containsValue(%s[%d])", keys_desc, i), !map.containsValue(keys[i]));
+        }
+        check(String.format("map expected size#1 m%d != k%d", map.size(), expectedSize1),
+                map.size() == expectedSize1);        
+    }    
+    
+    private static <T> void testComputeNonNull(Map<T, T> map, String keys_desc, T[] keys) {
+        // remove a third of the keys
+        // call compute() for all keys[]
+        // all keys should be present: removed keys -> EXTRA, others to k-1        
+        BiFunction<T,T,T> mappingFunction = (k, v) -> {
+                if (v == null) {
+                    return getExtraVal(keys[0]);
+                } else {
+                    return keys[Integer.parseInt(k.toString()) - 1];
         }
+            };        
+        T extraVal = getExtraVal(keys[0]);
+        removeThirdKeys(map, keys);
+        for (int i = 1; i < keys.length; i++) {
+            T retVal = map.compute(keys[i], mappingFunction);
+            if (i % 3 != 2) { // key present, should be mapped to k-1               
+                check(String.format("compute: retVal(%s[%d])", keys_desc, i), retVal == keys[i-1]);
+                check(String.format("compute: get(%s[%d])", keys_desc, i), keys[i-1] == map.get(keys[i]));
+            } else { // odd: was removed, should be replaced with EXTRA
+                check(String.format("compute: retVal(%s[%d])", keys_desc, i), retVal == extraVal);
+                check(String.format("compute: get(%s[%d])", keys_desc, i), extraVal == map.get(keys[i]));
     }
-
-    private static <T> void testRemove(Map<T, T> map, String keys_desc, T[] keys) {
-        check(String.format("remove: map expected size m%d != k%d", map.size(), keys.length),
+            check(String.format("compute: containsKey(%s[%d])", keys_desc, i), map.containsKey(keys[i]));
+        }
+        check(String.format("map expected size#1 m%d != k%d", map.size(), keys.length),
                 map.size() == keys.length);
+        check(String.format("compute: containsValue(%s[%s])", keys_desc, extraVal.toString()), map.containsValue(extraVal));
+        check(String.format("compute: !containsValue(%s,[null])", keys_desc), !map.containsValue(null));
+    }      
 
+    private static <T> void testComputeNull(Map<T, T> map, String keys_desc, T[] keys) {
+        // remove a third of the keys
+        // call compute() for all keys[]
+        // removed keys should -> EXTRA
+        // for other keys: func returns null, should have no mapping
+        BiFunction<T,T,T> mappingFunction = (k, v) -> {
+            // if absent/null -> EXTRA
+            // if present -> null
+            if (v == null) {
+                return getExtraVal(keys[0]);
+            } else {
+                return null;
+            }
+        };
+        T extraVal = getExtraVal(keys[0]);
+        int expectedSize = 0;
+        removeThirdKeys(map, keys);
         for (int i = 0; i < keys.length; i++) {
-            T each = keys[i];
-            check("remove: " + keys_desc + "[" + i + "]" + each, null != map.remove(each));
+            T retVal = map.compute(keys[i], mappingFunction);
+            if (i % 3 != 2) { // key present, func returned null, should be absent from map
+                check(String.format("compute: retVal(%s[%d])", keys_desc, i), retVal == null);
+                check(String.format("compute: get(%s[%d])", keys_desc, i), null == map.get(keys[i]));
+                check(String.format("compute: containsKey(%s[%d])", keys_desc, i), !map.containsKey(keys[i]));
+                check(String.format("compute: containsValue(%s[%s])", keys_desc, i), !map.containsValue(keys[i]));                
+            } else { // odd: was removed, should now be mapped to EXTRA
+                check(String.format("compute: retVal(%s[%d])", keys_desc, i), retVal == extraVal);
+                check(String.format("compute: get(%s[%d])", keys_desc, i), extraVal == map.get(keys[i]));
+                check(String.format("compute: containsKey(%s[%d])", keys_desc, i), map.containsKey(keys[i]));
+                expectedSize++;
+            }
+        }
+        check(String.format("compute: containsValue(%s[%s])", keys_desc, extraVal.toString()), map.containsValue(extraVal));
+        check(String.format("map expected size#1 m%d != k%d", map.size(), expectedSize),
+                map.size() == expectedSize);        
+    }
+    
+    private static <T> void testMergeNonNull(Map<T, T> map, String keys_desc, T[] keys) {
+        // remove a third of the keys
+        // call merge() for all keys[]
+        // all keys should be present: removed keys now -> EXTRA, other keys -> k-1        
+        
+        // Map to preceding key
+        BiFunction<T,T,T> mappingFunction = (k, v) -> keys[Integer.parseInt(k.toString()) - 1];        
+        T extraVal = getExtraVal(keys[0]);
+        removeThirdKeys(map, keys);
+        for (int i = 1; i < keys.length; i++) {
+            T retVal = map.merge(keys[i], extraVal, mappingFunction);
+            if (i % 3 != 2) { // key present, should be mapped to k-1               
+                check(String.format("compute: retVal(%s[%d])", keys_desc, i), retVal == keys[i-1]);
+                check(String.format("compute: get(%s[%d])", keys_desc, i), keys[i-1] == map.get(keys[i]));
+            } else { // odd: was removed, should be replaced with EXTRA
+                check(String.format("compute: retVal(%s[%d])", keys_desc, i), retVal == extraVal);
+                check(String.format("compute: get(%s[%d])", keys_desc, i), extraVal == map.get(keys[i]));
         }
-
-        check(String.format("remove: map empty. size=%d", map.size()),
-                (map.size() == 0) && map.isEmpty());
+            check(String.format("compute: containsKey(%s[%d])", keys_desc, i), map.containsKey(keys[i]));
     }
 
-    private static <T> void testKeysIteratorRemove(Map<T, T> map, String keys_desc, T[] keys) {
-        check(String.format("remove: map expected size m%d != k%d", map.size(), keys.length),
+        check(String.format("map expected size#1 m%d != k%d", map.size(), keys.length),
                 map.size() == keys.length);
+        check(String.format("compute: containsValue(%s[%s])", keys_desc, extraVal.toString()), map.containsValue(extraVal));
+        check(String.format("compute: !containsValue(%s,[null])", keys_desc), !map.containsValue(null));
 
-        Iterator<T> each = map.keySet().iterator();
-        while (each.hasNext()) {
-            T t = each.next();
-            each.remove();
-            check("not removed: " + each, !map.containsKey(t) );
-        }
-
-        check(String.format("remove: map empty. size=%d", map.size()),
-                (map.size() == 0) && map.isEmpty());
     }
 
-    private static <T> void testValuesIteratorRemove(Map<T, T> map, String keys_desc, T[] keys) {
-        check(String.format("remove: map expected size m%d != k%d", map.size(), keys.length),
-                map.size() == keys.length);
+    private static <T> void testMergeNull(Map<T, T> map, String keys_desc, T[] keys) {
+        // remove a third of the keys
+        // call merge() for all keys[]
+        // result: removed keys -> EXTRA, other keys absent
 
-        Iterator<T> each = map.values().iterator();
-        while (each.hasNext()) {
-            T t = each.next();
-            each.remove();
-            check("not removed: " + each, !map.containsValue(t) );
+        BiFunction<T,T,T> mappingFunction = (k, v) -> null;
+        T extraVal = getExtraVal(keys[0]);
+        int expectedSize = 0;
+        removeThirdKeys(map, keys);
+        for (int i = 0; i < keys.length; i++) {
+            T retVal = map.merge(keys[i], extraVal, mappingFunction);
+            if (i % 3 != 2) { // key present, func returned null, should be absent from map
+                check(String.format("compute: retVal(%s[%d])", keys_desc, i), retVal == null);
+                check(String.format("compute: get(%s[%d])", keys_desc, i), null == map.get(keys[i]));
+                check(String.format("compute: containsKey(%s[%d])", keys_desc, i), !map.containsKey(keys[i]));
+            } else { // odd: was removed, should now be mapped to EXTRA
+                check(String.format("compute: retVal(%s[%d])", keys_desc, i), retVal == extraVal);
+                check(String.format("compute: get(%s[%d])", keys_desc, i), extraVal == map.get(keys[i]));
+                check(String.format("compute: containsKey(%s[%d])", keys_desc, i), map.containsKey(keys[i]));
+                expectedSize++;
+            }
+            check(String.format("compute: containsValue(%s[%s])", keys_desc, i), !map.containsValue(keys[i]));                
+        }
+        check(String.format("compute: containsValue(%s[%s])", keys_desc, extraVal.toString()), map.containsValue(extraVal));
+        check(String.format("map expected size#1 m%d != k%d", map.size(), expectedSize),
+                map.size() == expectedSize);
         }
 
-        check(String.format("remove: map empty. size=%d", map.size()),
-                (map.size() == 0) && map.isEmpty());
+    /*
+     * Return the EXTRA val for the key type being used
+     */
+    private static <T> T getExtraVal(T key) {
+        if (key instanceof HashableInteger) {
+            return (T)EXTRA_INT_VAL;
+        } else {
+            return (T)EXTRA_STRING_VAL;
+        }           
     }
 
-    private static <T> void testEntriesIteratorRemove(Map<T, T> map, String keys_desc, T[] keys) {
-        check(String.format("remove: map expected size m%d != k%d", map.size(), keys.length),
-                map.size() == keys.length);
+    /*
+     * Remove half of the keys
+     */
+    private static <T> void removeOddKeys(Map<T, T> map, /*String keys_desc, */ T[] keys) {
+        int removes = 0;
+        for (int i = 0; i < keys.length; i++) {
+            if (i % 2 != 0) {
+                map.remove(keys[i]);
+                removes++;
+            }
+        }        
+        check(String.format("map expected size m%d != k%d", map.size(), keys.length - removes),
+                map.size() == keys.length - removes);                
+    }
 
-        Iterator<Map.Entry<T,T>> each = map.entrySet().iterator();
-        while (each.hasNext()) {
-            Map.Entry<T,T> t = each.next();
-            T key = t.getKey();
-            T value = t.getValue();
-            each.remove();
-            check("not removed: " + each, (map instanceof IdentityHashMap) || !map.entrySet().contains(t) );
-            check("not removed: " + each, !map.containsKey(key) );
-            check("not removed: " + each, !map.containsValue(value));
+    /*
+     * Remove every third key
+     * This will hopefully leave some removed keys in TreeBins for, e.g., computeIfAbsent
+     * w/ a func that returns null.
+     * 
+     * TODO: consider using this in other tests (and maybe adding a remapThirdKeys)
+     */
+    private static <T> void removeThirdKeys(Map<T, T> map, /*String keys_desc, */ T[] keys) {
+        int removes = 0;
+        for (int i = 0; i < keys.length; i++) {
+            if (i % 3 == 2) {
+                map.remove(keys[i]);
+                removes++;
+            }
+        }        
+        check(String.format("map expected size m%d != k%d", map.size(), keys.length - removes),
+                map.size() == keys.length - removes);                
         }
 
-        check(String.format("remove: map empty. size=%d", map.size()),
-                (map.size() == 0) && map.isEmpty());
+    /* 
+     * Re-map the odd-numbered keys to map to the EXTRA value
+     */
+    private static <T> void remapOddKeys(Map<T, T> map, /*String keys_desc, */ T[] keys) {
+        T extraVal = getExtraVal(keys[0]);
+        for (int i = 0; i < keys.length; i++) {
+            if (i % 2 != 0) {
+                map.put(keys[i], extraVal);
+            }
+        }        
     }
 
     //--------------------- Infrastructure ---------------------------
     static volatile int passed = 0, failed = 0;