反射是 Java 語言區別於 C/C++ 等靜態語言的關鍵特性之一,它賦予了程序在運行時檢查、修改類結構與行為的能力。從框架開發(Spring、Hibernate)、動態代理、註解處理,到熱部署、插件化架構,反射無處不在。本文系統梳理 Class 對象獲取、成員訪問、方法調用、構造器實例化,並深入剖析泛型擦除與反射繞過編譯檢查的機制。
每個類在 JVM 中只有一個 Class 實例,代表該類的「類類型」。獲取方式有三種,本質相同:
// 1. 類字面量(編譯期已知,最常用、性能最好)
Class<User> c1 = User.class;
// 2. 實例 getClass()(運行期對象已存在)
User user = new User();
Class<? extends User> c2 = user.getClass();
// 3. 字串全限定名動態加載(框架核心,支援熱插拔)
Class<?> c3 = Class.forName("com.wx.model.User");
// 驗證單例性
System.out.println(c1 == c2); // true
System.out.println(c2 == c3); // true
關鍵點:Class.forName() 不僅返回類類型,更會觸發類的初始化(執行靜態塊、靜態字段賦值),這是靜態加載與動態加載的分水嶺。
Class<Integer> cInt = int.class; // 基本類型
Class<Double> cDouble = double.class;
Class<Void> cVoid = void.class; // void 關鍵字
Class<String> cStr = String.class; // 引用類型
System.out.println(cInt.getName()); // "int"
System.out.println(cStr.getSimpleName()); // "String" (不含包名)
public static void printClassMethods(Object obj) {
Class<?> clazz = obj.getClass();
System.out.println("類名:" + clazz.getName());
// getMethods() 獲取所有 public 方法(含父類)
Method[] methods = clazz.getMethods();
for (Method m : methods) {
// 返回類型
Class<?> returnType = m.getReturnType();
System.out.print(returnType.getName() + " ");
// 方法名
System.out.print(m.getName() + "(");
// 參數列表
Class<?>[] paramTypes = m.getParameterTypes();
for (int i = 0; i < paramTypes.length; i++) {
System.out.print(paramTypes[i].getName());
if (i < paramTypes.length - 1) System.out.print(", ");
}
System.out.println(")");
}
}
// 字段
Field[] fields = clazz.getDeclaredFields();
for (Field f : fields) {
System.out.println(f.getType().getName() + " " + f.getName());
}
// 構造器
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
for (Constructor<?> c : constructors) {
System.out.print(c.getName() + "(");
Class<?>[] params = c.getParameterTypes();
for (int i = 0; i < params.length; i++) {
System.out.print(params[i].getName());
if (i < params.length - 1) System.out.print(", ");
}
System.out.println(")");
}
class A {
public void print() { System.out.println("helloworld"); }
public void print(int a, int b) { System.out.println(a + b); }
public void print(String a, String b) {
System.out.println(a.toUpperCase() + "," + b.toLowerCase());
}
}
public class MethodDemo {
public static void main(String[] args) throws Exception {
A a1 = new A();
Class<?> c = a1.getClass();
// 1. 獲取方法:方法名 + 參數類型數組/可變參數
Method m1 = c.getMethod("print", int.class, int.class);
Method m2 = c.getMethod("print", String.class, String.class);
Method m3 = c.getMethod("print"); // 無參
// 2. 調用:invoke(實例, 參數...)
// 無返回值返回 null,有返回值返回具體值
m1.invoke(a1, 10, 20); // 輸出 30
m2.invoke(a1, "hello", "WORLD"); // 輸出 HELLO,world
m3.invoke(a1); // 輸出 helloworld
}
}
注意:getMethod 僅查找 public 方法;私有方法需用 getDeclaredMethod 並調用 setAccessible(true)。
Java 泛型採用擦除機制:編譯後泛型信息被擦除,運行期 List<String> 和 List<Integer> 共用同一個 Class 對象。
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // true
// 反射繞過編譯器泛型檢查
Method addMethod = list1.getClass().getMethod("add", Object.class);
addMethod.invoke(list1, 123); // 成功將 Integer 放入 List!
System.out.println(list1); // [123]
這證明了:泛型僅在編譯期生效,運行期可通過反射繞過類型約束。這也是為什麼集合框架內部大量使用 Object[] 存儲元素的原因。
/**
* 根據標準 JavaBean 屬性名獲取屬性值
* @param obj 目標對象
* @param propertyName 屬性名(如 "userName")
* @return 屬性值
*/
public static Object getPropertyValue(Object obj, String propertyName)
throws ReflectiveOperationException {
// 1. 拼接 getter 方法名:get + 首字母大寫 + 其餘
String getterName = "get" + propertyName.substring(0, 1).toUpperCase()
+ propertyName.substring(1);
// 2. 獲取 Method 對象(public 無參)
Method getter = obj.getClass().getMethod(getterName);
// 3. 反射調用
return getter.invoke(obj);
}
反射是 Java 動態能力的核心,掌握 Class、Method、Field、Constructor 四大核心類的協作模式,理解泛型擦除本質,能讓你在框架開發、動態代理、配置綁定等場景遊刃有餘。但「大權在手,責任在肩」——合理控制反射使用範圍,做好緩存與異常處理,才能兼得靈活與性能。