본문 바로가기

Languages/Kotlin

[Kotlin] 코틀린 기초

코틀린은 타입 선언을 생략해도 되고, 변경 가능한 데이터보다 변경할 수 없은 불변 데이터 사용을 장려한다.

 

 

간단한 코틀린의 특징을 알아보자.

fun main(args: Array<String>){
	println("Hello world!")
}
  • 함수를 선언할 때 fun 키워드를 사용한다. 
  • 파라미터 이름 뒤에 그 파라미터의 타입을 쓴다.
  • 함수를 최상위 수준에 정의할 수 있다. 자바와 달리 꼭 클래스 안에 함수를 넣어야 할 필요가 없다.
  • 배열도 일반적인 클래스와 마찬가지다. 코틀린에는 자바와 달리 배열 처리를 위한 문법이 따로 존재하지 않는다.
    • cf) 코틀린에서는 배열도 일반적인 클래스와 유사한 방식으로 다루어진다. 코틀린에서 배열은 Array 클래스의 인스턴스로 표현되며, 특별한 키워드 없이도 배열을 생성하고 사용할 수 있습니다.
# 코틀린에서 배열을 생성하고 초기화하는 간단한 예제

fun main() {
    // 배열 생성
    val numbers: Array<Int> = arrayOf(1, 2, 3, 4, 5)

    // 배열 요소에 접근
    println(numbers[2]) // 출력: 3

    // 배열 순회
    for (number in numbers) {
        println(number)
    }
}
  • System.out.println 대신에 println이라고 쓴다. 코틀린 표준 라이브러리는 여러가지 표준 자바 라이브러리 함수를 간결하게 사용할 수 있게 감싼 래퍼를 제공한다.
  • 줄 끝에 세미콜론을 붙이지 않아도 된다.

 


1. 함수

fun 함수이름 : (파라미터 : 파라미터 타입) : 반환타입 {
	return 반환 값
}

 

 

 

 

cf)

  • 예제에서 코틀린  if는 값을 만들어내지 못하는 문장이 아니고 결과를 만드는 식 이라는 점이 흥미롭다.
    • 문장(statement): 어떤 동작을 수행하고, 결과를 반환하지 않는 코드 조각입니다. 프로그램의 제어 흐름을 변경하거나 동작을 수행합니다.
    • (expression): 값으로 평가되는 코드 조각으로, 어떤 계산을 수행하고 결과를 반환합니다.
//문장 예제
val x = 10
var y = 5

if (x > y) {
    println("x는 y보다 큽니다.")
} else {
    println("x는 y와 같거나 작습니다.")
}

//위의 코드에서 val x = 10과 var y = 5는 변수에 값을 할당하는 문장이다.
//그리고 if-else 구조도 문장으로, 특정 조건에 따라 다른 동작을 수행한다. 
//이들은 프로그램의 제어 흐름을 변경하거나 어떤 동작을 수행하며, 값으로 평가되지 않는다.


//식 예제
val sum = x + y
val product = x * y

//여기서 x + y와 x * y는 표현식으로, 값을 계산하고 그 값을 변수에 할당한다.
//이들은 값으로 평가되는 코드 조각이기 때문에 식이다.

 

 

식이 본문인 함수

 

함수를 더 간결하게 표현할 수도 있다. 앞의 함수 본문은 if식 하나로만 이뤄져 있다. 이럴 경우 다음과 같이 중괄호를 없애고 return을 제거하면서 등호를 식 앞에 붙이면 더 간결하게 함수를 표현할 수 있다.

 

본문이 중괄호로 둘러싸인 함수를 블록이 본문이 함수라 부르고, 등호와 식으로 이뤄진 함수를 식이 본문인 함수라고 부른다.

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

코틀린에서는 식이 본문인 함수가 자주 쓰인다.

 

 

반환 타입을 생략하면 함수를 더 간략하게 만들 수 있다.

 

여기서 반환 타입을 생략할 수 있는 이유는 무엇일까? 

코틀린은 정적 타입 지정 언어이므로 컴파일 시점에 모든 식의 타입을 지정해야하지 않나?

 

실제로 모든 변수나 모든 식에는 타입이 있으며, 모든 함수는 반환 타입이 정해져야한다. 하지만 식이 본문인 함수의 경우 굳이 사용자가 반환 타입을 적지 않아도 컴파일러가 함수 본문 식을 분석해서 식의 결과 타입을 함수 반환 타입으로 정해준다. 이렇게 컴파일러가 타입을 분석해 프로그래머 대신 프로그램 구성 요소의 타입을 정해주는 기능을 타입 추론이라한다.

 

이때, 식이 본문인 함수의 반환 타입만 생략 가능하다는 점에 유의해야한다. 블록이 분문인 함수가 값을 반환한다면 반드시 반환 타입을 지정하고 reuturn 문을 사용해 반환 값을 명시해야한다. 

 

 


2. 변수

자바에서는 변수를 선언할 때 타입이 맨 앞에 온다. 코틀린에서는 타입 지정을 생략하는 경우가 흔하다. 타입으로 변수 선언을 시작하면 타입을 생략할 경우 식과 변수 선언을 구별할 수 없다. 그런 이유로 코틀린에서는 키워드로 변수 선언을 시작ㅎ라는 대신 변수 이름 뒤에 타입을 명시하거나 생략하게 허용한다.

val answer = 42 //타입 표기를 생략했지만 원한다면 타입을 명시해도 된다.
val answer:Int=42

 

식이 본문인 함수에서와 마찬가지로 타입을 지정하지 않으면 컴파일러가 초기화 식을 분석해서 초기화 식의 타입을 변수 타입으로 지정한다. 초기화 식을 사용하지 않고 변수를 선언하려면 변수 타입을 반드시 명시해야한다.

 

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

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

가능한 모든 변수를 val을 사용해 불변 변수로 선언하고, 나중에 꼭 필요할 때에만 var로 변경해야한다.

변경 불가능한 참조와 변경 불가능한 객체를 부수 효과가 없는 함수와 조합해 사용하면 코드가 함수형 코드에 가까워진다.

val 변수는 블록을 실행할 때 정확히 한 번만 초기화돼야 한다. 하지만 어떤 블록이 실행될 때 오직 한 초기화 문장만 실행됨을 컴파일러가 확인할 수 있다면 조건에 따라 val 값을 다른 여러 값으로 초기화할 수도 있다.

 

 

val 참조 자체는 불변일지라도 그 참조가 가리키는 객체의 내부 값은 변경될 수 있다.

 

 

var 키워드를 사용하면 변수의 값을 변경할 수 있지만 변수의 타입은 고정돼 바뀌지 않는다. 예를 들어 다음 코드는 컴파일할 수 없다.

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

컴파일러는 변수 선언 시점의 초기화 식으로부터 변수의 타입을 추론하며, 변수 선언 이후 변수 재대입이 이뤄질 때는 이미 추론한 변수의 타입을 염두에 두고 대입문의 타입을 검사한다.

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

 

 


3. 클래스

// 자바 클래스 예제
public class Person {
    private String name;
    private int age;

    // 생성자
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getter 및 Setter 메서드
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}


// 코틀린 클래스 예제
data class Person(val name: String, val age: Int)

 

자바 클레스는 필드가 둘 이상으로 들어나면 생성자인 person의 본문에서 파라미터를 이름이 같은 필드에 대입하는 대입문의 수도 늘어난다.

코틀린에서는 그런 필드 대입 로직을 훨씬 더 적은 코드로 작성할 수 있다.

 


프로퍼티

클래스라는 개념의 목적은 데이터를 캡슐화하고 캡슐화한 데이터를 다루는 코드를 한 주체 아래 가두는 것이다. 자바에서는 데이터를 필드에 저장하며, 멤버 필드의 가시성은 보통 비공개다. 클래스는 자신을 사용하는 클라이언트가 그 데이터에 접근하는 통로로 쓸 수 있는 접근자 메서드(세터, 게터)를 제공한다. 자바에서는 필드와 접근자를 한데 묶어 프로퍼티라고 부르며, 프로퍼티라는 개념을 활용하는 프레임워크가 많다. 코틀린은 프로퍼티를 언어 기본 기능으로 제공하며, 코틀린 프로퍼티는 자바의 필드와 접근자 메서드를 완전히 대신한다. val로 선언한 프로퍼티는 읽기 전용이며, var로 선언한 프로퍼티는 변경 가능하다.

 

 

코틀린에서 프로퍼티를 선언하는 방식은 프로퍼티와 관련 있는 접근자를 선언하는 것이다.

코틀린에서는 게터를 호출하는 대신 프로퍼티를 직접 사용한다. 로직은 동일하지만 코드는 더 간결해졌다.

 


커스텀 접근자

직사각형 클래스인 Rectangle을 정의하면서 자신이 정사각형인지 알려주는 기능을 만들어보자. 

직사각형이 정사각형인지를 별도의 필드에 저장할 필요가 없다. 사각형의 너비와 높이가 같은지 검사하면 정사각형 여부를 그때그때 알 수 있다.

 

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

 

isSquare 프로퍼티에는 자체 값을 저장하는 필드가 필요 없다. 이 프로퍼티에는 자체 구현을 제공하는 게터만 존재한다. 클라이언트가 프로퍼티에 접근할 때마다 게터가 프로퍼티 값을 매번 다시 계산한다.

블록을 본문으로 하는 복잡한 구문을 꼭 사용하지 않아도 좋다. 이런 경우 get()=height==width라고 해도된다.

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

 

이 접근자를 자바에서 사용하려면 isSquare 메소드를 호출하면 된다.

 


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

모든 코틀린 파일의 맨 앞에 package문을 넣을 수 있다. 그러면 그 파일 안에 있는 모든 선언이 해당 패키지에 들어간다.

  • 같은 패키지에 속해 있다면 다른 파일에서 정의한 선언일지라도 직접 사용할 수 있다.
  • 다른 패키지에 정의한 선언을 사용하려면 임포트를 통해 선언을 불러와야한다. 

 

 

다른 패키지에 있는 함수 임포트하기

패키지 이름 뒤에 .*를 추가하면 패키지 안의 모든 선언을 임포트할 수 있다. 이런 스타 임포트를 사용하면 패키지 안에 있는 모든 클래스뿐 아니라 최상위에 정의된 함수나 프로퍼티까지 모두 불러온다.

 

자바와 코틀린 패키지와 디렉토리 차이

  • 자바
    • 디렉터리 구조가 패키지 구조를 그대로 따라야한다 
    • 패키지의 구조와 디렉터리 계층 구조를 만들고 클래스의 소스코드를 그 클래스가 속한 패키지와 같은 디렉터리에 위치시켜야한다.
    • 예를들어 shapes라는 패키지 안에 일부 클래스가 들어있다면 각각의 클래스를 자신의 이름과 똑같은 자바 파일로 저장하되 그 모든 파일을 shapes 디렉터리 안에 넣어야한다.

  • 코틀린
    • 패키지 구조와 디렉터리 구조가 맞아 떨어질 필요는 없다.
    • 여러 클래스를 한 파일에 넣을 수 있고 파일의 이름도 마음대로 정할 수 있다.
    • 예를들어 geometry.shapes라는 패키지가 있다면 그 패키지의 모든 내용을 shapes.kt 라는 파일에 넣고, 하위 패키지에 해당하는 별도의 디렉터리를 만들지 않고 geometry라는 폴더 안에 shapes.kt를 넣어도 된다.

하지만 대부분의 경우 자바와 같이 패키지별로 디렉터리를 구성하는 편이 낫다. 특히 자바와 코틀린을 함께 사용하는 프로젝트에서는 자바의 방식을 따르는게 중요하다.

 


선택 표현과 처리 : enum과 when

 

enum

enum은 자바 선언보다 코틀린 선언에 더 많은 키워드를 써야 한다.

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

  • enum(소프트 키워드) : enum은 class 앞에 있을 때는 특별한 의미를 지니지만 다른 곳에서는 이름에 사용할 수 있다.
  • class(키워드) : class라는 이름을 사용할 수 없으므로 클래스를 표현하는 변수 등을 정의할 떄는 clazz나 aClass와 같은 이름을 사용해야 한다.
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 * 256) + (g * 256) + b // enum 클래스 안에서 메서드를 정의한다.
}


print(Color.RED.rgb())

 

enum 클래스 안에 메서드를 정의하는 경우 반드시 enum 상수 목록과 메서드 정의 사이에 세미콜론을 넣어야한다.

 

 

