📖 2.1 기본 요소: 함수와 변수
🔖 2.1.1 첫 번째 코틀린 프로그램 작성: Hello, World!
fun main() {
println("Hello World!")
}
- 함수를 모든 코틀린 파일의 최상위 수준에 정의 가능
- main에 인자가 없어도 된다.
- 간결성 강조
- 세미콜론(;)을 붙이지 않는 것을 더 권장한다.
🔖 2.1.2 파라미터와 반환값이 있는 함수 선언
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}
- 파라미터 이름이 먼저 오고 그 뒤에 타입 지정
- 콜론(:)으로 구분
- 함수의 반환 타입은 파라미터 목록을 닫는 괄호 다음에 옴.
- 위와 같은 코드를 블록 본문 함수(block body function)이라고 부름.
🔖 2.1.3 식 본문을 사용해 함수를 더 간결하게 정의
fun max(a: Int, b: Int): Int = if (a > b) a else b
- 위와 같은 함수를 본문 함수(expression body function)이라고 부름.
fun max(a: Int, b: Int) = if (a > b) a else b
- 반환 타입이 없지만 컴파일러가 타입 추론(inference)하여 반환 타입을 지정
- 이 경우, 식 본문 함수만 반환 타입이 생략 가능
🔖 2.1.4 데이터를 저장하기 위해 변수 선언
val question: String = "삶, 우주, 그리고 모든 것에 대한 궁극적인 질문"
val answer: Int = 42
- 변수 선언은 val or var로 가능하다.
val question = "삶, 우주, 그리고 모든 것에 대한 궁극적인 질문"
val answer = 42
- 타입 선언 생략 가능
🔖 2.1.5 변수를 읽기 전용 변수나 재대입 가능 변수로 표시
val(value)
- 읽기 전용 참조(read-only reference)를 선언
- 단 한 번만 대입 가능
- 코틀린에서는 모든 변수를 val 키워드를 사용해 선언하는 방식을 지켜야 함.
var(variable)
- 재대입 가능한 참조(reassignable reference)를 선언
- 초기화 이후, 다른 값 대입 가능
- 반드시 필요할 때에만 var로 변경
fun main() {
val result: String
if (canPerformOperation()) {
result = "Success"
}
else {
result = "Can't perform operation"
}
}
- val 참조가 가리키는 객체의 내부 값은 변경될 수 있음.
fun main() {
var answer = 42
answer = "no answer"
}
- var는 변수의 값은 변경할 수 있지만 변수의 타입은 고정된다.
🔖 2.1.6 더 쉽게 문자열 형식 지정: 문자열 템플릿
fun main() {
val input = readln()
val name = if (input.isNotBlank()) input else "Kotlin"
println("Hello, $name!")
}
- 코틀린 또한 변수 이름 앞에 $를 덧붙이면 변수를 문자열 안에 참조할 수 있다.
- $ 문자를 문자열에 넣고 싶으면 \를 사용해 escape 시키면 됨.
fun main() {
val name = readln()
println("Hello, ${if (name.isBlank()) "someone" else name}!")
}
- 중괄호로 둘러쌓인 식 안에서 여전히 큰따옴표를 사용할 수 있다.
📖 2.2 행동과 데이터 캡슐화: 클래스와 프로퍼티
class Person(val name: String)
- java의 getter와 생성자가 한줄로 요약된다.
- public visibility modifier가 사라진다.
- 코틀린의 기본 가시성은 public이다.
🔖 2.2.1 클래스와 데이터를 연관시키고, 접근 가능하게 만들기: 프로퍼티
- 클래스라는 개념은 데이터를 캡슐화하고 캡슐화한 데이터를 다루는 코드를 한 주체 안에 가두는 것이다.
- 자바에서는 필드와 접근자를 한데 묶어 property라고 부른다.
- 코틀린 property는 자바의 필드와 접근자 메서드를 완전히 대신한다.
class Person(
val name: String,
var isStudent: Boolean
)
- val, var를 같이 선언할 수 있다.
- val는 getter를 만들어내고, var는 getter와 setter를 만들어낸다.
fun main() {
val person = Person("Bob", true)
println(person.name)
println(person.isStudent)
person.isStudent = false
println(person.isStudent)
}
- new 키워드 사용하지 않고 생성자 호출 가능
- 프로퍼티 이름을 직접 사용해도 getter, setter 호출 가능
🔖 2.2.2 프로퍼티 값을 저장하지 않고 계산: 커스텀 접근자
class Rectangle(var height: Int, var width: Int) {
val isSquare: Boolean
get() {
return height == width
}
}
- isSquare 프로퍼티는 on the go 프로퍼티이다.
- 커스텀 게터를 정의하는 방식과 클래스 안에 파라미터가 없는 함수를 정의하는 방식은 비슷하다.
- 구현이나 성능 차이 없음.
- 가독성 차이
- 클래스의 특성을 기술하고 싶다면 프로퍼티로 정의
- 클래스의 행동을 기술하고 싶다면 멤버 함수 선택
🔖 2.2.3 코틀린 소스코드 구조: 디렉터리와 패키지
package com.bottleh.studycodecollection.kotlin.chap2
class Rectangle(var height: Int, var width: Int) {
val isSquare: Boolean
get() {
return height == width
}
}
fun createUnitSquare(): Rectangle {
return Rectangle(1, 1)
}
- 같은 패키지에 속해 있다면 다른 파일에서 정의한 선언일지라도 직접 사용할 수 있다.
- 다른 패키지에 정의한 선언을 사용하려면 import 키워드를 사용해서 다른 패키지를 불러올 수 있다.
- 자바에서는 패키지의 구조와 디렉터리 계층구조가 같아야 한다.
- 코틀린에서는 패키지와 디렉터리 구조가 맞아 떨어질 필요가 없다.
- 다만, 자바와 같이 패키지별로 디렉터리를 구성하는 편이 낫다.
📖 2.3 선택 표현과 처리: 이넘과 when
🔖 2.3.1 이넘 클래스와 이넘 상수 정의
enum class Color {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}
- java는 enum이지만 kotlin은 enum class다.
- enum은 소프트 키워드라서 class 앞이 아니라면 다른곳에서 사용할 수 없다.
- class는 하드 키워드라서 클래스를 표현하는 변수 등을 정의할 때 clazz 등을 사용해야 한다.
package com.bottleh.studycodecollection.kotlin.chap2
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
fun printColor() = println("$this is ${rgb()}")
}
fun main() {
println(Color.BLUE.rgb())
Color.GREEN.printColor()
}
- enum class 안에 메서드를 정의하는 경우 반드시 이넘 상수 목록과 메서드 정의 사이에 세미콜론을 넣어야 한다.
- 코틀린에서 세미콜론이 필수인 유일한 경우다.
🔖 2.3.2 when으로 이넘 클래스 다루기
fun getMnemonic(color: Color) =
when (color) {
Color.RED -> "Richard"
Color.ORANGE -> "Of"
Color.YELLOW -> "York"
Color.GREEN -> "Gave"
Color.BLUE -> "Battle"
Color.INDIGO -> "In"
Color.VIOLET -> "Vain"
}
fun main() {
println(getMnemonic(Color.BLUE))
}
- java와 달리 break를 넣지 않아도 된다.
fun measureColor() = Color.ORANGE
fun getWarmthFromSensor(): String {
val color = measureColor()
return when(color) {
Color.RED, Color.ORANGE, Color.YELLOW -> "warm (red = ${color.r})"
Color.GREEN -> "neutral (green = ${color.g})"
Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold (blue = ${color.b})"
}
}
fun main() {
println(getWarmthFromSensor())
}
🔖 2.3.3 when식의 대상을 변수에 캡처
fun getWarmthFromSensor() =
when(val color = measureColor()) {
Color.RED, Color.ORANGE, Color.YELLOW -> "warm (red = ${color.r})"
Color.GREEN -> "neutral (green = ${color.g})"
Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold (blue = ${color.b})"
}
- when 식 대상 값을 변수에 넣을 수 있다.
- 캡처한 변수의 프로퍼티에 접근 가능하다.
🔖 2.3.4 when의 분기 조건에 임의의 객체 사용
fun mix(c1: Color, c2: Color) =
when(setOf(c1, c2)) {
setOf(RED, YELLOW) -> ORANGE
setOf(YELLOW, BLUE) -> GREEN
setOf(BLUE, VIOLET) -> INDIGO
else -> throw Exception("Dirty color")
}
fun main() {
println(mix(BLUE, YELLOW))
}
- when의 분기 조건 부분에 식을 넣을 수 있기 때문에 많은 경우 코드를 더 간결하고 아름답게 작성할 수 있다.
🔖 2.3.5 인자 없는 when 사용
fun mixOptimized(c1: Color, c2: Color) =
when {
(c1 == RED && c2 == YELLOW) || (c1 == YELLOW && c2 == RED) -> ORANGE
(c1 == YELLOW && c2 == BLUE) || (c1 == BLUE && c2 == YELLOW) -> GREEN
(c1 == BLUE && c2 == VIOLET) || (c1 == VIOLET && c2 == BLUE) -> INDIGO
else -> throw Exception("Dirty color")
}
fun main() {
println(mixOptimized(BLUE, YELLOW))
}
- 인자가 없는 when 식을 사용하면 불필요한 객체 생성을 막을 수 있다.
- 다만, 코드의 가독성은 떨어진다.
🔖 2.3.6 스마트 캐스트: 타입 검사와 타입 캐스트 조합
interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
- 여러 타입의 식 객체를 아우르는 공통 타입 역할만 수행하는 인터페이스를 마커 인터페이스라고 부른다.
fun eval(e: Expr): Int {
if (e is Num) {
val n = e as Num // 불필요한 중복
return n.value
}
if (e is Sum) {
return eval(e.left) + eval(e.right)
}
throw IllegalArgumentException("Unknown expression")
}
- 코틀린의 is 검사는 어떤 변수의 타입을 확인한 다음에 그 타입에 속한 멤버에 접근하기 위해 명시적으로 변수 타입을 변환하지 않아도 된다.
- 실제로는 컴파일러가 타입을 대신 변환(스마트 캐스트)
🔖 2.3.7 리팩터링: if를 when으로 변경
fun eval(e: Expr): Int =
if (e is Num) {
e.value
} else if (e is Sum) {
eval(e.right) + eval(e.left)
} else {
throw IllegalArgumentException("Unknown expression")
}
- if는 식이라서 삼항 연산자가 따로 없다.
- 중괄호를 없애고 본문 구문을 사용해 작성 가능하다.
fun eval(e: Expr): Int =
if (e is Num) e.value
else if (e is Sum) eval(e.right) + eval(e.left)
else throw IllegalArgumentException("Unknown expression")
- if의 분기에 식이 하나밖에 없다면 중괄호를 생략해도 된다.
- 블록을 사용한다면 블록의 마지막 식이 그 분기의 결괏값이다.
fun eval(e: Expr): Int =
when (e) {
is Num -> e.value
is Sum -> eval(e.right) + eval(e.left)
else -> throw IllegalArgumentException("Unknown expression")
}
- when으로 변경된 코드
🔖 2.3.8 if와 when의 분기에서 블록 사용
fun evalWithLogging(e: Expr): Int =
when (e) {
is Num -> {
println("num: ${e.value}")
e.value
}
is Sum -> {
val left = evalWithLogging(e.left)
val right = evalWithLogging(e.right)
println("sum: $left + $right")
left + right
}
else -> throw IllegalArgumentException("Unknown expression")
}
- if나 when 모두 분기에 블록을 사용할 수 있다.
- 블록의 마지막 식이 블록의 결과 라는 규칙은 블록이 값을 만들어내야 하는 경우 항상 성립한다.
📖 2.4 대상 이터레이션: while과 for 루프
🔖 2.4.1 조건이 참인 동안 코드 반복: while 루프
- 코틀린에는 while과 do-while 루프가 있다.
- 내포된 루프의 경우 레이블을 지정할 수 있다.
- break나 continue를 사용할 때 레이블을 참조할 수 있다.
🔖 2.4.2 수에 대해 이터레이션: 범위와 순열
val oneToTen = 1..10
- 코틀린에서는 범위를 사용하여 루프를 한다.
- 폐구간(양끝 포함)
- 두번째 값이 항상 범위에 포함
fun fizzBuzz(i: Int) = when {
i % 15 == 0 -> "FizzBuzz"
i % 3 == 0 -> "Fizz"
i % 5 == 0 -> "Buzz"
else -> "$i"
}
fun main() {
for (i in 1..100) {
println(fizzBuzz(i))
}
}
- 피즈버즈 게임 구현 예시
fun main() {
for (i in 100 downTo 1 step 2) {
println(fizzBuzz(i))
}
}
- 역방향 순열 예시
- 반폐구간에 대해 이터레이션 하고 싶다면 ..< 연산 사용
🔖 2.4.3 맵에 대한 이터레이션
fun main() {
val collection = listOf("red", "green", "blue")
for (color in collection) {
println("$color ")
}
}
- 단순 컬렉션 이터레이션 예시
fun main() {
val binaryReps = mutableMapOf<Char, String>()
for (char in 'A'..'F') {
val binary = char.code.toString(radix = 2) // 아스키 코드를 이진 표현으로 변환
binaryReps[char] = binary
}
for ((letter, binary) in binaryReps) {
println("$letter = $binary")
}
}
- java의 경우 map에 put method를 써야하지만 코틀린은 그렇지 않다.
fun main() {
val list = listOf("10", "11", "1001")
for ((index, element) in list.withIndex()) {
println("$index: $element")
}
}
- index와 함께 컬렉션을 이터레이션 함
🔖 2.4.4 in으로 컬렉션이나 범위의 원소 검사
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'
- !in을 사용하면 어떤 값이 범위에 속하지 않는지 검사할 수 있다.
- when에서 in 검사를 사용할 수 있다.
fun main() {
println("Kotlin" in "Java".."Scala")
}
- 알파벳 순서로 비교하기 때문에 true 반환
- 컬렉션도 마찬가지로 in 연산을 사용할 수 있다.
📖 2.5 코틀린에서 예외 던지고 잡아내기
- 자바와 달리 코틀린의 throw는 식이므로 다른 식에 포함될 수 있다.
🔖 2.5.1 try, catch, finally를 사용한 예외 처리와 오류 복구
fun readNumber(reader: BufferedReader): Int? {
try {
val line = reader.readLine()
return Integer.parseInt(line)
} catch (e: NumberFormatException) {
return null
} finally {
reader.close()
}
}
- java와 달리 함수가 던질 수 있는 예외를 명시할 필요가 없다. (throws)
- 코틀린은 checked exception과 unchecked exception을 구별하지 앟는다.
- 코틀린에서는 컴파일러가 예외 처리를 강제하지 않는다.
🔖 2.5.2 try를 식으로 사용
fun readNumber(reader: BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
return
}
println(number)
}
- try의 본문은 반드시 중괄호로 둘러싸야 한다.
- 마지막 식의 값이 전체 결과값이다.
