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)