함수/클래스/객체/인터페이스

Begin Kotlin

코틀린?

  • 타입 추론을 지원하는 정적 타입 지정 언어

    • 소스 코드의 정확성과 성능을 보장

    • 소스코드를 간결하게 유지

  • 객체지향함수형 프로그래밍 스타일을 모두 지원

    • 일급 시민 함수를 사용해 수준 높은 추상화가 가능

    • 불변 값 지원을 통해 다중 스레드 애플리케이션 개발과 테스트를더 쉽게 가능

  • 서버 애플리케이션 개발에 활용 가능

    • 기존 자바 프레임워크를 완벽하게 지원

    • HTML 생성기나 영속화등의 일반적인 작업을 위한 새로운 도구를 제공

  • 실용적이며 안전하고, 간결하며 좋은 상호 운용성

    • NPE 같이 흔히 발생하는 오류를 방지

    • 읽기 쉽고 간결한 코드를 지원

    • 자바와 아무런 제약 없이 통합

코틀린의 컴파일러(Kotlin Compiler)

코틀린 컴파일러는 JVM에서 실행될 수 있는 바이트코드가 포함된 클래스 파일을 생성

  • 코틀린은 자바 컴파일러가 아닌 코틀린 컴파일러에 의해 컴파일되므로 자바를 사용할 때와 다른 부분이 존재

    • ex. 코틀린에서는 모든 예외를 언체크 예외로 인식


기초

fun main(args: Array<String>) {
		println("Hello, world!")
}
  • 함수 선언 → fun

  • 파라미터 이름 뒤에 타입 → args: Array<String>

  • 함수를 최상위수준에 정의 가능

    • 클래스 안에 함수를 넣어야 할 필요가 없음

  • 배열도 일반적인 클래스

  • System.out.println → println

  • 줄 끝에 세미콜론(;) 없이 작성

함수

// 본문인 함수
fun max(a: Int, b: Int): Int {
		return if (a > b) a else b
}

// 식이 본문인 함수
fun max(a: Int, b: Int): Int = if (a > b) a else b

// 반환 타입 생략
fun max(a: Int, b: Int) = if (a > b) a else b

변수

코틀린에서는 보통 타입 지정을 생략

기본적으로는 모든 변수를 val 키워드를 사용해 불변 변수로 선언하고, 나중에 꼭 필요할 때에만 var로 변경

val answer = 42 // 타입 생략
val answer: Int = 42 // 타입 지정

// 초기화 식을 사용하지 않고 변수 선언 시 변수 타입 명시 필요
val answer: Int
answer = 42

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

  • val(=value): 변경 불가능한(immutable) 참조를 저장하는 변수 (like. java final)

  • var(=variable): 변경 가능한(mutable) 참조 (like. java 일반 변수)

문자열 템플릿

  • 문자열 리터럴의 필요한 곳에 변수를 넣되 변수 앞에 $를 추가

  • $ 문자를 문자열에 넣고 싶으면 println("$x")와 같이 \를 사용해 $를 이스케이프

fun main(args: Array<String>) {
		val name = if (args.size > 0) args[0] else "Kotlin"
		println("Hello, $name")
}

클래스와 프로퍼티

코틀린의 기본 가시성은 public

// Java
public class Person {
		private final String name;

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

		public String getName() {
				return name;
		}
}

...

// Kotlin
class Person(val name: String)

코틀린 프로퍼티는 자바의 필드와 접근자 메소드를 완전히 대신

class Person(
		// 읽기 전용 프로퍼티: (비공개)필드와 필드를 읽는 단순한 (공개)Getter를 생성
		val name: String, 
		// 쓸 수 있는 프로퍼티: (비공개)필드, (공개)Getter/Setter를 생성
		var isMarried: Boolean 
)
  • 코틀린에서는 클래스 임포트와 함수 임포트에 차이가 없으며, 모든 선언을 import 키워드로 가져올 수 있음

enum & when

when은 자바의 switch를 대치하되 훨씬 더 강력

enum

  • 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);
		
		// 메소드를 정의하는 경우 enum 상수 목록과 메소드 정의 사이에 세미콜론 필요
    fun rgb() = (r * 256 + g) * 256 + b
}

when 으로 enum 다루기

// 자바와 달리 각 분기 끝에 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"
}

// 갹체를 함께 사용 가능
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")
		}

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

코틀린에서는 is를 사용해 변수 타입을 검사 (like. java instanceof)

fun eval(e: Expr): Int {
    if (e is Num) { // **타입 검사와 타입 캐스트**
        val n = e as Num
        return n.value
    }
    if (e is Sum) { // **타입 검사와 타입 캐스트**
        return eval(e.right) + eval(e.left)
    }
    throw IllegalArgumentException("Unknown expression")
}

다중 if → when

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

Iteration (while & loop)

수에 대한 이터레이션

// 100부터 거꾸로 세되 짝수만으로 게임을 진행
fun main(args: Array<String>) {
    for (i in 100 downTo 1 step 2) {
        print(fizzBuzz(i))
    }
}

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

in으로 어떤 값이 범위에 속하는지 검사

!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…"
}

예외처리

함수는 정상적으로 종료할 수 있지만 오류가 발생하면 예외를 던질 수 있음

단, 함수가 던질 수 있는 예외를 선언하지 않아도 된다

// 조건이 참이면 number 값이 초기화되고, 거짓이면 초기화되지 않고 throw 호출
val number = try {
    Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
    return // 예외가 발생한 경우 catch 블록 다음의 코드는 실행되지 않음 
}

println("number") // 실행 X

try 식으로 사용

  • try 키워드는 if, when 과 마찬가지로 식이다.

  • 따라서 try의 값을 변수에 대입 가능


함수 정의와 호출

컬렉션 만들기

val set = hashSetOf(1, 7, 53)
val list = arrayListOf(1, 7, 53)
val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

...

fun main(args: Array<String>) {
    val strings = listOf("first", "second", "fourteenth")
    println(strings.last()) // 리스트의 마지막 원소
    val numbers = setOf(1, 14, 2)
    println(numbers.max()) // 컬렉션에서 최댓값
}

확장 함수와 확장 프로퍼티

메소드를 다른 클래스에 추가

확장 함수

  • 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수

  • 확장 함수를 만들려면 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이자

  • 확장 함수는 오버라이드할 수 없음

package strings
// 문자열의 마지막 문자를 돌려주는 확장 메소드
fun String.lastChar(): Char = this.get(this.length - 1)

...

// String = 클래스 이름 : 수신 객체 타입(receiver type)
// "Kotlin" = 확장 함수가 호출되는 대상 : 수신 객체(receiver object)
println("Kotlin".lastChar())

임포트와 확장함수

  • 확장 함수를 사용하기 위해서는 그 함수를 다른 클래스나 함수와 마찬가지로 임포트 필요

import strings.lastChar // 명시적으로 사용
import strings.* // * 사용 가능
import strings.lastChar as last // as 키워드를 사용 가능

함수를 호출하기 쉽게 만들기

// AS-IS
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()
}

val list = listOf(1, 2, 3)
println(joinToString(list, "; ", "(", ")"))
println(joinToString(collection = list, separator = ";", prefix = "(", postfix = ")"))
println(joinToString(list))
println(joinToString(list, "; "))

...

