Questions for Advanced Object Oriented Programming
1. What is the purpose of delegation in Kotlin, and how does it differ from inheritance?
Delegation in Kotlin allows an object to delegate the implementation of an interface or behavior to another object. Unlike inheritance, delegation enables composition over inheritance, promoting more flexible and reusable designs.
Example - Using `by` for Delegation:
interface Printer {
fun print()
}
class DefaultPrinter : Printer {
override fun print() {
println("Printing...")
}
}
class AdvancedPrinter(private val name: String) : Printer by DefaultPrinter() {
fun printWithDetails() {
println("Printer: $name")
print()
}
}
fun main() {
val printer = AdvancedPrinter("HP Printer")
printer.printWithDetails()
}
// Output:
// Printer: HP Printer
// Printing...
2. What are inner classes in Kotlin, and how do they differ from nested classes?
In Kotlin, a nested class is static by default, meaning it does not hold a reference to its enclosing class. An inner class, on the other hand, is explicitly marked with the `inner` keyword and can access members of its enclosing class.
Example - Difference Between Nested and Inner Classes:
class Outer {
private val outerValue = "Outer Value"
class Nested {
fun print() = println("Accessing nested class")
}
inner class Inner {
fun print() = println("Accessing inner class with $outerValue")
}
}
fun main() {
Outer.Nested().print() // Output: Accessing nested class
Outer().Inner().print() // Output: Accessing inner class with Outer Value
}
3. How does Kotlin implement the multiple inheritance of behavior through interfaces?
Kotlin allows multiple inheritance of behavior through interfaces, and it provides a way to resolve conflicts when multiple interfaces have methods with the same name. This is done using the `super` keyword with the interface name.
Example - Resolving Multiple Interface Inheritance:
interface A {
fun display() = println("From A")
}
interface B {
fun display() = println("From B")
}
class C : A, B {
override fun display() {
super.display() // Explicitly call A's implementation
super.display() // Explicitly call B's implementation
}
}
fun main() {
C().display()
}
// Output:
// From A
// From B
4. What is an anonymous inner class in Kotlin, and how is it used?
An anonymous inner class in Kotlin is an instance of a class that is created without explicitly naming the class. It is typically used to implement interfaces or abstract classes on-the-fly.
Example - Using an Anonymous Inner Class:
interface ClickListener {
fun onClick()
}
fun setClickListener(listener: ClickListener) {
listener.onClick()
}
fun main() {
setClickListener(object : ClickListener {
override fun onClick() {
println("Button clicked!")
}
})
}
// Output:
// Button clicked!
5. What is property delegation, and how does it simplify property handling in Kotlin?
Property delegation in Kotlin allows the logic for property access and modification to be delegated to another object using the `by` keyword. This is particularly useful for reusable patterns like lazy initialization or observable properties.
Example - Using `lazy` for Property Delegation:
class Example {
val lazyValue: String by lazy {
println("Computed!")
"Hello, Kotlin!"
}
}
fun main() {
val example = Example()
println("Before accessing lazyValue")
println("Value: ${example.lazyValue}")
println("Accessing lazyValue again: ${example.lazyValue}")
}
// Output:
// Before accessing lazyValue
// Computed!
// Value: Hello, Kotlin!
// Accessing lazyValue again: Hello, Kotlin!
6. What are companion objects in Kotlin, and how do they compare to static members in Java?
Companion objects in Kotlin allow you to define members that belong to a class rather than an instance of the class. Unlike Java’s `static` keyword, companion objects are first-class objects that can implement interfaces or contain properties and functions.
Example - Using Companion Objects:
class Utility {
companion object {
const val VERSION = "1.0"
fun printVersion() {
println("Version: $VERSION")
}
}
}
fun main() {
Utility.printVersion() // Accessed like a static method
}
// Output:
// Version: 1.0
7. How do data classes handle inheritance in Kotlin, and what are the limitations?
Data classes in Kotlin are primarily designed for immutable objects and cannot be open for inheritance by default. If you need a data class to extend another class, the base class must be open, abstract, or an interface.
Example - Data Class Extending an Abstract Class:
abstract class Shape(val name: String)
data class Circle(val radius: Double) : Shape("Circle")
fun main() {
val circle = Circle(5.0)
println("Shape: ${circle.name}, Radius: ${circle.radius}")
}
// Output:
// Shape: Circle, Radius: 5.0
8. What is object expression in Kotlin, and how does it differ from object declaration?
An object expression in Kotlin creates an anonymous object at runtime, while an object declaration creates a singleton at compile time. Object expressions are typically used for one-off, temporary objects.
Example - Object Expression vs Object Declaration:
interface Greeter {
fun greet()
}
fun main() {
// Object expression: Temporary object
val greeter = object : Greeter {
override fun greet() {
println("Hello from anonymous object!")
}
}
greeter.greet()
// Object declaration: Singleton
object SingletonGreeter : Greeter {
override fun greet() {
println("Hello from singleton!")
}
}
SingletonGreeter.greet()
}
// Output:
// Hello from anonymous object!
// Hello from singleton!
9. What are sealed interfaces in Kotlin, and how are they used?
Sealed interfaces in Kotlin restrict which classes or interfaces can implement them. This ensures a fixed and known set of subtypes at compile time, making them useful for modeling hierarchies like state or event systems.
Example - Using a Sealed Interface:
sealed interface Result
class Success(val data: String) : Result
class Failure(val error: String) : Result
fun handleResult(result: Result) {
when (result) {
is Success -> println("Success: ${result.data}")
is Failure -> println("Failure: ${result.error}")
}
}
fun main() {
handleResult(Success("Operation completed"))
handleResult(Failure("Something went wrong"))
}
// Output:
// Success: Operation completed
// Failure: Something went wrong
10. What is the difference between inline classes and value classes in Kotlin?
Inline classes, introduced in Kotlin 1.3, are now called value classes in Kotlin 1.5. They are used to create lightweight, type-safe wrappers around single values without adding runtime overhead.
Example - Using Value Classes:
@JvmInline
value class Username(val name: String)
fun greetUser(username: Username) {
println("Hello, ${username.name}")
}
fun main() {
val user = Username("JohnDoe")
greetUser(user)
}
// Output:
// Hello, JohnDoe
11. How does Kotlin handle covariance and contravariance in generics?
Kotlin supports variance annotations (`out` and `in`) to handle covariance and contravariance in generics. These keywords ensure type safety when using generic types in different contexts.
- Covariance (`out`): Allows a generic type to be a subtype of another generic type when used as a producer.
- Contravariance (`in`): Allows a generic type to be a supertype of another generic type when used as a consumer.
Example - Covariance and Contravariance:
open class Animal
class Dog : Animal()
class Box(val item: T) // Covariant: Only used as a producer
class Action { // Contravariant: Only used as a consumer
fun perform(action: T) {
println("Performing 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
}
12. What is the difference between a class initializer block and a secondary constructor in Kotlin?
In Kotlin, the primary constructor can include an initializer block (`init`) for logic that runs during object creation. Secondary constructors are additional constructors that delegate to the primary constructor or provide alternative initialization paths.
Example - Initializer Block vs Secondary Constructor:
class Person(val name: String) {
init {
println("Primary constructor: $name")
}
constructor(name: String, age: Int) : this(name) {
println("Secondary constructor: $name is $age years old")
}
}
fun main() {
val person1 = Person("Alice")
val person2 = Person("Bob", 30)
}
// Output:
// Primary constructor: Alice
// Primary constructor: Bob
// Secondary constructor: Bob is 30 years old
13. What is an open class in Kotlin, and how does it differ from a sealed class?
- Open class: A class that can be inherited by other classes. By default, classes in Kotlin are `final` (cannot be inherited).
- Sealed class: A class with a restricted hierarchy, where all subclasses must be declared in the same file. It is typically used for modeling a fixed set of types.
Example - Open vs Sealed Classes:
open class Animal {
open fun sound() = println("Animal sound")
}
class Dog : Animal() {
override fun sound() = println("Bark")
}
sealed class Shape {
class Circle(val radius: Double) : Shape()
class Square(val side: Double) : Shape()
}
fun main() {
val dog = Dog()
dog.sound() // Output: Bark
val shape: Shape = Shape.Circle(5.0)
when (shape) {
is Shape.Circle -> println("Circle with radius ${shape.radius}")
is Shape.Square -> println("Square with side ${shape.side}")
}
}
14. How does Kotlin's `inline` keyword work for classes, and how does it differ from inline functions?
The `inline` keyword is used differently for functions and classes in Kotlin:
- Inline functions: Reduce the overhead of function calls by inlining the function body at the call site.
- Inline classes (value classes): Provide a lightweight wrapper around a single value without additional object overhead.
Example - Inline Functions vs Inline Classes:
@JvmInline
value class ID(val id: String) // Inline class
inline fun log(message: String) { // Inline function
println("Log: $message")
}
fun main() {
val userId = ID("12345")
println("User ID: ${userId.id}")
log("This is an inline function")
}
// Output:
// User ID: 12345
// Log: This is an inline function
15. What is reflection in Kotlin, and how can it be used to inspect or modify objects at runtime?
Reflection in Kotlin allows you to inspect and modify objects at runtime by accessing their properties, methods, or constructors. The `kotlin.reflect` package provides APIs for this purpose.
Example - Using Reflection to Access Properties:
import kotlin.reflect.full.*
data class User(val id: Int, val name: String)
fun main() {
val user = User(1, "Alice")
val kClass = user::class
println("Class name: ${kClass.simpleName}")
println("Properties:")
kClass.memberProperties.forEach { println(it.name) }
val idProperty = kClass.memberProperties.find { it.name == "id" }
println("ID value: ${idProperty?.get(user)}")
}
// Output:
// Class name: User
// Properties:
// id
// name
// ID value: 1
16. How does Kotlin handle type erasure with generics, and how can you access type information at runtime?
Like Java, Kotlin's generics are erased at runtime due to type erasure. This means that the type arguments are removed during compilation. To retain type information, Kotlin provides `reified` type parameters with `inline` functions.
Example - Using `reified` to Retain Type Information:
inline fun isInstance(obj: Any): Boolean {
return obj is T
}
fun main() {
println(isInstance("Hello")) // Output: true
println(isInstance("Hello")) // Output: false
}
17. What are destructuring declarations in Kotlin, and how are they related to data classes?
Destructuring declarations allow you to unpack the properties of an object into separate variables. They are commonly used with data classes because data classes automatically generate `componentN` functions for their properties.
Example - Destructuring with a Data Class:
data class User(val id: Int, val name: String)
fun main() {
val user = User(1, "Alice")
val (id, name) = user
println("ID: $id")
println("Name: $name")
}
// Output:
// ID: 1
// Name: Alice
18. How do sealed classes and enums differ, and when should you use each?
Both sealed classes and enums are used to model restricted sets of values, but they differ in flexibility:
- Sealed classes: Allow subclasses with different properties and behavior. They are ideal for modeling complex hierarchies or state machines.
- Enums: Represent a fixed set of constants with shared behavior. They are simpler and more lightweight than sealed classes.
Example - Sealed Class vs Enum:
sealed class Shape {
class Circle(val radius: Double) : Shape()
class Square(val side: Double) : Shape()
}
enum class Direction {
NORTH, SOUTH, EAST, WEST
}
fun describeShape(shape: Shape) = when (shape) {
is Shape.Circle -> "Circle with radius ${shape.radius}"
is Shape.Square -> "Square with side ${shape.side}"
}
fun describeDirection(direction: Direction) = when (direction) {
Direction.NORTH -> "Heading North"
Direction.SOUTH -> "Heading South"
Direction.EAST -> "Heading East"
Direction.WEST -> "Heading West"
}
fun main() {
println(describeShape(Shape.Circle(5.0))) // Output: Circle with radius 5.0
println(describeDirection(Direction.NORTH)) // Output: Heading North
}