Kotlin Backend Developer Top 100 Interview Questions
100 Kotlin backend developer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable code. Covers null safety and the type system, coroutines and structured concurrency, flows, web services, data access, testing, JVM performance and system design, from the basics to production incidents.
Questions 1-20 of 100
Q1.What does a Kotlin backend developer own beyond writing API endpoints?
A Kotlin backend developer owns services from design to production: their APIs, domain model, concurrency, data access, reliability and operations. Endpoints are a thin layer; most of the work is in modeling the domain safely and making the service behave well under load.
Typical responsibilities:
- Domain modeling: data classes, sealed hierarchies and value classes that make invalid states hard to represent.
- Concurrency: coroutines with structured concurrency, cancellation and timeouts.
- API design: request validation, consistent errors and versioning.
- Data access: queries, transactions, connection pools and caching.
- Reliability: retries, circuit breaking, graceful shutdown and health checks.
- Performance: profiling on the JVM, tuning memory and avoiding blocking calls in coroutine code.
- Testing: fast unit tests with fakes, integration tests and tests for coroutine code with virtual time.
- Delivery: builds, container images and safe deployments.
Kotlin's strength is expressing intent concisely and safely, so a strong developer uses the type system to prevent bugs rather than relying only on tests to find them.
Q2.Why do teams choose Kotlin for backend services?
Kotlin combines a concise, safe language with the mature JVM runtime and its huge library ecosystem. Teams get modern language features without giving up proven performance, tooling and operational knowledge.
Main strengths:
- Null safety in the type system, which removes a large class of runtime errors.
- Concise code: data classes, type inference, extension functions and expressive collection operations reduce boilerplate.
- Coroutines: asynchronous code written in a sequential style, with structured concurrency and cancellation.
- Interoperability: existing JVM libraries and frameworks can be used directly, and codebases can migrate file by file.
- Expressive domain modeling: sealed hierarchies and
whenexpressions make business rules explicit and checked by the compiler. - Multiplatform options for sharing code, such as validation rules, with other targets.
Trade-offs:
- Builds can be slower than for some other languages, especially with compiler plugins.
- Powerful features, such as extension functions and DSLs, can make code clever but hard to read if overused.
- Interop with libraries that lack nullability information brings back some null risks.
Q3.How does null safety work in Kotlin?
Kotlin's type system separates references that can hold
nullfrom those that cannot.Stringcan never be null, whileString?can, and the compiler forces you to handle the null case before using a nullable value.Tools for nullable values:
- Safe call
?.:customer?.address?.cityreturns null if any part is null. - Elvis operator
?:: supplies a default or exits early, as inval id = raw ?: return badRequest(). - Smart casts: after
if (name != null), the compiler treatsnameas non-null. let: runs a block only for non-null values,user?.let { send(it) }.- Not-null assertion
!!: throws if the value is null; use it rarely, since it discards the safety.
Where nulls still appear:
- Platform types from JVM libraries without nullability annotations, which the compiler cannot check.
- Deserialized data that is missing fields despite non-null types, depending on the serialization library.
- Late initialization with
lateinit, which throws if read before being set.
Good practice: convert untrusted data into non-null domain types at the boundary, so the rest of the code works with guaranteed values.
Example
Safe calls and the Elvis operator handle every missing piece;
!!throws when its assumption is wrong.data class Address(val city: String?) data class Customer(val name: String, val address: Address?) fun cityOf(customer: Customer?): String = customer?.address?.city ?: "unknown" fun main() { println(cityOf(Customer("c1", Address("Pune")))) // Pune println(cityOf(Customer("c2", null))) // unknown println(cityOf(null)) // unknown val raw: String? = "42" println((raw?.toIntOrNull() ?: 0) + 1) // 43 val missing: String? = null try { missing!!.length } catch (e: NullPointerException) { println("!! threw on null") // !! threw on null } }- Safe call
Q4.What is the difference between `val` and `var`, and what does immutability mean in Kotlin?
valdeclares a reference that cannot be reassigned;vardeclares one that can. Neither says anything about whether the object itself can change: avalholding a mutable list can still have items added.Levels of immutability:
- Read-only references:
valprevents reassignment of the variable. - Read-only interfaces:
List,SetandMaphave no mutating methods, but the underlying collection may still be mutable through another reference, such as aMutableList. - Truly immutable data: data classes with only
valproperties of immutable types, and defensive copies of collections, such astoList().
Good practices:
- Prefer
valeverywhere; usevaronly for local state that genuinely changes, such as a loop accumulator. - Expose read-only types from classes, keeping mutable collections private.
- Copy on input when storing collections passed in by callers, so later changes by the caller do not leak in.
- Use
copyon data classes to create modified versions instead of mutating shared objects.
Immutability makes code easier to reason about and safe to share between coroutines and threads without locks.
- Read-only references:
Q5.How do data classes work in Kotlin?
A data class is a class whose main purpose is holding data. The compiler generates
equals,hashCode,toString,copyand component functions for destructuring, all based on the properties declared in the primary constructor.What you get:
- Value equality: two instances with equal properties are equal, so data classes work well as map keys and in sets.
- Readable
toString:Order(id=7, status=paid, total=1250). copy: creates a modified copy, such asorder.copy(status = "shipped"), leaving the original unchanged.- Destructuring:
val (id, status) = order.
Rules and cautions:
- Only properties in the primary constructor take part in the generated methods.
- Keep properties
val; mutable data classes used as map keys break when they change. copyis shallow: a copied data class shares the same list objects.- Destructuring depends on property order, so reordering constructor parameters silently changes destructured values.
- Data classes cannot be abstract or sealed themselves, but they can implement sealed interfaces.
Typical uses: request and response models, events, query results and value objects.
Example
Equality, readable printing, copying and destructuring all come from the constructor's properties.
data class Order(val id: Int, val status: String, val total: Long) fun main() { val a = Order(7, "paid", 1250) val b = Order(7, "paid", 1250) println(a == b) // true println(a) // Order(id=7, status=paid, total=1250) println(setOf(a, b).size) // 1 val shipped = a.copy(status = "shipped") val (id, status) = shipped println("$id $status ${a.status}") // 7 shipped paid }Q6.How do sealed classes and sealed interfaces model closed hierarchies?
A sealed class or interface restricts which types can extend it: all direct subtypes must be declared in the same module and package. The compiler therefore knows every possible case and can check that a
whenexpression handles them all.Why they are useful in backend code:
- Results with several outcomes: success, validation failure, not found and conflict, each with its own data.
- Domain states: an order that is pending, paid, shipped or cancelled, where each state carries different fields.
- Events and commands in message-driven systems.
Benefits:
- Exhaustive
when: adding a new case makes the compiler flag everywhenthat does not handle it. - Type-specific data: each subtype holds exactly the fields that make sense for it, unlike a single class with many nullable fields.
- Explicit error handling: failures become values in the type signature rather than hidden exceptions.
Choosing a form:
sealed interfacewhen subtypes might also implement other interfaces.data classsubtypes for cases with data, anddata objectfor cases without.
Example
Each payment outcome carries its own data, and the
whenmust cover every case.sealed interface PaymentResult { data class Success(val transactionId: String) : PaymentResult data class Declined(val reason: String) : PaymentResult data object Timeout : PaymentResult } fun message(result: PaymentResult): String = when (result) { is PaymentResult.Success -> "paid: ${result.transactionId}" is PaymentResult.Declined -> "declined: ${result.reason}" PaymentResult.Timeout -> "try again later" } fun main() { listOf(PaymentResult.Success("tx-1"), PaymentResult.Declined("insufficient funds"), PaymentResult.Timeout) .forEach { println(message(it)) } } // Output: // paid: tx-1 // declined: insufficient funds // try again laterQ7.When do you use an enum class and when a sealed hierarchy in Kotlin?
Both describe a closed set of possibilities that the compiler can check exhaustively. An enum has a fixed set of single instances with the same shape; a sealed hierarchy has subtypes that can each carry different data and have many instances.
Enum class Sealed hierarchy A fixed list of constant instances, such as PAIDandSHIPPEDA fixed list of subtypes, each possibly with its own fields Every constant has the same properties Each subtype can hold different data Built-in entries,valueOfand stable names for storageInstances created freely with their own values Best for simple states, categories and codes Best for results, events and states carrying data Practical guidance:
- Store enums by an explicit code property, not by ordinal position, so reordering constants does not corrupt stored data.
- Parse safely with
entries.firstOrNull { it.code == raw }rather thanvalueOf, which throws on unknown values. - Move to a sealed hierarchy when one case starts needing extra data, such as a cancellation reason.
- Put behavior on the type: enums and sealed types can have methods, such as allowed state transitions.
Q8.How do `when` expressions and smart casts work in Kotlin?
whenis an expression that picks the first matching branch and returns its value. It can match constants, ranges, types and arbitrary conditions. After a type check such asis String, the compiler smart-casts the value, so the branch can use it as that type without an explicit cast.Forms of
when:- With a subject:
when (status) { "paid" -> ...; "shipped", "delivered" -> ...; else -> ... }. - Type checks:
is Int ->with a smart cast inside the branch. - Ranges:
in 1..10 ->. - Without a subject: each branch is a Boolean condition, replacing long
if-elsechains.
Exhaustiveness: when the subject is an enum, a sealed type or a Boolean, the compiler requires every case to be covered if
whenis used as an expression; this turns new cases into compile errors rather than runtime surprises.Smart cast limits: they work only when the compiler can prove the value has not changed since the check, so they apply to local
vals and immutable properties, not tovarproperties that another thread could modify.Example
Type checks smart-cast the value inside each branch, and a subject-less
whenreplaces anifchain.fun describe(x: Any?): String = when (x) { null -> "nothing" is Int -> if (x > 100) "large number" else "number $x" is String -> "text of ${x.length} chars" is List<*> -> "list of ${x.size}" else -> "other" } fun shippingBand(kg: Double): String = when { kg <= 1 -> "light" kg <= 10 -> "standard" else -> "heavy" } fun main() { listOf(null, 7, 500, "hello", listOf(1, 2), 2.5).forEach { println(describe(it)) } println(shippingBand(4.5)) } // Output: // nothing // number 7 // large number // text of 5 chars // list of 2 // other // standard- With a subject:
Q9.How do extension functions work in Kotlin, and when should you use them?
An extension function adds a function to an existing type without changing or inheriting from it, such as
fun String.toSlug(). The compiler turns it into a static function that takes the receiver as its first parameter, so it can only use the type's public members.What extensions are good for:
- Readable helpers on types you do not own, such as strings, collections and library types.
- Domain conversions, such as
fun OrderEntity.toResponse(), keeping mapping code out of the entity. - Scoped utilities: extensions declared inside a class or file are visible only there.
- Extension properties, such as
val Long.asAmount: String get() = ..., for computed values.
Rules to remember:
- Extensions are resolved statically, by the declared type, not the runtime type, so they do not override member functions.
- A member function always wins over an extension with the same signature.
- Nullable receivers are allowed, as in
fun String?.orUnknown().
Avoid: large numbers of extensions on very common types, such as
AnyorString, which clutter autocompletion, and extensions that hide expensive work behind what looks like a simple property.Example
Extensions add a slug helper to
String, a formatting property toLongand a safe accessor to lists.fun String.toSlug(): String = lowercase().trim().replace(Regex("""[^\p{L}\p{N}]+"""), "-").trim('-') val Long.asAmount: String get() = "${this / 100}.${(this % 100).toString().padStart(2, '0')}" fun <T> List<T>.secondOrNull(): T? = getOrNull(1) fun main() { println("Senior Backend Developer (Remote)!".toSlug()) // senior-backend-developer-remote println(125050L.asAmount) // 1250.50 println(listOf("a").secondOrNull()) // null }Q10.How do higher-order functions and lambdas work in Kotlin?
A higher-order function takes functions as parameters or returns them. Lambdas, written
{ x -> x * 2 }, are function values that can capture variables from their surrounding scope. Function types, such as(Long) -> Long, describe them precisely.Syntax features:
- Trailing lambdas:
orders.filter { it.total > 1000 }puts the lambda outside the parentheses. itnames the single parameter of a lambda when not declared.- Function references:
::isValidorString::lengthpass existing functions. - Lambdas with receivers:
StringBuilder.() -> Unitlets the lambda call the receiver's members directly, which is how builders and DSLs work.
Closures: lambdas can read and modify captured local variables. This is convenient, but shared mutable captures are not thread-safe when lambdas run on other threads or coroutines.
Performance: non-inline higher-order functions allocate a function object for each capturing lambda. Standard collection functions are
inline, so their lambdas cost nothing extra; mark your own small higher-order functionsinlinewhen they are on hot paths.Uses in backend code: collection processing, retry and transaction helpers that take a block, configuration builders and strategies passed into services.
- Trailing lambdas:
Q11.What do inline functions and reified type parameters do in Kotlin?
An
inlinefunction's body, including the lambdas passed to it, is copied into each call site by the compiler. This removes the cost of allocating function objects for lambdas and allows two special features: non-local returns and reified type parameters.Non-local returns: inside a lambda passed to an inline function,
returnexits the enclosing function, which makes code likeforEach { if (it < 0) return }behave like a loop.Reified type parameters: normally, generic types are erased at run time, so
Tcannot be checked. In an inline function,reified Tkeeps the type, so you can writeit is T,T::classorfilterIsInstance<T>().When to use
inline:- Small higher-order functions called frequently, such as timing, locking or transaction helpers.
- Functions that need reified types, such as typed parsing or lookups.
When not to:
- Large functions, since inlining copies the body everywhere and increases code size.
- Functions without lambda parameters or reified types, where inlining gains nothing.
crossinlineandnoinlinemodifiers control lambdas that must not use non-local returns or must be stored.Example
A reified type parameter filters by type at run time, and a non-local return exits the outer function from inside
forEach.inline fun <reified T> List<Any>.ofType(): List<T> = filter { it is T }.map { it as T } fun firstPositive(values: List<Int>): Int? { values.forEach { if (it > 0) return it } return null } fun main() { val mixed: List<Any> = listOf(1, "a", 2.5, "b", 3) println(mixed.ofType<String>()) // [a, b] println(mixed.ofType<Int>().sum()) // 4 println(firstPositive(listOf(-1, 0, 3, 5))) // 3 }Q12.What do the scope functions `let`, `run`, `with`, `apply` and `also` do?
Scope functions run a block of code in the context of an object. They differ in how the block refers to the object, as
thisorit, and in what they return, the object itself or the block's result.letrunwithObject is it; returns the block's resultObject is this; returns the block's resultObject is this, passed as an argument; returns the block's resultNull checks: user?.let { send(it) }Compute a value from an object's members Group several calls on one object applyalsoObject is this; returns the objectObject is it; returns the objectConfigure a new object: Config().apply { port = 8080 }Side effects in a chain, such as logging Guidelines:
- Pick by intent:
applyto configure,letfor nullable values and transformations,alsofor side effects,runandwithto compute results. - Avoid nesting scope functions; nested
itandthisbecome confusing quickly. - Do not use them just to save a line; a plain local variable is often clearer.
Example
Each scope function used for its typical purpose.
data class Config(var host: String = "", var port: Int = 0) fun main() { val config = Config().apply { host = "db.internal" port = 6000 } val address = with(config) { "$host:$port" } val length = "order-42".let { it.length } val user: String? = "u7" user?.let { println("loading $it") } val ids = mutableListOf(1, 2).also { println("created with ${it.size} items") } val total = ids.run { sum() * 10 } println("$address $length $total") } // Output: // loading u7 // created with 2 items // db.internal:6000 8 30- Pick by intent:
Q13.How do read-only and mutable collections differ in Kotlin, and which operations matter most?
Kotlin separates collection interfaces into read-only ones,
List,SetandMap, and mutable ones,MutableList,MutableSetandMutableMap. Functions that only read data accept the read-only types, which documents intent and prevents accidental changes.Read-only interfaces Mutable interfaces List<T>,Set<T>,Map<K, V>MutableList<T>,MutableSet<T>,MutableMap<K, V>Expose only reading operations Add add,remove,putand similar operationsCreated with listOf,setOf,mapOfCreated with mutableListOf,mutableSetOf,mutableMapOfBest for parameters, return values and shared data Best for local building, then exposed as read-only Read-only is not immutable: a
Listreference may point to a list that someone else modifies; copy withtoList()when you need a stable snapshot.Operations worth knowing:
map,filter,flatMapandfoldfor transformations.groupBy,associateByandpartitionfor organizing data.sumOf,maxByOrNullandcountfor aggregation.windowedandchunkedfor batches and sliding windows.buildListandbuildMapto construct a collection with mutable operations, then get a read-only result.
Example
Grouping, partitioning and aggregation on a read-only list of orders.
data class Order(val region: String, val total: Long) fun main() { val orders = listOf(Order("north", 900), Order("south", 7400), Order("north", 7650)) println(orders.groupBy { it.region }.mapValues { (_, list) -> list.sumOf { it.total } }) // {north=8550, south=7400} val (large, small) = orders.partition { it.total > 1000 } println("${large.size} large, ${small.size} small") // 2 large, 1 small println(orders.maxBy { it.total }.region) // north println(orders.map { it.total }.chunked(2)) // [[900, 7400], [7650]] }Q14.How do sequences differ from collections in Kotlin?
Collection operations are eager: each step, such as
maporfilter, processes every element and builds a new intermediate list. Sequence operations are lazy: elements flow through the whole chain one at a time, and processing stops as soon as the result is known.Collections Sequences Each operation runs over all elements and creates a new list Elements pass through all operations one at a time Fast for small and medium data with few steps Better for long chains, large data or early termination Results are ready immediately Nothing runs until a terminal operation, such as toListorfirstCan be iterated many times Some sequences can be iterated only once When to use sequences:
- Large inputs with several chained steps, to avoid big intermediate lists.
- Searches that can stop early, such as
first { ... }. - Generated or infinite data, with
generateSequenceor thesequence { yield(...) }builder. - Reading lines from large files lazily.
When not to: small collections, where the overhead of laziness outweighs the benefit, and code that needs random access or sizes.
Example
The lazy version stops after six elements; the eager version maps all thousand before searching.
fun main() { var lazyCount = 0 val lazyResult = (1..1_000).asSequence().map { lazyCount++; it * 2 }.first { it > 10 } println("$lazyResult after $lazyCount mappings") // 12 after 6 mappings var eagerCount = 0 val eagerResult = (1..1_000).map { eagerCount++; it * 2 }.first { it > 10 } println("$eagerResult after $eagerCount mappings") // 12 after 1000 mappings }Q15.How do generics and variance work in Kotlin?
Generics let classes and functions work with many types safely. Variance describes how subtyping of type arguments relates to subtyping of the generic type: whether a
List<Int>can be used where aList<Number>is expected.Declaration-site variance:
out T(covariant): the type only producesT, soSource<Int>can be used asSource<Number>. Kotlin's read-onlyList<out E>works this way.in T(contravariant): the type only consumesT, so aComparator<Any>can be used as aComparator<String>.- Invariant by default:
MutableList<Int>is not aMutableList<Number>, because adding aDoublewould break it.
Use-site variance:
fun copy(from: Array<out Number>)restricts a parameter at the point of use when the declaration is invariant.Star projection:
List<*>means a list of some unknown type; you can read elements asAny?but cannot add any.Practical rules:
- Mark interfaces that only return
Tasout, and those that only acceptTasin. - Use
reifiedtype parameters in inline functions when you need the type at run time. - Keep generic signatures simple; complex bounds make APIs hard to use.
Q16.How do object declarations and companion objects work in Kotlin?
An
objectdeclaration defines a class and its single instance at once, created lazily and thread-safely on first access. Acompanion objectis such an object nested in a class, holding members that belong to the class rather than to instances, similar to static members in other languages.Typical uses of
object:- Stateless utilities and constants.
- Implementations of interfaces that need only one instance, such as a comparator.
data objectfor cases without data in sealed hierarchies.
Typical uses of
companion object:- Factory functions with names, such as
Money.of("12.50"), often alongside a private constructor that enforces validation. - Constants related to the class, with
const valfor compile-time values. - Implementing interfaces on the companion, so the class itself can act as a factory.
Cautions for backend code:
- Singletons with mutable state are shared across every request and coroutine, so they need synchronization and are hard to replace in tests.
- Prefer dependency injection for anything with side effects, such as clients or repositories, and keep
objectfor pure, stateless code.
Q17.How do class delegation and delegated properties work in Kotlin?
Delegation hands work to another object automatically. Class delegation,
class LoggingRepo(inner: Repo) : Repo by inner, implements an interface by forwarding every method toinner, so you override only the methods you want to change. Delegated properties,val x by something, move a property's getter and setter logic into a reusable delegate.Built-in property delegates:
lazy: computes the value on first access and caches it, thread-safely by default.Delegates.observable: runs a callback after each change.Delegates.vetoable: can reject a change.- Map delegation:
val name: String by mapreads the value from a map, useful for dynamic data.
Why class delegation is useful: it is the decorator pattern without boilerplate, such as adding logging, caching or metrics around a repository interface while keeping the original implementation untouched.
Cautions:
lazyvalues that throw during initialization will retry on the next access; make initialization safe or handle the failure.- Delegation forwards to a fixed instance chosen at construction.
Example
A lazy property computes once, an observable property reports changes and class delegation decorates one method.
import kotlin.properties.Delegates interface Notifier { fun send(message: String): String fun channel(): String } class EmailNotifier : Notifier { override fun send(message: String) = "email: $message" override fun channel() = "email" } class LoggingNotifier(private val inner: Notifier) : Notifier by inner { override fun send(message: String) = "[logged] " + inner.send(message) } class Connection { val handle by lazy { println("connecting"); "conn-1" } var status by Delegates.observable("new") { _, old, new -> println("status $old -> $new") } } fun main() { val c = Connection() println(c.handle) println(c.handle) c.status = "open" val notifier = LoggingNotifier(EmailNotifier()) println(notifier.send("hi") + " via " + notifier.channel()) } // Output: // connecting // conn-1 // conn-1 // status new -> open // [logged] email: hi via emailQ18.How do value classes help with type safety and performance in Kotlin?
A value class wraps a single value, such as an ID or an email address, in its own type. At run time, the compiler usually represents it as the underlying value, so you get distinct types without the cost of allocating wrapper objects.
Benefits:
- No mix-ups: a function taking
OrderIdcannot be called with aCustomerId, even though both wrap strings. - Validation in one place: a factory function parses and validates the raw value, so every instance is valid.
- Readable signatures:
fun refund(order: OrderId, amount: Money)documents itself. - Little overhead in most uses, since no extra object is created.
Limits:
- A value class holds exactly one property.
- It is boxed into a real object in some situations, such as when used as a generic type argument or a nullable type.
- Equality is based on the wrapped value.
- Serialization and database libraries may need configuration to handle them.
Good candidates: identifiers, email addresses, currency codes, percentages and amounts in minor units.
Example
Distinct ID types prevent mix-ups, and a private constructor forces validation through a factory.
@JvmInline value class OrderId(val value: String) @JvmInline value class Email private constructor(val value: String) { companion object { fun parse(raw: String): Email? = raw.trim().lowercase().takeIf { "@" in it }?.let(::Email) } } fun load(order: OrderId): String = "loading ${order.value}" fun main() { println(load(OrderId("ord-7"))) // loading ord-7 println(Email.parse(" Buyer@Example.com ")?.value) // buyer@example.com println(Email.parse("not-an-email")) // null }Calling
loadwith any other type, such as a plain string or a customer ID class, does not compile.- No mix-ups: a function taking
Q19.How does error handling work in Kotlin without checked exceptions?
Kotlin has no checked exceptions: the compiler never forces callers to catch or declare them. Errors are handled either with exceptions for unexpected failures, or with return types that make expected failures explicit, such as nullable results, sealed result types or the standard
Resulttype.Choosing an approach:
- Exceptions for programming errors and failures the caller cannot handle, such as a broken database connection.
- Nullable returns for simple "not found" cases, such as
findByIdreturningOrder?. - Sealed result types for business outcomes with several failure kinds, such as validation errors and conflicts.
ResultandrunCatchingto capture exceptions as values, withmap,getOrElseandonFailure.
Helpers for validation:
requirethrowsIllegalArgumentExceptionfor bad arguments,checkthrowsIllegalStateExceptionfor bad state anderror("message")throws immediately.Caution with coroutines:
runCatchingalso catches cancellation exceptions, which breaks cooperative cancellation; rethrow them or catch narrower exception types in suspending code.At API boundaries, convert exceptions and failure results into consistent error responses, and never expose stack traces.
Example
runCatchingturns parsing failures into values,requireadds a business rule andgetOrElsesupplies a default.fun parsePort(raw: String): Result<Int> = runCatching { raw.trim().toInt() } .mapCatching { port -> require(port in 1..65535) { "port out of range: $port" }; port } fun main() { println(parsePort("8080").getOrNull()) // 8080 println(parsePort("eighty").exceptionOrNull()!!::class.simpleName) // NumberFormatException println(parsePort("70000").exceptionOrNull()?.message) // port out of range: 70000 println(parsePort("abc").getOrElse { 80 }) // 80 }Q20.How do operator overloading and conventions work in Kotlin?
Kotlin maps operators to functions with fixed names marked
operator. Writinga + bcallsa.plus(b),a[i]callsget(i),a > bcallscompareToandx in rangecallscontains. Defining these functions lets your own types use natural syntax.Common conventions:
- Arithmetic:
plus,minus,times,div,remand unary operators. - Comparison:
compareTofor<,>,<=and>=. - Indexing:
getandsetfor[]. - Membership and ranges:
containsforin, andrangeTofor... - Invocation:
invokelets an object be called like a function. - Destructuring:
component1,component2and so on.
Good uses:
- Value types such as money, durations and vectors, where
+and comparisons have obvious meaning. - Domain collections, such as a cart with
cart[sku].
Cautions: overload operators only when the meaning is unsurprising.
+on money should refuse to add different currencies rather than silently convert, and operators should not have side effects on their operands.- Arithmetic: