Kotlin Serializable Classes

We are free to write our own serializable classes. All we need to do is have our class implement the java.io.Serializable interface to mark it as a candidate for serialization. All non-transient fields are written to the output stream and restored by the input stream. Here is a program that demonstrates using serialization with our own classes.

import java.io.*

data class Cook(val name : String = "Bob",
                val job : String = "Cook",
                @Transient val age : Int = 44) : Serializable

fun main(args : Array<String>){
    val file = "bob.ser"

    val tiredBob = Cook()
    println("Before Serialization")
    println(tiredBob)

    ObjectOutputStream(FileOutputStream(file)).use{ it -> it.writeObject(tiredBob)}

    println("Bob has been serialized")
    println()
    println("Time to wake Bob up")

    ObjectInputStream(FileInputStream(file)).use { it ->
        val restedBob = it.readObject()

        when (restedBob){
            is Cook -> {
                println("Does Bob remember his age?")
                println(restedBob)
            }
            else -> println("Failed to restore Bob")
        }
    }
}

Our program defines a Cook data class on lines 3-5. The class implements Serializable so it will work with the JVM serialization mechanism. The age property is annotated with @Transient, but more on that later.

The main method creates a tiredBob variable on line 10. On line 14, we open the file bob.ser by passing the name of the file to the FileOutputStream object. The FileOutputStream object is then passed to the constructor of the ObjectOutputStream object. We apply the use() function to make sure the file is closed when finished. The tiredBob object is written to disk by invoking the writeObject() method on the ObjectOutputStream object.

Restoring Bob is done on lines 20-30. Once again, we start by opening the bob.ser file by creating a FileInputStream object and passing the name of the file the constructor. The FileInputStream object is passed to the constructor of the ObjectInputStream object. We chain this operation with the use() function again to ensure the file is closed.

Line 21 restores Bob to memory by calling readObject(). The return type of readObject() is Any, so it’s up to us to down cast the object back into Cook. We do this on lines 23-28. On line 26, we print Bob to the console, but his age is 0. That’s because the age property is transient and therefore excluded from the serialization mechanism.

Transient Properties

Transient properties are defined by adding the @Transient annotation in front of the property (line 5). It’s used because the JVM only serializes primitives and Serializable objects. I once ran into a program where a class was mixed with Swing UI components and the state of the object needed to get sent over network sockets. The UI components were not serializable, so they were marked transient. Otherwise, the JVM would have failed to serialize the object and would have raised an exception.

This was a rather poor design, by the way. The class should have been designed with composition and have the data class separated from the UI components. However, that’s not how this program had been written. Either way, there are certain times where we would want to exclude serializable fields. A password field is a prime example of what should be marked transient.

However, it’s up to the developer to decide what should be included and not included in the serialized object. Kotlin provides the @Transient annotation to accomplish the task. Any object that isn’t @Transient will get included in the serialized object.

Advertisement

Kotlin Object Serialization

Whenever a class implements Serializable, it’s a candidate for object serialization. The serialization mechanism converts an object into bytes and then writes the object to the output stream. We use the class ObjectOutputStream to serialize a file and then ObjectInputStream to restore an object.

import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream

fun main(args : Array<String>){
    //Destination File
    val file = "belchers.burgers"

    //A map of family
    val family = mapOf(
            "Bob" to "Father",
            "Linda" to "Mother",
            "Tina" to "Oldest",
            "Gene" to "Middle",
            "Louise" to "Youngest")

    //Write the family map object to a file
    ObjectOutputStream(FileOutputStream(file)).use{ it -> it.writeObject(family)}

    println("Wrote $file")
    println()
    println("Time to read $file back")

    //Now time to read the family back into memory
    ObjectInputStream(FileInputStream(file)).use { it ->
        //Read the family back from the file
        val restedFamily = it.readObject()

        //Cast it back into a Map
        when (restedFamily) {
            //We can't use <String, String> because of type erasure
            is Map<*, *> -> println(restedFamily)
            else -> println("Deserialization failed")
        }
    }
}

The example program writes a map of strings to a file using object serialization. It begins by creating a map of test data on lines 11-16. Line 19 opens the file by creating a FileOutputStream object and passing in the file name to the constructor. The FileOutputStream object gets passed to the newly created ObjectOutputStream. We apply the use() function to make sure all resources are closed when finished.

Writing the map to the file is painless. All we need to do is use the writeObject() method found on ObjectOutputStream, shown on line 19. The class does all of the work of flattening the family Map object into bytes and writing the bytes to the file. The use() function closes the file and the serialization process is complete.

Reading the object back into memory is almost as simple. We open the file by creating a new FileInputStream object and supplying the constructor with the file name. The FileInputStream object is supplied to the constructor of the ObjectInputStream and we chain it to the use() function to make sure the file gets closed when finished.

