author-pic

Ferry S

An ISTJ, Type 5, Engineer, Gamer, and Thriller-Movies-Lover
Java: BigDecimal vs Double
Thu. Dec 15th, 2022 09:05 PM8 mins read
Java: BigDecimal vs Double
Source: flickr @Jesús García - Decimales

Ketika mengembangkan aplikasi, melakukan kalkulasi bilangan desimal terkadang cukup tricky. Apalagi kalau berhubungan dengan duit, seperti pada aplikasi perbankan, e-commerce, dan sejenisnya. Perhitungannya tentu harus akurat sesuai aturan yang diberlakukan oleh perusahaan. Kalau tidak teliti saat develop bisa salah perhitungannya. Salah satu hal yang dipertimbangkan saat develop adalah penggunaan tipe data. Seperti pada Java, ada dua tipe data yang bisa digunakan untuk menampung bilangan desimal dengan ukuran yang besar, yaitu BigDecimal dan Double. Walaupun sama-sama bisa menampung bilangan desimal dengan ukuran besar, keduanya memiliki perbedaan yang cukup signifikan.

Untuk hal ini double lebih unggul. Pada double kita cukup input angka dan tanda decimal saat membuat variable. Sedangkan untuk BigDecimal kita harus membungkus value lewat constructor maupun lewat static creation method.

double d = 0.1;
BigDecimal bigDecimal = BigDecimal.valueOf(0.1);

Tapi perlu diingat, BigDecimal punya beberapa kelemahan saat membuat objek. Ketika menggunakan static method valueOf(double) dengan scale lebih dari 16, maka Java akan melakukan pembulatan pada double. Sehingga value yang dihasilkan juga akan dibulatkan oleh Java. Untuk itu kalau kita membutuhkan value double yang presisi dengan scale lebih dari 16, maka gunakan constructor yang parameternya String. Selain constructor yang parameternya String, juga ada constructor yang parameternya double yang menghasilkan BigDecimal dengan floating point yang sangat presisi.

BigDecimal notPrecisionDecimal = BigDecimal.valueOf(0.1234567890123456789); //will be scaled automatically
BigDecimal precisionDecimal = new BigDecimal("0.1234567890123456789"); //will be precised
BigDecimal unlimitedFloatingDecimal = new BigDecimal(0.1); //with unlimited floating point

Untuk itu, kalau mau menggunakan BigDecimal dengan presisi yang tidak terlalu tinggi dari double value maka gunakan BigDecimal.valueOf(double). Untuk presisi yang tinggi maka gunakan constructor yang menerima String, new BigDecimal(String). Sedangkan constructor yang menerima double, new BigDecimal(double) sebaiknya dihindari kecuali paham fungsinya karena akan menghasilkan floating point.

Saat melakukan kalkulasi, pada Double kita bisa langsung menggunakan operator matematika secara native. Sedangkan untuk BigDecimal kita hanya bisa melakukannya lewat method yang terdapat pada BigDecimal. 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)));

Oh ya, perlu diperhatikan ketika menggunakan BigDecimal, urutan kalkulasinya tidak seperti standar matematika, melainkan dari paling kiri ke kanan, kecuali ada perhitungan 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.

Pada Double kalkulasi yang digunakan menggunakan system floating point yang dihitung secara binary. Contohnya ketika melakukan kalkulasi 0.1 + 0.2 maka hasilnya adalah 0.30000000000000004. Sedangkan secara akuntansi, 0.1 + 0.2 hasilnya adalah 0.3 seharusnya. Kalau kita menggunakannya pada use case yang berhubungan dengan perhitungan uang, tentu sangat tricky. Apalagi pada perbankan, beda sedikit saja, walaupun hanya selisih satu angka dibelakang koma, hasil kalkulasinya tentu sangat berdampak pada bisnis perusahaan. Tapi kalau kita menggunakan BigDecimal, hasilnya adalah 0.3 sesuai dengan yang kita harapkan.

double x = 0.1;
double y = 0.2;
System.out.println("(x + y) = " + (x + y));

BigDecimal a = BigDecimal.valueOf(0.1);
BigDecimal b = BigDecimal.valueOf(0.2);
System.out.println("a.add(b) = " + a.add(b));

Ini mungkin salah satu kekurangan dari BigDecimal. Ketika menggunakan primitive double, comparison bisa menggunakan symbol ==. Ketika menggunakan wrapper Double, comparison dilakukan menggunakan method equals(). Untuk BigDecimal kita tidak bisa menggunakan symbol == karena bukan primitive type, juga tidak 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));

