2 코틀린 기초.md

  • 함수, 변수, 클래스, enum, 프로퍼티를 선언하는 방법

  • 제어 구조

  • 스마트 캐스트

  • 예외 던지기와 예외 잡기

  • 변수, 함수, 클래스, 프로퍼티, 제어 구조, 스마트 캐스트 (타입 검사와 타입 캐스트, 타입 강제 변환을 하나로 엮은 기능), 예외처리

2.1 기본 요소: 함수와 변수

2.1.1 Hello, World!

fun main(args: Array<String>) {
  println("Hello, world!")
}
  • 함수 선언할 때 fun 키워드 사용

  • 파라미터 이름 뒤에 타입을 쓴다.

  • 함수를 최상위 수준에 정의할 수 있다. 클래스 안에 함수를 넣을 필요 없음.

  • 배열도 일반적인 클래스처럼. 배열 처리를 위한 문법이 따로 존재하지 않음

  • System.out.prinln 대신 println. 코틀린은 표준 자바 라이브러리 함수를 간결하게 사용할 수 있게 감싼 래퍼들을 제공함.

  • 세미콜론 붙이지 않음

2.1.2 함수

REPL 에서 실습

$ kotlinc
>>> fun max(a: Int, b: Int): Int {
...   return if (a > b) a else b
... }
>>> println(max(1, 2))
2
  • 문(statement)과 식(expression)의 구분

    • 코틀린에서 if 는 식이지 문이 아니다.

    • 식은 값을 만들어 내며 다른 식의 하위 요소로 계산에 참여할 수 있음

    • 문은 자신을 둘러싸고 있는 가장 안쪽 블록의 최상위 요소로 존재하며 아무런 값을 만들어내지 않는다

    • 반면 대입문은 자바에서는 식이었으나 코틀린에서는 문

식이 본문인 함수

fun max(a: Int, b: Int): Int = if (a > b) a else b
  • 본문이 중괄호로 둘러싸인 함수를 블록이 본문인 함수라 부름

  • 등호와 식으로 이뤄진 함수를 식이 본문인 함수라 부름

  • 인텔리J 아이디어 팁. 이 두 방식의 함수를 서로 변환하는 메뉴가 있음.

  • 반환 타입을 생략

    fun max(a: Int, b: Int) = if (a > b) a else b
  • 타입 추론을 해 주기 때문.

    • type inference: 컴파일러가 타입을 분석해 프로그래머 대신 프로그램 구성 요소의 타입을 정해주는 기능

  • 식이 본문인 함수의 반호나 타입만 생략 가능하다는 점에 유의

  • 블록이 본문인 함수가 값을 반환한다면 반드시 반환 타입을 지정하고 return 문을 사용해 반환 값을 명시해야 함.

2.1.3 변수

  • 자바는 타입이 맨 앞에 온다. (타입을 생략할 경우 식과 변수 선언을 구별할 수 없다.)

  • 코틀린에서는 타입 지정을 생략하는 경우가 흔함. 키워드로 변수 선인 시작. 변수 이름 뒤에 타입을 명시하거나 생략하게 허용.

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

원한다면 타입을 명시해도 됨

val answer: Int = 42

부동소수점 상수를 사용하면 변수 타입은 Double 이 됨

val yearsToComputer = 7.5e6 // 7.5 x 10^6 = 7500000.0

초기화 식을 사용하지 않고 변수 선언하려면 변수 타입 반드시 명시

val answer: Int
answer = 42

초기화 식이 없다면 컴파일러가 타입을 추론할 수 없기 때문에 반드시 지정

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

  • val(value) - 변경 불가능한(immutable) 참조를 저장하는 변수. 자바의 final 변수

  • var(variable) - 변경 가능한(mutable) 참조. 자바의 일반 변수

variable이 형용사로 쓰이면 '변할 수 있는'이라는 뜻. 변수 말 자체가 이미 변화라는 개념이 내재돼 있음. 함수형 프로그래머 중에는 변경 불가능한 변수를 표현하기 위해 변수라는 단어 대신 '값'이나 '이름'이라는 단얼르 사용하기도 함. 문맥에 따라 '값'과 '이름'을 혼용하기도 함.

  • val 을 기본으로 하고 필요할 때에만 var 로 변경

  • val 변수는 한번만 초기화돼야 함

val message: String
if (canPerforOperation()) {
  message = "Success"
  // ...
}
else {
  message = "Failed"
}
  • val 참조 자체는 불변일지라도 그 참조가 가리키는 객체의 내부 값은 변경될 수 있다

    val languages = arrayListOf("Java") // 불변 참조 선언
    languages.add("Kotlin") // 참조가 가리키는 객체 내부 변경
  • var 키워드 사용하면 변수의 값을 변경할 수 있지만 변수의 타입은 고정돼 바뀌지 않음.

  • 아래는 컴파일 오류. Error: type mismatch

    var answer = 42
    answer = "no answer"
  • 어떤 타입의 변수에 다른 타입의 값을 저장하고 싶다면 변환 함수를 써서 값을 변수의 타입으로 변환하거나, 값을 변수에 대입할 수 있는 타입으로 강제 형 변환(coerce)해야 한다. 원시 타입의 변환에 대해서는 6.2.3절에서 다룸

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

fun main(args: Array<String>) {
  val name = if (args.size > 0) args[0] else "Kotlin"
  println("Hello, $name!")
}
  • name 이라는 변수를 선언하고 문자열 리터럴 안에서 그 변수를 사용. 변수 앞에 $ 를 추가

  • 자바의 문자열 접합 연산 ("Hello, " + name + "!")과 동일한 기능을 하지만 좀 더 간결

  • $ escape 하려면 $ 하면 됨

println("\$x")
  • 복잡한 식도 중괄호({})로 둘러싸서 문자열 템플릿 안에 넣을 수 있음.

fun main(args: Array<String>) {
  if (args.size > 0) {
    println("Hello, ${args[0]}!")
  }
}
  • 한글을 문자열 템플릿에서 사용할 경우 주의할 점

    • 변수 이름에 한글이 들어갈 수 있음.

    • '$name님' unresolved reference 오류. ${name} 처럼 중괄호로 감싸는게 좋음

  • 중괄호 식 안에서 큰 따옴표 사용 가능

    fun main(args: Array<String>) {
    println("Hello, ${if (args.size > 0) args[0] else "someone"}!")
    }
  • 3.5절에서 더 많이.

2.2 클래스와 프로퍼티

  • 리스트 2.3 간단한 자바 클래스 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)

2.2.1 프로퍼티

  • 클래스라는 개념의 목적은 데이터를 캡슐화(encapsulate)라고 캡슐화한 데이털르 다루는 코드를 한 주체 아래 가두는 것

  • 자바에서는 데이터를 필드(field)에 저장하며, 멤버 필드의 가시성은 보통 비공개(private)

  • 클래스는 자신을 사용하는 클라이언트가 그 데이터에 접근한느 통로로 쓸 수 있는 접근자 메소드(accessor method)를 제공함.

  • 읽기 위한 게터(getter), 변경할 경우 세터(setter)를 추가 제공

  • 자바에서는 필드와 접근자를 한데 묶어 프로퍼티(property)라고 부름. 프로퍼티라는 개념을 활용하는 프레임워크가 많음. (Lombok 등)

  • 코틀린은 프로퍼티를 언어 기본 기능으로 제공. 코틀린 프로퍼티는 자바의 필드와 접근자 메소드를 완전히 대신함.

    class Person(
    val name: String,      // 비공개 필드, 공개 게터 만들어냄
    var isMarried: Boolean // 비공개 필드, 공개 게터, 공개 세터 만들어냄
    )
  • 리스트 2.6 자바에서 Person 클래스를 사용하는 방법

    >>> Person person = new Person("Bob", true);
    >>> System.out.println(person.getName());
    Bob
    >>> System.out.println(person.isMarried());
    true
  • 리스트 2.7 코틀린에서 Person 클래스 사용하기

    >>> val person = Person("Bob", true) // new 키워드 안씀
    >>> println(person.name) // 코틀린이 자동으로 게터를 호출
    Bob
    >>> println(person.isMarried)
    true

2.2.2 커스텀 접근자

class Rectangle(val height: Int, val width: Int) {
  val isSquare: Boolean
    get() {
      return height == width
    }
}
  • get() = height == width 라고 해도 됨.

>>> val rectangle = Rectangle(41, 43)
>>> println(rectangle.isSquare)
false
  • 파라미터가 없는 함수를 정의하는 방식과 커스텀 게터를 정의하는 방식. 두 방식 모두 비슷. 구현이나 성능상 차이는 없음. 차이는 가독성. 클래스의 특성을 정의하고 싶다면 프로퍼티로 그 특성을 정의해야 함.

2.2.3 코틀린 소스코드 구조: 디렉터리와 패키지

  • 리스트 2.8 클래스와 함수 선언을 패키지에 넣기

    pcakge geometry.shapes
    import java.util.Random
    class Rectangle(val height: Int, val width: Int) {
    val isSquare: Boolean
      get() = height == width
    }
    fun createRandomRectangle(): Rectangle {
    val random = Random()
    return Rectangle(random.nextInt(), random.nextInt())
    }
  • 리스트 2.9 다른 패키지에 있는 함수 임포트하기

    package geometry.example
    import geometry.shapes.createRandomRectangle
    fun main(args: Array<String>) {
    println(createRandomRectangle().isSquare)
    }

2.3 선택 표현과 처리: enum과 when

2.3.1 enum 클래스 정의

  • 리스트 2.10 간단한 enum 클래스 정의하기

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

리스트 2.11 프로퍼티와 메소드가 있는 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

2.32 when으로 enum 클래스 다루기

리스트 2.12 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

리스트 2.13 한 when 분기 안에 여러 값 사용하기

fun getWarmth(color: Color) = when(color) {
  Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
  Color.GREEN -> "neutral"
  Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}
>>> println(getWarmth(Color.ORANGE))
warm

리스트 2.14 enum 상수 값을 임포트해서 enum 클래스 수식자 없이 enum 사용하기

import ch02.colors.Color
import ch02.colors.Color.*

fun getWarmth(color: Color) = when(color) {
  RED, ORANGE, YELLOW -> "warm"
  GREEN -> "neutral"
  BLUE, INDIGO, VIOLET -> "cold"
}

2.3.3 when과 임의의 객체를 함꼐 사용

  • 리스트 2.15 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")
    }
    >>> println(mxi(BLUE, YELLOW))
    GREEN

2.3.4 인자 없는 when 사용

  • 리스트 16 인자가 없는 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 에 아무 인자도 없으려면 각 분기의 조건이 불리언 결과를 계산한느 식이어야 함.

  • mixOptimized 함수는 앞에서 살펴본 mix 함수와 같은 일을 함.

  • mixOptimized는 추가 객체를 만들지 않는다는 장점이 있지만 가독성은 더 떨어짐

  • 이제 when을 사용하는 과정에서 스마트 캐스트가 쓰이는 예를 살펴보자

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

  • 리스트 2.17 식을 표현하는 클래스 계층

    interface Expr
    class Num(val value: Int) : Expr
    class Sum(val left: Expr, val right: Expr) : Expr
    >>> println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))
    7
  • 리스트 2.18 if 연쇄를 사용해 식을 계산하기

    fun eval(e: Expr): Int {
    if (e is Num) {
      val n = e as Num // Num 으로 타입 변환하는데 불필요한 중복임
      return n.value
    }
    if (e is Sum) {
      return eval(e.right) + eval(e.left)
    }
    throw IllegalArgumentException("Unknown expression")
    }
    >>> println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))
    7
if (e is Sum) {
  return eval(e.right) + eval(e.left)
}

그림 2.5 IDE는 배경색으로 스마트 캐스트를 표시해준다. (e.right, e.left 의 e 가 강조 표시)

2.3.6 리팩토링: if를 when으로 변경

  • 리스트 2.19 값을 만들어내는 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")
    }
    >>> println(eval(Sum(Num(1), Num(2))))
    3
  • if의 분기에 식이 하나밖에 없다면 중괄호를 생략해도 됨. if 분기에 블록을 사용하는 경우 그 블록의 마지막 식이 그 분기의 결과 값임.

  • 리스트 2.20 if 중첩 대신 when 사용하기

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

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

  • 리스트 2.21 분기에 복잡한 동작이 들어가 있는 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")
      }

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

2.4.1 while 루프

while (조건) {
}
do {
} while (조건)

2.4.2 수에 대한 이터레이션: 범위와 수열

  • 리스트 2.22 when을 사용해 피즈버즈 게임 구현하기

    ```kotlin

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

### 2.4.3 맵에 대한 이터레이션
* 리스트 2.24 맵을 초기화하고 이터레이션하기
```kotlin
val binaryReps = TreeMap<Char, String>()
for (c in 'A'..'F') {
    val binary = Integer.toBinaryString(c.toInt()) // 아스키코드를 2진 표현으로 바꾼다.
    binaryReps[c] = binary
}

for ((letter, binary) in binaryReps) {
    println("$letter = $binary")
}
binaryReps[c] = binary

라는 코드는

binaryReps.put(c, binary)

라는 자바 코드와 같다.

출력

A = 1000001
B = 1000010
C = 1000011
D = 1000100
E = 1000101
F = 1000110
val list = arrayListOf("10", "11", "1001")
for ((index, element) in list.withIndex()) {
    println("$index: $element")
}
0: 10
1: 11
2: 1001
  • withIndex의 정체에 대해서는 3장에서 살펴본다.

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

  • 리스트 2.25

    fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
    fun isNotDigit(c: Char) = c !in '0'..'0'
