Setiap aplikasi keuangan dimulai dari satu pertanyaan: bagaimana peristiwa ekonomi — gaji masuk, bensin terisi, sewa terbayar — dinyatakan sebagai data? Pada Bagian 1 kita meletakkan fondasi aplikasi keuangan pribadi: model Transaksi berbentuk record, pembeda arah uang JenisTransaksi berbentuk enum, dan wadah catatan BukuKas milik Budi. Seluruh nominal memakai BigDecimal karena tipe tersebut menyimpan angka desimal sebagai digit, bukan pendekatan biner, sehingga jumlah uang tidak pernah bergeser sedikit pun. Di akhir bagian, pencatat menampilkan buku kas Januari 2026 beserta total dan saldo, sambil menolak transaksi cacat secara otomatis.
Kenapa Nominal Uang Selalu BigDecimal #
Tipe double mengikuti standar IEEE 754 dan menyimpan angka dalam basis dua. Pecahan desimal 0.1 tidak punya representasi biner yang berakhir, sama seperti 1/3 yang menjadi 0.3333… di basis sepuluh, sehingga penjumlahan sederhana pun menyisakan galat pembulatan. Dua baris pertama program demo menunjukkan persoalan tersebut sekali sebelum kita meninggalkannya untuk selamanya: jalur double menghasilkan 0.30000000000000004, sedangkan jalur BigDecimal menghasilkan 0.3 persis.
BigDecimal menyimpan nilai sebagai digit desimal beserta skala (jumlah digit pecahan), sehingga "8500000.00" tersimpan apa adanya. Seluruh operasi berbentuk method yang mengembalikan objek baru — add, subtract, multiply — karena BigDecimal bersifat immutable. Dengan disiplin skala 2 pada seluruh data, penjumlahan 8,500,000.00 dan 450,000.00 selalu berakhir .00 tanpa digit aneh di ekor.
Model Transaksi dengan Record dan Enum #
Arah uang hanya punya dua kemungkinan, jadi enum JenisTransaksi mengunci pilihan tersebut menjadi konstanta:
/** Jenis arus kas: uang masuk atau uang keluar. */
public enum JenisTransaksi {
PEMASUKAN,
PENGELUARAN
}Salah ketik semacam PEMASUKANNN gagal saat kompilasi, dan pembanding t.jenis() == JenisTransaksi.PEMASUKAN membandingkan referensi konstanta sehingga cepat. Kelas Transaksi merangkai lima field menjadi satu nilai data yang tak terpisahkan:
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* Satu baris catatan keuangan: tanggal, keterangan, jenis, kategori, dan nominal.
* Nominal selalu BigDecimal agar bebas galat pembulatan biner.
*/
public record Transaksi(LocalDate tanggal, String keterangan, JenisTransaksi jenis,
String kategori, BigDecimal jumlah) {
// Compact constructor: validasi berjalan otomatis pada setiap pembuatan record.
public Transaksi {
if (tanggal == null || keterangan == null || jenis == null
|| kategori == null || jumlah == null) {
throw new IllegalArgumentException("Seluruh field transaksi wajib terisi");
}
if (keterangan.isBlank()) {
throw new IllegalArgumentException("Keterangan tidak boleh kosong");
}
if (kategori.isBlank()) {
throw new IllegalArgumentException("Kategori tidak boleh kosong");
}
if (jumlah.signum() <= 0) {
throw new IllegalArgumentException("Jumlah harus positif, diterima: "
+ jumlah.toPlainString());
}
}
}Mari kita bedah bagian-bagian pentingnya:
- Daftar komponen (
record Transaksi(...)): lima field dideklarasikan tanpa kata kunciprivate finalkarena compiler javac menuliskannya otomatis beserta accessortanggal(),keterangan(),jenis(),kategori(), danjumlah(). - Compact constructor (
public Transaksi { ... }): constructor tanpa daftar parameter ini disisipkan compiler sebelum penugasan field, sehingga seluruh jalur pembuatanTransaksi— dari kode kita maupun dari deserialisasi nanti — melewati validasi yang sama. - Pemeriksaan null: satu kondisi panjang menjaring lima field sekaligus dengan pesan seragam.
jumlah.signum() <= 0: nominal nol atau negatif ditolak karena catatan keuangan hanya memuat perpindahan uang sungguhan.jumlah.toPlainString(): pesan galat mencetak nominal tanpa notasi ilmiah, sehingga-100.00terbaca manusia, bukan-1E+2.
Record juga menghadirkan equals, hashCode, dan toString tanpa kita tulis — kelak pembandingan data asli dengan data hasil muat ulang berkas pada Bagian 4 cukup memakai equals daftar.
Merakit Pencatat dalam BukuKas #
Model saja tidak cukup; kita butuh wadah yang mengelola kumpulan transaksi sekaligus menyembunyikan detail penyimpanannya:
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** Kumpulan transaksi milik satu pemilik, lengkap dengan total per jenis dan saldo. */
public class BukuKas {
private final List<Transaksi> daftar = new ArrayList<>();
/** Menambah satu transaksi; null ditolak sebelum sempat masuk daftar. */
public void tambah(Transaksi t) {
if (t == null) {
throw new IllegalArgumentException("Transaksi tidak boleh null");
}
daftar.add(t);
}
/** Daftar transaksi baca-saja agar pemanggil tidak dapat mengubah catatan dari luar. */
public List<Transaksi> semua() {
return Collections.unmodifiableList(daftar);
}
public int jumlahTransaksi() {
return daftar.size();
}
/** Menjumlahkan nominal seluruh transaksi satu jenis dengan loop eksplisit. */
public BigDecimal total(JenisTransaksi jenis) {
BigDecimal total = BigDecimal.ZERO;
for (Transaksi t : daftar) {
if (t.jenis() == jenis) {
total = total.add(t.jumlah());
}
}
return total;
}
/** Saldo = total pemasukan dikurangi total pengeluaran. */
public BigDecimal saldo() {
return total(JenisTransaksi.PEMASUKAN)
.subtract(total(JenisTransaksi.PENGELUARAN));
}
}Tiga keputusan desain layak dicermati. Pertama, daftar bermodifier private final, sehingga kode luar tidak memegang ArrayList mentah. Kedua, semua() membungkus daftar dengan Collections.unmodifiableList; upaya add dari tampilan tersebut melempar UnsupportedOperationException. Ketiga, total dihitung ulang saat dipanggil, bukan disimpan sebagai field — tidak ada dua sumber kebenaran yang bisa saling bertentangan.
Menjalankan Pencatatan Januari #
Berikut pintu masuk aplikasi untuk Bagian 1. Data contoh memakai tanggal beku LocalDate.of(2026, 1, ...) agar keluaran selalu identik setiap dijalankan:
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Locale;
/** Bagian 1: mencatat transaksi Januari 2026 milik Budi. */
public class DemoKeuangan {
public static void main(String[] args) {
// Bukti singkat kenapa nominal uang tidak boleh memakai double.
System.out.println("double 0.1 + 0.2 = " + (0.1 + 0.2));
System.out.println("BigDecimal 0.1 + 0.2 = "
+ new BigDecimal("0.1").add(new BigDecimal("0.2")));
System.out.println();
BukuKas kas = new BukuKas();
for (Transaksi t : contohJanuari()) {
kas.tambah(t);
}
System.out.println("=== Buku kas Budi — Januari 2026 ===");
System.out.printf(Locale.US, "%-10s %-11s %-12s %14s %s%n",
"Tanggal", "Jenis", "Kategori", "Jumlah", "Keterangan");
for (Transaksi t : kas.semua()) {
System.out.printf(Locale.US, "%-10s %-11s %-12s %,14.2f %s%n",
t.tanggal(), t.jenis(), t.kategori(), t.jumlah(), t.keterangan());
}
System.out.println();
System.out.printf(Locale.US, "Total pemasukan : %,14.2f%n",
kas.total(JenisTransaksi.PEMASUKAN));
System.out.printf(Locale.US, "Total pengeluaran: %,14.2f%n",
kas.total(JenisTransaksi.PENGELUARAN));
System.out.printf(Locale.US, "Saldo : %,14.2f%n", kas.saldo());
// Dua transaksi cacat: compact constructor Transaksi menolak keduanya.
System.out.println();
tolak("nominal negatif", () -> new Transaksi(LocalDate.of(2026, 1, 10),
"Coba pengeluaran palsu", JenisTransaksi.PENGELUARAN, "Makanan",
new BigDecimal("-100.00")));
tolak("keterangan kosong", () -> new Transaksi(LocalDate.of(2026, 1, 11),
" ", JenisTransaksi.PENGELUARAN, "Makanan",
new BigDecimal("25000.00")));
}
/** Menjalankan satu pembuatan Transaksi dan melaporkan pesan penolakan. */
private static void tolak(String kasus, Runnable pembuatan) {
try {
pembuatan.run();
System.out.println("DITOLAK " + kasus + ": penolakan tidak terjadi");
} catch (IllegalArgumentException e) {
System.out.println("DITOLAK " + kasus + ": " + e.getMessage());
}
}
private static List<Transaksi> contohJanuari() {
return List.of(
new Transaksi(LocalDate.of(2026, 1, 5), "Gaji bulanan Januari",
JenisTransaksi.PEMASUKAN, "Gaji", new BigDecimal("8500000.00")),
new Transaksi(LocalDate.of(2026, 1, 7), "Sewa tempat tinggal Januari",
JenisTransaksi.PENGELUARAN, "Sewa", new BigDecimal("2500000.00")),
new Transaksi(LocalDate.of(2026, 1, 10), "Belanja bahan makanan",
JenisTransaksi.PENGELUARAN, "Makanan", new BigDecimal("450000.00")),
new Transaksi(LocalDate.of(2026, 1, 18), "Makan di luar bersama Siti",
JenisTransaksi.PENGELUARAN, "Makanan", new BigDecimal("380000.00")),
new Transaksi(LocalDate.of(2026, 1, 22), "Isi bensin",
JenisTransaksi.PENGELUARAN, "Transportasi", new BigDecimal("350000.00")),
new Transaksi(LocalDate.of(2026, 1, 28), "Langganan streaming",
JenisTransaksi.PENGELUARAN, "Hiburan", new BigDecimal("250000.00")));
}
}Output yang Dihasilkan:
double 0.1 + 0.2 = 0.30000000000000004
BigDecimal 0.1 + 0.2 = 0.3
=== Buku kas Budi — Januari 2026 ===
Tanggal Jenis Kategori Jumlah Keterangan
2026-01-05 PEMASUKAN Gaji 8,500,000.00 Gaji bulanan Januari
2026-01-07 PENGELUARAN Sewa 2,500,000.00 Sewa tempat tinggal Januari
2026-01-10 PENGELUARAN Makanan 450,000.00 Belanja bahan makanan
2026-01-18 PENGELUARAN Makanan 380,000.00 Makan di luar bersama Siti
2026-01-22 PENGELUARAN Transportasi 350,000.00 Isi bensin
2026-01-28 PENGELUARAN Hiburan 250,000.00 Langganan streaming
Total pemasukan : 8,500,000.00
Total pengeluaran: 3,930,000.00
Saldo : 4,570,000.00
DITOLAK nominal negatif: Jumlah harus positif, diterima: -100.00
DITOLAK keterangan kosong: Keterangan tidak boleh kosongPerhatikan empat hal pada program tersebut:
printf(Locale.US, ...): locale dinyatakan eksplisit agar pemisah ribuan dan titik desimal tidak berubah mengikuti setelan komputer pembaca. Format%,14.2fmengelompokkan ribuan dan merata-kanankan nominal dalam kolom rapi.- Urutan sisipan = urutan kronologis: data contoh disusun menurut tanggal, dan
ArrayListmempertahankan urutan sisipan, sehingga buku kas tampil seperti jurnal yang ditulis urut. tolak(...)menerimaRunnable: lambda menunda pembuatanTransaksihingga berada di dalam bloktry, sehinggaIllegalArgumentExceptiondari compact constructor tertangkap dan pesannya dicetak — bukan menghentikan program.- Total dan saldo konsisten:
saldo()mengurangkan kedua total pada keluaran di atasnya, bukan membaca angka yang disimpan terpisah.
Pencatatan kini berdiri sendiri sebagai data yang sah, tetapi kategori pada transaksi masih berupa teks bebas — “Makanan” hari ini bisa menjadi “makanan” besok dan laporan pun terbelah. Bagian 2 mengangkat kategori menjadi entitas kelas satu yang terdaftar resmi, lalu menghadangkannya dengan anggaran bulanan.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
BukuKas.java
Unduh berkasimport java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** Kumpulan transaksi milik satu pemilik, lengkap dengan total per jenis dan saldo. */
public class BukuKas {
private final List<Transaksi> daftar = new ArrayList<>();
/** Menambah satu transaksi; null ditolak sebelum sempat masuk daftar. */
public void tambah(Transaksi t) {
if (t == null) {
throw new IllegalArgumentException("Transaksi tidak boleh null");
}
daftar.add(t);
}
/** Daftar transaksi baca-saja agar pemanggil tidak dapat mengubah catatan dari luar. */
public List<Transaksi> semua() {
return Collections.unmodifiableList(daftar);
}
public int jumlahTransaksi() {
return daftar.size();
}
/** Menjumlahkan nominal seluruh transaksi satu jenis dengan loop eksplisit. */
public BigDecimal total(JenisTransaksi jenis) {
BigDecimal total = BigDecimal.ZERO;
for (Transaksi t : daftar) {
if (t.jenis() == jenis) {
total = total.add(t.jumlah());
}
}
return total;
}
/** Saldo = total pemasukan dikurangi total pengeluaran. */
public BigDecimal saldo() {
return total(JenisTransaksi.PEMASUKAN)
.subtract(total(JenisTransaksi.PENGELUARAN));
}
}
DemoKeuangan.java
Unduh berkasimport java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Locale;
/** Bagian 1: mencatat transaksi Januari 2026 milik Budi. */
public class DemoKeuangan {
public static void main(String[] args) {
// Bukti singkat kenapa nominal uang tidak boleh memakai double.
System.out.println("double 0.1 + 0.2 = " + (0.1 + 0.2));
System.out.println("BigDecimal 0.1 + 0.2 = "
+ new BigDecimal("0.1").add(new BigDecimal("0.2")));
System.out.println();
BukuKas kas = new BukuKas();
for (Transaksi t : contohJanuari()) {
kas.tambah(t);
}
System.out.println("=== Buku kas Budi — Januari 2026 ===");
System.out.printf(Locale.US, "%-10s %-11s %-12s %14s %s%n",
"Tanggal", "Jenis", "Kategori", "Jumlah", "Keterangan");
for (Transaksi t : kas.semua()) {
System.out.printf(Locale.US, "%-10s %-11s %-12s %,14.2f %s%n",
t.tanggal(), t.jenis(), t.kategori(), t.jumlah(), t.keterangan());
}
System.out.println();
System.out.printf(Locale.US, "Total pemasukan : %,14.2f%n",
kas.total(JenisTransaksi.PEMASUKAN));
System.out.printf(Locale.US, "Total pengeluaran: %,14.2f%n",
kas.total(JenisTransaksi.PENGELUARAN));
System.out.printf(Locale.US, "Saldo : %,14.2f%n", kas.saldo());
// Dua transaksi cacat: compact constructor Transaksi menolak keduanya.
System.out.println();
tolak("nominal negatif", () -> new Transaksi(LocalDate.of(2026, 1, 10),
"Coba pengeluaran palsu", JenisTransaksi.PENGELUARAN, "Makanan",
new BigDecimal("-100.00")));
tolak("keterangan kosong", () -> new Transaksi(LocalDate.of(2026, 1, 11),
" ", JenisTransaksi.PENGELUARAN, "Makanan",
new BigDecimal("25000.00")));
}
/** Menjalankan satu pembuatan Transaksi dan melaporkan pesan penolakan. */
private static void tolak(String kasus, Runnable pembuatan) {
try {
pembuatan.run();
System.out.println("DITOLAK " + kasus + ": penolakan tidak terjadi");
} catch (IllegalArgumentException e) {
System.out.println("DITOLAK " + kasus + ": " + e.getMessage());
}
}
private static List<Transaksi> contohJanuari() {
return List.of(
new Transaksi(LocalDate.of(2026, 1, 5), "Gaji bulanan Januari",
JenisTransaksi.PEMASUKAN, "Gaji", new BigDecimal("8500000.00")),
new Transaksi(LocalDate.of(2026, 1, 7), "Sewa tempat tinggal Januari",
JenisTransaksi.PENGELUARAN, "Sewa", new BigDecimal("2500000.00")),
new Transaksi(LocalDate.of(2026, 1, 10), "Belanja bahan makanan",
JenisTransaksi.PENGELUARAN, "Makanan", new BigDecimal("450000.00")),
new Transaksi(LocalDate.of(2026, 1, 18), "Makan di luar bersama Siti",
JenisTransaksi.PENGELUARAN, "Makanan", new BigDecimal("380000.00")),
new Transaksi(LocalDate.of(2026, 1, 22), "Isi bensin",
JenisTransaksi.PENGELUARAN, "Transportasi", new BigDecimal("350000.00")),
new Transaksi(LocalDate.of(2026, 1, 28), "Langganan streaming",
JenisTransaksi.PENGELUARAN, "Hiburan", new BigDecimal("250000.00")));
}
}
JenisTransaksi.java
Unduh berkas/** Jenis arus kas: uang masuk atau uang keluar. */
public enum JenisTransaksi {
PEMASUKAN,
PENGELUARAN
}
Transaksi.java
Unduh berkasimport java.math.BigDecimal;
import java.time.LocalDate;
/**
* Satu baris catatan keuangan: tanggal, keterangan, jenis, kategori, dan nominal.
* Nominal selalu BigDecimal agar bebas galat pembulatan biner.
*/
public record Transaksi(LocalDate tanggal, String keterangan, JenisTransaksi jenis,
String kategori, BigDecimal jumlah) {
// Compact constructor: validasi berjalan otomatis pada setiap pembuatan record.
public Transaksi {
if (tanggal == null || keterangan == null || jenis == null
|| kategori == null || jumlah == null) {
throw new IllegalArgumentException("Seluruh field transaksi wajib terisi");
}
if (keterangan.isBlank()) {
throw new IllegalArgumentException("Keterangan tidak boleh kosong");
}
if (kategori.isBlank()) {
throw new IllegalArgumentException("Kategori tidak boleh kosong");
}
if (jumlah.signum() <= 0) {
throw new IllegalArgumentException("Jumlah harus positif, diterima: "
+ jumlah.toPlainString());
}
}
}