Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've likely written dozens of functions in Kotlin that don't return a value. You just leave the return type off, and it works. But as you start working with generics or interfacing with Java libraries, you'll run into two terms: Unit and Void. They seem to mean the same thing—"nothing comes back"—but they behave very differently under the hood.

To see this in action, let's build a simple TaskRunner. I want a system where I can define different types of tasks: some that return a result (like a string) and some that just perform an action and finish.

Defining a generic Task interface

I'll start by creating a generic interface. Since some tasks return data and others don't, the return type needs to be flexible.

interface Task<T> {
    fun execute(): T
}

Now, I want a task that just logs a message to the console. It doesn't need to return a value, so my instinct is to make it "void."

The mistake: Trying to use Void for 'nothing'

Coming from a Java background, I might try to use the Void type (capital V). In Java, Void is a placeholder used in generics to signal that the method returns nothing. Let's see what happens when I try that in Kotlin.

class LogTask(val message: String) : Task<Void> {
    override fun execute(): Void {
        println("Log: $message")
        // Wait, the compiler is complaining here!
        return null 
    }
}

Here's the problem: Void in Kotlin is actually the Java java.lang.Void type. It's a class, but it can't be instantiated. To satisfy the return type Void, I'm forced to return null. That feels clunky and wrong for a language that emphasizes null safety. I'm essentially lying to the compiler, saying "I'm returning a type that can't exist, so here's a null."

Switching to the Kotlin way with Unit

This is where Unit comes in. Unlike Java's void, which is a keyword meaning "no value," Kotlin's Unit is a real object. It's a singleton. When a function returns Unit, it's actually returning a value—it's just a value that carries no useful information.

Let's rewrite the LogTask using Unit instead.

class LogTask(val message: String) : Task<Unit> {
    override fun execute(): Unit {
        println("Log: $message")
        // I don't even need a return statement here.
        // Kotlin automatically returns the Unit object.
    }
}

This is much cleaner. Because Unit is a real type, it fits perfectly into the generic Task<T> interface. The execute() function still returns something, but that "something" is just the Unit singleton.

When would you actually use Void?

You might be wondering why Void even exists in Kotlin. You'll almost never use it in pure Kotlin code. You'll only encounter it when you're overriding a method from a Java library that was defined with <Void> in its generics. If the Java side demands Void, you have to provide null. Otherwise, always stick with Unit.

To wrap this up, let's look at how we'd use these tasks in a runner. Notice how we can treat them all the same because they both conform to the Task interface, regardless of whether they return a meaningful result or just Unit.

fun main() {
    val tasks: List<Task<*>> = listOf(
        LogTask("System starting..."),
        object : Task<String> {
            override fun execute() = "Task Complete!"
        }
    )

    for (task in tasks) {
        val result = task.execute()
        println("Result was: $result")
    }
}

When the LogTask executes, the result variable will literally hold the kotlin.Unit object. It's not null; it's just "Unit."




📋 Practical Task

Refactoring the NotificationDispatcher

You are working on a notification system. You have a generic Dispatcher<T> interface. Currently, the SmsDispatcher is incorrectly using Void and returning null, which is causing confusion and potential null-pointer risks in the codebase.

Your Task:

  1. Modify the SmsDispatcher class to use Unit instead of Void.
  2. Remove the return null statement from the send() method.
  3. Ensure the code compiles and correctly implements the Dispatcher<Unit> interface.
interface Dispatcher<T> {
    fun send(message: String): T
}

class SmsDispatcher : Dispatcher<Void> {
    override fun send(message: String): Void {
        println("Sending SMS: $message")
        return null // Fix this!
    }
}

fun main() {
    val dispatcher: Dispatcher<Unit> = SmsDispatcher() 
    dispatcher.send("Hello Kotlin!")
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.