Week 1 | Lesson 2

Kotlin Essentials

Variables, Operators, Functions, Control flow, Collections

Object-oriented programming

Object-oriented programming

There are four main principles in OOP ...
  1. Encapsulation
    is a concept of controlling access to the internal state of an object, protecting it from unauthorized access and ensuring data integrity.
  2. Inheritance
    enables class to have the same behavior as another class by inheriting its properties and methods.
  3. Polymorphism
    allows us to define one interface or method that can have multiple implementations. It means that the same method or property could exhibit different behavior in different instances of object implementing given interface.
  4. Abstraction
    is a mechanism to represent the object features without exposing the actual implementation details. In other words, user of such object only needs to know what it does, not how it does it.

We will come back to these principles later in the course.

Kotlin Language Syntax

Overview

Program entry point

Program entry point is the first function that is 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("Argument: $arg")
    										}
    									}
    								
    							

Functions

Function is declared using the fun keyword, followed by the function name, parameters in parentheses, return type, and function body enclosed in curly braces.
  • Function may have zero or more parameters, and it may return a value, in which case the return type is specified.
    								
    									fun myFunction(arg1: Int, arg2: Int): Int {
    										return arg1 + arg2
    									}
    								
    							
  • If a function does not return a value, the return type is Unit, but the return type can be omitted.
    								
    									fun myFunction(arg1: Int, arg2: Int) {
    										println(arg1 + arg2)
    									}
    								
    							

Variables

Variables in Kotlin are declared using the var or val keyword, followed by the variable name, type, and optional value.
  • Variables declared with var are mutable = their value can be changed during the program execution.
  • Variables declared with val are immutable = their value cannot be changed once it is assigned = they are read-only.
  • You can omit type in case variable is declared and initialized at the same time, it will be inferred by the compiler.
    								
    									val myNumber = 42
    
    									val myText = "Hello, world!"
    								
    							
  • If mutable variable is not initialized at the time of declaration, you must specify it's type.
    								
    									var myNumber: Int
    
    									var myText: String
    								
    							

Classes

Classes in Kotlin are declared using the class keyword, followed by the class name, an optional constructor, and the class body enclosed in curly braces.
  • Classes can have properties, functions, and multiple constructors.
  • Classes are also usually part of a package, which is a way to organize classes into namespaces.
  • Example:
    								
    									package com.package.domain
    
    									class MyClass {
    										var myVariable: Int = 42
    										val foo: Foo = Foo()
    
    										/*
    										Block comment
    										*/
    										fun myFunction(number: Long): String {
    											// important comment
    											return foo.bar()
    										}
    									}
    								
    							

We will talk more about classes later.

Printing

You can use print function to print on the same line, and println function to print with a new line at the end.
						
							fun main() {
								print("Hello, ")  // prints on one line
								println("world!") // prints on with a new line at the end
							}
						
					

Comments

You can use // for single-line comments and /* */ for multi-line comments.
						
							// single-line comment

							/*
							Multi-line comment
							*/
						
					

Naming Conventions

Kotlin naming conventions are similar to Java naming conventions ...
  • Package name is always written in all-lowercase.
  • Class or interface name should be nouns in mixed case with the first letter of each internal word capitalized. Should be sufficiently descriptive.
  • Method name should be verb, in mixed case with the first letter lowercase, with the first letter of each internal word capitalized.
  • Variable name should be in mixed case with a lowercase first letter. Internal words start with capital letters. Should be meaningful and descriptive
  • Constants name should be noun with each letter of internal word capitalized. Should be sufficiently descriptive.

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?

Kotlin Data Types

Kotlin data representation is based on Java data types,
but with some key differences ...

Java

  • There are two groups of data types in Java - primitive and non-primitive types
  • Primitive types are the basic data types that are built into the language.
  • Primitive types
    • byte, short, int, long, float, double, char, boolean
  • Non-primitive types
    • String, Arrays, Classes, ...

Kotlin

  • Kotlin has representation corresponding to Java primitive types, but unlike Java, they are all objects.
  • Because they are objects, they have methods and properties.
  • When Kotlin code gets compiled, the compiler will convert these objects to Java primitive types.

    Knowing what JVM primitive type they compile to is important for understanding how they are stored in memory.

    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.

Numeric Types

Integer types

Byte1 bytewhole number from -128 to 127
Short2 byteswhole number from -32768 to 32767
Int4 byteswhole number from -2147483648 to 2147483647
Long8 byteswhole number from -9223372036854775808 to 9223372036854775807


Unsigned Integer types

UByte1 bytewhole number from 0 to 255
UShort2 byteswhole number from 0 to 65535
UInt4 byteswhole number from 0 to 4,294,967,295 (232 - 1)
ULong8 byteswhole number from 0 to 18,446,744,073,709,551,615 (264 - 1)


Floating-point types

Float4 bytesfractional number up to 7 decimal digits
Double8 bytesfractional number up to 15 decimal digits

Numeric Types

Integer types

						
							val index: Byte = 127
							val smallNumber: Short = 32767
							val number: Int = 2147483647
							val bigNumber: Long = 9223372036854775807L // notice L at the end
						
					

Unsigned integer types

						
							val uIndex: UByte = 255u // u indicates unsigned type
							val uSmallNumber: UShort = 65535u
							val uNumber: UInt = 4294967295u
							val uBigNumber: ULong = 18446744073709551615u
						
					

Floating-point types

						
							val decimalNumber: Float = 123.12346f // f indicates float type
							val preciseNumber: Double = 123.12345886230469
						
					

Non-numeric Data Types

Boolean1 bytevalue of true or false
Char2 bytesa single 16-bit Unicode character
Stringapproximately 2 bytes per characterUTF-16 encoded string of characters
Arraydepends Fixed number of values of the same type or its subtypes.
						
							val isTrue: Boolean = true
							val character: Char = 'A'
							val text: String = "Hello, world!"

							val arrayOfStrings = arrayOf("hello", "world", "kotlin", "is", "fun")

							println(arrayOfStrings[2]) // will print "kotlin"
						
					
Unless there are specific memory performance requirements, you should prefer using Collections over Arryas.

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.

						
							val someValue: Any = "This is a string"

							if (someValue is String) {
								println("The value is a string")
							} else {
								println("The value is not a string")
							}
						
					

null

In Java/Kotlin, null is a special value that represents the absence of an instance.
It is used to indicate that a reference variable doesn't point to any memory location or object.
(Therefore by definition, primitive types cannot be null)

Some key point to keep in mind when working with nulls in Java/Kotlin code ...

  • Since Kotlin and Java are interoperable, you may need to explicitly handle null values when calling Java methods from Kotlin.
  • In Java
    • you can assign null to any reference variable (non-primitive types: objects, array, interface, etc)
    • for object reference types, the default value is null when they are defined as class members and not explicitly initialized
    • If you try to invoke a method or access a property on a reference variable with a null value, you will get a NullPointerException. This is a runtime exception in Java.
  • In Kotlin, object references that may hold null values must be explicitly declared as nullable types.
  • You can use null in comparison operations. For example, to check if an object is null, you can use if (object == null).
  • Assigning null to a variable makes it eligible for garbage collection if there are no other references to the object.
  • Null can be passed as an argument to a method and can also be returned from a method.

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"
								
							
Use the let function to execute a block of code only if the value is not null.
								
									nullableText?.let { printText(it) }
								
							

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:

							
								val number = 42             // type inferred as Int
								val text = "Hello, world!"  // type inferred as String
							
						

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

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

							println(something is Int)			 // true
							println(something is Long)			 // false
							println(something::class.simpleName) // Int
						
					

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 getSomething(): Any {
								return 1234567890
							}
						
					
						
							val something = getSomething()

							// Explicit type casting, only works if the value is of the same type
							val number = something as Int
						
					

Type Conversion

						
							val number = 1234567890
							val bigNumber = number.toLong() // Type conversion, no data loss
							val smallNumber = number.toShort() // Type conversion with DATA LOSS!
						
					

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.

Operators

Operators

  • Assignment operators
  • Arithmetic operators
  • Comparison operators
  • Logical operators
  • Bitwise operators
    but they are out of scope for this course

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()
						
					
Variables and constants can be declared as read-only, using val keyword, or mutable, using var keyword.
								
									var name: String = "John"
									name = "Jane"
									println(name)
								
							

Mutable variable value can be changed during the program execution.
								
									val name: String = "John"
									name = "John"
									println(name)
								
							

This will throw an error, because name is declared as read-only.

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.
+ addition
										
											val sum = a + b
										
									
a = 5, b = 2, sum = 7
- subtraction
										
											val difference = a - b
										
									
a = 5, b = 2, difference = 3
* multiplication
										
											val product = a * b
										
									
a = 5, b = 2, product = 10
/ division
										
											val quotient = a / b
										
									
a = 5, b = 2, quotient = 2 (integer division)
% modulus
										
											val remainder = a % b
										
									
a = 5, b = 2, remainder = 1
++ increment (by 1)
										
											val increment1 = ++a
											val increment2 = a++
										
									
a = 7, increment1 = 6, increment2 = 6
-- decrement (by 1)
										
											val decrement1 = --b
											val decrement2 = b--
										
									
b = 1, decrement1 = 0, decrement2 = 0

Arithmetic Operators

Notice ++ and -- can be put before or after a value. What is the difference?

What will this print?
A)
						
							var a = 0
							println(a++)
						
						0, because is incremented (by 1) after it is printed
					
B)
						
							var a = 0
							println(++a)
						
						1, because is incremented (by 1) before it is printed
					
C)
						
							val a: Int = 5
							val b: Int = 3

							println(a / b)
						
						
							1, because 5 / 3 = 1.666666666666667, but since it is stored as integer, the decimal places are dropped
						
					
D)
						
							val a: Int = 5
							val b: Int = 3

							println(a % b)
						
						
							2, because remainder of integer 5 /3 is 2
						
					

Addition Assignment

Combines assignment operators with arithmetic operators.
						
							var number = 0

							number += 3 // equivalent: number = number + 3

							number -= 3 // equivalent: number = number - 3

							number *= 3 // equivalent: number = number * 3

							number /= 3 // equivalent: number = number / 3

							number %= 3 // equivalent: number = number % 3
						
					

Comparison Operators

As the name suggest, comparison operators are used for comparing values.
They always yield a boolean value.
== equals
									
										val isEqual = a == b
									
								
!= not equals
									
										val isNotEqual = a != b
									
								
> is greater
									
										val isGreater = a > b
									
								
< is less
									
										val isLess = a > b
									
								
>= is greater or equal
									
										val isGreaterOrEqual = a >= b
									
								
<= is less or equal
									
										val isLessOrEqual = a >= b
									
								

Comparison Operators

Given these values, what will be the value of result?
						
							val a = 5
							val b = 3
							val c = 2
						
					
A)
						
							val result = a > b
						
					
B)
						
							val result = a <= b
						
					
C)
						
							val result = a > (b + c)
						
					
