Skip to main content

Kotlin Generic Classes

Introduction

Generic classes are a powerful feature in Kotlin that allow you to create classes that can work with different types while maintaining type safety. Instead of creating separate classes for different data types, you can define a class once with type parameters that can be specified when instantiating the class.

In this tutorial, you'll learn how to:

  • Define generic classes in Kotlin
  • Use type parameters effectively
  • Create generic constraints
  • Apply generics in real-world scenarios

What are Generic Classes?

Generic classes in Kotlin are classes that can operate on one or more types specified by the user at instantiation time. This enables you to write code that is both type-safe and reusable.

Basic Syntax

To define a generic class in Kotlin, you use angle brackets (<>) with type parameters following the class name:

kotlin
class Box<T>(var content: T) {
fun getContent(): T {
return content
}

fun setContent(newValue: T) {
content = newValue
}
}

In this example, Box is a generic class with a type parameter T. The type parameter T is used as the type of the content property.

Creating Generic Classes

Let's start with a simple example of creating and using a generic class:

kotlin
class Container<T>(var value: T) {
fun getValue(): T = value
fun setValue(newValue: T) {
value = newValue
}
}

fun main() {
// Create a Container for String
val stringContainer = Container("Hello, Generics!")
println("String container value: ${stringContainer.getValue()}")

// Create a Container for Int
val intContainer = Container(42)
println("Integer container value: ${intContainer.getValue()}")

// Update values
stringContainer.setValue("Updated value")
intContainer.setValue(100)

println("Updated string container: ${stringContainer.getValue()}")
println("Updated integer container: ${intContainer.getValue()}")
}

Output:

String container value: Hello, Generics!
Integer container value: 42
Updated string container: Updated value
Updated integer container: 100

In this example:

  • We defined a Container class that can hold any type of value
  • We created containers for both String and Int types
  • Kotlin's type inference automatically determined the type parameter based on the constructor argument

Multiple Type Parameters

You can define generic classes with multiple type parameters when your class needs to work with more than one type:

kotlin
class Pair<K, V>(val key: K, val value: V) {
override fun toString(): String {
return "($key, $value)"
}
}

fun main() {
val pair1 = Pair("name", "John")
val pair2 = Pair(1, true)
val pair3 = Pair(3.14, 'π')

println(pair1) // (name, John)
println(pair2) // (1, true)
println(pair3) // (3.14, π)
}

Output:

(name, John)
(1, true)
(3.14, π)

Type Constraints

Sometimes you may want to restrict the types that can be used with your generic class. You can achieve this using type constraints:

kotlin
// Class that only accepts types that are subclasses of Number
class NumericContainer<T : Number>(val value: T) {
fun getValueAsDouble(): Double = value.toDouble()
fun getValueAsInt(): Int = value.toInt()
}

fun main() {
val intContainer = NumericContainer(42)
val doubleContainer = NumericContainer(3.14)

println("Int as double: ${intContainer.getValueAsDouble()}")
println("Double as int: ${doubleContainer.getValueAsInt()}")

// This would cause a compilation error since String is not a subclass of Number
// val stringContainer = NumericContainer("Not a number")
}

Output:

Int as double: 42.0
Double as int: 3

In this example:

  • The NumericContainer class has a type constraint T : Number
  • This ensures that T can only be a Number subclass (like Int, Double, Float, etc.)
  • The constraint allows us to use Number's methods like toDouble() and toInt()

Nullable Type Parameters

By default, type parameters in Kotlin can accept nullable types. You can make this explicit or add constraints:

kotlin
// Default behavior - T can be nullable
class Box<T>(val item: T)

// Explicitly make T non-nullable
class NonNullBox<T : Any>(val item: T)

fun main() {
// Works fine - nullable String
val nullableBox = Box<String?>(null)

// This would cause a compilation error
// val nonNullBox = NonNullBox<String?>(null)

// This works because item is non-null
val nonNullBox = NonNullBox("Hello")

println("Nullable box: ${nullableBox.item}")
println("Non-null box: ${nonNullBox.item}")
}

Output:

Nullable box: null
Non-null box: Hello

Real-World Example: Generic Repository

Let's implement a more practical example - a simple generic repository class that can be used for different entity types in a data-driven application:

kotlin
// Entity interface that all domain entities will implement
interface Entity {
val id: Int
}

// Sample entities
data class User(override val id: Int, val name: String, val email: String) : Entity
data class Product(override val id: Int, val name: String, val price: Double) : Entity

// Generic repository class
class Repository<T : Entity> {
private val items = mutableListOf<T>()

fun add(item: T) {
items.add(item)
}

fun remove(id: Int) {
items.removeIf { it.id == id }
}

fun getById(id: Int): T? {
return items.find { it.id == id }
}

fun getAll(): List<T> {
return items.toList()
}
}

fun main() {
// Create repositories for different entity types
val userRepo = Repository<User>()
val productRepo = Repository<Product>()

// Add items to repositories
userRepo.add(User(1, "Alice", "[email protected]"))
userRepo.add(User(2, "Bob", "[email protected]"))

productRepo.add(Product(1, "Laptop", 1299.99))
productRepo.add(Product(2, "Phone", 799.99))

// Retrieve and display data
println("All users:")
userRepo.getAll().forEach { println("User: ${it.name}, Email: ${it.email}") }

println("\nAll products:")
productRepo.getAll().forEach { println("Product: ${it.name}, Price: $${it.price}") }

// Find specific items
val user = userRepo.getById(2)
val product = productRepo.getById(1)

println("\nFound user: ${user?.name}")
println("Found product: ${product?.name} - $${product?.price}")
}

Output:

All users:
User: Alice, Email: [email protected]
User: Bob, Email: [email protected]

All products:
Product: Laptop, Price: $1299.99
Product: Phone, Price: $799.99

Found user: Bob
Found product: Laptop - $1299.99

This example demonstrates:

  • A generic Repository<T> class that works with any Entity type
  • Type constraints ensuring only Entity types can be used
  • Code reuse across different entity types without duplication
  • Type safety ensuring that the userRepo only contains User objects and the productRepo only contains Product objects

Generic Methods in Generic Classes

You can also define generic methods inside a generic class. These methods can have their own type parameters distinct from the class's type parameters:

kotlin
class DataProcessor<T> {
private val data: MutableList<T> = mutableListOf()

fun addItem(item: T) {
data.add(item)
}

fun getItems(): List<T> = data.toList()

// Generic method with its own type parameter
fun <R> transform(transformer: (T) -> R): List<R> {
return data.map { transformer(it) }
}
}

fun main() {
val numberProcessor = DataProcessor<Int>()
numberProcessor.addItem(1)
numberProcessor.addItem(2)
numberProcessor.addItem(3)

// Use the generic transform method to convert integers to strings
val stringResults = numberProcessor.transform { "Number: $it" }
println("Original numbers: ${numberProcessor.getItems()}")
println("Transformed strings: $stringResults")

// Use the generic transform method to convert integers to their squares
val squaredResults = numberProcessor.transform { it * it }
println("Squared numbers: $squaredResults")
}

Output:

Original numbers: [1, 2, 3]
Transformed strings: [Number: 1, Number: 2, Number: 3]
Squared numbers: [1, 4, 9]

Covariance and Contravariance in Generic Classes

Kotlin supports variance annotations for generic classes, which are advanced concepts that determine how subtypes relate:

kotlin
// Covariant class (out T)
class Producer<out T>(private val item: T) {
fun get(): T = item
}

// Contravariant class (in T)
class Consumer<in T> {
fun consume(item: T) {
println("Consumed: $item")
}
}

fun main() {
// Covariance example
val stringProducer: Producer<String> = Producer("Hello")
val anyProducer: Producer<Any> = stringProducer // This works because Producer is covariant
println(anyProducer.get())

// Contravariance example
val anyConsumer: Consumer<Any> = Consumer()
val stringConsumer: Consumer<String> = anyConsumer // This works because Consumer is contravariant
stringConsumer.consume("World")
}

Output:

Hello
Consumed: World
  • Covariance (out): Allows a class of a more derived type to be used where a class of a less derived type is expected
  • Contravariance (in): Allows a class of a less derived type to be used where a class of a more derived type is expected

Summary

Generic classes in Kotlin provide a powerful way to create reusable, type-safe components that can work with different data types. In this tutorial, we've covered:

  • Basic generic class syntax using class Name<T>
  • Creating classes with multiple type parameters
  • Applying constraints to type parameters
  • Working with nullable type parameters
  • Creating practical generic classes like repositories
  • Implementing generic methods within generic classes
  • Understanding variance concepts (covariance and contravariance)

By mastering generic classes, you can write more flexible and reusable code while maintaining type safety and reducing code duplication.

Exercises

To reinforce your understanding of generic classes in Kotlin, try these exercises:

  1. Create a generic Stack<T> class with push, pop, and peek operations
  2. Implement a generic Result<T> class that can represent either a success (with data of type T) or an error (with an error message)
  3. Design a generic Mapper<T, R> class that converts objects of type T to type R
  4. Create a generic Cache<K, V> class that stores key-value pairs with an expiration mechanism
  5. Implement a generic EventBus<T> class that allows components to publish and subscribe to events of type T

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)