• 관례(convention): 어떤 언어 기능과 미리 정해진 이름의 함수를 연결해주는 기법
    • 예: 어떤 클래스 안에 plus라는 이름의 메소드를 정의하면 그 클래스의 인스턴스에 대해 + 연산자를 사용할 수 있다.
    • 이유: 기존 자바 클래스를 코틀린 언어에 적용하기 위함

산술 연산자 오버로딩

이항 산술 연산 오버로딩

  • 연산자를 오버로딩하는 함수 앞에는 꼭 operator 키워드가 있어야 한다.
data class Point(val x: Int, val y: Int) {  
  
   operator fun plus(other: Point): Point {  
      return Point(x + other.x, y + other.y)  
   }  
}
  • 프로젝트 안에서 직접 작성한 클래스에 대해 관례를 따르는 확장 함수를 만들어도 역시 잘 작동한다.
  • 오버로딩 가능한 이항 산술 연산자
함수 이름
a * btimes
a / bdiv
a % bmod(1.1부터 rem)
a + bplus
a - bminus
  • 직접 정의한 함수를 통해 구현하더라도 연산자 우선순위는 언제나 표준 숫자 타입에대한 연산자 우선순위와 같다.
  • 연산자 함수와 자바
    • 자바를 코틀린에서 호출하는 경우에는 함수 이름이 코틀린의 관례에 맞아 떨어지기만 하면 항상 연산자 식을 사용해 그 함수를 호출할 수 있다.
  • 코틀린 연산자가 자동으로 교환 법칙을 지원하지는 않는다.
  • 비트 연산자에 대해 특별한 연산자 함수를 사용하지 않는다.
    • 중의 연산자 표기법을 지원하는 일반 함수를 사용해 비트 연산을 수행한다.
    • shl: 왼쪽 시프트(자바 <<)
    • shr: 오른쪽 시프트(부호 비트 유지, 자바 >>)
    • ushr: 오른쪽 시프트(0으로 부호 비트 설정, 자바 >>>)
    • and: 비트 곱(자바 &)
    • or: 비트 합(자바 |)
    • xor: 비트 배타 합(자바 ^)
    • inv: 비트 반전(자바 ~)

복합 대입 연산자 오버로딩

  • 복합 대입 연산자: +=, -=
var p1 = Point(1, 2)  
p1 += Point(2, 3)
  • += 연산이 객체에 대한 참조를 다른 참조로 바꾸기보다 원래 객체 내부 상태를 변경하고 싶은 경우는, 반환 타입이 UnitplusAssign 함수를 정의하면 된다.
  • 이론적으로 +=plus, plusAssign 양쪽으로 컴파일할 수 있다.
    • 두 함수가 모두 정의되어 있고, +=에 사용가능한 경우 컴파일러는 오류를 보고한다.
    • varval로 바꿔서 plusAssign 적용이 불가능하게 할 수 도 있다.
    • 가능하면 plusplusAssign 연산을 동시에 정의하지 말라.
  • 코틀린 표준 라이브러리는 컬렉션에 대해 두 가지 접근 방법을 제공한다.
    • +, -는 항상 새로운 컬렉션을 반환
    • +=, -= 연산자는 항상 변경 가능한 컬렉션에 작용해 메모리에 있는 객체 상태 변화
      • 읽기 전용 컬렉션에서 +=-=는 변경을 적용한 복사본을 반환

단한 연산자 오버로딩

  • 오버로딩할 수 있는 단한 산술 연산자
함수 이름
+atimes
-adiv
!amod(1.1부터 rem)
++a, a++plus
--a, a--minus

비교 연산자 오버로딩

동등성 연산자: equals

  • == 연산자 호출을 equals 메소드 호출로 컴파일한다.
  • != 연산자를 사용하는 식도 equals 호출로 컴파일된다.
  • ==!=는 내부에 인자가 널인지 검사하므로 다른 연산과 달리 널이 될 수 있는 값에도 적용할 수 있다.

  • equals 함수는 Anyequals를 오버라이드하면서 operator를 붙이지 않아도 된다. Anyequals에는 operator가 붙어있다.

순서 연산자: compareTo

  • 자바에서 Comparable 인터페이스를 구현해야 클래스를 비교할 수 있다.
    • Complarable에 들어있는 compareTo 메소드는 한 객체와 다른 객체의 크기를 비교해 정수로 나타내준다.
  • 코틀린도 똑같은 Comparable 인터페이스를 지원한다.
  • 비교 연산자(<, >. <=, >=)는 compareTo 호출로 컴파일된다.
  • 코틀린 표준 라이브러리 comparaValuesBy
    • 인자로 넣은 여러 개의 식으로 두 객체의 대소를 알려주는 0이 아닌 값이 처음 나올때까지 인자로 받은 함수를 차례로 호출해 두 값을 비교하며, 모든 함수가 0을 반환하면 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)  
   }  
}

컬렉션과 범위에 대해 쓸 수 있는 관례

인덱스로 원소에 접근: get과 set

  • get() 메소드에 operator 변경자를 붙이면, 각괄호를 사용한 연산을 할 수 있다.
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")  
      }  
   }
}
val p1 = Point(100, 200)  
println(p1[1])
  • 2차원 행렬이나 배열을 표현하는 클래스에서 operator fun get(rowIndex: Int, colIndex: Int) 를 정의 하면 matrix[row, col]로 그 메소드를 호출할 수 있다.
  • set()도 비슷하게 지정이 가능하다.
operator fun set(index: Int, value: Int) {  
   when (index) {  
      0 -> x = value  
      1 -> y = value  
      else -> throw IndexOutOfBoundsException("Invalid coordinate $index")  
   }  
}

in 관례

  • in 연산자와 대응 하는 함수는 contains() 다.
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  
   }  
}
val rect = Rectangle(Point(10, 20), Point(50, 50))  
println(Point(20, 30) in rect)

rangeTo 관례

  • .. 연산자는 rangeTo() 함수를 간략하게 표현하는 방법이다.
  • 이 연산자는 아무 클래스에나 정의할 수 있다.
  • 하지만 Comparable 인터페이스를 구현하면 rangeTo()를 정의할 필요가 없다.
    • 코틀린 표준 라이브러리에는 모든 Comparable 객체에 대해 적용 가능한 rangeTo 함수가 들어있다.
operator fun <T: Comparable<T>> T. rangeTo (that: T) : ClosedRange<T>

for 루프를 위한 iterator 관례

  • for 루프 안에서 in 연산자는 의미가 다르다.
    • iterator() 함수를 호출해서 이터레이터를 얻은 다음, hasNext()next() 호출을 반복하는 식으로 변환한다.
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =  
   object : Iterator<LocalDate> {  
      var current = start  
  
      override fun hasNext(): Boolean {  
         return current <= endInclusive  
      }  
  
      override fun next(): LocalDate {  
         return current.apply {  
            current = plusDays(1)  
         }  
      }  
   }```

```kotlin
val newYear = LocalDate.ofYearDay(2017, 1)  
  
for (dayOff in newYear.minusDays(1)..newYear) {  
   println(dayOff)  
}

구조 분해 선언과 component 함수

  • 구조 분해 선언(destructuring declaration): 복합적인 값을 분해해서 여러 다른 변수를 한꺼번에 초기화할 수 있다.
  • 구조분해 선언은 componentN() 이라는 함수를 호출하는 관례를 사용한다.
  • data 클래스의 주 생성자에 들어있는 프로퍼티에 대해서는 컴파일러가 자동으로 componentN 함수를 만들어준다.
  • 컬렉션의 경우 코틀린 표준 라이브러리에서 맨 앞 다섯 원소에 대한 componentN을 제공한다.
    • 다만 컬렉션 크기를 벗어나는 위치의 원소에 대한 구조 분해 선언을 사용하면 IndexOutOfBoundsException 등의 예외가 발생한다.
  • 표준 라이브러리의 PairTriple 클래스를 사용하면 하뭇에서 여러 값을 더 간단하게 반환할 수 있다.
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)  
}

구조 분해 선언과 루프

  • 구조 분해 선언은 변수 선언이 들어갈 수 있는 장소라면 어디든 사용 가능하다.
  • 아래는 iterator와 componentN 두 가지 관례를 사용했다.
