--- old/src/java.base/share/classes/java/util/TreeMap.java 2019-07-15 10:39:07.353001000 +0000 +++ new/src/java.base/share/classes/java/util/TreeMap.java 2019-07-15 10:39:07.157001000 +0000 @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2019, 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 @@ -29,6 +29,7 @@ import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; +import java.util.function.Function; /** * A Red-Black tree based {@link NavigableMap} implementation. @@ -531,17 +532,109 @@ * does not permit null keys */ public V put(K key, V value) { - Entry t = root; - if (t == null) { - compare(key, key); // type (and possibly null) check + return put(key, value, true); + } + + private void addEntry(K key, V value, int cmp, Entry parent) { + Entry e = new Entry<>(key, value, parent); + if (cmp < 0) + parent.left = e; + else + parent.right = e; + fixAfterInsertion(e); + size++; + modCount++; + } - root = new Entry<>(key, value, null); - size = 1; - modCount++; + private void addEntryToEmptyMap(K key, V value) { + compare(key, key); // type (and possibly null) check + root = new Entry<>(key, value, null); + size = 1; + modCount++; + } + + private V put(K key, V value, boolean replaceOld) { + Entry t = root; + if (t == null) { + addEntryToEmptyMap(key, value); return null; } int cmp; - Entry parent; + Entry parent; + // split comparator and comparable paths + Comparator cpr = comparator; + if (cpr != null) { + do { + parent = t; + cmp = cpr.compare(key, t.key); + if (cmp < 0) + t = t.left; + else if (cmp > 0) + t = t.right; + else { + V oldValue = t.value; + if (replaceOld || oldValue == null) { + t.value = value; + } + return oldValue; + } + } while (t != null); + } else { + if (key == null) + throw new NullPointerException(); + @SuppressWarnings("unchecked") + Comparable k = (Comparable) key; + do { + parent = t; + cmp = k.compareTo(t.key); + if (cmp < 0) + t = t.left; + else if (cmp > 0) + t = t.right; + else { + V oldValue = t.value; + if (replaceOld || oldValue == null) { + t.value = value; + } + return oldValue; + } + } while (t != null); + } + addEntry(key, value, cmp, parent); + return null; + } + + @Override + public V putIfAbsent(K key, V value) { + return put(key, value, false); + } + + /** + * {@inheritDoc} + * + *

This method will, on a best-effort basis, throw a + * {@link ConcurrentModificationException} if it is detected that the + * remapping function modifies this map during computation. + * + * @throws ConcurrentModificationException if it is detected that the + * remapping function modified this map + */ + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + Objects.requireNonNull(mappingFunction); + V newValue; + Entry t = root; + if (t == null) { + newValue = getNewValueAndCheckModification(key, mappingFunction); + if (newValue != null) { + addEntryToEmptyMap(key, newValue); + return newValue; + } else { + return null; + } + } + int cmp; + Entry parent; // split comparator and comparable paths Comparator cpr = comparator; if (cpr != null) { @@ -553,14 +646,128 @@ else if (cmp > 0) t = t.right; else - return t.setValue(value); + return t.value; + } while (t != null); + } else { + if (key == null) + throw new NullPointerException(); + @SuppressWarnings("unchecked") + Comparable k = (Comparable) key; + do { + parent = t; + cmp = k.compareTo(t.key); + if (cmp < 0) + t = t.left; + else if (cmp > 0) + t = t.right; + else + return t.value; } while (t != null); } - else { + newValue = getNewValueAndCheckModification(key, mappingFunction); + if (newValue != null) { + addEntry(key, newValue, cmp, parent); + return newValue; + } + return null; + } + + private V getNewValueAndCheckModification(K key, Function mappingFunction) { + V newValue; + int mc = modCount; + newValue = mappingFunction.apply(key); + if (mc != modCount) { + throw new ConcurrentModificationException(); + } + return newValue; + } + + /** + * {@inheritDoc} + * + *

This method will, on a best-effort basis, throw a + * {@link ConcurrentModificationException} if it is detected that the + * remapping function modifies this map during computation. + * + * @throws ConcurrentModificationException if it is detected that the + * remapping function modified this map + */ + @Override + public V computeIfPresent(K key, BiFunction remappingFunction) { + Objects.requireNonNull(remappingFunction); + Entry oldEntry = getEntry(key); + if (oldEntry != null && oldEntry.value != null) { + return remapValue(oldEntry, key, remappingFunction); + } else { + return null; + } + } + + private V remapValue(Entry t, K key, BiFunction remappingFunction) { + V newValue = getNewValueAndCheckModification(key, t.value, remappingFunction); + if (newValue == null) { + deleteEntry(t); + return null; + } else { + // replace old mapping + t.value = newValue; + return newValue; + } + } + + private V getNewValueAndCheckModification(K key, V oldValue, BiFunction remappingFunction) { + int mc = modCount; + V newValue = remappingFunction.apply(key, oldValue); + if (mc != modCount) { + throw new ConcurrentModificationException(); + } + return newValue; + } + + /** + * {@inheritDoc} + * + *

This method will, on a best-effort basis, throw a + * {@link ConcurrentModificationException} if it is detected that the + * remapping function modifies this map during computation. + * + * @throws ConcurrentModificationException if it is detected that the + * remapping function modified this map + */ + @Override + public V compute(K key, BiFunction remappingFunction) { + Objects.requireNonNull(remappingFunction); + V newValue; + Entry t = root; + if (t == null) { + newValue = getNewValueAndCheckModification(key, null, remappingFunction); + if (newValue != null) { + addEntryToEmptyMap(key, newValue); + return newValue; + } else { + return null; + } + } + int cmp; + Entry parent; + // split comparator and comparable paths + Comparator cpr = comparator; + if (cpr != null) { + do { + parent = t; + cmp = cpr.compare(key, t.key); + if (cmp < 0) + t = t.left; + else if (cmp > 0) + t = t.right; + else + return remapValue(t, key, remappingFunction); + } while (t != null); + } else { if (key == null) throw new NullPointerException(); @SuppressWarnings("unchecked") - Comparable k = (Comparable) key; + Comparable k = (Comparable) key; do { parent = t; cmp = k.compareTo(t.key); @@ -569,21 +776,92 @@ else if (cmp > 0) t = t.right; else - return t.setValue(value); + return remapValue(t, key, remappingFunction); } while (t != null); } - Entry e = new Entry<>(key, value, parent); - if (cmp < 0) - parent.left = e; - else - parent.right = e; - fixAfterInsertion(e); - size++; - modCount++; + newValue = getNewValueAndCheckModification(key, null, remappingFunction); + if (newValue != null) { + addEntry(key, newValue, cmp, parent); + return newValue; + } return null; } /** + * {@inheritDoc} + * + *

This method will, on a best-effort basis, throw a + * {@link ConcurrentModificationException} if it is detected that the + * remapping function modifies this map during computation. + * + * @throws ConcurrentModificationException if it is detected that the + * remapping function modified this map + */ + @Override + public V merge(K key, V value, BiFunction remappingFunction) { + Objects.requireNonNull(remappingFunction); + Objects.requireNonNull(value); + Entry t = root; + if (t == null) { + addEntryToEmptyMap(key, value); + return value; + } + int cmp; + Entry parent; + // split comparator and comparable paths + Comparator cpr = comparator; + if (cpr != null) { + do { + parent = t; + cmp = cpr.compare(key, t.key); + if (cmp < 0) + t = t.left; + else if (cmp > 0) + t = t.right; + else return mergeValue(t, value, remappingFunction); + } while (t != null); + } else { + if (key == null) + throw new NullPointerException(); + @SuppressWarnings("unchecked") + Comparable k = (Comparable) key; + do { + parent = t; + cmp = k.compareTo(t.key); + if (cmp < 0) + t = t.left; + else if (cmp > 0) + t = t.right; + else return mergeValue(t, value, remappingFunction); + } while (t != null); + } + addEntry(key, value, cmp, parent); + return value; + } + + private V mergeValue(Entry t, V value, BiFunction remappingFunction) { + V oldValue = t.value; + V newValue; + if (t.value == null) { + newValue = value; + } else { + int mc = modCount; + newValue = remappingFunction.apply(oldValue, value); + if (mc != modCount) { + throw new ConcurrentModificationException(); + } + } + if (newValue == null) { + deleteEntry(t); + return null; + } else { + // replace old mapping + t.value = newValue; + return newValue; + } + } + + /** * Removes the mapping for this key from this TreeMap if present. * * @param key key for which mapping should be removed --- old/test/jdk/java/util/Map/FunctionalCMEs.java 2019-07-15 10:39:08.049001000 +0000 +++ new/test/jdk/java/util/Map/FunctionalCMEs.java 2019-07-15 10:39:07.861001000 +0000 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2019, 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 @@ -30,6 +30,7 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; +import java.util.TreeMap; import java.util.function.BiFunction; import org.testng.annotations.Test; @@ -53,6 +54,7 @@ new Object[]{new HashMap<>(), true}, new Object[]{new Hashtable<>(), true}, new Object[]{new LinkedHashMap<>(), true}, + new Object[]{new TreeMap<>(), true}, // Test default Map methods - no CME new Object[]{new Defaults.ExtendsAbstractMap<>(), false} ).iterator(); --- old/test/jdk/java/util/Map/InPlaceOpsCollisions.java 2019-07-15 10:39:08.669001000 +0000 +++ new/test/jdk/java/util/Map/InPlaceOpsCollisions.java 2019-07-15 10:39:08.481001000 +0000 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2019, 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 @@ -27,11 +27,19 @@ * @run testng/othervm -Dtest.map.collisions.shortrun=true InPlaceOpsCollisions * @summary Ensure overrides of in-place operations in Maps behave well with lots of collisions. */ + +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.TreeMap; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import static org.testng.Assert.assertTrue; import static org.testng.Assert.assertFalse; @@ -71,6 +79,19 @@ String.format("map expected size m%d != k%d", map.size(), keys.length)); } + @Test(dataProvider = "nullValueFriendlyMaps") + void testPutIfAbsentOverwriteNull(String desc, Supplier> ms) { + Map map = ms.get(); + map.put("key", null); + assertEquals(map.size(), 1, desc + ": size != 1"); + assertTrue(map.containsKey("key"), desc + ": does not have key"); + assertNull(map.get("key"), desc + ": value is not null"); + map.putIfAbsent("key", "value"); // must rewrite + assertEquals(map.size(), 1, desc + ": size != 1"); + assertTrue(map.containsKey("key"), desc + ": does not have key"); + assertEquals(map.get("key"), "value", desc + ": value is not 'value'"); + } + @Test(dataProvider = "mapsWithObjectsAndStrings") void testRemoveMapping(String desc, Supplier> ms, Object val) { Map map = ms.get(); @@ -496,4 +517,13 @@ } } + @DataProvider + public Iterator nullValueFriendlyMaps() { + return Arrays.asList( + new Object[]{"HashMap", (Supplier>) HashMap::new}, + new Object[]{"LinkedHashMap", (Supplier>) LinkedHashMap::new}, + new Object[]{"TreeMap", (Supplier>) TreeMap::new}, + new Object[]{"TreeMap(cmp)", (Supplier>) () -> new TreeMap<>(Comparator.reverseOrder())} + ).iterator(); + } } --- /dev/null 2019-06-04 22:44:10.544000000 +0000 +++ new/test/micro/org/openjdk/bench/java/util/TreeMapUpdate.java 2019-07-15 10:39:09.129001000 +0000 @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2019, 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. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.util; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.Comparator; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Fork(3) +@State(Scope.Thread) +public class TreeMapUpdate { + @Param({"10", "1000", "100000"}) + public int size; + + @Param({"true", "false"}) + public boolean shuffled; + + @Param({"true", "false"}) + public boolean comparator; + + private Supplier> supplier; + + @Setup + public void setUp() { + supplier = comparator ? () -> new TreeMap<>(Comparator.reverseOrder()) : TreeMap::new; + } + + private int nextKey(int prevKey) { + return shuffled ? prevKey + 1 : prevKey + 987_654_321; + } + + @Benchmark + public Map put(Blackhole bh) { + int s = size; + Map map = supplier.get(); + int key = 0; + for (int i = 0; i < s; i++) { + bh.consume(map.put(key, key)); + bh.consume(map.put(key, key)); + key = nextKey(key); + } + return map; + } + + @Benchmark + public Map putIfAbsent(Blackhole bh) { + int s = size; + Map map = supplier.get(); + int key = 0; + for (int i = 0; i < s; i++) { + bh.consume(map.putIfAbsent(key, key)); + bh.consume(map.putIfAbsent(key, key)); + key = nextKey(key); + } + return map; + } + + @Benchmark + public Map computeIfAbsent(Blackhole bh) { + int s = size; + Map map = supplier.get(); + int key = 0; + for (int i = 0; i < s; i++) { + bh.consume(map.computeIfAbsent(key, k -> k)); + bh.consume(map.computeIfAbsent(key, k -> k)); + key = nextKey(key); + } + return map; + } + + @Benchmark + public Map compute(Blackhole bh) { + int s = size; + Map map = supplier.get(); + int key = 0; + for (int i = 0; i < s; i++) { + bh.consume(map.compute(key, (k, old) -> k * 2)); + bh.consume(map.compute(key, (k, old) -> k * 2)); + key = nextKey(key); + } + return map; + } + + @Benchmark + public Map computeIfPresent(Blackhole bh) { + int s = size; + Map map = supplier.get(); + int key = 0; + for (int i = 0; i < s; i++) { + bh.consume(map.computeIfPresent(key, (k, old) -> old * 2)); + map.put(key, key); + bh.consume(map.computeIfPresent(key, (k, old) -> old * 2)); + key = nextKey(key); + } + return map; + } + + @Benchmark + public Map merge(Blackhole bh) { + int s = size; + Map map = supplier.get(); + int key = 0; + for (int i = 0; i < s; i++) { + bh.consume(map.merge(key, key, Integer::sum)); + bh.consume(map.merge(key, key, Integer::sum)); + key = nextKey(key); + } + return map; + } +}