Main Syntax #
Kotlin was designed with a clear philosophy: good code should be easy to read, safe from common errors like NullPointerException, and shouldn’t force developers to write unnecessary boilerplate. Kotlin’s syntax is the embodiment of that philosophy — concise yet expressive, familiar to Java developers but far more modern. This article covers every core Kotlin syntax element you’ll use daily, from the most basic program structure to the features that make Kotlin stand out from similar languages.
Program Structure #
Every Kotlin program has one entry point: the main function. Execution starts here, and understanding its structure is the foundation for everything else.
fun main() {
println("Hello, World!")
}
Three keywords you need to understand from the start:
| Element | Explanation |
|---|---|
fun | The keyword for defining a function |
main | The special name the JVM recognizes as the program entry point |
println() | A standard library function for printing to the console with a newline |
Kotlin also supports a main function with parameters to accept command line arguments:
fun main(args: Array<String>) {
if (args.isEmpty()) {
println("No arguments were given.")
return
}
println("Arguments received: ${args.size}")
args.forEachIndexed { index, arg ->
println(" [$index] $arg")
}
}
One thing you’ll notice right away is different from Java: no semicolons at the end of statements. Kotlin uses newlines as statement separators automatically. You can add semicolons if you want to write two statements on one line, but that’s not the recommended style.
// ANTI-PATTERN: unnecessary semicolons
val x = 10;
val y = 20;
println(x + y);
// CORRECT: no semicolons
val x = 10
val y = 20
println(x + y)
Variables and Declarations #
Kotlin has two keywords for declaring variables: val and var. The difference is fundamental and affects how you think about state in a program.
val (value) — its value can’t be changed after initialization. This doesn’t mean the value must be known at compile time — it only means the reference can’t be reassigned.
var (variable) — its value can be changed at any time.
// val: set once, can't be replaced
val pi = 3.14159
val appName = "KotlinApp"
// pi = 3.0 // ✗ error: val cannot be reassigned
// var: can change
var score = 0
score = 100
score += 50
println(score) // 150
Type Inference #
Kotlin has strong type inference — the compiler can guess the type from the given value. You don’t always need to write the type explicitly.
// ANTI-PATTERN: writing the type when it's already obvious from the value
val name: String = "Budi"
val age: Int = 25
val active: Boolean = true
// CORRECT: let the compiler guess
val name = "Budi" // String
val age = 25 // Int
val active = true // Boolean
val temperature = 36.5 // Double
val score = 36.5f // Float (f suffix)
val population = 8_000_000_000L // Long (L suffix)
Write the type explicitly only when it’s not clear from the value, or when you want a different type than the compiler would infer:
// Explicit needed: want Float, not Double
val temperature: Float = 36.5
// Explicit needed: a variable declared without an initial value
var result: Int
result = calculateSomething()
Principle: Prefer val over var
#
Use val as the default. Switch to var only if you actually need to change the value. This isn’t a rigid rule, but this habit produces code that’s more predictable and safer from bugs caused by unexpected mutation.
// ANTI-PATTERN: using var when the value never changes
var maximum = 100
var message = "Welcome"
// CORRECT: use val
val maximum = 100
val message = "Welcome"
Data Types #
Kotlin has primitive data types that are all represented as objects — there’s no primitive/wrapper distinction like in Java.
Numeric Types #
val byteVal: Byte = 127 // -128 to 127
val shortVal: Short = 32767 // -32768 to 32767
val intVal: Int = 2_147_483_647 // about ±2.1 billion
val longVal: Long = 9_223_372_036_854_775_807L
val floatVal: Float = 3.14f // ~7 decimal digits of precision
val doubleVal: Double = 3.141592653589793 // ~15 decimal digits of precision
Underscores (_) can be used inside number literals for readability — the compiler ignores them:
val worldPopulation = 8_000_000_000L
val hexColor = 0xFF_AA_33
val binaryMask = 0b1010_1010
String Types #
Strings in Kotlin can use template expressions directly inside them — no manual concatenation needed.
val name = "Rina"
val age = 28
val city = "Bandung"
// ANTI-PATTERN: manual concatenation
val introduction = "My name is " + name + ", age " + age + ", from " + city + "."
// CORRECT: string template
val introduction = "My name is $name, age $age, from $city."
// For complex expressions, use ${...}
val info = "Name length: ${name.length} characters"
val status = "Adult: ${if (age >= 18) "Yes" else "No"}"
Kotlin also supports raw strings (multiline strings) with triple quotes — useful for long text templates:
val query = """
SELECT u.name, u.email, o.total
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.status = 'completed'
ORDER BY o.total DESC
LIMIT 10
""".trimIndent()
.trimIndent() removes the uniform leading indentation for clean output.
Control Flow #
If — An Expression, Not Just a Statement #
In Kotlin, if is an expression that returns a value. This eliminates the need for a ternary operator (?: in Java/JavaScript).
val score = 75
// if as a statement (like other languages)
if (score >= 70) {
println("Passed")
} else {
println("Failed")
}
// CORRECT: if as an expression — more concise
val status = if (score >= 70) "Passed" else "Failed"
println(status)
// If expressions can be multiline
val grade = if (score >= 90) {
"A"
} else if (score >= 80) {
"B"
} else if (score >= 70) {
"C"
} else {
"D"
}
When — A Much More Powerful Switch #
when in Kotlin is far more flexible than switch in Java. It can also be used as an expression.
val code = 404
// Basic when
when (code) {
200 -> println("OK")
201 -> println("Created")
400 -> println("Bad Request")
401, 403 -> println("Auth Error") // multiple values in one branch
in 500..599 -> println("Server Error") // range
else -> println("Unknown status")
}
// when as an expression
val message = when (code) {
200 -> "Success"
404 -> "Not found"
in 500..599 -> "Server error"
else -> "Other status: $code"
}
when can also be used without arguments as a replacement for long if-else chains:
val temperature = 35.0
val condition = when {
temperature < 0 -> "Freezing"
temperature < 15 -> "Cold"
temperature < 25 -> "Cool"
temperature < 35 -> "Warm"
else -> "Hot"
}
println(condition) // Hot
Loops #
Kotlin provides three kinds of loops: for, while, and do-while.
// for with a range
for (i in 1..5) {
print("$i ") // 1 2 3 4 5
}
// exclusive range with until
for (i in 0 until 5) {
print("$i ") // 0 1 2 3 4
}
// step — skip N steps
for (i in 0..10 step 2) {
print("$i ") // 0 2 4 6 8 10
}
// backwards with downTo
for (i in 5 downTo 1) {
print("$i ") // 5 4 3 2 1
}
// iterating a collection
val fruits = listOf("Mangga", "Apel", "Jeruk")
for (item in fruits) {
println(item)
}
// with index
for ((index, item) in fruits.withIndex()) {
println("$index: $item")
}
// while
var counter = 0
while (counter < 5) {
println(counter)
counter++
}
// do-while: the block executes at least once
var input: String
do {
input = readInput()
} while (input.isBlank())
Functions #
Functions are defined with the fun keyword. Kotlin supports various forms of functions — from the simplest to the very expressive.
// Basic function with parameters and a return type
fun add(a: Int, b: Int): Int {
return a + b
}
// Single-expression function — more concise for simple functions
fun add(a: Int, b: Int): Int = a + b
// Return type can be inferred for single-expression functions
fun add(a: Int, b: Int) = a + b
Default Parameters and Named Arguments #
Kotlin supports default parameters — reducing the need for excessive function overloading.
fun createUser(
name: String,
age: Int = 0,
email: String = "",
active: Boolean = true
): String {
return "User($name, age=$age, email=$email, active=$active)"
}
// Call with all arguments
createUser("Budi", 25, "[email protected]", true)
// Call with some arguments (the rest use defaults)
createUser("Sari")
// Named arguments — free order, more explicit
createUser(name = "Ahmad", active = false, email = "[email protected]")
Functions with Vararg #
fun sum(vararg numbers: Int): Int {
return numbers.sum()
}
println(sum(1, 2, 3)) // 6
println(sum(10, 20, 30, 40)) // 100
Classes and Objects #
Defining a Class #
class Car(
val brand: String,
val model: String,
var year: Int
) {
// Additional property
var odometer: Double = 0.0
// Method
fun drive(distanceKm: Double) {
odometer += distanceKm
println("$brand $model drove $distanceKm km. Total: $odometer km")
}
// Descriptive automatic toString
override fun toString(): String {
return "$brand $model ($year) — ${odometer}km"
}
}
val car = Car("Toyota", "Avanza", 2022)
car.drive(150.0)
car.drive(75.5)
println(car) // Toyota Avanza (2022) — 225.5km
Data Class #
For classes whose job is only to hold data, use data class. Kotlin automatically generates equals(), hashCode(), toString(), and copy().
data class Student(
val name: String,
val id: String,
val gpa: Double
)
val student1 = Student("Budi", "2021001", 3.85)
val student2 = student1.copy(gpa = 3.90) // copy with different values
println(student1) // Student(name=Budi, id=2021001, gpa=3.85)
println(student1 == student2) // false (different gpa)
Object — Singleton #
object is used to create a singleton without needing a manual design pattern.
object AppConfig {
val version = "1.0.0"
val appName = "MyKotlinApp"
var debugMode = false
fun info() = "$appName v$version (debug: $debugMode)"
}
println(AppConfig.info())
AppConfig.debugMode = true
Companion Object #
A companion object is Kotlin’s equivalent of static members in Java.
class Connection private constructor(val url: String) {
companion object {
private var instance: Connection? = null
fun getInstance(url: String): Connection {
return instance ?: Connection(url).also { instance = it }
}
}
}
val db = Connection.getInstance("jdbc:postgresql://localhost/mydb")
Null Safety #
Null safety is one of Kotlin’s most valuable features. In Java, NullPointerException is a very common cause of crashes. Kotlin forces you to handle the possibility of null explicitly at the type level.
flowchart TD
A[Kotlin Variable] --> B{Nullable?}
B -- No --> C[Normal Type\nString, Int, etc]
B -- Yes --> D[Nullable Type\nString?, Int?, etc]
C --> E[Can't be null\nsafe for direct access]
D --> F{Access method?}
F --> G["Safe call (?.)"]
F --> H["Elvis operator (?:"]
F --> I["Non-null assertion (!!)"]
G --> J[Null if the object is null]
H --> K[Default value if null]
I --> L["Exception if null\n⚠ use with caution"]// Non-nullable type: can't be assigned null
var name: String = "Budi"
// name = null // ✗ compilation error!
// Nullable type: must be marked with ?
var nullableName: String? = null
nullableName = "Sari" // ✓ allowed
nullableName = null // ✓ allowed
Safe Call Operator ?.
#
Accesses a property or method only if the object isn’t null. Returns null if the object is null.
val name: String? = getNameFromDatabase()
// ANTI-PATTERN: manual null check like Java
if (name != null) {
println(name.length)
}
// CORRECT: safe call
println(name?.length) // null if name is null
// Chaining safe calls
val firstNameLength = user?.profile?.firstName?.length
Elvis Operator ?:
#
Provides a default value when the expression on its left is null.
val name: String? = getName()
// Default value if null
val display = name ?: "Anonymous User"
// Can be combined with safe call
val length = name?.length ?: 0
// Elvis with throw — concise validation
val guaranteedName = name ?: throw IllegalArgumentException("Name must not be null")
Non-Null Assertion !!
#
Tells the compiler “trust me, this isn’t null”. Throws a NullPointerException if it turns out to be null.
// ANTI-PATTERN: using !! carelessly
val length = name!!.length // crashes if name is null!
// CORRECT: use !! only if you're 100% sure it's not null
// and there's already a check before it
val config = System.getenv("DATABASE_URL")
?: throw RuntimeException("DATABASE_URL must be set")
// After the line above, config is definitely not null
val url = config // already safe, no !! needed
Avoid!!as much as possible. Every!!in your code is a potential unhandled crash. If you feel the need to use!!, reconsider your logic flow — there’s usually a safer way with?.,?:, orlet.
Let for Conditional Execution Blocks #
val email: String? = getEmail()
// Execute the block only if email is not null
email?.let { e ->
println("Sending email to: $e")
sendEmail(e)
}
Collections #
Kotlin distinguishes collections into two categories: immutable (can’t be changed) and mutable (can be changed). This is explicit at the type level.
// Immutable: can't add/remove/change elements
val nameList = listOf("Budi", "Sari", "Ahmad")
val numberSet = setOf(1, 2, 3, 4, 5)
val dictionary = mapOf("id" to "Indonesia", "en" to "English")
// Mutable: can be modified
val dynamicList = mutableListOf("Beginning")
dynamicList.add("Middle")
dynamicList.add("End")
val dynamicMap = mutableMapOf<String, Int>()
dynamicMap["one"] = 1
dynamicMap["two"] = 2
Collection Operations #
Kotlin provides a rich set of functional operations for processing collections without manual loops.
val students = listOf(
mapOf("name" to "Budi", "gpa" to 3.85, "major" to "Informatics"),
mapOf("name" to "Sari", "gpa" to 3.92, "major" to "Mathematics"),
mapOf("name" to "Ahmad", "gpa" to 3.71, "major" to "Informatics"),
mapOf("name" to "Rina", "gpa" to 3.60, "major" to "Physics"),
)
// filter — take elements matching a condition
val highGpa = students.filter { it["gpa"] as Double >= 3.80 }
// map — transform every element
val namesOnly = students.map { it["name"] }
// sortedBy — sort
val sorted = students.sortedByDescending { it["gpa"] as Double }
// groupBy — group
val byMajor = students.groupBy { it["major"] }
// find — find one element
val budiStudent = students.find { it["name"] == "Budi" }
// any and all — check conditions
val anyoneWithPerfectGpa = students.any { (it["gpa"] as Double) == 4.0 }
val allPassed = students.all { (it["gpa"] as Double) >= 3.0 }
Lambdas and Higher-Order Functions #
A lambda is an anonymous function that can be stored in a variable and passed as an argument. A higher-order function is a function that accepts or returns another function.
// Basic lambda
val greet: (String) -> String = { name -> "Hello, $name!" }
println(greet("Budi")) // Hello, Budi!
// Lambda with the implicit 'it' parameter (for a single parameter)
val square: (Int) -> Int = { it * it }
println(square(5)) // 25
// Higher-order function
fun processNumber(number: Int, operation: (Int) -> Int): Int {
return operation(number)
}
println(processNumber(10) { it * 2 }) // 20 — trailing lambda
println(processNumber(10) { it + 100 }) // 110
Trailing Lambda #
If the last parameter of a function is a lambda, you can write it outside the parentheses — this is called trailing lambda syntax and it makes code much cleaner.
// ANTI-PATTERN: lambda inside parentheses
nameList.forEach({ name -> println(name) })
// CORRECT: trailing lambda
nameList.forEach { name -> println(name) }
// With the implicit 'it'
nameList.forEach { println(it) }
Extension Functions #
Extension functions let you add new functions to existing classes — even classes from third-party or standard libraries — without inheritance.
// Add a function to String
fun String.isPalindrome(): Boolean {
val clean = this.lowercase().replace(" ", "")
return clean == clean.reversed()
}
println("kasur rusak".isPalindrome()) // true
println("kotlin".isPalindrome()) // false
// Add a function to Int
fun Int.percentOf(percent: Int): Double {
return this * percent / 100.0
}
println(500_000.percentOf(10)) // 50000.0
// Add a function to List
fun <T> List<T>.printAll() {
forEachIndexed { i, item -> println("$i. $item") }
}
listOf("One", "Two", "Three").printAll()
Extension functions are very useful for writing code that feels natural. Instead of StringUtils.capitalize(str), you can write str.capitalize().
Extension functions don’t actually modify the class being extended. They’re compiled into ordinary static methods on the JVM. This means extension functions can’t access private members of the extended class.
Summary #
valvsvar— usevalas the default, switch tovaronly if the value actually needs to change. Code withvalis more predictable and safer.- Type inference — you don’t always need to write the type explicitly; let the compiler guess from the given value. Write explicit types only when the type isn’t clear or differs from what would be inferred.
ifandwhenare expressions — both return values and can be used on the right side of an assignment, replacing verbose ternary operators and switches.- String templates — use
$variableor${expression}instead of manual concatenation with+.- Null safety at the type level — non-nullable types can’t be assigned null; nullable types are marked with
?. Use?.,?:, andletto handle nullables safely. Avoid!!unless you’re truly forced to.data classfor data objects — automatically getsequals,hashCode,toString, andcopy. Use it for models, DTOs, and similar objects.- Immutable collections as the default — use
listOf,setOf,mapOfby default; switch tomutableListOfetc. only if you actually need modification.- Extension functions — an elegant way to add functionality to existing classes without inheritance, producing code that feels more natural and readable.