관례(convention): 어떤 언어 기능과 미리 정해진 이름의 함수를 연결해주는 기법
- 기존 자바 클래스가 구현하는 인터페이스는 이미 고정되어있기 때문에 코틀린쪽에서 확장 함수를 사용해서 기존 클래스에 새로운 메소드를 추가할 수 있게 만들었다
7.1 산술 연산자 오버로딩
- 연산자를 오버로딩하는 함수 앞에는 operator를 붙여줘야한다
7.1.1 이항 산술 연산 오버로딩
- operator O
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}
fun main() {
val p1 = Point(10, 20)
val p2 = Point(30, 40)
val p3 = p1 + p2 // p1.plus(p2)
println(p3) // Point(x=40, y=60)
}
- operator X
data class Point(val x: Int, val y: Int) {
fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}
fun main() {
val p1 = Point(10, 20)
val p2 = Point(30, 40)
val p3 = p1 + p2
println(p3) // 'operator' modifier is required on 'plus' in 'Point'
}
- 오버로딩 가능한 이항 산술 연산자
- 오버로딩을 하더라도 연산자간 우선순위는 달라지지않는다.
식 함수 이름
a/b | div |
a*b | times |
a%b | mod(1.1부터 rem) |
a+b | plus |
a-b | minus |
- 연산자를 오버로딩 하더라도 교환 법칙을 지원하지 않는다.
data class Point(val x: Int, val y: Int) {
operator fun times(scale: Double): Point {
return Point((x * scale).toInt(), (y * scale).toInt())
}
}
fun main() {
val p1 = Point(10, 20)
println(p1 * 1.5) // Point(x=15, y=30)
}
data class Point(val x: Int, val y: Int) {
operator fun times(scale: Double): Point {
return Point((x * scale).toInt(), (y * scale).toInt())
}
}
fun main() {
val p1 = Point(10, 20)
println(1.5 * p1) // compile error
}
7.1.2 복합 대입 연산자 오버로딩
- +=, -= 같은 연산자를 복합 대입 연산자라 부른다.
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}
fun main() {
var p1 = Point(10, 20)
p1 += Point(3, 4) // p1.plus(Point(3,4))
println(p1) // Point(x=13, y=24)
}
- 코틀린 표준 라이브러리는 변경 가능한 컬렉션에 대해 plusAssign을 정의한다.
operator fun <T> MutableCollection<T>.plusAssign(element: T) {
this.add(element)
}
fun main() {
val arr = arrayListOf(10, 20)
arr += 30
println(arr) // [10, 20, 30]
// arr.plusAssign(30)
}
7.1.3 단항 연산자 오버로딩
data class Point(val x: Int, val y: Int)
operator fun Point.unaryMinus(): Point {
return Point(-x, -y)
}
fun main() {
val p = Point(10, 20)
println(-p) // Point(x=-10, y=-20)
// p.unaryMinus()
}
식 함수 이름
+a | unaryPlus |
-a | unaryMinus |
!a | not |
++a, a++ | inc |
—a, a— | dec |
7.2 비교 연산자 오버로딩
7.2.1 동등성 연산자
- 코틀린은 == 연산자 호출을 equals 메소드 호출로 컴파일한다
- 내부에서 인자가 null인지 검사한다.
class Point(val x: Int, val y: Int) {
override fun equals(other: Any?): Boolean {
if (other === this) return true
if (other !is Point) return false
return other.x == x && other.y == y;
}
}
fun main() {
val p1 = Point(10, 20)
val p2 = Point(10, 20)
println(p1 == p2) //true
// p1?.equals(p2) ?: (p2 == null)
}
- equals 함수는 Any에 정의된 메소드이므로 override가 필요하다
- 상위 클래스의 메소드에서 operator가 붙어있어서 안붙이고 override해도된다
public open class Any {
public open operator fun equals(other: Any?): Boolean
}
7.2.2 순서 연산자
- 비교 연산자는 compareTo 호출로 컴파일된다.
class Person(val lastName: String): Comparable<Person> {
override fun compareTo(other: Person): Int {
return compareValuesBy(this, other, Person::lastName)
}
}
fun main() {
val p1 = Person("Alice")
val p2 = Person("Bob")
println(p1 < p2) // true
// p1.compareTo(p2) < 0
}
7.3 컬렉션과 범위에 대해 쓸 수 있는 관례
7.3.1 인덱스로 원소에 접근
- 원소를 읽는 연산은 get 연산자 메소드로 변환된다.
data class Point(val x: Int, val y: Int) {
operator fun get(index: Int): Int {
return when (index) {
0 -> x
1 -> y
else ->
throw IndexOutOfBoundsException("Invalid coordinate $index")
}
}
}
fun main() {
val p = Point(10, 20)
println(p[1]) // 20
// p.get(1)
}
- 원소를 쓰는 연산은 set 연산자 메소드로 변환된다.
data class MutablePoint(var x: Int, var y: Int) {
operator fun set(index: Int, value: Int) {
when (index) {
0 -> x = value
1 -> y = value
else -> throw IndexOutOfBoundsException("invalid")
}
}
}
fun main() {
val p = MutablePoint(10, 20)
p[1] = 42 // == p.set(1, 42)
println(p) // MutablePoint(x=10, y=42)
}
7.3.2 in
- in 연산자와 대응하는 함수는 contains 함수다
data class Point(val x: Int, val y: Int)
data class Rectangle(val upperLeft: Point, val lowerRight: Point) {
operator fun contains(p: Point): Boolean {
return p.x in upperLeft.x until lowerRight.x && p.y in upperLeft.y until lowerRight.y
}
}
fun main() {
val rect = Rectangle(Point(10, 20), Point(50, 50))
println(Point(20, 30) in rect) // true
// == rect.contains(Point(20, 30))
}
7.3.3 rangeTo
- ..연산자는 rangeTo 함수를 간략하게 표현하는 방법이다
fun main() {
val now = LocalDate.now()
val vacation = now..now.plusDays(10) // now.rangeTo(now.plusDays(10))
println(now.plusWeeks(1) in vacation) // true
}
7.3.4 iterator
- for 루프 안에서의 in은 list.iterator()를 호출해서 이터레이터를 얻은 다음, hasNext, next 호출을 반복하는 식으로 변환된다.
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> = object : Iterator<LocalDate> {
var current = start
override fun hasNext() = current <= endInclusive
override fun next() = current.apply {
current = plusDays(1)
}
}
fun main() {
val newYear = LocalDate.ofYearDay(2022, 1)
val daysOff = newYear.minusDays(1)..newYear
for (dayOff in daysOff) {
println(dayOff)
// 2021-12-31
// 2022-01-01
}
}
7.4 구조 분해 선언과 component 함수
구조 분해 선언
- 복합적인 값을 분해해서 여러 다른 변수를 초기화 할 수 있다.
data class Point(val x: Int, val y: Int);
fun main() {
val p = Point(10, 20);
val (x, y) = p;
println(x) // 10
println(y) // 20
}
- 구조 분해 선언에도 관례를 사용할 수 있다.
- 주 생성자에 들어있는 프로퍼티에 대해서는 컴파일러가 자동으로 componentN 함수를 만들어준다
class Point(val x: Int, val y: Int) {
operator fun component1() = x
operator fun component2() = y
};
fun main() {
val p = Point(10, 20);
val (x, y) = p;
// val x = p.component1()
// val y = p.component2()
}
- 코틀린 표준 라이브러리에서는 맨 앞 다섯 원소에 대한 componentN를 제공한다. 이로 인해서 원소 갯수에 따라서 에러 발생 시점이 달라지게 되는데
- 컬렉션 크기를 벗어나는 위치에 원소에 대한 구조 분해 선언을 사용하면 런타임 에러가 발생한다.
- 여섯 개 이상의 변수를 사용하는 구조 분해를 컬렉션에 대해 사용하면 컴파일 에러가 발생한다.
fun main() {
val x = listOf(1, 2);
val (a,b,c) = x; //runtime error(Index 2 out of bounds for length 2)
val (d,e,f,g,h,i) = x; //compile error(Destructuring declaration initializer of type List<Int> must have a 'component6()' function)
}
7.4.1 구조 분해 선언과 루프
- 변수 선언이 들어갈 수 있는 장소라면 어디든 구조 분해 선언을 할 수 있다.
fun printEntries(map: Map<String, String>) {
for((key, value) in map) {
println("$key -> $value")
}
}
fun main() {
val map = mapOf("key1" to "value1", "key2" to "value2")
printEntries(map)
//key1 -> value1
//key2 -> value2
}
- 코틀린 라이브러리는 Map.Entry에 대한 확장 함수로 component1, component2를 제공한다.
fun printEntries(map: Map<String, String>) {
for((key, value) in map) {
println("$key -> $value")
}
//for (entry in map.entries) {
// val key = entry.component1()
// val value = entry.component2()
// println("$key -> $value")
//}
}
public inline operator fun <K, V> Map.Entry<K, V>.component1(): K = key
public inline operator fun <K, V> Map.Entry<K, V>.component2(): V = value
7.5 프로퍼티 접근자 로직 재활용: 위임 프로퍼티
위임(delegate)
- 객체가 직접 작업을 수행하지 않고 다른 도우미 객체가 그 작업을 처리하게 맡기는 디자인 패턴
- 이때 작업을 처리하는 도우미 객체를 위임 객체라고 부른다.
7.5.1 위임 프로퍼티 소개
class Foo {
var p: Type by Delegate() // by 키워드는 프로퍼티와 위임 객체를 연결한다.
// private val delegate = Delegate() (컴파일러가 생성한 숨겨진 도우미 프로퍼티)
// var p: Type
// set(value : Type) = delegate.setValue(..., value: Type)
// get() = delegate.getValue(...)
}
- p 프로퍼티는 접근자 로직을 다른 객체에게 위임한다.
- 컴파일러는 숨겨진 도우미 프로퍼티를 만들고 그 프로퍼티를 위임 객체의 인스턴스로 초기화한다.
- 프로퍼티 위임 관례를 따르는 Delegate 클래스는 getValue와 setValue 메소드를 제공해야한다.
class Delegate {// Delegate 클래스를 단순화한 코드이다
operator fun getValue(...) {...}
operator fun setValue(..., value: Type) {...}
}
class Foo {
var p: Type by Delegate()
}
fun main() {
val foo = Foo()
val oldValue = foo.p // delegate.getValue() 호출
foo.p = newValue // delegate.setValue() 호출
}
7.5.2 by lazy()를 사용한 프로퍼티 초기화 지연
지연 초기화
- 객체의 일부분을 초기화하지 않고 남겨뒀다가 실제로 그 부분의 값이 필요할 경우 초기화할 때 쓰이는 패턴이다.
- 초기화 과정에 자원을 많이 사용하거나, 객체를 사용할 때마다 초기화하지 않아도 되는 프로퍼티에 대해 사용할 수 있다.
위임 프로퍼티 사용하기 전 로직
- thread-safe하지 않다
fun loadEmails(person: Person): List<Email> {
// 대충 이메일 리스트 가져오는 로직
return listOf(...)
}
class Person(val name: String) {
private var _emails: List<Email>? = null
val emails: List<Email>
get() {
if (_emails == null) {
_emails = loadEmails(this)
}
return _emails!!
}
}
fun main() {
val p = Person("alice")
println(p.emails) //emails를 처음 읽을때만 요청하게 된다
println(p.emails)
}
위임 프로퍼티 사용한 후 로직
fun loadEmails(person: Person): List<Email> {
// 대충 이메일 리스트 가져오는 로직
return listOf(...)
}
class Person(val name: String) {
val emails by lazy { loadEmails(this) }
}
fun main() {
val p = Person("alice")
println(p.emails) //emails를 처음 읽을때만 요청하게 된다
println(p.emails)
}
lazy
- 위임 객체를 반환하는 표준 라이브러리 함수
- 코틀린 관례에 맞는 시그니처의 getValue 메소드가 들어있는 객체를 반환한다
- thread-safe 하다
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
7.5.3 위임 프로퍼티 구현
open class PropertyChangeAware {
protected val changeSupport = PropertyChangeSupport(this)
fun addPropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.addPropertyChangeListener(listener)
}
fun removePropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.removePropertyChangeListener(listener)
}
}
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
var age: Int = age
set(newValue) {
val oldValue = field
field = newValue
changeSupport.firePropertyChange("age", oldValue, newValue)
}
var salary: Int = salary
set(newValue) {
val oldValue = field
field = newValue
changeSupport.firePropertyChange("salary", oldValue, newValue)
}
}
fun main() {
val p = Person("alice", 34, 2000)
p.addPropertyChangeListener(PropertyChangeListener { event -> println("Property ${event.propertyName} changed from ${event.oldValue} to ${event.newValue}") })
p.age = 35 // Property age changed from 34 to 35
p.salary = 1000 // Property salary changed from 2000 to 1000
}
- 도우미 클래스를 통해 프로퍼티 변경 통지 구현하기
class **ObservableProperty**(val propName: String, var propValue: Int, val changeSupport: PropertyChangeSupport) {
fun getValue(): Int = propValue
fun setValue(newValue: Int) {
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(propName, oldValue, newValue)
}
}
open class PropertyChangeAware {
protected val changeSupport = PropertyChangeSupport(this)
fun addPropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.addPropertyChangeListener(listener)
}
fun removePropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.removePropertyChangeListener(listener)
}
}
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
val _age = ObservableProperty("age", age, changeSupport)
var age: Int
get() = _age.getValue()
set(value) {
_age.setValue(value)
}
val _salary = ObservableProperty("salary", salary, changeSupport)
var salary: Int
get() = _salary.getValue()
set(value) {
_salary.setValue(value)
}
}
fun main() {
val p = Person("alice", 34, 2000)
p.addPropertyChangeListener { event -> println("Property ${event.propertyName} changed from ${event.oldValue} to ${event.newValue}") }
p.age = 35 // Property age changed from 34 to 35
p.salary = 1000 // Property salary changed from 2000 to 1000
}
- ObservableProperty를 프로퍼티 위임에 사용할 수 있게 바꾸었다.
class ObservableProperty(var propValue: Int, val changeSupport: PropertyChangeSupport) {
operator fun getValue(p: Person, prop: KProperty<*>): Int = propValue
operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}
}
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
var age: Int by ObservableProperty(age, changeSupport)
var salary: Int by ObservableProperty(salary, changeSupport)
}
- 표준 라이브러리에는 이미 ObservableProperty와 비슷한 클래스가 있다. (Delegates.observable)
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
private val observer = {
prop: KProperty<*>, oldValue: Int, newValue: Int ->
changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}
var age: Int by Delegates.observable(age, observer)
var salary: Int by Delegates.observable(salary, observer)
}
open class PropertyChangeAware {
protected val changeSupport = PropertyChangeSupport(this)
fun addPropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.addPropertyChangeListener(listener)
}
fun removePropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.removePropertyChangeListener(listener)
}
}
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
private val observer = {
prop: KProperty<*>, oldValue: Int, newValue: Int ->
changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}
var age: Int by Delegates.observable(age, observer)
var salary: Int by Delegates.observable(salary, observer)
}
fun main() {
val p = Person("alice", 34, 2000)
p.addPropertyChangeListener { event -> println("Property ${event.propertyName} changed from ${event.oldValue} to ${event.newValue}") }
p.age = 35 // Property age changed from 34 to 35
p.salary = 1000 // Property salary changed from 2000 to 1000
}
7.5.4 위임 프로퍼티 컴파일 규칙
- 컴파일러는 MyDelegate 클래스의 인스턴스를 감춰진 <delegate> 프로퍼티에 저장하며 그 감춰진 프로퍼티를 <delegate>라는 이름으로 부른다
- 컴파일러는 프로퍼티를 표현하기 위해 KProperty 타입의 <property> 객체를 사용한다.
- 컴파일러는 모든 프로퍼티 접근자 안에 getValue와 setValue 호출 코드를 생성해준다.
class C {
var prop: Type by MyDelegate()
// private val <delegate> = MyDelegate()
// var prop: Type
// get() = <delegate>.getValue(this, <property>)
// set(value: Type) = <delegate>.setValue(this, <property>, value)
}
val c = C()
val x = c.prop // val x = <delegate>.getValue(c, <property>)
c.prop = x // <delegate>.setValue(c, <property>, x)
7.5.5 프로퍼티 값을 맵에 저장
- 위임 프로퍼티를 사용하지 않은 경우
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttributes(attrName: String, value: String) {
_attributes[attrName] = value
}
val name: String
get() = _attributes["name"]!!
}
fun main() {
val p = Person()
val data = mapOf("name" to "alice", "company" to "kakao")
for ((attName, value) in data)
p.setAttributes(attName, value)
println(p.name) // alice
}
- 위임 프로퍼티를 사용한 경우
- 표준 라이브러리가 Map과 MutableMap 인터페이스에 대해 getValue, setValue 확장 함수를 제공하기 때문에 더 간결하게 사용 가능하다
- 확장 가능한 객체
- 위임 프로퍼티를 활용해서 자신의 프로퍼티를 동적으로 정의할 수 있는 객체
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttributes(attrName: String, value: String) {
_attributes[attrName] = value
}
val name: String by _attributes // 위임 프로퍼티(확장 가능한 객체)
}
fun main() {
val p = Person()
val data = mapOf("name" to "alice", "company" to "kakao")
for ((attrName, value) in data) {
p.setAttributes(attrName, value)
}
println(p.name) // alice
// p.name -> _attributes.getValue(p, props) -> _attributes[prop.name]
}
7.5.6 프레임워크에서 위임 프로퍼티 활용
- 각 엔티티 속성(name, age)은 위임 프로퍼티이고, 컬럼 객체(Users.name, Users.age)를 위임 객체로 사용한다.
object Users: IdTable() {
val name = varchar("name", length = 50).index()
val age = integer("age")
}
class User(id: EntityID): Entity(id) {
var name: String by Users.name
var age: String by Users.age
}
- 프레임워크는 Column 클래스 안에 getValue와 setValue 메소드를 정의한다.
operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>): T {
// 컬럼 값 가져오기
}
operator fun <T> Column<T>.setValue(o: Entity, desc: KProperty<*>, value: T) {
// 컬럼 값 변경하기
}
7.6 요약
- 코틀린에서는 미리 정해진 이름의 함수를 이용해서 산술 연산자를 오버로딩할 수 있다.
- 비교연산자는 equals(등호)와 compareTo(부등호) 메소드로 변환된다
- 클래스에 get, set, contains라는 함수를 정의하면 그 클래스의 인스턴스에 대해 [], in 연산을 사용할 수 있고 컬렉션 객체와 비슷하게 다룰 수 있다.
- 위임 프로퍼티를 통해 프로퍼티 값을 저장, 초기화, 읽기, 변경할때 사용하는 로직을 재활용할 수 있다.
- Delegates.observable 함수를 사용하면 프로퍼티 변경을 관찰할 수 있는 관찰자를 쉽게 추가할 수 있다
- 맵을 위임 객체로 사용하는 위임 프로퍼티를 통해 다양한 속성을 제공하는 객체를 유연하게 다룰 수 있다.
'kotlin' 카테고리의 다른 글
[jackson] 데이터가 null일때 empty list로 바꾸는 옵션 (0) | 2022.09.19 |
---|---|
11. DSL 만들기 (0) | 2022.04.28 |
10. 애노테이션과 리플렉션 (0) | 2022.03.26 |
9. 제네릭스 (0) | 2022.02.28 |
8. 고차 함수: 파라미터와 반환 값으로 람다 사용 (0) | 2022.02.13 |