66、Java反射机制:注解查询、修饰符、成员类及对象操作详解

Java反射机制:注解查询、修饰符、成员类及对象操作详解

1. 注解查询

在Java中,可以使用类似于查询成员的方法来询问应用于类或接口的注解,这些方法都属于 AnnotatedElement 接口。所有表示程序元素的反射类(如 Class Field Method Constructor Package )都实现了该接口。

注解查询只能提供在运行时可用的注解信息,即保留策略为 RetentionPolicy.RUNTIME 的注解。以下是一些常用的注解查询方法:
- getAnnotations() :返回元素上存在的所有注解(包括直接声明和继承的),返回 Annotation 实例数组。
- getDeclaredAnnotations() :仅返回直接应用的注解,同样返回 Annotation 实例数组。
- getAnnotation(Class<T> annotationClass) :返回指定类型的注解对象,如果不存在则返回 null
- isAnnotationPresent(Class<? extends Annotation> annotationClass) :判断元素上是否存在指定类型的注解,返回布尔值。

与查询成员的类似方法不同,注解查询没有公共与非公共注解的概念,也没有安全检查。返回的注解实例是实现了给定注解类型定义的接口的代理对象,可以调用注解类型的方法来获取每个注解元素的值。

例如,假设有如下注解和类:

@BugsFixed( { "457605", "532456"} )
class Foo { /* ... */ }

可以使用以下代码获取注解值并打印:

Class<Foo> cls = Foo.class;
BugsFixed bugsFixed = (BugsFixed) cls.getAnnotation(BugsFixed.class);
String[] bugIds = bugsFixed.value();
for (String id : bugIds)
    System.out.println(id);

输出结果为:

457605
532456

如果注解方法表示一个 Class 类型的注解元素,且该类在运行时找不到,将抛出 TypeNotPresentException 。由于运行时可用的注解类型可能与用于注解被检查类的注解类型不同,尝试访问注解元素时可能会抛出 AnnotationTypeMismatchException IncompleteAnnotationException 。如果元素类型是枚举,且注解中的枚举常量不再存在于枚举中,则会抛出 EnumConstantNotPresentException

2. Modifier类

Modifier 类为所有非注解修饰符定义了 int 常量,如 ABSTRACT FINAL INTERFACE 等。对于每个常量,都有一个对应的查询方法 isMod(int modifiers) ,如果指定值中存在该修饰符,则返回 true

例如,对于以下字段声明:

public static final int OAK = 0;

Field 对象的 getModifiers 方法返回的值为:

Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL

strictfp 修饰符由常量 STRICT 表示。如果代码或类要以严格浮点模式进行评估,方法、类或接口的修饰符将包含 STRICT 标志。查询方法可以更符号化地询问问题,例如:

Modifier.isPrivate(field.getModifiers())

等价于:

(field.getModifiers() & Modifier.PRIVATE) != 0
3. 成员类

Field Constructor Method 类都实现了 Member 接口,该接口有四个方法用于所有成员共享的属性:
- getDeclaringClass() :返回声明该成员的类的 Class 对象。
- getName() :返回该成员的名称。
- getModifiers() :返回成员的语言修饰符,编码为整数,需要使用 Modifier 类进行解码。
- isSynthetic() :如果该成员是编译器创建的合成成员,则返回 true 。例如,内部类中经常会创建合成字段来保存对外部实例的引用,还会生成合成“桥接”方法来支持泛型。

所有 Member 类的 toString 方法包含成员的完整声明,类似于源代码中的显示方式,包括修饰符、类型和参数类型(适用时)。由于历史原因, toString 不包含泛型类型信息,而 toGenericString 方法提供了更完整的成员声明表示,包括类型参数、参数化类型和类型变量的使用。对于方法和构造函数,字符串中还包括 throws 列表。

4. 访问检查和AccessibleObject

