From 9bbd4423c27410cfbbbf456d3b8dbf69120e1a9b Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 26 Oct 2026 11:17:23 -0400 Subject: [PATCH] Allocate FlatHashtable tables with an array constructor, not reflection FlatHashtable minted its tables with Array.newInstance(entryClass, n), and grew them with Array.newInstance(table.getClass().getComponentType(), n), to keep a precise component type: typed reads, real array-store checks and a precise element type for the JIT. GraalVM 17's native-image rejects that call unless each entry array type is registered for reflection; the failure surfaces in a class initializer and poisons the class for the life of the process. The factories now take an array constructor instead of a Class, last per the functional-parameter convention, e.g. FlatHashtable.D1.createFixed(capacity, MyEntry[]::new). An array constructor reference compiles to a plain new MyEntry[n], so the table keeps the same precise runtime type with no reflection. D1 and D2 keep the constructor to grow, and the raw core's resize / resizingInsert take it too, so growth stays precisely typed as well. Co-Authored-By: Claude Opus 5.5 --- .../util/CaseInsensitiveMapBenchmark.java | 2 +- .../util/FlatHashtableIteratorBenchmark.java | 2 +- .../util/SingleThreadedMapBenchmark.java | 2 +- .../datadog/trace/util/FlatHashtable.java | 141 ++++++++++-------- .../trace/util/FlatHashtableD1Test.java | 6 +- .../trace/util/FlatHashtableD2Test.java | 6 +- .../datadog/trace/util/FlatHashtableTest.java | 75 ++++++---- 7 files changed, 131 insertions(+), 103 deletions(-) diff --git a/internal-api/src/jmh/java/datadog/trace/util/CaseInsensitiveMapBenchmark.java b/internal-api/src/jmh/java/datadog/trace/util/CaseInsensitiveMapBenchmark.java index c72a26430af..96d81b91095 100644 --- a/internal-api/src/jmh/java/datadog/trace/util/CaseInsensitiveMapBenchmark.java +++ b/internal-api/src/jmh/java/datadog/trace/util/CaseInsensitiveMapBenchmark.java @@ -293,7 +293,7 @@ public long hashOf(CIEntry entry) { static CIEntry[] _create_flat(float loadFactor) { // 16 distinct case-insensitive keys (foo-0..quux-3). CIEntry[] table = - FlatHashtable.create(CIEntry.class, PREFIXES.length * NUM_SUFFIXES, loadFactor); + FlatHashtable.create(PREFIXES.length * NUM_SUFFIXES, loadFactor, CIEntry[]::new); for (int suffix = 0; suffix < NUM_SUFFIXES; ++suffix) { for (String prefix : PREFIXES) { String key = prefix + "-" + suffix; diff --git a/internal-api/src/jmh/java/datadog/trace/util/FlatHashtableIteratorBenchmark.java b/internal-api/src/jmh/java/datadog/trace/util/FlatHashtableIteratorBenchmark.java index 43d928176b1..ef5b060f207 100644 --- a/internal-api/src/jmh/java/datadog/trace/util/FlatHashtableIteratorBenchmark.java +++ b/internal-api/src/jmh/java/datadog/trace/util/FlatHashtableIteratorBenchmark.java @@ -120,7 +120,7 @@ public long hashOf(ItEntry entry) { @Setup(Level.Trial) public void setUp() { // Sparse so the bucket is one contiguous run with a clean terminating empty slot. - table = FlatHashtable.create(ItEntry.class, BUCKET_SIZE, FlatHashtable.LOW_LOAD_FACTOR); + table = FlatHashtable.create(BUCKET_SIZE, FlatHashtable.LOW_LOAD_FACTOR, ItEntry[]::new); for (int i = 0; i < BUCKET_SIZE; ++i) { FlatHashtable.insert(table, new ItEntry(BUCKET_HASH, i)); // all share the hash => one bucket } diff --git a/internal-api/src/jmh/java/datadog/trace/util/SingleThreadedMapBenchmark.java b/internal-api/src/jmh/java/datadog/trace/util/SingleThreadedMapBenchmark.java index bd3c0e83cc6..3ab316d25db 100644 --- a/internal-api/src/jmh/java/datadog/trace/util/SingleThreadedMapBenchmark.java +++ b/internal-api/src/jmh/java/datadog/trace/util/SingleThreadedMapBenchmark.java @@ -209,7 +209,7 @@ public long hashOf(IntEntry entry) { static IntEntry[] newFilledFlat() { // Sized to the key count (FlatHashtable is fixed-capacity, no resize): load factor <= 0.5. - IntEntry[] table = FlatHashtable.create(IntEntry.class, INSERTION_KEYS.length); + IntEntry[] table = FlatHashtable.create(INSERTION_KEYS.length, IntEntry[]::new); for (int i = 0; i < INSERTION_KEYS.length; ++i) { FlatHashtable.insert(table, new IntEntry(INSERTION_KEYS[i], i), IntEntryKeyStrategy.INSTANCE); } diff --git a/internal-api/src/main/java/datadog/trace/util/FlatHashtable.java b/internal-api/src/main/java/datadog/trace/util/FlatHashtable.java index 285d725f0b5..7da805f14fd 100644 --- a/internal-api/src/main/java/datadog/trace/util/FlatHashtable.java +++ b/internal-api/src/main/java/datadog/trace/util/FlatHashtable.java @@ -2,13 +2,13 @@ import datadog.trace.api.function.Strategy; import datadog.trace.api.function.StrategyConsumer; -import java.lang.reflect.Array; import java.util.Arrays; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Objects; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.IntFunction; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -166,12 +166,19 @@ public static long hash(@Nullable Object key) { private final boolean growable; private final float loadFactor; + private final IntFunction newArray; // mints a grown table of the precise type private TEntry[] table; private int size; private int limit; // grow trigger when growable; hard cap when fixed - private D1(TEntry[] table, float loadFactor, boolean growable, int limit) { + private D1( + TEntry[] table, + float loadFactor, + boolean growable, + int limit, + IntFunction newArray) { this.growable = growable; + this.newArray = newArray; this.loadFactor = loadFactor; this.table = table; this.size = 0; @@ -184,14 +191,15 @@ private D1(TEntry[] table, float loadFactor, boolean growable, int limit) { */ @Nonnull public static > D1 createFixed( - @Nonnull Class type, int maxCapacity) { - return createFixed(type, maxCapacity, DEFAULT_LOAD_FACTOR); + int maxCapacity, @Nonnull IntFunction newArray) { + return createFixed(maxCapacity, DEFAULT_LOAD_FACTOR, newArray); } @Nonnull public static > D1 createFixed( - @Nonnull Class type, int maxCapacity, float loadFactor) { - return new D1<>(create(type, maxCapacity, loadFactor), loadFactor, false, maxCapacity); + int maxCapacity, float loadFactor, @Nonnull IntFunction newArray) { + return new D1<>( + create(maxCapacity, loadFactor, newArray), loadFactor, false, maxCapacity, newArray); } /** @@ -200,15 +208,15 @@ public static > D1 createFixed( */ @Nonnull public static > D1 createGrowable( - @Nonnull Class type, int initialCapacity) { - return createGrowable(type, initialCapacity, DEFAULT_LOAD_FACTOR); + int initialCapacity, @Nonnull IntFunction newArray) { + return createGrowable(initialCapacity, DEFAULT_LOAD_FACTOR, newArray); } @Nonnull public static > D1 createGrowable( - @Nonnull Class type, int initialCapacity, float loadFactor) { - E[] table = create(type, initialCapacity, loadFactor); - return new D1<>(table, loadFactor, true, (int) (table.length * loadFactor)); + int initialCapacity, float loadFactor, @Nonnull IntFunction newArray) { + E[] table = create(initialCapacity, loadFactor, newArray); + return new D1<>(table, loadFactor, true, (int) (table.length * loadFactor), newArray); } public int size() { @@ -302,7 +310,7 @@ public boolean insert(@Nonnull TEntry entry) { } private void grow() { - table = resize(table); + table = resize(table, newArray); limit = (int) (table.length * loadFactor); } @@ -378,12 +386,19 @@ public static long hash(@Nullable Object key1, @Nullable Object key2) { private final boolean growable; private final float loadFactor; + private final IntFunction newArray; // mints a grown table of the precise type private TEntry[] table; private int size; private int limit; // grow trigger when growable; hard cap when fixed - private D2(TEntry[] table, float loadFactor, boolean growable, int limit) { + private D2( + TEntry[] table, + float loadFactor, + boolean growable, + int limit, + IntFunction newArray) { this.growable = growable; + this.newArray = newArray; this.loadFactor = loadFactor; this.table = table; this.size = 0; @@ -396,14 +411,15 @@ private D2(TEntry[] table, float loadFactor, boolean growable, int limit) { */ @Nonnull public static > D2 createFixed( - @Nonnull Class type, int maxCapacity) { - return createFixed(type, maxCapacity, DEFAULT_LOAD_FACTOR); + int maxCapacity, @Nonnull IntFunction newArray) { + return createFixed(maxCapacity, DEFAULT_LOAD_FACTOR, newArray); } @Nonnull public static > D2 createFixed( - @Nonnull Class type, int maxCapacity, float loadFactor) { - return new D2<>(create(type, maxCapacity, loadFactor), loadFactor, false, maxCapacity); + int maxCapacity, float loadFactor, @Nonnull IntFunction newArray) { + return new D2<>( + create(maxCapacity, loadFactor, newArray), loadFactor, false, maxCapacity, newArray); } /** @@ -412,15 +428,15 @@ public static > D2 createFixed( */ @Nonnull public static > D2 createGrowable( - @Nonnull Class type, int initialCapacity) { - return createGrowable(type, initialCapacity, DEFAULT_LOAD_FACTOR); + int initialCapacity, @Nonnull IntFunction newArray) { + return createGrowable(initialCapacity, DEFAULT_LOAD_FACTOR, newArray); } @Nonnull public static > D2 createGrowable( - @Nonnull Class type, int initialCapacity, float loadFactor) { - E[] table = create(type, initialCapacity, loadFactor); - return new D2<>(table, loadFactor, true, (int) (table.length * loadFactor)); + int initialCapacity, float loadFactor, @Nonnull IntFunction newArray) { + E[] table = create(initialCapacity, loadFactor, newArray); + return new D2<>(table, loadFactor, true, (int) (table.length * loadFactor), newArray); } public int size() { @@ -520,7 +536,7 @@ public boolean insert(@Nonnull TEntry entry) { } private void grow() { - table = resize(table); + table = resize(table, newArray); limit = (int) (table.length * loadFactor); } @@ -700,27 +716,28 @@ public static int capacityFor(int cardinalityLimit, float loadFactor) { } /** - * Allocates a correctly-typed table for a cardinality budget ({@link #capacityFor} slots). - * Passing {@code type} makes the array's runtime component type {@code E} rather than {@code - * Object[]} — typed reads, real array-store checks, and a monomorphic element type for the JIT. - * Callers can't {@code new E[]} themselves under erasure; this does the one reflective allocation - * at construction (off any hot path). Note: this {@code create} mints the backing array; {@link - * CreateStrategy#create} mints an entry — different types, no ambiguity at the call site. + * Allocates a table for a cardinality budget ({@link #capacityFor} slots). {@code newArray} is an + * array constructor of the entry type, e.g. {@code MyEntry[]::new}, so the table's runtime + * component type is {@code E} rather than {@code Object[]}: typed reads, real array-store checks, + * and a precise element type for the JIT. Unlike {@code Array.newInstance}, an array constructor + * isn't reflective, so the table works unchanged under GraalVM native-image with no reflection + * registration. Note: this {@code create} mints the backing array; {@link CreateStrategy#create} + * mints an entry -- different types, no ambiguity at the call site. */ - @SuppressWarnings("unchecked") @Nonnull - public static E[] create(@Nonnull Class type, int cardinalityLimit) { - return (E[]) Array.newInstance(type, capacityFor(cardinalityLimit)); + public static E[] create(int cardinalityLimit, @Nonnull IntFunction newArray) { + return newArray.apply(capacityFor(cardinalityLimit)); } /** - * {@link #create(Class, int)} at an explicit {@code loadFactor} (see {@link #capacityFor(int, - * float)} and the {@link #DEFAULT_LOAD_FACTOR} / {@link #LOW_LOAD_FACTOR} constants). + * {@link #create(int, IntFunction)} at an explicit {@code loadFactor} (see {@link + * #capacityFor(int, float)} and the {@link #DEFAULT_LOAD_FACTOR} / {@link #LOW_LOAD_FACTOR} + * constants). */ - @SuppressWarnings("unchecked") @Nonnull - public static E[] create(@Nonnull Class type, int cardinalityLimit, float loadFactor) { - return (E[]) Array.newInstance(type, capacityFor(cardinalityLimit, loadFactor)); + public static E[] create( + int cardinalityLimit, float loadFactor, @Nonnull IntFunction newArray) { + return newArray.apply(capacityFor(cardinalityLimit, loadFactor)); } /** @@ -843,12 +860,13 @@ static int home(long hash, int mask) { * Doubles capacity and rehashes every entry into a new table — call when {@link #insert} returns * {@code false} and you want to grow rather than reject; the caller stores the returned array * back. Convenience over the {@link HashStrategy}-taking overload for {@link Entry}-based entries - * (the home comes from {@link Entry#hash}). See {@link #resizingInsert(Entry[], Entry)} to do - * both in one call, and its note on growing over unbounded key domains. + * (the home comes from {@link Entry#hash}). See {@link #resizingInsert(Entry[], Entry, + * IntFunction)} to do both in one call, and its note on growing over unbounded key domains. */ @Nonnull - public static E[] resize(@Nonnull E[] table) { - E[] grown = allocateGrown(table); + public static E[] resize( + @Nonnull E[] table, @Nonnull IntFunction newArray) { + E[] grown = newArray.apply(grownLength(table.length)); for (final E e : table) { if (e != null) { placeAt(grown, e, e.hash); @@ -858,13 +876,14 @@ public static E[] resize(@Nonnull E[] table) { } /** - * {@link #resize(Entry[])} for any entry type: each entry's home comes from {@link + * {@link #resize(Entry[], IntFunction)} for any entry type: each entry's home comes from {@link * HashStrategy#hashOf}. Not a {@link StrategyConsumer} — the rehash is a cold, one-off traversal, * not a hot specialization site. */ @Nonnull - public static E[] resize(@Nonnull E[] table, @Nonnull HashStrategy hashStrat) { - E[] grown = allocateGrown(table); + public static E[] resize( + @Nonnull E[] table, @Nonnull HashStrategy hashStrat, @Nonnull IntFunction newArray) { + E[] grown = newArray.apply(grownLength(table.length)); for (final E e : table) { if (e != null) { placeAt(grown, e, hashStrat.hashOf(e)); @@ -873,20 +892,12 @@ public static E[] resize(@Nonnull E[] table, @Nonnull HashStrategy hashSt return grown; } - /** - * A new, empty table of twice the capacity, of the same runtime component type as {@code table}. - */ - @SuppressWarnings("unchecked") - private static E[] allocateGrown(E[] table) { - return (E[]) Array.newInstance(table.getClass().getComponentType(), grownLength(table.length)); - } - /** * {@code currentLength << 1}, guarded against overflow. {@code currentLength << 1} overflows to a * negative value at {@code currentLength == 1 << 30}; without this check the failure surfaces as - * an opaque {@code NegativeArraySizeException} from {@code Array.newInstance} in {@link - * #allocateGrown}. Same overflow class as {@link #capacityFor}'s guard -- this is the growth-time - * counterpart. + * an opaque {@code NegativeArraySizeException} from the array constructor in {@link + * #resize(Entry[], IntFunction)}. Same overflow class as {@link #capacityFor}'s guard -- this is + * the growth-time counterpart. */ static int grownLength(int currentLength) { int grownLength = currentLength << 1; @@ -899,11 +910,11 @@ static int grownLength(int currentLength) { /** * {@link #insert(Entry[], Entry) insert} that grows on demand: adds {@code entry}, {@link - * #resize(Entry[]) resizing} first if the table is full, and returns the table to store back — - * the same array if it fit, a new larger one if it grew: + * #resize(Entry[], IntFunction) resizing} first if the table is full, and returns the table to + * store back — the same array if it fit, a new larger one if it grew: * *
{@code
-   * table = FlatHashtable.resizingInsert(table, entry); // always reassign
+   * table = FlatHashtable.resizingInsert(table, entry, MyEntry[]::new); // always reassign
    * }
* * Same comparison-free, caller-ensures-absence contract as {@link #insert}. @@ -924,25 +935,29 @@ static int grownLength(int currentLength) { * size (as {@link D1} / {@link D2} do) can grow at a load factor of its own choosing instead. */ @Nonnull - public static E[] resizingInsert(@Nonnull E[] table, @Nonnull E entry) { + public static E[] resizingInsert( + @Nonnull E[] table, @Nonnull E entry, @Nonnull IntFunction newArray) { E[] t = table; while (!insert(t, entry)) { - t = resize(t); // one doubling always suffices; the loop is belt-and-braces + t = resize(t, newArray); // one doubling always suffices; the loop is belt-and-braces } return t; } /** - * {@link #resizingInsert(Entry[], Entry)} for any entry type (home via {@link + * {@link #resizingInsert(Entry[], Entry, IntFunction)} for any entry type (home via {@link * HashStrategy#hashOf}). Same grows-unboundedly caution and same probe-to-full growth trigger. */ @StrategyConsumer @Nonnull public static E[] resizingInsert( - @Nonnull E[] table, @Nonnull E entry, @Nonnull HashStrategy hashStrat) { + @Nonnull E[] table, + @Nonnull E entry, + @Nonnull HashStrategy hashStrat, + @Nonnull IntFunction newArray) { E[] t = table; while (!insert(t, entry, hashStrat)) { - t = resize(t, hashStrat); + t = resize(t, hashStrat, newArray); } return t; } diff --git a/internal-api/src/test/java/datadog/trace/util/FlatHashtableD1Test.java b/internal-api/src/test/java/datadog/trace/util/FlatHashtableD1Test.java index 56594c292fc..a28aa8d24b3 100644 --- a/internal-api/src/test/java/datadog/trace/util/FlatHashtableD1Test.java +++ b/internal-api/src/test/java/datadog/trace/util/FlatHashtableD1Test.java @@ -57,11 +57,11 @@ static final class CollidingKeyEntry extends FlatHashtable.D1.Entry growable(int initialCapacity) { - return FlatHashtable.D1.createGrowable(StringIntEntry.class, initialCapacity); + return FlatHashtable.D1.createGrowable(initialCapacity, StringIntEntry[]::new); } private static FlatHashtable.D1 fixed(int maxCapacity) { - return FlatHashtable.D1.createFixed(StringIntEntry.class, maxCapacity); + return FlatHashtable.D1.createFixed(maxCapacity, StringIntEntry[]::new); } @Test @@ -171,7 +171,7 @@ void nullKeyIsPermittedAndDistinctFromAbsent() { void hashCollisionsResolveByEquality() { // Two distinct keys with the same hashCode: the probe must distinguish them via matches(). FlatHashtable.D1 table = - FlatHashtable.D1.createGrowable(CollidingKeyEntry.class, 4); + FlatHashtable.D1.createGrowable(4, CollidingKeyEntry[]::new); CollidingKey k1 = new CollidingKey("first", 17); CollidingKey k2 = new CollidingKey("second", 17); CollidingKeyEntry e1 = new CollidingKeyEntry(k1, 100); diff --git a/internal-api/src/test/java/datadog/trace/util/FlatHashtableD2Test.java b/internal-api/src/test/java/datadog/trace/util/FlatHashtableD2Test.java index 948501185cd..8dcc9fa17e0 100644 --- a/internal-api/src/test/java/datadog/trace/util/FlatHashtableD2Test.java +++ b/internal-api/src/test/java/datadog/trace/util/FlatHashtableD2Test.java @@ -57,11 +57,11 @@ static final class CollidingPairEntry extends FlatHashtable.D2.Entry growable(int initialCapacity) { - return FlatHashtable.D2.createGrowable(PairEntry.class, initialCapacity); + return FlatHashtable.D2.createGrowable(initialCapacity, PairEntry[]::new); } private static FlatHashtable.D2 fixed(int maxCapacity) { - return FlatHashtable.D2.createFixed(PairEntry.class, maxCapacity); + return FlatHashtable.D2.createFixed(maxCapacity, PairEntry[]::new); } @Test @@ -260,7 +260,7 @@ void fixedInsertReturnsFalseWhenFull() { @Test void hashCollisionsResolveByKeyEquality() { FlatHashtable.D2 table = - FlatHashtable.D2.createGrowable(CollidingPairEntry.class, 8); + FlatHashtable.D2.createGrowable(8, CollidingPairEntry[]::new); CollidingKey key1 = new CollidingKey("a", 7); CollidingKey key2 = new CollidingKey("x", 3); CollidingPairEntry e = new CollidingPairEntry(key1, key2, 100); diff --git a/internal-api/src/test/java/datadog/trace/util/FlatHashtableTest.java b/internal-api/src/test/java/datadog/trace/util/FlatHashtableTest.java index 0ecf3712d3f..0b23c758a95 100644 --- a/internal-api/src/test/java/datadog/trace/util/FlatHashtableTest.java +++ b/internal-api/src/test/java/datadog/trace/util/FlatHashtableTest.java @@ -162,6 +162,17 @@ public long hashOf(TestEntry entry) { } } + @Test + void tablesKeepThePreciseEntryArrayTypeThroughGrowth() { + // the array constructor mints a real TestHashedEntry[], not an Object[] or a base-type array + TestHashedEntry[] table = FlatHashtable.create(1, TestHashedEntry[]::new); + assertSame(TestHashedEntry[].class, table.getClass()); + + TestHashedEntry[] grown = FlatHashtable.resize(table, TestHashedEntry[]::new); + assertSame(TestHashedEntry[].class, grown.getClass()); + assertEquals(table.length * 2, grown.length); + } + @Test void capacityFor_roundsToPowerOfTwoAtLeastTwiceLimit() { assertEquals(2, FlatHashtable.capacityFor(1)); @@ -177,14 +188,14 @@ void capacityFor_rejectsNonPositive() { @Test void create_allocatesTypedTableOfCapacity() { - TestEntry[] table = FlatHashtable.create(TestEntry.class, 4); + TestEntry[] table = FlatHashtable.create(4, TestEntry[]::new); assertEquals(8, table.length); assertEquals(TestEntry.class, table.getClass().getComponentType()); } @Test void getOrCreate_insertsOnceAndReturnsTheExistingEntry() { - TestEntry[] table = FlatHashtable.create(TestEntry.class, 8); + TestEntry[] table = FlatHashtable.create(8, TestEntry[]::new); TestEntry first = FlatHashtable.tryGetOrCreate(table, "a", TestEntryStrategy.INSTANCE, CREATE); assertEquals("a", first.key); // A second call must return the SAME instance, not mint a new one. @@ -194,7 +205,7 @@ void getOrCreate_insertsOnceAndReturnsTheExistingEntry() { @Test void get_returnsNullForAbsentKey() { - TestEntry[] table = FlatHashtable.create(TestEntry.class, 8); + TestEntry[] table = FlatHashtable.create(8, TestEntry[]::new); assertNull(FlatHashtable.get(table, "missing", TestEntryStrategy.INSTANCE)); FlatHashtable.tryGetOrCreate(table, "present", TestEntryStrategy.INSTANCE, CREATE); assertNull(FlatHashtable.get(table, "still-missing", TestEntryStrategy.INSTANCE)); @@ -203,7 +214,7 @@ void get_returnsNullForAbsentKey() { @Test void getOrCreate_returnsNullWhenTableIsFull() { // capacityFor(1) == 2 slots. - TestEntry[] table = FlatHashtable.create(TestEntry.class, 1); + TestEntry[] table = FlatHashtable.create(1, TestEntry[]::new); assertTrue( FlatHashtable.tryGetOrCreate(table, "k0", TestEntryStrategy.INSTANCE, CREATE) != null); assertTrue( @@ -226,7 +237,7 @@ void hashKey_isStableForEqualKeys() { @Test void collision_probesPastOccupiedSlots_andResolvesEach() { // 8 slots; COLLIDING sends all to slot 0 - TestEntry[] table = FlatHashtable.create(TestEntry.class, 4); + TestEntry[] table = FlatHashtable.create(4, TestEntry[]::new); TestEntry a = FlatHashtable.tryGetOrCreate(table, "a", TestCollidingStrategy.INSTANCE, CREATE); // slot 0 taken -> 1 TestEntry b = FlatHashtable.tryGetOrCreate(table, "b", TestCollidingStrategy.INSTANCE, CREATE); @@ -251,7 +262,7 @@ void collision_probesPastOccupiedSlots_andResolvesEach() { @Test void collision_probeWrapsAroundToFront() { // 2 slots (0,1), mask=1; LAST_SLOT starts at 1 - TestEntry[] table = FlatHashtable.create(TestEntry.class, 1); + TestEntry[] table = FlatHashtable.create(1, TestEntry[]::new); // -> slot 1 TestEntry k0 = FlatHashtable.tryGetOrCreate(table, "k0", TestLastSlotStrategy.INSTANCE, CREATE); // taken -> wraps to 0 @@ -265,7 +276,7 @@ void collision_probeWrapsAroundToFront() { @Test void get_returnsNullWhenTableFullAndKeyAbsent() { - TestEntry[] table = FlatHashtable.create(TestEntry.class, 1); // 2 slots + TestEntry[] table = FlatHashtable.create(1, TestEntry[]::new); // 2 slots FlatHashtable.tryGetOrCreate(table, "k0", TestCollidingStrategy.INSTANCE, CREATE); // fills slots 0 and 1 FlatHashtable.tryGetOrCreate(table, "k1", TestCollidingStrategy.INSTANCE, CREATE); @@ -276,7 +287,7 @@ void get_returnsNullWhenTableFullAndKeyAbsent() { @Test void insert_generalFlavor_placesViaHashOfAndResolves() { - TestEntry[] table = FlatHashtable.create(TestEntry.class, 8); + TestEntry[] table = FlatHashtable.create(8, TestEntry[]::new); TestEntry e = new TestEntry("a"); // flavor 2: the home comes from TestEntryStrategy.INSTANCE.hashOf(e) assertTrue(FlatHashtable.insert(table, e, TestEntryStrategy.INSTANCE)); @@ -285,7 +296,7 @@ void insert_generalFlavor_placesViaHashOfAndResolves() { @Test void insert_entryFlavor_placesViaCachedHashAndResolves() { - TestHashedEntry[] table = FlatHashtable.create(TestHashedEntry.class, 8); + TestHashedEntry[] table = FlatHashtable.create(8, TestHashedEntry[]::new); TestHashedEntry e = new TestHashedEntry("a"); // flavor 1: the home comes from the Entry's own cached hash, no strategy needed assertTrue(FlatHashtable.insert(table, e)); @@ -294,7 +305,7 @@ void insert_entryFlavor_placesViaCachedHashAndResolves() { @Test void insert_returnsFalseWhenFull() { - TestEntry[] table = FlatHashtable.create(TestEntry.class, 1); // 2 slots + TestEntry[] table = FlatHashtable.create(1, TestEntry[]::new); // 2 slots assertTrue(FlatHashtable.insert(table, new TestEntry("k0"), TestEntryStrategy.INSTANCE)); assertTrue(FlatHashtable.insert(table, new TestEntry("k1"), TestEntryStrategy.INSTANCE)); // no room @@ -303,7 +314,7 @@ void insert_returnsFalseWhenFull() { @Test void forEach_visitsEveryEntry() { - TestEntry[] table = FlatHashtable.create(TestEntry.class, 8); + TestEntry[] table = FlatHashtable.create(8, TestEntry[]::new); FlatHashtable.tryGetOrCreate(table, "a", TestEntryStrategy.INSTANCE, CREATE); FlatHashtable.tryGetOrCreate(table, "b", TestEntryStrategy.INSTANCE, CREATE); FlatHashtable.tryGetOrCreate(table, "c", TestEntryStrategy.INSTANCE, CREATE); @@ -315,7 +326,7 @@ void forEach_visitsEveryEntry() { @Test void forEach_contextVariant_passesContextWithoutCapture() { - TestEntry[] table = FlatHashtable.create(TestEntry.class, 8); + TestEntry[] table = FlatHashtable.create(8, TestEntry[]::new); FlatHashtable.tryGetOrCreate(table, "a", TestEntryStrategy.INSTANCE, CREATE); FlatHashtable.tryGetOrCreate(table, "b", TestEntryStrategy.INSTANCE, CREATE); @@ -326,7 +337,7 @@ void forEach_contextVariant_passesContextWithoutCapture() { @Test void iterator_yieldsEveryEntrySharingTheHash() { - TestEntry[] table = FlatHashtable.create(TestEntry.class, 4); // COLLIDING sends all to slot 0 + TestEntry[] table = FlatHashtable.create(4, TestEntry[]::new); // COLLIDING sends all to slot 0 TestEntry a = FlatHashtable.tryGetOrCreate(table, "a", TestCollidingStrategy.INSTANCE, CREATE); TestEntry b = FlatHashtable.tryGetOrCreate(table, "b", TestCollidingStrategy.INSTANCE, CREATE); TestEntry c = FlatHashtable.tryGetOrCreate(table, "c", TestCollidingStrategy.INSTANCE, CREATE); @@ -341,7 +352,7 @@ void iterator_yieldsEveryEntrySharingTheHash() { @Test void iterator_filtersOutEntriesWithADifferentHash() { - TestEntry[] table = FlatHashtable.create(TestEntry.class, 4); // entries at slot 0, hashOf == 0 + TestEntry[] table = FlatHashtable.create(4, TestEntry[]::new); // entries at slot 0, hashOf == 0 FlatHashtable.tryGetOrCreate(table, "a", TestCollidingStrategy.INSTANCE, CREATE); FlatHashtable.tryGetOrCreate(table, "b", TestCollidingStrategy.INSTANCE, CREATE); @@ -354,7 +365,7 @@ void iterator_filtersOutEntriesWithADifferentHash() { @Test void iterator_emptyRunHasNoNext() { - TestEntry[] table = FlatHashtable.create(TestEntry.class, 4); + TestEntry[] table = FlatHashtable.create(4, TestEntry[]::new); Iterator it = FlatHashtable.iterator(table, 0, TestCollidingStrategy.INSTANCE); assertFalse(it.hasNext()); assertThrows(NoSuchElementException.class, it::next); @@ -364,7 +375,7 @@ void iterator_emptyRunHasNoNext() { void iterator_fullTable_yieldsMatchesIncludingTheWrappingSlot() { // 2 slots, both filled by colliding (hash 0) entries -> the probe has no empty slot to stop at, // so the traversal must yield the entry on the wrapping slot and then terminate on wrap. - TestEntry[] table = FlatHashtable.create(TestEntry.class, 1); + TestEntry[] table = FlatHashtable.create(1, TestEntry[]::new); TestEntry a = FlatHashtable.tryGetOrCreate(table, "a", TestCollidingStrategy.INSTANCE, CREATE); TestEntry b = FlatHashtable.tryGetOrCreate(table, "b", TestCollidingStrategy.INSTANCE, CREATE); @@ -380,7 +391,7 @@ void iterator_fullTable_yieldsMatchesIncludingTheWrappingSlot() { void iterator_fullTable_absentHash_terminatesOnWrap() { // Full table, iterating a hash no stored entry has (all hashOf == 0) -> the traversal walks // every slot and wraps without ever hitting an empty one, then reports no elements. - TestEntry[] table = FlatHashtable.create(TestEntry.class, 1); + TestEntry[] table = FlatHashtable.create(1, TestEntry[]::new); FlatHashtable.tryGetOrCreate(table, "a", TestCollidingStrategy.INSTANCE, CREATE); FlatHashtable.tryGetOrCreate(table, "b", TestCollidingStrategy.INSTANCE, CREATE); @@ -431,18 +442,18 @@ void grownLength_rejectsOverflowingCurrentLength() { @Test void create_honorsLoadFactor() { - TestEntry[] table = FlatHashtable.create(TestEntry.class, 4, FlatHashtable.LOW_LOAD_FACTOR); + TestEntry[] table = FlatHashtable.create(4, FlatHashtable.LOW_LOAD_FACTOR, TestEntry[]::new); assertEquals(16, table.length); } @Test void resize_generalFlavor_growsAndKeepsEveryEntryFindable() { // 8 slots with only 2 entries -> the rehash loop skips empty slots as well as copying entries. - TestEntry[] table = FlatHashtable.create(TestEntry.class, 4); + TestEntry[] table = FlatHashtable.create(4, TestEntry[]::new); FlatHashtable.insert(table, new TestEntry("a"), TestEntryStrategy.INSTANCE); FlatHashtable.insert(table, new TestEntry("b"), TestEntryStrategy.INSTANCE); - TestEntry[] grown = FlatHashtable.resize(table, TestEntryStrategy.INSTANCE); + TestEntry[] grown = FlatHashtable.resize(table, TestEntryStrategy.INSTANCE, TestEntry[]::new); assertEquals(16, grown.length); assertNotSame(table, grown); assertEquals("a", FlatHashtable.get(grown, "a", TestEntryStrategy.INSTANCE).key); @@ -451,11 +462,11 @@ void resize_generalFlavor_growsAndKeepsEveryEntryFindable() { @Test void resize_entryFlavor_growsAndKeepsEveryEntryFindable() { - TestHashedEntry[] table = FlatHashtable.create(TestHashedEntry.class, 4); // 8 slots, 2 entries + TestHashedEntry[] table = FlatHashtable.create(4, TestHashedEntry[]::new); // 8 slots, 2 entries FlatHashtable.insert(table, new TestHashedEntry("a")); FlatHashtable.insert(table, new TestHashedEntry("b")); - TestHashedEntry[] grown = FlatHashtable.resize(table); + TestHashedEntry[] grown = FlatHashtable.resize(table, TestHashedEntry[]::new); assertEquals(16, grown.length); assertEquals("a", FlatHashtable.get(grown, "a", TestHashedStrategy.INSTANCE).key); assertEquals("b", FlatHashtable.get(grown, "b", TestHashedStrategy.INSTANCE).key); @@ -463,10 +474,11 @@ void resize_entryFlavor_growsAndKeepsEveryEntryFindable() { @Test void resizingInsert_generalFlavor_growsPastCapacityAndReturnsTheTable() { - TestEntry[] table = FlatHashtable.create(TestEntry.class, 1); // 2 slots + TestEntry[] table = FlatHashtable.create(1, TestEntry[]::new); // 2 slots for (int i = 0; i < 10; ++i) { table = - FlatHashtable.resizingInsert(table, new TestEntry("k" + i), TestEntryStrategy.INSTANCE); + FlatHashtable.resizingInsert( + table, new TestEntry("k" + i), TestEntryStrategy.INSTANCE, TestEntry[]::new); } assertTrue(table.length >= 16); // grew from 2 to hold 10 at load factor <= 0.5 for (int i = 0; i < 10; ++i) { @@ -476,9 +488,10 @@ void resizingInsert_generalFlavor_growsPastCapacityAndReturnsTheTable() { @Test void resizingInsert_entryFlavor_growsPastCapacityAndReturnsTheTable() { - TestHashedEntry[] table = FlatHashtable.create(TestHashedEntry.class, 1); // 2 slots + TestHashedEntry[] table = FlatHashtable.create(1, TestHashedEntry[]::new); // 2 slots for (int i = 0; i < 10; ++i) { - table = FlatHashtable.resizingInsert(table, new TestHashedEntry("k" + i)); + table = + FlatHashtable.resizingInsert(table, new TestHashedEntry("k" + i), TestHashedEntry[]::new); } assertTrue(table.length >= 16); for (int i = 0; i < 10; ++i) { @@ -498,7 +511,7 @@ static final class TestHashEntry extends FlatHashtable.Entry { @Test void entryIterator_yieldsEveryEntrySharingTheHash() { - TestHashEntry[] table = FlatHashtable.create(TestHashEntry.class, 4); + TestHashEntry[] table = FlatHashtable.create(4, TestHashEntry[]::new); TestHashEntry a = new TestHashEntry("a", 0); TestHashEntry b = new TestHashEntry("b", 0); TestHashEntry c = new TestHashEntry("c", 0); @@ -517,7 +530,7 @@ void entryIterator_yieldsEveryEntrySharingTheHash() { @Test void entryIterator_filtersOutEntriesWithADifferentHash() { - TestHashEntry[] table = FlatHashtable.create(TestHashEntry.class, 4); // 8 slots, mask 7 + TestHashEntry[] table = FlatHashtable.create(4, TestHashEntry[]::new); // 8 slots, mask 7 FlatHashtable.insert(table, new TestHashEntry("a", 0)); FlatHashtable.insert(table, new TestHashEntry("b", 0)); // shares the home slot (0) but a different hash -> must be filtered out @@ -535,7 +548,7 @@ void entryIterator_filtersOutEntriesWithADifferentHash() { @Test void entryIterator_emptyRunHasNoNext() { - TestHashEntry[] table = FlatHashtable.create(TestHashEntry.class, 4); + TestHashEntry[] table = FlatHashtable.create(4, TestHashEntry[]::new); Iterator it = FlatHashtable.iterator(table, 0); assertFalse(it.hasNext()); assertThrows(NoSuchElementException.class, it::next); @@ -543,7 +556,7 @@ void entryIterator_emptyRunHasNoNext() { @Test void caseInsensitiveStrategy_matchesRegardlessOfCase() { - TestEntry[] table = FlatHashtable.create(TestEntry.class, 4); + TestEntry[] table = FlatHashtable.create(4, TestEntry[]::new); TestEntry stored = FlatHashtable.tryGetOrCreate( table, "Content-Type", TestCaseInsensitiveStrategy.INSTANCE, CREATE); @@ -575,7 +588,7 @@ void caseInsensitiveStrategy_matchesRegardlessOfCase() { void caseInsensitiveStrategy_doesNotFalseMissOnSupplementaryCasePair() { String s1 = new String(Character.toChars(0x10400)); // DESERET CAPITAL LETTER LONG I String s2 = new String(Character.toChars(0x10428)); // DESERET SMALL LETTER LONG I - TestEntry[] table = FlatHashtable.create(TestEntry.class, 4); + TestEntry[] table = FlatHashtable.create(4, TestEntry[]::new); TestEntry stored = FlatHashtable.tryGetOrCreate(table, s1, TestCaseInsensitiveStrategy.INSTANCE, CREATE);