025. 산술 연산자 오버로딩

산술 연산자 오버로딩

코틀린도 Java와 마찬가지로 산술 연산자가 존재한다.

Java의 산술 연산자는 원시타입에서만 사용 가능하고, + 연산자에 한정하여 문자열의 결합에 사용할 수 있다.

하지만 원시타입이 아닌 경우에도 사용할 수 있게한다면 매우 편리할 것이다.

예를 들어 BigInteger 클래스를 통해 숫자를 작업을 진행할 때 메소드를 명시적으로 호출하는 것보다 +로 작업하는 경우가 더 명시적이고 우아한 코드로 보일 것이다.

위와 같은 작업을 위해 수행해야하는 연산자 오버로딩에 대해 알아보자.

이항 산술 연산자에 대한 오버로딩

먼저, 특정 위치를 표현할 수 있는 Point라는 좌표 클래스가 있다고 할때, 두 좌표의 덧셈 연산을 구현해보도록 하자.

단순하게 구현한다면 아래와 같이 구현할 수 있을 것이다.

1
2
3
4
5
6
7
8
9
10
11
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 키워드가 앞에 붙여 연산자 오버로딩에 대한 명시적인 선언이 있어야 하며, 만약 키워드를 사용하지 않으면 operator modifier is required오류가 발생한다.

위의 예제의 경우 메소드 명이 plus이므로 + 연산자를 오버로딩한 것임을 파악할 수 있고, 이제 plus라는 함수를 시용해서도 부를 수 있다.

1
2
3
4
val p1 = Point(10, 20)
val p2 = Point(30, 40)
println(p1 + p2)
println(p1.plus(p2))

아래와 같이 특정 클래스의 멤버 함수가 아닌 확장 함수로도 연산자 오버로딩을 수행할 수 있다.

1
2
3
operator fun Point.plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}

+ 연산자를 plus 함수로 오버로딩한 것처럼 코틀린에서 사용할 수 있는 이항 산술 연산자는 한정되어 있다.

연산자 오버로딩을 하더라도 기존 연산자의 우선순위는 유지되며, 우선순위는 아래 표에 나온 순서와 동일하다.

*, /, %가 같은 우선순위를 가지고 있으며,+-가 그 다음 우선순위를 가진다.

아래 표를 통해 확인해보자.

Expression Function name
a * b times
a / b div
a % b mod rem
a + b plus
a - b minus

참고 코틀린 1.1버전부터 mod 대신 rem을 사용한다.
Operator overloading 문서 중 Note that the rem operator is supported since Kotlin 1.1. Kotlin 1.0 uses the mod operator, which is deprecated in Kotlin 1.1.

추가적으로 이항 연산자를 오버로딩할 때, 각 항의 타입이 동일할 필요는 없다.

예를 들어 좌표를 표현하는 Point 클래스를 실수를 사용해 곱하는 경우를 오버로딩한다면 아래와 같이 작성할 수 있다.

1
2
3
4
5
6
7
8
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)

연산자 오버로딩시 위의 p * 1.5에 대해 1.5 * p와 같은 교환 법칙이 성립하지 않는다.

코틀린에서 교환 법칙을 성립시키고 즉, 각 항의 위치와 상관없이 사용하고 싶다면 아래와 같이 별도의 연산자를 또 정의해야 한다.

1
2
3
4
5
6
7
8
9
// p * 1.5
operator fun Point.times(scale: Double): Point {
return Point((x * scale).toInt(), (y * scale).toInt())
}

// 1.5 * p
operator fun Double.times(p: Point): Point {
return Point((this * p.x).toInt(), (this * p.y).toInt())
}

각 항의 타입을 다르게 쓸 수 있는 것처럼 반환 타입도 다르게 작성할 수 있다.

아래 코드를 보자.

1
2
3
4
5
6
7
operator fun Char.times(count: Int): String {
return toString().repeat(count)
}

/* 출력 결과 */
>>> println('a' * 3)
aaa

위의 코드를 통해 문자타입 Char를 여러번 반복시켜 문자열타입 String을 만들어 냄을 알 수 있다.

복합 대입 연산자에 대한 오버로딩

일반적으로 plus와 같은 연산자를 정의할때 코틀린은 이항 연산자인 +뿐만 아니라 +=도 지원한다.

이때 +=-=과 같은 연산자들을 복합 대입 연산자라고 한다.

아래 예제를 통해 파악해보자.

1
2
3
4
>>> var point = Point(1, 2)
>>> point += Point(3, 4)
>>> println(point)
Point(x=4, y=6)

위의 point += Point(3, 4)point = point + Point(3, 4)과 같은 결과를 보여준다.

위의 point 변수처럼 사용중인 변수가 참조중인 개체를 수정할 수도 있지만, 참조의 재할당이 발생하지 않는 경우도 있다.

대표적인 예로 컬렉션에 연산자를 추가하는 경우가 있다.

아래 코드는 ArrayList<Int>에 42라는 요소를 추가하는 예제이다.

1
2
3
4
>>> val numbers = ArrayList<Int>()
>>> numbers += 42
>>> println(numbers[0])
42

+plus로 표현하듯 +=plusAssign이라는 함수로 표현할 수 있다.

참고 다른 이상 연산자도 마찬가지로 minusAssign, timesAssign 등으로 표현할 수 있다.

코틀린의 표준 라이브러리는 변경이 가능한 콜렉션에서의 함수 plusAssign를 정의하여 지원하고 있으며, 해당 코드를 통해 += 연산자가 요소를 추가함을 알 수 있다.

1
2
3
operator fun <T> MutableCollection<T>.plusAssign(element: T) {
this.add(element)
}

다시 위의 예제 코드를 가져와보자.

1
2
3
4
>>> var point = Point(1, 2)
>>> point += Point(3, 4)
>>> println(point)
Point(x=4, y=6)

위의 point += Point(3, 4)point = point + Point(3, 4)과 같은 결과를 보여준다고 상술한 것처럼

a += ba.plusAssign(b)로 표현할 수 있다.

여기까지는 괜찮지만 문제는 a.plusAssign(b)a = a.plus(b)와 똑같다는 것이다.

결론적으로 + 연산자와 += 연산자를 같은 클래스에서 구현하는 경우, 의미의 충돌이 발생하므로 컴파일 오류가 발생하게 된다.

따라서 plusplusAssign 함수는 동일 클래스에 동시에 작성할 수 없다.

코틀린의 표준 라이브러리는 콜렉션에 한정하여 위의 두 가지 접근 방법을 구현해두었다.

+-는 항상 새로운 컬렉션을 생성하여 반환하며, 변경이 가능한 MutableCollection에서는 객체의 상태를 바꾸고, 불변의 콜렉션에서는 +=-=에 대한 연산을 수행한 복사본을 생성하여 반환한다.

1
2
3
4
5
6
7
>>> val list = arrayListOf(1, 2)
>>> list += 3
>>> val newList = list + listOf(4, 5)
>>> println(list)
[1, 2, 3]
>>> println(newList)
[1, 2, 3, 4, 5]

So far, we’ve discussed overloading of binary operators—operators that are applied to two values, such as a + b. In addition, Kotlin allows you to overload unary operators, which are applied to a single value, as in -a.

단항 산술 연산자에 대한 오버로딩

단항 연산자에 대한 오버로딩도 이항 연산자의 오버로딩 방식과 동일하다.

기존에 정의된 메소드 명으로 operator 키워드와 함께 내부 멤버나 확장 함수로 선언하는 것이다.

아래 코드를 보자.

1
2
3
4
5
6
7
8
operator fun Point.unaryMinus(): Point {
return Point(-x, -y)
}

/* 출력 결과 */
>>> val p = Point(10, 20)
>>> println(-p)
Point(x=-10, y=-20)

단항 연산자는 피연산자 하나만 필요로 하므로, 별도의 파라미터가 필요없다.

코틀린의 단항 산술 연산자는 아래 표를 참고하자.

Expression Function name
+a unaryPlus
-a unaryMinus
!a not
++a, a++ inc
–a, a– dec

아래는 BigDecimal 클래스의 ++ 연산자 오버로딩의 예시이다.

1
2
3
4
5
6
7
8
operator fun BigDecimal.inc() = this + BigDecimal.ONE

/* 출력 결과 */
>>> var bd = BigDecimal.ZERO
>>> println(bd++)
0
>>> println(++bd)
2