1. 概述
Jackson 是一個廣泛使用的 Java 庫,它允許我們方便地序列化/反序列化 JSON 或 XML。
有時,當我們嘗試將 JSON 或 XML 反序列化為對象集合時,可能會遇到 java.lang.ClassCastException: java.util.LinkedHashMap 無法轉換為 X。
在本教程中,我們將討論此異常可能發生的原因以及如何解決該問題。
2. 瞭解問題
讓我們創建一個簡單的 Java 應用程序來重現這個異常,以便了解異常何時會發生。
2.1. 創建 POJO 類
首先,讓我們創建一個簡單的 POJO 類:
public class Book {
private Integer bookId;
private String title;
private String author;
//getters, setters, constructors, equals and hashcode omitted
}
假設我們有 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> 時會發生什麼:
@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.*");
}
我們使用了 AssertJ 庫來驗證當我們在調用 objectMapper.readValue() 方法時,是否會拋出預期異常,並且其消息與我們在問題陳述中註明的消息相匹配。
測試通過,這意味着我們已經成功地重現了該問題。
2.3. 異常拋出的原因
如果我們仔細研究異常消息 class java.util.LinkedHashMap cannot be cast to class … Book,可能會出現一些問題。
我們聲明瞭變量 bookList 為類型 List<Book>,但為什麼 Jackson 試圖將 LinkedHashMap 類型轉換為我們的 Book 類? 此外,LinkedHashMap 來自何處?
首先,我們確實將 bookList 聲明為類型 List<Book>。 但是,當我們調用 objectMapper.readValue() 方法時,我們傳遞了 ArrayList.class 作為 Class 對象。 因此,Jackson 將 JSON 內容反序列化為 ArrayList 對象,但它不知道 ArrayList 對象中應該包含什麼類型的內容。
其次,當 Jackson 嘗試反序列化 JSON 中的對象,但沒有提供任何目標類型信息時,它將使用默認類型:LinkedHashMap。 換句話説,在反序列化之後,我們將會得到一個 ArrayList<LinkedHashMap> 對象。 在 Map 中,鍵是屬性名稱,例如 “bookId”、“title” 並且如此.
值是對應屬性的值:
現在我們已經理解了問題的原因,讓我們討論如何解決它。
3. 將 TypeReference 傳遞給 objectMapper.readValue()
為了解決這個問題,我們需要 somehow 讓 Jackson 知道元素的類型。但是,編譯器不允許我們做類似 objectMapper.readValue(jsonString, ArrayList<Book>.class)的事情。
相反,我們可以將 TypeReference對象傳遞給 objectMapper.readValue(String content, TypeReference valueTypeRef)方法。
在這種情況下,我們只需要將 new TypeReference<List<Book>>() {}作為第二個參數傳遞即可:
@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 傳遞給 objectMapper.readValue()
在上一節中,我們討論了將 Class 對象或 TypeReference 對象作為第二個參數調用 objectMapper.readValue() 方法。
objectMapper.readValue() 方法仍然接受 JavaType 對象作為第二個參數。 JavaType 是類型令牌類及其泛型的父類。它將被反序列化器使用,以便反序列化器在反序列化過程中知道目標類型。
我們可以通過 TypeFactory 實例構建 JavaType 對象,並且我們可以從 objectMapper.getTypeFactory() 中檢索 TypeFactory 對象。
讓我們回到我們的示例書本。 在這個示例中,我們想要的目標類型是 ArrayList<Book>。
因此,我們可以使用這個要求構建 CollectionType:
objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, Book.class);
現在,讓我們編寫一個單元測試,以查看將 JavaType 傳遞給 readValue() 方法是否可以解決我們的問題:
@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 對象和 objectMapper.convertValue() 方法
我們已經看到了將 TypeReference 或 JavaType 對象傳遞到 objectMapper.readValue() 方法的解決方案。
或者,我們可以使用 Jackson 中的樹模型節點,然後通過調用 objectMapper.convertValue() 方法將 JsonNode 對象轉換為所需類型。
同樣,我們可以將 TypeReference 或 JavaType 對象傳遞到 objectMapper.convertValue() 方法。
讓我們看看每個方法在行動中的效果。
首先,讓我們使用 TypeReference 對象和 objectMapper.convertValue() 方法創建一個測試方法:
@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 對象傳遞到 objectMapper.convertValue() 方法時會發生什麼:
@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);
}
如果運行這兩個測試,它們都會通過。因此,使用 objectMapper.convertValue() 方法是解決該問題的一種替代方法。
6. 創建通用反序列化方法
到目前為止,我們已經解決了在將 JSON 數組反序列化為 Java 集合時類類型轉換的問題。在實際應用中,我們可能需要創建一個通用的方法來處理不同元素類型。
這並不難。
我們可以通過調用 objectMapper.readValue() 方法時傳遞一個 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 方法來構建通用方法,因為它看起來更緊湊?
讓我們創建一個通用的實用方法,並將相應的 TypeReference 對象傳遞給 objectMapper.readValue() 方法:
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 對象到 readValue() 方法,並且我們之前看到這種方式可以解決類類型轉換問題。所以,為什麼我們仍然看到相同的異常在這個案例中?
這是因為我們的方法是通用的。 類型參數 T 無法在運行時重體現,即使我們傳遞了一個帶有類型參數 T 的 TypeReference 實例。
7. XML 反序列化與 Jackson
除了 JSON 序列化/反序列化,Jackson 庫也可以用於序列化/反序列化 XML。
讓我們做一個快速示例來檢查在反序列化 XML 到 Java 集合時是否會發生同樣的問題。
首先,讓我們創建一個 XML 文件 books.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>
接下來,就像我們為 JSON 文件所做的那樣,我們創建一個單元測試方法來驗證當反序列化 XML 到 Java 集合時是否會拋出 ClassCastException:
@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.*");
}
我們的測試將通過,如果給予它一個運行。也就是説,同樣的問題也發生在 XML 反序列化中。
但是,如果我們知道如何解決 JSON 反序列化,那麼解決 XML 反序列化中的問題也很簡單。
由於 XmlMapper 是 ObjectMapper 的子類,我們之前解決 JSON 反序列化所提出的所有解決方案也適用於 XML 反序列化。
例如,我們可以將 TypeReference 對象傳遞給 xmlMapper.readValue() 方法來解決問題:
@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);
}
8. 結論
在本文中,我們討論了在使用 Jackson 進行 JSON 或 XML 反序列化時,可能出現的 java.util.LinkedHashMap 無法轉換為 X 異常的原因。
隨後,我們通過示例介紹瞭解決該問題的方法。