D)
						
							val result = a >= (b + c)
						
					

Logical Operators

Logical operators are used to evaluate logic between variables or values.
They always yields boolean value.
&& logical and returns true if both statements are true
										
											val isTrue = statement1 && statement2
										
									
|| logical or returns true if either statement is true
										
											val isEitherTrue = statement1 || statement2
										
									
! logical not reverses the result
									
										val neitherIsTrue = !(statement1 || statement2)
									
								
and logical and works same as && but doesn't short-circuit
									
										val isTrue = statement1 and statement2
									
								
or logical or works same as || but doesn't short-circuit
									
										val neitherIsTrue = !(statement1 or statement2)
									
								
Short circuiting means that if the first part of the statement is false, the second part will not be evaluated.

Logical Operators

Given two string variables that can have values cat, dog, or null.
						
							val pet1: String? = "cat"
							val pet2: String? = null
						
					
How would you test if ...

A) pet1 and pet2 are the same kind of pet?
							
								val hasTwoSamePets = pet1 == pet2
							
						
B) pet1 and pet2 are both cats?
							
								val has2Cats = pet1 == "cat" && pet2 == "cat"
							
						
C) At least one of pet1 and pet2 is a cat?
							
								val hasACat = pet1 == "cat" || pet2 == "cat"
							
						
D) At least one of pet1 and pet2 is a pet (not null)?
							
								val hasAPet = pet1 != null || pet2 != null
							
						

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
							}
						
					
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 NOT 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 tenary operator like Java, 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.
						
							val randomInt = arrayOf(0, 1, 2, 3, 4, 5).random()
						
					
						
							val result = when (randomInt) {
								0 -> "Zero"
								1 -> "One"
								2 -> "Two"
								3 -> "Three"
								4 -> "Four"
								5 -> "Five"
								else -> "Too much"
							}
						
					
						
							val result = when {
								randomInt < 0 -> "Less than 0"
								randomInt == 0 -> "Zero"
								else -> "Greater than 0"
							}
						
					
						
							val result = when (randomInt) {
								0 -> "Zero"
								in 1..3 -> "Between 1 and 3"
								in 3..5 -> "Between 3 and 5"
								else -> "Too much"
							}
						
					

For Loop

The for loop is used to iterate over anything that provides an iterator, such as a range, array, or a collection.
The basic syntax of for loop is ...
							
								for (item in collection) println(item)
							
						
or the body can be a block ...
							
								val languages = arrayOf("Java", "Kotlin", "Python")

								for (language in languages) {
									println(language)
								}
							
						
You can also use range expression to iterate over a range of numbers.
							
								for (i in 10 downTo 0 step 2) {
									println(i)
								}
							
						
							
								for (i in 1..5) {
									println(i)
								}
							
						

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.

						
							while (condition) {
							  // code block to be executed
							}
						
					

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

						
							do {
							  // code block to be executed
							} while (condition)
						
					

While Loop / Do While Loop

How may times will "Ahoy!" be printed?
A)
						
							var counter = 10

							while (counter < 10) {
								println("Ahoy!")
								counter++
							}
						
						0 - it will never be printed
					
B)
						
							var counter = 10

							do {
								println("Ahoy!")
								counter++
							} while (counter < 10)
						
						1 - once
					

Working with Strings

Working with Strings

						
							/*
							 Printing to console
							 */
							println("Printing to console with new line at the end")
							print("Printing to console without new line at the end")
							println() // just new line

							/*
							 String concatenation using the + operator
							 */
							val name = "Moni"
							val hello = "Hello"

							val greeting = hello + " " + name + "!"
							println(greeting)

							/*
							 String interpolation
							 */
							val fullGreeting = "$hello $name"
							println(fullGreeting)

							/*
							 String concatenation using the += operator
							 */
							var greeting2 = "Hola"
							greeting2 += " $name!" // Assignment addition String concatenation
							println(greeting2)

							/*
							 String formatting
							 */
							val formattedGreeting = String.format("%s %s!", hello, name) // String formatting
							println(formattedGreeting)
						
					

Working with Strings

						
							val text = "Banana, Apple, Orange, Kiwi, Mango, Pineapple, Watermelon, Strawberry"

							/*
							 Find the length of the text
							 */
							println("Length: " + text.length)

							/*
							 Split the text into an array of fruits
							 */
							val fruits = text.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
							for (fruit in fruits) {
								println(fruit.trim { it <= ' ' }) // trim to remove leading or trailing space
							}

							/*
							 Check if the text contains "Apple"
							 */
							println("Has apple: " + text.contains("Apple"))
							println(text.uppercase())
							println(text.lowercase())
							println(text.substring(10, 20))

							/*
							 Replace all occurrences of "Apple" with "Peach"
							 */
							val newText = text.replace("Apple", "Peach")
							println(newText)
						
					

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 add(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 }
								
							
Functions can be defined at the top level of a file, meaning they do not need to be part of a class.

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
							}
						
					

Default arguments

Kotlin allows you to specify default values for function arguments, making them optional.
								
									fun round(
										value: Double,
										decimals: Int = 2 // argument with default value od 2
									): Double {
										val factor = 10.0.pow(decimals.toDouble())
										return (value * factor).roundToInt() / factor
									}
								
							

Given a function with a default argument ...
								
									val result = round(3.14159)

									println(result) // prints 3.14
								
							

When the argument is not provided when calling the function, the default value is used.
								
									val result = round(3.14159, 4)

									println(result) // prints 3.1416
								
							

