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 closesWhat’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 methodint product = KotlinUtilsKt.multiply(5, 6); // 30
// Data class works like a Java classKotlinUtils.User user = new KotlinUtils.User("Alice", 30, null);System.out.println(user.getName()); // Alice
// Extension function compiles to static methodString reversed = KotlinUtilsKt.reverseKotlin("Java"); // avaJAdding 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
| Feature | Java 21+ | Kotlin 2.0+ |
|---|---|---|
| Syntax | Verbose, explicit | Concise, type inference |
| Null safety | Runtime NPEs | Compile-time checks |
| Concurrency | Virtual Threads | Coroutines (works with Virtual Threads too) |
| Multiplatform | No | Yes (KMP) |
| Backward compatibility | Decades of compatibility | 100% Java compatible |
| Ecosystem | Largest on JVM | Full access to Java ecosystem |
| Learning curve | Easy (lots of resources) | Easy for Java devs (1-2 weeks) |
| Compiler speed | Mature, fast | K2 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:
@RestControllerclass UserController { private static final Map<Long, User> USERS = Map.of( 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 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:
@RestControllerclass UserController { private val USERS = mapOf( 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") 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
Optionalwrapper (null safety built-in) - Elvis operator
?:handles nulls cleanly - Data classes generate equals/hashCode/toString automatically
suspendfunctions 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.ktsShared 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() {}Use from JavaScript (compiled):
import { validateUser } from './kmp-utils.js';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:
- 👨💻 Kotlin Official Website
- 👨💻 OpenJDK
- 👨💻 Java SE
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments