- 널이 될 수 있는 타입과 널을 처리하는 구문의 문법
- 코틀린 원시 타입 소개와 자바 타입과 코틀린 원시 타입의 관계
- 코틀린 컬렉션 소개와 자바 컬렉션과 코틀린 컬렉션의 관계
int strLen(String s) {
return s.lenth();
}
fun strLen(s: String) = s.length
strLen(null) // Error: Null can not be a value of a non-null type String
fun strLenSafe(s: String?) = ...
fun strLenSafe(s: String?) = s.length()
// 결과: Error: only safe(?.) or non-null asserted (!!.) calls are allowed on ..
val x: String? = null
var y: String = x
// 결과: Error: Type mismatch: inferred type is String? but String was expected
strLen(x)
// 결과: Error: Type mismatch: inferred type is String? but String was expected
이렇게 제약이 많으면 널이 될 수 있는 타입의 값으로 대체 뭘 할 수 있을까?
가장 중요한 일은 바로 null과 비교하는 것이다. 일단 null과 비교하고 나면 컴파일러는 그 사실을 기억하고 null이 아님이 확실한 영역에서는 해당 값을 널이 될 수 없는 타입의 값처럼 사용할 수 있다.
// if 검사를 통해 null 값 다루기
fun strLenSafe(s: String?): Int =
if (s != null) s.length else 0 // null 검사를 추가하면 코드가 컴파일된다.
val x: String? = null
println(strLenSafe(x)) // 0
println(strLenSafe("abc")) // 3
실행 시점에 널이 될 수 있는 타입이나 널이 될 수 없는 타입의 객체는 같다. 널이 될 수 있는 타입은 널이 될 수 없는 타입을 감싼 래퍼 타입이 아니다. 모든 검사는 컴파일 시점에 수행된다. 따라서 코틀린에서는 널이 될 수 있는 타입을 처리하는 데 별도의 실행 시점 부가 비용이 들지 않는다.
s?.toUpperCase()
와 if (s != null) s.toUpperCase() else null
는 같다.fun printAllCaps(s: String?) {
val allCaps: String? = s?.toUpperCase() // allCaps는 널일 수도 있다.
println(allCaps)
}
printAllCaps("abc") // ABC
printAllCaps(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
// 안전한 호출 연쇄시키기
class Address(val streetAddress: String, val zipCode: Int,
val city: String, val country: String)
class Company(val name: String, val address: Address?)
class Person(val name: String, val company: Company?)
fun Person.countryName(): String {
// 여러 안전한 호출 연산자를 연쇄해 사용한다.
val country = this.company?.address?.country
return if (country != null) country else "Unknown"
}
val person = Person("Dmitry", null)
println(person.countryName()) // Unknown
fun foo(s: String?) {
val t: String = s ?: ""
}
// 엘비스 연산자를 활용해 널 값 다루기
fun strLenSafe(s: String?): Int = s?.length ?: 0
print(strLenSafe("abc")) // 3
println(strLenSafe(null)) // 0
fun Person.countryName() = company?.address?.country ?: "Unknown"
// throw와 엘비스 연산자 함께 사용하기
class Address(val streetAddress: String, val zipCode: Int,
val city: String, val country: String)
class Company(val name: String, val address: Address?)
class Person(val name: String, val company: Company?)
fun printShippingLabel(person: Person) {
val address = person.company?.address
?: throw IllegalArgumentException("No address") // 주소가 없으면 예외를 발생시킨다.
with(address) {
println(streetAddress)
println("$zipCode $city, $country")
}
}
val address = Address("Elsestr. 47", 80687, "Munich", "Germany")
val jetbrains = Company("JetBrains", address)
val person = Person("Dmitry", jetbrains)
printShippingLabel(person)
// Elsestr. 47
// 80687 Munich, Germany
printShippingLabel(Person("Alexey", null))
// java.lang.IllegalArgumentException: No address
// 안전한 연산자를 사용해 equals 구현하기
class Person(val firstName: String, val lastName: String) {
override fun equals(o: Any?): Boolean {
// 타입이 서로 일치하지 않으면 false를 반환한다.
val otherPerson = o as? Person ?: return false
// 안전한 캐스트를 하고나면 otherPerson이 Person 타입으로 스마트 캐스트된다.
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
fun ignoreNulls(s: String?) {
val sNotNull: String = s!! // 예외는 이 지점을 가리킨다.
println(sNotNull.length)
}
ignoreNulls(null)
// Expceion in thread "main" kotlin.KotlinNullPointerException ...
// 스윙 액션에서 널 아님 단언 사용하기
class CopyRowAction(val list: JList<String>) : AbstractAction() {
override fun isEnabled(): Boolean =
list.selectedValue != null
// actionPerformed는 isEnabled가 "true"인 경우에만 호출된다.
override fun actionPerformed(e: ActionEvent) {
val value = list.selectedValue!!
}
// value를 클립보드로 복사
}
val value = list.selectedValue ?: return
처럼 널이 될 수 없는 타입의 값을 얻어야 한다.!!를 널에 대해 사용해서 발생하는 예외의 스택 트레이스(stack trace)에는 어떤 파일의 몇 번째 줄인지에 대한 정보는 들어있지만 어떤 식에서 예외가 발생햇는지에 대한 정보는 들어있지 않다. 어떤 값이 널이었는지 확실히 하기 위해 여러 !! 단언문을 한 줄에 함께 쓰는 일을 피하라.
✅ 스택 트레이스 : 프로그램이 시작된 시점부터 현재 위치까지의 메서드 호출 목록으로, 예외가 발생할 경우 JVM이 어디서 예외가 발생했는지 알려주는 역할을 한다.
fun sendEmailTo(email: String) { /*...*/ }
val email: string? = ...
sendEmailTo(email)
// 결과: Error: Type mismatch: inferred type is String? but String was expected
// 인자를 넘기기 전에 주어진 값이 널인지 검사해야 한다.
if (email != null) sendEmailTo(email)
// let을 사용해 null이 아닌 인자로 함수 호출하기
fun sendEmailTo(email: String) {
println("Sending email to $email")
}
var email: String? = "yole@example.com"
email?.let { sendEmailTo(it) }
// Sending email to yole@example.com
email = null
email?.let { sendEmailTo(it) } // 아무 일도 일어나지 않는다.
fun getTheBestPersonInTheWorld(): Person? = null
val person: Person? = getTheBestPersonInTheWorld()
if (person != null) sendEmailTo(person.email)
getTheBestPersonInTheWorld()?.let { sendEmailTo(it.email) }
class MyClass(val nonNullableProperty: String) {
// 특별한 메소드에서 초기화할 수 없음
fun initializeProperty(value: String) {
// 컴파일 오류: Val cannot be reassigned
nonNullableProperty = value
}
}
fun main() {
val instance = MyClass("initialValue")
instance.initializeProperty("newValue")
}
class MyClass(var nullableProperty: String?) {
fun initializeProperty(value: String) {
nullableProperty = value
}
}
fun main() {
val instance = MyClass(null)
println(instance.nullableProperty) // null
instance.initializeProperty("newValue")
println(instance.nullableProperty) // newValue
}
// 널 아님 단언을 사용해 널이 될 수 있는 프로퍼티 접근하기
class MyService {
fun performAction(): String = "foo"
}
class MyTest {
// null로 초기화하기 위해 널이 될 수 있는 타입인 프로퍼티를 선언한다.
private var myService: MyService? = null
// setUp 메소드 안에서 진짜 초기값을 지정한다.
@Before fun setUp() {
myService = myService()
}
@Test fun testAction() {
// 반드시 널 가능성에 신경 써야 한다. !!나 ?을 꼭 써야 한다.
Assert.assertEquals("foo", myService!!.performAction())
}
}
// 나중에 초기화하는 프로퍼티 사용하기
class myService {
fun performAction(): String = "foo"
}
class MyTest {
// 초기화하지 않고 널이 될 수 없는 프로퍼티를 선언한다.
private lateinit var myService: MyService
@Before fun setUp() {
myService = MyService()
}
// 널 검사를 수행하지 않고 프로퍼티를 사용한다.
@Test fun testAction() {
Assert.assertEquals("foo", myService.performAction())
}
}
class Example {
// 필드 선언시 초기값 설정
val name: String = "DefaultName"
}
// val 프로퍼티를 선언과 동시에 초기화하면 해당 초기화 구문이 생성자의 일부로 취급되어,
// 컴파일러는 이를 내부적으로 생성된 생성자 코드로 변환
class Example {
val name: String
constructor() {
this.name = "DefaultName"
}
}
널이 될 수 있는 타입에 대한 확장 함수를 정의하면 null 값을 다루는 강력한 도구로 활용할 수 있다.
일반 멤버 호출은 객체 인스턴스를 통해 디스패치되므로 그 인스턴스가 널인지 여부를 검사하지 않는다.
이는 Kotlin에서 스마트 캐스트(Smart Cast)가 활용되기 때문입니다.
// 널이 될 수 있는 Int에 대한 확장 함수 정의
fun Int?.safeSquare(): Int {
return this?.let { it * it } ?: 0
}
fun main() {
val nullableNumber: Int? = null
// 널이 될 수 있는 Int에 대한 확장 함수 호출
val result = nullableNumber.safeSquare()
println("Result: $result") // Result: 0
}
✅ 동적 디스패치 : 객체지향 언어에서 객체의 동적 타입에 따라 적절한 메소드를 호출해주는 방식
✅ 정적 디스패치 : 반대로 컴파일러가 컴파일 시점에 어떤 메소드가 호출될지 결정해서 코드를 생성하는 방식
일반적으로 동적 디스패치를 처리할 때는 객체별로 자신의 메소드에 대한 테이블을 저장하는 방법을 가장 많이 사용한다. 물론 대부분의 객체지향 언어에서 같은 클래스에 속한 객체는 같은 메소드 테이블을 공유하므로 보통 메소드 테이블은 클래스마다 하나씩만 만들고 각 객체는 자신의 클래스에 대한 참조를 통해 그 메소드 테이블을 찾아보는 경우가 많다.
// null이 될 수 있는 수신 객체에 대해 확장 함수 호출하기
fun verifyUserInput(input: String?) {
if (input.isNullOrBlank()) {
println("Please fill in the required fields")
}
}
verifyUserInput(" ") // Please fill in the required fields
// isNullOrBlank에 "null"을 수신 객체로 전달해도 아무런 예외가 발생하지 않는다.
verifyUserInput(null) // Please fill in the required fields
// 널이 될 수 있는 String의 확장
fun String?.isNullOrBlank(): Boolean =
// 두 번째 "this"에는 스마트 캐스트가 적용된다.
this == null || this.isBlank()
자바에서는 메서드 안의 this는 그 메소드가 호출된 수신 객체를 가리키므로 항상 널이 아니다.
코틀린에서는 널이 될 수 있는 타입의 확장 함수 안에서는 this가 널이 될 수 있다는 점이 자바와 다르다.
val person: Person? = ...
// 안전한 호출을 하지 않음. 따라서 "it"은 널이 될 수 있는 타입으로 취급됨
person.let { sendEmailTo(it) }
// 결과: Error: Type mismatch: inferred type is Person? but Person was expected
person?.let { sendEmailTo(it) } // 개선
직접 확장 함수를 작성한다면 그 확장 함수를 널이 될 수 있는 타입에 대해 정의할지 여부를 고민할 필요가 있다. 처음에는 널이 될 수 없는 타입에 대한 확장 함수를 정의하라. 나중에 대부분 널이 될 수 있는 타입에 대해 그 함수를 호출했다는 사실을 깨닫게 되면 확장 함수 안에서 널을 제대로 처리하게 하면(그 확장 함수를 사용하는 코드가 깨지지 않으므로) 안전하게 그 확장 함수를 널이 될 수 있는 타입에 대한 확장 함수로 바꿀 수 있다.
// 널이 될 수 있는 타입 파라미터 다루기
fun <T> printHashCode(t: T) {
// "t"가 null이 될 수 있으므로 안전한 호출을 써야만 한다.
println(t?.hashCode())
}
// "T"의 타입은 "Any?"로 추론된다.
printHashCode(null) // null
// 타입 파라미터에 대해 널이 될 수 없는 상한을 사용하기
fun <T: Any> printHashCode(t: T) { // 이제 "T"는 널이 될 수 없는 타입이다.
println(t.hashCode())
}
printHashCode(null) // 널이 될 수 없는 타입의 파라미터에 널을 넘길 수 없다.
// Error: Type parameter bound for `T` is not satisfied
printHashCode(42) // 42
// 널 가능성 애노테이션이 없는 자바 클래스
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
// 널 검사를 통해 자바 클래스 접근하기
fun yellAtSafe(person: Person) {
println((person.name ?: "Anyone").toUpperCase() + "!!!")
}
yellAtSafe(Person(null)) // ANYONE!!!
val i: Int = person.name
// 결과: Error: Type mismatch: inferred type is String! but Int was expected
// 자바 프로퍼티를 널이 될 수 있는 타입으로 볼 수 있다.
val s: String? = person.name
// 자바 프로퍼티를 널이 될 수 없는 타입으로 볼 수 있다.
val s1: String = person.name
// String 파라미터가 있는 자바 인터페이스
interface StringProcessor {
void process(String value);
}
// 자바 인터페이스를 여러 다른 널 가능성으로 구현하기
class StringPrinter: StringProcessor {
override fun process(value: String) {
print(value)
}
}
class NullableStringPrinter : StringProcessor {
override fun process(value: String?) {
if (value != null) {
print(value)
}
}
}
val i: Int = 1
val list: List<Int> = listOf(1, 2, 3)
fun showProgress(progress: Int) {
val percent = progress.coerceIn(0, 100)
println("We're ${percent}% done!")
}
showProgress(146) // We're 100% done!
// 널이 될 수 있는 원시 타입
data class Person(val name: String, val age: Int? = null) {
fun isOlderThan(other: Person): Boolean? {
if (age == null || other.age == null)
return null
return age > other.age
}
}
println(Person("Sam", 35).isOlderThan(Person("Amy", 42))) // false
println(Person("Sam", 35).isOlderThan(Person("Jane"))) // null
val i = 1
val l: Long = i // Error: type mismatch
// 직접 변환 메소드를 호출
val i = 1
val l: Long = i.toLong()
val x = 1
println(x in listOf(1L, 2L, 3L)) // false
println(x.toLong() in listOf(1L, 2L, 3L)) // true
fun foo(l: Long) = println(l)
val b: Byte = 1 // 상수 값은 적절한 타입으로 해석된다.
val l = b + 1L // +는 Byte와 Long을 인자로 받을 수 있다.
foo(42) // 컴파일러는 42를 Long 값으로 해석한다.
val answer: Any = 42 // Any가 참조 타입이기 때문에 42가 박싱된다.
fun f(): Unit { ... }
fun f() { ... }
interface Processor<T> {
fun progress(): T
fun progress2(value: T): T
}
class NoResultProcess : Processor<Unit> {
override fun process() { // Unit을 반환하지만 타입을 지정할 필요가 없다.
// 업무 처리 코드
// 여기서 return을 명시할 필요가 없다.
}
}
// 제네릭 함수를 오버라이드하는 경우
class resultProcess : Processor<String> {
override fun progress2(value: String): String {
// 업무 처리 코드
// 여기서 return을 명시할 필요가 있다.
}
}
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
fun main() {
try {
val result: String = fail("Error message")
// 이 부분은 실행되지 않음. throwError에서 예외가 던져짐.
println(result)
} catch (e: IllegalArgumentException) {
println("Caught an exception: ${e.message}")
// 결과: java.lang.IllegalStateException: Error message
}
}
val address = company.address ?: fail("No address")
println(address.city)
fun readNumbers(reader: BufferedReader): List<Int?> {
// 널이 될 수 있는 Int 값으로 이뤄진 리스트를 만든다.
val result = ArrayList<Int?>()
for (line in reader.lineSequence()) {
try {
val number = line.toInt()
result.add(number)
}
catch (e: NumberFormatException) {
result.add(null)
}
}
return result
}
fun addValidNumbers(numbers: List<Int?>) {
var sumOfValidNumbers = 0
var invalidNumbers = 0
// 리스트에서 널이 될 수 있는 값을 읽는다.
for (number in numbers) {
// 널에 대한 값을 확인한다.
if (number != null) {
sumOfValidNumbers += number
} else {
invalidNumbers++
}
}
println("Sum of valid numbers: $sumOfValidNumbers")
println("Invalid numbers: $invalidNumbers")
}
val reader = BufferedReader(StringBuilder("1\nabc\n42"))
val numbers = readNumbers(reader)
addValidNumbers(numbers)
//// 결과
// Sum of valid numbers: 43
// Invalid numbers: 1
fun addValidNumbers(numbers: List<Int?>) {
val validNumbers = numbers.filterNotNull()
println("Sum of valid numbers: ${validNumbers.sum()}")
println("Invalid numbers: ${numbers.size - validNumbers.size}")
}
코드에서 가능하면 항상 읽기 전용 인터페이스를 사용하는 것을 일반적인 규칙으로 삼아라. 코드가 컬렉션을 변경할 필요가 있을 때만 변경 가능한 버전을 사용하라.
fun <T> copyElements(source: Collection<T>,
target: MutableCollection<T>) {
for (item in source) {
target.add(item)
}
}
val source: Collections<Int> = arrayListOf(3, 5, 7)
val target: MutableCollection<Int> = arrayListOf(1)
copyElements(source, target)
println(target) // [1, 3, 5, 7]
val source: Collections<Int> = arrayListOf(3, 5, 7)
val target: Collections<Int> = arrayListOf(1)
copyElements(source, target)
// 결과: Error: Type mismatch: inferred type is Collection<Int> but MutableCollection<Int> was expected
컬렉셔 타입 | 읽기 전용 타입 | 변경 가능 타입 |
---|---|---|
List | listOf | mutableListOf, arrayListOf |
Set | setOf | mutableSetOf, hashSetOf, linkedSetOf, sortedSetOf |
Map | mapOf | mutableMapOf, hashMapOf, linkedMapOf, sortedMapOf |
fun uppercaseAll(items: MutableList<String>): List<String> {
for (i in 0 until items.size) {
items[i] = items[i].toUpperCase()
}
return items
}
fun printInUppercase(list: MutableList<String>) {
println(uppercaseAll(list)) // [A, B, C]
println(list.first()) // A
}
fun main() {
val list = mutableListOf("a", "b", "c")
printInUppercase(list)
}
fun main(args: Array<String>) {
// 배열의 인덱스 값의 범위에 대해 이터레이션하기 위해
// array.indices 확장 함수를 사용한다.
for (i in args.indices) {
// array[index]로 인덱스를 사용해 배열 원소에 접근한다.
println("Argument $i is: ${args[i]}")
}
}
fun main() {
// 문자열 배열을 생성하고 초기화
val stringArray = arrayOf("apple", "banana", "orange")
println(stringArray.joinToString()) // apple, banana, orange
// 정수 배열을 생성하고 초기화
val intArray = arrayOf(1, 2, 3, 4, 5)
println(intArray.joinToString()) // 1, 2, 3, 4, 5
// 혼합 타입 배열을 생성하고 초기화
val mixedArray = arrayOf("apple", 2, true)
println(mixedArray.joinToString()) // apple, 2, true
}
// 크기가 3이고 모든 원소가 null인 배열을 생성
val nullableArray = arrayOfNulls<String>(3)
println(nullableArray.joinToString()) // null, null, null
// 크기가 5이고 모든 원소가 null인 배열을 생성
val anotherNullableArray = arrayOfNulls<Int>(5)
println(anotherNullableArray.joinToString()) // null, null, null, null, null
}
// 알파벳으로 이뤄진 배열 만들기
// val letters = Array<String>(26) { i -> ('a' + i).toString() }
val letters = Array(26) { ('a' + it).toString() }
println(letters.joinToString("")) // abcdefghijklmnopqrstuvwxyz
// 컬렉션을 vararg 메소드에게 넘기기
val strings = listOf("a", "b", "c")
// vararg 인자를 넘기기 위해 스프레드 연산자(*)를 써야 한다.
println("%s/%s/%s".format(*strings.toTypedArray())) // a/b/c
fun main() {
// Int 배열
val intArray = IntArray(5)
println(intArray.joinToString()) // 0, 0, 0, 0, 0
// Double 배열
val doubleArray = DoubleArray(3)
println(doubleArray.joinToString()) // 0.0, 0.0, 0.0
// Char 배열
val charArray = CharArray(4)
println(charArray.joinToString()) // \u0000, \u0000, \u0000, \u0000
// Boolean 배열
val booleanArray = BooleanArray(2)
println(booleanArray.joinToString()) // false, false
}
fun main() {
// Int 배열을 생성하고 초기화
val intArray = intArrayOf(1, 2, 3, 4, 5)
println(intArray.joinToString()) // 1, 2, 3, 4, 5
// Double 배열을 생성하고 초기화
val doubleArray = doubleArrayOf(1.5, 2.0, 3.5)
println(doubleArray.joinToString()) // 1.5, 2.0, 3.5
// Char 배열을 생성하고 초기화
val charArray = charArrayOf('a', 'b', 'c')
println(charArray.joinToString()) // a, b, c
// Boolean 배열을 생성하고 초기화
val booleanArray = booleanArrayOf(true, false, true)
println(booleanArray.joinToString()) // true, false, true
}
fun main() {
val squares = IntArray(5) { i -> (i + 1) * (i + 1) }
println(squares.joinToString()) // 1, 4, 9, 16, 25
}
fun main() {
// 박싱된 값이 들어있는 리스트
val boxedList: List<Int?> = listOf(1, 2, null, 4, 5)
// toIntArray를 사용하여 박싱하지 않은 값이 들어있는 배열로 변환
val intArray: IntArray = boxedList.filterNotNull().toIntArray()
for (value in intArray) {
println(value)
}
}
fun main(args: Array<String>) {
args.forEachIndexed { index, element ->
println("Argument $index is: $element")
}
}