Skip to main content

Kotlin

Welcome to Kotlin

Kotlin is a modern, concise, and safe programming language that runs on the Java Virtual Machine (JVM), Android, JavaScript, and native platforms. Developed by JetBrains in 2011, Kotlin combines object-oriented and functional programming features with a focus on interoperability, safety, and tooling support.

Known for its null safety, extension functions, and coroutines, Kotlin reduces common programming errors while maintaining full compatibility with Java. This makes Kotlin an excellent choice for Android development, server-side applications, and multiplatform projects where productivity and code safety are priorities.

Introduction to Kotlin

How to set up a Kotlin development environment and write your first "Hello, World!" program.

Kotlin is a modern programming language that has gained rapid adoption, especially in Android development. Setting up a Kotlin development environment is straightforward with several options available.

Step 1: Install Kotlin

  1. Using IntelliJ IDEA
    Install IntelliJ IDEA Community or Ultimate edition, which comes with built-in Kotlin support. Download from jetbrains.com/idea.

  2. Using Command Line
    Install the Kotlin compiler manually:

    # On macOS with Homebrew
    brew install kotlin
    
    # On SDKMAN
    sdk install kotlin
    
    # Or download from GitHub releases
    # https://github.com/JetBrains/kotlin/releases
  3. Using Android Studio
    For Android development, install Android Studio which includes Kotlin support.

Step 2: Verify Installation

Open a terminal and type:

kotlinc -version

This should display the installed Kotlin compiler version.

Step 3: Write and Run Your First Kotlin Program

  1. Create a file named hello.kt with the following content:

    fun main() {
        println("Hello, World!")
    }
  2. Compile and run the program:

    kotlinc hello.kt -include-runtime -d hello.jar
    java -jar hello.jar
  3. Or use the Kotlin script runner:

    kotlin hello.kt

    You should see the output: Hello, World!

Step 4: Using REPL (Read-Eval-Print Loop)

Kotlin provides an interactive shell for quick testing:

kotlinc-jvm

Then type Kotlin code directly:

>>> println("Hello from REPL!")
Hello from REPL!
>>> val x = 5 + 3
>>> println(x)
8

Congratulations! You have successfully set up a Kotlin environment and run your first program. 🎉

Kotlin Syntax Basics

Kotlin syntax is designed to be concise, expressive, and safe. Understanding the basic syntax is crucial for writing correct Kotlin programs.

1. Basic Structure of a Kotlin Program

Kotlin programs have a flexible structure:

// Single expression function (no need for explicit return)
fun main() = println("Hello, Kotlin!")

// Traditional function with block body
fun main() {
    // Program statements go here
    println("Hello, Kotlin!")
}

2. Semicolons and Code Blocks

Kotlin doesn't require semicolons at the end of statements (they are optional). Braces {} define code blocks:

val x = 5        // No semicolon needed

if (x > 3) {      // Braces define the if block
    println("x is greater than 3")
}                 // Closing brace

3. Comments

Kotlin supports single-line and multi-line comments:

// This is a single-line comment

/*
 This is a multi-line comment
 It can span multiple lines
*/

/**
 * This is a KDoc comment
 * Used for documentation
 */

4. Case Sensitivity

Kotlin is case-sensitive, meaning it distinguishes between uppercase and lowercase letters:

val myVar = 5    // Different from myvar
val MyVar = 10   // Different from myVar

println(myVar)   // Outputs: 5
println(MyVar)   // Outputs: 10

5. Type Inference and Explicit Typing

Kotlin has strong type inference but also supports explicit type declarations:

val x = 10           // Type inferred as Int
val y = 3.14         // Type inferred as Double
val z = 'A'          // Type inferred as Char
val name = "Kotlin"  // Type inferred as String

// Explicit type declarations
val a: Int = 10
val b: Double = 3.14
val c: String = "Explicit"

println(x::class.simpleName)  // Outputs: Int

Conclusion

Understanding Kotlin syntax is essential for writing concise and safe programs. Key takeaways include:

  • Kotlin programs typically start from the main() function
  • Semicolons are optional and generally omitted
  • Code blocks are defined with braces {}
  • Kotlin is case-sensitive
  • Kotlin has excellent type inference but supports explicit typing

Output with println

The println function in Kotlin is used to display output on the console. It is part of the Kotlin standard library and is one of the most commonly used features for basic output and debugging.

1. Basic println Usage

The simplest way to use println is with a string or any value:

fun main() {
    println("Hello, World!")  // Outputs: Hello, World!
    println(42)               // Outputs: 42
    println(3.14)             // Outputs: 3.14
    println(true)             // Outputs: true
}

2. String Templates

Kotlin supports string templates for easy variable interpolation:

val name = "Alice"
val age = 25

println("Name: $name, Age: $age")  // Outputs: Name: Alice, Age: 25

// Expressions in templates
println("Next year: ${age + 1}")   // Outputs: Next year: 26

// Property access
data class Person(val name: String, val age: Int)
val person = Person("Bob", 30)
println("Person: ${person.name} is ${person.age} years old")

3. print vs println

Kotlin provides both print and println functions:

print("Hello")     // No newline
print(" ")         // No newline  
println("World!")  // Adds newline

// Output: Hello World!

4. Formatting Output

Kotlin provides various ways to format output:

val pi = 3.14159

// String formatting
println("Pi: %.2f".format(pi))  // Outputs: Pi: 3.14

// Padding and alignment
println("%10s".format("Hello"))  // Outputs: "     Hello"

// Multiple values
println("%s %d %.2f".format("Value:", 42, 3.14))

5. Multi-line Strings

Kotlin supports raw strings with triple quotes:

val text = """
    This is a multi-line
    string in Kotlin.
    It preserves all formatting
    including indentation.
""".trimIndent()

println(text)

Conclusion

The println function is a fundamental tool for output in Kotlin. Key points:

  • Use println for output with newline
  • Use print for output without newline
  • String templates ($variable) make output concise
  • Multi-line strings with triple quotes preserve formatting
  • Use string formatting for precise control over output

Arithmetic Operators in Kotlin

Kotlin provides standard arithmetic operators for mathematical calculations. These operators work with numeric data types like integers and floating-point numbers.

Basic Arithmetic Operators

fun main() {
    val a = 15
    val b = 4
    
    println("a + b = ${a + b}")  // Addition: 19
    println("a - b = ${a - b}")  // Subtraction: 11
    println("a * b = ${a * b}")  // Multiplication: 60
    println("a / b = ${a / b}")  // Division: 3 (integer division)
    println("a % b = ${a % b}")  // Modulus: 3
    
    val x = 15.0
    val y = 4.0
    println("x / y = ${x / y}")  // Division: 3.75 (double division)
}

Increment and Decrement Operators

var count = 5
println("count = $count")     // 5
println("++count = ${++count}") // 6 (pre-increment)
println("count++ = ${count++}") // 6 (post-increment)
println("count = $count")     // 7
println("--count = ${--count}") // 6 (pre-decrement)

Operator Overloading

data class Vector(val x: Int, val y: Int) {
    // Overload plus operator
    operator fun plus(other: Vector): Vector {
        return Vector(x + other.x, y + other.y)
    }
    
    // Overload minus operator
    operator fun minus(other: Vector): Vector {
        return Vector(x - other.x, y - other.y)
    }
}

fun main() {
    val v1 = Vector(2, 3)
    val v2 = Vector(1, 4)
    
    val sum = v1 + v2
    val diff = v1 - v2
    
    println("Sum: $sum")    // Sum: Vector(x=3, y=7)
    println("Diff: $diff")  // Diff: Vector(x=1, y=-1)
}

Common Pitfalls

  • Integer division truncates the fractional part: 5 / 2 equals 2, not 2.5
  • Division by zero throws ArithmeticException for integers
  • Division by zero for floating-point numbers results in Infinity

Comparison Operators in Kotlin

Comparison operators compare two values and return a boolean result (true or false). These are essential for conditional statements and loops.

Comparison Operators

fun main() {
    val x = 7
    val y = 10
    
    println("x == y: ${x == y}")  // Equal to: false
    println("x != y: ${x != y}")  // Not equal to: true
    println("x > y: ${x > y}")    // Greater than: false
    println("x < y: ${x < y}")    // Less than: true
    println("x >= 7: ${x >= 7}")  // Greater than or equal: true
    println("y <= 7: ${y <= 7}")  // Less than or equal: false
    
    // String comparison
    val str1 = "hello"
    val str2 = "HELLO"
    println("str1 == str2: ${str1 == str2}")        // false
    println("str1.equals(str2): ${str1.equals(str2)}") // false
    println("str1.equals(str2, true): ${str1.equals(str2, true)}") // true (ignore case)
}

Using Comparisons in Conditions

val age = 18
if (age >= 18) {
    println("You are an adult")
} else {
    println("You are a minor")
}

// Chained comparisons
val number = 15
if (number in 10..20) {
    println("Number is between 10 and 20")
}

// When expression with comparisons
val score = 85
when {
    score >= 90 -> println("A")
    score >= 80 -> println("B")
    score >= 70 -> println("C")
    else -> println("F")
}

Common Pitfalls

  • Use == for structural equality (compares values)
  • Use === for referential equality (compares references)
  • Comparing floating-point numbers for exact equality can be problematic due to precision issues

Logical Operators in Kotlin

Logical operators combine boolean expressions and return true or false. Kotlin provides three logical operators: && (AND), || (OR), and ! (NOT).

Logical Operators

fun main() {
    val isSunny = true
    val isWarm = false
    
    println("isSunny && isWarm: ${isSunny && isWarm}")  // AND: false
    println("isSunny || isWarm: ${isSunny || isWarm}")  // OR: true
    println("!isWarm: ${!isWarm}")                      // NOT: true
    
    // Complex conditions
    val age = 25
    val hasLicense = true
    
    if (age >= 18 && hasLicense) {
        println("You can drive")
    }
    
    // Combining multiple conditions
    val isWeekend = true
    val hasMoney = true
    if (isWeekend && (isSunny || hasMoney)) {
        println("Let's go out!")
    }
}

Short-Circuit Evaluation

// In AND (&&), if first condition is false, second isn't evaluated
// In OR (||), if first condition is true, second isn't evaluated

fun isValidNumber(num: Int): Boolean {
    println("Checking number: $num")
    return num > 0
}

val x = 5
if (x != 0 && isValidNumber(10 / x)) {
    println("Condition satisfied")
}

// Lazy evaluation with sequences
val numbers = listOf(1, 2, 3, 4, 5)
val result = numbers.asSequence()
    .filter { 
        println("Filtering: $it")
        it > 2 
    }
    .map { 
        println("Mapping: $it")
        it * 2 
    }
    .first()

Common Pitfalls

  • Using bitwise operators (&, |) instead of logical operators (&&, ||)
  • Forgetting that logical operators have lower precedence than comparison operators
  • Not leveraging short-circuit evaluation for performance

Bitwise Operations in Kotlin

Kotlin provides functions for bitwise operations on integer types. These are used in low-level programming, embedded systems, and performance optimization.

Bitwise Functions

fun main() {
    val a = 6   // binary: 0110
    val b = 3   // binary: 0011
    
    println("a = ${a.toString(2)} ($a)")
    println("b = ${b.toString(2)} ($b)")
    
    // Bitwise operations using functions
    println("a and b = ${(a and b).toString(2)} (${a and b})")  // AND: 0010 (2)
    println("a or b = ${(a or b).toString(2)} (${a or b})")     // OR: 0111 (7)
    println("a xor b = ${(a xor b).toString(2)} (${a xor b})")  // XOR: 0101 (5)
    println("a.inv() = ${a.inv().toString(2)} (${a.inv()})")    // NOT: 11111111111111111111111111111001 (-7)
    println("a shl 1 = ${(a shl 1).toString(2)} (${a shl 1})")  // Left shift: 1100 (12)
    println("b shr 1 = ${(b shr 1).toString(2)} (${b shr 1})")  // Right shift: 0001 (1)
    println("a ushr 1 = ${(a ushr 1).toString(2)} (${a ushr 1})") // Unsigned right shift
}

Practical Applications

// Setting flags
const val FLAG_A = 1  // 0001
const val FLAG_B = 2  // 0010
const val FLAG_C = 4  // 0100

var settings = 0
settings = settings or FLAG_A  // Set flag A
settings = settings or FLAG_C  // Set flag C

// Check if flag B is set
if (settings and FLAG_B != 0) {
    println("Flag B is set")
} else {
    println("Flag B is not set")
}

// Color manipulation (ARGB)
fun getAlpha(color: Int): Int = (color ushr 24) and 0xFF
fun getRed(color: Int): Int = (color ushr 16) and 0xFF
fun getGreen(color: Int): Int = (color ushr 8) and 0xFF
fun getBlue(color: Int): Int = color and 0xFF

val color = 0xFF336699.toInt()
println("Alpha: ${getAlpha(color)}")
println("Red: ${getRed(color)}")
println("Green: ${getGreen(color)}")
println("Blue: ${getBlue(color)}")

Common Pitfalls

  • Confusing bitwise and with logical &&
  • Forgetting that integers are signed in Kotlin
  • Shifting beyond the bit width can cause unexpected results
  • Using shr vs ushr for signed vs unsigned right shifts

Assignment Operators in Kotlin

Assignment operators assign values to variables. Kotlin provides compound assignment operators that combine assignment with arithmetic or bitwise operations.

Assignment Operators

fun main() {
    // Simple assignment
    var x = 10
    println("x = $x")
    
    x += 5            // Equivalent to x = x + 5
    println("x += 5: $x")  // 15
    
    x -= 3            // Equivalent to x = x - 3
    println("x -= 3: $x")  // 12
    
    x *= 2            // Equivalent to x = x * 2
    println("x *= 2: $x")  // 24
    
    x /= 4            // Equivalent to x = x / 4
    println("x /= 4: $x")  // 6
    
    x %= 4            // Equivalent to x = x % 4
    println("x %= 4: $x")  // 2
    
    // String assignment
    var text = "Hello"
    text += " Kotlin"
    println(text)  // "Hello Kotlin"
    
    // Bitwise assignment operations
    var y = 5
    y = y and 3     // AND assignment
    y = y or 8      // OR assignment
    y = y xor 4     // XOR assignment
}

Multiple Assignment and Destructuring

// Multiple assignment
var a = 1
var b = 2
var c = 3

// Swap values
a = b.also { b = a }
println("a=$a, b=$b")  // a=2, b=1

// Destructuring declarations
val (name, age) = Pair("Alice", 25)
println("Name: $name, Age: $age")

val (first, second, third) = listOf(1, 2, 3)
println("$first, $second, $third")

// Data class destructuring
data class Person(val name: String, val age: Int)
val person = Person("Bob", 30)
val (personName, personAge) = person
println("Person: $personName, $personAge")

// Compound assignment in loops
var sum = 0
for (i in 1..5) {
    sum += i  // Add i to sum
}
println("Sum: $sum")  // 15

Common Pitfalls

  • Using = (assignment) when you meant == (equality comparison)
  • Trying to use compound assignment with val (immutable variables)
  • Forgetting that assignment returns Unit, not the assigned value

Integer Data Types in Kotlin

Kotlin provides several integer types with different sizes and ranges. All integer types are signed and support the same set of operations.

Basic Integer Types

fun main() {
    // Different integer types
    val byteValue: Byte = 127           // 8-bit, -128 to 127
    val shortValue: Short = 32767       // 16-bit, -32768 to 32767
    val intValue: Int = 2147483647      // 32-bit, -2^31 to 2^31-1
    val longValue: Long = 9223372036854775807L  // 64-bit, -2^63 to 2^63-1
    
    // Type inference (usually infers Int)
    val inferredInt = 100              // Inferred as Int
    val inferredLong = 100L            // Inferred as Long
    
    println("Byte: $byteValue")
    println("Short: $shortValue") 
    println("Int: $intValue")
    println("Long: $longValue")
    
    // Range properties
    println("Byte range: ${Byte.MIN_VALUE} to ${Byte.MAX_VALUE}")
    println("Int range: ${Int.MIN_VALUE} to ${Int.MAX_VALUE}")
    println("Long range: ${Long.MIN_VALUE} to ${Long.MAX_VALUE}")
}

Integer Operations

val a = 10
val b = 3

println("a + b = ${a + b}")  // 13
println("a - b = ${a - b}")  // 7
println("a * b = ${a * b}")  // 30
println("a / b = ${a / b}")  // 3 (integer division)
println("a % b = ${a % b}")  // 1 (modulus)

// Overflow behavior
val maxInt = Int.MAX_VALUE
println("Max Int: $maxInt")
println("Max Int + 1: ${maxInt + 1}")  // Overflows to Min Int

// Safe operations to avoid overflow
println("Max Int + 1 (safe): ${maxInt.toLong() + 1}")

Type Conversion

// Explicit conversion between types
val intValue = 100
val longValue: Long = intValue.toLong()
val doubleValue: Double = intValue.toDouble()

// String to integer
val str = "123"
val number = str.toInt()
val safeNumber = str.toIntOrNull()  // Returns null if conversion fails

// Hexadecimal and binary
val hexValue = 0xFF        // 255 in hexadecimal
val binaryValue = 0b1010   // 10 in binary

println("Hex: $hexValue")
println("Binary: $binaryValue")

// Formatting numbers
val largeNumber = 1_000_000  // Underscores for readability
println("Formatted: $largeNumber")

Common Pitfalls

  • Integer overflow when result exceeds type's maximum value
  • Integer division truncates fractional parts
  • Forgetting to use L suffix for long literals
  • Using toInt() on strings without validation

Floating-Point Data Types in Kotlin

Floating-point types represent real numbers with fractional parts. Kotlin provides two floating-point types with different precision levels.

Floating-Point Types

fun main() {
    val floatValue: Float = 3.14159f           // Single precision (32-bit)
    val doubleValue: Double = 3.14159265358979 // Double precision (64-bit)
    
    println("Float: $floatValue")
    println("Double: $doubleValue")
    
    // Scientific notation
    val scientific = 1.23e4  // 12300.0
    println("Scientific: $scientific")
    
    // Special values
    val positiveInfinity = Double.POSITIVE_INFINITY
    val negativeInfinity = Double.NEGATIVE_INFINITY
    val nan = Double.NaN
    
    println("Positive Infinity: $positiveInfinity")
    println("Negative Infinity: $negativeInfinity") 
    println("NaN: $nan")
    
    // Checking special values
    println("Is NaN: ${nan.isNaN()}")
    println("Is Finite: ${doubleValue.isFinite()}")
    println("Is Infinite: ${positiveInfinity.isInfinite()}")
}

Floating-Point Operations

val a = 2.5
val b = 1.5

println("a + b = ${a + b}")  // 4.0
println("a - b = ${a - b}")  // 1.0
println("a * b = ${a * b}")  // 3.75
println("a / b = ${a / b}")  // 1.6666666666666667

// Mathematical functions
import kotlin.math.*

println("sqrt(a) = ${sqrt(a)}")
println("pow(a, b) = ${a.pow(b)}")
println("sin(pi/2) = ${sin(PI / 2)}")
println("round(2.7) = ${round(2.7)}")

// Division by zero
println("1.0 / 0.0 = ${1.0 / 0.0}")  // Infinity
println("-1.0 / 0.0 = ${-1.0 / 0.0}")  // -Infinity
println("0.0 / 0.0 = ${0.0 / 0.0}")  // NaN

Type Conversion and Precision

// String to floating-point
val str = "3.14159"
val pi = str.toDouble()
val safePi = str.toDoubleOrNull()

// Floating-point to integer (truncation)
val d = 3.99
val i = d.toInt()  // i becomes 3

// Integer to floating-point
val x = 5
val y = x.toDouble()  // y becomes 5.0

// Precision issues
val result = 0.1 + 0.2
println("0.1 + 0.2 = $result")  // 0.30000000000000004

// Comparing floating-point numbers
val epsilon = 1e-10
val areEqual = abs(result - 0.3) < epsilon
println("Are equal within epsilon: $areEqual")

Common Pitfalls

  • Floating-point numbers have limited precision and rounding errors
  • Comparing floating-point numbers for exact equality is often problematic
  • Forgetting f suffix for float literals
  • Operations with very large or very small numbers can cause overflow/underflow

Strings in Kotlin

Kotlin provides the String class for working with text. Strings are immutable sequences of characters and offer many convenient operations for text manipulation.

Creating and Using Strings

fun main() {
    // Different ways to create strings
    val s1 = "Hello"
    val s2 = String(charArrayOf('W', 'o', 'r', 'l', 'd'))
    val s3 = "A".repeat(5)  // "AAAAA"
    val s4 = "$s1 $s2"      // Concatenation with template
    
    println("s1: $s1")
    println("s2: $s2")
    println("s3: $s3")
    println("s4: $s4")
    
    // Accessing characters
    println("First character: ${s1[0]}")  // 'H'
    println("Second character: ${s1.get(1)}")  // 'e'
    
    // String properties
    println("Length of s1: ${s1.length}")
    println("Is s1 empty? ${s1.isEmpty()}")
    println("Is s1 blank? ${s1.isBlank()}")
    
    // Multi-line strings
    val multiline = """
        This is a
        multi-line
        string
    """.trimIndent()
    println(multiline)
}

String Comparison and Searching

val str1 = "apple"
val str2 = "banana"
    
// Comparison
if (str1 == str2) {
    println("Strings are equal")
} else if (str1 < str2) {
    println("$str1 comes before $str2")
} else {
    println("$str1 comes after $str2")
}

// Case-insensitive comparison
println("Ignore case: ${str1.equals("APPLE", ignoreCase = true)}")

// Searching
val text = "Hello World"
val containsWorld = text.contains("World")
val startsWithHello = text.startsWith("Hello")
val endsWithWorld = text.endsWith("World")

println("Contains 'World': $containsWorld")
println("Starts with 'Hello': $startsWithHello")
println("Ends with 'World': $endsWithWorld")

// Finding positions
val firstO = text.indexOf('o')
val lastO = text.lastIndexOf('o')
println("First 'o': $firstO, Last 'o': $lastO")

String Modification

var str = "Hello"
    
// String are immutable, so we create new strings
val newStr = str + " World"  // Concatenation
val upperCase = str.uppercase()
val lowerCase = str.lowercase()
val trimmed = "  Hello  ".trim()

// Substrings
val substring = str.substring(1, 4)  // "ell"
val dropFirst = str.drop(1)  // "ello"
val dropLast = str.dropLast(1)  // "Hell"

// Replacement
val replaced = str.replace('l', 'L')  // "HeLLo"
val regexReplaced = "123abc".replace(Regex("\\d"), "X")  // "XXXabc"

// Splitting
val words = "apple,banana,cherry".split(",")
println("Split: $words")  // [apple, banana, cherry]

// Padding
val padded = str.padEnd(10, '!')  // "Hello!!!!!"

Common Pitfalls

  • Accessing characters beyond string length throws StringIndexOutOfBoundsException
  • Strings are immutable - modification operations return new strings
  • Using == for structural equality vs === for referential equality
  • Forgetting that string operations are case-sensitive by default

String Functions in Kotlin

Kotlin's String class provides many useful extension functions for string manipulation, including filtering, transformation, and utility functions.

String Transformation Functions

fun main() {
    val text = "Hello World"
    
    // Case conversion
    println("Uppercase: ${text.uppercase()}")
    println("Lowercase: ${text.lowercase()}")
    println("Capitalize: ${text.replaceFirstChar { it.uppercase() }}")
    
    // Filtering and transformation
    println("Only letters: ${text.filter { it.isLetter() }}")
    println("Reversed: ${text.reversed()}")
    println("Each char: ${text.map { "$it!" }.joinToString("") }")
    
    // Take and drop
    println("First 5: ${text.take(5)}")        // "Hello"
    println("Last 5: ${text.takeLast(5)}")     // "World"
    println("Drop first 6: ${text.drop(6)}")   // "World"
    
    // Windowed for sliding windows
    println("3-char windows: ${text.windowed(3)}")
}

String Validation and Checking

val email = "user@example.com"
val emptyString = ""
val blankString = "   "
    
// Validation functions
println("Is email empty? ${email.isEmpty()}")
println("Is empty string empty? ${emptyString.isEmpty()}")
println("Is blank string blank? ${blankString.isBlank()}")
println("Is email not blank? ${email.isNotBlank()}")
    
// Character checks
println("All letters? ${"abc".all { it.isLetter() }}")  // true
println("All digits? ${"123".all { it.isDigit() }}")    // true
println("Any uppercase? ${text.any { it.isUpperCase() }}") // true
    
// Prefix and suffix checks
val filename = "document.txt"
println("Starts with 'doc': ${filename.startsWith("doc")}")
println("Ends with '.txt': ${filename.endsWith(".txt")}")
    
// Pattern matching with regular expressions
val regex = Regex("\\d+")
println("Contains digits: ${regex.containsMatchIn("abc123")}")

Advanced String Operations

// Partitioning
val (letters, others) = "Hello123!".partition { it.isLetter() }
println("Letters: $letters, Others: $others")
    
// Grouping
val grouped = "Mississippi".groupBy { it }
println("Grouped: $grouped")
    
// Zipping with another string
val letters = "ABCD"
val numbers = "1234"
val zipped = letters.zip(numbers) { a, b -> "$a$b" }
println("Zipped: $zipped")
    
// Common prefix/suffix
val commonPrefix = "Hello World".commonPrefixWith("Hello Kotlin")
val commonSuffix = "Hello World".commonSuffixWith("Kotlin World")
println("Common prefix: $commonPrefix")
println("Common suffix: $commonSuffix")
    
// String building with StringBuilder
val builder = StringBuilder()
builder.append("Hello")
builder.append(" ")
builder.append("World")
val result = builder.toString()
println("Built: $result")
    
// More concise builder usage
val built = buildString {
    append("Hello")
    append(" ")
    append("Kotlin")
}
println("Built string: $built")

Common Pitfalls

  • Using isEmpty when you meant isBlank
  • Forgetting that string functions return new strings (immutability)
  • Not handling StringIndexOutOfBoundsException when using indices
  • Using expensive operations in loops (like repeated concatenation)

String Formatting in Kotlin

Kotlin provides several ways to format strings, from simple string templates to advanced formatting with the format function and string builders.

String Templates

fun main() {
    val name = "Alice"
    val age = 25
    val score = 95.5
    
    // Basic string templates
    println("Name: $name, Age: $age, Score: $score")
    
    // Expression in templates
    println("Next year: ${age + 1}")
    println("Score status: ${if (score >= 90) "Excellent" else "Good"}")
    
    // Property access in templates
    data class Person(val firstName: String, val lastName: String)
    val person = Person("John", "Doe")
    println("Full name: ${person.firstName} ${person.lastName}")
    
    // Raw strings with templates
    val message = """
        User Information:
        Name: $name
        Age: $age
        Score: $score
    """.trimIndent()
    println(message)
}

Format Function

val pi = 3.14159265358979
    
// Number formatting
println("Pi: %.2f".format(pi))        // 3.14
println("Pi: %.4f".format(pi))        // 3.1416
println("Number: %10d".format(42))    // "        42"
println("Number: %-10d".format(42))   // "42        "
    
// Multiple values formatting
println("%s %d %.2f".format("Value:", 42, 3.14))
    
// Padding and alignment
println("'%10s'".format("Hello"))     // Right-aligned
println("'%-10s'".format("Hello"))    // Left-aligned
    
// Zero padding for numbers
println("%05d".format(42))            // 00042
    
// Locale-specific formatting
import java.util.Locale
println(Locale.US, "%,d".format(1000000))  // 1,000,000

Advanced Formatting Techniques

// Building complex strings
val table = buildString {
    appendLine("+--------+--------+")
    appendLine("| Name   | Age    |")
    appendLine("+--------+--------+")
    appendLine("| Alice  | %6d |".format(25))
    appendLine("| Bob    | %6d |".format(30))
    appendLine("+--------+--------+")
}
println(table)
    
// Formatting dates and times
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
    
val now = LocalDateTime.now()
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
println("Current time: ${now.format(formatter)}")
    
// Currency formatting
import java.text.NumberFormat
val amount = 1234.56
val currencyFormat = NumberFormat.getCurrencyInstance()
println("Amount: ${currencyFormat.format(amount)}")
    
// Custom format function
fun String.format(vararg args: Any): String {
    return this.replace(Regex("\\{\\}")) { 
        if (args.isNotEmpty()) args[0].toString() else "{}" 
    }
}
    
// Using custom format (simple version)
println("Hello {}, you are {} years old".format("Alice", 25))

Template Engines and DSLs

// Simple template engine
fun String.template(variables: Map<String, Any>): String {
    return this.replace(Regex("\\$\\{([^}]+)\\}")) { match ->
        val key = match.groupValues[1]
        variables[key]?.toString() ?: match.value
    }
}
    
val template = "Hello \${name}, you have \${count} messages"
val data = mapOf("name" to "Alice", "count" to 5)
println(template.template(data))
    
// Using Kotlin's string DSL capabilities
val html = """
    <html>
        <head>
            <title>\${title}</title>
        </head>
        <body>
            <h1>\${heading}</h1>
        </body>
    </html>
""".trimIndent()
    
val htmlData = mapOf("title" to "My Page", "heading" to "Welcome")
println(html.template(htmlData))

Common Pitfalls

  • Using string concatenation in loops instead of builders
  • Not escaping $ character when you want it literally
  • Forgetting that format specifiers must match the variable types
  • Using expensive formatting operations in performance-critical code

Arrays in Kotlin

Arrays in Kotlin are fixed-size collections of elements of the same type. They provide efficient random access but have a fixed size once created.

Creating and Using Arrays

fun main() {
    // Different ways to create arrays
    val numbers = arrayOf(1, 2, 3, 4, 5)
    val nulls = arrayOfNulls<Int>(5)  // Array of 5 nulls
    val computed = Array(5) { it * 2 }  // [0, 2, 4, 6, 8]
    
    // Primitive type arrays (more efficient)
    val intArray = intArrayOf(1, 2, 3, 4, 5)
    val doubleArray = doubleArrayOf(1.1, 2.2, 3.3)
    val charArray = charArrayOf('a', 'b', 'c')
    
    // Accessing elements
    println("First element: ${numbers[0]}")
    println("Second element: ${numbers.get(1)}")
    
    // Modifying elements
    numbers[0] = 10
    numbers.set(1, 20)
    
    // Array size
    println("Array size: ${numbers.size}")
    println("Last index: ${numbers.lastIndex}")
    
    // Iterating through array
    for (i in numbers.indices) {
        println("numbers[$i] = ${numbers[i]}")
    }
    
    // For-each loop
    for (number in numbers) {
        print("$number ")
    }
    println()
}

Multi-dimensional Arrays

// 2D array (matrix)
val matrix = Array(3) { IntArray(3) }
    
// Initialize with values
val matrix2 = arrayOf(
    intArrayOf(1, 2, 3),
    intArrayOf(4, 5, 6),
    intArrayOf(7, 8, 9)
)
    
// Accessing elements
println("Element at [1][2]: ${matrix2[1][2]}")  // 6
    
// Nested loops for 2D array
for (i in matrix2.indices) {
    for (j in matrix2[i].indices) {
        print("${matrix2[i][j]} ")
    }
    println()
}
    
// 3D array
val cube = Array(2) { Array(2) { IntArray(2) } }
cube[0][0][0] = 1
cube[1][1][1] = 8

Array Operations and Utilities

val numbers = arrayOf(5, 2, 8, 1, 9)
    
// Sorting
numbers.sort()
println("Sorted: ${numbers.joinToString()}")
    
// Searching
val index = numbers.indexOf(8)
println("Index of 8: $index")
    
// Checking conditions
val allPositive = numbers.all { it > 0 }
val anyEven = numbers.any { it % 2 == 0 }
println("All positive: $allPositive")
println("Any even: $anyEven")
    
// Filtering and transformation
val evenNumbers = numbers.filter { it % 2 == 0 }
val squared = numbers.map { it * it }
println("Even: $evenNumbers")
println("Squared: $squared")
    
// Aggregation
val sum = numbers.sum()
val max = numbers.maxOrNull()
val min = numbers.minOrNull()
println("Sum: $sum, Max: $max, Min: $min")
    
// Array to list conversion
val list = numbers.toList()
val set = numbers.toSet()

Common Pitfalls

  • Array indices start at 0, not 1
  • Accessing out-of-bounds indices throws ArrayIndexOutOfBoundsException
  • Arrays have fixed size - cannot add or remove elements
  • Using arrayOf for primitives creates boxed arrays (use primitive array functions for efficiency)

Array Indexing in Kotlin

Array indexing allows access to individual elements in an array using their position. Kotlin uses zero-based indexing, meaning the first element is at index 0.

Basic Array Indexing

fun main() {
    val numbers = arrayOf(10, 20, 30, 40, 50)
    
    // Accessing elements by index
    println("numbers[0] = ${numbers[0]}")  // First element: 10
    println("numbers[2] = ${numbers[2]}")  // Third element: 30
    println("numbers[4] = ${numbers[4]}")  // Last element: 50
    
    // Using get() method
    println("numbers.get(1) = ${numbers.get(1)}")  // 20
    
    // Modifying elements
    numbers[1] = 25  // Change second element from 20 to 25
    numbers.set(3, 45)  // Change fourth element from 40 to 45
    
    // Display modified array
    for (i in numbers.indices) {
        println("numbers[$i] = ${numbers[i]}")
    }
    
    // Safe access with getOrNull
    val safeValue = numbers.getOrNull(10)  // Returns null if index out of bounds
    println("Safe access: $safeValue")
    
    // Default value for out-of-bounds access
    val withDefault = numbers.getOrElse(10) { -1 }  // Returns -1 if index out of bounds
    println("With default: $withDefault")
}

Multi-dimensional Array Indexing

// 2D array indexing
val matrix = arrayOf(
    intArrayOf(1, 2, 3),
    intArrayOf(4, 5, 6)
)

println("matrix[0][1] = ${matrix[0][1]}")  // 2
println("matrix[1][2] = ${matrix[1][2]}")  // 6

// 3D array indexing
val cube = arrayOf(
    arrayOf(intArrayOf(1, 2), intArrayOf(3, 4)),
    arrayOf(intArrayOf(5, 6), intArrayOf(7, 8))
)
println("cube[1][0][1] = ${cube[1][0][1]}")  // 6

// Ragged array (arrays of different sizes)
val ragged = arrayOf(
    intArrayOf(1),
    intArrayOf(2, 3),
    intArrayOf(4, 5, 6)
)
println("ragged[2][1] = ${ragged[2][1]}")  // 5

Advanced Indexing Techniques

val data = arrayOf("a", "b", "c", "d", "e", "f")
    
// Range indexing
val slice = data.sliceArray(1..3)  // ["b", "c", "d"]
println("Slice: ${slice.joinToString()}")
    
// Step indexing
val stepped = data.sliceArray(0 until data.size step 2)  // ["a", "c", "e"]
println("Stepped: ${stepped.joinToString()}")
    
// Negative indexing (from end)
val lastThree = data.sliceArray(data.size - 3 until data.size)  // ["d", "e", "f"]
println("Last three: ${lastThree.joinToString()}")
    
// Using indices property
println("Valid indices: ${data.indices}")  // 0..5
for (i in data.indices) {
    println("data[$i] = ${data[i]}")
}
    
// Using withIndex for both index and value
for ((index, value) in data.withIndex()) {
    println("Index $index: $value")
}
    
// Finding indices of elements
val indicesOfA = data.indices.filter { data[it] == "a" }
println("Indices of 'a': $indicesOfA")

Common Pitfalls

  • Accessing out-of-bounds indices throws ArrayIndexOutOfBoundsException
  • Array indices start at 0, not 1
  • Negative indices are not supported (unlike some other languages)
  • Forgetting that multi-dimensional arrays are arrays of arrays

Array Functions in Kotlin

Kotlin provides extensive functionality for working with arrays through extension functions, including sorting, searching, transformation, and aggregation operations.

Array Manipulation Functions

fun main() {
    val numbers = arrayOf(5, 2, 8, 1, 9, 3)
    
    // Sorting
    numbers.sort()
    println("Sorted: ${numbers.joinToString()}")
    
    numbers.sortDescending()
    println("Descending: ${numbers.joinToString()}")
    
    // Custom sorting
    val strings = arrayOf("apple", "banana", "cherry", "date")
    strings.sortBy { it.length }  // Sort by string length
    println("By length: ${strings.joinToString()}")
    
    // Searching
    val index = numbers.indexOf(8)
    val lastIndex = numbers.lastIndexOf(3)
    println("Index of 8: $index")
    println("Last index of 3: $lastIndex")
    
    // Binary search (requires sorted array)
    val sortedNumbers = numbers.sortedArray()
    val foundIndex = sortedNumbers.binarySearch(8)
    println("Binary search for 8: $foundIndex")
    
    // Finding elements
    val firstEven = numbers.find { it % 2 == 0 }
    val lastEven = numbers.findLast { it % 2 == 0 }
    println("First even: $firstEven, Last even: $lastEven")
}

Transformation and Filtering

val numbers = arrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    
// Mapping (transform each element)
val squared = numbers.map { it * it }
println("Squared: ${squared.joinToString()}")
    
// Filtering
val evens = numbers.filter { it % 2 == 0 }
val odds = numbers.filterNot { it % 2 == 0 }
println("Evens: ${evens.joinToString()}")
println("Odds: ${odds.joinToString()}")
    
// Partitioning
val (evenList, oddList) = numbers.partition { it % 2 == 0 }
println("Partition - Evens: $evenList, Odds: $oddList")
    
// Grouping
val grouped = numbers.groupBy { 
    if (it % 2 == 0) "even" else "odd" 
}
println("Grouped: $grouped")
    
// Flattening nested arrays
val nested = arrayOf(arrayOf(1, 2), arrayOf(3, 4), arrayOf(5, 6))
val flat = nested.flatten()
println("Flattened: $flat")

Aggregation and Reduction

val numbers = arrayOf(1, 2, 3, 4, 5)
    
// Basic aggregation
println("Sum: ${numbers.sum()}")
println("Average: ${numbers.average()}")
println("Min: ${numbers.minOrNull()}")
println("Max: ${numbers.maxOrNull()}")
    
// Count and existence checks
println("Count: ${numbers.count()}")
println("Count even: ${numbers.count { it % 2 == 0 }}")
println("Any even: ${numbers.any { it % 2 == 0 }}")
println("All positive: ${numbers.all { it > 0 }}")
println("None negative: ${numbers.none { it < 0 }}")
    
// Reduction (fold and reduce)
val sumReduce = numbers.reduce { acc, value -> acc + value }
val productReduce = numbers.reduce { acc, value -> acc * value }
println("Reduce sum: $sumReduce")
println("Reduce product: $productReduce")
    
// Fold with initial value
val sumFold = numbers.fold(10) { acc, value -> acc + value }  // 10 + sum
println("Fold sum with initial 10: $sumFold")
    
// Running fold (intermediate results)
val runningSum = numbers.runningFold(0) { acc, value -> acc + value }
println("Running sum: $runningSum")

Array Creation and Conversion

// Creating arrays from ranges
val rangeArray = (1..5).toList().toTypedArray()
println("From range: ${rangeArray.joinToString()}")
    
// Creating arrays with generators
val generated = Array(5) { index -> index * index }
println("Generated: ${generated.joinToString()}")
    
// Converting between array types
val intArray = arrayOf(1, 2, 3).toIntArray()
val objectArray = intArray.toTypedArray()
    
// String representation
val stringRep = arrayOf("a", "b", "c").joinToString(prefix = "[", postfix = "]")
println("String representation: $stringRep")
    
// Copying arrays
val original = arrayOf(1, 2, 3)
val copy = original.copyOf()
val sizedCopy = original.copyOf(5)  // New size 5, padded with null or zero
println("Original: ${original.joinToString()}")
println("Copy: ${copy.joinToString()}")
println("Sized copy: ${sizedCopy.joinToString()}")
    
// Filling arrays
val fillArray = IntArray(5) 
fillArray.fill(42)
println("Filled: ${fillArray.joinToString()}")

Common Pitfalls

  • Modifying arrays during iteration can cause ConcurrentModificationException
  • Forgetting that transformation functions return new collections, not modify in place
  • Using binarySearch on unsorted arrays gives undefined results
  • Confusing reduce (no initial) with fold (with initial)

Lists in Kotlin

Lists are ordered collections that can be either mutable or immutable. They are part of Kotlin's standard library and provide many convenient operations.

Creating and Using Lists

fun main() {
    // Immutable lists (read-only)
    val immutableList = listOf("apple", "banana", "cherry")
    val numbers = listOf(1, 2, 3, 4, 5)
    
    // Mutable lists
    val mutableList = mutableListOf("a", "b", "c")
    val arrayList = arrayListOf(1, 2, 3)  // ArrayList implementation
    
    // Empty lists
    val emptyList = emptyList<String>()
    val emptyMutable = mutableListOf<Int>()
    
    // List with single element
    val single = listOf("single")
    
    println("Immutable: $immutableList")
    println("Mutable: $mutableList")
    
    // Accessing elements
    println("First: ${immutableList[0]}")
    println("First with function: ${immutableList.first()}")
    println("Last: ${immutableList.last()}")
    println("Element at: ${immutableList.elementAt(1)}")
    
    // Safe access
    println("Get or null: ${immutableList.getOrNull(10)}")
    println("Get or default: ${immutableList.getOrElse(10) { "default" }}")
}

List Operations

val fruits = mutableListOf("apple", "banana", "cherry", "date")
    
// Adding elements
fruits.add("elderberry")
fruits.add(1, "blueberry")  // Insert at specific position
fruits.addAll(listOf("fig", "grape"))
    
// Removing elements
fruits.remove("banana")
fruits.removeAt(0)
fruits.removeAll(listOf("date", "fig"))
    
// Modifying elements
fruits[0] = "avocado"
    
// Sublists
val subList = fruits.subList(1, 3)  // From index 1 to 3 (exclusive)
    
// List information
println("Size: ${fruits.size}")
println("Is empty: ${fruits.isEmpty()}")
println("Contains 'apple': ${fruits.contains("apple")}")
println("Index of 'cherry': ${fruits.indexOf("cherry")}")
    
// Iterating
println("Using indices:")
for (i in fruits.indices) {
    println("fruits[$i] = ${fruits[i]}")
}
    
println("Using for-each:")
for (fruit in fruits) {
    println(fruit)
}
    
println("Using forEach:")
fruits.forEach { println(it) }
    
println("With index:")
fruits.forEachIndexed { index, fruit ->
    println("$index: $fruit")
}

List vs Array

// Arrays: Fixed size, efficient for primitives
val array = arrayOf(1, 2, 3)
// array.add(4)  // Error - fixed size
    
// Lists: Dynamic size, more functionality
val list = mutableListOf(1, 2, 3)
list.add(4)  // OK - can grow
list.remove(1)  // OK - can shrink
    
// Performance characteristics
/*
Collection  | Add/Remove | Access | Memory
------------|------------|--------|--------
Array       | O(n)       | O(1)   | Less
ArrayList   | O(1)*      | O(1)   | More
LinkedList  | O(1)       | O(n)   | More per element

* = amortized constant time for ArrayList
*/
    
// Converting between arrays and lists
val arrayFromList = list.toTypedArray()
val listFromArray = array.toList()
val mutableListFromArray = array.toMutableList()

Common Pitfalls

  • Modifying immutable lists (created with listOf) throws UnsupportedOperationException
  • Using get without checking bounds can throw IndexOutOfBoundsException
  • Forgetting that subList is a view of the original list
  • Using lists for primitive types can have boxing overhead

Null Safety in Kotlin

Kotlin's type system distinguishes between nullable and non-nullable types, helping to prevent null pointer exceptions at compile time.

Nullable vs Non-nullable Types

fun main() {
    // Non-nullable types (cannot hold null)
    val name: String = "Kotlin"
    // name = null  // Compilation error!
    
    // Nullable types (can hold null)
    var nullableName: String? = "Kotlin"
    nullableName = null  // This is allowed
    
    // Type inference with null
    val inferredNotNull = "Hello"  // Inferred as String
    val inferredNull = null        // Inferred as Nothing?
    val explicitNull: String? = null
    
    // Working with nullable types
    if (nullableName != null) {
        // Smart cast - nullableName is now String (not String?)
        println("Length: ${nullableName.length}")
    }
    
    // Function parameters
    fun printLength(text: String?) {
        if (text != null) {
            println("Length: ${text.length}")
        } else {
            println("Text is null")
        }
    }
    
    printLength("Hello")
    printLength(null)
}

Checking for Null

val nullableString: String? = getStringOrNull()
    
// Traditional null check
if (nullableString != null) {
    println(nullableString.length)  // Smart cast to String
}
    
// Using isNullOrEmpty and similar functions
if (!nullableString.isNullOrEmpty()) {
    println("String: $nullableString")
}
    
// When expression with null
when (nullableString) {
    null -> println("Got null")
    else -> println("String length: ${nullableString.length}")
}
    
// Checking multiple values
val a: String? = "Hello"
val b: String? = "World"
    
if (a != null && b != null) {
    println("$a $b")  // Both are smart-cast to non-null
}
    
// Safe casting
val anyValue: Any = "Hello"
val stringValue = anyValue as? String  // Safe cast, returns String?
val length = stringValue?.length

Working with Collections and Nullability

// List of nullable elements
val nullableList: List<Int?> = listOf(1, 2, null, 4, null, 6)
    
// Filter out nulls
val nonNullList = nullableList.filterNotNull()
println("Without nulls: $nonNullList")  // [1, 2, 4, 6]
    
// Map with nullable transformation
val strings = listOf("1", "2", "three", "4")
val numbers = strings.map { it.toIntOrNull() }
println("Nullable numbers: $numbers")  // [1, 2, null, 4]
    
// Filter map for non-null results
val validNumbers = strings.mapNotNull { it.toIntOrNull() }
println("Valid numbers: $validNumbers")  // [1, 2, 4]
    
// First/last with null safety
val firstEven = listOf(1, 3, 5, 7).firstOrNull { it % 2 == 0 }
val lastEven = listOf(1, 3, 5, 7).lastOrNull { it % 2 == 0 }
println("First even: $firstEven")  // null
println("Last even: $lastEven")    // null