// TO-BE : 확장 함수로 유틸리티 함수 정의
fun <T> Collection<T>.joinToString(
        separator: String = ", ",
        prefix: String = "",
        postfix: String = ""
): String {
    val result = StringBuilder(prefix)

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

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

fun main(args: Array<String>) {
    val list = arrayListOf(1, 2, 3)
    println(list.joinToString(" "))
}

val list = listOf(1, 2, 3)
println(list.joinToString2("; ", "(", ")"))
println(list.joinToString2(separator = ";", prefix = "(", postfix = ")"))
println(list.joinToString2())
println(list.joinToString2("; "))

확장 프로퍼티

  • 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가

  • 상태를 저장할 적절한 방법이 없으므로 아무 상태도 가질 수 없음

  • 계산한 값을 담을 장소가 전혀 없으므로 초기화 코드도 사용 불가

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

var StringBuilder.lastChar: Char
    get() = get(length - 1) // 뒷받침하는 필드가 없어서 최소한 게터는 꼭 정의
    set(value: Char) {
        this.setCharAt(length - 1, value)
    }

@Test
fun `확장 프로퍼티`() {
    println("Kotlin".lastChar) // n
    
    val sb = StringBuilder("Kotlin?")
    sb.lastChar = '!'
    println(sb) // Kotlin!
}

가변 인자 함수

메소드를 호출할 때 원하는 개수만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 그 값들을 넣어주는 기능

  • 자바는 타입 뒤에 ...를 붙이지만, 코틀린은 파라미터 앞에 varag 변경자를 붙인다.

public fun <T> listOf(vararg elements: T): List<T> = 
		if (elements.size > 0) elements.asList() else emptyList()

fun main(args: Array<String>) {
    val list = listOf("one", "two", "eight")
}

이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때

  • 자바에서는 배열을 그냥 넘기면 되지만,

  • 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야 한다.

  • 기술적으로는 스프레드(spread) 연산자가 이러한 작업을 수행

fun main(args: Array<String>) {
    val list = listOf("args: ", *args)
}

값의 쌍 다루기 (중위호출, 구조 분해 선언)

맵을 만들려면 mapOf 함수를 사용

val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
  • 중위 호출이라는 특별한 방식으로 to라는 일반 메소드를 호출

  • 중위 호출 시에는 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣는다

1.to("one") // "to" 메소드를 일반적인 방식으로 호출함
1 to "one" // "to" 메소드를 중위 호출 방식으로 호출함

메소드를 중위 호출에 사용하도록 허용하고 싶을 경우 infix 변경자를 메소드 선언 앞에 추가

/**
 * Creates a tuple of type [Pair] from this and [that].
 *
 * This can be useful for creating [Map] literals with less noise, for example:
 * @sample samples.collections.Maps.Instantiation.mapFromPairs
 */
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

이러한 기능을 구조 분해 선언이라고 부른다.

  • Pair 인스턴스 외 다른 객체에도 구조 분해 적용 가능

    • ex) key, value 두 변수를 맵의 원소를 사용해 초기화

for ((index, element) in collection.withIndex()) {
		println("$index: $element")
}

문자열과 정규식

문자열 나누기

split 함수에 전달하는 값의 타입에 따라 정규식이나 일반 텍스트 중 어느 것으로 문자열을 분리하는지 쉽게 알 수 있다.

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

간단한 경우에는 꼭 정규식을 쓸 필요가 없다.

  • split 확장 함수를 오버로딩한 버전 중에는 구분 문자열을 하나 이상 인자로 받는 함수가 있다.

println("12.345-6.A".split(".","-")) // 여러 구분 문자열을 지정한다. 
[12, 345, 6, A]

3중 따옴표 문자열

역슬래시()를 포함한 어떤 문자도 이스케이프 불필요

// AS-IS
fun parsePath(path: String) {
    val directory = path.substringBeforeLast("/")
    val fullName = path.substringAfterLast("/")

    val fileName = fullName.substringBeforeLast(".")
    val extension = fullName.substringAfterLast(".")

    println("Dir: $directory, name: $fileName, ext: $extension")
    // Dir: /Users/yole/kotlin-book, name: chapter, ext: adoc
}

fun main(args: Array<String>) {
    parsePath("/Users/yole/kotlin-book/chapter.adoc")
}

...

// TO-BE
// 정규식을 사용하지 않고도 문자열을 쉽게 파싱
// 정규식이 필요할 때는 코틀린 라이브러리를 사용하면 더 편리
fun parsePath(path: String) {
    val regex = """(.+)/(.+)\\.(.+)""".toRegex()
    val matchResult = regex.matchEntire(path) // 정규식을 인자로 받은 path에 매치
    if (matchResult != null) {
		    // 그룹별로 분해한 매치 결과를 의미하는 destructured 프로퍼티를 각 변수에 대입
        val (directory, filename, extension) = matchResult.destructured
        // 구조 분해 선언은 Pair로 두 변수를 초기화할 때 썼던 구문과 동일
        println("Dir: $directory, name: $filename, ext: $extension")
    }
}

로컬 함수와 확장

함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있다.

문법적인 부가 비용을 들이지 않고도 깔끔하게 코드를 조직 가능

로컬 함수와 확장으로 코드를 다듬는 과정

// AS-IS
fun saveUser(user: User) {
    if (user.name.isEmpty()) {
        throw IllegalArgumentException(
            "Can't save user ${user.id}: empty Name")
    }

    if (user.address.isEmpty()) {
        throw IllegalArgumentException(
            "Can't save user ${user.id}: empty Address")
    }

    // Save user to the database
}

...

// 첫 번째 개선
fun saveUser(user: User) {
    fun validate(user: User,
                    value: String,
                    fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException(
                "Can't save user ${user.id}: empty $fieldName")
        }
    }

    validate(user, user.name, "Name")
    validate(user, user.address, "Address")
    // Save user to the database
}

...