>>> println(isLetter('q'))
true
>>> println(isNotDigit('x'))
true
c in 'a'..'z' // 'a' <= c && c <= 'z'로 변환된다.
  • 리스트 2.26

    fun recognize(c: Char) = when (c) {
      in '0'..'9' -> "It's a digit!"
      in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
      else -> "I don't kwno..."
    }
    >>> println(recognize('8'))
    It's a digit!
println("Kotlin" in "Java".."Scala") // true. "Java" <= "Kotlin" && "Kotlin" <= "Scala"
println("Kotlin" in setOf("Java", "Scala")) // false. "Java" <= "Kotlin" && "Kotlin" <= "Scala"

2.5 코틀린의 예외 처리

  • 예외를 던지는 방법

    if (percentage !in 0..100) {
    throw IllegalArgumentException(
      "A percentage value must be between 0 and 100: $percentage")
    }
val percentage =
  if (number in 0..100)
    number
  else
    throw IllegalArgumentException( // "throw"는 식이다.
      "A percentage value must be between 0 and 100: $percentage")
  • 자바와 달리 코틀린의 throw는 식이므로 다른 식에 포함될 수 있다.

  • p282 활용사례

    ```

    fun fail(message: String): Nothing {

    throw IllegalStateException(message)

    }

var address = company.address ?: fail("No_address") println(address.city)

* Nothing 이라는 특별한 반환 타입
* 엘비스 연산자의 우항에 사용해서 전제 조건(precondition)을 검사할 수 있음.
* 만약 자바 였다면 address 에 오류값을 넣고 address 가 오류값이면 fail 함수를 호출하는 코드를 더 작성해야 한다.


### 2.5.1 try, catch, finally

* 리스트 2.27 자바와 마찬가지로 try 사용하기
```kotlin
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

2.5.2 try를 식으로 사용

  • 리스트 2.28 try를 식으로 사용하기

    fun readNumber(reader: BufferedReader) {
      val number = try {
          Integer.parseInt(reader.readLine())
      }
      catch (e: NumberFormatException) {
          return
      }
      println(number)
    }
>>> val reader = BufferedReader(StringReader("not a number"))
>>> readNumber(reader)

아무것도 출력되지 않는다.

  • 리스트 2.29 catch에서 값 반환하기

    fun readNumber(reader: BufferedReader) {
      val number = try {
          Integer.parseInt(reader.readLine())
      }
      catch (e: NumberFormatException) {
          null
      }
      println(number)
    }
>>> val reader = BufferedReader(StringReader("not a number"))
>>> readNumber(reader)

예외가 발생했으므로 함수가 "null"을 출력한다.

2.6 요약

  • 함수를 정의할 때 fun 키워드 사용. val과 var는 각각 읽기 전용 변수와 변경 가능한 변수 선언할 때 쓰임

  • 문자열 템플릿을 사용하면 문자열을 연결하지 않아도 되므로 코드가 간결해진다. 변수 이름 앞에 $를 붙이거나, 식을 ${식}처럼 ${}로 둘러싸면 변수나 식의 값을 문자열 안에 넣을 수 있다.

  • 코틀린에서는 값 객체 클래스를 아주 간결하게 표현할 수 있다.

  • 다른 언어에도 있는 if는 코틀린에서 식이며, 값을 만들어낸다.

  • 코틀린 when은 자바의 switch와 비슷하지만 더 강력하다.

  • 어떤 변수의 타입을 검사하고 나면 굳이 그 변수를 캐스팅하지 않아도 검사한 타입의 변수처럼 사용할 수 있다. 그런 경우 컴파일러가 스마트 캐스트를 활용해 자동으로 타입을 바꿔준다.

  • for, while, do-while 루프는 자바가 제공하는 같은 키워드의 기능과 비슷하다. 하지만 코틀린의 for는 자바의 for보다 더 편리. 특히 맵을 이터레이션하거나 이터레이션하면서 컬렉션의 원소와 인덱스를 함께 사용해야 하는 경우 코틀린의 for가 더 편리

  • 1..5와 같은 식은 범위를 만들어낸다. 범위와 수열은 코틀린에서 같은 문법을 사용하며, for 루프에 대해 같은 추상화를 제공. 어떤 값이 범위 안에 들어 있거나 들어있지 않은지 검사하기 위해 in이나 !in을 사용

  • 코틀린 예외 처리는 자바와 비슷. 다만 코틀린에서는 함수가 던질 수 있는 예외를 선언하지 않아도 됨.

Last updated

Was this helpful?