最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java反射與注解的詳細講解(含代碼)

 更新時間:2026年06月04日 09:52:12   作者:likerhood  
注解和反射應(yīng)該說是后期JAVA開發(fā)里面比較重要的技術(shù)了,通過注解和反射可以簡化許許多多的操作和步驟,最重要的是為JAVA的運行提供了更多的靈活性,這篇文章主要介紹了Java反射與注解的詳細講解,需要的朋友可以參考下

前言

初學 Java SE 時,我們習慣手動 new 對象、調(diào)用方法,一切直白可控。

但進入 Spring、MyBatis 等框架后,@Autowired 自動裝配依賴,@Transactional 即可生效事務(wù)……看似神奇,其實底層依然是 反射與注解的默默支撐。

一、 為什么有反射?

在真正接觸框架之前,很多開發(fā)者會覺得反射又慢又麻煩,直接 new 對象不好嗎?

要回答這個問題,我們需要從代碼的演進思維說起。

1.1 硬編碼

日常業(yè)務(wù)開發(fā)中,我們絕大部分代碼都是硬編碼。所謂硬編碼,就是在寫代碼的時候(編譯期),就已經(jīng)完全確定了要使用哪個具體的類。

代碼演示:

// 定義一個接口
public interface UserService {
    void serve();
}

// 具體的實現(xiàn)類 A
public class UserServiceImpl implements UserService {
    @Override
    public void serve() { System.out.println("執(zhí)行舊版業(yè)務(wù)邏輯..."); }
}

// 我們的主程序
public class MyApp {
    public static void main(String[] args) {
        // 【硬編碼痛點】:主程序強依賴了 UserServiceImpl 這個具體的實現(xiàn)類
        UserService service = new UserServiceImpl(); 
        service.serve();
    }
}

分析:

編譯器清清楚楚地知道你要實例化 UserServiceImpl。這種寫法的優(yōu)點是運行速度極快、類型絕對安全。

但它的缺點是極度僵化(高耦合)。假設(shè)隨著業(yè)務(wù)發(fā)展,你寫了一個更好的實現(xiàn)類 UserServiceNewImpl,你必須打開 MyApp.java 的源代碼,把 new UserServiceImpl() 改成 new UserServiceNewImpl(),然后重新編譯整個項目,打包,停機發(fā)布。這在大型工程中是不可接受的。

1.2 軟編碼

為了解決硬編碼帶來的耦合問題,前人想出了一個聰明的辦法:把“到底用哪個類”的決定權(quán),從源代碼里抽離出來,放到外部的配置文件里。

代碼演示:

假設(shè)我們有一個外部配置文件 application.properties

# 在不修改源代碼的情況下,只需修改這里的字符串即可切換實現(xiàn)類
service.class=com.example.UserServiceNewImpl

我們的主程序變成這樣:

public class MyApp {
    public static void main(String[] args) throws Exception {
        // 1. 讀取外部配置文件,拿到類名的字符串(這里用硬編碼字符串模擬讀取過程)
        String className = "com.example.UserServiceNewImpl"; 

        // 2. 利用反射,把一個“字符串”變成活生生的“對象”
        Class<?> clazz = Class.forName(className);
        UserService service = (UserService) clazz.getDeclaredConstructor().newInstance();
        
        service.serve();
    }
}

分析:

這時候,MyApp.java 在編譯階段根本不知道自己最終要創(chuàng)建什么對象,它只依賴 UserService 這個抽象的接口。

只有在程序運行那一刻,讀取到了配置文件的字符串,才知道要加載誰。

這就是軟編碼。而反射機制,就是跨越“字符串”和“運行時對象”之間那道鴻溝的唯一橋梁。

1.3 框架的核心訴求:對象工廠模式

Spring 這樣的 IoC(控制反轉(zhuǎn))框架,本質(zhì)上就是一個超級、通用的對象工廠(Object Factory)。

框架的作者(比如 Spring 的創(chuàng)始人 Rod Johnson)在寫 Spring 的時候,完全不可能預(yù)知你(使用者)未來會寫出什么奇怪的類(比如 OrderController、UserMapper)。

框架為了能幫你統(tǒng)一管理這些未知的對象,只能采用這樣的設(shè)計:

代碼演示(極簡版 Spring 容器雛形):

import java.util.HashMap;
import java.util.Map;

// 這是一個通用的對象工廠(模擬 Spring 的 ApplicationContext)
public class SimpleBeanFactory {
    
    // 容器內(nèi)部維護一個 Map,存放:<Bean的名字, 類的全限定名>
    private Map<String, String> beanDefinitionMap = new HashMap<>();

    public SimpleBeanFactory() {
        // 框架在啟動時,會去掃描你的 XML 配置文件或 @Component 注解
        // 解析出你需要管理的類,并放進 Map 中
        beanDefinitionMap.put("userService", "com.example.UserServiceNewImpl");
        beanDefinitionMap.put("orderService", "com.example.OrderServiceImpl");
    }

    // 核心的工廠方法:你給我一個 Bean 的名字,我給你一個可用的對象
    public Object getBean(String beanName) throws Exception {
        String className = beanDefinitionMap.get(beanName);
        if (className == null) {
            throw new RuntimeException("容器中找不到對應(yīng)的 Bean 定義");
        }
        
        // 框架利用反射,動態(tài)地替你把對象造出來
        return Class.forName(className).getDeclaredConstructor().newInstance();
    }
}

// 開發(fā)者使用框架時的樣子:
public class DeveloperApp {
    public static void main(String[] args) throws Exception {
        SimpleBeanFactory factory = new SimpleBeanFactory();
        
        // 開發(fā)者再也不需要自己去 new 對象了,全權(quán)交給框架的反射去創(chuàng)建
        UserService userService = (UserService) factory.getBean("userService");
        userService.serve();
    }
}

