Pipeline yang kita bangun sejauh ini mengalirkan objek: Siswa, Produk, String. Masalah muncul saat data berupa angka biasa — int, long, double — karena Stream<T> hanya menerima objek, sehingga setiap angka harus dibungkus menjadi Integer, Long, atau Double melalui proses boxing. Untuk deret besar, pembungkusan ini menambah beban memori dan waktu tanpa memberi manfaat apa pun. Java menjawabnya dengan tiga stream khusus: IntStream, LongStream, dan DoubleStream, lengkap dengan operasi statistik bawaan yang tidak dimiliki Stream<T>.

Boxing dan Tiga Stream Primitif #

Generics di Java bekerja di atas objek, bukan tipe primitif; List<Integer> tidak bisa menyimpan int secara mentah. Ketika Stream<Integer> menjalankan filter, setiap perbandingan mengharuskan JIT membuka kotak (Integer menjadi int) lalu menutupnya kembali untuk hasil antara. Pada deret sejuta angka, bolak-balik ini menjadi pekerjaan yang sebenarnya tidak diperlukan.

IntStream menyimpan nilai int dalam struktur internal berbasis array primitif, sehingga tidak ada kotak sama sekali. Sebagai gantinya, stream primitif tidak bisa membawa objek apa pun — itulah sebabnya Java menyediakan jalur konversi seperti mapToObj dan boxed. Perbandingan ringkasnya:

AspekStream<Integer>IntStream
Isi elemenobjek Integer hasil boxingnilai int mentah
Operasi statistik bawaantidak adasum, min, max, average, summaryStatistics
Beban memorisatu objek per angkaarray primitif yang rapat
Cocok untukcampuran objek-angkakomputasi numerik murni

Saudara kandungnya mengikuti pola yang sama: LongStream untuk long dan DoubleStream untuk double. Seluruh method yang kita bahas di bawah (range, sum, summaryStatistics) punya padanan langsung pada ketiganya.

Deret Angka dan Statistik Bawaan #

Dua sumber deret paling populer adalah range dan iterate. range(a, b) menghasilkan angka dari a sampai sebelum b — batas atas tidak ikut — sedangkan rangeClosed(a, b) menyertakan batas atas. iterate(nilaiAwal, fungsiLanjutan) membangkitkan angka berikutnya dari fungsi, sehingga wajib dipagung limit. Program berikut mencoba keduanya lalu menghitung lima statistik deret 1 sampai 100:

import java.util.IntSummaryStatistics;
import java.util.stream.IntStream;

public class StatistikDeret {

    public static void main(String[] args) {
        // range: batas atas tidak ikut; rangeClosed: batas atas ikut
        System.out.println("range(1, 6)      = " + IntStream.range(1, 6).boxed().toList());
        System.out.println("rangeClosed(1, 5)= " + IntStream.rangeClosed(1, 5).boxed().toList());

        // iterate: nilai awal + fungsi lanjutan, dipagung limit
        System.out.println("pangkat dua      = " + IntStream.iterate(2, n -> n * 2).limit(6).boxed().toList());

        // lima operasi statistik bawaan pada deret 1..100
        System.out.println("sum              = " + IntStream.rangeClosed(1, 100).sum());
        System.out.println("min              = " + IntStream.rangeClosed(1, 100).min());
        System.out.println("max              = " + IntStream.rangeClosed(1, 100).max());
        System.out.println("average          = " + IntStream.rangeClosed(1, 100).average());
        System.out.println("count            = " + IntStream.rangeClosed(1, 100).count());

        // summaryStatistics: seluruh angka penting dalam satu penelusuran
        IntSummaryStatistics stat = IntStream.rangeClosed(1, 100).summaryStatistics();
        System.out.println("statistik        = " + stat);
    }
}

Output yang Dihasilkan:

text
range(1, 6)      = [1, 2, 3, 4, 5]
rangeClosed(1, 5)= [1, 2, 3, 4, 5]
pangkat dua      = [2, 4, 8, 16, 32, 64]
sum              = 5050
min              = OptionalInt[1]
max              = OptionalInt[100]
average          = OptionalDouble[50.5]
count            = 100
statistik        = IntSummaryStatistics{count=100, sum=5050, min=1, average=50.500000, max=100}

Mari kita telaah baris-baris kode di atas:

  1. Baris 8-9 (range dan rangeClosed): keduanya mencetak [1, 2, 3, 4, 5] karena batas yang dipilih sengaja disetarakan — range(1, 6) berhenti sebelum 6, rangeClosed(1, 5) sampai di 5. Kedua stream diakhiri boxed().toList() agar bisa dicetak sebagai daftar.
  2. Baris 12 (iterate(2, n -> n * 2).limit(6)): nilai awal 2 dikalikan dua berulang kali menjadi 2, 4, 8, 16, 32, 64. Tanpa limit, iterate tidak pernah berhenti karena stream primitif ini tak berujung.
  3. Baris 15 (sum()): mengembalikan int langsung — bukan OptionalInt — karena deret kosong punya jawaban yang pasti, yaitu 0.
  4. Baris 16-18 (min(), max(), average()): ketiganya menghasilkan OptionalInt atau OptionalDouble karena deret kosong tidak punya nilai terkecil, terbesar, maupun rata-rata. Output menampilkan wadahnya apa adanya: OptionalInt[1], OptionalInt[100], OptionalDouble[50.5].
  5. Baris 22 (summaryStatistics()): satu objek IntSummaryStatistics memuat kelima angka sekaligus. Bila laporan membutuhkan total, rata-rata, dan nilai ekstrem dari sumber yang sama, summaryStatistics menelusuri stream hanya sekali — lebih hemat daripada memanggil sum() lalu average() lalu max() secara terpisah.

Konversi Antar Stream dengan chars, mapToObj, dan boxed #

Teks adalah sumber angka primitif yang mengejutkan seringnya: method String.chars() mengembalikan IntStream berisi kode Unicode setiap karakter. Dari titik itu, seluruh peralatan IntStream langsung terpakai. Sebaliknya, saat hasil akhir berupa objek, kita keluar dari dunia primitif lewat mapToObj atau boxed:

import java.util.stream.Collectors;

public class AngkaDariTeks {

    public static void main(String[] args) {
        String nama = "Budi Santoso";

        // chars(): IntStream berisi kode Unicode tiap karakter
        long jumlahHuruf = nama.chars()
            .filter(c -> !Character.isWhitespace(c))
            .count();
        System.out.println("Jumlah huruf (tanpa spasi) = " + jumlahHuruf);

        long jumlahVokal = nama.chars()
            .filter(c -> "aeiouAEIOU".indexOf(c) >= 0)
            .count();
        System.out.println("Jumlah vokal               = " + jumlahVokal);

        // mapToObj: ubah int menjadi objek String agar bisa di-join
        String dipisah = nama.chars()
            .mapToObj(c -> String.valueOf((char) c))
            .collect(Collectors.joining("-"));
        System.out.println("Per karakter               = " + dipisah);

        // boxed: bungkus int menjadi Integer untuk kebutuhan Collection
        System.out.println("Kode 4 huruf pertama       = " + nama.chars().limit(4).boxed().toList());
    }
}

Output yang Dihasilkan:

text
Jumlah huruf (tanpa spasi) = 11
Jumlah vokal               = 5
Per karakter               = B-u-d-i- -S-a-n-t-o-s-o
Kode 4 huruf pertama       = [66, 117, 100, 105]

Mari kita telaah baris-baris kode di atas:

  1. Baris 9-11 (chars().filter(...).count()): chars() mengalirkan kode karakter, filter membuang spasi lewat Character.isWhitespace, dan count menghitung sisanya — 11 huruf dari "Budi Santoso".
  2. Baris 14-16 ("aeiouAEIOU".indexOf(c) >= 0): filter ini menguji keanggotaan dengan memanfaatkan indexOf yang mengembalikan -1 bila tidak ketemu; teknik ringkas untuk uji “apakah c termasuk salah satu dari daftar ini”.
  3. Baris 20-22 (mapToObj(c -> String.valueOf((char) c))): mapToObj keluar dari IntStream menuju Stream<String>. Pemindahan (char) c wajib karena tanpa pemindahan, String.valueOf(c) menghasilkan angka, bukan huruf.
  4. Baris 26 (boxed()): boxed membungkus setiap int menjadi Integer agar hasilnya bisa menjadi List<Integer>; kode huruf B, u, d, i tampil sebagai 66, 117, 100, 105.

mapToDouble dan flatMapToInt untuk Data Toko #

Arah sebaliknya juga tersedia: stream objek berubah menjadi stream primitif lewat mapToDouble, mapToInt, atau mapToLong — biasanya untuk memanen satu field numerik lalu menghitung. Sementara flatMapToInt melebur beberapa deret primitif menjadi satu:

import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;

public class KeranjangBelanja {

    record Produk(String nama, double harga) {}

    public static void main(String[] args) {
        List<Produk> keranjang = List.of(
            new Produk("Pena", 2500.0),
            new Produk("Buku Tulis", 5000.0),
            new Produk("Penghapus", 1500.5)
        );

        // mapToDouble: stream objek berubah menjadi DoubleStream
        double total = keranjang.stream().mapToDouble(Produk::harga).sum();
        System.out.printf("Total belanja = Rp%.2f%n", total);

        double rataRata = keranjang.stream().mapToDouble(Produk::harga)
            .average().orElse(0.0);
        System.out.printf("Rata-rata     = Rp%.2f%n", rataRata);

        // flatMapToInt: lebur beberapa deret int menjadi satu stream
        int[][] stokCabang = { {12, 8, 5}, {20, 4}, {7} };
        int[] seluruhStok = Arrays.stream(stokCabang)
            .flatMapToInt(IntStream::of)
            .toArray();
        System.out.println("Stok gabungan = " + Arrays.toString(seluruhStok));
        System.out.println("Total stok    = " + Arrays.stream(stokCabang)
            .flatMapToInt(IntStream::of).sum());
    }
}

Output yang Dihasilkan:

text
Total belanja = Rp9000.50
Rata-rata     = Rp3000.17
Stok gabungan = [12, 8, 5, 20, 4, 7]
Total stok    = 56

Mari kita telaah baris-baris kode di atas:

  1. Baris 17 (keranjang.stream().mapToDouble(Produk::harga).sum()): mapToDouble memanen field harga bertipe double, menghasilkan DoubleStream yang langsung punya sum(). Total Rp9000.50 tercetak lewat printf format %.2f agar rapi dua angka desimal.
  2. Baris 20-21 (average().orElse(0.0)): rata-rata DoubleStream berupa OptionalDouble; orElse(0.0) menyiapkan nilai cadangan untuk keranjang kosong tanpa memanggil method get() langsung.
  3. Baris 26-28 (Arrays.stream(stokCabang).flatMapToInt(IntStream::of)): Arrays.stream(int[][]) menghasilkan Stream<int[]> — stream dari larik, bukan angka. flatMapToInt(IntStream::of) mengubah setiap larik menjadi IntStream lalu meratakannya menjadi satu aliran angka, sehingga toArray() menghasilkan [12, 8, 5, 20, 4, 7] dan sum() menghasilkan 56.

Statistik bawaan stream primitif sering menghasilkan OptionalInt dan OptionalDouble. Wadah Optional itu bukan aksesoris — stream memang butuh cara menyatakan “tidak ada hasil”. Chapter berikutnya membedah Optional secara menyeluruh: cara membuatnya, mengalirkannya lewat map dan filter, serta perbedaan penting antara orElse dan orElseGet.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

AngkaDariTeks.java

Unduh berkas
java
import java.util.stream.Collectors;

public class AngkaDariTeks {

    public static void main(String[] args) {
        String nama = "Budi Santoso";

        // chars(): IntStream berisi kode Unicode tiap karakter
        long jumlahHuruf = nama.chars()
            .filter(c -> !Character.isWhitespace(c))
            .count();
        System.out.println("Jumlah huruf (tanpa spasi) = " + jumlahHuruf);

        long jumlahVokal = nama.chars()
            .filter(c -> "aeiouAEIOU".indexOf(c) >= 0)
            .count();
        System.out.println("Jumlah vokal               = " + jumlahVokal);

        // mapToObj: ubah int menjadi objek String agar bisa di-join
        String dipisah = nama.chars()
            .mapToObj(c -> String.valueOf((char) c))
            .collect(Collectors.joining("-"));
        System.out.println("Per karakter               = " + dipisah);

        // boxed: bungkus int menjadi Integer untuk kebutuhan Collection
        System.out.println("Kode 4 huruf pertama       = " + nama.chars().limit(4).boxed().toList());
    }
}

KeranjangBelanja.java

Unduh berkas
java
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;

public class KeranjangBelanja {

    record Produk(String nama, double harga) {}

    public static void main(String[] args) {
        List<Produk> keranjang = List.of(
            new Produk("Pena", 2500.0),
            new Produk("Buku Tulis", 5000.0),
            new Produk("Penghapus", 1500.5)
        );

        // mapToDouble: stream objek berubah menjadi DoubleStream
        double total = keranjang.stream().mapToDouble(Produk::harga).sum();
        System.out.printf("Total belanja = Rp%.2f%n", total);

        double rataRata = keranjang.stream().mapToDouble(Produk::harga)
            .average().orElse(0.0);
        System.out.printf("Rata-rata     = Rp%.2f%n", rataRata);

        // flatMapToInt: lebur beberapa deret int menjadi satu stream
        int[][] stokCabang = { {12, 8, 5}, {20, 4}, {7} };
        int[] seluruhStok = Arrays.stream(stokCabang)
            .flatMapToInt(IntStream::of)
            .toArray();
        System.out.println("Stok gabungan = " + Arrays.toString(seluruhStok));
        System.out.println("Total stok    = " + Arrays.stream(stokCabang)
            .flatMapToInt(IntStream::of).sum());
    }
}

StatistikDeret.java

Unduh berkas
java
import java.util.IntSummaryStatistics;
import java.util.stream.IntStream;

public class StatistikDeret {

    public static void main(String[] args) {
        // range: batas atas tidak ikut; rangeClosed: batas atas ikut
        System.out.println("range(1, 6)      = " + IntStream.range(1, 6).boxed().toList());
        System.out.println("rangeClosed(1, 5)= " + IntStream.rangeClosed(1, 5).boxed().toList());

        // iterate: nilai awal + fungsi lanjutan, dipagung limit
        System.out.println("pangkat dua      = " + IntStream.iterate(2, n -> n * 2).limit(6).boxed().toList());

        // lima operasi statistik bawaan pada deret 1..100
        System.out.println("sum              = " + IntStream.rangeClosed(1, 100).sum());
        System.out.println("min              = " + IntStream.rangeClosed(1, 100).min());
        System.out.println("max              = " + IntStream.rangeClosed(1, 100).max());
        System.out.println("average          = " + IntStream.rangeClosed(1, 100).average());
        System.out.println("count            = " + IntStream.rangeClosed(1, 100).count());

        // summaryStatistics: seluruh angka penting dalam satu penelusuran
        IntSummaryStatistics stat = IntStream.rangeClosed(1, 100).summaryStatistics();
        System.out.println("statistik        = " + stat);
    }
}