Tampilan
Kenapa penting
Setelah data tersimpan di List, Set, atau Map, kamu hampir selalu perlu menelusurinya dan mengurutkannya: menampilkan katalog termurah dulu, membuang pesanan yang batal sebelum dihitung, mengambil sepuluh produk terlaris untuk halaman beranda. Kesalahan kecil di sini punya akibat nyata — ConcurrentModificationException di tengah request, urutan harga yang kacau karena overflow, atau produk yang "hilang" dari hasil karena comparator yang keliru. Bab ini membekali kamu cara iterasi yang aman dan cara menyusun urutan yang benar dan mudah dibaca.
Konsep inti
Tiga cara iterasi
java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class IterationDemo {
public static void main(String[] args) {
List<String> orders = new ArrayList<>(List.of("INV-001", "INV-002", "BATAL-003", "INV-004"));
for (String order : orders) { // 1. enhanced for: paling sering dipakai
System.out.print(order + " ");
}
System.out.println();
for (int i = 0; i < orders.size(); i++) { // 2. indeks: bila butuh posisi
System.out.print(i + ":" + orders.get(i) + " ");
}
System.out.println();
Iterator<String> iterator = orders.iterator(); // 3. Iterator: bila perlu menghapus
while (iterator.hasNext()) {
String order = iterator.next();
if (order.startsWith("BATAL")) {
iterator.remove(); // aman: menghapus lewat iterator
}
}
System.out.println(orders);
}
}Output:
text
INV-001 INV-002 BATAL-003 INV-004
0:INV-001 1:INV-002 2:BATAL-003 3:INV-004
[INV-001, INV-002, INV-004]- Enhanced for cocok untuk hampir semua kasus membaca. Ia bekerja untuk semua collection, termasuk
Setyang tidak punya indeks. UntukMap, iterasientrySet()seperti di bab sebelumnya. - Indeks hanya untuk
List, saat kamu butuh posisinya. Hindariget(i)padaLinkedList— setiapgetmenelusuri dari awal. Iteratoradalah yang dipakai enhanced for di balik layar. Pakai langsung bila kamu perlu menghapus elemen saat menelusuri.
ConcurrentModificationException
Menghapus atau menambah elemen langsung ke list di dalam enhanced for membuat iterator kehilangan jejak:
java
import java.util.ArrayList;
import java.util.List;
public class RemoveInForEach {
public static void main(String[] args) {
List<String> orders = new ArrayList<>(List.of("BATAL-001", "INV-002", "INV-003"));
for (String order : orders) {
if (order.startsWith("BATAL")) {
orders.remove(order); // mengubah list di tengah iterasi
}
}
System.out.println(orders);
}
}text
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1096)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1050)
at RemoveInForEach.main(RemoveInForEach.java:7)Iterator mencatat jumlah perubahan list; saat next() dipanggil dan jumlahnya berbeda, ia berhenti dengan exception (cara menangani exception dibahas mulai bab berikutnya). Lebih licik lagi: bila yang dihapus kebetulan elemen kedua dari akhir, loop berhenti tanpa error dan elemen terakhir tidak pernah diperiksa. Solusinya selalu iterator.remove(). Java juga punya orders.removeIf(...) yang lebih ringkas, tetapi memakai lambda — dibahas di M04.
Comparable: urutan alami
Class yang punya satu urutan "wajar" — angka, teks, versi, tanggal — mengimplementasikan interface Comparable dan method compareTo. Hasilnya negatif bila this lebih kecil, 0 bila setara, dan positif bila lebih besar. (<...> pada Comparable<ApiVersion> menyebut tipe yang dibandingkan; detail generics di M04.)
java
import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;
record ApiVersion(int major, int minor) implements Comparable<ApiVersion> {
@Override
public int compareTo(ApiVersion other) {
int byMajor = Integer.compare(major, other.major);
if (byMajor != 0) {
return byMajor;
}
return Integer.compare(minor, other.minor);
}
@Override
public String toString() {
return "v" + major + "." + minor;
}
}
public class ComparableDemo {
public static void main(String[] args) {
List<ApiVersion> versions = new ArrayList<>(List.of(
new ApiVersion(2, 0), new ApiVersion(1, 10), new ApiVersion(1, 2)));
versions.sort(null); // null = pakai urutan alami (compareTo)
System.out.println(versions);
System.out.println(new ApiVersion(1, 10).compareTo(new ApiVersion(1, 2)) > 0);
System.out.println(new TreeSet<>(List.of(new ApiVersion(3, 1), new ApiVersion(1, 0))));
List<String> names = new ArrayList<>(List.of("sari", "Budi", "andi"));
names.sort(null);
System.out.println(names);
}
}Output: [v1.2, v1.10, v2.0], true, [v1.0, v3.1], [Budi, andi, sari]. Perhatikan v1.10 benar berada setelah v1.2 karena dibandingkan sebagai angka, bukan teks. Baris terakhir menunjukkan urutan alami String: berdasarkan kode karakter, sehingga huruf besar (B) mendahului huruf kecil. Collections.sort(list) melakukan hal yang sama dengan list.sort(null).
Usahakan compareTo konsisten dengan equals: compareTo bernilai 0 tepat ketika equals bernilai true. TreeSet dan TreeMap memakai compareTo, bukan equals, untuk menentukan duplikat.
Comparator: urutan lain tanpa mengubah class
Produk bisa diurutkan berdasarkan harga, nama, atau penjualan — tidak ada satu urutan alami. Comparator adalah object terpisah yang membandingkan dua nilai lewat method compare. Bentuk paling dasar adalah class biasa:
java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
record Product(String sku, String name, long price, int sold) {}
class ByPrice implements Comparator<Product> { // bentuk class biasa
@Override
public int compare(Product a, Product b) {
return Long.compare(a.price(), b.price());
}
}
public class ComparatorDemo {
public static void main(String[] args) {
List<Product> products = new ArrayList<>(List.of(
new Product("KOPI-250", "Kopi Gayo", 85_000, 120),
new Product("TEH-100", "Teh Melati", 40_000, 300),
new Product("GULA-1K", "Gula Aren", 40_000, 75),
new Product("MADU-500", "Madu Hutan", 120_000, 40)));
products.sort(new ByPrice());
print("termurah", products);
products.sort(Comparator.comparing(Product::name));
print("nama A-Z", products);
products.sort(Comparator.comparingLong(Product::price).reversed()
.thenComparing(Product::name));
print("termahal, seri → nama", products);
products.sort((a, b) -> Integer.compare(b.sold(), a.sold()));
print("terlaris", products);
}
static void print(String label, List<Product> products) {
List<String> names = new ArrayList<>();
for (Product product : products) {
names.add(product.name());
}
System.out.println(label + ": " + names);
}
}Output:
text
termurah: [Teh Melati, Gula Aren, Kopi Gayo, Madu Hutan]
nama A-Z: [Gula Aren, Kopi Gayo, Madu Hutan, Teh Melati]
termahal, seri → nama: [Madu Hutan, Kopi Gayo, Gula Aren, Teh Melati]
terlaris: [Teh Melati, Kopi Gayo, Gula Aren, Madu Hutan]Menulis class untuk setiap urutan cepat melelahkan, karena itu kode Java modern memakai notasi singkat:
Product::nameartinya "ambilname()dari setiap produk" (method reference).Comparator.comparing(...)membuat comparator dari nilai itu; untuklong/intpakaicomparingLong/comparingInt..thenComparing(...)menambah kriteria berikutnya bila kriteria pertama seri..reversed()membalik urutan seluruh comparator yang ada di kirinya.(a, b) -> ...adalah lambda: cara ringkas menulis isi methodcompare.
Sintaks :: dan -> dibahas lengkap di M04. Untuk sekarang, cukup baca pola ini sebagai kalimat: "urutkan berdasarkan harga, dibalik, lalu berdasarkan nama".
Sort itu stabil
List.sort dan Collections.sort bersifat stabil: elemen yang dianggap setara oleh comparator tetap pada urutan relatifnya semula. Pada output "termurah" di atas, Teh Melati tetap di depan Gula Aren karena keduanya seharga 40.000 dan Teh Melati memang lebih dulu di list. Stabilitas berguna, tetapi jangan mengandalkannya untuk urutan yang penting bagi bisnis — tulis kriteria pembeda secara eksplisit dengan thenComparing.
Jebakan a - b dan TreeSet dengan comparator
Comparator berbasis pengurangan terlihat ringkas, tetapi bisa overflow:
java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class SubtractionBug {
public static void main(String[] args) {
List<Integer> balances = new ArrayList<>(List.of(1_500_000_000, -1_000_000_000, 0));
Comparator<Integer> wrong = (a, b) -> a - b; // bisa overflow
Comparator<Integer> right = (a, b) -> Integer.compare(a, b);
balances.sort(wrong);
System.out.println(balances);
balances.sort(right);
System.out.println(balances);
System.out.println(1_500_000_000 - (-1_000_000_000));
}
}Output: [1500000000, -1000000000, 0], [-1000000000, 0, 1500000000], -1794967296. Hasil pengurangan berputar menjadi negatif (ingat overflow di bab Tipe Data), sehingga urutannya kacau. Selalu pakai Integer.compare, Long.compare, atau Comparator.comparingInt/comparingLong.
Comparator juga menentukan duplikat di TreeSet/TreeMap:
java
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;
record Product(String sku, String name, long price) {}
public class TreeComparator {
public static void main(String[] args) {
TreeSet<Product> byPrice = new TreeSet<>(Comparator.comparingLong(Product::price));
byPrice.add(new Product("TEH-100", "Teh Melati", 40_000));
byPrice.add(new Product("GULA-1K", "Gula Aren", 40_000)); // harga sama → dianggap duplikat!
byPrice.add(new Product("KOPI-250", "Kopi Gayo", 85_000));
System.out.println(byPrice.size());
TreeSet<Product> safe = new TreeSet<>(Comparator.comparingLong(Product::price)
.thenComparing(Product::sku)); // pembeda terakhir: kunci unik
safe.addAll(List.of(new Product("TEH-100", "Teh Melati", 40_000),
new Product("GULA-1K", "Gula Aren", 40_000), new Product("KOPI-250", "Kopi Gayo", 85_000)));
System.out.println(safe.size() + " " + safe.first().name());
Map<String, Integer> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
headers.put("Content-Type", 1);
headers.put("accept", 2);
System.out.println(headers.get("content-type") + " " + headers);
}
}Output: 2, 3 Gula Aren, 1 {accept=2, Content-Type=1}. Gula Aren "hilang" dari set pertama karena comparator menilai harganya sama dengan Teh Melati. Tambahkan kunci unik sebagai pembeda terakhir. Sebaliknya, perilaku ini bisa dimanfaatkan: TreeMap dengan String.CASE_INSENSITIVE_ORDER cocok untuk header HTTP yang namanya tidak peka huruf besar/kecil.
Contoh di dunia backend
Endpoint GET /products?sort=price_desc&limit=3 mengembalikan katalog dengan urutan pilihan klien. Service memetakan parameter sort ke comparator dengan switch expression, mengurutkan salinan data, lalu memotongnya:
java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
record Product(String sku, String name, long price, int sold) {}
class ProductQueryService {
private final List<Product> catalog;
ProductQueryService(List<Product> catalog) {
this.catalog = List.copyOf(catalog);
}
List<Product> list(String sort, int limit) {
Comparator<Product> order = switch (sort) {
case "price_asc" -> Comparator.comparingLong(Product::price).thenComparing(Product::sku);
case "price_desc" -> Comparator.comparingLong(Product::price).reversed().thenComparing(Product::sku);
case "bestseller" -> Comparator.comparingInt(Product::sold).reversed().thenComparing(Product::sku);
default -> Comparator.comparing(Product::name);
};
List<Product> result = new ArrayList<>(catalog); // jangan urutkan data sumber
result.sort(order);
return result.subList(0, Math.min(limit, result.size()));
}
}
public class CatalogApi {
public static void main(String[] args) {
ProductQueryService service = new ProductQueryService(List.of(
new Product("KOPI-250", "Kopi Gayo", 85_000, 120),
new Product("TEH-100", "Teh Melati", 40_000, 300),
new Product("GULA-1K", "Gula Aren", 40_000, 75),
new Product("MADU-500", "Madu Hutan", 120_000, 40)));
for (String sort : List.of("price_asc", "price_desc", "bestseller", "tidak-dikenal")) {
List<String> skus = new ArrayList<>();
for (Product product : service.list(sort, 3)) {
skus.add(product.sku());
}
System.out.println(sort + " → " + skus);
}
}
}Output:
text
price_asc → [GULA-1K, TEH-100, KOPI-250]
price_desc → [MADU-500, KOPI-250, GULA-1K]
bestseller → [TEH-100, KOPI-250, GULA-1K]
tidak-dikenal → [GULA-1K, KOPI-250, MADU-500]Tiga kebiasaan baik terlihat di sini. Pertama, setiap urutan diakhiri pembeda unik (sku), sehingga hasil halaman selalu deterministik — penting untuk pagination, agar produk yang sama tidak muncul di dua halaman. Kedua, data sumber List.copyOf tidak pernah diurutkan langsung; setiap request bekerja pada salinannya. Ketiga, nilai sort yang tidak dikenal jatuh ke urutan default. Di aplikasi yang memakai database, pengurutan biasanya didorong ke query ORDER BY, tetapi pola memetakan parameter ke kriteria urut tetap sama.
Pertanyaan interview
1. Bagaimana cara menghapus elemen dari List saat sedang menelusurinya?
Jangan memanggil list.remove(...) di dalam enhanced for — hasilnya ConcurrentModificationException (atau, lebih buruk, elemen terlewat tanpa error). Pakai Iterator dan iterator.remove(), atau list.removeIf(...) dengan lambda. Alternatif lain: kumpulkan hasil ke list baru.
2. Apa beda Comparable dan Comparator, dan kapan memakai masing-masing?
Comparable diimplementasikan class itu sendiri (compareTo) untuk satu urutan alami, seperti angka atau versi. Comparator adalah object terpisah (compare) sehingga satu tipe bisa diurutkan dengan banyak cara, termasuk class yang tidak bisa kamu ubah. Di kode bisnis, Comparator.comparing(...).thenComparing(...) adalah pilihan paling umum.
3. Kenapa return a - b; di comparator dianggap bug?
Pengurangan dua int bisa overflow (mis. bilangan positif besar dikurangi bilangan negatif besar), sehingga tanda hasilnya terbalik dan urutan menjadi salah tanpa error. Pakai Integer.compare(a, b) / Long.compare(a, b) atau Comparator.comparingInt.
Jebakan umum
list.remove()di dalam enhanced for. Pakaiiterator.remove(); jangan mengandalkan "kebetulan tidak error".a - bdicompareTo/compare. Rawan overflow; pakaiInteger.compare/Long.compare.- Comparator tanpa pembeda unik di
TreeSet/TreeMap. Elemen dengan nilai sama "ditelan"; akhiri denganthenComparingpada kunci unik. reversed()di posisi yang salah.reversed()membalik semua kriteria di kirinya, termasuk yang sudah ditambahkanthenComparingsebelumnya.- Mengurutkan list milik orang lain.
sortmengubah list di tempat;List.of(...)bahkan melemparUnsupportedOperationException. Salin dulu dengannew ArrayList<>(...). get(i)dalam loop padaLinkedList. Setiap pemanggilan menelusuri dari awal; pakai enhanced for.
Ringkasan
- Enhanced for untuk membaca, indeks bila butuh posisi,
Iteratorbila perlu menghapus saat iterasi. - Mengubah list langsung di tengah enhanced for memicu
ConcurrentModificationException. Comparable(compareTo) = satu urutan alami di dalam class;Comparator(compare) = urutan tambahan di luar.Comparator.comparing(...).thenComparing(...).reversed()menyusun urutan bertingkat yang mudah dibaca.- Sort di Java stabil, tetapi tetap tulis pembeda unik — terutama untuk pagination dan
TreeSet/TreeMap. - Bandingkan angka dengan
Integer.compare/Long.compare, jangana - b.