About Kotlin
Table of Contents
- Lesson 1: Kotlin Basics
- Lesson 2: Functions
- Lesson 3: Classes and Objects
- Lesson 4: Concurrent Programming
- Lesson 5: Evolution of Kotlin
Lesson 1: Kotlin Basics
Overview
In this lesson, you'll learn the fundamental concepts of Kotlin programming:
- Get started: Setting up IntelliJ IDEA and creating your first project
- Operators: Mathematical, comparison, and assignment operators
- Data types: Integer types, floating-point numbers, and strings
- Variables: Mutable and immutable variables with type inference
- Conditionals: Control flow with if/else, when statements, and loops
- Lists and arrays: Working with collections
- Null safety: Kotlin's approach to preventing null pointer exceptions
Getting Started
Your First Kotlin Program
fun main() { println("Hello, world!") }
Passing Arguments to main()
fun main(args: Array<String>) { val name = args.getOrNull(0)?.takeIf { it.isNotBlank() } ?: "Kotlin" println("Hello, $name!") }
Operators
Mathematical Operators
fun main() { // Math operators with integers println("1 + 1 = ${1 + 1}") println("53 - 3 = ${53 - 3}") println("50 / 10 = ${50 / 10}") println("9 % 3 = ${9 % 3}") // Math operators with doubles println("1.0 / 2.0 = ${1.0 / 2.0}") println("2.0 * 3.5 = ${2.0 * 3.5}") }
Numeric Operator Methods
fun main() { // Kotlin lets you call methods on numbers println("2.times(3) = ${2.times(3)}") println("3.5.plus(4) = ${3.5.plus(4)}") println("2.4.div(2) = ${2.4.div(2)}") }
Data Types
Working with Different Types
fun main() { // Integer types val byteValue: Byte = 127 val shortValue: Short = 32767 val intValue: Int = 2147483647 val longValue: Long = 9223372036854775807L // Floating-point types val floatValue: Float = 3.14f val doubleValue: Double = 3.141592653589793 // Other types val charValue: Char = 'K' val booleanValue: Boolean = true println("Byte: $byteValue") println("Short: $shortValue") println("Int: $intValue") println("Long: $longValue") println("Float: $floatValue") println("Double: $doubleValue") println("Char: $charValue") println("Boolean: $booleanValue") }
Type Casting
fun main() { val i: Int = 6 println("Int to Byte: ${i.toByte()}") println("Int to Float: ${i.toFloat()}") println("Int to Double: ${i.toDouble()}") // Using underscores for long numbers val oneMillion = 1_000_000 val hexBytes = 0xFF_EC_DE_5E println("One million: $oneMillion") println("Hex bytes: $hexBytes") }
Strings
String Basics and Templates
fun main() { // Basic strings val s1 = "Hello world!" val s2 = "Hello world!\n" // Multi-line strings with triple quotes val text = """ This is a multi-line string """.trimIndent() // String concatenation val numberOfDogs = 3 val numberOfCats = 2 val message = "I have $numberOfDogs dogs" + " and $numberOfCats cats" // String templates val i = 10 val s = "abc" val length = "$s.length is ${s.length}" // Expression in templates val numberOfShirts = 10 val numberOfPants = 5 val total = "I have ${numberOfShirts + numberOfPants} items of clothing" println(s1) println(text) println(message) println("i = $i") println(length) println(total) }
Variables
Mutable and Immutable Variables
fun main() { // Immutable variable (val) val name = "Jennifer" println("Name: $name") // Mutable variable (var) var score = 10 println("Initial score: $score") score = 20 println("Updated score: $score") // Explicit type declaration var width: Int = 12 var length: Double = 2.5 println("Width: $width, Length: $length") // Type inference val inferredInt = 42 // Int val inferredDouble = 3.14 // Double val inferredString = "Hello" // String println("Inferred types:") println("$inferredInt is ${inferredInt::class.simpleName}") println("$inferredDouble is ${inferredDouble::class.simpleName}") println("$inferredString is ${inferredString::class.simpleName}") }
Conditionals
If/Else Statements
fun main() { val numberOfCups = 30 val numberOfPlates = 50 if (numberOfCups > numberOfPlates) { println("Too many cups!") } else { println("Not enough cups!") } // Multiple conditions val guests = 30 if (guests == 0) { println("No guests") } else if (guests < 20) { println("Small group of people") } else { println("Large group of people!") } // If as expression val temperature = 20 val isHot = if (temperature > 40) true else false println("Is it hot? $isHot") }
Ranges and When Statements
fun main() { // Ranges val numberOfStudents = 50 if (numberOfStudents in 1..100) { println("Valid number of students: $numberOfStudents") } // When statement val results = 45 when (results) { 0 -> println("No results") in 1..39 -> println("Got results!") else -> println("That's a lot of results!") } // When as expression val grade = when (results) { in 90..100 -> "A" in 80..89 -> "B" in 70..79 -> "C" in 60..69 -> "D" else -> "F" } println("Grade: $grade") }
Loops
fun main() { // For loop with array val pets = arrayOf("dog", "cat", "canary") print("Pets: ") for (element in pets) { print("$element ") } println() // For loop with index println("Pets with index:") for ((index, element) in pets.withIndex()) { println("Item at $index is $element") } // Range loops print("Numbers 1-5: ") for (i in 1..5) print(i) println() print("Countdown: ") for (i in 5 downTo 1) print(i) println() print("Even numbers: ") for (i in 2..10 step 2) print(i) println() // While loop var bicycles = 0 while (bicycles < 3) { bicycles++ println("Bicycle $bicycles added") } // Repeat loop print("Repeat: ") repeat(3) { print("Hello! ") } println() }
Lists and Arrays
Working with Lists
fun main() { // Immutable list val instruments = listOf("trumpet", "piano", "violin") println("Instruments: $instruments") // Mutable list val myList = mutableListOf("trumpet", "piano", "violin") myList.remove("violin") myList.add("drums") println("Modified list: $myList") // List operations val books = listOf("nature", "biology", "birds") val booksStartingWithB = books.filter { it[0] == 'b' } println("Books starting with 'b': $booksStartingWithB") }
Working with Arrays
fun main() { // Array of strings val pets = arrayOf("dog", "cat", "canary") println("Pets: ${pets.contentToString()}") // Mixed type array val mix = arrayOf("hats", 2) println("Mixed array: ${mix.contentToString()}") // Typed arrays val numbers = intArrayOf(1, 2, 3) println("Numbers: ${numbers.contentToString()}") // Combining arrays val numbers2 = intArrayOf(4, 5, 6) val combined = numbers + numbers2 println("Combined: ${combined.contentToString()}") }
Null Safety
Null Safety Features
fun main() { // Variables cannot be null by default var numberOfBooks: Int? = null println("Books: $numberOfBooks") // Safe call operator numberOfBooks = 6 numberOfBooks = numberOfBooks?.dec() println("After decrement: $numberOfBooks") // Elvis operator numberOfBooks = null val finalCount = numberOfBooks?.dec() ?: 0 println("Final count: $finalCount") // Safe call with string val name: String? = "Kotlin" println("Name length: ${name?.length}") val nullName: String? = null println("Null name length: ${nullName?.length}") // Not-null assertion (use carefully!) val definitelyNotNull: String? = "Hello" val length = definitelyNotNull!!.length println("Definite length: $length") }
Error Handling with Result
Managing errors in Kotlin can be done using the Result<T>
class, which allows you to handle success and failure cases without throwing exceptions.
fun divide(a: Int, b: Int): Result<Double> { return if (b == 0) { Result.failure(Exception("Division par zéro")) } else { Result.success(a.toDouble() / b) } } fun main() { val result = divide(10, 2) result.fold( onSuccess = { value -> println("✅ Résultat: $value") }, onFailure = { error -> println("❌ Erreur: ${error.message}") } ) // Avec erreur divide(10, 0).fold( onSuccess = { value -> println("✅ Résultat: $value") }, onFailure = { error -> println("❌ Erreur: ${error.message}") } ) }
Lesson 2: Functions
Overview
In this lesson, you'll learn about:
- Programs in Kotlin: Creating main functions and running programs
- Expressions: Everything has a value in Kotlin
- Functions: Creating and using functions with parameters
- Compact functions: Single-expression functions
- Lambdas and higher-order functions: Functional programming concepts
- List filters: Working with collections functionally
Programs in Kotlin
Creating a Main Function
fun main(args: Array<String>) { println("Hello, world!") // Using arguments if provided if (args.isNotEmpty()) { println("Hello, ${args[0]}!") } }
Everything Has a Value
Expressions vs Statements
fun main() { // If as expression val temperature = 20 val isHot = if (temperature > 40) true else false println("Is hot: $isHot") // Even println returns a value (Unit) val isUnit = println("This is an expression") println("println returns: $isUnit") // When as expression val number = 42 val description = when { number < 0 -> "negative" number == 0 -> "zero" number > 0 -> "positive" else -> "unknown" } println("$number is $description") }
Functions in Kotlin
Basic Functions
// Simple function fun printHello() { println("Hello World") } // Function with parameters fun greet(name: String) { println("Hello, $name!") } // Function with return value fun add(a: Int, b: Int): Int { return a + b } // Unit returning function (explicit) fun printMessage(message: String): Unit { println(message) } // Unit returning function (implicit) fun printAnotherMessage(message: String) { println(message) } fun main() { printHello() greet("Kotlin") val sum = add(5, 3) println("Sum: $sum") printMessage("Explicit Unit") printAnotherMessage("Implicit Unit") }
Function Parameters
// Default parameters fun drive(speed: String = "fast") { println("driving $speed") } // Required parameters fun tempToday(day: String, temp: Int) { println("Today is $day and it's $temp degrees.") } // Mix of default and required parameters fun reformat( str: String, divideByCamelHumps: Boolean, wordSeparator: Char, normalizeCase: Boolean = true ) { println("Reformatting: $str with separator '$wordSeparator'") } fun main() { // Using default parameter drive() drive("slow") drive(speed = "turtle-like") // Required parameters tempToday("Monday", 25) // Named arguments reformat("TodayIsADayLikeNoOther", false, '_') reformat("TodayIsADayLikeNoOther", divideByCamelHumps = false, wordSeparator = '_') }
Compact Functions
Single-Expression Functions
// Regular function fun doubleRegular(x: Int): Int { return x * 2 } // Compact function fun double(x: Int): Int = x * 2 // Compact function with type inference fun triple(x: Int) = x * 3 // More examples fun square(x: Int) = x * x fun isEven(x: Int) = x % 2 == 0 fun max(a: Int, b: Int) = if (a > b) a else b fun main() { println("Double 5: ${double(5)}") println("Triple 4: ${triple(4)}") println("Square 6: ${square(6)}") println("Is 8 even? ${isEven(8)}") println("Max of 10 and 15: ${max(10, 15)}") }
Lambdas and Higher-Order Functions
Lambda Functions
fun main() { // Basic lambda val waterFilter = { level: Int -> level / 2 } var dirtLevel = 20 println("Filtered water: ${waterFilter(dirtLevel)}") // Lambda with explicit type val waterFilter2: (Int) -> Int = { level -> level / 2 } println("Filtered water 2: ${waterFilter2(dirtLevel)}") // Lambda with multiple parameters val multiply: (Int, Int) -> Int = { a, b -> a * b } println("Multiply 4 and 5: ${multiply(4, 5)}") // Lambda with no parameters val greeting: () -> String = { "Hello from lambda!" } println(greeting()) // Lambda with multiple statements val multiStatement = { x: Int, y: Int -> println("Calculating sum of $x and $y") val result = x + y println("Result: $result") result // Last expression is the return value } println(multiStatement(3, 4)) }
Higher-Order Functions
// Higher-order function that takes a function as parameter fun encodeMsg(msg: String, encode: (String) -> String): String { return encode(msg) } // Named function to pass as argument fun reverseString(input: String): String = input.reversed() fun main() { // Using lambda val enc1: (String) -> String = { input -> input.uppercase() } println("Encoded with lambda: ${encodeMsg("hello", enc1)}") // Using function reference println("Encoded with function reference: ${encodeMsg("hello", ::reverseString)}") // Inline lambda println("Encoded inline: ${encodeMsg("hello") { it.lowercase() }}") // Using built-in higher-order function repeat(3) { println("Repeated message $it") } }
List Filters
Basic Filtering
fun main() { // Basic filter val numbers = listOf(1, 2, 3, 4, 5, 6) val evenNumbers = numbers.filter { it % 2 == 0 } println("Even numbers: $evenNumbers") // Filter with explicit parameter val positiveNumbers = numbers.filter { n: Int -> n > 0 } println("Positive numbers: $positiveNumbers") // Filter strings val books = listOf("nature", "biology", "birds") val booksStartingWithB = books.filter { it[0] == 'b' } println("Books starting with 'b': $booksStartingWithB") // Using 'it' implicitly val colors = listOf("red", "red-orange", "dark red", "orange", "bright orange") val redColors = colors.filter { it.contains("red") } println("Colors containing 'red': $redColors") }
Eager vs Lazy Filters
fun main() { val instruments = listOf("viola", "cello", "violin", "guitar", "piano") // Eager filter (creates new list immediately) val eager = instruments.filter { it[0] == 'v' } println("Eager filter: $eager") // Lazy filter using sequence val lazy = instruments.asSequence().filter { it[0] == 'v' } println("Lazy filter sequence: $lazy") // Convert sequence back to list val lazyToList = lazy.toList() println("Lazy to list: $lazyToList") // Chaining operations with sequences val result = instruments.asSequence() .filter { it.length > 5 } .map { it.uppercase() } .toList() println("Chained operations: $result") }
Other List Transformations
fun main() { // Map transformation val numbers = listOf(1, 2, 3, 4, 5) val doubled = numbers.map { it * 2 } println("Doubled: $doubled") val names = listOf("alice", "bob", "charlie") val uppercased = names.map { it.uppercase() } println("Uppercased: $uppercased") // Flatten nested collections val numberSets = listOf(setOf(1, 2, 3), setOf(4, 5), setOf(1, 2)) val flattened = numberSets.flatten() println("Flattened: $flattened") // Combining operations val words = listOf("hello", "world", "kotlin", "programming") val result = words .filter { it.length > 5 } .map { it.uppercase() } .sorted() println("Combined operations: $result") }
Lesson 3: Classes and Objects
Overview
In this lesson, you'll learn about:
- Classes: Blueprints for objects with properties and methods
- Constructors: Creating and initializing objects
- Inheritance: Extending classes and implementing interfaces
- Extension functions: Adding functionality to existing classes
- Special classes: Data classes, enums, objects, and companion objects
- Code organization: Packages and visibility modifiers
Classes
Basic Class Definition
// Simple class class House { val color: String = "white" val numberOfWindows: Int = 2 val isForSale: Boolean = false fun updateColor(newColor: String) { println("Updating color to $newColor") } } // Class with constructor parameters class Person(val name: String, var age: Int) // Class with behavior class Circle(val radius: Double) { fun area(): Double { return Math.PI * radius * radius } fun circumference(): Double { return 2 * Math.PI * radius } } fun main() { val myHouse = House() println("House color: ${myHouse.color}") myHouse.updateColor("blue") val person = Person("Alice", 30) println("Person: ${person.name}, age ${person.age}") person.age = 31 println("Updated age: ${person.age}") val circle = Circle(5.0) println("Circle area: ${circle.area()}") println("Circle circumference: ${circle.circumference()}") }
Constructors and Initialization
// Class with default parameters class Box(val length: Int, val width: Int = 20, val height: Int = 40) { init { println("Creating box: ${length}x${width}x${height}") } fun volume() = length * width * height } // Class with multiple constructors class Rectangle(val width: Double, val height: Double) { constructor(side: Double) : this(side, side) { println("Creating square with side $side") } constructor(width: Int, height: Int) : this(width.toDouble(), height.toDouble()) { println("Creating rectangle from integers") } fun area() = width * height fun perimeter() = 2 * (width + height) } fun main() { val box1 = Box(100, 20, 40) val box2 = Box(length = 100) println("Box1 volume: ${box1.volume()}") println("Box2 volume: ${box2.volume()}") val rectangle1 = Rectangle(10.0, 5.0) val square = Rectangle(8.0) val rectangle2 = Rectangle(12, 6) println("Rectangle area: ${rectangle1.area()}") println("Square area: ${square.area()}") println("Rectangle2 perimeter: ${rectangle2.perimeter()}") }
Properties with Custom Getters and Setters
class Person(var firstName: String, var lastName: String) { // Custom getter val fullName: String get() = "$firstName $lastName" // Property with custom getter and setter var displayName: String = "" get() = if (field.isEmpty()) fullName else field set(value) { field = value.trim() } // Computed property val initials: String get() = "${firstName.first()}${lastName.first()}" } class Temperature { var celsius: Double = 0.0 set(value) { field = value println("Temperature set to $value°C") } val fahrenheit: Double get() = celsius * 9/5 + 32 val kelvin: Double get() = celsius + 273.15 } fun main() { val person = Person("John", "Doe") println("Full name: ${person.fullName}") println("Display name: ${person.displayName}") println("Initials: ${person.initials}") person.displayName = "Johnny" println("Custom display name: ${person.displayName}") val temp = Temperature() temp.celsius = 25.0 println("${temp.celsius}°C = ${temp.fahrenheit}°F = ${temp.kelvin}K") }
Inheritance
Interfaces
interface Shape { fun computeArea(): Double fun computePerimeter(): Double // Interface can have default implementation fun describe(): String = "This is a shape with area ${computeArea()}" } interface Drawable { fun draw(): String } class Circle(val radius: Double) : Shape, Drawable { override fun computeArea() = Math.PI * radius * radius override fun computePerimeter() = 2 * Math.PI * radius override fun draw() = "Drawing a circle with radius $radius" } class Rectangle(val width: Double, val height: Double) : Shape, Drawable { override fun computeArea() = width * height override fun computePerimeter() = 2 * (width + height) override fun draw() = "Drawing a rectangle ${width}x${height}" } fun main() { val circle = Circle(3.0) val rectangle = Rectangle(4.0, 5.0) println("Circle: ${circle.describe()}") println("Rectangle: ${rectangle.describe()}") println(circle.draw()) println(rectangle.draw()) val shapes: List<Shape> = listOf(circle, rectangle) shapes.forEach { shape -> println("Area: ${shape.computeArea()}, Perimeter: ${shape.computePerimeter()}") } }
Class Inheritance
// Base class must be marked as 'open' open class Vehicle(val brand: String, val model: String) { open fun start() { println("$brand $model is starting...") } open fun stop() { println("$brand $model is stopping...") } fun info() = "$brand $model" } class Car(brand: String, model: String, val doors: Int) : Vehicle(brand, model) { override fun start() { println("Car $brand $model with $doors doors is starting with ignition...") } } class Motorcycle(brand: String, model: String, val engineSize: Int) : Vehicle(brand, model) { override fun start() { println("Motorcycle $brand $model with ${engineSize}cc engine is starting...") } fun wheelie() { println("$brand $model is doing a wheelie!") } } fun main() { val car = Car("Toyota", "Camry", 4) val motorcycle = Motorcycle("Honda", "CBR", 600) car.start() car.stop() println("Car info: ${car.info()}") motorcycle.start() motorcycle.wheelie() motorcycle.stop() // Polymorphism val vehicles: List<Vehicle> = listOf(car, motorcycle) vehicles.forEach { it.start() } }
Abstract Classes
abstract class Animal(val name: String) { abstract val species: String abstract fun makeSound(): String // Concrete method fun introduce() { println("Hi, I'm $name, a $species") println("I say: ${makeSound()}") } open fun sleep() { println("$name is sleeping...") } } class Dog(name: String) : Animal(name) { override val species = "Canis lupus" override fun makeSound() = "Woof!" fun fetch() { println("$name is fetching the ball!") } } class Cat(name: String) : Animal(name) { override val species = "Felis catus" override fun makeSound() = "Meow!" override fun sleep() { println("$name is sleeping for 16 hours...") } } fun main() { val dog = Dog("Buddy") val cat = Cat("Whiskers") dog.introduce() dog.fetch() dog.sleep() println() cat.introduce() cat.sleep() // Using as Animal type val animals: List<Animal> = listOf(dog, cat) animals.forEach { animal -> println("${animal.name} says ${animal.makeSound()}") } }
Extension Functions
Adding Functions to Existing Classes
// Extension function for Int fun Int.isOdd(): Boolean = this % 2 == 1 fun Int.isEven(): Boolean = this % 2 == 0 // Extension function for String fun String.removeWhitespace(): String = this.replace("\\s".toRegex(), "") fun String.wordCount(): Int = this.trim().split("\\s+".toRegex()).size // Extension function for List fun <T> List<T>.secondOrNull(): T? = if (this.size >= 2) this[1] else null // Extension property val String.lastChar: Char? get() = if (isEmpty()) null else this[length - 1] fun main() { // Using extension functions on Int println("Is 5 odd? ${5.isOdd()}") println("Is 4 even? ${4.isEven()}") // Using extension functions on String val text = "Hello World Kotlin" println("Original: '$text'") println("No whitespace: '${text.removeWhitespace()}'") println("Word count: ${text.wordCount()}") println("Last character: ${text.lastChar}") // Using extension function on List val numbers = listOf(1, 2, 3, 4, 5) val emptyList = emptyList<Int>() println("Second element: ${numbers.secondOrNull()}") println("Second element in empty list: ${emptyList.secondOrNull()}") }
Special Classes
Data Classes
// Data class automatically generates toString, equals, hashCode, copy data class Player(val name: String, val score: Int, val level: Int = 1) data class Point(val x: Int, val y: Int) fun main() { val player1 = Player("Alice", 1000, 5) val player2 = Player("Bob", 850) val player3 = player1.copy(score = 1200) println("Player 1: $player1") println("Player 2: $player2") println("Player 3: $player3") // Destructuring val (name, score, level) = player1 println("Destructured: $name has $score points at level $level") // Using Pair and Triple val bookAuthor = Pair("1984", "George Orwell") val bookInfo = Triple("1984", "George Orwell", 1949) println("Book: $bookAuthor") println("Book with year: $bookInfo") // Using 'to' infix function val bookAuthor2 = "Animal Farm" to "George Orwell" println("Book 2: $bookAuthor2") // Working with maps val authors = mapOf( "1984" to "George Orwell", "Brave New World" to "Aldous Huxley", "Fahrenheit 451" to "Ray Bradbury" ) println("Authors: $authors") }
Enum Classes
enum class Color(val r: Int, val g: Int, val b: Int) { RED(255, 0, 0), GREEN(0, 255, 0), BLUE(0, 0, 255), WHITE(255, 255, 255), BLACK(0, 0, 0); fun toHex(): String = "#%02X%02X%02X".format(r, g, b) } enum class Direction { NORTH, SOUTH, EAST, WEST; fun opposite(): Direction = when (this) { NORTH -> SOUTH SOUTH -> NORTH EAST -> WEST WEST -> EAST } } enum class Planet(val mass: Double, val radius: Double) { MERCURY(3.303e+23, 2.4397e6), VENUS(4.869e+24, 6.0518e6), EARTH(5.976e+24, 6.37814e6), MARS(6.421e+23, 3.3972e6); fun surfaceGravity(): Double = 6.67300E-11 * mass / (radius * radius) } fun main() { // Using Color enum println("Red RGB: ${Color.RED.r}, ${Color.RED.g}, ${Color.RED.b}") println("Blue hex: ${Color.BLUE.toHex()}") // Using Direction enum println("North opposite: ${Direction.NORTH.opposite()}") // Using Planet enum println("Earth surface gravity: ${Planet.EARTH.surfaceGravity()}") // Iterating over enum values println("All colors:") Color.values().forEach { color -> println("${color.name}: ${color.toHex()}") } // Using enum in when expression val direction = Direction.NORTH val movement = when (direction) { Direction.NORTH -> "Moving up" Direction.SOUTH -> "Moving down" Direction.EAST -> "Moving right" Direction.WEST -> "Moving left" } println("Direction: $movement") }
Object and Companion Object
// Singleton object object Calculator { fun add(a: Int, b: Int): Int = a + b fun subtract(a: Int, b: Int): Int = a - b fun multiply(a: Int, b: Int): Int = a * b fun divide(a: Int, b: Int): Double = a.toDouble() / b } // Object expression (anonymous object) object DatabaseConfig { const val URL = "jdbc:mysql://localhost:3306/mydb" const val USERNAME = "admin" const val PASSWORD = "secret" fun getConnectionString(): String = "$URL?user=$USERNAME" } // Class with companion object class MathUtils { companion object { const val PI = 3.14159 const val E = 2.71828 fun circleArea(radius: Double): Double = PI * radius * radius fun factorial(n: Int): Long = if (n <= 1) 1 else n * factorial(n - 1) } fun instanceMethod() { println("This is an instance method") } } // Named companion object class PhysicsSystem { companion object WorldConstants { const val GRAVITY = 9.8 const val UNIT = "metric" fun computeForce(mass: Double, acceleration: Double): Double { return mass * acceleration } } } fun main() { // Using singleton object println("Calculator: 5 + 3 = ${Calculator.add(5, 3)}") println("Calculator: 10 / 3 = ${Calculator.divide(10, 3)}") // Using object configuration println("Database URL: ${DatabaseConfig.URL}") println("Connection string: ${DatabaseConfig.getConnectionString()}") // Using companion object println("PI: ${MathUtils.PI}") println("Circle area (r=5): ${MathUtils.circleArea(5.0)}") println("5! = ${MathUtils.factorial(5)}") // Still can create instances val mathUtils = MathUtils() mathUtils.instanceMethod() // Using named companion object println("Gravity: ${PhysicsSystem.WorldConstants.GRAVITY}") println("Force: ${PhysicsSystem.WorldConstants.computeForce(10.0, 9.8)}") }
Code Organization
Packages and Visibility
// Public by default class PublicClass { val publicProperty = "I'm public" private val privateProperty = "I'm private" protected val protectedProperty = "I'm protected" internal val internalProperty = "I'm internal" fun publicFunction() = "Public function" private fun privateFunction() = "Private function" protected fun protectedFunction() = "Protected function" internal fun internalFunction() = "Internal function" } open class BaseClass { protected val protectedValue = "Accessible to subclasses" private val privateValue = "Not accessible to subclasses" protected fun protectedMethod() { println("Protected method called") } } class DerivedClass : BaseClass() { fun accessProtected() { println(protectedValue) // OK protectedMethod() // OK // println(privateValue) // Compilation error } } // Top-level private function private fun topLevelPrivate() = "Top level private" // Top-level internal function internal fun topLevelInternal() = "Top level internal" fun main() { val obj = PublicClass() println(obj.publicProperty) // println(obj.privateProperty) // Compilation error println(obj.internalProperty) // OK in same module val derived = DerivedClass() derived.accessProtected() println(topLevelInternal()) println(topLevelPrivate()) }
Lesson 4: Concurrent Programming
Kotlin coroutines are a powerful tool for managing background tasks, making asynchronous programming easier and more efficient. This comprehensive guide covers all aspects of concurrent programming in Kotlin.
Introduction to Coroutines
What are Coroutines?
Coroutines in Kotlin are a powerful tool for handling asynchronous programming, enabling developers to write efficient, non-blocking code. They are lightweight threads that can be suspended and resumed at specific points, making your code more readable and maintainable.
Key Benefits
- Lightweight: Much lighter than threads in terms of memory overhead
- Non-blocking: Don't block the calling thread
- Sequential Code: Write asynchronous code that looks synchronous
- Built-in cancellation support: Cancellation is propagated automatically through the running coroutine hierarchy
- Fewer memory leaks: Use structured concurrency to run operations within a scope
Coroutine Builders
Launch - Fire and Forget
import kotlinx.coroutines.* fun main() = runBlocking { // Start a coroutine with launch launch { delay(1000) println("Coroutine says: Hello, World!") } println("Main function continues...") }
Characteristics:
- Returns a
Job
object - Used for tasks that don't return a result
- Fire-and-forget operations
Async - Result-Oriented
Launches a coroutine that returns a result asynchronously. Returns a Deferred<T>
, which is like a Future in Java :
import kotlinx.coroutines.* suspend fun performTask(task: String): String { delay(1000) return "Task $task completed" } fun main() = runBlocking { val task1 = async { performTask("A") } val task2 = async { performTask("B") } println(task1.await()) println(task2.await()) }
RunBlocking - Bridging Blocking and Non-blocking
import kotlinx.coroutines.* fun main() = runBlocking { launch { delay(500) println("Task 1 completed") } launch { delay(500) println("Task 2 completed") } println("Waiting for tasks to complete...") }
Dispatchers - Thread Management
Dispatchers control where and how your coroutines run, like assigning them to specific threads or pools .
Types of Dispatchers
import kotlinx.coroutines.* fun main() = runBlocking { // Default dispatcher - CPU-intensive work launch(Dispatchers.Default) { // Heavy computation println("CPU work on ${Thread.currentThread().name}") } // IO dispatcher - Network/File operations launch(Dispatchers.IO) { // Network request or file operation println("IO work on ${Thread.currentThread().name}") } // Unconfined dispatcher - Not recommended for general use launch(Dispatchers.Unconfined) { println("Unconfined work") } delay(100) // Wait for all tasks to complete }
WithContext - Switching Context
withContext() calls the given code with the specified coroutine context, is suspended until it completes, and returns the result :
import kotlinx.coroutines.* suspend fun fetchFromNetwork(): String { delay(1000) // Simulate network delay return "Data from network" } suspend fun updateUI(data: String) { println("UI updated with: $data") } suspend fun fetchDataAndSave() { val data = withContext(Dispatchers.IO) { // Network call fetchFromNetwork() } withContext(Dispatchers.Default) { // Update UI (using Default instead of Main for playground) updateUI(data) } } fun main() = runBlocking { fetchDataAndSave() }
Structured Concurrency
The CoroutineScope is the foundation of structured concurrency. It serves as a container for coroutines, defining the scope within which coroutines are launched .
CoroutineScope
import kotlinx.coroutines.* fun main() = runBlocking { // Creating a CoroutineScope val mainScope = CoroutineScope(Dispatchers.Default) // Launching a coroutine within the scope mainScope.launch { delay(1000) println("Coroutine executed on the Default thread") } // Wait for coroutine to complete delay(1500) // Cancelling the entire scope cancels all launched coroutines mainScope.cancel() println("Scope cancelled") }
Structured Hierarchy
With structured concurrency, you can specify the major context elements (like dispatcher) once, when creating the top-level coroutine. All the nested coroutines then inherit the context and modify it only if needed :
import kotlinx.coroutines.* suspend fun loadData1(): String { delay(1000) return "Data from source 1" } suspend fun loadData2(): String { delay(1500) return "Data from source 2" } suspend fun loadData3(): String { delay(500) return "Data from source 3" } suspend fun loadDataConcurrently(): List<String> = coroutineScope { val deferred1 = async { loadData1() } val deferred2 = async { loadData2() } val deferred3 = async { loadData3() } listOf(deferred1.await(), deferred2.await(), deferred3.await()) } fun main() = runBlocking { val results = loadDataConcurrently() println("Results: $results") }
Supervision
import kotlinx.coroutines.* fun main() = runBlocking { println("Starting supervised scope...") supervisorScope { val child1 = launch { try { delay(1000) throw Exception("Child 1 failed") } catch (e: Exception) { println("Child 1 failed: ${e.message}") } } val child2 = launch { delay(2000) println("Child 2 completed successfully") } // Wait for both children to complete child1.join() child2.join() } println("All children completed") }
Channels - Communication Between Coroutines
Channels provide a way to share information between different coroutines .
Basic Channel Usage
import kotlinx.coroutines.* import kotlinx.coroutines.channels.* fun main() = runBlocking { val channel = Channel<String>() // Producer launch { channel.send("Hello") channel.send("World") channel.close() } // Consumer launch { for (message in channel) { println("Received: $message") } } delay(100) // Wait for all tasks to complete }
Channel Types
By default, a "Rendezvous" channel is created :
import kotlinx.coroutines.* import kotlinx.coroutines.channels.* fun main() { runBlocking { // Different types of channels val rendezvousChannel = Channel<String>() val bufferedChannel = Channel<String>(10) val conflatedChannel = Channel<String>(Channel.CONFLATED) val unlimitedChannel = Channel<String>(Channel.UNLIMITED) println("Created different channel types:") println("- Rendezvous channel (capacity 0)") println("- Buffered channel (capacity 10)") println("- Conflated channel (latest value only)") println("- Unlimited channel (unlimited capacity)") // Demonstrating buffered channel launch { repeat(5) { i -> bufferedChannel.send("Message $i") println("Sent: Message $i") } bufferedChannel.close() } launch { for (message in bufferedChannel) { println("Received: $message") delay(100) } } delay(600) // Wait for all operations to complete } }
Producer Pattern
import kotlinx.coroutines.* import kotlinx.coroutines.channels.* fun CoroutineScope.produceNumbers() = produce<Int> { var x = 1 while (true) { send(x++) delay(100) } } fun main() { runBlocking { val numbers = produceNumbers() repeat(5) { println(numbers.receive()) } numbers.cancel() println("Producer cancelled") } }
Flow - Reactive Streams
Think of Flows as streams of data flowing through your server-side code, like the continuous stream of orders coming from the tables .
Basic Flow
import kotlinx.coroutines.* import kotlinx.coroutines.flow.* fun simpleFlow(): Flow<Int> = flow { for (i in 1..3) { delay(100) emit(i) } } fun main() = runBlocking { simpleFlow().collect { value -> println("Received: $value") } }
Flow Operators
import kotlinx.coroutines.* import kotlinx.coroutines.flow.* fun main() = runBlocking { flowOf(1, 2, 3, 4, 5) .filter { it % 2 == 0 } .map { it * it } .collect { println("Result: $it") } }
Cold vs Hot Flows
import kotlinx.coroutines.* import kotlinx.coroutines.flow.* // Cold Flow - starts when collected fun coldFlow() = flow { println("Cold flow started") emit(1) emit(2) } fun main() { runBlocking { println("=== Cold Flow Demo ===") coldFlow().collect { println("Cold flow value: $it") } println("\n=== Hot Flow Demo ===") val sharedFlow = MutableSharedFlow<Int>() val job1 = launch { sharedFlow.take(2).collect { println("Collector 1: $it") } } val job2 = launch { sharedFlow.take(2).collect { println("Collector 2: $it") } } delay(100) // Wait for collectors to be set up sharedFlow.emit(1) sharedFlow.emit(2) // Wait for both collectors to complete job1.join() job2.join() println("Demo completed!") } }
Exception Handling
Structured concurrency enhances error handling by propagating exceptions up to the nearest exception handler in the coroutine hierarchy .
Try-Catch in Coroutines
import kotlinx.coroutines.* suspend fun divideNumbers(a: Int, b: Int): Int { delay(1000) return a / b } fun main() = runBlocking { try { val result = coroutineScope { async { divideNumbers(10, 0) }.await() } println("Result: $result") } catch (e: Exception) { println("Exception caught: $e") } }
CoroutineExceptionHandler
import kotlinx.coroutines.* fun main() = runBlocking { val handler = CoroutineExceptionHandler { _, exception -> println("Caught exception: $exception") } val scope = CoroutineScope(Dispatchers.Default + handler) scope.launch { throw IllegalArgumentException("Something went wrong") } delay(1000) }
Cancellation and Timeout
Cooperative Cancellation
import kotlinx.coroutines.* fun main() = runBlocking { val job = launch { try { repeat(1000) { i -> println("Working $i...") delay(500) } } catch (e: CancellationException) { println("Job was cancelled") throw e } finally { println("Cleanup work") } } delay(1300) println("Cancelling job...") job.cancelAndJoin() }
Timeout
import kotlinx.coroutines.* fun main() = runBlocking { try { withTimeout(1300) { repeat(1000) { i -> println("Working $i...") delay(500) } } } catch (e: TimeoutCancellationException) { println("Timed out!") } }
Thread Safety and Synchronization
One approach to addressing shared mutable state is by using thread-safe data structures provided by the Kotlin standard library, such as Atomic types .
Atomic Operations
import kotlinx.coroutines.* import java.util.concurrent.atomic.AtomicInteger fun main() = runBlocking { val counter = AtomicInteger(0) val jobs = List(100) { GlobalScope.launch { repeat(1000) { counter.incrementAndGet() } } } jobs.forEach { it.join() } println("Final counter value: ${counter.get()}") }
Mutex
import kotlinx.coroutines.* import kotlinx.coroutines.sync.* fun main() = runBlocking { val mutex = Mutex() var counter = 0 val jobs = List(100) { launch { repeat(1000) { mutex.withLock { counter++ } } } } jobs.forEach { it.join() } println("Final counter value: $counter") }
Lesson 5 : Evolution of Kotlin
Warning
Kotlin playground not available for those new features
Explicit Backing Fields (Issue 278)
data class Element(val name: String)
class C {
val elementList: List<Element>
field = mutableListOf()
fun addElement(element: Element) {
(field as MutableList<Element>).add(element)
}
}
fun main() {
val c = C()
c.addElement(Element("First"))
c.addElement(Element("Second"))
println("Elements: ${c.elementList}")
println("Size: ${c.elementList.size}")
// field is the backing field, elementList exposes it as List<Element>
}
Collection Literals (KT-43871)
// Proposed collection literals syntax (not yet implemented)
val list = [1, 2, 3] // List<Int>
val map = ["one": 1, "two": 2, "three": 3] // Map<String, Int>
val set: Set<Int> = [1, 2, 3]
val map: MutableMap<String, Int> = ["one": 1, "two": 2, "three": 3]
val array = Array [1, 2, 3] // Array<Int>
typealias IntList = List<Int>
IntList [1, 2, 3]
fun foo(a: Set<Int>) {
println("Received set: $a")
}
fun main() {
// Using the proposed literals
foo([1, 2, 3])
println("List: $list")
println("Map: $map")
println("Set: $set")
println("Array: ${array.contentToString()}")
val intList = IntList [1, 2, 3]
println("IntList: $intList")
}
Name based destructuring (Kotlin 2.4)
data class Talk(val title: String, val speakerName: String)
fun main() {
val talk = Talk("Kotlin Coroutines", "John Doe")
// Name-based destructuring (proposed syntax)
(val speakerName, val title) = talk
println("Good!")
println("Speaker: $speakerName")
println("Title: $title")
// Benefits of name-based destructuring
println("\nBenefits:")
println("• Order doesn't matter")
println("• Properties selected by name")
println("• More readable than positional")
}
Rich Errors (KT-68296)
// Rich errors with union types (proposed syntax)
data class User(val name: String)
data class TransactionId(val id: String)
data class FetchingError(val message: String)
data class TransactionError(val errorMessage: String)
fun fetchUser(): User | FetchingError {
return if (Math.random() > 0.5) {
User("Alice")
} else {
FetchingError("Network error")
}
}
fun User.charge(amount: Double): TransactionId | TransactionError {
return if (amount > 0 && amount <= 1000) {
TransactionId("txn_${System.currentTimeMillis()}")
} else {
TransactionError("Invalid amount: $amount")
}
}
fun main() {
val user = fetchUser()
val transaction = user.charge(amount = 10.0)
when (transaction) {
is TransactionId -> println("Transaction succeeded")
is FetchingError -> println("Fetching failed")
is TransactionError ->
println("Transaction failed: ${transaction.errorMessage}")
}
println("Rich errors provide type-safe error handling")
}
Must return values
// Must-use return values (proposed syntax)
data class User(val name: String)
data class TransactionId(val id: String)
data class FetchingError(val message: String)
data class TransactionError(val errorMessage: String)
fun fetchUser(): User | FetchingError {
return if (Math.random() > 0.5) {
User("Alice")
} else {
FetchingError("Network error")
}
}
fun User.charge(amount: Double): TransactionId | TransactionError {
return if (amount > 0 && amount <= 1000) {
TransactionId("txn_${System.currentTimeMillis()}")
} else {
TransactionError("Invalid amount: $amount")
}
}
fun main() {
val user = fetchUser()
when (val transaction = user?.charge(amount = 100.0)) {
is TransactionId -> println("Transaction completed! Thanks!")
is TransactionError -> println("Error: ${transaction.errorMessage}")
else -> println("User fetch failed")
}
// Must-use prevents ignoring critical return values
// fetchUser() // Would cause compiler error - return value must be used
}
Additional Kotlin Language Features
Feature | Description | Status | Example Use Case |
---|---|---|---|
Multi-field value classes | Extension of value classes to support multiple fields while maintaining performance benefits | Proposed | value class Coordinates(val x: Double, val y: Double) - Efficient data containers with multiple properties |
Context parameters | Implicit parameter passing mechanism, similar to Scala's implicit parameters | Experimental | Dependency injection, configuration passing without explicit parameter threading |
Infinite loop guards | Compiler protection against accidental infinite loops in certain contexts | Proposed | Prevent while(true) without break conditions, timeout mechanisms for long-running operations |
HexFormat | Built-in hexadecimal formatting and parsing utilities | Available in Kotlin 1.9+ | HexFormat.of().formatHex(byteArray) - Easy hex string conversion for cryptography, debugging |
Kotlin statics and static extensions | Enhanced static member support and extension functions on companion objects | Proposed | Better Java interop, cleaner static utility functions, enhanced companion object capabilities |
Resources for Latest Updates
Resource | Purpose |
---|---|
github.com/Kotlin/KEEP | Official Kotlin Enhancement Proposals - track feature development and proposals |
x.com/kotlin | Official Kotlin Twitter/X account - announcements, updates, and community news |