본문 바로가기

코틀린

코틀린(Kotlin) - 산술 연산자 오버로딩

코틀린에서는 특정 연산자의 역할을 함수로 정의할 수 있습니다. 이를 Convention(관례)이라고 합니다.

 

가장 기본적인 예로는 산술 연산자가 있습니다.

자바에서는 원시 타입(primitive)에 대해서만 산술 연산자를 사용할 수 있고, 추가로 String에 대해 "+" 연산자를 사용할 수 있습니다.

그러나 다른 클래스에서도 이러한 산술 연산자가 유용한 경우가 있기 마련입니다.

 

지금부터, 어떻게 클래스에 대한 일반 산술 연산자를 정의할 수 있는지에 대해 살펴보도록 하겠습니다.

 

이항 산술 연산 오버로딩

코틀린에서는 +, - 같은 산술 연산자를 오버로딩해서 사용할 수 있습니다.

객체끼리 더하거나 뺄 때, 원하는 동작을 함수 안에 구현하면 연산자를 통해 이를 표현할 수 있습니다.

 

/* 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)

 

연산자를 오버로딩하는 함수 앞에는 plus 함수와 같이 함수명 앞에 operator 키워드를 붙여야 하며, 

operator 키워드를 붙임으로써 이 함수가 Convention(관례)을 따르는 함수임을 명확히 할 수 있습니다.

 

만약, operator 없이 함수명을 우연히 plus로 했다면 "operator modifier is required...(operator 키워드를 추가해야함)" 이라는 오류가 뜨기 때문에 개발자가 문제를 해결할 수 있습니다.

 

위의 예제에서 operator plus 함수를 선언하고 나면 이제 "+" 기호로 두 Point 객체를 더할 수 있게 됩니다.

 

 

+ 연산자는 plus 함수 호출로 컴파일 됩니다.

 

이러한 연산자를 멤버 함수로 만드는 대신 확장 함수로도 정의할 수 있고, 보통 operator 선언은 확장 함수로 만들어서 주로 사용합니다.

 

/* 연산자를 확장 함수로 구현하기 */
operator fun Point.plus(other: Point) : Point {
   return Point(x + other.x, y + other.y)
}

 

이 구현도 앞에 구현과 동일하게 "+" 기호로 사용할 수 있습니다.

 

코틀린은 언어에서 미리 정해둔 연산자만! 오버로딩할 수 있기 때문에, 다른 언어와 비교해서 오버로딩 연산자를 정의하고 사용하는 것이 더 쉽고 편리합니다.

 

함수 이름
a * b times
a / b div
a % b mod (version 1.1 이상부터는 rem)
a + b plus
a - b minus

 

연산자를 정의할 때 두 피연산자(연산자 함수의 두 파라미터)가 같은 타입일 필요는 없습니다.

예를 들어 Point 객체를 어떤 비율에 따라 확대/축소하는 연산자를 정의하는 예제로 알아보도록 하겠습니다.

 

/* 두 피연산자의 타입이 서로 다른 연산자 정의하기 */
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)

 

또한, 연산자 함수의 반환 타입이 꼭 두 피연산자와 일치하지 않아도 됩니다.

예를 들면 Char 타입과 Int 타입으로 연산을 하고 결과는 String으로 반환하는 연산자 조합도 완전히 합법적이며 유용하게 사용되는 오버로딩 케이스입니다!

 

/* 결과 타입이 피연산자 타입과 전혀 다른 연산자 정의하기 */
operator fun Char.times(count: Int) : String {
   return this.toString().repeat(count)
}

>>> println('A' * 3)

AAA

 

복합 대입 연산자 오버로딩

"+=", "-=" 등의 연산자를 복합 대입 연산자라고 하며, "+" 대응 함수인 plus와 같은 연산자를 오버로딩하면 코틀린은 그와 관련된 연산자인 "+=" 도 자동으로 구현해줍니다. 👍

 

>>> var point = Point(1, 2)
>>> point += Point(3, 4)  // point = point + Point(3, 4)와 동일
>>> println(point)

Point(x=4, y=6)

 

단항 연산자 오버로딩

위에 내용까지는 두 피연산자 값에 작용하는 이항 연산자에 대해서 살펴보았고, 이번에는 하나의 값에만 작용하는 단항 연산자에 살펴보겠습니다.

단항 연산자를 오버로딩하는 방법도 이항 연산자와 마찬가지로 미리 정해진 함수를 선언하면서 operator로 표시하면 됩니다.

 

/* 단항 연산자 정의하기 */
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

 

"++" 와 "--" 의 경우 inc(), dec()만 구현해두면 알아서 전위와 후위 증가/감소 연산을 해줍니다.

 

/* 장그 연산자 정의하기 */
operator fun Int.inc() = this + 2

>>> var number = 0
>>> println(number++)
0
>>> println(number)
2
>>> printlin(++number)
4

 

비교 연산자 오버로딩 - 동등성 연산자 : equals

코틀린에서는 모든 객체에 대해 비교 연산을 수행하는 경우, equals나 compareTo를 호출해야 하는 자바와는 달리 "==" 비교 연산자를 직접 사용할 수 있어서 비교하는 코드가 더 간결하며 이해하기 쉽습니다. 👍

 

코틀린은 "==" 연산자 호출을 equals()로 컴파일하며, "!=" 역시 equals() 를 사용하여 결과 값을 not 처리하는 식으로 동작합니다.

a == b 라는 코드는 실제로 내부에서 인자의 null 체크를 하므로 다른 연산과 달리 null이 될 수 있는 값에도 적용할 수 있는 이유입니다.

실제 내부 코드는 아래와 같이 구현되어 있습니다.

 

 

동등성 검사 ==는 equals 호출과 null 체크로 컴파일됩니다.

 

이 경우 a가 null인지 판단해서 null이 아닌 경우에만 a.equals(b)가 호출되고, 만약 a가 null이라면 b도 null인 경우에만 true가 반환됩니다.

 

class Point(val x: Int, val y: Int) {
   ...
   override fun equals(other: Any?): Boolean {
      return super.equals(other)
   }
   ...
}

 

equals() 함수 앞에는 override가 붙어있는 것을 볼 수가 있는데, equals()는 Any에 정의된 함수이므로 override가 필요합니다.

아래는 Any 클래스의 구현이며, equals() 함수 앞에 operator 가 명시되어 있는 것을 확인할 수 있습니다.

 

package kotlin

/**
 * The root of the Kotlin class hierarchy. Every Kotlin class has [Any] as a superclass.
 */
public open class Any {
    ...
    public open operator fun equals(other: Any?): Boolean

    ...
    public open fun hashCode(): Int

    ...
    public open fun toString(): String
}

 

그렇기 때문에, 하위 클래스에서는 override를 해서 사용할 수 있지만, Any가 최상위 객체이며 Any를 상속받는 equals()가 확장 함수보다 우선순위가 높기 때문에 equals()는 사실상 확장 함수로 재정의해서 사용할 수가 없다는 사실에 유의해야 합니다!

 

비교 연산자 오버로딩 - 순서 연산자 : compareTo

자바에서 정렬이나 최댓값, 최솟값 등 값을 비교해야 하는 알고리즘에는 Comparable 인터페이스를 사용합니다.

Comparable의 compareTo 메소드를 이용해서 한 객체와 다른 객체의 크기를 비교해 정수로 나타내 줍니다.

하지만 자바에서는 이 메소드를 짧게 호출할 수 있는 방법이 없어서 항상 object1.compareTo(object2) 의 형태로 명시적으로 사용해야 합니다.

 

코틀린도 똑같은 Comparable 인터페이스를 지원하며, 게다가 코틀린은 Comparable 인터페이스 안에 있는 compareTo 메소드 호출의 convention을 제공하여 비교 연산자 ( < , > , <= , >= ) 는 compareTo 호출로 컴파일이 됩니다.

 

두 객체를 비교하는 식은 compareTo의 결과를 0과 비교하는 코드로 컴파일 됩니다.

 

이와 같이 Comparable 인터페이스를 구현하는 모든 자바 클래스를 코틀린에서는 간결한 연산자 구문으로 비교할 수 있습니다.