Kotlin에서 Java 호출하기

Kotlin에서 Java 호출하기 (Calling Java from Kotlin)

Kotlin은 Java 상호 운용성을 염두에 두고 설계되었습니다. 기존 Java 코드를 Kotlin에서 자연스럽게 호출할 수 있고, Kotlin 코드도 Java에서 꽤 매끄럽게 사용할 수 있죠. 이 섹션에서는 Kotlin에서 Java 코드를 호출하는 몇 가지 세부 사항을 설명할게요.

거의 모든 Java 코드는 아무 문제 없이 사용할 수 있습니다.

import java.util.*

fun demo(source: List<Int>) {
    val list = ArrayList<Int>()
    // Java 컬렉션에서 'for' 루프가 동작한다:
    for (item in source) {
        list.add(item)
    }
    // 연산자 관례도 동작한다:
    for (i in 0..source.size - 1) {
        list[i] = source[i] // get과 set이 호출된다
    }
}

출처: Kotlin 공식 문서

본문

Getter와 setter

Java의 getter·setter 관례를 따르는 메서드(get으로 시작하는 이름의 인자 없는 메서드, set으로 시작하는 이름의 단일 인자 메서드)는 Kotlin에서 속성으로 표현됩니다. 이런 속성을 **합성 속성(synthetic property)**이라고도 해요. Boolean 접근자 메서드(getter 이름이 is로 시작하고 setter 이름이 set으로 시작하는 경우)는 getter 메서드와 같은 이름을 가진 속성으로 표현됩니다.

import java.util.Calendar

fun calendarDemo() {
    val calendar = Calendar.getInstance()
    if (calendar.firstDayOfWeek == Calendar.SUNDAY) { // call getFirstDayOfWeek()
        calendar.firstDayOfWeek = Calendar.MONDAY // call setFirstDayOfWeek()
    }
    if (!calendar.isLenient) { // call isLenient()
        calendar.isLenient = true // call setLenient()
    }
}

위의 calendar.firstDayOfWeek는 합성 속성의 예시입니다. Java 클래스에 setter만 있고 getter가 없으면, Kotlin은 set-only 속성을 지원하지 않기 때문에 그것을 속성으로 볼 수 없다는 점에 유의하세요.

Java 합성 속성 참조

이 기능은 Experimental입니다. 언제든 제거되거나 변경될 수 있어요. 평가 목적으로만 사용하는 것을 권장합니다.

Kotlin 1.8.20부터 Java 합성 속성에 대한 참조를 만들 수 있습니다. 다음 Java 코드를 고려해 볼게요.

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

Kotlin은 항상 person.age라고 쓸 수 있게 해 줬는데, 여기서 age는 합성 속성이에요. 이제 Person::ageperson::age에 대한 참조도 만들 수 있습니다. name에도 마찬가지로 적용됩니다.

val persons = listOf(Person("Jack", 11), Person("Sofie", 12), Person("Peter", 11))
    persons
         // Java 합성 속성에 대한 참조 호출:
        .sortedBy(Person::age)
         // Kotlin 속성 문법을 통해 Java getter 호출:
        .forEach { person -> println(person.name) }

Java 합성 속성 참조 활성화 방법

이 기능을 활성화하려면 -language-version 2.1 컴파일러 옵션을 설정하세요. Gradle 프로젝트에서는 build.gradle(.kts)에 다음을 추가해 설정할 수 있어요.

tasks
    .withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask<*>>()
    .configureEach {
        compilerOptions
            .languageVersion
            .set(
                org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_1
            )
    }
tasks
    .withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask.class)
    .configureEach {
        compilerOptions.languageVersion
            = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_1
}

Kotlin 1.9.0 이전에는 이 기능을 활성화하려면 -language-version 1.9 컴파일러 옵션을 설정해야 했어요.

void를 반환하는 메서드

Java 메서드가 void를 반환하면 Kotlin에서 호출할 때는 Unit을 반환합니다. 만에 하나 누군가 그 반환 값을 사용하면, 값 자체가 미리 알려져 있어서(Unit이므로) Kotlin 컴파일러가 호출 지점에서 그것을 할당해요.

Kotlin 키워드인 Java 식별자 이스케이프하기

Kotlin 키워드 중 일부는 Java에서 유효한 식별자입니다: in, object, is 등이요. Java 라이브러리가 메서드에 Kotlin 키워드를 사용한다면, 백틱(`) 문자로 이스케이프해 그 메서드를 호출할 수 있어요.

foo.`is`(bar)

널 안전성과 플랫폼 타입

Java의 모든 참조는 null일 수 있어서, Java에서 온 객체에 Kotlin의 엄격한 널 안전성 요구를 적용하는 것은 비현실적입니다. Java 선언의 타입은 Kotlin에서 비-표기 가능(non-denotable) 타입으로 취급되며, 이를 플랫폼 타입(platform type)이라고 합니다. 비-표기 가능 타입은 코드에 명시적으로 쓸 수 없어요. 따라서 플랫폼 값을 Kotlin 변수에 할당할 때는 다음 중 하나를 할 수 있습니다.

  • 타입 추론에 의존하기. 이 경우 변수는 추론된 플랫폼 타입을 가져요.
  • 기대하는 타입을 고르기. Kotlin은 널 가능과 널 불가 타입을 모두 허용합니다.

이런 타입에는 널 검사가 완화되어, 안전성 보장이 Java와 같습니다(mapped types에서 자세히 봐요). 다음 예시를 고려해 보세요.

val list = ArrayList<String>() // non-null (constructor result)
list.add("Item")
val size = list.size // non-null (primitive int)
val item = list[0] // platform type inferred (ordinary Java object)

플랫폼 타입의 변수에서 메서드를 호출할 때 Kotlin은 컴파일 타임에 널 가능성 오류를 내지 않지만, 그 호출은 런타임에 실패할 수 있어요. null 포인터 예외나, Kotlin이 null의 전파를 막기 위해 생성하는 단언 때문에요.

item.substring(1) // 허용되지만, item == null이면 예외를 던진다

플랫폼 타입의 값은 널 가능·널 불가 Kotlin 타입 모두의 변수에 할당할 수 있어요. 하지만 그런 값을 널 불가 타입의 변수에 할당했는데 런타임에 값이 실제로 null이면, Kotlin은 NullPointerException을 던집니다. 이를 피하려면 Kotlin 코드에 명시적 널 가능성을 추가하세요.

val nullable: String? = item // 허용, 항상 동작
val notNull: String = item // 허용, 런타임에 실패할 수 있음

널 불가 타입을 고르면 컴파일러가 할당 시 단언을 내보냅니다. 이는 Kotlin의 널 불가 변수가 null을 담는 것을 막아 줍니다. 널이 아닌 값을 기대하는 Kotlin 함수에 플랫폼 값을 전달할 때와 다른 경우에도 단언이 생성됩니다. 전반적으로 컴파일러는 null이 프로그램을 통해 멀리 전파되는 것을 막기 위해 최선을 다하지만, 제네릭 때문에 그것을 완전히 없애는 것이 불가능한 경우도 있습니다.

플랫폼 타입 표기

앞서 언급했듯이 플랫폼 타입은 프로그램에서 명시적으로 언급할 수 없으므로, 언어에 그것을 위한 문법이 없어요. 그럼에도 컴파일러와 IDE는 때때로 그것을 표시해야 하므로(예: 오류 메시지나 파라미터 정보에서), 그것을 위한 기억 보조적인 표기법이 있습니다.

  • T!는 "T 또는 T?"를 뜻합니다.
  • (Mutable)Collection<T>!는 "T의 Java 컬렉션이 가변일 수도 있고 아닐 수도, 널 가능일 수도 아닐 수도 있음"을 뜻해요.
  • Array<(out) T>!는 "T(또는 T의 서브타입)의 Java 배열, 널 가능일 수도 아닐 수도 있음"을 뜻합니다.

오류 메시지나 IDE 툴팁에서 이 표기를 보면, Kotlin 변수에 명시적 타입 애노테이션을 추가해 널 안전성 검사를 복원하거나, 널 가능성 애노테이션을 사용해 근원에서 플랫폼 타입을 없애세요.

널 가능성 애노테이션

널 가능성 애노테이션이 있는 Java 타입은 플랫폼 타입이 아니라 실제 널 가능·널 불가 Kotlin 타입으로 표현됩니다. 컴파일러는 여러 종류의 널 가능성 애노테이션을 지원합니다.

  • JetBrains(org.jetbrains.annotations 패키지의 @Nullable@NotNull)
  • JSpecify(org.jspecify.annotations)
  • Android(com.android.annotationsandroid.support.annotations)
  • JSR-305(javax.annotation)
  • FindBugs(edu.umd.cs.findbugs.annotations)
  • Eclipse(org.eclipse.jdt.annotation)
  • Lombok(lombok.NonNull)
  • RxJava 3(io.reactivex.rxjava3.annotations)
  • Vert.x(io.vertx.codegen.annotations)

특정 널 가능성 애노테이션에 대한 널 가능성 불일치를 보고하도록 컴파일러에 지시할 수 있는 컴파일러 옵션이 있어요.

-Xnullability-annotations=@<package-name>:<report-level>

완전히 정규화된 널 가능성 애노테이션의 패키지 이름과 다음 보고 수준 중 하나를 지정하세요.

  • ignore — 널 가능성 불일치를 무시.
  • warn — 경고 보고.
  • strict — 오류 보고.

JSpecify는 기본적으로 strict 보고 수준을 사용하는 유일한 지원 종류이에요. 추가 구성 없이 널 가능성 애노테이션의 오류를 보고하는 데 사용하세요.

지원되는 널 가능성 애노테이션의 전체 목록은 Kotlin 컴파일러 소스 코드에서 확인할 수 있어요.

변경 가능성 애노테이션

Java 선언에 변경 가능성(mutability) 애노테이션을 붙여, 반환된 컬렉션이 Kotlin에서 읽기 전용인지 가변인지 지정할 수 있습니다. 다른 변경 가능성의 컬렉션 타입에 값을 할당하면 컴파일러가 타입 불일치를 보고해요. 진단 심각도는 특정 변경 가능성 애노테이션에 따라 달라집니다.

컴파일러는 여러 변경 가능성 애노테이션을 지원합니다.

  • kotlin.annotations.jvm.ReadOnly
  • kotlin.annotations.jvm.Mutable
  • org.jetbrains.annotations.Unmodifiable
  • org.jetbrains.annotations.UnmodifiableView

지원되는 변경 가능성 애노테이션의 전체 목록은 Kotlin 컴파일러 소스 코드에서 확인하세요.

타입 인자와 타입 파라미터 애노테이션

제네릭 타입의 타입 인자와 타입 파라미터에도 애노테이션을 붙여, 그것들에 대한 널 가능성 정보를 제공할 수 있어요.

이 섹션의 모든 예시는 org.jetbrains.annotations 패키지의 JetBrains 널 가능성 애노테이션을 사용합니다.

타입 인자

Java 선언의 다음 애노테이션들을 고려해 보세요.

@NotNull
Set<@NotNull String> toSet(@NotNull Collection<@NotNull String> elements) { ... }

그 결과는 Kotlin에서 다음 시그니처가 됩니다.

fun toSet(elements: (Mutable)Collection<String>) : (Mutable)Set<String> { ... }

타입 인자에 @NotNull 애노테이션이 없으면, 대신 플랫폼 타입을 얻습니다.

fun toSet(elements: (Mutable)Collection<String!>) : (Mutable)Set<String!> { ... }

Kotlin은 기반 클래스와 인터페이스의 타입 인자에 있는 널 가능성 애노테이션도 고려합니다. 예를 들어 아래 시그니처를 가진 Java 클래스 두 개가 있습니다.

public class Base<T> {}

public class Derived extends Base<@Nullable String> {}

Kotlin 코드에서 Base<String>이 가정되는 곳에 Derived 인스턴스를 전달하면 경고가 발생합니다.

fun takeBaseOfNotNullStrings(x: Base<String>) {}

fun main() {
    takeBaseOfNotNullStrings(Derived()) // warning: nullability mismatch
}

Derived의 상한(upper bound)은 Base<String?>로 설정되는데, 이것은 Base<String>과 다릅니다.

Kotlin의 Java 제네릭에 대해 더 배워 보세요.

타입 파라미터

기본적으로 Kotlin과 Java 모두에서 평범한 타입 파라미터의 널 가능성은 정의되지 않습니다. Java에서는 널 가능성 애노테이션으로 그것을 지정할 수 있어요. Base 클래스의 타입 파라미터에 애노테이션을 붙여 볼게요.

public class Base<@NotNull T> {}

Base에서 상속할 때 Kotlin은 널 불가 타입 인자나 타입 파라미터를 기대합니다. 그래서 다음 Kotlin 코드는 경고를 만듭니다.

class Derived<K> : Base<K> {} // warning: K has undefined nullability

상한 K : Any를 지정해 고칠 수 있어요.

Kotlin은 Java 타입 파라미터의 경계에 있는 널 가능성 애노테이션도 지원합니다. Base에 경계를 추가해 볼게요.

public class BaseWithBound<T extends @NotNull Number> {}

Kotlin은 이것을 다음과 같이 번역합니다.

class BaseWithBound<T : Number> {}

그래서 타입 인자나 타입 파라미터로 널 가능 타입을 전달하면 경고가 발생합니다.

타입 인자와 타입 파라미터의 애노테이션은 Java 8 타깃 이상에서 동작합니다. 이 기능은 널 가능성 애노테이션이 TYPE_USE 타깃을 지원해야 합니다(org.jetbrains.annotations는 버전 15 이상에서 지원).

널 가능성 애노테이션이 TYPE_USE 타깃 외에도 타입에 적용 가능한 다른 타깃을 지원하면, TYPE_USE가 우선합니다. 예를 들어 @NullableTYPE_USEMETHOD 타깃을 모두 가진다면, Java 메서드 시그니처 @Nullable String[] f()는 Kotlin에서 fun f(): Array<String?>!이 됩니다.

JSpecify 지원

Kotlin은 JSpecify 널 가능성 애노테이션을 지원합니다. 이것은 Java 널 가능성을 위한 통일된 애노테이션 세트를 제공해요. JSpecify를 사용하면 Java 선언에 대한 상세한 널 가능성 정보를 제공할 수 있어, Kotlin이 Java 코드로 작업할 때 널 안전성을 유지하는 데 도움이 됩니다.

Kotlin은 org.jspecify.annotations 패키지의 다음 애노테이션을 지원합니다.

  • @Nullable — 타입을 널 가능으로 표시.
  • @NonNull — 타입을 널 불가로 표시.
  • @NullMarked — 스코프(예: 클래스나 패키지) 안의 모든 타입을, 달리 애노테이션되지 않는 한 기본적으로 널 불가로 표시. 이 애노테이션은 지역 변수와 타입 변수(제네릭)에는 적용되지 않아요. 타입 변수는 특정 널 가능·널 불가 타입이 제공될 때까지 "널 무관(null-agnostic)" 상태로 남습니다.
  • @NullUnmarked@NullMarked의 효과를 뒤집어, 스코프 안의 모든 타입을 플랫폼 타입으로 만듭니다.

JSpecify 애노테이션이 있는 다음 Java 클래스를 고려해 보세요.

// Java
import org.jspecify.annotations.*;

@NullMarked
public class InventoryService {
    public String notNull() { return ""; }
    public @Nullable String nullable() { return null; }
}

Kotlin에서 이것들은 플랫폼 타입이 아니라 일반적인 널 가능·널 불가 타입으로 취급됩니다.

// Kotlin
fun test(inventory: InventoryService) {
   inventory.notNull().length // OK
   inventory.nullable().length // Error: only safe (?.) or non-null asserted (!!) calls are allowed
}

기본적으로 Kotlin 컴파일러는 JSpecify 애노테이션의 널 가능성 불일치를 오류로 보고합니다. JSpecify 널 가능성 진단의 심각도는 다음 컴파일러 옵션으로 커스터마이즈할 수 있어요.

-Xjspecify-annotations=<report-level>

사용 가능한 보고 수준은 다음과 같습니다.

수준 설명
strict 널 가능성 불일치에 대해 오류 보고(기본값).
warn 경고 보고.
ignore 널 가능성 불일치 무시.

JSpecify 애노테이션에 대한 자세한 내용은 JSpecify user guide를 참고하세요.

JSR-305 지원

JSR-305에 정의된 @Nonnull 애노테이션은 Java 타입의 널 가능성을 나타내는 데 지원됩니다.

@Nonnull(when = ...) 값이 When.ALWAYS이면 애노테이션된 타입은 널 불가로 취급되고, When.MAYBEWhen.NEVER는 널 가능 타입을, When.UNKNOWN은 타입을 플랫폼 타입으로 강제합니다.

라이브러리는 JSR-305 애노테이션을 대상으로 컴파일될 수 있지만, 애노테이션 아티팩트(예: jsr305.jar)를 라이브러리 소비자의 컴파일 의존성으로 만들 필요는 없어요. Kotlin 컴파일러는 클래스패스에 애노테이션이 없어도 라이브러리에서 JSR-305 애노테이션을 읽을 수 있습니다.

커스텀 널 가능성 한정자 (KEEP-79)도 지원됩니다(아래 참조).

타입 한정자 별명

애노테이션 타입이 @TypeQualifierNickname과 JSR-305 @Nonnull(또는 @CheckForNull 같은 그것의 다른 별명)로 모두 애노테이션되면, 그 애노테이션 타입 자체가 정확한 널 가능성을 가져오는 데 사용되며 그 널 가능성 애노테이션과 같은 의미를 가집니다.

@TypeQualifierNickname
@Nonnull(when = When.ALWAYS)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyNonnull {
}

@TypeQualifierNickname
@CheckForNull // another type qualifier nickname에 대한 별명
@Retention(RetentionPolicy.RUNTIME)
public @interface MyNullable {
}

interface A {
    @MyNullable String foo(@MyNonnull String x);
    // in Kotlin (strict mode): `fun foo(x: String): String?`

    String bar(List<@MyNonnull String> x);
    // in Kotlin (strict mode): `fun bar(x: List<String>!): String!`
}
타입 한정자 기본값

@TypeQualifierDefault는 적용될 때 애노테이션된 요소의 스코프 안에서 기본 널 가능성을 정의하는 애노테이션을 도입할 수 있게 해줍니다.

그런 애노테이션 타입은 스스로 @Nonnull(또는 그것의 별명)과, 하나 이상의 ElementType 값을 가진 @TypeQualifierDefault(...)로 둘 다 애노테이션되어야 합니다.

  • ElementType.METHOD — 메서드의 반환 타입.
  • ElementType.PARAMETER — 값 파라미터.
  • ElementType.FIELD — 필드.
  • ElementType.TYPE_USE — 타입 인자, 타입 파라미터의 상한, 와일드카드 타입을 포함한 모든 타입.

기본 널 가능성은 타입 자체가 널 가능성 애노테이션으로 애노테이션되지 않았을 때 사용되며, 기본값은 타입 사용과 일치하는 ElementType을 가진 타입 한정자 기본 애노테이션으로 애노테이션된 가장 안쪽의 감싸는 요소에 의해 결정됩니다.

@Nonnull
@TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER})
public @interface NonNullApi {
}

@Nonnull(when = When.MAYBE)
@TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE_USE})
public @interface NullableApi {
}

@NullableApi
interface A {
    String foo(String x); // fun foo(x: String?): String?

    @NotNullApi // overriding default from the interface
    String bar(String x, @Nullable String y); // fun bar(x: String, y: String?): String

    // List<String> 타입 인자는 `@NullableApi`가 `TYPE_USE` 요소 타입을
    // 가지므로 널 가능으로 보인다:
    String baz(List<String> x); // fun baz(List<String?>?): String?

    // `x` 파라미터의 타입은 명시적 UNKNOWN 표시 널 가능성
    // 애노테이션이 있으므로 플랫폼으로 남는다:
    String qux(@Nonnull(when = When.UNKNOWN) String x); // fun baz(x: String!): String?
}

이 예시의 타입들은 strict 모드가 활성화된 경우에만 적용됩니다. 그렇지 않으면 플랫폼 타입이 유지돼요. @UnderMigration 애노테이션컴파일러 구성 섹션을 참고하세요.

패키지 수준의 기본 널 가능성도 지원됩니다.

// FILE: test/package-info.java
@NonNullApi // declaring all types in package 'test' as non-nullable by default
package test;
@UnderMigration 애노테이션

@UnderMigration 애노테이션(별도 아티팩트 kotlin-annotations-jvm에서 제공)은 라이브러리 유지보수자가 널 가능성 타입 한정자의 마이그레이션 상태를 정의하는 데 사용할 수 있어요.

@UnderMigration(status = ...)의 상태 값은 컴파일러가 Kotlin에서 애노테이션된 타입의 부적절한 사용(예: @MyNullable 애노테이션 타입 값을 널 불가로 사용)을 어떻게 취급할지 지정합니다.

  • MigrationStatus.STRICT — 애노테이션이 일반 널 가능성 애노테이션처럼 동작하게 함. 즉 부적절한 사용에 대해 오류를 보고하고, 애노테이션된 선언의 타입을 Kotlin에서 보이는 대로 영향을 줍니다.
  • MigrationStatus.WARN — 부적절한 사용이 오류가 아닌 컴파일 경고로 보고되지만, 애노테이션된 선언의 타입은 플랫폼으로 유지됩니다.
  • MigrationStatus.IGNORE — 컴파일러가 널 가능성 애노테이션을 완전히 무시하게 함.

라이브러리 유지보수자는 타입 한정자 별명과 타입 한정자 기본값 둘 다에 @UnderMigration 상태를 추가할 수 있어요.

@Nonnull(when = When.ALWAYS)
@TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER})
@UnderMigration(status = MigrationStatus.WARN)
public @interface NonNullApi {
}

// 클래스의 타입들은 널 불가이지만, `@NonNullApi`가
// `@UnderMigration(status = MigrationStatus.WARN)`으로 애노테이션되었으므로
// 경고만 보고된다
@NonNullApi
public class Test {}

널 가능성 애노테이션의 마이그레이션 상태는 그것의 타입 한정자 별명에 상속되지 않지만, 기본 타입 한정자에서의 사용에는 적용됩니다. 기본 타입 한정자가 타입 한정자 별명을 사용하고 둘 다 @UnderMigration이면, 기본 타입 한정자의 상태가 사용됩니다.

컴파일러 구성

JSR-305 검사는 -Xjsr305 컴파일러 플래그에 다음 옵션(및 그 조합)을 추가해 구성할 수 있어요.

  • -Xjsr305={strict|warn|ignore} — 비-@UnderMigration 애노테이션에 대한 동작 설정. 커스텀 널 가능성 한정자, 특히 @TypeQualifierDefault는 이미 많은 잘 알려진 라이브러리에 퍼져 있어서, 사용자는 JSR-305 지원이 포함된 Kotlin 버전으로 업데이트할 때 매끄럽게 마이그레이션해야 할 수 있어요. Kotlin 1.1.60부터 이 플래그는 비-@UnderMigration 애노테이션에만 영향을 줍니다.
  • -Xjsr305=under-migration:{strict|warn|ignore}@UnderMigration 애노테이션에 대한 동작 덮어쓰기. 사용자는 라이브러리의 마이그레이션 상태에 대해 다른 견해를 가질 수 있어요. 공식 마이그레이션 상태가 WARN인데 오류를 원할 수도 있고, 그 반대로 일부에 대한 오류 보고를 마이그레이션을 완료할 때까지 미루고 싶을 수도 있죠.
  • -Xjsr305=@<fq.name>:{strict|warn|ignore} — 단일 애노테이션에 대한 동작 덮어쓰기. 여기서 <fq.name>은 애노테이션의 완전히 정규화된 클래스 이름입니다. 다른 애노테이션에 대해 여러 번 나타날 수 있어요. 특정 라이브러리의 마이그레이션 상태를 관리하는 데 유용합니다.

strict, warn, ignore 값은 MigrationStatus와 같은 의미를 가지며, strict 모드만 Kotlin에서 보이는 애노테이션된 선언의 타입에 영향을 줍니다.

참고: 내장 JSR-305 애노테이션 @Nonnull, @Nullable, @CheckForNull은 항상 활성화되어, -Xjsr305 플래그의 컴파일러 구성과 관계없이 Kotlin에서 애노테이션된 선언의 타입에 영향을 줍니다.

예를 들어 컴파일러 인자에 -Xjsr305=ignore -Xjsr305=under-migration:ignore [email protected]:warn을 추가하면, @org.library.MyNullable로 애노테이션된 타입의 부적절한 사용에 대해 컴파일러가 경고를 생성하고 다른 모든 JSR-305 애노테이션은 무시합니다.

기본 동작은 -Xjsr305=warn과 같습니다. strict 값은 실험적인 것으로 간주해야 해요(향후 더 많은 검사가 추가될 수 있어요).

매핑된 타입

Kotlin은 일부 Java 타입을 특별하게 취급합니다. 그런 타입은 Java에서 "그대로" 로드되지 않고, 해당 Kotlin 타입으로 매핑됩니다. 매핑은 컴파일 타임에만 의미가 있으며, 런타임 표현은 변하지 않아요. Java의 기본(primitive) 타입들은 해당 Kotlin 타입으로 매핑됩니다(플랫폼 타입을 염두에 두고):

Java 타입 Kotlin 타입
byte kotlin.Byte
short kotlin.Short
int kotlin.Int
long kotlin.Long
char kotlin.Char
float kotlin.Float
double kotlin.Double
boolean kotlin.Boolean

일부 비-기본 내장 클래스도 매핑됩니다.

Java 타입 Kotlin 타입
java.lang.Object kotlin.Any!
java.lang.Cloneable kotlin.Cloneable!
java.lang.Comparable kotlin.Comparable!
java.lang.Enum kotlin.Enum!
java.lang.annotation.Annotation kotlin.Annotation!
java.lang.CharSequence kotlin.CharSequence!
java.lang.String kotlin.String!
java.lang.Number kotlin.Number!
java.lang.Throwable kotlin.Throwable!

Java의 박스형 기본 타입(boxed primitive types)은 널 가능 Kotlin 타입으로 매핑됩니다.

Java 타입 Kotlin 타입
java.lang.Byte kotlin.Byte?
java.lang.Short kotlin.Short?
java.lang.Integer kotlin.Int?
java.lang.Long kotlin.Long?
java.lang.Character kotlin.Char?
java.lang.Float kotlin.Float?
java.lang.Double kotlin.Double?
java.lang.Boolean kotlin.Boolean?

타입 파라미터로 사용된 박스형 기본 타입은 플랫폼 타입으로 매핑된다는 점에 유의하세요. 예를 들어 List<java.lang.Integer>는 Kotlin에서 List<Int!>가 됩니다.

컬렉션 타입은 Kotlin에서 읽기 전용이거나 가변일 수 있으므로, Java의 컬렉션은 다음과 같이 매핑됩니다(이 표의 모든 Kotlin 타입은 kotlin.collections 패키지에 있습니다):

Java 타입 Kotlin 읽기 전용 타입 Kotlin 가변 타입 로드된 플랫폼 타입
Iterator Iterator MutableIterator (Mutable)Iterator!
Iterable Iterable MutableIterable (Mutable)Iterable!
Collection Collection MutableCollection (Mutable)Collection!
Set Set MutableSet (Mutable)Set!
List List MutableList (Mutable)List!
ListIterator ListIterator MutableListIterator (Mutable)ListIterator!
Map<K, V> Map<K, V> MutableMap<K, V> (Mutable)Map<K, V>!
Map.Entry<K, V> Map.Entry<K, V> MutableMap.MutableEntry<K,V> (Mutable)Map.(Mutable)Entry<K, V>!

Java의 배열은 아래에서 언급한 대로 매핑됩니다.

Java 타입 Kotlin 타입
int[] kotlin.IntArray!
String[] kotlin.Array<(out) String!>!

이 Java 타입들의 정적 멤버는 Kotlin 타입의 컴패니언 객체에서 직접 접근할 수 없어요. 그것들을 호출하려면 java.lang.Integer.toHexString(foo)처럼 Java 타입의 완전히 정규화된 이름을 사용하세요.

Kotlin의 Java 제네릭

Kotlin의 제네릭은 Java의 것과 조금 다릅니다(Generics 참고). Java 타입을 Kotlin으로 가져올 때 다음 변환이 이루어집니다.

  • Java의 와일드카드는 타입 프로젝션으로 변환됩니다.
    • Foo<? extends Bar>Foo<out Bar!>!이 됩니다.
    • Foo<? super Bar>Foo<in Bar!>!이 됩니다.
  • Java의 원시 타입(raw type)은 스타 프로젝션으로 변환됩니다.
    • ListList<*>!List<out Any?>!이 됩니다.

Java처럼 Kotlin의 제네릭은 런타임에 유지되지 않습니다. 객체는 생성자에 전달된 실제 타입 인자에 대한 정보를 담지 않아요. 예를 들어 ArrayList<Integer>()ArrayList<Character>()와 구별할 수 없습니다. 이로 인해 제네릭을 고려한 is 검사를 수행하는 것이 불가능하죠. Kotlin은 스타 프로젝션된 제네릭 타입에 대해서만 is 검사를 허용합니다.

if (a is List<Int>) // Error: cannot check if it is really a List of Ints
// but
if (a is List<*>) // OK: no guarantees about the contents of the list

Java 배열

Kotlin의 배열은 Java와 달리 불변(주로 invariant)입니다. 즉 Kotlin은 Array<String>Array<Any>에 할당하는 것을 허용하지 않아, 가능한 런타임 실패를 막습니다. 서브클래스의 배열을 수퍼클래스의 배열로 Kotlin 메서드에 전달하는 것도 금지되지만, Array<(out) String>! 형태의 플랫폼 타입을 통해서는 Java 메서드에 허용됩니다.

배열은 박싱/언박싱 연산의 비용을 피하기 위해 Java 플랫폼에서 기본 데이터 타입과 함께 사용됩니다. Kotlin은 그런 구현 세부 사항을 숨기므로, Java 코드와 인터페이스하려면 우회책이 필요해요. 각 기본 배열 타입마다 특수 클래스(IntArray, DoubleArray, CharArray 등)가 있어 이 경우를 처리합니다. 이것들은 Array 클래스와 관련이 없고, 최대 성능을 위해 Java의 기본 배열로 컴파일됩니다.

인덱스의 int 배열을 받는 Java 메서드가 있다고 가정해 볼게요.

public class JavaArrayExample {
    public void removeIndices(int[] indices) {
        // code here...
    }
}

기본 값의 배열을 전달하려면 Kotlin에서 다음과 같이 할 수 있어요.

val javaObj = JavaArrayExample()
val array = intArrayOf(0, 1, 2, 3)
javaObj.removeIndices(array)  // passes int[] to method

JVM 바이트코드로 컴파일할 때 컴파일러는 배열 접근을 최적화해서 오버헤드가 발생하지 않습니다.

val array = arrayOf(1, 2, 3, 4)
array[1] = array[1] * 2 // no actual calls to get() and set() generated
for (x in array) { // no iterator created
    print(x)
}

인덱스로 탐색해도 오버헤드가 도입되지 않아요.

for (i in array.indices) { // no iterator created
    array[i] += 2
}

마지막으로 in 검사도 오버헤드가 없습니다.

if (i in array.indices) { // same as (i >= 0 && i < array.size)
    print(array[i])
}

Java varargs

Java 클래스는 때때로 가변 인자(varargs)의 인덱스를 위한 메서드 선언을 사용합니다.

public class JavaArrayExample {

    public void removeIndicesVarArg(int... indices) {
        // code here...
    }
}

그런 경우 IntArray를 전달하려면 스프레드 연산자 *를 사용해야 해요.

val javaObj = JavaArrayExample()
val array = intArrayOf(0, 1, 2, 3)
javaObj.removeIndicesVarArg(*array)

연산자

Java에는 연산자 문법을 사용하는 것이 합리적인 메서드를 표시할 방법이 없으므로, Kotlin은 올바른 이름과 시그니처를 가진 어떤 Java 메서드든 연산자 오버로드와 다른 관례(invoke() 등)로 사용하는 것을 허용합니다. 중위 호출 문법으로 Java 메서드를 호출하는 것은 허용되지 않아요.

Checked 예외

Kotlin에서 모든 예외는 unchecked입니다. 즉 컴파일러가 어떤 예외도 잡도록 강제하지 않습니다. 그래서 checked 예외를 선언하는 Java 메서드를 호출할 때 Kotlin은 어떤 것도 강제하지 않아요.

fun render(list: List<*>, to: Appendable) {
    for (item in list) {
        to.append(item.toString()) // Java would require us to catch IOException here
    }
}

Object 메서드

Java 타입을 Kotlin으로 가져오면 java.lang.Object 타입의 모든 참조가 Any로 바뀝니다. Any는 플랫폼 특화적이지 않으므로 멤버로 toString(), hashCode(), equals()만 선언합니다. 그래서 java.lang.Object의 다른 멤버를 사용할 수 있게 하기 위해 Kotlin은 확장 함수를 사용해요.

wait()와 notify()

wait()notify() 메서드는 Any 타입의 참조에서는 사용할 수 없습니다. 그것들의 사용은 일반적으로 java.util.concurrent를 선호해 권장되지 않아요.

이 메서드를 호출해야 한다면 Java 객체를 통해 접근하고 PLATFORM_CLASS_MAPPED_TO_KOTLIN 경고를 억제하세요.

import java.util.LinkedList

class SimpleBlockingQueue<T>(private val capacity: Int) {
    private val queue = LinkedList<T>()

    // java.lang.Object는 특히 wait()와 notify()에 접근하기 위해 사용된다
    // Kotlin에서는 표준 'Any' 타입이 이 메서드들을 노출하지 않는다
    @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
    private val lock = Object()

    fun put(item: T) {
        synchronized(lock) {
            while (queue.size >= capacity) {
                lock.wait()
            }
            queue.add(item)
            println("Produced: $item")

            lock.notifyAll()
        }
    }

    fun take(): T {
        synchronized(lock) {
            while (queue.isEmpty()) {
                lock.wait()
            }
            val item = queue.removeFirst()
            println("Consumed: $item")

            lock.notifyAll()
            return item
        }
    }
}

또는 java.lang.Object로 명시적으로 캐스팅하고 PLATFORM_CLASS_MAPPED_TO_KOTLIN 경고를 억제합니다.

@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
(foo as java.lang.Object).wait()

getClass()

객체의 Java 클래스를 가져오려면 클래스 참조에서 java 확장 속성을 사용하세요.

val fooClass = foo::class.java

위 코드는 바운드 클래스 참조를 사용합니다. javaClass 확장 속성도 쓸 수 있어요.

val fooClass = foo.javaClass

clone()

clone()을 오버라이드하려면 클래스가 kotlin.Cloneable을 확장해야 합니다.

class Example : Cloneable {
    override fun clone(): Any { ... }
}

Effective Java, 3rd Edition의 Item 13, "Override clone judiciously"를 잊지 마세요.

finalize()

finalize()를 오버라이드하려면 override 키워드 없이 선언하기만 하면 됩니다.

class C {
    protected fun finalize() {
        // finalization logic
    }
}

Java의 규칙에 따라 finalize()private이어서는 안 됩니다.

Java 클래스로부터의 상속

Java 클래스 하나(그리고 원하는 만큼의 Java 인터페이스)가 Kotlin에서 클래스의 수퍼타입이 될 수 있어요.

정적 멤버 접근

Java 클래스의 정적 멤버는 그 클래스의 "컴패니언 객체"를 형성합니다. 그런 "컴패니언 객체"를 값으로 전달할 수는 없지만, 멤버에는 명시적으로 접근할 수 있어요. 예를 들어:

if (Character.isLetter(a)) { ... }

매핑된 Kotlin 타입인 Java 타입의 정적 멤버에 접근하려면 Java 타입의 완전히 정규화된 이름을 사용하세요: java.lang.Integer.bitCount(foo).

Java 리플렉션

Java 리플렉션은 Kotlin 클래스에서 동작하고, 그 반대도 마찬가지입니다. 앞서 언급했듯이 instance::class.java, ClassName::class.java 또는 instance.javaClass를 사용해 java.lang.Class를 통해 Java 리플렉션으로 들어갈 수 있어요. 이 목적에 ClassName.javaClass를 사용하지 마세요. 그것은 ClassName의 컴패니언 객체 클래스를 가리켜서, ClassName.Companion::class.java와 같지 ClassName::class.java와 같지 않기 때문입니다.

각 기본 타입에는 두 개의 서로 다른 Java 클래스가 있고, Kotlin은 둘 다 얻는 방법을 제공합니다. 예를 들어 Int::class.java는 기본 타입 자체를 나타내는 클래스 인스턴스를 반환하며, Java의 Integer.TYPE에 해당합니다. 해당 래퍼 타입의 클래스를 얻으려면 Int::class.javaObjectType을 사용하세요. 이것은 Java의 Integer.class와 동등합니다.

다른 지원되는 경우는 Kotlin 속성에 대한 Java getter/setter 메서드나 백킹 필드 얻기, Java 필드에 대한 KProperty, KFunction에 대한 Java 메서드나 생성자 얻기와 그 반대를 포함합니다.

SAM 변환

Kotlin은 Java와 Kotlin 인터페이스 둘 다에 대해 SAM 변환을 지원합니다. Java에 대한 이 지원은, 인터페이스 메서드의 파라미터 타입이 Kotlin 함수의 파라미터 타입과 일치하는 한, Kotlin 함수 리터럴이 단일 비-기본(비-default) 메서드를 가진 Java 인터페이스의 구현으로 자동 변환될 수 있다는 뜻이에요.

이것을 SAM 인터페이스의 인스턴스를 만드는 데 쓸 수 있습니다.

val runnable = Runnable { println("This runs in a runnable") }

...그리고 메서드 호출에서도요.

val executor = ThreadPoolExecutor()
// Java signature: void execute(Runnable command)
executor.execute { println("This runs in a thread pool") }

Java 클래스에 함수형 인터페이스를 받는 메서드가 여러 개 있다면, 람다를 특정 SAM 타입으로 변환하는 어댑터 함수를 사용해 호출하려는 것을 고를 수 있어요. 그런 어댑터 함수는 필요할 때 컴파일러가 생성하기도 합니다.

executor.execute(Runnable { println("This runs in a thread pool") })

SAM 변환은 단일 추상 메서드를 가진 경우라도 인터페이스에만 동작하며, 추상 클래스에는 동작하지 않아요.

Kotlin에서 JNI 사용하기

네이티브(C 또는 C++) 코드로 구현된 함수를 선언하려면 그것을 external 수정자로 표시해야 해요.

external fun foo(x: Int): Double

나머지 절차는 Java에서와 정확히 같은 방식으로 동작합니다.

속성 getter와 setter도 external로 표시할 수 있어요.

var myProperty: String
    external get
    external set

뒤에서 이것은 getMyPropertysetMyProperty라는 두 함수를 만들며, 둘 다 external로 표시됩니다.

Kotlin에서 Lombok 생성 선언 사용하기

Kotlin 코드에서 Java의 Lombok 생성 선언을 사용할 수 있어요. 같은 혼합 Java/Kotlin 모듈에서 이러한 선언을 생성하고 사용해야 한다면, Lombok compiler plugin 페이지에서 하는 방법을 배울 수 있습니다. 다른 모듈에서 그런 선언을 호출한다면, 그 모듈을 컴파일하는 데이 플러그인을 사용할 필요가 없어요.

더 알아보기 (Learn more)