// 두 번째 개선
fun saveUser(user: User) {
        // user 파라미터를 중복 사용하지 않는다. 
    fun validate(value: String, fieldName: String) { 
        if (value.isEmpty()) {
            throw IllegalArgumentException(
                    // 바깥 함수의 파라미터에 직접 접근할 수 있다. 
                "Can't save user ${user.id}: " +
                    "empty $fieldName")
        }
    }

    validate(user.name, "Name")
    validate(user.address, "Address")
    // Save user to the database
}

...

// TO-BE (User 클래스를 확장한 함수)
fun User.validateBeforeSave() {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException(
                "Can't save user $id: empty $fieldName")
        }
    }

    validate(name, "Name")
    validate(address, "Address")
}

fun saveUser(user: User) {
    user.validateBeforeSave()
    // Save user to the database
}

클래스, 객체, 인터페이스

코틀린과 자바의 클래스, 인터페이스 개념은 약간 다르다.

  • 인터페이스에 프로퍼티 선언이 들어갈 수 있음

  • 기본적으로 final, public

  • 중첩 클래스는 기본적으로는 내부 클래스가 아니므로 외부 클래스에 대한 참조가 없음

  • 클래스를 data로 선언하면 컴파일러가 일부 표준 메소드를 생성

  • 위임(delegation)을 사용하면 위임을 처리하기 위한 준비 메소드를 직접 작성할 필요가 없음

코틀린 인터페이스

추상 메소드뿐 아니라 구현이 있는 메소드도 정의 가능

interface Clickable {
		fun click()
}

class Button : Clickable {
		override fun click() = println("I was clicked") // I was clicked
}
  • 클래스 이름 뒤에 콜론(:)을 붙이고 인터페이스와 클래스 이름을 적는 것으로 클래스 확장과 인터페이스 구현을 모두 처리

인터페이스 메소드도 디폴트 구현을 제공

  • 자바에서 메소드 앞에 default를 붙이는 것과 달리, 그냥 메소드 본문을 메소드 시그니처 뒤에 추가

interface Clickable {
    fun click() // 일반 메소드 선언
    fun showOff() = println("I'm clickable!") // 디폴트 구현이 있는 메소드
}

다중 인터페이스 상속 관계에서 동일한 메소드가 구현되어 있을 경우

  • 중복된 상위 메소드는 하위 클래스에서 반드시 구현되어야 한다는 컴파일 오류 발생

interface Focusable {
    fun setFocus(b: Boolean) =
        println("I ${if (b) "got" else "lost"} focus.")

    fun showOff() = println("I'm focusable!")
}

---

class Button : Clickable, Focusable {
    override fun click() = println("I was clicked")
}

open, final, abstract 변경자

코틀린의 클래스와 메소드는 기본적으로 final

  • 특정 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 한다.

  • 오버라이드를 허용하고 싶은 메소드나 프러퍼티의 앞에도 open 변경자를 붙여야 한다.

open class RichButton : Clickable { // open class, 다른 클래스가 이 클래스를 상속 가능
    fun disable() {} // final method, 하위 클래스가 오버라이드 불가
    open fun animate() {} // open method, 하위 클래스에서 오버라이드 가능
    override fun click() {} // (상위 클래스에서 선언된) open method override.
}

오버라이드하는 메소드의 구현을 하위 클래스에서 오버라이드하지 못하게 금지하려면 final 명시

  • final이 없는 override 메소드나 프로퍼티는 기본적으로 open

open class RichButton : Clickable {
    final override fun click() {}
}

자바처럼 코틀린에서도 클래스를 abstract 선언 가능

  • abstract 선언 추상 클래스는 인스턴스화 불가

  • 따라서 추상 멤버 앞에 open 변경자를 명시할 필요가 없음

// 추상 클래스 (인스턴스 생성 불가)
abstract class Animated { 
		// 추상 함수 (구현이 없고, 하위 클래스에서는 이 함수를 반드시 오버라이드 필요) 
		abstract fun animate() 
		// 추상 클래스에 속했더라도 비추상 함수는 기본적으로 파이널이지만 원한다면 open으로 오버라이드를 허용 가능
		open fun stopAnimating() { ... }
		fun animateTwice() { ... } 
}

인터페이스 멤버의 경우 final, open, abstract를 사용하지 않는다.

  • 인터페이스 멤버는 항상 열려 있으며 final로 변경 불가

  • 인터페이스 멤버에게 본문이 없으면 자동으로 추상 멤버가 되지만,

  • 멤버 선언 앞에 abstract 키워드를 덧붙일 필요가 없음

