JAVA之反射

Java 中的 Class 类以及反射机制详解

1. Class 类的本质

Java 中,除了基本数据类型 (int, boolean, float 等) 之外,所有的类型都是类 (class) 或接口 (interface) 。当 JVM 加载某个类时,它会为该类生成一个对应的 Class 对象。Class 类的一个对象本质上是 JVM 对该类的抽象表示,封装了有关该类的信息,如类名、包名、父类、接口、方法和字段等。

Class 类的定义

Class 类是 Java 中一个非常特殊的类,它用于表示类的类型。它的构造器是私有的,说明该类的对象只能由 JVM 创建,不能通过用户代码直接实例化。它的定义如下:

1
2
3
public final class Class<T> {
private Class() {} // 私有构造器,不能通过代码直接实例化
}

1. public final class Class<T>:

  • public: 该类是公有的,可以在 Java 中任何地方访问。

  • final: 表示 Class 类不能被继承。这样设计是为了确保 Java 中的 Class 实例行为不被子类改变,防止对反射机制的破坏。

  • <T>: 泛型参数,表示 Class 类是一个泛型类,T 代表具体的类型。例如,Class<String> 表示 String 类的 Class 实例,Class<Integer> 表示 Integer 类的 Class 实例。

    1
    Class<String> cls = String.class;

2. private Class():

  • private: 这是一个私有构造器,意味着 Class 类的对象不能通过代码直接使用 new Class() 进行实例化。这样做的目的是为了防止程序员随意创建 Class 类的实例,避免破坏 Java 的运行时类型系统。所有的 Class 对象实例化过程由 JVM 负责处理。

    为什么是私有的?

    • 类加载机制的控制权: Class 对象的创建与管理是 JVM 的职责,它通过类加载器动态加载类,并为每个加载的类创建对应的 Class 实例。这个私有构造器确保了外部程序无法通过常规手段创建 Class 实例,从而防止程序在运行时随意操作类型。
    • 唯一性: 在 JVM 中,每个类在内存中有且只有一个 Class 实例与之对应,使用私有构造方法能够保证 Class 对象的唯一性。每当 JVM 加载一个类时,都会自动为其创建唯一的 Class 实例。

3. 如何创建 Class 实例?

虽然 Class 的构造方法是私有的,但我们可以通过以下方式获取类的 Class 实例:

  • 静态方式:

    1
    Class<String> cls = String.class;  // 获取 String 类的 Class 实例
  • 通过实例对象:

    1
    2
    String s = "Hello";
    Class<? extends String> cls = s.getClass(); // 获取对象的 Class 实例
  • 动态加载类:

    1
    Class<?> cls = Class.forName("java.lang.String");  // 通过类名字符串获取 Class 实例

4. Class 实例的作用:

Class 类的实例用于在运行时获取某个类的详细信息,通常用于 反射。通过 Class 实例,可以获取类的:

  • 类名
  • 包名
  • 超类和接口
  • 字段和方法
  • 构造方法等

举个简单的例子,获取 String 类的详细信息:

1
2
3
4
5
Class<?> cls = String.class;
System.out.println("类名: " + cls.getName());
System.out.println("包名: " + cls.getPackage().getName());
System.out.println("父类: " + cls.getSuperclass().getName());
System.out.println("是否是接口: " + cls.isInterface());

5. JVM 和 Class 的关系:

JVM 在运行时加载 Java 类并为每个类创建一个对应的 Class 对象。这个 Class 对象包含了类的所有结构信息,JVM 在内存中管理这些对象。通过反射机制,开发者可以在运行时获取和操作类的内部信息(如方法、字段、构造函数等),这是 Java 动态语言特性的基础。

每当 JVM 加载一个类时,都会为该类创建一个 Class 实例并关联这个类的所有信息。这些信息包括类名、包名、继承关系、实现的接口、字段、方法等等。JVM 为每个类(或者接口)创建的 Class 实例是唯一的,即对于每一个类,只会有一个 Class 对象表示它。

2. 反射机制

反射是一种非常强大的机制,它允许程序在运行时动态地获取一个类的内部信息,并且能动态创建对象、调用方法、访问字段等。反射使得 Java 程序能够在运行时动态操作代码,而不仅仅是在编译时操作。

获取 Class 对象的三种方法

Java 提供了三种方式来获取一个类的 Class 对象:

  1. 通过 .class 语法

    1
    Class<String> cls1 = String.class;

    这种方式最直接,通过类的静态属性 class 来获取 Class 对象。

  2. 通过对象实例调用 getClass()

    1
    2
    String s = "Hello";
    Class<? extends String> cls2 = s.getClass();

    通过已有的对象调用其 getClass() 方法,可以得到对象实际的类型。

  3. 通过 Class.forName(String className)

    1
    Class<?> cls3 = Class.forName("java.lang.String");

    这种方式可以通过类的全限定名(包名 + 类名)动态加载类,尤其适用于程序在运行时根据条件动态加载类。

这三种方式获取 Class 对象在 Java 中使用场景不同,底层机制有一些差异。

1. 通过 .class 语法

1
Class<String> cls1 = String.class;

  • 作用: .class 是获取某个类的 Class 对象的最直接方式,适用于已知编译时类型的情况。
  • 机制: .class 是 Java 编译器的内置语法。它告诉编译器直接在运行时将类的 Class 对象加载进来。String.class 在编译时就会生成字节码,JVM 会根据类加载器去获取对应的 Class 实例。
  • 特点:
    • 静态绑定:在编译时已经确定类的类型,因此 .class 是编译期的确定类型操作。
    • 无需抛出异常:String.class 一定能返回一个合法的 Class 实例。
    • 没有动态性,适合编译时就确定类的情况。

2. 通过对象实例调用 getClass()

1
2
String s = "Hello";
Class<? extends String> cls2 = s.getClass();

  • 作用: 通过对象的 getClass() 方法,可以获取该对象的实际运行时类型。即使对象是通过多态形式赋值的,getClass() 返回的依然是对象的实际类类型。
  • 机制: getClass() 是每个对象从 java.lang.Object 继承的方法。JVM 会在对象被创建时分配它的具体 Class 对象,并在 getClass() 被调用时返回此对象。
  • 特点:
    • 适用于运行时获取对象的动态类型。
    • 支持多态:即使对象被声明为某个父类类型,调用 getClass() 时返回的依然是它的真实子类类型。
    • 无需处理异常:因为对象已实例化,它的 Class 已经存在。

3. 通过 Class.forName(String className)

1
Class<?> cls3 = Class.forName("java.lang.String");

  • 作用: Class.forName() 通过类的完全限定名(即包含包名的类名)在运行时加载类。这是反射机制的一部分,可以用于动态加载类。
  • 机制: Class.forName() 在运行时根据传入的类名,使用类加载器动态加载指定的类。它会查找类的字节码文件(.class 文件),并将其加载到 JVM 中。如果类不存在,或者无法加载,会抛出 ClassNotFoundException 异常。
  • 特点:
    • 动态加载:适用于在运行时确定类的情况,尤其是在反射或框架(如 JDBC)中动态加载类时常用。
    • 需要异常处理:Class.forName() 可能抛出 ClassNotFoundException,必须处理此异常。
    • 可以用来加载尚未被 JVM 加载的类,这与 .classgetClass() 的行为不同,后者只返回已加载的 Class 对象。

底层机制区别:

  • .class:
    • 直接访问 JVM 中的 Class 对象。
    • 该对象可能已经被加载到 JVM 中,编译时确定,且无须重新加载类。
    • 适合静态使用,类的类型在编译时确定。
  • getClass():
    • 访问的是当前对象的运行时类型。
    • 通过对象实例动态确定,适用于运行时获取对象的具体类型。
    • 这种方式不会加载新的类,只是返回对象的 Class 对象。
  • Class.forName():
    • 在运行时根据类的完全限定名,通过类加载器加载类。
    • 如果类还没有加载,JVM 会通过类加载器加载该类。
    • 适用于需要动态加载类的场景(如反射、插件系统等)。

什么时候使用哪种方式?

  • .class: 当你已经知道类的编译时类型时,最直接、最简单的方法。

  • getClass(): 当你有一个对象,并且想要获取它的实际运行时类型时使用,尤其适合多态情况下获取真实的类信息。

  • Class.forName(): 当你只知道类的名字(通常是字符串格式),并且希望在运行时动态加载类时使用,尤其在框架或反射中常用。

例子对比

1
2
3
4
5
6
7
8
9
10
11
12
13
// .class 语法
Class<String> cls1 = String.class;

// 通过对象获取
String str = "hello";
Class<? extends String> cls2 = str.getClass();

// 动态加载类
try {
Class<?> cls3 = Class.forName("java.lang.String");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

总结来说:

  • .class 静态绑定,编译期已知类型;
  • getClass() 动态绑定,适合运行时确定类型;
  • Class.forName() 动态加载类,适合反射和框架使用。
Class 实例的唯一性

无论使用哪种方式获取 Class 实例,JVM 确保对于每一个类或接口,Class 对象是唯一的。可以使用 == 运算符来比较两个 Class 对象是否是同一个:

1
2
3
4
Class<String> cls1 = String.class;
String s = "Hello";
Class<? extends String> cls2 = s.getClass();
System.out.println(cls1 == cls2); // 输出 true

3. 通过 Class 获取类的详细信息

通过反射,Class 对象不仅可以用来标识类,还可以用来获取该类的所有信息。以下是一些常用方法:

  • getName(): 返回类的全限定名。
  • getSimpleName(): 返回类的简单名(不包括包名)。
  • getPackage(): 返回类的包信息。
  • isInterface(): 判断是否为接口。
  • isEnum(): 判断是否为枚举类型。
  • isArray(): 判断是否为数组类型。
  • isPrimitive(): 判断是否为基本类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
public static void main(String[] args) {
printClassInfo(String.class);
printClassInfo(Runnable.class);
printClassInfo(int.class);
printClassInfo(String[].class);
}

static void printClassInfo(Class<?> cls) {
System.out.println("Class name: " + cls.getName());
System.out.println("Simple name: " + cls.getSimpleName());
if (cls.getPackage() != null) {
System.out.println("Package name: " + cls.getPackage().getName());
}
System.out.println("is interface: " + cls.isInterface());
System.out.println("is enum: " + cls.isEnum());
System.out.println("is array: " + cls.isArray());
System.out.println("is primitive: " + cls.isPrimitive());
}
}

4. 反射创建实例

通过反射可以动态创建对象实例。通常,我们可以使用 Class.newInstance() 方法来创建对象,但它只能调用无参构造函数:

1
2
Class<String> cls = String.class;
String s = cls.newInstance(); // 通过反射创建String实例

如果类没有无参构造函数,或者需要调用带参的构造函数,可以通过 Constructor 类进行操作:

1
2
Constructor<Person> constructor = Person.class.getConstructor(String.class);
Person p = constructor.newInstance("John");

5. 动态加载类

JVM 动态加载类的机制非常重要,它允许 Java 程序根据运行时的需求动态加载类,而不需要预先加载所有类。例如,下面的代码展示了根据条件动态加载不同的日志实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LogFactory factory = null;
if (isClassPresent("org.apache.logging.log4j.Logger")) {
factory = createLog4j();
} else {
factory = createJdkLog();
}

boolean isClassPresent(String name) {
try {
Class.forName(name);
return true;
} catch (Exception e) {
return false;
}
}

这种动态加载机制在很多场景下非常有用,比如插件系统、依赖注入框架(如 Spring)、和跨平台库的实现等。

6. instanceofClass 的区别

在判断一个对象的类型时,instanceofClass 对象的 == 有不同的用途:

  • instanceof 不仅可以判断对象是否属于某个类,还可以判断对象是否属于该类的子类或实现类。
  • Class== 判断则更加严格,它只能判断对象是否属于某个具体的类,而不会考虑子类。
1
2
3
4
5
6
7
Integer n = new Integer(123);

boolean b1 = n instanceof Integer; // true
boolean b2 = n instanceof Number; // true

boolean b3 = n.getClass() == Integer.class; // true
boolean b4 = n.getClass() == Number.class; // false

Java 反射机制访问和修改类中的字段

1. 获取字段信息

获取字段的几种方式:

  • getField(name)
    • 获取指定名称的 public 字段,包括继承自父类的 public 字段。
    • 例如:Class.getField("name") 可以获取父类 Person 中的 public String name; 字段。
  • getDeclaredField(name)
    • 获取当前类中的指定字段,不论是否 public,但不包括父类的字段。
    • 例如:Class.getDeclaredField("grade") 可以获取子类 Student 中的 private int grade; 字段。
  • getFields()
    • 获取当前类及其父类中所有的 public 字段。
  • getDeclaredFields()
    • 获取当前类的所有字段(包括 privateprotected 字段),但不包含父类的字段。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
public static void main(String[] args) throws Exception {
Class stdClass = Student.class;
// 获取public字段"score":
System.out.println(stdClass.getField("score"));
// 获取继承的public字段"name":
System.out.println(stdClass.getField("name"));
// 获取private字段"grade":
System.out.println(stdClass.getDeclaredField("grade"));
}
}

class Student extends Person {
public int score;
private int grade;
}

class Person {
public String name;
}

输出的内容是:

1
2
3
public int Student.score
public java.lang.String Person.name
private int Student.grade

2. Field 对象

Field 对象代表了一个类中的字段,包含了字段的名称、类型和修饰符等信息。它的常用方法如下:

  • getName():返回字段名称,例如 "name"
  • getType():返回字段类型,通常是一个 Class 实例。例如,String.class
  • getModifiers():返回字段的修饰符,以整数形式表示。可以使用 Modifier 类中的静态方法检查字段是否为 publicprivatefinal 等。

String 类的 value 字段为例:

1
2
3
4
5
6
7
8
9
Field f = String.class.getDeclaredField("value");
f.getName(); // 返回 "value"
f.getType(); // 返回 class [B,表示 byte[] 类型
int m = f.getModifiers();
Modifier.isFinal(m); // true
Modifier.isPublic(m); // false
Modifier.isProtected(m); // false
Modifier.isPrivate(m); // true
Modifier.isStatic(m); // false

3. 获取字段值

通过反射,我们可以获取某个实例的字段值。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.lang.reflect.Field;

public class Main {
public static void main(String[] args) throws Exception {
Object p = new Person("Xiao Ming");
Class<?> c = p.getClass();
Field f = c.getDeclaredField("name");
f.setAccessible(true); // 允许访问 private 字段
Object value = f.get(p);
System.out.println(value); // "Xiao Ming"
}
}

class Person {
private String name;

public Person(String name) {
this.name = name;
}
}
  • Field.get(Object obj):通过反射获取 obj 实例的字段值。在调用之前,我们需要使用 f.setAccessible(true) 来确保可以访问 private 字段。否则会抛出 IllegalAccessException

    通过 f.get(p),可以获取到 p 对应实例中 name 字段的值,即 "Xiao Ming"

4. 修改字段值

通过反射不仅可以获取字段值,还可以直接修改字段的值。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.lang.reflect.Field;

public class Main {
public static void main(String[] args) throws Exception {
Person p = new Person("Xiao Ming");
System.out.println(p.getName()); // "Xiao Ming"

Class<?> c = p.getClass();
Field f = c.getDeclaredField("name");
f.setAccessible(true); // 允许访问 private 字段
f.set(p, "Xiao Hong"); // 修改字段值
System.out.println(p.getName()); // "Xiao Hong"
}
}

class Person {
private String name;

public Person(String name) {
this.name = name;
}

public String getName() {
return this.name;
}
}

在这段代码中:

  • f.set(Object obj, Object value):用于修改指定实例 obj 的字段值为 value
  • 通过 f.set(p, "Xiao Hong"),将 Person 实例 pname 字段值从 "Xiao Ming" 修改为 "Xiao Hong"

修改字段值的过程与获取字段值类似,关键点是使用 setAccessible(true) 来允许修改 private 字段的值。

5. 反射的局限性

尽管反射可以绕过 Java 的访问修饰符限制,直接访问和修改 private 字段,但这种做法有以下局限性:

  • 性能开销:反射机制比直接访问字段要慢很多,尤其是在频繁调用的情况下,可能会带来较大的性能开销。
  • 代码复杂性:反射代码相对复杂且易出错,增加了代码的维护难度。
  • 安全性风险:反射允许访问和修改私有字段,打破了封装性。尤其是在 JVM 启用了 SecurityManager 的情况下,setAccessible(true) 可能会被拒绝,防止对敏感类的字段(如 java.lang.String)进行修改。

反射与方法

在Java反射中,通过Class类可以获取类的所有方法信息,并且可以通过Method对象来调用这些方法。使用反射进行方法调用主要分为几个步骤:获取Method对象,设置访问权限(如果需要),然后通过invoke()进行调用。

1. 获取方法信息

通过Class对象,可以获取类中的方法信息。Java的Class类提供了几种方法来获取类的Method对象:

  • Method getMethod(String name, Class<?>... parameterTypes):获取类或父类中某个public方法(包括继承的公共方法)。
  • Method getDeclaredMethod(String name, Class<?>... parameterTypes):获取当前类中定义的某个方法(包括private方法,但不包括父类中的方法)。
  • Method[] getMethods():获取当前类及其父类的所有public方法。
  • Method[] getDeclaredMethods():获取当前类中声明的所有方法(包括private方法)。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 通过反射获取方法
public class Main {
public static void main(String[] args) throws Exception {
Class<Student> stdClass = Student.class;

// 获取public方法getScore,参数为String:
Method getScoreMethod = stdClass.getMethod("getScore", String.class);
System.out.println(getScoreMethod); // 打印方法信息

// 获取继承的public方法getName,无参数:
Method getNameMethod = stdClass.getMethod("getName");
System.out.println(getNameMethod); // 打印继承的方法信息

// 获取private方法getGrade,参数为int:
Method getGradeMethod = stdClass.getDeclaredMethod("getGrade", int.class);
System.out.println(getGradeMethod); // 打印private方法信息
}
}

class Student extends Person {
public int getScore(String type) {
return 99;
}

private int getGrade(int year) {
return 1;
}
}

class Person {
public String getName() {
return "Person";
}
}

输出的结果类似如下:

1
2
3
public int Student.getScore(java.lang.String)
public java.lang.String Person.getName()
private int Student.getGrade(int)

2. 获取方法的属性

Method对象中,我们可以获取到方法的多种信息:

  • getName():获取方法的名称。
  • getReturnType():获取方法的返回类型。
  • getParameterTypes():获取方法的参数类型。
  • getModifiers():获取方法的修饰符,返回一个整数值,可以使用Modifier类来判断具体修饰符。

示例代码:获取方法属性

1
2
3
4
5
6
Method m = String.class.getMethod("substring", int.class);
System.out.println("Method Name: " + m.getName());
System.out.println("Return Type: " + m.getReturnType());
System.out.println("Parameter Types: " + Arrays.toString(m.getParameterTypes()));
int modifiers = m.getModifiers();
System.out.println("Is Public: " + Modifier.isPublic(modifiers));

输出示例:

1
2
3
4
Method Name: substring
Return Type: class java.lang.String
Parameter Types: [int]
Is Public: true

3. 调用方法

通过Method对象的invoke()方法,可以对某个实例对象调用该方法。调用时,invoke()的第一个参数是调用方法的实例对象,剩余参数是方法所需的参数。

示例代码:调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// reflection
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
// String对象:
String s = "Hello world";
// 获取String类的substring(int)方法,参数为int:
Method m = String.class.getMethod("substring", int.class);
// 调用substring方法并获取结果:
String result = (String) m.invoke(s, 6);
// 打印调用结果:
System.out.println(result); // 输出: "world"
}
}

在上面的例子中,invoke()的第一个参数是String对象"Hello world",表示在这个对象上调用substring方法,第二个参数是6,表示substring方法的参数。

4. 调用静态方法

对于静态方法,由于不需要实例对象,所以invoke()的第一个参数可以传入null

示例代码:调用静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
// reflection
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
// 获取Integer类的静态方法parseInt,参数为String:
Method parseIntMethod = Integer.class.getMethod("parseInt", String.class);
// 调用静态方法并获取结果:
Integer result = (Integer) parseIntMethod.invoke(null, "12345");
// 打印调用结果:
System.out.println(result); // 输出: 12345
}
}

5. 调用非public方法

如果需要调用非public方法,需要先调用setAccessible(true),以允许访问该方法。

示例代码:调用private方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
Person p = new Person();
// 获取private方法setName:
Method setNameMethod = p.getClass().getDeclaredMethod("setName", String.class);
// 设置允许访问:
setNameMethod.setAccessible(true);
// 调用方法:
setNameMethod.invoke(p, "Alice");
// 打印结果:
System.out.println(p.getName()); // 输出: Alice
}
}

class Person {
private String name;

private void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

通过setAccessible(true),可以突破Java的访问控制,访问private方法。

6. 多态行为

即使通过反射,方法的调用仍然遵循Java的多态机制。即,当子类覆写了父类的方法时,调用的是实际对象的子类方法,而不是父类方法。

示例代码:反射中的多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
// 获取Person类的hello方法:
Method helloMethod = Person.class.getMethod("hello");
// 对Student实例调用hello方法:
helloMethod.invoke(new Student()); // 输出: Student:hello
}
}

class Person {
public void hello() {
System.out.println("Person:hello");
}
}

class Student extends Person {
@Override
public void hello() {
System.out.println("Student:hello");
}
}

在这段代码中,虽然获取的是Person类的hello()方法,但调用时由于多态,实际上调用的是Student类的hello()方法。

  • 通过反射可以获取方法信息,并通过Method.invoke()调用方法。
  • 反射支持调用非public方法,需要先设置setAccessible(true)
  • 反射调用时依然遵循Java的多态机制,调用的是实际对象的覆写方法。

通过反射机制调用构造方法

在Java中,通过反射机制调用构造方法,可以用Constructor对象来代替传统的new操作符创建新的实例。使用反射可以调用任何构造方法,包括带参数的、非public的构造方法。

1. 获取Constructor对象

要通过反射获取类的构造方法,使用Class类提供的以下方法:

方法 说明
getConstructor(Class...) 获取某个public构造方法,参数类型通过Class...指定。
getDeclaredConstructor(Class...) 获取任意构造方法,包括privateprotectedpackage-private的。
getConstructors() 获取当前类的所有public构造方法。
getDeclaredConstructors() 获取当前类的所有构造方法,包括private等非public的构造方法。

注意:获取的Constructor对象与方法或字段类似,它封装了构造方法的相关信息,包括参数类型、修饰符等。

2. 创建对象实例

通过获取的Constructor对象,可以调用其newInstance()方法来创建一个新的实例。这个方法接受与构造方法参数匹配的参数,创建实例的具体操作如下:

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.lang.reflect.Constructor;

public class Main {
public static void main(String[] args) throws Exception {
// 获取Integer(int)的构造方法:
Constructor<Integer> cons1 = Integer.class.getConstructor(int.class);
// 调用构造方法创建实例:
Integer n1 = cons1.newInstance(123);
System.out.println(n1); // 输出: 123

// 获取Integer(String)的构造方法:
Constructor<Integer> cons2 = Integer.class.getConstructor(String.class);
// 调用构造方法创建实例:
Integer n2 = cons2.newInstance("456");
System.out.println(n2); // 输出: 456
}
}

在上面的代码中,Integer类有两个构造方法,一个接受int参数,另一个接受String参数。通过反射获取到这两个Constructor对象后,分别调用newInstance()方法来创建Integer实例。

3. 处理非public构造方法

对于非public的构造方法,例如private构造方法,直接通过getDeclaredConstructor()获取它后,需要通过setAccessible(true)来允许访问该构造方法,类似于访问private字段或方法。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.lang.reflect.Constructor;

public class Main {
public static void main(String[] args) throws Exception {
// 获取带有private修饰符的Person构造方法
Constructor<Person> cons = Person.class.getDeclaredConstructor(String.class);
// 允许访问private构造方法:
cons.setAccessible(true);
// 调用构造方法创建实例:
Person p = cons.newInstance("John");
System.out.println(p.getName()); // 输出: John
}
}

class Person {
private String name;

// private构造方法
private Person(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

在这个例子中,Person类有一个private构造方法,不能直接通过new操作符调用,但通过反射,我们使用setAccessible(true)来允许访问这个private构造方法,并成功创建了Person实例。

4. 注意事项

  • setAccessible(true)可能会失败:在一些情况下,如果JVM正在运行一个SecurityManager(安全管理器),它会检查是否允许调用setAccessible(true)。对于某些核心类(如javajavax包),出于安全考虑,安全管理器可能阻止对它们的private构造方法调用setAccessible(true)

  • 异常处理:调用Constructor.newInstance()可能抛出许多异常,包括:

    • IllegalAccessException:当没有权限调用该构造方法时抛出。
    • InstantiationException:如果调用的是一个抽象类的构造方法,抛出此异常。
    • InvocationTargetException:如果构造方法本身抛出异常,会封装在此异常中。
    • NoSuchMethodException:如果指定的构造方法不存在,会抛出此异常。

5. Constructor和Method的区别

  • Constructor是构造方法:返回一个类的实例。
  • Method是普通方法:调用时可以返回任意类型的结果(或void)。

例子

1. 调用带参数的public构造方法

通过反射调用带参数的public构造方法,首先需要通过getConstructor()方法获取构造方法,然后调用newInstance()来创建实例。

示例:

1
2
3
4
5
6
7
8
9
10
11
import java.lang.reflect.Constructor;

public class Main {
public static void main(String[] args) throws Exception {
// 获取String(String)构造方法:
Constructor<String> cons = String.class.getConstructor(String.class);
// 通过构造方法创建实例:
String str = cons.newInstance("Hello World");
System.out.println(str); // 输出: Hello World
}
}

此例中,String类的构造方法接收一个String参数,通过反射成功创建了String对象。

2. 调用private构造方法

对于private构造方法,需要使用getDeclaredConstructor()并通过setAccessible(true)来解除访问权限。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.lang.reflect.Constructor;

public class Main {
public static void main(String[] args) throws Exception {
// 获取Person类的private构造方法:
Constructor<Person> cons = Person.class.getDeclaredConstructor(String.class);
// 设置为可访问:
cons.setAccessible(true);
// 调用private构造方法创建实例:
Person p = cons.newInstance("Alice");
System.out.println(p.getName()); // 输出: Alice
}
}

class Person {
private String name;

// private构造方法
private Person(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

这个例子演示了如何调用private构造方法,通过setAccessible(true)解除访问限制。

3. 调用无参的protected构造方法

假设有一个protected的无参构造方法,依然可以通过反射获取它并实例化对象。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.lang.reflect.Constructor;

public class Main {
public static void main(String[] args) throws Exception {
// 获取Animal类的protected无参构造方法:
Constructor<Animal> cons = Animal.class.getDeclaredConstructor();
// 设置为可访问:
cons.setAccessible(true);
// 创建实例:
Animal animal = cons.newInstance();
System.out.println(animal.getType()); // 输出: Unknown
}
}

class Animal {
protected Animal() {
System.out.println("Protected Constructor Called");
}

public String getType() {
return "Unknown";
}
}

此例展示了如何调用protected无参构造方法,并成功创建对象。

4. 处理构造方法中的异常

如果构造方法可能抛出异常,可以在调用newInstance()时捕获这些异常。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.lang.reflect.Constructor;

public class Main {
public static void main(String[] args) {
try {
// 获取可能抛出异常的构造方法:
Constructor<Car> cons = Car.class.getDeclaredConstructor(String.class);
// 调用构造方法:
Car car = cons.newInstance("Toyota");
System.out.println(car.getBrand()); // 输出: Toyota
} catch (Exception e) {
System.out.println("Exception occurred: " + e.getMessage());
}
}
}

class Car {
private String brand;

public Car(String brand) throws Exception {
if (brand == null || brand.isEmpty()) {
throw new Exception("Brand cannot be null or empty");
}
this.brand = brand;
}

public String getBrand() {
return brand;
}
}

在这个例子中,Car类的构造方法可能抛出异常,因此在反射调用时捕获并处理了该异常。

5. 获取所有构造方法并调用

通过getConstructors()getDeclaredConstructors()可以获取类的所有构造方法,然后遍历这些构造方法,并根据需要调用不同的构造方法。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.lang.reflect.Constructor;

public class Main {
public static void main(String[] args) throws Exception {
// 获取Person类的所有构造方法:
Constructor<?>[] constructors = Person.class.getDeclaredConstructors();
for (Constructor<?> cons : constructors) {
System.out.println("Constructor: " + cons);

// 如果是无参构造方法,调用它:
if (cons.getParameterCount() == 0) {
Person p = (Person) cons.newInstance();
System.out.println("Created Person: " + p);
}
}
}
}

class Person {
private String name;

// public无参构造方法
public Person() {
this.name = "Unknown";
}

// private构造方法
private Person(String name) {
this.name = name;
}

@Override
public String toString() {
return "Person{name='" + name + "'}";
}
}

这个示例展示了如何遍历类的所有构造方法,并根据其参数情况进行调用。

6. 调用构造方法并捕获抛出的异常

通过反射调用构造方法时,如果构造方法内部抛出异常,newInstance()方法会封装该异常为InvocationTargetException,可以通过getCause()获取原始异常。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Main {
public static void main(String[] args) {
try {
// 获取可能抛出异常的构造方法:
Constructor<Car> cons = Car.class.getConstructor(String.class);
// 调用构造方法时处理异常:
Car car = cons.newInstance("");
} catch (InvocationTargetException e) {
System.out.println("Constructor threw an exception: " + e.getCause().getMessage());
} catch (Exception e) {
System.out.println("Exception occurred: " + e.getMessage());
}
}
}

class Car {
private String brand;

public Car(String brand) throws Exception {
if (brand == null || brand.isEmpty()) {
throw new Exception("Brand cannot be null or empty");
}
this.brand = brand;
}
}

在此例中,当构造方法抛出异常时,InvocationTargetException捕获并处理了具体异常信息。

通过反射获取继承关系

1. 获取Class对象的三种方式

在Java中,我们可以通过三种方式获取类的Class对象,每种方式都适用于不同的场景。关键点是,无论哪种方式,JVM对于每个加载的类只会创建一个Class对象实例。

方式一:使用.class

通过类名直接获取对应的Class对象。这种方式适用于已知类的情况:

1
Class<String> cls = String.class;

方式二:通过getClass()

通过实例对象获取它的Class对象,适用于已知某个对象的场景:

1
2
String s = "Hello";
Class<? extends String> cls = s.getClass();

方式三:使用Class.forName()

通过类的完全限定名(包名+类名)获取Class对象,适用于动态加载类的情况,通常用于反射框架:

1
Class<?> cls = Class.forName("java.lang.String");

三种方式获取的Class对象都相同,因为JVM对每个类只创建一个Class实例。

2. 获取父类的Class

通过getSuperclass()方法,可以获取类的父类。如果当前类是Object,则返回null,因为Object是所有类的基类,没有父类。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main {
public static void main(String[] args) {
Class<?> cls = Integer.class;

// 获取父类
Class<?> superclass = cls.getSuperclass();
System.out.println("Integer的父类: " + superclass.getName()); // Number

// 获取父类的父类
Class<?> parentSuperclass = superclass.getSuperclass();
System.out.println("Number的父类: " + parentSuperclass.getName()); // Object

// Object的父类为null
System.out.println("Object的父类: " + parentSuperclass.getSuperclass()); // null
}
}

输出结果:

1
2
3
Integer的父类: java.lang.Number
Number的父类: java.lang.Object
Object的父类: null

3. 获取实现的接口

类可能实现多个接口,使用getInterfaces()可以获取当前类直接实现的接口。如果一个类没有实现任何接口,getInterfaces()返回一个空数组。此方法不包括父类实现的接口。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Main {
public static void main(String[] args) {
// 获取Integer类的所有接口
Class<?> cls = Integer.class;
Class<?>[] interfaces = cls.getInterfaces();

System.out.println("Integer实现的接口:");
for (Class<?> iface : interfaces) {
System.out.println(iface.getName());
}

// 获取父类Number的所有接口
Class<?> superclass = cls.getSuperclass();
Class<?>[] parentInterfaces = superclass.getInterfaces();

System.out.println("Number实现的接口:");
for (Class<?> iface : parentInterfaces) {
System.out.println(iface.getName());
}
}
}

输出结果:

1
2
3
4
5
6
7
Integer实现的接口:
java.lang.Comparable
java.lang.constant.Constable
java.lang.constant.ConstantDesc

Number实现的接口:
java.io.Serializable

4. 获取接口的父接口

接口本身也可以继承其他接口。对接口调用getInterfaces()方法可以获取它继承的父接口,但对接口调用getSuperclass()方法总是返回null

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
public static void main(String[] args) {
// 获取Closeable接口的父接口
Class<?> iface = java.io.Closeable.class;
Class<?>[] parentInterfaces = iface.getInterfaces();

System.out.println("Closeable继承的接口:");
for (Class<?> parentIface : parentInterfaces) {
System.out.println(parentIface.getName());
}

// 接口的getSuperclass()总是返回null
System.out.println("Closeable的父类: " + iface.getSuperclass()); // null
}
}

输出结果:

1
2
3
Closeable继承的接口:
java.io.AutoCloseable
Closeable的父类: null

5. instanceofisAssignableFrom()

instanceofisAssignableFrom()方法用于判断类与类之间的继承关系或实现关系,二者使用场景略有不同:

  • instanceof:用于判断一个对象是否是某个类的实例,或是否实现了某个接口。
  • isAssignableFrom():用于判断一个Class对象是否可以赋值给另一个Class对象,即判断类的类型转换是否合法。

示例 1:instanceof判断

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
Object obj = Integer.valueOf(42);

// 判断对象是否为某个类的实例
System.out.println(obj instanceof Integer); // true
System.out.println(obj instanceof Number); // true
System.out.println(obj instanceof Double); // false
System.out.println(obj instanceof java.io.Serializable); // true
}
}

示例 2:isAssignableFrom()判断类的可赋值关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
public static void main(String[] args) {
// Integer是否可以赋值给Integer
System.out.println(Integer.class.isAssignableFrom(Integer.class)); // true

// Integer是否可以赋值给Number
System.out.println(Number.class.isAssignableFrom(Integer.class)); // true

// Integer是否可以赋值给Object
System.out.println(Object.class.isAssignableFrom(Integer.class)); // true

// Number是否可以赋值给Integer
System.out.println(Integer.class.isAssignableFrom(Number.class)); // false
}
}
  • 获取Class对象的三种方式.classgetClass()Class.forName()
  • 获取父类和接口:使用getSuperclass()获取父类,使用getInterfaces()获取类实现的接口。
  • 接口继承与getSuperclass()的关系:接口没有父类,getSuperclass()返回null,获取接口的父接口需要使用getInterfaces()
  • 继承关系判断:使用instanceof判断对象的类型关系,使用isAssignableFrom()判断类之间的赋值关系。

静态代理与动态代理

Java 的动态代理机制通过 Proxy.newProxyInstance() 在运行期动态生成接口的实现,这种机制为接口的实现提供了一种灵活的方式,使得我们不需要显式地编写实现类即可实例化接口并实现其中的方法。

静态 vs 动态代理

静态代理 是指在编译时就已经确定代理类的代码,并通过实现接口来代理目标对象。编写静态代理类有几个步骤:

  1. 定义接口
  2. 实现接口并编写代理逻辑
  3. 在代理类中通过组合目标对象的方式,代理目标对象的接口方法。

但是,静态代理的弊端在于每次需要代理一个接口时,都必须手动编写一个代理类,而这在项目规模扩大时显得非常繁琐。

动态代理 则是在运行时通过反射机制来创建一个代理对象,而不需要手动去实现接口。它是在运行时生成字节码,并加载到内存中进行执行。因此,动态代理可以为任意接口生成代理对象,而无需提前编写具体的代理类。

动态代理的核心组件

要使用动态代理,涉及以下几个核心组件:

  1. 接口(Interface): 代理目标必须是接口,因为动态代理只能代理接口类型。

  2. InvocationHandler 接口: 这个接口用来处理对代理对象的方法调用。实现 InvocationHandler 接口的类,需要重写 invoke() 方法,该方法会在代理对象调用任意方法时执行。invoke() 方法的签名是:

    1
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
    • proxy:代理对象本身。
    • method:被调用的方法。
    • args:调用方法时传入的参数。
  3. Proxy 类: Proxy 是 Java 提供的核心类,通过它的 newProxyInstance() 方法,可以生成代理对象。该方法接收三个参数:

    • ClassLoader loader:定义了哪个类加载器来加载代理类。
    • Class<?>[] interfaces:代理对象需要实现的接口。
    • InvocationHandler h:处理代理对象上所有方法调用的处理器。

运行时如何创建代理对象

通过 Proxy.newProxyInstance() 方法,我们可以在运行时创建代理对象。下面对该方法的工作流程进行详细说明:

步骤 1: 创建 InvocationHandler

首先,我们需要实现 InvocationHandler 接口,负责代理对象方法调用时的处理逻辑:

1
2
3
4
5
6
7
8
9
10
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Method: " + method.getName());
if ("morning".equals(method.getName())) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};

在这个示例中,InvocationHandler 负责拦截对 morning 方法的调用,并输出相应的问候。

步骤 2: 调用 Proxy.newProxyInstance()

接下来,通过 Proxy.newProxyInstance() 方法动态生成代理对象:

1
2
3
4
5
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 传入ClassLoader
new Class[] { Hello.class }, // 传入要实现的接口
handler // 传入InvocationHandler实例
);

这里我们向 Proxy.newProxyInstance() 方法传入了:

  • Hello.class.getClassLoader():使用接口 Hello 的类加载器。
  • new Class[] { Hello.class }:指定要代理的接口。
  • handler:处理方法调用的 InvocationHandler 实例。

该方法返回的对象是 Hello 接口的代理实现,调用它的 morning() 方法时,实际上是触发了 handler.invoke() 方法。

步骤 3: 调用代理对象的方法

生成代理对象后,我们可以像普通对象一样调用它的方法:

1
hello.morning("Bob");

此时,代理对象的 morning() 方法被调用,实际上是 handler.invoke() 方法在运行,通过 Method 对象反射调用实际的方法。

动态代理 vs 静态代理的区别

特性 静态代理 动态代理
实现方式 需要手动编写代理类 通过反射机制在运行时生成代理类
代理对象 只能代理特定的接口 可以代理任意接口,只要有相应的 InvocationHandler 实现
扩展性 需要为每个接口编写单独的代理类 只需编写一次 InvocationHandler 实现,可代理多个接口
性能 编译时确定,运行效率较高 运行时动态生成代理类,性能稍差
维护性 随着接口的增多,代理类越来越多,难以维护 只需维护 InvocationHandler,扩展性强

动态代理原理解析

Java 动态代理的核心是 反射机制类加载器Proxy.newProxyInstance() 实际上是创建了一个代理类的字节码,并加载到 JVM 中。在调用代理类的方法时,JVM 将拦截方法调用,并将其转发给 InvocationHandlerinvoke() 方法。

具体来说:

  1. 类加载器Proxy.newProxyInstance() 会使用指定的 ClassLoader 动态生成代理类的字节码。
  2. 字节码生成:在运行时,JVM 动态生成实现了指定接口的代理类,代理类的所有方法都会委托给 InvocationHandlerinvoke() 方法进行处理。
  3. 方法拦截:在代理类中,所有方法的调用都被重定向到 InvocationHandlerinvoke() 方法,该方法可以根据反射信息自行决定如何处理方法调用。

动态代理的应用场景

动态代理广泛应用于各种场景,尤其是在 AOP(面向切面编程)中。常见的应用包括:

  • 日志记录:在方法调用前后打印日志,无需修改方法本身。
  • 权限控制:在方法调用前检查用户权限。
  • 事务管理:在方法调用前后自动开启和提交事务。
  • 远程调用:将方法调用通过网络发送到远程服务器执行。