Comparison between Kotlin and Java: Versatility and Android Applications

It’s undeniable that Kotlin has overtaken Java as the go-to language for Android, becoming Google’s favorite and thus the more suitable option for new mobile apps. However, both languages boast a wide array of advantages for general-purpose programming. Consequently, understanding their differences is crucial for developers, especially for tasks like transitioning from Java to Kotlin. This article will delve into the disparities and commonalities between Kotlin and Java, providing insights for informed decision-making and seamless switching between the two.

Are Kotlin and Java Alike?

From a broader perspective, Kotlin and Java share a considerable amount of common ground. Both operate on the Java Virtual Machine (JVM) rather than directly compiling to native code, and they can interact with each other effortlessly: Java code can be called from Kotlin and vice versa. Java proves useful in various domains, including server-side applications, databases, web front-end applications, embedded systems, enterprise applications, mobile development, and beyond. Similarly, Kotlin exhibits versatility, targeting the JVM, Android, JavaScript, and Kotlin/Native, while also being applicable to server-side, web, and desktop development.

Java, initially released in 1996, holds a significant maturity advantage over Kotlin. Although Kotlin 1.0 emerged much later in 2016, it quickly rose to prominence, becoming the officially preferred language for Android development in 2019. However, outside the Android realm, there is no recommendation to replace Java with Kotlin.

Year
Java
Kotlin
1995–2006
JDK Beta, JDK 1.0, JDK 1.1, J2SE 1.2, J2SE 1.3, J2SE 1.4, J2SE 5.0, Java SE 6
N/A
2007
Project Loom first commit
N/A
2010
N/A
Kotlin development started
2011
Java SE 7
Kotlin project announced
2012
N/A
Kotlin open sourced
2014
Java SE 8 (LTS)
N/A
2016
N/A
Kotlin 1.0
2017
Java SE 9
Kotlin 1.2; Kotlin support for Android announced
2018
Java SE 10, Java SE 11 (LTS)
Kotlin 1.3 (coroutines)
2019
Java SE 12, Java SE 13
Kotlin 1.4 (interoperability for Objective-C and Swift); Kotlin announced as Google’s preferred language for developers
2020
Java SE 14, Java SE 15
N/A
2021
Java SE 16, Java SE 17 (LTS)
Kotlin 1.5, Kotlin 1.6
2022
Java SE 18, JDK 19
Kotlin 1.7 (alpha version of Kotlin K2 compiler), Kotlin 1.8
2023
Java SE 20, Java SE 21, JDK 20, JDK 21
Kotlin 1.9
2024
Java SE 22 (scheduled)
Kotlin 2.0 (potentially)

Kotlin vs. Java: Evaluating Performance and Memory Usage

Prior to delving into the specifics of Kotlin and Java’s features, let’s assess their performance and memory consumption, as these aspects often hold considerable weight for developers and clients alike.

Kotlin, Java, and other JVM languages, while not identical, exhibit relatively similar performance characteristics, particularly when juxtaposed with languages belonging to different compiler families like GCC or Clang. The JVM’s initial design in the 1990s targeted embedded systems with limited resources. This resulted in two primary limitations:

  • Streamlined JVM Bytecode: The latest JVM iteration, upon which both Kotlin and Java are compiled, comprises a mere 205 instructions. In stark contrast, a modern x64 processor can effortlessly accommodate over 6,000 encoded instructions, with the precise count depending on the calculation method employed.
  • Runtime (vs. Compile-Time) Operations: The emphasis on a multiplatform approach (“Write once and run anywhere”) prioritizes optimizations performed at runtime instead of compile time. Consequently, the JVM translates the majority of its bytecode into instructions during program execution. Nevertheless, to enhance performance, developers can leverage open-source JVM implementations such as HotSpot, which pre-compiles the bytecode to expedite its execution by the interpreter.