Field Constructor Method 类都是 AccessibleObject 类的子类,该类允许启用或禁用对语言级访问修饰符(如 public private )的检查。通常,使用反射访问成员的尝试会受到与常规显式代码相同的访问检查。可以通过调用 setAccessible(true) 来禁用此检查,使对象无论语言级访问控制如何都可访问。

AccessibleObject 类提供了以下方法:
- setAccessible(boolean flag) :将对象的可访问标志设置为指定的布尔值。 true 表示对象应抑制语言级访问控制, false 表示应强制执行语言级访问控制。如果不允许更改对象的可访问性,将抛出 SecurityException
- setAccessible(AccessibleObject[] array, boolean flag) :为对象数组设置可访问标志。如果设置某个对象的标志时抛出 SecurityException ,则只有数组中较早的对象的标志会设置为给定值,其他对象保持不变。
- isAccessible() :返回对象的可访问标志的当前值。

5. Field类

Field 类定义了询问字段类型以及设置和获取字段值的方法。结合继承的 Member 方法,可以了解字段声明的所有信息并操作特定对象或类的字段。

getGenericType() 方法返回表示字段声明类型的 Type 实例。对于普通类型(如 String int ),返回关联的 Class 对象;对于参数化类型(如 List<String> ),返回 ParameterizedType 实例;对于类型变量(如 T ),返回 TypeVariable 实例。

getType() 是一个遗留方法,返回字段类型的 Class 对象。对于普通类型,其行为与 getGenericType() 相同;对于参数化类型,返回参数化类型擦除后的类对象;对于类型变量,返回类型变量擦除后的类对象。

可以使用 isEnumConstant() 方法判断字段是否为枚举常量,使用 get() set() 方法获取和设置字段的值。这些方法有通用形式(接受 Object 参数并返回 Object 值)和更具体的处理基本类型的形式。所有这些方法都需要一个参数来指定要操作的对象,对于静态字段,该对象会被忽略,可以为 null

例如,以下方法用于打印对象的 short 字段的值:

public static void printShortField(Object o, String name)
    throws NoSuchFieldException, IllegalAccessException
{
    Field field = o.getClass().getField(name);
    short value = (Short) field.get(o);
    System.out.println(value);
}

get() 方法的返回值是字段引用的对象,如果字段是基本类型,则返回相应类型的包装对象。对于 short 字段, get() 返回一个包含字段值的 Short 对象,该值会自动拆箱存储在局部变量中。

set() 方法的使用方式类似,以下是设置 short 字段值的方法示例:

public static void
    setShortField(Object o, String name, short nv)
    throws NoSuchFieldException, IllegalAccessException
{
    Field field = o.getClass().getField(name);
    field.set(o, nv);
}

虽然 set() 方法接受 Object 参数,但可以直接传递 short 值,自动装箱转换会将其包装在 Short 对象中。

如果指定对象的字段不可访问且正在强制执行访问控制,将抛出 IllegalAccessException ;如果传递的对象没有声明该字段的类型,将抛出 IllegalArgumentException ;如果字段是非静态的且传递的对象引用为 null ,将抛出 NullPointerException ;访问静态字段可能需要初始化类,因此也可能抛出 ExceptionInInitializerError

Field 类还提供了获取和设置基本类型的特定方法,如 getShort() setShort() ,可以避免使用包装对象。

Field 类实现了 AnnotatedElement 接口,可以按照前面介绍的注解查询方法来查询字段上的注解。

在正常情况下,尝试设置声明为 final 的字段的值会抛出 IllegalAccessException 。但在特殊情况下(如自定义反序列化时),可以通过反射更改实例字段的值,但前提是必须在 Field 对象上调用 setAccessible(true)

6. 修饰符、成员类及对象操作的应用示例

以下是一个简单的流程图,展示了使用反射访问字段和方法的基本流程:

graph TD;
    A[获取Class对象] --> B[获取Field或Method对象];
    B --> C{是否需要访问控制};
    C -- 是 --> D[调用setAccessible(true)];
    C -- 否 --> E[直接操作];
    D --> E;
    E --> F[获取或设置值/调用方法];
    F --> G{是否抛出异常};
    G -- 是 --> H[处理异常];
    G -- 否 --> I[操作成功];

在实际应用中,反射机制可以用于实现一些通用的工具类,如动态创建对象、调用方法等。但需要注意的是,反射会增加代码的复杂性和运行时开销,应谨慎使用。例如,在编写调试器或其他需要解释用户输入作为对象操作的通用应用程序时,可以合理使用反射机制。但在一般情况下,尽量使用直接的代码调用,以提高代码的可读性和性能。

通过以上对Java反射机制中注解查询、修饰符、成员类及对象操作的详细介绍,我们可以更深入地理解Java的反射特性,并在合适的场景中灵活运用。

Java反射机制:注解查询、修饰符、成员类及对象操作详解

7. Method类

Method 类结合其继承的 Member 方法,能让我们获取方法声明的完整信息。以下是一些关键方法:
- getGenericReturnType() :返回该方法返回类型的 Type 对象。若方法声明为 void ,则返回 void.class
- getGenericParameterTypes() :返回一个 Type 对象数组,包含该方法每个参数的类型,按参数声明顺序排列。若方法无参数,则返回空数组。
- getGenericExceptionTypes() :返回一个 Type 对象数组,包含该方法 throws 子句中列出的每个异常类型,按声明顺序排列。若没有声明异常,则返回空数组。

此外,还有遗留方法 getReturnType() getParameterTypes() getExceptionTypes() ,它们返回 Class 对象而非 Type 对象。与 Field.getType() 类似,参数化类型和类型变量由其擦除后的 Class 对象表示。

Method 类实现了 AnnotatedElement 接口,可按之前介绍的注解查询方法查询方法上的注解。同时, Method 类提供了 getParameterAnnotations() 方法,用于访问应用于方法参数的注解,该方法返回一个 Annotation 数组的数组,最外层数组的每个元素对应方法的一个参数,按声明顺序排列。若某个参数没有注解,则为该参数提供一个长度为零的 Annotation 数组。若 Method 对象表示注解的一个元素,则 getDefaultValue() 方法返回表示该元素默认值的 Object ,若不是注解元素或没有默认值,则返回 null

Method 类还实现了 GenericDeclaration 接口,定义了 getTypeParameters() 方法,返回一个 TypeVariable 对象数组。若给定的 Method 对象不是泛型方法,则返回空数组。

可以使用 isVarArgs() 方法询问 Method 对象是否为可变参数方法,使用 isBridge() 方法询问是否为桥接方法。

Method 对象最有趣的用途是反射性地调用它,通过 invoke() 方法实现:

public Object invoke(Object onThis, Object... args) throws IllegalAccessException, 
IllegalArgumentException, InvocationTargetException

该方法在 onThis 对象上调用此 Method 对象定义的方法,从 args 中的值设置方法的参数。对于非静态方法, onThis 的实际类型决定调用的方法实现;对于静态方法, onThis 被忽略,通常为 null args 值的数量必须等于方法的参数数量,且这些值的类型必须都可赋值给方法的参数类型,否则将抛出 IllegalArgumentException 。对于可变参数方法,最后一个参数是一个数组,必须用要传递的实际“可变”参数填充。若尝试调用没有访问权限的方法,将抛出 IllegalAccessException ;若此方法不是 onThis 对象的方法,将抛出 IllegalArgumentException ;若 onThis null 且方法不是静态的,将抛出 NullPointerException ;若 Method 对象表示静态方法且类尚未初始化,可能会抛出 ExceptionInInitializerError ;若方法抛出异常,将抛出 InvocationTargetException ,其原因是该异常。

