How to recursively serialize an object using reflection?
I want to navigate to the N-th level of an object, and serialize it's properties in String format. For Example:
class Animal {
public String name;
public int weight;
public Animal friend;
public Set<Animal> children = new HashSet<Animal>() ;
}
should be serialized like this:
{name:"Monkey",
weight:200,
friend:{name:"Monkey Friend",weight:300 ,children:{...if has children}},
children:{name:"MonkeyChild1",weight:100,children:{... recursively nested}}
}
And you may probably notice that it is similar to serializing an object to json. I know there're many libs(Gson,Jackson...) can do this, can y开发者_StackOverflow社区ou give me some instructive ideas on how to write this by myself?
Google Gson can do this particular task in a single line:
String json = new Gson().toJson(animal);
The other way round is by the way also as easy:
Animal animal = new Gson().fromJson(json, Animal.class);
I haven't seen another JSON serializer yet with better support for generics, collections/maps and (nested) javabeans.
Update: To the point, you just need to learn about reflection API. I recommend to get yourself through the Sun tutorial on the subject first. In a nutshell, you can use Object#getClass()
and all the methods provided by java.lang.Class
, java.lang.reflect.Method
, etc to determine the one and other. Google Gson is open source, take your benefit of it as well.
Serialization is basically deep cloning.
You need to track each object reference for reoccurrencies (for example by using IdentityHashMap). What ever is your final implementation method (if not external library) remember to check for object reoccurrencies or you may end up in an infinent loop (when object A has reference to object B that again has reference to object A or something more complex loop in an object graph).
One way is to traverse through object graph with DFS-like algorithm and build the clone (serialized string) from there.
This pseudo-code hopefully explains how:
visited = {}
function visit(node) {
if node in visited {
doStuffOnReoccurence(node)
return
}
visited.add(node)
doStuffBeforeOthers(node)
for each otherNode in node.expand() visit(otherNode)
doStuffAfterOthers(node)
}
The visited set in the example is where I would use identity set (if there was one) or IdentityHashMap.
When finding out fields reflectively (thats the node.expand() part) remember to go through superclass fields also.
Reflection should not be used in a "normal" development case. Reflection handles code as a data and you can ignore all normal object access restrictions. I've used this reflective deep copy stuff only for tests:
In a test that checked different kinds of objects for deep object graph equality
In a test that analyzed object graph size and other properties
A clean way to approach this is to use a visitor pattern to keep your encoding implementation separate from your business objects. Some people will argue that you can simply implement the Externalizable
interface along with readExternal
/ writeExternal
but this has the problems that:
- Your protocol is embededded within your business object, meaning it is distributed across your codebase rather than being in one place.
- Your application cannot support multiple protocols (as offered by Google Protocol Buffers).
Example
/**
* Our visitor definition. Includes a visit method for each
* object it is capable of encoding.
*/
public interface Encoder {
void visitAnimal(Animal a);
void visitVegetable(Vegetable v);
void visitMineral(Mineral m);
}
/**
* Interface to be implemented by each class that can be encoded.
*/
public interface Encodable {
void applyEncoder(Encoder e);
}
public class Animal implements Encodable {
public void applyEncoder(Encoder e) {
// Make call back to encoder to encode this particular Animal.
// Different encoder implementations can be passed to an Animal
// *without* it caring.
e.visitAnimal(this);
}
}
Typically one would then define a stateful Encoder
implementation that would "push" each object to an OutputStream
when its visitXXX
method is called; e.g.
public class EncoderImpl implements Encoder {
private final DataOutputStream daos;
public EncoderImpl(File file) throws IOException {
this.daos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
}
public void visitAnimal(Animal a) {
daos.writeInt(a.getWeight());
daos.writeUTF(a.getName());
// Write the number of children followed by an encoding of each child animal.
// This allows for easy decoding.
daos.writeInt(a.getChildren().size());
for (Animal child : a.getChildren()) {
visitAnimal(child);
}
}
// TODO: Implement other visitXXX methods.
/**
* Called after visiting each object that requires serializing.
*/
public void done() {
daos.close();
}
}
精彩评论