總結(jié):

從這三個步驟的演進,你就能深刻體會到:沒有反射,就沒有軟編碼;沒有軟編碼,就寫不出任何具備擴展性的現(xiàn)代 Java 框架。

反射就是為了解決在“運行時處理未知類”這一核心訴求而誕生的。

二、 什么是反射?

2.1 萬物皆對象

在 Java 中,“一切皆對象”這條箴言需要更精確的表述:不僅類的實例是對象,類本身也是一個對象。

在常規(guī)的面向?qū)ο缶幊讨?,我們的認知是這樣的:

  • 類(Class) 是圖紙、是模具。
  • 對象(Object) 是根據(jù)圖紙造出來的汽車、是模具刻出來的餅干。

圖紙就是圖紙,汽車就是汽車,兩者截然不同。但是,在 JVM 的世界里,為了能讓程序在運行時去讀取和修改圖紙,JVM 把“圖紙”本身也封裝成了一個“產(chǎn)品”(對象)。

2.1.1 概念理解

假設(shè)你現(xiàn)在就是詹姆斯·高斯林(Java 之父),你需要設(shè)計一種數(shù)據(jù)結(jié)構(gòu),用來在內(nèi)存中保存程序員寫出來的 User、Order、Product 等各種各樣的類的信息。你會怎么設(shè)計?

你肯定會專門寫一個類,用來描述“類”這種抽象事物。這個用來描述“類”的類,就叫 java.lang.Class。

它的底層邏輯(偽代碼)大概長這樣:

// 這是 JDK 源碼里真實存在的 java.lang.Class 類的縮影
public final class Class<T> {
    private String name;           // 記錄類的全限定名(如 com.example.User)
    private Field[] fields;        // 記錄類里面有哪些屬性
    private Method[] methods;      // 記錄類里面有哪些方法
    private Constructor[] cons;    // 記錄類里面有哪些構(gòu)造器
    private Class superclass;      // 記錄它的父類是誰
    
    // ... 各種獲取這些信息的方法
}

在 Java 中,實例化(創(chuàng)建對象)不僅發(fā)生在業(yè)務(wù)層面,也發(fā)生在底層系統(tǒng)層面。這其實是一個三層結(jié)構(gòu)

  1. 第一層:萬物之母 java.lang.Class 。

    • 這是 JDK 源碼里自帶的一個類。你可以把它理解為一家「專門生產(chǎn) 3D 打印機的工業(yè)母機」。

    • 它的工作只有一個:定義“3D打印機(圖紙)”應(yīng)該長什么樣(必須有類名、有字段數(shù)組、有方法數(shù)組)。

  2. 第二層:圖紙對象 User.class (中間態(tài))

    • 當 JVM 啟動,加載到你寫的 User 代碼時,這臺“工業(yè)母機”就自動啟動了,它為你生產(chǎn)出了一臺「印著 User 標簽的專屬 3D 打印機」**。 **

    • 這臺印著 User 標簽的 3D 打印機,本身就是一個實實在在的 產(chǎn)品(對象)!它是 java.lang.Class 生產(chǎn)出來的實例。

  3. 第三層:業(yè)務(wù)對象 new User() (最終態(tài))。

    • 現(xiàn)在,你在代碼里寫下了 new User()(或者用反射 newInstance),就相當于啟動了這臺專屬的 User 3D 打印機。

    • 最后打印出了一個個具體的「塑料小人(user 業(yè)務(wù)對象)」。

代碼證明如下:

User userObj = new User(); // 這是一個 User 對象

// 證明 1:userObj 是誰的實例?
System.out.println(userObj instanceof User); // true (塑料小人是 3D 打印機印出來的)

Class<?> userClassObj = User.class; // 這是一個 Class 對象

// 證明 2:User.class 是誰的實例?
System.out.println(userClassObj instanceof java.lang.Class); // true (3D 打印機是工業(yè)母機造出來的)

2.1.2 底層分析

當你的程序啟動,執(zhí)行到 User user = new User(); 時,背后發(fā)生了兩件事:

  1. 印戶口本(僅一次):JVM 的類加載器讀取了 User.class 字節(jié)碼文件,并在堆內(nèi)存中實例化了一個 java.lang.Class 對象。這個對象里的 name 屬性被填上了 "com.example.User",fields 數(shù)組被填上了你寫的屬性。這面“鏡子”就此誕生。
  2. 造汽車(可無數(shù)次):JVM 照著這面鏡子(圖紙),在堆內(nèi)存的另一塊區(qū)域,為你開辟空間,真正造出了那個包含數(shù)據(jù)的 user 實例對象。

核心總結(jié)User 類的實例是 user 對象;而 User 類本身,則是 java.lang.Class 這個類的實例對象!這就是所謂的“萬物皆對象”。

2.2 Class 對象

2.2.1 概念理解

Class 是 Java 中用來描述「類本身」的最終類java.lang.Class),無法被繼承。

每一個被 JVM 加載的類 / 類型,在內(nèi)存中都會生成唯一的 Class 對象,這個對象封裝了該類的全部元數(shù)據(jù)信息(類名、父類、接口、字段、方法、構(gòu)造器等),是 Java 反射、動態(tài)代理、多態(tài)實現(xiàn)的核心基石。

通俗類比:

概念生活類比核心作用
類(Class 代碼)建筑圖紙定義「結(jié)構(gòu)、能力」的模板
Class 對象(類對象)圖紙的官方唯一備案檔案記錄圖紙的全部信息,全局唯一
實例對象(Instance)按圖紙建成的房子按模板生成的具體個體,有獨立屬性

三個核心概念的精準區(qū)分

概念代碼示例核心本質(zhì)生命周期
類(普通類)public class Dog {}編譯期生成的.class字節(jié)碼文件,是代碼模板編譯期生成,運行期被 JVM 加載
Class 對象(類對象)Dog.class類加載后 JVM 在內(nèi)存中生成的唯一對象,代表類本身和類的生命周期綁定,類卸載時回收
實例對象new Dog()基于類模板創(chuàng)建的具體實例,堆內(nèi)存中的對象隨 new 創(chuàng)建,無引用時被 GC 回收

Class 對象的核心特性(必記)

  1. 絕對單例性(有邊界):同一個類,在同一個 JVM 實例 + 同一個類加載器下,只會生成一個 Class 對象,== 比對永遠返回 true。
  2. 全類型覆蓋:所有 Java 類型都有對應(yīng)的 Class 對象,包括:
  3. 只讀性:Class 對象封裝的類元數(shù)據(jù),運行期常規(guī)手段無法修改(僅可通過字節(jié)碼注入等特殊技術(shù)修改)。
  4. JVM 私有創(chuàng)建:Class 類的構(gòu)造方法是 private,只有 JVM 能創(chuàng)建 Class 對象,開發(fā)者無法通過new Class()手動生成。

2.2.2 使用操作

關(guān)于泛型 Class<?>

平時代碼中??吹?Class<?>。

這里的 ? 是通配符,表示未知的類型。因為我們在編寫反射通用代碼時,通常不知道要處理的具體是哪個類,使用 Class<?> 可以避免編譯器的未經(jīng)檢查警告(Unchecked Warning),保持代碼的泛型安全。

有三種常見方式,應(yīng)用場景各不相同:

// 1. Class.forName("全限定名"):最常用,適用于完全不知道類,只有類名字符串的情況(如加載數(shù)據(jù)庫驅(qū)動)
Class<?> clazz1 = Class.forName("com.example.User");

// 2. 類名.class:適用于在編譯前就已經(jīng)明確知道并且引入了該類的情況(常用于鎖對象、參數(shù)類型傳遞)
Class<?> clazz2 = User.class;

// 3. 對象.getClass():適用于已經(jīng)有了該類的實例對象,想反推它屬于哪個類
User user = new User();
Class<?> clazz3 = user.getClass();

一個類在同一個類加載器中,只有一個唯一的 Class 對象。無論通過哪種方式獲取,得到的都是同一個實例:

Class<User> c1 = User.class;
Class<?> c2 = Class.forName("com.example.User"); 
Class<?> c3 = new User().getClass(); 
System.out.println(c1 == c2);    // true
System.out.println(c2 == c3);    // true

這個唯一性保障了同步鎖(User.class 可作為同步監(jiān)視器)、類型比較(instanceof 底層用到 Class 對象)等基礎(chǔ)機制的正確運行。

2.3 反射的定義

反射(Reflection) 是 Java 語言提供的一種機制。它允許程序在運行時(Runtime)

  1. 動態(tài)探索:獲取任意一個類的所有內(nèi)部結(jié)構(gòu)(包含私有屬性、方法、構(gòu)造器、注解等)。

  2. 動態(tài)操作:動態(tài)調(diào)用任意一個對象的方法,修改其屬性。

理解編譯期和運行期,可以看我的另一篇博客:java的運行機制:編譯期、運行期和半編譯半解釋性

反射的本質(zhì)

  1. 先拿到目標類的 Class 對象(反射的入口)

  2. 通過 Class 對象獲取它的 “全息檔案”(動態(tài)探索)

  3. 通過檔案里的信息,去操作具體的實例對象(動態(tài)操作)

2.4 反射的核心 API 詳解

java.lang.reflect 包下提供了反射的核心組件:

  1. Constructor:用來「造對象」,核心是 newInstance,私有構(gòu)造器記得 setAccessible(true)

  2. Field:用來「改屬性」,核心是 getset,static 字段傳 null。

  3. Method:用來「調(diào)方法」,核心是 invoke,異常記得用 getCause() 解包。

它們就是三個類,和 StringArrayList 一樣:

java.lang.reflect.Constructor<T>   // 代表一個構(gòu)造方法
java.lang.reflect.Field            // 代表一個成員變量(字段)
java.lang.reflect.Method           // 代表一個成員方法

當你調(diào)用 clazz.getConstructor() 時,JVM 并不是返回了一團神秘的東西,而是老老實實地 new 了一個 Constructor 對象給你。

同理,getDeclaredField() 返回一個 Field 實例,getMethod() 返回一個 Method 實例。

Java 虛擬機(JVM)在加載一個類時,不僅會把類本身封裝成一個 Class 對象,還會把這個類里面的每一個成員(構(gòu)造器、字段、方法)都解析并封裝成對應(yīng)的 Constructor、FieldMethod 對象。

真正的構(gòu)造器、字段、方法的定義,在 JVM 底層是以 C++ 數(shù)據(jù)結(jié)構(gòu)的形式,存放在元空間(Metaspace) 的。

這些原始數(shù)據(jù)對 Java 程序是不可見的。

Constructor、FieldMethod 的作用,就是把那些黑盒的底層元數(shù)據(jù),包裝成 Java 對象,讓你能通過調(diào)用這些對象的方法,去間接操縱底層的實際內(nèi)容。

在常規(guī)開發(fā)中,我們是這樣想的:“我要調(diào)用 user 對象的 getName 方法”。

在反射開發(fā)中,思維模型要倒轉(zhuǎn)過來:“我拿到了一個名為 getName 的 Method 對象,我要讓這個 Method 對象去作用于 user 實例”。

2.4.1Constructor(構(gòu)造器對象)

用于動態(tài)創(chuàng)建實例。

  • getConstructor(Class<?>... parameterTypes):獲取 public 構(gòu)造器。
  • getDeclaredConstructor(...):獲取所有聲明的構(gòu)造器(包含 private)。
  • newInstance(Object... initargs):執(zhí)行構(gòu)造器創(chuàng)建對象。
  • setAccessible(true): 突破 private 限制

**注意:**當我們使用單例模式或某些隱藏了實例創(chuàng)建邏輯的框架時,類的構(gòu)造器往往是 private 的。此時,必須配合 setAccessible(true) 使用,否則會拋出 IllegalAccessException。

public class User {
    private String name;
    // 私有構(gòu)造器
    private User(String name) {
        this.name = name;
    }
}

// 反射調(diào)用測試
Class<?> clazz = User.class;
// 1. 獲取私有構(gòu)造器 (注意:這里用 getDeclaredConstructor)
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
// 2. 打破封裝,允許訪問私有成員
constructor.setAccessible(true);
// 3. 動態(tài)實例化對象
Object userInstance = constructor.newInstance("ReflectUser");

2.4.2Field(字段對象)

用于動態(tài)讀取和修改屬性。

  • getDeclaredField(String name):獲取當前類聲明的指定名稱的字段(無論什么訪問修飾符)。
  • setAccessible(true)反射的破壁者。操作 private 字段前必須調(diào)用此方法打破封裝。
  • get(Object obj):獲取指定對象 obj 中該字段的值。
  • set(Object obj, Object value):將指定對象 obj 中該字段的值修改為 value。

注意:

  • 靜態(tài)(static)字段操作:由于靜態(tài)字段屬于類級別而不是對象級別,所以在調(diào)用 get()set() 時,傳入的對象實例可以直接寫 null。
  • 類型安全檢查:如果 set 時傳入的數(shù)據(jù)類型與字段實際類型不匹配,會引發(fā) IllegalArgumentException。
// 假設(shè)前面的 userInstance 已經(jīng)創(chuàng)建
Field nameField = clazz.getDeclaredField("name");
// 打破私有屬性的封裝
nameField.setAccessible(true);

// 讀取屬性值
String currentName = (String) nameField.get(userInstance);
System.out.println("修改前: " + currentName); // 輸出: ReflectUser

// 修改屬性值
nameField.set(userInstance, "NewName");
System.out.println("修改后: " + nameField.get(userInstance)); // 輸出: NewName

2.4.3Method(方法對象)

用于動態(tài)執(zhí)行方法。

  • getDeclaredMethod(String name, Class<?>... parameterTypes):根據(jù)方法名和參數(shù)列表獲取當前類聲明的方法。
  • invoke(Object obj, Object... args)核心執(zhí)行方法。
    • obj:要執(zhí)行該方法的具體對象實例。
    • args:方法運行所需的實際參數(shù)。

注意:

  • 靜態(tài)方法調(diào)用:如果反射調(diào)用的方法是 static 的,invoke 的第一個參數(shù) objnull 即可(因為靜態(tài)方法不依賴于具體的對象實例)。
  • 異常解包機制:當被反射調(diào)用的目標方法內(nèi)部拋出異常時,反射機制會將其包裝成一個 InvocationTargetException 拋出。要獲取目標方法真實拋出的業(yè)務(wù)異常,必須調(diào)用 exception.getCause() 進行解包。

代碼:

public class Calculator {
    private int add(int a, int b) {
        return a + b;
    }
    
    public static void staticMethod() {
        System.out.println("靜態(tài)方法被調(diào)用");
    }
}

Class<?> calcClass = Calculator.class;
Object calcInstance = calcClass.getDeclaredConstructor().newInstance();

// 1. 調(diào)用私有實例方法
Method addMethod = calcClass.getDeclaredMethod("add", int.class, int.class);
addMethod.setAccessible(true);
// obj 傳實例對象,后面?zhèn)鲗崊?
Object result = addMethod.invoke(calcInstance, 10, 20); 
System.out.println("10 + 20 = " + result);

// 2. 調(diào)用靜態(tài)方法
Method staticMethod = calcClass.getDeclaredMethod("staticMethod");
// 靜態(tài)方法不需要實例,obj 傳 null
staticMethod.invoke(null);

三、注解

如果說反射是底層執(zhí)行的引擎,那么注解就是指揮引擎運行的“路標”。

3.1 為什么要使用注解?

在早期 Spring 或 Struts,軟編碼大量依賴龐大且冗長的 XML 配置文件。

這被稱為“XML 地獄”。開發(fā)人員不得不在 Java 代碼和 XML 文件之間反復橫跳。

案例:聲明一個 UserService,并給它注入一個 Dao 對象。

Java 代碼本身極其干凈(但毫無線索):

public class UserService {
    private UserDao userDao;
    // 必須寫又長又啰嗦的 setter 方法供 XML 調(diào)用
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

對應(yīng)的 XML 配置文件(災(zāi)難的開始):

<!-- applicationContext.xml 里面可能堆積了成百上千個這樣的標簽 -->
<bean id="userDao" class="com.example.dao.UserDaoImpl" />

<bean id="userService" class="com.example.service.UserService">
    <!-- 靠全限定類名和字符串來匹配,極易寫錯 -->
    <property name="userDao" ref="userDao" />
</bean>

痛點解析:

  1. 反復橫跳:開發(fā)者看代碼時,不知道這個類被誰用、怎么配的;看 XML 時,又不知道這個類的具體邏輯。必須在 .java.xml 文件之間瘋狂切換。
  2. 重構(gòu)災(zāi)難:一旦修改了類名或包名,如果在 XML 里忘了同步修改(由于是純字符串,編譯器不會報錯),程序在運行時就會直接崩潰。
  3. 極度臃腫:大型項目的 XML 文件動輒幾千行,甚至需要拆分成幾十個 XML 互相引入,維護成本極高。

注解的出現(xiàn)解決了這個問題:它允許將元數(shù)據(jù)(Metadata)直接標記在代碼本身上(類、方法、字段上),做到了配置與代碼的高內(nèi)聚,大大提升了可讀性和開發(fā)效率。

// @Service 路標:告訴 Spring 引擎,這是一個業(yè)務(wù)邏輯類,請幫我實例化
@Service
public class UserService {
    
    // @Autowired 路標:告訴 Spring 引擎,請把我需要的 UserDao 自動拿過來給我
    @Autowired
    private UserDao userDao;
    
    // 連 setter 方法都不需要寫了!
}

一個極其容易陷入誤區(qū)的點:很多人認為 @Autowired 能自動注入對象,是因為這個注解里寫了注入的代碼。

其實不是,因為:“注解本身不包含任何邏輯,它沒有任何行為能力。”

注意:注解在本質(zhì)上只是一個標記、一個標簽。就好比你在超市商品上貼了一個“打八折”的紅色貼紙(注解)。貼紙本身不能改變商品的價格,真正改變價格的,是收銀員(反射機制)看到了這個貼紙,然后按照收銀機里的邏輯給你減錢。

3.2 注解是什么

3.2.1 基本概念

注解(Annotation),也叫元數(shù)據(jù)(Metadata)。簡單來說,它就是一種代碼級別的說明和標簽。

它是 JDK 1.5 引入的特性,與類、接口、枚舉屬于同一個層次。它可以像“便利貼”一樣,貼在包、類、字段、方法、局部變量、方法參數(shù)等的前面。

那么,貼上這些“便利貼”到底有什么用呢?總體來說,注解的作用可以分為三大類:

  1. 編譯檢查:通過注解讓編譯器實現(xiàn)基本的編譯檢查(比如告訴編譯器這個方法是重寫的,幫我盯緊點)。
  2. 編寫文檔:配合工具(如 javadoc),通過代碼里標識的注解生成 API 文檔。
  3. 代碼分析與運行(核心):在運行期,框架可以通過反射機制讀取這些注解,進而動態(tài)地改變程序的運行邏輯。

JDK 提供的最常用的三個基礎(chǔ)注解,它們主要負責編譯階段的檢查與提示。

@Override: 標記在成員方法上,用于標識當前方法是重寫父類(父接口)方法,編譯器在對該方法進行編譯時會檢查是否符合重寫規(guī)則,如果不符合,編譯報錯。
@Deprecated: 用于標記當前類、成員變量、成員方法或者構(gòu)造方法過時如果開發(fā)者調(diào)用了被標記為過時的方法,編譯器在編譯期進行警告。
@SuppressWarnings: 壓制警告注解,可放置在類和方法上,該注解的作用是阻止編譯器發(fā)出某些警告信息。

1. JDK自帶三種注解

  1. @Override:這個注解只能標記在方法上,用來告訴編譯器:“我這個方法是重寫父類(或接口)的,請幫我檢查一下方法名和參數(shù)對不對”。

    public class Animal {
        public void speak() {
            System.out.println("動物發(fā)聲");
        }
    }
    
    public class Dog extends Animal {
        // 加上 @Override 后,如果下面方法名錯寫成 spaek(),編譯器會直接標紅報錯!
        // 如果不加注解,寫成 spaek() 編譯器只會認為你寫了一個新方法,導致潛藏 bug。
        @Override 
        public void speak() {
            System.out.println("汪汪汪");
        }
    }
    
  2. @Deprecated:隨著軟件迭代,有些老方法或老類存在缺陷或性能問題,不推薦大家繼續(xù)使用了,但為了向前兼容又不能直接刪除。這時就可以用它。

    public class StringUtils {
        
        // 標記為已過時。如果你在其他地方調(diào)用這個方法,IDEA 會給它劃上一條刪除線
        @Deprecated
        public static void oldFormat() {
            System.out.println("這個方法太老了,不安全,別用了!");
        }
        
        public static void newFormat() {
            System.out.println("請使用全新的格式化方法");
        }
    }
    
  3. @SuppressWarnings:有時候我們的代碼會產(chǎn)生一些黃色的警告(比如未使用的變量、未經(jīng)檢查的泛型轉(zhuǎn)換等)。如果你確信代碼沒問題,可以用它來讓編譯器“閉嘴”。

    public class Test {
        // 壓制“未使用”和“未檢查的強轉(zhuǎn)”警告,讓代碼看起來干干凈凈
        @SuppressWarnings({"unused", "unchecked"})
        public void doSomething() {
            int a = 10; // 雖然沒被使用,但加了注解后,IDEA 不會再報黃色的警告
            
            List list = new ArrayList();
            list.add("Hello");
            // 泛型強轉(zhuǎn)警告也會被壓制
            List<String> strList = (List<String>) list; 
        }
    }
    

2. 現(xiàn)代 Web 框架中的注解

JDK 內(nèi)置注解只是牛刀小試。

注解真正的威力,在于結(jié)合反射機制,徹底改變了現(xiàn)代 Java 后端(如 Spring MVC)的開發(fā)模式,改進了老版本的了讓人頭疼的“XML 配置文件”。

在現(xiàn)代開發(fā)中,注解就像是指揮框架運行的路標。

// @RestController:告訴 Spring,這是一個處理 Web 請求的類,幫我把它變成一個 Bean 放到容器里。
@RestController
// @RequestMapping:定義這個類處理所有以 "/api/users" 開頭的請求。
@RequestMapping("/api/users")
public class UserController {

    // @Autowired:自動裝配。告訴 Spring,請幫我在內(nèi)存里找一個 UserService 對象,強行塞到這個字段里,不用我自己 new 了!
    @Autowired
    private UserService userService;

    // @GetMapping:處理 HTTP 的 GET 請求。如果瀏覽器訪問 "/api/users/1",就會進到這里。
    // @PathVariable:告訴框架,把 URL 里的 "{id}" 摳出來,賦值給入?yún)?userId。
    @GetMapping("/{id}")
    public User getUserById(@PathVariable("id") Long userId) {
        return userService.findById(userId);
    }
}

在這個例子中,代碼極其簡潔,沒有任何多余的配置代碼。

這全都歸功于注解:開發(fā)者負責貼標簽,Spring 框架在底層負責用反射讀取標簽并執(zhí)行對應(yīng)的操作。

3.3 注解的本質(zhì)

注解并不是什么憑空出現(xiàn)的全新數(shù)據(jù)結(jié)構(gòu),它的本質(zhì),就是一個普普通通的 Java 接口(Interface)。

我們可以通過代碼反編譯,來拆穿編譯器的這層“語法糖”。

假設(shè)我們自定義了一個極其簡單的注解:

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;

// 【步驟一:定義注解】
@Retention(RetentionPolicy.RUNTIME) // 關(guān)鍵:告訴JVM在運行時保留這個注解
public @interface MyAnnotation {
    String value() default "Hello";
    int age();
}

// 【步驟二:使用注解(貼標簽)】
@MyAnnotation(value = "張三", age = 18) // 貼在類上
public class Student {
    
    @MyAnnotation(age = 20) // 貼在方法上(value有默認值,可以省略)
    public void study() {
        System.out.println("好好學習!");
    }
}

// 【步驟三:解析注解(讀標簽)】
public class Main {
    public static void main(String[] args) throws Exception {
        
        // 1. 獲取類上的注解數(shù)據(jù)
        MyAnnotation classAnno = Student.class.getAnnotation(MyAnnotation.class);
        if (classAnno != null) {
            System.out.println("類上的注解 -> 值: " + classAnno.value() + ", 年齡: " + classAnno.age());
        }

        // 2. 獲取方法上的注解數(shù)據(jù)
        Method method = Student.class.getMethod("study");
        MyAnnotation methodAnno = method.getAnnotation(MyAnnotation.class);
        if (methodAnno != null) {
            System.out.println("方法上的注解 -> 值: " + methodAnno.value() + ", 年齡: " + methodAnno.age());
        }
    }
}

賦值:在 Student 類中寫下 @MyAnnotation(value = "張三", age = 18) 時,其實就是給注解的屬性賦了值。

取值:在 Main 類中,通過 getAnnotation() 方法,就可以把剛剛賦進去的 "張三"18 動態(tài)拿出來。

本質(zhì):注解本身是不包含任何邏輯的,它僅僅是用來“存數(shù)據(jù)”的標簽。必須配合反射把數(shù)據(jù)取出來,它才有意義。

當我們使用 javac 將其編譯成 .class 字節(jié)碼文件,然后再使用 javap -c 命令對其進行反編譯后,你看到的真實代碼形態(tài)是這樣的:

// 是一個接口
public interface MyAnnotation extends java.lang.annotation.Annotation {
    
    // 所謂的屬性,變成了抽象方法
    public abstract String value();
    
    public abstract int age();
}

解析:

  1. 隱式繼承:所有的注解類,在編譯后都會自動繼承 java.lang.annotation.Annotation 接口。
  2. **@interface **:僅僅是 Java 編譯器提供的一個語法糖,作用是告訴編譯器:“嘿,請幫我把這個類編譯成繼承了 Annotation 的接口”。

3.4 為什么注解里的“屬性”要帶括號

注解聲明屬性時,一般要在后面加上一對括號,比如 String value(); 而不是 String value;。

現(xiàn)在知道了它的本質(zhì)是接口,一切就豁然開朗了:

  • 在 Java 的接口中,不允許定義普通的成員變量(只能定義 public static final 的常量)。
  • 為了讓接口能夠“攜帶”數(shù)據(jù),Java 的設(shè)計者只能利用抽象方法來模擬屬性。
  • 當你寫下 String value(); 時,你不是在定義一個變量,而是在定義一個有返回值的方法。當你在代碼中使用 @MyAnnotation(value = "Test") 時,其實是在告訴 JVM:“待會兒如果有人調(diào)用 value() 這個方法,你就給他返回 "Test" 這個字符串”。

3.5 既然是接口,那是誰實例化了它

我們知道,接口是不能直接 new 的。既然注解是接口,那么當我們在運行期通過反射調(diào)用 clazz.getAnnotation(MyAnnotation.class) 時,拿到的那個注解對象,究竟是個什么東西?是誰實現(xiàn)了這個接口?

答案是:JDK 動態(tài)代理。

當我們在代碼里寫下 @MyAnnotation 并通過反射去獲取它時,JVM 在底層做了一系列非常隱蔽的操作:

  1. 解析常量池:JVM 會讀取 .class 文件常量池中關(guān)于這個注解的配置信息。
  2. 生成代理類:JVM 在內(nèi)存中動態(tài)生成了一個實現(xiàn)了 MyAnnotation 接口的代理類(類似于 $Proxy1)。
  3. 攔截方法調(diào)用:這個代理類內(nèi)部持有一個 AnnotationInvocationHandler(注解調(diào)用處理器)。這個處理器內(nèi)部維護了一個 Map<String, Object>,里面存儲了你寫在代碼里的屬性名和屬性值(比如 key="value", value="Test")。
  4. 返回結(jié)果:當你調(diào)用 myAnnotation.value() 時,實際上是被代理類攔截了。代理類會去那個 Map 里面,根據(jù)方法名(“value”)取出對應(yīng)的值,并返回給你。

所以整體流程是這樣的:

  • 編寫期:你寫下 @interface,看起來像個標簽。
  • 編譯期:編譯器把它轉(zhuǎn)成 interface extends Annotation,屬性變方法。
  • 運行期:JVM 通過 動態(tài)代理 生成這個接口的實現(xiàn)類對象,并通過反射把你在代碼里配的值,存進代理對象內(nèi)部的 Map 里。最后,你調(diào)用方法獲取值。

簡單總結(jié):

  1. 表象:反射拿到的并不是真正的注解實現(xiàn)類,而是 JVM 在運行期臨時生成的一個動態(tài)代理對象(Proxy)。

  2. 核心:這個代理對象內(nèi)部持有一個名為 AnnotationInvocationHandler 的調(diào)用處理器。

  3. 機制:這個處理器里面維護了一個 Map,里面存著我們寫注解時賦的值(比如 age=18)。當我們通過反射調(diào)用注解的屬性方法時,其實是被這個處理器攔截了,它直接從 Map 里查出對應(yīng)的值返回給我們。所以調(diào)用注解方法,本質(zhì)上是在查 Map 字典!

3.6 自定義注解的使用舉例

“元注解”就是用來標注注解的注解,它們定義了你的自定義注解的作用域和生命周期。

  1. @Retention(保留策略)

    定義注解存活的時間,由 RetentionPolicy 枚舉控制:

    • SOURCE:只在源碼中存在,編譯后被抹除(如 @Override,只是給編譯器看的)。

    • CLASS:保留到字節(jié)碼文件中,但 JVM 運行時不加載。

    • RUNTIME最重要! 保留到運行時,可以通過反射讀取。所有需要配合反射使用的注解,必須設(shè)置為 RUNTIME。

  2. @Target(作用目標)

定義注解可以貼在誰身上,由 ElementType 枚舉控制:

  • TYPE:類、接口。

  • FIELD:字段屬性。

  • METHOD:方法。

  • PARAMETER:方法參數(shù)。

代碼測試:

import java.lang.annotation.*;

// 元注解:指定注解的保留策略為運行時(只有這個策略才能被反射讀?。?
@Retention(RetentionPolicy.RUNTIME)
// 元注解:指定注解可以標注在字段和方法上
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface MyAnnotation {
    // 注解的屬性,本質(zhì)上是接口的抽象方法
    boolean required() default true;
}


// 類上的注解
@MyAnnotation("類注解")
public class User {
    // 字段上的注解
    @MyAnnotation("字段注解")
    private String name;

    // 方法上的注解
    @MyAnnotation("方法注解")
    public void sayHi() {}
}

// 反射讀取注解
public class AnnotationReadDemo {
    public static void main(String[] args) throws Exception {
        Class<User> userClass = User.class;

        // 1. 讀取類上的注解
        if (userClass.isAnnotationPresent(MyAnnotation.class)) {
            MyAnnotation classAnnotation = userClass.getAnnotation(MyAnnotation.class);
            System.out.println("類注解值:" + classAnnotation.value());
        }

        // 2. 讀取字段上的注解
        Field nameField = userClass.getDeclaredField("name");
        if (nameField.isAnnotationPresent(MyAnnotation.class)) {
            MyAnnotation fieldAnnotation = nameField.getAnnotation(MyAnnotation.class);
            System.out.println("字段注解值:" + fieldAnnotation.value());
        }

        // 3. 讀取方法上的注解
        Method sayHiMethod = userClass.getDeclaredMethod("sayHi");
        if (sayHiMethod.isAnnotationPresent(MyAnnotation.class)) {
            MyAnnotation methodAnnotation = sayHiMethod.getAnnotation(MyAnnotation.class);
            System.out.println("方法注解值:" + methodAnnotation.value());
        }
    }
}

四、 反射與注解的結(jié)合

只有反射,只能盲目地操作類;

只有注解,它只是一堆不會生效的死文本;

當反射去讀取注解時,兩個機制就產(chǎn)生了非常好的效果。

4.1 結(jié)合的底層原理

怎么讓注解生效?原理其實極其樸素,歸納起來就是“一套 API,三個步驟”。

在程序運行時,框架的底層其實就在無限循環(huán)地做下面這三件事:

  1. 全面掃描(找位置):利用反射 API 獲取目標類的所有結(jié)構(gòu)(Class.getDeclaredFields()、getDeclaredMethods() 等)。
  2. 精準定位(看路標):遍歷這些結(jié)構(gòu),調(diào)用 isAnnotationPresent(TargetAnnotation.class) 方法,像雷達一樣探測上面有沒有貼特定的注解。
  3. 強行介入(做邏輯):一旦探測到注解(返回 true),就利用 getAnnotation() 把注解里配置的數(shù)據(jù)(如 value)讀出來。然后根據(jù)業(yè)務(wù)需求,利用反射 API(如 Field.set()Method.invoke())打破封裝,執(zhí)行特殊的邏輯。

4.2 Spring Boot 中的經(jīng)典應(yīng)用:依賴注入

在 Spring 時代之前,我們寫代碼最大的痛苦就是無休止地 new 對象。而現(xiàn)在,我們在開發(fā) Spring MVC 或 Spring Boot 項目時,極其常用 @Autowired 注解來實現(xiàn)依賴注入(Dependency Injection)。

當我們寫下這行極簡的代碼時:

@Autowired 
private UserService userService;

表面上看: 我們只是聲明了一個私有變量,連對象都沒創(chuàng)建,更沒有寫 setter 方法。

實際上: Spring 容器在后臺偷偷把一切都打理好了,簡單來說步驟如下:

流程如下:

4.3 代碼實戰(zhàn)

結(jié)合我們上面學到的所有知識,寫一個最簡陋的 DI 容器來展示這一過程:

import java.lang.reflect.Field;

// 1. 定義測試類
class OrderService {
    public void pay() { System.out.println("訂單支付成功!"); }
}

class OrderController {
    @AutoInject // 使用我們剛才自定義的注解
    private OrderService orderService;
    
