Side-by-side, interactive cheatsheets for Kotlin programmers
comparing Kotlin to other languages. Every example runs live in your browser — no
setup, no installation.
Choose your own path by reordering languages
Radical simplicity, and the concurrency your coroutines were modeled on. Go deletes most of Kotlin's expressiveness on purpose — no exceptions, no null safety, no inheritance, no default arguments, if is a statement — and offers in exchange a language you can hold in your head, plus the goroutines and channels that Kotlin's launch, Channel, and select pay homage to.
go func() is launch { } unstructured — no scope owns a goroutine, and sync.WaitGroup does by hand what structured concurrency does automaticallyChannel and select came from Go — send, receive, close, range-until-closed translate line for line(value, error) and every caller writes if err != nilT?: nil is back and unchecked — the comma-ok idiom (value, ok := …) is the closest thing to ?.The compiler is gone. Python trades away everything Kotlin's type system buys you — null safety, exhaustive when, a typo caught at build time — for a language that says what it means in a third of the lines. Type hints look just like your annotations and are enforced by nobody; the safety net exists, but you install it yourself.
AttributeError at runtime, not a compile errordef double(number: int) happily accepts a string; mypy is an external, opt-in checkerT?, no ?., no ?:, no smart casts — None is unguarded null, and every safe-call chain gets written out by handmap/filter chains; range() is half-open where Kotlin's .. is inclusive@dataclass is data class, match/case is when (a statement, non-exhaustive), decorators are the thing Kotlin has no answer forasyncio is coroutines minus the threads — one event loop, a GIL, and no structured concurrency until TaskGroupThe Android-to-Flutter move, and the friendliest target Kotlin has. Null safety with ?, !, ?? and ?.; $ string interpolation; named arguments; expression bodies — the first day is genuinely easy. What is hard is structural: no data classes, no it, no varargs, and a concurrency model with no shared memory at all.
data class: ==, hashCode, toString and copyWith are hand-written, which is why every real codebase runs the freezed code generatorswitch match when — and object, list, and map patterns destructure while they match, which Kotlin cannot doif and collection-for build lists declaratively inside the literal, which is how every Flutter children: list is writtenasync/await never runs in parallel, and CPU work goes to an isolate that shares no memory and copies its arguments@Composable function becomes a Widget class with a build method, and remember { mutableStateOf } becomes a State object plus an explicit setStateWhat the JVM was hiding, made explicit. Rust's type system will feel like Kotlin's taken seriously — val/var is let/let mut, T? is Option, sealed classes are enums, when is match — but the garbage collector is gone, replaced by ownership rules the compiler enforces at build time.
& lends it, and the aliased mutation Kotlin allows freely is a compile errorT? becomes Option<T> — ?. is .map(), ?: is .unwrap_or(), !! is .unwrap(), and ?: return is let-elsedata class is struct plus an explicit #[derive(…)] listResult<T, E> with the ? operator; panic! exists but is for bugs, not control flowasSequence()) — and ranges flip: Kotlin's 1..5 is Rust's 1..=5Send/Sync encode thread safety in the types, and shared mutable state requires Arc<Mutex<…>> to even compileYour twin on the other phone. Kotlin and Swift converged on the same modern answers — val/var is let/var, T? optionals on both sides, sealed classes are enums with associated values, when is switch, trailing lambdas everywhere — so this page is mostly about the two real differences: structs (and arrays!) copy on assignment, and ARC replaces the garbage collector with deterministic deinit.
mutating, and a let struct is deeply frozen — the inverse of data-class reference semantics?. and ?? for ?. and ?:, if let for smart casts, and guard let as a first-class ?: returnthrows in the signature, try at the call, try? to fold failures into optionalssuspend is async with visible await; coroutineScope is withTaskGroup; and deinit fires deterministically where the JVM offers only use { }Strong inference on both sides — but the types are erased, structural, and gradual. TypeScript will feel familiar (?. and ?? work like home, chains are chains, async/await maps to coroutines) until the type system inverts your JVM instincts: shape is identity, nothing exists at runtime, and any is a legal lie Kotlin never offers.
JSON.parse returns unverified any, and zod-style validators guard the boundaries?. and ?? as at home, smart casts as "narrowing" — but there are TWO absences, null and undefinednumber | string inline, discriminated unions with a never-based exhaustiveness check for whenasync/await is suspend on ONE thread — no Dispatchers, no blocking, no parallelism without workers; Promise.all plays awaitAll minus cancellationPartial, keyof, Pick) compute types from types — the corner where TypeScript out-expresses Kotlin's generics entirelySame JVM, maximum paradigm distance. No types, no classes, no methods, no statements, and no mutation. What is left is functions and data — and the bet Clojure makes is that it is enough, that generic operations over plain maps beat a type per shape, and that a language you can extend yourself beats one you wait on.
assoc (which is copy()), and every library function already knows how to work with themsynchronized, no lock, no defensive copy — the data-race category simply does not arisedefmacro means ->, when, cond and defn are library code; a feature Kotlin needs a compiler release for, you can write this afternoonwhennil is safe almost everywhere ((:name nil) is nil, (count nil) is 0), so the ?. operator is mostly unnecessary — until you touch JavaThe sibling language — which is exactly what makes it dangerous. C# has null-safe types, properties, records, extension methods, and async/await, so almost everything looks like home. The false friends are where it bites: var means inference (not mutability), sealed means final (not a closed hierarchy), and T? is a warning the runtime never enforces.
string? is a warning, ! emits no runtime check at all, and null still reaches your non-nullable fieldsvar is the type-inference keyword — every local is mutable, and there is no val for locals anywhere in the languagetypeof(T) and is T just work with no inline/reified, and List<int> does not boxstruct gives you real user-defined value types, copied on assignment — far beyond @JvmInline value classmap → Select, filter → Where, fold → Aggregate) and is lazy by defaultTask is hot, no coroutineScope owns it, and CancellationToken is threaded by handThe language Kotlin was built to replace — and to live beside. Same bytecode, same libraries, same garbage collector, and none of the syntax you learned Kotlin for. Everything Kotlin removed comes back: getters, constructors, overloads, throws clauses, and a class wrapped around every function. What returns with it is a language that has quietly caught up in three places, and stands still everywhere else.
Optional is a library band-aid for return values, and everything else is a NullPointerException waiting== means reference identity again, and the Integer cache makes that silently correct up to 127 and wrong at 128switch are data classes and exhaustive when, feature for feature — the pages that read almost like Kotlinapply becomes a local variable and a run of assignmentsthrows clauseFunctional-first purity where Kotlin blends pragmatically. Haskell takes the ideas Kotlin borrowed — sealed types, nullability tracking, lambdas, inference — to their logical conclusion: effects live only in IO and the signature says so, everything is lazy, nothing mutates, and interfaces become type classes the compiler resolves by inference.
Int -> Int provably cannot println, mutate, or launch anything — effects appear in the type (IO Int) or not at allSequence is Haskell's everything, and infinite lists like [0, 2 ..] are ordinary valuesT? is Maybe, exceptions are Either, and ?.let chains are do blocks that stop at the first Nothingadd 10 IS the partially applied function, and operator sections like (* 2) replace { it * 2 }data class unbundles into deriving (Show, Eq, Ord), sealed classes into one data declaration, value class into newtypefmap is one map for lists, Maybe, and IO alikeThe other serious JVM language, and the sharpest contrast available on the same runtime. Kotlin borrowed val/var, data classes, companion objects, and declaration-site variance from here — then stopped. What it left behind is the half that makes Scala a different language: implicits, higher-kinded types, and one for-comprehension that works over every container you have.
F[_]) let you abstract over List, Option and Future themselves; Kotlin cannot even express the signaturefor … yield is sugar for flatMap, so the same syntax chains fallible steps over Option, Either, Try, List and Futurenull and no ?. — absence is a value (Option[T]) that composes, not a compiler check that is erasedunapply can be a pattern, so you match on parsed integers and JSON shapes, not just types and constantsThe concurrency contrast Go did not fully cover. A coroutine is a cheap unit of scheduling over shared memory; a BEAM process is a cheap unit of isolation — its own heap, its own garbage collector, reachable only by message. The data race is not mitigated here, it is structurally impossible. And when something breaks, nobody catches it: a supervisor restarts it.
@Volatile, no Mutex — a message send copies the value, and that copy is what makes the isolation realDispatchers.IO and no yield() to remembercoroutineScope makes one child's failure cancel its siblings; a supervisor restarts the dead child from a known-good state, with no rescue anywhere= is pattern matching, not assignment — and that one idea reshapes function heads, case, with, and receive alike|> replaces the dot chain{:ok, value} / {:error, reason} replaces both nullable types and exceptions — a failure is a value that carries whyEvery guarantee traded for expressiveness. No compiler, no types, no null safety, no exhaustive when — and in return, a language where classes are open, blocks are everywhere, and methods can be written at runtime. Kotlin protects you from yourself; Ruby hands you the keys and trusts you.
NoMethodError when that line runs — the test suite is the type checkernil has methods, classes are objects, and 3.times { } is a method on the integerString or Integer and redefine anything, globally; extension functions with none of the guardrailsyield are the whole language, and Enumerable is the stdlib you know (filter → select, fold → reduce, &:upcase)<=>, include Comparable, get every operator freedefine_method and method_missing are how attr_accessor and Rails work