Set Deep Dive: Uniqueness, Identity, Hashing, and Ordering
Learn Java Array, Collections, Iterator/Iterable, Stream - Part 010
Deep dive Set di Java: uniqueness boundary, HashSet, LinkedHashSet, TreeSet, EnumSet, equality vs identity, hashing, ordering, duplicate handling, set algebra, dan production correctness.
Part 010 — Set Deep Dive: Uniqueness, Identity, Hashing, and Ordering
1. Tujuan Part Ini
Part ini membahas Set sebagai abstraction untuk uniqueness boundary.
Banyak engineer memakai Set hanya karena ingin contains lebih cepat daripada List.
Itu benar, tetapi belum cukup.
Skill yang lebih penting:
Mampu menggunakan
Setuntuk mengekspresikan invariant domain: sesuatu harus unik, membership penting, duplicate adalah kondisi bermakna, dan ordering harus eksplisit jika diperlukan.
Set bukan sekadar data structure.
Dalam production system, Set sering dipakai untuk:
- deduplication;
- permission model;
- feature flags;
- visited markers;
- uniqueness validation;
- membership index;
- conflict detection;
- domain invariant;
- graph/workflow traversal support;
- audit diff;
- eligibility rule evaluation.
Kesalahan kecil pada Set dapat menghasilkan:
- duplicate yang lolos;
- element hilang secara “misterius”;
- output tidak deterministic;
- comparator inconsistent bug;
- mutable key/element corruption;
- null handling bug;
- memory blow-up;
- incorrect access control;
- audit mismatch.
2. Kaufman Deconstruction: Skill Map untuk Set
Kaufman-style target:
- pahami
Setsebagai semantic contract; - pahami bagaimana equality/hashing/ordering menentukan duplicate;
- pilih implementation berdasarkan order, performance, dan domain need;
- gunakan set algebra untuk logic yang jelas;
- hindari mutable element corruption;
- desain API yang eksplisit tentang uniqueness dan ordering.
3. Mental Model: Set Adalah Boundary untuk “Sudah Ada atau Belum?”
Set merepresentasikan collection yang tidak mengandung duplicate.
Contoh:
Set<String> permissions = new HashSet<>();
permissions.add("READ");
permissions.add("WRITE");
permissions.add("READ");
System.out.println(permissions.size()); // 2
Set.add mengembalikan boolean.
boolean added = permissions.add("READ");
Maknanya:
true: element belum ada, sekarang ditambahkan;false: element sudah ada menurut equality semantics set.
Ini sangat berguna untuk invariant:
if (!seenCaseIds.add(caseId)) {
throw new DuplicateCaseException(caseId);
}
Jangan abaikan return value add jika duplicate adalah error.
4. Set Contract: Apa yang Dijanjikan?
Set menjanjikan uniqueness.
Tetapi tidak menjanjikan:
- ordering universal;
- fast lookup universal;
- null support universal;
- thread-safety;
- immutability;
- stable iteration order;
- identity-based uniqueness;
- deep immutability;
- deterministic serialization order.
Set juga sangat bergantung pada definisi equality.
Untuk HashSet, duplicate ditentukan oleh equals dan hashCode.
Untuk TreeSet, duplicate ditentukan oleh comparator/natural ordering, yaitu ketika compare menghasilkan 0.
Ini perbedaan besar.
5. HashSet: General-Purpose Membership Set
HashSet adalah implementation paling umum.
Mental model:
Karakteristik:
- backed by
HashMap; - tidak menjamin iteration order;
- mengizinkan satu
nullelement; - lookup/add/remove rata-rata efisien;
- bergantung pada
hashCodedanequals; - tidak synchronized.
Contoh:
Set<String> ids = new HashSet<>();
ids.add("C-001");
ids.add("C-002");
ids.add("C-001");
System.out.println(ids); // order tidak boleh diasumsikan
Gunakan HashSet untuk:
- membership lookup;
- deduplication tanpa order;
- visited marker;
- duplicate detection;
- intermediate index.
Jangan gunakan HashSet jika output order penting.
6. Hashing Contract: HashSet Tidak Bisa Menyelamatkan Object yang Salah
Contoh benar dengan record:
record CustomerId(String value) { }
Set<CustomerId> ids = new HashSet<>();
ids.add(new CustomerId("C-001"));
ids.add(new CustomerId("C-001"));
System.out.println(ids.size()); // 1
Contoh salah:
final class CustomerId {
private final String value;
CustomerId(String value) {
this.value = value;
}
}
Set<CustomerId> ids = new HashSet<>();
ids.add(new CustomerId("C-001"));
ids.add(new CustomerId("C-001"));
System.out.println(ids.size()); // 2
Karena equals/hashCode tidak dioverride.
Perbaikan:
final class CustomerId {
private final String value;
CustomerId(String value) {
this.value = Objects.requireNonNull(value);
}
@Override
public boolean equals(Object other) {
return other instanceof CustomerId that
&& this.value.equals(that.value);
}
@Override
public int hashCode() {
return value.hashCode();
}
}
Rule:
HashSetcorrectness adalah correctnessequals/hashCodedari element-nya.
7. Mutable Element Hazard
Bug paling berbahaya:
final class UserKey {
private String username;
UserKey(String username) {
this.username = username;
}
void rename(String username) {
this.username = username;
}
@Override
public boolean equals(Object other) {
return other instanceof UserKey that
&& Objects.equals(username, that.username);
}
@Override
public int hashCode() {
return Objects.hash(username);
}
}
Lalu:
UserKey key = new UserKey("alice");
Set<UserKey> users = new HashSet<>();
users.add(key);
key.rename("bob");
System.out.println(users.contains(key)); // bisa false
Kenapa?
Karena hash bucket dihitung saat insert berdasarkan hash lama.
Setelah state berubah, object “terletak” di bucket lama tetapi dicari dengan hash baru.
Prinsip:
Field yang dipakai untuk equality/hashing harus stabil selama element berada di hash-based collection.
Solusi:
- gunakan immutable value object;
- gunakan record;
- jangan mutate field identity;
- remove lalu add ulang jika identity memang berubah;
- jangan gunakan mutable DTO sebagai set element jika equality berbasis mutable field.
8. LinkedHashSet: Uniqueness + Encounter Order
LinkedHashSet menjaga uniqueness seperti hash set, tetapi mempertahankan encounter order.
Biasanya insertion order.
Contoh:
Set<String> ids = new LinkedHashSet<>();
ids.add("C");
ids.add("A");
ids.add("B");
ids.add("A");
System.out.println(ids); // [C, A, B]
Gunakan LinkedHashSet jika Anda butuh:
- deduplication sambil mempertahankan order input;
- deterministic output berdasarkan input order;
- stable serialization order;
- first-seen-wins behavior.
Contoh production:
static List<String> uniquePreservingOrder(List<String> input) {
return List.copyOf(new LinkedHashSet<>(input));
}
Ini pattern sangat berguna.
Jika Anda memakai HashSet lalu convert ke list, order bisa tidak deterministic.
9. TreeSet: Sorted Uniqueness
TreeSet memberikan sorted set.
Duplicate ditentukan oleh comparator/natural ordering.
Contoh:
Set<String> names = new TreeSet<>();
names.add("Bima");
names.add("Ayu");
names.add("Citra");
System.out.println(names); // [Ayu, Bima, Citra]
Gunakan TreeSet jika Anda butuh:
- sorted iteration;
- range query;
- floor/ceiling/lower/higher operations;
- navigable set behavior;
- deterministic order by comparator.
Contoh:
NavigableSet<LocalDate> dates = new TreeSet<>();
dates.add(LocalDate.parse("2026-06-01"));
dates.add(LocalDate.parse("2026-06-15"));
dates.add(LocalDate.parse("2026-07-01"));
LocalDate next = dates.ceiling(LocalDate.parse("2026-06-10"));
System.out.println(next); // 2026-06-15
Tetapi TreeSet tidak menggunakan hash.
Ia memakai ordering.
10. TreeSet Trap: Comparator Determines Equality
Contoh:
record Person(String name, int age) { }
Set<Person> people = new TreeSet<>(Comparator.comparing(Person::name));
people.add(new Person("Ayu", 20));
people.add(new Person("Ayu", 30));
System.out.println(people.size()); // 1
Kenapa?
Comparator hanya membandingkan name.
Bagi TreeSet, dua element dianggap duplicate jika comparator menghasilkan 0.
Padahal record equals akan menganggap mereka berbeda karena age berbeda.
Ini bukan bug Java.
Ini desain comparator.
Jika ingin unique berdasarkan semua field:
Set<Person> people = new TreeSet<>(
Comparator.comparing(Person::name)
.thenComparingInt(Person::age)
);
Rule:
Comparator pada
TreeSetadalah identity function untuk uniqueness dalam set itu.
Jika comparator tidak consistent with equals, dokumentasikan dengan sangat jelas.
11. BigDecimal dan Consistency Trap
BigDecimal adalah contoh klasik.
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
System.out.println(a.equals(b)); // false
System.out.println(a.compareTo(b)); // 0
Maka:
Set<BigDecimal> hash = new HashSet<>();
hash.add(a);
hash.add(b);
System.out.println(hash.size()); // 2
Set<BigDecimal> tree = new TreeSet<>();
tree.add(a);
tree.add(b);
System.out.println(tree.size()); // 1
Ini menunjukkan bahwa pilihan set implementation bisa mengubah duplicate semantics.
Dalam domain uang, biasanya lebih baik normalize scale atau gunakan money value object.
record Money(BigDecimal amount, Currency currency) {
Money {
amount = amount.setScale(2, RoundingMode.UNNECESSARY);
}
}
12. EnumSet: Set Paling Tepat untuk Enum
Jika element type adalah enum, gunakan EnumSet.
Contoh:
enum Permission {
READ,
WRITE,
APPROVE,
DELETE
}
EnumSet<Permission> permissions = EnumSet.of(Permission.READ, Permission.WRITE);
Karakteristik:
- khusus untuk enum;
- sangat compact secara konsep karena dapat direpresentasikan seperti bit vector;
- tidak mengizinkan
null; - iteration mengikuti natural order enum declaration;
- punya factory seperti
noneOf,allOf,of,range,complementOf.
Contoh:
EnumSet<Permission> all = EnumSet.allOf(Permission.class);
EnumSet<Permission> dangerous = EnumSet.of(Permission.DELETE, Permission.APPROVE);
EnumSet<Permission> safe = EnumSet.complementOf(dangerous);
Untuk permission, flags, states, capabilities, EnumSet biasanya lebih jelas dan efisien daripada HashSet<Enum>.
13. Set.of dan Set.copyOf
Set.of(...) membuat unmodifiable set.
Set<String> terminalStatuses = Set.of("APPROVED", "REJECTED", "CANCELLED");
Karakteristik:
- tidak bisa dimodifikasi;
- tidak menerima
null; - tidak menerima duplicate pada argumen factory;
- iteration order tidak boleh dijadikan kontrak kecuali dokumentasi implementation tertentu menyatakan demikian;
- shallow immutable.
Contoh duplicate:
Set.of("A", "A"); // IllegalArgumentException
Set.copyOf(collection) membuat unmodifiable snapshot.
Set<String> permissions = Set.copyOf(inputPermissions);
Catatan penting:
Jika input collection berisi duplicates, hasil set akan deduplicate.
Kalau duplicate adalah error, jangan pakai Set.copyOf sebagai validation tanpa membandingkan size.
static Set<String> requireUnique(Collection<String> input) {
Set<String> copy = Set.copyOf(input);
if (copy.size() != input.size()) {
throw new IllegalArgumentException("duplicates are not allowed");
}
return copy;
}
Tetapi hati-hati: jika input adalah collection yang duplicate-nya sudah tidak terlihat, seperti Set, size comparison tidak membantu.
14. Null Semantics pada Set
Null support bergantung implementation.
Contoh:
Set<String> hash = new HashSet<>();
hash.add(null); // allowed
Set<String> linked = new LinkedHashSet<>();
linked.add(null); // allowed
Set<String> immutable = Set.of(null); // NullPointerException
Untuk TreeSet, null behavior bergantung comparator/natural ordering dan Java version/implementation semantics. Secara production, jangan desain sorted set yang menerima null kecuali comparator Anda eksplisit menanganinya.
Contoh comparator null-safe:
Set<String> values = new TreeSet<>(Comparator.nullsLast(Comparator.naturalOrder()));
values.add(null);
values.add("A");
Tetapi lebih baik reject null di boundary untuk domain collection.
static Set<String> normalizePermissions(Collection<String> input) {
Set<String> result = new LinkedHashSet<>();
for (String permission : input) {
if (permission == null) {
throw new IllegalArgumentException("permission must not be null");
}
result.add(permission.trim().toUpperCase(Locale.ROOT));
}
return Set.copyOf(result);
}
15. Identity-Based Set
Kadang Anda butuh uniqueness berdasarkan object identity, bukan equals.
Java menyediakan IdentityHashMap, dan Anda bisa membuat set berbasis identity:
Set<Object> identitySet = Collections.newSetFromMap(new IdentityHashMap<>());
Contoh:
String a = new String("x");
String b = new String("x");
Set<String> normal = new HashSet<>();
normal.add(a);
normal.add(b);
System.out.println(normal.size()); // 1
Set<String> identity = Collections.newSetFromMap(new IdentityHashMap<>());
identity.add(a);
identity.add(b);
System.out.println(identity.size()); // 2
Gunakan sangat hati-hati.
Use case:
- object graph traversal by instance identity;
- cycle detection pada object graph;
- framework internals;
- serialization identity tracking.
Jangan gunakan untuk business key uniqueness.
16. Set Algebra: Bahasa yang Lebih Jelas untuk Logic
Banyak business logic lebih jelas jika diekspresikan dengan set algebra.
Union
Set<String> union = new HashSet<>(a);
union.addAll(b);
Makna: semua element yang ada di a atau b.
Intersection
Set<String> intersection = new HashSet<>(a);
intersection.retainAll(b);
Makna: element yang ada di a dan b.
Difference
Set<String> onlyA = new HashSet<>(a);
onlyA.removeAll(b);
Makna: element yang ada di a tetapi tidak di b.
Symmetric Difference
Set<String> symmetric = new HashSet<>(a);
symmetric.addAll(b);
Set<String> intersection = new HashSet<>(a);
intersection.retainAll(b);
symmetric.removeAll(intersection);
Makna: element yang ada tepat di salah satu set.
Diagram:
17. Production Pattern: Permission Diff
Misalnya Anda punya permission lama dan baru.
record PermissionChange(
Set<String> added,
Set<String> removed,
Set<String> unchanged
) { }
Implementation:
static PermissionChange diffPermissions(Set<String> oldPermissions, Set<String> newPermissions) {
Set<String> added = new TreeSet<>(newPermissions);
added.removeAll(oldPermissions);
Set<String> removed = new TreeSet<>(oldPermissions);
removed.removeAll(newPermissions);
Set<String> unchanged = new TreeSet<>(oldPermissions);
unchanged.retainAll(newPermissions);
return new PermissionChange(
Set.copyOf(added),
Set.copyOf(removed),
Set.copyOf(unchanged)
);
}
Kenapa TreeSet?
Agar output deterministic.
Jika order output harus berdasarkan input order, gunakan LinkedHashSet.
Jika order tidak penting secara internal tetapi output JSON harus deterministic, sort sebelum boundary.
18. Production Pattern: Duplicate Detection with Diagnostics
Set bagus untuk mendeteksi duplicate.
Naive:
if (new HashSet<>(ids).size() != ids.size()) {
throw new IllegalArgumentException("duplicate ids");
}
Tapi diagnostiknya buruk.
Lebih baik:
static <T> List<T> duplicates(List<T> input) {
Set<T> seen = new HashSet<>();
Set<T> duplicates = new LinkedHashSet<>();
for (T item : input) {
if (!seen.add(item)) {
duplicates.add(item);
}
}
return List.copyOf(duplicates);
}
Contoh:
List<String> duplicatedIds = duplicates(List.of("A", "B", "A", "C", "B"));
System.out.println(duplicatedIds); // [A, B]
LinkedHashSet menjaga order kemunculan duplicate pertama.
19. Production Pattern: First-Seen Wins Deduplication
Sering dibutuhkan ketika input berurutan dan duplicate harus dihapus tanpa mengubah order.
static <T> List<T> deduplicatePreservingOrder(List<T> input) {
return List.copyOf(new LinkedHashSet<>(input));
}
Contoh:
List<String> normalized = deduplicatePreservingOrder(
List.of("C", "A", "C", "B", "A")
);
System.out.println(normalized); // [C, A, B]
Tetapi jika duplicate harus error, jangan deduplicate diam-diam.
static <T> List<T> requireNoDuplicates(List<T> input) {
Set<T> seen = new HashSet<>();
for (T item : input) {
if (!seen.add(item)) {
throw new IllegalArgumentException("duplicate item: " + item);
}
}
return List.copyOf(input);
}
Rule:
Deduplication policy harus eksplisit: reject, first wins, last wins, merge, atau accumulate error.
20. Set as Membership Index
Kadang output tetap List, tetapi lookup harus memakai Set.
Contoh:
List<String> requestedIds = List.of("A", "B", "C", "D");
List<String> allowedIds = List.of("B", "D");
Buruk:
List<String> result = requestedIds.stream()
.filter(allowedIds::contains)
.toList();
Jika allowedIds besar, contains linear.
Lebih baik:
Set<String> allowed = new HashSet<>(allowedIds);
List<String> result = requestedIds.stream()
.filter(allowed::contains)
.toList();
Output tetap mengikuti order requestedIds.
Set hanya dipakai sebagai index membership.
21. EnumSet for State Machines
Untuk state machine, EnumSet sangat natural.
enum CaseStatus {
DRAFT,
SUBMITTED,
IN_REVIEW,
APPROVED,
REJECTED,
CLOSED
}
Terminal states:
private static final EnumSet<CaseStatus> TERMINAL = EnumSet.of(
CaseStatus.APPROVED,
CaseStatus.REJECTED,
CaseStatus.CLOSED
);
Method:
static boolean isTerminal(CaseStatus status) {
return TERMINAL.contains(status);
}
Jika expose keluar:
static Set<CaseStatus> terminalStatuses() {
return Set.copyOf(TERMINAL);
}
Atau:
static EnumSet<CaseStatus> terminalStatusesMutableCopy() {
return EnumSet.copyOf(TERMINAL);
}
Jangan expose mutable static EnumSet langsung.
Buruk:
public static final EnumSet<CaseStatus> TERMINAL = EnumSet.of(...);
Caller bisa mutate.
22. Set and Ordering: Jangan Kabur
Pertanyaan penting:
Apakah set Anda butuh order?
Jika tidak:
Set<String> ids = new HashSet<>();
Jika butuh insertion/encounter order:
Set<String> ids = new LinkedHashSet<>();
Jika butuh sorted order:
Set<String> ids = new TreeSet<>();
Jika element enum:
EnumSet<Permission> permissions = EnumSet.noneOf(Permission.class);
Jangan menulis test yang bergantung pada order HashSet.
Buruk:
assertEquals(List.of("A", "B", "C"), new ArrayList<>(hashSet));
Benar:
assertEquals(Set.of("A", "B", "C"), hashSet);
Atau jika order penting, gunakan ordered set.
23. Set sebagai API Return Type
Return Set jika caller harus tahu bahwa:
- hasil unik;
- membership penting;
- duplicate tidak bermakna;
- order mungkin tidak penting atau dijelaskan oleh implementation/contract.
Contoh tepat:
Set<Permission> effectivePermissions(User user);
Karena permission adalah unique capability.
Jika order juga penting, pertimbangkan:
SequencedSet<Permission> permissionsInDisplayOrder(User user);
atau dokumentasikan:
LinkedHashSet<Permission> permissionsInDisplayOrder(User user);
Namun return concrete type biasanya mengikat implementation. Lebih baik return interface dengan dokumentasi order.
Set<Permission> permissionsInDisplayOrder(User user);
Javadoc harus menyatakan iteration order jika dijamin.
24. Set sebagai API Input Type
Gunakan Set sebagai parameter jika uniqueness adalah precondition.
void assignPermissions(UserId userId, Set<Permission> permissions) {
...
}
Ini memberi sinyal bahwa duplicate tidak relevan.
Tetapi jika Anda perlu mendeteksi duplicate dari caller input, jangan menerima Set, karena duplicates sudah hilang.
Buruk:
void importRows(Set<Row> rows) {
// tidak bisa tahu apakah input awal punya duplicate
}
Lebih baik:
void importRows(List<Row> rows) {
List<Row> duplicates = duplicates(rows);
if (!duplicates.isEmpty()) {
throw new DuplicateRowsException(duplicates);
}
}
Rule:
Terima
Listjika duplicate adalah informasi yang perlu divalidasi; terimaSetjika uniqueness sudah menjadi precondition.
25. Set and Idempotency
Set.add cocok untuk idempotent accumulation.
Contoh:
Set<EventId> processed = new HashSet<>();
boolean firstTime = processed.add(event.id());
if (!firstTime) {
return; // already processed in this batch
}
handle(event);
Tetapi hati-hati:
- ini hanya in-memory;
- tidak aman untuk distributed idempotency;
- tidak menggantikan database unique constraint;
- tidak menggantikan message dedup store.
Dalam batch processing lokal, pattern ini sangat berguna.
Dalam distributed system, gunakan durable idempotency key storage.
26. Set and Security/Authorization
Permission biasanya set.
record AccessContext(Set<Permission> permissions) {
AccessContext {
permissions = Set.copyOf(permissions);
}
boolean has(Permission permission) {
return permissions.contains(permission);
}
}
Jika permission berbasis enum:
record AccessContext(EnumSet<Permission> permissions) {
AccessContext {
permissions = EnumSet.copyOf(permissions);
}
boolean has(Permission permission) {
return permissions.contains(permission);
}
Set<Permission> asSet() {
return Set.copyOf(permissions);
}
}
Hindari List<Permission> untuk permission karena duplicate dan order tidak bermakna.
Hindari string permission mentah jika domain bisa memakai enum/value object.
27. Set and Validation Accumulation
Jika Anda ingin mengumpulkan unique validation errors, gunakan set.
record ValidationError(String field, String code, String message) { }
Set<ValidationError> errors = new LinkedHashSet<>();
errors.add(new ValidationError("email", "required", "email is required"));
errors.add(new ValidationError("email", "required", "email is required"));
LinkedHashSet membuat error unik dan mempertahankan order first occurrence.
Tetapi jika output harus sorted:
List<ValidationError> output = errors.stream()
.sorted(Comparator
.comparing(ValidationError::field)
.thenComparing(ValidationError::code)
.thenComparing(ValidationError::message))
.toList();
Pilih order berdasarkan kebutuhan:
- first occurrence order: debugging flow;
- sorted order: deterministic contract;
- severity order: user-facing priority.
28. retainAll, removeAll, containsAll: Bulk Operations
Bulk set operations expressive.
if (!grantedPermissions.containsAll(requiredPermissions)) {
throw new AccessDeniedException();
}
Untuk missing permissions:
Set<Permission> missing = EnumSet.copyOf(requiredPermissions);
missing.removeAll(grantedPermissions);
if (!missing.isEmpty()) {
throw new AccessDeniedException(missing);
}
Untuk enum set, perlu hati-hati jika required kosong.
EnumSet.copyOf(Collection) pada collection kosong yang bukan EnumSet tidak bisa menentukan enum type.
Lebih aman:
EnumSet<Permission> missing = EnumSet.noneOf(Permission.class);
missing.addAll(requiredPermissions);
missing.removeAll(grantedPermissions);
29. EnumSet.copyOf Empty Collection Trap
Contoh:
List<Permission> empty = List.of();
EnumSet<Permission> permissions = EnumSet.copyOf(empty); // IllegalArgumentException
Kenapa?
Karena dari empty collection biasa, EnumSet tidak bisa tahu element type.
Gunakan:
EnumSet<Permission> permissions = EnumSet.noneOf(Permission.class);
permissions.addAll(empty);
Atau jika input sudah EnumSet:
EnumSet<Permission> copy = EnumSet.copyOf(existingEnumSet);
Rule:
Untuk
EnumSet, kosong tetap membutuhkan enum type.
30. Set and Serialization Output
Set tidak selalu punya order stabil.
Jika Anda serialize HashSet ke JSON array:
record PermissionResponse(Set<String> permissions) { }
Output order bisa tidak sesuai harapan.
Untuk external API, lebih aman expose list dengan order eksplisit:
record PermissionResponse(List<String> permissions) {
static PermissionResponse from(Set<String> permissions) {
return new PermissionResponse(
permissions.stream().sorted().toList()
);
}
}
Internal model tetap set.
External representation bisa list.
Prinsip:
Internal uniqueness dan external ordering adalah dua concern berbeda.
31. Set and Large Data: Memory/Performance Reasoning
HashSet sering cepat, tetapi tidak gratis.
Biaya:
- hash table storage;
- node/entry overhead;
- load factor slack;
- hash computation;
- equality checks;
- object references;
- resize cost;
- poor cache locality dibanding array/list.
Jika data sangat kecil, List.contains kadang cukup dan lebih sederhana.
Contoh:
private static final List<String> SMALL_ALLOWED = List.of("A", "B", "C");
Untuk tiga elemen, Set belum tentu bernilai secara readability.
Tetapi untuk membership besar atau repeated lookup, set lebih tepat.
Decision:
| Size/lookup | Pilihan |
|---|---|
| sangat kecil, sedikit lookup | List.of mungkin cukup |
| banyak lookup | HashSet |
| butuh stable order | LinkedHashSet |
| butuh sorted/range | TreeSet |
| enum | EnumSet |
32. Hash Flooding dan Worst Case Awareness
Secara umum, HashSet memberi operasi efisien.
Namun hash collision tetap mungkin.
Jika banyak element punya hash sama, bucket menjadi mahal.
Contoh buruk:
final class BadKey {
private final String value;
BadKey(String value) {
this.value = value;
}
@Override
public boolean equals(Object other) {
return other instanceof BadKey that
&& value.equals(that.value);
}
@Override
public int hashCode() {
return 1;
}
}
Semua key masuk bucket sama.
Dalam Java modern, hash map punya mekanisme tree bin pada kondisi tertentu, tetapi ini bukan alasan untuk menulis hashCode buruk.
Rule:
hashCodeharus murah, stabil, dan cukup menyebar.
33. Set vs List for Business Semantics
Contoh 1: Status history.
List<StatusChange> history;
Benar, karena order dan duplicate event mungkin penting.
Contoh 2: Current roles.
Set<Role> roles;
Benar, karena role current biasanya unique.
Contoh 3: Error messages untuk display.
List<ValidationError> errors;
Mungkin benar jika order/severity penting.
Contoh 4: Unique error codes.
Set<String> errorCodes;
Benar jika hanya membership unique yang penting.
Rule:
Jangan pakai
Listkarena UI menampilkan array. Jangan pakaiSetkarena lookup cepat. Pilih berdasarkan makna.
34. Domain Wrapper untuk Set
Raw set sering terlalu lemah.
Buruk:
Set<String> permissions;
Lebih baik:
public final class Permissions {
private final EnumSet<Permission> values;
private Permissions(EnumSet<Permission> values) {
this.values = values.clone();
}
public static Permissions of(Collection<Permission> permissions) {
EnumSet<Permission> values = EnumSet.noneOf(Permission.class);
for (Permission permission : permissions) {
values.add(Objects.requireNonNull(permission));
}
return new Permissions(values);
}
public boolean contains(Permission permission) {
return values.contains(permission);
}
public Permissions plus(Permission permission) {
EnumSet<Permission> copy = values.clone();
copy.add(permission);
return new Permissions(copy);
}
public Set<Permission> asSet() {
return Set.copyOf(values);
}
}
Keuntungan:
- invariant terkapsulasi;
- null ditolak;
- representation bisa berubah;
- operasi domain bisa diberi nama;
- external mutation dicegah.
35. Set and Streams
Deduplication dengan stream:
List<String> unique = input.stream()
.distinct()
.toList();
distinct mempertahankan encounter order untuk ordered stream.
Tetapi jika Anda butuh set result:
Set<String> unique = input.stream()
.collect(Collectors.toSet());
Jangan asumsikan Collectors.toSet() menghasilkan HashSet atau order tertentu.
Jika butuh specific set:
Set<String> uniqueOrdered = input.stream()
.collect(Collectors.toCollection(LinkedHashSet::new));
Jika butuh sorted:
Set<String> sorted = input.stream()
.collect(Collectors.toCollection(TreeSet::new));
Rule:
Collector harus menyatakan implementation jika ordering/mutability penting.
36. Concurrent Sets: Hanya Preview Konseptual
Detail concurrency sudah dibahas di seri concurrency, jadi di sini hanya boundary.
Jangan gunakan HashSet mutable biasa dari banyak thread tanpa koordinasi.
Alternatif tergantung kebutuhan:
ConcurrentHashMap.newKeySet();Collections.synchronizedSet(...);CopyOnWriteArraySet;- immutable snapshot set.
Tetapi jangan memilih concurrent set untuk menyembunyikan desain ownership yang buruk.
Pertanyaan desain:
- siapa owner set?
- siapa boleh mutate?
- kapan snapshot dibuat?
- apakah iteration harus consistent?
- apakah duplicate detection harus atomic?
37. Production Decision Matrix
| Kebutuhan | Pilihan | Catatan |
|---|---|---|
| membership umum tanpa order | HashSet | default uniqueness/membership |
| dedupe preserve input order | LinkedHashSet | first-seen-wins |
| deterministic sorted output | TreeSet | comparator identity penting |
| enum flags/states | EnumSet | compact dan expressive |
| immutable small set | Set.of | no null, no duplicates |
| defensive immutable snapshot | Set.copyOf | dedupe silently |
| identity tracking | Collections.newSetFromMap(new IdentityHashMap<>()) | framework-level use |
| output JSON deterministic | sorted list from set | separate internal/external model |
| detect duplicates with diagnostics | seen + duplicates sets | jangan hanya size compare |
| security permissions | EnumSet/domain wrapper | avoid raw strings jika bisa |
38. Code Review Checklist untuk Set
Contract
- Apakah uniqueness adalah requirement nyata?
- Apakah duplicate harus ditolak, diabaikan, atau dimerge?
- Apakah order output penting?
- Apakah null element valid?
- Apakah set boleh dimutasi caller?
Equality/Hashing
- Apakah element punya
equals/hashCodebenar? - Apakah field identity immutable?
- Apakah ada mutable DTO sebagai set element?
- Apakah array dipakai sebagai element/key dan equality identity tidak disengaja?
Ordering
- Apakah
HashSetoutput dijadikan deterministic? - Apakah
TreeSetcomparator consistent dengan intent uniqueness? - Apakah comparator punya tie-breaker?
- Apakah
LinkedHashSetlebih tepat?
API Design
- Apakah parameter
Setmembuat duplicate input tidak bisa dideteksi? - Apakah return
Setcukup menjelaskan order? - Apakah perlu wrapper domain?
- Apakah external API harus return sorted list?
Performance
- Apakah set dibuat berulang kali di loop?
- Apakah set besar disimpan lebih lama dari perlu?
- Apakah
EnumSetbisa menggantikanHashSet<Enum>? - Apakah small list lebih sederhana daripada set?
39. Common Bugs dan Fix
Bug 1: Mutable Element in HashSet
Buruk:
Set<User> users = new HashSet<>();
User user = new User("alice");
users.add(user);
user.setUsername("bob");
users.contains(user); // unreliable
Fix:
record UserKey(String username) { }
Gunakan immutable key.
Bug 2: HashSet Order in Tests
Buruk:
assertEquals(List.of("A", "B"), new ArrayList<>(service.ids()));
Fix jika order tidak penting:
assertEquals(Set.of("A", "B"), service.ids());
Fix jika order penting:
assertEquals(List.of("A", "B"), service.ids().stream().sorted().toList());
Bug 3: TreeSet Comparator Drops Data
Buruk:
new TreeSet<>(Comparator.comparing(Person::name));
Jika name tidak unique, data hilang.
Fix:
new TreeSet<>(Comparator
.comparing(Person::name)
.thenComparing(Person::id));
Bug 4: Set.copyOf Silent Deduplication
Buruk:
this.ids = Set.copyOf(ids); // duplicate input hilang diam-diam
Jika duplicate harus error:
Set<String> unique = new HashSet<>();
for (String id : ids) {
if (!unique.add(id)) {
throw new IllegalArgumentException("duplicate id: " + id);
}
}
this.ids = Set.copyOf(unique);
Bug 5: EnumSet.copyOf Empty List
Buruk:
EnumSet<Permission> permissions = EnumSet.copyOf(inputList);
Jika inputList kosong, bisa gagal.
Fix:
EnumSet<Permission> permissions = EnumSet.noneOf(Permission.class);
permissions.addAll(inputList);
40. Practice: 20-Hour Drill untuk Set
Drill 1 — Replace Wrong Lists
Cari field atau method yang memakai List tetapi sebenarnya uniqueness adalah invariant.
Refactor menjadi Set atau domain wrapper.
Drill 2 — Duplicate Policy Audit
Untuk setiap deduplication:
- duplicate diabaikan?
- duplicate error?
- first wins?
- last wins?
- merge?
- accumulate diagnostics?
Tuliskan eksplisit.
Drill 3 — Hashing Contract Test
Buat test untuk value object yang masuk HashSet:
assertEquals(
Set.of(new CustomerId("C-001")),
Set.of(new CustomerId("C-001"))
);
Pastikan equality bekerja.
Drill 4 — TreeSet Comparator Test
Untuk setiap TreeSet, buat dua object yang comparator-nya mungkin sama.
Pastikan apakah mereka harus dianggap duplicate atau tidak.
Drill 5 — Deterministic Output
Cari API response yang expose Set.
Jika output JSON array harus deterministic, ubah menjadi sorted list atau ordered set dengan contract jelas.
Drill 6 — EnumSet Refactor
Cari Set<SomeEnum> yang memakai HashSet.
Ubah menjadi EnumSet internal jika memungkinkan.
41. Mini Case Study: Access Policy Engine
Masalah:
Anda membangun access policy sederhana.
Input:
- user permissions;
- required permissions;
- denied permissions.
Rules:
- jika ada denied permission yang dimiliki user, reject;
- jika required tidak semuanya dimiliki user, reject;
- output harus menjelaskan missing dan denied secara deterministic.
Model:
enum Permission {
READ,
WRITE,
APPROVE,
DELETE
}
record AccessDecision(
boolean allowed,
Set<Permission> missing,
Set<Permission> denied
) { }
Implementation:
static AccessDecision decide(
Set<Permission> userPermissions,
Set<Permission> requiredPermissions,
Set<Permission> deniedPermissions
) {
EnumSet<Permission> user = EnumSet.noneOf(Permission.class);
user.addAll(userPermissions);
EnumSet<Permission> required = EnumSet.noneOf(Permission.class);
required.addAll(requiredPermissions);
EnumSet<Permission> deniedRules = EnumSet.noneOf(Permission.class);
deniedRules.addAll(deniedPermissions);
EnumSet<Permission> missing = required.clone();
missing.removeAll(user);
EnumSet<Permission> denied = deniedRules.clone();
denied.retainAll(user);
boolean allowed = missing.isEmpty() && denied.isEmpty();
return new AccessDecision(
allowed,
Set.copyOf(missing),
Set.copyOf(denied)
);
}
Catatan:
- internal memakai
EnumSet; - output unmodifiable;
- duplicate tidak mungkin dalam set;
- order enum declaration deterministic untuk banyak kebutuhan display internal;
- jika external output butuh custom order, convert ke sorted/custom list.
42. Advanced Case: Import Validation with Duplicate Diagnostics
Requirement:
- input rows berupa list;
- duplicate
externalIdharus dilaporkan; - duplicate tidak boleh hilang diam-diam;
- output error deterministic berdasarkan kemunculan pertama duplicate.
Model:
record ImportRow(String externalId, String payload) { }
record ImportError(String externalId, String code, String message) { }
Implementation:
static List<ImportError> validateDuplicates(List<ImportRow> rows) {
Set<String> seen = new HashSet<>();
Set<String> duplicates = new LinkedHashSet<>();
for (ImportRow row : rows) {
String id = Objects.requireNonNull(row.externalId(), "externalId");
if (!seen.add(id)) {
duplicates.add(id);
}
}
return duplicates.stream()
.map(id -> new ImportError(id, "duplicate", "duplicate externalId: " + id))
.toList();
}
Mengapa input tetap List?
Karena duplicate adalah informasi yang harus dideteksi.
Jika parameter langsung Set<ImportRow>, duplicate awal bisa hilang sebelum validator bekerja.
43. Baeldung-Style Summary
Set adalah abstraction untuk uniqueness dan membership.
Gunakan HashSet untuk membership umum tanpa ordering guarantee. Gunakan LinkedHashSet ketika ingin dedupe sambil mempertahankan encounter/insertion order. Gunakan TreeSet ketika sorted uniqueness atau range operation penting. Gunakan EnumSet untuk enum karena lebih expressive dan cocok untuk flags/states/permissions.
HashSet bergantung pada equals dan hashCode. TreeSet bergantung pada comparator atau natural ordering. Ini berarti pilihan implementation dapat mengubah definisi duplicate.
Jangan masukkan mutable object ke hash-based set jika field yang dipakai untuk equality/hashing dapat berubah.
Jangan expose HashSet sebagai JSON array jika order output harus deterministic.
Gunakan set algebra untuk membuat business logic lebih jelas: union, intersection, difference, dan symmetric difference.
Jika duplicate adalah error, jangan deduplicate diam-diam. Deteksi dan laporkan.
44. Referensi Resmi
- Java SE 25
Set: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Set.html - Java SE 25
HashSet: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/HashSet.html - Java SE 25
LinkedHashSet: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/LinkedHashSet.html - Java SE 25
TreeSet: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/TreeSet.html - Java SE 25
EnumSet: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/EnumSet.html - Java SE 25
Collections.newSetFromMap: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Collections.html - Java SE 25
Comparator: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Comparator.html
45. Apa yang Harus Dikuasai Sebelum Lanjut
Sebelum lanjut ke Part 011 tentang Map, pastikan Anda bisa menjawab:
- Apa arti uniqueness dalam
HashSet,LinkedHashSet,TreeSet, danEnumSet? - Kenapa mutable element berbahaya di
HashSet? - Kenapa comparator
TreeSetbisa membuat data “hilang”? - Kapan
LinkedHashSetlebih tepat daripadaHashSet? - Kapan
EnumSet.copyOf(collection)bisa gagal? - Apa beda deduplicate silently dan duplicate validation?
- Bagaimana membuat permission model dengan
EnumSet? - Bagaimana menulis set difference untuk audit diff?
- Kenapa
Set.copyOfbukan validator duplicate yang cukup? - Kapan external API sebaiknya expose sorted list, bukan raw set?
Jika Anda bisa menjawabnya dengan contoh production, Anda sudah mulai melihat Set sebagai invariant boundary, bukan sekadar container.
You just completed lesson 10 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.