使用 invoke() 时,可以直接传递基本类型,也可以使用合适类型的包装器。包装器表示的类型必须可赋值给声明的参数类型。例如,可以使用 Long Float Double 包装 double 参数,但不能使用 Double 包装 long float 参数,因为 double 不能赋值给 long float invoke() 返回的 Object 处理方式与 Field.get() 类似,基本类型以其包装类形式返回。若方法声明为 void invoke() 返回 null

以下是一个使用反射调用 String indexOf() 方法的示例:

Throwable failure;
try {
    Method indexM = String.class.
        getMethod("indexOf", String.class, int.class);
    return (Integer) indexM.invoke(str, ".", 8);
} catch (NoSuchMethodException e) {
    failure = e;
} catch (InvocationTargetException e) {
    failure = e.getCause();
} catch (IllegalAccessException e) {
    failure = e;
}
throw failure;

反射代码具有语义上等效的安全检查,但编译器对直接调用所做的检查在使用 invoke() 时只能在运行时进行。访问检查的方式可能有所不同,即使可以直接调用某个方法,安全管理器也可能拒绝访问该方法。因此,在可能的情况下应避免使用这种调用方式。不过,在编写调试器或其他需要解释用户输入作为对象操作的通用应用程序时,使用 invoke() Field get/set 方法是合理的。

8. 创建新对象和Constructor类

可以使用 Class 对象的 newInstance() 方法创建其表示类型的新实例(对象),该方法调用类的无参构造函数并返回新创建对象的引用。对于 Class<T> 类型的类对象,返回的对象类型为 T

例如,以下是一个修改后的 TestSort 类的 main 方法:

static double[] testData = { 0.3, 1.3e-2, 7.9, 3.17 };
public static void main(String[] args) {
    try {
        for (String name : args) {
            Class<?> classFor = Class.forName(name);
            SortDouble sorter
                = (SortDouble) classFor.newInstance();
            SortMetrics metrics
                = sorter.sort(testData);
            System.out.println(name + ": " + metrics);
            for (double data : testData)
                System.out.println("\t" + data);
        }
    } catch (Exception e) {
        System.err.println(e);        // report the error
    }
}

此方法可用于测试任何提供无参构造函数的 SortDouble 子类。

需要注意的是, newInstance() 返回 T ,而 Class.forName() 返回 Class<?> ,这意味着 newInstance() 返回的实际对象类型未知,因此需要进行强制类型转换。也可以使用 asSubclass() 方法获取所需确切类型的类对象,然后调用 newInstance() 而无需强制类型转换:

Class<? extends SortDouble> classFor =
    Class.forName(name).asSubclass(SortDouble.class);
SortDouble sorter = classFor.newInstance();

无论哪种情况,若指定的类不是 SortDouble 的子类型,将抛出 ClassCastException

newInstance() 方法若使用不当,可能会抛出多种不同的异常。若类没有无参构造函数、是抽象类或接口,或者由于其他原因创建失败,将抛出 InstantiationException ;若类或无参构造函数不可访问,将抛出 IllegalAccessException ;若当前安全策略不允许创建新对象,将抛出 SecurityException ;创建新对象可能需要初始化类,因此也可能抛出 ExceptionInInitializerError

Class newInstance() 方法仅调用无参构造函数。若要调用其他构造函数,必须使用 Class 对象获取相关的 Constructor 对象,并在该 Constructor 上调用 newInstance() ,传递适当的参数。

Constructor 类结合其继承的 Member 方法,能让我们获取构造函数声明的完整信息,并调用构造函数以获取该类的新实例。以下是一些关键方法:
- getGenericParameterTypes() :返回一个 Type 对象数组,包含该构造函数接受的每个参数类型,按参数声明顺序排列。若构造函数无参数,则返回空数组。
- getGenericExceptionTypes() :返回一个 Type 对象数组,包含该构造函数 throws 子句中列出的每个异常类型,按声明顺序排列。若没有声明异常,则返回空数组。