Given their comparable compilation processes and runtime environments, the performance disparities between Kotlin and Java are generally minor and stem from their unique features. For instance:

  • Kotlin’s inline functions, which circumvent the overhead of function calls, contribute to improved performance, while Java’s conventional function calls introduce additional memory overhead.
  • Kotlin’s higher-order functions outperform Java lambdas by avoiding the dedicated InvokeDynamic call, resulting in enhanced performance.
  • Kotlin’s generated bytecode incorporates assertions for nullity checks when interacting with external dependencies, leading to a slight performance disadvantage compared to Java.

Shifting our focus to memory usage, it’s theoretically true that utilizing objects to represent primitive data types (as Kotlin does) necessitates more memory allocation compared to using primitive data types directly (as in Java). However, in practical scenarios, Java’s bytecode often relies on autoboxing and unboxing calls to manipulate objects, which can introduce computational overhead if excessively employed. For example, Java’s String.format method exclusively accepts objects as input, necessitating the boxing of a Java int into an Integer object before invoking String.format.

Overall, the performance and memory characteristics of Java and Kotlin are largely comparable. While online benchmarks may reveal minor discrepancies in micro-benchmarks, these findings cannot be extrapolated to gauge the behavior of full-fledged production applications.

Contrasting Unique Features

Despite their fundamental similarities, Kotlin and Java possess distinct, unique features. Since Kotlin’s ascension as Google’s preferred language for Android development, I’ve found extension functions and explicit nullability to be the most beneficial additions. Conversely, when working with Kotlin, the Java features I find myself missing the most are the protected keyword and the ternary operator.

From left to right are shown a white Variable oval, an equals sign, a green First Expression box, a question mark, a dark blue Second Expression box, a colon, and a light blue Third Expression box. The First Expression box has two arrows: one labeled “Is True” points to the Second Expression box, and the second labeled “Is False” points to the Third Expression box. Second Expression and Third Expression each have their own Return Value arrow pointing to the Variable oval.
The Ternary Operator

Let’s delve into a more granular comparison of the features offered by Kotlin and Java. For a more interactive learning experience, feel free to experiment with the provided examples using the Kotlin Playground or a Java compiler.

Feature
Kotlin
Java
Description
Extension functions
Yes
No
Allows you to extend a class or an interface with new functionalities such as added properties or methods without having to create a new class:

class Example {}

// extension function declaration
fun Example.printHelloWorld() { println("Hello World!") }

// extension function usage
Example().printHelloWorld()
Smart casts
Yes
No
Keeps track of conditions inside if statements, safe casting automatically:
fun example(a: Any) {
  if (a is String) {
    println(a.length) // automatic cast to String
  }
}

Kotlin also provides safe and unsafe cast operators:
// unsafe "as" cast throws exceptions
val a: String = b as String
// safe "as?" cast returns null on failure
val c: String? = d as? String
Inline functions
Yes
No
Reduces overhead memory costs and improves speed by inlining function code (copying it to the call site): inline fun example().
Native support for delegation
Yes
No
Supports the delegation design pattern natively with the use of the by keyword: class Derived(b: Base) : Base by b.
Type aliases
Yes
No
Provides shortened or custom names for existing types, including functions and inner or nested classes: typealias ShortName = LongNameExistingType.
Non-private fields
No
Yes
Offers protected and default (also known as package-private) modifiers, in addition to public and private modifiers. Java has all four access modifiers, while Kotlin is missing protected and the default modifier.
Ternary operator
No
Yes
Replaces an if/else statement with simpler and more readable code:

if (firstExpression) { // if/else
  variable = secondExpression;
} else {
  variable = thirdExpression;
}

// ternary operator
variable = (firstExpression) ? secondExpression : thirdExpression;
Implicit widening conversions
No
Yes
Allows for automatic conversion from a smaller data type to a larger data type:

int i = 10;
long l = i; // first widening conversion: int to long
float f = l; // second widening conversion: long to float
Checked exceptions
No
Yes
Requires, at compile time, a method to catch exceptions with the throws keyword or handles exceptions with a try-catch block.

Note: Checked exceptions were intended to encourage developers to design robust software. However, they can create boilerplate code, make refactoring difficult, and lead to poor error handling when misused. Whether this feature is a pro or con depends on developer preference.

