1. Overview
Kryo is a Java serialization framework with a focus on speed, efficiency, and a user-friendly API.
In this article, we’ll explore the key features of the Kryo framework and implement examples to showcase its capabilities.
2. Maven Dependency
The first thing we need to do is to add the kryo dependency to our pom.xml:
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.1</version>
</dependency>
The latest version of this artifact can be found on Maven Central.
3. Kryo Basics
Let’s start by looking at how Kryo works and how we can serialize and deserialize objects with it.
3.1. Introduction
The framework provides the Kryo class as the main entry point for all its functionality.
This class orchestrates the serialization process and maps classes to Serializer instances which handle the details of converting an object’s graph to a byte representation.
Once the bytes are ready, they’re written to a stream using an Output object. This way they can be stored in a file, a database or transmitted over the network.
Later, when the object is needed, an Input instance is used to read those bytes and decode them into Java objects.
3.2. Serializing Objects
Before diving into examples, let’s first create a utility method to initialize some variables we’ll use for each test case in this article:
@Before
public void init() {
kryo = new Kryo();
output = new Output(new FileOutputStream("file.dat"));
input = new Input(new FileInputStream("file.dat"));
}
Now, we can look a how easy is to write and read an object using Kryo:
@Test
public void givenObject_whenSerializing_thenReadCorrectly() {
Object someObject = "Some string";
kryo.writeClassAndObject(output, someObject);
output.close();
Object theObject = kryo.readClassAndObject(input);
input.close();
assertEquals(theObject, "Some string");
}
Notice the call to the close() method. This is needed since Output and Input classes inherit from OutputStream and InputStream respectively.
Serializing multiple objects is similarly straightforward:
@Test
public void givenObjects_whenSerializing_thenReadCorrectly() {
String someString = "Multiple Objects";
Date someDate = new Date(915170400000L);
kryo.writeObject(output, someString);
kryo.writeObject(output, someDate);
output.close();
String readString = kryo.readObject(input, String.class);
Date readDate = kryo.readObject(input, Date.class);
input.close();
assertEquals(readString, "Multiple Objects");
assertEquals(readDate.getTime(), 915170400000L);
}
Notice that we’re passing the appropriate class to the readObject() method, this makes our code cast-free.
4. Serializers
In this section, we’ll show which Serializers are already available, and then we’ll create our own.
4.1. Default Serializers
When Kryo serializes an object, it creates an instance of a previously registered Serializer class to do the conversion to bytes. These are called default serializers and can be used without any setup on our part.
The library already provides several such serializers that process primitives, lists, maps, enums, etc. If no serializer is found for a given class, then a FieldSerializer is used, which can handle almost any type of object.
Let’s see how this looks like. First, lets’ create a Person class:
public class Person {
private String name = "John Doe";
private int age = 18;
private Date birthDate = new Date(933191282821L);
// standard constructors, getters, and setters
}
Now, let’s write an object from this class and then read it back:
@Test
public void givenPerson_whenSerializing_thenReadCorrectly() {
Person person = new Person();
kryo.writeObject(output, person);
output.close();
Person readPerson = kryo.readObject(input, Person.class);
input.close();
assertEquals(readPerson.getName(), "John Doe");
}
Notice that we didn’t have to specify anything to serialize a Person object since a FieldSerializer is created automatically for us.
4.2. Custom Serializers
If we need more control over the serialization process, we have two options; we can write our own Serializer class and register it with Kryo or let the class handle the serialization by itself.
To demonstrate the first option, let’s create a class that extends Serializer:
public class PersonSerializer extends Serializer<Person> {
public void write(Kryo kryo, Output output, Person object) {
output.writeString(object.getName());
output.writeLong(object.getBirthDate().getTime());
}
public Person read(Kryo kryo, Input input, Class<Person> type) {
Person person = new Person();
person.setName(input.readString());
long birthDate = input.readLong();
person.setBirthDate(new Date(birthDate));
person.setAge(calculateAge(birthDate));
return person;
}
private int calculateAge(long birthDate) {
// Some custom logic
return 18;
}
}
Now, let’s put it to test:
@Test
public void givenPerson_whenUsingCustomSerializer_thenReadCorrectly() {
Person person = new Person();
person.setAge(0);
kryo.register(Person.class, new PersonSerializer());
kryo.writeObject(output, person);
output.close();
Person readPerson = kryo.readObject(input, Person.class);
input.close();
assertEquals(readPerson.getName(), "John Doe");
assertEquals(readPerson.getAge(), 18);
}
Notice that the age field is equal to 18, even though we set it previously to 0.
We can also use the @DefaultSerializer annotation to let Kryo know we want to use the PersonSerializer each time it needs to handle a Person object. This helps avoid the call to the register() method:
@DefaultSerializer(PersonSerializer.class)
public class Person implements KryoSerializable {
// ...
}
For the second option, let’s modify our Person class to extend the KryoSerializable interface:
public class Person implements KryoSerializable {
// ...
public void write(Kryo kryo, Output output) {
output.writeString(name);
// ...
}
public void read(Kryo kryo, Input input) {
name = input.readString();
// ...
}
}
Since the test case for this option is equal to a previous one, is not included here. However, you can find it in the source code for this article.
4.3. Java Serializer
In sporadic cases, Kryo won’t be able to serialize a class. If this happens, and writing a custom serializer isn’t an option, we can use the standard Java serialization mechanism using a JavaSerializer. This requires that the class implements the Serializable interface as usual.
Here’s an example that uses the aforementioned serializer:
public class ComplexObject implements Serializable {
private String name = "Bael";
// standard getters and setters
}
@Test
public void givenJavaSerializable_whenSerializing_thenReadCorrectly() {
ComplexClass complexObject = new ComplexClass();
kryo.register(ComplexClass.class, new JavaSerializer());
kryo.writeObject(output, complexObject);
output.close();
ComplexClass readComplexObject = kryo.readObject(input, ComplexClass.class);
input.close();
assertEquals(readComplexObject.getName(), "Bael");
}
5. Conclusion
In this tutorial, we explored the most notable features of the Kryo library.
We serialized multiple simple objects and used the FieldSerializer class to deal with a custom one. We also created a custom serializer and demonstrated how to fallback to the standard Java serialization mechanism if needed.
As always, the complete source code for this article can be found over on Github.