# Data Structures and Algorithmic Optimization for Java

This guide covers the non-obvious parts of choosing the right data structure in Java -- the tradeoffs that matter in practice when profiling reveals collection operations as hotspots. For JIT-level behavior, see the memory and CPU agent references.

## The Decision Framework

Before changing any data structure, answer three questions:

1. **What operations dominate?** (lookup, insert, delete, iterate, sort, concurrent access)
2. **What's the data size?** (10 items = doesn't matter; 10K+ = big-O matters; 1M+ = memory layout matters)
3. **Is this on the hot path?** (confirm with JFR/async-profiler before optimizing)

If you can't answer #3 with profiler data, profile first. Never optimize cold code.

## Collection Selection Guide

### When to use what

| Need | Use | Not | Why |
|------|-----|-----|-----|
| Indexed access + append | `ArrayList` | `LinkedList` | ArrayList is cache-friendly, O(1) amortized append. LinkedList has pointer-chasing overhead on every access. |
| FIFO queue (add tail + remove head) | `ArrayDeque` | `LinkedList` | ArrayDeque uses a circular array -- no per-element node allocation, better cache locality. |
| Key-value lookup | `HashMap` | `TreeMap` | HashMap is O(1) average. TreeMap is O(log n). Use TreeMap only when you need sorted key order. |
| Sorted key-value | `TreeMap` | sorted `HashMap` | TreeMap maintains sorted order automatically. Sorting a HashMap on every read is O(n log n). |
| Insertion-ordered key-value | `LinkedHashMap` | manual tracking | LinkedHashMap maintains insertion order with near-HashMap performance. |
| Enum keys | `EnumMap` | `HashMap<MyEnum, V>` | EnumMap uses a flat array indexed by ordinal -- no hashing, no collisions, ~2x faster and less memory. |
| Membership testing | `HashSet` | `ArrayList.contains()` | HashSet is O(1) average, ArrayList.contains is O(n). Crossover at ~10-30 elements depending on element type. |
| Sorted unique elements | `TreeSet` | sorted `HashSet` | TreeSet maintains sorted order. Re-sorting a HashSet on read is O(n log n). |
| Enum members | `EnumSet` | `HashSet<MyEnum>` | EnumSet uses a bitmask internally -- O(1) add/contains/remove, near-zero memory overhead. |
| Priority queue | `PriorityQueue` | sorted list | PriorityQueue is O(log n) add/poll. Re-sorting a list after each add is O(n log n). |
| Stack (LIFO) | `ArrayDeque` | `Stack` | Stack is synchronized (unnecessary overhead). ArrayDeque is faster for single-threaded use. |
| Thread-safe map | `ConcurrentHashMap` | `Collections.synchronizedMap` | ConcurrentHashMap uses stripe-level locking -- concurrent reads are lock-free, concurrent writes lock only the affected segment. synchronizedMap locks the entire map on every operation. |
| Thread-safe list (read-heavy) | `CopyOnWriteArrayList` | `Collections.synchronizedList` | CopyOnWriteArrayList has zero-cost reads (no locking). Writes copy the entire array -- only use when reads vastly outnumber writes. |
| Immutable list | `List.of(...)` (JDK 9+) | `Collections.unmodifiableList` | `List.of` returns a compact, specialized implementation (0, 1, 2, or N elements). `unmodifiableList` wraps a mutable list with delegation overhead. |
| Bit flags / dense integer sets | `BitSet` | `HashSet<Integer>` | BitSet uses ~1 bit per element vs ~48 bytes per Integer in HashSet. Orders of magnitude less memory for dense integer ranges. |

### Crossover points: when switching containers matters

These are approximate -- always benchmark with your actual data and JDK version.

| Scenario | Crossover | Notes |
|----------|-----------|-------|
| `ArrayList.contains()` vs `HashSet.contains()` | ~10-30 elements | Below this, ArrayList scan beats hashing overhead. Above it, O(n) vs O(1) dominates. |
| `HashMap` vs `EnumMap` | Always prefer EnumMap for enum keys | Even at 2-3 entries, EnumMap is faster and uses less memory. No hashing, no collision handling. |
| `HashMap` vs linear search in array | ~5-8 entries | For tiny maps, array scan avoids hashing overhead. HashMap wins as soon as n exceeds the hash computation cost. |
| `TreeMap` vs sorted `ArrayList` + binary search | Read-heavy: ArrayList wins. Write-heavy: TreeMap wins. | ArrayList binary search is O(log n) but cache-friendly. TreeMap insertion is O(log n) without copy. |
| `ConcurrentHashMap` vs `synchronized HashMap` | Any contention level | ConcurrentHashMap is never slower for reads and dramatically faster under concurrent writes. |

### Memory overhead per element (approximate, 64-bit JVM, compressed oops)

| Container | Per-element overhead | Base overhead | Notes |
|-----------|---------------------|---------------|-------|
| `ArrayList` | 4 bytes (reference) | 48 bytes | Over-allocates by 50% on grow. Trim with `trimToSize()`. |
| `LinkedList` | 24 bytes (Node: prev + next + item) | 48 bytes | 6x more memory per element than ArrayList. |
| `HashMap` | ~48 bytes (Entry: hash + key + value + next) | 64 + 16*capacity bytes | Load factor 0.75 means 25% empty slots. |
| `TreeMap` | ~48 bytes (Entry: key + value + left + right + parent + color) | 48 bytes | No wasted slots but high per-node overhead. |
| `EnumMap` | 4 bytes (reference in array) | 16 + 4*enum.values().length bytes | Flat array, minimal overhead. |
| `HashSet` | ~48 bytes (backed by HashMap) | Same as HashMap | A HashSet IS a HashMap<E, PRESENT>. |
| `EnumSet` | ~0.015 bytes (1 bit in long) | 24 bytes for RegularEnumSet | Bit vector. Minimal memory for up to 64 enum values. |
| `ArrayDeque` | 4 bytes (reference) | 48 bytes | Circular buffer, doubles on grow. |
| `int[]` | 4 bytes | 16 bytes | No boxing. Baseline for comparison. |
| `Integer[]` | ~20 bytes (16 header + 4 payload) | 16 bytes | 5x more than int[]. |

## Autoboxing Costs

Autoboxing is the silent performance killer in Java. Every `int` -> `Integer` conversion allocates an object on the heap (unless it hits the Integer cache).

### Integer cache

Java caches `Integer` values from -128 to 127. Values in this range return the same object -- no allocation. Values outside this range allocate a new `Integer` every time.

```java
// No allocation -- cached
Integer a = 42;      // Integer.valueOf(42) -> cached
Integer b = 42;      // same object as a
assert a == b;       // true (same reference)

// Allocation every time
Integer c = 200;     // Integer.valueOf(200) -> new Integer(200)
Integer d = 200;     // different object
assert c != d;       // true (different references -- use .equals())
```

### Where autoboxing hides

```java
// HIDDEN AUTOBOXING: HashMap<Integer, Double> boxes every key and value
Map<Integer, Double> scores = new HashMap<>();
for (int i = 0; i < 1_000_000; i++) {
    scores.put(i, i * 1.5);  // boxes i to Integer, i*1.5 to Double -- 2 allocations per iteration
}

// FIX: use primitive collections (Eclipse Collections)
IntDoubleHashMap scores = new IntDoubleHashMap(1_000_000);
for (int i = 0; i < 1_000_000; i++) {
    scores.put(i, i * 1.5);  // no boxing
}
```

### Primitive collection libraries

When profiling shows autoboxing as a significant allocator, consider:

| Library | Key types | Notes |
|---------|-----------|-------|
| **Eclipse Collections** | IntList, IntIntHashMap, IntObjectHashMap, etc. | Full primitive collection suite. Production-ready, widely used. |
| **fastutil** | Int2IntOpenHashMap, IntArrayList, etc. | Lower-level, slightly faster in microbenchmarks. |
| **HPPC** | IntIntHashMap, IntArrayList | Smallest dependency footprint. |
| **Koloboke** | HashIntIntMap | Highest throughput for primitive maps in benchmarks. Less actively maintained. |

**Decision**: Eclipse Collections is the safest default -- broadest API, best documentation, active maintenance. Use fastutil if Eclipse Collections is already not a dependency and you need the smallest possible addition.

## String Optimization

### StringBuilder vs StringBuffer vs String.concat

