Skip to content

Commit d3c995c

Browse files
cpovirkcopybara-github
authored andcommitted
Improve nullness of types in some APIs related to Map merging, and fix Collectors.toMap null-handling.
- Restrict `Collections.toMap` value-type arguments to non-nullable types. - ...in J2KT, following what [we'd found in JSpecify research](jspecify/jdk@15eda89) - Fix `Collections.toMap` to remove the key in question when `mergeFunction` returns `null`. - ...in J2KT - ...in J2CL - Use `@NonNull` / `& Any` in a few places in `Map.merge` and `Map.computeIfPresent`. - ...in J2KT - ...in Guava `Map` implementations, even though we don't yet include `@NonNull` annotations in the JDK APIs that we build Guava against. (See post-submit discussion on cl/559605577. But I've taken the shortcut of not waiting for the JDK APIs.) - Use `@Nullable` (to match the existing Kotlin `?` types) in the return types of `Map.computeIfPresent` and `Map.compute`. - ...in J2KT - Test a bunch of this. Note that the test for `mergeFunction` has to work around an overly restricted `toMap` signature that J2KT inherited from JSpecify. As discussed in a code comment there, this is fundamentally the same issue as we have in Guava with `ImmutableMap.toImmutableMap`, which is discussed as part of google/guava#6824. PiperOrigin-RevId: 611445633
1 parent 67a00eb commit d3c995c

File tree

4 files changed

+30
-30
lines changed

4 files changed

+30
-30
lines changed

j2kt/jre/java/common/java/util/Map.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import javaemul.internal.annotations.KtName;
2424
import javaemul.internal.annotations.KtNative;
2525
import javaemul.internal.annotations.KtProperty;
26+
import jsinterop.annotations.JsNonNull;
2627
import org.jspecify.nullness.NullMarked;
2728
import org.jspecify.nullness.Nullable;
2829

@@ -71,7 +72,7 @@ Comparator<Map.Entry<K, V>> comparingByValue() {
7172
void clear();
7273

7374
default @Nullable V compute(
74-
K key, BiFunction<? super K, ? super @Nullable V, ? extends V> remappingFunction) {
75+
K key, BiFunction<? super K, ? super @Nullable V, ? extends @Nullable V> remappingFunction) {
7576
return ktNative();
7677
}
7778

@@ -81,7 +82,7 @@ default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunctio
8182

8283
@KtName("java_computeIfPresent")
8384
default @Nullable V computeIfPresent(
84-
K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
85+
K key, BiFunction<? super K, ? super @JsNonNull V, ? extends @Nullable V> remappingFunction) {
8586
return ktNative();
8687
}
8788

@@ -115,7 +116,10 @@ default void forEach(BiConsumer<? super K, ? super V> action) {
115116

116117
@KtName("java_merge")
117118
default @Nullable V merge(
118-
K key, V value, BiFunction<? super V, ? super V, ? extends @Nullable V> remappingFunction) {
119+
K key,
120+
@JsNonNull V value,
121+
BiFunction<? super @JsNonNull V, ? super @JsNonNull V, ? extends @Nullable V>
122+
remappingFunction) {
119123
return ktNative();
120124
}
121125

j2kt/jre/java/common/javaemul/lang/JavaMap.kt

+10-10
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ interface JavaMap<K, V> : MutableMap<K, V> {
4343
fun computeIfAbsent(key: K, mappingFunction: Function<in K, out V>): V =
4444
default_computeIfAbsent(key, mappingFunction)
4545

46-
fun java_computeIfPresent(key: K, remappingFunction: BiFunction<in K, in V, out V?>): V? =
46+
fun java_computeIfPresent(key: K, remappingFunction: BiFunction<in K, in V & Any, out V?>): V? =
4747
default_computeIfPresent(key, remappingFunction)
4848

4949
fun forEach(action: BiConsumer<in K, in V>) = default_forEach(action)
@@ -66,7 +66,7 @@ interface JavaMap<K, V> : MutableMap<K, V> {
6666

6767
// The Java `merge` function exists on Kotlin/JVM but is undocumented. So we rename our `merge` to
6868
// avoid a collision.
69-
fun java_merge(key: K, value: V, remap: BiFunction<in V, in V, out V?>): V? =
69+
fun java_merge(key: K, value: V & Any, remap: BiFunction<in V & Any, in V & Any, out V?>): V? =
7070
default_merge(key, value, remap)
7171

7272
fun java_putAll(t: MutableMap<out K, out V>)
@@ -97,7 +97,7 @@ fun <K, V> MutableMap<K, V>.java_remove(key: Any?): V? = remove(key as K)
9797

9898
fun <K, V> MutableMap<K, V>.java_computeIfPresent(
9999
key: K,
100-
mappingFunction: BiFunction<in K, in V, out V?>
100+
mappingFunction: BiFunction<in K, in V & Any, out V?>,
101101
): V? =
102102
if (this is JavaMap) java_computeIfPresent(key, mappingFunction)
103103
else default_computeIfPresent(key, mappingFunction)
@@ -108,8 +108,8 @@ fun <K, V> MutableMap<K, V>.java_getOrDefault(key: Any?, defaultValue: V?): V? =
108108

109109
fun <K, V> MutableMap<K, V>.java_merge(
110110
key: K,
111-
value: V,
112-
remap: BiFunction<in V, in V, out V?>
111+
value: V & Any,
112+
remap: BiFunction<in V & Any, in V & Any, out V?>,
113113
): V? = if (this is JavaMap) java_merge(key, value, remap) else default_merge(key, value, remap)
114114

115115
fun <K, V> MutableMap<K, V>.java_remove(key: Any?, value: Any?): Boolean =
@@ -121,7 +121,7 @@ internal inline fun <K, V> MutableMap<K, V>.default_forEach(action: BiConsumer<i
121121

122122
internal fun <K, V> MutableMap<K, V>.default_compute(
123123
key: K,
124-
remappingFunction: BiFunction<in K, in V?, out V?>
124+
remappingFunction: BiFunction<in K, in V?, out V?>,
125125
): V? {
126126
val oldValue = this[key]
127127
val newValue = remappingFunction.apply(key, oldValue)
@@ -142,7 +142,7 @@ internal fun <K, V> MutableMap<K, V>.default_compute(
142142

143143
internal fun <K, V> MutableMap<K, V>.default_computeIfAbsent(
144144
key: K,
145-
mappingFunction: Function<in K, out V>
145+
mappingFunction: Function<in K, out V>,
146146
): V {
147147
val oldValue = this[key]
148148
if (oldValue == null) {
@@ -155,7 +155,7 @@ internal fun <K, V> MutableMap<K, V>.default_computeIfAbsent(
155155

156156
private fun <K, V> MutableMap<K, V>.default_computeIfPresent(
157157
key: K,
158-
remappingFunction: BiFunction<in K, in V, out V?>
158+
remappingFunction: BiFunction<in K, in V & Any, out V?>,
159159
): V? {
160160
val oldValue = this[key]
161161
if (oldValue != null) {
@@ -176,8 +176,8 @@ internal fun <K, V> MutableMap<K, V>.default_getOrDefault(key: Any?, defaultValu
176176

177177
private fun <K, V> MutableMap<K, V>.default_merge(
178178
key: K,
179-
value: V,
180-
remap: BiFunction<in V, in V, out V?>
179+
value: V & Any,
180+
remap: BiFunction<in V & Any, in V & Any, out V?>,
181181
): V? {
182182
val oldValue = get(key)
183183
val newValue: V? = if (oldValue == null) value else remap.apply(oldValue, value)

j2kt/jre/java/native/java/util/stream/Collectors.java

+7-15
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ public static Collector<CharSequence,?,String> joining(CharSequence delimiter) {
351351
return toCollection(ArrayList::new);
352352
}
353353

354-
public static <T extends @Nullable Object, K extends @Nullable Object, U extends @Nullable Object>
354+
public static <T extends @Nullable Object, K extends @Nullable Object, U>
355355
Collector<T, ?, Map<K, U>> toMap(
356356
final Function<? super T, ? extends K> keyMapper,
357357
final Function<? super T, ? extends U> valueMapper) {
@@ -363,19 +363,15 @@ public static Collector<CharSequence,?,String> joining(CharSequence delimiter) {
363363
});
364364
}
365365

366-
public static <T extends @Nullable Object, K extends @Nullable Object, U extends @Nullable Object>
366+
public static <T extends @Nullable Object, K extends @Nullable Object, U>
367367
Collector<T, ?, Map<K, U>> toMap(
368368
Function<? super T, ? extends K> keyMapper,
369369
Function<? super T, ? extends U> valueMapper,
370370
BinaryOperator<U> mergeFunction) {
371371
return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
372372
}
373373

374-
public static <
375-
T extends @Nullable Object,
376-
K extends @Nullable Object,
377-
U extends @Nullable Object,
378-
M extends Map<K, U>>
374+
public static <T extends @Nullable Object, K extends @Nullable Object, U, M extends Map<K, U>>
379375
Collector<T, ?, M> toMap(
380376
final Function<? super T, ? extends K> keyMapper,
381377
final Function<? super T, ? extends U> valueMapper,
@@ -385,12 +381,8 @@ public static Collector<CharSequence,?,String> joining(CharSequence delimiter) {
385381
mapSupplier,
386382
(map, item) -> {
387383
K key = keyMapper.apply(item);
388-
U newValue = valueMapper.apply(item);
389-
if (map.containsKey(key)) {
390-
map.put(key, mergeFunction.apply(map.get(key), newValue));
391-
} else {
392-
map.put(key, newValue);
393-
}
384+
U value = valueMapper.apply(item);
385+
map.merge(key, value, mergeFunction);
394386
},
395387
(m1, m2) -> mergeAll(m1, m2, mergeFunction),
396388
Collector.Characteristics.IDENTITY_FINISH);
@@ -418,8 +410,8 @@ D streamAndCollect(Collector<? super T, A, D> downstream, List<T> list) {
418410
return downstream.finisher().apply(a);
419411
}
420412

421-
private static <K extends @Nullable Object, V extends @Nullable Object, M extends Map<K, V>>
422-
M mergeAll(M m1, M m2, BinaryOperator<V> mergeFunction) {
413+
private static <K extends @Nullable Object, V, M extends Map<K, V>> M mergeAll(
414+
M m1, M m2, BinaryOperator<V> mergeFunction) {
423415
for (Map.Entry<K, V> entry : m2.entrySet()) {
424416
m1.merge(entry.getKey(), entry.getValue(), mergeFunction);
425417
}

j2kt/jre/javatests/smoke/CollectionsTest.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import java.util.function.Predicate;
5656
import java.util.function.UnaryOperator;
5757
import java.util.stream.Stream;
58+
import jsinterop.annotations.JsNonNull;
5859
import org.jspecify.nullness.Nullable;
5960
import org.junit.Test;
6061
import org.junit.runner.RunWith;
@@ -334,7 +335,8 @@ public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction
334335
@Override
335336
@SuppressWarnings("nullness:override") // Checker expects @PolyNull (missing in JSpecify)
336337
public @Nullable V computeIfPresent(
337-
K key, BiFunction<? super K, ? super V, ? extends @Nullable V> remappingFunction) {
338+
K key,
339+
BiFunction<? super K, ? super @JsNonNull V, ? extends @Nullable V> remappingFunction) {
338340
return super.computeIfPresent(key, remappingFunction);
339341
}
340342

@@ -354,7 +356,9 @@ public void forEach(BiConsumer<? super K, ? super V> action) {
354356
// Checker framework uses @PolyNull V, JSpecify uses @Nullable V
355357
@SuppressWarnings({"nullness:override", "argument.type"})
356358
public @Nullable V merge(
357-
K key, V value, BiFunction<? super V, ? super V, ? extends @Nullable V> remap) {
359+
K key,
360+
@JsNonNull V value,
361+
BiFunction<? super @JsNonNull V, ? super @JsNonNull V, ? extends @Nullable V> remap) {
358362
return super.merge(key, value, remap);
359363
}
360364

0 commit comments

Comments
 (0)