Skip to content

Commit 6d155a4

Browse files
author
Stuart Marks
committed
8308167: SequencedMap::firstEntry throws NPE when first entry has null key or value
Reviewed-by: bchristi
1 parent 4b15349 commit 6d155a4

File tree

4 files changed

+214
-6
lines changed

4 files changed

+214
-6
lines changed

src/java.base/share/classes/java/util/SequencedMap.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525

2626
package java.util;
2727

28+
import jdk.internal.util.NullableKeyValueHolder;
29+
2830
/**
2931
* A Map that has a well-defined encounter order, that supports operations at both ends, and
3032
* that is reversible. The <a href="SequencedCollection.html#encounter">encounter order</a>
@@ -148,7 +150,7 @@ public interface SequencedMap<K, V> extends Map<K, V> {
148150
*/
149151
default Map.Entry<K,V> firstEntry() {
150152
var it = entrySet().iterator();
151-
return it.hasNext() ? Map.Entry.copyOf(it.next()) : null;
153+
return it.hasNext() ? new NullableKeyValueHolder<>(it.next()) : null;
152154
}
153155

154156
/**
@@ -165,7 +167,7 @@ default Map.Entry<K,V> firstEntry() {
165167
*/
166168
default Map.Entry<K,V> lastEntry() {
167169
var it = reversed().entrySet().iterator();
168-
return it.hasNext() ? Map.Entry.copyOf(it.next()) : null;
170+
return it.hasNext() ? new NullableKeyValueHolder<>(it.next()) : null;
169171
}
170172

