2장 코틀린 기초

코틀린 쿡북 2장을 요약한 내용 입니다.

코틀린에서 널 허용 타입 사용하기

널 허용(nullable) 타입은 안전 호출 연산자(?.)나 엘비스 연산자(?:)와 결합해서 사용한다.

// Person 클래스 
class Person(val first: String,
							val middel: String?,
							val last: String)

middle 값에 따라서 middleNameLength 값을 채우려고 할 때, 자바에 익숙하다면 우리는 다음과 같이 작성할 것이다.

val p = Person(first = "north", middle = null, last = "West")
if (p.middle != null) {
	val middleNameLength = p.middle.length
}

그런데 만약 p가 val 대신 var을 사용한다면 변수 p가 정의된 시점과 p의 middle 속성에 접근하는 시점 중간에 값이 변경되었을 수도 있다고 가정한다면 위와 같은 코드는 영리한 타입 변환을 수행하지 않는다. 이를 우회하는 한 가지 방법은 널 아님 단언 연산자라 부르는 거듭 느낌표(!!)를 사용하는 것이다.

val middleNameLength = p.middle!!.length

그러나 널 아님 단언 연산자 !!가 하나라도 있다면 이는 코드 스멜이다. 널 값에 !! 연산자를 사용하는 것은 코틀린에서 NullPointerException을 만날 수 있는 몇 가지 상황 중 하나이므로, 가능하면 사용하지 않도록 노력하자.

안전 호출 연산자 사용하기

val p = Person(first = "north", middle = null, last = "West")
val middleNameLength = p.middle?.length

middleNameLength의 타입은 아마 사용하고자 했던 타입이 아니라 Int? 타입이므로 다음과 같이 안전 호출 연산자와 엘비스 연산자(?:)를 병행해서 사용하는 것이 유용하다.

안전 호출 연산자와 엘비스 연산자

val p = Person(first = "north", middle = null, last = "West")
val middleNameLength = p.middle?.length ?: 0

안전 타입 변환 연산자

안전 타입 변환 연산자의 목적은 타입 변환이 올바르게 동작하지 않는 경우 ClassCastException이 발생하는 상황을 방지하는 것이다. 예를 들어 어떤 인스턴스를 Person 타입으로 변환을 시도했지만 해당 인스턴스가 널일 수도 있는 상황이라면 다음과 같이 작성할 수 있다.

val p1 = p as? Person

자바를 위한 메소드 중복

기본 파라미터를 가진 코틀린 함수가 있는데, 자바에서 각 파라미터의 값을 직접적으로 명시하지 않고 해당 코틀린 함수를 호출하고 싶다면 @JvmOverloads 애노테이션을 해당 함수에 추가해라.

fun addProduct(name: String, price: Doule = 0.0, desc: String? = null) =
	"Adding product with $name, ${desc ?: "None" }, and " + 
		NumberFormat.getCurrencyInstance().format(price)

addProduct 함수는 문자열 name이 필수지만 description과 price는 기본값이 있다. description에 널을 할당할 수 있고 기본값도 널인 반면 price의 기본값은 0이다. 그러므로 1개 또는 2개나 3개의 인자와 함께 addProduct 함수를 쉽게 호출할 수 있다.

@Test
fun `check all overloads`() {
	assertAll("Overloads called from Kotlin",
		{ println(addProduct("Name", 5.0, "Desc")) },
		{ println(addProduct("Name", 5.0)) },
		{ println(addProduct("Name")) },
	)
}

Optional이나 널 허용 속성은 함수 시그니처의 마지막에 위치시켜야 한다. 그렇게 해야만 위치 인자를 사용해서 함수를 호출할 때 Optional 또는 널 허용 속성을 생략할 수 있다.

하지만 자바는 메소드 기본 인자를 지원하지 않기 때문에 자바에서 addProduct 함수를 호출하려면 파라미터를 다 채워야 한다.

@Test
void supplyAllArguments() {
	System.out.println(OverloadsKt.addProduct("Name", 0.5, "Desc"));
}

그러나 addProduct 함수에 @JvmOverloads 애노테이션을 추가하면 컴파일 후 생성된 클래스는 다음과 같이 모든 함수 중복을 지원할 것이다.

@Test
fun checkOverloads() {
	assertAll("Overloads called from Kotlin",
		{ System.out.println(addProduct("Name", 5.0, "Desc")) },
		{ System.out.println(addProduct("Name", 5.0)) },
		{ System.out.println(addProduct("Name")) },
	)
}

함수 중복이 동작하는 이유는 코틀린이 생성한 바이트코드를 디컴파일하면 알 수 있다. 다음은 @JvmOverloads 애노테이션이 없이 생성된 코드이다.

@NotNull
public static final String addProduct(@NotNull String name,
	double price, @Nullable String desc) {
		Intrinsics.checkParameterIsNotNull(name, "name");
		// ...
}

반면 @JvmOverloads 애노테이션을 추가한 함수를 디컴파일한 결과는 다음과 비슷하다.

@JvmOverloads
@NotNull
public static final String addProduct(
	@NotNull String name,
	double price, 
	@Nullable String desc
) {
		Intrinsics.checkParameterIsNotNull(name, "name");
		// ...
}

@JvmOverloads
@NotNull
public static final String addProduct(
	@NotNull String name,
	double price
) {
		Intrinsics.checkParameterIsNotNull(name, "name");
		// ...
}

@JvmOverloads
@NotNull
public static final String addProduct(
	@NotNull String name
) {
		Intrinsics.checkParameterIsNotNull(name, "name");
		// ...
}

to로 Pair 인스턴스 생성하기

직접 Pair 클래스의 인스턴스를 생성하기보다는 중의(inflix) to 함수를 사용해라

Pair는 first와 second라는 이름의 두 개의 원소를 갖는 데이터 클래스다.

data class Pair<out A, out B> : seriaizable

2개의 인자를 받는 생성자를 사용해서 Pair 클래스를 생성할 수 있지만 to 함수를 사용하는 것이 더 일반적이다.

@Test
fun `create map using infix to function`() {
	val map = mapOf("a" to 1, "b" to 2, "c" to 2, p1,  Pair("a", 1))

	assertAll(
		{ assertThat(map, hasKey("a")) },
		{ assertThat(map, hasKey("b")) },
		{ assertThat(map, hasKey("c")) }
	)
}

@Test
fun `create a Pair from constructor vs to function`() {
	val p1 = Pair("a", 1)
	val p2 = "a" to 1

	assertAll(
		{ assertThat(p1.first, `is`("a")) },
		{ assertThat(p1.second, `is`("1")) },
		{ assertThat(p2.first, `is`("a")) },
		{ assertThat(p2.second, `is`("1")) },		
		{ assertThat(p1, `is`("a")) }
	)
}

3개의 값을 나타내는 Triple이라는 이름의 클래스가 코틀린 표준 라이브러리에 들어 있다. Pair와는 다르게 Triple 인스턴스를 생성하는 확장 함수는 존재하지 않지만 3개의 인자를 받는 생성자를 직접 사용해서 인스턴스를 생성할 수 있다.

Last updated