1. 简介
Jackson 是 Java 中广泛使用的库,用于方便地处理 JSON 或 XML 的序列化与反序列化操作。
在将 JSON 或 XML 数据反序列化为对象集合时,我们可能会遇到如下异常:
java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to X
本文将深入探讨该异常出现的原因,并提供几种有效的解决方案。
2. 问题分析
我们先通过一个简单的例子来复现这个异常,从而更好地理解它的触发条件。
2.1. 创建 POJO 类
我们定义一个简单的 Book
类:
public class Book {
private Integer bookId;
private String title;
private String author;
// 省略 getter、setter、构造函数、equals 和 hashCode
}
假设我们有一个 books.json
文件,内容如下,是一个包含三本书的 JSON 数组:
[ {
"bookId" : 1,
"title" : "A Song of Ice and Fire",
"author" : "George R. R. Martin"
}, {
"bookId" : 2,
"title" : "The Hitchhiker's Guide to the Galaxy",
"author" : "Douglas Adams"
}, {
"bookId" : 3,
"title" : "Hackers And Painters",
"author" : "Paul Graham"
} ]
接下来我们尝试将这个 JSON 反序列化为 List<Book>
,看看会发生什么。
2.2. 将 JSON 反序列化为 List<Book>
我们尝试将 JSON 字符串反序列化为 List<Book>
并读取元素,看看是否会抛出类型转换异常:
@Test
void givenJsonString_whenDeserializingToList_thenThrowingClassCastException()
throws JsonProcessingException {
String jsonString = readFile("/to-java-collection/books.json");
List<Book> bookList = objectMapper.readValue(jsonString, ArrayList.class);
assertThat(bookList).size().isEqualTo(3);
assertThatExceptionOfType(ClassCastException.class)
.isThrownBy(() -> bookList.get(0).getBookId())
.withMessageMatching(".*java.util.LinkedHashMap cannot be cast to .*com.baeldung.jackson.tocollection.Book.*");
}
测试通过,说明我们成功复现了问题。
2.3. 为什么会出现这个异常?
从异常信息 class java.util.LinkedHashMap cannot be cast to class … Book
可以看出几个问题:
- 我们明明声明的是
List<Book>
,为什么 Jackson 试图将LinkedHashMap
转换为Book
? LinkedHashMap
是从哪来的?
✅ 根本原因如下:
- 我们调用
objectMapper.readValue(jsonString, ArrayList.class)
时,只告诉 Jackson 返回一个ArrayList
,但没有告诉它里面元素的类型。 - 因此 Jackson 会默认将每个 JSON 对象反序列化为
LinkedHashMap
,最终得到的是ArrayList<LinkedHashMap>
。 - 当我们试图调用
.getBookId()
时,就会抛出类型转换异常。
下图展示了反序列化后的结构:
3. 使用 TypeReference
解决问题
为了解决这个问题,我们需要明确告诉 Jackson 集合中元素的类型。
✅ 推荐方式:使用 TypeReference
@Test
void givenJsonString_whenDeserializingWithTypeReference_thenGetExpectedList()
throws JsonProcessingException {
String jsonString = readFile("/to-java-collection/books.json");
List<Book> bookList = objectMapper.readValue(jsonString, new TypeReference<List<Book>>() {});
assertThat(bookList.get(0)).isInstanceOf(Book.class);
assertThat(bookList).isEqualTo(expectedBookList);
}
测试通过,说明 TypeReference
成功解决了类型擦除问题。
4. 使用 JavaType
解决问题
除了 TypeReference
,我们还可以使用 Jackson 提供的 JavaType
来解决。
✅ 使用 JavaType
构造目标类型:
@Test
void givenJsonString_whenDeserializingWithJavaType_thenGetExpectedList()
throws JsonProcessingException {
String jsonString = readFile("/to-java-collection/books.json");
CollectionType listType =
objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, Book.class);
List<Book> bookList = objectMapper.readValue(jsonString, listType);
assertThat(bookList.get(0)).isInstanceOf(Book.class);
assertThat(bookList).isEqualTo(expectedBookList);
}
同样可以解决问题。
5. 使用 JsonNode
+ convertValue()
方法
另一种方式是先将 JSON 解析为 JsonNode
,再通过 convertValue()
转换为目标类型。
✅ 使用 convertValue()
配合 TypeReference
或 JavaType
:
@Test
void givenJsonString_whenDeserializingWithConvertValueAndTypeReference_thenGetExpectedList()
throws JsonProcessingException {
String jsonString = readFile("/to-java-collection/books.json");
JsonNode jsonNode = objectMapper.readTree(jsonString);
List<Book> bookList = objectMapper.convertValue(jsonNode, new TypeReference<List<Book>>() {});
assertThat(bookList.get(0)).isInstanceOf(Book.class);
assertThat(bookList).isEqualTo(expectedBookList);
}
或者使用 JavaType
:
@Test
void givenJsonString_whenDeserializingWithConvertValueAndJavaType_thenGetExpectedList()
throws JsonProcessingException {
String jsonString = readFile("/to-java-collection/books.json");
JsonNode jsonNode = objectMapper.readTree(jsonString);
List<Book> bookList = objectMapper.convertValue(jsonNode,
objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, Book.class));
assertThat(bookList.get(0)).isInstanceOf(Book.class);
assertThat(bookList).isEqualTo(expectedBookList);
}
两种方式都 ✅ 通过测试。
6. 编写通用的反序列化方法
在实际开发中,我们可能需要一个通用的方法来处理不同类型的集合。
✅ 使用 JavaType
实现通用方法:
public static <T> List<T> jsonArrayToList(String json, Class<T> elementClass) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
CollectionType listType =
objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, elementClass);
return objectMapper.readValue(json, listType);
}
测试方法:
@Test
void givenJsonString_whenCalljsonArrayToList_thenGetExpectedList() throws IOException {
String jsonString = readFile("/to-java-collection/books.json");
List<Book> bookList = JsonToCollectionUtil.jsonArrayToList(jsonString, Book.class);
assertThat(bookList.get(0)).isInstanceOf(Book.class);
assertThat(bookList).isEqualTo(expectedBookList);
}
测试 ✅ 通过。
⚠️ 注意:不能使用 TypeReference
来实现通用方法!
public static <T> List<T> jsonArrayToList(String json, Class<T> elementClass) throws IOException {
return new ObjectMapper().readValue(json, new TypeReference<List<T>>() {});
}
虽然代码看起来没问题,但运行时会抛出同样的异常:
java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.baeldung...Book ...
原因在于泛型擦除,TypeReference<List<T>>
中的 T
在运行时是无法获取的。
7. XML 反序列化中的相同问题
Jackson 不仅支持 JSON,也支持 XML 的序列化与反序列化。
✅ XML 反序列化也会出现同样的问题:
<ArrayList>
<item>
<bookId>1</bookId>
<title>A Song of Ice and Fire</title>
<author>George R. R. Martin</author>
</item>
<item>
<bookId>2</bookId>
<title>The Hitchhiker's Guide to the Galaxy</title>
<author>Douglas Adams</author>
</item>
<item>
<bookId>3</bookId>
<title>Hackers And Painters</title>
<author>Paul Graham</author>
</item>
</ArrayList>
测试代码:
@Test
void givenXml_whenDeserializingToList_thenThrowingClassCastException()
throws JsonProcessingException {
String xml = readFile("/to-java-collection/books.xml");
List<Book> bookList = xmlMapper.readValue(xml, ArrayList.class);
assertThat(bookList).size().isEqualTo(3);
assertThatExceptionOfType(ClassCastException.class)
.isThrownBy(() -> bookList.get(0).getBookId())
.withMessageMatching(".*java.util.LinkedHashMap cannot be cast to .*com.baeldung.jackson.tocollection.Book.*");
}
测试 ✅ 通过,说明问题同样存在。
✅ 解决方式与 JSON 一致:
@Test
void givenXml_whenDeserializingWithTypeReference_thenGetExpectedList()
throws JsonProcessingException {
String xml = readFile("/to-java-collection/books.xml");
List<Book> bookList = xmlMapper.readValue(xml, new TypeReference<List<Book>>() {});
assertThat(bookList.get(0)).isInstanceOf(Book.class);
assertThat(bookList).isEqualTo(expectedBookList);
}
因为 XmlMapper
是 ObjectMapper
的子类,所以所有 JSON 的解决方案都适用于 XML。
8. 总结
本文详细解释了 Jackson 在反序列化 JSON 或 XML 为集合时,为什么会抛出 LinkedHashMap cannot be cast to X
的异常,并提供了以下几种解决方案:
✅ 推荐方式:
- 使用
TypeReference
明确指定泛型类型 - 使用
JavaType
构造目标类型 - 使用
JsonNode
+convertValue()
进行类型转换
⚠️ 踩坑提醒:
- 泛型方法中不能使用
TypeReference<List<T>>
,因为泛型擦除 - XML 和 JSON 的反序列化机制一致,解决方案通用
所有代码示例均可在 GitHub 获取。