Retensi mengatur kapan annotation terbaca; @Target mengatur di mana annotation boleh dipasang. Tanpa pembatasan, sebuah annotation bisa saja terpasang di class padahal maknanya hanya relevan untuk method — dan kesalahan semacam itu baru terasa ketika framework gagal menemukan metadata yang dicarinya. Java menyelesaikan masalah ini dengan meta-annotation @Target yang menerima nilai enum ElementType: compiler javac menolak pemasangan annotation di jenis deklarasi yang tidak terdaftar, lengkap dengan pesan error yang menyebut pelanggarannya. Pada Chapter ini kita akan memetakan nilai-nilai ElementType, lalu menguji pembatasan target pada class, field, constructor, parameter, sampai komponen record.

Peta ElementType #

Setiap nilai ElementType mewakili satu jenis deklarasi tempat annotation boleh dipasang. Nilai yang paling sering dipakai sehari-hari dapat diringkas begini:

Nilai ElementTypeBoleh dipasang pada
TYPEclass, interface, enum, record, annotation type
FIELDfield milik class atau enum (bukan variabel lokal)
METHODmethod (termasuk method default dan static di interface)
PARAMETERparameter method atau constructor
CONSTRUCTORconstructor
RECORD_COMPONENTkomponen dari deklarasi record
LOCAL_VARIABLEvariabel lokal di dalam method
ANNOTATION_TYPEdeklarasi annotation type (dipakai meta-annotation seperti @Target sendiri)
TYPE_PARAMETERparameter tipe generik, misalnya <T>
TYPE_USEsetiap pemakaian tipe — tipe kembalian, cast, generic argument, dst.

Satu annotation dapat menerima beberapa lokasi sekaligus dengan menuliskan array nilai, misalnya @Target({ElementType.TYPE, ElementType.METHOD}). Sebaliknya, annotation tanpa @Target sama sekali boleh dipasang di semua jenis deklarasi — inilah perilaku bawaan yang dipakai annotation sederhana seperti @Bebas pada contoh di bawah.

Membatasi dan Membaca Annotation Berbagai Target #

Program berikut mendeklarasikan lima annotation dengan target berbeda-beda, memasang masing-masing pada tempat yang benar, lalu membaca keberadaannya lewat reflection. Annotation Bebas sengaja tidak diberi @Target sebagai pembanding:

import java.lang.annotation.*;
import java.lang.reflect.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@interface Fungsional {}   // hanya untuk deklarasi tipe dan method

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface KunciUtama {}   // hanya untuk field

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.CONSTRUCTOR)
@interface DibangunOleh {} // hanya untuk constructor

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@interface Divalidasi {}   // hanya untuk parameter method

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.RECORD_COMPONENT)
@interface Terindeks {}    // hanya untuk komponen record

@Retention(RetentionPolicy.RUNTIME)
@interface Bebas {}        // tanpa @Target: boleh di mana saja

@Fungsional
@Bebas
class Katalog {
    @KunciUtama
    private String kode;

    @DibangunOleh
    public Katalog() {}

    @Fungsional
    public void tampil() {}

    public void simpan(@Divalidasi @Bebas String kode) {
        this.kode = kode;
    }
}

record Buku(@Terindeks String judul) {}

public class ContohTarget {
    public static void main(String[] args) throws Exception {
        System.out.println("@Fungsional di class Katalog : "
                + (Katalog.class.getAnnotation(Fungsional.class) != null));

        Field kode = Katalog.class.getDeclaredField("kode");
        System.out.println("@KunciUtama di field kode    : "
                + (kode.getAnnotation(KunciUtama.class) != null));

        Constructor<?> ctor = Katalog.class.getDeclaredConstructor();
        System.out.println("@DibangunOleh di constructor : "
                + (ctor.getAnnotation(DibangunOleh.class) != null));

        Method simpan = Katalog.class.getDeclaredMethod("simpan", String.class);
        Annotation[][] param = simpan.getParameterAnnotations();
        System.out.println("@Divalidasi di parameter     : "
                + (param.length > 0 && param[0].length > 0));

        RecordComponent rc = Buku.class.getRecordComponents()[0];
        System.out.println("@Terindeks di record judul   : "
                + (rc.getAnnotation(Terindeks.class) != null));
    }
}

Output yang Dihasilkan:

text
@Fungsional di class Katalog : true
@KunciUtama di field kode    : true
@DibangunOleh di constructor : true
@Divalidasi di parameter     : true
@Terindeks di record judul   : true

