8176894: Provide specialized implementation for default methods putIfAbsent, computeIfAbsent, computeIfPresent, compute, merge in TreeMap

Co-authored-by: Sergey Kuksenko <sergey.kuksenko@oracle.com>
Reviewed-by: martin, stuefe, rriggs
This commit is contained in:
Tagir F. Valeev 2020-04-02 05:44:04 +00:00
parent 3790e58090
commit 0386b7d0c3
4 changed files with 474 additions and 19 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 1997, 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1997, 2020, 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.io.Serializable;
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.
@ -341,8 +342,7 @@ public class TreeMap<K,V>
// Offload comparator-based version for sake of performance
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
Objects.requireNonNull(key);
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
@ -531,14 +531,37 @@ public class TreeMap<K,V>
* does not permit null keys
*/
public V put(K key, V value) {
return put(key, value, true);
}
@Override
public V putIfAbsent(K key, V value) {
return put(key, value, false);
}
/**
* {@inheritDoc}
*
* <p>This method will, on a best-effort basis, throw a
* {@link ConcurrentModificationException} if it is detected that the
* mapping function modifies this map during computation.
*
* @throws ConcurrentModificationException if it is detected that the
* mapping function modified this map
*/
@Override
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
Objects.requireNonNull(mappingFunction);
V newValue;
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
newValue = callMappingFunctionWithCheck(key, mappingFunction);
if (newValue != null) {
addEntryToEmptyMap(key, newValue);
return newValue;
} else {
return null;
}
}
int cmp;
Entry<K,V> parent;
@ -553,14 +576,12 @@ public class TreeMap<K,V>
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();
} else {
Objects.requireNonNull(key);
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
@ -569,20 +590,271 @@ public class TreeMap<K,V>
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
return t.value;
} while (t != null);
}
newValue = callMappingFunctionWithCheck(key, mappingFunction);
if (newValue != null) {
addEntry(key, newValue, parent, cmp < 0);
return newValue;
}
return null;
}
/**
* {@inheritDoc}
*
* <p>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<? super K, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
Entry<K,V> oldEntry = getEntry(key);
if (oldEntry != null && oldEntry.value != null) {
return remapValue(oldEntry, key, remappingFunction);
} else {
return null;
}
}
/**
* {@inheritDoc}
*
* <p>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<? super K, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
V newValue;
Entry<K,V> t = root;
if (t == null) {
newValue = callRemappingFunctionWithCheck(key, null, remappingFunction);
if (newValue != null) {
addEntryToEmptyMap(key, newValue);
return newValue;
} else {
return null;
}
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> 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 {
Objects.requireNonNull(key);
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return remapValue(t, key, remappingFunction);
} while (t != null);
}
newValue = callRemappingFunctionWithCheck(key, null, remappingFunction);
if (newValue != null) {
addEntry(key, newValue, parent, cmp < 0);
return newValue;
}
return null;
}
/**
* {@inheritDoc}
*
* <p>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<? super V, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
Objects.requireNonNull(value);
Entry<K,V> t = root;
if (t == null) {
addEntryToEmptyMap(key, value);
return value;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> 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 {
Objects.requireNonNull(key);
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) 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, parent, cmp < 0);
return value;
}
private V callMappingFunctionWithCheck(K key, Function<? super K, ? extends V> mappingFunction) {
int mc = modCount;
V newValue = mappingFunction.apply(key);
if (mc != modCount) {
throw new ConcurrentModificationException();
}
return newValue;
}
private V callRemappingFunctionWithCheck(K key, V oldValue, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
int mc = modCount;
V newValue = remappingFunction.apply(key, oldValue);
if (mc != modCount) {
throw new ConcurrentModificationException();
}
return newValue;
}
private void addEntry(K key, V value, Entry<K, V> parent, boolean addToLeft) {
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
if (addToLeft)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
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<K,V> t = root;
if (t == null) {
addEntryToEmptyMap(key, value);
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> 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 {
Objects.requireNonNull(key);
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) 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, parent, cmp < 0);
return null;
}
private V remapValue(Entry<K,V> t, K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
V newValue = callRemappingFunctionWithCheck(key, t.value, remappingFunction);
if (newValue == null) {
deleteEntry(t);
return null;
} else {
// replace old mapping
t.value = newValue;
return newValue;
}
}
private V mergeValue(Entry<K,V> t, V value, BiFunction<? super V, ? super V, ? extends V> 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.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2020, 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.Hashtable;
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 @@ public class FunctionalCMEs {
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();

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2013, 2020, 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 @@ public class InPlaceOpsCollisions extends MapWithCollisionsProviders {
String.format("map expected size m%d != k%d", map.size(), keys.length));
}
@Test(dataProvider = "nullValueFriendlyMaps")
void testPutIfAbsentOverwriteNull(String desc, Supplier<Map<Object, Object>> ms) {
Map<Object, Object> 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<Map<Object, Object>> ms, Object val) {
Map<Object, Object> map = ms.get();
@ -496,4 +517,13 @@ public class InPlaceOpsCollisions extends MapWithCollisionsProviders {
}
}
@DataProvider
public Iterator<Object[]> nullValueFriendlyMaps() {
return Arrays.asList(
new Object[]{"HashMap", (Supplier<Map<?, ?>>) HashMap::new},
new Object[]{"LinkedHashMap", (Supplier<Map<?, ?>>) LinkedHashMap::new},
new Object[]{"TreeMap", (Supplier<Map<?, ?>>) TreeMap::new},
new Object[]{"TreeMap(cmp)", (Supplier<Map<?, ?>>) () -> new TreeMap<>(Comparator.reverseOrder())}
).iterator();
}
}

View File

@ -0,0 +1,151 @@
/*
* Copyright (c) 2020, 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.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@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 comparator;
@Param({"true", "false"})
public boolean preFill;
@Param({"0"})
public long seed;
private Supplier<TreeMap<Integer, Integer>> supplier;
private Integer[] keys;
@Setup
public void setUp() {
supplier = comparator ? () -> new TreeMap<>(Comparator.reverseOrder()) : TreeMap::new;
keys = IntStream.range(0, size).boxed().toArray(Integer[]::new);
Random rnd = seed == 0 ? new Random() : new Random(seed);
Collections.shuffle(Arrays.asList(keys, rnd));
if (preFill) {
TreeMap<Integer, Integer> template = Arrays.stream(keys)
.collect(Collectors.toMap(Function.identity(), Function.identity(), (a, b) -> a, supplier));
supplier = () -> new TreeMap<>(template);
}
}
@Benchmark
public Map<Integer, Integer> baseline() {
// Just create map (empty or pre-filled)
return supplier.get();
}
@Benchmark
public Map<Integer, Integer> put(Blackhole bh) {
Map<Integer, Integer> map = supplier.get();
Integer[] keys = this.keys;
for (Integer key : keys) {
bh.consume(map.put(key, key));
}
return map;
}
@Benchmark
public Map<Integer, Integer> putIfAbsent(Blackhole bh) {
Map<Integer, Integer> map = supplier.get();
Integer[] keys = this.keys;
for (Integer key : keys) {
bh.consume(map.putIfAbsent(key, key));
}
return map;
}
@Benchmark
public Map<Integer, Integer> computeIfAbsent(Blackhole bh) {
Map<Integer, Integer> map = supplier.get();
Integer[] keys = this.keys;
for (Integer key : keys) {
bh.consume(map.computeIfAbsent(key, k -> k));
}
return map;
}
@Benchmark
public Map<Integer, Integer> compute(Blackhole bh) {
Map<Integer, Integer> map = supplier.get();
Integer[] keys = this.keys;
for (Integer key : keys) {
bh.consume(map.compute(key, (k, old) -> k));
}
return map;
}
@Benchmark
public Map<Integer, Integer> computeIfPresent(Blackhole bh) {
Map<Integer, Integer> map = supplier.get();
Integer[] keys = this.keys;
for (Integer key : keys) {
bh.consume(map.computeIfPresent(key, (k, old) -> k));
}
return map;
}
@Benchmark
public Map<Integer, Integer> merge(Blackhole bh) {
Map<Integer, Integer> map = supplier.get();
Integer[] keys = this.keys;
for (Integer key : keys) {
bh.consume(map.merge(key, key, (k1, k2) -> k1));
}
return map;
}
}