fun printEntries(map: map<String, String>) {
	for ((key, value) in map) {
		println("$key -> $value")
	}
}

프로퍼티 접근자 로직 재활용: 위임 프로퍼티

  • 위임: 객체가 직접 자신의 작업을 수행하지 않고 다른 도우미 객체가 그 작업을 처리하게 맡기는 디자인 패턴
  • 위임 객체(delegate): 작업을 처리하는 도우미 객체

위임 프로퍼티 소개

  • 위임 프로퍼티의 일반적인 문법은 다음과 같다.
    • p 프로퍼티는 접근자 로직을 Delegate 클래스의 객체에게 위임한다.
class Foo {  
   var p: Type by Delegate()  
}
  • 다음과 같이 컴파일러는 숨겨진 도우미 프로퍼티를 만들고 그 프로퍼티를 위임 객체의 인스턴스로 초기화 한다.
    • p 프로퍼티는 바로 그 위임 객체에게 자신의 작업을 위임한다.
    • 프로퍼티 위임 관례에 따르는 Delegate 클래스는 getValuesetValue(변경이 가능한 경우) 메소드를 제공해야 한다.
    • 관례를 사용하는 다른 경우와 마찬가지로 getValuesetValue는 멤버 메소드이거나 확장 함수일 수 있다.

위임 프로퍼티 사용: by lazy()를 사용한 프로퍼티 초기화 지연

  • 지연 초기화(lazy initialization): 객체의 일부분을 초기화하지 않고 남겨뒀다가 실제로 그 부분의 값이 필요할 경우 초기화하는 패턴
  • 뒷받침하는 프로퍼티(backing property)라는 기법을 사용한다.
    • _emails라는 프로퍼티는 값을 저장하고, 다른 프로퍼티인 emails_emails라는 프로퍼티에 대한 읽기 연산을 제공한다.

  • 위임 프로퍼티를 사용하면 위 코드가 더 간단해진다.
    • 위임 프로퍼티는 데이터를 저장할 떄 쓰이는 뒷받침하는 프로퍼티와 값이 오직 한 번만 초기화됨을 보장하는 게터 로직을 함께 캡슐화해준다.
    • lazy 함수를 통해 이를 사용할 수 있다.
    • lazy 함수의 인자는 값을 초기화할 때 호출할 람다다.
    • lazy 함수는 기본적으로 스레드 안전하다. 하지만 필요에 따라 동기화에 사용할 락을 함꼐 전달할 수도 있다.

위임 프로퍼티 구현

  • 어떤 객체의 프로퍼티가 바뀔 때마다 리스너에게 변경 통지를 보내기 구현
  • 자바에서는 PropertyChangeSupportPropertyChangeEvent 클래스를 사용해 구현한다.
    • PropertyChangeSupport: 리스너의 목록을 관리
    • PropertyChangeEvent: 이벤트가 들어오면 목록의 모든 리스너테에 이벤트를 통지한다.
    • 자바 빈 클래스의 필드에 PropertyChangeSupport 인스턴스를 저장하고 프로퍼티 변경 시 그 인스턴스에게 처리를 위임하는 방식으로 구현한다.
  • 모든 클래스에 PropertyChangeSupport를 추가하고 싶지 않으므로, 변경 통지를 할 클래스의 부모 타입이 될 PropertyChangeAware 클래스를 만든다.
  • 변경 통지를 위임할 ObservableProperty 클래스를 만든다.
open class PropertyChangeAware {  
  
   protected val changeSupport = PropertyChangeSupport(this)  
  
   fun addPropertyChangeListener(listener: PropertyChangeListener) {  
      changeSupport.addPropertyChangeListener(listener)  
   }  
  
   fun removePropertyChangeListener(listener: PropertyChangeListener) {  
      changeSupport.removePropertyChangeListener(listener)  
   }  
}  
  
