Week 1 | Lesson 1

Kotlin Essentials

History, Syntax, Types, Operators, Conditionals



© 2026 by Monika Protivová

Learning to Code in the Age of AI

Why Are You Here?

ChatGPT can write "Hello World" in Kotlin in 2 seconds. So why spend 3 weeks learning to code?

What AI changed

  • AI can generate code from natural language descriptions
  • AI can explain code, fix bugs, and write tests
  • AI can produce working solutions in seconds
  • AI is getting better every month

So... is learning to code pointless?

What AI did NOT change

  • Generating code and understanding code are different skills
  • AI produces code that looks correct but often has subtle flaws
  • AI cannot design systems -- only individual components
  • AI does not know your users, your constraints, or your business
  • Someone must evaluate, test, and take responsibility for the code

That someone is you.

❗ ️The New Role of a Developer: You are not here to learn to type code faster than AI. You are here to learn to think, design, evaluate, and decide. AI is your power tool. Your judgment is what makes it useful.

Five Principles for the AI Era

These principles will guide everything we do in this course.
  1. Understand before you generate.
    You must be able to read and evaluate code before asking AI to write it. If you can't spot a bug in AI output, you can't safely use AI.
  2. Design first, implement second.
    The most valuable skill is deciding WHAT to build and HOW to structure it. AI is good at implementation, poor at design.
  3. Every line of code has a cost.
    AI generates code freely, creating a new danger: unnecessary complexity, over-engineering, and maintenance burden.
  4. Context is everything AI lacks.
    AI doesn't know your team, deadlines, users, or production constraints. Supplying and reasoning about context is your irreplaceable value.
  5. Verify, don't trust.
    AI output is a draft, never a final product. The professional workflow is: specify, generate, review, test, refine.

The Three Hats

Throughout this course, you will practice switching between three modes of thinking.

The Learner

Understanding concepts deeply so you can evaluate AI output.

  • "I need to understand what polymorphism IS before I can judge whether AI used it correctly."
  • Dominant in Week 1


The Architect

Making design decisions that AI cannot.

  • "Should this be a class or an interface? What are the trade-offs?"
  • Dominant in Week 2

The Reviewer

Critically evaluating code regardless of who (or what) wrote it.

  • "This code works, but is it right for this system? Is it secure? Maintainable?"
  • Dominant in Week 3


💡 ️ These hats are not sequential -- you will wear all three throughout the course. But the emphasis shifts as your skills grow.

Why Kotlin?

Why Kotlin?

Java has been the dominant language for backend development for many years.
Kotlin is considered a modern alternative to Java.

Kotlin is fully interoperable with Java, which means that you develop and deploy Kotlin applications in the same way as Java applications, and you can use existing Java libraries and frameworks with Kotlin.

Both Java and Kotlin are a solid choice for backend development because of their design:

  • Statically typed languages, which means developers can catch many errors at compile time.
  • Run on the Java Virtual Machine (JVM), which means that they can run on any platform that supports it.
  • Can be containerized easily and is easy to deploy and scale in cloud environments.
  • Have a large ecosystem of libraries and frameworks that can be used to build applications.
  • Have a strong community and support from major companies (e.g., Google for Android, JetBrains for Kotlin).
  • There is some performance overhead with JVM, but it is generally negligible for most applications and JVM can be tuned for performance.

What we will learn in this course is not specific to Kotlin and can be applied to any backend language.

Also, an honest answer is that it is the language I know best, and therefore I can teach it best.

"But won't AI make languages irrelevant?"

There is a popular claim that when AI writes all the code, the programming language won't matter. Let's examine that.

Why languages still matter

  • AI doesn't eliminate the language -- it makes the language's safety features more important
  • When AI generates code, someone must review it. A strong compiler catches mistakes the reviewer might miss.
  • Kotlin's null safety, exhaustive when, and static types act as a free second reviewer on every AI-generated line.
  • Concise syntax means AI generates less code -- fewer lines, fewer places for bugs to hide.

Where Kotlin is used today

  • Android -- official language, ~3 billion devices
  • Backend -- Spring Boot, Ktor (Netflix, Google, Amazon)
  • Multiplatform -- server, mobile, web from one codebase
  • JVM -- 30 years of battle-tested infrastructure
💡 ️ AI generates better Kotlin than most languages because there is less boilerplate. But it still cannot design your system, choose your architecture, or know your production constraints.
❗ ️The real point: OOP, design patterns, testing, architecture -- these skills transcend any language. Kotlin is a particularly good vehicle to learn them, and the compiler will have your back while you do.

How You Will Be Assessed

How You Will Be Assessed

Assessment is based on exercises and assignments completed throughout the course.

Exercises & Assignments

  • Coding Exercises
    Hands-on Kotlin coding tasks completed in LearningKit.
  • Non-Coding Exercises
    Conceptual questions and quizzes to test your understanding.
  • Architectural Assignments
    Design and document system architecture with justified decisions.
  • Code Reviews
    Review given code and identify issues with quality explanations.
  • Coding Projects
    Larger implementation tasks that combine multiple concepts.
  • Discussions
    Live conversations about your design choices and understanding.