when으로 enum 클래스 다루기

코틀린의 when은 자바의 switch에 해당한다.

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

fun getMnemonic(color:Color) = //함수의 반환 값으로 when 식을 직접 사용한다.
   when(color){ // 색이 특정 enum 상수와 같을 때 그 상수에 대응하는 문자열을 돌려준다.
      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" // 한 when 분기 안에 ,로 여러 값을 사용할 수 있다.
        Color.GREEN -> "neutral"
        Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
    }

 

위의 예제는 Color.YELLOW 처럼 Color라는 enum 클래스 이름을 enum 상수 이름 앞에 붙인 전체 이름을 사용했다. 상수 값을 임포트하면 이 코드를 더 간단하게 만들 수 있다.

import geometry.colors.Color // 다른 패키지에서 정의한 Color 클래스를 임포트한다.
import geometry.colors.Color.* // 짧은 이름으로 사용하기 위해 enum 상수를 모두 임포트한다.

fun getWarmth(color:Color) = when(color){
    RED,ORANGE,YELLOW ->"warm" // 임포트한 enum 상수를 이름만으로 사용한다.
    GREEN -> "nentral"
    BLUE, INDIGO, VIOLET -> "cold"
}

 

 

분기 조건에 상수(enum 상수나 숫자 리터럴)만을 사용할 수 있는 자바 switch와 달리 코틀린의 when의 분기 조건은 임의의 객체를 허용한다.

fun mix(c1:Color, c2:Color) =
    when(setOf(c1,c2)){ //when 식의 인자로 아무 객체나 사용할 수 있다.
        setOf(RED,YELLOW) -> ORANGE // 두 색을 혼합해서 다른 색을 만들 수 있는 경우를 열거한다.
        setOf(YELLOW,BLUE) -> GREEN
        setOf(BLUE,VIOLET) -> INDIGO
        else -> throw Exception("Dirty color") // 매치되는 분기 조건이 없으면 이 문장을 실행한다.
    }

fun main(args:Array<String>){
    print(mix(BLUE,YELLOW))
}

 

이 함수는 호출될 때 마다 함수 인자로 주어진 두 색이 when의 분기 조건에 있는 다른 두 색과 같은지 비교하기 위해 여러 Set인스턴스를 생성한다. 보통은 이런 비효율성이 크게 문제가 되지 않지만 이 함수가 아주 자주 호출된다면 불필요한 가비지 객체가 늘어나는 것을 방지하기 위해 함수를 고쳐 쓰는 편이 낫다. 인자가 없는 when 식을 사용하면 불필요한 객체 생성을 막을 수 있다.

 

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

 

when에 아무 인자도 없으려면 각 분기의 조건이 불리언 결과를 계산하는 식이어야 한다. mixOptimized는 추가 객체를 만들지 않는다는 장점이 있지만 가독성은 더 떨어진다.

 


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

 

interface Expr
class Num(val value:Int) : Expr // value라는 프로퍼티만 존재하는 단순한 클래스로 Expr 인터페이스를 구현한다.
class Sum(val left:Expr, val right: Expr) : Expr // Expr 타입의 객체라면 어떤 것이나 Sum 연산의 인자가 될 수 있다. 따라서 Num이나 다른 Sum이 인자로 올 수 있다.

 

Sum은 Expr의 왼쪽과 오른쪽 인자에 대한 참조를 left와 right 프로퍼티로 저장한다.

이 예제에서 left와 right는 각각 Num이나 Sum일 수 있다.

(1+2) + 4 라는 식을 저장하면 Sum(Sum(Num(1),Num(2)), Num(4)) 라는 구조의 객체가 생긴다.

 

위의 Expr 인터페이스에는 두 가지 구현 클래스가 존재한다. 따라서 식을 평가하려면 두 가지 경우를 고려해야한다.

  • 어떤 식이 수라면 그 값을 반환한다.
  • 어떤 식이 합계라면 좌항과 우항의 값을 계산한 다음에 그 두 값을 합한 값을 반환한다.
// 자바 스타일로 작성한 함수
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) // 변수 e에 대해 스마트 캐스트를 사용한다.
    }
    throw IllegalArgumentException("UnKnown expression")
}

