1. Introduction

In programming, destructors are functions that clean up resources when they’re no longer needed, ensuring proper memory management. Moreover, languages like C++ and PHP explicitly use destructors to handle resource cleanup.

However, in Kotlin, destructors differ from those in C++ or PHP. Kotlin relies on garbage collection to automatically handle memory management, so there isn’t a direct equivalent to traditional destructors. Even so, developers often need to perform cleanup operations, such as releasing resources or closing connections, when they no longer need an object.

In this tutorial, we’ll explore how to achieve resource management in Kotlin, focusing on various techniques that mimic the functionality of destructors.

2. Using try-finally Blocks

The most straightforward way to manage resources in Kotlin is by using try-finally blocks. Code in the finally block always executes after the try block, regardless of whether an exception was thrown. Let’s use the finally block to ensure we correctly clean up resources when we’re done with them:

@Throws(IOException::class)
fun readFile(reader: BufferedReader): String {
    try {
        val content = StringBuilder()
        var line: String?
        while ((reader.readLine().also { line = it }) != null) {
            content.append(line)
        }
        return content.toString()
    } finally {
        reader.close()
    }
}

In this code snippet, we use the BufferedReader class to read a file, copy its content to a StringBuilder, and return it as a string. The finally block closes the reader, even if an error occurs during reading.

Now, we need to test this function to ensure that the finally block executes correctly:

@Test
@Throws(IOException::class)
fun `perform resource cleaning with try-finally block`() {
    val mockReader = mock(BufferedReader::class.java)
    `when`(mockReader.readLine()).thenReturn("Hello, Kotlin!", null)
    val content = readFile(mockReader)
    assertEquals("Hello, Kotlin!", content)

    verify(mockReader).close()
}

In this test, we first mock the behavior of the BufferedReader. This second mock return value of null for readLine() simulates the end of the file. After reading the content, we use the verify() function to check that the close() function executes as expected.

Furthermore, let’s demonstrate that the close() function still executes when readLine() throws an IOException:

@Test
@Throws(IOException::class)
fun `perform resource cleaning with try-finally block when IOException occurs`() {
    val mockReader = mock(BufferedReader::class.java)
    `when`(mockReader.readLine()).thenThrow(IOException("Test exception"))

    assertFailsWith<IOException> {
        readFile(mockReader)
    }

    verify(mockReader).close()
}

This test confirms the close() function gets called when an IOException occurs during the read operation. Furthermore, our code handles resource cleanup correctly even in exceptional situations.

3. Using the use() Function

Kotlin’s use() function is an extension function that automatically closes a resource after running the associated lambda. Consequently, this function simplifies resource management and reduces boilerplate code.

For instance, we can rewrite our readFile() function above using the use() function instead of the try-finally block and achieve the same result:

@Throws(IOException::class)
fun readFileUsingUse(reader: BufferedReader): String {
    return reader.use { br ->
        val content = StringBuilder()
        var line: String?
        while ((br.readLine().also { line = it }) != null) {
            content.append(line)
        }
        content.toString()
    }
}

In this case, we call the use() function on our BufferedReader, which will close the resource after the block executes. The use() function only works with Closeable objects because it automatically calls the close() function at the end of its execution. This approach streamlines resource management by handling the closure of the resource automatically.

Again, to observe and verify this in tests, we’ll use a mocked BufferedReader:

@Test
@Throws(IOException::class)
fun `perform resource cleaning with use`() {
    val mockReader = mock(BufferedReader::class.java)
    `when`(mockReader.readLine()).thenReturn("Hello, Kotlin!", null) 
    val content = readFileUsingUse(mockReader)
    assertEquals("Hello, Kotlin!", content)

    verify(mockReader).close()
}

Here, we mock the BufferedReader and test the use() function to ensure that it calls the close() function, confirming proper resource management.

Similarly, we also need to ensure that the close() function executes even in case of errors during reading:

@Test
@Throws(IOException::class)
fun `perform resource cleaning with use() when IOException occurs`() {
    val mockReader = mock(BufferedReader::class.java)
    `when`(mockReader.readLine()).thenThrow(IOException("Test exception"))

    assertFailsWith<IOException> {
        readFileUsingUse(mockReader)
    }

    verify(mockReader).close()
}

In this code snippet, we confirm that the BufferedReader indeed closes even if an IOException occurs during reading within use().

4. Conclusion

In this article, we’ve explored how Kotlin handles resource management in the absence of traditional destructors, as seen in languages like C++ or PHP.

We can manage resources effectively by using techniques like try-finally blocks and Kotlin’s use() function. These approaches ensure that essential resource cleanup operations execute even if an exception occurs. We’ve also seen how to observe and verify these cleanup processes through unit tests. This makes resource management both robust and reliable.

Although Kotlin’s garbage collection simplifies memory management, these techniques offer a structured way to handle resource cleanup. These approaches mimic the functionality of destructors, leading to safer and more maintainable code.

As usual, all the examples are available over on GitHub.