Grading

Each exercise and assignment carries points.

Your lesson grade is calculated based on the score you achieve in that lesson's exercises and assignments.

Your final grade is derived from your individual lesson grades across the entire course.

LearningKit

Exercises, Submissions & AI Feedback

LearningKit

A web application for practicing Kotlin through guided exercises with automated testing and AI feedback.

What is it?

  • A platform where you complete Kotlin coding exercises directly in the browser.
  • Each exercise comes with a description, instructions, and hints.
  • Your code is tested automatically against a set of predefined tests.
  • AI-powered feedback helps you improve your solutions before final submission.


How to access

Open LearningKit in your browser and sign in with your GitHub account.

Exercise Workflow

How to work through exercises in the Learning App.

Step by Step

  1. Browse Exercises
    Navigate to the exercise list and pick an exercise for your current lesson.
  2. Write Your Solution
    Use the built-in code editor to write your Kotlin code directly in the browser.
  3. Save Draft
    Save your work at any time. Your code is preserved between sessions.
  4. Test
    Click Test to run the automated tests against your code and see which pass.
  5. Review
    Click Review to get AI feedback on your solution along with test results.
  6. Submit
    When you are happy with your solution, click Submit for final grading by the instructor.

Grading

How your work is evaluated.

Automated Testing

  • Exercises are automatically tested when you click Test or Review.
  • You can see which tests pass and which fail before submitting.


AI Feedback

  • AI reviews highlight areas for improvement in your code.
  • Use the feedback to iterate on your solution before final submission.


Final Grade

  • Final grades are assigned by the instructor after you submit.
  • You can submit multiple times — only the last submission counts.

Development Environment

IDE

What is Integrated Development Environment (IDE)

Being able to efficiently use an IDE is just as important as being able to write code.
Gives you a set of tools that you need as a developer.
    Provides code editor with syntax highlighting
    Easy ways to manage code, project and builds
    Helps your productivity
    • contextual suggestions
    • autocomplete
    • code navigation
    • refactoring
    • language tips

    Helps you avoid some errors in your programs
    • can warn you about problematic code
    • gives you tools inspect your code
    • gives you tools test your code

    Integrates tools
    • source code management, such as git
    • database clients
    • documentation
    • project management tools
    • AI

    Usually has a community led plugin ecosystem

Your first Kotlin program

Getting Started with the Course Repository

Prerequisites

You will need:

  1. A text editor or IDE
    with a modern operating system (Windows, macOS, Linux). Recommended: IntelliJ IDEA.
  2. Git
    for version control. You can download it from https://git-scm.com/downloads
  3. A GitHub account
    for submitting your code and collaborating with others. You can create an account at https://github.com

Your first Kotlin program

Clone the course repository and run your first Kotlin program.

Getting Started

  1. Clone the repository
    Open your terminal and run:
    git clone https://github.com/Monika-Protivova-Education/kotlin-lessons.git
  2. Open the project in IntelliJ IDEA
    Open the cloned folder as a project in your IDE.
  3. Run Main
    Find and run the main function to verify everything works.


Using the Repository

  • Use this project as a playground to experiment with Kotlin code during the course.
  • I will be posting updates to the repo based on what we discuss in class — pull regularly to stay up to date.

Introduction to Java

Brief history of Java

Why do we need to talk about Java first?

While Java and Kotlin are two separate languages, they are closely related. They both run on the Java Virtual Machine (JVM), and they are interoperable. In fact, Kotlin compiles to Java bytecode, which means that it can run on any platform that supports Java. Understanding some basic principles in Java will help you understand Kotlin better.


Java Logoto-rightKotlin Logo

Java has two main components ...

Java Runtime Environment (JRE)

The JRE provides the libraries, the Java Virtual Machine (JVM), and other components needed to run applications written in Java. It does not include developer tools such as compilers and debuggers.

As a user, you would use the JRE to run Java programs on your system.


Java Development Kit (JDK)

The JDK includes the JRE as well as a set of development tools for writing and running Java programs. These tools include the Java compiler (javac), an archiver (jar), a documentation generator (javadoc), and other tools needed in Java development.

Since open-sourcing, multiple implementations of JDK have existed, including Oracle JDK, OpenJDK, Amazon Corretto, and others.

As a programmer, you would use the JDK to develop Java applications and JRE to run them.

Introduction to Kotlin

What is Kotlin?

  • Developed by JetBrains and officially released in 2016.
  • Statically-typed programming language that runs on the Java Virtual Machine (JVM).
  • Fully interoperable with Java, which means that you can use Java libraries in Kotlin and vice versa.
  • Designed to improve on Java's shortcomings, and it is considered a modern alternative to Java.
  • It has modern and intuitive syntax, and it is designed to be concise and expressive.
JetBrains Logo

Important features

  • Null safety by distinguishing nullable and non-nullable types
  • Interoperability with Java, allowing developers to use Java libraries in Kotlin and vice versa
  • Conciseness reducing boilerplate code and improving readability
  • Coroutines provide built-in support for coroutines for easy and efficient concurrent programming
  • Extension Functions allowing you to add new functions to existing classes without modifying their source code
  • Data Classes providing a concise way to create classes that only hold data
  • Higher-Order Functions and Lambdas supporting functional programming paradigms
  • Companion Objects providing a way to create static methods and properties in Kotlin
  • Smart Casts used to automatically casts types when certain conditions are met
  • Sealed Classes providing a way to restrict inheritance