One topic intentionally omitted from this table is null safety in Kotlin versus Java, as it merits a more in-depth analysis.

Kotlin vs. Java: A Closer Look at Null Safety

In my view, non-nullability stands out as one of Kotlin’s most significant advantages. This feature saves developers valuable time by eliminating the need to handle NullPointerExceptions (which are classified as RuntimeExceptions).

In Java, any variable can be assigned a null value by default:

1
2
3
4
5
6
7
String x = null;
// Running this code throws a NullPointerException
try {
    System.out.println("First character: " + x.charAt(0));
} catch (NullPointerException e) {
    System.out.println("NullPointerException thrown!");
}

Kotlin, on the other hand, offers two distinct options: declaring a variable as either nullable or non-nullable:

1
2
3
4
5
6
7
8
9
var nonNullableNumber: Int = 1

// This line throws a compile-time error because you can't assign a null value
nonNullableNumber = null

var nullableNumber: Int? = 2

// This line does not throw an error since we used a nullable variable
nullableNumber = null

As a best practice, I generally default to non-nullable variables and minimize the use of nullable ones. The examples provided here are solely for demonstrating the differences between Kotlin and Java. Novice Kotlin developers should be wary of unnecessarily declaring variables as nullable (which can occur when converting Java code to Kotlin).

However, there are certain scenarios where using nullable variables in Kotlin is justified:

Scenario
Example
You are searching for an item in a list that is not there (usually when dealing with the data layer).
val list: List<Int> = listOf(1,2,3)
val searchResultItem = list.firstOrNull { it == 0 }
searchResultItem?.let { 
  // Item found, do something 
} ?: run { 
  // Item not found, do something
}
You want to initialize a variable during runtime, using lateinit.
lateinit var text: String

fun runtimeFunction() { // e.g., Android onCreate
  text = "First text set"
  // After this, the variable can be used
}

When I initially started using Kotlin, I admit to overusing lateinit variables. However, I’ve since drastically reduced their usage, primarily reserving them for defining view bindings and variable injections in Android development:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Inject // With the Hilt library, this is initialized automatically
lateinit var manager: SomeManager

lateinit var viewBinding: ViewBinding

fun onCreate() { // i.e., Android onCreate

  binding = ActivityMainBinding.inflate(layoutInflater, parentView, true)
  // ...
}

In summary, Kotlin’s approach to null safety offers increased flexibility and a superior developer experience compared to Java.

Shared Feature Discrepancies: Navigating Between Java and Kotlin

Although each language boasts unique features, Kotlin and Java share a substantial number of features as well. Understanding their subtle differences is crucial for smooth transitions between the two languages. Let’s explore four common concepts that function differently in Kotlin and Java:

Feature
Java
Kotlin
Data transfer objects (DTOs)
Java records, which hold information about data or state and include toString, equals, and hashCode methods by default, have been available since Java SE 15:

public record Employee(
    int id,
    String firstName,
    String lastName
)
Kotlin data classes function similarly to Java records, with toString, equals, and copy methods available:

data class Employee(
    val id: Int,
    val firstName: String,
    val lastName: String
)
Lambda expressions
Java lambda expressions (available since Java 8) follow a simple parameter -> expression syntax, with parentheses used for multiple parameters: (parameter1, parameter2) -> { code }:

ArrayList<Integer> ints =
  new ArrayList<>();
ints.add(5);
ints.add(9);
ints.forEach( (i) ->
  { System.out.println(i); } );
Kotlin lambda expressions follow the syntax { parameter1, parameter2 -> code } and are always surrounded by curly braces:

var p: List<String> =
    listOf("firstPhrase", "secondPhrase")
  val isShorter = { s1: String,
    s2: String -> s1.length < s2.length }
  println(isShorter(p.first(), p.last()))
