[Kotlin in Action] part6_코틀린 타입 시스템

지슈·2023년 2월 7일
0

Kotlin in Action

목록 보기
6/9

널 가능성

널이 될 수 있는 타입

코틀린은 널이 될 수 있는 타입을 명시적으로 지원한다는 점이 특징이다.

널이 될 수 있는 타입 == 프로퍼티나 변수에 null을 허용

null을 허용하는 인자를 받을 수 있게 하려면 타입 이름 뒤에 ?를 명시한다.

String은 문자열만 인자로 받을 수 있는 타입이지만,

Sring? 은 null 또는 문자열을 인자로 받을 수 있는 타입이 된다는 것이다.

fun strLenSafe(s: String?): Int =
if (s != null) s.length else 0
>>> val x: String? = null
>>> println(strLenSafe(x))
0
>>> println(strLenSafe("abc"))
3

타입의 의미

자바의 타입 시스템은 null을 잘 다루지 못한다. 자바의 String 타입은 null과 String 두 가지 종류의 값을 포함하지만 두 종류의 연산은 완전히 다르기 때문에 자바는 이 둘을 구분하지 못한다는 것이다.

반면, 코틀린의 null이 될 수 있는 타입은 이에 대한 해법을 제시한다.

null이 될 수 있는 타입과 null이 될 수 없는 타입을 구분한다는 것이다.

안전한 호출 연산자: ?.

?. 연산자로 메서드를 호출하는 경우,

호출하려는 값이 null이면 null이 결과값이 되고, 호출하려는 값이 null이 아니면, 일반 호출처럼 작동한다.

값이 null이 될 가능성이 있다면, ?. 연산자로 안전하게 호출하는 것이다.

class Employee(val name: String, val manager: Employee?)
fun managerName(employee: Employee): String? = employee.manager?.name
>>> val ceo = Employee("Da Boss", null)
>>> val developer = Employee("Bob Smith", ceo)
>>> println(managerName(developer))
Da Boss
>>> println(managerName(ceo))
null

엘비스 연산자: ?:

엘비스 연산자는 null 대신 사용할 디폴트 값을 지정할 때 편리하게 사용된다.

fun foo(s: String?) {
val t: String = s ?: ""
}

이 연산자는,

좌항 값이 null이 아니면 좌항 값을 사용하고 좌항 값이 null이면 우항 값을 결과로 한다.

안전한 캐스트: as?

as? 연산자는 어떤 값을 지정한 타입으로 캐스트한다. 대상 타입으로 반환할 수 없으면 null을 반환한다.

class Person(val firstName: String, val lastName: String) {
override fun equals(o: Any?): Boolean {
val otherPerson = o as? Person ?: return false
return otherPerson.firstName == firstName &&
otherPerson.lastName == lastName
}
override fun hashCode(): Int =
firstName.hashCode() * 37 + lastName.hashCode()
}
>>> val p1 = Person("Dmitry", "Jemerov")
>>> val p2 = Person("Dmitry", "Jemerov")
>>> println(p1 == p2)
true
>>> println(p1.equals(42))
false

널 아님 단언: !!

!!을 사용하면 어떤 값이든 null이 될 수 없는 타입으로 (강제로) 바꿀 수 있다.

널 아님 단언을 사용하는 예제이다.

fun ignoreNulls(s: String?) {
val sNotNull: String = s!!
println(sNotNull.length)
}
>>> ignoreNulls(null)
Exception in thread "main" kotlin.KotlinNullPointerException
at <...>.ignoreNulls(07_NotnullAssertions.kt:2)

null 아님 단언을 사용하는 경우는 어떤 경우일까

어떤 함수의 값이 null인지 확인하고 다른 함수를 호출해도 안전하게 값을 사용할 수 있다.

하지만 null이 아닌 값을 전달받는다는 사실이 분명하다면 이 검사를 수행하지 않을 수 있다.

이 경우, null 아님 단언을 사용할 수 있다.

let 함수

let 함수는 null이 될 수 있는 값을 null이 아닌 값만 인자로 받는 함수에 넘기는 경우에 사용할 수 있다.

let 함수는 자신의 수신 객체를 인자로 전달받은 람다에게 넘긴다.

null이 될 수 있는 값에 대해 안전한 호출 구문을 사용해 let을 호출하되

null이 될 수 없는 타입을 인자로 받는 람다를 let에 전달한다.

나중에 초기화할 프로퍼티

나중에 초기화할 프로퍼티는 항상 var이다. (val 프로퍼티는 생성자 안에서 반드시 초기화해야 함)

널이 될 수 있는 타입 확장

확장 함수를 사용하면 직접 변수에 대해 메서드를 호출해도 확장 함수인 메서드가 알아서 null을 처리해주기 때문에 편리하다.

널이 될 수 있는 타입의 확장 함수는 안전한 호출 없이도 호출 가능하다.

타입 파라미터의 널 가능성

코틀린에서는 함수나 클래스의 모든 타입 파라미터는 기본적으로 null이 될 수 있다.

타입 파라미터 T를 클래스나 함수 안에서 타입으로 사용하면 이름 끝에 ?가 없더라도 T가 null이 될 수 있는 타입이다.

fun <T> printHashCode(t: T) {
println(t?.hashCode())
}
>>> printHashCode(null)
null

T 파라미터의 타입 이름 T에는 물음표가 붙어있지 않지만 T는 null을 받을 수 있다.

널 가능성과 자바

자바와 코틀린을 조합할 때 null을 검사할 수 있는 방법은

  1. 자바 코드에 애노테이션으로 표시된 널 가능성 정보가 있다면 코틀린도 그 정보를 활용한다.

자바의 @Nullable String코틀린의 String? 과 같고,

자바의 @NotNullable String코틀린의 String 과 같다.

  1. 플랫폼 타입은 코틀린이 널 관련 정보를 알 수 없는 타입을 말한다.

이를 널이 될 수 있는 타입으로도, 널이 될 수 없는 타입으로도 처리할 수 있다.

이 플랫폼 타입의 값이 널이 아님을 알고 있다면 검사 없이 그 값을 직접 사용할 수 있고, 만약 널이라면 NullPointerException 이 발생한다.

코틀린의 원시 타입

원시 타입: Int, Boolean 등

코틀린은 원시 타입과 래퍼 타입을 구분하지 않는다.

또 코틀린에서는 숫자 타입 등 원시 타입 값에 대해 메서드를 호출할 수 있다.

Int 와 같은 코틀린 타입에는 null 참조가 들어갈 수 없기 때문에 그에 상응하는 자바 원시 타입으로 컴파일 가능하다.

널이 될 수 있는 원시 타입: Int?, Boolean? 등

Int? 타입의 두 값을 비교하려면 먼저 null 검사를 해야한다. 검사를 한 뒤, 두 값을 일반적인 값처럼 취급한다.

숫자 변환

코틀린은 숫자를 변환하는 방식이 자바와 또 다르다.

코틀린은 한 타입의 숫자를 다른 타입의 숫자로 자동 변환하지 않는다.

대신 직접 변환 메서드를 호출한다.

val i = 1
val l: Long = i.toLong()

코틀린은 타입 변환을 명시하기로 하는 규칙이 있다.

val x = 1
val list = listOf(1L, 2L, 3L)
x in list     //false

타입을 명시적으로 변환해서 같은 타입의 값으로 만든 후 비교해야 한다.

숫자 리터럴은 보통 변환 함수를 호출할 필요가 없다. 별도의 변환 없이도 잘 작동한다.

Any, Any?: 최상위 타입

코틀린에서는 Any 타입이 모든 null 이 될 수 없는 타입의 조상 타입이다.

자바와 달리, 코틀린에서는 Any가 Int 등의 원시 타입을 포함한 모든 타입의 조상 타입이다.

(Any는 null이 될 수 없는 타입이다!)

null을 포함한 모든 값을 대입할 변수를 선언할 때는 Any? 타입을 사용해야한다.

val answer: Any = 42
//Any가 참조 타입이기 때문에 42가 박싱된다.

Unit 타입: 코틀린의 void

코틀린의 Unit 타입은 자바 void와 비슷한 기능을 한다.

아무것도 반환하지 않는 함수의 반환 타입으로 Unit을 쓸 수 있다.

코틀린의 Unit 타입이 자바의 void와 다른 점은 Unit은 모든 기능을 갖는 일반적인 타입이고,

void와 달리 Unit을 타입 인자로 쓸 수 있다는 것이다.

Nothing 타입: 이 함수는 결코 정상적으로 끝나지 않는다

코틀린에서는 Nothing이라는 반환 타입을 쓰면 정상적으로 끝나지 않는 함수를 표시할 수 있다.

Nothing 타입은 아무 값도 저장하지 않는다.

fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
>>> fail("Error occurred")
java.lang.IllegalStateException: Error occurred

컬렉션과 배열

널 가능성과 컬렉션

변수 타입 뒤에 ?를 붙이면 그 변수에 null을 저장할 수 있다는 뜻인 것처럼

타입 인자로 쓰인 타입에도 ?를 붙일 수 있다.

List<Int?>는 Int?타입의 값을 저장할 수 있다. Int 또는 null을 저장할 수 있다는 것이다.

어떤 변수 타입의 널 가능성과 타입 파라미터로 쓰이는 타입의 널 가능성 사이에는 어떤 차이가 있을까.

List<Int?>의 경우 리스트 자체는 항상 null이 아니다. 하지만 리스트의 각 원소는 null이 될 수 있다.

List<Int>?의 경우 리스트는 null이 될 수 있지만 리스트의 각 원소는 null이 아닌 값만 들어갈 수 있다.

읽기 전용과 변경 가능한 컬렉션

코틀린에서는 컬렉션 안의 데이터에 접근하는 인터페이스와, 컬렉션 안의 데이터를 변경하는 인터페이스를 분리한다.

kotlin.collections.Collection 인터페이스를 사용하면 컬렉션 안의 원소를 이터레이션하고, 크기를 얻고, 어떤 값인지 검사하고, 데이터를 읽을 수 있지만 원소를 추가하거나 제거할 수는 없다.

kotlin.collection.MutableCollection 인터페이스를 사용하면 원소를 추가하거나 삭제하거나 원소를 모두 지울 수 있는 메서드를 제공한다.(kotlin.collections.Collection을 확장함)

이렇게 인터페이스를 구분하는 이유는 데이터에 어떤 일이 일어나는지 쉽게 이해하기 위해서이다.

MutableCollection 이 아닌 Collection 타입의 인자를 받는다면 그 함수가 컬렉션을 읽기만 한다는 것을 알 수 있다.

코틀린 컬렉션과 자바

코틀린은 모든 자바 컬렉션 인터페이스에 대해 읽기 전용과 변경 가능한 인터페이스를 제공한다.

변경 가능한 인터페이스는 읽기 전용 인터페이스를 확장한다.

자바는 읽기 전용 컬렉션과 변경 가능 컬렉션을 구분하지 않으므로 코틀린에서 읽기 전용 Collection으로 선언된 객체라도 자바 코드에서는 그 객체의 내용을 변경할 수 있다. 그러나 코틀린에서는 이를 받아도변경할 수 없다.

따라서 자바/코틀린 혼용 프로그램을 쓸 때 호출하려는 자바 코드가 컬렉션을 변경할지에 따라 올바른 파라미터 타입을 사용할 책임은 사용자에게 있다.

/* Java */
// CollectionUtils.java
public class CollectionUtils {
public static List<String> uppercaseAll(List<String> items) {
for (int i = 0; i < items.size(); i++) {
items.set(i, items.get(i).toUpperCase());
}
return items;
}
}
// Kotlin
// collections.kt
fun printInUppercase(list: List<String>) {  //읽기 전용 파라미터 선언
println(CollectionUtils.uppercaseAll(list)) //컬렉션 변경하는 자바 함수 호출
println(list.first())                       //컬렉션 변경되었는지 확인
}
>>> val list = listOf("a", "b", "c")
>>> printInUppercase(list)
[A, B, C]
A

컬렉션을 플랫폼 타입으로 다루기

플랫폼 타입의 경우 코틀린에서는 null 관련 정보가 없다. 따라서 컴파일러는 코틀린 코드가 그 타입을 null이 될 수 있는 타입이나 될 수 없는 타입 어느쪽으로든 사용할 수 있게 허용한다.

자바 쪽에서 선언한 컬렉션 타입의 변수도 플랫폼 타입으로 본다.
자바 컬렉션이 들어간 경우, 어떤 코틀린이 들어가야할 지 정해야한다.

  • 컬렉션이 null이 될 수 있는가?
  • 컬렉션의 원소가 null이 될 수 있는가?
  • 오버라이드하는 메서드가 컬렉션을 변경할 수 있는가?

객체의 배열과 원시 타입의 배열

fun main(args: Array<String>) {
for (i in args.indices) {
println("Argument $i is: ${args[i]}")
}
}

코틀린에서 배열을 만드는 방법은 다양하다.

  • arrayOf 함수에 원소를 넘긴다
  • arrayOfNulls 함수에 정수 값을 인자로 넘기면 모든 원소가 Null이고 인자로 넘긴 값을 크기로 갖는 배열을 만들 수 있다. 원소 타입이 null이 될 수 있는 타입인 경우만 이 함수를 쓸 수 있다.
  • Array 생성자는 배열 크기와 람다를 인자로 받아서 각 배열 원소를 초기화한다. arrayOf을 쓰지 않고 null이 아닌 배열을 만들어야 하는 경우 이 생성자를 사용한다.
profile
공댕이😎_블체

0개의 댓글