8199318: add idempotent copy operation for Map.Entry

Reviewed-by: alanb, psandoz, dfuchs
This commit is contained in:
Stuart Marks 2021-06-04 17:13:05 +00:00
parent b27599b3ec
commit cd0678fcf6
3 changed files with 139 additions and 21 deletions
src/java.base/share/classes/java/util
test/jdk/java/util/Map

@ -592,8 +592,11 @@ public abstract class AbstractMap<K,V> implements Map<K,V> {
/**
* An Entry maintaining a key and a value. The value may be
* changed using the {@code setValue} method. This class
* facilitates the process of building custom map
* changed using the {@code setValue} method. Instances of
* this class are not associated with any map's entry-set view.
*
* @apiNote
* This class facilitates the process of building custom map
* implementations. For example, it may be convenient to return
* arrays of {@code SimpleEntry} instances in method
* {@code Map.entrySet().toArray}.
@ -725,10 +728,21 @@ public abstract class AbstractMap<K,V> implements Map<K,V> {
}
/**
* An Entry maintaining an immutable key and value. This class
* does not support method {@code setValue}. This class may be
* convenient in methods that return thread-safe snapshots of
* key-value mappings.
* An unmodifiable Entry maintaining a key and a value. This class
* does not support the {@code setValue} method. Instances of
* this class are not associated with any map's entry-set view.
*
* @apiNote
* Instances of this class are not necessarily immutable, as the key
* and value may be mutable. An instance of <i>this specific class</i>
* is unmodifiable, because the key and value references cannot be
* changed. A reference of this <i>type</i> may not be unmodifiable,
* as a subclass may be modifiable or may provide the appearance of modifiability.
* <p>
* This class may be convenient in methods that return thread-safe snapshots of
* key-value mappings. For alternatives, see the
* {@link Map#entry Map::entry} and {@link Map.Entry#copyOf Map.Entry::copyOf}
* methods.
*
* @since 1.6
*/
@ -788,7 +802,10 @@ public abstract class AbstractMap<K,V> implements Map<K,V> {
* Replaces the value corresponding to this entry with the specified
* value (optional operation). This implementation simply throws
* {@code UnsupportedOperationException}, as this class implements
* an <i>immutable</i> map entry.
* an unmodifiable map entry.
*
* @implSpec
* The implementation in this class always throws {@code UnsupportedOperationException}.
*
* @param value new value to be stored in this entry
* @return (Does not return)

@ -393,14 +393,33 @@ public interface Map<K, V> {
Set<Map.Entry<K, V>> entrySet();
/**
* A map entry (key-value pair). The {@code Map.entrySet} method returns
* a collection-view of the map, whose elements are of this class. The
* <i>only</i> way to obtain a reference to a map entry is from the
* iterator of this collection-view. These {@code Map.Entry} objects are
* valid <i>only</i> for the duration of the iteration; more formally,
* the behavior of a map entry is undefined if the backing map has been
* modified after the entry was returned by the iterator, except through
* the {@code setValue} operation on the map entry.
* A map entry (key-value pair). The Entry may be unmodifiable, or the
* value may be modifiable if the optional {@code setValue} method is
* implemented. The Entry may be independent of any map, or it may represent
* an entry of the entry-set view of a map.
* <p>
* Instances of the {@code Map.Entry} interface may be obtained by iterating
* the entry-set view of a map. These instances maintain a connection to the
* original, backing map. This connection to the backing map is valid
* <i>only</i> for the duration of iteration over the entry-set view.
* During iteration of the entry-set view, if supported by the backing map,
* a change to a {@code Map.Entry}'s value via the
* {@link Map.Entry#setValue setValue} method will be visible in the backing map.
* The behavior of such a {@code Map.Entry} instance is undefined outside of
* iteration of the map's entry-set view. It is also undefined if the backing
* map has been modified after the {@code Map.Entry} was returned by the
* iterator, except through the {@code Map.Entry.setValue} method. In particular,
* a change to the value of a mapping in the backing map might or might not be
* visible in the corresponding {@code Map.Entry} element of the entry-set view.
*
* @apiNote
* It is possible to create a {@code Map.Entry} instance that is disconnected
* from a backing map by using the {@link Map.Entry#copyOf copyOf} method. For example,
* the following creates a snapshot of a map's entries that is guaranteed not to
* change even if the original map is modified:
* <pre> {@code
* var entries = map.entrySet().stream().map(Map.Entry::copyOf).toList()
* }</pre>
*
* @see Map#entrySet()
* @since 1.2
@ -559,6 +578,37 @@ public interface Map<K, V> {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
}
/**
* Returns a copy of the given {@code Map.Entry}. The returned instance is not
* associated with any map. The returned instance has the same characteristics
* as instances returned by the {@link Map#entry Map::entry} method.
*
* @apiNote
* An instance obtained from a map's entry-set view has a connection to that map.
* The {@code copyOf} method may be used to create a {@code Map.Entry} instance,
* containing the same key and value, that is independent of any map.
*
* @implNote
* If the given entry was obtained from a call to {@code copyOf} or {@code Map::entry},
* calling {@code copyOf} will generally not create another copy.
*
* @param <K> the type of the key
* @param <V> the type of the value
* @param e the entry to be copied
* @return a map entry equal to the given entry
* @throws NullPointerException if e is null or if either of its key or value is null
* @since 17
*/
@SuppressWarnings("unchecked")
public static <K, V> Map.Entry<K, V> copyOf(Map.Entry<? extends K, ? extends V> e) {
Objects.requireNonNull(e);
if (e instanceof KeyValueHolder) {
return (Map.Entry<K, V>) e;
} else {
return Map.entry(e.getKey(), e.getValue());
}
}
}
// Comparison and hashing

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2021, 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
@ -34,7 +34,6 @@ import java.util.Iterator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.testng.annotations.DataProvider;
@ -45,8 +44,8 @@ import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotEquals;
import static org.testng.Assert.assertNotSame;
import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertThrows;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
/*
* @test
@ -470,7 +469,7 @@ public class MapFactories {
Map<Integer, String> copy = Map.copyOf(map);
}
// Map.entry() tests
// Map::entry tests
@Test(expectedExceptions=NullPointerException.class)
public void entryWithNullKeyDisallowed() {
@ -482,6 +481,12 @@ public class MapFactories {
Map.Entry<Integer,String> e = Map.entry(0, null);
}
@Test
public void entrySetValueDisallowed() {
var e = Map.entry("a", "b");
assertThrows(UnsupportedOperationException.class, () -> e.setValue("x"));
}
@Test
public void entryBasicTests() {
Map.Entry<String,String> kvh1 = Map.entry("xyzzy", "plugh");
@ -492,8 +497,54 @@ public class MapFactories {
assertTrue(sie.equals(kvh1));
assertFalse(kvh2.equals(sie));
assertFalse(sie.equals(kvh2));
assertEquals(sie.hashCode(), kvh1.hashCode());
assertEquals(sie.toString(), kvh1.toString());
assertEquals(kvh1.hashCode(), sie.hashCode());
assertEquals(kvh1.toString(), sie.toString());
}
// Map.Entry::copyOf tests
@Test(expectedExceptions=NullPointerException.class)
public void entryCopyNullDisallowed() {
Map.Entry.copyOf(null);
}
@Test
public void entryCopyWithNullKeyDisallowed() {
var e = new AbstractMap.SimpleEntry<>(null, "b");
assertThrows(NullPointerException.class, () -> Map.Entry.copyOf(e));
}
@Test
public void entryCopyWithNullValueDisallowed() {
var e = new AbstractMap.SimpleEntry<>("a", null);
assertThrows(NullPointerException.class, () -> Map.Entry.copyOf(e));
}
@Test
public void entryCopySetValueDisallowed() {
var e = new AbstractMap.SimpleEntry<>("a", "b");
var c = Map.Entry.copyOf(e);
assertThrows(UnsupportedOperationException.class, () -> c.setValue("x"));
}
@Test
public void entryCopyBasicTests() {
Map.Entry<String,String> orig = new AbstractMap.SimpleImmutableEntry<>("xyzzy", "plugh");
Map.Entry<String,String> copy1 = Map.Entry.copyOf(orig);
Map.Entry<String,String> copy2 = Map.Entry.copyOf(copy1);
assertEquals(orig, copy1);
assertEquals(copy1, orig);
assertEquals(orig, copy2);
assertEquals(copy2, orig);
assertEquals(copy1, copy2);
assertEquals(copy2, copy1);
assertNotSame(orig, copy1);
assertSame(copy1, copy2);
assertEquals(copy1.hashCode(), orig.hashCode());
assertEquals(copy1.toString(), orig.toString());
}
// compile-time test of wildcards