클래스 내에서 상속 제어 변경자의 의미

가시성 변경자: 기본적으로 공개 (internal)

패키지를 네임스페이스 관리 용도로만 사용하고 가시성 제어에 사용하지 않는다.

  • 패키지 전용 가시성에 대한 대안으로 internal이라는 새로운 가시성 변경자를 도입

  • “모듈 내부에서만 볼 수 있음"이라는 뜻

    • 모듈은 한 번에 컴파일되는 코틀린 파일들을 의미

코틀린에서는 최상위 선언에 대해 private 가시성을 허용

  • 그런 최상위 선언에는 클래스, 함수, 프로퍼티 등이 포함

  • 비공개 가시성인 최상위 선언은 그 선언이 들어있는 파일 내부에서만 사용 가능

  • 이 또한 하위 시스템의 자세한 구현 사항을 외부에 감추고 싶을 때 유용한 방법

public 함수인 giveSpeech 안에서 그보다 가시성이 더 낮은 타입인 internal TalkativeButton을 참조 불가

  • 자바와 다르게 코틀린은 같은 패키지 안에서 protected 멤버에 접근할 수 없음

internal open class TalkativeButton : Focusable {
		private fun yell() = println("Hey!")
		protected fun whisper() = println("Let's talk!")
}

---

fun TalkativeButton.giveSpeesh() {
		yell() // Cannot access 'yell': it is private in 'TalkativeButton'
		whisper() // Cannot access 'whisper': it is protected in 'TalkativeButton'
}

코틀린과 자바의 가시성

코틀린의 변경자는 컴파일된 자바 바이트코드 안에서도 그대로 유지

  • 단, private 클래스와 internal 변경자는 제외

  • 코틀린 private 클래스 → 자바 패키지 전용 클래스로 컴파일

  • 코틀린 internal 변경자 → 자바 public

내부 클래스와 중첩된 클래스: 기본적으로 중첩 클래스 (inner)

자바와의 차이는 코틀린의 중첩 클래스는 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없다

코틀린 중첩 클래스에 아무런 변경자가 붙지 않으면 자바 static 중첩 클래스와 동일

  • 내부 클래스로 변경해서 바깥쪽 클래스에 대한 참조를 포함하게 만들고 싶다면 inner 변경자 선언

interface State: Serializable

interface View {
		fun getCurrentState(): State
		fun restoreState(state: State) { }
}

---

class Button : View {
		override fun getCurrentState(): State = ButtonState();
		override fun resoreState(State state) { /*...*/ }
		class ButtonState : State { /*...*/ }
}

코틀린에서 바깥쪽 클래스의 인스턴스를 가르키려면 내부 클래스 Inner 안에서 this@Outer라고 써야 한다.

https://livebook.manning.com/book/kotlin-in-action/chapter-4/83

class Outer {
    private val key: String = "hello"

    fun getKey(): String = key

    inner class Inner {
        fun getOuterKey() = this@Outer.getKey()
    }
}

---

@Test
fun `중첩 클래스`() {
    val outer = Outer()
    assertEquals("hello", outer.getKey())

    val inner = Outer().Inner()
    assertEquals("hello", inner.getOuterKey())
}

봉인된 클래스: 클래스 계층 정의 시 계층 확장 제한 (sealed)

when 식에서 하위 클래스를 처리하면 편리하지만 else 분기를 반드시 넣어주어야 한다. 이 문제는 sealed 클래스 변경자를 통해 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있다.

sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야 한다.

  • 하위 클래스를 when 절에서 정의하지 않았을 경우 아래 컴파일 에러 발생

    • 'when' expression must be exhaustive, add necessary 'is Other' branch or 'else' branch instead

// AS-IS
interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
class Other(val value: Int) : Expr

@Test
fun `봉인된 클래스`() {
    fun eval(e: Expr): Int =
        when (e) {
            is Num -> e.value
            is Sum -> eval(e.right) + eval(e.left)
            else -> // "else" 분기는 꼭 필요
                throw IllegalArgumentException("Unknown expression")
        }

    println(eval(Num(1))) // 1
    println(eval(Sum(Num(1), Num(2)))) // 3
    assertThrows(IllegalArgumentException::class.java) {
        println(eval(Other(3))) // 3
    }
}

---

