Interview prep
The follow-up ladder
Interviews don't stop at the first question. Each card starts with one question, then climbs through the follow-ups an interviewer asks next. Try each step out loud before you open the answers.
Kotlin and DSA basics
QWhat is the difference between ?. and !! in Kotlin?
Show answers
Junior answer
?. calls the member only if the value is not null, otherwise it gives null. !! throws if the value is null.
Senior answer
?. turns a nullable chain into a nullable result, so the null case stays visible in the type. !! says "I know better than the compiler" and moves the failure to runtime as a NullPointerException.
↳ 1When is !! acceptable?
Show answers
Junior answer
When you are sure the value is not null.
Senior answer
Almost never in app code. If I'm sure, I'd rather make the type non-null or use requireNotNull with a message, so the crash explains itself. Tests are the one place I use it freely.
↳ 2A Kotlin app crashes with a NullPointerException, but the code has no !!. How?
Show answers
Junior answer
Maybe a Java library returned null.
Senior answer
Platform types. A value from Java without nullability annotations has type String!, so Kotlin lets you treat it as non-null. If Java returns null, it fails where you use it, or at the assignment if you declared the variable as non-null.
↳ 3How would you make a Java API safe for Kotlin callers?
Show answers
Junior answer
Check for null everywhere in Kotlin.
Senior answer
Add nullability annotations such as @Nullable and @NonNull to the Java side. Kotlin reads them and gives proper nullable or non-null types, so the compiler checks every caller.
How did you do?QWhat are Any, Unit and Nothing?
Show answers
Junior answer
Any is like Object, Unit is like void, Nothing means no value.
Senior answer
Any is the root of every non-null type. Unit is a real type with one value, used when a function returns nothing useful. Nothing has no values at all: a function returning Nothing never returns normally.
↳ 1What is the type of a throw expression?
Show answers
Junior answer
It doesn't have one.
Senior answer
Nothing. Because Nothing is a subtype of every type, you can write val name = input ?: throw IllegalArgumentException() and name is still a String.
↳ 2Why does TODO() compile inside a function that must return a String?
Show answers
Junior answer
It's a special keyword.
Senior answer
It's a normal function that returns Nothing. Nothing fits wherever a String is expected, and the compiler knows the code after it can't run.
How did you do?QWhat does the data keyword give you?
Show answers
Junior answer
equals, hashCode, toString and copy.
Senior answer
equals, hashCode, toString, copy and componentN functions for destructuring, all generated from the primary constructor properties.
↳ 1Which properties take part in equals?
Show answers
Junior answer
All of them.
Senior answer
Only the ones in the primary constructor. A property declared in the class body is ignored by equals, hashCode, toString and copy.
↳ 2Why is a data class with var properties risky as a HashMap key?
Show answers
Junior answer
Because the values can change.
Senior answer
hashCode depends on those properties. Change one after inserting the key and the map looks in the wrong bucket, so get returns null for a key that is still inside.
↳ 3copy() is shallow. What does that mean for a property that holds a MutableList?
Show answers
Junior answer
The list is copied too.
Senior answer
The list is not copied. The original and the copy share the same list, so adding to one changes both. Use read-only List types, or copy the list yourself.
How did you do?What does this print?
data class User(val name: String) { var age = 0 } val a = User("Asha").apply { age = 30 } val b = User("Asha").apply { age = 40 } println(a == b) println(a)Show output
true User(name=Asha)age is declared in the class body, not the primary constructor, so equals and toString ignore it.
How did you do?QWhen would you use a sealed class instead of an enum?
Show answers
Junior answer
When you need more than constants.
Senior answer
An enum has a fixed set of single instances that all share the same shape. A sealed class has a fixed set of types, and each type can carry its own data, like Loading, Error(message) and Success(items).
↳ 1Why doesn't a when on a sealed type need an else branch?
Show answers
Junior answer
Because Kotlin knows all the cases.
Senior answer
The compiler knows every direct subtype, so it can check the when is exhaustive. That is the real benefit: add a new subtype and every when that misses it stops compiling.
↳ 2Where can the subclasses of a sealed class live?
Show answers
Junior answer
In the same file.
Senior answer
Since Kotlin 1.5, in the same package and the same module, not only the same file. That's what lets a sealed hierarchy be split across files while still being closed to outside code.
How did you do?QWhat is a value class?
Show answers
Junior answer
A class that wraps one value.
Senior answer
@JvmInline value class UserId(val value: String) gives a distinct type at compile time. At runtime it usually compiles down to the plain String, so you get type safety without an extra object.
↳ 1Why not just use a typealias?
Show answers
Junior answer
They're the same.
Senior answer
A typealias is only another name: you can pass an OrderId where a UserId is expected. A value class is a separate type, so the compiler catches the mix-up.
↳ 2When does a value class get boxed?
Show answers
Junior answer
Never.
Senior answer
When it's used as a generic type argument such as List<UserId>, through an interface it implements, or as a nullable type when it wraps a primitive like Int. Then the wrapper object is created, so the saving is lost.
How did you do?QWhat is the difference between lazy and lateinit?
Show answers
Junior answer
lazy is for val and runs the block on first use. lateinit is for var that you assign later.
Senior answer
lazy is a delegate: the value is computed once, on first read, and can't change. lateinit is a promise that a var will be assigned before it's read, which suits values set by a framework, like injected fields or test setup.
↳ 1What happens if you read a lateinit property before it is assigned?
Show answers
Junior answer
The app crashes.
Senior answer
It throws UninitializedPropertyAccessException, which names the property. You can check first with this::name.isInitialized, but needing that check often means the design should use a nullable type or a constructor parameter.
↳ 2Why can't you use lateinit with Int?
Show answers
Junior answer
Kotlin doesn't allow it for primitives.
Senior answer
lateinit uses null in the backing field to mean "not assigned yet". An Int property compiles to a primitive int, which can't hold null, so there is no way to mark it as unassigned.
↳ 3Is lazy thread-safe?
Show answers
Junior answer
Yes.
Senior answer
By default, yes: LazyThreadSafetyMode.SYNCHRONIZED locks so the block runs once. PUBLICATION lets several threads run it and keeps the first result. NONE skips locking, which is fine when only the main thread touches it.
How did you do?QWhat does inline do?
Show answers
Junior answer
It makes the function faster.
Senior answer
The compiler copies the function body and its lambda arguments into each call site. That removes the lambda object and the call, which matters for small higher-order functions called often.
↳ 1Why can you return from the outer function inside forEach { }?
Show answers
Junior answer
Because forEach allows it.
Senior answer
forEach is inline, so the lambda body is pasted into your function and return exits your function. That is a non-local return. With a non-inline function you'd need return@forEach.
↳ 2What does reified do, and why does it need inline?
Show answers
Junior answer
It lets you use T as a real type.
Senior answer
Generic types are erased at runtime, so T is unknown inside a normal function. Because an inline function is copied into each call site, the compiler can put the real type there, which allows is T and T::class.
↳ 3When should you not mark a function inline?
Show answers
Junior answer
When it is big.
Senior answer
When it takes no lambda, when the body is large, or when it's called from many places: every call site gets a copy and the bytecode grows. noinline and crossinline handle lambdas that are stored or called from another context.
How did you do?QWhat is the difference between a List chain and a Sequence chain?
Show answers
Junior answer
Sequences are lazy.
Senior answer
A List chain runs each step over the whole list and builds a new list each time. A Sequence passes each element through all the steps before the next element, and only when a terminal operation like toList or first runs.
↳ 1When is a Sequence slower?
Show answers
Junior answer
It's always faster.
Senior answer
On small collections, or chains with one or two steps. The per-element overhead of the lazy machinery costs more than the temporary lists it saves. Sequences win with big inputs, many steps, or an early exit like first.
↳ 2You build a Sequence with map and filter but nothing prints. Why?
Show answers
Junior answer
There's a bug in the lambda.
Senior answer
There's no terminal operation. Intermediate steps on a Sequence only describe the work. Nothing runs until something like toList, first or forEach asks for elements.
How did you do?What does this print?
val result = listOf(1, 2, 3) .asSequence() .map { println("map $it"); it * 2 } .filter { println("filter $it"); it > 2 } .first() println(result)Show output
map 1 filter 2 map 2 filter 4 4A Sequence sends one element through every step before the next. first() stops at the first match, so 3 is never mapped.
How did you do?
Your marks stay in this browser only. "Shaky first" brings the cards you marked shaky or no idea to the top.