Java 17: Map.Entry.copyOf

Updated:

logo

Java 17 introduces a copyOf function for the Map.Entry object.

The goal is to generate a new object from a map entry that contains the same value of the original object but without reference to the map and without undefined behaviours.

mapentry

This should simplify our work when we work with data extracted from maps, reducing the risk of surprises working with Map.Entry. As documented, with Map.Entry obtained during the iteration of a Map, the behavior of such a Map.Entry instance is undefined outside of iteration of the map's entry-set view.

To use .copyOf you need to call the static method in Map.Entry, example:

Map.Entry<Integer, String> copyOfTheEntry = Map.Entry.copyOf(simpleMap.entrySet().stream().toList().get(0));

Here you can see the complete example.

@Test 
public void testMapEntry_copyOf() { 
  // we create a simple map to test the feature introduced in Java 17 
  Map<Integer, String> simpleMap = new HashMap<>(); 
  simpleMap.put(1, "Initial value of the entry"); 
 
  // we extract the new entry from the map 
  Map.Entry<Integer, String> originalEntry = simpleMap.entrySet().stream().toList().get(0); 
 
  // the entry is just a reference to the one in the map 
  assertEquals(originalEntry, simpleMap.entrySet().stream().toList().get(0)); 
  assertTrue(originalEntry == simpleMap.entrySet().stream().toList().get(0)); 
 
  // now we create a copy of the new entry in the map 
  Map.Entry<Integer, String> copyOfTheEntry = Map.Entry.copyOf(simpleMap.entrySet().stream().toList().get(0)); 
 
  // we check if the copy and the entry are the same 
  // for MapEntry only the Key and the Value are evaluated 
  assertEquals(copyOfTheEntry, originalEntry); 
  // ... but the objects are different 
  assertFalse(copyOfTheEntry == originalEntry); 
 
  // now we update the entry in the map 
  simpleMap.put(1, "The entry value has changed"); 
 
  // the two entries are not more equals, originalEntry is only a reference to the value in the map 
  assertNotEquals(copyOfTheEntry, originalEntry); 
 
  // now we copy the initial value in the original entry, the value of the map is modified too 
  originalEntry.setValue("Initial value of the entry"); 
  assertEquals(copyOfTheEntry, originalEntry); 
} 

.get() gives us a reference, .copyOf() gives us a new object

The ‘issue’ with .get() is that we get only a reference of the original object that is still in the Map. If a developer starts to manipulate the object received, he will update the original list. On the other side, if he wants to work with the data of the object he has to copy the values in a new object, this requires one or more extra steps. With a separate object the Runtime can optimise the memory in case the original collection is not required anymore.

.equals(): the values are ‘the same’

The Map.Entry created with copyOf has the same characteristics of the original object in the Map for this reason if you try to compare the two objects with .equals() you will receive a true as result.

Here the code of equals().

public boolean equals(Object o) { 
  Object k, v; Map.Entry<?,?> e; 
  return ((o instanceof Map.Entry) && 
    (k = (e = (Map.Entry<?,?>)o).getKey()) != null && 
    (v = e.getValue()) != null && 
    (k == key || k.equals(key)) && 
    (v == val || v.equals(val))); 
} 

To compare if the objects are the same you can compare their instance with ==. With this you can verify that copyOf() generates a new instance of the object.

… and the classes … small surprise … the copy is immutable

Can we update the copy of the original object? The answer is no.

// which objects are we using? 
assertEquals("java.util.HashMap$Node", originalEntry.getClass().getName()); 
assertEquals("java.util.KeyValueHolder", copyOfTheEntry.getClass().getName()); 

When we copy the obejct in the map we receive a KeyValueHolder this object is immutable.

In our case the Map.entry() method is called and a KeyValueHolder is returned.

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()); // <- this is our case 
  } 
} 

If we try to update the value of our ‘copy object’ we will receive the following error message

java.lang.UnsupportedOperationException: not supported 
at java.base/java.util.KeyValueHolder.setValue(KeyValueHolder.java:93) 

Create a snapshot of a Map

This example comes from the official Java documentation, it is possible to iterate a Map and generate a ‘snapshot’ using copyOf:

var entries = map.entrySet().stream().map(Map.Entry::copyOf).toList() 

WebApp built by Marco using SpringBoot 3.2.4 and Java 21, in a Server in Switzerland