본문 바로가기

3.구현/Java or Kotlin

Kotlin 배우기2 - 심화

들어가기

심화에서는 kotlin만의 특징적인 기능과 추가적인 부분을 정리했다.

작성자: ospace114@empal.com, http://ospace.tistory.com/

연산자

“?.” 연산자를 사용해서 객체가 null아닌 경우만 참조할 수 있다.

fun main() {
  var str:String? = null
  var len = str?.length
  println("length is $len")
}

“?:(elvis)” 연산자를 사용해서 객체가 null인 경우 기본값을 정의할 수 있다.

fun main() {
  var str:String? = null
  var len = str?.length ?: 0
  println("length is $len")
}

“!!.” 연산자를 사용해 객체가 절대 null이 되면 안되게 정의할 수 있다.

fun main() {
  var str:String? = null
  var len = str!!.length // NullPointerException 예외 발생
  println("length is $len")
}

is 연산자는 java의 instanceOf와 같은 연산자로서 객체가 해당 클래스의 인스탄스인지를 확인한다.

fun main() {
  var str:String = "Hello Foo"
  if (str is String) println("str is String")
}

let을 사용해 객체에 대한 람다식 호출한다.

fun main() {
  var str:String = "Hello Foo"
  str.let {
      println("size is ${it.length}.")
  }
}

람다식 인자가 있다면 블록 내에서 인자로 액세스할 수도 있지만 없다면 블록 내에서 it 명칭을 사용할 수 있다.

데이터 클래스

데이터 클래스는 속성만 있는 클래스로 class 앞에 data 키워드를 사용한다.

data class Vec2(val x: Int, val y: Int)

fun main() {
    var vec = Vec2(10, 20)
    println(vec)
}

data를 사용하면 다음 메소드가 자동 생성된다. 만약 일반 클래스이면 구현해야 한다.

  • toString()
  • equals(): == 연산자에서 호출
  • hashCode()

가급적 데이터 클래스는 불변 클래스(val 사용)를 권하며 속성 값 수정할 경우 copy()을 사용해서 객체 복제해서 속성 변경한다.

data class Vec2(val x: Int, val y: Int)

fun main() {
  var vec1 = Vec2(10, 20)
  var vec2 = vec1.copy(y=30)
  println(vec1)
  println(vec2)
}

열거형 클래스

열거형은 값을 나열하는 타입을 정의한다. class 앞에 enum 키워드를 붙여서 사용한다.

enum class Color {
  RED, GREEN, BLUE,
}

fun main() {
  println(Color.RED)
  for (c in Color.entries) println(c.toString())
  println("first is ${Color.valueOf("RED")}")
  println(Color.RED.name)    // 이름
  println(Color.RED.ordinal) // 위치
  // 제너릭 함수 사용(참고용)
  // fun <T> enumValues(): Array<T>
  println(enumValues<Color>().joinToString {it.name})
  // fun <T> enumValueOf(name: String): T
  println(enumValueOf<Color>("RED"))
}

각 값은 객체이고 콤마로 구분한다. 열거형에 각 값은 enum class의 인스탄스화 객체이다.

enum class Color(val rgb: Int) {
  RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF),
}

fun main() {
  println(Color.RED)
  println(Color.REG.rgb)
}

enum class에 추상 메소드를 선언하고 각 객체에서 이를 구현할 수 있다.

enum class ProtocolState {
    WAITING {
        override fun signal() = TALKING
    },
    TALKING {
        override fun signal() = WAITING
    };
    abstract fun signal(): ProtocolState
}

fun main() {
  var state = ProtocolState.WAITING
  println(state)
  state = state.signal()
  println(state)
}

주의할 부분은 객체 나열에 마지막에 세미콜론(”;”)으로 마무리하고 추상 메소드를 선언해줘야 한다.

이런 형태의 메소드 오버라딩은 인터페이스 구현에도 적용할 수 있다. 또한 enum class은 기본적으로 Comparable 인터페이스가 구현되어 있다. 크기 비교는 순서에 의해 비교 된다.

연산자 재정의

산술 연산자를 재정의할 수 있다. 재정의할 때에 기존 연산자 대신 약속된 메소드 명을 사용해야 한다.

이항 연산자 메소드

  • a + b: a.plus(b)
  • a - b: a.minus(b)
  • a * b: a.times(b)
  • a / b: a.div(b)
  • a % b: a.rem(b)

