You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jena.apache.org by an...@apache.org on 2026/05/13 07:50:36 UTC

(jena) branch main updated (2a3775bb92 -> 25d036a644)

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

afs pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git


    from 2a3775bb92 Bump axios from 1.15.2 to 1.16.0 in /jena-fuseki2/jena-fuseki-ui
     new 6507b34fac GH-3900: misc mem improvements
     new 25d036a644 GH-3900: misc mem improvements

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 jena-benchmarks/jena-benchmarks-jmh/pom.xml        |   2 +-
 ...estSparseArraySpliteratorsForeachRemaining.java |   9 +-
 .../TestSparseArraySpliteratorsStreamParallel.java |   9 +-
 .../TestSparseArraySpliteratorsTryAdvance.java     |   9 +-
 .../jena-benchmarks-shadedJena560/pom.xml          |   2 +-
 jena-benchmarks/pom.xml                            |   2 +-
 .../apache/jena/mem/collection/FastHashBase.java   | 276 ++++++---
 .../apache/jena/mem/collection/FastHashMap.java    | 112 ++--
 .../apache/jena/mem/collection/FastHashSet.java    | 140 ++---
 .../apache/jena/mem/collection/HashCommonBase.java |  17 +-
 .../apache/jena/mem/collection/HashCommonMap.java  |  13 +-
 .../org/apache/jena/mem/collection/JenaMap.java    |   1 +
 .../apache/jena/mem/collection/JenaMapIndexed.java |  74 +++
 .../jena/mem/collection/JenaMapSetCommon.java      |  22 +-
 .../org/apache/jena/mem/collection/JenaSet.java    |  14 +-
 .../jena/mem/collection/JenaSetHashOptimized.java  |  41 +-
 .../apache/jena/mem/collection/JenaSetIndexed.java |  60 ++
 .../collection/Sized.java}                         |  14 +-
 .../jena/mem/iterator/IteratorOfJenaSets.java      |  19 +-
 .../mem/iterator/SparseArrayIndexedIterator.java   | 109 ----
 .../jena/mem/iterator/SparseArrayIterator.java     |  47 +-
 .../org/apache/jena/mem/pattern/MatchPattern.java  |  13 +-
 .../apache/jena/mem/pattern/PatternClassifier.java |  33 +-
 .../jena/mem/spliterator/ArraySpliterator.java     |  51 +-
 .../jena/mem/spliterator/ArraySubSpliterator.java  |  54 +-
 .../spliterator/SparseArrayIndexedSpliterator.java | 131 ----
 .../mem/spliterator/SparseArraySpliterator.java    |  51 +-
 .../mem/spliterator/SparseArraySubSpliterator.java |  53 +-
 .../apache/jena/mem/store/fast/FastArrayBunch.java |  47 +-
 .../jena/mem/store/fast/FastHashedBunchMap.java    |  15 +-
 .../jena/mem/store/fast/FastHashedTripleBunch.java |  23 +-
 .../jena/mem/store/fast/FastTripleBunch.java       |  36 +-
 .../jena/mem/store/fast/FastTripleStore.java       |  44 +-
 .../apache/jena/mem/store/legacy/ArrayBunch.java   |   8 +-
 .../roaring/strategies/EagerStoreStrategy.java     |  15 +-
 .../org/apache/jena/mem/AbstractGraphMemTest.java  |  32 +
 .../java/org/apache/jena/mem/GraphMemFastTest.java |  23 +
 .../java/org/apache/jena/mem/TS4_GraphMem.java     |   3 +
 .../mem/collection/AbstractJenaMapNodeTest.java    |  40 +-
 .../mem/collection/AbstractJenaSetTripleTest.java  |  29 +-
 .../jena/mem/collection/FastHashMapTest2.java      |  67 +-
 .../jena/mem/collection/FastHashSetTest2.java      | 161 ++---
 .../iterator/SparseArrayIndexedIteratorTest.java   | 171 ------
 .../jena/mem/iterator/SparseArrayIteratorTest.java |  33 +-
 .../jena/mem/pattern/PatternClassifierTest.java    |  39 +-
 .../jena/mem/spliterator/ArraySpliteratorTest.java | 192 +++---
 .../mem/spliterator/ArraySubSpliteratorTest.java   | 191 +++---
 .../SparseArrayIndexedSpliteratorTest.java         | 675 ---------------------
 .../spliterator/SparseArraySpliteratorTest.java    | 141 ++---
 .../spliterator/SparseArraySubSpliteratorTest.java | 141 ++---
 .../mem/store/fast/FastHashedBunchMapTest.java     | 143 +++++
 51 files changed, 1505 insertions(+), 2142 deletions(-)
 create mode 100644 jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java
 create mode 100644 jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java
 copy jena-core/src/main/java/org/apache/jena/{rdf/model/StatementTerm.java => mem/collection/Sized.java} (70%)
 delete mode 100644 jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java
 delete mode 100644 jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java
 delete mode 100644 jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java
 delete mode 100644 jena-core/src/test/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliteratorTest.java
 create mode 100644 jena-core/src/test/java/org/apache/jena/mem/store/fast/FastHashedBunchMapTest.java


(jena) 02/02: GH-3900: misc mem improvements

Posted by an...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

afs pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git

commit 25d036a644b673bfd19bbdb554ba52957e1f0531
Author: bern-SOPTIM <ar...@soptim.de>
AuthorDate: Wed May 6 15:09:42 2026 +0200

    GH-3900: misc mem improvements
    
    Fixing jena-benckmarks:
    - updated pom.xml files to version "6.2.0-SNAPSHOT"
    - fixed spliterator benchmarks to support new Sized parameter
---
 jena-benchmarks/jena-benchmarks-jmh/pom.xml                      | 2 +-
 .../spliterator/TestSparseArraySpliteratorsForeachRemaining.java | 9 ++++++++-
 .../spliterator/TestSparseArraySpliteratorsStreamParallel.java   | 9 ++++++++-
 .../mem/spliterator/TestSparseArraySpliteratorsTryAdvance.java   | 9 ++++++++-
 jena-benchmarks/jena-benchmarks-shadedJena560/pom.xml            | 2 +-
 jena-benchmarks/pom.xml                                          | 2 +-
 6 files changed, 27 insertions(+), 6 deletions(-)

diff --git a/jena-benchmarks/jena-benchmarks-jmh/pom.xml b/jena-benchmarks/jena-benchmarks-jmh/pom.xml
index 8c612c8f74..d3dcb89012 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/pom.xml
+++ b/jena-benchmarks/jena-benchmarks-jmh/pom.xml
@@ -25,7 +25,7 @@
     <parent>
         <groupId>org.apache.jena</groupId>
         <artifactId>jena-benchmarks</artifactId>
-        <version>6.1.0</version>
+        <version>6.2.0-SNAPSHOT</version>
     </parent>
 
     <name>Apache Jena - Benchmarks JMH</name>
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsForeachRemaining.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsForeachRemaining.java
index 54e19b413e..01d765a749 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsForeachRemaining.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsForeachRemaining.java
@@ -28,6 +28,7 @@ import java.util.Spliterator;
 import org.apache.jena.atlas.iterator.ActionCount;
 import org.apache.jena.jmh.JmhDefaultOptions;
 
+import org.apache.jena.mem.collection.Sized;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -75,12 +76,18 @@ public class TestSparseArraySpliteratorsForeachRemaining {
             if (count != elementsCount) {
                 throw new RuntimeException("Concurrent modification detected");
             }
+        } ;
+        final var sized = new Sized() {
+            @Override
+            public int size() {
+                return elementsCount;
+            }
         };
         return switch (param1_iteratorImplementation) {
             case "memvalue.SparseArraySpliterator" ->
                     new org.apache.jena.memvalue.SparseArraySpliterator<>(arrayWithNulls, count, checkForConcurrentModification);
             case "mem2.SparseArraySpliterator" ->
-                    new SparseArraySpliterator<>(arrayWithNulls, checkForConcurrentModification);
+                    new SparseArraySpliterator<>(arrayWithNulls, sized);
             default ->
                     throw new IllegalArgumentException("Unknown spliterator implementation: " + param1_iteratorImplementation);
         };
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsStreamParallel.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsStreamParallel.java
index c28435e8f2..62020e2468 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsStreamParallel.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsStreamParallel.java
@@ -29,6 +29,7 @@ import java.util.stream.StreamSupport;
 import org.apache.jena.atlas.iterator.ActionCount;
 import org.apache.jena.jmh.JmhDefaultOptions;
 
+import org.apache.jena.mem.collection.Sized;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -77,11 +78,17 @@ public class TestSparseArraySpliteratorsStreamParallel {
                 throw new RuntimeException("Concurrent modification detected");
             }
         };
+        final var sized = new Sized() {
+            @Override
+            public int size() {
+                return elementsCount;
+            }
+        };
         return switch (param1_iteratorImplementation) {
             case "memvalue.SparseArraySpliterator" ->
                     new org.apache.jena.memvalue.SparseArraySpliterator<>(arrayWithNulls, count, checkForConcurrentModification);
             case "mem2.SparseArraySpliterator" ->
-                    new SparseArraySpliterator<>(arrayWithNulls, checkForConcurrentModification);
+                    new SparseArraySpliterator<>(arrayWithNulls, sized);
             default ->
                     throw new IllegalArgumentException("Unknown spliterator implementation: " + param1_iteratorImplementation);
         };
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsTryAdvance.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsTryAdvance.java
index d558282458..cca4a1601f 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsTryAdvance.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsTryAdvance.java
@@ -28,6 +28,7 @@ import java.util.Spliterator;
 import org.apache.jena.atlas.iterator.ActionCount;
 import org.apache.jena.jmh.JmhDefaultOptions;
 
+import org.apache.jena.mem.collection.Sized;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -78,11 +79,17 @@ public class TestSparseArraySpliteratorsTryAdvance {
                 throw new RuntimeException("Concurrent modification detected");
             }
         };
+        final var sized = new Sized() {
+            @Override
+            public int size() {
+                return elementsCount;
+            }
+        };
         return switch (param1_iteratorImplementation) {
             case "memvalue.SparseArraySpliterator" ->
                     new org.apache.jena.memvalue.SparseArraySpliterator<>(arrayWithNulls, count, checkForConcurrentModification);
             case "mem2.SparseArraySpliterator" ->
-                    new SparseArraySpliterator<>(arrayWithNulls, checkForConcurrentModification);
+                    new SparseArraySpliterator<>(arrayWithNulls, sized);
             default ->
                     throw new IllegalArgumentException("Unknown spliterator implementation: " + param1_iteratorImplementation);
         };
diff --git a/jena-benchmarks/jena-benchmarks-shadedJena560/pom.xml b/jena-benchmarks/jena-benchmarks-shadedJena560/pom.xml
index 37918ddd1e..1319d82d4a 100644
--- a/jena-benchmarks/jena-benchmarks-shadedJena560/pom.xml
+++ b/jena-benchmarks/jena-benchmarks-shadedJena560/pom.xml
@@ -25,7 +25,7 @@
     <parent>
         <groupId>org.apache.jena</groupId>
         <artifactId>jena-benchmarks</artifactId>
-        <version>6.1.0</version>
+        <version>6.2.0-SNAPSHOT</version>
     </parent>
 
     <name>Apache Jena - Benchmarks Shaded Jena 5.6.0</name>
diff --git a/jena-benchmarks/pom.xml b/jena-benchmarks/pom.xml
index 917b36ebbb..6f9afb658d 100644
--- a/jena-benchmarks/pom.xml
+++ b/jena-benchmarks/pom.xml
@@ -25,7 +25,7 @@
   <parent>
     <groupId>org.apache.jena</groupId>
     <artifactId>jena</artifactId>
-    <version>6.1.0</version>
+    <version>6.2.0-SNAPSHOT</version>
   </parent>
 
   <name>Apache Jena - Benchmark Suite</name>


(jena) 01/02: GH-3900: misc mem improvements

Posted by an...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

afs pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git

commit 6507b34faca188f095d465141621fd2fe08c9249
Author: bern-SOPTIM <ar...@soptim.de>
AuthorDate: Tue May 5 16:58:20 2026 +0200

    GH-3900: misc mem improvements
    
    Minor bugs fixed:
    - FastArrayBunch does not hold references to orphans after #tryRemove and #removeUnchecked
    - FastHashMap#computeIfAbsent does not creat an invalid state if call of absentValueSupplier fails
    - FastHashMap#compute now grows on insert like all other insert operations of this map
    
    Improvements:
    - iterators and spliterators don't need a runnable for concurrency check any more. They check the size directly, which has been shown to be faster.
    - FastHashBase#fillPositionsArray now iterates over the dense keys array, which should be faster in most cases.
    - Removed FastHashSet#IndexedKey and the corresponding iterator and spliterator implementations
    --> introduced FastHashBase#forEachKey and #forEachKeyParallel instead
    - added JavaDoc
    - added tests
---
 .../apache/jena/mem/collection/FastHashBase.java   | 276 ++++++---
 .../apache/jena/mem/collection/FastHashMap.java    | 112 ++--
 .../apache/jena/mem/collection/FastHashSet.java    | 140 ++---
 .../apache/jena/mem/collection/HashCommonBase.java |  17 +-
 .../apache/jena/mem/collection/HashCommonMap.java  |  13 +-
 .../org/apache/jena/mem/collection/JenaMap.java    |   1 +
 .../apache/jena/mem/collection/JenaMapIndexed.java |  74 +++
 .../jena/mem/collection/JenaMapSetCommon.java      |  22 +-
 .../org/apache/jena/mem/collection/JenaSet.java    |  14 +-
 .../jena/mem/collection/JenaSetHashOptimized.java  |  41 +-
 .../apache/jena/mem/collection/JenaSetIndexed.java |  60 ++
 .../org/apache/jena/mem/collection/Sized.java}     |  17 +-
 .../jena/mem/iterator/IteratorOfJenaSets.java      |  19 +-
 .../mem/iterator/SparseArrayIndexedIterator.java   | 109 ----
 .../jena/mem/iterator/SparseArrayIterator.java     |  47 +-
 .../org/apache/jena/mem/pattern/MatchPattern.java  |  13 +-
 .../apache/jena/mem/pattern/PatternClassifier.java |  33 +-
 .../jena/mem/spliterator/ArraySpliterator.java     |  51 +-
 .../jena/mem/spliterator/ArraySubSpliterator.java  |  54 +-
 .../spliterator/SparseArrayIndexedSpliterator.java | 131 ----
 .../mem/spliterator/SparseArraySpliterator.java    |  51 +-
 .../mem/spliterator/SparseArraySubSpliterator.java |  53 +-
 .../apache/jena/mem/store/fast/FastArrayBunch.java |  47 +-
 .../jena/mem/store/fast/FastHashedBunchMap.java    |  15 +-
 .../jena/mem/store/fast/FastHashedTripleBunch.java |  23 +-
 .../jena/mem/store/fast/FastTripleBunch.java       |  36 +-
 .../jena/mem/store/fast/FastTripleStore.java       |  44 +-
 .../apache/jena/mem/store/legacy/ArrayBunch.java   |   8 +-
 .../roaring/strategies/EagerStoreStrategy.java     |  15 +-
 .../org/apache/jena/mem/AbstractGraphMemTest.java  |  32 +
 .../java/org/apache/jena/mem/GraphMemFastTest.java |  23 +
 .../java/org/apache/jena/mem/TS4_GraphMem.java     |   3 +
 .../mem/collection/AbstractJenaMapNodeTest.java    |  40 +-
 .../mem/collection/AbstractJenaSetTripleTest.java  |  29 +-
 .../jena/mem/collection/FastHashMapTest2.java      |  67 +-
 .../jena/mem/collection/FastHashSetTest2.java      | 161 ++---
 .../iterator/SparseArrayIndexedIteratorTest.java   | 171 ------
 .../jena/mem/iterator/SparseArrayIteratorTest.java |  33 +-
 .../jena/mem/pattern/PatternClassifierTest.java    |  39 +-
 .../jena/mem/spliterator/ArraySpliteratorTest.java | 192 +++---
 .../mem/spliterator/ArraySubSpliteratorTest.java   | 191 +++---
 .../SparseArrayIndexedSpliteratorTest.java         | 675 ---------------------
 .../spliterator/SparseArraySpliteratorTest.java    | 141 ++---
 .../spliterator/SparseArraySubSpliteratorTest.java | 141 ++---
 .../mem/store/fast/FastHashedBunchMapTest.java     | 143 +++++
 45 files changed, 1481 insertions(+), 2136 deletions(-)

diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashBase.java b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashBase.java
index 6166da8147..aeba704d75 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashBase.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashBase.java
@@ -24,58 +24,80 @@ import org.apache.jena.mem.iterator.SparseArrayIterator;
 import org.apache.jena.mem.spliterator.SparseArraySpliterator;
 import org.apache.jena.util.iterator.ExtendedIterator;
 
-import java.util.ConcurrentModificationException;
 import java.util.Spliterator;
 import java.util.function.Predicate;
 
 /**
- * This is the base class for {@link FastHashSet} and {@link FastHashSet}.
- * It only grows but never shrinks.
- * This map does not guarantee any order. Although due to the way it is implemented the elements have a certain order.
- * This map does not allow null keys.
- * This map is not thread safe.
+ * Base class for {@link FastHashSet} and {@link FastHashMap}.
+ * The collection grows on demand but never shrinks. It does not guarantee any
+ * iteration order (although the implementation does produce a stable order
+ * for a given insertion/deletion history). It does not allow {@code null}
+ * keys and is not thread-safe.
+ * <h2>Internal layout</h2>
+ * <ul>
+ *   <li><b>positions</b>: power-of-two sized array used as the open-addressing
+ *   probe table (like in {@link java.util.HashMap}). It is indexed by
+ *   {@code hashCode &amp; (positions.length - 1)}. A value of {@code 0} marks
+ *   an empty slot - faster to test than a {@code null} reference. Non-empty
+ *   slots store the bitwise complement ({@code ~}) of the index of the entry
+ *   in the {@code keys}/{@code hashCodesOrDeletedIndices} arrays, so a real
+ *   stored index of {@code 0} encodes as {@code -1} and is therefore distinct
+ *   from "empty".</li>
+ *   <li><b>keys</b>: dense array of keys, generally filled from index 0 up to
+ *   {@code keysPos}. Slots emptied by deletion become {@code null} and are
+ *   reused before the array is grown. The dense layout enables fast iteration.</li>
+ *   <li><b>hashCodesOrDeletedIndices</b>: parallel array to {@code keys}. For
+ *   live entries it stores the cached hash code of the key. For deleted slots
+ *   it stores the index of the previously deleted slot, forming a freelist
+ *   whose head is {@code lastDeletedIndex} ({@code -1} if empty).</li>
+ *   <li><b>keysPos</b> / <b>removedKeysCount</b>: high-water mark and freelist
+ *   length, respectively; the live size is {@code keysPos - removedKeysCount}.</li>
+ * </ul>
+ * The {@code keys} and {@code hashCodesOrDeletedIndices} arrays grow together
+ * by approximately a factor of 1.5 (similar to {@link java.util.ArrayList}).
  * <p>
- * The positions array stores negative indices to the entries and hashCode arrays.
- * The positions array is implemented as a power of two sized array. (like in {@link java.util.HashMap}) This allows
- * to use a fast modulo operation to calculate the index. The indices of the positions array are derived from the
- * hashCodes.
- * Any position 0 indicates an empty element. The comparison with 0 is faster than comparing elements with null.
- * <p>
- * The keys are stored in a keys array and the hashCodesOrDeletedIndices array
- * stores the hashCodes of the keys.
- * hashCodesOrDeletedIndices is also used to store the indices of the deleted keys to save memory. It works like a
- * linked list of deleted keys. The index of the previously deleted key is stored in the hashCodesOrDeletedIndices
- * array. lastDeletedIndex is the index of the last deleted key in the hashCodesOrDeletedIndices array and serves as
- * the head of the linked list of deleted keys.
- * These two arrays grow together. They grow like {@link java.util.ArrayList} with a factor of 1.5.
- * <p>
- * keysPos is the index of the next free position in the keys array.
- * The keys array is usually completely filled from index 0 to keysPos. Exceptions are the deleted keys.
- * Indices that have been deleted are reused for new keys before the keys array is extended.
- * The dense nature of the keys array enables fast iteration.
- * <p>
- * The index of a key in the keys array never changes. So the index of a key can be used as a handle to the key and
- * for random access.
+ * Once a key is inserted, its index in the {@code keys} array never changes
+ * until it is removed. The index can therefore be used as a stable handle for
+ * O(1) random access, e.g. to coordinate parallel arrays of associated data.
  *
  * @param <K> the type of the keys
  */
 public abstract class FastHashBase<K> implements JenaMapSetCommon<K> {
+    /** Initial size of the {@link #positions} probe table. */
     protected static final int MINIMUM_HASHES_SIZE = 16;
+    /** Initial size of the {@link #keys} / {@link #hashCodesOrDeletedIndices} arrays. */
     protected static final int MINIMUM_ELEMENTS_SIZE = 8;
+    /** High-water mark in {@link #keys}; one past the largest slot ever used. */
     protected int keysPos = 0;
+    /** Dense array of stored keys; {@code null} marks a freed slot. */
     protected K[] keys;
+    /**
+     * For live entries: cached {@link Object#hashCode()} of the corresponding key.
+     * For freed slots: index of the previously freed slot (singly-linked freelist
+     * whose head is {@link #lastDeletedIndex}).
+     */
     protected int[] hashCodesOrDeletedIndices;
+    /** Head of the freelist of removed slots, or {@code -1} if the freelist is empty. */
     protected int lastDeletedIndex = -1;
+    /** Number of freelist entries (i.e. slots in {@link #keys} currently {@code null}). */
     protected int removedKeysCount = 0;
 
     /**
-     * The negative indices to the entries and hashCode arrays.
-     * The indices of the positions array are derived from the hashCodes.
-     * Any position 0 indicates an empty element.
+     * Probe table mapping a hash bucket to an entry index in {@link #keys}.
+     * A slot's value is the bitwise complement ({@code ~}) of the entry index;
+     * a value of {@code 0} marks an empty slot.
      */
     protected int[] positions;
 
-    protected FastHashBase(int initialSize) {
+    /**
+     * Creates a base collection sized to hold at least {@code initialSize}
+     * entries before growing.
+     *
+     * @param initialSize the initial capacity of the keys array; the probe
+     *                    table is sized to the next power of two at least
+     *                    twice as large
+     */
+    protected FastHashBase(final int initialSize) {
         var positionsSize = Integer.highestOneBit(initialSize << 1);
         if (positionsSize < initialSize << 1) {
             positionsSize <<= 1;
@@ -85,6 +107,11 @@ public abstract class FastHashBase<K> implements JenaMapSetCommon<K> {
         this.hashCodesOrDeletedIndices = new int[initialSize];
     }
 
+    /**
+     * Creates a base collection with the default minimum capacities
+     * ({@link #MINIMUM_HASHES_SIZE} for the probe table and
+     * {@link #MINIMUM_ELEMENTS_SIZE} for the keys array).
+     */
     protected FastHashBase() {
         this.positions = new int[MINIMUM_HASHES_SIZE];
         this.keys = newKeysArray(MINIMUM_ELEMENTS_SIZE);
@@ -95,17 +122,17 @@ public abstract class FastHashBase<K> implements JenaMapSetCommon<K> {
      * Copy constructor.
      * The new map will contain all the same keys of the map to copy.
      *
-     * @param baseToCopy
+     * @param baseToCopy instance to copy
      */
-    protected <T extends FastHashBase<?>> FastHashBase(final T baseToCopy)  {
+    protected <T extends FastHashBase<K>> FastHashBase(final T baseToCopy)  {
         this.positions = new int[baseToCopy.positions.length];
         System.arraycopy(baseToCopy.positions, 0, this.positions, 0, baseToCopy.positions.length);
 
         this.hashCodesOrDeletedIndices = new int[baseToCopy.hashCodesOrDeletedIndices.length];
-        System.arraycopy(baseToCopy.hashCodesOrDeletedIndices, 0, this.hashCodesOrDeletedIndices, 0, baseToCopy.hashCodesOrDeletedIndices.length);
+        System.arraycopy(baseToCopy.hashCodesOrDeletedIndices, 0, this.hashCodesOrDeletedIndices, 0, baseToCopy.keysPos);
 
         this.keys = newKeysArray(baseToCopy.keys.length);
-        System.arraycopy(baseToCopy.keys, 0, this.keys, 0, baseToCopy.keys.length);
+        System.arraycopy(baseToCopy.keys, 0, this.keys, 0, baseToCopy.keysPos);
 
         this.keysPos = baseToCopy.keysPos;
         this.lastDeletedIndex = baseToCopy.lastDeletedIndex;
@@ -143,6 +170,17 @@ public abstract class FastHashBase<K> implements JenaMapSetCommon<K> {
         return -1;
     }
 
+    private void fillPositionsArray(int newSize) {
+        this.positions = new int[newSize];
+        var pos = keysPos - 1;
+        while (-1 < pos) {
+            if (null != keys[pos]) {
+                this.positions[findEmptySlotWithoutEqualityCheck(hashCodesOrDeletedIndices[pos])] = ~pos;
+            }
+            pos--;
+        }
+    }
+
     /**
      * Grows the positions array if needed.
      */
@@ -151,13 +189,7 @@ public abstract class FastHashBase<K> implements JenaMapSetCommon<K> {
         if (newSize < 0) {
             return;
         }
-        final var oldPositions = this.positions;
-        this.positions = new int[newSize];
-        for (int oldPosition : oldPositions) {
-            if (0 != oldPosition) {
-                this.positions[findEmptySlotWithoutEqualityCheck(hashCodesOrDeletedIndices[~oldPosition])] = oldPosition;
-            }
-        }
+        fillPositionsArray(newSize);
     }
 
     /**
@@ -170,13 +202,7 @@ public abstract class FastHashBase<K> implements JenaMapSetCommon<K> {
         if (newSize < 0) {
             return false;
         }
-        final var oldPositions = this.positions;
-        this.positions = new int[newSize];
-        for (int oldPosition : oldPositions) {
-            if (0 != oldPosition) {
-                this.positions[findEmptySlotWithoutEqualityCheck(hashCodesOrDeletedIndices[~oldPosition])] = oldPosition;
-            }
-        }
+        fillPositionsArray(newSize);
         return true;
     }
 
@@ -245,21 +271,23 @@ public abstract class FastHashBase<K> implements JenaMapSetCommon<K> {
     }
 
     /**
-     * Removes the element at the given position.
+     * Remove the given element and return the index it occupied before removal.
      *
-     * @param e the element
-     * @return the index of the removed element or -1 if the element was not found
+     * @param e the element to remove
+     * @return the former index of the element, or {@code -1} if it was not present
      */
     public final int removeAndGetIndex(final K e) {
         return removeAndGetIndex(e, e.hashCode());
     }
 
     /**
-     * Removes the element at the given position.
+     * Remove the given element and return the index it occupied before removal.
+     * Lets the caller supply the precomputed hash code to avoid an extra
+     * {@code hashCode()} call.
      *
-     * @param e        the element
-     * @param hashCode the hash code of the element. This is a performance optimization.
-     * @return the index of the removed element or -1 if the element was not found
+     * @param e        the element to remove
+     * @param hashCode {@code e.hashCode()}
+     * @return the former index of the element, or {@code -1} if it was not present
      */
     public final int removeAndGetIndex(final K e, final int hashCode) {
         final var pIndex = findPosition(e, hashCode);
@@ -281,18 +309,19 @@ public abstract class FastHashBase<K> implements JenaMapSetCommon<K> {
     }
 
     /**
-     * Removes the element at the given position.
-     * <p>
-     * This is an implementation of Knuth's Algorithm R from tAoCP vol3, p 527,
-     * with exchanging of the roles of i and j so that they can be usefully renamed
-     * to <i>here</i> and <i>scan</i>.
+     * Removes the entry referenced by the {@code positions} slot at index
+     * {@code here} and rehashes the affected probe chain.
      * <p>
-     * It relies on linear probing but doesn't require a distinguished REMOVED
-     * value. Since we resize the table when it gets fullish, we don't worry [much]
-     * about the overhead of the linear probing.
+     * This is an implementation of Knuth's Algorithm R from <em>The Art of
+     * Computer Programming</em>, vol. 3, p. 527, with the roles of {@code i}
+     * and {@code j} swapped so they can be usefully renamed to <i>here</i>
+     * and <i>scan</i>.
      * <p>
+     * It relies on linear probing but doesn't require a distinguished
+     * {@code REMOVED} sentinel. Since the table is resized once it gets
+     * fullish, the overhead of linear probing is not a concern.
      *
-     * @param here the index in the positions array
+     * @param here the index in the {@link #positions} array of the slot to clear
      */
     protected void removeFrom(int here) {
         final var pIndex = ~positions[here];
@@ -345,9 +374,14 @@ public abstract class FastHashBase<K> implements JenaMapSetCommon<K> {
     }
 
     /**
-     * Attentions: Due to the ordering of the keys, this method may be slow
-     * if matching elements are at the start of the list.
-     * Try to use {@link #anyMatchRandomOrder(Predicate)} instead.
+     * {@inheritDoc}
+     * <p>
+     * Iterates the keys in dense (insertion-order-ish) order. This is fast when
+     * matches are rare or expected near the end of the array, but can be slow
+     * when matches are clustered at the start of the array. For workloads
+     * where many matches are expected, prefer {@link #anyMatchRandomOrder(Predicate)},
+     * which scans in probe-table order and tends to find matches sooner when
+     * they are abundant.
      */
     @Override
     public final boolean anyMatch(Predicate<K> predicate) {
@@ -362,11 +396,16 @@ public abstract class FastHashBase<K> implements JenaMapSetCommon<K> {
     }
 
     /**
-     * This method can be faster than {@link #anyMatch(Predicate)} if one expects
-     * to find many matches. But it is slower if one expects to find no matches or just a single one.
+     * Like {@link #anyMatch(Predicate)} but scans the probe table rather than
+     * the dense {@code keys} array, yielding a roughly hash-based order.
+     * <p>
+     * This is faster than {@link #anyMatch(Predicate)} when many matches are
+     * expected (the predicate is more likely to short-circuit early), but
+     * slower when no or only a single match exists (each iteration must
+     * test against an empty slot first).
      *
-     * @param predicate the predicate to apply to elements of this collection
-     * @return {@code true} if any element of the collection matches the predicate
+     * @param predicate the predicate to apply
+     * @return {@code true} if any element matches the predicate
      */
     public final boolean anyMatchRandomOrder(Predicate<K> predicate) {
         var pIndex = positions.length - 1;
@@ -381,14 +420,22 @@ public abstract class FastHashBase<K> implements JenaMapSetCommon<K> {
 
     @Override
     public final ExtendedIterator<K> keyIterator() {
-        final var initialSize = size();
-        final Runnable checkForConcurrentModification = () ->
-        {
-            if (size() != initialSize) throw new ConcurrentModificationException();
-        };
-        return new SparseArrayIterator<>(keys, keysPos, checkForConcurrentModification);
+        return new SparseArrayIterator<>(keys, keysPos, this);
     }
 
+    /**
+     * Locates the slot in {@link #positions} that holds {@code e} (with the
+     * given precomputed hash code).
+     * <p>
+     * If the key is present, returns the (non-negative) probe-table slot
+     * index. If the key is absent, returns the bitwise complement of the
+     * empty probe-table slot at which the key would be inserted, allowing
+     * insertion to proceed without a second probe walk.
+     *
+     * @param e        the key to locate
+     * @param hashCode {@code e.hashCode()}
+     * @return the position index if found, or {@code ~insertionPosition} if not
+     */
     protected final int findPosition(final K e, final int hashCode) {
         var pIndex = calcStartIndexByHashCode(hashCode);
         while (true) {
@@ -405,6 +452,15 @@ public abstract class FastHashBase<K> implements JenaMapSetCommon<K> {
         }
     }
 
+    /**
+     * Locates the next empty slot in {@link #positions} along the probe chain
+     * for the given hash code, without checking any existing entries for
+     * equality. Used after a positions-array resize, when no duplicates can
+     * exist in the rebuilt table.
+     *
+     * @param hashCode the hash code being placed
+     * @return the index of an empty slot in the probe table
+     */
     protected final int findEmptySlotWithoutEqualityCheck(final int hashCode) {
         var pIndex = calcStartIndexByHashCode(hashCode);
         while (true) {
@@ -435,11 +491,63 @@ public abstract class FastHashBase<K> implements JenaMapSetCommon<K> {
 
     @Override
     public final Spliterator<K> keySpliterator() {
-        final var initialSize = this.size();
-        final Runnable checkForConcurrentModification = () ->
-        {
-            if (this.size() != initialSize) throw new ConcurrentModificationException();
-        };
-        return new SparseArraySpliterator<>(keys, keysPos, checkForConcurrentModification);
+        return new SparseArraySpliterator<>(keys, keysPos, this);
+    }
+
+    /**
+     * Gets the key at the given index.
+     * Array bounds are not checked. The caller must ensure the index is valid and corresponds to a non-null key.
+     *
+     * @param i the index
+     * @return the key at the given index
+     */
+    public K getKeyAt(int i) {
+        return keys[i];
+    }
+
+    /**
+     * Returns the index of the entry holding {@code key}, or {@code -1} if not present.
+     *
+     * @param key the key to look up
+     * @return the entry index, or {@code -1} if the key is absent
+     */
+    public int indexOf(K key) {
+        final var pIndex = findPosition(key, key.hashCode());
+        if (pIndex < 0) {
+            return -1;
+        } else {
+            return ~positions[pIndex];
+        }
+    }
+
+    /**
+     * Functional interface used by {@link #forEachKey} to receive each live
+     * key along with the stable index it occupies.
+     *
+     * @param <K> the key type
+     */
+    @FunctionalInterface
+    public interface KeyAndIndexConsumer<K> {
+        /**
+         * Receive a single key and its index.
+         *
+         * @param key   the key
+         * @param index the stable index of the key in the underlying array
+         */
+        void accept(K key, int index);
+    }
+
+    /**
+     * Sequentially invokes {@code consumer} for every live key with its index.
+     * Skips freed slots.
+     *
+     * @param consumer receives each key/index pair
+     */
+    public void forEachKey(KeyAndIndexConsumer<K> consumer) {
+        for (int i = 0; i < keysPos; i++) {
+            if(keys[i] != null) {
+                consumer.accept(keys[i], i);
+            }
+        }
     }
 }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java
index 04c2761416..e3f741ba48 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java
@@ -25,39 +25,56 @@ import org.apache.jena.mem.iterator.SparseArrayIterator;
 import org.apache.jena.mem.spliterator.SparseArraySpliterator;
 import org.apache.jena.util.iterator.ExtendedIterator;
 
-import java.util.ConcurrentModificationException;
 import java.util.Spliterator;
 import java.util.function.Supplier;
 import java.util.function.UnaryOperator;
 
 /**
- * Map which grows, if needed but never shrinks.
- * This map does not guarantee any order. Although due to the way it is implemented the elements have a certain order.
- * This map does not allow null keys.
- * This map is not thread safe.
- * It´s purpose is to support fast add, remove, contains and stream / iterate operations.
- * Only remove operations are not as fast as in {@link java.util.HashMap}
- * Iterating over this map does not get much faster again after removing elements because the map is not compacted.
+ * Hash map specialization built on top of {@link FastHashBase}.
+ * Grows on demand but never shrinks, does not guarantee iteration order,
+ * does not allow {@code null} keys, and is not thread-safe.
+ * <p>
+ * Optimized for fast {@code add} / {@code containsKey} / {@code stream} /
+ * iterate operations. Removal is somewhat slower than in
+ * {@link java.util.HashMap} because of the back-shifting performed on the
+ * probe table. Iteration speed does not recover after many removals because
+ * the dense {@code keys} array is not compacted.
+ *
+ * @param <K> the key type
+ * @param <V> the value type
  */
-public abstract class FastHashMap<K, V> extends FastHashBase<K> implements JenaMap<K, V> {
+public abstract class FastHashMap<K, V> extends FastHashBase<K> implements JenaMapIndexed<K, V> {
 
+    /**
+     * Parallel array to {@code keys} holding the value associated with each
+     * stored key. {@code values[i]} is the value for {@code keys[i]} when
+     * {@code keys[i]} is non-null.
+     */
     protected V[] values;
 
+    /**
+     * Creates a map with the given initial key-array capacity.
+     *
+     * @param initialSize the initial capacity of the keys/values arrays
+     */
     protected FastHashMap(int initialSize) {
         super(initialSize);
         this.values = newValuesArray(keys.length);
     }
 
+    /**
+     * Creates a map with the default initial capacity.
+     */
     protected FastHashMap() {
         super();
         this.values = newValuesArray(keys.length);
     }
 
     /**
-     * Copy constructor.
-     * The new map will contain all the same keys and values of the map to copy.
+     * Copy constructor. The new map contains the same keys and the same
+     * value references as {@code mapToCopy}.
      *
-     * @param mapToCopy
+     * @param mapToCopy the source map
      */
     protected FastHashMap(final FastHashMap<K, V> mapToCopy) {
         super(mapToCopy);
@@ -66,10 +83,13 @@ public abstract class FastHashMap<K, V> extends FastHashBase<K> implements JenaM
     }
 
     /**
-     * Copy constructor with value processor.
+     * Copy constructor that transforms each value via {@code valueProcessor}.
+     * Useful when the values are mutable and need to be deep-copied to keep
+     * the new map independent from the source.
      *
-     * @param mapToCopy
-     * @param valueProcessor
+     * @param mapToCopy      the source map
+     * @param valueProcessor function applied to every non-null value to obtain
+     *                       the value to put in the new map
      */
     protected FastHashMap(final FastHashMap<K, V> mapToCopy, final UnaryOperator<V> valueProcessor) {
         super(mapToCopy);
@@ -82,6 +102,12 @@ public abstract class FastHashMap<K, V> extends FastHashBase<K> implements JenaM
         }
     }
 
+    /**
+     * Gets a new array of values with the given size.
+     *
+     * @param size the size of the array
+     * @return the new array
+     */
     protected abstract V[] newValuesArray(int size);
 
     @Override
@@ -106,12 +132,10 @@ public abstract class FastHashMap<K, V> extends FastHashBase<K> implements JenaM
 
     @Override
     public boolean tryPut(K key, V value) {
+        growPositionsArrayIfNeeded();
         final var hashCode = key.hashCode();
-        var pIndex = findPosition(key, hashCode);
+        final var pIndex = findPosition(key, hashCode);
         if (pIndex < 0) {
-            if (tryGrowPositionsArrayIfNeeded()) {
-                pIndex = findPosition(key, hashCode);
-            }
             final var eIndex = getFreeKeyIndex();
             keys[eIndex] = key;
             values[eIndex] = value;
@@ -126,12 +150,10 @@ public abstract class FastHashMap<K, V> extends FastHashBase<K> implements JenaM
 
     @Override
     public void put(K key, V value) {
+        growPositionsArrayIfNeeded();
         final var hashCode = key.hashCode();
-        var pIndex = findPosition(key, hashCode);
+        final var pIndex = findPosition(key, hashCode);
         if (pIndex < 0) {
-            if (tryGrowPositionsArrayIfNeeded()) {
-                pIndex = findPosition(key, hashCode);
-            }
             final var eIndex = getFreeKeyIndex();
             keys[eIndex] = key;
             values[eIndex] = value;
@@ -142,8 +164,27 @@ public abstract class FastHashMap<K, V> extends FastHashBase<K> implements JenaM
         }
     }
 
+    @Override
+    public int putAndGetIndex(K key, V value) {
+        growPositionsArrayIfNeeded();
+        final int hashCode = key.hashCode();
+        final var pIndex = findPosition(key, hashCode);
+        final int eIndex;
+        if (pIndex < 0) {
+            eIndex = getFreeKeyIndex();
+            keys[eIndex] = key;
+            hashCodesOrDeletedIndices[eIndex] = hashCode;
+            positions[~pIndex] = ~eIndex;
+        } else {
+            eIndex = ~positions[pIndex];
+        }
+        values[eIndex] = value;
+        return eIndex;
+    }
+
     /**
      * Returns the value at the given index.
+     * Array bounds are not checked. The caller must ensure the index is valid and corresponds to a non-null key.
      *
      * @param i index
      * @return value
@@ -178,12 +219,12 @@ public abstract class FastHashMap<K, V> extends FastHashBase<K> implements JenaM
         var pIndex = findPosition(key, hashCode);
         if (pIndex < 0) {
             if (tryGrowPositionsArrayIfNeeded()) {
-                pIndex = findPosition(key, hashCode);
+                pIndex = ~findEmptySlotWithoutEqualityCheck(hashCode);
             }
+            final var value = absentValueSupplier.get();
             final var eIndex = getFreeKeyIndex();
             keys[eIndex] = key;
             hashCodesOrDeletedIndices[eIndex] = hashCode;
-            final var value = absentValueSupplier.get();
             values[eIndex] = value;
             positions[~pIndex] = ~eIndex;
             return value;
@@ -194,18 +235,20 @@ public abstract class FastHashMap<K, V> extends FastHashBase<K> implements JenaM
 
     @Override
     public void compute(K key, UnaryOperator<V> valueProcessor) {
-        final int hashCode = key.hashCode();
+        final var hashCode = key.hashCode();
         var pIndex = findPosition(key, hashCode);
         if (pIndex < 0) {
             final var value = valueProcessor.apply(null);
             if (value == null)
                 return;
+            if(tryGrowPositionsArrayIfNeeded()) {
+                pIndex = ~findEmptySlotWithoutEqualityCheck(hashCode);
+            }
             final var eIndex = getFreeKeyIndex();
             keys[eIndex] = key;
             hashCodesOrDeletedIndices[eIndex] = hashCode;
             values[eIndex] = value;
             positions[~pIndex] = ~eIndex;
-            tryGrowPositionsArrayIfNeeded();
         } else {
             var eIndex = ~positions[pIndex];
             final var value = valueProcessor.apply(values[eIndex]);
@@ -217,24 +260,13 @@ public abstract class FastHashMap<K, V> extends FastHashBase<K> implements JenaM
         }
     }
 
-
     @Override
     public ExtendedIterator<V> valueIterator() {
-        final var initialSize = size();
-        final Runnable checkForConcurrentModification = () ->
-        {
-            if (size() != initialSize) throw new ConcurrentModificationException();
-        };
-        return new SparseArrayIterator<>(values, keysPos, checkForConcurrentModification);
+        return new SparseArrayIterator<>(values, keysPos, this);
     }
 
     @Override
     public Spliterator<V> valueSpliterator() {
-        final var initialSize = this.size();
-        final Runnable checkForConcurrentModification = () ->
-        {
-            if (this.size() != initialSize) throw new ConcurrentModificationException();
-        };
-        return new SparseArraySpliterator<>(values, keysPos, checkForConcurrentModification);
+        return new SparseArraySpliterator<>(values, keysPos, this);
     }
 }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java
index 134a0092e2..5adf3232c4 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java
@@ -21,39 +21,42 @@
 
 package org.apache.jena.mem.collection;
 
-import org.apache.jena.mem.iterator.SparseArrayIndexedIterator;
-import org.apache.jena.mem.spliterator.SparseArrayIndexedSpliterator;
-import org.apache.jena.util.iterator.ExtendedIterator;
-
-import java.util.ConcurrentModificationException;
-import java.util.Spliterator;
-import java.util.stream.Stream;
-import java.util.stream.StreamSupport;
-
 /**
- * Set which grows, if needed but never shrinks.
- * This set does not guarantee any order. Although due to the way it is implemented the elements have a certain order.
- * This set does not allow null values.
- * This set is not thread safe.
- * It´s purpose is to support fast add, remove, contains and stream / iterate operations.
- * Only remove operations are not as fast as in {@link java.util.HashSet}
- * Iterating over this set not get much faster again after removing elements because the set is not compacted.
+ * Hash set specialization built on top of {@link FastHashBase}.
+ * Grows on demand but never shrinks, does not guarantee iteration order,
+ * does not allow {@code null} elements, and is not thread-safe.
+ * <p>
+ * Optimized for fast {@code add} / {@code containsKey} / {@code stream} /
+ * iterate operations. Removal is somewhat slower than in
+ * {@link java.util.HashSet} because of the back-shifting performed on the
+ * probe table. Iteration speed does not recover after many removals because
+ * the dense {@code keys} array is not compacted.
+ *
+ * @param <K> the element type
  */
-public abstract class FastHashSet<K> extends FastHashBase<K> implements JenaSetHashOptimized<K> {
+public abstract class FastHashSet<K> extends FastHashBase<K> implements JenaSetIndexed<K> {
 
-    protected FastHashSet(int initialSize) {
+    /**
+     * Creates a set with the given initial key-array capacity.
+     *
+     * @param initialSize the initial capacity of the keys array
+     */
+    public FastHashSet(final int initialSize) {
         super(initialSize);
     }
 
-    protected FastHashSet() {
+    /**
+     * Creates a set with the default initial capacity.
+     */
+    public FastHashSet() {
         super();
     }
 
     /**
-     * Copy constructor.
-     * The new set will contain all the same keys of the set to copy.
+     * Copy constructor. The new set contains the same elements as
+     * {@code setToCopy}.
      *
-     * @param setToCopy
+     * @param setToCopy the source set
      */
     protected FastHashSet(final FastHashSet<K> setToCopy) {
         super(setToCopy);
@@ -65,12 +68,12 @@ public abstract class FastHashSet<K> extends FastHashBase<K> implements JenaSetH
     }
 
     @Override
-    public boolean tryAdd(K value, int hashCode) {
+    public boolean tryAdd(K key, int hashCode) {
         growPositionsArrayIfNeeded();
-        var pIndex = findPosition(value, hashCode);
+        final var pIndex = findPosition(key, hashCode);
         if (pIndex < 0) {
             final var eIndex = getFreeKeyIndex();
-            keys[eIndex] = value;
+            keys[eIndex] = key;
             hashCodesOrDeletedIndices[eIndex] = hashCode;
             positions[~pIndex] = ~eIndex;
             return true;
@@ -79,28 +82,23 @@ public abstract class FastHashSet<K> extends FastHashBase<K> implements JenaSetH
     }
 
     /**
-     * Add and get the index of the added element.
+     * Add an element and return the index it was stored at.
+     * If the element is already present, returns the bitwise complement
+     * ({@code ~existingIndex}) of the existing index, so callers can
+     * distinguish "newly inserted" from "already present" while still
+     * recovering the index in both cases.
      *
-     * @param value the value to add
-     * @return the index of the added element or the inverse (~) index of the existing element
+     * @param key the element to add
+     * @return the new index, or {@code ~existingIndex} if already present
      */
-    public int addAndGetIndex(K value) {
-        return addAndGetIndex(value, value.hashCode());
-    }
-
-    /**
-     * Add and get the index of the added element.
-     *
-     * @param value    the value to add
-     * @param hashCode the hash code of the value. This is a performance optimization.
-     * @return the index of the added element or the inverse (~) index of the existing element
-     */
-    public int addAndGetIndex(final K value, final int hashCode) {
+    @Override
+    public int addAndGetIndex(K key) {
         growPositionsArrayIfNeeded();
-        final var pIndex = findPosition(value, hashCode);
+        final var hashCode = key.hashCode();
+        final var pIndex = findPosition(key, hashCode);
         if (pIndex < 0) {
             final var eIndex = getFreeKeyIndex();
-            keys[eIndex] = value;
+            keys[eIndex] = key;
             hashCodesOrDeletedIndices[eIndex] = hashCode;
             positions[~pIndex] = ~eIndex;
             return eIndex;
@@ -132,62 +130,4 @@ public abstract class FastHashSet<K> extends FastHashBase<K> implements JenaSetH
     public K getKeyAt(int i) {
         return keys[i];
     }
-
-    /**
-     * Entry pairing a key with its index in the set.
-     * @param index index of the key in the set
-     * @param key the key
-     * @param <K> the type of the key
-     */
-    public record IndexedKey<K>(int index, K key) {}
-
-    /**
-     * Get an iterator over pairs of keys and their indices in the set.
-     * The iterator is not thread safe.
-     *
-     * @return an iterator over pairs of keys and their indices in the set
-     */
-    public final ExtendedIterator<IndexedKey<K>> indexedKeyIterator() {
-        final var initialSize = size();
-        final Runnable checkForConcurrentModification = () ->
-        {
-            if (size() != initialSize) throw new ConcurrentModificationException();
-        };
-        return new SparseArrayIndexedIterator<>(keys, keysPos, checkForConcurrentModification);
-    }
-
-    /**
-     * Get a spliterator over pairs of keys and their indices in the set.
-     * The spliterator is not thread safe.
-     *
-     * @return a spliterator over pairs of keys and their indices in the set
-     */
-    public final Spliterator<IndexedKey<K>> indexedKeySpliterator() {
-        final var initialSize = this.size();
-        final Runnable checkForConcurrentModification = () ->
-        {
-            if (this.size() != initialSize) throw new ConcurrentModificationException();
-        };
-        return new SparseArrayIndexedSpliterator<>(keys, keysPos, checkForConcurrentModification);
-    }
-
-    /**
-     * Get a stream over pairs of keys and their indices in the set.
-     * The stream is not thread safe.
-     *
-     * @return a stream over pairs of keys and their indices in the set
-     */
-    public final Stream<IndexedKey<K>> indexedKeyStream() {
-        return StreamSupport.stream(indexedKeySpliterator(), false);
-    }
-
-    /**
-     * Get a parallel stream over pairs of keys and their indices in the set.
-     * The stream is not thread safe.
-     *
-     * @return a parallel stream over pairs of keys and their indices in the set
-     */
-    public final Stream<IndexedKey<K>> indexedKeyStreamParallel() {
-        return StreamSupport.stream(indexedKeySpliterator(), true);
-    }
 }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java
index 5664a90017..b277789c71 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java
@@ -25,7 +25,6 @@ import org.apache.jena.mem.spliterator.SparseArraySpliterator;
 import org.apache.jena.shared.JenaException;
 import org.apache.jena.util.iterator.ExtendedIterator;
 
-import java.util.ConcurrentModificationException;
 import java.util.Spliterator;
 import java.util.function.Predicate;
 
@@ -36,7 +35,7 @@ import java.util.function.Predicate;
  *
  * @param <E> the element type
  */
-public abstract class HashCommonBase<E> {
+public abstract class HashCommonBase<E> implements JenaMapSetCommon<E> {
     /**
      * Jeremy suggests, from his experiments, that load factors more than
      * 0.6 leave the table too dense, and little advantage is gained below 0.4.
@@ -78,7 +77,7 @@ public abstract class HashCommonBase<E> {
      * Copy constructor.
      * The new table will contain all the same keys of the table to copy.
      *
-     * @param baseToCopy
+     * @param baseToCopy the table to copy
      */
     protected HashCommonBase(final HashCommonBase<E> baseToCopy) {
         this.keys = newKeysArray(baseToCopy.keys.length);
@@ -209,18 +208,10 @@ public abstract class HashCommonBase<E> {
     }
 
     public ExtendedIterator<E> keyIterator() {
-        final var initialSize = size;
-        final Runnable checkForConcurrentModification = () -> {
-            if (size != initialSize) throw new ConcurrentModificationException();
-        };
-        return new SparseArrayIterator<>(keys, checkForConcurrentModification);
+        return new SparseArrayIterator<>(keys, this);
     }
 
     public Spliterator<E> keySpliterator() {
-        final var initialSize = size;
-        final Runnable checkForConcurrentModification = () -> {
-            if (size != initialSize) throw new ConcurrentModificationException();
-        };
-        return new SparseArraySpliterator<>(keys, checkForConcurrentModification);
+        return new SparseArraySpliterator<>(keys, this);
     }
 }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java
index 62e7bd5673..dcdd555765 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java
@@ -24,7 +24,6 @@ import org.apache.jena.mem.iterator.SparseArrayIterator;
 import org.apache.jena.mem.spliterator.SparseArraySpliterator;
 import org.apache.jena.util.iterator.ExtendedIterator;
 
-import java.util.ConcurrentModificationException;
 import java.util.Spliterator;
 import java.util.function.Supplier;
 import java.util.function.UnaryOperator;
@@ -207,19 +206,11 @@ public abstract class HashCommonMap<K, V> extends HashCommonBase<K> implements J
 
     @Override
     public ExtendedIterator<V> valueIterator() {
-        final var initialSize = size;
-        final Runnable checkForConcurrentModification = () -> {
-            if (size != initialSize) throw new ConcurrentModificationException();
-        };
-        return new SparseArrayIterator<>(values, checkForConcurrentModification);
+        return new SparseArrayIterator<>(values, this);
     }
 
     @Override
     public Spliterator<V> valueSpliterator() {
-        final var initialSize = size;
-        final Runnable checkForConcurrentModification = () -> {
-            if (size != initialSize) throw new ConcurrentModificationException();
-        };
-        return new SparseArraySpliterator<>(values, checkForConcurrentModification);
+        return new SparseArraySpliterator<>(values, this);
     }
 }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java
index 3e13613b08..6d2423e009 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java
@@ -30,6 +30,7 @@ import java.util.stream.StreamSupport;
 
 /**
  * A map from keys of type {@code K} to values of type {@code V}.
+ * Not thread-safe and does not allow {@code null} keys.
  *
  * @param <K> the type of the keys in the map
  * @param <V> the type of the values in the map
diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java
new file mode 100644
index 0000000000..67c366d00e
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+package org.apache.jena.mem.collection;
+
+/**
+ * Extension of {@link JenaMap} that exposes index-based access and lets callers
+ * supply a precomputed hash code for the key. Indices are stable handles to
+ * entries (returned by {@link #putAndGetIndex(Object, Object)}) and remain
+ * valid until the corresponding entry is removed.
+ * <p>
+ * The hash-code overloads are a performance shortcut for callers that already
+ * have the hash at hand (typically because the same key is stored in several
+ * collections). The supplied hash code MUST equal {@code key.hashCode()}, or
+ * the map will misbehave.
+ *
+ * @param <K> the type of the keys in the map
+ * @param <V> the type of the values in the map
+ */
+public interface JenaMapIndexed<K, V> extends JenaMap<K, V> {
+
+    /**
+     * Returns the index of the entry with the given key, or a negative value
+     * if no such entry exists.
+     *
+     * @param key the key to look up
+     * @return the index of the entry, or a negative value if absent
+     */
+    int indexOf(K key);
+
+    /**
+     * Returns the key stored at the given index.
+     *
+     * @param index the index of the entry
+     * @return the key at that index
+     */
+    K getKeyAt(int index);
+
+    /**
+     * Returns the value stored at the given index.
+     *
+     * @param index the index of the entry
+     * @return the value at that index
+     */
+    V getValueAt(int index);
+
+    /**
+     * Put a key-value pair and return the index of the affected entry.
+     * If the key is already present, its value is updated and the existing
+     * index is returned.
+     *
+     * @param key   the key to put
+     * @param value the value to put
+     * @return the index of the entry holding {@code key}
+     */
+    int putAndGetIndex(K key, V value);
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java
index 2533714ce6..7f96baa19f 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java
@@ -28,22 +28,23 @@ import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 
 /**
- * Common interface for {@link JenaMap} and {@link JenaSet}. *
+ * Operations shared between the map ({@link JenaMap}) and the set
+ * ({@link JenaSet}) collections used in the {@code mem} triple store
+ * implementations.
+ * <p>
+ * These collections trade some flexibility for speed: they expose only the
+ * operations needed by triple-store internals (no full {@link java.util.Map}
+ * or {@link java.util.Set} contract). They are not thread-safe.
  *
- * @param <E> the type of the keys/elements in the collection
+ * @param <E> the type of the keys (or elements, for sets) in the collection
  */
-public interface JenaMapSetCommon<E> {
+public interface JenaMapSetCommon<E> extends Sized {
 
     /**
      * Clear the collection.
      */
     void clear();
 
-    /**
-     * @return the number of elements in the collection
-     */
-    int size();
-
     /**
      * @return true if the collection is empty
      */
@@ -75,7 +76,10 @@ public interface JenaMapSetCommon<E> {
 
     /**
      * Removes a key from the collection.
-     * Attention: Implementations may assume that the key is present.
+     * <p>
+     * Attention: implementations may assume the key is present and may produce
+     * undefined behavior (including silently corrupting internal state) if it
+     * is not. Use {@link #tryRemove(Object)} when in doubt.
      *
      * @param key the key to remove
      */
diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java
index d3b8a557be..03848073f5 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java
@@ -21,9 +21,10 @@
 package org.apache.jena.mem.collection;
 
 /**
- * Set interface specialized for the use cases in triple store implementations.
+ * Set interface specialized for the use cases in triple-store implementations.
+ * Not thread-safe; does not allow {@code null} elements.
  *
- * @param <E>
+ * @param <E> the element type of the set
  */
 public interface JenaSet<E> extends JenaMapSetCommon<E> {
 
@@ -31,13 +32,16 @@ public interface JenaSet<E> extends JenaMapSetCommon<E> {
      * Add the key to the set if it is not already present.
      *
      * @param key the key to add
-     * @return true if the key was added, false if it was already present
+     * @return {@code true} if the key was added, {@code false} if it was already present
      */
     boolean tryAdd(E key);
 
     /**
-     * Add the key to the set without checking if it is already present.
-     * Attention: This method must only be used if it is guaranteed that the key is not already present.
+     * Add the key to the set without checking whether it is already present.
+     * <p>
+     * Attention: this method must only be used if the caller has ensured that
+     * the key is not already in the set; otherwise the set's invariants will
+     * break (duplicates may be inserted).
      *
      * @param key the key to add
      */
diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java
index 8cc8aad8da..0e1d032b35 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java
@@ -22,17 +22,50 @@ package org.apache.jena.mem.collection;
 
 
 /**
- * Extension of {@link JenaSet} that allows to add and remove elements
- * with a given hash code.
- * This is useful if the hash code is already known.
- * Attention: The hash code must be consistent with E::hashCode().
+ * Extension of {@link JenaSet} that lets callers supply a precomputed hash
+ * code.
+ * <p>
+ * Attention: any caller-supplied hash code MUST equal {@code E.hashCode()};
+ * if it does not, the set will misbehave.
+ *
+ * @param <E> the element type of the set
  */
 public interface JenaSetHashOptimized<E> extends JenaSet<E> {
+
+    /**
+     * Add an element with the given precomputed hash code if it is not
+     * already present.
+     *
+     * @param key      the element to add
+     * @param hashCode {@code key.hashCode()}
+     * @return {@code true} if added, {@code false} if already present
+     */
     boolean tryAdd(E key, int hashCode);
 
+    /**
+     * Add an element with the given precomputed hash code without checking
+     * whether it is already present. The caller MUST ensure the key is absent.
+     *
+     * @param key      the element to add
+     * @param hashCode {@code key.hashCode()}
+     */
     void addUnchecked(E key, int hashCode);
 
+    /**
+     * Try to remove an element with the given precomputed hash code.
+     *
+     * @param key      the element to remove
+     * @param hashCode {@code key.hashCode()}
+     * @return {@code true} if removed, {@code false} if it was not present
+     */
     boolean tryRemove(E key, int hashCode);
 
+    /**
+     * Remove an element assumed to be present, with the given precomputed
+     * hash code. Behavior is undefined if the element is not in the set.
+     *
+     * @param key      the element to remove
+     * @param hashCode {@code key.hashCode()}
+     */
     void removeUnchecked(E key, int hashCode);
 }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java
new file mode 100644
index 0000000000..c7c3d2e1dd
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+package org.apache.jena.mem.collection;
+
+
+/**
+ * Extension of {@link JenaSetHashOptimized} that exposes index-based access to elements.
+ * Indices are stable handles to entries (returned by {@link #addAndGetIndex(Object)}) and remain
+ * valid until the corresponding entry is removed.
+ *
+ * @param <E> the element type of the set
+ */
+public interface JenaSetIndexed<E> extends JenaSetHashOptimized<E> {
+
+    /**
+     * Add an element and return the index it was stored at. If the element
+     * is already present, returns a negative value (typically the bitwise
+     * complement of the existing index).
+     *
+     * @param key      the element to add
+     * @return the index of the inserted element, or a negative value if the
+     *         element was already present
+     */
+    int addAndGetIndex(final E key);
+
+    /**
+     * Returns the element stored at the given index.
+     *
+     * @param index the index to read
+     * @return the element at that index
+     */
+    E getKeyAt(int index);
+
+    /**
+     * Returns the index of the given element, or a negative value if it is
+     * not in the set.
+     *
+     * @param key the element to look up
+     * @return the index of {@code key}, or a negative value if absent
+     */
+    int indexOf(E key);
+}
diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java
similarity index 68%
copy from jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java
copy to jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java
index 868a79a34d..237740ce8e 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java
@@ -19,12 +19,17 @@
  *   SPDX-License-Identifier: Apache-2.0
  */
 
-package org.apache.jena.mem;
+package org.apache.jena.mem.collection;
 
-public class GraphMemFastTest extends AbstractGraphMemTest {
+/**
+ * Base interface for sized collections.
+ * It is typically used to detect concurrent modifications in iterators and spliterators
+ * by snapshotting the size at construction time and rechecking it at each advance/forEach boundary.
+ */
+public interface Sized {
 
-    @Override
-    protected GraphMem createGraph() {
-        return new GraphMemFast();
-    }
+    /**
+     * @return the number of elements in the collection
+     */
+    int size();
 }
\ No newline at end of file
diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java
index b0ac6e994b..8cfc8948a2 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java
@@ -30,16 +30,27 @@ import java.util.NoSuchElementException;
 import java.util.function.Consumer;
 
 /**
- * Iterator that iterates over the entries of sets which are contained in the given iterator of sets.
+ * Flat-map style iterator that yields every element of every {@link JenaSet}
+ * produced by the given parent iterator. Empty inner sets are silently
+ * skipped. Equivalent in spirit to a one-level {@code flatMap} but tailored
+ * to the {@link JenaSet} API and to {@link NiceIterator}.
  *
- * @param <E> the type of the elements
+ * @param <E> the element type of the inner sets
  */
 public class IteratorOfJenaSets<E> extends NiceIterator<E> {
 
-    final Iterator<? extends JenaSet<E>> parentIterator;
+    /** Source iterator producing the sets to flatten. */
+    private final Iterator<? extends JenaSet<E>> parentIterator;
 
-    ExtendedIterator<E> currentIterator;
+    /** Iterator over the keys of the set currently being consumed. */
+    private ExtendedIterator<E> currentIterator;
 
+    /**
+     * Create a flat iterator over the elements of every set produced by
+     * {@code parentIterator}.
+     *
+     * @param parentIterator the source iterator of sets
+     */
     public IteratorOfJenaSets(Iterator<? extends JenaSet<E>> parentIterator) {
         this.parentIterator = parentIterator;
         this.currentIterator = parentIterator.hasNext()
diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java
deleted file mode 100644
index 37f103eae2..0000000000
--- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.
- *
- *   SPDX-License-Identifier: Apache-2.0
- */
-
-package org.apache.jena.mem.iterator;
-
-import org.apache.jena.mem.collection.FastHashSet;
-import org.apache.jena.util.iterator.NiceIterator;
-
-import java.util.Iterator;
-import java.util.NoSuchElementException;
-import java.util.function.Consumer;
-
-/**
- * An iterator over a sparse array, that skips null entries.
- * This iterator returns elements as {@link FastHashSet.IndexedKey} objects,
- * which contain both the index and the value of the element.
- *
- * The iterator works in ascending order, starting from index 0 up to the specified exclusive index.
- *
- * This iterator will check for concurrent modifications by invoking a {@link Runnable}
- *
- * @param <E> the type of the array elements
- */
-@SuppressWarnings("all")
-public class SparseArrayIndexedIterator<E> extends NiceIterator<FastHashSet.IndexedKey<E>> implements Iterator<FastHashSet.IndexedKey<E>> {
-
-    private final E[] entries;
-    private final Runnable checkForConcurrentModification;
-    private int pos = 0;
-    private final int toIndexExclusive;
-    private boolean hasNext = false;
-
-    public SparseArrayIndexedIterator(final E[] entries, final Runnable checkForConcurrentModification) {
-        this.entries = entries;
-        this.toIndexExclusive = entries.length;
-        this.checkForConcurrentModification = checkForConcurrentModification;
-    }
-
-    public SparseArrayIndexedIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) {
-        this.entries = entries;
-        this.toIndexExclusive = toIndexExclusive;
-        this.checkForConcurrentModification = checkForConcurrentModification;
-    }
-
-    /**
-     * Returns {@code true} if the iteration has more elements.
-     * (In other words, returns {@code true} if {@link #next} would
-     * return an element rather than throwing an exception.)
-     *
-     * @return {@code true} if the iteration has more elements
-     */
-    @Override
-    public boolean hasNext() {
-        while (toIndexExclusive > pos) {
-            if (null != entries[pos]) {
-                hasNext = true;
-                return true;
-            }
-            pos++;
-        }
-        hasNext = false;
-        return false;
-    }
-
-    /**
-     * Returns the next element in the iteration.
-     *
-     * @return the next element in the iteration
-     * @throws NoSuchElementException if the iteration has no more elements
-     */
-    @Override
-    public FastHashSet.IndexedKey<E> next() {
-        this.checkForConcurrentModification.run();
-        if (hasNext || hasNext()) {
-            hasNext = false;
-            return new FastHashSet.IndexedKey<>(pos, entries[pos++]);
-        }
-        throw new NoSuchElementException();
-    }
-
-    @Override
-    public void forEachRemaining(Consumer<? super FastHashSet.IndexedKey<E>> action) {
-        while (toIndexExclusive > pos) {
-            if (null != entries[pos]) {
-                action.accept(new FastHashSet.IndexedKey<>(pos, entries[pos]));
-            }
-            pos++;
-        }
-        this.checkForConcurrentModification.run();
-    }
-}
diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java
index 936476a80f..e0b79cd1ff 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java
@@ -21,34 +21,55 @@
 
 package org.apache.jena.mem.iterator;
 
+import org.apache.jena.mem.collection.Sized;
 import org.apache.jena.util.iterator.NiceIterator;
 
-import java.util.Iterator;
+import java.util.ConcurrentModificationException;
 import java.util.NoSuchElementException;
 import java.util.function.Consumer;
 
 /**
- * An iterator over a sparse array, that skips null entries.
+ * Iterator over a sparse array, walking from high index to low and skipping
+ * {@code null} entries. Detects concurrent modifications by snapshotting
+ * {@code set.size()} at construction time and rechecking it on each call to
+ * {@link #next()} / {@link #forEachRemaining(Consumer)}; throws
+ * {@link ConcurrentModificationException} if the size has changed.
  *
  * @param <E> the type of the array elements
  */
-public class SparseArrayIterator<E> extends NiceIterator<E> implements Iterator<E> {
+public class SparseArrayIterator<E> extends NiceIterator<E> {
 
     private final E[] entries;
-    private final Runnable checkForConcurrentModification;
+    private final Sized set;
+    private final int sizeOfSetAtStart;
     private int pos;
     private boolean hasNext = false;
 
-    public SparseArrayIterator(final E[] entries, final Runnable checkForConcurrentModification) {
+    /**
+     * Iterate over the whole array.
+     *
+     * @param entries the backing array (not copied)
+     * @param set     the owning collection used to detect concurrent modifications
+     */
+    public SparseArrayIterator(final E[] entries, final Sized set) {
         this.entries = entries;
         this.pos = entries.length - 1;
-        this.checkForConcurrentModification = checkForConcurrentModification;
+        this.set = set;
+        this.sizeOfSetAtStart = set.size();
     }
 
-    public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) {
+    /**
+     * Iterate over {@code entries[0 .. toIndexExclusive)} (in reverse order).
+     *
+     * @param entries          the backing array (not copied)
+     * @param toIndexExclusive exclusive upper bound on the iterated slice
+     * @param set              the owning collection used to detect concurrent modifications
+     */
+    public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Sized set) {
         this.entries = entries;
         this.pos = toIndexExclusive - 1;
-        this.checkForConcurrentModification = checkForConcurrentModification;
+        this.set = set;
+        this.sizeOfSetAtStart = set.size();
     }
 
     /**
@@ -62,13 +83,11 @@ public class SparseArrayIterator<E> extends NiceIterator<E> implements Iterator<
     public boolean hasNext() {
         while (-1 < pos) {
             if (null != entries[pos]) {
-                hasNext = true;
-                return true;
+                return hasNext = true;
             }
             pos--;
         }
-        hasNext = false;
-        return false;
+        return hasNext = false;
     }
 
     /**
@@ -79,7 +98,7 @@ public class SparseArrayIterator<E> extends NiceIterator<E> implements Iterator<
      */
     @Override
     public E next() {
-        this.checkForConcurrentModification.run();
+        if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException();
         if (hasNext || hasNext()) {
             hasNext = false;
             return entries[pos--];
@@ -95,6 +114,6 @@ public class SparseArrayIterator<E> extends NiceIterator<E> implements Iterator<
             }
             pos--;
         }
-        this.checkForConcurrentModification.run();
+        if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException();
     }
 }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java
index 94008b155f..d8536f5631 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java
@@ -22,8 +22,17 @@
 package org.apache.jena.mem.pattern;
 
 /**
- * A pattern for matching triples.
- * The pattern is defined by the wildcard positions for the subject, predicate and object.
+ * Categorizes a triple-match pattern by which of the subject, predicate and
+ * object slots are concrete and which are wildcards (i.e. {@code Node.ANY}
+ * or {@code null}).
+ * <p>
+ * The eight enum values cover every possible combination. Triple-store
+ * implementations dispatch on this enum to pick the most efficient lookup
+ * path for each kind of pattern (e.g. a fully concrete {@link #SUB_PRE_OBJ}
+ * is answered directly from the triple set, while a partially open pattern
+ * such as {@link #ANY_PRE_OBJ} is answered through an index intersection).
+ *
+ * @see PatternClassifier
  */
 public enum MatchPattern {
     /**
diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java
index 32a6ba182a..e4cf5644ec 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java
@@ -25,14 +25,15 @@ import org.apache.jena.graph.Node;
 import org.apache.jena.graph.Triple;
 
 /**
- * Classify a triple match into one of the 8 match patterns.
+ * Utility class that classifies a triple match into one of the eight
+ * {@link MatchPattern} values.
  * <p>
- * The classification is based on the concrete-ness of the subject, predicate and object.
- * A concrete node is one that is not a variable.
+ * The classification is based on which of the subject, predicate and object
+ * are <em>concrete</em> (anything that is not a variable / wildcard /
+ * {@code null}) and which are wildcards. The result is used by triple-store
+ * implementations to dispatch to the most efficient lookup path.
  * <p>
- * The classification is used to select the most efficient implementation of a triple store.
- * <p>
- * This is a utility class; there is no need to instantiate it.
+ * All operations are stateless; this class is not meant to be instantiated.
  *
  * @see MatchPattern
  */
@@ -41,8 +42,16 @@ public class PatternClassifier {
     private PatternClassifier() {
     }
 
+    /**
+     * Classify a triple match.
+     *
+     * @param tripleMatch the match triple, possibly containing wildcard nodes
+     * @return the corresponding {@link MatchPattern}
+     */
     public static MatchPattern classify(Triple tripleMatch) {
-        if (tripleMatch.isConcrete()) {
+        if (tripleMatch.getSubject().isConcrete()
+                && tripleMatch.getPredicate().isConcrete()
+                && tripleMatch.getObject().isConcrete()) {
             return MatchPattern.SUB_PRE_OBJ;
         } else {
             if (tripleMatch.getSubject().isConcrete()) {
@@ -73,6 +82,15 @@ public class PatternClassifier {
         }
     }
 
+    /**
+     * Classify a triple match given as three nodes.
+     * Any {@code null} or non-concrete node is treated as a wildcard.
+     *
+     * @param sm subject node, or {@code null}/wildcard
+     * @param pm predicate node, or {@code null}/wildcard
+     * @param om object node, or {@code null}/wildcard
+     * @return the corresponding {@link MatchPattern}
+     */
     public static MatchPattern classify(Node sm, Node pm, Node om) {
         if (null != sm && sm.isConcrete()) {
             if (null != pm && pm.isConcrete()) {
@@ -103,6 +121,5 @@ public class PatternClassifier {
                 }
             }
         }
-
     }
 }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java
index 43bbfeeaea..a5033c22cd 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java
@@ -21,52 +21,57 @@
 
 package org.apache.jena.mem.spliterator;
 
+import org.apache.jena.mem.collection.Sized;
+
+import java.util.ConcurrentModificationException;
 import java.util.Spliterator;
 import java.util.function.Consumer;
 
 /**
- * A spliterator for arrays. This spliterator will iterate over the array
- * entries within the given boundaries.
- * <p>
- * This spliterator supports splitting into sub-spliterators.
+ * Top-level spliterator over a contiguous array slice {@code [0, toIndex)},
+ * iterating from high index to low. Supports splitting into
+ * {@link ArraySubSpliterator} children for parallel traversal.
  * <p>
- * The spliterator will check for concurrent modifications by invoking a {@link Runnable}
- * before each action.
+ * Detects concurrent modifications by snapshotting {@code set.size()} at
+ * construction time and rechecking it at each advance/forEach boundary.
+ * Throws {@link ConcurrentModificationException} if the size has changed.
  *
- * @param <E>
+ * @param <E> the element type
  */
 public class ArraySpliterator<E> implements Spliterator<E> {
 
     private final E[] entries;
-    private final Runnable checkForConcurrentModification;
+    private final Sized set;
+    private final int sizeOfSetAtStart;
     private int pos;
 
     /**
-     * Create a spliterator for the given array, with the given size.
+     * Create a spliterator over {@code entries[0 .. toIndex)}.
      *
-     * @param entries                        the array
-     * @param toIndex                        the index of the last element, exclusive
-     * @param checkForConcurrentModification runnable to check for concurrent modifications
+     * @param entries the backing array (not copied)
+     * @param toIndex exclusive upper bound on the iterated slice
+     * @param set     the owning collection used to detect concurrent modifications
      */
-    public ArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) {
+    public ArraySpliterator(final E[] entries, final int toIndex, final Sized set) {
         this.entries = entries;
         this.pos = toIndex;
-        this.checkForConcurrentModification = checkForConcurrentModification;
+        this.set = set;
+        this.sizeOfSetAtStart = set.size();
     }
 
     /**
-     * Create a spliterator for the given array, with the given size.
+     * Create a spliterator over the entire array.
      *
-     * @param entries                        the array
-     * @param checkForConcurrentModification runnable to check for concurrent modifications
+     * @param entries the backing array (not copied)
+     * @param set     the owning collection used to detect concurrent modifications
      */
-    public ArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) {
-        this(entries, entries.length, checkForConcurrentModification);
+    public ArraySpliterator(final E[] entries, final Sized set) {
+        this(entries, entries.length, set);
     }
 
     @Override
     public boolean tryAdvance(Consumer<? super E> action) {
-        this.checkForConcurrentModification.run();
+        if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException();
         if (-1 < --pos) {
             action.accept(entries[pos]);
             return true;
@@ -79,7 +84,7 @@ public class ArraySpliterator<E> implements Spliterator<E> {
         while (-1 < --pos) {
             action.accept(entries[pos]);
         }
-        this.checkForConcurrentModification.run();
+        if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException();
     }
 
     @Override
@@ -89,7 +94,7 @@ public class ArraySpliterator<E> implements Spliterator<E> {
         }
         final int toIndexOfSubIterator = this.pos;
         this.pos = pos >>> 1;
-        return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification);
+        return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set);
     }
 
     @Override
@@ -101,4 +106,4 @@ public class ArraySpliterator<E> implements Spliterator<E> {
     public int characteristics() {
         return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE;
     }
-}
+}
\ No newline at end of file
diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java
index 74994708b5..638f2bb0c9 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java
@@ -21,55 +21,61 @@
 
 package org.apache.jena.mem.spliterator;
 
+import org.apache.jena.mem.collection.Sized;
+
+import java.util.ConcurrentModificationException;
 import java.util.Spliterator;
 import java.util.function.Consumer;
 
 /**
- * A spliterator for arrays. This spliterator will iterate over the array
- * entries within the given boundaries.
- * <p>
- * This spliterator supports splitting into sub-spliterators.
+ * Sub-range spliterator over a contiguous array slice {@code [fromIndex, toIndex)},
+ * iterating from high index to low. Produced by splitting an
+ * {@link ArraySpliterator} (or another {@link ArraySubSpliterator}); supports
+ * further recursive splits for parallel traversal.
  * <p>
- * The spliterator will check for concurrent modifications by invoking a {@link Runnable}
- * before each action.
+ * Detects concurrent modifications by snapshotting {@code set.size()} at
+ * construction time and rechecking it at each advance/forEach boundary.
+ * Throws {@link ConcurrentModificationException} if the size has changed.
  *
- * @param <E>
+ * @param <E> the element type
  */
 public class ArraySubSpliterator<E> implements Spliterator<E> {
 
     private final E[] entries;
     private final int fromIndex;
-    private final Runnable checkForConcurrentModification;
+    private final Sized set;
+    private final int sizeOfSetAtStart;
     private int pos;
 
     /**
-     * Create a spliterator for the given array, with the given size.
+     * Create a spliterator over {@code entries[fromIndex .. toIndex)}.
      *
-     * @param entries                        the array
-     * @param fromIndex                      the index of the first element, inclusive
-     * @param toIndex                        the index of the last element, exclusive
-     * @param checkForConcurrentModification runnable to check for concurrent modifications
+     * @param entries   the backing array (not copied)
+     * @param fromIndex inclusive lower bound on the iterated slice
+     * @param toIndex   exclusive upper bound on the iterated slice
+     * @param set       the owning collection used to detect concurrent modifications
      */
-    public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) {
+    public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) {
         this.entries = entries;
         this.fromIndex = fromIndex;
         this.pos = toIndex;
-        this.checkForConcurrentModification = checkForConcurrentModification;
+        this.set = set;
+        this.sizeOfSetAtStart = set.size();
     }
 
     /**
-     * Create a spliterator for the given array, with the given size.
+     * Create a spliterator over the entire array.
      *
-     * @param entries                        the array
-     * @param checkForConcurrentModification runnable to check for concurrent modifications
+     * @param entries the backing array (not copied)
+     * @param set     the owning collection used to detect concurrent modifications
      */
-    public ArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) {
-        this(entries, 0, entries.length, checkForConcurrentModification);
+    public ArraySubSpliterator(final E[] entries, final Sized set) {
+        this(entries, 0, entries.length, set);
     }
 
     @Override
     public boolean tryAdvance(Consumer<? super E> action) {
-        this.checkForConcurrentModification.run();
+        if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException();
         if (fromIndex <= --pos) {
             action.accept(entries[pos]);
             return true;
@@ -82,7 +88,7 @@ public class ArraySubSpliterator<E> implements Spliterator<E> {
         while (fromIndex <= --pos) {
             action.accept(entries[pos]);
         }
-        this.checkForConcurrentModification.run();
+        if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException();
     }
 
     @Override
@@ -93,7 +99,7 @@ public class ArraySubSpliterator<E> implements Spliterator<E> {
         }
         final int toIndexOfSubIterator = this.pos;
         this.pos = fromIndex + (entriesCount >>> 1);
-        return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification);
+        return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set);
     }
 
     @Override
@@ -105,4 +111,4 @@ public class ArraySubSpliterator<E> implements Spliterator<E> {
     public int characteristics() {
         return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE;
     }
-}
+}
\ No newline at end of file
diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java
deleted file mode 100644
index 704c964270..0000000000
--- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.
- *
- *   SPDX-License-Identifier: Apache-2.0
- */
-
-package org.apache.jena.mem.spliterator;
-
-import java.util.Spliterator;
-import java.util.function.Consumer;
-
-import org.apache.jena.mem.collection.FastHashSet;
-
-/**
- * A spliterator for sparse arrays. This spliterator will iterate over the array
- * skipping null entries.
- * This spliterator returns elements as {@link FastHashSet.IndexedKey} objects,
- * which contain both the index and the value of the element.
- * <p>
- * This spliterator works in ascending order, starting from the given start up to the specified exclusive index.
- * <p>
- * This spliterator supports splitting into sub-spliterators.
- * <p>
- * The spliterator will check for concurrent modifications by invoking a {@link Runnable}
- * before each action.
- *
- * @param <E> the type of the array elements
- */
-@SuppressWarnings("all")
-public class SparseArrayIndexedSpliterator<E> implements Spliterator<FastHashSet.IndexedKey<E>> {
-
-    private final E[] entries;
-    private int currentPositionMinusOne;
-    private final int toIndexExclusive;
-    private final Runnable checkForConcurrentModification;
-
-    /**
-     * Create a spliterator for the given array, with the given size.
-     *
-     * @param entries                        the array
-     * @param fromIndexInclusive             the index of the first element, inclusive
-     * @param toIndexExclusive               the index of the last element, exclusive
-     * @param checkForConcurrentModification runnable to check for concurrent modifications
-     */
-    public SparseArrayIndexedSpliterator(final E[] entries, final int fromIndexInclusive, final int toIndexExclusive, final Runnable checkForConcurrentModification) {
-        this.entries = entries;
-        this.currentPositionMinusOne = fromIndexInclusive-1; // Start at fromIndexInclusive - 1, so that the first call to tryAdvance will increment pos to fromIndexInclusive
-        this.toIndexExclusive = toIndexExclusive;
-        this.checkForConcurrentModification = checkForConcurrentModification;
-    }
-
-    /**
-     * Create a spliterator for the given array, with the given size.
-     *
-     * @param entries                        the array
-     * @param toIndexExclusive               the index of the last element, exclusive
-     * @param checkForConcurrentModification runnable to check for concurrent modifications
-     */
-    public SparseArrayIndexedSpliterator(final E[] entries, final int toIndexExclusive, final Runnable checkForConcurrentModification) {
-        this(entries, 0, toIndexExclusive, checkForConcurrentModification);
-    }
-
-    /**
-     * Create a spliterator for the given array, with the given size.
-     *
-     * @param entries                        the array
-     * @param checkForConcurrentModification runnable to check for concurrent modifications
-     */
-    public SparseArrayIndexedSpliterator(final E[] entries, final Runnable checkForConcurrentModification) {
-        this(entries, entries.length, checkForConcurrentModification);
-    }
-
-
-    @Override
-    public boolean tryAdvance(Consumer<? super FastHashSet.IndexedKey<E>> action) {
-        this.checkForConcurrentModification.run();
-        while (toIndexExclusive > ++currentPositionMinusOne) {
-            if (null != entries[currentPositionMinusOne]) {
-                action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne]));
-                return true;
-            }
-        }
-        return false;
-    }
-
-    @Override
-    public void forEachRemaining(Consumer<? super FastHashSet.IndexedKey<E>> action) {
-        while (toIndexExclusive > ++currentPositionMinusOne) {
-            if (null != entries[currentPositionMinusOne]) {
-                action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne]));
-            }
-        }
-        this.checkForConcurrentModification.run();
-    }
-
-    @Override
-    public Spliterator<FastHashSet.IndexedKey<E>> trySplit() {
-        final var nextPos = currentPositionMinusOne + 1;
-        final var remaining = toIndexExclusive - nextPos;
-        if ( remaining < 2) {
-            return null;
-        }
-        final var mid = nextPos + ( remaining >>> 1);
-        final var fromIndexInclusive = nextPos;
-        this.currentPositionMinusOne = mid-1;
-        return new SparseArrayIndexedSpliterator<>(entries, fromIndexInclusive, mid, checkForConcurrentModification);
-    }
-
-    @Override
-    public long estimateSize() { return (long) toIndexExclusive - currentPositionMinusOne; }
-
-    @Override
-    public int characteristics() {
-        return DISTINCT | NONNULL | IMMUTABLE;
-    }
-}
diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java
index 6752cc9a1c..add45739dc 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java
@@ -21,17 +21,24 @@
 
 package org.apache.jena.mem.spliterator;
 
+import org.apache.jena.mem.collection.Sized;
+
+import java.util.ConcurrentModificationException;
 import java.util.Spliterator;
 import java.util.function.Consumer;
 
 /**
- * A spliterator for sparse arrays. This spliterator will iterate over the array
- * skipping null entries.
- * <p>
- * This spliterator supports splitting into sub-spliterators.
+ * Top-level spliterator over a sparse array slice {@code [0, toIndex)},
+ * iterating from high index to low and skipping {@code null} entries.
+ * Produced for backing arrays such as those of
+ * {@link org.apache.jena.mem.collection.FastHashBase}, where removed slots
+ * are represented by {@code null}.
  * <p>
- * The spliterator will check for concurrent modifications by invoking a {@link Runnable}
- * before each action.
+ * Supports splitting into {@link SparseArraySubSpliterator} children for
+ * parallel traversal. Detects concurrent modifications by snapshotting
+ * {@code set.size()} at construction time and rechecking it at each
+ * advance/forEach boundary; throws {@link ConcurrentModificationException}
+ * if the size has changed.
  *
  * @param <E> the type of the array elements
  */
@@ -39,35 +46,37 @@ public class SparseArraySpliterator<E> implements Spliterator<E> {
 
     private final E[] entries;
     private int pos;
-    private final Runnable checkForConcurrentModification;
+    private final Sized set;
+    private final int sizeOfSetAtStart;
 
     /**
-     * Create a spliterator for the given array, with the given size.
+     * Create a spliterator over {@code entries[0 .. toIndex)}, skipping nulls.
      *
-     * @param entries                        the array
-     * @param toIndex                        the index of the last element, exclusive
-     * @param checkForConcurrentModification runnable to check for concurrent modifications
+     * @param entries the backing array (not copied)
+     * @param toIndex exclusive upper bound on the iterated slice
+     * @param set     the owning collection used to detect concurrent modifications
      */
-    public SparseArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) {
+    public SparseArraySpliterator(final E[] entries, final int toIndex, final Sized set) {
         this.entries = entries;
         this.pos = toIndex;
-        this.checkForConcurrentModification = checkForConcurrentModification;
+        this.set = set;
+        this.sizeOfSetAtStart = set.size();
     }
 
     /**
-     * Create a spliterator for the given array, with the given size.
+     * Create a spliterator over the entire array, skipping nulls.
      *
-     * @param entries                        the array
-     * @param checkForConcurrentModification runnable to check for concurrent modifications
+     * @param entries the backing array (not copied)
+     * @param set     the owning collection used to detect concurrent modifications
      */
-    public SparseArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) {
-        this(entries, entries.length, checkForConcurrentModification);
+    public SparseArraySpliterator(final E[] entries, final Sized set) {
+        this(entries, entries.length, set);
     }
 
 
     @Override
     public boolean tryAdvance(Consumer<? super E> action) {
-        this.checkForConcurrentModification.run();
+        if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException();
         while (-1 < --pos) {
             if (null != entries[pos]) {
                 action.accept(entries[pos]);
@@ -86,7 +95,7 @@ public class SparseArraySpliterator<E> implements Spliterator<E> {
             }
             pos--;
         }
-        this.checkForConcurrentModification.run();
+        if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException();
     }
 
     @Override
@@ -96,7 +105,7 @@ public class SparseArraySpliterator<E> implements Spliterator<E> {
         }
         final int toIndexOfSubIterator = this.pos;
         this.pos = pos >>> 1;
-        return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification);
+        return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set);
     }
 
     @Override
diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java
index 3eb0784326..d79242ac78 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java
@@ -21,55 +21,62 @@
 
 package org.apache.jena.mem.spliterator;
 
+import org.apache.jena.mem.collection.Sized;
+
+import java.util.ConcurrentModificationException;
 import java.util.Spliterator;
 import java.util.function.Consumer;
 
 /**
- * A spliterator for sparse arrays. This spliterator will iterate over the array
- * skipping null entries.
- * <p>
- * This spliterator supports splitting into sub-spliterators.
+ * Sub-range spliterator over a sparse array slice {@code [fromIndex, toIndex)},
+ * iterating from high index to low and skipping {@code null} entries.
+ * Produced by splitting a {@link SparseArraySpliterator} (or another
+ * {@link SparseArraySubSpliterator}); supports further recursive splits for
+ * parallel traversal.
  * <p>
- * The spliterator will check for concurrent modifications by invoking a {@link Runnable}
- * before each action.
+ * Detects concurrent modifications by snapshotting {@code set.size()} at
+ * construction time and rechecking it at each advance/forEach boundary;
+ * throws {@link ConcurrentModificationException} if the size has changed.
  *
- * @param <E>
+ * @param <E> the type of the array elements
  */
 public class SparseArraySubSpliterator<E> implements Spliterator<E> {
 
     private final E[] entries;
     private final int fromIndex;
-    private final Runnable checkForConcurrentModification;
+    private final Sized set;
+    private final int sizeOfSetAtStart;
     private int pos;
 
     /**
-     * Create a spliterator for the given array, with the given size.
+     * Create a spliterator over {@code entries[fromIndex .. toIndex)}, skipping nulls.
      *
-     * @param entries                        the array
-     * @param fromIndex                      the index of the first element, inclusive
-     * @param toIndex                        the index of the last element, exclusive
-     * @param checkForConcurrentModification runnable to check for concurrent modifications
+     * @param entries   the backing array (not copied)
+     * @param fromIndex inclusive lower bound on the iterated slice
+     * @param toIndex   exclusive upper bound on the iterated slice
+     * @param set       the owning collection used to detect concurrent modifications
      */
-    public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) {
+    public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) {
         this.entries = entries;
         this.fromIndex = fromIndex;
         this.pos = toIndex;
-        this.checkForConcurrentModification = checkForConcurrentModification;
+        this.set = set;
+        this.sizeOfSetAtStart = set.size();
     }
 
     /**
-     * Create a spliterator for the given array, with the given size.
+     * Create a spliterator over the entire array, skipping nulls.
      *
-     * @param entries                        the array
-     * @param checkForConcurrentModification runnable to check for concurrent modifications
+     * @param entries the backing array (not copied)
+     * @param set     the owning collection used to detect concurrent modifications
      */
-    public SparseArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) {
-        this(entries, 0, entries.length, checkForConcurrentModification);
+    public SparseArraySubSpliterator(final E[] entries, final Sized set) {
+        this(entries, 0, entries.length, set);
     }
 
     @Override
     public boolean tryAdvance(Consumer<? super E> action) {
-        this.checkForConcurrentModification.run();
+        if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException();
         while (fromIndex <= --pos) {
             if (null != entries[pos]) {
                 action.accept(entries[pos]);
@@ -88,7 +95,7 @@ public class SparseArraySubSpliterator<E> implements Spliterator<E> {
             }
             pos--;
         }
-        this.checkForConcurrentModification.run();
+        if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException();
     }
 
     @Override
@@ -99,7 +106,7 @@ public class SparseArraySubSpliterator<E> implements Spliterator<E> {
         }
         final int toIndexOfSubIterator = this.pos;
         this.pos = fromIndex + (entriesCount >>> 1);
-        return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification);
+        return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set);
     }
 
 
diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java
index 07ccc9634a..f0fba80517 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java
@@ -32,26 +32,41 @@ import java.util.function.Consumer;
 import java.util.function.Predicate;
 
 /**
- * An ArrayBunch implements TripleBunch with a linear search of a short-ish
- * array of Triples. The array grows by factor 2.
+ * Linear-scan implementation of {@link FastTripleBunch} backed by a packed
+ * {@link Triple} array. Used as long as a bunch stays small; once it grows
+ * past the configured threshold (see {@link FastTripleStore}) it is replaced
+ * with a {@link FastHashedTripleBunch}.
+ * <p>
+ * The array grows by a factor of two when full. Equality of triples within a
+ * bunch is delegated to {@link #areEqual(Triple, Triple)}, which subclasses
+ * specialize to compare only the two nodes that are <em>not</em> already
+ * implied by the enclosing map's key. This avoids redundant equality checks
+ * on the shared subject/predicate/object.
+ * <p>
+ * Not thread-safe.
  */
 public abstract class FastArrayBunch implements FastTripleBunch {
 
     private static final int INITIAL_SIZE = 4;
 
+    /** Number of valid entries in {@link #elements}. */
     protected int size = 0;
+    /** Packed array of triples; entries from {@code 0} to {@code size-1} are live. */
     protected Triple[] elements;
 
+    /**
+     * Creates an empty bunch with the default initial capacity.
+     */
     protected FastArrayBunch() {
         elements = new Triple[INITIAL_SIZE];
     }
 
     /**
-     * Copy constructor.
-     * The new bunch will contain all the same triples of the bunch to copy.
-     * But it will reserve only the space needed to contain them. Growing is still possible.
+     * Copy constructor. The new bunch contains the same triples as
+     * {@code bunchToCopy}; its backing array is sized to fit exactly,
+     * but can grow further if needed.
      *
-     * @param bunchToCopy
+     * @param bunchToCopy the source bunch
      */
     protected FastArrayBunch(final FastArrayBunch bunchToCopy) {
         this.elements = new Triple[bunchToCopy.size];
@@ -59,7 +74,17 @@ public abstract class FastArrayBunch implements FastTripleBunch {
         this.size = bunchToCopy.size;
     }
 
-    public abstract boolean areEqual(final Triple a, final Triple b);
+    /**
+     * Compare two triples for equality within this bunch.
+     * <p>
+     * Subclasses specialize this to skip the already-shared component
+     * (subject, predicate or object) and compare only the remaining two.
+     *
+     * @param a first triple
+     * @param b second triple
+     * @return {@code true} if the triples are considered equal in this bunch
+     */
+    protected abstract boolean areEqual(final Triple a, final Triple b);
 
     @Override
     public boolean containsKey(Triple t) {
@@ -127,6 +152,7 @@ public abstract class FastArrayBunch implements FastTripleBunch {
         for (int i = 0; i < size; i++) {
             if (areEqual(t, elements[i])) {
                 elements[i] = elements[--size];
+                elements[size] = null;
                 return true;
             }
         }
@@ -138,6 +164,7 @@ public abstract class FastArrayBunch implements FastTripleBunch {
         for (int i = 0; i < size; i++) {
             if (areEqual(t, elements[i])) {
                 elements[i] = elements[--size];
+                elements[size] = null;
                 return;
             }
         }
@@ -174,11 +201,7 @@ public abstract class FastArrayBunch implements FastTripleBunch {
 
     @Override
     public Spliterator<Triple> keySpliterator() {
-        final var initialSize = size;
-        final Runnable checkForConcurrentModification = () -> {
-            if (size != initialSize) throw new ConcurrentModificationException();
-        };
-        return new ArraySpliterator<>(elements, size, checkForConcurrentModification);
+        return new ArraySpliterator<>(elements, size, this);
     }
 
     @Override
diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java
index b89d331204..a49d6b5400 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java
@@ -25,21 +25,28 @@ import org.apache.jena.graph.Node;
 import org.apache.jena.mem.collection.FastHashMap;
 
 /**
- * Map from nodes to triple bunches.
+ * {@link FastHashMap} specialized to map a {@link Node} to its associated
+ * {@link FastTripleBunch}. Used by {@link FastTripleStore} to maintain the
+ * three subject/predicate/object indices.
  */
 public class FastHashedBunchMap
         extends FastHashMap<Node, FastTripleBunch>
         implements Copyable<FastHashedBunchMap> {
 
+    /**
+     * Creates an empty bunch map with the default initial capacity.
+     */
     public FastHashedBunchMap() {
         super();
     }
 
     /**
-     * Copy constructor.
-     * The new map will contain all the same nodes as keys of the map to copy, but copies of the bunches as values .
+     * Copy constructor. The new map has the same node keys as
+     * {@code mapToCopy}; each value is replaced by a deep copy of the
+     * corresponding bunch (via {@link FastTripleBunch#copy()}) so that
+     * mutations of either map cannot affect the other.
      *
-     * @param mapToCopy
+     * @param mapToCopy the source map
      */
     private FastHashedBunchMap(final FastHashedBunchMap mapToCopy) {
         super(mapToCopy, FastTripleBunch::copy);
diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java
index 459e78c818..65c9ab70fb 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java
@@ -25,13 +25,21 @@ import org.apache.jena.mem.collection.FastHashSet;
 import org.apache.jena.mem.collection.JenaSet;
 
 /**
- * A set of triples - backed by {@link FastHashSet}.
+ * Hashed implementation of {@link FastTripleBunch} built on top of
+ * {@link FastHashSet}. Used by {@link FastTripleStore} once a bunch grows
+ * past the size threshold at which a linear-scan {@link FastArrayBunch}
+ * stops being faster.
  */
 public class FastHashedTripleBunch extends FastHashSet<Triple> implements FastTripleBunch {
+
     /**
-     * Create a new triple bunch from the given set of triples.
+     * Create a new hashed bunch pre-populated from the given set of triples.
+     * The initial capacity is chosen at 1.5x the source size, so the new bunch
+     * fits the existing triples and has some headroom for growth before it
+     * needs to rehash.
      *
-     * @param set the set of triples
+     * @param set the source set of triples (typically the array bunch being
+     *            promoted)
      */
     public FastHashedTripleBunch(final JenaSet<Triple> set) {
         super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth
@@ -39,15 +47,18 @@ public class FastHashedTripleBunch extends FastHashSet<Triple> implements FastTr
     }
 
     /**
-     * Copy constructor.
-     * The new bunch will contain all the same triples of the bunch to copy.
+     * Copy constructor. The new bunch contains the same triples as
+     * {@code bunchToCopy}.
      *
-     * @param bunchToCopy
+     * @param bunchToCopy the source bunch
      */
     private FastHashedTripleBunch(final FastHashedTripleBunch bunchToCopy) {
         super(bunchToCopy);
     }
 
+    /**
+     * Creates an empty hashed bunch with the default initial capacity.
+     */
     public FastHashedTripleBunch() {
         super();
     }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java
index 68f79e72f8..fe05028318 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java
@@ -29,27 +29,39 @@ import org.apache.jena.mem.collection.JenaSetHashOptimized;
 import java.util.function.Predicate;
 
 /**
- * A bunch of triples - a stripped-down set with specialized methods. A
- * bunch is expected to store triples that share some useful property
- * (such as having the same subject or predicate).
+ * Set-like container for a "bunch" of triples that share some useful
+ * property - typically they all have the same subject, predicate or object,
+ * because the bunch is the value of a node-keyed map in a
+ * {@link FastTripleStore}.
+ * <p>
+ * The interface is a stripped-down set with a few extras tuned for the
+ * triple-store hot path; concrete implementations are
+ * {@link FastArrayBunch} (linear scan, used while the bunch is small) and
+ * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a
+ * threshold).
  */
 public interface FastTripleBunch extends JenaSetHashOptimized<Triple>, Copyable<FastTripleBunch> {
     /**
-     * Answer true iff this bunch is implemented as an array.
-     * This field is used to optimize some operations by avoiding the need for instanceOf tests.
+     * Answer {@code true} iff this bunch is backed by a flat array (i.e. is
+     * a {@link FastArrayBunch}). Exposed as an explicit method so callers can
+     * avoid {@code instanceof} checks on this hot path.
      *
-     * @return true iff this bunch is implemented as an arrays
+     * @return {@code true} if this bunch is array-backed
      */
     boolean isArray();
 
     /**
-     * This method is used to optimize _PO match operations.
-     * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches.
-     * This method is faster if there are many matches and the set is ordered in an unfavorable way.
-     * _PO matches usually fall into this category.
+     * Predicate test that scans elements in hash-table order rather than
+     * dense insertion order. Tuned for {@code _PO} (any-predicate-object)
+     * matches.
+     * <p>
+     * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches
+     * are rare or absent; this method is faster when many matches exist and
+     * the dense ordering would force scanning past clustered non-matches
+     * before finding a hit. Both variants short-circuit on the first match.
      *
-     * @param predicate the predicate to match
-     * @return true if any triple in the bunch matches the predicate
+     * @param predicate the predicate to test against each triple
+     * @return {@code true} if any triple in the bunch satisfies the predicate
      */
     boolean anyMatchRandomOrder(Predicate<Triple> predicate);
 }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java
index 8877bcffe9..8ed81dc577 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java
@@ -68,20 +68,43 @@ import java.util.stream.StreamSupport;
  */
 public class FastTripleStore implements TripleStore {
 
+    /**
+     * Object-bunch size above which {@code _PO} matches consider a
+     * secondary lookup in the predicate bunch.
+     */
     protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400;
+    /**
+     * Maximum size of a subject-keyed array bunch before it is promoted
+     * to a hashed bunch. Lower than the predicate/object threshold because
+     * the subject map is the primary entry point for {@code contains}.
+     */
     protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16;
+    /**
+     * Maximum size of a predicate- or object-keyed array bunch before it is
+     * promoted to a hashed bunch.
+     */
     protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32;
-    final FastHashedBunchMap subjects;
-    final FastHashedBunchMap predicates;
-    final FastHashedBunchMap objects;
+    private final FastHashedBunchMap subjects;
+    private final FastHashedBunchMap predicates;
+    private final FastHashedBunchMap objects;
     private int size = 0;
 
+    /**
+     * Creates a new, empty fast triple store.
+     */
     public FastTripleStore() {
         subjects = new FastHashedBunchMap();
         predicates = new FastHashedBunchMap();
         objects = new FastHashedBunchMap();
     }
 
+    /**
+     * Copy constructor used by {@link #copy()}; produces an independent store
+     * by deep-copying each of the three index maps (which in turn deep-copy
+     * their bunches).
+     *
+     * @param tripleStoreToCopy the source store
+     */
     private FastTripleStore(final FastTripleStore tripleStoreToCopy) {
         subjects = tripleStoreToCopy.subjects.copy();
         predicates = tripleStoreToCopy.predicates.copy();
@@ -380,6 +403,11 @@ public class FastTripleStore implements TripleStore {
         return new FastTripleStore(this);
     }
 
+    /**
+     * Array bunch used as the value in the subject-keyed map: every triple in
+     * the bunch shares the same subject, so equality only needs to compare
+     * predicate and object.
+     */
     protected static class ArrayBunchWithSameSubject extends FastArrayBunch {
 
         public ArrayBunchWithSameSubject() {
@@ -402,6 +430,11 @@ public class FastTripleStore implements TripleStore {
         }
     }
 
+    /**
+     * Array bunch used as the value in the predicate-keyed map: every triple
+     * in the bunch shares the same predicate, so equality only needs to
+     * compare subject and object.
+     */
     protected static class ArrayBunchWithSamePredicate extends FastArrayBunch {
 
         public ArrayBunchWithSamePredicate() {
@@ -424,6 +457,11 @@ public class FastTripleStore implements TripleStore {
         }
     }
 
+    /**
+     * Array bunch used as the value in the object-keyed map: every triple in
+     * the bunch shares the same object, so equality only needs to compare
+     * subject and predicate.
+     */
     protected static class ArrayBunchWithSameObject extends FastArrayBunch {
 
         public ArrayBunchWithSameObject() {
diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java
index 0a8ad7bf7e..097760b335 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java
@@ -54,7 +54,7 @@ public class ArrayBunch implements TripleBunch {
      * The new bunch will contain all the same triples of the bunch to copy.
      * But it will reserve only the space needed to contain them. Growing is still possible.
      *
-     * @param bunchToCopy
+     * @param bunchToCopy the bunch to copy
      */
     private ArrayBunch(final ArrayBunch bunchToCopy) {
         this.elements = new Triple[bunchToCopy.size];
@@ -168,11 +168,7 @@ public class ArrayBunch implements TripleBunch {
 
     @Override
     public Spliterator<Triple> keySpliterator() {
-        final var initialSize = size;
-        final Runnable checkForConcurrentModification = () -> {
-            if (size != initialSize) throw new ConcurrentModificationException();
-        };
-        return new ArraySpliterator<>(elements, size, checkForConcurrentModification);
+        return new ArraySpliterator<>(elements, size, this);
     }
 
     @Override
diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java
index b201c6cfaa..ae550fd636 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java
@@ -97,8 +97,7 @@ public class EagerStoreStrategy implements StoreStrategy {
      */
     private void indexAll() {
         // Initialize the index by adding all triples to the index
-        triples.indexedKeyIterator().forEachRemaining(entry ->
-                addToIndex(entry.key(), entry.index()));
+        triples.forEachKey(this::addToIndex);
     }
 
     /**
@@ -108,15 +107,15 @@ public class EagerStoreStrategy implements StoreStrategy {
      */
     private void indexAllParallel() {
         final var futureIndexSubjects = CompletableFuture.runAsync(() ->
-                triples.indexedKeyIterator().forEachRemaining(entry ->
-                        addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index())));
+                triples.forEachKey((triple, index) ->
+                        addIndex(spoBitmaps[0], triple.getSubject(), index)));
 
         final var futureIndexPredicates = CompletableFuture.runAsync(() ->
-                triples.indexedKeyIterator().forEachRemaining(entry ->
-                        addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index())));
+                triples.forEachKey((triple, index) ->
+                        addIndex(spoBitmaps[1], triple.getPredicate(), index)));
 
-        triples.indexedKeyIterator().forEachRemaining(entry ->
-                addIndex(spoBitmaps[2], entry.key().getObject(), entry.index()));
+        triples.forEachKey((triple, index) ->
+                addIndex(spoBitmaps[2], triple.getObject(), index));
 
         CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join();
     }
diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java
index 5659e6a20c..b75b60c6e9 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java
@@ -37,6 +37,8 @@ import org.apache.jena.graph.Triple;
 import org.hamcrest.collection.IsEmptyCollection;
 import org.hamcrest.collection.IsIterableContainingInAnyOrder;
 
+import java.util.ArrayList;
+
 public abstract class AbstractGraphMemTest {
 
     protected GraphMem sut;
@@ -1044,4 +1046,34 @@ public abstract class AbstractGraphMemTest {
         assertFalse(sut.contains(triple("s3 p3 o3")));
     }
 
+    @Test
+    public void testDeleteAll() {
+        for(var subjects=1; subjects <= 8 ; subjects++) {
+            for(var predicates=1; predicates <= 8 ; predicates++) {
+                for(var objects=1; objects <= 8 ; objects++) {
+                    sut = createGraph();
+                    var triples = new ArrayList<Triple>();
+                    for(var s=0; s < subjects ; s++) {
+                        for(var p=0; p < predicates ; p++) {
+                            for(var o=0; o < objects ; o++) {
+                                var t = triple("s" + s + " p" + p + " o" + o);
+                                triples.add(t);
+                                sut.add(t);
+                                assertTrue(sut.contains(t));
+                            }
+                        }
+                    }
+                    assertEquals(subjects*predicates*objects, sut.size());
+                    // print subjects, predicates, objects and size
+                    // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size());
+                    for (var triple : triples) {
+                        assertTrue(sut.contains(triple));
+                        sut.delete(triple);
+                        assertFalse(sut.contains(triple));
+                    }
+                    assertEquals(0, sut.size());
+                }
+            }
+        }
+    }
 }
diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java
index 868a79a34d..95546c3f4b 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java
@@ -21,10 +21,33 @@
 
 package org.apache.jena.mem;
 
+import org.junit.Test;
+
+import static org.apache.jena.testing_framework.GraphHelper.triple;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Concrete instantiation of {@link AbstractGraphMemTest} that exercises
+ * {@link GraphMemFast} (a {@link GraphMem} backed by a
+ * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared
+ * contract assertions live in the abstract base; this class only adds tests
+ * that are specific to the {@code GraphMemFast} variant.
+ */
 public class GraphMemFastTest extends AbstractGraphMemTest {
 
     @Override
     protected GraphMem createGraph() {
         return new GraphMemFast();
     }
+
+    @Test
+    public void copyReturnsAGraphMemFastInstance() {
+        sut.add(triple("s p o"));
+        final var copy = sut.copy();
+        // The override on GraphMemFast must preserve the runtime type so
+        // callers don't lose subclass-specific functionality through copy().
+        assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast);
+        assertNotSame(sut, copy);
+    }
 }
\ No newline at end of file
diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java
index 4fecf61d8a..65320b9973 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java
@@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.ArraySubSpliteratorTest;
 import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest;
 import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest;
 import org.apache.jena.mem.store.fast.FastArrayBunchTest;
+import org.apache.jena.mem.store.fast.FastHashedBunchMapTest;
 import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest;
 import org.apache.jena.mem.store.fast.FastTripleStoreTest;
 import org.apache.jena.mem.store.legacy.*;
@@ -62,8 +63,10 @@ import org.junit.runners.Suite;
     // store/fast
     FastTripleStoreTest.class,
     FastArrayBunchTest.class,
+    FastHashedBunchMapTest.class,
     FastHashedTripleBunchTest.class,
 
+
     // store/roaring
     RoaringTripleStoreTest.class,
     RoaringBitmapTripleIteratorTest.class,
diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java
index ba9d9c15b0..c3ae6f94a2 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java
@@ -171,14 +171,14 @@ public abstract class AbstractJenaMapNodeTest {
     public void testKeyIteratorEmpty() {
         var iter = sut.keyIterator();
         assertFalse(iter.hasNext());
-        assertThrows(NoSuchElementException.class, () -> iter.next());
+        assertThrows(NoSuchElementException.class, iter::next);
     }
 
     @Test
     public void testValueIteratorEmpty2() {
         var iter = sut.valueIterator();
         assertFalse(iter.hasNext());
-        assertThrows(NoSuchElementException.class, () -> iter.next());
+        assertThrows(NoSuchElementException.class, iter::next);
     }
 
     @Test
@@ -577,7 +577,7 @@ public abstract class AbstractJenaMapNodeTest {
     @Test
     public void computeIfAbsend1000Nodes() {
         for (int i = 0; i < 1000; i++) {
-            sut.computeIfAbsent(node("s" + i), () -> new Object());
+            sut.computeIfAbsent(node("s" + i), Object::new);
         }
         assertEquals(1000, sut.size());
     }
@@ -613,38 +613,4 @@ public abstract class AbstractJenaMapNodeTest {
         }
         assertTrue(sut.isEmpty());
     }
-
-
-    private static class HashCommonNodeMap extends HashCommonMap<Node, Object> {
-        public HashCommonNodeMap() {
-            super(10);
-        }
-
-        @Override
-        protected Node[] newKeysArray(int size) {
-            return new Node[size];
-        }
-
-        @Override
-        public void clear() {
-            super.clear(10);
-        }
-
-        @Override
-        protected Object[] newValuesArray(int size) {
-            return new Object[size];
-        }
-    }
-
-    private static class FastNodeHashMap extends FastHashMap<Node, Object> {
-        @Override
-        protected Node[] newKeysArray(int size) {
-            return new Node[size];
-        }
-
-        @Override
-        protected Object[] newValuesArray(int size) {
-            return new Object[size];
-        }
-    }
 }
\ No newline at end of file
diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java
index 1cd962e2d5..edaf60e26e 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java
@@ -106,7 +106,7 @@ public abstract class AbstractJenaSetTripleTest {
     public void testKeyIteratorEmpty() {
         var iter = sut.keyIterator();
         assertFalse(iter.hasNext());
-        assertThrows(NoSuchElementException.class, () -> iter.next());
+        assertThrows(NoSuchElementException.class, iter::next);
     }
 
     @Test
@@ -114,7 +114,7 @@ public abstract class AbstractJenaSetTripleTest {
         sut.tryAdd(triple("s o p"));
         var iter = sut.keyIterator();
         sut.tryAdd(triple("s o p2"));
-        assertThrows(ConcurrentModificationException.class, () -> iter.next());
+        assertThrows(ConcurrentModificationException.class, iter::next);
     }
 
     @Test
@@ -362,29 +362,4 @@ public abstract class AbstractJenaSetTripleTest {
         }
         assertTrue(sut.isEmpty());
     }
-
-
-    private static class HashCommonTripleSet extends HashCommonSet<Triple> {
-        public HashCommonTripleSet() {
-            super(10);
-        }
-
-        @Override
-        protected Triple[] newKeysArray(int size) {
-            return new Triple[size];
-        }
-
-        @Override
-        public void clear() {
-            super.clear(10);
-        }
-    }
-
-    private static class FastTripleHashSet extends FastHashSet<Triple> {
-        @Override
-        protected Triple[] newKeysArray(int size) {
-            return new Triple[size];
-        }
-    }
-
 }
\ No newline at end of file
diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java
index f098548b3b..d932ce2320 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java
@@ -24,9 +24,11 @@ import org.apache.jena.graph.Node;
 import org.junit.Test;
 
 import java.util.function.UnaryOperator;
+import java.util.HashMap;
+import java.util.HashSet;
 
 import static org.apache.jena.testing_framework.GraphHelper.node;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.*;
 
 public class FastHashMapTest2 {
 
@@ -113,6 +115,69 @@ public class FastHashMapTest2 {
         assertEquals(2, (int) original.get(node("s2")));
     }
 
+    @Test
+    public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() {
+        var sut = new FastNodeHashMap();
+        // First-time puts assign new indices.
+        final int i0 = sut.putAndGetIndex(node("s"), 100);
+        final int i1 = sut.putAndGetIndex(node("s1"), 200);
+        final int i2 = sut.putAndGetIndex(node("s2"), 300);
+        assertEquals(0, i0);
+        assertEquals(1, i1);
+        assertEquals(2, i2);
+        assertEquals(100, (int) sut.getValueAt(i0));
+        assertEquals(200, (int) sut.getValueAt(i1));
+        assertEquals(300, (int) sut.getValueAt(i2));
+    }
+
+    @Test
+    public void testPutAndGetIndexOverwritesValueForExistingKey() {
+        var sut = new FastNodeHashMap();
+        final int i0 = sut.putAndGetIndex(node("s"), 100);
+        // Re-putting the same key returns the SAME index but with the new value.
+        final int i0Again = sut.putAndGetIndex(node("s"), 999);
+        assertEquals(i0, i0Again);
+        assertEquals(999, (int) sut.get(node("s")));
+        assertEquals(1, sut.size());
+    }
+
+    @Test
+    public void testForEachKeyVisitsEveryEntryWithItsIndex() {
+        var sut = new FastNodeHashMap();
+        sut.putAndGetIndex(node("a"), 0);
+        sut.putAndGetIndex(node("b"), 1);
+        sut.putAndGetIndex(node("c"), 2);
+
+        final HashMap<Node, Integer> seen = new HashMap<>();
+        sut.forEachKey(seen::put);
+
+        assertEquals(3, seen.size());
+        assertEquals(Integer.valueOf(0), seen.get(node("a")));
+        assertEquals(Integer.valueOf(1), seen.get(node("b")));
+        assertEquals(Integer.valueOf(2), seen.get(node("c")));
+    }
+
+    @Test
+    public void testForEachKeySkipsRemovedSlots() {
+        var sut = new FastNodeHashMap();
+        sut.putAndGetIndex(node("a"), 0);
+        sut.putAndGetIndex(node("b"), 1);
+        sut.putAndGetIndex(node("c"), 2);
+        sut.tryRemove(node("b"));
+
+        final HashSet<Node> visited = new HashSet<>();
+        sut.forEachKey((k, i) -> visited.add(k));
+        assertEquals(2, visited.size());
+        assertTrue(visited.contains(node("a")));
+        assertTrue(visited.contains(node("c")));
+    }
+
+    @Test
+    public void testForEachKeyOnEmptyMapIsNoOp() {
+        var sut = new FastNodeHashMap();
+        sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map"));
+    }
+
     private static class FastNodeHashMap extends FastHashMap<Node, Object> {
 
         public FastNodeHashMap() {
diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java
index 5841491b89..1fc61f1a57 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java
@@ -23,8 +23,6 @@ package org.apache.jena.mem.collection;
 import org.junit.Before;
 import org.junit.Test;
 
-import java.util.ArrayList;
-import java.util.ConcurrentModificationException;
 import java.util.List;
 
 import static org.apache.jena.testing_framework.GraphHelper.node;
@@ -55,13 +53,33 @@ public class FastHashSetTest2 {
 
     @Test
     public void testAddAndGetIndexWithSameHashCode() {
-        assertEquals(0, sut.addAndGetIndex("a", 0));
-        assertEquals(1, sut.addAndGetIndex("b", 0));
-        assertEquals(2, sut.addAndGetIndex("c", 0));
+        final var a = new Object() {
+            @Override
+            public int hashCode() {
+                return 0;
+            }
+        };
+        final var b = new Object() {
+            @Override
+            public int hashCode() {
+                return 0;
+            }
+        };
+        final var c = new Object() {
+            @Override
+            public int hashCode() {
+                return 0;
+            }
+        };
+
+        var objectHashSet = new FastObjectHashSet();
+        assertEquals(0, objectHashSet.addAndGetIndex(a));
+        assertEquals(1, objectHashSet.addAndGetIndex(b));
+        assertEquals(2, objectHashSet.addAndGetIndex(c));
 
-        assertEquals(~0, sut.addAndGetIndex("a", 0));
-        assertEquals(~1, sut.addAndGetIndex("b", 0));
-        assertEquals(~2, sut.addAndGetIndex("c", 0));
+        assertEquals(~0, objectHashSet.addAndGetIndex(a));
+        assertEquals(~1, objectHashSet.addAndGetIndex(b));
+        assertEquals(~2, objectHashSet.addAndGetIndex(c));
     }
 
     @Test
@@ -188,127 +206,44 @@ public class FastHashSetTest2 {
     }
 
     @Test
-    public void testindexedKeyIterator() {
+    public void testForEachKeyVisitsEveryKeyWithItsIndex() {
         var items = List.of("a", "b", "c", "d", "e");
-
         sut = new FastStringHashSet(3);
         for (String item : items) {
             sut.addAndGetIndex(item);
         }
 
-        var iterator = sut.indexedKeyIterator();
-        for (var i=0; i<items.size(); i++) {
-            assertTrue(iterator.hasNext());
-            var indexedKey = iterator.next();
-            assertEquals(items.get(i), indexedKey.key());
-            assertEquals(i, indexedKey.index());
-        }
-        assertFalse(iterator.hasNext());
-    }
-
-    @Test
-    public void testindexedKeyIteratorEmpty() {
-        sut = new FastStringHashSet(3);
-        var iterator = sut.indexedKeyIterator();
-        assertFalse(iterator.hasNext());
-    }
-
-    @Test
-    public void testIndexedKeySpliterator() {
-        var items = List.of("a", "b", "c", "d", "e");
+        final java.util.Map<String, Integer> seen = new java.util.HashMap<>();
+        sut.forEachKey(seen::put);
 
-        sut = new FastStringHashSet(3);
-        for (String item : items) {
-            sut.addAndGetIndex(item);
-        }
-
-        var iterator = sut.indexedKeySpliterator();
-        for (var i=0; i<items.size(); i++) {
-            final var index = i;
-            assertTrue(iterator.tryAdvance(indexedKey -> {
-                assertEquals(items.get(index), indexedKey.key());
-                assertEquals(index, indexedKey.index());
-            }));
-        }
-        assertFalse(iterator.tryAdvance(indexedKey -> {
-            fail("There should be no more elements in the iterator");
-        }));
-    }
-
-    @Test
-    public void testIndexedKeyStream() {
-        var items = List.of("a", "b", "c", "d", "e");
-
-        sut = new FastStringHashSet(3);
-        for (String item : items) {
-            sut.addAndGetIndex(item);
-        }
-
-        var indexedKeys = sut.indexedKeyStream().toList();
-        assertEquals(items.size(), indexedKeys.size());
-        for (var i=0; i<items.size(); i++) {
-            assertEquals(items.get(i), indexedKeys.get(i).key());
-            assertEquals(i, indexedKeys.get(i).index());
+        assertEquals(items.size(), seen.size());
+        for (var i = 0; i < items.size(); i++) {
+            // forEachKey reports the stable insertion index per key.
+            assertEquals(Integer.valueOf(i), seen.get(items.get(i)));
         }
     }
 
     @Test
-    public void testIndexedKeyStreamParallel() {
-        Integer checkSum = 0;
-        final var items = new ArrayList<Integer>();
-        for (var i = 0; i < 1000; i++) {
-            items.add(i);
-            checkSum+= i;
-        }
-
-        sut = new FastStringHashSet();
-        for (var value : items) {
-            sut.addAndGetIndex(value.toString());
-        }
-
-        final var sum = sut.indexedKeyStreamParallel()
-                        .map(pair -> Integer.parseInt(pair.key()))
-                        .reduce(0, Integer::sum);
-        assertEquals(checkSum, sum);
-    }
-
-    @Test
-    public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() {
-        sut = new FastStringHashSet(3);
-        sut.tryAdd("a");
-        var spliterator = sut.indexedKeySpliterator();
-        sut.tryAdd("b");
-        assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> {
-        }));
-    }
-
-    @Test
-    public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() {
-        sut = new FastStringHashSet(3);
-        sut.tryAdd("a");
-        var spliterator = sut.indexedKeySpliterator();
-        sut.tryAdd("b");
-        assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> {
-        }));
-    }
-
-    @Test
-    public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() {
+    public void testForEachKeyOnEmptySetIsNoOp() {
         sut = new FastStringHashSet(3);
-        sut.tryAdd("a");
-        var spliterator = sut.indexedKeyIterator();
-        sut.tryAdd("b");
-        assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> {
-        }));
+        sut.forEachKey((k, i) -> fail("consumer must not be called on empty set"));
     }
 
     @Test
-    public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() {
+    public void testForEachKeySkipsRemovedSlots() {
         sut = new FastStringHashSet(3);
-        sut.tryAdd("a");
-        var spliterator = sut.indexedKeyIterator();
-        sut.tryAdd("b");
-        assertThrows(ConcurrentModificationException.class, spliterator::next);
+        sut.addAndGetIndex("a");
+        sut.addAndGetIndex("b");
+        sut.addAndGetIndex("c");
+        // Remove the middle entry; forEachKey must not visit the freed slot.
+        sut.tryRemove("b");
+
+        final var keys = new java.util.ArrayList<String>();
+        sut.forEachKey((k, i) -> keys.add(k));
+        assertEquals(2, keys.size());
+        assertTrue(keys.contains("a"));
+        assertTrue(keys.contains("c"));
+        assertFalse(keys.contains("b"));
     }
 
     private static class FastObjectHashSet extends FastHashSet<Object> {
diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java
deleted file mode 100644
index cfffcf9390..0000000000
--- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.
- *
- *   SPDX-License-Identifier: Apache-2.0
- */
-package org.apache.jena.mem.iterator;
-
-import org.junit.Test;
-
-import java.util.NoSuchElementException;
-
-import static org.junit.Assert.*;
-
-public class SparseArrayIndexedIteratorTest {
-
-    private SparseArrayIndexedIterator<String> iterator;
-
-    @Test
-    public void testHasNextAndNextWithNonNullEntries() {
-        String[] entries = new String[]{"first", "second", "third"};
-        iterator = new SparseArrayIndexedIterator<>(entries, () -> {
-        });
-
-        assertTrue(iterator.hasNext());
-        var entry = iterator.next();
-        assertEquals(0, entry.index());
-        assertEquals("first", entry.key());
-
-        assertTrue(iterator.hasNext());
-        entry = iterator.next();
-        assertEquals(1, entry.index());
-        assertEquals("second", entry.key());
-
-        assertTrue(iterator.hasNext());
-        entry = iterator.next();
-        assertEquals(2, entry.index());
-        assertEquals("third", entry.key());
-
-        assertFalse(iterator.hasNext());
-    }
-
-    @Test
-    public void testConstrucorWithToIndexConstraint3() {
-        String[] entries = new String[]{"first", "second", "third"};
-        iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> {
-        });
-
-        assertTrue(iterator.hasNext());
-        var entry = iterator.next();
-        assertEquals(0, entry.index());
-        assertEquals("first", entry.key());
-
-        assertTrue(iterator.hasNext());
-        entry = iterator.next();
-        assertEquals(1, entry.index());
-        assertEquals("second", entry.key());
-
-        assertTrue(iterator.hasNext());
-        entry = iterator.next();
-        assertEquals(2, entry.index());
-        assertEquals("third", entry.key());
-
-        assertFalse(iterator.hasNext());
-    }
-
-    @Test
-    public void testConstrucorWithToIndexConstraint2() {
-        String[] entries = new String[]{"first", "second", "third"};
-        iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> {
-        });
-
-        assertTrue(iterator.hasNext());
-        var entry = iterator.next();
-        assertEquals(0, entry.index());
-        assertEquals("first", entry.key());
-
-        assertTrue(iterator.hasNext());
-        entry = iterator.next();
-        assertEquals(1, entry.index());
-        assertEquals("second", entry.key());
-
-        assertFalse(iterator.hasNext());
-    }
-
-    @Test
-    public void testConstrucorWithToIndexConstraint1() {
-        String[] entries = new String[]{"first", "second", "third"};
-        iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> {
-        });
-
-        assertTrue(iterator.hasNext());
-        var entry = iterator.next();
-        assertEquals(0, entry.index());
-        assertEquals("first", entry.key());
-
-        assertFalse(iterator.hasNext());
-    }
-
-    @Test
-    public void testConstrucorWithToIndexConstraint0() {
-        String[] entries = new String[]{"first", "second", "third"};
-        iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> {
-        });
-
-        assertFalse(iterator.hasNext());
-        assertThrows(NoSuchElementException.class, () -> iterator.next());
-    }
-
-    @Test
-    public void testHasNextAndNextWithNullEntries() {
-        String[] entries = new String[]{"first", null, "third", null, "fifth"};
-        iterator = new SparseArrayIndexedIterator<>(entries, () -> {
-        });
-
-        assertTrue(iterator.hasNext());
-        var entry = iterator.next();
-        assertEquals(0, entry.index());
-        assertEquals("first", entry.key());
-
-        assertTrue(iterator.hasNext());
-        entry = iterator.next();
-        assertEquals(2, entry.index());
-        assertEquals("third", entry.key());
-
-        assertTrue(iterator.hasNext());
-        entry = iterator.next();
-        assertEquals(4, entry.index());
-        assertEquals("fifth", entry.key());
-
-        assertFalse(iterator.hasNext());
-    }
-
-    @Test
-    public void testHasNextAndNextWithNoElements() {
-        String[] entries = new String[]{};
-        iterator = new SparseArrayIndexedIterator<>(entries, () -> {
-        });
-
-        assertFalse(iterator.hasNext());
-        assertThrows(NoSuchElementException.class, () -> iterator.next());
-    }
-
-    @Test
-    public void testForEachRemaining() {
-        String[] entries = new String[]{"first", null, "third", null, "fifth"};
-        iterator = new SparseArrayIndexedIterator<>(entries, () -> {
-        });
-        int[] count = new int[]{0};
-        iterator.forEachRemaining(entry -> {
-            assertNotNull(entry);
-            count[0]++;
-        });
-        assertEquals(3, count[0]);
-    }
-}
-
diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java
index d93d8a3beb..393e3019cb 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java
@@ -20,6 +20,8 @@
  */
 package org.apache.jena.mem.iterator;
 
+import org.apache.jena.mem.collection.FastHashSet;
+import org.apache.jena.mem.collection.JenaSet;
 import org.junit.Test;
 
 import java.util.NoSuchElementException;
@@ -28,13 +30,19 @@ import static org.junit.Assert.*;
 
 public class SparseArrayIteratorTest {
 
+    private static final JenaSet<Object> dummySetForConcurrencyCheck = new FastHashSet<>() {
+        @Override
+        protected Object[] newKeysArray(int size) {
+            return new Object[size];
+        }
+    };
+
     private SparseArrayIterator<String> iterator;
 
     @Test
     public void testHasNextAndNextWithNonNullEntries() {
         String[] entries = new String[]{"first", "second", "third"};
-        iterator = new SparseArrayIterator<>(entries, () -> {
-        });
+        iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck);
 
         assertTrue(iterator.hasNext());
         assertEquals("third", iterator.next());
@@ -48,8 +56,7 @@ public class SparseArrayIteratorTest {
     @Test
     public void testConstrucorWithToIndexConstraint3() {
         String[] entries = new String[]{"first", "second", "third"};
-        iterator = new SparseArrayIterator<>(entries, 3, () -> {
-        });
+        iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck);
 
         assertTrue(iterator.hasNext());
         assertEquals("third", iterator.next());
@@ -63,8 +70,7 @@ public class SparseArrayIteratorTest {
     @Test
     public void testConstrucorWithToIndexConstraint2() {
         String[] entries = new String[]{"first", "second", "third"};
-        iterator = new SparseArrayIterator<>(entries, 2, () -> {
-        });
+        iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck);
 
         assertTrue(iterator.hasNext());
         assertEquals("second", iterator.next());
@@ -76,8 +82,7 @@ public class SparseArrayIteratorTest {
     @Test
     public void testConstrucorWithToIndexConstraint1() {
         String[] entries = new String[]{"first", "second", "third"};
-        iterator = new SparseArrayIterator<>(entries, 1, () -> {
-        });
+        iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck);
 
         assertTrue(iterator.hasNext());
         assertEquals("first", iterator.next());
@@ -87,8 +92,7 @@ public class SparseArrayIteratorTest {
     @Test
     public void testConstrucorWithToIndexConstraint0() {
         String[] entries = new String[]{"first", "second", "third"};
-        iterator = new SparseArrayIterator<>(entries, 0, () -> {
-        });
+        iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck);
 
         assertFalse(iterator.hasNext());
         assertThrows(NoSuchElementException.class, () -> iterator.next());
@@ -97,8 +101,7 @@ public class SparseArrayIteratorTest {
     @Test
     public void testHasNextAndNextWithNullEntries() {
         String[] entries = new String[]{"first", null, "third", null, "fifth"};
-        iterator = new SparseArrayIterator<>(entries, () -> {
-        });
+        iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck);
 
         assertTrue(iterator.hasNext());
         assertEquals("fifth", iterator.next());
@@ -112,8 +115,7 @@ public class SparseArrayIteratorTest {
     @Test
     public void testHasNextAndNextWithNoElements() {
         String[] entries = new String[]{};
-        iterator = new SparseArrayIterator<>(entries, () -> {
-        });
+        iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck);
 
         assertFalse(iterator.hasNext());
         assertThrows(NoSuchElementException.class, () -> iterator.next());
@@ -122,8 +124,7 @@ public class SparseArrayIteratorTest {
     @Test
     public void testForEachRemaining() {
         String[] entries = new String[]{"first", null, "third", null, "fifth"};
-        iterator = new SparseArrayIterator<>(entries, () -> {
-        });
+        iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck);
         int[] count = new int[]{0};
         iterator.forEachRemaining(entry -> {
             assertNotNull(entry);
diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java
index 23643c6da4..99e1562d53 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java
@@ -20,16 +20,29 @@
  */
 package org.apache.jena.mem.pattern;
 
+import org.apache.jena.graph.Node;
 import org.junit.Test;
 
 import static org.apache.jena.testing_framework.GraphHelper.node;
 import static org.apache.jena.testing_framework.GraphHelper.triple;
 import static org.junit.Assert.assertEquals;
 
+/**
+ * Unit tests for {@link PatternClassifier}: maps a triple match into one of
+ * the eight {@link MatchPattern} buckets based on which of the subject,
+ * predicate and object slots are concrete and which are wildcards.
+ * <p>
+ * Both classification overloads are tested for every combination of
+ * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is
+ * additionally tested with explicit {@code null} arguments and
+ * {@link Node#ANY}, both of which must be treated as wildcards.
+ */
 public class PatternClassifierTest {
 
     @Test
-    public void testClassifyTriple() {
+    public void classifyTripleCoversAllEightCombinations() {
+        // The wildcard "??" is parsed as a non-concrete (variable) node by
+        // the test helper.
         assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o")));
         assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??")));
         assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o")));
@@ -41,7 +54,7 @@ public class PatternClassifierTest {
     }
 
     @Test
-    public void testClassifyNodes() {
+    public void classifyNodesCoversAllEightCombinations() {
         assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o")));
         assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??")));
         assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o")));
@@ -52,4 +65,26 @@ public class PatternClassifierTest {
         assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??")));
     }
 
+    @Test
+    public void classifyNodesTreatsNullAsWildcard() {
+        // The graph-find contract allows callers to pass null for a slot
+        // they don't care about; the classifier must handle that without NPE.
+        assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o")));
+        assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null));
+        assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o")));
+        assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null));
+        assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o")));
+        assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null));
+        assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o")));
+        assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null));
+    }
+
+    @Test
+    public void classifyNodesTreatsNodeAnyAsWildcard() {
+        // Node.ANY is the standard wildcard sentinel used by Graph.find.
+        assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY));
+        assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o")));
+        assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o")));
+        assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY));
+    }
 }
\ No newline at end of file
diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java
index 4fe0d7382f..ae2afc7fd4 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java
@@ -20,6 +20,9 @@
  */
 package org.apache.jena.mem.spliterator;
 
+
+import org.apache.jena.mem.collection.FastHashSet;
+import org.apache.jena.mem.collection.JenaSet;
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -30,149 +33,113 @@ import static org.junit.Assert.*;
 
 public class ArraySpliteratorTest {
 
+    private static final JenaSet<Object> dummySetForConcurrencyCheck = new FastHashSet<>() {
+        @Override
+        protected Object[] newKeysArray(int size) {
+            return new Object[size];
+        }
+    };
+
     @Test
     public void tryAdvanceEmpty() {
-        {
-            Integer[] array = new Integer[0];
-            Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-            });
-            assertFalse(spliterator.tryAdvance((i) -> {
-                fail("Should not have advanced");
-            }));
-        }
+        Integer[] array = new Integer[0];
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
+        assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced")));
     }
 
     @Test
     public void tryAdvanceOne() {
-        {
-            Integer[] array = new Integer[]{1};
-            Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-            });
-            var itemsFound = new ArrayList<>();
-            while (spliterator.tryAdvance((i) -> {
-                itemsFound.add(1);
-            }));
-            assertEquals(1, itemsFound.size());
-            itemsFound.contains(1);
-        }
+        Integer[] array = new Integer[]{1};
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
+        var itemsFound = new ArrayList<>();
+        while (spliterator.tryAdvance((i) -> itemsFound.add(1)));
+        assertEquals(1, itemsFound.size());
+        assertTrue(itemsFound.contains(1));
     }
 
     @Test
     public void tryAdvanceTwo() {
-        {
-            Integer[] array = new Integer[]{1, 2};
-            Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-            });
-            var itemsFound = new ArrayList<>();
-            while (spliterator.tryAdvance((i) -> {
-                itemsFound.add(i);
-            }));
-            assertEquals(2, itemsFound.size());
-            itemsFound.contains(1);
-            itemsFound.contains(2);
-        }
+        Integer[] array = new Integer[]{1, 2};
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
+        var itemsFound = new ArrayList<>();
+        while (spliterator.tryAdvance(itemsFound::add));
+        assertEquals(2, itemsFound.size());
+        assertTrue(itemsFound.contains(1));
+        assertTrue(itemsFound.contains(2));
     }
 
     @Test
     public void tryAdvanceThree() {
-        {
-            Integer[] array = new Integer[]{1, 2, 3};
-            Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-            });
-            var itemsFound = new ArrayList<>();
-            while (spliterator.tryAdvance((i) -> {
-                itemsFound.add(i);
-            }));
-            assertEquals(3, itemsFound.size());
-            itemsFound.contains(1);
-            itemsFound.contains(2);
-            itemsFound.contains(3);
-        }
+        Integer[] array = new Integer[]{1, 2, 3};
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
+        var itemsFound = new ArrayList<>();
+        while (spliterator.tryAdvance(itemsFound::add));
+        assertEquals(3, itemsFound.size());
+        assertTrue(itemsFound.contains(1));
+        assertTrue(itemsFound.contains(2));
+        assertTrue(itemsFound.contains(3));
     }
 
     @Test
     public void forEachRemainingEmpty() {
-        {
-            Integer[] array = new Integer[]{};
-            Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-            });
-            var itemsFound = new ArrayList<>();
-            spliterator.forEachRemaining((i) -> {
-                itemsFound.add(i);
-            });
-            assertEquals(0, itemsFound.size());
-        }
+        Integer[] array = new Integer[]{};
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
+        var itemsFound = new ArrayList<>();
+        spliterator.forEachRemaining(itemsFound::add);
+        assertEquals(0, itemsFound.size());
     }
 
     @Test
     public void forEachRemainingOne() {
-        {
-            Integer[] array = new Integer[]{1};
-            Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-            });
-            var itemsFound = new ArrayList<>();
-            spliterator.forEachRemaining((i) -> {
-                itemsFound.add(i);
-            });
-            assertEquals(1, itemsFound.size());
-            itemsFound.contains(1);
-        }
+        Integer[] array = new Integer[]{1};
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
+        var itemsFound = new ArrayList<>();
+        spliterator.forEachRemaining(itemsFound::add);
+        assertEquals(1, itemsFound.size());
+        assertTrue(itemsFound.contains(1));
     }
 
     @Test
     public void forEachRemainingTwo() {
-        {
-            Integer[] array = new Integer[]{1, 2};
-            Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-            });
-            var itemsFound = new ArrayList<>();
-            spliterator.forEachRemaining((i) -> {
-                itemsFound.add(i);
-            });
-            assertEquals(2, itemsFound.size());
-            itemsFound.contains(1);
-            itemsFound.contains(2);
-        }
+        Integer[] array = new Integer[]{1, 2};
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
+        var itemsFound = new ArrayList<>();
+        spliterator.forEachRemaining(itemsFound::add);
+        assertEquals(2, itemsFound.size());
+        assertTrue(itemsFound.contains(1));
+        assertTrue(itemsFound.contains(2));
     }
 
     @Test
     public void forEachRemainingThree() {
-        {
-            Integer[] array = new Integer[]{1, 2, 3};
-            Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-            });
-            var itemsFound = new ArrayList<>();
-            spliterator.forEachRemaining((i) -> {
-                itemsFound.add(i);
-            });
-            assertEquals(3, itemsFound.size());
-            itemsFound.contains(1);
-            itemsFound.contains(2);
-            itemsFound.contains(3);
-        }
+        Integer[] array = new Integer[]{1, 2, 3};
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
+        var itemsFound = new ArrayList<>();
+        spliterator.forEachRemaining(itemsFound::add);
+        assertEquals(3, itemsFound.size());
+        assertTrue(itemsFound.contains(1));
+        assertTrue(itemsFound.contains(2));
+        assertTrue(itemsFound.contains(3));
     }
 
     @Test
     public void trySplitEmpty() {
         Integer[] array = new Integer[]{};
-        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
         assertNull(spliterator.trySplit());
     }
 
     @Test
     public void trySplitOne() {
         Integer[] array = new Integer[]{1};
-        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
         assertNull(spliterator.trySplit());
     }
 
     @Test
     public void trySplitTwo() {
         Integer[] array = new Integer[]{1, 2};
-        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertBetween(2, 3, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -183,8 +150,7 @@ public class ArraySpliteratorTest {
     @Test
     public void trySplitThree() {
         Integer[] array = new Integer[]{1, 2, 3};
-        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertBetween(3, 4, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -195,8 +161,7 @@ public class ArraySpliteratorTest {
     @Test
     public void trySplitFour() {
         Integer[] array = new Integer[]{1, 2, 3, 4};
-        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertBetween(4, 5, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -207,8 +172,7 @@ public class ArraySpliteratorTest {
     @Test
     public void trySplitFive() {
         Integer[] array = new Integer[]{1, 2, 3, 4, 5};
-        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertBetween(5, 6, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -224,8 +188,7 @@ public class ArraySpliteratorTest {
                 array[i] = i;
             }
         }
-        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertEquals(array.length, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -241,56 +204,49 @@ public class ArraySpliteratorTest {
     @Test
     public void estimateSizeZero() {
         Integer[] array = new Integer[]{};
-        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
         assertBetween(0, 1, spliterator.estimateSize());
     }
 
     @Test
     public void estimateSizeOne() {
         Integer[] array = new Integer[]{1};
-        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
         assertBetween(1, 2, spliterator.estimateSize());
     }
 
     @Test
     public void estimateSizeTwo() {
         Integer[] array = new Integer[]{1, 2};
-        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
         assertBetween(2, 3, spliterator.estimateSize());
     }
 
     @Test
     public void estimateSizeFive() {
         Integer[] array = new Integer[]{1, 2, 3, 4, 5};
-        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
         assertBetween(5, 6, spliterator.estimateSize());
     }
 
     @Test
     public void characteristics() {
         Integer[] array = new Integer[]{1, 2, 3, 4, 5};
-        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
         assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics());
     }
 
     @Test
     public void splitWithOneElementNull() {
         Integer[] array = new Integer[]{1};
-        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
         assertNull(spliterator.trySplit());
     }
 
     @Test
     public void splitWithOneRemainingElementNull() {
         Integer[] array = new Integer[]{1, 2};
-        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck);
         spliterator.tryAdvance((i) -> {
         });
         assertNull(spliterator.trySplit());
diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java
index f8d44700da..696b6be7be 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java
@@ -20,6 +20,8 @@
  */
 package org.apache.jena.mem.spliterator;
 
+import org.apache.jena.mem.collection.FastHashSet;
+import org.apache.jena.mem.collection.JenaSet;
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -30,149 +32,113 @@ import static org.junit.Assert.*;
 
 public class ArraySubSpliteratorTest {
 
+    private static final JenaSet<Object> dummySetForConcurrencyCheck = new FastHashSet<>() {
+        @Override
+        protected Object[] newKeysArray(int size) {
+            return new Object[size];
+        }
+    };
+
     @Test
     public void tryAdvanceEmpty() {
-        {
-            Integer[] array = new Integer[0];
-            Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-            });
-            assertFalse(spliterator.tryAdvance((i) -> {
-                fail("Should not have advanced");
-            }));
-        }
+        Integer[] array = new Integer[0];
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
+        assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced")));
     }
 
     @Test
     public void tryAdvanceOne() {
-        {
-            Integer[] array = new Integer[]{1};
-            Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-            });
-            var itemsFound = new ArrayList<>();
-            while (spliterator.tryAdvance((i) -> {
-                itemsFound.add(1);
-            }));
-            assertEquals(1, itemsFound.size());
-            itemsFound.contains(1);
-        }
+        Integer[] array = new Integer[]{1};
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
+        var itemsFound = new ArrayList<>();
+        while (spliterator.tryAdvance((i) -> itemsFound.add(1)));
+        assertEquals(1, itemsFound.size());
+        assertTrue(itemsFound.contains(1));
     }
 
     @Test
     public void tryAdvanceTwo() {
-        {
-            Integer[] array = new Integer[]{1, 2};
-            Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-            });
-            var itemsFound = new ArrayList<>();
-            while (spliterator.tryAdvance((i) -> {
-                itemsFound.add(i);
-            }));
-            assertEquals(2, itemsFound.size());
-            itemsFound.contains(1);
-            itemsFound.contains(2);
-        }
+        Integer[] array = new Integer[]{1, 2};
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
+        var itemsFound = new ArrayList<>();
+        while (spliterator.tryAdvance(itemsFound::add));
+        assertEquals(2, itemsFound.size());
+        assertTrue(itemsFound.contains(1));
+        assertTrue(itemsFound.contains(2));
     }
 
     @Test
     public void tryAdvanceThree() {
-        {
-            Integer[] array = new Integer[]{1, 2, 3};
-            Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-            });
-            var itemsFound = new ArrayList<>();
-            while (spliterator.tryAdvance((i) -> {
-                itemsFound.add(i);
-            }));
-            assertEquals(3, itemsFound.size());
-            itemsFound.contains(1);
-            itemsFound.contains(2);
-            itemsFound.contains(3);
-        }
+        Integer[] array = new Integer[]{1, 2, 3};
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
+        var itemsFound = new ArrayList<>();
+        while (spliterator.tryAdvance(itemsFound::add));
+        assertEquals(3, itemsFound.size());
+        assertTrue(itemsFound.contains(1));
+        assertTrue(itemsFound.contains(2));
+        assertTrue(itemsFound.contains(3));
     }
 
     @Test
     public void forEachRemainingEmpty() {
-        {
-            Integer[] array = new Integer[]{};
-            Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-            });
-            var itemsFound = new ArrayList<>();
-            spliterator.forEachRemaining((i) -> {
-                itemsFound.add(i);
-            });
-            assertEquals(0, itemsFound.size());
-        }
+        Integer[] array = new Integer[]{};
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
+        var itemsFound = new ArrayList<>();
+        spliterator.forEachRemaining(itemsFound::add);
+        assertEquals(0, itemsFound.size());
     }
 
     @Test
     public void forEachRemainingOne() {
-        {
-            Integer[] array = new Integer[]{1};
-            Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-            });
-            var itemsFound = new ArrayList<>();
-            spliterator.forEachRemaining((i) -> {
-                itemsFound.add(i);
-            });
-            assertEquals(1, itemsFound.size());
-            itemsFound.contains(1);
-        }
+        Integer[] array = new Integer[]{1};
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
+        var itemsFound = new ArrayList<>();
+        spliterator.forEachRemaining(itemsFound::add);
+        assertEquals(1, itemsFound.size());
+        assertTrue(itemsFound.contains(1));
     }
 
     @Test
     public void forEachRemainingTwo() {
-        {
-            Integer[] array = new Integer[]{1, 2};
-            Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-            });
-            var itemsFound = new ArrayList<>();
-            spliterator.forEachRemaining((i) -> {
-                itemsFound.add(i);
-            });
-            assertEquals(2, itemsFound.size());
-            itemsFound.contains(1);
-            itemsFound.contains(2);
-        }
+        Integer[] array = new Integer[]{1, 2};
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
+        var itemsFound = new ArrayList<>();
+        spliterator.forEachRemaining(itemsFound::add);
+        assertEquals(2, itemsFound.size());
+        assertTrue(itemsFound.contains(1));
+        assertTrue(itemsFound.contains(2));
     }
 
     @Test
     public void forEachRemainingThree() {
-        {
-            Integer[] array = new Integer[]{1, 2, 3};
-            Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-            });
-            var itemsFound = new ArrayList<>();
-            spliterator.forEachRemaining((i) -> {
-                itemsFound.add(i);
-            });
-            assertEquals(3, itemsFound.size());
-            itemsFound.contains(1);
-            itemsFound.contains(2);
-            itemsFound.contains(3);
-        }
+        Integer[] array = new Integer[]{1, 2, 3};
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
+        var itemsFound = new ArrayList<>();
+        spliterator.forEachRemaining(itemsFound::add);
+        assertEquals(3, itemsFound.size());
+        assertTrue(itemsFound.contains(1));
+        assertTrue(itemsFound.contains(2));
+        assertTrue(itemsFound.contains(3));
     }
 
     @Test
     public void trySplitEmpty() {
         Integer[] array = new Integer[]{};
-        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         assertNull(spliterator.trySplit());
     }
 
     @Test
     public void trySplitOne() {
         Integer[] array = new Integer[]{1};
-        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         assertNull(spliterator.trySplit());
     }
 
     @Test
     public void trySplitTwo() {
         Integer[] array = new Integer[]{1, 2};
-        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertBetween(2, 3, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -183,8 +149,7 @@ public class ArraySubSpliteratorTest {
     @Test
     public void trySplitThree() {
         Integer[] array = new Integer[]{1, 2, 3};
-        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertBetween(3, 4, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -195,8 +160,7 @@ public class ArraySubSpliteratorTest {
     @Test
     public void trySplitFour() {
         Integer[] array = new Integer[]{1, 2, 3, 4};
-        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertBetween(4, 5, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -207,8 +171,7 @@ public class ArraySubSpliteratorTest {
     @Test
     public void trySplitFive() {
         Integer[] array = new Integer[]{1, 2, 3, 4, 5};
-        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertBetween(5, 6, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -224,8 +187,7 @@ public class ArraySubSpliteratorTest {
                 array[i] = i;
             }
         }
-        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertEquals(array.length, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -241,56 +203,49 @@ public class ArraySubSpliteratorTest {
     @Test
     public void estimateSizeZero() {
         Integer[] array = new Integer[]{};
-        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         assertBetween(0, 1, spliterator.estimateSize());
     }
 
     @Test
     public void estimateSizeOne() {
         Integer[] array = new Integer[]{1};
-        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         assertBetween(1, 2, spliterator.estimateSize());
     }
 
     @Test
     public void estimateSizeTwo() {
         Integer[] array = new Integer[]{1, 2};
-        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         assertBetween(2, 3, spliterator.estimateSize());
     }
 
     @Test
     public void estimateSizeFive() {
         Integer[] array = new Integer[]{1, 2, 3, 4, 5};
-        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         assertBetween(5, 6, spliterator.estimateSize());
     }
 
     @Test
     public void characteristics() {
         Integer[] array = new Integer[]{1, 2, 3, 4, 5};
-        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics());
     }
 
     @Test
     public void splitWithOneElementNull() {
         Integer[] array = new Integer[]{1};
-        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         assertNull(spliterator.trySplit());
     }
 
     @Test
     public void splitWithOneRemainingElementNull() {
         Integer[] array = new Integer[]{1, 2};
-        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         spliterator.tryAdvance((i) -> {
         });
         assertNull(spliterator.trySplit());
diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliteratorTest.java
deleted file mode 100644
index 11e4fb8a4e..0000000000
--- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliteratorTest.java
+++ /dev/null
@@ -1,675 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.
- *
- *   SPDX-License-Identifier: Apache-2.0
- */
-package org.apache.jena.mem.spliterator;
-
-import org.junit.Test;
-
-import org.apache.jena.mem.collection.FastHashSet;
-import java.util.ArrayList;
-
-import static java.util.Spliterator.*;
-import static org.junit.Assert.*;
-
-public class SparseArrayIndexedSpliteratorTest {
-
-    @Test
-    public void tryAdvanceEmpty() {
-        {
-            final var array = new String[0];
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            assertFalse(spliterator.tryAdvance(i -> fail("Should not have advanced")));
-        }
-        {
-            final var array = new String[1];
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            assertFalse(spliterator.tryAdvance(i -> fail("Should not have advanced")));
-        }
-    }
-
-    @Test
-    public void tryAdvanceOne() {
-        {
-            final var array = new String[]{"a"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            for(int i=0; i<array.length; i++) {
-                final var index = i;
-                assertTrue(spliterator.tryAdvance(entry -> {
-                    assertEquals(index, entry.index());
-                    assertEquals(array[index], entry.key());
-                }));
-            }
-            assertFalse(spliterator.tryAdvance(i -> fail("Should not have advanced")));
-        }
-        {
-            final var array = new String[]{"a", null};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var itemsFound = new ArrayList<>();
-            while (true) {
-                if (!spliterator.tryAdvance(entry -> itemsFound.add(entry.key()))) {
-                    break;
-                }
-            }
-            assertEquals(1, itemsFound.size());
-            assertTrue(itemsFound.contains("a"));
-        }
-        {
-            final var array = new String[]{null, "a"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            assertTrue(spliterator.tryAdvance(entry -> {
-                assertEquals(1, entry.index());
-                assertEquals("a", entry.key());
-            }));
-            assertFalse(spliterator.tryAdvance(i -> fail("Should not have advanced")));
-        }
-    }
-
-    @Test
-    public void tryAdvanceTwo() {
-        {
-            final var array = new String[]{"a", "b"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            while (spliterator.tryAdvance(i -> {
-                keysFound.add(i.key());
-                indicesFound.add(i.index());
-            }));
-            assertEquals(2, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals(0, indicesFound.get(0));
-            assertEquals(1, indicesFound.get(1));
-        }
-        {
-            final var array = new String[]{"a", null, "b"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            while (spliterator.tryAdvance(i -> {
-                keysFound.add(i.key());
-                indicesFound.add(i.index());
-            }));
-            assertEquals(2, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals(0, indicesFound.get(0));
-            assertEquals(2, indicesFound.get(1));
-        }
-        {
-            final var array = new String[]{"a", null, null, "b"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            while (spliterator.tryAdvance(i -> {
-                keysFound.add(i.key());
-                indicesFound.add(i.index());
-            }));
-            assertEquals(2, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals(0, indicesFound.get(0));
-            assertEquals(3, indicesFound.get(1));
-        }
-        {
-            final var array = new String[]{null, "a", null, "b"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            while (spliterator.tryAdvance(i -> {
-                keysFound.add(i.key());
-                indicesFound.add(i.index());
-            }));
-            assertEquals(2, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals(1, indicesFound.get(0));
-            assertEquals(3, indicesFound.get(1));
-        }
-        {
-            final var array = new String[]{null, "a", null, null, "b"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            while (spliterator.tryAdvance(i -> {
-                keysFound.add(i.key());
-                indicesFound.add(i.index());
-            }));
-            assertEquals(2, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals(1, indicesFound.get(0));
-            assertEquals(4, indicesFound.get(1));
-        }
-    }
-
-    @Test
-    public void tryAdvanceThree() {
-        {
-            final var array = new String[]{"a", "b", "c"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            while (spliterator.tryAdvance(i -> {
-                keysFound.add(i.key());
-                indicesFound.add(i.index());
-            }));
-            assertEquals(3, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals("c", keysFound.get(2));
-            assertEquals(0, indicesFound.get(0));
-            assertEquals(1, indicesFound.get(1));
-            assertEquals(2, indicesFound.get(2));
-        }
-        {
-            final var array = new String[]{"a", null, "b", "c"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            while (spliterator.tryAdvance(i -> {
-                keysFound.add(i.key());
-                indicesFound.add(i.index());
-            }));
-            assertEquals(3, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals("c", keysFound.get(2));
-            assertEquals(0, indicesFound.get(0));
-            assertEquals(2, indicesFound.get(1));
-            assertEquals(3, indicesFound.get(2));
-        }
-        {
-            final var array = new String[]{"a", null, null, "b", "c"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            while (spliterator.tryAdvance(i -> {
-                keysFound.add(i.key());
-                indicesFound.add(i.index());
-            }));
-            assertEquals(3, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals("c", keysFound.get(2));
-            assertEquals(0, indicesFound.get(0));
-            assertEquals(3, indicesFound.get(1));
-            assertEquals(4, indicesFound.get(2));
-        }
-        {
-            final var array = new String[]{null, "a", null, "b", null, "c"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            while (spliterator.tryAdvance(i -> {
-                keysFound.add(i.key());
-                indicesFound.add(i.index());
-            }));
-            assertEquals(3, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals("c", keysFound.get(2));
-            assertEquals(1, indicesFound.get(0));
-            assertEquals(3, indicesFound.get(1));
-            assertEquals(5, indicesFound.get(2));
-        }
-        {
-            final var array = new String[]{null, "a", null, null, "b", null, "c"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            while (spliterator.tryAdvance(i -> {
-                keysFound.add(i.key());
-                indicesFound.add(i.index());
-            }));
-            assertEquals(3, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals("c", keysFound.get(2));
-            assertEquals(1, indicesFound.get(0));
-            assertEquals(4, indicesFound.get(1));
-            assertEquals(6, indicesFound.get(2));
-        }
-    }
-
-    @Test
-    public void forEachRemainingEmpty() {
-        {
-            final var array = new String[]{};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            var itemsFound = new ArrayList<FastHashSet.IndexedKey<String>>();
-            spliterator.forEachRemaining(itemsFound::add);
-            assertEquals(0, itemsFound.size());
-        }
-        {
-            final var array = new String[]{null};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            var itemsFound = new ArrayList<>();
-            spliterator.forEachRemaining(itemsFound::add);
-            assertEquals(0, itemsFound.size());
-        }
-    }
-
-    @Test
-    public void forEachRemainingOne() {
-        {
-            final var array = new String[]{"a"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            spliterator.forEachRemaining(entry -> {
-                keysFound.add(entry.key());
-                indicesFound.add(entry.index());
-            });
-            assertEquals(1, keysFound.size());
-            assertTrue(keysFound.contains("a"));
-            assertTrue(indicesFound.contains(0));
-        }
-        {
-            final var array = new String[]{null, "a"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            spliterator.forEachRemaining(entry -> {
-                keysFound.add(entry.key());
-                indicesFound.add(entry.index());
-            });
-            assertEquals(1, keysFound.size());
-            assertTrue(keysFound.contains("a"));
-            assertTrue(indicesFound.contains(1));
-        }
-        {
-            final var array = new String[]{"a", null};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            spliterator.forEachRemaining(entry -> {
-                keysFound.add(entry.key());
-                indicesFound.add(entry.index());
-            });
-            assertEquals(1, keysFound.size());
-            assertTrue(keysFound.contains("a"));
-            assertTrue(indicesFound.contains(0));
-        }
-    }
-
-    @Test
-    public void forEachRemainingTwo() {
-        {
-            final var array = new String[]{"a", "b"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            spliterator.forEachRemaining(entry -> {
-                keysFound.add(entry.key());
-                indicesFound.add(entry.index());
-            });
-            assertEquals(2, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals(0, indicesFound.get(0));
-            assertEquals(1, indicesFound.get(1));
-        }
-        {
-            final var array = new String[]{"a", null, "b"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            spliterator.forEachRemaining(entry -> {
-                keysFound.add(entry.key());
-                indicesFound.add(entry.index());
-            });
-            assertEquals(2, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals(0, indicesFound.get(0));
-            assertEquals(2, indicesFound.get(1));
-        }
-        {
-            final var array = new String[]{"a", null, null, "b"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            spliterator.forEachRemaining(entry -> {
-                keysFound.add(entry.key());
-                indicesFound.add(entry.index());
-            });
-            assertEquals(2, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals(0, indicesFound.get(0));
-            assertEquals(3, indicesFound.get(1));
-        }
-        {
-            final var array = new String[]{null, "a", null, "b"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            spliterator.forEachRemaining(entry -> {
-                keysFound.add(entry.key());
-                indicesFound.add(entry.index());
-            });
-            assertEquals(2, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals(1, indicesFound.get(0));
-            assertEquals(3, indicesFound.get(1));
-        }
-        {
-            final var array = new String[]{null, "a", null, null, "b"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            spliterator.forEachRemaining(entry -> {
-                keysFound.add(entry.key());
-                indicesFound.add(entry.index());
-            });
-            assertEquals(2, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals(1, indicesFound.get(0));
-            assertEquals(4, indicesFound.get(1));
-        }
-    }
-
-    @Test
-    public void forEachRemainingThree() {
-        {
-            final var array = new String[]{"a", "b", "c"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            spliterator.forEachRemaining(entry -> {
-                keysFound.add(entry.key());
-                indicesFound.add(entry.index());
-            });
-            assertEquals(3, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals("c", keysFound.get(2));
-            assertEquals(0, indicesFound.get(0));
-            assertEquals(1, indicesFound.get(1));
-            assertEquals(2, indicesFound.get(2));
-        }
-        {
-            final var array = new String[]{"a", null, "b", "c"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            spliterator.forEachRemaining(entry -> {
-                keysFound.add(entry.key());
-                indicesFound.add(entry.index());
-            });
-            assertEquals(3, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals("c", keysFound.get(2));
-            assertEquals(0, indicesFound.get(0));
-            assertEquals(2, indicesFound.get(1));
-            assertEquals(3, indicesFound.get(2));
-        }
-        {
-            final var array = new String[]{"a", null, null, "b", "c"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            spliterator.forEachRemaining(entry -> {
-                keysFound.add(entry.key());
-                indicesFound.add(entry.index());
-            });
-            assertEquals(3, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals("c", keysFound.get(2));
-            assertEquals(0, indicesFound.get(0));
-            assertEquals(3, indicesFound.get(1));
-            assertEquals(4, indicesFound.get(2));
-        }
-        {
-            final var array = new String[]{null, "a", null, "b", "c"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            spliterator.forEachRemaining(entry -> {
-                keysFound.add(entry.key());
-                indicesFound.add(entry.index());
-            });
-            assertEquals(3, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals("c", keysFound.get(2));
-            assertEquals(1, indicesFound.get(0));
-            assertEquals(3, indicesFound.get(1));
-            assertEquals(4, indicesFound.get(2));
-        }
-        {
-            final var array = new String[]{null, "a", null, null, "b", "c"};
-            final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-            });
-            final var keysFound = new ArrayList<>();
-            final var indicesFound = new ArrayList<>();
-            spliterator.forEachRemaining(entry -> {
-                keysFound.add(entry.key());
-                indicesFound.add(entry.index());
-            });
-            assertEquals(3, keysFound.size());
-            assertEquals("a", keysFound.get(0));
-            assertEquals("b", keysFound.get(1));
-            assertEquals("c", keysFound.get(2));
-            assertEquals(1, indicesFound.get(0));
-            assertEquals(4, indicesFound.get(1));
-            assertEquals(5, indicesFound.get(2));
-        }
-    }
-
-    @Test
-    public void trySplitEmpty() {
-        final var array = new String[]{};
-        final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-        });
-        assertNull(spliterator.trySplit());
-    }
-
-    @Test
-    public void trySplitOne() {
-        final var array = new String[]{"a"};
-        final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-        });
-        assertNull(spliterator.trySplit());
-    }
-
-    @Test
-    public void trySplitTwo() {
-        final var array = new String[]{"a", "b"};
-        final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-        });
-        // Estimated size is not exact
-        assertBetween(2, 3, spliterator.estimateSize());
-        final var split = spliterator.trySplit();
-        assertBetween(1, 2, spliterator.estimateSize());
-        assertBetween(1, 3, split.estimateSize());
-    }
-
-    @Test
-    public void trySplitThree() {
-        final var array = new String[]{"a", "b", "c"};
-        final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-        });
-        // Estimated size is not exact
-        assertBetween(3, 4, spliterator.estimateSize());
-        final var split = spliterator.trySplit();
-        assertBetween(1, 3, spliterator.estimateSize());
-        assertBetween(2, 3, split.estimateSize());
-    }
-
-    @Test
-    public void trySplitFour() {
-        final var array = new String[]{"a", "b", "c", "d"};
-        final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-        });
-        // Estimated size is not exact
-        assertBetween(4, 5, spliterator.estimateSize());
-        final var split = spliterator.trySplit();
-        assertBetween(2, 3, spliterator.estimateSize());
-        assertBetween(2, 4, split.estimateSize());
-    }
-
-    @Test
-    public void trySplitFive() {
-        final var array = new String[]{"a", "b", "c", "d", "e"};
-        final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-        });
-        // Estimated size is not exact
-        assertBetween(5, 6, spliterator.estimateSize());
-        final var split = spliterator.trySplit();
-        assertBetween(2, 4, spliterator.estimateSize());
-        assertBetween(2, 4, split.estimateSize());
-
-        final var keysFound = new ArrayList<>();
-        final var indicesFound = new ArrayList<>();
-        split.forEachRemaining(entry -> {
-            keysFound.add(entry.key());
-            indicesFound.add(entry.index());
-        });
-        spliterator.forEachRemaining(entry -> {
-            keysFound.add(entry.key());
-            indicesFound.add(entry.index());
-        });
-        assertEquals(5, keysFound.size());
-        assertEquals("a", keysFound.get(0));
-        assertEquals("b", keysFound.get(1));
-        assertEquals("c", keysFound.get(2));
-        assertEquals("d", keysFound.get(3));
-        assertEquals("e", keysFound.get(4));
-        assertEquals(0, indicesFound.get(0));
-        assertEquals(1, indicesFound.get(1));
-        assertEquals(2, indicesFound.get(2));
-        assertEquals(3, indicesFound.get(3));
-        assertEquals(4, indicesFound.get(4));
-    }
-
-    @Test
-    public void trySplitOneHundred() {
-        Integer[] array = new Integer[200];
-        for (int i = 0; i < array.length; i++) {
-            if (i % 2 == 0) {
-                array[i] = i;
-            }
-        }
-        final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-        });
-        // Estimated size is not exact
-        assertBetween(array.length, array.length+1, spliterator.estimateSize());
-        final var split = spliterator.trySplit();
-        assertBetween(array.length / 2, (array.length / 2) + 1, spliterator.estimateSize());
-        assertBetween(array.length / 2, (array.length / 2) + 1, split.estimateSize());
-    }
-
-    private void assertBetween(long min, long max, long estimateSize) {
-        assertTrue("estimateSize=" + estimateSize + " min=" + min + " max=" + max, estimateSize >= min);
-        assertTrue("estimateSize=" + estimateSize + " min=" + min + " max=" + max, estimateSize <= max);
-    }
-
-    @Test
-    public void estimateSizeZero() {
-        final var array = new String[]{};
-        final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-        });
-        assertBetween(0, 1, spliterator.estimateSize());
-    }
-
-    @Test
-    public void estimateSizeOne() {
-        final var array = new String[]{"a"};
-        final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-        });
-        assertBetween(1, 2, spliterator.estimateSize());
-    }
-
-    @Test
-    public void estimateSizeTwo() {
-        final var array = new String[]{"a", "b"};
-        final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-        });
-        assertBetween(2, 3, spliterator.estimateSize());
-    }
-
-    @Test
-    public void estimateSizeFive() {
-        final var array = new String[]{"a", "b", "c", "d", "e"};
-        final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-        });
-        assertBetween(5, 6, spliterator.estimateSize());
-    }
-
-    @Test
-    public void characteristics() {
-        final var array = new String[]{"a", "b", "c", "d", "e"};
-        final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-        });
-        assertEquals(DISTINCT | NONNULL | IMMUTABLE, spliterator.characteristics());
-    }
-
-    @Test
-    public void splitWithOneElementNull() {
-        final var array = new String[]{null};
-        final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-        });
-        assertNull(spliterator.trySplit());
-    }
-
-    @Test
-    public void splitWithOneRemainingElementNull() {
-        final var array = new String[]{"a", null};
-        final var spliterator = new SparseArrayIndexedSpliterator<>(array, () -> {
-        });
-        spliterator.tryAdvance(i -> {});
-        assertNull(spliterator.trySplit());
-    }
-}
\ No newline at end of file
diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/SparseArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/SparseArraySpliteratorTest.java
index c40114b176..bef1779184 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/SparseArraySpliteratorTest.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/SparseArraySpliteratorTest.java
@@ -20,6 +20,8 @@
  */
 package org.apache.jena.mem.spliterator;
 
+import org.apache.jena.mem.collection.FastHashSet;
+import org.apache.jena.mem.collection.JenaSet;
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -30,18 +32,23 @@ import static org.junit.Assert.*;
 
 public class SparseArraySpliteratorTest {
 
+    private static final JenaSet<Object> dummySetForConcurrencyCheck = new FastHashSet<>() {
+        @Override
+        protected Object[] newKeysArray(int size) {
+            return new Object[size];
+        }
+    };
+
     @Test
     public void tryAdvanceEmpty() {
         {
             Integer[] array = new Integer[0];
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             assertFalse(spliterator.tryAdvance(i -> fail("Should not have advanced")));
         }
         {
             Integer[] array = new Integer[1];
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             assertFalse(spliterator.tryAdvance(i -> fail("Should not have advanced")));
         }
     }
@@ -50,8 +57,7 @@ public class SparseArraySpliteratorTest {
     public void tryAdvanceOne() {
         {
             Integer[] array = new Integer[]{1};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance( itemsFound::add)) { /*empty*/ }
             assertEquals(1, itemsFound.size());
@@ -59,8 +65,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(1, itemsFound.size());
@@ -68,8 +73,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(1, itemsFound.size());
@@ -81,8 +85,7 @@ public class SparseArraySpliteratorTest {
     public void tryAdvanceTwo() {
         {
             Integer[] array = new Integer[]{1, 2};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(2, itemsFound.size());
@@ -91,8 +94,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null, 2};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(2, itemsFound.size());
@@ -101,8 +103,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null, null, 2};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(2, itemsFound.size());
@@ -111,8 +112,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1, null, 2};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(2, itemsFound.size());
@@ -121,8 +121,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1, null, null, 2};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(2, itemsFound.size());
@@ -135,8 +134,7 @@ public class SparseArraySpliteratorTest {
     public void tryAdvanceThree() {
         {
             Integer[] array = new Integer[]{1, 2, 3};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(3, itemsFound.size());
@@ -146,8 +144,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null, 2, 3};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(3, itemsFound.size());
@@ -157,8 +154,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null, null, 2, 3};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(3, itemsFound.size());
@@ -168,8 +164,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1, null, 2, null, 3};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(3, itemsFound.size());
@@ -179,8 +174,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1, null, null, 2, null, 3};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(3, itemsFound.size());
@@ -194,16 +188,14 @@ public class SparseArraySpliteratorTest {
     public void forEachRemainingEmpty() {
         {
             Integer[] array = new Integer[]{};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(0, itemsFound.size());
         }
         {
             Integer[] array = new Integer[]{null};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(0, itemsFound.size());
@@ -214,8 +206,7 @@ public class SparseArraySpliteratorTest {
     public void forEachRemainingOne() {
         {
             Integer[] array = new Integer[]{1};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(1, itemsFound.size());
@@ -223,8 +214,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(1, itemsFound.size());
@@ -232,8 +222,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(1, itemsFound.size());
@@ -245,8 +234,7 @@ public class SparseArraySpliteratorTest {
     public void forEachRemainingTwo() {
         {
             Integer[] array = new Integer[]{1, 2};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(2, itemsFound.size());
@@ -255,8 +243,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null, 2};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(2, itemsFound.size());
@@ -265,8 +252,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null, null, 2};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(2, itemsFound.size());
@@ -275,8 +261,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1, null, 2};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(2, itemsFound.size());
@@ -285,8 +270,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1, null, null, 2};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(2, itemsFound.size());
@@ -299,8 +283,7 @@ public class SparseArraySpliteratorTest {
     public void forEachRemainingThree() {
         {
             Integer[] array = new Integer[]{1, 2, 3};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(3, itemsFound.size());
@@ -310,8 +293,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null, 2, 3};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(3, itemsFound.size());
@@ -321,8 +303,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null, null, 2, 3};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(3, itemsFound.size());
@@ -332,8 +313,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1, null, 2, 3};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(3, itemsFound.size());
@@ -343,8 +323,7 @@ public class SparseArraySpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1, null, null, 2, 3};
-            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(3, itemsFound.size());
@@ -354,24 +333,21 @@ public class SparseArraySpliteratorTest {
     @Test
     public void trySplitEmpty() {
         Integer[] array = new Integer[]{};
-        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
         assertNull(spliterator.trySplit());
     }
 
     @Test
     public void trySplitOne() {
         Integer[] array = new Integer[]{1};
-        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
         assertNull(spliterator.trySplit());
     }
 
     @Test
     public void trySplitTwo() {
         Integer[] array = new Integer[]{1, 2};
-        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertBetween(2, 3, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -382,8 +358,7 @@ public class SparseArraySpliteratorTest {
     @Test
     public void trySplitThree() {
         Integer[] array = new Integer[]{1, 2, 3};
-        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertBetween(3, 4, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -394,8 +369,7 @@ public class SparseArraySpliteratorTest {
     @Test
     public void trySplitFour() {
         Integer[] array = new Integer[]{1, 2, 3, 4};
-        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertBetween(4, 5, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -406,8 +380,7 @@ public class SparseArraySpliteratorTest {
     @Test
     public void trySplitFive() {
         Integer[] array = new Integer[]{1, 2, 3, 4, 5};
-        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertBetween(5, 6, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -423,8 +396,7 @@ public class SparseArraySpliteratorTest {
                 array[i] = i;
             }
         }
-        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertEquals(array.length, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -440,56 +412,49 @@ public class SparseArraySpliteratorTest {
     @Test
     public void estimateSizeZero() {
         Integer[] array = new Integer[]{};
-        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
         assertBetween(0, 1, spliterator.estimateSize());
     }
 
     @Test
     public void estimateSizeOne() {
         Integer[] array = new Integer[]{1};
-        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
         assertBetween(1, 2, spliterator.estimateSize());
     }
 
     @Test
     public void estimateSizeTwo() {
         Integer[] array = new Integer[]{1, 2};
-        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
         assertBetween(2, 3, spliterator.estimateSize());
     }
 
     @Test
     public void estimateSizeFive() {
         Integer[] array = new Integer[]{1, 2, 3, 4, 5};
-        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
         assertBetween(5, 6, spliterator.estimateSize());
     }
 
     @Test
     public void characteristics() {
         Integer[] array = new Integer[]{1, 2, 3, 4, 5};
-        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
         assertEquals(DISTINCT | NONNULL | IMMUTABLE, spliterator.characteristics());
     }
 
     @Test
     public void splitWithOneElementNull() {
         Integer[] array = new Integer[]{null};
-        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
         assertNull(spliterator.trySplit());
     }
 
     @Test
     public void splitWithOneRemainingElementNull() {
         Integer[] array = new Integer[]{1, null};
-        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, dummySetForConcurrencyCheck);
         spliterator.tryAdvance(i -> {
         });
         assertNull(spliterator.trySplit());
diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/SparseArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/SparseArraySubSpliteratorTest.java
index 5354297c4b..8b83aeb373 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/SparseArraySubSpliteratorTest.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/SparseArraySubSpliteratorTest.java
@@ -20,6 +20,8 @@
  */
 package org.apache.jena.mem.spliterator;
 
+import org.apache.jena.mem.collection.FastHashSet;
+import org.apache.jena.mem.collection.JenaSet;
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -30,18 +32,23 @@ import static org.junit.Assert.*;
 
 public class SparseArraySubSpliteratorTest {
 
+    private static final JenaSet<Object> dummySetForConcurrencyCheck = new FastHashSet<>() {
+        @Override
+        protected Object[] newKeysArray(int size) {
+            return new Object[size];
+        }
+    };
+
     @Test
     public void tryAdvanceEmpty() {
         {
             Integer[] array = new Integer[0];
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             assertFalse(spliterator.tryAdvance(i -> fail("Should not have advanced")));
         }
         {
             Integer[] array = new Integer[1];
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             assertFalse(spliterator.tryAdvance(i -> fail("Should not have advanced")));
         }
     }
@@ -50,8 +57,7 @@ public class SparseArraySubSpliteratorTest {
     public void tryAdvanceOne() {
         {
             Integer[] array = new Integer[]{1};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(1, itemsFound.size());
@@ -59,8 +65,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(1, itemsFound.size());
@@ -68,8 +73,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(1, itemsFound.size());
@@ -81,8 +85,7 @@ public class SparseArraySubSpliteratorTest {
     public void tryAdvanceTwo() {
         {
             Integer[] array = new Integer[]{1, 2};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(2, itemsFound.size());
@@ -91,8 +94,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null, 2};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(2, itemsFound.size());
@@ -101,8 +103,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null, null, 2};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(2, itemsFound.size());
@@ -111,8 +112,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1, null, 2};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(2, itemsFound.size());
@@ -121,8 +121,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1, null, null, 2};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(2, itemsFound.size());
@@ -135,8 +134,7 @@ public class SparseArraySubSpliteratorTest {
     public void tryAdvanceThree() {
         {
             Integer[] array = new Integer[]{1, 2, 3};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(3, itemsFound.size());
@@ -146,8 +144,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null, 2, 3};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(3, itemsFound.size());
@@ -157,8 +154,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null, null, 2, 3};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(3, itemsFound.size());
@@ -168,8 +164,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1, null, 2, null, 3};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(3, itemsFound.size());
@@ -179,8 +174,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1, null, null, 2, null, 3};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             while (spliterator.tryAdvance(itemsFound::add)) { /*empty*/ }
             assertEquals(3, itemsFound.size());
@@ -194,16 +188,14 @@ public class SparseArraySubSpliteratorTest {
     public void forEachRemainingEmpty() {
         {
             Integer[] array = new Integer[]{};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(0, itemsFound.size());
         }
         {
             Integer[] array = new Integer[]{null};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(0, itemsFound.size());
@@ -214,8 +206,7 @@ public class SparseArraySubSpliteratorTest {
     public void forEachRemainingOne() {
         {
             Integer[] array = new Integer[]{1};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(1, itemsFound.size());
@@ -223,8 +214,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(1, itemsFound.size());
@@ -232,8 +222,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(1, itemsFound.size());
@@ -245,8 +234,7 @@ public class SparseArraySubSpliteratorTest {
     public void forEachRemainingTwo() {
         {
             Integer[] array = new Integer[]{1, 2};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(2, itemsFound.size());
@@ -255,8 +243,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null, 2};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(2, itemsFound.size());
@@ -265,8 +252,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null, null, 2};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(2, itemsFound.size());
@@ -275,8 +261,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1, null, 2};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(2, itemsFound.size());
@@ -285,8 +270,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1, null, null, 2};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(2, itemsFound.size());
@@ -299,8 +283,7 @@ public class SparseArraySubSpliteratorTest {
     public void forEachRemainingThree() {
         {
             Integer[] array = new Integer[]{1, 2, 3};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(3, itemsFound.size());
@@ -310,8 +293,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null, 2, 3};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(3, itemsFound.size());
@@ -321,8 +303,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{1, null, null, 2, 3};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(3, itemsFound.size());
@@ -332,8 +313,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1, null, 2, 3};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(3, itemsFound.size());
@@ -343,8 +323,7 @@ public class SparseArraySubSpliteratorTest {
         }
         {
             Integer[] array = new Integer[]{null, 1, null, null, 2, 3};
-            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-            });
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
             var itemsFound = new ArrayList<>();
             spliterator.forEachRemaining(itemsFound::add);
             assertEquals(3, itemsFound.size());
@@ -357,24 +336,21 @@ public class SparseArraySubSpliteratorTest {
     @Test
     public void trySplitEmpty() {
         Integer[] array = new Integer[]{};
-        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         assertNull(spliterator.trySplit());
     }
 
     @Test
     public void trySplitOne() {
         Integer[] array = new Integer[]{1};
-        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         assertNull(spliterator.trySplit());
     }
 
     @Test
     public void trySplitTwo() {
         Integer[] array = new Integer[]{1, 2};
-        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertBetween(2, 3, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -385,8 +361,7 @@ public class SparseArraySubSpliteratorTest {
     @Test
     public void trySplitThree() {
         Integer[] array = new Integer[]{1, 2, 3};
-        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertBetween(3, 4, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -397,8 +372,7 @@ public class SparseArraySubSpliteratorTest {
     @Test
     public void trySplitFour() {
         Integer[] array = new Integer[]{1, 2, 3, 4};
-        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertBetween(4, 5, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -409,8 +383,7 @@ public class SparseArraySubSpliteratorTest {
     @Test
     public void trySplitFive() {
         Integer[] array = new Integer[]{1, 2, 3, 4, 5};
-        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertBetween(5, 6, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -426,8 +399,7 @@ public class SparseArraySubSpliteratorTest {
                 array[i] = i;
             }
         }
-        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         // Estimated size is not exact
         assertEquals(array.length, spliterator.estimateSize());
         Spliterator<Integer> split = spliterator.trySplit();
@@ -443,56 +415,49 @@ public class SparseArraySubSpliteratorTest {
     @Test
     public void estimateSizeZero() {
         Integer[] array = new Integer[]{};
-        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         assertBetween(0, 1, spliterator.estimateSize());
     }
 
     @Test
     public void estimateSizeOne() {
         Integer[] array = new Integer[]{1};
-        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         assertBetween(1, 2, spliterator.estimateSize());
     }
 
     @Test
     public void estimateSizeTwo() {
         Integer[] array = new Integer[]{1, 2};
-        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         assertBetween(2, 3, spliterator.estimateSize());
     }
 
     @Test
     public void estimateSizeFive() {
         Integer[] array = new Integer[]{1, 2, 3, 4, 5};
-        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         assertBetween(5, 6, spliterator.estimateSize());
     }
 
     @Test
     public void characteristics() {
         Integer[] array = new Integer[]{1, 2, 3, 4, 5};
-        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         assertEquals(DISTINCT | NONNULL | IMMUTABLE, spliterator.characteristics());
     }
 
     @Test
     public void splitWithOneElementNull() {
         Integer[] array = new Integer[]{null};
-        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         assertNull(spliterator.trySplit());
     }
 
     @Test
     public void splitWithOneRemainingElementNull() {
         Integer[] array = new Integer[]{1, null};
-        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, () -> {
-        });
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, dummySetForConcurrencyCheck);
         spliterator.tryAdvance(i -> {});
         assertNull(spliterator.trySplit());
     }
diff --git a/jena-core/src/test/java/org/apache/jena/mem/store/fast/FastHashedBunchMapTest.java b/jena-core/src/test/java/org/apache/jena/mem/store/fast/FastHashedBunchMapTest.java
new file mode 100644
index 0000000000..b4b6e97eba
--- /dev/null
+++ b/jena-core/src/test/java/org/apache/jena/mem/store/fast/FastHashedBunchMapTest.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+package org.apache.jena.mem.store.fast;
+
+import org.apache.jena.graph.Node;
+import org.junit.Test;
+
+import static org.apache.jena.testing_framework.GraphHelper.node;
+import static org.apache.jena.testing_framework.GraphHelper.triple;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for {@link FastHashedBunchMap}: a {@link Node}-keyed map of
+ * {@link FastTripleBunch}es, with a deep-copying constructor.
+ */
+public class FastHashedBunchMapTest {
+
+    @Test
+    public void newMapIsEmpty() {
+        final var map = new FastHashedBunchMap();
+        assertEquals(0, map.size());
+        assertTrue(map.isEmpty());
+    }
+
+    @Test
+    public void putAndGetReturnSameBunch() {
+        final var map = new FastHashedBunchMap();
+        final Node key = node("s");
+        final FastTripleBunch bunch = new FastHashedTripleBunch();
+        bunch.tryAdd(triple("s p o"));
+        bunch.tryAdd(triple("s p o2"));
+
+        map.put(key, bunch);
+        assertEquals(1, map.size());
+        assertSame(bunch, map.get(key));
+    }
+
+    @Test
+    public void getReturnsNullForMissingKey() {
+        final var map = new FastHashedBunchMap();
+        assertNull(map.get(node("missing")));
+    }
+
+    @Test
+    public void copyClonesEveryBunch() {
+        final var src = new FastHashedBunchMap();
+        final var bunchA = new FastHashedTripleBunch();
+        bunchA.tryAdd(triple("a p1 o1"));
+        bunchA.tryAdd(triple("a p2 o2"));
+        final var bunchB = new FastHashedTripleBunch();
+        bunchB.tryAdd(triple("b p1 o1"));
+        src.put(node("a"), bunchA);
+        src.put(node("b"), bunchB);
+
+        final var copy = src.copy();
+        assertEquals(2, copy.size());
+
+        final var copiedA = copy.get(node("a"));
+        final var copiedB = copy.get(node("b"));
+        assertNotNull(copiedA);
+        assertNotNull(copiedB);
+
+        // Bunches are deep-copied: same content, different identity.
+        assertNotSame(bunchA, copiedA);
+        assertNotSame(bunchB, copiedB);
+        assertEquals(bunchA.size(), copiedA.size());
+        assertEquals(bunchB.size(), copiedB.size());
+        assertTrue(copiedA.containsKey(triple("a p1 o1")));
+        assertTrue(copiedA.containsKey(triple("a p2 o2")));
+        assertTrue(copiedB.containsKey(triple("b p1 o1")));
+    }
+
+    @Test
+    public void copyIsIndependentOfSource() {
+        final var src = new FastHashedBunchMap();
+        final var bunch = new FastHashedTripleBunch();
+        bunch.tryAdd(triple("s p o"));
+        src.put(node("s"), bunch);
+
+        final var copy = src.copy();
+        // mutating the source bunch must not be visible in the copy
+        bunch.tryAdd(triple("s p o2"));
+        assertEquals(2, src.get(node("s")).size());
+        assertEquals(1, copy.get(node("s")).size());
+        assertFalse(copy.get(node("s")).containsKey(triple("s p o2")));
+    }
+
+    @Test
+    public void computeIfAbsentInsertsAndReusesValues() {
+        final var map = new FastHashedBunchMap();
+        final var produced = new FastHashedTripleBunch();
+        final FastTripleBunch first = map.computeIfAbsent(node("k"), () -> produced);
+        assertSame(produced, first);
+
+        // calling again must return the same bunch without invoking the supplier
+        final FastTripleBunch second = map.computeIfAbsent(node("k"),
+                () -> { throw new AssertionError("supplier must not be called when key is present"); });
+        assertSame(produced, second);
+    }
+
+    @Test
+    public void putReplacesExistingBunch() {
+        final var map = new FastHashedBunchMap();
+        final var b1 = new FastHashedTripleBunch();
+        b1.tryAdd(triple("s p o"));
+        final var b2 = new FastHashedTripleBunch();
+        b2.tryAdd(triple("s p o2"));
+
+        map.put(node("k"), b1);
+        map.put(node("k"), b2);
+        assertEquals(1, map.size());
+        assertSame(b2, map.get(node("k")));
+    }
+
+    @Test
+    public void removeRemovesEntry() {
+        final var map = new FastHashedBunchMap();
+        map.put(node("k"), new FastHashedTripleBunch());
+        assertTrue(map.tryRemove(node("k")));
+        assertEquals(0, map.size());
+        assertNull(map.get(node("k")));
+        // removing again is a no-op
+        assertFalse(map.tryRemove(node("k")));
+    }
+}