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)
println(p1 + p2) // + 기호를 쓰면 plus 함수 호출
}
plus 함수 앞에 operator 키워드를 붙여야 한다.
연산자를 확장 함수로 정의할 수도 있다.
표현식
함수 이름
a + b
plus
a - b
minus
a * b
times
a / b
div
a % b
mod
연산자 우선순위는 표준 숫자 타입에 대한 연산자 우선순위와 같다.
operator fun Point.times(scale: Double): Point {
return Point((x * scale).toInt(), (y * scale).toInt())
}
fun main() {
val p = Point(10, 20)
println(p * 1.5)
}
연산자를 정의할 때 두 피연산자가 같은 타입일 필요는 없다.
자동으로 교환법칙을 지원하지 않는다.
operator fun Char.times(count: Int): String {
return toString().repeat(count)
}
fun main() {
println('a' * 3)
}
연산자 함수의 반환 타입이 꼭 두 피연산자 중 하나와 일치해야만 하는 것도 아니다.
🔖 9.1.2 연산을 적용한 다음에 그 결과를 바로 대입: 복합 대입 연산자 오버로딩
fun main() {
var point = Point(1, 2)
point += Point(3, 4)
println(point)
}
plus와 같은 연산자를 오버로딩하면 복합 대입 연산자를 자동으로 지원한다.
operator fun <T> MutableCollection<T>.plusAssign(element: T) {
this.add(element)
}
어떤 클래스가 plus, plusAssign을 모두 정의하고 +=을 사용하면 오류를 보고한다.
코틀린 표준 라이브러리는 컬렉션에 대해 2가지 접근 방법을 제공
+,-는 항상 새로운 컬렉션 반환
+=, -= 연산자는 항상 변경 가능한 컬렉션에 작용해 메모리에 있는 객체 상태를 변화
🔖 9.1.3 피연산자가 1개뿐인 연산자: 단항 연산자 오버로딩
operator fun Point.unaryMinus(): Point {
return Point(-x, -y)
}
단항 연산자를 오버로딩하기 위해 사용하는 함수는 인자를 취하지 않는다.
표현식
함수 이름
+a
unaryPlus
-a
unaryMinus
!a
not
a++, ++a
inc
a-- , --a
dec
fun main() {
var bd = BigDecimal.ZERO
println(bd++) // 후위 증가 연산자는 println이 실행된 다음에 값 증가
println(bd)
println(++bd) // 전위 증가 연산자는 println이 실행되기 전에 값 증가
}
📖 9.2 비교 연산자를 오버로딩해서 객체들 사이의 관계를 쉽게 검사
🔖 9.2.1 동등성 연산자: equals
!= 연산자를 사용하는 식도 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
}
}
동등성 비교 연산자(===)는 자바 == 연산자와 같다.
자신의 두 피연산자가 서로 같은 객체ㅡㄹ 가리키는지 비교한다.
===를 오버로딩할 수는 없다.
🔖 9.2.2 순서 연산자: compareTo (<, >, <=, >=)
class Person(val firstName: String, val lastName: String) : Comparable<Person> {
override fun compareTo(other: Person): Int {
return compareValuesBy(this, other, Person::lastName, Person::firstName)
}
}
fun main() {
val p1 = Person("Alice", "Smith")
val p2 = Person("Bob", "Johnson")
println(p1 < p2)
}
비교 연산자를 제공한다.
compareValuesBy는 두 객체와 여러 비교 함수를 인자로 받는다.
첫 번째 비교 함수에 두 객체를 넘겨 두 객체가 같지 않다는 결과가 나오면 그 결괏값을 즉시 반환
비교 연산자를 자바 클래스에 대해 사용하기 위해 특별히 확장 메서드를 만들거나 할 필요는 없다.
📖 9.3 컬렉션과 범위에 대해 쓸 수 있는 관례
🔖 9.3.1 인덱스로 원소 접근: get과 set
operator fun Point.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])
}
get 메서드의 파라미터로 Int가 아닌 타입도 사용할 수 있다.
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")
}
}
fun main() {
val p = MutablePoint(10, 20)
p[1] = 42
println(p)
}
set도 관례로 표현할 수 있다.
🔖 9.3.2 어떤 객체가 컬렉션에 들어있는지 검사: in 관레
data class Rectangle(val upperLeft: Point, val lowerRight: Point)
operator fun Rectangle.contains(p: Point): Boolean {
return p.x in upperLeft.x..<lowerRight.x &&
p.y in upperLeft.y..<lowerRight.y // 열린 범위
}
fun main() {
val rect = Rectangle(Point(10, 20), Point(50, 50))
println(Point(20, 30) in rect)
println(Point(5, 5) in rect)
}
..< 연산자를 쓰면 열린 범위를 만들 수 있다.
in 함수는 contains 메서드의 수신 객체가 되고 in의 왼쪽에 있는 객체는 contains 메서드에 인자로 전달된다.
🔖 9.3.3 객체로부터 범위 만들기: rangeTo와 rangeUntil 관레
operator fun <T: Comparable<T>> T.rangeTo(other: T): ClosedRange<T>
코틀린 표준 라이브러리에는 모든 Comparable 객체에 대해 적용 가능한 rangeTo 함수가 들어있다.
fun main() {
val now = LocalDate.now()
val vacation = now..now.plusDays(10)
println(now.plusWeeks(1) in vacation)
}
now..now.plusDays(10) 식은 컴파일러에 의해 now.rangeTo(now.plusDays(10))으로 변환된다.
rangeTo 연산자는 다른 산술 연산자보다 우선순위가 낮다.
혼동을 피하기 위해 괄호를 쓰자!
fun main() {
val n = 9
(0..n).forEach { println(it) }
}
범위의 메서드를 호출하려면 범위를 괄호로 둘러싸야 한다.
rangeUntil 연산자(..<)>=는 열린 범위를 만든다.
🔖 9.3.4 자신의 타입에 대해 루프 수행: iterator 관례
operator fun CharSequence.iterator(): CharIterator
이 라이브러리 함수는 문자열을 이터레이션할 수 있게 해준다.
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
object : Iterator<LocalDate> {
var current = start
override fun hasNext(): Boolean =
current <= endInclusive
override fun next(): LocalDate {
val thisDate = current
current = current.plusDays(1)
return thisDate
}
}
fun main() {
val newYear = LocalDate.ofYearDay(2042, 1)
val daysOff = newYear.minusDays(1)..newYear
for (dayOff in daysOff) {
println(dayOff)
}
}
ClosedRange<LocalDate>에 대한 확장 함수 iterator를 정의했기 때문에 LocalDate의 범위 객체를 for 루프에 사용할 수 있다.
📖 9.4 component 함수를 사용해 구조 분해 선언 제공
fun main() {
val p = Point(10, 20)
val (x, y) = p
println(x)
println(y)
}
구조 분해 선언은 일반 변수 선언과 비슷해 보인다.
내부에서 구조 분해 선언의 각 변수를 초기화하고자 componentN이라는 함수를 호출한다.
N은 구조 분해 선언에 있는 변수 위치에 따라 붙는 번호
data class의 주 생성자에 들어있는 프로퍼티에 대해서는 컴파일러가 자동으로 componentN 함수를 만들어준다.
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])
}
fun main() {
val (name, ext) = splitFileName("example.kt")
println(name)
println(ext)
}
구조 분해 선언을 사용해 여러 값 반환이 가능하다.
data class NameComponents(val name: String, val extension: String)
fun splitFileName(fullName: String): NameComponents {
val (name, extension) = fullName.split('.', limit = 2)
return NameComponents(name, extension)
}
컬렉션에 대해 구조 분해 선언 사용이 가능하다.
🔖 9.4.1 구조 분해 선언과 루프
fun printEntries(map: Map<String, String>) {
for ((key, value) in map) {
println("$key -> $value")
}
}
fun main() {
val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
printEntries(map)
}
data class Person(val firstName: String, val lastName: String, val age: Int, val city: String)
fun introducePerson(p: Person) {
val (firstName, lastName, age, city) = p
println("This is $firstName, aged $age.")
}
전체 객체를 구조 분해해야만 하는 것은 아니기 때문에 구조 분해 선언에서 뒤쪽의 구조 분해 선언을 제거할 수는 잇다.
val (firstName, lastName, age) = p
lastName을 그냥 없앨 수는 없다.
fun introducePerson(p: Person) {
val (firstName, _, age) = p
println("This is $firstName, aged $age.")
}
_ 를 쓰면 컴포넌트를 무시하고 구조 분해 선언이 가능하다.
📖 9.5 프로퍼티 접근자 로직 재활용: 위임 프로퍼티
🔖 9.5.1 위임 프로퍼티의 기본 문법과 내부 동작
class Foo {
var p = Type by Delegate()
}
p 프로퍼티는 접근자 로직을 다른 객체에 위임한다.
class Foo {
private val delegate = Delegate()
var p: Type
set(value: Type) = delegate.setValue(/**/, value)
get() = delegate.getValue(/**/)
}
프로퍼티 위임 관례에 따라 Delegate 클래스는 get, set을 제공해야한다.
by 키워드는 프로퍼티와 위임객체를 연결한다.
🔖 9.5.2 위임 프로퍼티 사용: by lazy()를 사용한 지연 초기화
지연 초기화는 객체의 일부분을 초기화하지 않고 남겨뒀다가 실제로 그 부분의 값이 필요할 경우 초기화할 때 쓰이는 패턴
class Person(val name: String) {
private var _emails: List<String>? = null
val emails: List<String>
get() {
if (_emails == null) {
_emails = loadEamils(this)
}
return _emails!!
}
}
fun main() {
val p = Person("Alice")
p.emails
}
뒷받침하는 프로퍼티 기법 사용
클래스 같은 개념을 표헌하는 프로퍼티가 2개 있을 때 비공개 프로퍼티 앞에 밑줄을 붙이며, 공개 프로퍼티에는 아무것도 붙이지 않는다.
class Person(val name: String) {
val emails by lazy { loadEmails(this) }
}
위임 프로퍼티를 사용하면 훨씬 간단해진다.
lazy 함수는 기본적으로 스레드 안전하다.
🔖 9.5.3 위임 프로퍼티 구현
fun interface Observer {
fun onChange(name: String, oldValue: Any?, newValue: Any?)
}
open class Observable {
val observers = mutableListOf<Observer>()
fun notifyObservers(propName: String, oldValue: Any?, newValue: Any?) {
for (obs in observers) {
obs.onChange(propName, oldValue, newValue)
}
}
}
class Person(val name: String, age: Int, salary: Int) : Observable() {
var age: Int = age
set(newValue) {
val oldValue = field
field = newValue
notifyObservers("age", oldValue, newValue)
}
var salary: Int = salary
set(newValue) {
val oldValue = field
field = newValue
notifyObservers("salary", oldValue, newValue)
}
}
field 키워드를 사용해 age와 salary 프로퍼티를 뒷받침하는 필드에 접근하는 방법을 보여준다.
class ObservableProperty(val propName: String, var propValue: Int, val observable: Observable) {
fun getValue(): Int = propValue
fun setValue(newValue: Int) {
val oldValue = propValue
propValue = newValue
observable.notifyObservers(propName, oldValue, newValue)
}
}
class Person(val name: String, age: Int, salary: Int) : Observable() {
val _age = ObservableProperty("age", age, this)
var age: Int
get() = _age.getValue()
set(value) {
_age.setValue(value)
}
val _salary = ObservableProperty("salary", salary, this)
var salary: Int
get() = _salary.getValue()
set(value) {
_salary.setValue(value)
}
}
프로퍼티 값을 저장하고 그 값이 바뀌면 자동으로 변경 통지를 전달해주는 클래스를 만들었다.
위임 프로퍼티를 사용하면 더 간단해진다.
class ObservableProperty(var propValue: Int, val observable: Observable) {
operator fun getValue(thisRef: Any?, prop: KProperty<*>): Int = propValue
operator fun setValue(thisRef: Any?, prop: KProperty<*>, newValue: Int) {
val oldValue = propValue
propValue = newValue
observable.notifyObservers(prop.name, oldValue, newValue)
}
}
operator 변경자가 붙는다.
KProperty 타입의 객체를 사용해 프로퍼티 표현
class Person(val name: String, age: Int, salary: Int) : Observable() {
val age by ObservableProperty(age, this)
var salary by ObservableProperty(salary, this)
}
여러 작업을 컴파일러가 자동으로 처리해준다.
by의 오른쪽에 있는 식이 꼭 새 인스턴스를 만들 필요는 없다.
🔖 9.5.4 위임 프로퍼티는 커스텀 접근자가 있는 감춰진 프로퍼티로 변환된다
class C {
private val <delegate> = MyDelegate()
var prop: Type
get() = <delegate>.getValue(this, <property>)
set(value: Type) = <delegate>.setValue(this, <property>, value)
}
컴파일러는 모든 프로퍼티 접근자 안에 getValue, setValue 호출 코드를 생성해준다.
프로퍼티 값이 저장될 장소를 바꿀 수도 있고 프로퍼티를 읽거나 쓸 때 벌어질 일을 변경할 수도 있다.
🔖 9.5.5 맵에 위임해서 동적으로 애트리뷰트 접근
class Person {
private val _attributes = mutableMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
var name: String
get() = _attributes["name"]!!
set(value) {
_attributes["name"] = value
}
}
위임 프로퍼티를 활용하도록 변경하는 것은 아주 쉽다.
class Person {
private val _attributes = mutableMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
var name: String by _attributes
}
Map에 대해 getValue, setValue 확장 함수를 제공하기에 동작함.
getValue에서 맵에 프로퍼티 값을 저장할 때는 자동으로 프로퍼티 이름을 키로 활용
🔖 9.5.6 실전 프레임워크가 위임 프로퍼티를 활용하는 방법
위임 프로퍼티는 프로퍼티의 접근 로직을 재사용 가능하게 만들어, 코드의 중복을 줄이고 유지보수성을 향상시킨다.
프레임워크나 라이브러리에서 공통된 프로퍼티 동작을 캡슐화하여, 사용자 코드의 간결함과 일관성을 유지할 수 있다.