When the argument is provided when calling the function, the provided value is used.

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 ofthe comes in handy when refactoring the code, adding or changing order of arguments.

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

Given function ...
								
									val result = round(value = 3.14159)
								
							

You may specify the argument name.
								
									val result = round(
										decimals = 4, // notice the changed order of arguments
										value = 3.14159
									)
								
							

You may specify the argument names in any order.

Named arguments

A bigger example of using named arguments:
								
									fun calculateEvapotranspiration(
										temperature: Double,
										solarRadiation: Double,
										humidity: Double,
										windSpeed: Double,
										atmosphericPressure: Double,
										location: Pair<Double, Double>,
										time: LocalDateTime,
										soilType: SoilType,
										cropType: CropType
									): Double {
										// calculation
									}
								
							
Without argument names ...
								
									calculateEvapotranspiration(
										25.0,
										800.0,
										0.6,
										3.0,
										1013.25,
										13.7655756 to 100.5675686,
										LocalDateTime.now(),
										SoilType.CLAY,
										CropType.WHEAT,
									)
								
							
With argument names ...
								
									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): Int {
								println("Hello, ${names.joinToString(", ")}!")
							}
						
					
						
							fun main() {
								sayHello("Alice", "Bob", "Charlie") // prints "Hello, Alice, Bob, Charlie!"
							}
						
					

If you need more than one argument, the vararg argument must be the last one.

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

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", "Paris")

							// 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 [] 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.
						
							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 property.
						
							println(array.size) // prints 5
						
					

Array Operations

Given and array, some of the common operations on arrays include ...
Iterating an array using a for loop or forEach function.
							
								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.sort() // in ascending order

								val reversed = array.reverse()

								val shuffled = array.shuffle()
							
						
We will talk more about array and collection operations in the next lessons.

Collections

Collections

Collections are similar to arrays, but they are more flexible and have more features at the cost of being less efficient in terms of memory and performance.

The main difference between arrays and collections is that collections can grow or shrink in size. They generally provide more functionality and are easier to work with than arrays, but also are less efficient in terms of memory and performance.

There are several types of collections in Kotlin, such as List, Set, Map, etc.

Unlike and array, which is basic data structure, collections are interfaces that define a set of operations that can be performed on a group of objects.

Collections

Collection is a group of variable number of objects of the same type (and its subtypes).

The Kotlin Standard Library provides implementations for basic collection types: sets, lists, and maps. A pair of interfaces represent each collection type:

  • read-only interface
    provides operations for accessing collection elements.
  • mutable interface
    extends the corresponding read-only interface with write operations: adding, removing, and updating its elements.

See Kotlin documentation for more details.

Collections

There are 3 main types of collections in Kotlin: lists, sets, and maps.

Lists

Lists are ordered collections of elements that can contain duplicates and individual elements can be accessed by their index.

Sets

Sets are unordered collections of unique elements, meaning order is not guaranteed, and they don't allow duplicate elements.

You can work with a set just like you would with a list, but there are some differences:

  • You cannot access elements by index, because sets are unordered.
  • Adding an element that already exists in the set will not add a duplicate.
  • Removing an element that does not exist in the set will not throw an exception.


Maps

Maps are collections of key-value pairs, where keys are unique and are used to access values. Values can be duplicates.

Kotlin provides standard library functions for working with collections, which we will explore in more detail.

Constructing Collections

There are standard library functions for constructing collections in Kotlin for both read-only and mutable collections.

Collections are constructed using functions listOf<Type>(), setOf<Type>() or mapOf<KeyType, ValueType>() for read-only collections.

						
							val list = listOf<String>()

							val set = setOf<Int>()

							val map = mapOf<String, Int>()
						
					

Or by variable type declaration.

						
							val list: List<String> = listOf()
						
					

If type can be inferred from the elements, you can omit the type declaration.

						
							val list = listOf("Java", "Kotlin", "JavaScript", "TypeScript", "Python")

							val set = setOf(2020, 2021, 2022, 2023, 2024, 2025)

							val map = mapOf(
								"Java" to 1995,
								"Kotlin" to 2011,
								"JavaScript" to 1995,
								"TypeScript" to 2012,
								"Python" to 1991
							)
						
					

Constructing Collections

There are standard library functions for constructing collections in Kotlin for both read-only and mutable collections.

Mutable collections can be created using mutableListOf<Type>(), mutableSetOf<Type>() and mutableMapOf<KeyType, ValueType>().

To construct an empty collection, you can use the emptyList<Type>(), emptySet<Type>() or emptyMap<KeyType, ValueType>() functions.

						
							val emptyList = emptyList<String>()

							val emptySet = emptySet<Int>()

							val emptyMap = emptyMap<String, Int>()
						
					

Similarly, empty collections can be created emptyMutableList<Type>(), emptyMutableSet<Type>() and emptyMutableMap<KeyType, ValueType>() functions.

Arrays vs. Collections

Both arrays and Collections are used to store data.
There are however some notable differences that make them suitable for different use cases.
Arrays Collections
Size Arrays have fixed size. This may lead to memory wastage, but is also inconvenient to work with. Collections can grow or shrink dynamically to accommodate the data.
Type Safety Arrays are type-safe Collections are type-safe (through generic typing)
Performance Arrays can perform better than collections for some operations because of their simpler memory layout, lower overhead, and ability to employ direct indexing. Collections have more overhead than arrays, and certain operations may be slower as a result. However, the built-in utilities in collections make them more convenient for complex data manipulation.
Functionality Arrays offer basic functionality such as adding elements, getting elements, and modifying existing elements. Collections provide a wide variety of functionalities. They can be sorted, reversed, shuffled. They support operations like addition, inspection, modification, deletion, searching and other.
Use Cases Arrays are best for fixed-size collections where performance is critical. Collections are best for dynamic collections with rich functionality and advanced operations.

