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
}
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.
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
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.
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.
sound()), since they exist in Animal.Cat using an Animal reference.is or as.
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
}
}
// 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:
lateinit only works with var (mutable, non-null).const val must be initialized immediately.MyClass.name or MyClass.VERSION.
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
}
}
lateinit.| 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 |
final?
A final class cannot be inherited or extended.
final class Animal
class Cat : Animal() // ❌ Error
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
}
}
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
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 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:
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()
}
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:
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)
}
| 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 |
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.
onCleared() method is called when:
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.
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("") }
remember or rememberSaveablederivedStateOf for computed values
override fun getItemViewType(position: Int): Int {
return if ((position + 1) % 3 == 0) AD_TYPE else NORMAL_TYPE
}
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
fun provideBaseUrl(): String {
return if (BuildConfig.DEBUG) STAGING_URL else PROD_URL
}
}
async and launch| Feature | launch | async |
|---|---|---|
| Returns | Job | Deferred<T> |
| Use Case | Fire & forget | Return result |
| Example | launch { doTask() } | val result = async { getData() }.await() |
coroutineScope {
val a = async { api1() }
val b = async { api2() }
val c = async { api3() }
val results = listOf(a.await(), b.await(), c.await())
}
val semaphore = Semaphore(10)
imageIds.map { id ->
async {
semaphore.withPermit { downloadImage(id) }
}
}.awaitAll()
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
}
}
401.refreshToken() to get a new token.getToken() and refreshToken() are thread-safe.401 responses.
// 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()
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
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.
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
}
Problem: If a number like 8 is given, return the next number in the Fibonacci sequence,
which is 13.
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
}
Problem: You have three jugs: 8L, 5L, and 3L with no scaling marks. The goal is to measure exactly 4 litres.
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 | 8L | 5L | 3L | Explanation |
|---|---|---|---|---|
| 1 | 8 | 0 | 0 | Start with 8L full |
| 2 | 3 | 5 | 0 | Pour 5L into 5L jug |
| 3 | 3 | 2 | 3 | Pour from 5L → 3L |
| 4 | 6 | 2 | 0 | Pour from 3L → 8L |
| 5 | 6 | 0 | 2 | Pour from 5L → 3L |
| 6 | 1 | 5 | 2 | Pour from 8L → 5L |
| 7 | 1 | 4 | 3 | Pour from 5L → 3L |
| ✅ | 4 | 4 | 0 | Now 8L jug has 4L (Goal achieved) |
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()
}
| Problem | Concept | Approach |
|---|---|---|
| Equilibrium Index | Arrays | Prefix Sum Optimization |
| Next Fibonacci | Math / Series | Iterative Loop |
| Beaker Problem | State Space Search | BFS Algorithm / Logic Steps |