open 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 DelegatedPerson(  
   val name: String, age: Int  
) : PropertyChangeAware() {  
   val _age = ObservableProperty("age", age, changeSupport)  
   var age: Int  
      get() = _age.getValue()  
      set(value) { _age.setValue(value) }  
}
  • 위임 프로퍼티를 사용할 수 있도록 다음과 같이 수정한다.
    • 코틀린 관례에 사용하는 다른 함수와 마찬가지로 getValuesetValue함수에도 operator 변경자가 붙는다.
    • getValuesetValue는 프로퍼티가 포함된 객체와 프로퍼티를 표현하는 객체를 파라미터로 받는다. (KProperty)
    • KProperty 인자를 통해 프로퍼티 이름을 전달받으므로 주 생성자에서는 name 프로퍼티를 없앤다.
    • https://kotlinlang.org/docs/delegated-properties.html#translation-rules-for-delegated-properties
open class PropertyChangeAware {  
  
   protected val changeSupport = PropertyChangeSupport(this)  
  
   fun addPropertyChangeListener(listener: PropertyChangeListener) {  
      changeSupport.addPropertyChangeListener(listener)  
   }  
  
   fun removePropertyChangeListener(listener: PropertyChangeListener) {  
      changeSupport.removePropertyChangeListener(listener)  
   }  
}  
  
open class ObservableProperty(  
   var propValue: Int,  
   val changeSupport: PropertyChangeSupport  
) {  
   operator fun getValue(p: DelegatedPerson, prop: KProperty<*>): Int = propValue  
   operator fun setValue(p: DelegatedPerson, prop: KProperty<*>, newValue: Int) {  
      val oldValue = propValue  
      propValue = newValue  
      changeSupport.firePropertyChange(prop.name, oldValue, newValue)  
   }  
  
}  
  
class DelegatedPerson(  
   val name: String, age: Int  
) : PropertyChangeAware() {  
   var age: Int by ObservableProperty(age, changeSupport)  
}
  • 코틀린 표준 라이브러리에서 비슷한 클래스를 제공해준다.
    • Delegates.observable(property, observer)
    • observer: 프로퍼티 값 변경을 통지할 때, 로직을 작성하는 람다.
class DelegatedPerson(  
   val name: String, age: 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)  
}

위임 프로퍼티 컴파일 규칙

  • MyDelegate 클래스의 인스턴스를 감춰진 프로퍼티에 저장하며 <delegate>라는 이름으로 부른다.
  • 프로퍼티를 표현하기 위해 KProperty 타입의 객체를 사용하며 <property>라고 부른다.

  • 이 매커니즘은 프로퍼티 값이 저장될 장소를 바꿀 수도 있고, 프로퍼티를 읽거나 쓸 때 벌어질 일을 변경할 수도 있다.

프로퍼티 값을 맵에 저장

  • 확장 가능한 객체: 자신의 프로퍼티를 동적으로 정의할 수 있는 객체
  • 맵을 통해 위임 프로퍼티를 사용하면 구현이 가능하다.
  • 정보를 모두 맵에 저장하되 그 맵을 통해 처리하는 프로퍼티를 통해 필수 정보를 제공할 수 있다.
  • MapMutableMap 인터페이스에 대해 getValuesetValue 확장 함수를 제공한다.
    • getValue에서 맵에 프로퍼티 값을 저장할 때는 자동으로 프로퍼티 이름을 키로 활용한다.
class ExpandoPerson {  
   private val _attributes = hashMapOf<String, String>()  
  
   fun setAttribute(attrName: String, value: String) {  
      _attributes[attrName] = value  
   }  
  
   val name: String by _attributes  
}

프로퍼티에서 위임 프로퍼티 활용

  • 객체 프로퍼티를 저장하거나 변경하는 방법을 바꿀 수 있으면 프레임워크를 개발할 때 유용하다.
  • Users 객체는 데이터베이스 테이블을 표현한다.
  • User의 상위 클래스인 Entity 클래스는 데이터베이스 칼럼을 엔티티 속성값으로 연결해주는 매핑이 있다.
  • Column 객체(Users.name. Users.age)를 위임 객체로 사용한다.

  • 프레임워크는 Column 객체 안에 getValuesetValue메소드를 정의해주기만 하면 된다.
    • User 객체를 변경하면 그 객체는 dirty 상태로 변하고, 나중에 적절히 데이터베이스에 변경 내용을 반영한다.

  • 이 예제의 완전한 구현은 Exposed 프레임워크 소스코드에서 볼 수 있다.