본문 바로가기
kotlin/Kotlin In Action

1. 코틀린 기초

by 유저혀 2021. 6. 10.
반응형

Kotlin IN ACTION 책을 공부하면서 정리한 내용입니다.

http://www.acornpub.co.kr/book/kotlin-in-action

 

Kotlin in Action

이 책은 코틀린 언어를 개발한 젯브레인의 코틀린 컴파일러 개발자들이 직접 쓴 일종의 공식 서적이라 할 수 있다.

www.acornpub.co.kr

 

1.  함수와 변수

 

1.1. Hello, World!

fun main(args: Array<String>) {
  println("Hello, world!")
}
  • 함수를 사용할 때는 fun 키워드를 사용한다.
  • 파라미터 이름 뒤에 파라미터 타입을 쓴다.
  • 함수를 최상위 수준에 정의할 수 있다 (자바와 달리) 꼭 클래스 안에 함수를 넣어야 할 필요가 없다
  • 배열도 일반적인 클래스이다. 코틀린에는 자바와 달리 배열 처리를 위한 문법이 존재하지 않는다
  • 세미콜론(;)을 붙이지 않아도 된다.

 

1.2. 함수

return 타입이 있는 경우 아래와 같이 작성한다

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

 

식이 본문인 함수

위 코드는 if 식 하나로만 이루어져 있다. 이런 경우 중괄호를 없애고 return을 제거하면서 =를 앞에 붙이면 더 간결하게 함수를 표현할 수 있다. 본문이 중괄호로 둘러싸인 함수를 블록이 본문인 함수라고 부르고, 등호와 식으로 이루어진 함수를 식이 본문인 함수라고 부른다.

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

이 때, return 타입을 생략하면 max 함수를 더 간략하게 만들 수 있다.

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

모든 함수는 반환 타입이 정해져야 하지만, 식이 본문인 함수의 경우 굳이 사용자가 리턴 타입을 적지 않아도 컴파일러가 식의 결과 타입을 함수 반환 타입으로 정해준다. 이를 타입 추론(type inference)이라고 한다. 식이 본문인 함수의 반환 타입만 생략 가능하다는 점에 유의해야한다. 블록이 본문인 함수가 값을 반환하면 반드시 리턴 타입을 지정하고 return 문을 사용해 리턴 값을 명시해야 한다.

 

 

1.3. 변수

val answer = 42
val answer: Int = 42

val anwer: Int
answer = 42

원한다면 타입을 명시해도 된다. 타입을 지정하지 않으면 컴파일러가 초기화 식의 타입을 변수 타입으로 지정한다.

초기화 식이 없는 경우에는 반드시 변수 타입을 지정해줘야 한다.

 

val과 var

  • val: value, 변경불가능(immutable) 참조를 저장하는 변수. 초기화한 후 변경 불가. 자바의 final에 해당. val 참조 자체는 불변일지라도 그 참조가 가리키는 객체 내부 값은 변경될 수 있다.
val language = arrayListOf("Java")
language.add("Kotlin")
  • var: variable, 변경가능(mutable) 참조. 값이 바뀔 수 있다. 자바의 일반 변수에 해당. var 키워들르 사용하면 변수 값은 변경할 수 있지만 변수 타입은 고정되어 바뀌지 않는다.
var answer = 42
answer = "no answer" // 컴파일 오류

 


 

2.  클래스와 프로퍼티

/* 자바 */
public class Person {
  private final String name;
  
  public Person(String name){
    this.name = name;
  }
  
  public String getName(){
    return name;
  }
}

 

자바를 코틀린으로 변환한 결과 public이 사라졌음을 알 수 있다. 코틀린은 visible modifier의 default값이 public이므로 이런 경우 생략해도 된다.

/* 코틀린 */
class Person(val name: String)

 

2.1. 프로퍼티

코틀린에서 val로 선언한 프로퍼티는 읽기 전용이며, var로 선언한 프로퍼티는 변경 가능하다.

 

class Person (
  val name: String,  // 읽기 전용 프로퍼티 private 필드와 getter를 만들어 냄
  var isMarried: Boolean  // 쓸 수 프로퍼티, private 필드로 getter, setter를 생성함
)

 

2.2. custom 접근자

class Rectangle(val height: Int, val width: Int){
  val isSquare: Boolean 
    get() = height == width
}

 

 


 

3. enum과 when

when은 자바의 switch를 대치하며 훨씬 더 강력하다.

 

3.1. enum 클래스 정의

자바에서는 enum을 사용하지만 코틀린에서는 enum class를 사용한다.

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

 

enum에서도 일반적인 클래스와 마찬가지로 생성자와 프로퍼티를 선언한다. enum 상수를 정의할 때는 그 상수에 해당하는 프로퍼티 값을 지정해야 한다. 추가로 enum 클래스 안에 메소드를 정의하는 경우 반드시 enum 상수 리스트와 메소드를 구별하기 위해 세미콜론(;)을 넣어야 한다.

 

enum class Color (
  var 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
}

print(Color.BLUE.rgb()) // 255

 

3.2. when으로 enum 클래스 다루기

if 와 마찬가지로 when도 값을 만들어내는 식이다. 따라서 식이 본문인 함수에 when을 바로 사용할 수 있다.

자바와 달리 break를 넣지 않아도 된다.

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"
  }
  
print(getMnemonic(Color.BLUE)) // Battle
  
fun getWarmth(color: Color) = when(color) {
  Color.RED, Color.ORANGE, Color.YELLOW -> "Warm"
  Color.GREEN -> "neutral"
  Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}

print(getWarmth(Color.ORANGE)) // warm

 

상수 값을 모두 임포트 하면 위 코드를 더 간단하게 만들 수 있다.

 

import ch01.color.Color
import ch01.color.Colors.*   // enum 상수 모두 임포트

fun getWarmth(color: Color) = when(color) {
  RED, ORANGE, YELLOW -> "Warm"
  GREEN -> "Neutral"
  BLUE, INDIGO, VIOLET -> "Cold"
}

 

 

3.3. when과 임의의 객체 함께 사용

코틀린에서 when은 자바의 switch보다 강력하다. 분기 조건에 상수만을 사용할 수 있는 자바 switch와는 달리 코틀린 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")
  }
  
print(mix(BLUE, YELLOW)) // GREEN
  

 

3.4. 인자 없는 when 사용

when에 아무 인자도 없으려면 각 분기 조건의 결과가 boolean 결과를 계싼하는 식이어야 한다.

fun mixOptimized(c1: Color, c2: Color) = 
  when {   // 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")
  }
  
 print(mixOptimized(BLUE, YELLOW))  // GREEN

 

 

3.5. 스마트 캐스트

코틀린에서는 is를 사용해 변수 타입을 검사한다. is 검사는 자바의 instanceOf와 비슷하다. 코틀린에서는 어떤 변수가 원하는 타입인지 일단 is로 검사하고 나면 굳이 변수를 원하는 타입으로 캐스팅 하지 않아도 컴파일러가 캐스팅을 수행해준다. 스마트캐스트는 is로 변수에 든 값의 타입을 검사한 다음에 그 값이 바뀔 수 없는 경우에만 작동한다. 예를 들어 클래스의 프로퍼티에 대해 스마트 캐스트를 사용한다면 그 프로퍼티는 반드시 val이어야 하며, custom 접근자를 사용한 것이어도 안된다. val이 아니거나 val이어도 custom 접근자를 사용한 경우에는 해당 프로퍼티에 대한 접근이 항상 같은 값을 내놓는다고 확신할 수 없기 때문이다.

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

 

val n = e as Num

 

 

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

타입을 검사하고 나면 스마트 캐스트가 이뤄진다. 따라서 Num이나 Sum의 멤버에 접근할 때 강제로 캐스팅할 필요가 없다.

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

 

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

 

 

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

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

 


4. while과 for 루프

코틀린의 for는 for <item> in <list>의 형태를 취한다.

.. 연산자는 양끝을 포함하는 리스트를 만들 수 있다. 이는 두 번째 값이 항상 범위에 포함된다는 뜻이다.

 

for (i in 1..100){
  println(i)  // 100도 포함
}

 

step은 증가값을 나타낸다. 증가값을 사용하면 수를 건너 뛸 수 있다. 

아래 예제에서 100 downTo 1은 역방향 수열을 만든다. (역방향수열의 기본 증가값은 -1이다. )

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

 

..는 항상 범위의 끝값을 포함한다. 하지만 끝 값을 포함하지 않는 범위를 만들고 싶다면 until 함수를 사용한다.

예를 들어 (x in 0 until size)라는 루프는 for(x in 0..size-1)과 같다.

 

 

4.1. map에 대한 이터레이션

get과 put을 사용하는 대신 map[key]나 map[key]=value를 사용해 값을 가져오거나 설정할 수 있다.

import java.util.TreeMap

fun main(){
  val binaryReps = TreeMap<Char,String>()
  for(c in 'A'..'F'){
    val binary = Integer.toBinaryString(c.toInt())
    binaryReps[c] = binary
  }
  for((letter,binary) in binaryReps) {
    println("$letter = $binary")
  }
}

 

맵에 사용했던 구조 분해 구문은 맵이 아닌 컬렉션에서도 활용할 수 있다.

val list = arrayListOf("10","11","1001")
for ((index, element) in list.withIndex()) {  // 인덱스와 함께 collection을 iteration한다
  println("$index: $element")
}

 

4.2. in으로 collection이나 범위의 원소 검사

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

 

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 known.."
}

println(recognize('8'))  // It's a digit!

 

 


5. 예외처리

다른 클래스와 마찬가지로 예외 인스턴스를 만들 때도 new를 붙일 필요가 없다. 자바와 달리 코틀린의 throw는 식이므로 다른 식에 포함될 수 있다.

 

val percentage = 
  if(number in 0..100)
    number
  else 
    throw IllegalArgumentException("A percentage value must be between 0 and 100 $number")

 

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

>>> val reader = BufferedReader(StringReader("239"))
>>> println(readNumber(reader))  // 239

 

 

5.2. try를 식으로 사용

코틀린의 try 키워드는 if나 when과 같이 식이다. 따라서 try의 값을 변수에 대입할 수 있지만 if와 달리 try의 본문은 반드시 중괄호 {}로 둘러싸야 한다. 다른 문장과 마찬가지로 try도 본문 내부에 여러 문장이 있으면 마지막 식의 값이 return 값이다.

fun readNumber(reader: BufferedReader): Int? {
  val number = try {
    Integer.parseInt(reader.readLine())
  } catch (e: NumberFormatException) {
    return
  }
}

>>> val reader = BufferedReader(StringReader("239"))
>>> println(readNumber(reader))  // 239

 

728x90

'kotlin > Kotlin In Action' 카테고리의 다른 글

4. 람다로 프로그래밍  (0) 2021.06.17
3. 클래스, 객체, 인터페이스  (0) 2021.06.14
2. 함수 정의와 호출  (0) 2021.06.11

댓글