Tampilan
Kenapa penting
Kode backend modern penuh dengan "perilaku yang dikirim sebagai data": aturan validasi, urutan sorting, aturan diskon, callback setelah pembayaran berhasil, sampai handler di Spring dan operasi Stream API di bab berikutnya. Sebelum Java 8, semua itu ditulis sebagai class anonim yang panjang. Lambda membuat perilaku itu bisa ditulis dalam satu baris, disimpan di variabel, dan disusun ulang. Kalau kamu belum lancar membaca Order::total, andThen, atau map.merge(k, v, Long::sum), kode di hampir setiap codebase Java hari ini akan terasa asing.
Konsep inti
Functional interface dan sintaks lambda
Functional interface adalah interface dengan tepat satu method abstrak. Lambda adalah cara singkat menulis implementasi method itu. Anotasi @FunctionalInterface opsional, tetapi membuat compiler menolak bila interface itu tidak sengaja punya dua method abstrak.
java
@FunctionalInterface
interface DiscountRule {
long apply(long price);
}
public class LambdaBasics {
public static void main(String[] args) {
// Class anonim: cara lama sebelum Java 8
DiscountRule oldStyle = new DiscountRule() {
@Override
public long apply(long price) {
return price - 5_000;
}
};
DiscountRule flat = price -> price - 5_000; // satu parameter, satu ekspresi
DiscountRule tenPercent = (long price) -> price * 90 / 100;
DiscountRule capped = price -> { // blok: butuh return
long cut = price * 20 / 100;
return price - Math.min(cut, 25_000);
};
System.out.println(oldStyle.apply(100_000));
System.out.println(flat.apply(100_000));
System.out.println(tenPercent.apply(100_000));
System.out.println(capped.apply(200_000));
}
}Output:
text
95000
95000
90000
175000Bentuk sintaks lambda:
| Bentuk | Contoh |
|---|---|
| tanpa parameter | () -> "ORD-1001" |
| satu parameter | price -> price - 5_000 |
| beberapa parameter | (price, qty) -> price * qty |
| tipe parameter eksplisit | (long price) -> price * 90 / 100 |
| badan blok (perlu return) | price -> { ...; return hasil; } |
Tipe parameter biasanya tidak perlu ditulis: compiler menyimpulkannya dari target type — tipe variabel atau parameter tempat lambda itu dipakai. Lambda yang sama x -> x + 1 bisa menjadi DiscountRule atau Function<Long, Long> tergantung konteksnya. Interface lama seperti Comparator dan Runnable juga functional interface, jadi semuanya bisa diisi dengan lambda.
Effectively final: lambda hanya boleh membaca
Lambda boleh memakai variabel lokal dari method di sekitarnya (capture), asalkan variabel itu effectively final: tidak pernah di-assign ulang setelah diinisialisasi, meski tidak ditulis final.
java
import java.util.function.Predicate;
public class Capture {
public static void main(String[] args) {
long minimum = 50_000; // tidak pernah diubah → effectively final
Predicate<Long> freeShipping = total -> total >= minimum;
System.out.println(freeShipping.test(75_000L) + " " + freeShipping.test(20_000L));
}
}Output: true false. Sebaliknya, mencoba mengakumulasi total dari dalam lambda ditolak compiler:
java
long total = 0;
List<Long> prices = List.of(10_000L, 25_000L);
prices.forEach(p -> total += p);text
CaptureError.java:7: error: local variables referenced from a lambda expression must be final or effectively final
prices.forEach(p -> total += p);
^Aturan ini mencegah bug halus: lambda bisa dijalankan nanti atau di thread lain, saat variabel lokal itu sudah tidak ada. Untuk akumulasi, pakai loop biasa atau operasi agregasi Stream (sum, count) yang dibahas di bab berikutnya. Field object tidak terkena aturan ini — tetapi mengubah state bersama dari lambda adalah sumber bug, jadi hindari.
Method reference: empat jenis
Bila lambda hanya memanggil satu method yang sudah ada, method reference (::) lebih ringkas dan lebih mudah dibaca.
java
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
record Product(String sku, long price) {}
public class MethodRefs {
public static void main(String[] args) {
// 1. Static method
Function<String, Integer> parse = Integer::parseInt; // s -> Integer.parseInt(s)
// 2. Instance method milik object tertentu (bound)
String prefix = "SKU-";
Function<String, String> withPrefix = prefix::concat; // s -> prefix.concat(s)
// 3. Instance method milik object sembarang bertipe tertentu (unbound)
Function<String, String> upper = String::toUpperCase; // s -> s.toUpperCase()
// 4. Constructor
Supplier<List<String>> newList = ArrayList::new; // () -> new ArrayList<>()
BiFunction<String, Long, Product> create = Product::new; // (s, p) -> new Product(s, p)
System.out.println(parse.apply("42") + 1);
System.out.println(withPrefix.apply("kopi"));
System.out.println(upper.apply("kopi"));
List<String> list = newList.get();
list.add("teh");
System.out.println(list);
System.out.println(create.apply("KOPI-250", 85_000L));
}
}Output:
text
43
SKU-kopi
KOPI
[teh]
Product[sku=KOPI-250, price=85000]Perbedaan yang sering membingungkan adalah jenis 2 dan 3. Pada bound (prefix::concat), object pemanggil sudah ditentukan. Pada unbound (String::toUpperCase), object pemanggil menjadi parameter pertama lambda. Accessor record seperti Product::price termasuk unbound — bentuk yang paling sering kamu pakai bersama Comparator dan Stream.
Paket java.util.function
Kamu jarang perlu membuat functional interface sendiri; Java sudah menyediakan yang umum:
| Interface | Method abstrak | Bentuk | Contoh pemakaian |
|---|---|---|---|
Function<T, R> | R apply(T) | T → R | mapping entity ke DTO |
Predicate<T> | boolean test(T) | T → boolean | aturan validasi, filter |
Supplier<T> | T get() | () → T | pembuat ID, nilai default yang mahal |
Consumer<T> | void accept(T) | T → (tidak ada) | log, kirim notifikasi |
BiFunction<T,U,R> | R apply(T, U) | (T, U) → R | hitung subtotal dari harga dan qty |
UnaryOperator<T> | T apply(T) | T → T | normalisasi teks, aturan harga |
Untuk primitif ada varian tanpa boxing seperti IntPredicate, LongFunction, dan ToLongFunction. Yang membuat paket ini kuat adalah method default untuk komposisi:
java
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
public class Composition {
public static void main(String[] args) {
UnaryOperator<String> trim = String::strip;
UnaryOperator<String> upper = String::toUpperCase;
Function<String, String> normalizeSku = trim.andThen(upper); // trim dulu, lalu upper
System.out.println(normalizeSku.apply(" kopi-250 "));
Function<Long, Long> addTax = p -> p * 111 / 100;
Function<Long, Long> minus10k = p -> p - 10_000;
System.out.println(minus10k.andThen(addTax).apply(100_000L)); // dikurangi dulu, baru pajak
System.out.println(minus10k.compose(addTax).apply(100_000L)); // pajak dulu, baru dikurangi
Predicate<String> blank = String::isBlank;
Predicate<String> skuFormat = s -> s.startsWith("SKU-");
Predicate<String> validSku = blank.negate().and(skuFormat);
System.out.println(validSku.test("SKU-9") + " " + validSku.test("kopi") + " "
+ validSku.or(s -> s.equals("TEST")).test("TEST"));
}
}Output:
text
KOPI-250
99900
101000
true false truef.andThen(g) artinya "jalankan f, lalu g"; f.compose(g) artinya "jalankan g dulu, baru f". Untuk Predicate tersedia and, or, dan negate. Perhatikan: andThen milik UnaryOperator mengembalikan Function, bukan UnaryOperator.
Lambda di collection: Comparator, removeIf, forEach, merge
Di M03 kamu menulis Comparator dan loop penghitung secara manual. Dengan lambda, kode yang sama jauh lebih ringkas:
java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
record Order(String id, String customer, long total) {}
public class CollectionsWithLambda {
public static void main(String[] args) {
List<Order> orders = new ArrayList<>(List.of(
new Order("O-1", "budi", 120_000),
new Order("O-2", "ani", 45_000),
new Order("O-3", "budi", 300_000),
new Order("O-4", "citra", 45_000)));
orders.sort(Comparator.comparingLong(Order::total).reversed()
.thenComparing(Order::customer));
orders.forEach(o -> System.out.println(o.id() + " " + o.customer() + " " + o.total()));
boolean removed = orders.removeIf(o -> o.total() < 50_000);
System.out.println(removed + " " + orders.size());
Map<String, Long> spendPerCustomer = new TreeMap<>();
for (Order o : List.of(new Order("O-1", "budi", 120_000), new Order("O-2", "ani", 45_000),
new Order("O-3", "budi", 300_000))) {
spendPerCustomer.merge(o.customer(), o.total(), Long::sum);
}
System.out.println(spendPerCustomer);
Map<String, List<String>> ordersByCustomer = new TreeMap<>();
ordersByCustomer.computeIfAbsent("budi", k -> new ArrayList<>()).add("O-1");
ordersByCustomer.computeIfAbsent("budi", k -> new ArrayList<>()).add("O-3");
ordersByCustomer.computeIfAbsent("ani", k -> new ArrayList<>()).add("O-2");
System.out.println(ordersByCustomer);
ordersByCustomer.forEach((customer, ids) -> System.out.println(customer + " -> " + ids.size()));
}
}Output:
text
O-3 budi 300000
O-1 budi 120000
O-2 ani 45000
O-4 citra 45000
true 2
{ani=45000, budi=420000}
{ani=[O-2], budi=[O-1, O-3]}
ani -> 1
budi -> 2Comparator.comparingLong(Order::total).reversed().thenComparing(Order::customer)— total terbesar dulu; bila sama, nama pelanggan A–Z.removeIf(predicate)menghapus elemen yang cocok dan mengembalikantruebila ada yang terhapus.merge(key, value, fn)menggantikan polagetOrDefault(k, 0) + 1dari M03: simpanvaluebila kunci belum ada, atau gabungkan dengan nilai lama memakaifn.computeIfAbsent(key, k -> new ArrayList<>())membuat list hanya bila kunci belum ada, lalu mengembalikannya — pola standar untukMap<K, List<V>>.
Contoh di dunia backend
Aturan checkout di toko online sering berubah: minimal belanja, batas qty, voucher, ongkir. Menyimpan aturan sebagai Predicate dan UnaryOperator membuat service mudah dibaca, dan menambah aturan cukup menambah satu baris.
java
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
record CartLine(String sku, int qty, long unitPrice) {}
record Cart(String customerId, List<CartLine> lines, String voucher) {
long subtotal() {
long sum = 0;
for (CartLine line : lines) {
sum += line.unitPrice() * line.qty();
}
return sum;
}
}
record Rule(String message, Predicate<Cart> violated) {}
public class CheckoutService {
private final List<Rule> rules = List.of(
new Rule("keranjang kosong", cart -> cart.lines().isEmpty()),
new Rule("qty harus 1–10", CheckoutService::hasInvalidQty),
new Rule("minimal belanja Rp20.000", cart -> cart.subtotal() < 20_000));
// Aturan harga: dijalankan berurutan lewat andThen
private final Function<Long, Long> pricing;
CheckoutService(String voucher) {
UnaryOperator<Long> discount = "HEMAT10".equals(voucher)
? total -> total * 90 / 100
: UnaryOperator.identity();
UnaryOperator<Long> shipping = total -> total >= 100_000 ? total : total + 15_000;
this.pricing = discount.andThen(shipping);
}
static boolean hasInvalidQty(Cart cart) {
for (CartLine line : cart.lines()) {
if (line.qty() < 1 || line.qty() > 10) {
return true;
}
}
return false;
}
List<String> validate(Cart cart) {
List<String> errors = new ArrayList<>();
rules.forEach(rule -> {
if (rule.violated().test(cart)) {
errors.add(rule.message());
}
});
return errors;
}
long finalTotal(Cart cart) {
return pricing.apply(cart.subtotal());
}
public static void main(String[] args) {
Cart cart = new Cart("C-7", List.of(
new CartLine("KOPI-250", 1, 85_000),
new CartLine("GULA-500", 1, 30_000)), "HEMAT10");
CheckoutService service = new CheckoutService(cart.voucher());
System.out.println(service.validate(cart) + " total=" + service.finalTotal(cart));
Cart bad = new Cart("C-8", List.of(new CartLine("TEH-100", 0, 40_000)), null);
System.out.println(new CheckoutService(bad.voucher()).validate(bad));
}
}Output:
text
[] total=103500
[qty harus 1–10, minimal belanja Rp20.000]Perhatikan bahwa errors.add(...) di dalam lambda diperbolehkan: variabel errors sendiri tidak di-assign ulang (effectively final), yang berubah adalah isi object list-nya. Aturan yang logikanya lebih dari satu baris (hasInvalidQty) dipindah ke method bernama lalu dirujuk dengan method reference — lebih mudah dites dan dibaca.
Pertanyaan interview
1. Apa itu functional interface, dan kenapa Comparator termasuk di dalamnya padahal punya banyak method?
Functional interface adalah interface dengan tepat satu method abstrak. Comparator hanya punya satu method abstrak, compare; method lain seperti reversed dan thenComparing adalah method default atau static yang sudah punya implementasi. Method yang berasal dari Object (misalnya equals) juga tidak dihitung.
2. Kenapa variabel lokal yang dipakai lambda harus effectively final?
Lambda menyalin nilai variabel lokal saat dibuat, dan bisa dijalankan belakangan atau di thread lain setelah method selesai. Bila variabel itu boleh berubah, nilai yang dilihat lambda menjadi ambigu dan rawan race condition. Karena itu Java hanya mengizinkan capture variabel yang tidak pernah di-assign ulang.
3. Apa beda method reference String::length dan text::length?
String::length adalah unbound: object String menjadi parameter, jadi cocok sebagai Function<String, Integer>. text::length adalah bound pada object text tertentu, jadi tidak menerima parameter dan cocok sebagai Supplier<Integer>.
Jebakan umum
- Mengubah variabel lokal dari lambda → error compile "must be final or effectively final". Pakai loop atau agregasi Stream.
- Tertukar
andThendancompose.f.andThen(g)= f lalu g;f.compose(g)= g lalu f. Urutan memengaruhi hasil, misalnya diskon sebelum atau sesudah pajak. removeIfpadaList.of/Map.of→UnsupportedOperationException. Salin keArrayList/HashMapdulu.- Lambda panjang berbaris-baris. Pindahkan ke method bernama dan rujuk dengan method reference agar bisa dites.
- Lambda yang melempar checked exception. Interface
java.util.functiontidak mendeklarasikanthrows, jadi memanggil method yang melemparIOExceptiondi dalamFunctiontidak bisa dikompilasi tanpatry/catch. Tangani di dalam lambda atau di method pembantu. - Memakai
Function<Long, Long>di kode yang sangat sering dipanggil menambah biaya boxing. Varian primitif sepertiLongUnaryOperatormenghindarinya.
Ringkasan
- Functional interface punya tepat satu method abstrak; lambda adalah implementasi ringkasnya.
@FunctionalInterfacememinta compiler menjaga aturan itu. - Lambda hanya boleh memakai variabel lokal yang effectively final.
- Empat jenis method reference: static (
Integer::parseInt), bound (prefix::concat), unbound (String::toUpperCase), constructor (ArrayList::new). java.util.functionmenyediakanFunction,Predicate,Supplier,Consumer,BiFunction,UnaryOperator, yang bisa disusun denganandThen,compose,and,or,negate.- Collection API memanfaatkan lambda:
Comparator.comparing,removeIf,forEach,merge,computeIfAbsent.