Kotlin Releases: Major Versions

  • 2010: Project Kotlin was born. JetBrains unveiled Project Kotlin, a new language for the JVM, which had been under development for a year.
  • 2012: JetBrains open sourced Project Kotlin. The company has set up a Web demo for the language, and a plugin is already available for IntelliJ IDEA 11.
  • 2016: Kotlin 1.0 was officially released. It was considered stable and ready for production.
  • 2017: Google officially announced Kotlin as a first-class language for Android applications development during Google I/O. This played a crucial role in Kotlin's popularity among Android developers.
  • 2019: Google announced Kotlin as its preferred language for Android app developers, meaning that development tooling would be optimized for Kotlin, and that Kotlin-specific APIs would be prioritized.
  • 2020: Kotlin 1.4 released with focusing on improving the performance and tooling.
  • 2021: Release of Kotlin 1.5.0 with stable language features like JVM records, sealed interfaces and the new default JVM IR compiler.
  • 2021: Kotlin 1.6 was released in November 2021.
  • 2022: Kotlin 1.7 was released in June 2022, including the alpha version of the new Kotlin K2 compiler.
  • 2023: Kotlin 1.8 was released in January 2023. Kotlin 1.9 was released in July 2023.
  • 2024: Kotlin 2.0 was released in May 2024, featuring the new K2 compiler.
  • 2024: Kotlin 2.1 was released in November 2024.

Program Entry Point

Your First Kotlin Program

Program entry point

Program entry point is the first function executed when the program is run.
  • All Kotlin files should have .kt extension, for example MyProgram.kt.
  • In Kotlin, the main program entry point is defined as a top-level function, which means that it is not part of a class.
    fun main() { println("Hello world!") }
  • The main function may accept an array of strings as an argument,which can be used to pass command-line arguments to the program.
    fun main(args: Array<String>) { println("Number of arguments: " + args.size) for (arg in args) { println(arg) } }

Kotlin Data Types

Data types

Java/Kotlin is a high-level programming language with automatic memory management and safe reference handling (unlike C/C++ pointer which are not safe).


Why is this still important for us to understand?



Understanding memory management, garbage collection, and type systems is crucial for writing efficient code, avoiding memory leaks, and preventing issues like data loss from type mismatches.

💡 ️ AI doesn't know the context of what it's building. It doesn't know how many objects will be created, how much memory is available, or how long they'll live. It may use Long where Int suffices, add unnecessary nullable types (forcing heap allocation), or choose data structures that waste memory at scale. If you don't understand types and their memory cost, you can't catch these decisions.

What Are Types?

A type tells the compiler two things: how much memory to reserve and what operations are allowed.

When you write val age: Int = 25, the compiler:

  1. Reserves memory -- allocates the right amount of space (4 bytes for Int)
  2. Stores the value -- writes 25 into that memory
  3. Enforces rules -- only allows operations valid for that type (arithmetic, comparison, etc.)

Choosing the right type matters because it determines how much memory each value consumes and what you can do with it. Multiply that by thousands or millions of objects and the choice becomes significant.

Primitive vs Reference Types

The JVM has two storage mechanisms for values -- and it decides which one to use.

Primitive Types

  • The variable holds the value directly
  • Stored on the stack -- fast, lightweight, fixed size
  • Fixed set: byte, short, int, long, float, double, char, boolean
  • Cannot be null -- there is always a value
val age: Int = 25 // 4 bytes on the stack, holds 25 directly

Reference Types

  • The variable holds a reference (pointer) to an object stored elsewhere (the heap)
  • The object lives on the heap -- managed by the garbage collector
  • Everything else: String, Array, all classes, collections, ...
  • Can be null -- the reference can point to nothing
val name: String = "Alice" // reference on stack, object on heap
💡 ️ In Kotlin you always write Int, Boolean, etc. -- they look like objects. But the Kotlin compiler and JVM decide whether to store them as a fast primitive or a boxed reference. Key factors: nullability (Int can be a primitive, Int? must be boxed), generics (type parameters always require boxing), and collections (List<Int> stores boxed Integers). You can help: prefer non-nullable types, use IntArray over Array<Int>, and avoid unnecessary nullable wrappers.

Kotlin Data Types

In Kotlin, everything looks like an object -- but the compiler is smarter than it seems.

What you write

  • In Kotlin, all types are objects -- you can call methods on any value
  • There is no syntactic difference between Int and String
val number = 42 println(number.toString()) // methods on "primitive"!

What the compiler does

  • The Kotlin compiler converts non-nullable types like Int to JVM primitives (int) for performance
  • Nullable types like Int? must remain as reference types (Integer) -- because primitives cannot be null
💡 ️ This is why Int and Int? are not the same under the hood. Making a type nullable has a real cost -- it forces boxing into a heap object.

Numeric Types

Integer types:

Byte 1 byte whole number from -128 to 127
Short 2 bytes whole number from -32768 to 32767
Int 4 bytes whole number from -2147483648 to 2147483647
Long 8 bytes whole number from -9223372036854775808 to 9223372036854775807

Unsigned Integer types:

UByte 1 byte whole number from 0 to 255
UShort 2 bytes whole number from 0 to 65535
UInt 4 bytes whole number from 0 to 4,294,967,295
ULong 8 bytes whole number from 0 to 18,446,744,073,709,551,615

Floating-point types:

Float 4 bytes fractional number up to 7 decimal digits
Double 8 bytes fractional number up to 15 decimal digits

Numeric Types

Examples

Integer types

val byteValue: Byte = 100 val shortValue: Short = 1000 val intValue: Int = 100000 val longValue: Long = 100000L // L suffix for Long

Unsigned integer types

val uByteValue: UByte = 200u // u suffix for unsigned val uShortValue: UShort = 50000u val uIntValue: UInt = 3000000000u val uLongValue: ULong = 18000000000000000000u

Floating-point types

val floatValue: Float = 3.14f // f suffix for Float val doubleValue: Double = 3.14159265359

Non-numeric Data Types

Boolean 1 byte value of true or false
Char 2 bytes a single 16-bit Unicode character
String approximately 2 bytes per character UTF-16 encoded string of characters
Array depends Fixed number of values of the same type or its subtypes

val charValue: Char = 'A' val stringValue: String = "Hello, Kotlin!" val arrayValue: Array<Int> = arrayOf(1, 2, 3, 4, 5)

Unless there are specific memory performance requirements, you should prefer using Collections over Arrays.

Any type

Any is the root of the Kotlin class hierarchy. This means that all type classes in Kotlin are subclasses of Any.

Anytime we don't know the type of variable, parameter or return type, we can use Any to accept any type. However, this is not recommended and should be used with caution.

Any type is equivalent to Java's Object type.


fun checkType(value: Any) { when (value) { is String -> println("This is a String: $value") is Int -> println("This is an Int: $value") is Boolean -> println("This is a Boolean: $value") else -> println("Unknown type: ${value::class.simpleName}") } } fun main() { checkType(123) }

null

null is a special value that means "no value" or "nothing here".

A variable that is null does not hold any object or data -- it points to nothing.