Double tiga = 3;
Double tigaLagi = 3;

System.out.println("result = " + (tiga.equals(tigaLagi)));

BigDecimal satu = BigDecimal.valueOf(1);
BigDecimal satuDecimal = new BigDecimal("1.0");

System.out.println("result = " + satu.equals(satuDecimal));

Code BigDecimal di atas hasilnya adalah false karena akan membandingkan objek "1" dengan "1.0" secara strict meskipun secara akuntansi itu value yang sama. Method equals() itu artinya kita akan 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 bilangan di belakang koma = 0 dengan objek yang memiliki jumlah bilangan di belakang koma = 1, sehingga hasilnya false. Untuk membandingkan value, kita perlu menggunakan method compareTo() == 0 untuk equals, compareTo() < 0 untuk less than, dan compareTo() > 0 untuk greater than ketika membandingkan nilai angka "1" dan "1.0". Contohnya seperti berikut:

BigDecimal satu = BigDecimal.valueOf(1);
BigDecimal satuDecimal = new BigDecimal("1.0");

System.out.println("result = " + satu.compareTo(satuDecimal) == 0);

Dengan begitu hasilnya sudah sesuai dengan yang diharapkan. Sekarang yang dibandingkan hanya valuenya saja.

Scale adalah jumlah angka di belakang koma pada bilangan desimal. Pada BigDecimal kita bisa tentukan scale atau jumlah digit desimalnya sesuai yang kita mau saat digunakan. Ketika melakukan perhitungan, scale pada BigDecimal adalah tak terhingga. Makanya kita disarankan menentukan scale dan rounding pada saat melakukan pembagian untuk menghindari scale tak terhingga yang mengakibatkan error ArithmeticException. Contohnya ketika membagi 10 dibagi 3 yang menghasilkan angka desimal tak terhingga. Sedangkan pada Double kita tidak bisa langsung menentukan scale, kita hanya bisa menetukan jumlah pembulatan ketika dikonversi ke String. Atau dengan convert menjadi BigDecimal lalu set scale dan convert lagi ke Double. Pada saat melakukan pembagian 10 dibagi 3 menggunakan Double tidak akan error, tapi akan otomatis melakukan pembulatan menggunakan system floating point.

double d = 0.123456789;
String formatted = String.format("%.2f", d);
double formattedDouble = Double.valueOf(formatted);

BigDecimal dec = BigDecimal.valueOf(d).setScale(2, RoundingMode.HALF_EVEN);

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)); //no error
System.out.println(tenDec.divide(threeDec)); //throw exception

Dalam beberapa kasus, scaling di tengah-tengah kalkulasi dapat membuat hasil akhir kurang akurat. Scaling biasanya dilakukan di akhir kalkulasi. Untuk itu salah satu alternatifnya adalah menggunakan scale yang cukup tinggi seperti 10 saat kalkulasi di pertengahan. Atau alternatif lainnya menggunakan MathContext untuk pembagian yang cukup presisi dan mencegah error. MathContext adalah objek khusus yang menyimpan konfigurasi rounding dan precision yang umum dipakai saat kalkulasi. Precision adalah jumlah digit angka bilangan asli beserta jumlah angka di belakang koma. Misalkan angka 123.4567 berarti bilangan itu precision-nya adalah 7 dan scale-nya adalah 4. MathContext secara umum terdiri dari 3, yaitu MathContext.DECIMAL32, MathContext.DECIMAL64, MathContext.DECIMAL128. Semakin tinggi semakin presisi. Atau bisa juga bikin custom konfigurasi menggunakan new MathContext(int, RoundingMode). MathContext juga kadang diperlukan saat melakukan perkalian agar hasil perkaliannya tidak terlalu presisi. Semuanya kembali lagi sesuai kebutuhan bisnis.

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));

Ini adalah alasan kuat BigDecimal lebih unggul daripada double. By default, RoundingMode yang digunakan pada Double adalah mengikuti system floating point atau Half Even saat menggunakan NumberFormat. Kita hanya bisa mengubahnya lewat 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 menentukan scale kita juga disarankan menentukan RoundingMode yang diinginkan pada parameter untuk mencegah ArithmeticException. RoundingMode sangat penting pada bisnis perusahaan yang memiliki rules tertentu saat melakukan pembulatan. Misalnya saat melakukan kalkulasi diskon, perusahaan tersebut memiliki kebijakan melakukan pembulatan dengan RoundingMode tertentu.

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 yang bisa digunakan:

Secara gampangnya, pembulatannya selalu ke arah yang lebih positif. Contohnya bilangan 0.563 dengan scale 2 angka di belakang koma menggunakan RoundingMode Ceiling, maka hasilnya adalah 0.57. Sedangkan untuk bilangan negatif, -0.563, maka hasilnya adalah -0.56 karena -0.56 lebih positif daripada -0.57.

Ini adalah kebalikan dari RoundingMode Ceiling, kalau RoundingMode Floor pembulatannya selalu ke arah yang lebih negatif. Contohnya bilangan 0.567 dengan scale 2 angka di belakang koma menggunakan RoundingMode Floor, maka hasilnya adalah 0.56. Sedangkan untuk bilangan negatif, -0.567, maka hasilnya adalah -0.57 karena -0.57 lebih negatif daripada -0.56.

RoundingMode Up adalah pembulatannya menjauhi 0. Contohnya bilangan 0.563 dengan scale 2 angka di belakang koma menggunakan RoundingMode Up, maka hasilnya adalah 0.57. Begitu juga dengan bilangan negatif, -0.563, maka hasilnya adalah -0.57 karena -0.57 lebih jauh dari angka 0 dibanding -0.56.

RoundingMode Down kebalikannya RoundingMode Up, yaitu pembulatannya mendekati 0. Contohnya bilangan 0.567 dengan scale 2 angka di belakang koma menggunakan RoundingMode Down, maka hasilnya adalah 0.56. Begitu juga dengan bilangan negatif, -0.567, maka hasilnya adalah -0.56 karena -0.56 lebih mendekati 0 daripada -0.57.

Pada RoundingMode Up, pembulatannya selalu menjauhi 0 berapapun bilangan terakhirnya. Sedangkan untuk RoundingMode Half Up, jika angka pembulatan terakhirnya rentang 1-4 maka pembulatannya mendekati 0. Untuk angka pembulatan terakhirnya rentang 6-9 maka pembulatannya menjauhi 0. Sedangkan untuk angka pembulatan terakhirnya 5, maka pembulatannya menjauhi 0. Contohnya pada tabel berikut menggunakan scale 2 digit:

Number Rounding Result
0.567 0.57
0.563 0.56
0.565 0.57
-0.565 -0.57
-0.563 -0.56
-0.567 -0.57

Untuk RoundingMode Half Down, jika angka pembulatan terakhirnya rentang 1-4 maka pembulatannya mendekati 0. Untuk angka pembulatan terakhirnya rentang 6-9 maka pembulatannya menjauhi 0. Sedangkan untuk angka pembulatan terakhirnya 5, maka pembulatannya mendekati 0. Contohnya pada tabel berikut menggunakan scale 2 digit:

Number Rounding Result
0.567 0.57
0.563 0.56
0.565 0.56
-0.565 -0.56
-0.563 -0.56
-0.567 -0.57

Ini juga sama seperti kedua RoundingMode Half sebelumnya, untuk angka pembulatan terakhirnya rentang 1-4 pembulatannya mendekati 0 dan untuk rentang 6-9 pembulatannya menjauhi 0. Yang membedakan adalah ketika angka pembulatan terakhirnya 5, maka pembulatannya menjadi angka genap. Contohnya 0.565, maka pembulatannya digenapkan ke bawah menjadi 0.56 karena itu adalah bilangan genap. Sedangkan untuk bilangan 0.575, maka pembulatannya digenapkan ke atas menjadi 0.58. Ini adalah RoundingMode yang umum digunakan perbankan untuk mengolah data keuangan.

Number Rounding Result
0.567 0.57
0.563 0.56
0.565 0.56
0.575 0.58
-0.575 -0.58
-0.565 -0.56
-0.563 -0.56
-0.567 -0.57

Unnecessary artinya tidak ada pembulatan sama sekali. Ini harus dihindari saat melakukan pembagian karena dapat menghasilkan error ArithmeticException. Ini adalah default rounding dari BigDecimal, makanya ketika melakukan pembagian menggunakan BigDecimal kita disarankan menentukan scale dan rounding selain Unnecessary pada method divide untuk menghindari hasil yang tidak diinginkan.

Itulah beberapa perbedaan BigDecimal dengan Double. BigDecimal memiliki fitur yang lebih canggih dibanding Double karena kita bisa menentukan scale dan rounding yang diinginkan sesuai kebutuhan bisnis. Ketika 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 khusus 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 akuntansi seperti hal-hal yang berkaitan dengan keuangan. Sedangkan Double lebih cocok untuk hal-hal umum lainnya yang ga perlu angka desimal dengan aturan yang spesifik seperti menghitung berat atau ukuran volume.