예제로 공략하는 코틀린 76제

코틀린 Kotlin 프로그래밍 언어
젯브레인스(JetBrains)에서 만든 프로그래밍 언어 코틀린(Kotlin)은, 자바(Java)에서 더 발전한 현대적인 프로그래밍 언어입니다. 자바와 매우 비슷해 변화가 크지 않으면서도, 최신 프로그래밍 언어에 있는 세련된 기능들이 잘 녹아들어 있어 훨씬 깔끔하고 편하게 개발할 수 있습니다.
이미 그 가치를 널리 인정받아서, 안드로이드 앱을 개발하는 표준 언어로 자리 잡았습니다. 안드로이드 개발뿐만 아니라, 백엔드 개발에도 자바의 사실상 표준 프레임워크라고 볼 수 있는 스프링에서도 코틀린을 표준으로 지원하고 있을 만큼 환영받고 있습니다. 아마도 조만간, 자바의 현재 위치를 대체할 수 있을 거라고 보는 사람이 많을 겁니다.
자료 소개
이 웹사이트는, 코틀린 공식 홈페이지에 있는 Kotlin by example이라는 문서를 편역해 구성했습니다. 원문을 성실히 번역하겠다는 욕심은 버리고, 구성과 예제를 거의 그대로 가져오되, 한국어로 표현하기 좋은 문장으로 바꾸었고, 편역자 편의에 따라 예제도 수정하였습니다. 원문도 자유로운 라이선스 정책을 선택했기에, 이 편역본도 자유로운 라이선스로 공개하겠습니다.
제안하실 내용이 있으면, PR을 보내주시면 감사하겠습니다.
예제 코드 깃허브
이 웹사이트에 있는 소스코드 76개는, 웹사이트에서 곧바로 실행이 가능합니다. 하지만, 더 자세한 실험을 원하시는 분들은, 아래 깃허브에서도 받아서 실습하실 수 있습니다. IntelliJ등 로컬 개발환경에서 실습해보실 분들은, 아래 깃허브 주소를 참고해주세요.
대상 독자
자바 개발을 하다가 코틀린으로 이전해야 하는 백엔드 개발자나, 안드로이드 앱 개발을 위해서 새로이 코틀린을 배우는 분들을 포함해서, 코틀린의 다양한 문법을 빠르게 알아보고 싶은 분들을 대상으로 작성했습니다. 예제 코드 중심으로 다양한 문법을 소개합니다. 글로는 다소 어렵게 느껴지는 내용도, 예제 코드를 통해 쉽게 이해할 수 있습니다. 코드를 따라 읽으며 코틀린의 편리한 문법들을 한눈에 파악하실 수 있습니다. 긴 글을 읽기보다 예제 코드를 중심으로 학습하시려는 분들에게 적합합니다.
예제 중심 실습 환경
이 웹사이트의 예제 코드는 웹브라우저 환경에서 곧바로 실습해 볼 수 있습니다. 별도의 개발 환경을 설정하지 않고도, 예제 코드가 보이는 웹 페이지에서 곧바로 실행해 결과를 확인해 볼 수 있어서, 궁금한 내용으로 편집해 가며 실습하실 수 있습니다.
저자 소개
김대현은, 카카오(Daum), NHN, LINE+에서 개발자, 개발팀장으로 일한 경험이 있습니다. 다양한 프로그래밍 언어로 실무 개발에 임했고, 그중 코틀린도 백엔드 개발에 유용하게 활용한 경력이 있습니다. 자바로 할 수 있던 일들은 대부분 코틀린이 훨씬 더 편하게, 그리고 부담 없이 대체할 수 있다고 믿고 있습니다.
온라인 강의 (유료)
이 책은, 온라인 강의로도 준비했습니다. 강의는 유료로 판매합니다만, 이 온라인 학습자료는 계속 무료로 제공하며, 자유롭게 활용하실 수 있습니다. 온라인 강의로 편하게 학습하실 분께서는 아래 주소에서 강의를 구매하실 수 있습니다.
그럼, 아래에 다음 버튼을 눌러 학습을 시작해 보세요!
연락처: hatemogi at gmail.com
헬로 월드
프로그래밍 언어 학습에 "Hello, World!"가 빠질 수 없죠. 아래 소스코드 우상단에 있는 초록색 실행 버튼을 눌러봐 주세요. 코틀린 소스코드를 실행한 결과가 곧바로 브라우저 화면에 보입니다. 아래 "Hello, World!" 문자열을 "헬로 월드!"로 수정해서 실행해 봅시다.
package org.kotlinlang.play // 1
fun main() { // 2
println("Hello, World!") // 3
}
소스 코드에 주석(코멘트)으로 1, 2, 3 숫자가 붙어있는 부분에 대해, 이하 설명이 이어집니다.
- 코틀린 코드는 패키지 안에 정의합니다. 하지만 패키지 선언을 꼭 해야 하는 건 아닙니다. 소스 파일에 패키지를 지정하지 않으면, 디폴트 패키지에 속하게 됩니다.
- 코틀린 애플리케이션의 시작점은
main함수입니다. 함수의 반환 타입이 지정하지 않았기 때문에, 아무것도 반환하지 않습니다. println는 표준 출력에 한 줄을 프린트합니다. 세미콜론(;)이 없어도 되는 점도 살펴봐 주세요.
만약, 애플리케이션 실행 파라미터를 입력받으려면, 아래 코드처럼 main 함수가 Array<String> 타입의 파라미터를 받도록 합니다.
fun main(args: Array<String>) {
println("Hello, World!")
}
실습 환경 설명
코드를 더 본격적으로 편집해보고 싶으면 소스코드 왼쪽 아래에 있는 "Open in Playground" 링크를 눌러 코틀린 플레이그라운드 사이트로 이동해서 실습하실 수 있습니다.
함수
기본값 파라미터와 이름지정 파라미터
fun printMessage(message: String): Unit { // 1
println(message)
}
fun printMessageWithPrefix(message: String, prefix: String = "Info") { // 2
println("[$prefix] $message")
}
fun sum(x: Int, y: Int): Int { // 3
return x + y
}
fun multiply(x: Int, y: Int) = x * y // 4
fun main() {
printMessage("Hello") // 5
printMessageWithPrefix("Hello", "Log") // 6
printMessageWithPrefix("Hello") // 7
printMessageWithPrefix(prefix = "Log", message = "Hello") // 8
println(sum(1, 2)) // 9
println(multiply(2, 4)) // 10
}
- 이 함수는
String타입의 파라미터를 하나 받아서Unit을 반환했습니다.Unit은 반환할 값이 없다는 의미입니다. - 이 함수의 두 번째 파라미터는, 기본값 파라미터로
Info를 받습니다. 함수 반환 타입을 생략했는데, 이러면Unit을 반환한다는 의미입니다. - 이 함수는 정수
Int를 반환합니다. - 이 한 줄짜리 함수도 정수를 반환합니다. 반환 타입을 생략했지만, 타입추론기능 덕분에 정수 타입을 반환한다고 인식합니다.
printMessage함수를Hello라는 파라미터를 주고 호출했습니다.printMessageWithPrefix함수를 두 파라미터 모두 전달해서 호출했습니다.- 두 번째 파라미터를 생략해서 호출하면, 기본값인
Info가 전달됩니다. - 같은 함수를 이름지정 파라미터 기능을 써서 호출했고, 심지어 파라미터 순서를 바꿨습니다.
sum함수 결괏값을 프린트 합니다.multiply함수 결괏값을 프린트합니다.
중위 함수 Infix Functions
파라미터를 하나만 받는 멤버 함수나 확장(extension) 함수는, 해당 함수를 "중위 함수"형태로 쓸 수 있습니다.
fun main() {
infix fun Int.times(str: String) = str.repeat(this) // 1
println(2 times "Bye ") // 2
val pair = "자바" to "오라클" // 3
println(pair)
infix fun String.onto(other: String) = Pair(this, other) // 4
val myPair = "코틀린" onto "젯브레인스"
println(myPair)
val koo = Person("손석구")
val yeon = Person("장도연")
koo likes yeon // 5
}
class Person(val name: String) {
val likedPeople = mutableListOf<Person>()
infix fun likes(other: Person) { likedPeople.add(other) } // 6
}
Int에 중위 확장 함수를 정의했습니다.- 중위 함수를 호출했습니다.
- 표준 라이브러리에 있는 중위 함수
to를 호출해서Pair를 만들었습니다. - 이렇게
to랑 같은 일을 하는onto를 직접 구현할 수도 있습니다. - 중위 표현법은 멤버 함수나 메서드에도 쓸 수 있습니다.
- 선언된 클래스 자체가 첫 번째 파라미터가 됩니다.
이 예제에서 지역 함수(어떤 함수 안에 있는 함수)를 사용한 점도 살펴봐주세요.
연산자 함수 Operator Functions
특정 함수들은 연산자로 "업그레이드"될 수 있고, 해당 연산자를 써서 그 함수를 호출할 수 있습니다.
fun main() {
//sampleStart
operator fun Int.times(str: String) = str.repeat(this) // 1
println(2 * "하이 ") // 2
operator fun String.get(range: IntRange) = substring(range) // 3
val str = "웃어라! 온 세상이 너와 함께 웃을 것이다."
println(str[0..3]) // 4
//sampleEnd
}
times라는 확장 중위 함수에operator제어자(modifier)를 붙였습니다.times()에 대응하는 연산자 기호는*이기 때문에,2 * "Bye"의 형태로 해당 함수를 호출할 수 있게 됩니다.- 이 연산자 함수로 문자열의 특정 범위에 쉽게 접근할 수 있게 만들었습니다.
get()연산자를 써서 인덱스 접근 문법을 활용합니다.
vararg 파라미터
varargs 파라미터를 쓰면 쉼표로 연달아 쓴 파라미터들을 전달합니다. 파라미터를 몇 개 받는지 미리 정해두지 않고도, 여러 개를 전달할 수 있게 됩니다.
fun main() {
//sampleStart
fun printAll(vararg messages: String) { // 1
for (m in messages) println(m)
}
printAll("Hello", "Hallo", "Salut", "Hola", "안녕") // 2
fun printAllWithPrefix(vararg messages: String, prefix: String) { // 3
for (m in messages) println(prefix + m)
}
printAllWithPrefix(
"Hello", "Hallo", "Salut", "Hola", "안녕",
prefix = "Greeting: " // 4
)
fun log(vararg entries: String) {
printAll(*entries) // 5
}
log("Hello", "Hallo", "Salut", "Hola", "안녕")
//sampleEnd
}
vararg제어자를 붙여 씁니다.- 이렇게 하면
printAll에 임의 갯수의 문자열 파라미터를 전할 수 있습니다. - 이름지정(named) 파라미터로, vararg에 이어서 같은 타입의 또다른 파라미터를 추가로 전달할 수도 있습니다.
- 이름지정(named) 파라미터를 써서,
prefix값을 vararg에 이어 추가로 전달했습니다. - 실행시점에 vararg는 평범한 배열값입니다. 배열을 vararg 파라미터로 전달하려면,
*연산자를 써서*entries처럼 전달할 수 있습니다.*연산자 없이entries로 쓰면Array<String>으로 인식됩니다.
변수
코틀린에는 강력한 타입추론(type inference) 기능이 있습니다. 변수의 타입을 명시해도 되지만, 생략하고 컴파일러에게 추론하도록 해도 됩니다. 코틀린은 불변(immutability) 변수 사용을 권장합니다. 기본적으로 var 대신 val을 쓰면 좋습니다. 불변 변수는 한 번 선언하고 나면 값을 바꿀 수 없습니다.
fun main() {
//sampleStart
var a: String = "처음값" // 1
println(a)
val b: Int = 1 // 2
val c = 3 // 3
//sampleEnd
}
- 변이(mutable) 변수를 선언하면서 초기화했습니다.
- 불변(immutable) 변수를 선언하고 초기화했습니다.
- 불변 변수를 선언하고 초기화했고, 타입 선언은 생략했습니다. 컴파일러가 3이라는 값을 보고
Int타입이라고 추론합니다.
fun main() {
//sampleStart
var e: Int // 1
println(e) // 2
//sampleEnd
}
- 초기화하지 않고 변수를 선언했습니다.
- 초기화하지 않은 변수를 읽으려 하면 컴파일러가
Variable 'e' must be initialized라고 에러를 냅니다.
언제 변수를 초기화할지는 자유롭게 정할 수 있습니다만, 처음 읽기 전에는 반드시 초기화해야 합니다.
fun someCondition() = true
fun main() {
//sampleStart
val d: Int // 1
if (someCondition()) {
d = 1 // 2
} else {
d = 2 // 2
}
println(d) // 3
//sampleEnd
}
- 변수를 선언했으나, 초기화하지 않았습니다.
- 조건에 따라서 다른 값으로 초기화했습니다.
- 조건의 참과 거짓 모든 경우에 변수를 초기화했기 때문에 문제없이 값을 읽을 수 있습니다.
Null 안전성
NullPointerException 예외가 발생하는 상황을 대폭 줄이기 위해, 기본 코틀린의 변수에는 null을 지정할 수 없게 되어있습니다. 만약 변수가 null이 될 수도 있게 하려면, 타입의 끝에 ?표시를 붙여 두어야만 합니다.
fun main() {
//sampleStart
var neverNull: String = "이 변수는 null이 될 수 없습니다" // 1
neverNull = null // 2
var nullable: String? = "이 변수는 null이 될 수 있습니다" // 3
nullable = null // 4
var inferredNonNull = "컴파일러 타입추론은 null-불가로 봅니다" // 5
inferredNonNull = null // 6
fun strLength(notNull: String): Int { // 7
return notNull.length
}
strLength(neverNull) // 8
strLength(nullable) // 9
//sampleEnd
}
- null-불가 문자열 변수를 선언했습니다.
- null-불가 변수에
null을 대입하려고 하면, 컴파일 에러가 납니다. - null-가능 문자열 변수를 선언했습니다.
- null-가능 변수에
null을 지정했습니다. 이건 문제없습니다. - 컴파일러가 변수 타입을 추론할 때는 null-불가 타입이라고 가정합니다.
- 컴파일러가 추론한 타입의 변수에
null을 대입하려면, 컴파일 에러가 납니다. - null-불가 문자열 파라미터를 받는 함수를 선언했습니다.
- null이 될 수 없는
String파라미터를 전달해 함수를 호출했습니다. 잘 됩니다. - 같은 함수를
String?타입의 파라미터로 호출하려고 하면, 컴파일 에러가 납니다.
Null을 다루는 방법
물론, 코틀린 프로그램에서 null을 써야 할 경우도 있습니다. 자바 코드와 소통해야 할 때나, 값이 없는 경우를 표현하려고 할 때 null을 쓰기도 합니다. 코틀린에는 그럴 때 우아하게 대처할 수 있는 기능이 있습니다.
//sampleStart
fun describeString(maybeString: String?): String { // 1
if (maybeString != null && maybeString.length > 0) { // 2
return "문자열 길이: ${maybeString.length}"
} else {
return "빈 문자열이거나 null입니다" // 3
}
}
//sampleEnd
fun main() {
println(describeString(null))
}
- null-가능 문자열 파라미터를 받아서, 길이를 설명하는 문자열을 반환하는 함수입니다.
- 주어진 문자열이
null이 아니고 빈 문자열도 아니라면, 그 문자열의 길이를 포함한 설명을 반환합니다. - 그 외에는, 주어진 문자열이 비었거나 null이라고 알려줍니다.
클래스
클래스 선언은, 클래스 이름, 클래스 헤더(타입 파라미터, 주 생성자)에 이어 중괄호쌍({})으로 감싼 본문으로 구성합니다. 클래스에 본문이 없는 경우는 중괄호를 생략해도 됩니다.
class Customer // 1
class Contact(val id: Int, var email: String) // 2
fun main() {
val customer = Customer() // 3
val contact = Contact(1, "mary@gmail.com") // 4
println(contact.id) // 5
contact.email = "jane@gmail.com" // 6
}
- 속성이나 추가 생성자 없이
Customer라는 이름으로 클래스를 선언했습니다. (본문이 없어서 중괄호도 생략했습니다) - 불변(immutable) 속성
id와 변이(mutable) 속성email을 파라미터로 받는 생성자와 함께 클래스를 선언했습니다. Customer클래스의 기본 생성자로 인스턴스를 만들었습니다. 코틀린에서는new키워드를 따로 쓰지 않습니다.Contact클래스의 생성자에 두 파라미터를 전달해 인스턴스를 하나 만들었습니다.id속성을 읽었습니다.email속성값을 변경했습니다.
제네릭 Generics
제네릭은 현대의 프로그래밍 언어에 있어 표준처럼 자리 잡은 일반화 기법입니다. 제네릭 클래스와 함수는 공통 코드에서 불특정 타입을 두고, 그 외에 독립적인 공통 코드를 잘 감싸서 쓸 수 있게 해서 코드 재사용성을 높여줍니다. List<T>에 있는 코드들이 타입 T에 관계없이 잘 작동하는 것처럼요.
제너릭 클래스
코틀린에서 제네릭을 쓰는 첫 번째 방법은, 제네릭 클래스를 이용하는 것입니다.
//sampleStart
class MutableStack<E>(vararg items: E) { // 1
private val elements = items.toMutableList()
fun push(element: E) = elements.add(element) // 2
fun peek(): E = elements.last() // 3
fun pop(): E = elements.removeAt(elements.size - 1)
fun isEmpty() = elements.isEmpty()
fun size() = elements.size
override fun toString() = "MutableStack(${elements.joinToString()})"
}
//sampleEnd
fun main() {
val stack = MutableStack(0.62, 3.14, 2.7)
stack.push(9.87)
println(stack)
println("peek(): ${stack.peek()}")
println(stack)
for (i in 1..stack.size()) {
println("pop(): ${stack.pop()}")
println(stack)
}
}
MutableStack<E>라는 제네릭 클래스를 정의했고, 여기서E를 제네릭 타입 파라미터라고 부릅니다. 이를 사용할 때는MutableStack<Int>처럼E자리에Int처럼 구체적인 타입을 지정해서 씁니다.- 제네릭 클래스 안에서는,
E는 다른 보통의 타입들과 마찬가지로 쓸 수 있습니다. E를 반환 타입 자리에 쓸 수도 있습니다.
제네릭 함수
어떤 함수가 특정 타입에 독립적이라면, 제네릭 함수로 정의해도 됩니다. 예제로, 스택을 만드는 유틸리티 함수를 작성해 보겠습니다.
class MutableStack<E>(vararg items: E) { // 1
private val elements = items.toMutableList()
fun push(element: E) = elements.add(element) // 2
fun peek(): E = elements.last() // 3
fun pop(): E = elements.removeAt(elements.size - 1)
fun isEmpty() = elements.isEmpty()
fun size() = elements.size
override fun toString() = "MutableStack(${elements.joinToString()})"
}
//sampleStart
fun <E> mutableStackOf(vararg elements: E) = MutableStack(*elements)
fun main() {
val stack = mutableStackOf(0.62, 3.14, 2.7)
println(stack)
}
//sampleEnd
컴파일러가 mutableStackOf 함수에 전달한 제네릭 파라미터를 추론할 수 있기 때문에, mutableStackOf<Double>(...)처럼 길게 쓰지 않고도 잘 동작하였습니다.
상속 Inheritance
코틀린은 일반적인 객체 지향 언어에 있는 상속 기능을 모두 지원합니다.
open class Dog { // 1
open fun sayHello() { // 2
println("멍멍!")
}
}
class Yorkshire : Dog() { // 3
override fun sayHello() { // 4
println("왈왈!")
}
}
fun main() {
val dog: Dog = Yorkshire()
dog.sayHello()
}
- 코틀린에서 클래스는 기본적으로 최종(final) 클래스입니다. 상속을 허용하려면 반드시
open제어자를 붙여 표시해야 합니다. - 코틀린에서 메서드도 기본적으로 최종(final)입니다. 클래스와 마찬가지로 재정의(override)를 허용하려면
open키워드를 앞에 붙여야 합니다. - 클래스를 상속하려면
: SuperclassName()같이 클래스 이름 뒤에 상속하고자 하는 상위 클래스명을 명시합니다. 빈괄호(())는 상위 클래스의 기본 생성자를 사용하겠다는 뜻입니다. - 메서드나 속성을 재정의 하려면
override키워드를 꼭 붙여야 합니다.
파라미터를 받는 생성자가 있는 클래스를 상속하기
//sampleStart
open class Tiger(val origin: String) {
fun sayHello() {
println("${origin}의 타이거 왈: 어흥!")
}
}
class SiberianTiger : Tiger("시베리아") // 1
fun main() {
val tiger: Tiger = SiberianTiger()
tiger.sayHello()
}
//sampleEnd
- 하위 클래스를 만들 때 상위 클래스의 파라미터 있는 생성자를 쓰려면, 생성자에 전달할 파라미터를 하위 클래스 선언 시에 제공토록 합니다.
생성자 파라미터를 상위 클래스 생성자에도 전달하기
//sampleStart
open class Lion(val name: String, val origin: String) {
fun sayHello() {
println("$name, ${origin}에서 온 사자 왈: 으르렁!")
}
}
class Asiatic(name: String) : Lion(name = name, origin = "인도") // 1
fun main() {
val lion: Lion = Asiatic("심바") // 2
lion.sayHello()
}
//sampleEnd
Asiatic클래스 선언에 있는name이var도 아니고val도 아닙니다. 이는 생성자 파라미터인데, 상위 클래스Lion에name속성으로 전달했습니다.심바라는 이름으로Asiatic인스턴스를 만들었습니다.Lion생성자를 호출할 때심바와인도를 전달했습니다.
When 조건문
코틀린에서는, 흔히 쓰는 switch 구문 대신에 더 유연하고 명확한 when 문법을 씁니다. 따로 반환값이 남지 않는 명령문(statement) 방식이나, 결국 반환값으로 돌아오는 표현식(expression) 형태로 쓸 수 있습니다.
When 명령문
fun main() {
cases("Hello")
cases(1)
cases(0L)
cases(MyClass())
cases("hello")
}
fun cases(obj: Any) {
when (obj) { // 1
1 -> println("하나") // 2
"안녕" -> println("인사") // 3
is Long -> println("Long") // 4
!is String -> println("문자열 아님") // 5
else -> println("그 외") // 6
}
}
class MyClass
- 이건
when명령문(statement)입니다. 어떤 일을 하되, 반환값은 없습니다. obj가1이랑 같은지 확인합니다.obj가"Hello"와 같은지 확인합니다.- 타입 검사를 합니다.
- 특정 타입이 아닌지 검사합니다.
- 기본(디폴트) 명령문인데 생략할 수도 있습니다.
특정 조건이 만족될 때까지 차례로 모든 조건을 검사하게 됩니다. 즉, 처음으로 만족된 조건에 따른 명령만 수행하게 됩니다. 한 조건이 맞아서 수행하면, 이후 조건은 따로 검사하지 않습니다.
When 표현식
fun main() {
println(whenAssign("안녕"))
println(whenAssign(3.4))
println(whenAssign(1))
println(whenAssign(MyClass()))
}
fun whenAssign(obj: Any): Any {
val result = when (obj) { // 1
1 -> "하나" // 2
"안녕" -> 1 // 3
is Long -> false // 4
else -> 42 // 5
}
return result
}
class MyClass
- 이번엔
when식입니다. 조건에 일치한 결괏값이 최종값으로 반환됩니다. obj값이1과 같다면"하나"라고 지정합니다.obj값이"안녕"과 같다면1로 지정합니다.obj가Long타입의 값이라면false로 지정합니다.- 위의 모든 조건들이 모두 어긋났다면
42로 지정합니다.when명령문에서와는 달리,when식에서는 보통 기본 조건(else) 식이 반드시 필요합니다.when식에서는 가능한 모든 경우를 다 처리해야만 하기 때문입니다.
반복문
코틀린은 흔히 쓰는 반복문 구문인, for, while, do-while 모두를 지원합니다.
for
코틀린에서의 for 구문은 대부분의 언어에서 쓰는 방식과 비슷하게 씁니다.
fun main(args: Array<String>) {
//sampleStart
val cakes = listOf("당근", "치즈", "초콜릿")
for (cake in cakes) { // 1
println("맛있는 ${cake}케이크!")
}
//sampleEnd
}
- 리스트에 있는 모든 케이크에 대해 반복합니다.
while과 do-while
while과 do-while도 대부분의 언어와 비슷하게 동작합니다.
fun eatACake() = println("케이크 먹기")
fun bakeACake() = println("케이크 굽기")
fun main(args: Array<String>) {
var cakesEaten = 0
var cakesBaked = 0
while (cakesEaten < 5) { // 1
eatACake()
cakesEaten++
}
do { // 2
bakeACake()
cakesBaked++
} while (cakesBaked < cakesEaten)
}
- 조건이 참인 동안 해당 블록을 반복 수행합니다.
- 일단 해당 블록을 수행한 다음, 조건을 확인합니다.
이터레이터 Iterators
클래스를 만들 때 iterator 연산자를 구현하면, 나만의 이터레이터를 정의할 수 있습니다.
class Animal(val name: String)
class Zoo(val animals: List<Animal>) {
operator fun iterator(): Iterator<Animal> { // 1
return animals.iterator() // 2
}
}
fun main() {
val zoo = Zoo(listOf(Animal("얼룩말"), Animal("사자")))
for (animal in zoo) { // 3
println("여기 ${animal.name} 있어요!")
}
}
- 클래스 안에 이터레이터를 정의했습니다. 반드시
iterator라는 이름으로 해야 하고operator제어자를 붙여야 합니다. - 아래 요구 사항을 만족하는 이터레이터를 반환해야 합니다.
next():AnimalhasNext():Boolean
- 내가 만든 이터레이터를 써서 동물원에 있는 동물들에 대해 반복했습니다.
이터레이터는 타입 안에 직접 선언하거나, 확장(extension) 함수의 형태로 타입 밖에서 따로 선언할 수도 있습니다.
범위 Ranges
코틀린에서 다양한 방식으로 범위를 표현할 수 있습니다.
fun main() {
//sampleStart
for (i in 0..3) { // 1
print(i)
}
print(" ")
for (i in 0 until 3) { // 2
print(i)
}
print(" ")
for (i in 2..8 step 2) { // 3
print(i)
}
print(" ")
for (i in 3 downTo 0) { // 4
print(i)
}
print(" ")
//sampleEnd
}
- 0부터 3(마지막 포함)까지 범위를 차례로 처리했습니다. C/C++/Java 같은 언어에서
for (i = 0; i <= 3; ++i)로 쓰는 것과 비슷합니다. - 0부터 3(마지막 제외)까지 범위를 순회했습니다. 파이썬의 반복문이나 C/C++/Java 같은 언어들에서의
for (i = 0; i < 3; ++i)구문과 비슷합니다. - 연속 요소에서 몇 단계씩 증가할지 지정했습니다.
- 특정 범위를 거꾸로 순회했습니다.
문자(Char) 범위
fun main() {
//sampleStart
for (c in 'a'..'d') { // 1
print(c)
}
print(" ")
for (c in 'z' downTo 's' step 2) { // 2
print(c)
}
print(" ")
//sampleEnd
}
- 문자 범위를 알파벳 순서대로 순회했습니다.
- 문자 범위에도
step이나downTo를 쓸 수 있습니다.
if문 안에서 범위
fun main() {
//sampleStart
val x = 2
if (x in 1..5) { // 1
print("x는 1에서 5 사이의 수")
}
println()
if (x !in 6..10) { // 2
print("x는 6에서 10 사이의 수가 아님")
}
//sampleEnd
}
- 값이 범위 안에 있는지 확인했습니다.
!in는in의 반대입니다.
같은지 비교
코틀린에서는 구조적으로 비교할 때 ==를 쓰고, 참조값을 비교할 때 ===를 씁니다. 정확하게는, a == b가 if (a == null) b == null else a.equals(b)로 컴파일되는 방식입니다.
fun main() {
//sampleStart
val authors = setOf("셰익스피어", "헤밍웨이", "트웨인")
val writers = setOf("트웨인", "셰익스피어", "헤밍웨이")
println(authors == writers) // 1
println(authors === writers) // 2
//sampleEnd
}
authors.equals(writers)를 호출하게 되고, 집합은 요소의 순서를 무시하기 때문에, 결과적으로true를 반환합니다.authors와writers는 다른 인스턴스(참조값)이기 때문에false를 반환합니다.
if 조건식
코틀린에는 조건 ? 참일 때 : 거짓일 때 3항 연산자가 따로 없습니다. if문이 식의 형태로 그 역할을 대신합니다.
fun main() {
//sampleStart
fun max(a: Int, b: Int) = if (a > b) a else b // 1
println(max(99, -42))
//sampleEnd
}
if문이 식(expression)의 형태로 쓰여서 반환값을 주었습니다.
데이터 클래스
데이터 클래스는 평범한 값들을 보관하는 용도의 클래스를 쉽게 만드는 데 씁니다. 데이터 클래스로 만들면 클래스를 복제하거나 문자열 표현을 하거나 컬렉션의 값으로 쓸 때 필요한 메서드들을 자동으로 만들어 줍니다. 물론, 이 메서드들을 필요에 따라 재정의할 수도 있습니다.
data class User(val name: String, val id: Int) { // 1
override fun equals(other: Any?) =
other is User && other.id == this.id // 2
}
fun main() {
val user = User("석구", 1)
println(user) // 3
val secondUser = User("석구", 1)
val thirdUser = User("동석", 2)
println("user == secondUser: ${user == secondUser}") // 4
println("user == thirdUser: ${user == thirdUser}")
// hashCode() function
println(user.hashCode()) // 5
println(secondUser.hashCode())
println(thirdUser.hashCode())
// copy() function
println(user.copy()) // 6
println(user === user.copy()) // 7
println(user.copy("동석")) // 8
println(user.copy(id = 3)) // 9
println("name = ${user.component1()}") // 10
println("id = ${user.component2()}")
}
data제어자를 붙여서 데이터 클래스를 정의했습니다.- User 인스턴스이고
id값만 같다면 전체 값이 같다고 판단하도록,equals메서드를 재정의했습니다. toString메서드는 자동으로 생성되며,println으로 확인해 보기 좋습니다.- 두 인스턴스의
id만 같다면equals메서드는 참을 반환합니다. - 데이터 클래스의 속성들이 같다면
hashCode값도 똑같게 나옵니다. - 자동 생성된
copy함수로 새 인스턴스를 쉽게 만들어 낼 수 있습니다. copy는 새로운 인스턴스를 만들기 때문에, 원래의 인스턴스와 새 인스턴스는 다른 참조값을 갖습니다.- 데이터 클래스를 복제할 때 특정 속성을 바꿀 수 있습니다.
copy는 클래스 생성자와 같은 순서로 파라미터를 받습니다. copy에 이름지정(named) 파라미터를 전달해서 순서 상관없이 특정 속성 값을 바꿀 수 있습니다.- 자동 생성되는
componentN함수를 써서, 데이터 클래스를 정의할 때의 속성들을 순서대로 접근할 수 있습니다.
Enum 클래스
Enum 클래스는 딱 정해진 수의 구분된 값들을 표현하는 데에 쓰는 타입입니다. 방향, 상태, 모드 등을 표현하기에 좋습니다.
enum class State {
IDLE, RUNNING, FINISHED // 1
}
fun main() {
val state = State.RUNNING // 2
val message = when (state) { // 3
State.IDLE -> "대기 중"
State.RUNNING -> "실행 중"
State.FINISHED -> "완료"
}
println(message)
}
- Enum 상수 세 개가 있는 간단한 Enum 클래스를 선언했습니다. 상수의 수는 미리 정해져 있어야 하고 모두 서로 구분되는 다른 값이어야 합니다.
- Enum 상수는 클래스 이름을 통해서 접근합니다.
- Enum 값을
when-조건식에 쓸 때에는 컴파일러가 모든 경우의 수를 모두 다루었는지 확인할 수 있기 때문에else-조건이 없어도 됩니다. 각 조건으로 Enum에 있는 모든 값을 처리했습니다.
Enum 클래스는 보통 클래스와 마찬가지로 속성이나 메서드를 추가할 수 있고, 각 Enum 상수는 세미콜론으로 구분해 선언합니다.
enum class Color(val rgb: Int) { // 1
RED(0xFF0000), // 2
GREEN(0x00FF00),
BLUE(0x0000FF),
YELLOW(0xFFFF00);
fun containsRed() = (this.rgb and 0xFF0000 != 0) // 3
}
fun main() {
val red = Color.RED
println(red) // 4
println(red.containsRed()) // 5
println(Color.BLUE.containsRed()) // 6
println(Color.YELLOW.containsRed()) // 7
}
- Enum 클래스에 속성 한 개와 메서드 하나를 정의했습니다.
- 이 경우 각각의 Enum 상수에 생성자 파라미터를 하나씩 전달해야 합니다.
- 각 Enum 클래스 멤버는 세미콜론으로 끊어서 나열합니다.
- 기본
toString메서드는 상수값 자체를 반환합니다. 이 경우"RED"입니다. - Enum 상수의 메서드를 호출했습니다.
- Enum 클래스 이름을 통해 메서드를 호출했습니다.
RED와YELLOW의 RGB 색상값은 앞부분 비트가 모두FF이기 때문에true를 반환합니다.
봉인 클래스 Sealed Classes
봉인 클래스로 클래스 상속을 특정 범위로 제한할 수 있습니다. 클래스를 봉인하면, 그 봉인 클래스는 오로지 같은 패키지 안에서만 상속해서 하위 클래스를 만들 수 있습니다. 봉인 클래스가 선언된 패키지가 아닌 다른 패키지에서는 하위 클래스를 만들 수 없습니다.
sealed class Mammal(val name: String) // 1
class Cat(val catName: String) : Mammal(catName) // 2
class Human(val humanName: String, val job: String) : Mammal(humanName)
fun greetMammal(mammal: Mammal): String {
when (mammal) { // 3
is Human ->
return "안녕하세요, ${mammal.name}님. 직업은 ${mammal.job}이군요." // 4
is Cat ->
return "안녕 ${mammal.name}" // 5
} // 6
}
fun main() {
println(greetMammal(Cat("Snowy")))
}
- 봉인 클래스를 정의했습니다.
- 하위 클래스를 정의합니다. 하위 클래스 모두가 같은 패키지 안에 있습니다.
- 봉인 클래스의 인스턴스를
when식에서 쓰고 있습니다. - 스마트캐스트 기능으로
Mammal이Human로 캐스팅되었습니다. - 스마트캐스트 기능으로
Mammal이Cat으로 캐스팅되었습니다. - 봉인 클래스의 모든 하위 클래스를 다뤘으므로,
else조건은 필요치 않습니다. 보통 클래스였다면else조건이 필요합니다.
오브젝트 키워드
코틀린에서 클래스와 오브젝트는 대부분의 객체 지향 언어에서와 같은 방식으로 작동합니다. 청사진 역할을 하는 클래스를 하나 정의하고, 그 클래스의 여러 인스턴스(오브젝트)를 만들어 쓰곤 합니다.
import java.util.Random
class LuckDispatcher { // 1
fun getNumber() { // 2
var objRandom = Random()
println(objRandom.nextInt(90))
}
}
fun main() {
val d1 = LuckDispatcher() // 3
val d2 = LuckDispatcher()
d1.getNumber() // 4
d2.getNumber()
}
- 클래스(청사진)를 정의했습니다.
- 메서드를 정의했습니다.
- 클래스의 인스턴스를 만들었습니다.
- 만든 인스턴스의 메서드를 호출했습니다.
코틀린에는 object 키워드를 써서 특별한 단일 인스턴스를 만들 수 있습니다. 자바 개발을 해보신 분은 "단일"의 뜻이 싱글톤(singleton) 패턴 인스턴스를 의미한다고 보시면 됩니다. 여러 스레드에서 동시에 사용하려고 하더라도 딱 하나의 인스턴스만 생성됩니다.
코틀린에서는 단일(싱글톤) 인스턴스를 만들기 위해서 object를 정의하면 됩니다. 클래스도 아니고, 생성자도 따로 없고, 딱 하나의 지연(lazy) 인스턴스만 준비됩니다. 지연(lazy) 인스턴스라는 이유는, 해당 오브젝트가 처음 접근될 때 생성되고, 접근되지 않는다면 아예 생성되지 않기 때문입니다.
object 식
아래에 object 식을 활용해서 속성들을 담고 있는 오브젝트를 쉽게 만들었습니다. 이렇게 간단히 할 때는 클래스를 선언할 필요 없이 오브젝트를 하나 만들어서 그 안에 멤버들을 선언해 두고 접근해 쓸 수 있습니다.
fun rentPrice(standardDays: Int, festivityDays: Int, specialDays: Int): Unit { // 1
val dayRates = object { // 2
var standard: Int = 30 * standardDays
var festivity: Int = 50 * festivityDays
var special: Int = 100 * specialDays
}
val total = dayRates.standard + dayRates.festivity + dayRates.special // 3
print("Total price: $$total") // 4
}
fun main() {
rentPrice(10, 2, 1) // 5
}
- 파라미터들을 받는 함수를 만들었습니다.
- 결괏값을 계산하려고 오브젝트를 하나 만들었습니다.
- 오브젝트의 속성들을 읽었습니다
- 결과를 프린트합니다.
- 함수를 호출했습니다. 이때 해당 오브젝트가 생성됩니다.
object 단일 인스턴스
object 선언을 할 수도 있습니다. 식의 형태가 아니라면, 변수에 값을 대입하는 용도로 쓸 수 없습니다. 별도의 인스턴스를 만들지 않고, 해당 오브젝트의 멤버에 곧바로 접근할 수 있습니다.
object DoAuth { // 1
fun takeParams(username: String, password: String) { // 2
println("input Auth parameters = $username:$password")
}
}
fun main(){
DoAuth.takeParams("foo", "qwerty") // 3
}
- 오브젝트를 선언했습니다.
- 오브젝트 메서드를 정의했습니다.
- 메서드를 호출했습니다. 보통은 메서드 호출 시점에 오브젝트가 생성되곤 합니다.
동반 오브젝트 Companion Objects
클래스 정의 안쪽에 선언하는 동반 오브젝트를 만들 수 있습니다. 문법적으로 자바의 정적(static) 메서드와 비슷하게 클래스 이름을 통해서 해당 오브젝트 멤버에 접근할 수 있습니다. 하지면 코틀린에서 동반 오브젝트를 쓸만한 상황이라면, 패키지에 곧바로 함수를 선언해 쓰는 방법을 고려해 주세요.
class BigBen { //1
companion object Bonger { //2
fun getBongs(nTimes: Int) { //3
for (i in 1 .. nTimes) {
print("BONG ")
}
}
}
}
fun main() {
BigBen.getBongs(12) //4
}
- 클래스를 정의합니다.
- 동반 오브젝트를 선언합니다. 이름은 생략할 수도 있습니다.
- 동반 오브젝트에 함수를 정의했습니다.
- 클래스 이름을 통해서 동반 오브젝트에 있는 메서드를 호출했습니다.
고차 함수 Higher-Order Functions
고차 함수는 다른 함수를 파라미터로 받거나, 반환값으로 함수를 돌려주는 함수를 말합니다.
함수를 파라미터로 받기
fun calculate(x: Int, y: Int, operation: (Int, Int) -> Int): Int { // 1
return operation(x, y) // 2
}
fun sum(x: Int, y: Int) = x + y // 3
fun main() {
val sumResult = calculate(4, 5, ::sum) // 4
val mulResult = calculate(4, 5) { a, b -> a * b } // 5
println("sumResult $sumResult, mulResult $mulResult")
}
- 고차 함수를 선언했습니다. 정수 파라미터로
x와y두 개와operation이라는 함수를 파라미터로 받았습니다.operation파라미터는 함수 타입입니다. - 고차 함수의 결괏값은
operation함수에, 주어진x,y를 전달해 호출한 결괏값을 그대로 반환했습니다. operation함수 타입과 일치하는 함수를 하나 선언했습니다.- 정수값 두 개와 함수 파라미터
::sum을 전달해 호출했습니다.::는 어떤 함수를 이름으로 지칭하는 표기법입니다. - 람다 함수 문법을 써서 해당 고차 함수에 전달해 호출했습니다. 훨씬 깔끔하죠?
함수를 반환하는 함수
fun operation(): (Int) -> Int { // 1
return ::square
}
fun square(x: Int) = x * x // 2
fun main() {
val func = operation() // 3
println(func(2)) // 4
}
- 함수를 반환하는 고차 함수를 선언했습니다.
(Int) -> Int는square함수의 파라미터 타입과 반환 타입을 나타냅니다. - 해당 타입과 일치하는 함수를 선언했습니다.
operation함수를 호출한 결괏값을 변수에 대입했습니다. 여기서func는operation함수에서 반환한square함수를 가리키게 됩니다.func함수를 호출했습니다. 결과적으로square함수가 실행됩니다.
람다 함수
람다 함수는 그 자리에서 곧바로 함수를 만드는 간단한 방법입니다. 타입추론 기능과 암묵적 it 변수 덕분에 매우 간결하게 람다 함수를 작성할 수 있습니다.
fun main() {
//sampleStart
// 이하 모든 예제는 주어진 문자열을 대문자로 바꿉니다.
// 즉, 모두 String을 받아서 String을 반환하는 함수입니다.
val upperCase1: (String) -> String = { str: String -> str.uppercase() } // 1
val upperCase2: (String) -> String = { str -> str.uppercase() } // 2
val upperCase3 = { str: String -> str.uppercase() } // 3
// val upperCase4 = { str -> str.uppercase() } // 4
val upperCase5: (String) -> String = { it.uppercase() } // 5
val upperCase6: (String) -> String = String::uppercase // 6
println(upperCase1("hello"))
println(upperCase2("hello"))
println(upperCase3("hello"))
println(upperCase5("hello"))
println(upperCase6("hello"))
//sampleEnd
}
- 람다 함수의 모든 요소에 타입을 명시했습니다. 람다 본문은 중괄호 안에 작성했고,
(String) -> String타입의 변수에 지정했습니다. - 람다 안쪽에 타입추론 기능을 활용했습니다. 지정되는 변수의 타입으로부터 람다의 타입을 추론할 수 있습니다.
- 람다 바깥으로 타입추론이 이어졌습니다. 람다의 파라미터와 반환 타입으로부터 바깥으로 변수의 타입이 추론되었습니다.
- 둘 다 추론을 할 수는 없습니다. 컴파일러가 어떤 타입인지 추론할 방법이 없습니다.
- 파라미터 한 개를 받는 람다의 경우, 별도로 이름을 주지 않고, 암묵적으로
it변수를 사용할 수 있습니다.it의 타입을 추론할 수 있는 경우에 특히 더 유용하게 쓰입니다. - 람다가 딱 한 번의 함수 호출로 구성된다면, 함수를 지칭하는 (
::) 표기법으로 선언할 수 있습니다.
확장함수와 확장속성
코틀린에 있는 확장 기능으로 이미 있는 특정 클래스에 새로운 멤버를 나중에 추가할 수 있습니다. 확장함수와 확장속성이 있고, 둘 다 보통의 함수와 속성처럼 생겼지만 한 가지 중요한 차이점이 있습니다. 어떤 타입에서 확장할지 명시한다는 점이 다릅니다.
data class Item(val name: String, val price: Int) // 1
data class Order(val items: Collection<Item>)
fun Order.maxPricedItemValue(): Int = this.items.maxByOrNull { it.price }?.price ?: 0 // 2
fun Order.maxPricedItemName() = this.items.maxByOrNull { it.price }?.name ?: "NO_PRODUCTS"
val Order.commaDelimitedItemNames: String // 3
get() = items.map { it.name }.joinToString()
fun main() {
val order = Order(listOf(Item("빵", 5000), Item("와인", 29000), Item("생수", 1500)))
println("가장 비싼 식료품: ${order.maxPricedItemName()}") // 4
println("가장 비싼 가격: ${order.maxPricedItemValue()}")
println("식료품: ${order.commaDelimitedItemNames}") // 5
}
Item과Order를 정의했습니다.Order는Item컬렉션이 들어있습니다.Order타입에 확장 함수를 추가했습니다.Order타입에 확장 속성을 추가했습니다.Order인스턴스에 있는 확장 함수를 호출했습니다.Order인스턴스에 있는 확장 속성을 읽었습니다.
심지어 null에도 확장 함수를 붙여서, 어떤 인스턴스가 null인지 아닌지 확인할 수 있습니다.
//sampleStart
fun <T> T?.nullSafeToString() = this?.toString() ?: "NULL" // 1
//sampleEnd
fun main() {
println(null.nullSafeToString())
println("Kotlin".nullSafeToString())
}
리스트 List
리스트는 어떤 순서대로 나열한 컬렉션입니다. 코틀린의 리스트는 크게 두 종류가 있는데, 하나는 변경 가능한 리스트(MutableList)고, 다른 하나는 읽기전용 리스트(List)가 있습니다. 리스트를 만들려면, 표준 라이브러리에 있는 함수인 listOf()를 써서 읽기전용 리스트를 만들거나, mutableListOf() 함수를 써서 변경가능 리스트를 만듭니다. 리스트를 이후에 변경하지 못하게 막으려면, 리스트를 List로 캐스트(타입변경)해서 써도 됩니다.
val systemUsers: MutableList<Int> = mutableListOf(1, 2, 3) // 1
val sudoers: List<Int> = systemUsers // 2
fun addSystemUser(newUser: Int) { // 3
systemUsers.add(newUser)
}
fun getSysSudoers(): List<Int> { // 4
return sudoers
}
fun main() {
addSystemUser(4) // 5
println("전체 sudoers: ${getSysSudoers().size}") // 6
getSysSudoers().forEach { // 7
i -> println("이용자 ${i}에 대한 정보")
}
// getSysSudoers().add(5) <- Error! // 8
}
MutableList를 만들었습니다.- 그 리스트를 읽기 전용으로 캐스트(타입변경)했습니다.
MutableList에 새로운 아이템을 추가했습니다.- 읽기전용
List를 반환했습니다. MutableList를 변경했습니다. 불변 리스트로 캐스팅해서 쓰던 것들도 모두 함께 변경됩니다. 실제로는 같은 인스턴스를 가리키고 있기 때문입니다.- 읽기전용 리스트의 크기를 조회했습니다.
- 리스트 아이템을 차례로 읽어 프린트했습니다.
- 읽기전용 리스트에 값을 쓰려고 하면 컴파일 에러가 발생합니다.
집합 Set
집합은 순서는 무관하되 중복값이 없는 컬렉션입니다. setOf() 함수나 mutableSetOf() 함수를 써서 집합을 만들 수 있습니다. 변이(mutable) 집합에 대한 읽기전용 뷰를 만들려면, Set으로 캐스팅하면 됩니다.
val openIssues: MutableSet<String> =
mutableSetOf("uniqueDescr1", "uniqueDescr2", "uniqueDescr3") // 1
fun addIssue(uniqueDesc: String): Boolean {
return openIssues.add(uniqueDesc) // 2
}
fun getStatusLog(isAdded: Boolean): String {
return if (isAdded) "잘 남겼습니다." else "중복 이슈입니다." // 3
}
fun main() {
val aNewIssue: String = "uniqueDescr4"
val anIssueAlreadyIn: String = "uniqueDescr2"
val aNewIssueStat = getStatusLog(addIssue(aNewIssue))
val anIssueAlreadyInStat = getStatusLog(addIssue(anIssueAlreadyIn))
println("이슈 $aNewIssue $aNewIssueStat") // 4
println("이슈 $anIssueAlreadyIn $anIssueAlreadyInStat") // 5
}
- 주어진 아이템들을 담고 있는
Set을 만들었습니다. - 아이템을 추가하고, 잘 들어갔는지 불린 값을 반환합니다.
- 함수의 파라미터에 따라 문자열을 반환합니다.
- 새로운 요소를
Set에 잘 추가했으면 성공 메시지를 프린트합니다. - 이미 똑같은 요소가 있어서 추가할 수 없다면 실패 메시지를 프린트합니다.
맵 Map
맵은 키-값 쌍으로 이뤄진 컬렉션입니다. 각 키는 고유해야 하고, 쌍으로 연결된 값을 가져올 때 사용합니다. 맵을 만들려면 mapOf()나 mutableMapOf() 함수를 씁니다. 중위함수 to를 쓰면 좀 더 간결하게 맵을 만들 수 있습니다. 변경가능(mutable) 맵의 읽기전용 뷰를 얻으려면 Map으로 캐스팅하면 됩니다.
const val POINTS_X_PASS: Int = 15
val EZPassAccounts: MutableMap<Int, Int> = mutableMapOf(1 to 100, 2 to 100, 3 to 100) // 1
val EZPassReport: Map<Int, Int> = EZPassAccounts // 2
fun updatePointsCredit(accountId: Int) {
if (EZPassAccounts.containsKey(accountId)) { // 3
println("업데이트 $accountId...")
EZPassAccounts[accountId] = EZPassAccounts.getValue(accountId) + POINTS_X_PASS // 4
} else {
println("에러: 계정이 없습니다 (id: $accountId)")
}
}
fun accountsReport() {
println("EZ-Pass 현황:")
EZPassReport.forEach { // 5
k, v -> println("ID $k: 포인트 $v")
}
}
fun main() {
accountsReport() // 6
updatePointsCredit(1) // 7
updatePointsCredit(1)
updatePointsCredit(5) // 8
accountsReport() // 9
}
- 변경가능
Map을 만들었습니다. - 그
Map의 읽기전용 뷰를 만들었습니다. Map에 이미 해당 키가 있는지 확인합니다.- 해당하는 값을 읽어서 상수를 더합니다.
- 불변
Map의 모든 요소에 대해 키-값 쌍을 프린트합니다. - 업데이트에 앞서, 계정 잔액을 확인해 둡니다.
- 기존 계정을 두 번 업데이트합니다.
- 계정을 없는데 업데이트하려면 에러 메시지가 프린트됩니다.
- 업데이트를 끝내고 나서, 계정 잔액을 확인합니다.
필터 filter
filter 함수로 컬렉션의 요소들을 선별할 수 있습니다. 원하는 조건을 판단하기 위해 함수 파라미터를 받습니다. 각각의 요소에 대해 주어진 함수를 적용해 true가 나오는 요소들만 모은 새 컬렉션을 반환합니다.
fun main() {
//sampleStart
val numbers = listOf(1, -2, 3, -4, 5, -6) // 1
val positives = numbers.filter { x -> x > 0 } // 2
val negatives = numbers.filter { it < 0 } // 3
//sampleEnd
println("Numbers: $numbers")
println("Positive Numbers: $positives")
println("Negative Numbers: $negatives")
}
- 숫자를 담고 있는 컬렉션을 만들었습니다.
- 양수만 취합니다.
it표기법을 써서 음수만 취했습니다.
있나없나 any, all, none
이하, 컬렉션에 어떤 요소가 있는지 없는지 확인하는 고차 함수들입니다.
하나라도 있나? any
any 함수는 컬렉션에 주어진 명제에 참인 요소가 하나라도 있으면 true를 반환합니다.
fun main() {
//sampleStart
val numbers = listOf(1, -2, 3, -4, 5, -6) // 1
val anyNegative = numbers.any { it < 0 } // 2
val anyGT6 = numbers.any { it > 6 } // 3
//sampleEnd
println("숫자들: $numbers")
println("0보다 작은 수가 있나: $anyNegative")
println("6보다 큰 수가 있나: $anyGT6")
}
- 숫자들을 담고 있는 컬렉션을 준비했습니다.
- 음수가 있는지 확인해 봅니다.
- 6보다 큰 수가 있나 확인합니다.
모두 그런가? all
all 함수는 모든 요소가 해당 명제를 만족할 때 true를 반환합니다.
fun main() {
//sampleStart
val numbers = listOf(1, -2, 3, -4, 5, -6) // 1
val allEven = numbers.all { it % 2 == 0 } // 2
val allLess6 = numbers.all { it < 6 } // 3
//sampleEnd
println("숫자들: $numbers")
println("모든 수가 짝수인가: $allEven")
println("모든 수가 6보다 작은가: $allLess6")
}
- 여러 숫자를 담은 컬렉션을 준비했습니다.
- 모든 숫자가 짝수인지 확인합니다.
- 모든 수가 6보다 작은지 확인합니다.
없나? none
none 함수는, 주어진 조건에 해당하는 요소가 하나도 없는 경우에만 true를 반환합니다. 하나라도 해당되면 false가 나옵니다.
fun main() {
//sampleStart
val numbers = listOf(1, -2, 3, -4, 5, -6) // 1
val allEven = numbers.none { it % 2 == 1 } // 2
val allLess6 = numbers.none { it > 6 } // 3
//sampleEnd
println("숫자들: $numbers")
println("모든 수가 짝수인가: $allEven")
println("6보다 큰 수가 없나: $allLess6")
}
- 여러 숫자를 담은 컬렉션을 준비합니다.
- 홀수가 하나도 없는지 확인합니다. (모두 짝수인지)
- 6보다 큰 수가 없는지 확인합니다.
찾기
find 함수는 어떤 컬렉션에 주어진 명제에 참인 있는지 앞에서부터 차례로 찾아봅니다. findLast 함수는 마찬가지로 주어진 명제에 참인 요소를 찾는데, 뒤에서부터 순서대로 찾는다는 점이 다릅니다. 두 함수 모두, 조건에 해당하는 요소가 없다면 null을 반환합니다.
fun main() {
//sampleStart
val words = listOf("컬렉션에", "있는", "어떤", "아이템", "골라서", "찾기") // 1
val first = words.find { it.startsWith("아이") } // 2
val last = words.findLast { it.startsWith("아이") } // 3
val nothing = words.find { it.contains("없는") } // 4
//sampleEnd
println("\"아이\"로 시작하는 첫번째 단어는 \"$first\"입니다.")
println("\"아이\"로 시작하는 마지막 단어는 \"$last\"입니다.")
println("\"없는\"을 포함한 단어는 ${nothing?.let { "\"$it\"" } ?: "null"}입니다.")
}
- 단어들이 들어있는 컬렉션을 준비합니다.
- "아이"로 시작하는 첫 번째 단어를 찾아봅니다.
- "아이"로 시작하는 마지막 단어를 찾습니다.
- "없는"을 담고 있는 첫 번째 단어를 찾습니다.
처음과 마지막
first와 last
이 함수들은 컬렉션의 첫 번째와 마지막 아이템을 반환합니다. 명제로 찾을 수도 있는데, 그 경우 명제에 참인 첫 아이템 또는 마지막 아이템을 찾아줍니다. 컬렉션이 비어있거나 조건에 해당하는 값이 없다면, NoSuchElementException 예외가 발생합니다.
fun main() {
//sampleStart
val numbers = listOf(1, -2, 3, -4, 5, -6) // 1
val first = numbers.first() // 2
val last = numbers.last() // 3
val firstEven = numbers.first { it % 2 == 0 } // 4
val lastOdd = numbers.last { it % 2 != 0 } // 5
//sampleEnd
println("숫자들: $numbers")
println("처음은 $first, 마지막은 $last, 첫 짝수는 $firstEven, 마지막 홀수는 $lastOdd")
}
- 여러 숫자를 담은 컬렉션을 만들었습니다.
- 첫 번째 아이템을 구했습니다.
- 마지막 아이템을 구했습니다.
- 첫 짝수를 구했습니다.
- 마지막 홀수를 구했습니다.
firstOrNull과 lastOrNull
이 함수들은 위 함수들과 거의 같지만, 한 가지 차이점이 있습니다. 해당 값이 없는 경우 예외를 발생시키는 대신에 null을 반환한다는 점이 다릅니다.
fun main() {
//sampleStart
val words = listOf("foo", "bar", "baz", "faz") // 1
val empty = emptyList<String>() // 2
val first = empty.firstOrNull() // 3
val last = empty.lastOrNull() // 4
val firstF = words.firstOrNull { it.startsWith('f') } // 5
val firstZ = words.firstOrNull { it.startsWith('z') } // 6
val lastF = words.lastOrNull { it.endsWith('f') } // 7
val lastZ = words.lastOrNull { it.endsWith('z') } // 8
//sampleEnd
println("빈 리스트에 대해: 처음은 $first, 마지막은 $last")
println("단어 리스트: 'f'로 시작하는 첫 아이템은 $firstF, 'z'로 시작하는 첫 아이템은 $firstZ")
println("단어 리스트: 'f'로 끝나는 마지막 아이템은 is $lastF, 'z'로 끝나는 마지막 아이템은 $lastZ")
}
- 단어들을 담은 컬렉션을 만들었습니다.
- 빈 컬렉션을 준비했습니다.
- 빈 컬렉션에서 첫 아이템을 구했습니다.
null이 반환됩니다. - 빈 컬렉션에서 마지막 아이템을 구했습니다. 마찬가지로
null이 반환됩니다. - 'f'로 시작하는 첫 단어를 구합니다.
- 'z'로 시작하는 첫 단어를 구합니다.
- 'f'로 끝나는 마지막 단어를 구합니다.
- 'z'로 끝나는 마지막 단어를 구합니다.
몇 개 있나
count 함수는 컬렉션에 몇 개의 아이템이 들어있는지 반환합니다. 참 또는 거짓을 판단하는 함수를 전달하면 해당하는 아이템이 몇 개 있는지 확인해 줍니다.
fun main() {
//sampleStart
val numbers = listOf(1, -2, 3, -4, 5, -6) // 1
val totalCount = numbers.count() // 2
val evenCount = numbers.count { it % 2 == 0 } // 3
//sampleEnd
println("전체 아이템 갯수: $totalCount")
println("짝수 갯수: $evenCount")
}
- 여러 숫자를 담은 컬렉션을 만들었습니다.
- 전체 개수를 구합니다.
- 짝수가 몇 개 있는지 확인합니다.
변환
map 확장함수로 컬렉션에 있는 각 아이템을 모두 변환할 수 있습니다. 각 아이템을 변환하는 함수를 파라미터로 전달합니다.
fun main() {
//sampleStart
val numbers = listOf(1, -2, 3, -4, 5, -6) // 1
val doubled = numbers.map { x -> x * 2 } // 2
val tripled = numbers.map { it * 3 } // 3
//sampleEnd
println("숫자들: $numbers")
println("두 배한 숫자들: $doubled")
println("세 배한 숫자들: $tripled")
}
- 숫자들을 담고 있는 컬렉션을 만듭니다.
- 숫자들을 두 배로 만듭니다.
it표기법을 써서 세 배로 만들었습니다.
맵으로 만들기
associateBy 함수와 groupBy 함수를 써서, 어떤 컬렉션에 있는 아이템들 담은 맵으로 만들 수 있습니다. keySelector 파라미터를 전달해서 각 아이템으로부터 키를 만들고, 선택적으로 valueSelector 함수도 전달해서 맵에 들어갈 값을 변환하는 용도로 쓸 수 있습니다.
두 함수의 차이점은, 컬렉션 요소들 중 똑같은 키값이 있는 경우에 드러납니다.
associateBy는 이전 값은 무시하고 가장 마지막 값만 보관합니다.groupBy는 해당 키값에 일치하는 모든 요소를 리스트로 담아서 보관합니다.
반환되는 맵에 들어가는 키-값 순서는 원래 컬렉션에 담긴 아이템의 순서와 일치합니다.
fun main() {
//sampleStart
data class Person(val name: String, val city: String, val phone: String) // 1
val people = listOf( // 2
Person("석구", "서울", "02-1234-5678"),
Person("성태", "부산", "051-234-9876"),
Person("효리", "제주", "064-123-4567"),
Person("상순", "제주", "064-234-8901"))
val phoneBook = people.associateBy { it.phone } // 3
val cityBook = people.associateBy(Person::phone, Person::city) // 4
val peopleCities = people.groupBy(Person::city, Person::name) // 5
val lastPersonCity = people.associateBy(Person::city, Person::name) // 6
//sampleEnd
println("전화번호부: $phoneBook")
println("전화번호별 도시: $cityBook")
println("도시별 사는 사람들: $peopleCities")
println("각 도시에 사는 마지막 사람: $lastPersonCity")
}
- 사람을 표현하는 데이터 클래스를 정의했습니다.
- 사람들을 컬렉션에 담았습니다.
- 전화번호를 기준으로 맵을 만들었습니다. 여기서
it.phone이keySelector가 되었습니다.valueSelector를 따로 지정하지 않았으므로,Person인스턴스 자체가 맵에 값으로 들어갑니다. - 전화번호 기준으로 맵을 만들면서, 그 사람들이 사는 도시를 담았습니다.
Person::city가valueSelector로 주어졌으며, 맵에는 도시 정보만 값으로 담기게 됩니다. - 도시별로 사는 사람들을 담은 맵을 만들었습니다. 맵에 담긴 각각의 값은 사람들 "이름을 담고 있는 리스트"입니다.
- 도시별로 살고 있는 사람들을 한 명씩만 담은 맵을 만듭니다. 원래 컬렉션 기준 순서로 따졌을 때, 해당 도시에 사는 마지막 사람만 맵에 담기게 됩니다.
둘로 가르기
partition 함수는 컬렉션에 있는 아이템을 주어진 명제에 대해 참인 것들과 거짓인 것들로 나누어 한 쌍(Pair)의 리스트로 반환합니다.
- 해당 명제에
true로 나오는 아이템들 - 해당 명제에
false로 나오는 아이템들
fun main() {
//sampleStart
val numbers = listOf(1, -2, 3, -4, 5, -6) // 1
val evenOdd = numbers.partition { it % 2 == 0 } // 2
val (positives, negatives) = numbers.partition { it > 0 } // 3
//sampleEnd
println("숫자들: $numbers")
println("짝수: ${evenOdd.first}")
println("홀수: ${evenOdd.second}")
println("양수: $positives")
println("음수: $negatives")
}
- 여러 숫자들을 담은 컬렉션을 준비했습니다.
numbers안에 있는 숫자들을 짝수와 홀수 리스트로 나눠서Pair에 담았습니다.numbers에 있는 숫자를 양수와 음수로 나눈Pair에 두 리스트를 담았고, 그 두 리스트를 다시 구조분해(destructuring)로 두 변수에 나눠 담았습니다.
변환해 펼치기, 플랫맵
flatMap은 컬렉션에 있는 아이템을 각각 순회 가능한 형태로 변환한 다음, 전체 결과를 하나의 리스트로 합쳐냅니다. 각 아이템을 어떻게 변환할지는 사용자가 직접 정의합니다.
fun main() {
//sampleStart
val fruitsBag = listOf("사과", "오렌지", "바나나") // 1
val clothesBag = listOf("티셔츠", "양말", "청바지") // 2
val cart = listOf(fruitsBag, clothesBag) // 3
val mapBag = cart.map { it } // 4
val flatMapBag = cart.flatMap { it } // 5
//sampleEnd
println("장바구니 묶음: $mapBag")
println("구매한 물품들: $flatMapBag")
}
- 과일을 담은 컬렉션을 정의했습니다.
- 의류를 담은 컬렉션을 정의했습니다.
- 두 리스트
fruitsBag과clothesBag를 다시cart라는 리스트에 담았습니다. cart의 아이템들을map으로 변환했습니다. 두 리스트가 각각 그대로 들어있습니다.- 이번에는
flatMap메서드를 썼기 때문에, 두 리스트 전체가 하나로 리스트로 합쳐집니다.
최소와 최대
minOrNull과 maxOrNull 함수는, 각각 컬렉션에 있는 최솟값과 최댓값을 찾아줍니다. 만약 컬렉션이 비어있다면, null을 반환합니다.
fun main() {
//sampleStart
val numbers = listOf(1, 2, 3)
val empty = emptyList<Int>()
val only = listOf(3)
println("숫자들: $numbers, 최소 = ${numbers.minOrNull()} 최대 = ${numbers.maxOrNull()}") // 1
println("빈 리스트: $empty, 최소 = ${empty.minOrNull()}, 최대 = ${empty.maxOrNull()}") // 2
println("한개짜리: $only, 최소 = ${only.minOrNull()}, 최대 = ${only.maxOrNull()}") // 3
//sampleEnd
}
- 뭔가 들어 있는 컬렉션의 경우에는, 최솟값과 최댓값이 반환됩니다.
- 빈 컬렉션에 대해서는
null이 반환됩니다. - 딱 한 개의 원소가 들어있는 컬렉션에 대해서는, 두 함수 모두 그 한 값이 반환됩니다.
정렬
컬렉션의 sorted 메서드는 전체 아이템을 오름차순으로 정렬합니다. sortedBy 메서드는 각 요소들에 대해 어떤 기준으로 정렬할지 정하는 함수를 파라미터로 받아서 오름차순으로 정렬한 리스트를 반환합니다.
import kotlin.math.abs
fun main() {
//sampleStart
val shuffled = listOf(5, 4, 2, 1, 3, -10) // 1
val natural = shuffled.sorted() // 2
val inverted = shuffled.sortedBy { -it } // 3
val descending = shuffled.sortedDescending() // 4
val descendingBy = shuffled.sortedByDescending { abs(it) } // 5
//sampleEnd
println("무작위순: $shuffled")
println("오름차순: $natural")
println("오른차순 역순: $inverted")
println("내림차순: $descending")
println("절댓값의 내림차순: $descendingBy")
}
- 마구 섞인 숫자들을 담은 컬렉션을 준비했습니다.
- 숫자들을 차례로 정렬했습니다.
-it를 기준 함수로 전달해서, 전체 숫자들을 역순으로 정렬했습니다.sortedDescending메서드를 써서 내림차순으로 정렬했습니다.abs(it)를 기준 함수로 쓰고, 역순으로 정렬했습니다.
맵 조회
맵에 [] 연산자를 쓰면 키에 대응되는 값을 찾습니다. 만약 해당 키 값이 맵에 없다면 null이 반환됩니다.
getValue 함수는 대응하는 값을 반환하거나, 없다면 예외를 발생시킵니다. withDefault로 만든 맵의 경우에는, getValue로 없는 키를 조회해도 예외를 발생시키지 않고 미리 지정한 기본값을 반환해 줍니다.
fun main(args: Array<String>) {
//sampleStart
val map = mapOf("키" to 42)
val value1 = map["키"] // 1
val value2 = map["키2"] // 2
val value3: Int = map.getValue("키") // 1
val mapWithDefault = map.withDefault { k -> k.length }
val value4 = mapWithDefault.getValue("키2") // 3
try {
map.getValue("없는 키") // 4
} catch (e: NoSuchElementException) {
println("메시지: $e")
}
//sampleEnd
println("value1 = $value1")
println("value2 = $value2")
println("value3 = $value3")
println("value4 = $value4")
}
"키"에 해당하는 값인 42가 반환됩니다."키2"는 이 맵에 없기 때문에null을 반환합니다."키2"가 이 맵에 없지만, 디폴트 값인 2가 반환됩니다."없는 키"가 이 맵에 없기 때문에,NoSuchElementException예외가 발생합니다.
지퍼
zip 함수는 주어진 두 컬렉션의 각 아이템을 합쳐서 새로운 컬렉션 한 개로 만듭니다. 기본적으로, 결과 컬렉션에는 주어진 두 컬렉션의 각 아이템들을 순서대로 하나씩 취한 Pair를 차례로 담습니다. 별도로, 결과 컬렉션을 어떤 구조로 만들어낼지 정의할 수도 있습니다.
결과 컬렉션에는, 주어진 두 컬렉션에 담긴 아이템 개수 중 더 작은 수만큼 담기게 됩니다. 두 컬렉션의 아이템이 같은 개수라면, 결과 컬렉션에도 같은 개수의 아이템이 담기게 됩니다.
fun main() {
//sampleStart
val A = listOf("a", "b", "c") // 1
val B = listOf(1, 2, 3, 4) // 1
val resultPairs = A zip B // 2
val resultReduce = A.zip(B) { a, b -> "$a$b" } // 3
//sampleEnd
println("A zip B: $resultPairs")
println("\$A\$B: $resultReduce")
}
- 컬렉션을 두 개 준비했습니다.
- 두 컬렉션을 합쳐서 하나의 리스트로 합쳤습니다. 중위 표기법으로
zip을 썼습니다. - 두 컬렉션을 합치는데, 각 아이템 쌍을 한 문자열로 합치도록 했습니다.
안전한 조회
getOrElse 메서드로 컬렉션의 아이템에 안전하게 접근할 수 있습니다. 몇 번째 요소에 접근할지 인덱스에 더불어, 값이 없을 경우나 범위를 초과한 경우에 반환할 기본값을 지정합니다.
fun main() {
//sampleStart
val list = listOf(0, 10, 20)
println(list.getOrElse(1) { 42 }) // 1
println(list.getOrElse(10) { 42 }) // 2
//sampleEnd
}
1번 인덱스에 있는 아이템을 구해 프린트합니다.10인덱스는 범위를 초과했기 때문에, 기본값인42를 프린트했습니다.
Map의 getOrElse
Map에 대해서도 getOrElse 메서드를 써서 특정 키로 조회할 수 있습니다.
fun main() {
//sampleStart
val map = mutableMapOf<String, Int?>()
println(map.getOrElse("x") { 1 }) // 1
map["x"] = 3
println(map.getOrElse("x") { 1 }) // 2
map["x"] = null
println(map.getOrElse("x") { 1 }) // 3
//sampleEnd
}
- 맵에
"x"키가 없으므로, 기본값을 프린트했습니다. - 이번에는
"x"키에 해당하는 값인3을 프린트합니다. "x"가 다시 없어졌기에 기본 값을 프린트합니다.
let 블록
코틀린 표준 라이브러리에 있는 let 함수는 범위를 제한할 때나 null-검사를 할 때 씁니다. 오브젝트에 대해 let으로 감싼 코드블록을 실행한 후 최종 결괏값을 전체 식(expression)의 반환값으로 돌려받습니다. let으로 전달한 코드블록 안에서는 it(다른 이름으로도 지정가능)으로 해당 오브젝트를 참조할 수 있습니다.
fun customPrint(s: String) {
print(s.uppercase())
}
fun main() {
//sampleStart
val empty = "test".let { // 1
customPrint(it) // 2
it.isEmpty() // 3
}
println(" 비었나: $empty")
fun printNonNull(str: String?) {
println("\"$str\" 프린트:")
str?.let { // 4
print("\t")
customPrint(it)
println()
}
}
fun printIfBothNonNull(strOne: String?, strTwo: String?) {
strOne?.let { firstString -> // 5
strTwo?.let { secondString ->
customPrint("$firstString : $secondString")
println()
}
}
}
printNonNull(null)
printNonNull("문자열")
printIfBothNonNull("첫번째", "두번째")
//sampleEnd
}
"test"문자열 값에 대해, 주어진 코드 블록을 실행합니다.it으로"test"에 접근합니다.let은 블록 전체의 마지막 값을 반환합니다.- 안전한 호출방식을 썼기 때문에,
let과 그 코드블록은null이 아닌 경우에만 실행됩니다.null일 때는 실행되지 않고 무시됩니다. it대신에 다른 이름으로 접근할 수도 있습니다. 이 방법으로 통해, 바깥쪽let구문에 참조되는 오브젝트에 접근했습니다.
run 블록
let과 마찬가지로, run도 표준 라이브러리에 있는 블록 함수입니다. 기본적으로, 주어진 코드 블록을 실행하고 결괏값을 반환하는 점이 같습니다.
run이 다른 점은, 대상 오브젝트를 it이 아닌, this로 접근한다는 점입니다. 코드블록 안에서 원래 오브젝트의 메서드를 주로 호출하는 경우에 유용합니다.
fun main() {
//sampleStart
fun getNullableLength(ns: String?) {
println("대상 \"$ns\":")
ns?.run { // 1
println("\t비었나? => " + isEmpty()) // 2
println("\t길이 => $length")
length // 3
}
}
getNullableLength(null)
getNullableLength("")
getNullableLength("코틀린에서 어떤 문자열")
//sampleEnd
}
- 주어진 코드블록을 null-가능 변수에 대해 실행했습니다.
null이므로 무시합니다. run코드블록 안에서는 원래 오브젝트의 멤버에 곧바로 접근할 수 있습니다.- 주어진
String이 null이 아니라면,run코드블록은 해당 문자열의 길이를 결괏값으로 돌려줍니다.
with 블록
with는 (확장 함수가 아닌) 일반 함수인데, 주어진 코드블록 안에서, 파라미터로 전달한 대상의 멤버에 접근할 때 인스턴스 이름을 생략할 수 있습니다.
class Configuration(var host: String, var port: Int)
fun main() {
val configuration = Configuration(host = "127.0.0.1", port = 9000)
//sampleStart
with(configuration) {
println("$host:$port") // 1
}
// instead of:
println("${configuration.host}:${configuration.port}")
//sampleEnd
}
- with 블록 안에서는
configuration.host대신에host로 간단히 접근했습니다.
apply 블록
오브젝트에 대해 호출하는 apply 블록은 주어진 코드블록을 수행한 다음, 전체 반환값으로 원래 오브젝트를 그대로 돌려줍니다. 코드블록 안에서는 객체에 접근할 때 this로 참조할 수 있습니다. 오브젝트를 초기화할 때 편리하게 쓸 수 있습니다.
data class Person(var name: String, var age: Int, var about: String) {
constructor() : this("", 0, "")
}
fun main() {
//sampleStart
val jake = Person() // 1
val stringDescription = jake.apply { // 2
name = "Jake" // 3
age = 30
about = "안드로이드 개발자"
}.toString() // 4
//sampleEnd
println(stringDescription)
}
Person()인스턴스를 기본 속성값으로 만들었습니다.- 만든 인스턴스에 대해 코드블록을 실행합니다.
apply안에서는,jake.name = "Jake"로 쓴 것과 동일합니다.- 전체 반환값은 인스턴스 자체가 됩니다. 그래서, 곧바로 이어서 다른 메서드(
.toString())를 호출했습니다.
also 블록
also 블록도 apply 블록과 비슷하게 동작합니다. 주어진 코드블록을 수행하고 오브젝트 자체를 반환합니다. 블록 안에서 오브젝트는 it으로 접근합니다. 다른 함수에 파라미터로 전달하기 편리합니다. 로그를 남긴다거나 하는 추가 작업을 할 때 쓰기 좋습니다.
data class Person(var name: String, var age: Int, var about: String) {
constructor() : this("", 0, "")
}
fun writeCreationLog(p: Person) {
println("${p.name} 인스턴스를 만들었습니다.")
}
fun main() {
//sampleStart
val jake = Person("Jake", 30, "안드로이드 개발자") // 1
.also { // 2
writeCreationLog(it) // 3
}
//sampleEnd
}
- 주 생성자로
Person()오브젝트를 생성했습니다. also블록을 실행했고, 그 반환값은 원래 오브젝트입니다.- 원래 오브젝트(
it)를 파라미터로 주어 로그를 남기는 함수를 호출했습니다.
위임 패턴
코틀린에는 위임 패턴을 구현하기 간편한 방법이 있습니다.
interface SoundBehavior { // 1
fun makeSound()
}
class ScreamBehavior(val n:String): SoundBehavior { // 2
override fun makeSound() = println("${n.toUpperCase()} !!!")
}
class RockAndRollBehavior(val n:String): SoundBehavior { // 2
override fun makeSound() = println("I'm The King of Rock 'N' Roll: $n")
}
// Tom Araya is the "singer" of Slayer
class TomAraya(n:String): SoundBehavior by ScreamBehavior(n) // 3
// You should know ;)
class ElvisPresley(n:String): SoundBehavior by RockAndRollBehavior(n) // 3
fun main() {
val tomAraya = TomAraya("Thrash Metal")
tomAraya.makeSound() // 4
val elvisPresley = ElvisPresley("Dancin' to the Jailhouse Rock.")
elvisPresley.makeSound()
}
SoundBehavior인터페이스를 정의했습니다.ScreamBehavior클래스와RockAndRollBehavior클래스는 해당 인터페이스를 구현했고, 각각 필요한 메서드를 작성했습니다.TomAraya클래스와ElvisPresley클래스도 마찬가지로SoundBehavior인터페이스를 구현했습니다. 인터페이스에 필요한 메서드를 직접 구현하지 않고,by키워드로 다른 오브젝트에 위임했습니다.tomAraya나elvisPresley의makeSound()메서드를 호출하면, 각각 위임 오브젝트의 메서드가 불립니다.
위임속성
코틀린에 있는 위임속성 기능은 인스턴스의 set과 get 속성에 접근하는 걸 위임할 수 있게 해 줍니다. 위임할 객체에는 getValue 메서드가 꼭 있어야 합니다. 변경 가능한(mutable) 속성이라면 setValue 메서드도 작성해야 합니다.
import kotlin.reflect.KProperty
class Example {
var p: String by Delegate() // 1
override fun toString() = "예제 클래스"
}
class Delegate() {
operator fun getValue(thisRef: Any?, prop: KProperty<*>): String { // 2
return "$thisRef, '${prop.name}' 속성을 위임했습니다!"
}
operator fun setValue(thisRef: Any?, prop: KProperty<*>, value: String) { // 2
println("$thisRef 인스턴스에 있는 ${prop.name} 속성으로 지정된 값은 ${value}입니다.")
}
}
fun main() {
val e = Example()
println(e.p)
e.p = "NEW"
}
Delegate인스턴스의String타입 속성을 프로퍼티p로 위임했습니다. 위임할 객체는by키워드 뒤에 선언했습니다.- 위임 메서드를 구현했습니다. 항상 이런 파라미터 타입을 씁니다. 구현 자체는 필요한 나름의 방식으로 구현하면 됩니다. 만약 불변(immutable) 속성이라면
getValue메서드만 작성해도 됩니다.
표준 위임속성
코틀린 표준 라이브러리에는 lazy, observable 같은 유용한 기본 위임속성들이 있습니다. 예를 들어, lazy 속성은 지연 초기화에 사용합니다.
class LazySample {
init {
println("created!") // 1
}
val lazyStr: String by lazy {
println("계산!") // 2
"지연 값"
}
}
fun main() {
val sample = LazySample() // 1
println("lazyStr = ${sample.lazyStr}") // 2
println(" = ${sample.lazyStr}") // 3
}
- 인스턴스 생성 시
lazy속성은 아직 초기화되지 않은 상태입니다. - 처음
get()이 호출되면 람다식을 실행한 결과가 반환되고, 이 값이 보관됩니다. - 이후
get()호출은 최초 보관된 값을 그대로 반환합니다.
스레드 안전성을 고려한다면, blockingLazy()를 씁니다. 이는 해당 값이 딱 한 스레드에서만 계산되도록 보장하고, 나머지 스레드에서 이 값을 사용하게 해 줍니다.
맵에 속성 저장하기
위임 속성을 써서, 맵에 속성들을 저장할 수도 있습니다. JSON 파싱 작업 같이 동적(dynamic) 작업을 할 때 유용하게 씁니다.
class User(val map: Map<String, Any?>) {
val name: String by map // 1
val age: Int by map // 1
}
fun main() {
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
println("name = ${user.name}, age = ${user.age}")
}
map에 문자열 키를 위임해서, 속성 이름으로 쓰게 했습니다.
변경가능(mutable) 속성도 맵에 위임할 수 있습니다. 그런 위임속성에 값을 지정할 때, 맵의 값도 바뀌게 됩니다. 그러려면 읽기전용인 Map 타입 대신에 MutableMap을 써야 합니다.
이름지정 파라미터
코틀린에서도, 대부분의 프로그래밍 언어(자바, C++ 등)와 마찬가지로, 메서드나 생성자에 파라미터는 정의된 순서대로 전달합니다. 코틀린에는 이름지정 파라미터도 있는데, 이는 파라미터 이름을 명시함으로써 파라미터 순서로 인한 실수를 줄여줍니다. 똑같은 타입의 두 파라미터를 나란히 전달하는 경우에 순서를 거꾸로 전달하는 것 같은 실수는 컴파일러에게 발견되지도 않기 때문에 찾기 어려운 문제가 되기도 하는데, 이럴 때 이름지정 파라미터를 활용하면 실수를 줄일 수 있습니다.
fun format(userName: String, domain: String) = "$userName@$domain"
fun main() {
//sampleStart
println(format("mario", "example.com")) // 1
println(format("domain.com", "username")) // 2
println(format(userName = "foo", domain = "bar.com")) // 3
println(format(domain = "frog.com", userName = "pepe")) // 4
//sampleEnd
}
- 파라미터들을 전달해서 함수를 호출했습니다.
- 도메인과 이용자이름 파라미터를 거꾸로 전달했습니다.
- 이름지정 파라미터를 써서 호출했습니다.
- 이름지정 파라미터로 함수를 호출하면, 순서와 무관하게 올바른 값이 전달됩니다.
문자열 템플릿
문자열 템플릿은 문자열 안에 변수를 담아서 표현할 수 있게 해 줍니다. 실제 해당 문자열이 쓰이는 시점에 (예를 들어, println에 전달된다거나 할 때) 문자열 안에 있는 식들이 모두 평가되어 해당 값으로 치환됩니다.
fun main() {
//sampleStart
val greeting = "Kotliner"
println("Hello $greeting") // 1
println("Hello ${greeting.uppercase()}") // 2
//sampleEnd
}
- 변수를 담은 문자열을 프린트했습니다. 치환을 위한 변수 참조는
$기호로 시작합니다. - 식을 담은 문자열을 프린트했습니다. 식도
$로 시작하고 중괄호({})로 감싸서 표기합니다.
구조분해 Destructuring
구조분해 문법을 사용하면, 인스턴스의 멤버들에 접근할 때 아주 편리합니다. 인스턴스 이름을 생략하고도 원하는 속성을 쓸 수 있어서 여러 줄의 코드가 절약되기도 합니다.
fun findMinMax(list: List<Int>): Pair<Int, Int> {
// do the math
return Pair(50, 100)
}
fun main() {
//sampleStart
val (x, y, z) = arrayOf(5, 10, 15) // 1
val map = mapOf("손석구" to 40, "장도연" to 38)
for ((name, age) in map) { // 2
println("${name}님은 올해 ${age}세입니다.")
}
val (min, max) = findMinMax(listOf(100, 90, 50, 98, 76, 83)) // 3
//sampleEnd
}
Array를 구조분해 했습니다. 왼쪽의 변수 개수가, 우측의 값 개수와 일치합니다.- 맵도 구조분해할 수 있습니다.
name과age변수가 맵의 각 키-값 쌍에 대응됩니다. - 기본 자료 구조인
Pair와Triple타입에 대해서도 구조분해 문법을 쓸 수 있습니다.
data class User(val username: String, val email: String) // 1
fun getUser() = User("Mary", "mary@somewhere.com")
fun main() {
val user = getUser()
val (username, email) = user // 2
println(username == user.component1()) // 3
val (_, emailAddress) = getUser() // 4
}
- 데이터 클래스를 선언했습니다.
- 데이터 클래스 인스턴스를 구조분해 했습니다. 인스턴스 필드의 값들을 가져왔습니다.
- 데이터 클래스는
component1()나component1()같은 구조분해에 필요한 메서드들 자동으로 만들어줍니다. - 구조분해 후 사용하지 않는 값은, 밑줄(
_)로 표시해서 컴파일러에게 사용하지 않고 무시한다고 알려줄 수 있습니다.
class Pair<K, V>(val first: K, val second: V) { // 1
operator fun component1(): K {
return first
}
operator fun component2(): V {
return second
}
}
fun main() {
val (num, name) = Pair(1, "one") // 2
println("num = $num, name = $name")
}
Pair클래스를component1()와component2()메서드를 써서 직접 정의해 봤습니다.- 기본으로 있는
Pair와 똑같은 방식으로 구조분해할 수 있습니다.
스마트캐스트
코틀린 컴파일러는 대부분의 경우, 필요에 맞게 알아서, 자동으로 타입을 전환해 줍니다.
- null-가능 타입을 null-불가 타입으로 캐스트(타입전환)합니다.
- 상위 타입에서 하위 타입으로 캐스트 합니다.
import java.time.LocalDate
import java.time.chrono.ChronoLocalDate
fun main() {
//sampleStart
val date: ChronoLocalDate? = LocalDate.now() // 1
if (date != null) {
println(date.isLeapYear) // 2
}
if (date != null && date.isLeapYear) { // 3
println("윤년입니다!")
}
if (date == null || !date.isLeapYear) { // 4
println("올해에는 2월 29일이 없습니다...")
}
if (date is LocalDate) {
val month = date.monthValue // 5
println(month)
}
//sampleEnd
}
- null-가능 변수를 선언했습니다.
- null-불가 타입으로 스마트캐스트 되었습니다. 따라서
isLeapYear를 바로 쓸 수 있습니다. - 조건 안에서 스마트캐스트를 했습니다. (코틀린도 자바와 마찬가지로 short-circuit방식을 쓰기 때문에 가능합니다)
- 조건 안에서 스마트캐스트 했습니다. (마찬가지입니다)
- 하위 타입인
LocalDate로 캐스트되었습니다.
이런 방식으로, 뻔한 타입 변환을 명시하지 않고도 자동으로 변환해 쓸 수 있습니다.
끝으로
이제까지, 예제를 중심으로 코틀린 문법의 좋은 기능들을 파헤쳐 봤습니다. 장황한 설명은 과감히 생략하고 실제 동작하는 코드를 중심으로 실습해 가며 배울 수 있었습니다. 코틀린은 자바와의 상호운영성이 아주 빼어난 언어인 데다, 현대적 편의 기능이 잘 준비돼 있어서 유용합니다.
안드로이드 네이티브 앱 개발은 물론이고, 자바 중심의 백엔드 개발에도 훌륭히 쓸 수 있는 언어이고, 멀티플랫폼 언어로 나아가려는 의욕이 엿보이는 만큼, 앞으로의 기대가 더 큽니다.
마무리까지 함께 해주셔서 감사합니다.
온라인 강의 (유료)
이 책은, 온라인 강의로도 준비했습니다. 강의는 유료로 판매합니다만, 이 온라인 학습자료는 계속 무료로 제공하며, 자유롭게 활용하실 수 있습니다. 온라인 강의로 편하게 학습하실 분께서는 아래 주소에서 강의를 구매하실 수 있습니다.
감사 표시
- Kotlin by example
- Kotlin Playground 라이브러리
- Elm - 프런트엔드 함수형 프로그래밍 언어
- mdBook - 마크다운으로 온라인 책 만드는 툴
- Pico.css - Minimal CSS Framework
- Halcyon VSCode 색상 테마
감사합니다.