The object is restored with the readObject() method, but there is a catch. The readObject() method returns Any. It’s our job to downcast to the proper type. On line 31, we use the when() function and on line 33, we check that it is a Map. Since map is a generic interface and serialization doesn’t save type, we use *, * for the type arguments. At this point, we can work on the restedFamily object normally.

Kotlin Data Streams

Data streams are used to write binary data. The DataOutputStream writes binary data of primitive types, while DataInputStream reads data back from the binary stream and converts it to primitive types. Here is an example program that writes data to a file and then reads it back into memory.

import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.FileInputStream
import java.io.FileOutputStream

fun main(args : Array<String>){
    val burgers = "data.burgers"

    //Open the file in binary mode
    DataOutputStream(FileOutputStream(burgers)).use { dos ->
        with(dos){
            //Notice we have to write our data types
            writeInt("Bob is Great\n".length) //Record length of the array
            writeChars("Bob is Great\n") //Write the array
            writeBoolean(true) //Write a boolean

            writeInt("How many burgers can Bob cook?\n".length) //Record length of array
            writeBytes("How many burgers can Bob cook?\n") //Write the array
            writeInt(Int.MAX_VALUE) //Write an int

            for (i in 0..5){
                writeByte(i) //Write a byte
                writeDouble(i.toDouble()) //Write a double
                writeFloat(i.toFloat()) //Write a float
                writeInt(i) //Write an int
                writeLong(i.toLong()) //Write a long
            }
        }
    }

    //Open a binary file in read mode. It has to be read in the same order
    //in which it was written
    DataInputStream(FileInputStream(burgers)).use {dis ->
        with (dis){
            val bobSize = readInt() //Read back the size of the array
            for (i in 0 until bobSize){
                print(readChar()) //Print the array one character at a time
            }
            println(readBoolean()) //Read a boolean

            val burgerSize = readInt() //Length of the next array
            for (i in 0 until burgerSize){
                print(readByte().toChar()) //Print array one character at a time
            }
            println(readInt()) //Read an int

            for (i in 0..5){
                println(readByte()) //Read a byte
                println(readDouble()) //Read a double
                println(readFloat()) //Read a float
                println(readInt()) //Read an int
                println(readLong()) //Read a long
            }
        }

    }
}

The program creates a FileOutputStream object and passes the name of the file to its constructor. The FileOutputStream object is then passed to the constructor of DataOutputStream. We apply the use() function to ensure all resources are freed properly when we have finished. The file is now open for writing in binary mode.

When we wish to use the same object repeatedly, we can pass it to the with() function. In our case, we intend to keep using our DataOutputStream object, so on line 11, we pass it to the with() function. Inside of the with() function, all method calls will target the dos object because it was supplied to with().

Since we intend to write a string to the file, we need to record the length of the string. We do this using the writeInt function and passing the length of our string to it. Then we can use writeChars() to write a character array to the file. The String argument is converted to a character array and written to the file. Finally, we call writeBoolean to write true/false values to the file.

The next section is a repeat of the first. We intend to write another string to the file, but do so, we need to record the length of the file. Once again, we turn to writeInt() to record an int value. The next line, we use writeBytes() rather than writeChars() to demonstrate how we can write a byte array rather than a String. The DataOutputStream class sees to the details of turning a String into a byte array. Finally, we write another int value to the stream.

Next, we enter a for loop on line 21. Inside of the for loop, we demonstrate writing different primitive types to the file. We can use writeByte() for a byte, writeDouble() for a double, and so on for each primitive type. The DataOutputStream class knows the size of each primitive type and writes the correct number of bytes for each primitive.

When we are done writing the object, we open it again to read it. Line 33 creates a FileInputStream object that accepts the path to the file in its constructor. The FileInputStream object is chained to DataInputStream by passing it to the constructor of DataInputStream. We apply the use() function to ensure all resources are properly closed.

Reading the file requires the file to be read in the same order in which it is written. Our first order of business is to grab the size of the character array we wrote to the file earlier. We use readInt() on line 35 followed by a for loop that terminates at the size of the array on line 36. Each iteration of the for loop calls readChar() and the String is printed to the console. When we are finished, we read a boolean on line 39.

Our next array was a byte array. Once again, we need it’s final size so we call readInt() on line 41. Lines 42-44 run through the array and call readByte() until the loop terminates. Each byte is converted to a character object using toChar(). On line 45, we read an int using readInt().

The final portion of the program repeats the for loop found earlier. In this case, we enter a for loop that terminates after five iterations (line 47). Inside of the for loop, we call readByte(), readDouble(), readFloat(), and so on. Each call prints the restored variable to the console.

%d bloggers like this: