Kotlin: Modern JVM Language Fundamentals
Kotlin: Modern JVM Language Fundamentals
Up to 20 questions, shuffled on every run
Kotlin reads like a friendlier Java for about a week, and then you meet the parts that are genuinely different: nullability living in the type system, coroutines with a scope that owns its children, and extension functions that look like methods but do not dispatch like them.
This quiz targets those. Most questions are about a distinction that compiles either way, which is what makes them worth knowing: the wrong choice does not fail, it quietly does something else.
Several questions are really about production behaviour rather than syntax, and one in particular is worth seeing laid out, because it is where the coroutine model meets a blocking JDBC driver.
Read the explanations even on the ones you get right. Most name the specific failure mode rather than restating the answer, and the failure mode is the reason the distinction exists at all.
Answer key and explanations14 questions
The quiz above draws 14 questions at random from these 14, so a second attempt will not be the same run. Everything in the pool is listed here.
Why does `s.length` fail to compile when `s` is declared as `String?`
Answer`String?` is a different type from `String`, and the compiler will not allow member access until null is handled
Nullability is part of the type. `String` and `String?` are distinct types, and the compiler refuses any member access on the nullable one until you have handled the null case. That is what makes Kotlin null safety a compile-time guarantee rather than a convention, and it is why `?.`, `?:` and smart casts exist.
What does `!!` actually do?
AnswerAsserts the value is non-null, throwing a KotlinNullPointerException if it is not
It asserts non-null and throws `KotlinNullPointerException` if the value is null. Its legitimate use is narrow: you know something the compiler cannot prove and you would rather crash than proceed. Using it to silence a compile error throws away the guarantee the type system was giving you, and turns a compile-time error into a production crash.
What is a platform type, written `String!` in IDE hints?
AnswerA type from Java with unknown nullability, which Kotlin neither enforces nor rejects
A type coming from Java where nullability is unknown. The compiler cannot prove it is non-null and will not force you to treat it as nullable, so it trusts you. This is the main route by which NullPointerExceptions still enter a Kotlin codebase, and the fix is to annotate the Java side or wrap the boundary in a nullable Kotlin type immediately.
Which members does `data class User(val id: String, val name: String)` generate?
Answerequals, hashCode, toString, copy, and componentN, from the constructor properties only
The compiler generates `equals`, `hashCode`, `toString`, `copy` and `componentN` functions for destructuring. They are derived only from properties declared in the primary constructor. A property declared in the class body is excluded from equals and copy, which produces two objects that compare equal despite differing state.
What is the difference between `val` and `const val`?
Answer`const val` is a compile-time constant inlined at use sites; `val` is assigned at runtime
`val` is a read-only reference assigned once at runtime, so it can be initialised from a function call. `const val` is a compile-time constant, must be a primitive or String, must be top level or in an object, and is inlined at every use site. Inlining matters for library authors: changing a `const val` requires recompiling consumers.
What does `coroutineScope { }` guarantee that plain `launch` on a global scope does not?
AnswerIt waits for all children, and cancels the siblings if one fails
It will not return until every child coroutine has completed, and if any child fails it cancels the remaining siblings and rethrows to the caller. That is structured concurrency: work cannot outlive the scope that started it, and a failure cannot be silently orphaned. `GlobalScope.launch` gives you neither, which is why it is discouraged outside application-lifetime tasks.
How do `launch` and `async` differ in how an exception surfaces?
Answer`launch` propagates immediately; `async` holds the exception until `await()` is called
`launch` returns a `Job` and propagates an exception to the parent scope as soon as it happens. `async` returns a `Deferred` and holds the exception until you call `await()`. So a failing `async` that is never awaited can appear to do nothing until the surrounding scope cancels, which is the most misunderstood behaviour in the coroutines API.
Does marking a function `suspend` make a blocking JDBC call non-blocking?
AnswerNo. Wrap it in withContext(Dispatchers.IO), or use a non-blocking driver
No. `suspend` allows a function to pause and release its thread at suspension points, but a blocking call has no suspension point: it holds the thread until it returns. On `Dispatchers.Default`, sized to roughly one thread per core, a few concurrent blocking queries starve every other coroutine on that dispatcher. Wrap it in `withContext(Dispatchers.IO)`, whose pool is elastic, or use a non-blocking driver.
What is the difference between `apply` and `let`?
Answer`apply` returns the receiver; `let` returns the lambda result
`apply` runs a block with the object as `this` and returns the object, so it is for configuring something. `let` passes the object as `it` and returns the block result, so it is for transforming. Choosing wrong compiles fine and returns the wrong value, which makes it a quiet bug. `also` is `apply` with `it`; `run` is `let` with `this`.
What does a `sealed` class give you that an abstract class does not?
AnswerA closed set of subclasses, so `when` can be checked for exhaustiveness without an else
The set of direct subclasses is known at compile time and restricted to the same module and package, so the compiler can verify a `when` covers every case. That makes the `else` branch unnecessary, and it means adding a new subclass turns every exhaustive `when` into a compile error rather than a silent fallthrough. It is the mechanism behind modelling results and states safely.
How is an extension function dispatched?
AnswerStatically, by the declared type; it is compiled to a static method and cannot be overridden
Statically, based on the declared type of the expression, because an extension is compiled to a static method taking the receiver as its first parameter. It is not a member of the class and cannot be overridden. So if a base and a derived type both have an extension of the same name, the one chosen depends on the declared type of the variable, not the runtime object.
What is `lateinit var` for, and what is its main limitation?
AnswerDeferring initialisation of a non-null property; it cannot be used with primitives and throws if read too early
It defers initialisation of a non-null property so you do not need a nullable type or a dummy value, which suits dependency injection and test setup. It cannot be used with primitive types, it must be a `var`, and reading it before assignment throws `UninitializedPropertyAccessException`. `by lazy` is the better tool when the value can be computed on first access.
Why does an `object` declaration differ from a class with a companion object?
AnswerAn `object` is a standalone singleton; a companion object is a singleton belonging to a class
An `object` declaration is a singleton: the declaration and the sole instance are the same thing, initialised lazily and thread-safely on first access. A companion object is a singleton *attached to* a class, giving it something like static members while still being a real object that can implement interfaces. Use `object` for a standalone singleton, a companion for factory methods and constants belonging to a type.
What does `@JvmStatic` change when Kotlin code is called from Java?
AnswerIt emits a real static method, so Java can call it without going through `Companion`
It generates a genuine static method so Java can call `MyClass.create()` instead of `MyClass.Companion.create()`. Without it the companion is a real object and Java must go through the `Companion` field. This is one of a family of interop annotations, alongside `@JvmOverloads` for default arguments and `@JvmName` for renaming, which exist because Kotlin features have no direct Java equivalent.








