The best way to understand a language is to write code in it. Not read about it, not watch webinars, not save articles to "read later" (we all know how that ends). Kotlin from Scratch: Tutorial for Beginners - 1Open an IDE and build something that actually works.

That's exactly what this tutorial is for. We'll go from zero to a first working mini-app. If you know Java, things will move even faster — I'll be drawing parallels the whole way.

Running Kotlin: two options, one of which takes zero minutes

Right in the browser. Head to play.kotlinlang.org and just start writing. Nothing to install, nothing to configure, no Stack Overflow rabbit holes about why the setup failed. Perfect for a first look.

In IntelliJ IDEA. Kotlin support is built right in. Create a new Kotlin project and you're ready to go. No plugins, no ceremony.

Your first program:

fun main() {
    println("Hello, Kotlin!")
}

In Java, that same thing looks like this:

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, Kotlin!");
    }
}

Both do exactly the same thing. Kotlin just doesn't have public static void or the wrapper class, because... why did we need those again? Good question. Java never quite answered it.

val and var: two words to remember

In Kotlin, variables are declared with either val or var.

val name = "Alice"   // Immutable. Like final in Java.
var age = 25         // Mutable.

age = 26             // Fine.
name = "Bob"         // Compile error.

Simple rule: use val everywhere you don't need to change the value. var only when you actually need mutation. It's like making a promise upfront: "I'm not going to change this." And Kotlin makes sure you keep it.

You don't need to specify the type — Kotlin infers it:

val score = 100     // Kotlin figures out: this is an Int
val name = "Alice"  // This is a String

Or explicitly, if you prefer:

val score: Int = 100
val name: String = "Alice"

Null-safety: how Kotlin kills NullPointerException

Tony Hoare invented null in 1965 and later called it his "billion-dollar mistake." Sounds like self-criticism, but honestly — it's underselling it. NullPointerException has cost the industry far more than a billion dollars.

In Java, any variable can turn out to be null. The compiler stays quiet. You find out at runtime. In production. Sometimes on a live demo in front of a client — yes, that happens too.

Kotlin solves this at the type system level:

var name: String = "Alice"
name = null  // Compile error. Right away. Before anything runs.

If a variable can be null, you have to say so explicitly using ?:

var name: String? = "Alice"
name = null  // We agreed it's nullable — now that's fine.

So what do you do when you need to call a method on something that might be null?

println(name?.length)        // If null — returns null. Doesn't crash.
println(name?.length ?: 0)   // If null — returns 0. Elvis operator.

It's called the Elvis operator — because ?: looks like Elvis Presley's hair. That's not a joke. It's the actual explanation in the official documentation.

Functions

In Java:

public static int add(int a, int b) {
    return a + b;
}

In Kotlin:

fun add(a: Int, b: Int): Int {
    return a + b
}

Or even shorter — for single-expression functions:

fun add(a: Int, b: Int) = a + b

Kotlin supports default parameter values. In Java, you'd write several overloaded methods for this. In Kotlin — just one:

fun greet(name: String, greeting: String = "Hello") {
    println("$greeting, $name!")
}

greet("Alice")        // Hello, Alice!
greet("Bob", "Hey")   // Hey, Bob!

String templates — $name right inside the string. Kotlin substitutes the value directly. No +, no concatenation — like a sensible language should work.

Data class: goodbye, boilerplate

Here's a classic task. You need a model: a user with a name, age, and email.

In Java that means a constructor, three getters, three setters, equals(), hashCode(), toString(). Or Lombok. Or accepting your fate and writing it all by hand like it's 2005.

In Kotlin:

data class User(val name: String, val age: Int, val email: String)

One line. The compiler generates everything else, including copy():

val alice = User("Alice", 30, "alice@example.com")
val olderAlice = alice.copy(age = 31)

println(alice)
// User(name=Alice, age=30, email=alice@example.com)

Look at copy(). You create a new object, changing only the fields you need. In Java you'd usually write a Builder for this. Or just suffer.

Extension functions: adding methods to classes you don't own

Imagine you want to add a method to the String class. In Java — you write a utility class with static methods, and then everywhere you're writing StringUtils.doSomething(myString). It works, but it feels like a workaround.

In Kotlin — extension functions:

fun String.isValidEmail(): Boolean {
    return this.contains("@") && this.contains(".")
}

println("user@example.com".isValidEmail())  // true
println("not-an-email".isValidEmail())      // false

It reads like a native method on the class. No inheritance, no wrappers. You just added a method — and now you use it.

Putting it all together: a user validator

Let's take everything we've covered and write something real.

data class User(val name: String, val email: String?)

fun String.isValidEmail() = contains("@") && contains(".")

fun validateUser(user: User): String {
    val emailStatus = user.email?.let {
        if (it.isValidEmail()) "valid" else "invalid"
    } ?: "not provided"

    return "${user.name}: email $emailStatus"
}

fun main() {
    val users = listOf(
        User("Alice", "alice@example.com"),
        User("Bob", "bob-not-email"),
        User("Charlie", null)
    )
    users.forEach { println(validateUser(it)) }
}

Output:

Alice: email valid
Bob: email invalid
Charlie: email not provided

Notice: null is handled without a single if. The ?. and ?: operators take care of it elegantly. Charlie didn't provide an email — and there's no NPE. Life is good.

This was just the beginning

We covered the basics here: variables, null-safety, functions, data classes, extension functions.

It gets more interesting from here. Coroutines — async code without the pain. Sealed classes — a powerful alternative to switch that won't let you forget to handle a case. Functional collection operations — map, filter, reduce in one line.

If you want to learn Kotlin systematically rather than in scattered pieces from browser tabs — give our course a try. 62 levels, 1,100+ tasks with auto-verification, 3 projects for your portfolio. The first level is free.

codegym.cc/courses/kotlin

Keep reading