Kotlin Generics: Type Parameters, Variance (in/out), Reified Types & Bounds
1. What are generics in Kotlin?
Generics in Kotlin allow classes, functions, and interfaces to operate on types specified as parameters, enabling type-safe, reusable code. They use type parameters (e.g., <T>) to ensure compile-time type checking, preventing runtime errors. Kotlin generics support variance (in/out) and reified types (inline functions), making them more flexible than Java generics.
Benefits of Generics:
- Type Safety: Compile-time checks avoid casting and runtime errors.
- Reusability: Write code once for multiple types (e.g., generic lists).
- Performance: No boxing/unboxing overhead on JVM.
- Expressiveness: Variance and reified types simplify complex designs.
How do generics differ from C/C++?
- Kotlin: Type-safe, erased at runtime (like Java), supports variance and reified types.
- C++: Templates generate type-specific code at compile-time, no runtime erasure.
- Kotlin Advantage: Simpler syntax, null safety, and seamless JVM integration.
2. How do you define generic classes and interfaces in Kotlin?
Use type parameters in angle brackets (<T>) for classes/interfaces.
Syntax:
class GenericClass<T>(val item: T)
interface GenericInterface<T>
Multiple Parameters: <T, U>.
Bounds: T : UpperBound to constrain types.
3. Can you give an example of generic classes and interfaces?
// Generic class
class Box<T>(private val item: T) {
fun getItem(): T = item
fun <R> map(transform: (T) -> R): Box<R> = Box(transform(item))
}
// Generic interface
interface Repository<T> {
fun save(item: T): Boolean
fun findById(id: Int): T?
}
// Implementation
class StringRepository : Repository<String> {
private val data = mutableListOf<String>()
override fun save(item: String): Boolean {
return data.add(item)
}
override fun findById(id: Int): String? {
return data.getOrNull(id)
}
}
// Usage
fun main() {
val stringBox = Box("Hello")
println(stringBox.getItem()) // Hello
val intBox = Box(42)
val doubled = intBox.map { it * 2 }
println(doubled.getItem()) // 84
val repo = StringRepository()
repo.save("Kotlin")
println(repo.findById(0)) // Kotlin
}
Output:
Hello
84
Kotlin
Note:
Box<T>stores and retrievesT.Repository<T>defines operations onT.mapuses a generic return type<R>.
4. How do you define generic functions in Kotlin?
Place type parameters before the function name (e.g., fun <T> func(item: T)).
Bounds: <T : Comparable<T>> for constrained types.
Reified Types: Use inline for runtime type access (e.g., inline fun <reified T> func()).
5. Can you give an example of generic functions?
// Generic function
fun <T> findMax(list: List<T>): T where T : Comparable<T> {
return list.maxOrNull() ?: throw IllegalArgumentException("Empty list")
}
// Reified generic function
inline fun <reified T> isType(obj: Any): Boolean {
return obj is T
}
// Usage
fun main() {
val numbers = listOf(5, 3, 8, 1)
val maxNum = findMax(numbers)
println("Max number: $maxNum") // 8
val strings = listOf("apple", "banana", "cherry")
val maxString = findMax(strings)
println("Max string: $maxString") // cherry
println("Is 42 a String? ${isType<String>(42)}") // false
println("Is 'hello' a String? ${isType<String>("hello")}") // true
}
Output:
Max number: 8
Max string: cherry
Is 42 a String? false
Is 'hello' a String? true
Note:
findMaxrequiresT : Comparable<T>for comparison.isTypeusesreifiedfor runtime type checks.
6. What is variance in Kotlin generics?
Variance: Defines how generics behave with inheritance (e.g., List<out T> for read-only).
- Covariance (out): Allows using a subtype where a supertype is expected (read-only).
- Contravariance (in): Allows using a supertype where a subtype is expected (write-only).
- Invariant: No variance (default, like Java generics).
Use Case: Safe type handling (e.g., List<out Animal> accepts List<Dog>).
7. Can you give an example of variance?
open class Animal(val name: String)
class Dog(name: String) : Animal(name)
class Cat(name: String) : Animal(name)
fun <T> processAnimals(animals: List<T>) {
animals.forEach { println(it.name) }
}
// Covariant: List<out Animal> accepts List<Dog>
fun printAnimalNames(animals: List<out Animal>) {
animals.forEach { println(it.name) }
}
// Contravariant: Function accepting Animal accepts Dog
fun <T> feedAnimal(animal: T) where T : Animal {
println("Feeding ${animal.name}")
}
// Usage
fun main() {
val dogs = listOf(Dog("Buddy"), Dog("Max"))
val cats = listOf(Cat("Whiskers"))
// Covariance: List<Dog> is accepted as List<out Animal>
printAnimalNames(dogs)
// Contravariance: feedAnimal(Dog) works with Animal parameter
feedAnimal(Dog("Rex"))
feedAnimal(Cat("Luna"))
}
Output:
Buddy
Max
Feeding Rex
Feeding Luna
Note:
printAnimalNamesusesoutfor read-only access.feedAnimalusesinimplicitly viaT : Animalfor write operations.
8. What are reified generics in Kotlin?
Reified Generics: Allow access to type parameters at runtime (erased in Java/Kotlin generics). Requires inline functions.
Syntax: inline fun <reified T> func().
Use Case: Type checks, serialization, or reflection.
9. Can you give an example of reified generics?
inline fun <reified T> isType(obj: Any): Boolean {
return obj is T
}
inline fun <reified T> createInstance(): T {
return T::class.java.newInstance() // Simplified; use factory for complex types
}
fun main() {
val num = 42
val str = "Hello"
// Type checking with reified
println("42 is Int: ${isType<Int>(num)}") // true
println("Hello is String: ${isType<String>(str)}") // true
// Creating instance (example with data class)
data class Point(val x: Int, val y: Int)
// For data classes, use factory function
inline fun <reified T : Point> createPoint(x: Int, y: Int): T {
return T::class.constructors.first().call(x, y) as T
}
val point = createPoint<Point>(10, 20)
println("Point: $point")
}
Output:
42 is Int: true
Hello is String: true
Point: Point(x=10, y=20)
Note:
isTypeusesreifiedfor runtime type checks.- Requires
inlineto inline the function at call site.
10. Can you provide a comprehensive example of generics in Kotlin?
data class Employee(val name: String, val salary: Double)
// Generic class with variance
class Repository<out T>(private val items: List<T>) {
fun getAll(): List<T> = items
fun <R> map(transform: (T) -> R): List<R> = items.map(transform)
}
// Generic interface
interface Processor<T> {
fun process(item: T): String
}
// Generic function with reified
inline fun <reified T> createRepository(vararg items: T): Repository<T> {
return Repository(listOf(*items))
}
// Covariant usage
fun <T : Comparable<T>> findMax(items: List<T>): T {
return items.maxOrNull() ?: throw IllegalArgumentException("Empty list")
}
// Usage
fun main() {
// Generic repository
val repo = createRepository("Krishna", "Ram", "Charlie")
println("Repository items: ${repo.getAll()}")
val mapped = repo.map { it.uppercase() }
println("Mapped: $mapped")
// Find max
val salaries = listOf(60000.0, 55000.0, 65000.0)
val maxSalary = findMax(salaries)
println("Max salary: $maxSalary")
// Reified check
val list = listOf("Hello", 42, true)
println("List contains String: ${list.any { it is String }}")
}
Output:
Repository items: [Krishna, Ram, Charlie]
Mapped: [Krishna, Ram, CHARLIE]
Max salary: 65000.0
List contains String: true
Description:
- Generic Class:
Repository<out T>for read-only access. - Generic Function:
createRepository<reified T>with varargs. - Bounded Generics:
findMax<T : Comparable<T>>for comparable types. - Integrates variance, reified, and bounded generics.
11. What are common mistakes in Kotlin generics?
Generic Classes/Functions:
- Forgetting type bounds, causing type errors (e.g., non-comparable types).
- Overusing generics, complicating code unnecessarily.
Variance:
- Misusing
outfor mutable collections, causing type safety issues. - Applying
into read-heavy operations, limiting flexibility.
Reified Generics:
- Using
reifiedwithoutinline, causing compilation errors. - Overusing
reifiedin non-performance-critical code.
General:
- Ignoring type erasure, leading to runtime type checks.
- Not documenting generic constraints for clarity.
12. What are best practices for Kotlin generics?
Generic Classes/Functions:
- Use type parameters only when necessary for reusability.
- Specify bounds (e.g.,
T : Comparable<T>) for safety.
Variance:
- Use
outfor read-only generics (e.g.,List<out T>). - Use
infor write-only generics (e.g.,Consumer<in T>). - Avoid invariant generics when variance is applicable.
Reified Generics:
- Use
inlineforreifiedtypes to access runtime information. - Limit to specific use cases (e.g., type checks, serialization).
General:
- Document type parameters and bounds in KDoc.
- Test generics with multiple types to ensure correctness.
- Use Kotlin's type inference to simplify code.