예를 들어 보자. Vector에서 대해 “+” 연산자를 재정의하려면 plus()을 정의해야한다. 정의할 때에 operator 키워드를 앞에 붙여주면 된다.

data class Vec2(val x: Int, val y: Int) {
  operator fun plus(other: Vec2): Vec2 {
    return Vec2(x + other.x, y + other.y)
  }
}

fun main() {
  var vec1 = Vec2(10, 20)
  var vec2 = Vec2(30, 40)
  println(vec1 + vec2)
}

이외의 연산자 메소드들은 다음과 같다.

단항 연산자 메소드

  • +a: a.unaryPlus()
  • a: a.unaryMinux()
  • !a: a.not()

증감 연산자 메소드

  • ++a: a.inc()
  • --a: a.dec()

복합 대입 연산자 메소드

  • +=: a.plusAssign(b)
  • =: a.minusAssign(b)
  • =: a.timesAssign(b)
  • /=: a.divAssign(b)
  • %=: a.remAssign(b)

비교 연산자 메소드

  • a == b: a?.equals(b)?:(b==null)
  • a != b: !(a?.equals(b)?:(b==null))
  • a > b: a.compareTo(b) > 0
  • a < b: a.compareTo(b) < 0
  • a >= b: a.compareTo(b) >= 0
  • a <= b: a.compareTo(b) <= 0

인덱스 접근 연산자 메소드

  • a[i]: a.get(i)
  • a[i,j]: a.get(i,j)
  • a[i_1,...,i_n]: a.get(i_1,...,i_n)
  • a[i] = b: a.set(i,b)
  • a[i,j] = b: a.set(i,j,b)
  • a[i_1, ..., i_n] = b: a.set(i_1,...,i_n,b)

그외 연산자 메소드

  • a in b: a.contain(b)
  • a..b: a.rangeTo(b)
  • a(): a.invoke()

클래스 확장

기존 클래스에 속성과 메소드를 추가하려면 클래스 내에서 추가 작업이 필요하지만, 외부에서도 확장할 수 있다.

class Greeting(var name:String = "") {
  fun say() = println("Hello $name!")
}

val Greeting.isEmpty: Boolean
  get() = 0 == name.length

fun Greeting.say2() {
  if (isEmpty) {
    println("Hello world!")
  } else {
    println("Hello $name!")
  }
}

fun main() {
  var greeting1 = Greeting()
  var greeting2 = Greeting("Foo")
  greeting1.say()
  greeting1.say2()
  greeting2.say()
  greeting2.say2()
}

외부에서 확장할 때에 기존 속성이나 메소드를 오버라이드할 수 없다.

위임

클래스

일반 클래스는 final로 되어 있기 때문에 상속이 불가능하다. 위임 패턴을 사용해 일반 클래스를 확장할 수 있다. 이럴 경우에는 확장될 클래스를 속성으로 정의하고 모든 메소드들을 확장될 클래스에 동일한 메소드명으로 구현해줘야 한다.

interface Sayable {
  fun hi()
  fun bye()
}

class Greeting : Sayable {
  override fun hi() = println("Hello!")
  override fun bye() = println("Bye!")
}

class GoodMorning(val impl:Sayable) {
  fun hi() = println("Good morning!")
  fun bye() = impl.bye()
}

fun main() {
  var greeting = GoodMorning(Greeting())
  greeting.hi()
  greeting.bye()
}

모든 메소드명을 구현하기에는 매우 번거롭기 때문에 by 키워드를 사용해서 확장될 클래스 메소드들을 넘겨주는 기능이 있다.

interface Sayable {
  fun hi()
  fun bye()
}

class Greeting : Sayable {
  override fun hi() = println("Hello!")
  override fun bye() = println("Bye!")
}

class GoodMorning(val impl:Sayable) : Sayable by impl {
   override fun hi() = println("Good morning!")
}

fun main() {
  var greeting = GoodMorning(Greeting())
  greeting.hi()
  greeting.bye()
}

상속하지 않고 위임을 통해 클래스를 확장할 수 있다.

속성

getter/setter를 다른 객체에게 처리를 위임할 수 있다. 예를 들어 디버깅용으로 각 속성에 값을 변경시 출력하고 싶다면 다음 처럼 할 수 있다.

class DebugProp(var name:String) {
  var value:String = ""
  fun getter(): String = value
  fun setter(newValue: String) {
    println("SET: $name[$newValue]")
    value = newValue
  }
}

class Greeting {
  val debugProp = DebugProp("name")
  var name:String
    get() = debugProp.getter()
    set(value) = debugProp.setter(value)

  constructor(name: String) {
    this.name = name
  }

  fun say() = println("Hello $name!")
}

fun main() {
  var greeting = Greeting("Foo")
  greeting.say()
}

getter/setter도 by 키워드를 사용해서 다른 객체로 처리를 위임할 수 있다.

import kotlin.reflect.*

class DebugProp {
  var value:String = ""
  operator fun getValue(obj: Any?, prop: KProperty<*>): String = value
  operator fun setValue(obj: Any?, prop: KProperty<*>, newValue: String) {
    println("SET: ${prop.name}[$newValue]")
    value = newValue
  }
}

class Greeting {
  var name:String by DebugProp()

  constructor(name: String) {
    this.name = name
  }

  fun say() = println("Hello $name!")
}

fun main() {
  var greeting = Greeting("Foo")
  greeting.say()
}

아주 간단하게 속성에 대한 위임 처리가 가능하다.

by에 의해서 위임 처리할 클래스를 정의할때에 조건이 필요하다.

  • getter의 메소드 명은 getValue이고 첫번째 인자는 첫번째는 객체를 받기위한 Any형, 두번째 인자는 속성 정보를 받기 위한 KProperty<*>또는 Any형이다.
  • setter의 메소드 명은 setValue이고 첫번째 인자와 두번째 인자는 getter와 동일하고 세번째 인자는 할당할 새로운 값과 동일한 자료형을 사용하거나 Any형을 사용한다.

observable

위임을 좀더 쉽게 하기위해 observable()을 사용할 수 있다. Delegates의 observable() 메서드를 통해 Delegator같은 위임 객체를 만들어준다.

import kotlin.properties.Delegates

class Greeting {
  var name:String by Delegates.observable("") {
    prop, oldValue, newValue -> println("SET ${prop.name}[$oldValue] = $newValue")
  }
  constructor(name: String) {
    this.name = name
  }
  fun say() = println("Hello $name!")
}

fun main() {
  var greeting = Greeting("Foo")
  greeting.say()
}

observable()호출할 때 초기값을 넘겨준다. 초기값은 호출은 안된다.

map 위임

map객체를 위임에 사용할 수 있다. 위임을 사용하면 map 객체에서 필요한 값만 추출해서 저장된다.

class Greeting(val map: MutableMap<String, Any?>) {
  var msg: String by map
  var name: String by map
}

fun main() {
  var greeting = Greeting(mutableMapOf("msg" to "Hello", "name" to "Foo"))
  println("msg[${greeting.msg}] name[${greeting.name}]")
}

지연 초기화

지연 초기화는 속성 값을 객체 생성시 초기화하는게 아니라 사용할 때에 초기화하는 방식이다. 보통 초기화하는데 오래 걸리는데 당장 필요로하지 않은 경우에 사용한다.

fun loadData() : List<String> {
  return listOf("AA", "BB", "CC")
}

class Foo {
  private var dataList_: List<String>? = null
  val dataList: List<String>
    get() {
      if (null == dataList_) dataList_ = loadData()
      return dataList_!!
    }
}

fun main() {
    var foo = Foo()
    println(foo.dataList)
}

매번 lazy을 위해 구현하기 번거롭다. by를 사용하면 간편하게 처리할 수 있다.

fun loadData() : List<String> {
  return listOf("AA", "BB", "CC")
}

class Foo {
  val data by lazy({ loadData() })
}

fun main() {
    var foo = Foo()
    println(foo.data)
}

val을 사용해서 일반적으로 처음 한번 초기화되고 변경될 수 없도록 한다.

인라인 함수

람다를 사용할 경우 메모리 할당과 가상 호출 때문에 런타임 오버헤드가 있다. 고차 함수에서 인라인 함수로 정의하면 이런 오버헤드를 줄일 수 있다.

inline fun concat(l: String, r: String) = l + r

fun main() {
    var msg = concat("Hello ", "world")
    println(msg)
}

concat()이 호출 되는 부분을 cancat() 내용으로 치환된다. 간혹 인라인으로 사용하지 않으려면 함수 인자 앞에 noinline을 사용하면 된다.
주의할 부분은 인라인을 사용할 경우 바이트 코드가 커진다.

오브젝트

Object 표현은 익명 클래스의 객체 생성한다. 클래스 정의시 class 선언 없이 바로 선언하고 1회성으로 사용된다. 익명 클래스 형태로 선언되고 바로 객체가 생성된다.

fun main() {
    val helloWorld = object {
        val msg = "Hello World"
        override fun toString() = msg
    }
    println(helloWorld)
}

간단하게 부모 클래스를 상속해서 구현할 수 있다.

interface Sayable {
    fun say()
}

fun run(obj:Sayable) = obj.say()

fun main() {
    val helloWorld = object : Sayable {
        val msg = "Hello World"
        override fun say() = println(msg)
    }
    run(helloWorld)
}

일반 클래스에서 생성된 객체와 동일하게 사용할 수 있다. 이벤트 핸들러를 등록할 때 유용하게 활용할 수 있다.

object을 사용해 싱글패턴 구현에도 유용하다.

object Env {
    val msg = "Hello World"
}

fun main() {
    println(Env.msg)
}

data object로 데이터 객체를 생성할 수 있다. 일반적인 데이터 클래스와 동일한 기능을 수행하지만 toString()인 경우는 data object 이름만 출력한다.

data object Greeting {
    val msg = "Hello World"
}

fun main() {
    println(Greeting)
}

주의할 부분은 data object 사용할 때에 equals()와 hashCode()을 재정의할 수 없다.

클래스 내에서도 object을 정의할 수 있다. 이때에는 companion 키워드를 붙여주어야 한다. object 이름은 있어도 상관없지만 무시된다.

class Greeting {
    companion object {
        fun create(): Greeting = Greeting()
    }
    fun say() = println("Hello World")
}
fun main() {
    val greeting = Greeting.create()
    greeting.say()
}

정적 메소드 호출처럼 사용할 수 있다. 물론 속성도 동일하게 처리된다.

Collection

컬랙션에는 mutable과 immutable인 두가지 종류가 있다. mutable을 읽기와 쓰기가 가능하고 immutable을 읽기만 가능하다.

List

데이터 목록을 관리한다.

List는 기본적으로 immutable로 변경이 불가능 하다. mutable는 MutableList나 ArrayList을 사용한다.

fun main() {
    val list1: List<Int> = List(5, {i -> i*2}) // 개수, 초기화식
    val list2 = listOf(1, 2, 3, 4, 5)
    println(list1)
    println(list2)
    println(list2[0])
    var list2 = MutableList<Int>(5, {i -> i*2})
    var list3 = mutableListOf(1, 2, 3, 4, 5)
    list3[0] = 10
    list3.add(6)
    println(list2)
    println(list3)
    var list4 = ArrayList<Int>()
    var list5 = arrayListOf(1, 2, 3, 4, 5)
    println(list4)
    println(list5)
}

listOf()으로 List 객체를 생성한다. mutableListOf()으로 MutableList 객체를 생성한다.

Set

중복 허용하지 않고 정렬되지 않았다. mutable과 immutable 타입이 있다.

fun main() {
    val set1: Set<Int> = setOf<Int>(1,3,4,2,3)
    println(set1)
    val set2: MutalbeSet<Int> = mutableSetOf(1,3,4,2,3)
    val set3: MutalbeSet<Int> = hashSetOf(1,3,4,2,3)
    set2.add(5)
    set3.remove(4)
}

정렬된 set은 SortedSet을 사용한다.

fun main() {
    val set1: Set<Int> = sortedSetOf<Int>(3, 2, 1, 5, 4)
    println(set1)
}

Map

키값에 의해 관리한다. mutable과 immutable 타입이 있다.

fun main() {
    val map1: Map<String, Int> = mapOf("one" to 1, "two" to 2)
    val map2: Map<String, Int> = mapOf(Pair("one", 1), Pair("two", 2))
    val map3: MutableMap<String, Int> = mutableMapOf("one" to 1, "two" to 2)
    val map4: HashMap<String, Int> = hashMapOf("one" to 1, "two" to 20)
    map3.put("three", 3)
    map4.set("two", 2)
    map4.remove("one")
}

참조

[1] Kotlin docs, https://kotlinlang.org/docs/home.html
[2] online kotlin builder, https://play.kotlinlang.org/
[3] Collection interface, https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-collection/

반응형