diff --git a/jena-benchmarks/jena-benchmarks-jmh/pom.xml b/jena-benchmarks/jena-benchmarks-jmh/pom.xml index 8c612c8f74e..d3dcb890122 100644 --- a/jena-benchmarks/jena-benchmarks-jmh/pom.xml +++ b/jena-benchmarks/jena-benchmarks-jmh/pom.xml @@ -25,7 +25,7 @@ org.apache.jena jena-benchmarks - 6.1.0 + 6.2.0-SNAPSHOT Apache Jena - Benchmarks JMH diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsForeachRemaining.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsForeachRemaining.java index 54e19b413ef..01d765a749b 100644 --- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsForeachRemaining.java +++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsForeachRemaining.java @@ -28,6 +28,7 @@ import org.apache.jena.atlas.iterator.ActionCount; import org.apache.jena.jmh.JmhDefaultOptions; +import org.apache.jena.mem.collection.Sized; import org.junit.Assert; import org.junit.Test; @@ -75,12 +76,18 @@ public Spliterator createSut(Object[] arrayWithNulls, int elementsCount) if (count != elementsCount) { throw new RuntimeException("Concurrent modification detected"); } + } ; + final var sized = new Sized() { + @Override + public int size() { + return elementsCount; + } }; return switch (param1_iteratorImplementation) { case "memvalue.SparseArraySpliterator" -> new org.apache.jena.memvalue.SparseArraySpliterator<>(arrayWithNulls, count, checkForConcurrentModification); case "mem2.SparseArraySpliterator" -> - new SparseArraySpliterator<>(arrayWithNulls, checkForConcurrentModification); + new SparseArraySpliterator<>(arrayWithNulls, sized); default -> throw new IllegalArgumentException("Unknown spliterator implementation: " + param1_iteratorImplementation); }; diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsStreamParallel.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsStreamParallel.java index c28435e8f2a..62020e2468c 100644 --- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsStreamParallel.java +++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsStreamParallel.java @@ -29,6 +29,7 @@ import org.apache.jena.atlas.iterator.ActionCount; import org.apache.jena.jmh.JmhDefaultOptions; +import org.apache.jena.mem.collection.Sized; import org.junit.Assert; import org.junit.Test; @@ -77,11 +78,17 @@ public Spliterator createSut(Object[] arrayWithNulls, int elementsCount) throw new RuntimeException("Concurrent modification detected"); } }; + final var sized = new Sized() { + @Override + public int size() { + return elementsCount; + } + }; return switch (param1_iteratorImplementation) { case "memvalue.SparseArraySpliterator" -> new org.apache.jena.memvalue.SparseArraySpliterator<>(arrayWithNulls, count, checkForConcurrentModification); case "mem2.SparseArraySpliterator" -> - new SparseArraySpliterator<>(arrayWithNulls, checkForConcurrentModification); + new SparseArraySpliterator<>(arrayWithNulls, sized); default -> throw new IllegalArgumentException("Unknown spliterator implementation: " + param1_iteratorImplementation); }; diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsTryAdvance.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsTryAdvance.java index d5582824587..cca4a1601fe 100644 --- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsTryAdvance.java +++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsTryAdvance.java @@ -28,6 +28,7 @@ import org.apache.jena.atlas.iterator.ActionCount; import org.apache.jena.jmh.JmhDefaultOptions; +import org.apache.jena.mem.collection.Sized; import org.junit.Assert; import org.junit.Test; @@ -78,11 +79,17 @@ public Spliterator createSut(Object[] arrayWithNulls, int elementsCount) throw new RuntimeException("Concurrent modification detected"); } }; + final var sized = new Sized() { + @Override + public int size() { + return elementsCount; + } + }; return switch (param1_iteratorImplementation) { case "memvalue.SparseArraySpliterator" -> new org.apache.jena.memvalue.SparseArraySpliterator<>(arrayWithNulls, count, checkForConcurrentModification); case "mem2.SparseArraySpliterator" -> - new SparseArraySpliterator<>(arrayWithNulls, checkForConcurrentModification); + new SparseArraySpliterator<>(arrayWithNulls, sized); default -> throw new IllegalArgumentException("Unknown spliterator implementation: " + param1_iteratorImplementation); }; diff --git a/jena-benchmarks/jena-benchmarks-shadedJena560/pom.xml b/jena-benchmarks/jena-benchmarks-shadedJena560/pom.xml index 37918ddd1e7..1319d82d4a6 100644 --- a/jena-benchmarks/jena-benchmarks-shadedJena560/pom.xml +++ b/jena-benchmarks/jena-benchmarks-shadedJena560/pom.xml @@ -25,7 +25,7 @@ org.apache.jena jena-benchmarks - 6.1.0 + 6.2.0-SNAPSHOT Apache Jena - Benchmarks Shaded Jena 5.6.0 diff --git a/jena-benchmarks/pom.xml b/jena-benchmarks/pom.xml index 917b36ebbbe..6f9afb658d1 100644 --- a/jena-benchmarks/pom.xml +++ b/jena-benchmarks/pom.xml @@ -25,7 +25,7 @@ org.apache.jena jena - 6.1.0 + 6.2.0-SNAPSHOT Apache Jena - Benchmark Suite diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashBase.java b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashBase.java index 6166da81479..aeba704d75d 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashBase.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashBase.java @@ -24,58 +24,80 @@ import org.apache.jena.mem.spliterator.SparseArraySpliterator; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Predicate; /** - * This is the base class for {@link FastHashSet} and {@link FastHashSet}. - * It only grows but never shrinks. - * This map does not guarantee any order. Although due to the way it is implemented the elements have a certain order. - * This map does not allow null keys. - * This map is not thread safe. + * Base class for {@link FastHashSet} and {@link FastHashMap}. + * The collection grows on demand but never shrinks. It does not guarantee any + * iteration order (although the implementation does produce a stable order + * for a given insertion/deletion history). It does not allow {@code null} + * keys and is not thread-safe. + * Internal layout + * + * positions: power-of-two sized array used as the open-addressing + * probe table (like in {@link java.util.HashMap}). It is indexed by + * {@code hashCode & (positions.length - 1)}. A value of {@code 0} marks + * an empty slot - faster to test than a {@code null} reference. Non-empty + * slots store the bitwise complement ({@code ~}) of the index of the entry + * in the {@code keys}/{@code hashCodesOrDeletedIndices} arrays, so a real + * stored index of {@code 0} encodes as {@code -1} and is therefore distinct + * from "empty". + * keys: dense array of keys, generally filled from index 0 up to + * {@code keysPos}. Slots emptied by deletion become {@code null} and are + * reused before the array is grown. The dense layout enables fast iteration. + * hashCodesOrDeletedIndices: parallel array to {@code keys}. For + * live entries it stores the cached hash code of the key. For deleted slots + * it stores the index of the previously deleted slot, forming a freelist + * whose head is {@code lastDeletedIndex} ({@code -1} if empty). + * keysPos / removedKeysCount: high-water mark and freelist + * length, respectively; the live size is {@code keysPos - removedKeysCount}. + * + * The {@code keys} and {@code hashCodesOrDeletedIndices} arrays grow together + * by approximately a factor of 1.5 (similar to {@link java.util.ArrayList}). * - * The positions array stores negative indices to the entries and hashCode arrays. - * The positions array is implemented as a power of two sized array. (like in {@link java.util.HashMap}) This allows - * to use a fast modulo operation to calculate the index. The indices of the positions array are derived from the - * hashCodes. - * Any position 0 indicates an empty element. The comparison with 0 is faster than comparing elements with null. - * - * The keys are stored in a keys array and the hashCodesOrDeletedIndices array - * stores the hashCodes of the keys. - * hashCodesOrDeletedIndices is also used to store the indices of the deleted keys to save memory. It works like a - * linked list of deleted keys. The index of the previously deleted key is stored in the hashCodesOrDeletedIndices - * array. lastDeletedIndex is the index of the last deleted key in the hashCodesOrDeletedIndices array and serves as - * the head of the linked list of deleted keys. - * These two arrays grow together. They grow like {@link java.util.ArrayList} with a factor of 1.5. - * - * keysPos is the index of the next free position in the keys array. - * The keys array is usually completely filled from index 0 to keysPos. Exceptions are the deleted keys. - * Indices that have been deleted are reused for new keys before the keys array is extended. - * The dense nature of the keys array enables fast iteration. - * - * The index of a key in the keys array never changes. So the index of a key can be used as a handle to the key and - * for random access. + * Once a key is inserted, its index in the {@code keys} array never changes + * until it is removed. The index can therefore be used as a stable handle for + * O(1) random access, e.g. to coordinate parallel arrays of associated data. * * @param the type of the keys */ public abstract class FastHashBase implements JenaMapSetCommon { + /** Initial size of the {@link #positions} probe table. */ protected static final int MINIMUM_HASHES_SIZE = 16; + /** Initial size of the {@link #keys} / {@link #hashCodesOrDeletedIndices} arrays. */ protected static final int MINIMUM_ELEMENTS_SIZE = 8; + /** High-water mark in {@link #keys}; one past the largest slot ever used. */ protected int keysPos = 0; + /** Dense array of stored keys; {@code null} marks a freed slot. */ protected K[] keys; + /** + * For live entries: cached {@link Object#hashCode()} of the corresponding key. + * For freed slots: index of the previously freed slot (singly-linked freelist + * whose head is {@link #lastDeletedIndex}). + */ protected int[] hashCodesOrDeletedIndices; + /** Head of the freelist of removed slots, or {@code -1} if the freelist is empty. */ protected int lastDeletedIndex = -1; + /** Number of freelist entries (i.e. slots in {@link #keys} currently {@code null}). */ protected int removedKeysCount = 0; /** - * The negative indices to the entries and hashCode arrays. - * The indices of the positions array are derived from the hashCodes. - * Any position 0 indicates an empty element. + * Probe table mapping a hash bucket to an entry index in {@link #keys}. + * A slot's value is the bitwise complement ({@code ~}) of the entry index; + * a value of {@code 0} marks an empty slot. */ protected int[] positions; - protected FastHashBase(int initialSize) { + /** + * Creates a base collection sized to hold at least {@code initialSize} + * entries before growing. + * + * @param initialSize the initial capacity of the keys array; the probe + * table is sized to the next power of two at least + * twice as large + */ + protected FastHashBase(final int initialSize) { var positionsSize = Integer.highestOneBit(initialSize << 1); if (positionsSize < initialSize << 1) { positionsSize <<= 1; @@ -85,6 +107,11 @@ protected FastHashBase(int initialSize) { this.hashCodesOrDeletedIndices = new int[initialSize]; } + /** + * Creates a base collection with the default minimum capacities + * ({@link #MINIMUM_HASHES_SIZE} for the probe table and + * {@link #MINIMUM_ELEMENTS_SIZE} for the keys array). + */ protected FastHashBase() { this.positions = new int[MINIMUM_HASHES_SIZE]; this.keys = newKeysArray(MINIMUM_ELEMENTS_SIZE); @@ -95,17 +122,17 @@ protected FastHashBase() { * Copy constructor. * The new map will contain all the same keys of the map to copy. * - * @param baseToCopy + * @param baseToCopy instance to copy */ - protected > FastHashBase(final T baseToCopy) { + protected > FastHashBase(final T baseToCopy) { this.positions = new int[baseToCopy.positions.length]; System.arraycopy(baseToCopy.positions, 0, this.positions, 0, baseToCopy.positions.length); this.hashCodesOrDeletedIndices = new int[baseToCopy.hashCodesOrDeletedIndices.length]; - System.arraycopy(baseToCopy.hashCodesOrDeletedIndices, 0, this.hashCodesOrDeletedIndices, 0, baseToCopy.hashCodesOrDeletedIndices.length); + System.arraycopy(baseToCopy.hashCodesOrDeletedIndices, 0, this.hashCodesOrDeletedIndices, 0, baseToCopy.keysPos); this.keys = newKeysArray(baseToCopy.keys.length); - System.arraycopy(baseToCopy.keys, 0, this.keys, 0, baseToCopy.keys.length); + System.arraycopy(baseToCopy.keys, 0, this.keys, 0, baseToCopy.keysPos); this.keysPos = baseToCopy.keysPos; this.lastDeletedIndex = baseToCopy.lastDeletedIndex; @@ -143,6 +170,17 @@ private int calcNewPositionsSize() { return -1; } + private void fillPositionsArray(int newSize) { + this.positions = new int[newSize]; + var pos = keysPos - 1; + while (-1 < pos) { + if (null != keys[pos]) { + this.positions[findEmptySlotWithoutEqualityCheck(hashCodesOrDeletedIndices[pos])] = ~pos; + } + pos--; + } + } + /** * Grows the positions array if needed. */ @@ -151,13 +189,7 @@ protected final void growPositionsArrayIfNeeded() { if (newSize < 0) { return; } - final var oldPositions = this.positions; - this.positions = new int[newSize]; - for (int oldPosition : oldPositions) { - if (0 != oldPosition) { - this.positions[findEmptySlotWithoutEqualityCheck(hashCodesOrDeletedIndices[~oldPosition])] = oldPosition; - } - } + fillPositionsArray(newSize); } /** @@ -170,13 +202,7 @@ protected final boolean tryGrowPositionsArrayIfNeeded() { if (newSize < 0) { return false; } - final var oldPositions = this.positions; - this.positions = new int[newSize]; - for (int oldPosition : oldPositions) { - if (0 != oldPosition) { - this.positions[findEmptySlotWithoutEqualityCheck(hashCodesOrDeletedIndices[~oldPosition])] = oldPosition; - } - } + fillPositionsArray(newSize); return true; } @@ -245,21 +271,23 @@ public final boolean tryRemove(K e, int hashCode) { } /** - * Removes the element at the given position. + * Remove the given element and return the index it occupied before removal. * - * @param e the element - * @return the index of the removed element or -1 if the element was not found + * @param e the element to remove + * @return the former index of the element, or {@code -1} if it was not present */ public final int removeAndGetIndex(final K e) { return removeAndGetIndex(e, e.hashCode()); } /** - * Removes the element at the given position. + * Remove the given element and return the index it occupied before removal. + * Lets the caller supply the precomputed hash code to avoid an extra + * {@code hashCode()} call. * - * @param e the element - * @param hashCode the hash code of the element. This is a performance optimization. - * @return the index of the removed element or -1 if the element was not found + * @param e the element to remove + * @param hashCode {@code e.hashCode()} + * @return the former index of the element, or {@code -1} if it was not present */ public final int removeAndGetIndex(final K e, final int hashCode) { final var pIndex = findPosition(e, hashCode); @@ -281,18 +309,19 @@ public final void removeUnchecked(K e, int hashCode) { } /** - * Removes the element at the given position. - * - * This is an implementation of Knuth's Algorithm R from tAoCP vol3, p 527, - * with exchanging of the roles of i and j so that they can be usefully renamed - * to here and scan. + * Removes the entry referenced by the {@code positions} slot at index + * {@code here} and rehashes the affected probe chain. * - * It relies on linear probing but doesn't require a distinguished REMOVED - * value. Since we resize the table when it gets fullish, we don't worry [much] - * about the overhead of the linear probing. + * This is an implementation of Knuth's Algorithm R from The Art of + * Computer Programming, vol. 3, p. 527, with the roles of {@code i} + * and {@code j} swapped so they can be usefully renamed to here + * and scan. * + * It relies on linear probing but doesn't require a distinguished + * {@code REMOVED} sentinel. Since the table is resized once it gets + * fullish, the overhead of linear probing is not a concern. * - * @param here the index in the positions array + * @param here the index in the {@link #positions} array of the slot to clear */ protected void removeFrom(int here) { final var pIndex = ~positions[here]; @@ -345,9 +374,14 @@ public final boolean containsKey(K o) { } /** - * Attentions: Due to the ordering of the keys, this method may be slow - * if matching elements are at the start of the list. - * Try to use {@link #anyMatchRandomOrder(Predicate)} instead. + * {@inheritDoc} + * + * Iterates the keys in dense (insertion-order-ish) order. This is fast when + * matches are rare or expected near the end of the array, but can be slow + * when matches are clustered at the start of the array. For workloads + * where many matches are expected, prefer {@link #anyMatchRandomOrder(Predicate)}, + * which scans in probe-table order and tends to find matches sooner when + * they are abundant. */ @Override public final boolean anyMatch(Predicate predicate) { @@ -362,11 +396,16 @@ public final boolean anyMatch(Predicate predicate) { } /** - * This method can be faster than {@link #anyMatch(Predicate)} if one expects - * to find many matches. But it is slower if one expects to find no matches or just a single one. + * Like {@link #anyMatch(Predicate)} but scans the probe table rather than + * the dense {@code keys} array, yielding a roughly hash-based order. + * + * This is faster than {@link #anyMatch(Predicate)} when many matches are + * expected (the predicate is more likely to short-circuit early), but + * slower when no or only a single match exists (each iteration must + * test against an empty slot first). * - * @param predicate the predicate to apply to elements of this collection - * @return {@code true} if any element of the collection matches the predicate + * @param predicate the predicate to apply + * @return {@code true} if any element matches the predicate */ public final boolean anyMatchRandomOrder(Predicate predicate) { var pIndex = positions.length - 1; @@ -381,14 +420,22 @@ public final boolean anyMatchRandomOrder(Predicate predicate) { @Override public final ExtendedIterator keyIterator() { - final var initialSize = size(); - final Runnable checkForConcurrentModification = () -> - { - if (size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(keys, keysPos, checkForConcurrentModification); + return new SparseArrayIterator<>(keys, keysPos, this); } + /** + * Locates the slot in {@link #positions} that holds {@code e} (with the + * given precomputed hash code). + * + * If the key is present, returns the (non-negative) probe-table slot + * index. If the key is absent, returns the bitwise complement of the + * empty probe-table slot at which the key would be inserted, allowing + * insertion to proceed without a second probe walk. + * + * @param e the key to locate + * @param hashCode {@code e.hashCode()} + * @return the position index if found, or {@code ~insertionPosition} if not + */ protected final int findPosition(final K e, final int hashCode) { var pIndex = calcStartIndexByHashCode(hashCode); while (true) { @@ -405,6 +452,15 @@ protected final int findPosition(final K e, final int hashCode) { } } + /** + * Locates the next empty slot in {@link #positions} along the probe chain + * for the given hash code, without checking any existing entries for + * equality. Used after a positions-array resize, when no duplicates can + * exist in the rebuilt table. + * + * @param hashCode the hash code being placed + * @return the index of an empty slot in the probe table + */ protected final int findEmptySlotWithoutEqualityCheck(final int hashCode) { var pIndex = calcStartIndexByHashCode(hashCode); while (true) { @@ -435,11 +491,63 @@ public void clear() { @Override public final Spliterator keySpliterator() { - final var initialSize = this.size(); - final Runnable checkForConcurrentModification = () -> - { - if (this.size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(keys, keysPos, checkForConcurrentModification); + return new SparseArraySpliterator<>(keys, keysPos, this); + } + + /** + * Gets the key at the given index. + * Array bounds are not checked. The caller must ensure the index is valid and corresponds to a non-null key. + * + * @param i the index + * @return the key at the given index + */ + public K getKeyAt(int i) { + return keys[i]; + } + + /** + * Returns the index of the entry holding {@code key}, or {@code -1} if not present. + * + * @param key the key to look up + * @return the entry index, or {@code -1} if the key is absent + */ + public int indexOf(K key) { + final var pIndex = findPosition(key, key.hashCode()); + if (pIndex < 0) { + return -1; + } else { + return ~positions[pIndex]; + } + } + + /** + * Functional interface used by {@link #forEachKey} to receive each live + * key along with the stable index it occupies. + * + * @param the key type + */ + @FunctionalInterface + public interface KeyAndIndexConsumer { + /** + * Receive a single key and its index. + * + * @param key the key + * @param index the stable index of the key in the underlying array + */ + void accept(K key, int index); + } + + /** + * Sequentially invokes {@code consumer} for every live key with its index. + * Skips freed slots. + * + * @param consumer receives each key/index pair + */ + public void forEachKey(KeyAndIndexConsumer consumer) { + for (int i = 0; i < keysPos; i++) { + if(keys[i] != null) { + consumer.accept(keys[i], i); + } + } } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java index 04c2761416b..e3f741ba485 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java @@ -25,39 +25,56 @@ import org.apache.jena.mem.spliterator.SparseArraySpliterator; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Supplier; import java.util.function.UnaryOperator; /** - * Map which grows, if needed but never shrinks. - * This map does not guarantee any order. Although due to the way it is implemented the elements have a certain order. - * This map does not allow null keys. - * This map is not thread safe. - * It´s purpose is to support fast add, remove, contains and stream / iterate operations. - * Only remove operations are not as fast as in {@link java.util.HashMap} - * Iterating over this map does not get much faster again after removing elements because the map is not compacted. + * Hash map specialization built on top of {@link FastHashBase}. + * Grows on demand but never shrinks, does not guarantee iteration order, + * does not allow {@code null} keys, and is not thread-safe. + * + * Optimized for fast {@code add} / {@code containsKey} / {@code stream} / + * iterate operations. Removal is somewhat slower than in + * {@link java.util.HashMap} because of the back-shifting performed on the + * probe table. Iteration speed does not recover after many removals because + * the dense {@code keys} array is not compacted. + * + * @param the key type + * @param the value type */ -public abstract class FastHashMap extends FastHashBase implements JenaMap { +public abstract class FastHashMap extends FastHashBase implements JenaMapIndexed { + /** + * Parallel array to {@code keys} holding the value associated with each + * stored key. {@code values[i]} is the value for {@code keys[i]} when + * {@code keys[i]} is non-null. + */ protected V[] values; + /** + * Creates a map with the given initial key-array capacity. + * + * @param initialSize the initial capacity of the keys/values arrays + */ protected FastHashMap(int initialSize) { super(initialSize); this.values = newValuesArray(keys.length); } + /** + * Creates a map with the default initial capacity. + */ protected FastHashMap() { super(); this.values = newValuesArray(keys.length); } /** - * Copy constructor. - * The new map will contain all the same keys and values of the map to copy. + * Copy constructor. The new map contains the same keys and the same + * value references as {@code mapToCopy}. * - * @param mapToCopy + * @param mapToCopy the source map */ protected FastHashMap(final FastHashMap mapToCopy) { super(mapToCopy); @@ -66,10 +83,13 @@ protected FastHashMap(final FastHashMap mapToCopy) { } /** - * Copy constructor with value processor. + * Copy constructor that transforms each value via {@code valueProcessor}. + * Useful when the values are mutable and need to be deep-copied to keep + * the new map independent from the source. * - * @param mapToCopy - * @param valueProcessor + * @param mapToCopy the source map + * @param valueProcessor function applied to every non-null value to obtain + * the value to put in the new map */ protected FastHashMap(final FastHashMap mapToCopy, final UnaryOperator valueProcessor) { super(mapToCopy); @@ -82,6 +102,12 @@ protected FastHashMap(final FastHashMap mapToCopy, final UnaryOperator } } + /** + * Gets a new array of values with the given size. + * + * @param size the size of the array + * @return the new array + */ protected abstract V[] newValuesArray(int size); @Override @@ -106,12 +132,10 @@ public void clear() { @Override public boolean tryPut(K key, V value) { + growPositionsArrayIfNeeded(); final var hashCode = key.hashCode(); - var pIndex = findPosition(key, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { - if (tryGrowPositionsArrayIfNeeded()) { - pIndex = findPosition(key, hashCode); - } final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; values[eIndex] = value; @@ -126,12 +150,10 @@ public boolean tryPut(K key, V value) { @Override public void put(K key, V value) { + growPositionsArrayIfNeeded(); final var hashCode = key.hashCode(); - var pIndex = findPosition(key, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { - if (tryGrowPositionsArrayIfNeeded()) { - pIndex = findPosition(key, hashCode); - } final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; values[eIndex] = value; @@ -142,8 +164,27 @@ public void put(K key, V value) { } } + @Override + public int putAndGetIndex(K key, V value) { + growPositionsArrayIfNeeded(); + final int hashCode = key.hashCode(); + final var pIndex = findPosition(key, hashCode); + final int eIndex; + if (pIndex < 0) { + eIndex = getFreeKeyIndex(); + keys[eIndex] = key; + hashCodesOrDeletedIndices[eIndex] = hashCode; + positions[~pIndex] = ~eIndex; + } else { + eIndex = ~positions[pIndex]; + } + values[eIndex] = value; + return eIndex; + } + /** * Returns the value at the given index. + * Array bounds are not checked. The caller must ensure the index is valid and corresponds to a non-null key. * * @param i index * @return value @@ -178,12 +219,12 @@ public V computeIfAbsent(K key, Supplier absentValueSupplier) { var pIndex = findPosition(key, hashCode); if (pIndex < 0) { if (tryGrowPositionsArrayIfNeeded()) { - pIndex = findPosition(key, hashCode); + pIndex = ~findEmptySlotWithoutEqualityCheck(hashCode); } + final var value = absentValueSupplier.get(); final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; - final var value = absentValueSupplier.get(); values[eIndex] = value; positions[~pIndex] = ~eIndex; return value; @@ -194,18 +235,20 @@ public V computeIfAbsent(K key, Supplier absentValueSupplier) { @Override public void compute(K key, UnaryOperator valueProcessor) { - final int hashCode = key.hashCode(); + final var hashCode = key.hashCode(); var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var value = valueProcessor.apply(null); if (value == null) return; + if(tryGrowPositionsArrayIfNeeded()) { + pIndex = ~findEmptySlotWithoutEqualityCheck(hashCode); + } final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; values[eIndex] = value; positions[~pIndex] = ~eIndex; - tryGrowPositionsArrayIfNeeded(); } else { var eIndex = ~positions[pIndex]; final var value = valueProcessor.apply(values[eIndex]); @@ -217,24 +260,13 @@ public void compute(K key, UnaryOperator valueProcessor) { } } - @Override public ExtendedIterator valueIterator() { - final var initialSize = size(); - final Runnable checkForConcurrentModification = () -> - { - if (size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(values, keysPos, checkForConcurrentModification); + return new SparseArrayIterator<>(values, keysPos, this); } @Override public Spliterator valueSpliterator() { - final var initialSize = this.size(); - final Runnable checkForConcurrentModification = () -> - { - if (this.size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(values, keysPos, checkForConcurrentModification); + return new SparseArraySpliterator<>(values, keysPos, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java index 134a0092e22..5adf3232c4e 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java @@ -21,39 +21,42 @@ package org.apache.jena.mem.collection; -import org.apache.jena.mem.iterator.SparseArrayIndexedIterator; -import org.apache.jena.mem.spliterator.SparseArrayIndexedSpliterator; -import org.apache.jena.util.iterator.ExtendedIterator; - -import java.util.ConcurrentModificationException; -import java.util.Spliterator; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - /** - * Set which grows, if needed but never shrinks. - * This set does not guarantee any order. Although due to the way it is implemented the elements have a certain order. - * This set does not allow null values. - * This set is not thread safe. - * It´s purpose is to support fast add, remove, contains and stream / iterate operations. - * Only remove operations are not as fast as in {@link java.util.HashSet} - * Iterating over this set not get much faster again after removing elements because the set is not compacted. + * Hash set specialization built on top of {@link FastHashBase}. + * Grows on demand but never shrinks, does not guarantee iteration order, + * does not allow {@code null} elements, and is not thread-safe. + * + * Optimized for fast {@code add} / {@code containsKey} / {@code stream} / + * iterate operations. Removal is somewhat slower than in + * {@link java.util.HashSet} because of the back-shifting performed on the + * probe table. Iteration speed does not recover after many removals because + * the dense {@code keys} array is not compacted. + * + * @param the element type */ -public abstract class FastHashSet extends FastHashBase implements JenaSetHashOptimized { +public abstract class FastHashSet extends FastHashBase implements JenaSetIndexed { - protected FastHashSet(int initialSize) { + /** + * Creates a set with the given initial key-array capacity. + * + * @param initialSize the initial capacity of the keys array + */ + public FastHashSet(final int initialSize) { super(initialSize); } - protected FastHashSet() { + /** + * Creates a set with the default initial capacity. + */ + public FastHashSet() { super(); } /** - * Copy constructor. - * The new set will contain all the same keys of the set to copy. + * Copy constructor. The new set contains the same elements as + * {@code setToCopy}. * - * @param setToCopy + * @param setToCopy the source set */ protected FastHashSet(final FastHashSet setToCopy) { super(setToCopy); @@ -65,12 +68,12 @@ public boolean tryAdd(K key) { } @Override - public boolean tryAdd(K value, int hashCode) { + public boolean tryAdd(K key, int hashCode) { growPositionsArrayIfNeeded(); - var pIndex = findPosition(value, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var eIndex = getFreeKeyIndex(); - keys[eIndex] = value; + keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; positions[~pIndex] = ~eIndex; return true; @@ -79,28 +82,23 @@ public boolean tryAdd(K value, int hashCode) { } /** - * Add and get the index of the added element. + * Add an element and return the index it was stored at. + * If the element is already present, returns the bitwise complement + * ({@code ~existingIndex}) of the existing index, so callers can + * distinguish "newly inserted" from "already present" while still + * recovering the index in both cases. * - * @param value the value to add - * @return the index of the added element or the inverse (~) index of the existing element + * @param key the element to add + * @return the new index, or {@code ~existingIndex} if already present */ - public int addAndGetIndex(K value) { - return addAndGetIndex(value, value.hashCode()); - } - - /** - * Add and get the index of the added element. - * - * @param value the value to add - * @param hashCode the hash code of the value. This is a performance optimization. - * @return the index of the added element or the inverse (~) index of the existing element - */ - public int addAndGetIndex(final K value, final int hashCode) { + @Override + public int addAndGetIndex(K key) { growPositionsArrayIfNeeded(); - final var pIndex = findPosition(value, hashCode); + final var hashCode = key.hashCode(); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var eIndex = getFreeKeyIndex(); - keys[eIndex] = value; + keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; positions[~pIndex] = ~eIndex; return eIndex; @@ -132,62 +130,4 @@ public void addUnchecked(K value, int hashCode) { public K getKeyAt(int i) { return keys[i]; } - - /** - * Entry pairing a key with its index in the set. - * @param index index of the key in the set - * @param key the key - * @param the type of the key - */ - public record IndexedKey(int index, K key) {} - - /** - * Get an iterator over pairs of keys and their indices in the set. - * The iterator is not thread safe. - * - * @return an iterator over pairs of keys and their indices in the set - */ - public final ExtendedIterator> indexedKeyIterator() { - final var initialSize = size(); - final Runnable checkForConcurrentModification = () -> - { - if (size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIndexedIterator<>(keys, keysPos, checkForConcurrentModification); - } - - /** - * Get a spliterator over pairs of keys and their indices in the set. - * The spliterator is not thread safe. - * - * @return a spliterator over pairs of keys and their indices in the set - */ - public final Spliterator> indexedKeySpliterator() { - final var initialSize = this.size(); - final Runnable checkForConcurrentModification = () -> - { - if (this.size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIndexedSpliterator<>(keys, keysPos, checkForConcurrentModification); - } - - /** - * Get a stream over pairs of keys and their indices in the set. - * The stream is not thread safe. - * - * @return a stream over pairs of keys and their indices in the set - */ - public final Stream> indexedKeyStream() { - return StreamSupport.stream(indexedKeySpliterator(), false); - } - - /** - * Get a parallel stream over pairs of keys and their indices in the set. - * The stream is not thread safe. - * - * @return a parallel stream over pairs of keys and their indices in the set - */ - public final Stream> indexedKeyStreamParallel() { - return StreamSupport.stream(indexedKeySpliterator(), true); - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java index 5664a900170..b277789c717 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java @@ -25,7 +25,6 @@ import org.apache.jena.shared.JenaException; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Predicate; @@ -36,7 +35,7 @@ * * @param the element type */ -public abstract class HashCommonBase { +public abstract class HashCommonBase implements JenaMapSetCommon { /** * Jeremy suggests, from his experiments, that load factors more than * 0.6 leave the table too dense, and little advantage is gained below 0.4. @@ -78,7 +77,7 @@ protected HashCommonBase(int initialCapacity) { * Copy constructor. * The new table will contain all the same keys of the table to copy. * - * @param baseToCopy + * @param baseToCopy the table to copy */ protected HashCommonBase(final HashCommonBase baseToCopy) { this.keys = newKeysArray(baseToCopy.keys.length); @@ -209,18 +208,10 @@ public boolean anyMatch(final Predicate predicate) { } public ExtendedIterator keyIterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(keys, checkForConcurrentModification); + return new SparseArrayIterator<>(keys, this); } public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(keys, checkForConcurrentModification); + return new SparseArraySpliterator<>(keys, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java index 62e7bd56733..dcdd5557654 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java @@ -24,7 +24,6 @@ import org.apache.jena.mem.spliterator.SparseArraySpliterator; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Supplier; import java.util.function.UnaryOperator; @@ -207,19 +206,11 @@ protected void removeFrom(int here) { @Override public ExtendedIterator valueIterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(values, checkForConcurrentModification); + return new SparseArrayIterator<>(values, this); } @Override public Spliterator valueSpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(values, checkForConcurrentModification); + return new SparseArraySpliterator<>(values, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java index 3e13613b08f..6d2423e0097 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java @@ -30,6 +30,7 @@ /** * A map from keys of type {@code K} to values of type {@code V}. + * Not thread-safe and does not allow {@code null} keys. * * @param the type of the keys in the map * @param the type of the values in the map diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java new file mode 100644 index 00000000000..67c366d00eb --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.mem.collection; + +/** + * Extension of {@link JenaMap} that exposes index-based access and lets callers + * supply a precomputed hash code for the key. Indices are stable handles to + * entries (returned by {@link #putAndGetIndex(Object, Object)}) and remain + * valid until the corresponding entry is removed. + * + * The hash-code overloads are a performance shortcut for callers that already + * have the hash at hand (typically because the same key is stored in several + * collections). The supplied hash code MUST equal {@code key.hashCode()}, or + * the map will misbehave. + * + * @param the type of the keys in the map + * @param the type of the values in the map + */ +public interface JenaMapIndexed extends JenaMap { + + /** + * Returns the index of the entry with the given key, or a negative value + * if no such entry exists. + * + * @param key the key to look up + * @return the index of the entry, or a negative value if absent + */ + int indexOf(K key); + + /** + * Returns the key stored at the given index. + * + * @param index the index of the entry + * @return the key at that index + */ + K getKeyAt(int index); + + /** + * Returns the value stored at the given index. + * + * @param index the index of the entry + * @return the value at that index + */ + V getValueAt(int index); + + /** + * Put a key-value pair and return the index of the affected entry. + * If the key is already present, its value is updated and the existing + * index is returned. + * + * @param key the key to put + * @param value the value to put + * @return the index of the entry holding {@code key} + */ + int putAndGetIndex(K key, V value); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java index 2533714ce6b..7f96baa19f9 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java @@ -28,22 +28,23 @@ import java.util.stream.StreamSupport; /** - * Common interface for {@link JenaMap} and {@link JenaSet}. * + * Operations shared between the map ({@link JenaMap}) and the set + * ({@link JenaSet}) collections used in the {@code mem} triple store + * implementations. + * + * These collections trade some flexibility for speed: they expose only the + * operations needed by triple-store internals (no full {@link java.util.Map} + * or {@link java.util.Set} contract). They are not thread-safe. * - * @param the type of the keys/elements in the collection + * @param the type of the keys (or elements, for sets) in the collection */ -public interface JenaMapSetCommon { +public interface JenaMapSetCommon extends Sized { /** * Clear the collection. */ void clear(); - /** - * @return the number of elements in the collection - */ - int size(); - /** * @return true if the collection is empty */ @@ -75,7 +76,10 @@ public interface JenaMapSetCommon { /** * Removes a key from the collection. - * Attention: Implementations may assume that the key is present. + * + * Attention: implementations may assume the key is present and may produce + * undefined behavior (including silently corrupting internal state) if it + * is not. Use {@link #tryRemove(Object)} when in doubt. * * @param key the key to remove */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java index d3b8a557be9..03848073f56 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java @@ -21,9 +21,10 @@ package org.apache.jena.mem.collection; /** - * Set interface specialized for the use cases in triple store implementations. + * Set interface specialized for the use cases in triple-store implementations. + * Not thread-safe; does not allow {@code null} elements. * - * @param + * @param the element type of the set */ public interface JenaSet extends JenaMapSetCommon { @@ -31,13 +32,16 @@ public interface JenaSet extends JenaMapSetCommon { * Add the key to the set if it is not already present. * * @param key the key to add - * @return true if the key was added, false if it was already present + * @return {@code true} if the key was added, {@code false} if it was already present */ boolean tryAdd(E key); /** - * Add the key to the set without checking if it is already present. - * Attention: This method must only be used if it is guaranteed that the key is not already present. + * Add the key to the set without checking whether it is already present. + * + * Attention: this method must only be used if the caller has ensured that + * the key is not already in the set; otherwise the set's invariants will + * break (duplicates may be inserted). * * @param key the key to add */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java index 8cc8aad8daf..0e1d032b356 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java @@ -22,17 +22,50 @@ /** - * Extension of {@link JenaSet} that allows to add and remove elements - * with a given hash code. - * This is useful if the hash code is already known. - * Attention: The hash code must be consistent with E::hashCode(). + * Extension of {@link JenaSet} that lets callers supply a precomputed hash + * code. + * + * Attention: any caller-supplied hash code MUST equal {@code E.hashCode()}; + * if it does not, the set will misbehave. + * + * @param the element type of the set */ public interface JenaSetHashOptimized extends JenaSet { + + /** + * Add an element with the given precomputed hash code if it is not + * already present. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + * @return {@code true} if added, {@code false} if already present + */ boolean tryAdd(E key, int hashCode); + /** + * Add an element with the given precomputed hash code without checking + * whether it is already present. The caller MUST ensure the key is absent. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + */ void addUnchecked(E key, int hashCode); + /** + * Try to remove an element with the given precomputed hash code. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + * @return {@code true} if removed, {@code false} if it was not present + */ boolean tryRemove(E key, int hashCode); + /** + * Remove an element assumed to be present, with the given precomputed + * hash code. Behavior is undefined if the element is not in the set. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + */ void removeUnchecked(E key, int hashCode); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java new file mode 100644 index 00000000000..c7c3d2e1ddb --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.mem.collection; + + +/** + * Extension of {@link JenaSetHashOptimized} that exposes index-based access to elements. + * Indices are stable handles to entries (returned by {@link #addAndGetIndex(Object)}) and remain + * valid until the corresponding entry is removed. + * + * @param the element type of the set + */ +public interface JenaSetIndexed extends JenaSetHashOptimized { + + /** + * Add an element and return the index it was stored at. If the element + * is already present, returns a negative value (typically the bitwise + * complement of the existing index). + * + * @param key the element to add + * @return the index of the inserted element, or a negative value if the + * element was already present + */ + int addAndGetIndex(final E key); + + /** + * Returns the element stored at the given index. + * + * @param index the index to read + * @return the element at that index + */ + E getKeyAt(int index); + + /** + * Returns the index of the given element, or a negative value if it is + * not in the set. + * + * @param key the element to look up + * @return the index of {@code key}, or a negative value if absent + */ + int indexOf(E key); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java new file mode 100644 index 00000000000..237740ce8e3 --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.mem.collection; + +/** + * Base interface for sized collections. + * It is typically used to detect concurrent modifications in iterators and spliterators + * by snapshotting the size at construction time and rechecking it at each advance/forEach boundary. + */ +public interface Sized { + + /** + * @return the number of elements in the collection + */ + int size(); +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java index b0ac6e994bb..8cfc8948a25 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java @@ -30,16 +30,27 @@ import java.util.function.Consumer; /** - * Iterator that iterates over the entries of sets which are contained in the given iterator of sets. + * Flat-map style iterator that yields every element of every {@link JenaSet} + * produced by the given parent iterator. Empty inner sets are silently + * skipped. Equivalent in spirit to a one-level {@code flatMap} but tailored + * to the {@link JenaSet} API and to {@link NiceIterator}. * - * @param the type of the elements + * @param the element type of the inner sets */ public class IteratorOfJenaSets extends NiceIterator { - final Iterator extends JenaSet> parentIterator; + /** Source iterator producing the sets to flatten. */ + private final Iterator extends JenaSet> parentIterator; - ExtendedIterator currentIterator; + /** Iterator over the keys of the set currently being consumed. */ + private ExtendedIterator currentIterator; + /** + * Create a flat iterator over the elements of every set produced by + * {@code parentIterator}. + * + * @param parentIterator the source iterator of sets + */ public IteratorOfJenaSets(Iterator extends JenaSet> parentIterator) { this.parentIterator = parentIterator; this.currentIterator = parentIterator.hasNext() diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java deleted file mode 100644 index 37f103eae25..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.iterator; - -import org.apache.jena.mem.collection.FastHashSet; -import org.apache.jena.util.iterator.NiceIterator; - -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.function.Consumer; - -/** - * An iterator over a sparse array, that skips null entries. - * This iterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * The iterator works in ascending order, starting from index 0 up to the specified exclusive index. - * - * This iterator will check for concurrent modifications by invoking a {@link Runnable} - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedIterator extends NiceIterator> implements Iterator> { - - private final E[] entries; - private final Runnable checkForConcurrentModification; - private int pos = 0; - private final int toIndexExclusive; - private boolean hasNext = false; - - public SparseArrayIndexedIterator(final E[] entries, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = entries.length; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - public SparseArrayIndexedIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Returns {@code true} if the iteration has more elements. - * (In other words, returns {@code true} if {@link #next} would - * return an element rather than throwing an exception.) - * - * @return {@code true} if the iteration has more elements - */ - @Override - public boolean hasNext() { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - hasNext = true; - return true; - } - pos++; - } - hasNext = false; - return false; - } - - /** - * Returns the next element in the iteration. - * - * @return the next element in the iteration - * @throws NoSuchElementException if the iteration has no more elements - */ - @Override - public FastHashSet.IndexedKey next() { - this.checkForConcurrentModification.run(); - if (hasNext || hasNext()) { - hasNext = false; - return new FastHashSet.IndexedKey<>(pos, entries[pos++]); - } - throw new NoSuchElementException(); - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - action.accept(new FastHashSet.IndexedKey<>(pos, entries[pos])); - } - pos++; - } - this.checkForConcurrentModification.run(); - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java index 936476a80ff..e0b79cd1ff6 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java @@ -21,34 +21,55 @@ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.Sized; import org.apache.jena.util.iterator.NiceIterator; -import java.util.Iterator; +import java.util.ConcurrentModificationException; import java.util.NoSuchElementException; import java.util.function.Consumer; /** - * An iterator over a sparse array, that skips null entries. + * Iterator over a sparse array, walking from high index to low and skipping + * {@code null} entries. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it on each call to + * {@link #next()} / {@link #forEachRemaining(Consumer)}; throws + * {@link ConcurrentModificationException} if the size has changed. * * @param the type of the array elements */ -public class SparseArrayIterator extends NiceIterator implements Iterator { +public class SparseArrayIterator extends NiceIterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; private boolean hasNext = false; - public SparseArrayIterator(final E[] entries, final Runnable checkForConcurrentModification) { + /** + * Iterate over the whole array. + * + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, final Sized set) { this.entries = entries; this.pos = entries.length - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } - public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { + /** + * Iterate over {@code entries[0 .. toIndexExclusive)} (in reverse order). + * + * @param entries the backing array (not copied) + * @param toIndexExclusive exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Sized set) { this.entries = entries; this.pos = toIndexExclusive - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** @@ -62,13 +83,11 @@ public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnab public boolean hasNext() { while (-1 < pos) { if (null != entries[pos]) { - hasNext = true; - return true; + return hasNext = true; } pos--; } - hasNext = false; - return false; + return hasNext = false; } /** @@ -79,7 +98,7 @@ public boolean hasNext() { */ @Override public E next() { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (hasNext || hasNext()) { hasNext = false; return entries[pos--]; @@ -95,6 +114,6 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java index 94008b155f1..d8536f56311 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java @@ -22,8 +22,17 @@ package org.apache.jena.mem.pattern; /** - * A pattern for matching triples. - * The pattern is defined by the wildcard positions for the subject, predicate and object. + * Categorizes a triple-match pattern by which of the subject, predicate and + * object slots are concrete and which are wildcards (i.e. {@code Node.ANY} + * or {@code null}). + * + * The eight enum values cover every possible combination. Triple-store + * implementations dispatch on this enum to pick the most efficient lookup + * path for each kind of pattern (e.g. a fully concrete {@link #SUB_PRE_OBJ} + * is answered directly from the triple set, while a partially open pattern + * such as {@link #ANY_PRE_OBJ} is answered through an index intersection). + * + * @see PatternClassifier */ public enum MatchPattern { /** diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java index 32a6ba182a1..e4cf5644eca 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java @@ -25,14 +25,15 @@ import org.apache.jena.graph.Triple; /** - * Classify a triple match into one of the 8 match patterns. + * Utility class that classifies a triple match into one of the eight + * {@link MatchPattern} values. * - * The classification is based on the concrete-ness of the subject, predicate and object. - * A concrete node is one that is not a variable. + * The classification is based on which of the subject, predicate and object + * are concrete (anything that is not a variable / wildcard / + * {@code null}) and which are wildcards. The result is used by triple-store + * implementations to dispatch to the most efficient lookup path. * - * The classification is used to select the most efficient implementation of a triple store. - * - * This is a utility class; there is no need to instantiate it. + * All operations are stateless; this class is not meant to be instantiated. * * @see MatchPattern */ @@ -41,8 +42,16 @@ public class PatternClassifier { private PatternClassifier() { } + /** + * Classify a triple match. + * + * @param tripleMatch the match triple, possibly containing wildcard nodes + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Triple tripleMatch) { - if (tripleMatch.isConcrete()) { + if (tripleMatch.getSubject().isConcrete() + && tripleMatch.getPredicate().isConcrete() + && tripleMatch.getObject().isConcrete()) { return MatchPattern.SUB_PRE_OBJ; } else { if (tripleMatch.getSubject().isConcrete()) { @@ -73,6 +82,15 @@ public static MatchPattern classify(Triple tripleMatch) { } } + /** + * Classify a triple match given as three nodes. + * Any {@code null} or non-concrete node is treated as a wildcard. + * + * @param sm subject node, or {@code null}/wildcard + * @param pm predicate node, or {@code null}/wildcard + * @param om object node, or {@code null}/wildcard + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Node sm, Node pm, Node om) { if (null != sm && sm.isConcrete()) { if (null != pm && pm.isConcrete()) { @@ -103,6 +121,5 @@ public static MatchPattern classify(Node sm, Node pm, Node om) { } } } - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java index 43bbfeeaea8..a5033c22cde 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java @@ -21,52 +21,57 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a contiguous array slice {@code [0, toIndex)}, + * iterating from high index to low. Supports splitting into + * {@link ArraySubSpliterator} children for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySpliterator implements Spliterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public ArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (-1 < --pos) { action.accept(entries[pos]); return true; @@ -79,7 +84,7 @@ public void forEachRemaining(Consumer super E> action) { while (-1 < --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -89,7 +94,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -101,4 +106,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java index 74994708b53..638f2bb0c9e 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java @@ -21,55 +21,61 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a contiguous array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low. Produced by splitting an + * {@link ArraySpliterator} (or another {@link ArraySubSpliterator}); supports + * further recursive splits for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public ArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (fromIndex <= --pos) { action.accept(entries[pos]); return true; @@ -82,7 +88,7 @@ public void forEachRemaining(Consumer super E> action) { while (fromIndex <= --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -93,7 +99,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -105,4 +111,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java deleted file mode 100644 index 704c9642706..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.spliterator; - -import java.util.Spliterator; -import java.util.function.Consumer; - -import org.apache.jena.mem.collection.FastHashSet; - -/** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * This spliterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * This spliterator works in ascending order, starting from the given start up to the specified exclusive index. - * - * This spliterator supports splitting into sub-spliterators. - * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedSpliterator implements Spliterator> { - - private final E[] entries; - private int currentPositionMinusOne; - private final int toIndexExclusive; - private final Runnable checkForConcurrentModification; - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param fromIndexInclusive the index of the first element, inclusive - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int fromIndexInclusive, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.currentPositionMinusOne = fromIndexInclusive-1; // Start at fromIndexInclusive - 1, so that the first call to tryAdvance will increment pos to fromIndexInclusive - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this(entries, 0, toIndexExclusive, checkForConcurrentModification); - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); - } - - - @Override - public boolean tryAdvance(Consumer super FastHashSet.IndexedKey> action) { - this.checkForConcurrentModification.run(); - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - return true; - } - } - return false; - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - } - } - this.checkForConcurrentModification.run(); - } - - @Override - public Spliterator> trySplit() { - final var nextPos = currentPositionMinusOne + 1; - final var remaining = toIndexExclusive - nextPos; - if ( remaining < 2) { - return null; - } - final var mid = nextPos + ( remaining >>> 1); - final var fromIndexInclusive = nextPos; - this.currentPositionMinusOne = mid-1; - return new SparseArrayIndexedSpliterator<>(entries, fromIndexInclusive, mid, checkForConcurrentModification); - } - - @Override - public long estimateSize() { return (long) toIndexExclusive - currentPositionMinusOne; } - - @Override - public int characteristics() { - return DISTINCT | NONNULL | IMMUTABLE; - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java index 6752cc9a1c1..add45739dc2 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java @@ -21,17 +21,24 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a sparse array slice {@code [0, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced for backing arrays such as those of + * {@link org.apache.jena.mem.collection.FastHashBase}, where removed slots + * are represented by {@code null}. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Supports splitting into {@link SparseArraySubSpliterator} children for + * parallel traversal. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it at each + * advance/forEach boundary; throws {@link ConcurrentModificationException} + * if the size has changed. * * @param the type of the array elements */ @@ -39,35 +46,37 @@ public class SparseArraySpliterator implements Spliterator { private final E[] entries; private int pos; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}, skipping nulls. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public SparseArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (-1 < --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -86,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -96,7 +105,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java index 3eb0784326f..d79242ac78c 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java @@ -21,55 +21,62 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a sparse array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced by splitting a {@link SparseArraySpliterator} (or another + * {@link SparseArraySubSpliterator}); supports further recursive splits for + * parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary; + * throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the type of the array elements */ public class SparseArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}, skipping nulls. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public SparseArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (fromIndex <= --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -88,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -99,7 +106,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java index 07ccc9634a9..f0fba805175 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java @@ -32,26 +32,41 @@ import java.util.function.Predicate; /** - * An ArrayBunch implements TripleBunch with a linear search of a short-ish - * array of Triples. The array grows by factor 2. + * Linear-scan implementation of {@link FastTripleBunch} backed by a packed + * {@link Triple} array. Used as long as a bunch stays small; once it grows + * past the configured threshold (see {@link FastTripleStore}) it is replaced + * with a {@link FastHashedTripleBunch}. + * + * The array grows by a factor of two when full. Equality of triples within a + * bunch is delegated to {@link #areEqual(Triple, Triple)}, which subclasses + * specialize to compare only the two nodes that are not already + * implied by the enclosing map's key. This avoids redundant equality checks + * on the shared subject/predicate/object. + * + * Not thread-safe. */ public abstract class FastArrayBunch implements FastTripleBunch { private static final int INITIAL_SIZE = 4; + /** Number of valid entries in {@link #elements}. */ protected int size = 0; + /** Packed array of triples; entries from {@code 0} to {@code size-1} are live. */ protected Triple[] elements; + /** + * Creates an empty bunch with the default initial capacity. + */ protected FastArrayBunch() { elements = new Triple[INITIAL_SIZE]; } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. - * But it will reserve only the space needed to contain them. Growing is still possible. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}; its backing array is sized to fit exactly, + * but can grow further if needed. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -59,7 +74,17 @@ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.size = bunchToCopy.size; } - public abstract boolean areEqual(final Triple a, final Triple b); + /** + * Compare two triples for equality within this bunch. + * + * Subclasses specialize this to skip the already-shared component + * (subject, predicate or object) and compare only the remaining two. + * + * @param a first triple + * @param b second triple + * @return {@code true} if the triples are considered equal in this bunch + */ + protected abstract boolean areEqual(final Triple a, final Triple b); @Override public boolean containsKey(Triple t) { @@ -127,6 +152,7 @@ public boolean tryRemove(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return true; } } @@ -138,6 +164,7 @@ public void removeUnchecked(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return; } } @@ -174,11 +201,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java index b89d3312048..a49d6b54009 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java @@ -25,21 +25,28 @@ import org.apache.jena.mem.collection.FastHashMap; /** - * Map from nodes to triple bunches. + * {@link FastHashMap} specialized to map a {@link Node} to its associated + * {@link FastTripleBunch}. Used by {@link FastTripleStore} to maintain the + * three subject/predicate/object indices. */ public class FastHashedBunchMap extends FastHashMap implements Copyable { + /** + * Creates an empty bunch map with the default initial capacity. + */ public FastHashedBunchMap() { super(); } /** - * Copy constructor. - * The new map will contain all the same nodes as keys of the map to copy, but copies of the bunches as values . + * Copy constructor. The new map has the same node keys as + * {@code mapToCopy}; each value is replaced by a deep copy of the + * corresponding bunch (via {@link FastTripleBunch#copy()}) so that + * mutations of either map cannot affect the other. * - * @param mapToCopy + * @param mapToCopy the source map */ private FastHashedBunchMap(final FastHashedBunchMap mapToCopy) { super(mapToCopy, FastTripleBunch::copy); diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java index 459e78c8181..65c9ab70fbf 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java @@ -25,13 +25,21 @@ import org.apache.jena.mem.collection.JenaSet; /** - * A set of triples - backed by {@link FastHashSet}. + * Hashed implementation of {@link FastTripleBunch} built on top of + * {@link FastHashSet}. Used by {@link FastTripleStore} once a bunch grows + * past the size threshold at which a linear-scan {@link FastArrayBunch} + * stops being faster. */ public class FastHashedTripleBunch extends FastHashSet implements FastTripleBunch { + /** - * Create a new triple bunch from the given set of triples. + * Create a new hashed bunch pre-populated from the given set of triples. + * The initial capacity is chosen at 1.5x the source size, so the new bunch + * fits the existing triples and has some headroom for growth before it + * needs to rehash. * - * @param set the set of triples + * @param set the source set of triples (typically the array bunch being + * promoted) */ public FastHashedTripleBunch(final JenaSet set) { super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth @@ -39,15 +47,18 @@ public FastHashedTripleBunch(final JenaSet set) { } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ private FastHashedTripleBunch(final FastHashedTripleBunch bunchToCopy) { super(bunchToCopy); } + /** + * Creates an empty hashed bunch with the default initial capacity. + */ public FastHashedTripleBunch() { super(); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java index 68f79e72f8a..fe050283188 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java @@ -29,27 +29,39 @@ import java.util.function.Predicate; /** - * A bunch of triples - a stripped-down set with specialized methods. A - * bunch is expected to store triples that share some useful property - * (such as having the same subject or predicate). + * Set-like container for a "bunch" of triples that share some useful + * property - typically they all have the same subject, predicate or object, + * because the bunch is the value of a node-keyed map in a + * {@link FastTripleStore}. + * + * The interface is a stripped-down set with a few extras tuned for the + * triple-store hot path; concrete implementations are + * {@link FastArrayBunch} (linear scan, used while the bunch is small) and + * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a + * threshold). */ public interface FastTripleBunch extends JenaSetHashOptimized, Copyable { /** - * Answer true iff this bunch is implemented as an array. - * This field is used to optimize some operations by avoiding the need for instanceOf tests. + * Answer {@code true} iff this bunch is backed by a flat array (i.e. is + * a {@link FastArrayBunch}). Exposed as an explicit method so callers can + * avoid {@code instanceof} checks on this hot path. * - * @return true iff this bunch is implemented as an arrays + * @return {@code true} if this bunch is array-backed */ boolean isArray(); /** - * This method is used to optimize _PO match operations. - * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches. - * This method is faster if there are many matches and the set is ordered in an unfavorable way. - * _PO matches usually fall into this category. + * Predicate test that scans elements in hash-table order rather than + * dense insertion order. Tuned for {@code _PO} (any-predicate-object) + * matches. + * + * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
- * The positions array stores negative indices to the entries and hashCode arrays. - * The positions array is implemented as a power of two sized array. (like in {@link java.util.HashMap}) This allows - * to use a fast modulo operation to calculate the index. The indices of the positions array are derived from the - * hashCodes. - * Any position 0 indicates an empty element. The comparison with 0 is faster than comparing elements with null. - *
- * The keys are stored in a keys array and the hashCodesOrDeletedIndices array - * stores the hashCodes of the keys. - * hashCodesOrDeletedIndices is also used to store the indices of the deleted keys to save memory. It works like a - * linked list of deleted keys. The index of the previously deleted key is stored in the hashCodesOrDeletedIndices - * array. lastDeletedIndex is the index of the last deleted key in the hashCodesOrDeletedIndices array and serves as - * the head of the linked list of deleted keys. - * These two arrays grow together. They grow like {@link java.util.ArrayList} with a factor of 1.5. - *
- * keysPos is the index of the next free position in the keys array. - * The keys array is usually completely filled from index 0 to keysPos. Exceptions are the deleted keys. - * Indices that have been deleted are reused for new keys before the keys array is extended. - * The dense nature of the keys array enables fast iteration. - *
- * The index of a key in the keys array never changes. So the index of a key can be used as a handle to the key and - * for random access. + * Once a key is inserted, its index in the {@code keys} array never changes + * until it is removed. The index can therefore be used as a stable handle for + * O(1) random access, e.g. to coordinate parallel arrays of associated data. * * @param the type of the keys */ public abstract class FastHashBase implements JenaMapSetCommon { + /** Initial size of the {@link #positions} probe table. */ protected static final int MINIMUM_HASHES_SIZE = 16; + /** Initial size of the {@link #keys} / {@link #hashCodesOrDeletedIndices} arrays. */ protected static final int MINIMUM_ELEMENTS_SIZE = 8; + /** High-water mark in {@link #keys}; one past the largest slot ever used. */ protected int keysPos = 0; + /** Dense array of stored keys; {@code null} marks a freed slot. */ protected K[] keys; + /** + * For live entries: cached {@link Object#hashCode()} of the corresponding key. + * For freed slots: index of the previously freed slot (singly-linked freelist + * whose head is {@link #lastDeletedIndex}). + */ protected int[] hashCodesOrDeletedIndices; + /** Head of the freelist of removed slots, or {@code -1} if the freelist is empty. */ protected int lastDeletedIndex = -1; + /** Number of freelist entries (i.e. slots in {@link #keys} currently {@code null}). */ protected int removedKeysCount = 0; /** - * The negative indices to the entries and hashCode arrays. - * The indices of the positions array are derived from the hashCodes. - * Any position 0 indicates an empty element. + * Probe table mapping a hash bucket to an entry index in {@link #keys}. + * A slot's value is the bitwise complement ({@code ~}) of the entry index; + * a value of {@code 0} marks an empty slot. */ protected int[] positions; - protected FastHashBase(int initialSize) { + /** + * Creates a base collection sized to hold at least {@code initialSize} + * entries before growing. + * + * @param initialSize the initial capacity of the keys array; the probe + * table is sized to the next power of two at least + * twice as large + */ + protected FastHashBase(final int initialSize) { var positionsSize = Integer.highestOneBit(initialSize << 1); if (positionsSize < initialSize << 1) { positionsSize <<= 1; @@ -85,6 +107,11 @@ protected FastHashBase(int initialSize) { this.hashCodesOrDeletedIndices = new int[initialSize]; } + /** + * Creates a base collection with the default minimum capacities + * ({@link #MINIMUM_HASHES_SIZE} for the probe table and + * {@link #MINIMUM_ELEMENTS_SIZE} for the keys array). + */ protected FastHashBase() { this.positions = new int[MINIMUM_HASHES_SIZE]; this.keys = newKeysArray(MINIMUM_ELEMENTS_SIZE); @@ -95,17 +122,17 @@ protected FastHashBase() { * Copy constructor. * The new map will contain all the same keys of the map to copy. * - * @param baseToCopy + * @param baseToCopy instance to copy */ - protected > FastHashBase(final T baseToCopy) { + protected > FastHashBase(final T baseToCopy) { this.positions = new int[baseToCopy.positions.length]; System.arraycopy(baseToCopy.positions, 0, this.positions, 0, baseToCopy.positions.length); this.hashCodesOrDeletedIndices = new int[baseToCopy.hashCodesOrDeletedIndices.length]; - System.arraycopy(baseToCopy.hashCodesOrDeletedIndices, 0, this.hashCodesOrDeletedIndices, 0, baseToCopy.hashCodesOrDeletedIndices.length); + System.arraycopy(baseToCopy.hashCodesOrDeletedIndices, 0, this.hashCodesOrDeletedIndices, 0, baseToCopy.keysPos); this.keys = newKeysArray(baseToCopy.keys.length); - System.arraycopy(baseToCopy.keys, 0, this.keys, 0, baseToCopy.keys.length); + System.arraycopy(baseToCopy.keys, 0, this.keys, 0, baseToCopy.keysPos); this.keysPos = baseToCopy.keysPos; this.lastDeletedIndex = baseToCopy.lastDeletedIndex; @@ -143,6 +170,17 @@ private int calcNewPositionsSize() { return -1; } + private void fillPositionsArray(int newSize) { + this.positions = new int[newSize]; + var pos = keysPos - 1; + while (-1 < pos) { + if (null != keys[pos]) { + this.positions[findEmptySlotWithoutEqualityCheck(hashCodesOrDeletedIndices[pos])] = ~pos; + } + pos--; + } + } + /** * Grows the positions array if needed. */ @@ -151,13 +189,7 @@ protected final void growPositionsArrayIfNeeded() { if (newSize < 0) { return; } - final var oldPositions = this.positions; - this.positions = new int[newSize]; - for (int oldPosition : oldPositions) { - if (0 != oldPosition) { - this.positions[findEmptySlotWithoutEqualityCheck(hashCodesOrDeletedIndices[~oldPosition])] = oldPosition; - } - } + fillPositionsArray(newSize); } /** @@ -170,13 +202,7 @@ protected final boolean tryGrowPositionsArrayIfNeeded() { if (newSize < 0) { return false; } - final var oldPositions = this.positions; - this.positions = new int[newSize]; - for (int oldPosition : oldPositions) { - if (0 != oldPosition) { - this.positions[findEmptySlotWithoutEqualityCheck(hashCodesOrDeletedIndices[~oldPosition])] = oldPosition; - } - } + fillPositionsArray(newSize); return true; } @@ -245,21 +271,23 @@ public final boolean tryRemove(K e, int hashCode) { } /** - * Removes the element at the given position. + * Remove the given element and return the index it occupied before removal. * - * @param e the element - * @return the index of the removed element or -1 if the element was not found + * @param e the element to remove + * @return the former index of the element, or {@code -1} if it was not present */ public final int removeAndGetIndex(final K e) { return removeAndGetIndex(e, e.hashCode()); } /** - * Removes the element at the given position. + * Remove the given element and return the index it occupied before removal. + * Lets the caller supply the precomputed hash code to avoid an extra + * {@code hashCode()} call. * - * @param e the element - * @param hashCode the hash code of the element. This is a performance optimization. - * @return the index of the removed element or -1 if the element was not found + * @param e the element to remove + * @param hashCode {@code e.hashCode()} + * @return the former index of the element, or {@code -1} if it was not present */ public final int removeAndGetIndex(final K e, final int hashCode) { final var pIndex = findPosition(e, hashCode); @@ -281,18 +309,19 @@ public final void removeUnchecked(K e, int hashCode) { } /** - * Removes the element at the given position. - * - * This is an implementation of Knuth's Algorithm R from tAoCP vol3, p 527, - * with exchanging of the roles of i and j so that they can be usefully renamed - * to here and scan. + * Removes the entry referenced by the {@code positions} slot at index + * {@code here} and rehashes the affected probe chain. * - * It relies on linear probing but doesn't require a distinguished REMOVED - * value. Since we resize the table when it gets fullish, we don't worry [much] - * about the overhead of the linear probing. + * This is an implementation of Knuth's Algorithm R from The Art of + * Computer Programming, vol. 3, p. 527, with the roles of {@code i} + * and {@code j} swapped so they can be usefully renamed to here + * and scan. * + * It relies on linear probing but doesn't require a distinguished + * {@code REMOVED} sentinel. Since the table is resized once it gets + * fullish, the overhead of linear probing is not a concern. * - * @param here the index in the positions array + * @param here the index in the {@link #positions} array of the slot to clear */ protected void removeFrom(int here) { final var pIndex = ~positions[here]; @@ -345,9 +374,14 @@ public final boolean containsKey(K o) { } /** - * Attentions: Due to the ordering of the keys, this method may be slow - * if matching elements are at the start of the list. - * Try to use {@link #anyMatchRandomOrder(Predicate)} instead. + * {@inheritDoc} + * + * Iterates the keys in dense (insertion-order-ish) order. This is fast when + * matches are rare or expected near the end of the array, but can be slow + * when matches are clustered at the start of the array. For workloads + * where many matches are expected, prefer {@link #anyMatchRandomOrder(Predicate)}, + * which scans in probe-table order and tends to find matches sooner when + * they are abundant. */ @Override public final boolean anyMatch(Predicate predicate) { @@ -362,11 +396,16 @@ public final boolean anyMatch(Predicate predicate) { } /** - * This method can be faster than {@link #anyMatch(Predicate)} if one expects - * to find many matches. But it is slower if one expects to find no matches or just a single one. + * Like {@link #anyMatch(Predicate)} but scans the probe table rather than + * the dense {@code keys} array, yielding a roughly hash-based order. + * + * This is faster than {@link #anyMatch(Predicate)} when many matches are + * expected (the predicate is more likely to short-circuit early), but + * slower when no or only a single match exists (each iteration must + * test against an empty slot first). * - * @param predicate the predicate to apply to elements of this collection - * @return {@code true} if any element of the collection matches the predicate + * @param predicate the predicate to apply + * @return {@code true} if any element matches the predicate */ public final boolean anyMatchRandomOrder(Predicate predicate) { var pIndex = positions.length - 1; @@ -381,14 +420,22 @@ public final boolean anyMatchRandomOrder(Predicate predicate) { @Override public final ExtendedIterator keyIterator() { - final var initialSize = size(); - final Runnable checkForConcurrentModification = () -> - { - if (size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(keys, keysPos, checkForConcurrentModification); + return new SparseArrayIterator<>(keys, keysPos, this); } + /** + * Locates the slot in {@link #positions} that holds {@code e} (with the + * given precomputed hash code). + * + * If the key is present, returns the (non-negative) probe-table slot + * index. If the key is absent, returns the bitwise complement of the + * empty probe-table slot at which the key would be inserted, allowing + * insertion to proceed without a second probe walk. + * + * @param e the key to locate + * @param hashCode {@code e.hashCode()} + * @return the position index if found, or {@code ~insertionPosition} if not + */ protected final int findPosition(final K e, final int hashCode) { var pIndex = calcStartIndexByHashCode(hashCode); while (true) { @@ -405,6 +452,15 @@ protected final int findPosition(final K e, final int hashCode) { } } + /** + * Locates the next empty slot in {@link #positions} along the probe chain + * for the given hash code, without checking any existing entries for + * equality. Used after a positions-array resize, when no duplicates can + * exist in the rebuilt table. + * + * @param hashCode the hash code being placed + * @return the index of an empty slot in the probe table + */ protected final int findEmptySlotWithoutEqualityCheck(final int hashCode) { var pIndex = calcStartIndexByHashCode(hashCode); while (true) { @@ -435,11 +491,63 @@ public void clear() { @Override public final Spliterator keySpliterator() { - final var initialSize = this.size(); - final Runnable checkForConcurrentModification = () -> - { - if (this.size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(keys, keysPos, checkForConcurrentModification); + return new SparseArraySpliterator<>(keys, keysPos, this); + } + + /** + * Gets the key at the given index. + * Array bounds are not checked. The caller must ensure the index is valid and corresponds to a non-null key. + * + * @param i the index + * @return the key at the given index + */ + public K getKeyAt(int i) { + return keys[i]; + } + + /** + * Returns the index of the entry holding {@code key}, or {@code -1} if not present. + * + * @param key the key to look up + * @return the entry index, or {@code -1} if the key is absent + */ + public int indexOf(K key) { + final var pIndex = findPosition(key, key.hashCode()); + if (pIndex < 0) { + return -1; + } else { + return ~positions[pIndex]; + } + } + + /** + * Functional interface used by {@link #forEachKey} to receive each live + * key along with the stable index it occupies. + * + * @param the key type + */ + @FunctionalInterface + public interface KeyAndIndexConsumer { + /** + * Receive a single key and its index. + * + * @param key the key + * @param index the stable index of the key in the underlying array + */ + void accept(K key, int index); + } + + /** + * Sequentially invokes {@code consumer} for every live key with its index. + * Skips freed slots. + * + * @param consumer receives each key/index pair + */ + public void forEachKey(KeyAndIndexConsumer consumer) { + for (int i = 0; i < keysPos; i++) { + if(keys[i] != null) { + consumer.accept(keys[i], i); + } + } } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java index 04c2761416b..e3f741ba485 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java @@ -25,39 +25,56 @@ import org.apache.jena.mem.spliterator.SparseArraySpliterator; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Supplier; import java.util.function.UnaryOperator; /** - * Map which grows, if needed but never shrinks. - * This map does not guarantee any order. Although due to the way it is implemented the elements have a certain order. - * This map does not allow null keys. - * This map is not thread safe. - * It´s purpose is to support fast add, remove, contains and stream / iterate operations. - * Only remove operations are not as fast as in {@link java.util.HashMap} - * Iterating over this map does not get much faster again after removing elements because the map is not compacted. + * Hash map specialization built on top of {@link FastHashBase}. + * Grows on demand but never shrinks, does not guarantee iteration order, + * does not allow {@code null} keys, and is not thread-safe. + * + * Optimized for fast {@code add} / {@code containsKey} / {@code stream} / + * iterate operations. Removal is somewhat slower than in + * {@link java.util.HashMap} because of the back-shifting performed on the + * probe table. Iteration speed does not recover after many removals because + * the dense {@code keys} array is not compacted. + * + * @param the key type + * @param the value type */ -public abstract class FastHashMap extends FastHashBase implements JenaMap { +public abstract class FastHashMap extends FastHashBase implements JenaMapIndexed { + /** + * Parallel array to {@code keys} holding the value associated with each + * stored key. {@code values[i]} is the value for {@code keys[i]} when + * {@code keys[i]} is non-null. + */ protected V[] values; + /** + * Creates a map with the given initial key-array capacity. + * + * @param initialSize the initial capacity of the keys/values arrays + */ protected FastHashMap(int initialSize) { super(initialSize); this.values = newValuesArray(keys.length); } + /** + * Creates a map with the default initial capacity. + */ protected FastHashMap() { super(); this.values = newValuesArray(keys.length); } /** - * Copy constructor. - * The new map will contain all the same keys and values of the map to copy. + * Copy constructor. The new map contains the same keys and the same + * value references as {@code mapToCopy}. * - * @param mapToCopy + * @param mapToCopy the source map */ protected FastHashMap(final FastHashMap mapToCopy) { super(mapToCopy); @@ -66,10 +83,13 @@ protected FastHashMap(final FastHashMap mapToCopy) { } /** - * Copy constructor with value processor. + * Copy constructor that transforms each value via {@code valueProcessor}. + * Useful when the values are mutable and need to be deep-copied to keep + * the new map independent from the source. * - * @param mapToCopy - * @param valueProcessor + * @param mapToCopy the source map + * @param valueProcessor function applied to every non-null value to obtain + * the value to put in the new map */ protected FastHashMap(final FastHashMap mapToCopy, final UnaryOperator valueProcessor) { super(mapToCopy); @@ -82,6 +102,12 @@ protected FastHashMap(final FastHashMap mapToCopy, final UnaryOperator } } + /** + * Gets a new array of values with the given size. + * + * @param size the size of the array + * @return the new array + */ protected abstract V[] newValuesArray(int size); @Override @@ -106,12 +132,10 @@ public void clear() { @Override public boolean tryPut(K key, V value) { + growPositionsArrayIfNeeded(); final var hashCode = key.hashCode(); - var pIndex = findPosition(key, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { - if (tryGrowPositionsArrayIfNeeded()) { - pIndex = findPosition(key, hashCode); - } final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; values[eIndex] = value; @@ -126,12 +150,10 @@ public boolean tryPut(K key, V value) { @Override public void put(K key, V value) { + growPositionsArrayIfNeeded(); final var hashCode = key.hashCode(); - var pIndex = findPosition(key, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { - if (tryGrowPositionsArrayIfNeeded()) { - pIndex = findPosition(key, hashCode); - } final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; values[eIndex] = value; @@ -142,8 +164,27 @@ public void put(K key, V value) { } } + @Override + public int putAndGetIndex(K key, V value) { + growPositionsArrayIfNeeded(); + final int hashCode = key.hashCode(); + final var pIndex = findPosition(key, hashCode); + final int eIndex; + if (pIndex < 0) { + eIndex = getFreeKeyIndex(); + keys[eIndex] = key; + hashCodesOrDeletedIndices[eIndex] = hashCode; + positions[~pIndex] = ~eIndex; + } else { + eIndex = ~positions[pIndex]; + } + values[eIndex] = value; + return eIndex; + } + /** * Returns the value at the given index. + * Array bounds are not checked. The caller must ensure the index is valid and corresponds to a non-null key. * * @param i index * @return value @@ -178,12 +219,12 @@ public V computeIfAbsent(K key, Supplier absentValueSupplier) { var pIndex = findPosition(key, hashCode); if (pIndex < 0) { if (tryGrowPositionsArrayIfNeeded()) { - pIndex = findPosition(key, hashCode); + pIndex = ~findEmptySlotWithoutEqualityCheck(hashCode); } + final var value = absentValueSupplier.get(); final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; - final var value = absentValueSupplier.get(); values[eIndex] = value; positions[~pIndex] = ~eIndex; return value; @@ -194,18 +235,20 @@ public V computeIfAbsent(K key, Supplier absentValueSupplier) { @Override public void compute(K key, UnaryOperator valueProcessor) { - final int hashCode = key.hashCode(); + final var hashCode = key.hashCode(); var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var value = valueProcessor.apply(null); if (value == null) return; + if(tryGrowPositionsArrayIfNeeded()) { + pIndex = ~findEmptySlotWithoutEqualityCheck(hashCode); + } final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; values[eIndex] = value; positions[~pIndex] = ~eIndex; - tryGrowPositionsArrayIfNeeded(); } else { var eIndex = ~positions[pIndex]; final var value = valueProcessor.apply(values[eIndex]); @@ -217,24 +260,13 @@ public void compute(K key, UnaryOperator valueProcessor) { } } - @Override public ExtendedIterator valueIterator() { - final var initialSize = size(); - final Runnable checkForConcurrentModification = () -> - { - if (size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(values, keysPos, checkForConcurrentModification); + return new SparseArrayIterator<>(values, keysPos, this); } @Override public Spliterator valueSpliterator() { - final var initialSize = this.size(); - final Runnable checkForConcurrentModification = () -> - { - if (this.size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(values, keysPos, checkForConcurrentModification); + return new SparseArraySpliterator<>(values, keysPos, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java index 134a0092e22..5adf3232c4e 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java @@ -21,39 +21,42 @@ package org.apache.jena.mem.collection; -import org.apache.jena.mem.iterator.SparseArrayIndexedIterator; -import org.apache.jena.mem.spliterator.SparseArrayIndexedSpliterator; -import org.apache.jena.util.iterator.ExtendedIterator; - -import java.util.ConcurrentModificationException; -import java.util.Spliterator; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - /** - * Set which grows, if needed but never shrinks. - * This set does not guarantee any order. Although due to the way it is implemented the elements have a certain order. - * This set does not allow null values. - * This set is not thread safe. - * It´s purpose is to support fast add, remove, contains and stream / iterate operations. - * Only remove operations are not as fast as in {@link java.util.HashSet} - * Iterating over this set not get much faster again after removing elements because the set is not compacted. + * Hash set specialization built on top of {@link FastHashBase}. + * Grows on demand but never shrinks, does not guarantee iteration order, + * does not allow {@code null} elements, and is not thread-safe. + * + * Optimized for fast {@code add} / {@code containsKey} / {@code stream} / + * iterate operations. Removal is somewhat slower than in + * {@link java.util.HashSet} because of the back-shifting performed on the + * probe table. Iteration speed does not recover after many removals because + * the dense {@code keys} array is not compacted. + * + * @param the element type */ -public abstract class FastHashSet extends FastHashBase implements JenaSetHashOptimized { +public abstract class FastHashSet extends FastHashBase implements JenaSetIndexed { - protected FastHashSet(int initialSize) { + /** + * Creates a set with the given initial key-array capacity. + * + * @param initialSize the initial capacity of the keys array + */ + public FastHashSet(final int initialSize) { super(initialSize); } - protected FastHashSet() { + /** + * Creates a set with the default initial capacity. + */ + public FastHashSet() { super(); } /** - * Copy constructor. - * The new set will contain all the same keys of the set to copy. + * Copy constructor. The new set contains the same elements as + * {@code setToCopy}. * - * @param setToCopy + * @param setToCopy the source set */ protected FastHashSet(final FastHashSet setToCopy) { super(setToCopy); @@ -65,12 +68,12 @@ public boolean tryAdd(K key) { } @Override - public boolean tryAdd(K value, int hashCode) { + public boolean tryAdd(K key, int hashCode) { growPositionsArrayIfNeeded(); - var pIndex = findPosition(value, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var eIndex = getFreeKeyIndex(); - keys[eIndex] = value; + keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; positions[~pIndex] = ~eIndex; return true; @@ -79,28 +82,23 @@ public boolean tryAdd(K value, int hashCode) { } /** - * Add and get the index of the added element. + * Add an element and return the index it was stored at. + * If the element is already present, returns the bitwise complement + * ({@code ~existingIndex}) of the existing index, so callers can + * distinguish "newly inserted" from "already present" while still + * recovering the index in both cases. * - * @param value the value to add - * @return the index of the added element or the inverse (~) index of the existing element + * @param key the element to add + * @return the new index, or {@code ~existingIndex} if already present */ - public int addAndGetIndex(K value) { - return addAndGetIndex(value, value.hashCode()); - } - - /** - * Add and get the index of the added element. - * - * @param value the value to add - * @param hashCode the hash code of the value. This is a performance optimization. - * @return the index of the added element or the inverse (~) index of the existing element - */ - public int addAndGetIndex(final K value, final int hashCode) { + @Override + public int addAndGetIndex(K key) { growPositionsArrayIfNeeded(); - final var pIndex = findPosition(value, hashCode); + final var hashCode = key.hashCode(); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var eIndex = getFreeKeyIndex(); - keys[eIndex] = value; + keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; positions[~pIndex] = ~eIndex; return eIndex; @@ -132,62 +130,4 @@ public void addUnchecked(K value, int hashCode) { public K getKeyAt(int i) { return keys[i]; } - - /** - * Entry pairing a key with its index in the set. - * @param index index of the key in the set - * @param key the key - * @param the type of the key - */ - public record IndexedKey(int index, K key) {} - - /** - * Get an iterator over pairs of keys and their indices in the set. - * The iterator is not thread safe. - * - * @return an iterator over pairs of keys and their indices in the set - */ - public final ExtendedIterator> indexedKeyIterator() { - final var initialSize = size(); - final Runnable checkForConcurrentModification = () -> - { - if (size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIndexedIterator<>(keys, keysPos, checkForConcurrentModification); - } - - /** - * Get a spliterator over pairs of keys and their indices in the set. - * The spliterator is not thread safe. - * - * @return a spliterator over pairs of keys and their indices in the set - */ - public final Spliterator> indexedKeySpliterator() { - final var initialSize = this.size(); - final Runnable checkForConcurrentModification = () -> - { - if (this.size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIndexedSpliterator<>(keys, keysPos, checkForConcurrentModification); - } - - /** - * Get a stream over pairs of keys and their indices in the set. - * The stream is not thread safe. - * - * @return a stream over pairs of keys and their indices in the set - */ - public final Stream> indexedKeyStream() { - return StreamSupport.stream(indexedKeySpliterator(), false); - } - - /** - * Get a parallel stream over pairs of keys and their indices in the set. - * The stream is not thread safe. - * - * @return a parallel stream over pairs of keys and their indices in the set - */ - public final Stream> indexedKeyStreamParallel() { - return StreamSupport.stream(indexedKeySpliterator(), true); - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java index 5664a900170..b277789c717 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java @@ -25,7 +25,6 @@ import org.apache.jena.shared.JenaException; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Predicate; @@ -36,7 +35,7 @@ * * @param the element type */ -public abstract class HashCommonBase { +public abstract class HashCommonBase implements JenaMapSetCommon { /** * Jeremy suggests, from his experiments, that load factors more than * 0.6 leave the table too dense, and little advantage is gained below 0.4. @@ -78,7 +77,7 @@ protected HashCommonBase(int initialCapacity) { * Copy constructor. * The new table will contain all the same keys of the table to copy. * - * @param baseToCopy + * @param baseToCopy the table to copy */ protected HashCommonBase(final HashCommonBase baseToCopy) { this.keys = newKeysArray(baseToCopy.keys.length); @@ -209,18 +208,10 @@ public boolean anyMatch(final Predicate predicate) { } public ExtendedIterator keyIterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(keys, checkForConcurrentModification); + return new SparseArrayIterator<>(keys, this); } public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(keys, checkForConcurrentModification); + return new SparseArraySpliterator<>(keys, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java index 62e7bd56733..dcdd5557654 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java @@ -24,7 +24,6 @@ import org.apache.jena.mem.spliterator.SparseArraySpliterator; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Supplier; import java.util.function.UnaryOperator; @@ -207,19 +206,11 @@ protected void removeFrom(int here) { @Override public ExtendedIterator valueIterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(values, checkForConcurrentModification); + return new SparseArrayIterator<>(values, this); } @Override public Spliterator valueSpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(values, checkForConcurrentModification); + return new SparseArraySpliterator<>(values, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java index 3e13613b08f..6d2423e0097 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java @@ -30,6 +30,7 @@ /** * A map from keys of type {@code K} to values of type {@code V}. + * Not thread-safe and does not allow {@code null} keys. * * @param the type of the keys in the map * @param the type of the values in the map diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java new file mode 100644 index 00000000000..67c366d00eb --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.mem.collection; + +/** + * Extension of {@link JenaMap} that exposes index-based access and lets callers + * supply a precomputed hash code for the key. Indices are stable handles to + * entries (returned by {@link #putAndGetIndex(Object, Object)}) and remain + * valid until the corresponding entry is removed. + * + * The hash-code overloads are a performance shortcut for callers that already + * have the hash at hand (typically because the same key is stored in several + * collections). The supplied hash code MUST equal {@code key.hashCode()}, or + * the map will misbehave. + * + * @param the type of the keys in the map + * @param the type of the values in the map + */ +public interface JenaMapIndexed extends JenaMap { + + /** + * Returns the index of the entry with the given key, or a negative value + * if no such entry exists. + * + * @param key the key to look up + * @return the index of the entry, or a negative value if absent + */ + int indexOf(K key); + + /** + * Returns the key stored at the given index. + * + * @param index the index of the entry + * @return the key at that index + */ + K getKeyAt(int index); + + /** + * Returns the value stored at the given index. + * + * @param index the index of the entry + * @return the value at that index + */ + V getValueAt(int index); + + /** + * Put a key-value pair and return the index of the affected entry. + * If the key is already present, its value is updated and the existing + * index is returned. + * + * @param key the key to put + * @param value the value to put + * @return the index of the entry holding {@code key} + */ + int putAndGetIndex(K key, V value); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java index 2533714ce6b..7f96baa19f9 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java @@ -28,22 +28,23 @@ import java.util.stream.StreamSupport; /** - * Common interface for {@link JenaMap} and {@link JenaSet}. * + * Operations shared between the map ({@link JenaMap}) and the set + * ({@link JenaSet}) collections used in the {@code mem} triple store + * implementations. + * + * These collections trade some flexibility for speed: they expose only the + * operations needed by triple-store internals (no full {@link java.util.Map} + * or {@link java.util.Set} contract). They are not thread-safe. * - * @param the type of the keys/elements in the collection + * @param the type of the keys (or elements, for sets) in the collection */ -public interface JenaMapSetCommon { +public interface JenaMapSetCommon extends Sized { /** * Clear the collection. */ void clear(); - /** - * @return the number of elements in the collection - */ - int size(); - /** * @return true if the collection is empty */ @@ -75,7 +76,10 @@ public interface JenaMapSetCommon { /** * Removes a key from the collection. - * Attention: Implementations may assume that the key is present. + * + * Attention: implementations may assume the key is present and may produce + * undefined behavior (including silently corrupting internal state) if it + * is not. Use {@link #tryRemove(Object)} when in doubt. * * @param key the key to remove */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java index d3b8a557be9..03848073f56 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java @@ -21,9 +21,10 @@ package org.apache.jena.mem.collection; /** - * Set interface specialized for the use cases in triple store implementations. + * Set interface specialized for the use cases in triple-store implementations. + * Not thread-safe; does not allow {@code null} elements. * - * @param + * @param the element type of the set */ public interface JenaSet extends JenaMapSetCommon { @@ -31,13 +32,16 @@ public interface JenaSet extends JenaMapSetCommon { * Add the key to the set if it is not already present. * * @param key the key to add - * @return true if the key was added, false if it was already present + * @return {@code true} if the key was added, {@code false} if it was already present */ boolean tryAdd(E key); /** - * Add the key to the set without checking if it is already present. - * Attention: This method must only be used if it is guaranteed that the key is not already present. + * Add the key to the set without checking whether it is already present. + * + * Attention: this method must only be used if the caller has ensured that + * the key is not already in the set; otherwise the set's invariants will + * break (duplicates may be inserted). * * @param key the key to add */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java index 8cc8aad8daf..0e1d032b356 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java @@ -22,17 +22,50 @@ /** - * Extension of {@link JenaSet} that allows to add and remove elements - * with a given hash code. - * This is useful if the hash code is already known. - * Attention: The hash code must be consistent with E::hashCode(). + * Extension of {@link JenaSet} that lets callers supply a precomputed hash + * code. + * + * Attention: any caller-supplied hash code MUST equal {@code E.hashCode()}; + * if it does not, the set will misbehave. + * + * @param the element type of the set */ public interface JenaSetHashOptimized extends JenaSet { + + /** + * Add an element with the given precomputed hash code if it is not + * already present. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + * @return {@code true} if added, {@code false} if already present + */ boolean tryAdd(E key, int hashCode); + /** + * Add an element with the given precomputed hash code without checking + * whether it is already present. The caller MUST ensure the key is absent. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + */ void addUnchecked(E key, int hashCode); + /** + * Try to remove an element with the given precomputed hash code. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + * @return {@code true} if removed, {@code false} if it was not present + */ boolean tryRemove(E key, int hashCode); + /** + * Remove an element assumed to be present, with the given precomputed + * hash code. Behavior is undefined if the element is not in the set. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + */ void removeUnchecked(E key, int hashCode); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java new file mode 100644 index 00000000000..c7c3d2e1ddb --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.mem.collection; + + +/** + * Extension of {@link JenaSetHashOptimized} that exposes index-based access to elements. + * Indices are stable handles to entries (returned by {@link #addAndGetIndex(Object)}) and remain + * valid until the corresponding entry is removed. + * + * @param the element type of the set + */ +public interface JenaSetIndexed extends JenaSetHashOptimized { + + /** + * Add an element and return the index it was stored at. If the element + * is already present, returns a negative value (typically the bitwise + * complement of the existing index). + * + * @param key the element to add + * @return the index of the inserted element, or a negative value if the + * element was already present + */ + int addAndGetIndex(final E key); + + /** + * Returns the element stored at the given index. + * + * @param index the index to read + * @return the element at that index + */ + E getKeyAt(int index); + + /** + * Returns the index of the given element, or a negative value if it is + * not in the set. + * + * @param key the element to look up + * @return the index of {@code key}, or a negative value if absent + */ + int indexOf(E key); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java new file mode 100644 index 00000000000..237740ce8e3 --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.mem.collection; + +/** + * Base interface for sized collections. + * It is typically used to detect concurrent modifications in iterators and spliterators + * by snapshotting the size at construction time and rechecking it at each advance/forEach boundary. + */ +public interface Sized { + + /** + * @return the number of elements in the collection + */ + int size(); +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java index b0ac6e994bb..8cfc8948a25 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java @@ -30,16 +30,27 @@ import java.util.function.Consumer; /** - * Iterator that iterates over the entries of sets which are contained in the given iterator of sets. + * Flat-map style iterator that yields every element of every {@link JenaSet} + * produced by the given parent iterator. Empty inner sets are silently + * skipped. Equivalent in spirit to a one-level {@code flatMap} but tailored + * to the {@link JenaSet} API and to {@link NiceIterator}. * - * @param the type of the elements + * @param the element type of the inner sets */ public class IteratorOfJenaSets extends NiceIterator { - final Iterator extends JenaSet> parentIterator; + /** Source iterator producing the sets to flatten. */ + private final Iterator extends JenaSet> parentIterator; - ExtendedIterator currentIterator; + /** Iterator over the keys of the set currently being consumed. */ + private ExtendedIterator currentIterator; + /** + * Create a flat iterator over the elements of every set produced by + * {@code parentIterator}. + * + * @param parentIterator the source iterator of sets + */ public IteratorOfJenaSets(Iterator extends JenaSet> parentIterator) { this.parentIterator = parentIterator; this.currentIterator = parentIterator.hasNext() diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java deleted file mode 100644 index 37f103eae25..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.iterator; - -import org.apache.jena.mem.collection.FastHashSet; -import org.apache.jena.util.iterator.NiceIterator; - -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.function.Consumer; - -/** - * An iterator over a sparse array, that skips null entries. - * This iterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * The iterator works in ascending order, starting from index 0 up to the specified exclusive index. - * - * This iterator will check for concurrent modifications by invoking a {@link Runnable} - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedIterator extends NiceIterator> implements Iterator> { - - private final E[] entries; - private final Runnable checkForConcurrentModification; - private int pos = 0; - private final int toIndexExclusive; - private boolean hasNext = false; - - public SparseArrayIndexedIterator(final E[] entries, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = entries.length; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - public SparseArrayIndexedIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Returns {@code true} if the iteration has more elements. - * (In other words, returns {@code true} if {@link #next} would - * return an element rather than throwing an exception.) - * - * @return {@code true} if the iteration has more elements - */ - @Override - public boolean hasNext() { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - hasNext = true; - return true; - } - pos++; - } - hasNext = false; - return false; - } - - /** - * Returns the next element in the iteration. - * - * @return the next element in the iteration - * @throws NoSuchElementException if the iteration has no more elements - */ - @Override - public FastHashSet.IndexedKey next() { - this.checkForConcurrentModification.run(); - if (hasNext || hasNext()) { - hasNext = false; - return new FastHashSet.IndexedKey<>(pos, entries[pos++]); - } - throw new NoSuchElementException(); - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - action.accept(new FastHashSet.IndexedKey<>(pos, entries[pos])); - } - pos++; - } - this.checkForConcurrentModification.run(); - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java index 936476a80ff..e0b79cd1ff6 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java @@ -21,34 +21,55 @@ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.Sized; import org.apache.jena.util.iterator.NiceIterator; -import java.util.Iterator; +import java.util.ConcurrentModificationException; import java.util.NoSuchElementException; import java.util.function.Consumer; /** - * An iterator over a sparse array, that skips null entries. + * Iterator over a sparse array, walking from high index to low and skipping + * {@code null} entries. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it on each call to + * {@link #next()} / {@link #forEachRemaining(Consumer)}; throws + * {@link ConcurrentModificationException} if the size has changed. * * @param the type of the array elements */ -public class SparseArrayIterator extends NiceIterator implements Iterator { +public class SparseArrayIterator extends NiceIterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; private boolean hasNext = false; - public SparseArrayIterator(final E[] entries, final Runnable checkForConcurrentModification) { + /** + * Iterate over the whole array. + * + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, final Sized set) { this.entries = entries; this.pos = entries.length - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } - public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { + /** + * Iterate over {@code entries[0 .. toIndexExclusive)} (in reverse order). + * + * @param entries the backing array (not copied) + * @param toIndexExclusive exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Sized set) { this.entries = entries; this.pos = toIndexExclusive - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** @@ -62,13 +83,11 @@ public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnab public boolean hasNext() { while (-1 < pos) { if (null != entries[pos]) { - hasNext = true; - return true; + return hasNext = true; } pos--; } - hasNext = false; - return false; + return hasNext = false; } /** @@ -79,7 +98,7 @@ public boolean hasNext() { */ @Override public E next() { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (hasNext || hasNext()) { hasNext = false; return entries[pos--]; @@ -95,6 +114,6 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java index 94008b155f1..d8536f56311 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java @@ -22,8 +22,17 @@ package org.apache.jena.mem.pattern; /** - * A pattern for matching triples. - * The pattern is defined by the wildcard positions for the subject, predicate and object. + * Categorizes a triple-match pattern by which of the subject, predicate and + * object slots are concrete and which are wildcards (i.e. {@code Node.ANY} + * or {@code null}). + * + * The eight enum values cover every possible combination. Triple-store + * implementations dispatch on this enum to pick the most efficient lookup + * path for each kind of pattern (e.g. a fully concrete {@link #SUB_PRE_OBJ} + * is answered directly from the triple set, while a partially open pattern + * such as {@link #ANY_PRE_OBJ} is answered through an index intersection). + * + * @see PatternClassifier */ public enum MatchPattern { /** diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java index 32a6ba182a1..e4cf5644eca 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java @@ -25,14 +25,15 @@ import org.apache.jena.graph.Triple; /** - * Classify a triple match into one of the 8 match patterns. + * Utility class that classifies a triple match into one of the eight + * {@link MatchPattern} values. * - * The classification is based on the concrete-ness of the subject, predicate and object. - * A concrete node is one that is not a variable. + * The classification is based on which of the subject, predicate and object + * are concrete (anything that is not a variable / wildcard / + * {@code null}) and which are wildcards. The result is used by triple-store + * implementations to dispatch to the most efficient lookup path. * - * The classification is used to select the most efficient implementation of a triple store. - * - * This is a utility class; there is no need to instantiate it. + * All operations are stateless; this class is not meant to be instantiated. * * @see MatchPattern */ @@ -41,8 +42,16 @@ public class PatternClassifier { private PatternClassifier() { } + /** + * Classify a triple match. + * + * @param tripleMatch the match triple, possibly containing wildcard nodes + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Triple tripleMatch) { - if (tripleMatch.isConcrete()) { + if (tripleMatch.getSubject().isConcrete() + && tripleMatch.getPredicate().isConcrete() + && tripleMatch.getObject().isConcrete()) { return MatchPattern.SUB_PRE_OBJ; } else { if (tripleMatch.getSubject().isConcrete()) { @@ -73,6 +82,15 @@ public static MatchPattern classify(Triple tripleMatch) { } } + /** + * Classify a triple match given as three nodes. + * Any {@code null} or non-concrete node is treated as a wildcard. + * + * @param sm subject node, or {@code null}/wildcard + * @param pm predicate node, or {@code null}/wildcard + * @param om object node, or {@code null}/wildcard + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Node sm, Node pm, Node om) { if (null != sm && sm.isConcrete()) { if (null != pm && pm.isConcrete()) { @@ -103,6 +121,5 @@ public static MatchPattern classify(Node sm, Node pm, Node om) { } } } - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java index 43bbfeeaea8..a5033c22cde 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java @@ -21,52 +21,57 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a contiguous array slice {@code [0, toIndex)}, + * iterating from high index to low. Supports splitting into + * {@link ArraySubSpliterator} children for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySpliterator implements Spliterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public ArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (-1 < --pos) { action.accept(entries[pos]); return true; @@ -79,7 +84,7 @@ public void forEachRemaining(Consumer super E> action) { while (-1 < --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -89,7 +94,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -101,4 +106,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java index 74994708b53..638f2bb0c9e 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java @@ -21,55 +21,61 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a contiguous array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low. Produced by splitting an + * {@link ArraySpliterator} (or another {@link ArraySubSpliterator}); supports + * further recursive splits for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public ArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (fromIndex <= --pos) { action.accept(entries[pos]); return true; @@ -82,7 +88,7 @@ public void forEachRemaining(Consumer super E> action) { while (fromIndex <= --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -93,7 +99,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -105,4 +111,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java deleted file mode 100644 index 704c9642706..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.spliterator; - -import java.util.Spliterator; -import java.util.function.Consumer; - -import org.apache.jena.mem.collection.FastHashSet; - -/** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * This spliterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * This spliterator works in ascending order, starting from the given start up to the specified exclusive index. - * - * This spliterator supports splitting into sub-spliterators. - * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedSpliterator implements Spliterator> { - - private final E[] entries; - private int currentPositionMinusOne; - private final int toIndexExclusive; - private final Runnable checkForConcurrentModification; - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param fromIndexInclusive the index of the first element, inclusive - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int fromIndexInclusive, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.currentPositionMinusOne = fromIndexInclusive-1; // Start at fromIndexInclusive - 1, so that the first call to tryAdvance will increment pos to fromIndexInclusive - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this(entries, 0, toIndexExclusive, checkForConcurrentModification); - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); - } - - - @Override - public boolean tryAdvance(Consumer super FastHashSet.IndexedKey> action) { - this.checkForConcurrentModification.run(); - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - return true; - } - } - return false; - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - } - } - this.checkForConcurrentModification.run(); - } - - @Override - public Spliterator> trySplit() { - final var nextPos = currentPositionMinusOne + 1; - final var remaining = toIndexExclusive - nextPos; - if ( remaining < 2) { - return null; - } - final var mid = nextPos + ( remaining >>> 1); - final var fromIndexInclusive = nextPos; - this.currentPositionMinusOne = mid-1; - return new SparseArrayIndexedSpliterator<>(entries, fromIndexInclusive, mid, checkForConcurrentModification); - } - - @Override - public long estimateSize() { return (long) toIndexExclusive - currentPositionMinusOne; } - - @Override - public int characteristics() { - return DISTINCT | NONNULL | IMMUTABLE; - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java index 6752cc9a1c1..add45739dc2 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java @@ -21,17 +21,24 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a sparse array slice {@code [0, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced for backing arrays such as those of + * {@link org.apache.jena.mem.collection.FastHashBase}, where removed slots + * are represented by {@code null}. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Supports splitting into {@link SparseArraySubSpliterator} children for + * parallel traversal. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it at each + * advance/forEach boundary; throws {@link ConcurrentModificationException} + * if the size has changed. * * @param the type of the array elements */ @@ -39,35 +46,37 @@ public class SparseArraySpliterator implements Spliterator { private final E[] entries; private int pos; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}, skipping nulls. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public SparseArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (-1 < --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -86,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -96,7 +105,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java index 3eb0784326f..d79242ac78c 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java @@ -21,55 +21,62 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a sparse array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced by splitting a {@link SparseArraySpliterator} (or another + * {@link SparseArraySubSpliterator}); supports further recursive splits for + * parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary; + * throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the type of the array elements */ public class SparseArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}, skipping nulls. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public SparseArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (fromIndex <= --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -88,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -99,7 +106,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java index 07ccc9634a9..f0fba805175 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java @@ -32,26 +32,41 @@ import java.util.function.Predicate; /** - * An ArrayBunch implements TripleBunch with a linear search of a short-ish - * array of Triples. The array grows by factor 2. + * Linear-scan implementation of {@link FastTripleBunch} backed by a packed + * {@link Triple} array. Used as long as a bunch stays small; once it grows + * past the configured threshold (see {@link FastTripleStore}) it is replaced + * with a {@link FastHashedTripleBunch}. + * + * The array grows by a factor of two when full. Equality of triples within a + * bunch is delegated to {@link #areEqual(Triple, Triple)}, which subclasses + * specialize to compare only the two nodes that are not already + * implied by the enclosing map's key. This avoids redundant equality checks + * on the shared subject/predicate/object. + * + * Not thread-safe. */ public abstract class FastArrayBunch implements FastTripleBunch { private static final int INITIAL_SIZE = 4; + /** Number of valid entries in {@link #elements}. */ protected int size = 0; + /** Packed array of triples; entries from {@code 0} to {@code size-1} are live. */ protected Triple[] elements; + /** + * Creates an empty bunch with the default initial capacity. + */ protected FastArrayBunch() { elements = new Triple[INITIAL_SIZE]; } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. - * But it will reserve only the space needed to contain them. Growing is still possible. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}; its backing array is sized to fit exactly, + * but can grow further if needed. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -59,7 +74,17 @@ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.size = bunchToCopy.size; } - public abstract boolean areEqual(final Triple a, final Triple b); + /** + * Compare two triples for equality within this bunch. + * + * Subclasses specialize this to skip the already-shared component + * (subject, predicate or object) and compare only the remaining two. + * + * @param a first triple + * @param b second triple + * @return {@code true} if the triples are considered equal in this bunch + */ + protected abstract boolean areEqual(final Triple a, final Triple b); @Override public boolean containsKey(Triple t) { @@ -127,6 +152,7 @@ public boolean tryRemove(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return true; } } @@ -138,6 +164,7 @@ public void removeUnchecked(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return; } } @@ -174,11 +201,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java index b89d3312048..a49d6b54009 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java @@ -25,21 +25,28 @@ import org.apache.jena.mem.collection.FastHashMap; /** - * Map from nodes to triple bunches. + * {@link FastHashMap} specialized to map a {@link Node} to its associated + * {@link FastTripleBunch}. Used by {@link FastTripleStore} to maintain the + * three subject/predicate/object indices. */ public class FastHashedBunchMap extends FastHashMap implements Copyable { + /** + * Creates an empty bunch map with the default initial capacity. + */ public FastHashedBunchMap() { super(); } /** - * Copy constructor. - * The new map will contain all the same nodes as keys of the map to copy, but copies of the bunches as values . + * Copy constructor. The new map has the same node keys as + * {@code mapToCopy}; each value is replaced by a deep copy of the + * corresponding bunch (via {@link FastTripleBunch#copy()}) so that + * mutations of either map cannot affect the other. * - * @param mapToCopy + * @param mapToCopy the source map */ private FastHashedBunchMap(final FastHashedBunchMap mapToCopy) { super(mapToCopy, FastTripleBunch::copy); diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java index 459e78c8181..65c9ab70fbf 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java @@ -25,13 +25,21 @@ import org.apache.jena.mem.collection.JenaSet; /** - * A set of triples - backed by {@link FastHashSet}. + * Hashed implementation of {@link FastTripleBunch} built on top of + * {@link FastHashSet}. Used by {@link FastTripleStore} once a bunch grows + * past the size threshold at which a linear-scan {@link FastArrayBunch} + * stops being faster. */ public class FastHashedTripleBunch extends FastHashSet implements FastTripleBunch { + /** - * Create a new triple bunch from the given set of triples. + * Create a new hashed bunch pre-populated from the given set of triples. + * The initial capacity is chosen at 1.5x the source size, so the new bunch + * fits the existing triples and has some headroom for growth before it + * needs to rehash. * - * @param set the set of triples + * @param set the source set of triples (typically the array bunch being + * promoted) */ public FastHashedTripleBunch(final JenaSet set) { super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth @@ -39,15 +47,18 @@ public FastHashedTripleBunch(final JenaSet set) { } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ private FastHashedTripleBunch(final FastHashedTripleBunch bunchToCopy) { super(bunchToCopy); } + /** + * Creates an empty hashed bunch with the default initial capacity. + */ public FastHashedTripleBunch() { super(); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java index 68f79e72f8a..fe050283188 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java @@ -29,27 +29,39 @@ import java.util.function.Predicate; /** - * A bunch of triples - a stripped-down set with specialized methods. A - * bunch is expected to store triples that share some useful property - * (such as having the same subject or predicate). + * Set-like container for a "bunch" of triples that share some useful + * property - typically they all have the same subject, predicate or object, + * because the bunch is the value of a node-keyed map in a + * {@link FastTripleStore}. + * + * The interface is a stripped-down set with a few extras tuned for the + * triple-store hot path; concrete implementations are + * {@link FastArrayBunch} (linear scan, used while the bunch is small) and + * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a + * threshold). */ public interface FastTripleBunch extends JenaSetHashOptimized, Copyable { /** - * Answer true iff this bunch is implemented as an array. - * This field is used to optimize some operations by avoiding the need for instanceOf tests. + * Answer {@code true} iff this bunch is backed by a flat array (i.e. is + * a {@link FastArrayBunch}). Exposed as an explicit method so callers can + * avoid {@code instanceof} checks on this hot path. * - * @return true iff this bunch is implemented as an arrays + * @return {@code true} if this bunch is array-backed */ boolean isArray(); /** - * This method is used to optimize _PO match operations. - * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches. - * This method is faster if there are many matches and the set is ordered in an unfavorable way. - * _PO matches usually fall into this category. + * Predicate test that scans elements in hash-table order rather than + * dense insertion order. Tuned for {@code _PO} (any-predicate-object) + * matches. + * + * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
- * This is an implementation of Knuth's Algorithm R from tAoCP vol3, p 527, - * with exchanging of the roles of i and j so that they can be usefully renamed - * to here and scan. + * Removes the entry referenced by the {@code positions} slot at index + * {@code here} and rehashes the affected probe chain. *
- * It relies on linear probing but doesn't require a distinguished REMOVED - * value. Since we resize the table when it gets fullish, we don't worry [much] - * about the overhead of the linear probing. + * This is an implementation of Knuth's Algorithm R from The Art of + * Computer Programming, vol. 3, p. 527, with the roles of {@code i} + * and {@code j} swapped so they can be usefully renamed to here + * and scan. *
+ * It relies on linear probing but doesn't require a distinguished + * {@code REMOVED} sentinel. Since the table is resized once it gets + * fullish, the overhead of linear probing is not a concern. * - * @param here the index in the positions array + * @param here the index in the {@link #positions} array of the slot to clear */ protected void removeFrom(int here) { final var pIndex = ~positions[here]; @@ -345,9 +374,14 @@ public final boolean containsKey(K o) { } /** - * Attentions: Due to the ordering of the keys, this method may be slow - * if matching elements are at the start of the list. - * Try to use {@link #anyMatchRandomOrder(Predicate)} instead. + * {@inheritDoc} + *
+ * Iterates the keys in dense (insertion-order-ish) order. This is fast when + * matches are rare or expected near the end of the array, but can be slow + * when matches are clustered at the start of the array. For workloads + * where many matches are expected, prefer {@link #anyMatchRandomOrder(Predicate)}, + * which scans in probe-table order and tends to find matches sooner when + * they are abundant. */ @Override public final boolean anyMatch(Predicate predicate) { @@ -362,11 +396,16 @@ public final boolean anyMatch(Predicate predicate) { } /** - * This method can be faster than {@link #anyMatch(Predicate)} if one expects - * to find many matches. But it is slower if one expects to find no matches or just a single one. + * Like {@link #anyMatch(Predicate)} but scans the probe table rather than + * the dense {@code keys} array, yielding a roughly hash-based order. + * + * This is faster than {@link #anyMatch(Predicate)} when many matches are + * expected (the predicate is more likely to short-circuit early), but + * slower when no or only a single match exists (each iteration must + * test against an empty slot first). * - * @param predicate the predicate to apply to elements of this collection - * @return {@code true} if any element of the collection matches the predicate + * @param predicate the predicate to apply + * @return {@code true} if any element matches the predicate */ public final boolean anyMatchRandomOrder(Predicate predicate) { var pIndex = positions.length - 1; @@ -381,14 +420,22 @@ public final boolean anyMatchRandomOrder(Predicate predicate) { @Override public final ExtendedIterator keyIterator() { - final var initialSize = size(); - final Runnable checkForConcurrentModification = () -> - { - if (size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(keys, keysPos, checkForConcurrentModification); + return new SparseArrayIterator<>(keys, keysPos, this); } + /** + * Locates the slot in {@link #positions} that holds {@code e} (with the + * given precomputed hash code). + * + * If the key is present, returns the (non-negative) probe-table slot + * index. If the key is absent, returns the bitwise complement of the + * empty probe-table slot at which the key would be inserted, allowing + * insertion to proceed without a second probe walk. + * + * @param e the key to locate + * @param hashCode {@code e.hashCode()} + * @return the position index if found, or {@code ~insertionPosition} if not + */ protected final int findPosition(final K e, final int hashCode) { var pIndex = calcStartIndexByHashCode(hashCode); while (true) { @@ -405,6 +452,15 @@ protected final int findPosition(final K e, final int hashCode) { } } + /** + * Locates the next empty slot in {@link #positions} along the probe chain + * for the given hash code, without checking any existing entries for + * equality. Used after a positions-array resize, when no duplicates can + * exist in the rebuilt table. + * + * @param hashCode the hash code being placed + * @return the index of an empty slot in the probe table + */ protected final int findEmptySlotWithoutEqualityCheck(final int hashCode) { var pIndex = calcStartIndexByHashCode(hashCode); while (true) { @@ -435,11 +491,63 @@ public void clear() { @Override public final Spliterator keySpliterator() { - final var initialSize = this.size(); - final Runnable checkForConcurrentModification = () -> - { - if (this.size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(keys, keysPos, checkForConcurrentModification); + return new SparseArraySpliterator<>(keys, keysPos, this); + } + + /** + * Gets the key at the given index. + * Array bounds are not checked. The caller must ensure the index is valid and corresponds to a non-null key. + * + * @param i the index + * @return the key at the given index + */ + public K getKeyAt(int i) { + return keys[i]; + } + + /** + * Returns the index of the entry holding {@code key}, or {@code -1} if not present. + * + * @param key the key to look up + * @return the entry index, or {@code -1} if the key is absent + */ + public int indexOf(K key) { + final var pIndex = findPosition(key, key.hashCode()); + if (pIndex < 0) { + return -1; + } else { + return ~positions[pIndex]; + } + } + + /** + * Functional interface used by {@link #forEachKey} to receive each live + * key along with the stable index it occupies. + * + * @param the key type + */ + @FunctionalInterface + public interface KeyAndIndexConsumer { + /** + * Receive a single key and its index. + * + * @param key the key + * @param index the stable index of the key in the underlying array + */ + void accept(K key, int index); + } + + /** + * Sequentially invokes {@code consumer} for every live key with its index. + * Skips freed slots. + * + * @param consumer receives each key/index pair + */ + public void forEachKey(KeyAndIndexConsumer consumer) { + for (int i = 0; i < keysPos; i++) { + if(keys[i] != null) { + consumer.accept(keys[i], i); + } + } } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java index 04c2761416b..e3f741ba485 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java @@ -25,39 +25,56 @@ import org.apache.jena.mem.spliterator.SparseArraySpliterator; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Supplier; import java.util.function.UnaryOperator; /** - * Map which grows, if needed but never shrinks. - * This map does not guarantee any order. Although due to the way it is implemented the elements have a certain order. - * This map does not allow null keys. - * This map is not thread safe. - * It´s purpose is to support fast add, remove, contains and stream / iterate operations. - * Only remove operations are not as fast as in {@link java.util.HashMap} - * Iterating over this map does not get much faster again after removing elements because the map is not compacted. + * Hash map specialization built on top of {@link FastHashBase}. + * Grows on demand but never shrinks, does not guarantee iteration order, + * does not allow {@code null} keys, and is not thread-safe. + * + * Optimized for fast {@code add} / {@code containsKey} / {@code stream} / + * iterate operations. Removal is somewhat slower than in + * {@link java.util.HashMap} because of the back-shifting performed on the + * probe table. Iteration speed does not recover after many removals because + * the dense {@code keys} array is not compacted. + * + * @param the key type + * @param the value type */ -public abstract class FastHashMap extends FastHashBase implements JenaMap { +public abstract class FastHashMap extends FastHashBase implements JenaMapIndexed { + /** + * Parallel array to {@code keys} holding the value associated with each + * stored key. {@code values[i]} is the value for {@code keys[i]} when + * {@code keys[i]} is non-null. + */ protected V[] values; + /** + * Creates a map with the given initial key-array capacity. + * + * @param initialSize the initial capacity of the keys/values arrays + */ protected FastHashMap(int initialSize) { super(initialSize); this.values = newValuesArray(keys.length); } + /** + * Creates a map with the default initial capacity. + */ protected FastHashMap() { super(); this.values = newValuesArray(keys.length); } /** - * Copy constructor. - * The new map will contain all the same keys and values of the map to copy. + * Copy constructor. The new map contains the same keys and the same + * value references as {@code mapToCopy}. * - * @param mapToCopy + * @param mapToCopy the source map */ protected FastHashMap(final FastHashMap mapToCopy) { super(mapToCopy); @@ -66,10 +83,13 @@ protected FastHashMap(final FastHashMap mapToCopy) { } /** - * Copy constructor with value processor. + * Copy constructor that transforms each value via {@code valueProcessor}. + * Useful when the values are mutable and need to be deep-copied to keep + * the new map independent from the source. * - * @param mapToCopy - * @param valueProcessor + * @param mapToCopy the source map + * @param valueProcessor function applied to every non-null value to obtain + * the value to put in the new map */ protected FastHashMap(final FastHashMap mapToCopy, final UnaryOperator valueProcessor) { super(mapToCopy); @@ -82,6 +102,12 @@ protected FastHashMap(final FastHashMap mapToCopy, final UnaryOperator } } + /** + * Gets a new array of values with the given size. + * + * @param size the size of the array + * @return the new array + */ protected abstract V[] newValuesArray(int size); @Override @@ -106,12 +132,10 @@ public void clear() { @Override public boolean tryPut(K key, V value) { + growPositionsArrayIfNeeded(); final var hashCode = key.hashCode(); - var pIndex = findPosition(key, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { - if (tryGrowPositionsArrayIfNeeded()) { - pIndex = findPosition(key, hashCode); - } final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; values[eIndex] = value; @@ -126,12 +150,10 @@ public boolean tryPut(K key, V value) { @Override public void put(K key, V value) { + growPositionsArrayIfNeeded(); final var hashCode = key.hashCode(); - var pIndex = findPosition(key, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { - if (tryGrowPositionsArrayIfNeeded()) { - pIndex = findPosition(key, hashCode); - } final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; values[eIndex] = value; @@ -142,8 +164,27 @@ public void put(K key, V value) { } } + @Override + public int putAndGetIndex(K key, V value) { + growPositionsArrayIfNeeded(); + final int hashCode = key.hashCode(); + final var pIndex = findPosition(key, hashCode); + final int eIndex; + if (pIndex < 0) { + eIndex = getFreeKeyIndex(); + keys[eIndex] = key; + hashCodesOrDeletedIndices[eIndex] = hashCode; + positions[~pIndex] = ~eIndex; + } else { + eIndex = ~positions[pIndex]; + } + values[eIndex] = value; + return eIndex; + } + /** * Returns the value at the given index. + * Array bounds are not checked. The caller must ensure the index is valid and corresponds to a non-null key. * * @param i index * @return value @@ -178,12 +219,12 @@ public V computeIfAbsent(K key, Supplier absentValueSupplier) { var pIndex = findPosition(key, hashCode); if (pIndex < 0) { if (tryGrowPositionsArrayIfNeeded()) { - pIndex = findPosition(key, hashCode); + pIndex = ~findEmptySlotWithoutEqualityCheck(hashCode); } + final var value = absentValueSupplier.get(); final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; - final var value = absentValueSupplier.get(); values[eIndex] = value; positions[~pIndex] = ~eIndex; return value; @@ -194,18 +235,20 @@ public V computeIfAbsent(K key, Supplier absentValueSupplier) { @Override public void compute(K key, UnaryOperator valueProcessor) { - final int hashCode = key.hashCode(); + final var hashCode = key.hashCode(); var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var value = valueProcessor.apply(null); if (value == null) return; + if(tryGrowPositionsArrayIfNeeded()) { + pIndex = ~findEmptySlotWithoutEqualityCheck(hashCode); + } final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; values[eIndex] = value; positions[~pIndex] = ~eIndex; - tryGrowPositionsArrayIfNeeded(); } else { var eIndex = ~positions[pIndex]; final var value = valueProcessor.apply(values[eIndex]); @@ -217,24 +260,13 @@ public void compute(K key, UnaryOperator valueProcessor) { } } - @Override public ExtendedIterator valueIterator() { - final var initialSize = size(); - final Runnable checkForConcurrentModification = () -> - { - if (size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(values, keysPos, checkForConcurrentModification); + return new SparseArrayIterator<>(values, keysPos, this); } @Override public Spliterator valueSpliterator() { - final var initialSize = this.size(); - final Runnable checkForConcurrentModification = () -> - { - if (this.size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(values, keysPos, checkForConcurrentModification); + return new SparseArraySpliterator<>(values, keysPos, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java index 134a0092e22..5adf3232c4e 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java @@ -21,39 +21,42 @@ package org.apache.jena.mem.collection; -import org.apache.jena.mem.iterator.SparseArrayIndexedIterator; -import org.apache.jena.mem.spliterator.SparseArrayIndexedSpliterator; -import org.apache.jena.util.iterator.ExtendedIterator; - -import java.util.ConcurrentModificationException; -import java.util.Spliterator; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - /** - * Set which grows, if needed but never shrinks. - * This set does not guarantee any order. Although due to the way it is implemented the elements have a certain order. - * This set does not allow null values. - * This set is not thread safe. - * It´s purpose is to support fast add, remove, contains and stream / iterate operations. - * Only remove operations are not as fast as in {@link java.util.HashSet} - * Iterating over this set not get much faster again after removing elements because the set is not compacted. + * Hash set specialization built on top of {@link FastHashBase}. + * Grows on demand but never shrinks, does not guarantee iteration order, + * does not allow {@code null} elements, and is not thread-safe. + * + * Optimized for fast {@code add} / {@code containsKey} / {@code stream} / + * iterate operations. Removal is somewhat slower than in + * {@link java.util.HashSet} because of the back-shifting performed on the + * probe table. Iteration speed does not recover after many removals because + * the dense {@code keys} array is not compacted. + * + * @param the element type */ -public abstract class FastHashSet extends FastHashBase implements JenaSetHashOptimized { +public abstract class FastHashSet extends FastHashBase implements JenaSetIndexed { - protected FastHashSet(int initialSize) { + /** + * Creates a set with the given initial key-array capacity. + * + * @param initialSize the initial capacity of the keys array + */ + public FastHashSet(final int initialSize) { super(initialSize); } - protected FastHashSet() { + /** + * Creates a set with the default initial capacity. + */ + public FastHashSet() { super(); } /** - * Copy constructor. - * The new set will contain all the same keys of the set to copy. + * Copy constructor. The new set contains the same elements as + * {@code setToCopy}. * - * @param setToCopy + * @param setToCopy the source set */ protected FastHashSet(final FastHashSet setToCopy) { super(setToCopy); @@ -65,12 +68,12 @@ public boolean tryAdd(K key) { } @Override - public boolean tryAdd(K value, int hashCode) { + public boolean tryAdd(K key, int hashCode) { growPositionsArrayIfNeeded(); - var pIndex = findPosition(value, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var eIndex = getFreeKeyIndex(); - keys[eIndex] = value; + keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; positions[~pIndex] = ~eIndex; return true; @@ -79,28 +82,23 @@ public boolean tryAdd(K value, int hashCode) { } /** - * Add and get the index of the added element. + * Add an element and return the index it was stored at. + * If the element is already present, returns the bitwise complement + * ({@code ~existingIndex}) of the existing index, so callers can + * distinguish "newly inserted" from "already present" while still + * recovering the index in both cases. * - * @param value the value to add - * @return the index of the added element or the inverse (~) index of the existing element + * @param key the element to add + * @return the new index, or {@code ~existingIndex} if already present */ - public int addAndGetIndex(K value) { - return addAndGetIndex(value, value.hashCode()); - } - - /** - * Add and get the index of the added element. - * - * @param value the value to add - * @param hashCode the hash code of the value. This is a performance optimization. - * @return the index of the added element or the inverse (~) index of the existing element - */ - public int addAndGetIndex(final K value, final int hashCode) { + @Override + public int addAndGetIndex(K key) { growPositionsArrayIfNeeded(); - final var pIndex = findPosition(value, hashCode); + final var hashCode = key.hashCode(); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var eIndex = getFreeKeyIndex(); - keys[eIndex] = value; + keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; positions[~pIndex] = ~eIndex; return eIndex; @@ -132,62 +130,4 @@ public void addUnchecked(K value, int hashCode) { public K getKeyAt(int i) { return keys[i]; } - - /** - * Entry pairing a key with its index in the set. - * @param index index of the key in the set - * @param key the key - * @param the type of the key - */ - public record IndexedKey(int index, K key) {} - - /** - * Get an iterator over pairs of keys and their indices in the set. - * The iterator is not thread safe. - * - * @return an iterator over pairs of keys and their indices in the set - */ - public final ExtendedIterator> indexedKeyIterator() { - final var initialSize = size(); - final Runnable checkForConcurrentModification = () -> - { - if (size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIndexedIterator<>(keys, keysPos, checkForConcurrentModification); - } - - /** - * Get a spliterator over pairs of keys and their indices in the set. - * The spliterator is not thread safe. - * - * @return a spliterator over pairs of keys and their indices in the set - */ - public final Spliterator> indexedKeySpliterator() { - final var initialSize = this.size(); - final Runnable checkForConcurrentModification = () -> - { - if (this.size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIndexedSpliterator<>(keys, keysPos, checkForConcurrentModification); - } - - /** - * Get a stream over pairs of keys and their indices in the set. - * The stream is not thread safe. - * - * @return a stream over pairs of keys and their indices in the set - */ - public final Stream> indexedKeyStream() { - return StreamSupport.stream(indexedKeySpliterator(), false); - } - - /** - * Get a parallel stream over pairs of keys and their indices in the set. - * The stream is not thread safe. - * - * @return a parallel stream over pairs of keys and their indices in the set - */ - public final Stream> indexedKeyStreamParallel() { - return StreamSupport.stream(indexedKeySpliterator(), true); - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java index 5664a900170..b277789c717 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java @@ -25,7 +25,6 @@ import org.apache.jena.shared.JenaException; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Predicate; @@ -36,7 +35,7 @@ * * @param the element type */ -public abstract class HashCommonBase { +public abstract class HashCommonBase implements JenaMapSetCommon { /** * Jeremy suggests, from his experiments, that load factors more than * 0.6 leave the table too dense, and little advantage is gained below 0.4. @@ -78,7 +77,7 @@ protected HashCommonBase(int initialCapacity) { * Copy constructor. * The new table will contain all the same keys of the table to copy. * - * @param baseToCopy + * @param baseToCopy the table to copy */ protected HashCommonBase(final HashCommonBase baseToCopy) { this.keys = newKeysArray(baseToCopy.keys.length); @@ -209,18 +208,10 @@ public boolean anyMatch(final Predicate predicate) { } public ExtendedIterator keyIterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(keys, checkForConcurrentModification); + return new SparseArrayIterator<>(keys, this); } public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(keys, checkForConcurrentModification); + return new SparseArraySpliterator<>(keys, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java index 62e7bd56733..dcdd5557654 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java @@ -24,7 +24,6 @@ import org.apache.jena.mem.spliterator.SparseArraySpliterator; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Supplier; import java.util.function.UnaryOperator; @@ -207,19 +206,11 @@ protected void removeFrom(int here) { @Override public ExtendedIterator valueIterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(values, checkForConcurrentModification); + return new SparseArrayIterator<>(values, this); } @Override public Spliterator valueSpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(values, checkForConcurrentModification); + return new SparseArraySpliterator<>(values, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java index 3e13613b08f..6d2423e0097 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java @@ -30,6 +30,7 @@ /** * A map from keys of type {@code K} to values of type {@code V}. + * Not thread-safe and does not allow {@code null} keys. * * @param the type of the keys in the map * @param the type of the values in the map diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java new file mode 100644 index 00000000000..67c366d00eb --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.mem.collection; + +/** + * Extension of {@link JenaMap} that exposes index-based access and lets callers + * supply a precomputed hash code for the key. Indices are stable handles to + * entries (returned by {@link #putAndGetIndex(Object, Object)}) and remain + * valid until the corresponding entry is removed. + * + * The hash-code overloads are a performance shortcut for callers that already + * have the hash at hand (typically because the same key is stored in several + * collections). The supplied hash code MUST equal {@code key.hashCode()}, or + * the map will misbehave. + * + * @param the type of the keys in the map + * @param the type of the values in the map + */ +public interface JenaMapIndexed extends JenaMap { + + /** + * Returns the index of the entry with the given key, or a negative value + * if no such entry exists. + * + * @param key the key to look up + * @return the index of the entry, or a negative value if absent + */ + int indexOf(K key); + + /** + * Returns the key stored at the given index. + * + * @param index the index of the entry + * @return the key at that index + */ + K getKeyAt(int index); + + /** + * Returns the value stored at the given index. + * + * @param index the index of the entry + * @return the value at that index + */ + V getValueAt(int index); + + /** + * Put a key-value pair and return the index of the affected entry. + * If the key is already present, its value is updated and the existing + * index is returned. + * + * @param key the key to put + * @param value the value to put + * @return the index of the entry holding {@code key} + */ + int putAndGetIndex(K key, V value); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java index 2533714ce6b..7f96baa19f9 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java @@ -28,22 +28,23 @@ import java.util.stream.StreamSupport; /** - * Common interface for {@link JenaMap} and {@link JenaSet}. * + * Operations shared between the map ({@link JenaMap}) and the set + * ({@link JenaSet}) collections used in the {@code mem} triple store + * implementations. + * + * These collections trade some flexibility for speed: they expose only the + * operations needed by triple-store internals (no full {@link java.util.Map} + * or {@link java.util.Set} contract). They are not thread-safe. * - * @param the type of the keys/elements in the collection + * @param the type of the keys (or elements, for sets) in the collection */ -public interface JenaMapSetCommon { +public interface JenaMapSetCommon extends Sized { /** * Clear the collection. */ void clear(); - /** - * @return the number of elements in the collection - */ - int size(); - /** * @return true if the collection is empty */ @@ -75,7 +76,10 @@ public interface JenaMapSetCommon { /** * Removes a key from the collection. - * Attention: Implementations may assume that the key is present. + * + * Attention: implementations may assume the key is present and may produce + * undefined behavior (including silently corrupting internal state) if it + * is not. Use {@link #tryRemove(Object)} when in doubt. * * @param key the key to remove */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java index d3b8a557be9..03848073f56 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java @@ -21,9 +21,10 @@ package org.apache.jena.mem.collection; /** - * Set interface specialized for the use cases in triple store implementations. + * Set interface specialized for the use cases in triple-store implementations. + * Not thread-safe; does not allow {@code null} elements. * - * @param + * @param the element type of the set */ public interface JenaSet extends JenaMapSetCommon { @@ -31,13 +32,16 @@ public interface JenaSet extends JenaMapSetCommon { * Add the key to the set if it is not already present. * * @param key the key to add - * @return true if the key was added, false if it was already present + * @return {@code true} if the key was added, {@code false} if it was already present */ boolean tryAdd(E key); /** - * Add the key to the set without checking if it is already present. - * Attention: This method must only be used if it is guaranteed that the key is not already present. + * Add the key to the set without checking whether it is already present. + * + * Attention: this method must only be used if the caller has ensured that + * the key is not already in the set; otherwise the set's invariants will + * break (duplicates may be inserted). * * @param key the key to add */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java index 8cc8aad8daf..0e1d032b356 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java @@ -22,17 +22,50 @@ /** - * Extension of {@link JenaSet} that allows to add and remove elements - * with a given hash code. - * This is useful if the hash code is already known. - * Attention: The hash code must be consistent with E::hashCode(). + * Extension of {@link JenaSet} that lets callers supply a precomputed hash + * code. + * + * Attention: any caller-supplied hash code MUST equal {@code E.hashCode()}; + * if it does not, the set will misbehave. + * + * @param the element type of the set */ public interface JenaSetHashOptimized extends JenaSet { + + /** + * Add an element with the given precomputed hash code if it is not + * already present. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + * @return {@code true} if added, {@code false} if already present + */ boolean tryAdd(E key, int hashCode); + /** + * Add an element with the given precomputed hash code without checking + * whether it is already present. The caller MUST ensure the key is absent. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + */ void addUnchecked(E key, int hashCode); + /** + * Try to remove an element with the given precomputed hash code. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + * @return {@code true} if removed, {@code false} if it was not present + */ boolean tryRemove(E key, int hashCode); + /** + * Remove an element assumed to be present, with the given precomputed + * hash code. Behavior is undefined if the element is not in the set. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + */ void removeUnchecked(E key, int hashCode); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java new file mode 100644 index 00000000000..c7c3d2e1ddb --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.mem.collection; + + +/** + * Extension of {@link JenaSetHashOptimized} that exposes index-based access to elements. + * Indices are stable handles to entries (returned by {@link #addAndGetIndex(Object)}) and remain + * valid until the corresponding entry is removed. + * + * @param the element type of the set + */ +public interface JenaSetIndexed extends JenaSetHashOptimized { + + /** + * Add an element and return the index it was stored at. If the element + * is already present, returns a negative value (typically the bitwise + * complement of the existing index). + * + * @param key the element to add + * @return the index of the inserted element, or a negative value if the + * element was already present + */ + int addAndGetIndex(final E key); + + /** + * Returns the element stored at the given index. + * + * @param index the index to read + * @return the element at that index + */ + E getKeyAt(int index); + + /** + * Returns the index of the given element, or a negative value if it is + * not in the set. + * + * @param key the element to look up + * @return the index of {@code key}, or a negative value if absent + */ + int indexOf(E key); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java new file mode 100644 index 00000000000..237740ce8e3 --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.mem.collection; + +/** + * Base interface for sized collections. + * It is typically used to detect concurrent modifications in iterators and spliterators + * by snapshotting the size at construction time and rechecking it at each advance/forEach boundary. + */ +public interface Sized { + + /** + * @return the number of elements in the collection + */ + int size(); +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java index b0ac6e994bb..8cfc8948a25 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java @@ -30,16 +30,27 @@ import java.util.function.Consumer; /** - * Iterator that iterates over the entries of sets which are contained in the given iterator of sets. + * Flat-map style iterator that yields every element of every {@link JenaSet} + * produced by the given parent iterator. Empty inner sets are silently + * skipped. Equivalent in spirit to a one-level {@code flatMap} but tailored + * to the {@link JenaSet} API and to {@link NiceIterator}. * - * @param the type of the elements + * @param the element type of the inner sets */ public class IteratorOfJenaSets extends NiceIterator { - final Iterator extends JenaSet> parentIterator; + /** Source iterator producing the sets to flatten. */ + private final Iterator extends JenaSet> parentIterator; - ExtendedIterator currentIterator; + /** Iterator over the keys of the set currently being consumed. */ + private ExtendedIterator currentIterator; + /** + * Create a flat iterator over the elements of every set produced by + * {@code parentIterator}. + * + * @param parentIterator the source iterator of sets + */ public IteratorOfJenaSets(Iterator extends JenaSet> parentIterator) { this.parentIterator = parentIterator; this.currentIterator = parentIterator.hasNext() diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java deleted file mode 100644 index 37f103eae25..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.iterator; - -import org.apache.jena.mem.collection.FastHashSet; -import org.apache.jena.util.iterator.NiceIterator; - -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.function.Consumer; - -/** - * An iterator over a sparse array, that skips null entries. - * This iterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * The iterator works in ascending order, starting from index 0 up to the specified exclusive index. - * - * This iterator will check for concurrent modifications by invoking a {@link Runnable} - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedIterator extends NiceIterator> implements Iterator> { - - private final E[] entries; - private final Runnable checkForConcurrentModification; - private int pos = 0; - private final int toIndexExclusive; - private boolean hasNext = false; - - public SparseArrayIndexedIterator(final E[] entries, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = entries.length; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - public SparseArrayIndexedIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Returns {@code true} if the iteration has more elements. - * (In other words, returns {@code true} if {@link #next} would - * return an element rather than throwing an exception.) - * - * @return {@code true} if the iteration has more elements - */ - @Override - public boolean hasNext() { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - hasNext = true; - return true; - } - pos++; - } - hasNext = false; - return false; - } - - /** - * Returns the next element in the iteration. - * - * @return the next element in the iteration - * @throws NoSuchElementException if the iteration has no more elements - */ - @Override - public FastHashSet.IndexedKey next() { - this.checkForConcurrentModification.run(); - if (hasNext || hasNext()) { - hasNext = false; - return new FastHashSet.IndexedKey<>(pos, entries[pos++]); - } - throw new NoSuchElementException(); - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - action.accept(new FastHashSet.IndexedKey<>(pos, entries[pos])); - } - pos++; - } - this.checkForConcurrentModification.run(); - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java index 936476a80ff..e0b79cd1ff6 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java @@ -21,34 +21,55 @@ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.Sized; import org.apache.jena.util.iterator.NiceIterator; -import java.util.Iterator; +import java.util.ConcurrentModificationException; import java.util.NoSuchElementException; import java.util.function.Consumer; /** - * An iterator over a sparse array, that skips null entries. + * Iterator over a sparse array, walking from high index to low and skipping + * {@code null} entries. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it on each call to + * {@link #next()} / {@link #forEachRemaining(Consumer)}; throws + * {@link ConcurrentModificationException} if the size has changed. * * @param the type of the array elements */ -public class SparseArrayIterator extends NiceIterator implements Iterator { +public class SparseArrayIterator extends NiceIterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; private boolean hasNext = false; - public SparseArrayIterator(final E[] entries, final Runnable checkForConcurrentModification) { + /** + * Iterate over the whole array. + * + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, final Sized set) { this.entries = entries; this.pos = entries.length - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } - public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { + /** + * Iterate over {@code entries[0 .. toIndexExclusive)} (in reverse order). + * + * @param entries the backing array (not copied) + * @param toIndexExclusive exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Sized set) { this.entries = entries; this.pos = toIndexExclusive - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** @@ -62,13 +83,11 @@ public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnab public boolean hasNext() { while (-1 < pos) { if (null != entries[pos]) { - hasNext = true; - return true; + return hasNext = true; } pos--; } - hasNext = false; - return false; + return hasNext = false; } /** @@ -79,7 +98,7 @@ public boolean hasNext() { */ @Override public E next() { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (hasNext || hasNext()) { hasNext = false; return entries[pos--]; @@ -95,6 +114,6 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java index 94008b155f1..d8536f56311 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java @@ -22,8 +22,17 @@ package org.apache.jena.mem.pattern; /** - * A pattern for matching triples. - * The pattern is defined by the wildcard positions for the subject, predicate and object. + * Categorizes a triple-match pattern by which of the subject, predicate and + * object slots are concrete and which are wildcards (i.e. {@code Node.ANY} + * or {@code null}). + * + * The eight enum values cover every possible combination. Triple-store + * implementations dispatch on this enum to pick the most efficient lookup + * path for each kind of pattern (e.g. a fully concrete {@link #SUB_PRE_OBJ} + * is answered directly from the triple set, while a partially open pattern + * such as {@link #ANY_PRE_OBJ} is answered through an index intersection). + * + * @see PatternClassifier */ public enum MatchPattern { /** diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java index 32a6ba182a1..e4cf5644eca 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java @@ -25,14 +25,15 @@ import org.apache.jena.graph.Triple; /** - * Classify a triple match into one of the 8 match patterns. + * Utility class that classifies a triple match into one of the eight + * {@link MatchPattern} values. * - * The classification is based on the concrete-ness of the subject, predicate and object. - * A concrete node is one that is not a variable. + * The classification is based on which of the subject, predicate and object + * are concrete (anything that is not a variable / wildcard / + * {@code null}) and which are wildcards. The result is used by triple-store + * implementations to dispatch to the most efficient lookup path. * - * The classification is used to select the most efficient implementation of a triple store. - * - * This is a utility class; there is no need to instantiate it. + * All operations are stateless; this class is not meant to be instantiated. * * @see MatchPattern */ @@ -41,8 +42,16 @@ public class PatternClassifier { private PatternClassifier() { } + /** + * Classify a triple match. + * + * @param tripleMatch the match triple, possibly containing wildcard nodes + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Triple tripleMatch) { - if (tripleMatch.isConcrete()) { + if (tripleMatch.getSubject().isConcrete() + && tripleMatch.getPredicate().isConcrete() + && tripleMatch.getObject().isConcrete()) { return MatchPattern.SUB_PRE_OBJ; } else { if (tripleMatch.getSubject().isConcrete()) { @@ -73,6 +82,15 @@ public static MatchPattern classify(Triple tripleMatch) { } } + /** + * Classify a triple match given as three nodes. + * Any {@code null} or non-concrete node is treated as a wildcard. + * + * @param sm subject node, or {@code null}/wildcard + * @param pm predicate node, or {@code null}/wildcard + * @param om object node, or {@code null}/wildcard + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Node sm, Node pm, Node om) { if (null != sm && sm.isConcrete()) { if (null != pm && pm.isConcrete()) { @@ -103,6 +121,5 @@ public static MatchPattern classify(Node sm, Node pm, Node om) { } } } - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java index 43bbfeeaea8..a5033c22cde 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java @@ -21,52 +21,57 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a contiguous array slice {@code [0, toIndex)}, + * iterating from high index to low. Supports splitting into + * {@link ArraySubSpliterator} children for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySpliterator implements Spliterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public ArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (-1 < --pos) { action.accept(entries[pos]); return true; @@ -79,7 +84,7 @@ public void forEachRemaining(Consumer super E> action) { while (-1 < --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -89,7 +94,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -101,4 +106,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java index 74994708b53..638f2bb0c9e 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java @@ -21,55 +21,61 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a contiguous array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low. Produced by splitting an + * {@link ArraySpliterator} (or another {@link ArraySubSpliterator}); supports + * further recursive splits for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public ArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (fromIndex <= --pos) { action.accept(entries[pos]); return true; @@ -82,7 +88,7 @@ public void forEachRemaining(Consumer super E> action) { while (fromIndex <= --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -93,7 +99,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -105,4 +111,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java deleted file mode 100644 index 704c9642706..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.spliterator; - -import java.util.Spliterator; -import java.util.function.Consumer; - -import org.apache.jena.mem.collection.FastHashSet; - -/** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * This spliterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * This spliterator works in ascending order, starting from the given start up to the specified exclusive index. - * - * This spliterator supports splitting into sub-spliterators. - * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedSpliterator implements Spliterator> { - - private final E[] entries; - private int currentPositionMinusOne; - private final int toIndexExclusive; - private final Runnable checkForConcurrentModification; - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param fromIndexInclusive the index of the first element, inclusive - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int fromIndexInclusive, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.currentPositionMinusOne = fromIndexInclusive-1; // Start at fromIndexInclusive - 1, so that the first call to tryAdvance will increment pos to fromIndexInclusive - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this(entries, 0, toIndexExclusive, checkForConcurrentModification); - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); - } - - - @Override - public boolean tryAdvance(Consumer super FastHashSet.IndexedKey> action) { - this.checkForConcurrentModification.run(); - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - return true; - } - } - return false; - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - } - } - this.checkForConcurrentModification.run(); - } - - @Override - public Spliterator> trySplit() { - final var nextPos = currentPositionMinusOne + 1; - final var remaining = toIndexExclusive - nextPos; - if ( remaining < 2) { - return null; - } - final var mid = nextPos + ( remaining >>> 1); - final var fromIndexInclusive = nextPos; - this.currentPositionMinusOne = mid-1; - return new SparseArrayIndexedSpliterator<>(entries, fromIndexInclusive, mid, checkForConcurrentModification); - } - - @Override - public long estimateSize() { return (long) toIndexExclusive - currentPositionMinusOne; } - - @Override - public int characteristics() { - return DISTINCT | NONNULL | IMMUTABLE; - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java index 6752cc9a1c1..add45739dc2 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java @@ -21,17 +21,24 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a sparse array slice {@code [0, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced for backing arrays such as those of + * {@link org.apache.jena.mem.collection.FastHashBase}, where removed slots + * are represented by {@code null}. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Supports splitting into {@link SparseArraySubSpliterator} children for + * parallel traversal. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it at each + * advance/forEach boundary; throws {@link ConcurrentModificationException} + * if the size has changed. * * @param the type of the array elements */ @@ -39,35 +46,37 @@ public class SparseArraySpliterator implements Spliterator { private final E[] entries; private int pos; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}, skipping nulls. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public SparseArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (-1 < --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -86,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -96,7 +105,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java index 3eb0784326f..d79242ac78c 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java @@ -21,55 +21,62 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a sparse array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced by splitting a {@link SparseArraySpliterator} (or another + * {@link SparseArraySubSpliterator}); supports further recursive splits for + * parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary; + * throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the type of the array elements */ public class SparseArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}, skipping nulls. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public SparseArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (fromIndex <= --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -88,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -99,7 +106,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java index 07ccc9634a9..f0fba805175 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java @@ -32,26 +32,41 @@ import java.util.function.Predicate; /** - * An ArrayBunch implements TripleBunch with a linear search of a short-ish - * array of Triples. The array grows by factor 2. + * Linear-scan implementation of {@link FastTripleBunch} backed by a packed + * {@link Triple} array. Used as long as a bunch stays small; once it grows + * past the configured threshold (see {@link FastTripleStore}) it is replaced + * with a {@link FastHashedTripleBunch}. + * + * The array grows by a factor of two when full. Equality of triples within a + * bunch is delegated to {@link #areEqual(Triple, Triple)}, which subclasses + * specialize to compare only the two nodes that are not already + * implied by the enclosing map's key. This avoids redundant equality checks + * on the shared subject/predicate/object. + * + * Not thread-safe. */ public abstract class FastArrayBunch implements FastTripleBunch { private static final int INITIAL_SIZE = 4; + /** Number of valid entries in {@link #elements}. */ protected int size = 0; + /** Packed array of triples; entries from {@code 0} to {@code size-1} are live. */ protected Triple[] elements; + /** + * Creates an empty bunch with the default initial capacity. + */ protected FastArrayBunch() { elements = new Triple[INITIAL_SIZE]; } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. - * But it will reserve only the space needed to contain them. Growing is still possible. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}; its backing array is sized to fit exactly, + * but can grow further if needed. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -59,7 +74,17 @@ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.size = bunchToCopy.size; } - public abstract boolean areEqual(final Triple a, final Triple b); + /** + * Compare two triples for equality within this bunch. + * + * Subclasses specialize this to skip the already-shared component + * (subject, predicate or object) and compare only the remaining two. + * + * @param a first triple + * @param b second triple + * @return {@code true} if the triples are considered equal in this bunch + */ + protected abstract boolean areEqual(final Triple a, final Triple b); @Override public boolean containsKey(Triple t) { @@ -127,6 +152,7 @@ public boolean tryRemove(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return true; } } @@ -138,6 +164,7 @@ public void removeUnchecked(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return; } } @@ -174,11 +201,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java index b89d3312048..a49d6b54009 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java @@ -25,21 +25,28 @@ import org.apache.jena.mem.collection.FastHashMap; /** - * Map from nodes to triple bunches. + * {@link FastHashMap} specialized to map a {@link Node} to its associated + * {@link FastTripleBunch}. Used by {@link FastTripleStore} to maintain the + * three subject/predicate/object indices. */ public class FastHashedBunchMap extends FastHashMap implements Copyable { + /** + * Creates an empty bunch map with the default initial capacity. + */ public FastHashedBunchMap() { super(); } /** - * Copy constructor. - * The new map will contain all the same nodes as keys of the map to copy, but copies of the bunches as values . + * Copy constructor. The new map has the same node keys as + * {@code mapToCopy}; each value is replaced by a deep copy of the + * corresponding bunch (via {@link FastTripleBunch#copy()}) so that + * mutations of either map cannot affect the other. * - * @param mapToCopy + * @param mapToCopy the source map */ private FastHashedBunchMap(final FastHashedBunchMap mapToCopy) { super(mapToCopy, FastTripleBunch::copy); diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java index 459e78c8181..65c9ab70fbf 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java @@ -25,13 +25,21 @@ import org.apache.jena.mem.collection.JenaSet; /** - * A set of triples - backed by {@link FastHashSet}. + * Hashed implementation of {@link FastTripleBunch} built on top of + * {@link FastHashSet}. Used by {@link FastTripleStore} once a bunch grows + * past the size threshold at which a linear-scan {@link FastArrayBunch} + * stops being faster. */ public class FastHashedTripleBunch extends FastHashSet implements FastTripleBunch { + /** - * Create a new triple bunch from the given set of triples. + * Create a new hashed bunch pre-populated from the given set of triples. + * The initial capacity is chosen at 1.5x the source size, so the new bunch + * fits the existing triples and has some headroom for growth before it + * needs to rehash. * - * @param set the set of triples + * @param set the source set of triples (typically the array bunch being + * promoted) */ public FastHashedTripleBunch(final JenaSet set) { super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth @@ -39,15 +47,18 @@ public FastHashedTripleBunch(final JenaSet set) { } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ private FastHashedTripleBunch(final FastHashedTripleBunch bunchToCopy) { super(bunchToCopy); } + /** + * Creates an empty hashed bunch with the default initial capacity. + */ public FastHashedTripleBunch() { super(); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java index 68f79e72f8a..fe050283188 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java @@ -29,27 +29,39 @@ import java.util.function.Predicate; /** - * A bunch of triples - a stripped-down set with specialized methods. A - * bunch is expected to store triples that share some useful property - * (such as having the same subject or predicate). + * Set-like container for a "bunch" of triples that share some useful + * property - typically they all have the same subject, predicate or object, + * because the bunch is the value of a node-keyed map in a + * {@link FastTripleStore}. + * + * The interface is a stripped-down set with a few extras tuned for the + * triple-store hot path; concrete implementations are + * {@link FastArrayBunch} (linear scan, used while the bunch is small) and + * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a + * threshold). */ public interface FastTripleBunch extends JenaSetHashOptimized, Copyable { /** - * Answer true iff this bunch is implemented as an array. - * This field is used to optimize some operations by avoiding the need for instanceOf tests. + * Answer {@code true} iff this bunch is backed by a flat array (i.e. is + * a {@link FastArrayBunch}). Exposed as an explicit method so callers can + * avoid {@code instanceof} checks on this hot path. * - * @return true iff this bunch is implemented as an arrays + * @return {@code true} if this bunch is array-backed */ boolean isArray(); /** - * This method is used to optimize _PO match operations. - * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches. - * This method is faster if there are many matches and the set is ordered in an unfavorable way. - * _PO matches usually fall into this category. + * Predicate test that scans elements in hash-table order rather than + * dense insertion order. Tuned for {@code _PO} (any-predicate-object) + * matches. + * + * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
+ * This is faster than {@link #anyMatch(Predicate)} when many matches are + * expected (the predicate is more likely to short-circuit early), but + * slower when no or only a single match exists (each iteration must + * test against an empty slot first). * - * @param predicate the predicate to apply to elements of this collection - * @return {@code true} if any element of the collection matches the predicate + * @param predicate the predicate to apply + * @return {@code true} if any element matches the predicate */ public final boolean anyMatchRandomOrder(Predicate predicate) { var pIndex = positions.length - 1; @@ -381,14 +420,22 @@ public final boolean anyMatchRandomOrder(Predicate predicate) { @Override public final ExtendedIterator keyIterator() { - final var initialSize = size(); - final Runnable checkForConcurrentModification = () -> - { - if (size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(keys, keysPos, checkForConcurrentModification); + return new SparseArrayIterator<>(keys, keysPos, this); } + /** + * Locates the slot in {@link #positions} that holds {@code e} (with the + * given precomputed hash code). + * + * If the key is present, returns the (non-negative) probe-table slot + * index. If the key is absent, returns the bitwise complement of the + * empty probe-table slot at which the key would be inserted, allowing + * insertion to proceed without a second probe walk. + * + * @param e the key to locate + * @param hashCode {@code e.hashCode()} + * @return the position index if found, or {@code ~insertionPosition} if not + */ protected final int findPosition(final K e, final int hashCode) { var pIndex = calcStartIndexByHashCode(hashCode); while (true) { @@ -405,6 +452,15 @@ protected final int findPosition(final K e, final int hashCode) { } } + /** + * Locates the next empty slot in {@link #positions} along the probe chain + * for the given hash code, without checking any existing entries for + * equality. Used after a positions-array resize, when no duplicates can + * exist in the rebuilt table. + * + * @param hashCode the hash code being placed + * @return the index of an empty slot in the probe table + */ protected final int findEmptySlotWithoutEqualityCheck(final int hashCode) { var pIndex = calcStartIndexByHashCode(hashCode); while (true) { @@ -435,11 +491,63 @@ public void clear() { @Override public final Spliterator keySpliterator() { - final var initialSize = this.size(); - final Runnable checkForConcurrentModification = () -> - { - if (this.size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(keys, keysPos, checkForConcurrentModification); + return new SparseArraySpliterator<>(keys, keysPos, this); + } + + /** + * Gets the key at the given index. + * Array bounds are not checked. The caller must ensure the index is valid and corresponds to a non-null key. + * + * @param i the index + * @return the key at the given index + */ + public K getKeyAt(int i) { + return keys[i]; + } + + /** + * Returns the index of the entry holding {@code key}, or {@code -1} if not present. + * + * @param key the key to look up + * @return the entry index, or {@code -1} if the key is absent + */ + public int indexOf(K key) { + final var pIndex = findPosition(key, key.hashCode()); + if (pIndex < 0) { + return -1; + } else { + return ~positions[pIndex]; + } + } + + /** + * Functional interface used by {@link #forEachKey} to receive each live + * key along with the stable index it occupies. + * + * @param the key type + */ + @FunctionalInterface + public interface KeyAndIndexConsumer { + /** + * Receive a single key and its index. + * + * @param key the key + * @param index the stable index of the key in the underlying array + */ + void accept(K key, int index); + } + + /** + * Sequentially invokes {@code consumer} for every live key with its index. + * Skips freed slots. + * + * @param consumer receives each key/index pair + */ + public void forEachKey(KeyAndIndexConsumer consumer) { + for (int i = 0; i < keysPos; i++) { + if(keys[i] != null) { + consumer.accept(keys[i], i); + } + } } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java index 04c2761416b..e3f741ba485 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java @@ -25,39 +25,56 @@ import org.apache.jena.mem.spliterator.SparseArraySpliterator; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Supplier; import java.util.function.UnaryOperator; /** - * Map which grows, if needed but never shrinks. - * This map does not guarantee any order. Although due to the way it is implemented the elements have a certain order. - * This map does not allow null keys. - * This map is not thread safe. - * It´s purpose is to support fast add, remove, contains and stream / iterate operations. - * Only remove operations are not as fast as in {@link java.util.HashMap} - * Iterating over this map does not get much faster again after removing elements because the map is not compacted. + * Hash map specialization built on top of {@link FastHashBase}. + * Grows on demand but never shrinks, does not guarantee iteration order, + * does not allow {@code null} keys, and is not thread-safe. + * + * Optimized for fast {@code add} / {@code containsKey} / {@code stream} / + * iterate operations. Removal is somewhat slower than in + * {@link java.util.HashMap} because of the back-shifting performed on the + * probe table. Iteration speed does not recover after many removals because + * the dense {@code keys} array is not compacted. + * + * @param the key type + * @param the value type */ -public abstract class FastHashMap extends FastHashBase implements JenaMap { +public abstract class FastHashMap extends FastHashBase implements JenaMapIndexed { + /** + * Parallel array to {@code keys} holding the value associated with each + * stored key. {@code values[i]} is the value for {@code keys[i]} when + * {@code keys[i]} is non-null. + */ protected V[] values; + /** + * Creates a map with the given initial key-array capacity. + * + * @param initialSize the initial capacity of the keys/values arrays + */ protected FastHashMap(int initialSize) { super(initialSize); this.values = newValuesArray(keys.length); } + /** + * Creates a map with the default initial capacity. + */ protected FastHashMap() { super(); this.values = newValuesArray(keys.length); } /** - * Copy constructor. - * The new map will contain all the same keys and values of the map to copy. + * Copy constructor. The new map contains the same keys and the same + * value references as {@code mapToCopy}. * - * @param mapToCopy + * @param mapToCopy the source map */ protected FastHashMap(final FastHashMap mapToCopy) { super(mapToCopy); @@ -66,10 +83,13 @@ protected FastHashMap(final FastHashMap mapToCopy) { } /** - * Copy constructor with value processor. + * Copy constructor that transforms each value via {@code valueProcessor}. + * Useful when the values are mutable and need to be deep-copied to keep + * the new map independent from the source. * - * @param mapToCopy - * @param valueProcessor + * @param mapToCopy the source map + * @param valueProcessor function applied to every non-null value to obtain + * the value to put in the new map */ protected FastHashMap(final FastHashMap mapToCopy, final UnaryOperator valueProcessor) { super(mapToCopy); @@ -82,6 +102,12 @@ protected FastHashMap(final FastHashMap mapToCopy, final UnaryOperator } } + /** + * Gets a new array of values with the given size. + * + * @param size the size of the array + * @return the new array + */ protected abstract V[] newValuesArray(int size); @Override @@ -106,12 +132,10 @@ public void clear() { @Override public boolean tryPut(K key, V value) { + growPositionsArrayIfNeeded(); final var hashCode = key.hashCode(); - var pIndex = findPosition(key, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { - if (tryGrowPositionsArrayIfNeeded()) { - pIndex = findPosition(key, hashCode); - } final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; values[eIndex] = value; @@ -126,12 +150,10 @@ public boolean tryPut(K key, V value) { @Override public void put(K key, V value) { + growPositionsArrayIfNeeded(); final var hashCode = key.hashCode(); - var pIndex = findPosition(key, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { - if (tryGrowPositionsArrayIfNeeded()) { - pIndex = findPosition(key, hashCode); - } final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; values[eIndex] = value; @@ -142,8 +164,27 @@ public void put(K key, V value) { } } + @Override + public int putAndGetIndex(K key, V value) { + growPositionsArrayIfNeeded(); + final int hashCode = key.hashCode(); + final var pIndex = findPosition(key, hashCode); + final int eIndex; + if (pIndex < 0) { + eIndex = getFreeKeyIndex(); + keys[eIndex] = key; + hashCodesOrDeletedIndices[eIndex] = hashCode; + positions[~pIndex] = ~eIndex; + } else { + eIndex = ~positions[pIndex]; + } + values[eIndex] = value; + return eIndex; + } + /** * Returns the value at the given index. + * Array bounds are not checked. The caller must ensure the index is valid and corresponds to a non-null key. * * @param i index * @return value @@ -178,12 +219,12 @@ public V computeIfAbsent(K key, Supplier absentValueSupplier) { var pIndex = findPosition(key, hashCode); if (pIndex < 0) { if (tryGrowPositionsArrayIfNeeded()) { - pIndex = findPosition(key, hashCode); + pIndex = ~findEmptySlotWithoutEqualityCheck(hashCode); } + final var value = absentValueSupplier.get(); final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; - final var value = absentValueSupplier.get(); values[eIndex] = value; positions[~pIndex] = ~eIndex; return value; @@ -194,18 +235,20 @@ public V computeIfAbsent(K key, Supplier absentValueSupplier) { @Override public void compute(K key, UnaryOperator valueProcessor) { - final int hashCode = key.hashCode(); + final var hashCode = key.hashCode(); var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var value = valueProcessor.apply(null); if (value == null) return; + if(tryGrowPositionsArrayIfNeeded()) { + pIndex = ~findEmptySlotWithoutEqualityCheck(hashCode); + } final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; values[eIndex] = value; positions[~pIndex] = ~eIndex; - tryGrowPositionsArrayIfNeeded(); } else { var eIndex = ~positions[pIndex]; final var value = valueProcessor.apply(values[eIndex]); @@ -217,24 +260,13 @@ public void compute(K key, UnaryOperator valueProcessor) { } } - @Override public ExtendedIterator valueIterator() { - final var initialSize = size(); - final Runnable checkForConcurrentModification = () -> - { - if (size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(values, keysPos, checkForConcurrentModification); + return new SparseArrayIterator<>(values, keysPos, this); } @Override public Spliterator valueSpliterator() { - final var initialSize = this.size(); - final Runnable checkForConcurrentModification = () -> - { - if (this.size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(values, keysPos, checkForConcurrentModification); + return new SparseArraySpliterator<>(values, keysPos, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java index 134a0092e22..5adf3232c4e 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java @@ -21,39 +21,42 @@ package org.apache.jena.mem.collection; -import org.apache.jena.mem.iterator.SparseArrayIndexedIterator; -import org.apache.jena.mem.spliterator.SparseArrayIndexedSpliterator; -import org.apache.jena.util.iterator.ExtendedIterator; - -import java.util.ConcurrentModificationException; -import java.util.Spliterator; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - /** - * Set which grows, if needed but never shrinks. - * This set does not guarantee any order. Although due to the way it is implemented the elements have a certain order. - * This set does not allow null values. - * This set is not thread safe. - * It´s purpose is to support fast add, remove, contains and stream / iterate operations. - * Only remove operations are not as fast as in {@link java.util.HashSet} - * Iterating over this set not get much faster again after removing elements because the set is not compacted. + * Hash set specialization built on top of {@link FastHashBase}. + * Grows on demand but never shrinks, does not guarantee iteration order, + * does not allow {@code null} elements, and is not thread-safe. + * + * Optimized for fast {@code add} / {@code containsKey} / {@code stream} / + * iterate operations. Removal is somewhat slower than in + * {@link java.util.HashSet} because of the back-shifting performed on the + * probe table. Iteration speed does not recover after many removals because + * the dense {@code keys} array is not compacted. + * + * @param the element type */ -public abstract class FastHashSet extends FastHashBase implements JenaSetHashOptimized { +public abstract class FastHashSet extends FastHashBase implements JenaSetIndexed { - protected FastHashSet(int initialSize) { + /** + * Creates a set with the given initial key-array capacity. + * + * @param initialSize the initial capacity of the keys array + */ + public FastHashSet(final int initialSize) { super(initialSize); } - protected FastHashSet() { + /** + * Creates a set with the default initial capacity. + */ + public FastHashSet() { super(); } /** - * Copy constructor. - * The new set will contain all the same keys of the set to copy. + * Copy constructor. The new set contains the same elements as + * {@code setToCopy}. * - * @param setToCopy + * @param setToCopy the source set */ protected FastHashSet(final FastHashSet setToCopy) { super(setToCopy); @@ -65,12 +68,12 @@ public boolean tryAdd(K key) { } @Override - public boolean tryAdd(K value, int hashCode) { + public boolean tryAdd(K key, int hashCode) { growPositionsArrayIfNeeded(); - var pIndex = findPosition(value, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var eIndex = getFreeKeyIndex(); - keys[eIndex] = value; + keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; positions[~pIndex] = ~eIndex; return true; @@ -79,28 +82,23 @@ public boolean tryAdd(K value, int hashCode) { } /** - * Add and get the index of the added element. + * Add an element and return the index it was stored at. + * If the element is already present, returns the bitwise complement + * ({@code ~existingIndex}) of the existing index, so callers can + * distinguish "newly inserted" from "already present" while still + * recovering the index in both cases. * - * @param value the value to add - * @return the index of the added element or the inverse (~) index of the existing element + * @param key the element to add + * @return the new index, or {@code ~existingIndex} if already present */ - public int addAndGetIndex(K value) { - return addAndGetIndex(value, value.hashCode()); - } - - /** - * Add and get the index of the added element. - * - * @param value the value to add - * @param hashCode the hash code of the value. This is a performance optimization. - * @return the index of the added element or the inverse (~) index of the existing element - */ - public int addAndGetIndex(final K value, final int hashCode) { + @Override + public int addAndGetIndex(K key) { growPositionsArrayIfNeeded(); - final var pIndex = findPosition(value, hashCode); + final var hashCode = key.hashCode(); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var eIndex = getFreeKeyIndex(); - keys[eIndex] = value; + keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; positions[~pIndex] = ~eIndex; return eIndex; @@ -132,62 +130,4 @@ public void addUnchecked(K value, int hashCode) { public K getKeyAt(int i) { return keys[i]; } - - /** - * Entry pairing a key with its index in the set. - * @param index index of the key in the set - * @param key the key - * @param the type of the key - */ - public record IndexedKey(int index, K key) {} - - /** - * Get an iterator over pairs of keys and their indices in the set. - * The iterator is not thread safe. - * - * @return an iterator over pairs of keys and their indices in the set - */ - public final ExtendedIterator> indexedKeyIterator() { - final var initialSize = size(); - final Runnable checkForConcurrentModification = () -> - { - if (size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIndexedIterator<>(keys, keysPos, checkForConcurrentModification); - } - - /** - * Get a spliterator over pairs of keys and their indices in the set. - * The spliterator is not thread safe. - * - * @return a spliterator over pairs of keys and their indices in the set - */ - public final Spliterator> indexedKeySpliterator() { - final var initialSize = this.size(); - final Runnable checkForConcurrentModification = () -> - { - if (this.size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIndexedSpliterator<>(keys, keysPos, checkForConcurrentModification); - } - - /** - * Get a stream over pairs of keys and their indices in the set. - * The stream is not thread safe. - * - * @return a stream over pairs of keys and their indices in the set - */ - public final Stream> indexedKeyStream() { - return StreamSupport.stream(indexedKeySpliterator(), false); - } - - /** - * Get a parallel stream over pairs of keys and their indices in the set. - * The stream is not thread safe. - * - * @return a parallel stream over pairs of keys and their indices in the set - */ - public final Stream> indexedKeyStreamParallel() { - return StreamSupport.stream(indexedKeySpliterator(), true); - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java index 5664a900170..b277789c717 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java @@ -25,7 +25,6 @@ import org.apache.jena.shared.JenaException; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Predicate; @@ -36,7 +35,7 @@ * * @param the element type */ -public abstract class HashCommonBase { +public abstract class HashCommonBase implements JenaMapSetCommon { /** * Jeremy suggests, from his experiments, that load factors more than * 0.6 leave the table too dense, and little advantage is gained below 0.4. @@ -78,7 +77,7 @@ protected HashCommonBase(int initialCapacity) { * Copy constructor. * The new table will contain all the same keys of the table to copy. * - * @param baseToCopy + * @param baseToCopy the table to copy */ protected HashCommonBase(final HashCommonBase baseToCopy) { this.keys = newKeysArray(baseToCopy.keys.length); @@ -209,18 +208,10 @@ public boolean anyMatch(final Predicate predicate) { } public ExtendedIterator keyIterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(keys, checkForConcurrentModification); + return new SparseArrayIterator<>(keys, this); } public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(keys, checkForConcurrentModification); + return new SparseArraySpliterator<>(keys, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java index 62e7bd56733..dcdd5557654 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java @@ -24,7 +24,6 @@ import org.apache.jena.mem.spliterator.SparseArraySpliterator; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Supplier; import java.util.function.UnaryOperator; @@ -207,19 +206,11 @@ protected void removeFrom(int here) { @Override public ExtendedIterator valueIterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(values, checkForConcurrentModification); + return new SparseArrayIterator<>(values, this); } @Override public Spliterator valueSpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(values, checkForConcurrentModification); + return new SparseArraySpliterator<>(values, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java index 3e13613b08f..6d2423e0097 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java @@ -30,6 +30,7 @@ /** * A map from keys of type {@code K} to values of type {@code V}. + * Not thread-safe and does not allow {@code null} keys. * * @param the type of the keys in the map * @param the type of the values in the map diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java new file mode 100644 index 00000000000..67c366d00eb --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.mem.collection; + +/** + * Extension of {@link JenaMap} that exposes index-based access and lets callers + * supply a precomputed hash code for the key. Indices are stable handles to + * entries (returned by {@link #putAndGetIndex(Object, Object)}) and remain + * valid until the corresponding entry is removed. + * + * The hash-code overloads are a performance shortcut for callers that already + * have the hash at hand (typically because the same key is stored in several + * collections). The supplied hash code MUST equal {@code key.hashCode()}, or + * the map will misbehave. + * + * @param the type of the keys in the map + * @param the type of the values in the map + */ +public interface JenaMapIndexed extends JenaMap { + + /** + * Returns the index of the entry with the given key, or a negative value + * if no such entry exists. + * + * @param key the key to look up + * @return the index of the entry, or a negative value if absent + */ + int indexOf(K key); + + /** + * Returns the key stored at the given index. + * + * @param index the index of the entry + * @return the key at that index + */ + K getKeyAt(int index); + + /** + * Returns the value stored at the given index. + * + * @param index the index of the entry + * @return the value at that index + */ + V getValueAt(int index); + + /** + * Put a key-value pair and return the index of the affected entry. + * If the key is already present, its value is updated and the existing + * index is returned. + * + * @param key the key to put + * @param value the value to put + * @return the index of the entry holding {@code key} + */ + int putAndGetIndex(K key, V value); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java index 2533714ce6b..7f96baa19f9 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java @@ -28,22 +28,23 @@ import java.util.stream.StreamSupport; /** - * Common interface for {@link JenaMap} and {@link JenaSet}. * + * Operations shared between the map ({@link JenaMap}) and the set + * ({@link JenaSet}) collections used in the {@code mem} triple store + * implementations. + * + * These collections trade some flexibility for speed: they expose only the + * operations needed by triple-store internals (no full {@link java.util.Map} + * or {@link java.util.Set} contract). They are not thread-safe. * - * @param the type of the keys/elements in the collection + * @param the type of the keys (or elements, for sets) in the collection */ -public interface JenaMapSetCommon { +public interface JenaMapSetCommon extends Sized { /** * Clear the collection. */ void clear(); - /** - * @return the number of elements in the collection - */ - int size(); - /** * @return true if the collection is empty */ @@ -75,7 +76,10 @@ public interface JenaMapSetCommon { /** * Removes a key from the collection. - * Attention: Implementations may assume that the key is present. + * + * Attention: implementations may assume the key is present and may produce + * undefined behavior (including silently corrupting internal state) if it + * is not. Use {@link #tryRemove(Object)} when in doubt. * * @param key the key to remove */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java index d3b8a557be9..03848073f56 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java @@ -21,9 +21,10 @@ package org.apache.jena.mem.collection; /** - * Set interface specialized for the use cases in triple store implementations. + * Set interface specialized for the use cases in triple-store implementations. + * Not thread-safe; does not allow {@code null} elements. * - * @param + * @param the element type of the set */ public interface JenaSet extends JenaMapSetCommon { @@ -31,13 +32,16 @@ public interface JenaSet extends JenaMapSetCommon { * Add the key to the set if it is not already present. * * @param key the key to add - * @return true if the key was added, false if it was already present + * @return {@code true} if the key was added, {@code false} if it was already present */ boolean tryAdd(E key); /** - * Add the key to the set without checking if it is already present. - * Attention: This method must only be used if it is guaranteed that the key is not already present. + * Add the key to the set without checking whether it is already present. + * + * Attention: this method must only be used if the caller has ensured that + * the key is not already in the set; otherwise the set's invariants will + * break (duplicates may be inserted). * * @param key the key to add */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java index 8cc8aad8daf..0e1d032b356 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java @@ -22,17 +22,50 @@ /** - * Extension of {@link JenaSet} that allows to add and remove elements - * with a given hash code. - * This is useful if the hash code is already known. - * Attention: The hash code must be consistent with E::hashCode(). + * Extension of {@link JenaSet} that lets callers supply a precomputed hash + * code. + * + * Attention: any caller-supplied hash code MUST equal {@code E.hashCode()}; + * if it does not, the set will misbehave. + * + * @param the element type of the set */ public interface JenaSetHashOptimized extends JenaSet { + + /** + * Add an element with the given precomputed hash code if it is not + * already present. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + * @return {@code true} if added, {@code false} if already present + */ boolean tryAdd(E key, int hashCode); + /** + * Add an element with the given precomputed hash code without checking + * whether it is already present. The caller MUST ensure the key is absent. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + */ void addUnchecked(E key, int hashCode); + /** + * Try to remove an element with the given precomputed hash code. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + * @return {@code true} if removed, {@code false} if it was not present + */ boolean tryRemove(E key, int hashCode); + /** + * Remove an element assumed to be present, with the given precomputed + * hash code. Behavior is undefined if the element is not in the set. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + */ void removeUnchecked(E key, int hashCode); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java new file mode 100644 index 00000000000..c7c3d2e1ddb --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.mem.collection; + + +/** + * Extension of {@link JenaSetHashOptimized} that exposes index-based access to elements. + * Indices are stable handles to entries (returned by {@link #addAndGetIndex(Object)}) and remain + * valid until the corresponding entry is removed. + * + * @param the element type of the set + */ +public interface JenaSetIndexed extends JenaSetHashOptimized { + + /** + * Add an element and return the index it was stored at. If the element + * is already present, returns a negative value (typically the bitwise + * complement of the existing index). + * + * @param key the element to add + * @return the index of the inserted element, or a negative value if the + * element was already present + */ + int addAndGetIndex(final E key); + + /** + * Returns the element stored at the given index. + * + * @param index the index to read + * @return the element at that index + */ + E getKeyAt(int index); + + /** + * Returns the index of the given element, or a negative value if it is + * not in the set. + * + * @param key the element to look up + * @return the index of {@code key}, or a negative value if absent + */ + int indexOf(E key); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java new file mode 100644 index 00000000000..237740ce8e3 --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.mem.collection; + +/** + * Base interface for sized collections. + * It is typically used to detect concurrent modifications in iterators and spliterators + * by snapshotting the size at construction time and rechecking it at each advance/forEach boundary. + */ +public interface Sized { + + /** + * @return the number of elements in the collection + */ + int size(); +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java index b0ac6e994bb..8cfc8948a25 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java @@ -30,16 +30,27 @@ import java.util.function.Consumer; /** - * Iterator that iterates over the entries of sets which are contained in the given iterator of sets. + * Flat-map style iterator that yields every element of every {@link JenaSet} + * produced by the given parent iterator. Empty inner sets are silently + * skipped. Equivalent in spirit to a one-level {@code flatMap} but tailored + * to the {@link JenaSet} API and to {@link NiceIterator}. * - * @param the type of the elements + * @param the element type of the inner sets */ public class IteratorOfJenaSets extends NiceIterator { - final Iterator extends JenaSet> parentIterator; + /** Source iterator producing the sets to flatten. */ + private final Iterator extends JenaSet> parentIterator; - ExtendedIterator currentIterator; + /** Iterator over the keys of the set currently being consumed. */ + private ExtendedIterator currentIterator; + /** + * Create a flat iterator over the elements of every set produced by + * {@code parentIterator}. + * + * @param parentIterator the source iterator of sets + */ public IteratorOfJenaSets(Iterator extends JenaSet> parentIterator) { this.parentIterator = parentIterator; this.currentIterator = parentIterator.hasNext() diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java deleted file mode 100644 index 37f103eae25..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.iterator; - -import org.apache.jena.mem.collection.FastHashSet; -import org.apache.jena.util.iterator.NiceIterator; - -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.function.Consumer; - -/** - * An iterator over a sparse array, that skips null entries. - * This iterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * The iterator works in ascending order, starting from index 0 up to the specified exclusive index. - * - * This iterator will check for concurrent modifications by invoking a {@link Runnable} - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedIterator extends NiceIterator> implements Iterator> { - - private final E[] entries; - private final Runnable checkForConcurrentModification; - private int pos = 0; - private final int toIndexExclusive; - private boolean hasNext = false; - - public SparseArrayIndexedIterator(final E[] entries, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = entries.length; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - public SparseArrayIndexedIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Returns {@code true} if the iteration has more elements. - * (In other words, returns {@code true} if {@link #next} would - * return an element rather than throwing an exception.) - * - * @return {@code true} if the iteration has more elements - */ - @Override - public boolean hasNext() { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - hasNext = true; - return true; - } - pos++; - } - hasNext = false; - return false; - } - - /** - * Returns the next element in the iteration. - * - * @return the next element in the iteration - * @throws NoSuchElementException if the iteration has no more elements - */ - @Override - public FastHashSet.IndexedKey next() { - this.checkForConcurrentModification.run(); - if (hasNext || hasNext()) { - hasNext = false; - return new FastHashSet.IndexedKey<>(pos, entries[pos++]); - } - throw new NoSuchElementException(); - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - action.accept(new FastHashSet.IndexedKey<>(pos, entries[pos])); - } - pos++; - } - this.checkForConcurrentModification.run(); - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java index 936476a80ff..e0b79cd1ff6 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java @@ -21,34 +21,55 @@ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.Sized; import org.apache.jena.util.iterator.NiceIterator; -import java.util.Iterator; +import java.util.ConcurrentModificationException; import java.util.NoSuchElementException; import java.util.function.Consumer; /** - * An iterator over a sparse array, that skips null entries. + * Iterator over a sparse array, walking from high index to low and skipping + * {@code null} entries. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it on each call to + * {@link #next()} / {@link #forEachRemaining(Consumer)}; throws + * {@link ConcurrentModificationException} if the size has changed. * * @param the type of the array elements */ -public class SparseArrayIterator extends NiceIterator implements Iterator { +public class SparseArrayIterator extends NiceIterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; private boolean hasNext = false; - public SparseArrayIterator(final E[] entries, final Runnable checkForConcurrentModification) { + /** + * Iterate over the whole array. + * + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, final Sized set) { this.entries = entries; this.pos = entries.length - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } - public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { + /** + * Iterate over {@code entries[0 .. toIndexExclusive)} (in reverse order). + * + * @param entries the backing array (not copied) + * @param toIndexExclusive exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Sized set) { this.entries = entries; this.pos = toIndexExclusive - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** @@ -62,13 +83,11 @@ public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnab public boolean hasNext() { while (-1 < pos) { if (null != entries[pos]) { - hasNext = true; - return true; + return hasNext = true; } pos--; } - hasNext = false; - return false; + return hasNext = false; } /** @@ -79,7 +98,7 @@ public boolean hasNext() { */ @Override public E next() { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (hasNext || hasNext()) { hasNext = false; return entries[pos--]; @@ -95,6 +114,6 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java index 94008b155f1..d8536f56311 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java @@ -22,8 +22,17 @@ package org.apache.jena.mem.pattern; /** - * A pattern for matching triples. - * The pattern is defined by the wildcard positions for the subject, predicate and object. + * Categorizes a triple-match pattern by which of the subject, predicate and + * object slots are concrete and which are wildcards (i.e. {@code Node.ANY} + * or {@code null}). + * + * The eight enum values cover every possible combination. Triple-store + * implementations dispatch on this enum to pick the most efficient lookup + * path for each kind of pattern (e.g. a fully concrete {@link #SUB_PRE_OBJ} + * is answered directly from the triple set, while a partially open pattern + * such as {@link #ANY_PRE_OBJ} is answered through an index intersection). + * + * @see PatternClassifier */ public enum MatchPattern { /** diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java index 32a6ba182a1..e4cf5644eca 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java @@ -25,14 +25,15 @@ import org.apache.jena.graph.Triple; /** - * Classify a triple match into one of the 8 match patterns. + * Utility class that classifies a triple match into one of the eight + * {@link MatchPattern} values. * - * The classification is based on the concrete-ness of the subject, predicate and object. - * A concrete node is one that is not a variable. + * The classification is based on which of the subject, predicate and object + * are concrete (anything that is not a variable / wildcard / + * {@code null}) and which are wildcards. The result is used by triple-store + * implementations to dispatch to the most efficient lookup path. * - * The classification is used to select the most efficient implementation of a triple store. - * - * This is a utility class; there is no need to instantiate it. + * All operations are stateless; this class is not meant to be instantiated. * * @see MatchPattern */ @@ -41,8 +42,16 @@ public class PatternClassifier { private PatternClassifier() { } + /** + * Classify a triple match. + * + * @param tripleMatch the match triple, possibly containing wildcard nodes + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Triple tripleMatch) { - if (tripleMatch.isConcrete()) { + if (tripleMatch.getSubject().isConcrete() + && tripleMatch.getPredicate().isConcrete() + && tripleMatch.getObject().isConcrete()) { return MatchPattern.SUB_PRE_OBJ; } else { if (tripleMatch.getSubject().isConcrete()) { @@ -73,6 +82,15 @@ public static MatchPattern classify(Triple tripleMatch) { } } + /** + * Classify a triple match given as three nodes. + * Any {@code null} or non-concrete node is treated as a wildcard. + * + * @param sm subject node, or {@code null}/wildcard + * @param pm predicate node, or {@code null}/wildcard + * @param om object node, or {@code null}/wildcard + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Node sm, Node pm, Node om) { if (null != sm && sm.isConcrete()) { if (null != pm && pm.isConcrete()) { @@ -103,6 +121,5 @@ public static MatchPattern classify(Node sm, Node pm, Node om) { } } } - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java index 43bbfeeaea8..a5033c22cde 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java @@ -21,52 +21,57 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a contiguous array slice {@code [0, toIndex)}, + * iterating from high index to low. Supports splitting into + * {@link ArraySubSpliterator} children for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySpliterator implements Spliterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public ArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (-1 < --pos) { action.accept(entries[pos]); return true; @@ -79,7 +84,7 @@ public void forEachRemaining(Consumer super E> action) { while (-1 < --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -89,7 +94,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -101,4 +106,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java index 74994708b53..638f2bb0c9e 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java @@ -21,55 +21,61 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a contiguous array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low. Produced by splitting an + * {@link ArraySpliterator} (or another {@link ArraySubSpliterator}); supports + * further recursive splits for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public ArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (fromIndex <= --pos) { action.accept(entries[pos]); return true; @@ -82,7 +88,7 @@ public void forEachRemaining(Consumer super E> action) { while (fromIndex <= --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -93,7 +99,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -105,4 +111,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java deleted file mode 100644 index 704c9642706..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.spliterator; - -import java.util.Spliterator; -import java.util.function.Consumer; - -import org.apache.jena.mem.collection.FastHashSet; - -/** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * This spliterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * This spliterator works in ascending order, starting from the given start up to the specified exclusive index. - * - * This spliterator supports splitting into sub-spliterators. - * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedSpliterator implements Spliterator> { - - private final E[] entries; - private int currentPositionMinusOne; - private final int toIndexExclusive; - private final Runnable checkForConcurrentModification; - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param fromIndexInclusive the index of the first element, inclusive - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int fromIndexInclusive, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.currentPositionMinusOne = fromIndexInclusive-1; // Start at fromIndexInclusive - 1, so that the first call to tryAdvance will increment pos to fromIndexInclusive - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this(entries, 0, toIndexExclusive, checkForConcurrentModification); - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); - } - - - @Override - public boolean tryAdvance(Consumer super FastHashSet.IndexedKey> action) { - this.checkForConcurrentModification.run(); - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - return true; - } - } - return false; - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - } - } - this.checkForConcurrentModification.run(); - } - - @Override - public Spliterator> trySplit() { - final var nextPos = currentPositionMinusOne + 1; - final var remaining = toIndexExclusive - nextPos; - if ( remaining < 2) { - return null; - } - final var mid = nextPos + ( remaining >>> 1); - final var fromIndexInclusive = nextPos; - this.currentPositionMinusOne = mid-1; - return new SparseArrayIndexedSpliterator<>(entries, fromIndexInclusive, mid, checkForConcurrentModification); - } - - @Override - public long estimateSize() { return (long) toIndexExclusive - currentPositionMinusOne; } - - @Override - public int characteristics() { - return DISTINCT | NONNULL | IMMUTABLE; - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java index 6752cc9a1c1..add45739dc2 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java @@ -21,17 +21,24 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a sparse array slice {@code [0, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced for backing arrays such as those of + * {@link org.apache.jena.mem.collection.FastHashBase}, where removed slots + * are represented by {@code null}. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Supports splitting into {@link SparseArraySubSpliterator} children for + * parallel traversal. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it at each + * advance/forEach boundary; throws {@link ConcurrentModificationException} + * if the size has changed. * * @param the type of the array elements */ @@ -39,35 +46,37 @@ public class SparseArraySpliterator implements Spliterator { private final E[] entries; private int pos; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}, skipping nulls. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public SparseArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (-1 < --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -86,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -96,7 +105,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java index 3eb0784326f..d79242ac78c 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java @@ -21,55 +21,62 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a sparse array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced by splitting a {@link SparseArraySpliterator} (or another + * {@link SparseArraySubSpliterator}); supports further recursive splits for + * parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary; + * throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the type of the array elements */ public class SparseArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}, skipping nulls. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public SparseArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (fromIndex <= --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -88,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -99,7 +106,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java index 07ccc9634a9..f0fba805175 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java @@ -32,26 +32,41 @@ import java.util.function.Predicate; /** - * An ArrayBunch implements TripleBunch with a linear search of a short-ish - * array of Triples. The array grows by factor 2. + * Linear-scan implementation of {@link FastTripleBunch} backed by a packed + * {@link Triple} array. Used as long as a bunch stays small; once it grows + * past the configured threshold (see {@link FastTripleStore}) it is replaced + * with a {@link FastHashedTripleBunch}. + * + * The array grows by a factor of two when full. Equality of triples within a + * bunch is delegated to {@link #areEqual(Triple, Triple)}, which subclasses + * specialize to compare only the two nodes that are not already + * implied by the enclosing map's key. This avoids redundant equality checks + * on the shared subject/predicate/object. + * + * Not thread-safe. */ public abstract class FastArrayBunch implements FastTripleBunch { private static final int INITIAL_SIZE = 4; + /** Number of valid entries in {@link #elements}. */ protected int size = 0; + /** Packed array of triples; entries from {@code 0} to {@code size-1} are live. */ protected Triple[] elements; + /** + * Creates an empty bunch with the default initial capacity. + */ protected FastArrayBunch() { elements = new Triple[INITIAL_SIZE]; } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. - * But it will reserve only the space needed to contain them. Growing is still possible. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}; its backing array is sized to fit exactly, + * but can grow further if needed. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -59,7 +74,17 @@ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.size = bunchToCopy.size; } - public abstract boolean areEqual(final Triple a, final Triple b); + /** + * Compare two triples for equality within this bunch. + * + * Subclasses specialize this to skip the already-shared component + * (subject, predicate or object) and compare only the remaining two. + * + * @param a first triple + * @param b second triple + * @return {@code true} if the triples are considered equal in this bunch + */ + protected abstract boolean areEqual(final Triple a, final Triple b); @Override public boolean containsKey(Triple t) { @@ -127,6 +152,7 @@ public boolean tryRemove(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return true; } } @@ -138,6 +164,7 @@ public void removeUnchecked(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return; } } @@ -174,11 +201,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java index b89d3312048..a49d6b54009 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java @@ -25,21 +25,28 @@ import org.apache.jena.mem.collection.FastHashMap; /** - * Map from nodes to triple bunches. + * {@link FastHashMap} specialized to map a {@link Node} to its associated + * {@link FastTripleBunch}. Used by {@link FastTripleStore} to maintain the + * three subject/predicate/object indices. */ public class FastHashedBunchMap extends FastHashMap implements Copyable { + /** + * Creates an empty bunch map with the default initial capacity. + */ public FastHashedBunchMap() { super(); } /** - * Copy constructor. - * The new map will contain all the same nodes as keys of the map to copy, but copies of the bunches as values . + * Copy constructor. The new map has the same node keys as + * {@code mapToCopy}; each value is replaced by a deep copy of the + * corresponding bunch (via {@link FastTripleBunch#copy()}) so that + * mutations of either map cannot affect the other. * - * @param mapToCopy + * @param mapToCopy the source map */ private FastHashedBunchMap(final FastHashedBunchMap mapToCopy) { super(mapToCopy, FastTripleBunch::copy); diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java index 459e78c8181..65c9ab70fbf 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java @@ -25,13 +25,21 @@ import org.apache.jena.mem.collection.JenaSet; /** - * A set of triples - backed by {@link FastHashSet}. + * Hashed implementation of {@link FastTripleBunch} built on top of + * {@link FastHashSet}. Used by {@link FastTripleStore} once a bunch grows + * past the size threshold at which a linear-scan {@link FastArrayBunch} + * stops being faster. */ public class FastHashedTripleBunch extends FastHashSet implements FastTripleBunch { + /** - * Create a new triple bunch from the given set of triples. + * Create a new hashed bunch pre-populated from the given set of triples. + * The initial capacity is chosen at 1.5x the source size, so the new bunch + * fits the existing triples and has some headroom for growth before it + * needs to rehash. * - * @param set the set of triples + * @param set the source set of triples (typically the array bunch being + * promoted) */ public FastHashedTripleBunch(final JenaSet set) { super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth @@ -39,15 +47,18 @@ public FastHashedTripleBunch(final JenaSet set) { } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ private FastHashedTripleBunch(final FastHashedTripleBunch bunchToCopy) { super(bunchToCopy); } + /** + * Creates an empty hashed bunch with the default initial capacity. + */ public FastHashedTripleBunch() { super(); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java index 68f79e72f8a..fe050283188 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java @@ -29,27 +29,39 @@ import java.util.function.Predicate; /** - * A bunch of triples - a stripped-down set with specialized methods. A - * bunch is expected to store triples that share some useful property - * (such as having the same subject or predicate). + * Set-like container for a "bunch" of triples that share some useful + * property - typically they all have the same subject, predicate or object, + * because the bunch is the value of a node-keyed map in a + * {@link FastTripleStore}. + * + * The interface is a stripped-down set with a few extras tuned for the + * triple-store hot path; concrete implementations are + * {@link FastArrayBunch} (linear scan, used while the bunch is small) and + * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a + * threshold). */ public interface FastTripleBunch extends JenaSetHashOptimized, Copyable { /** - * Answer true iff this bunch is implemented as an array. - * This field is used to optimize some operations by avoiding the need for instanceOf tests. + * Answer {@code true} iff this bunch is backed by a flat array (i.e. is + * a {@link FastArrayBunch}). Exposed as an explicit method so callers can + * avoid {@code instanceof} checks on this hot path. * - * @return true iff this bunch is implemented as an arrays + * @return {@code true} if this bunch is array-backed */ boolean isArray(); /** - * This method is used to optimize _PO match operations. - * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches. - * This method is faster if there are many matches and the set is ordered in an unfavorable way. - * _PO matches usually fall into this category. + * Predicate test that scans elements in hash-table order rather than + * dense insertion order. Tuned for {@code _PO} (any-predicate-object) + * matches. + * + * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
+ * If the key is present, returns the (non-negative) probe-table slot + * index. If the key is absent, returns the bitwise complement of the + * empty probe-table slot at which the key would be inserted, allowing + * insertion to proceed without a second probe walk. + * + * @param e the key to locate + * @param hashCode {@code e.hashCode()} + * @return the position index if found, or {@code ~insertionPosition} if not + */ protected final int findPosition(final K e, final int hashCode) { var pIndex = calcStartIndexByHashCode(hashCode); while (true) { @@ -405,6 +452,15 @@ protected final int findPosition(final K e, final int hashCode) { } } + /** + * Locates the next empty slot in {@link #positions} along the probe chain + * for the given hash code, without checking any existing entries for + * equality. Used after a positions-array resize, when no duplicates can + * exist in the rebuilt table. + * + * @param hashCode the hash code being placed + * @return the index of an empty slot in the probe table + */ protected final int findEmptySlotWithoutEqualityCheck(final int hashCode) { var pIndex = calcStartIndexByHashCode(hashCode); while (true) { @@ -435,11 +491,63 @@ public void clear() { @Override public final Spliterator keySpliterator() { - final var initialSize = this.size(); - final Runnable checkForConcurrentModification = () -> - { - if (this.size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(keys, keysPos, checkForConcurrentModification); + return new SparseArraySpliterator<>(keys, keysPos, this); + } + + /** + * Gets the key at the given index. + * Array bounds are not checked. The caller must ensure the index is valid and corresponds to a non-null key. + * + * @param i the index + * @return the key at the given index + */ + public K getKeyAt(int i) { + return keys[i]; + } + + /** + * Returns the index of the entry holding {@code key}, or {@code -1} if not present. + * + * @param key the key to look up + * @return the entry index, or {@code -1} if the key is absent + */ + public int indexOf(K key) { + final var pIndex = findPosition(key, key.hashCode()); + if (pIndex < 0) { + return -1; + } else { + return ~positions[pIndex]; + } + } + + /** + * Functional interface used by {@link #forEachKey} to receive each live + * key along with the stable index it occupies. + * + * @param the key type + */ + @FunctionalInterface + public interface KeyAndIndexConsumer { + /** + * Receive a single key and its index. + * + * @param key the key + * @param index the stable index of the key in the underlying array + */ + void accept(K key, int index); + } + + /** + * Sequentially invokes {@code consumer} for every live key with its index. + * Skips freed slots. + * + * @param consumer receives each key/index pair + */ + public void forEachKey(KeyAndIndexConsumer consumer) { + for (int i = 0; i < keysPos; i++) { + if(keys[i] != null) { + consumer.accept(keys[i], i); + } + } } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java index 04c2761416b..e3f741ba485 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashMap.java @@ -25,39 +25,56 @@ import org.apache.jena.mem.spliterator.SparseArraySpliterator; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Supplier; import java.util.function.UnaryOperator; /** - * Map which grows, if needed but never shrinks. - * This map does not guarantee any order. Although due to the way it is implemented the elements have a certain order. - * This map does not allow null keys. - * This map is not thread safe. - * It´s purpose is to support fast add, remove, contains and stream / iterate operations. - * Only remove operations are not as fast as in {@link java.util.HashMap} - * Iterating over this map does not get much faster again after removing elements because the map is not compacted. + * Hash map specialization built on top of {@link FastHashBase}. + * Grows on demand but never shrinks, does not guarantee iteration order, + * does not allow {@code null} keys, and is not thread-safe. + * + * Optimized for fast {@code add} / {@code containsKey} / {@code stream} / + * iterate operations. Removal is somewhat slower than in + * {@link java.util.HashMap} because of the back-shifting performed on the + * probe table. Iteration speed does not recover after many removals because + * the dense {@code keys} array is not compacted. + * + * @param the key type + * @param the value type */ -public abstract class FastHashMap extends FastHashBase implements JenaMap { +public abstract class FastHashMap extends FastHashBase implements JenaMapIndexed { + /** + * Parallel array to {@code keys} holding the value associated with each + * stored key. {@code values[i]} is the value for {@code keys[i]} when + * {@code keys[i]} is non-null. + */ protected V[] values; + /** + * Creates a map with the given initial key-array capacity. + * + * @param initialSize the initial capacity of the keys/values arrays + */ protected FastHashMap(int initialSize) { super(initialSize); this.values = newValuesArray(keys.length); } + /** + * Creates a map with the default initial capacity. + */ protected FastHashMap() { super(); this.values = newValuesArray(keys.length); } /** - * Copy constructor. - * The new map will contain all the same keys and values of the map to copy. + * Copy constructor. The new map contains the same keys and the same + * value references as {@code mapToCopy}. * - * @param mapToCopy + * @param mapToCopy the source map */ protected FastHashMap(final FastHashMap mapToCopy) { super(mapToCopy); @@ -66,10 +83,13 @@ protected FastHashMap(final FastHashMap mapToCopy) { } /** - * Copy constructor with value processor. + * Copy constructor that transforms each value via {@code valueProcessor}. + * Useful when the values are mutable and need to be deep-copied to keep + * the new map independent from the source. * - * @param mapToCopy - * @param valueProcessor + * @param mapToCopy the source map + * @param valueProcessor function applied to every non-null value to obtain + * the value to put in the new map */ protected FastHashMap(final FastHashMap mapToCopy, final UnaryOperator valueProcessor) { super(mapToCopy); @@ -82,6 +102,12 @@ protected FastHashMap(final FastHashMap mapToCopy, final UnaryOperator } } + /** + * Gets a new array of values with the given size. + * + * @param size the size of the array + * @return the new array + */ protected abstract V[] newValuesArray(int size); @Override @@ -106,12 +132,10 @@ public void clear() { @Override public boolean tryPut(K key, V value) { + growPositionsArrayIfNeeded(); final var hashCode = key.hashCode(); - var pIndex = findPosition(key, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { - if (tryGrowPositionsArrayIfNeeded()) { - pIndex = findPosition(key, hashCode); - } final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; values[eIndex] = value; @@ -126,12 +150,10 @@ public boolean tryPut(K key, V value) { @Override public void put(K key, V value) { + growPositionsArrayIfNeeded(); final var hashCode = key.hashCode(); - var pIndex = findPosition(key, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { - if (tryGrowPositionsArrayIfNeeded()) { - pIndex = findPosition(key, hashCode); - } final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; values[eIndex] = value; @@ -142,8 +164,27 @@ public void put(K key, V value) { } } + @Override + public int putAndGetIndex(K key, V value) { + growPositionsArrayIfNeeded(); + final int hashCode = key.hashCode(); + final var pIndex = findPosition(key, hashCode); + final int eIndex; + if (pIndex < 0) { + eIndex = getFreeKeyIndex(); + keys[eIndex] = key; + hashCodesOrDeletedIndices[eIndex] = hashCode; + positions[~pIndex] = ~eIndex; + } else { + eIndex = ~positions[pIndex]; + } + values[eIndex] = value; + return eIndex; + } + /** * Returns the value at the given index. + * Array bounds are not checked. The caller must ensure the index is valid and corresponds to a non-null key. * * @param i index * @return value @@ -178,12 +219,12 @@ public V computeIfAbsent(K key, Supplier absentValueSupplier) { var pIndex = findPosition(key, hashCode); if (pIndex < 0) { if (tryGrowPositionsArrayIfNeeded()) { - pIndex = findPosition(key, hashCode); + pIndex = ~findEmptySlotWithoutEqualityCheck(hashCode); } + final var value = absentValueSupplier.get(); final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; - final var value = absentValueSupplier.get(); values[eIndex] = value; positions[~pIndex] = ~eIndex; return value; @@ -194,18 +235,20 @@ public V computeIfAbsent(K key, Supplier absentValueSupplier) { @Override public void compute(K key, UnaryOperator valueProcessor) { - final int hashCode = key.hashCode(); + final var hashCode = key.hashCode(); var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var value = valueProcessor.apply(null); if (value == null) return; + if(tryGrowPositionsArrayIfNeeded()) { + pIndex = ~findEmptySlotWithoutEqualityCheck(hashCode); + } final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; values[eIndex] = value; positions[~pIndex] = ~eIndex; - tryGrowPositionsArrayIfNeeded(); } else { var eIndex = ~positions[pIndex]; final var value = valueProcessor.apply(values[eIndex]); @@ -217,24 +260,13 @@ public void compute(K key, UnaryOperator valueProcessor) { } } - @Override public ExtendedIterator valueIterator() { - final var initialSize = size(); - final Runnable checkForConcurrentModification = () -> - { - if (size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(values, keysPos, checkForConcurrentModification); + return new SparseArrayIterator<>(values, keysPos, this); } @Override public Spliterator valueSpliterator() { - final var initialSize = this.size(); - final Runnable checkForConcurrentModification = () -> - { - if (this.size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(values, keysPos, checkForConcurrentModification); + return new SparseArraySpliterator<>(values, keysPos, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java index 134a0092e22..5adf3232c4e 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java @@ -21,39 +21,42 @@ package org.apache.jena.mem.collection; -import org.apache.jena.mem.iterator.SparseArrayIndexedIterator; -import org.apache.jena.mem.spliterator.SparseArrayIndexedSpliterator; -import org.apache.jena.util.iterator.ExtendedIterator; - -import java.util.ConcurrentModificationException; -import java.util.Spliterator; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - /** - * Set which grows, if needed but never shrinks. - * This set does not guarantee any order. Although due to the way it is implemented the elements have a certain order. - * This set does not allow null values. - * This set is not thread safe. - * It´s purpose is to support fast add, remove, contains and stream / iterate operations. - * Only remove operations are not as fast as in {@link java.util.HashSet} - * Iterating over this set not get much faster again after removing elements because the set is not compacted. + * Hash set specialization built on top of {@link FastHashBase}. + * Grows on demand but never shrinks, does not guarantee iteration order, + * does not allow {@code null} elements, and is not thread-safe. + * + * Optimized for fast {@code add} / {@code containsKey} / {@code stream} / + * iterate operations. Removal is somewhat slower than in + * {@link java.util.HashSet} because of the back-shifting performed on the + * probe table. Iteration speed does not recover after many removals because + * the dense {@code keys} array is not compacted. + * + * @param the element type */ -public abstract class FastHashSet extends FastHashBase implements JenaSetHashOptimized { +public abstract class FastHashSet extends FastHashBase implements JenaSetIndexed { - protected FastHashSet(int initialSize) { + /** + * Creates a set with the given initial key-array capacity. + * + * @param initialSize the initial capacity of the keys array + */ + public FastHashSet(final int initialSize) { super(initialSize); } - protected FastHashSet() { + /** + * Creates a set with the default initial capacity. + */ + public FastHashSet() { super(); } /** - * Copy constructor. - * The new set will contain all the same keys of the set to copy. + * Copy constructor. The new set contains the same elements as + * {@code setToCopy}. * - * @param setToCopy + * @param setToCopy the source set */ protected FastHashSet(final FastHashSet setToCopy) { super(setToCopy); @@ -65,12 +68,12 @@ public boolean tryAdd(K key) { } @Override - public boolean tryAdd(K value, int hashCode) { + public boolean tryAdd(K key, int hashCode) { growPositionsArrayIfNeeded(); - var pIndex = findPosition(value, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var eIndex = getFreeKeyIndex(); - keys[eIndex] = value; + keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; positions[~pIndex] = ~eIndex; return true; @@ -79,28 +82,23 @@ public boolean tryAdd(K value, int hashCode) { } /** - * Add and get the index of the added element. + * Add an element and return the index it was stored at. + * If the element is already present, returns the bitwise complement + * ({@code ~existingIndex}) of the existing index, so callers can + * distinguish "newly inserted" from "already present" while still + * recovering the index in both cases. * - * @param value the value to add - * @return the index of the added element or the inverse (~) index of the existing element + * @param key the element to add + * @return the new index, or {@code ~existingIndex} if already present */ - public int addAndGetIndex(K value) { - return addAndGetIndex(value, value.hashCode()); - } - - /** - * Add and get the index of the added element. - * - * @param value the value to add - * @param hashCode the hash code of the value. This is a performance optimization. - * @return the index of the added element or the inverse (~) index of the existing element - */ - public int addAndGetIndex(final K value, final int hashCode) { + @Override + public int addAndGetIndex(K key) { growPositionsArrayIfNeeded(); - final var pIndex = findPosition(value, hashCode); + final var hashCode = key.hashCode(); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var eIndex = getFreeKeyIndex(); - keys[eIndex] = value; + keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; positions[~pIndex] = ~eIndex; return eIndex; @@ -132,62 +130,4 @@ public void addUnchecked(K value, int hashCode) { public K getKeyAt(int i) { return keys[i]; } - - /** - * Entry pairing a key with its index in the set. - * @param index index of the key in the set - * @param key the key - * @param the type of the key - */ - public record IndexedKey(int index, K key) {} - - /** - * Get an iterator over pairs of keys and their indices in the set. - * The iterator is not thread safe. - * - * @return an iterator over pairs of keys and their indices in the set - */ - public final ExtendedIterator> indexedKeyIterator() { - final var initialSize = size(); - final Runnable checkForConcurrentModification = () -> - { - if (size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIndexedIterator<>(keys, keysPos, checkForConcurrentModification); - } - - /** - * Get a spliterator over pairs of keys and their indices in the set. - * The spliterator is not thread safe. - * - * @return a spliterator over pairs of keys and their indices in the set - */ - public final Spliterator> indexedKeySpliterator() { - final var initialSize = this.size(); - final Runnable checkForConcurrentModification = () -> - { - if (this.size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIndexedSpliterator<>(keys, keysPos, checkForConcurrentModification); - } - - /** - * Get a stream over pairs of keys and their indices in the set. - * The stream is not thread safe. - * - * @return a stream over pairs of keys and their indices in the set - */ - public final Stream> indexedKeyStream() { - return StreamSupport.stream(indexedKeySpliterator(), false); - } - - /** - * Get a parallel stream over pairs of keys and their indices in the set. - * The stream is not thread safe. - * - * @return a parallel stream over pairs of keys and their indices in the set - */ - public final Stream> indexedKeyStreamParallel() { - return StreamSupport.stream(indexedKeySpliterator(), true); - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java index 5664a900170..b277789c717 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java @@ -25,7 +25,6 @@ import org.apache.jena.shared.JenaException; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Predicate; @@ -36,7 +35,7 @@ * * @param the element type */ -public abstract class HashCommonBase { +public abstract class HashCommonBase implements JenaMapSetCommon { /** * Jeremy suggests, from his experiments, that load factors more than * 0.6 leave the table too dense, and little advantage is gained below 0.4. @@ -78,7 +77,7 @@ protected HashCommonBase(int initialCapacity) { * Copy constructor. * The new table will contain all the same keys of the table to copy. * - * @param baseToCopy + * @param baseToCopy the table to copy */ protected HashCommonBase(final HashCommonBase baseToCopy) { this.keys = newKeysArray(baseToCopy.keys.length); @@ -209,18 +208,10 @@ public boolean anyMatch(final Predicate predicate) { } public ExtendedIterator keyIterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(keys, checkForConcurrentModification); + return new SparseArrayIterator<>(keys, this); } public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(keys, checkForConcurrentModification); + return new SparseArraySpliterator<>(keys, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java index 62e7bd56733..dcdd5557654 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java @@ -24,7 +24,6 @@ import org.apache.jena.mem.spliterator.SparseArraySpliterator; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Supplier; import java.util.function.UnaryOperator; @@ -207,19 +206,11 @@ protected void removeFrom(int here) { @Override public ExtendedIterator valueIterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(values, checkForConcurrentModification); + return new SparseArrayIterator<>(values, this); } @Override public Spliterator valueSpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(values, checkForConcurrentModification); + return new SparseArraySpliterator<>(values, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java index 3e13613b08f..6d2423e0097 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java @@ -30,6 +30,7 @@ /** * A map from keys of type {@code K} to values of type {@code V}. + * Not thread-safe and does not allow {@code null} keys. * * @param the type of the keys in the map * @param the type of the values in the map diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java new file mode 100644 index 00000000000..67c366d00eb --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.mem.collection; + +/** + * Extension of {@link JenaMap} that exposes index-based access and lets callers + * supply a precomputed hash code for the key. Indices are stable handles to + * entries (returned by {@link #putAndGetIndex(Object, Object)}) and remain + * valid until the corresponding entry is removed. + * + * The hash-code overloads are a performance shortcut for callers that already + * have the hash at hand (typically because the same key is stored in several + * collections). The supplied hash code MUST equal {@code key.hashCode()}, or + * the map will misbehave. + * + * @param the type of the keys in the map + * @param the type of the values in the map + */ +public interface JenaMapIndexed extends JenaMap { + + /** + * Returns the index of the entry with the given key, or a negative value + * if no such entry exists. + * + * @param key the key to look up + * @return the index of the entry, or a negative value if absent + */ + int indexOf(K key); + + /** + * Returns the key stored at the given index. + * + * @param index the index of the entry + * @return the key at that index + */ + K getKeyAt(int index); + + /** + * Returns the value stored at the given index. + * + * @param index the index of the entry + * @return the value at that index + */ + V getValueAt(int index); + + /** + * Put a key-value pair and return the index of the affected entry. + * If the key is already present, its value is updated and the existing + * index is returned. + * + * @param key the key to put + * @param value the value to put + * @return the index of the entry holding {@code key} + */ + int putAndGetIndex(K key, V value); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java index 2533714ce6b..7f96baa19f9 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java @@ -28,22 +28,23 @@ import java.util.stream.StreamSupport; /** - * Common interface for {@link JenaMap} and {@link JenaSet}. * + * Operations shared between the map ({@link JenaMap}) and the set + * ({@link JenaSet}) collections used in the {@code mem} triple store + * implementations. + * + * These collections trade some flexibility for speed: they expose only the + * operations needed by triple-store internals (no full {@link java.util.Map} + * or {@link java.util.Set} contract). They are not thread-safe. * - * @param the type of the keys/elements in the collection + * @param the type of the keys (or elements, for sets) in the collection */ -public interface JenaMapSetCommon { +public interface JenaMapSetCommon extends Sized { /** * Clear the collection. */ void clear(); - /** - * @return the number of elements in the collection - */ - int size(); - /** * @return true if the collection is empty */ @@ -75,7 +76,10 @@ public interface JenaMapSetCommon { /** * Removes a key from the collection. - * Attention: Implementations may assume that the key is present. + * + * Attention: implementations may assume the key is present and may produce + * undefined behavior (including silently corrupting internal state) if it + * is not. Use {@link #tryRemove(Object)} when in doubt. * * @param key the key to remove */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java index d3b8a557be9..03848073f56 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java @@ -21,9 +21,10 @@ package org.apache.jena.mem.collection; /** - * Set interface specialized for the use cases in triple store implementations. + * Set interface specialized for the use cases in triple-store implementations. + * Not thread-safe; does not allow {@code null} elements. * - * @param + * @param the element type of the set */ public interface JenaSet extends JenaMapSetCommon { @@ -31,13 +32,16 @@ public interface JenaSet extends JenaMapSetCommon { * Add the key to the set if it is not already present. * * @param key the key to add - * @return true if the key was added, false if it was already present + * @return {@code true} if the key was added, {@code false} if it was already present */ boolean tryAdd(E key); /** - * Add the key to the set without checking if it is already present. - * Attention: This method must only be used if it is guaranteed that the key is not already present. + * Add the key to the set without checking whether it is already present. + * + * Attention: this method must only be used if the caller has ensured that + * the key is not already in the set; otherwise the set's invariants will + * break (duplicates may be inserted). * * @param key the key to add */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java index 8cc8aad8daf..0e1d032b356 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java @@ -22,17 +22,50 @@ /** - * Extension of {@link JenaSet} that allows to add and remove elements - * with a given hash code. - * This is useful if the hash code is already known. - * Attention: The hash code must be consistent with E::hashCode(). + * Extension of {@link JenaSet} that lets callers supply a precomputed hash + * code. + * + * Attention: any caller-supplied hash code MUST equal {@code E.hashCode()}; + * if it does not, the set will misbehave. + * + * @param the element type of the set */ public interface JenaSetHashOptimized extends JenaSet { + + /** + * Add an element with the given precomputed hash code if it is not + * already present. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + * @return {@code true} if added, {@code false} if already present + */ boolean tryAdd(E key, int hashCode); + /** + * Add an element with the given precomputed hash code without checking + * whether it is already present. The caller MUST ensure the key is absent. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + */ void addUnchecked(E key, int hashCode); + /** + * Try to remove an element with the given precomputed hash code. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + * @return {@code true} if removed, {@code false} if it was not present + */ boolean tryRemove(E key, int hashCode); + /** + * Remove an element assumed to be present, with the given precomputed + * hash code. Behavior is undefined if the element is not in the set. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + */ void removeUnchecked(E key, int hashCode); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java new file mode 100644 index 00000000000..c7c3d2e1ddb --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.mem.collection; + + +/** + * Extension of {@link JenaSetHashOptimized} that exposes index-based access to elements. + * Indices are stable handles to entries (returned by {@link #addAndGetIndex(Object)}) and remain + * valid until the corresponding entry is removed. + * + * @param the element type of the set + */ +public interface JenaSetIndexed extends JenaSetHashOptimized { + + /** + * Add an element and return the index it was stored at. If the element + * is already present, returns a negative value (typically the bitwise + * complement of the existing index). + * + * @param key the element to add + * @return the index of the inserted element, or a negative value if the + * element was already present + */ + int addAndGetIndex(final E key); + + /** + * Returns the element stored at the given index. + * + * @param index the index to read + * @return the element at that index + */ + E getKeyAt(int index); + + /** + * Returns the index of the given element, or a negative value if it is + * not in the set. + * + * @param key the element to look up + * @return the index of {@code key}, or a negative value if absent + */ + int indexOf(E key); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java new file mode 100644 index 00000000000..237740ce8e3 --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.mem.collection; + +/** + * Base interface for sized collections. + * It is typically used to detect concurrent modifications in iterators and spliterators + * by snapshotting the size at construction time and rechecking it at each advance/forEach boundary. + */ +public interface Sized { + + /** + * @return the number of elements in the collection + */ + int size(); +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java index b0ac6e994bb..8cfc8948a25 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java @@ -30,16 +30,27 @@ import java.util.function.Consumer; /** - * Iterator that iterates over the entries of sets which are contained in the given iterator of sets. + * Flat-map style iterator that yields every element of every {@link JenaSet} + * produced by the given parent iterator. Empty inner sets are silently + * skipped. Equivalent in spirit to a one-level {@code flatMap} but tailored + * to the {@link JenaSet} API and to {@link NiceIterator}. * - * @param the type of the elements + * @param the element type of the inner sets */ public class IteratorOfJenaSets extends NiceIterator { - final Iterator extends JenaSet> parentIterator; + /** Source iterator producing the sets to flatten. */ + private final Iterator extends JenaSet> parentIterator; - ExtendedIterator currentIterator; + /** Iterator over the keys of the set currently being consumed. */ + private ExtendedIterator currentIterator; + /** + * Create a flat iterator over the elements of every set produced by + * {@code parentIterator}. + * + * @param parentIterator the source iterator of sets + */ public IteratorOfJenaSets(Iterator extends JenaSet> parentIterator) { this.parentIterator = parentIterator; this.currentIterator = parentIterator.hasNext() diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java deleted file mode 100644 index 37f103eae25..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.iterator; - -import org.apache.jena.mem.collection.FastHashSet; -import org.apache.jena.util.iterator.NiceIterator; - -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.function.Consumer; - -/** - * An iterator over a sparse array, that skips null entries. - * This iterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * The iterator works in ascending order, starting from index 0 up to the specified exclusive index. - * - * This iterator will check for concurrent modifications by invoking a {@link Runnable} - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedIterator extends NiceIterator> implements Iterator> { - - private final E[] entries; - private final Runnable checkForConcurrentModification; - private int pos = 0; - private final int toIndexExclusive; - private boolean hasNext = false; - - public SparseArrayIndexedIterator(final E[] entries, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = entries.length; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - public SparseArrayIndexedIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Returns {@code true} if the iteration has more elements. - * (In other words, returns {@code true} if {@link #next} would - * return an element rather than throwing an exception.) - * - * @return {@code true} if the iteration has more elements - */ - @Override - public boolean hasNext() { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - hasNext = true; - return true; - } - pos++; - } - hasNext = false; - return false; - } - - /** - * Returns the next element in the iteration. - * - * @return the next element in the iteration - * @throws NoSuchElementException if the iteration has no more elements - */ - @Override - public FastHashSet.IndexedKey next() { - this.checkForConcurrentModification.run(); - if (hasNext || hasNext()) { - hasNext = false; - return new FastHashSet.IndexedKey<>(pos, entries[pos++]); - } - throw new NoSuchElementException(); - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - action.accept(new FastHashSet.IndexedKey<>(pos, entries[pos])); - } - pos++; - } - this.checkForConcurrentModification.run(); - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java index 936476a80ff..e0b79cd1ff6 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java @@ -21,34 +21,55 @@ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.Sized; import org.apache.jena.util.iterator.NiceIterator; -import java.util.Iterator; +import java.util.ConcurrentModificationException; import java.util.NoSuchElementException; import java.util.function.Consumer; /** - * An iterator over a sparse array, that skips null entries. + * Iterator over a sparse array, walking from high index to low and skipping + * {@code null} entries. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it on each call to + * {@link #next()} / {@link #forEachRemaining(Consumer)}; throws + * {@link ConcurrentModificationException} if the size has changed. * * @param the type of the array elements */ -public class SparseArrayIterator extends NiceIterator implements Iterator { +public class SparseArrayIterator extends NiceIterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; private boolean hasNext = false; - public SparseArrayIterator(final E[] entries, final Runnable checkForConcurrentModification) { + /** + * Iterate over the whole array. + * + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, final Sized set) { this.entries = entries; this.pos = entries.length - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } - public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { + /** + * Iterate over {@code entries[0 .. toIndexExclusive)} (in reverse order). + * + * @param entries the backing array (not copied) + * @param toIndexExclusive exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Sized set) { this.entries = entries; this.pos = toIndexExclusive - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** @@ -62,13 +83,11 @@ public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnab public boolean hasNext() { while (-1 < pos) { if (null != entries[pos]) { - hasNext = true; - return true; + return hasNext = true; } pos--; } - hasNext = false; - return false; + return hasNext = false; } /** @@ -79,7 +98,7 @@ public boolean hasNext() { */ @Override public E next() { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (hasNext || hasNext()) { hasNext = false; return entries[pos--]; @@ -95,6 +114,6 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java index 94008b155f1..d8536f56311 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java @@ -22,8 +22,17 @@ package org.apache.jena.mem.pattern; /** - * A pattern for matching triples. - * The pattern is defined by the wildcard positions for the subject, predicate and object. + * Categorizes a triple-match pattern by which of the subject, predicate and + * object slots are concrete and which are wildcards (i.e. {@code Node.ANY} + * or {@code null}). + * + * The eight enum values cover every possible combination. Triple-store + * implementations dispatch on this enum to pick the most efficient lookup + * path for each kind of pattern (e.g. a fully concrete {@link #SUB_PRE_OBJ} + * is answered directly from the triple set, while a partially open pattern + * such as {@link #ANY_PRE_OBJ} is answered through an index intersection). + * + * @see PatternClassifier */ public enum MatchPattern { /** diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java index 32a6ba182a1..e4cf5644eca 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java @@ -25,14 +25,15 @@ import org.apache.jena.graph.Triple; /** - * Classify a triple match into one of the 8 match patterns. + * Utility class that classifies a triple match into one of the eight + * {@link MatchPattern} values. * - * The classification is based on the concrete-ness of the subject, predicate and object. - * A concrete node is one that is not a variable. + * The classification is based on which of the subject, predicate and object + * are concrete (anything that is not a variable / wildcard / + * {@code null}) and which are wildcards. The result is used by triple-store + * implementations to dispatch to the most efficient lookup path. * - * The classification is used to select the most efficient implementation of a triple store. - * - * This is a utility class; there is no need to instantiate it. + * All operations are stateless; this class is not meant to be instantiated. * * @see MatchPattern */ @@ -41,8 +42,16 @@ public class PatternClassifier { private PatternClassifier() { } + /** + * Classify a triple match. + * + * @param tripleMatch the match triple, possibly containing wildcard nodes + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Triple tripleMatch) { - if (tripleMatch.isConcrete()) { + if (tripleMatch.getSubject().isConcrete() + && tripleMatch.getPredicate().isConcrete() + && tripleMatch.getObject().isConcrete()) { return MatchPattern.SUB_PRE_OBJ; } else { if (tripleMatch.getSubject().isConcrete()) { @@ -73,6 +82,15 @@ public static MatchPattern classify(Triple tripleMatch) { } } + /** + * Classify a triple match given as three nodes. + * Any {@code null} or non-concrete node is treated as a wildcard. + * + * @param sm subject node, or {@code null}/wildcard + * @param pm predicate node, or {@code null}/wildcard + * @param om object node, or {@code null}/wildcard + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Node sm, Node pm, Node om) { if (null != sm && sm.isConcrete()) { if (null != pm && pm.isConcrete()) { @@ -103,6 +121,5 @@ public static MatchPattern classify(Node sm, Node pm, Node om) { } } } - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java index 43bbfeeaea8..a5033c22cde 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java @@ -21,52 +21,57 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a contiguous array slice {@code [0, toIndex)}, + * iterating from high index to low. Supports splitting into + * {@link ArraySubSpliterator} children for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySpliterator implements Spliterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public ArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (-1 < --pos) { action.accept(entries[pos]); return true; @@ -79,7 +84,7 @@ public void forEachRemaining(Consumer super E> action) { while (-1 < --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -89,7 +94,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -101,4 +106,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java index 74994708b53..638f2bb0c9e 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java @@ -21,55 +21,61 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a contiguous array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low. Produced by splitting an + * {@link ArraySpliterator} (or another {@link ArraySubSpliterator}); supports + * further recursive splits for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public ArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (fromIndex <= --pos) { action.accept(entries[pos]); return true; @@ -82,7 +88,7 @@ public void forEachRemaining(Consumer super E> action) { while (fromIndex <= --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -93,7 +99,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -105,4 +111,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java deleted file mode 100644 index 704c9642706..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.spliterator; - -import java.util.Spliterator; -import java.util.function.Consumer; - -import org.apache.jena.mem.collection.FastHashSet; - -/** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * This spliterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * This spliterator works in ascending order, starting from the given start up to the specified exclusive index. - * - * This spliterator supports splitting into sub-spliterators. - * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedSpliterator implements Spliterator> { - - private final E[] entries; - private int currentPositionMinusOne; - private final int toIndexExclusive; - private final Runnable checkForConcurrentModification; - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param fromIndexInclusive the index of the first element, inclusive - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int fromIndexInclusive, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.currentPositionMinusOne = fromIndexInclusive-1; // Start at fromIndexInclusive - 1, so that the first call to tryAdvance will increment pos to fromIndexInclusive - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this(entries, 0, toIndexExclusive, checkForConcurrentModification); - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); - } - - - @Override - public boolean tryAdvance(Consumer super FastHashSet.IndexedKey> action) { - this.checkForConcurrentModification.run(); - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - return true; - } - } - return false; - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - } - } - this.checkForConcurrentModification.run(); - } - - @Override - public Spliterator> trySplit() { - final var nextPos = currentPositionMinusOne + 1; - final var remaining = toIndexExclusive - nextPos; - if ( remaining < 2) { - return null; - } - final var mid = nextPos + ( remaining >>> 1); - final var fromIndexInclusive = nextPos; - this.currentPositionMinusOne = mid-1; - return new SparseArrayIndexedSpliterator<>(entries, fromIndexInclusive, mid, checkForConcurrentModification); - } - - @Override - public long estimateSize() { return (long) toIndexExclusive - currentPositionMinusOne; } - - @Override - public int characteristics() { - return DISTINCT | NONNULL | IMMUTABLE; - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java index 6752cc9a1c1..add45739dc2 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java @@ -21,17 +21,24 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a sparse array slice {@code [0, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced for backing arrays such as those of + * {@link org.apache.jena.mem.collection.FastHashBase}, where removed slots + * are represented by {@code null}. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Supports splitting into {@link SparseArraySubSpliterator} children for + * parallel traversal. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it at each + * advance/forEach boundary; throws {@link ConcurrentModificationException} + * if the size has changed. * * @param the type of the array elements */ @@ -39,35 +46,37 @@ public class SparseArraySpliterator implements Spliterator { private final E[] entries; private int pos; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}, skipping nulls. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public SparseArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (-1 < --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -86,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -96,7 +105,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java index 3eb0784326f..d79242ac78c 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java @@ -21,55 +21,62 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a sparse array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced by splitting a {@link SparseArraySpliterator} (or another + * {@link SparseArraySubSpliterator}); supports further recursive splits for + * parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary; + * throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the type of the array elements */ public class SparseArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}, skipping nulls. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public SparseArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (fromIndex <= --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -88,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -99,7 +106,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java index 07ccc9634a9..f0fba805175 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java @@ -32,26 +32,41 @@ import java.util.function.Predicate; /** - * An ArrayBunch implements TripleBunch with a linear search of a short-ish - * array of Triples. The array grows by factor 2. + * Linear-scan implementation of {@link FastTripleBunch} backed by a packed + * {@link Triple} array. Used as long as a bunch stays small; once it grows + * past the configured threshold (see {@link FastTripleStore}) it is replaced + * with a {@link FastHashedTripleBunch}. + * + * The array grows by a factor of two when full. Equality of triples within a + * bunch is delegated to {@link #areEqual(Triple, Triple)}, which subclasses + * specialize to compare only the two nodes that are not already + * implied by the enclosing map's key. This avoids redundant equality checks + * on the shared subject/predicate/object. + * + * Not thread-safe. */ public abstract class FastArrayBunch implements FastTripleBunch { private static final int INITIAL_SIZE = 4; + /** Number of valid entries in {@link #elements}. */ protected int size = 0; + /** Packed array of triples; entries from {@code 0} to {@code size-1} are live. */ protected Triple[] elements; + /** + * Creates an empty bunch with the default initial capacity. + */ protected FastArrayBunch() { elements = new Triple[INITIAL_SIZE]; } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. - * But it will reserve only the space needed to contain them. Growing is still possible. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}; its backing array is sized to fit exactly, + * but can grow further if needed. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -59,7 +74,17 @@ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.size = bunchToCopy.size; } - public abstract boolean areEqual(final Triple a, final Triple b); + /** + * Compare two triples for equality within this bunch. + * + * Subclasses specialize this to skip the already-shared component + * (subject, predicate or object) and compare only the remaining two. + * + * @param a first triple + * @param b second triple + * @return {@code true} if the triples are considered equal in this bunch + */ + protected abstract boolean areEqual(final Triple a, final Triple b); @Override public boolean containsKey(Triple t) { @@ -127,6 +152,7 @@ public boolean tryRemove(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return true; } } @@ -138,6 +164,7 @@ public void removeUnchecked(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return; } } @@ -174,11 +201,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java index b89d3312048..a49d6b54009 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java @@ -25,21 +25,28 @@ import org.apache.jena.mem.collection.FastHashMap; /** - * Map from nodes to triple bunches. + * {@link FastHashMap} specialized to map a {@link Node} to its associated + * {@link FastTripleBunch}. Used by {@link FastTripleStore} to maintain the + * three subject/predicate/object indices. */ public class FastHashedBunchMap extends FastHashMap implements Copyable { + /** + * Creates an empty bunch map with the default initial capacity. + */ public FastHashedBunchMap() { super(); } /** - * Copy constructor. - * The new map will contain all the same nodes as keys of the map to copy, but copies of the bunches as values . + * Copy constructor. The new map has the same node keys as + * {@code mapToCopy}; each value is replaced by a deep copy of the + * corresponding bunch (via {@link FastTripleBunch#copy()}) so that + * mutations of either map cannot affect the other. * - * @param mapToCopy + * @param mapToCopy the source map */ private FastHashedBunchMap(final FastHashedBunchMap mapToCopy) { super(mapToCopy, FastTripleBunch::copy); diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java index 459e78c8181..65c9ab70fbf 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java @@ -25,13 +25,21 @@ import org.apache.jena.mem.collection.JenaSet; /** - * A set of triples - backed by {@link FastHashSet}. + * Hashed implementation of {@link FastTripleBunch} built on top of + * {@link FastHashSet}. Used by {@link FastTripleStore} once a bunch grows + * past the size threshold at which a linear-scan {@link FastArrayBunch} + * stops being faster. */ public class FastHashedTripleBunch extends FastHashSet implements FastTripleBunch { + /** - * Create a new triple bunch from the given set of triples. + * Create a new hashed bunch pre-populated from the given set of triples. + * The initial capacity is chosen at 1.5x the source size, so the new bunch + * fits the existing triples and has some headroom for growth before it + * needs to rehash. * - * @param set the set of triples + * @param set the source set of triples (typically the array bunch being + * promoted) */ public FastHashedTripleBunch(final JenaSet set) { super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth @@ -39,15 +47,18 @@ public FastHashedTripleBunch(final JenaSet set) { } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ private FastHashedTripleBunch(final FastHashedTripleBunch bunchToCopy) { super(bunchToCopy); } + /** + * Creates an empty hashed bunch with the default initial capacity. + */ public FastHashedTripleBunch() { super(); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java index 68f79e72f8a..fe050283188 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java @@ -29,27 +29,39 @@ import java.util.function.Predicate; /** - * A bunch of triples - a stripped-down set with specialized methods. A - * bunch is expected to store triples that share some useful property - * (such as having the same subject or predicate). + * Set-like container for a "bunch" of triples that share some useful + * property - typically they all have the same subject, predicate or object, + * because the bunch is the value of a node-keyed map in a + * {@link FastTripleStore}. + * + * The interface is a stripped-down set with a few extras tuned for the + * triple-store hot path; concrete implementations are + * {@link FastArrayBunch} (linear scan, used while the bunch is small) and + * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a + * threshold). */ public interface FastTripleBunch extends JenaSetHashOptimized, Copyable { /** - * Answer true iff this bunch is implemented as an array. - * This field is used to optimize some operations by avoiding the need for instanceOf tests. + * Answer {@code true} iff this bunch is backed by a flat array (i.e. is + * a {@link FastArrayBunch}). Exposed as an explicit method so callers can + * avoid {@code instanceof} checks on this hot path. * - * @return true iff this bunch is implemented as an arrays + * @return {@code true} if this bunch is array-backed */ boolean isArray(); /** - * This method is used to optimize _PO match operations. - * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches. - * This method is faster if there are many matches and the set is ordered in an unfavorable way. - * _PO matches usually fall into this category. + * Predicate test that scans elements in hash-table order rather than + * dense insertion order. Tuned for {@code _PO} (any-predicate-object) + * matches. + * + * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
+ * Optimized for fast {@code add} / {@code containsKey} / {@code stream} / + * iterate operations. Removal is somewhat slower than in + * {@link java.util.HashMap} because of the back-shifting performed on the + * probe table. Iteration speed does not recover after many removals because + * the dense {@code keys} array is not compacted. + * + * @param the key type + * @param the value type */ -public abstract class FastHashMap extends FastHashBase implements JenaMap { +public abstract class FastHashMap extends FastHashBase implements JenaMapIndexed { + /** + * Parallel array to {@code keys} holding the value associated with each + * stored key. {@code values[i]} is the value for {@code keys[i]} when + * {@code keys[i]} is non-null. + */ protected V[] values; + /** + * Creates a map with the given initial key-array capacity. + * + * @param initialSize the initial capacity of the keys/values arrays + */ protected FastHashMap(int initialSize) { super(initialSize); this.values = newValuesArray(keys.length); } + /** + * Creates a map with the default initial capacity. + */ protected FastHashMap() { super(); this.values = newValuesArray(keys.length); } /** - * Copy constructor. - * The new map will contain all the same keys and values of the map to copy. + * Copy constructor. The new map contains the same keys and the same + * value references as {@code mapToCopy}. * - * @param mapToCopy + * @param mapToCopy the source map */ protected FastHashMap(final FastHashMap mapToCopy) { super(mapToCopy); @@ -66,10 +83,13 @@ protected FastHashMap(final FastHashMap mapToCopy) { } /** - * Copy constructor with value processor. + * Copy constructor that transforms each value via {@code valueProcessor}. + * Useful when the values are mutable and need to be deep-copied to keep + * the new map independent from the source. * - * @param mapToCopy - * @param valueProcessor + * @param mapToCopy the source map + * @param valueProcessor function applied to every non-null value to obtain + * the value to put in the new map */ protected FastHashMap(final FastHashMap mapToCopy, final UnaryOperator valueProcessor) { super(mapToCopy); @@ -82,6 +102,12 @@ protected FastHashMap(final FastHashMap mapToCopy, final UnaryOperator } } + /** + * Gets a new array of values with the given size. + * + * @param size the size of the array + * @return the new array + */ protected abstract V[] newValuesArray(int size); @Override @@ -106,12 +132,10 @@ public void clear() { @Override public boolean tryPut(K key, V value) { + growPositionsArrayIfNeeded(); final var hashCode = key.hashCode(); - var pIndex = findPosition(key, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { - if (tryGrowPositionsArrayIfNeeded()) { - pIndex = findPosition(key, hashCode); - } final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; values[eIndex] = value; @@ -126,12 +150,10 @@ public boolean tryPut(K key, V value) { @Override public void put(K key, V value) { + growPositionsArrayIfNeeded(); final var hashCode = key.hashCode(); - var pIndex = findPosition(key, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { - if (tryGrowPositionsArrayIfNeeded()) { - pIndex = findPosition(key, hashCode); - } final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; values[eIndex] = value; @@ -142,8 +164,27 @@ public void put(K key, V value) { } } + @Override + public int putAndGetIndex(K key, V value) { + growPositionsArrayIfNeeded(); + final int hashCode = key.hashCode(); + final var pIndex = findPosition(key, hashCode); + final int eIndex; + if (pIndex < 0) { + eIndex = getFreeKeyIndex(); + keys[eIndex] = key; + hashCodesOrDeletedIndices[eIndex] = hashCode; + positions[~pIndex] = ~eIndex; + } else { + eIndex = ~positions[pIndex]; + } + values[eIndex] = value; + return eIndex; + } + /** * Returns the value at the given index. + * Array bounds are not checked. The caller must ensure the index is valid and corresponds to a non-null key. * * @param i index * @return value @@ -178,12 +219,12 @@ public V computeIfAbsent(K key, Supplier absentValueSupplier) { var pIndex = findPosition(key, hashCode); if (pIndex < 0) { if (tryGrowPositionsArrayIfNeeded()) { - pIndex = findPosition(key, hashCode); + pIndex = ~findEmptySlotWithoutEqualityCheck(hashCode); } + final var value = absentValueSupplier.get(); final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; - final var value = absentValueSupplier.get(); values[eIndex] = value; positions[~pIndex] = ~eIndex; return value; @@ -194,18 +235,20 @@ public V computeIfAbsent(K key, Supplier absentValueSupplier) { @Override public void compute(K key, UnaryOperator valueProcessor) { - final int hashCode = key.hashCode(); + final var hashCode = key.hashCode(); var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var value = valueProcessor.apply(null); if (value == null) return; + if(tryGrowPositionsArrayIfNeeded()) { + pIndex = ~findEmptySlotWithoutEqualityCheck(hashCode); + } final var eIndex = getFreeKeyIndex(); keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; values[eIndex] = value; positions[~pIndex] = ~eIndex; - tryGrowPositionsArrayIfNeeded(); } else { var eIndex = ~positions[pIndex]; final var value = valueProcessor.apply(values[eIndex]); @@ -217,24 +260,13 @@ public void compute(K key, UnaryOperator valueProcessor) { } } - @Override public ExtendedIterator valueIterator() { - final var initialSize = size(); - final Runnable checkForConcurrentModification = () -> - { - if (size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(values, keysPos, checkForConcurrentModification); + return new SparseArrayIterator<>(values, keysPos, this); } @Override public Spliterator valueSpliterator() { - final var initialSize = this.size(); - final Runnable checkForConcurrentModification = () -> - { - if (this.size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(values, keysPos, checkForConcurrentModification); + return new SparseArraySpliterator<>(values, keysPos, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java index 134a0092e22..5adf3232c4e 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/FastHashSet.java @@ -21,39 +21,42 @@ package org.apache.jena.mem.collection; -import org.apache.jena.mem.iterator.SparseArrayIndexedIterator; -import org.apache.jena.mem.spliterator.SparseArrayIndexedSpliterator; -import org.apache.jena.util.iterator.ExtendedIterator; - -import java.util.ConcurrentModificationException; -import java.util.Spliterator; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - /** - * Set which grows, if needed but never shrinks. - * This set does not guarantee any order. Although due to the way it is implemented the elements have a certain order. - * This set does not allow null values. - * This set is not thread safe. - * It´s purpose is to support fast add, remove, contains and stream / iterate operations. - * Only remove operations are not as fast as in {@link java.util.HashSet} - * Iterating over this set not get much faster again after removing elements because the set is not compacted. + * Hash set specialization built on top of {@link FastHashBase}. + * Grows on demand but never shrinks, does not guarantee iteration order, + * does not allow {@code null} elements, and is not thread-safe. + * + * Optimized for fast {@code add} / {@code containsKey} / {@code stream} / + * iterate operations. Removal is somewhat slower than in + * {@link java.util.HashSet} because of the back-shifting performed on the + * probe table. Iteration speed does not recover after many removals because + * the dense {@code keys} array is not compacted. + * + * @param the element type */ -public abstract class FastHashSet extends FastHashBase implements JenaSetHashOptimized { +public abstract class FastHashSet extends FastHashBase implements JenaSetIndexed { - protected FastHashSet(int initialSize) { + /** + * Creates a set with the given initial key-array capacity. + * + * @param initialSize the initial capacity of the keys array + */ + public FastHashSet(final int initialSize) { super(initialSize); } - protected FastHashSet() { + /** + * Creates a set with the default initial capacity. + */ + public FastHashSet() { super(); } /** - * Copy constructor. - * The new set will contain all the same keys of the set to copy. + * Copy constructor. The new set contains the same elements as + * {@code setToCopy}. * - * @param setToCopy + * @param setToCopy the source set */ protected FastHashSet(final FastHashSet setToCopy) { super(setToCopy); @@ -65,12 +68,12 @@ public boolean tryAdd(K key) { } @Override - public boolean tryAdd(K value, int hashCode) { + public boolean tryAdd(K key, int hashCode) { growPositionsArrayIfNeeded(); - var pIndex = findPosition(value, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var eIndex = getFreeKeyIndex(); - keys[eIndex] = value; + keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; positions[~pIndex] = ~eIndex; return true; @@ -79,28 +82,23 @@ public boolean tryAdd(K value, int hashCode) { } /** - * Add and get the index of the added element. + * Add an element and return the index it was stored at. + * If the element is already present, returns the bitwise complement + * ({@code ~existingIndex}) of the existing index, so callers can + * distinguish "newly inserted" from "already present" while still + * recovering the index in both cases. * - * @param value the value to add - * @return the index of the added element or the inverse (~) index of the existing element + * @param key the element to add + * @return the new index, or {@code ~existingIndex} if already present */ - public int addAndGetIndex(K value) { - return addAndGetIndex(value, value.hashCode()); - } - - /** - * Add and get the index of the added element. - * - * @param value the value to add - * @param hashCode the hash code of the value. This is a performance optimization. - * @return the index of the added element or the inverse (~) index of the existing element - */ - public int addAndGetIndex(final K value, final int hashCode) { + @Override + public int addAndGetIndex(K key) { growPositionsArrayIfNeeded(); - final var pIndex = findPosition(value, hashCode); + final var hashCode = key.hashCode(); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var eIndex = getFreeKeyIndex(); - keys[eIndex] = value; + keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; positions[~pIndex] = ~eIndex; return eIndex; @@ -132,62 +130,4 @@ public void addUnchecked(K value, int hashCode) { public K getKeyAt(int i) { return keys[i]; } - - /** - * Entry pairing a key with its index in the set. - * @param index index of the key in the set - * @param key the key - * @param the type of the key - */ - public record IndexedKey(int index, K key) {} - - /** - * Get an iterator over pairs of keys and their indices in the set. - * The iterator is not thread safe. - * - * @return an iterator over pairs of keys and their indices in the set - */ - public final ExtendedIterator> indexedKeyIterator() { - final var initialSize = size(); - final Runnable checkForConcurrentModification = () -> - { - if (size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIndexedIterator<>(keys, keysPos, checkForConcurrentModification); - } - - /** - * Get a spliterator over pairs of keys and their indices in the set. - * The spliterator is not thread safe. - * - * @return a spliterator over pairs of keys and their indices in the set - */ - public final Spliterator> indexedKeySpliterator() { - final var initialSize = this.size(); - final Runnable checkForConcurrentModification = () -> - { - if (this.size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIndexedSpliterator<>(keys, keysPos, checkForConcurrentModification); - } - - /** - * Get a stream over pairs of keys and their indices in the set. - * The stream is not thread safe. - * - * @return a stream over pairs of keys and their indices in the set - */ - public final Stream> indexedKeyStream() { - return StreamSupport.stream(indexedKeySpliterator(), false); - } - - /** - * Get a parallel stream over pairs of keys and their indices in the set. - * The stream is not thread safe. - * - * @return a parallel stream over pairs of keys and their indices in the set - */ - public final Stream> indexedKeyStreamParallel() { - return StreamSupport.stream(indexedKeySpliterator(), true); - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java index 5664a900170..b277789c717 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java @@ -25,7 +25,6 @@ import org.apache.jena.shared.JenaException; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Predicate; @@ -36,7 +35,7 @@ * * @param the element type */ -public abstract class HashCommonBase { +public abstract class HashCommonBase implements JenaMapSetCommon { /** * Jeremy suggests, from his experiments, that load factors more than * 0.6 leave the table too dense, and little advantage is gained below 0.4. @@ -78,7 +77,7 @@ protected HashCommonBase(int initialCapacity) { * Copy constructor. * The new table will contain all the same keys of the table to copy. * - * @param baseToCopy + * @param baseToCopy the table to copy */ protected HashCommonBase(final HashCommonBase baseToCopy) { this.keys = newKeysArray(baseToCopy.keys.length); @@ -209,18 +208,10 @@ public boolean anyMatch(final Predicate predicate) { } public ExtendedIterator keyIterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(keys, checkForConcurrentModification); + return new SparseArrayIterator<>(keys, this); } public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(keys, checkForConcurrentModification); + return new SparseArraySpliterator<>(keys, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java index 62e7bd56733..dcdd5557654 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java @@ -24,7 +24,6 @@ import org.apache.jena.mem.spliterator.SparseArraySpliterator; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Supplier; import java.util.function.UnaryOperator; @@ -207,19 +206,11 @@ protected void removeFrom(int here) { @Override public ExtendedIterator valueIterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(values, checkForConcurrentModification); + return new SparseArrayIterator<>(values, this); } @Override public Spliterator valueSpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(values, checkForConcurrentModification); + return new SparseArraySpliterator<>(values, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java index 3e13613b08f..6d2423e0097 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java @@ -30,6 +30,7 @@ /** * A map from keys of type {@code K} to values of type {@code V}. + * Not thread-safe and does not allow {@code null} keys. * * @param the type of the keys in the map * @param the type of the values in the map diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java new file mode 100644 index 00000000000..67c366d00eb --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.mem.collection; + +/** + * Extension of {@link JenaMap} that exposes index-based access and lets callers + * supply a precomputed hash code for the key. Indices are stable handles to + * entries (returned by {@link #putAndGetIndex(Object, Object)}) and remain + * valid until the corresponding entry is removed. + * + * The hash-code overloads are a performance shortcut for callers that already + * have the hash at hand (typically because the same key is stored in several + * collections). The supplied hash code MUST equal {@code key.hashCode()}, or + * the map will misbehave. + * + * @param the type of the keys in the map + * @param the type of the values in the map + */ +public interface JenaMapIndexed extends JenaMap { + + /** + * Returns the index of the entry with the given key, or a negative value + * if no such entry exists. + * + * @param key the key to look up + * @return the index of the entry, or a negative value if absent + */ + int indexOf(K key); + + /** + * Returns the key stored at the given index. + * + * @param index the index of the entry + * @return the key at that index + */ + K getKeyAt(int index); + + /** + * Returns the value stored at the given index. + * + * @param index the index of the entry + * @return the value at that index + */ + V getValueAt(int index); + + /** + * Put a key-value pair and return the index of the affected entry. + * If the key is already present, its value is updated and the existing + * index is returned. + * + * @param key the key to put + * @param value the value to put + * @return the index of the entry holding {@code key} + */ + int putAndGetIndex(K key, V value); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java index 2533714ce6b..7f96baa19f9 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java @@ -28,22 +28,23 @@ import java.util.stream.StreamSupport; /** - * Common interface for {@link JenaMap} and {@link JenaSet}. * + * Operations shared between the map ({@link JenaMap}) and the set + * ({@link JenaSet}) collections used in the {@code mem} triple store + * implementations. + * + * These collections trade some flexibility for speed: they expose only the + * operations needed by triple-store internals (no full {@link java.util.Map} + * or {@link java.util.Set} contract). They are not thread-safe. * - * @param the type of the keys/elements in the collection + * @param the type of the keys (or elements, for sets) in the collection */ -public interface JenaMapSetCommon { +public interface JenaMapSetCommon extends Sized { /** * Clear the collection. */ void clear(); - /** - * @return the number of elements in the collection - */ - int size(); - /** * @return true if the collection is empty */ @@ -75,7 +76,10 @@ public interface JenaMapSetCommon { /** * Removes a key from the collection. - * Attention: Implementations may assume that the key is present. + * + * Attention: implementations may assume the key is present and may produce + * undefined behavior (including silently corrupting internal state) if it + * is not. Use {@link #tryRemove(Object)} when in doubt. * * @param key the key to remove */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java index d3b8a557be9..03848073f56 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java @@ -21,9 +21,10 @@ package org.apache.jena.mem.collection; /** - * Set interface specialized for the use cases in triple store implementations. + * Set interface specialized for the use cases in triple-store implementations. + * Not thread-safe; does not allow {@code null} elements. * - * @param + * @param the element type of the set */ public interface JenaSet extends JenaMapSetCommon { @@ -31,13 +32,16 @@ public interface JenaSet extends JenaMapSetCommon { * Add the key to the set if it is not already present. * * @param key the key to add - * @return true if the key was added, false if it was already present + * @return {@code true} if the key was added, {@code false} if it was already present */ boolean tryAdd(E key); /** - * Add the key to the set without checking if it is already present. - * Attention: This method must only be used if it is guaranteed that the key is not already present. + * Add the key to the set without checking whether it is already present. + * + * Attention: this method must only be used if the caller has ensured that + * the key is not already in the set; otherwise the set's invariants will + * break (duplicates may be inserted). * * @param key the key to add */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java index 8cc8aad8daf..0e1d032b356 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java @@ -22,17 +22,50 @@ /** - * Extension of {@link JenaSet} that allows to add and remove elements - * with a given hash code. - * This is useful if the hash code is already known. - * Attention: The hash code must be consistent with E::hashCode(). + * Extension of {@link JenaSet} that lets callers supply a precomputed hash + * code. + * + * Attention: any caller-supplied hash code MUST equal {@code E.hashCode()}; + * if it does not, the set will misbehave. + * + * @param the element type of the set */ public interface JenaSetHashOptimized extends JenaSet { + + /** + * Add an element with the given precomputed hash code if it is not + * already present. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + * @return {@code true} if added, {@code false} if already present + */ boolean tryAdd(E key, int hashCode); + /** + * Add an element with the given precomputed hash code without checking + * whether it is already present. The caller MUST ensure the key is absent. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + */ void addUnchecked(E key, int hashCode); + /** + * Try to remove an element with the given precomputed hash code. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + * @return {@code true} if removed, {@code false} if it was not present + */ boolean tryRemove(E key, int hashCode); + /** + * Remove an element assumed to be present, with the given precomputed + * hash code. Behavior is undefined if the element is not in the set. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + */ void removeUnchecked(E key, int hashCode); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java new file mode 100644 index 00000000000..c7c3d2e1ddb --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.mem.collection; + + +/** + * Extension of {@link JenaSetHashOptimized} that exposes index-based access to elements. + * Indices are stable handles to entries (returned by {@link #addAndGetIndex(Object)}) and remain + * valid until the corresponding entry is removed. + * + * @param the element type of the set + */ +public interface JenaSetIndexed extends JenaSetHashOptimized { + + /** + * Add an element and return the index it was stored at. If the element + * is already present, returns a negative value (typically the bitwise + * complement of the existing index). + * + * @param key the element to add + * @return the index of the inserted element, or a negative value if the + * element was already present + */ + int addAndGetIndex(final E key); + + /** + * Returns the element stored at the given index. + * + * @param index the index to read + * @return the element at that index + */ + E getKeyAt(int index); + + /** + * Returns the index of the given element, or a negative value if it is + * not in the set. + * + * @param key the element to look up + * @return the index of {@code key}, or a negative value if absent + */ + int indexOf(E key); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java new file mode 100644 index 00000000000..237740ce8e3 --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.mem.collection; + +/** + * Base interface for sized collections. + * It is typically used to detect concurrent modifications in iterators and spliterators + * by snapshotting the size at construction time and rechecking it at each advance/forEach boundary. + */ +public interface Sized { + + /** + * @return the number of elements in the collection + */ + int size(); +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java index b0ac6e994bb..8cfc8948a25 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java @@ -30,16 +30,27 @@ import java.util.function.Consumer; /** - * Iterator that iterates over the entries of sets which are contained in the given iterator of sets. + * Flat-map style iterator that yields every element of every {@link JenaSet} + * produced by the given parent iterator. Empty inner sets are silently + * skipped. Equivalent in spirit to a one-level {@code flatMap} but tailored + * to the {@link JenaSet} API and to {@link NiceIterator}. * - * @param the type of the elements + * @param the element type of the inner sets */ public class IteratorOfJenaSets extends NiceIterator { - final Iterator extends JenaSet> parentIterator; + /** Source iterator producing the sets to flatten. */ + private final Iterator extends JenaSet> parentIterator; - ExtendedIterator currentIterator; + /** Iterator over the keys of the set currently being consumed. */ + private ExtendedIterator currentIterator; + /** + * Create a flat iterator over the elements of every set produced by + * {@code parentIterator}. + * + * @param parentIterator the source iterator of sets + */ public IteratorOfJenaSets(Iterator extends JenaSet> parentIterator) { this.parentIterator = parentIterator; this.currentIterator = parentIterator.hasNext() diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java deleted file mode 100644 index 37f103eae25..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.iterator; - -import org.apache.jena.mem.collection.FastHashSet; -import org.apache.jena.util.iterator.NiceIterator; - -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.function.Consumer; - -/** - * An iterator over a sparse array, that skips null entries. - * This iterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * The iterator works in ascending order, starting from index 0 up to the specified exclusive index. - * - * This iterator will check for concurrent modifications by invoking a {@link Runnable} - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedIterator extends NiceIterator> implements Iterator> { - - private final E[] entries; - private final Runnable checkForConcurrentModification; - private int pos = 0; - private final int toIndexExclusive; - private boolean hasNext = false; - - public SparseArrayIndexedIterator(final E[] entries, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = entries.length; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - public SparseArrayIndexedIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Returns {@code true} if the iteration has more elements. - * (In other words, returns {@code true} if {@link #next} would - * return an element rather than throwing an exception.) - * - * @return {@code true} if the iteration has more elements - */ - @Override - public boolean hasNext() { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - hasNext = true; - return true; - } - pos++; - } - hasNext = false; - return false; - } - - /** - * Returns the next element in the iteration. - * - * @return the next element in the iteration - * @throws NoSuchElementException if the iteration has no more elements - */ - @Override - public FastHashSet.IndexedKey next() { - this.checkForConcurrentModification.run(); - if (hasNext || hasNext()) { - hasNext = false; - return new FastHashSet.IndexedKey<>(pos, entries[pos++]); - } - throw new NoSuchElementException(); - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - action.accept(new FastHashSet.IndexedKey<>(pos, entries[pos])); - } - pos++; - } - this.checkForConcurrentModification.run(); - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java index 936476a80ff..e0b79cd1ff6 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java @@ -21,34 +21,55 @@ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.Sized; import org.apache.jena.util.iterator.NiceIterator; -import java.util.Iterator; +import java.util.ConcurrentModificationException; import java.util.NoSuchElementException; import java.util.function.Consumer; /** - * An iterator over a sparse array, that skips null entries. + * Iterator over a sparse array, walking from high index to low and skipping + * {@code null} entries. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it on each call to + * {@link #next()} / {@link #forEachRemaining(Consumer)}; throws + * {@link ConcurrentModificationException} if the size has changed. * * @param the type of the array elements */ -public class SparseArrayIterator extends NiceIterator implements Iterator { +public class SparseArrayIterator extends NiceIterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; private boolean hasNext = false; - public SparseArrayIterator(final E[] entries, final Runnable checkForConcurrentModification) { + /** + * Iterate over the whole array. + * + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, final Sized set) { this.entries = entries; this.pos = entries.length - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } - public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { + /** + * Iterate over {@code entries[0 .. toIndexExclusive)} (in reverse order). + * + * @param entries the backing array (not copied) + * @param toIndexExclusive exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Sized set) { this.entries = entries; this.pos = toIndexExclusive - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** @@ -62,13 +83,11 @@ public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnab public boolean hasNext() { while (-1 < pos) { if (null != entries[pos]) { - hasNext = true; - return true; + return hasNext = true; } pos--; } - hasNext = false; - return false; + return hasNext = false; } /** @@ -79,7 +98,7 @@ public boolean hasNext() { */ @Override public E next() { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (hasNext || hasNext()) { hasNext = false; return entries[pos--]; @@ -95,6 +114,6 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java index 94008b155f1..d8536f56311 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java @@ -22,8 +22,17 @@ package org.apache.jena.mem.pattern; /** - * A pattern for matching triples. - * The pattern is defined by the wildcard positions for the subject, predicate and object. + * Categorizes a triple-match pattern by which of the subject, predicate and + * object slots are concrete and which are wildcards (i.e. {@code Node.ANY} + * or {@code null}). + * + * The eight enum values cover every possible combination. Triple-store + * implementations dispatch on this enum to pick the most efficient lookup + * path for each kind of pattern (e.g. a fully concrete {@link #SUB_PRE_OBJ} + * is answered directly from the triple set, while a partially open pattern + * such as {@link #ANY_PRE_OBJ} is answered through an index intersection). + * + * @see PatternClassifier */ public enum MatchPattern { /** diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java index 32a6ba182a1..e4cf5644eca 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java @@ -25,14 +25,15 @@ import org.apache.jena.graph.Triple; /** - * Classify a triple match into one of the 8 match patterns. + * Utility class that classifies a triple match into one of the eight + * {@link MatchPattern} values. * - * The classification is based on the concrete-ness of the subject, predicate and object. - * A concrete node is one that is not a variable. + * The classification is based on which of the subject, predicate and object + * are concrete (anything that is not a variable / wildcard / + * {@code null}) and which are wildcards. The result is used by triple-store + * implementations to dispatch to the most efficient lookup path. * - * The classification is used to select the most efficient implementation of a triple store. - * - * This is a utility class; there is no need to instantiate it. + * All operations are stateless; this class is not meant to be instantiated. * * @see MatchPattern */ @@ -41,8 +42,16 @@ public class PatternClassifier { private PatternClassifier() { } + /** + * Classify a triple match. + * + * @param tripleMatch the match triple, possibly containing wildcard nodes + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Triple tripleMatch) { - if (tripleMatch.isConcrete()) { + if (tripleMatch.getSubject().isConcrete() + && tripleMatch.getPredicate().isConcrete() + && tripleMatch.getObject().isConcrete()) { return MatchPattern.SUB_PRE_OBJ; } else { if (tripleMatch.getSubject().isConcrete()) { @@ -73,6 +82,15 @@ public static MatchPattern classify(Triple tripleMatch) { } } + /** + * Classify a triple match given as three nodes. + * Any {@code null} or non-concrete node is treated as a wildcard. + * + * @param sm subject node, or {@code null}/wildcard + * @param pm predicate node, or {@code null}/wildcard + * @param om object node, or {@code null}/wildcard + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Node sm, Node pm, Node om) { if (null != sm && sm.isConcrete()) { if (null != pm && pm.isConcrete()) { @@ -103,6 +121,5 @@ public static MatchPattern classify(Node sm, Node pm, Node om) { } } } - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java index 43bbfeeaea8..a5033c22cde 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java @@ -21,52 +21,57 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a contiguous array slice {@code [0, toIndex)}, + * iterating from high index to low. Supports splitting into + * {@link ArraySubSpliterator} children for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySpliterator implements Spliterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public ArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (-1 < --pos) { action.accept(entries[pos]); return true; @@ -79,7 +84,7 @@ public void forEachRemaining(Consumer super E> action) { while (-1 < --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -89,7 +94,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -101,4 +106,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java index 74994708b53..638f2bb0c9e 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java @@ -21,55 +21,61 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a contiguous array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low. Produced by splitting an + * {@link ArraySpliterator} (or another {@link ArraySubSpliterator}); supports + * further recursive splits for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public ArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (fromIndex <= --pos) { action.accept(entries[pos]); return true; @@ -82,7 +88,7 @@ public void forEachRemaining(Consumer super E> action) { while (fromIndex <= --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -93,7 +99,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -105,4 +111,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java deleted file mode 100644 index 704c9642706..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.spliterator; - -import java.util.Spliterator; -import java.util.function.Consumer; - -import org.apache.jena.mem.collection.FastHashSet; - -/** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * This spliterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * This spliterator works in ascending order, starting from the given start up to the specified exclusive index. - * - * This spliterator supports splitting into sub-spliterators. - * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedSpliterator implements Spliterator> { - - private final E[] entries; - private int currentPositionMinusOne; - private final int toIndexExclusive; - private final Runnable checkForConcurrentModification; - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param fromIndexInclusive the index of the first element, inclusive - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int fromIndexInclusive, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.currentPositionMinusOne = fromIndexInclusive-1; // Start at fromIndexInclusive - 1, so that the first call to tryAdvance will increment pos to fromIndexInclusive - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this(entries, 0, toIndexExclusive, checkForConcurrentModification); - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); - } - - - @Override - public boolean tryAdvance(Consumer super FastHashSet.IndexedKey> action) { - this.checkForConcurrentModification.run(); - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - return true; - } - } - return false; - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - } - } - this.checkForConcurrentModification.run(); - } - - @Override - public Spliterator> trySplit() { - final var nextPos = currentPositionMinusOne + 1; - final var remaining = toIndexExclusive - nextPos; - if ( remaining < 2) { - return null; - } - final var mid = nextPos + ( remaining >>> 1); - final var fromIndexInclusive = nextPos; - this.currentPositionMinusOne = mid-1; - return new SparseArrayIndexedSpliterator<>(entries, fromIndexInclusive, mid, checkForConcurrentModification); - } - - @Override - public long estimateSize() { return (long) toIndexExclusive - currentPositionMinusOne; } - - @Override - public int characteristics() { - return DISTINCT | NONNULL | IMMUTABLE; - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java index 6752cc9a1c1..add45739dc2 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java @@ -21,17 +21,24 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a sparse array slice {@code [0, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced for backing arrays such as those of + * {@link org.apache.jena.mem.collection.FastHashBase}, where removed slots + * are represented by {@code null}. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Supports splitting into {@link SparseArraySubSpliterator} children for + * parallel traversal. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it at each + * advance/forEach boundary; throws {@link ConcurrentModificationException} + * if the size has changed. * * @param the type of the array elements */ @@ -39,35 +46,37 @@ public class SparseArraySpliterator implements Spliterator { private final E[] entries; private int pos; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}, skipping nulls. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public SparseArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (-1 < --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -86,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -96,7 +105,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java index 3eb0784326f..d79242ac78c 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java @@ -21,55 +21,62 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a sparse array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced by splitting a {@link SparseArraySpliterator} (or another + * {@link SparseArraySubSpliterator}); supports further recursive splits for + * parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary; + * throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the type of the array elements */ public class SparseArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}, skipping nulls. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public SparseArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (fromIndex <= --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -88,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -99,7 +106,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java index 07ccc9634a9..f0fba805175 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java @@ -32,26 +32,41 @@ import java.util.function.Predicate; /** - * An ArrayBunch implements TripleBunch with a linear search of a short-ish - * array of Triples. The array grows by factor 2. + * Linear-scan implementation of {@link FastTripleBunch} backed by a packed + * {@link Triple} array. Used as long as a bunch stays small; once it grows + * past the configured threshold (see {@link FastTripleStore}) it is replaced + * with a {@link FastHashedTripleBunch}. + * + * The array grows by a factor of two when full. Equality of triples within a + * bunch is delegated to {@link #areEqual(Triple, Triple)}, which subclasses + * specialize to compare only the two nodes that are not already + * implied by the enclosing map's key. This avoids redundant equality checks + * on the shared subject/predicate/object. + * + * Not thread-safe. */ public abstract class FastArrayBunch implements FastTripleBunch { private static final int INITIAL_SIZE = 4; + /** Number of valid entries in {@link #elements}. */ protected int size = 0; + /** Packed array of triples; entries from {@code 0} to {@code size-1} are live. */ protected Triple[] elements; + /** + * Creates an empty bunch with the default initial capacity. + */ protected FastArrayBunch() { elements = new Triple[INITIAL_SIZE]; } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. - * But it will reserve only the space needed to contain them. Growing is still possible. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}; its backing array is sized to fit exactly, + * but can grow further if needed. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -59,7 +74,17 @@ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.size = bunchToCopy.size; } - public abstract boolean areEqual(final Triple a, final Triple b); + /** + * Compare two triples for equality within this bunch. + * + * Subclasses specialize this to skip the already-shared component + * (subject, predicate or object) and compare only the remaining two. + * + * @param a first triple + * @param b second triple + * @return {@code true} if the triples are considered equal in this bunch + */ + protected abstract boolean areEqual(final Triple a, final Triple b); @Override public boolean containsKey(Triple t) { @@ -127,6 +152,7 @@ public boolean tryRemove(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return true; } } @@ -138,6 +164,7 @@ public void removeUnchecked(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return; } } @@ -174,11 +201,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java index b89d3312048..a49d6b54009 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java @@ -25,21 +25,28 @@ import org.apache.jena.mem.collection.FastHashMap; /** - * Map from nodes to triple bunches. + * {@link FastHashMap} specialized to map a {@link Node} to its associated + * {@link FastTripleBunch}. Used by {@link FastTripleStore} to maintain the + * three subject/predicate/object indices. */ public class FastHashedBunchMap extends FastHashMap implements Copyable { + /** + * Creates an empty bunch map with the default initial capacity. + */ public FastHashedBunchMap() { super(); } /** - * Copy constructor. - * The new map will contain all the same nodes as keys of the map to copy, but copies of the bunches as values . + * Copy constructor. The new map has the same node keys as + * {@code mapToCopy}; each value is replaced by a deep copy of the + * corresponding bunch (via {@link FastTripleBunch#copy()}) so that + * mutations of either map cannot affect the other. * - * @param mapToCopy + * @param mapToCopy the source map */ private FastHashedBunchMap(final FastHashedBunchMap mapToCopy) { super(mapToCopy, FastTripleBunch::copy); diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java index 459e78c8181..65c9ab70fbf 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java @@ -25,13 +25,21 @@ import org.apache.jena.mem.collection.JenaSet; /** - * A set of triples - backed by {@link FastHashSet}. + * Hashed implementation of {@link FastTripleBunch} built on top of + * {@link FastHashSet}. Used by {@link FastTripleStore} once a bunch grows + * past the size threshold at which a linear-scan {@link FastArrayBunch} + * stops being faster. */ public class FastHashedTripleBunch extends FastHashSet implements FastTripleBunch { + /** - * Create a new triple bunch from the given set of triples. + * Create a new hashed bunch pre-populated from the given set of triples. + * The initial capacity is chosen at 1.5x the source size, so the new bunch + * fits the existing triples and has some headroom for growth before it + * needs to rehash. * - * @param set the set of triples + * @param set the source set of triples (typically the array bunch being + * promoted) */ public FastHashedTripleBunch(final JenaSet set) { super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth @@ -39,15 +47,18 @@ public FastHashedTripleBunch(final JenaSet set) { } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ private FastHashedTripleBunch(final FastHashedTripleBunch bunchToCopy) { super(bunchToCopy); } + /** + * Creates an empty hashed bunch with the default initial capacity. + */ public FastHashedTripleBunch() { super(); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java index 68f79e72f8a..fe050283188 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java @@ -29,27 +29,39 @@ import java.util.function.Predicate; /** - * A bunch of triples - a stripped-down set with specialized methods. A - * bunch is expected to store triples that share some useful property - * (such as having the same subject or predicate). + * Set-like container for a "bunch" of triples that share some useful + * property - typically they all have the same subject, predicate or object, + * because the bunch is the value of a node-keyed map in a + * {@link FastTripleStore}. + * + * The interface is a stripped-down set with a few extras tuned for the + * triple-store hot path; concrete implementations are + * {@link FastArrayBunch} (linear scan, used while the bunch is small) and + * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a + * threshold). */ public interface FastTripleBunch extends JenaSetHashOptimized, Copyable { /** - * Answer true iff this bunch is implemented as an array. - * This field is used to optimize some operations by avoiding the need for instanceOf tests. + * Answer {@code true} iff this bunch is backed by a flat array (i.e. is + * a {@link FastArrayBunch}). Exposed as an explicit method so callers can + * avoid {@code instanceof} checks on this hot path. * - * @return true iff this bunch is implemented as an arrays + * @return {@code true} if this bunch is array-backed */ boolean isArray(); /** - * This method is used to optimize _PO match operations. - * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches. - * This method is faster if there are many matches and the set is ordered in an unfavorable way. - * _PO matches usually fall into this category. + * Predicate test that scans elements in hash-table order rather than + * dense insertion order. Tuned for {@code _PO} (any-predicate-object) + * matches. + * + * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
+ * Optimized for fast {@code add} / {@code containsKey} / {@code stream} / + * iterate operations. Removal is somewhat slower than in + * {@link java.util.HashSet} because of the back-shifting performed on the + * probe table. Iteration speed does not recover after many removals because + * the dense {@code keys} array is not compacted. + * + * @param the element type */ -public abstract class FastHashSet extends FastHashBase implements JenaSetHashOptimized { +public abstract class FastHashSet extends FastHashBase implements JenaSetIndexed { - protected FastHashSet(int initialSize) { + /** + * Creates a set with the given initial key-array capacity. + * + * @param initialSize the initial capacity of the keys array + */ + public FastHashSet(final int initialSize) { super(initialSize); } - protected FastHashSet() { + /** + * Creates a set with the default initial capacity. + */ + public FastHashSet() { super(); } /** - * Copy constructor. - * The new set will contain all the same keys of the set to copy. + * Copy constructor. The new set contains the same elements as + * {@code setToCopy}. * - * @param setToCopy + * @param setToCopy the source set */ protected FastHashSet(final FastHashSet setToCopy) { super(setToCopy); @@ -65,12 +68,12 @@ public boolean tryAdd(K key) { } @Override - public boolean tryAdd(K value, int hashCode) { + public boolean tryAdd(K key, int hashCode) { growPositionsArrayIfNeeded(); - var pIndex = findPosition(value, hashCode); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var eIndex = getFreeKeyIndex(); - keys[eIndex] = value; + keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; positions[~pIndex] = ~eIndex; return true; @@ -79,28 +82,23 @@ public boolean tryAdd(K value, int hashCode) { } /** - * Add and get the index of the added element. + * Add an element and return the index it was stored at. + * If the element is already present, returns the bitwise complement + * ({@code ~existingIndex}) of the existing index, so callers can + * distinguish "newly inserted" from "already present" while still + * recovering the index in both cases. * - * @param value the value to add - * @return the index of the added element or the inverse (~) index of the existing element + * @param key the element to add + * @return the new index, or {@code ~existingIndex} if already present */ - public int addAndGetIndex(K value) { - return addAndGetIndex(value, value.hashCode()); - } - - /** - * Add and get the index of the added element. - * - * @param value the value to add - * @param hashCode the hash code of the value. This is a performance optimization. - * @return the index of the added element or the inverse (~) index of the existing element - */ - public int addAndGetIndex(final K value, final int hashCode) { + @Override + public int addAndGetIndex(K key) { growPositionsArrayIfNeeded(); - final var pIndex = findPosition(value, hashCode); + final var hashCode = key.hashCode(); + final var pIndex = findPosition(key, hashCode); if (pIndex < 0) { final var eIndex = getFreeKeyIndex(); - keys[eIndex] = value; + keys[eIndex] = key; hashCodesOrDeletedIndices[eIndex] = hashCode; positions[~pIndex] = ~eIndex; return eIndex; @@ -132,62 +130,4 @@ public void addUnchecked(K value, int hashCode) { public K getKeyAt(int i) { return keys[i]; } - - /** - * Entry pairing a key with its index in the set. - * @param index index of the key in the set - * @param key the key - * @param the type of the key - */ - public record IndexedKey(int index, K key) {} - - /** - * Get an iterator over pairs of keys and their indices in the set. - * The iterator is not thread safe. - * - * @return an iterator over pairs of keys and their indices in the set - */ - public final ExtendedIterator> indexedKeyIterator() { - final var initialSize = size(); - final Runnable checkForConcurrentModification = () -> - { - if (size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIndexedIterator<>(keys, keysPos, checkForConcurrentModification); - } - - /** - * Get a spliterator over pairs of keys and their indices in the set. - * The spliterator is not thread safe. - * - * @return a spliterator over pairs of keys and their indices in the set - */ - public final Spliterator> indexedKeySpliterator() { - final var initialSize = this.size(); - final Runnable checkForConcurrentModification = () -> - { - if (this.size() != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIndexedSpliterator<>(keys, keysPos, checkForConcurrentModification); - } - - /** - * Get a stream over pairs of keys and their indices in the set. - * The stream is not thread safe. - * - * @return a stream over pairs of keys and their indices in the set - */ - public final Stream> indexedKeyStream() { - return StreamSupport.stream(indexedKeySpliterator(), false); - } - - /** - * Get a parallel stream over pairs of keys and their indices in the set. - * The stream is not thread safe. - * - * @return a parallel stream over pairs of keys and their indices in the set - */ - public final Stream> indexedKeyStreamParallel() { - return StreamSupport.stream(indexedKeySpliterator(), true); - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java index 5664a900170..b277789c717 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonBase.java @@ -25,7 +25,6 @@ import org.apache.jena.shared.JenaException; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Predicate; @@ -36,7 +35,7 @@ * * @param the element type */ -public abstract class HashCommonBase { +public abstract class HashCommonBase implements JenaMapSetCommon { /** * Jeremy suggests, from his experiments, that load factors more than * 0.6 leave the table too dense, and little advantage is gained below 0.4. @@ -78,7 +77,7 @@ protected HashCommonBase(int initialCapacity) { * Copy constructor. * The new table will contain all the same keys of the table to copy. * - * @param baseToCopy + * @param baseToCopy the table to copy */ protected HashCommonBase(final HashCommonBase baseToCopy) { this.keys = newKeysArray(baseToCopy.keys.length); @@ -209,18 +208,10 @@ public boolean anyMatch(final Predicate predicate) { } public ExtendedIterator keyIterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(keys, checkForConcurrentModification); + return new SparseArrayIterator<>(keys, this); } public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(keys, checkForConcurrentModification); + return new SparseArraySpliterator<>(keys, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java index 62e7bd56733..dcdd5557654 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/HashCommonMap.java @@ -24,7 +24,6 @@ import org.apache.jena.mem.spliterator.SparseArraySpliterator; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Supplier; import java.util.function.UnaryOperator; @@ -207,19 +206,11 @@ protected void removeFrom(int here) { @Override public ExtendedIterator valueIterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArrayIterator<>(values, checkForConcurrentModification); + return new SparseArrayIterator<>(values, this); } @Override public Spliterator valueSpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new SparseArraySpliterator<>(values, checkForConcurrentModification); + return new SparseArraySpliterator<>(values, this); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java index 3e13613b08f..6d2423e0097 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMap.java @@ -30,6 +30,7 @@ /** * A map from keys of type {@code K} to values of type {@code V}. + * Not thread-safe and does not allow {@code null} keys. * * @param the type of the keys in the map * @param the type of the values in the map diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java new file mode 100644 index 00000000000..67c366d00eb --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapIndexed.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.mem.collection; + +/** + * Extension of {@link JenaMap} that exposes index-based access and lets callers + * supply a precomputed hash code for the key. Indices are stable handles to + * entries (returned by {@link #putAndGetIndex(Object, Object)}) and remain + * valid until the corresponding entry is removed. + * + * The hash-code overloads are a performance shortcut for callers that already + * have the hash at hand (typically because the same key is stored in several + * collections). The supplied hash code MUST equal {@code key.hashCode()}, or + * the map will misbehave. + * + * @param the type of the keys in the map + * @param the type of the values in the map + */ +public interface JenaMapIndexed extends JenaMap { + + /** + * Returns the index of the entry with the given key, or a negative value + * if no such entry exists. + * + * @param key the key to look up + * @return the index of the entry, or a negative value if absent + */ + int indexOf(K key); + + /** + * Returns the key stored at the given index. + * + * @param index the index of the entry + * @return the key at that index + */ + K getKeyAt(int index); + + /** + * Returns the value stored at the given index. + * + * @param index the index of the entry + * @return the value at that index + */ + V getValueAt(int index); + + /** + * Put a key-value pair and return the index of the affected entry. + * If the key is already present, its value is updated and the existing + * index is returned. + * + * @param key the key to put + * @param value the value to put + * @return the index of the entry holding {@code key} + */ + int putAndGetIndex(K key, V value); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java index 2533714ce6b..7f96baa19f9 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java @@ -28,22 +28,23 @@ import java.util.stream.StreamSupport; /** - * Common interface for {@link JenaMap} and {@link JenaSet}. * + * Operations shared between the map ({@link JenaMap}) and the set + * ({@link JenaSet}) collections used in the {@code mem} triple store + * implementations. + * + * These collections trade some flexibility for speed: they expose only the + * operations needed by triple-store internals (no full {@link java.util.Map} + * or {@link java.util.Set} contract). They are not thread-safe. * - * @param the type of the keys/elements in the collection + * @param the type of the keys (or elements, for sets) in the collection */ -public interface JenaMapSetCommon { +public interface JenaMapSetCommon extends Sized { /** * Clear the collection. */ void clear(); - /** - * @return the number of elements in the collection - */ - int size(); - /** * @return true if the collection is empty */ @@ -75,7 +76,10 @@ public interface JenaMapSetCommon { /** * Removes a key from the collection. - * Attention: Implementations may assume that the key is present. + * + * Attention: implementations may assume the key is present and may produce + * undefined behavior (including silently corrupting internal state) if it + * is not. Use {@link #tryRemove(Object)} when in doubt. * * @param key the key to remove */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java index d3b8a557be9..03848073f56 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java @@ -21,9 +21,10 @@ package org.apache.jena.mem.collection; /** - * Set interface specialized for the use cases in triple store implementations. + * Set interface specialized for the use cases in triple-store implementations. + * Not thread-safe; does not allow {@code null} elements. * - * @param + * @param the element type of the set */ public interface JenaSet extends JenaMapSetCommon { @@ -31,13 +32,16 @@ public interface JenaSet extends JenaMapSetCommon { * Add the key to the set if it is not already present. * * @param key the key to add - * @return true if the key was added, false if it was already present + * @return {@code true} if the key was added, {@code false} if it was already present */ boolean tryAdd(E key); /** - * Add the key to the set without checking if it is already present. - * Attention: This method must only be used if it is guaranteed that the key is not already present. + * Add the key to the set without checking whether it is already present. + * + * Attention: this method must only be used if the caller has ensured that + * the key is not already in the set; otherwise the set's invariants will + * break (duplicates may be inserted). * * @param key the key to add */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java index 8cc8aad8daf..0e1d032b356 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java @@ -22,17 +22,50 @@ /** - * Extension of {@link JenaSet} that allows to add and remove elements - * with a given hash code. - * This is useful if the hash code is already known. - * Attention: The hash code must be consistent with E::hashCode(). + * Extension of {@link JenaSet} that lets callers supply a precomputed hash + * code. + * + * Attention: any caller-supplied hash code MUST equal {@code E.hashCode()}; + * if it does not, the set will misbehave. + * + * @param the element type of the set */ public interface JenaSetHashOptimized extends JenaSet { + + /** + * Add an element with the given precomputed hash code if it is not + * already present. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + * @return {@code true} if added, {@code false} if already present + */ boolean tryAdd(E key, int hashCode); + /** + * Add an element with the given precomputed hash code without checking + * whether it is already present. The caller MUST ensure the key is absent. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + */ void addUnchecked(E key, int hashCode); + /** + * Try to remove an element with the given precomputed hash code. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + * @return {@code true} if removed, {@code false} if it was not present + */ boolean tryRemove(E key, int hashCode); + /** + * Remove an element assumed to be present, with the given precomputed + * hash code. Behavior is undefined if the element is not in the set. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + */ void removeUnchecked(E key, int hashCode); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java new file mode 100644 index 00000000000..c7c3d2e1ddb --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.mem.collection; + + +/** + * Extension of {@link JenaSetHashOptimized} that exposes index-based access to elements. + * Indices are stable handles to entries (returned by {@link #addAndGetIndex(Object)}) and remain + * valid until the corresponding entry is removed. + * + * @param the element type of the set + */ +public interface JenaSetIndexed extends JenaSetHashOptimized { + + /** + * Add an element and return the index it was stored at. If the element + * is already present, returns a negative value (typically the bitwise + * complement of the existing index). + * + * @param key the element to add + * @return the index of the inserted element, or a negative value if the + * element was already present + */ + int addAndGetIndex(final E key); + + /** + * Returns the element stored at the given index. + * + * @param index the index to read + * @return the element at that index + */ + E getKeyAt(int index); + + /** + * Returns the index of the given element, or a negative value if it is + * not in the set. + * + * @param key the element to look up + * @return the index of {@code key}, or a negative value if absent + */ + int indexOf(E key); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java new file mode 100644 index 00000000000..237740ce8e3 --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.mem.collection; + +/** + * Base interface for sized collections. + * It is typically used to detect concurrent modifications in iterators and spliterators + * by snapshotting the size at construction time and rechecking it at each advance/forEach boundary. + */ +public interface Sized { + + /** + * @return the number of elements in the collection + */ + int size(); +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java index b0ac6e994bb..8cfc8948a25 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java @@ -30,16 +30,27 @@ import java.util.function.Consumer; /** - * Iterator that iterates over the entries of sets which are contained in the given iterator of sets. + * Flat-map style iterator that yields every element of every {@link JenaSet} + * produced by the given parent iterator. Empty inner sets are silently + * skipped. Equivalent in spirit to a one-level {@code flatMap} but tailored + * to the {@link JenaSet} API and to {@link NiceIterator}. * - * @param the type of the elements + * @param the element type of the inner sets */ public class IteratorOfJenaSets extends NiceIterator { - final Iterator extends JenaSet> parentIterator; + /** Source iterator producing the sets to flatten. */ + private final Iterator extends JenaSet> parentIterator; - ExtendedIterator currentIterator; + /** Iterator over the keys of the set currently being consumed. */ + private ExtendedIterator currentIterator; + /** + * Create a flat iterator over the elements of every set produced by + * {@code parentIterator}. + * + * @param parentIterator the source iterator of sets + */ public IteratorOfJenaSets(Iterator extends JenaSet> parentIterator) { this.parentIterator = parentIterator; this.currentIterator = parentIterator.hasNext() diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java deleted file mode 100644 index 37f103eae25..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.iterator; - -import org.apache.jena.mem.collection.FastHashSet; -import org.apache.jena.util.iterator.NiceIterator; - -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.function.Consumer; - -/** - * An iterator over a sparse array, that skips null entries. - * This iterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * The iterator works in ascending order, starting from index 0 up to the specified exclusive index. - * - * This iterator will check for concurrent modifications by invoking a {@link Runnable} - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedIterator extends NiceIterator> implements Iterator> { - - private final E[] entries; - private final Runnable checkForConcurrentModification; - private int pos = 0; - private final int toIndexExclusive; - private boolean hasNext = false; - - public SparseArrayIndexedIterator(final E[] entries, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = entries.length; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - public SparseArrayIndexedIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Returns {@code true} if the iteration has more elements. - * (In other words, returns {@code true} if {@link #next} would - * return an element rather than throwing an exception.) - * - * @return {@code true} if the iteration has more elements - */ - @Override - public boolean hasNext() { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - hasNext = true; - return true; - } - pos++; - } - hasNext = false; - return false; - } - - /** - * Returns the next element in the iteration. - * - * @return the next element in the iteration - * @throws NoSuchElementException if the iteration has no more elements - */ - @Override - public FastHashSet.IndexedKey next() { - this.checkForConcurrentModification.run(); - if (hasNext || hasNext()) { - hasNext = false; - return new FastHashSet.IndexedKey<>(pos, entries[pos++]); - } - throw new NoSuchElementException(); - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - action.accept(new FastHashSet.IndexedKey<>(pos, entries[pos])); - } - pos++; - } - this.checkForConcurrentModification.run(); - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java index 936476a80ff..e0b79cd1ff6 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java @@ -21,34 +21,55 @@ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.Sized; import org.apache.jena.util.iterator.NiceIterator; -import java.util.Iterator; +import java.util.ConcurrentModificationException; import java.util.NoSuchElementException; import java.util.function.Consumer; /** - * An iterator over a sparse array, that skips null entries. + * Iterator over a sparse array, walking from high index to low and skipping + * {@code null} entries. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it on each call to + * {@link #next()} / {@link #forEachRemaining(Consumer)}; throws + * {@link ConcurrentModificationException} if the size has changed. * * @param the type of the array elements */ -public class SparseArrayIterator extends NiceIterator implements Iterator { +public class SparseArrayIterator extends NiceIterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; private boolean hasNext = false; - public SparseArrayIterator(final E[] entries, final Runnable checkForConcurrentModification) { + /** + * Iterate over the whole array. + * + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, final Sized set) { this.entries = entries; this.pos = entries.length - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } - public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { + /** + * Iterate over {@code entries[0 .. toIndexExclusive)} (in reverse order). + * + * @param entries the backing array (not copied) + * @param toIndexExclusive exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Sized set) { this.entries = entries; this.pos = toIndexExclusive - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** @@ -62,13 +83,11 @@ public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnab public boolean hasNext() { while (-1 < pos) { if (null != entries[pos]) { - hasNext = true; - return true; + return hasNext = true; } pos--; } - hasNext = false; - return false; + return hasNext = false; } /** @@ -79,7 +98,7 @@ public boolean hasNext() { */ @Override public E next() { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (hasNext || hasNext()) { hasNext = false; return entries[pos--]; @@ -95,6 +114,6 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java index 94008b155f1..d8536f56311 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java @@ -22,8 +22,17 @@ package org.apache.jena.mem.pattern; /** - * A pattern for matching triples. - * The pattern is defined by the wildcard positions for the subject, predicate and object. + * Categorizes a triple-match pattern by which of the subject, predicate and + * object slots are concrete and which are wildcards (i.e. {@code Node.ANY} + * or {@code null}). + * + * The eight enum values cover every possible combination. Triple-store + * implementations dispatch on this enum to pick the most efficient lookup + * path for each kind of pattern (e.g. a fully concrete {@link #SUB_PRE_OBJ} + * is answered directly from the triple set, while a partially open pattern + * such as {@link #ANY_PRE_OBJ} is answered through an index intersection). + * + * @see PatternClassifier */ public enum MatchPattern { /** diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java index 32a6ba182a1..e4cf5644eca 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java @@ -25,14 +25,15 @@ import org.apache.jena.graph.Triple; /** - * Classify a triple match into one of the 8 match patterns. + * Utility class that classifies a triple match into one of the eight + * {@link MatchPattern} values. * - * The classification is based on the concrete-ness of the subject, predicate and object. - * A concrete node is one that is not a variable. + * The classification is based on which of the subject, predicate and object + * are concrete (anything that is not a variable / wildcard / + * {@code null}) and which are wildcards. The result is used by triple-store + * implementations to dispatch to the most efficient lookup path. * - * The classification is used to select the most efficient implementation of a triple store. - * - * This is a utility class; there is no need to instantiate it. + * All operations are stateless; this class is not meant to be instantiated. * * @see MatchPattern */ @@ -41,8 +42,16 @@ public class PatternClassifier { private PatternClassifier() { } + /** + * Classify a triple match. + * + * @param tripleMatch the match triple, possibly containing wildcard nodes + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Triple tripleMatch) { - if (tripleMatch.isConcrete()) { + if (tripleMatch.getSubject().isConcrete() + && tripleMatch.getPredicate().isConcrete() + && tripleMatch.getObject().isConcrete()) { return MatchPattern.SUB_PRE_OBJ; } else { if (tripleMatch.getSubject().isConcrete()) { @@ -73,6 +82,15 @@ public static MatchPattern classify(Triple tripleMatch) { } } + /** + * Classify a triple match given as three nodes. + * Any {@code null} or non-concrete node is treated as a wildcard. + * + * @param sm subject node, or {@code null}/wildcard + * @param pm predicate node, or {@code null}/wildcard + * @param om object node, or {@code null}/wildcard + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Node sm, Node pm, Node om) { if (null != sm && sm.isConcrete()) { if (null != pm && pm.isConcrete()) { @@ -103,6 +121,5 @@ public static MatchPattern classify(Node sm, Node pm, Node om) { } } } - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java index 43bbfeeaea8..a5033c22cde 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java @@ -21,52 +21,57 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a contiguous array slice {@code [0, toIndex)}, + * iterating from high index to low. Supports splitting into + * {@link ArraySubSpliterator} children for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySpliterator implements Spliterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public ArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (-1 < --pos) { action.accept(entries[pos]); return true; @@ -79,7 +84,7 @@ public void forEachRemaining(Consumer super E> action) { while (-1 < --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -89,7 +94,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -101,4 +106,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java index 74994708b53..638f2bb0c9e 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java @@ -21,55 +21,61 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a contiguous array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low. Produced by splitting an + * {@link ArraySpliterator} (or another {@link ArraySubSpliterator}); supports + * further recursive splits for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public ArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (fromIndex <= --pos) { action.accept(entries[pos]); return true; @@ -82,7 +88,7 @@ public void forEachRemaining(Consumer super E> action) { while (fromIndex <= --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -93,7 +99,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -105,4 +111,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java deleted file mode 100644 index 704c9642706..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.spliterator; - -import java.util.Spliterator; -import java.util.function.Consumer; - -import org.apache.jena.mem.collection.FastHashSet; - -/** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * This spliterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * This spliterator works in ascending order, starting from the given start up to the specified exclusive index. - * - * This spliterator supports splitting into sub-spliterators. - * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedSpliterator implements Spliterator> { - - private final E[] entries; - private int currentPositionMinusOne; - private final int toIndexExclusive; - private final Runnable checkForConcurrentModification; - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param fromIndexInclusive the index of the first element, inclusive - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int fromIndexInclusive, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.currentPositionMinusOne = fromIndexInclusive-1; // Start at fromIndexInclusive - 1, so that the first call to tryAdvance will increment pos to fromIndexInclusive - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this(entries, 0, toIndexExclusive, checkForConcurrentModification); - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); - } - - - @Override - public boolean tryAdvance(Consumer super FastHashSet.IndexedKey> action) { - this.checkForConcurrentModification.run(); - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - return true; - } - } - return false; - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - } - } - this.checkForConcurrentModification.run(); - } - - @Override - public Spliterator> trySplit() { - final var nextPos = currentPositionMinusOne + 1; - final var remaining = toIndexExclusive - nextPos; - if ( remaining < 2) { - return null; - } - final var mid = nextPos + ( remaining >>> 1); - final var fromIndexInclusive = nextPos; - this.currentPositionMinusOne = mid-1; - return new SparseArrayIndexedSpliterator<>(entries, fromIndexInclusive, mid, checkForConcurrentModification); - } - - @Override - public long estimateSize() { return (long) toIndexExclusive - currentPositionMinusOne; } - - @Override - public int characteristics() { - return DISTINCT | NONNULL | IMMUTABLE; - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java index 6752cc9a1c1..add45739dc2 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java @@ -21,17 +21,24 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a sparse array slice {@code [0, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced for backing arrays such as those of + * {@link org.apache.jena.mem.collection.FastHashBase}, where removed slots + * are represented by {@code null}. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Supports splitting into {@link SparseArraySubSpliterator} children for + * parallel traversal. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it at each + * advance/forEach boundary; throws {@link ConcurrentModificationException} + * if the size has changed. * * @param the type of the array elements */ @@ -39,35 +46,37 @@ public class SparseArraySpliterator implements Spliterator { private final E[] entries; private int pos; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}, skipping nulls. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public SparseArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (-1 < --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -86,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -96,7 +105,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java index 3eb0784326f..d79242ac78c 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java @@ -21,55 +21,62 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a sparse array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced by splitting a {@link SparseArraySpliterator} (or another + * {@link SparseArraySubSpliterator}); supports further recursive splits for + * parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary; + * throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the type of the array elements */ public class SparseArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}, skipping nulls. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public SparseArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (fromIndex <= --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -88,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -99,7 +106,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java index 07ccc9634a9..f0fba805175 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java @@ -32,26 +32,41 @@ import java.util.function.Predicate; /** - * An ArrayBunch implements TripleBunch with a linear search of a short-ish - * array of Triples. The array grows by factor 2. + * Linear-scan implementation of {@link FastTripleBunch} backed by a packed + * {@link Triple} array. Used as long as a bunch stays small; once it grows + * past the configured threshold (see {@link FastTripleStore}) it is replaced + * with a {@link FastHashedTripleBunch}. + * + * The array grows by a factor of two when full. Equality of triples within a + * bunch is delegated to {@link #areEqual(Triple, Triple)}, which subclasses + * specialize to compare only the two nodes that are not already + * implied by the enclosing map's key. This avoids redundant equality checks + * on the shared subject/predicate/object. + * + * Not thread-safe. */ public abstract class FastArrayBunch implements FastTripleBunch { private static final int INITIAL_SIZE = 4; + /** Number of valid entries in {@link #elements}. */ protected int size = 0; + /** Packed array of triples; entries from {@code 0} to {@code size-1} are live. */ protected Triple[] elements; + /** + * Creates an empty bunch with the default initial capacity. + */ protected FastArrayBunch() { elements = new Triple[INITIAL_SIZE]; } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. - * But it will reserve only the space needed to contain them. Growing is still possible. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}; its backing array is sized to fit exactly, + * but can grow further if needed. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -59,7 +74,17 @@ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.size = bunchToCopy.size; } - public abstract boolean areEqual(final Triple a, final Triple b); + /** + * Compare two triples for equality within this bunch. + * + * Subclasses specialize this to skip the already-shared component + * (subject, predicate or object) and compare only the remaining two. + * + * @param a first triple + * @param b second triple + * @return {@code true} if the triples are considered equal in this bunch + */ + protected abstract boolean areEqual(final Triple a, final Triple b); @Override public boolean containsKey(Triple t) { @@ -127,6 +152,7 @@ public boolean tryRemove(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return true; } } @@ -138,6 +164,7 @@ public void removeUnchecked(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return; } } @@ -174,11 +201,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java index b89d3312048..a49d6b54009 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java @@ -25,21 +25,28 @@ import org.apache.jena.mem.collection.FastHashMap; /** - * Map from nodes to triple bunches. + * {@link FastHashMap} specialized to map a {@link Node} to its associated + * {@link FastTripleBunch}. Used by {@link FastTripleStore} to maintain the + * three subject/predicate/object indices. */ public class FastHashedBunchMap extends FastHashMap implements Copyable { + /** + * Creates an empty bunch map with the default initial capacity. + */ public FastHashedBunchMap() { super(); } /** - * Copy constructor. - * The new map will contain all the same nodes as keys of the map to copy, but copies of the bunches as values . + * Copy constructor. The new map has the same node keys as + * {@code mapToCopy}; each value is replaced by a deep copy of the + * corresponding bunch (via {@link FastTripleBunch#copy()}) so that + * mutations of either map cannot affect the other. * - * @param mapToCopy + * @param mapToCopy the source map */ private FastHashedBunchMap(final FastHashedBunchMap mapToCopy) { super(mapToCopy, FastTripleBunch::copy); diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java index 459e78c8181..65c9ab70fbf 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java @@ -25,13 +25,21 @@ import org.apache.jena.mem.collection.JenaSet; /** - * A set of triples - backed by {@link FastHashSet}. + * Hashed implementation of {@link FastTripleBunch} built on top of + * {@link FastHashSet}. Used by {@link FastTripleStore} once a bunch grows + * past the size threshold at which a linear-scan {@link FastArrayBunch} + * stops being faster. */ public class FastHashedTripleBunch extends FastHashSet implements FastTripleBunch { + /** - * Create a new triple bunch from the given set of triples. + * Create a new hashed bunch pre-populated from the given set of triples. + * The initial capacity is chosen at 1.5x the source size, so the new bunch + * fits the existing triples and has some headroom for growth before it + * needs to rehash. * - * @param set the set of triples + * @param set the source set of triples (typically the array bunch being + * promoted) */ public FastHashedTripleBunch(final JenaSet set) { super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth @@ -39,15 +47,18 @@ public FastHashedTripleBunch(final JenaSet set) { } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ private FastHashedTripleBunch(final FastHashedTripleBunch bunchToCopy) { super(bunchToCopy); } + /** + * Creates an empty hashed bunch with the default initial capacity. + */ public FastHashedTripleBunch() { super(); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java index 68f79e72f8a..fe050283188 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java @@ -29,27 +29,39 @@ import java.util.function.Predicate; /** - * A bunch of triples - a stripped-down set with specialized methods. A - * bunch is expected to store triples that share some useful property - * (such as having the same subject or predicate). + * Set-like container for a "bunch" of triples that share some useful + * property - typically they all have the same subject, predicate or object, + * because the bunch is the value of a node-keyed map in a + * {@link FastTripleStore}. + * + * The interface is a stripped-down set with a few extras tuned for the + * triple-store hot path; concrete implementations are + * {@link FastArrayBunch} (linear scan, used while the bunch is small) and + * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a + * threshold). */ public interface FastTripleBunch extends JenaSetHashOptimized, Copyable { /** - * Answer true iff this bunch is implemented as an array. - * This field is used to optimize some operations by avoiding the need for instanceOf tests. + * Answer {@code true} iff this bunch is backed by a flat array (i.e. is + * a {@link FastArrayBunch}). Exposed as an explicit method so callers can + * avoid {@code instanceof} checks on this hot path. * - * @return true iff this bunch is implemented as an arrays + * @return {@code true} if this bunch is array-backed */ boolean isArray(); /** - * This method is used to optimize _PO match operations. - * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches. - * This method is faster if there are many matches and the set is ordered in an unfavorable way. - * _PO matches usually fall into this category. + * Predicate test that scans elements in hash-table order rather than + * dense insertion order. Tuned for {@code _PO} (any-predicate-object) + * matches. + * + * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
+ * The hash-code overloads are a performance shortcut for callers that already + * have the hash at hand (typically because the same key is stored in several + * collections). The supplied hash code MUST equal {@code key.hashCode()}, or + * the map will misbehave. + * + * @param the type of the keys in the map + * @param the type of the values in the map + */ +public interface JenaMapIndexed extends JenaMap { + + /** + * Returns the index of the entry with the given key, or a negative value + * if no such entry exists. + * + * @param key the key to look up + * @return the index of the entry, or a negative value if absent + */ + int indexOf(K key); + + /** + * Returns the key stored at the given index. + * + * @param index the index of the entry + * @return the key at that index + */ + K getKeyAt(int index); + + /** + * Returns the value stored at the given index. + * + * @param index the index of the entry + * @return the value at that index + */ + V getValueAt(int index); + + /** + * Put a key-value pair and return the index of the affected entry. + * If the key is already present, its value is updated and the existing + * index is returned. + * + * @param key the key to put + * @param value the value to put + * @return the index of the entry holding {@code key} + */ + int putAndGetIndex(K key, V value); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java index 2533714ce6b..7f96baa19f9 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaMapSetCommon.java @@ -28,22 +28,23 @@ import java.util.stream.StreamSupport; /** - * Common interface for {@link JenaMap} and {@link JenaSet}. * + * Operations shared between the map ({@link JenaMap}) and the set + * ({@link JenaSet}) collections used in the {@code mem} triple store + * implementations. + * + * These collections trade some flexibility for speed: they expose only the + * operations needed by triple-store internals (no full {@link java.util.Map} + * or {@link java.util.Set} contract). They are not thread-safe. * - * @param the type of the keys/elements in the collection + * @param the type of the keys (or elements, for sets) in the collection */ -public interface JenaMapSetCommon { +public interface JenaMapSetCommon extends Sized { /** * Clear the collection. */ void clear(); - /** - * @return the number of elements in the collection - */ - int size(); - /** * @return true if the collection is empty */ @@ -75,7 +76,10 @@ public interface JenaMapSetCommon { /** * Removes a key from the collection. - * Attention: Implementations may assume that the key is present. + * + * Attention: implementations may assume the key is present and may produce + * undefined behavior (including silently corrupting internal state) if it + * is not. Use {@link #tryRemove(Object)} when in doubt. * * @param key the key to remove */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java index d3b8a557be9..03848073f56 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java @@ -21,9 +21,10 @@ package org.apache.jena.mem.collection; /** - * Set interface specialized for the use cases in triple store implementations. + * Set interface specialized for the use cases in triple-store implementations. + * Not thread-safe; does not allow {@code null} elements. * - * @param + * @param the element type of the set */ public interface JenaSet extends JenaMapSetCommon { @@ -31,13 +32,16 @@ public interface JenaSet extends JenaMapSetCommon { * Add the key to the set if it is not already present. * * @param key the key to add - * @return true if the key was added, false if it was already present + * @return {@code true} if the key was added, {@code false} if it was already present */ boolean tryAdd(E key); /** - * Add the key to the set without checking if it is already present. - * Attention: This method must only be used if it is guaranteed that the key is not already present. + * Add the key to the set without checking whether it is already present. + * + * Attention: this method must only be used if the caller has ensured that + * the key is not already in the set; otherwise the set's invariants will + * break (duplicates may be inserted). * * @param key the key to add */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java index 8cc8aad8daf..0e1d032b356 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java @@ -22,17 +22,50 @@ /** - * Extension of {@link JenaSet} that allows to add and remove elements - * with a given hash code. - * This is useful if the hash code is already known. - * Attention: The hash code must be consistent with E::hashCode(). + * Extension of {@link JenaSet} that lets callers supply a precomputed hash + * code. + * + * Attention: any caller-supplied hash code MUST equal {@code E.hashCode()}; + * if it does not, the set will misbehave. + * + * @param the element type of the set */ public interface JenaSetHashOptimized extends JenaSet { + + /** + * Add an element with the given precomputed hash code if it is not + * already present. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + * @return {@code true} if added, {@code false} if already present + */ boolean tryAdd(E key, int hashCode); + /** + * Add an element with the given precomputed hash code without checking + * whether it is already present. The caller MUST ensure the key is absent. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + */ void addUnchecked(E key, int hashCode); + /** + * Try to remove an element with the given precomputed hash code. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + * @return {@code true} if removed, {@code false} if it was not present + */ boolean tryRemove(E key, int hashCode); + /** + * Remove an element assumed to be present, with the given precomputed + * hash code. Behavior is undefined if the element is not in the set. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + */ void removeUnchecked(E key, int hashCode); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java new file mode 100644 index 00000000000..c7c3d2e1ddb --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.mem.collection; + + +/** + * Extension of {@link JenaSetHashOptimized} that exposes index-based access to elements. + * Indices are stable handles to entries (returned by {@link #addAndGetIndex(Object)}) and remain + * valid until the corresponding entry is removed. + * + * @param the element type of the set + */ +public interface JenaSetIndexed extends JenaSetHashOptimized { + + /** + * Add an element and return the index it was stored at. If the element + * is already present, returns a negative value (typically the bitwise + * complement of the existing index). + * + * @param key the element to add + * @return the index of the inserted element, or a negative value if the + * element was already present + */ + int addAndGetIndex(final E key); + + /** + * Returns the element stored at the given index. + * + * @param index the index to read + * @return the element at that index + */ + E getKeyAt(int index); + + /** + * Returns the index of the given element, or a negative value if it is + * not in the set. + * + * @param key the element to look up + * @return the index of {@code key}, or a negative value if absent + */ + int indexOf(E key); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java new file mode 100644 index 00000000000..237740ce8e3 --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.mem.collection; + +/** + * Base interface for sized collections. + * It is typically used to detect concurrent modifications in iterators and spliterators + * by snapshotting the size at construction time and rechecking it at each advance/forEach boundary. + */ +public interface Sized { + + /** + * @return the number of elements in the collection + */ + int size(); +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java index b0ac6e994bb..8cfc8948a25 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java @@ -30,16 +30,27 @@ import java.util.function.Consumer; /** - * Iterator that iterates over the entries of sets which are contained in the given iterator of sets. + * Flat-map style iterator that yields every element of every {@link JenaSet} + * produced by the given parent iterator. Empty inner sets are silently + * skipped. Equivalent in spirit to a one-level {@code flatMap} but tailored + * to the {@link JenaSet} API and to {@link NiceIterator}. * - * @param the type of the elements + * @param the element type of the inner sets */ public class IteratorOfJenaSets extends NiceIterator { - final Iterator extends JenaSet> parentIterator; + /** Source iterator producing the sets to flatten. */ + private final Iterator extends JenaSet> parentIterator; - ExtendedIterator currentIterator; + /** Iterator over the keys of the set currently being consumed. */ + private ExtendedIterator currentIterator; + /** + * Create a flat iterator over the elements of every set produced by + * {@code parentIterator}. + * + * @param parentIterator the source iterator of sets + */ public IteratorOfJenaSets(Iterator extends JenaSet> parentIterator) { this.parentIterator = parentIterator; this.currentIterator = parentIterator.hasNext() diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java deleted file mode 100644 index 37f103eae25..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.iterator; - -import org.apache.jena.mem.collection.FastHashSet; -import org.apache.jena.util.iterator.NiceIterator; - -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.function.Consumer; - -/** - * An iterator over a sparse array, that skips null entries. - * This iterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * The iterator works in ascending order, starting from index 0 up to the specified exclusive index. - * - * This iterator will check for concurrent modifications by invoking a {@link Runnable} - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedIterator extends NiceIterator> implements Iterator> { - - private final E[] entries; - private final Runnable checkForConcurrentModification; - private int pos = 0; - private final int toIndexExclusive; - private boolean hasNext = false; - - public SparseArrayIndexedIterator(final E[] entries, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = entries.length; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - public SparseArrayIndexedIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Returns {@code true} if the iteration has more elements. - * (In other words, returns {@code true} if {@link #next} would - * return an element rather than throwing an exception.) - * - * @return {@code true} if the iteration has more elements - */ - @Override - public boolean hasNext() { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - hasNext = true; - return true; - } - pos++; - } - hasNext = false; - return false; - } - - /** - * Returns the next element in the iteration. - * - * @return the next element in the iteration - * @throws NoSuchElementException if the iteration has no more elements - */ - @Override - public FastHashSet.IndexedKey next() { - this.checkForConcurrentModification.run(); - if (hasNext || hasNext()) { - hasNext = false; - return new FastHashSet.IndexedKey<>(pos, entries[pos++]); - } - throw new NoSuchElementException(); - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - action.accept(new FastHashSet.IndexedKey<>(pos, entries[pos])); - } - pos++; - } - this.checkForConcurrentModification.run(); - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java index 936476a80ff..e0b79cd1ff6 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java @@ -21,34 +21,55 @@ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.Sized; import org.apache.jena.util.iterator.NiceIterator; -import java.util.Iterator; +import java.util.ConcurrentModificationException; import java.util.NoSuchElementException; import java.util.function.Consumer; /** - * An iterator over a sparse array, that skips null entries. + * Iterator over a sparse array, walking from high index to low and skipping + * {@code null} entries. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it on each call to + * {@link #next()} / {@link #forEachRemaining(Consumer)}; throws + * {@link ConcurrentModificationException} if the size has changed. * * @param the type of the array elements */ -public class SparseArrayIterator extends NiceIterator implements Iterator { +public class SparseArrayIterator extends NiceIterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; private boolean hasNext = false; - public SparseArrayIterator(final E[] entries, final Runnable checkForConcurrentModification) { + /** + * Iterate over the whole array. + * + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, final Sized set) { this.entries = entries; this.pos = entries.length - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } - public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { + /** + * Iterate over {@code entries[0 .. toIndexExclusive)} (in reverse order). + * + * @param entries the backing array (not copied) + * @param toIndexExclusive exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Sized set) { this.entries = entries; this.pos = toIndexExclusive - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** @@ -62,13 +83,11 @@ public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnab public boolean hasNext() { while (-1 < pos) { if (null != entries[pos]) { - hasNext = true; - return true; + return hasNext = true; } pos--; } - hasNext = false; - return false; + return hasNext = false; } /** @@ -79,7 +98,7 @@ public boolean hasNext() { */ @Override public E next() { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (hasNext || hasNext()) { hasNext = false; return entries[pos--]; @@ -95,6 +114,6 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java index 94008b155f1..d8536f56311 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java @@ -22,8 +22,17 @@ package org.apache.jena.mem.pattern; /** - * A pattern for matching triples. - * The pattern is defined by the wildcard positions for the subject, predicate and object. + * Categorizes a triple-match pattern by which of the subject, predicate and + * object slots are concrete and which are wildcards (i.e. {@code Node.ANY} + * or {@code null}). + * + * The eight enum values cover every possible combination. Triple-store + * implementations dispatch on this enum to pick the most efficient lookup + * path for each kind of pattern (e.g. a fully concrete {@link #SUB_PRE_OBJ} + * is answered directly from the triple set, while a partially open pattern + * such as {@link #ANY_PRE_OBJ} is answered through an index intersection). + * + * @see PatternClassifier */ public enum MatchPattern { /** diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java index 32a6ba182a1..e4cf5644eca 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java @@ -25,14 +25,15 @@ import org.apache.jena.graph.Triple; /** - * Classify a triple match into one of the 8 match patterns. + * Utility class that classifies a triple match into one of the eight + * {@link MatchPattern} values. * - * The classification is based on the concrete-ness of the subject, predicate and object. - * A concrete node is one that is not a variable. + * The classification is based on which of the subject, predicate and object + * are concrete (anything that is not a variable / wildcard / + * {@code null}) and which are wildcards. The result is used by triple-store + * implementations to dispatch to the most efficient lookup path. * - * The classification is used to select the most efficient implementation of a triple store. - * - * This is a utility class; there is no need to instantiate it. + * All operations are stateless; this class is not meant to be instantiated. * * @see MatchPattern */ @@ -41,8 +42,16 @@ public class PatternClassifier { private PatternClassifier() { } + /** + * Classify a triple match. + * + * @param tripleMatch the match triple, possibly containing wildcard nodes + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Triple tripleMatch) { - if (tripleMatch.isConcrete()) { + if (tripleMatch.getSubject().isConcrete() + && tripleMatch.getPredicate().isConcrete() + && tripleMatch.getObject().isConcrete()) { return MatchPattern.SUB_PRE_OBJ; } else { if (tripleMatch.getSubject().isConcrete()) { @@ -73,6 +82,15 @@ public static MatchPattern classify(Triple tripleMatch) { } } + /** + * Classify a triple match given as three nodes. + * Any {@code null} or non-concrete node is treated as a wildcard. + * + * @param sm subject node, or {@code null}/wildcard + * @param pm predicate node, or {@code null}/wildcard + * @param om object node, or {@code null}/wildcard + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Node sm, Node pm, Node om) { if (null != sm && sm.isConcrete()) { if (null != pm && pm.isConcrete()) { @@ -103,6 +121,5 @@ public static MatchPattern classify(Node sm, Node pm, Node om) { } } } - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java index 43bbfeeaea8..a5033c22cde 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java @@ -21,52 +21,57 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a contiguous array slice {@code [0, toIndex)}, + * iterating from high index to low. Supports splitting into + * {@link ArraySubSpliterator} children for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySpliterator implements Spliterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public ArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (-1 < --pos) { action.accept(entries[pos]); return true; @@ -79,7 +84,7 @@ public void forEachRemaining(Consumer super E> action) { while (-1 < --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -89,7 +94,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -101,4 +106,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java index 74994708b53..638f2bb0c9e 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java @@ -21,55 +21,61 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a contiguous array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low. Produced by splitting an + * {@link ArraySpliterator} (or another {@link ArraySubSpliterator}); supports + * further recursive splits for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public ArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (fromIndex <= --pos) { action.accept(entries[pos]); return true; @@ -82,7 +88,7 @@ public void forEachRemaining(Consumer super E> action) { while (fromIndex <= --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -93,7 +99,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -105,4 +111,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java deleted file mode 100644 index 704c9642706..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.spliterator; - -import java.util.Spliterator; -import java.util.function.Consumer; - -import org.apache.jena.mem.collection.FastHashSet; - -/** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * This spliterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * This spliterator works in ascending order, starting from the given start up to the specified exclusive index. - * - * This spliterator supports splitting into sub-spliterators. - * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedSpliterator implements Spliterator> { - - private final E[] entries; - private int currentPositionMinusOne; - private final int toIndexExclusive; - private final Runnable checkForConcurrentModification; - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param fromIndexInclusive the index of the first element, inclusive - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int fromIndexInclusive, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.currentPositionMinusOne = fromIndexInclusive-1; // Start at fromIndexInclusive - 1, so that the first call to tryAdvance will increment pos to fromIndexInclusive - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this(entries, 0, toIndexExclusive, checkForConcurrentModification); - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); - } - - - @Override - public boolean tryAdvance(Consumer super FastHashSet.IndexedKey> action) { - this.checkForConcurrentModification.run(); - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - return true; - } - } - return false; - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - } - } - this.checkForConcurrentModification.run(); - } - - @Override - public Spliterator> trySplit() { - final var nextPos = currentPositionMinusOne + 1; - final var remaining = toIndexExclusive - nextPos; - if ( remaining < 2) { - return null; - } - final var mid = nextPos + ( remaining >>> 1); - final var fromIndexInclusive = nextPos; - this.currentPositionMinusOne = mid-1; - return new SparseArrayIndexedSpliterator<>(entries, fromIndexInclusive, mid, checkForConcurrentModification); - } - - @Override - public long estimateSize() { return (long) toIndexExclusive - currentPositionMinusOne; } - - @Override - public int characteristics() { - return DISTINCT | NONNULL | IMMUTABLE; - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java index 6752cc9a1c1..add45739dc2 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java @@ -21,17 +21,24 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a sparse array slice {@code [0, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced for backing arrays such as those of + * {@link org.apache.jena.mem.collection.FastHashBase}, where removed slots + * are represented by {@code null}. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Supports splitting into {@link SparseArraySubSpliterator} children for + * parallel traversal. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it at each + * advance/forEach boundary; throws {@link ConcurrentModificationException} + * if the size has changed. * * @param the type of the array elements */ @@ -39,35 +46,37 @@ public class SparseArraySpliterator implements Spliterator { private final E[] entries; private int pos; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}, skipping nulls. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public SparseArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (-1 < --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -86,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -96,7 +105,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java index 3eb0784326f..d79242ac78c 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java @@ -21,55 +21,62 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a sparse array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced by splitting a {@link SparseArraySpliterator} (or another + * {@link SparseArraySubSpliterator}); supports further recursive splits for + * parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary; + * throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the type of the array elements */ public class SparseArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}, skipping nulls. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public SparseArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (fromIndex <= --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -88,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -99,7 +106,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java index 07ccc9634a9..f0fba805175 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java @@ -32,26 +32,41 @@ import java.util.function.Predicate; /** - * An ArrayBunch implements TripleBunch with a linear search of a short-ish - * array of Triples. The array grows by factor 2. + * Linear-scan implementation of {@link FastTripleBunch} backed by a packed + * {@link Triple} array. Used as long as a bunch stays small; once it grows + * past the configured threshold (see {@link FastTripleStore}) it is replaced + * with a {@link FastHashedTripleBunch}. + * + * The array grows by a factor of two when full. Equality of triples within a + * bunch is delegated to {@link #areEqual(Triple, Triple)}, which subclasses + * specialize to compare only the two nodes that are not already + * implied by the enclosing map's key. This avoids redundant equality checks + * on the shared subject/predicate/object. + * + * Not thread-safe. */ public abstract class FastArrayBunch implements FastTripleBunch { private static final int INITIAL_SIZE = 4; + /** Number of valid entries in {@link #elements}. */ protected int size = 0; + /** Packed array of triples; entries from {@code 0} to {@code size-1} are live. */ protected Triple[] elements; + /** + * Creates an empty bunch with the default initial capacity. + */ protected FastArrayBunch() { elements = new Triple[INITIAL_SIZE]; } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. - * But it will reserve only the space needed to contain them. Growing is still possible. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}; its backing array is sized to fit exactly, + * but can grow further if needed. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -59,7 +74,17 @@ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.size = bunchToCopy.size; } - public abstract boolean areEqual(final Triple a, final Triple b); + /** + * Compare two triples for equality within this bunch. + * + * Subclasses specialize this to skip the already-shared component + * (subject, predicate or object) and compare only the remaining two. + * + * @param a first triple + * @param b second triple + * @return {@code true} if the triples are considered equal in this bunch + */ + protected abstract boolean areEqual(final Triple a, final Triple b); @Override public boolean containsKey(Triple t) { @@ -127,6 +152,7 @@ public boolean tryRemove(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return true; } } @@ -138,6 +164,7 @@ public void removeUnchecked(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return; } } @@ -174,11 +201,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java index b89d3312048..a49d6b54009 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java @@ -25,21 +25,28 @@ import org.apache.jena.mem.collection.FastHashMap; /** - * Map from nodes to triple bunches. + * {@link FastHashMap} specialized to map a {@link Node} to its associated + * {@link FastTripleBunch}. Used by {@link FastTripleStore} to maintain the + * three subject/predicate/object indices. */ public class FastHashedBunchMap extends FastHashMap implements Copyable { + /** + * Creates an empty bunch map with the default initial capacity. + */ public FastHashedBunchMap() { super(); } /** - * Copy constructor. - * The new map will contain all the same nodes as keys of the map to copy, but copies of the bunches as values . + * Copy constructor. The new map has the same node keys as + * {@code mapToCopy}; each value is replaced by a deep copy of the + * corresponding bunch (via {@link FastTripleBunch#copy()}) so that + * mutations of either map cannot affect the other. * - * @param mapToCopy + * @param mapToCopy the source map */ private FastHashedBunchMap(final FastHashedBunchMap mapToCopy) { super(mapToCopy, FastTripleBunch::copy); diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java index 459e78c8181..65c9ab70fbf 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java @@ -25,13 +25,21 @@ import org.apache.jena.mem.collection.JenaSet; /** - * A set of triples - backed by {@link FastHashSet}. + * Hashed implementation of {@link FastTripleBunch} built on top of + * {@link FastHashSet}. Used by {@link FastTripleStore} once a bunch grows + * past the size threshold at which a linear-scan {@link FastArrayBunch} + * stops being faster. */ public class FastHashedTripleBunch extends FastHashSet implements FastTripleBunch { + /** - * Create a new triple bunch from the given set of triples. + * Create a new hashed bunch pre-populated from the given set of triples. + * The initial capacity is chosen at 1.5x the source size, so the new bunch + * fits the existing triples and has some headroom for growth before it + * needs to rehash. * - * @param set the set of triples + * @param set the source set of triples (typically the array bunch being + * promoted) */ public FastHashedTripleBunch(final JenaSet set) { super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth @@ -39,15 +47,18 @@ public FastHashedTripleBunch(final JenaSet set) { } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ private FastHashedTripleBunch(final FastHashedTripleBunch bunchToCopy) { super(bunchToCopy); } + /** + * Creates an empty hashed bunch with the default initial capacity. + */ public FastHashedTripleBunch() { super(); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java index 68f79e72f8a..fe050283188 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java @@ -29,27 +29,39 @@ import java.util.function.Predicate; /** - * A bunch of triples - a stripped-down set with specialized methods. A - * bunch is expected to store triples that share some useful property - * (such as having the same subject or predicate). + * Set-like container for a "bunch" of triples that share some useful + * property - typically they all have the same subject, predicate or object, + * because the bunch is the value of a node-keyed map in a + * {@link FastTripleStore}. + * + * The interface is a stripped-down set with a few extras tuned for the + * triple-store hot path; concrete implementations are + * {@link FastArrayBunch} (linear scan, used while the bunch is small) and + * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a + * threshold). */ public interface FastTripleBunch extends JenaSetHashOptimized, Copyable { /** - * Answer true iff this bunch is implemented as an array. - * This field is used to optimize some operations by avoiding the need for instanceOf tests. + * Answer {@code true} iff this bunch is backed by a flat array (i.e. is + * a {@link FastArrayBunch}). Exposed as an explicit method so callers can + * avoid {@code instanceof} checks on this hot path. * - * @return true iff this bunch is implemented as an arrays + * @return {@code true} if this bunch is array-backed */ boolean isArray(); /** - * This method is used to optimize _PO match operations. - * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches. - * This method is faster if there are many matches and the set is ordered in an unfavorable way. - * _PO matches usually fall into this category. + * Predicate test that scans elements in hash-table order rather than + * dense insertion order. Tuned for {@code _PO} (any-predicate-object) + * matches. + * + * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
+ * These collections trade some flexibility for speed: they expose only the + * operations needed by triple-store internals (no full {@link java.util.Map} + * or {@link java.util.Set} contract). They are not thread-safe. * - * @param the type of the keys/elements in the collection + * @param the type of the keys (or elements, for sets) in the collection */ -public interface JenaMapSetCommon { +public interface JenaMapSetCommon extends Sized { /** * Clear the collection. */ void clear(); - /** - * @return the number of elements in the collection - */ - int size(); - /** * @return true if the collection is empty */ @@ -75,7 +76,10 @@ public interface JenaMapSetCommon { /** * Removes a key from the collection. - * Attention: Implementations may assume that the key is present. + * + * Attention: implementations may assume the key is present and may produce + * undefined behavior (including silently corrupting internal state) if it + * is not. Use {@link #tryRemove(Object)} when in doubt. * * @param key the key to remove */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java index d3b8a557be9..03848073f56 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java @@ -21,9 +21,10 @@ package org.apache.jena.mem.collection; /** - * Set interface specialized for the use cases in triple store implementations. + * Set interface specialized for the use cases in triple-store implementations. + * Not thread-safe; does not allow {@code null} elements. * - * @param + * @param the element type of the set */ public interface JenaSet extends JenaMapSetCommon { @@ -31,13 +32,16 @@ public interface JenaSet extends JenaMapSetCommon { * Add the key to the set if it is not already present. * * @param key the key to add - * @return true if the key was added, false if it was already present + * @return {@code true} if the key was added, {@code false} if it was already present */ boolean tryAdd(E key); /** - * Add the key to the set without checking if it is already present. - * Attention: This method must only be used if it is guaranteed that the key is not already present. + * Add the key to the set without checking whether it is already present. + * + * Attention: this method must only be used if the caller has ensured that + * the key is not already in the set; otherwise the set's invariants will + * break (duplicates may be inserted). * * @param key the key to add */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java index 8cc8aad8daf..0e1d032b356 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java @@ -22,17 +22,50 @@ /** - * Extension of {@link JenaSet} that allows to add and remove elements - * with a given hash code. - * This is useful if the hash code is already known. - * Attention: The hash code must be consistent with E::hashCode(). + * Extension of {@link JenaSet} that lets callers supply a precomputed hash + * code. + * + * Attention: any caller-supplied hash code MUST equal {@code E.hashCode()}; + * if it does not, the set will misbehave. + * + * @param the element type of the set */ public interface JenaSetHashOptimized extends JenaSet { + + /** + * Add an element with the given precomputed hash code if it is not + * already present. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + * @return {@code true} if added, {@code false} if already present + */ boolean tryAdd(E key, int hashCode); + /** + * Add an element with the given precomputed hash code without checking + * whether it is already present. The caller MUST ensure the key is absent. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + */ void addUnchecked(E key, int hashCode); + /** + * Try to remove an element with the given precomputed hash code. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + * @return {@code true} if removed, {@code false} if it was not present + */ boolean tryRemove(E key, int hashCode); + /** + * Remove an element assumed to be present, with the given precomputed + * hash code. Behavior is undefined if the element is not in the set. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + */ void removeUnchecked(E key, int hashCode); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java new file mode 100644 index 00000000000..c7c3d2e1ddb --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.mem.collection; + + +/** + * Extension of {@link JenaSetHashOptimized} that exposes index-based access to elements. + * Indices are stable handles to entries (returned by {@link #addAndGetIndex(Object)}) and remain + * valid until the corresponding entry is removed. + * + * @param the element type of the set + */ +public interface JenaSetIndexed extends JenaSetHashOptimized { + + /** + * Add an element and return the index it was stored at. If the element + * is already present, returns a negative value (typically the bitwise + * complement of the existing index). + * + * @param key the element to add + * @return the index of the inserted element, or a negative value if the + * element was already present + */ + int addAndGetIndex(final E key); + + /** + * Returns the element stored at the given index. + * + * @param index the index to read + * @return the element at that index + */ + E getKeyAt(int index); + + /** + * Returns the index of the given element, or a negative value if it is + * not in the set. + * + * @param key the element to look up + * @return the index of {@code key}, or a negative value if absent + */ + int indexOf(E key); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java new file mode 100644 index 00000000000..237740ce8e3 --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.mem.collection; + +/** + * Base interface for sized collections. + * It is typically used to detect concurrent modifications in iterators and spliterators + * by snapshotting the size at construction time and rechecking it at each advance/forEach boundary. + */ +public interface Sized { + + /** + * @return the number of elements in the collection + */ + int size(); +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java index b0ac6e994bb..8cfc8948a25 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java @@ -30,16 +30,27 @@ import java.util.function.Consumer; /** - * Iterator that iterates over the entries of sets which are contained in the given iterator of sets. + * Flat-map style iterator that yields every element of every {@link JenaSet} + * produced by the given parent iterator. Empty inner sets are silently + * skipped. Equivalent in spirit to a one-level {@code flatMap} but tailored + * to the {@link JenaSet} API and to {@link NiceIterator}. * - * @param the type of the elements + * @param the element type of the inner sets */ public class IteratorOfJenaSets extends NiceIterator { - final Iterator extends JenaSet> parentIterator; + /** Source iterator producing the sets to flatten. */ + private final Iterator extends JenaSet> parentIterator; - ExtendedIterator currentIterator; + /** Iterator over the keys of the set currently being consumed. */ + private ExtendedIterator currentIterator; + /** + * Create a flat iterator over the elements of every set produced by + * {@code parentIterator}. + * + * @param parentIterator the source iterator of sets + */ public IteratorOfJenaSets(Iterator extends JenaSet> parentIterator) { this.parentIterator = parentIterator; this.currentIterator = parentIterator.hasNext() diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java deleted file mode 100644 index 37f103eae25..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.iterator; - -import org.apache.jena.mem.collection.FastHashSet; -import org.apache.jena.util.iterator.NiceIterator; - -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.function.Consumer; - -/** - * An iterator over a sparse array, that skips null entries. - * This iterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * The iterator works in ascending order, starting from index 0 up to the specified exclusive index. - * - * This iterator will check for concurrent modifications by invoking a {@link Runnable} - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedIterator extends NiceIterator> implements Iterator> { - - private final E[] entries; - private final Runnable checkForConcurrentModification; - private int pos = 0; - private final int toIndexExclusive; - private boolean hasNext = false; - - public SparseArrayIndexedIterator(final E[] entries, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = entries.length; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - public SparseArrayIndexedIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Returns {@code true} if the iteration has more elements. - * (In other words, returns {@code true} if {@link #next} would - * return an element rather than throwing an exception.) - * - * @return {@code true} if the iteration has more elements - */ - @Override - public boolean hasNext() { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - hasNext = true; - return true; - } - pos++; - } - hasNext = false; - return false; - } - - /** - * Returns the next element in the iteration. - * - * @return the next element in the iteration - * @throws NoSuchElementException if the iteration has no more elements - */ - @Override - public FastHashSet.IndexedKey next() { - this.checkForConcurrentModification.run(); - if (hasNext || hasNext()) { - hasNext = false; - return new FastHashSet.IndexedKey<>(pos, entries[pos++]); - } - throw new NoSuchElementException(); - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - action.accept(new FastHashSet.IndexedKey<>(pos, entries[pos])); - } - pos++; - } - this.checkForConcurrentModification.run(); - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java index 936476a80ff..e0b79cd1ff6 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java @@ -21,34 +21,55 @@ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.Sized; import org.apache.jena.util.iterator.NiceIterator; -import java.util.Iterator; +import java.util.ConcurrentModificationException; import java.util.NoSuchElementException; import java.util.function.Consumer; /** - * An iterator over a sparse array, that skips null entries. + * Iterator over a sparse array, walking from high index to low and skipping + * {@code null} entries. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it on each call to + * {@link #next()} / {@link #forEachRemaining(Consumer)}; throws + * {@link ConcurrentModificationException} if the size has changed. * * @param the type of the array elements */ -public class SparseArrayIterator extends NiceIterator implements Iterator { +public class SparseArrayIterator extends NiceIterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; private boolean hasNext = false; - public SparseArrayIterator(final E[] entries, final Runnable checkForConcurrentModification) { + /** + * Iterate over the whole array. + * + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, final Sized set) { this.entries = entries; this.pos = entries.length - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } - public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { + /** + * Iterate over {@code entries[0 .. toIndexExclusive)} (in reverse order). + * + * @param entries the backing array (not copied) + * @param toIndexExclusive exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Sized set) { this.entries = entries; this.pos = toIndexExclusive - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** @@ -62,13 +83,11 @@ public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnab public boolean hasNext() { while (-1 < pos) { if (null != entries[pos]) { - hasNext = true; - return true; + return hasNext = true; } pos--; } - hasNext = false; - return false; + return hasNext = false; } /** @@ -79,7 +98,7 @@ public boolean hasNext() { */ @Override public E next() { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (hasNext || hasNext()) { hasNext = false; return entries[pos--]; @@ -95,6 +114,6 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java index 94008b155f1..d8536f56311 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java @@ -22,8 +22,17 @@ package org.apache.jena.mem.pattern; /** - * A pattern for matching triples. - * The pattern is defined by the wildcard positions for the subject, predicate and object. + * Categorizes a triple-match pattern by which of the subject, predicate and + * object slots are concrete and which are wildcards (i.e. {@code Node.ANY} + * or {@code null}). + * + * The eight enum values cover every possible combination. Triple-store + * implementations dispatch on this enum to pick the most efficient lookup + * path for each kind of pattern (e.g. a fully concrete {@link #SUB_PRE_OBJ} + * is answered directly from the triple set, while a partially open pattern + * such as {@link #ANY_PRE_OBJ} is answered through an index intersection). + * + * @see PatternClassifier */ public enum MatchPattern { /** diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java index 32a6ba182a1..e4cf5644eca 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java @@ -25,14 +25,15 @@ import org.apache.jena.graph.Triple; /** - * Classify a triple match into one of the 8 match patterns. + * Utility class that classifies a triple match into one of the eight + * {@link MatchPattern} values. * - * The classification is based on the concrete-ness of the subject, predicate and object. - * A concrete node is one that is not a variable. + * The classification is based on which of the subject, predicate and object + * are concrete (anything that is not a variable / wildcard / + * {@code null}) and which are wildcards. The result is used by triple-store + * implementations to dispatch to the most efficient lookup path. * - * The classification is used to select the most efficient implementation of a triple store. - * - * This is a utility class; there is no need to instantiate it. + * All operations are stateless; this class is not meant to be instantiated. * * @see MatchPattern */ @@ -41,8 +42,16 @@ public class PatternClassifier { private PatternClassifier() { } + /** + * Classify a triple match. + * + * @param tripleMatch the match triple, possibly containing wildcard nodes + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Triple tripleMatch) { - if (tripleMatch.isConcrete()) { + if (tripleMatch.getSubject().isConcrete() + && tripleMatch.getPredicate().isConcrete() + && tripleMatch.getObject().isConcrete()) { return MatchPattern.SUB_PRE_OBJ; } else { if (tripleMatch.getSubject().isConcrete()) { @@ -73,6 +82,15 @@ public static MatchPattern classify(Triple tripleMatch) { } } + /** + * Classify a triple match given as three nodes. + * Any {@code null} or non-concrete node is treated as a wildcard. + * + * @param sm subject node, or {@code null}/wildcard + * @param pm predicate node, or {@code null}/wildcard + * @param om object node, or {@code null}/wildcard + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Node sm, Node pm, Node om) { if (null != sm && sm.isConcrete()) { if (null != pm && pm.isConcrete()) { @@ -103,6 +121,5 @@ public static MatchPattern classify(Node sm, Node pm, Node om) { } } } - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java index 43bbfeeaea8..a5033c22cde 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java @@ -21,52 +21,57 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a contiguous array slice {@code [0, toIndex)}, + * iterating from high index to low. Supports splitting into + * {@link ArraySubSpliterator} children for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySpliterator implements Spliterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public ArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (-1 < --pos) { action.accept(entries[pos]); return true; @@ -79,7 +84,7 @@ public void forEachRemaining(Consumer super E> action) { while (-1 < --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -89,7 +94,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -101,4 +106,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java index 74994708b53..638f2bb0c9e 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java @@ -21,55 +21,61 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a contiguous array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low. Produced by splitting an + * {@link ArraySpliterator} (or another {@link ArraySubSpliterator}); supports + * further recursive splits for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public ArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (fromIndex <= --pos) { action.accept(entries[pos]); return true; @@ -82,7 +88,7 @@ public void forEachRemaining(Consumer super E> action) { while (fromIndex <= --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -93,7 +99,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -105,4 +111,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java deleted file mode 100644 index 704c9642706..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.spliterator; - -import java.util.Spliterator; -import java.util.function.Consumer; - -import org.apache.jena.mem.collection.FastHashSet; - -/** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * This spliterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * This spliterator works in ascending order, starting from the given start up to the specified exclusive index. - * - * This spliterator supports splitting into sub-spliterators. - * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedSpliterator implements Spliterator> { - - private final E[] entries; - private int currentPositionMinusOne; - private final int toIndexExclusive; - private final Runnable checkForConcurrentModification; - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param fromIndexInclusive the index of the first element, inclusive - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int fromIndexInclusive, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.currentPositionMinusOne = fromIndexInclusive-1; // Start at fromIndexInclusive - 1, so that the first call to tryAdvance will increment pos to fromIndexInclusive - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this(entries, 0, toIndexExclusive, checkForConcurrentModification); - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); - } - - - @Override - public boolean tryAdvance(Consumer super FastHashSet.IndexedKey> action) { - this.checkForConcurrentModification.run(); - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - return true; - } - } - return false; - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - } - } - this.checkForConcurrentModification.run(); - } - - @Override - public Spliterator> trySplit() { - final var nextPos = currentPositionMinusOne + 1; - final var remaining = toIndexExclusive - nextPos; - if ( remaining < 2) { - return null; - } - final var mid = nextPos + ( remaining >>> 1); - final var fromIndexInclusive = nextPos; - this.currentPositionMinusOne = mid-1; - return new SparseArrayIndexedSpliterator<>(entries, fromIndexInclusive, mid, checkForConcurrentModification); - } - - @Override - public long estimateSize() { return (long) toIndexExclusive - currentPositionMinusOne; } - - @Override - public int characteristics() { - return DISTINCT | NONNULL | IMMUTABLE; - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java index 6752cc9a1c1..add45739dc2 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java @@ -21,17 +21,24 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a sparse array slice {@code [0, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced for backing arrays such as those of + * {@link org.apache.jena.mem.collection.FastHashBase}, where removed slots + * are represented by {@code null}. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Supports splitting into {@link SparseArraySubSpliterator} children for + * parallel traversal. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it at each + * advance/forEach boundary; throws {@link ConcurrentModificationException} + * if the size has changed. * * @param the type of the array elements */ @@ -39,35 +46,37 @@ public class SparseArraySpliterator implements Spliterator { private final E[] entries; private int pos; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}, skipping nulls. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public SparseArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (-1 < --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -86,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -96,7 +105,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java index 3eb0784326f..d79242ac78c 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java @@ -21,55 +21,62 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a sparse array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced by splitting a {@link SparseArraySpliterator} (or another + * {@link SparseArraySubSpliterator}); supports further recursive splits for + * parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary; + * throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the type of the array elements */ public class SparseArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}, skipping nulls. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public SparseArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (fromIndex <= --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -88,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -99,7 +106,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java index 07ccc9634a9..f0fba805175 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java @@ -32,26 +32,41 @@ import java.util.function.Predicate; /** - * An ArrayBunch implements TripleBunch with a linear search of a short-ish - * array of Triples. The array grows by factor 2. + * Linear-scan implementation of {@link FastTripleBunch} backed by a packed + * {@link Triple} array. Used as long as a bunch stays small; once it grows + * past the configured threshold (see {@link FastTripleStore}) it is replaced + * with a {@link FastHashedTripleBunch}. + * + * The array grows by a factor of two when full. Equality of triples within a + * bunch is delegated to {@link #areEqual(Triple, Triple)}, which subclasses + * specialize to compare only the two nodes that are not already + * implied by the enclosing map's key. This avoids redundant equality checks + * on the shared subject/predicate/object. + * + * Not thread-safe. */ public abstract class FastArrayBunch implements FastTripleBunch { private static final int INITIAL_SIZE = 4; + /** Number of valid entries in {@link #elements}. */ protected int size = 0; + /** Packed array of triples; entries from {@code 0} to {@code size-1} are live. */ protected Triple[] elements; + /** + * Creates an empty bunch with the default initial capacity. + */ protected FastArrayBunch() { elements = new Triple[INITIAL_SIZE]; } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. - * But it will reserve only the space needed to contain them. Growing is still possible. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}; its backing array is sized to fit exactly, + * but can grow further if needed. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -59,7 +74,17 @@ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.size = bunchToCopy.size; } - public abstract boolean areEqual(final Triple a, final Triple b); + /** + * Compare two triples for equality within this bunch. + * + * Subclasses specialize this to skip the already-shared component + * (subject, predicate or object) and compare only the remaining two. + * + * @param a first triple + * @param b second triple + * @return {@code true} if the triples are considered equal in this bunch + */ + protected abstract boolean areEqual(final Triple a, final Triple b); @Override public boolean containsKey(Triple t) { @@ -127,6 +152,7 @@ public boolean tryRemove(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return true; } } @@ -138,6 +164,7 @@ public void removeUnchecked(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return; } } @@ -174,11 +201,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java index b89d3312048..a49d6b54009 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java @@ -25,21 +25,28 @@ import org.apache.jena.mem.collection.FastHashMap; /** - * Map from nodes to triple bunches. + * {@link FastHashMap} specialized to map a {@link Node} to its associated + * {@link FastTripleBunch}. Used by {@link FastTripleStore} to maintain the + * three subject/predicate/object indices. */ public class FastHashedBunchMap extends FastHashMap implements Copyable { + /** + * Creates an empty bunch map with the default initial capacity. + */ public FastHashedBunchMap() { super(); } /** - * Copy constructor. - * The new map will contain all the same nodes as keys of the map to copy, but copies of the bunches as values . + * Copy constructor. The new map has the same node keys as + * {@code mapToCopy}; each value is replaced by a deep copy of the + * corresponding bunch (via {@link FastTripleBunch#copy()}) so that + * mutations of either map cannot affect the other. * - * @param mapToCopy + * @param mapToCopy the source map */ private FastHashedBunchMap(final FastHashedBunchMap mapToCopy) { super(mapToCopy, FastTripleBunch::copy); diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java index 459e78c8181..65c9ab70fbf 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java @@ -25,13 +25,21 @@ import org.apache.jena.mem.collection.JenaSet; /** - * A set of triples - backed by {@link FastHashSet}. + * Hashed implementation of {@link FastTripleBunch} built on top of + * {@link FastHashSet}. Used by {@link FastTripleStore} once a bunch grows + * past the size threshold at which a linear-scan {@link FastArrayBunch} + * stops being faster. */ public class FastHashedTripleBunch extends FastHashSet implements FastTripleBunch { + /** - * Create a new triple bunch from the given set of triples. + * Create a new hashed bunch pre-populated from the given set of triples. + * The initial capacity is chosen at 1.5x the source size, so the new bunch + * fits the existing triples and has some headroom for growth before it + * needs to rehash. * - * @param set the set of triples + * @param set the source set of triples (typically the array bunch being + * promoted) */ public FastHashedTripleBunch(final JenaSet set) { super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth @@ -39,15 +47,18 @@ public FastHashedTripleBunch(final JenaSet set) { } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ private FastHashedTripleBunch(final FastHashedTripleBunch bunchToCopy) { super(bunchToCopy); } + /** + * Creates an empty hashed bunch with the default initial capacity. + */ public FastHashedTripleBunch() { super(); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java index 68f79e72f8a..fe050283188 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java @@ -29,27 +29,39 @@ import java.util.function.Predicate; /** - * A bunch of triples - a stripped-down set with specialized methods. A - * bunch is expected to store triples that share some useful property - * (such as having the same subject or predicate). + * Set-like container for a "bunch" of triples that share some useful + * property - typically they all have the same subject, predicate or object, + * because the bunch is the value of a node-keyed map in a + * {@link FastTripleStore}. + * + * The interface is a stripped-down set with a few extras tuned for the + * triple-store hot path; concrete implementations are + * {@link FastArrayBunch} (linear scan, used while the bunch is small) and + * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a + * threshold). */ public interface FastTripleBunch extends JenaSetHashOptimized, Copyable { /** - * Answer true iff this bunch is implemented as an array. - * This field is used to optimize some operations by avoiding the need for instanceOf tests. + * Answer {@code true} iff this bunch is backed by a flat array (i.e. is + * a {@link FastArrayBunch}). Exposed as an explicit method so callers can + * avoid {@code instanceof} checks on this hot path. * - * @return true iff this bunch is implemented as an arrays + * @return {@code true} if this bunch is array-backed */ boolean isArray(); /** - * This method is used to optimize _PO match operations. - * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches. - * This method is faster if there are many matches and the set is ordered in an unfavorable way. - * _PO matches usually fall into this category. + * Predicate test that scans elements in hash-table order rather than + * dense insertion order. Tuned for {@code _PO} (any-predicate-object) + * matches. + * + * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
+ * Attention: implementations may assume the key is present and may produce + * undefined behavior (including silently corrupting internal state) if it + * is not. Use {@link #tryRemove(Object)} when in doubt. * * @param key the key to remove */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java index d3b8a557be9..03848073f56 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSet.java @@ -21,9 +21,10 @@ package org.apache.jena.mem.collection; /** - * Set interface specialized for the use cases in triple store implementations. + * Set interface specialized for the use cases in triple-store implementations. + * Not thread-safe; does not allow {@code null} elements. * - * @param + * @param the element type of the set */ public interface JenaSet extends JenaMapSetCommon { @@ -31,13 +32,16 @@ public interface JenaSet extends JenaMapSetCommon { * Add the key to the set if it is not already present. * * @param key the key to add - * @return true if the key was added, false if it was already present + * @return {@code true} if the key was added, {@code false} if it was already present */ boolean tryAdd(E key); /** - * Add the key to the set without checking if it is already present. - * Attention: This method must only be used if it is guaranteed that the key is not already present. + * Add the key to the set without checking whether it is already present. + * + * Attention: this method must only be used if the caller has ensured that + * the key is not already in the set; otherwise the set's invariants will + * break (duplicates may be inserted). * * @param key the key to add */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java index 8cc8aad8daf..0e1d032b356 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java @@ -22,17 +22,50 @@ /** - * Extension of {@link JenaSet} that allows to add and remove elements - * with a given hash code. - * This is useful if the hash code is already known. - * Attention: The hash code must be consistent with E::hashCode(). + * Extension of {@link JenaSet} that lets callers supply a precomputed hash + * code. + * + * Attention: any caller-supplied hash code MUST equal {@code E.hashCode()}; + * if it does not, the set will misbehave. + * + * @param the element type of the set */ public interface JenaSetHashOptimized extends JenaSet { + + /** + * Add an element with the given precomputed hash code if it is not + * already present. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + * @return {@code true} if added, {@code false} if already present + */ boolean tryAdd(E key, int hashCode); + /** + * Add an element with the given precomputed hash code without checking + * whether it is already present. The caller MUST ensure the key is absent. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + */ void addUnchecked(E key, int hashCode); + /** + * Try to remove an element with the given precomputed hash code. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + * @return {@code true} if removed, {@code false} if it was not present + */ boolean tryRemove(E key, int hashCode); + /** + * Remove an element assumed to be present, with the given precomputed + * hash code. Behavior is undefined if the element is not in the set. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + */ void removeUnchecked(E key, int hashCode); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java new file mode 100644 index 00000000000..c7c3d2e1ddb --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.mem.collection; + + +/** + * Extension of {@link JenaSetHashOptimized} that exposes index-based access to elements. + * Indices are stable handles to entries (returned by {@link #addAndGetIndex(Object)}) and remain + * valid until the corresponding entry is removed. + * + * @param the element type of the set + */ +public interface JenaSetIndexed extends JenaSetHashOptimized { + + /** + * Add an element and return the index it was stored at. If the element + * is already present, returns a negative value (typically the bitwise + * complement of the existing index). + * + * @param key the element to add + * @return the index of the inserted element, or a negative value if the + * element was already present + */ + int addAndGetIndex(final E key); + + /** + * Returns the element stored at the given index. + * + * @param index the index to read + * @return the element at that index + */ + E getKeyAt(int index); + + /** + * Returns the index of the given element, or a negative value if it is + * not in the set. + * + * @param key the element to look up + * @return the index of {@code key}, or a negative value if absent + */ + int indexOf(E key); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java new file mode 100644 index 00000000000..237740ce8e3 --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.mem.collection; + +/** + * Base interface for sized collections. + * It is typically used to detect concurrent modifications in iterators and spliterators + * by snapshotting the size at construction time and rechecking it at each advance/forEach boundary. + */ +public interface Sized { + + /** + * @return the number of elements in the collection + */ + int size(); +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java index b0ac6e994bb..8cfc8948a25 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java @@ -30,16 +30,27 @@ import java.util.function.Consumer; /** - * Iterator that iterates over the entries of sets which are contained in the given iterator of sets. + * Flat-map style iterator that yields every element of every {@link JenaSet} + * produced by the given parent iterator. Empty inner sets are silently + * skipped. Equivalent in spirit to a one-level {@code flatMap} but tailored + * to the {@link JenaSet} API and to {@link NiceIterator}. * - * @param the type of the elements + * @param the element type of the inner sets */ public class IteratorOfJenaSets extends NiceIterator { - final Iterator extends JenaSet> parentIterator; + /** Source iterator producing the sets to flatten. */ + private final Iterator extends JenaSet> parentIterator; - ExtendedIterator currentIterator; + /** Iterator over the keys of the set currently being consumed. */ + private ExtendedIterator currentIterator; + /** + * Create a flat iterator over the elements of every set produced by + * {@code parentIterator}. + * + * @param parentIterator the source iterator of sets + */ public IteratorOfJenaSets(Iterator extends JenaSet> parentIterator) { this.parentIterator = parentIterator; this.currentIterator = parentIterator.hasNext() diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java deleted file mode 100644 index 37f103eae25..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.iterator; - -import org.apache.jena.mem.collection.FastHashSet; -import org.apache.jena.util.iterator.NiceIterator; - -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.function.Consumer; - -/** - * An iterator over a sparse array, that skips null entries. - * This iterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * The iterator works in ascending order, starting from index 0 up to the specified exclusive index. - * - * This iterator will check for concurrent modifications by invoking a {@link Runnable} - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedIterator extends NiceIterator> implements Iterator> { - - private final E[] entries; - private final Runnable checkForConcurrentModification; - private int pos = 0; - private final int toIndexExclusive; - private boolean hasNext = false; - - public SparseArrayIndexedIterator(final E[] entries, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = entries.length; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - public SparseArrayIndexedIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Returns {@code true} if the iteration has more elements. - * (In other words, returns {@code true} if {@link #next} would - * return an element rather than throwing an exception.) - * - * @return {@code true} if the iteration has more elements - */ - @Override - public boolean hasNext() { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - hasNext = true; - return true; - } - pos++; - } - hasNext = false; - return false; - } - - /** - * Returns the next element in the iteration. - * - * @return the next element in the iteration - * @throws NoSuchElementException if the iteration has no more elements - */ - @Override - public FastHashSet.IndexedKey next() { - this.checkForConcurrentModification.run(); - if (hasNext || hasNext()) { - hasNext = false; - return new FastHashSet.IndexedKey<>(pos, entries[pos++]); - } - throw new NoSuchElementException(); - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - action.accept(new FastHashSet.IndexedKey<>(pos, entries[pos])); - } - pos++; - } - this.checkForConcurrentModification.run(); - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java index 936476a80ff..e0b79cd1ff6 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java @@ -21,34 +21,55 @@ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.Sized; import org.apache.jena.util.iterator.NiceIterator; -import java.util.Iterator; +import java.util.ConcurrentModificationException; import java.util.NoSuchElementException; import java.util.function.Consumer; /** - * An iterator over a sparse array, that skips null entries. + * Iterator over a sparse array, walking from high index to low and skipping + * {@code null} entries. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it on each call to + * {@link #next()} / {@link #forEachRemaining(Consumer)}; throws + * {@link ConcurrentModificationException} if the size has changed. * * @param the type of the array elements */ -public class SparseArrayIterator extends NiceIterator implements Iterator { +public class SparseArrayIterator extends NiceIterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; private boolean hasNext = false; - public SparseArrayIterator(final E[] entries, final Runnable checkForConcurrentModification) { + /** + * Iterate over the whole array. + * + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, final Sized set) { this.entries = entries; this.pos = entries.length - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } - public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { + /** + * Iterate over {@code entries[0 .. toIndexExclusive)} (in reverse order). + * + * @param entries the backing array (not copied) + * @param toIndexExclusive exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Sized set) { this.entries = entries; this.pos = toIndexExclusive - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** @@ -62,13 +83,11 @@ public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnab public boolean hasNext() { while (-1 < pos) { if (null != entries[pos]) { - hasNext = true; - return true; + return hasNext = true; } pos--; } - hasNext = false; - return false; + return hasNext = false; } /** @@ -79,7 +98,7 @@ public boolean hasNext() { */ @Override public E next() { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (hasNext || hasNext()) { hasNext = false; return entries[pos--]; @@ -95,6 +114,6 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java index 94008b155f1..d8536f56311 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java @@ -22,8 +22,17 @@ package org.apache.jena.mem.pattern; /** - * A pattern for matching triples. - * The pattern is defined by the wildcard positions for the subject, predicate and object. + * Categorizes a triple-match pattern by which of the subject, predicate and + * object slots are concrete and which are wildcards (i.e. {@code Node.ANY} + * or {@code null}). + * + * The eight enum values cover every possible combination. Triple-store + * implementations dispatch on this enum to pick the most efficient lookup + * path for each kind of pattern (e.g. a fully concrete {@link #SUB_PRE_OBJ} + * is answered directly from the triple set, while a partially open pattern + * such as {@link #ANY_PRE_OBJ} is answered through an index intersection). + * + * @see PatternClassifier */ public enum MatchPattern { /** diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java index 32a6ba182a1..e4cf5644eca 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java @@ -25,14 +25,15 @@ import org.apache.jena.graph.Triple; /** - * Classify a triple match into one of the 8 match patterns. + * Utility class that classifies a triple match into one of the eight + * {@link MatchPattern} values. * - * The classification is based on the concrete-ness of the subject, predicate and object. - * A concrete node is one that is not a variable. + * The classification is based on which of the subject, predicate and object + * are concrete (anything that is not a variable / wildcard / + * {@code null}) and which are wildcards. The result is used by triple-store + * implementations to dispatch to the most efficient lookup path. * - * The classification is used to select the most efficient implementation of a triple store. - * - * This is a utility class; there is no need to instantiate it. + * All operations are stateless; this class is not meant to be instantiated. * * @see MatchPattern */ @@ -41,8 +42,16 @@ public class PatternClassifier { private PatternClassifier() { } + /** + * Classify a triple match. + * + * @param tripleMatch the match triple, possibly containing wildcard nodes + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Triple tripleMatch) { - if (tripleMatch.isConcrete()) { + if (tripleMatch.getSubject().isConcrete() + && tripleMatch.getPredicate().isConcrete() + && tripleMatch.getObject().isConcrete()) { return MatchPattern.SUB_PRE_OBJ; } else { if (tripleMatch.getSubject().isConcrete()) { @@ -73,6 +82,15 @@ public static MatchPattern classify(Triple tripleMatch) { } } + /** + * Classify a triple match given as three nodes. + * Any {@code null} or non-concrete node is treated as a wildcard. + * + * @param sm subject node, or {@code null}/wildcard + * @param pm predicate node, or {@code null}/wildcard + * @param om object node, or {@code null}/wildcard + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Node sm, Node pm, Node om) { if (null != sm && sm.isConcrete()) { if (null != pm && pm.isConcrete()) { @@ -103,6 +121,5 @@ public static MatchPattern classify(Node sm, Node pm, Node om) { } } } - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java index 43bbfeeaea8..a5033c22cde 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java @@ -21,52 +21,57 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a contiguous array slice {@code [0, toIndex)}, + * iterating from high index to low. Supports splitting into + * {@link ArraySubSpliterator} children for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySpliterator implements Spliterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public ArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (-1 < --pos) { action.accept(entries[pos]); return true; @@ -79,7 +84,7 @@ public void forEachRemaining(Consumer super E> action) { while (-1 < --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -89,7 +94,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -101,4 +106,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java index 74994708b53..638f2bb0c9e 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java @@ -21,55 +21,61 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a contiguous array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low. Produced by splitting an + * {@link ArraySpliterator} (or another {@link ArraySubSpliterator}); supports + * further recursive splits for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public ArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (fromIndex <= --pos) { action.accept(entries[pos]); return true; @@ -82,7 +88,7 @@ public void forEachRemaining(Consumer super E> action) { while (fromIndex <= --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -93,7 +99,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -105,4 +111,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java deleted file mode 100644 index 704c9642706..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.spliterator; - -import java.util.Spliterator; -import java.util.function.Consumer; - -import org.apache.jena.mem.collection.FastHashSet; - -/** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * This spliterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * This spliterator works in ascending order, starting from the given start up to the specified exclusive index. - * - * This spliterator supports splitting into sub-spliterators. - * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedSpliterator implements Spliterator> { - - private final E[] entries; - private int currentPositionMinusOne; - private final int toIndexExclusive; - private final Runnable checkForConcurrentModification; - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param fromIndexInclusive the index of the first element, inclusive - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int fromIndexInclusive, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.currentPositionMinusOne = fromIndexInclusive-1; // Start at fromIndexInclusive - 1, so that the first call to tryAdvance will increment pos to fromIndexInclusive - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this(entries, 0, toIndexExclusive, checkForConcurrentModification); - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); - } - - - @Override - public boolean tryAdvance(Consumer super FastHashSet.IndexedKey> action) { - this.checkForConcurrentModification.run(); - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - return true; - } - } - return false; - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - } - } - this.checkForConcurrentModification.run(); - } - - @Override - public Spliterator> trySplit() { - final var nextPos = currentPositionMinusOne + 1; - final var remaining = toIndexExclusive - nextPos; - if ( remaining < 2) { - return null; - } - final var mid = nextPos + ( remaining >>> 1); - final var fromIndexInclusive = nextPos; - this.currentPositionMinusOne = mid-1; - return new SparseArrayIndexedSpliterator<>(entries, fromIndexInclusive, mid, checkForConcurrentModification); - } - - @Override - public long estimateSize() { return (long) toIndexExclusive - currentPositionMinusOne; } - - @Override - public int characteristics() { - return DISTINCT | NONNULL | IMMUTABLE; - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java index 6752cc9a1c1..add45739dc2 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java @@ -21,17 +21,24 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a sparse array slice {@code [0, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced for backing arrays such as those of + * {@link org.apache.jena.mem.collection.FastHashBase}, where removed slots + * are represented by {@code null}. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Supports splitting into {@link SparseArraySubSpliterator} children for + * parallel traversal. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it at each + * advance/forEach boundary; throws {@link ConcurrentModificationException} + * if the size has changed. * * @param the type of the array elements */ @@ -39,35 +46,37 @@ public class SparseArraySpliterator implements Spliterator { private final E[] entries; private int pos; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}, skipping nulls. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public SparseArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (-1 < --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -86,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -96,7 +105,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java index 3eb0784326f..d79242ac78c 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java @@ -21,55 +21,62 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a sparse array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced by splitting a {@link SparseArraySpliterator} (or another + * {@link SparseArraySubSpliterator}); supports further recursive splits for + * parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary; + * throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the type of the array elements */ public class SparseArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}, skipping nulls. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public SparseArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (fromIndex <= --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -88,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -99,7 +106,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java index 07ccc9634a9..f0fba805175 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java @@ -32,26 +32,41 @@ import java.util.function.Predicate; /** - * An ArrayBunch implements TripleBunch with a linear search of a short-ish - * array of Triples. The array grows by factor 2. + * Linear-scan implementation of {@link FastTripleBunch} backed by a packed + * {@link Triple} array. Used as long as a bunch stays small; once it grows + * past the configured threshold (see {@link FastTripleStore}) it is replaced + * with a {@link FastHashedTripleBunch}. + * + * The array grows by a factor of two when full. Equality of triples within a + * bunch is delegated to {@link #areEqual(Triple, Triple)}, which subclasses + * specialize to compare only the two nodes that are not already + * implied by the enclosing map's key. This avoids redundant equality checks + * on the shared subject/predicate/object. + * + * Not thread-safe. */ public abstract class FastArrayBunch implements FastTripleBunch { private static final int INITIAL_SIZE = 4; + /** Number of valid entries in {@link #elements}. */ protected int size = 0; + /** Packed array of triples; entries from {@code 0} to {@code size-1} are live. */ protected Triple[] elements; + /** + * Creates an empty bunch with the default initial capacity. + */ protected FastArrayBunch() { elements = new Triple[INITIAL_SIZE]; } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. - * But it will reserve only the space needed to contain them. Growing is still possible. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}; its backing array is sized to fit exactly, + * but can grow further if needed. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -59,7 +74,17 @@ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.size = bunchToCopy.size; } - public abstract boolean areEqual(final Triple a, final Triple b); + /** + * Compare two triples for equality within this bunch. + * + * Subclasses specialize this to skip the already-shared component + * (subject, predicate or object) and compare only the remaining two. + * + * @param a first triple + * @param b second triple + * @return {@code true} if the triples are considered equal in this bunch + */ + protected abstract boolean areEqual(final Triple a, final Triple b); @Override public boolean containsKey(Triple t) { @@ -127,6 +152,7 @@ public boolean tryRemove(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return true; } } @@ -138,6 +164,7 @@ public void removeUnchecked(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return; } } @@ -174,11 +201,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java index b89d3312048..a49d6b54009 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java @@ -25,21 +25,28 @@ import org.apache.jena.mem.collection.FastHashMap; /** - * Map from nodes to triple bunches. + * {@link FastHashMap} specialized to map a {@link Node} to its associated + * {@link FastTripleBunch}. Used by {@link FastTripleStore} to maintain the + * three subject/predicate/object indices. */ public class FastHashedBunchMap extends FastHashMap implements Copyable { + /** + * Creates an empty bunch map with the default initial capacity. + */ public FastHashedBunchMap() { super(); } /** - * Copy constructor. - * The new map will contain all the same nodes as keys of the map to copy, but copies of the bunches as values . + * Copy constructor. The new map has the same node keys as + * {@code mapToCopy}; each value is replaced by a deep copy of the + * corresponding bunch (via {@link FastTripleBunch#copy()}) so that + * mutations of either map cannot affect the other. * - * @param mapToCopy + * @param mapToCopy the source map */ private FastHashedBunchMap(final FastHashedBunchMap mapToCopy) { super(mapToCopy, FastTripleBunch::copy); diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java index 459e78c8181..65c9ab70fbf 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java @@ -25,13 +25,21 @@ import org.apache.jena.mem.collection.JenaSet; /** - * A set of triples - backed by {@link FastHashSet}. + * Hashed implementation of {@link FastTripleBunch} built on top of + * {@link FastHashSet}. Used by {@link FastTripleStore} once a bunch grows + * past the size threshold at which a linear-scan {@link FastArrayBunch} + * stops being faster. */ public class FastHashedTripleBunch extends FastHashSet implements FastTripleBunch { + /** - * Create a new triple bunch from the given set of triples. + * Create a new hashed bunch pre-populated from the given set of triples. + * The initial capacity is chosen at 1.5x the source size, so the new bunch + * fits the existing triples and has some headroom for growth before it + * needs to rehash. * - * @param set the set of triples + * @param set the source set of triples (typically the array bunch being + * promoted) */ public FastHashedTripleBunch(final JenaSet set) { super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth @@ -39,15 +47,18 @@ public FastHashedTripleBunch(final JenaSet set) { } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ private FastHashedTripleBunch(final FastHashedTripleBunch bunchToCopy) { super(bunchToCopy); } + /** + * Creates an empty hashed bunch with the default initial capacity. + */ public FastHashedTripleBunch() { super(); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java index 68f79e72f8a..fe050283188 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java @@ -29,27 +29,39 @@ import java.util.function.Predicate; /** - * A bunch of triples - a stripped-down set with specialized methods. A - * bunch is expected to store triples that share some useful property - * (such as having the same subject or predicate). + * Set-like container for a "bunch" of triples that share some useful + * property - typically they all have the same subject, predicate or object, + * because the bunch is the value of a node-keyed map in a + * {@link FastTripleStore}. + * + * The interface is a stripped-down set with a few extras tuned for the + * triple-store hot path; concrete implementations are + * {@link FastArrayBunch} (linear scan, used while the bunch is small) and + * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a + * threshold). */ public interface FastTripleBunch extends JenaSetHashOptimized, Copyable { /** - * Answer true iff this bunch is implemented as an array. - * This field is used to optimize some operations by avoiding the need for instanceOf tests. + * Answer {@code true} iff this bunch is backed by a flat array (i.e. is + * a {@link FastArrayBunch}). Exposed as an explicit method so callers can + * avoid {@code instanceof} checks on this hot path. * - * @return true iff this bunch is implemented as an arrays + * @return {@code true} if this bunch is array-backed */ boolean isArray(); /** - * This method is used to optimize _PO match operations. - * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches. - * This method is faster if there are many matches and the set is ordered in an unfavorable way. - * _PO matches usually fall into this category. + * Predicate test that scans elements in hash-table order rather than + * dense insertion order. Tuned for {@code _PO} (any-predicate-object) + * matches. + * + * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
+ * Attention: this method must only be used if the caller has ensured that + * the key is not already in the set; otherwise the set's invariants will + * break (duplicates may be inserted). * * @param key the key to add */ diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java index 8cc8aad8daf..0e1d032b356 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetHashOptimized.java @@ -22,17 +22,50 @@ /** - * Extension of {@link JenaSet} that allows to add and remove elements - * with a given hash code. - * This is useful if the hash code is already known. - * Attention: The hash code must be consistent with E::hashCode(). + * Extension of {@link JenaSet} that lets callers supply a precomputed hash + * code. + *
+ * Attention: any caller-supplied hash code MUST equal {@code E.hashCode()}; + * if it does not, the set will misbehave. + * + * @param the element type of the set */ public interface JenaSetHashOptimized extends JenaSet { + + /** + * Add an element with the given precomputed hash code if it is not + * already present. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + * @return {@code true} if added, {@code false} if already present + */ boolean tryAdd(E key, int hashCode); + /** + * Add an element with the given precomputed hash code without checking + * whether it is already present. The caller MUST ensure the key is absent. + * + * @param key the element to add + * @param hashCode {@code key.hashCode()} + */ void addUnchecked(E key, int hashCode); + /** + * Try to remove an element with the given precomputed hash code. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + * @return {@code true} if removed, {@code false} if it was not present + */ boolean tryRemove(E key, int hashCode); + /** + * Remove an element assumed to be present, with the given precomputed + * hash code. Behavior is undefined if the element is not in the set. + * + * @param key the element to remove + * @param hashCode {@code key.hashCode()} + */ void removeUnchecked(E key, int hashCode); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java new file mode 100644 index 00000000000..c7c3d2e1ddb --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/JenaSetIndexed.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.mem.collection; + + +/** + * Extension of {@link JenaSetHashOptimized} that exposes index-based access to elements. + * Indices are stable handles to entries (returned by {@link #addAndGetIndex(Object)}) and remain + * valid until the corresponding entry is removed. + * + * @param the element type of the set + */ +public interface JenaSetIndexed extends JenaSetHashOptimized { + + /** + * Add an element and return the index it was stored at. If the element + * is already present, returns a negative value (typically the bitwise + * complement of the existing index). + * + * @param key the element to add + * @return the index of the inserted element, or a negative value if the + * element was already present + */ + int addAndGetIndex(final E key); + + /** + * Returns the element stored at the given index. + * + * @param index the index to read + * @return the element at that index + */ + E getKeyAt(int index); + + /** + * Returns the index of the given element, or a negative value if it is + * not in the set. + * + * @param key the element to look up + * @return the index of {@code key}, or a negative value if absent + */ + int indexOf(E key); +} diff --git a/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java new file mode 100644 index 00000000000..237740ce8e3 --- /dev/null +++ b/jena-core/src/main/java/org/apache/jena/mem/collection/Sized.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.mem.collection; + +/** + * Base interface for sized collections. + * It is typically used to detect concurrent modifications in iterators and spliterators + * by snapshotting the size at construction time and rechecking it at each advance/forEach boundary. + */ +public interface Sized { + + /** + * @return the number of elements in the collection + */ + int size(); +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java index b0ac6e994bb..8cfc8948a25 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/IteratorOfJenaSets.java @@ -30,16 +30,27 @@ import java.util.function.Consumer; /** - * Iterator that iterates over the entries of sets which are contained in the given iterator of sets. + * Flat-map style iterator that yields every element of every {@link JenaSet} + * produced by the given parent iterator. Empty inner sets are silently + * skipped. Equivalent in spirit to a one-level {@code flatMap} but tailored + * to the {@link JenaSet} API and to {@link NiceIterator}. * - * @param the type of the elements + * @param the element type of the inner sets */ public class IteratorOfJenaSets extends NiceIterator { - final Iterator extends JenaSet> parentIterator; + /** Source iterator producing the sets to flatten. */ + private final Iterator extends JenaSet> parentIterator; - ExtendedIterator currentIterator; + /** Iterator over the keys of the set currently being consumed. */ + private ExtendedIterator currentIterator; + /** + * Create a flat iterator over the elements of every set produced by + * {@code parentIterator}. + * + * @param parentIterator the source iterator of sets + */ public IteratorOfJenaSets(Iterator extends JenaSet> parentIterator) { this.parentIterator = parentIterator; this.currentIterator = parentIterator.hasNext() diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java deleted file mode 100644 index 37f103eae25..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIndexedIterator.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.iterator; - -import org.apache.jena.mem.collection.FastHashSet; -import org.apache.jena.util.iterator.NiceIterator; - -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.function.Consumer; - -/** - * An iterator over a sparse array, that skips null entries. - * This iterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * The iterator works in ascending order, starting from index 0 up to the specified exclusive index. - * - * This iterator will check for concurrent modifications by invoking a {@link Runnable} - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedIterator extends NiceIterator> implements Iterator> { - - private final E[] entries; - private final Runnable checkForConcurrentModification; - private int pos = 0; - private final int toIndexExclusive; - private boolean hasNext = false; - - public SparseArrayIndexedIterator(final E[] entries, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = entries.length; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - public SparseArrayIndexedIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Returns {@code true} if the iteration has more elements. - * (In other words, returns {@code true} if {@link #next} would - * return an element rather than throwing an exception.) - * - * @return {@code true} if the iteration has more elements - */ - @Override - public boolean hasNext() { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - hasNext = true; - return true; - } - pos++; - } - hasNext = false; - return false; - } - - /** - * Returns the next element in the iteration. - * - * @return the next element in the iteration - * @throws NoSuchElementException if the iteration has no more elements - */ - @Override - public FastHashSet.IndexedKey next() { - this.checkForConcurrentModification.run(); - if (hasNext || hasNext()) { - hasNext = false; - return new FastHashSet.IndexedKey<>(pos, entries[pos++]); - } - throw new NoSuchElementException(); - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > pos) { - if (null != entries[pos]) { - action.accept(new FastHashSet.IndexedKey<>(pos, entries[pos])); - } - pos++; - } - this.checkForConcurrentModification.run(); - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java index 936476a80ff..e0b79cd1ff6 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/iterator/SparseArrayIterator.java @@ -21,34 +21,55 @@ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.Sized; import org.apache.jena.util.iterator.NiceIterator; -import java.util.Iterator; +import java.util.ConcurrentModificationException; import java.util.NoSuchElementException; import java.util.function.Consumer; /** - * An iterator over a sparse array, that skips null entries. + * Iterator over a sparse array, walking from high index to low and skipping + * {@code null} entries. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it on each call to + * {@link #next()} / {@link #forEachRemaining(Consumer)}; throws + * {@link ConcurrentModificationException} if the size has changed. * * @param the type of the array elements */ -public class SparseArrayIterator extends NiceIterator implements Iterator { +public class SparseArrayIterator extends NiceIterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; private boolean hasNext = false; - public SparseArrayIterator(final E[] entries, final Runnable checkForConcurrentModification) { + /** + * Iterate over the whole array. + * + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, final Sized set) { this.entries = entries; this.pos = entries.length - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } - public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) { + /** + * Iterate over {@code entries[0 .. toIndexExclusive)} (in reverse order). + * + * @param entries the backing array (not copied) + * @param toIndexExclusive exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications + */ + public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Sized set) { this.entries = entries; this.pos = toIndexExclusive - 1; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** @@ -62,13 +83,11 @@ public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnab public boolean hasNext() { while (-1 < pos) { if (null != entries[pos]) { - hasNext = true; - return true; + return hasNext = true; } pos--; } - hasNext = false; - return false; + return hasNext = false; } /** @@ -79,7 +98,7 @@ public boolean hasNext() { */ @Override public E next() { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (hasNext || hasNext()) { hasNext = false; return entries[pos--]; @@ -95,6 +114,6 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java index 94008b155f1..d8536f56311 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/MatchPattern.java @@ -22,8 +22,17 @@ package org.apache.jena.mem.pattern; /** - * A pattern for matching triples. - * The pattern is defined by the wildcard positions for the subject, predicate and object. + * Categorizes a triple-match pattern by which of the subject, predicate and + * object slots are concrete and which are wildcards (i.e. {@code Node.ANY} + * or {@code null}). + * + * The eight enum values cover every possible combination. Triple-store + * implementations dispatch on this enum to pick the most efficient lookup + * path for each kind of pattern (e.g. a fully concrete {@link #SUB_PRE_OBJ} + * is answered directly from the triple set, while a partially open pattern + * such as {@link #ANY_PRE_OBJ} is answered through an index intersection). + * + * @see PatternClassifier */ public enum MatchPattern { /** diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java index 32a6ba182a1..e4cf5644eca 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java @@ -25,14 +25,15 @@ import org.apache.jena.graph.Triple; /** - * Classify a triple match into one of the 8 match patterns. + * Utility class that classifies a triple match into one of the eight + * {@link MatchPattern} values. * - * The classification is based on the concrete-ness of the subject, predicate and object. - * A concrete node is one that is not a variable. + * The classification is based on which of the subject, predicate and object + * are concrete (anything that is not a variable / wildcard / + * {@code null}) and which are wildcards. The result is used by triple-store + * implementations to dispatch to the most efficient lookup path. * - * The classification is used to select the most efficient implementation of a triple store. - * - * This is a utility class; there is no need to instantiate it. + * All operations are stateless; this class is not meant to be instantiated. * * @see MatchPattern */ @@ -41,8 +42,16 @@ public class PatternClassifier { private PatternClassifier() { } + /** + * Classify a triple match. + * + * @param tripleMatch the match triple, possibly containing wildcard nodes + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Triple tripleMatch) { - if (tripleMatch.isConcrete()) { + if (tripleMatch.getSubject().isConcrete() + && tripleMatch.getPredicate().isConcrete() + && tripleMatch.getObject().isConcrete()) { return MatchPattern.SUB_PRE_OBJ; } else { if (tripleMatch.getSubject().isConcrete()) { @@ -73,6 +82,15 @@ public static MatchPattern classify(Triple tripleMatch) { } } + /** + * Classify a triple match given as three nodes. + * Any {@code null} or non-concrete node is treated as a wildcard. + * + * @param sm subject node, or {@code null}/wildcard + * @param pm predicate node, or {@code null}/wildcard + * @param om object node, or {@code null}/wildcard + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Node sm, Node pm, Node om) { if (null != sm && sm.isConcrete()) { if (null != pm && pm.isConcrete()) { @@ -103,6 +121,5 @@ public static MatchPattern classify(Node sm, Node pm, Node om) { } } } - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java index 43bbfeeaea8..a5033c22cde 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java @@ -21,52 +21,57 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a contiguous array slice {@code [0, toIndex)}, + * iterating from high index to low. Supports splitting into + * {@link ArraySubSpliterator} children for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySpliterator implements Spliterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public ArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (-1 < --pos) { action.accept(entries[pos]); return true; @@ -79,7 +84,7 @@ public void forEachRemaining(Consumer super E> action) { while (-1 < --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -89,7 +94,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -101,4 +106,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java index 74994708b53..638f2bb0c9e 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java @@ -21,55 +21,61 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a contiguous array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low. Produced by splitting an + * {@link ArraySpliterator} (or another {@link ArraySubSpliterator}); supports + * further recursive splits for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public ArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (fromIndex <= --pos) { action.accept(entries[pos]); return true; @@ -82,7 +88,7 @@ public void forEachRemaining(Consumer super E> action) { while (fromIndex <= --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -93,7 +99,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -105,4 +111,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java deleted file mode 100644 index 704c9642706..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.spliterator; - -import java.util.Spliterator; -import java.util.function.Consumer; - -import org.apache.jena.mem.collection.FastHashSet; - -/** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * This spliterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * This spliterator works in ascending order, starting from the given start up to the specified exclusive index. - * - * This spliterator supports splitting into sub-spliterators. - * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedSpliterator implements Spliterator> { - - private final E[] entries; - private int currentPositionMinusOne; - private final int toIndexExclusive; - private final Runnable checkForConcurrentModification; - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param fromIndexInclusive the index of the first element, inclusive - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int fromIndexInclusive, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.currentPositionMinusOne = fromIndexInclusive-1; // Start at fromIndexInclusive - 1, so that the first call to tryAdvance will increment pos to fromIndexInclusive - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this(entries, 0, toIndexExclusive, checkForConcurrentModification); - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); - } - - - @Override - public boolean tryAdvance(Consumer super FastHashSet.IndexedKey> action) { - this.checkForConcurrentModification.run(); - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - return true; - } - } - return false; - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - } - } - this.checkForConcurrentModification.run(); - } - - @Override - public Spliterator> trySplit() { - final var nextPos = currentPositionMinusOne + 1; - final var remaining = toIndexExclusive - nextPos; - if ( remaining < 2) { - return null; - } - final var mid = nextPos + ( remaining >>> 1); - final var fromIndexInclusive = nextPos; - this.currentPositionMinusOne = mid-1; - return new SparseArrayIndexedSpliterator<>(entries, fromIndexInclusive, mid, checkForConcurrentModification); - } - - @Override - public long estimateSize() { return (long) toIndexExclusive - currentPositionMinusOne; } - - @Override - public int characteristics() { - return DISTINCT | NONNULL | IMMUTABLE; - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java index 6752cc9a1c1..add45739dc2 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java @@ -21,17 +21,24 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a sparse array slice {@code [0, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced for backing arrays such as those of + * {@link org.apache.jena.mem.collection.FastHashBase}, where removed slots + * are represented by {@code null}. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Supports splitting into {@link SparseArraySubSpliterator} children for + * parallel traversal. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it at each + * advance/forEach boundary; throws {@link ConcurrentModificationException} + * if the size has changed. * * @param the type of the array elements */ @@ -39,35 +46,37 @@ public class SparseArraySpliterator implements Spliterator { private final E[] entries; private int pos; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}, skipping nulls. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public SparseArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (-1 < --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -86,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -96,7 +105,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java index 3eb0784326f..d79242ac78c 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java @@ -21,55 +21,62 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a sparse array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced by splitting a {@link SparseArraySpliterator} (or another + * {@link SparseArraySubSpliterator}); supports further recursive splits for + * parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary; + * throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the type of the array elements */ public class SparseArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}, skipping nulls. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public SparseArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (fromIndex <= --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -88,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -99,7 +106,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java index 07ccc9634a9..f0fba805175 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java @@ -32,26 +32,41 @@ import java.util.function.Predicate; /** - * An ArrayBunch implements TripleBunch with a linear search of a short-ish - * array of Triples. The array grows by factor 2. + * Linear-scan implementation of {@link FastTripleBunch} backed by a packed + * {@link Triple} array. Used as long as a bunch stays small; once it grows + * past the configured threshold (see {@link FastTripleStore}) it is replaced + * with a {@link FastHashedTripleBunch}. + * + * The array grows by a factor of two when full. Equality of triples within a + * bunch is delegated to {@link #areEqual(Triple, Triple)}, which subclasses + * specialize to compare only the two nodes that are not already + * implied by the enclosing map's key. This avoids redundant equality checks + * on the shared subject/predicate/object. + * + * Not thread-safe. */ public abstract class FastArrayBunch implements FastTripleBunch { private static final int INITIAL_SIZE = 4; + /** Number of valid entries in {@link #elements}. */ protected int size = 0; + /** Packed array of triples; entries from {@code 0} to {@code size-1} are live. */ protected Triple[] elements; + /** + * Creates an empty bunch with the default initial capacity. + */ protected FastArrayBunch() { elements = new Triple[INITIAL_SIZE]; } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. - * But it will reserve only the space needed to contain them. Growing is still possible. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}; its backing array is sized to fit exactly, + * but can grow further if needed. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -59,7 +74,17 @@ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.size = bunchToCopy.size; } - public abstract boolean areEqual(final Triple a, final Triple b); + /** + * Compare two triples for equality within this bunch. + * + * Subclasses specialize this to skip the already-shared component + * (subject, predicate or object) and compare only the remaining two. + * + * @param a first triple + * @param b second triple + * @return {@code true} if the triples are considered equal in this bunch + */ + protected abstract boolean areEqual(final Triple a, final Triple b); @Override public boolean containsKey(Triple t) { @@ -127,6 +152,7 @@ public boolean tryRemove(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return true; } } @@ -138,6 +164,7 @@ public void removeUnchecked(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return; } } @@ -174,11 +201,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java index b89d3312048..a49d6b54009 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java @@ -25,21 +25,28 @@ import org.apache.jena.mem.collection.FastHashMap; /** - * Map from nodes to triple bunches. + * {@link FastHashMap} specialized to map a {@link Node} to its associated + * {@link FastTripleBunch}. Used by {@link FastTripleStore} to maintain the + * three subject/predicate/object indices. */ public class FastHashedBunchMap extends FastHashMap implements Copyable { + /** + * Creates an empty bunch map with the default initial capacity. + */ public FastHashedBunchMap() { super(); } /** - * Copy constructor. - * The new map will contain all the same nodes as keys of the map to copy, but copies of the bunches as values . + * Copy constructor. The new map has the same node keys as + * {@code mapToCopy}; each value is replaced by a deep copy of the + * corresponding bunch (via {@link FastTripleBunch#copy()}) so that + * mutations of either map cannot affect the other. * - * @param mapToCopy + * @param mapToCopy the source map */ private FastHashedBunchMap(final FastHashedBunchMap mapToCopy) { super(mapToCopy, FastTripleBunch::copy); diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java index 459e78c8181..65c9ab70fbf 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java @@ -25,13 +25,21 @@ import org.apache.jena.mem.collection.JenaSet; /** - * A set of triples - backed by {@link FastHashSet}. + * Hashed implementation of {@link FastTripleBunch} built on top of + * {@link FastHashSet}. Used by {@link FastTripleStore} once a bunch grows + * past the size threshold at which a linear-scan {@link FastArrayBunch} + * stops being faster. */ public class FastHashedTripleBunch extends FastHashSet implements FastTripleBunch { + /** - * Create a new triple bunch from the given set of triples. + * Create a new hashed bunch pre-populated from the given set of triples. + * The initial capacity is chosen at 1.5x the source size, so the new bunch + * fits the existing triples and has some headroom for growth before it + * needs to rehash. * - * @param set the set of triples + * @param set the source set of triples (typically the array bunch being + * promoted) */ public FastHashedTripleBunch(final JenaSet set) { super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth @@ -39,15 +47,18 @@ public FastHashedTripleBunch(final JenaSet set) { } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ private FastHashedTripleBunch(final FastHashedTripleBunch bunchToCopy) { super(bunchToCopy); } + /** + * Creates an empty hashed bunch with the default initial capacity. + */ public FastHashedTripleBunch() { super(); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java index 68f79e72f8a..fe050283188 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java @@ -29,27 +29,39 @@ import java.util.function.Predicate; /** - * A bunch of triples - a stripped-down set with specialized methods. A - * bunch is expected to store triples that share some useful property - * (such as having the same subject or predicate). + * Set-like container for a "bunch" of triples that share some useful + * property - typically they all have the same subject, predicate or object, + * because the bunch is the value of a node-keyed map in a + * {@link FastTripleStore}. + * + * The interface is a stripped-down set with a few extras tuned for the + * triple-store hot path; concrete implementations are + * {@link FastArrayBunch} (linear scan, used while the bunch is small) and + * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a + * threshold). */ public interface FastTripleBunch extends JenaSetHashOptimized, Copyable { /** - * Answer true iff this bunch is implemented as an array. - * This field is used to optimize some operations by avoiding the need for instanceOf tests. + * Answer {@code true} iff this bunch is backed by a flat array (i.e. is + * a {@link FastArrayBunch}). Exposed as an explicit method so callers can + * avoid {@code instanceof} checks on this hot path. * - * @return true iff this bunch is implemented as an arrays + * @return {@code true} if this bunch is array-backed */ boolean isArray(); /** - * This method is used to optimize _PO match operations. - * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches. - * This method is faster if there are many matches and the set is ordered in an unfavorable way. - * _PO matches usually fall into this category. + * Predicate test that scans elements in hash-table order rather than + * dense insertion order. Tuned for {@code _PO} (any-predicate-object) + * matches. + * + * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
+ * The eight enum values cover every possible combination. Triple-store + * implementations dispatch on this enum to pick the most efficient lookup + * path for each kind of pattern (e.g. a fully concrete {@link #SUB_PRE_OBJ} + * is answered directly from the triple set, while a partially open pattern + * such as {@link #ANY_PRE_OBJ} is answered through an index intersection). + * + * @see PatternClassifier */ public enum MatchPattern { /** diff --git a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java index 32a6ba182a1..e4cf5644eca 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java +++ b/jena-core/src/main/java/org/apache/jena/mem/pattern/PatternClassifier.java @@ -25,14 +25,15 @@ import org.apache.jena.graph.Triple; /** - * Classify a triple match into one of the 8 match patterns. + * Utility class that classifies a triple match into one of the eight + * {@link MatchPattern} values. *
- * The classification is based on the concrete-ness of the subject, predicate and object. - * A concrete node is one that is not a variable. + * The classification is based on which of the subject, predicate and object + * are concrete (anything that is not a variable / wildcard / + * {@code null}) and which are wildcards. The result is used by triple-store + * implementations to dispatch to the most efficient lookup path. *
- * The classification is used to select the most efficient implementation of a triple store. - *
- * This is a utility class; there is no need to instantiate it. + * All operations are stateless; this class is not meant to be instantiated. * * @see MatchPattern */ @@ -41,8 +42,16 @@ public class PatternClassifier { private PatternClassifier() { } + /** + * Classify a triple match. + * + * @param tripleMatch the match triple, possibly containing wildcard nodes + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Triple tripleMatch) { - if (tripleMatch.isConcrete()) { + if (tripleMatch.getSubject().isConcrete() + && tripleMatch.getPredicate().isConcrete() + && tripleMatch.getObject().isConcrete()) { return MatchPattern.SUB_PRE_OBJ; } else { if (tripleMatch.getSubject().isConcrete()) { @@ -73,6 +82,15 @@ public static MatchPattern classify(Triple tripleMatch) { } } + /** + * Classify a triple match given as three nodes. + * Any {@code null} or non-concrete node is treated as a wildcard. + * + * @param sm subject node, or {@code null}/wildcard + * @param pm predicate node, or {@code null}/wildcard + * @param om object node, or {@code null}/wildcard + * @return the corresponding {@link MatchPattern} + */ public static MatchPattern classify(Node sm, Node pm, Node om) { if (null != sm && sm.isConcrete()) { if (null != pm && pm.isConcrete()) { @@ -103,6 +121,5 @@ public static MatchPattern classify(Node sm, Node pm, Node om) { } } } - } } diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java index 43bbfeeaea8..a5033c22cde 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySpliterator.java @@ -21,52 +21,57 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - *
- * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a contiguous array slice {@code [0, toIndex)}, + * iterating from high index to low. Supports splitting into + * {@link ArraySubSpliterator} children for parallel traversal. *
- * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySpliterator implements Spliterator { private final E[] entries; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public ArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (-1 < --pos) { action.accept(entries[pos]); return true; @@ -79,7 +84,7 @@ public void forEachRemaining(Consumer super E> action) { while (-1 < --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -89,7 +94,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -101,4 +106,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java index 74994708b53..638f2bb0c9e 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/ArraySubSpliterator.java @@ -21,55 +21,61 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for arrays. This spliterator will iterate over the array - * entries within the given boundaries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a contiguous array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low. Produced by splitting an + * {@link ArraySpliterator} (or another {@link ArraySubSpliterator}); supports + * further recursive splits for parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public ArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (fromIndex <= --pos) { action.accept(entries[pos]); return true; @@ -82,7 +88,7 @@ public void forEachRemaining(Consumer super E> action) { while (fromIndex <= --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -93,7 +99,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -105,4 +111,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java deleted file mode 100644 index 704c9642706..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.spliterator; - -import java.util.Spliterator; -import java.util.function.Consumer; - -import org.apache.jena.mem.collection.FastHashSet; - -/** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * This spliterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * This spliterator works in ascending order, starting from the given start up to the specified exclusive index. - * - * This spliterator supports splitting into sub-spliterators. - * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedSpliterator implements Spliterator> { - - private final E[] entries; - private int currentPositionMinusOne; - private final int toIndexExclusive; - private final Runnable checkForConcurrentModification; - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param fromIndexInclusive the index of the first element, inclusive - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int fromIndexInclusive, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.currentPositionMinusOne = fromIndexInclusive-1; // Start at fromIndexInclusive - 1, so that the first call to tryAdvance will increment pos to fromIndexInclusive - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this(entries, 0, toIndexExclusive, checkForConcurrentModification); - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); - } - - - @Override - public boolean tryAdvance(Consumer super FastHashSet.IndexedKey> action) { - this.checkForConcurrentModification.run(); - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - return true; - } - } - return false; - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - } - } - this.checkForConcurrentModification.run(); - } - - @Override - public Spliterator> trySplit() { - final var nextPos = currentPositionMinusOne + 1; - final var remaining = toIndexExclusive - nextPos; - if ( remaining < 2) { - return null; - } - final var mid = nextPos + ( remaining >>> 1); - final var fromIndexInclusive = nextPos; - this.currentPositionMinusOne = mid-1; - return new SparseArrayIndexedSpliterator<>(entries, fromIndexInclusive, mid, checkForConcurrentModification); - } - - @Override - public long estimateSize() { return (long) toIndexExclusive - currentPositionMinusOne; } - - @Override - public int characteristics() { - return DISTINCT | NONNULL | IMMUTABLE; - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java index 6752cc9a1c1..add45739dc2 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java @@ -21,17 +21,24 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a sparse array slice {@code [0, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced for backing arrays such as those of + * {@link org.apache.jena.mem.collection.FastHashBase}, where removed slots + * are represented by {@code null}. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Supports splitting into {@link SparseArraySubSpliterator} children for + * parallel traversal. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it at each + * advance/forEach boundary; throws {@link ConcurrentModificationException} + * if the size has changed. * * @param the type of the array elements */ @@ -39,35 +46,37 @@ public class SparseArraySpliterator implements Spliterator { private final E[] entries; private int pos; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}, skipping nulls. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public SparseArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (-1 < --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -86,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -96,7 +105,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java index 3eb0784326f..d79242ac78c 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java @@ -21,55 +21,62 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a sparse array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced by splitting a {@link SparseArraySpliterator} (or another + * {@link SparseArraySubSpliterator}); supports further recursive splits for + * parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary; + * throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the type of the array elements */ public class SparseArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}, skipping nulls. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public SparseArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (fromIndex <= --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -88,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -99,7 +106,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java index 07ccc9634a9..f0fba805175 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java @@ -32,26 +32,41 @@ import java.util.function.Predicate; /** - * An ArrayBunch implements TripleBunch with a linear search of a short-ish - * array of Triples. The array grows by factor 2. + * Linear-scan implementation of {@link FastTripleBunch} backed by a packed + * {@link Triple} array. Used as long as a bunch stays small; once it grows + * past the configured threshold (see {@link FastTripleStore}) it is replaced + * with a {@link FastHashedTripleBunch}. + * + * The array grows by a factor of two when full. Equality of triples within a + * bunch is delegated to {@link #areEqual(Triple, Triple)}, which subclasses + * specialize to compare only the two nodes that are not already + * implied by the enclosing map's key. This avoids redundant equality checks + * on the shared subject/predicate/object. + * + * Not thread-safe. */ public abstract class FastArrayBunch implements FastTripleBunch { private static final int INITIAL_SIZE = 4; + /** Number of valid entries in {@link #elements}. */ protected int size = 0; + /** Packed array of triples; entries from {@code 0} to {@code size-1} are live. */ protected Triple[] elements; + /** + * Creates an empty bunch with the default initial capacity. + */ protected FastArrayBunch() { elements = new Triple[INITIAL_SIZE]; } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. - * But it will reserve only the space needed to contain them. Growing is still possible. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}; its backing array is sized to fit exactly, + * but can grow further if needed. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -59,7 +74,17 @@ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.size = bunchToCopy.size; } - public abstract boolean areEqual(final Triple a, final Triple b); + /** + * Compare two triples for equality within this bunch. + * + * Subclasses specialize this to skip the already-shared component + * (subject, predicate or object) and compare only the remaining two. + * + * @param a first triple + * @param b second triple + * @return {@code true} if the triples are considered equal in this bunch + */ + protected abstract boolean areEqual(final Triple a, final Triple b); @Override public boolean containsKey(Triple t) { @@ -127,6 +152,7 @@ public boolean tryRemove(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return true; } } @@ -138,6 +164,7 @@ public void removeUnchecked(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return; } } @@ -174,11 +201,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java index b89d3312048..a49d6b54009 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java @@ -25,21 +25,28 @@ import org.apache.jena.mem.collection.FastHashMap; /** - * Map from nodes to triple bunches. + * {@link FastHashMap} specialized to map a {@link Node} to its associated + * {@link FastTripleBunch}. Used by {@link FastTripleStore} to maintain the + * three subject/predicate/object indices. */ public class FastHashedBunchMap extends FastHashMap implements Copyable { + /** + * Creates an empty bunch map with the default initial capacity. + */ public FastHashedBunchMap() { super(); } /** - * Copy constructor. - * The new map will contain all the same nodes as keys of the map to copy, but copies of the bunches as values . + * Copy constructor. The new map has the same node keys as + * {@code mapToCopy}; each value is replaced by a deep copy of the + * corresponding bunch (via {@link FastTripleBunch#copy()}) so that + * mutations of either map cannot affect the other. * - * @param mapToCopy + * @param mapToCopy the source map */ private FastHashedBunchMap(final FastHashedBunchMap mapToCopy) { super(mapToCopy, FastTripleBunch::copy); diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java index 459e78c8181..65c9ab70fbf 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java @@ -25,13 +25,21 @@ import org.apache.jena.mem.collection.JenaSet; /** - * A set of triples - backed by {@link FastHashSet}. + * Hashed implementation of {@link FastTripleBunch} built on top of + * {@link FastHashSet}. Used by {@link FastTripleStore} once a bunch grows + * past the size threshold at which a linear-scan {@link FastArrayBunch} + * stops being faster. */ public class FastHashedTripleBunch extends FastHashSet implements FastTripleBunch { + /** - * Create a new triple bunch from the given set of triples. + * Create a new hashed bunch pre-populated from the given set of triples. + * The initial capacity is chosen at 1.5x the source size, so the new bunch + * fits the existing triples and has some headroom for growth before it + * needs to rehash. * - * @param set the set of triples + * @param set the source set of triples (typically the array bunch being + * promoted) */ public FastHashedTripleBunch(final JenaSet set) { super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth @@ -39,15 +47,18 @@ public FastHashedTripleBunch(final JenaSet set) { } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ private FastHashedTripleBunch(final FastHashedTripleBunch bunchToCopy) { super(bunchToCopy); } + /** + * Creates an empty hashed bunch with the default initial capacity. + */ public FastHashedTripleBunch() { super(); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java index 68f79e72f8a..fe050283188 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java @@ -29,27 +29,39 @@ import java.util.function.Predicate; /** - * A bunch of triples - a stripped-down set with specialized methods. A - * bunch is expected to store triples that share some useful property - * (such as having the same subject or predicate). + * Set-like container for a "bunch" of triples that share some useful + * property - typically they all have the same subject, predicate or object, + * because the bunch is the value of a node-keyed map in a + * {@link FastTripleStore}. + * + * The interface is a stripped-down set with a few extras tuned for the + * triple-store hot path; concrete implementations are + * {@link FastArrayBunch} (linear scan, used while the bunch is small) and + * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a + * threshold). */ public interface FastTripleBunch extends JenaSetHashOptimized, Copyable { /** - * Answer true iff this bunch is implemented as an array. - * This field is used to optimize some operations by avoiding the need for instanceOf tests. + * Answer {@code true} iff this bunch is backed by a flat array (i.e. is + * a {@link FastArrayBunch}). Exposed as an explicit method so callers can + * avoid {@code instanceof} checks on this hot path. * - * @return true iff this bunch is implemented as an arrays + * @return {@code true} if this bunch is array-backed */ boolean isArray(); /** - * This method is used to optimize _PO match operations. - * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches. - * This method is faster if there are many matches and the set is ordered in an unfavorable way. - * _PO matches usually fall into this category. + * Predicate test that scans elements in hash-table order rather than + * dense insertion order. Tuned for {@code _PO} (any-predicate-object) + * matches. + * + * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
- * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a contiguous array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low. Produced by splitting an + * {@link ArraySpliterator} (or another {@link ArraySubSpliterator}); supports + * further recursive splits for parallel traversal. *
- * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary. + * Throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the element type */ public class ArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public ArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public ArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); if (fromIndex <= --pos) { action.accept(entries[pos]); return true; @@ -82,7 +88,7 @@ public void forEachRemaining(Consumer super E> action) { while (fromIndex <= --pos) { action.accept(entries[pos]); } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -93,7 +99,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override @@ -105,4 +111,4 @@ public long estimateSize() { public int characteristics() { return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE; } -} +} \ No newline at end of file diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java deleted file mode 100644 index 704c9642706..00000000000 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArrayIndexedSpliterator.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.mem.spliterator; - -import java.util.Spliterator; -import java.util.function.Consumer; - -import org.apache.jena.mem.collection.FastHashSet; - -/** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * This spliterator returns elements as {@link FastHashSet.IndexedKey} objects, - * which contain both the index and the value of the element. - * - * This spliterator works in ascending order, starting from the given start up to the specified exclusive index. - * - * This spliterator supports splitting into sub-spliterators. - * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedSpliterator implements Spliterator> { - - private final E[] entries; - private int currentPositionMinusOne; - private final int toIndexExclusive; - private final Runnable checkForConcurrentModification; - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param fromIndexInclusive the index of the first element, inclusive - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int fromIndexInclusive, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.currentPositionMinusOne = fromIndexInclusive-1; // Start at fromIndexInclusive - 1, so that the first call to tryAdvance will increment pos to fromIndexInclusive - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this(entries, 0, toIndexExclusive, checkForConcurrentModification); - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); - } - - - @Override - public boolean tryAdvance(Consumer super FastHashSet.IndexedKey> action) { - this.checkForConcurrentModification.run(); - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - return true; - } - } - return false; - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - } - } - this.checkForConcurrentModification.run(); - } - - @Override - public Spliterator> trySplit() { - final var nextPos = currentPositionMinusOne + 1; - final var remaining = toIndexExclusive - nextPos; - if ( remaining < 2) { - return null; - } - final var mid = nextPos + ( remaining >>> 1); - final var fromIndexInclusive = nextPos; - this.currentPositionMinusOne = mid-1; - return new SparseArrayIndexedSpliterator<>(entries, fromIndexInclusive, mid, checkForConcurrentModification); - } - - @Override - public long estimateSize() { return (long) toIndexExclusive - currentPositionMinusOne; } - - @Override - public int characteristics() { - return DISTINCT | NONNULL | IMMUTABLE; - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java index 6752cc9a1c1..add45739dc2 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java @@ -21,17 +21,24 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a sparse array slice {@code [0, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced for backing arrays such as those of + * {@link org.apache.jena.mem.collection.FastHashBase}, where removed slots + * are represented by {@code null}. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Supports splitting into {@link SparseArraySubSpliterator} children for + * parallel traversal. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it at each + * advance/forEach boundary; throws {@link ConcurrentModificationException} + * if the size has changed. * * @param the type of the array elements */ @@ -39,35 +46,37 @@ public class SparseArraySpliterator implements Spliterator { private final E[] entries; private int pos; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}, skipping nulls. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public SparseArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (-1 < --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -86,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -96,7 +105,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java index 3eb0784326f..d79242ac78c 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java @@ -21,55 +21,62 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a sparse array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced by splitting a {@link SparseArraySpliterator} (or another + * {@link SparseArraySubSpliterator}); supports further recursive splits for + * parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary; + * throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the type of the array elements */ public class SparseArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}, skipping nulls. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public SparseArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (fromIndex <= --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -88,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -99,7 +106,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java index 07ccc9634a9..f0fba805175 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java @@ -32,26 +32,41 @@ import java.util.function.Predicate; /** - * An ArrayBunch implements TripleBunch with a linear search of a short-ish - * array of Triples. The array grows by factor 2. + * Linear-scan implementation of {@link FastTripleBunch} backed by a packed + * {@link Triple} array. Used as long as a bunch stays small; once it grows + * past the configured threshold (see {@link FastTripleStore}) it is replaced + * with a {@link FastHashedTripleBunch}. + * + * The array grows by a factor of two when full. Equality of triples within a + * bunch is delegated to {@link #areEqual(Triple, Triple)}, which subclasses + * specialize to compare only the two nodes that are not already + * implied by the enclosing map's key. This avoids redundant equality checks + * on the shared subject/predicate/object. + * + * Not thread-safe. */ public abstract class FastArrayBunch implements FastTripleBunch { private static final int INITIAL_SIZE = 4; + /** Number of valid entries in {@link #elements}. */ protected int size = 0; + /** Packed array of triples; entries from {@code 0} to {@code size-1} are live. */ protected Triple[] elements; + /** + * Creates an empty bunch with the default initial capacity. + */ protected FastArrayBunch() { elements = new Triple[INITIAL_SIZE]; } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. - * But it will reserve only the space needed to contain them. Growing is still possible. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}; its backing array is sized to fit exactly, + * but can grow further if needed. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -59,7 +74,17 @@ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.size = bunchToCopy.size; } - public abstract boolean areEqual(final Triple a, final Triple b); + /** + * Compare two triples for equality within this bunch. + * + * Subclasses specialize this to skip the already-shared component + * (subject, predicate or object) and compare only the remaining two. + * + * @param a first triple + * @param b second triple + * @return {@code true} if the triples are considered equal in this bunch + */ + protected abstract boolean areEqual(final Triple a, final Triple b); @Override public boolean containsKey(Triple t) { @@ -127,6 +152,7 @@ public boolean tryRemove(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return true; } } @@ -138,6 +164,7 @@ public void removeUnchecked(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return; } } @@ -174,11 +201,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java index b89d3312048..a49d6b54009 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java @@ -25,21 +25,28 @@ import org.apache.jena.mem.collection.FastHashMap; /** - * Map from nodes to triple bunches. + * {@link FastHashMap} specialized to map a {@link Node} to its associated + * {@link FastTripleBunch}. Used by {@link FastTripleStore} to maintain the + * three subject/predicate/object indices. */ public class FastHashedBunchMap extends FastHashMap implements Copyable { + /** + * Creates an empty bunch map with the default initial capacity. + */ public FastHashedBunchMap() { super(); } /** - * Copy constructor. - * The new map will contain all the same nodes as keys of the map to copy, but copies of the bunches as values . + * Copy constructor. The new map has the same node keys as + * {@code mapToCopy}; each value is replaced by a deep copy of the + * corresponding bunch (via {@link FastTripleBunch#copy()}) so that + * mutations of either map cannot affect the other. * - * @param mapToCopy + * @param mapToCopy the source map */ private FastHashedBunchMap(final FastHashedBunchMap mapToCopy) { super(mapToCopy, FastTripleBunch::copy); diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java index 459e78c8181..65c9ab70fbf 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java @@ -25,13 +25,21 @@ import org.apache.jena.mem.collection.JenaSet; /** - * A set of triples - backed by {@link FastHashSet}. + * Hashed implementation of {@link FastTripleBunch} built on top of + * {@link FastHashSet}. Used by {@link FastTripleStore} once a bunch grows + * past the size threshold at which a linear-scan {@link FastArrayBunch} + * stops being faster. */ public class FastHashedTripleBunch extends FastHashSet implements FastTripleBunch { + /** - * Create a new triple bunch from the given set of triples. + * Create a new hashed bunch pre-populated from the given set of triples. + * The initial capacity is chosen at 1.5x the source size, so the new bunch + * fits the existing triples and has some headroom for growth before it + * needs to rehash. * - * @param set the set of triples + * @param set the source set of triples (typically the array bunch being + * promoted) */ public FastHashedTripleBunch(final JenaSet set) { super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth @@ -39,15 +47,18 @@ public FastHashedTripleBunch(final JenaSet set) { } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ private FastHashedTripleBunch(final FastHashedTripleBunch bunchToCopy) { super(bunchToCopy); } + /** + * Creates an empty hashed bunch with the default initial capacity. + */ public FastHashedTripleBunch() { super(); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java index 68f79e72f8a..fe050283188 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java @@ -29,27 +29,39 @@ import java.util.function.Predicate; /** - * A bunch of triples - a stripped-down set with specialized methods. A - * bunch is expected to store triples that share some useful property - * (such as having the same subject or predicate). + * Set-like container for a "bunch" of triples that share some useful + * property - typically they all have the same subject, predicate or object, + * because the bunch is the value of a node-keyed map in a + * {@link FastTripleStore}. + * + * The interface is a stripped-down set with a few extras tuned for the + * triple-store hot path; concrete implementations are + * {@link FastArrayBunch} (linear scan, used while the bunch is small) and + * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a + * threshold). */ public interface FastTripleBunch extends JenaSetHashOptimized, Copyable { /** - * Answer true iff this bunch is implemented as an array. - * This field is used to optimize some operations by avoiding the need for instanceOf tests. + * Answer {@code true} iff this bunch is backed by a flat array (i.e. is + * a {@link FastArrayBunch}). Exposed as an explicit method so callers can + * avoid {@code instanceof} checks on this hot path. * - * @return true iff this bunch is implemented as an arrays + * @return {@code true} if this bunch is array-backed */ boolean isArray(); /** - * This method is used to optimize _PO match operations. - * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches. - * This method is faster if there are many matches and the set is ordered in an unfavorable way. - * _PO matches usually fall into this category. + * Predicate test that scans elements in hash-table order rather than + * dense insertion order. Tuned for {@code _PO} (any-predicate-object) + * matches. + * + * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
- * This spliterator works in ascending order, starting from the given start up to the specified exclusive index. - *
- * This spliterator supports splitting into sub-spliterators. - *
- * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. - * - * @param the type of the array elements - */ -@SuppressWarnings("all") -public class SparseArrayIndexedSpliterator implements Spliterator> { - - private final E[] entries; - private int currentPositionMinusOne; - private final int toIndexExclusive; - private final Runnable checkForConcurrentModification; - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param fromIndexInclusive the index of the first element, inclusive - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int fromIndexInclusive, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this.entries = entries; - this.currentPositionMinusOne = fromIndexInclusive-1; // Start at fromIndexInclusive - 1, so that the first call to tryAdvance will increment pos to fromIndexInclusive - this.toIndexExclusive = toIndexExclusive; - this.checkForConcurrentModification = checkForConcurrentModification; - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param toIndexExclusive the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final int toIndexExclusive, final Runnable checkForConcurrentModification) { - this(entries, 0, toIndexExclusive, checkForConcurrentModification); - } - - /** - * Create a spliterator for the given array, with the given size. - * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications - */ - public SparseArrayIndexedSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); - } - - - @Override - public boolean tryAdvance(Consumer super FastHashSet.IndexedKey> action) { - this.checkForConcurrentModification.run(); - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - return true; - } - } - return false; - } - - @Override - public void forEachRemaining(Consumer super FastHashSet.IndexedKey> action) { - while (toIndexExclusive > ++currentPositionMinusOne) { - if (null != entries[currentPositionMinusOne]) { - action.accept(new FastHashSet.IndexedKey<>(currentPositionMinusOne, entries[currentPositionMinusOne])); - } - } - this.checkForConcurrentModification.run(); - } - - @Override - public Spliterator> trySplit() { - final var nextPos = currentPositionMinusOne + 1; - final var remaining = toIndexExclusive - nextPos; - if ( remaining < 2) { - return null; - } - final var mid = nextPos + ( remaining >>> 1); - final var fromIndexInclusive = nextPos; - this.currentPositionMinusOne = mid-1; - return new SparseArrayIndexedSpliterator<>(entries, fromIndexInclusive, mid, checkForConcurrentModification); - } - - @Override - public long estimateSize() { return (long) toIndexExclusive - currentPositionMinusOne; } - - @Override - public int characteristics() { - return DISTINCT | NONNULL | IMMUTABLE; - } -} diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java index 6752cc9a1c1..add45739dc2 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySpliterator.java @@ -21,17 +21,24 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a sparse array slice {@code [0, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced for backing arrays such as those of + * {@link org.apache.jena.mem.collection.FastHashBase}, where removed slots + * are represented by {@code null}. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Supports splitting into {@link SparseArraySubSpliterator} children for + * parallel traversal. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it at each + * advance/forEach boundary; throws {@link ConcurrentModificationException} + * if the size has changed. * * @param the type of the array elements */ @@ -39,35 +46,37 @@ public class SparseArraySpliterator implements Spliterator { private final E[] entries; private int pos; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}, skipping nulls. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public SparseArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (-1 < --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -86,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -96,7 +105,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java index 3eb0784326f..d79242ac78c 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java @@ -21,55 +21,62 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a sparse array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced by splitting a {@link SparseArraySpliterator} (or another + * {@link SparseArraySubSpliterator}); supports further recursive splits for + * parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary; + * throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the type of the array elements */ public class SparseArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}, skipping nulls. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public SparseArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (fromIndex <= --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -88,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -99,7 +106,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java index 07ccc9634a9..f0fba805175 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java @@ -32,26 +32,41 @@ import java.util.function.Predicate; /** - * An ArrayBunch implements TripleBunch with a linear search of a short-ish - * array of Triples. The array grows by factor 2. + * Linear-scan implementation of {@link FastTripleBunch} backed by a packed + * {@link Triple} array. Used as long as a bunch stays small; once it grows + * past the configured threshold (see {@link FastTripleStore}) it is replaced + * with a {@link FastHashedTripleBunch}. + * + * The array grows by a factor of two when full. Equality of triples within a + * bunch is delegated to {@link #areEqual(Triple, Triple)}, which subclasses + * specialize to compare only the two nodes that are not already + * implied by the enclosing map's key. This avoids redundant equality checks + * on the shared subject/predicate/object. + * + * Not thread-safe. */ public abstract class FastArrayBunch implements FastTripleBunch { private static final int INITIAL_SIZE = 4; + /** Number of valid entries in {@link #elements}. */ protected int size = 0; + /** Packed array of triples; entries from {@code 0} to {@code size-1} are live. */ protected Triple[] elements; + /** + * Creates an empty bunch with the default initial capacity. + */ protected FastArrayBunch() { elements = new Triple[INITIAL_SIZE]; } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. - * But it will reserve only the space needed to contain them. Growing is still possible. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}; its backing array is sized to fit exactly, + * but can grow further if needed. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -59,7 +74,17 @@ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.size = bunchToCopy.size; } - public abstract boolean areEqual(final Triple a, final Triple b); + /** + * Compare two triples for equality within this bunch. + * + * Subclasses specialize this to skip the already-shared component + * (subject, predicate or object) and compare only the remaining two. + * + * @param a first triple + * @param b second triple + * @return {@code true} if the triples are considered equal in this bunch + */ + protected abstract boolean areEqual(final Triple a, final Triple b); @Override public boolean containsKey(Triple t) { @@ -127,6 +152,7 @@ public boolean tryRemove(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return true; } } @@ -138,6 +164,7 @@ public void removeUnchecked(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return; } } @@ -174,11 +201,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java index b89d3312048..a49d6b54009 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java @@ -25,21 +25,28 @@ import org.apache.jena.mem.collection.FastHashMap; /** - * Map from nodes to triple bunches. + * {@link FastHashMap} specialized to map a {@link Node} to its associated + * {@link FastTripleBunch}. Used by {@link FastTripleStore} to maintain the + * three subject/predicate/object indices. */ public class FastHashedBunchMap extends FastHashMap implements Copyable { + /** + * Creates an empty bunch map with the default initial capacity. + */ public FastHashedBunchMap() { super(); } /** - * Copy constructor. - * The new map will contain all the same nodes as keys of the map to copy, but copies of the bunches as values . + * Copy constructor. The new map has the same node keys as + * {@code mapToCopy}; each value is replaced by a deep copy of the + * corresponding bunch (via {@link FastTripleBunch#copy()}) so that + * mutations of either map cannot affect the other. * - * @param mapToCopy + * @param mapToCopy the source map */ private FastHashedBunchMap(final FastHashedBunchMap mapToCopy) { super(mapToCopy, FastTripleBunch::copy); diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java index 459e78c8181..65c9ab70fbf 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java @@ -25,13 +25,21 @@ import org.apache.jena.mem.collection.JenaSet; /** - * A set of triples - backed by {@link FastHashSet}. + * Hashed implementation of {@link FastTripleBunch} built on top of + * {@link FastHashSet}. Used by {@link FastTripleStore} once a bunch grows + * past the size threshold at which a linear-scan {@link FastArrayBunch} + * stops being faster. */ public class FastHashedTripleBunch extends FastHashSet implements FastTripleBunch { + /** - * Create a new triple bunch from the given set of triples. + * Create a new hashed bunch pre-populated from the given set of triples. + * The initial capacity is chosen at 1.5x the source size, so the new bunch + * fits the existing triples and has some headroom for growth before it + * needs to rehash. * - * @param set the set of triples + * @param set the source set of triples (typically the array bunch being + * promoted) */ public FastHashedTripleBunch(final JenaSet set) { super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth @@ -39,15 +47,18 @@ public FastHashedTripleBunch(final JenaSet set) { } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ private FastHashedTripleBunch(final FastHashedTripleBunch bunchToCopy) { super(bunchToCopy); } + /** + * Creates an empty hashed bunch with the default initial capacity. + */ public FastHashedTripleBunch() { super(); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java index 68f79e72f8a..fe050283188 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java @@ -29,27 +29,39 @@ import java.util.function.Predicate; /** - * A bunch of triples - a stripped-down set with specialized methods. A - * bunch is expected to store triples that share some useful property - * (such as having the same subject or predicate). + * Set-like container for a "bunch" of triples that share some useful + * property - typically they all have the same subject, predicate or object, + * because the bunch is the value of a node-keyed map in a + * {@link FastTripleStore}. + * + * The interface is a stripped-down set with a few extras tuned for the + * triple-store hot path; concrete implementations are + * {@link FastArrayBunch} (linear scan, used while the bunch is small) and + * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a + * threshold). */ public interface FastTripleBunch extends JenaSetHashOptimized, Copyable { /** - * Answer true iff this bunch is implemented as an array. - * This field is used to optimize some operations by avoiding the need for instanceOf tests. + * Answer {@code true} iff this bunch is backed by a flat array (i.e. is + * a {@link FastArrayBunch}). Exposed as an explicit method so callers can + * avoid {@code instanceof} checks on this hot path. * - * @return true iff this bunch is implemented as an arrays + * @return {@code true} if this bunch is array-backed */ boolean isArray(); /** - * This method is used to optimize _PO match operations. - * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches. - * This method is faster if there are many matches and the set is ordered in an unfavorable way. - * _PO matches usually fall into this category. + * Predicate test that scans elements in hash-table order rather than + * dense insertion order. Tuned for {@code _PO} (any-predicate-object) + * matches. + * + * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
- * This spliterator supports splitting into sub-spliterators. + * Top-level spliterator over a sparse array slice {@code [0, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced for backing arrays such as those of + * {@link org.apache.jena.mem.collection.FastHashBase}, where removed slots + * are represented by {@code null}. *
- * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Supports splitting into {@link SparseArraySubSpliterator} children for + * parallel traversal. Detects concurrent modifications by snapshotting + * {@code set.size()} at construction time and rechecking it at each + * advance/forEach boundary; throws {@link ConcurrentModificationException} + * if the size has changed. * * @param the type of the array elements */ @@ -39,35 +46,37 @@ public class SparseArraySpliterator implements Spliterator { private final E[] entries; private int pos; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[0 .. toIndex)}, skipping nulls. * - * @param entries the array - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySpliterator(final E[] entries, final int toIndex, final Sized set) { this.entries = entries; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, entries.length, checkForConcurrentModification); + public SparseArraySpliterator(final E[] entries, final Sized set) { + this(entries, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (-1 < --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -86,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -96,7 +105,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = pos >>> 1; - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java index 3eb0784326f..d79242ac78c 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java +++ b/jena-core/src/main/java/org/apache/jena/mem/spliterator/SparseArraySubSpliterator.java @@ -21,55 +21,62 @@ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.Sized; + +import java.util.ConcurrentModificationException; import java.util.Spliterator; import java.util.function.Consumer; /** - * A spliterator for sparse arrays. This spliterator will iterate over the array - * skipping null entries. - * - * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a sparse array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced by splitting a {@link SparseArraySpliterator} (or another + * {@link SparseArraySubSpliterator}); supports further recursive splits for + * parallel traversal. * - * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary; + * throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the type of the array elements */ public class SparseArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}, skipping nulls. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public SparseArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (fromIndex <= --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -88,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -99,7 +106,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java index 07ccc9634a9..f0fba805175 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java @@ -32,26 +32,41 @@ import java.util.function.Predicate; /** - * An ArrayBunch implements TripleBunch with a linear search of a short-ish - * array of Triples. The array grows by factor 2. + * Linear-scan implementation of {@link FastTripleBunch} backed by a packed + * {@link Triple} array. Used as long as a bunch stays small; once it grows + * past the configured threshold (see {@link FastTripleStore}) it is replaced + * with a {@link FastHashedTripleBunch}. + * + * The array grows by a factor of two when full. Equality of triples within a + * bunch is delegated to {@link #areEqual(Triple, Triple)}, which subclasses + * specialize to compare only the two nodes that are not already + * implied by the enclosing map's key. This avoids redundant equality checks + * on the shared subject/predicate/object. + * + * Not thread-safe. */ public abstract class FastArrayBunch implements FastTripleBunch { private static final int INITIAL_SIZE = 4; + /** Number of valid entries in {@link #elements}. */ protected int size = 0; + /** Packed array of triples; entries from {@code 0} to {@code size-1} are live. */ protected Triple[] elements; + /** + * Creates an empty bunch with the default initial capacity. + */ protected FastArrayBunch() { elements = new Triple[INITIAL_SIZE]; } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. - * But it will reserve only the space needed to contain them. Growing is still possible. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}; its backing array is sized to fit exactly, + * but can grow further if needed. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -59,7 +74,17 @@ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.size = bunchToCopy.size; } - public abstract boolean areEqual(final Triple a, final Triple b); + /** + * Compare two triples for equality within this bunch. + * + * Subclasses specialize this to skip the already-shared component + * (subject, predicate or object) and compare only the remaining two. + * + * @param a first triple + * @param b second triple + * @return {@code true} if the triples are considered equal in this bunch + */ + protected abstract boolean areEqual(final Triple a, final Triple b); @Override public boolean containsKey(Triple t) { @@ -127,6 +152,7 @@ public boolean tryRemove(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return true; } } @@ -138,6 +164,7 @@ public void removeUnchecked(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return; } } @@ -174,11 +201,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java index b89d3312048..a49d6b54009 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java @@ -25,21 +25,28 @@ import org.apache.jena.mem.collection.FastHashMap; /** - * Map from nodes to triple bunches. + * {@link FastHashMap} specialized to map a {@link Node} to its associated + * {@link FastTripleBunch}. Used by {@link FastTripleStore} to maintain the + * three subject/predicate/object indices. */ public class FastHashedBunchMap extends FastHashMap implements Copyable { + /** + * Creates an empty bunch map with the default initial capacity. + */ public FastHashedBunchMap() { super(); } /** - * Copy constructor. - * The new map will contain all the same nodes as keys of the map to copy, but copies of the bunches as values . + * Copy constructor. The new map has the same node keys as + * {@code mapToCopy}; each value is replaced by a deep copy of the + * corresponding bunch (via {@link FastTripleBunch#copy()}) so that + * mutations of either map cannot affect the other. * - * @param mapToCopy + * @param mapToCopy the source map */ private FastHashedBunchMap(final FastHashedBunchMap mapToCopy) { super(mapToCopy, FastTripleBunch::copy); diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java index 459e78c8181..65c9ab70fbf 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java @@ -25,13 +25,21 @@ import org.apache.jena.mem.collection.JenaSet; /** - * A set of triples - backed by {@link FastHashSet}. + * Hashed implementation of {@link FastTripleBunch} built on top of + * {@link FastHashSet}. Used by {@link FastTripleStore} once a bunch grows + * past the size threshold at which a linear-scan {@link FastArrayBunch} + * stops being faster. */ public class FastHashedTripleBunch extends FastHashSet implements FastTripleBunch { + /** - * Create a new triple bunch from the given set of triples. + * Create a new hashed bunch pre-populated from the given set of triples. + * The initial capacity is chosen at 1.5x the source size, so the new bunch + * fits the existing triples and has some headroom for growth before it + * needs to rehash. * - * @param set the set of triples + * @param set the source set of triples (typically the array bunch being + * promoted) */ public FastHashedTripleBunch(final JenaSet set) { super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth @@ -39,15 +47,18 @@ public FastHashedTripleBunch(final JenaSet set) { } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ private FastHashedTripleBunch(final FastHashedTripleBunch bunchToCopy) { super(bunchToCopy); } + /** + * Creates an empty hashed bunch with the default initial capacity. + */ public FastHashedTripleBunch() { super(); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java index 68f79e72f8a..fe050283188 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java @@ -29,27 +29,39 @@ import java.util.function.Predicate; /** - * A bunch of triples - a stripped-down set with specialized methods. A - * bunch is expected to store triples that share some useful property - * (such as having the same subject or predicate). + * Set-like container for a "bunch" of triples that share some useful + * property - typically they all have the same subject, predicate or object, + * because the bunch is the value of a node-keyed map in a + * {@link FastTripleStore}. + * + * The interface is a stripped-down set with a few extras tuned for the + * triple-store hot path; concrete implementations are + * {@link FastArrayBunch} (linear scan, used while the bunch is small) and + * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a + * threshold). */ public interface FastTripleBunch extends JenaSetHashOptimized, Copyable { /** - * Answer true iff this bunch is implemented as an array. - * This field is used to optimize some operations by avoiding the need for instanceOf tests. + * Answer {@code true} iff this bunch is backed by a flat array (i.e. is + * a {@link FastArrayBunch}). Exposed as an explicit method so callers can + * avoid {@code instanceof} checks on this hot path. * - * @return true iff this bunch is implemented as an arrays + * @return {@code true} if this bunch is array-backed */ boolean isArray(); /** - * This method is used to optimize _PO match operations. - * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches. - * This method is faster if there are many matches and the set is ordered in an unfavorable way. - * _PO matches usually fall into this category. + * Predicate test that scans elements in hash-table order rather than + * dense insertion order. Tuned for {@code _PO} (any-predicate-object) + * matches. + * + * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
- * This spliterator supports splitting into sub-spliterators. + * Sub-range spliterator over a sparse array slice {@code [fromIndex, toIndex)}, + * iterating from high index to low and skipping {@code null} entries. + * Produced by splitting a {@link SparseArraySpliterator} (or another + * {@link SparseArraySubSpliterator}); supports further recursive splits for + * parallel traversal. *
- * The spliterator will check for concurrent modifications by invoking a {@link Runnable} - * before each action. + * Detects concurrent modifications by snapshotting {@code set.size()} at + * construction time and rechecking it at each advance/forEach boundary; + * throws {@link ConcurrentModificationException} if the size has changed. * - * @param + * @param the type of the array elements */ public class SparseArraySubSpliterator implements Spliterator { private final E[] entries; private final int fromIndex; - private final Runnable checkForConcurrentModification; + private final Sized set; + private final int sizeOfSetAtStart; private int pos; /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over {@code entries[fromIndex .. toIndex)}, skipping nulls. * - * @param entries the array - * @param fromIndex the index of the first element, inclusive - * @param toIndex the index of the last element, exclusive - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param fromIndex inclusive lower bound on the iterated slice + * @param toIndex exclusive upper bound on the iterated slice + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) { + public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Sized set) { this.entries = entries; this.fromIndex = fromIndex; this.pos = toIndex; - this.checkForConcurrentModification = checkForConcurrentModification; + this.set = set; + this.sizeOfSetAtStart = set.size(); } /** - * Create a spliterator for the given array, with the given size. + * Create a spliterator over the entire array, skipping nulls. * - * @param entries the array - * @param checkForConcurrentModification runnable to check for concurrent modifications + * @param entries the backing array (not copied) + * @param set the owning collection used to detect concurrent modifications */ - public SparseArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) { - this(entries, 0, entries.length, checkForConcurrentModification); + public SparseArraySubSpliterator(final E[] entries, final Sized set) { + this(entries, 0, entries.length, set); } @Override public boolean tryAdvance(Consumer super E> action) { - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); while (fromIndex <= --pos) { if (null != entries[pos]) { action.accept(entries[pos]); @@ -88,7 +95,7 @@ public void forEachRemaining(Consumer super E> action) { } pos--; } - this.checkForConcurrentModification.run(); + if (sizeOfSetAtStart != set.size()) throw new ConcurrentModificationException(); } @Override @@ -99,7 +106,7 @@ public Spliterator trySplit() { } final int toIndexOfSubIterator = this.pos; this.pos = fromIndex + (entriesCount >>> 1); - return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification); + return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, set); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java index 07ccc9634a9..f0fba805175 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastArrayBunch.java @@ -32,26 +32,41 @@ import java.util.function.Predicate; /** - * An ArrayBunch implements TripleBunch with a linear search of a short-ish - * array of Triples. The array grows by factor 2. + * Linear-scan implementation of {@link FastTripleBunch} backed by a packed + * {@link Triple} array. Used as long as a bunch stays small; once it grows + * past the configured threshold (see {@link FastTripleStore}) it is replaced + * with a {@link FastHashedTripleBunch}. + * + * The array grows by a factor of two when full. Equality of triples within a + * bunch is delegated to {@link #areEqual(Triple, Triple)}, which subclasses + * specialize to compare only the two nodes that are not already + * implied by the enclosing map's key. This avoids redundant equality checks + * on the shared subject/predicate/object. + * + * Not thread-safe. */ public abstract class FastArrayBunch implements FastTripleBunch { private static final int INITIAL_SIZE = 4; + /** Number of valid entries in {@link #elements}. */ protected int size = 0; + /** Packed array of triples; entries from {@code 0} to {@code size-1} are live. */ protected Triple[] elements; + /** + * Creates an empty bunch with the default initial capacity. + */ protected FastArrayBunch() { elements = new Triple[INITIAL_SIZE]; } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. - * But it will reserve only the space needed to contain them. Growing is still possible. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}; its backing array is sized to fit exactly, + * but can grow further if needed. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -59,7 +74,17 @@ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.size = bunchToCopy.size; } - public abstract boolean areEqual(final Triple a, final Triple b); + /** + * Compare two triples for equality within this bunch. + * + * Subclasses specialize this to skip the already-shared component + * (subject, predicate or object) and compare only the remaining two. + * + * @param a first triple + * @param b second triple + * @return {@code true} if the triples are considered equal in this bunch + */ + protected abstract boolean areEqual(final Triple a, final Triple b); @Override public boolean containsKey(Triple t) { @@ -127,6 +152,7 @@ public boolean tryRemove(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return true; } } @@ -138,6 +164,7 @@ public void removeUnchecked(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return; } } @@ -174,11 +201,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java index b89d3312048..a49d6b54009 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java @@ -25,21 +25,28 @@ import org.apache.jena.mem.collection.FastHashMap; /** - * Map from nodes to triple bunches. + * {@link FastHashMap} specialized to map a {@link Node} to its associated + * {@link FastTripleBunch}. Used by {@link FastTripleStore} to maintain the + * three subject/predicate/object indices. */ public class FastHashedBunchMap extends FastHashMap implements Copyable { + /** + * Creates an empty bunch map with the default initial capacity. + */ public FastHashedBunchMap() { super(); } /** - * Copy constructor. - * The new map will contain all the same nodes as keys of the map to copy, but copies of the bunches as values . + * Copy constructor. The new map has the same node keys as + * {@code mapToCopy}; each value is replaced by a deep copy of the + * corresponding bunch (via {@link FastTripleBunch#copy()}) so that + * mutations of either map cannot affect the other. * - * @param mapToCopy + * @param mapToCopy the source map */ private FastHashedBunchMap(final FastHashedBunchMap mapToCopy) { super(mapToCopy, FastTripleBunch::copy); diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java index 459e78c8181..65c9ab70fbf 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java @@ -25,13 +25,21 @@ import org.apache.jena.mem.collection.JenaSet; /** - * A set of triples - backed by {@link FastHashSet}. + * Hashed implementation of {@link FastTripleBunch} built on top of + * {@link FastHashSet}. Used by {@link FastTripleStore} once a bunch grows + * past the size threshold at which a linear-scan {@link FastArrayBunch} + * stops being faster. */ public class FastHashedTripleBunch extends FastHashSet implements FastTripleBunch { + /** - * Create a new triple bunch from the given set of triples. + * Create a new hashed bunch pre-populated from the given set of triples. + * The initial capacity is chosen at 1.5x the source size, so the new bunch + * fits the existing triples and has some headroom for growth before it + * needs to rehash. * - * @param set the set of triples + * @param set the source set of triples (typically the array bunch being + * promoted) */ public FastHashedTripleBunch(final JenaSet set) { super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth @@ -39,15 +47,18 @@ public FastHashedTripleBunch(final JenaSet set) { } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ private FastHashedTripleBunch(final FastHashedTripleBunch bunchToCopy) { super(bunchToCopy); } + /** + * Creates an empty hashed bunch with the default initial capacity. + */ public FastHashedTripleBunch() { super(); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java index 68f79e72f8a..fe050283188 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java @@ -29,27 +29,39 @@ import java.util.function.Predicate; /** - * A bunch of triples - a stripped-down set with specialized methods. A - * bunch is expected to store triples that share some useful property - * (such as having the same subject or predicate). + * Set-like container for a "bunch" of triples that share some useful + * property - typically they all have the same subject, predicate or object, + * because the bunch is the value of a node-keyed map in a + * {@link FastTripleStore}. + * + * The interface is a stripped-down set with a few extras tuned for the + * triple-store hot path; concrete implementations are + * {@link FastArrayBunch} (linear scan, used while the bunch is small) and + * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a + * threshold). */ public interface FastTripleBunch extends JenaSetHashOptimized, Copyable { /** - * Answer true iff this bunch is implemented as an array. - * This field is used to optimize some operations by avoiding the need for instanceOf tests. + * Answer {@code true} iff this bunch is backed by a flat array (i.e. is + * a {@link FastArrayBunch}). Exposed as an explicit method so callers can + * avoid {@code instanceof} checks on this hot path. * - * @return true iff this bunch is implemented as an arrays + * @return {@code true} if this bunch is array-backed */ boolean isArray(); /** - * This method is used to optimize _PO match operations. - * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches. - * This method is faster if there are many matches and the set is ordered in an unfavorable way. - * _PO matches usually fall into this category. + * Predicate test that scans elements in hash-table order rather than + * dense insertion order. Tuned for {@code _PO} (any-predicate-object) + * matches. + * + * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
+ * The array grows by a factor of two when full. Equality of triples within a + * bunch is delegated to {@link #areEqual(Triple, Triple)}, which subclasses + * specialize to compare only the two nodes that are not already + * implied by the enclosing map's key. This avoids redundant equality checks + * on the shared subject/predicate/object. + *
+ * Not thread-safe. */ public abstract class FastArrayBunch implements FastTripleBunch { private static final int INITIAL_SIZE = 4; + /** Number of valid entries in {@link #elements}. */ protected int size = 0; + /** Packed array of triples; entries from {@code 0} to {@code size-1} are live. */ protected Triple[] elements; + /** + * Creates an empty bunch with the default initial capacity. + */ protected FastArrayBunch() { elements = new Triple[INITIAL_SIZE]; } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. - * But it will reserve only the space needed to contain them. Growing is still possible. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}; its backing array is sized to fit exactly, + * but can grow further if needed. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -59,7 +74,17 @@ protected FastArrayBunch(final FastArrayBunch bunchToCopy) { this.size = bunchToCopy.size; } - public abstract boolean areEqual(final Triple a, final Triple b); + /** + * Compare two triples for equality within this bunch. + *
+ * Subclasses specialize this to skip the already-shared component + * (subject, predicate or object) and compare only the remaining two. + * + * @param a first triple + * @param b second triple + * @return {@code true} if the triples are considered equal in this bunch + */ + protected abstract boolean areEqual(final Triple a, final Triple b); @Override public boolean containsKey(Triple t) { @@ -127,6 +152,7 @@ public boolean tryRemove(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return true; } } @@ -138,6 +164,7 @@ public void removeUnchecked(final Triple t) { for (int i = 0; i < size; i++) { if (areEqual(t, elements[i])) { elements[i] = elements[--size]; + elements[size] = null; return; } } @@ -174,11 +201,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java index b89d3312048..a49d6b54009 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedBunchMap.java @@ -25,21 +25,28 @@ import org.apache.jena.mem.collection.FastHashMap; /** - * Map from nodes to triple bunches. + * {@link FastHashMap} specialized to map a {@link Node} to its associated + * {@link FastTripleBunch}. Used by {@link FastTripleStore} to maintain the + * three subject/predicate/object indices. */ public class FastHashedBunchMap extends FastHashMap implements Copyable { + /** + * Creates an empty bunch map with the default initial capacity. + */ public FastHashedBunchMap() { super(); } /** - * Copy constructor. - * The new map will contain all the same nodes as keys of the map to copy, but copies of the bunches as values . + * Copy constructor. The new map has the same node keys as + * {@code mapToCopy}; each value is replaced by a deep copy of the + * corresponding bunch (via {@link FastTripleBunch#copy()}) so that + * mutations of either map cannot affect the other. * - * @param mapToCopy + * @param mapToCopy the source map */ private FastHashedBunchMap(final FastHashedBunchMap mapToCopy) { super(mapToCopy, FastTripleBunch::copy); diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java index 459e78c8181..65c9ab70fbf 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastHashedTripleBunch.java @@ -25,13 +25,21 @@ import org.apache.jena.mem.collection.JenaSet; /** - * A set of triples - backed by {@link FastHashSet}. + * Hashed implementation of {@link FastTripleBunch} built on top of + * {@link FastHashSet}. Used by {@link FastTripleStore} once a bunch grows + * past the size threshold at which a linear-scan {@link FastArrayBunch} + * stops being faster. */ public class FastHashedTripleBunch extends FastHashSet implements FastTripleBunch { + /** - * Create a new triple bunch from the given set of triples. + * Create a new hashed bunch pre-populated from the given set of triples. + * The initial capacity is chosen at 1.5x the source size, so the new bunch + * fits the existing triples and has some headroom for growth before it + * needs to rehash. * - * @param set the set of triples + * @param set the source set of triples (typically the array bunch being + * promoted) */ public FastHashedTripleBunch(final JenaSet set) { super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth @@ -39,15 +47,18 @@ public FastHashedTripleBunch(final JenaSet set) { } /** - * Copy constructor. - * The new bunch will contain all the same triples of the bunch to copy. + * Copy constructor. The new bunch contains the same triples as + * {@code bunchToCopy}. * - * @param bunchToCopy + * @param bunchToCopy the source bunch */ private FastHashedTripleBunch(final FastHashedTripleBunch bunchToCopy) { super(bunchToCopy); } + /** + * Creates an empty hashed bunch with the default initial capacity. + */ public FastHashedTripleBunch() { super(); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java index 68f79e72f8a..fe050283188 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleBunch.java @@ -29,27 +29,39 @@ import java.util.function.Predicate; /** - * A bunch of triples - a stripped-down set with specialized methods. A - * bunch is expected to store triples that share some useful property - * (such as having the same subject or predicate). + * Set-like container for a "bunch" of triples that share some useful + * property - typically they all have the same subject, predicate or object, + * because the bunch is the value of a node-keyed map in a + * {@link FastTripleStore}. + * + * The interface is a stripped-down set with a few extras tuned for the + * triple-store hot path; concrete implementations are + * {@link FastArrayBunch} (linear scan, used while the bunch is small) and + * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a + * threshold). */ public interface FastTripleBunch extends JenaSetHashOptimized, Copyable { /** - * Answer true iff this bunch is implemented as an array. - * This field is used to optimize some operations by avoiding the need for instanceOf tests. + * Answer {@code true} iff this bunch is backed by a flat array (i.e. is + * a {@link FastArrayBunch}). Exposed as an explicit method so callers can + * avoid {@code instanceof} checks on this hot path. * - * @return true iff this bunch is implemented as an arrays + * @return {@code true} if this bunch is array-backed */ boolean isArray(); /** - * This method is used to optimize _PO match operations. - * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches. - * This method is faster if there are many matches and the set is ordered in an unfavorable way. - * _PO matches usually fall into this category. + * Predicate test that scans elements in hash-table order rather than + * dense insertion order. Tuned for {@code _PO} (any-predicate-object) + * matches. + * + * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
+ * The interface is a stripped-down set with a few extras tuned for the + * triple-store hot path; concrete implementations are + * {@link FastArrayBunch} (linear scan, used while the bunch is small) and + * {@link FastHashedTripleBunch} (hashed, used once the bunch grows past a + * threshold). */ public interface FastTripleBunch extends JenaSetHashOptimized, Copyable { /** - * Answer true iff this bunch is implemented as an array. - * This field is used to optimize some operations by avoiding the need for instanceOf tests. + * Answer {@code true} iff this bunch is backed by a flat array (i.e. is + * a {@link FastArrayBunch}). Exposed as an explicit method so callers can + * avoid {@code instanceof} checks on this hot path. * - * @return true iff this bunch is implemented as an arrays + * @return {@code true} if this bunch is array-backed */ boolean isArray(); /** - * This method is used to optimize _PO match operations. - * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches. - * This method is faster if there are many matches and the set is ordered in an unfavorable way. - * _PO matches usually fall into this category. + * Predicate test that scans elements in hash-table order rather than + * dense insertion order. Tuned for {@code _PO} (any-predicate-object) + * matches. + * + * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
+ * {@link JenaMapSetCommon#anyMatch(Predicate)} is faster when matches + * are rare or absent; this method is faster when many matches exist and + * the dense ordering would force scanning past clustered non-matches + * before finding a hit. Both variants short-circuit on the first match. * - * @param predicate the predicate to match - * @return true if any triple in the bunch matches the predicate + * @param predicate the predicate to test against each triple + * @return {@code true} if any triple in the bunch satisfies the predicate */ boolean anyMatchRandomOrder(Predicate predicate); } diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java index 8877bcffe9a..8ed81dc577b 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/fast/FastTripleStore.java @@ -68,20 +68,43 @@ */ public class FastTripleStore implements TripleStore { + /** + * Object-bunch size above which {@code _PO} matches consider a + * secondary lookup in the predicate bunch. + */ protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400; + /** + * Maximum size of a subject-keyed array bunch before it is promoted + * to a hashed bunch. Lower than the predicate/object threshold because + * the subject map is the primary entry point for {@code contains}. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16; + /** + * Maximum size of a predicate- or object-keyed array bunch before it is + * promoted to a hashed bunch. + */ protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32; - final FastHashedBunchMap subjects; - final FastHashedBunchMap predicates; - final FastHashedBunchMap objects; + private final FastHashedBunchMap subjects; + private final FastHashedBunchMap predicates; + private final FastHashedBunchMap objects; private int size = 0; + /** + * Creates a new, empty fast triple store. + */ public FastTripleStore() { subjects = new FastHashedBunchMap(); predicates = new FastHashedBunchMap(); objects = new FastHashedBunchMap(); } + /** + * Copy constructor used by {@link #copy()}; produces an independent store + * by deep-copying each of the three index maps (which in turn deep-copy + * their bunches). + * + * @param tripleStoreToCopy the source store + */ private FastTripleStore(final FastTripleStore tripleStoreToCopy) { subjects = tripleStoreToCopy.subjects.copy(); predicates = tripleStoreToCopy.predicates.copy(); @@ -380,6 +403,11 @@ public FastTripleStore copy() { return new FastTripleStore(this); } + /** + * Array bunch used as the value in the subject-keyed map: every triple in + * the bunch shares the same subject, so equality only needs to compare + * predicate and object. + */ protected static class ArrayBunchWithSameSubject extends FastArrayBunch { public ArrayBunchWithSameSubject() { @@ -402,6 +430,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the predicate-keyed map: every triple + * in the bunch shares the same predicate, so equality only needs to + * compare subject and object. + */ protected static class ArrayBunchWithSamePredicate extends FastArrayBunch { public ArrayBunchWithSamePredicate() { @@ -424,6 +457,11 @@ public boolean areEqual(final Triple a, final Triple b) { } } + /** + * Array bunch used as the value in the object-keyed map: every triple in + * the bunch shares the same object, so equality only needs to compare + * subject and predicate. + */ protected static class ArrayBunchWithSameObject extends FastArrayBunch { public ArrayBunchWithSameObject() { diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java index 0a8ad7bf7ef..097760b3358 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/legacy/ArrayBunch.java @@ -54,7 +54,7 @@ public ArrayBunch() { * The new bunch will contain all the same triples of the bunch to copy. * But it will reserve only the space needed to contain them. Growing is still possible. * - * @param bunchToCopy + * @param bunchToCopy the bunch to copy */ private ArrayBunch(final ArrayBunch bunchToCopy) { this.elements = new Triple[bunchToCopy.size]; @@ -168,11 +168,7 @@ public void forEachRemaining(Consumer super Triple> action) { @Override public Spliterator keySpliterator() { - final var initialSize = size; - final Runnable checkForConcurrentModification = () -> { - if (size != initialSize) throw new ConcurrentModificationException(); - }; - return new ArraySpliterator<>(elements, size, checkForConcurrentModification); + return new ArraySpliterator<>(elements, size, this); } @Override diff --git a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java index b201c6cfaa0..ae550fd6365 100644 --- a/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java +++ b/jena-core/src/main/java/org/apache/jena/mem/store/roaring/strategies/EagerStoreStrategy.java @@ -97,8 +97,7 @@ public EagerStoreStrategy(final TripleSet triples, EagerStoreStrategy strategyTo */ private void indexAll() { // Initialize the index by adding all triples to the index - triples.indexedKeyIterator().forEachRemaining(entry -> - addToIndex(entry.key(), entry.index())); + triples.forEachKey(this::addToIndex); } /** @@ -108,15 +107,15 @@ private void indexAll() { */ private void indexAllParallel() { final var futureIndexSubjects = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[0], entry.key().getSubject(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[0], triple.getSubject(), index))); final var futureIndexPredicates = CompletableFuture.runAsync(() -> - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[1], entry.key().getPredicate(), entry.index()))); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[1], triple.getPredicate(), index))); - triples.indexedKeyIterator().forEachRemaining(entry -> - addIndex(spoBitmaps[2], entry.key().getObject(), entry.index())); + triples.forEachKey((triple, index) -> + addIndex(spoBitmaps[2], triple.getObject(), index)); CompletableFuture.allOf(futureIndexSubjects, futureIndexPredicates).join(); } diff --git a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java index 5659e6a20c8..b75b60c6e98 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/AbstractGraphMemTest.java @@ -37,6 +37,8 @@ import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import java.util.ArrayList; + public abstract class AbstractGraphMemTest { protected GraphMem sut; @@ -1044,4 +1046,34 @@ public void testCopyHasNoSideEffects() { assertFalse(sut.contains(triple("s3 p3 o3"))); } + @Test + public void testDeleteAll() { + for(var subjects=1; subjects <= 8 ; subjects++) { + for(var predicates=1; predicates <= 8 ; predicates++) { + for(var objects=1; objects <= 8 ; objects++) { + sut = createGraph(); + var triples = new ArrayList(); + for(var s=0; s < subjects ; s++) { + for(var p=0; p < predicates ; p++) { + for(var o=0; o < objects ; o++) { + var t = triple("s" + s + " p" + p + " o" + o); + triples.add(t); + sut.add(t); + assertTrue(sut.contains(t)); + } + } + } + assertEquals(subjects*predicates*objects, sut.size()); + // print subjects, predicates, objects and size + // System.out.println(subjects + " - " + predicates + " - " + objects + " : " + sut.size()); + for (var triple : triples) { + assertTrue(sut.contains(triple)); + sut.delete(triple); + assertFalse(sut.contains(triple)); + } + assertEquals(0, sut.size()); + } + } + } + } } diff --git a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java index 868a79a34d8..95546c3f4b8 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/GraphMemFastTest.java @@ -21,10 +21,33 @@ package org.apache.jena.mem; +import org.junit.Test; + +import static org.apache.jena.testing_framework.GraphHelper.triple; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Concrete instantiation of {@link AbstractGraphMemTest} that exercises + * {@link GraphMemFast} (a {@link GraphMem} backed by a + * {@link org.apache.jena.mem.store.fast.FastTripleStore}). The shared + * contract assertions live in the abstract base; this class only adds tests + * that are specific to the {@code GraphMemFast} variant. + */ public class GraphMemFastTest extends AbstractGraphMemTest { @Override protected GraphMem createGraph() { return new GraphMemFast(); } + + @Test + public void copyReturnsAGraphMemFastInstance() { + sut.add(triple("s p o")); + final var copy = sut.copy(); + // The override on GraphMemFast must preserve the runtime type so + // callers don't lose subclass-specific functionality through copy(). + assertTrue("copy() must return a GraphMemFast", copy instanceof GraphMemFast); + assertNotSame(sut, copy); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java index 4fecf61d8a6..65320b99733 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java +++ b/jena-core/src/test/java/org/apache/jena/mem/TS4_GraphMem.java @@ -30,6 +30,7 @@ import org.apache.jena.mem.spliterator.SparseArraySpliteratorTest; import org.apache.jena.mem.spliterator.SparseArraySubSpliteratorTest; import org.apache.jena.mem.store.fast.FastArrayBunchTest; +import org.apache.jena.mem.store.fast.FastHashedBunchMapTest; import org.apache.jena.mem.store.fast.FastHashedTripleBunchTest; import org.apache.jena.mem.store.fast.FastTripleStoreTest; import org.apache.jena.mem.store.legacy.*; @@ -62,8 +63,10 @@ // store/fast FastTripleStoreTest.class, FastArrayBunchTest.class, + FastHashedBunchMapTest.class, FastHashedTripleBunchTest.class, + // store/roaring RoaringTripleStoreTest.class, RoaringBitmapTripleIteratorTest.class, diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java index ba9d9c15b09..c3ae6f94a20 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaMapNodeTest.java @@ -171,14 +171,14 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test public void testValueIteratorEmpty2() { var iter = sut.valueIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -577,7 +577,7 @@ public void tryPut1000Nodes() { @Test public void computeIfAbsend1000Nodes() { for (int i = 0; i < 1000; i++) { - sut.computeIfAbsent(node("s" + i), () -> new Object()); + sut.computeIfAbsent(node("s" + i), Object::new); } assertEquals(1000, sut.size()); } @@ -613,38 +613,4 @@ public void tryPutAndTryRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonNodeMap extends HashCommonMap { - public HashCommonNodeMap() { - super(10); - } - - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - public void clear() { - super.clear(10); - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } - - private static class FastNodeHashMap extends FastHashMap { - @Override - protected Node[] newKeysArray(int size) { - return new Node[size]; - } - - @Override - protected Object[] newValuesArray(int size) { - return new Object[size]; - } - } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java index 1cd962e2d56..edaf60e26e2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/AbstractJenaSetTripleTest.java @@ -106,7 +106,7 @@ public void testContainKey() { public void testKeyIteratorEmpty() { var iter = sut.keyIterator(); assertFalse(iter.hasNext()); - assertThrows(NoSuchElementException.class, () -> iter.next()); + assertThrows(NoSuchElementException.class, iter::next); } @Test @@ -114,7 +114,7 @@ public void testKeyIteratorNextThrowsConcurrentModificationException() { sut.tryAdd(triple("s o p")); var iter = sut.keyIterator(); sut.tryAdd(triple("s o p2")); - assertThrows(ConcurrentModificationException.class, () -> iter.next()); + assertThrows(ConcurrentModificationException.class, iter::next); } @Test @@ -362,29 +362,4 @@ public void addAndRemove1000Triples() { } assertTrue(sut.isEmpty()); } - - - private static class HashCommonTripleSet extends HashCommonSet { - public HashCommonTripleSet() { - super(10); - } - - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - - @Override - public void clear() { - super.clear(10); - } - } - - private static class FastTripleHashSet extends FastHashSet { - @Override - protected Triple[] newKeysArray(int size) { - return new Triple[size]; - } - } - } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java index f098548b3b3..d932ce23208 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashMapTest2.java @@ -24,9 +24,11 @@ import org.junit.Test; import java.util.function.UnaryOperator; +import java.util.HashMap; +import java.util.HashSet; import static org.apache.jena.testing_framework.GraphHelper.node; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class FastHashMapTest2 { @@ -113,6 +115,69 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { assertEquals(2, (int) original.get(node("s2"))); } + @Test + public void testPutAndGetIndexAssignsSequentialIndicesAndReturnsExistingForRepeats() { + var sut = new FastNodeHashMap(); + // First-time puts assign new indices. + final int i0 = sut.putAndGetIndex(node("s"), 100); + final int i1 = sut.putAndGetIndex(node("s1"), 200); + final int i2 = sut.putAndGetIndex(node("s2"), 300); + assertEquals(0, i0); + assertEquals(1, i1); + assertEquals(2, i2); + assertEquals(100, (int) sut.getValueAt(i0)); + assertEquals(200, (int) sut.getValueAt(i1)); + assertEquals(300, (int) sut.getValueAt(i2)); + } + + @Test + public void testPutAndGetIndexOverwritesValueForExistingKey() { + var sut = new FastNodeHashMap(); + final int i0 = sut.putAndGetIndex(node("s"), 100); + // Re-putting the same key returns the SAME index but with the new value. + final int i0Again = sut.putAndGetIndex(node("s"), 999); + assertEquals(i0, i0Again); + assertEquals(999, (int) sut.get(node("s"))); + assertEquals(1, sut.size()); + } + + @Test + public void testForEachKeyVisitsEveryEntryWithItsIndex() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + + final HashMap seen = new HashMap<>(); + sut.forEachKey(seen::put); + + assertEquals(3, seen.size()); + assertEquals(Integer.valueOf(0), seen.get(node("a"))); + assertEquals(Integer.valueOf(1), seen.get(node("b"))); + assertEquals(Integer.valueOf(2), seen.get(node("c"))); + } + + @Test + public void testForEachKeySkipsRemovedSlots() { + var sut = new FastNodeHashMap(); + sut.putAndGetIndex(node("a"), 0); + sut.putAndGetIndex(node("b"), 1); + sut.putAndGetIndex(node("c"), 2); + sut.tryRemove(node("b")); + + final HashSet visited = new HashSet<>(); + sut.forEachKey((k, i) -> visited.add(k)); + assertEquals(2, visited.size()); + assertTrue(visited.contains(node("a"))); + assertTrue(visited.contains(node("c"))); + } + + @Test + public void testForEachKeyOnEmptyMapIsNoOp() { + var sut = new FastNodeHashMap(); + sut.forEachKey((k, i) -> fail("consumer must not be called on an empty map")); + } + private static class FastNodeHashMap extends FastHashMap { public FastNodeHashMap() { diff --git a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java index 5841491b892..1fc61f1a578 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java +++ b/jena-core/src/test/java/org/apache/jena/mem/collection/FastHashSetTest2.java @@ -23,8 +23,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.List; import static org.apache.jena.testing_framework.GraphHelper.node; @@ -55,13 +53,33 @@ public void testAddAndGetIndex() { @Test public void testAddAndGetIndexWithSameHashCode() { - assertEquals(0, sut.addAndGetIndex("a", 0)); - assertEquals(1, sut.addAndGetIndex("b", 0)); - assertEquals(2, sut.addAndGetIndex("c", 0)); + final var a = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var b = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + final var c = new Object() { + @Override + public int hashCode() { + return 0; + } + }; + + var objectHashSet = new FastObjectHashSet(); + assertEquals(0, objectHashSet.addAndGetIndex(a)); + assertEquals(1, objectHashSet.addAndGetIndex(b)); + assertEquals(2, objectHashSet.addAndGetIndex(c)); - assertEquals(~0, sut.addAndGetIndex("a", 0)); - assertEquals(~1, sut.addAndGetIndex("b", 0)); - assertEquals(~2, sut.addAndGetIndex("c", 0)); + assertEquals(~0, objectHashSet.addAndGetIndex(a)); + assertEquals(~1, objectHashSet.addAndGetIndex(b)); + assertEquals(~2, objectHashSet.addAndGetIndex(c)); } @Test @@ -188,127 +206,44 @@ public void testCopyConstructorAddAndDeleteHasNoSideEffects() { } @Test - public void testindexedKeyIterator() { + public void testForEachKeyVisitsEveryKeyWithItsIndex() { var items = List.of("a", "b", "c", "d", "e"); - sut = new FastStringHashSet(3); for (String item : items) { sut.addAndGetIndex(item); } - var iterator = sut.indexedKeyIterator(); - for (var i=0; i seen = new java.util.HashMap<>(); + sut.forEachKey(seen::put); - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var iterator = sut.indexedKeySpliterator(); - for (var i=0; i { - assertEquals(items.get(index), indexedKey.key()); - assertEquals(index, indexedKey.index()); - })); - } - assertFalse(iterator.tryAdvance(indexedKey -> { - fail("There should be no more elements in the iterator"); - })); - } - - @Test - public void testIndexedKeyStream() { - var items = List.of("a", "b", "c", "d", "e"); - - sut = new FastStringHashSet(3); - for (String item : items) { - sut.addAndGetIndex(item); - } - - var indexedKeys = sut.indexedKeyStream().toList(); - assertEquals(items.size(), indexedKeys.size()); - for (var i=0; i(); - for (var i = 0; i < 1000; i++) { - items.add(i); - checkSum+= i; - } - - sut = new FastStringHashSet(); - for (var value : items) { - sut.addAndGetIndex(value.toString()); - } - - final var sum = sut.indexedKeyStreamParallel() - .map(pair -> Integer.parseInt(pair.key())) - .reduce(0, Integer::sum); - assertEquals(checkSum, sum); - } - - @Test - public void testIndexedKeySpliteratorAdvanceThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> { - })); - } - - @Test - public void testIndexedKeySpliteratorForEachRemainingThrowsConcurrentModificationException() { - sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeySpliterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); - } - - @Test - public void testIndexedKeyIteratorForEachRemainingThrowsConcurrentModificationException() { + public void testForEachKeyOnEmptySetIsNoOp() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(t -> { - })); + sut.forEachKey((k, i) -> fail("consumer must not be called on empty set")); } @Test - public void testIndexedKeyIteratorNextThrowsConcurrentModificationException() { + public void testForEachKeySkipsRemovedSlots() { sut = new FastStringHashSet(3); - sut.tryAdd("a"); - var spliterator = sut.indexedKeyIterator(); - sut.tryAdd("b"); - assertThrows(ConcurrentModificationException.class, spliterator::next); + sut.addAndGetIndex("a"); + sut.addAndGetIndex("b"); + sut.addAndGetIndex("c"); + // Remove the middle entry; forEachKey must not visit the freed slot. + sut.tryRemove("b"); + + final var keys = new java.util.ArrayList(); + sut.forEachKey((k, i) -> keys.add(k)); + assertEquals(2, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("c")); + assertFalse(keys.contains("b")); } private static class FastObjectHashSet extends FastHashSet { diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java deleted file mode 100644 index cfffcf93904..00000000000 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIndexedIteratorTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.apache.jena.mem.iterator; - -import org.junit.Test; - -import java.util.NoSuchElementException; - -import static org.junit.Assert.*; - -public class SparseArrayIndexedIteratorTest { - - private SparseArrayIndexedIterator iterator; - - @Test - public void testHasNextAndNextWithNonNullEntries() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint3() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 3, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint2() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 2, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(1, entry.index()); - assertEquals("second", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint1() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 1, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testConstrucorWithToIndexConstraint0() { - String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIndexedIterator<>(entries, 0, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testHasNextAndNextWithNullEntries() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertTrue(iterator.hasNext()); - var entry = iterator.next(); - assertEquals(0, entry.index()); - assertEquals("first", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(2, entry.index()); - assertEquals("third", entry.key()); - - assertTrue(iterator.hasNext()); - entry = iterator.next(); - assertEquals(4, entry.index()); - assertEquals("fifth", entry.key()); - - assertFalse(iterator.hasNext()); - } - - @Test - public void testHasNextAndNextWithNoElements() { - String[] entries = new String[]{}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - - assertFalse(iterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> iterator.next()); - } - - @Test - public void testForEachRemaining() { - String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIndexedIterator<>(entries, () -> { - }); - int[] count = new int[]{0}; - iterator.forEachRemaining(entry -> { - assertNotNull(entry); - count[0]++; - }); - assertEquals(3, count[0]); - } -} - diff --git a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java index d93d8a3beb2..393e3019cb2 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/iterator/SparseArrayIteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.iterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.NoSuchElementException; @@ -28,13 +30,19 @@ public class SparseArrayIteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + private SparseArrayIterator iterator; @Test public void testHasNextAndNextWithNonNullEntries() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -48,8 +56,7 @@ public void testHasNextAndNextWithNonNullEntries() { @Test public void testConstrucorWithToIndexConstraint3() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 3, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 3, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("third", iterator.next()); @@ -63,8 +70,7 @@ public void testConstrucorWithToIndexConstraint3() { @Test public void testConstrucorWithToIndexConstraint2() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 2, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 2, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("second", iterator.next()); @@ -76,8 +82,7 @@ public void testConstrucorWithToIndexConstraint2() { @Test public void testConstrucorWithToIndexConstraint1() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 1, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 1, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("first", iterator.next()); @@ -87,8 +92,7 @@ public void testConstrucorWithToIndexConstraint1() { @Test public void testConstrucorWithToIndexConstraint0() { String[] entries = new String[]{"first", "second", "third"}; - iterator = new SparseArrayIterator<>(entries, 0, () -> { - }); + iterator = new SparseArrayIterator<>(entries, 0, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -97,8 +101,7 @@ public void testConstrucorWithToIndexConstraint0() { @Test public void testHasNextAndNextWithNullEntries() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertTrue(iterator.hasNext()); assertEquals("fifth", iterator.next()); @@ -112,8 +115,7 @@ public void testHasNextAndNextWithNullEntries() { @Test public void testHasNextAndNextWithNoElements() { String[] entries = new String[]{}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, () -> iterator.next()); @@ -122,8 +124,7 @@ public void testHasNextAndNextWithNoElements() { @Test public void testForEachRemaining() { String[] entries = new String[]{"first", null, "third", null, "fifth"}; - iterator = new SparseArrayIterator<>(entries, () -> { - }); + iterator = new SparseArrayIterator<>(entries, dummySetForConcurrencyCheck); int[] count = new int[]{0}; iterator.forEachRemaining(entry -> { assertNotNull(entry); diff --git a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java index 23643c6da4f..99e1562d530 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/pattern/PatternClassifierTest.java @@ -20,16 +20,29 @@ */ package org.apache.jena.mem.pattern; +import org.apache.jena.graph.Node; import org.junit.Test; import static org.apache.jena.testing_framework.GraphHelper.node; import static org.apache.jena.testing_framework.GraphHelper.triple; import static org.junit.Assert.assertEquals; +/** + * Unit tests for {@link PatternClassifier}: maps a triple match into one of + * the eight {@link MatchPattern} buckets based on which of the subject, + * predicate and object slots are concrete and which are wildcards. + * + * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator
+ * Both classification overloads are tested for every combination of + * concrete/wildcard slots, and the {@code (Node, Node, Node)} overload is + * additionally tested with explicit {@code null} arguments and + * {@link Node#ANY}, both of which must be treated as wildcards. + */ public class PatternClassifierTest { @Test - public void testClassifyTriple() { + public void classifyTripleCoversAllEightCombinations() { + // The wildcard "??" is parsed as a non-concrete (variable) node by + // the test helper. assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(triple("s p o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(triple("s p ??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(triple("s ?? o"))); @@ -41,7 +54,7 @@ public void testClassifyTriple() { } @Test - public void testClassifyNodes() { + public void classifyNodesCoversAllEightCombinations() { assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), node("??"))); assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), node("??"), node("o"))); @@ -52,4 +65,26 @@ public void testClassifyNodes() { assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(node("??"), node("??"), node("??"))); } + @Test + public void classifyNodesTreatsNullAsWildcard() { + // The graph-find contract allows callers to pass null for a slot + // they don't care about; the classifier must handle that without NPE. + assertEquals(MatchPattern.SUB_PRE_OBJ, PatternClassifier.classify(node("s"), node("p"), node("o"))); + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), null)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), null, node("o"))); + assertEquals(MatchPattern.SUB_ANY_ANY, PatternClassifier.classify(node("s"), null, null)); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(null, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_PRE_ANY, PatternClassifier.classify(null, node("p"), null)); + assertEquals(MatchPattern.ANY_ANY_OBJ, PatternClassifier.classify(null, null, node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(null, null, null)); + } + + @Test + public void classifyNodesTreatsNodeAnyAsWildcard() { + // Node.ANY is the standard wildcard sentinel used by Graph.find. + assertEquals(MatchPattern.SUB_PRE_ANY, PatternClassifier.classify(node("s"), node("p"), Node.ANY)); + assertEquals(MatchPattern.SUB_ANY_OBJ, PatternClassifier.classify(node("s"), Node.ANY, node("o"))); + assertEquals(MatchPattern.ANY_PRE_OBJ, PatternClassifier.classify(Node.ANY, node("p"), node("o"))); + assertEquals(MatchPattern.ANY_ANY_ANY, PatternClassifier.classify(Node.ANY, Node.ANY, Node.ANY)); + } } \ No newline at end of file diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java index 4fe0d7382f2..ae2afc7fd47 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySpliteratorTest.java @@ -20,6 +20,9 @@ */ package org.apache.jena.mem.spliterator; + +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +33,113 @@ public class ArraySpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +150,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +161,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -207,8 +172,7 @@ public void trySplitFour() { @Test public void trySplitFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(5, 6, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -224,8 +188,7 @@ public void trySplitOneHundred() { array[i] = i; } } - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertEquals(array.length, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -241,56 +204,49 @@ private void assertBetween(long min, long max, long estimateSize) { @Test public void estimateSizeZero() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(0, 1, spliterator.estimateSize()); } @Test public void estimateSizeOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(1, 2, spliterator.estimateSize()); } @Test public void estimateSizeTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(2, 3, spliterator.estimateSize()); } @Test public void estimateSizeFive() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertBetween(5, 6, spliterator.estimateSize()); } @Test public void characteristics() { Integer[] array = new Integer[]{1, 2, 3, 4, 5}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertEquals(DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE, spliterator.characteristics()); } @Test public void splitWithOneElementNull() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void splitWithOneRemainingElementNull() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySpliterator<>(array, dummySetForConcurrencyCheck); spliterator.tryAdvance((i) -> { }); assertNull(spliterator.trySplit()); diff --git a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java index f8d44700da9..696b6be7be9 100644 --- a/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java +++ b/jena-core/src/test/java/org/apache/jena/mem/spliterator/ArraySubSpliteratorTest.java @@ -20,6 +20,8 @@ */ package org.apache.jena.mem.spliterator; +import org.apache.jena.mem.collection.FastHashSet; +import org.apache.jena.mem.collection.JenaSet; import org.junit.Test; import java.util.ArrayList; @@ -30,149 +32,113 @@ public class ArraySubSpliteratorTest { + private static final JenaSet dummySetForConcurrencyCheck = new FastHashSet<>() { + @Override + protected Object[] newKeysArray(int size) { + return new Object[size]; + } + }; + @Test public void tryAdvanceEmpty() { - { - Integer[] array = new Integer[0]; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - assertFalse(spliterator.tryAdvance((i) -> { - fail("Should not have advanced"); - })); - } + Integer[] array = new Integer[0]; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + assertFalse(spliterator.tryAdvance((i) -> fail("Should not have advanced"))); } @Test public void tryAdvanceOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(1); - })); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance((i) -> itemsFound.add(1))); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void tryAdvanceTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void tryAdvanceThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - while (spliterator.tryAdvance((i) -> { - itemsFound.add(i); - })); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + while (spliterator.tryAdvance(itemsFound::add)); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void forEachRemainingEmpty() { - { - Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(0, itemsFound.size()); - } + Integer[] array = new Integer[]{}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(0, itemsFound.size()); } @Test public void forEachRemainingOne() { - { - Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(1, itemsFound.size()); - itemsFound.contains(1); - } + Integer[] array = new Integer[]{1}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(1, itemsFound.size()); + assertTrue(itemsFound.contains(1)); } @Test public void forEachRemainingTwo() { - { - Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(2, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - } + Integer[] array = new Integer[]{1, 2}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(2, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); } @Test public void forEachRemainingThree() { - { - Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); - var itemsFound = new ArrayList<>(); - spliterator.forEachRemaining((i) -> { - itemsFound.add(i); - }); - assertEquals(3, itemsFound.size()); - itemsFound.contains(1); - itemsFound.contains(2); - itemsFound.contains(3); - } + Integer[] array = new Integer[]{1, 2, 3}; + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); + var itemsFound = new ArrayList<>(); + spliterator.forEachRemaining(itemsFound::add); + assertEquals(3, itemsFound.size()); + assertTrue(itemsFound.contains(1)); + assertTrue(itemsFound.contains(2)); + assertTrue(itemsFound.contains(3)); } @Test public void trySplitEmpty() { Integer[] array = new Integer[]{}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitOne() { Integer[] array = new Integer[]{1}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); assertNull(spliterator.trySplit()); } @Test public void trySplitTwo() { Integer[] array = new Integer[]{1, 2}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(2, 3, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -183,8 +149,7 @@ public void trySplitTwo() { @Test public void trySplitThree() { Integer[] array = new Integer[]{1, 2, 3}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(3, 4, spliterator.estimateSize()); Spliterator split = spliterator.trySplit(); @@ -195,8 +160,7 @@ public void trySplitThree() { @Test public void trySplitFour() { Integer[] array = new Integer[]{1, 2, 3, 4}; - Spliterator spliterator = new ArraySubSpliterator<>(array, () -> { - }); + Spliterator spliterator = new ArraySubSpliterator<>(array, dummySetForConcurrencyCheck); // Estimated size is not exact assertBetween(4, 5, spliterator.estimateSize()); Spliterator