Kotlin OOP Interview Questions — 20 Questions on Classes, Inheritance, and Design
Object-Oriented Programming questions are a staple in every Android interview. Interviewers use them to gauge how well you understand Kotlin’s class system, inheritance model, and design decisions. These questions go beyond textbook definitions — they test whether you can explain trade-offs, pick the right abstraction, and apply OOP concepts to real Android problems. This guide covers the OOP interview questions you’re most likely to face.
Classes & Objects
1. What is the difference between a class and an object in Kotlin?
// class — blueprint, can create MULTIPLE instances
class User(val name: String)
val alice = User("Alice")
val bob = User("Bob") // two separate instances
// object — SINGLETON, exactly one instance
object Analytics {
fun track(event: String) { /* ... */ }
}
Analytics.track("click") // no instantiation — single instance
// object is initialised lazily on first access and is thread-safe
// It's the Kotlin replacement for Java's static singleton pattern
2. What is a companion object? How is it different from a regular object?
// companion object lives INSIDE a class — accessed via the class name
class User(val name: String) {
companion object {
const val MAX_NAME_LENGTH = 50
fun create(name: String) = User(name.take(MAX_NAME_LENGTH))
}
}
User.MAX_NAME_LENGTH // like static access
User.create("Alice") // factory method
// Regular object is standalone
object UserValidator {
fun isValid(name: String) = name.length <= 50
}
UserValidator.isValid("Alice")
// Key differences:
// companion object: tied to a class, accessed via class name, can access private members
// regular object: standalone singleton, independent of any class
// companion object can implement interfaces and be used as factory
3. What are primary and secondary constructors?
// Primary constructor — in the class header, concise
class User(val name: String, val age: Int = 0)
// Secondary constructor — in the class body, must delegate to primary
class User(val name: String) {
var email: String = ""
constructor(name: String, email: String) : this(name) {
this.email = email
}
}
// init block runs after primary constructor
class User(val name: String) {
init {
require(name.isNotBlank()) { "Name cannot be blank" }
}
}
// Best practice: prefer primary constructor + default values over secondary
// Use secondary constructors only for Java interop or complex initialisation
4. What is the init block? When does it run?
class User(val name: String) {
// Property initializers and init blocks run in ORDER OF APPEARANCE
val upperName = name.uppercase() // 1st — property initializer
init {
println("Init block: $name") // 2nd — init block
require(name.isNotBlank())
}
val nameLength = name.length // 3rd — property initializer
init {
println("Second init: $nameLength") // 4th — second init block
}
}
// Multiple init blocks are allowed — they run top to bottom
// All init blocks and property initializers run BEFORE secondary constructors
Data Classes
5. What does a data class generate? What are the rules?
data class User(val name: String, val age: Int)
// Auto-generated methods:
// equals() — compares all properties in primary constructor
// hashCode() — based on all properties in primary constructor
// toString() — "User(name=Alice, age=25)"
// copy() — create modified copy: user.copy(age = 26)
// componentN() — destructuring: val (name, age) = user
// Rules:
// - Must have at least one val/var in primary constructor
// - Cannot be abstract, open, sealed, or inner
// - ONLY properties in primary constructor are used in generated methods
data class User(val name: String, val age: Int) {
var loginCount = 0 // NOT included in equals/hashCode/toString/copy!
}
val a = User("Alice", 25).apply { loginCount = 5 }
val b = User("Alice", 25).apply { loginCount = 10 }
println(a == b) // true! loginCount is ignored
6. When should you use a data class vs a regular class?
// Use data class when:
// - The class is primarily a DATA HOLDER
// - You need equals/hashCode for comparisons (DiffUtil, collections)
// - You need copy() for immutable updates
// - Examples: API responses, DB entities, UI state, DTOs
data class Article(val id: String, val title: String, val body: String)
data class UiState(val isLoading: Boolean, val data: List<Item>)
// Use regular class when:
// - The class has BEHAVIOUR, not just data
// - Identity matters more than content (two ViewModels aren't "equal")
// - You need inheritance (data classes can't be open)
// - Examples: ViewModel, Repository, Service, Manager
class ArticleRepository(private val api: Api, private val dao: Dao) {
suspend fun getArticles(): List<Article> { /* ... */ }
}
Sealed Classes & Enums
7. What is a sealed class/interface? Why use it?
// Sealed = restricted type hierarchy — compiler knows ALL subtypes
sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T>
data class Error(val exception: Throwable) : Result<Nothing>
data object Loading : Result<Nothing>
}
// when is EXHAUSTIVE — compiler forces you to handle all cases
fun <T> handleResult(result: Result<T>) = when (result) {
is Result.Success -> showData(result.data)
is Result.Error -> showError(result.exception)
is Result.Loading -> showLoading()
// no else needed — all cases covered
}
// If you add a new subtype, every when expression breaks at compile time
// This prevents bugs from unhandled cases
// sealed interface vs sealed class:
// sealed interface: subtypes can extend other classes too (more flexible)
// sealed class: subtypes can inherit state from the sealed class
8. What is the difference between sealed class and enum class?
// Enum — each value is a SINGLE INSTANCE with fixed state
enum class Direction { NORTH, SOUTH, EAST, WEST }
// Direction.NORTH is always the same object
// Sealed — each subtype is a CLASS that can hold different state
sealed interface PaymentResult {
data class Success(val transactionId: String, val amount: Double) : PaymentResult
data class Failed(val errorCode: Int, val message: String) : PaymentResult
data object Cancelled : PaymentResult
}
// Key differences:
// Enum: fixed number of INSTANCES, each instance is identical in structure
// Sealed: fixed number of TYPES, each type can have different properties
// Enum: good for simple constants (Color, Direction, Status)
// Sealed: good for state with data (UiState, Result, Event)
Inheritance & Interfaces
9. How does inheritance work in Kotlin?
// Classes are FINAL by default — you must use "open" to allow inheritance
open class Animal(val name: String) {
open fun sound(): String = "..." // must be open to override
fun describe() = "$name says ${sound()}" // not open — can't override
}
class Dog(name: String) : Animal(name) {
override fun sound() = "Woof!"
}
class Cat(name: String) : Animal(name) {
override fun sound() = "Meow!"
}
val dog = Dog("Rex")
println(dog.describe()) // "Rex says Woof!"
// Kotlin's "final by default" philosophy prevents accidental inheritance
// In Java, you must remember to add "final" — in Kotlin, you must add "open"
10. What is the difference between abstract class and interface?
// Interface — no constructor, no state, multiple allowed
interface Clickable {
fun click() // abstract
fun showRipple() { println("Ripple") } // default implementation
}
interface Loggable {
fun log(message: String) { println(message) }
}
// Abstract class — has constructor, can hold state, single inheritance
abstract class BaseViewModel(val tag: String) {
abstract fun loadData()
fun log(msg: String) { Log.d(tag, msg) } // uses constructor state
}
// A class can implement MULTIPLE interfaces but extend only ONE class
class MyViewModel : BaseViewModel("MyVM"), Clickable, Loggable {
override fun loadData() { /* ... */ }
override fun click() { /* ... */ }
}
// Use interface when: defining a contract/capability
// Use abstract class when: sharing state + behaviour among related classes
11. What happens when two interfaces have the same default method?
interface A {
fun greet() { println("Hello from A") }
}
interface B {
fun greet() { println("Hello from B") }
}
// Class must OVERRIDE to resolve the conflict
class MyClass : A, B {
override fun greet() {
super<A>.greet() // call A's version
super<B>.greet() // call B's version
println("Hello from MyClass")
}
}
MyClass().greet()
// "Hello from A"
// "Hello from B"
// "Hello from MyClass"
// If you don't override, the compiler reports an error
// You can call either, both, or neither super implementation
Special Classes
12. What are inner classes vs nested classes?
class Outer {
val outerValue = 10
// Nested class (default) — does NOT have reference to Outer
class Nested {
// can't access outerValue
fun demo() = "Nested class"
}
// Inner class — HAS reference to Outer
inner class Inner {
fun demo() = "Inner class, outerValue = $outerValue"
}
}
val nested = Outer.Nested() // no Outer instance needed
val inner = Outer().Inner() // needs Outer instance
// Kotlin vs Java:
// Kotlin nested class = Java static nested class (no outer reference)
// Kotlin inner class = Java inner class (has outer reference)
// Kotlin's default (no outer reference) prevents accidental memory leaks
13. What is a value class (inline class)?
// value class wraps a single value without runtime allocation overhead
@JvmInline
value class UserId(val id: String)
@JvmInline
value class Email(val value: String) {
init { require(value.contains("@")) { "Invalid email" } }
}
// Type-safe without performance cost
fun getUser(userId: UserId): User { /* ... */ }
fun sendEmail(email: Email) { /* ... */ }
// These won't compile — prevents mixing up String parameters:
// getUser(Email("test@test.com")) // ❌ type mismatch
// sendEmail(UserId("123")) // ❌ type mismatch
// At runtime, UserId is unwrapped to just a String (no object allocation)
// Rules: exactly one val property, cannot extend classes
14. What is delegation in Kotlin?
// Class delegation — delegate interface implementation to another object
interface Logger {
fun log(message: String)
}
class ConsoleLogger : Logger {
override fun log(message: String) = println("LOG: $message")
}
// "by" delegates all Logger methods to consoleLogger
class Repository(private val logger: Logger) : Logger by logger {
fun fetchData() {
log("Fetching data...") // delegated to ConsoleLogger
}
}
// Property delegation — delegate getter/setter to another object
class UserPreferences(private val prefs: SharedPreferences) {
var username: String by prefs.string("username", "")
var darkMode: Boolean by prefs.boolean("dark_mode", false)
}
// Built-in delegates: lazy, observable, vetoable, map
val expensiveObject by lazy { ExpensiveObject() } // created on first access
Generics & Variance
15. What is the difference between out and in in generics?
// out (covariance) — type can only be PRODUCED (returned), not consumed
// Box<Dog> is a subtype of Box<Animal>
class Producer<out T>(private val item: T) {
fun get(): T = item // ✅ produce T
// fun set(t: T) { } // ❌ can't consume T
}
val dogProducer: Producer<Dog> = Producer(Dog())
val animalProducer: Producer<Animal> = dogProducer // ✅ covariant
// in (contravariance) — type can only be CONSUMED (accepted), not produced
// Comparator<Animal> is a subtype of Comparator<Dog>
class Consumer<in T> {
fun process(item: T) { } // ✅ consume T
// fun get(): T { } // ❌ can't produce T
}
// Remember: out = produce = read, in = consume = write
// List<out E> — read-only, covariant
// Comparable<in T> — accepts T, contravariant
16. What are reified type parameters?
// Generic types are ERASED at runtime — you can't check "is T"
// reified preserves the type in inline functions
inline fun <reified T> isType(value: Any): Boolean {
return value is T // ✅ works with reified
}
println(isType<String>("Hello")) // true
println(isType<Int>("Hello")) // false
// Practical Android uses:
inline fun <reified T : Activity> Context.startActivity() {
startActivity(Intent(this, T::class.java))
}
startActivity<DetailActivity>() // clean API
inline fun <reified T> Gson.fromJson(json: String): T {
return fromJson(json, T::class.java)
}
val user = gson.fromJson<User>(jsonString) // no class parameter needed
Kotlin vs Java OOP
17. What are the key OOP differences between Kotlin and Java?
// 1. Classes are FINAL by default (Java: open by default)
class Foo // final in Kotlin
open class Bar // must explicitly mark open
// 2. No static members — use companion object or top-level functions
companion object { const val TAG = "MyClass" } // Kotlin
// static final String TAG = "MyClass"; // Java
// 3. Data classes auto-generate equals/hashCode/toString/copy
data class User(val name: String) // one line
// Java: 50+ lines for the same with equals, hashCode, toString
// 4. Sealed classes for restricted hierarchies
sealed interface State // compiler-enforced exhaustive when
// 5. Default visibility is PUBLIC (Java: package-private)
// 6. Has "internal" visibility (module-level, no Java equivalent)
// 7. Properties instead of fields + getters/setters
// 8. Smart casts after type checks
// 9. Extension functions
// 10. Null safety built into the type system
18. Why are classes final by default in Kotlin?
// Kotlin follows the "design for inheritance or prohibit it" principle
// (from Effective Java by Joshua Bloch)
// Problems with open-by-default (Java):
// - Subclasses can break parent class invariants
// - Base class changes can accidentally break subclasses ("fragile base class")
// - Hard to reason about a class if anyone can override anything
// Kotlin's approach:
// - Classes and methods are final by default
// - You must EXPLICITLY opt in with "open"
// - This forces you to DESIGN for extension
// - If a class is open, you've thought about it
open class Animal {
open fun sound() = "..." // explicitly designed to be overridden
fun breathe() { } // NOT open — subclasses can't change this
}
Design Patterns
19. How do you implement the Singleton pattern in Kotlin?
// Simply use object declaration — it's thread-safe and lazy by default
object DatabaseHelper {
private val connection = createConnection()
fun query(sql: String): List<Row> { /* ... */ }
}
// No double-checked locking, no synchronized blocks
// The JVM guarantees thread-safe initialisation of object declarations
// For singletons that need parameters (e.g., Application context):
class AppDatabase private constructor(context: Context) {
companion object {
@Volatile private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: AppDatabase(context.applicationContext).also {
INSTANCE = it
}
}
}
}
}
20. How do you implement the Builder pattern in Kotlin?
// In Java, Builder is common because constructors with many parameters are ugly
// In Kotlin, you often DON'T NEED a Builder — use default + named arguments:
data class Config(
val host: String = "localhost",
val port: Int = 8080,
val ssl: Boolean = false,
val timeout: Int = 30_000
)
val config = Config(
host = "api.example.com",
ssl = true
// port and timeout use defaults
)
// When you DO need a Builder (DSL-style configuration):
class HttpClient private constructor(
val baseUrl: String,
val timeout: Int,
val headers: Map<String, String>
) {
class Builder {
var baseUrl: String = ""
var timeout: Int = 30_000
private val headers = mutableMapOf<String, String>()
fun header(key: String, value: String) = apply { headers[key] = value }
fun build() = HttpClient(baseUrl, timeout, headers)
}
}
val client = HttpClient.Builder().apply {
baseUrl = "https://api.example.com"
timeout = 10_000
header("Authorization", "Bearer token")
}.build()
Summary
- Know the difference between class, object, companion object, data class, sealed class, enum class — and when to use each
- Understand data class rules — only primary constructor properties in generated methods, body properties are excluded
- Know sealed vs enum — sealed for types with different data, enum for fixed instances
- Understand inheritance in Kotlin — final by default, open required, single class + multiple interfaces
- Know abstract class vs interface — state + constructor vs contract + multiple inheritance
- Understand visibility modifiers — public default, internal for module, protected for subclasses
- Know out (produce) and in (consume) for generics variance
- Know reified for runtime type access in inline functions
- Understand delegation — both class delegation (by) and property delegation (lazy, observable)
- Know why Kotlin is final by default and how it prevents the fragile base class problem
- Know when to use named arguments over Builder pattern — Kotlin often doesn’t need Builder
OOP questions reveal how you think about code structure and design. Interviewers aren’t looking for memorised definitions — they want to hear you reason about trade-offs. Why sealed over enum? Why interface over abstract class? Why data class instead of regular class? Answer the “why” confidently, and the rest follows.
Happy coding!
Comments (0)
Sign in to leave a comment.
No comments yet. Be the first to share your thoughts.