[Kotlin] 고급 문법 정리

참고 영상

https://www.inflearn.com/course/코틀린-요약-강의/dashboard

1. 람다식 (Lambda Expression)

람다식은 마치 value처럼 다룰 수 있는 익명함수이다.
1) 메소드의 파라미터로 넘겨줄 수 있다.
2) 리턴값으로 사용할 수 있다.

람다의 기본 정의
val lambdaName : Type = { argumentList -> codeBody }
val lambdaName = { argumentName : Type -> codeBody }

이미지 출처: https://youtu.be/z8prdZgk4kA?t=317

//val square : (Int) -> (Int) = {number -> number*number}
val square = {number: Int -> number*number}

val nameAge = {name: String, age: Int ->
    // 표현식이 여러 줄인 경우, 마지막 줄을 리턴함.
    "My name is $name I'm $age"
}

fun main(){
    println(square(12))
    println(nameAge("Haeun", 22))
}

144
My name is Haeun I'm 22

1-2. 확장 함수 (extension function)

확장 함수는 클래스에 함수를 추가하고 싶을 때 사용하는 익명함수이다. 예를 들어 String 클래스를 확장하는 익명함수를 만들어보자.

fun main(){
    val a = "haeun said"
    val b = "mike said"
    println(a.pizzaIsGreat())
    println(b.pizzaIsGreat())
}

val pizzaIsGreat : String.() -> String = {
    // this는 이 확장함수가 불러올 객체 (여기서는 String)
    "$this : pizza is the best!"
}

haeun said : pizza is the best!
mike said : pizza is the best!

확장 함수의 기본 형식
val 람다명 : 클래스명.(입력 타입) -> 출력 타입 = { 함수 내용 및 결과 반환 }

cf) 코틀린에서는 여러 개의 값을 리턴할 수 있을까 궁금해서 찾아보니, 무작위적인 튜플을 만들 수는 없고 Pair나 Triple 클래스, 배열, 리스트를 사용할 수 있다고 한다. 보다 일반적인 건 데이터 클래스를 사용하는 것!

https://stackoverflow.com/questions/47307782/how-to-return-multiple-values-from-a-function-in-kotlin-like-we-do-in-swift

1-3. 람다의 리턴

fun main() {
    val a = "haeun said"
    val b = "mike said"
    // 확장함수의 매개변수가 없는 경우
    println(a.pizzaIsGreat())
    println(b.pizzaIsGreat())
    
    // 확장함수의 매개변수가 있는 경우
    println(extendString("haeun", 22))
    println(calculateGrade(100))
}

// String 클래스를 확장하는 익명함수
val pizzaIsGreat : String.() -> String = { // 매개변수가 없는 경우
    // this는 확장함수가 불러올 String 객체 자체
    "$this : pizza is the best!"
}

fun extendString(name: String, age: Int) : String {
    // 매개변수가 1개인 경우 it를 사용한다.
    val introduceMyself: String.(Int) -> String = {
        // this는 확장함수가 불러올 String 객체 자체
        "I am $this and $it years olds"
    }
    return name.introduceMyself(age)
}

// 람다의 리턴
val calculateGrade : (Int) -> String = {
    when(it){ // when이 expression으로 쓰일 때는 else문 필수
        in 0..40 -> "fail"
        in 41..70 -> "pass"
        in 71..100 -> "perfect"
        else -> "Error"
    }
}

haeun said : pizza is the best!
mike said : pizza is the best!
I am haeun and 22 years olds
perfect

1-4. 람다를 표현하는 2가지 방법

fun main() {
    // 1. 람다식 정의 후 사용하기
    val lambda = { number: Double ->
        number == 4.3213
    }
    println(invokeLambda(lambda)) // 5.2343 == 4.3213 ? false

    // 2. 람다 리터럴 (중괄호를 바로 사용하는 경우)
    println(invokeLambda({ it > 3.22 })) // 5.2343 > 3.22 ? true
}

// 람다를 표현하는 2가지 방법
// 람다식은 마치 'value처럼' 사용할 수 있는 익명함수여서
// 함수의 매개변수 또는 리턴값이 될 수 있다.
fun invokeLambda(lambda: (Double) -> Boolean) : Boolean {
    return lambda(5.2343)
}

1-5. 이벤트를 처리하는 리스너에서 람다식 사용하기

기존의 자바는 xml에서 사용한 모든 뷰를 메모리에 객체화시키기 위해서 findViewById 메소드로 인플레이트를 시키는 과정을 거쳐야 했다. 이것은 매우 매우 번거로웠는데 아래 코드처럼 앱 단위의 build.gradle 파일에 '코틀린 안드로이드 익스텐센 플러그인'을 적용하면, 이 과정을 생략할 수 있다!!

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-android-extensions' // 익스텐션 플러그인 적용
}

아래 코드처럼 setOnClickListener에서도 람다식을 사용하면 획기적으로 코드 길이를 줄일 수 있다.

import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?){
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        button.setOnClickListener(object: View.OnClickListener{
            override fun onClick(p0: View?) {
                // to do...
            }
        })

        // View.OnClickListener처럼
        // 코틀린 인터페이스가 아닌 자바 인터페이스이고,
        // 인터페이스의 메소드가 하나만 있는 경우, 람다식으로 쓸 수 있다.
        button.setOnClickListener { 
            // to do...
        }
        
    }
}
// View.class 파일을 열어보면, OnClickListener 인터페이스에는 딱 하나의 메소드만 있다.
public interface OnClickListener {
        void onClick(View var1);
    }

2. 데이터 클래스

코틀린에서는 데이터를 담는 그릇이 되는 클래스를 따로 만들 수 있다. 자바는 POJO 클래스로 '비어있는 틀 역할'을 하는 클래스를 만들었지만, 이는 비슷한 코드가 여러 번 중복되어서 보일러 플레이트 코드가 생긴다는 문제가 있었다. 하지만 코틀린의 데이터 클래스를 사용하면 이런 문제를 해결할 수 있다.

https://charlezz.medium.com/보일러플레이트-코드란-boilerplate-code-83009a8d3297

예를 들어, 비행기 티켓에 대한 정보를 담는 Ticket 데이터 클래스를 만들어보자.

data class Ticket(
    val companyName: String,
    val name: String,
    var date: String,
    var seatNumber: Int
)

데이터 클래스는 위와 같이 주 생성자만 선언해도 toString(), hashCode(), equals(), copy() 메소드를 자동으로 생성해준다.

data class Ticket(
    val companyName: String,
    val name: String,
    var date: String,
    var seatNumber: Int
)

class TicketNormal(
    val companyName: String,
    val name: String,
    var date: String,
    var seatNumber: Int
)

fun main() {
    val ticketA = Ticket("koreaAir", "haeunLee", "2022-01-03", 14)
    val ticketB = TicketNormal("koreaAir", "haeunLee", "2022-01-03", 14)
    println(ticketA)
    println(ticketB) // ticketB 객체가 할당된 메모리 주소 출력
}

Ticket(companyName=koreaAir, name=haeunLee, date=2022-01-03, seatNumber=14)
com.tutorial.kotlinbasic.TicketNormal@2d6e8792

일반적인 클래스의 객체는 메모리 주소가 출력되지만, 코틀린의 데이터 클래스는 객체 하나가 담고 있는 정보를 보여준다.


3. Companion Object

https://wikidocs.net/228
https://www.bsidesoft.com/8187

자바에서 static은 언제 사용할까?

모든 객체가 동일한 값을 가진다면 매번 새롭게 메모리를 할당할 필요 없이 static으로 만들어주면 된다. static은 말그대로 한번 할당된 메모리가 '정적'이어서 모든 객체가 공유할 수 있다. static 멤버는 클래스 멤버라고도 불리는데, 그 이유는 객체를 생성하지 않고도 '클래스명으로' 메소드를 직접 호출할 수 있기 때문이다.

Companion object

companion object는 말그대로 이해하면 클래스와 함께 동반되는 객체이다. 클래스명으로 직접 호출할 수 있다는 점에서 자바의 static과 비슷해 보이지만, companion object는 어쨌든 객체라는 점에서 static과 다르다.

class MyClass{
    companion object{
        val prop = "나는 Companion object의 속성이다."
        fun method() = "나는 Companion object의 메소드다."
    }
}
fun main() {
    // companion object의 멤버에 접근할 때,
    // Companion 키워드를 생략하고 클래스명으로 바로 접근할 수 있다!
    println(MyClass.Companion.prop)
    println(MyClass.Companion.method())
    println(MyClass.prop)
    println(MyClass.method())

    val comp1 = MyClass.Companion // --(1)
    println(comp1.prop)
    println(comp1.method())

    // 명시적으로 객체를 생성할 때도 Companion 키워드 생략 가능
    val comp2 = MyClass // --(2)
    println(comp2.prop)
    println(comp2.method())
}

위 코드에서 주석 (1)처럼 companion 객체는 변수에 할당하는 것이 가능하지만, 자바의 static 멤버는 이런 것이 불가능하다.
그리고 주석 (2)에서 또 기억해야 할 것은 클래스 내에 정의된 companion object는 클래스 이름만으로도 참조 접근이 가능하다는 것이다.
여기서 알 수 있듯이 static 키워드만으로는 클래스 멤버를 companion object처럼 하나의 독립된 객체로 여길 수 없다.

Companion object가 왜 필요할까?

class Book private constructor(val id: Int, val name: String){
    companion object {
        fun create() = Book(0, "animal farm")
    }
}

fun main() {
    //val book = Book() // Cannot access '<init>': it is private in 'Book'
    val book = Book.Companion.create()
    println("${book.id} ${book.name}")
}

위의 코드처럼 private 생성자로 인해 외부에서 객체를 생성하지 못하지만 클래스의 멤버에는 접근하고 싶을 때, companion 객체를 사용할 수 있다.

class Book private constructor(val id: Int, val name: String){
    companion object BookFactory : IdProvider{
        override fun getId(): Int {
            return 100
        }
        val myBook = "new book"
        fun create() = Book(getId(), myBook)
    }
}

interface IdProvider {
    fun getId() : Int
}

fun main() {
    val book = Book.create()
    println("${book.id} ${book.name}")

    // companion object가 오버라이딩 한 메소드 역시 클래스명으로 접근 가능하다.
    val bookId = Book.getId()
    println(bookId)
}

4. Object

코틀린에는 자바에 없는 독특한 싱글턴(singleton; 인스턴스가 하나만 있는 클래스) 선언 방법이 있다. 아래처럼 class 키워드 대신 object 키워드를 사용하면 된다.

// Singleton Pattern: 인스턴스가 오직 하나만 생성되어야 하는 경우 사용하는 패턴
// CarFactory 객체는 모든 앱을 실행할 때 딱 한번만 만들어진다. (불필요한 메모리 사용 방지)
object CarFactory {
    val cars = mutableListOf<Car>()
    fun makeCar(horsePower: Int) : Car {
        val car = Car(horsePower)
        cars.add(car)
        return car
    }
}

data class Car(val horsePower: Int)

fun main() {
    // CarFactory 인스턴스는 컴파일할 때 한번만 만들어서 계속 재사용
    val car = CarFactory.makeCar(10)
    val car2 = CarFactory.makeCar(200)
    println(car)
    println(car2)
    println(CarFactory.cars.size.toString())
}

Car(horsePower=10)
Car(horsePower=200)
2

object는 var obj = object: MyClass(){} 또는 var obj = object: MyInterface{} 처럼 특정 클래스나 인터페이스를 확장해서 만들 수 있으며, 선언문이 아닌 표현식(var obj = object{})으로 생성할 수 있다.
싱글톤이기 때문에 object의 메소드를 정의하여 시스템 전체에서 사용되는 기능을 수행하는 데는 큰 도움이 될 수 있지만, 전역 상태를 유지하다가 스레드 경합 등의 문제가 생길 수 있으니 주의해야 한다.
cf) 경합: 어떤 스레드가 다른 스레드가 획득하고 있는 락(lock)이 해제되기를 기다리는 상태

언어 수준에서 안전한 싱글턴을 만들어 준다는 점에서 object는 매우 유용하다.

좋은 웹페이지 즐겨찾기