이 글은 새로 배우면서 익히는 정보들을 기억하기 위한 글이며, 아래 내용들은 "kotlin IN ACTION" 책에서 발췌해온 내용과 개인적인 생각을 넣은 내용입니다.

문자열 나누기

자바 split 메서드로는 점(.)을 이용하여 문자열을 분리할 수 없다. 그 이유는 split의 구분 문자열은 실제로 정규식이기 때문이다.
정규식에서는 마침표(.)가 모든 문자를 나타낸다.

코틀린에서는 자바의 split 대신에 여러 가지 다른 조합의 파라미터를 받는 split 확장 함수를 제공함으로써 혼동을 야기하는 메서드를 감춘다. 정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받는다.

>>> println("12.345-6.A".split("\\. |-".toRegex()) // 정규식을 명시적으로 만든다.
[12, 345, 6, A]

코틀린의 정규식 문법은 자바와 똑같다. 뿐만 아니라 꼭 정규식을 사용할 필요 없이 split 확장 함수를 이용하여 하나 이상의 인자를 넣을 수 있다.

>> println(12.345-6.A".split(".", "-")) // println(12.345-6.A".split('.', '-'))
[12, 345, 6, A]

문자열이나 문자를 넣어도 마찬가지 결과가 나온다.

- kotlin IN ACTION 129 ~ 130p -

정규식과 3중 따옴표로 묶은 문자열

val path = "/Users/yole/kotlin-book/chapter.adoc"

위의 전체 경로 명을 String 확장 함수를 통해 구분 할 수 있다.

val directory = path.substringBeforeLast("/") // /Users/yole/kotlin-book
val fullName = path.substringAfterLast("/") // chapter.adoc
val fileName = fullName.substringBeforeLast(".") // chapter
val extension = fullName.substringAfterLast(".) // adoc

3중 따옴표 문자열을 사용하여 정규식을 만들수 있다.

val regex = """(.+)/(.+)\.(.+)""".toRegex()

3중 따옴표 문자열은 역슬래시(\)를 포함한 어떤 문자도 이스케이프할 필요가 없다. 마침표 기호를 이스케이프 하려면 \\. 라고 써야 하지만, 3중 따옴표 문자열에서는 \.라고 쓰면 된다.

- kotlin IN ACTION 130 ~ 133p -

'개발 언어 > Kotlin' 카테고리의 다른 글

Kotlin 함수 정의와 호출  (0) 2022.01.11
Kotlin 기초  (0) 2021.12.31
이 글은 새로 배우면서 익히는 정보들을 기억하기 위한 글이며, 아래 내용들은 "kotlin IN ACTION" 책에서 발췌해온 내용과 개인적인 생각을 넣은 내용입니다.

이름 붙인 인자

코틀린에서는 함수를 호출할 때 파라미터 명을 지정할 수 있다.

fun <T> joinToString(collection: Collection<T>, separator: String, prefix: String, postfix: String): String {
    val result = StringBuilder(prefix)

    for((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }

    result.append(postfix)
    return result.toString()
}

>>> println(joinToString(collection = collection, separator = ",", prefix = "(", postfix=")"))

위와 같이 파라미터 명을 지정할 수 있으며 파라미터 명을 하나라도 지정했을 경우 혼동을 막기 위해 모든 인자 값의 파라미터 명을 지정해줘야 한다.

- kotlin IN ACTION 107 ~ 108p -

디폴트 파라미터 값

자바에서는 일부 클래스에서 오버로딩한 메서드가 너무 많아진다는 문제가 있다. java.lang.Thread에 있는 8가지 생성자를 살펴보라.

코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있으므로 이런 오버로드 중 상당수를 피할 수 있다.

fun <T> joinToString(
collection: Collection<T>, 
separator: String = ", ", 
prefix: String = "", 
postfix: String = ""
): String

>>> println(joinToString(collection = collection))

위의 호출 예시처럼 컬렉션만 인자 값으로 넣어도 나머지 파라미터는 디폴트 값을 사용할 수 있다.

함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아니라 함수 선언 쪽에서 지정된다는 사실을 기억하라.

자바에는 디폴트 파라미터 값이라는 개념이 없어서 코틀린 함수를 자바에서 호출하는 경우에는 그 코틀린 함수가 디폴트 파라미터 값을 제공하더라도 모든 인자를 명시해야 한다.
그럴 때 @JvmOverloads 애노테이션을 함수에 추가할 수 있다. @JvmOverloads를 함수에 추가하면 코틀린 컴파일러가 자동으로 맨 마지막 파라미터로부터 하나씩 생략한 오버로딩한 자바 메서드를 추가해준다.

- kotlin IN ACTION 109 ~ 110p -

정적인 유틸리티 클래스 없애기: 최상위 함수

자바를 아는 사람은 객체지향 언어인 자바에서는 모든 코드를 클래스의 메서드로 작성해야 한다는 사살을 알고 있다. 보통 그런 구조는 잘 작동한다. 하지만 실전에서는 어느 한 클래스에 포함시키기 어려운 코드가 많이 생긴다. 일부 연산에는 비슷하게 중요한 역할을 하는 클래스가 둘 이상 있을 수도 있다. 중요한 객체는 하나뿐이지만 그 연산을 객체의 인스턴스 API에 추가해서 API를 너무 크게 만들고 싶지는 않은 경우도 있다.

코틀린에서는 이런 무의미한 클래스가 필요 없다. 대신 함수를 직접 소스 파일 최상위 수준, 모든 다른 클래스의 밖에 위치시키면 된다.

아래 예시는 join.kt에 최상위 메서드를 만든 예시이다.

package strings

fun joinToString(...) : String { ... }

자바로 변환하면 아래와 같다.

package strings;

public clas JoinKt {
    public static String joinToString(...) { ... }
}

코틀린 join.kt파일을 컴파일한 결과와 같은 클래스를 자바 코드로 보여준다. 파일 명으로 클래스가 생성되는 것을 볼 수 있다.

최상위 함수가 포함된 클래스의 이름을 바꾸고 싶다면 @JvmName 애노테이션을 사용하면 파일 명을 변경할 수 있다.

@file:JvmName("StringFunctions")

- kotlin IN ACTION 111 ~ 113p -

최상위 프로퍼티

최상위 함수와 마찬가지로 클래스 밖에 프로퍼티를 추가할 수 있다.

상수를 추가하고 싶다면 const val str = ""과 같이 const를 추가하면 상수로 사용할 수 있다.

자바로 변환하면 public static final과 동일하다

- kotlin IN ACTION 113 ~ 114p -

메서드를 다른 클래스에 추가: 확장 함수와 확장 프로퍼티

확장 함수는 소유하고 있는 API가 아니더라도 함수를 추가할 수 있는 기능이다.
그러면 기존 API를 재작성하지 않고도 편리한 기능을 사용할 수 있다.

fun String.lastChar() : Char = this.get(this.length - 1)

위와 같이 String 클래스에 lastChar라는 함수를 추가하여 마지막 자리의 문자를 얻는 기능을 추가하였다.
여기서 String수신 객체 타입이라고 하고 this수신 객체라고 한다.

이런 게 추가된 함수를 사용하면 아래와 같다.

>>> println("Kotlin".lastChar()) // n

String 클래스에 추가된 lastChar 함수를 바로 사용할 수 있다.
뿐만 아니라 수신 객체 멤버에 this 없이 접근할 수도 있다.

fun String.lastChar() : Char = get(length - 1)

단, 함장 함수는 클래스 내부에서만 사용할 수 있는 private, protected와 같은 멤버를 사용할 수 없다.

확장 함수를 사용하기 위해서는 다른 클래스나 함수와 마찬가지로 임포트 해야만 한다. 확장 함수를 정의하자마자 어디서든 그 함수를 쓸 수 있다면 한 클래스에 같은 이름의 확장 함수가 둘 이상 있어서 이름이 충돌하는 경우가 자주 생길 수 있다.

import strings.lastChar
// import strings.* // * 를 사용해도 잘 된다.
val c = "Kotlin".lastChar()

한 파일 안에서 다른 여러 패키지에 속해있는 이름이 같은 함수를 가져와 사용해야 하는 경우 이름을 바꿔서 임포트 하면 이름 충돌을 막을 수 있다.

import strings.lastChar as last
val c = "Kotlin".last()

따라서 임포트 할 때 이름을 바꾸는 것이 확장 함수 이름 충돌을 해결할 수 있는 유일한 방법이다.

- kotlin IN ACTION 115 ~ 117p -

확장 함수로 유틸리티 함수 정의

이전에 만들었던 joinToString을 최종 버전으로 만들어보자.

fun <T> Collection<T>.joinToString( // Collection<T>에 대한 확장 함수
    separator: String, // 디폴트 값 지정
    prefix: String, // 디폴트 값 지정
    postfix: String): String {  // 디폴트 값 지정
   
    val result = StringBuilder(prefix)

    for((index, element) in this.withIndex()) { // this는 수신 객체를 가리킨다. 여기서 T의 원소로 이뤄진 컬렉션이다.
        if (index > 0) result.append(separator)
        result.append(element)
    }

    result.append(postfix)
    return result.toString()
}

확장 함수를 이용해서 Collection에서 바로 사용할 수 있게 하였다.
또한 구체적인 원소 타입을 지정할 수 있다.

fun Collection<String>.join(
    separator: String,
    prefix: String,
    postfix: String) = joinToString(separator, prefix,postfix)

위의 두 예제 모두 다 아래와 같이 사용할 수 있다.

listOf(1, 2, 3).joinToString(separator = "; ", prefix = "(", postfix = ")") // (1; 2; 3)
listOf("one", "two", "three").joint(" ") // one two three

- kotlin IN ACTION 118 ~ 119p -

동적 디스패치와 정적 디스패치

실행 시점에 객체 타입에 따라 동적으로 호출될 대상 메서드를 결정하는 방식을 동적 디스패치(dynamic dispatch)라고 한다. 반면 컴파일 시점에 알려진 변수 타입에 따라 정해진 메소드를 호출하는 방식은 정적 디스패치(static dispatch)라고 부른다. 참고로 프로그래밍 언어 용어에서 '정적'이라는 말은 컴파일 시점을 의미하고, '동적'이라는 말은 실행 시점을 의미한다.

- kotlin IN ACTION 120p -

확장 함수는 오버라이드 할 수 없다.

일반적인 오버라이드부터 확인해보자

open class View {
    open fun click() = println("view clicked"
}

class Button : View {
    override fun click() = println("button clicked")
}

val view: View = Button()
view.click()

button clicked

위의 예제를 보면 알 수 있듯이 View 타입 변수에 대해 click과 같은 일반 메서드를 호출했는데 click을 Button 클래스가 오버라이드 했다면 실제로 Button이 오버라이드 한 click이 호출된다.

하지만 확장 함수는 클래스 외부에 있는 것이기 때문에 오버라이드가 되지 않는다.

fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")

val view: View = Button()
view.showOff()

I'm a view!

view가 가리키는 객체의 실제 타입은 Button이지만, 이 경우 view의 타입이 View이기 때문에 무조건 View의 확장 함수가 호출된다.

일반 함수는 동적으로 결정되지만, 확장 함수는 정적으로 결정된다.

/* 노트 */
어떤 클래스를 확장한 함수와 그 클래스의 멤버 ㅂ함수의 이름과 시그니처가 같다면 확장 함수가 아니라 멤버 함수가 호출된다(멤버 함수의 우선순위가 더 높다). 클래스의 API를 변경할 경우 항상 이를 염두에 둬야 한다. 여러분이 코드 소유권을 가진 클래스에 대한 확장 함수를 정의해서 사용하는 외부 클라이언트 프로젝트가 있다고 하자. 그 확장 함수와 이름과 시그니처가 같은 멤버 함수를 여러분의 클래스 내부에 추가하면 클라이언트 프로젝트를 재 컴파일하는 순간부터 그 클라이언트는 확장 함수가 아닌 새로 추가된 멤버 함수를 허용하게 된다.

- kotlin IN ACTION 120 ~ 122p -

메서드를 다른 클래스에 추가: 확장 프로퍼티

확장 함수와 동일하게 수신 객체를 추가해서 프로퍼티를 만들 수 있다.

val String.lastChar
    get() = get(length - 1)

확장 프로퍼티는 뒷받침하는 필드가 없어서 기본 게터 구현을 제공할 수 없으므로 최소한 게터는 꼭 정의해야 한다. 마찬가지로 초기화 코드에서 계산한 값을 담을 장소가 전혀 없으므로 초기화 코드도 쓸 수 없다.

변경 가능한 프로퍼티는 var로 만들 수 있다.

var StringBuilder.lastChar
    get() = get(length - 1)
    set(value: Char) {
         this.setCharAt(length - 1, value)
    }

자바에서 확장 프로퍼티를 사용하려면 게터나 세터를 명시적으로 호출해야 한다.

- kotlin IN ACTION 122 ~ 124p -

자바 컬렉션 API 확장

코틀린이 자바 라이브러리를 사용하여 더 많은 기능을 제공할 수 있었던 것은 바로 위에서 배운 확장 함수를 이용한 것이다.

- kotlin IN ACTION 124 ~ 125p -

가변 인자 함수: 인자의 개수가 달라질 수 있는 함수 정의

자바에서는 가변 인자를 받기 위해 타입 뒤에 ... 을 붙여서 배열 형태로 담아 준다. 코틀린도 같은 방식이지만 문법적으로 다르다.
코틀린에서는 파라미터 앞에 vararg 변경자를 붙인다.

fun <T> test(vararg values: T) {
    val list = listOf(*values)
    println(list)
}

test("1", "2", "3")

>>> [1, 2, 3]

이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때도 코틀린과 자바 구문이 다르다. 자바에서는 배열을 그냥 넘기면 되지만 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야 한다. 기술적으로는 스프레드 연산자(*)가 그런 작업을 해준다.

이 예제는 스프레드 연산자를 통하면 배열에 들어있는 값과 다른 여러 값을 함께 써서 함수를 호출할 수 있음을 보여준다. 이런 기능은 자바에서는 사용할 수 없다.

- kotlin IN ACTION 126p -

값의 쌍 다루기: 중위 호출과 구조 분해 선언

val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

이 코드는 중위 호출이라는 특별한 방식으로 to라는 일반 메서드를 호출한 것이다. 중위 호출 시에는 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣는다.(이때 객체, 메소드 이름, 유일한 인자 사이에는 공백이 들어가야 한다.)

1.to("one")
1 to "one"

위의 두 호출은 동일하다.
인자가 하나뿐인 일반 메서드나 인자가 하나뿐인 확장 함수에 중위 호출을 사용할 수 있다. 함수를 중위 호출에 사용하게 허용하고 싶으면 infix 변경자를 함수 선언 앞에 추가해야 한다.

infix fun Any.to(other: Any) = Pair(this, other)

Pair는 두 변수를 즉시 초기화할 수 있다. 이런 기능을 구조 분해 선언(destructuring declaration)이라고 부른다.

val (number, name) = 1 to "one"

-> 1 to "one"
-> Pair(1, "one")
-> val (number, name) = 1 to "one"

위와 같이 Pair가 동작한다.

to는 확장 함수이다. to를 사용하려면 타입과 관계없이 임의의 순서쌍을 만들 수 있다. 이는 to의 수신 객체가 제네릭하다는 뜻이다.
1 to "one", "one" to 1, list to list.size() 등의 호출이 모두 잘 동작한다.

fun <K, V> mapOf(vararg values: Pair<K, V>): Map<K, V>

- kotlin IN ACTION 127 ~ 129p -

'개발 언어 > Kotlin' 카테고리의 다른 글

Kotlin 문자열과 정규식 다루기  (0) 2022.01.19
Kotlin 기초  (0) 2021.12.31
이직 후 코틀린을 새로 배우면서 익히는 정보들을 기억하기 위한 글입니다.
아래 내용들은 "kotlin IN ACTION" 책에서 발췌해온 내용과 개인적인 생각을 넣은 내용입니다.

변수의 타입 선언

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

- kotlin IN ACTION 64p -

초기화에 따른 변수 타입 선언

초기화 식을 사용하지 않고 변수를 선언하려면 변수 타입을 반드시 명시해야 한다.
초기화 식이 없다면 변수에 저장될 값에 대해 아무 정보가 없기 때문에 컴파일러가 타입을 추론할 수 없다. 따라서 그런 경우 타입을 반드시 지정해야 한다.

- kotlin IN ACTION 65p -

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

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

- kotlin IN ACTION 65p -

타입 추론의 방법

컴파일러는 변수 선언 시점의 초기화 식으로부터 변수의 타입을 추론하며, 변수 선언 이후 변수 재 대입이 이뤄질 때는 이미 추론한 변수의 타입을 염두에 두고 대입문의 타입을 검사한다.
어떤 타입으로 변환하거나, 값을 변수에 대입할 수 있는 타입으로 강제 형 변환해야 한다.

- kotlin IN ACTION 67p -

클래스의 기본 제어자

자바 클래스의 기본 제어자는 default이지만, 코트린 클래스의 기본 제어자는 public이다.
기본 제어자란 클래스의 각 변수에 제어자를 따로 설정하지 않았을 경우 설정되는 제어자를 말한다.

- kotlin IN ACTION 70p -

프로퍼티

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

코틀린은 프로퍼티를 언어 기본 기능으로 제공하며, 코틀린 프로퍼티는 자바의 필드와 접근자 메서드를 완전히 대신한다. 클래스에서 프로퍼티를 선언할 때는 앞에서 살펴본 변수를 선언하는 방법과 마찬가지로 val이나 var를 사용한다. val로 선언한 프로퍼티는 읽기 전용이며, var로 선언한 프로퍼티는 변경 가능하다.

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


게터와 세터의 이름을 정하는 규칙에는 예외가 있다. 이름이 is로 시작하는 프로퍼티의 게터에는 get이 붙지 않고 원래 이름을 그대로 사용하며, 세터에는 is를 set으로 바꾼 이름을 사용한다.

코틀린에서는 게터와 세터 대신 프로퍼티를 사용할 수 있다. 

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

- kotlin IN ACTION 71 ~ 72p -

커스텀 접근자

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

           or
           get() = height == width
}

클라이언트가 프로퍼티에 접근할 때마다 게터가 프로퍼티 값을 매번 다시 계산한다.

- kotlin IN ACTION 73 ~ 74p -

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

자바 패키지의 경우 하나의 패키지 아래에 여러 파일이 있어서 해당 패키지를 import 하면 하위 파일들에 접근할 수 있다.
코틀린은 한 파일에 여러 개의 클래스를 넣을 수 있어 파일마다 패키지를 지정하여 사용할 수 있다.

만약 자바와 코틀린을 같이 사용하게 된다면 자바 소스코드 구조를 따르는 게 맞다. 자바 방식을 따르지 않으면 자바 클래스를 코틀린 클래스로 마이그레이션 하는데 문제가 발생할 수 있다. 하지만 여러 클래스를 한 파일에 넣는 것을 주저해서는 안된다. 코틀린의 경우 클래스 소스코드 크기가 작은 경우가 자주 있다.

- kotlin IN ACTION 74 ~ 77p -

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), INDOGO(75, 0, 130), VIOLET(238, 130, 238);

    fun rgb() = (r * 256 + g) * 256 + b
}

코틀린에서 유일하게 세미콜론을 사용해야 하는 부분이다.
각 상수를 정의할 때 클래스에 선언한 프로퍼티 값을 지정해야 한다.

- kotlin IN ACTION 78p -

when

코틀린의 if문과 마찬가지고 식이 본문인 함수에 when을 바로 사용할 수 있다.

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.BIOLET -> "Vain"
    }