Common Pitfalls

  • Using !! operator without being sure the value is non-null
  • Forgetting that platform types from Java can be null
  • Not handling null cases in when expressions
  • Assuming collections can't contain nulls without explicit declaration

Safe Calls in Kotlin

The safe call operator (?.) allows you to call methods or access properties on nullable objects without explicit null checks.

Basic Safe Call Usage

fun main() {
    val nullableString: String? = "Hello"
    val nullString: String? = null
    
    // Safe call on properties
    val length1 = nullableString?.length  // Int? = 5
    val length2 = nullString?.length      // Int? = null
    
    println("Length 1: $length1")
    println("Length 2: $length2")
    
    // Safe call on methods
    val uppercase1 = nullableString?.uppercase()  // String? = "HELLO"
    val uppercase2 = nullString?.uppercase()      // String? = null
    
    println("Uppercase 1: $uppercase1")
    println("Uppercase 2: $uppercase2")
    
    // Chained safe calls
    data class Person(val name: String?, val address: Address?)
    data class Address(val street: String?, val city: String?)
    
    val person: Person? = Person("Alice", Address("Main St", "New York"))
    val nullPerson: Person? = null
    
    val city1 = person?.address?.city  // String? = "New York"
    val city2 = nullPerson?.address?.city  // String? = null
    
    println("City 1: $city1")
    println("City 2: $city2")
}

Safe Calls with let and run

val nullableString: String? = "Hello World"
    
// Using let with safe call
nullableString?.let { 
    // This block only executes if nullableString is not null
    // 'it' is the non-null string
    println("String length: ${it.length}")
    println("Uppercase: ${it.uppercase()}")
}
    
// Using let for transformation
val words = nullableString?.let { 
    it.split(" ").size 
}  // Int? = 2
    
// Using run for multiple operations
val result = nullableString?.run {
    // 'this' is the non-null string
    val words = split(" ")
    "First word: ${words.first()}, Length: ${length}"
}  // String? = "First word: Hello, Length: 11"
    
// Using also for side effects
val processed = nullableString?.also {
    println("Processing: $it")
}?.uppercase()
    
println("Processed: $processed")

Safe Calls in Complex Scenarios

// Safe calls with collections
val nullableList: List<String>? = listOf("a", "b", "c")
val firstElement = nullableList?.firstOrNull()  // String? = "a"
val size = nullableList?.size                   // Int? = 3
    
// Safe calls with arrays
val nullableArray: Array<String>? = arrayOf("x", "y", "z")
val firstArrayElement = nullableArray?.get(0)   // String? = "x"
    
// Safe calls with maps
val nullableMap: Map<String, Int>? = mapOf("a" to 1, "b" to 2)
val value = nullableMap?.get("a")               // Int? = 1
    
// Safe calls with function references
val string: String? = "hello"
val lengthFunc: (() -> Int)? = string?.::length
val length = lengthFunc?.invoke()               // Int? = 5
    
// Safe calls in when expressions
when (val city = person?.address?.city) {
    null -> println("No city specified")
    else -> println("City: $city")
}
    
// Safe calls with extension functions
fun String?.isNotNullOrBlank(): Boolean = 
    this?.isNotBlank() ?: false
    
val check1 = "Hello".isNotNullOrBlank()  // true
val check2 = "   ".isNotNullOrBlank()    // false  
val check3 = null.isNotNullOrBlank()     // false

Common Pitfalls

  • Overusing safe calls can lead to complex nullable chains
  • Forgetting that safe calls return nullable types
  • Using safe calls when a null check would be clearer
  • Not considering performance of long safe call chains

Elvis Operator in Kotlin

The Elvis operator (?:) provides a default value when a nullable expression is null. It's named for its resemblance to Elvis Presley's hairstyle.

Basic Elvis Operator Usage

fun main() {
    val nullableString: String? = "Hello"
    val nullString: String? = null
    
    // Basic Elvis operator
    val length1 = nullableString?.length ?: 0  // Int = 5
    val length2 = nullString?.length ?: 0      // Int = 0
    
    println("Length 1: $length1")
    println("Length 2: $length2")
    
    // With different default values
    val name: String? = null
    val greeting = "Hello, ${name ?: "Guest"}!"  // "Hello, Guest!"
    println(greeting)
    
    // Complex expressions as defaults
    val input: String? = null
    val result = input?.toIntOrNull() ?: throw IllegalArgumentException("Invalid input")
    // If input is null, exception is thrown
    
    // Elvis with function calls
    fun getDefaultMessage(): String = "Default message"
    val message = nullableString ?: getDefaultMessage()
}

Elvis Operator with Safe Calls

data class Person(val name: String?, val age: Int?)
    
val person: Person? = Person(null, 25)
val nullPerson: Person? = null
    
// Combining safe call and Elvis
val personName = person?.name ?: "Unknown"  // "Unknown"
val nullPersonName = nullPerson?.name ?: "Unknown"  // "Unknown"
    
// Complex chains
val street = person?.let { 
    it.name ?: "No name"
} ?: "No person"
    
// Multiple levels of nullability
val age = person?.age ?: 0  // Int = 25
    
// With collections
val names: List<String>? = null
val first = names?.firstOrNull() ?: "No names"  // "No names"
    
// In function returns
fun processInput(input: String?): String {
    return input?.trim()?.takeIf { it.isNotBlank() } ?: "Default"
}
    
println(processInput("  Hello  "))  // "Hello"
println(processInput("   "))        // "Default"
println(processInput(null))         // "Default"

Advanced Elvis Patterns

// Elvis with early return
fun processUser(user: User?) {
    val name = user?.name ?: return  // Early return if user or name is null
    println("Processing: $name")
}
    
// Elvis with error handling
fun parseNumber(str: String?): Int {
    return str?.toIntOrNull() ?: error("Invalid number: $str")
}
    
// Elvis in when expressions
val status: String? = null
when (val actualStatus = status ?: "unknown") {
    "active" -> println("User is active")
    "inactive" -> println("User is inactive") 
    "unknown" -> println("Status unknown")
}
    
// Elvis with logging
val value: String? = null
val result = value ?: run {
    println("Warning: value was null, using default")
    "default"
}
    
// Elvis in property delegation
class Config {
    var apiKey: String by lazy {
        System.getenv("API_KEY") ?: throw IllegalStateException("API_KEY not set")
    }
}
    
// Elvis for providing alternative computations
fun computeResult(data: Data?): Result {
    return data?.let { processData(it) } ?: Result.EMPTY
}

Common Pitfalls

  • Using expensive operations as Elvis defaults
  • Forgetting that Elvis operator has lower precedence than most operators
  • Overusing Elvis when a proper null check would be clearer
  • Not considering that the default value itself might need null checking

Variables in Kotlin

Variables are named storage locations in memory that hold data. Kotlin has two types of variable declarations: val (immutable) and var (mutable).

Variable Declaration and Initialization

fun main() {
    // Immutable variables (val - value)
    val name = "Alice"
    val age = 25
    val score = 95.5
    val isActive = true
    
    // name = "Bob"  // Error: val cannot be reassigned
    
    // Mutable variables (var - variable)
    var counter = 0
    counter = 1     // This is allowed
    counter += 5    // Compound assignment
    
    // Multiple variables
    var x = 5, y = 10, z = 15
    
    // Type inference
    val inferredString = "Hello"    // Inferred as String
    val inferredInt = 42           // Inferred as Int
    val inferredDouble = 3.14      // Inferred as Double
    
    // Explicit type declarations
    val explicitString: String = "Explicit"
    val explicitNumber: Number = 42  // Number is supertype of Int
    
    println("Name: $name")
    println("Counter: $counter")
}

Variable Scope

// Top-level variables
const val PI = 3.14159
val globalCounter = 0

fun demoFunction() {
    // Local variables
    val localVar = 50
    println("Local: $localVar, Global: $globalCounter")
    
    // Block scope
    {
        val blockVar = 25
        println("Block: $blockVar")
    }
    // println(blockVar)  // Error: blockVar not accessible here
}

class MyClass {
    // Property (member variable)
    var property = "property"
    
    fun demo() {
        // Local variable shadows property
        val property = "local"
        println("Local: $property")           // "local"
        println("Property: ${this.property}") // "property"
    }
}

fun main() {
    demoFunction()
    // println(localVar)  // Error: localVar not accessible here
}

Constants and Late Initialization

// Compile-time constants (only primitive and String)
const val MAX_SIZE = 100
const val APP_NAME = "MyApp"
    
// Late initialization (for variables that can't be initialized in constructor)
class MyService {
    lateinit var service: SomeService
    
    fun initialize() {
        service = SomeService()  // Must initialize before use
    }
    
    fun useService() {
        if (::service.isInitialized) {
            service.doSomething()
        }
    }
}
    
// Lazy initialization (initialized on first access)
val lazyValue: String by lazy {
    println("Computing lazy value")
    "Hello"
}
    
fun main() {
    println(lazyValue)  // "Computing lazy value" then "Hello"
    println(lazyValue)  // "Hello" (already computed)
}

Properties and Custom Accessors

class Rectangle(val width: Int, val height: Int) {
    // Computed property
    val area: Int
        get() = width * height
    
    // Property with custom setter
    var text: String = ""
        set(value) {
            field = value.trim().uppercase()
        }
    
    // Property with validation
    var positiveNumber: Int = 1
        set(value) {
            require(value > 0) { "Number must be positive" }
            field = value
        }
}
    
fun main() {
    val rect = Rectangle(5, 3)
    println("Area: ${rect.area}")  // 15
    
    rect.text = "  hello world  "
    println("Text: ${rect.text}")  // "HELLO WORLD"
    
    // rect.positiveNumber = -5  // Throws IllegalArgumentException
}

Common Pitfalls

  • Using var when val would be sufficient
  • Accessing lateinit variables before initialization
  • Using lazy for properties that might never be used
  • Creating mutable global state with top-level var declarations

Conditional Statements: if, else if, else

Conditional statements allow your program to make decisions and execute different code blocks based on conditions.

Basic if Statement

fun main() {
    print("Enter a number: ")
    val number = readLine()?.toIntOrNull() ?: 0
    
    // Simple if statement
    if (number > 0) {
        println("The number is positive")
    }
    
    // if-else statement
    if (number % 2 == 0) {
        println("The number is even")
    } else {
        println("The number is odd")
    }
    
    // if as expression (returns a value)
    val parity = if (number % 2 == 0) "even" else "odd"
    println("The number is $parity")
}

if-else if-else Chain

print("Enter your score (0-100): ")
val score = readLine()?.toIntOrNull() ?: 0

// Traditional if-else if chain
if (score >= 90) {
    println("Grade: A")
} else if (score >= 80) {
    println("Grade: B")
} else if (score >= 70) {
    println("Grade: C")
} else if (score >= 60) {
    println("Grade: D")
} else {
    println("Grade: F")
}

// Using when expression (often better for multiple conditions)
val grade = when {
    score >= 90 -> "A"
    score >= 80 -> "B" 
    score >= 70 -> "C"
    score >= 60 -> "D"
    else -> "F"
}
println("Grade: $grade")

Nested if Statements

print("Enter your age: ")
val age = readLine()?.toIntOrNull() ?: 0
print("Do you have a license? (true/false): ")
val hasLicense = readLine()?.toBoolean() ?: false
    
if (age >= 18) {
    if (hasLicense) {
        println("You can drive legally")
    } else {
        println("You need to get a license first")
    }
} else {
    println("You are too young to drive")
}

// Flattened version (often preferred)
if (age >= 18 && hasLicense) {
    println("You can drive legally")
} else if (age >= 18) {
    println("You need to get a license first")
} else {
    println("You are too young to drive")
}

if as Expression

val a = 10
val b = 20
    
// if expression returns a value
val max = if (a > b) a else b
println("Maximum: $max")
    
// Multi-line if expression (last expression is returned)
val description = if (a > b) {
    "a is greater than b"
} else if (a < b) {
    "a is less than b" 
} else {
    "a is equal to b"
}
println(description)
    
// Using if with null safety
val input: String? = "42"
val number = if (input != null) input.toIntOrNull() else null
    
// Complex conditions
val x = 5
val y = 10
val z = 15
val largest = if (x > y && x > z) {
    x
} else if (y > x && y > z) {
    y
} else {
    z
}
println("Largest: $largest")

Common Pitfalls

  • Using assignment = instead of equality == in conditions
  • Forgetting that if returns Unit if used as statement
  • Not covering all cases in if-else chains
  • Using complex if expressions when when would be clearer

for Loop in Kotlin

The for loop iterates over anything that provides an iterator, including ranges, arrays, collections, and custom iterable objects.

Basic for Loop

fun main() {
    // Iterate over a range
    for (i in 1..5) {
        println("Count: $i")
    }
    
    // Countdown
    for (i in 10 downTo 1) {
        print("$i ")
    }
    println()
    
    // With step
    for (i in 0 until 10 step 2) {
        print("$i ")  // 0 2 4 6 8
    }
    println()
    
    // Iterate through array
    val numbers = arrayOf(2, 4, 6, 8, 10)
    for (number in numbers) {
        print("$number ")
    }
    println()
    
    // With index
    for (index in numbers.indices) {
        println("numbers[$index] = ${numbers[index]}")
    }
}

Advanced for Loop Variations

// Iterate over lists
val fruits = listOf("apple", "banana", "cherry")
for (fruit in fruits) {
    println(fruit)
}
    
// With index using withIndex()
for ((index, fruit) in fruits.withIndex()) {
    println("$index: $fruit")
}
    
// Iterate over maps
val map = mapOf("a" to 1, "b" to 2, "c" to 3)
for ((key, value) in map) {
    println("$key -> $value")
}
    
// Iterate over strings
val text = "Kotlin"
for (char in text) {
    print("$char ")
}
println()
    
// Iterate with custom step
for (i in 10 downTo 0 step 3) {
    print("$i ")  // 10 7 4 1
}
println()
    
// Using forEach (alternative to for loop)
fruits.forEach { fruit ->
    println(fruit)
}
    
fruits.forEachIndexed { index, fruit ->
    println("$index: $fruit")
}

Nested for Loops

// Multiplication table
for (i in 1..5) {
    for (j in 1..5) {
        print("${i * j}\t")
    }
    println()
}
    
// Pattern printing
for (i in 1..5) {
    for (j in 1..i) {
        print("*")
    }
    println()
}
    
// Iterate over 2D array
val matrix = arrayOf(
    intArrayOf(1, 2, 3),
    intArrayOf(4, 5, 6),
    intArrayOf(7, 8, 9)
)
    
for (row in matrix) {
    for (element in row) {
        print("$element ")
    }
    println()
}
    
// Using indices for 2D array
for (i in matrix.indices) {
    for (j in matrix[i].indices) {
        print("${matrix[i][j]} ")
    }
    println()
}

Common Pitfalls

  • Confusing .. (inclusive) with until (exclusive)
  • Modifying collections while iterating (can cause ConcurrentModificationException)
  • Using expensive operations in loop conditions
  • Not using forEach when you don't need break/continue functionality

while Loop in Kotlin

The while loop repeats a block of code as long as a condition is true. Kotlin provides both while and do-while loops.

Basic while Loop

fun main() {
    // Count from 1 to 5
    var i = 1
    while (i <= 5) {
        println("Count: $i")
        i++
    }
    
    // User input validation
    var number: Int
    do {
        print("Enter a positive number: ")
        number = readLine()?.toIntOrNull() ?: -1
    } while (number <= 0)
    println("Thank you! You entered: $number")
    
    // Infinite loop with break
    var counter = 0
    while (true) {
        println("Counter: $counter")
        counter++
        if (counter >= 5) break
    }
}

do-while Loop

// do-while executes at least once
var choice: String
do {
    println("Menu:")
    println("1. Option One")
    println("2. Option Two") 
    println("q. Quit")
    print("Enter your choice: ")
    choice = readLine() ?: ""
    
    when (choice) {
        "1" -> println("Option One selected")
        "2" -> println("Option Two selected")
        "q" -> println("Goodbye!")
        else -> println("Invalid choice")
    }
} while (choice != "q")
    
// Reading until sentinel value
var sum = 0
var value: Int
do {
    print("Enter a number (0 to stop): ")
    value = readLine()?.toIntOrNull() ?: 0
    sum += value
} while (value != 0)
println("Sum: $sum")
    
// Password validation
var password: String
var attempts = 0
do {
    print("Enter password: ")
    password = readLine() ?: ""
    attempts++
    if (password != "secret" && attempts < 3) {
        println("Incorrect password. Try again.")
    }
} while (password != "secret" && attempts < 3)
    
if (password == "secret") {
    println("Access granted!")
} else {
    println("Too many attempts!")
}

Practical while Loop Examples

// Processing collections with while
val numbers = mutableListOf(1, 2, 3, 4, 5)
while (numbers.isNotEmpty()) {
    val number = numbers.removeAt(0)
    println("Processing: $number")
}
    
// Reading lines until empty
println("Enter lines (empty line to stop):")
var line: String
while (readLine().also { line = it ?: "" }.isNotEmpty()) {
    println("You entered: $line")
}
    
// Simulation loop
var time = 0
var temperature = 20.0
while (time < 100) {
    temperature += (25 - temperature) * 0.1
    if (time % 10 == 0) {
        println("Time: $time, Temperature: ${"%.2f".format(temperature)}")
    }
    time++
}
    
// Game loop example
var gameRunning = true
var score = 0
while (gameRunning) {
    // Game logic here
    score += 10
    println("Score: $score")
    
    // Condition to end game
    if (score >= 100) {
        gameRunning = false
        println("Game Over!")
    }
}

Common Pitfalls

  • Forgetting to update the loop control variable (infinite loops)
  • Using while when for would be more appropriate
  • Not validating input in do-while validation loops
  • Using mutable variables when immutable would be safer

Loop Control Statements

Kotlin provides loop control statements to alter the normal flow of loops: break, continue, and labels for precise control.

break Statement

fun main() {
    // break exits the loop immediately
    for (i in 1..10) {
        if (i == 5) {
            break  // Exit loop when i reaches 5
        }
        print("$i ")
    }
    println()  // Output: 1 2 3 4
    
    // Search example
    val numbers = listOf(2, 4, 6, 8, 10, 12, 14)
    val target = 8
    
    for (i in numbers.indices) {
        if (numbers[i] == target) {
            println("Found $target at position $i")
            break  // Stop searching once found
        }
    }
    
    // break in while loop
    var count = 0
    while (true) {
        println("Count: $count")
        count++
        if (count >= 3) break
    }
}

continue Statement

// continue skips the rest of current iteration
for (i in 1..10) {
    if (i % 2 == 0) {
        continue  // Skip even numbers
    }
    print("$i ")  // Only odd numbers printed
}
println()  // Output: 1 3 5 7 9

// Skip specific elements in collection
val words = listOf("apple", "banana", "cherry", "date", "elderberry")
for (word in words) {
    if (word.length < 5) {
        continue  // Skip short words
    }
    println(word)  // Only prints words with 5+ characters
}

// continue in nested loops
for (i in 1..3) {
    for (j in 1..3) {
        if (i == j) {
            continue  // Skip when i equals j
        }
        println("i=$i, j=$j")
    }
}

// Using continue with when
for (number in 1..10) {
    when {
        number % 3 == 0 -> continue  // Skip multiples of 3
        number % 2 == 0 -> println("$number is even")
        else -> println("$number is odd")
    }
}

Labels for Loop Control

// Labeled break - break out of specific loop
outer@ for (i in 1..3) {
    inner@ for (j in 1..3) {
        if (i * j > 4) {
            println("Breaking outer loop")
            break@outer  // Break both loops
        }
        println("i=$i, j=$j")
    }
}

// Labeled continue - continue specific loop
matrix@ for (i in 1..3) {
    for (j in 1..3) {
        if (j == 2) {
            continue@matrix  // Continue with next i
        }
        println("matrix[$i][$j]")
    }
}

// Using labels with return in lambdas
val numbers = listOf(1, 2, 3, 4, 5)
numbers.forEach lambda@{
    if (it == 3) return@lambda  // Continue to next element
    println(it)
}

// Alternative syntax for lambda returns
numbers.forEach {
    if (it == 3) return@forEach  // Continue to next element
    println(it)
}

// Using run with label for complex control flow
run loop@{
    numbers.forEach {
        if (it > 3) return@loop  // Break out of the run block
        println(it)
    }
}

Common Pitfalls

  • Using break when continue is more appropriate
  • Overusing labels makes code harder to read
  • break and continue only work with loops, not forEach
  • Forgetting that unlabeled break only affects the innermost loop

Nested Loops in Kotlin

Nested loops are loops within loops. They are essential for working with multi-dimensional data, matrices, and complex patterns.

Basic Nested Loops

fun main() {
    // Simple nested loop
    for (i in 1..3) {
        for (j in 1..3) {
            print("($i,$j) ")
        }
        println()
    }
    /* Output:
    (1,1) (1,2) (1,3) 
    (2,1) (2,2) (2,3) 
    (3,1) (3,2) (3,3) */
    
    // Multiplication table
    println("\nMultiplication Table:")
    for (i in 1..5) {
        for (j in 1..5) {
            print("${i * j}\t")
        }
        println()
    }
    
    // Different loop types in nesting
    var i = 1
    while (i <= 3) {
        for (j in 1..3) {
            println("i=$i, j=$j")
        }
        i++
    }
}

Pattern Printing with Nested Loops

val rows = 5
    
// Right triangle
for (i in 1..rows) {
    for (j in 1..i) {
        print("*")
    }
    println()
}
    
// Inverted triangle
for (i in rows downTo 1) {
    for (j in 1..i) {
        print("*")
    }
    println()
}
    
// Pyramid
for (i in 1..rows) {
    // Print spaces
    for (j in 1..rows - i) {
        print(" ")
    }
    // Print stars
    for (j in 1..2 * i - 1) {
        print("*")
    }
    println()
}
    
// Number patterns
for (i in 1..5) {
    for (j in 1..i) {
        print("$j ")
    }
    println()
}
    
// Hollow square
val size = 5
for (i in 1..size) {
    for (j in 1..size) {
        if (i == 1 || i == size || j == 1 || j == size) {
            print("* ")
        } else {
            print("  ")
        }
    }
    println()
}

Working with 2D Arrays and Matrices

// Matrix operations
val matrix = arrayOf(
    intArrayOf(1, 2, 3),
    intArrayOf(4, 5, 6),
    intArrayOf(7, 8, 9)
)
    
// Print matrix
println("Matrix:")
for (i in matrix.indices) {
    for (j in matrix[i].indices) {
        print("${matrix[i][j]} ")
    }
    println()
}
    
// Sum of all elements
var sum = 0
for (row in matrix) {
    for (element in row) {
        sum += element
    }
}
println("Sum of all elements: $sum")
    
// Transpose matrix
println("Transpose:")
for (i in matrix[0].indices) {
    for (j in matrix.indices) {
        print("${matrix[j][i]} ")
    }
    println()
}
    
// Matrix multiplication
val a = arrayOf(
    intArrayOf(1, 2, 3),
    intArrayOf(4, 5, 6)
)
val b = arrayOf(
    intArrayOf(7, 8),
    intArrayOf(9, 10),
    intArrayOf(11, 12)
)
    
val result = Array(a.size) { IntArray(b[0].size) }
for (i in a.indices) {
    for (j in b[0].indices) {
        for (k in b.indices) {
            result[i][j] += a[i][k] * b[k][j]
        }
    }
}
    
println("Matrix multiplication result:")
for (row in result) {
    println(row.joinToString(" "))
}

Performance Considerations

// O(n²) time complexity example
val n = 1000
    
// Inefficient nested loops
var operations = 0
for (i in 0 until n) {
    for (j in 0 until n) {
        operations++  // This runs n² times
    }
}
println("Operations: $operations")  // 1,000,000
    
// Optimizing nested loops
// Move invariant computations outside inner loops
val data = List(n) { it }
var sum = 0
    
// Less efficient
for (i in data.indices) {
    for (j in data.indices) {
        sum += data[i] + data[j]  // data[i] accessed n times
    }
}
    
// More efficient  
for (i in data.indices) {
    val value = data[i]  // Compute once per outer iteration
    for (j in data.indices) {
        sum += value + data[j]
    }
}
    
// Using built-in functions when possible
val totalSum = data.flatMap { i ->
    data.map { j -> i + j }
}.sum()
println("Total sum: $totalSum")

Common Pitfalls

  • O(n²) time complexity can be inefficient for large data
  • Using wrong loop variables in inner vs outer loops
  • Forgetting to reset inner loop variables when needed
  • Excessive nesting reduces readability (try to keep to 2-3 levels)

Functions in Kotlin

Functions are reusable blocks of code that perform specific tasks. Kotlin supports both top-level functions and member functions, with features like default parameters, named arguments, and extension functions.

Function Definition and Usage

// Top-level function
fun greet(name: String): String {
    return "Hello, $name!"
}
    
// Single-expression function
fun square(x: Int): Int = x * x
    
// Function without parameters
fun getGreeting(): String = "Hello, World!"
    
// Function without return value (Unit is implicit)
fun printMessage(message: String) {
    println("Message: $message")
}
    
fun main() {
    // Calling functions
    println(greet("Alice"))
    println(square(5))
    println(getGreeting())
    printMessage("Functions make code organized!")
    
    // Function as expression
    val result = greet("Bob")
    println(result)
}

Function Parameters

// Default parameters
fun displayInfo(name: String, age: Int = 18, city: String = "Unknown") {
    println("Name: $name, Age: $age, City: $city")
}
    
// Named arguments
fun connectToDatabase(
    host: String = "localhost",
    port: Int = 5432,
    username: String,
    password: String
) {
    println("Connecting to $host:$port as $username")
}
    
// Variable number of arguments (vararg)
fun printAll(vararg messages: String) {
    for (message in messages) {
        println(message)
    }
}
    
fun sumAll(vararg numbers: Int): Int {
    return numbers.sum()
}
    
fun main() {
    // Using default parameters
    displayInfo("Alice")              // Uses default age and city
    displayInfo("Bob", 25)            // Uses default city
    displayInfo("Charlie", 30, "NYC") // Uses all provided values
    
    // Using named arguments
    connectToDatabase(username = "admin", password = "secret")
    connectToDatabase(host = "192.168.1.1", port = 3306, 
                     username = "user", password = "pass")
    
    // Using vararg
    printAll("Hello", "World", "Kotlin")
    println("Sum: ${sumAll(1, 2, 3, 4, 5)}")
    
    // Spread operator
    val numbers = intArrayOf(1, 2, 3)
    println("Spread sum: ${sumAll(*numbers)}")
}

Extension Functions and Infix Functions

// Extension function - adds functionality to existing class
fun String.addExcitement(): String = "$this!"
    
fun Int.isEven(): Boolean = this % 2 == 0
    
fun List<Int>.product(): Int = this.fold(1) { acc, value -> acc * value }
    
// Infix function - can be called without dot and parentheses
infix fun Int.times(str: String): String = str.repeat(this)
    
infix fun String.shouldEqual(other: String): Boolean = this == other
    
fun main() {
    // Using extension functions
    println("Hello".addExcitement())  // "Hello!"
    println(5.isEven())               // false
    println(listOf(2, 3, 4).product()) // 24
    
    // Using infix functions
    println(3 times "Hello ")  // "Hello Hello Hello "
    println("kotlin" shouldEqual "kotlin")  // true
    
    // Infix with custom types
    data class Point(val x: Int, val y: Int)
    
    infix fun Point.distanceTo(other: Point): Double {
        return Math.sqrt((x - other.x).toDouble().pow(2) + 
                        (y - other.y).toDouble().pow(2))
    }
    
    val p1 = Point(0, 0)
    val p2 = Point(3, 4)
    println("Distance: ${p1 distanceTo p2}")  // 5.0
}

Higher-Order Functions and Lambdas

// Function types
val multiplier: (Int, Int) -> Int = { a, b -> a * b }
val greeter: (String) -> String = { name -> "Hello, $name" }
    
// Higher-order function - takes function as parameter
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
    return operation(a, b)
}
    
// Higher-order function - returns function
fun getMultiplier(factor: Int): (Int) -> Int {
    return { number -> number * factor }
}
    
fun main() {
    // Using function types
    println(multiplier(5, 3))  // 15
    println(greeter("Alice"))  // "Hello, Alice"
    
    // Passing lambdas to higher-order functions
    val sum = calculate(10, 5) { a, b -> a + b }
    val product = calculate(10, 5) { a, b -> a * b }
    println("Sum: $sum, Product: $product")
    
    // Using returned function
    val double = getMultiplier(2)
    val triple = getMultiplier(3)
    println("Double 5: ${double(5)}")   // 10
    println("Triple 5: ${triple(5)}")   // 15
    
    // Common higher-order functions
    val numbers = listOf(1, 2, 3, 4, 5)
    val doubled = numbers.map { it * 2 }
    val evens = numbers.filter { it % 2 == 0 }
    val sumAll = numbers.reduce { acc, value -> acc + value }
    
    println("Doubled: $doubled")     // [2, 4, 6, 8, 10]
    println("Evens: $evens")         // [2, 4]
    println("Sum: $sumAll")          // 15
}

Common Pitfalls

  • Forgetting that single-expression functions need =
  • Mixing positional and named arguments incorrectly
  • Using extension functions when inheritance would be better
  • Creating expensive lambdas that are executed frequently

Packages and Imports in Kotlin

Packages help organize code into namespaces, while imports make code from other packages accessible. Kotlin has concise import syntax and supports various import features.

Package Declaration and Structure

// File: com/example/utils/MathUtils.kt
package com.example.utils
    
object MathUtils {
    const val PI = 3.14159
    
    fun factorial(n: Int): Long {
        return if (n <= 1) 1 else n * factorial(n - 1)
    }
    
    fun isPrime(n: Int): Boolean {
        if (n <= 1) return false
        for (i in 2..n/2) {
            if (n % i == 0) return false
        }
        return true
    }
}
    
// File: com/example/utils/StringUtils.kt  
package com.example.utils
    
fun String.isPalindrome(): Boolean {
    return this == this.reversed()
}
    
fun String.countWords(): Int {
    return this.split("\\s+".toRegex()).size
}
    
// File: com/example/Main.kt
package com.example
    
fun main() {
    // Using fully qualified names
    val fact = com.example.utils.MathUtils.factorial(5)
    println("Factorial: $fact")
}

Import Statements

// Main.kt with imports
package com.example
    
// Import specific declarations
import com.example.utils.MathUtils
import com.example.utils.MathUtils.PI
import com.example.utils.StringUtils.isPalindrome
    
// Import all declarations from package
import com.example.utils.*
    
// Import with alias
import com.example.utils.MathUtils as Math
import kotlin.math.PI as KPI
    
fun main() {
    // Using imported declarations
    println("PI: $PI")  // From MathUtils
    println("Kotlin PI: $KPI")  // From kotlin.math
    
    val result = Math.factorial(5)
    println("Factorial: $result")
    
    val text = "racecar"
    println("Is palindrome: ${text.isPalindrome()}")
}

Standard Library Imports

