본문 바로가기

코틀린

[코틀린] 자바와 코틀린의 상호 운용성

 

안드로이드 개발하는 kancho입니다.

이번 포스팅에서는 자바와 코틀린 코드의 상호 운용성에 대해 알아보고자 합니다.

'코틀린 완벽 가이드' 책을 참고하였습니다.

 

 

코틀린은 JVM을 대상으로 만들어졌기 때문에 자바 코드를 코틀린에서 쉽게 사용할 수 있다.

주로 자바에 없는 코틀린 기능으로 문제가 발생하는데 이를 해결하는 방법들을 알아보자.

 

 

자바 코드를 코틀린에서 사용하기

 

 

1. 자바 메서드와 필드


자바 메서드를 아무 문제없이 코틀린 함수처럼 노출시킬 수 있다.

각 언어의 내용에 따라 뉘앙스 차이는 있을 수 있다.

 

자바 언어로 된 Java 클래스

public class Java {
    private String hello = "Hello";

    public String getHello() {
        return hello;
    }
}

 

코틀린 코드에서 자바 함수를 문제없이 사용할 수 있다.

fun main() {
    val java = Java()
    println(java.hello) // Hello
}

 

 

 

2. Unit과 Void


코틀린에는 반환값이 없음을 나타내는 void 키워드가 없다. 자바의 void 함수는 코틀린에서 Unit을 반환하는 함수로 보인다.

public class Java {
    public void voidToUnit() { }
}

 

코틀린에서 자바 void 함수는 kotlin.Unit을 출력한다.

fun main() {
    val java = Java()
    println(java.voidToUnit())  // kotlin.Unit
}

 

컴파일러가 Unit 객체에 대한 참조를 생성한 후 이를 출력해준다.

public static final void main() {
   Java java = new Java();
   java.voidToUnit();
   Unit var1 = Unit.INSTANCE;
   System.out.println(var1);
}

 

 

 

3. 합성 프로퍼티


자바에는 합성 프로퍼티가 없고 getter, setter를 사용한다. 

코틀린 컴파일러는 자바의 getter, setter를 코틀린 프로퍼티처럼 사용할 수 있도록 합성 프로퍼티를 노출시켜준다.

이를 위해 접근자는 아래와 같은 관습을 따라야 한다.

 

  • getter는 파라미터가 없는 메서드이며 이름이 get으로 시작해야 한다
  • setter는 파라미터가 하나만 있는 메서드이며 이름이 set으로 시작해야 한다

 

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

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
    
    public boolean isEmployed() {
        return isEmployed;
    }
    
    public void setEmployed(boolean employed) {
        isEmployed = employed;
    }
}

 

코틀린에서는 Person 인스턴스를 사용할 때 name, age 가변 프로퍼티가 정의된 것처럼 사용할 수 있다.

만약 getter 이름이 is로 시작된다면 합성 프로퍼티는 getter와 이름이 같다.

 

fun main() {
    val person = Person("Kancho", 25)
    person.name = "Harry"
    person.age = 30

    println("${person.name}, ${person.age}")    // Harry, 30
    
    person.isEmployed = false
    println(person.isEmployed)  // false
}

 

자바에 getter만 있으면 코틀린에서는 자동으로 불변 프로퍼티가 된다.

코틀린은 쓰기 전용 필드를 제공하지 않으므로 자바에 setter만 있는 경우 아무 프로퍼티도 노출되지 않는다.

 

 

 

4. 플랫폼 타입


자바에서는 nullable, non null 타입을 구분하지 않기 때문에 코틀린 컴파일러는 nullable 여부에 대해 알 수 없다. 

코틀린 컴파일러는 자바 타입을 명확한 널 가능성이 지정되지 않은 타입인 것처럼 취급한다.

코틀린에서 자바 코드로부터 비롯된 객체는 플랫폼 타입(platform type)이라는 특별한 타입에 속한다.

 

플랫폼 타입은 널이 될 수 있고 없는 타입이기도 하다. 기본적인 타입 안전성 보증은 자바와 동일하다.

널이 될 수 있거나 없는 문맥에서 모두 사용 가능하지만 널이 될 수 없는 문맥에서는 NPE가 발생할 수 있다.

 

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 void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

 

아래 코드에서 person.name 은 null 가능성을 판단할 수 없는 플랫폼 타입이다.

컴파일은 되지만 length에 접근하는 부분은 런타임으로 미뤄진다.

 