========================================================================
fun getMnemonic(color: Color) = 
    when(color) {
        Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
        Color.GREEN -> "neutral"
        Color.BLUE, Color.INDIGO, Color.BIOLET  ->  "cold"
    }
========================================================================
import colors.Color

fun getMnemonic(color: Color) = 
    when(color) {
       RED, ORANGE, YELLOW -> "warm"
       GREEN -> "neutral"
       BLUE, INDIGO, BIOLET  ->  "cold"
    }
========================================================================
자바와 달리 코틀린은 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")
    }

set 컬렉션을 이용하여 c1이 RED 거나 YELLOW 둘 중 순서는 상관이 없다. 분기 조건이 맞는 게 없다면 else 구문을 실행한다.
위의 예제는 분기를 검사할 때마다 set 객체가 생성되어 효율적이지 않다. 그렇다고 성능에 큰 영향을 미치지는 않는다.
위의 예제보다 조금 더 효율적인 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")
    }

위의 예제 보다 효율적이긴 하지만 가독성이 떨어진다는 단점이 있다.

- kotlin IN ACTION 77 ~ 86p -

블록이 본문인 함수 Return

"블록의 마지막 식이 블록의 결과"라는 규칙은 블록이 값을 만들어내야 하는 경우 항상 성립한다.

if(e is Num) {
    println("e is Number")
    e.value // 마지막 식이 블록의 결과로 반환이 된다.
}

위의 예제처럼 블록 가장 마지막 식이 결과로 반환이 된다.
식이 본문인 함수는 블록을 본문으로 가질 수 없고 본문인 함수는 내부에 return문이 반드시 있어야 한다.

- kotlin IN ACTION 88 ~ 89p -

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

코틀린 특성 중 자바와 가장 비슷한 것이 이터레이션이다.
코틀린의 for는 C#과 마찬가지로 for <아이템> in <원소들> 형태를 취한다.
코틀린에는 while과 do-while 루프가 있다

while(조건) {
     /*...*/
}

do {
     /*...*/
} while(조건)


코틀린 for문은 범위를 사용한다.
범위는 기본적으로 두 값으로 이뤄진 구간이다. 보통은 그 두 값은 정수 등의 숫자 타입의 값이며, .. 연산자로 시작 값과 끝 값을 연결해서 범위를 만든다.

ex) for (x in 1..10)

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

증가 값을 같은 범위도 있다.

ex) for (x in 100 downTo 1 step 2)

증가 값 step을 갖는 수열에 대해 이터레이션한다. 증가 값을 사용하면 수를 건너뛸 수 있다. 증가 값을 음수로 만들면 정방향 수열이 아닌 역방향 수열을 만들 수 있다. 이 예제에서 100 downTo 1은 역방향 수열을 만든다(역방향 수열의 기본 증가 값은 -1이다)
그 뒤에 step 2를 붙이면 증가 값의 절댓값이 2로 바뀐다(이때 증가 값의 방향은 바뀌지 않는다. 따라서 증가 값은 실제로는 -2와 같다).

