프로그래밍언어/Kotlin 2020. 9. 19. 19:56

 

이번 포스팅에서는 코틀린의 타입 시스템에 대해 다루어볼 것이며, 주로 코틀린에서 Null을 다루는 방법을 주로 다루어볼 것이다.

 

널 가능성

널 가능성은 NPE 오류를 피할 수 있게 돕기 위한 코틀린 타입 시스템의 특성이다. 자바 객체는 기본적으로 null을 허용하고, null로 받은 객체 이지만, 해당 객체의 모든 메서드를 호출할 수 있게 설계 되었기 때문에, 런타입에 NPE가 많이 발생한다. 하지만 코틀린은 null 문제를 런타임 시점이 아니라 컴파일 시점으로 옮김으로써, 컴파일 시점에 실행 시점에 발생할 수 있는 여러가지 문제의 가능성을 줄여준다.

 

널이 될 수 있는 타입

코틀린과 자바의 가장 중요한 차이는 코틀린 타입 시스템은 널이 될 수 있는 타입을 명시적으로 지원한다는 것이다. 여기서 널이 될 수 있는 타입이란, 프로그램 안의 프로퍼티나 변수에 null을 허용하게 만드는 방법이다. 여기까지 자바와 무슨 차이가 있냐 생각이 들겠지만, 아래 예제를 보면 확실히 코틀린과 자바의 null을 다루는 방법은 다르다.

 

//컴파일 에러
fun strLen(string: String?) = string.length

 

자바는 위 예제의 메서드를 정의하는데 아무런 컴파일 에러 없이 넘어갈 수 있지만, 코틀린은 Null을 허용하는 타입을 인자로 받는 함수에서 위와 같이 작성하면 컴파일 에러가 발생한다. 그말은 null이 될 수 있는 타입의 메서드 호출을 컴파일 타임에 제한하여 런타임에 NPE 여부를 사전에 차단해버리는 것이다. 또한 아래와 같은 것도 컴파일 시점에 잡아버린다.

 

fun strLength(str: String) = str.length

fun main() {
    strLength(null) //컴파일 에러
}

 

함수의 정의에서 널이 될 수 없는 String을 인자로 받고 있으므로, str.length에서는 컴파일에러가 나지 않는다. 하지만, 이 함수를 가져다 쓸때 인자에 null을 넣으면 컴파일 에러가 발생한다. 그 이유는 널이 될 수 없는 타입에 null을 인자로 넘겼기 때문이다.

 

val x: String? = null
val y: String = x

 

위와 같은 코드도 허용하지 않는다. 널이 될 수 있는 타입을 널이 될 수 없는 타입에 대입이 불가능한것이다. 그러면 여기까지 널이 될 수 있는 타입으로 뭘할 수 있을까? 너무 제약이 많다고 생각이 들 수 있지만, 사실 널이 될 수 있는 타입과 null을 비교하여 null이 아닌 것을 증명하면 그 뒤로는 널이 될 수 없는 타입처럼 사용이 가능하다.

 

fun strLenSafe(s: String?): Int =
        if (s != null) s.length else 0

 

위 코드는 더 간결하게 표현 가능하지만, 여기서 다루지 않고 뒷 내용에서 다룬다.

 

타입의 의미

자바에서는 null을 다루기 위해, Optional이라는 해법이 나왔다. 하지만, 이 또한 어떠한 객체를 래핑하는 래퍼 역할이기 때문에 자칫하면 런타임의 성능이 떨어질 수 있다. 하지만 코틀린에서 널이 될 수 있는 타입이나 널이 될 수 없는 타입은 런타임에 각은 객체 타입이기 때문에 실행 시점의 성능은 똑같고, 부가 비용이 들지 않는다. 그 이유는 모든 가능성을 최대한 컴파일 타임에 잡기 때문이다.

 

안전한 호출 연산자: ?.

코틀린이 제공하는 가장 유용한 도구 중 하나가 안전한 호출 연산자인 "?."이다. "?."은 null 검사와 메서드 호출을 한번의 연산으로 수행한다. 예를 들어, "str?.toUpperCase()""if (str != null) s.toUpperCase() else null"과 같다. 다시 풀어서 이야기하면, 호출하려는 값이 null이 아니면 일반 메서드 호출처럼 작동하고 만약 호출하려는 값이 null이면 null이 결과가 된다.

 

foo?.bar() -> foo != null -> foo.bar()
           -> foo == null -> null

 

여기서 유의 해야할 점은 안전한 호출의 결과 타입도 널이 될 수 있는 타입이라는 것이다.

 

fun getUpperCase(s: String?) = s?.toUpperCase()

val upperCase: String? = getUpperCase("str")

 

안전한 호출은 클래스의 프로퍼티를 다룰때도 적용할 수 있다.

 

class Employee(val name: String, val manager: Employee?)
fun managerName(employee: Employee): String? = employee.manager?.name

 

이러한 안전한 호출은 연쇄해서 호출가능하다.

 

class Address(val streetAddress: String, val zipCode: Int, val city: String, val country: String)
class Company(val name: String, val address: Address?)
class Person(val name: String, val company: Company?)
fun Person.countryName(): String {
    val country = this.company?.address?.country
    return if (country != null) country else "Unknown"
}

 

널 검사가 들어간 호출이 연달아 있는 경우를 자바 코드에서 아주 자주 볼 수 있다. 하지만 코틀린에서는 훨씬 간결하게 널 검사가 가능하다. 사실 위 코드에서 if문을 없애서 간결하게 표현이 가능하다.

 

엘비스 연산자: ?:

코틀린은 null 대신 사용할 디폴트 값을 지정할 때 편리하게 사용할 수 있는 연산자를 제공한다. 그 연산자는 엘비스 연산자라고 한다. 위 코드를 엘비스 연산자를 사용하여 한줄로 리팩토링해보자.

 

fun Person.countryName() = this.company?.address?.country ?: "Unknown"

 

훨씬 더 코드가 간결해졌다. 엘비스 연산자는 이항 연산자로 좌항을 계산한 값이 널인지 검사한다. 좌항 값이 널이 아니라면 좌항값을 그대로 리턴하고, 만약 좌항값이 널이면 우항 값으로 리턴한다.

 

foo ?: bar -> foo != null -> foo
           -> foo == null -> bar

 

위 리팩토링한 코드 예제와 같이 엘비스 연산자를 객체가 널인 경우 널을 반환하는 안전한 호출 연산자와 함께 사용해서 객체가 널인 경우 디폴트 값을 반환하도록 많이 사용한다. 그리고 코틀린에서는 return&throw 등의 연산도 식이기 때문에, 엘비스 연산자 우항에 throw 식을 넣어 null 일 경우 예외를 발생시키는 등의 로직을 만들 수 있다. 이러한 패턴으로 함수의 전제 조건을 검사하는 경우 아주 유용한 연산자가 될 수 있다.

 

class Address(val streetAddress: String, val zipCode: Int, val city: String, val country: String)
class Company(val name: String, val address: Address?)
class Person(val name: String, val company: Company?)

fun printShippingLabel(person: Person) {
    val address = person.company?.address ?: throw IllegalArgumentException("empty address")
    with (address) {
        println(streetAddress)
        println("$zipCode $city, $country")
    }
}

 

안전한 호출 연사자, 엘비스 연산자, with 함수를 이용하여 위와 같은 코드를 만들 수 있다.

 

안전한 캐스트: as?

코틀린은 자바와 같이 타입 캐스트시 대상 타입으로 캐스팅을 할 수 없을 경우, ClassCastException이 발생한다. 하지만 역시 코틀린에서는 컴파일 타임에 최대한 해당 예외 발생을 줄여주기 위해 안전한 캐스트 연산자인 "as?" 를 지원한다.

 

as?연산자는 어떤 값을 지정한 타입으로 캐스팅하는데, 캐스팅할 수 없는 타입이라면 null을 반환한다.

 

foo as? Type -> foo is Type -> foo as Type
             -> foo !is Type -> null

 

안전한 캐스팅을 이용할때는 보통 엘비스 연산자를 같이 사용하는 경우가 많다.

 

class Person(val name: String, val company: Company?) {
    override fun equals(other: Any?): Boolean {
        val otherPerson = other as? Person ?: return false
        
        return otherPerson.name == this.name
    }
}

 

위 예제는 other가 Person 타입이 아닐 경우, 바로 equals 함수의 값을 false로 리턴한다.

 

let 함수

let 함수를 이용하면 널이 될 수 있는 식을 쉽게 다룰 수 있다. let 함수를 안전한 호출 연산자와 함께 사용하면 원하는 식을 평가해서 결과가 널인지 검사한 다음에 그 결과를 변수에 넣는 작업을 간단한 식을 사용해 한번에 처리할 수 있다.

 

fun sendEmailTo(email: String) {
    println("send email to $email")
}

fun main() {
    val email: String? = null
    if (email != null) sendEmailTo(email)
}

 

위 예제를 보면 sendEmailTo 함수를 호출하기전에 인자 값이 널인지를 체크해야지만 컴파일 에러가 나지 않는다. 하지만 let 함수를 사용해 위와 같은 로직을 더 간결하게 표현할 수 있다.

 

fun main() {
    val email: String? = null
    email?.let { sendEmailTo(it) }
}

 

email의 안전한 호출 구문을 호출했고, email이 널이 아닐 때만, let의 수신 객체로 들어가게 된다. 즉, let 함수 안의 it이 널이 될 수 없는 email이 되는 것이다.

 

foo?.let { -> foo != null -> let함수가 수행되고, it은 널이 아니다.
	..it.. -> foo == null -> 아무일도 일어나지 않고, null이 담긴다.
}

 

let 함수는 긴 식이 있고, 그 값이 널이 아닐 때 수행해야하는 로직이 있을 때 아주 유용하다.

 

Lazy init

코틀린에서 널이 될 수 없는 타입의 프로퍼티는 반드시 생성자 안에서 초기화하지 않고, 특별한 메서드 안에서 초기화할 수는 없다. 코틀린에서는 일반적으로 생성자에서 모든 프로퍼티를 초기화해야 한다. 게다가 널이 될 수 없는 타입의 프로퍼티는 널이 아닌 값으로 초기화해야한다. 그렇다고 널이 될 수 있는 프로퍼티에 null를 할당하고, 생성자가 아닌 함수에서 초기화를 해버리면 해당 프로퍼티를 사용하는 곳에서 항상 null 체크를 하는 코드를 넣어줘야 한다.

 

class MyService {
    fun performAction(): String = "foo"
}

class MyTest {
    private var myService: MyService = null
    
    @Before
    fun setUp() {
        myService = MyService()
    }
    
    @Test
    fun testAction() {
        Assert.assertEquals("foo", myService!!.performAction())
    }
}

 

위와 같은 예제와 비슷하게 스프링 프레임워크와 코틀린을 같이 사용하는 경우 DI를 위해 생성자 레벨에서 초기화를 하지 못하는 경우가 종종있다. 이러한 문제 해결을 위해 코틀린에서는 lazy init(지연 초기화)를 제공한다.

 

class MyTest {
    private lateinit var myService: MyService

    @Before
    fun setUp() {
        myService = MyService()
    }

    @Test
    fun testAction() {
        Assert.assertEquals("foo", myService.performAction())
    }
}

 

지연 초기화를 사용하기 위해서는 프로퍼티가 항상 var여야 한다. val 프로퍼티는 final 필드로 컴파일되며, 생성자에서 반드시 초기화해야한다. 만약 lateinit으로 선언한 프로퍼티를 초기화하지 않고 어딘가에서 사용한다면 어떻게 될까? 그럴 경우에는 코틀린은 런타임 시점에 아직 초기화가 되지 않았다라는 에러를 발생시켜준다.

 

널이 될 수 있는 타입 확장 함수

널이 될 수 있는 타입의 확장함수를 정의하면 null에 대한 대비를 조금더 우아하게 할 수 있다. String 클래스의 함수를 예제로 살펴보자.

 

@kotlin.internal.InlineOnly
public inline fun CharSequence?.isNullOrBlank(): Boolean {
    contract {
        returns(false) implies (this@isNullOrBlank != null)
    }

    return this == null || this.isBlank()
}

 

수신 객체가 null임을 체크함으로써 String 객체가 null 일 경우를 대비할 수 있다.

 

타입 파라미터의 널 가능성

코틀린에서 타입 파라미터는 기본적으로 널이 될 수 있는 타입이다. 즉, Any? 타입이 되는 것이다.

 

fun <T> printObject(t: T) {
    println(t?.toString())
}

fun <T: Any> printObject(t: T) {
    println(t.toString())
}

 

하지만 위 예제처럼 타입 파라미터의 upper bound를 지정하여 널이 될 수 없는 타입 파라미터로 정의할 수 있다.

 

여기까지 코틀린에 타입 시스템에 대해 간단히 다루어 보았다. 마지막으로, 코틀린에서 자바 API를 이용하며, 상속 혹은 인터페이스를 구현할때, 보통 메서드를 오버라이드한다. 그때 자바 상위타입 메서드 선언의 인자의 타입을 널이 허용되는 타입으로 봐야할까 널이 허용되지 않는 타입으로 봐야할까? 그것은 코틀린에서 구현하는 사람이 정해야한다. 그 말은 둘다 가능하다는 말이다. 실제 그 상위 인터페이스 혹은 클래스의 용도를 잘 분석해 널을 허용하는 인자로 볼지 널을 허용하지 않는 인자로 볼지 잘 결정하고 구현해야한다.

posted by 여성게
:
프로그래밍언어/Kotlin 2020. 9. 19. 14:47

오늘 다루어볼 내용은 코틀린의 with와 apply 함수이다. 바로 예제로 들어간다.

 

with 함수
fun alphabet(): String {
    val result = StringBuilder()
    for (letter in 'A'..'Z') {
        result.append(letter)
    }
    result.append("\nNow I Know the alphabet!")
    return result.toString()
}

 

위 코드는 알파벳을 출력해주는 함수이다. 뭔가 StringBuilder를 생성하여 특정 변수에 담고, 해당 변수를 사용해 함수를 호출하고 뭔가 군더더기가 많이 붙어있는 느낌이다. 해당 코드를 with 함수를 통해 리팩토링이 가능하다.

 

fun alphabet() = with(StringBuilder()) {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I Know the alphabet!")
    toString()
}

 

코드가 뭔가 훨씬 깔끔해졌다. 눈에 띄는 것이 with 함수이다. 간단하게 with 함수는 하나의 인자를 받는 함수 같지만, 실제로는 2개의 인자를 받는 함수이다.

 

  • 첫번째 인자 : 수신객체
  • 두번째 인자: 첫번째에 받은 인자가 수신 객체인 람다

 

람다에서는 첫번째 인자로 받은 StringBuilder의 인스턴스가 수신객체가 되기때문에 람다안에서 마치 StringBuilder의 내부 함수처럼 사용이 가능하다.(this 키워드로 참조가 가능하지만, 겹치는 함수 이름이 없어 this를 생략하였다.) 그리고 해당 with 함수의 반환값은 람다의 반환값이 된다. with 함수의 내부를 살펴보자.

 

@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

 

우선 with 함수는 T 타입을 인자로 받고 R타입을 리턴하는 함수이다. 내부 코드를 보면 조금 특이하다. 두번째 인자로 받은 람다가 마치 첫번째로 받은 인자 타입이 수신객체인 확장함수처럼 동작을 하고 있는 것이다. 이런 with 함수를 이용하면 특정 상황에서 훨씬 깔끔한 코드가 나올 수 있다.

 

apply함수

with 함수는 람다의 반환값이 with의 반환값이 된다. 하지만, 람다는 람다대로 수행하고 실제 수신 객체가 함수의 리턴값이 되길 바랄때가 있는데 이럴때 apply 함수를 이용한다.

 

fun alphabetUsingApply() = StringBuilder().apply {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I Know the alphabet!")
}.toString()

 

위 예제는 with 예제와는 다르게, 우선 람다대로 수행하고 리턴값이 수신 객체의 인스턴스 자기 자신이 되는 것이다.

 

  • with : 람다의 마지막 식(결과)가 with함수의 반환값이 된다. 즉, with의 두번째 인자 람다의 반환은 특정 객체를 리턴해야하는 것이다.
  • apply : 람다는 람다대로 수행하고, 수신 객체 자기자신을 반환한다. 즉, apply의 인자인 람다의 반환은 Unit(void)이다.

 

apply의 함수 내부를 살펴보자.

 

@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

 

apply 함수 내부를 보면, apply를 호출한 인스턴스 타입이 수신 객체인 확장 함수 람다를 인자로 받는다. 해당 람다의 반환은 Unit이다. 실제 코드 내부에서는 해당 람다를 수행하고 자기자신(this)를 리턴하고 있다. 실제 apply를 활용하여 코틀린이 구현한 유용한 함수의 예제로 아래와 같다.

 

/**
 * buildString은 StringBuilder를 만들어주는 것과 toString을 호출하는것을 대신해준다.
 */
fun alphabetUsingBuildString() = buildString {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I Know the alphabet!")
}

@kotlin.internal.InlineOnly
public inline fun buildString(builderAction: StringBuilder.() -> Unit): String =
    StringBuilder().apply(builderAction).toString()

 

여기까지 코틀린의 with와 apply에 대해 간단히 다루어보았다.

posted by 여성게
:
프로그래밍언어/Kotlin 2020. 9. 17. 10:09

 

이전까지 간단하게 코틀린에 대한 간략한 문법들을 다루어봤는데, 이번 포스팅은 코틀린의 클래스, 객체, 인터페이스에 대해 다루어본다.

 

인터페이스

자바의 인터페이스와 크게 다르지 않다.

 

interface SampleInterface1 {
    val property: String
    fun method1()
    fun method3() = println("method2")
}

interface SampleInterface2 {
    fun method2()
    fun method3() = println("method2")
}

class SampleImpl(): SampleInterface1, SampleInterface2 {
    override val property: String
        get() = TODO("Not yet implemented")

    override fun method1() {
        TODO("Not yet implemented")
    }

    override fun method3() {
        super<SampleInterface2>.method3()
    }

    override fun method2() {
        TODO("Not yet implemented")
    }
}

 

자바처럼 여러 인터페이스를 한 클래스에서 구현할 수도 있다. 또한 자바의 default 메서드처럼 코틀린에서는 그냥 함수 구현체를 정의하면 그게 default 함수가 된다. 하지만 유의할점은, 구현하고 있는 인터페이스 두개에서 같은 이름의 함수가 있다면, 구현하고 있는 하위 함수에서 오버라이드 해주어야한다.(혹은 특정 상위 인터페이스의 구현을 사용한다고 명시할 수도 있다.) 

 

자바는 @Override 어노테이션을 사용하지만 코틀린은 override 키워드를 사용한다. 마지막으로 자바와는 다르게 코틀린은 인터페이스에서 프로퍼티를 선언할 수 있다. 만약 인터페이스에 프로퍼티가 선언되어 있다면, 구현체에서 Getter를 오버라이드 해주면 된다.

 

코틀린의 열린 클래스(클래스상속)

자바에서는 기본적으로 클래스를 상속하면 다른 클래스에서 상속을 받을 수 있는 구조이다(public, not final) 하지만, 코틀린은 기본적으로 클래스가 모두 final 이기때문에 상속을 받을 수 없다. 하지만, 일반 클래스도 상속을 허용하기 위해 열린 클래스를 정의할 수 있다.

 

open class SampleOpen(val value: String): SampleInterface1 {
    override val property: String
        get() = TODO("Not yet implemented")

    fun disable() {}
    open fun animate() {}
    override fun method1() {
        TODO("Not yet implemented")
    }
}

class SampleOpenImpl(val key: String, value: String): SampleOpen(value) {
    override fun animate() {
        super.animate()
    }
}

 

open이라는 키워드를 붙여 상속이 가능한 형태의 클래스를 만들 수 있다. 하지만 자바와는 조금 문법이 다르게 하위 클래스에서 상위 클래스의 생성자 인자만큼 괄호안에 프로퍼티를 넣어줘야하는데, 그 프로퍼티를 생성자 시점에 넘겨줘야한다.(자바와 비슷) 하지만 자바와는 다르게 하위클래스의 생성자 프로퍼티 명을 그대로 상위 클래스 생성자 아규먼트로 넣어주면 된다. 또한 open class는 구현체 함수도 들어갈 수 있고, 상속을 강제하기 위해 open 키워드를 붙인 함수를 정의할 수 있다.

 

확장함수의 접근제한자

코틀린에서는 확장함수라는 아주 편한 문법이 존재한다. 하지만 하나 유의해야할 점이 확장함수를 정의할때 수신 객체 타입의 접근제한자의 레벨과 같거나 낮아야한다. 그말은 코틀린의 internal(같은 모듈에서만 사용가능)으로 수신객체의 접근제한자로 정의해놓았다면, 확장 함수는 반드시 internal 제한과 같거나 낮아야한다.(public으로는 internal 수신 객체의 확장함수를 정의할 수 없다.)

 

internal class SampleInternal {
    
}

internal fun SampleInternal.expendFunction() {} -> ok
fun SampleInternal.expendFunction() {} -> 컴파일 에러

 

sealed 클래스

아래와 같은 코드가 있다고 가정하자.

 

interface InterfaceExam {}
class InterfaceImpl1: InterfaceExam
class InterfaceImpl2: InterfaceExam
class InterfaceImpl3: InterfaceExam

fun whenExam(obj: InterfaceExam) = when (obj) {
    is InterfaceImpl1 -> println("interfaceImpl1")
    is InterfaceImpl2 -> println("interfaceImpl2")
    is InterfaceImpl3 -> println("interfaceImpl3")
    else -> println("els")
}

 

when 식에서 인터페이스 구현체의 타입을 분기하는 로직인데, 실제로 모든 구현체를 다 명시했음에도 else 구문이 들어가야한다. 하지만 코틀린에서 sealed 타입을 구현함으로써 else문을 명시하지 않아도된다.(sealed로 상위 타입을 명시함으로써 when 식에 모든 타입이 들어왔으니 else 구문이 오지 않을 것이라라고 강제할 수 있어 else 구문이 필요없다.)

 

sealed class SampleSealed {
    open fun method() {}
}

class SampleSealedImpl1: SampleSealed() {
    override fun method() {
        super.method()
    }
}
class SampleSealedImpl2: SampleSealed() {
    override fun method() {
        super.method()
    }
}

class SampleSealedImpl3: SampleSealed() {
    override fun method() {
        super.method()
    }
}

 

클래스 생성자

코틀린에서 주 생성자는 자바보다 간결하게 정의가능하다.

 

class User1(val name: String)

#java
public final class User1 {
	private final String name;
    
    public User1(String name) {
    	this.name = name
    }
}

 

위 정의는 모든 필드가 final이고, 그 필드를 모든 필드를 인자로 받는 생성자로 초기화하는 것과 같은 것이다. 위와 같은 생성자는 코틀린에서 펼친 표현으로는 아래와 같다.

 

class User2 constructor(name: String){
    val name: String
    init {
        this.name = name
    }
}

class User3(name: String){
    val name: String = name
}

 

복잡한 초기화 로직을 적용해야하는 클래스는 주 생성자말고 부 생성자를 여러개 정의할 수 있다.

 

class Secretive private constructor() {}

open class View {
    constructor(title: String): this(title, "")
    constructor(title: String, content: String) {}
}

class MyButton: View {
    constructor(title: String): super(title){

    }

    constructor(title: String, content: String): super(title, content) {

    }
}

 

인터페이스 프로퍼티 오버라이드

코틀린의 인터페이스에서는 프로퍼티를 선언할 수 있는데, 그 예로 아래와 같다.

 

interface Book {
    val name: String
}

class EBook1(override val name: String): Book
class EBook2: Book {
    override val name: String
        get() = TODO("Not yet implemented")
}
class EBook3(val category: String): Book {
    override val name = getBookName(category)
    private fun getBookName(category: String) = "ebook"
}

 

데이터 클래스

자바의 레코드 클래스와 비슷하게 코틀린에는 데이터 클래스가 있다. 데이터 클래스는 코틀린에서 필요한 함수를 모두 숨겨서 구현해주고 있다.( equal(), hashCode(), toString() , copy())

 

data class BookDto(val name: String, val price: Int) {}

 

위임클래스

코틀린에서는 delegate pattern을 아주 쉽게 구현할 수 있는 수단이 존재한다. 만약 자바에서 HashMap을 상속하여 커스텀한 로직을 넣은 클래스를 만든다면 여러 불필요한 코드가 많고, 실제 상위 클래스의 메서드에 의존해야한다라는 단점이 있는데, 코틀린은 이를 아주 우하하게 해결해주었다.

 

/**
 * 자바와 달리 데코레이터 패턴을 아주 쉽게 구현할 수 있다.
 * 보통 특정 클래스를 상속할때 상위 클래스의 기능이 변경되면, 하위 클래스의 동작에도 문제가 생기는데
 * 아래처럼 위임 관계를 만들어주면 실제 상위 클래스의 구현에 의존하지 않고, 위임하기때문에 상위클래스 변경에
 * 안전하다.
 */
class DelegatingCollection<T>(
        private val innerList: Collection<T> = ArrayList<T>()
): Collection<T> by innerList {

}

class BookNamePriceMap(
        private val map: Map<String, Int> = hashMapOf()
): Map<String, Int> by map {

}

 

만약 오버라이드해야하는 함수가 있다면 오버라이드하면 되고, 나머지는 "by map"이라는 키워드로 인해 자동으로 상위 클래스의 함수로 위임해준다. 

 

객체의 선언과 생성을 동시에 해주는 Object

코틀린에는 자바와는 다르게 object라는 타입이 존재한다. 간단하게는 싱글톤을 만들어주고, 마치 static한 유틸클래스처럼 이용가능한 타입이다.

 

object Payroll {
    const val companyName = "company"
    init {
    }
}

 

선언과 생성이 동시에 이루어지기 때문에, 상태값을 담은 프로퍼티 선언을 하지 않고 보통은 상수정도의 프로퍼티를 갖는다. object 타입에 함수를 정의하면 Payroll.method() 처럼 정적인 클래스처럼 이용가능하다.

 

동반객체(companion object)

특정 클래스의 생성을 담당할 팩토리 클래스 등을 만들때 아주 좋은 코틀린의 기능이 있다. 또한 마치 확장함수와 같은 기능을 제공하기도 하는데 아래 예제를 살펴보자.

 

class OuterClass private constructor(val name: String) {
    //내부 객체선언이 동반 객체선언이라면, 내부 객체 선언의 private 멤버 혹은 함수 접근이 가능하다.
    private fun method2() = method()
    private fun method3() = value
    
    companion object OuterClassFactory {
        private const val value = ""
        
        fun newOuterClass() = OuterClass("outerName")
        
        private fun method() = println("")
        
        fun companionMethod() = println("")
    }
}

OuterClass.newOuterClass()
OuterClass.companionMethod()

 

자바와는 달리 코틀린의 내부클래스는 외부 클래스를 참조하지 않는다. 또한 companion 키워드를 넣어주면, 바깥 클래스에서 내부 객체의 private한 멤버도 참조가능하다. 그렇다면 그냥 object로 선언된 내부 객체는 어떠할까

 

class OuterClass private constructor(val name: String) {
    private fun method() = println("private method")
    //내부 객체선언이 일반 객체선언이라면, 내부 객체 선언의 private 멤버 혹은 함수 접근이 안된다.
    private fun method2() = OuterClassFactory.method() //컴파일에러
    
    object OuterClassFactory {
        fun newOuterClass() = OuterClass("outerName")
        
        private fun method() = println("")
    }
}

 

일반 object 내부 객체로 정의되어있다면, 외부 클래스에서 내부 객체의 private 멤버에 접근할 수 없다. 이를 잘 활용하면 팩토리 클래스를 따로 분리할 필요없이 클래스 내부에 정의가 가능해지는 것이다. 또한 이와 비슷한 느낌의 로직도 한곳에 다모이게 되어 관리가 쉬워질 것이다.

 

여기까지 코틀린의 클래스, 인터페이스, 객체에 대해 간단히 다루어보았다.

posted by 여성게
:
프로그래밍언어/Kotlin 2020. 9. 16. 13:05

 

오늘은 코틀린의 함수 정의와 호출에 대해 다루어 본다.

 

컬렉션 객체 만들기
fun createHashSet() = hashSetOf(1, 7, 53)

fun createArrayList() = arrayListOf(1, 7, 53)

fun createHashMap() = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

 

기본적으로 코틀린은 컬렉션을 만드는 함수를 기본 라이브러리에 내장이 되어 있다. 또한 마지막에 hashMap을 만드는 함수 안에 "to"라는 키워드가 있는데, 사실 키워드가 아니고 일반 함수이다. 이점은 뒤에서 자세히 설명한다. 또한 생성하는 컬렉션 객체는 코틀린만의 컬렉션 객체가 아니고, 자바의 컬렉션 객체를 생성한다. 하지만 자바에서 제공하고 있는 기능보다 더 유용한 기능들을 제공한다.

 

fun collectionOperate() {
    val list = listOf("one", "two", "three")
    //리스트의 마지막 원소를 가져온다.
    println(list.last())
    
    val numbers = setOf(1, 14, 2)
    //숫자로 이루어진 컬렉션(Set)에서 가장 큰 값을 가져온다.
    println(numbers.max())
}

 

디폴트 파라미터 값
fun <T> defaultParamValue(collection: Collection<T>, separator: String = ", ", prefix: String = "", postfix: String = ""): String {
    val result = StringBuilder(prefix)
    for ((index, elem) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(elem)
    }
    result.append(postfix)
    return result.toString()
}

fun main() {
    println(defaultParamValue(list))
}

->
1, 2, 3

 

만약 이름있는 파라미터를 사용하면, 함수 호출시 파라미터 순서를 지정할 필요가 없어진다.

 

println(defaultParamValue(list, postfix = "#", prefix = ";"))

 

이러면, 순서와 상관없이 파라미터를 지정할 수 있고, seperate라는 파라미터도 디폴트 값이 적용된다.

 

최상위 함수와 프로퍼티
fun topLevelFunction() {
    println("top level function")
}

 

코틀린에서 클래스 밖에 함수를 정의하면 마치 자바의 static method와 같이 작동한다. 하지만 자바와는 다르게 클래스명을 임포트 할 필요 없이 함수명을 바로 임포트해서 사용할 수 있다. 어떻게 이렇게 실행 될 수 있을까? 이는 바로 JVM이 컴파일할때 새로운 클래스를 정의해주기 때문이다.(file이름을 기반으로 클래스 생성) 실제로 자바에서 해당 함수를 가져다 쓰려면 컴파일러가 만들어준 클래스명을 명시해서 사용해야 한다. ex) JoinKt.joinToString(..)

 

함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있다. 기본적으로 최상위 프로퍼티도 다른 모든 프로퍼티처럼 접근자 메서드를 통해 자바 코드에 노출된다. 겉으론 상수처럼 보이지만, 실제로는 게터를 이용해야한다니 조금 이상하다. 그렇게 때문에 더 자연스럽게 자바처럼 public static final 같은 구문을 넣어주는 것이 자연스럽기 때문에 최상위 프로퍼티는 보통 아래와 같이 정의한다.

 

//Java : public static final String UNIX_LINE_SEPARATOR = "\n"
const val UNIX_LINE_SEPARATOR = "\n"

 

확장 함수와 확장 프로퍼티

기존 자바 API를 이용해 확장함수 및 확장 프로퍼티를 정의하는 것이 가능하다. 그 말은 우리가 지금까지 사용했던 setOf(), arrayListOf()등을 호출할 수 있었던 이유다. 다음 예제를 보자.

 

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

fun main() {
    println("Kotlin".lastChar())
}

 

함수명 앞에 "String." 을 붙여서 확장할 클래스명을 넣어준다. 이것을 수신객체타입이라 부르고, 함수 내용에 "this" 키워드로 수신객체 타입을 참조할 수 있다. 이 this 키워드로 참조하는 객체를 수신객체(위에서 "Kotlin"이라는 문자열이 수신객체가 된다.)라고 부른다. 여기서 기억할 것은 확장 함수가 캡슐화를 깨지않는다는 것이다. 그 이유는 수신 객체의 private 메서드 같은 것은 접근할 수 없기 때문이다. 여기서 만약, 다른 패키지의 많은 확장함수를 임포트해서 사용하는데 함수의 시그니쳐 등이 모두 동일하면 충돌하는 현상이 발생되는데 이런 경우는 아래와 같이 "as" 키워드로 확장함수의 별칭을 지어줄 수 있다.

 

import com.example.kotlin.ch_3.lastChar as last

fun main() {
    println("Kotlin".last())
}

 

확장함수로 유틸리티 클래스 만들기

기존 자바 API 혹은 코틀린 클래스를 수신객체로 확장함수를 사용해 유틸클래스를 만들면 유용하다.

 

fun <T> Collection<T>.joinToString(seperate: String): String {
    val result = StringBuilder()
    for ((i, elem) in this.withIndex()) {
        if (i > 0) result.append(seperate)
        result.append(elem)
    }
    return result.toString()
}

fun main() {
    val list = listOf(1,2,3)
    println(list.joinToString(","))
}

 

이렇게 제네릭 타입으로 확장함수를 만들수 있고, 특정 타입으로 제한하여 확장함수를 만들 수 있다.

 

fun main() {
    val list = listOf(1,2,3)
    println(list.join2(","))
}

fun Collection<String>.join2(seperate: String): String {
    val result = StringBuilder()
    for ((i, elem) in this.withIndex()) {
        if (i > 0) result.append(seperate)
        result.append(elem)
    }
    return result.toString()
}

 

만약 String을 타입으로 받는 확장함수를 만들었는데, Int를 요소로 가지는 리스트에 대해 확장함수를 호출하면 컴파일 에러가 발생한다. 마지막으로 "확장함수는 함수 오버라이드가 불가능하다." 또한 확장함수와 그 수신객체의 함수의 시그니쳐가 동일하면 수신객체의 멤버 함수를 우선시 한다.

 

상속, 인터페이스 구현관계의 확장함수

아래와 같은 예제가 있다고 가정하자. 코틀린에서 클래스는 final 클래스와 같으므로, 그냥 상속이 안된다. 그렇기 때문에 class 앞에 open 키워드를 넣어준다. 이는 함수 레벨도 동일하다.

 

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

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

fun main() {
    val view: View = Button()
    view.click()
}

->
Button clicked

 

위 예제는 상속을 예제로 코드를 작성하였는데, 결과로는 Button의 오버라이드된 메서드의 결과가 출력된다. 그렇다면 아래 예제는 어떨까?

 

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

fun main() {
    val view: View = Button()
    view.showOff()
}

->
I'm a view !

 

위 결과는 상속과는 다르게 동작한다. 실제 변수에 담긴 타입은 Button()이지만, 결과는 View의 확장함수로 작동한다.

 

가변 인자 함수
fun <T> listOf(vararg values: T): List<T> {...}

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

 

위 listOf는 "vararg"라는 키워드로 가변인자를 받을 수 있는 파라미터를 정의했다. 자바에서는 "..."으로 배열인 가변인자를 받을 수 있지만, 코틀린은 vararg라는 키워드를 이용한다. 그리고 또 다른 자바와의 차이점은 배열자체를 인자로 넘기는 것이 아니고, 배열 변수 앞에 "*"를 붙여서 명시적으로 배열을 풀어서 넣는 것이다. 기술적으로는 스프레드 연산자가 그런 작업을 해준다. 그리고 또하나의 차이점은 자바와는 달리 배열 혹은 인자를 나열하는 것이 아니라, 인자+배열로 여러 값을 함께 인자로 넘길 수 있다.

 

값의 쌍을 다루기

이전에  Map 오브젝트를 만드는 예제에서 "to"라는 키워드를 사용한 적이 있었다. 여기서 "to"는 키워드가 아니고, 함수라고 이야기를 했는데, 이 함수를 바로 중위함수라는 특별한 방식으로 동작하는 함수이다.

 

fun pair() {
    val pair = 1 to "one"
    println(pair)
}

fun main() {
    pair()
}

->
(1, one)

 

여기서 to, 중위함수는 파라미터가 유일한 함수를 이렇게 호출할 수 있게 되는 것이다. 사실은 "1.to("one")"과 같은 구문이며, 실제로는 to는 파라미터가 하나로 유일한 함수인 것이다. 이것을 실제로 예제로 구현해보면 아래와 같다.

 

infix fun Int.pair(v: String): Pair<Int, String> {
    return Pair(this, v)
}

fun main() {
    1 pair "one"
}

 

함수의 정의를 보면, Int의 확장함수로 선언했으며 실제로 파라미터는 유일하게 하나만 받고 있다. 이러한 함수를 중위호출에 사용하려면 "infix"라는 키워드를 선언하면 된다.

 

구조 분해 선언
fun main() {
    val (left, right) = Pair(1, "one")
    val list = listOf(1, 2, 3)
    for ((index, value) in list.withIndex()) {}
}

 

Pair과 같이 값과 쌍으로 이루어진 객체의 값을 분해해서 받을 수 있는데, 이것을 구조 분해 선언이라고 한다.

 

문자열 나누기

자바의 split과 비슷하게 사용하면 되지만, 조금더 확장된 함수를 제공한다. 아래와 같이 문자열 하나 혹은 문자열 여러개를 넘겨 여러 구분 문자열로 나눌수도 있고, 혹은 직접 정규식을 생성해서 split의 구분문자 정규식을 넘길 수도 있다.

 

fun main() {
    val str = "12.345-6.A"

    println(str.split(".", "-"))
    println(str.split("\\.|-".toRegex()))
}

 

Stirng의 substring 함수
fun main() {
    val path = "/Users/levi/kotlin/chapter3.kt"
    val directory = path.substringBeforeLast("/")
    val fullName = path.substringAfterLast("/")
    val fileName = fullName.substringBefore(".")
    val extension = fullName.substringAfter(".")
    println("Dir : $directory, FullName : $fullName, FileName : $fileName, Extension : $extension")
}

->
Dir : /Users/levi/kotlin, FullName : chapter3.kt, FileName : chapter3, Extension : kt

 

 

로컬 함수와 확장

코틀린에서는 중복된 로직을 깔끔하게 리팩토링할 수 있는 해법이 있다. 코틀린에서는 함수에서 추출한 함수를 원 ㅎ마수 내부에 중첩시킬 수 있고, 그렇게 하면 문법적인 부가 비용을 들이지 않고 깔끔하게 코드를 조작할 수 있다. 바로 예시를 살펴보자.

 

class User(val id: Long, val name: String, val address: String) {
    fun saveUser(user: User) {
        if (user.name.isEmpty()) {
            throw IllegalArgumentException("empty user name")
        }
        
        if (user.address.isEmpty()) {
            throw IllegalArgumentException("empty user address")
        }
        
        // user를 데이터베이스에 저장하는 로직.
    }
}

 

위와 같은 코드가 있다. 해당 코드에서는 검즈하는 필드 값만 다를뿐 사실 같은 중복된 검증 로직(문자열이 비어있는지)을 가지고 있다. 이러한 중복된 코드 내용은 로컬 함수로 분리하면서 제거할 수 있다.

 

class User(val id: Long, val name: String, val address: String) {
    fun saveUser(user: User) {
        fun validate(user: User, value: String, fieldName: String) {
            if (value.isEmpty()) {
                throw IllegalArgumentException("empty ${user.id} $fieldName")
            }
        }
        
        validate(user, user.name, "name")
        validate(user, user.address, "address")

        // user를 데이터베이스에 저장하는 로직.
    }
}

 

위 예제는 중복된 검증 로직을 로컬 함수로 분리함으로써 중복을 제거하였다. 하지만, 뭔가 User 객체를 넘기는 것이 보기 싫다. 조금 더 개선하자면, 로컬 함수는 자신을 감싸고 있는 함수의 변수를 캡쳐링해서 가져갈 수있는 클로저로 동작하기 때문에 굳이 User 객체를 인자로 넘길 필요가 없이 아래처럼 리팩토링이 가능하다.

 

class User(val id: Long, val name: String, val address: String) {
    fun saveUser(user: User) {
        fun validate(value: String, fieldName: String) {
            if (value.isEmpty()) {
                throw IllegalArgumentException("empty ${user.id} $fieldName")
            }
        }

        validate(user.name, "name")
        validate(user.address, "address")

        // user를 데이터베이스에 저장하는 로직.
    }
}

 

여기서 멈추지 않고, 조금더 깔끔하게 확장함수를 활용해 리팩토링해보자.

 

class User(val id: Long, val name: String, val address: String) {
    private fun User.validate() {
        fun validateParam(value: String, fieldName: String) {
            fun validate(value: String, fieldName: String) {
                if (value.isEmpty()) {
                    throw IllegalArgumentException("empty ${this.id} $fieldName")
                }
            }            
        }
        validateParam(this.name, "name")
        validateParam(this.address, "address")
    }
    fun saveUser(user: User) {
        user.validate()
        // user를 데이터베이스에 저장하는 로직.
    }
}

 

클래스 내부에 확장함수를 정의해서 saveUser 함수 내부는 훨씬더 군더더기 없는 코드로 리팩토링 되었다. 클래스 내부에 적용한 확장함수는 해당 클래스 내부에서만 사용가능하다는 것을 유의하자(private 키워드를 붙이지 않더라도 해당 클래스 내부에서만 사용가능하다.)

 

여기까지 코틀린의 함수에 대한 내용은 간단히 다루어보았다. 사실 위 내용들보다 많은 내용이 있지만, 다음 포스팅들에서 다루어보지 못한 내용들을 더 많이 다루어볼 것이다.

posted by 여성게
:
프로그래밍언어/Kotlin 2020. 9. 15. 20:33

 

오늘은 코틀린에 대해 아주 기초를 다루어본다.

 

함수(Function)

코틀린에서 함수는 "fun"이라는 키워드로 정의한다. 간단하게 리턴값이 있고, 없는 함수와 바디 내용이 식으로만 이루어졌을때 함수를 간략화 하는 방법은 아래와 같다.

/**
 * 리턴이 없는 함수
 */
fun helloWorld() {
    println("hello, world")
}

/**
 * 리턴값이 있는 함수
 */
fun max(a: Int, b: Int): Int {
    //코틀린의 if문은 식(리턴 값이 존재)이지 문(block, return이 없음)이 아니다.
    return if (a > b) a else b
}

/**
 * max 함수와 간략 버전
 * 함수의 본문이 식으로만 이루어져있다면, 아래처럼 간략하게 바꿀 수 있다.
 * 또한 반환 타입도 생략이 가능하다. 그 이유는 식이 반환하는 타입을 컴파일러가 추론하기 때문이다.
 * fun max2(a: Int, b: Int): Int = if (a > b) a else b
 * ->
 * fun max2(a: Int, b: Int) = if (a > b) a else b
 */
fun max2(a: Int, b: Int) = if (a > b) a else b

 

변수(Variable)

크게 어색하지 않은 키워드이다. 하지만 val은 자바의 final과 거의 비슷한 느낌인데, 선언시 반드시 초기화 식이 들어가지 않아도 된다. 그 이유는 블록내에 컴파일러에게 오직 한번만 할당한다라는 보장을 줄 수 있다면, 블록내에 여러군대에서 할당하는 코드가 들어가도 된다.

//타입을 명시하지 않아도 컴파일러가 타입을 추론한다.
val question = "sample question" -> String
val num = 42 -> Int
val yearsToCompute = 7.5e6 -> Double

//변수의 타입을 직접 명시해줄 수도 있다.
val answer: Int = 42

//변경, 재할당이 가능한 변수
//한번 타입이 추론되면 다른 곳에서 어싸인할때 다른 타입으로 못넣음.
var variable = 1

/**
 * val 키워드지만, 컴파일러가 오직 하나의 초기화 문장만
 * 실행됨이 확실하면 여러 곳에서 할당하는 코드가 들어갈 수 있다.
 */
fun variableExam(b: Boolean): String {
    val message: String

    if (b) {
        message = "true"
    } else {
        message = "false"
    }

    return message
}

/**
* 타입이 생략가능하다지만, 다른 타입을 재할당 할 수는 없다.
*/
var intValue = 1
intValue = "123" -> 컴파일에러

 

마지막에 var에 다른 타입으로 재할당이 불가능하지만,  변환 함수나 강제 형변환을 통해 다른 타입의 값을 변수에 재할당 가능하다.

 

문자열 템플릿

 

문자열을 다룰때 "$" 키워드로 변수값을 그대로 넣을 수 있다. 마치 자바에서 "Hello" + name + "!" 과 비슷하지만 조금더 간결하게 작성가능하다. 또한 "${}"를 이용하여 변수명만 아니라, 복잡한 식도 넣을 수 있다.

//컴파일된 코드는 StringBuilder를 사용하고, 문자열 상수와 변수의 값을 append로 문자열 빌더 뒤에 추가한다.
fun main(args: Array<String>) {
    val name = if (args.isNotEmpty()) args[0] else "Kotlin"
    println("Hello, $name!")
}

fun stringTemplate(args: Array<String>) {
    if (args.isNotEmpty()) {
        println("Hello, ${args[0]}!")
    }
}

fun stringTemplate2(args: Array<String>) {
    if (args.isNotEmpty()) {
        println("Hello, ${if (args.isNotEmpty()) args[0] else "Kotlin"}!")
    }
}

 

클래스와 프로퍼티

자바의 클래스와 코틀린의 클래스 선언 방법을 비교해보자.

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

/**
 * Kotlin class definition
 */
class Person(val name: String)

 

자바에서는 프로퍼티가 증가할 수록 생성자에 매개변수가 증가함에 따라 내부 this 연산자로 프로퍼티를 초기화하는 코드도 증가한다.(물론 롬복이라는 라이브러리를 사용하면 되지만..) 코틀린은 프로퍼티가 많아져도 아주 간단하게 클래스를 정의할 수 있다. 또한 코틀린은 기본 접근제어자가 public 이기 때문에 public 키워드도 생략가능하다.

 

/**
 * Kotlin class definition
 */
class Person(val name: String,
             var age: Int)

fun main(args: Array<String>) {
    val person = Person("levi", 29)
    println(person.name)
    println(person.age)
    person.age = 30
}

 

생성자에서 var로 프로퍼티를 선언하면, 실제 변경가능한 프로퍼티로 생성된다. 자바와 달리 setxx, getxx이 필요없고, 실제 프로퍼티를 참조해 값을 가져오거나 값을 변경할 수 있다.(프로퍼티를 참조하면 내부적으로 getter, setter를 호출해준다.)

 

/**
 * Kotlin class definition
 */
class Person(val name: String,
             var age: Int) {
    val isProgrammer: Boolean
        get() {
            return name == "levi"
        }
        //get() = name == "levi"
}

fun main(args: Array<String>) {
    val person = Person("levi", 29)
    println(person.isProgrammer)
}

 

또한 위 예제처럼 실제 초기화 시점에는 값을 받을 필요가 없는 프로퍼티의 경우 직접 프로퍼티의 접근자(getter)를 정의해줄 수 있다. 함수로 fun isProgrammer() = name == "levi"를 정의할 수 있지만, 성능상 차이는 없고, 가독성 차이 뿐이다.

 

디렉토리와 패키지

코틀린은 자바와 패키지에 대한 개념이 비슷하다. 하지만 아래와 같이, 최상위 함수가 정의되어있다면 다른 패키지에서 해당 함수만 임포트해서 사용가능하다.

 

package com.example.kotlin.ch_1

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

    fun sumHeightAndWidth() = height + width
}

fun createRandomRectangle(): Rectangle {
    val random = Random()
    return Rectangle(random.nextInt(), random.nextInt())
}

=========================================================

import com.example.kotlin.ch_1.createRandomRectangle

fun main(args: Array<String>) {
    var rectangle = createRandomRectangle()
}

 

또한 같은 패키지라면, 함수를 임포트할 필요없이 얼마든지 가져다 사용할 수 있다.

 

선택 표현과 처리: enum과 when

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);
    
    fun rgb() = (r * 256 + g) * 256 + b
}

 

when 절은 자바의 switch case와 상응하는 문법이다. if else와 같이 리턴값을 가지는 식이다.

 

fun getMnemonic(color: Color) = when (color) {
    Color.RED -> "GOOD"
    Color.ORANGE -> "NOT BAD"
    Color.GREEN, Color.YELLOW -> "BAD"
}

 

자바의 switch 문과 달리 코틀린의 when은 임의의 객체를 인자로 받을 수 있다.

 

#setOf Set 객체를 만드는 함수
fun mix(c1: Color, c2: Color) = when (setOf(c1, c2)) {
    setOf(Color.RED, Color.ORANGE) -> "first"
    setOf(Color.YELLOW, Color.GREEN) -> "second"
    else -> throw Exception("exception")
}

 

또한 when은 인자가 없이 만들 수도 있다.

 

fun mixOptimized(c1: Color, c2: Color) = when {
    (c1 == Color.RED && c2 == Color.YELLOW) -> "first"
    (c1 == Color.YELLOW && c2 == Color.GREEN) -> "second"
    else -> throw Exception("exception")
}

 

위처럼 인자가 없는 when 식을 사용하면, 각 분기의 조건이 Boolean을 반환하는 식을 넣으면 된다.

 

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

자바와 달리 코틀린은 명시적으로 타입 캐스팅하는 코드를 넣을 필요가 없다.

 

interface Expr

class Num(val value: Int) : Expr

class Sum(val left: Expr, val right: Expr) : Expr

fun eval(e: Expr): Int =
        if (e is Num) {
            val num = e as Num
            num.value
        } else if (e is Sum) {
            eval(e.left) + eval(e.right)
        } else {
            throw IllegalArgumentException("Unknown expression")
        }

 첫번째 if문은 자바처럼 명시적으로 "as" 키워드를 사용해 타입 캐스팅을 하였다. 하지만 else if 문을 보면 딱히 타입 캐스팅을 하지 않았는데도, Sum 타입으로 변환이 되었다. 이것을 스마트 캐스팅이라고 한다.

 

이번엔 위 eval 함수를 when식로 리팩토링 해보자.

 

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

 

코틀린에서 조금 특이하게 동등성 검사가 아닌 다른 기능에도 when식을 사용할 수 있다. 그리고 조금 특이한것이 블록 내에 return이 생략되어 있는 것을 볼 수 있다. 이 말은 "블록의 마지막 식이 블록의 결과"라는 규칙이 항상 성립되는 것이다.

 

for문

코틀린은 자바의 forEach문만 지원한다. 이말은 for(int i=0; i<10; i++) 같은 문법을 지원하지 않는것이다.

 

package com.example.kotlin.ch_2

fun forEach() {
    val list = listOf("a", "b", "c")

    for (str in list) {
        println(str)
    }
}

fun forEach1() {
	val list = arrayListOf(10, 11, 12)
    for ((index, elem) in list.withIndex()) {
    	println("$index, $elem")
    }
}

fun forEach2() {
    //range 1부터 10까지 포함
    val oneToTen = 1..10

    for (i in oneToTen) {
        println(i)
    }
}

fun forEach3() {
    for (i in 10 downTo 1 step 2) {
        println(i)
    }
}

fun main() {
    forEach()
    println("===============")
    forEach2()
    println("===============")
    forEach3()
}

->
a
b
c
===============
0, 10
1, 11
2, 12
===============
1
2
3
4
5
6
7
8
9
10
===============
10
8
6
4
2

 

특이하게 코틀린은 range라는 변수를 선언할 수 있어서 자바의 for문처럼 흉내를 낼 수 있다. 또한 downTo, step 문법을 지원해 역순으로 출력하는 것도 가능하고, withIndex로 리스트의 인덱스와 요소를 같이 받을 수도 있다.

 

맵에 대한 for문
fun forEach4() {
    val lowerUpperCaseMap = HashMap<Char, Char>()

    for (c in 'A'..'F') {
        val lowerCase = c.toLowerCase()
        //자바의 map.put(lowerCase, c)와 같다.
        lowerUpperCaseMap[lowerCase] = c
    }

    for ((lower, upper) in lowerUpperCaseMap) {
        println("$lower $upper")
    }
}

 

in으로 컬렉션이나 범위의 원소 검사
fun recognize(c: Char) = when (c) {
    //'0' <= c && c <= '9'
    in '0'..'9' -> "It's digit!"
    //'a' <= c && c <= 'z'
    in 'a'..'z', in 'A'..'Z' -> "It's a letter"
    else -> "I don't know"
}

 

Comparable 등이 구현되어 있다면, 일반 오브젝트에서도 "in" 사용이 가능하다.

 

코틀린의 예외 처리

코틀린은 자바와 같이 checked exception과 unchecked exception을 구분하지 않는다. 즉, 예외 처리를 강제하지 않아며, 메서드에 throws 문을 명시하지 않아도 된다.

 

fun readNumber(reader: BufferedReader): Int? {
    try {
        val line = reader.readLine() //checked exception이 발생
        return Integer.parseInt(line)
    } catch (e: NumberFormatException) {
        return null
    } finally {
        reader.close()
    }
}

 

위 코드의 경우 reader.readLine()이 IOException(checked exception)을 발생시키지만, 코틀린에서는 어디에도 다시 throw하거나, 캐치해서 처리하지 않는다. 즉, checked exception을 강제하지 않는 것이다.

 

또한 try-catch는 물론 식이므로 아래와 같이 코드를 변경할 수 있다.

 

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

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

    println(number)
}

 

하지만 조금 다른 것이 catch에서 null을 반환하냐, return 키워드를 쓰냐의 차이인데 return 키워드를 사용하면 catch문 아래의 로직은 수행하지 않고, 해당 메서드가 스택에서 반환된다.

 

여기까지 아주 간단한 코틀린 문법에 대해 다루어 보았다. 다음 포스팅에는 조금더 자세히 코틀린에 대해 다루어본다.

 

<참고 서적>

posted by 여성게
:

오늘 다루어볼 내용은 jdk1.8의 날짜&시간을 다루는 java.time 패키지를 다루어볼 것이다. 바로 java.time 패키지 내용을 다루기 전에 우선 프로그래밍에서의 날짜와 시간에 대해 표준인 ISO-8601에 대해 먼저 알아본다.

 

ISO-8601

<wiki>
ISO 8601 Data elements and interchange formats - Information interchange - Representation of dates and times은 날짜와 시간과 관련된 데이터 교환을 다루는 국제 표준이다. 이 표준은 국제 표준화 기구(ISO)에 의해 공포되었으며 1988년에 처음으로 공개되었다. 이 표준의 목적은 날짜와 시간을 표현함에 있어 명백하고 잘 정의된 방법을 제공함으로써, 날짜와 시간의 숫자 표현에 대한 오해를 줄이고자함에 있는데, 숫자로 된 날짜와 시간 작성에 있어 다른 관례를 가진 나라들간의 데이터가 오갈때 특히 그렇다.

일반적으로, ISO 8601는 그레고리력 (proleptic Gregorian도 가능)에서의 날짜와 (부가적으로 시간대 정보를 포함하는) 24시간제에 기반하는 시간, 시간 간격(time interval) 그리고 그들의 조합에 대한 표현과 형식에 적용된다. 이 표준은 표현할 날짜/시간 요소에 어떠한 특정 의미도 할당하지 않는다; 그 의미는 사용 맥락에 따라 달라질 것이다. 추가로, 표현될 날짜와 시간은 표준 내에서의 지정된 의미의 숫자(예를 들자면, 중국 달력의 년도 이름)가 아니고서는 단어를 포함할 수 없으며 단어들은 문자(예: 이미지, 소리)를 사용하지 않는다.

교환을 위한 표현에서, 날짜와 시간은 재배치되어서, 가장 큰 시간 용어(년도)가 왼쪽에 놓이며 각각의 더 작은 용어들은 이전 용어의 우측에 놓이게 된다. 표현은 아라비아 숫자와 표준 내에서 특정 의미를 제공하는 ("-", ":", "T", "W" 그리고 "Z"와 같은) 어떤 문자들로 작성되어야 한다. 그것이 의미하는 바는, "January" 혹은 "Thursday"처럼 날짜의 일부를 작성하는 어떤 평범한 방법이 교환 표현에서는 허용되지 않는다는 것이다.

참조 : https://ko.wikipedia.org/wiki/ISO_8601

 

위 내용은 위키 내용을 인용하였는데, 한마디로 세계에서 각기 다른 시간대에서 사용하기 위한 날짜&시간에 대한 표준을 정해놓은 것이다. 포맷으로는 보통 아래와 같은 포맷을 다룬다.(물론 기본형식이 있지만 아래 확장 형식을 대부분 사용하는 듯하다.)

 

- 날짜(년월일) : YYYY-MM-DD
- 날짜(년월) : YYYY-MM
- 날짜&시간 : YYYY-MM-DDThh:mm:ss(YYYY-MM-DDThh:mm:ss.sss)
- 시간 : hh:mm:ss(hh:mm:ss.sss)

 

날짜와 시간을 다루는데 아주 중요한 것중 하나는 "표준 시간대 지정자"이다. ISO-8601의 표준 시간대는 (불특정 위치의) "지역 시간"(local time), "UTC" 혹은 "UTC의 오프셋"으로 표현된다.

 

만약 UTC 관계 정보에 시간 표현이 함께 주어지지 않는다면, 시간은 지역 시간으로 간주된다. 동일한 시간대에서 통신 시 지역 시간을 가정하는 것이 가장 안전할지 몰라도, 시간대가 다른(국가와 국가간 시차) 지역간의 통신에서 지역시간을 사용하는 경우는 아주 모호하게 된다.

(UTC 오프셋이 표현되지 않는다면 해당 시간이 어느나라 기준의 시간인지 알수 없기에 각자 나라의 시간대로 표현이 불가능하다.)

 

UTC

시간이 UTC인 경우, 시간 뒤에 빈칸없이 "Z" 를 직접 추가해야 한다. Z는 오프셋이 0인 UTC를 위한 지역 지정자이다. 그러므로 "09:30:12"의 UTC는 "09:30:12Z"로 표현된다. 

 

UTC에서의 시간 오프셋

UTC에서의 오프셋은 위에서 Z를 붙였던 것과 동일한 방법으로 시간뒤에 덧붙인다. 우리나라는 기준시보다 9시간이 빠른 나라이기 때문에 시간을 UTC 오프셋으로 표현하게 되면 "09:30:12+09:00"으로 표현하게 된다. 이렇게 UTC 오프셋으로 시간을 표현하게 되면 기준시에 오프셋이 붙어있는 것이기 때문에 해당 시간으로 다른 시간대의 나라의 시간으로도 표현이 명확히 가능하게 된다.(느린 시간대라면 -로 오프셋을 표현한다.)

 

java.time 패키지의 핵심 클래스

날짜와 시간을 하나로 표현하는 Calendar클래스와 달리, java.time 패키지에서는 날짜와 시간을 별도의 클래스로 분리해 놓았다. 시간을 표현할 때는 LocalTime 클래스를 사용하고, 날짜를 표현할 때는 LocalDate클래스를 사용한다. 그리고 날짜와 시간이 모두 필요할 때는 LocalDateTime클래스를 사용하면 된다. 만약 여기에 Time-Zone까지 다뤄야 한다면, ZonedDateTime클래스를 사용한다.

 

LocalDateTime은 기본적으로 Time-zone이 없는 형태이다. 그 말은 ZoneOffset을 표기 하지 않는 날짜 형식을 출력해준다. 물론 ZoneOffset을 고려해서 LocalDateTime 오브젝트를 만들수 있다. 하지만 Time-zone이 없는 개념이기 때문에, 아래와 같이 출력이 된다.

 

LocalDateTime.now() - 2020-07-30T00:38:55.215245
ZonedDateTime.now() - 2020-07-30T00:38:55.215245+09:00[Asia/Seoul]

 

Jdk1.8 이전의 날짜&시간을 다루는 Calendar는 ZonedDateTime처럼, 날짜와 시간 그리고 시간대까지 모두 가지고 있다. Date와 유사한 클래스로는 Instant가 있는데, 이 클래스는 날짜와 시간을 초 단위(엄밀히는 나노초까지)로 표현한다. 날짜와 시간을 초단위로 표현한 값을 time stamp라고 부르는데, 이 값은 날짜와 시간을 하나의 정수로 표현할 수 있으므로 날짜와 시간의 차이를 계산하거나 순서를 비교하는데 유리해서 데이터 베이스에서 많이 사용한다.(타임스탬프는 타임존에 대한 정보없이 절대적인 시간을 다루기 때문에 아주 명확한 값을 가지게 된다.) 이외에도 날짜를 더 세부적으로 다룰 수 있는 Year, YearMonth, MonthDay와 같은 클래스도 있다.

 

예제 코드는 millisecond는 조금 다르지만 같은 시간이라 가정하자.

LocalDateTime.now() 
-> 2020-07-30T01:04:38.488822

LocalDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneId.systemDefault())
-> 2020-07-30T01:04:38.490

LocalDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneId.of("Asia/Seoul"))
-> 2020-07-30T01:04:38.490

LocalDateTime.ofInstant(Instant.ofEpochSecond(System.currentTimeMillis() / 1000), ZoneId.systemDefault())
-> 2020-07-30T01:04:38

LocalDateTime.ofInstant(Instant.ofEpochSecond(System.currentTimeMillis() / 1000), ZoneId.of("Asia/Seoul"))
-> 2020-07-30T01:04:38

LocalDateTime.ofInstant(Instant.ofEpochSecond(System.currentTimeMillis() / 1000), ZoneId.systemDefault()).plusDays(7)
-> 2020-08-06T01:04:38

LocalDateTime.ofEpochSecond(System.currentTimeMillis() / 1000, 0, ZoneOffset.UTC)
-> 2020-07-29T16:04:38

LocalDateTime.ofEpochSecond(System.currentTimeMillis() / 1000, 0, ZoneOffset.ofHours(9))
-> 2020-07-30T01:04:38

ZonedDateTime.now()
-> 2020-07-30T01:04:38.491441+09:00[Asia/Seoul]

LocalDateTime.now().atZone(ZoneId.of("Asia/Seoul"))
-> 2020-07-30T01:04:38.491568+09:00[Asia/Seoul]

 

작성중...

posted by 여성게
:

리액터의 switchIfEmpty라는 메서드를 다루기 전에 자바의 Lazy evaluation(지연 평가)에 대해 다루어보자. 자바는 논리 operation을 평가할때 lazy evaluation을 사용한다. 예제 코드를 예로 들면 아래와 같다.

 

@Test
void lazyEvaluationTest() {
    boolean isLazy = lazyEvaluation();
    if (true || isLazy) {
        System.out.println("method execute!!!");
    }
}
private boolean lazyEvaluation() {
    System.out.println("lazy evaluation");
    return true;
}

 

위 테스트에서 결과는 어떻게 될 것인가?

 

lazy evaluation
method execute!!!

 

위와 같이 사용하지 않는 boolean 변수이지만, 미리 isLazy를 만들기 위해 메서드를 호출한다. 여기서 사용하지 않는 변수라는 뜻은 자바에서는 논리 operation에서 이미 참 혹은 거짓이라고 판단이 난 논리식이면 나머지 값 자체를 참조하지 않는다. 이 예는 위와 같이 true || isLazy일때, 이미 앞의 true에서 참이 되었기에 뒤의 isLazy 자체를 참조하지 않는다. 이것을 테스트 하기 위해 아래와 같이 코드를 만들어보았다.

 

@Test
void lazyEvaluationTest() {
    if (true || lazyEvaluation()) {
        System.out.println("method execute!!!");
    }
}

private boolean lazyEvaluation() {
    System.out.println("lazy evaluation");
    return true;
}

 

위 예제코드에서는 lazyEvaluation()를 호출자체를 하지 않는다. 그렇다면 아래와 같은 코드는 결과가 어떻게 될까?

 

@Test
void lazyEvaluationTest() {
    System.out.println("before boolean operation");
    if (true && lazyEvaluation()) {
        System.out.println("method execute!!!");
    }
}

private boolean lazyEvaluation() {
    System.out.println("lazy evaluation");
    return true;
}

 

결과는 아래와 같다.

 

before boolean operation
lazy evaluation
method execute!!!

 

실제 lazyEvaluation()가 필요한 시점에 호출하게 되는 것이다. 여기까지 자바에서 논리 operation은 lazy evaluation 한다는 것은 알았고, 다른 상황에서는 어떻게 될까?

 

자바는 어떠한 지역변수에 값을 할당할때, 그리고 어떠한 메서드의 매개변수를 만들때는 eager evaluation 전략을 따른다. 그 말은 무엇이냐면 어떠한 메서드를 처리하기 전에 그 메서드의 매개변수의 값이 미리 준비가 되어 있어야한다는 것이다. 아래 예제코드를 한번 살펴보자.

 

@Test
void eagerEvaluationTest() {
    System.out.println(convertBooleanToString(eagerEvaluation(true)));
}

private String convertBooleanToString(final boolean bool) {
    System.out.println("convertBooleanToString");
    return bool ? "true" : "false";
}

private boolean eagerEvaluation(final boolean bool) {
    System.out.println("eager evaluation!!!!");
    return bool;
}

 

위 메서드의 결과는 어떻게 될까?

 

eager evaluation!!!!
convertBooleanToString
true

 

convertBooleanToString을 실행시키기전에 매개변수의 값을 미리 만들기 위해서 eagerEvaluation 메서드를 호출해 값을 만든다. 그렇다면 lazy evaluation하게 하려면 어떻게 하면 될까?

 

@Test
void lazyEvaluationTest2() {
    System.out.println(convertBooleanToString(() -> lazyEval(true)));
}

private String convertBooleanToString(final Supplier<Boolean> f) {
    System.out.println("convertBooleanToString");
    return f.get() ? "true" : "false";
}

private boolean lazyEval(final boolean bool) {
    System.out.println("lazy evaluation");
    return bool;
}

 

위와 같이 매개변수로 람다를 전달하면 된다. 람다를 전달하게 되면 해당 람다가 사용되는 시점에 메서드를 호출하기 때문에 lazy하게 프로그래밍하게 할 수 있다. 이것은 꼭 실행하지 않아도될 로직을 처리하지 않게 할 수 있고, 혹은 실행 시점을 뒤로 미룰 수도 있기 때문에 알고 있으면 아주 좋은 개념이 될것이다. 위 예제를 결과는 아래와 같다.

 

convertBooleanToString
lazy evaluation
true

 

실행 순서가 바뀐 것을 볼 수 있다. 그리고 매서드의 매개변수로 담기는 메서드의 lazy 로딩하는 것 외에 지역변수의 초기화를 지연 시킬 수 도 있다.

 

@Test
void lazyEvaluationTest3() {
    Supplier<Boolean> supplier = () -> lazyEval(true);
    System.out.println("before method");
    if (supplier.get()) {
        System.out.println("method !!");
    }
}

 

여기까지 자바의 eager, lazy evaluation을 다루어보았고, 이제 본론으로 switchIfEmpty를 사용할때 주의해야할 점을 알아보자 !

 

@Test
void switchIfEmptyTest() {
    Mono.just("str")
            .map(s -> {
                System.out.println(s);
                return s;
            })
            .switchIfEmpty(defaultStr())
            .subscribe();
}

private Mono<String> defaultStr() {
    System.out.println("defalutStr");
    return Mono.just("default");
}

<console>
defalutStr
str

 

위 코드를 보면 우리는 보통 Mono.just가 Mono.empty를 리턴하면 switchIfEmpty를 실행하겠지? 라는 생각을 하기 쉽다. 하지만, 실제 동작은 그렇지 않다. 자바에서는 보통 메서드의 매개변수의 값을 미리 결정시켜놓으려한다.(eager evaluation) 그래서 실제 map을 실행하기 전에 switchIfEmpty를 먼저 실행시켜 값을 만들어 놓는다. 만약 switchIfEmpty안에 실행되는 메서드가 비용이 크고, 실제로 Mono.just는 empty를 반환하지 않는다면, 굳이 실행되지 않아도 되는 비싼 비용의 메서드를 호출해야한다. 이럴때는 우리는 lazy evaluation 전략을 사용하여 switchIfEmpty안의 실행을 실제 필요시점으로 미룰 수 있게 하는 것이다. 

 

@Test
void switchIfEmptyTest2() {
    Mono.just("str")
            .map(s -> {
                System.out.println(s);
                return s;
            })
            .switchIfEmpty(Mono.defer(this::lazyDefaultStr))
            .subscribe();
}

@Test
void switchIfEmptyTest3() {
    Mono.just("str")
            .map(s -> {
                System.out.println(s);
                return s;
            })
            .switchIfEmpty(Mono.fromSupplier(() -> "defaultStr"))
            .subscribe();
}

private Mono<String> lazyDefaultStr() {
    System.out.println("defalutStr");
    return Mono.just("default");
}

<console>
str

 

위와 같이 Mono.defer 혹은 Mono.fromSupplier를 사용하면 해당 메서드의 매개변수로 Supplier를 넘기기 때문에 미리 값을 만들어 놓지 않고, 실제 호출되는 시점으로 실행을 지연시킬 수 있다. 위 코드에서는 아예 메서드 호출자체를 하지도 않는다. 필자도 지금까지는 이러한 것을 크게 인지하지 못하고 코딩을 했었는데, 이렇게 lazy programming을 조금 생각하고 코딩하게 된다면 조금이라도 성능향상을 할 수 있지 않을까 생각이 든다.

posted by 여성게
:

 

오늘은 클로저(Closure)와 커링(Currying)에 대해 다루어본다. 사실 이전에 자바스크립트를 간단히 공부하면서 봤던 기억이 있는 개념이었는데, 사실 정확한 개념을 알지 못하고 사용했던 것 같은데 이번에 정리해본다.

 

클로저(Closure)

클로저는 내부함수가 외부함수의 맥락(context)에 접근할 수 있는 것을 뜻한다. 그 뜻은 외부 함수안에 있는 내부 함수가 외부함수의 지역변수를 사용할 수 있다라는 뜻이다. 특이한 것은 외부 함수가 종료되더라도 내부함수에서 참조하는 외부함수의 context는 유지 된다는 것이다. 그것을 간단하게 자바 코드로 짜면 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
public class closure {
    @Test
    void closure() {
        final var supplier = outerMethod();
        System.out.println(supplier.get());
    }
 
    private Supplier<String> outerMethod() {
        final String str = "outer method local variable";
        return () -> str;
    }
}
cs

 

위 코드를 보면 outerMethod는 Supplier를 반환하는데, 그 내부의 Supplier는 외부 메서드의 지역변수를 참조해 그대로 리턴하고 있다. 그리고 해당 메서드를 사용하는 @Test 메서드를 보자. outerMethod()를 호출했고 그것을 변수로 받고 있는데, 이 시점에는 outerMethod()는 종료되어 소멸되었지만, Supplier를 get하면 이미 종료된 함수의 지역변수를 그대로 출력하고 있다. 어떻게 이미 종료된 외부함수의 지역변수를 참조할 수 있는 것일까? 그 이유는 클로저가 생성되는 시점에 함수 자체가 복사되어 따로 컨텍스트를 유지하기 때문이다. 조금더 자세히 설명하면 익명 클래스에 컨텍스트를 넘겨주는 것이 클로저다. 컴파일러는 이 필요한 정보를 복사해서 넘겨주는데 이를 Variable capture 라고 한다.

 

자바에서 클로저가 어떻게 동작하는지 조금 더 자세히 살펴보면, 내부함수가 사용하는 외부함수의 지역변수를 클로저가 생성되는 시점에 final로 간주된다. final로 간주된다는 뜻은 새로운 인스턴스를 할당하지 못하게 되는 것이다. 1.7이전 자바는 명시적으로 final을 붙여줘야했지만 1.8 이후부터는 외부함수의 지역변수는 유사파이널로 간주되어 final를 명시적으로 붙이지 않아도 컴파일 타임에 final로 간주하게 된다.

 

그리고 위 코드를 아래와 같이 변경하게 되면 컴파일 에러가 난다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
void closure() {
    final var supplier = outerMethod();
    System.out.println(supplier.get());
}
 
private Supplier<String> outerMethod() {
    String str = "outer method local variable";
    return () -> {
        str = "aa";
        return str;
    };
}
cs

 

final로 간주되는 str 변수에 새로운 주소값을 할당하려하니 컴파일 에러가 나는 것이다. 하지만 이를 우회하는 방법으로 객체를 사용할 수 있다. 객체는 final로 생성되더라도, 안에 프로퍼티를 변경 할수 있기 때문이다.

 

그렇다면 자바에서 람다와 클로저의 차이점은 무엇일까?

 

람다와 클로저의 차이점

람다와 클로저는 모두 익명의 특정 기능 블록이고, 차이점은 클로저는 외부 변수를 참조하고, 람다는 자신이 받는 매개변수만 참조한다는 것이다.

 

// Lambda.
(server) -> server.isRunning();

// Closure. 외부의 server 라는 변수를 참조
() -> server.isRunning();

 

즉, 자바에서 클로저는 외부 변수를 참조하는 익명 클래스이고, 람다는 메서드의 매개변수만 참조하는 익명클래스가 되는 것이다.

 

private Supplier<String> outerMethod() {
    String str = "outer method local variable";
    return new Supplier<String>() {
        @Override
        public String get() {
            return str;
        }
    };
}

 

 

커링(Currying)

Currying 은 1967년 Christopher Strachey 가 Haskell Brooks Curry의 이름에서 착안한 것이다. Currying은 여러 개의 인자를 가진 함수를 호출 할 경우, 파라미터의 수보다 적은 수의 파라미터를 인자로 받으면서 누락된 파라미터를 인자로 받는 기법을 말한다. 즉 커링은 함수 하나가 n개의 인자를 받는 과정을 n개의 함수로 각각의 인자를 받도록 하는 것이다. 부분적으로 적용된 함수를 체인으로 계속 생성해 결과적으로 값을 처리하도록 하는 것이 그 본질이다.

 

private static List<Integer> calculate(List<Integer> list, Integer a) {
  return list.map(new Function<Integer, Function<Integer, Function<Integer, Integer>>>() {
    @Override
    public Function<Integer, Function<Integer, Integer>> apply(final Integer x) {
      return new Function<Integer, Function<Integer, Integer>>() {
        @Override
        public Function<Integer, Integer> apply(final Integer y) {
          return new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer t) {
              return x + y * t;
            }
          };
        }
      };
    }
  }.apply(b).apply(a));
}

 

위와 같이 매개변수를 하나씩 받고 해당 매개변수가 일부반영된 Function을 다시 리턴하는 식으로 마지막 적용될 함수에 매개변수를 일부씩 적용시키는 것이다. 이것을 조금더 간소화 시키면,

 

private Stream<Integer> calculate(Stream<Integer> stream, Integer a) {
  return stream.map(((F3) x -> y -> z -> x + y * z).apply(b).apply(a));
}

 

위와 같이 적용도 가능하다.

 

https://futurecreator.github.io/2018/08/09/java-lambda-and-closure/

 

Java Lambda (7) 람다와 클로저

람다와 클로저 자바 커뮤니티에서는 클로저와 람다를 혼용하면서 개념 상 혼란이 있었습니다. 그래서 자바 8부터 클로저를 지원한다는 글을 보기도 합니다. 이번 포스트에서는 자바에서의 람다

futurecreator.github.io

 

http://egloos.zum.com/ryukato/v/1160506

 

Java 8의 문제점: 커링(currying)대 클로져(closure))

원문을 번역한 것입니다.Closure 예제커링 사용하기자동 커링currying의 다른 응용들정리

Java 8의 문제점: 커링(currying)대

 

egloos.zum.com

 

posted by 여성게
: