Fix java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer

intermediateโ˜• Java2026-03-24| Java 8+, any OS (Windows / Linux / macOS), any JVM-based framework (Spring, Jakarta EE, plain Java)

Error Message

java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer
#java#classcast#casting#generics#runtime-error

The Error

java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer

This one hits at runtime โ€” often in code the compiler waved through without a single warning. You wrote the cast, it compiled, everything looked fine. Then a real value flows through and the JVM objects loudly. The mismatch was always there; Java just didn't catch it until execution.

Why This Happens

Java enforces cast compatibility at runtime, not always at compile time. Three situations cause this regularly:

  • Raw types / unchecked casts โ€” using a List without generics, then casting elements back out.
  • Wrong assumptions about object type โ€” a method returns Object and you cast it to whichever type you expect to be there.
  • Deserialization / reflection โ€” reading data from JSON, YAML, a database, or a config file and assuming the type survived the round-trip intact.

Reproducing the Problem

Simplest version of the bug:

List list = new ArrayList(); // raw type โ€” no generics
list.add("hello");
Integer num = (Integer) list.get(0); // ClassCastException here

The compiler warns you with "unchecked cast" but still compiles. At runtime, the JVM sees a String where you promised an Integer โ€” crash.

Maps with Object values are another frequent offender:

Map<String, Object> config = loadConfig();
Integer timeout = (Integer) config.get("timeout"); // blows up if timeout is "30" (a String)

This pattern is everywhere in Spring config loading and custom property readers. A value that prints as 30 may actually be stored as the string "30", not the integer 30.

Fixes

Fix 1 โ€” Use Generics

Switching from raw types to properly parameterized generics lets the compiler catch type mismatches before you ever run anything.

// Before (raw type)
List list = new ArrayList();
list.add("hello");
Integer num = (Integer) list.get(0); // runtime crash

// After (generic type)
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // no cast needed, compiler-safe

No cast, no explosion. That's the goal.

Fix 2 โ€” Check with instanceof Before Casting

Dealing with Object returns or external data? Verify the type first, always:

Object value = config.get("timeout");

if (value instanceof Integer) {
    Integer timeout = (Integer) value;
    // use timeout
} else if (value instanceof String) {
    Integer timeout = Integer.parseInt((String) value);
    // use timeout
} else {
    throw new IllegalArgumentException("Unexpected type for timeout: " + value.getClass());
}

Java 16+ pattern matching condenses this nicely:

// Java 16+ pattern matching instanceof
if (value instanceof Integer timeout) {
    System.out.println("Timeout: " + timeout);
} else if (value instanceof String s) {
    System.out.println("Timeout (string): " + Integer.parseInt(s));
}

Pattern matching eliminates the extra cast line entirely โ€” the variable is already the right type inside the block.

Fix 3 โ€” Fix Deserialization and Map Type Issues

Jackson and Gson behave differently depending on the value. A JSON number like 30 deserializes as Integer when it fits, Long above 2,147,483,647, and Double if there's a decimal. You can't always predict which you'll get from a Map<String, Object>.

// Jackson: deserialize into a typed class instead of a raw Map
ObjectMapper mapper = new ObjectMapper();
MyConfig config = mapper.readValue(json, MyConfig.class); // typed, safe

// If you're stuck with Map<String, Object>, use Number as a bridge
Object raw = map.get("count");
int count = ((Number) raw).intValue(); // handles Integer, Long, Double safely

Number is the common supertype for all numeric boxed types. Casting to it first, then calling .intValue(), sidesteps the ambiguity entirely.

Fix 4 โ€” Wrap Legacy Raw Collections

Older APIs sometimes return raw List or Collection. Two options: suppress and cast, or copy defensively.

// Option A: suppress and cast (only when you're confident in the API contract)
@SuppressWarnings("unchecked")
List<String> items = (List<String>) legacyApi.getItems();

// Option B: copy with a type check (safer when the API is sketchy)
List<String> items = new ArrayList<>();
for (Object obj : legacyApi.getItems()) {
    if (obj instanceof String) {
        items.add((String) obj);
    }
    // silently skip or log unexpected types
}

Option B is slower but never throws. Use it when the data source is external or untrusted.

Reading the Stack Trace

Don't skip past the exception message โ€” it's precise:

java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer
    at com.example.MyService.processConfig(MyService.java:42)
    at com.example.MyService.init(MyService.java:18)

  • Line 42 in MyService.java โ€” that's where the cast failed. Start there.
  • The message tells you what you actually had (String) vs what you asked for (Integer).
  • Trace backwards from that line to find where the value entered the collection or was returned by a method. That's where the type got wrong.

Verifying the Fix

Three checks before you call it done:

  • Compile with javac -Xlint:unchecked MyClass.java โ€” zero unchecked cast warnings means you're clean.
  • Run unit tests with edge-case inputs: strings where integers are expected, nulls, and mixed-type collections.
  • If the bug lived in a data pipeline, write a test that feeds actual raw data โ€” not mocked, actual โ€” through the parsing path.
@Test
void testConfigParsing() {
    Map<String, Object> config = Map.of("timeout", "30"); // String, not Integer
    assertDoesNotThrow(() -> MyService.parseTimeout(config));
    assertEquals(30, MyService.parseTimeout(config));
}

Quick Tips

  • Turn on compiler warnings: javac -Xlint:all, or configure your IDE to flag unchecked casts. Don't treat them as noise โ€” each one is a potential runtime crash.
  • Return Object only when you have no choice: if a method signature returns Object, that's a design signal worth questioning. Generics or a sealed interface usually fix it cleanly.
  • Use Number as a bridge type: when you don't know if a numeric value is Integer, Long, or Double, cast to Number first, then call .intValue(). Covers all three cases.
  • Consider typed containers for mixed maps: if you genuinely need heterogeneous values in a single map, Guava's ClassToInstanceMap is purpose-built for this. It enforces type safety at the container level instead of at each get-site.

Related Error Notes