Tampilan
Kenapa penting
Hampir setiap baris kode backend modern memakai generics, entah kamu sadari atau tidak: List<Order>, Map<String, Product>, Optional<User>, ResponseEntity<OrderDto>, JpaRepository<Product, Long>. Di level Basic kamu sudah memakai generics lewat collection. Di bab ini kamu belajar membuat sendiri: class dan method yang bekerja untuk banyak tipe tanpa kehilangan pengecekan compiler. Kemampuan ini membuatmu bisa menulis komponen yang bisa dipakai ulang — wrapper response API, hasil pagination, repository — dan membaca kode Spring tanpa bingung melihat <T, ID> di mana-mana.
Konsep inti
Masalah sebelum generics: Object dan cast
Sebelum Java 5, cara membuat "wadah serbaguna" adalah memakai Object, tipe induk semua class. Wadah seperti ini menerima apa saja, tetapi pemanggil harus melakukan cast — dan compiler tidak bisa memeriksa apakah cast itu benar.
java
public class ObjectBoxDemo {
static class ObjectBox {
private Object value;
void set(Object value) { this.value = value; }
Object get() { return value; }
}
public static void main(String[] args) {
ObjectBox box = new ObjectBox();
box.set("KOPI-250");
String sku = (String) box.get(); // wajib cast
System.out.println(sku.toLowerCase());
box.set(85_000); // compiler diam saja
String oops = (String) box.get(); // meledak saat runtime
System.out.println(oops);
}
}Output:
text
kopi-250
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
at ObjectBoxDemo.main(ObjectBoxDemo.java:15)Kesalahannya ada di baris box.set(85_000), tetapi baru ketahuan saat baris lain dijalankan. Di aplikasi nyata, set dan get bisa berjarak ribuan baris dan berbeda class — bug semacam ini mahal dilacak.
Generic class: Box<T>
Generics memindahkan pengecekan itu ke compiler. T adalah type parameter: placeholder yang diisi tipe nyata saat class dipakai. Box<String> berarti "kotak yang T-nya String".
java
import java.util.List;
public class BoxDemo {
static class Box<T> {
private T value;
Box(T value) { this.value = value; }
T get() { return value; }
void set(T value) { this.value = value; }
boolean isEmpty() { return value == null; }
}
static <T> T first(List<T> items) {
if (items.isEmpty()) {
throw new IllegalArgumentException("List kosong");
}
return items.get(0);
}
public static void main(String[] args) {
Box<String> skuBox = new Box<>("KOPI-250");
String sku = skuBox.get(); // tanpa cast
System.out.println(sku.toLowerCase());
Box<Integer> qtyBox = new Box<>(3);
int qty = qtyBox.get() + 2; // auto-unboxing
System.out.println(qty);
String firstTag = first(List.of("promo", "kopi"));
Integer firstPrice = first(List.of(85_000, 40_000));
System.out.println(firstTag + " " + firstPrice);
System.out.println(BoxDemo.<String>first(List.of("eksplisit")));
}
}Output:
text
kopi-250
5
promo 85000
eksplisitSekarang skuBox.set(85_000) ditolak sebelum program sempat jalan:
text
BoxError.java:9: error: incompatible types: int cannot be converted to String
skuBox.set(85_000);
^Dua keuntungan sekaligus: type safety (tipe salah gagal dikompilasi) dan tanpa cast (get() langsung mengembalikan String). Type parameter hanya bisa diisi tipe referensi — Box<int> tidak sah, pakai Box<Integer>; autoboxing mengurus konversinya.
Generic method dan type inference
Method first di atas adalah generic method: ia mendeklarasikan type parameter sendiri, <T>, yang ditulis sebelum tipe kembalian. Class BoxDemo sendiri tidak generic — generic method boleh ada di class biasa, dan sering berupa method static utilitas.
Saat dipanggil, compiler menyimpulkan T dari argumen (type inference): first(List.of("promo", "kopi")) membuat T = String, sehingga hasilnya bisa langsung disimpan ke String. Penulisan eksplisit BoxDemo.<String>first(...) jarang dibutuhkan, tetapi berguna untuk dikenali saat membaca kode orang lain.
Diamond <>
Sejak Java 7, new Box<String>("KOPI-250") cukup ditulis new Box<>("KOPI-250"). Tanda <> (diamond) meminta compiler mengisi tipe dari konteks — sisi kiri deklarasi, tipe parameter method, atau argumen konstruktor. Bila sisi kiri memakai var, compiler mengambil tipe dari argumen: var box = new Box<>("KOPI-250") tetap Box<String>.
Hati-hati dengan var list = new ArrayList<>(); tanpa argumen: tidak ada petunjuk tipe, jadi compiler memilih ArrayList<Object> — type safety hilang diam-diam. Tulis List<String> list = new ArrayList<>();.
Beberapa type parameter
Class atau method boleh punya lebih dari satu type parameter, dipisah koma. Map<K, V> adalah contoh yang sudah kamu kenal. Record juga bisa generic:
java
import java.util.List;
import java.util.Map;
public class PairDemo {
record Pair<A, B>(A first, B second) {
<C> Pair<A, C> withSecond(C newSecond) {
return new Pair<>(first, newSecond);
}
}
static <K, V> Map<V, K> invert(Map<K, V> source) {
Map<V, K> result = new java.util.TreeMap<>();
for (Map.Entry<K, V> entry : source.entrySet()) {
result.put(entry.getValue(), entry.getKey());
}
return result;
}
public static void main(String[] args) {
Pair<String, Integer> line = new Pair<>("KOPI-250", 2);
System.out.println(line + " -> " + line.first() + " x" + line.second());
Pair<String, Long> priced = line.withSecond(170_000L);
System.out.println(priced);
var pairs = List.of(new Pair<>("TEH-100", true), new Pair<>("GULA-500", false));
System.out.println(pairs.get(0).second());
System.out.println(invert(Map.of("ID", "Indonesia", "SG", "Singapura")));
}
}Output:
text
Pair[first=KOPI-250, second=2] -> KOPI-250 x2
Pair[first=KOPI-250, second=170000]
true
{Indonesia=ID, Singapura=SG}Perhatikan withSecond: method di dalam class generic boleh menambah type parameter baru (<C>) di samping A dan B milik class. Hasilnya Pair<String, Long> — tipe yang berbeda dari Pair<String, Integer> asalnya.
Konvensi nama type parameter
Type parameter ditulis satu huruf kapital agar langsung terbedakan dari nama class:
| Nama | Arti umum | Contoh di JDK / Spring |
|---|---|---|
T | Type — tipe apa saja | Optional<T>, Box<T> |
E | Element di collection | List<E>, Set<E> |
K, V | Key dan Value | Map<K, V> |
R | Result / tipe hasil | Function<T, R> (bab lambda) |
S, U | tipe kedua, ketiga | <U> Optional<U> map(...) |
ID | tipe identitas entity | CrudRepository<T, ID> |
ID adalah pengecualian yang lazim di Spring Data: dua huruf, tetapi maknanya jelas. Hindari nama seperti Product sebagai type parameter — pembaca akan mengira itu class Product sungguhan.
Generic interface
Interface juga bisa generic, dan class yang mengimplementasikannya memilih: mengisi type parameter dengan tipe konkret (implements Comparable<Version>, seperti yang kamu lihat di bab sorting), atau tetap generic dan meneruskan parameternya (class InMemoryRepository<T, ID> implements Repository<T, ID>). Pola kedua dipakai Spring Data: JpaRepository<T, ID> adalah interface generic, dan kamu cukup menulis interface ProductRepository extends JpaRepository<Product, Long> untuk mendapat save, findById, findAll, dan lainnya dengan tipe yang tepat.
Contoh di dunia backend
Kita bangun versi mini pola Spring Data: interface Repository<T, ID>, implementasi in-memory yang generic, lalu subclass konkret untuk produk (ID String) dan pesanan (ID Long). Ditambah Page<T> — wrapper hasil pagination yang dipakai untuk tipe apa pun.
java
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
interface Repository<T, ID> {
T save(T entity);
T findById(ID id); // null bila tidak ada — versi yang lebih aman di bab Optional
List<T> findAll();
boolean existsById(ID id);
long count();
}
abstract class InMemoryRepository<T, ID> implements Repository<T, ID> {
private final Map<ID, T> store = new LinkedHashMap<>();
protected abstract ID idOf(T entity); // subclass yang tahu cara mengambil ID
@Override
public T save(T entity) {
store.put(idOf(entity), entity);
return entity;
}
@Override
public T findById(ID id) { return store.get(id); }
@Override
public List<T> findAll() { return new ArrayList<>(store.values()); }
@Override
public boolean existsById(ID id) { return store.containsKey(id); }
@Override
public long count() { return store.size(); }
}
record Product(String sku, String name, long price) {}
record Order(Long id, String customer, long total) {}
class ProductRepository extends InMemoryRepository<Product, String> {
@Override
protected String idOf(Product product) { return product.sku(); }
}
class OrderRepository extends InMemoryRepository<Order, Long> {
@Override
protected Long idOf(Order order) { return order.id(); }
}
record Page<T>(List<T> content, int page, int size, long totalElements) {
int totalPages() {
return (int) ((totalElements + size - 1) / size);
}
}
public class RepositoryDemo {
static <T> Page<T> paginate(List<T> all, int page, int size) {
int from = Math.min(page * size, all.size());
int to = Math.min(from + size, all.size());
return new Page<>(List.copyOf(all.subList(from, to)), page, size, all.size());
}
public static void main(String[] args) {
Repository<Product, String> products = new ProductRepository();
products.save(new Product("KOPI-250", "Kopi Gayo 250 g", 85_000));
products.save(new Product("TEH-100", "Teh Melati 100 g", 40_000));
products.save(new Product("GULA-500", "Gula Aren 500 g", 30_000));
Repository<Order, Long> orders = new OrderRepository();
orders.save(new Order(1001L, "andi", 170_000));
Product teh = products.findById("TEH-100");
System.out.println(teh.name() + " " + teh.price());
System.out.println(orders.existsById(1001L) + " " + orders.count());
// products.findById(1001L); // error kompilasi: long cannot be converted to String
Page<Product> page = paginate(products.findAll(), 1, 2);
System.out.println(page.content() + " | halaman " + page.page() + " dari " + page.totalPages());
}
}Output:
text
Teh Melati 100 g 40000
true 1
[Product[sku=GULA-500, name=Gula Aren 500 g, price=30000]] | halaman 1 dari 2Logika penyimpanan ditulis sekali di InMemoryRepository, lalu dipakai ulang untuk setiap entity. Subclass hanya mengisi type parameter dan memberi tahu cara mengambil ID. Compiler menjaga agar findById produk tidak menerima ID pesanan. Nomor halaman di Page dimulai dari 0 — sama seperti PageRequest.of(page, size) di Spring Data — sehingga halaman 1 adalah halaman kedua.
Pertanyaan interview
1. Apa keuntungan generics dibanding memakai Object?
Generics membuat compiler tahu tipe isi sebuah class atau method. Akibatnya, memasukkan tipe yang salah gagal dikompilasi alih-alih menjadi ClassCastException saat runtime, dan pemanggil tidak perlu menulis cast manual. Kode juga lebih jelas: Map<String, Product> langsung menjelaskan isinya, sedangkan Map mentah tidak.
2. Apa beda generic class dan generic method?
Generic class mendeklarasikan type parameter di nama class (class Box<T>), dan tipe itu ditentukan saat object dibuat (new Box<String>). Generic method mendeklarasikan type parameter sendiri sebelum tipe kembalian (<T> T first(List<T>)), dan tipenya disimpulkan per pemanggilan dari argumen. Generic method bisa berada di class non-generic, misalnya method utilitas static.
3. Kenapa JpaRepository punya dua type parameter, T dan ID?
T adalah tipe entity yang dikelola (misalnya Product), sedangkan ID adalah tipe primary key-nya (Long, UUID, atau String). Dengan dua parameter, method seperti findById(ID id) dan save(T entity) punya tipe yang tepat untuk setiap entity tanpa perlu menulis ulang implementasinya.
Jebakan umum
- Memakai tipe primitif sebagai type argument.
List<int>tidak sah; pakaiList<Integer>. Waspadai unboxingnullkeintyang memicuNullPointerException. var list = new ArrayList<>();tanpa petunjuk tipe → menjadiArrayList<Object>. Tulis tipe di sisi kiri.- Lupa
<T>sebelum tipe kembalian generic method. Compiler akan mengiraTadalah class dan melaporcannot find symbol. - Memberi nama type parameter seperti class sungguhan (
<Product>), sehingga ia "menutupi" classProductyang asli dan membingungkan pembaca. Pakai satu huruf kapital. - Kembali ke
Object+ cast "supaya fleksibel". Itu membuang semua pengecekan compiler; buat class-nya generic.
Ringkasan
- Generics memindahkan pengecekan tipe dari runtime ke compile time dan menghapus kebutuhan cast manual.
- Generic class mendeklarasikan type parameter di namanya (
Box<T>); generic method menuliskannya sebelum tipe kembalian (<T> T first(List<T>)). - Diamond
<>dan type inference membuat compiler mengisi tipe dari konteks; tanpa konteks, hasilnyaObject. - Boleh banyak type parameter (
Pair<A, B>,Map<K, V>); konvensi namaT,E,K,V,R, plusIDdi Spring. - Generic interface seperti
Repository<T, ID>memungkinkan satu implementasi dipakai ulang untuk banyak entity.