Working with Collections

Working with Collections

The Kotlin standard library provides a rich set of functions for working with collections.

Collection operations are declared in the standard library in two ways:

  1. Member functions of collection interfaces defining operations that are essential for the collection type.
  2. Extension functions providing additional functionality.

This is important to know in case you want to implement you own collection type as you will need to implement all functions in the given interface(s).

Some of the common operations on collections include:

  • Transformations
  • Filtering
  • plus and minus operators
  • Grouping
  • Retrieving collection parts
  • Retrieving single elements
  • Ordering
  • Aggregate operations

Adding, Removing and Retrieving Elements

Adding Elements

For immutable collections, you can use the plus() function to create a new collection with the added element.

						
							val list = mutableListOf("Java", "Kotlin", "JavaScript", "TypeScript")

							list.plus("Python")
						
					

For mutable collections, you can use the add(), addFirst(), addLast() and addAll()functions to add an elements to the collection.

						
							val mutableList = mutableListOf("Java", "Kotlin")

							mutableList.add("C#")
							mutableList.addLast("Rust")
							mutableList.addAll(listOf("JavaScript", "TypeScript"))
						
					

For both mutable and immutable collections, you can use the + operator to create a new collection with the added element.

						
							val newList = list + "Python"
						
					

You can also add elements to mutable collections using the += operator.

						
							mutableList += "Python"
						
					

And finally, you can use the addAll(), addFirst(), and addLast() functions to add multiple elements to a mutable collection.

Removing Elements

Removing elements is similar.

Immutable collections provide the minus() function and the - operator to create a new collection with the removed element.

						
							val list = mutableListOf("Java", "Kotlin", "JavaScript", "TypeScript", "Python")

							val newList = list - "JavaScript"
						
					

Mutable collections provide the remove(), removeFirst(), removeLast() removeAt(), removeAll() and also removeIf() functions to remove an element from the collection.

						
							val mutableList = mutableListOf("Java", "Kotlin", "JavaScript", "TypeScript", "Python")

							// removes element "JavaScript"
							mutableList.remove("JavaScript")

							// removes element at index 2
							mutableList.removeAt(2)

							// removes elements "Java" and "Kotlin"
							mutableList.removeAll(listOf("Java", "Kotlin"))

							// removes elements with length > 5
							mutableList.removeIf { it.length > 5 }
						
					

You can also use the -= operator to remove an element from a mutable collection.

						
							mutableList -= "JavaScript"
						
					

Retrieving Elements

Retrieving elements from a collection is straightforward and similar to arrays.

Here are few examples:

						
							val list = listOf("Java", "Kotlin", "JavaScript", "TypeScript", "Python")

							// get element at index 2
							val element = list[2]

							// get first element
							val first = list.first()

							// get last element
							val last = list.last()

							// get element at index 2 or return "C++" if index is out of bounds
							val elementAtOrElse = list.getOrElse(2) { "C++" }

							// get element at index 10 or return null if index is out of bounds
							val elementAtOrNull = list.getOrNull(10)

							// you can also use the random() function to get a random element from the collection
							val randomElement = list.random()
						
					

Retrieving collection parts

You are not limited to retrieving single elements from a collection. You can also retrieve parts of a collection, or slices.

These are some of the functions available in the Kotlin SDK:

  • slice - returns a list of elements at the specified indices.
  • take - returns a list of the first n elements.
  • takeLast - returns a list of the last n elements.
  • takeWhile - returns a list of elements that match the predicate.
  • drop - returns a list of elements after the first n elements.
  • dropLast - returns a list of elements before the last n elements.
  • dropWhile - returns a list of elements after the first element that does not match the predicate.

Traversing Collections

For Loop

You can use a for loop to iterate over collections that implement the Iterable interface
(or its subtypes).

Iterators are not the most idiomatic way to iterate over collections, so Kotlin provides other ways to iterate over collections which implement the Iterable interface.

One of such ways is to use a for loop.

						
							val list = listOf("Java", "Kotlin", "JavaScript", "TypeScript", "Python")

							for (element in list) {
								println(element)
							}
						
					

forEach and forEachIndexed

Another way to iterate over collections is to use the forEach and forEachIndexed functions.

Both are higher-order function that takes a lambda as an argument. The basic syntax is ...

						
							val list = listOf("Java", "Kotlin", "JavaScript", "TypeScript", "Python")

							list.forEach {
								println(it)
							}
						
					

By default, the lambda passed to the forEach function takes a single argument, which can be referenced using the it keyword.

You can also specify the argument name explicitly (in this case, element).

						
							list.forEach { element ->
								println(element)
							}
						
					

There is also a shorthand syntax for the lambda if it takes a single argument: list.forEach(::println).

The forEachIndexed also provides the index of the element as the first argument to the lambda. This may be useful in some situations.

						
							list.forEachIndexed { index, element ->
								println("Element at index $index is $element")
							}
						
					

Collection Transformations

Collection Transformations

There are many operations that can be performed on collections to transform them in some way which are part of the Kotlin SDK.

Some common transformation operations performed on collections include:

  • map using functions like map, flatMap, mapNotNull, mapIndexed, mapIndexedNotNull.
  • filter using functions like filter, filterNot, filterIndexed, filterNotNull, distinct, distinctBy.
  • sort using functions like sorted, sortedBy, sortedWith, sortedDescending, sortedByDescending, reversed, shuffled.
  • group using functions like groupBy, partition, associate, associateBy, associateWith.
  • plus, minus to add or remove elements from a collection.
  • other transformation functions like reduce, zip, zipWithNext, unzip, flatten, fold.

All of these transformations return a new collection with the transformation applied, they do not modify the original collection.

Mapping functions

map is a transformation operation that applies a function to each element in the collection and returns a new collection with the results.

The returned collection can be of any type, not necessarily the same as the original collection.

There are several map functions available in the Kotlin standard library:

  • map - applies a function to each element and returns a list of the results.
  • mapNotNull - applies a function to each element and returns a list of non-null results.
  • mapIndexed - applies a function to each element and its index and returns a list of the results.
  • mapIndexedNotNull - applies a function to each element and its index and returns a list of non-null results.
  • flatMap - applies a function to each element and returns a list of the results, which are then flattened into a single list.
  • mapTo, mapIndexedTo, mapNotNullTo, etc ... - applies a function to each element and adds the results to the given destination.

Filtering

Kotlin SDK again provides us with a rich set of functions for filtering collections. Like map, filter returns a new collection with the elements that satisfy a predicate.

Generally, the returned collection is the same type as the original collection.

Some of the filter functions available in the Kotlin standard library include:

  • filter - filters elements based on a predicate and returns a list of elements that satisfy the predicate.
  • filterNot - filters elements based on a predicate and returns a list of elements that do not satisfy the predicate.
  • filterNotNull - filters out null elements and returns a list of non-null elements.
  • filterIndex - filters elements based on a predicate with index and returns a list of elements that satisfy the predicate.

Grouping

Given a collection, you can group elements based on a key.

The result of grouping operations is a map where the key is the result of the selector function and the value is a list of elements.

Some of the grouping functions available in the Kotlin SDK include:

  • groupBy - groups elements by the result of the given selector function.
  • partition - splits the collection into a pair of lists based on a predicate.
  • associate - creates a map from the elements of the collection.
  • associateBy - creates a map from the elements of the collection using the provided key selector function.
  • associateWith - creates a map from the elements of the collection using the provided value selector function.
								
									val priceList = listOf(
										"Mango" to 20,
										"Apple" to 25,
										"Banana" to 10,
										"Coconut" to 15,
										"Pineapple" to 30,
										"Orange" to 5,
										"Grapes" to 40
									)

									val priceCategory = priceList.groupBy { (_, price) ->
										when (price) {
											in 0..10 -> "Cheap"
											in 11..20 -> "Affordable"
											in 21..30 -> "Expensive"
											else -> "Very Expensive"
										}
									}
								
							

Result of this will be:

								
									Affordable=[(Mango, 20), (Coconut, 15)]
									Expensive=[(Apple, 25), (Pineapple, 30)]
									Cheap=[(Banana, 10), (Orange, 5)]
									Very Expensive=[(Grapes, 40)]
								
							

Sorting

Collections can be sorted in various ways using the Kotlin SDK.
  • sorted - sorts elements in natural order.
  • sortedBy - sorts elements by the result of the given selector function.
  • sortedWith - sorts elements using the given comparator.
  • sortedDescending - sorts elements in reverse natural order.
  • sortedByDescending - sorts elements by the result of the given selector function in reverse order.
  • reversed - reverses the order of elements in the collection.
  • shuffled - shuffles the elements in the collection.

Examples:

						
								val priceList = listOf(
									"Mango" to 20,
									"Apple" to 25,
									"Banana" to 10,
									"Coconut" to 15,
									"Pineapple" to 30,
									"Orange" to 5,
									"Grapes" to 40
								)

								priceList.sortedBy { (_, price) -> price }

								priceList.sortedByDescending { (_, price) -> price }

								priceList.sortedWith(
									compareBy(
										{ it.first },
										{ it.second }
									)
								)
						
					

Other Transformation Functions

There are even more useful transformation functions available in the Kotlin SKD. Some worth mentioning are:

  • reduce - combines elements of a collection into a single value.
  • zip - combines two collections into a single collection of pairs.
  • zipWithNext - combines each element with the next element in the collection.
  • unzip - splits a collection of pairs into two collections.
  • flatten - flattens a collection of collections into a single collection.
  • fold - combines elements of a collection into a single value starting with an initial value.

Sequence

Sequence

Kotlin standard library provides a Sequence additional to collections.

Unlike collections, sequences don't contain elements, they produce them while iterating. This is useful when you need to perform multi-step operations on a collection.

Operations on collections are executed eagerly, meaning they perform all operations on all elements immediately.
Operations sequences are executed lazily, meaning they perform operations on elements only when needed.

This can be beneficial for large collections or when you need to perform complex operations on elements.
On the other hand, sequences may be less efficient for small collections or simple operations.

Sequences offer the similar functions as collections, such as forEach, map, filter, etc.

The main difference is that when working with sequences, we distinguish between intermediate and terminal operations, where intermediate operations return a new sequence and terminal operations return a result.

What that means is that when you call a terminal operation, all intermediate operations are executed and the collection is so called "consumed".

!!!    Keep this in mind because if you call a terminal operation prematurely, you may either end up with unexpected results, or at least with a performance hit.    !!!

Creating Sequences

From elements:
						
							val sequence = sequenceOf(1, 2, 3, 4, 5)
						
					
From an Iterable:
						
							val sequence = listOf(1, 2, 3, 4, 5).asSequence()
						
					
From a function:
						
							val sequence = generateSequence(1) { it + 1 }
						
					
From chunks:
						
							val sequence = sequence {
								for (i in 1..5) {
									yield(i)
								}
							}
						
					

Packages, Imports and Modifiers

Packages

Packages are used to organize code into namespaces, making it easier to manage and avoid naming conflicts.
  • The package declaration is usually the first line in a Kotlin file. It specifies the package to which the file belongs.
  • Package names are typically written in all lowercase and follow the reverse domain name convention.
  • To use classes and functions from other packages, you need to import them using the import keyword.
  • If no package is specified, the file belongs to the default package.
						
							package com.motycka.edu.model // <- package name

							import java.time.LocalDate // <- import of class LocalDate from java.time package

							class User(
								val name: String,
								val birthDate: LocalDate
							)
						
					

Modifiers

Modifiers in programming languages are keywords that you can use to change the properties or behavior of classes, methods, and variables.

They can be broadly categorized into two types:


Access Modifiers

These define the visibility or accessibility of functions, classes, methods, and variables.


Non-Access Modifiers

These define other characteristics such as behavior, state, or implementation details.

Access Modifiers

Access, modifiers define the visibility or accessibility of functions, classes, methods, and variables.
modifier on class on method on properties
public accessible from anywhere accessible from anywhere accessible from anywhere
private only accessible within the same package only accessible within same class only accessible within same class
protected accessible within the same package only accessible within same class or it's subclass only accessible within same class or it's subclass
internal accessible within the same module accessible within the same module N/A
default same as public same as public same as public

Non-Access Modifiers

Non-access modifiers define other characteristics such as behavior, state, or implementation details.
modifier on class on method / block on properties
abstract Class marked as abstract cannot be directly instantiated. Method marked as abstract does not provide implementation, but expects a subclass to implement it. N/A
final prevents inheritance prevents method overloading Makes variable a constant = value cannot be changed after initialization.
open Allows class to be inherited Allows method to be overridden Allows property to be changed
override N/A Indicates that a method is overriding a method in a superclass. N/A
lateinit N/A Indicates that a property will be initialized later. N/A
const N/A N/A Indicates that a property is a compile-time constant.
companion N/A N/A Defines a companion object, which is an object that is tied to a class and can access its private members.

Advanced Functions (Optional)

Anonymous Functions & Lambda Expressions
Extension Functions
Scope Functions

Anonymous Function & Lambda Expression

Anonymous functions and lambda expressions are used to define functions without names.

Lambda Expression

Lambda expressions are typically used for short, concise functions that are passed as arguments to higher-order functions. They are commonly used in collection operations like , filter, and forEach.

						
							val lambdaName: (Type) -> ReturnType = { argument: Type -> body }
						
					
If lambda expression has a single parameter, you can use the default name it.

Anonymous Function

Anonymous functions are used when you need more control over the function's return type or when you need to use the return statement to exit the function itself rather than the enclosing function.

						
							val lambdaName = fun(name: Type): Type {
								return value
							}
						
					
Opposite of anonymous function is called a named function.

Lambda Expression

Examples

Function with one parameter and no return value:

						
							val greet: (String) -> Unit = { name -> println("Hello, $name!") }

							greet("World")
						
					

If lambda expression has a single parameter, you can use the default name it:

						
							val greet: (String) -> Unit = { println("Hello, $it!") }

							greet("World")
						
					

Function with two parameters and a return value:

						
							val multiply: (Int, Int) -> Int = { a, b -> a * b }

							val result = multiply(10, 20)
						
					

Common examples of lambda function is the forEach:

						
							val cities = listOf("Bangkok", "Barcelona", "Tokyo", "London", "New York")

							cities.forEach { city ->
								println(city)
							}
						
					

Anonymous Function

Examples
Anonymous function with one parameter and no return value:
							
								val greet = fun(name: String) {
									println("Hello, $name!")
								}

								greet("World")
							
						
Anonymous function with two parameters and a return value:
							
								val multiply = fun(a: Int, b: Int): Int {
									return a * b
								}

								val result = multiply(10, 20)
							
						

Anonymous Function & Lambda Expression

Usage

In summary, lambda expressions are more concise and are typically used for simpler functions, while anonymous functions provide more flexibility with explicit return types and return behavior.

There are few use cases for lambda expressions and anonymous functions:

  • Passing functions as arguments to higher-order functions
  • Returning functions from other functions
  • Defining local functions that are not needed outside the scope of the enclosing function

Anonymous Function & Lambda Expression

Usage

In this example, the operation function takes two integers and a lambda function as arguments.

						
							fun operation(x: Int, y: Int, func: (Int, Int) -> Int): Int {
								return func(x, y)
							}
						
					

The most common use way pass the function argument is:

						
							val result = operation(10, 20) { x, y -> x + y }
						
					

Another possibility is to pass a function reference (it can be a named function or a member function):

						
							val multiply: (Int, Int) -> Int = { a, b -> a * b }

							val result = operation(10, 20, multiply)
						
					

Anonymous Function & Lambda Expression

Usage

You can also return functions from other functions.

						
							fun getCalculator(): (Int, Long, Double) -> Double {
								return { a, b, c -> a + b + c }
							}
						
					
						
							val calculator = getCalculator()

							val result = calculator(1, 2, 3.0)
						
					

Extension functions

Extension functions allow adding new methods to existing classes without modifying their source code.

Extension functions are one of the most powerful and popular features of Kotlin. You define an extension function by prefixing the function name with the type you want to extend.


