Kotlin vs. Java: All-purpose Uses and Android Apps

It’s true that Java lost the Android battle to Kotlin, which is now Google’s preferred language and therefore better suited to new mobile apps. But both Kotlin and Java offer many strengths as general-purpose languages, and it’s important for developers to understand the language differences, for purposes such as migrating from Java to Kotlin. In this article, we will break down Kotlin’s and Java’s differences and similarities so you can make informed decisions and move seamlessly between the two.

Are Kotlin and Java Similar?

Indeed, the two languages have a lot in common from a high-level perspective. Both Kotlin and Java run on the Java Virtual Machine (JVM) instead of building directly to native code. And the two languages can call into each other easily: You can call Java code from Kotlin and Kotlin code from Java. Java can be used in server-side applications, databases, web front-end applications, embedded systems and enterprise applications, mobile, and more. Kotlin is similarly versatile: It targets the JVM , Android, JavaScript, and Kotlin/Native, and can also be used for server-side, web, and desktop development.

Java is a much more mature language than Kotlin, with its first release in 1996. Though Kotlin 1.0 was introduced much later, in 2016, Kotlin quickly became the official preferred language for Android development in 2019. Outside of Android, however, 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 EAB (Project Loom)

Kotlin 1.7 (alpha version of Kotlin K2 compiler)

Kotlin vs. Java: Performance and Memory

Before detailing Kotlin’s and Java’s features, we’ll examine their performance and memory consumption as these factors are generally important considerations for developers and clients.

Kotlin, Java, and the other JVM languages, although not equal, are fairly similar in terms of performance, at least when compared to languages in other compiler families like GCC or Clang. The JVM was initially designed to target embedded systems with limited resources in the 1990s. The related environmental requirements led to two main constraints:

  • Simple JVM bytecode: The current version of JVM, in which both Kotlin and Java are compiled, has only 205 instructions. In comparison, a modern x64 processor can easily support over 6,000 encoded instructions, depending on the counting method.
  • Runtime (versus compile-time) operations: The multiplatform approach (“Write once and run anywhere”) encourages runtime (instead of compile-time) optimizations. In other words, the JVM translates the bulk of its bytecode into instructions at runtime. However, to improve performance, you may use open-source implementations of the JVM, such as HotSpot, which pre-compiles the bytecode to run faster through the interpreter.

With similar compilation processes and runtime environments, Kotlin and Java have only minor performance differences resulting from their distinct features. For example:

  • Kotlin’s inline functions avoid a function call, improving performance, whereas Java invokes additional overhead memory.
  • Kotlin’s higher-order functions avoid Java lambda’s special call to InvokeDynamic, improving performance.
  • Kotlin’s generated bytecode contains assertions for nullity checks when using external dependencies, slowing performance compared to Java.

Now let’s turn to memory. It is true in theory that the use of objects for base types (i.e., Kotlin’s implementation) requires more allocation than primitive data types (i.e., Java’s implementation). However, in practice, Java’s bytecode uses autoboxing and unboxing calls to work with objects, which can add computational overhead when used in excess. For example, Java’s String.format method only takes objects as input, so formatting a Java int will box it in an Integer object before the call to String.format.

On the whole, there are no significant Java and Kotlin differences related to performance and memory. You may examine online benchmarks which show minor differences in micro-benchmarks, but these cannot be generalized to the scale of a full production application.

Unique Feature Comparison

Kotlin and Java have core similarities, but each language offers different, unique features. Since Kotlin became Google’s preferred language for Android development, I’ve found extension functions and explicit nullability to be the most useful features. On the other hand, when using Kotlin, the Java features that I miss 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 examine a more detailed breakdown of features available in Kotlin versus Java. You may follow along with my examples using the Kotlin Playground or a Java compiler for a more hands-on learning approach.

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.

There is one topic I’ve intentionally excluded from this table: null safety in Kotlin versus Java. This topic warrants a more detailed Kotlin to Java comparison.

Kotlin vs. Java: Null Safety

In my opinion, non-nullability is one of the greatest Kotlin features. This feature saves time because developers don’t have to handle NullPointerExceptions (which are RuntimeExceptions).

In Java, by default, you can assign a null value to any variable:

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!");
}

In Kotlin, on the other hand, we have two options, making a variable nullable or non-nullable:

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

I use non-nullable variables by default, and minimize the use of nullable variables for best practices; these Kotlin versus Java examples are meant to demonstrate differences in the languages. Kotlin beginners should avoid the trap of setting variables to be nullable without a purpose (this can also happen when you convert Java code to Kotlin).

However, there are a few cases where you would use nullable variables in Kotlin:

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
}

I was guilty of overusing lateinit variables when I first got started with Kotlin. Eventually, I stopped using them almost completely, except when defining view bindings and variable injections in Android:

@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)
  // ...
}

On the whole, null safety in Kotlin provides added flexibility and an improved developer experience compared to Java.

Shared Feature Differences: Moving Between Java and Kotlin

While each language has unique features, Kotlin and Java share many features too, and it is necessary to understand their peculiarities in order to transition between the two languages. Let’s examine four common concepts that operate 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()))

Concurrency

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. Kotlin 1.7.20’s new memory manager 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() {/*...*/}
    }
}

Of course, Kotlin and Java also have varying syntaxes. Discussing every syntax difference is beyond our scope, but a consideration of loops should give you an idea of 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--
}

An in-depth understanding of Kotlin features will assist in transitions between Kotlin and Java.

Android Project Planning: Additional Considerations

We’ve examined many important factors to think about when deciding between Kotlin and Java in a general-purpose context. However, no Kotlin versus Java analysis is complete without addressing the elephant in the room: Android. Are you making an Android application from scratch and wondering if you should use Java or Kotlin? Choose Kotlin, Google’s preferred Android language, without a doubt.

However, this question is moot for existing Android applications. In my experience across a wide range of clients, the two more important questions are: How are you treating tech debt? and How are you taking care of your developer experience (DX)?

So, how are you treating tech debt? If your Android app is using Java in 2022, your company is likely pushing for new features instead of dealing with tech debt. It’s understandable. The market is competitive and demands a fast turnaround cycle for app updates. But tech debt has a hidden effect: It causes increased costs with each update because engineers have to work around unstable code that is challenging to refactor. Companies can easily enter a never-ending cycle of tech debt and cost. It may be worth pausing and investing in long-term solutions, even if this means large-scale code refactors or updating your codebase to use a modern language like Kotlin.

And how are you taking care of your developers through DX? Developers require support across all levels of their careers:

  • Junior developers benefit from proper resources.
  • Mid-level developers grow through opportunities to lead and teach.
  • Senior developers require the power to architect and implement beautiful code.

Attention to DX for senior developers is especially important since their expertise trickles down and affects all engineers. Senior developers love to learn and experiment with the latest technologies. Keeping up with newer trends and language releases will allow your team members to reach their greatest potential. This is important regardless of the team’s language choice, though different languages have varying timelines: With young languages like Kotlin, an engineer working on legacy code can fall behind trends in less than one year; with mature languages like Java, it will take longer.

Kotlin and Java: Two Powerful Languages

While Java has a wide range of applications, Kotlin has undeniably stolen its thunder as the preferred language for the development of new Android apps. Google has put all of its efforts into Kotlin, and its new technologies are Kotlin-first. Developers of existing apps might consider integrating Kotlin into any new code—IntelliJ comes with an automatic Java to Kotlin tool—and should examine factors that reach beyond our initial question of language choice.


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

Further Reading on the Toptal Engineering Blog:


Like this post? Please share to your friends:
Leave a Reply

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: