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
-
Using IntelliJ IDEA
Install IntelliJ IDEA Community or Ultimate edition, which comes with built-in Kotlin support. Download from jetbrains.com/idea. -
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
-
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
-
Create a file named
hello.kt
with the following content:fun main() { println("Hello, World!") }
-
Compile and run the program:
kotlinc hello.kt -include-runtime -d hello.jar java -jar hello.jar
-
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
equals2
, not2.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
vsushr
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 meantisBlank
- 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) withfold
(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
) throwsUnsupportedOperationException
- Using
get
without checking bounds can throwIndexOutOfBoundsException
- 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
whenval
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
returnsUnit
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) withuntil
(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
whenfor
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
whencontinue
is more appropriate - Overusing labels makes code harder to read
break
andcontinue
only work with loops, notforEach
- 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 needMutableList
and vice versa - Choosing the wrong collection type for performance requirements
- Modifying collections during iteration
- Forgetting that map keys must have proper
equals
andhashCode
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
whenval
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