Fix: Java ClassCastException: class X cannot be cast to class Y
Quick Answer
How to fix Java ClassCastException by using instanceof checks, fixing generic type erasure, resolving ClassLoader conflicts, correcting raw types, and using pattern matching in Java 16+.
The Error
You run your Java application and it crashes with:
Exception in thread "main" java.lang.ClassCastException: class com.example.Dog cannot be cast to class com.example.Cat
at com.example.Main.main(Main.java:15)Or you see a variation like:
java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.IntegerIn modular or container environments, you might encounter the longer form:
java.lang.ClassCastException: class com.example.User (in module app) cannot be cast to class com.example.User (in module lib)The JVM throws ClassCastException when you try to cast an object to a type it does not belong to. This is a runtime error — the compiler cannot always catch it, especially when generics, raw types, or reflection are involved.
Why This Happens
Java is a strongly typed language. Every object has a concrete type determined at instantiation time. When you write a cast like (Cat) animal, you are telling the JVM: “trust me, this object is a Cat.” If it is not, the JVM throws a ClassCastException instead of allowing corrupted data to propagate silently.
Several common scenarios trigger this error:
- Unsafe downcasting without checking the actual type first.
- Generic type erasure — at runtime,
List<String>andList<Integer>are both justList, so the JVM cannot enforce generic types during casting. - ClassLoader conflicts — two different ClassLoaders load the same class, producing two distinct
Classobjects that the JVM considers incompatible even though the bytecode is identical. - Raw types in collections — using unparameterized collections allows any object type to sneak in, causing cast failures when you retrieve elements.
- Incorrect type hierarchies — casting between sibling classes or unrelated interfaces that share no inheritance relationship.
- Serialization round-trips — deserializing an object can produce a type from a different ClassLoader or a different version of the class.
- Spring and DI containers — proxy objects, AOP wrappers, and CGLIB subclasses can cause unexpected type mismatches.
Understanding which scenario applies to your case determines the correct fix.
Fix 1: Use instanceof Before Casting
The most direct cause of ClassCastException is an unguarded downcast. Always verify the type before casting.
Wrong:
Object obj = getResult();
String value = (String) obj; // ClassCastException if obj is not a StringRight:
Object obj = getResult();
if (obj instanceof String) {
String value = (String) obj;
System.out.println(value);
} else {
System.err.println("Unexpected type: " + obj.getClass().getName());
}This pattern is essential anywhere you receive objects from generic APIs, framework callbacks, or deserialization methods where the return type is Object.
For method parameters, add a guard at the top:
public void process(Object input) {
if (!(input instanceof MyDto)) {
throw new IllegalArgumentException(
"Expected MyDto but got " + input.getClass().getName()
);
}
MyDto dto = (MyDto) input;
// safe to use dto
}Throwing IllegalArgumentException with a descriptive message is far more debuggable than letting a raw ClassCastException surface from deep inside your logic.
Pro Tip: When you catch yourself writing multiple
instanceofchecks in a chain, consider whether the polymorphism is missing a proper interface or visitor pattern. Excessive casting often signals a design issue upstream.
Fix 2: Fix Generic Type Erasure Issues
Java generics are erased at compile time. This means List<String> becomes List in the bytecode, and the JVM has no knowledge of the generic type parameter at runtime. This leads to subtle ClassCastException errors that compile without warnings.
The trap:
List<String> strings = new ArrayList<>();
strings.add("hello");
List rawList = strings; // no warning in older Java, unchecked warning in modern Java
rawList.add(42); // compiles fine -- raw type bypasses generics
for (String s : strings) { // ClassCastException on the Integer element
System.out.println(s);
}The cast to String is inserted by the compiler automatically during iteration. When it hits the Integer value 42, it throws ClassCastException.
Fix this by:
- Never using raw types. Always parameterize your collections:
List<String> strings = new ArrayList<>();- Enabling compiler warnings and treating them as errors in your build:
<!-- Maven -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-Xlint:unchecked</arg>
<arg>-Werror</arg>
</compilerArgs>
</configuration>
</plugin>- Using
Collections.checkedList()for runtime type safety when you must interact with raw-type APIs:
List<String> safeList = Collections.checkedList(new ArrayList<>(), String.class);
safeList.add("hello"); // fine
safeList.add((Object) 42); // throws ClassCastException immediately at insertionThis catches the problem at the point of insertion rather than at retrieval, making debugging far easier.
If you are dealing with similar type confusion issues in other languages, the approach differs — for example, NullPointerException in Java has its own set of patterns for safe access.
Fix 3: Fix ClassLoader Conflicts (Same Class, Different Loaders)
This is one of the most confusing ClassCastException scenarios. The error message looks like:
java.lang.ClassCastException: class com.example.User cannot be cast to class com.example.UserThe class name is identical on both sides. This happens because two different ClassLoaders loaded the same .class file, and the JVM treats each loaded version as a distinct type.
Common causes:
- Application servers (Tomcat, WildFly) using separate ClassLoaders per WAR deployment.
- OSGi bundles with mismatched imports/exports.
- Custom ClassLoaders in plugin architectures.
- Hot-reloading tools like Spring DevTools creating a restart ClassLoader.
Diagnose it by printing the ClassLoader of both sides:
System.out.println("Source ClassLoader: " + obj.getClass().getClassLoader());
System.out.println("Target ClassLoader: " + User.class.getClassLoader());If these print different ClassLoader instances, you have confirmed the problem.
Fix strategies:
Move the shared class to a parent ClassLoader. In application servers, place the JAR in the server’s shared lib directory (e.g.,
$CATALINA_HOME/libfor Tomcat) so both deployments use the same ClassLoader.In Spring Boot with DevTools, exclude problematic classes from the restart ClassLoader:
# application.properties
spring.devtools.restart.exclude=com/example/shared/**- Use interfaces from a common module. Instead of casting to a concrete class, cast to an interface loaded by a parent ClassLoader:
// Interface in shared module (parent ClassLoader)
public interface Identifiable {
String getId();
}
// Cast to interface instead of concrete class
Identifiable entity = (Identifiable) obj;This pattern is also relevant when dealing with ClassNotFoundException errors, which share the same ClassLoader root causes.
Fix 4: Fix Collection Raw Types (List vs List<String>)
Raw types are the single biggest source of ClassCastException in legacy Java codebases. When you use List instead of List<String>, the compiler cannot enforce what goes into the collection.
The problem in legacy code:
// Old method returns raw List
public List getUsers() {
List users = new ArrayList();
users.add(new User("Alice"));
users.add("not a user"); // compiles fine with raw type
return users;
}
// Caller assumes List<User>
List<User> users = getUsers(); // unchecked assignment
for (User u : users) { // ClassCastException on "not a user"
System.out.println(u.getName());
}Fix this by adding generics to the legacy method:
public List<User> getUsers() {
List<User> users = new ArrayList<>();
users.add(new User("Alice"));
// users.add("not a user"); // now a compile error
return users;
}If you cannot modify the legacy code (e.g., a third-party library), filter the list safely:
List<?> rawList = legacyApi.getResults();
List<User> users = new ArrayList<>();
for (Object item : rawList) {
if (item instanceof User) {
users.add((User) item);
} else {
log.warn("Skipping unexpected type: {}", item.getClass().getName());
}
}For Map types, the same pattern applies:
// Wrong
Map config = loadConfig(); // raw type
String value = (String) config.get("timeout"); // ClassCastException if value is Integer
// Right
Map<String, Object> config = loadConfig();
Object raw = config.get("timeout");
if (raw instanceof String s) {
// use s
} else if (raw instanceof Integer i) {
// use i
}Fix 5: Use Proper Type Hierarchies (Interfaces, Abstract Classes)
ClassCastException often reveals a design problem. If you are casting between sibling classes, no amount of runtime checking fixes the architectural issue.
Wrong — casting between siblings:
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
Animal animal = new Dog();
Cat cat = (Cat) animal; // ClassCastException: Dog cannot be cast to CatDog and Cat are siblings in the hierarchy. A Dog instance will never be a Cat, regardless of any runtime conditions.
Fix by programming to interfaces:
interface Feedable {
void feed(String food);
}
class Dog extends Animal implements Feedable {
public void feed(String food) { /* ... */ }
}
class Cat extends Animal implements Feedable {
public void feed(String food) { /* ... */ }
}
// No casting needed
Feedable pet = getPet();
pet.feed("kibble");When you need type-specific behavior, use the visitor pattern or sealed classes (Java 17+) instead of casting:
sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
};
}Sealed classes combined with pattern matching eliminate the need for explicit casts entirely. The compiler verifies exhaustiveness, so you cannot miss a case.
This kind of type-safety issue is conceptually related to NoSuchMethodError, where the JVM expects one method signature but finds another at runtime.
Fix 6: Fix Serialization/Deserialization Casts
Serialization introduces ClassCastException in two ways: the deserialized object might be a different type than expected, or it might be loaded by a different ClassLoader.
JSON deserialization (Jackson):
ObjectMapper mapper = new ObjectMapper();
// Wrong: assuming the JSON always maps to User
User user = mapper.readValue(json, User.class); // works
// But if the JSON structure doesn't match:
Object result = mapper.readValue(json, Object.class);
User user = (User) result; // ClassCastException -- result is a LinkedHashMapJackson deserializes to LinkedHashMap when the target type is Object. Fix this by always specifying the correct target type:
User user = mapper.readValue(json, User.class);
// For collections, use TypeReference:
List<User> users = mapper.readValue(json, new TypeReference<List<User>>() {});Java native serialization:
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.ser"));
User user = (User) ois.readObject(); // ClassCastException if class changedThis fails when the serialized class has a different serialVersionUID or when the deserializing JVM uses a different ClassLoader. Fix it by:
- Explicitly declaring
serialVersionUID:
public class User implements Serializable {
private static final long serialVersionUID = 1L;
// fields...
}- Adding an
instanceofcheck before casting:
Object obj = ois.readObject();
if (obj instanceof User user) {
// safe to use
} else {
throw new IllegalStateException("Expected User, got " + obj.getClass().getName()
+ " from ClassLoader " + obj.getClass().getClassLoader());
}Common Mistake: When using Jackson with polymorphic types, forgetting to add
@JsonTypeInfoand@JsonSubTypesannotations is a frequent cause of cast errors. Without these, Jackson cannot determine the correct subtype during deserialization and defaults to a generic Map.
Fix 7: Fix Spring Bean Casting Errors
Spring applications encounter ClassCastException due to proxy objects. Spring creates proxies for beans that use @Transactional, @Async, @Cacheable, or AOP aspects. These proxies may not be castable to the concrete class.
The error typically looks like:
java.lang.ClassCastException: class com.sun.proxy.$Proxy85 cannot be cast to class com.example.UserServiceImplWhy it happens: Spring creates JDK dynamic proxies by default, which implement the bean’s interfaces but do not extend its concrete class. When you inject and cast to the implementation class, it fails.
Fix 1: Cast to the interface, not the implementation:
// Wrong
@Autowired
private UserServiceImpl userService; // fails if proxied
// Right
@Autowired
private UserService userService; // interface type works with proxiesFix 2: Force CGLIB proxies if you must use the concrete type:
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Application { }Or in application.properties:
spring.aop.proxy-target-class=trueNote: Spring Boot 2.0+ uses CGLIB proxies by default, so this issue is more common in older Spring Framework applications or when using @EnableAsync or @EnableCaching which may override the proxy strategy.
Fix 3: Check your @Bean method return types:
// Wrong: returns concrete type but Spring proxies it
@Bean
public UserServiceImpl userService() {
return new UserServiceImpl();
}
// Right: return the interface type
@Bean
public UserService userService() {
return new UserServiceImpl();
}If you are encountering class loading issues with Spring in general, check whether UnsupportedClassVersionError applies — it signals a JDK version mismatch that can compound proxy-related problems.
Fix 8: Use Pattern Matching for instanceof (Java 16+)
Java 16 introduced pattern matching for instanceof, which eliminates the need for separate cast statements. This makes ClassCastException structurally impossible in code that uses it.
Old style (pre-Java 16):
if (obj instanceof String) {
String s = (String) obj; // redundant cast
System.out.println(s.length());
}New style (Java 16+):
if (obj instanceof String s) {
System.out.println(s.length()); // s is already cast and scoped
}The variable s is only in scope inside the if block, and it is guaranteed to be a String. No explicit cast, no risk of ClassCastException.
Use it in combination with logical operators:
if (obj instanceof String s && s.length() > 5) {
// s is safely cast AND has length > 5
}Pattern matching in switch (Java 21+):
Object obj = getData();
String result = switch (obj) {
case Integer i -> "Integer: " + i;
case String s -> "String: " + s;
case Double d -> "Double: " + d;
case null -> "null";
default -> "Unknown: " + obj.getClass().getName();
};This is the modern replacement for chains of instanceof checks followed by casts. The compiler ensures the patterns are exhaustive (with default), and each branch has a properly typed variable.
Migrate existing code gradually. Search your codebase for the old pattern:
grep -rn "instanceof.*\b\w\+\b)" --include="*.java" src/Then refactor each match to use the binding variable syntax. If you are still on Java 11 or 8, you cannot use this feature — focus on Fixes 1 through 7 instead.
For teams maintaining concurrent Java applications, combining safe casting with proper collection handling avoids both ClassCastException and ConcurrentModificationException during iteration.
Still Not Working?
If you have tried the fixes above and still see ClassCastException, check these less obvious causes:
Hot-reload tools: Spring DevTools, JRebel, and similar tools create new ClassLoaders on reload. Restart the application fully to rule this out.
Fat JAR conflicts: If you package a fat/uber JAR, duplicate classes from different dependencies can shadow each other. Run
mvn dependency:treeorgradle dependenciesand look for conflicting versions of the same library.Reflection and
Class.forName(): When you load classes via reflection, the ClassLoader context matters. UseThread.currentThread().getContextClassLoader().loadClass()instead ofClass.forName()in container environments.JNDI lookups: Objects retrieved via JNDI in application servers may come from a different ClassLoader. Cast to interfaces defined in a shared ClassLoader rather than implementation classes.
Kotlin/Scala interop: If you mix Kotlin or Scala with Java, be aware that Kotlin’s
asoperator and Scala’s.asInstanceOf[]throwClassCastExceptionthe same way. Useas?in Kotlin (safe cast) or pattern matching in Scala.Check the full stack trace: The line number in the stack trace tells you exactly which cast failed. If it points to compiler-generated code (lambda or generic bridge method), the real issue is the generic type mismatch at the point where the object entered the collection or callback.
Enable verbose class loading for ClassLoader debugging:
java -verbose:class -jar myapp.jar 2>&1 | grep "com.example.User"This prints every class load event, showing which ClassLoader loaded each class and from which JAR file.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Java ConcurrentModificationException
How to fix Java ConcurrentModificationException caused by modifying a collection while iterating, HashMap concurrent access, stream operations, and multi-threaded collection usage.
Fix: Java NoSuchMethodError
How to fix Java NoSuchMethodError caused by classpath conflicts, incompatible library versions, wrong dependency scope, shaded JARs, and compile vs runtime version mismatches.
Fix: Java java.lang.IllegalArgumentException
How to fix Java IllegalArgumentException caused by null arguments, invalid enum values, negative numbers, wrong format strings, and Spring/Hibernate validation failures.
Fix: Java java.lang.NullPointerException
How to fix Java NullPointerException by reading stack traces, adding null checks, using Optional, fixing uninitialized variables, avoiding null returns, handling auto-unboxing, and using static analysis annotations.