1 /*
   2  * Copyright (c) 2013, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 /*
  25  * @test
  26  * @bug 8005698
  27  * @run testng/othervm -Dtest.map.collisions.shortrun=true InPlaceOpsCollisions
  28  * @summary Ensure overrides of in-place operations in Maps behave well with lots of collisions.
  29  */
  30 
  31 import java.util.Arrays;
  32 import java.util.Comparator;
  33 import java.util.HashMap;
  34 import java.util.Iterator;
  35 import java.util.LinkedHashMap;
  36 import java.util.Map;
  37 import java.util.TreeMap;
  38 import java.util.function.BiFunction;
  39 import java.util.function.Function;
  40 import java.util.function.Supplier;
  41 
  42 import org.testng.annotations.DataProvider;
  43 import org.testng.annotations.Test;
  44 import static org.testng.Assert.assertTrue;
  45 import static org.testng.Assert.assertFalse;
  46 import static org.testng.Assert.assertEquals;
  47 import static org.testng.Assert.assertNull;
  48 
  49 public class InPlaceOpsCollisions extends MapWithCollisionsProviders {
  50 
  51     @Test(dataProvider = "mapsWithObjectsAndStrings")
  52     void testPutIfAbsent(String desc, Supplier<Map<Object, Object>> ms, Object val) {
  53         Map<Object, Object> map = ms.get();
  54         Object[] keys = map.keySet().toArray();
  55         Object retVal;
  56         removeOddKeys(map, keys);
  57         for (int i = 0; i < keys.length; i++) {
  58             retVal = map.putIfAbsent(keys[i], val);
  59             if (i % 2 == 0) { // even: not absent, not put
  60 
  61                 assertEquals(retVal, keys[i],
  62                         String.format("putIfAbsent: (%s[%d]) retVal", desc, i));
  63                 assertEquals(keys[i], map.get(keys[i]),
  64                         String.format("putIfAbsent: get(%s[%d])", desc, i));
  65                 assertTrue(map.containsValue(keys[i]),
  66                         String.format("putIfAbsent: containsValue(%s[%d])", desc, i));
  67             } else { // odd: absent, was put
  68                 assertNull(retVal,
  69                         String.format("putIfAbsent: (%s[%d]) retVal", desc, i));
  70                 assertEquals(val, map.get(keys[i]),
  71                         String.format("putIfAbsent: get(%s[%d])", desc, i));
  72                 assertFalse(map.containsValue(keys[i]),
  73                         String.format("putIfAbsent: !containsValue(%s[%d])", desc, i));
  74             }
  75             assertTrue(map.containsKey(keys[i]),
  76                     String.format("insertion: containsKey(%s[%d])", desc, i));
  77         }
  78         assertEquals(map.size(), keys.length,
  79                 String.format("map expected size m%d != k%d", map.size(), keys.length));
  80     }
  81 
  82     @Test(dataProvider = "nullValueFriendlyMaps")
  83     void testPutIfAbsentOverwriteNull(String desc, Supplier<Map<Object, Object>> ms) {
  84         Map<Object, Object> map = ms.get();
  85         map.put("key", null);
  86         assertEquals(map.size(), 1, desc + ": size != 1");
  87         assertTrue(map.containsKey("key"), desc + ": does not have key");
  88         assertNull(map.get("key"), desc + ": value is not null");
  89         map.putIfAbsent("key", "value"); // must rewrite
  90         assertEquals(map.size(), 1, desc + ": size != 1");
  91         assertTrue(map.containsKey("key"), desc + ": does not have key");
  92         assertEquals(map.get("key"), "value", desc + ": value is not 'value'");
  93     }
  94 
  95     @Test(dataProvider = "mapsWithObjectsAndStrings")
  96     void testRemoveMapping(String desc, Supplier<Map<Object, Object>> ms, Object val) {
  97         Map<Object, Object> map = ms.get();
  98         Object[] keys = map.keySet().toArray();
  99         boolean removed;
 100         int removes = 0;
 101         remapOddKeys(map, keys, val);
 102         for (int i = 0; i < keys.length; i++) {
 103             removed = map.remove(keys[i], keys[i]);
 104             if (i % 2 == 0) { // even: original mapping, should be removed
 105                 assertTrue(removed,
 106                         String.format("removeMapping: retVal(%s[%d])", desc, i));
 107                 assertNull(map.get(keys[i]),
 108                         String.format("removeMapping: get(%s[%d])", desc, i));
 109                 assertFalse(map.containsKey(keys[i]),
 110                         String.format("removeMapping: !containsKey(%s[%d])", desc, i));
 111                 assertFalse(map.containsValue(keys[i]),
 112                         String.format("removeMapping: !containsValue(%s[%d])", desc, i));
 113                 removes++;
 114             } else { // odd: new mapping, not removed
 115                 assertFalse(removed,
 116                         String.format("removeMapping: retVal(%s[%d])", desc, i));
 117                 assertEquals(val, map.get(keys[i]),
 118                         String.format("removeMapping: get(%s[%d])", desc, i));
 119                 assertTrue(map.containsKey(keys[i]),
 120                         String.format("removeMapping: containsKey(%s[%d])", desc, i));
 121                 assertTrue(map.containsValue(val),
 122                         String.format("removeMapping: containsValue(%s[%d])", desc, i));
 123             }
 124         }
 125         assertEquals(map.size(), keys.length - removes,
 126                 String.format("map expected size m%d != k%d", map.size(), keys.length - removes));
 127     }
 128 
 129     @Test(dataProvider = "mapsWithObjectsAndStrings")
 130     void testReplaceOldValue(String desc, Supplier<Map<Object, Object>> ms, Object val) {
 131         // remap odds to val
 132         // call replace to replace for val, for all keys
 133         // check that all keys map to value from keys array
 134         Map<Object, Object> map = ms.get();
 135         Object[] keys = map.keySet().toArray();
 136         boolean replaced;
 137         remapOddKeys(map, keys, val);
 138 
 139         for (int i = 0; i < keys.length; i++) {
 140             replaced = map.replace(keys[i], val, keys[i]);
 141             if (i % 2 == 0) { // even: original mapping, should not be replaced
 142                 assertFalse(replaced,
 143                         String.format("replaceOldValue: retVal(%s[%d])", desc, i));
 144             } else { // odd: new mapping, should be replaced
 145                 assertTrue(replaced,
 146                         String.format("replaceOldValue: get(%s[%d])", desc, i));
 147             }
 148             assertEquals(keys[i], map.get(keys[i]),
 149                     String.format("replaceOldValue: get(%s[%d])", desc, i));
 150             assertTrue(map.containsKey(keys[i]),
 151                     String.format("replaceOldValue: containsKey(%s[%d])", desc, i));
 152             assertTrue(map.containsValue(keys[i]),
 153                     String.format("replaceOldValue: containsValue(%s[%d])", desc, i));
 154         }
 155         assertFalse(map.containsValue(val),
 156                 String.format("replaceOldValue: !containsValue(%s[%s])", desc, val));
 157         assertEquals(map.size(), keys.length,
 158                 String.format("map expected size m%d != k%d", map.size(), keys.length));
 159     }
 160 
 161     @Test(dataProvider = "mapsWithObjectsAndStrings")
 162     void testReplaceIfMapped(String desc, Supplier<Map<Object, Object>> ms, Object val) {
 163         // remove odd keys
 164         // call replace for all keys[]
 165         // odd keys should remain absent, even keys should be mapped to EXTRA, no value from keys[] should be in map
 166         Map<Object, Object> map = ms.get();
 167         Object[] keys = map.keySet().toArray();
 168         int expectedSize1 = 0;
 169         removeOddKeys(map, keys);
 170         int expectedSize2 = map.size();
 171 
 172         for (int i = 0; i < keys.length; i++) {
 173             Object retVal = map.replace(keys[i], val);
 174             if (i % 2 == 0) { // even: still in map, should be replaced
 175                 assertEquals(retVal, keys[i],
 176                         String.format("replaceIfMapped: retVal(%s[%d])", desc, i));
 177                 assertEquals(val, map.get(keys[i]),
 178                         String.format("replaceIfMapped: get(%s[%d])", desc, i));
 179                 assertTrue(map.containsKey(keys[i]),
 180                         String.format("replaceIfMapped: containsKey(%s[%d])", desc, i));
 181                 expectedSize1++;
 182             } else { // odd: was removed, should not be replaced
 183                 assertNull(retVal,
 184                         String.format("replaceIfMapped: retVal(%s[%d])", desc, i));
 185                 assertNull(map.get(keys[i]),
 186                         String.format("replaceIfMapped: get(%s[%d])", desc, i));
 187                 assertFalse(map.containsKey(keys[i]),
 188                         String.format("replaceIfMapped: containsKey(%s[%d])", desc, i));
 189             }
 190             assertFalse(map.containsValue(keys[i]),
 191                     String.format("replaceIfMapped: !containsValue(%s[%d])", desc, i));
 192         }
 193         assertTrue(map.containsValue(val),
 194                 String.format("replaceIfMapped: containsValue(%s[%s])", desc, val));
 195         assertEquals(map.size(), expectedSize1,
 196                 String.format("map expected size#1 m%d != k%d", map.size(), expectedSize1));
 197         assertEquals(map.size(), expectedSize2,
 198                 String.format("map expected size#2 m%d != k%d", map.size(), expectedSize2));
 199 
 200     }
 201 
 202     private static <T> void testComputeIfAbsent(Map<T, T> map, String desc, T[] keys,
 203             Function<T, T> mappingFunction) {
 204         // remove a third of the keys
 205         // call computeIfAbsent for all keys, func returns EXTRA
 206         // check that removed keys now -> EXTRA, other keys -> original val
 207         T expectedVal = mappingFunction.apply(keys[0]);
 208         T retVal;
 209         int expectedSize = 0;
 210         removeThirdKeys(map, keys);
 211         for (int i = 0; i < keys.length; i++) {
 212             retVal = map.computeIfAbsent(keys[i], mappingFunction);
 213             if (i % 3 != 2) { // key present, not computed
 214                 assertEquals(retVal, keys[i],
 215                         String.format("computeIfAbsent: (%s[%d]) retVal", desc, i));
 216                 assertEquals(keys[i], map.get(keys[i]),
 217                         String.format("computeIfAbsent: get(%s[%d])", desc, i));
 218                 assertTrue(map.containsValue(keys[i]),
 219                         String.format("computeIfAbsent: containsValue(%s[%d])", desc, i));
 220                 assertTrue(map.containsKey(keys[i]),
 221                         String.format("insertion: containsKey(%s[%d])", desc, i));
 222                 expectedSize++;
 223             } else { // key absent, computed unless function return null
 224                 assertEquals(retVal, expectedVal,
 225                         String.format("computeIfAbsent: (%s[%d]) retVal", desc, i));
 226                 assertEquals(expectedVal, map.get(keys[i]),
 227                         String.format("computeIfAbsent: get(%s[%d])", desc, i));
 228                 assertFalse(map.containsValue(keys[i]),
 229                         String.format("computeIfAbsent: !containsValue(%s[%d])", desc, i));
 230                 // mapping should not be added if function returns null
 231                 assertTrue(map.containsKey(keys[i]) != (expectedVal == null),
 232                         String.format("insertion: containsKey(%s[%d])", desc, i));
 233                 if (expectedVal != null) {
 234                     expectedSize++;
 235                 }
 236             }
 237         }
 238         if (expectedVal != null) {
 239             assertTrue(map.containsValue(expectedVal),
 240                     String.format("computeIfAbsent: containsValue(%s[%s])", desc, expectedVal));
 241         }
 242         assertEquals(map.size(), expectedSize,
 243                 String.format("map expected size m%d != k%d", map.size(), expectedSize));
 244     }
 245 
 246     @Test(dataProvider = "mapsWithObjectsAndStrings")
 247     void testComputeIfAbsentNonNull(String desc, Supplier<Map<Object, Object>> ms, Object val) {
 248         Map<Object, Object> map = ms.get();
 249         Object[] keys = map.keySet().toArray();
 250         testComputeIfAbsent(map, desc, keys, (k) -> val);
 251     }
 252 
 253     @Test(dataProvider = "mapsWithObjectsAndStrings")
 254     void testComputeIfAbsentNull(String desc, Supplier<Map<Object, Object>> ms, Object val) {
 255         Map<Object, Object> map = ms.get();
 256         Object[] keys = map.keySet().toArray();
 257         testComputeIfAbsent(map, desc, keys, (k) -> null);
 258     }
 259 
 260     private static <T> void testComputeIfPresent(Map<T, T> map, String desc, T[] keys,
 261             BiFunction<T, T, T> mappingFunction) {
 262         // remove a third of the keys
 263         // call testComputeIfPresent for all keys[]
 264         // removed keys should remain absent, even keys should be mapped to $RESULT
 265         // no value from keys[] should be in map
 266         T funcResult = mappingFunction.apply(keys[0], keys[0]);
 267         int expectedSize1 = 0;
 268         removeThirdKeys(map, keys);
 269 
 270         for (int i = 0; i < keys.length; i++) {
 271             T retVal = map.computeIfPresent(keys[i], mappingFunction);
 272             if (i % 3 != 2) { // key present
 273                 if (funcResult == null) { // was removed
 274                     assertFalse(map.containsKey(keys[i]),
 275                             String.format("replaceIfMapped: containsKey(%s[%d])", desc, i));
 276                 } else { // value was replaced
 277                     assertTrue(map.containsKey(keys[i]),
 278                             String.format("replaceIfMapped: containsKey(%s[%d])", desc, i));
 279                     expectedSize1++;
 280                 }
 281                 assertEquals(retVal, funcResult,
 282                         String.format("computeIfPresent: retVal(%s[%s])", desc, i));
 283                 assertEquals(funcResult, map.get(keys[i]),
 284                         String.format("replaceIfMapped: get(%s[%d])", desc, i));
 285 
 286             } else { // odd: was removed, should not be replaced
 287                 assertNull(retVal,
 288                         String.format("replaceIfMapped: retVal(%s[%d])", desc, i));
 289                 assertNull(map.get(keys[i]),
 290                         String.format("replaceIfMapped: get(%s[%d])", desc, i));
 291                 assertFalse(map.containsKey(keys[i]),
 292                         String.format("replaceIfMapped: containsKey(%s[%d])", desc, i));
 293             }
 294             assertFalse(map.containsValue(keys[i]),
 295                     String.format("replaceIfMapped: !containsValue(%s[%d])", desc, i));
 296         }
 297         assertEquals(map.size(), expectedSize1,
 298                 String.format("map expected size#1 m%d != k%d", map.size(), expectedSize1));
 299     }
 300 
 301     @Test(dataProvider = "mapsWithObjectsAndStrings")
 302     void testComputeIfPresentNonNull(String desc, Supplier<Map<Object, Object>> ms, Object val) {
 303         Map<Object, Object> map = ms.get();
 304         Object[] keys = map.keySet().toArray();
 305         testComputeIfPresent(map, desc, keys, (k, v) -> val);
 306     }
 307 
 308     @Test(dataProvider = "mapsWithObjectsAndStrings")
 309     void testComputeIfPresentNull(String desc, Supplier<Map<Object, Object>> ms, Object val) {
 310         Map<Object, Object> map = ms.get();
 311         Object[] keys = map.keySet().toArray();
 312         testComputeIfPresent(map, desc, keys, (k, v) -> null);
 313     }
 314 
 315     @Test(dataProvider = "hashMapsWithObjects")
 316     void testComputeNonNull(String desc, Supplier<Map<IntKey, IntKey>> ms, IntKey val) {
 317         // remove a third of the keys
 318         // call compute() for all keys[]
 319         // all keys should be present: removed keys -> EXTRA, others to k-1
 320         Map<IntKey, IntKey> map = ms.get();
 321         IntKey[] keys = map.keySet().stream().sorted().toArray(IntKey[]::new);
 322         BiFunction<IntKey, IntKey, IntKey> mappingFunction = (k, v) -> {
 323             if (v == null) {
 324                 return val;
 325             } else {
 326                 return keys[k.getValue() - 1];
 327             }
 328         };
 329         removeThirdKeys(map, keys);
 330         for (int i = 1; i < keys.length; i++) {
 331             IntKey retVal = map.compute(keys[i], mappingFunction);
 332             if (i % 3 != 2) { // key present, should be mapped to k-1
 333                 assertEquals(retVal, keys[i - 1],
 334                         String.format("compute: retVal(%s[%d])", desc, i));
 335                 assertEquals(keys[i - 1], map.get(keys[i]),
 336                         String.format("compute: get(%s[%d])", desc, i));
 337             } else { // odd: was removed, should be replaced with EXTRA
 338                 assertEquals(retVal, val,
 339                         String.format("compute: retVal(%s[%d])", desc, i));
 340                 assertEquals(val, map.get(keys[i]),
 341                         String.format("compute: get(%s[%d])", desc, i));
 342             }
 343             assertTrue(map.containsKey(keys[i]),
 344                     String.format("compute: containsKey(%s[%d])", desc, i));
 345         }
 346         assertEquals(map.size(), keys.length,
 347                 String.format("map expected size#1 m%d != k%d", map.size(), keys.length));
 348         assertTrue(map.containsValue(val),
 349                 String.format("compute: containsValue(%s[%s])", desc, val));
 350         assertFalse(map.containsValue(null),
 351                 String.format("compute: !containsValue(%s,[null])", desc));
 352     }
 353 
 354     @Test(dataProvider = "mapsWithObjectsAndStrings")
 355     void testComputeNull(String desc, Supplier<Map<Object, Object>> ms, Object val) {
 356         // remove a third of the keys
 357         // call compute() for all keys[]
 358         // removed keys should -> EXTRA
 359         // for other keys: func returns null, should have no mapping
 360         Map<Object, Object> map = ms.get();
 361         Object[] keys = map.keySet().toArray();
 362         BiFunction<Object, Object, Object> mappingFunction = (k, v) -> {
 363             // if absent/null -> EXTRA
 364             // if present -> null
 365             if (v == null) {
 366                 return val;
 367             } else {
 368                 return null;
 369             }
 370         };
 371         int expectedSize = 0;
 372         removeThirdKeys(map, keys);
 373         for (int i = 0; i < keys.length; i++) {
 374             Object retVal = map.compute(keys[i], mappingFunction);
 375             if (i % 3 != 2) { // key present, func returned null, should be absent from map
 376                 assertNull(retVal,
 377                         String.format("compute: retVal(%s[%d])", desc, i));
 378                 assertNull(map.get(keys[i]),
 379                         String.format("compute: get(%s[%d])", desc, i));
 380                 assertFalse(map.containsKey(keys[i]),
 381                         String.format("compute: containsKey(%s[%d])", desc, i));
 382                 assertFalse(map.containsValue(keys[i]),
 383                         String.format("compute: containsValue(%s[%s])", desc, i));
 384             } else { // odd: was removed, should now be mapped to EXTRA
 385                 assertEquals(retVal, val,
 386                         String.format("compute: retVal(%s[%d])", desc, i));
 387                 assertEquals(val, map.get(keys[i]),
 388                         String.format("compute: get(%s[%d])", desc, i));
 389                 assertTrue(map.containsKey(keys[i]),
 390                         String.format("compute: containsKey(%s[%d])", desc, i));
 391                 expectedSize++;
 392             }
 393         }
 394         assertTrue(map.containsValue(val),
 395                 String.format("compute: containsValue(%s[%s])", desc, val));
 396         assertEquals(map.size(), expectedSize,
 397                 String.format("map expected size#1 m%d != k%d", map.size(), expectedSize));
 398     }
 399 
 400     @Test(dataProvider = "hashMapsWithObjects")
 401     void testMergeNonNull(String desc, Supplier<Map<IntKey, IntKey>> ms, IntKey val) {
 402         // remove a third of the keys
 403         // call merge() for all keys[]
 404         // all keys should be present: removed keys now -> EXTRA, other keys -> k-1
 405         Map<IntKey, IntKey> map = ms.get();
 406         IntKey[] keys = map.keySet().stream().sorted().toArray(IntKey[]::new);
 407 
 408         // Map to preceding key
 409         BiFunction<IntKey, IntKey, IntKey> mappingFunction
 410                 = (k, v) -> keys[k.getValue() - 1];
 411         removeThirdKeys(map, keys);
 412         for (int i = 1; i < keys.length; i++) {
 413             IntKey retVal = map.merge(keys[i], val, mappingFunction);
 414             if (i % 3 != 2) { // key present, should be mapped to k-1
 415                 assertEquals(retVal, keys[i - 1],
 416                         String.format("compute: retVal(%s[%d])", desc, i));
 417                 assertEquals(keys[i - 1], map.get(keys[i]),
 418                         String.format("compute: get(%s[%d])", desc, i));
 419             } else { // odd: was removed, should be replaced with EXTRA
 420                 assertEquals(retVal, val,
 421                         String.format("compute: retVal(%s[%d])", desc, i));
 422                 assertEquals(val, map.get(keys[i]),
 423                         String.format("compute: get(%s[%d])", desc, i));
 424             }
 425             assertTrue(map.containsKey(keys[i]),
 426                     String.format("compute: containsKey(%s[%d])", desc, i));
 427         }
 428 
 429         assertEquals(map.size(), keys.length,
 430                 String.format("map expected size#1 m%d != k%d", map.size(), keys.length));
 431         assertTrue(map.containsValue(val),
 432                 String.format("compute: containsValue(%s[%s])", desc, val));
 433         assertFalse(map.containsValue(null),
 434                 String.format("compute: !containsValue(%s,[null])", desc));
 435     }
 436 
 437     @Test(dataProvider = "mapsWithObjectsAndStrings")
 438     void testMergeNull(String desc, Supplier<Map<Object, Object>> ms, Object val) {
 439         // remove a third of the keys
 440         // call merge() for all keys[]
 441         // result: removed keys -> EXTRA, other keys absent
 442 
 443         Map<Object, Object> map = ms.get();
 444         Object[] keys = map.keySet().toArray();
 445         BiFunction<Object, Object, Object> mappingFunction = (k, v) -> null;
 446         int expectedSize = 0;
 447         removeThirdKeys(map, keys);
 448         for (int i = 0; i < keys.length; i++) {
 449             Object retVal = map.merge(keys[i], val, mappingFunction);
 450             if (i % 3 != 2) { // key present, func returned null, should be absent from map
 451                 assertNull(retVal,
 452                         String.format("compute: retVal(%s[%d])", desc, i));
 453                 assertNull(map.get(keys[i]),
 454                         String.format("compute: get(%s[%d])", desc, i));
 455                 assertFalse(map.containsKey(keys[i]),
 456                         String.format("compute: containsKey(%s[%d])", desc, i));
 457             } else { // odd: was removed, should now be mapped to EXTRA
 458                 assertEquals(retVal, val,
 459                         String.format("compute: retVal(%s[%d])", desc, i));
 460                 assertEquals(val, map.get(keys[i]),
 461                         String.format("compute: get(%s[%d])", desc, i));
 462                 assertTrue(map.containsKey(keys[i]),
 463                         String.format("compute: containsKey(%s[%d])", desc, i));
 464                 expectedSize++;
 465             }
 466             assertFalse(map.containsValue(keys[i]),
 467                     String.format("compute: containsValue(%s[%s])", desc, i));
 468         }
 469         assertTrue(map.containsValue(val),
 470                 String.format("compute: containsValue(%s[%s])", desc, val));
 471         assertEquals(map.size(), expectedSize,
 472                 String.format("map expected size#1 m%d != k%d", map.size(), expectedSize));
 473     }
 474 
 475     /*
 476      * Remove half of the keys
 477      */
 478     private static <T> void removeOddKeys(Map<T, T> map, /*String keys_desc, */ T[] keys) {
 479         int removes = 0;
 480         for (int i = 0; i < keys.length; i++) {
 481             if (i % 2 != 0) {
 482                 map.remove(keys[i]);
 483                 removes++;
 484             }
 485         }
 486         assertEquals(map.size(), keys.length - removes,
 487                 String.format("map expected size m%d != k%d", map.size(), keys.length - removes));
 488     }
 489 
 490     /*
 491      * Remove every third key
 492      * This will hopefully leave some removed keys in TreeBins for, e.g., computeIfAbsent
 493      * w/ a func that returns null.
 494      *
 495      * TODO: consider using this in other tests (and maybe adding a remapThirdKeys)
 496      */
 497     private static <T> void removeThirdKeys(Map<T, T> map, /*String keys_desc, */ T[] keys) {
 498         int removes = 0;
 499         for (int i = 0; i < keys.length; i++) {
 500             if (i % 3 == 2) {
 501                 map.remove(keys[i]);
 502                 removes++;
 503             }
 504         }
 505         assertEquals(map.size(), keys.length - removes,
 506                 String.format("map expected size m%d != k%d", map.size(), keys.length - removes));
 507     }
 508 
 509     /*
 510      * Re-map the odd-numbered keys to map to the EXTRA value
 511      */
 512     private static <T> void remapOddKeys(Map<T, T> map, T[] keys, T val) {
 513         for (int i = 0; i < keys.length; i++) {
 514             if (i % 2 != 0) {
 515                 map.put(keys[i], val);
 516             }
 517         }
 518     }
 519 
 520     @DataProvider
 521     public Iterator<Object[]> nullValueFriendlyMaps() {
 522         return Arrays.asList(
 523                 new Object[]{"HashMap", (Supplier<Map<?, ?>>) HashMap::new},
 524                 new Object[]{"LinkedHashMap", (Supplier<Map<?, ?>>) LinkedHashMap::new},
 525                 new Object[]{"TreeMap", (Supplier<Map<?, ?>>) TreeMap::new},
 526                 new Object[]{"TreeMap(cmp)", (Supplier<Map<?, ?>>) () -> new TreeMap<>(Comparator.reverseOrder())}
 527         ).iterator();
 528     }
 529 }