OOP & Android Interview Questions and Answers

OOP Concepts

1. How Can a Class Like Animal Be Initialized with Cat?

In Kotlin, when a class Cat inherits from Animal, it forms an “is-a” relationship — meaning a Cat is also an Animal. Because of this relationship, an Animal reference can hold a Cat object. This concept is known as upcasting.


open class Animal {
    open fun sound() = println("Some generic animal sound")
}

class Cat : Animal() {
    override fun sound() = println("Meow")
}

fun main() {
    val animal: Animal = Cat()  // ✅ Parent reference holds Child object
    animal.sound()              // Output: Meow
}

2. Why Does This Work?

Since Cat extends Animal, it inherits all of Animal’s properties and methods. Therefore, the compiler allows assigning a Cat instance to an Animal variable. This lets us treat different subclasses (like Cat, Dog, Cow) as the same base type Animal.

3. What Happens at Runtime?

When animal.sound() is called, the JVM checks the actual object type stored in memory — in this case, Cat — and executes the overridden sound() function from the Cat class instead of Animal. This behavior is called runtime polymorphism.


val animal: Animal = Cat()
animal.sound() // Output: Meow

4. Why Is It Useful?

This allows writing reusable and flexible code. For example, you can pass different Animal types to the same function:


fun makeSound(animal: Animal) {
    animal.sound()
}

makeSound(Cat()) // Meow
makeSound(Dog()) // Woof

Thus, initializing Animal with Cat() is perfectly valid due to inheritance and polymorphism, enabling cleaner and extensible object-oriented design.

Can an Animal Reference Access Properties or Functions of Cat?

No ❌ — an Animal reference cannot directly access properties or functions that are only defined in Cat. Even though the actual object in memory is a Cat, the reference type (Animal) limits access to only what’s declared in Animal.


open class Animal {
    open fun sound() = println("Some sound")
}

class Cat : Animal() {
    override fun sound() = println("Meow")
    fun scratch() = println("Cat is scratching!") // Unique to Cat
}

fun main() {
    val animal: Animal = Cat()

    animal.sound()   // ✅ Allowed → Output: Meow
    // animal.scratch() ❌ Not allowed — scratch() not in Animal

    // To access Cat-specific methods:
    if (animal is Cat) {
        animal.scratch() // ✅ Allowed after type check
    }
}

The reason: Kotlin (and Java) use the reference type to decide what members you can access at compile time. The object type (actual instance) decides which implementation runs at runtime.

Summary

Can I Declare a Static Variable Without Initialization?

🟢 In Kotlin

Kotlin does not have true static variables. Instead, it uses companion objects to replicate static-like behavior. However, just like Java, you cannot leave a variable uninitialized unless you use lateinit for mutable non-null types.


// ❌ Error – must be initialized or declared as lateinit
class MyClass {
    companion object {
        var name: String
    }
}
✅ Correct Kotlin Examples

// Option 1: Initialize immediately
class MyClass {
    companion object {
        var name: String = "Default"
    }
}

// Option 2: Use lateinit (assign later)
class MyClass {
    companion object {
        lateinit var name: String
    }
}

fun main() {
    MyClass.name = "Talha"
    println(MyClass.name) // Output: Talha
}

// Option 3: Use const val for compile-time constants
class MyClass {
    companion object {
        const val VERSION = 1
    }
}

Notes:


🟣 In Java

Java supports true static variables. You can declare a static variable without initializing it — but you must assign it a value before using it. If not initialized explicitly, it takes the default value for its type.


public class MyClass {
    static String name; // ✅ Allowed, default value = null
    static int count;   // ✅ Allowed, default value = 0

    public static void main(String[] args) {
        System.out.println(name);  // null
        System.out.println(count); // 0

        name = "Talha";
        count = 10;
        System.out.println(name);  // Talha
        System.out.println(count); // 10
    }
}
🧠 Key Difference

✅ Summary

Feature Kotlin Java
Static keyword No — uses companion object Yes — uses static keyword
Default value if uninitialized ❌ Not allowed (must initialize or use lateinit) ✅ Allowed (default values like 0 or null)
Constant declaration const val static final
Access syntax MyClass.name MyClass.name

3. What Happens When a Class Is Declared final?

A final class cannot be inherited or extended.


final class Animal
class Cat : Animal() // ❌ Error
  

4. What Is static in Java? Can Static Functions Access Class Variables?

The static keyword means a member belongs to the class rather than an instance. Static functions cannot access non-static variables since they don’t belong to an instance.


class Demo {
    static int a = 10;
    int b = 20;

    static void show() {
        System.out.println(a); // ✅ OK
        System.out.println(b); // ❌ Error
    }
}
  

5. Example: Animal–Cat Polymorphism


open class Animal {
    open fun sound() = println("Some sound")
}

class Cat : Animal() {
    override fun sound() = println("Meow")
}

val obj: Animal = Cat()
obj.sound() // Output: Meow
  

6. What Is Method Overloading?

Method overloading means having multiple methods with the same name but different parameters. It is resolved at compile-time.


fun add(a: Int, b: Int) = a + b
fun add(a: Double, b: Double) = a + b
  

Composition vs Aggregation in OOP

1. What Is Composition?

Composition represents a “strong has-a” relationship between two classes. It means one class owns another class — if the parent object is destroyed, the child object also ceases to exist. This is also called a whole–part relationship.


class Engine {
    fun start() = println("Engine started")
}

class Car {
    private val engine = Engine() // Composition: Car owns Engine

    fun startCar() {
        engine.start()
        println("Car is running")
    }
}

fun main() {
    val car = Car()
    car.startCar()
    // When Car is destroyed, Engine is destroyed too.
}

Key Points:

🧠 Real-life Example:

A Human has a Heart. If the human dies, the heart cannot function independently.


class Heart {
    fun beat() = println("Heart beating...")
}

class Human {
    private val heart = Heart()
    fun live() = heart.beat()
}

2. What Is Aggregation?

Aggregation represents a “weak has-a” relationship. It means one class uses another, but they can exist independently. The parent does not own the child’s lifecycle.


class Student(val name: String)

class School(val name: String) {
    private val students = mutableListOf<Student>()

    fun addStudent(student: Student) {
        students.add(student)
    }
}

fun main() {
    val student = Student("Talha")
    val school = School("ABC School")
    school.addStudent(student)
    // Even if School is destroyed, Student can still exist.
}

Key Points:

🧠 Real-life Example:

A Library has many Books. If the library closes, the books can still exist elsewhere.


class Book(val title: String)

class Library {
    private val books = mutableListOf<Book>()
    fun addBook(book: Book) = books.add(book)
}

3. ⚖️ Composition vs Aggregation — Comparison Table

Feature Composition Aggregation
Relationship Type Strong has-a Weak has-a
Object Dependency Child cannot exist without parent Child can exist independently
Ownership Parent owns the child Parent only references the child
Example Car → Engine School → Student
Lifetime Relationship Parent and child share same lifecycle Independent lifecycles

✅ Summary

Android Specific Questions

1. Lifecycle of ViewModel & When It’s Destroyed

A ViewModel is tied to a ViewModelStoreOwner, such as an Activity or Fragment. It helps you store UI-related data and survive configuration changes like screen rotations.

Examples

Activity-scoped ViewModel (XML)


class MyViewModel : ViewModel() {
    val counter = MutableLiveData(0)

    override fun onCleared() {
        super.onCleared()
        println("ViewModel is destroyed")
    }
}

// In Activity
val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
viewModel.counter.observe(this) { value ->
    println("Counter: $value")
}

Fragment-scoped ViewModel (XML)


class MyFragment : Fragment() {
    private val viewModel: MyViewModel by viewModels() // scoped to this fragment

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.counter.observe(viewLifecycleOwner) { value ->
            println("Counter in Fragment: $value")
        }
    }
}

Compose + rememberSaveable


@Composable
fun CounterScreen(viewModel: MyViewModel = viewModel()) {
    val counter by viewModel.counter.observeAsState(0)

    Button(onClick = { viewModel.counter.value = counter + 1 }) {
        Text("Count: $counter")
    }
}

Key point: The ViewModel survives rotation or recomposition, but will be cleared when the Activity/Fragment is destroyed permanently.

2. Avoid Resetting Data During Screen Rotation

Use ViewModel (for XML) or rememberSaveable (for Compose) to preserve state.


// XML
val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)

// Compose
val state = rememberSaveable { mutableStateOf("") }
  

3. Types of ViewModel

4. Avoid Repetitive Recomposition

5. Show Ads in RecyclerView After Every Two Cards


override fun getItemViewType(position: Int): Int {
    return if ((position + 1) % 3 == 0) AD_TYPE else NORMAL_TYPE
}
  

6. Separate URLs for Staging & Production (No Build Flavor)


@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    fun provideBaseUrl(): String {
        return if (BuildConfig.DEBUG) STAGING_URL else PROD_URL
    }
}
  

7. Difference Between async and launch

Featurelaunchasync
ReturnsJobDeferred<T>
Use CaseFire & forgetReturn result
Examplelaunch { doTask() }val result = async { getData() }.await()

8. Run 3 API Calls in Parallel


coroutineScope {
    val a = async { api1() }
    val b = async { api2() }
    val c = async { api3() }

    val results = listOf(a.await(), b.await(), c.await())
}
  

9. 200 Images on Server (Limit Requests)


val semaphore = Semaphore(10)
imageIds.map { id ->
    async {
        semaphore.withPermit { downloadImage(id) }
    }
}.awaitAll()
  

10. Auto Token Refresh When API Fails

This TokenInterceptor automatically adds the authentication token to API requests and refreshes it if the server responds with 401 Unauthorized. It ensures seamless user experience without manual token handling for each request.


class TokenInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        // 1️⃣ Add current token to the request
        var request = chain.request().newBuilder()
            .addHeader("Authorization", "Bearer ${getToken()}")
            .build()

        // 2️⃣ Execute the request
        val response = chain.proceed(request)

        // 3️⃣ Check if token is expired or invalid
        if (response.code == 401) {
            // 4️⃣ Refresh the token
            val newToken = refreshToken()

            // 5️⃣ Retry the request with the new token
            val newRequest = request.newBuilder()
                .header("Authorization", "Bearer $newToken")
                .build()

            return chain.proceed(newRequest) // Retry request
        }

        // 6️⃣ Return the original response if token is valid
        return response
    }
}
How It Works:
Important Notes:
Usage Example:

// Add interceptor to OkHttpClient
val okHttpClient = OkHttpClient.Builder()
    .addInterceptor(TokenInterceptor())
    .build()

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .client(okHttpClient)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

Problem Solving

1. Reverse a String Without Built-in Functions


fun reverseString(str: String): String {
    var result = ""
    for (i in str.length - 1 downTo 0) {
        result += str[i]
    }
    return result
}

fun main() {
    println(reverseString("Talha Abbas")) // ahlaT sabbA
}
  

Output: ahlaT sabbA

Summary

Problem Solving — Algorithmic Practice (Interview Prep)

1️⃣ Find Element Where Left Sum Equals Right Sum

Problem: Given an array [10, 5, 1, 2, 3, 4], find an element such that the sum of elements on its left equals the sum of elements on its right. For example, 5 is such an element because left sum = 10 and right sum = 1+2+3+4.

💡 Verbal Explanation

The efficient approach is to calculate the total sum first. Then iterate through the array, keeping track of the left sum. For each element, compute right sum as totalSum - leftSum - currentElement. If both are equal, that’s our equilibrium point.


fun findEquilibrium(arr: IntArray): Int? {
    val totalSum = arr.sum()
    var leftSum = 0

    for (i in arr.indices) {
        val rightSum = totalSum - leftSum - arr[i]
        if (leftSum == rightSum) return arr[i]
        leftSum += arr[i]
    }
    return null
}

fun main() {
    val arr = intArrayOf(10, 5, 1, 2, 3, 4)
    println(findEquilibrium(arr)) // Output: 5
}
            

2️⃣ Next Number in the Fibonacci Series

Problem: If a number like 8 is given, return the next number in the Fibonacci sequence, which is 13.

💡 Verbal Explanation

We start from the base of the Fibonacci series (0, 1) and keep generating numbers until we reach the given number. Once found, we return the next one in sequence.


fun nextFibonacci(n: Int): Int {
    var a = 0
    var b = 1
    while (b < n) {
        val temp = a + b
        a = b
        b = temp
    }
    return if (b == n) a + b else -1
}

fun main() {
    println(nextFibonacci(8))  // Output: 13
}
            

3️⃣ Water Jug Problem — Measure 4 Litres (8L, 5L, 3L)

Problem: You have three jugs: 8L, 5L, and 3L with no scaling marks. The goal is to measure exactly 4 litres.

💡 Verbal Explanation

This is a state-based search problem. You can think of each jug’s volume as a state. Pour water between jugs while following the capacity limits until one of them contains 4 litres. You can either explain manually or solve algorithmically using BFS (Breadth-First Search).

🧩 Step-by-Step Logical Solution

Step8L5L3LExplanation
1800Start with 8L full
2350Pour 5L into 5L jug
3323Pour from 5L → 3L
4620Pour from 3L → 8L
5602Pour from 5L → 3L
6152Pour from 8L → 5L
7143Pour from 5L → 3L
440Now 8L jug has 4L (Goal achieved)

💻 BFS Algorithmic Solution (Kotlin)


data class State(val a: Int, val b: Int, val c: Int)

fun measure4L() {
    val cap = intArrayOf(8, 5, 3)
    val visited = mutableSetOf()
    val queue = ArrayDeque()

    queue.add(State(8, 0, 0))

    while (queue.isNotEmpty()) {
        val cur = queue.removeFirst()
        if (cur.a == 4 || cur.b == 4 || cur.c == 4) {
            println("Found solution: $cur")
            return
        }

        if (cur in visited) continue
        visited.add(cur)

        val amounts = intArrayOf(cur.a, cur.b, cur.c)
        for (i in 0..2) {
            for (j in 0..2) {
                if (i == j || amounts[i] == 0 || amounts[j] == cap[j]) continue
                val new = amounts.copyOf()
                val pour = minOf(amounts[i], cap[j] - amounts[j])
                new[i] -= pour
                new[j] += pour
                queue.add(State(new[0], new[1], new[2]))
            }
        }
    }
    println("No solution found")
}

fun main() {
    measure4L()
}
            

📘 Summary Table

ProblemConceptApproach
Equilibrium IndexArraysPrefix Sum Optimization
Next FibonacciMath / SeriesIterative Loop
Beaker ProblemState Space SearchBFS Algorithm / Logic Steps