Basics
Welcome to this comprehensive Kotlin crash course! I’ve designed this guide to cover the most essential 85% of Kotlin features you’ll use daily, while giving you the foundation to explore the remaining 15% on your own.
Introduction to Kotlin
Kotlin is a modern, statically-typed programming language that runs on the Java Virtual Machine (JVM). Created by JetBrains in 2011, it has gained tremendous popularity, especially after Google announced it as an official language for Android development in 2017.
Why Kotlin?
- Concise syntax: Significantly less boilerplate code than Java
- Null safety: Built-in protection against null pointer exceptions
- Java interoperability: 100% compatible with existing Java code
- Modern features: Combines object-oriented and functional programming
- Industry adoption: Used by Google, Amazon, Netflix, and many others
With these powerful features in mind, let’s get your environment set up so you can start coding right away.
Setting Up Your Environment
Prerequisites:
- Basic programming knowledge
- Computer with internet connection
Installation:
Install JDK (Java Development Kit):
- Download JDK 17 or 21 from Oracle or use OpenJDK
Install IntelliJ IDEA:
# Steps: 1. Go to https://www.jetbrains.com/idea/download/ 2. Download Community Edition (free) 3. Run the installer and follow instructions
Create your first Kotlin project:
# In IntelliJ IDEA: 1. Select File > New > Project 2. Choose "Kotlin" and "JVM | IDEA" 3. Name your project and select your installed JDK 4. Click "Create"
Now that your environment is ready, let’s write your first Kotlin program to get a feel for the language.
Your First Kotlin Program
Create a new Kotlin file in your project and add:
fun main() {
// This is a comment
println("Hello, Kotlin!") // Prints to console
}
// Run this program by clicking the green "play" button in IntelliJ
// Output: Hello, Kotlin!
This simple program demonstrates:
fun
keyword to declare functionsmain()
as the entry pointprintln()
to output text- Comments with
//
With this basic foundation, let’s explore how Kotlin handles variables and data types, which form the building blocks of any program.
Variables and Data Types
In Kotlin, everything starts with declaring variables. Unlike some languages, Kotlin makes you think about whether your data can change over time:
// Variables are declared using var (mutable) or val (immutable)
val name: String = "Kotlin" // Immutable - cannot be changed
var age: Int = 12 // Mutable - can be changed
age = 13 // This works
// name = "Java" // This would cause an error
// Type inference (compiler determines the type)
val language = "Kotlin" // Type String is inferred
var version = 1.7 // Type Double is inferred
// Basic data types
val count: Int = 42 // Integer numbers
val temperature: Double = 23.5 // Floating-point numbers
val isActive: Boolean = true // true or false
val letter: Char = 'K' // Single character
val message: String = "Hello" // Text
One of Kotlin’s major innovations is how it handles null values, which addresses a common source of errors in many programming languages.
Null Safety
Kotlin’s type system is designed to eliminate the danger of null references, often called “The Billion Dollar Mistake” in other languages:
// Non-nullable type (cannot be null)
val text: String = "Hello"
// text = null // Compilation error!
// Nullable type (can be null)
val nullableText: String? = null // Notice the question mark
// Safe call operator (?.)
// Returns null if nullableText is null instead of throwing an exception
val length = nullableText?.length // Result: null
// Elvis operator (?:) - provides a default value
val nonNullLength = nullableText?.length ?: 0 // Result: 0
// Not-null assertion (!!) - use cautiously!
// Will throw NullPointerException if nullableText is null
// val forcedLength = nullableText!!.length // Would throw exception
To better understand how Kotlin handles null values, let’s visualize the flow:
graph TD A[Variable Declaration] --> B{Nullable?} B -->|No| C[Type: String] B -->|Yes| D[Type: String?] D --> E{Using the variable} E -->|Safe Access| F["Use ?. operator
Returns null if variable is null"] E -->|Default Value| G["Use ?: operator
Provides default if null"] E -->|Forced Unwrap| H["Use !! operator
Throws NPE if null"] style A fill:#f5f5f5,stroke:#333 style B fill:#f5f5f5,stroke:#333 style C fill:#d4edda,stroke:#333 style D fill:#f8d7da,stroke:#333 style E fill:#f5f5f5,stroke:#333 style F fill:#d1ecf1,stroke:#333 style G fill:#d1ecf1,stroke:#333 style H fill:#f8d7da,stroke:#333
With variables and null safety covered, we can now move on to controlling the flow of your programs.
Control Flow
Programs need to make decisions and repeat actions. Kotlin provides elegant ways to control your program’s flow.
If Expressions
Unlike many languages, Kotlin’s if
is an expression that can return a value:
// Basic if-else
val age = 20
if (age >= 18) {
println("Adult")
} else {
println("Minor")
}
// If as an expression (returns a value)
val status = if (age >= 18) "Adult" else "Minor"
println(status) // Output: Adult
// Multi-branch if
val score = 85
val grade = if (score >= 90) {
"A"
} else if (score >= 80) {
"B"
} else if (score >= 70) {
"C"
} else {
"F"
}
println(grade) // Output: B
When Expression
For more complex branching, Kotlin replaces the traditional switch statement with the more powerful when
expression:
val day = 3
val dayName = when (day) {
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6 -> "Saturday"
7 -> "Sunday"
else -> "Invalid day"
}
println(dayName) // Output: Wednesday
// When with ranges and multiple conditions
val number = 15
val description = when (number) {
in 1..10 -> "Between 1 and 10"
in 11..20 -> "Between 11 and 20"
30, 40, 50 -> "Either 30, 40, or 50"
else -> "Some other number"
}
println(description) // Output: Between 11 and 20
Decision-making only gets us so far. Let’s look at how Kotlin handles repetition with loops.
Loops
For Loop
Kotlin’s for loop is designed to iterate over anything that provides an iterator:
// Loop through a range
for (i in 1..5) {
println(i) // Prints 1, 2, 3, 4, 5
}
// Loop with a step
for (i in 1..10 step 2) {
println(i) // Prints 1, 3, 5, 7, 9
}
// Loop in reverse
for (i in 5 downTo 1) {
println(i) // Prints 5, 4, 3, 2, 1
}
// Loop through a collection
val fruits = listOf("Apple", "Banana", "Cherry")
for (fruit in fruits) {
println(fruit)
}
// Loop with index
for ((index, fruit) in fruits.withIndex()) {
println("$index: $fruit") // Prints "0: Apple", "1: Banana", etc.
}
While Loop
When you need to repeat actions based on a condition:
// Standard while loop
var counter = 0
while (counter < 5) {
println(counter)
counter++
}
// Do-while loop (executes at least once)
var number = 10
do {
println(number)
number--
} while (number > 5)
Now that we can control program flow, let’s look at how Kotlin handles text manipulation, which is essential for almost any application.
String Manipulation
Kotlin provides powerful tools for working with text:
// String templates
val name = "Kotlin"
val version = 1.7
println("I'm learning $name $version") // Output: I'm learning Kotlin 1.7
// Complex expressions in templates
println("The length of $name is ${name.length}") // Output: The length of Kotlin is 6
// Multiline strings (preserves formatting)
val poem = """
Roses are red,
Violets are blue,
Kotlin is awesome,
And so are you!
""".trimIndent()
println(poem)
These string features are quite useful, but to build truly reusable code, we need functions.
Functions
Functions allow you to organize code into reusable blocks. Kotlin makes function declaration clean and flexible:
// Simple function
fun greet() {
println("Hello!")
}
greet() // Output: Hello!
// Function with parameters
fun greetPerson(name: String) {
println("Hello, $name!")
}
greetPerson("Alice") // Output: Hello, Alice!
// Function with return value
fun sum(a: Int, b: Int): Int {
return a + b
}
println(sum(3, 4)) // Output: 7
// Single-expression function (simplified syntax)
fun multiply(a: Int, b: Int): Int = a * b
println(multiply(5, 6)) // Output: 30
// Function with default parameter values
fun greetWithDefault(name: String = "Guest") {
println("Hello, $name!")
}
greetWithDefault() // Output: Hello, Guest!
greetWithDefault("Bob") // Output: Hello, Bob!
// Named arguments
fun createUser(name: String, age: Int, isAdmin: Boolean) {
println("User: $name, Age: $age, Admin: $isAdmin")
}
createUser(name = "Alice", age = 30, isAdmin = true)
createUser(age = 25, isAdmin = false, name = "Bob") // Order can be changed
Functions are essential, but we also need ways to organize data. Let’s explore Kotlin’s collection types.
Collections
Collections help us work with groups of related data. Kotlin provides rich APIs for different collection types.
Lists
// Immutable list (can't add/remove elements)
val fruits = listOf("Apple", "Banana", "Cherry")
println(fruits[0]) // Output: Apple
// Mutable list (can add/remove elements)
val vegetables = mutableListOf("Carrot", "Potato")
vegetables.add("Broccoli")
println(vegetables) // Output: [Carrot, Potato, Broccoli]
// List operations
println(fruits.size) // Output: 3
println(fruits.contains("Apple")) // Output: true
println(fruits.indexOf("Banana")) // Output: 1
Maps (Key-Value Pairs)
// Immutable map
val ages = mapOf("Alice" to 30, "Bob" to 25, "Charlie" to 35)
println(ages["Alice"]) // Output: 30
// Mutable map
val scores = mutableMapOf("Math" to 90, "English" to 85)
scores["Science"] = 95
println(scores) // Output: {Math=90, English=85, Science=95}
// Map operations
println(ages.keys) // Output: [Alice, Bob, Charlie]
println(ages.values) // Output: [30, 25, 35]
for ((name, age) in ages) {
println("$name is $age years old")
}
Sets (Unique Elements)
// Immutable set
val uniqueNumbers = setOf(1, 2, 3, 3, 4) // Duplicates are removed
println(uniqueNumbers) // Output: [1, 2, 3, 4]
// Mutable set
val tags = mutableSetOf("Kotlin", "Programming")
tags.add("JVM")
tags.add("Kotlin") // Duplicate is ignored
println(tags) // Output: [Kotlin, Programming, JVM]
These collections become even more powerful when combined with Kotlin’s functional programming capabilities.
Functional Programming
Kotlin embraces functional programming, allowing you to write more expressive and concise code:
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// Filter - keep elements that match a condition
val evenNumbers = numbers.filter { it % 2 == 0 }
println(evenNumbers) // Output: [2, 4, 6, 8, 10]
// Map - transform each element
val squared = numbers.map { it * it }
println(squared) // Output: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
// Find - get the first element matching a condition
val firstEven = numbers.find { it % 2 == 0 }
println(firstEven) // Output: 2
// Any/All/None - check if elements match a condition
println(numbers.any { it > 5 }) // Output: true
println(numbers.all { it < 20 }) // Output: true
println(numbers.none { it < 0 }) // Output: true
// Reduce - combine elements
val sum = numbers.reduce { acc, num -> acc + num }
println(sum) // Output: 55 (1+2+3+...+10)
// Grouping
val grouped = numbers.groupBy { if (it % 2 == 0) "even" else "odd" }
println(grouped) // Output: {odd=[1, 3, 5, 7, 9], even=[2, 4, 6, 8, 10]}
These functional approaches help you write more declarative code, focusing on what you want to accomplish rather than how to do it. Moving from individual functions to more complex structures, let’s explore how Kotlin handles object-oriented programming.
Classes and Objects
Kotlin provides a modern take on object-oriented programming, making classes concise yet powerful.
Basic Class
// Class with primary constructor
class Person(val name: String, var age: Int) {
// Property with custom getter
val isAdult: Boolean
get() = age >= 18
// Method
fun introduce() {
println("Hi, I'm $name and I'm $age years old.")
}
// Init block - runs when instance is created
init {
println("Person created: $name")
}
}
// Using the class
val alice = Person("Alice", 30)
println(alice.name) // Output: Alice
alice.age = 31 // Changing mutable property
println(alice.isAdult) // Output: true
alice.introduce() // Output: Hi, I'm Alice and I'm 31 years old.
Data Classes
For classes whose main purpose is to hold data, Kotlin provides a concise syntax:
// Data class automatically provides equals(), hashCode(), toString(), copy()
data class User(val name: String, val email: String, val age: Int)
val user1 = User("Bob", "bob@example.com", 25)
println(user1) // Output: User(name=Bob, email=bob@example.com, age=25)
// Copying with some properties changed
val user2 = user1.copy(age = 26)
println(user2) // Output: User(name=Bob, email=bob@example.com, age=26)
// Destructuring
val (name, email, age) = user1
println("$name, $email, $age") // Output: Bob, bob@example.com, 25
Inheritance
Object-oriented programming often involves inheritance. Kotlin provides a clean approach:
// Base class (open allows inheritance)
open class Animal(val name: String) {
open fun makeSound() {
println("Some generic sound")
}
}
// Derived class
class Dog(name: String, val breed: String) : Animal(name) {
override fun makeSound() {
println("Woof!")
}
fun fetch() {
println("$name is fetching...")
}
}
// Using the classes
val animal = Animal("Generic Animal")
animal.makeSound() // Output: Some generic sound
val dog = Dog("Rex", "German Shepherd")
dog.makeSound() // Output: Woof!
dog.fetch() // Output: Rex is fetching...
Here’s a visualization of Kotlin’s class inheritance:
classDiagram class Animal { -name: String +makeSound() } class Dog { +override makeSound() } class Cat { +override makeSound() } class Bird { +override makeSound() } Animal <|-- Dog Animal <|-- Cat Animal <|-- Bird style Animal fill:#d4edda,stroke:#333 style Dog fill:#d1ecf1,stroke:#333 style Cat fill:#d1ecf1,stroke:#333 style Bird fill:#d1ecf1,stroke:#333
Interfaces
For more flexible code structures, Kotlin provides interfaces:
// Interface definition
interface Swimmer {
fun swim() // Abstract method (no implementation)
fun dive() { // Method with default implementation
println("Diving...")
}
}
// Class implementing an interface
class Fish(val species: String) : Swimmer {
override fun swim() {
println("$species is swimming")
}
// We can use the default implementation of dive() or override it
}
val goldfish = Fish("Goldfish")
goldfish.swim() // Output: Goldfish is swimming
goldfish.dive() // Output: Diving...
Object Keyword (Singletons)
Kotlin provides a simple way to create singleton objects:
// Singleton object (only one instance ever exists)
object DatabaseConnection {
private var count = 0
fun connect() {
count++
println("Connected to database (connection #$count)")
}
}
DatabaseConnection.connect() // Output: Connected to database (connection #1)
DatabaseConnection.connect() // Output: Connected to database (connection #2)
Companion Objects (Static-like Members)
For class-level functionality similar to static members in other languages:
class MathUtils {
// Similar to static members in other languages
companion object {
const val PI = 3.14159
fun square(x: Double): Double {
return x * x
}
}
fun nonStaticMethod() {
println("This requires an instance")
}
}
// Access companion object members without creating an instance
println(MathUtils.PI) // Output: 3.14159
println(MathUtils.square(4.0)) // Output: 16.0
// Non-companion methods require an instance
val mathUtils = MathUtils()
mathUtils.nonStaticMethod() // Output: This requires an instance
Finally, let’s explore how Kotlin handles errors, which is crucial for writing robust applications.
Error Handling
Kotlin uses exceptions for error handling, similar to many other languages, but with some functional programming twists:
fun divide(a: Int, b: Int): Int {
if (b == 0) {
throw IllegalArgumentException("Cannot divide by zero")
}
return a / b
}
// Try-catch block
try {
val result = divide(10, 0)
println(result)
} catch (e: IllegalArgumentException) {
println("Error: ${e.message}") // Output: Error: Cannot divide by zero
} finally {
println("This always executes")
}
// Try as an expression
val result = try {
divide(10, 2)
} catch (e: Exception) {
0 // Default value in case of exception
}
println(result) // Output: 5
Now that we’ve covered all the essential components, let’s understand how a Kotlin program executes.
Program Execution Flow
The journey from Kotlin code to running program involves several steps:
graph TD A[Write Kotlin Code
.kt files] --> B[Kotlin Compiler] B --> C[Java Bytecode
.class files] C --> D[JVM
Java Virtual Machine] D --> E[Program Execution] style A fill:#d4edda,stroke:#333 style B fill:#f8d7da,stroke:#333 style C fill:#d1ecf1,stroke:#333 style D fill:#fff3cd,stroke:#333 style E fill:#d4edda,stroke:#333
This process allows Kotlin to be fully interoperable with Java and run on any platform that supports the JVM.
The Remaining 15%: Advanced Kotlin
With the core concepts mastered, you’re now ready to explore the more advanced features of Kotlin. These topics build on the foundation we’ve established and will enable you to write even more powerful applications:
Coroutines: Kotlin’s approach to asynchronous programming
// Basic coroutine example import kotlinx.coroutines.* fun main() = runBlocking { launch { delay(1000L) println("World!") } println("Hello,") // Prints "Hello," immediately, then "World!" after 1 second }
Sealed Classes: Restricted class hierarchies for more type-safe code
sealed class Result class Success(val data: String) : Result() class Error(val message: String) : Result() // When used with sealed classes is exhaustive fun handleResult(result: Result) = when(result) { is Success -> println("Success: ${result.data}") is Error -> println("Error: ${result.message}") // No need for 'else' branch as all cases are covered }
Delegated Properties: Implement properties by delegation
// Example: lazy initialization val expensiveResource: String by lazy { println("Computing expensive resource...") "Result" // Only computed on first access }
Scope Functions:
let
,run
,with
,apply
, andalso
for specialized operations on objectsExtension Functions: Add new functions to existing classes
fun String.addExclamation(): String { return "$this!" } println("Hello".addExclamation()) // Output: Hello!
Flow API: Reactive streams built on top of coroutines
Multiplatform Development: Write code for multiple platforms (JVM, JS, Native)
Type-Safe Builders: Create domain-specific languages (DSLs)
Reflection: Runtime introspection of code
Operator Overloading: Customize operators for your classes
Conclusion
You’ve now completed a comprehensive tour of Kotlin’s essential features—the 85% you’ll use in everyday programming. We’ve covered everything from basic syntax and variables to advanced object-oriented concepts and functional programming techniques.
Kotlin’s elegant design combines the best of object-oriented and functional programming paradigms, all while maintaining full interoperability with Java. Its concise syntax, null safety, and modern features make it an excellent choice for both new projects and existing Java codebases.
For the remaining 15% of advanced features, you now have a solid foundation to explore them on your own. As you build real-world applications, you’ll naturally encounter situations where these advanced features shine.
The best way to deepen your knowledge is through practice. Try building small projects, refactor existing Java code to Kotlin, or contribute to open-source Kotlin projects. Remember these official resources:
- Kotlin Documentation
- Kotlin Koans - Interactive exercises
- Kotlin by Example
Happy coding with Kotlin!