Learn Java Language Object Model Metaprogramming Part 020 Generics Mental Model
title: Learn Java Language Object Model, API Design & Metaprogramming - Part 020 description: Deep mental model of Java generics: parameterized types, bounds, invariance, generic methods, inference, API design, and runtime limitations. series: learn-java-language-object-model-metaprogramming seriesTitle: Learn Java Language Object Model, API Design & Metaprogramming order: 20 partTitle: Generics Mental Model tags:
- java
- generics
- type-system
- api-design
- type-erasure
- object-model
- compile-time-safety date: 2026-06-30
Part 020 — Generics Mental Model
Goal: memahami generics sebagai alat desain kontrak compile-time, bukan sebagai “tipe runtime”. Setelah bagian ini, Anda harus bisa membaca dan merancang generic API tanpa terjebak asumsi salah tentang inheritance, runtime type, dan type erasure.
Generics adalah salah satu bagian Java yang paling sering “dipakai setiap hari tetapi tidak benar-benar dipahami”. Banyak engineer bisa menulis List<String> dan Map<String, Object>, tetapi mulai goyah saat melihat:
<T extends Comparable<? super T>>
atau:
public <R> Pipeline<R> map(Function<? super T, ? extends R> mapper)
Generics di Java adalah sistem kontrak compile-time yang dibangun di atas object model nominal Java dan dijalankan dengan model runtime yang sebagian besar terhapus melalui type erasure. Artinya, generics meningkatkan safety dan expressiveness saat compile-time, tetapi tidak membuat JVM menyimpan setiap T sebagai runtime type biasa seperti Class<T>.
Bagian ini membangun mental model dasar. Wildcards, variance, erasure detail, bridge methods, raw types, heap pollution, dan type token akan didalami di bagian berikutnya.
1. Kaufman Deconstruction
Kita pecah generics menjadi sub-skill:
| Sub-skill | Harus bisa | Kesalahan umum |
|---|---|---|
| Parameterized type | Memahami List<String> sebagai List dengan type argument | Mengira List<String> subtype dari List<Object> |
| Type variable | Mendesain T, R, K, V, E sebagai hubungan antar parameter/return | Menggunakan generic saat tidak ada hubungan type yang perlu dijaga |
| Bounds | Membatasi kemampuan yang boleh dipakai di generic body | Over-bound atau under-bound |
| Generic method | Memakai type parameter di method level | Menaruh generic di class padahal hanya method yang butuh |
| Inference | Membaca bagaimana compiler menebak type | Bingung saat lambda/overload/generic method saling berinteraksi |
| Runtime limitation | Tahu apa yang hilang akibat erasure | instanceof List<String>, new T(), generic array creation |
| API ergonomics | Membuat generic API yang fleksibel tapi readable | Signature terlalu abstrak sampai caller tersiksa |
Target performa: Anda bisa menjawab “apa relasi tipe yang sedang dijaga?” setiap kali melihat generic signature.
2. Mental Model Inti
Generics menjawab pertanyaan:
Bagaimana compiler bisa memastikan beberapa nilai memiliki relasi type tertentu tanpa membuat class baru untuk setiap kombinasi type?
Contoh:
List<String> names = new ArrayList<>();
names.add("Ayu");
String first = names.getFirst();
Compiler tahu bahwa:
- list menerima
String; getFirst()menghasilkanString;- tidak perlu cast manual di call site;
- jika memasukkan
Integer, compile error.
Namun secara runtime, object list tetap instance dari ArrayList, bukan instance khusus ArrayList<String>.
Diagram mental:
Generics adalah compile-time relationship, bukan runtime specialization.
3. Generic Class
Generic class mendefinisikan type variable di level class:
public final class Box<T> {
private final T value;
public Box(T value) {
this.value = value;
}
public T value() {
return value;
}
}
Penggunaan:
Box<String> box = new Box<>("hello");
String value = box.value();
T menyatakan hubungan:
- constructor menerima
T; - field menyimpan
T; - method
value()mengembalikanTyang sama.
Jika tidak ada hubungan seperti ini, generic mungkin tidak dibutuhkan.
3.1 Generic yang buruk
public final class Printer<T> {
public void print(String value) {
System.out.println(value);
}
}
T tidak dipakai. Ini noise.
3.2 Generic yang terlalu luas
public final class Response<T> {
private final T body;
private final int status;
public T body() { return body; }
public int status() { return status; }
}
Ini masuk akal jika body bisa bermacam-macam dan caller perlu type safety.
Namun jangan memakai Response<Object> sebagai default dumping ground. Itu menghilangkan manfaat generic.
4. Generic Method
Generic method punya type parameter sendiri:
public static <T> T requirePresent(Optional<T> optional, String name) {
return optional.orElseThrow(() -> new IllegalArgumentException(name + " is required"));
}
Penggunaan:
String username = requirePresent(user.username(), "username");
Integer age = requirePresent(user.age(), "age");
T hanya relevan untuk method itu. Tidak perlu membuat class generic.
4.1 Class generic vs method generic
Pilih class generic jika type parameter adalah bagian dari identitas object:
public final class Repository<T, ID> {
public Optional<T> findById(ID id) { ... }
public T save(T entity) { ... }
}
Pilih method generic jika type parameter hanya relasi lokal:
public final class JsonCodec {
public <T> T decode(String json, Class<T> type) { ... }
}
4.2 Generic method sebagai relation declaration
public static <T> List<T> copyOf(Iterable<? extends T> source) { ... }
Signature ini menyatakan:
- method menghasilkan
List<T>; - source boleh berisi subtype dari
T; - caller/target type bisa membantu inference.
Kita akan bahas wildcard detail di Part 021.
5. Naming Type Parameters
Konvensi umum:
| Name | Makna umum |
|---|---|
T | Type umum |
E | Element |
K | Key |
V | Value |
R | Result/return |
ID | Identifier type |
SELF | Self type dalam fluent API |
Contoh:
public interface Mapper<S, T> {
T map(S source);
}
Kadang S dan T lebih jelas sebagai IN dan OUT:
public interface Transformer<IN, OUT> {
OUT transform(IN input);
}
Untuk public API, readability lebih penting daripada konvensi pendek jika signature kompleks.
6. Bounds: Memberi Kemampuan pada T
Tanpa bound, compiler hanya tahu T adalah Object.
public static <T> T max(T a, T b) {
// tidak bisa a.compareTo(b)
return a;
}
Tambahkan bound:
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
Sekarang compiler tahu T punya compareTo(T).
Namun signature ini masih kurang fleksibel untuk beberapa kasus. Versi lebih umum:
public static <T extends Comparable<? super T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
Makna:
Tharus comparable;- compare target boleh supertype dari
T; - ini mendukung class yang mewarisi comparability dari parent.
Detail ? super T akan dibahas di Part 021.
6.1 Multiple bounds
public static <T extends AutoCloseable & Runnable> void runAndClose(T resource) throws Exception {
try (resource) {
resource.run();
}
}
Rules:
- paling banyak satu class bound;
- class bound, jika ada, harus pertama;
- interface bounds bisa banyak.
Contoh valid:
<T extends BaseJob & Auditable & Runnable>
Contoh invalid:
<T extends Auditable & BaseJob>
Jika BaseJob adalah class, ia harus pertama.
7. Invariance: Sumber Kebingungan Nomor Satu
String adalah subtype dari Object.
Tetapi:
List<String> bukan subtype dari List<Object>
Kenapa?
Bayangkan ini boleh:
List<String> strings = new ArrayList<>();
List<Object> objects = strings; // Java melarang ini
objects.add(42);
String value = strings.get(0); // rusak
Jika List<String> dianggap List<Object>, maka kita bisa memasukkan Integer ke list string. Jadi Java membuat generic type invariant secara default.
Diagram:
Untuk fleksibilitas, Java memakai wildcard:
List<? extends Object> readable = List.of("a", "b");
Tetapi setelah wildcard, kemampuan write/read berubah. Detailnya Part 021.
8. Arrays vs Generics
Arrays di Java covariant:
String[] strings = new String[1];
Object[] objects = strings;
objects[0] = 42; // ArrayStoreException runtime
Generics invariant:
List<String> strings = new ArrayList<>();
// List<Object> objects = strings; // compile error
Perbandingan:
| Aspek | Array | Generic collection |
|---|---|---|
| Variance | Covariant | Invariant by default |
| Type check | Runtime component type | Compile-time type argument |
| Failure | ArrayStoreException | Compile error |
| Reifiable | Ya, component type runtime diketahui | Type argument umumnya tidak reifiable |
Ini alasan generic array creation dibatasi:
// List<String>[] array = new List<String>[10]; // illegal
Karena runtime tidak bisa menjaga component type List<String> secara penuh.
9. Type Inference
Java sering bisa menebak type argument.
List<String> names = new ArrayList<>();
Diamond <> memakai target type List<String>.
Generic method:
var names = List.of("Ayu", "Bima");
Compiler menginfer List<String>.
Namun inference bisa menjadi kompleks saat:
- lambda;
- method reference;
- overload;
- nested generic method;
- wildcard;
- target type tidak jelas;
- raw type masuk ke expression.
Contoh:
var empty = List.of();
Tanpa target type, compiler bisa memilih List<Object> atau type yang paling sesuai menurut konteks. Jika ingin jelas:
List<String> emptyNames = List.of();
atau:
var emptyNames = List.<String>of();
9.1 Explicit type witness
List<String> names = Collections.<String>emptyList();
<String> sebelum method name disebut type witness. Jarang dibutuhkan, tetapi berguna saat inference gagal atau readability meningkat.
10. Generics dan API Relationship
Generic type parameter harus menyatakan relationship. Jika tidak, signature menjadi abstraksi palsu.
10.1 Good: input-output relationship
public interface Decoder {
<T> T decode(String payload, Class<T> type);
}
Class<T> dan return T terhubung.
10.2 Bad: generic return tanpa evidence
public <T> T get(String key) {
return (T) values.get(key);
}
Ini terlihat type-safe, tetapi compiler tidak punya evidence. Caller bisa menulis:
Integer age = registry.get("username");
Compile berhasil, runtime gagal.
Lebih baik memakai typed key:
public record Key<T>(String name, Class<T> type) {}
public final class Registry {
private final Map<Key<?>, Object> values = new HashMap<>();
public <T> void put(Key<T> key, T value) {
values.put(key, key.type().cast(value));
}
public <T> Optional<T> get(Key<T> key) {
Object value = values.get(key);
return value == null ? Optional.empty() : Optional.of(key.type().cast(value));
}
}
Sekarang return T didukung oleh Key<T>.
11. Class<T> sebagai Runtime Type Token Sederhana
Karena generic type argument umumnya tidak tersedia sebagai runtime class biasa, API sering meminta Class<T>.
public <T> T instantiate(Class<T> type) {
try {
return type.getDeclaredConstructor().newInstance();
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException("Cannot instantiate " + type.getName(), e);
}
}
Penggunaan:
Customer customer = instantiate(Customer.class);
Class<T> menyediakan evidence runtime untuk T.
Keterbatasan:
Class<List<String>> type = List<String>.class; // illegal
Class<T> tidak bisa merepresentasikan List<String> secara langsung. Ia bisa merepresentasikan raw class List.class, bukan parameterized type lengkap. Untuk itu diperlukan type token yang lebih kaya, misalnya Type, ParameterizedType, atau library abstraction.
12. Generic Constructors
Constructor juga bisa generic meskipun class-nya tidak generic atau punya generic sendiri.
public final class AuditEntry {
private final String actor;
private final String payloadType;
public <T> AuditEntry(User actor, T payload) {
this.actor = actor.id().value();
this.payloadType = payload.getClass().getName();
}
}
Namun generic constructor jarang dibutuhkan. Sering kali static factory lebih jelas:
public static <T> AuditEntry from(User actor, T payload) {
return new AuditEntry(actor.id().value(), payload.getClass().getName());
}
Static factory juga memberi nama intent dan inference lebih enak.
13. Generic Static Members
Class generic type parameter tidak tersedia untuk static field/method context.
Invalid:
public final class Box<T> {
// private static T cached; // illegal
}
Kenapa? Karena T milik instance-level parameterization. Static member milik class, bukan instance Box<String> atau Box<Integer>.
Static method harus mendeklarasikan type parameter sendiri:
public final class Boxes {
public static <T> Box<T> of(T value) {
return new Box<>(value);
}
}
14. Raw Types: Compatibility Escape Hatch yang Berbahaya
Raw type adalah generic type tanpa type argument:
List raw = new ArrayList<String>();
raw.add(42);
Raw type ada demi backward compatibility dengan kode sebelum Java 5. Tetapi raw type melemahkan type checking dan bisa menimbulkan heap pollution.
Prefer:
List<?> unknown = new ArrayList<String>();
List<?> berarti “list of some unknown type”. Compiler membatasi operasi write agar lebih aman.
Raw type akan dibahas lebih dalam di Part 022, tetapi aturan praktisnya sederhana:
Jangan gunakan raw type di kode baru kecuali sedang berinteraksi dengan legacy API yang tidak bisa diubah.
15. Generic API Design Patterns Dasar
15.1 Repository
public interface Repository<T, ID> {
Optional<T> findById(ID id);
T save(T aggregate);
void deleteById(ID id);
}
Relasi:
- repository menyimpan aggregate type
T; - id type
IDmencegah salah ID antar aggregate; findByIddandeleteByIdkonsisten.
Lebih kuat:
public interface Identified<ID> {
ID id();
}
public interface Repository<T extends Identified<ID>, ID> {
Optional<T> findById(ID id);
T save(T aggregate);
}
Sekarang repository tahu entity punya id() dengan type yang sama.
15.2 Mapper
public interface Mapper<S, T> {
T map(S source);
}
Jika mapper bisa compose:
public interface Mapper<S, T> {
T map(S source);
default <R> Mapper<S, R> andThen(Mapper<? super T, ? extends R> next) {
Objects.requireNonNull(next, "next");
return source -> next.map(map(source));
}
}
Ini mulai memakai variance. Part 021 akan menjelaskan kenapa ? super T dan ? extends R penting.
15.3 Validator
public interface Validator<T> {
ValidationResult validate(T value);
}
Composition:
public default Validator<T> and(Validator<? super T> other) {
Objects.requireNonNull(other, "other");
return value -> validate(value).combine(other.validate(value));
}
Validator untuk supertype bisa dipakai untuk subtype.
16. Generics dan Fluent API
Fluent API sering butuh self type.
Contoh naive:
public class BaseBuilder {
public BaseBuilder name(String name) {
return this;
}
}
public class UserBuilder extends BaseBuilder {
public UserBuilder email(String email) {
return this;
}
}
Masalah:
new UserBuilder()
.name("Ayu")
.email("ayu@example.com"); // compile error jika name() return BaseBuilder
Solusi F-bounded self type:
public abstract class BaseBuilder<SELF extends BaseBuilder<SELF>> {
private String name;
public SELF name(String name) {
this.name = name;
return self();
}
protected abstract SELF self();
}
public final class UserBuilder extends BaseBuilder<UserBuilder> {
private String email;
public UserBuilder email(String email) {
this.email = email;
return this;
}
@Override
protected UserBuilder self() {
return this;
}
}
Ini advanced pattern. Gunakan hanya jika hierarchy builder benar-benar diperlukan. Banyak kasus lebih baik memakai composition daripada inheritance antar builder.
17. Generics Tidak Menggantikan Domain Types
Buruk:
public record Pair<A, B>(A first, B second) {}
Pair bisa berguna secara internal, tetapi untuk public domain API sering menghapus makna.
Pair<String, BigDecimal> result = calculate(...);
Apa String? currency? accountId? reason? Apa BigDecimal? amount? rate? balance?
Lebih baik:
public record PricingResult(Currency currency, BigDecimal amount) {}
Generics memberi type relation, bukan semantic name. Untuk API publik, semantic type lebih berharga daripada generic container abstrak.
18. What Generics Cannot Do
Generics tidak bisa langsung:
18.1 new T()
public final class Factory<T> {
public T create() {
// return new T(); // illegal
throw new UnsupportedOperationException();
}
}
Solusi:
public final class Factory<T> {
private final Supplier<? extends T> supplier;
public Factory(Supplier<? extends T> supplier) {
this.supplier = supplier;
}
public T create() {
return supplier.get();
}
}
Atau pakai Class<T> jika reflection memang tepat.
18.2 instanceof List<String>
if (value instanceof List<String>) { // illegal
}
Runtime tidak menyimpan type argument seperti itu untuk check biasa.
Gunakan:
if (value instanceof List<?> list) {
// inspect elements if needed
}
18.3 Generic primitive specialization
List<int> numbers; // illegal
Java generics bekerja dengan reference types, bukan primitive type secara langsung. Gunakan wrapper Integer atau primitive-specialized APIs seperti IntStream, IntFunction, IntPredicate, tergantung kebutuhan.
19. Reading Complex Generic Signatures
Gunakan metode tiga langkah:
- Temukan type variables:
T,R,K,V. - Temukan relationship: input, output, ownership, constraint.
- Temukan variance: producer/consumer boundary.
Contoh:
public <R> Pipeline<R> map(Function<? super T, ? extends R> mapper)
Baca:
- method memperkenalkan result type
R; - pipeline saat ini membawa item
T; - mapper boleh menerima
Tatau supertype dariT; - mapper menghasilkan
Ratau subtype dariR; - hasilnya pipeline baru berisi
R.
Contoh lain:
public static <T extends Comparable<? super T>> T max(Collection<? extends T> values)
Baca:
- method mencari maksimum dari nilai bertipe
T; Tcomparable terhadap dirinya atau supertype-nya;- collection boleh berisi subtype dari
T; - return tepat
T.
20. Generic Design Checklist
Saat menulis generic API, tanyakan:
- Apakah type parameter menyatakan relationship nyata?
- Apakah generic harus di class level atau method level?
- Apakah bound memberi kemampuan yang benar, tidak lebih?
- Apakah caller perlu runtime type evidence seperti
Class<T>atauType? - Apakah
Objectlebih jujur daripada generic palsu? - Apakah wildcard akan membuat API lebih fleksibel?
- Apakah return type terlalu generic sampai kehilangan semantic?
- Apakah raw type muncul? Jika iya, kenapa?
- Apakah inference di call site mudah?
- Apakah error compiler akan bisa dipahami caller?
21. Latihan 20 Jam — Part 020
Latihan 1 — Relationship audit
Ambil 10 generic class/method di codebase. Untuk tiap signature, tulis:
Type parameter:
Relationship yang dijaga:
Apakah class-level atau method-level sudah tepat:
Apakah bound sudah tepat:
Apakah caller butuh runtime type evidence:
Jika tidak bisa menjelaskan relationship, generic itu mungkin noise.
Latihan 2 — Invariance proof
Tulis snippet yang menunjukkan kenapa List<String> tidak boleh menjadi List<Object>. Jelaskan dengan kemungkinan memasukkan Integer.
Latihan 3 — Refactor unsafe registry
Mulai dari API ini:
<T> T get(String key);
Refactor menjadi typed key:
record Key<T>(String name, Class<T> type) {}
Pastikan put dan get menjaga type relationship.
Latihan 4 — Generic method vs class generic
Buat dua versi JsonCodec:
class JsonCodec<T> { T decode(String json); }
versus:
class JsonCodec { <T> T decode(String json, Class<T> type); }
Jelaskan mana lebih tepat untuk shared codec service dan kenapa.
22. Ringkasan
Generics adalah alat untuk membuat compiler menjaga hubungan antar tipe. Generic bukan metadata runtime lengkap, bukan pengganti semantic domain types, dan bukan dekorasi untuk membuat API terlihat advanced.
Mental model ringkas:
Type parameter harus selalu menjawab: “nilai mana yang harus punya tipe yang sama atau terhubung?”
Jika tidak ada relationship, jangan gunakan generic. Jika relationship membutuhkan runtime evidence, tambahkan Class<T>, Type, typed key, supplier, atau abstraction lain. Jika API perlu fleksibel terhadap subtype/supertype, wildcard akan menjadi alat utama — dan itu topik berikutnya.
References
- Java Language Specification SE 25 — Chapter 4, Types, Values, and Variables: https://docs.oracle.com/javase/specs/jls/se25/html/jls-4.html
- Java Language Specification SE 25 — Type Erasure, Reifiable Types, Raw Types: https://docs.oracle.com/javase/specs/jls/se25/html/jls-4.html#jls-4.6
- Java SE 25 API —
java.lang.reflect.Type: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/reflect/Type.html - Java SE 25 API —
java.lang.reflect.ParameterizedType: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/reflect/ParameterizedType.html - Java SE 25 API —
java.util.function.Function: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/function/Function.html
You just completed lesson 20 in deepen practice. 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.