diff --git a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java
index efc2d087c1..4d2db24240 100644
--- a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java
+++ b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java
@@ -515,6 +515,10 @@ public abstract class CollectionUtils {
* Return a (partially unmodifiable) map that combines the provided two
* maps. Invoking {@link Map#put(Object, Object)} or {@link Map#putAll(Map)}
* on the returned map results in an {@link UnsupportedOperationException}.
+ *
+ *
In the case of a key collision, {@code first} takes precedence over
+ * {@code second}. In other words, entries in {@code second} with a key
+ * that is also mapped by {@code first} are effectively ignored.
* @param first the first map to compose
* @param second the second map to compose
* @return a new map that composes the given two maps
@@ -531,6 +535,10 @@ public abstract class CollectionUtils {
* {@link UnsupportedOperationException} {@code putFunction} is
* {@code null}. The same applies to {@link Map#putAll(Map)} and
* {@code putAllFunction}.
+ *
+ *
In the case of a key collision, {@code first} takes precedence over
+ * {@code second}. In other words, entries in {@code second} with a key
+ * that is also mapped by {@code first} are effectively ignored.
* @param first the first map to compose
* @param second the second map to compose
* @param putFunction applied when {@code Map::put} is invoked. If
diff --git a/spring-core/src/main/java/org/springframework/util/CompositeMap.java b/spring-core/src/main/java/org/springframework/util/CompositeMap.java
index 3a1ff07b07..0e6a01bd74 100644
--- a/spring-core/src/main/java/org/springframework/util/CompositeMap.java
+++ b/spring-core/src/main/java/org/springframework/util/CompositeMap.java
@@ -58,7 +58,7 @@ final class CompositeMap implements Map {
Assert.notNull(first, "First must not be null");
Assert.notNull(second, "Second must not be null");
this.first = first;
- this.second = second;
+ this.second = new FilteredMap<>(second, key -> !this.first.containsKey(key));
this.putFunction = putFunction;
this.putAllFunction = putAllFunction;
}
diff --git a/spring-core/src/main/java/org/springframework/util/FilteredCollection.java b/spring-core/src/main/java/org/springframework/util/FilteredCollection.java
new file mode 100644
index 0000000000..a9ae6d8d80
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/util/FilteredCollection.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.util;
+
+import java.util.AbstractCollection;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.function.Predicate;
+
+/**
+ * Collection that filters out values that do not match a predicate.
+ * This type is used by {@link CompositeMap}.
+ * @author Arjen Poutsma
+ * @since 6.2
+ * @param the type of elements maintained by this collection
+ */
+class FilteredCollection extends AbstractCollection {
+
+ private final Collection delegate;
+
+ private final Predicate filter;
+
+
+ public FilteredCollection(Collection delegate, Predicate filter) {
+ Assert.notNull(delegate, "Delegate must not be null");
+ Assert.notNull(filter, "Filter must not be null");
+
+ this.delegate = delegate;
+ this.filter = filter;
+ }
+
+ @Override
+ public int size() {
+ int size = 0;
+ for (E e : this.delegate) {
+ if (this.filter.test(e)) {
+ size++;
+ }
+ }
+ return size;
+ }
+
+ @Override
+ public Iterator iterator() {
+ return new FilteredIterator<>(this.delegate.iterator(), this.filter);
+ }
+
+ @Override
+ public boolean add(E e) {
+ boolean added = this.delegate.add(e);
+ return added && this.filter.test(e);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean remove(Object o) {
+ boolean removed = this.delegate.remove(o);
+ return removed && this.filter.test((E) o);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean contains(Object o) {
+ if (this.delegate.contains(o)) {
+ return this.filter.test((E) o);
+ }
+ else {
+ return false;
+ }
+ }
+
+ @Override
+ public void clear() {
+ this.delegate.clear();
+ }
+}
diff --git a/spring-core/src/main/java/org/springframework/util/FilteredIterator.java b/spring-core/src/main/java/org/springframework/util/FilteredIterator.java
new file mode 100644
index 0000000000..17a7caac4a
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/util/FilteredIterator.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.util;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.function.Predicate;
+
+import org.springframework.lang.Nullable;
+
+/**
+ * Iterator that filters out values that do not match a predicate.
+ * This type is used by {@link CompositeMap}.
+ * @author Arjen Poutsma
+ * @since 6.2
+ * @param the type of elements returned by this iterator
+ */
+final class FilteredIterator implements Iterator {
+
+ private final Iterator delegate;
+
+ private final Predicate filter;
+
+ @Nullable
+ private E next;
+
+ private boolean nextSet;
+
+
+ public FilteredIterator(Iterator delegate, Predicate filter) {
+ Assert.notNull(delegate, "Delegate must not be null");
+ Assert.notNull(filter, "Filter must not be null");
+
+ this.delegate = delegate;
+ this.filter = filter;
+ }
+
+
+ @Override
+ public boolean hasNext() {
+ if (this.nextSet) {
+ return true;
+ }
+ else {
+ return setNext();
+ }
+ }
+
+ @Override
+ public E next() {
+ if (!this.nextSet) {
+ if (!setNext()) {
+ throw new NoSuchElementException();
+ }
+ }
+ this.nextSet = false;
+ Assert.state(this.next != null, "Next should not be null");
+ return this.next;
+ }
+
+ private boolean setNext() {
+ while (this.delegate.hasNext()) {
+ E next = this.delegate.next();
+ if (this.filter.test(next)) {
+ this.next = next;
+ this.nextSet = true;
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/spring-core/src/main/java/org/springframework/util/FilteredMap.java b/spring-core/src/main/java/org/springframework/util/FilteredMap.java
new file mode 100644
index 0000000000..91254695c2
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/util/FilteredMap.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.util;
+
+import java.util.AbstractMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
+
+import org.springframework.lang.Nullable;
+
+/**
+ * Map that filters out values that do not match a predicate.
+ * This type is used by {@link CompositeMap}.
+ * @author Arjen Poutsma
+ * @since 6.2
+ * @param the type of keys maintained by this map
+ * @param the type of mapped values
+ */
+final class FilteredMap extends AbstractMap {
+
+ private final Map delegate;
+
+ private final Predicate filter;
+
+
+ public FilteredMap(Map delegate, Predicate filter) {
+ Assert.notNull(delegate, "Delegate must not be null");
+ Assert.notNull(filter, "Filter must not be null");
+
+ this.delegate = delegate;
+ this.filter = filter;
+ }
+
+ @Override
+ public Set> entrySet() {
+ return new FilteredSet<>(this.delegate.entrySet(), entry -> this.filter.test(entry.getKey()));
+ }
+
+ @Override
+ public int size() {
+ int size = 0;
+ for (K k : keySet()) {
+ if (this.filter.test(k)) {
+ size++;
+ }
+ }
+ return size;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean containsKey(Object key) {
+ if (this.delegate.containsKey(key)) {
+ return this.filter.test((K) key);
+ }
+ else {
+ return false;
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ @Nullable
+ public V get(Object key) {
+ V value = this.delegate.get(key);
+ if (value != null && this.filter.test((K) key)) {
+ return value;
+ }
+ else {
+ return null;
+ }
+ }
+
+ @Override
+ @Nullable
+ public V put(K key, V value) {
+ V oldValue = this.delegate.put(key, value);
+ if (oldValue != null && this.filter.test(key)) {
+ return oldValue;
+ }
+ else {
+ return null;
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ @Nullable
+ public V remove(Object key) {
+ V oldValue = this.delegate.remove(key);
+ if (oldValue != null && this.filter.test((K) key)) {
+ return oldValue;
+ }
+ else {
+ return null;
+ }
+ }
+
+ @Override
+ public void clear() {
+ this.delegate.clear();
+ }
+
+ @Override
+ public Set keySet() {
+ return new FilteredSet<>(this.delegate.keySet(), this.filter);
+ }
+
+}
diff --git a/spring-core/src/main/java/org/springframework/util/FilteredSet.java b/spring-core/src/main/java/org/springframework/util/FilteredSet.java
new file mode 100644
index 0000000000..97c542c019
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/util/FilteredSet.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.util;
+
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * Set that filters out values that do not match a predicate.
+ * This type is used by {@link CompositeMap}.
+ * @author Arjen Poutsma
+ * @since 6.2
+ * @param the type of elements maintained by this set
+ */
+final class FilteredSet extends FilteredCollection implements Set {
+
+ public FilteredSet(Set delegate, Predicate filter) {
+ super(delegate, filter);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ else if (obj instanceof Set> set) {
+ if (set.size() != size()) {
+ return false;
+ }
+ try {
+ return containsAll(set);
+ }
+ catch (ClassCastException | NullPointerException ignored) {
+ return false;
+ }
+ }
+ else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ int hashCode = 0;
+ for (E obj : this) {
+ if (obj != null) {
+ hashCode += obj.hashCode();
+ }
+ }
+ return hashCode;
+ }
+}
diff --git a/spring-core/src/test/java/org/springframework/util/CompositeMapTests.java b/spring-core/src/test/java/org/springframework/util/CompositeMapTests.java
index a32dad8446..a7b337f8d1 100644
--- a/spring-core/src/test/java/org/springframework/util/CompositeMapTests.java
+++ b/spring-core/src/test/java/org/springframework/util/CompositeMapTests.java
@@ -23,6 +23,7 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -218,4 +219,85 @@ class CompositeMapTests {
Set> entries = composite.entrySet();
assertThat(entries).containsExactly(entry("foo", "bar"), entry("baz", "qux"));
}
+
+ @Nested
+ class CollisionTests {
+
+ @Test
+ void size() {
+ Map first = Map.of("foo", "bar", "baz", "qux");
+ Map second = Map.of("baz", "quux", "corge", "grault");
+ CompositeMap composite = new CompositeMap<>(first, second);
+
+ assertThat(composite).hasSize(3);
+ }
+
+ @Test
+ void containsValue() {
+ Map first = Map.of("foo", "bar", "baz", "qux");
+ Map second = Map.of("baz", "quux", "corge", "grault");
+ CompositeMap composite = new CompositeMap<>(first, second);
+
+ assertThat(composite.containsValue("bar")).isTrue();
+ assertThat(composite.containsValue("qux")).isTrue();
+ assertThat(composite.containsValue("quux")).isFalse();
+ assertThat(composite.containsValue("grault")).isTrue();
+ }
+
+ @Test
+ void get() {
+ Map first = Map.of("foo", "bar", "baz", "qux");
+ Map second = Map.of("baz", "quux", "corge", "grault");
+ CompositeMap composite = new CompositeMap<>(first, second);
+
+ assertThat(composite.get("foo")).isEqualTo("bar");
+ assertThat(composite.get("baz")).isEqualTo("qux");
+ assertThat(composite.get("corge")).isEqualTo("grault");
+ }
+
+ @Test
+ void remove() {
+ Map first = new HashMap<>(Map.of("foo", "bar", "baz", "qux"));
+ Map second = new HashMap<>(Map.of("baz", "quux", "corge", "grault"));
+ CompositeMap composite = new CompositeMap<>(first, second);
+
+ assertThat(composite.remove("baz")).isEqualTo("qux");
+ assertThat(composite.containsKey("baz")).isFalse();
+ assertThat(first).containsExactly(entry("foo", "bar"));
+ assertThat(second).containsExactly(entry("corge", "grault"));
+ }
+
+ @Test
+ void keySet() {
+ Map first = Map.of("foo", "bar", "baz", "qux");
+ Map second = Map.of("baz", "quux", "corge", "grault");
+ CompositeMap composite = new CompositeMap<>(first, second);
+
+ Set keySet = composite.keySet();
+ assertThat(keySet).containsExactlyInAnyOrder("foo", "baz", "corge");
+ }
+
+
+ @Test
+ void values() {
+ Map first = Map.of("foo", "bar", "baz", "qux");
+ Map second = Map.of("baz", "quux", "corge", "grault");
+ CompositeMap composite = new CompositeMap<>(first, second);
+
+ Collection values = composite.values();
+ assertThat(values).containsExactlyInAnyOrder("bar", "qux", "grault");
+ }
+
+ @Test
+ void entrySet() {
+ Map first = Map.of("foo", "bar", "baz", "qux");
+ Map second = Map.of("baz", "quux", "corge", "grault");
+ CompositeMap composite = new CompositeMap<>(first, second);
+
+ Set> entries = composite.entrySet();
+ assertThat(entries).containsExactlyInAnyOrder(entry("foo", "bar"), entry("baz", "qux"), entry("corge", "grault"));
+ }
+
+
+ }
}
diff --git a/spring-core/src/test/java/org/springframework/util/FilteredCollectionTests.java b/spring-core/src/test/java/org/springframework/util/FilteredCollectionTests.java
new file mode 100644
index 0000000000..79cfb2fc97
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/util/FilteredCollectionTests.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Arjen Poutsma
+ */
+class FilteredCollectionTests {
+
+ @Test
+ void size() {
+ List list = List.of("foo", "bar", "baz");
+ FilteredCollection filtered = new FilteredCollection<>(list, s -> !s.equals("bar"));
+
+ assertThat(filtered).hasSize(2);
+ }
+
+ @Test
+ void iterator() {
+ List list = List.of("foo", "bar", "baz");
+ FilteredCollection filtered = new FilteredCollection<>(list, s -> !s.equals("bar"));
+
+ assertThat(filtered.iterator()).toIterable().containsExactly("foo", "baz");
+ }
+
+ @Test
+ void add() {
+ List list = new ArrayList<>(List.of("foo"));
+ FilteredCollection filtered = new FilteredCollection<>(list, s -> !s.equals("bar"));
+ boolean added = filtered.add("bar");
+ assertThat(added).isFalse();
+ assertThat(filtered).containsExactly("foo");
+ assertThat(list).containsExactly("foo", "bar");
+ }
+
+ @Test
+ void remove() {
+ List list = new ArrayList<>(List.of("foo", "bar"));
+ FilteredCollection filtered = new FilteredCollection<>(list, s -> !s.equals("bar"));
+ assertThat(list).contains("bar");
+ assertThat(filtered).doesNotContain("bar");
+ boolean removed = filtered.remove("bar");
+ assertThat(removed).isFalse();
+ assertThat(filtered).doesNotContain("bar");
+ assertThat(list).doesNotContain("bar");
+ }
+
+ @Test
+ void contains() {
+ List list = List.of("foo", "bar", "baz");
+ FilteredCollection filtered = new FilteredCollection<>(list, s -> !s.equals("bar"));
+ boolean contained = filtered.contains("bar");
+ assertThat(contained).isFalse();
+ }
+
+}
diff --git a/spring-core/src/test/java/org/springframework/util/FilteredIteratorTests.java b/spring-core/src/test/java/org/springframework/util/FilteredIteratorTests.java
new file mode 100644
index 0000000000..2f8eef35bd
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/util/FilteredIteratorTests.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.util;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Arjen Poutsma
+ */
+final class FilteredIteratorTests {
+
+ @Test
+ void filter() {
+ List list = List.of("foo", "bar", "baz");
+ FilteredIterator filtered = new FilteredIterator<>(list.iterator(), s -> !s.equals("bar"));
+
+ assertThat(filtered).toIterable().containsExactly("foo", "baz");
+ }
+
+}
diff --git a/spring-core/src/test/java/org/springframework/util/FilteredMapTests.java b/spring-core/src/test/java/org/springframework/util/FilteredMapTests.java
new file mode 100644
index 0000000000..135313b4cd
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/util/FilteredMapTests.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.util;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+
+import static java.util.Map.entry;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Arjen Poutsma
+ */
+class FilteredMapTests {
+
+ @Test
+ void size() {
+ Map map = Map.of("foo", "bar", "baz", "qux", "quux", "corge");
+ FilteredMap filtered = new FilteredMap<>(map, s -> !s.equals("baz"));
+
+ assertThat(filtered).hasSize(2);
+ }
+
+ @Test
+ void entrySet() {
+ Map map = Map.of("foo", "bar", "baz", "qux", "quux", "corge");
+ FilteredMap filtered = new FilteredMap<>(map, s -> !s.equals("baz"));
+
+ assertThat(filtered.entrySet()).containsExactlyInAnyOrder(entry("foo", "bar"), entry("quux", "corge"));
+ }
+
+ @Test
+ void containsKey() {
+ Map map = Map.of("foo", "bar", "baz", "qux", "quux", "corge");
+ FilteredMap filtered = new FilteredMap<>(map, s -> !s.equals("baz"));
+
+ boolean contained = filtered.containsKey("baz");
+ assertThat(contained).isFalse();
+ }
+
+ @Test
+ void get() {
+ Map map = Map.of("foo", "bar", "baz", "qux", "quux", "corge");
+ FilteredMap filtered = new FilteredMap<>(map, s -> !s.equals("baz"));
+
+ String value = filtered.get("baz");
+ assertThat(value).isNull();
+ }
+
+ @Test
+ void put() {
+ Map map = new HashMap<>(Map.of("foo", "bar", "quux", "corge"));
+ FilteredMap filtered = new FilteredMap<>(map, s -> !s.equals("baz"));
+
+ String value = filtered.put("baz", "qux");
+ assertThat(value).isNull();
+ assertThat(filtered.containsKey("baz")).isFalse();
+ assertThat(map.get("baz")).isEqualTo("qux");
+
+ // overwrite
+ value = filtered.put("baz", "QUX");
+ assertThat(value).isNull();
+ assertThat(filtered.containsKey("baz")).isFalse();
+ assertThat(map.get("baz")).isEqualTo("QUX");
+ }
+
+ @Test
+ void remove() {
+ Map map = new HashMap<>(Map.of("foo", "bar", "baz", "qux", "quux", "corge"));
+ FilteredMap filtered = new FilteredMap<>(map, s -> !s.equals("baz"));
+
+ String value = filtered.remove("baz");
+ assertThat(value).isNull();
+ assertThat(filtered.containsKey("baz")).isFalse();
+ assertThat(map.containsKey("baz")).isFalse();
+ }
+
+ @Test
+ void keySet() {
+ Map map = Map.of("foo", "bar", "baz", "qux", "quux", "corge");
+ FilteredMap filtered = new FilteredMap<>(map, s -> !s.equals("baz"));
+
+ Set keySet = filtered.keySet();
+ assertThat(keySet).containsExactlyInAnyOrder("foo", "quux");
+ }
+}
diff --git a/spring-core/src/test/java/org/springframework/util/FilteredSetTests.java b/spring-core/src/test/java/org/springframework/util/FilteredSetTests.java
new file mode 100644
index 0000000000..e5f39c7172
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/util/FilteredSetTests.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.util;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Arjen Poutsma
+ */
+class FilteredSetTests {
+
+ @Test
+ void testEquals() {
+ Set set = Set.of("foo", "bar", "baz");
+ FilteredSet filtered = new FilteredSet<>(set, s -> !s.equals("bar"));
+
+ Set expected = Set.of("foo", "baz");
+
+ assertThat(filtered.equals(expected)).isTrue();
+ assertThat(filtered.equals(set)).isFalse();
+ assertThat(filtered.equals(Collections.emptySet())).isFalse();
+ }
+}