Kotlin

Kotlinの演算子オーバーロードでBigDecimalを直感的に使おうとした話


はじめに

Kotlinには演算子オーバーロードという演算子をクラス独自に再定義できる機能があります。

公式リファレンス

この機能を使えばJavaで個人的に非常に見にくいと思っていたBigDecimalでの計算が直感的に出来るようになるのでは!?と思い色々試行錯誤した際の奮闘のまとめメモです。


先述するとKotlinのBigDecimalでは最初から算術演算子が使えます


演算子のオーバーロード使い方

operator修飾子を付けてそれぞれの演算子に合わせて命名したメソッドを実装していくだけです。

data class Num(val n: Int)

operator fun Num.plus(other: Num) = Num(n + other.n)

fun main(args: Array<String>) {
val num1 = Num(3)
val num2 = Num(7)
println(num1 + num2) // => Num(n=10)
}

上記の例は「+」演算子をオーバーロードした例です。

「+」演算子をオーバーロードする際はoperator修飾子を付けてメソッド名をplusにする。以上です。

その他の主な演算子の対応は以下のような感じになっています。  

下記以外にもありますが使う頻度が高そうなものを抜粋。


算術演算子

演算子
命名

+
plus

-
minus

*
times

/
div

%
rem

++
inc

--
dec


代入演算子

演算子
命名

+=
plusAssign

-=
minusAssign

*=
timesAssign

/=
divAssign

%=
remAssign


比較演算子

演算子
命名

==
equals

>, <
compareTo

in
contains

..
rangeTo


お金クラスとか作れそう

これを使えば個人的に取っつきにくかったBigDecimalの面倒くさい計算処理が演算子で直感的に使えるのでは!?


業務アプリではお金の計算はBigDecimalでとよく聞くので(実際にやったことはない)演算子オーバーロードを使ってお金クラスとかを作ってみようとしたら既にKotlinでは演算子で計算できた。

val money1 = BigDecimal(100)

val money2 = BigDecimal(300)
println(money1 + money2) // => 400

実際に見てみるとこのような形で定義されていました(一部抜粋)

/**

* Enables the use of the `+` operator for [BigDecimal] instances.
*/
@kotlin.internal.InlineOnly
public inline operator fun BigDecimal.plus(other: BigDecimal): BigDecimal = this.add(other)

/**
* Enables the use of the `-` operator for [BigDecimal] instances.
*/
@kotlin.internal.InlineOnly
public inline operator fun BigDecimal.minus(other: BigDecimal): BigDecimal = this.subtract(other)

/**
* Enables the use of the `*` operator for [BigDecimal] instances.
*/
@kotlin.internal.InlineOnly
public inline operator fun BigDecimal.times(other: BigDecimal): BigDecimal = this.multiply(other)

/**
* Enables the use of the `/` operator for [BigDecimal] instances.
*
* The scale of the result is the same as the scale of `this` (divident), and for rounding the [RoundingMode.HALF_EVEN]
* rounding mode is used.
*/
@kotlin.internal.InlineOnly
public inline operator fun BigDecimal.div(other: BigDecimal): BigDecimal = this.divide(other, RoundingMode.HALF_EVEN)

代入演算子などの処理は実装されていませんでした。

また気になる点としては除算時の丸めの設定がRoundingMode.HALF_EVENで固定されてしまうところ辺りでしょうか?


この辺りは仕様に合わせて実装してあげたい気持ちもなくはなさそうなので練習兼ねて↓のようなものを実装してみました。


実装したもの

data class Money(val value: BigDecimal){

constructor(int: Int): this(BigDecimal("${int}"))
constructor(long: Long): this(BigDecimal("${long}"))
constructor(double: Double): this(BigDecimal("${double}"))
constructor(float: Float): this(BigDecimal("${float}"))

private val ROUNDING_ERROR_SCALE = 2

/**
* 算術演算子
*/
// +
operator fun plus(other: Money)= Money(value.add(other.value))
// -
operator fun minus(other: Money) = Money(value.subtract(other.value))
// *
operator fun times(other: Money) = Money(value.multiply(other.value))
// /
operator fun div(other: Money) = Money(value.divide(other.value, ROUNDING_ERROR_SCALE, BigDecimal.ROUND_HALF_UP))

/**
* 比較演算子
*/
// >, >=, <, <=
operator fun compareTo(other: Money) = value.compareTo(other.value)

}


 さいごに

仕様に合わせてBigDecimalを制限して値オブジェクトを使っていきたい時等に、演算子オーバーロードを上手く使えばより直感的な値オブジェクトやクラスを作れそうなのでうまく使っていけるようになりたいです。