본문으로 건너뛰기

Java에서 Kotlin 호출하기

Kotlin 코드는 Java에서 쉽게 호출할 수 있습니다. 예를 들어, Kotlin 클래스의 인스턴스는 Java 메서드에서 원활하게 생성하고 작동할 수 있습니다. 그러나 Kotlin 코드를 Java에 통합할 때 주의해야 할 Java와 Kotlin 간의 특정 차이점이 있습니다. 이 페이지에서는 Kotlin 코드를 Java 클라이언트와 상호 운용하도록 조정하는 방법에 대해 설명합니다.

속성

Kotlin 속성은 다음 Java 요소로 컴파일됩니다.

  • get 접두사를 붙여 계산된 이름의 getter 메서드
  • set 접두사를 붙여 계산된 이름의 setter 메서드(var 속성에만 해당)
  • 속성 이름과 동일한 이름의 private 필드(backing field가 있는 속성에만 해당)

예를 들어, var firstName: String은 다음 Java 선언으로 컴파일됩니다.

private String firstName;

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

속성 이름이 is로 시작하는 경우 다른 이름 매핑 규칙이 사용됩니다. getter 이름은 속성 이름과 같고 setter 이름은 isset으로 바꿔서 얻습니다. 예를 들어, isOpen 속성의 경우 getter는 isOpen()으로 호출되고 setter는 setOpen()으로 호출됩니다. 이 규칙은 Boolean뿐만 아니라 모든 유형의 속성에 적용됩니다.

패키지 수준 함수

확장 함수를 포함하여 org.example 패키지 내의 app.kt 파일에 선언된 모든 함수 및 속성은 org.example.AppKt라는 Java 클래스의 static 메서드로 컴파일됩니다.

// app.kt
package org.example

class Util

fun getTime() { /*...*/ }

// Java
new org.example.Util();
org.example.AppKt.getTime();

생성된 Java 클래스에 사용자 지정 이름을 설정하려면 @JvmName annotation을 사용합니다.

@file:JvmName("DemoUtils")

package org.example

class Util

fun getTime() { /*...*/ }

// Java
new org.example.Util();
org.example.DemoUtils.getTime();

생성된 Java 클래스 이름(동일한 패키지 및 동일한 이름 또는 동일한 @JvmName annotation)이 같은 파일이 여러 개 있으면 일반적으로 오류입니다. 그러나 컴파일러는 지정된 이름을 갖고 해당 이름을 가진 모든 파일의 모든 선언을 포함하는 단일 Java facade 클래스를 생성할 수 있습니다. 이러한 facade 생성을 활성화하려면 해당 파일 모두에서 @JvmMultifileClass annotation을 사용합니다.

// oldutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass

package org.example

fun getTime() { /*...*/ }
// newutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass

package org.example

fun getDate() { /*...*/ }
// Java
org.example.Utils.getTime();
org.example.Utils.getDate();

인스턴스 필드

Kotlin 속성을 Java에서 필드로 노출해야 하는 경우 @JvmField annotation으로 표시합니다. 필드는 기본 속성과 동일한 가시성을 갖습니다. 다음 조건에 해당하면 속성을 @JvmField로 annotation할 수 있습니다.

  • backing field가 있습니다.
  • private이 아닙니다.
  • open, override 또는 const modifier가 없습니다.
  • delegated property가 아닙니다.
class User(id: String) {
@JvmField val ID = id
}

// Java
class JavaClient {
public String getID(User user) {
return user.ID;
}
}

Late-Initialized 속성도 필드로 노출됩니다. 필드의 가시성은 lateinit 속성 setter의 가시성과 같습니다.

Static 필드

named object 또는 companion object에 선언된 Kotlin 속성은 해당 named object 또는 companion object를 포함하는 클래스에 static backing field를 갖습니다.

일반적으로 이러한 필드는 private이지만 다음 방법 중 하나로 노출될 수 있습니다.

  • @JvmField annotation
  • lateinit modifier
  • const modifier

이러한 속성을 @JvmField로 annotation하면 속성 자체와 동일한 가시성을 가진 static 필드가 됩니다.

class Key(val value: Int) {
companion object {
@JvmField
val COMPARATOR: Comparator<Key> = compareBy<Key> { it.value }
}
}
// Java
Key.COMPARATOR.compare(key1, key2);
// public static final field in Key class

object 또는 companion object의 late-initialized 속성은 속성 setter와 동일한 가시성을 가진 static backing field를 갖습니다.

object Singleton {
lateinit var provider: Provider
}

// Java
Singleton.provider = new Provider();
// public static non-final field in Singleton class

클래스뿐만 아니라 최상위 수준에서 const로 선언된 속성은 Java에서 static 필드로 바뀝니다.

// file example.kt

object Obj {
const val CONST = 1
}

class C {
companion object {
const val VERSION = 9
}
}

const val MAX = 239

Java에서:


int constant = Obj.CONST;
int max = ExampleKt.MAX;
int version = C.VERSION;

Static 메서드

위에서 언급했듯이 Kotlin은 패키지 수준 함수를 static 메서드로 나타냅니다. Kotlin은 이러한 함수를 @JvmStatic으로 annotation하면 named object 또는 companion object에 정의된 함수에 대한 static 메서드를 생성할 수도 있습니다. 이 annotation을 사용하면 컴파일러는 object의 enclosing 클래스에 static 메서드와 object 자체에 인스턴스 메서드를 모두 생성합니다. 예:

class C {
companion object {
@JvmStatic fun callStatic() {}
fun callNonStatic() {}
}
}

이제 callStatic()은 Java에서 static이고 callNonStatic()은 static이 아닙니다.


C.callStatic(); // works fine
C.callNonStatic(); // error: not a static method
C.Companion.callStatic(); // instance method remains
C.Companion.callNonStatic(); // the only way it works

named object도 마찬가지입니다.

object Obj {
@JvmStatic fun callStatic() {}
fun callNonStatic() {}
}

Java에서:


Obj.callStatic(); // works fine
Obj.callNonStatic(); // error
Obj.INSTANCE.callNonStatic(); // works, a call through the singleton instance
Obj.INSTANCE.callStatic(); // works too

Kotlin 1.3부터 @JvmStatic은 인터페이스의 companion object에 정의된 함수에도 적용됩니다. 이러한 함수는 인터페이스의 static 메서드로 컴파일됩니다. 인터페이스의 static 메서드는 Java 1.8에서 도입되었으므로 해당 대상을 사용해야 합니다.

interface ChatBot {
companion object {
@JvmStatic fun greet(username: String) {
println("Hello, $username")
}
}
}

@JvmStatic annotation은 object 또는 companion object의 속성에도 적용하여 해당 getter 및 setter 메서드를 해당 object 또는 companion object를 포함하는 클래스의 static 멤버로 만들 수 있습니다.

인터페이스의 기본 메서드

노트

기본 메서드는 JVM 1.8 이상 대상에만 사용할 수 있습니다.

JDK 1.8부터 Java의 인터페이스는 기본 메서드를 포함할 수 있습니다. Kotlin 인터페이스의 모든 추상적이지 않은 멤버를 Java 클래스에 구현하는 기본값으로 만들려면 -Xjvm-default=all 컴파일러 옵션으로 Kotlin 코드를 컴파일합니다.

다음은 기본 메서드가 있는 Kotlin 인터페이스의 예입니다.

// compile with -Xjvm-default=all

interface Robot {
fun move() { println("~walking~") } // will be default in the Java interface
fun speak(): Unit
}

기본 구현은 인터페이스를 구현하는 Java 클래스에서 사용할 수 있습니다.

//Java implementation
public class C3PO implements Robot {
// move() implementation from Robot is available implicitly
@Override
public void speak() {
System.out.println("I beg your pardon, sir");
}
}
C3PO c3po = new C3PO();
c3po.move(); // default implementation from the Robot interface
c3po.speak();

인터페이스 구현은 기본 메서드를 재정의할 수 있습니다.

//Java
public class BB8 implements Robot {
//own implementation of the default method
@Override
public void move() {
System.out.println("~rolling~");
}

@Override
public void speak() {
System.out.println("Beep-beep");
}
}
노트

Kotlin 1.4 이전에는 기본 메서드를 생성하기 위해 이러한 메서드에서 @JvmDefault annotation을 사용할 수 있었습니다. 일반적으로 1.4+에서 -Xjvm-default=all로 컴파일하는 것은 인터페이스의 모든 추상적이지 않은 메서드를 @JvmDefault로 annotation하고 -Xjvm-default=enable로 컴파일하는 것과 같습니다. 그러나 동작이 다른 경우가 있습니다. Kotlin 블로그의 이 게시물에 Kotlin 1.4의 기본 메서드 생성 변경 사항에 대한 자세한 정보가 제공되어 있습니다.

기본 메서드의 호환성 모드

-Xjvm-default=all 옵션 없이 컴파일된 Kotlin 인터페이스를 사용하는 클라이언트가 있는 경우 이 옵션으로 컴파일된 코드와 이진 호환되지 않을 수 있습니다. 이러한 클라이언트와의 호환성이 손상되지 않도록 하려면 -Xjvm-default=all 모드를 사용하고 인터페이스를 @JvmDefaultWithCompatibility annotation으로 표시합니다. 이를 통해 퍼블릭 API의 모든 인터페이스에 이 annotation을 한 번 추가할 수 있으며 새로운 비공개 코드에 annotation을 사용할 필요가 없습니다.

노트

Kotlin 1.6.20부터 -Xjvm-default=disable 컴파일러 옵션인 기본 모드에서 -Xjvm-default=all 또는 -Xjvm-default=all-compatibility 모드로 컴파일된 모듈에 대해 모듈을 컴파일할 수 있습니다.

호환성 모드에 대해 자세히 알아보세요.

disable

기본 동작입니다. JVM 기본 메서드를 생성하지 않고 @JvmDefault annotation 사용을 금지합니다.

all

모듈의 본문이 있는 모든 인터페이스 선언에 대해 JVM 기본 메서드를 생성합니다. disable 모드에서 기본적으로 생성되는 본문이 있는 인터페이스 선언에 대해 DefaultImpls 스텁을 생성하지 마세요.

인터페이스가 disable 모드로 컴파일된 인터페이스에서 본문이 있는 메서드를 상속하고 재정의하지 않는 경우 해당 메서드에 대한 DefaultImpls 스텁이 생성됩니다.

일부 클라이언트 코드가 DefaultImpls 클래스의 존재에 의존하는 경우 이진 호환성을 손상시킵니다.

노트

인터페이스 위임이 사용되는 경우 모든 인터페이스 메서드가 위임됩니다. 유일한 예외는 더 이상 사용되지 않는 @JvmDefault annotation으로 annotation된 메서드입니다.

all-compatibility

all 모드 외에도 DefaultImpls 클래스에 호환성 스텁을 생성합니다. 호환성 스텁은 라이브러리 및 런타임 작성자가 이전 라이브러리 버전에 대해 컴파일된 기존 클라이언트에 대한 이전 버전과의 이진 호환성을 유지하는 데 유용할 수 있습니다. allall-compatibility 모드는 라이브러리 재컴파일 후 클라이언트가 사용할 라이브러리 ABI 표면을 변경합니다. 이러한 의미에서 클라이언트는 이전 라이브러리 버전과 호환되지 않을 수 있습니다. 일반적으로 SemVer에서 주요 버전 증가와 같이 적절한 라이브러리 버전 관리가 필요함을 의미합니다.

컴파일러는 @Deprecated annotation으로 DefaultImpls의 모든 멤버를 생성합니다. 컴파일러는 호환성을 위해서만 멤버를 생성하므로 Java 코드에서 이러한 멤버를 사용해서는 안 됩니다.

all 또는 all-compatibility 모드로 컴파일된 Kotlin 인터페이스에서 상속하는 경우 DefaultImpls 호환성 스텁은 표준 JVM 런타임 확인 의미 체계를 사용하여 인터페이스의 기본 메서드를 호출합니다.

일부 경우에는 disable 모드에서 특수화된 서명이 있는 추가적인 암시적 메서드가 생성된 제네릭 인터페이스를 상속하는 클래스에 대한 추가 호환성 검사를 수행합니다. disable 모드와 달리 이러한 메서드를 명시적으로 재정의하지 않고 클래스를 @JvmDefaultWithoutCompatibility로 annotation하지 않으면 컴파일러는 오류를 보고합니다(이 YouTrack 문제에서 자세한 내용을 참조하세요).

가시성

Kotlin 가시성 modifier는 다음과 같은 방식으로 Java에 매핑됩니다.

  • private 멤버는 private 멤버로 컴파일됩니다.
  • private 최상위 수준 선언은 private 최상위 수준 선언으로 컴파일됩니다. 패키지 비공개 접근자는 클래스 내에서 액세스하는 경우에도 포함됩니다.
  • protectedprotected로 유지됩니다(Java는 동일한 패키지의 다른 클래스에서 protected 멤버에 액세스할 수 있지만 Kotlin은 그렇지 않으므로 Java 클래스는 코드에 더 넓게 액세스할 수 있습니다).
  • internal 선언은 Java에서 public이 됩니다. internal 클래스의 멤버는 이름 mangling을 거쳐 Java에서 실수로 사용하는 것을 더 어렵게 만들고 Kotlin 규칙에 따라 서로 볼 수 없는 동일한 서명의 멤버에 대한 오버로드를 허용합니다.
  • publicpublic으로 유지됩니다.

KClass

경우에 따라 유형 KClass의 매개변수를 사용하여 Kotlin 메서드를 호출해야 합니다. Class에서 KClass로의 자동 변환은 없으므로 Class<T>.kotlin 확장 속성과 동일한 항목을 호출하여 수동으로 수행해야 합니다.

kotlin.jvm.JvmClassMappingKt.getKotlinClass(MainView.class)

@JvmName으로 서명 충돌 처리

경우에 따라 바이트코드에서 다른 JVM 이름이 필요한 Kotlin의 named function이 있습니다. 가장 두드러진 예는 유형 삭제로 인해 발생합니다.

fun List<String>.filterValid(): List<String>
fun List<Int>.filterValid(): List<Int>

JVM 서명이 동일하기 때문에 이러한 두 함수를 나란히 정의할 수 없습니다. filterValid(Ljava/util/List;)Ljava/util/List;. Kotlin에서 이름이 같도록 하려면 둘 중 하나(또는 둘 다)를 @JvmName으로 annotation하고 다른 이름을 지정할 수 있습니다. 인수로 지정합니다.

fun List<String>.filterValid(): List<String>

@JvmName("filterValidInt")
fun List<Int>.filterValid(): List<Int>

Kotlin에서는 이름 filterValid로 액세스할 수 있지만 Java에서는 filterValidfilterValidInt입니다.

속성 x를 함수 getX()와 함께 사용해야 하는 경우에도 동일한 방법이 적용됩니다.

val x: Int
@JvmName("getX_prop")
get() = 15

fun getX() = 10

명시적으로 구현된 getter 및 setter가 없는 속성에 대해 생성된 접근자 메서드의 이름을 변경하려면 @get:JvmName@set:JvmName을 사용할 수 있습니다.

@get:JvmName("x")
@set:JvmName("changeX")
var x: Int = 23

오버로드 생성

일반적으로 기본 매개변수 값이 있는 Kotlin 함수를 작성하면 모든 매개변수가 있는 전체 서명으로만 Java에 표시됩니다. Java 호출자에게 여러 오버로드를 노출하려면 @JvmOverloads annotation을 사용할 수 있습니다.

이 annotation은 생성자, static 메서드 등에도 적용됩니다. 인터페이스에 정의된 메서드를 포함하여 추상 메서드에서는 사용할 수 없습니다.

class Circle @JvmOverloads constructor(centerX: Int, centerY: Int, radius: Double = 1.0) {
@JvmOverloads fun draw(label: String, lineWidth: Int = 1, color: String = "red") { /*...*/ }
}

기본값이 있는 모든 매개변수에 대해 매개변수 목록에서 해당 매개변수와 해당 매개변수의 오른쪽에 있는 모든 매개변수가 제거된 추가 오버로드 하나가 생성됩니다. 이 예에서는 다음이 생성됩니다.

// Constructors:
Circle(int centerX, int centerY, double radius)
Circle(int centerX, int centerY)

// Methods
void draw(String label, int lineWidth, String color) { }
void draw(String label, int lineWidth) { }
void draw(String label) { }

보조 생성자에 설명된 것처럼 클래스에 모든 생성자 매개변수에 대한 기본값이 있는 경우 인수가 없는 퍼블릭 생성자가 생성됩니다. @JvmOverloads annotation이 지정되지 않은 경우에도 작동합니다.

Checked 예외

Kotlin에는 checked 예외가 없습니다. 따라서 일반적으로 Kotlin 함수의 Java 서명은 발생하는 예외를 선언하지 않습니다. 따라서 다음과 같은 Kotlin 함수가 있는 경우:

// example.kt
package demo

fun writeToFile() {
/*...*/
throw IOException()
}

Java에서 호출하여 예외를 잡으려면:


// Java
try {
demo.Example.writeToFile();
} catch (IOException e) {
// error: writeToFile() does not declare IOException in the throws list
// ...
}

Java 컴파일러에서 오류 메시지가 표시됩니다. writeToFile()IOException을 선언하지 않기 때문입니다. 이 문제를 해결하려면 Kotlin에서 @Throws annotation을 사용합니다.

@Throws(IOException::class)
fun writeToFile() {
/*...*/
throw IOException()
}

Null-safety

Java에서 Kotlin 함수를 호출할 때 아무도 null 불가능한 매개변수로 null을 전달하는 것을 막을 수 없습니다. 이것이 Kotlin이 null이 아닌 값을 예상하는 모든 public 함수에 대한 런타임 검사를 생성하는 이유입니다. 이러한 방식으로 Java 코드에서 즉시 NullPointerException을 가져옵니다.

Variant 제네릭

Kotlin 클래스가 선언 사이트 변형을 사용하는 경우 Java 코드에서 사용되는 방식을 보는 두 가지 옵션이 있습니다. 예를 들어, 다음 클래스와 이를 사용하는 두 함수가 있다고 가정해 보겠습니다.

class Box<out T>(val value: T)

interface Base
class Derived : Base

fun boxDerived(value: Derived): Box<Derived> = Box(value)
fun unboxBase(box: Box<Base>): Base = box.value

이러한 함수를 Java로 변환하는 순진한 방법은 다음과 같습니다.

Box<Derived> boxDerived(Derived value) { ... }
Base unboxBase(Box<Base> box) { ... }

문제는 Kotlin에서 unboxBase(boxDerived(Derived()))를 작성할 수 있지만 Java에서는 불가능하다는 것입니다. Java에서 클래스 Box는 매개변수 T에서 불변이므로 Box<Derived>Box<Base>의 하위 유형이 아니기 때문입니다. Java에서 작동하도록 하려면 다음과 같이 unboxBase를 정의해야 합니다.

Base unboxBase(Box<? extends Base> box) { ... }  

이 선언은 Java의 와일드카드 유형(? extends Base)을 사용하여 사용 사이트 변형을 통해 선언 사이트 변형을 에뮬레이션합니다.

Kotlin API가 Java에서 작동하도록 하기 위해 컴파일러는 Box<Super>를 공변적으로 정의된 Box에 대해 Box<? extends Super>로 생성합니다. (또는 반공변적으로 정의된 Foo의 경우 Foo<? super Bar>) 매개변수로 나타날 때. 리턴 값일 때는 와일드카드가 생성되지 않습니다. 그렇지 않으면 Java 클라이언트가 이를 처리해야 하기 때문입니다(일반적인 Java 코딩 스타일과 반대됨). 따라서 우리 예제의 함수는 실제로 다음과 같이 번역됩니다.


// return type - no wildcards
Box<Derived> boxDerived(Derived value) { ... }

// parameter - wildcards
Base unboxBase(Box<? extends Base> box) { ... }
노트

인수 유형이 final인 경우 일반적으로 와일드카드를 생성할 필요가 없으므로 Box<String>은 위치에 관계없이 항상 Box<String>입니다.

기본적으로 생성되지 않는 위치에 와일드카드가 필요한 경우 @JvmWildcard annotation을 사용합니다.

fun boxDerived(value: Derived): Box<@JvmWildcard Derived> = Box(value)
// is translated to
// Box<? extends Derived> boxDerived(Derived value) { ... }

반대의 경우, 생성되는 위치에 와일드카드가 필요하지 않은 경우 @JvmSuppressWildcards를 사용합니다.

fun unboxBase(box: Box<@JvmSuppressWildcards Base>): Base = box.value
// is translated to
// Base unboxBase(Box<Base> box) { ... }
노트

@JvmSuppressWildcards는 개별 유형 인수에만 사용할 수 있을 뿐만 아니라 함수 또는 클래스와 같은 전체 선언에도 사용할 수 있으므로 내부에 있는 모든 와일드카드가 억제됩니다.

유형 Nothing 변환

Nothing 유형은 Java에 자연스러운 대응물이 없기 때문에 특별합니다. 실제로 모든 Java 참조 유형(java.lang.Void 포함)은 null을 값으로 허용하고 Nothing은 그조차 허용하지 않습니다. 따라서 이 유형은 Java 세계에서 정확하게 표현할 수 없습니다. 이것이 Kotlin이 Nothing 유형의 인수가 사용되는 원시 유형을 생성하는 이유입니다.

fun emptyList(): List<Nothing> = listOf()
// is translated to
// List emptyList() { ... }