skip to content

A developer wants 'a separate static counter for each parameterization of `Counter<T>`' so `Counter<String>` and `Counter<Integer>` count independently. Why doesn't a static field in `Counter<T>` give them this, and what should they do instead?

level: seniorimportance: nice to knowfreq 22%

answer

  1. Static count is shared by ALL T → combined total, a bug
  2. Erasure: one Counter.class, no per-type slot
  3. Carry a Class<T> type token
  4. Key a Map<Class<?>, AtomicInteger> by it
  5. C# reified would do it for free; Java won't

basics

~20 s

A static field is shared by all instances of the class, and Java erases the type argument, so Counter<String> and Counter<Integer> use the very same static counter — not separate ones. To count per type, key a map by the Class object, e.g. a Map<Class<?>, Integer>.

solid answer

~50 s

Two facts defeat the wish. First, a `static` field belongs to the one class, shared by every instance regardless of its type argument. Second, due to **erasure** `Counter<String>` and `Counter<Integer>` are the same runtime class, so even conceptually there's no per-parameterization storage. A `static int count` would be a single counter incremented by all of them together. (This is also why you can't even *type* a static field with `T`.) To get independent counts you must carry the type information yourself at runtime: pass a `Class<T>` token (the common 'type token' pattern) and use it as a key, e.g. `static final Map<Class<?>, AtomicInteger> COUNTS`. Each `Counter<String>` instance, given `String.class`, increments the `String.class` entry; `Counter<Integer>` increments a different entry. The map's key replaces the runtime type distinction erasure threw away. Note: C#'s reified generics would give per-type statics for free — Java does not.

code

java · 17 lines
java
// Wrong expectation: this counter is shared across ALL parameterizations
class BadCounter<T> {
    static int count;
    BadCounter() { count++; }   // Counter<String> and Counter<Integer> both bump THIS
}

// Right: carry the type and key a concrete-typed static map
class Counter<T> {
    private static final Map<Class<?>, AtomicInteger> COUNTS = new ConcurrentHashMap<>();
    Counter(Class<T> type) {
        COUNTS.computeIfAbsent(type, k -> new AtomicInteger()).incrementAndGet();
    }
    static int countFor(Class<?> type) {
        AtomicInteger c = COUNTS.get(type);
        return c == null ? 0 : c.get();
    }
}

go deeper

for a junior

Recognizes a static field is shared by all instances, so it won't count per type.

for a middle

Adds the erasure reason and knows the fix is to key a map by the type, but may miss thread-safety.

for a senior

Implements the Class<T> type-token + concurrent map solution and explains the C# contrast and concurrency concerns.

for a principal

Weighs design alternatives (type tokens vs per-type subclasses vs registry), classloader/memory implications, and when per-type static state is even a good idea.

## The terms - **Static field**: one storage slot owned by the class, shared by every instance; not per-object. - **Type argument / parameterization**: the `<String>` in `Counter<String>`. - **Type erasure**: the type argument is removed at compile time; at runtime `Counter<String>` and `Counter<Integer>` are the **same** class `Counter` (one `Counter.class`). - **Type token / `Class<T>`**: a runtime object representing a class, e.g. `String.class` is a `Class<String>`. Passing one lets a method *know at runtime* a type that would otherwise be erased. ## The mistaken expectation The developer imagines: ```java class Counter<T> { static int count; // hoping: one counter per <T> Counter() { count++; } } ``` and expects `new Counter<String>()` to bump a String-specific counter while `new Counter<Integer>()` bumps a separate Integer one. ## Why it doesn't work — two layered reasons 1. **Static is per-class, not per-type-argument.** Even ignoring generics, a static field is shared by *all* instances of the class. So every `Counter`, whatever its `T`, increments the *same* `count`. 2. **Erasure removes the distinction anyway.** At runtime there is only one `Counter` class; `Counter<String>` and `Counter<Integer>` are indistinguishable. There is no separate runtime type to attach a separate counter to. (And recall you can't even declare `static T something` — the class's `T` is illegal in a static context.) Result: the single shared `count` is the combined total, not per-type — almost certainly a bug. ## The correct approach: carry the type yourself Since the runtime forgot the type, you must supply it. Pass a **type token** and key a map by it: ```java import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; class Counter<T> { private static final Map<Class<?>, AtomicInteger> COUNTS = new ConcurrentHashMap<>(); private final Class<T> type; Counter(Class<T> type) { this.type = type; COUNTS.computeIfAbsent(type, k -> new AtomicInteger()).incrementAndGet(); } static int countFor(Class<?> type) { AtomicInteger c = COUNTS.get(type); return c == null ? 0 : c.get(); } } // new Counter<>(String.class); new Counter<>(String.class); new Counter<>(Integer.class); // Counter.countFor(String.class) == 2 // Counter.countFor(Integer.class) == 1 ``` Key points: - The static field is now a `Map<Class<?>, AtomicInteger>` — a *concrete* type, perfectly legal as a static. - `Class<T>` is the runtime evidence of the type argument that erasure stripped; the map key restores the per-type distinction. - `ConcurrentHashMap` + `AtomicInteger` make the counting thread-safe (a shared static touched by many threads). ## Alternatives & caveats - **Distinct subclasses**: `class StringCounter extends Counter<String> {}` would have its *own* class object, hence its own statics — but that's a separate class per type, not a parameterized generic, and doesn't scale to arbitrary `T`. - **C# contrast**: with reified generics, `static int count;` in `Counter<T>` genuinely gives one counter per type argument. Engineers coming from C# are exactly the ones who hit this Java pitfall. - **Memory note**: a `Map<Class<?>, ...>` can pin `Class` objects; in dynamic-classloading environments consider weak keys if needed. ## Takeaway 'Per-parameterization static state' is impossible in Java because static is per-class *and* generics are erased. When you truly need per-type state, make the type explicit with a `Class<T>` token and key a concrete-typed static map by it.

  • Why is `static int count` shared even ignoring generics?
    Because any static field belongs to the class itself, so every instance — regardless of type argument — reads and writes the same single slot.
  • How does the Class<T> token solve it?
    It supplies, at runtime, the type information erasure removed. Used as a map key it gives each type its own counter entry.
  • Why AtomicInteger / ConcurrentHashMap here?
    The static state is shared across all instances and likely multiple threads; atomic increments and a concurrent map avoid lost updates and corruption.

saying these in an interview costs you the question

  • Believing a static field auto-separates per type argument (C# behavior, not Java).
  • Trying `static T` to fix it — that won't even compile.
  • Forgetting thread-safety on the shared static counter map.

context