This is an automated email from the ASF dual-hosted git repository.

paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new f6e2248d12 GROOVY-12036/GROOVY-12037: GDK: cache Collectors instances 
in StreamGroovyMethods and ParallelCollectionExtensions
f6e2248d12 is described below

commit f6e2248d1262e2d0cb0f9f835f1aee40f0d162b2
Author: Paul King <[email protected]>
AuthorDate: Sun May 24 10:57:15 2026 +1000

    GROOVY-12036/GROOVY-12037: GDK: cache Collectors instances in 
StreamGroovyMethods and ParallelCollectionExtensions
---
 .../groovy/runtime/ArrayGroovyMethods.java         | 295 +++++++++++++++++++++
 .../groovy/runtime/DefaultGroovyMethods.java       |  86 ++++++
 .../runtime/ParallelCollectionExtensions.java      |  23 +-
 .../groovy/runtime/StreamGroovyMethods.java        | 207 ++++++++++++++-
 4 files changed, 600 insertions(+), 11 deletions(-)

diff --git a/src/main/java/org/codehaus/groovy/runtime/ArrayGroovyMethods.java 
b/src/main/java/org/codehaus/groovy/runtime/ArrayGroovyMethods.java
index efa2aeb979..7b891b980a 100644
--- a/src/main/java/org/codehaus/groovy/runtime/ArrayGroovyMethods.java
+++ b/src/main/java/org/codehaus/groovy/runtime/ArrayGroovyMethods.java
@@ -9961,6 +9961,138 @@ public class ArrayGroovyMethods extends 
DefaultGroovyMethodsSupport {
         return new ArrayList<>(Arrays.asList(self));
     }
 
+    
//--------------------------------------------------------------------------
+    // toImmutableList
+
+    /**
+     * Converts this array to an immutable List of the same size, with each
+     * element added to the list.
+     *
+     * @param self a boolean array
+     * @return An immutable list containing the contents of this array.
+     * @since 6.0.0
+     */
+    public static List<Boolean> toImmutableList(boolean[] self) {
+        if (self.length == 0) return Collections.emptyList();
+        return Collections.unmodifiableList(toList(self));
+    }
+
+    /**
+     * Converts this array to an immutable List of the same size, with each
+     * element added to the list.
+     *
+     * @param self a byte array
+     * @return An immutable list containing the contents of this array.
+     * @since 6.0.0
+     */
+    public static List<Byte> toImmutableList(byte[] self) {
+        if (self.length == 0) return Collections.emptyList();
+        return Collections.unmodifiableList(toList(self));
+    }
+
+    /**
+     * Converts this array to an immutable List of the same size, with each
+     * element added to the list.
+     *
+     * @param self a char array
+     * @return An immutable list containing the contents of this array.
+     * @since 6.0.0
+     */
+    public static List<Character> toImmutableList(char[] self) {
+        if (self.length == 0) return Collections.emptyList();
+        return Collections.unmodifiableList(toList(self));
+    }
+
+    /**
+     * Converts this array to an immutable List of the same size, with each
+     * element added to the list.
+     *
+     * @param self a short array
+     * @return An immutable list containing the contents of this array.
+     * @since 6.0.0
+     */
+    public static List<Short> toImmutableList(short[] self) {
+        if (self.length == 0) return Collections.emptyList();
+        return Collections.unmodifiableList(toList(self));
+    }
+
+    /**
+     * Converts this array to an immutable List of the same size, with each
+     * element added to the list.
+     *
+     * @param self an int array
+     * @return An immutable list containing the contents of this array.
+     * @since 6.0.0
+     */
+    public static List<Integer> toImmutableList(int[] self) {
+        if (self.length == 0) return Collections.emptyList();
+        return Collections.unmodifiableList(toList(self));
+    }
+
+    /**
+     * Converts this array to an immutable List of the same size, with each
+     * element added to the list.
+     *
+     * @param self a long array
+     * @return An immutable list containing the contents of this array.
+     * @since 6.0.0
+     */
+    public static List<Long> toImmutableList(long[] self) {
+        if (self.length == 0) return Collections.emptyList();
+        return Collections.unmodifiableList(toList(self));
+    }
+
+    /**
+     * Converts this array to an immutable List of the same size, with each
+     * element added to the list.
+     *
+     * @param self a float array
+     * @return An immutable list containing the contents of this array.
+     * @since 6.0.0
+     */
+    public static List<Float> toImmutableList(float[] self) {
+        if (self.length == 0) return Collections.emptyList();
+        return Collections.unmodifiableList(toList(self));
+    }
+
+    /**
+     * Converts this array to an immutable List of the same size, with each
+     * element added to the list.
+     *
+     * @param self a double array
+     * @return An immutable list containing the contents of this array.
+     * @since 6.0.0
+     */
+    public static List<Double> toImmutableList(double[] self) {
+        if (self.length == 0) return Collections.emptyList();
+        return Collections.unmodifiableList(toList(self));
+    }
+
+    /**
+     * Converts this array to an immutable List of the same size, with each
+     * element added to the list. Null elements are preserved (unlike
+     * {@link List#of(Object[])}, which rejects nulls). The returned list is
+     * unmodifiable; mutation attempts throw {@link 
UnsupportedOperationException}.
+     * Returns the canonical empty list ({@link Collections#emptyList()}) when
+     * the array is empty.
+     * <pre class="language-groovy groovyTestCase">
+     * import static groovy.test.GroovyAssert.shouldFail
+     * String[] arr = ['a', null, 'b']
+     * def list = arr.toImmutableList()
+     * assert list == ['a', null, 'b']
+     * shouldFail(UnsupportedOperationException) { list &lt;&lt; 'c' }
+     * assert (new String[0]).toImmutableList() === Collections.emptyList()
+     * </pre>
+     *
+     * @param self an object array
+     * @return An immutable list containing the contents of this array.
+     * @since 6.0.0
+     */
+    public static <T> List<T> toImmutableList(T[] self) {
+        if (self.length == 0) return Collections.emptyList();
+        return Collections.unmodifiableList(new 
ArrayList<>(Arrays.asList(self)));
+    }
+
     
//--------------------------------------------------------------------------
     // toSet
 
@@ -10111,6 +10243,169 @@ public class ArrayGroovyMethods extends 
DefaultGroovyMethodsSupport {
         return DefaultGroovyMethods.toSet(Arrays.asList(self));
     }
 
+    
//--------------------------------------------------------------------------
+    // toImmutableSet
+
+    /**
+     * Converts this array to an immutable Set, with each unique element added 
to the set.
+     * <pre class="language-groovy groovyTestCase">
+     * boolean[] array = [true, false, true]
+     * Set expected = [true, false]
+     * assert array.toImmutableSet() == expected
+     * </pre>
+     *
+     * @param self a boolean array
+     * @return An immutable set containing the unique contents of this array.
+     * @since 6.0.0
+     */
+    public static Set<Boolean> toImmutableSet(boolean[] self) {
+        if (self.length == 0) return Collections.emptySet();
+        return Collections.unmodifiableSet(toSet(self));
+    }
+
+    /**
+     * Converts this array to an immutable Set, with each unique element added 
to the set.
+     * <pre class="language-groovy groovyTestCase">
+     * byte[] array = [1, 2, 3, 2, 1]
+     * Set expected = [1, 2, 3]
+     * assert array.toImmutableSet() == expected
+     * </pre>
+     *
+     * @param self a byte array
+     * @return An immutable set containing the unique contents of this array.
+     * @since 6.0.0
+     */
+    public static Set<Byte> toImmutableSet(byte[] self) {
+        if (self.length == 0) return Collections.emptySet();
+        return Collections.unmodifiableSet(toSet(self));
+    }
+
+    /**
+     * Converts this array to an immutable Set, with each unique element added 
to the set.
+     * <pre class="language-groovy groovyTestCase">
+     * char[] array = 'xyzzy'.chars
+     * Set expected = ['x', 'y', 'z']
+     * assert array.toImmutableSet() == expected
+     * </pre>
+     *
+     * @param self a char array
+     * @return An immutable set containing the unique contents of this array.
+     * @since 6.0.0
+     */
+    public static Set<Character> toImmutableSet(char[] self) {
+        if (self.length == 0) return Collections.emptySet();
+        return Collections.unmodifiableSet(toSet(self));
+    }
+
+    /**
+     * Converts this array to an immutable Set, with each unique element added 
to the set.
+     * <pre class="language-groovy groovyTestCase">
+     * short[] array = [1, 2, 3, 2, 1]
+     * Set expected = [1, 2, 3]
+     * assert array.toImmutableSet() == expected
+     * </pre>
+     *
+     * @param self a short array
+     * @return An immutable set containing the unique contents of this array.
+     * @since 6.0.0
+     */
+    public static Set<Short> toImmutableSet(short[] self) {
+        if (self.length == 0) return Collections.emptySet();
+        return Collections.unmodifiableSet(toSet(self));
+    }
+
+    /**
+     * Converts this array to an immutable Set, with each unique element added 
to the set.
+     * <pre class="language-groovy groovyTestCase">
+     * int[] array = [1, 2, 3, 2, 1]
+     * Set expected = [1, 2, 3]
+     * assert array.toImmutableSet() == expected
+     * </pre>
+     *
+     * @param self an int array
+     * @return An immutable set containing the unique contents of this array.
+     * @since 6.0.0
+     */
+    public static Set<Integer> toImmutableSet(int[] self) {
+        if (self.length == 0) return Collections.emptySet();
+        return Collections.unmodifiableSet(toSet(self));
+    }
+
+    /**
+     * Converts this array to an immutable Set, with each unique element added 
to the set.
+     * <pre class="language-groovy groovyTestCase">
+     * long[] array = [1, 2, 3, 2, 1]
+     * Set expected = [1, 2, 3]
+     * assert array.toImmutableSet() == expected
+     * </pre>
+     *
+     * @param self a long array
+     * @return An immutable set containing the unique contents of this array.
+     * @since 6.0.0
+     */
+    public static Set<Long> toImmutableSet(long[] self) {
+        if (self.length == 0) return Collections.emptySet();
+        return Collections.unmodifiableSet(toSet(self));
+    }
+
+    /**
+     * Converts this array to an immutable Set, with each unique element added 
to the set.
+     * <pre class="language-groovy groovyTestCase">
+     * float[] array = [1.0f, 2.0f, 3.0f, 2.0f, 1.0f]
+     * Set expected = [1.0f, 2.0f, 3.0f]
+     * assert array.toImmutableSet() == expected
+     * </pre>
+     *
+     * @param self a float array
+     * @return An immutable set containing the unique contents of this array.
+     * @since 6.0.0
+     */
+    public static Set<Float> toImmutableSet(float[] self) {
+        if (self.length == 0) return Collections.emptySet();
+        return Collections.unmodifiableSet(toSet(self));
+    }
+
+    /**
+     * Converts this array to an immutable Set, with each unique element added 
to the set.
+     * <pre class="language-groovy groovyTestCase">
+     * double[] array = [1.0d, 2.0d, 3.0d, 2.0d, 1.0d]
+     * Set expected = [1.0d, 2.0d, 3.0d]
+     * assert array.toImmutableSet() == expected
+     * </pre>
+     *
+     * @param self a double array
+     * @return An immutable set containing the unique contents of this array.
+     * @since 6.0.0
+     */
+    public static Set<Double> toImmutableSet(double[] self) {
+        if (self.length == 0) return Collections.emptySet();
+        return Collections.unmodifiableSet(toSet(self));
+    }
+
+    /**
+     * Converts this array to an immutable Set, with each unique element added
+     * to the set. Null elements are preserved (unlike {@link 
Set#copyOf(java.util.Collection)},
+     * which rejects nulls). The returned set is unmodifiable; mutation 
attempts
+     * throw {@link UnsupportedOperationException}. Returns the canonical empty
+     * set ({@link Collections#emptySet()}) when the array is empty.
+     * <pre class="language-groovy groovyTestCase">
+     * import static groovy.test.GroovyAssert.shouldFail
+     * String[] arr = ['a', null, 'b', 'a']
+     * def set = arr.toImmutableSet()
+     * assert set == ['a', null, 'b'] as Set
+     * shouldFail(UnsupportedOperationException) { set &lt;&lt; 'c' }
+     * assert (new String[0]).toImmutableSet() === Collections.emptySet()
+     * </pre>
+     *
+     * @param self an object array
+     * @return An immutable set containing the unique contents of this array.
+     * @since 6.0.0
+     */
+    public static <T> Set<T> toImmutableSet(T[] self) {
+        Set<T> answer = toSet(self);
+        return answer.isEmpty() ? Collections.emptySet() : 
Collections.unmodifiableSet(answer);
+    }
+
     
//--------------------------------------------------------------------------
     // toSorted
 
diff --git 
a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java 
b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
index f5a6093490..dfecf3286f 100644
--- a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
+++ b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
@@ -15689,6 +15689,37 @@ public class DefaultGroovyMethods extends 
DefaultGroovyMethodsSupport {
         return answer;
     }
 
+    /**
+     * Convert an iterator to an immutable List. The iterator will become
+     * exhausted of elements after making this conversion. Returns the
+     * canonical empty list ({@link Collections#emptyList()}) when the iterator
+     * is empty.
+     * <p>
+     * Null elements are preserved (unlike {@link 
List#copyOf(java.util.Collection)},
+     * which rejects nulls). The returned list is unmodifiable; mutation
+     * attempts throw {@link UnsupportedOperationException}.
+     * <p>
+     * <pre class="language-groovy groovyTestCase">
+     * import static groovy.test.GroovyAssert.shouldFail
+     * def list = [1, 2, null, 3].iterator().toImmutableList()
+     * assert list == [1, 2, null, 3]
+     * shouldFail(UnsupportedOperationException) { list &lt;&lt; 4 }
+     * assert [].iterator().toImmutableList() === Collections.emptyList()
+     * </pre>
+     *
+     * @param self an iterator
+     * @return an immutable List of the elements from the iterator
+     * @since 6.0.0
+     */
+    public static <T> List<T> toImmutableList(Iterator<T> self) {
+        if (!self.hasNext()) return Collections.emptyList();
+        List<T> answer = new ArrayList<>();
+        while (self.hasNext()) {
+            answer.add(self.next());
+        }
+        return Collections.unmodifiableList(answer);
+    }
+
     /**
      * Convert an Iterable to a List. The Iterable's iterator will
      * become exhausted of elements after making this conversion.
@@ -15706,6 +15737,18 @@ public class DefaultGroovyMethods extends 
DefaultGroovyMethodsSupport {
         return toList(self.iterator());
     }
 
+    /**
+     * Convert an Iterable to an immutable List. The Iterable's iterator will
+     * become exhausted of elements after making this conversion.
+     *
+     * @param self an Iterable
+     * @return an immutable List of the elements from the Iterable
+     * @since 6.0.0
+     */
+    public static <T> List<T> toImmutableList(Iterable<T> self) {
+        return toImmutableList(self.iterator());
+    }
+
     /**
      * Convert an enumeration to a List.
      *
@@ -15854,6 +15897,18 @@ public class DefaultGroovyMethods extends 
DefaultGroovyMethodsSupport {
         return toSet(self.iterator());
     }
 
+    /**
+     * Convert an Iterable to an immutable Set. The Iterable's iterator will
+     * become exhausted of elements after making this conversion.
+     *
+     * @param self an Iterable
+     * @return an immutable Set of the elements from the Iterable
+     * @since 6.0.0
+     */
+    public static <T> Set<T> toImmutableSet(Iterable<T> self) {
+        return toImmutableSet(self.iterator());
+    }
+
     /**
      * Convert an iterator to a Set. The iterator will become
      * exhausted of elements after making this conversion.
@@ -15870,6 +15925,37 @@ public class DefaultGroovyMethods extends 
DefaultGroovyMethodsSupport {
         return answer;
     }
 
+    /**
+     * Convert an iterator to an immutable Set. The iterator will become
+     * exhausted of elements after making this conversion. Returns the
+     * canonical empty set ({@link Collections#emptySet()}) when the iterator
+     * is empty.
+     * <p>
+     * Null elements are preserved (unlike {@link 
Set#copyOf(java.util.Collection)},
+     * which rejects nulls). The returned set is unmodifiable; mutation
+     * attempts throw {@link UnsupportedOperationException}.
+     * <p>
+     * <pre class="language-groovy groovyTestCase">
+     * import static groovy.test.GroovyAssert.shouldFail
+     * def set = [1, 2, null, 3].iterator().toImmutableSet()
+     * assert set == [1, 2, null, 3] as Set
+     * shouldFail(UnsupportedOperationException) { set &lt;&lt; 4 }
+     * assert [].iterator().toImmutableSet() === Collections.emptySet()
+     * </pre>
+     *
+     * @param self an iterator
+     * @return an immutable Set of the elements from the iterator
+     * @since 6.0.0
+     */
+    public static <T> Set<T> toImmutableSet(Iterator<T> self) {
+        if (!self.hasNext()) return Collections.emptySet();
+        Set<T> answer = new HashSet<>();
+        while (self.hasNext()) {
+            answer.add(self.next());
+        }
+        return Collections.unmodifiableSet(answer);
+    }
+
     /**
      * Convert an enumeration to a Set.
      *
diff --git 
a/src/main/java/org/codehaus/groovy/runtime/ParallelCollectionExtensions.java 
b/src/main/java/org/codehaus/groovy/runtime/ParallelCollectionExtensions.java
index fc11727a20..3ffd7d8752 100644
--- 
a/src/main/java/org/codehaus/groovy/runtime/ParallelCollectionExtensions.java
+++ 
b/src/main/java/org/codehaus/groovy/runtime/ParallelCollectionExtensions.java
@@ -31,6 +31,7 @@ import java.util.function.BinaryOperator;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.stream.Collector;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 
@@ -52,6 +53,20 @@ import java.util.stream.IntStream;
  */
 public class ParallelCollectionExtensions {
 
+    // Cached Collector: stateless config object; the per-call mutable 
ArrayList
+    // is produced fresh by the Supplier inside collect(), so sharing this
+    // instance across threads is safe and saves a per-call allocation.
+    // toCollection(ArrayList::new) is used (rather than toList()) because the
+    // JDK contract on Collectors.toList() doesn't guarantee a mutable result 
or
+    // a concrete ArrayList type, while these GDK methods promise both.
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    private static final Collector TO_LIST = 
Collectors.toCollection(ArrayList::new);
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    private static <T> Collector<T, ?, List<T>> toListCollector() {
+        return (Collector) TO_LIST;
+    }
+
     // ---- Iteration ------------------------------------------------------
 
     /**
@@ -71,7 +86,7 @@ public class ParallelCollectionExtensions {
      */
     public static <T, R> List<R> collectParallel(Collection<T> self, 
Function<T, R> transform) {
         return withCurrentFJP(fjp ->
-                fjp.submit(() -> 
self.parallelStream().map(transform).collect(Collectors.toList())).join()
+                fjp.submit(() -> 
self.parallelStream().map(transform).collect(toListCollector())).join()
         );
     }
 
@@ -82,7 +97,7 @@ public class ParallelCollectionExtensions {
      */
     public static <T> List<T> findAllParallel(Collection<T> self, Predicate<T> 
filter) {
         return withCurrentFJP(fjp ->
-                fjp.submit(() -> 
self.parallelStream().filter(filter).collect(Collectors.toList())).join()
+                fjp.submit(() -> 
self.parallelStream().filter(filter).collect(toListCollector())).join()
         );
     }
 
@@ -203,7 +218,7 @@ public class ParallelCollectionExtensions {
         return withCurrentFJP(fjp ->
                 fjp.submit(() -> self.parallelStream()
                         .flatMap(e -> transform.apply(e).stream())
-                        .collect(Collectors.toList())).join()
+                        .collect(toListCollector())).join()
         );
     }
 
@@ -249,7 +264,7 @@ public class ParallelCollectionExtensions {
         return withCurrentFJP(fjp ->
                 fjp.submit(() -> self.parallelStream()
                         .filter(e -> InvokerHelper.invokeMethod(filter, 
"isCase", e) != Boolean.FALSE)
-                        .collect(Collectors.toList())).join()
+                        .collect(toListCollector())).join()
         );
     }
 
