Interview Questions on Intermediate Kotlin Features
1. What are inline classes in Kotlin, and how do they improve performance?
Inline classes in Kotlin are a feature that allows wrapping a single value in a type-safe way without introducing runtime overhead. They are declared using the `value` modifier and are inlined at runtime, meaning no additional object is created.
Inline classes are useful for creating lightweight wrappers for primitive types or other classes, improving performance by avoiding boxing and unboxing.
Example:
@JvmInline
value class UserId(val id: String)
fun printUserId(userId: UserId) {
println("User ID: ${userId.id}")
}
fun main() {
val userId = UserId("12345")
printUserId(userId) // Output: User ID: 12345
}
2. What are delegated properties in Kotlin, and how do they work?
Delegated properties in Kotlin allow a property’s behavior to be delegated to another object using the `by` keyword. This is commonly used for lazy initialization, observable properties, or storing properties in custom objects.
Delegates can handle property logic, such as validation or caching, making the code more concise and reusable.
Example - Lazy Initialization:
class Example {
val lazyValue: String by lazy {
println("Computing value...")
"Hello, Kotlin"
}
}
fun main() {
val example = Example()
println(example.lazyValue) // Output: Computing value... Hello, Kotlin
println(example.lazyValue) // Output: Hello, Kotlin
}
3. How does Kotlin handle default arguments and named arguments in function calls?
Kotlin allows functions to define default values for parameters, reducing the need for overloaded methods. You can also use named arguments to explicitly specify which parameter a value is for, improving readability and avoiding ambiguity.
These features make function calls more concise and maintainable.
Example:
fun greet(name: String = "Guest", age: Int = 18) {
println("Hello, $name. You are $age years old.")
}
fun main() {
greet() // Output: Hello, Guest. You are 18 years old.
greet("Alice") // Output: Hello, Alice. You are 18 years old.
greet(age = 25, name = "Bob") // Output: Hello, Bob. You are 25 years old.
}
4. What is the difference between `object` expressions and `object` declarations in Kotlin?
In Kotlin, `object` expressions and `object` declarations both define single instances of a class, but they differ in usage and scope:
- `object` expression: Creates an anonymous object (similar to an anonymous inner class in Java). It is used locally and does not have a name.
- `object` declaration: Creates a named singleton object. It is instantiated lazily and globally accessible.
Example:
// Object expression
val anonymousObject = object {
val name = "Anonymous"
fun greet() = "Hello from $name"
}
// Object declaration
object Singleton {
val name = "Singleton"
fun greet() = "Hello from $name"
}
fun main() {
println(anonymousObject.greet()) // Output: Hello from Anonymous
println(Singleton.greet()) // Output: Hello from Singleton
}
5. What is the purpose of the `inline` keyword for higher-order functions in Kotlin?
The `inline` keyword in Kotlin is used for higher-order functions to improve performance by avoiding the creation of additional objects for function parameters. Instead, the function body is inlined at the call site.
This is particularly useful for lambda expressions and can help reduce runtime overhead, but it should be used judiciously to avoid code bloat.
Example:
inline fun execute(block: () -> Unit) {
println("Before execution")
block()
println("After execution")
}
fun main() {
execute {
println("Executing block")
}
// Output:
// Before execution
// Executing block
// After execution
}
6. What are reified types in Kotlin, and how are they used?
Reified types in Kotlin are used with inline functions to access the actual type information at runtime. Normally, type parameters are erased at runtime due to type erasure, but the `reified` keyword allows the type to be retained.
Reified types are commonly used for functions that require type checking or casting.
Example:
inline fun checkType(value: Any) {
if (value is T) {
println("The value is of type ${T::class.simpleName}")
} else {
println("The value is not of type ${T::class.simpleName}")
}
}
fun main() {
checkType("Kotlin") // Output: The value is of type String
checkType("Kotlin") // Output: The value is not of type Int
}
7. How do `vararg` parameters work in Kotlin, and when should you use them?
The `vararg` keyword in Kotlin is used to define a parameter that can accept a variable number of arguments. It allows you to pass multiple values of the same type to a function as a single parameter.
This feature is useful for functions like `println` or custom utility functions that handle a flexible number of inputs.
Example:
fun printAll(vararg items: String) {
for (item in items) {
println(item)
}
}
fun main() {
printAll("Kotlin", "Java", "Python")
// Output:
// Kotlin
// Java
// Python
}
8. What is the difference between `read-only` and `mutable` collections in Kotlin?
In Kotlin, collections are categorized into two types: `read-only` and `mutable`:
- Read-only collections: Cannot be modified after creation (e.g., `listOf`, `mapOf`, `setOf`).
- Mutable collections: Can be modified by adding, removing, or updating elements (e.g., `mutableListOf`, `mutableMapOf`, `mutableSetOf`).
Example:
fun main() {
val readOnlyList = listOf("Kotlin", "Java")
// readOnlyList.add("Python") // Compilation error
val mutableList = mutableListOf("Kotlin", "Java")
mutableList.add("Python") // Allowed
println(mutableList) // Output: [Kotlin, Java, Python]
}
9. How does the `with` function work in Kotlin, and how is it different from `apply`?
The `with` function in Kotlin is a scope function used to perform operations on an object within a block of code. Unlike `apply`, it does not return the object but instead returns the result of the block.
The primary difference between `with` and `apply` is that `apply` always returns the object itself, while `with` is used when you need a specific result from the block.
Example:
data class User(var name: String, var age: Int)
fun main() {
val user = User("Alice", 30)
val result = with(user) {
println("Name: $name")
println("Age: $age")
age + 10 // Returns the result of the block
}
println("Result: $result") // Output: Result: 40
}
10. What are higher-order functions with lambdas as receivers in Kotlin?
Higher-order functions with lambdas as receivers in Kotlin allow you to invoke a lambda on a specific receiver object. This means that inside the lambda, the receiver object is available as `this`, enabling more concise and readable code.
These are commonly used in scope functions like `apply` and `run`.
Example:
fun buildString(action: StringBuilder.() -> Unit): String {
val stringBuilder = StringBuilder()
stringBuilder.action() // Invokes the lambda on the receiver
return stringBuilder.toString()
}
fun main() {
val result = buildString {
append("Hello, ")
append("Kotlin!")
}
println(result) // Output: Hello, Kotlin!
}
11. What are destructuring declarations in Kotlin, and how are they used?
Destructuring declarations in Kotlin allow you to unpack an object into multiple variables at once. They work by calling the component functions (`component1()`, `component2()`, etc.) defined in the object's class.
Destructuring is commonly used with data classes and collections.
Example - Data Classes:
data class User(val name: String, val age: Int)
fun main() {
val user = User("Alice", 30)
val (name, age) = user
println("Name: $name, Age: $age") // Output: Name: Alice, Age: 30
}
Example - Collections:
fun main() {
val map = mapOf(1 to "Kotlin", 2 to "Java")
for ((key, value) in map) {
println("Key: $key, Value: $value")
}
}
12. What are inline properties in Kotlin, and how are they defined?
Inline properties in Kotlin allow you to use inline accessors (`get` and `set`) to optimize property access by avoiding unnecessary function calls. They are particularly useful for computed properties.
By marking accessors with the `inline` keyword, the compiler replaces the call with the actual code at the call site.
Example:
val pi: Double
inline get() = 3.14159
fun main() {
println(pi) // Output: 3.14159
}
13. How do `reified` and `inline` work together in Kotlin generics?
In Kotlin, the `inline` keyword allows functions to be inlined at the call site, while `reified` enables access to the actual type parameters of generic functions. They work together to overcome type erasure and provide type information at runtime.
A function with a `reified` type parameter must also be marked as `inline`.
Example:
inline fun filterOfType(list: List): List {
return list.filterIsInstance()
}
fun main() {
val items = listOf("Kotlin", 42, "Java", 3.14)
val strings = filterOfType(items)
println(strings) // Output: [Kotlin, Java]
}
14. What are the key differences between `run`, `let`, and `apply` in Kotlin?
In Kotlin, `run`, `let`, and `apply` are scope functions with different use cases:
- `run`: Executes a block of code on an object and returns the result of the block. Often used for transformations.
- `let`: Executes a block of code with the object as an argument (`it`) and returns the block's result. Useful for chaining or null checks.
- `apply`: Executes a block of code on an object and always returns the object itself. Ideal for initializing objects.
Example:
data class User(var name: String, var age: Int)
fun main() {
val user = User("Alice", 30)
// run
val ageNextYear = user.run {
age + 1
}
println("Age next year: $ageNextYear") // Output: Age next year: 31
// let
user.let {
println("User's name: ${it.name}") // Output: User's name: Alice
}
// apply
user.apply {
name = "Bob"
age = 35
}
println(user) // Output: User(name=Bob, age=35)
}
15. What is the difference between `takeIf` and `takeUnless` in Kotlin?
`takeIf` and `takeUnless` are utility functions in Kotlin used to filter or validate objects based on a condition:
- `takeIf`: Returns the object if the condition is true, otherwise returns `null`.
- `takeUnless`: Returns the object if the condition is false, otherwise returns `null`.
Example:
fun main() {
val number = 10
val validNumber = number.takeIf { it > 5 }
println(validNumber) // Output: 10
val invalidNumber = number.takeUnless { it > 5 }
println(invalidNumber) // Output: null
}
16. How does Kotlin's `when` expression work with multiple conditions?
The `when` expression in Kotlin can handle multiple conditions for a single branch. This is achieved using commas to separate the conditions. It acts as a more powerful replacement for a `switch` statement in Java.
Example:
fun describeNumber(number: Int): String {
return when (number) {
1, 3, 5, 7, 9 -> "Odd number"
2, 4, 6, 8, 10 -> "Even number"
else -> "Unknown number"
}
}
fun main() {
println(describeNumber(3)) // Output: Odd number
println(describeNumber(8)) // Output: Even number
}
17. What are type projections in Kotlin generics, and why are they useful?
Type projections in Kotlin are used to specify restrictions on generic types, ensuring type safety while allowing flexibility. They are especially useful for handling variance in generic types.
- `out`: Denotes a producer type (covariance).
- `in`: Denotes a consumer type (contravariance).
Example - Covariance with `out`:
fun printList(list: List) {
for (item in list) {
println(item)
}
}
fun main() {
val intList = listOf(1, 2, 3)
val doubleList = listOf(1.1, 2.2, 3.3)
printList(intList) // Output: 1, 2, 3
printList(doubleList) // Output: 1.1, 2.2, 3.3
}
18. What is the purpose of Kotlin's `by lazy` initialization?
The `by lazy` initialization in Kotlin is used to initialize a property lazily, i.e., only when it is accessed for the first time. This improves performance and ensures that resources are not wasted on unused properties.
Lazy initialization is thread-safe by default and can be customized using a lazy delegate.
Example:
class Example {
val lazyValue: String by lazy {
println("Initializing lazyValue")
"Hello, Kotlin"
}
}
fun main() {
val example = Example()
println("Before accessing lazyValue")
println(example.lazyValue) // Output: Initializing lazyValue, Hello, Kotlin
}
19. How does Kotlin support function references and method references?
Kotlin allows referencing functions and methods directly using the `::` operator. This feature can be used to pass a function as a parameter or store it in a variable.
Function references simplify higher-order functions and functional programming.
Example:
fun greet(name: String) {
println("Hello, $name!")
}
fun main() {
val greeter: (String) -> Unit = ::greet
greeter("Kotlin") // Output: Hello, Kotlin!
}
20. What is the difference between `lazy` and `lateinit` properties in Kotlin?
Both `lazy` and `lateinit` in Kotlin are used for deferred initialization, but they serve different purposes:
- `lazy`: Used with `val` properties. It is initialized only once on the first access and is thread-safe by default.
- `lateinit`: Used with `var` properties. It allows a non-null property to be initialized later. Accessing an uninitialized `lateinit` property throws an exception.
Example:
class Example {
lateinit var name: String
val lazyValue: String by lazy {
println("Initializing lazyValue")
"Hello, Kotlin"
}
}
fun main() {
val example = Example()
// lateinit
example.name = "Alice"
println(example.name) // Output: Alice
// lazy
println(example.lazyValue) // Output: Initializing lazyValue, Hello, Kotlin
}