Android Interview Series 2024 — Part 4 (Kotlin Basics)
This is Part 4 of the android interview question series. This part will focus on Kotlin basics.
- Part 1 — Android basics
- Part 2 — Android experts
- Part 3 — Java basics
- Part 4 — Kotlin basics -> You are here
- Part 5 — Kotlin coroutines
- Part 6 — Kotlin Flows
- Part 7 — Jetpack Compose
- Part 8 — Android architecture & framework
1. What is Kotlin?
Kotlin is a modern programming language that runs on the Java Virtual Machine (JVM). This means Kotlin is fully compatible with Java, allowing you to use it alongside existing Java code seamlessly.
2. What is the difference between variables declared with val
and var
?
- A variable declared with
var
is mutable. Its value can be changed after its initial assignment. - A variable declared with
val
is read-only. Once a value is assigned to it, it cannot be changed. It is similar to a final variable in Java.
3. What is the const
keyword?
A variable declared with const val
is a compile-time constant. It must be assigned a value at the time of declaration and cannot be changed afterward. It is more restrictive than val
and can only be used with primitive types and strings.
4. When should we use the lateinit
keyword?
lateinit
allows you to initialize properties after object creation, which is useful for properties that cannot be initialized in the constructor. lateinit
properties must be initialized before access.
One example would be when we declare RecyclerView
adapter classes inside the Fragment
but only initialize it on the onCreateView
of the Fragment
.
private lateinit var adapter: SimpleAdapter
5. What are the different visibility modifiers?
Similar to Java, kotlin provides four visibility modifiers: public, private, protected, and internal.
- The public modifier is the default visibility. Members marked with
public
are accessible from anywhere in the project. - The private modifier restricts the visibility to the containing scope. If a member is marked as
private
, it can only be accessed within the same file or class. - The protected modifier allows visibility within the class and its subclasses.
- The internal modifier restricts visibility to the same module.
6. What are Anonymous Functions?
An anonymous function is just what its name implies — it has no name. Anonymous functions and lambda expressions are both unnamed functions that can be passed as values.
//Example of Anonymous Function:
val sum: (String, String) -> String = fun(a, b): String {
return "$a $b"
}
7. What are Higher-Order Functions?
A higher-order function is a function that takes another function as a parameter, returns a function, or both. They enable more abstract and flexible code structures.
fun List<Int>.customFilter(isEvenNumber: (Int) -> Boolean): List<Int> {
val result = mutableListOf<Int>()
for (item in this) {
if (isEvenNumber(item)) {
result.add(item)
}
}
return result
}
In this example, customFilter
is a higher-order function that takes another function operation as a parameter.
8. What are Inline Functions?
Inline functions reduce the overhead of higher-order functions by copying the code of the function parameter directly into the call site. When a function is marked as inline
, the compiler will replace function calls with the actual code of the function at compile time, reducing memory allocations and improving runtime performance.
For example:
inline fun performOperation(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
fun main() {
val result = performOperation(5, 3) { x, y -> x + y }
println("Result: $result") // Output: Result: 8
}
In this example, the performOperation
function is inline. It takes two integers and a lambda as parameters, then applies the lambda operation to those integers. Since it's marked inline
, the compiler inlines the function body wherever it is called, which can improve performance when used with lambdas.
Here’s how the inline transformation might look after compilation (simplified):
fun main() {
val x = 5
val y = 3
val result = x + y // Directly inlining the lambda's body
println("Result: $result") // Output: 8
}
9. Advantages of inline functions
The main difference between calls to an inline function and a regular function is that: when you call a regular function, an instance of the anonymous class body is created that implements our lambda and its instance is passed to the regular function.
In the case of an inline function, the calling code and the inline function code are combined and inserted directly into the call location, which eliminates the need to create an anonymous class for the passed lambda.
Example:
private inline fun inlineFun(body: () -> String) {
println("inline func code, " + body.invoke())
}
fun testInline() {
inlineFun { "external inline code" }
}
private fun regularFun(body: () -> String) {
println("regular func code, " + body.invoke())
}
fun testRegular() {
regularFun { "external regular code" }
}
If you look at the decompiled code of the above, you will notice that an instance of the anonymous class body is created that implements our lambda and its instance is passed to the regular function:
// In the case of an inline function, the calling code and the // inline function code are combined and inserted directly into // the call location, which eliminates the need to create an
// anonymous class for the passed lambda.
public final void testInline() {
String var4 = (new StringBuilder())
.append("inline func code, ")
.append("external inline code")
.toString();
System.out.println(var4);
}
public final void testRegular() {
Function0 body = (Function0)(new Function0() {
public final String invoke() {
return "external regular code";
}
});
this.regularFun(body);
}
10. When to use inline functions in Android?
- Inline functions are particularly useful for small, frequently called functions that are critical to the performance of the code.
- Another example is to inline a function that is used as an argument to another function.
11. What are crossline
functions?
crossinline
is used in inline functions to ensure that the lambda parameter cannot use non-local returns (i.e.,return
statements that attempt to exit the containing function).- In layman’s terms, when you mark a lambda parameter with
crossinline
, you’re telling Kotlin, “This lambda can’t usereturn
to exit the parent function it’s inside.” This restriction is helpful if the lambda is passed into another function or a coroutine, where usingreturn
could cause unexpected behavior. - When using the
crossinline
keyword in an inline function, the compiler will still replace the lambda function with its body at the call site, just like it does with any inline function. The key distinction withcrossinline
is not about whether the lambda is inlined or not—it indeed is inlined—but about controlling the behavior of the lambda to prevent it from using non-local returns.
inline fun performTask(
task: () -> Unit,
crossinline onComplete: () -> Unit
) {
task() // Perform some task
// Crossinline ensures onComplete can't use `return` to exit `performTask`
onComplete()
}
fun main() {
performTask(
task = {
println("Doing some work...")
},
onComplete = {
println("Task completed.")
// Trying to use `return` here would cause an error
// return // This line would not compile because of `crossinline`
}
)
}
12. What are noinline
functions?
noinline
is used when you have an inline
function, but you don’t want all the lambda parameters to be inlined. By marking a lambda with noinline
, you’re telling Kotlin, “Don’t inline this specific lambda parameter.” This can be useful when you need to pass the lambda to other functions or when inlining wouldn’t make sense for some parameters.
Say you have multiple lambdas in your inlined function and you don’t want all of them to be inlined, you can mark the lambdas you don’t want to be inlined with the noinline
keyword.
inline fun executeTasks(
inlineTask: () -> Unit,
noinline deferredTask: () -> Unit
) {
inlineTask() // This will be inlined
deferredTask() // This will not be inlined
}
fun main() {
// Call `executeTasks` with two lambdas
executeTasks(
inlineTask = {
println("Performing inline task...")
},
deferredTask = {
println("Performing deferred (noinline) task...")
}
)
}
13. What is a reified
keyword in Kotlin?
reified types allow us to use actual type information at runtime, specifically within inline functions. Normally, type information is erased at runtime due to type erasure in Java and Kotlin, which means you can’t access the type parameter of a generic at runtime. However, by making a function inline
and using the reified
keyword on the type parameter, Kotlin "reifies" (or keeps) the type information, allowing us to work with it at runtime.
Imagine you’re working with JSON parsing, and you want to write a function that takes a JSON string and converts it into an object of any type. With reified types, we don’t need to pass a Class
object as a parameter; we can simply use reified T
to get the class type at runtime.
inline fun <reified T> Gson.fromJson(json: String): T {
return fromJson(json, T::class.java)
}
fun main() {
val json = """{"name": "John", "age": 30}"""
// Parse JSON to a Kotlin data class
val person = Gson().fromJson<Person>(json)
println("Parsed object: $person")
}
data class Person(val name: String, val age: Int)
reified
types are a powerful feature in Kotlin that make it easier to work with generics at runtime, especially useful in tasks like JSON parsing, type checking, and dependency injection in Android development.
14. What are extension functions?
An extension function in Kotlin allows you to add new functionality to existing classes without modifying their source code. It’s a way to “extend” a class by adding a new function that acts as if it were part of the original class.
// Extension function to count words in a String
fun String.wordCount(): Int {
return this.split(" ").size
}
fun main() {
val text = "Hello Kotlin world"
println("Word count: ${text.wordCount()}") // Output: Word count: 3
}
15. What are Scoped Functions?
Scoped Functions are functions that allow you to execute a block of code within the context of an object. These functions provide a way to work with objects more concisely, especially when you need to initialize, configure, or apply transformations to an object. Kotlin has five main scoped functions:
let
: Executes the code block on the object and returns the result of the block. It’s commonly used for null checks.
val name: String? = "Kotlin"
// Using `let` to safely perform an action if the object is not null
name?.let {
println("The name is $it") // Output: The name is Kotlin
}
- The
run
function is often used when you need to operate on an object and return a result. It can be used to initialize or transform an object.
val result = "Kotlin".run {
toUpperCase() // Transformation to uppercase
}
// Output: KOTLIN
println(result)
- The
with
function is similar torun
but is not called directly on the object; instead, the object is passed as an argument. This makes it ideal for configuring objects without directly calling them.
val person = Person("John", 25)
val description = with(person) {
"Name: $name, Age: $age"
}
// Output: Name: John, Age: 25
println(description)
- The
apply
function is used for configuring an object, as it returns the object itself after applying some configuration. It’s often used to initialize or modify objects.
val person = Person().apply {
name = "Anitaa"
age = 30
}
// Output: Person(name=Anitaa, age=30)
println(person)
- The
also
function is similar toapply
but is used when you want to perform some additional operations on the object (like logging or debugging) and return the object itself.
val number = 10.also {
println("The number is $it") // Output: The number is 10
}
// Output: 10
println(number)
16. How is the use
keyword used in Kotlin?
- The
use
function is a scope function that is specifically designed for working with Closable resources, such as files, streams, or database connections. It ensures that the resource is closed automatically after the block of code is executed, even if an exception occurs, helping to prevent resource leaks. - When you call
use
, it opens a scope, performs operations within the block, and then automatically calls.close()
on the resource at the end of the block.
import java.io.File
// Let's take an example of reading from a file.
// By using use, we ensure that the file is closed properly after reading,
// even if an exception occurs
fun readFile(filePath: String): String {
return File(filePath).bufferedReader().use { reader ->
reader.readText() // Automatically closes the reader after this block
}
}
fun main() {
val content = readFile("example.txt")
println(content)
}
17. What is a Type Alias?
A type alias in Kotlin is a way to provide an alternative name for an existing type. It doesn’t create a new type but allows you to refer to an existing type with a different name. This can make your code more readable and expressive, especially when dealing with complex types.
data class ContactModel(
val id: String,
val displayName: String,
val phoneNumber: String,
val photoThumbnailUri: String?,
val photoUri: String?
)
// In this example, we've created a typealias for `ContactModel` // to convert it into a Map which includes the alphabets as the // key and the list of `ContactModel` as the values of the Map.
typealias GroupedContacts = Map<String, List<ContactModel>>
18. What are infix
functions?
infix functions
are a special type of function that allows you to call the function in a more natural, readable way, similar to an operator. Infix functions are called using infix notation, which means you can call them without using the usual dot (.
) notation or parentheses.
// an infix function defined on Context that checks if a specific permission is granted.
infix fun Context.hasPermission(permission: String): Boolean {
return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
}
fun main(context: Context) {
// Instead of calling context.hasPermission(android.Manifest.permission.CAMERA), we use // the infix notation context hasPermission android.Manifest.permission.CAMERA, making
// the code more readable.
if (context hasPermission android.Manifest.permission.CAMERA) {
println("Camera permission granted")
} else {
println("Camera permission not granted")
}
}
Kotlin’s standard library includes several infix functions. Some popular examples include:
to
for creating pairs:val pair = "key" to "value"
until
for ranges:val range = 1 until 10
downTo
for creating a descending range:val countdown = 10 downTo 1
19. How is the by
keyword used in kotlin?
The by
keyword is used for delegation, which allows one object to delegate certain functionality to another object. The by
keyword helps reduce boilerplate and allows reusing functionality without having to implement it from scratch. Kotlin provides two main ways to use by
:
- Delegating properties: You can delegate the behavior of a property to another object (for example, using
by lazy
).by lazy
keyword allows you to initialise a property only when it’s accessed for the first time.
// Here, binding is only initialized when it’s accessed for the // first time, which can improve performance, especially if the // view binding setup is delayed until needed.
val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
- Delegating interfaces: You can delegate the implementation of an interface to another object, which can reduce code duplication.
interface Printer {
fun print()
}
// SimplePrinter implements Printer and provides
// the logic for printMessage
class SimplePrinter : Printer {
override fun print() = println("Printing from SimplePrinter")
}
// AdvancedPrinter doesn’t implement printMessage itself but
// delegates it to the printer instance (an instance of
// SimplePrinter). This allows AdvancedPrinter to reuse
// SimplePrinter’s implementation without redefining it.
class AdvancedPrinter(printer: Printer) : Printer by printer
fun main() {
val printer = SimplePrinter()
val advancedPrinter = AdvancedPrinter(printer)
advancedPrinter.print() // Output: Printing from SimplePrinter
}
20. Stack Memory in Kotlin?
In Kotlin, as in many other languages, memory is split into two main parts: the stack and the heap.
- The stack is a part of memory that stores temporary variables created by functions. This includes variables used in the main function and other functions your code calls. Stack memory is used for static memory allocation, which means the memory size for variables is known when the program is compiled.
- What is stored in Stack?
- Primitive Data Types: Simple data types like Int, Double, Char, Boolean, etc., are stored here.
- References to Objects: While actual objects are stored in the heap, the references (or pointers) to those objects are stored in the stack.
- Each time a function is called, a new “stack frame” is created on the stack, holding that function’s local variables and parameters. When the function returns, its stack frame is removed, freeing the memory.
- Fast Access: Stack memory is very fast to allocate and deallocate. Last-In-First-Out (LIFO): Variables are stored and removed in the reverse order they were added. Fixed Size: Stack memory is limited and relatively small compared to heap memory. Automatic Management: Memory allocation and deallocation are handled automatically by the JVM as functions are called and return.
// The values of a, b, and result are stored on the stack
// because they are local to their functions. When calculate
// finishes, its stack frame (containing a and b) is popped off, // freeing memory.
fun calculate(): Int {
val a = 5 // Stored in stack
val b = 10 // Stored in stack
return a + b
}
fun main() {
val result = calculate() // `result` is stored in stack
println(result)
}
21. Heap Memory in Kotlin?
The heap is another part of memory used for dynamic memory allocation. This means that memory can be allocated and resized as needed during the program’s runtime. Objects and other larger data structures live here. stack memory is faster and used for small, temporary data with limited scope, while heap memory is for larger objects and data that need to stick around longer, managed by garbage collection.
// The Person object is created on the heap because it’s an
// instance of a class. The reference to this Person object
// (person) is stored on the stack. Even when createPerson
// finishes, the Person object remains in memory (on the heap)
// as long as there’s a reference to it in main.
class Person(val name: String)
fun createPerson(): Person {
// `person` reference on stack; object on heap
val person = Person("Alice")
return person
}
fun main() {
val person = createPerson() // `person` reference on stack; points to heap object
println(person.name)
}
22. Stack vs Heap memory?
23. How is memory allocated for nullable types?
- For primitive types like
Int
,Double
,Boolean
, etc., Kotlin normally uses JVM primitives (int
,double
,boolean
) to save memory and improve performance. - However, when you make a primitive type nullable (e.g.,
Int?
orDouble?
), Kotlin boxes it, meaning it wraps the primitive in an object form. This way, it can represent both the value andnull
. - Boxing creates an extra object on the heap, which consumes more memory compared to non-nullable primitives stored on the stack.
- Nullable types are typically stored on the heap because they require additional metadata to indicate their nullability. This metadata is not just the value itself but also an indication of whether the variable is null.
fun main() {
// Stored as a primitive `int` on the stack
val nonNullableInt: Int = 5
// Stored as `Integer` object on the heap
val nullableInt: Int? = 5
// Holds `null` reference, no heap allocation for `Integer`
val anotherNullableInt: Int? = null
}
24. Difference between == and ===?
- The
==
operator checks structural equality, meaning it compares the content or values of two objects. Under the hood,==
calls the.equals()
method. - The
===
operator checks referential equality, meaning it checks whether two references point to the same object in memory. This is useful to see if two variables actually reference the exact same instance.
val a = "Kotlin"
val b = "Kotlin"
val c = a
// Output: true, because Kotlin caches strings and both point to // the same object
println(a === b)
// Output: true, because `c` is assigned directly from `a`
println(a === c)
25. How to create a Singleton
class in Kotlin?
Using object
is the simplest and most common way to create a singleton in Kotlin. When you declare a class with object
, Kotlin automatically ensures that there’s only one instance of that class, and it’s lazily initialized when first accessed.
// ApiClient is a singleton object that holds a single instance of Retrofit.
object ApiClient {
private const val BASE_URL = "https://api.example.com/"
// Lazy Initialization: The retrofit instance is created only once when first accessed, // optimizing resource usage.
val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
26. List the different operations that can be performed on a List
?
Kotlin provides a variety of operations to work with lists efficiently:
27. List the different operations that can be performed on a Map
?
Kotlin provides a variety of operations to work with maps efficiently:
28. List the different operations that can be performed on a Set
?
Sets are collections that contain unique elements. Kotlin provides a variety of operations to work with Sets
efficiently:
29. What are sealed
classes?
sealed
classes are a special type of class that allow you to represent a restricted hierarchy of classes, where all possible subclasses are known at compile-time. This makes them ideal for modeling restricted data types or state hierarchies.- Unlike regular classes, sealed classes enforce a strict set of subclasses, allowing the compiler to ensure all cases are handled when performing
when
expressions, making code safer and more expressive.
sealed class NetworkResult {
data class Success(val data: String) : NetworkResult()
data class Error(val error: String) : NetworkResult()
object Loading : NetworkResult()
}
fun handleNetworkResult(result: NetworkResult) {
when (result) {
is NetworkResult.Success -> println("Data received: ${result.data}")
is NetworkResult.Error -> println("Error occurred: ${result.error}")
NetworkResult.Loading -> println("Loading...")
}
}
30. What are sealed interfaces?
A sealed interface is similar to a sealed class but allows more flexibility by enabling classes or objects to implement the sealed interface. This is useful when you want to restrict implementations to a known set while allowing different types of classes or objects to implement the interface.
31. What are data
classes?
data
classes are a special type of class intended to hold data. They automatically provide useful functionalities for working with data objects, such as equals()
, hashCode()
, toString()
, and copy()
methods. Data classes are typically used to create immutable objects that primarily hold data without requiring additional boilerplate code.
data class User(val name: String, val age: Int)
fun main() {
val user1 = User("Alice", 25)
val user2 = User("Alice", 25)
val user3 = User("Bob", 30)
// toString() - prints the object details
println(user1) // Output: User(name=Alice, age=25)
// equals() - compares objects based on their data
println(user1 == user2) // Output: true
println(user1 == user3) // Output: false
// copy() - creates a new object with modified properties
val user4 = user1.copy(name = "Charlie")
println(user4) // Output: User(name=Charlie, age=25)
// Destructuring declarations
val (name, age) = user1
println("Name: $name, Age: $age") // Output: Name: Alice, Age: 25
}
32. What is a companion object
?
A companion object
is a special object associated with a class that allows you to define properties and functions directly within the class, similar to static members in Java. However, unlike Java’s static
keyword, companion objects are more flexible and can be used as a singleton object that can hold methods and properties common to all instances of the class.
class Counter {
companion object {
var count = 0
fun increment() {
count++
}
}
}
fun main() {
Counter.increment()
Counter.increment()
println("Count: ${Counter.count}") // Output: Count: 2
}
33. What is an Abstract Class
?
An abstract class is a class that cannot be instantiated on its own and is meant to serve as a base class for other classes. Abstract classes allow you to define abstract methods (methods without implementations) and properties that must be implemented by any subclass. They can also contain concrete methods with implementations, making them versatile for building reusable code structures.
34. What is a Generic Class?
- A
generic
class is a class that can work with different data types. By defining a generic class, you can make the class more flexible and reusable because it can operate on any data type specified at the time of instantiation. - Generic classes use type parameters (often denoted by
<T>
) to specify the type of data they can handle. - Generics provide compile-time type safety, reducing the need for casting and preventing type errors.
- Kotlin allows you to set constraints on type parameters using the
:
syntax, specifying that the type must be a subclass of a specific class (eg:NumberBox<T : Number>
).
data class ApiResponse<T>(val data: T?, val error: String?)
35. How is the open
keyword used?
The open
keyword is used to allow classes and members (such as properties and functions) to be inherited or overridden. By default, all classes, functions, and properties in Kotlin are final, meaning they cannot be extended or overridden. This is different from languages like Java, where classes and members are inheritable by default.
36. Why Kotlin Doesn’t Support Multiple Inheritance by Classes?
Kotlin doesn’t support multiple inheritance by classes to avoid the complexity and ambiguity associated with it, which can lead to the “diamond problem.”
In multiple inheritance, a subclass can inherit from multiple classes with overlapping members, leading to ambiguity. For instance, if two parent classes define the same method or property, the compiler would face uncertainty about which one to inherit. This is called the “diamond problem”.
Kotlin allows a class to implement multiple interfaces, and interfaces can contain default implementations of methods. This approach enables multiple inheritance of behavior without the ambiguity associated with multiple class inheritance.
Thanks for reading!
Hope you find this useful. This is just a list of questions I personally found useful in interviews. This list is by no means exhaustive. Let me know your thoughts in the responses. Happy coding!