# Java to Kotlin Converter: 10 Critical Pitfalls to Review for Correctness and Efficiency

> Automated Java to Kotlin conversion has pitfalls. Review nullability, mutability, exceptions, and visibility for correct, efficient, idiomatic Kotlin code.

- Repository: [JetBrains/kotlin](https://github.com/jetbrains/kotlin)
- Tags: deep-dive
- Published: 2026-02-19

---

**The automated Java to Kotlin converter in the JetBrains/kotlin repository produces syntactically valid Kotlin code but leaves nullability, collection mutability, checked exceptions, and visibility semantics unresolved, requiring manual review to ensure idiomatic and type-safe results.**

The Kotlin compiler ships with an automatic Java to Kotlin converter that powers the "Convert Java File to Kotlin File" action in IntelliJ IDEA. While this tool handles the mechanical translation of syntax, it relies on `JavaToKotlinClassMap` and `JavaToKotlinClassMapper` to map Java types to their Kotlin equivalents without full semantic analysis. This architectural limitation means certain Java patterns translate to platform types, mutable collections, or top-level functions that may not reflect your actual API contract.

## How the Java to Kotlin Converter Works

Before diving into specific pitfalls, understanding the converter's architecture clarifies why these issues occur. The conversion process relies on two core components in the JetBrains/kotlin codebase:

* **`JavaToKotlinClassMap`** – Located in [`core/compiler.common.jvm/src/org/jetbrains/kotlin/builtins/jvm/JavaToKotlinClassMap.kt`](https://github.com/JetBrains/kotlin/blob/main/core/compiler.common.jvm/src/org/jetbrains/kotlin/builtins/jvm/JavaToKotlinClassMap.kt), this class maintains a static mapping of Java platform types to their Kotlin equivalents (e.g., `java.util.List` → `kotlin.collections.List`). It also contains the `mutabilityMappings` list that determines how Java collections map to read-only versus mutable Kotlin interfaces.

* **`JavaToKotlinClassMapper`** – Found in [`core/descriptors.jvm/src/org/jetbrains/kotlin/builtins/jvm/JavaToKotlinClassMapper.kt`](https://github.com/JetBrains/kotlin/blob/main/core/descriptors.jvm/src/org/jetbrains/kotlin/builtins/jvm/JavaToKotlinClassMapper.kt), this runtime façade resolves the mappings, handles mutability decisions via methods like `isMutable`, and provides utility methods used by the converter.

The converter walks the Java AST, applies these mappings, and emits Kotlin code. Because the conversion is **syntactic** and does not involve full semantic analysis of the target Kotlin environment, several patterns translate incorrectly or sub-optimally.

## Critical Pitfalls to Review After Conversion

When using the automated Java to Kotlin converter, manually review the generated code for these ten specific patterns to ensure correctness and efficiency.

### Nullability and Platform Types

Java types lack nullability annotations. The converter therefore produces **platform types** (e.g., `String!`) which are treated as "unknown" nullability.

*Why this happens:* `JavaToKotlinClassMap` only maps class identifiers; it does not attach nullability information from JSR-305 or other annotations.

*Risk:* Unchecked null dereferences and unnecessary defensive null checks throughout the codebase.

**Manual check:** Add explicit `?` for nullable types or non-null assertions (`!!`) where guaranteed, or annotate the original Java code with `@Nullable`/`@NotNull` before conversion to guide the tool.

### Collection Mutability Mismatches

Java collections are mutable by default, but Kotlin distinguishes between **read-only** (`List`) and **mutable** (`MutableList`) interfaces.

*Why this happens:* `JavaToKotlinClassMap` contains a `mutabilityMappings` list (e.g., `Iterable` ↔ `MutableIterable`). The mapper defaults to the mutable variant to preserve Java semantics.

*Risk:* Generated code may expose mutable collections in APIs that should be immutable, allowing unintended modifications.

**Manual check:** Verify whether the target Kotlin API truly requires mutability. Replace `MutableList` with `List` and use factory functions like `listOf()` or `mapOf()` if the collection is never modified after creation.

### Raw Types and Unchecked Generics

Java code often uses raw types (`List` without a generic argument). The converter substitutes `List<Any?>` which can hide type-safety problems.

*Why this happens:* The converter cannot infer generic arguments from usage alone without full type resolution.

*Risk:* Runtime `ClassCastException`s and loss of compile-time safety.

**Manual check:** Add appropriate generic parameters after conversion, or introduce type aliases to preserve the original type intent.

### Static Members Become Top-Level Declarations

Java static fields and methods are mapped to **top-level** Kotlin declarations (or companion objects, depending on context).

*Why this happens:* Kotlin does not have static members; the mapper uses heuristics found in the `KotlinToJava` mappings to determine placement.

*Risk:* Name clashes with other files, hidden visibility differences, and loss of encapsulation when utility functions are scattered globally.

**Manual check:** Prefer placing static members inside an `object` or `companion object` that reflects the original class's responsibility. Add `@JvmStatic` if Java interoperability requires static access.

### SAM Conversion and Lambda Semantics

Java's Single-Abstract-Method (SAM) interfaces are automatically turned into Kotlin lambdas.

*Why this happens:* The mapper detects `java.lang.Runnable` and other functional interfaces via `JavaToKotlinClassMapper.isMutable` and similar checks.

*Risk:* Over-eager lambda conversion can change semantics when the SAM interface also defines additional members (e.g., `java.util.concurrent.Callable` with `cancel`).

**Manual check:** Ensure that any additional methods (default or otherwise) are still accessible; if not, keep the explicit implementation class rather than using a lambda.

### Disappearing Checked Exceptions

Kotlin does **not** have checked exceptions. The converter simply removes `throws` clauses.

*Why this happens:* Kotlin's type system does not model checked exceptions, so the mapper discards this information.

*Risk:* Call-sites lose the contractual information that a method may throw, potentially hiding error handling requirements.

**Manual check:** Add the `@Throws(IOException::class)` annotation (substituting the appropriate exception type) to maintain API contracts and Java interoperability.

### Method Overload Resolution Differences

Java permits overloads that differ only by generic type erasure, while Kotlin's overload resolution is stricter. The converter may pick the *wrong* overload or generate ambiguous calls.

*Why this happens:* The mapper does not simulate Kotlin's overload resolution algorithm during conversion.

*Risk:* Compilation errors or unintended method calls at runtime.

**Manual check:** Disambiguate calls using named arguments or cast the receiver to the desired type to ensure the correct overload is invoked.

### Annotation Mapping Gaps

Annotations such as `@SuppressWarnings` are translated to `@Suppress`, but others (e.g., `@Generated`) may be dropped.

*Why this happens:* The conversion only knows a limited set of annotation mappings hardcoded in the converter logic.

*Risk:* Loss of metadata that tools rely on (e.g., code generators, coverage tools, or serialization frameworks).

**Manual check:** Re-add or replace missing annotations with Kotlin equivalents (`@JvmName`, `@JvmStatic`, `@JvmOverloads`, etc.).

### Visibility and Package-Private Members

Java package-private fields become **public** in Kotlin because Kotlin does not have package-private visibility.

*Why this happens:* The mapper lacks a direct representation for package-private access.

*Risk:* Unintended API exposure when internal implementation details become publicly accessible.

**Manual check:** Change visibility to `internal` or move the member into the appropriate module to emulate package-private behavior.

### Enum Translation Quirks

Java `enum` constants are mapped to Kotlin enum entries, but any **anonymous class bodies** inside enum constants are turned into separate classes, losing the syntactic sugar.

*Why this happens:* Kotlin does not support enum constant specific class bodies directly.

*Risk:* Behavioural differences if the enum constant overrides methods or maintains state.

**Manual check:** Refactor such enums into sealed classes or regular classes with object instances to preserve behavior.

## Code Examples

The following examples demonstrate common conversion scenarios and their manual corrections.

### Example 1: Nullability and Platform Types

Original Java:

```java
// Java
String getName() {
    return possiblyNull();
}

```

Automated conversion:

```kotlin
fun getName(): String! = possiblyNull()

```

Manual fix:

```kotlin
fun getName(): String? = possiblyNull()   // if null allowed
// or
fun getName(): String = possiblyNull()!! // if non-null guaranteed

```

### Example 2: Collection Mutability

Original Java:

```java
// Java
List<String> names = new ArrayList<>();
names.add("Alice");

```

Automated conversion:

```kotlin
val names: MutableList<String> = arrayListOf()
names.add("Alice")

```

Manual fix (read-only collection):

```kotlin
val names = listOf("Alice")   // immutable list if no further mutations

```

### Example 3: Static Members

Original Java:

```java
// Java
class Utils {
    public static int max(int a, int b) { return Math.max(a, b); }
}

```

Automated conversion:

```kotlin
fun max(a: Int, b: Int): Int = Math.max(a, b)

```

Manual fix:

```kotlin
object Utils {
    fun max(a: Int, b: Int): Int = kotlin.math.max(a, b)
}

```

### Example 4: Checked Exceptions

Original Java:

```java
// Java
void read() throws IOException { ... }

```

Automated conversion:

```kotlin
fun read() { ... }   // throws clause removed

```

Manual fix:

```kotlin
@Throws(IOException::class)
fun read() { ... }

```

## Key Source Files in the Kotlin Compiler

Understanding the converter's implementation helps identify where these limitations originate. The following files in the JetBrains/kotlin repository define the type mapping and conversion logic:

| File | Purpose | Link |
|------|---------|------|
| [`core/compiler.common.jvm/src/org/jetbrains/kotlin/builtins/jvm/JavaToKotlinClassMap.kt`](https://github.com/JetBrains/kotlin/blob/main/core/compiler.common.jvm/src/org/jetbrains/kotlin/builtins/jvm/JavaToKotlinClassMap.kt) | Central static mapping of Java → Kotlin class identifiers, mutability tables, and primitive wrappers. | [JavaToKotlinClassMap.kt](https://github.com/JetBrains/kotlin/blob/master/core/compiler.common.jvm/src/org/jetbrains/kotlin/builtins/jvm/JavaToKotlinClassMap.kt) |
| [`core/descriptors.jvm/src/org/jetbrains/kotlin/builtins/jvm/JavaToKotlinClassMapper.kt`](https://github.com/JetBrains/kotlin/blob/main/core/descriptors.jvm/src/org/jetbrains/kotlin/builtins/jvm/JavaToKotlinClassMapper.kt) | Runtime façade that resolves the mappings, provides mutability checks via `isMutable`, and is used by the converter. | [JavaToKotlinClassMapper.kt](https://github.com/JetBrains/kotlin/blob/master/core/descriptors.jvm/src/org/jetbrains/kotlin/builtins/jvm/JavaToKotlinClassMapper.kt) |
| [`compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/platform/JvmPlatformConfigurator.kt`](https://github.com/JetBrains/kotlin/blob/main/compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/platform/JvmPlatformConfigurator.kt) | Registers the `JavaToKotlinClassMapper` as the platform‑to‑Kotlin mapper for the JVM backend. | [JvmPlatformConfigurator.kt](https://github.com/JetBrains/kotlin/blob/master/compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/platform/JvmPlatformConfigurator.kt) |
| [`libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinGradlePluginIT.kt`](https://github.com/JetBrains/kotlin/blob/main/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinGradlePluginIT.kt) | Integration test exercising the Java‑to‑Kotlin conversion during a Gradle build (`testConvertJavaToKotlin`). | [KotlinGradlePluginIT.kt](https://github.com/JetBrains/kotlin/blob/master/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinGradlePluginIT.kt) |
| [`plugins/kapt/kapt-compiler/src/org/jetbrains/kotlin/kapt/stubs/KaptStubConverter.kt`](https://github.com/JetBrains/kotlin/blob/main/plugins/kapt/kapt-compiler/src/org/jetbrains/kotlin/kapt/stubs/KaptStubConverter.kt) | Example usage of the mapper for converting annotations and stub code during KAPT processing. | [KaptStubConverter.kt](https://github.com/JetBrains/kotlin/blob/master/plugins/kapt/kapt-compiler/src/org/jetbrains/kotlin/kapt/stubs/KaptStubConverter.kt) |

## Summary

When using the automated Java to Kotlin converter, treat the output as a starting point rather than production-ready code. Focus your manual review on these critical areas:

- **Nullability**: Replace platform types (`String!`) with explicit nullable (`String?`) or non-null (`String`) types based on actual usage.
- **Collection mutability**: Change `MutableList` to `List` where the collection is never modified after creation.
- **Static members**: Move top-level functions into `object` or `companion object` declarations to preserve encapsulation.
- **Checked exceptions**: Add `@Throws` annotations to methods that declare checked exceptions in Java to maintain API contracts.
- **Visibility**: Convert package-private members to `internal` visibility to prevent unintended public API exposure.
- **Raw types**: Specify generic type parameters explicitly instead of using `Any?` placeholders.
- **SAM interfaces**: Verify that lambda conversions do not lose access to additional interface methods beyond the single abstract method.

## Frequently Asked Questions

### Does the Java to Kotlin converter handle null safety automatically?

No. Because Java lacks nullability annotations by default, the converter generates **platform types** (e.g., `String!`) that represent unknown nullability. You must manually review these and add `?` for nullable types or remove the platform type entirely for non-null guarantees. Annotating the original Java code with `@Nullable` or `@NotNull` before conversion helps the converter infer the correct nullability.

### Why does the converter generate MutableList instead of List?

The converter uses `JavaToKotlinClassMap` and `JavaToKotlinClassMapper` to translate Java collection types. Since Java collections are inherently mutable and Kotlin distinguishes between read-only and mutable interfaces, the converter defaults to the mutable variant (e.g., `MutableList`) to preserve Java semantics. If your code only reads the collection, manually change these to `List` or use `listOf()` factory functions.

### What happens to checked exceptions when converting Java to Kotlin?

Kotlin does not have checked exceptions, so the converter simply strips `throws` clauses from method signatures. This removes the contractual obligation that callers must handle specific exceptions. To maintain API documentation and Java interoperability, manually add the `@Throws(IOException::class)` annotation (or appropriate exception type) to the converted Kotlin function.

### Are static methods preserved as static in Kotlin?

No. Kotlin does not support static members directly. The converter typically moves Java static methods and fields to **top-level** functions and properties in the generated Kotlin file, or places them in a `companion object`. To preserve logical grouping and encapsulation, review these conversions and consider moving top-level functions into an `object` declaration or adding `@JvmStatic` if Java interoperability is required.