How does the Java compiler implement a traditional switch in bytecode, and what scoping subtlety exists for variables declared inside a switch block?
answer
- tableswitch = dense, O(1) jump table
- lookupswitch = sparse, O(log n) binary search
- Both key on 32-bit int (=> no long)
- String: hashCode then equals; enum: ordinal
- One shared scope => wrap a case in { }
basics
~20 sThe compiler turns a switch into one of two jump instructions: tableswitch (a dense jump table) or lookupswitch (a sorted list of key/offset pairs). All cases share one scope, so a variable declared in one case is visible (but maybe uninitialized) in the others.
solid answer
~50 sA traditional switch compiles to one of two JVM instructions. When the case label values are dense (close together), the compiler emits tableswitch — a contiguous jump table indexed directly by the value, giving O(1) dispatch. When they are sparse, it emits lookupswitch — a sorted table of (key, branch-offset) pairs that the JVM binary-searches, giving O(log n). Both key on 32-bit int, which is why long is unsupported; String switches first switch on hashCode (an int) then verify with equals, and enum switches switch on ordinal. The scoping subtlety: the entire body between the braces is a single block with one shared scope. A local variable declared in one case is in scope for all later cases, so you can reference it there — but if that case was not actually executed it may be unassigned, which the compiler rejects as 'variable might not have been initialized.' The fix is to wrap a case body in its own { } block.
code
java · 28 lines// Shared-scope pitfall and its fix
int x = 2;
// BROKEN: one shared scope across all cases
/*
switch (x) {
case 1:
int total = 100; // assigned only on case 1
break;
case 2:
total += 5; // ERROR: 'total' might not have been initialized
break;
}
*/
// FIXED: per-case block scopes
switch (x) {
case 1: {
int total = 100;
System.out.println(total);
break;
}
case 2: {
int total = 5;
System.out.println(total);
break;
}
}go deeper
Not expected to know bytecode; should at least know each case isn't a fresh scope and wrapping in braces helps.
Aware that switch is faster than an if-chain and that variables declared in a case leak into the shared switch scope.
Explains tableswitch vs lookupswitch, the 32-bit-int keying (hashCode for String, ordinal for enum), and diagnoses the shared-scope/definite-assignment error.
Uses the bytecode model to reason about hot-path dispatch performance and sets idioms (block-scoped cases, enum switches) to avoid the scope pitfalls across a codebase.
## Two bytecode instructions When `javac` compiles a switch over integral/enum/String values it does **not** generate a chain of comparisons. Instead it picks one of two specialized JVM instructions, based on how the case label *values* are distributed: ### `tableswitch` — for dense values If the case values are **contiguous or nearly so** (e.g. 1,2,3,4,5), the compiler builds a **jump table**: an array of branch targets indexed directly by `value - low`. The JVM computes the index in constant time and jumps. This is **O(1)** dispatch and is the faster, preferred form. To keep tables from getting huge, the compiler may pad small gaps with entries pointing at `default`. ### `lookupswitch` — for sparse values If the values are **scattered** (e.g. 1, 100, 5000, 99999), a dense table would waste enormous space, so the compiler emits a **sorted list of `(key, offset)` pairs**. The JVM **binary-searches** the keys, giving **O(log n)** dispatch. The compiler chooses between them by weighing table density against size; you do not control it directly, but understanding it explains performance and the type restrictions. ### Why this restricts the types Both instructions key on **32-bit `int`**. That single fact explains: - **`long` is forbidden** (64-bit won't fit the key), - **`String` switches** are compiled in two steps: first a `lookupswitch` on `String.hashCode()` (an `int`), then an `.equals` check to disambiguate hash collisions, - **`enum` switches** compile to a switch on the constant's `ordinal()` (an `int`), typically via a small generated lookup array. ## The scoping subtlety The body of a switch — everything between `{` and `}` — is **one single block with one shared scope**, not a separate scope per case. The `case` labels are just jump targets *into* that one block. This has a surprising consequence for local variables: ```java switch (x) { case 1: int y = 10; // declared here System.out.println(y); break; case 2: // y is IN SCOPE here (same block) ... System.out.println(y); // COMPILE ERROR: y might not have been initialized break; } ``` Because `case 1` and `case 2` share the same scope, the **declaration** of `y` is visible in `case 2`. But if the switch jumped straight to `case 2`, the assignment `int y = 10` never ran, so `y` would be **unassigned**. Java's definite-assignment analysis catches this and refuses to compile — "variable `y` might not have been initialized." A related gotcha: you also cannot have two `case` labels each declaring a variable with the **same name**, because they live in one scope (name clash). ### The fix: give each case its own block Wrap the case body in braces to create a nested scope: ```java switch (x) { case 1: { int y = 10; // scoped to this block only System.out.println(y); break; } case 2: { int y = 20; // independent y, no clash System.out.println(y); break; } } ``` Now each `{ }` is its own scope, so the names don't collide and there is no "might not be initialized" problem. ## Why a senior should know this - It explains the **type restrictions** (the 32-bit-int key) from first principles rather than as a rule to memorize. - It explains **performance**: dense switches are essentially free dispatch; converting a long `if/else if` ladder over an int to a switch can be a real win. - The **scoping rule** is a genuine source of confusing compile errors that a senior should be able to diagnose instantly and fix with a block.
- When does the compiler choose lookupswitch over tableswitch?When the case values are sparse, so a dense indexed table would waste too much space; lookupswitch stores sorted (key,offset) pairs and binary-searches them.
- Why can't you reference a variable declared in an earlier case from a later case?All cases share one scope, so the name is visible, but if the switch jumped past the declaration the variable is unassigned — definite-assignment analysis rejects it. Wrap the case in its own { } block.
saying these in an interview costs you the question
- Saying switch compiles to a sequential if/else chain
- Thinking each case gets its own scope automatically
- Claiming you can redeclare the same variable name in two bare cases
- Assuming the developer chooses tableswitch vs lookupswitch directly