From 6a28882bad948ee6f226938871bfcb9493f69a0c Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 29 Jul 2026 11:08:20 -0400 Subject: [PATCH 1/3] Unify Hashtable static API with ConcurrentHashtable; deprecate Support Move the static building blocks off the nested Support class onto Hashtable itself, mirroring ConcurrentHashtable's flat layout, and add createFixedBuckets(Class, int) factories on Hashtable/D1/D2 for family symmetry. Support becomes a thin @Deprecated facade delegating to the new statics (retaining the scaled create(int, float)/MAX_RATIO helpers, which have no blessed equivalent), so client-side-statistics callers keep compiling untouched. Rename the context type parameter -> on the context-passing forEach overloads, and add D2.Entry.key1()/key2() accessors to match D1/the concurrent variant. No behavior change; pure API relocation + deprecation. Co-Authored-By: Claude Opus 4.8 --- .../java/datadog/trace/util/Hashtable.java | 492 ++++++++++++------ 1 file changed, 339 insertions(+), 153 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/util/Hashtable.java b/internal-api/src/main/java/datadog/trace/util/Hashtable.java index cc7bc4eddf5..286d401d017 100644 --- a/internal-api/src/main/java/datadog/trace/util/Hashtable.java +++ b/internal-api/src/main/java/datadog/trace/util/Hashtable.java @@ -23,10 +23,14 @@ * Convenience classes are provided for lower key dimensions. * *

For higher key dimensions, client code must implement its own class, but can still use the - * support class to ease the implementation complexity. + * static building blocks on this class to ease the implementation complexity. * *

This outer class is a pure namespace -- it can't be instantiated. The actual table types are - * {@link D1}, {@link D2}, and (for higher-arity callers) {@link Support}-driven custom tables. + * {@link D1}, {@link D2}, and (for higher-arity callers) custom tables driven by the static + * building blocks on this class (see {@link #createFixedBuckets(Class, int)}, {@link + * #bucket(Hashtable.Entry[], long)}, {@link #insertHeadEntry(Hashtable.Entry[], int, + * Hashtable.Entry)}, and friends). The deprecated {@link Support} class is a thin facade over those + * same statics, retained for source compatibility. */ public final class Hashtable { private Hashtable() {} @@ -37,7 +41,8 @@ private Hashtable() {} * *

Subclasses add the actual key field(s) and a {@code matches(...)} method tailored to their * key arity. See {@link D1.Entry} and {@link D2.Entry}; for higher arities, client code can - * subclass this directly and use {@link Support} to drive the table mechanics. + * subclass this directly and drive the table with the static building blocks on {@link + * Hashtable}. */ public abstract static class Entry { public final long keyHash; @@ -121,25 +126,40 @@ public static long hash(Object key) { } } - // Package-private so iterator tests in the same package can drive Support.bucketIterator and - // friends directly against the table's bucket array. + // Package-private so iterator tests in the same package can drive the Hashtable static + // building blocks directly against the table's bucket array. final Hashtable.Entry[] buckets; private int size; public D1(int capacity) { - this.buckets = Support.create(capacity); + this.buckets = new Hashtable.Entry[sizeFor(capacity)]; this.size = 0; } + /** + * Creates a single-key table with a fixed bucket count sized for {@code capacity} entries. The + * {@code entryClass} pins the concrete entry type so the compiler infers both {@code K} and + * {@code TEntry} at the call site -- e.g. {@code D1.createFixedBuckets(MyEntry.class, 64)} -- + * keeping the factory symmetric with the rest of the flat-collections family (see {@link + * Hashtable#createFixedBuckets(Class, int)} for why the class isn't otherwise consumed). + * Capacity is fixed; the table does not resize. + */ + public static > D1 createFixedBuckets( + Class entryClass, int capacity) { + return new D1<>(capacity); + } + public int size() { return this.size; } public TEntry get(K key) { long keyHash = D1.Entry.hash(key); - for (TEntry te = Support.bucket(this.buckets, keyHash); te != null; te = te.next()) { - if (te.keyHash == keyHash && te.matches(key)) { - return te; + for (TEntry curEntry = bucket(this.buckets, keyHash); + curEntry != null; + curEntry = curEntry.next()) { + if (curEntry.keyHash == keyHash && curEntry.matches(key)) { + return curEntry; } } return null; @@ -148,8 +168,7 @@ public TEntry get(K key) { public TEntry remove(K key) { long keyHash = D1.Entry.hash(key); - for (MutatingBucketIterator iter = - Support.mutatingBucketIterator(this.buckets, keyHash); + for (MutatingBucketIterator iter = mutatingBucketIterator(this.buckets, keyHash); iter.hasNext(); ) { TEntry curEntry = iter.next(); @@ -164,13 +183,13 @@ public TEntry remove(K key) { } public void insert(TEntry newEntry) { - Support.insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); + insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); this.size += 1; } public TEntry insertOrReplace(TEntry newEntry) { for (MutatingBucketIterator iter = - Support.mutatingBucketIterator(this.buckets, newEntry.keyHash); + mutatingBucketIterator(this.buckets, newEntry.keyHash); iter.hasNext(); ) { TEntry curEntry = iter.next(); @@ -180,7 +199,7 @@ public TEntry insertOrReplace(TEntry newEntry) { } } - Support.insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); + insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); this.size += 1; return null; } @@ -197,24 +216,26 @@ public TEntry insertOrReplace(TEntry newEntry) { */ public TEntry getOrCreate(K key, Function creator) { long keyHash = D1.Entry.hash(key); - for (TEntry te = Support.bucket(this.buckets, keyHash); te != null; te = te.next()) { - if (te.keyHash == keyHash && te.matches(key)) { - return te; + for (TEntry curEntry = bucket(this.buckets, keyHash); + curEntry != null; + curEntry = curEntry.next()) { + if (curEntry.keyHash == keyHash && curEntry.matches(key)) { + return curEntry; } } TEntry newEntry = creator.apply(key); - Support.insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); + insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); this.size += 1; return newEntry; } public void clear() { - Support.clear(this.buckets); + Hashtable.clear(this.buckets); this.size = 0; } public void forEach(Consumer consumer) { - Support.forEach(this.buckets, consumer); + Hashtable.forEach(this.buckets, consumer); } /** @@ -222,8 +243,8 @@ public void forEach(Consumer consumer) { * -- pass a non-capturing {@link BiConsumer} (typically a {@code static final}) plus whatever * side-band state it needs as {@code context}. */ - public void forEach(T context, BiConsumer consumer) { - Support.forEach(this.buckets, context, consumer); + public void forEach(C context, BiConsumer consumer) { + Hashtable.forEach(this.buckets, context, consumer); } } @@ -270,6 +291,16 @@ protected Entry(K1 key1, K2 key2) { this.key2 = key2; } + /** The first key part this entry was created with. */ + public K1 key1() { + return this.key1; + } + + /** The second key part this entry was created with. */ + public K2 key2() { + return this.key2; + } + public boolean matches(K1 key1, K2 key2) { return Objects.equals(this.key1, key1) && Objects.equals(this.key2, key2); } @@ -291,19 +322,34 @@ public static long hash(Object key1, Object key2) { private int size; public D2(int capacity) { - this.buckets = Support.create(capacity); + this.buckets = new Hashtable.Entry[sizeFor(capacity)]; this.size = 0; } + /** + * Creates a composite-key table with a fixed bucket count sized for {@code capacity} entries. + * The {@code entryClass} pins the concrete entry type so the compiler infers {@code K1}, {@code + * K2}, and {@code TEntry} at the call site -- e.g. {@code D2.createFixedBuckets(MyEntry.class, + * 64)} -- keeping the factory symmetric with the rest of the flat-collections family (see + * {@link Hashtable#createFixedBuckets(Class, int)} for why the class isn't otherwise consumed). + * Capacity is fixed; the table does not resize. + */ + public static > D2 createFixedBuckets( + Class entryClass, int capacity) { + return new D2<>(capacity); + } + public int size() { return this.size; } public TEntry get(K1 key1, K2 key2) { long keyHash = D2.Entry.hash(key1, key2); - for (TEntry te = Support.bucket(this.buckets, keyHash); te != null; te = te.next()) { - if (te.keyHash == keyHash && te.matches(key1, key2)) { - return te; + for (TEntry curEntry = bucket(this.buckets, keyHash); + curEntry != null; + curEntry = curEntry.next()) { + if (curEntry.keyHash == keyHash && curEntry.matches(key1, key2)) { + return curEntry; } } return null; @@ -312,8 +358,7 @@ public TEntry get(K1 key1, K2 key2) { public TEntry remove(K1 key1, K2 key2) { long keyHash = D2.Entry.hash(key1, key2); - for (MutatingBucketIterator iter = - Support.mutatingBucketIterator(this.buckets, keyHash); + for (MutatingBucketIterator iter = mutatingBucketIterator(this.buckets, keyHash); iter.hasNext(); ) { TEntry curEntry = iter.next(); @@ -328,13 +373,13 @@ public TEntry remove(K1 key1, K2 key2) { } public void insert(TEntry newEntry) { - Support.insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); + insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); this.size += 1; } public TEntry insertOrReplace(TEntry newEntry) { for (MutatingBucketIterator iter = - Support.mutatingBucketIterator(this.buckets, newEntry.keyHash); + mutatingBucketIterator(this.buckets, newEntry.keyHash); iter.hasNext(); ) { TEntry curEntry = iter.next(); @@ -344,7 +389,7 @@ public TEntry insertOrReplace(TEntry newEntry) { } } - Support.insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); + insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); this.size += 1; return null; } @@ -357,24 +402,26 @@ public TEntry insertOrReplace(TEntry newEntry) { public TEntry getOrCreate( K1 key1, K2 key2, BiFunction creator) { long keyHash = D2.Entry.hash(key1, key2); - for (TEntry te = Support.bucket(this.buckets, keyHash); te != null; te = te.next()) { - if (te.keyHash == keyHash && te.matches(key1, key2)) { - return te; + for (TEntry curEntry = bucket(this.buckets, keyHash); + curEntry != null; + curEntry = curEntry.next()) { + if (curEntry.keyHash == keyHash && curEntry.matches(key1, key2)) { + return curEntry; } } TEntry newEntry = creator.apply(key1, key2); - Support.insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); + insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); this.size += 1; return newEntry; } public void clear() { - Support.clear(this.buckets); + Hashtable.clear(this.buckets); this.size = 0; } public void forEach(Consumer consumer) { - Support.forEach(this.buckets, consumer); + Hashtable.forEach(this.buckets, consumer); } /** @@ -382,195 +429,334 @@ public void forEach(Consumer consumer) { * -- pass a non-capturing {@link BiConsumer} (typically a {@code static final}) plus whatever * side-band state it needs as {@code context}. */ - public void forEach(T context, BiConsumer consumer) { - Support.forEach(this.buckets, context, consumer); + public void forEach(C context, BiConsumer consumer) { + Hashtable.forEach(this.buckets, context, consumer); + } + } + + // ============================================================================================ + // Static building blocks over a caller-owned bucket array. + // + // Use these to assemble a custom table (higher arity, primitive keys, extra value fields) when + // D1/D2 don't fit; D1/D2 delegate to them internally. This is the same "static functions over a + // caller-owned array" shape as the concurrent variant (ConcurrentHashtable); see how + // AggregateTable drives a Hashtable.Entry[] with these. The calling class owns the array and + // exposes whatever operations it needs. + // + // Not thread-safe: there is no locking here. Concurrent access, including mixing reads with + // writes, requires external synchronization. + // + // These were previously nested under the Support class; that class is now a deprecated facade + // delegating here (retained for source compatibility with existing callers such as client-side + // statistics). + // ============================================================================================ + + /** Upper bound on the bucket-array length returned by {@link #sizeFor(int)}. */ + static final int MAX_BUCKETS = 1 << 30; + + /** + * Allocates a fixed-size bucket array sized to hold {@code capacity} entries: {@code capacity} + * rounded up to the next power of two. + * + *

Returns a concrete {@code Hashtable.Entry[]} (chain heads are stored at the base type), so + * the array assigns directly to a caller's {@code Hashtable.Entry[]} field. As with the + * concurrent variant's {@code createFixedBuckets}, {@code entryClass} is not consumed to + * allocate -- the array is a heterogeneous {@code Entry[]}, not a reflectively-allocated {@code + * TEntry[]}. It is accepted only to keep the factory call-shape symmetric across the + * flat-collections family ({@code createFixedBuckets(MyEntry.class, n)}). Capacity is fixed; the + * table does not resize. + * + *

For load-factor headroom over a target working-set size, size {@code capacity} yourself + * (e.g. {@code createFixedBuckets(MyEntry.class, (int) (n * 4 / 3f))}); the deprecated {@link + * Support#create(int, float)} bundled that scaling but has no blessed equivalent. + */ + public static Hashtable.Entry[] createFixedBuckets( + Class entryClass, int capacity) { + return new Entry[sizeFor(capacity)]; + } + + /** + * Rounds {@code requestedSize} up to the next power of two, capped at {@link #MAX_BUCKETS}, and + * returns the bucket-array length to allocate. Throws {@link IllegalArgumentException} for + * negative inputs or inputs above the cap. The concurrent variant shares this so the two families + * round identically. + */ + public static int sizeFor(int requestedSize) { + if (requestedSize < 0) { + throw new IllegalArgumentException("requestedSize must be non-negative: " + requestedSize); + } + if (requestedSize > MAX_BUCKETS) { + throw new IllegalArgumentException( + "requestedSize exceeds maximum bucket count (" + MAX_BUCKETS + "): " + requestedSize); + } + if (requestedSize <= 1) { + return 1; + } + return Integer.highestOneBit(requestedSize - 1) << 1; + } + + public static int bucketIndex(Object[] buckets, long keyHash) { + return (int) (keyHash & buckets.length - 1); + } + + /** + * Returns the head entry of the bucket that {@code keyHash} maps to, cast to the caller's + * concrete entry type. The unchecked cast lives here so the chain-walk loop at the call site + * doesn't need to thread a raw {@link Entry} variable through. + */ + @SuppressWarnings("unchecked") + public static TEntry bucket(Hashtable.Entry[] buckets, long keyHash) { + return (TEntry) buckets[bucketIndex(buckets, keyHash)]; + } + + /** + * Splices {@code entry} in as the new head of the chain at {@code bucketIndex}. Caller is + * responsible for size accounting -- this method only touches the chain pointers. + */ + public static void insertHeadEntry( + Hashtable.Entry[] buckets, int bucketIndex, Hashtable.Entry entry) { + entry.setNext(buckets[bucketIndex]); + buckets[bucketIndex] = entry; + } + + /** + * Convenience overload of {@link #insertHeadEntry(Hashtable.Entry[], int, Hashtable.Entry)} that + * derives the bucket index from {@code keyHash}. Use this when the caller has the hash but not + * the index; if the index has already been computed for another reason, prefer the int-taking + * overload to avoid the redundant mask. + */ + public static void insertHeadEntry( + Hashtable.Entry[] buckets, long keyHash, Hashtable.Entry entry) { + insertHeadEntry(buckets, bucketIndex(buckets, keyHash), entry); + } + + public static void clear(Hashtable.Entry[] buckets) { + Arrays.fill(buckets, null); + } + + /** + * Walks every entry in {@code buckets} and invokes {@code consumer} on it. The unchecked cast to + * {@code TEntry} lives here (mirroring {@link Entry#next()}) so callers don't have to sprinkle it + * across their own forEach loops. + */ + @SuppressWarnings("unchecked") + public static void forEach( + Hashtable.Entry[] buckets, Consumer consumer) { + for (int i = 0; i < buckets.length; i++) { + for (Hashtable.Entry e = buckets[i]; e != null; e = e.next()) { + consumer.accept((TEntry) e); + } + } + } + + /** + * Context-passing variant of {@link #forEach(Hashtable.Entry[], Consumer)}. Pair a non-capturing + * {@link BiConsumer} (typically a {@code static final}) with side-band state passed as {@code + * context} to avoid a fresh-Consumer allocation each call. + */ + @SuppressWarnings("unchecked") + public static void forEach( + Hashtable.Entry[] buckets, C context, BiConsumer consumer) { + for (int i = 0; i < buckets.length; i++) { + for (Hashtable.Entry e = buckets[i]; e != null; e = e.next()) { + consumer.accept(context, (TEntry) e); + } } } + public static BucketIterator bucketIterator( + Hashtable.Entry[] buckets, long keyHash) { + return new BucketIterator(buckets, keyHash); + } + + public static + MutatingBucketIterator mutatingBucketIterator( + Hashtable.Entry[] buckets, long keyHash) { + return new MutatingBucketIterator(buckets, keyHash); + } + + /** + * Returns a {@link MutatingTableIterator} over every entry in {@code buckets}. Useful for sweeps + * -- eviction, expunge -- that aren't keyed to a specific hash. + */ + public static + MutatingTableIterator mutatingTableIterator(Hashtable.Entry[] buckets) { + return new MutatingTableIterator(buckets, 0, buckets.length); + } + /** - * Building blocks for hash-table operations. + * Variant of {@link #mutatingTableIterator(Hashtable.Entry[])} that walks only the half-open + * bucket range {@code [startBucket, endBucket)}. Useful for resumable sweeps -- e.g. cursor-based + * eviction in {@code AggregateTable} -- where one call drives {@code [cursor, length)} and a + * wrap-around call drives {@code [0, cursor)}. The iterator does not wrap around within a + * single instance; callers compose two iterators when wrap-around is desired. An empty range + * ({@code startBucket == endBucket}) produces an immediately exhausted iterator. * - *

Used by {@link D1} and {@link D2}, and available to callers that want to assemble their own - * higher-arity table (3+ key parts) without re-implementing the bucket-array mechanics. The - * typical recipe: + * @param startBucket inclusive lower bound; must be in {@code [0, buckets.length]}. + * @param endBucket exclusive upper bound; must be in {@code [startBucket, buckets.length]}. + */ + public static + MutatingTableIterator mutatingTableIterator( + Hashtable.Entry[] buckets, int startBucket, int endBucket) { + return new MutatingTableIterator(buckets, startBucket, endBucket); + } + + /** + * Deprecated facade over the static building blocks that are now methods on {@link Hashtable} + * itself (mirroring the concurrent variant). Each method here delegates to its {@code + * Hashtable.*} counterpart; the two sizing helpers with no blessed equivalent -- {@link + * #create(int, float)} and {@link #MAX_RATIO} -- keep their real bodies here. * - *

    - *
  • Subclass {@link Hashtable.Entry} directly, adding the key fields and a {@code - * matches(...)} method of your chosen arity. - *
  • Allocate a backing array with {@link #create(int)} or {@link #create(int, float)} (the - * latter scales for a target load factor; see {@link #MAX_RATIO}). - *
  • Use {@link #bucketIndex(Object[], long)} for the bucket lookup, {@link - * #bucketIterator(Hashtable.Entry[], long)} for read-only chain walks, and {@link - * #mutatingBucketIterator(Hashtable.Entry[], long)} when you also need {@code remove} / - * {@code replace}. - *
  • Use {@link #insertHeadEntry(Hashtable.Entry[], int, Hashtable.Entry)} to splice a new - * entry as the head of a bucket chain. - *
  • Iterate every entry with {@link #forEach(Hashtable.Entry[], Consumer)} or its - * context-passing sibling. For full-table sweeps with {@code remove}, use {@link - * #mutatingTableIterator(Hashtable.Entry[])}. - *
  • Clear with {@link #clear(Hashtable.Entry[])}. - *
+ *

Retained only for source compatibility with existing callers (e.g. client-side statistics). + * New code should call the {@code Hashtable.*} statics directly. * - *

All bucket arrays produced by {@code create} have a power-of-two length, so {@link - * #bucketIndex(Object[], long)} can use a bit mask. + * @deprecated use the static building blocks on {@link Hashtable} directly. */ + @Deprecated public static final class Support { + private Support() {} + /** - * Allocates a bucket array sized to hold {@code requestedSize} entries. Returned length is - * {@code requestedSize} rounded up to the next power of two (capped at {@link #MAX_BUCKETS}). + * @deprecated use {@link Hashtable#createFixedBuckets(Class, int)}. */ - public static final Hashtable.Entry[] create(int requestedSize) { + @Deprecated + public static Hashtable.Entry[] create(int requestedSize) { return new Entry[sizeFor(requestedSize)]; } /** - * Variant of {@link #create(int)} that scales the requested working-set size before sizing the - * bucket array. Pair with {@link #MAX_RATIO} to leave headroom over the working set for a - * desired load factor; the canonical call is {@code create(n, MAX_RATIO)}. + * Scales the requested working-set size before sizing the bucket array. Pair with {@link + * #MAX_RATIO} to leave headroom over the working set for a desired load factor; the canonical + * call is {@code create(n, MAX_RATIO)}. + * + *

The scaled size is truncated to {@code int} before going through {@link + * Hashtable#sizeFor(int)}. Truncation rather than {@code ceil} is intentional: {@code sizeFor} + * rounds up to the next power of two anyway, so the fractional part would only matter when + * float fuzz pushes the result across a power-of-two boundary -- {@code ceil} would then double + * the array size for no reason (e.g. {@code 12 * 4/3 = 16.0...0005f -> ceil 17 -> sizeFor 32}). * - *

The scaled size is truncated to {@code int} before going through {@link #sizeFor(int)}. - * Truncation rather than {@code ceil} is intentional: {@code sizeFor} rounds up to the next - * power of two anyway, so the fractional part would only matter when float fuzz pushes the - * result across a power-of-two boundary -- {@code ceil} would then double the array size for no - * reason (e.g. {@code 12 * 4/3 = 16.0...0005f -> ceil 17 -> sizeFor 32}). + *

No blessed equivalent: callers wanting load-factor headroom size the capacity themselves + * and call {@link Hashtable#createFixedBuckets(Class, int)}. + * + * @deprecated size the capacity yourself and use {@link Hashtable#createFixedBuckets(Class, + * int)}. */ - public static final Hashtable.Entry[] create(int requestedSize, float scale) { + @Deprecated + public static Hashtable.Entry[] create(int requestedSize, float scale) { return new Entry[sizeFor((int) (requestedSize * scale))]; } - /** Upper bound on the bucket array length returned by {@link #sizeFor(int)}. */ - static final int MAX_BUCKETS = 1 << 30; - /** * Inverse of a 75% load factor. Callers that size their bucket array from a target working-set * size {@code n} should pass {@code create(n, MAX_RATIO)} to leave ~25% headroom in the array. */ - public static final float MAX_RATIO = 4.0f / 3.0f; + @Deprecated public static final float MAX_RATIO = 4.0f / 3.0f; /** - * Rounds {@code requestedSize} up to the next power of two, capped at {@link #MAX_BUCKETS}. - * Throws {@link IllegalArgumentException} for negative inputs or inputs above the cap. Returns - * the bucket-array length to allocate. + * @deprecated use {@link Hashtable#sizeFor(int)}. */ - static final int sizeFor(int requestedSize) { - if (requestedSize < 0) { - throw new IllegalArgumentException("requestedSize must be non-negative: " + requestedSize); - } - if (requestedSize > MAX_BUCKETS) { - throw new IllegalArgumentException( - "requestedSize exceeds maximum bucket count (" + MAX_BUCKETS + "): " + requestedSize); - } - if (requestedSize <= 1) { - return 1; - } - return Integer.highestOneBit(requestedSize - 1) << 1; + @Deprecated + static int sizeFor(int requestedSize) { + return Hashtable.sizeFor(requestedSize); } - public static final void clear(Hashtable.Entry[] buckets) { - Arrays.fill(buckets, null); + /** + * @deprecated use {@link Hashtable#clear(Hashtable.Entry[])}. + */ + @Deprecated + public static void clear(Hashtable.Entry[] buckets) { + Hashtable.clear(buckets); } - public static final BucketIterator bucketIterator( + /** + * @deprecated use {@link Hashtable#bucketIterator(Hashtable.Entry[], long)}. + */ + @Deprecated + public static BucketIterator bucketIterator( Hashtable.Entry[] buckets, long keyHash) { - return new BucketIterator(buckets, keyHash); + return Hashtable.bucketIterator(buckets, keyHash); } - public static final + /** + * @deprecated use {@link Hashtable#mutatingBucketIterator(Hashtable.Entry[], long)}. + */ + @Deprecated + public static MutatingBucketIterator mutatingBucketIterator( Hashtable.Entry[] buckets, long keyHash) { - return new MutatingBucketIterator(buckets, keyHash); + return Hashtable.mutatingBucketIterator(buckets, keyHash); } /** - * Returns a {@link MutatingTableIterator} over every entry in {@code buckets}. Useful for - * sweeps -- eviction, expunge -- that aren't keyed to a specific hash. + * @deprecated use {@link Hashtable#mutatingTableIterator(Hashtable.Entry[])}. */ - public static final + @Deprecated + public static MutatingTableIterator mutatingTableIterator(Hashtable.Entry[] buckets) { - return new MutatingTableIterator(buckets, 0, buckets.length); + return Hashtable.mutatingTableIterator(buckets); } /** - * Variant of {@link #mutatingTableIterator(Hashtable.Entry[])} that walks only the half-open - * bucket range {@code [startBucket, endBucket)}. Useful for resumable sweeps -- e.g. cursor- - * based eviction in {@code AggregateTable} -- where one call drives {@code [cursor, length)} - * and a wrap-around call drives {@code [0, cursor)}. The iterator does not wrap around - * within a single instance; callers compose two iterators when wrap-around is desired. An empty - * range ({@code startBucket == endBucket}) produces an immediately exhausted iterator. - * - * @param startBucket inclusive lower bound; must be in {@code [0, buckets.length]}. - * @param endBucket exclusive upper bound; must be in {@code [startBucket, buckets.length]}. + * @deprecated use {@link Hashtable#mutatingTableIterator(Hashtable.Entry[], int, int)}. */ - public static final + @Deprecated + public static MutatingTableIterator mutatingTableIterator( Hashtable.Entry[] buckets, int startBucket, int endBucket) { - return new MutatingTableIterator(buckets, startBucket, endBucket); + return Hashtable.mutatingTableIterator(buckets, startBucket, endBucket); } - public static final int bucketIndex(Object[] buckets, long keyHash) { - return (int) (keyHash & buckets.length - 1); + /** + * @deprecated use {@link Hashtable#bucketIndex(Object[], long)}. + */ + @Deprecated + public static int bucketIndex(Object[] buckets, long keyHash) { + return Hashtable.bucketIndex(buckets, keyHash); } /** - * Splices {@code entry} in as the new head of the chain at {@code bucketIndex}. Caller is - * responsible for size accounting -- this method only touches the chain pointers. + * @deprecated use {@link Hashtable#insertHeadEntry(Hashtable.Entry[], int, Hashtable.Entry)}. */ - public static final void insertHeadEntry( + @Deprecated + public static void insertHeadEntry( Hashtable.Entry[] buckets, int bucketIndex, Hashtable.Entry entry) { - entry.setNext(buckets[bucketIndex]); - buckets[bucketIndex] = entry; + Hashtable.insertHeadEntry(buckets, bucketIndex, entry); } /** - * Convenience overload of {@link #insertHeadEntry(Hashtable.Entry[], int, Hashtable.Entry)} - * that derives the bucket index from {@code keyHash}. Use this when the caller has the hash but - * not the index; if the index has already been computed for another reason, prefer the - * int-taking overload to avoid the redundant mask. + * @deprecated use {@link Hashtable#insertHeadEntry(Hashtable.Entry[], long, Hashtable.Entry)}. */ - public static final void insertHeadEntry( + @Deprecated + public static void insertHeadEntry( Hashtable.Entry[] buckets, long keyHash, Hashtable.Entry entry) { - insertHeadEntry(buckets, bucketIndex(buckets, keyHash), entry); + Hashtable.insertHeadEntry(buckets, keyHash, entry); } /** - * Returns the head entry of the bucket that {@code keyHash} maps to, cast to the caller's - * concrete entry type. The unchecked cast lives here so the chain-walk loop at the call site - * doesn't need to thread a raw {@link Entry} variable through. + * @deprecated use {@link Hashtable#bucket(Hashtable.Entry[], long)}. */ - @SuppressWarnings("unchecked") - public static final TEntry bucket( + @Deprecated + public static TEntry bucket( Hashtable.Entry[] buckets, long keyHash) { - return (TEntry) buckets[bucketIndex(buckets, keyHash)]; + return Hashtable.bucket(buckets, keyHash); } /** - * Walks every entry in {@code buckets} and invokes {@code consumer} on it. The unchecked cast - * to {@code TEntry} lives here (mirroring {@link Entry#next()}) so callers don't have to - * sprinkle it across their own forEach loops. + * @deprecated use {@link Hashtable#forEach(Hashtable.Entry[], Consumer)}. */ - @SuppressWarnings("unchecked") - public static final void forEach( + @Deprecated + public static void forEach( Hashtable.Entry[] buckets, Consumer consumer) { - for (int i = 0; i < buckets.length; i++) { - for (Hashtable.Entry e = buckets[i]; e != null; e = e.next()) { - consumer.accept((TEntry) e); - } - } + Hashtable.forEach(buckets, consumer); } /** - * Context-passing variant of {@link #forEach(Hashtable.Entry[], Consumer)}. Pair a - * non-capturing {@link BiConsumer} (typically a {@code static final}) with side-band state - * passed as {@code context} to avoid a fresh-Consumer allocation each call. + * @deprecated use {@link Hashtable#forEach(Hashtable.Entry[], Object, BiConsumer)}. */ - @SuppressWarnings("unchecked") - public static final void forEach( - Hashtable.Entry[] buckets, T context, BiConsumer consumer) { - for (int i = 0; i < buckets.length; i++) { - for (Hashtable.Entry e = buckets[i]; e != null; e = e.next()) { - consumer.accept(context, (TEntry) e); - } - } + @Deprecated + public static void forEach( + Hashtable.Entry[] buckets, C context, BiConsumer consumer) { + Hashtable.forEach(buckets, context, consumer); } } From e4643cba678ace8408c39116520eb0ca37b0a50a Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 29 Jul 2026 11:13:30 -0400 Subject: [PATCH 2/3] Migrate HashtableTest to blessed Hashtable static API Point the tests at the relocated static building blocks on Hashtable (createFixedBuckets, sizeFor, bucketIndex, clear, insertHeadEntry, and the iterator factories) instead of the now-deprecated Support facade. Keep a small DeprecatedSupportTests group covering the deprecated-only scaled create(int, float) + MAX_RATIO, which have no blessed equivalent and remain in use by client-side statistics. Co-Authored-By: Claude Opus 4.8 --- .../datadog/trace/util/HashtableTest.java | 118 ++++++++++-------- 1 file changed, 66 insertions(+), 52 deletions(-) diff --git a/internal-api/src/test/java/datadog/trace/util/HashtableTest.java b/internal-api/src/test/java/datadog/trace/util/HashtableTest.java index 953453ca3aa..f566d04ee0d 100644 --- a/internal-api/src/test/java/datadog/trace/util/HashtableTest.java +++ b/internal-api/src/test/java/datadog/trace/util/HashtableTest.java @@ -23,16 +23,16 @@ class HashtableTest { - // ============ Support ============ + // ============ Static building blocks ============ @Nested - class SupportTests { + class StaticBuildingBlockTests { @Test void createRoundsCapacityUpToPowerOfTwo() { // The Hashtable.D1 / D2 size() reflects entries, but the bucket array length is // a power of two >= requestedCapacity. We can verify indirectly via bucketIndex masking. - Hashtable.Entry[] buckets = Support.create(5); + Hashtable.Entry[] buckets = Hashtable.createFixedBuckets(StringIntEntry.class, 5); // Length must be a power of two >= 5 int len = buckets.length; assertTrue(len >= 5); @@ -41,51 +41,78 @@ void createRoundsCapacityUpToPowerOfTwo() { @Test void sizeForReturnsAtLeastOne() { - assertEquals(1, Support.sizeFor(0)); - assertEquals(1, Support.sizeFor(1)); + assertEquals(1, Hashtable.sizeFor(0)); + assertEquals(1, Hashtable.sizeFor(1)); } @Test void sizeForRoundsUpToPowerOfTwo() { - assertEquals(2, Support.sizeFor(2)); - assertEquals(4, Support.sizeFor(3)); - assertEquals(4, Support.sizeFor(4)); - assertEquals(8, Support.sizeFor(5)); - assertEquals(1 << 30, Support.sizeFor(1 << 30)); + assertEquals(2, Hashtable.sizeFor(2)); + assertEquals(4, Hashtable.sizeFor(3)); + assertEquals(4, Hashtable.sizeFor(4)); + assertEquals(8, Hashtable.sizeFor(5)); + assertEquals(1 << 30, Hashtable.sizeFor(1 << 30)); } @Test void sizeForRejectsCapacityAboveMax() { - assertThrows(IllegalArgumentException.class, () -> Support.sizeFor((1 << 30) + 1)); - assertThrows(IllegalArgumentException.class, () -> Support.sizeFor(Integer.MAX_VALUE)); + assertThrows(IllegalArgumentException.class, () -> Hashtable.sizeFor((1 << 30) + 1)); + assertThrows(IllegalArgumentException.class, () -> Hashtable.sizeFor(Integer.MAX_VALUE)); } @Test void sizeForRejectsNegativeCapacity() { - assertThrows(IllegalArgumentException.class, () -> Support.sizeFor(-1)); - assertThrows(IllegalArgumentException.class, () -> Support.sizeFor(Integer.MIN_VALUE)); + assertThrows(IllegalArgumentException.class, () -> Hashtable.sizeFor(-1)); + assertThrows(IllegalArgumentException.class, () -> Hashtable.sizeFor(Integer.MIN_VALUE)); } @Test void bucketIndexIsBoundedByArrayLength() { - Hashtable.Entry[] buckets = Support.create(16); + Hashtable.Entry[] buckets = Hashtable.createFixedBuckets(StringIntEntry.class, 16); for (long h : new long[] {0L, 1L, -1L, Long.MIN_VALUE, Long.MAX_VALUE, 12345L}) { - int idx = Support.bucketIndex(buckets, h); + int idx = Hashtable.bucketIndex(buckets, h); assertTrue(idx >= 0 && idx < buckets.length, "bucketIndex out of range for hash " + h); } } @Test void clearNullsAllBuckets() { - Hashtable.Entry[] buckets = Support.create(4); + Hashtable.Entry[] buckets = Hashtable.createFixedBuckets(StringIntEntry.class, 4); buckets[0] = new StringIntEntry("x", 1); buckets[1] = new StringIntEntry("y", 2); - Support.clear(buckets); + Hashtable.clear(buckets); for (Hashtable.Entry b : buckets) { assertNull(b); } } + @Test + void insertHeadEntrySplicesAsNewHead() { + Hashtable.Entry[] buckets = Hashtable.createFixedBuckets(StringIntEntry.class, 4); + StringIntEntry a = new StringIntEntry("a", 1); + StringIntEntry b = new StringIntEntry("b", 2); + Hashtable.insertHeadEntry(buckets, 0, a); + assertSame(a, buckets[0]); + assertNull(a.next()); + + Hashtable.insertHeadEntry(buckets, 0, b); + assertSame(b, buckets[0]); + assertSame(a, b.next()); + assertNull(a.next()); + } + } + + // ============ Deprecated Support facade ============ + + /** + * The scaled {@code create(int, float)} factory and {@code MAX_RATIO} are deprecated-only: they + * have no blessed equivalent on {@link Hashtable} but remain in use by client-side statistics, so + * they keep dedicated coverage here. + */ + @Nested + @SuppressWarnings("deprecation") + class DeprecatedSupportTests { + @Test void maxRatioScalesTargetForLoadFactor() { // 75% load factor => bucket array sized at requestedSize * 4/3, rounded up to power of 2. @@ -101,21 +128,6 @@ void createWithScaleRoundsUpToPowerOfTwo() { Hashtable.Entry[] buckets = Support.create(7, 1.5f); assertEquals(16, buckets.length); } - - @Test - void insertHeadEntrySplicesAsNewHead() { - Hashtable.Entry[] buckets = Support.create(4); - StringIntEntry a = new StringIntEntry("a", 1); - StringIntEntry b = new StringIntEntry("b", 2); - Support.insertHeadEntry(buckets, 0, a); - assertSame(a, buckets[0]); - assertNull(a.next()); - - Support.insertHeadEntry(buckets, 0, b); - assertSame(b, buckets[0]); - assertSame(a, b.next()); - assertNull(a.next()); - } } // ============ BucketIterator ============ @@ -126,7 +138,7 @@ class BucketIteratorTests { @Test void walksOnlyMatchingHash() { // Build a bucket array with two entries that share a bucket but have different hashes. - // Use Hashtable.D1 to seed; then call Support.bucketIterator directly with the matching + // Use Hashtable.D1 to seed; then call Hashtable.bucketIterator directly with the matching // hash and verify it only returns the matching entry. Hashtable.D1 table = new Hashtable.D1<>(4); CollidingKey k1 = new CollidingKey("first", 17); @@ -136,7 +148,7 @@ void walksOnlyMatchingHash() { table.insert(new CollidingKeyEntry(k2, 2)); table.insert(new CollidingKeyEntry(k3, 3)); // All three share the same hash (17), so a bucket iterator over hash=17 yields all three. - BucketIterator it = Support.bucketIterator(table.buckets, 17L); + BucketIterator it = Hashtable.bucketIterator(table.buckets, 17L); int count = 0; while (it.hasNext()) { assertNotNull(it.next()); @@ -150,7 +162,7 @@ void exhaustedIteratorThrowsNoSuchElement() { Hashtable.D1 table = new Hashtable.D1<>(4); table.insert(new StringIntEntry("only", 1)); long h = Hashtable.D1.Entry.hash("only"); - BucketIterator it = Support.bucketIterator(table.buckets, h); + BucketIterator it = Hashtable.bucketIterator(table.buckets, h); it.next(); assertFalse(it.hasNext()); assertThrows(NoSuchElementException.class, it::next); @@ -174,7 +186,7 @@ void removeFromHeadOfChainUnlinks() { table.insert(new CollidingKeyEntry(k3, 3)); MutatingBucketIterator it = - Support.mutatingBucketIterator(table.buckets, 17L); + Hashtable.mutatingBucketIterator(table.buckets, 17L); it.next(); // first match (head of chain in insertion-reverse order) it.remove(); // Two should remain @@ -207,7 +219,7 @@ void replaceSwapsEntryAndPreservesChain() { table.insert(e2); MutatingBucketIterator it = - Support.mutatingBucketIterator(table.buckets, 17L); + Hashtable.mutatingBucketIterator(table.buckets, 17L); CollidingKeyEntry first = it.next(); CollidingKeyEntry replacement = new CollidingKeyEntry(first.key, 999); it.replace(replacement); @@ -223,7 +235,7 @@ void removeWithoutNextThrows() { Hashtable.D1 table = new Hashtable.D1<>(4); table.insert(new StringIntEntry("a", 1)); MutatingBucketIterator it = - Support.mutatingBucketIterator(table.buckets, Hashtable.D1.Entry.hash("a")); + Hashtable.mutatingBucketIterator(table.buckets, Hashtable.D1.Entry.hash("a")); assertThrows(IllegalStateException.class, it::remove); } } @@ -241,7 +253,8 @@ void walksEveryEntryAcrossBuckets() { table.insert(new StringIntEntry("c", 3)); Set seen = new HashSet<>(); - for (MutatingTableIterator it = Support.mutatingTableIterator(table.buckets); + for (MutatingTableIterator it = + Hashtable.mutatingTableIterator(table.buckets); it.hasNext(); ) { seen.add(it.next().key); } @@ -254,7 +267,7 @@ void walksEveryEntryAcrossBuckets() { @Test void emptyTableIteratorIsExhausted() { Hashtable.D1 table = new Hashtable.D1<>(8); - MutatingTableIterator it = Support.mutatingTableIterator(table.buckets); + MutatingTableIterator it = Hashtable.mutatingTableIterator(table.buckets); assertFalse(it.hasNext()); assertThrows(NoSuchElementException.class, it::next); } @@ -268,7 +281,7 @@ void removeUnlinksBucketHead() { table.insert(new CollidingKeyEntry(k2, 2)); // The head of the chain is whichever was inserted last (insert prepends). - MutatingTableIterator it = Support.mutatingTableIterator(table.buckets); + MutatingTableIterator it = Hashtable.mutatingTableIterator(table.buckets); CollidingKeyEntry head = it.next(); it.remove(); @@ -289,7 +302,7 @@ void removeUnlinksMidChainEntry() { table.insert(new CollidingKeyEntry(k3, 3)); // Walk to the second entry, remove it. - MutatingTableIterator it = Support.mutatingTableIterator(table.buckets); + MutatingTableIterator it = Hashtable.mutatingTableIterator(table.buckets); it.next(); CollidingKeyEntry victim = it.next(); it.remove(); @@ -320,7 +333,7 @@ void removeSkipsOverEmptyBuckets() { table.insert(new StringIntEntry("beta", 2)); table.insert(new StringIntEntry("gamma", 3)); - MutatingTableIterator it = Support.mutatingTableIterator(table.buckets); + MutatingTableIterator it = Hashtable.mutatingTableIterator(table.buckets); it.next(); it.remove(); int remaining = 0; @@ -335,7 +348,7 @@ void removeSkipsOverEmptyBuckets() { void removeWithoutNextThrows() { Hashtable.D1 table = new Hashtable.D1<>(4); table.insert(new StringIntEntry("a", 1)); - MutatingTableIterator it = Support.mutatingTableIterator(table.buckets); + MutatingTableIterator it = Hashtable.mutatingTableIterator(table.buckets); assertThrows(IllegalStateException.class, it::remove); } @@ -344,7 +357,7 @@ void removeTwiceWithoutInterveningNextThrows() { Hashtable.D1 table = new Hashtable.D1<>(4); table.insert(new StringIntEntry("a", 1)); table.insert(new StringIntEntry("b", 2)); - MutatingTableIterator it = Support.mutatingTableIterator(table.buckets); + MutatingTableIterator it = Hashtable.mutatingTableIterator(table.buckets); it.next(); it.remove(); assertThrows(IllegalStateException.class, it::remove); @@ -362,7 +375,7 @@ void halfOpenRangeOmitsBucketsOutsideTheRange() { Set seen = new HashSet<>(); for (MutatingTableIterator it = - Support.mutatingTableIterator(table.buckets, 5, 10); + Hashtable.mutatingTableIterator(table.buckets, 5, 10); it.hasNext(); ) { seen.add(it.next().key.label); } @@ -376,7 +389,8 @@ void emptyHalfOpenRangeIsExhausted() { // pass [0, cursor) when cursor == 0 in resumable sweeps. Hashtable.D1 table = new Hashtable.D1<>(8); table.insert(new StringIntEntry("a", 1)); - MutatingTableIterator it = Support.mutatingTableIterator(table.buckets, 0, 0); + MutatingTableIterator it = + Hashtable.mutatingTableIterator(table.buckets, 0, 0); assertFalse(it.hasNext()); } @@ -385,14 +399,14 @@ void rangeBoundsOutOfOrderThrows() { Hashtable.D1 table = new Hashtable.D1<>(8); assertThrows( IndexOutOfBoundsException.class, - () -> Support.mutatingTableIterator(table.buckets, -1, 4)); + () -> Hashtable.mutatingTableIterator(table.buckets, -1, 4)); assertThrows( IndexOutOfBoundsException.class, - () -> Support.mutatingTableIterator(table.buckets, 4, 2)); // end < start + () -> Hashtable.mutatingTableIterator(table.buckets, 4, 2)); // end < start assertThrows( IndexOutOfBoundsException.class, () -> - Support.mutatingTableIterator( + Hashtable.mutatingTableIterator( table.buckets, 0, table.buckets.length + 1)); // end > len } @@ -403,7 +417,7 @@ void currentBucketReportsLandingIndex() { Hashtable.D1 table = new Hashtable.D1<>(16); table.insert(new CollidingKeyEntry(new CollidingKey("b3", 3), 1)); - MutatingTableIterator it = Support.mutatingTableIterator(table.buckets); + MutatingTableIterator it = Hashtable.mutatingTableIterator(table.buckets); assertEquals(-1, it.currentBucket(), "before any next() currentBucket should be -1"); it.next(); assertEquals(3, it.currentBucket(), "currentBucket should report the entry's bucket"); From 8c1b05d2f44a87b555f948c88ff6bfa843d25c28 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 29 Jul 2026 12:41:50 -0400 Subject: [PATCH 3/3] Hashtable: annotate nullability (@Nonnull/@Nullable) Co-Authored-By: Claude Opus 4.8 --- .../java/datadog/trace/util/Hashtable.java | 142 +++++++++++------- 1 file changed, 91 insertions(+), 51 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/util/Hashtable.java b/internal-api/src/main/java/datadog/trace/util/Hashtable.java index 286d401d017..32351cbf208 100644 --- a/internal-api/src/main/java/datadog/trace/util/Hashtable.java +++ b/internal-api/src/main/java/datadog/trace/util/Hashtable.java @@ -8,6 +8,8 @@ import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * Light weight simple Hashtable system that can be useful when HashMap would be unnecessarily @@ -52,11 +54,12 @@ protected Entry(long keyHash) { this.keyHash = keyHash; } - public final void setNext(TEntry next) { + public final void setNext(@Nullable TEntry next) { this.next = next; } @SuppressWarnings("unchecked") + @Nullable public final TEntry next() { return (TEntry) this.next; } @@ -99,17 +102,18 @@ public static final class D1> { public abstract static class Entry extends Hashtable.Entry { final K key; - protected Entry(K key) { + protected Entry(@Nullable K key) { super(hash(key)); this.key = key; } /** The key this entry was created with. */ + @Nullable public K key() { return this.key; } - public boolean matches(Object key) { + public boolean matches(@Nullable Object key) { return Objects.equals(this.key, key); } @@ -121,7 +125,7 @@ public boolean matches(Object key) { * [Integer.MIN_VALUE, Integer.MAX_VALUE]}; real-key collisions in chains are resolved by * {@link #matches(Object)}. */ - public static long hash(Object key) { + public static long hash(@Nullable Object key) { return (key == null) ? Long.MIN_VALUE : key.hashCode(); } } @@ -144,8 +148,9 @@ public D1(int capacity) { * Hashtable#createFixedBuckets(Class, int)} for why the class isn't otherwise consumed). * Capacity is fixed; the table does not resize. */ + @Nonnull public static > D1 createFixedBuckets( - Class entryClass, int capacity) { + @Nonnull Class entryClass, int capacity) { return new D1<>(capacity); } @@ -153,7 +158,8 @@ public int size() { return this.size; } - public TEntry get(K key) { + @Nullable + public TEntry get(@Nullable K key) { long keyHash = D1.Entry.hash(key); for (TEntry curEntry = bucket(this.buckets, keyHash); curEntry != null; @@ -165,7 +171,8 @@ public TEntry get(K key) { return null; } - public TEntry remove(K key) { + @Nullable + public TEntry remove(@Nullable K key) { long keyHash = D1.Entry.hash(key); for (MutatingBucketIterator iter = mutatingBucketIterator(this.buckets, keyHash); @@ -182,12 +189,13 @@ public TEntry remove(K key) { return null; } - public void insert(TEntry newEntry) { + public void insert(@Nonnull TEntry newEntry) { insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); this.size += 1; } - public TEntry insertOrReplace(TEntry newEntry) { + @Nullable + public TEntry insertOrReplace(@Nonnull TEntry newEntry) { for (MutatingBucketIterator iter = mutatingBucketIterator(this.buckets, newEntry.keyHash); iter.hasNext(); ) { @@ -214,7 +222,9 @@ public TEntry insertOrReplace(TEntry newEntry) { * that calls {@code super(key)}. A mismatched hash will leave the new entry inserted at a * bucket that future {@link #get} calls won't probe. */ - public TEntry getOrCreate(K key, Function creator) { + @Nonnull + public TEntry getOrCreate( + @Nullable K key, @Nonnull Function creator) { long keyHash = D1.Entry.hash(key); for (TEntry curEntry = bucket(this.buckets, keyHash); curEntry != null; @@ -234,7 +244,7 @@ public void clear() { this.size = 0; } - public void forEach(Consumer consumer) { + public void forEach(@Nonnull Consumer consumer) { Hashtable.forEach(this.buckets, consumer); } @@ -243,7 +253,7 @@ public void forEach(Consumer consumer) { * -- pass a non-capturing {@link BiConsumer} (typically a {@code static final}) plus whatever * side-band state it needs as {@code context}. */ - public void forEach(C context, BiConsumer consumer) { + public void forEach(C context, @Nonnull BiConsumer consumer) { Hashtable.forEach(this.buckets, context, consumer); } } @@ -285,23 +295,25 @@ public abstract static class Entry extends Hashtable.Entry { final K1 key1; final K2 key2; - protected Entry(K1 key1, K2 key2) { + protected Entry(@Nullable K1 key1, @Nullable K2 key2) { super(hash(key1, key2)); this.key1 = key1; this.key2 = key2; } /** The first key part this entry was created with. */ + @Nullable public K1 key1() { return this.key1; } /** The second key part this entry was created with. */ + @Nullable public K2 key2() { return this.key2; } - public boolean matches(K1 key1, K2 key2) { + public boolean matches(@Nullable K1 key1, @Nullable K2 key2) { return Objects.equals(this.key1, key1) && Objects.equals(this.key2, key2); } @@ -312,7 +324,7 @@ public boolean matches(K1 key1, K2 key2) { * combinations whose chained hash equals {@code hash(0, 0) = 0} or similar values. {@link * #matches(Object, Object)} resolves any such collision. */ - public static long hash(Object key1, Object key2) { + public static long hash(@Nullable Object key1, @Nullable Object key2) { return LongHashingUtils.hash(key1, key2); } } @@ -334,8 +346,9 @@ public D2(int capacity) { * {@link Hashtable#createFixedBuckets(Class, int)} for why the class isn't otherwise consumed). * Capacity is fixed; the table does not resize. */ + @Nonnull public static > D2 createFixedBuckets( - Class entryClass, int capacity) { + @Nonnull Class entryClass, int capacity) { return new D2<>(capacity); } @@ -343,7 +356,8 @@ public int size() { return this.size; } - public TEntry get(K1 key1, K2 key2) { + @Nullable + public TEntry get(@Nullable K1 key1, @Nullable K2 key2) { long keyHash = D2.Entry.hash(key1, key2); for (TEntry curEntry = bucket(this.buckets, keyHash); curEntry != null; @@ -355,7 +369,8 @@ public TEntry get(K1 key1, K2 key2) { return null; } - public TEntry remove(K1 key1, K2 key2) { + @Nullable + public TEntry remove(@Nullable K1 key1, @Nullable K2 key2) { long keyHash = D2.Entry.hash(key1, key2); for (MutatingBucketIterator iter = mutatingBucketIterator(this.buckets, keyHash); @@ -372,12 +387,13 @@ public TEntry remove(K1 key1, K2 key2) { return null; } - public void insert(TEntry newEntry) { + public void insert(@Nonnull TEntry newEntry) { insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); this.size += 1; } - public TEntry insertOrReplace(TEntry newEntry) { + @Nullable + public TEntry insertOrReplace(@Nonnull TEntry newEntry) { for (MutatingBucketIterator iter = mutatingBucketIterator(this.buckets, newEntry.keyHash); iter.hasNext(); ) { @@ -399,8 +415,11 @@ public TEntry insertOrReplace(TEntry newEntry) { * both lookup and (on miss) insert. The {@code creator} is expected to build an entry whose * {@code keyHash} equals {@link Entry#hash(Object, Object) D2.Entry.hash(key1, key2)}. */ + @Nonnull public TEntry getOrCreate( - K1 key1, K2 key2, BiFunction creator) { + @Nullable K1 key1, + @Nullable K2 key2, + @Nonnull BiFunction creator) { long keyHash = D2.Entry.hash(key1, key2); for (TEntry curEntry = bucket(this.buckets, keyHash); curEntry != null; @@ -420,7 +439,7 @@ public void clear() { this.size = 0; } - public void forEach(Consumer consumer) { + public void forEach(@Nonnull Consumer consumer) { Hashtable.forEach(this.buckets, consumer); } @@ -429,7 +448,7 @@ public void forEach(Consumer consumer) { * -- pass a non-capturing {@link BiConsumer} (typically a {@code static final}) plus whatever * side-band state it needs as {@code context}. */ - public void forEach(C context, BiConsumer consumer) { + public void forEach(C context, @Nonnull BiConsumer consumer) { Hashtable.forEach(this.buckets, context, consumer); } } @@ -470,8 +489,9 @@ public void forEach(C context, BiConsumer consume * (e.g. {@code createFixedBuckets(MyEntry.class, (int) (n * 4 / 3f))}); the deprecated {@link * Support#create(int, float)} bundled that scaling but has no blessed equivalent. */ + @Nonnull public static Hashtable.Entry[] createFixedBuckets( - Class entryClass, int capacity) { + @Nonnull Class entryClass, int capacity) { return new Entry[sizeFor(capacity)]; } @@ -495,7 +515,7 @@ public static int sizeFor(int requestedSize) { return Integer.highestOneBit(requestedSize - 1) << 1; } - public static int bucketIndex(Object[] buckets, long keyHash) { + public static int bucketIndex(@Nonnull Object[] buckets, long keyHash) { return (int) (keyHash & buckets.length - 1); } @@ -505,7 +525,9 @@ public static int bucketIndex(Object[] buckets, long keyHash) { * doesn't need to thread a raw {@link Entry} variable through. */ @SuppressWarnings("unchecked") - public static TEntry bucket(Hashtable.Entry[] buckets, long keyHash) { + @Nullable + public static TEntry bucket( + @Nonnull Hashtable.Entry[] buckets, long keyHash) { return (TEntry) buckets[bucketIndex(buckets, keyHash)]; } @@ -514,7 +536,7 @@ public static TEntry bucket(Hashtable.Entry[] buckets, lo * responsible for size accounting -- this method only touches the chain pointers. */ public static void insertHeadEntry( - Hashtable.Entry[] buckets, int bucketIndex, Hashtable.Entry entry) { + @Nonnull Hashtable.Entry[] buckets, int bucketIndex, @Nonnull Hashtable.Entry entry) { entry.setNext(buckets[bucketIndex]); buckets[bucketIndex] = entry; } @@ -526,11 +548,11 @@ public static void insertHeadEntry( * overload to avoid the redundant mask. */ public static void insertHeadEntry( - Hashtable.Entry[] buckets, long keyHash, Hashtable.Entry entry) { + @Nonnull Hashtable.Entry[] buckets, long keyHash, @Nonnull Hashtable.Entry entry) { insertHeadEntry(buckets, bucketIndex(buckets, keyHash), entry); } - public static void clear(Hashtable.Entry[] buckets) { + public static void clear(@Nonnull Hashtable.Entry[] buckets) { Arrays.fill(buckets, null); } @@ -541,7 +563,7 @@ public static void clear(Hashtable.Entry[] buckets) { */ @SuppressWarnings("unchecked") public static void forEach( - Hashtable.Entry[] buckets, Consumer consumer) { + @Nonnull Hashtable.Entry[] buckets, @Nonnull Consumer consumer) { for (int i = 0; i < buckets.length; i++) { for (Hashtable.Entry e = buckets[i]; e != null; e = e.next()) { consumer.accept((TEntry) e); @@ -556,7 +578,9 @@ public static void forEach( */ @SuppressWarnings("unchecked") public static void forEach( - Hashtable.Entry[] buckets, C context, BiConsumer consumer) { + @Nonnull Hashtable.Entry[] buckets, + C context, + @Nonnull BiConsumer consumer) { for (int i = 0; i < buckets.length; i++) { for (Hashtable.Entry e = buckets[i]; e != null; e = e.next()) { consumer.accept(context, (TEntry) e); @@ -564,14 +588,16 @@ public static void forEach( } } + @Nonnull public static BucketIterator bucketIterator( - Hashtable.Entry[] buckets, long keyHash) { + @Nonnull Hashtable.Entry[] buckets, long keyHash) { return new BucketIterator(buckets, keyHash); } + @Nonnull public static MutatingBucketIterator mutatingBucketIterator( - Hashtable.Entry[] buckets, long keyHash) { + @Nonnull Hashtable.Entry[] buckets, long keyHash) { return new MutatingBucketIterator(buckets, keyHash); } @@ -579,8 +605,9 @@ MutatingBucketIterator mutatingBucketIterator( * Returns a {@link MutatingTableIterator} over every entry in {@code buckets}. Useful for sweeps * -- eviction, expunge -- that aren't keyed to a specific hash. */ + @Nonnull public static - MutatingTableIterator mutatingTableIterator(Hashtable.Entry[] buckets) { + MutatingTableIterator mutatingTableIterator(@Nonnull Hashtable.Entry[] buckets) { return new MutatingTableIterator(buckets, 0, buckets.length); } @@ -595,9 +622,10 @@ MutatingTableIterator mutatingTableIterator(Hashtable.Entry[] buckets) { * @param startBucket inclusive lower bound; must be in {@code [0, buckets.length]}. * @param endBucket exclusive upper bound; must be in {@code [startBucket, buckets.length]}. */ + @Nonnull public static MutatingTableIterator mutatingTableIterator( - Hashtable.Entry[] buckets, int startBucket, int endBucket) { + @Nonnull Hashtable.Entry[] buckets, int startBucket, int endBucket) { return new MutatingTableIterator(buckets, startBucket, endBucket); } @@ -620,6 +648,7 @@ private Support() {} * @deprecated use {@link Hashtable#createFixedBuckets(Class, int)}. */ @Deprecated + @Nonnull public static Hashtable.Entry[] create(int requestedSize) { return new Entry[sizeFor(requestedSize)]; } @@ -642,6 +671,7 @@ public static Hashtable.Entry[] create(int requestedSize) { * int)}. */ @Deprecated + @Nonnull public static Hashtable.Entry[] create(int requestedSize, float scale) { return new Entry[sizeFor((int) (requestedSize * scale))]; } @@ -664,7 +694,7 @@ static int sizeFor(int requestedSize) { * @deprecated use {@link Hashtable#clear(Hashtable.Entry[])}. */ @Deprecated - public static void clear(Hashtable.Entry[] buckets) { + public static void clear(@Nonnull Hashtable.Entry[] buckets) { Hashtable.clear(buckets); } @@ -672,8 +702,9 @@ public static void clear(Hashtable.Entry[] buckets) { * @deprecated use {@link Hashtable#bucketIterator(Hashtable.Entry[], long)}. */ @Deprecated + @Nonnull public static BucketIterator bucketIterator( - Hashtable.Entry[] buckets, long keyHash) { + @Nonnull Hashtable.Entry[] buckets, long keyHash) { return Hashtable.bucketIterator(buckets, keyHash); } @@ -681,9 +712,10 @@ public static BucketIterator bucketIter * @deprecated use {@link Hashtable#mutatingBucketIterator(Hashtable.Entry[], long)}. */ @Deprecated + @Nonnull public static MutatingBucketIterator mutatingBucketIterator( - Hashtable.Entry[] buckets, long keyHash) { + @Nonnull Hashtable.Entry[] buckets, long keyHash) { return Hashtable.mutatingBucketIterator(buckets, keyHash); } @@ -691,8 +723,9 @@ MutatingBucketIterator mutatingBucketIterator( * @deprecated use {@link Hashtable#mutatingTableIterator(Hashtable.Entry[])}. */ @Deprecated + @Nonnull public static - MutatingTableIterator mutatingTableIterator(Hashtable.Entry[] buckets) { + MutatingTableIterator mutatingTableIterator(@Nonnull Hashtable.Entry[] buckets) { return Hashtable.mutatingTableIterator(buckets); } @@ -700,9 +733,10 @@ MutatingTableIterator mutatingTableIterator(Hashtable.Entry[] buckets) { * @deprecated use {@link Hashtable#mutatingTableIterator(Hashtable.Entry[], int, int)}. */ @Deprecated + @Nonnull public static MutatingTableIterator mutatingTableIterator( - Hashtable.Entry[] buckets, int startBucket, int endBucket) { + @Nonnull Hashtable.Entry[] buckets, int startBucket, int endBucket) { return Hashtable.mutatingTableIterator(buckets, startBucket, endBucket); } @@ -710,7 +744,7 @@ MutatingTableIterator mutatingTableIterator( * @deprecated use {@link Hashtable#bucketIndex(Object[], long)}. */ @Deprecated - public static int bucketIndex(Object[] buckets, long keyHash) { + public static int bucketIndex(@Nonnull Object[] buckets, long keyHash) { return Hashtable.bucketIndex(buckets, keyHash); } @@ -719,7 +753,7 @@ public static int bucketIndex(Object[] buckets, long keyHash) { */ @Deprecated public static void insertHeadEntry( - Hashtable.Entry[] buckets, int bucketIndex, Hashtable.Entry entry) { + @Nonnull Hashtable.Entry[] buckets, int bucketIndex, @Nonnull Hashtable.Entry entry) { Hashtable.insertHeadEntry(buckets, bucketIndex, entry); } @@ -728,7 +762,7 @@ public static void insertHeadEntry( */ @Deprecated public static void insertHeadEntry( - Hashtable.Entry[] buckets, long keyHash, Hashtable.Entry entry) { + @Nonnull Hashtable.Entry[] buckets, long keyHash, @Nonnull Hashtable.Entry entry) { Hashtable.insertHeadEntry(buckets, keyHash, entry); } @@ -736,8 +770,9 @@ public static void insertHeadEntry( * @deprecated use {@link Hashtable#bucket(Hashtable.Entry[], long)}. */ @Deprecated + @Nullable public static TEntry bucket( - Hashtable.Entry[] buckets, long keyHash) { + @Nonnull Hashtable.Entry[] buckets, long keyHash) { return Hashtable.bucket(buckets, keyHash); } @@ -746,7 +781,7 @@ public static TEntry bucket( */ @Deprecated public static void forEach( - Hashtable.Entry[] buckets, Consumer consumer) { + @Nonnull Hashtable.Entry[] buckets, @Nonnull Consumer consumer) { Hashtable.forEach(buckets, consumer); } @@ -755,7 +790,9 @@ public static void forEach( */ @Deprecated public static void forEach( - Hashtable.Entry[] buckets, C context, BiConsumer consumer) { + @Nonnull Hashtable.Entry[] buckets, + C context, + @Nonnull BiConsumer consumer) { Hashtable.forEach(buckets, context, consumer); } } @@ -775,7 +812,7 @@ public static final class BucketIterator implements Iterat private final long keyHash; private Hashtable.Entry nextEntry; - BucketIterator(Hashtable.Entry[] buckets, long keyHash) { + BucketIterator(@Nonnull Hashtable.Entry[] buckets, long keyHash) { this.keyHash = keyHash; Hashtable.Entry cur = buckets[Support.bucketIndex(buckets, keyHash)]; while (cur != null && cur.keyHash != keyHash) { @@ -791,6 +828,7 @@ public boolean hasNext() { @Override @SuppressWarnings("unchecked") + @Nonnull public TEntry next() { Hashtable.Entry cur = this.nextEntry; if (cur == null) { @@ -837,7 +875,7 @@ public static final class MutatingBucketIterator /** The next entry to be returned by next */ private Hashtable.Entry nextEntry; - MutatingBucketIterator(Hashtable.Entry[] buckets, long keyHash) { + MutatingBucketIterator(@Nonnull Hashtable.Entry[] buckets, long keyHash) { this.buckets = buckets; this.keyHash = keyHash; @@ -871,6 +909,7 @@ public boolean hasNext() { @Override @SuppressWarnings("unchecked") + @Nonnull public TEntry next() { Hashtable.Entry curEntry = this.nextEntry; if (curEntry == null) { @@ -915,7 +954,7 @@ public void remove() { this.curEntry = null; } - public void replace(TEntry replacementEntry) { + public void replace(@Nonnull TEntry replacementEntry) { Hashtable.Entry oldCurEntry = this.curEntry; if (oldCurEntry == null) { throw new IllegalStateException(); @@ -935,7 +974,7 @@ public void replace(TEntry replacementEntry) { this.curEntry = replacementEntry; } - void setPrevNext(Hashtable.Entry nextEntry) { + void setPrevNext(@Nullable Hashtable.Entry nextEntry) { if (this.curPrevEntry == null) { Hashtable.Entry[] buckets = this.buckets; buckets[Support.bucketIndex(buckets, this.keyHash)] = nextEntry; @@ -992,7 +1031,7 @@ public static final class MutatingTableIterator */ private Hashtable.Entry curEntry; - MutatingTableIterator(Hashtable.Entry[] buckets, int startBucket, int endBucket) { + MutatingTableIterator(@Nonnull Hashtable.Entry[] buckets, int startBucket, int endBucket) { this.buckets = buckets; if (startBucket < 0 || startBucket > buckets.length) { throw new IndexOutOfBoundsException( @@ -1029,6 +1068,7 @@ public boolean hasNext() { @Override @SuppressWarnings("unchecked") + @Nonnull public TEntry next() { Hashtable.Entry e = this.nextEntry; if (e == null) {