DEV Community

Florian Röser
Florian Röser

Posted on

Kotlin for Java Developers (Part 3)

This is part 3 of a series, that outlines the most common hints and quirks a java developer might face when switching to kotlin. This article handles following topics:

  • Forgetting Default Arguments And Parameters
  • Using for Loops Instead Of forEach
  • Using lazy Initialization
  • Reference: Kotlin Coding Conventions

If you are interested in the previous articles, here you can find Part 1 and Part 2

Forgetting Default Arguments And Parameters

In contrast to Java, Kotlin allows the definition of default values for function parameters, eliminating the need for overloading functions with different parameter counts. This is especially useful when a function has many parameters that often take the same value.

Default Function Parameters

Java way:

public void greet(String name, String greeting) {
  System.out.println(greeting + " " + name);
}

/**
 * Overloaded "greet" method with default value
 */
public void greet(String name) {
  greet(name, "Hello");
}
Enter fullscreen mode Exit fullscreen mode

Kotlin way:

with default value for the parameter greeting

fun greet(name: String, greeting: String = "Hello") {
  println("$greeting $name")
}
Enter fullscreen mode Exit fullscreen mode

example calls:

greet("Alice") // Hello Alice
greet("Bob", "Hi") // Hi Bob
Enter fullscreen mode Exit fullscreen mode

This approach removes duplicated code and reduces the method count, especially when the method has many parameters.

Default Constructor Arguments

Default values are not only allowed in function parameters, but also in constructor arguments. This is especially helpful when you need to create objects with optional parameters.

Java way:

public class Person {
  private String name;
  private int age;

  public Person(String name) {
    this(name, 42);
  }

  public Person(String name, int age) {
    this.name = name;
    this.age = age;
  }

  public void introduce() {
    System.out.println("Hi, I'm " + name + ", and I'm " + age + " years old.");
  }
}
Enter fullscreen mode Exit fullscreen mode

Usage in Java:

public class Main {
    public static void main(String[] args) {
        Person person1 = new Person("Alice");  // Uses default age 42
        Person person2 = new Person("Bob", 25);  // Uses custom age 25

        person1.introduce();  // Outputs: Hi, I'm Alice, and I'm 42 years old.
        person2.introduce();  // Outputs: Hi, I'm Bob, and I'm 25 years old.
    }
}
Enter fullscreen mode Exit fullscreen mode

Instead of overloading the constructor, you can use default values in Kotlin:

Kotlin way:

class Person(val name: String, val age: Int = 42) {
    fun introduce() {
        println("Hi, I'm $name, and I'm $age years old.")
    }
}
Enter fullscreen mode Exit fullscreen mode

Usage in Kotlin:

val person1 = Person("Alice")  // Uses default age 42
val person2 = Person("Bob", 25)  // Uses custom age 25

person1.introduce()  // Outputs: Hi, I'm Alice, and I'm 42 years old.
person2.introduce()  // Outputs: Hi, I'm Bob, and I'm 25 years old.
Enter fullscreen mode Exit fullscreen mode

Using for Loops Instead Of forEach

In Kotlin, you can use the higher-order function forEach to iterate over collections, however, Kotlin's for loop is often more idiomatic and can be a better choice in some cases. Understanding when to use which loop is crucial for writing clean Kotlin code.

The for loop in Kotlin

Basic for loop example using a range over a collection:

val numbers = listOf(1, 2, 3, 4, 5)

for (number in numbers) {
    println(number)
}
Enter fullscreen mode Exit fullscreen mode

Basic forEach loop in Kotlin

The forEach function is part of Kotlin's functional programming features, it takes a lambda function as an argument and executes it on each element of the collection:

val numbers = listOf(1, 2, 3, 4, 5)

numbers.forEach { number -> println(number) }
Enter fullscreen mode Exit fullscreen mode

The element is available as it by default, if you don't specify a name for the lambda parameter:

val numbers = listOf(1, 2, 3, 4, 5)

numbers.forEach { println(it) }
Enter fullscreen mode Exit fullscreen mode

When to use for loops