Mari kita telaah bagian-bagian penting kode di atas:

  1. Baris 3-4 (@Target({ElementType.TYPE, ElementType.METHOD})): annotation Fungsional hanya boleh dipasang pada deklarasi tipe atau method. Pemakaan ganda pada class Katalog dan method tampil() di bawah sama-sama sah.
  2. Baris 8-21 (empat @Target satu nilai): KunciUtama, DibangunOleh, Divalidasi, dan Terindeks masing-masing dikunci pada satu jenis deklarasi — field, constructor, parameter, dan komponen record. Compiler menolak pemakaian di luar lokasi tersebut.
  3. Baris 23-24 (@interface Bebas {}): tanpa @Target, annotation berlaku di semua jenis deklarasi. Karena fleksibilitas ini, Bebas berhasil dipasang pada class sekaligus parameter.
  4. Baris 55-56 (getDeclaredMethod("simpan", String.class)): mencari method berdasarkan nama dan tipe parameter secara eksplisit. Pencarian lewat getDeclaredMethods() lalu mengambil elemen pertama tidak stabil karena urutan hasilnya tidak dijamin.
  5. Baris 57-59 (getParameterAnnotations()): mengembalikan array dua dimensi — satu array annotation untuk setiap parameter method. Baris ini memeriksa bahwa parameter pertama (dan satu-satunya) membawa annotation.
  6. Baris 61-62 (getRecordComponents()[0]): record Buku punya satu komponen judul, sehingga indeks 0 aman dipakai. RecordComponent adalah refleksi khusus komponen record sejak Java 16.

Semua hasil tercetak true karena setiap annotation terpasang pada lokasi yang diizinkan targetnya — dan seluruhnya ber-retensi RUNTIME agar terbaca. Kombinasi @Retention + @Target adalah pasangan standar pada hampir semua annotation framework.

Compiler Menolak Salah Tempat #

Pembatasan target ditegakkan sejak kompilasi. Berkas berikut memasang @HanyaUntukMethod — yang targetnya ElementType.METHOD — pada deklarasi class:

java TargetSalah.java
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface HanyaUntukMethod {}

@HanyaUntukMethod // salah tempat: annotation ini hanya boleh di method
public class TargetSalah {
    public static void main(String[] args) {}
}

Compiler javac menolaknya dengan pesan yang kita verifikasi langsung:

text
TargetSalah.java:7: error: annotation interface not applicable to this kind of declaration
@HanyaUntukMethod // salah tempat: annotation ini hanya boleh di method
^
1 error

Pesan annotation interface not applicable to this kind of declaration menyebut bahwa annotation tidak berlaku untuk jenis deklarasi tersebut — tanpa menyebut lokasi yang benar, jadi kita perlu membaca kembali deklarasi @Target-nya untuk tahu tempat yang diizinkan. Tanpa @Target sama sekali, kesalahan penempatan seperti ini tidak pernah tertangkap: annotation terpasang di mana saja dan framework pembacanya harus berhati-hati memfilter.

Dengan @Retention dan @Target kita menguasai dua sisi deklaratif annotation. Pertanyaan berikutnya adalah sisi pemakaian: bagaimana sebuah program sungguhan membaca metadata ini dan mengubah perilakunya? Chapter berikutnya membangun mini-framework pembaca annotation yang menghasilkan DDL SQL dari class domain — teknik inti di balik ORM seperti Hibernate.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

ContohTarget.java

Unduh berkas
java
import java.lang.annotation.*;
import java.lang.reflect.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@interface Fungsional {}   // hanya untuk deklarasi tipe dan method

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface KunciUtama {}   // hanya untuk field

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.CONSTRUCTOR)
@interface DibangunOleh {} // hanya untuk constructor

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@interface Divalidasi {}   // hanya untuk parameter method

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.RECORD_COMPONENT)
@interface Terindeks {}    // hanya untuk komponen record

@Retention(RetentionPolicy.RUNTIME)
@interface Bebas {}        // tanpa @Target: boleh di mana saja

@Fungsional
@Bebas
class Katalog {
    @KunciUtama
    private String kode;

    @DibangunOleh
    public Katalog() {}

    @Fungsional
    public void tampil() {}

    public void simpan(@Divalidasi @Bebas String kode) {
        this.kode = kode;
    }
}

record Buku(@Terindeks String judul) {}

public class ContohTarget {
    public static void main(String[] args) throws Exception {
        System.out.println("@Fungsional di class Katalog : "
                + (Katalog.class.getAnnotation(Fungsional.class) != null));

        Field kode = Katalog.class.getDeclaredField("kode");
        System.out.println("@KunciUtama di field kode    : "
                + (kode.getAnnotation(KunciUtama.class) != null));

        Constructor<?> ctor = Katalog.class.getDeclaredConstructor();
        System.out.println("@DibangunOleh di constructor : "
                + (ctor.getAnnotation(DibangunOleh.class) != null));

        Method simpan = Katalog.class.getDeclaredMethod("simpan", String.class);
        Annotation[][] param = simpan.getParameterAnnotations();
        System.out.println("@Divalidasi di parameter     : "
                + (param.length > 0 && param[0].length > 0));

        RecordComponent rc = Buku.class.getRecordComponents()[0];
        System.out.println("@Terindeks di record judul   : "
                + (rc.getAnnotation(Terindeks.class) != null));
    }
}

TargetSalah.java

Unduh berkas
java
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface HanyaUntukMethod {}

@HanyaUntukMethod // salah tempat: annotation ini hanya boleh di method
public class TargetSalah {
    public static void main(String[] args) {}
}