GH-1279: Improve the implementation of in-memory, general-purpose,non-transactional graphs.

Summary:
- Improved performance of GraphMem for:
  - Graph#find
    - Slightly by tuning the filter predicates
    - Significantly when results are processed via Iterator#forEachRemaining
  - Graph#stream
    - Slightly by tuning the filter predicates
    - Significantly for most stream operations
    - java.util.stream.BaseStream#parallel is now fully supported.
      (Before these changes, parallel execution was even slower than single-threaded execution)
  - Graph#contains
    - Only for non-concrete (fluent) patterns
- GraphMem used a variety of different Iterator implementations, helper classes, and wrappers:
  -> None of them supported #forEachRemaining
  -> It seemed appropriate to almost universally implement #forEachRemaining to avoid a wrapper or helper
     from breaking the newly gained performance advantages of #forEachRemaining

Details:

Implemented Iterator#forEachRemaining in an optimized way for many iterators throughout the Jena project:
- Replaced hasNext();next(); calls by #forEachRemaining in some promising places (not all)

Optimized NiceIterator:
- NiceIterator#hasNext now avoids redundant calls to current.hasNext()
- NiceIterator#andThen has optimized code to handle expensive hasNext calls of wrapped iterators.

Tuned GraphMem:
- Removed unused classes in the 'mem' namespace.
- Iterators:
  - Tuned Iterator implementations, mainly by adding forEachRemaining implementations
  - Optimized code in BasicKeyIterator. The new iterator works in reverse order, so I had to adapt HashCommon#removeFrom
  - Optimized code in HashedBunchMap#iterator
  - TrackingTripleIterator#forEachRemaining now simply calls super.forEachRemaining() and sets current to null
    -> This had a significant impact on performance
- Spliterators:
  - Created specialized spliterators SparseArraySpliterator and SparseArraySubSpliterator (+unit tests)
  - Replaced Spliterator implementation within HashCommon and HashedBunchMap with SparseArraySpliterator
  - Implemented Spliterators to support fast stream operations, where the SparseSpliterator*
    implementations were not suitable
- Replaced usage of org.apache.jena.graph.impl.GraphBase#containsByFind with optimized implementations:
  - Introduced org.apache.jena.graph.impl.TripleStore#containsMatch
  - Implemented org.apache.jena.mem.NodeToTriplesMapBase#containsMatch using the spliterator

- Filter operations:
  - Introduced org.apache.jena.graph.Triple.Field#filterOnConcrete to avoid double-checking of Node#isConcrete (+unit tests)
  - Created org.apache.jena.mem.FieldFilter to efficiently build filter predicates only when needed, and only with
    the required conditions (+unit tests)
  - Used org.apache.jena.mem.FieldFilter#filterOn in NodeToTriplesMap#iterator, NodeToTriplesMapBase#stream,
    and NodeToTriplesMapBase#containsMatch to only filter when a filter is needed.
    For example: For find(sub, ANY, ANY), there is no need for a filter in the underlying TripleBunch
diff --git a/jena-arq/src/main/java/org/apache/jena/query/ResultSet.java b/jena-arq/src/main/java/org/apache/jena/query/ResultSet.java
index 1ba28f9..44db088 100644
--- a/jena-arq/src/main/java/org/apache/jena/query/ResultSet.java
+++ b/jena-arq/src/main/java/org/apache/jena/query/ResultSet.java
@@ -20,6 +20,7 @@
 
 import java.util.Iterator ;
 import java.util.List ;
+import java.util.function.Consumer;
 
 import org.apache.jena.rdf.model.Model ;
 import org.apache.jena.sparql.engine.binding.Binding ;
@@ -53,6 +54,9 @@
     @Override
     public QuerySolution next() ;
 
+    @Override
+    public void forEachRemaining(Consumer<? super QuerySolution> action);
+
     /** Moves onto the next result (legacy - use .next()). */
     public QuerySolution nextSolution() ;
 
diff --git a/jena-arq/src/main/java/org/apache/jena/riot/protobuf/ProtobufRDF.java b/jena-arq/src/main/java/org/apache/jena/riot/protobuf/ProtobufRDF.java
index d9f8e4c..e4012ca 100644
--- a/jena-arq/src/main/java/org/apache/jena/riot/protobuf/ProtobufRDF.java
+++ b/jena-arq/src/main/java/org/apache/jena/riot/protobuf/ProtobufRDF.java
@@ -29,7 +29,6 @@
 import org.apache.jena.riot.system.StreamRDF;
 import org.apache.jena.riot.thrift.ThriftRDF;
 import org.apache.jena.sparql.core.Var;
-import org.apache.jena.sparql.engine.binding.Binding;
 import org.apache.jena.sparql.exec.RowSet;
 import org.apache.jena.sparql.exec.RowSetStream;
 
@@ -155,10 +154,7 @@
         try {
             List<Var> vars = rowSet.getResultVars();
             try ( Binding2Protobuf b2p = new Binding2Protobuf(out, vars, false) ) {
-                for ( ; rowSet.hasNext() ; ) {
-                    Binding b = rowSet.next();
-                    b2p.output(b);
-                }
+                rowSet.forEachRemaining(b2p::output);
             }
         } finally { IO.flush(out); }
     }
diff --git a/jena-arq/src/main/java/org/apache/jena/riot/rowset/rw/RowSetReaderTSV.java b/jena-arq/src/main/java/org/apache/jena/riot/rowset/rw/RowSetReaderTSV.java
index 5722e6e..f25a5cf 100644
--- a/jena-arq/src/main/java/org/apache/jena/riot/rowset/rw/RowSetReaderTSV.java
+++ b/jena-arq/src/main/java/org/apache/jena/riot/rowset/rw/RowSetReaderTSV.java
@@ -24,6 +24,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.*;
+import java.util.function.Consumer;
 import java.util.regex.Pattern;
 
 import org.apache.jena.atlas.io.IO;
@@ -199,6 +200,22 @@
            return row;
        }
 
+       @Override
+       public void forEachRemaining(Consumer<? super Binding> action) {
+           if ( finished )
+               return;
+           if ( null != currentBinding ) {
+               action.accept(currentBinding);
+               currentBinding = null;
+           }
+           Binding row;
+           while (null != (row = parseNextBinding())) {
+               action.accept(row);
+           }
+           IO.close(reader);
+           finished = true;
+       }
+
        private Binding parseNextBinding() {
            String line;
            try {
diff --git a/jena-arq/src/main/java/org/apache/jena/riot/system/RiotLib.java b/jena-arq/src/main/java/org/apache/jena/riot/system/RiotLib.java
index 77f641c..f1ffae0 100644
--- a/jena-arq/src/main/java/org/apache/jena/riot/system/RiotLib.java
+++ b/jena-arq/src/main/java/org/apache/jena/riot/system/RiotLib.java
@@ -283,10 +283,7 @@
      * Collect all the matching triples
      */
     public static void accTriples(Collection<Triple> acc, Graph graph, Node s, Node p, Node o) {
-        ExtendedIterator<Triple> iter = graph.find(s, p, o);
-        while (iter.hasNext())
-            acc.add(iter.next());
-        iter.close();
+        graph.find(s, p, o).forEach(acc::add);
     }
 
     public static void writeBase(IndentedWriter out, String base, boolean newStyle) {
diff --git a/jena-arq/src/main/java/org/apache/jena/riot/system/StreamRDFOps.java b/jena-arq/src/main/java/org/apache/jena/riot/system/StreamRDFOps.java
index 76e5745..9019c1d 100644
--- a/jena-arq/src/main/java/org/apache/jena/riot/system/StreamRDFOps.java
+++ b/jena-arq/src/main/java/org/apache/jena/riot/system/StreamRDFOps.java
@@ -116,11 +116,7 @@
     /** Set triples to a StreamRDF - does not call .start/.finish */
     public static void sendTriplesToStream(Iterator<Triple> iter, StreamRDF dest)
     {
-        for ( ; iter.hasNext() ; )
-        {
-            Triple t = iter.next() ;
-            dest.triple(t) ;
-        }
+        iter.forEachRemaining(dest::triple);
     }
 
     /** Send quads of a dataset (including default graph as quads) to a StreamRDF, without prefixes */
@@ -132,10 +128,6 @@
     /** Set quads to a StreamRDF - does not call .start/.finish */
     public static void sendQuadsToStream(Iterator<Quad> iter, StreamRDF dest)
     {
-        for ( ; iter.hasNext() ; )
-        {
-            Quad q = iter.next() ;
-            dest.quad(q) ;
-        }
+        iter.forEachRemaining(dest::quad);
     }
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/JsonIterator.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/JsonIterator.java
index 898ae76..1b5ee3d 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/JsonIterator.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/JsonIterator.java
@@ -20,6 +20,7 @@
 
 import java.util.Iterator;
 import java.util.Map;
+import java.util.function.Consumer;
 
 import org.apache.jena.atlas.json.JsonObject;
 import org.apache.jena.graph.Node;
@@ -44,4 +45,9 @@
 
     @Override
     public JsonObject next() { return results.next(); }
+
+    @Override
+    public void forEachRemaining(Consumer<? super JsonObject> action) {
+        results.forEachRemaining(action);
+    }
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/ResultSetCheckCondition.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/ResultSetCheckCondition.java
index aa7925c..a279492 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/ResultSetCheckCondition.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/ResultSetCheckCondition.java
@@ -19,6 +19,7 @@
 package org.apache.jena.sparql.engine;
 
 import java.util.List ;
+import java.util.function.Consumer;
 
 import org.apache.jena.query.QueryExecution ;
 import org.apache.jena.query.QuerySolution ;
@@ -79,6 +80,16 @@
         other.close();
     }
 
+    /**
+     * Attention: The check is only done once before the first consumer accept call.
+     * @param action The action to be performed for each element
+     */
+    @Override
+    public void forEachRemaining(Consumer<? super QuerySolution> action) {
+        check() ;
+        other.forEachRemaining(action);
+    }
+
     @Override
     public int getRowNumber() {
         check() ;
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/ResultSetStream.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/ResultSetStream.java
index f0e5e59..6099dfd 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/ResultSetStream.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/ResultSetStream.java
@@ -21,6 +21,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.NoSuchElementException;
+import java.util.function.Consumer;
 
 import org.apache.jena.atlas.iterator.Iter;
 import org.apache.jena.query.QuerySolution;
@@ -125,6 +126,17 @@
     @Override
     public QuerySolution next() { return nextSolution(); }
 
+    @Override
+    public void forEachRemaining(Consumer<? super QuerySolution> action) {
+        if ( queryExecutionIter == null )
+            return;
+        queryExecutionIter.forEachRemaining(binding -> {
+            rowNumber++;
+            action.accept(new ResultBinding(model, binding));
+        });
+        close();
+    }
+
     /** Return the "row number" - a count of the number of possibilities returned so far.
      *  Remains valid (as the total number of possibilities) after the iterator ends.
      */
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingInputStream.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingInputStream.java
index 422ede0..f4beee1 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingInputStream.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingInputStream.java
@@ -27,6 +27,7 @@
 import java.util.Collections ;
 import java.util.Iterator ;
 import java.util.List ;
+import java.util.function.Consumer;
 
 import org.apache.jena.atlas.iterator.IteratorSlotted ;
 import org.apache.jena.atlas.lib.Closeable ;
@@ -107,6 +108,11 @@
     }
 
     @Override
+    public void forEachRemaining(Consumer<? super Binding> action) {
+        iter.forEachRemaining(action);
+    }
+
+    @Override
     public void remove()
     { iter.remove() ; }
 
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/JsonResults.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/JsonResults.java
index 618ac46..3d5e379 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/JsonResults.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/JsonResults.java
@@ -21,6 +21,7 @@
 import java.util.Iterator;
 import java.util.Map;
 import java.util.NoSuchElementException;
+import java.util.function.Consumer;
 
 import org.apache.jena.atlas.json.JsonArray;
 import org.apache.jena.atlas.json.JsonObject;
@@ -83,6 +84,15 @@
             return jsonObject;
         }
 
+        @Override
+        public void forEachRemaining(Consumer<? super JsonObject> action) {
+            if ( queryIterator == null )
+                return;
+            queryIterator.forEachRemaining(binding
+                    -> action.accept( JsonResults.generateJsonObject(binding, template) ));
+            close();
+        }
+
         /** Close the query iterator */
         private void close() {
             queryIterator.close();
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/resultset/ResultSetMem.java b/jena-arq/src/main/java/org/apache/jena/sparql/resultset/ResultSetMem.java
index 9d917be..319adb2 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/resultset/ResultSetMem.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/resultset/ResultSetMem.java
@@ -20,6 +20,7 @@
 
 import java.util.ArrayList ;
 import java.util.List ;
+import java.util.function.Consumer;
 
 import org.apache.jena.atlas.iterator.PeekIterator ;
 import org.apache.jena.query.QuerySolution ;
@@ -147,6 +148,15 @@
     public QuerySolution next() { return nextSolution() ; }
 
     @Override
+    public void forEachRemaining(Consumer<? super QuerySolution> action) {
+        iterator.forEachRemaining(binding -> {
+            rowNumber++;
+            action.accept(new ResultBinding(model, binding));
+        });
+
+    }
+
+    @Override
     public void close() {}
 
     /** Reset this result set back to the beginning */
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/resultset/ResultSetPeeking.java b/jena-arq/src/main/java/org/apache/jena/sparql/resultset/ResultSetPeeking.java
index 8aa9327..219ed0c 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/resultset/ResultSetPeeking.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/resultset/ResultSetPeeking.java
@@ -20,6 +20,7 @@
 
 import java.util.List;
 import java.util.NoSuchElementException;
+import java.util.function.Consumer;
 
 import org.apache.jena.atlas.lib.Closeable;
 import org.apache.jena.query.QuerySolution ;
@@ -76,6 +77,13 @@
     }
 
     @Override
+    public void forEachRemaining(Consumer<? super QuerySolution> action) {
+        while (this.hasNext()) {
+            action.accept(this.next());
+        }
+    }
+
+    @Override
     public QuerySolution nextSolution() {
         return this.next();
     }
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/resultset/ResultSetWrapper.java b/jena-arq/src/main/java/org/apache/jena/sparql/resultset/ResultSetWrapper.java
index 157036a..f39ebdb 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/resultset/ResultSetWrapper.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/resultset/ResultSetWrapper.java
@@ -19,6 +19,7 @@
 package org.apache.jena.sparql.resultset;
 
 import java.util.List ;
+import java.util.function.Consumer;
 
 import org.apache.jena.query.QuerySolution ;
 import org.apache.jena.query.ResultSet ;
@@ -46,6 +47,11 @@
     }
 
     @Override
+    public void forEachRemaining(Consumer<? super QuerySolution> action) {
+        get().forEachRemaining(action);
+    }
+
+    @Override
     public QuerySolution nextSolution() {
         return get().nextSolution();
     }
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/iterator/Iter.java b/jena-base/src/main/java/org/apache/jena/atlas/iterator/Iter.java
index b444355..c08868c 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/iterator/Iter.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/iterator/Iter.java
@@ -218,10 +218,7 @@
     /** See {@link Stream#collect(Supplier, BiConsumer, BiConsumer)}, except without the {@code BiConsumer<R, R> combiner} */
     public static <T,R> R collect(Iterator<T> iter, Supplier<R> supplier, BiConsumer<R, ? super T> accumulator) {
         R result = supplier.get();
-        while(iter.hasNext()) {
-            T elt = iter.next();
-            accumulator.accept(result, elt);
-        }
+        iter.forEachRemaining(elt -> accumulator.accept(result, elt));
         return result;
     }
 
@@ -235,10 +232,7 @@
      * @see #map(Iterator, Function)
      */
     public static <T> void apply(Iterator<? extends T> stream, Consumer<T> action) {
-        for (; stream.hasNext();) {
-            T item = stream.next();
-            action.accept(item);
-        }
+        stream.forEachRemaining(action);
     }
 
     // ---- Filter
@@ -293,6 +287,22 @@
             throw new NoSuchElementException("filter.next");
         }
 
+        @Override
+        public void forEachRemaining(Consumer<? super T> action) {
+            if ( finished )
+                return;
+            if ( slotOccupied ) {
+                action.accept(slot);
+            }
+            T t;
+            while (stream.hasNext()) {
+                t = stream.next();
+                if ( filter.test(t) )
+                    action.accept(t);
+            }
+            slotOccupied = false;
+        }
+
         private void closeIterator() {
             if ( finished )
                 return;
@@ -418,6 +428,11 @@
         }
 
         @Override
+        public void forEachRemaining(Consumer<? super R> action) {
+            stream.forEachRemaining(item->action.accept(converter.apply(item)));
+        }
+
+        @Override
         public void close() {
             Iter.close(stream);
         }
@@ -461,6 +476,14 @@
         }
 
         @Override
+        public void forEachRemaining(Consumer<? super T> action) {
+            stream.forEachRemaining(item->{
+                this.action.accept(item);
+                action.accept(item);
+            });
+        }
+
+        @Override
         public void close() {
             Iter.close(stream);
         }
@@ -651,17 +674,14 @@
 
     /** Count the iterator (this is destructive on the iterator) */
     public static <T> long count(Iterator<T> iterator) {
-        long x = 0;
-        while (iterator.hasNext()) {
-            iterator.next();
-            x++;
-        }
-        return x;
+        ActionCount<T> action = new ActionCount<>();
+        iterator.forEachRemaining(action);
+        return action.getCount();
     }
 
     /** Consume the iterator */
     public static <T> void consume(Iterator<T> iterator) {
-        count(iterator);
+        iterator.forEachRemaining(x->{}); // Do nothing.
     }
 
     /** Create a string from an iterator, using the separator. Note: this consumes the iterator. */
@@ -748,15 +768,12 @@
 
     /** Print an iterator (destructive) */
     public static <T> void print(PrintStream out, Iterator<T> stream) {
-        apply(stream, out::println);
+        stream.forEachRemaining(out::println);
     }
 
     /** Send the elements of the iterator to a sink - consumes the iterator */
     public static <T> void sendToSink(Iterator<T> iter, Sink<T> sink) {
-        while ( iter.hasNext() ) {
-            T thing = iter.next();
-            sink.send(thing);
-        }
+        iter.forEachRemaining(sink::send);
         sink.close();
     }
 
@@ -887,6 +904,11 @@
         iterator.forEachRemaining(action);
     }
 
+    @Override
+    public void forEachRemaining(Consumer<? super T> action) {
+        iterator.forEachRemaining(action);
+    }
+
     /** Consume the {@code Iter} and produce a {@code Set} */
     public Set<T> toSet() {
         return toSet(iterator);
@@ -1009,7 +1031,7 @@
 
     /** Apply an action to every element of an iterator */
     public void apply(Consumer<T> action) {
-        apply(iterator, action);
+        iterator.forEachRemaining(action);
     }
 
     /** Join on an {@code Iterator}..
@@ -1077,7 +1099,7 @@
     /** Count the iterator (this is destructive on the iterator) */
     public long count() {
         ActionCount<T> action = new ActionCount<>();
-        apply(action);
+        this.forEachRemaining(action);
         return action.getCount();
     }
 
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorConcat.java b/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorConcat.java
index 9f36ec3..8d01eb1 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorConcat.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorConcat.java
@@ -22,6 +22,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.NoSuchElementException;
+import java.util.function.Consumer;
 
 /**
  * Iterator of Iterators IteratorConcat is better when there are lots of iterators to
@@ -91,6 +92,24 @@
     }
 
     @Override
+    public void forEachRemaining(Consumer<? super T> action) {
+        if( finished )
+            return;
+        if( current != null ) {
+            current.forEachRemaining(action);
+            Iter.close(current);
+        }
+        idx++;
+        for ( ; idx < iterators.size() ; idx++ ) {
+            current = iterators.get(idx);
+            current.forEachRemaining(action);
+            Iter.close(current);
+        }
+        current = null;
+        finished = true;
+    }
+
+    @Override
     public void close() {
         //iterators.forEach(Iter::close);
         // Earlier iterators already closed
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorCons.java b/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorCons.java
index ca83a9c..e4d3122 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorCons.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorCons.java
@@ -20,6 +20,7 @@
 
 import java.util.Iterator;
 import java.util.NoSuchElementException;
+import java.util.function.Consumer;
 
 import org.apache.jena.atlas.lib.Lib;
 
@@ -99,6 +100,18 @@
     }
 
     @Override
+    public void forEachRemaining(Consumer<? super T> action) {
+        if ( iter1 != null ) {
+            iter1.forEachRemaining(action);
+            iter1 = null;
+        }
+        if ( iter2 != null ) {
+            iter2.forEachRemaining(action);
+            iter2 = null;
+        }
+    }
+
+    @Override
     public void remove() {
         if ( null == removeFrom )
             throw new IllegalStateException("no calls to next() since last call to remove()");
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorDelayedInitialization.java b/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorDelayedInitialization.java
index 2d7f8e5..63f7feb 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorDelayedInitialization.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorDelayedInitialization.java
@@ -20,6 +20,7 @@
 
 import java.util.Iterator ;
 import java.util.NoSuchElementException ;
+import java.util.function.Consumer;
 
 /** Class to delay the initialization of an iterator until first call of an Iterator operation. */
 
@@ -61,6 +62,13 @@
     }
 
     @Override
+    public void forEachRemaining(Consumer<? super T> action) {
+        init() ;
+        iterator.forEachRemaining(action);
+        close();
+    }
+
+    @Override
     public void remove()
     {
         init() ;
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorFlatMap.java b/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorFlatMap.java
index 84c8291..9d5b9dd 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorFlatMap.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorFlatMap.java
@@ -20,6 +20,7 @@
 
 import java.util.Iterator;
 import java.util.NoSuchElementException;
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.stream.Stream;
 
@@ -78,6 +79,25 @@
     }
 
     @Override
+    public void forEachRemaining(Consumer<? super OUT> action) {
+        if ( finished )
+            return;
+        if ( current != null ) {
+            current.forEachRemaining(action);
+            Iter.close(current);
+            current = null;
+        }
+        input.forEachRemaining(x->{
+            current = mapper.apply(x);
+            if ( current == null )
+                return;
+            current.forEachRemaining(action);
+            Iter.close(current);
+        });
+        current = null;
+    }
+
+    @Override
     public void close() {
         if ( current != null )
             Iter.close(current);
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorInteger.java b/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorInteger.java
index 896c6f7..921f822 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorInteger.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorInteger.java
@@ -19,6 +19,7 @@
 package org.apache.jena.atlas.iterator;
 
 import java.util.Iterator ;
+import java.util.function.Consumer;
 
 public class IteratorInteger implements Iterator<Long>
 {
@@ -49,8 +50,14 @@
     @Override
     public Long next()
     {
-        Long v = current;
-        current++ ;
-        return v ;
+        return current++;
+    }
+
+    @Override
+    public void forEachRemaining(Consumer<? super Long> action) {
+        while (current < finish) {
+            action.accept(current);
+            current++;
+        }
     }
 }
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorOnClose.java b/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorOnClose.java
index d89b433..668d1ba 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorOnClose.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorOnClose.java
@@ -20,6 +20,7 @@
 
 import java.util.Iterator;
 import java.util.NoSuchElementException;
+import java.util.function.Consumer;
 
 /**
  * Add an "onClose" action to an Iterator.
@@ -58,6 +59,12 @@
     }
 
     @Override
+    public void forEachRemaining(Consumer<? super T> action) {
+        super.forEachRemaining(action);
+        close();
+    }
+
+    @Override
     public void close() {
         if ( ! hasClosed ) {
             try {
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorWrapper.java b/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorWrapper.java
index e26962d..c3a45e5 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorWrapper.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorWrapper.java
@@ -19,6 +19,7 @@
 package org.apache.jena.atlas.iterator;
 
 import java.util.Iterator;
+import java.util.function.Consumer;
 
 public class IteratorWrapper<T> implements IteratorCloseable<T> {
     protected final Iterator<T> iterator;
@@ -47,6 +48,11 @@
     }
 
     @Override
+    public void forEachRemaining(Consumer<? super T> action) {
+        get().forEachRemaining(action);
+    }
+
+    @Override
     public void close() {
         Iter.close(iterator);
     }
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/iterator/PushbackIterator.java b/jena-base/src/main/java/org/apache/jena/atlas/iterator/PushbackIterator.java
index 3fa0f9b..2bbd6a2 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/iterator/PushbackIterator.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/iterator/PushbackIterator.java
@@ -21,6 +21,7 @@
 import java.util.ArrayDeque ;
 import java.util.Deque ;
 import java.util.Iterator ;
+import java.util.function.Consumer;
 
 /**
  * An iterator where you can push items back into the iterator, to be yielded (LIFO) next time.
@@ -53,4 +54,12 @@
             return items.pop() ;
         return iter.next() ;
     }
+
+    @Override
+    public void forEachRemaining(Consumer<? super T> action) {
+        while(!items.isEmpty()) {
+            action.accept(items.pop());
+        }
+        iter.forEachRemaining(action);
+    }
 }
diff --git a/jena-base/src/test/java/org/apache/jena/atlas/iterator/TestIter.java b/jena-base/src/test/java/org/apache/jena/atlas/iterator/TestIter.java
index f5c4ee9..fe4c3b4 100644
--- a/jena-base/src/test/java/org/apache/jena/atlas/iterator/TestIter.java
+++ b/jena-base/src/test/java/org/apache/jena/atlas/iterator/TestIter.java
@@ -128,6 +128,15 @@
         assertFalse(iter.hasNext());
     }
 
+    private static void testWithForeachRemaining(Iterator<? > iter, Object...items) {
+        Integer i[] = {0};
+        iter.forEachRemaining(x -> {
+            assertTrue(i[0] < items.length);
+            assertEquals(items[i[0]], x);
+            i[0]++;
+        });
+    }
+
     static Iter.Folder<String, String> f1 = (acc, arg)->acc + arg ;
 
     @Test
@@ -159,12 +168,64 @@
     }
 
     @Test
+    public void operate_01() {
+        var elements = new ArrayList<>(Arrays.asList("x", "y", "z"));
+        Iterator<String> it = Iter.operate(data2.iterator(), item -> elements.remove(item));
+        test(it, elements.toArray());
+        assertEquals(0, elements.size());
+    }
+
+    @Test
+    public void operate_02() {
+        var elements = new ArrayList<>(Arrays.asList("x", "y", "z"));
+        Iterator<String> it = Iter.operate(data2.iterator(), item -> elements.remove(item));
+        testWithForeachRemaining(it, elements.toArray());
+        assertEquals(0, elements.size());
+    }
+
+    @Test
+    public void limit_01() {
+        Iterator<String> it = Iter.limit(data2.iterator(), 0);
+        assertFalse(it.hasNext());
+    }
+
+    @Test
+    public void limit_02() {
+        Iterator<String> it = Iter.limit(data2.iterator(), 1);
+        test(it, "x");
+    }
+
+    @Test
+    public void limit_03() {
+        Iterator<String> it = Iter.limit(data2.iterator(), 2);
+        test(it, "x", "y");
+    }
+
+    @Test
+    public void limit_04() {
+        Iterator<String> it = Iter.limit(data2.iterator(), 3);
+        test(it, "x", "y", "z");
+    }
+
+    @Test
+    public void limit_05() {
+        Iterator<String> it = Iter.limit(data2.iterator(), 4);
+        test(it, "x", "y", "z");
+    }
+
+    @Test
     public void map_01() {
         Iterator<String> it = Iter.map(data2.iterator(), item -> item + item);
         test(it, "xx", "yy", "zz");
     }
 
     @Test
+    public void map_02() {
+        Iterator<String> it = Iter.map(data2.iterator(), item -> item + item);
+        testWithForeachRemaining(it, "xx", "yy", "zz");
+    }
+
+    @Test
     public void flatmap_01() {
         Iterator<String> it = Iter.flatMap(data2.iterator(), item -> Arrays.asList(item+item, item).iterator());
         test(it, "xx", "x", "yy", "y", "zz", "z");
@@ -179,7 +240,6 @@
         });
         test(it, 1, 9);
     }
-
     @Test
     public void flatmap_03() {
         List<Integer> data = Arrays.asList(1,2,3);
@@ -195,6 +255,37 @@
         Iter<String> it = Iter.iter(data.iterator()).flatMap(mapper);
         test(it, "two");
     }
+    @Test
+    public void flatmap_04() {
+        Iterator<String> it = Iter.flatMap(data2.iterator(), item -> Arrays.asList(item+item, item).iterator());
+        testWithForeachRemaining(it, "xx", "x", "yy", "y", "zz", "z");
+    }
+
+    @Test
+    public void flatmap_05() {
+        List<Integer> data = Arrays.asList(1,2,3);
+        Iterator<Integer> it = Iter.flatMap(data.iterator(), x -> {
+            if ( x == 2 ) return Iter.nullIterator();
+            return Arrays.asList(x*x).iterator();
+        });
+        testWithForeachRemaining(it, 1, 9);
+    }
+
+    @Test
+    public void flatmap_06() {
+        List<Integer> data = Arrays.asList(1,2,3);
+        Function<Integer, Iterator<String>> mapper = x -> {
+            switch(x) {
+                case 1: return Iter.nullIterator();
+                case 2: return Arrays.asList("two").iterator();
+                case 3: return Iter.nullIterator();
+                default: throw new IllegalArgumentException();
+            }
+        };
+
+        Iter<String> it = Iter.iter(data.iterator()).flatMap(mapper);
+        testWithForeachRemaining(it, "two");
+    }
 
     private Predicate<String> filter = item -> item.length() == 1;
 
@@ -515,6 +606,18 @@
     }
 
     @Test
+    public void filter_04() {
+        Iterator<String> it = Iter.filter(data3.iterator(), item -> "x".equals(item) || "z".equals(item));
+        testWithForeachRemaining(it, "x", "z");
+    }
+
+    @Test
+    public void filter_05() {
+        Iterator<String> it = Iter.filter(data3.iterator(), item -> null == item || "x".equals(item));
+        testWithForeachRemaining(it, null, "x", null, null, null, null);
+    }
+
+    @Test
     public void distinct_01() {
         List<String> x = Arrays.asList("a", "b", "a");
         Iterator<String> iter = Iter.distinct(x.iterator());
diff --git a/jena-core/src/main/java/org/apache/jena/graph/Triple.java b/jena-core/src/main/java/org/apache/jena/graph/Triple.java
index 3f9a1d5..93d7f5d 100644
--- a/jena-core/src/main/java/org/apache/jena/graph/Triple.java
+++ b/jena-core/src/main/java/org/apache/jena/graph/Triple.java
@@ -197,6 +197,8 @@
 
         public abstract Predicate<Triple> filterOn( Node n );
 
+        public abstract Predicate<Triple> filterOnConcrete( Node n );
+
         public final Predicate<Triple> filterOn( Triple t )
             { return filterOn( getField( t ) ); }
 
@@ -214,8 +216,11 @@
                     : anyTriple
                     ;
                 }
+            @Override public Predicate<Triple> filterOnConcrete( final Node n )
+                { return x -> n.equals( x.subj ); }
             };
 
+
         public static final Field fieldObject = new Field()
             {
             @Override public Node getField( Triple t )
@@ -226,6 +231,9 @@
                     ? x -> n.sameValueAs( x.obj )
                     : anyTriple;
                 }
+
+            @Override public Predicate<Triple> filterOnConcrete( final Node n )
+                { return x -> n.sameValueAs( x.obj ); }
             };
 
         public static final Field fieldPredicate = new Field()
@@ -238,6 +246,9 @@
                     ? x -> n.equals( x.pred )
                     : anyTriple;
                 }
+
+            @Override public Predicate<Triple> filterOnConcrete( final Node n )
+                { return x -> n.equals( x.pred ); }
             };
         }
     }
diff --git a/jena-core/src/main/java/org/apache/jena/graph/impl/GraphBase.java b/jena-core/src/main/java/org/apache/jena/graph/impl/GraphBase.java
index dbbd1ec..ae21977 100644
--- a/jena-core/src/main/java/org/apache/jena/graph/impl/GraphBase.java
+++ b/jena-core/src/main/java/org/apache/jena/graph/impl/GraphBase.java
@@ -18,6 +18,7 @@
 
 package org.apache.jena.graph.impl;
 
+import org.apache.jena.atlas.iterator.Iter;
 import org.apache.jena.graph.* ;
 import org.apache.jena.shared.AddDeniedException ;
 import org.apache.jena.shared.ClosedException ;
@@ -307,9 +308,7 @@
 		ExtendedIterator<Triple> it = GraphUtil.findAll( this );
         try
             {
-            int tripleCount = 0;
-            while (it.hasNext()) { it.next(); tripleCount += 1; }
-            return tripleCount;
+            return (int) Iter.count(it);
             }
         finally
             { it.close(); }
diff --git a/jena-core/src/main/java/org/apache/jena/graph/impl/TripleStore.java b/jena-core/src/main/java/org/apache/jena/graph/impl/TripleStore.java
index e9b044d..cfda319 100644
--- a/jena-core/src/main/java/org/apache/jena/graph/impl/TripleStore.java
+++ b/jena-core/src/main/java/org/apache/jena/graph/impl/TripleStore.java
@@ -21,6 +21,8 @@
 import org.apache.jena.graph.* ;
 import org.apache.jena.util.iterator.ExtendedIterator ;
 
+import java.util.stream.Stream;
+
 /**
      TripleStore - interface for bulk storage of triples used in composed graphs.
 */
@@ -57,6 +59,11 @@
     public abstract boolean contains( Triple t );
 
     /**
+         Answer true iff this triple store contains the triple match <code>t</code>.
+     */
+    public abstract boolean containsMatch( Triple t );
+
+    /**
          Answer an setwise iterator over all the subjects of triples in this store.
     */
     public ExtendedIterator<Node> listSubjects();
@@ -78,6 +85,12 @@
     public abstract ExtendedIterator<Triple> find( Triple t );
 
     /**
+     Answer an ExtendedIterator returning all the triples from this store that
+     match the pattern <code>m = (S, P, O)</code>.
+     */
+    public abstract Stream<Triple> stream(Node sm, Node pm, Node om);
+
+    /**
         Clear this store, ie remove all triples from it.
     */
     public abstract void clear();
diff --git a/jena-core/src/main/java/org/apache/jena/mem/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/ArrayBunch.java
index 8baf555..6349cf7 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/ArrayBunch.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/ArrayBunch.java
@@ -19,6 +19,8 @@
 package org.apache.jena.mem;
 
 import java.util.ConcurrentModificationException ;
+import java.util.Spliterator;
+import java.util.function.Consumer;
 
 import org.apache.jena.graph.Triple ;
 import org.apache.jena.util.iterator.ExtendedIterator ;
@@ -65,7 +67,7 @@
         { 
         if (size == elements.length) grow();
         elements[size++] = t; 
-        changes += 1;
+        changes++;
         }
     
     /**
@@ -84,7 +86,7 @@
     @Override
     public void remove( Triple t )
         {
-        changes += 1;
+        changes++;
         for (int i = 0; i < size; i += 1)
             {
             if (t.equals( elements[i] ))
@@ -108,29 +110,84 @@
             protected final int initialChanges = changes;
             
             protected int i = size;
-            protected final Triple [] e = elements;
-            
+
             @Override public boolean hasNext()
                 { 
-                if (changes > initialChanges) throw new ConcurrentModificationException();
-                return i > 0; 
+                return 0 < i;
                 }
         
             @Override public Triple next()
                 {
-                if (changes > initialChanges) throw new ConcurrentModificationException();
+                if (changes != initialChanges) throw new ConcurrentModificationException();
                 if (i == 0) noElements( "no elements left in ArrayBunch iteration" );
-                return e[--i]; 
+                return elements[--i];
                 }
-            
+
+            @Override
+                public void forEachRemaining(Consumer<? super Triple> action)
+                {
+                while(0 < i--) action.accept(elements[i]);
+                if (changes != initialChanges) throw new ConcurrentModificationException();
+                }
+
             @Override public void remove()
                 {
-                if (changes > initialChanges) throw new ConcurrentModificationException();
+                if (changes != initialChanges) throw new ConcurrentModificationException();
                 int last = --size;
-                e[i] = e[last];
-                e[last] = null;
+                elements[i] = elements[last];
+                elements[last] = null;
                 if (size == 0) container.emptied();
                 }
             };
         }
+
+        @Override
+        public Spliterator<Triple> spliterator() {
+
+            return new Spliterator<Triple>() {
+
+                protected final int initialChanges = changes;
+
+                int i = size;
+
+                @Override
+                public boolean tryAdvance(Consumer<? super Triple> action)
+                    {
+                    if(0 < i)
+                        {
+                        action.accept(elements[--i]);
+                        if (changes != initialChanges) throw new ConcurrentModificationException();
+                        return true;
+                        }
+                    return false;
+                    }
+
+                @Override
+                public void forEachRemaining(Consumer<? super Triple> action) {
+                    while(0 < i--) action.accept(elements[i]);
+                    if (changes != initialChanges) throw new ConcurrentModificationException();
+                }
+
+                @Override
+                public Spliterator<Triple> trySplit() {
+                    /* the number of elements here should always be small, so splitting is not wise  */
+                    return null;
+                }
+
+                @Override
+                public long estimateSize() {
+                    return i;
+                }
+
+                @Override
+                public long getExactSizeIfKnown() {
+                    return i;
+                }
+
+                @Override
+                public int characteristics() {
+                    return DISTINCT | SIZED | NONNULL | IMMUTABLE;
+                }
+            };
+        }
     }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/BunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/BunchMap.java