또한 마지막 값을 포함하지 않는 범위도 있다.

ex) for(x in 0 until size) == for(x in 0..size-1)

위의 예제에서 size가 100이라고 했을 때 99까지가 범위가 된다.

======================================
for(x in 1..10) print("${x}, ") // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
for(x in 1 until 10) print("${x}, ") // 1, 2, 3, 4, 5, 6, 7, 8, 9, 
for(x in 1..10 step 2) print("${x}, ") // 1, 3, 5, 7, 9,
for(x in 10 downTo 1) print("${x}, ") // 10, 9, 8, 7, 6, 5, 4, 3, 2, 1,
for(x in 10 downTo 1 step 2) print("${x}, ") // 10, 8, 6, 4, 2, 
======================================

- kotlin IN ACTION 90 ~ 92p -

맵에 대한 이터레이션

fun main() {
    val binaryReps = TreeMap<Char, String>()

    for(c in 'A'..'F'){ // 문자 타입도 이터레이션할 수 있다.
        val binary = Integer.toBinaryString(c.toInt())
        binaryReps[c] = binary // mpa[key] = value 형식으로 자바로 바꾸면 map.put(key, value)와 같다.
    }

    for((letter, binary) in binaryReps) { // 객체에 대한 key와 value를 가져올 수 있다.
        println("${letter} = ${binary}")
    }
}

