가성비 좋게 코틀린 시작해보기 1

juhyeon·2020년 9월 30일
5

특징만 하나하나 정리하는건 내가 오래 기억하지 못하는 공부법이기 때문에.. 코드와 함께 알게된 점들을 한번 주절주절 해보겠다 😎

Data Classes

data class Person(val name: String, val age: Int)

fun getPeople(): List<Person> {
    return listOf(Person("Juhyeon", 25), Person("Dev", 1))
}

modifier data 는 클래스의 equals(), hashCode(), toString() 을 생성해준다.
ToString() 은 이런 포맷이다 => "Person(name=Juhyeon, age=25)"

Null Safety

// java ver.
public void sendMessageToClient(
    @Nullable Client client,
    @Nullable String message,
    @NotNull Mailer mailer
) {
    if (client == null || message == null) return;

    PersonalInfo personalInfo = client.getPersonalInfo();
    if (personalInfo == null) return;

    String email = personalInfo.getEmail();
    if (email == null) return;

    mailer.sendMessage(email, message);
}

Java 의 경우 null reference 멤버에 접근할때 NPE 를 유발할 수 있다.
이에 반해 코틀린은 코드에서 발생하는 NPE 를 제거하고자 하는데, Safe Call 이라는 걸 이용한다.

위 코드는 코틀린에서 이렇게 바뀐다.

fun sendMessageToClient(
    client: Client?, message: String?, mailer: Mailer
){
    val persionalInfo: PersonalInfo? = client?.personalInfo // 생략 가능
    val email: String? = persionalInfo?.email
    if (email != null && message != null) {
    	mailer.sendMessage(email, message)
    }
}

여기선 두가지를 볼 수 있다.

  1. 코틀린은 null 이 될 수 있는 것과, 절대 null 이 되지 않는 것을 구별한다.
    그래서 null 이 될 수 있는 것에 대해서는 Safe Call 을 할 수 있는데, (당연히 not null variable 에는 불필요하다)

    val a = "hyeondev"
    val b: String? = null
    println(b?.length)
    println(a?.length) // Unnecessary safe call

    이렇게 null 일 수 있는 변수 (null 체크가 필요한 변수) 에 ? 를 붙임으로서,
    if (b == null) 과 같은 조건문을 간소화시킬 수 있다.

    이게 만약 오브젝트 형이라고 생각하면, 이런 꼴까지 만나게 될 수 있다 : bob?.department?.head?.name

  2. 코틀린의 getter 는 그냥 변수에 접근하듯 작성한다. (persionalInfo?.email)
    Java st : persionalInfo.getEmail

    음 그런데 아직은 내가
    class Client (val personalInfo: PersonalInfo?)
    이렇게 "오브젝트 생성자 파라미터로 들어가는 타입 변수를 꺼내올때" 만을 경험해봐서 단언하진 못하겠다.
    객체 생성시 전달하는 파라미터를 get 하는 경우 말고 일반 멤버변수를 꺼내올 땐 어떻게 가져올지 봐야겠다 🧐

Smart Cast

코틀린에서는 변수를 내가 굳이 원하는 타입으로 캐스팅 (explicit casting) 하지 않더라도
컴파일러가 알아서 safe-casting 해주는 Smart cast 라는 기능이 있다.
컴파일러는 is 라는 키워드를 자동으로 검사 -> auto casting 까지 해주는데,
이걸 smart cast 라고 한다.

기존에 자바에서 casting 은 이런식으로 해왔을 것이다.

if (item instanceof Number) {
    return ((Number) item).getValue();
}
if (item instanceof Sum) {
    Sum sum = (Sum) item;
    return eval(sum.getLeft()) + eval(sum.getRight());
}

이렇게 자바에서 하나하나 조건을 걸어 명시적으로 캐스팅해줬던 아이들을 코틀린에서는 is 키워드로 간단히 해결한다.

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

캐스팅은 컴파일러가 수행하고 있으므로, 컴파일러가 이걸 안전하게 캐스팅하지 못한다고 판단했다면 작성한 구현부를 실행하지 않는다.
참고로 나는 '구현부' 라고 표현했는데 reference 에서는 check and usage 로 표현하고 있다.

unsafe casting

물론 unsafe casting 도 있다. 이건 as 키워드로 작성하는데,

val x: String = y as String

이걸 unsafe casting 이라고 부르는 이유는 캐스팅이 불가능한 경우 exception 을 던지고 있기 때문이다.

위 코드에서 ynull 이면 NPE 를 던질 수 있는데, 이런 상황에서도 코드를 유효하게 만들어주고 싶다면 다음과 같이 작성하면 된다.

val x: String? = y as String?

 

this

이미 제공되고 있는 클래스에 확장 함수를 작성하고자 할때, this 라는 키워드를 사용할 수 있다.
Extension function 을 작성하기 때문에 특정 reference 를 통해 내가 정의한 함수를 호출할 수 있게 되는건데, 이 함수 구현부에서는 this 로 그 reference 가 가진 값을 그대로 가져올 수 있다.

이미 자바에 익숙해진 나로선 좀 적응하기 힘들겠다고 느낀 특징이다 😭

fun Int.r(): RationalNumber = RationalNumber(this, 1) // this = int type value
fun Pair<Int, Int>.r(): RationalNumber = RationalNumber(this.first, this.second)

data class RationalNumber(val numerator: Int, val denominator: Int)

 

Object Expressions

Object Expression 는 특정 슈퍼 클래스를 상속한 자식 클래스를 작성하고 (변경하고)
익명으로 자식 클래스의 객체를 전달해주기 위해 사용한다. (자식 클래스 = anonymous class)

