Post

Understanding Kotlin's Type Hierarchy: Any, Unit, and Nothing

Understanding Kotlin's Type Hierarchy: Any, Unit, and Nothing

In Kotlin’s type system, three special types play crucial roles in different scenarios: Any, Unit, and Nothing. Let’s explore what makes each unique and when to use them.

Any

Any is the root of Kotlin’s type hierarchy - similar to Java’s Object class. Every class in Kotlin implicitly inherits from Any, which provides three fundamental methods: equals, hashCode, and toString.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Definition of Any class
public open class Any {
  public open operator fun equals(other: Any?): Boolean
  public open fun hashCode(): Int
  public open fun toString(): String
}

// Real-world example: Data class with custom equality
data class User(
  val id: Int,
  val email: String,
  val displayName: String? = null
) {
  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other !is User) return false
    // Users are equal if they have the same email, regardless of display name
    return email.equals(other.email, ignoreCase = true)
  }
  
  override fun hashCode(): Int = email.lowercase().hashCode()
  
  override fun toString(): String = "User(id=$id, email=$email${displayName?.let { ", name=$it" } ?: ""})"
}

fun main() {
  val user1 = User(1, "alice@example.com", "Alice")
  val user2 = User(2, "alice@example.com", "Al")  // Different ID and name, same email
  val user3 = User(3, "bob@example.com")
  
  println(user1 == user2)  // true (same email)
  println(user1 == user3)  // false (different email)
  
  // Using Any as a type parameter
  val userMap = mutableMapOf<Any, String>()
  userMap[user1] = "Active"
  println(userMap[user2])  // Prints: "Active" (because user1 equals user2)
}

Unit

Unit in Kotlin is the equivalent of void in Java, but with an important distinction: Unit is an actual type with a single instance. It’s commonly used for functions that perform actions rather than produce values.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Definition of Unit
public object Unit {
  override fun toString() = "kotlin.Unit"
}

// Real-world example: Event handling system
class EventDispatcher {
  private val listeners = mutableMapOf<String, (Event) -> Unit>()
  
  fun addEventListener(eventName: String, handler: (Event) -> Unit) {
    listeners[eventName] = handler
  }
  
  fun dispatchEvent(event: Event) {
    listeners[event.name]?.invoke(event)
  }
}

class UserService(private val dispatcher: EventDispatcher) {
  fun registerUser(email: String): Unit = runBlocking {
    try {
      // Simulate user registration
      delay(1000)
      dispatcher.dispatchEvent(Event("userRegistered", email))
    } catch (e: Exception) {
      dispatcher.dispatchEvent(Event("registrationFailed", e.message ?: "Unknown error"))
    }
  }
}

data class Event(val name: String, val data: Any)

fun main() {
  val dispatcher = EventDispatcher()
  val userService = UserService(dispatcher)
  
  // Event listener returns Unit implicitly
  dispatcher.addEventListener("userRegistered") { event ->
    println("User registered: ${event.data}")
  }
  
  userService.registerUser("alice@example.com")
}

Nothing

Nothing is a special type that represents a value that never exists - it’s used to mark code locations that can never be reached. This type has no instances and is commonly used for error handling and representing impossible states.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class Nothing private constructor()

// Real-world example: API Response handling
sealed class ApiResponse<out T> {
  data class Success<T>(val data: T): ApiResponse<T>()
  data class Error(val code: Int, val message: String): ApiResponse<Nothing>()
  data object Loading : ApiResponse<Nothing>()
}

class UserRepository(private val api: UserApi) {
  suspend fun getUser(id: Int): ApiResponse<User> {
    return try {
      ApiResponse.Success(api.fetchUser(id))
    } catch (e: Exception) {
      when (e) {
        is HttpException -> ApiResponse.Error(e.code(), e.message())
        else -> ApiResponse.Error(500, "Unknown error occurred")
      }
    }
  }
  
  fun throwError(message: String): Nothing {
    throw IllegalStateException(message)
  }
}

fun main() = runBlocking {
  val repository = UserRepository(mockApi())
  
  when (val response = repository.getUser(1)) {
    is ApiResponse.Success -> println("User: ${response.data}")
    is ApiResponse.Error -> println("Error: ${response.message}")
    is ApiResponse.Loading -> println("Loading...")
  }
  
  try {
    repository.throwError("Invalid state")
    println("This line never executes")
  } catch (e: IllegalStateException) {
    println("Error caught: ${e.message}")
  }
}

Each of these types serves a specific purpose in Kotlin’s type system:

  • Any provides a common base type and basic object operations
  • Unit represents the absence of a return value while still being a type
  • Nothing indicates that code will never reach a normal completion

Understanding these types helps in creating more expressive and type-safe Kotlin code.

This post is licensed under CC BY 4.0 by the author.