BottleH Blog

Kotlin in Action - 2장 코틀린 기초

    Tags

  • Kotlin
Kotlin in Action - 2장 코틀린 기초 thumbnail

📖 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의 본문은 반드시 중괄호로 둘러싸야 한다.
  • 마지막 식의 값이 전체 결과값이다.
Written by@BottleH
Back-End Developer

GitHub