Method 对象类似,上述方法也有对应的遗留版本 getParameterTypes() getExceptionTypes()

Constructor 类与 Method 类类似,实现了相同的接口( AnnotatedElement GenericDeclaration ),并定义了类似的方法( getParameterAnnotations() isVarArgs() )。

要从 Constructor 对象创建类的新实例,可调用其 newInstance() 方法:

public T newInstance(Object... args) throws InstantiationException, 
IllegalAccessException, IllegalArgumentException, InvocationTargetException

该方法使用此 Constructor 对象表示的构造函数创建并初始化构造函数声明类的新实例,使用指定的初始化参数。返回新初始化对象的引用。 Constructor.newInstance() Method.invoke() 非常相似, args 值的数量必须等于构造函数的参数数量,且这些值的类型必须都可赋值给构造函数的参数类型,否则将抛出 IllegalArgumentException 。对于可变参数构造函数,最后一个参数是一个数组,必须用要传递的实际“可变”参数填充。若声明类是抽象类,将抛出 InstantiationException ;若构造函数不可访问,将抛出 IllegalAccessException ;若构造函数本身抛出异常,将抛出 InvocationTargetException ,其原因是该异常。

若通过通配符引用引用 Constructor 对象,则必须将 newInstance() 返回的对象强制转换为正确的类型。

9. 内部类构造函数

内部类(不包括局部和匿名内部类)永远不会有无参构造函数,因为编译器会将所有内部类构造函数转换为接受一个指向外部对象的第一个参数。这意味着不能使用 Class.newInstance() 创建内部类对象,必须使用 Constructor 对象。内部类的 Constructor 对象反映的是转换后的代码,而非程序员编写的代码。

例如,考虑 BankAccount 类及其关联的内部 Action 类。 Action 类有一个接受 String 参数和 long 值的构造函数。使用 getDeclaredConstructors() 获取该 Constructor 对象并使用 toString() 打印其签名,将得到:

BankAccount$Action(BankAccount,java.lang.String,long)

可以按以下方式检索此构造函数:

Class<Action> actionClass = Action.class;
Constructor<Action> con =
    actionClass.getDeclaredConstructor(BankAccount.class,
            String.class, long.class);

若要构造一个 Action 对象,必须提供一个适当的外部对象引用:

BankAccount acct = new BankAccount();
// ...
Action a = con.newInstance(acct, "Embezzle", 10000L);
10. 总结与建议

以下是对Java反射机制中各部分的总结表格:
| 类/接口 | 主要功能 | 关键方法 |
| ---- | ---- | ---- |
| AnnotatedElement | 查询注解信息 | getAnnotations() getDeclaredAnnotations() getAnnotation() isAnnotationPresent() |
| Modifier | 处理非注解修饰符 | isMod() |
| Member | 定义成员的通用属性 | getDeclaringClass() getName() getModifiers() isSynthetic() |
| AccessibleObject | 控制访问检查 | setAccessible() isAccessible() |
| Field | 操作字段 | getGenericType() getType() get() set() getShort() setShort() 等 |
| Method | 操作方法 | getGenericReturnType() getGenericParameterTypes() getGenericExceptionTypes() invoke() |
| Constructor | 操作构造函数 | getGenericParameterTypes() getGenericExceptionTypes() newInstance() |

Java反射机制提供了强大的功能,能让我们在运行时动态地操作类、方法、字段和构造函数。但反射也带来了一些问题,如性能开销、代码可读性降低等。因此,在使用反射时,应遵循以下建议:
- 谨慎使用 :仅在必要时使用反射,如编写调试器、通用工具类等场景。
- 注意异常处理 :反射操作可能会抛出多种异常,应在代码中进行适当的异常处理。
- 性能优化 :尽量减少反射操作的使用频率,避免在性能敏感的代码中使用反射。

通过深入理解Java反射机制的各个方面,我们可以在合适的场景中灵活运用,为开发带来更多的可能性。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值