Skip to content

Kotlin and Java in 2026: What's actually changed

Why I’m writing this

I’ve been using Java for 15 years and Kotlin for the last 5. With Java 21 LTS and Kotlin 2.0 both out, I wanted to figure out what’s actually changed and whether I should switch my projects. This post covers the updates that matter in practice, not just feature lists.

Quick background: Java vs Kotlin

Java

  • Site: https://openjdk.org
  • Core idea: Write Once, Run Anywhere
  • Focus: Backward compatibility above all else
  • Maintained by: OpenJDK community (Oracle leads, contributions from Google, Amazon, IBM, etc.)

What Java does well:

  • Old code still runs on new versions (I have Java 8 code running on Java 21 with zero changes)
  • Biggest ecosystem on JVM - libraries for everything
  • Enterprise support everywhere (banks, insurance, e-commerce)
  • Performance keeps getting better without breaking anything

Downside:

  • Verbose compared to modern languages
  • No compile-time null safety (NPEs still happen at runtime)
  • More boilerplate code

Kotlin

  • Site: https://kotlinlang.org
  • Core idea: Fix Java’s pain points while staying compatible
  • Focus: Null safety, coroutines, multi-platform
  • Maintained by: JetBrains + open source community
  • Official language for Android (Google, 2019)

What Kotlin does well:

  • Null safety at compile time (NPEs basically disappear)
  • Coroutines make async code readable (no callback hell)
  • 100% compatible with Java - call Java from Kotlin and vice versa
  • Kotlin Multiplatform (KMP) - share code between JVM, JS, iOS, Android
  • Less boilerplate (data classes, type inference, extension functions)

Downside:

  • Smaller ecosystem than Java for niche stuff
  • KMP and advanced coroutines take time to learn

What’s new in Java 21+

Java moved to a 6-month release cycle with LTS versions every 2-3 years. Java 21 (LTS, 2023) is the current stable version, with Java 22 and 23 adding incremental improvements.

Features that actually matter:

Virtual Threads (JEP 444) Lightweight threads that let you run millions of concurrent requests without the memory overhead of OS threads. I tested this on a Spring Boot service - went from ~200 threads handling 5k requests to 10k virtual threads handling 50k requests. Same hardware, no code changes.

Pattern Matching for Switch (JEP 441) Cleans up the if-else chains and type checks. Before:

if (obj instanceof String s) {
return s.toUpperCase();
} else if (obj instanceof Integer i) {
return i * 2;
}

After:

return switch (obj) {
case String s -> s.toUpperCase();
case Integer i -> i * 2;
default -> "unknown";
};

Sequenced Collections (JEP 431) Finally, a consistent way to get first/last elements from any ordered collection. No more list.get(0) and list.get(list.size() - 1).

String Templates (JEP 459, preview in 21, enhanced in 22/23) Safe string interpolation for SQL, JSON, etc. No more “SELECT * FROM users WHERE name = ’” + name + ”’” - the template handles escaping.

Foreign Function & Memory API (JEP 454) Replaces JNI with a safer way to call native code and work with off-heap memory.

Unnamed Patterns & Variables (JEP 443) Use _ for variables you don’t care about:

try (var _ = ScopedContext.acquire()) {
// do stuff
} // automatically closes

What’s new in Kotlin 2.0+

Kotlin 2.0 (2024) was a major release. The K2 compiler rewrite and stable KMP are the big changes.

Stable KMP (Kotlin Multiplatform) KMP is no longer experimental. You can write shared business logic once and compile to JVM, JS, Native (iOS/macOS/Linux), Android, and WebAssembly. I use this for a utility library - one codebase instead of maintaining separate Java and JavaScript versions.

K2 Compiler 20-50% faster compilation. On a large project (~500k lines), clean build went from 8 minutes to 5. Incremental builds are noticeably snappier. Better error messages too.

Context Receivers (stable in 2.0) Lets you scope functions and properties without passing context through every function call. Useful for dependency injection, database transactions, DSLs.

Data Object (1.9+) Type-safe singletons without static final:

data object Config {
val apiUrl = "https://api.example.com"
val timeout = 30000
}

Java 21+ Interoperability Full support for Java’s Virtual Threads, Pattern Matching, and Sequenced Collections. Kotlin code can use these features directly.

Better Coroutines & Flow (2.0/2.1) Structured concurrency improvements, better cancellation, optimized Flow operators. Coroutine debugging in IntelliJ is actually usable now.

Java-Kotlin interoperability in practice

Kotlin and Java compile to the same bytecode. You can call Java from Kotlin and Kotlin from Java without any adapters or wrappers.

Kotlin calling Java

Java code:

public class StringUtils {
public static String toUpperCase(String input) {
if (input == null) return "";
return input.toUpperCase();
}
public String concatenate(String a, String b) {
return a + " " + b;
}
}

Kotlin calls it:

fun main() {
// Static method
val upper = StringUtils.toUpperCase("kotlin")
println(upper) // KOTLIN
// Instance method
val util = StringUtils()
val combined = util.concatenate("Java", "Kotlin")
println(combined) // Java Kotlin
// Kotlin null safety handles Java's nullable returns
val nullUpper = StringUtils.toUpperCase(null)
println(nullUpper) // (empty string, no NPE)
}

Java calling Kotlin

Kotlin code:

fun multiply(a: Int, b: Int): Int = a * b
data class User(val name: String, val age: Int, val email: String?)
fun String.reverseKotlin(): String = this.reversed()

Java calls it:

// Top-level function compiles to static method
int product = KotlinUtilsKt.multiply(5, 6); // 30
// Data class works like a Java class
KotlinUtils.User user = new KotlinUtils.User("Alice", 30, null);
System.out.println(user.getName()); // Alice
// Extension function compiles to static method
String reversed = KotlinUtilsKt.reverseKotlin("Java"); // avaJ

Adding Kotlin to a Java project

If you have an existing Java project (Maven or Gradle), you can add Kotlin without restructuring anything.

Gradle setup:

plugins {
id 'java'
id 'org.jetbrains.kotlin.jvm' version '2.0.0'
}
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.0.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0'
}
sourceSets {
main {
kotlin { srcDirs 'src/main/kotlin' }
java { srcDirs 'src/main/java' }
}
}
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}

Maven setup:

<properties>
<kotlin.version>2.0.0</kotlin.version>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
</plugins>
</build>

Then create src/main/kotlin and src/test/kotlin directories. Your Java and Kotlin code compile together.

Comparison table

FeatureJava 21+Kotlin 2.0+
SyntaxVerbose, explicitConcise, type inference
Null safetyRuntime NPEsCompile-time checks
ConcurrencyVirtual ThreadsCoroutines (works with Virtual Threads too)
MultiplatformNoYes (KMP)
Backward compatibilityDecades of compatibility100% Java compatible
EcosystemLargest on JVMFull access to Java ecosystem
Learning curveEasy (lots of resources)Easy for Java devs (1-2 weeks)
Compiler speedMature, fastK2 is 20-50% faster than before

When to use what

Legacy enterprise systems

Stick with Java 21+ - your existing code keeps working. Upgrade to Java 21 to get Virtual Threads and Pattern Matching for free performance gains.

Add Kotlin incrementally - write new features in Kotlin within the same project to reduce boilerplate.

New Spring Boot backend

Kotlin 2.0+ if your team is open to it - null safety and coroutines make backend code cleaner. Spring Boot has first-class Kotlin support.

Java 21+ if your team only knows Java and doesn’t want to learn right now - Virtual Threads and Pattern Matching are solid improvements.

Android development

Kotlin - Google’s official language. Java still works but gets no new Android features.

Cross-platform (JVM/JS/iOS/Android)

Kotlin KMP - share business logic across platforms. UI is still platform-specific, but you write validation, data models, networking once.

Reactive/async systems

Kotlin coroutines - simpler than CompletableFuture/Virtual Threads for sequential async code. Coroutines integrate with Java’s Virtual Threads too.

Team only knows Java

Start with Java 21+ and add Kotlin gradually - the interoperability is seamless. IntelliJ has a Java-to-Kotlin converter that helps learning.

Real examples

Example 1: REST endpoint (Spring Boot 3.2+)

Same endpoint in Java 21 and Kotlin 2.0:

