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.

Kotlin Buffered Text Files

The BufferedReader and BufferedWriter classes improve the performance of reading and writing operations by adding an in-memory buffer to the streams. By using a memory buffer, the program and reduce the number of calls required to the underlying read and write streams and thus improve performance. Here is an example program that makes use of both BufferedReader and BufferedWriter.

fun main(args : Array<String>){
    when (args.size){
        //Check for two command line arguments
        2 -> {
            //Grab source and destination files
            val src = args[0]
            val dest = args[1]

            //Check if the destination file exists. We can create it
            //if needed
            with (File(dest)){
                if(!exists()){
                    createNewFile()
                }
            }

            //Now, open the source file in read mode. The BufferedReader
            //provides buffering to improve performance
            BufferedReader(FileReader(src)).use { reader ->

                //Likewise, open the destination file in write mode
                //The BufferedWriter class provides buffering for performance
                BufferedWriter(FileWriter(dest)).use { writer ->

                    //Read through the source file one character at a time
                    var character = reader.read()
                    while(character != -1){

                        //Write the character to the destination file
                        writer.write(character)

                        //Read the next character.
                        character = reader.read()
                    }
                }
            }
        }
        else -> {
            println("Source file followed by destination file required")
            System.exit(-1)
        }
    }
}

The example program copies the source file to the destination file. We begin by using the when() function to check if we have two and only two command line arguments. If we have a source and destination file, the program continues starting on line 6 otherwise it jumps down to line 39 and exits after printing an error message.

On lines 6 and 7, we grab our source and destination files from the command line parameters. On line 11, we create a new File object and pass it to the with() function to see if we need to make a new file for the destination. Line 12 uses the exits() property to see if the file exists, and if it doesn’t exist, line 13 creates the new file.

Starting at line 19, we open the source file and begin our copy operation. The file is opened by creating a new FileReader object and passing in the name of the source file. The FileReader object is then passed to the constructor of BufferedReader. We utilize the use() function to ensure that all resources are properly closed when we are finished with the read operation. It’s also worth noting that we call the lambda parameter reader rather than it to improve code readability.

Line 23 opens the destination file for writing. We create a FileWriter (the companion object to FileReader) and pass the name of the destination file to the FileWriter’s constructor. The FileWriter object is passed to the BufferedWriter constructor to provide buffering support. Once again, we utilize the use() function to ensure that all resources are closed when finished.

The copy operation is fairly anti-climatic. We read the first character on line 26 and then enter into a while loop that terminates when character == -1. Inside of the while loop, we write the character to the destination file (line 30) and then read the next character (line 33). The use() function that was applied to both the BufferedReader and BufferedWriter objects closes the files when finished.

The program can be run by using the following commands at the command line.

kotlinc BufferedCopy.kt -include-runtime -d BufferedCopy.jar
ava -jar BufferedCopy.jar  [dest file]

When finished, the dest file will be an exact copy of source file.