171173
/**
@@ -185,7 +187,7 @@ default Map.Entry<K,V> lastEntry() {
185187
default Map.Entry<K,V> pollFirstEntry() {
186188
var it = entrySet().iterator();
187189
if (it.hasNext()) {
188-
var entry = Map.Entry.copyOf(it.next());
190+
var entry = new NullableKeyValueHolder<>(it.next());
189191
it.remove();
190192
return entry;
191193
} else {
@@ -210,7 +212,7 @@ default Map.Entry<K,V> pollFirstEntry() {
210212
default Map.Entry<K,V> pollLastEntry() {
211213
var it = reversed().entrySet().iterator();
212214
if (it.hasNext()) {
213-
var entry = Map.Entry.copyOf(it.next());
215+
var entry = new NullableKeyValueHolder<>(it.next());
214216
it.remove();
215217
return entry;
216218
} else {
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation. Oracle designates this
8+
* particular file as subject to the "Classpath" exception as provided
9+
* by Oracle in the LICENSE file that accompanied this code.
10+
*
11+
* This code is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
* version 2 for more details (a copy is included in the LICENSE file that
15+
* accompanied this code).
16+
*
17+
* You should have received a copy of the GNU General Public License version
18+
* 2 along with this work; if not, write to the Free Software Foundation,
19+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
*
21+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
* or visit www.oracle.com if you need additional information or have any
23+
* questions.
24+
*/
25+
26+
package jdk.internal.util;
27+
28+
import java.util.Map;
29+
import java.util.Objects;
30+
31+
/**
32+
* An immutable container for a key and a value, both of which are nullable.
33+
*
34+
* <p>This is a <a href="{@docRoot}/java.base/java/lang/doc-files/ValueBased.html">value-based</a>
35+
* class; programmers should treat instances that are
36+
* {@linkplain #equals(Object) equal} as interchangeable and should not
37+
* use instances for synchronization, or unpredictable behavior may
38+
* occur. For example, in a future release, synchronization may fail.
39+
*
40+
* @apiNote
41+
* This class is not exported. Instances are created by various Map implementations
42+
* when they need a Map.Entry that isn't connected to the Map.
43+
*
44+
* <p>This class differs from AbstractMap.SimpleImmutableEntry in that it is not
45+
* serializable and that it is final. This class differs from java.util.KeyValueHolder
46+
* in that the key and value are nullable.
47+
*
48+
* <p>In principle this class could be a variation on KeyValueHolder. However,
49+
* making that class selectively support nullable keys and values is quite intricate.
50+
* Various specifications (such as Map.ofEntries and Map.entry) specify non-nullability
51+
* of the key and the value. Map.Entry.copyOf also requires non-null keys and values;
52+
* but it simply passes through KeyValueHolder instances, assuming their keys and values
53+
* are non-nullable. If a KVH with nullable keys and values were introduced, some way
54+
* to distinguish it would be necessary. This could be done by introducing a subclass
55+
* (requiring KVH to be made non-final) or by introducing some kind of "mode" field
56+
* (potentially increasing the size of every KVH instance, though another field could
57+
* probably fit into the object's padding in most JVMs.) More critically, a mode field
58+
* would have to be checked in all the right places to get the right behavior.
59+
*
60+
* <p>A longer range possibility is to selectively relax the restrictions against nulls in
61+
* Map.entry and Map.Entry.copyOf. This would also require some intricate specification
62+
* changes and corresponding implementation changes (e.g., the implementations backing
63+
* Map.of might still need to reject nulls, and so would Map.ofEntries) but allowing
64+
* a Map.Entry itself to contain nulls seems beneficial in general. If this is done,
65+
* merging KeyValueHolder and NullableKeyValueHolder should be reconsidered.
66+
*
67+
* @param <K> the key type
68+
* @param <V> the value type
69+
*/
70+
@jdk.internal.ValueBased
71+
public final class NullableKeyValueHolder<K,V> implements Map.Entry<K,V> {
72+
final K key;
73+
final V value;
74+
75+
/**
76+
* Constructs a NullableKeyValueHolder.
77+
*
78+
* @param k the key, may be null
79+
* @param v the value, may be null
80+
*/
81+
public NullableKeyValueHolder(K k, V v) {
82+
key = k;
83+
value = v;
84+
}
85+
86+
/**
87+
* Constructs a NullableKeyValueHolder from a Map.Entry. No need for an
88+
* idempotent copy at this time.
89+
*
90+
* @param entry the entry, must not be null
91+
*/
92+
public NullableKeyValueHolder(Map.Entry<K,V> entry) {
93+
Objects.requireNonNull(entry);
94+
key = entry.getKey();
95+
value = entry.getValue();
96+
}
97+
98+
/**
99+
* Gets the key from this holder.
100+
*
101+
* @return the key, may be null
102+
*/
103+
@Override
104+
public K getKey() {
105+
return key;
106+
}
107+
108+
/**
109+
* Gets the value from this holder.
110+
*
111+
* @return the value, may be null
112+
*/
113+
@Override
114+
public V getValue() {
115+
return value;
116+
}
117+
118+
/**
119+
* Throws {@link UnsupportedOperationException}.
120+
*
121+
* @param value ignored
122+
* @return never returns normally
123+
*/
124+
@Override
125+
public V setValue(V value) {
126+
throw new UnsupportedOperationException("not supported");
127+
}
128+
129+
/**
130+
* Compares the specified object with this entry for equality.
131+
* Returns {@code true} if the given object is also a map entry and
132+
* the two entries' keys and values are equal.
133+
*/
134+
@Override
135+
public boolean equals(Object o) {
136+
return o instanceof Map.Entry<?, ?> e
137+
&& Objects.equals(key, e.getKey())
138+
&& Objects.equals(value, e.getValue());
139+
}
140+
141+
private int hash(Object obj) {
142+
return (obj == null) ? 0 : obj.hashCode();
143+
}
144+
145+
/**
146+
* Returns the hash code value for this map entry. The hash code
147+
* is {@code key.hashCode() ^ value.hashCode()}.
148+
*/
149+
@Override
150+
public int hashCode() {
151+
return hash(key) ^ hash(value);
152+
}
153+
154+
/**
155+
* Returns a String representation of this map entry. This
156+
* implementation returns the string representation of this
157+
* entry's key followed by the equals character ("{@code =}")
158+
* followed by the string representation of this entry's value.
159+
*
160+
* @return a String representation of this map entry
161+
*/
162+
@Override
163+
public String toString() {
164+
return key + "=" + value;
165+
}
166+
}

test/jdk/java/util/AbstractMap/SimpleEntries.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@
2323

2424
/*
2525
* @test
26-
* @bug 4904074 6328220 6330389
27-
* @summary Basic tests for SimpleEntry, SimpleImmutableEntry
26+
* @bug 4904074 6328220 6330389 8308167
27+
* @modules java.base/jdk.internal.util
28+
* @summary Basic tests for several Map.Entry implementations
2829
* @author Martin Buchholz
2930
*/
3031

