diff --git a/src/main/java/it/aboutbits/springboot/toolbox/util/FilterUtil.java b/src/main/java/it/aboutbits/springboot/toolbox/util/FilterUtil.java
new file mode 100644
index 0000000..b61f5c3
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/toolbox/util/FilterUtil.java
@@ -0,0 +1,72 @@
+package it.aboutbits.springboot.toolbox.util;
+
+import lombok.NonNull;
+import org.springframework.data.util.Streamable;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * This is a collection of tools to simply filter items to a new collection or stream.
+ *
+ * The returned collections are unmodifiable.
+ */
+public final class FilterUtil {
+ private FilterUtil() {
+ }
+
+ public static Set filterToSet(@NonNull Collection items, @NonNull Predicate filter) {
+ return items.stream()
+ .filter(filter)
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
+ public static Set filterToSet(@NonNull Streamable items, @NonNull Predicate filter) {
+ return items.stream()
+ .filter(filter)
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
+ public static Set filterToSet(@NonNull Stream items, @NonNull Predicate filter) {
+ return items
+ .filter(filter)
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
+ public static List filterToList(@NonNull Collection items, @NonNull Predicate filter) {
+ return items.stream()
+ .filter(filter)
+ .toList();
+ }
+
+ public static List filterToList(@NonNull Streamable items, @NonNull Predicate filter) {
+ return items.stream()
+ .filter(filter)
+ .toList();
+ }
+
+ public static List filterToList(@NonNull Stream items, @NonNull Predicate filter) {
+ return items
+ .filter(filter)
+ .toList();
+ }
+
+ public static Stream filterToStream(@NonNull Collection items, @NonNull Predicate filter) {
+ return items.stream()
+ .filter(filter);
+ }
+
+ public static Stream filterToStream(@NonNull Streamable items, @NonNull Predicate filter) {
+ return items.stream()
+ .filter(filter);
+ }
+
+ public static Stream filterToStream(@NonNull Stream items, @NonNull Predicate filter) {
+ return items
+ .filter(filter);
+ }
+}
diff --git a/src/test/java/it/aboutbits/springboot/toolbox/util/FilterUtilTest.java b/src/test/java/it/aboutbits/springboot/toolbox/util/FilterUtilTest.java
new file mode 100644
index 0000000..505955e
--- /dev/null
+++ b/src/test/java/it/aboutbits/springboot/toolbox/util/FilterUtilTest.java
@@ -0,0 +1,265 @@
+package it.aboutbits.springboot.toolbox.util;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.data.util.Streamable;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class FilterUtilTest {
+ @Nested
+ class FilterToSet {
+ @Test
+ @DisplayName("Should filter Collection to Set using predicate")
+ void shouldFilterCollectionToSetUsingPredicate() {
+ // given
+ var numbers = Arrays.asList(1, 2, 3, 4, 5);
+
+ // when
+ var result = FilterUtil.filterToSet(numbers, n -> n > 2);
+
+ // then
+ assertThat(result).containsExactlyInAnyOrder(3, 4, 5);
+ }
+
+ @Test
+ @DisplayName("Should handle empty Collection when filtering to Set")
+ void shouldHandleEmptyCollectionWhenFilteringToSet() {
+ // given
+ var emptyList = Collections.emptyList();
+
+ // when
+ var result = FilterUtil.filterToSet(emptyList, n -> n > 2);
+
+ // then
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should remove duplicates when filtering Collection to Set")
+ void shouldRemoveDuplicatesWhenFilteringCollectionToSet() {
+ // given
+ var numbersWithDuplicates = Arrays.asList(1, 2, 2, 3, 3, 3);
+
+ // when
+ var result = FilterUtil.filterToSet(numbersWithDuplicates, n -> n >= 2);
+
+ // then
+ assertThat(result).containsExactlyInAnyOrder(2, 3);
+ assertThat(result).hasSize(2);
+ }
+
+ @Test
+ @DisplayName("Should filter Streamable to Set using predicate")
+ void shouldFilterStreamableToSetUsingPredicate() {
+ // given
+ var numbers = Streamable.of(1, 2, 3, 4, 5);
+
+ // when
+ var result = FilterUtil.filterToSet(numbers, n -> n % 2 == 0);
+
+ // then
+ assertThat(result).containsExactlyInAnyOrder(2, 4);
+ }
+
+ @Test
+ @DisplayName("Should handle empty Streamable when filtering to Set")
+ void shouldHandleEmptyStreamableWhenFilteringToSet() {
+ // given
+ var emptyStreamable = Streamable.empty();
+
+ // when
+ var result = FilterUtil.filterToSet(emptyStreamable, n -> n > 2);
+
+ // then
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should remove duplicates when filtering Stream to Set")
+ void shouldRemoveDuplicatesWhenFilteringStreamToSet() {
+ // given
+ var numbersWithDuplicates = Stream.of(1, 2, 2, 3, 3, 3);
+
+ // when
+ var result = FilterUtil.filterToSet(numbersWithDuplicates, n -> n >= 2);
+
+ // then
+ assertThat(result).containsExactlyInAnyOrder(2, 3);
+ assertThat(result).hasSize(2);
+ }
+ }
+
+ @Nested
+ class FilterToList {
+ @Test
+ @DisplayName("Should filter Collection to List using predicate")
+ void shouldFilterCollectionToListUsingPredicate() {
+ // given
+ var numbers = Arrays.asList(1, 2, 3, 4, 5);
+
+ // when
+ var result = FilterUtil.filterToList(numbers, n -> n % 2 == 1);
+
+ // then
+ assertThat(result).containsExactly(1, 3, 5);
+ }
+
+ @Test
+ @DisplayName("Should handle empty Collection when filtering to List")
+ void shouldHandleEmptyCollectionWhenFilteringToList() {
+ // given
+ var emptyList = Collections.emptyList();
+
+ // when
+ var result = FilterUtil.filterToList(emptyList, n -> n > 2);
+
+ // then
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should preserve duplicates when filtering Collection to List")
+ void shouldPreserveDuplicatesWhenFilteringCollectionToList() {
+ // given
+ var numbersWithDuplicates = Arrays.asList(1, 2, 2, 3, 3, 3);
+
+ // when
+ var result = FilterUtil.filterToList(numbersWithDuplicates, n -> n >= 2);
+
+ // then
+ assertThat(result).containsExactly(2, 2, 3, 3, 3);
+ assertThat(result).hasSize(5);
+ }
+
+ @Test
+ @DisplayName("Should filter Streamable to List using predicate")
+ void shouldFilterStreamableToListUsingPredicate() {
+ // given
+ var numbers = Streamable.of(1, 2, 3, 4, 5);
+
+ // when
+ var result = FilterUtil.filterToList(numbers, n -> n < 4);
+
+ // then
+ assertThat(result).containsExactly(1, 2, 3);
+ }
+
+ @Test
+ @DisplayName("Should handle empty Streamable when filtering to List")
+ void shouldHandleEmptyStreamableWhenFilteringToList() {
+ // given
+ var emptyStreamable = Streamable.empty();
+
+ // when
+ var result = FilterUtil.filterToList(emptyStreamable, n -> n > 2);
+
+ // then
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should preserve duplicates when filtering Stream to List")
+ void shouldPreserveDuplicatesWhenFilteringStreamToList() {
+ // given
+ var numbersWithDuplicates = Stream.of(1, 2, 2, 3, 3, 3);
+
+ // when
+ var result = FilterUtil.filterToList(numbersWithDuplicates, n -> n >= 2);
+
+ // then
+ assertThat(result).containsExactly(2, 2, 3, 3, 3);
+ assertThat(result).hasSize(5);
+ }
+ }
+
+ @Nested
+ class FilterToStream {
+ @Test
+ @DisplayName("Should filter Collection to Stream using predicate")
+ void shouldFilterCollectionToStreamUsingPredicate() {
+ // given
+ var numbers = Arrays.asList(1, 2, 3, 4, 5);
+
+ // when
+ var resultStream = FilterUtil.filterToStream(numbers, n -> n >= 4);
+
+ // then
+ var result = resultStream.toList();
+ assertThat(result).containsExactly(4, 5);
+ }
+
+ @Test
+ @DisplayName("Should handle empty Collection when filtering to Stream")
+ void shouldHandleEmptyCollectionWhenFilteringToStream() {
+ // given
+ var emptyList = Collections.emptyList();
+
+ // when
+ var resultStream = FilterUtil.filterToStream(emptyList, n -> n > 2);
+
+ // then
+ assertTrue(resultStream.toList().isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should filter Streamable to Stream using predicate")
+ void shouldFilterStreamableToStreamUsingPredicate() {
+ // given
+ var numbers = Streamable.of(1, 2, 3, 4, 5);
+
+ // when
+ var resultStream = FilterUtil.filterToStream(numbers, n -> n <= 2);
+
+ // then
+ var result = resultStream.toList();
+ assertThat(result).containsExactly(1, 2);
+ }
+
+ @Test
+ @DisplayName("Should handle empty Streamable when filtering to Stream")
+ void shouldHandleEmptyStreamableWhenFilteringToStream() {
+ // given
+ var emptyStreamable = Streamable.empty();
+
+ // when
+ var resultStream = FilterUtil.filterToStream(emptyStreamable, n -> n > 2);
+
+ // then
+ assertTrue(resultStream.toList().isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should filter Stream to Stream using predicate")
+ void shouldFilterStreamToStreamUsingPredicate() {
+ // given
+ var numbers = Stream.of(1, 2, 3, 4, 5);
+
+ // when
+ var resultStream = FilterUtil.filterToStream(numbers, n -> n % 2 == 0);
+
+ // then
+ var result = resultStream.toList();
+ assertThat(result).containsExactly(2, 4);
+ }
+
+ @Test
+ @DisplayName("Should handle empty Stream when filtering to Stream")
+ void shouldHandleEmptyStreamWhenFilteringToStream() {
+ // given
+ var emptyStream = Stream.empty();
+
+ // when
+ var resultStream = FilterUtil.filterToStream(emptyStream, n -> n > 2);
+
+ // then
+ assertTrue(resultStream.toList().isEmpty());
+ }
+ }
+}