관례?
여기서 관례(컨벤션)는, 코틀린에서 쓰는 plus, equals, get 같은 함수명을 관례라고 칭한다.
7.1 산술 연산자 오버로딩
자바에서 산술 연산자는 원시(Primitive) 타입과 String에 대해서만 사용할 수 있다.
코틀린에서는 클래스에서 산술 연산자를 인자값을 다르게 해서 새로 정의함으로써 원하는대로 쓸 수 있다.
산술 연산자 오버로딩은 코틀린에서 이미 정의해놓은 연산자(+, *, /, ...)를 내가 만든 클래스에서 해당 함수(.plus(), .times(), ...)의 인자값을 다르게 받으면 그 경우로 새로 만들어 쓸 수 있다는 뜻이다.
다음부터는 코틀린에 어떤 산술 연산자가 있고 어떻게 사용하는지 알아볼 수 있다.
7.1.1 이항 산술 연산 오버로딩
리스트 7.1 plus 연산자 구현하기
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x+other.x, y+other.y)
}
}
정해진 이름의 함수(여기서는 plus)를 오버로딩 한 것이다.
데이터 클래스 Point에서 plus 함수를 Point 타입의 객체를 인자로 받는 경우를 정의해서 해당 객체를 + 연산자로 내가 정의한 대로 쓸 수 있다.
a + b 이렇게 쓰면 a.plus(b)로 컴파일 된다.
+연산자는 plus 함수 호출로 컴파일된다는 뜻이다.
plus 함수를 정의할때 operator 키워드를 붙여야한다.
하지만 프로그래머가 직접 연산자를 만들어 사용할 수 없다.
.plus() 같은 함수들은 코틀린에 기본적으로 정의된 산술연산자 함수이고 operator 키워드를 붙여 오버로딩해서 재정의해 사용할 수 있는 것이다.
연산자 함수와 자바
코틀린 연산자를 자바에서도 호출할 수 있다.
자바 코드를 코틀린에서 호출하는 경우에는 함수이름이 코틀린의 컨벤션에 맞아 떨어지기만 하면 항상 연산자 식을 사용해 그 함수를 호출할 수 있다.
자바함수 + 자바함수
→자바함수.plus(자바함수)
함수이름이 코틀린의 컨벤션에 맞아 떨어지기만 하면 → 코틀린에 있는 .plus() 함수를 이름 그대로 클래스에서 오버로딩해 사용한다면 + 연산자를 사용해 plus() 함수를 호출할 수 있다는 뜻.
하지만 자바에는 따로 연산자에 표시를 할 수 없다. 그래서 코틀린의 operator 변경자를 사용해야한다는 요구 사항을 자바 메소드에 적용할 수는 없다. 따라서 이름과 파라미터만 맞게 쓰면 된다.
→ 클래스에서 plus 함수를 오버로딩 하는 경우 자바에서 operator 변경자를 쓸 수 없음.
자바 클래스에 연산자 기능을 제공하는 메소드가 이미 있지만, 이름만 다르다면 컨벤션에 맞는 이름을 가진 확장 함수를 작성하고 연산을 기존 자바 메소드에 위임하면 된다.
연산자를 정의할 때 두 피연산자(연산자 함수의 두 파라미터)가 같은 타입일 필요는 없다.
리스트 7.3 두 피연산자의 타입이 다른 연산자 정의하기
operator fun Point.times(scale: Double): Point {
return Point((x*scale).toInt(), (y*scale).toInt())
}
→Double 타입의 인자를 받는 경우도 오버로딩 해주면 됨.
1.5 * p 라고 쓰는 경우는?
operator fun Double.times(P: Point) : Point { return ... }
→이렇게 Double 타입의 경우도 오버로딩 해주면 쓸 수 있음.
코틀린 연산자가 자동으로 교환 법칙을 지원하지는 않는다.
7.1.2 복합 대입 연산자 오버로딩
+=, -= 연산자는 복합 대입 compound assignment 연산자라 불린다.
point += Point(3,4)
→ point = point + Point(3,4)
→ point = point.plus(Point(3,4))
위처럼 += 연산은 +연산자를 사용하고 이것은 기존의 객체의 정보를 가져다가 새로운 객체를 만들어서 반환 할 수도 있지만,
새로운 객체를 만들지 않고 원래 객체의 내부 상태를 변경하게 만들 수도 있다.
그 예 중 하나가 변경 가능한 컬렉션에 원소를 추가하는 것이다.
plusAssign 함수를 정의하면 += 연산자에 이 함수를 사용한다.
변경 가능한 컬렉션에 대해 plusAssign을 정의할 수 있다.
operator fun <T> MutableCollection<T>.plusAssign(element: T) {
this.add(element)
}
+= 연산자는 plus 또는 plusassign 함수 둘 다 호출이 가능하다.
plus와 plusAssign 연산을 동시에 정의하지 말아야 한다.
어떤 클래스가 두 함수를 모두 정의하고 둘 다 +=에 사용 가능한 경우 컴파일러는 오류를 보고한다.
a += b
-
a = a.plus(b)
-
a.plusassign(b)
val list = arrayListOf(1, 2) list += 3 // +=는 "list"를 변경한다.
val newList = list + listOf(4, 5) // +는 두 리스트의 모든 원소를 포함하는 새로운 리스트를 반환한다.
println(list)
//[1,2,3]
println(newList)
//[1,2,3,4,5]
컬렉션에서 +,-는 새로운 컬렉션을 반환하고
+=,-=는 (변경 가능한 컬렉션에서) 내부의 객체 상태를 변환한다.
지금까지는 이항 연산이였고 이제 단항 연산자를 설명할 차례!
7.1.3 단항 연산자 오버로딩
리스트 7.5 단항 연산자 정의하기
operator fun Point.unaryMinus(): Point { return Point (-x, -y) } val p = Point(10,20) println(-p)
operator fun Point.unaryMinus(): Point
return Point (-x, -y)
}
val p = Point(10,20)
println(-p)
단항 함수는 파라미터가 없다.
+a → a.unaryPlus()
단항 함수를 정의해 증가/감소 연산자를 오버로딩하는 경우 컴파일러는 일반적인 값에 대한 전위와 후위 증가/감소 연산자와 같은 의미를 제공한다.
후위 증가 연산자는 실행된 다음에 값을 증가시킨다.
전위 증가 연산자는 실행되기 전에 값을 증가시킨다.
전위와 후위 연산을 처리하기 위해 별다른 처리를 해주지 않아도 제대로 증가 연산자가 작동한다.
7.2 비교 연산자 오버로딩
equals나 compareTo를 호출해야하는 자바와 달리 코틀린에서는 == 비교 연산자를 직접 사용할 수 있다.
직접 사용?
→ == 연산자는 equals 함수를 오버라이드 해서 내 맘대로 쓸 수 있다.
7.2.1 동등성 연산자: equals
코틀린은 == 연산자 호출을 equals 메소드 호출로 컴파일 한다.
!= 연산자를 사용하는 식도 equals 호출로 컴파일 된다.
이 경우에는 비교 결과를 뒤집은 값을 결과 값으로 사용한다.
==와 !=는 내부에서 인자가 널인지 검사하므로 널이 될 수 있는 값에도 적용할 수 있다.
a == b 라는 비교를 처리할때 코틀린은 a가 널인지 판단해서 널이 아닌 경우에만 a.equals(b)를 호출한다. a가 널이라면 b가 널인 경우에 true를 반환한다.
a == b → a?.equals(b) ?: (b==null)
동등성 검사 ==는 equals 호출과 널 검사로 컴파일된다.
코드 해석
?: 앨비스 연산자로 왼쪽에 있는 코드의 결과가 null이라면 오른쪽이 실행되고 null이 아니라면 왼쪽이 실행된다.
a?.equals(b)
a가 널이 아니라면 a의 equals(b) 함수를 호출한다.
null이라면 아무일도 일어나지 않으며 null일 경우 앨비스 연산자로 인해 오른쪽이 실행된다.
b==null
a가 null일 때 실행되는 코드. a가 null일때 b도 null이여야 true를 반환한다. 그러므로 b가 null인지 물어본다. a가 null인지는 이미 검사해서 이 코드로 넘어왔으므로 b도 null이면 둘다 null이므로 true. b가 null이 아니라면 null인 a와 다르므로 false를 반환하게 만들었다.
→ null 검사를 equals 는 무료로 해줍니다...
Data 클래스는 컴파일러가 equals를 자동으로 생성해준다.
직접 equals를 구현할 수도 있다.
리스트 7.7 equals 메소드 구현하기
class Point(val x: Int, val y: Int) {
override fun equals(obj: Any?): Boolean {
if (obj === this) return true
//파라미터인 obj가 this와 같은 객체(같은 참조)인지 검사한다.
//나자신이라면 검사할 필요가 없다.
if (obj !is Point) return false
//파라미터인 obj의 타입이 Point인지 검사한다.
//타입이 다르다면 검사할 필요가 없다.
return obj.x == x && obj.y == y
//나 자신도 아니고, 타입도 Point라서 비교할 수 있는 대상인지 검사하고나면
//진짜 검사를 한다. 내 x값과 파라미터(obj)의 x값과 y값이 같은지 비교.
}
}
equals 함수는 Any에 정의된 메소드이므로 override를 해야한다.
Any에 == 연산자의 함수가 정의가 되어있음.
7.2.2 순서 연산자: compareTo
자바에서는 정렬, 최대값, 최소값 등 값을 비교해야 하는 알고리즘에 사용할 클래스는 Comparable 인터페이스를 구현(implements)해야한다.
Comparable에 있는 compareTo 메소드는 한 객체와 다른 객체의 크기를 비교해 정수로 나타내준다.
자바에서 비교연산자는 primitive 타입의 값만 비교할 수 있고 이 외 다른 타입의 값에는element1.compareTo(element2)를 명시적으로 사용해야한다.
하지만 코틀린에서는 compareTo 메소드를 모든 타입에 연산자로 호출할 수 있다.
compareTo의 return 타입은 Int이다.
a>=b → a.compareTo(b)>=0
두 객체를 비교하는 식은 compareTo의 결과를 0과 비교하는 코드로 컴파일된다.
리스트 7.8 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)
}
}
val p1 = Person("Alice", "Smith")
val p2 = Person("Bob","Johnson")
println( p1 < p2 ) // false
compareTo(객체1, 객체2, 비교조건1, 비교조건2)
비교조건1에서 모든 조건에 대한 return이 0(같다)이 나오면 비교조건2로 비교한다.
비교조건1인 lastName을 비교했을때 알파벳 우선순위로 p1의 S가 P2의 J보다 우선이기때문에
p1 < p2 → p2가 p1보다 먼저인가요? 에 대한 답은 false이다.
7.3 컬렉션과 범위에 대해 쓸 수 있는 관례
컬렉션을 다룰 때 가장 많이 쓰는 연산은 인덱스를 사용해 원소를 읽거나 쓰는 연산과 어떤 값이 컬렉션에 속해있는지 검사하는 연산이다.
이 모든 연산을 연산자 구문으로 사용할 수 있다.
인덱스를 사용해 원소를 설정하거나 가져오고 싶을 때는 a[b]라는 식을 사용한다.
(이를 인덱스 연산자라고 부른다)
in 연산자는
1.원소가 컬렉션이나 범위에 속하는지 검사하거나
2.컬렉션에 있는 원소를 이터레이션할 때 사용한다.
7.3.1 인덱스로 원소에 접근: get과 set
코틀린에서나 자바에서나 맵과 배열 원소에 접근할 때 모두 각괄호( [ ] )를 사용한다
같은 연산자를 사용해 변경 가능 맵에 키/값 쌍을 넣거나 이미 맵에 들어있는 키/값 연관 관계를 변경할 수 있다.
mutableMap[key] = newValue
코틀린에서 인덱스 연산자를 사용해 원소를 읽는 연산은 get 연산자 메소드로 변환되고,
원소를 쓰는 연산은 set 연산자 메소드로 변환된다.
Map과 MutableMap 인터페이스에는 그 두 메소드가 이미 들어있다.
리스트 7.9 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
// p[1]은 p.get(1)로 변환된다.
operator 변경자를 붙인 get 이라는 메소드를 만들면 된다.
x[a, b] → x.get(a, b)
각괄호를 사용한 접근은 get 함수 호출로 변경된다.
get 메소드의 파라미터로 Int가 아닌 타입도 사용할 수 있다.
예를 들면 맵 인덱스 연산의 경우 get 파라미터 타입은 맵의 키 타입과 같은 타입이 될 수 있다.
또한 여러 파라미터를 사용하는 get을 정의할 수도 있다.
예를 들면 2차원 행렬이나 배열을 표현하는 클래스에
operator fun get(rowIndex: Int, colIndex: Int)를 정의하면
matrix[row, col] 으로 get 메소드를 호출할 수 있다.
컬렉션 클래스가 다양한 키 타입을 지원해야 한다면 다양한 파라미터 타입에 대해 오버로딩한 get 메소드를 여럿 정의할 수도 있다.
인덱스에 해당하는( a[index] ) 컬렉션 원소를 쓰고 싶을 때는 set 이라는 함수를 정의하면된다.
불변 클래스는 set을 쓸 수 없다. 변경 가능한 클래스에서 가능하다.
리스트 7.10 관례를 따르는 set 구현하기
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)
x[a, b] = c → x.set(a, b, c)
7.3.2 in 관례
in은 객체가 컬렉션에 들어있는지 검사한다. (true/false)
이 경우에 in 연산자는 contain 함수를 호출한다.
리스트 7.11 in 관례 구현하기
data class Rectangle(val upperLeft: Point, val lowerRight: Point)
operator fun Rectangle.comtains(p: Point) : Boolean {
return p.x in upperLeft.x until lowerRight.x &&
p.y in upperLeft.y until lowerRight.y
// upperLeft.x until lowerRight.x -> 범위를 만든다
// p.x in uper... -> .contains(p.x) -> 범위안에 p.x가 있는지 검사한다
}
in의 우항에 있는 객체는 contains 메소드의 수신객체가 되고,
in의 좌항에 있는 객체는 contains 메소드에 인자로 전달된다.
a in c → c.contains(a)
in은 열린범위이다.
열린범위란? 끝 값을 포함하지 않는 범위.
..은 닫힌범위이다. (10..20)
닫힌범위란? 끝 값을 포함하는 범위.
7.3.3 rangeTo 관례
.. 연산자는 rangeTo 함수를 호출한다.
start .. end → start.rangeTo(end)
rangeTo 함수는 범위를 반환한다.
이 연산자(rangeTo 함수)는 아무 클래스에나 정의할 수 있지만,
클래스가 Comparable 인터페이스를 구현하면 이 연산자(rangeTo 함수)를 정의하지 않아도 된다.
왜냐면 코틀린 표준 라이브러리에는 모든 Comparable 객체에 대해 적용 가능한 rangeTo 함수가 들어있다.
operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>
Comparable을 implements하는 T 타입을 받겠다
<T: Comparable<T>> = T.ran
CloseRange 닫힌범위 → rangeTo는 이거
OpenRange 열린범위 → until은 이거
자바 8 표준 라이브러리 LocalDate 클래스를 사용해 날짜의 범위를 만들기
리스트 7.12 날짜의 범위 다루기
val now = LocalDate.now()
val vacation = now..now.plusDays(10)
// now.rangeTo(now.plusDays(10)) 으로 컴파일러에 의해 변환된다.
println(now.plusWeeks(1) in vacation)
// vacation.contains(now.plusWeeks(1))
// now.plusWeeks(1) 이 날짜가 vacation 범위에 들어가는지 검사한다.
// true
7.3.4 for 루프를 위한 iterator 관례
코틀린의 for 루프는 in 연산자를 사용하는데
이때 in 연산자는 iterator 함수를 호출한다.
for (x in list) { ... } 와 같은 문장은
list.iterator()를 호출한다.
iterator에 대해 hasNext와 next 호출을 반복한다.
코틀린에서는 iterator 메소드를 확장 함수로 정의할 수 있다.
그렇기때문에 자바 문자열에 대한 for 루프도 가능하다.
코틀린 표준 라이브러리는 String의 상위 클래스인 charSequence에 대한 iterator 확장 함수를 제공한다.
operator fun CharSequence.iterator(): CharIterator
// 이 라이브러리 함수는 문자열을 이터레이션 할 수 있게 해준다.
for (c in "abc") {}
클래스 안에 직접 iterator 메소드를 구현하기.
리스트 7.13 날짜 범위에 대한 이터레이터 구현하기
operator fun ClosedRange<LocalData>.iterator(): Iterator<LocalDate> =
object : Iterator<LocalDate> {
// 이 객체는 LocalDate 원소에 대한 iterator를 구현한다.
var current = start
override fun hasNext() =
current <= endInclusive
// compareTo 관례를 사용해 날짜를 비교한다.
// current.compareTo(endInclusive) <= 0
override fun next() = current.apply {
current = plusDays(1)
}
// 현재 날짜를 저장한 다음에 날짜를 변경한다. 그 후 저장해둔 날짜를 반환한다.
}
}
코드 해석
CloseRange<LocalData>
: LocalData 타입의 원소를 가지고 있는 컬렉션인 CloseRange.
.iterator() : Iterator<LocalDate>
: 확장 함수 iterator() 정의. 반환 타입은 LocalDate 원소를 가지고 있는 Iterator 컬렉션.
object : Iterator<LocalDate>
: LocalDate 타입의 원소를 가지고 있는 Iterator 컬렉션이 타입인 익명객체를 만듬.
var current = start
: start 는 원소의 첫번째값이 들어가는것같다 (추측)
: 변수 current에 할당해줌.
hasNext()
: current에 원소가 들어있는지 확인해서 있으면 true를 반환한다.
: true면 for문의 본문이 실행된다.
: false면 for문 종료
: current <= endInclusive
: current.compareTo(endInclusive) <= 0
: current.compareTo(endInclusive) 의 값이 -거나 0이 나오면 true.
: +값이 나오거나 바로 false가 반환되면 for문 종료
: compareTo가 어떻게 구현되어 있는지를 모르겠다;
next()
: current 값을 다음 원소 값으로 변경해준다.
: hasNext()가 true를 반환할경우 next() 에서 적용된 원소값으로 본문을 실행한다.
7.4 구조 분해 선언과 component 함수
구조 분해를 사용하면 복합적인 값을 분해해서 여러 다른 변수를 한꺼번에 초기화할 수 있다.
구조 분해 선언은 여러 변수를 괄호로 묶는다.
val (a,b) = p → val a = p.component1(), val b = p.component2()
구조 분해 선언은 componentN 함수 호출로 변환된다.
구조 분해 선언은 함수에서 여러 값을 반환할 때 유용하다.
리스트 7.14 구조 분해 선언을 사용해 여러 값 반환하기.
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, ext) = splitFilename("example.kt")
// val name = splitFilename("example.kt").component1()
7.4.1 구조 분해 선언과 루프
7.5 프로퍼티 접근자 로직 재활용: 위임 프로퍼티
7.5.1 위임 프로퍼티 소개
'Kotlin' 카테고리의 다른 글
Kotlin in action 6장 코틀린 타입 시스템 (0) | 2020.04.13 |
---|---|
Kotlin in action 5장 람다로 프로그래밍 (0) | 2020.04.09 |