Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/java.base/share/classes/java/util/Collections.java
Original file line number Diff line number Diff line change
Expand Up @@ -1642,7 +1642,7 @@ public static <K,V> Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m) {
/**
* @serial include
*/
private static class UnmodifiableMap<K,V> implements Map<K,V>, Serializable {
static class UnmodifiableMap<K,V> implements Map<K,V>, Serializable {
@java.io.Serial
private static final long serialVersionUID = -1034234728574286014L;

Expand Down
31 changes: 27 additions & 4 deletions src/java.base/share/classes/java/util/HashMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,18 @@ public HashMap(Map<? extends K, ? extends V> m) {
putMapEntries(m, false);
}

@SuppressWarnings({"unchecked"})
private void putMapEntries(HashMap<? extends K, ? extends V> src, boolean evict) {
if (src.table != null) {
for (Node<? extends K, ? extends V> node : src.table) {
while (node != null) {
putVal(node.hash, node.key, node.value, false, evict);
node = node.next;
}
}
}
}

/**
* Implements Map.putAll and Map constructor.
*
Expand All @@ -501,6 +513,11 @@ public HashMap(Map<? extends K, ? extends V> m) {
* true (relayed to method afterNodeInsertion).
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
if (m.getClass() == Collections.UnmodifiableMap.class) {
@SuppressWarnings("unchecked")
Map<? extends K, ? extends V> unwrapped = ((Collections.UnmodifiableMap<K, V>) m).m;
m = unwrapped;
}
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
Expand All @@ -517,10 +534,16 @@ final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
resize();
}

for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
if (m.getClass() == HashMap.class) {
@SuppressWarnings("unchecked")
HashMap<K, V> hashMap = (HashMap<K, V>) m;
putMapEntries(hashMap, evict);
} else {
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
}
Expand Down
46 changes: 46 additions & 0 deletions test/jdk/java/util/Collection/MOAT.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ public static void realMain(String[] args) {
testImmutableSet(AccessFlag.maskToAccessFlags(Modifier.PUBLIC | Modifier.STATIC | Modifier.SYNCHRONIZED, AccessFlag.Location.METHOD), AccessFlag.ABSTRACT);
testImmutableList(unmodifiableList(Arrays.asList(1,2,3)));
testImmutableMap(unmodifiableMap(Collections.singletonMap(1,2)));
testImmutableMap(unmodifiableMap(new HashMap<>(Map.of(1, 101, 2, 202, 3, 303))));
testImmutableSeqColl(unmodifiableSequencedCollection(Arrays.asList(1,2,3)), 99);
testImmutableSeqColl(unmodifiableSequencedSet(new LinkedHashSet<>(Arrays.asList(1,2,3))), 99);
var lhm = new LinkedHashMap<Integer,Integer>(); lhm.put(1,2); lhm.put(3, 4);
Expand All @@ -157,6 +158,8 @@ public static void realMain(String[] args) {
testMapMutatorsAlwaysThrow(unmodifiableMap(Collections.emptyMap()));
testEmptyMapMutatorsAlwaysThrow(unmodifiableMap(Collections.emptyMap()));

testHashMapPutAll();

// Empty collections
final List<Integer> emptyArray = Arrays.asList(new Integer[]{});
testCollection(emptyArray);
Expand Down Expand Up @@ -419,6 +422,34 @@ public static void realMain(String[] args) {
testMapMutatorsAlwaysThrow(mapCollected2);
}

// Test HashMap.putAll() optimization paths
private static void testHashMapPutAll() {
Map<Integer,Integer> testData = Map.of(1, 101, 2, 202, 3, 303);
HashMap<Integer,Integer> target = new HashMap<>();

target.putAll(new HashMap<>(testData));
equal(target.size(), testData.size());
check(target.equals(testData));

target.clear();

target.putAll(new TreeMap<>(testData));
equal(target.size(), testData.size());
check(target.equals(testData));

target.clear();

target.putAll(unmodifiableMap(new HashMap<>(testData)));
equal(target.size(), testData.size());
check(target.equals(testData));

target.clear();

target.putAll(unmodifiableMap(new TreeMap<>(testData)));
equal(target.size(), testData.size());
check(target.equals(testData));
}

private static void checkContainsSelf(Collection<Integer> c) {
check(c.containsAll(c));
check(c.containsAll(Arrays.asList(c.toArray())));
Expand Down Expand Up @@ -715,6 +746,11 @@ private static void testImmutableMap(final Map<Integer,Integer> m) {
() -> m.remove(first),
() -> m.clear());
testImmutableMapEntry(m.entrySet().iterator().next());

// Test putAll from immutable map to HashMap
HashMap<Integer,Integer> target = new HashMap<>();
target.putAll(m);
check(target.equals(m));
}
testImmutableSet(m.keySet(), 99);
testImmutableCollection(m.values(), 99);
Expand Down Expand Up @@ -1423,6 +1459,16 @@ private static void testMap(Map<Integer,Integer> m) {
check(m.size() == 2);
checkFunctionalInvariants(m);
checkNPEConsistency(m);

// Test putAll with HashMap
HashMap<Integer,Integer> source = new HashMap<>();
source.put(1, 101);
source.put(2, 202);
source.put(3, 303);

m.clear();
m.putAll(source);
check(m.equals(source));
}
catch (Throwable t) { unexpected(t); }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package org.openjdk.bench.java.util;

import org.openjdk.jmh.annotations.*;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
* Benchmark comparing HashMap constructor performance against manual iteration.
*
* Tests HashMap.<init>(Map) performance across different source map types, with and without
* call site poisoning to simulate real-world megamorphic conditions.
*
* The setup poisons polymorphic call sites by using five different map types
* in both the constructor and manual iteration patterns to ensure megamorphic behavior.
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 1, jvmArgs = {"-XX:+UseParallelGC", "-Xmx3g"})
public class HashMapConstructorBenchmark {

private static final int POISON_ITERATIONS = 40000;

@Param({"0", "5", "25"})
private int mapSize;

@Param({"true", "false"})
private boolean poisonCallSites;

@Param({"HashMap", "TreeMap", "ConcurrentHashMap", "UnmodifiableMap(HashMap)", "UnmodifiableMap(TreeMap)"})
private String inputType;

private HashMap<String, Integer> inputHashMap;
private TreeMap<String, Integer> inputTreeMap;
private LinkedHashMap<String, Integer> inputLinkedHashMap;
private ConcurrentHashMap<String, Integer> inputConcurrentHashMap;
private WeakHashMap<String, Integer> inputWeakHashMap;
private Map<String, Integer> inputUnmodifiableMap;
private Map<String, Integer> inputUnmodifiableTreeMap;

private Map<String, Integer> sourceMap;

@Setup(Level.Trial)
public void setup() {
// Create test data with identical contents
inputHashMap = new HashMap<>();
inputTreeMap = new TreeMap<>();
inputLinkedHashMap = new LinkedHashMap<>();
inputConcurrentHashMap = new ConcurrentHashMap<>();
inputWeakHashMap = new WeakHashMap<>();

for (int i = 0; i < mapSize; i++) {
String key = "key" + i;
Integer value = i;
inputHashMap.put(key, value);
inputTreeMap.put(key, value);
inputLinkedHashMap.put(key, value);
inputConcurrentHashMap.put(key, value);
inputWeakHashMap.put(key, value);
}

// Create wrapper maps for poisoning
inputUnmodifiableMap = Collections.unmodifiableMap(new HashMap<>(inputHashMap));
inputUnmodifiableTreeMap = Collections.unmodifiableMap(new TreeMap<>(inputTreeMap));

// Set source map based on inputType parameter
sourceMap = switch (inputType) {
case "HashMap" -> inputHashMap;
case "TreeMap" -> inputTreeMap;
case "ConcurrentHashMap" -> inputConcurrentHashMap;
case "UnmodifiableMap(HashMap)" -> inputUnmodifiableMap;
case "UnmodifiableMap(TreeMap)" -> inputUnmodifiableTreeMap;
default -> throw new IllegalArgumentException("Unknown inputType: " + inputType);
};

if (poisonCallSites) {
poisonCallSites();
}
}

private void poisonCallSites() {
@SuppressWarnings("unchecked")
Map<String, Integer>[] sources = new Map[] { inputHashMap, inputTreeMap, inputLinkedHashMap,
inputConcurrentHashMap, inputWeakHashMap };

// Poison HashMap.<init>(Map) call site
for (int i = 0; i < POISON_ITERATIONS; i++) {
Map<String, Integer> source = sources[i % sources.length];
HashMap<String, Integer> temp = new HashMap<>(source);
if (temp.size() != mapSize)
throw new RuntimeException();
}

// Poison entrySet iteration call sites
for (int i = 0; i < POISON_ITERATIONS; i++) {
Map<String, Integer> source = sources[i % sources.length];
HashMap<String, Integer> temp = new HashMap<>(source.size());
for (Map.Entry<String, Integer> entry : source.entrySet()) {
temp.put(entry.getKey(), entry.getValue());
}
if (temp.size() != mapSize)
throw new RuntimeException();
}

// Poison UnmodifiableMap call sites
@SuppressWarnings("unchecked")
Map<String, Integer>[] umSources = new Map[]{
Collections.unmodifiableMap(inputHashMap),
Collections.unmodifiableMap(inputTreeMap),
Collections.unmodifiableMap(inputLinkedHashMap),
Collections.unmodifiableMap(inputConcurrentHashMap),
Collections.unmodifiableMap(inputWeakHashMap)
};

for (int i = 0; i < POISON_ITERATIONS; i++) {
Map<String, Integer> source = umSources[i % umSources.length];
HashMap<String, Integer> temp = new HashMap<>(source);
if (temp.size() != mapSize)
throw new RuntimeException();
}

for (int i = 0; i < POISON_ITERATIONS; i++) {
Map<String, Integer> source = umSources[i % umSources.length];
HashMap<String, Integer> temp = new HashMap<>(source.size());
for (Map.Entry<String, Integer> entry : source.entrySet()) {
temp.put(entry.getKey(), entry.getValue());
}
if (temp.size() != mapSize) throw new RuntimeException();
}
}

/**
* Benchmark using HashMap's built-in constructor that takes a Map parameter.
* Performance varies based on source map type and call site polymorphism.
*/
@Benchmark
public HashMap<String, Integer> hashMapConstructor() {
return new HashMap<>(sourceMap);
}

/**
* Benchmark using manual iteration over entrySet with individual put() calls.
* This approach bypasses bulk operations and their polymorphic call sites.
*/
@Benchmark
public HashMap<String, Integer> manualEntrySetLoop() {
HashMap<String, Integer> result = new HashMap<>();
for (Map.Entry<String, Integer> entry : sourceMap.entrySet()) {
result.put(entry.getKey(), entry.getValue());
}
return result;
}
}