| Method | Thread-safe | Performance | Use when |
|--------|-------------|-------------|----------|
| `StringBuilder` | No | Fastest | Building strings in loops, single-threaded |
| `StringBuffer` | Yes (synchronized) | ~30% slower than StringBuilder | Almost never -- use StringBuilder + external sync if needed |
| `String.concat()` / `+` | N/A | JDK 9+ uses `invokedynamic` + `StringConcatFactory` | Simple concatenation of 2-3 strings. JDK 9+ optimizes this well. |

```java
// BAD: string concatenation in loop creates intermediate String objects (pre-JDK 9)
String result = "";
for (String s : items) {
    result += s + ",";  // pre-JDK 9: O(n^2). JDK 9+: still creates intermediates per iteration.
}

// GOOD: StringBuilder
StringBuilder sb = new StringBuilder(items.size() * 20);  // estimate capacity
for (String s : items) {
    sb.append(s).append(',');
}
String result = sb.toString();

// BETTER: String.join (JDK 8+)
String result = String.join(",", items);
```

### String.intern() trade-offs

`String.intern()` stores the string in the JVM's string table (in metaspace). Subsequent calls with the same value return the interned instance, saving heap memory.

**When it helps**: Known, bounded sets of repeated strings (country codes, status values, column names). Can save significant heap when millions of objects share the same small set of string values.

**When it hurts**: Unbounded or high-cardinality strings (user IDs, URLs). The string table grows without bound, increasing metaspace usage and GC overhead. The string table lookup itself has a hash-based cost.

**Alternative**: For bounded deduplication, use a private `ConcurrentHashMap<String, String>` so you control the cache size and eviction.

### Compact Strings (JDK 9+)

Since JDK 9, `String` uses a `byte[]` internally instead of `char[]`. Strings that contain only Latin-1 characters use 1 byte per character (LATIN1 encoding). Strings with non-Latin-1 characters use 2 bytes per character (UTF16 encoding). This halves memory for ASCII-heavy workloads.

**Impact**: If your application stores millions of strings that are predominantly ASCII, you get ~50% string memory reduction for free on JDK 9+. No code changes needed.

## Algorithm Patterns

### The Index-First Pattern (O(n*m) -> O(n+m))

The single most impactful optimization pattern. Whenever you see nested loops where the inner loop searches for a match:

```java
// BEFORE: O(n * m)
for (Order order : orders) {
    for (Customer customer : customers) {
        if (customer.getId().equals(order.getCustomerId())) {
            process(order, customer);
            break;
        }
    }
}

// AFTER: O(n + m)
Map<Long, Customer> customerById = new HashMap<>(customers.size() * 4 / 3 + 1);
for (Customer c : customers) {
    customerById.put(c.getId(), c);
}
for (Order order : orders) {
    Customer customer = customerById.get(order.getCustomerId());
    if (customer != null) {
        process(order, customer);
    }
}
```

### Stream vs Loop Performance

Streams add overhead from lambda allocation, iterator creation, and pipeline composition. For simple operations on small-to-medium collections, traditional loops are faster.

```java
// Stream: cleaner, but ~2-5x slower for simple operations on small collections
int sum = items.stream()
    .filter(i -> i.isActive())
    .mapToInt(Item::getValue)
    .sum();

// Loop: faster for hot paths
int sum = 0;
for (Item item : items) {
    if (item.isActive()) {
        sum += item.getValue();
    }
}
```

**When streams win**: Code clarity for complex pipelines (multi-stage filter/map/reduce). When readability is more important than nanosecond-level performance. When using `parallelStream()` on CPU-heavy operations with large collections.

**When loops win**: Hot paths identified by profiling. Simple operations where stream overhead is proportionally significant. When you need early termination control.

### Parallel Stream Pitfalls

`parallelStream()` uses the common `ForkJoinPool` (shared across the entire JVM). Blocking operations in parallel streams can exhaust the common pool and affect unrelated code.

```java
// DANGEROUS: blocking I/O in parallel stream
items.parallelStream()
    .map(item -> fetchFromNetwork(item))  // blocks a ForkJoinPool thread
    .collect(Collectors.toList());

// If ForkJoinPool commonPool has 7 threads (default: processors - 1),
// only 7 items process concurrently. All OTHER parallelStream users in the JVM are blocked.
```

**Rules for parallel streams**:
- Only use for CPU-bound operations (not I/O)
- Collection must be large enough (>10K elements typically) for parallelism to overcome overhead
- Source must support efficient splitting (`ArrayList`, `int[]` split well; `LinkedList`, `Stream.iterate` do not)
- Operation must be stateless and associative (no shared mutable state)

## JIT-Friendly Patterns

### Method inlining

The JIT compiler inlines methods smaller than 325 bytecodes (default `-XX:MaxInlineSize=325`). Frequently called methods up to 35 bytecodes (`-XX:FreqInlineSize=325` default, but effective threshold is ~35 for hot methods) are inlined more aggressively.

**Why it matters**: Inlining eliminates call overhead AND enables further optimizations (escape analysis, constant folding, dead code elimination) that can only happen when the JIT sees the full code path.

**What prevents inlining**:
- Methods over the bytecode threshold (refactor into smaller methods)
- `native` methods (JNI calls are never inlined)
- Megamorphic call sites (>2 concrete types at a call site prevent inlining)
- Explicit `synchronized` blocks (some JIT paths avoid inlining synchronized code)

### Loop unrolling

The JIT unrolls counted loops (loops with a known trip count and simple induction variable). Ensure loops use `int` index variables and access arrays directly for best unrolling.

```java
// JIT-friendly: counted loop with array access
for (int i = 0; i < array.length; i++) {
    sum += array[i];
}

// Less JIT-friendly: iterator-based loop (iterator allocation, hasNext/next virtual calls)
for (int value : collection) {
    sum += value;
}
```

### Escape analysis

The JIT can allocate objects on the stack (scalar replacement) if it can prove the object does not escape the method. This eliminates heap allocation and GC pressure entirely.

**What breaks escape analysis**:
- Object stored in a field (`this.cache = new Result(...)`)
- Object passed to a method the JIT can't inline (too large, native, polymorphic)
- Object stored in an array (arrays are not scalar-replaced)
- Object used in `synchronized` block

## Sorting

### Arrays.sort

- **Primitive arrays**: Dual-pivot quicksort. O(n log n) average, O(n^2) worst case. In-place, not stable.
- **Object arrays**: TimSort (merge sort variant). O(n log n) guaranteed. Stable. Uses O(n) temporary storage.

### Collections.sort

Delegates to `Arrays.sort` on the backing array. Same TimSort behavior for objects.

### Sorting tips

- **Pre-sorted or nearly-sorted data**: TimSort detects runs and merges them efficiently -- O(n) for already-sorted input.
- **Custom comparators**: Use `Comparator.comparingInt()` / `comparingLong()` to avoid autoboxing in sort comparisons.
- **Partial sort (top-k)**: Use `PriorityQueue` of size k instead of sorting the entire collection. O(n log k) vs O(n log n).

```java
// BAD: sort entire list to get top 10
list.sort(Comparator.comparingInt(Item::getScore).reversed());
List<Item> top10 = list.subList(0, 10);

// GOOD: PriorityQueue for top-k -- O(n log k)
PriorityQueue<Item> heap = new PriorityQueue<>(10, Comparator.comparingInt(Item::getScore));
for (Item item : list) {
    heap.offer(item);
    if (heap.size() > 10) {
        heap.poll();  // evict smallest
    }
}
List<Item> top10 = new ArrayList<>(heap);
```

## Common Traps

### HashMap initial capacity

HashMap resizes when `size > capacity * loadFactor` (default loadFactor = 0.75). Each resize rehashes all entries. If you know the expected size, pre-size to avoid resizes:

```java
// BAD: default capacity 16, resizes multiple times for 1000 entries
Map<String, Value> map = new HashMap<>();

// GOOD: pre-sized to avoid any resizing
// Formula: expectedSize / loadFactor + 1
Map<String, Value> map = new HashMap<>(1000 * 4 / 3 + 1);

// JDK 19+: factory method handles the math
Map<String, Value> map = HashMap.newHashMap(1000);
```

### Iteration after heavy deletion

Like Python dicts, iterating a HashMap after heavy deletion still walks the full table. Rebuild the map to compact it:

```java
// After deleting 90% of entries, iteration is still O(original_capacity)
map = new HashMap<>(map);  // rebuild to compact
```

### Collections.unmodifiableList is NOT immutable

`Collections.unmodifiableList(list)` returns a view -- the backing list can still be mutated. Use `List.of(...)` or `List.copyOf(list)` (JDK 10+) for true immutability.

### toArray() patterns

```java
// JDK 11+: preferred -- JIT can optimize the empty array hint
String[] arr = list.toArray(String[]::new);

// Pre-JDK 11: avoid pre-sized array (counter-intuitively slower due to reflection + zeroing)
String[] arr = list.toArray(new String[0]);  // faster than new String[list.size()]
```

## Profiling for Data Structure Issues

### Identifying the problem

Data structure issues manifest as:
- **High CPU in collection operations**: `HashMap.get`, `ArrayList.contains`, `TreeMap.put` high in profiler
- **High allocation rate from collections**: JFR shows `java.util.HashMap$Node` or `Integer` as top allocators
- **Unexpected quadratic scaling**: Doubling input size -> 4x runtime
- **GC pressure from autoboxing**: JFR allocation profiling shows boxed primitives as dominant allocators

### JFR workflow

```bash
# Record allocation profiling
jcmd <pid> JFR.start name=alloc settings=profile duration=60s filename=/tmp/alloc.jfr

# Or from command line
java -XX:StartFlightRecording=name=alloc,settings=profile,duration=60s,filename=/tmp/alloc.jfr ...
```

Look for `jdk.ObjectAllocationInNewTLAB` and `jdk.ObjectAllocationOutsideTLAB` events to find dominant allocators.

### Scaling test

The most reliable way to confirm a complexity issue:

```java
for (int scale : new int[]{1, 2, 4, 8}) {
    List<Item> data = generateTestData(1000 * scale);
    long start = System.nanoTime();
    targetMethod(data);
    long elapsed = System.nanoTime() - start;
    System.out.printf("n=%d  time=%dms%n", 1000 * scale, elapsed / 1_000_000);
}
```

If time quadruples when n doubles = O(n^2). If time doubles = O(n log n) or O(n).

## Pitfalls

- **Don't optimize cold code**: Profile first. The slow collection might only be populated once at startup.
- **Don't assume data size**: An O(n^2) algorithm on 10 items is faster than O(n) with a HashMap due to constant factors.
- **HashSet is not always faster**: For collections under ~10 elements, ArrayList.contains can beat HashSet due to cache locality and no hashing overhead.
- **LinkedList is almost never the right choice**: Despite its O(1) add/remove at known positions, pointer chasing and poor cache locality make it slower than ArrayList for nearly all real workloads. Use ArrayDeque for queue/deque needs.
- **EnumMap/EnumSet are always worth it**: If your keys are enum values, EnumMap and EnumSet are strictly better -- faster and less memory. There is no crossover point where HashMap wins.
- **Streams have startup cost**: The first stream operation in a method pays lambda linkage cost (~1ms). This is amortized across calls but can dominate for infrequently-called methods.
- **ConcurrentHashMap.size() is expensive**: It traverses all segments. Use `mappingCount()` for an estimate or track size externally if you need it frequently.

## Collection Contract Traps

Patterns that compile but silently break at runtime:

| Trap | Example | Fix |
|------|---------|-----|
| **Iteration order** | `HashMap` order changes between JDK versions | Use `LinkedHashMap` if order matters |
| **Duplicate handling** | `Set.add()` silently drops duplicates | Check return value or use `List` if dups expected |
| **Null keys/values** | `ConcurrentHashMap` throws NPE on null; `HashMap` allows it | Validate before insert or use `Optional` |
| **Thread safety** | `HashMap.put` from two threads corrupts internal table | `ConcurrentHashMap` or external synchronization |
| **Sorted iteration** | `TreeMap` comparator must be consistent with `equals` | Implement both `compareTo` and `equals` correctly |
| **Kotlin type constraints** | `MutableMap<K, V>` erases to `Map` at runtime | Use `@JvmSuppressWildcards` for generic interop |

## Polymorphic Dispatch Safety

When optimizing collection operations or changing data structures, these Java-specific traps cause correctness regressions that compile cleanly but break at runtime.

### Type erasure traps

Generic types are erased at runtime. Guards that depend on type parameters fail silently:

```java
// DANGEROUS: raw type check is true for ANY List, not just List<String>
if (items instanceof List) {
    // Fast path assumes String elements -- ClassCastException at runtime
    // if items is actually List<Integer>
}
```

When swapping generic collections, verify that all consumers handle the actual element types, not just the declared generic type. This is especially dangerous when replacing `Object[]` with typed collections -- the array version fails visibly with `ArrayStoreException`, but generics erase silently.

### equals/hashCode contract violations

Changing collection types can break equality contracts:

| Change | Risk |
|--------|------|
| `ArrayList` -> `HashSet` | Elements MUST have consistent `equals`/`hashCode`. If they don't, `HashSet` silently drops or misplaces elements. |
| `HashMap` key type change | New key type must implement `equals`/`hashCode`. Mutable keys corrupt the bucket structure if modified after insertion. |
| `TreeMap`/`TreeSet` with `Comparator` | Comparator MUST be consistent with `equals`: `compareTo(x) == 0` must imply `x.equals(y)`. Inconsistency causes silent data loss. |
| Custom object as `Map` key | If you add `hashCode` where there was none, verify ALL existing call sites that rely on reference equality (`==`). |
| `EnumMap`/`EnumSet` | Only works with enum keys/elements. If code later adds non-enum subtypes, it breaks. |

### Interface default method conflicts

When restructuring to use a different interface:

```java
// If you change from implementing InterfaceA to InterfaceB,
// and both define default method process(), the class MUST override it.
// If the class already has process(), the semantics may differ
// between InterfaceA.process() and InterfaceB.process().
// Verify the override behavior matches both contracts.
```

### Sealed class hierarchy changes (JDK 17+)

If the target class is part of a sealed hierarchy (`sealed class Shape permits Circle, Square`), changing its data structures may affect pattern matching exhaustiveness in `switch` expressions. Verify all `switch` statements and `instanceof` pattern matches over the sealed type still cover all permitted subtypes.

### Covariant return type assumptions

When changing a method's return type to a more specific collection:

```java
// Before: List<Item> getItems() -- could return ArrayList, LinkedList, etc.
// After:  List<Item> getItems() -- now returns List.of(...) (unmodifiable)
// Risk: callers that call .add(), .sort(), or cast to ArrayList will break
//       at RUNTIME with UnsupportedOperationException
```

Always check return type contracts: if the method's contract (or callers' assumptions) says "returns a mutable list," returning an immutable or unmodifiable list breaks callers even though it compiles.

### Verification checklist for collection type changes

Before committing any collection type swap:

1. `grep` for all usages of the changed field/variable/return type
2. Check `equals`/`hashCode` on element types (for `Set`/`Map` changes)
3. Check if the collection is serialized (Java serialization, Jackson, protobuf)
4. Check if the collection is exposed via a public API (return type or parameter)
5. Check for iteration order dependencies (`HashMap` -> `LinkedHashMap` if order matters)
6. Run the full test suite, not just the target test -- collection type changes have far-reaching effects

## Reflection to MethodHandle Migration

When profiling reveals `Class.forName`, `getDeclaredMethod`, or `newInstance` on a hot path:

```java
// BEFORE: reflective call per invocation (~200ns overhead)
Method m = clazz.getDeclaredMethod("process", String.class);
m.invoke(instance, arg);

// AFTER: cache MethodHandle at class load (~2ns per call)
private static final MethodHandle PROCESS_MH;
static {
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    PROCESS_MH = lookup.findVirtual(Target.class, "process",
        MethodType.methodType(void.class, String.class));
}
// ...
PROCESS_MH.invoke(instance, arg);
```

For dynamic dispatch: use `LambdaMetafactory` to generate a functional interface at linkage time — eliminates reflection entirely.

## JMH Benchmark Template

Standard template for validating collection/algorithm changes:

```java
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 10, time = 1)
@Fork(2)
public class CollectionBenchmark {

    @Param({"100", "10000", "1000000"})
    int size;

    private List<Integer> data;

    @Setup(Level.Trial)
    public void setup() {
        data = ThreadLocalRandom.current()
            .ints(size).boxed().collect(Collectors.toList());
    }

    @Benchmark
    public int baseline(Blackhole bh) {
        // original implementation
    }

    @Benchmark
    public int optimized(Blackhole bh) {
        // optimized implementation
    }
}
```

**Key rules:** Always use `Blackhole` or return results (prevent DCE). Use `@Param` for multiple sizes (prevent constant folding). Use `@Fork(2)+` to isolate JIT behavior across runs.
