Ketika mengembangkan aplikasi, melakukan kalkulasi bilangan desimal terkadang cukup tricky. Apalagi kalau berhubungan dengan duit💸, seperti pada aplikasi perbankan, e-commerce, dan sejenisnya. Perhitungannya harus sesuai aturan yang berlaku. Kalau ga teliti saat develop bisa salah perhitungannya. Salah satu hal yang dipertimbangkan saat develop adalah penggunaan tipe data. Pada Java, ada dua tipe data yang bisa digunakan untuk menampung bilangan desimal ukuran besar, yaitu BigDecimal dan double. Walaupun sama-sama bisa menampung bilangan desimal ukuran besar, keduanya memiliki perbedaan yang cukup signifikan.
Initialization
Untuk hal ini double lebih unggul. Pada double kita cukup input angka dan tanda decimal menggunakan .
atau suffix D
saat membuat variable. Sedangkan untuk BigDecimal kita harus membungkus value lewat constructor ataupun lewat static creation method.
double d = 0.1;
double dd = 12345678901D;
BigDecimal bigDecimal = BigDecimal.valueOf(0.1);
BigDecimal punya beberapa kelemahan saat membuat objek. Ketika menggunakan static method valueOf(double)
dengan angka desimal yang panjang, ada kemungkinan terjadi pembulatan pada double karena floating point sehingga value BigDecimal yang dihasilkan juga akan dibulatkan. Alternatifnya, gunakan constructor yang parameternya String biar ga ada floating point. Selain itu, juga ada constructor yang parameternya double yang menghasilkan BigDecimal dengan floating point dan presisi tak terhingga.
BigDecimal notPrecisionDecimal = BigDecimal.valueOf(0.1234567890123456789); //will be scaled automatically
BigDecimal precisionDecimal = new BigDecimal("0.1234567890123456789"); //will be precised as String value
BigDecimal unlimitedFloatingDecimal = new BigDecimal(0.1); //with floating point🥴
Untuk itu, kalau mau menggunakan BigDecimal dengan presisi yang ga terlalu tinggi dari double maka gunakan BigDecimal.valueOf(double)
. Untuk presisi yang tinggi tanpa floating point maka gunakan constructor yang menerima String, new BigDecimal(String)
. Sedangkan constructor yang menerima double, new BigDecimal(double)
sebaiknya dihindari karena ada floating point dengan presisi tak terhingga.
Precision Limit
Untuk hal ini BigDecimal lebih unggul. Kelemahan dari double adalah value minimal & maksimalnya antara -1.7976931348623157E+308 hingga 1.7976931348623157E+308 dan presisinya sekitar 15-17 digit. Presisi adalah jumlah digit angka bilangan asli beserta jumlah angka di belakang koma yang dapat diproses tanpa kehilangan detail angka tersebut. Misalkan angka 123.4568 berarti angka itu presisinya adalah 7. Meskipun saat initialization double kita isi dengan angka desimal yang panjang, saat diproses value itu akan dibulatkan pakai system floating point.
double ddd = 0.1234567890123456789012;
double ddd2 = 1234567890123456789012D;
System.out.println("ddd = " + ddd); //result 0.1234567890123457
System.out.println("ddd2 = " + ddd2); //result 1.2345678901234568E21
System.out.println(1234567890123456800000D == 1234567890123456789012D); //result true🫣
BigDecimal bigDecimal = new BigDecimal("0.1234567890123456789012");
BigDecimal bigDecimal2 = new BigDecimal("1234567890123456789012");
System.out.println("bigDecimal = " + bigDecimal); //result 0.1234567890123456789012
System.out.println("bigDecimal2 = " + bigDecimal2); //result 1234567890123456789012
Seperti pada angka 0.1234567890123456789012 angkanya akan dibulatkan jadi presisi 17 digit angka saja. Angka 1234567890123456789012 juga akan dibulatkan jadi presisi 17 digit berbasis exponent, yaitu 1.2345678901234568E21. Angka tersebut sama dengan 1234567890123456800000. Detail angka dari digit ke-18 ga kehitung😱. Sedangkan BigDecimal ga ada batas apapun😎. Makanya ga dianjurkan menggunakan double pada kasus yang berhubungan dengan keuangan karena jika jumlah uang yang dikelola digitnya udah banyak maka akan dibulatkan otomatis oleh mesin pakai floating point sehingga hasil perhitungannya ga akurat dan bisa menimbulkan kerugian😭.
Native Operation
Saat melakukan kalkulasi, pada double kita bisa langsung menggunakan operator matematika secara native. Sedangkan pada BigDecimal kita hanya bisa melakukannya lewat method. Perlu diperhatikan, saat menggunakan BigDecimal urutan kalkulasinya dari paling kiri ke kanan. Ga ada prioritas pada perkalian/pembagian kecuali dilakukan di dalam tanda kurung. Sedangkan pada double, urutan kalkulasinya sesuai standar matematika, yaitu dari kiri ke kanan dengan prioritas perkalian/pembagian terlebih dahulu, baru setelah itu penjumlahan/pengurangan, kecuali ada perhitungan di dalam tanda kurung. Contohnya seperti berikut:
double dd = 0.5 + 0.3 - 0.1 * 4 / 2;
BigDecimal bigDecimal1 = BigDecimal.valueOf(0.5)
.add(BigDecimal.valueOf(0.3))
.subtract(BigDecimal.valueOf(0.1)
.multiply(BigDecimal.valueOf(4)
.divide(BigDecimal.valueOf(2), RoundingMode.HALF_EVEN))); //result 0.6
BigDecimal divide = BigDecimal.valueOf(0.5)
.add(BigDecimal.valueOf(0.3))
.subtract(BigDecimal.valueOf(0.1))
.multiply(BigDecimal.valueOf(4))
.divide(BigDecimal.valueOf(2), RoundingMode.HALF_EVEN); //result 1.4
Calculation Result
Pada double kalkulasinya menggunakan system floating point yang dihitung secara binary. Contohnya saat kalkulasi 0.1 + 0.2 maka hasilnya 0.30000000000000004. Sedangkan secara akuntansi maupun scientific, 0.1 + 0.2 = 0.3 seharusnya. Kalau menggunakan double pada use case yang berhubungan dengan uang, tentu sangat tricky. Apalagi pada perbankan, beda dikit aja dibelakang koma hasil kalkulasinya akan sangat berdampak pada bisnis. Tapi kalau kita menggunakan BigDecimal, hasilnya adalah 0.3 sesuai harapan.
double x = 0.1;
double y = 0.2;
System.out.println("(x + y) = " + (x + y)); //result 0.30000000000000004🤔
BigDecimal a = BigDecimal.valueOf(0.1);
BigDecimal b = BigDecimal.valueOf(0.2);
System.out.println("a.add(b) = " + a.add(b)); //result 0.3
Comparison
Ini mungkin salah satu kekurangan dari BigDecimal yang agak tricky. Saat menggunakan primitive double, comparison menggunakan symbol ==
. Kalau menggunakan wrapper Double, comparison menggunakan method equals()
. Untuk BigDecimal kita ga bisa menggunakan symbol ==
karena bukan primitive type, juga ga bisa menggunakan method equals()
karena akan membandingkan dua state objek secara strict. Seperti contoh berikut:
double dua = 2;
double duaLagi = 2;
System.out.println("result = " + (dua == duaLagi)); //true
Double tiga = 3;
Double tigaLagi = 3;
System.out.println("result = " + (tiga.equals(tigaLagi))); //true
BigDecimal satu = BigDecimal.valueOf(1);
BigDecimal satuDecimal = new BigDecimal("1.0");
System.out.println("result = " + satu.equals(satuDecimal)); //false🤦
Code BigDecimal di atas hasilnya adalah false karena akan membandingkan objek 1
dengan 1.0
secara strict meskipun itu harusnya sama aja. Method equals()
itu membandingkan state objek, dimana BigDecimal itu behind the scene menyimpan properti jumlah bilangan di belakang koma. Dalam hal ini BigDecimal akan membandingkan objek yang memiliki jumlah angka di belakang koma = 0 dengan objek yang memiliki jumlah angka di belakang koma = 1, makanya hasilnya false. Untuk membandingkan value gunakan method compareTo() == 0
untuk equals, compareTo() < 0
untuk less than, dan compareTo() > 0
untuk greater than. Contohnya seperti berikut:
BigDecimal satu = BigDecimal.valueOf(1);
BigDecimal satuDecimal = new BigDecimal("1.0");
System.out.println("result = " + satu.compareTo(satuDecimal) == 0); //true👏
Scale
Scale adalah jumlah angka di belakang koma pada bilangan desimal. Standar scale perhitungan keuangan di Indonesia itu biasanya 2 angka di belakang koma. Pada BigDecimal kita bisa set scale sesuai selera. Sedangkan pada double kita tidak bisa langsung set scale, tapi harus dikonversi ke String lewat method format()
atau lewat objek utility NumberFormat. Saat melakukan perhitungan, scale pada BigDecimal adalah tak terhingga. Makanya kita disarankan set scale dan rounding pada saat pembagian untuk menghindari scale tak terhingga yang mengakibatkan ArithmeticException. Contohnya 10 dibagi 3 yang menghasilkan angka desimal tak terhingga. Ini akan error jika dilakukan menggunakan BigDecimal tanpa scale & rounding. Tapi kalau menggunakan double ga akan error, karena double akan otomatis scaling berkat system floating point.
double d = 0.123456789;
String formatted = String.format("%.2f", d);
double formattedDouble = Double.valueOf(formatted);
double ten = 10;
double three = 3;
System.out.println(ten / three); //result 3.3333333333333335
BigDecimal tenDec = BigDecimal.valueOf(10);
BigDecimal threeDec = BigDecimal.valueOf(3);
System.out.println(tenDec.divide(threeDec, 2, RoundingMode.UP)); //result 3.34 and no error
System.out.println(tenDec.divide(threeDec)); //throw exception🤯
Perlu diingat, scaling di tengah-tengah kalkulasi dapat membuat hasil akhir kurang akurat. Scaling biasanya dilakukan di akhir kalkulasi, kecuali ada requirement khusus secara bisnis. Untuk itu salah satu alternatifnya adalah menggunakan scale yang cukup tinggi seperti 10 saat pembagian di pertengahan, atau menggunakan MathContext. MathContext adalah objek yang menyimpan konfigurasi rounding dan precision yang umum dipakai saat kalkulasi. Secara umum kita bisa menggunakan 3 constant, yaitu MathContext.DECIMAL32
, MathContext.DECIMAL64
, dan MathContext.DECIMAL128
. Semakin tinggi semakin presisi. Atau bisa juga bikin custom menggunakan new MathContext(int, RoundingMode)
.
BigDecimal tenDec = BigDecimal.valueOf(10);
BigDecimal threeDec = BigDecimal.valueOf(3);
System.out.println(tenDec.divide(threeDec, 10, RoundingMode.HALF_EVEN));
System.out.println(tenDec.divide(threeDec, MathContext.DECIMAL64));
System.out.println(tenDec.divide(threeDec, new MathContext(20, RoundingMode.UP)));
System.out.println(BigDecimal.valueOf(0.12345).multiply(BigDecimal.valueOf(0.54321)));
System.out.println(BigDecimal.valueOf(0.12345).multiply(BigDecimal.valueOf(0.54321), MathContext.DECIMAL32));
Best practice yang gw tahu, kalau ga ada requirement bisnis yang mengharuskan scale di tengah-tengah kalkulasi saat pembagian, maka gunakan scale dengan angka yang tinggi atau gunakan MathContext agar lebih akurat dan mencegah error. Lalu pada hasil akhir barulah set scale sesuai kebutuhan bisnis, misalnya menjadi 2 angka di belakang koma.
BigDecimal discount1 = BigDecimal.valueOf(10)
.divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_EVEN)
.multiply(BigDecimal.valueOf(100)); //result 333.00❌
BigDecimal dicsount2 = BigDecimal.valueOf(10)
.divide(BigDecimal.valueOf(3), MathContext.DECIMAL128)
.multiply(BigDecimal.valueOf(100))
.setScale(2, RoundingMode.HALF_EVEN); //result 333.33✅
Rounding
By default, RoundingMode yang digunakan pada double mengikuti system floating point atau Half Even saat menggunakan NumberFormat. Kita hanya bisa mengubahnya lewat bantuan objek NumberFormat dalam bentuk String. Sedangkan pada BigDecimal by default RoundingMode-nya adalah Unnecessary, tapi kita bisa mengubah RoundingMode yang diinginkan secara langsung pada objeknya. Makanya saat melakukan pembagian, selain set scale kita juga wajib set RoundingMode yang diinginkan pada parameter untuk mencegah ArithmeticException. RoundingMode sangat penting pada bisnis yang memiliki rules tertentu saat melakukan pembulatan. Misalnya saat kalkulasi diskon, perusahaan kadang memiliki kebijakan tertentu saat pembulatan.
double d = 0.123456789;
NumberFormat numberInstance = NumberFormat.getNumberInstance();
numberInstance.setMaximumFractionDigits(6);
numberInstance.setRoundingMode(RoundingMode.UP);
String formatted = numberInstance.format(d);
BigDecimal dec = BigDecimal.valueOf(d).setScale(6, RoundingMode.UP);
Terdapat 6 jenis RoundingMode:
RoundingMode Ceiling
Secara gampangnya, pembulatannya selalu ke arah yang lebih positif. Contohnya 0.163 dengan scale 2 angka di belakang koma dibulatkan menggunakan RoundingMode Ceiling. Angka desimalnya akan dipotong jadi 2 digit dan digit ke-2 akan dibulatkan ke arah yang lebih positif, sehingga hasilnya 0.17. Sedangkan untuk bilangan negatif, -0.163 dibulatkan hasilnya -0.16 karena -0.16 lebih positif daripada -0.17.
RoundingMode Floor
Ini adalah kebalikan dari Ceiling, kalau Floor pembulatannya selalu ke arah yang lebih negatif. Contohnya 0.167 dengan scale 2 angka di belakang koma dibulatkan menggunakan RoundingMode Floor. Angka desimalnya akan dipotong jadi 2 digit dan digit ke-2 akan dibulatkan ke arah yang lebih negatif, sehingga hasilnya 0.16. Sedangkan untuk bilangan negatif, -0.167 dibulatkan hasilnya -0.17 karena -0.17 lebih negatif daripada -0.16.
RoundingMode Up
RoundingMode Up adalah pembulatannya selalu menjauhi 0. Contohnya 0.163 dengan scale 2 angka di belakang koma dibulatkan menggunakan RoundingMode Up. Angka desimalnya akan dipotong jadi 2 digit dan digit ke-2 akan dibulatkan menjauhi 0, sehingga hasilnya 0.17. Begitu juga dengan bilangan negatif, -0.163 dibulatkan hasilnya -0.17 karena -0.17 lebih jauh dari angka 0 dibanding -0.16.
RoundingMode Down
RoundingMode Down kebalikannya RoundingMode Up, yaitu pembulatannya selalu mendekati 0. Contohnya 0.167 dengan scale 2 angka di belakang koma dibulatkan menggunakan RoundingMode Down. Angka desimalnya akan dipotong jadi 2 digit dan digit ke-2 akan dibulatkan mendekati 0, sehingga hasilnya 0.16. Begitu juga dengan bilangan negatif, -0.167, hasilnya -0.16 karena -0.16 lebih mendekati 0 daripada -0.17.
RoundingMode Half Up
Sedangkan untuk RoundingMode Half Up, jika angka desimal setelah digit pembulatan kurang dari 0.5 maka pembulatannya mendekati 0. Jika angka desimal setelah digit pembulatan lebih besar dari 0.5 maka pembulatannya menjauhi 0. Lalu jika angka desimal setelah digit pembulatan adalah 0.5 maka pembulatannya menjauhi 0. Misalnya 0.165 akan dibulatkan menggunakan Half Up dengan scale 2 angka di belakang koma. Maka kita perlu ambil angka desimal setelah digit desimal ke-2, yaitu 0.5. Sesuai rules Half Up, maka hasilnya dibulatkan ke atas menjadi 0.17. Contoh lainnya pada tabel berikut:
Number | Rounding Result |
---|---|
0.167 | 0.17 |
0.163 | 0.16 |
0.165 | 0.17 |
0.16521 | 0.17 |
-0.16521 | -0.17 |
-0.165 | -0.17 |
-0.163 | -0.16 |
-0.167 | -0.17 |
RoundingMode Half Down
Untuk RoundingMode Half Down, jika angka desimal setelah digit pembulatan kurang dari 0.5 maka pembulatannya mendekati 0. Jika angka desimal setelah digit pembulatan lebih besar dari 0.5 maka pembulatannya menjauhi 0. Sedangkan jika angka desimal setelah digit pembulatan adalah 0.5 maka pembulatannya mendekati 0. Misalnya 0.165 akan kita rounding menggunakan Half Down dengan scale 2 angka di belakang koma. Maka kita perlu ambil angka desimal setelah digit desimal ke-2, yaitu 0.5. Sesuai rules Half Down, maka hasilnya dibulatkan ke bawah menjadi 0.16. Lalu misalkan 0.16521 akan kita rounding menggunakan Half Down dengan scale 2 angka di belakang koma. Maka kita perlu ambil angka desimal setelah digit desimal ke-2, yaitu 0.521. Karena 0.521 lebih besar dari 0.5, maka 0.16521 dibulatkan ke atas jadi 0.17, bukan 0.16. Ini yang sering bikin orang salah sangka😅. Contoh lengkapnya pada tabel berikut:
Number | Rounding Result |
---|---|
0.167 | 0.17 |
0.163 | 0.16 |
0.165 | 0.16 |
0.16521 | 0.17 |
-0.16521 | -0.17 |
-0.165 | -0.16 |
-0.163 | -0.16 |
-0.167 | -0.17 |
RoundingMode Half Even
Half Even adalah RoundingMode yang umum digunakan perbankan untuk mengelola data keuangan. Ini mirip seperti kedua RoundingMode Half sebelumnya. Untuk angka desimal setelah digit pembulatan kurang dari 0.5 maka pembulatannya mendekati 0, kalau lebih besar dari 0.5 maka pembulatannya menjauhi 0. Yang membedakannya ketika angka desimal setelah digit pembulatan adalah 0.5 maka pembulatannya menggunakan angka genap. Contohnya 0.165 akan dibulatkan menggunakan Half Even dengan scale 2 angka di belakang koma. Maka kita ambil angka desimal setelah digit desimal ke-2, yaitu 0.5. Sesuai rules Half Even, maka hasilnya digenapkan ke bawah menjadi 0.16 karena 16 adalah bilangan genap. Sedangkan untuk angka 0.175, saat dibulatkan dengan scale 2 angka di belakang koma menggunakan Half Even maka pembulatannya digenapkan ke atas menjadi 0.18 karena 18 adalah bilangan genap. Misalkan 0.16521 akan kita bulatkan dengan scale 2 angka di belakang koma menggunakan Half Even. Maka kita ambil angka desimal setelah digit desimal ke-2, yaitu 0.521. Karena 0.521 lebih besar dari 0.5, maka sesuai rules dibulatkan ke atas jadi 0.17.
Number | Rounding Result |
---|---|
0.167 | 0.17 |
0.163 | 0.16 |
0.165 | 0.16 |
0.175 | 0.18 |
0.16521 | 0.17 |
-0.16521 | -0.17 |
-0.175 | -0.18 |
-0.165 | -0.16 |
-0.163 | -0.16 |
-0.167 | -0.17 |
RoundingMode Unnecessary
Unnecessary artinya tidak ada pembulatan sama sekali. Ini harus dihindari karena dapat mengakibatkan ArithmeticException. Ini adalah default rounding dari BigDecimal, makanya saat melakukan pembagian menggunakan BigDecimal kita diwajibkan set scale dan rounding selain Unnecessary pada method divide
untuk menghindari error.
Verdict
Itulah beberapa perbedaan BigDecimal dengan double. Berdasarkan performa tentu saja double lebih unggul dibanding BigDecimal karena double didukung secara native oleh mesin, sedangkan BigDecimal itu objek. Di lain sisi, BigDecimal memiliki fitur yang lebih canggih dibanding double. BigDecimal bisa memiliki presisi tanpa batas dan kita bisa mengatur scale dan rounding yang diinginkan dengan mudah. Saat melakukan pembagian menggunakan BigDecimal, kita wajib menentukan RoundingMode dan scale untuk menghindari ArithmeticException karena default RoundingMode-nya adalah Unnecessary dan default scale-nya adalah tak terhingga. Juga perlu diperhatikan, urutan kalkulasi pada BigDecimal adalah dari yang paling kiri atau sesuai tanda kurung, bukan mengikuti standar matematika seperti double. Secara penggunaan, BigDecimal lebih kompleks daripada double karena merupakan objek pada Java, kita harus membungkus setiap angka yang ingin dihitung menjadi BigDecimal dan menggunakan method yang ada di dalamnya untuk melakukan kalkulasi. Berbeda dengan double yang lebih gampang digunakan menggunakan operator matematika biasa. Secara penggunaan, BigDecimal sangat cocok untuk perhitungan yang berkaitan dengan keuangan ataupun kalkulasi scientific. Sedangkan double lebih cocok untuk hal-hal umum lainnya yang ga perlu angka desimal presisi dengan aturan spesifik seperti menyimpan ukuran volume. Apalagi untuk menyimpan data berat badan, maka float lebih cocok karena ukurannya lebih kecil.