3132
import java.util.Map;
33+
import jdk.internal.util.NullableKeyValueHolder;
3234

3335
import static java.util.AbstractMap.SimpleEntry;
3436
import static java.util.AbstractMap.SimpleImmutableEntry;
@@ -40,8 +42,12 @@ public class SimpleEntries {
4042
private static void realMain(String[] args) throws Throwable {
4143
testEntry(new SimpleEntry<String,Long>(k,v));
4244
testEntry(new SimpleImmutableEntry<String,Long>(k,v));
45+
testEntry(Map.entry(k,v));
46+
testEntry(Map.Entry.copyOf(Map.entry(k,v)));
47+
testEntry(new NullableKeyValueHolder(k,v));
4348
testNullEntry(new SimpleEntry<String,Long>(null,null));
4449
testNullEntry(new SimpleImmutableEntry<String,Long>(null,null));
50+
testNullEntry(new NullableKeyValueHolder(null,null));
4551
}
4652

4753
private static void testEntry(Map.Entry<String,Long> e) {
@@ -52,6 +58,7 @@ private static void testEntry(Map.Entry<String,Long> e) {
5258
check(! e.equals(null));
5359
equal(e, new SimpleImmutableEntry<String,Long>(k,v));
5460
equal(e.toString(), k+"="+v);
61+
check(e.hashCode() == 101575); // hash("foo") ^ hash(1L)
5562
if (e instanceof SimpleEntry) {
5663
equal(e.setValue(v2), v);
5764
equal(e.getValue(), v2);
@@ -70,6 +77,7 @@ private static void testNullEntry(Map.Entry<String,Long> e) {
7077
equal(e, new SimpleEntry<String,Long>(null, null));
7178
equal(e, new SimpleImmutableEntry<String,Long>(null, null));
7279
equal(e.toString(), "null=null");
80+
check(e.hashCode() == 0);
7381
if (e instanceof SimpleEntry) {
7482
equal(e.setValue(v), null);
7583
equal(e.getValue(), v);

test/jdk/java/util/SequencedCollection/BasicMap.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,16 @@ public Iterator<Object[]> viewAddThrows() {
258258
return cases.iterator();
259259
}
260260

261+
@DataProvider(name="nullableEntries")
262+
public Iterator<Object[]> nullableEntries() {
263+
return Arrays.asList(
264+
new Object[] { "firstEntry" },
265+
new Object[] { "lastEntry" },
266+
new Object[] { "pollFirstEntry" },
267+
new Object[] { "pollLastEntry" }
268+
).iterator();
269+
}
270+
261271
// ========== Assertions ==========
262272

263273
/**
@@ -519,6 +529,11 @@ public void checkChecked(SequencedMap<String, Integer> map) {
519529
assertThrows(CCE, () -> { objMap.reversed().sequencedEntrySet().reversed().getFirst().setValue(new Object()); });
520530
}
521531

532+
public void checkEntry(Map.Entry<String, Integer> entry, String key, Integer value) {
533+
assertEquals(entry.getKey(), key);
534+
assertEquals(entry.getValue(), value);
535+
}
536+
522537
// ========== Tests ==========
523538

524539
@Test(dataProvider="all")
@@ -889,4 +904,21 @@ public void testEntrySetAddThrows(String label,
889904
assertThrows(UOE, () -> entrySet.addFirst(Map.entry("x", 99)));
890905
checkContents(map, baseref);
891906
}
907+
908+
@Test(dataProvider="nullableEntries")
909+
public void testNullableKeyValue(String mode) {
910+
// TODO this relies on LHM to inherit SequencedMap default
911+
// methods which are actually being tested here.
912+
SequencedMap<String, Integer> map = new LinkedHashMap<>();
913+
map.put(null, 1);
914+
map.put("two", null);
915+
916+
switch (mode) {
917+
case "firstEntry" -> checkEntry(map.firstEntry(), null, 1);
918+
case "lastEntry" -> checkEntry(map.lastEntry(), "two", null);
919+
case "pollFirstEntry" -> checkEntry(map.pollFirstEntry(), null, 1);
920+
case "pollLastEntry" -> checkEntry(map.pollLastEntry(), "two", null);
921+
default -> throw new AssertionError("illegal mode " + mode);
922+
}
923+
}
892924
}

0 commit comments

Comments
 (0)