fun main() {
    val person = Person("Kancho", 30)
    println(person.name.length)     // 6

    val person2 = Person(null, 30)
    println(person2.name.length)    // NPE
}

 

플랫폼 타입은 코드에 명시할 수 없고 컴파일러만 구성할 수 있다.

 

플랫폼 타입의 식을 변수에 대입하거나 명시적인 타입을 지정하지 않고 함수에서 반환하면 플랫폼 타입이 전파된다.

 

/**
 * Declaration has type inferred from a platform call, 
 * which can lead to unchecked nullability issues. 
 * Specify type explicitly as nullable or non-nullable.
 */ 
 // BigInteger! 반환 타입
fun Int.toBigInt() = BigInteger.valueOf(toLong())

// BigInteger! 타입
val num = 123.toBigInt()

 

타입을 명시적으로 지정하면 플랫폼 타입을 강제로 지정할 수 있다.

 

fun Int.toBigInt(): BigInteger = BigInteger.valueOf(toLong())

 

코틀린은 자바의 컬렉션 타입을 표현할 때도 플랫폼 타입을 사용한다.

자바에서는 mutable, immutable 컬렉션을 구분하지 않기 때문이다.

List, Set, Map 등의 표준 자바 컬렉션 인스턴스는 불변, 가변으로 모두 생각될 수 있다.

 

 

 

5. 널 가능성 어노테이션


자바에서 null 안전성을 보장할 때 어노테이션을 사용한다.

붙은 어노테이션에 따라 자바 타입은 코틀린에서 nullable, non-null 타입 중 하나로 정해지며 플랫폼 타입이 되지 않는다.

 

코틀린 컴파일러가 지원하는 널 가능성 어노테이션은 다음과 같다.

 

  • org.jetbrains.annotations 패키지의 @Nullable@NotNull
  • Android SDK의 @Nullable @NotNull 변종들
  • javax.annotation 패키지의 @Nonnull 등의 JSR-350 널 가능성 어노테이션

 

// @NotNull 어노테이션을 통해 널이 될 수 없는 타입으로 지정
public class Person {
    @NotNull
    private String name;
    private int age;

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