If you're performing simple iterations and don't need the additional power, the for loop is usually the better choice, especially for performance reasons. forEach involves function-calls (higher-order functions), which can incur slight overhead, while a for loop is generally more efficient.

Example (Performance Consideration):

val numbers = listOf(1, 2, 3, 4, 5)

var sum = 0
for (number in numbers) {
    sum += number
}
println(sum)
Enter fullscreen mode Exit fullscreen mode

In this case, the for loop does not involve any additional function calls, making it more efficient.

Example (Using break and continue):

The for loop has built-in support for break and continue statements, allowing you control over the flow more easily.

val numbers = listOf(1, 2, 3, 4, 5)

for (number in numbers) {
    if (number == 3) {
        break // exit the loop when number is 3
    }
    println(number) // Will print 1, 2
}
Enter fullscreen mode Exit fullscreen mode

When to use forEach loops

1. Cleaner and more expressive for functional style programming

The forEach function is more idiomatic when you're working with a functional programming style or when you want the code to be more declarative and expressive. If you don't need break or continue, the forEach can be the more readable and elegant choice, especially for smaller functions.

Example (Functional Style):

val numbers = listOf(1, 2, 3, 4, 5)
numbers.forEach { println(it) }
Enter fullscreen mode Exit fullscreen mode

2. When you want to use lambdas for more complex operations

forEach allows you to use lambdas for more complex operations, such as filtering, mapping, or reducing elements.

Example (Using Lambda):

val numbers = listOf(1, 2, 3, 4, 5)

numbers.forEach {
    val squared = it * it
    println("The square of $it is $squared")
}
Enter fullscreen mode Exit fullscreen mode

3. Functionally chaining operations

When working with collections, forEach can be more intuitive when you're chaining multiple operations together like map, `filter, etc.

Example (Chaining Operations):

`kotlin
val numbers = listOf(1, 2, 3, 4, 5)

numbers.filter { it % 2 == 0 }
.forEach { println(it) } // Prints only even numbers
`

Performance considerations

While forEach is more convenient and expressive, there is a small performance overhead due to the lambda function calls. The difference in performance between those two is usually minimal for small collections, however, if performance is critical, especially with large collections, for loops might be preferable.


Using lazy Initialization

Another Kotlin-feature is the lazy initialisation, which allows to defer the creation of an object or the evaluation of an expression until it's actually needed. This can be useful to improve performance of your application by delaying expensive computations until they are actually needed. Kotlin's lazy initialisation is thread-safe by default.

Example: Lazy Initialization

`kotlin
val name: String by lazy {
println("Initializing name")
"Kotlin"
}

fun main() {
println("Before accessing name")
println(name)
println("After accessing name")

// Output:
// Before accessing name
// Initializing name
// Kotlin
// After accessing name
Enter fullscreen mode Exit fullscreen mode

}
`

Example: Lazy Initialization for Expensive Objects

Example of lazy database connection

`kotlin
class Database {
init {
println("Connecting to database...")
}

fun queryData(): String {
    return "Database Data"
}
Enter fullscreen mode Exit fullscreen mode

}

class DataFetcher {
val database: Database by lazy { Database() } // only created when accessed

fun fetchData() = database.queryData()
Enter fullscreen mode Exit fullscreen mode

}

fun main() {
val dataFetcher = DataFetcher()
println("Before fetching data")
println(dataFetcher.fetchData()) // the database-connection is created here
println("After fetching data")

// Output:
// Before fetching data
// Connecting to database...
// Database Data
// After fetching data
Enter fullscreen mode Exit fullscreen mode

}
`

Notice that the database-connection is only created when the database property is actually accessed.

Other use-cases

Other use-cases for lazy initialization include:

  • Singletons
  • Heavy computation
  • Resources, such as database connections, large datasets or API clients
  • Optional features, that might not be used in every execution path

Kotlin Coding Conventions

Kotlin has dedicated a whole page to coding conventions, which you can find here.
Please make yourself familiar with these conventions, as they are widely accepted in the Kotlin community.

Top comments (0)