Java threads make concurrency possible, and the java.util.concurrency package allows for easy multithreading through its utility classes. The Executor and ExecutorService classes are especially beneficial for concurrency. (Project Loom also offers lightweight threads.)
Kotlin coroutines, from the kotlinx.coroutines library, facilitate concurrency and include a separate library branch for multithreading. The memory manager in Kotlin 1.7.20 and later versions reduces previous limitations on concurrency and multithreading for developers moving between iOS and Android.
Static behavior in classes
Java static members facilitate the sharing of code among class instances and ensure that only a single copy of an item is created. The static keyword can be applied to variables, functions, blocks, and more:

class Example {
    static void f() {/*...*/}
}
Kotlin companion objects offer static behavior in classes, but the syntax is not as straightforward:

class Example {
    companion object {
        fun f() {/*...*/}
    }
}

Naturally, Kotlin and Java also differ in their syntax. While a comprehensive discussion of every syntax difference is beyond the scope of this article, examining loops should provide a glimpse into the overall situation:

Loop Type
Java
Kotlin
for, using in
for (int i=0; i<=5; i++) {
    System.out.println("printed 6 times");
}
for (i in 0..5) {
    println("printed 6 times")
}
for, using until
for (int i=0; i<5; i++) {
  System.out.println("printed 5 times");
}
for (i in 0 until 5) {
  println("printed 5 times")
}
forEach
List<String> list = Arrays.asList("first", "second");

for (String value: list) {
  System.out.println(value);
}
var list: List<String> =
  listOf("first", "second")

list.forEach {
  println(it)
}
while
int i = 5;
while (i > 0) {
  System.out.println("printed 5 times");
  i--;
}
var i = 5
while (i > 0) {
  println("printed 5 times")
  i--
}

A thorough understanding of Kotlin’s features will greatly facilitate transitions between Kotlin and Java.

Android Project Planning: Factors Beyond Language Choice

We’ve covered numerous crucial factors to consider when choosing between Kotlin and Java in a general-purpose context. However, no Kotlin versus Java analysis would be complete without addressing the elephant in the room: Android.

If you’re starting an Android project from scratch and pondering whether to use Java or Kotlin, the answer is clear: choose Kotlin, Google’s preferred language for Android development.

However, this question becomes less relevant for existing Android applications. Based on my experience working with a diverse clientele, the more pressing questions are: How are you addressing technical debt? and How are you prioritizing developer experience (DX)?

So, how are you tackling technical debt? If your Android app is still using Java in 2023, your company is likely focused on pushing out new features rather than confronting technical debt. This is understandable, given the competitive market and the demand for rapid app update cycles. However, technical debt has insidious consequences: it increases costs with each update as engineers are forced to work around unstable and difficult-to-refactor code. Companies can easily find themselves trapped in a vicious cycle of mounting technical debt and escalating costs. It may be worthwhile to pause and invest in long-term solutions, even if it entails large-scale code refactoring or migrating your codebase to a modern language like Kotlin.

And how are you supporting your developers through DX? Developers need support throughout their careers:

  • Junior developers thrive with access to appropriate resources.
  • Mid-level developers grow through opportunities to lead and mentor.
  • Senior developers require the autonomy to design and implement elegant code.

Prioritizing DX for senior developers is particularly crucial, as their expertise has a cascading effect on the entire engineering team. Senior developers are intrinsically motivated to learn and experiment with the latest technologies. Staying abreast of emerging trends and language releases enables your team members to reach their full potential. While this holds true regardless of the chosen language, different languages evolve at different paces: with rapidly evolving languages like Kotlin, an engineer working on legacy code can fall behind in less than a year; with mature languages like Java, this process takes longer.

Kotlin and Java: Two Powerful Tools in the Developer’s Arsenal

While Java boasts a wide range of applications, Kotlin has undoubtedly usurped its position as the preferred language for developing new Android apps. Google has thrown its full weight behind Kotlin, prioritizing it in their latest technologies. Developers working on existing apps should consider incorporating Kotlin into any new code—IntelliJ provides an automatic Java to Kotlin tool—and should carefully weigh factors beyond the initial question of language choice.


The editorial team of the Toptal Engineering Blog expresses its sincere gratitude to Thomas Wuillemin for reviewing the code samples and other technical content presented in this article.

Licensed under CC BY-NC-SA 4.0