// TO-BE
sealed interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

@Test
fun `봉인된 클래스`() {
    fun eval(e: Expr): Int =
        when (e) {
            is Num -> e.value
            is Sum -> eval(e.right) + eval(e.left)
        }

    println(eval(Num(1))) // 1
    println(eval(Sum(Num(1), Num(2)))) // 3
}

https://livebook.manning.com/book/kotlin-in-action/chapter-4/95

클래스 초기화: 주 생성자와 초기화 블록

코틀린은 주(primary) 생성자와 부(secondary) 생성자를 구분

또한 초기화 블록(initializer block)을 통해 초기화 로직 추가 가능

주 생성자의 목적

  • 생성자 파라미터를 지정

  • 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의

// 파라미터가 하나만 있는 주 생성자
class User constructor(_nickname: String) { 
		val nickName: String

		init { // 초기화 블록(주 생성자와 함께 사용)
				nickName = _nickName
		}
}

부 생성자: 상위 클래스를 다른 방식으로 초기화

  • 일반적으로 코틀린에서는 생성자가 여럿 있는 경우가 자바보다 훨씬 적다.

  • 생성자가 여럿 필요한 경우는

    • 자바 상호운용성

    • 인스턴스를 생성할 때 파라미터 목록이 다른 생성 방법이 여럿 존재하는 경우

인터페이스에 선언된 프로퍼티 구현

인터페이스는 아무 상태도 포함할 수 없으므로 상태를 저장할 필요가 있다면 인터페이스를 구현한

하위 클래스에서 상태 저장을 위한 프로퍼티 등을 만들어야 한다.

interface User {
		val nickName: String
}

// 주 생성자에 있는 프로퍼티
class PrivateUser(override val nickname: String) : User 

class SubscribingUser(val email: String) : User {
    override val nickname: String // 커스텀 게터
        get() = email.substringBefore('@')
}

class FacebookUser(val accountId: Int) : User {
    override val nickname = getFacebookName(accountId) // 프로퍼티 초기화 식
}

인터페이스에는 추상 프로퍼티뿐 아니라 게터와 세터가 있는 프로퍼티를 선언할 수도 있다.

  • 이런 게터와 세터는 뒷받침하는 필드를 참조할 수 없다.

    • 뒷받침하는 필드가 있다면 인터페이스에 상태를 추가하는 의미지만 인터페이스는 상태를 저장할 수 없다

  • 하위 클래스는 추상 프로퍼티인 email을 반드시 오버라이드해야 한다.

    • 반면 nickname은 오버라이드하지 않고 상속할 수 있다.

interface User {
		val email: String
		val nickName: String
				// 프로퍼티에 뒷받침하는 필드가 없는 대신 매번 결과를 계산해 돌려준다.
				get() = email.substringBefore('@')
}

게터와 세터에서 뒷받침하는 필드에 접근

값을 저장하는 동시에 로직을 실행할 수 있게 하기 위해서는

접근자 안에서 프로퍼티를 뒷받침하는 필드에 접근할 수 있어야 한다.

프로퍼티에 저장된 값의 변경 이력을 로그에 남기려는 경우를 생각해 보면

  • 변경 가능한 프로퍼티를 정의하되

  • 세터에서 프로퍼티 값을 바꿀 때마다 약간의 코드를 추가로 실행해야 한다.

field 식별자를 통해 프로퍼티 접근자(게터와 세터) 안에서 프로퍼티의 데이터를 저장하는 데 쓰이는 뒷받침하는 필드를 참조할 수 있다.

@Test
fun `게터와 세터에서 뒷받침하는 필드에 접근`() {
    class User(val name: String) {
        var address: String = "unspecified" // 변경 가능한 프로퍼티
            set(value) {
                println("""
                    Address was changed for $name: "$field" -> "$value".""".trimIndent())
                field = value
            }
    }

    val user = User("Alice")
    user.address = "Elsenheimerstrasse 47, 80687 Muenchen" // Address was changed for Alice: "unspecified" -> "Elsenheimerstrasse 47, 80687 Muenchen".
    assertEquals("Elsenheimerstrasse 47, 80687 Muenchen", user.address)
}

접근자의 가시성 변경

접근자의 가시성은 기본적으로 프로퍼티의 가시성과 같다.

하지만 원할 경우 get이나 set 앞에 가시성 변경자를 추가해서 접근자의 가시성을 변경할 수 있다.

@Test
fun `접근자의 가시성 변경`() {
    class LengthCounter {
        var counter: Int = 0
            private set // 이 클래스 밖에서 값을 바꿀 수 없다.

        fun addWord(word: String) {
            counter += word.length
        }
    }

    val counter = LengthCounter()
    counter.addWord("add")
    counter.addWord("word")
    assertEquals("7", counter.counter.toString())
}

데이터 클래스: 모든 클래스가 정의해야 하는 메소드 자동 생성

data라는 변경자를 클래스 앞에 붙이면 필요한 메소드를 컴파일러가 자동으로 만들어준다.

data class Client(val name: String, val postalCode: Int)
  • 자바에서 요구하는 모든 메소드(euqals, hashCode, toString)를 포함

데이터 클래스와 불변성: copy() 메소드

  • 데이터 클래스 인스턴스를 불변 객체로 더 쉽게 활용할 수 있게 객체를 복사하면서 일부 프로퍼티를 바꿀 수 있게 해주는 copy 메소드를 제공

  • 복사본은 원본과 다른 생명주기를 가지며, 복사를 하면서 일부 프로퍼티 값을 바꾸거나 복사본을 제거해도 프로그램에서 원본을 참조하는 다른 부분에 전혀 영향을 끼치지 않는다.

클래스 위임: by 키워드 사용

인터페이스를 구현할 때 by 키워드를 통해 그 인터페이스에 대한 구현을 다른 객체에 위임 중이라는 사실을 명시할 수있다.

@Test
fun `클래스 위임 by 키워드 사용`() {
    class CountingSet<T>(
        val innerSet: MutableCollection<T> = HashSet()
    ) : MutableCollection<T> by innerSet { // 나머지 메소드는 내부 컨테이너(innerSet)에게 위임

        var objectsAdded = 0

				// add와 addAll을 오버라이드해서 카운터를 증가
        override fun add(element: T): Boolean {
            objectsAdded++
            return innerSet.add(element)
        }

        override fun addAll(c: Collection<T>): Boolean {
            objectsAdded += c.size
            return innerSet.addAll(c)
        }
    }

    val countingSet = CountingSet<Int>()
    countingSet.add(1)
    countingSet.add(2)
    countingSet.add(3)
    assertEquals(3, countingSet.objectsAdded)

    countingSet.addAll(listOf(4, 5, 6))
    assertEquals(6, countingSet.objectsAdded)

    countingSet.remove(5)
    assertTrue(countingSet.containsAll(listOf(1, 2, 3, 4, 6)))

    countingSet.removeAll(listOf(1, 2, 3))
    assertTrue(countingSet.containsAll(listOf(4, 6)))
}

object 키워드: 클래스 선언과 인스턴스 생성

객체 선언: 싱글턴을 쉽게 만들기

코틀린은 객체 선언 기능을 통해 싱글턴을 언어에서 기본 지원

Utils 클래스에 적용할 수 있을 것 같다.

객체 선언: 클래스 선언과 그 클래스에 속한 단일 인스턴스의 선언을 합친 선언

  • 객체 선언도 클래스나 인터페이스를 상속할 수 있다.

  • 프레임워크를 사용하기 위해 특정 인터페이스를 구현해야 하는데, 그 구현 내부에 다른 상태가 필요하지 않은 경우에 이런 기능이 유용

object CaseInsensitiveFileComparator : Comparator<File> {
    override fun compare(file1: File, file2: File): Int {
        return file1.path.compareTo(file2.path, ignoreCase = true)
    }
}

@Test
fun `객체 선언 싱글턴을 쉽게 만들기`() {
    assertEquals(0, CaseInsensitiveFileComparator.compare(File("/User"), File("/user")))

    val files = listOf(File("/Z"), File("/a"))
    assertEquals(listOf(File("/a"), File("/Z")),
        files.sortedWith(CaseInsensitiveFileComparator))
}

클래스 안에서 객체를 선언할 수도 있다. 이런 객체도 인스턴스는 단 하나뿐!

data class Person(val name: String) {
		// 클래스 안에서 객체 선언
    object NameComparator : Comparator<Person> {
        override fun compare(p1: Person, p2: Person): Int =
            p1.name.compareTo(p2.name)
    }
}

@Test
fun `클래스 안에서 객체를 선언`() {
    val persons = listOf(Person("Bob"), Person("Alice"))
    assertEquals(listOf(Person("Alice"), Person("Bob")),
        persons.sortedWith(Person.NameComparator))
}

동반 객체: 팩토리 메소드와 정적 멤버가 들어갈 장소

코틀린 언어는 자바 static 키워드를 지원하지 않는다.

그 대신 패키지 수준의 최상위 함수와 객체 선언을 활용할 수 있다.

클래스 안에 정의된 객체 중 하나에 companion 라는 특별한 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있다.

  • 동반 객체의 프로퍼티나 메소드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용

 class A {
    companion object { // 동반 객체
        fun bar(): String {
            return "Companion object called"
        }
    }
}

@Test
fun `동반 객체`() {
    assertEquals("Companion object called", A.bar())
}

동반 객체가 private 생성자를 호출하기 좋은 위치

  • 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다.

class User private constructor(val nickname: String) {
    companion object { // 동반 객체
        fun newSubscribingUser(email: String) =
            User(email.substringBefore('@'))

        fun newFacebookUser(accountId: Int) =
            User(getFacebookName(accountId))

        private fun getFacebookName(accountId: Int): String = "facebook " + accountId
    }
}

@Test
fun `동반 객체 2`() {
    val subscribingUser = User.newSubscribingUser("bob@gmail.com")
    val facebookUser = User.newFacebookUser(4)
    assertEquals("bob", subscribingUser.nickname)
    assertEquals("facebook 4", facebookUser.nickname)
}

동반 객체를 일반 객체처럼 사용

동반 객체는 클래스 안에 정의된 일반 객체

따라서 동반 객체에 이름을 붙이거나, 동반 객체가 인터페이스를 상속하거나, 동반 객체 안에 확장 함수와 프로퍼티를 정의할 수 있다.

class Person(val name: String) {
    companion object Loader {
        val gson = Gson()
        fun fromJSON(jsonText: String) : Person {
            val mapType = object : TypeToken<Map<String, String>>() {}.type
            val map: Map<String, String> = gson.fromJson(jsonText, mapType)
            return Person(map.get("name") as String)
        }
    }
}

@Test
fun `동반 객체를 일반 객체처럼 사용`() {
    val person = Person.Loader.fromJSON("{name: 'Dmitry'}")
    assertEquals("Dmitry", person.name)

    val person2 = Person.fromJSON("{name: 'Brent'}")
    assertEquals("Brent", person2.name)
}

동반 객체에서 인터페이스 구현

다른 객체 선언과 마찬가지로 동반 객체도 인터페이스를 구현할 수 있다.

class Person(val name: String) {
    companion object : JSONFactory<Person> { // 이름이 없는 동반 객체
        val gson = Gson()
        override fun fromJSON(jsonText: String): Person {
            val mapType = object : TypeToken<Map<String, String>>() {}.type
            val map: Map<String, String> = gson.fromJson(jsonText, mapType)
            return Person(map.get("name") as String)
        }
    }
}

@Test
fun `동반 객체에서 인터페이스 구현`() {
    // 동반 객체에 이름을 붙이지 않았다면 자바 쪽에서 Companion 라는 이름으로 그 참조에 접근
    val person = Person.Companion.fromJSON("{name: 'Dmitry'}")
    assertEquals("Dmitry", person.name)

    val person2 = Person.fromJSON("{name: 'Brent'}")
    assertEquals("Brent", person2.name)
}

객체 식: 무명 내부 클래스를 다른 방식으로 작성

무명 객체(anonymous object)를 정의할 때도 object 키워드를 사용

무명 객체는 자바의 무명 내부 클래스를 대신

한 인터페이스만 구현하거나 한 클래스만 확장할 수 있는 자바의 무명 내부 클래스와 달리

  • 코틀린 무명 클래스는 여러 인터페이스를 구현하거나 클래스를 확장하면서 인터페이스를 구현할 수 있다.

자바의 무명 클래스와 같이 객체 식 안의 코드는 그 식이 포함된 함수의 변수에 접근할 수 있다.

  • 하지만 자바와 달리 final이 아닌 변수도 객체 식 안에서 사용할 수 있다.

window.addMouseListener(
		object : MouseAdapter() { // 무명 객체
				override fun mouseClicked(e: MouseEvent) { ... }
				override fun mouseEntered(e: MouseEvent) { ... }
		}
)

val listener = object : MouseAdapter() { // 무명 객체
		override fun mouseClicked(e: MouseEvent) { ... }
		override fun mouseEntered(e: MouseEvent) { ... }
}

Last updated