개발/코틀린

(코틀린) 자바 상호 운용성

DinoDev 2022. 12. 3. 23:41
728x90
반응형

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

코틀린은 JVM을 주 대상으로 설계됐기 때문에 자바 코드를 코틀린에서 쉽게 사용할 수 있습니다.

자바 메서드와 필드

Unit과 void

자바에서 void는 코틀린에서 Unit으로 변환되서 사용합니다.

연산자 관습

Map.get()과 같은 몇몇 자바 메서드는 코틀린에서 연산자 관습을 만족합니다. 자바 메서드에는 operator가 붙어있지 않지만 코틀린에서는 연산자처럼 사용할 수 있습니다. 그래서 List나 Map의 get을 코틀린에서는 []로 접근 할 수 있습니다.

합성 프로퍼티

자바에는 합성 프로퍼티가 없고 getter와 setter를 사용하는 일이 많습니다. 이로 인해 코틀린 컴파일러는 자바의 getter와 setter를 일반적인 프로퍼티처럼 사용할 수 있게 변형 해줍니다.

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;
    }
}

위와 같은 자바 클래스가 있다고 가정하고 아래 코틀린 코드에서 어떻게 사용하는지 확인해봅니다.

fun main() {
    val person = Person("석준", 31)
    person.name = "Dino"
    person.age = 32
    println("${person.name}, ${person.age}") // Dino, 32
}

이렇게 자바에서 getter와 setter를 코틀린에서 일반 프로퍼티처럼 접근해서 사용 할 수 있습니다.

플랫폼 타입

자바가 널이 될 수 있는 타입과 그렇지 않는 타입을 구분하지 않기 때문에 일반적으로 코틀린 컴파일러는 자바 코드에서 비롯된 객체가 널인지 여부에 대해 아무런 가정도 할 수 없습니다. 코틀린에서 자바 코드로부터 비롯된 객체는 플랫폼 타입(platform type)이라는 특별한 타입에 속합니다. 플랫폼 타입은 널이 될 수 있는 타입이기도 하고 널이 될 수 없는 타입이기도 하며, 이런 타입에 대한 타입 안전성 보증은 기본적으로 자바와 동일합니다. 플랫폼 타입은 NPE(NullPointerException)가 발생할 수도 있습니다.

fun main() {
    val person = Person(null, 25)
    println(person.name.length) // NPE
}

널 가능성 Annotation

자바 세계에서 널 안전성을 보장하는 방법은 @NotNull annotation을 사용하는 것 입니다.

위 Person예제에서 name에 @NotNull을 넣어주게 되면 name은 코틀린에서 nonnull 타입으로 사용할 수 있습니다.

import org.jetbrains.annotations.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;
    }
}

자바/코틀린 타입 매핑

int, boolean같은 원시타입이나 List, Map같은 컬렉션타입이 코틀린에서 맵핑되서 사용할 수 있습니다.

byte/Byte Byte
short/Short Short
int/Integer Int
long/Long Long
char/Character Char
float/Float Float
double/Double Double

제네릭 타입의 매핑은 양 언어 제네릭 구문의 차이 때문에 단순하지만은 않은 변환이 필요합니다.

  • 자바의 extends 와일드카드는 코틀린 공변 프로젝션으로 변환됩니다. TreeNode<? extends Person>은 TreeNode<out Person>으로 변경됩니다.
  • 자바의 super 와일드카드는 코틀린 불공변 프로젝션으로 변환됩니다. TreeNode<? super Person>은 TreeNode<in Person>으로 변경됩니다.
  • 자바의 로우 타입(raw type)은 코틀린 스타 프로젝션으로 바뀐다. TreeNode는 TreeNode<*>으로 변경됩니다.

단일 추상 메서드 인터페이스(Single Abstract Method)

추상 메서드가 하나뿐인 자바 인터페이스가 있다면 이 인터페이스는 기본적으로 코틀린 함수 타입처럼 동작합니다.

자동으로 람다와 적절한 SAM 타입 인스턴스 사이의 변환을 지원해주는 자바8+와 비슷합니다.

public interface Runnable {
    void run();
}
import java.util.concurrent.ScheduledThreadPoolExecutor

fun main() {
    val executor = ScheduledThreadPoolExecutor(5)
    executor.execute(object : Runnable {
        override fun run() {
            println("Working on Asynchronous task...")
        }
    })
    executor.execute { println("Working on Asynchronous task...") }
    executor.shutdown()
}

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

IntelliJ 에서 자바 소스 파일을 코틀린 코드로 변환해주는 도구가 있습니다. cmd + option + shift + k를 누르면 자바 파일을 코틀린 파일로 변경해줍니다.

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

자바에 직접적으로 대응하는 코틀린 기능이 없을 수 있습니다. 코틀린의 기능을 자바에서 사용할 수 있는 방법을 알아보겠습니다.

프로퍼티 접근

자바나 JVM에는 프로퍼티 개념이 없기 때문에 코틀린의 프로퍼티는 메서드로 표현됩니다.

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

    public static void main(String[] args) {
        Person person = new Person("석준", 31);
        System.out.println(person.getAge()); // 31

        person.setName("Dino");
        System.out.println(person.getName()); // Dino
    }
}

 프로퍼티에 @JvmField annotation을 붙이면 get, set method가 아니라 프로퍼티 처럼 접근 할 수 있습니다.

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

    public static void main(String[] args) {
        Person person = new Person("석준", 31);
        System.out.println(person.age); // 31

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

@JvmField annotation은 커스텀 게터에 사용할 수 없고 final이 아닌 프로퍼티에 사용할 수 없습니다.

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

    // JvmField can only be applied to final property
    @JvmField open var description: String = "Hello"
}

object의 프로퍼티에도 @JvmField를 적용할 수 있습니다. const가 붙은 프로퍼티도 정적 필드로 제공 됩니다.

object Application {
    @JvmField val name1 = "My Application"
    const val name2 = "My Application"
}
public class Main {

    public static void main(String[] args) {
        System.out.println(Application.name1);
        System.out.println(Application.name2);
    }
}

lateinit var를 사용하면 두가지 형태로 사용이 가능합니다.

class Person(val firstName: String, val familyName: String) {
    lateinit var fullName: String

    fun init() {
        fullName = "$firstName, $familyName"
    }
}
public class Main {

    public static void main(String[] args) {
        Person person = new Person("석준", "정");
        person.init();

        // 필드에 직접 접근
        System.out.println(person.fullName);
        // 접근자 호출
        System.out.println(person.getFullName());

    }
}

파일 퍼사드와 최상위 선언

코틀린에서는 파일에 바로 선언하는 최상위 선언을 자주 사용합니다. 이런 프로퍼티나 함수를 자바에서 접근하기 위해서는 파일 이름 뒤에 Kt를 덧붙인 클래스가 생성되는데 이것을 파일 퍼사드라고 부릅니다.

// Util.kt
class Person(val firstName: String, val familyName: String)

val Person.fulName
    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, "")
    }
}
public class Main {
    public static void main(String[] args) {
        Person person = UtilKt.readPerson();
        if (person == null) return;
        System.out.println(UtilKt.getFulName(person));
    }
}

@JvmName annotation을 사용하면 퍼사드 클래스 이름을 지정할 수 있습니다.

@file:JvmName(name = "MyUtils")
class Person(val firstName: String, val familyName: String)

val Person.fullName: String
    get() = "$firstName $familyName"
public class Main {
    public static void main(String[] args) {
        Person person = new Person("석준", "정");
        System.out.println(MyUtils.getFullName(person));
    }
}

@JvmMultifileClass과 @JvmName을 같이 사용하게 되면 파일이 달라도 하나의 퍼사드 클래스로 지정할 수 있습니다.

객체와 정적 멤버

object에 val를 선언 해서 사용할 때 INSTANCE필드를 접근해서 사용해야만 합니다. 하지만 @JvmStatic annotation을 사용하게 되면 INSTANCE필드를 사용하지 않고 접근 할 수 있습니다.

import java.io.InputStream

object Application {
    @JvmStatic var stdin: InputStream = System.`in`
    @JvmStatic fun exit() {}
}
import java.io.ByteArrayInputStream;

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

노출된 선언 이름 변경하기

@JvmName을 사용하면 퍼사드 클래스 이름을 변경할 수 있지만 함수나 프로퍼티에 사용하면 함수나 프로퍼티의 이름도 변경 할 수 있습니다. 이 방식은 코틀린에서는 허용하는 예약어를 자바에서 허용하지 않을 때 종종 사용합니다.

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

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

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

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

코틀린 함수는 디폴트 값을 가질 수 있지만 이 함수를 자바에서 그대로 사용하는 경우 모든 파라미터를 전달 해줘야 합니다. 이런 경우에는 @JvmOverloads annotation을 사용하면 디폴트 값을 가진 함수를 사용 할 수 있습니다.

import kotlin.math.max
import kotlin.math.min

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

예외 선언하기

코틀린에서는 명시적으로 함수가 예외를 던진다고 선언할 필요가 없지만 자바는 명시적으로 선언해야합니다. 이때 @Throws annotation을 사용해서 처리 합니다.

import java.io.File
import java.io.IOException

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

public class Main {
    public static void main(String[] args) {
        try {
            for (String line : UtilKt.loadData()) {
                System.out.println(line);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

인라인 함수

자바에는 인라인 함수가 없기 대문에 코틀린에서는 inline 변경자가 붙은 함수는 자바에서 일반 메서드로 호출 할 수 있지만 인라인되지는 않습니다.

타입 별명

코틀린 타입 별명은 자바 코드에서 쓸 수 없습니다. 

728x90
반응형

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

(코틀린) Testing with Kotest  (0) 2022.12.18
(코틀린) 동시성  (0) 2022.12.11
(코틀린) 도메인 특화 언어(DSL)  (0) 2022.11.27
(코틀린) 제네릭스  (0) 2022.11.12
(코틀린) 클래스 계층 이해하기  (0) 2022.11.06