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!