fun main(args : Array<String> ){
    print(eval(Sum(Sum(Num(1),Num(2)), Num(4))))
}

 

코틀린에서는 is를 사용해 변수 타입을 검사한다. is 검사는 자바의 instanceof와 비슷하다.

  • 자바 : 어떤 변수의 타입을 instanceof로 확인한 다음에 그 타입에 속한 멤버에 접근하기 위해서는 명시적으로 변수 타입을 캐스팅해야한다. 이런 멤버 접근을 여러 번 수행해야 한다면 변수에 따로 캐스팅한 결과를 저장한 후 사용해야 한다.
  • 코틀린 : 컴파일러가 캐스팅을 해준다. 어떤 변수가 원하는 타입인지 일단 is로 검사하고 나면 굳이 변수를 원하는 타입으로 캐스팅하지 않아도 마치 처음부터 그 변수가 원하는 타입으로 선언된 것처럼 사용할 수 있다. 이를 스마트 캐스트라고 부른다.

eval 함수에서 e의 타입이 Num인지 검사한 다음 부분에서 컴파일러는 e의 타입을 Num으로 해석한다. 그렇기 때문에 Num의 프로퍼티인 value를 명시적 캐스팅 없이 e.value로 사용할 수 있다.

IDE를 사용하면 스마트 캐스트 부분의 배경색을 달리 표시해주므로 이런 변환이 자동으로 이뤄졌음을 쉽게 알 수 있다.

 

스마트 캐스트는 is로 변수에 든 값의 타입을 검사한 다음에 그 값이 바뀔 수 없는 경우에만 작동한다.

예를 들어 앞에서 본 예제처럼 클래스의 프로퍼티에 대해 스마트 캐스트를 사용한다면 그 프로퍼티는 반드시 val 이여야 하며 커스텀 접근자를 사용한 것이어도 안된다. 

val이 아니거나 val 이지만 커스텀 접근자를 사용하는 경우에는 해당 프로퍼티에 대한 접근이 항상 같은 값을 내놓는다고 확신할 수 없기 때문이다.

 


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

위 코드를 코틀린 스타일로 변경해보자

 

1. 값을 만들어내는 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의 분기에 식이 하나밖에 없다면 중괄호를 생략해도 된다.

if 분기에 블록을 사용하는 경우 그 블록의 마지막 식이 그 분기의 결과 값이다.

이 코드를 when 을 사용해 더 다듬을 수도 있다.

 

 

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

타입을 검사하고 나면 스마트 캐스트가 이뤄진다.

 

 

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

if나 when의 각 분기에서 수행해야 하는 로직이 복잡해지면 분기 본문에 블록을 사용할 수 있다. 

그런 경우 블록의 마지막 문장이 블록 전체의 결과가 된다. 

예제로 봤던 eval 함수에 로그를 추가하고 싶다면 각 분기를 블록으로 만들고 블록의 맨 마지막에 그 분기의 결과 값을 위치시키면 된다.

 

 

분기에 복잡한 동작이 들어가 있는 when 사용하기 

fun evalWithLogging(e:Expr) :Int =
    when(e) {
        is Num -> {
            println("num: ${e.value}")
            e.value // 이 식이 블록의 마지막 식이므로 e의 타입이 Num이면 e.value가 반환된다.
        }
        is Sum -> {
            val left = evalWithLogging(e.left)
            val right = evalWithLogging(e.right)
            println("sum: $left + $right")
            left + right // e의 타입이 Sum이면 이 식의 값이 반환된다.
        }
        else -> throw IllegalArgumentException("UnKnown expression")
    }

 

 


while과 for 루프

1. while 루프

코틀린의 while 루프는 자바와 똑같다.

 

 

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

코틀린에는 자바의 for 루프에 해당하는 요소가 없다. 이런 루프의 가장 흔한 용례인 초깃값, 증가 값, 최종 값을 사용한 루프를 대신하기 위해 코틀린에서는 범위를 사용한다.

val oneToTen = 1..10

코틀린의 범위는 폐구간(닫힌 구간) 또는 양끝을 포함하는 구간이다.

만약 끝 값을 포함하지 않는 반만 닫힌 범위를 만들고 싶다면 until 함수를 사용하면된다.

 

 

when을 사용해 피즈버즈 게임 구현하기

fun fizzBuzz(i:Int) = when{
    i % 15 == 0 -> "FizzBuzz " // i 가 15로 나눠 떨어지면 FizzBuzz를 반환한다.
    i % 3 == 0 -> "Fizz " // i 가 3로 나눠 떨어지면 Fizz를 반환한다.
    i % 5 == 0 -> "Buzz " // i 가 5로 나눠 떨어지면 Buzz를 반환한다.
    else -> "$i " // 다른 경우에는 그 수 자체를 반환한다.
}

fun main(args : Array<String> ){
    for(i in 1..100){
        print(fizzBuzz(i))
    }
	//1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz Fizz 22 23 Fizz Buzz 26 Fizz ...


	// 증가 값을 갖고 범위 이터레이션하기
    for(i in 100 downTo 1 step 2){
            print(fizzBuzz(i))
        }
	//Buzz 98 Fizz 94 92 FizzBuzz 88 86 Fizz 82 Buzz Fizz 76 74 Fizz Buzz 68 Fizz 64 62 FizzBuzz 58
    
    
    // 끝 값을 포함하지 않는 반만 닫힌 범위
    for(i in 0 until 100){
        print(i)
    }
    //0 1 2 3 4 5 6 7 8 9

 

 

맵에 대한 이터레이션

import java.util.*

fun main(args : Array<String>){
    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에 이르는 문자를 모두 포함하는 범위를 만든다.
  • for루프를 사용해 이터레이션하려는 컬렉션의 원소를 푸는 방법을 보여준다.

 

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

 

 

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

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

fun main(args : Array<String>){
    println(isLetter('q')) //true
    println(isNotDigit('x')) //true
}

 

 

when에서 in 사용하기

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

fun main(args : Array<String>){
    println(recognize('8')) //It's a digit!
}

범위는 문자에만 국한되지 않는다. 비교가 가능한 클래스라면 그 클래스의 인스턴스 객체를 사용해 범위를 만들 수 있다.

println("Kotlin" in "Java".."Scala") // 'Java' <= 'Kotlin' && 'Kotlin' <= 'Scala' 와 같다. Java와 Scala 범위에 Kotlin 이 속하는지 체크
println("Kotlin" in setOf("Java","Scala")) // 집합에 속하는지 체크

 

 


코틀린의 예외 처리

코틀린의 예외처리는 자바나 다른 언어의 예외 처리와 비슷하다.

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

 

 

 

try, catch, finally

자바와 마찬가지로 예외를 처리하려면 try와 try, catch, finally절을 함께 사용한다.

 

자바 코드와 가장 큰 차이는 throws 절이 코드에 없다는 점이다.

자바에서는 함수를 작성할 때 함수 선언 뒤에 체크 예외인 throws IOException을 붙여야한다. 어떤 함수가 던질 가능성이 있는 예외나 그 함수가 호출한 다른 함수에서 발생할 수 있는 예외를 모두 catch로 처리해야 하며, 처리하지 않은 예외는 throws 절에 명시해야한다.

 

코틀린은 체크 예외와 언체크 예외를 구별하지 않는다. 

 

 

try를 식으로 사용하기

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


val reader = BufferedReader(StringReader("dd"))
readNumber(reader) //아무것도 출력되지 않는다.

코틀린의 try키워드는 if와 when과 마찬가지로 식이기 때문에 변수에 대입할 수 있다.

if와 달리 try의 본문을 반드시 중괄호 {}로 둘러싸야 한다. 다른 문장과 마찬가지로 try의 본문도 내부에 여러 문장이 있으면 마지막 식의 값이 전체 결과 값이다.

 

 

catch에서 값 반환하기

fun readNumber(reader : BufferedReader){
    val number = try{
        Integer.parseInt(reader.readLine())
    }catch (e:NumberFormatException){
        null // 예외가 발생하면 null 값을 사용한다.
    }
    println(number)
}

val reader = BufferedReader(StringReader("dd"))
readNumber(reader) // null

 

 

 

요약

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

'Languages > Kotlin' 카테고리의 다른 글

[Kotlin] 1장. 코틀린이란 무엇이며, 왜 필요한가?  (0) 2023.11.05