DEV Community

Cover image for From Java to Kotlin: Elevating Back-End Development with Conciseness and Expressivity
Lucas Fugisawa
Lucas Fugisawa

Posted on • Edited on • Originally published at fugisawa.com

From Java to Kotlin: Elevating Back-End Development with Conciseness and Expressivity

In the evolving world of back-end development, the choice of programming language plays a pivotal role in defining the efficiency, expressiveness, and overall experience of a developer. Kotlin, a modern language that interops seamlessly with Java, has emerged as a game-changer. Why is this transition important? It's because Kotlin offers an amalgamation of concise syntax and expressive features that significantly enhance developer productivity and project quality. This discussion is not just theoretical; it's a reflection of a broader shift in the industry towards more modern, efficient, and enjoyable coding practices.

As we delve into the core of our discussion, it's crucial to recognize that the transition from Java to Kotlin is more than a mere shift in programming language preference; it represents a significant evolution in the realm of back-end development. In the following sections, we'll explore a series of key ideas that distinctly highlight Kotlin's superiority in terms of conciseness, expressiveness, and overall code robustness.

Each point is designed as an actionable insight, aimed at demonstrating how Kotlin's unique features not only streamline the coding process but also enhance the quality and maintainability of the codebase. By juxtaposing Kotlin with Java, we'll uncover the tangible benefits that make Kotlin an increasingly popular choice among professional developers seeking to engage in more enjoyable and forward-thinking project environments.

1. Concise Syntax: Less Boilerplate, More Clarity

Kotlin reduces the boilerplate code common in Java, making code more readable and maintainable. Examples include data classes, which replace verbose Java POJOs; extension functions that allow adding functionality to existing classes without inheritance; named parameters and default arguments that simplify method calls; and the when function, which is a more powerful and flexible version of Java's switch statement.

Kotlin's design philosophy prioritizes conciseness, aiming to reduce the amount of boilerplate code that developers need to write. This focus on brevity not only makes the code more readable but also enhances its maintainability. Let's explore some of the ways Kotlin achieves this:

Data Classes

In Java, a typical Plain Old Java Object (POJO) requires a significant amount of boilerplate code, including getters, setters, equals(), hashCode(), and toString() methods. Kotlin simplifies this with data classes and class properties. A single line in Kotlin can replace dozens of lines in Java.

Java Example:

public class User {
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    // Getters, setters, equals(), hashCode(), toString()...
}
Enter fullscreen mode Exit fullscreen mode

Kotlin Example:

data class User(val name: String, val email: String)
Enter fullscreen mode Exit fullscreen mode

Kotlin's emphasis on concise syntax plays a pivotal role in reducing boilerplate and enhancing code clarity. This is evident not only in its streamlined approach to class declarations and extension functions but also in its versatile handling of control flow and its innovative use of scope functions like let, also, run, and apply.

The when function

The when construct in Kotlin is a more powerful and flexible version of Java's switch statement. It can be used with various types of expressions, not just constants, and can even test for conditions.

Example of when:

fun describeNumber(number: Int): String = when(number) {
    1 -> "One"
    2 -> "Two"
    in 3..10 -> "Between three and ten"
    else -> "Unknown number"
}
Enter fullscreen mode Exit fullscreen mode

Using when, if, and try/catch as Expressions

In Kotlin, when, if, and try/catch can be used as expressions, meaning they can return a value. This allows for more concise and expressive code.

Example of if as an Expression:

val result = if (number % 2 == 0) "Even" else "Odd"
Enter fullscreen mode Exit fullscreen mode

Example of try/catch as an Expression:

val data = try {
    loadData()
} catch (e: IOException) {
    "Error loading data"
}
Enter fullscreen mode Exit fullscreen mode

Extension Functions

Kotlin allows developers to add functions to existing classes without altering their source code or using inheritance, a concept known as extension functions. This capability is particularly useful for enhancing classes from Java libraries.

Kotlin Example:

fun String.addExclamation() = this + "!"

val excitedString = "Hello".addExclamation()  // Outputs "Hello!"
Enter fullscreen mode Exit fullscreen mode

Named Parameters and Default Arguments

Kotlin introduces named parameters and default arguments, which can greatly simplify method calls and reduce the need for method overloading or the Builder pattern frequently used in Java.

Kotlin Example:

fun buildMessage(greeting: String, name: String = "User") = "$greeting, $name!"

val message1 = buildMessage(greeting = "Hello", name = "Alice") // "Hello, Alice!"
val message2 = buildMessage(greeting = "Hi") // "Hi, User!"
Enter fullscreen mode Exit fullscreen mode

In this Kotlin example, buildMessage function provides a default value for name. This feature allows calling the function without specifying all arguments, thus reducing the need for multiple overloaded methods as seen in Java.

These examples demonstrate only a small portion of how Kotlin's concise syntax not only reduces the volume of code but also makes it more readable and easier to manage, marking a significant shift towards more efficient coding practices in back-end development.

2. Null Safety: Preventing the Billion-Dollar Mistake

One of Kotlin's most lauded features is its approach to null safety, designed to prevent the infamous Null Pointer Exception (NPE), often referred to as the "billion-dollar mistake". In Java, dealing with nulls can be quite troublesome, as any object can hold a null value, leading to NPEs if not handled meticulously. Kotlin tackles this issue head-on by distinguishing between nullable and non-nullable types at the language level, thereby incorporating a form of safety directly into the type system.

Explicit Nullability in Type System

In Kotlin, types are non-nullable by default. If you want a variable to hold a null value, it must be explicitly declared.

Kotlin Example:

var nonNullableString: String = "Hello" // Cannot be set to null
var nullableString: String? = null     // Can be set to null
Enter fullscreen mode Exit fullscreen mode

Null Safe Calls (?.)

Kotlin introduces the null safe call operator (?.), which allows you to call a method or access a property on an object only if that object is not null. This prevents NPEs and eliminates the need for excessive null checks.

Kotlin Example:

val length = nullableString?.length  // Safe call, returns length or null
Enter fullscreen mode Exit fullscreen mode

The Elvis Operator (?:)

The Elvis operator (?:) in Kotlin provides a concise way to handle null cases. It allows specifying a default value to use if the expression to the left of ?: is null.

Kotlin Example:

val lengthOrDefault = nullableString?.length ?: 0  // Returns 0 if nullableString is null
Enter fullscreen mode Exit fullscreen mode

Smart Casts after Null Checks

Kotlin is smart enough to perform automatic 'safe' casts after null checks, which further reduces the boilerplate code.

Kotlin Example:

if (nullableString != null) {
    val length = nullableString.length  // Automatically cast to non-nullable
}
Enter fullscreen mode Exit fullscreen mode

Through these features, Kotlin provides a much safer environment, mitigating the risk of null-related errors and improving the overall reliability of the code. This proactive approach to null safety is a significant advantage for developers, especially when compared to the often verbose and error-prone null handling in Java.

3. Coroutines: Simplifying Asynchronous Programming

In the realm of back-end development, managing asynchronous operations efficiently and effectively is crucial. Kotlin's coroutine framework dramatically simplifies this aspect of programming, especially when compared to Java's approach. Java often relies on complex and verbose constructs like threads, executors, and futures to handle asynchrony, which can be cumbersome and error-prone. Kotlin coroutines, conversely, offer a more intuitive and less boilerplate-intensive solution. They enable developers to write asynchronous code in a way that is both easier to understand and maintain.

Kotlin Coroutines for Asynchronous Tasks

Kotlin coroutines provide an elegant way of handling asynchronous tasks like database operations, file I/O, or network calls in a back-end environment.

Example: Asynchronous Database Operations

suspend fun fetchUserDetails(userId: Int): UserDetails {
    return withContext(Dispatchers.IO) {
        // Simulate an asynchronous database operation
        database.fetchUserDetails(userId)
    }
}

suspend fun updateUserDetails(user: UserDetails) {
    withContext(Dispatchers.IO) {
        // Update user details in the database asynchronously
        database.updateUser(user)
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, withContext(Dispatchers.IO) is used to perform IO-intensive tasks off the main thread, and the suspend keyword allows the function to be paused and resumed, facilitating non-blocking operations.

Example: Handling Multiple Asynchronous Calls
Kotlin coroutines shine when you need to handle multiple asynchronous operations, either concurrently or sequentially.

suspend fun fetchMultipleUserData(userId: Int): CombinedUserData {
    val userDetails = async { fetchUserDetails(userId) }
    val userPreferences = async { fetchUserPreferences(userId) }

    // Await both operations to complete and then combine results
    return combineUserData(userDetails.await(), userPreferences.await())
}
Enter fullscreen mode Exit fullscreen mode

In this example, async is used to initiate asynchronous operations, and await waits for their completion without blocking the main thread.

Advantages Over Java's Concurrency Tools

Kotlin coroutines offer several advantages over traditional Java approaches like threads or CompletableFuture:

  1. Simpler Syntax: Coroutines reduce boilerplate and increase code readability, making asynchronous code easier to write and understand.

  2. Structured Concurrency: Kotlin promotes structured concurrency, where coroutines are launched in specific scopes, leading to safer and more manageable code.

  3. Exception Handling: Error handling in coroutines is more intuitive, using familiar try-catch blocks.

  4. Efficient Resource Management: Coroutines are lightweight compared to threads and provide efficient utilization of system resources.

By leveraging coroutines, Kotlin enables back-end developers to handle complex asynchronous operations with much more simplicity and clarity. This leads to more robust and maintainable code, enhancing the overall development experience and application performance.

4. Extension Functions: Enhancing Existing Classes

Kotlin's extension functions stand out as a powerful feature that enhances the capabilities of existing classes without modifying their code. This feature allows developers to "extend" a class with new functionality, akin to adding methods to the class itself. It's particularly useful in back-end development for adding utility functions to classes, especially when working with classes from Java libraries where source modification is not possible. Extension functions maintain code cleanliness and reduce the need for cumbersome utility classes or subclasses.

Adding Utility Functions to Existing Types

Kotlin can extend Java's collection classes to add more intuitive or specific operations.

Let's say you frequently need to check if a string is a valid email format. Instead of a utility class, you can add an extension function directly to the String class.

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

val email = "example@test.com"
println(email.isValidEmail())  // Outputs true
Enter fullscreen mode Exit fullscreen mode

Or perhaps you need to check if a list contains a specific element. You can add an extension function to the List class.

fun <T> List<T>.secondOrNull(): T? = if (this.size >= 2) this[1] else null

val numbers = listOf(1, 2, 3)
println(numbers.secondOrNull())  // Outputs 2
Enter fullscreen mode Exit fullscreen mode

Extension Functions with Receivers

Extension functions can also be used with receivers, allowing you to scope functions to a particular context. This is useful for DSL (Domain Specific Language) creation or configuring objects.

class Configuration {
    var host: String = ""
    var port: Int = 0
}

fun configuration(init: Configuration.() -> Unit): Configuration {
    val config = Configuration()
    config.init()
    return config
}

val myConfig = configuration {
    host = "localhost"
    port = 8080
}
Enter fullscreen mode Exit fullscreen mode

In this example, the configuration function allows you to set properties of Configuration in a DSL-like manner, enhancing readability and ease of configuration setup.

Enhancing Functionality of External Libraries

Kotlin's extension functions are particularly powerful when working with external Java libraries. You can add custom methods to classes from these libraries, improving their usability without altering the original codebase.

fun ResultSet.getNullableString(columnLabel: String): String? {
    val value = this.getString(columnLabel)
    return if (this.wasNull()) null else value
}

// Usage in a database operation
val resultSet: ResultSet = //... get ResultSet from a query
val name = resultSet.getNullableString("name")
Enter fullscreen mode Exit fullscreen mode

Here, getNullableString is an extension function on ResultSet, allowing for a more Kotlin-esque handling of nullable database columns.

Infix Functions

Infix functions in Kotlin are a distinctive feature that enhance the language's expressiveness and readability, particularly when you need to define operations or interactions between objects in a more intuitive and human-readable way. Marked with the infix keyword, these functions can be called without using the dot and parentheses, resembling natural language syntax. This feature is especially useful in making the code more concise and fluent. Infix functions work well in scenarios where the function is acting on two objects, akin to operators, making the code not only more concise but also significantly more readable. Their implementation can range from simple utility functions to more complex domain-specific logic, adding a layer of expressiveness that aligns well with Kotlin's philosophy of clarity and conciseness.

Kotlin Infix Function for Authorization Checks

class User(val name: String, val roles: Set<String>)

infix fun User.hasRole(role: String): Boolean {
    return role in roles
}

fun main() {
    val user = User("Alice", setOf("admin", "editor"))

    if (user hasRole "admin") {
        println("User has admin access")
    } else {
        println("User does not have admin access")
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, hasRole is an infix function that checks if a User object has a specific role. It simplifies the syntax for role checking, making the code more intuitive. The usage of user hasRole "admin" in an if-statement reads almost like natural language, thereby improving the readability and maintainability of the code.

This kind of function can be particularly useful in scenarios involving user authentication and authorization, where clarity and conciseness of the code are crucial. It demonstrates the power of infix functions in creating more domain-specific and readable code in Kotlin.

Through these examples, it becomes evident that extension functions in Kotlin not only add to the robustness of the code but also enhance its readability and maintainability, making them a valuable tool in any Kotlin developer's arsenal, especially in back-end development.

5. Interoperability with Java: Seamless Integration

A key strength of Kotlin is its seamless interoperability with Java, which is particularly crucial for back-end development where legacy Java codebases are common. Kotlin is designed to work flawlessly with Java, allowing developers to gradually migrate to Kotlin or to use Kotlin alongside existing Java code. This interoperability means that Kotlin can use Java libraries and frameworks without any hassle, and Java code can call Kotlin code as if it were native Java. This feature significantly eases the transition to Kotlin for Java projects, providing flexibility and reducing the risks associated with adopting a new language.

Calling Java from Kotlin

Kotlin can effortlessly make use of existing Java libraries and classes. This example shows how Kotlin code can interact with a Java class.

Java Class Example:

public class JavaLogger {
    public void log(String message) {
        System.out.println("Log: " + message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Kotlin Usage Example:

fun main() {
    val javaLogger = JavaLogger()
    javaLogger.log("This is a log message from Kotlin!")
}
Enter fullscreen mode Exit fullscreen mode

In this example, a Kotlin function is using a Java class (JavaLogger) directly, showcasing the interoperability between Kotlin and Java. The Kotlin code treats the Java class as if it were a Kotlin class.

For another example, consider a standard JDBC setup in a Java environment for context:

// Java JDBC setup example (for reference)
public class DatabaseConnector {
    public Connection connectToDatabase(String url, String user, String password) throws SQLException {
        return DriverManager.getConnection(url, user, password);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's see how Kotlin can utilize this Java JDBC setup for executing a database query.

Kotlin Code Example:

import java.sql.DriverManager

fun main() {
    val url = "jdbc:mysql://localhost:3306/mydatabase"
    val user = "root"
    val password = "password"

    val connection = DriverManager.getConnection(url, user, password)
    val statement = connection.createStatement()
    val resultSet = statement.executeQuery("SELECT * FROM users")

    while (resultSet.next()) {
        val userName = resultSet.getString("username")
        println("User: $userName")
    }

    resultSet.close()
    statement.close()
    connection.close()
}
Enter fullscreen mode Exit fullscreen mode

In this Kotlin example, we're directly using the java.sql.DriverManager class from Java's JDBC API to connect to a SQL database and execute a query. This demonstrates how Kotlin can interact with Java libraries without any special adapters or wrappers. The Kotlin code is almost identical to how it would be written in Java, but with Kotlin's more concise syntax.

This seamless interoperability allows Kotlin developers to take advantage of the vast ecosystem of Java libraries and frameworks, facilitating easier adoption and migration in back-end projects where such libraries are already in use.

These examples highlight the flexibility and compatibility that Kotlin offers, making it an attractive choice for modernizing Java-based back-end systems without needing to rewrite existing codebases entirely.

6. Smart Casts and Delegated Properties: Boosting Productivity

Kotlin's introduction of smart casts and delegated properties significantly enhances developer productivity by streamlining code complexity and reducing boilerplate.

Smart Casts: Simplifying Type Checking

Smart casts in Kotlin significantly reduce the need for explicit casting and type checks, resulting in cleaner and more intuitive code. This feature is particularly useful when dealing with hierarchical type structures or polymorphism.

Improved Example of Smart Casts:

interface Animal {
    fun makeSound()
}

class Dog : Animal {
    override fun makeSound() = println("Woof")
    fun fetch() = println("Fetching...")
}

class Cat : Animal {
    override fun makeSound() = println("Meow")
    fun nap() = println("Napping...")
}

fun interactWithAnimal(animal: Animal) {
    animal.makeSound()
    when (animal) {
        is Dog -> animal.fetch()  // Smart cast to Dog
        is Cat -> animal.nap()    // Smart cast to Cat
    }
}

val myDog = Dog()
interactWithAnimal(myDog)
Enter fullscreen mode Exit fullscreen mode

In this example, interactWithAnimal function accepts an Animal type. Inside the function, the when expression checks whether the Animal is a Dog or a Cat. Upon confirmation, Kotlin smartly casts the animal to the respective type, allowing access to specific methods like fetch for Dog and nap for Cat. This eliminates the need for explicit casting and makes the code more readable and less error-prone.

Delegated Properties: Efficient Property Management

Delegated properties allow Kotlin developers to delegate the logic of getting and setting a property to a separate entity, such as a lazy initializer or an external storage.

Example of Lazy Initialization:

val heavyConfiguration: Configuration by lazy {
    // Configuration is only created when first accessed
    loadConfiguration()
}

fun loadConfiguration(): Configuration {
    println("Loading heavy configuration")
    // Configuration loading logic
    return Configuration()
}

class Configuration
Enter fullscreen mode Exit fullscreen mode

In this case, heavyConfiguration is a property that is initialized lazily. The Configuration object is only created when heavyConfiguration is accessed for the first time, demonstrating efficient resource management and initialization on demand.

These two Kotlin features, smart casts and delegated properties, simplify many common coding tasks, allowing developers to focus more on the business logic rather than on the intricacies of type casting and property management. This results in cleaner, more maintainable, and efficient code, especially in complex back-end development environments.

Conclusion

Transitioning from Java to Kotlin is not just about adopting a new language; it's about embracing a more modern and efficient approach to back-end development. My experience as a speaker at the International Developer Career Day 2023, particularly during my talk "Refactoring Java to Kotlin: A Close-Up On The Nuts And Bolts", underlines the practical benefits and enhanced productivity that Kotlin brings to the table. For professional back-end developers looking to craft more concise, expressive, and effective code, Kotlin is the key to engaging with truly enjoyable and modern projects and teams.

--
This article was originally posted to my Lucas Fugisawa on Kotlin blog, at: https://fugisawa.com/from-java-to-kotlin-elevating-back-end-development-with-conciseness-and-expressivity/

To explore more about this transition and its impact, subscribe to my newsletter on https://fugisawa.com/ and stay tuned for more insights and updates.

Top comments (0)