// Many Kotlin features are automatically imported
// kotlin.*, kotlin.annotation.*, kotlin.collections.*, etc.
    
// Common imports
import kotlin.math.*          // Mathematical functions
import kotlin.collections.*   // Collection types
import kotlin.io.*            // I/O operations
import kotlin.text.*          // Text processing
    
// Java interop imports
import java.util.*            // Java utilities
import java.io.File           // Java file handling
    
fun demonstrateImports() {
    // Using kotlin.math
    println("Square root: ${sqrt(16.0)}")
    println("Power: ${2.0.pow(3.0)}")
    println("Absolute: ${abs(-5)}")
    
    // Using collections
    val list = listOf(1, 2, 3)
    val map = mapOf("a" to 1, "b" to 2)
    
    // Using Java classes
    val date = Date()
    val file = File("example.txt")
}

Import Best Practices and Patterns

// File: utils/Validations.kt
package utils
    
// Using import on demand vs specific imports
import kotlin.text.Regex
import kotlin.text.isDigit
    
fun validateEmail(email: String): Boolean {
    val pattern = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\$")
    return pattern.matches(email)
}
    
fun validatePhone(phone: String): Boolean {
    return phone.all { it.isDigit() } && phone.length == 10
}
    
// File: Main.kt with organized imports
package main
    
import kotlin.math.PI
import kotlin.math.pow
import utils.validateEmail
import utils.validatePhone
    
// Importing extension functions
import kotlin.random.Random
import kotlin.random.nextInt
    
fun main() {
    // Using imported functions
    println("Area of circle: ${PI * 5.0.pow(2)}")
    
    val email = "user@example.com"
    val phone = "1234567890"
    
    println("Email valid: ${validateEmail(email)}")
    println("Phone valid: ${validatePhone(phone)}")
    
    // Using imported extension from Random
    val random = Random.nextInt(1..100)
    println("Random number: $random")
}
    
// Importing for testing
import org.junit.Test
import org.junit.Assert.*
    
class MathTests {
    @Test
    fun testFactorial() {
        assertEquals(120, MathUtils.factorial(5))
    }
}

Common Pitfalls

  • Name conflicts when importing multiple packages with same names
  • Forgetting to import necessary packages
  • Using wildcard imports in large projects (can cause confusion)
  • Circular dependencies between packages

File Input/Output in Kotlin

Kotlin provides extensive file I/O capabilities through extension functions on the File class and utilities in the kotlin.io package.

Basic File Operations

import java.io.File
    
fun main() {
    // Writing to a file
    File("example.txt").writeText("Hello, File!\n")
    File("example.txt").appendText("This is line 2\n")
    
    // Reading from a file
    val content = File("example.txt").readText()
    println("File content:\n$content")
    
    // Reading line by line
    val lines = File("example.txt").readLines()
    lines.forEachIndexed { index, line ->
        println("${index + 1}: $line")
    }
    
    // Using useLines for efficient large file reading
    val wordCount = File("example.txt").useLines { lines ->
        lines.flatMap { it.split("\\s+".toRegex()) }
            .count { it.isNotBlank() }
    }
    println("Word count: $wordCount")
}

File and Directory Management

import java.io.File
    
fun demonstrateFileOperations() {
    // Creating files and directories
    val dir = File("mydir")
    dir.mkdir()  // Create directory
    
    val file = File(dir, "data.txt")
    file.writeText("Some data")
    
    // Checking file properties
    println("Exists: ${file.exists()}")
    println("Is file: ${file.isFile}")
    println("Is directory: ${file.isDirectory}")
    println("Name: ${file.name}")
    println("Path: ${file.path}")
    println("Absolute path: ${file.absolutePath}")
    println("Parent: ${file.parent}")
    println("Size: ${file.length()} bytes")
    println("Last modified: ${file.lastModified()}")
    
    // Listing directory contents
    val currentDir = File(".")
    println("Directory contents:")
    currentDir.list()?.forEach { println(it) }
    
    // Recursive directory walking
    println("All files:")
    currentDir.walk().forEach { println(it) }
    
    // Filtering files
    val txtFiles = currentDir.walk()
        .filter { it.isFile && it.extension == "txt" }
        .toList()
    println("Text files: $txtFiles")
}

Advanced File Operations

import java.io.File
import java.io.BufferedReader
import java.io.BufferedWriter
    
fun demonstrateAdvancedIO() {
    // Using buffered readers and writers
    File("output.txt").bufferedWriter().use { writer ->
        writer.write("Line 1\n")
        writer.write("Line 2\n")
        writer.write("Line 3\n")
    }
    
    // Reading with buffered reader
    File("output.txt").bufferedReader().use { reader ->
        reader.lineSequence().forEach { line ->
            println("Read: $line")
        }
    }
    
    // Copying files
    File("output.txt").copyTo(File("output_copy.txt"), overwrite = true)
    
    // Moving/renaming files
    File("output_copy.txt").renameTo(File("renamed.txt"))
    
    // Deleting files
    File("renamed.txt").delete()
    
    // Temporary files
    val tempFile = File.createTempFile("prefix", ".txt")
    tempFile.writeText("Temporary content")
    println("Temp file: ${tempFile.absolutePath}")
    tempFile.deleteOnExit()  // Delete when JVM exits
    
    // File traversal with custom logic
    val projectDir = File(".")
    val totalSize = projectDir.walk()
        .filter { it.isFile }
        .map { it.length() }
        .sum()
    println("Total project size: $totalSize bytes")
}

Working with Different File Formats

import java.io.File
import kotlinx.serialization.*
import kotlinx.serialization.json.*
    
data class Person(val name: String, val age: Int, val email: String)
    
fun demonstrateFormats() {
    // CSV file handling
    val csvData = """
        name,age,email
        Alice,25,alice@example.com
        Bob,30,bob@example.com
        Charlie,35,charlie@example.com
    """.trimIndent()
    
    File("people.csv").writeText(csvData)
    
    // Reading CSV
    val people = File("people.csv").readLines().drop(1)  // Skip header
        .map { line ->
            val parts = line.split(",")
            Person(parts[0], parts[1].toInt(), parts[2])
        }
    
    println("People from CSV:")
    people.forEach { println(it) }
    
    // JSON handling (with kotlinx.serialization)
    val jsonString = """
        [
            {"name": "Alice", "age": 25, "email": "alice@example.com"},
            {"name": "Bob", "age": 30, "email": "bob@example.com"}
        ]
    """.trimIndent()
    
    File("people.json").writeText(jsonString)
    
    // Writing configuration files
    val config = mapOf(
        "database.host" to "localhost",
        "database.port" to "5432",
        "app.name" to "MyApp"
    )
    
    File("config.properties").writer().use { writer ->
        config.forEach { (key, value) ->
            writer.write("$key=$value\n")
        }
    }
    
    // Reading properties file
    val loadedConfig = File("config.properties").readLines()
        .associate { line ->
            val (key, value) = line.split("=")
            key to value
        }
    println("Loaded config: $loadedConfig")
}

Common Pitfalls

  • Not using use for resource management (potential resource leaks)
  • Reading entire large files into memory with readText()
  • Not checking if files exist before operations
  • Forgetting to handle character encoding properly

Collections Introduction

Kotlin provides a rich set of collection types that are built on Java collections but with more concise and safe APIs. Collections are categorized as mutable and immutable.

Collection Types Overview

fun main() {
    // List - ordered collection
    val readOnlyList = listOf("a", "b", "c")
    val mutableList = mutableListOf(1, 2, 3)
    
    // Set - unique elements
    val readOnlySet = setOf("x", "y", "z")
    val mutableSet = mutableSetOf(1, 2, 3)
    
    // Map - key-value pairs
    val readOnlyMap = mapOf("a" to 1, "b" to 2, "c" to 3)
    val mutableMap = mutableMapOf("x" to 10, "y" to 20)
    
    println("List: $readOnlyList")
    println("Set: $readOnlySet")
    println("Map: $readOnlyMap")
    
    // Collection properties
    println("List size: ${readOnlyList.size}")
    println("Set contains 'x': ${readOnlySet.contains("x")}")
    println("Map value for 'a': ${readOnlyMap["a"]}")
    
    // Collection creation functions
    val emptyList = emptyList<String>()
    val listWithSize = List(5) { it * 2 }  // [0, 2, 4, 6, 8]
    val linkedList = LinkedList(listOf(1, 2, 3))
    
    println("Created list: $listWithSize")
}

Mutable vs Immutable Collections

// Immutable collections (read-only)
val immutableList = listOf(1, 2, 3)
val immutableSet = setOf("a", "b", "c")
val immutableMap = mapOf("key" to "value")
    
// These operations create new collections, not modify existing ones
val newList = immutableList + 4
val newSet = immutableSet - "a"
    
// Mutable collections
val mutableList = mutableListOf(1, 2, 3)
val mutableSet = mutableSetOf("a", "b", "c") 
val mutableMap = mutableMapOf("key" to "value")
    
// These operations modify the existing collection
mutableList.add(4)
mutableSet.remove("a")
mutableMap["newKey"] = "newValue"
    
// Converting between mutable and immutable
val readOnly: List<Int> = mutableList.toList()
val mutable: MutableList<Int> = readOnly.toMutableList()
    
// Read-only views (still mutable through original reference)
val view: List<Int> = mutableList
mutableList.add(5)  // This changes the view too!
println("View: $view")  // [1, 2, 3, 4, 5]

Collection Hierarchy and Interfaces

// Collection interfaces
val collection: Collection<Int> = listOf(1, 2, 3)
val list: List<String> = listOf("a", "b", "c")
val set: Set<Int> = setOf(1, 2, 3)
val map: Map<String, Int> = mapOf("a" to 1, "b" to 2)
    
// Mutable interfaces
val mutableCollection: MutableCollection<Int> = mutableListOf(1, 2, 3)
val mutableList: MutableList<String> = mutableListOf("a", "b", "c")
val mutableSet: MutableSet<Int> = mutableSetOf(1, 2, 3)
val mutableMap: MutableMap<String, Int> = mutableMapOf("a" to 1, "b" to 2)
    
// Iterable operations
val numbers = listOf(1, 2, 3, 4, 5)
    
// Transformation
val doubled = numbers.map { it * 2 }
val strings = numbers.map { "Number: $it" }
    
// Filtering
val evens = numbers.filter { it % 2 == 0 }
val odds = numbers.filterNot { it % 2 == 0 }
    
// Aggregation
val sum = numbers.sum()
val average = numbers.average()
val max = numbers.maxOrNull()
val min = numbers.minOrNull()
    
println("Doubled: $doubled")
println("Evens: $evens")
println("Sum: $sum, Average: $average")

Common Pitfalls

  • Modifying collections while iterating over them
  • Assuming read-only collections are deeply immutable
  • Using the wrong collection type for the use case
  • Forgetting that some operations create new collections

Collection Types in Kotlin

Kotlin provides various collection types optimized for different use cases, including lists, sets, maps, and their mutable counterparts.

List Types and Operations

fun main() {
    // Different list implementations
    val arrayList = arrayListOf(1, 2, 3)        // ArrayList
    val linkedList = LinkedList(listOf(1, 2, 3)) // LinkedList
    
    // List operations
    val fruits = mutableListOf("apple", "banana", "cherry")
    
    // Adding elements
    fruits.add("date")
    fruits.add(1, "blueberry")
    fruits.addAll(listOf("elderberry", "fig"))
    
    // Removing elements
    fruits.remove("banana")
    fruits.removeAt(0)
    fruits.removeAll(listOf("fig", "grape"))
    
    // Accessing elements
    println("First: ${fruits.first()}")
    println("Last: ${fruits.last()}")
    println("Element at 1: ${fruits[1]}")
    println("Get or default: ${fruits.getOrElse(10) { "unknown" }}")
    
    // Sublists
    val subList = fruits.subList(0, 2)
    println("Sublist: $subList")
    
    // List information
    println("Size: ${fruits.size}")
    println("Is empty: ${fruits.isEmpty()}")
    println("Contains 'apple': ${fruits.contains("apple")}")
    println("Index of 'cherry': ${fruits.indexOf("cherry")}")
}

Set Types and Operations

// Set implementations
val hashSet = hashSetOf(1, 2, 3)          // HashSet (no order guarantee)
val linkedHashSet = linkedSetOf(1, 2, 3)  // LinkedHashSet (insertion order)
val sortedSet = sortedSetOf(3, 1, 2)      // TreeSet (sorted order)
    
println("HashSet: $hashSet")        // [1, 2, 3] (order may vary)
println("LinkedHashSet: $linkedHashSet")  // [1, 2, 3] (insertion order)
println("SortedSet: $sortedSet")          // [1, 2, 3] (sorted order)
    
// Set operations
val set1 = setOf(1, 2, 3, 4, 5)
val set2 = setOf(4, 5, 6, 7, 8)
    
// Set operations
println("Union: ${set1 union set2}")           // [1, 2, 3, 4, 5, 6, 7, 8]
println("Intersection: ${set1 intersect set2}") // [4, 5]
println("Difference: ${set1 subtract set2}")   // [1, 2, 3]
    
// Mutable set operations
val mutableSet = mutableSetOf(1, 2, 3)
mutableSet.add(4)
mutableSet.remove(1)
mutableSet.addAll(setOf(5, 6))
mutableSet.retainAll(setOf(2, 3, 4))  // Keep only these elements
    
println("Mutable set: $mutableSet")  // [2, 3, 4]

Map Types and Operations

// Map implementations
val hashMap = hashMapOf("a" to 1, "b" to 2)          // HashMap
val linkedMap = linkedMapOf("a" to 1, "b" to 2)      // LinkedHashMap (insertion order)
val sortedMap = sortedMapOf("b" to 2, "a" to 1)      // TreeMap (sorted by key)
    
println("HashMap: $hashMap")        // {a=1, b=2} (order may vary)
println("LinkedMap: $linkedMap")    // {a=1, b=2} (insertion order)
println("SortedMap: $sortedMap")    // {a=1, b=2} (sorted by key)
    
// Map operations
val map = mutableMapOf(
    "Alice" to 25,
    "Bob" to 30,
    "Charlie" to 35
)
    
// Accessing values
println("Alice's age: ${map["Alice"]}")
println("Get or default: ${map.getOrDefault("David", 0)}")
    
// Adding and updating
map["David"] = 40           // Add new entry
map["Alice"] = 26           // Update existing
map.putAll(mapOf("Eve" to 28, "Frank" to 32))
    
// Removing entries
map.remove("Bob")
map.remove("Charlie", 30)   // Only remove if value matches
    
// Map views
println("Keys: ${map.keys}")
println("Values: ${map.values}")
println("Entries: ${map.entries}")
    
// Filtering maps
val adults = map.filter { (_, age) -> age >= 18 }
val namesStartingWithA = map.filterKeys { it.startsWith("A") }
val youngAges = map.filterValues { it < 30 }
    
println("Adults: $adults")
println("A names: $namesStartingWithA")

Performance Characteristics

/* 
Collection Type    | Add     | Remove  | Access  | Search  | Memory
-------------------|---------|---------|---------|---------|--------
ArrayList         | O(1)*   | O(n)    | O(1)    | O(n)    | Less
LinkedList        | O(1)    | O(1)    | O(n)    | O(n)    | More
HashSet           | O(1)*   | O(1)*   | O(1)*   | O(1)*   | More
LinkedHashSet     | O(1)*   | O(1)*   | O(1)*   | O(1)*   | More
TreeSet           | O(log n)| O(log n)| O(log n)| O(log n)| More
HashMap           | O(1)*   | O(1)*   | O(1)*   | O(1)*   | More
LinkedHashMap     | O(1)*   | O(1)*   | O(1)*   | O(1)*   | More
TreeMap           | O(log n)| O(log n)| O(log n)| O(log n)| More

* = amortized constant time
*/
    
// Choosing the right collection
fun demonstrateCollectionChoice() {
    // Frequent access by index - use ArrayList
    val userList = ArrayList<User>()
    
    // Frequent insertions/removals at ends - use LinkedList  
    val taskQueue = LinkedList<Task>()
    
    // Unique elements, fast lookup - use HashSet
    val uniqueUsers = HashSet<User>()
    
    // Unique elements, insertion order - use LinkedHashSet
    val orderedUsers = LinkedHashSet<User>()
    
    // Sorted elements - use TreeSet
    val sortedUsers = TreeSet<User>(compareBy { it.name })
    
    // Key-value pairs, fast lookup - use HashMap
    val userCache = HashMap<String, User>()
    
    // Key-value pairs, insertion order - use LinkedHashMap
    val configuration = LinkedHashMap<String, Any>()
    
    // Sorted key-value pairs - use TreeMap
    val sortedConfig = TreeMap<String, Any>()
}

Common Pitfalls

  • Using List when you need MutableList and vice versa
  • Choosing the wrong collection type for performance requirements
  • Modifying collections during iteration
  • Forgetting that map keys must have proper equals and hashCode implementations

Collection Operations in Kotlin

Kotlin provides extensive operations for working with collections, including transformation, filtering, aggregation, and utility functions.

Transformation Operations

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    
    // Mapping - transform each element
    val doubled = numbers.map { it * 2 }
    val strings = numbers.map { "Number: $it" }
    println("Doubled: $doubled")       // [2, 4, 6, 8, 10]
    println("Strings: $strings")       // [Number: 1, Number: 2, ...]
    
    // Map indexed - transform with index
    val withIndex = numbers.mapIndexed { index, value -> 
        "Index $index: $value" 
    }
    println("With index: $withIndex")
    
    // Flat map - transform and flatten
    val nested = listOf(listOf(1, 2), listOf(3, 4), listOf(5, 6))
    val flat = nested.flatMap { it }
    println("Flat: $flat")             // [1, 2, 3, 4, 5, 6]
    
    // Zip - combine two collections
    val letters = listOf("A", "B", "C")
    val zipped = numbers.zip(letters)  // [(1, A), (2, B), (3, C)]
    println("Zipped: $zipped")
    
    // Associate - create maps from collections
    val map = numbers.associateWith { it * it }  // {1=1, 2=4, 3=9, 4=16, 5=25}
    val byString = numbers.associateBy { "key$it" }  // {key1=1, key2=2, ...}
    println("Associated: $map")
}

Filtering Operations

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    
// Basic filtering
val evens = numbers.filter { it % 2 == 0 }
val odds = numbers.filterNot { it % 2 == 0 }
println("Evens: $evens")   // [2, 4, 6, 8, 10]
println("Odds: $odds")     // [1, 3, 5, 7, 9]
    
// Filter by type
val mixed = listOf(1, "hello", 2, "world", 3.14)
val numbersOnly = mixed.filterIsInstance<Int>()
println("Numbers only: $numbersOnly")  // [1, 2]
    
// Partition - split into two collections
val (evenList, oddList) = numbers.partition { it % 2 == 0 }
println("Even partition: $evenList")  // [2, 4, 6, 8, 10]
println("Odd partition: $oddList")    // [1, 3, 5, 7, 9]
    
// Take and drop
val firstThree = numbers.take(3)      // [1, 2, 3]
val lastThree = numbers.takeLast(3)   // [8, 9, 10]
val withoutFirstThree = numbers.drop(3)  // [4, 5, 6, 7, 8, 9, 10]
    
println("First three: $firstThree")
println("Last three: $lastThree")
    
// Take/drop while (while condition is true)
val whileSmall = numbers.takeWhile { it < 5 }  // [1, 2, 3, 4]
val afterSmall = numbers.dropWhile { it < 5 }  // [5, 6, 7, 8, 9, 10]
    
// Distinct - remove duplicates
val withDuplicates = listOf(1, 2, 2, 3, 4, 4, 4, 5)
val distinct = withDuplicates.distinct()  // [1, 2, 3, 4, 5]
println("Distinct: $distinct")
    
// Distinct by - remove duplicates by property
data class Person(val name: String, val age: Int)
val people = listOf(
    Person("Alice", 25),
    Person("Bob", 30),
    Person("Alice", 28)  // Same name, different age
)
val distinctNames = people.distinctBy { it.name }
println("Distinct names: $distinctNames")  // [Alice(25), Bob(30)]

Aggregation and Reduction

val numbers = listOf(1, 2, 3, 4, 5)
    
// Basic aggregation
println("Sum: ${numbers.sum()}")              // 15
println("Average: ${numbers.average()}")      // 3.0
println("Min: ${numbers.minOrNull()}")        // 1
println("Max: ${numbers.maxOrNull()}")        // 5
println("Count: ${numbers.count()}")          // 5
    
// Count with condition
val evenCount = numbers.count { it % 2 == 0 }  // 2
    
// Reduce - combine elements into a single result
val sumReduce = numbers.reduce { acc, value -> acc + value }  // 15
val productReduce = numbers.reduce { acc, value -> acc * value }  // 120
    
// Fold - reduce with initial value
val sumFold = numbers.fold(10) { acc, value -> acc + value }  // 25 (10 + sum)
val productFold = numbers.fold(2) { acc, value -> acc * value }  // 240 (2 * product)
    
println("Sum reduce: $sumReduce")
println("Sum fold: $sumFold")
    
// Running fold - get intermediate results
val runningSum = numbers.runningFold(0) { acc, value -> acc + value }
println("Running sum: $runningSum")  // [0, 1, 3, 6, 10, 15]
    
// Group by - group elements by key
val words = listOf("apple", "banana", "cherry", "date", "elderberry")
val byLength = words.groupBy { it.length }
println("Group by length: $byLength")  // {5=[apple], 6=[banana, cherry], 4=[date], 10=[elderberry]}
    
// Group by with transformation
val byLengthFirstChar = words.groupBy({ it.length }, { it.first() })
println("Group by length, first char: $byLengthFirstChar")

Utility and Search Operations

val numbers = listOf(5, 2, 8, 1, 9, 3, 7, 4, 6)
    
// Sorting
val sorted = numbers.sorted()                    // [1, 2, 3, 4, 5, 6, 7, 8, 9]
val sortedDesc = numbers.sortedDescending()      // [9, 8, 7, 6, 5, 4, 3, 2, 1]
val sortedBy = numbers.sortedBy { it % 3 }       // Sort by remainder when divided by 3
    
println("Sorted: $sorted")
println("Sorted desc: $sortedDesc")
    
// Reversing
val reversed = numbers.reversed()                // [6, 4, 7, 3, 9, 1, 8, 2, 5]
    
// Shuffling
val shuffled = numbers.shuffled()                // Random order
    
// Searching
val indexOf8 = numbers.indexOf(8)               // 2
val lastIndexOf = numbers.lastIndexOf(3)         // 5
val binarySearch = sorted.binarySearch(5)       // 4 (requires sorted list)
    
println("Index of 8: $indexOf8")
println("Binary search for 5: $binarySearch")
    
// Finding elements
val firstEven = numbers.find { it % 2 == 0 }    // 2
val lastEven = numbers.findLast { it % 2 == 0 } // 6
val firstOrNull = numbers.firstOrNull { it > 10 } // null
    
// Checking conditions
val allPositive = numbers.all { it > 0 }        // true
val anyEven = numbers.any { it % 2 == 0 }       // true
val noneNegative = numbers.none { it < 0 }      // true
    
println("All positive: $allPositive")
println("Any even: $anyEven")
    
// Chunking - split into chunks
val chunks = numbers.chunked(3)  // [[5, 2, 8], [1, 9, 3], [7, 4, 6]]
println("Chunks: $chunks")
    
// Windowing - sliding window
val windows = numbers.windowed(3)  // [[5, 2, 8], [2, 8, 1], [8, 1, 9], ...]
println("Windows: $windows")

Common Pitfalls

  • Using reduce on empty collections (throws exception)
  • Forgetting that collection operations create new collections
  • Using inefficient operations on large collections
  • Not using sequence for chained operations on large data

Classes and Objects in Kotlin

Classes in Kotlin are blueprints for creating objects. Kotlin provides concise syntax for classes with properties, constructors, and various class modifiers.

Basic Class Definition

// Simple class
class Person {
    var name: String = ""
    var age: Int = 0
    
    fun speak() {
        println("Hello, my name is $name and I'm $age years old")
    }
}
    
// Primary constructor
class Car(val brand: String, val model: String, var year: Int) {
    fun displayInfo() {
        println("$brand $model ($year)")
    }
}
    
// Class with init block
class Rectangle(val width: Int, val height: Int) {
    val area: Int
    
    init {
        area = width * height
        println("Rectangle created: ${width}x$height, area: $area")
    }
}
    
fun main() {
    // Creating objects
    val person = Person()
    person.name = "Alice"
    person.age = 25
    person.speak()
    
    val car = Car("Toyota", "Camry", 2022)
    car.displayInfo()
    
    val rect = Rectangle(5, 3)
    println("Area: ${rect.area}")
}

Properties and Accessors

class BankAccount {
    // Property with custom getter
    var balance: Double = 0.0
        get() {
            println("Balance accessed: $field")
            return field
        }
        set(value) {
            require(value >= 0) { "Balance cannot be negative" }
            field = value
            println("Balance updated: $field")
        }
    
    // Read-only property with custom getter
    val isOverdrawn: Boolean
        get() = balance < 0
    
    // Property with backing field
    private var _transactions = mutableListOf<String>()
    val transactions: List<String>
        get() = _transactions.toList()  // Return read-only copy
    
    fun deposit(amount: Double) {
        balance += amount
        _transactions.add("Deposited: $$amount")
    }
    
    fun withdraw(amount: Double) {
        balance -= amount
        _transactions.add("Withdrew: $$amount")
    }
}
    
fun main() {
    val account = BankAccount()
    account.deposit(100.0)
    account.withdraw(50.0)
    println("Balance: ${account.balance}")
    println("Transactions: ${account.transactions}")
}

Data Classes and Destructuring

// Data class - automatically generates equals, hashCode, toString, copy
data class User(val id: Int, val name: String, val email: String)
    
// Data class with default values
data class Product(
    val id: Int,
    val name: String,
    val price: Double,
    val category: String = "General"
)
    
fun main() {
    val user1 = User(1, "Alice", "alice@example.com")
    val user2 = User(1, "Alice", "alice@example.com")
    
    // Auto-generated equals
    println("Users equal: ${user1 == user2}")  // true
    
    // Auto-generated toString
    println("User: $user1")  // User(id=1, name=Alice, email=alice@example.com)
    
    // Copy with modification
    val user3 = user1.copy(id = 2, name = "Bob")
    println("Copied user: $user3")
    
    // Destructuring declarations
    val (id, name, email) = user1
    println("ID: $id, Name: $name, Email: $email")
    
    // Component functions (generated for data classes)
    println("Component 1: ${user1.component1()}")  // id
    println("Component 2: ${user1.component2()}")  // name
    
    val product = Product(1, "Laptop", 999.99, "Electronics")
    println("Product: $product")
}

Companion Objects and Static Members

class MathUtils {
    companion object {
        // Like static members in Java
        const val PI = 3.14159
        
        fun square(x: Int): Int = x * x
        
        fun factorial(n: Int): Long {
            return if (n <= 1) 1 else n * factorial(n - 1)
        }
    }
}
    
// Object declaration (singleton)
object Logger {
    private var logLevel = "INFO"
    
    fun setLevel(level: String) {
        logLevel = level
    }
    
    fun info(message: String) {
        println("[$logLevel] $message")
    }
    
    fun error(message: String) {
        println("[$logLevel] ERROR: $message")
    }
}
    
fun main() {
    // Using companion object members
    println("PI: ${MathUtils.PI}")
    println("Square of 5: ${MathUtils.square(5)}")
    println("Factorial of 5: ${MathUtils.factorial(5)}")
    
    // Using object (singleton)
    Logger.info("Application started")
    Logger.error("Something went wrong")
    Logger.setLevel("DEBUG")
    Logger.info("Debug message")
}

Common Pitfalls

  • Forgetting to initialize properties in classes
  • Using var when val would be sufficient
  • Not using data classes when you need value semantics
  • Creating unnecessary backing fields for computed properties

Inheritance in Kotlin

Kotlin supports single inheritance with all classes implicitly inheriting from Any. Classes are final by default and must be explicitly marked as open to be inherited.

Basic Inheritance

// Base class must be marked open
open class Animal(val name: String, val age: Int) {
    
    // Methods must be marked open to be overridden
    open fun makeSound() {
        println("$name makes a generic sound")
    }
    
    open fun eat() {
        println("$name is eating")
    }
    
    // Final method cannot be overridden
    fun sleep() {
        println("$name is sleeping")
    }
}
    
// Derived class
class Dog(name: String, age: Int, val breed: String) : Animal(name, age) {
    
    // Override base class method
    override fun makeSound() {
        println("$name barks: Woof! Woof!")
    }
    
    // New method in derived class
    fun fetch() {
        println("$name is fetching the ball")
    }
}
    
// Another derived class
class Cat(name: String, age: Int) : Animal(name, age) {
    override fun makeSound() {
        println("$name meows: Meow! Meow!")
    }
    
    fun climb() {
        println("$name is climbing a tree")
    }
}
    
fun main() {
    val dog = Dog("Buddy", 3, "Golden Retriever")
    val cat = Cat("Whiskers", 2)
    
    dog.makeSound()  // Buddy barks: Woof! Woof!
    dog.eat()        // Buddy is eating
    dog.fetch()      // Buddy is fetching the ball
    
    cat.makeSound()  // Whiskers meows: Meow! Meow!
    cat.climb()      // Whiskers is climbing a tree
}

Abstract Classes and Interfaces

// Abstract class (cannot be instantiated)
abstract class Shape(val name: String) {
    // Abstract property (must be implemented by subclasses)
    abstract val area: Double
    
    // Abstract method (must be implemented by subclasses)
    abstract fun draw()
    
    // Concrete method
    fun displayInfo() {
        println("Shape: $name, Area: $area")
    }
}
    
// Interface
interface Drawable {
    fun draw()  // Abstract method
    
    // Default implementation (Kotlin 1.4+)
    fun describe() {
        println("This is a drawable object")
    }
}
    
interface Resizable {
    fun resize(factor: Double)
}
    
// Implementing abstract class and interfaces
class Circle(val radius: Double) : Shape("Circle"), Drawable, Resizable {
    override val area: Double
        get() = Math.PI * radius * radius
    
    override fun draw() {
        println("Drawing a circle with radius $radius")
    }
    
    override fun resize(factor: Double) {
        println("Resizing circle by factor $factor")
    }
    
    // Can override default interface method
    override fun describe() {
        println("This is a circle with radius $radius")
    }
}
    
class Rectangle(val width: Double, val height: Double) : Shape("Rectangle"), Drawable {
    override val area: Double
        get() = width * height
    
    override fun draw() {
        println("Drawing a rectangle ${width}x$height")
    }
}
    
fun main() {
    val circle = Circle(5.0)
    val rectangle = Rectangle(4.0, 3.0)
    
    circle.displayInfo()
    circle.draw()
    circle.describe()
    circle.resize(2.0)
    
    rectangle.displayInfo()
    rectangle.draw()
}

Property and Constructor Inheritance

