- 연산자 오버로딩
- 관례: 여러 연산을 지원하기 위해 특별한 이름이 붙은 메소드
- 위임 프로퍼티
관례
어떤 언어 기능과 미리 정해진 이름의 함수를 연결해주는 기법
기존 자바 클래스가 구현하는 인터페이스는 이미 고정돼 있고 코틀린 쪽에서 자바 클래스가 새로운 인터페이스를 만들 수는 없다
확장 함수를 사용하면 기존 클래스에 새로운 메소드를 추가할 수 있다
-> 기존 자바 클래스에 대해 확장 함수를 구현하면서 관례에 따라 이름을 붙이면 기존 자바 코드를 바꾸지 않아도 새로운 기능을 쉽게 부여할 수 있다
/* plus 연산자 구현하기 */
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point { //"plus" 라는 연산자 함수를 정의합니다
return Point(x + other.x, y + other.y)
}
}
>>> val point1 = Point(10, 20)
>>> val point2 = Point(30, 40)
>>> println(point1 + point2) //"+"로 계산하면 "plus"함수가 호출됩니다
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 |
직접 정의한 함수를 통해 구현하더라도 연산자 우선순위는 언제나 표준 숫자 타입에 대한 연산자 우선순위와 같다
ex) a + b c -> 는 + 보다 먼저 수행된다
연산자를 정의할 때 두 피연산자가 같은 타입일 필요는 없다
// 두 피연산자의 타입이 다른 연산자 정의하기
operator fun Point.times(scale: Double) : Point {
return Point( (x * scale).toInt(), (y * scale).toInt() )
}
>>> val point = Point(10, 20)
>>> println(point * 1.5)
Point(x=15, y=30)
// 1.5 * p , p * 1.5 둘 다 지원하려면
operator fun Double.times(p: Point): Point를 정의해야 한다
// 결과 타입이 피연산자 타입과 다른 연산자 정의하기
operator fun Char.times(count: Int) : String {
return this.toString().repeat(count)
}
>>> println('A' * 3)
AAA
비트 연산자에 대해 특별한 연산자 함수를 사용하지 않는다
코틀린은 표준 숫자 타입에 대해 비트 연산자를 정의하지 않는다
커스텀 타입에서 비트 연산자를 정의할 수 없다
>>> var point = Point(1, 2)
>>> point += Point(3, 4) // point = point + Point(3, 4)와 동일
>>> println(point)
Point(x=4, y=6)
// 객체에 대한 참조를 다른 참조로 바꾸기보다
// 원래 객체의 내부 상태를 변경하게 만들 때도 사용 가능
>>> val numbers = ArrayList<Int>()
>>> numbers += 42
>>> println(numbers[0])
42
plus와 plusAssign 연산을 동시에 정의하지 말자
읽기 전용 컬렉션에서 += 와 -=는 변경을 적용한 복사본을 반환한다
// 단항 연산자 정의하기
operator fun Point.unaryMinus() : Point { //단항연산자는 파라미터가 없습니다.
return Point(-x, -y) //각 좌표에 -(음수)를 취한 좌표를 반환
}
>>> val point = Point(10, 20)
>>> println(-point)
Point(x = -10, y = -20)
식 | 함수 이름 |
---|---|
+a | unaryPlus |
-a | unaryMinus |
!a | not |
++a, a++ | inc |
--a, a-- | dec |
== 연산자/ != 연산자 호출은 equals 메소드 호출로 컴파일한다
== / != 는 내부에서 인자가 널인지 검사하므로 다른 연산과 달리 널이 될 수 있는 값에도 적용할 수 있다
식별자 비교
=== 연산자를 사용해 같은 객체를 가리키는지 비교
equals 함수에는 override가 붙어있다
-> 다른 연산자 오버로딩 관례와 달리 equals는 Any에 정의된 메소드이므로 override가 필요하다
-> Any의 equals에는 operator가 붙어있기 때문에 하위 클래스 메소드 앞에는 붙이지 않아도 된다
Any에서 상속받은 equals가 확장 함수보다 우선순위가 높기 때문에 equals를 확장 함수로 정의할 수 없다
인덱스 연산자를 사용해
원소를 읽는 연산은get 연산자 메소드로 변환
원소를 쓰는 연산은 set 연산자 메소드로 변환
get 메소드의 파라미터로 Int가 아닌 타입도 사용할 수 있다
-> map 같은 경우, matrix[row, col]
ex) 1부터 10 -> 1..10 / 1부터 10전까지 -> 1 until 10
ex) for( i in list )
val p = Point(10, 20)
val (x, y) = p // x와 y를 한 번에 초기화한다
// x = 10, 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])
}
fun main() {
// 구조 분해 선언 구문을 사용해 데이터 클래스를 푼다
val (name, ext) = splitFileName("example.kt")
println("name : $name, ext : $ext")
}
name : example, ext : kt
-----------------------------------------------
// 컬렉션에 대해 구조 분해 선언 사용하기
data class NameComponents(val name: String, val extension: String)
fun splitFileName(fullName: String): NameComponents {
val (name, extention) = fullName.split('.', limit = 2)
return NameComponents(name, extention)
}
// 구조 분해 선언을 사용해 맵 이터레이션 하기
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)
}
Oracle -> Java
JetBrains -> Kotlin
-----------------------------------------------
// Map.Entry에 대한 확장 함수로 component1과 component2를 제공
for (entry in map.entries) {
val key = entry.component1()
val value = entry.component2()
}
위임 프로퍼티 를 사용하면 값을 뒷받침하는 필드에 단순히 저장하는 것보다 더 복잡한 방식으로 작동하는 프로퍼티를 쉽게 구현할 수 있다
-> 접근자 로직을 매번 구현할 필요도 없다
프로퍼티 위임을 사용해 자신의 값을 필드가 아니라 데이터베이스 테이블, 브라우저 세션, 맵 등에 저장할 수 있다
-> 이런 기반에는 위임이 있다
위임
객체가 직접 작업을 수행하지 않고 다른 도우미 객체가 그 작업을 처리하게 맡기는 디자인 패턴을 말한다
-> 작업을 처리하는 도우미 객체를 위임 객체(delegate)라고 부른다
class Foo {
var p: Type by Delegate()
}
p 프로퍼티는 접근자 로직을 닫른 객체에게 위임한다
-> Delegate 클래스의 인스턴스를 위임 객체로 사용한다
by 뒤에 있는 식을 계산해서 위임에 쓰일 객체를 얻는다
프로퍼티 위임 객체가 따라야 하는 관례를 따르는 모든 객체를 위임에 사용할 수 있다
// 위임 관례를 따르는 클래스는 getValue, setValue 메소드를 제공해야함
class Delegate {
// getValue는 게터를 구현하는 로직을 담음
operator fun getValue(...) { ... }
// setValue는 세터를 구현하는 로직을 담음
operator fun setValue(...) { ... }
}
class Foo {
// by 키워드는 프로퍼티와 위임 객체를 연결
var p: Type by Delegate()
}
>>> val foo = Foo()
>>> val oldValue = foo.p
>>> foo.p = newValue
지연 초기화
객체의 일부분을 초기화하지 않고 남겨뒀다가 실제로 그 부분의 값이 필요할 경우 초기화할 때 쓰이는 패턴
초기화 과정에 자원을 많이 사용하거나
객체를 사용할 때마다 꼭 초기화하지 않아도 되는 프로퍼티에 대해 지연 초기화 패턴을 사용할 수 있다
위임 프로퍼티는 데이터를 저장할 때 쓰이는 뒷받침하는 프로퍼티와 값이 오직 한 번만 초기화됨을 보장하는 게터 로직을 함께 캡슐화해준다
// 이메일 클래스
class Email(val content: String)
// 이메일을 가져오는 메소드
fun loadEmails(person: Person): List<Email> {
println("${person.name}의 이메일 목록")
return listOf(/*...*/)
}
class Person(val name: String) {
val emails by lazy { loadEmails(this) }
}
lazy함수는 getValue 메서드가 들어있는 객체를 반환한다
-> lazy를 by 키워드가 함께 사용해 위임 프로퍼티를 만들 수 있다
lazy 함수의 인자는 값을 초기화할 때 호출할 람다
lazy 함수는 기본적으로 thread-safe하지만, 필요에 따라 동기화에 사용할 lock을 lazy 함수에 전달할 수도 있고 다중 스레드 환경에서 사용하지 않을 프로퍼티를 위해 lazy 함수가 동기화를 하지 못하게 막을 수도 있다
위임 프로퍼티 예시
-> 어떤 객체의 프로퍼티가 바뀔 때마다 리스너에게 변경 통지를 보내고 싶을 때
자바에서는 PropertyChangeSupport 와 PropertyChangeEvent 클래스를 사용해 이런 통지를 처리하는 경우가 존재
->
PropertyChangeSupport 클래스는 리스너의 목록을 관리
PropertyChangeEvent 이벤트가 들어오면 목록의 모든 리스너에게 이벤트를 통지
1) 위임 프로퍼티 없이 프로퍼티 변경 통지를 직접 구현
class Person (val name: String, age: Int, salary: Int): PropertyAware() {
var age: Int = age
set(newValue) {
val oldValue = field
field = newValue
changeSupport.firePropertyChange(
"age", oldValue, newValue)
}
var salary: Int = salary
set(newValue) {
... // 위와 동일
}
}
val p = Person("Kim",29,150)
p.addPropertyChangeListener( ... ) <- 프로퍼티 변경 리스너를 추가한다
이 코드는 field 키워드를 사용해 age 와 salary 프로퍼티를
뒷받침하는 필드에 접근하는 방법을 보여준다
중복값이 많이 보인다
------------------------------------------------------------
2) 도우미 클래스를 통해 프로퍼티 변경 통지 구현하기
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) }
var _salary = ObservableProperty("salary",salary, changeSupport)
...
}
로직의 중복을 상당 부분을 제거했다
하지만 아직도 각각의 프로퍼티마다 ObservableProperty를 만들고 게터/세터에서 ObservableProperty에 작업을 위임하는 부분이 많다
------------------------------------------------------------
3) 위임 프로퍼티를 통해 프로퍼티 변경 통지
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)
}
getValue/setValue 에도 operator 변경자가 붙는다
by 키워드를 사용해 위임 객체를 지정하면 직접 짜야 했던 코딩들을 컴파일러가 자동으로 처리해준다
by 오른쪽에 오는 객체를 위임 객체라고 부른다
위임 객체를 감춰진 프로퍼티에 저장하고, 주 객체이 프로퍼티를 읽거나 쓸 때마다 위임 객체의 getValue/setValue를 호출해준다
// Delegates.observable을 사용해 프로퍼티 변경 통지 구현하기
class ObservableProperty(
var propValue: Int, val changeSupport: PropertyChangeSupport
) {
private val observer = {
prop: KProperty<*>, oldValue: Int, newValue ->
changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}
var age: Int by Delegates.observable(age, observer)
var salary: Int by Delegates.observable(salary, observer)
// 값을 맵에 저장하는 위임 프로퍼티 사용하기
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
// 위임 프로퍼티를 맵에 사용
val name: String by _attributes
}