HTTP 200 OK

Memento mori & Carpe diem

Kotlin

[Kotlin] 기초

sjoongh 2022. 8. 1. 17:56

코틀린 요약

  • 코틀린은 타입 추론을 지원하는 정적 타입 지정 언어다. 따라서 소스코드의 정확성과 성능을 보장하면서도 소스코드를 간결하게 유지 할 수 있다.
  • 안정성이 있다.(null체크 & 캐스트)
  • Java 코드와 상호운용하기 쉽다.

kotlin 기초


함수

fun main(args: Array<String>) {
    printLn("Hello, world!")
}
  • 함수를 선언할 때 fun 키워드를 사용한다.
  • 파라미터 이름 뒤에 그 파라미터의 타입을 쓴다. 변수를 선언할 때에도 마찬가지로 타입을 지정한다.

반환형 함수

fun max(a: Int, b: Int): Int {
    return if (a > b) a else b
}
  • max -> 함수이름
  • a: Int, b: Int -> 파라미터 목록
  • Int -> 반환타입
  • return {} -> 함수본문

변수

코틀린에서는 타입 지정을 생략하는 경우가 흔하다. 코틀린에서는 변수 이름 뒤에 타입을 명시하거나 생략한다.

val question = "삶, 우주..."
val answer = 42

// 위처럼 타입 표기를 생략해도 좋지만 타입을 명시해도 무관하다.
val answer: Int = 42
  • val : 변경 불가능한 참조를 저장하는 변수다. val로 선언된 변수는 일단 초기화하고 나면 재대입이 불가능하다. 자바로 말하면 final이다.
  • var : 변경 가능한 참조다. 이런 변수의 값은 바뀔 수 있다. 자바의 일반 변수에 해당한다.


기본적으로는 모든 변수를 val 키워드를 사용해 불변 변수로 선언하고 나중에 꼭 필요할 경우에만 var로 변경, val 변수는 블록을 실행할 때 정확히 한번만 초기화, 조건문을 사용해 여러 값으로 초기화도 가능하며 val 참조 자체는 불변일지라도 그 참조가 가리키는 객체의 내부 값은 변경될 수 있다.

val languages = arrayListOf("Java") // 불변 참조를 선언
languages.add("Kotlin") // 참조가 가리키는 객체 내부를 변경

var 키워드를 사용하면 변수의 값을 변경할 수 있지만 변수의 타입은 고정돼 바뀌지 않는다.

var answer = 42
answer = "no answer"
// 컴파일 오류 발생함
  • 문자열 리터럴에서 컴파일 오류가 발생한다. 그 이유는 그 타입(String)이 컴파일러가 기대하는 타입(Int)와 다르기 때문이다.

코틀린의 조건문(if, when)

fun max(a: int, b: int) = if (a > b) a else b
  • 코틀린에서 if는 문이 아닌 식으로 작성할 수 있다. 모든 제어 구조가 문인 반면 코틀린에서는 루프를 제외한 대부분의 제어구조가 식이다.
  • when은 java의 switch와 같은 동작을 수행한다. default는 else로 구현할 수 있다.

문자열 템플릿

fun main(args: Array<String>) {
    val name = if (args.size > 0) args[0] else "Kotlin"
    printLn("Hello, $name!")
}

문자열 리터럴의 필요한곳에 변수를 넣되 변수 앞에 $를 추가해야 한다.

  • 자바의 문자열 접합 연산("Hello, " + name + "!")과 동일한 기능이다.
  • printLn("Hello, ${args[0]}!") 과 같이 복잡한 식도 넣을 수 있다.
  • "Hello, $name"을 "${name}님 반가워요"처럼 변경하는 것이 좋다.

class와 property

class Person(val name: String)

위와 같은 유형의 클래스(코드가 없이 데이터만 저장하는 클래스)를 값 객체라 부른다.

프로퍼티

클래스에서 프로퍼티를 선언할 때는 val이나 var를 사용한다. val로 선언한 프로퍼티는 읽기 전용이며, var로 선언한 프로퍼티는 변경 가능하다.
코틀린에서 프로퍼티는 필드와 접근자를 통칭하는 용어이다.
ex) var, val, getter/setter

  • 클래스 안에서 변경 가능한 프로퍼티 선언
    class Person {
      val name: String, // 읽기 전용 프로퍼티로, 코틀린은(비공개) 필드와 필드를 읽는 단순한(공개) 게터를 만들어낸다.
      var isMarried: Boolean // 쓸 수 있는 프로퍼티로, 코틀린은 (비공개)필드, (공개) 게터, (공개) 세터를 만들어낸다.
    }
  • 기본적으로 코틀린에서 프로퍼티를 선언하는 방식은 프로퍼티와 관련 있는 접근자를 선언
    • 읽기 전용 프로퍼티의 경우 게터만 선언하며 변경할 수 있는 프로퍼티의 경우 게터와 세터를 모두 선언한다.
  • 코틀린은 값을 저장하기 위한 비공개 필드와 그 필드에 값을 저장하기 위한 세터
  • 필드의 값을 읽기 위한 게터로 이루어진 간단한 디폴트 접근자 구현을 제공한다.
  • getter의 경우 (get + 프로퍼티명)으로 결정된다.
  • getter와 setter이름을 정하는 규칙에는 예외가 있다.
    • 이름이 is로 시작하는 property의 getter에는 get이 붙지 않고 원래 이름을 그대로 사용
    • setter에는 is를 set으로 바꾼 이름을 사용한다. 따라서 자바에서 isMarried property의 getter를 호출하려면 isMarried()를 사용해야 한다.
val person = Person("Bob", true) // new 키워드를 사용하지 않고 생성자를 호출한다.
printLn(person.name) -> Bob 
printLn(person.isMarried) -> true
// 위와 같이 프로퍼티 이름을 직접 사용해도 코틀린이 자동으로 게터를 호출해준다.
  • 게터를 호출하는 대신 프로퍼티를 직접 사용했음에 유의
  • java : person.setMarried(false)
  • kotlin : person.isMarried = false
    • 자바에서 선언한 클래스에 대해 코틀린 문법을 사용해도 된다.

프로퍼티와 메소드가 있는 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
}
printLn(Color.BLUE.rgb()) -> 255
  • enum에서도 일반적인 클래스와 마찬가지로 생성자와 프로퍼티를 선언한다.
  • 각 enum 상수를 정의할 때는 그 상수에 해당하는 프로퍼티 값을 지정해야 한다.
  • enum 클래스 안에 메소드를 정의하는 경우 반드시 enum상수 목록과 메소드 정의 사이에 세미클론을 넣어야 한다.

when으로 enum클래스 다루기

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"
    }
printLn(getMnemonic(Color.BLUE)) -> Battle

fun getWarmth(color: Color) {
    Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
}
printLn(getWarmth(Color.ORANGE)) -> warm
  • 각 분기의 끝에 break를 사용하지 않아도 괜찮다.
  • 한 분기 안에서 여러 값을 매치 패턴으로 사용할 수도 있다. 그럴 경우 값 사이를 콤마로 구분한다.

when 과 임의의 객체 함께 사용

  • 코틀린 when의 분기조건은 임의의 객체를 허용한다.
fun mix(c1: Color, c2: Color) = 
    when (setOf(c1, c2)) { // when식의 인자로 아무 객체나 사용할 수 있다. when은 이렇게 인자로 받은 객체가 각 분기 조건에 있는 객체와 같은지 테스트한다.
        setOf(RED, YELLOW) -> ORANGE // 두 색을 혼합해서 다른 색을 만들 수 있는 경우를 열거한다.
        setOf(YELLOW, BLUE) -> GREEN
        setOf(BLUE, VIOLET) -> INDIGO
        else -> throw Exception("Dirty color") // 매치되는 분기 조건이 없으면 이 문장을 실행한다.
    }
printLn(mix(BLUE, YELLOW)) -> GREEN

인자 없는 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")
    }
 printLn(mixOptimized(BLUE, YELLOW)) -> GREEN 
  • when에 아무 인자도 없으려면 각 분기의 조건이 boolean 결과를 계산하는 식이어야 한다.
  • mixOptimized 함수는 앞에서 살펴본 mix 함수와 같은 일을 한다.

스마트 캐스트: 타입 검사와 타입 캐스트를 조합

식을 표현하는 클래스 계층

interface Expr
class Num(val value: Int): Expr // value라는 프로퍼티만 존재하는 단순한 클래스로 Expr 인터페이스를 구현한다.
class Sum(val left: Expr, val right: Expr): Expr // Expr 타입의 객체라면 어떤 것이나 Sum 연산의 인자가 될 수 있다. 따라서 Num이나 다른 Sum이 인자로 올 수 있다.
  • 클래스가 구현하는 인터페이스를 지정하기 위해서 콜론(:) 뒤에 인터페이스 이름을 사용한다.
  • Expr 인터페이스에는 두 가지 구현 클래스가 존재한다.
    • 어떤 식이 수라면 그 값을 반환한다.
    • 어떤 식이 합계라면 좌항과 우항의 값을 계산한 다음에 그 두 값을 합한 값을 반환한다.

 

if 연쇄를 사용해 식을 계산하기

fun eval(e: Expr): Int {
    if (e is Num) {
        val n = e as Num
        return n.value
    }
    if (e is Sum) {
        return eval(e.right) + eval(e.left) // 변수 e에 대해 스마트캐스트를 사용한다
    }
    throw IllegalArgumentException("Unknown expression")
}
printLn(eval(Sum(Sum(Num(1), Num(2)), Num(4)))) -> 7
  • 코틀린에서는 is를 사용해 변수 타입을 검사한다.
  • is 검사는 자바의 instanceof와 비슷하다.
  • 스마트 캐스트 : 어떠한 변수가 원하는 타입인지 is로 검사하고 나면 굳이 변수를 원하는 타입으로 캐스팅하지 않아도 마치 처음부터 그 변수가 원하는 타입으로 선언된 것처럼 사용할 수 있다.
    • 컴파일러가 캐스팅을 수행한다.
  • eval 함수에서 e의 타입이 Num인지 검사한 다음 부분에서 컴파일러는 e의 타입을 Num으로 해석한다. 그렇기 때문에 Num의 프로퍼티인 value를 명시적 캐스팅 없이 e.value로 사용할 수 있다.
  • Sum의 프로퍼티인 right와 left도 마찬가지다. Sum 타입인지 검사한 다음부터는 e.right와 e.left를 사용할 수 있다.
    if (e is Sum) {
      return eval(e.right) + eval(e.left)
    }
    // IDE에서는 배경색으로 스마트 캐스트를 표시해준다.
  • 스마트 캐스트는 is로 변수에 든 값의 타입을 검사한 다음에 그 값이 바뀔 수 없는 경우에만 작동한다.
  • 스마트 캐스트를 사용한다면 그 프로퍼티는 반드시 val이어야 하며 커스텀 접근자를 사용한것도 안된다.
  • 원하는 타입으로 명시적으로 타입 캐스팅하려면 as 키워드를 사용한다 -> val n = e as Num

if와 when의 분기에서 블록 사용

  • 블록의 마지막 식이 블록의 결과라는 규칙은 블록이 값을 만들어내야 하는 경우 항상 성립
  • 식이 본문인 함수는 블록을 가질 수 없고 블록이 본문인 함수는 내부에 return문이 반드시 있어야 한다.

수에 대한 이터레이션(반복문): 범위와 수열

  • 코틀린에는 자바의 for 루프에 해당하는 형태가 존재하지 않고 for-each 루프에 해당하는 형태만 존재한다.
    • 코틀린에서는 범위(range)를 사용한다
    • 범위는 기본적으로 두 값으로 이루어진 구간이다. 보통의 그 두 값은 정수 등의 숫자 타입의 값이며, ..연산자로 시작 값과 끝 값을 연결해서 범위를 만든다.
    • val oneToTen = 1..10
  • 코틀린의 범위는 폐구간(닫힌 구간) 또는 양끝을 포함하는 구간이다. 이는 두 번째 값(10)이 항상 범위에 포함된다는 뜻이다.
  • 정수 범위로 수행할 수 있는 가장 단순한 작업은 범위에 속한 모든 값에 대한 이터레이션이다. 이런 식으로 어떤 범위에 속한 값을 일정한 순서로 이터레이션하는 경우를 수열이라고 부른다.

fizzbuzz with when

fun fizzBuzz(i: Int) = when {
    i % 15 == 0 -> "FizzBuzz"
    i % 3 == 0 -> "Fizz"
    i % 5 == 0 -> "Buzz"
    else -> "$i"
}
for (i in 1..100) {
    print(fizzBuzz(i))
}

증가 값을 갖고 범위 이터레이션

for (i in 100 downTo 1 step 2) {
    print(fizzbuzz(i))
}
  • 여기서는 증가 값 step을 갖는 수열에 대해 이터레이션 한다. 증가 값을 사용하면 수를 건너 뛸 수 있다. 증가 값을 음수로 만들면 정방향 수열이 아닌 역방향 수열을 만들 수 있다.
  • 위 예제는 100 downTo 1로서 역방향 수열을 만든다 그 뒤에 step 2를 붙이면 증가값의 절대값이 2로 바뀐다.
  • ..은 항상 범위의 끝 값(..의 우항)을 포함한다. 하지만 끝 값을 포함하지 않는 반만 닫힌 범위에 대해 이터레이션하고 싶은 경우는
    • until 함수를 사용
    • for (x in 0 until size)라는 루프는 for (x in 0..size-1)과 같다.

맵에 대한 이터레이션

val binaryReps = TreeMap<Char, String>() // 키에 대한 정렬하기 위해 TreeMap을 사용한다.

for (c in 'A'..'F') { // A부터 F까지 문자의 범위를 사용해 이터레이션한다.
    val binary = Integer.toBinaryString(c.toInt()) // 아스키 코드를 2진 표현으로 바꾼다.
    binaryReps[c] = binary // c를 키로 c의 2진 표현을 맵에 넣는다.
}
for ((letter, binary) in binaryReps) { // 맵에 대해 이터레이션한다. 맵의 키와 값을 두 변수에 각각 대입한다.
    printLn("$letter = $binary")
}
  • ..연산자는 숫자 타입뿐만이 아닌 문자 타입의 값에도 적용할 수 있다. 'A'..'F'는 A부터 F에 이르는 문자를 모두 포함하는 범위를 만든다.
  • binaryReps[c] = binary는 binaryReps.put(c, binary)라는 자바 코드와 같다.
val list = arrayListOf("10", "11", "1001")
for ((index, element) in list.withIndex()) { // 인덱스와 함께 컬렉션을 이터레이션한다.
    printLn("$index: $element")
}
// 0: 10
// 1: 11
// 2: 1001

in으로 컬렉션이나 범위의 원소 검사

in 연산자를 사용해 어떤 값이 범위에 속하는지 검사할 수 있다. 반대로 !in을 사용하면 어떤 값이 범위에 속하지 않는지 검사할 수 있다.

in을 사용해 값이 범위에 속하는지 검사하기

fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'
printLn(isLetter('q')) -> true
printLn(isNotDigit('x')) -> true

in 'a'..'z', in'A'..'Z' -> "It's a letter!" // 여러 범위 조건을 함께 사용해도 된다.

printLn("Kotlin" in "Java".."Scala") // "Java" <= "Kotlin" && "Kotlin" <= "Scala"와 같다
-> true
// String에 있는 Comparable구현이 두 문자열을 알파벳 순서로 비교하기 때문에 in 검사에서도 문자열을 알파벳 순서로 비교한다.
// 알파벳 첫 글자를 비교하기에 true이다.

코틀린의 예외 처리

if (percentage !in 0..100) {
    throw IllegalArgumentException( // new를 붙일 필요 없음
        "A percentage value must be between 0 and 100: $percentage")
}

val percentage = 
    if (numer in 0..100)
        number
    else
        throw IllegalArgumentException(
            "A percentage value must be between 0 and 100: $number"
        )
// if의 조건이 참이므로 percentage변수가 number의 값으로 초기화된다.
// 조건이 거짓이면 변수가 초기화되지 않는다.

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()
    }
}
val reader = BufferedReader(StringReader("239"))
printLn(readNumber(reader)) -> 239
  • ? : null일 수 있음
  • !! : null이 아니다.
  • 자바 코드와 가장 큰 차이는 throws절이 코드에 없다는 점이다

try를 식으로 사용

fun readNumber(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine()) // 이 식의 값이 try식의 값이 된다.
    } catch (e: NumberFormatException) {
        return
    }
    printLn(number)
}
val reader = BufferedReader(StringReader("not a number"))
readNumber(reader) // 아무것도 출력X
  • 코틀린의 try 키워드는 if나 when과 마찬가지로 식이다. 따라서 try의 값을 변수에 대입할 수 있다.
  • if와 달리 try의 본문을 반드시 중괄호 {} 로 둘러싸야 한다. 다른 문장과 마찬가지로 try의 본문도 내부에 여러 문장이 있으면 마지막 식의 값이 전체 결과 값이다.
  • 위의 예제는 catch안에서 return을 사용했으므로 다음의 코드는 진행되지 않는다.
  • 계속 진행하고 싶다면 catch블록도 값을 만들어야 한다.
fun readNumber(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine()) // 예외가 발생하지 않으면 이 값을 사용
    } catch (e: NumberFormatException) { // 예외가 발생하면 null값을 사용
        null
    }
    printLn(number)
}
val reader = BufferedReader(StringReader("not a number"))
readNumber(reader) -> null // 예외가 발생하였으므로 함수가 null을 출력

요약

  • 함수를 정의할 때 fun 키워드를 사용한다. val과 var는 각각 읽기 전용 변수와 변경 가능한 변수를 선언할 때 쓰인다.
  • 문자열 템플릿을 사용하면 문자열을 연결하지 않아도 되므로 코드가 간결해진다. 변수 이름 앞에 $를 붙이거나, 식을 ${식} 처럼 ${}로 둘러싸면 변수나 식의 값을 문자열 안에 넣을 수 있다.
  • 코틀린에서는 값 객체 클래스를 아주 간결하게 표현할 수 있다.
  • if는 코틀린에서 식이며, 값을 만들어낸다.
  • 코틀린 when은 자바의 switch와 비슷하지만 더 강력하다.
  • 어떤 변수의 타입을 검사하고 나면 굳이 그 변수를 캐스팅하지 않아도 검사한 타입이 변수처럼 사용, 그런 경우 컴파일러가 스마트 캐스트를 활용해 자동으로 타입 변경
  • for, while, do-while 루프는 자바가 제공하는 같은 키워드의 기능과 비슷하다. 특히 맵을 이터레이션하거나 이터레이션하면서 컬렉션의 원소와 인덱스를 함께 사용해야 하는 경우 코틀린의 for가 더 편리하다.
  • 1..5와 같은 식은 범위를 만들어낸다. 범위와 수열은 코틀린에서 같은 문법을 사용하며, for 루프에 대해 같은 추상화를 제공한다. 어떤 값이 범위에 들어있거나 들어있지 않은지 검사하기 위해서 in이나 !in을 사용한다.
  • 코틀린 예외 처리는 자바와 비슷하다. 다만 코틀린에서는 함수가 던질 수 있는 예외를 선언하지 않아도 된다.

'Kotlin' 카테고리의 다른 글

JSON 직렬화가 왜 안돼?..  (1) 2024.11.24
[Kotlin] Result란?  (0) 2023.09.12
[Kotlin] Error Handling  (0) 2023.08.26
Run Catching  (0) 2023.03.01