2013-01-24 7 views
8

Piszę klasę, która połączy się z serwerem i na podstawie pewnych argumentów, pobierze ciąg json, który zostanie sparsowany przez GSON do określonej (przez generyczne) klasy .Dziwne zachowanie podczas deserializacji klas zagnieżdżonych, ogólnych za pomocą GSON

uproszczoną wersję klasy odpowiedzialnej wygląda następująco:

class Executor<T> { 

    private Response<T> response; 

    public void execute() { 
     Type responseType = new TypeToken<Response<T>>() {}.getType(); 
     this.response = new Gson().fromJson(json, responseType); 
    } 

    public Response<T> getResponse() { return this.response; } 

} 

(JSON -variable looks like this.)

Klasa, która przechowuje dane raz de-serializowane wygląda następująco:

class Response<T> { 

    private List<T> data = null; 

    public List<T> getData() { return this.data; } 

} 

Klasa której dane stara się być de-szeregowane do:

public class Language { 
    public String alias; 
    public String label; 
} 

I kod, który działa wykorzystuje klas powyżej:

Executor<Language> executor = new Executor<Language(); 
List<Language> languages = executor.execute().getResponse().getData(); 
System.out.println(languages.get(0).alias); // exception occurs here 

co skutkuje następującym wyjątkiem

ClassCastException: com.google.gson.internal.StringMap nie może być oddane do sunnerberg.skolbibliotek.book.Language

Każda pomoc lub sugestie są mile widziane!

+0

Czy na pewno występuje wyjątek w tym wierszu? Nie widzę, gdzie wywoływane jest wywołanie execute() w przykładowym kodzie. –

+0

@SamuelEUSTACHI Och, mój zły. Pytanie edytowane, wymknęło się, gdy wkleiłem kod (jest nieco uproszczony). Tak, jestem w 100% pewien, że występuje dokładnie w tej linii (co najmniej zgodnie ze stosem stacków). – Zar

+0

@Zar executor = nowy executor Visruth

Odpowiedz

19

Krótka odpowiedź jest taka, że ​​trzeba przenieść utworzenie TypeToken z Executor, wiążą T w Response<T> podczas tworzenia tokena (new TypeToken<Response<Language>>() {}) i przechodzą w typie tokena do konstruktora Executor.

Długa odpowiedź brzmi:

Generics od typu są zazwyczaj usuwane w czasie wykonywania, chyba że typ jest skompilowany z parametru rodzajowego związana. W takim przypadku kompilator wstawia ogólną informację o typie do skompilowanej klasy. W innych przypadkach nie jest to możliwe.

Tak na przykład, należy rozważyć:

List<Integer> foo = new ArrayList<Integer>(); 

class IntegerList extends ArrayList<Integer> { ... } 
List<Integer> bar = new IntegerList(); 

W czasie wykonywania Java wiebar zawiera liczby całkowite, ponieważ typ Integer jest zobowiązany do ArrayList w czasie kompilacji, więc ogólne informacje typu jest zapisywany w klasie IntegerList plik. Jednak ogólna informacja o typie dla foo jest wymazana, więc w czasie rzeczywistym nie jest możliwe stwierdzenie, że foo ma zawierać Integer s.

Często pojawia się potrzeba generycznych informacji o typie w sytuacji, w której normalnie zostałaby wymazana przed uruchomieniem, na przykład tutaj w przypadku analizy danych JSON w GSON.W takich sytuacjach możemy wykorzystać fakt, że informacje o typie są zachowywane, gdy są powiązane w czasie kompilacji (jak w powyższym przykładzie IntegerList) za pomocą type tokens, które są tak naprawdę małymi anonimowymi klasami, które wygodnie przechowują ogólne informacje o typie.

Teraz do kodu:

Type responseType = new TypeToken<Response<T>>() {}.getType(); 

W tej linii swojej klasie Executor tworzymy klasę anonimowy (dziedziczenie z TypeToken), która ma typ Response<T> zakodowanego (związany) w czasie kompilacji. Tak więc w czasie wykonywania GSON może ustalić, czy chcesz obiekt o numerze Response<T>. Ale nie wie, co to jest T, ponieważ nie określiłeś go podczas kompilacji! Zatem GSON nie może określić, jaki typ będzie w obiekcie List tworzonego obiektu, a zamiast tego tworzy po prostu StringMap.

Morał z tej historii polega na tym, że podczas kompilacji trzeba określić, że T. Jeśli zamierzamy używać generycznie Executor, prawdopodobnie trzeba utworzyć token typu poza tą klasą w kodzie klienta. Coś takiego:

class Executor<T> { 

    private TypeToken<Response<T>> responseType; 
    private Response<T> response; 

    public Executor(TypeToken<Response<T>> responseType) { 
     this.responseType = responseType; 
    } 

    public void execute() { 
     this.response = new Gson().fromJson(json, responseType.getType()); 
    } 

    public Response<T> getResponse() { return this.response; } 

} 

// client code: 
Executor<Language> executor = new Executor<Language>(new TypeToken<Response<Language>>() { }); 
executor.execute(); 
List<Language> languages = executor.getResponse().getData(); 
System.out.println(languages.get(0).alias); // prints "be" 

Przy okazji, testowałem powyższe na mojej maszynie.

Przepraszam, jeśli to było za długie!

+3

Po pierwsze, muszę powiedzieć, że użytkownicy StackOverflow nigdy nie przestają mnie zadziwiać. Wow, jaka odpowiedź. Dokładnie tego, czego szukałem (i potrzebowałem!), Wielkie dzięki dla ciebie! Twoje rozwiązanie doskonale rozwiązało mój problem. Nawiasem mówiąc, czy istnieje jakiś powód, dla którego JVM wymazuje informacje o typie? – Zar

+0

Dziękuję za udzielenie szczegółowego i kompletnego pytania! Jeśli chodzi o usuwanie typu, uważam, że Java robi to w ten sposób, aby być kompatybilnym wstecz. Skompilowany kod, który używa generycznych, wygląda trochę jak skompilowany kod, który nie używa generycznych. Na przykład, jeśli masz jakiś stary kod, który używa * SomeLibrary 1.0 *, możesz przełączyć się do * SomeLibrary 2.0 (teraz z genericami) * bez ponownej kompilacji kodu. Sprawdź to [post na blogu] (http://blog.adaptivesoftware.biz/2009/02/what-is-type-erasure-in-java.html), aby uzyskać inne wyjaśnienie. – matts

+0

Myślę, że to ma sens, dzięki jeszcze raz! Interesujący link, przy okazji. – Zar

0

Twój problem jest powiązany z usuwaniem typu Java. Generics są znane tylko podczas kompilacji.

Niestety, nie ma sposobu, aby GSON wiedział, do której klasy powinien deserializować, używając refleksji.

+0

Hmm, okay. Czy istnieje sposób na pokonanie tego? Domyślam się, że jestem otwarty na alternatywną strukturę klas, dopóki nie muszę tworzyć nowej klasy dla każdej pary Executorów. – Zar

2

Użytkownik nie nazwał response.execute(); po Executor<Language> executor = new Executor<Language>(); tym oświadczeniu. Nie można tu używać generycznych java, ale można uzyskać ten sam efekt za pomocą następującego kodu.

Response.java

import java.io.Serializable; 
import java.util.List; 

/** 
* 
* @author visruth 
*/ 
public class Response<T> implements Serializable { 

    private List<T> data = null; 

    public List<T> getData() { 
     return this.data; 
    } 

    public void setData(List<T> data) { 
     this.data = data; 
    } 

} 

Language.java

import java.io.Serializable; 

/** 
* 
* @author visruth 
*/ 
public class Language implements Serializable { 
    private String alias; 
    private String label; 

    public String getAlias() { 
     return alias; 
    } 

    public void setAlias(String alias) { 
     this.alias = alias; 
    } 

    public String getLabel() { 
     return label; 
    } 

    public void setLabel(String label) { 
     this.label = label; 
    } 

} 

Wreszcie Executor.java

import com.google.gson.Gson; 
import java.util.*; 

/** 
* 
* @author visruth 
*/ 
public class Executor<T> { 
private Response<T> response; 

    public Response<T> getResponse() { 
     return response; 
    } 

    /** 
    * @param args the command line arguments 
    */ 
    public void executor() { 
     //sample data 
     Response<Language> response=new Response<Language>(); 
     Language lan1=new Language(); 
     lan1.setAlias("alias1"); 
     lan1.setLabel("label1"); 
     Language lan2=new Language(); 
     lan2.setAlias("alias2"); 
     lan2.setLabel("label2"); 
     List<Language> listOfLangauges=new ArrayList<Language>(); 
     listOfLangauges.add(lan1); 
     listOfLangauges.add(lan2); 
     response.setData(listOfLangauges); 
     Gson gson=new Gson(); 
     String json = gson.toJson(response); 

     System.out.println(json); 
     Response<Language> jsonResponse = gson.fromJson(json, Response.class); 
     List list=jsonResponse.getData(); 

     List<Language> langs=new ArrayList<Language>();    
     for(int i=0; i<list.size(); i++) {    
     Language lan=gson.fromJson(list.get(i).toString(), Language.class); 
     langs.add(lan); 
     //System.out.println(lan.getAlias()); 
     } 
     Response<Language> responseMade=new Response<Language>(); 
     responseMade.setData(langs); 
     this.response=(Response<T>) responseMade; 

    } 
} 

Można przetestować go następująco

Executor<Language> executor = new Executor<Language>(); 
executor.executor(); 
List<Language> data = executor.getResponse().getData(); 
for(Language langu: data) { 
    System.out.println(langu.getAlias()); 
    System.out.println(langu.getLabel()); 
} 
+0

Wielkie dzięki za odpowiedź, dałem mi (mam nadzieję) dobre pomysły! – Zar