Let composite/filtered collections accept null elements

Closes gh-36923

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
This commit is contained in:
Yanming Zhou
2026-07-08 15:17:17 +02:00
committed by Sébastien Deleuze
parent 7e0818f4e9
commit 9726c7ed5f
16 changed files with 128 additions and 16 deletions
@@ -28,10 +28,11 @@ import org.jspecify.annotations.Nullable;
* exposed through {@link CompositeMap#values()}.
*
* @author Arjen Poutsma
* @author Yanming Zhou
* @since 6.2
* @param <E> the type of elements maintained by this collection
*/
class CompositeCollection<E> implements Collection<E> {
class CompositeCollection<E extends @Nullable Object> implements Collection<E> {
private final Collection<E> first;
@@ -21,6 +21,8 @@ import java.util.LinkedHashSet;
import java.util.NoSuchElementException;
import java.util.Set;
import org.jspecify.annotations.Nullable;
/**
* Composite iterator that combines multiple other iterators,
* as registered via {@link #add(Iterator)}.
@@ -30,10 +32,11 @@ import java.util.Set;
*
* @author Erwin Vervaet
* @author Juergen Hoeller
* @author Yanming Zhou
* @since 3.0
* @param <E> the element type
*/
public class CompositeIterator<E> implements Iterator<E> {
public class CompositeIterator<E extends @Nullable Object> implements Iterator<E> {
private final Set<Iterator<E>> iterators = new LinkedHashSet<>();
@@ -32,17 +32,18 @@ import org.jspecify.annotations.Nullable;
* {@link CollectionUtils#compositeMap(Map, Map, BiFunction, Consumer)}.
*
* @author Arjen Poutsma
* @author Yanming Zhou
* @since 6.2
* @param <K> the type of keys maintained by this map
* @param <V> the type of mapped values
*/
final class CompositeMap<K, V> implements Map<K, V> {
final class CompositeMap<K, V extends @Nullable Object> implements Map<K, V> {
private final Map<K,V> first;
private final Map<K, V> first;
private final Map<K,V> second;
private final Map<K, V> second;
private final @Nullable BiFunction<K,V,V> putFunction;
private final @Nullable BiFunction<K, V, V> putFunction;
private final @Nullable Consumer<Map<K, V>> putAllFunction;
@@ -53,7 +54,7 @@ final class CompositeMap<K, V> implements Map<K, V> {
CompositeMap(Map<K, V> first, Map<K, V> second,
@Nullable BiFunction<K, V, V> putFunction,
@Nullable Consumer<Map<K,V>> putAllFunction) {
@Nullable Consumer<Map<K, V>> putAllFunction) {
Assert.notNull(first, "First must not be null");
Assert.notNull(second, "Second must not be null");
@@ -106,7 +107,7 @@ final class CompositeMap<K, V> implements Map<K, V> {
}
@Override
public @Nullable V put(K key, V value) {
public V put(K key, V value) {
if (this.putFunction == null) {
throw new UnsupportedOperationException();
}
@@ -116,7 +117,7 @@ final class CompositeMap<K, V> implements Map<K, V> {
}
@Override
public @Nullable V remove(Object key) {
public V remove(Object key) {
V firstResult = this.first.remove(key);
V secondResult = this.second.remove(key);
if (firstResult != null) {
@@ -25,10 +25,11 @@ import org.jspecify.annotations.Nullable;
* {@link CompositeMap#keySet()} and {@link CompositeMap#entrySet()}.
*
* @author Arjen Poutsma
* @author Yanming Zhou
* @since 6.2
* @param <E> the type of elements maintained by this set
*/
final class CompositeSet<E> extends CompositeCollection<E> implements Set<E> {
final class CompositeSet<E extends @Nullable Object> extends CompositeCollection<E> implements Set<E> {
CompositeSet(Set<E> first, Set<E> second) {
super(first, second);
@@ -21,15 +21,18 @@ import java.util.Collection;
import java.util.Iterator;
import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
/**
* Collection that filters out values that do not match a predicate.
* This type is used by {@link CompositeMap}.
*
* @author Arjen Poutsma
* @author Yanming Zhou
* @since 6.2
* @param <E> the type of elements maintained by this collection
*/
class FilteredCollection<E> extends AbstractCollection<E> {
class FilteredCollection<E extends @Nullable Object> extends AbstractCollection<E> {
private final Collection<E> delegate;
@@ -28,10 +28,11 @@ import org.jspecify.annotations.Nullable;
* <p>This type is used by {@link CompositeMap}.
*
* @author Arjen Poutsma
* @author Yanming Zhou
* @since 6.2
* @param <E> the type of elements returned by this iterator
*/
final class FilteredIterator<E> implements Iterator<E> {
final class FilteredIterator<E extends @Nullable Object> implements Iterator<E> {
private final Iterator<E> delegate;
@@ -56,12 +57,11 @@ final class FilteredIterator<E> implements Iterator<E> {
}
@Override
public E next() {
public @Nullable E next() {
if (!this.hasNext && !setNext()) {
throw new NoSuchElementException();
}
this.hasNext = false;
Assert.state(this.next != null, "Next should not be null");
return this.next;
}
@@ -28,11 +28,12 @@ import org.jspecify.annotations.Nullable;
* This type is used by {@link CompositeMap}.
*
* @author Arjen Poutsma
* @author Yanming Zhou
* @since 6.2
* @param <K> the type of keys maintained by this map
* @param <V> the type of mapped values
*/
final class FilteredMap<K, V> extends AbstractMap<K, V> {
final class FilteredMap<K, V extends @Nullable Object> extends AbstractMap<K, V> {
private final Map<K, V> delegate;
@@ -26,10 +26,11 @@ import org.jspecify.annotations.Nullable;
* This type is used by {@link CompositeMap}.
*
* @author Arjen Poutsma
* @author Yanming Zhou
* @since 6.2
* @param <E> the type of elements maintained by this set
*/
final class FilteredSet<E> extends FilteredCollection<E> implements Set<E> {
final class FilteredSet<E extends @Nullable Object> extends FilteredCollection<E> implements Set<E> {
public FilteredSet(Set<E> delegate, Predicate<E> filter) {
super(delegate, filter);
@@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -28,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* @author Arjen Poutsma
* @author Yanming Zhou
*/
class CompositeCollectionTests {
@@ -192,4 +194,17 @@ class CompositeCollectionTests {
assertThat(first).isEmpty();
assertThat(second).isEmpty();
}
@Test
void nullable() {
List<@Nullable String> first = new ArrayList<>();
first.add("foo");
first.add(null);
List<@Nullable String> second = new ArrayList<>();
second.add("bar");
second.add(null);
CompositeCollection<@Nullable String> composite = new CompositeCollection<>(first, second);
assertThat(composite).containsExactly("foo", null, "bar", null);
}
}
@@ -21,6 +21,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -33,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
*
* @author Erwin Vervaet
* @author Juergen Hoeller
* @author Yanming Zhou
*/
class CompositeIteratorTests {
@@ -99,4 +101,13 @@ class CompositeIteratorTests {
it.add(iterator));
}
@Test
void nullable() {
List<@Nullable String> first = Arrays.asList("1", null);
List<@Nullable String> second = Arrays.asList("2", null);
CompositeIterator<String> it = new CompositeIterator<>();
it.add(first.iterator());
it.add(second.iterator());
assertThat(it).toIterable().containsExactly("1", null, "2", null);
}
}
@@ -23,6 +23,7 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@@ -32,6 +33,7 @@ import static org.assertj.core.api.Assertions.entry;
/**
* @author Arjen Poutsma
* @author Yanming Zhou
*/
class CompositeMapTests {
@@ -220,6 +222,17 @@ class CompositeMapTests {
assertThat(entries).containsExactly(entry("foo", "bar"), entry("baz", "qux"));
}
@Test
void nullable() {
Map<String, @Nullable String> first = new HashMap<>();
first.put("foo", "bar");
Map<String, @Nullable String> second = new HashMap<>();
second.put("baz", null);
CompositeMap<String, @Nullable String> composite = new CompositeMap<>(first, second);
assertThat(composite).containsExactly(entry("foo", "bar"), entry("baz", null));
}
@Nested
class CollisionTests {
@@ -20,12 +20,14 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Arjen Poutsma
* @author Yanming Zhou
*/
class CompositeSetTests {
@@ -44,4 +46,16 @@ class CompositeSetTests {
assertThat(composite).isNotEqualTo(Collections.emptySet());
}
@Test
void nullable() {
Set<@Nullable String> first = new HashSet<>();
first.add("foo");
first.add(null);
Set<@Nullable String> second = new HashSet<>();
second.add("bar");
first.add(null);
CompositeSet<@Nullable String> composite = new CompositeSet<>(first, second);
assertThat(composite).containsExactlyInAnyOrder("foo", null, "bar");
}
}
@@ -17,14 +17,17 @@
package org.springframework.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Arjen Poutsma
* @author Yanming Zhou
*/
class FilteredCollectionTests {
@@ -74,4 +77,11 @@ class FilteredCollectionTests {
assertThat(contained).isFalse();
}
@Test
void nullable() {
List<@Nullable String> list = Arrays.asList("foo", "bar", null);
FilteredCollection<@Nullable String> filtered = new FilteredCollection<>(list, s -> !"bar".equals(s));
assertThat(filtered).containsExactlyInAnyOrder("foo", null);
}
}
@@ -16,14 +16,17 @@
package org.springframework.util;
import java.util.Arrays;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Arjen Poutsma
* @author Yanming Zhou
*/
final class FilteredIteratorTests {
@@ -35,4 +38,12 @@ final class FilteredIteratorTests {
assertThat(filtered).toIterable().containsExactly("foo", "baz");
}
@Test
void nullable() {
List<@Nullable String> list = Arrays.asList("foo", "bar", null);
FilteredIterator<@Nullable String> filtered = new FilteredIterator<>(list.iterator(), s -> !"bar".equals(s));
assertThat(filtered).toIterable().containsExactly("foo", null);
}
}
@@ -20,6 +20,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import static java.util.Map.entry;
@@ -27,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Arjen Poutsma
* @author Yanming Zhou
*/
class FilteredMapTests {
@@ -100,4 +102,15 @@ class FilteredMapTests {
Set<String> keySet = filtered.keySet();
assertThat(keySet).containsExactlyInAnyOrder("foo", "quux");
}
@Test
void nullable() {
Map<String, @Nullable String> map = new HashMap<>();
map.put("foo", null);
map.put("bar", "bar");
map.put("baz", "baz");
FilteredMap<String, @Nullable String> filtered = new FilteredMap<>(map, s -> !s.equals("baz"));
assertThat(filtered).containsEntry("foo", null).containsEntry("bar", "bar").doesNotContainKeys("baz");
}
}
@@ -17,14 +17,17 @@
package org.springframework.util;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Arjen Poutsma
* @author Yanming Zhou
*/
class FilteredSetTests {
@@ -39,4 +42,15 @@ class FilteredSetTests {
assertThat(filtered).isNotEqualTo(set);
assertThat(filtered).isNotEqualTo(Collections.emptySet());
}
@Test
void nullable() {
Set<@Nullable String> set = new HashSet<>();
set.add("foo");
set.add("bar");
set.add(null);
FilteredSet<@Nullable String> filtered = new FilteredSet<>(set, s -> !"bar".equals(s));
assertThat(filtered).containsExactlyInAnyOrder("foo", null);
}
}