Jackson: java.util.LinkedHashMap cannot be cast to X

本文翻譯自:https://www.baeldung.com/jackson-linkedhashmap-cannot-be-cast

1.概述:

Jackson是一個廣泛使用的 Java 庫,它允許我們方便地序列化/反序列化 JSON 或 XML。

有時,當我們嘗試將 JSON 或 XML 反序列化爲對象集合時,可能會遇到“ java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to 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>時會發生什麼:

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.*");
}

我們使用AssertJ庫來驗證當我們調用bookList.get(0).getBookId()時是否引發了預期的異常,並且它的消息與我們的問題陳述中記錄的消息相匹配

測試通過,這意味着我們已經成功地重現了問題。 

2.3. 爲什麼拋出異常

現在,如果我們仔細查看異常消息:“ class java.util.LinkedHashMap cannot be cast to class ... Book ”,可能會出現幾個問題。

我們已經用List<Book>類型 聲明瞭變量bookList,但是爲什麼 Jackson 嘗試將LinkedHashMap類型轉換爲我們的Book類?此外,LinkedHashMap是從哪裏來的?

首先,確實我們用List<Book>類型聲明瞭 bookList 。但是,當我們調用objectMapper.readValue()方法時,我們將ArrayList.class作爲Class對象傳遞 。因此,Jackson 會將 JSON 內容反序列化爲ArrayList對象,但它不知道ArrayList對象中應該包含什麼類型的元素。

其次,當 Jackson 嘗試反序列化 JSON 中的對象,但沒有給出目標類型信息時,它將使用默認類型:LinkedHashMap。換句話說,在反序列化之後,我們會得到一個ArrayList<LinkedHashMap>對象。在 Map中,鍵是屬性的名稱——例如,“ bookId ”、“ title ”等。這些值是相應屬性的值:

 

 

 

 現在我們瞭解了問題的原因,讓我們討論如何解決它。

3.將TypeReference傳遞給objectMapper.readValue()

爲了解決這個問題,我們需要讓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實例。

 

本篇文章如有幫助到您,請給「翎野君」點個贊,感謝您的支持。

首發鏈接:https://www.cnblogs.com/lingyejun/p/16379972.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章