Java 21 with Virtual Threads:

@RestController
class UserController {
private static final Map<Long, User> USERS = Map.of(
1L, new User(1L, "Alice", 30, "[email protected]"),
2L, new User(2L, "Bob", 25, null)
);
@GetMapping("/users/{id}")
public UserResponse getUser(@PathVariable Long id) {
return Optional.ofNullable(USERS.get(id))
.map(user -> {
String email = switch (user.getEmail()) {
case null -> "[email protected]";
case String e -> e;
};
return new UserResponse(user.getId(), user.getName(), user.getAge(), email);
})
.orElseThrow(() -> new IllegalArgumentException("User not found: " + id));
}
record User(Long id, String name, Integer age, String email) {}
record UserResponse(Long id, String name, Integer age, String email) {}
}

Kotlin 2.0 with Coroutines:

@RestController
class UserController {
private val USERS = mapOf(
1L to User(1L, "Alice", 30, "[email protected]"),
2L to User(2L, "Bob", 25, null)
)
@GetMapping("/users/{id}")
suspend fun getUser(@PathVariable id: Long): UserResponse {
val user = USERS[id] ?: throw IllegalArgumentException("User not found: $id")
val email = user.email ?: "[email protected]"
return UserResponse(user.id, user.name, user.age, email)
}
data class User(val id: Long, val name: String, val age: Int, val email: String?)
data class UserResponse(val id: Long, val name: String, val age: Int, val email: String)
}

The Kotlin version is shorter because:

  • No Optional wrapper (null safety built-in)
  • Elvis operator ?: handles nulls cleanly
  • Data classes generate equals/hashCode/toString automatically
  • suspend functions for async (no explicit async/await)

Both versions run identically at runtime - same JVM bytecode.

Example 2: KMP shared library

Goal: Validation library that works on JVM and JavaScript without duplicating code.

KMP project structure:

kmp-utils/
├── src/
│ ├── commonMain/kotlin/Utils.kt # Shared code
│ ├── jvmMain/kotlin/ # JVM-specific
│ ├── jsMain/kotlin/ # JS-specific
│ └── commonTest/kotlin/ # Shared tests
└── build.gradle.kts

Shared code (runs on JVM and JS):

fun isValidEmail(email: String): Boolean {
val emailPattern = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$".toRegex()
return emailPattern.matches(email)
}
data class ValidatedUser(
val name: String,
val email: String,
val isEmailValid: Boolean
)
fun validateUser(name: String, email: String): ValidatedUser {
require(name.isNotBlank()) { "Name cannot be blank" }
return ValidatedUser(name, email, isValidEmail(email))
}

Use from JVM:

fun main() {
val user = validateUser("Alice", "[email protected]")
println(user) // ValidatedUser(Alice, [email protected], true)
}

Use from JavaScript (compiled):

import { validateUser } from './kmp-utils.js';
const user = validateUser("Bob", "[email protected]");
console.log(user); // { name: 'Bob', email: '[email protected]', isEmailValid: true }

No Java equivalent - you’d maintain two separate codebases.

Summary

Java 21+ strengths

  • Backward compatibility - old code runs forever
  • Virtual Threads - massive concurrency improvement
  • Ecosystem - libraries for everything
  • Enterprise support - 8+ years LTS support
  • Incremental updates - new features without breaking changes

Kotlin 2.0+ strengths

  • Null safety - NPEs gone at compile time
  • KMP - one codebase for multiple platforms
  • Coroutines - async code that reads sequentially
  • K2 compiler - faster compilation
  • Java interop - use any Java library directly

The reality

Java and Kotlin aren’t competitors - they’re complementary. Java gives you stability and backward compatibility. Kotlin gives you productivity and multi-platform.

Practical approach:

  • Keep legacy Java systems on Java 21+ - get Virtual Threads for free
  • Write new Spring Boot backends in Kotlin 2.0+ - null safety and coroutines pay off quickly
  • Use KMP for shared business logic across platforms
  • Add Kotlin to existing Java projects incrementally - no big rewrite needed

The JVM ecosystem is in good shape. Pick the right tool for your situation and you can’t go wrong.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments