본문 바로가기
책을 읽겠습니다!/Kotlin in Action

[Kotlin in Action] 2장 코틀린 기초

by Unagi_zoso 2023. 9. 23.

기본 요소: 함수와 변수

fun main(args: Array<String>) {
    println("Hello, world!")
}
  • 함수를 선언할 때 fun 키워드를 쓴다.
  • 파라미터 이름 뒤에 그 파라미터의 타입을 쓴다. 변수를 선언할 때도 마찬가지
  • 함수를 최상위 수준에 정의할 수 있다. (자바와 달리) 꼭 클래스 안에 함수를 넣어야 할 필요가 없다.
  • 배열도 일반적인 클래스와 마찬가지다. 코틀린에는 자바와 달리 배열 처리를 위한 문법이 따로 존재하지 않는다.
  • System.out.println 대신 println이라고 쓴다. 코틀린 표준 라이브러리는 여러 가지 표준 자바 라이브러리 함수를 간결하게 사용할 수 있게 감싼 래퍼를 제공한다.
  • 최신 프로그래밍 언어 경향과 마찬가지로 줄 끝에 세미콜론을 붙이지 않아도 좋다.

함수

fun max(a: Int, b: Int): Int {
    return if (a > b) a else b
}

println(max(1, 2))

반환타입은 뒤에 따라온다 패러미터 괄호 뒤 ': 반환 타입' 형태로 온다.

코틀린의 if는 값을 맨ㄷ를어내지 못하는 문장이 아닌 결과를 만드는 표현식

문(statement)과 식(expression)의 구분

식은 값을 만들어 내며 다른 식의 하위 요소로 계산에 참여할 수 있는 반면 , 자신을 둘러싸고 있는 가장 안 쪽 블록의 최상위 요소로 존재하며 아무런 값을 만들어내지 않는다는 차이가 있다.

자바에서는 모든 제어 구조가 문인 반면 코틀린에서는 루ㅡ프를 제외한 대부분의 제어 구조가 식이다.

대입문의 경우 자바는 식이지만 코틀린에서는 문이 되었다.

식이 본문인 함수

자바의 functional interface 같네!

fun max (a: Int, b Int): Int = if (a > b) a else b

코틀린에서는 식이 본문인 함수가 자주 쓰인다. 그러한 본문 식에는 단순한 산술식이나 함수 호출 식 뿐 아니라 if, when, try 등의 더 복잡한 식도 자주 쓰인다.

여기서 반환 타입도 생략할 수 있다.

fun max (a: Int, b Int) = if (a > b) a else b

타입추론 덕분이야! 식이 본문인 함수에 한한거지만... 소스코드는 컴파일러를 거치기 전이라 코드를 읽는 사람 입장에선 헷갈리겠는데.. 난 타입을 적을거 같아... 식이 본문이면 간단한 함수일테니 크게 영향이 없을까나?

변수

코틀린에서느 타입 지정을 생략하는 경우가 흔하다. 타입으로 변수 선언을 시작하면 타입을 생략할 경우 식과 변수 선언을 구별하ㅏㄹ 수 없다. 그런 이유로 코틀린에서는 타입을 이름뒤에 명시하거나 생략하게 허용한다.

val question = "삶, 우주, 그리고 모든 것에 대한 궁극적인 질문"
val answer = 42

이 예제에서는 타입 표기를 생략했지만 원한다면 타입을 명시해도 된다.

val answer: Int = 42

식이 본문인 함수에서와 마찬가지로 여러분이 타입을 ㄹ지정하지 않으면 컴파일러가 초기화 식 분석해 초기화 식의 타입을 변수 타입으로 지정한다.

초기화 없이 변수 선언하려면 반드시 타입 적어야 해

val answer: Int

변경 가능한 변수와 불가능한 변수

  • val(값을 뜻하는 value에서 따옴) - 변경 불가능한 ^immutable^ 참조를 저장하는 변수다. val로 선언된 변수는 일단 초기화하고 나면 재대입이 불가능하다. 자바로 치면 final
  • var(변수를 뜻하는 variable) - 변경 가능한 ^mutable^ 참조다. 바뀔 수 있다. 자바의 일반 변수

기본적으로 모든 변수를 val, 나중에 필요할 때만 var로

불변 참조와 불변 객체

val 변수는 블록 실행 시 한 번만 초기화해야한다. val 참조는 불변이라도 참조가 가리키는 객체의 내부 값은 변경될 수 있다.

val languages = arrayListOf("Java") // 불변 참조를 선언했다. 참조하는 대상이 바뀌지 않는다는건가
languages.add("Kotlin") // 참조가 가리키는 객체 내부를 변경한다.

6장에서 뮤터블 객체, 이뮤터블 객체에 대해 본다.

var 키워드 사용 시 변수의 값을 변겨알 수 있지만 타입은 바꾸지 못한다.

var answer = 42
answer = "no answer"

어떤 타입의 변수에 다른 타입의 값을 저장하고 싶다면 변환 함수를 써서 값을 변수의 타입으로 변환하거나 값을 변수에 대입할 수 있는 타입으로 강제 형 변환(coerce) 해야한다.

더 쉽게 문자열 형식 지정: 문자열 템플릿

println("Hello, $name!")

복잡한 식도 중괄호로 싸서 넣을 수 있다.

println("Hello, ${args[0]}")

영어변수 뒤 한글 유니코드 붙어있음 같은건 줄 알고 오류 생겨

중괄호 생활화 하자

중괄호로 둘러싸면 "도 사용 가능하다. 중과호 안에 식 넣어서도 가능 if else로 어ㅓㅈ구 저쩌구

클래스와 프로퍼티

간단한 자바빈 클래스 Person

public class Person {
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

생성자나 게터 세터 같은거 만들어야 한다. 코틀린에는 훨씬 적은 코드로 작성 가능

코틀린에선 이렇다

class Person(val name: String)

이런 유형의 클래스(코드가 없이 데이터만 저장하는 클래스)를 값 객체 vo라 부른다.
자바를 코틀린으로 변환하니 public 가시성 변경자가 사라졌음을 확인하자. 코틀린의 기본 가시성은 public이므로 이런 경우 변경자를 생략해도된다.

프로퍼티

클래스 개념의 목적은 데이터를 캡슐화 하고 캡슐화한 데이터를 다루는 코드를 한 주체 아래 가두는 것. 자바에선 데이터를 ㅣㄹ드에 저장하며 가시성은 보통 비공개이다. 그리고 접근 가능한 접근자 메서드를 제공한다. 자바에서는 필드와 접근자를 한데 묶어 프로퍼티라고 부르며, 이를 활용하는 프레임워크는 많다. 코틀린은 프로퍼티를 언어 기본 기능으로 제공하며, 코틀린 프로퍼티는 자바의 필드와 접근자 메소드를 대신한다.

val, var를 클래스의 프로퍼티로 선언

class Person(
    val name: String, // 읽기 전용 프로퍼티, 코틀린은 (비공개) 필드와 필드를 읽는 단순한 (공개) 게터를 만들어낸다.
    var isMarried: Boolean // 쓸 수 있는 프로퍼티로, 코틀린은 (비공개) 필드, (공개) 게터, (공개) 세터를 만들어낸다.
}

기본적인 코틀린에서 프로퍼티 선언하려면 프로퍼티와 관련 있는 접근자를 선언하는 것. 비공개 필드와 디폴트 접근자로 구현된다..

getName, setName을 만들어주고 is 로 시작하는 애는 get이 붙지않고 그대로 가고 set은 is를 set으로 대체해서 생성된다.

val person = Person("Bob", true) // new 키워드를 사용하지 않고 생성자를 호출한다.
println(person.name) // 프로퍼티 이름을 직접 사용해도 코틀린이 자동으로 게터를 호출해준다.
println(person.isMarried)

person.setMarried(false)로 어떤 사람이 이혼했다는 사실을 기록하지만, 코틀린에서는 person.isMarried = false 진짜 퍼블릭 접근하듯 사용하네 내부에선 세터가 사용된다해도

자바에서 선언한 클래스에 대해 코틀린 문법을 사용해도 된다. 자바 클래스의 게터를 val 프로퍼티처럼 사용할 수 있고,게터/세터 쌍이 있으면 var 프로퍼티 처럼 쓸 수도 있다.

뒷받침하는 필드(backing field) = 프로퍼티 저장 필드.

커스텀 접근자

프로퍼티 접근자 직접 작성.

class Rectangle(val height: Int, val width: Int) {
val isSquare: Boolean
get() { // 프로퍼티 게터 선언
return height == width
}
}

package 쓸 수 있다.
package에 속해 있으면 다른 파일에서 정의한 선언도 사용 가능
다른 패키지에 정의한 거 쓰려면 import 해야 해

스타 임포트를 사용할 시 클래스 뿐만 아니라 최상위에 정의된 함수나 프로퍼티까지 모두 불러온다.

패키지에 있던 클래스들은 디렉터리에서도 그 안에 있어야 해. 디렉터리 구조가 패키지 구조를 따라야 한다.

코틀린에서는 여러 클래스를 한 파일에 너을 수 있고, 파일의 이름도 정할 수 있다. 디스크 상 어느 디렉터리에 소스코드 파일을 위치 시켜도 문제 없다. 마이그레이션 시 문제 있을 수도 있으니 그냥 자바 방식 따르자. 이거 왜 만든겨 클래스 크기가 작은 경우엔 한 파일에 넣어도 괜찮다...

선택 표현과 처리 : enum과 when

when은 자바의 switch를 대치한다.

enum 클래스 정의

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

코틀린에서느 enum class를 사용하지만 자바에서는 enum 을 사용. 코틀린 enum은 소프트 키워드라 부르는 존재다.

enum class Color(
    val r: Int, val g: Int, val b: Int // 상수의 프로퍼티를 정의한다.
) {
    RED(255, 0, 0), ORANGE(255, 165), // 각 상수를 생성할 때 그에 대한 프로퍼티 값 지정
    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 클래스 안에서 메소드 정의
}

생성자와 프로퍼티 선언하고 enum 상수 정의시 해당 프로퍼티 값을 지정해야한다.

When으로 enum 클래스 다루기

when도 값을 만들어내는 식이다.

fun getMnemonic(color: Color) =
    When (color) {
        Color.RED -> "Richard"
        Color.ORANGE -> "York"
    }

앞의 코드는 color로 전달된 값과 같은 분기를 찾는다. 자바와 달리 각 분기의 끝에 break를 넣지 않아도 된다. 성공적으로 매치되는 분기를 찾으면 그 분기를 실행한다. 한 분기 안에서 여러 값 매치 패턴으로 사용 가능. 그럴 경우 값 사이 , 로 분리

When과 임의의 객체를 함께 사용

코틀린에서 when은 자바의 switch보다 더 강력하다. 분기 조건에서 상수(enum 상수나 숫자 리터러)만 사용할 수 있는 자바 스위치 와 달리
코틀린은 임의 객체 허용한다.

인자 없는 when

분기의 조건ㅇ ㅣ불리언 식이면 인자 없어도 된다. 가독성은 좀 떨어진다...

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

클래스 사인 뒤 : 상속, 구현 클래스

fun eval (e: Expr): Int {
    if (e is Num) {
        val n = e as Num // 타입변환
        return n.value
    }
    if (e is Sum) {
        return eval (e.rght) + eval(e.left) // 변수 e에 대해 스마트 캐스트를 사용한다.
    }
    throw IlleagalArgumentException("Unknown expression")
}

코틀린에서는 is 를 사용해 변수 타입을 검사한다. C#과 비슷. is의 검사는 instatnceof와 비슷하다. 동시에 캐스팅도 해준다. 컴파일러가 대신 캐스팅을 수행해주는거다. 부모크래스로 바뀌어있는 애라도 찐또가 그거면 true 그리고 캐스팅

스마트 캐스팅은 타입을 검사한 다음 그 값이 바뀔 수 없는 경우에만 작동 가능하다. 커스텀 접근자를 사용해서도 안된다.

원하는 타입으로 명시적으로 타입 캐스팅을 하려면 as 키워드를 써야한다.

리팩토링 if를 when

 fun eval (e: Expr) : Int = 
     if (e is Num) {
        e.value
    } else if (e is Sum) {
        eval(e.left) + eval(e.right)
    } else {
        throw IlleagalException
    }
}

if 중첩 대신 when

fun eval (e: Expr) : Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.left) + eval(e.right)
        else -> throw IllegalArgumentException("Unknown expression")
    }

블록으로도 가능

마지막 문장이 결과 값

fun evalWIthLogging(e: Expr) : Int =
    when (e) {
        is Num -> {
            println("num: ${e.value}")
            e.value
        }
        is Sum -> {
            val left = e.left
            val right = e.right
            println("${left} ${right}")
            left + right
        }
        else -> throw IllegalArgumentException()
       }

블록이 값을 만들어 내야하는 경우 값은 마지막 줄에 들어가야한다.

함수에서 블록을 본문으로 가질 경우 return문이 있어야한다.

대상을 이터레이션: while과 for 루프

while은 자바와 같으며 for는 for-each 형태만 존재한다. (원소로 하나 하나 증가시켜 동작하는거 좋았는데...)

코틀린에는 while, do-while이 있다.

자바와 다르지 않다.

for의 경우

초깃값, 증가 값, 최종 값을 사용한 루프를 대신하기 위해 범위를 사용한다.

범위는 봍 우 값의 정수 등 숫자로 이뤄지며 그 수를 연결해 범위를 만든다.

val oneToTen = 1..0

코틀린의 범위는 폐구간 또는 양끝을 포함하는 구간이다. 이건 다른거랑 좀 다르네

범윙에 속한 수를 일정한 순서로 이터레이션하는 것을 수열 ^progression^이라 부른다.


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() {}

무슨 차이일까 밑에껀 블록인 본문을 함수에 집어넣는거고
main() {}은 그냥 정의하는건가... 블럭으로 대입할 때는 반환객체가 무었인지 명시해야한다?

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

for (i in 100 downTo 1 step 2) {
    print(fizzBuzz(i))
}

100에서 1로 역방향을 따른다. downTo 그리고 증가값의 절댓값은 2로 바뀐다.

반만 닫힌 구간으로 만들고 싶으면 until 함수를 사용해라

for (x in 1 until size) {}

맵에 대한 이터레이션

import java.util.TreeMap

fun main() {

    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}")}
}

마지막 for에서는 트리의 내용을 풀어서 진행하네. 맵 안에 넣을 땐 배열의 원소에 넣듯 binaryReps[c] = binary
binaryReps.put(c, binary)라는 코드와 같다.

맵에 대한 구조 분해 구문을 컬렉션에서도 사용할 수 있다. 이를 사용하면 원소의 현재 인덱스를 유지하면서 컬렉션을 이터레이션할 수 있다.

val list = arrayListOf("10", "11", "1001")
for ((index, element) in list.withIndex()) {
    println("${index}: ${element}")
}

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

in 키워드로 어떤 값이 범위나 컬렉션에 들어있는 지 알고싶을 때도 in을 사용한다.

fun isLetter(c: Char) = c in 'a'..'z' || in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'

println(isNotDigit('x'))

이러한 비교 로직은 표준 라이브러리의 범위 클래스 구현 안에 깔끔하게 감춰져 있다.

c in 'a'..'z' = 'a' <= n && n <= 'z'로 변환된다.

in, !in 연산자를 when 식에서 사용해도 된다.

when 에서 in 사용하기

찾고자하는 값 in 범위 결과는 불리언