    @NotNull public String getName() { return name; }
    public void setName(@NotNull String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

 

코틀린에서 person.name 은 String 타입이 된다.

 

fun main() {
    val person = Person("Kancho", 30)
    println(person.name.length)     // 6

    // error : Null can not be a value of a non-null type String
    val person2 = Person(null, 30)
}

 

타입 파라미터에서 어노테이션이 붙지 않으면 플랫폼 타입을 사용한다.

 

public class Person {
    @NotNull private Set<Person> friends = new HashSet<>();
    @NotNull public Set<Person> getFriends() {
        return friends;
    }

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

 

만약 Person 클래스의 Set<Person> 타입에 대해 @NotNull 어노테이션이 붙지 않았다면

person.friends는 (Mutable)Set<Person!>! 타입이 되었을 것이다.

 

 

fun main() {
    val person = Person("Kancho", 30)
    println(person.friends) // (Mutable)Set<Person!>
}

 

 

 

6. 자바 / 코틀린 타입 매핑


자바와 코틀린에서 비슷한 의미를 가지는 타입도 있다.

코틀린은 JVM 플랫폼에서 실행되도록 컴파일하는 경우 자바 코드 내용을 코틀린 코드에서 사용하거나 코틀린 코드 내용을 자바 코드에서 사용할 때 타입을 상호 변환해준다.

 

자바 / 코틀린 타입 매핑 규칙 (역방향도 적용)

자바 타입 코틀린 타입
byte / Byte Byte
short / Short Short
int / Integer Int
long / Long Long
char / Character Char
float / Float Float
double / Double Double

 

JVM에서 기본 코틀린 타입의 값은 원시 타입이거나 이에 상응하는 박싱 타입이다. (코틀린 Int? -> 자바 Integer)

 

java.lang 패키지의 원시 타입이 아닌 내장 클래스 중 kotlin 패키지에 상응하는 클래스로 매핑된다. (역방향도 성립)

이 경우 클래스 이름이 같다. 

 

  • Object (자바 Object)
  • Cloneable
  • Comparable
  • Enum
  • Annotation
  • CharSequence
  • String
  • Number
  • Throwable

 

코틀린으로 매핑된 자바 클래스의 정적 멤버를 코틀린 동반 객체에서 직접 접근할 수 없고 자바 클래스의 전체 이름을 언급해야 한다.

 

val n = java.lang.Long.bitCount(1234)

 

또한 코틀린 표준 컬렉션 타입은 모두 java.util 패키지의 컬렉션 타입으로 매핑된다. (역방향 X)

자바에서 코틀린으로의 매핑은 플랫폼 타입으로 인해 허용되지 않는다. 자바 컬렉션은 불변과 가변 구현이 같은 API를 사용한다.

 

  • Iterable / Iterator / ListIterator
  • Collection
  • Set
  • List
  • Map / Map.Entry

 

제네릭 타입의 매핑은 자바와 코틀린의 제네릭 구문 차이로 인해 단순하지 않은 변환이 필요하다.

 

  • 자바 extends 와일드카드는 코틀린 공변 projection으로 변환된다
  • 자바 super 와일드카드는 코틀린 반공변 projection으로 변환된다
  • 자바 raw type은 코틀린 star projection으로 바뀐다

 

TreeNode<? extends Person> ---> TreeNode<out Person>
TreeNode<? super Person> ---> TreeNode<in Person>
TreeNode ---> TreeNode<*>

 

원시 타입의 자바 배열(int[ ])은 박싱 / 언박싱을 피하기 위해 특화된 코틀린 배열 클래스인 IntArray로 매핑된다.

다른 모든 배열은 Array<(out)T>의 특별한 플랫폼 타입의 배열 인스턴스로 변환된다.

 

이런 변환으로 인해 상위 타입의 배열을 예상한 자바 메서드에게 하위 타입의 배열을 넘길 수 있다.

deepToString은 파라미터로 Object[ ] 배열을 받는데 String으로 이루어진 배열을 전달한다.

 

fun main() {
    val strings = arrayOf("a", "b", "c")
    println(Arrays.deepToString(strings))
}

 

이런 동작은 배열 타입이 공변적인 자바의 의미와 맞아떨어진다. 하지만 코틀린 배열은 무공변이므로 코틀린 메서드에 대해서는 이렇게 사용할 수 없다.

 

 

 

7. 단일 추상 메서드 인터페이스


자바 단일 추상 메서드(SAM) 인터페이스가 있다면 기본적으로 코틀린 함수 타입처럼 동작한다.

코틀린은 자바 SAM 인터페이스가 필요한 위치에 람다를 넘길 수 있도록 지원한다. (SAM 변환)

 

 

코틀린은 Runnable을 SAM 인터페이스로 판정해 코드에서 execute 함수에 람다를 넘길 수 있도록 해준다.

 

import java.util.concurrent.ScheduledThreadPoolExecutor

fun main() {
    val executor = ScheduledThreadPoolExecutor(5)
    
    executor.execute { 
        println("Working on asynchronous task...")
    }
    executor.shutdown()
}

 

execute 메서드는 Runnable 객체를 인자로 받는다.

public void execute(Runnable command) {
    schedule(command, 0, NANOSECONDS);
}

 

Runnable 인터페이스

public interface Runnable {
    public abstract void run();
}

 

컴파일러가 문맥을 파악하지 못해 적절한 변환을 찾지 못할 수도 있다.

 

fun main() {
    val executor = ScheduledThreadPoolExecutor(5)
    // 암시적으로 Runnable로 변환된다
    val future = executor.submit { 1 + 2 }

    println(future.get())   // null
    executor.shutdown()
}

 

ScheduledThreadPoolExecutor의 submit 함수는 아래와 같이 Runnable과 Callable 객체를 파라미터로 받을 수 있다.

public Future<?> submit(Runnable task) {
    return schedule(task, 0, NANOSECONDS);
}

public <T> Future<T> submit(Callable<T> task) {
    return schedule(task, 0, NANOSECONDS);
}

 

Callable 인터페이스

public interface Callable<V> {
    V call() throws Exception;
}

 

Runnable과 Callable 모두 SAM 인터페이스이지만 Runnable 타입이 더 구체적인 시그니처를 가졌기에 Runnable을 암시적으로 선택한 것이다.

 

Callable을 전달하고 싶다면 대상 타입을 지정해서 변환을 명시해야 한다.

이런 식을 SAM 생성자라고 한다.

fun main() {
    val executor = ScheduledThreadPoolExecutor(5)
    val future = executor.submit(
        Callable { 1 + 2 }
    )

    println(future.get())   // 3
    executor.shutdown()
}

 

SAM 변환은 인터페이스에만 적용되고 클래스에는 적용되지 않는다.

또한 코틀린 인터페이스에 대해서도 SAM 변환을 사용할 수 없다.

 

 

 

8. 자바를 코틀린으로 변환하는 변환기 사용


Intellij 플러그인에 자바 소스 파일을 동등한 코틀린 코드로 변환해주는 도구가 있다.

Code Menu --> Convert Java File to Kotlin File

 

 


 

코틀린 코드를 자바에서 사용하기

 

 

1. 프로퍼티 접근


자바나 JVM에는 코틀린 프로퍼티에 대한 개념이 없어 프로퍼티에 직접 접근할 수 없다. 컴파일된 JVM 바이트코드에서 각 프로퍼티가 접근자 메서드로 표현되어 자바에서는 접근자 메서드를 통해 프로퍼티에 접근할 수 있다.

 

  • getter는 파라미터가 없는 메서드이며 리턴 타입은 원래 프로퍼티 타입과 같다. 이름은 프로퍼티 이름의 첫 번째 글자를 대문자로 바꾼 후 get을 붙여서 생성된다.
  • setter는 새로운 값 파라미터를 하나만 받는 메서드다. 이름은 프로퍼티 이름의 첫 번째 글자를 대문자로 바꾼 다음 set을 붙여서 생성된다.

 

코틀린의 Person 클래스는

class Person(var name: String, val age: Int)

 

자바에서 아래와 같고 코드에서 접근자 메서드를 통해 프로퍼티에 접근할 수 있다.

 

public final class Person {
   @NotNull
   private String name;
   private final int age;

   @NotNull
   public final String getName() { return this.name; }
   public final void setName(@NotNull String var1) { ... }
   public final int getAge() { return this.age; }
}


public class KotlinToJava {
    public static void main(String[] args) {
        Person person = new Person("Kancho", 30);
        System.out.println(person.getAge());

        person.setName("John");
        System.out.println(person);
    }
}

 

프로퍼티 이름이 is로 시작하는 경우에는

 

  • getter 이름은 프로퍼티와 같다
  • setter 이름은 맨 앞의 is를 set으로 바꾼 이름이다

 

isEmployed 프로퍼티가 추가된 Person 클래스

class Person(var name: String, val age: Int, var isEmployed: Boolean)

 

자바에서 isEmployed를 접근할 때 아래와 같이 사용한다.

public class KotlinToJava {
    public static void main(String[] args) {
        Person person = new Person("Kancho", 30, false);
        person.setEmployed(true);
        System.out.println(person.isEmployed());    // true
    }
}

 

코틀린 프로퍼티에 backing field가 필요한 경우 컴파일러가 접근자 메서드와 함께 필드도 만들어준다. 

기본적으로 필드는 비공개이기 때문에 자바에서 필요한 경우 @JvmField 어노테이션을 붙여줘야 한다.

 

class Person(@JvmField var name: String, @JvmField val age: Int)

 

@JvmField를 붙여주게 되면 자바에서 컴파일러가 생성해주는 필드에 접근할 수 있다.

이 경우 접근자 메서드는 생성되지 않고 backing field가 프로퍼티 자체와 동일한 가시성을 만들어진다.

public class KotlinToJava {
    public static void main(String[] args) {
        Person person = new Person("Kancho", 30);
        System.out.println(person.age);

        person.name = "John";
        System.out.println(person.name);
    }
}

 

프로퍼티 접근자에서 게터가 필드 값을 돌려주기만 하고 세터는 받은 값을 그대로 필드에 저장하기만 한다면 @JvmField를 사용할 수 없다.

 

class Person(val firstName: String, val familyName: String) {
    // error : This annotation is not applicable to target 'member property without backing field or delegate'
    @JvmField val fullName
        get() = "$firstName $familyName"
}

 

@JvmField를 추상 프로퍼티나 열린 프로퍼티에 사용할 수 없다. 프로퍼티를 오버라이드 해서 커스텀 접근자를 만들 수 있기 때문이다.

 

open class Person(val firstName: String, val familyName: String) {
    // error : JvmField can only be applied to final property
    @JvmField open var descsription: String = "Hello"
}

 

이름 붙은 객체의 프로퍼티에 대해 @JvmField를 사용하면 인스턴스 필드가 아닌 정적 필드를 만들어낸다.

object Application {
    @JvmField val name = "My Application"
}

 

Application의 name 프로퍼티는 자바에서 public이다.

@JvmField
@NotNull
public static final String name;
@NotNull
public static final Application INSTANCE;

 

따라서 Application.name 정적 필드를 통해 접근할 수 있다.

public class KotlinToJava {
    public static void main(String[] args) {
        System.out.println(Application.name);
    }
}

 

@JvmField가 붙지 않았다면 name은 private이 되어 getName 메서드를 통해 호출해야 한다.

@NotNull
private static final String name;
@NotNull
public static final Application INSTANCE;

@NotNull
public final String getName() {
   return name;
}

 

const 변경자가 붙은 프로퍼티도 정적 필드를 생성한다.

object Application {
    const val name = "My Application"
}

 

lateinit 프로퍼티를 사용하면 backing field를 노출할 수 있다.

lateinit 프로퍼티에는 @JvmField를 붙일 수 없다.

class Person(val firstName: String, val familyName: String) {
    lateinit var fullName: String
    
    fun init() {
        fullName = "$firstName $familyName"
    }
}

 

접근자와 backing field가 모두 프로퍼티와 동일한 가시성을 가지게 된다.

public class KotlinToJava {
    public static void main(String[] args) {
        Person person = new Person("Kancho", "Park");
        person.init();
        // 필드에 직접 접근
        System.out.println(person.fullName);
        // 접근자 호출
        System.out.println(person.getFullName());
    }
}

 

객체에서 lateinit은 @JvmField 어노테이션이 붙은 필드와 비슷한 정적 필드를 생성한다.

object Application {
    lateinit var name: String
}

 

lateinit 프로퍼티의 필드에 접근하는 경우와 접근자 메서드에 접근하는 경우의 차이이다.

public class KotlinToJava {
    public static void main(String[] args) {
        // 직접 프로퍼티 접근. 초기화 되기 전이라 null
        System.out.println(Application.name);
        // 접근자 호출
        Application.INSTANCE.setName("Application1");
        // 직접 프로퍼티 접근
        Application.name = "Application2";
    }
}

 

 

 

 

2. 파일 퍼사드와 최상위 선언


자바와 JVM에서는 일반적으로 모든 메서드는 클래스에 속해야 한다. 

코틀린에서는 최상위 선언을 자주 사용하기에 컴파일러는 최상위 함수와 프로퍼티를 자동으로 생성된 파일 퍼사드(file facade) 클래스에 넣는다. 퍼사드 클래스는 소스코드 파일 이름 뒤에 Kt를 붙인 이름이다.

 

Interoperability 파일을 컴파일하면

// Interoperability.kt

class Person(val firstName: String, val familyName: String)

val Person.fullName
    get() = "$firstName $familyName"

fun readPerson(): Person? {
    val fullName = readLine() ?: return null
    val p = fullName.indexOf(' ')
    return if (p >= 0) {
        Person(fullName.substring(0, p), fullName.substring(p + 1))
    } else {
        Person(fullName, "")
    }
}

 

아래와 같이 파일 이름 뒤에 Kt이 붙은 퍼사드 클래스가 만들어진다.

최상위에 선언된 클래스는 퍼사드 클래스에 들어가지 않는다.

public final class InteroperabilityKt {
   @NotNull
   public static final String getFullName(@NotNull Person $this$fullName) {
      Intrinsics.checkNotNullParameter($this$fullName, "$this$fullName");
      return $this$fullName.getFirstName() + ' ' + $this$fullName.getFamilyName();
   }

   @Nullable
   public static final Person readPerson() { ... }
}

// 클래스는 JVM과 자바에서 최상위 수준에 존재할 수 있다
public final class Person {
   @NotNull
   private final String firstName;
   @NotNull
   private final String familyName;

   @NotNull
   public final String getFirstName() { return this.firstName; }
   @NotNull
   public final String getFamilyName() { return this.familyName; }

   public Person(@NotNull String firstName, @NotNull String familyName) { ... }
}

 

퍼사드 클래스에 생성된 메서드는 정적 메서드이므로 자바에서 최상위 메서드에 접근할 때 퍼사드 클래스를 인스턴스화 하지 않아도 된다.

public class KotlinToJava {
    public static void main(String[] args) {
        Person person = InteroperabilityKt.readPerson();
        if (person == null) return;
        System.out.println(InteroperabilityKt.getFullName(person));
    }
}

 

@JvmName 어노테이션을 통해 퍼사드 클래스 이름을 지정할 수 있다.

@file:JvmName("Utils")
class Person(val firstName: String, val familyName: String)

 

자바에서는 Utils라는 지정된 이름으로 접근할 수 있게 된다.

public class KotlinToJava {
    public static void main(String[] args) {
        Person person = new Person("Kancho", "Park");
        System.out.println(Utils.getFullName(person));
    }
}

 

@JvmMultifileClass 어노테이션을 합치고 싶은 파일마다 붙여 @JvmName 어노테이션을 통해 클래스 이름을 지정하면 여러 파일에 있는 최상위 선언을 한 클래스로 모을 수 있다.

 

Interoperability 클래스

@file:JvmMultifileClass
@file:JvmName("MyUtils")

val Person.fullName
    get() = "$firstName $familyName"

 

utils1 클래스

@file:JvmMultifileClass
@file:JvmName("MyUtils")
fun readPerson(): Person? {
    val fullName = readLine() ?: return null
    val p = fullName.indexOf(' ')
    return if (p >= 0) {
        Person(fullName.substring(0, p), fullName.substring(p + 1))
    } else {
        Person(fullName, "")
    }
}

 

자바에서는 그대로 MyUtils 퍼사드 클래스를 사용할 수 있다.

public class KotlinToJava {
    public static void main(String[] args) {
        Person person = MyUtils.readPerson();
        if (person == null) return;
        System.out.println(MyUtils.getFullName(person));
    }
}

 

코틀린 코드에서는 퍼사드 클래스에 접근할 수 없고 JVM 클라이언트만 접근할 수 있다.

 

 

 

3. 객체와 정적 멤버


JVM에서 코틀린 객체 선언은 정적 INSTANCE 필드가 있는 클래스로 컴파일된다.

 

Application 객체 코틀린 클래스는

object Application {
    val name = "My Applicaton"
    fun exit() { }
}

 

정적인 INSTANCE 필드를 가지고 있다.

public final class Application {
   @NotNull
   private static final String name;
   @NotNull
   public static final Application INSTANCE;

   @NotNull
   public final String getName() { return name; }
   
   public final void exit() { }

   private Application() { }

   static {
      Application var0 = new Application();
      INSTANCE = var0;
      name = "My Applicaton";
   }
}

 

자바에서는 Application.INSTANCE 필드를 통해 객체의 멤버에 접근할 수 있다.

public class KotlinToJava {
    public static void main(String[] args) {
        System.out.println(Application.INSTANCE.getName());
        Application.INSTANCE.exit();
    }
}

 

@JvmStatic 어노테이션을 사용해 객체 함수나 프로퍼티 접근자를 정적 메서드로 만들 수 있다.

 

아래 @JvmStatic을 붙인 Application 클래스는

import java.io.InputStream

object Application {
    @JvmStatic var stdin: InputStream = System.`in`
    @JvmStatic fun exit() { }
}

 

아래와 같이 컴파일된다.

public final class Application {
   @NotNull
   private static InputStream stdin;
   @NotNull
   public static final Application INSTANCE;

   /** @deprecated */
   // $FF: synthetic method
   @JvmStatic
   public static void getStdin$annotations() {
   }

   @NotNull
   public static final InputStream getStdin() {
      return stdin;
   }

   public static final void setStdin(@NotNull InputStream var0) {
      Intrinsics.checkNotNullParameter(var0, "<set-?>");
      stdin = var0;
   }

   @JvmStatic
   public static final void exit() {
   }

   private Application() {
   }

   static {
      Application var0 = new Application();
      INSTANCE = var0;
      InputStream var10000 = System.in;
      Intrinsics.checkNotNullExpressionValue(var10000, "System.`in`");
      stdin = var10000;
   }
}

 

자바 코드에서 구체적인 인스턴스를 지정하지 않아도 함수나 프로퍼티에 접근할 수 있다.

public class KotlinToJava {
    public static void main(String[] args) {
        Application.setStdin(new ByteArrayInputStream("hello".getBytes()));
        Application.exit();
    }
}

 

 

 

4. 노출된 선언 이름 변경하기


@JvmName 어노테이션을 사용해 함수나 프로퍼티 접근자에도 적용할 수 있다. 이를 통해 함수나 프로퍼티 접근자 이름을 변경할 수 있다.

코틀린에서는 올바른 선언이지만 자바에서는 금지된 선언이 되는 시그니처 충돌을 막을 수 있다.

 

class Person(val firstName: String, val familyName: String)

val Person.fullName
    get() = "$firstName $familyName"

// 플랫폼 선언 충돌: 다음 선언에 동일한 JVM 시그니처가 있습니다(getFullName(LPerson;)Ljava/lang/String;).
fun getFullName(person: Person): String {
    return "${person.familyName}, ${person.firstName}"
}

 

위 코드에서 getFullName 메서드는 JVM에서 프로퍼티와 함수가 똑같은 시그니처의 메서드를 만들어내서 모호성이 생기기 때문에 컴파일러가 오류를 표시한다.

 

getFullName 메서드에 @JvmName 어노테이션을 붙여 이름을 바꿔 해결할 수 있다.

@JvmName("getFullNameFamilyFirst")
fun getFullName(person: Person): String {
    return "${person.familyName}, ${person.firstName}"
}

 

자바에서는 지정된 이름으로 호출할 수 있지만 코틀린에서는 원래의 이름을 사용해야 한다.

fun main() {
    val person = Person("Kancho", "Park")
    println(getFullName(person)) // Park, Kancho
}

 

또한 @JvmName을 통해 프로퍼티 접근자에 적용되는 표준 명명 방식을 우회할 수 있다.

// mutable 변수에만 가능하다
class Person(
    @set:JvmName("changeName")
    var name: String,
    val age: Int
)

 

자바 코드에서는 setName 대신에 changeName() 메서드가 생기게 되어 이를 사용할 수 있다.

public class KotlinToJava {
    public static void main(String[] args) {
        Person person = new Person("Kancho", 30);
        person.changeName("John");
        System.out.println(person.getName());  // John
    }
}

 

또한 @JvmName은 코틀린 함수 이름이 자바 키워드와 우연히 겹쳐서 사용할 수 없는 경우에도 유용하다.

goto 는 자바에서 예약된 키워드이므로 goto() 함수를 호출할 수 없기에 이름을 지정해주면 사용할 수 있다.

class Person(val firstName: String, val familyName: String) {
    @JvmName("visit")
    fun goto(person: Person) {
        println("$this is visiting $person")
    }

    override fun toString(): String = "$firstName $familyName"
}

 

 

 

5. 오버로딩한 메서드 생성하기


코틀린 함수에서 디폴트 값이 지정된 경우 함수 인자 중 일부를 생략할 수 있다.

 

fun restrictToRange(
    what: Int,
    from: Int = Int.MIN_VALUE,
    to: Int = Int.MAX_VALUE
): Int = max(from, min(to, what))

fun main() {
    println(restrictToRange(100, 1, 10))    // 10
    println(restrictToRange(100, 1))        // 100
    println(restrictToRange(100))           // 100
}

 

하지만 위 함수는 자바에서 아래와 같이 정의된다.

자바에는 디폴트 개념이 없으므로 항상 모든 인자를 넘겨야 한다.

public static final int restrictToRange(int what, int from, int to) {
   int var3 = Math.min(to, what);
   return Math.max(from, var3);
}

public class KotlinToJava {
    public static void main(String[] args) {
        System.out.println(EtcKt.restrictToRange(100, 1, 10));
        System.out.println(EtcKt.restrictToRange(100, 1));  // Error
    }
}

 

@JvmOverloads 어노테이션을 사용하면 된다.

@JvmOverloads를 적용하면 원래의 코틀린 함수 외에 오버로딩된 함수를 추가로 생성해준다.

@JvmOverloads
fun restrictToRange(
    what: Int,
    from: Int = Int.MIN_VALUE,
    to: Int = Int.MAX_VALUE
): Int = max(from, min(to, what))

 

위 함수는 자바에서 아래와 같이 컴파일된다.

public final class EtcKt {
   @JvmOverloads
   public static final int restrictToRange(int what, int from, int to) {
      int var3 = Math.min(to, what);
      return Math.max(from, var3);
   }

   // 추가로 오버로딩된 함수
   @JvmOverloads
   public static final int restrictToRange(int what, int from) {
      return restrictToRange$default(what, from, 0, 4, (Object)null);
   }
   // 추가로 오버로딩된 함수
   @JvmOverloads
   public static final int restrictToRange(int what) {
      return restrictToRange$default(what, 0, 0, 6, (Object)null);
   }
   
   public static int restrictToRange$default(int var0, int var1, int var2, int var3, Object var4) {
      if ((var3 & 2) != 0) {
         var1 = Integer.MIN_VALUE;
      }

      if ((var3 & 4) != 0) {
         var2 = Integer.MAX_VALUE;
      }

      return restrictToRange(var0, var1, var2);
   }

}

 

이제 자바에서 파라미터를 생략하고 함수를 사용할 수 있게 된다.

public class KotlinToJava {
    public static void main(String[] args) {
        System.out.println(EtcKt.restrictToRange(100, 1, 10));
        System.out.println(EtcKt.restrictToRange(100, 1));  // OK
    }
}

 

 

 

 

 

6. 예외 선언하기


코틀린은 검사 예외와 비검사 예외를 구분하지 않는다. 함수나 프로퍼티는 예외의 유형과 무관하게 코드를 추가할 필요 없이 예외를 그냥 던지면 된다.

자바에서는 명시적으로 함수 본문에서 처리하지 않고 외부로 던져지는 검사 예외 목록을 함수에 추가해야 한다.

 

// Etc.kt
fun loadData() = File("data.txt").readLines()

 

위 함수를 자바에서 아래와 같이 사용할 수 있다.

public class KotlinToJava {
    public static void main(String[] args) {
        for (String line : EtcKt.loadData()) {
            System.out.println(line);
        }
    }
}

 

만약 파일을 읽을 수 없으면 함수는 IO Exception을 던지게 된다.

이때 자바에서 예외 처리를 추가하려고 하면 에러가 발생한다.

public class KotlinToJava {
    public static void main(String[] args) {
        try {
            for (String line : EtcKt.loadData()) {
                System.out.println(line);
            }
        } catch (IOException e) {   // Error: 'java.io.IOException'은(는) 해당 try 블록에서 한 번도 던져지지 않습니다
            System.out.println("Can't load data");
        }
    }
}

 

자바에서는 try 블록 안의 코드에서 발생하는 것으로 선언되지 않은 검사 예외를 catch로 처리하는 것을 금지하기 때문이다.

 

loadData 함수는 자바에서 아래와 같이 표현된다.

@NotNull
public static final List loadData() {
   return FilesKt.readLines$default(new File("data.txt"), (Charset)null, 1, (Object)null);
}

 

이 함수는 예외에 대해 정보를 제공하지 않으므로 @Throws 어노테이션을 사용해 예외 클래스를 지정하면 된다.

@Throws(IOException::class)
fun loadData() = File("data.txt").readLines()

 

어노테이션을 사용하게 되면 예외를 처리할 수 있는 메서드로 표현된다.

@NotNull
public static final List loadData() throws IOException {
   return FilesKt.readLines$default(new File("data.txt"), (Charset)null, 1, (Object)null);
}

 

 

 

7. 인라인 함수


자바에는 인라인 함수가 없다. 

코틀린에서 inline 변경자가 붙은 함수는 자바에서 일반 메서드로 노출된다.

 

구체화한 타입 파라미터가 있는 제네릭 인라인 함수의 경우, 인라인을 사용하지 않고 타입 구체화를 구현할 방법이 없어서 자바에서는 이런 함수를 호출할 수 없다.

 

cast 함수

inline fun <reified T : Any> Any.cast(): T? = this as? T

 

cast 함수는 퍼사드 클래스의 비공개 멤버로 노출되기에 외부에서 함수를 호출할 수 없다.

public class KotlinToJava {
    public static void main(String[] args) {
        EtcKt.<Integer>cast("");	// error
    }
}

 

 

 

8. 타입 별명


코틀린 타입 별명은 자바에서 사용할 수 없다.

자바에서 타입 별명을 참조하는 선언은 모두 원래 타입을 가리키는 것으로 보인다.

 

 

typealias Name = String
class Person(val firstName: Name, val familyName: Name)

 

Person 정의는 Name 별명을 String으로 치환한 Person 클래스로 보인다.

public class KotlinToJava {
    public static void main(String[] args) {
        Person person = new Person("Kancho", "Park");
        System.out.println(person.getFamilyName());
    }
}

 

'코틀린' 카테고리의 다른 글

[코틀린] 코틀린 테스팅  (0) 2022.12.18
[코틀린] DSL (도메인 특화 언어)  (0) 2022.11.27
[코틀린] Annotation  (0) 2022.11.21
[코틀린] 제네릭  (0) 2022.11.13
[코틀린] 상속(Inheritance)  (0) 2022.11.06