1111 * <p>Two ways to use it, trading convenience for indirection:
1212 *
1313 * <ul>
14- * <li>{@link Support} — static algorithm over <b>raw arrays</b>. Keep the arrays in your own
15- * (ideally {@code static final}) fields and the JIT folds the refs to constants. The fastest
16- * path; nothing to dereference.
14+ * <li>{@link EmbeddingSupport} — static algorithm over <b>raw arrays</b> you <b>embed</b> in your
15+ * own (ideally {@code static final}) fields; the JIT folds the refs to constants. The fastest
16+ * path, nothing to dereference. (Named for the same role as {@code
17+ * LightMap.EmbeddingSupport}: the static ops you reach for when you own the backing arrays
18+ * directly.)
1719 * <li>The {@code StringIndex} <b>instance</b> ({@link #of}) — a convenience wrapper holding the
18- * arrays; {@link #indexOf}/{@link #contains} delegate to {@link Support }. Costs an
20+ * arrays; {@link #indexOf}/{@link #contains} delegate to {@link EmbeddingSupport }. Costs an
1921 * instance-field load per call (the indirection the static path removes) — fine off the hot
2022 * path.
2123 * </ul>
2527 * mapIntValues}/{@code mapLongValues} build such an array at construction; {@code lookup}/{@code
2628 * lookupOrDefault} read one back in a single call (slot resolve + array read).
2729 *
28- * <p>Slot 0-value is the empty sentinel: {@link Support #hash} never returns 0, so {@code hashes[i]
29- * == 0} unambiguously means an empty slot.
30+ * <p>Slot 0-value is the empty sentinel: {@link EmbeddingSupport #hash} never returns 0, so {@code
31+ * hashes[i] == 0} unambiguously means an empty slot.
3032 *
31- * <p>Trades memory for simplicity (and, incidentally, speed). The table is 2x-oversized (load
32- * factor ≤ 0.5) so build-time placement always finds a free slot and never has to rehash or
33- * resize — short probe chains are a welcome side effect, not the design goal. The cached {@code
34- * int[]} hashes gate {@code equals()}. Both cost memory, so a tightly-packed set is more compact:
35- * prefer {@link java.util.Set#copyOf} (the JDK's {@code SetN}) when you only need membership, and
36- * reach for {@code StringIndex} for the {@code indexOf}->parallel-array (name→id)
37- * capability or the hot, allocation-free static {@link Support} path. (If footprint ever matters
38- * more than build simplicity, a higher load factor with construction-time rehashing would close the
39- * gap.)
33+ * <p>Trades memory for simplicity (and, incidentally, speed). The table is 2x-oversized ({@link
34+ * EmbeddingSupport#DEFAULT_LOAD_FACTOR} ≤ 0.5) so build-time placement always finds a free slot
35+ * and never has to rehash or resize — short probe chains are a welcome side effect, not the design
36+ * goal. The cached {@code int[]} hashes gate {@code equals()}. Both cost memory, so a
37+ * tightly-packed set is more compact: prefer {@link java.util.Set#copyOf} (the JDK's {@code SetN})
38+ * when you only need membership, and reach for {@code StringIndex} for the {@code
39+ * indexOf}->parallel-array (name→id) capability or the hot, allocation-free static {@link
40+ * EmbeddingSupport} path. (If footprint matters more than build simplicity, build via {@link
41+ * EmbeddingSupport#capacityFor(int, float)} at a higher load factor — placement still finds a slot
42+ * at any factor < 1, so no rehash is needed.)
4043 */
4144public final class StringIndex {
4245 private final int [] hashes ;
@@ -48,16 +51,19 @@ private StringIndex(int[] hashes, String[] names) {
4851 }
4952
5053 /**
51- * Convenience instance — wraps the placed arrays. For the hot path prefer raw {@link Support}.
54+ * Convenience instance — wraps the placed arrays. For the hot path prefer raw {@link
55+ * EmbeddingSupport}.
5256 */
5357 public static StringIndex of (String ... names ) {
54- Data data = Support .create (names );
58+ Data data = EmbeddingSupport .create (names );
5559 return new StringIndex (data .hashes , data .names );
5660 }
5761
58- /** Slot of {@code name}, or -1. Delegates to {@link Support} on the instance's arrays. */
62+ /**
63+ * Slot of {@code name}, or -1. Delegates to {@link EmbeddingSupport} on the instance's arrays.
64+ */
5965 public int indexOf (String name ) {
60- return Support .indexOf (this .hashes , this .names , name );
66+ return EmbeddingSupport .indexOf (this .hashes , this .names , name );
6167 }
6268
6369 public boolean contains (String name ) {
@@ -77,49 +83,49 @@ public int numSlots() {
7783 * can't allocate a generic array without it). Pair with {@link #lookup(Object[], String)}.
7884 */
7985 public <T > T [] mapValues (Class <T > type , Function <String , T > fn ) {
80- return Support .mapValues (this .names , type , fn );
86+ return EmbeddingSupport .mapValues (this .names , type , fn );
8187 }
8288
8389 /** Slot-aligned {@code int[]} of values; absent slots stay 0. See {@link #mapValues}. */
8490 public int [] mapIntValues (ToIntFunction <String > fn ) {
85- return Support .mapIntValues (this .names , fn );
91+ return EmbeddingSupport .mapIntValues (this .names , fn );
8692 }
8793
8894 /** Slot-aligned {@code long[]} of values; absent slots stay 0. See {@link #mapValues}. */
8995 public long [] mapLongValues (ToLongFunction <String > fn ) {
90- return Support .mapLongValues (this .names , fn );
96+ return EmbeddingSupport .mapLongValues (this .names , fn );
9197 }
9298
9399 // --- lookup: resolve a key and read its parallel value in one call ---
94100
95101 /** {@code data[indexOf(key)]}, or {@code null} when {@code key} is absent. */
96102 public <T > T lookup (T [] data , String key ) {
97- return Support .lookup (this .hashes , this .names , data , key );
103+ return EmbeddingSupport .lookup (this .hashes , this .names , data , key );
98104 }
99105
100106 /** {@code data[indexOf(key)]}, or {@code defaultValue} when {@code key} is absent. */
101107 public <T > T lookupOrDefault (T [] data , String key , T defaultValue ) {
102- return Support .lookupOrDefault (this .hashes , this .names , data , key , defaultValue );
108+ return EmbeddingSupport .lookupOrDefault (this .hashes , this .names , data , key , defaultValue );
103109 }
104110
105111 /** {@code data[indexOf(key)]}, or 0 when {@code key} is absent. */
106112 public int lookup (int [] data , String key ) {
107- return Support .lookup (this .hashes , this .names , data , key );
113+ return EmbeddingSupport .lookup (this .hashes , this .names , data , key );
108114 }
109115
110116 /** {@code data[indexOf(key)]}, or {@code defaultValue} when {@code key} is absent. */
111117 public int lookupOrDefault (int [] data , String key , int defaultValue ) {
112- return Support .lookupOrDefault (this .hashes , this .names , data , key , defaultValue );
118+ return EmbeddingSupport .lookupOrDefault (this .hashes , this .names , data , key , defaultValue );
113119 }
114120
115121 /** {@code data[indexOf(key)]}, or 0 when {@code key} is absent. */
116122 public long lookup (long [] data , String key ) {
117- return Support .lookup (this .hashes , this .names , data , key );
123+ return EmbeddingSupport .lookup (this .hashes , this .names , data , key );
118124 }
119125
120126 /** {@code data[indexOf(key)]}, or {@code defaultValue} when {@code key} is absent. */
121127 public long lookupOrDefault (long [] data , String key , long defaultValue ) {
122- return Support .lookupOrDefault (this .hashes , this .names , data , key , defaultValue );
128+ return EmbeddingSupport .lookupOrDefault (this .hashes , this .names , data , key , defaultValue );
123129 }
124130
125131 /** Build-time carrier. Pull the fields into your own (static final) fields; don't keep this. */
@@ -136,27 +142,52 @@ public static final class Data {
136142 /**
137143 * Static algorithm over raw arrays. Query helpers take raw arrays, never a Data or a StringIndex.
138144 */
139- public static final class Support {
140- private Support () {}
145+ public static final class EmbeddingSupport {
146+ private EmbeddingSupport () {}
141147
142148 /** Spread of String.hashCode; 0 reserved as the empty sentinel. */
143149 public static int hash (String name ) {
144150 int h = name .hashCode (); // cached on String -> field load
145151 return h == 0 ? 0xDD06 : h ^ (h >>> 16 );
146152 }
147153
148- /** Power-of-two size, 2x-oversized so load factor stays <= 0.5. */
149- public static int tableSizeFor (int n ) {
150- int size = 1 ;
151- while (size <= n ) {
152- size <<= 1 ;
154+ /**
155+ * Balanced default load factor — target fill {@code <= 0.5} ({@code >= 2x} capacity). (Mirrors
156+ * {@code FlatHashtable.DEFAULT_LOAD_FACTOR}; duplicated while the two are separate PRs, to be
157+ * unified when the flat-collection family converges.)
158+ */
159+ public static final float DEFAULT_LOAD_FACTOR = 0.5f ;
160+
161+ /** Sparse load factor — target fill {@code <= 0.25} ({@code >= 4x} capacity). */
162+ public static final float LOW_LOAD_FACTOR = 0.25f ;
163+
164+ /** Power-of-two capacity for {@code n} names at the {@link #DEFAULT_LOAD_FACTOR}. */
165+ public static int capacityFor (int n ) {
166+ return capacityFor (n , DEFAULT_LOAD_FACTOR );
167+ }
168+
169+ /**
170+ * Power-of-two capacity for {@code n} names at {@code loadFactor}: the smallest power of two
171+ * {@code >= ceil(n / loadFactor)} (so the achieved fill is {@code <= loadFactor}). {@code n ==
172+ * 0} yields a minimal 2-slot table (StringIndex allows the empty set, unlike FlatHashtable).
173+ */
174+ public static int capacityFor (int n , float loadFactor ) {
175+ if (n < 0 ) {
176+ throw new IllegalArgumentException ("n must be non-negative: " + n );
177+ }
178+ if (!(loadFactor > 0f && loadFactor < 1f )) {
179+ throw new IllegalArgumentException ("loadFactor must be in (0, 1): " + loadFactor );
180+ }
181+ if (n == 0 ) {
182+ return 2 ; // empty set -> minimal table (one always-empty slot suffices, 2 keeps it pow2)
153183 }
154- return size << 1 ;
184+ int min = (int ) Math .ceil (n / (double ) loadFactor );
185+ return Integer .highestOneBit (min - 1 ) << 1 ;
155186 }
156187
157188 /** Build the placed table. Returns a Data carrier; pull its arrays into your own fields. */
158189 public static Data create (String ... names ) {
159- int size = tableSizeFor (names .length );
190+ int size = capacityFor (names .length );
160191 int [] hashes = new int [size ];
161192 String [] placed = new String [size ];
162193 for (String name : names ) {
0 commit comments