
Chapter 01 - 코틀린이란 무엇이며, 왜 필요한가
대상 플랫폼 = 자바가 실행되는 모든 곳
- 자바가 사용되는 모든 용도에 적합하면서 더 간결하고 생산적이며 대체 가능한 언어를 만드는 것이 주목적
- 일반적으로는 Backend 서버 상의 코드와, 안드로이드 디바이스에서 실행되는 모바일 애플리케이션에서 사용됨
정적 타입 지정 언어
- 자바와 마찬가지로 코틀린도 정적 타입(statically typed) 지정 언어
- 정적 타입 지정 : 모든 프로그램 구성 요소의 타입을 컴파일 시점에 알 수 있고,
프로그램 안에서 객체의 필드나 메소드를 사용할 때마다 컴파일러가 타입을 검증해주는 것 - 코틀린의 타입 추론(type inference) : 대부분의 경우 프로그래머가 변수 타입을 명시하지 않아도 컴파일러가 유추
- 정적 타입 지정의 장점은...
성능 : 실행 시점에 어떤 메소드를 호출할지 알아내는 과정이 필요없으므로 메소드 호출이 빠름
신뢰성 : 컴파일러가 프로그램의 정확성을 검증해주기 때문에 실행 시 오류로 인한 중단 가능성이 적어짐
유지 보수성 : 객체가 어떤 타입에 속하는지 알 수 있기 때문에 처음 보는 코드를 다룰 때에도 용이
도구 지원 : 안전하게 리팩토링할 수 있으며, 더 정확한 코드 자동 완성 기능을 제공할 수 있음
❖ 참고 - 동적 타입(dynamically typed) 지정 언어
- JVM 언어 중에는 Groovy와 JRuby가 대표적
- 타입과 관계 없이 모든 값을 변수에 넣을 수 있고, 메소드나 필드 접근에 대한 검증이 실행 시점에 일어나며,
이에 따라 코드가 더욱 짧아지고 데이터 구조를 더 유연하게 생성 및 사용 가능
함수형 프로그래밍과 객체지향 프로그래밍
함수형 프로그래밍의 핵심 개념
- first-class 함수
- 함수를 일반 값처럼 다룰 수 있음
- 함수를 변수에 저장하거나, 인자로 다른 함수에 전달하거나, 함수에 새로운 함수를 만들어서 반환할 수 있음
- immutability
- 일단 만들어지고 나면 내부 상태가 절대로 바뀌지 않는 불변 객체를 사용해서 프로그래밍하게 됨
- side effect 없음
- 입력이 같다면 항상 같은 출력
- 다른 객체의 상태 변경 X
- 외부와 상호작용하지 않는 pure function
함수형 프로그래밍의 장점
- 간결성
- 명령형(imperative) 코드에 비해서 더욱 간결하며 우아함
- 함수를 값처럼 사용함으로써 더 강력한 추상화가 가능하고, 이를 통해 코드 중복을 덜어낼 수 있음
- safe multithreading
- 다중 스레드 프로그램 작성 시 적절한 동기화 없이 동일 데이터를
여러 스레드가 변경하는 경우에 가장 빈번하게 문제가 발생함 - 불변 데이터 구조에 순수 함수를 적용하면 동일 데이터를 여러 스레드가 변경할 수 없음
- 다중 스레드 프로그램 작성 시 적절한 동기화 없이 동일 데이터를
- 테스트하기 쉬움
- side effect가 없기 때문에 setup code도 필요 없고, 순수 함수는 독립적으로 테스트 가능
코틀린과 함수형 프로그래밍
- 사실 일반적으로는 언어와 관계 없이 함수형 스타일을 사용할 수 있음
- 하지만 모든 언어가 함수형 프로그래밍을 편하게 사용하도록 충분한 라이브러리 및 문법 지원을 해주지 않음
- 코틀린에서는 함수형 프로그래밍을 위해...
함수 타입 지원 : 함수가 다른 함수를 파라미터로 받거나 함수가 새로운 함수 반환 가능
람다 식 지원 : 번거로운 준비 코드 없이 코드 블록을 쉽게 정의하고 전달할 수 있음
data class : 불변적인 값 객체를 간편하게 만들 수 있는 구문 제공
표준 라이브러리 : 객체와 컬렉션을 함수형 스타일로 다룰 수 있는 API 제공
Chapter 02 - 코틀린 기초
함수
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}
우선 식(expression)과 문(state)의 차이를 알아야 함!
- 식 : 값을 만들어내며 다른 식의 하위 요소로 계산에 참여 가능
- 문 : 자신을 둘러싸고 있는 가장 안쪽 블록의 최상위 요소로 존재하며 값을 만들지 못함
- 자바에서는 모든 제어 구조가 문인 반면, 코틀린에서는 루프를 제외한 대부분의 제어 구조가 식
- 즉, 코틀린의 if는 값을 만들어내지 못하는 문이 아니고, 결과를 만들어내는 식!
- 반면에 대입문은 자바에서 식이지만 코틀린에서는 문
식이 본문인 함수
fun max(a: Int, b: Int) = if (a > b) a else b
- 본문이 중괄호로 둘러싸인 함수를 블록이 본문인 함수라 부르고,
등호와 식으로 이뤄진 함수를 식이 본문인 함수라 부름 - 식이 본문인 함수에 한해서만 위와 같이 타입 추론을 통해서 반환 타입을 생략할 수 있음
- IntelliJ IDEA는 Convert to expression body와 Convert to block body 기능 제공
변수
val
- value에서 따옴
- immutable 참조
- 초기화 이후 재대입 불가능
- 자바의 final 변수와 같음
var
- variable에서 따옴
- mutable 참조
- 변수의 값 변경 가능
❖ val 변수를 초기화할 때 오직 한 문장만이 초기화에 적용된다는 것을 컴파일러가 확인할 수 있다면,
조건에 따라 다르게 변수를 초기화할 수 있음
val message: String
if (canPerformOperation()) {
message = "Success"
} else {
message = "Failed"
}
❖ val 변수를 활용해서 참조 타입의 객체를 저장했다면 참조가 가리키는 내부의 객체는 변경 가능
❖ var 키워드를 사용하더라도 객체의 타입까지 자유분방하게 바꿀 수 있는 것은 아님
문자열 템플릿
fun main(args: Array<String>) {
println("Hello, ${if (args.size > 0) args[0] else "someone"}!")
}
- $ 를 사용해서 변수를 문자열 안에서 사용
- 자바의 StringBuilder를 사용하기 때문에 내부에서 효율적으로 연산
- 존재하지 않는 변수 사용 시 컴파일 에러 발생
프로퍼티
- 코틀린에서는 언어 기본 기능으로 제공하며, 자바의 필드 및 접근자 메소드를 완전히 대신할 수 있음
- property 선언은 변수 선언과 마찬가지로 val 혹은 var 사용
- val은 getter 제공, var는 getter 및 setter 제공
- person.getName() 대신 person.name 방식으로 호출 가능
- setter 또한 person.name = Loko 방식으로 설정 가능
커스텀 접근자
class Rectangle(val height: Int, val width: Int) {
val isSquare: Boolean
get() {
return height == width
}
}
- 식이 본문인 함수로 변형하면, get() = height == width
enum
enum class Color(
val r: Int, val g: Int, val b: Int
) {
RED(255, 0, 0), ORANGE(255, 165, 0),
YELLOW(255, 255, 0), GREEN(0, 255, 0), BLUE(0, 0, 255),
INDIGO(75, 0, 130), VIOLET(238, 130, 238);
fun rgb() = (r * 256 + g) * 256 + b
}
- 자바에선 enum 으로 선언하지만 코틀린에서는 soft keyword이기 때문에 enum class 라고 정의해야 함
- () 안에서 상수의 프로퍼티를 정의하고 {} 안에서 프로퍼티 값 지정
- 상수 목록과 메소드 사이에 반드시 세미 콜론(;)이 있어야 함
when
fun getWarmth(color: Color) = when(color) {
Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
Color.GREEN -> "neutral"
Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
else -> throw Exception("other")
}
- 자바의 switch 에 해당하는 코틀린의 when
- 코틀린의 if 와 마찬가지로 값을 만들어내는 식
- 자바와 다르게 각 분기 끝에 break 를 넣지 않아도 됨
is
- 변수의 타입을 검사해주는 기능
- 자바의 instanceof 와도 비슷하긴 하지만 is 는 추가적으로 검사 이후에 자동 캐스팅 (smart cast)
- 만약 원하는 타입으로 명시적인 타입 캐스팅을 하려면 as 를 사용해야 함
for
- 일단 while과 do-while은 자바의 것과 완벽히 동일
fun fizzBuzz(i: Int) = when {
i % 15 == 0 -> "FizzBuzz"
i % 3 == 0 -> "Fizz"
i % 5 == 0 -> "Buzz"
else -> "$i"
}
for (i in 1..100) {
println(fizzBuzz(i))
}
- 자바처럼 초기값, 증가값, 최종값을 사용하는 대신 range를 사용
- range 표현 방식 : 1..100
- .. 을 사용하는 범위 표현은 마지막 값을 포함
- 마지막 값을 포함하지 않고자 한다면 .. 대신 until 을 사용할 것
for (i in 100 downTo 1 step 2) {
println(fizzBuzz(i))
}
- step 은 증가값, downTo 는 역방향 순회
Map 순회
val binaryReps = TreeMap<Char, String>()
for (c in 'A'..'F') {
val binary = Integer.toBinaryString(c.toInt())
binaryReps[c] = binary
}
for ((letter, binary) in binaryReps) {
println("$letter = $binary")
}
- 위 루프에서 key와 value를 letter, binary 에 대입했음
- get이나 put 없이 조회 및 삽입했음을 알 수 있음
List 순회
val list = arrayListOf("10", "11", "1001")
for ((index, element) in list.withIndex()) {
println("$index: $element")
}
예외 처리
val percentage =
if (number in 0..100)
number
else
throw IllegalArgumentException("$percentage is unvalid")
- 다른 언어와 마찬가지로 throw 한 예외는 catch 가 있을 때까지 rethrow
- 예외 인스턴스에 new 를 붙일 필요가 없음
- throw 가 식이므로 다른 식에 포함될 수 있음
fun readNumber(reader: BufferedReader): Int? {
try {
val line = reader.readLine()
return Integer.parseInt(line)
} catch(e: NumberFormatException) {
return null
} finally {
reader.close()
}
}
- 자바와 다르게 throws 절이 코드 상에 없음
- 자바는 checked exception 처리를 강제하지만 코틀린에서는 굳이 처리하지 않아도 됨
fun readNumber(reader: BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine())
} catch(e: NumberFormatException) {
null
}
println(number)
}
- 코틀린의 try 는 if 나 when 과 마찬가지로 식
- 그러므로 try 의 값을 변수에 대입 가능
- 그런데 if 와는 다르게 본문을 반드시 {} 로 둘러싸야 함
- 본문 내부에 여러 식이 있다면 마지막 식의 값이 전체 결과 값
- 마찬가지로 catch 블록도 그 안의 마지막 식이 블록 전체의 값
Chapter 03 - 함수 정의와 호출
코틀린에서 컬렉션 만들기
val set = hashSetOf(1, 7, 53)
val list = arrayListOf(1, 7, 53)
val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
확장 함수 (extension function)
package strings
fun String.lastChar(): Char = this.get(this.length - 1)
- 기존 자바 API를 재작성하지 않고 확장할 수 있도록 코틀린에서 지원해주는 기능
- 확장할 클래스 이름 뒤에 추가하려는 함수 이름을 적고나서 일반 함수처럼 작성하면 그만
- 여기서 앞의 확장할 클래스를 receiver type 이라 부르고, this 부분은 receiver object 라 부름
- 위 예제는 어떤 면에서 String 클래스에 새로운 메소드를 추가하는 것과 같음
- private 멤버나 protected 멤버를 사용할 수 있기 때문에 캡슐화를 깨지 않음
import strings.lastChar
val c = "Kotlin".lastChar()
- 단, 확장 함수를 사용하려면 위처럼 확장함수도 같이 import를 해줘야 함
- 와일드카드(*)를 사용해서 import할 경우에는 명시할 필요 없음
import strings.lastChar as last
val c = "Kotlin".last()
- 위와 같이 as 를 사용해서 별명으로 부를 수도 있음
❖ 확장 함수는 클래스의 일부가 아니며 클래스 밖에 선언된 것이므로 정적 타입에 의존하게 되며,
그렇기 때문에 오버라이드할 수가 없음
확장 프로퍼티
val String.lastChar: Char
get() = get(length - 1)
set(value: Char) {
this.setCharAt(length - 1, value)
}
- 기존 클래스 객체에 대해 프로퍼티 형식으로 사용할 API 추가 가능
- 상태를 저장할 방법이 없기 때문에 아무런 상태도 가질 수 없음
- 기본 getter 구현이 제공되지 않으므로 꼭 정의해야 하며, 초기화 코드도 사용할 수 없음
vararg와 가변 인자 함수
fun listOf<T>(vararg values: T): List<T> { ・・・ }
- vararg 인자는 메소드 호출 시 원하는 개수만큼 값을 넘기면 컴파일러가 배열에 그 값들을 넣어줌
- 자바에서는 타입 뒤에 ... 을 사용하지만 코틀린에서는 타입 앞에 vararg 사용
fun main(args: Array<String>) {
val list = listOf("args: ", *args)
println(list)
}
- spread 연산자도 사용 가능하지만 배열 변수 앞에 * 을 붙여도 됨
정규표현식
fun parsePath(path: String) {
val directory = path.substringBeforeLast("/")
val fullName = path.substringAfterLast("/")
val fileName = fullName.substringBeforeLast(".")
val extension = fullName.substringAfterLast(".")
println("Dir: $directory, name: $fileName, ext: $extension")
}
- String 확장 함수를 활용하는 방법
fun parseParh(path: String) {
val regex = """(.+)/(.+)\.(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if (matchResult != null) {
val (directory, fileName, extension) = matchResult.destructured
println("Dir: $directory, name: $fileName, ext: $extension")
}
}
- 정규식을 활용하는 방법
- 3중 따옴표 문자열에서는 \ 를 포함한 어떤 문자도 이스케이프할 필요가 없음
- 위 표현식은 / 와 . 을 기준으로 문자열을 3 그룹으로 분리
- 패턴 . 은 임의의 문자와 매치
- 그러므로 (.+) 는 구간 내에 모든 문자라는 뜻
Chapter 04 - 클래스, 객체, 인터페이스
인터페이스
interface Clickable {
fun click()
fun showOff() = println("I'm clickable!")
}
class Button: Clickable {
override fun click() = println("I was clicked")
}
- 자바의 extends 나 implements 대신 콜론(:) 으로 클래스든 인터페이스든 모두 처리
- 함수 앞의 override 변경자는 말그대로 상속받았다는 뜻이며, 자바와 달리 반드시 명시해야만 함
- 디폴트 구현도 제공하며, 자바처럼 default 키워드를 붙일 필요 없음
open, final, abstract 변경자
변경자 | override | 설명 |
final | override 불가 | 클래스 멤버의 기본 변경자 |
open | override 가능 | 반드시 명시해야 override가 가능해짐 |
abstract | 반드시 override해야 함 | 추상 클래스의 멤버에만 붙을 수 있으며, 구현이 있어선 안됨 |
override | override하고 있는 함수 | 하위로 다시 override하는 것이 가능 이를 방지하고자 한다면 final 명시 |
- 코틀린에서 클래스와 메소드는 기본적으로 final 이기 때문에
상속을 허용하고 싶다면 open 변경자를 붙여야 함 - 참고로 인터페이스는 항상 열려 있기 때문에 상속 제어 변경자를 사용하지 않음
가시성 변경자
변경자 | 클래스 멤버 | 최상위 선언 |
public | 모든 곳에서 볼 수 있음 | 모든 곳에서 볼 수 있음 |
internal | 같은 모듈 안에서만 볼 수 있음 | 같은 모듈 안에서만 볼 수 있음 |
protected | 하위 클래스 안에서만 볼 수 있음 | 최상위 선언 불가 |
private | 같은 클래스 안에서만 볼 수 있음 | 같은 파일 안에서만 볼 수 있음 |
- 아무 변경자도 없는 경우 디폴트로 public
- 자바의 기본 가시성인 package-private 은 없음
- 대신 internal 이라는 변경자를 사용하며, 이는 module 단위로 가시성을 관리
- 최상위 선언에 대해 private 허용
inner class
class Outer {
inner class Inner {
fun getOuterReference(): Outer = this@Outer
}
}
- 자바의 경우 중첩 클래스가 static 이어야만 직렬화가 가능한데, 코틀린에서 중첩 클래스는 기본적으로 static
- 외부에 static 을 안 써도 되는 대신 내부에는 inner 변경자를 붙여야 함
sealed class
sealed class Expr {
class Num(val value: Int): Expr()
class Sum(val left: Expr, val right: Expr): Expr()
}
fun eval(e: Expr): Int =
when(e) {
is Expr.Num -> e.value
is Expr.Sum -> eval(e.left) + eval(e.right)
}
- when 은 디폴트 분기 else 를 강제하는데, sealed 클래스의 하위 클래스를 활용하면 없어도 됨
- sealed 클래스는 모두 open
primary constructor
class User(val nickname: String, val isSubscribed: Boolean = true)
- 클래스 이름 뒤 () 부분이 primary constructor
class TwitterUser(nickname: String): User(nickname) { ・・・ }
- 상속 받은 클래스는 빈 생성자 () 라도 붙여야 하고 인터페이스는 필요 없기 때문에
이를 통해 클래스를 상속받았는지, 인터페이스를 상속받았는지 구분 가능
secondary constructor
class View {
constructor(ctx: Context) {}
constructor(ctx: Context, attr: AttributeSet) {}
}
- 일반적으로 코틀린에서 자바처럼 여러 개의 생성자를 갖지 않음
- 그래도 constructor 키워드를 통해 부 생성자를 정의하도록 도와줌
data class
data class Client(val name: String, val postalCode: Int)
- 컴파일러가 필요한 메소드들을 자동으로 만들어주는 클래스
- 주 생성자에 나열된 모든 프로퍼티를 기반으로 자바에서 요구하는 모든 메소드를 자동 생성
object 키워드
object Payroll {
val allEmployees = arrayListOf<Person>()
fun calculateSalary() {
for (person in allEmployees) { ・・・ }
}
}
- object 는 코틀린에서 싱글턴을 정의하는 방법 중 하나
- 클래스와 마찬가지로 프로퍼티, 메소드, 초기화 블록 등이 들어갈 수 있지만 생성자는 사용 불가
Payroll.allEmployees.add(Person(...))
Payroll.calculateSalary()
- 체이닝 방식으로 간단하게 객체 사용 가능
- 싱글톤 패턴과 마찬가지로, 대규모 시스템에서 적합하지 못함
- 그래서 자바와 마찬가지로 의존관계 주입 프레임워크를 사용하기도 함
Chapter 05 - 람다로 프로그래밍
멤버 참조
val getAge = Person::age
- 자바와 마찬가지로, 함수를 값으로 저장할 수 있음
- :: 를 사용하는 식을 member reference 라고 부름
- 참조 대상이 함수인지 프로퍼티인지와는 관계 없이 뒤에 괄호를 넣을 수 없음
❖ 멤버 참조와 람다는 같은 타입이므로 자유롭게 바꿔서 사용 가능
people.maxBy(Person::age)
people.maxBy { p -> p.age }
people.maxBy { it.age }
컬렉션 함수형 API
- filter : 컬렉션에서 원치 않는 원소 제거
- map : 컬렉션의 원소들 변환
- all & any : 모든 원소가 어떤 조건을 만족하는지 여부 반환
- count : 조건을 만족하는 원소의 개수 반환
- find : 조건을 만족하는 첫 번째 원소 반환
- groupBy : 컬렉션의 모든 원소를 특성에 따라 여러 그룹으로 나눠서 반환
- flatMap : 인자로 주어진 람다를 컬렉션의 모든 객체에 적용해서 얻어지는 여러 리스트를 한 리스트로 모아서 반환
- flatten : 중첩된 리스트를 특별한 변환 과정 없이 펼쳐서 반환
lazy 컬렉션 연산
- 컬렉션 함수형 API들은 결과 컬렉션을 즉시(eagerly) 생성
- 만일 컬렉션 함수를 연쇄로 사용하면 중간 결과를 새로운 컬렉션에 임시로 담게 된다는 뜻
- 이런 때에 sequence 를 사용하면 중간 임시 컬렉션 없이 컬렉션 함수 연쇄 가능
people.asSequence()
.map(Person::name)
.filter { it.startsWith("A") }
.toList()
코틀린 지연 계산 시퀀스는 Sequence 인터페이스에서 시작
- 인터페이스는 단지 한 번에 하나씩 열거될 수 있는 원소의 시퀀스를 표현할 뿐
- 시퀀스의 원소는 필요할 때 비로소 계산되므로, 중간 결과를 저장하지 않음
- asSequence 확장 함수를 통해 어떤 컬렉션이든 시퀀스로 변경 가능
- 시퀀스를 리스트로 만들 때는 toList 사용
수신 객체 지정 람다 with & apply
- 코틀린 람다의 독특한 기능
- 수신 객체를 명시하지 않고 람다의 본문 안에서 다른 객체의 메소드 호출
- 이러한 람다를 lambda with receiver 라고 부름
with
fun alphabet() = with(StringBuilder()) {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
toString()
}
- with의 파라미터는 수신 객체와 람다 2개
- 일반 함수에 확장 함수가 있듯이, 일반 람다에 수신 객체 지정 람다가 있다고 생각하면 편함
- 위 예제에서는 StringBuilder 인스턴스를 만들고 즉시 with 으로 넘겼음
- with이 반환하는 값은 람다 코드를 실행한 결과
apply
fun alphabet() = SringBuilder().apply {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}.toString()
- 거의 with 과 같지만 항상 자신에게 전달된 객체를 반환한다는 점이 다름
- 위 코드에서도 apply 의 결과는 StringBuilder 객체
- 객체의 인스턴스를 만들면서 즉시 프로퍼티 중 일부를 초기화해야 하는 경우에 유용
- 자바에서는 보통 이럴 때 Builder 를 사용하지만 코틀린은 그냥 apply 만 사용해도 됨
fun createViewWithCustomerAttributes(ctx: Context) =
TextView(ctx).apply {
text = "Sample Text"
textSize = 20.0
setPadding(10, 0, 0, 0)
}
❖ 사실 이 예제는 표준 라이브러리 함수 buildString 으로 간단하게 해결 가능
fun alphabet() = buildString {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}
- buildString 은 StringBuilder 를 활용해서 String 을 만드는 경우 사용할 수 있는 우아한 해법
❖ 수신 객체 지정 람다는 Domain Specific Language 를 다룰 때 더욱 흥미진진한 예제들을 볼 수 있음!
Chapter 06 - 코틀린 타입 시스템
? nullable 타입
- 변수 뒤에 ? 가 붙어 있으면 null 값이 들어오는 것이 허용됨
- nullable 타입 사용 시 메소드 호출에 제약이 생김
fun strLenSafe(s: String?): Int =
if (s != null) s.length else 0
?. 안전한 호출 연산자
- null 검사와 메소드 호출을 한 번의 연산으로 수행
- 안전한 호출의 결과 또한 nullable type이어야만 함
- 메소드 뿐만 아니라 프로퍼티를 가져올 때도 사용 가능하고 연쇄 사용도 가능
val result = s?.toUppercase
val country = person.company?.address?.country
?: elvis 연산자
- null 값임이 확인되었을 때 null 대신 사용할 디폴트 값을 지정할 때 사용
- 이름이 elvis인 이유는 시계 방향으로 90도 돌렸을 때 Elvis Presley 특유의 헤어스타일과 눈이 보이기 때문
- 더 심각한 이름을 좋아하는 사람을 위해 null coalescing 연산자라는 이름도 있음
fun Person.countryName() = company?.address?.country ?: "Unknown"
- return 이나 throw 등의 연산도 식이므로, elvis 연산자의 우항에 넣을 수 있음
val address = person.company?.address ?: throw IllegalArgumentException("No address")
as? 안전한 캐스트
- 지정 타입으로 캐스트하되, 변환이 불가능하면 null 반환
- 안전한 캐스트를 사용하는 일반적인 패턴은 캐스트 수행 뒤 elvis 연산자를 사용하는 것
val otherPerson = o as? Person ?: return false
!! not-null assertion
- nullable 타입을 다루는 도구 중에서 가장 단순하면서 무딘 도구
- 어떤 값이든 null이 될 수 없는 타입으로 강제로 바꿈
- 만약 실제 null 값에 대해 !! 를 적용하면 NPE 발생
- !! 는 컴파일러에게 "나는 이 값이 null이 아님을 알고 있으며, 예외가 발생해도 감수하겠다" 고 말하는 꼴
- 만약 1번 함수에서 null이 아님을 검증받았다면 2번 함수에서 !! 를 사용하는 것은 합당함
❖ UI 프레임워크에서의 !! 사용 예제
class CopyRowAction(val listL JList<String>): AbstractAction() {
override fun isEnabled(): Boolean = list.selectedValue ! = null
override fun actionPerformed(e: ActionEvent) {
val value = list.selectedValue!!
/* copy value to clipboard */
}
}
❖ 예외를 파악하기 힘든 !! 의 안 좋은 예제
person.company!!.address!!.country
let
- null이 될 수 있는 식을 더욱 쉽게 다룰 수 있게 해주는 함수
- nullable 타입을 non-nullable 타입으로 바꿔서 람다에 전달
- 가장 흔한 용례는 nullable 타입을 받아서 null이 아닌 값만 인자로 넘기는 방식
getTheBestPersonInTheWorld()?.let { sendEmailTo(it.email) }
- let을 지나치게 중첩해서 사용하면 가독성이 떨어지므로 if 문이 나은 케이스도 있음
lateinit
- 프로퍼티를 나중에 초기화할 수 있도록 해줌
- lateinit 변수는 항상 var 이어야만 함 (val 은 final 이기 때문)
- lateinit 을 사용하면 nullable 타입이더라도 생성자 안에서 초기화할 필요가 없어짐
- 초기화를 하지 않아서 에러가 발생하더라도 NPE가 아닌 lateinit 관련 에러 발생
- lateinit 은 DI 프레임워크와 함께 사용되는 경우가 많음
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())
}
}
Int, Boolean, Any 등
- 코틀린은 원시 타입과 래퍼 타입을 구분하지 않음
- Int 와 같은 타입은 대부분의 경우 자바의 int 로 컴파일
- 원시 타입으로 사용될 경우에는 non-nullable임에 유의할 것
- 컬렉션에 담는 경우에는 Integer 와 같은 래퍼 타입이 됨
toByte(), toShort(), toChar() 등의 변환 함수
- 코틀린에서는 다른 타입의 숫자 타입으로 자동 변환할 수 없음
- 대신 Boolean 을 제외한 모든 원시 타입에 대해 변환 함수 제공
- 코틀린에서는 반드시 타입을 명시적으로 변환해야 함
Any, Any? 최상위 타입
- 자바에서는 Object 가 클래스 계층의 최상위 타입이듯 코틀린에서는 Any 가 모든 non-nullable 타입의 조상
- 자바는 참조 타입들의 조상이지만 코틀린은 원시 타입도 포함해서 조상
Unit 코틀린의 void
- 대부분의 경우 Unit 과 void 는 차이가 없으므로 주로 void 로 컴파일됨
- Unit 과 void 의 차이점
- Unit 은 모든 기능을 갖는 일반적인 타입
- 그러므로 Unit 을 타입 인자로 사용할 수 있음
- Unit 타입에 속한 값은 하나뿐이며, 그 이름도 Unit
- Unit 타입의 함수는 Unit 값을 묵시적으로 반환
- 그러므로 제네릭 파라미터를 반환하는 함수를 override하면서 반환 타입을 Unit으로 사용할 때 유용
interface Processor<T> {
fun process(): T
}
class NoResultProcessor: Processor<Unit> {
override fun process { ・・・ }
}
- 위 코드에서 process() 는 어떤 값을 반환하도록 요구
- Unit 타입도 Unit 값을 제공하기 때문에 값 반환에 문제 없음
- 심지어 override 함수 내에서 Unit 을 명시적으로 반환할 필요도 없음
Nothing 결코 정상적으로 끝나지 않는 함수
- 반환 값이 의미 없는 함수를 만들게 되는 경우가 있음
- 예를 들어 테스트 라이브러리들은 fail 함수를 제공해서 메시지와 함께 테스트를 실패시킬 수 있게 함
- 이러한 함수를 호출하는 코드를 분석할 때, 함수가 정상적으로 끝나지 않는다는 것을 알면 유용
- 이러한 경우를 표현하기 위해 Nothing 이라는 특별한 반환 타입 제공
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
- Nothing 타입은 아무 값도 포함하지 않음
- 따라서 반환 타입에 쓰일 파라미터로만 사용 가능
- Nothing 을 반환하는 함수를 elvis 연산자의 우항에 사용해서 precondition을 검사할 수 있음
val address = company.address ?: fail("No address")
- 위 예제에서 컴파일러는 address 변수가 non-nullable임을 추론함
읽기 전용 컬렉션과 변경 가능 컬렉션
- 코틀린에서는 컬렉션 안의 데이터에 접근하는 인터페이스와 변경하는 인터페이스를 분리했음
- 이 구분은 kotlin.collections 패키지에서부터 시작
- kotlin.collections.Collection
- kotlin.collections.MutableCollection
- Collection 의 메소드 : size, iterator(), contains() 등
- MutableCollection 의 메소드 : add(), remove(), clear() 등
- val 과 var 를 구분하는 것과 마찬가지로, 이렇게 하는 이유는 프로그램의 동작을 예측하기 위함
IntArray, ByteArray, CharArray, BooleanArray 등의 원시 타입 배열
- Array<Int> 와 같이 사용하면 참조 타입을 사용하게 되므로 원시 타입으로 만들고자 한다면 위 타입들을 사용해야 함
- 이들은 자바의 int[], byte[], char[] 등으로 컴파일됨
val fiveZeros = IntArray(5)
val fiveZerosToo = intArrayOf(0, 0, 0, 0, 0)
val square = IntArray(5) { i -> (i+1) * (i+1) }