文章首發(fā)我的博客,歡迎訪問:https://blog.itzhouq.cn/annotation-reflection
最近又回顧了一下 Java 中的注解和反射知識點,注解在日常開發(fā)中使用很多,但是反射比較少。值得注意的是 Java 的各種框架底層源碼中大量使用了注解和反射,閱讀源碼,這些是基本功,面試中這部分內容也經(jīng)常問到。這里面概念不多,內容略微有些枯燥,但是通過一些簡單的例子,能讓我們明白一些基本概念和 API 的使用。所以,說到底,這篇博客只能算是一個簡單的筆記,希望對你有幫助。 以前也寫過枚舉類和注解的相關筆記,可以看看歷史文章 Java枚舉類和注解梳理。
1、什么是注解
注解 Annotation 是從JDK1.5 開始引入的新技術。
注解的作用:不是程序本身,可以對程序作出解釋,能被其他程序讀取到。
注解使用的位置:package、class、method、field 等上面,相當于給他們添加了額外的輔助信息。我們可以通過反射機制實現(xiàn)對這些元數(shù)據(jù)的訪問。
2、元注解
元注解的作用就是負責注解其他的注解,Java 定義了 4 個標準的 meta-Annotation類型,他們被用來提供對其他 Annotation 類型做說明。
-
Target:用于描述注解的使用范圍,注解可以用在什么地方。隨便點擊一個注解,查看源碼可以看到這個位置使用ElementType枚舉類表示,主要可以放在類上,方法上,屬性上等,我這就不細說了。 -
Retention:表示在什么級別保存該注解的信息,用于描述注解的生命周期。SOURCE < CLASS <RUNTIME。同樣看源碼,使用RetentionPolicy枚舉類表示,有SOURCE,CLASS,RUNTIME?;疽娒?。 -
Document:說明該注解會被包含在 Javadoc中。 -
Inherited:說明子類可以繼承父類的該注解。
3、自定義注解
使用 @interface 自定義注解時,自動繼承了 java.lang.annotation.Annotation 接口。
分析:
-
@interface:用來聲明一個注解,格式:public @interface 注解名{定義內容}; - 其中的每一個方法實際上就是一個配置參數(shù);
- 方法的名稱就是參數(shù)的名稱;
- 返回的類型就是參數(shù)的類型(返回值只能是基本類型、Class、String、enum);
- 可以通過 default 來聲明參數(shù)的默認值;
- 如果只有一個參數(shù)成員,一般參數(shù)名為 value;
- 注解元素必須要有值,我們定義注解元素時,經(jīng)常使用空字符串,0作為默認值。
// 自定義注解
public class Test {
@MyAnnotation(age = 18, name = "Hello")
public void test() {}
}
@interface MyAnnotation {
// 注解的參數(shù):參數(shù)類型 + 參數(shù)名();
String name() default "";
int age();
int id() default -1;
String[] schools() default {"清華大學", "北京大學"};
}
4、什么是反射
反射(Reflection):是 Java 被視為動態(tài)語言的關鍵,反射機制允許程序在執(zhí)行期借助于 Reflection API 取得任何類的內部信息,并能直接操作任意對象的內部屬性及方法。
加載完類后,在堆內存的方法區(qū)中就產(chǎn)生了一個 Class 類型的對象(一個類只有一個 Class 對象),這個對象包含了完整的類的結構信息。我們可以通過這個對象看到類的結構。這個對象就像一面鏡子,透過和鏡子看到類的結構。所以我們形象地稱之為反射。

Java發(fā)射的優(yōu)缺點:
優(yōu)點:可以實現(xiàn)動態(tài)創(chuàng)建對象和編譯,體現(xiàn)很大的靈活性;
缺點:對性能有影響,使用反射基本上是一種解釋操作,這類操作總是慢于直接執(zhí)行相同的操作。
public class Test01 {
public static void main(String[] args) throws ClassNotFoundException {
// 通過反射獲取類的Class對象
Class c1 = Class.forName("Test01");
Class c2 = Class.forName("Test01");
System.out.println(c1); // class Test01
// 一個類在內存中只有一個Class對象
// 一個類被加載后,類的整個結構都會被封裝在Class對象中。
System.out.println(c1.hashCode()); // 685325104
System.out.println(c2.hashCode()); // 685325104
}
}
class User{
private String name;
private int age;
public User() {}
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
5、Class 類及其創(chuàng)建方式
在Object類中定義了一下方法,此方法將被所有子類繼承。
public final Class getClass()
此方法的返回值類型是一個Class類,此類是Java反射的源頭,實際上所謂反射從程序的運行結果來看也很好理解:即:可以通過對象反射求出類的名稱。
Class 類的特點:
- Class本身也是一個類
- Class對象只能由系統(tǒng)建立對象
- 一個加載的類在 JVM 中只會有一個Class實例
- 一個Class對象對應的是一個加載到JVM中的一個.class文件
- 每個類的實例都會記得自己是由哪一個Class實例所生成的
- 通過Class可以完整的得到一個類中所有被加載的結構
- Class類是Reflection的根源針對任何你想要動態(tài)加載、運行的類,唯有先獲取相依的Class對象。
/**
* 測試Class類的創(chuàng)建方式有哪些
*/
public class Test02 {
public static void main(String[] args) throws ClassNotFoundException {
Person person = new Student();
System.out.println("這個人是:" + person.name); // 這個人是:學生
// 方式一:通過對象獲得
Class c1 = person.getClass();
System.out.println(c1.hashCode()); // 460141958
// 方式二:forName獲取
Class c2 = Class.forName("Student");
System.out.println(c2.hashCode()); // 460141958
// 方式三:通過類名.class獲取【最為安全可靠,性能最高】
Class c3 = Student.class;
System.out.println(c3.hashCode()); // 460141958
// 方式四:基本內置類型的包裝類都有一個Type屬性
Class c4 = Integer.TYPE;
System.out.println(c4); // int
}
}
class Person {
String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}
class Student extends Person{
public Student() {
this.name = "學生";
}
}
class Teacher extends Person{
public Teacher() {
this.name = "老師";
}
}
注意:Class 類創(chuàng)建方式很大概率在面試中會被問到。
6、類的加載過程和ClassLoader的理解

當程序主動使用某個類時,如果該類還未被加載到內存中,則系統(tǒng)會通過如下三個步驟來對該類進行初始化:

- 加載:將class文件字節(jié)碼內容加載到內存中,并將這些靜態(tài)數(shù)據(jù)轉換成方法區(qū)的運行時數(shù)據(jù)結構,然后生成一個代表這個類的java.lang.Class對象;
- 鏈接:將類的二進制代碼合并到JVM的運行狀態(tài)之中的過程。
- 驗證:確保的加載的類信息符合JVM規(guī)范,沒有安全方面的問題;
- 準備:正式為類變量(static)分配內存并設置類變量默認值的階段,這些內存都將在方法區(qū)中進行分配;
- 解析:虛擬機常量池內的符號引用(常量名)替換為直接引用(地址)的過程;
- 初始化:
- 執(zhí)行類構造器
<clinit>()方法的過程。類構造器<clinit>()方法是由編譯器自動收集類中所有類變量的賦值動作和靜態(tài)代碼塊中的語句合并產(chǎn)生的; - 當初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行初始化,則需要先觸發(fā)其父類的初始化;
- 虛擬機會保證一個類的
<clinit>()方法在多線程環(huán)境中被正確加鎖和同步。
- 執(zhí)行類構造器
public class Test03 {
public static void main(String[] args) {
A a = new A();
System.out.println(a.m);
}
}
class A {
static {
System.out.println("A類靜態(tài)代碼塊初始化");
m = 300;
}
static int m = 100;
public A () {
System.out.println("A類的無參構造初始化");
}
// A類靜態(tài)代碼塊初始化
// A類的無參構造初始化
// 100
/**
* 過程分析:
* 1. 加載到內存,會產(chǎn)生一個類對象Class對象
* 2. 鏈接, 鏈接結束后 m = 0
* 3. 初始化
* <clinit>(){
* System.out.println("A類靜態(tài)代碼塊初始化");
* m = 300;
* m = 100;
* }
*/
}

7、分析類的初始化
什么時候會發(fā)生類的初始化?
- 類的主動引用:一定會發(fā)生類的初始化
- 當虛擬機啟動,先初始化 main 方法所在的類
- new 一個類的對象
- 調用類的靜態(tài)方法(除了 final 常量)和靜態(tài)方法
- 使用
java.lang.reflection包的方法對類進行反射調用 - 當初始化一個類,如果其父類沒有被初始化,則會先初始化其父類
- 類的被動引用:不會發(fā)生類的初始化
- 當訪問一個靜態(tài)域時,只有真正聲明這個域的類才會被初始化。如:當通過子類引用父類的靜態(tài)變量,不會導致子類被初始化
- 通過數(shù)組定義類的引用,不會觸發(fā)類的初始化
- 引用常量不會觸發(fā)此類的初始化(常量在鏈接階段就存入調用類的常量池中了)。
// 測試類什么時候會被初始化
public class Test04 {
static {
System.out.println("Main類被加載");
}
public static void main(String[] args) throws ClassNotFoundException {
// 1. 主動引用
// Son son = new Son();
// Main類被加載
// 父類被加載
// 子類被加載
// 2. 反射也會產(chǎn)生主動引用
// Class.forName("Son");
// Main類被加載
// 父類被加載
// 子類被加載
// 3. 不會產(chǎn)生類的引用方法
// System.out.println(Son.b);
// Main類被加載
// 父類被加載
// 3
Son[] array = new Son[5]; // Main類被加載
}
}
class Father{
static int b = 3;
static {
System.out.println("父類被加載");
}
}
class Son extends Father{
static {
System.out.println("子類被加載");
m = 300;
}
static int m = 100;
static final int M = 1;
}
8、類加載器的作用
類加載器的作用:將class文件字節(jié)碼內容加載到內存中,并將這些靜態(tài)數(shù)據(jù)轉換成方法區(qū)的運行時數(shù)據(jù)結構,然后再堆中生成這個類的java.lang.Class對象,作為方法區(qū)中類數(shù)據(jù)的訪問入口。
類緩存:標準的 JavaSE 類加載器可以按要求查找類,但一旦某個類被加載到類加載器中,他將維持加載(緩存)一段時間。不過 JVM 垃圾回收機制可以回收這些Class對象。

類加載器作用:用來把類(class)裝載進內存的。JVM 規(guī)范定義了如下類型的類的加載器。

public class Test05 {
public static void main(String[] args) throws ClassNotFoundException {
// 獲取系統(tǒng)類加載器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// 獲取系統(tǒng)類加載器的父類---> 擴展類加載器
ClassLoader systemClassLoaderParent = systemClassLoader.getParent();
System.out.println(systemClassLoaderParent); // sun.misc.Launcher$ExtClassLoader@1b6d3586
// 測試當前類是哪個類加載器加載的
ClassLoader classLoader = Class.forName("Test05").getClassLoader();
System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// 測試 JDK 內置的類是哪個加載器加載的
ClassLoader loader = Class.forName("java.lang.Object").getClassLoader();
System.out.println(loader); // null 引導類加載器
}
}
9、獲取運行時類的完整結構
通過反射獲取運行時累的完整結構
Filed、Method、Constructor、Superclass、Interface、Annotation。
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
// 獲得類的信息
public class Test06 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException {
Class c1 = Class.forName("pojo.User");
// 獲得類的名稱
System.out.println(c1.getName()); // 包名+類名 pojo.User
System.out.println(c1.getSimpleName()); // 類名 User
// 獲得類的屬性
Field[] fields = c1.getFields(); // 只能找到public屬性
Field[] declaredFields = c1.getDeclaredFields(); // 找到全部屬性 private int pojo.User.id
// 獲得指定屬性的值
Field name = c1.getDeclaredField("name");
Method[] methods = c1.getMethods(); // 獲得本類及其父類的全部 public 方法
Method[] declaredMethods = c1.getDeclaredMethods(); // 獲得本類的所有方法
// 獲得指定方法
Method getName = c1.getMethod("getName", null);
Method setName = c1.getMethod("setName", String.class);
// 獲得指定構造器
Constructor[] constructors = c1.getConstructors();
Constructor[] declaredConstructors = c1.getDeclaredConstructors();
Constructor declaredConstructor = c1.getDeclaredConstructor(int.class, String.class);
System.out.println(declaredConstructor); // public pojo.User(int,java.lang.String)
}
}
10、動態(tài)創(chuàng)建對象執(zhí)行方法
有了 Class 對象之后,能做什么?
創(chuàng)建類的對象:調用 Class 對象的 newInstance() 方法。
- 類必須有一個無參構造器
- 類的構造器的訪問權限需要足夠。
思考?難道沒有無參構造器就不能創(chuàng)建對象了嗎?
答:只要操作的時候明確的調用類中的構造器。并將參數(shù)傳遞進去之后,才可以實例化操作。
import pojo.User;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
// 通過反射動態(tài)的創(chuàng)建對象
public class Test07 {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
// 獲得 Class 對象
Class c1 = Class.forName("pojo.User");
// 構造一個對象
User user = (User) c1.newInstance(); // 本質調用了類的無參構造器
System.out.println(user); // User{id=0, name='null'}
// 通過構造器創(chuàng)建對象
Constructor constructor = c1.getDeclaredConstructor(int.class, String.class);
User jack = (User) constructor.newInstance(1, "Jack");
System.out.println(jack); // User{id=1, name='Jack'}
// 通過反射調用普通方法
User user2 = (User) c1.newInstance();
// 通過反射調用一個方法
Method setName = c1.getDeclaredMethod("setName", String.class);
setName.invoke(user2, "小米");
System.out.println(user2); // User{id=0, name='小米'}
// 通過反射操作屬性
User user3 = (User) c1.newInstance();
Field name = c1.getDeclaredField("name");
// 不能直接操作私有屬性,需要取消安全監(jiān)測
name.setAccessible(true);
name.set(user3, "小黑");
System.out.println(user3); // User{id=0, name='小黑'}
}
}
Method 和 Field、Constructor 對象都有 setAccessible() 方法。
setAccessible 作用是啟動和禁用訪問安全檢查的開關。
參數(shù)值為 true 則指示反射的對象在使用時應該取消 Java 語言訪問檢查。
11、反射操作泛型(generics)
Java 采用泛型擦除的機制來引入泛型,Java 中的泛型僅僅是給編譯器 javac 使用的,確保數(shù)據(jù)的安全性和免去強制類型轉換問題,但是一旦編譯完成,所有和泛型有關的類型全部擦除。
為了通過反射操作這些類型, Java 新增了 ParameteriedType, GenericArrayType, TypeVariable和WildcardType 幾種類型來代表不能被歸一到 Class 類中的類型但是又和原始類型齊名的類型。
-
ParameteriedType:表示一個參數(shù)化類型,比如Collection<String> -
GenericArrayType:表示一個元素類型是參數(shù)化類型或者類型變量的數(shù)組類型 -
TypeVariable:是各種類型變量的公共父接口 -
WildcardType:代表一種通配符類型的表達式。
// 獲取泛型(generics)信息
public class Test08 {
public void test01 (Map<String, User> map, List<User> list) {
System.out.println("test01");
}
public Map<String, User> test02 () {
System.out.println("test02");
return null;
}
public static void main(String[] args) throws NoSuchMethodException {
Method method = Test08.class.getMethod("test01", Map.class, List.class);
Type[] genericParameterTypes = method.getGenericParameterTypes();
for (Type genericParameterType : genericParameterTypes) {
System.out.println(genericParameterType);
// java.util.Map<java.lang.String, pojo.User>
// java.util.List<pojo.User>
if (genericParameterType instanceof ParameterizedType) {
Type[] actualTypeArguments = ((ParameterizedType) genericParameterType).getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments) {
System.out.println("##" + actualTypeArgument);
// java.util.Map<java.lang.String, pojo.User>
// ##class java.lang.String
// ##class pojo.User
// java.util.List<pojo.User>
// ##class pojo.User
}
}
}
System.out.println("=======================");
// 獲取返回值泛型
Method method1 = Test08.class.getMethod("test02", null);
Type genericReturnType = method1.getGenericReturnType();
if (genericReturnType instanceof ParameterizedType) {
Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments) {
System.out.println("##" + actualTypeArgument);
// ##class java.lang.String
// ##class pojo.User
}
}
}
}
12、反射操作注解
練習:ORM
使用注解和反射完成類和表結構映射。類和表結構對應,屬性和字段對應、對象和記錄對應。

import java.lang.annotation.*;
import java.lang.reflect.Field;
// 練習反射操作注解
public class Test09 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
Class c1 = Class.forName("Student");
// 通過反射獲得注解
Annotation[] annotations = c1.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation); // @TableMy(value=db_student)
}
// 獲得注解的 value 的值
TableMy tableMy = (TableMy) c1.getAnnotation(TableMy.class);
String value = tableMy.value();
System.out.println(value); // db_student
// 獲得類指定的注解
Field f = c1.getDeclaredField("id");
FiledMy annotation = f.getAnnotation(FiledMy.class);
System.out.println(annotation.columnName()); // db_id
System.out.println(annotation.type()); // int
System.out.println(annotation.length()); // 10
}
}
// 類名的注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface TableMy{
String value();
}
// 屬性的注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface FiledMy {
String columnName();
String type();
int length();
}
@TableMy("db_student")
class Student {
@FiledMy(columnName = "db_id", type = "int", length = 10)
private int id;
@FiledMy(columnName = "db_age", type = "int", length = 10)
private String name;
@FiledMy(columnName = "db_name", type = "varchar", length = 3)
private int age;
public Student() {
}
public Student(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}