I am developing a Java web application for a school project and I am following the MVC architecture pattern as best as I can; to accomplish that, I created a set of JavaBean classes, and to serialize and deserialize instances of these I am using Gson. The problem I'm having shows that I haven't fully understood how inheritance and generics work in Java, so I hope to shed light on those arguments with this question.
I have two abstract classes Item
and ItemVariant
, which are extended by two bean classes each, respectively CatalogItem
, ArchivedItem
and CatalogItemVariant
, ArchivedItemVariant
.
An istance of Item
can refer to a collection of ItemVariant
, and vice versa, an istance of ItemVariant
can refer to its parent item (I know this can cause loops in the serialization process but this isn't the problem I am experiencing). So ideally, an instance of CatalogItem
should always refer to a collection CatalogItemVariant
and an instance of CatalogItemVariant
should always refer to one of CatalogItem
, and the same applies for ArchivedItem
and ArchivedItemVariant
. However, I am not interested into forcing this relationship; for example, letting an instance of CatalogItemVariant
refer to an instance of ArchivedItem
is allowed.
Currently the code for these two abstract classes looks like this:
public abstract class Item implements Serializable {
...
protected Collection<ItemVariant> variants;
...
public <T extends ItemVariant> Collection<T> getVariants() {
return (Collection<T>) variants;
}
...
public <T extends ItemVariant> void setVariants(Collection<T> variants) {
this.variants = (Collection<ItemVariant>) variants;
}
}
public abstract class ItemVariant implements Serializable {
...
protected Item parentItem;
...
public <T extends Item> T getParentItem() {
return (T) parentItem;
}
...
public <T extends Item> void setParentItem(T parentItem) {
this.parentItem = parentItem;
}
...
}
This is producing unchecked cast warnings and I am aware that it probably isn't the most elegant solution; however, this isn't the major issue that I'm experiencing.
The bean classes that extend these two simply add a couple properties each along with their getters and setters, and instances of those concrete classes are the ones actually used throughout the application. Now comes the real problem: consider, as an example, the following JSON string that must be deserialized into an instance of CatalogItemVariant
.
{"parentItem":{"name":"Sample name","category":"Sample category","color":"Sample color"},"size":"Sample size"}
Ideally, parentItem
should be deserialized into an istance of CatalogItem
, but given the way those classes are currently designed, Gson tries to create an instance of Item
, which is abstract and thus causes the following exception:
java.lang.RuntimeException: Failed to invoke public model.transfer.catalog.Item() with no args
To work around this, I thought of making the two methods setVariants
in Item
and setParentItem
in ItemVariant
abstract, thus forcing their subclasses two override them. Something like this:
public abstract class ItemVariant implements Serializable {
...
protected Item parentItem;
...
public abstract <T extends Item> void setParentItem(T parentItem);
...
}
public class CatalogItemVariant extends ItemVariant {
...
@Override
public void setParentItem(CatalogItem parentItem) {
this.parentItem = parentItem;
}
...
}
But this isnt working because the type CatalogItem
doesn't match T
, making the @Override
annotation invalid.
A workaround to this would be sending to the servlet that deserializes the object two separate JSON strings, one for the item variant and one for its parent item, deserialize both of them into two different objects using the correct classes (CatalogItem
for the first one and CatalogItemVariant
for the second), and then manually set the attribute parentItem
of the new CatalogItemVariant
instance using setParentItem()
. Of course, when applying this solution, the JSON string for the item variant must miss its parentItem
field. Is there a better solution to this? If so, how should I redesign the classes and methods that are affected by this problem?
EDIT: since I've been asked to provide the actual code, here's a simplified version of the classes I'm using:
public abstract class Item implements Serializable {
private static final long serialVersionUID = -6170402744115745097L;
protected String name;
protected String category;
protected String color;
protected Collection<ItemVariant> variants;
public String getName() {
return this.name;
}
public String getCategory() {
return this.category;
}
public String getColor() {
return this.color;
}
public <T extends ItemVariant> Collection<T> getVariants() {
return (Collection<T>) variants;
}
public void setName(String name) {
this.name = name;
}
public void setCategory(String category) {
this.category = category;
}
public void setColor(String color) {
this.color = color;
}
public <T extends ItemVariant> void setVariants(Collection<T> variants) {
this.variants = (Collection<ItemVariant>) variants;
}
}
public abstract class ItemVariant implements Serializable {
private static final long serialVersionUID = 4245549003952140725L;
protected Item parentItem;
protected String size;
public <T extends Item> T getParentItem() {
return (T) parentItem;
}
public String getSize() {
return size;
}
public <T extends Item> void setParentItem(T parentItem) {
this.parentItem = parentItem;
}
public void setSize(String size) {
this.size = size;
}
}
public class CatalogItem extends Item implements Serializable {
private static final long serialVersionUID = 993286101083002293L;
protected String description;
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
public class CatalogItemVariant extends ItemVariant implements Serializable {
private static final long serialVersionUID = -2266390484210707778L;
protected Integer availability;
public Integer getAvailability() {
return availability;
}
public void setAvailability(Integer availability) {
this.availability = availability;
}
}
The error I'm experienced can be simulated by running the following snippet of code, and it's thrown in the last line:
Gson gson = new GsonBuilder().create();
CatalogItem catalogItem;
CatalogItemVariant catalogItemVariant;
CatalogItemVariant deserializedCatalogItemVariant;
catalogItem = new CatalogItem();
catalogItem.setName("Sample name");
catalogItem.setCategory("Sample category");
catalogItem.setColor("Sample color");
catalogItemVariant = new CatalogItemVariant();
catalogItemVariant.setSize("Sample size");
catalogItemVariant.setParentItem(catalogItem);
String serializedCatalogItemVariant = gson.toJson(catalogItemVariant); // Builds a JSON string of the object to serialize
System.out.println(serializedCatalogItemVariant); // Prints the JSON string of the serialized object which is fed for deserialization in the next instruction
deserializedCatalogItemVariant = gson.fromJson(serializedCatalogItemVariant, CatalogItemVariant.class);
For clarification, I fully understand that the error is caused because of the way these classes are designed and I don't expect the program to behave differently; I just want to understand if there's a way I can use generics and inheritance to redesign those classes, and if so, what that way is.
For anyone who runs into this question, have a look at this very simple solution!