Total omzet, stok gabungan, produk termahal, satu paragraf dari banyak kalimat — semuanya adalah bentuk pertanyaan yang sama: banyak nilai masuk, satu nilai keluar. Operasi reduce adalah jawaban paling umum di Stream API untuk pola tersebut. Kita memberikan dua bahan — sebuah nilai awal (identity) dan sebuah fungsi penggabung (accumulator) — lalu JVM melipat seluruh elemen menjadi satu hasil, persis seperti melipat kertas berisi angka menjadi satu baris jumlah. Pada Chapter ini kita akan membedah dua bentuk reduce, membuktikan pentingnya identity yang benar, dan menimbang kapan reduce layak dipakai dibanding operasi agregasi khusus seperti sum dan count.
Melipat Elemen menjadi Satu Nilai #
Bentuk pertama reduce menerima dua argumen: reduce(identity, accumulator). Identity adalah nilai netral — titik awal pelipatan yang tidak mengubah hasil jika stream kosong. Accumulator bertipe BinaryOperator<T>: fungsi yang menggabungkan hasil sementara dengan satu elemen, dipanggil berulang dari elemen pertama sampai terakhir. Untuk penjumlahan, identity yang benar adalah 0 karena 0 + x = x; untuk penggabungan teks, identity yang benar adalah "" karena teks kosong digabung apa adanya. Program berikut mengumpulkan empat variasi sekaligus — termasuk satu contoh sengaja salah agar kita melihat akibatnya:
import java.util.List;
public class AgregasiHarga {
record Produk(String nama, String kategori, int harga) {}
public static void main(String[] args) {
List<Produk> produk = List.of(
new Produk("Kopi Arabika", "minuman", 85000),
new Produk("Teh Hijau", "minuman", 45000),
new Produk("Roti Gandum", "makanan", 30000),
new Produk("Madu Hutan", "makanan", 120000)
);
// reduce(identity, accumulator): identitas 0 netral bagi penjumlahan
int total = produk.stream()
.map(Produk::harga)
.reduce(0, (a, b) -> a + b);
System.out.println("Total : " + total);
// Identitas yang SALAH: 1 bukan elemen netral penjumlahan
int keliru = produk.stream()
.map(Produk::harga)
.reduce(1, (a, b) -> a + b);
System.out.println("Keliru : " + keliru + " (tergeser +1)");
// Nilai terbesar dengan reduce tanpa identitas -> Optional
int termahal = produk.stream()
.map(Produk::harga)
.reduce(Integer::max)
.orElse(0);
System.out.println("Termahal : " + termahal);
// Concat teks: identitas "" netral bagi penggabungan
String gabungan = produk.stream()
.map(Produk::nama)
.reduce((a, b) -> a + " + " + b)
.orElse("(tidak ada produk)");
System.out.println("Gabungan : " + gabungan);
}
}Output yang Dihasilkan:
Total : 280000
Keliru : 280001 (tergeser +1)
Termahal : 120000
Gabungan : Kopi Arabika + Teh Hijau + Roti Gandum + Madu HutanMari kita bedah keempat pelipatan tersebut:
- Baris 16-18 (
reduce(0, (a, b) -> a + b)): pelipatan berjalan seperti((0+85000)+45000)+30000)+120000hingga menghasilkan 280000. Parameteraselalu membawa hasil sementara,badalah elemen berikutnya yang baru dibaca. - Baris 22-24 (
reduce(1, ...)): identity diganti1dan hasilnya tergeser menjadi 280001 — compiler tidak protes, hasilnya saja yang salah diam-diam. Identity wajib memenuhiaccumulator.apply(identity, x)sama denganx; angka1melanggar syarat itu untuk penjumlahan. - Baris 28-31 (
reduce(Integer::max)): bentuk tanpa identity menghasilkanOptional<Integer>— JVM tidak punya nilai awal untuk dikembalikan jika stream kosong.orElse(0)membuka bungkusnya dengan nilai cadangan; hasilnya 120000 dari Madu Hutan. - Baris 35-38 (
reduce((a, b) -> a + " + " + b)): concat teks menyisipkan pemisah" + "di antara hasil sementara dan elemen berikutnya, membentuk satu baris daftar produk. VarianOptionaldipilih agar stream kosong terwakili"(tidak ada produk)".
Hasil yang Mungkin Tidak Ada #
Bentuk kedua, reduce(accumulator), mengembalikan Optional<T> — bentuk yang jujur terhadap kenyataan bahwa stream kosong tidak punya nilai untuk dikembalikan. Optional memaksa pemanggil menghadapi kemungkinan itu lewat orElse, isPresent, atau ifPresent. Pola ini cocok untuk method utilitas seperti “harga termurah dari sekumpulan produk”:
import java.util.List;
import java.util.Optional;
public class ReduceOpsional {
record Produk(String nama, String kategori, int harga) {}
static Optional<Integer> termurah(List<Produk> sumber) {
return sumber.stream()
.map(Produk::harga)
.reduce(Integer::min);
}
public static void main(String[] args) {
List<Produk> terisi = List.of(
new Produk("Kopi Arabika", "minuman", 85000),
new Produk("Air Mineral", "minuman", 8000)
);
List<Produk> kosong = List.of();
System.out.println("Termurah (terisi) : " + termurah(terisi).orElse(-1));
System.out.println("Termurah (kosong) : " + termurah(kosong).orElse(-1));
System.out.println("Kosong ada nilai? : " + termurah(kosong).isPresent());
}
}Output yang Dihasilkan:
Termurah (terisi) : 8000
Termurah (kosong) : -1
Kosong ada nilai? : falseMari kita amati perilakunya:
- Baris 8-12 (method
termurah): signatureOptional<Integer>menyatakan kontraknya sejak baris pertama — “mungkin tidak ada jawaban”. Pemanggil tidak bisa mengabaikan kasus kosong karena compiler memaksaOptionaldibuka lebih dulu. - Baris 21 (
termurah(terisi).orElse(-1)): dua harga dilipat denganInteger::minmenjadi 8000 — harga Air Mineral. - Baris 22-23 (
termurah(kosong)): stream kosong membuatreducemengembalikanOptional.empty(), sehinggaorElse(-1)menghasilkan-1danisPresent()bernilaifalse.
Sebenarnya masih ada bentuk ketiga: reduce(identity, accumulator, combiner) dengan satu fungsi tambahan bernama combiner untuk menggabungkan hasil antar bagian. Bentuk ini wajib saat stream berjalan paralel dan accumulator tidak cukup untuk menyatukan hasil parsial dari beberapa thread:
// Contoh teruji: menghitung total panjang seluruh kata.
int totalPanjang = kata.stream()
.reduce(0,
(jumlah, kataSatu) -> jumlah + kataSatu.length(), // akumulator
Integer::sum); // penggabungPada stream berurutan combiner jarang terpakai; panggung utamanya adalah Chapter Parallel Streams. Yang perlu diingat sekarang: accumulator boleh berganti tipe (di contoh di atas String dilipat menjadi Integer), dan itulah alasan bentuk tiga argumen tidak bisa disederhanakan menjadi dua.
Kapan reduce, Kapan Operasi Khusus #
Jumlah elemen, total angka, rata-rata, nilai terbesar — pola-pola ini begitu umum sehingga Stream API menyediakan operasi akhir khusus: count(), sum(), average(), min(), max(). Semua yang bisa dilakukan reduce dengan lambda, operasi khusus lakukan dengan nama yang langsung terbaca. Program berikut membandingkan keduanya pada data yang sama:
import java.util.List;
public class KhususVsReduce {
record Produk(String nama, String kategori, int harga) {}
public static void main(String[] args) {
List<Produk> produk = List.of(
new Produk("Kopi Arabika", "minuman", 85000),
new Produk("Teh Hijau", "minuman", 45000),
new Produk("Roti Gandum", "makanan", 30000)
);
// reduce manual: identitas + accumulator
int viaReduce = produk.stream()
.map(Produk::harga)
.reduce(0, Integer::sum);
System.out.println("reduce : " + viaReduce);
// Operasi khusus IntStream: langsung tanpa lambda
int viaSum = produk.stream()
.mapToInt(Produk::harga)
.sum();
System.out.println("sum : " + viaSum);
long jumlahElemen = produk.stream().count();
System.out.println("count : " + jumlahElemen);
double rataRata = produk.stream()
.mapToInt(Produk::harga)
.average()
.orElse(0);
System.out.printf("average : %.2f\n", rataRata);
}
}Output yang Dihasilkan:
reduce : 160000
sum : 160000
count : 3
average : 53333.33Perbandingannya membaca begini:
- Baris 15-17 vs 21-23:
reduce(0, Integer::sum)danmapToInt(...).sum()sama-sama menghasilkan 160000; bedanyasum()hidup diIntStreamsehingga bebas dari pembungkusanIntegerdan terbaca sebagai satu kata, bukan satu ekspresi. - Baris 26-27 (
count()): penghitung elemen paling cepat; pada beberapa sumber JVM menjawab tanpa melipat elemen satu per satu. - Baris 29-33 (
average()): menghasilkanOptionalDouble— sekali lagi bentuk jujur untuk stream kosong — dan%.2fmerapikan cetakan pecahan. - Kesimpulan praktisnya: untuk agregasi standar, pakai operasi khusus; simpan
reduceuntuk lipatan yang benar-benar khusus — menggabungkan teks dengan pemisah, menghitung transaksi beruntun, atau melipat record menjadi ringkasan berbentuk record lain.
Ada satu batasan yang perlu kita bawa ke Chapter berikutnya: reduce melipat nilai menjadi nilai baru — hasil antara tidak boleh dimodifikasi, hanya digabung. Bagaimana jika hasil yang kita butuhkan adalah List, Set, atau String yang dirakit bertahap? Di situlah collect mengambil peran dengan container yang bisa diubah, diisi elemen demi elemen — Chapter collect membedah mekanismenya beserta Collectors.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
AgregasiHarga.java
Unduh berkasimport java.util.List;
public class AgregasiHarga {
record Produk(String nama, String kategori, int harga) {}
public static void main(String[] args) {
List<Produk> produk = List.of(
new Produk("Kopi Arabika", "minuman", 85000),
new Produk("Teh Hijau", "minuman", 45000),
new Produk("Roti Gandum", "makanan", 30000),
new Produk("Madu Hutan", "makanan", 120000)
);
// reduce(identity, accumulator): identitas 0 netral bagi penjumlahan
int total = produk.stream()
.map(Produk::harga)
.reduce(0, (a, b) -> a + b);
System.out.println("Total : " + total);
// Identitas yang SALAH: 1 bukan elemen netral penjumlahan
int keliru = produk.stream()
.map(Produk::harga)
.reduce(1, (a, b) -> a + b);
System.out.println("Keliru : " + keliru + " (tergeser +1)");
// Nilai terbesar dengan reduce tanpa identitas -> Optional
int termahal = produk.stream()
.map(Produk::harga)
.reduce(Integer::max)
.orElse(0);
System.out.println("Termahal : " + termahal);
// Concat teks: identitas "" netral bagi penggabungan
String gabungan = produk.stream()
.map(Produk::nama)
.reduce((a, b) -> a + " + " + b)
.orElse("(tidak ada produk)");
System.out.println("Gabungan : " + gabungan);
}
}
KhususVsReduce.java
Unduh berkasimport java.util.List;
public class KhususVsReduce {
record Produk(String nama, String kategori, int harga) {}
public static void main(String[] args) {
List<Produk> produk = List.of(
new Produk("Kopi Arabika", "minuman", 85000),
new Produk("Teh Hijau", "minuman", 45000),
new Produk("Roti Gandum", "makanan", 30000)
);
// reduce manual: identitas + accumulator
int viaReduce = produk.stream()
.map(Produk::harga)
.reduce(0, Integer::sum);
System.out.println("reduce : " + viaReduce);
// Operasi khusus IntStream: langsung tanpa lambda
int viaSum = produk.stream()
.mapToInt(Produk::harga)
.sum();
System.out.println("sum : " + viaSum);
long jumlahElemen = produk.stream().count();
System.out.println("count : " + jumlahElemen);
double rataRata = produk.stream()
.mapToInt(Produk::harga)
.average()
.orElse(0);
System.out.printf("average : %.2f\n", rataRata);
}
}
ReduceOpsional.java
Unduh berkasimport java.util.List;
import java.util.Optional;
public class ReduceOpsional {
record Produk(String nama, String kategori, int harga) {}
static Optional<Integer> termurah(List<Produk> sumber) {
return sumber.stream()
.map(Produk::harga)
.reduce(Integer::min);
}
public static void main(String[] args) {
List<Produk> terisi = List.of(
new Produk("Kopi Arabika", "minuman", 85000),
new Produk("Air Mineral", "minuman", 8000)
);
List<Produk> kosong = List.of();
System.out.println("Termurah (terisi) : " + termurah(terisi).orElse(-1));
System.out.println("Termurah (kosong) : " + termurah(kosong).orElse(-1));
System.out.println("Kosong ada nilai? : " + termurah(kosong).isPresent());
}
}