fun recognize(c: Char) = when (c) {
    in '0'..'9' -> "It's a digit!" // c값이 0부터 9 사이에 있는지 검사한다.
    in 'a'..'z', in 'A'..'Z' -> "It's a letter" // 여러 범위 조건을 함께 사용해도 된다. ,을 통해서 이건 각 범위들을 () 친 다음 and로 묶은거겠지

범위는 문자에만 국한되지 않고. 비교가가능한 클래스(java.lang.Comparable 인터페이스 구현한 클래스라면) 그 클래스의 객체를 사용해 범위를 만들 수 있다.

Comparable을 사용하는 범위의 경우 그 범위 내 모든 객체를 항상 이터레이션 하지는 못한다. 예를 들어 'Java'와 ' Kotlin' 사이의 모든 문자열을 이터레이션 할 수 있을까? 그럴 순 없다. 하지만 in 연산자를 사용하면 값이 범위 안에 속하는 지 항상 결정할 수 있다.

println("Kotlin" in "Java".."Scala") // "Java" <= "Kotlin" && "Kotlin" <= "Scala" 와 같다. 결과가 true인데 참 어렵네

String에 있는 Comparable 구현이 두 문자열을 알파벳 순서로 비교하기 때문에 여기에 있는 in 검사에서도 문자열을 알파벳 순서로 비교한다. 컬렉션에도 마찬가지로 in 연산을 사용할 수 있다.

println("Kotlin" in setOf("Java", "Scala")) // 이 집합엔 "Kotlin"이 없다

함수에 = {}랑 {} 로 선언하는거 뭐가 다른걸까...

코틀린의 예외 처리

자바와 유사하다.

throw는 식이므로 다른 식에 포함될 수 있따.

val percentage =
    if (number in 0..100) number
    else 
        throw IlleagalArgumentException(
            "Aerosmith")

try, catch, finally

자바 와 같게 예외 처리 위해 try, catch, finally 절을 사용한다.

fun readNumber(reader: BufferedReader): Int? { // 함수가 던질 수 있는 예외를 명시할 필요가 없다. (오!!!!!!!)
    try {
        val line = reader.readLine()
        return Integer.parseInt(line)
    }
    catch (e: NumberFormatException) { //예외 타입을 :의 오른쪽에 쓴다.
        return null
    }
    finally { // 자바와 같이 작동한다.
        rrader.close()
    }
}

IOException은 체크 예외라 자바에서는 명시적으로 함수시그니쳐에 적어야했는데 여긴 없다. 최신 JVM언어나 코틀린이은 체크 언체크 에러를 구분하지 않는다. 코틀린에서는 함수가 던지는 에외를 지정하지 않고 바생한 예외를 잡아내도 잡아내지 않아도 된다.

기존 자바에서 체크 에러는 돌려서 막기 식으로 많이해서 그 의미가 제대로 나타나지 못하는 경우가 많았다.

애초에 에러를 처리한다는게 에러 발생 시 이를 부드럽게 다음 단계로 이어나가겠금 해야하는데 실제 에러가 발생하면 처리를 한다해도 그 다음 단계로 넘어가지 못하고 실패해버리는 경우가 많다. 그러니 애초에 에러를 잡아내는 행위가 의미가 크지 않다는 말이였다.

그러니 체크 에러의 의미가 완벽히 작동하는 경우가 보장되지 않는다.

try 를 식으로 사용

try - with -resource를 위한 문법 요소는 없지만 라이브러리 함수로 같은 기능을 구현한다.

finally 절을 없애고 파일에서 읽은 수를 출력하는 코드다.

fun readNumber(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine()) // 이 시긔 값이 try 식의 값이 된다.
    } catch (e: NumberFormatException) {
        return
    }
    println(number)
}

catch에서 값 반환하기

fun readNumber(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine()) // 이 시긔 값이 try 식의 값이 된다.
    } catch (e: NumberFormatException) {
        null
    }
    println(number)
}

try - resouce 어떻게 구현하는건데!!!!!!!!!!!!

요약으로 마무리~

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

댓글