    public void doOrder() {
        orderService.pay();
    }
}

// 2. 手寫迷你容器
public class MiniDiContainer {
    public static void main(String[] args) throws Exception {
        // 第一步:反射實例化目標對象 (模擬框架啟動)
        Class<?> controllerClass = OrderController.class;
        Object controllerObj = controllerClass.getDeclaredConstructor().newInstance();
        
        // 第二步:依賴注入的核心邏輯
        Field[] fields = controllerClass.getDeclaredFields();
        for (Field field : fields) {
            // 檢查字段上是否有 @AutoInject 注解
            if (field.isAnnotationPresent(AutoInject.class)) {
                // 拿到需要注入的類型 (這里是 OrderService)
                Class<?> fieldType = field.getType();
                // 實例化依賴的對象
                Object dependencyObj = fieldType.getDeclaredConstructor().newInstance();
                
                // 暴力注入!
                field.setAccessible(true);
                field.set(controllerObj, dependencyObj);
                System.out.println("完成自動注入: " + field.getName());
            }
        }
        
        // 第三步:測試調(diào)用
        OrderController controller = (OrderController) controllerObj;
        controller.doOrder(); // 如果不報錯并打印成功,說明注入生效了!
    }
}

總結(jié)

  1. 反射:讓程序在運行時動態(tài)探索類的結(jié)構(gòu),并操作對象,實現(xiàn)軟編碼與高度可擴展的框架設(shè)計。
  2. 注解:提供元數(shù)據(jù)標記,告訴框架“做什么”,自身不具邏輯,通過反射才能生效。
  3. 結(jié)合應(yīng)用:現(xiàn)代框架(如 Spring)靠反射讀取注解,實現(xiàn)依賴注入、事務(wù)控制、路由映射等“開箱即用”的功能。

核心理念:反射是執(zhí)行引擎,注解是指令標記,兩者結(jié)合才成就現(xiàn)代 Java 框架的便捷。

到此這篇關(guān)于Java反射與注解詳細講解的文章就介紹到這了,更多相關(guān)Java反射與注解內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Java面向?qū)ο蠡A(chǔ)詳解

    Java面向?qū)ο蠡A(chǔ)詳解

    這篇文章主要介紹了JJava面向?qū)ο蟮幕A(chǔ),文中有非常詳細的代碼示例,對正在學習java基礎(chǔ)的小伙伴們有很好的幫助,需要的朋友可以參考下
    2021-10-10
  • Java設(shè)置httponly?cookie的實現(xiàn)示例

    Java設(shè)置httponly?cookie的實現(xiàn)示例

    本文主要介紹了Java設(shè)置httponly?cookie的實現(xiàn)示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2022-08-08
  • java語言圖形用戶登錄界面代碼

    java語言圖形用戶登錄界面代碼

    這篇文章主要為大家詳細介紹了java語言圖形用戶登錄界面代碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2016-06-06
  • java web實現(xiàn)簡單留言板功能

    java web實現(xiàn)簡單留言板功能

    這篇文章主要為大家詳細介紹了java web實現(xiàn)簡單留言板功能,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-11-11
  • java 打造阻塞式線程池的實例詳解

    java 打造阻塞式線程池的實例詳解

    這篇文章主要介紹了java 打造阻塞式線程池的實例詳解的相關(guān)資料,這里提供實例幫助大家,理解這部分知識,需要的朋友可以參考下
    2017-07-07
  • SpringBoot 代碼風格推薦

    SpringBoot 代碼風格推薦

    本文主要介紹了Spring Boot 項目定義了統(tǒng)一的包結(jié)構(gòu)與命名規(guī)范,涵蓋分層架構(gòu)、實體對象、工具類的命名與放置規(guī)則,強調(diào)各層對象隔離與技術(shù)層分,下面就來詳細的介紹一下
    2026-05-05
  • Mybatis之解決collection一對多問題(顯示的結(jié)果沒有整合到一起)

    Mybatis之解決collection一對多問題(顯示的結(jié)果沒有整合到一起)

    這篇文章主要介紹了Mybatis之解決collection一對多問題(顯示的結(jié)果沒有整合到一起),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-03-03
  • MyBatis一級與二級緩存相關(guān)配置

    MyBatis一級與二級緩存相關(guān)配置

    mybatis-plus是一個Mybatis的增強工具,在Mybatis的基礎(chǔ)上只做增強不做改變,為簡化開發(fā)、提高效率而生,這篇文章帶你了解Mybatis的一級和二級緩存
    2023-01-01
  • SpringBoot實現(xiàn)掃碼登錄的示例代碼

    SpringBoot實現(xiàn)掃碼登錄的示例代碼

    本文主要介紹了SpringBoot實現(xiàn)掃碼登錄的示例代碼,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-03-03
  • Java文件下載ZIP報錯:Out of Memory的問題排查

    Java文件下載ZIP報錯:Out of Memory的問題排查

    本文主要介紹了Java項目中下載大文件(超過2G的ZIP文件)時出現(xiàn)內(nèi)存溢出(OutOfMemory:JavaHeapSpace)的問題,具有一定的參考價值,感興趣的可以了解一下
    2025-01-01

最新評論

抚顺县| 成安县| 泸溪县| 沂水县| 班戈县| 侯马市| 盐源县| 奉化市| 北票市| 仪征市| 富阳市| 綦江县| 夏津县| 胶州市| 曲阜市| 石嘴山市| 广灵县| 新龙县| 北海市| 中西区| 广平县| 榕江县| 汽车| 武城县| 土默特右旗| 瓦房店市| 富源县| 浦江县| 南宫市| 田林县| 曲阳县| 枣强县| 利辛县| 疏附县| 长治市| 视频| 海宁市| 福贡县| 泽库县| 平远县| 高邮市|