>>> 
A = 1000001
B = 1000010
C = 1000011
D = 1000100
E = 1000101
F = 1000110

===============================================================
fun main() {
    val list = arrayListOf("10", "11", "12")

    for((idx, ele) in list.withIndex()) { // withIndex()를 이용하여 list의 인덱스 값을 구할 수 있다.
        println("${idx}: ${ele}")
    }
}

- kotlin IN ACTION 92 ~ 94p -

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 연산자로 범위 안에 속해 있는지, !in 연산자로 범위 안에 속해 있지 않은지를 확인할 수 있으면 when에도 사용할 수 있다.

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

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

c in 'a'..'z' ===> 'a' <= c && c <= 'z'와 동일

ex) println("Kotlin" in "Java".."Scala") // true
위의 예제에서는 String에 있는 Comparable 구현이 두 문자열을 알파벳 순서로 비교하기 때문에 여기 있는 in 검사에서도 문자열을 알파벳 순서로 비교한다.

ex) println("Kotlin" in setOf("Java", "Scala") // false
이 집합에는 "Kotlin"이 들어있지 않아서 false가 출력된다.

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

- kotlin IN ACTION 94 ~ 95p -

코틀린의 예외 처리

코틀린의 예외 처리는 자바나 다른 언어의 예외처리와 비슷하다. 함수는 정상적으로 종료할 수 있지만 오류가 발생하면 예외를 던질 수 있다. 함수를 호출하는 쪽에서 그 예외를 잡아 처리할 수 있다. 발생한 예외를 함수 호출 단에서 처리하지 않으면 함수 호출 스택을 거슬러 올라가면서 예외를 처리하는 부분이 나올 때까지 예외를 다시 던진다.

자바와 달리 코틀린의 throw는 식이므로 다른 식에 포함될 수 있다.

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

if의 조건이 참이면 percentage 변수가 number의 값으로 초기화되지만, 조건이 거짓이면 초기화 되지 않는다.

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

자바 코드와 가장 큰 차이는 throws(이 경우 s가 붙어있다) 절이 코드에 없다는 점이다. 자바에서는 함수를 작성할 때 함수 선언 뒤에 throws IOexception을 붙여야 한다. 이유는 IOException이 체크 예외(checked exception)이기 때문이다. 자바에서는 체크 예외를 명시적으로 처리해야 한다. 어떤 함수가 던질 가능성이 있는 예외나 그 함수가 호출한 다른 함수에서 발생할 수 있는 예외를 모두 catch로 처리해야 하며, 처리하지 않은 예외는 throws 절에 명시해야 한다.

다른 최신 JVM 언어와 마찬가지로 코튼린도 체크 예외와 언체크 예외를 구별하지 않는다. 코틀린에서는 함수가 던지는 예외를 지정하지 않고 발생한 예외를 잡아내도 되고 잡아내지 않아도 된다. 실제 자바 프로그래머들이 체크 예외를 사용하는 방식을 고려해 이렇게 코틀린 예외를 설계했다.

BufferedReader.close는 IOException을 던질 수 있는데, 그 예외는 체크 예외이므로 자바에서는 반드시 처리해야 한다. 하지만 실제 스트림을 닫다가 실패하는 경우 특별히 스트림을 사용하는 클라이언트 프로그램이 취할 수 있는 의미 있는 동작은 없다. 그러므로 이 IOException을 잡아내는 코드는 그냥 불필요하다.

try도 if와 when과 마찬가지고 본문을 식으로 사용할 수 있다.

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

        println(number
    }

    val reader = BufferedReader(StringReader("239"))
    readNumber(reader)
}

- kotlin IN ACTION 96 ~ 100p -

 

요약

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

'개발 언어 > Kotlin' 카테고리의 다른 글

Kotlin 문자열과 정규식 다루기  (0) 2022.01.19
Kotlin 함수 정의와 호출  (0) 2022.01.11

자바는 Single Underscore만으로 변수를 생성할 수 없다.

int _ = 10;

그러나 Underscore를 포함하여 변수를 생성할 수 있다.

int _10 = 10;
String _a = "a";

이때 Double Underscore로 변수를 생성할 수도 있다.

int __ = 10;

Double Underscore (__)로 변수를 생성할 수 있는 이유는 자바에서 변수명을 정할때 underscore로 시작되거나 포함할 수 있다.

그래서 첫 문자를 underscore로 시작하고 그 뒤에 어떤 문자가 오든 상관 없으니 underscore를 붙여서 변수명으로 사용이 가능하다.

보통 람다식에서 콜백 함수의 파라미터를 사용하지 않을 경우 사용되하는 것으로 보인다.

// underscore 적용
... __ -> {
    System.out.println("test");
});

// 일반 콜백 함수
... res -> {
    System.out.println(res);
});

'개발 언어 > Java' 카테고리의 다른 글

[Java] Enum to Class  (0) 2021.09.07
[Java] 예외 처리  (0) 2021.08.27
[Java] 내부 클래스  (0) 2021.08.27
[Java] 인터페이스  (0) 2021.08.27
[Java] 추상 클래스&추상 메서드  (0) 2021.08.26

+ Recent posts