diff --git a/src/main/java/org/codehaus/groovy/runtime/StreamGroovyMethods.java 
b/src/main/java/org/codehaus/groovy/runtime/StreamGroovyMethods.java
index 3ee3ca8bed..d2c20ee9c4 100644
--- a/src/main/java/org/codehaus/groovy/runtime/StreamGroovyMethods.java
+++ b/src/main/java/org/codehaus/groovy/runtime/StreamGroovyMethods.java
@@ -25,7 +25,9 @@ import java.lang.reflect.Array;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Enumeration;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
@@ -37,6 +39,7 @@ import java.util.Spliterator;
 import java.util.Spliterators;
 import java.util.function.Consumer;
 import java.util.stream.BaseStream;
+import java.util.stream.Collector;
 import java.util.stream.Collectors;
 import java.util.stream.DoubleStream;
 import java.util.stream.IntStream;
@@ -49,6 +52,42 @@ public class StreamGroovyMethods {
     private StreamGroovyMethods() {
     }
 
+    // ---- Cached Collectors -------------------------------------------------
+    // Collector instances are stateless configuration objects; the per-call
+    // accumulator is freshly produced by the Supplier on each collect()
+    // invocation. So sharing the Collector across calls and threads is safe —
+    // and saves a per-call allocation that the JDK never elides. Same trick
+    // Eclipse Collections' Collectors2 uses.
+    //
+    // toCollection(ArrayList::new) / toCollection(HashSet::new) (rather than
+    // toList()/toSet()) is deliberate: the JDK contract on Collectors.toList()
+    // and toSet() doesn't guarantee mutability or concrete type, and several
+    // public GDK methods promise a "new mutable List/Set". toCollection pins
+    // the supplier so the guarantee is held by the implementation, not by
+    // current JDK behaviour.
+    //
+    // For the immutable Set case (toImmutableSet on Stream / BaseStream), we
+    // collect via the cached TO_SET and wrap with Collections.unmodifiableSet
+    // at the call site rather than using Collectors.toUnmodifiableSet() — the
+    // latter's finisher calls Set.copyOf which rejects null elements, which
+    // would diverge from Groovy's null-tolerant idiom and from the matching
+    // toImmutableSet methods on Iterator/Iterable/T[].
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    private static final Collector TO_LIST = 
Collectors.toCollection(ArrayList::new);
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    private static final Collector TO_SET = 
Collectors.toCollection(HashSet::new);
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    private static <T> Collector<T, ?, List<T>> toListCollector() {
+        return (Collector) TO_LIST;
+    }
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    private static <T> Collector<T, ?, Set<T>> toSetCollector() {
+        return (Collector) TO_SET;
+    }
+
     /**
      * Returns element at {@code index} or {@code null}.
      * <p>
@@ -122,7 +161,7 @@ public class StreamGroovyMethods {
      */
     public static <T> List<T> getAt(final Stream<T> self, final IntRange 
range) {
         if (range.isReverse()) throw new IllegalArgumentException("reverse 
range");
-        return 
self.skip(range.getFromInt()).limit(range.size()).collect(Collectors.toList());
+        return 
self.skip(range.getFromInt()).limit(range.size()).collect(toListCollector());
     }
 
     /**
@@ -692,16 +731,32 @@ public class StreamGroovyMethods {
     }
 
     /**
-     * Accumulates the elements of stream into a new List.
+     * Accumulates the elements of stream into a new mutable List.
+     * <p>
+     * <strong>Note:</strong> since JDK 16, {@link Stream} has a native
+     * {@code toList()} method that returns an <em>unmodifiable</em> list.
+     * Java instance methods take precedence over GDK extensions in both
+     * {@code @CompileStatic} and dynamic dispatch, so {@code stream.toList()}
+     * now resolves to the native call and yields an immutable result —
+     * <em>not</em> a fresh {@code ArrayList} as it did pre-JDK&nbsp;16.
+     * Direct callers of this extension method are pointed at the explicit
+     * replacements; see the deprecation note.
      *
      * @param self the stream
      * @param <T> the type of element
-     * @return a new {@code java.util.List} instance
+     * @return a new mutable {@code java.util.List} instance
      *
      * @since 2.5.0
+     *
+     * @deprecated since 6.0.0; the native {@link Stream#toList()} 
(JDK&nbsp;16+)
+     *             shadows this and returns an <em>unmodifiable</em> list. If
+     *             you need a mutable list, call {@link 
#toMutableList(Stream)};
+     *             otherwise use {@code stream.toList()} directly (faster than
+     *             this extension because it skips the {@code Collectors} 
path).
      */
+    @Deprecated(since = "6.0.0")
     public static <T> List<T> toList(final Stream<T> self) {
-        return self.collect(Collectors.toList());
+        return self.collect(toListCollector());
     }
 
     /**
@@ -714,7 +769,7 @@ public class StreamGroovyMethods {
      * @since 2.5.0
      */
     public static <T> List<T> toList(final BaseStream<T, ? extends BaseStream> 
self) {
-        return stream(self.iterator()).collect(Collectors.toList());
+        return stream(self.iterator()).collect(toListCollector());
     }
 
     /**
@@ -727,7 +782,7 @@ public class StreamGroovyMethods {
      * @since 2.5.0
      */
     public static <T> Set<T> toSet(final Stream<T> self) {
-        return self.collect(Collectors.toSet());
+        return self.collect(toSetCollector());
     }
 
     /**
@@ -740,6 +795,144 @@ public class StreamGroovyMethods {
      * @since 2.5.0
      */
     public static <T> Set<T> toSet(final BaseStream<T, ? extends BaseStream> 
self) {
-        return stream(self.iterator()).collect(Collectors.toSet());
+        return stream(self.iterator()).collect(toSetCollector());
+    }
+
+    /**
+     * Accumulates the elements of stream into a new mutable List.
+     * <p>
+     * Explicit alternative to the native {@link Stream#toList()} (which
+     * returns an <em>unmodifiable</em> list since JDK&nbsp;16). Use this when
+     * you need to add to, remove from, or sort the returned list. The returned
+     * list is a concrete {@link ArrayList} — the cached collector pins the
+     * supplier so this is contract, not happenstance.
+     * <pre class="language-groovy groovyTestCase">
+     * import java.util.stream.Stream
+     * import org.codehaus.groovy.runtime.StreamGroovyMethods
+     * def list = StreamGroovyMethods.toMutableList(Stream.of('a', 'b'))
+     * assert list == ['a', 'b']
+     * list &lt;&lt; 'c'   // mutable — no UnsupportedOperationException
+     * assert list == ['a', 'b', 'c']
+     * </pre>
+     *
+     * @param self the stream
+     * @param <T> the type of element
+     * @return a new mutable {@code java.util.List} instance
+     *
+     * @since 6.0.0
+     */
+    public static <T> List<T> toMutableList(final Stream<T> self) {
+        return self.collect(toListCollector());
+    }
+
+    /**
+     * Accumulates the elements of stream into a new mutable List.
+     * <p>
+     * Explicit-mutability alias for {@link #toList(BaseStream)}, symmetric 
with
+     * {@link #toMutableList(Stream)}.
+     *
+     * @param self the {@code java.util.stream.BaseStream}
+     * @param <T> the type of element
+     * @return a new mutable {@code java.util.List} instance
+     *
+     * @since 6.0.0
+     */
+    public static <T> List<T> toMutableList(final BaseStream<T, ? extends 
BaseStream> self) {
+        return stream(self.iterator()).collect(toListCollector());
+    }
+
+    /**
+     * Accumulates the elements of stream into a new mutable Set.
+     * <p>
+     * Explicit-mutability alias for {@link #toSet(Stream)}. Provided 
defensively
+     * so user intent stays unambiguous if a future JDK release adds a native
+     * {@code Stream.toSet()} (which would shadow the GDK extension the way
+     * native {@link Stream#toList()} did in JDK&nbsp;16).
+     *
+     * @param self the stream
+     * @param <T> the type of element
+     * @return a new mutable {@code java.util.Set} instance
+     *
+     * @since 6.0.0
+     */
+    public static <T> Set<T> toMutableSet(final Stream<T> self) {
+        return self.collect(toSetCollector());
+    }
+
+    /**
+     * Accumulates the elements of stream into a new mutable Set.
+     * <p>
+     * Explicit-mutability alias for {@link #toSet(BaseStream)}, symmetric with
+     * {@link #toMutableSet(Stream)}.
+     *
+     * @param self the {@code java.util.stream.BaseStream}
+     * @param <T> the type of element
+     * @return a new mutable {@code java.util.Set} instance
+     *
+     * @since 6.0.0
+     */
+    public static <T> Set<T> toMutableSet(final BaseStream<T, ? extends 
BaseStream> self) {
+        return stream(self.iterator()).collect(toSetCollector());
+    }
+
+    /**
+     * Accumulates the elements of stream into an immutable List.
+     *
+     * @param self the {@code java.util.stream.BaseStream}
+     * @param <T> the type of element
+     * @return an immutable {@code java.util.List} instance
+     *
+     * @since 6.0.0
+     */
+    public static <T> List<T> toImmutableList(final BaseStream<T, ? extends 
BaseStream> self) {
+        return stream(self.iterator()).toList();
+    }
+
+    /**
+     * Accumulates the elements of stream into an immutable Set.
+     * <p>
+     * No native {@code Stream.toSet()} exists, so this provides the immutable
+     * counterpart to {@link #toSet(Stream)}. Null elements are preserved
+     * (unlike {@link java.util.stream.Collectors#toUnmodifiableSet()} which
+     * rejects nulls); the returned set is unmodifiable and mutation attempts
+     * throw {@link UnsupportedOperationException}. Returns the canonical
+     * empty set ({@link Collections#emptySet()}) when the stream is empty.
+     * <pre class="language-groovy groovyTestCase">
+     * import java.util.stream.Stream
+     * import static groovy.test.GroovyAssert.shouldFail
+     * import static 
org.codehaus.groovy.runtime.StreamGroovyMethods.toImmutableSet
+     * def set = toImmutableSet(Stream.of('a', null, 'b', 'a'))
+     * assert set == ['a', null, 'b'] as Set
+     * shouldFail(UnsupportedOperationException) { set &lt;&lt; 'c' }
+     * assert toImmutableSet(Stream.&lt;String&gt;empty()) === 
Collections.emptySet()
+     * </pre>
+     *
+     * @param self the stream
+     * @param <T> the type of element
+     * @return an immutable {@code java.util.Set} instance
+     *
+     * @since 6.0.0
+     */
+    public static <T> Set<T> toImmutableSet(final Stream<T> self) {
+        Set<T> answer = self.collect(toSetCollector());
+        return answer.isEmpty() ? Collections.emptySet() : 
Collections.unmodifiableSet(answer);
+    }
+
+    /**
+     * Accumulates the elements of stream into an immutable Set.
+     * <p>
+     * See {@link #toImmutableSet(Stream)} for the null-tolerance and empty-set
+     * semantics — this overload is the {@link BaseStream} counterpart with the
+     * same contract.
+     *
+     * @param self the {@code java.util.stream.BaseStream}
+     * @param <T> the type of element
+     * @return an immutable {@code java.util.Set} instance
+     *
+     * @since 6.0.0
+     */
+    public static <T> Set<T> toImmutableSet(final BaseStream<T, ? extends 
BaseStream> self) {
+        Set<T> answer = stream(self.iterator()).collect(toSetCollector());
+        return answer.isEmpty() ? Collections.emptySet() : 
Collections.unmodifiableSet(answer);
     }
 }

Reply via email to