open class Vehicle(val brand: String, val maxSpeed: Int) {
    open val description: String
        get() = "Vehicle: $brand, Max speed: $maxSpeed km/h"
    
    open fun start() {
        println("Vehicle starting...")
    }
}
    
// Calling superclass constructor
class Car(brand: String, maxSpeed: Int, val fuelType: String) 
    : Vehicle(brand, maxSpeed) {
    
    // Overriding property
    override val description: String
        get() = "Car: $brand, Max speed: $maxSpeed km/h, Fuel: $fuelType"
    
    // Overriding method
    override fun start() {
        super.start()  // Call superclass method
        println("Car engine started with $fuelType")
    }
    
    // New method
    fun drive() {
        println("Driving $brand car")
    }
}
    
// Secondary constructor with super call
class Motorcycle : Vehicle {
    val engineSize: Int
    
    constructor(brand: String, maxSpeed: Int, engineSize: Int) 
        : super(brand, maxSpeed) {
        this.engineSize = engineSize
    }
    
    override val description: String
        get() = "Motorcycle: $brand, Engine: ${engineSize}cc"
}
    
fun main() {
    val car = Car("Toyota", 200, "Gasoline")
    val motorcycle = Motorcycle("Yamaha", 180, 600)
    
    println(car.description)
    car.start()
    car.drive()
    
    println(motorcycle.description)
    motorcycle.start()
}

Common Pitfalls

  • Forgetting to mark classes and methods as open for inheritance
  • Not calling superclass methods when overriding
  • Using inheritance when composition would be better
  • Deep inheritance hierarchies that are hard to understand

Polymorphism in Kotlin

Polymorphism allows objects of different types to be treated as objects of a common type. Kotlin supports both compile-time (function overloading) and runtime (inheritance) polymorphism.

Runtime Polymorphism with Inheritance

open class Animal(val name: String) {
    open fun makeSound() {
        println("$name makes a sound")
    }
}
    
class Dog(name: String) : Animal(name) {
    override fun makeSound() {
        println("$name barks")
    }
    
    fun fetch() {
        println("$name is fetching")
    }
}
    
class Cat(name: String) : Animal(name) {
    override fun makeSound() {
        println("$name meows")
    }
    
    fun climb() {
        println("$name is climbing")
    }
}
    
class Bird(name: String) : Animal(name) {
    override fun makeSound() {
        println("$name chirps")
    }
    
    fun fly() {
        println("$name is flying")
    }
}
    
fun main() {
    val animals: List<Animal> = listOf(
        Dog("Buddy"),
        Cat("Whiskers"), 
        Bird("Tweety"),
        Dog("Rex")
    )
    
    // Runtime polymorphism - calls appropriate derived class method
    for (animal in animals) {
        animal.makeSound()
        
        // Type checking and smart casting
        when (animal) {
            is Dog -> animal.fetch()   // Smart cast to Dog
            is Cat -> animal.climb()   // Smart cast to Cat
            is Bird -> animal.fly()    // Smart cast to Bird
        }
    }
}

Compile-time Polymorphism (Overloading)

class Calculator {
    // Function overloading - same name, different parameters
    fun add(a: Int, b: Int): Int = a + b
    fun add(a: Double, b: Double): Double = a + b
    fun add(a: Int, b: Int, c: Int): Int = a + b + c
    fun add(vararg numbers: Int): Int = numbers.sum()
    
    // Different parameter types
    fun multiply(a: Int, b: Int): Int = a * b
    fun multiply(a: Double, b: Double): Double = a * b
    
    // Different return types with same parameters (not allowed)
    // fun process(x: Int): Int = x * 2
    // fun process(x: Int): String = "Number: $x"  // Error: conflicts
    
    // Can have different return types if parameters differ
    fun process(x: Int): Int = x * 2
    fun process(x: String): String = "String: $x"
}
    
// Extension function overloading
fun String.repeat(times: Int): String = this.repeat(times)
fun String.repeat(times: Int, separator: String): String = 
    this.split("").joinToString(separator).repeat(times)
    
fun main() {
    val calc = Calculator()
    
    println(calc.add(2, 3))           // 5
    println(calc.add(2.5, 3.5))       // 6.0
    println(calc.add(1, 2, 3))        // 6
    println(calc.add(1, 2, 3, 4, 5))  // 15
    
    println(calc.multiply(3, 4))      // 12
    println(calc.multiply(2.5, 3.0))  // 7.5
    
    println(calc.process(5))          // 10
    println(calc.process("hello"))    // "String: hello"
    
    println("Hi".repeat(3))           // "HiHiHi"
    println("Hi".repeat(3, " "))      // "H i H i H i"
}

Operator Overloading

data class Vector(val x: Int, val y: Int) {
    // Overload plus operator
    operator fun plus(other: Vector): Vector {
        return Vector(x + other.x, y + other.y)
    }
    
    // Overload minus operator
    operator fun minus(other: Vector): Vector {
        return Vector(x - other.x, y - other.y)
    }
    
    // Overload times operator (scalar multiplication)
    operator fun times(scalar: Int): Vector {
        return Vector(x * scalar, y * scalar)
    }
    
    // Overload unary minus
    operator fun unaryMinus(): Vector {
        return Vector(-x, -y)
    }
    
    // Overload get operator (for indexing)
    operator fun get(index: Int): Int {
        return when (index) {
            0 -> x
            1 -> y
            else -> throw IndexOutOfBoundsException("Invalid index: $index")
        }
    }
    
    // Overload contains operator
    operator fun contains(value: Int): Boolean {
        return value == x || value == y
    }
}
    
// Extension function for operator overloading
operator fun Vector.rangeTo(other: Vector): List<Vector> {
    return listOf(this, other)
}
    
fun main() {
    val v1 = Vector(2, 3)
    val v2 = Vector(1, 4)
    
    // Using overloaded operators
    val sum = v1 + v2        // Vector(3, 7)
    val diff = v1 - v2       // Vector(1, -1)
    val scaled = v1 * 3      // Vector(6, 9)
    val negative = -v1       // Vector(-2, -3)
    
    println("Sum: $sum")
    println("Difference: $diff")
    println("Scaled: $scaled")
    println("Negative: $negative")
    
    // Using get operator
    println("v1[0] = ${v1[0]}, v1[1] = ${v1[1]}")
    
    // Using contains operator
    println("3 in v1: ${3 in v1}")  // true
    println("5 in v1: ${5 in v1}")  // false
    
    // Using rangeTo operator
    val range = v1..v2
    println("Range: $range")
}

Type Checking and Casting

open class Employee(val name: String, val salary: Double)
    
class Manager(name: String, salary: Double, val department: String) 
    : Employee(name, salary)
    
class Developer(name: String, salary: Double, val language: String) 
    : Employee(name, salary)
    
fun processEmployee(emp: Employee) {
    // Type checking with is
    if (emp is Manager) {
        println("Manager: ${emp.name}, Department: ${emp.department}")
    } else if (emp is Developer) {
        println("Developer: ${emp.name}, Language: ${emp.language}")
    }
    
    // Smart casting - emp is automatically cast in the scope
    when (emp) {
        is Manager -> {
            println("Managing department: ${emp.department}")  // Smart cast to Manager
        }
        is Developer -> {
            println("Programming in: ${emp.language}")  // Smart cast to Developer
        }
        else -> println("Regular employee: ${emp.name}")
    }
}
    
fun main() {
    val employees = listOf(
        Manager("Alice", 75000.0, "Engineering"),
        Developer("Bob", 65000.0, "Kotlin"),
        Employee("Charlie", 50000.0)
    )
    
    for (emp in employees) {
        processEmployee(emp)
    }
    
    // Explicit casting
    val employee: Employee = Manager("Diana", 80000.0, "Sales")
    
    // Safe cast (returns null if cast fails)
    val manager = employee as? Manager
    println("Manager: $manager")
    
    // Unsafe cast (throws exception if cast fails)
    try {
        val developer = employee as Developer  // ClassCastException
    } catch (e: ClassCastException) {
        println("Cast failed: ${e.message}")
    }
}

Common Pitfalls

  • Overusing operator overloading making code unclear
  • Using unsafe casts without proper type checking
  • Forgetting that function overloading is resolved at compile time
  • Not leveraging smart casting in when expressions

Advanced Kotlin Concepts

Advanced Kotlin features provide powerful tools for writing concise, safe, and expressive code, including delegation, coroutines, sealed classes, and more.

Delegation Patterns

// Interface
interface SoundMaker {
    fun makeSound()
}
    
// Implementation
class LoudSoundMaker : SoundMaker {
    override fun makeSound() {
        println("LOUD SOUND!")
    }
}
    
// Class delegation - implementing interface by delegating to another object
class Animal(val name: String, soundMaker: SoundMaker) : SoundMaker by soundMaker {
    fun eat() {
        println("$name is eating")
    }
}
    
// Property delegation
class Example {
    // Lazy delegation - computed on first access
    val lazyValue: String by lazy {
        println("Computing lazy value")
        "Hello"
    }
    
    // Observable property
    var observedValue: String by Delegates.observable("initial") { prop, old, new ->
        println("$old -> $new")
    }
    
    // Vetoable property - can reject changes
    var vetoableValue: Int by Delegates.vetoable(0) { _, old, new ->
        new >= 0  // Only allow non-negative values
    }
}
    
// Custom delegation
class RangeDelegate(private var value: Int, private val range: IntRange) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return value
    }
    
    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: Int) {
        if (newValue in range) {
            value = newValue
        } else {
            throw IllegalArgumentException("Value $newValue not in range $range")
        }
    }
}
    
class Person {
    var age: Int by RangeDelegate(0, 0..150)
}
    
fun main() {
    // Class delegation
    val soundMaker = LoudSoundMaker()
    val animal = Animal("Lion", soundMaker)
    animal.makeSound()  // Delegated to soundMaker
    animal.eat()
    
    // Property delegation
    val example = Example()
    println(example.lazyValue)  // "Computing lazy value" then "Hello"
    println(example.lazyValue)  // "Hello" (already computed)
    
    example.observedValue = "first"  // "initial -> first"
    example.observedValue = "second" // "first -> second"
    
    example.vetoableValue = 10  // Allowed
    example.vetoableValue = -5  // Rejected (stays 10)
    
    // Custom delegation
    val person = Person()
    person.age = 25  // Allowed
    try {
        person.age = 200  // Throws IllegalArgumentException
    } catch (e: IllegalArgumentException) {
        println(e.message)
    }
}

Sealed Classes and Interfaces

// Sealed class - restricted class hierarchies
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val message: String) : Result<Nothing>()
    object Loading : Result<Nothing>()
}
    
// Sealed interface (Kotlin 1.5+)
sealed interface Response
class SuccessResponse(val data: String) : Response
class ErrorResponse(val error: String) : Response
    
fun handleResult(result: Result<String>) {
    // Exhaustive when expression (no else needed)
    when (result) {
        is Result.Success -> println("Success: ${result.data}")
        is Result.Error -> println("Error: ${result.message}")
        Result.Loading -> println("Loading...")
    }
}
    
fun processResponse(response: Response) {
    when (response) {
        is SuccessResponse -> println("Success: ${response.data}")
        is ErrorResponse -> println("Error: ${response.error}")
    }
}
    
// Using sealed classes with generics
sealed class NetworkState
object Idle : NetworkState()
object Loading : NetworkState()
data class Loaded<T>(val data: T) : NetworkState()
data class Error(val exception: Throwable) : NetworkState()
    
fun <T> handleNetworkState(state: NetworkState, onSuccess: (T) -> Unit) {
    when (state) {
        is Idle -> println("Network is idle")
        is Loading -> println("Loading data...")
        is Loaded -> onSuccess(state.data as T)
        is Error -> println("Error: ${state.exception.message}")
    }
}
    
fun main() {
    val success = Result.Success("Data loaded")
    val error = Result.Error("Failed to load")
    
    handleResult(success)  // Success: Data loaded
    handleResult(error)    // Error: Failed to load
    handleResult(Result.Loading)  // Loading...
    
    val networkState = Loaded("Network data")
    handleNetworkState<String>(networkState) { data ->
        println("Received: $data")
    }
}

Inline Classes and Value Classes

// Inline class (Kotlin 1.3+) - type-safe wrappers with no runtime overhead
@JvmInline
value class Password(val value: String) {
    init {
        require(value.length >= 8) { "Password must be at least 8 characters" }
    }
}
    
@JvmInline  
value class Email(val value: String) {
    init {
        require(value.contains("@")) { "Invalid email format" }
    }
}
    
// Value class (Kotlin 1.5+) - more flexible than inline classes
value class UserName(val value: String) {
    fun display(): String = "User: $value"
}
    
fun createAccount(email: Email, password: Password, username: UserName) {
    println("Creating account:")
    println("Email: ${email.value}")
    println("Username: ${username.display()}")
    // Password is secure, don't print it
}
    
// Type-safe IDs
@JvmInline
value class UserId(val value: Int)
    
@JvmInline
value class ProductId(val value: Int)
    
fun getUser(userId: UserId): String = "User ${userId.value}"
fun getProduct(productId: ProductId): String = "Product ${productId.value}"
    
fun main() {
    try {
        val email = Email("user@example.com")
        val password = Password("secure123")
        val username = UserName("john_doe")
        
        createAccount(email, password, username)
        
        // Type safety - cannot mix UserId and ProductId
        val userId = UserId(123)
        val productId = ProductId(456)
        
        println(getUser(userId))        // OK
        println(getProduct(productId))  // OK
        // println(getUser(productId))  // Compilation error!
        
    } catch (e: IllegalArgumentException) {
        println("Validation error: ${e.message}")
    }
}

Coroutines Basics

import kotlinx.coroutines.*
    
// Suspending functions
suspend fun fetchUserData(userId: Int): String {
    delay(1000)  // Simulate network call
    return "User data for $userId"
}
    
suspend fun fetchUserProfile(userId: Int): String {
    delay(800)  // Simulate network call
    return "User profile for $userId"
}
    
// Coroutine scope and async/await
fun main() = runBlocking {
    println("Starting coroutines...")
    
    // Sequential execution
    val startTime = System.currentTimeMillis()
    val userData = fetchUserData(1)
    val userProfile = fetchUserProfile(1)
    val sequentialTime = System.currentTimeMillis() - startTime
    println("Sequential time: ${sequentialTime}ms")
    
    // Concurrent execution
    val concurrentStartTime = System.currentTimeMillis()
    val userDataDeferred = async { fetchUserData(2) }
    val userProfileDeferred = async { fetchUserProfile(2) }
    val userData2 = userDataDeferred.await()
    val userProfile2 = userProfileDeferred.await()
    val concurrentTime = System.currentTimeMillis() - concurrentStartTime
    println("Concurrent time: ${concurrentTime}ms")
    
    println("User data: $userData2")
    println("User profile: $userProfile2")
    
    // Coroutine context and dispatchers
    withContext(Dispatchers.IO) {
        println("Running on IO dispatcher: ${Thread.currentThread().name}")
    }
    
    withContext(Dispatchers.Default) {
        println("Running on Default dispatcher: ${Thread.currentThread().name}")
    }
    
    // Structured concurrency
    val result = coroutineScope {
        val job1 = async { fetchUserData(3) }
        val job2 = async { fetchUserProfile(3) }
        "${job1.await()} and ${job2.await()}"
    }
    println("Structured result: $result")
}

Common Pitfalls

  • Using global CoroutineScope instead of structured concurrency
  • Not handling exceptions in coroutines properly
  • Using inline classes for types that need boxing anyway
  • Forgetting to mark sealed class subclasses in the same file