- 연산자 오버로딩
- 관례: 여러 연산을 지원하기 위해 특별한 이름이 붙은 메소드
- 위임 프로퍼티
// plus 연산자 구현하기
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}
val p1 = Point(10, 20)
val p2 = Point(30, 40)
println(p1 + p2) // Point(x=40, y=60)
// 연산자를 확장 함수로 정의하기
operator fun Point.plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
외부 함수의 클래스에 대한 연산자를 정의할 때는 관례를 따르는 이름의 확장 함수로 구현하는 게 일반적인 패턴이다.
식 | 함수 이름 |
---|---|
a * b | times |
a / b | div |
a % b | mod(1.1부터 rem) |
a + b | plus |
a - b | minus |
// 두 피연산자의 타입이 다른 연산자 정의하기
operator fun Point.times(scale: Double): Point {
return Point((x * scale).toInt(), (y * scale).toInt())
}
val p = Point(10, 20)
println(p * 1.5) // Point(x=15, y=30)
코틀린 연산자가 자동으로 교환 법칙(a op b == b op a인 설정)을 지원하지는 않음에 유의하라.
operator fun Double.times(point: Point): Point {
return Point((this * point.x).toInt(), (this * point.y).toInt())
}
fun main() {
val p = Point(10, 20)
println(1.5 * p) // Point(x=15, y=30)
}
// 결과 타입이 피연산자 타입과 다른 연산자 정의하기
operator fun Char.times(count: Int): String {
return toString().repeat(count)
}
println('a' * 3) // aaa
비트 연산자에 대해 특별한 연산자 함수를 사용하지 않는다.
코틀린은 표준 숫자 타입에 대해 비트 연산자를 정의하지 않는다. 따라서 커스텀 타입에서 비트 연산자를 정의할 수 없다. 대신에 중의 연산자 표기법을 지원하는 일반 함수(and, shl, xor 등)를 사용해 비트 연산을 수행한다. 커스텀 타입에서도 그와 비슷한 함수를 정의해 사용할 수 있다.
var point = Point(1, 2)
point += Point(3, 4)
println(point) // Point(x=4, y=6)
val numbers = ArrayList<Int>()
numbers += 42
println(numbers[0]) // 42
operator fun <T> MutableCollection<T>.plusAssign(element: T) {
this.add(element)
}
plus와 plusAssign 연산을 동시에 정의하지 말라.
val list = arrayListOf(1, 2)
// +=는 "list"를 변경한다.
list += 3
// +는 두 리스트의 모든 원소를 포함하는 새로운 리스트를 반환한다.
val newList = list + listOf(4, 5)
println(list) // [1, 2, 3]
println(newList) // [1, 2, 3, 4, 5]
// 단항 minus(음수) 함수는 파라미터가 없다.
operator fun Point.unaryMinus(): Point {
// 좌표에서 각 성분의 음수를 취한 새 점을 반환한다.
return Point(-x, -y)
}
val p = Point(10, 20)
println(-p) // Point(x=-10, y=-20)
식 | 함수 이름 |
---|---|
+a | unaryPlus |
-a | unaryMinus |
!a | not |
++a, a++ | inc |
--a, a-- | dec |
operator fun BigDecimal.inc() = this + BigDecimal.ONE
var bd = BigDecimal.ZERO
// 후위 증가 연산자는 println이 실행된 다음에 값을 증가시킨다.
println(bd++) // 0
// 전위 증가 연산자는 println이 실행되기 전에 값을 증가시킨다.
println(++bd) // 2
a == b
➡️ a?.equals(b) ?: (b == null)
class Point(val x: Int, val y: Int) {
// Any에 정의된 메소드를 오버라이딩한다.
override fun equals(obj: Any?): Boolean {
// 최적화: 파라미터가 "this"와 같은 객체인지 살펴본다.
if (obj === this) return true
// 파라미터 타입을 검사한다.
if (obj !is Point) return false
// Point로 스마트 캐스트해서 x와 y 프로퍼티에 접근한다.
return obj.x == x && obj.y == y
}
}
println(Point(10, 20) == Point(10, 20)) // true
println(Point(10, 20) != Point(5, 5)) // true
println(null == Point(1, 2)) // false
a >= b
➡️ a.compareTo(b) >= 0
class Person(
val firstName: String, val lastName: String
): Comparable<Person> {
override fun compareTo(other: Person): Int {
// 인자로 받은 함수를 차례로 호출하면서 값을 비교한다.
return compareValuesBy(this, other,
Person::lastName, Person::firstName)
}
}
val p1 = Person("Alice", "Smith")
val p2 = Person("Bob", "Johnson")
println(p1 < p2) // false
그렇지만 필드를 직접 비교하면 코드는 조금 더 복잡해지지만 비교 속도는 훨씬 더 빨라진다. 언제나 그렇듯이 처음에는 성능에 신경 쓰지 말고 이해하기 쉽고 간결하게 코드를 작성하고, 나중에 그 코드가 자주 호출됨에 따라 성능이 문제가 되면 성능을 개선하라
class Person(
val firstName: String, val lastName: String
): Comparable<Person> {
override fun compareTo(other: Person): Int {
// lastName 비교
val lastNameComparison = this.lastName.compareTo(other.lastName)
if (lastNameComparison != 0) {
return lastNameComparison
}
// lastName이 같으면 firstName 비교
return this.firstName.compareTo(other.firstName)
}
}
fun main() {
val p1 = Person("Alice", "Smith")
val p2 = Person("Bob", "Johnson")
println(p1 < p2) // false
}
// get 관례 구현하기
operator fun Point.get(index: Int): Int {
return when(index) {
0 -> x
1 -> y
else ->
throw IndexOutOfBoundsException("Invalid coordinate $index")
}
}
val p = Point(10, 20)
println(p[1]) // 20
data class MutablePoint(var x: Int, var y: Int)
operator fun MutablePoint.set(index: Int, value: Int) {
when(index) {
0 -> x = value
1 -> y = value
else ->
throw IndexOutOfBoundsException("Invalid coordinate $index")
}
}
val p = MutablePoint(10, 20)
p[1] = 42
println(p) // MutablePoint(x=10, y=42)
data class Rectangle(val upperLeft: Point, val lowerRight: Point)
operator fun Rectangle.contains(p: Point): Boolean {
// 범위를 만들고 좌표가 그 범위 안에 있는지 검사한다.
return p.x in upperLeft.x unitl lowerRight.x &&
p.y in upperLeft.y until lowerRight.y
}
val rect = Rectangle(Point(10, 20), Point(50, 50))
println(Point(20, 30) in rect) // true
println(Point(5, 5) in rect) // false
operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>
// 날짜의 범위 다루기
val now = LocalDate.now()
// val vacation = now.rangeTo(now.plusDays(10))
val vacation = now..now.plusDays(10)
println(now.plusWeeks(1) in vacation) // true
val n = 9
println(0..(n + 1)) // 0..10
0..n.forEach {}
와 같은 식은 컴파일할 수 없음에 유의하라. 범위 연산자는 우선 순위가 낮아서 범위의 메소드를 호출하려면 범위를 괄호로 둘러싸야 한다.(0..n).forEach { println(it) } // 0123456789
for (x in list) { ... }
와 같은 문장은 list.iterator()를 호출해서 이터레이터를 얻은 다음, 자바와 마찬가지로 그 이터레이터에 대해 hasNext와 next 호출을 반복하는 식으로 변환된다.fun main() {
val list = listOf(1, 2, 3, 4, 5)
val iterator = list.iterator()
while (iterator.hasNext()) {
val x = iterator.next()
println(x)
}
}
// CharaIterator 라이브러리 : 문자열을 이터레이션할 수 있게 해준다.
operator fun CharSequence.iterator(): CharaIterator
for (c in "abc") {}
// 날짜 범위에 대한 이터레이터 구현하기
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
// 이 객체는 LocalDate 원소에 대한 Iterator를 구현한다.
object : Iterator<LocalDate> {
var current = start
// compareTo 관례를 사용해 날짜를 비교한다.
override fun hasNext() = current <= endInclusive
// 현재 날짜를 저장한 다음에 날짜를 변경한다. 그 후 저장해둔 날짜를 반환한다.
override fun next() = current.apply {
current = plusDays(1) // 현재 날짜를 1일 뒤로 변경한다.
}
}
val newYear = LocalDate.ofYearDay(2017, 1)
val daysOff = newYear.minusDays(1)..newYear
// daysOff에 대응하는 iterator 함수가 있으면 daysOff에 대해 이터레이션한다.
for (dayOff in daysOff) { println(dayOff) }
// 2016-12-31
// 2017-01-01
val p = Point(10, 20)
// x와 y 변수를 선언한 다음에 p의 여러 컴포넌트로 초기화한다.
val (x, y) = p
println(x) // 10
println(y) // 20
=
의 좌변에 여러 변수를 괄호로 묶었다는 점이 다르다.class Point(val x: Int, val y: Int) {
operator fun component1() = x
operator fun component2() = y
}
// 값을 저장하기 위한 데이터 클래스 선언한다.
data class NameComponents(val name: String, val extension: String)
fun splitFilename(fullName: String): NameComponents {
// val result = fullName.split('.', limit = 2)
// return NameComponents(result[0], result[1])
// 컬렉션에 대해 구조 분해 선언 사용하기
val (name, extension) = fullName.split('.', limit=2)
return NameComponents(name, extension)
}
// 구조 분해 선언을 사용해 여러 값 반환하기
val (name, next) = splitFilename("example.kt")
println(name) // example
println(ext) // kt
// 구조 분해 선언을 사용해 맵 이터레이션하기
fun printEntries(map: Map<String, String>) {
// 루프 변수에 구조 분해 선언을 사용한다.
for ((key, value) in map) {
println("$key -> $value")
}
}
val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
printEntries(map)
// Oracle -> Java
// JetBrains -> Kotlin
for (entry in map.entries) {
val key = entry.component1()
val value = entry.component2()
// ...
}
class Foo {
var p: Type by Delegate()
}
class Foo {
private val delegate: Delegate() // 컴파일러가 생성한 도우미 프로퍼티다.
// "p" 프로퍼티를 위해 컴파일러가 생성한 접근자는
// "delegate"의 getValue와 setValue 메소드를 호출한다.
var p: Type
set(value: Type) = delegate.setValue(..., value)
get() = delegate.getValue(...)
}
class Delegate {
// getValue는 게터를 구현하는 로직을 담는다.
operator fun getValue(...) { ... }
// setValue 메소드는 세터를 구현하는 로직을 담는다.
operator fun setValue(..., value: Type) { ... }
}
class Foo {
// "by" 키워드는 프로퍼티와 위임 객체를 연결한다.
var p: Type by Delegate()
}
val foo = Foo()
val oldValue = foo.p
foo.p = newValue
class Email { /*...*/ }
fun loadEmails(person: Person): List<Email> {
println("${person.name}의 이메일을 가져옴")
return listOf(/*...*/)
}
class Person(val name: String) {
// 데이터를 저장하고 emails의 위임 객체 역할을 하는 emails 프로퍼티
private var _emails: List<Email>? = null
val emails: List<Email>
get() {
if (_emails == null) {
// 최초 접근 시 이메일을 가져온다.
_emails = loadEmails(this)
}
// 저장해둔 데이터가 있으면 그 데이터를 반환한다.
return _emails!!
}
}
val p = Person("Alice")
p.emails
// 최초로 emails를 읽을 때 단 한번만 이메일을 가져온다.
// 결과: Load emails for Alice
p.emails
// 지연 초기화를 위임 프로퍼티를 통해 구현하기
class Person(val name: String) {
val emails by lazy { loadEmails(this) }
}
import kotlin.concurrent.thread
class ExpensiveObject {
init {
println("ExpensiveObject initialized")
}
}
class Example {
// 동기화에 사용할 락을 lazy 함수에 전달
val lazyProperty: ExpensiveObject by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
println("Initializing Lazy Property")
ExpensiveObject()
}
// 다중 스레드 환경에서 사용하지 않을 프로퍼티를 위해 lazy 함수가 동기화를 하지 못하게 막기
// val lazyProperty: ExpensiveObject by lazy(LazyThreadSafetyMode.NONE) {
// ...
// }
}
fun main() {
val example = Example()
// 여러 스레드에서 동시에 접근하는 상황 시뮬레이션
repeat(5) {
thread {
println("Accessing Lazy Property: ${example.lazyProperty}")
}
}
// 잠시 대기하여 모든 스레드가 실행을 마치도록 함
Thread.sleep(1000)
}
✅ 자바 빈 클래스 : 일정한 규약을 따르는 클래스로, 기본 생성자, getter 및 setter 메서드를 포함하며, 멤버 변수는 private으로 선언되고 이에 접근하기 위한 표준 메서드를 제공한다. 이러한 클래스는 재사용 가능한 컴포넌트를 만드는데 사용된다.
// PropertyChangeSupport를 사용하기 위한 도우미 클래스
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)
}
}
val p = Person("Dmitry", 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 = 2100 // Property salary changed from 2000 to 2100
// 도우미 클래스를 통해 프로퍼티 변경 통지 구현하기
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)
}
}
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)
val salary: Int
get() = _salary.getValue()
set(value) { _salary.setValue(value) }
}
// 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)
}
}
getValue | setValue |
---|---|
thisRef : 위임을 사용하는 클래스와 같은 타입이거나 Any 타입이어야 한다. | thisRef : 위임을 사용하는 클래스와 같은 타입이거나 Any 타입이어야 한다. |
property : Property<*>거나 Any 타입이어야 한다. | property : Property<*>거나 Any 타입이어야 한다. |
newValue : 위임을 사용하는 프로퍼티와 같은 타입이거나 Any 타입이어야 한다. |
✅ KProperty : 코틀린의 리플렉션 API 중 하나로, 프로퍼티를 나타내는 역할을 한다. KProperty의 인스턴스는
::
연산자로 얻을 수 있습니다.✅ 리플렉션(Reflection) : 실행 중인 프로그램의 클래스, 메소드, 필드 등과 같은 구조를 동적으로 살펴보거나 수정할 수 있는 능력
// 위임 프로퍼티를 통해 프로퍼티 변경 통지 받기
class Person(
val name: String, age: Int, salary: Int
) : PropertyChangeAware() {
var age: Int by ObservableProperty(age, changeSupport)
var salary: Int by ObservableProperty(salary, changeSupport)
}
// Delegates.observable을 사용해 프로퍼티 변경 통지 구현하기
class Person(
val name: String, age: Int, salary: Int
) : PropertyChangeAware() {
private val observer = {
prop: KProperty<*>, oldValue: Int, newValue: Int ->
changeSupport.firePropertyChange(prop.namme, oldValue, newValue)
}
var age: Int by Delegates.observable(age, observer)
var salary: Int by Delegates.observable(salary, observer)
}
class C {
var prop: Type by MyDelegate()
}
val c = C()
<delegate>
라는 이름으로 부른다.<property>
라고 부른다.class C {
private val <delegate> = MyDelegate()
var prop: Type
get() = <delegate>.getValue(this, <property>)
set(value: Type) = <delegate>.setValue(this, <property>, value)
}
확장 가능한 객체
라고 부르기도 한다.// 값을 맵에 저장하는 프로퍼티 정의하기
class Person {
// 추가 정보
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
// 필수 정보
val name: String
// 수동으로 맵에서 정보를 꺼낸다.
get() = _attributes["name"]!!
}
val p = Person()
val data = mapOf("name" to "Dmitry", "company" to "JetBrains")
for ((attrName, value) in data)
p.setAttribute(attrName, value)
println(p.name) // Dmitry
// 값을 맵에 저장하는 위임 프로퍼티 사용하기
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
// 위임 프로퍼티로 맵을 사용한다.
val name: String by _attributes
}
p.name
은 _attributes.getValue(p, prop)
라는 호출을 대신하고, _attributes.getValue(p, prop)
는 다시 _attributes[prop.name]
을 통해 구현된다.// 위임 프로퍼티를 사용해 데이터베이스 칼럼 접근하기
object Users: IdTable() { // 객체는 데이터베이스 테이블에 해당한다.
val name = varchar("name", length = 50).index()
val age = integer("age")
}
// 각 User 인스턴스는 테이블에 들어있는 구체적인 엔티티에 해당한다.
class User(id: EntityID) : Entity(id) {
// 사용자 이름은 데이터베이스 "name" 칼럼에 들어 있다.
var name: String by Users.name
var age: Int by Users.age
}
operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>): T {
// 데이터베이스에서 칼럼 값 가져오기
}
operator fun <T> Column<T>.setValue(o: Entity, desc: KProperty<*>, value: T) {
// 데이터베이스의 값 변경하기
}
user.age += 1
이라는 식을 코드에서 사용하면 그 식은 user.ageDelegate.setValue(user.ageDelegate.getValue() + 1)
과 비슷한 코드로 변환한다(객체 인스턴스와 프로퍼티 파라미터는 생략함)