index 4ca9058..79f9708 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/BunchMap.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/BunchMap.java
@@ -18,6 +18,8 @@
 
 package org.apache.jena.mem;
 
+import java.util.Iterator;
+import java.util.Spliterator;
 import java.util.function.Function ;
 
 import org.apache.jena.util.iterator.ExtendedIterator ;
@@ -68,4 +70,8 @@
         Answer an iterator over all the keys in this map.
     */
     public ExtendedIterator<Object> keyIterator();
+
+    public Iterator<TripleBunch> iterator();
+
+    public Spliterator<TripleBunch> spliterator();
     }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/FieldFilter.java b/jena-core/src/main/java/org/apache/jena/mem/FieldFilter.java
new file mode 100644
index 0000000..c9d2ce7
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem/FieldFilter.java
@@ -0,0 +1,69 @@
+/*
+ * 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
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jena.mem;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+
+import java.util.function.Predicate;
+
+/**
+ * A class that encapsulates a filter on fields on a triple.
+ * <p>
+ * The filter is a predicate that takes a triple and returns true if it passes
+ * the filter and false otherwise.
+ * </p>
+ */
+public class FieldFilter {
+
+    public static final FieldFilter EMPTY = new FieldFilter();
+
+    private final Predicate<Triple> filter;
+
+    private final boolean hasFilter;
+
+    private FieldFilter(Predicate<Triple> filter) {
+        this.filter = filter;
+        this.hasFilter = true;
+    }
+
+    private FieldFilter() {
+        this.filter = null;
+        this.hasFilter = false;
+    }
+
+    public boolean hasFilter() {
+        return hasFilter;
+    }
+
+    public Predicate<Triple> getFilter() {
+        return filter;
+    }
+
+    public static FieldFilter filterOn(Triple.Field f1, Node n1, Triple.Field f2, Node n2) {
+        if(n1.isConcrete()) {
+            if(n2.isConcrete()) {
+                return new FieldFilter(f1.filterOnConcrete(n1).and(f2.filterOnConcrete(n2)));
+            }
+            return new FieldFilter(f1.filterOnConcrete(n1));
+        } else if (n2.isConcrete()) {
+            return new FieldFilter(f2.filterOnConcrete(n2));
+        }
+        return FieldFilter.EMPTY;
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem/GraphMem.java b/jena-core/src/main/java/org/apache/jena/mem/GraphMem.java
index 3b4ddf2..78fc000 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/GraphMem.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/GraphMem.java
@@ -22,6 +22,8 @@
 import org.apache.jena.graph.impl.TripleStore ;
 import org.apache.jena.util.iterator.ExtendedIterator ;
 
+import java.util.stream.Stream;
+
 /** @deprecated This implementation of GraphMem will be replaced by a new implementation at Jena 4.6.0.
  *   Application should be using {@link Factory#createDefaultGraph()} for a general purpose graph or {@link Factory#createGraphMem()}
  *   to specific this style of implementation.
@@ -60,7 +62,7 @@
          Otherwise we use the default implementation.
      */
     @Override public boolean graphBaseContains( Triple t )
-    { return t.isConcrete() ? store.contains( t ) : super.graphBaseContains( t ); }
+    { return t.isConcrete() ? store.contains( t ) : store.containsMatch( t ); }
 
     /**
         Clear this GraphMem, ie remove all its triples (delegated to the store).
@@ -71,6 +73,11 @@
         getEventManager().notifyEvent(this, GraphEvents.removeAll ) ;
     }
 
+    @Override
+    public Stream<Triple> stream(Node s, Node p, Node o) {
+        return store.stream(s, p, o);
+    }
+
     /**
     Clear this GraphMem, ie remove all its triples (delegated to the store).
      */
diff --git a/jena-core/src/main/java/org/apache/jena/mem/GraphMemBase.java b/jena-core/src/main/java/org/apache/jena/mem/GraphMemBase.java
index a096d57..39ffd1d 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/GraphMemBase.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/GraphMemBase.java
@@ -61,7 +61,7 @@
     */
     public GraphMemBase openAgain()
         { 
-        count += 1; 
+        count++;
         return this;
         }
 
diff --git a/jena-core/src/main/java/org/apache/jena/mem/GraphTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/GraphTripleStore.java
deleted file mode 100644
index 5a915a6..0000000
--- a/jena-core/src/main/java/org/apache/jena/mem/GraphTripleStore.java
+++ /dev/null
@@ -1,42 +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
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.jena.mem;
-
-import org.apache.jena.graph.* ;
-import org.apache.jena.graph.Triple.* ;
-import org.apache.jena.graph.impl.TripleStore ;
-
-/**
-    GraphTripleStore - the underlying triple-indexed triple store for GraphMem et al,
-    ripped out from the heart of GraphMem as part of simplifying the reification code.
-    A GraphTripleStore is a searchable repository for triples. 
-
-*/
-
-public class GraphTripleStore extends GraphTripleStoreBase implements TripleStore
-    {   
-    public GraphTripleStore( Graph parent )
-        { 
-        super( parent,
-            new NodeToTriplesMap( Field.fieldSubject, Field.fieldPredicate, Field.fieldObject ),
-            new NodeToTriplesMap( Field.fieldPredicate, Field.fieldObject, Field.fieldSubject ),
-            new NodeToTriplesMap( Field.fieldObject, Field.fieldSubject, Field.fieldPredicate )
-            ); 
-        }
-    }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/GraphTripleStoreBase.java b/jena-core/src/main/java/org/apache/jena/mem/GraphTripleStoreBase.java
index 80b46a8..d7570a8 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/GraphTripleStoreBase.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/GraphTripleStoreBase.java
@@ -19,6 +19,7 @@
 package org.apache.jena.mem;
 
 import java.util.Iterator;
+import java.util.stream.Stream;
 
 import org.apache.jena.graph.Graph ;
 import org.apache.jena.graph.Node ;
@@ -129,10 +130,23 @@
      @Override
     public boolean contains( Triple t )
          { return subjects.containsBySameValueAs( t ); }
-     
-     public boolean containsByEquality( Triple t )
-         { return subjects.contains( t ); }
-     
+
+     @Override
+    public boolean containsMatch(Triple t)
+         {
+         Node pm = t.getPredicate();
+         Node om = t.getObject();
+         Node sm = t.getSubject();
+         if (sm.isConcrete())
+             return subjects.containsMatch( sm, pm, om );
+         else if (om.isConcrete())
+             return objects.containsMatch( om, sm, pm );
+         else if (pm.isConcrete())
+             return predicates.containsMatch( pm, om, sm );
+         else
+             return !this.isEmpty();
+         }
+
      /** 
          Answer an ExtendedIterator returning all the triples from this store that
          match the pattern <code>m = (S, P, O)</code>.
@@ -166,4 +180,40 @@
          else
              return new StoreTripleIterator( parent, subjects.iterateAll(), subjects, predicates, objects );
          }
+
+     /**
+         Answer a Stream returning all the triples from this store that
+         match the pattern <code>m = (S, P, O)</code>.
+
+         <p>Because the node-to-triples maps index on each of subject, predicate,
+         and (non-literal) object, concrete S/P/O patterns can immediately select
+         an appropriate map. Because the match for literals must be by sameValueAs,
+         not equality, the optimisation is not applied for literals. [This is probably a
+         Bad Thing for strings.]
+
+         <p>Practice suggests doing the predicate test <i>last</i>, because there are
+         "usually" many more statements than predicates, so the predicate doesn't
+         cut down the search space very much. By "practice suggests" I mean that
+         when the order went, accidentally, from S/O/P to S/P/O, performance on
+         (ANY, P, O) searches on largish models with few predicates declined
+         dramatically - specifically on the not-galen.owl ontology.
+     */
+        @Override
+    public Stream<Triple> stream(Node sm, Node pm, Node om)
+        {
+        if (null == sm) sm = Node.ANY;
+        if (null == pm) pm = Node.ANY;
+        if (null == om) om = Node.ANY;
+
+        if (sm.isConcrete())
+            return subjects.stream( sm, pm, om );
+        else if (om.isConcrete())
+            return objects.stream( om, sm, pm );
+        else if (pm.isConcrete())
+            return predicates.stream( pm, om, sm );
+        else
+            return subjects.streamAll();
+        }
     }
+
+
diff --git a/jena-core/src/main/java/org/apache/jena/mem/HashCommon.java b/jena-core/src/main/java/org/apache/jena/mem/HashCommon.java
index 2c818f0..6983c23 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/HashCommon.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/HashCommon.java
@@ -19,6 +19,7 @@
 package org.apache.jena.mem;
 
 import java.util.*;
+import java.util.function.Consumer;
 
 import org.apache.jena.shared.BrokenException ;
 import org.apache.jena.shared.JenaException ;
@@ -178,7 +179,7 @@
     public void remove( Key key )
         { primitiveRemove( key ); }
 
-    private void primitiveRemove( Key key )
+    protected void primitiveRemove( Key key )
         {
         int slot = findSlot( key );
         if (slot < 0) removeFrom( ~slot );
@@ -235,7 +236,7 @@
         about the overhead of the linear probing.
     <p>
         Iterators running over the keys may miss elements that are moved from the
-        top of the table to the bottom because of Iterator::remove. removeFrom
+        bottom of the table to the top because of Iterator::remove. removeFrom
         returns such a moved key as its result, and null otherwise.
     */
     protected Key removeFrom( int here )
@@ -258,9 +259,8 @@
                     { /* Nothing. We'd have preferred an `unless` statement. */}
                 else
                     {
-                    if (here <= original && scan > original) {
-                        wrappedAround = keys[scan];
-                    }
+                    if (here >= original && scan < original)
+                        { wrappedAround = keys[scan]; }
                     keys[here] = keys[scan];
                     moveAssociatedValues( here, scan );
                     here = scan;
@@ -318,15 +318,20 @@
 
         @Override public boolean hasNext()
             { 
-            if (changes > initialChanges) throw new ConcurrentModificationException( "changes " + changes + " > initialChanges " + initialChanges );
-            return index < movedKeys.size(); 
+            return index < movedKeys.size();
             }
 
         @Override public Key next()
             {
+            if (changes > initialChanges) throw new ConcurrentModificationException( "changes " + changes + " > initialChanges " + initialChanges );
+            if (index < movedKeys.size()) return movedKeys.get( index++ );
+            return noElements( "" );
+            }
+
+        @Override public void forEachRemaining(Consumer<? super Key> action)
+            {
+            while(index < movedKeys.size()) action.accept( movedKeys.get( index++ ) );
             if (changes > initialChanges) throw new ConcurrentModificationException();
-            if (hasNext() == false) noElements( "" );
-            return movedKeys.get( index++ );
             }
 
         @Override public void remove()
@@ -347,7 +352,7 @@
         {
         protected final List<Key> movedKeys;
 
-        int index = 0;
+        int pos = capacity-1;
         final int initialChanges;
         final NotifyEmpty container;
 
@@ -360,16 +365,30 @@
 
         @Override public boolean hasNext()
             {
-            if (changes > initialChanges) throw new ConcurrentModificationException();
-            while (index < capacity && keys[index] == null) index += 1;
-            return index < capacity;
+            while(-1 < pos)
+                {
+                if(null != keys[pos])
+                    return true;
+                pos--;
+                }
+            return false;
             }
 
         @Override public Key next()
             {
             if (changes > initialChanges) throw new ConcurrentModificationException();
-            if (hasNext() == false) noElements( "HashCommon keys" );
-            return keys[index++];
+            if (-1 < pos && null != keys[pos]) return keys[pos--];
+            throw new NoSuchElementException("HashCommon keys");
+            }
+
+        @Override public void forEachRemaining(Consumer<? super Key> action)
+            {
+            while(-1 < pos)
+                {
+                if(null != keys[pos]) action.accept(keys[pos]);
+                pos--;
+                }
+            if (changes > initialChanges) throw new ConcurrentModificationException();
             }
 
         @Override public void remove()
@@ -377,11 +396,21 @@
             if (changes > initialChanges) throw new ConcurrentModificationException();
             // System.err.println( ">> keyIterator::remove, size := " + size +
             // ", removing " + keys[index + 1] );
-            Key moved = removeFrom( index - 1 );
+            Key moved = removeFrom( pos + 1 );
             if (moved != null) movedKeys.add( moved );
             if (size == 0) container.emptied();
             if (size < 0) throw new BrokenException( "BROKEN" );
             showkeys();
             }
         }
+
+        public Spliterator<Key> keySpliterator()
+        {
+            final var initialChanges = changes;
+            final Runnable checkForConcurrentModification = () ->
+            {
+                if (changes != initialChanges) throw new ConcurrentModificationException();
+            };
+            return new SparseArraySpliterator<>(keys, size, checkForConcurrentModification);
+        }
     }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/HashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/HashedBunchMap.java
index 02fb56e..2e34b1c 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/HashedBunchMap.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/HashedBunchMap.java
@@ -18,9 +18,13 @@
 
 package org.apache.jena.mem;
 
+import java.util.*;
+import java.util.function.Consumer;
 import java.util.function.Function ;
 
 import org.apache.jena.shared.BrokenException ;
+import org.apache.jena.util.iterator.ExtendedIterator;
+import org.apache.jena.util.iterator.NiceIterator;
 
 /**
     An implementation of BunchMap that does open-addressed hashing.
@@ -28,7 +32,7 @@
 public class HashedBunchMap extends HashCommon<Object> implements BunchMap
     {
     protected TripleBunch [] values;
-    
+
     public HashedBunchMap()
         {
         super( 10 );
@@ -37,22 +41,22 @@
 
     @Override protected Object[] newKeyArray( int size )
         { return new Object[size]; }
-    
+
     /**
-        Clear this map: all entries are removed. The keys <i>and value</i> array 
+        Clear this map: all entries are removed. The keys <i>and value</i> array
         elements are set to null (so the values may be garbage-collected).
     */
     @Override
     public void clear()
         {
         size = 0;
-        for (int i = 0; i < capacity; i += 1) keys[i] = values[i] = null; 
-        }  
-    
+        for (int i = 0; i < capacity; i += 1) keys[i] = values[i] = null;
+        }
+
     @Override
     public long size()
         { return size; }
-        
+
     @Override
     public TripleBunch get( Object key )
         {
@@ -81,7 +85,7 @@
         put$(slot, key, value) ;
         return value ;
         }
-    
+
     private void put$(int slot, Object key, TripleBunch value) {
         keys[slot] = key;
         values[slot] = value;
@@ -89,7 +93,7 @@
         if ( size == threshold )
             grow();
     }
-    
+
     protected void grow()
         {
         Object [] oldContents = keys;
@@ -101,10 +105,10 @@
         for (int i = 0; i < oldCapacity; i += 1)
             {
             Object key = oldContents[i];
-            if (key != null) 
+            if (key != null)
                 {
                 int j = findSlot( key );
-                if (j < 0) 
+                if (j < 0)
                     {
                     throw new BrokenException( "oh dear, already have a slot for " + key  + ", viz " + ~j );
                     }
@@ -120,11 +124,133 @@
     */
     @Override protected void removeAssociatedValues( int here )
         { values[here] = null; }
-    
+
     /**
         Called by HashCommon when a key is moved: move the
         associated element of the <code>values</code> array.
     */
     @Override protected void moveAssociatedValues( int here, int scan )
         { values[here] = values[scan]; }
+
+    @Override public Iterator<TripleBunch> iterator()
+        {
+        final List<Object> movedKeys = new ArrayList<>();
+        ExtendedIterator<TripleBunch> basic = new BasicValueIterator( changes, movedKeys );
+        ExtendedIterator<TripleBunch> leftovers = new MovedValuesIterator( changes, movedKeys );
+        return basic.andThen( leftovers );
+        }
+
+        /**
+         The MovedKeysIterator iterates over the elements of the <code>keys</code>
+         list. It's not sufficient to just use List::iterator, because the .remove
+         method must remove elements from the hash table itself.
+         <p>
+         Note that the list supplied on construction will be empty: it is filled before
+         the first call to <code>hasNext()</code>.
+         */
+        protected final class MovedValuesIterator extends NiceIterator<TripleBunch>
+        {
+            private final List<Object> movedKeys;
+
+            protected int index = 0;
+            final int initialChanges;
+
+            protected MovedValuesIterator(int initialChanges, List<Object> movedKeys)
+            {
+                this.movedKeys = movedKeys;
+                this.initialChanges = initialChanges;
+            }
+
+            @Override public boolean hasNext()
+            {
+                return index < movedKeys.size();
+            }
+
+            @Override public TripleBunch next()
+            {
+                if (changes > initialChanges) throw new ConcurrentModificationException( "changes " + changes + " > initialChanges " + initialChanges );
+                if (index < movedKeys.size()) return get(movedKeys.get( index++ ));
+                return noElements( "" );
+            }
+
+            @Override public void forEachRemaining(Consumer<? super TripleBunch> action)
+            {
+                while(index < movedKeys.size()) action.accept( get(movedKeys.get( index++ )) );
+                if (changes > initialChanges) throw new ConcurrentModificationException();
+            }
+
+            @Override public void remove()
+            {
+                if (changes > initialChanges) throw new ConcurrentModificationException();
+                primitiveRemove( movedKeys.get( index - 1 ) );
+            }
+        }
+
+        /**
+         The BasicKeyIterator iterates over the <code>keys</code> array.
+         If a .remove call moves an unprocessed key underneath the iterator's
+         index, that key value is added to the <code>movedKeys</code>
+         list supplied to the constructor.
+         */
+        protected final class BasicValueIterator extends NiceIterator<TripleBunch>
+        {
+            protected final List<Object> movedKeys;
+
+            int pos = capacity-1;
+            final int initialChanges;
+
+            protected BasicValueIterator(int initialChanges, List<Object> movedKeys)
+            {
+                this.movedKeys = movedKeys;
+                this.initialChanges = initialChanges;
+            }
+
+            @Override public boolean hasNext()
+            {
+                while(-1 < pos)
+                {
+                    if(null != values[pos])
+                        return true;
+                    pos--;
+                }
+                return false;
+            }
+
+            @Override public TripleBunch next()
+            {
+                if (changes > initialChanges) throw new ConcurrentModificationException();
+                if (-1 < pos && null != values[pos]) return values[pos--];
+                throw new NoSuchElementException("HashCommon keys");
+            }
+
+            @Override public void forEachRemaining(Consumer<? super TripleBunch> action)
+            {
+                while(-1 < pos)
+                {
+                    if(null != values[pos]) action.accept(values[pos]);
+                    pos--;
+                }
+                if (changes > initialChanges) throw new ConcurrentModificationException();
+            }
+
+            @Override public void remove()
+            {
+                if (changes > initialChanges) throw new ConcurrentModificationException();
+                // System.err.println( ">> keyIterator::remove, size := " + size +
+                // ", removing " + keys[index + 1] );
+                Object moved = removeFrom( pos + 1 );
+                if (moved != null) movedKeys.add( moved );
+                if (size < 0) throw new BrokenException( "BROKEN" );
+            }
+        }
+
+    @Override public Spliterator<TripleBunch> spliterator() {
+        final var initialChanges = changes;
+        final Runnable checkForConcurrentModification = () ->
+        {
+            if (changes != initialChanges) throw new ConcurrentModificationException();
+        };
+
+        return new SparseArraySpliterator<>(values, size, checkForConcurrentModification);
+        }
     }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/HashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/HashedTripleBunch.java
index b534e63..5369e89 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/HashedTripleBunch.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/HashedTripleBunch.java
@@ -18,7 +18,7 @@
 
 package org.apache.jena.mem;
 
-import java.util.Iterator ;
+import java.util.Spliterator;
 
 import org.apache.jena.graph.Triple ;
 import org.apache.jena.util.iterator.ExtendedIterator ;
@@ -28,7 +28,7 @@
     public HashedTripleBunch( TripleBunch b )
         {
         super( nextSize( (int) (b.size() / loadFactor) ) );
-        for (Iterator<Triple> it = b.iterator(); it.hasNext();) add( it.next() );
+        b.spliterator().forEachRemaining(this::add);
         changes = 0;
         }
 
@@ -74,7 +74,7 @@
     public void add( Triple t )
         {
         keys[findSlot( t )] = t;
-        changes += 1;
+        changes++;
         if (++size > threshold) grow();
         }
 
@@ -94,7 +94,7 @@
     @Override public void remove( Triple t )
         {
         super.remove( t );
-        changes += 1;
+        changes++;
         }
 
     @Override
@@ -104,4 +104,7 @@
     @Override
     public ExtendedIterator<Triple> iterator( final NotifyEmpty container )
         { return keyIterator( container ); }
-}
+
+    @Override public Spliterator<Triple> spliterator()
+        { return super.keySpliterator(); }
+    }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/NodeToTriplesMap.java b/jena-core/src/main/java/org/apache/jena/mem/NodeToTriplesMap.java
deleted file mode 100644
index a41f748..0000000
--- a/jena-core/src/main/java/org/apache/jena/mem/NodeToTriplesMap.java
+++ /dev/null
@@ -1,142 +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
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.jena.mem;
-
-import static org.apache.jena.util.iterator.WrappedIterator.create;
-
-import java.util.*;
-import java.util.function.Predicate;
-
-import org.apache.jena.graph.* ;
-import org.apache.jena.graph.Triple.* ;
-import org.apache.jena.util.iterator.* ;
-
-/**
-	NodeToTriplesMap: a map from nodes to sets of triples.
-	Subclasses must override at least one of useXXXInFilter methods.
-*/
-public class NodeToTriplesMap extends NodeToTriplesMapBase 
-    {    
-    public NodeToTriplesMap( Field indexField, Field f2, Field f3 )
-        { super( indexField, f2, f3 ); }
-
-    @Override public boolean add( Triple t ) 
-        {
-        Object o = getIndexField( t );
-        
-        // Feb 2016 : no measurable difference.
-        //OpenSetBunch s = (OpenSetBunch) bunchMap.getOrSet(o, (k)->createSetBunch()) ;
-        
-        OpenSetBunch s = (OpenSetBunch) bunchMap.get( o );
-        if (s == null) bunchMap.put( o, s = createSetBunch() );
-        if (s.baseSet().add( t )) { size += 1; return true; } else return false; 
-        }
-
-    private static class OpenSetBunch extends SetBunch
-        {
-        private static final TripleBunch empty = new ArrayBunch();
-        
-        public OpenSetBunch()
-            { super( empty ); }
-        
-        public Set<Triple> baseSet()
-            { return elements; }
-        }
-    
-    private OpenSetBunch createSetBunch()
-        { return new OpenSetBunch(); }
-    
-    @Override public boolean remove( Triple t )
-        { 
-        Object o = getIndexField( t );
-        OpenSetBunch s = (OpenSetBunch) bunchMap.get( o );
-        if (s == null)
-            return false;
-        else
-            {
-            Set<Triple> base = s.baseSet();
-            boolean result = base.remove( t );
-            if (result) size -= 1;
-            if (base.isEmpty()) bunchMap.remove( o );
-            return result;
-        	} 
-        }
-    
-    @Override public ExtendedIterator<Triple> iterator( Object o, HashCommon.NotifyEmpty container )
-        {
-        TripleBunch b = bunchMap.get( o );
-        return b == null ? NullIterator.<Triple>instance() : b.iterator();
-        }
-    
-    @Override public boolean contains( Triple t )
-        { 
-        TripleBunch s = bunchMap.get( getIndexField( t ) );
-        return s == null ? false : s.contains( t );
-        }
-
-    protected static boolean equalsObjectOK( Triple t )
-        { 
-        Node o = t.getObject();
-        return o.isLiteral() ? o.getLiteralDatatype() == null : true;
-        }
-
-    @Override
-    public boolean containsBySameValueAs( Triple t )
-        { return equalsObjectOK( t ) ? contains( t ) : slowContains( t ); }
-    
-    protected boolean slowContains( Triple t )
-        { 
-        TripleBunch s = bunchMap.get( getIndexField( t ) );
-        if (s == null)
-            return false;
-        else
-            {
-            Iterator<Triple> it = s.iterator();
-            while (it.hasNext()) if (t.matches( it.next() )) return true;
-            return false;
-            }
-        }
-    
-	public ExtendedIterator<Triple> iterateAll(Triple pattern) {
-		Predicate<Triple> filter = indexField.filterOn(pattern)
-				.and(f2.filterOn(pattern)).and(f3.filterOn(pattern));
-		return create(iterateAll()).filterKeep(filter);  
-	}
-
-    @Override public ExtendedIterator<Triple> iterator( Node index, Node n2, Node n3 )
-        {
-        TripleBunch s = bunchMap.get( index.getIndexingValue() );
-        if (s == null) return NullIterator.<Triple>instance();
-        final Predicate<Triple> filter = f2.filterOn( n2 ).and( f3.filterOn( n3 ) );
-        return create(s.iterator()).filterKeep(filter);    
-        }
-
-    /**
-        Answer an iterator over all the triples that are indexed by the item <code>y</code>.
-        Note that <code>y</code> need not be a Node (because of indexing values).
-    */
-    @Override public Iterator<Triple> iteratorForIndexed( Object y )
-        { return get( y ).iterator();  }
-    
-    /** 
-        @see org.apache.jena.mem.Temp#get(java.lang.Object)
-    */
-    private TripleBunch get( Object y )
-        { return bunchMap.get( y ); }
-    }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/NodeToTriplesMapBase.java b/jena-core/src/main/java/org/apache/jena/mem/NodeToTriplesMapBase.java
index 74ec968..5861945 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/NodeToTriplesMapBase.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/NodeToTriplesMapBase.java
@@ -19,6 +19,9 @@
 package org.apache.jena.mem;
 
 import java.util.*;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 
 import org.apache.jena.graph.* ;
 import org.apache.jena.graph.Triple.Field ;
@@ -60,7 +63,7 @@
     */
     public abstract boolean remove( Triple t );
 
-    public abstract Iterator<Triple> iterator( Object o, HashCommon.NotifyEmpty container );
+    public abstract ExtendedIterator<Triple> iterator( Object o, HashCommon.NotifyEmpty container );
 
     /**
          Answer true iff this NTM contains the concrete triple <code>t</code>.
@@ -100,22 +103,22 @@
         Answer an iterator over all the triples that are indexed by the item <code>y</code>.
         Note that <code>y</code> need not be a Node (because of indexing values).
     */
-    public abstract Iterator<Triple> iteratorForIndexed( Object y );
+    public abstract ExtendedIterator<Triple> iteratorForIndexed( Object y );
     
     /**
         Answer an iterator over all the triples in this NTM.
     */
     public ExtendedIterator<Triple> iterateAll()
         {
-        final Iterator<Object> nodes = domain();
         return new NiceIterator<Triple>() 
             {
+            private final Iterator<TripleBunch> bunchIterator = bunchMap.iterator();
             private Iterator<Triple> current = NullIterator.instance();
             private NotifyMe emptier = new NotifyMe();
             
             @Override public Triple next()
                 {
-                if (hasNext() == false) noElements( "NodeToTriples iterator" );
+                if (!hasNext()) noElements( "NodeToTriples iterator" );
                 return current.next();
                 }
 
@@ -123,7 +126,7 @@
                 {
                 @Override
                 public void emptied()
-                    { nodes.remove(); }
+                    { bunchIterator.remove(); }
                 }
             
             @Override public boolean hasNext()
@@ -131,14 +134,56 @@
                 while (true)
                     {
                     if (current.hasNext()) return true;
-                    if (nodes.hasNext() == false) return false;
-                    Object next = nodes.next();
-                    current = NodeToTriplesMapBase.this.iterator( next, emptier );
+                    if (!bunchIterator.hasNext()) return false;
+                    current = bunchIterator.next().iterator( emptier );
                     }
                 }
 
-            @Override public void remove()
+            @Override public void forEachRemaining(Consumer<? super Triple> action)
+                {
+                if (current != null) current.forEachRemaining(action);
+                bunchIterator.forEachRemaining(next ->
+                    {
+                    current = next.iterator();
+                    current.forEachRemaining(action);
+                    });
+                }
+
+                @Override public void remove()
                 { current.remove(); }
             };
         }
+
+
+        public Stream<Triple> streamAll()
+            {
+            return StreamSupport.stream(bunchMap.spliterator(), false)
+                    .flatMap(bunch -> StreamSupport.stream(bunch.spliterator(), false));
+            }
+
+        public Stream<Triple> stream( Node index, Node n2, Node n3 )
+            {
+            Object indexValue = index.getIndexingValue();
+            TripleBunch s = bunchMap.get( indexValue );
+            if (s == null) return Stream.empty();
+            var filter = FieldFilter.filterOn(f2, n2, f3, n3);
+            return filter.hasFilter()
+                    ? StreamSupport.stream(s.spliterator(), false).filter(filter.getFilter())
+                    : StreamSupport.stream(s.spliterator(), false);
+            }
+
+        public boolean containsMatch( Node index, Node n2, Node n3 )
+            {
+            TripleBunch s = bunchMap.get( index.getIndexingValue() );
+            if (s == null)
+                return false;
+            var filter = FieldFilter.filterOn(f2, n2, f3, n3);
+            if (!filter.hasFilter())
+                return true;
+            var spliterator = s.spliterator();
+            final boolean[] found = {false};
+            Consumer<Triple> tester = triple -> found[0] = filter.getFilter().test(triple);
+            while (!found[0] && spliterator.tryAdvance(tester));
+            return found[0];
+            }
     }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/NodeToTriplesMapMem.java b/jena-core/src/main/java/org/apache/jena/mem/NodeToTriplesMapMem.java
index f95bab6..2385ae2 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/NodeToTriplesMapMem.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/NodeToTriplesMapMem.java
@@ -18,9 +18,6 @@
 
 package org.apache.jena.mem;
 
-import static org.apache.jena.util.iterator.WrappedIterator.create;
-
-import java.util.Iterator ;
 import java.util.function.Predicate;
 
 import org.apache.jena.graph.Node ;
@@ -84,11 +81,18 @@
         Answer an iterator over all the triples in this NTM which have index node
         <code>o</code>.
     */
-    @Override public Iterator<Triple> iterator( Object o, HashCommon.NotifyEmpty container ) 
+    @Override public ExtendedIterator<Triple> iterator( Object o, HashCommon.NotifyEmpty container )
        {
        TripleBunch s = bunchMap.get( o );
        return s == null ? NullIterator.<Triple>instance() : s.iterator( container );
        }
+
+    public ExtendedIterator<Triple> iterateAll(Triple pattern)
+        {
+        Predicate<Triple> filter = indexField.filterOn(pattern)
+                .and(f2.filterOn(pattern)).and(f3.filterOn(pattern));
+        return iterateAll().filterKeep(filter);
+        }
     
     public class NotifyMe implements HashCommon.NotifyEmpty
         {
@@ -129,17 +133,19 @@
        TripleBunch s = bunchMap.get( indexValue );
 //       System.err.println( ">> ntmf::iterator: " + (s == null ? (Object) "None" : s.getClass()) );
        if (s == null) return NullIterator.<Triple>instance();
-       final Predicate<Triple> filter = f2.filterOn( n2 ).and( f3.filterOn( n3 ) );
-       return create(s.iterator( new NotifyMe( indexValue ))).filterKeep(filter);
-       }    
+           var filter = FieldFilter.filterOn(f2, n2, f3, n3);
+           return filter.hasFilter()
+               ? s.iterator( new NotifyMe( indexValue ) ).filterKeep( filter.getFilter() )
+               : s.iterator( new NotifyMe( indexValue ) );
+       }
 
-    protected TripleBunch get( Object index )
+        protected TripleBunch get( Object index )
         { return bunchMap.get( index ); }
     
     /**
      Answer an iterator over all the triples that are indexed by the item <code>y</code>.
         Note that <code>y</code> need not be a Node (because of indexing values).
     */
-    @Override public Iterator<Triple> iteratorForIndexed( Object y )
+    @Override public ExtendedIterator<Triple> iteratorForIndexed( Object y )
         { return get( y ).iterator();  }
     }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/ObjectIterator.java b/jena-core/src/main/java/org/apache/jena/mem/ObjectIterator.java
index f495cb6..1ba1a55 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/ObjectIterator.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/ObjectIterator.java
@@ -19,6 +19,7 @@
 package org.apache.jena.mem;
 
 import java.util.*;
+import java.util.function.Consumer;
 
 import org.apache.jena.graph.* ;
 import org.apache.jena.util.CollectionFactory ;
@@ -57,7 +58,25 @@
             ( "ObjectIterator.next()" );
         return pending.remove( pending.size() - 1 );
         }
-    
+
+    @Override public void forEachRemaining(Consumer<? super Node> action)
+        {
+            pending.forEach(action);
+            domain.forEachRemaining(y ->
+                {
+                if (y instanceof Node)
+                    action.accept( (Node) y );
+                else
+                    {
+                    iteratorFor( y ).forEachRemaining(triple ->
+                        {
+                        if (seen.add( triple.getObject() )) action.accept( triple.getObject() );
+                        });
+                    }
+                }
+            );
+        }
+
     protected void refillPending()
         {
         Object y = domain.next();
@@ -65,12 +84,9 @@
             pending.add( (Node) y );
         else
             {
-            Iterator<Triple> z = iteratorFor( y );
-            while (z.hasNext())
-                {
-                Node object = z.next().getObject();
-                if (seen.add( object )) pending.add( object );
-                }
+            iteratorFor( y ).forEachRemaining(triple -> {
+                if (seen.add( triple.getObject() )) pending.add( triple.getObject() );
+            });
             }
         }
     
diff --git a/jena-core/src/main/java/org/apache/jena/mem/SetBunch.java b/jena-core/src/main/java/org/apache/jena/mem/SetBunch.java
deleted file mode 100644
index 28dac82..0000000
--- a/jena-core/src/main/java/org/apache/jena/mem/SetBunch.java
+++ /dev/null
@@ -1,88 +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
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.jena.mem;
-
-import java.util.HashSet ;
-import java.util.Iterator ;
-import java.util.Set ;
-
-import org.apache.jena.graph.Node ;
-import org.apache.jena.graph.Triple ;
-import org.apache.jena.util.iterator.ExtendedIterator ;
-import org.apache.jena.util.iterator.WrappedIterator ;
-
-public class SetBunch implements TripleBunch
-    {
-    protected Set<Triple> elements = new HashSet<>(20);
-    
-    public SetBunch( TripleBunch b )
-        { 
-        for (Iterator<Triple> it = b.iterator(); it.hasNext();) 
-            elements.add( it.next() );
-        }
-
-    protected static boolean equalsObjectOK( Triple t )
-        { 
-        Node o = t.getObject();
-        return o.isLiteral() ? o.getLiteralDatatype() == null : true;
-        }
-
-    @Override
-    public boolean contains( Triple t )
-        { return elements.contains( t ); }
-    
-    @Override
-    public boolean containsBySameValueAs( Triple t )
-        { return equalsObjectOK( t ) ? elements.contains( t ) : slowContains( t ); }
-    
-    protected boolean slowContains( Triple t )
-        {
-            for ( Triple element : elements )
-            {
-                if ( t.matches( element ) )
-                {
-                    return true;
-                }
-            }
-        return false;
-        }
-
-    @Override
-    public int size()
-        { return elements.size(); }
-    
-    @Override
-    public void add( Triple t )
-        { elements.add( t ); }
-    
-    @Override
-    public void remove( Triple t )
-        { elements.remove( t ); }
-    
-    @Override
-    public ExtendedIterator<Triple> iterator( HashCommon.NotifyEmpty container )
-        {
-        return iterator();
-        }
-    
-    @Override
-    public ExtendedIterator<Triple> iterator()
-        { return WrappedIterator.create( elements.iterator() ); }        
-    
-    }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/SparseArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/SparseArraySpliterator.java
new file mode 100644
index 0000000..8c8fb98
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem/SparseArraySpliterator.java
@@ -0,0 +1,203 @@
+/*
+ * 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
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.mem;
+
+import java.util.Spliterator;
+import java.util.function.Consumer;
+
+/**
+ * A spliterator for sparse arrays. This spliterator will iterate over the array
+ * skipping null entries.
+ *
+ * This spliterator supports splitting into sub-spliterators.
+ *
+ * The spliterator will check for concurrent modifications by invoking a {@link Runnable}
+ * before each action.
+ *
+ * @param <E> the type of the array elements
+ */
+public class SparseArraySpliterator<E> implements Spliterator<E> {
+
+    private final E[] entries;
+    private int pos;
+    private final float fillRatio;
+    private final Runnable checkForConcurrentModification;
+
+    /**
+     * Create a spliterator for the given array, with the given size.
+     * @param entries the array
+     * @param estimatedElementsCount the estimated size
+     */
+    public SparseArraySpliterator(final E[] entries, final int estimatedElementsCount, final Runnable checkForConcurrentModification) {
+        this.entries = entries;
+        this.pos = entries.length;
+        this.fillRatio = (float) estimatedElementsCount / (float)entries.length;
+        this.checkForConcurrentModification = checkForConcurrentModification;
+    }
+
+
+    /**
+     * If a remaining element exists, performs the given action on it,
+     * returning {@code true}; else returns {@code false}.  If this
+     * Spliterator is {@link #ORDERED} the action is performed on the
+     * next element in encounter order.  Exceptions thrown by the
+     * action are relayed to the caller.
+     *
+     * @param action The action
+     * @return {@code false} if no remaining elements existed
+     * upon entry to this method, else {@code true}.
+     * @throws NullPointerException if the specified action is null
+     */
+    @Override
+    public boolean tryAdvance(Consumer<? super E> action) {
+        this.checkForConcurrentModification.run();
+        while(-1 < --pos) {
+            if(null != entries[pos]) {
+                action.accept(entries[pos]);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Performs the given action for each remaining element, sequentially in
+     * the current thread, until all elements have been processed or the action
+     * throws an exception.  If this Spliterator is {@link #ORDERED}, actions
+     * are performed in encounter order.  Exceptions thrown by the action
+     * are relayed to the caller.
+     *
+     * @param action The action
+     * @throws NullPointerException if the specified action is null
+     * @implSpec The default implementation repeatedly invokes {@link #tryAdvance} until
+     * it returns {@code false}.  It should be overridden whenever possible.
+     */
+    @Override
+    public void forEachRemaining(Consumer<? super E> action) {
+        pos--;
+        while (-1 < pos) {
+            if(null != entries[pos]) {
+                action.accept(entries[pos]);
+            }
+            pos--;
+        }
+        this.checkForConcurrentModification.run();
+    }
+
+    /**
+     * If this spliterator can be partitioned, returns a Spliterator
+     * covering elements, that will, upon return from this method, not
+     * be covered by this Spliterator.
+     *
+     * <p>If this Spliterator is {@link #ORDERED}, the returned Spliterator
+     * must cover a strict prefix of the elements.
+     *
+     * <p>Unless this Spliterator covers an infinite number of elements,
+     * repeated calls to {@code trySplit()} must eventually return {@code null}.
+     * Upon non-null return:
+     * <ul>
+     * <li>the value reported for {@code estimateSize()} before splitting,
+     * must, after splitting, be greater than or equal to {@code estimateSize()}
+     * for this and the returned Spliterator; and</li>
+     * <li>if this Spliterator is {@code SUBSIZED}, then {@code estimateSize()}
+     * for this spliterator before splitting must be equal to the sum of
+     * {@code estimateSize()} for this and the returned Spliterator after
+     * splitting.</li>
+     * </ul>
+     *
+     * <p>This method may return {@code null} for any reason,
+     * including emptiness, inability to split after traversal has
+     * commenced, data structure constraints, and efficiency
+     * considerations.
+     *
+     * @return a {@code Spliterator} covering some portion of the
+     * elements, or {@code null} if this spliterator cannot be split
+     * @apiNote An ideal {@code trySplit} method efficiently (without
+     * traversal) divides its elements exactly in half, allowing
+     * balanced parallel computation.  Many departures from this ideal
+     * remain highly effective; for example, only approximately
+     * splitting an approximately balanced tree, or for a tree in
+     * which leaf nodes may contain either one or two elements,
+     * failing to further split these nodes.  However, large
+     * deviations in balance and/or overly inefficient {@code
+     * trySplit} mechanics typically result in poor parallel
+     * performance.
+     */
+    @Override
+    public Spliterator<E> trySplit() {
+        if (pos < 2) {
+            return null;
+        }
+        if (this.estimateSize() < 2L) {
+            return null;
+        }
+        final int toIndexOfSubIterator = this.pos;
+        this.pos = pos >>> 1;
+        return new SparseArraySubSpliterator<E>(entries, this.pos, toIndexOfSubIterator, fillRatio, checkForConcurrentModification);
+    }
+
+    /**
+     * Returns an estimate of the number of elements that would be
+     * encountered by a {@link #forEachRemaining} traversal, or returns {@link
+     * Long#MAX_VALUE} if infinite, unknown, or too expensive to compute.
+     *
+     * <p>If this Spliterator is {@link #SIZED} and has not yet been partially
+     * traversed or split, or this Spliterator is {@link #SUBSIZED} and has
+     * not yet been partially traversed, this estimate must be an accurate
+     * count of elements that would be encountered by a complete traversal.
+     * Otherwise, this estimate may be arbitrarily inaccurate, but must decrease
+     * as specified across invocations of {@link #trySplit}.
+     *
+     * @return the estimated size, or {@code Long.MAX_VALUE} if infinite,
+     * unknown, or too expensive to compute.
+     * @apiNote Even an inexact estimate is often useful and inexpensive to compute.
+     * For example, a sub-spliterator of an approximately balanced binary tree
+     * may return a value that estimates the number of elements to be half of
+     * that of its parent; if the root Spliterator does not maintain an
+     * accurate count, it could estimate size to be the power of two
+     * corresponding to its maximum depth.
+     */
+    @Override
+    public long estimateSize() { return ((long) (this.fillRatio  * pos)) + 1L; }
+
+    /**
+     * Returns a set of characteristics of this Spliterator and its
+     * elements. The result is represented as ORed values from {@link
+     * #ORDERED}, {@link #DISTINCT}, {@link #SORTED}, {@link #SIZED},
+     * {@link #NONNULL}, {@link #IMMUTABLE}, {@link #CONCURRENT},
+     * {@link #SUBSIZED}.  Repeated calls to {@code characteristics()} on
+     * a given spliterator, prior to or in-between calls to {@code trySplit},
+     * should always return the same result.
+     *
+     * <p>If a Spliterator reports an inconsistent set of
+     * characteristics (either those returned from a single invocation
+     * or across multiple invocations), no guarantees can be made
+     * about any computation using this Spliterator.
+     *
+     * @return a representation of characteristics
+     * @apiNote The characteristics of a given spliterator before splitting
+     * may differ from the characteristics after splitting.  For specific
+     * examples see the characteristic values {@link #SIZED}, {@link #SUBSIZED}
+     * and {@link #CONCURRENT}.
+     */
+    @Override
+    public int characteristics() {
+        return DISTINCT | NONNULL | IMMUTABLE;
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem/SparseArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/SparseArraySubSpliterator.java
new file mode 100644
index 0000000..f17e5d1
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem/SparseArraySubSpliterator.java
@@ -0,0 +1,220 @@
+/*
+ * 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
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.mem;
+
+import java.util.Spliterator;
+import java.util.function.Consumer;
+
+/**
+ * A spliterator for sparse arrays. This spliterator will iterate over the array
+ * skipping null entries.
+ *
+ * This spliterator supports splitting into sub-spliterators.
+ *
+ * The spliterator will check for concurrent modifications by invoking a {@link Runnable}
+ * before each action.
+ *
+ * @param <E>
+ */
+public class SparseArraySubSpliterator<E> implements Spliterator<E> {
+
+    private final E[] entries;
+    private final int fromIndex;
+    private int pos;
+    private final float fillRatio;
+    private final Runnable checkForConcurrentModification;
+
+    /**
+     * Create a spliterator for the given array, with the given size.
+     *
+     * @param entries                        the array
+     * @param fromIndex                      the index of the first element, inclusive
+     * @param toIndex                        the index of the last element, exclusive
+     * @param fillRatio                      the ratio of elements containing null
+     * @param checkForConcurrentModification
+     */
+    public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final float fillRatio, final Runnable checkForConcurrentModification) {
+        this.entries = entries;
+        this.fromIndex = fromIndex;
+        this.pos = toIndex;
+        this.fillRatio = fillRatio;
+        this.checkForConcurrentModification = checkForConcurrentModification;
+    }
+
+    /**
+     * Create a spliterator for the given array, with the given size.
+     * @param entries   the array
+     * @param estimatedElementsCount the estimated size
+     */
+    public SparseArraySubSpliterator(final E[] entries, final int estimatedElementsCount, final Runnable checkForConcurrentModification) {
+       this(entries, 0, entries.length, ((float)estimatedElementsCount / (float)entries.length), checkForConcurrentModification);
+    }
+
+
+    /**
+     * If a remaining element exists, performs the given action on it,
+     * returning {@code true}; else returns {@code false}.  If this
+     * Spliterator is {@link #ORDERED} the action is performed on the
+     * next element in encounter order.  Exceptions thrown by the
+     * action are relayed to the caller.
+     *
+     * @param action The action
+     * @return {@code false} if no remaining elements existed
+     * upon entry to this method, else {@code true}.
+     * @throws NullPointerException if the specified action is null
+     */
+    @Override
+    public boolean tryAdvance(Consumer<? super E> action) {
+        this.checkForConcurrentModification.run();
+        while(fromIndex <= --pos) {
+            if(null != entries[pos]) {
+                action.accept(entries[pos]);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Performs the given action for each remaining element, sequentially in
+     * the current thread, until all elements have been processed or the action
+     * throws an exception.  If this Spliterator is {@link #ORDERED}, actions
+     * are performed in encounter order.  Exceptions thrown by the action
+     * are relayed to the caller.
+     *
+     * @param action The action
+     * @throws NullPointerException if the specified action is null
+     * @implSpec The default implementation repeatedly invokes {@link #tryAdvance} until
+     * it returns {@code false}.  It should be overridden whenever possible.
+     */
+    @Override
+    public void forEachRemaining(Consumer<? super E> action) {
+        pos--;
+        while (fromIndex <= pos) {
+            if(null != entries[pos]) {
+                action.accept(entries[pos]);
+            }
+            pos--;
+        }
+        this.checkForConcurrentModification.run();
+    }
+
+    /**
+     * If this spliterator can be partitioned, returns a Spliterator
+     * covering elements, that will, upon return from this method, not
+     * be covered by this Spliterator.
+     *
+     * <p>If this Spliterator is {@link #ORDERED}, the returned Spliterator
+     * must cover a strict prefix of the elements.
+     *
+     * <p>Unless this Spliterator covers an infinite number of elements,
+     * repeated calls to {@code trySplit()} must eventually return {@code null}.
+     * Upon non-null return:
+     * <ul>
+     * <li>the value reported for {@code estimateSize()} before splitting,
+     * must, after splitting, be greater than or equal to {@code estimateSize()}
+     * for this and the returned Spliterator; and</li>
+     * <li>if this Spliterator is {@code SUBSIZED}, then {@code estimateSize()}
+     * for this spliterator before splitting must be equal to the sum of
+     * {@code estimateSize()} for this and the returned Spliterator after
+     * splitting.</li>
+     * </ul>
+     *
+     * <p>This method may return {@code null} for any reason,
+     * including emptiness, inability to split after traversal has
+     * commenced, data structure constraints, and efficiency
+     * considerations.
+     *
+     * @return a {@code Spliterator} covering some portion of the
+     * elements, or {@code null} if this spliterator cannot be split
+     * @apiNote An ideal {@code trySplit} method efficiently (without
+     * traversal) divides its elements exactly in half, allowing
+     * balanced parallel computation.  Many departures from this ideal
+     * remain highly effective; for example, only approximately
+     * splitting an approximately balanced tree, or for a tree in
+     * which leaf nodes may contain either one or two elements,
+     * failing to further split these nodes.  However, large
+     * deviations in balance and/or overly inefficient {@code
+     * trySplit} mechanics typically result in poor parallel
+     * performance.
+     */
+    @Override
+    public Spliterator<E> trySplit() {
+        final int entriesCount = pos - fromIndex;
+        if (entriesCount < 2) {
+            return null;
+        }
+        if (this.estimateSize() < 2L) {
+            return null;
+        }
+        final int toIndexOfSubIterator = this.pos;
+        this.pos = fromIndex + (entriesCount >>> 1);
+        return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, fillRatio, checkForConcurrentModification);
+    }
+
+    /**
+     * Returns an estimate of the number of elements that would be
+     * encountered by a {@link #forEachRemaining} traversal, or returns {@link
+     * Long#MAX_VALUE} if infinite, unknown, or too expensive to compute.
+     *
+     * <p>If this Spliterator is {@link #SIZED} and has not yet been partially
+     * traversed or split, or this Spliterator is {@link #SUBSIZED} and has
+     * not yet been partially traversed, this estimate must be an accurate
+     * count of elements that would be encountered by a complete traversal.
+     * Otherwise, this estimate may be arbitrarily inaccurate, but must decrease
+     * as specified across invocations of {@link #trySplit}.
+     *
+     * @return the estimated size, or {@code Long.MAX_VALUE} if infinite,
+     * unknown, or too expensive to compute.
+     * @apiNote Even an inexact estimate is often useful and inexpensive to compute.
+     * For example, a sub-spliterator of an approximately balanced binary tree
+     * may return a value that estimates the number of elements to be half of
+     * that of its parent; if the root Spliterator does not maintain an
+     * accurate count, it could estimate size to be the power of two
+     * corresponding to its maximum depth.
+     */
+    @Override
+    public long estimateSize() { return ((long) (fillRatio * (pos - fromIndex))) + 1L; }
+
+
+    /**
+     * Returns a set of characteristics of this Spliterator and its
+     * elements. The result is represented as ORed values from {@link
+     * #ORDERED}, {@link #DISTINCT}, {@link #SORTED}, {@link #SIZED},
+     * {@link #NONNULL}, {@link #IMMUTABLE}, {@link #CONCURRENT},
+     * {@link #SUBSIZED}.  Repeated calls to {@code characteristics()} on
+     * a given spliterator, prior to or in-between calls to {@code trySplit},
+     * should always return the same result.
+     *
+     * <p>If a Spliterator reports an inconsistent set of
+     * characteristics (either those returned from a single invocation
+     * or across multiple invocations), no guarantees can be made
+     * about any computation using this Spliterator.
+     *
+     * @return a representation of characteristics
+     * @apiNote The characteristics of a given spliterator before splitting
+     * may differ from the characteristics after splitting.  For specific
+     * examples see the characteristic values {@link #SIZED}, {@link #SUBSIZED}
+     * and {@link #CONCURRENT}.
+     */
+    @Override
+    public int characteristics() {
+        return DISTINCT | NONNULL | IMMUTABLE;
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem/TrackingTripleIterator.java b/jena-core/src/main/java/org/apache/jena/mem/TrackingTripleIterator.java
index bf0c844..4972be5 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/TrackingTripleIterator.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/TrackingTripleIterator.java
@@ -19,6 +19,7 @@
 package org.apache.jena.mem;
 
 import java.util.Iterator;
+import java.util.function.Consumer;
 
 import org.apache.jena.graph.* ;
 import org.apache.jena.util.iterator.WrappedIterator ;
@@ -27,6 +28,8 @@
     A WrappedIterator which remembers the last object next'ed in a
     protected instance variable, so that subclasses have access to it 
     during .remove.
+    After a call to {@link TrackingTripleIterator#forEachRemaining} current is null. So calling #remove after
+    #forEachRemaining is not supported.
 */
 public class TrackingTripleIterator extends WrappedIterator<Triple>
     {
@@ -44,5 +47,14 @@
     */
     @Override
     public Triple next()
-        { return current = super.next(); }       
+        { return current = super.next(); }
+
+    @Override
+        public void forEachRemaining(Consumer<? super Triple> action)
+        {
+            /** The behavior of {@link java.util.Iterator#remove}is undefined after a call to forEachRemaining.
+               So it should be okay, to not waste performance here. */
+            this.current = null;
+            super.forEachRemaining(action);
+        }
     }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/TripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/TripleBunch.java
index c1e2ba2..2dfb353 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/TripleBunch.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/TripleBunch.java
@@ -21,6 +21,8 @@
 import org.apache.jena.graph.Triple ;
 import org.apache.jena.util.iterator.ExtendedIterator ;
 
+import java.util.Spliterator;
+
 /**
     A bunch of triples - a stripped-down set with specialized methods. A
     bunch is expected to store triples that share some useful property 
@@ -72,5 +74,10 @@
         <code>container</code> is invoked.
     */
     public abstract ExtendedIterator<Triple> iterator( HashCommon.NotifyEmpty container );
+
+    /**
+        Answer a spliterator over all the triples in this bunch.
+    */
+    public abstract Spliterator<Triple> spliterator();
     
     }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/WrappedHashMap.java b/jena-core/src/main/java/org/apache/jena/mem/WrappedHashMap.java
deleted file mode 100644
index b5c929e..0000000
--- a/jena-core/src/main/java/org/apache/jena/mem/WrappedHashMap.java
+++ /dev/null
@@ -1,64 +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
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.jena.mem;
-
-import java.util.Map;
-import java.util.function.Function ;
-
-import org.apache.jena.util.CollectionFactory ;
-import org.apache.jena.util.iterator.* ;
-
-/**
-    An implementation of BunchMap that delegates to a [Hashed]Map.
-*/
-public class WrappedHashMap implements BunchMap
-    {
-    protected final Map<Object, TripleBunch> map = CollectionFactory.createHashedMap();
-    
-    public WrappedHashMap() {}
-    
-    @Override
-    public void clear()
-        { map.clear(); }
-    
-    @Override
-    public long size()
-        { return map.size(); }
-
-    @Override
-    public TripleBunch get( Object key )
-        { return map.get( key ); }
-
-    @Override
-    public void put( Object key, TripleBunch value )
-        { map.put( key, value ); }
-
-    @Override
-    public TripleBunch getOrSet( Object key, Function<Object, TripleBunch> setter) {
-        return map.computeIfAbsent(key, setter);
-    }
-
-    @Override
-    public void remove( Object key )
-        { map.remove( key ); }
-
-    @Override
-    public ExtendedIterator<Object> keyIterator()
-        { return WrappedIterator.create( map.keySet().iterator() ); }
-    }
diff --git a/jena-core/src/main/java/org/apache/jena/util/IteratorCollection.java b/jena-core/src/main/java/org/apache/jena/util/IteratorCollection.java
index d049225..a6dd8ee 100644
--- a/jena-core/src/main/java/org/apache/jena/util/IteratorCollection.java
+++ b/jena-core/src/main/java/org/apache/jena/util/IteratorCollection.java
@@ -45,7 +45,7 @@
     public static <T> Set<T> iteratorToSet( Iterator<? extends T> i )
         {
         Set<T> result = CollectionFactory.createHashedSet();
-        try { while (i.hasNext()) result.add( i.next() ); }
+        try { i.forEachRemaining(result::add); }
         finally { NiceIterator.close( i ); }
         return result;
         }
@@ -54,13 +54,13 @@
         Answer the elements of the given iterator as a list, in the order that they
         arrived from the iterator. The iterator is consumed by this operation:
         even if an exception is thrown, the iterator will be closed.
-    	@param it the iterator to convert
-    	@return a list of the elements of <code>it</code>, in order
+     @param it the iterator to convert
+     @return a list of the elements of <code>it</code>, in order
      */
     public static <T> List<T> iteratorToList( Iterator<? extends T> it )
         {
         List<T> result = new ArrayList<>();
-        try { while (it.hasNext()) result.add( it.next() ); }
+        try { it.forEachRemaining(result::add); }
         finally { NiceIterator.close( it ); }
         return result;
         }
diff --git a/jena-core/src/main/java/org/apache/jena/util/iterator/FilterIterator.java b/jena-core/src/main/java/org/apache/jena/util/iterator/FilterIterator.java
index c5ddb83..26a0a99 100644
--- a/jena-core/src/main/java/org/apache/jena/util/iterator/FilterIterator.java
+++ b/jena-core/src/main/java/org/apache/jena/util/iterator/FilterIterator.java
@@ -20,6 +20,7 @@
 
 import java.util.Iterator;
 import java.util.NoSuchElementException;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 
 /** 
@@ -85,4 +86,18 @@
             }
 		throw new NoSuchElementException();
         }
+
+    @Override
+        public void forEachRemaining(Consumer<? super T> action)
+        {
+        if (hasCurrent) {
+            action.accept(current);
+            hasCurrent = false;
+        }
+        super.forEachRemaining(e -> {
+            if (f.test(e)) {
+                action.accept(e);
+            }
+        });
+        }
     }
diff --git a/jena-core/src/main/java/org/apache/jena/util/iterator/LazyIterator.java b/jena-core/src/main/java/org/apache/jena/util/iterator/LazyIterator.java
index 7ed6fd3..fb154bd 100644
--- a/jena-core/src/main/java/org/apache/jena/util/iterator/LazyIterator.java
+++ b/jena-core/src/main/java/org/apache/jena/util/iterator/LazyIterator.java
@@ -18,6 +18,8 @@
 
 package org.apache.jena.util.iterator;
 
+import java.util.function.Consumer;
+
 /** An ExtendedIterator that is created lazily.
  * This is useful when constructing an iterator is expensive and 
  * you'd prefer to delay doing it until certain it's actually needed.
@@ -53,6 +55,12 @@
 	}
 
 	@Override
+	public void forEachRemaining(Consumer<? super T> action) {
+		lazy();
+		it.forEachRemaining(action);
+	}
+
+	@Override
     public void remove() {
 		lazy();
 		it.remove();
diff --git a/jena-core/src/main/java/org/apache/jena/util/iterator/Map1Iterator.java b/jena-core/src/main/java/org/apache/jena/util/iterator/Map1Iterator.java
index 9df725b..4f4ba7c 100644
--- a/jena-core/src/main/java/org/apache/jena/util/iterator/Map1Iterator.java
+++ b/jena-core/src/main/java/org/apache/jena/util/iterator/Map1Iterator.java
@@ -19,6 +19,7 @@
 package org.apache.jena.util.iterator;
 
 import java.util.Iterator;
+import java.util.function.Consumer;
 import java.util.function.Function;
 
 /**
@@ -48,6 +49,11 @@
 	public @Override boolean hasNext()
 	    { return base.hasNext(); }
 
+
+	public @Override void forEachRemaining(Consumer<? super To> action)
+		{ this.base.forEachRemaining(
+				x -> action.accept( map.apply( x ) ) ); }
+
 	public @Override void remove()
 	    { base.remove(); }
 
diff --git a/jena-core/src/main/java/org/apache/jena/util/iterator/MapFilterIterator.java b/jena-core/src/main/java/org/apache/jena/util/iterator/MapFilterIterator.java
index ec49ad7..3668622 100644
--- a/jena-core/src/main/java/org/apache/jena/util/iterator/MapFilterIterator.java
+++ b/jena-core/src/main/java/org/apache/jena/util/iterator/MapFilterIterator.java
@@ -19,6 +19,7 @@
 package org.apache.jena.util.iterator;
 
 import java.util.*;
+import java.util.function.Consumer;
 
 /**
     A MapFilterIterator takes a MapFilter and an [Extended]Iterator and returns a new 
@@ -91,4 +92,20 @@
         }
         throw new NoSuchElementException();
     }
+
+    @Override
+    synchronized public void forEachRemaining(Consumer<? super X> action) {
+        if(dead)
+            return;
+        if(current != null) {
+            action.accept(current);
+            current = null;
+        }
+        underlying.forEachRemaining( x -> {
+            X y = f.accept(x);
+            if(y != null)
+                action.accept(y);
+        });
+        dead = true;
+    }
 }
diff --git a/jena-core/src/main/java/org/apache/jena/util/iterator/NiceIterator.java b/jena-core/src/main/java/org/apache/jena/util/iterator/NiceIterator.java
index cf86331..1696791 100644
--- a/jena-core/src/main/java/org/apache/jena/util/iterator/NiceIterator.java
+++ b/jena-core/src/main/java/org/apache/jena/util/iterator/NiceIterator.java
@@ -19,6 +19,7 @@
 package org.apache.jena.util.iterator;
 
 import java.util.*;
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
@@ -102,10 +103,18 @@
             private Iterator<? extends T> current = a;
             private Iterator<? extends T> removeFrom = null;
 
+            boolean hasNext = false;
+
             @Override public boolean hasNext()
                 {
-                while (current.hasNext() == false && index < pending.size()) current = advance();
-                return current.hasNext();
+                if (hasNext) return true;
+                if (current.hasNext()) return hasNext = true;
+                while (index < pending.size())
+                    {
+                    current = advance();
+                    if(current.hasNext()) return hasNext = true;
+                    }
+                return false;
                 }
 
             private Iterator< ? extends T> advance()
@@ -120,9 +129,20 @@
                 {
                 if (!hasNext()) noElements( "concatenation" );
                 removeFrom = current;
+                hasNext = false;
                 return current.next();
                 }
 
+            @Override public void forEachRemaining(Consumer<? super T> action)
+                {
+                current.forEachRemaining(action);
+                while(index < pending.size())
+                    {
+                    current = advance();
+                    current.forEachRemaining(action);
+                    }
+                }
+
             @Override public void close()
                 {
                 close( current );
@@ -206,7 +226,7 @@
     public static <T> Set<T> asSet( ExtendedIterator<T> it )
         {
         Set<T> result = new HashSet<>();
-        while (it.hasNext()) result.add( it.next() );
+        it.forEachRemaining(result::add);
         return result;
         }
 
@@ -217,7 +237,7 @@
     public static <T> List<T> asList( ExtendedIterator<T> it )
         {
         List<T> result = new ArrayList<>();
-        while (it.hasNext()) result.add( it.next() );
+        it.forEachRemaining(result::add);
         return result;
         }
     }
diff --git a/jena-core/src/main/java/org/apache/jena/util/iterator/SingletonIterator.java b/jena-core/src/main/java/org/apache/jena/util/iterator/SingletonIterator.java
index 1d36a66..59c0f64 100644
--- a/jena-core/src/main/java/org/apache/jena/util/iterator/SingletonIterator.java
+++ b/jena-core/src/main/java/org/apache/jena/util/iterator/SingletonIterator.java
@@ -18,6 +18,8 @@
 
 package org.apache.jena.util.iterator;
 
+import java.util.function.Consumer;
+
 /**
  * A ClosableIterator that contains only one element
  */
@@ -58,4 +60,11 @@
         }
     }
 
+    @Override
+    public void forEachRemaining(Consumer<? super T> action) {
+        if(!delivered) {
+            action.accept(item);
+            delivered = true;
+        }
+    }
 }
diff --git a/jena-core/src/main/java/org/apache/jena/util/iterator/WrappedIterator.java b/jena-core/src/main/java/org/apache/jena/util/iterator/WrappedIterator.java
index f45a154..cc1d385 100644
--- a/jena-core/src/main/java/org/apache/jena/util/iterator/WrappedIterator.java
+++ b/jena-core/src/main/java/org/apache/jena/util/iterator/WrappedIterator.java
@@ -19,6 +19,7 @@
 package org.apache.jena.util.iterator;
 
 import java.util.Iterator;
+import java.util.function.Consumer;
 import java.util.stream.Stream;
 
 /**
@@ -106,10 +107,11 @@
     @Override public T next()
         { return base.next(); }
 
-    /**
-         if .remove() is allowed, delegate to the base iterator's .remove;
-         otherwise, throw an UnsupportedOperationException.
-    */
+    /** forEachRemaining: defer to the base iterator */
+    @Override
+        public void forEachRemaining(Consumer<? super T> action)
+        { base.forEachRemaining(action); }
+
     @Override public void remove()
         {
         if (removeDenied) throw new UnsupportedOperationException();
diff --git a/jena-core/src/test/java/org/apache/jena/graph/test/TestNodeToTriplesMap.java b/jena-core/src/test/java/org/apache/jena/graph/test/TestNodeToTriplesMapMem.java
similarity index 74%
rename from jena-core/src/test/java/org/apache/jena/graph/test/TestNodeToTriplesMap.java
rename to jena-core/src/test/java/org/apache/jena/graph/test/TestNodeToTriplesMapMem.java
index 1adcdd4..0efbb8b 100644
--- a/jena-core/src/test/java/org/apache/jena/graph/test/TestNodeToTriplesMap.java
+++ b/jena-core/src/test/java/org/apache/jena/graph/test/TestNodeToTriplesMapMem.java
@@ -23,26 +23,26 @@
 import junit.framework.TestSuite;
 import org.apache.jena.graph.* ;
 import org.apache.jena.graph.Triple.* ;
-import org.apache.jena.mem.NodeToTriplesMap ;
+import org.apache.jena.mem.NodeToTriplesMapMem ;
 
 /**
  	TestNodeToTriplesMap: added, post-hoc, by kers once NTM got
  	rather complicated. So these tests may be (are, at the moment)
  	incomplete.
 */
-public class TestNodeToTriplesMap extends GraphTestBase
+public class TestNodeToTriplesMapMem extends GraphTestBase
     {
-    public TestNodeToTriplesMap( String name )
+    public TestNodeToTriplesMapMem(String name )
         { super( name ); }
     
     public static TestSuite suite()
-        { return new TestSuite( TestNodeToTriplesMap.class ); }
+        { return new TestSuite( TestNodeToTriplesMapMem.class ); }
     
-    protected NodeToTriplesMap ntS = new NodeToTriplesMap( Field.fieldSubject, Field.fieldPredicate, Field.fieldObject );
+    protected NodeToTriplesMapMem ntS = new NodeToTriplesMapMem( Field.fieldSubject, Field.fieldPredicate, Field.fieldObject );
     	
-    protected NodeToTriplesMap ntP = new NodeToTriplesMap( Field.fieldPredicate, Field.fieldObject, Field.fieldSubject );
+    protected NodeToTriplesMapMem ntP = new NodeToTriplesMapMem( Field.fieldPredicate, Field.fieldObject, Field.fieldSubject );
     	
-    protected NodeToTriplesMap ntO = new NodeToTriplesMap( Field.fieldObject, Field.fieldPredicate, Field.fieldSubject );
+    protected NodeToTriplesMapMem ntO = new NodeToTriplesMapMem( Field.fieldObject, Field.fieldPredicate, Field.fieldSubject );
 
     protected static final Node x = node( "x" );
     
@@ -53,7 +53,7 @@
         testZeroSize( "fresh NTM", ntS );
         }
     
-    protected void testZeroSize( String title, NodeToTriplesMap nt )
+    protected void testZeroSize( String title, NodeToTriplesMapMem nt )
         {
         assertEquals( title + " should have size 0", 0, nt.size() );
         assertEquals( title + " should be isEmpty()", true, nt.isEmpty() );
@@ -72,7 +72,7 @@
         testJustOne( x, ntS );
         }
     
-    protected void testJustOne( Node x, NodeToTriplesMap nt )
+    protected void testJustOne( Node x, NodeToTriplesMapMem nt )
         {
         assertEquals( 1, nt.size() );
         assertEquals( false, nt.isEmpty() );
@@ -136,7 +136,49 @@
             }
         assertEquals( tripleSet( "x nice a; x nice c; y nice d; y nice f" ), ntS.iterateAll().toSet() );
         }
-    
+
+    public void testRemoveByIteratorTriggerMove()
+        {
+            /*need hash collisions to be able to test moves caused by iterator#remove*/
+            var nodeA = new Node_URI("A") {
+                @Override
+                public int hashCode() {
+                    return 1;
+                }
+            };
+            var nodeB = new Node_URI("B") {
+                @Override
+                public int hashCode() {
+                    return 1;
+                }
+            };
+            var nodeC = new Node_URI("C") {
+                @Override
+                public int hashCode() {
+                    return 1;
+                }
+            };
+            ntS.add(Triple.create(nodeA, NodeFactory.createURI("loves"), nodeB));
+            ntS.add(Triple.create(nodeB, NodeFactory.createURI("loves"), nodeC));
+            ntS.add(Triple.create(nodeC, NodeFactory.createURI("loves"), nodeA));
+
+            var triplesToFind = ntS.iterateAll().toSet();
+
+            Iterator<Triple> it = ntS.iterateAll();
+            while (it.hasNext())
+            {
+                Triple t = it.next();
+                triplesToFind.remove(t);
+                if (t.getSubject().equals( nodeA )) it.remove();
+            }
+            assertTrue(triplesToFind.isEmpty());
+
+            var expectedRemainingTripples = new HashSet<Triple>();
+            expectedRemainingTripples.add(Triple.create(nodeB, NodeFactory.createURI("loves"), nodeC));
+            expectedRemainingTripples.add(Triple.create(nodeC, NodeFactory.createURI("loves"), nodeA));
+            assertEquals( expectedRemainingTripples, ntS.iterateAll().toSet() );
+        }
+
     public void testIteratorWIthPatternOnEmpty()
         {
         assertEquals( tripleSet( "" ), ntS.iterateAll( triple( "a P b" ) ).toSet() );
@@ -207,7 +249,7 @@
     
     // TODO more here
     
-    protected void addTriples( NodeToTriplesMap nt, String facts )
+    protected void addTriples( NodeToTriplesMapMem nt, String facts )
         {
         Triple [] t = tripleArray( facts );
             for ( Triple aT : t )
diff --git a/jena-core/src/test/java/org/apache/jena/graph/test/TestPackage_graph.java b/jena-core/src/test/java/org/apache/jena/graph/test/TestPackage_graph.java
index b52dcd9..2a8c2b9 100644
--- a/jena-core/src/test/java/org/apache/jena/graph/test/TestPackage_graph.java
+++ b/jena-core/src/test/java/org/apache/jena/graph/test/TestPackage_graph.java
@@ -41,7 +41,7 @@
         addTest( TestNode.suite() );
         addTest( TestTriple.suite() );
         addTest( TestTripleField.suite() );
-        addTest( TestNodeToTriplesMap.suite() );
+        addTest( TestNodeToTriplesMapMem.suite() );
         addTest( TestReifier.suite() );
         addTest( TestTypedLiterals.suite() );
         addTest( TestDateTime.suite() );
diff --git a/jena-core/src/test/java/org/apache/jena/graph/test/TestTripleField.java b/jena-core/src/test/java/org/apache/jena/graph/test/TestTripleField.java
index 46d3b6d..d63e0df 100644
--- a/jena-core/src/test/java/org/apache/jena/graph/test/TestTripleField.java
+++ b/jena-core/src/test/java/org/apache/jena/graph/test/TestTripleField.java
@@ -69,7 +69,25 @@
         assertTrue( Field.fieldPredicate.filterOn( node( "P" ) ).test( triple( "a P b" ) ) );
         assertFalse( Field.fieldPredicate.filterOn( node( "Q" ) ).test( triple( "a P b" ) ) );
         }
-    
+
+    public void testFilterOnConcreteSubject()
+        {
+        assertTrue( Field.fieldSubject.filterOnConcrete( node( "a" ) ).test( triple( "a P b" ) ) );
+        assertFalse( Field.fieldSubject.filterOnConcrete( node( "x" ) ).test( triple( "a P b" ) ) );
+        }
+
+    public void testFilterOnConcreteObject()
+        {
+        assertTrue( Field.fieldObject.filterOnConcrete( node( "b" ) ).test( triple( "a P b" ) ) );
+        assertFalse( Field.fieldObject.filterOnConcrete( node( "c" ) ).test( triple( "a P b" ) ) );
+        }
+
+    public void testFilterOnConcretePredicate()
+        {
+        assertTrue( Field.fieldPredicate.filterOnConcrete( node( "P" ) ).test( triple( "a P b" ) ) );
+        assertFalse( Field.fieldPredicate.filterOnConcrete( node( "Q" ) ).test( triple( "a P b" ) ) );
+        }
+
     public void testFilterByTriple()
         {
         assertTrue( Field.fieldSubject.filterOn( triple( "s P o" ) ).test( triple( "s Q p" ) ) );
diff --git a/jena-core/src/test/java/org/apache/jena/mem/FieldFilterTest.java b/jena-core/src/test/java/org/apache/jena/mem/FieldFilterTest.java
new file mode 100644
index 0000000..0b4ded2
--- /dev/null
+++ b/jena-core/src/test/java/org/apache/jena/mem/FieldFilterTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jena.mem;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+import org.junit.Test;
+
+import static org.apache.jena.graph.test.GraphTestBase.node;
+import static org.apache.jena.graph.test.GraphTestBase.triple;
+import static org.junit.Assert.*;
+
+public class FieldFilterTest {
+
+    @Test
+    public void filterOnTwoNodes() {
+        var sut = FieldFilter.filterOn(Triple.Field.fieldSubject, node("a"), Triple.Field.fieldPredicate, node("P"));
+        assertTrue(sut.hasFilter());
+        var filter = sut.getFilter();
+        assertTrue(filter.test(triple( "a P b" )));
+        assertFalse(filter.test(triple( "c P b" )));
+        assertFalse(filter.test(triple( "a Q b" )));
+    }
+
+    @Test
+    public void filterOnFirstNode() {
+        var sut = FieldFilter.filterOn(Triple.Field.fieldSubject, node("a"), Triple.Field.fieldPredicate, Node.ANY);
+        assertTrue(sut.hasFilter());
+        var filter = sut.getFilter();
+        assertTrue(filter.test(triple( "a P b" )));
+        assertFalse(filter.test(triple( "c P b" )));
+        assertTrue(filter.test(triple( "a Q b" )));
+    }
+
+    @Test
+    public void filterOnSecondNode() {
+        var sut = FieldFilter.filterOn(Triple.Field.fieldSubject, Node.ANY, Triple.Field.fieldPredicate, node("P"));
+        assertTrue(sut.hasFilter());
+        var filter = sut.getFilter();
+        assertTrue(filter.test(triple( "a P b" )));
+        assertTrue(filter.test(triple( "c P b" )));
+        assertFalse(filter.test(triple( "a Q b" )));
+    }
+
+    @Test
+    public void filterOnAny() {
+        var sut = FieldFilter.filterOn(Triple.Field.fieldSubject, Node.ANY, Triple.Field.fieldPredicate, Node.ANY);
+        assertFalse(sut.hasFilter());
+        assertNull(sut.getFilter());
+    }
+}
\ No newline at end of file
diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphTripleStoreMem_CS.java b/jena-core/src/test/java/org/apache/jena/mem/GraphTripleStoreMem_CS.java
index 2768dd3..cda2f88 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/GraphTripleStoreMem_CS.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/GraphTripleStoreMem_CS.java
@@ -27,7 +27,7 @@
 import org.xenei.junit.contract.IProducer;
 
 @RunWith(ContractSuite.class)
-@ContractImpl(GraphTripleStore.class)
+@ContractImpl(GraphTripleStoreMem.class)
 public class GraphTripleStoreMem_CS {
 
 	private IProducer<GraphTripleStoreMem> producer = new IProducer<GraphTripleStoreMem>() {
diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphTripleStore_CS.java b/jena-core/src/test/java/org/apache/jena/mem/GraphTripleStore_CS.java
deleted file mode 100644
index 96cad99..0000000
--- a/jena-core/src/test/java/org/apache/jena/mem/GraphTripleStore_CS.java
+++ /dev/null
@@ -1,50 +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
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.jena.mem;
-
-import org.junit.runner.RunWith;
-import org.xenei.junit.contract.Contract;
-import org.xenei.junit.contract.ContractImpl;
-import org.xenei.junit.contract.ContractSuite;
-
-import org.apache.jena.graph.Graph;
-import org.xenei.junit.contract.IProducer;
-
-@RunWith(ContractSuite.class)
-@ContractImpl(GraphTripleStore.class)
-public class GraphTripleStore_CS {
-
-	private IProducer<GraphTripleStore> producer = new IProducer<GraphTripleStore>() {
-
-		@Override
-		public GraphTripleStore newInstance() {
-			return new GraphTripleStore(Graph.emptyGraph);
-		}
-
-		@Override
-		public void cleanUp() {
-		}
-
-	};
-
-	@Contract.Inject
-	public IProducer<GraphTripleStore> getTripleStore() {
-		return producer;
-	}
-}
diff --git a/jena-core/src/test/java/org/apache/jena/mem/HashedBunchMap_CS.java b/jena-core/src/test/java/org/apache/jena/mem/HashedBunchMap_CS.java
index cb64b57..20843e8 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/HashedBunchMap_CS.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/HashedBunchMap_CS.java
@@ -24,7 +24,7 @@
 import org.xenei.junit.contract.IProducer;
 
 @RunWith(ContractSuite.class)
-@ContractImpl(WrappedHashMap.class)
+@ContractImpl(HashedBunchMap.class)
 public class HashedBunchMap_CS {
 
 	protected IProducer<HashedBunchMap> mapProducer = new IProducer<HashedBunchMap>() {
diff --git a/jena-core/src/test/java/org/apache/jena/mem/SetBunch_CS.java b/jena-core/src/test/java/org/apache/jena/mem/SetBunch_CS.java
deleted file mode 100644
index b6b1f69..0000000
--- a/jena-core/src/test/java/org/apache/jena/mem/SetBunch_CS.java
+++ /dev/null
@@ -1,50 +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
-
-        http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
- */
-package org.apache.jena.mem;
-
-import org.junit.runner.RunWith;
-import org.xenei.junit.contract.Contract.Inject;
-import org.xenei.junit.contract.ContractImpl;
-import org.xenei.junit.contract.ContractSuite;
-import org.xenei.junit.contract.IProducer;
-
-@RunWith(ContractSuite.class)
-@ContractImpl(SetBunch.class)
-public class SetBunch_CS {
-
-	protected IProducer<SetBunch> mapProducer = new IProducer<SetBunch>() {
-
-		@Override
-		public SetBunch newInstance() {
-			return new SetBunch( new ArrayBunch() );
-		}
-
-		@Override
-		public void cleanUp() {
-			// nothing to do
-		}
-
-	};
-
-	@Inject
-	public IProducer<SetBunch> getGraphProducer() {
-		return mapProducer;
-	}
-
-
-}
diff --git a/jena-core/src/test/java/org/apache/jena/mem/SparseArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/SparseArraySpliteratorTest.java
new file mode 100644
index 0000000..8d62e25
--- /dev/null
+++ b/jena-core/src/test/java/org/apache/jena/mem/SparseArraySpliteratorTest.java
@@ -0,0 +1,527 @@
+/*
+ * 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
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jena.mem;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Spliterator;
+
+import static org.junit.Assert.*;
+
+public class SparseArraySpliteratorTest {
+
+    @Test
+    public void tryAdvanceEmpty() {
+        {
+            Integer[] array = new Integer[0];
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 0, () -> {
+            });
+            assertFalse(spliterator.tryAdvance((i) -> {
+                fail("Should not have advanced");
+            }));
+        }
+        {
+            Integer[] array = new Integer[1];
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 0, () -> {
+            });
+            assertFalse(spliterator.tryAdvance((i) -> {
+                fail("Should not have advanced");
+            }));
+        }
+    }
+
+    @Test
+    public void tryAdvanceOne() {
+        {
+            Integer[] array = new Integer[] { 1 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 1, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            while (spliterator.tryAdvance((i) -> {
+                itemsFound.add(1);
+            })) ;
+            assertEquals(1, itemsFound.size());
+            itemsFound.contains(1);
+        }
+        {
+            Integer[] array = new Integer[]{ 1 , null };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 1, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            while (spliterator.tryAdvance((i) -> {
+                itemsFound.add(1);
+            })) ;
+            assertEquals(1, itemsFound.size());
+            itemsFound.contains(1);
+        }
+        {
+            Integer[] array = new Integer[]{ null , 1 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 1, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            while (spliterator.tryAdvance((i) -> {
+                itemsFound.add(1);
+            })) ;
+            assertEquals(1, itemsFound.size());
+            itemsFound.contains(1);
+        }
+    }
+
+    @Test
+    public void tryAdvanceTwo() {
+        {
+            Integer[] array = new Integer[]{ 1 , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 2, () -> {
+            });
+            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 , null , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 2, () -> {
+            });
+            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 , null , null , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 2, () -> {
+            });
+            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[]{ null , 1 , null , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 2, () -> {
+            });
+            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[]{ null , 1 , null , null , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 2, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            while (spliterator.tryAdvance((i) -> {
+                itemsFound.add(i);
+            })) ;
+            assertEquals(2, itemsFound.size());
+            itemsFound.contains(1);
+            itemsFound.contains(2);
+        }
+    }
+
+    @Test
+    public void tryAdvanceThree() {
+        {
+            Integer[] array = new Integer[]{ 1 , 2 , 3 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 3, () -> {
+            });
+            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 , null , 2 , 3 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 3, () -> {
+            });
+            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 , null , null , 2 , 3 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 3, () -> {
+            });
+            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[]{ null , 1 , null , 2 , null , 3 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 3, () -> {
+            });
+            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[]{null, 1, null, null, 2, null, 3};
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 3, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            while (spliterator.tryAdvance((i) -> {
+                itemsFound.add(i);
+            })) ;
+            assertEquals(3, itemsFound.size());
+            itemsFound.contains(1);
+            itemsFound.contains(2);
+            itemsFound.contains(3);
+        }
+    }
+
+    @Test
+    public void forEachRemainingEmpty() {
+        {
+            Integer[] array = new Integer[]{};
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 1, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            spliterator.forEachRemaining((i) -> {
+                itemsFound.add(i);
+            });
+            assertEquals(0, itemsFound.size());
+        }
+        {
+            Integer[] array = new Integer[]{ null };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 1, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            spliterator.forEachRemaining((i) -> {
+                itemsFound.add(i);
+            });
+            assertEquals(0, itemsFound.size());
+        }
+    }
+
+    @Test
+    public void forEachRemainingOne() {
+        {
+            Integer[] array = new Integer[]{ 1 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 1, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            spliterator.forEachRemaining((i) -> {
+                itemsFound.add(i);
+            });
+            assertEquals(1, itemsFound.size());
+            itemsFound.contains(1);
+        }
+        {
+            Integer[] array = new Integer[]{ null , 1 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 1, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            spliterator.forEachRemaining((i) -> {
+                itemsFound.add(i);
+            });
+            assertEquals(1, itemsFound.size());
+            itemsFound.contains(1);
+        }
+        {
+            Integer[] array = new Integer[]{ 1 , null };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 1, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            spliterator.forEachRemaining((i) -> {
+                itemsFound.add(i);
+            });
+            assertEquals(1, itemsFound.size());
+            itemsFound.contains(1);
+        }
+    }
+
+    @Test
+    public void forEachRemainingTwo() {
+        {
+            Integer[] array = new Integer[]{ 1 , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 2, () -> {
+            });
+            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 , null , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 2, () -> {
+            });
+            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 , null , null , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 2, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            spliterator.forEachRemaining((i) -> {
+                itemsFound.add(i);
+            });
+            assertEquals(2, itemsFound.size());
+            itemsFound.contains(1);
+            itemsFound.contains(2);
+        }
+        {
+            Integer[] array = new Integer[]{ null , 1 , null , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 2, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            spliterator.forEachRemaining((i) -> {
+                itemsFound.add(i);
+            });
+            assertEquals(2, itemsFound.size());
+            itemsFound.contains(1);
+            itemsFound.contains(2);
+        }
+        {
+            Integer[] array = new Integer[]{ null , 1 , null , null , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 2, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            spliterator.forEachRemaining((i) -> {
+                itemsFound.add(i);
+            });
+            assertEquals(2, itemsFound.size());
+            itemsFound.contains(1);
+            itemsFound.contains(2);
+        }
+    }
+
+    @Test
+    public void forEachRemainingThree(){
+        {
+            Integer[] array = new Integer[]{ 1 , 2 , 3 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 3, () -> {
+            });
+            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 , null , 2 , 3 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 3, () -> {
+            });
+            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 , null , null , 2 , 3 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 3, () -> {
+            });
+            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[]{ null , 1 , null , 2 , 3 };
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 3, () -> {
+            });
+            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[]{null, 1, null, null, 2, 3};
+            Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 3, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            spliterator.forEachRemaining((i) -> {
+                itemsFound.add(i);
+            });
+            assertEquals(3, itemsFound.size());
+        }
+    }
+
+    @Test
+    public void trySplitEmpty() {
+        Integer[] array = new Integer[]{};
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 1, () -> {
+        });
+        assertNull(spliterator.trySplit());
+    }
+
+    @Test
+    public void trySplitOne() {
+        Integer[] array = new Integer[]{ 1 };
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 1, () -> {
+        });
+        assertNull(spliterator.trySplit());
+    }
+
+    @Test
+    public void trySplitTwo() {
+        Integer[] array = new Integer[]{ 1 , 2 };
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 2, () -> {
+        });
+        // Estimated size is not exact
+        assertBetween(2, 3, spliterator.estimateSize());
+        Spliterator<Integer> split = spliterator.trySplit();
+        assertBetween(1, 2, spliterator.estimateSize());
+        assertBetween(1, 3, split.estimateSize());
+    }
+
+    @Test
+    public void trySplitThree() {
+        Integer[] array = new Integer[]{ 1 , 2 , 3 };
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 3, () -> {
+        });
+        // Estimated size is not exact
+        assertBetween(3, 4, spliterator.estimateSize());
+        Spliterator<Integer> split = spliterator.trySplit();
+        assertBetween(1, 2, spliterator.estimateSize());
+        assertBetween(2, 3, split.estimateSize());
+    }
+
+    @Test
+    public void trySplitFour() {
+        Integer[] array = new Integer[]{ 1 , 2 , 3 , 4 };
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 4, () -> {
+        });
+        // Estimated size is not exact
+        assertBetween(4, 5, spliterator.estimateSize());
+        Spliterator<Integer> split = spliterator.trySplit();
+        assertBetween(2, 3, spliterator.estimateSize());
+        assertBetween(2, 4, split.estimateSize());
+    }
+
+    @Test
+    public void trySplitFive() {
+        Integer[] array = new Integer[]{ 1 , 2 , 3 , 4 , 5 };
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 5, () -> {
+        });
+        // Estimated size is not exact
+        assertBetween(5, 6, spliterator.estimateSize());
+        Spliterator<Integer> split = spliterator.trySplit();
+        assertBetween(2, 3, spliterator.estimateSize());
+        assertBetween(3, 4, split.estimateSize());
+    }
+
+    @Test
+    public void trySplitOneHundred() {
+        Integer[] array = new Integer[200];
+        for (int i = 0; i < array.length; i++) {
+            if(i % 2 == 0) {
+                array[i] = i;
+            }
+        }
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 100, () -> {
+        });
+        // Estimated size is not exact
+        assertBetween(100, 101, spliterator.estimateSize());
+        Spliterator<Integer> split = spliterator.trySplit();
+        assertBetween(50, 51, spliterator.estimateSize());
+        assertBetween(50, 51, 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() {
+        Integer[] array = new Integer[]{};
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 0, () -> {
+        });
+        assertBetween(0, 1, spliterator.estimateSize());
+    }
+
+    @Test
+    public void estimateSizeOne() {
+        Integer[] array = new Integer[]{ 1 };
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 1, () -> {
+        });
+        assertBetween(1, 2, spliterator.estimateSize());
+    }
+
+    @Test
+    public void estimateSizeTwo() {
+        Integer[] array = new Integer[]{ 1 , 2 };
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 2, () -> {
+        });
+        assertBetween(2, 3, spliterator.estimateSize());
+    }
+
+    @Test
+    public void estimateSizeFive() {
+        Integer[] array = new Integer[]{ 1 , 2 , 3 , 4 , 5 };
+        Spliterator<Integer> spliterator = new SparseArraySpliterator<>(array, 5, () -> {
+        });
+        assertBetween(5, 6, spliterator.estimateSize());
+    }
+}
\ No newline at end of file
diff --git a/jena-core/src/test/java/org/apache/jena/mem/SparseArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/SparseArraySubSpliteratorTest.java
new file mode 100644
index 0000000..4a48e84
--- /dev/null
+++ b/jena-core/src/test/java/org/apache/jena/mem/SparseArraySubSpliteratorTest.java
@@ -0,0 +1,527 @@
+/*
+ * 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
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jena.mem;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Spliterator;
+
+import static org.junit.Assert.*;
+
+public class SparseArraySubSpliteratorTest {
+
+    @Test
+    public void tryAdvanceEmpty() {
+        {
+            Integer[] array = new Integer[0];
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 0, () -> {
+            });
+            assertFalse(spliterator.tryAdvance((i) -> {
+                fail("Should not have advanced");
+            }));
+        }
+        {
+            Integer[] array = new Integer[1];
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 0, () -> {
+            });
+            assertFalse(spliterator.tryAdvance((i) -> {
+                fail("Should not have advanced");
+            }));
+        }
+    }
+
+    @Test
+    public void tryAdvanceOne() {
+        {
+            Integer[] array = new Integer[] { 1 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 1, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            while (spliterator.tryAdvance((i) -> {
+                itemsFound.add(1);
+            })) ;
+            assertEquals(1, itemsFound.size());
+            itemsFound.contains(1);
+        }
+        {
+            Integer[] array = new Integer[]{ 1 , null };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 1, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            while (spliterator.tryAdvance((i) -> {
+                itemsFound.add(1);
+            })) ;
+            assertEquals(1, itemsFound.size());
+            itemsFound.contains(1);
+        }
+        {
+            Integer[] array = new Integer[]{ null , 1 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 1, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            while (spliterator.tryAdvance((i) -> {
+                itemsFound.add(1);
+            })) ;
+            assertEquals(1, itemsFound.size());
+            itemsFound.contains(1);
+        }
+    }
+
+    @Test
+    public void tryAdvanceTwo() {
+        {
+            Integer[] array = new Integer[]{ 1 , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 2, () -> {
+            });
+            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 , null , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 2, () -> {
+            });
+            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 , null , null , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 2, () -> {
+            });
+            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[]{ null , 1 , null , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 2, () -> {
+            });
+            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[]{ null , 1 , null , null , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 2, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            while (spliterator.tryAdvance((i) -> {
+                itemsFound.add(i);
+            })) ;
+            assertEquals(2, itemsFound.size());
+            itemsFound.contains(1);
+            itemsFound.contains(2);
+        }
+    }
+
+    @Test
+    public void tryAdvanceThree() {
+        {
+            Integer[] array = new Integer[]{ 1 , 2 , 3 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 3, () -> {
+            });
+            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 , null , 2 , 3 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 3, () -> {
+            });
+            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 , null , null , 2 , 3 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 3, () -> {
+            });
+            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[]{ null , 1 , null , 2 , null , 3 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 3, () -> {
+            });
+            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[]{null, 1, null, null, 2, null, 3};
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 3, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            while (spliterator.tryAdvance((i) -> {
+                itemsFound.add(i);
+            })) ;
+            assertEquals(3, itemsFound.size());
+            itemsFound.contains(1);
+            itemsFound.contains(2);
+            itemsFound.contains(3);
+        }
+    }
+
+    @Test
+    public void forEachRemainingEmpty() {
+        {
+            Integer[] array = new Integer[]{};
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 1, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            spliterator.forEachRemaining((i) -> {
+                itemsFound.add(i);
+            });
+            assertEquals(0, itemsFound.size());
+        }
+        {
+            Integer[] array = new Integer[]{ null };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 1, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            spliterator.forEachRemaining((i) -> {
+                itemsFound.add(i);
+            });
+            assertEquals(0, itemsFound.size());
+        }
+    }
+
+    @Test
+    public void forEachRemainingOne() {
+        {
+            Integer[] array = new Integer[]{ 1 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 1, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            spliterator.forEachRemaining((i) -> {
+                itemsFound.add(i);
+            });
+            assertEquals(1, itemsFound.size());
+            itemsFound.contains(1);
+        }
+        {
+            Integer[] array = new Integer[]{ null , 1 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 1, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            spliterator.forEachRemaining((i) -> {
+                itemsFound.add(i);
+            });
+            assertEquals(1, itemsFound.size());
+            itemsFound.contains(1);
+        }
+        {
+            Integer[] array = new Integer[]{ 1 , null };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 1, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            spliterator.forEachRemaining((i) -> {
+                itemsFound.add(i);
+            });
+            assertEquals(1, itemsFound.size());
+            itemsFound.contains(1);
+        }
+    }
+
+    @Test
+    public void forEachRemainingTwo() {
+        {
+            Integer[] array = new Integer[]{ 1 , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 2, () -> {
+            });
+            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 , null , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 2, () -> {
+            });
+            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 , null , null , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 2, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            spliterator.forEachRemaining((i) -> {
+                itemsFound.add(i);
+            });
+            assertEquals(2, itemsFound.size());
+            itemsFound.contains(1);
+            itemsFound.contains(2);
+        }
+        {
+            Integer[] array = new Integer[]{ null , 1 , null , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 2, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            spliterator.forEachRemaining((i) -> {
+                itemsFound.add(i);
+            });
+            assertEquals(2, itemsFound.size());
+            itemsFound.contains(1);
+            itemsFound.contains(2);
+        }
+        {
+            Integer[] array = new Integer[]{ null , 1 , null , null , 2 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 2, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            spliterator.forEachRemaining((i) -> {
+                itemsFound.add(i);
+            });
+            assertEquals(2, itemsFound.size());
+            itemsFound.contains(1);
+            itemsFound.contains(2);
+        }
+    }
+
+    @Test
+    public void forEachRemainingThree(){
+        {
+            Integer[] array = new Integer[]{ 1 , 2 , 3 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 3, () -> {
+            });
+            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 , null , 2 , 3 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 3, () -> {
+            });
+            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 , null , null , 2 , 3 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 3, () -> {
+            });
+            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[]{ null , 1 , null , 2 , 3 };
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 3, () -> {
+            });
+            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[]{null, 1, null, null, 2, 3};
+            Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 3, () -> {
+            });
+            var itemsFound = new ArrayList<>();
+            spliterator.forEachRemaining((i) -> {
+                itemsFound.add(i);
+            });
+            assertEquals(3, itemsFound.size());
+        }
+    }
+
+    @Test
+    public void trySplitEmpty() {
+        Integer[] array = new Integer[]{};
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 1, () -> {
+        });
+        assertNull(spliterator.trySplit());
+    }
+
+    @Test
+    public void trySplitOne() {
+        Integer[] array = new Integer[]{ 1 };
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 1, () -> {
+        });
+        assertNull(spliterator.trySplit());
+    }
+
+    @Test
+    public void trySplitTwo() {
+        Integer[] array = new Integer[]{ 1 , 2 };
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 2, () -> {
+        });
+        // Estimated size is not exact
+        assertBetween(2, 3, spliterator.estimateSize());
+        Spliterator<Integer> split = spliterator.trySplit();
+        assertBetween(1, 2, spliterator.estimateSize());
+        assertBetween(1, 3, split.estimateSize());
+    }
+
+    @Test
+    public void trySplitThree() {
+        Integer[] array = new Integer[]{ 1 , 2 , 3 };
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 3, () -> {
+        });
+        // Estimated size is not exact
+        assertBetween(3, 4, spliterator.estimateSize());
+        Spliterator<Integer> split = spliterator.trySplit();
+        assertBetween(1, 2, spliterator.estimateSize());
+        assertBetween(2, 3, split.estimateSize());
+    }
+
+    @Test
+    public void trySplitFour() {
+        Integer[] array = new Integer[]{ 1 , 2 , 3 , 4 };
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 4, () -> {
+        });
+        // Estimated size is not exact
+        assertBetween(4, 5, spliterator.estimateSize());
+        Spliterator<Integer> split = spliterator.trySplit();
+        assertBetween(2, 3, spliterator.estimateSize());
+        assertBetween(2, 4, split.estimateSize());
+    }
+
+    @Test
+    public void trySplitFive() {
+        Integer[] array = new Integer[]{ 1 , 2 , 3 , 4 , 5 };
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 5, () -> {
+        });
+        // Estimated size is not exact
+        assertBetween(5, 6, spliterator.estimateSize());
+        Spliterator<Integer> split = spliterator.trySplit();
+        assertBetween(2, 3, spliterator.estimateSize());
+        assertBetween(3, 4, split.estimateSize());
+    }
+
+    @Test
+    public void trySplitOneHundred() {
+        Integer[] array = new Integer[200];
+        for (int i = 0; i < array.length; i++) {
+            if(i % 2 == 0) {
+                array[i] = i;
+            }
+        }
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 100, () -> {
+        });
+        // Estimated size is not exact
+        assertBetween(100, 101, spliterator.estimateSize());
+        Spliterator<Integer> split = spliterator.trySplit();
+        assertBetween(50, 51, spliterator.estimateSize());
+        assertBetween(50, 51, 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() {
+        Integer[] array = new Integer[]{};
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 0, () -> {
+        });
+        assertBetween(0, 1, spliterator.estimateSize());
+    }
+
+    @Test
+    public void estimateSizeOne() {
+        Integer[] array = new Integer[]{ 1 };
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 1, () -> {
+        });
+        assertBetween(1, 2, spliterator.estimateSize());
+    }
+
+    @Test
+    public void estimateSizeTwo() {
+        Integer[] array = new Integer[]{ 1 , 2 };
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 2, () -> {
+        });
+        assertBetween(2, 3, spliterator.estimateSize());
+    }
+
+    @Test
+    public void estimateSizeFive() {
+        Integer[] array = new Integer[]{ 1 , 2 , 3 , 4 , 5 };
+        Spliterator<Integer> spliterator = new SparseArraySubSpliterator<>(array, 5, () -> {
+        });
+        assertBetween(5, 6, spliterator.estimateSize());
+    }
+}
\ No newline at end of file
diff --git a/jena-core/src/test/java/org/apache/jena/mem/WrappedHashMap_CS.java b/jena-core/src/test/java/org/apache/jena/mem/WrappedHashMap_CS.java
deleted file mode 100644
index 25c2bb6..0000000
--- a/jena-core/src/test/java/org/apache/jena/mem/WrappedHashMap_CS.java
+++ /dev/null
@@ -1,52 +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
-
-        http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
- */
-package org.apache.jena.mem;
-
-import org.junit.runner.RunWith;
-import org.xenei.junit.contract.Contract.Inject;
-import org.xenei.junit.contract.ContractImpl;
-import org.xenei.junit.contract.ContractSuite;
-import org.xenei.junit.contract.IProducer;
-
-@RunWith(ContractSuite.class)
-@ContractImpl(WrappedHashMap.class)
-public class WrappedHashMap_CS {
-
-	protected IProducer<WrappedHashMap> mapProducer = new IProducer<WrappedHashMap>() {
-
-		@Override
-		public WrappedHashMap newInstance() {
-			return new WrappedHashMap();
-		}
-
-		@Override
-		public void cleanUp() {
-			// nothing to do
-		}
-
-		
-
-	};
-
-	@Inject
-	public IProducer<WrappedHashMap> getGraphProducer() {
-		return mapProducer;
-	}
-
-
-}
diff --git a/jena-core/src/test/java/org/apache/jena/mem/test/TestConcurrentModificationException.java b/jena-core/src/test/java/org/apache/jena/mem/test/TestConcurrentModificationException.java
index fa5696f..cd9fe06 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/test/TestConcurrentModificationException.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/test/TestConcurrentModificationException.java
@@ -37,8 +37,7 @@
     public static TestSuite suite()
         { 
         TestSuite result = new TestSuite();
-        result.addTestSuite( TestArrayBunchCME.class ); 
-        result.addTestSuite( TestSetBunchCME.class ); 
+        result.addTestSuite( TestArrayBunchCME.class );
         result.addTestSuite( TestHashedBunchCME.class ); 
         return result;
         }
@@ -51,16 +50,7 @@
         @Override public TripleBunch getBunch()
             { return new ArrayBunch(); }
         }
-    
-    public static class TestSetBunchCME extends TestConcurrentModificationException
-        {
-        public TestSetBunchCME(String name)
-            { super( name ); }
 
-        @Override public TripleBunch getBunch()
-            { return new SetBunch( new ArrayBunch() ); }
-        }
-    
     public static class TestHashedBunchCME extends TestConcurrentModificationException
         {
         public TestHashedBunchCME(String name)
@@ -88,6 +78,7 @@
         TripleBunch b = getBunch();
         b.add( NodeCreateUtils.createTriple( "a P b" ) );
         b.add( NodeCreateUtils.createTriple( "c Q d" ) );
+        b.add( NodeCreateUtils.createTriple( "e R f" ) );
         ExtendedIterator<Triple> it = b.iterator();
         it.next();
         b.remove( NodeCreateUtils.createTriple( "a P b" ) );
diff --git a/jena-core/src/test/java/org/apache/jena/mem/test/TestGraphTripleStore.java b/jena-core/src/test/java/org/apache/jena/mem/test/TestGraphTripleStoreMem.java
similarity index 79%
rename from jena-core/src/test/java/org/apache/jena/mem/test/TestGraphTripleStore.java
rename to jena-core/src/test/java/org/apache/jena/mem/test/TestGraphTripleStoreMem.java
index bf793cc..0ac4855 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/test/TestGraphTripleStore.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/test/TestGraphTripleStoreMem.java
@@ -22,17 +22,17 @@
 import org.apache.jena.graph.Graph ;
 import org.apache.jena.graph.impl.TripleStore ;
 import org.apache.jena.graph.test.AbstractTestTripleStore ;
-import org.apache.jena.mem.GraphTripleStore ;
+import org.apache.jena.mem.GraphTripleStoreMem;
 
-public class TestGraphTripleStore extends AbstractTestTripleStore
+public class TestGraphTripleStoreMem extends AbstractTestTripleStore
     {
-    public TestGraphTripleStore( String name )
+    public TestGraphTripleStoreMem(String name )
         { super( name ); }
     
     public static TestSuite suite()
-        { return new TestSuite( TestGraphTripleStore.class ); }
+        { return new TestSuite( TestGraphTripleStoreMem.class ); }
     
     @Override
     public TripleStore getTripleStore()
-        { return new GraphTripleStore( Graph.emptyGraph ); }
+        { return new GraphTripleStoreMem( Graph.emptyGraph ); }
     }
diff --git a/jena-core/src/test/java/org/apache/jena/mem/test/TestHashCommon.java b/jena-core/src/test/java/org/apache/jena/mem/test/TestHashCommon.java
index 948e5da..83dd7d0 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/test/TestHashCommon.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/test/TestHashCommon.java
@@ -105,17 +105,18 @@
 
     public void testRemoveSimpleMove()
         {
-        ProbeHashCommon h = probeWith( "0:2:X 1:1:Y 2:2:Z" );
-        assertSame( null, h.removeFrom( 1 ) );
-        assertAlike( probeWith( "1:2:X 2:2:Z"), h );
+        ProbeHashCommon h = probeWith( "0:0:X 1:2:Y -1:2:Z" );
+        Item moved = (Item) h.removeFrom( 1 );
+        assertSame( null, moved );
+        assertAlike( probeWith( "0:0:X 1:2:Z" ), h );
         }
 
     public void testRemoveCircularMove()
         {
-        ProbeHashCommon h = probeWith( "0:0:X 1:2:Y -1:2:Z" );
+        ProbeHashCommon h = probeWith( "0:2:X 1:1:Y 2:2:Z" );
         Item moved = (Item) h.removeFrom( 1 );
-        assertAlike( probeWith( "0:0:X 1:2:Z" ), h );
-        assertEquals( new Item( 2, "Z" ), moved );
+        assertEquals( new Item( 2, "X" ), moved );
+        assertAlike( probeWith( "1:2:X 2:2:Z"), h );
         }
 
     public void testKeyIterator()
diff --git a/jena-core/src/test/java/org/apache/jena/mem/test/TestMemPackage.java b/jena-core/src/test/java/org/apache/jena/mem/test/TestMemPackage.java
index 065552b..3cab3ca 100644
--- a/jena-core/src/test/java/org/apache/jena/mem/test/TestMemPackage.java
+++ b/jena-core/src/test/java/org/apache/jena/mem/test/TestMemPackage.java
@@ -32,9 +32,8 @@
     public static TestSuite suite()
         { 
         TestSuite result = new TestSuite();
-        result.addTest( TestGraphTripleStore.suite() );
+        result.addTest( TestGraphTripleStoreMem.suite() );
         result.addTest( new TestSuite( TestArrayTripleBunch.class ) );
-        result.addTest( new TestSuite( TestWrappedSetTripleBunch.class ) );
         result.addTest( new TestSuite( TestHashedTripleBunch.class ) );
         result.addTestSuite( TestHashedBunchMap.class );
         result.addTestSuite( TestHashCommon.class );
diff --git a/jena-core/src/test/java/org/apache/jena/mem/test/TestWrappedSetTripleBunch.java b/jena-core/src/test/java/org/apache/jena/mem/test/TestWrappedSetTripleBunch.java
deleted file mode 100644
index b6a9fec..0000000
--- a/jena-core/src/test/java/org/apache/jena/mem/test/TestWrappedSetTripleBunch.java
+++ /dev/null
@@ -1,31 +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
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.jena.mem.test;
-
-import org.apache.jena.mem.* ;
-
-public class TestWrappedSetTripleBunch extends TestTripleBunch
-    {
-    public TestWrappedSetTripleBunch( String name )
-        { super( name ); }
-
-    @Override
-    public TripleBunch getBunch()
-        { return new SetBunch( emptyBunch ); }
-    }
diff --git a/jena-jdbc/jena-jdbc-core/src/test/java/org/apache/jena/jdbc/results/AbstractResultSetTests.java b/jena-jdbc/jena-jdbc-core/src/test/java/org/apache/jena/jdbc/results/AbstractResultSetTests.java
index e12104a..c994845 100644
--- a/jena-jdbc/jena-jdbc-core/src/test/java/org/apache/jena/jdbc/results/AbstractResultSetTests.java
+++ b/jena-jdbc/jena-jdbc-core/src/test/java/org/apache/jena/jdbc/results/AbstractResultSetTests.java
@@ -237,7 +237,12 @@
      */
     @Test
     public void results_select_objects_02() throws SQLException {
-        ResultSet rset = this.createResults(ds, "SELECT ?o { ?s ?p ?o . FILTER(ISNUMERIC(?o)) }");
+        /* Ordering the numbers is important to ensure we get the right type as first result.
+         * When org.apache.jena.jdbc.JdbcCompatibility.HIGH is used, the first result is used
+         * to determine the type of the column.
+         */
+
+        ResultSet rset = this.createResults(ds, "SELECT ?o { ?s ?p ?o . FILTER(ISNUMERIC(?o)) } ORDER BY DESC(?o)");
         Assert.assertNotNull(rset);
         Assert.assertFalse(rset.isClosed());
         Assert.assertTrue(rset.isBeforeFirst());
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/parser/Constraints.java b/jena-shacl/src/main/java/org/apache/jena/shacl/parser/Constraints.java
index ed78d53..2eeb959 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/parser/Constraints.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/parser/Constraints.java
@@ -147,20 +147,19 @@
     /*package*/ static List<Constraint> parseConstraints(Graph shapesGraph, Node shape, Map<Node, Shape> parsed, Set<Node> traversed) {
         List<Constraint> constraints = new ArrayList<>();
         Iterator<Triple> iter = G.find(shapesGraph, shape, null, null);
-        while(iter.hasNext()) {
-            Triple t = iter.next();
+        iter.forEachRemaining(t -> {
             Node p = t.getPredicate();
             // The parser handles sh:property specially as a PropertyShape.
             if ( SHACL.property.equals(p) )
-                continue;
+                return;
             if ( SHACL.path.equals(p) )
-                continue;
+                return;
             Node s = t.getSubject();
             Node o = t.getObject();
             Constraint c = parseConstraint(shapesGraph, s, p, o, parsed, traversed);
             if ( c != null )
                 constraints.add(c);
-        }
+        });
         return constraints;
     }
 
diff --git a/jena-tdb1/src/main/java/org/apache/jena/tdb/sys/DatasetControlMRSW.java b/jena-tdb1/src/main/java/org/apache/jena/tdb/sys/DatasetControlMRSW.java
index 460bd32..ba47f12 100644
--- a/jena-tdb1/src/main/java/org/apache/jena/tdb/sys/DatasetControlMRSW.java
+++ b/jena-tdb1/src/main/java/org/apache/jena/tdb/sys/DatasetControlMRSW.java
@@ -24,6 +24,7 @@
 import java.util.Iterator ;
 import java.util.NoSuchElementException ;
 import java.util.concurrent.atomic.AtomicLong ;
+import java.util.function.Consumer;
 
 import org.apache.jena.atlas.iterator.Iter ;
 import org.apache.jena.atlas.lib.Closeable ;
@@ -127,6 +128,13 @@
         }
 
         @Override
+        public void forEachRemaining(Consumer<? super T> action) {
+            iter.forEachRemaining(action);
+            checkIterConcurrentModification();
+            close();
+        }
+
+        @Override
         public void remove() {
             checkIterConcurrentModification();
             iter.remove();
diff --git a/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/IteratorCheckNotConcurrent.java b/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/IteratorCheckNotConcurrent.java
index 70917bd..75598fb 100644
--- a/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/IteratorCheckNotConcurrent.java
+++ b/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/IteratorCheckNotConcurrent.java
@@ -24,6 +24,7 @@
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
 
 import org.apache.jena.atlas.iterator.Iter;
 import org.apache.jena.atlas.lib.Closeable;
@@ -78,6 +79,13 @@
     }
 
     @Override
+    public void forEachRemaining(Consumer<? super T> action) {
+        iter.forEachRemaining(action);
+        close();
+        checkConcurrentModification();
+    }
+
+    @Override
     public void remove() {
         checkConcurrentModification();
         iter.remove();