상속으로 설명했지만 reference 를 참고해보니 extends, implements 모두 해당되는 듯 하다.

여기서 object 키워드로 익명 클래스로 만든 그 객체를 표현하게 되는건데,
이걸 이해하기 위해 코틀린 레퍼런스 속 코드를 몇개 들고와봤다 😎

window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /*...*/ }

    override fun mouseEntered(e: MouseEvent) { /*...*/ }
})

여기에는 MouseAdapter 를 상속받는 익명 클래스가 작성되어 있다.
mouseClicked()mouseEntered() 를 필요에 맞게 오버라이딩 한 뒤,
이 객체를 addMouseListener() 에 전달해 주었다.

object : 슈퍼 클래스 객체 {
    // method overriding
}

즉, 위와 같은 포맷으로 특정 메소드를 override익명 클래스의 객체를 생성해서 넘겨주는 개념이다.

한가지만 더 보고 가겠다 (아직 잘 모르겠다)

open class A(x: Int) {
    public open val y: Int = x
}

interface B { /*...*/ }

val ab: A = object : A(1), B {
    override val y = 15
}

상속받으려고 하는 클래스 생성자에 파라미터가 존재할 경우에는 또 파라미터를 넘겨주어야 한다.

여기선 A 클래스를 상속 + B 클래스를 구현하며 val y 를 재정의하는 클래스의 anonymous object 를 만들어서 => ab 에 저장하고 있다.

fun foo() {
    val adHoc = object {
        var x: Int = 0
        var y: Int = 0
    }
    print(adHoc.x + adHoc.y)
}

이 뿐 아니라 특별히 상속받을 클래스가 없는 경우에도 object expression 을 사용한다.
Reference 에서는 just an object 라고 표현하는데,
클래스 이름과 타입이 없을 뿐, 객체가 생성되어 adHoc 에 저장된다.

Note that!

object expression 을 사용하면서 주의할 점도 있다.

class C {
    // Private function, so the return type is the anonymous object type
    private fun foo() = object {
        val x: String = "x"
    }

    // Public function, so the return type is Any
    fun publicFoo() = object {
        val x: String = "x"
    }

    fun bar() {
        val x1 = foo().x        // Works
        val x2 = publicFoo().x  // ERROR: Unresolved reference 'x'
    }
}

anonymous object 는 local | private 으로 선언했을 때만 anonymous object type 으로서 사용할 수 있다.

object 키워드를 사용해서 생성한 anonymous object 가 결과적으로 public method 나 property 에 저장된다면

  • 그 method | property 는 실제론
    anonymous object 가 상속받은 슈퍼클래스 타입이 되거나, Any 타입이 된다.
  • 따라서 anonymous object 에서 추가된 멤버들은 사용할 수 없다.

SAM 변환

코틀린은 코드 간소화를 위하여 람다 표현식을 지원하고 있는데,
지원하는 범위 중 하나인 SAM 변환 에 대해 다루어 보겠다.

코틀린은 하나의 함수만을 가지고 있는 자바 Functional Interface 에 대해 SAM 변환을 지원한다.

Kotlin function literals can be automatically converted into implementations of Java interfaces with a single non-default method

SAM 생성자는 컴파일러가 코틀린 람다식을 자바의 functional interface 포맷으로 자동 변환시켜주는 함수이다.
따라서 우리는 그냥 인터페이스의 추상함수 내부 구현만 람다식으로 표현해주면 된다.

그럼 컴파일 후엔 자바 인터페이스를 구현한 객체가 될 수 있다! 는 특징이 바로 SAM 변환이다.

setMyInterface()MyInterface 인터페이스를 구현한 객체를 매개변수로 받는다고 명시적으로 선언된 함수이다.
따라서 코드에서 람다만 전달하더라도
컴파일러는 알아서 매개변수에 선언된 (인터페이스를 구현한) 익명 클래스를 자동으로 만들어준다.

그리고 애초에 SAM 변환을 지원하는 대상은 Functional Interface 이다.

따라서 그 인터페이스에 추상 함수가 1개만 있으므로,

  • 컴파일러는 인터페이스에 존재하는 1개의 추상 메소드를 오버라이드 받고
  • 개발자가 작성한 코드(println("hello sam")) 를 오버라이드 받은 함수 내부에 추가해주는 작업을 한다.

한마디로, 코틀린은 자바로 작성된 Functional Interface 에 한해 SAM 변환을 지원하여 인터페이스 구현부를 람다식으로 표현할 수 있다.

Code

object expression 을 사용하여 어떤 리스트를 내림차순으로 정렬하는 로직은 다음과 같이 구현할 수 있다.

import java.util.*

fun getList(): List<Int> {
    val arrayList = arrayListOf(1, 5, 2)
    Collections.sort(arrayList, object : Comparator<Int> {
        override fun compare(x: Int, y: Int) = y - x
    })
    return arrayList
}

여기서 구현한 Comparatorcompare() 메소드 1개만 가지고 있는 Functional Interface 이다.

따라서 코틀린에서 지원하는 SAM 변환을 사용하여 이렇게 바꿔 작성할 수 있다.

import java.util.*

fun getList(): List<Int> {
    val arrayList = arrayListOf(1, 5, 2)
    Collections.sort(arrayList, { x, y -> y - x })
    return arrayList
}

이건 역시 Comparator 가 Java 의 Functional Interface 였기 때문에 가능한 부분이다.

 

References

profile
Just do it~ 😎

0개의 댓글