fun findUser(id: Int): String? { // Imagine looking up a user in a database return if (id == 1) "Alice" else null // no user found } fun main() { val user1 = findUser(1) val user2 = findUser(99) println(user1) // Alice println(user2) // null }

This is useful when a value may legitimately not exist -- for example, a search that finds no result, an optional configuration, or a field that hasn't been set yet.

The Problem with null

null represents the absence of a value. It is one of the most common sources of bugs in programming.

The Problem

  • In Java (and most languages), any reference variable can be null at any time
  • Calling a method on a null reference causes a NullPointerException -- a runtime crash
  • The compiler cannot warn you -- null bugs only appear when the code runs
💡 ️ "I call it my billion-dollar mistake. It was the invention of the null reference in 1965. I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years." -- Tony Hoare, 2009

Kotlin's Solution

  • Types are non-nullable by default -- String can never be null
  • You must explicitly opt in with ? -- String? says "this might be null"
  • The compiler enforces null checks at compile time, not runtime
  • Result: entire categories of bugs are eliminated before the code ever runs

Nullables

Nullables are Kotlin types that can hold a null value.

Any type can be nullable by adding a ? after the type. When working with nullable types, you need to handle null values to avoid NullPointerException.

val nullableNumber: Int? = null val nullableText: String? = null

You can use the ?. operator to safely access properties or methods of nullable types.

nullableNumber?.toString()

You can use the !! operator to tell the compiler that you are sure the value is not null.

nullableNumber!!.toString()

You can also use the ?: elvis operator to provide a default value if the value is null.

val text = nullableText ?: "default"

You can also use a simple if check. The compiler smart-casts the variable to non-null inside the block.

if (nullableText != null) { println(nullableText) }
💡 ️ Null safety is one of Kotlin's biggest advantages over Java -- and one of the things AI-generated code most often gets wrong when converting between the two languages. Pay attention to how ?, !!, and ?: work.

Nullables

Example
fun processString(nullableString: String?) { val length = nullableString?.length ?: 0 // Safe call and Elvis operator println("String length: $length") if (nullableString != null) { // Smart cast to non-null println("String is not null: $nullableString") } else { println("String is null") } val maybeNull = nullableString?.length // Safe call operator ? returns null instead of throwing exception val notNull = nullableString!!.length // Not-null assertion operator !! throws exception if null } fun main() { var nullableString: String? = "Hello" processString(nullableString) nullableString = null // This is allowed processString(nullableString) }

Type inference & Type checks

Type inference is a feature that allows the compiler to automatically determine the data type of a variable based on the value assigned to it.

For example, this is how you can declare a variable with type inference:

fun getSomething(): Any { return 1234567890 } fun main() { val something = getSomething() println(something) }

You can check the type of variable using the is operator.

fun getSomething(): Any { return 1234567890 } fun main() { val something = getSomething() println(something is Int) println(something is Long) println(something::class.simpleName) }

Type Conversion

Type conversion is a method of converting one data type to another. Keep in mind, that type conversion may result in data loss!

Type Casting:

fun main() { val obj: Any = "Hello" val str = obj as String // Explicit cast val safeStr = obj as? String // Safe cast, returns null if fails }

Type Conversion:

fun main() { val intValue = 100 val longValue = intValue.toLong() // Convert to Long val shortValue = intValue.toShort() // May lose data if too large! val floatValue = intValue.toFloat() val stringValue = intValue.toString() }

Assigning smaller data type to larger data type - generally doesn't result in data loss.
Assigning larger data type to smaller data type - may result in data loss.
💡 ️ AI often silently uses !! or unsafe casts. When reviewing AI-generated code, check every type conversion for potential data loss or NPE.

Arrays

Arrays

Array is a fixed-size sequential collection of elements of the same type.

Declaration and Initialization

  • Arrays can be declared using the arrayOf<Type>() function.
  • Arrays are fixed-size, meaning their size cannot be changed once created.

Type-Safety

  • Arrays in Kotlin are type-safe - they can only hold elements of the specified type (and its subtypes).
    If array contains elements of different types, the type of the array is inferred to be the least common supertype of the elements,
    or Any in case of no common supertype.
  • The type declaration can be omitted if the type of the array can be inferred from the elements passed to the function.

Access and Modification

  • Elements can be accessed or modified using their index and [] operator. Arrays are zero-based
    For example array[0] will access first element.
  • Modifying an element using an index that is out of bounds will throw an ArrayIndexOutOfBoundsException.
  • Arrays can be iterated using loops.

Array Declaration and Initialization

  • Arrays can be declared using the arrayOf<Type>() function.
    Type declaration can be omitted if the type of the array can be inferred from the elements passed to the arrayOf() function.
  • Arrays in Kotlin are type-safe - they can only hold elements of the specified type.
    If array contains elements of different types, the type of the array is inferred to be the least common supertype of the elements,
    or Any in case of no common supertype.
  • Arrays are fixed-size, meaning their size cannot be changed once created.
// Declaring an array of integers val numbers = arrayOf(1, 2, 3, 4, 5) // Declaring an array of strings val cities = arrayOf("Bangkok", "Beijing", "Tokyo", "London", "Prague") // Declaring an array of mixed types val mixed = arrayOf(1, "Bangkok", 3.14, 'A', true) val empty = emptyArray<String>() // size 0 val arrayOfNulls = arrayOfNulls<String>(5) // size 5, all elements are null

Array Access and Modification

  • Elements can be accessed or modified using their index and []Arrays are zero-based
    For example array[0]
  • Modifying an element using an index that is out of bounds will throw an ArrayIndexOutOfBoundsException
val array = arrayOf(1, 2, 3, 4, 5) // updating an element on index 4 (5th element) array[4] = 42 // accessing an element on index 4 (5th element) println(array[4]) // accessing an element on index 5 (6th element) - will throw ArrayIndexOutOfBoundsException try { println(array[5]) } catch (e: ArrayIndexOutOfBoundsException) { println(e.message) }
You can get size of the array using size
println(array.size) // prints 5

Array Operations

Given an array, some of the common operations on arrays include ...
Iterating an array using a forforEach
for (element in array) { println(element) }
array.forEach { println(it) }
Filtering an array
val filtered = array.filter { it % 2 == 0 }
Checking if an array contains an element
array.contains(3) // returns true
Sorting, reversing, and shuffling an array
val sorted = array.sortedArray() // returns a new sorted array val reversed = array.reversedArray() // returns a new reversed array val shuffled = array.toList().shuffled() // returns a new shuffled list
We will talk more about array and collection operations in the next lessons.

Operators

Operators

Operators are special symbols that perform operations on variables and values.

We will discuss these kotlin operators:

  1. Assignment operators
    used to assign value to variable or constant
  2. Arithmetic operators
    used to perform mathematical operations
  3. Comparison operators
    used to compare two values
  4. Logical operators
    used to combine multiple boolean expressions
  5. Bitwise operators
    used to perform operations on binary representations of numbers

Assignment Operators

Assignment operators are used to assign value to variable or constant.

Variables and constants don't need to have explicit type declaration, in case the compiler can infer the type from the value assigned to it.

val number = 3 val text = "Hello" val date = LocalDate.now()

Variables and constants can be declared as read-only using val keyword, or mutable, using var keyword.

Mutable variable value can be changed during the program execution.
var name: String = "John" name = "Jane" println(name)
This will throw an error, because name is declared as read-only.
val name: String = "John" name = "John" println(name)
Can you think of reason why we would want to declare a variable as read-only and reasons why we would want to declare a variable as mutable?

Arithmetic Operators

Given two variables var a = 5 and var b = 2.
Operator Name Example Result
+ Addition a + b 7
- Subtraction a - b 3
* Multiplication a * b 10
/ Division a / b 2
% Modulus a % b 1
++ Increment a++ 6
-- Decrement a-- 4

Arithmetic Operators

fun main() { var a = 10 var b = 3 println("a + b = ${a + b}") // 13 println("a - b = ${a - b}") // 7 println("a * b = ${a * b}") // 30 println("a / b = ${a / b}") // 3 (integer division) println("a % b = ${a % b}") // 1 (remainder) // Pre-increment vs Post-increment println("a++ = ${a++}") // 10 (returns old value, then increments) println("a = $a") // 11 println("++a = ${++a}") // 12 (increments first, then returns new value) // Floating point division val c = 10.0 val d = 3.0 println("c / d = ${c / d}") // 3.3333333333333335 }

Question: What's the difference between a++ and ++a?

Comparison Operators

As the name suggest, comparison operators are used for comparing values. They always yield a boolean value.
Operator Name Example
== Equals a == b
!= Not equals a != b
> Greater than a > b
< Less than a < b
>= Greater than or equal a >= b
<= Less than or equal a <= b

Comparison Operators

Given a = 5, b = 3, c = 2


fun main() { val a = 5 val b = 3 val c = 2 println("a == b: ${a == b}") // false println("a != b: ${a != b}") // true println("a > b: ${a > b}") // true println("b < a: ${b < a}") // true println("a >= c: ${a >= c}") // true println("b <= c: ${b <= c}") // false // What will these print? println("a + c == b + b: ${a + c == b + b}") // ? println("a > b && b > c: ${a > b && b > c}") // ? }

Logical Operators

Logical operators are used to evaluate logic between variables or values. They always yields boolean value.
Operator Name Description
&& Logical and Returns true if both statements are true
|| Logical or Returns true if either statement is true
! Logical not Reverses the result
and(other) Logical and Works same as && but doesn't short-circuit (not to be confused with bitwise and)
or(other) Logical or Works same as || but doesn't short-circuit (not to be confused with bitwise or)

Short circuiting means that if the first part of the statement is false, the second part will not be evaluated.

Logical Operators

fun main() { val name: String? = null val age: Int? = 25 // Logical AND if (name != null && name.length > 0) { println("Name is not empty: $name") } else { println("Name is null or empty") } // Logical OR if (name == null || name.isEmpty()) { println("Name is missing") } // Logical NOT val isNotNull = name != null val isEmpty = !isNotNull println("Is empty: $isEmpty") // Complex conditions if ((age != null && age >= 18) && (name != null && name.isNotBlank())) { println("Valid adult user") } else { println("Invalid user data") } }

Bitwise Operators

Operator Name Example Result
and Binary and 5 and 3 1
or Binary or 5 or 3 7
xor Binary xor 5 xor 3 6
shl Signed left shift 5 shl 1 10
shr Signed right shift 5 shr 1 2
ushr Unsigned right shift 5 ushr 1 2
.inv() Binary complement 5.inv() -6

fun main() { val a = 5 // Binary: 101 val b = 3 // Binary: 011 println("a and b = ${a and b}") // 1 (Binary: 001) println("a or b = ${a or b}") // 7 (Binary: 111) println("a xor b = ${a xor b}") // 6 (Binary: 110) println("a shl 1 = ${a shl 1}") // 10 (Binary: 1010) println("a.inv() = ${a.inv()}") // -6 }

Compound Assignment

Combines assignment operators with arithmetic operators.
Operator Example Equivalent
+= a += b a = a + b
-= a -= b a = a - b
*= a *= b a = a * b
/= a /= b a = a / b
%= a %= b a = a % b

fun main() { var score = 100 score += 50 // score is now 150 score -= 25 // score is now 125 score *= 2 // score is now 250 score /= 5 // score is now 50 score %= 7 // score is now 1 println("Final score: $score") // 1 }

Conditionals

Control Flow Statements

if ... else if ... else

Because Kotlin, like Java is based on C++ syntax, you can expect similar control flow statements.

You can write just simple if statement.

if (a <= b) { // execute if condition is met }

The else branch is not required, but it is highly recommendable.

if (a <= b) { // execute if condition is met } else { // execute if condition is NOT met }

You can also evaluate multiple conditions with else if.

if (a < b) { // execute if first condition is met } else if (a == b) { // execute if second condition is met } else if (a == null) { // execute if third condition is met } else { // execute if no condition is met }

if ... else if ... else

Kotlin allows you to return value from if else statement.

Traditionally, you would write this code ...

var result: String? = null if (a < b) { result = "a is less than b" } else if (a == b) { result = "a is equal to b" } else { result = "a is greater than b" }

Kotlin allows you to write this in a more concise way.

val result = if (a < b) { "a is less than b" } else if (a == b) { "a is equal to b" } else { "a is greater than b" }

Kotlin has no ternary operator like Java (condition ? value1 : value2), but you can use if else as a replacement.

val result = if (a < b) "a is less than b" else "a is greater than or equal to b"

when

One of the most powerful control flow statements in Kotlin is when.

It is similar to switch in Java, but it is more powerful. It can be used as both a statement and an expression.

  • Matches a value against multiple branches
  • No fall-through — only the first matching branch executes
  • Can match on values, ranges, types, and arbitrary conditions

Basic syntax:

when (value) { matchValue1 -> { /* execute if value == matchValue1 */ } matchValue2 -> { /* execute if value == matchValue2 */ } else -> { /* execute if no other branch matches */ } }

when

Given ...
val randomInt = arrayOf(0, 1, 2, 3, 4, 5).random()
You can use when as a statement or as an expression.
You can check if a value is within a range or in a collection using in keyword.
val result = when (randomInt) { 0 -> "Zero" 1 -> "One" 2 -> "Two" 3 -> "Three" 4 -> "Four" 5 -> "Five" else -> "Too much" }
val result = when (randomInt) { 0 -> "Zero" in 1..3 -> "Between 1 and 3" in 3..5 -> "Between 3 and 5" else -> "Too much" }
You can use when without an argument, which allows you to evaluate arbitrary conditions.
You can also check the type of a variable using is keyword.
val result = when { randomInt < 0 -> "Less than 0" randomInt == 0 -> "Zero" else -> "Greater than 0" }
fun describe(obj: Any): String = when (obj) { 1 -> "One" "Hello" -> "Greeting" is Long -> "Long" !is String -> "Not a string" else -> "Unknown" }

For Loop

Basic for loop syntax
fun main() { for (i in 1..5) { println("Number: $i") } }
Iterating over arrays
fun main() { val fruits = arrayOf("apple", "banana", "orange") for (fruit in fruits) { println("Fruit: $fruit") } }
Range expressions with downTo and step
fun main() { for (i in 10 downTo 1 step 2) { println("Countdown: $i") } }
Iterating with indices
fun main() { val fruits = arrayOf("apple", "banana", "orange") for (index in fruits.indices) { println("$index: ${fruits[index]}") } }
Using withIndex() for both index and value
fun main() { val fruits = arrayOf("apple", "banana", "orange") for ((index, value) in fruits.withIndex()) { println("$index -> $value") } }
Iterating over characters in a string
fun main() { for (char in "Hello") { print("$char ") } }

While Loop / Do While Loop

While loop is used to execute a block of code repeatedly as long as a given condition is true.

The while evaluates condition at the beginning of the loop block, before any code is executed.

fun main() { var counter = 1 while (counter <= 5) { println("Counter: $counter") counter++ } }

The do while first executes the code block once, and then evaluates the condition.

fun main() { var number = 1 do { println("Number: $number") number++ } while (number <= 3) }

Practice: While Loop / Do While Loop

What's the difference between these two loops?

Example 1 - while loop:

What will this print?
fun main() { var counter1 = 10 while (counter1 < 5) { println("Counter: $counter1") counter1++ } println("Final counter: $counter1") }

Example 2 - do-while loop:

What will this print?
fun main() { var counter2 = 10 do { println("Counter: $counter2") counter2++ } while (counter2 < 5) println("Final counter: $counter2") }

Answer:

The while loop won't execute at all (condition is false from start), but the do-while loop will execute once before checking the condition.

Working with Strings

Working with Strings

This example demonstrates basic string operations including printing to console, concatenation, and formatting.

fun main() { print("Hello, ") // Prints without newline println("World") // Prints with newline println("I am learning Kotlin!") }
fun main() { val firstName = "John" val lastName = "Doe" val age = 25 var learning = "I am learning " println("My name is" + firstName + " " + lastName + ".") // Concatenation with + operator (discouraged) println("I am $age years old." ) // String interpolation with $ syntax (commonly used) println("Next year I'll be ${age + 1}.") // Expression in curly braces (commonly used) learning += "Kotlin!" // Concatenation with += operator (discouraged, not very common) println(learning) }
fun main() { val formatted = String.format("Student %s scored %.1f%%", "Alice", 95.7) println(formatted) }

Working with Strings

This example demonstrates various string operations including length, substrings, conversion, and content checks.

fun main() { val text = " Hello, Kotlin World! " val empty = "" val blank = " " val textLength = text.length val trimmedText = text.trim() val substring1 = trimmedText.substring(0, 5) val substring2 = trimmedText.substring(7) val words = text.trim().split(", ") val upperCaseText = text.uppercase() val lowerCaseText = text.lowercase() val replacedText = text.replace("World", "Universe") val containsString = text.contains("Kotlin") val startsWithString = text.trim().startsWith("Hello") val endsWithString = text.trim().endsWith("!") val isEmpty = empty.isEmpty() val isBlank = empty.isBlank() val isEmpty2 = blank.isEmpty() val isBlank2 = blank.isBlank() println("Original: '$text'") println("Trimmed: '$trimmedText'") println("Length: $textLength") println("Substring (0, 5): $substring1") println("Substring from 7: $substring2") println("Words: $words") println("Uppercase: $upperCaseText") println("Lowercase: $lowerCaseText") println("Replace 'World' with 'Universe': $replacedText") println("Contains 'Kotlin': $containsString") println("Starts with 'Hello': $startsWithString") println("Ends with '!': $endsWithString") println("Empty is empty: $isEmpty") println("Empty is blank: $isBlank") println("Blank is empty: $isEmpty2") println("Blank is blank: $isBlank2") }

Functions

Function

Kotlin is a modern language, supporting both traditional functions and object-oriented programming and functional programming paradigms.
Functions are defined using the fun keyword, followed by the function name, arguments, and return type.
fun add(a: Int, b: Int): Int { return a + b } val sum = add(2, 3)
If the function does not return anything, the return type is Unit, and does not need to be specified explicitly.
fun greet(name: String) { println("Hello, $name!") } greet("Alice")

Functions can be written in a single expression, in which case the return type can be omitted.
fun multiply(a: Int, b: Int) = a * b

Functions can be passed as arguments to other functions and returned from functions.
fun operation(a: Int, b: Int, operation: (Int, Int) -> Int): Int { return operation(a, b) } val sum = operation(2, 3) { a, b -> a + b }

Local functions

Functions can be defined inside other functions, in which case they are called local functions.
Defining a local functions is useful when you want to encapsulate some logic that is used multiple times but only within the function in which it is defined.
// outer function fun factorial(n: Int): Int { // local function fun fact(x: Int, acc: Int): Int { return if (x <= 1) acc else fact(x - 1, x * acc) // tail recursion } // calls the local function return fact(n, 1) } // main entry point with a call to the outer function fun main() { println(factorial(5)) // prints 120 }
Functions can be defined at the top level of a file, meaning they do not need to be part of a class.

Default arguments

Kotlin allows you to specify default values for function arguments, making them optional.

Given a function with a default argument ...
fun round( value: Double, decimals: Int = 2 // argument with default value of 2 ): Double { val factor = 10.0.pow(decimals.toDouble()) return (value * factor).roundToInt() / factor }

When the argument is not provided when calling the function, the default value is used.
import kotlin.math.pow import kotlin.math.roundToInt fun round( value: Double, decimals: Int = 2 // argument with default value of 2 ): Double { val factor = 10.0.pow(decimals.toDouble()) return (value * factor).roundToInt() / factor } fun main() { val result = round(3.14159) println(result) // prints 3.14 }

When the argument is provided when calling the function, the provided value is used.
import kotlin.math.pow import kotlin.math.roundToInt fun round( value: Double, decimals: Int = 2 // argument with default value of 2 ): Double { val factor = 10.0.pow(decimals.toDouble()) return (value * factor).roundToInt() / factor } fun main() { val result = round(3.14159, 4) println(result) // prints 3.1416 }

Named arguments

Kotlin allows you to specify the name of the arguments when calling a function.

This is useful when you have a function with many arguments, and you want to make the code more readable, for example when you have a function with many arguments.

It also allows you to specify arguments in any order, as long as you specify the name. This often comes in handy when refactoring the code, adding or changing order of arguments.


Given function ...
fun round( value: Double, decimals: Int = 2 ): Double { val factor = 10.0.pow(decimals.toDouble()) return (value * factor).roundToInt() / factor }

You may specify the argument name.
import kotlin.math.pow import kotlin.math.roundToInt fun round( value: Double, decimals: Int = 2 ): Double { val factor = 10.0.pow(decimals.toDouble()) return (value * factor).roundToInt() / factor } fun main() { val result = round(value = 3.14159) println(result) }

You may specify the argument names in any order.
import kotlin.math.pow import kotlin.math.roundToInt fun round( value: Double, decimals: Int = 2 ): Double { val factor = 10.0.pow(decimals.toDouble()) return (value * factor).roundToInt() / factor } fun main() { val result = round( decimals = 4, // notice the changed order of arguments value = 3.14159 ) println(result) }

Named arguments

This is an example of a function with many parameters, where using named arguments at the call site can improve readability significantly.

fun calculateEvapotranspiration( temperature: Double, solarRadiation: Double, humidity: Double, windSpeed: Double, atmosphericPressure: Double, location: Pair<Double, Double>, time: LocalDateTime, soilType: SoilType, cropType: CropType ): Double { // calculation }

A) Calling the function without named arguments makes it hard to understand what each argument represents ...

val et = calculateEvapotranspiration( 25.0, 800.0, 0.6, 3.0, 1013.25, 13.7655756 to 100.5675686, LocalDateTime.now(), SoilType.CLAY, CropType.WHEAT, )

B) Named arguments clarify the purpose of each argument ...

val et = calculateEvapotranspiration( soilType = SoilType.CLAY, cropType = CropType.WHEAT, time = LocalDateTime.now(), location = 13.7655756 to 100.5675686, temperature = 25.0, solarRadiation = 800.0, humidity = 0.6, windSpeed = 3.0, atmosphericPressure = 1013.25 )

Variable Arguments

Variadic functions are functions that can take a variable number of arguments.

You can define a function that takes a variable number of arguments by using the vararg keyword.

fun sayHello(vararg names: String) { println("Hello, ${names.joinToString(", ")}!") } fun main() { sayHello("Alice", "Bob", "Charlie") }

The vararg parameter can appear in any position, but if it is not the last one, subsequent arguments must be passed using named syntax.

fun sayHello(greeting: String, vararg names: String) { println("Hello, ${names.joinToString(", ")}!") } fun main() { sayHello(greeting = "Hi", "Alice", "Bob", "Charlie") }