Introduction to the Kotlin Language

If:

  • You are a Java developer tired of its verbose syntax
  • You are a developer who wants to write less code so you can leave work on time ;))
  • You are a team lead writing post-mortems for production bugs caused by code review fatigue
  • You believe in the philosophy of less code, less bugs

Then Kotlin can definitely help you ;))

Drawn together by strengths
Driven apart by misunderstandings
Trí Xàm ;))

Here are the key strengths of Kotlin compared to Java, which will hopefully inspire you to explore Kotlin:

  1. JVM language
  2. Clean, compact, sugar syntax
  3. Null safety
  4. Unchecked exceptions
  5. Data classes
  6. Default values
  7. Named arguments
  8. Functional programming support
  9. Extension functions
  10. Coroutines: lightweight threads

1. JVM language

Kotlin, much like Scala, Clojure, Groovy…, is a JVM language (full list). Therefore, it inherits several major advantages:

  • Compiles to bytecode and runs on the JRE (Java Runtime Environment): Reuses existing infrastructure without changes.
  • Shares Java’s vast ecosystem and libraries: As one of the most popular languages in the world, Java provides an enormous ecosystem. You won’t need to spend time searching for or rewriting libraries when adopting Kotlin.
  • Interoperates seamlessly with Java code in the same module/project: If you have an existing Java project, you don’t need to rewrite it 100%. With just a few build configurations, you can have a hybrid project containing both Java and Kotlin. This is ideal for enterprise projects requiring stability and safety. Personally, I took over several Java projects and integrated Kotlin over two years ago. While Kotlin lines of code only accounted for around 35%, they handled roughly 70% of the business logic.

I will write a few articles sharing practical experiences and conversion strategies from Java to Kotlin. Stay tuned!!

Update: Check out the series here: From Java to Kotlin

2. Clean, compact, sugar syntax

Discussing how Kotlin simplifies code could take several articles, but here are the highlights I love most. Assuming you are already familiar with Java, I’ll jump straight into Kotlin syntax:

Main function
1
2
3
fun main() {
    println("Hello world!")
}
Functions
1
2
3
fun sum(a: Int, b: Int): Int {
    return a + b
}

Or more concisely with single-expression syntax:

1
fun sum(a: Int, b: Int) = a + b
Variables

Kotlin provides two keywords, val and var, for variable declarations. Types are inferred automatically if omitted. val is immutable (read-only), while var is mutable:

1
2
3
4
val a: Int = 1  // immediate assignment
val b = 2   // `Int` type is inferred
val c: Int  // Type required when no initializer is provided
c = 3       // deferred assignment
String templates
1
2
3
4
5
6
7
var a = 1
// simple name in template:
val s1 = "a is $a" 

a = 2
// arbitrary expression in template:
val s2 = "${s1.replace("is", "was")}, but now is $a"
Statements as expressions

Statements like if/else, try/catch, and when can return expressions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
val bigger = if (a > b) a else b
val color = when {
  relax -> GREEN
  studyTime -> YELLOW
  else -> BLUE
}
val obj = try {
  gson.fromJson(json)
} catch (e: Throwable) {
  null
}
Type checks and automatic casts
1
2
3
4
5
6
fun getStringLength(obj: Any): Int? {
    if (obj !is String) return null

    // `obj` is automatically cast to `String` in this branch
    return obj.length
}
Elimination of new keyword
1
2
val rectangle = Rectangle(5.0, 2.0)
val triangle = Triangle(3.0, 4.0, 5.0)

3. Null safety

As you may know, Null is considered the billion-dollar mistake, and Kotlin provides powerful tools to eliminate NullPointerException at compile time.

By default, types in Kotlin are non-nullable:

1
2
var a: String = "abc" // Regular initialization means non-null by default
a = null // compilation error

To allow a variable to hold null, explicitly append ?:

1
2
3
var b: String? = "abc" // can be set to null
b = null // ok
print(b)

Direct method calls on nullable types are disallowed by the compiler:

1
val l = b.length // error: variable 'b' can be null

To call methods safely, use the safe-call operator ?.:

1
val l = b?.length // l is nullable (Int?)

Use the Elvis operator ?: to provide a fallback default:

1
val l = b?.length ?: 0 // if (b == null) l = 0 else l = b.length

Safe Casts

1
2
val aInt = a as? Int // aInt: Int?
val aIntDefault = a as? Int ?: 0 // val aInt = if (a !is Int) 0 else a as Int

4. Unchecked exceptions

In Try/Catch Explain, I discussed why checked exceptions often create unnecessary friction. Many others in the industry share this viewpoint:

Fortunately, all exceptions in Kotlin are unchecked ;))

5. Data classes

Suppose we need a User class with two fields, name and age. In Kotlin, all you need is:

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

Kotlin automatically generates:

  • Getters and setters (for var properties)
  • equals() and hashCode()
  • toString() like User(name=John, age=42)
  • copy(): to clone instances while altering specific fields (covered in 7. Named arguments)

6. Default values

Kotlin supports default parameter values in constructors and methods:

1
data class User(val name: String = "Tri Le", val age: Int = 30)

Now you can instantiate objects flexibly:

1
2
val trile = User() // User(name=Tri Le, age=30)
val join = User("Join") // User(name=Join, age=30)

7. Named arguments

Combining default values with named arguments gives you clean, expressive code without needing builder patterns:

1
2
3
val join = User(name = "Join") // User(name=Join, age=30)
val trile = User(age = 30) // User(name=Tri Le, age=30)
val mary = User(name = "Mary", age = 16) // User(name=Mary, age=16)

8. Functional programming support

Lambdas were a huge improvement in Java 8, and Kotlin elevates functional programming even further.

Suppose we define a Student class:

1
2
3
4
5
6
class Student(
    val name: String,
    val surname: String,
    val passing: Boolean,
    val averageGrade: Double
)

Given a list of students, suppose the requirement is:

Get 10 passing students with an average grade greater than 4.0
Prioritize students with higher grades
Sorted alphabetically by student name (surname first, then name)

In Java, this requires considerable ceremony. In Kotlin, it takes just 4 fluent lines ;)):

1
2
3
4
students.filter { it.passing && it.averageGrade > 4.0 } // get only students who are passing with GPA > 4.0
    .sortedBy { it.averageGrade } // sort by average grade
    .take(10) // take first 10 students
    .sortedWith(compareBy({ it.surname }, { it.name })) // sort alphanumerically by surname then name

This example comes from this blog post. I will write a dedicated deep dive when time permits ;))

9. Extension functions

This is one of my favorite features in Kotlin. You can extend existing classes with new methods or properties without modifying their original source code—making it fantastic for integrating third-party libraries and modules. Remember that this is compiler syntactic sugar and does not modify the underlying class hierarchy.

For instance, adding a checkEmpty helper to String?:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fun String?.checkEmpty() = this == null || this.isEmpty()

fun main() {

  val nullString: String? = null
  val emptyString = ""
  val string = "hello"
  nullString.checkEmpty() // false
  emptyString.checkEmpty() // false
  string.checkEmpty() // true

}

Similarly, we can define extension properties:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
val String?.empty
  get() = this == null || this.isEmpty()

fun main() {

  val nullString: String? = null
  val emptyString = ""
  val string = "hello"
  nullString.empty // false
  emptyString.empty // false
  string.empty // true

}

10. Coroutines: lightweight threads

When working on network programming in the past, I dealt extensively with Java threads and encountered several drawbacks:

  • Heavy resource usage: Each thread allocates roughly 330KB (32-bit OS) to 1MB (64-bit OS) of stack space, limiting the number of concurrent threads a single process can spawn.
  • Resource contention and locking: Increases project complexity and performance overhead.
  • Challenging debugging, tracing, and testability.
  • Risk of deadlocks.

These challenges are addressed by Coroutines. A full series from basics to advanced patterns is necessary to cover coroutines comprehensively.

As a quick demonstration of what coroutines make possible that traditional Java threads struggle with ;)):

1
2
3
4
5
6
7
8
fun main() = runBlocking {
    repeat(100_000) { // launch a lot of coroutines
        launch {
            delay(5000L)
            print(".")
        }
    }
}

The code above spawns 100,000 coroutines, each printing a dot after a 5-second delay. Trying this with 100k OS threads in Java would quickly trigger an OutOfMemoryError.

Update: Check out the full series on Kotlin Coroutines

Conclusion

I hope this overview inspires you to dive into Kotlin and consider adopting it for your upcoming projects!

updatedupdated2026-09-052026-09-05
Load Comments?