Benefits of extension functions include:
  • Adding new functionality to existing classes which you may not have access to.
  • Using extension functions to create a more fluent API and DSLs.
  • Improving readability and maintainability of code by encapsulating and naming logic in extension functions.
  • Using extension functions to transform objects into other objects.

Extension functions: Examples

Adding new functionality to existing classes which you may not have access to.

For example, you can add a new method to the String class to capitalize the first letter of each word in a sentence.

						
							fun String.capitalizeWords(): String {
								return this.split(" ").joinToString(" ") { it.capitalize() }
							}
						
					

You can then use this extension function on any String object.

						
							fun main() {
								val sentence = "hello world from kotlin"
								println(sentence.capitalizeWords()) // Output: "Hello World From Kotlin"
							}
						
					

Extension functions: Examples

Using extension functions to create a more fluent API and DSLs.

You can use extension functions to create a more fluent API by adding methods to existing classes that allow you to chain method calls together.

						
							data class Person(
								val name: String,
								val age: Int,
								val country: String
							)
						
					
						
							fun List<Person>.filterByCountry(country: String): List<Person> {
								return this.filter { it.country.lowercase() == country.lowercase()) }
							}

							fun List<Person>.sortByName(): List<Person> {
								return this.sortedBy { person -> person.name }
							}
						
					

You can then use these extension functions to create a more readable and expressive code.

						
							val people = listOf(
								// list of people
							)

							val filteredPeople = people
								.filterByCountry("Thailand")
								.sortByName()
						
					

Extension functions: Examples

Improving readability and maintainability of code by encapsulating and naming logic in extension functions.
						
							data class Person(
								val name: String,
								val age: Int,
								val country: String
							)
						
					
						
							fun Person.canDrinkBeer(): Boolean {
								val legalAge = when (country) {
									"USA" -> 21
									else -> 18
								}
								return age >= legalAge
							}
						
					
						
							fun main() {
								val person = Person("John", 21, "USA")
								println("${person.name} can drink beer: ${person.canDrinkBeer()}")
							}
						
					

Extension functions: Examples

Using extension functions to transform objects into other objects.
						
							data class Person(
								val name: String,
								val age: Int,
								val country: String
							)
						
					
						
							data class Student(
								val name: String,
								val country: String,
								val dateEnrolled: LocalDate
							)
						
					
						
							fun Person.toStudent() = Student(
								name = name,
								country = country,
								dateEnrolled = LocalDate.now()
							)
						
					
						
							val person = Person("John", 21, "USA")
    						val student = person.toStudent()
						
					

Scope Functions

Scope functions allow you to execute a block of code within the context of an object.

When you use a scope function, you can access the object's properties and functions without having to use the object's name.

The scope functions in Kotlin are let, run, with, apply, and also.

Each scope function has a different context object and return value, which makes them useful for different use cases.


Function Context object Return value Usage
let it Result of the lambda expression Execute a block of code on the result of a call chain or to work with nullable objects
run this Result of the lambda expression Often used when you want to perform multiple operations on an object and return a result
with this Result of the lambda expression Execute a block of code on an object
apply this The context object itself Typically used for initializing or configuring an object.
also it The context object itself Perform additional operations on an object without changing the object itself

let

Execute a block of code on the result of a call chain or to work with nullable objects.

The context object of the let function is referred to as it and the return value is the result of the lambda expression.

The let function is particularly useful when used with a nullable types, because it allows us to chain multiple operations on a nullable object.


Example: using let to work with nullable objects
						
							data class Message(
								val text: String?,
								var acknowledged: Boolean = false
							) {
								fun send(): Message {
									TODO("Response message")
								}
							}
						
					
						
							val message = Message(text = "Hello")

							val response = message.send()

							val text = response.text?.let {
								println("Received message: $it")
							}

							// process text
						
					

run

Often used when you want to perform multiple operations on an object and return a result.

The context object of the run function is referred to as this and the return value is the result of the lambda expression.

Example 1: Using run to initialize an object
						
							data class Message(
								val text: String?,
								var acknowledged: Boolean = false
							) {
								fun send(): Message {
									TODO("Response message")
								}
							}
						
					
						
							val message = Message(text = "Hello").run {
								if (send().acknowledged) {
									println("Message acknowledged")
								} else {
									println("Message not acknowledged")
								}
								this
							}
						
					

with

User when you want to execute a block of code on an object.
Example 1: Using run to initialize an object
						
							data class Message(
								val text: String?,
								var acknowledged: Boolean = false
							) {
								fun send(): Message {
									TODO("Response message")
								}
							}
						
					
						
							val message =  Message(text = "Hello")

							with(message) {
								if (send().acknowledged) {
									println("Message acknowledged")
								} else {
									println("Message not acknowledged")
								}
							}
						
					

apply

Typically used for initializing or configuring an object.
Example 1: Using apply to initialize an object
						
							data class Message(
								val text: String?,
								var acknowledged: Boolean = false
							) {
								fun send(): Message {
									TODO("Response message")
								}
							}
						
					
						
							   val message =  Message(text = "Hello").apply {
									acknowledged = true
								}
						
					

also

used when you want to perform additional operations on an object without changing the object itself.
Example 1: Using also to print the object
						
							data class Message(
								val text: String?,
								var acknowledged: Boolean = false
							) {
								fun send(): Message {
									TODO("Response message")
								}
							}
						
					
						
							val message = Message(text = "Hello")
								.also { println(it)  }

							val response = message.send()
								.also { println(it)  }
						
					

Next Lesson

Next Lesson

The topic of the next lesson will be Object-Oriented Programming in Kotlin.

We will also cover Classes and Objects.