Kotlin Interview Questions for Generics
1. What are generics in Kotlin, and why are they important?
Generics in Kotlin allow you to create classes, interfaces, and functions that operate on a type parameter, providing type safety and code reusability. They ensure that you can use the same implementation for different data types without sacrificing type checking.
Why are generics important?
- Type Safety: Generics enforce compile-time checks, reducing runtime errors by ensuring type compatibility.
- Code Reusability: A single implementation can work with multiple types, reducing redundancy.
- Flexibility: They enable building more flexible and reusable libraries or APIs.
Example - Generic Function:
fun printList(items: List) {
for (item in items) {
println(item)
}
}
fun main() {
val intList = listOf(1, 2, 3)
val stringList = listOf("Kotlin", "Java", "Python")
printList(intList) // Works with Int
printList(stringList) // Works with String
}
Generics are a cornerstone of Kotlin’s type system, enabling safer and more expressive code.
2. What is variance in Kotlin generics, and how does it improve type safety?
Variance in Kotlin generics describes how subtype relationships between types are preserved when applied to generic types. Kotlin uses two keywords to control variance:
- out: Denotes covariance, allowing a generic type to produce values of the specified type.
- in: Denotes contravariance, allowing a generic type to consume values of the specified type.
Example - Covariance and Contravariance:
open class Animal
class Dog : Animal()
// Covariance: Can only produce T (e.g., return values)
class Box(val item: T)
// Contravariance: Can only consume T (e.g., as parameters)
class Action {
fun perform(action: T) { println("Action on $action") }
}
fun main() {
val dogBox: Box = Box(Dog())
val animalBox: Box = dogBox // Covariance allows this
val action: Action = Action()
val dogAction: Action = action // Contravariance allows this
}
Variance ensures that generic types are used safely while maintaining flexibility in type hierarchies.
3. What is the difference between `reified` and non-reified type parameters in Kotlin?
Kotlin uses type erasure for generics, meaning type information is not available at runtime. However, by marking a type parameter as reified, you can retain type information at runtime in inline functions.
- Non-Reified: Type parameters cannot be checked at runtime due to type erasure.
- Reified: Reified parameters retain their type information, enabling runtime checks and operations.
Example - Reified Type Parameters:
inline fun isInstance(obj: Any): Boolean {
return obj is T
}
fun main() {
println(isInstance("Kotlin")) // Output: true
println(isInstance("Kotlin")) // Output: false
}
Reified type parameters are a powerful tool for working with generic types that require runtime type inspection.
4. What is the `where` keyword in Kotlin generics, and how is it used?
The where keyword in Kotlin is used to add constraints to generic type parameters, ensuring that they adhere to specific bounds or implement certain interfaces.
Usage:
- Restrict a type parameter to a specific class or its subclasses.
- Enforce that a type parameter implements one or more interfaces.
Example - Using where
with Multiple Constraints:
fun printSum(list: List) where T : Number, T : Comparable {
println(list.sumOf { it.toDouble() })
}
fun main() {
printSum(listOf(1, 2, 3)) // Works with Int
printSum(listOf(1.5, 2.5, 3.5)) // Works with Double
}
The
where
keyword provides precise control over generic type parameters, ensuring they meet specific requirements.
5. How do Kotlin’s generic functions differ from generic classes?
Kotlin supports generics for both functions and classes, but they differ in how and when type parameters are used:
- Generic Functions: The type parameter is declared and used only within the function. It provides flexibility for one-off operations.
- Generic Classes: The type parameter is tied to the class and can be used across all its methods and properties.
Example - Generic Function vs Generic Class:
// Generic Function
fun printItem(item: T) {
println("Item: $item")
}
// Generic Class
class Container(private val value: T) {
fun getValue(): T = value
}
fun main() {
printItem(42) // Works with Int
printItem("Kotlin") // Works with String
val container = Container(100)
println("Container value: ${container.getValue()}")
}
Generic functions are more lightweight, while generic classes are suited for scenarios requiring consistent type handling across multiple methods.
6. How does Kotlin handle generic type erasure, and what are the implications?
Like Java, Kotlin generics use type erasure, meaning type information is removed at runtime. This ensures backward compatibility with Java but comes with some limitations.
Implications of Type Erasure:
- Generic type information is not available at runtime. For example, a List
and List
both appear as List
at runtime.
- You cannot check the type of a generic type parameter at runtime using is
or !is
.
- To overcome this, Kotlin provides the reified keyword in inline functions to retain type information.
Example:
fun checkType(list: List) {
if (list is List) { // Compilation error due to type erasure
println("This is a List of Strings")
}
}
While type erasure simplifies compatibility, developers need to use alternative approaches, like reified types, for runtime type checks.
7. What are star projections (`*`) in Kotlin generics, and when are they used?
Star projections (*
) in Kotlin are used to represent unknown or wildcard types in generics. They allow you to work with generic types when you do not know or care about the specific type argument.
Usage:
- When reading from a generic type without modifying it.
- When the specific type parameter is not important in the context.
Example:
fun printListItems(list: List<*>) {
for (item in list) {
println(item)
}
}
fun main() {
val stringList = listOf("A", "B", "C")
val intList = listOf(1, 2, 3)
printListItems(stringList) // Prints: A, B, C
printListItems(intList) // Prints: 1, 2, 3
}
Star projections are useful for making code more flexible while ensuring type safety in operations that do not depend on the exact type.
8. What are bounded type parameters in Kotlin generics?
Bounded type parameters in Kotlin restrict the types that can be used as arguments for a generic type. This is done using the :
operator to specify an upper bound.
Benefits of Bounded Types:
- They allow you to enforce constraints on type parameters.
- They enable access to properties and methods of the specified upper bound.
Example:
fun sumNumbers(list: List): Double {
return list.sumOf { it.toDouble() }
}
fun main() {
val intList = listOf(1, 2, 3)
val doubleList = listOf(1.5, 2.5, 3.5)
println(sumNumbers(intList)) // Output: 6.0
println(sumNumbers(doubleList)) // Output: 7.5
}
Bounded type parameters provide flexibility while maintaining type safety by enforcing specific constraints.
9. What are generic type aliases in Kotlin, and how do they simplify complex generics?
Generic type aliases in Kotlin provide a way to create shorter and more readable names for complex generic types. They simplify code by reducing verbosity and improving maintainability.
Syntax: typealias NewName = OriginalType
Example:
typealias StringMap = Map
typealias Callback = (T) -> Unit
fun printMap(map: StringMap) {
for ((key, value) in map) {
println("$key -> $value")
}
}
fun executeCallback(callback: Callback) {
callback(42)
}
fun main() {
val map: StringMap = mapOf("A" to "Apple", "B" to "Banana")
printMap(map)
executeCallback { value -> println("Callback received: $value") }
}
// Output:
// A -> Apple
// B -> Banana
// Callback received: 42
Type aliases make code more concise and expressive when dealing with complex or frequently used generic types.
10. What are generic type constraints, and how do they work with interfaces?
Kotlin allows you to specify constraints on generic type parameters, ensuring they implement specific interfaces or are subclasses of a certain type. This is done using the where
keyword.
Usage: Type constraints can enforce multiple conditions, such as implementing multiple interfaces or extending a particular class.
Example:
interface Identifiable {
val id: Int
}
fun printIds(items: List) where T : Identifiable {
for (item in items) {
println(item.id)
}
}
data class User(override val id: Int, val name: String) : Identifiable
fun main() {
val users = listOf(User(1, "Alice"), User(2, "Bob"))
printIds(users)
}
// Output:
// 1
// 2
Type constraints ensure type safety while providing access to specific methods or properties of the constrained type.
11. What is the difference between `List<T>` and `List<out T>` in Kotlin?
The key difference between List<T>
and List<out T>
lies in variance:
- List<T>
: This is an invariant list, meaning you cannot assign a List<Dog>
to a List<Animal>
even if Dog
is a subtype of Animal
. The types must match exactly.
- List<out T>: This is a covariant list, meaning you can assign a List
to a List
. Covariance allows a generic type to be treated as a subtype of another type.
Example:
open class Animal
class Dog : Animal()
fun printAnimals(animals: List) {
for (animal in animals) println(animal)
}
fun main() {
val dogs: List = listOf(Dog(), Dog())
// printAnimals(dogs) // Error: List is not a List
val animals: List = dogs // Covariance allows this
printAnimals(animals)
}
Using
out
makes collections safer and more flexible when working with hierarchies.
12. How do generics work with inline functions in Kotlin?
Generics in inline functions allow the use of reified type parameters, enabling runtime operations that are otherwise impossible with normal generics due to type erasure.
Example - Reified Type Parameter:
inline fun filterByType(items: List): List {
return items.filterIsInstance()
}
fun main() {
val mixedList = listOf(1, "Kotlin", 2.5, "Java")
val strings = filterByType(mixedList)
println(strings) // Output: [Kotlin, Java]
}
Reified generics with inline functions make type-safe operations possible at runtime, enhancing the flexibility of generic functions.
13. What is the purpose of generic constraints on class declarations in Kotlin?
Generic constraints on class declarations restrict the types that can be used as type arguments for the class. This ensures that the class can only operate on valid types, improving type safety and functionality.
Example - Generic Constraint on a Class:
class Repository where T : Comparable {
fun save(entity: T) {
println("Saved entity: $entity")
}
}
open class Entity(val id: Int)
class User(id: Int) : Entity(id), Comparable {
override fun compareTo(other: User): Int = id.compareTo(other.id)
}
fun main() {
val userRepo = Repository()
userRepo.save(User(1)) // Output: Saved entity: User(id=1)
}
By enforcing constraints, generic classes can rely on specific properties or methods of the type parameter, making them more robust and expressive.
14. What are the limitations of generics in Kotlin?
Kotlin generics, like Java generics, have some limitations due to type erasure. These include:
- No Runtime Type Checking: Generic type parameters are erased at runtime, so operations like is
or !is
cannot be used directly on generic types.
- No Arrays of Generic Types: Kotlin does not allow arrays of generic types due to type safety issues. For example, Array
cannot be used if T
is a generic type.
- Type Information Loss: You cannot retrieve the generic type information of a class at runtime unless the type is explicitly retained, such as using reified types in inline functions.
Despite these limitations, Kotlin provides workarounds like reified types, type-safe builders, and reflection to handle specific scenarios.
15. How does Kotlin handle multiple constraints on generic types?
Kotlin allows you to apply multiple constraints to a generic type parameter using the where
keyword. This ensures that the type parameter meets multiple conditions, such as implementing several interfaces or extending a particular class.
Example - Multiple Constraints:
interface Identifiable {
val id: Int
}
interface Printable {
fun print()
}
fun process(item: T) where T : Identifiable, T : Printable {
println("ID: ${item.id}")
item.print()
}
class Document(override val id: Int) : Identifiable, Printable {
override fun print() {
println("Printing document #$id")
}
}
fun main() {
val doc = Document(101)
process(doc)
}
// Output:
// ID: 101
// Printing document #101
Multiple constraints enable developers to enforce complex requirements on generic types, ensuring type safety and flexibility.