简体   繁体   中英

XStream deserialization of generic class returns java.lang.Object

I'm struggling to convert some XML into objects of generic classes. My code works well with non-generic classes, but doesn't with generic ones.

Let's look at this example, it makes it easier to explain. I use both generic and non-generic classes to compare their behaviour.

Xml file:

<?xml version="1.0" encoding="UTF-8"?>
<Box>
    <doubleFieldA>
        <myGenericField>20.3</myGenericField>
    </doubleFieldA>
    <doubleFieldB>
        <myNonGenericField>20.3</myNonGenericField>
    </doubleFieldB>
</Box>

Xsd schema:

<xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="Box">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="doubleFieldA">
          <xs:complexType>
            <xs:sequence>
              <xs:element type="xs:double" name="myGenericField"/>
            </xs:sequence>
          </xs:complexType>
        </xs:element>
        <xs:element name="doubleFieldB">
          <xs:complexType>
            <xs:sequence>
              <xs:element type="xs:double" name="myNonGenericField"/>
            </xs:sequence>
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    </xs:complexType>
</xs:element>

GenericClass.java:

public class GenericClass<T> {
    public T myGenericField;
}

NonGenericClass.java:

public class NonGenericClass {
    public double myNonGenericField;
}

Box.java:

import com.thoughtworks.xstream.XStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Box {
    public GenericClass<Double> doubleFieldA;
    public NonGenericClass doubleFieldB;

    private void deserialize() {
        Box b;
        String s;
        XStream xs = new XStream();
        try{
            s = new String(Files.readAllBytes(Paths.get("src/my_xml.xml")));
            b = (Box)xs.fromXML(s);
            doubleFieldA = b.doubleFieldA;
            doubleFieldB = b.doubleFieldB;
        } catch (IOException ioe) {
            System.err.println("IOException: " + ioe.getMessage());
        }
    }

    public Box() {
        deserialize();
    }
}

Main.java:

public class Main {

    public static void main(String[] args) {
        Box box = new Box();
        System.out.println("myGenericField = " + box.doubleFieldA.myGenericField);
        System.out.println("myNonGenericField = " + box.doubleFieldB.myNonGenericField);
    }
}

Output:

myGenericField = java.lang.Object@2d928643

myNonGenericField = 20.3

Expected output (what I want!):

myGenericField = 20.3

myNonGenericField = 20.3

I have a Box class with two attributes: one is a generic class, the other just a normal class.
Both have an attribute, whose type is double in this particular example.
I try to initialize them with the values stored in the XML file (20.3 for both), using XStream.
Eventually, when I print these attributes, I get the correct value for the non-generic class. The generic class attribute, on the contrary, results in a java.lang.Object.
How should I modify my code to obtain the expected behaviour?

I suspect the issue may be in the way XStream handles generic types, but I'm not sure. I don't have much experience with XStream and XML technologies in general, so I'd really appreciate an answer with working code/examples rather than "you need to implement XYZ, find out how in this messy tutorial!".

Thank you very much!

Note: the only purpose of this code is to show my issue. I encountered this problem in a way larger project, so this is just a small example to reproduce it.

There is no good way to do it with XStream, it just doesn't have any idea that generics even exist. You can add custom converter:

public static class BoxConverter implements Converter {

    public boolean canConvert(Class clazz) {
            return clazz.equals(Box.class);
    }

    public void marshal(Object value, HierarchicalStreamWriter writer,
                    MarshallingContext context) {
            throw new RuntimeException("to do");
    }

    public Object unmarshal(HierarchicalStreamReader reader,
                    UnmarshallingContext context) {
            Box box = new Box();
             while (reader.hasMoreChildren()) {
                    reader.moveDown();
                    if ("doubleFieldA".equals(reader.getNodeName())) {
                            reader.moveDown();
                            Double val = Double.valueOf(reader.getValue());
                            reader.moveUp();
                            GenericClass<Double> genericObject = new GenericClass<>();
                            genericObject.myGenericField = val;
                            box.doubleFieldA = genericObject;
                    } else if ("doubleFieldB".equals(reader.getNodeName())) {
                            box.doubleFieldB =(NonGenericClass)context.convertAnother(box, NonGenericClass.class);
                    }
                    reader.moveUp();
            }
            return box;
    }
}

And the register it:

XStream xs = new XStream();
xs.registerConverter(new BoxConverter());
Box b = (Box) xs.fromXML(input);

But that requires to write a separate converter for each class that has GenericClass as a field/member. Notice that when you marshal such object with XStream it generates:

<Box>
  <doubleFieldA>
    <myGenericField class="double">20.3</myGenericField>
  </doubleFieldA>
  <doubleFieldB>
    <myNonGenericField>20.3</myNonGenericField>
  </doubleFieldB>
</Box>

and that additional class="double" is something that XStream is unable to infer on its own.

The Problem here is TypeErasure , especially

Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.

So

public T myGenericField;

becomes

public Object myGenericField; 

at runtime/in the bytecode.

So there's no way for XStream to determine myGenericField should be a double... and so it get's deserialized into an Object instance.

java.lang.Object@2d928643 is just the output of myGenericField.toString()

So, only way I see is to create a custom converter .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM