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反射机制的各个方面,我们可以在合适的场景中灵活运用,为开发带来更多的可能性。
超级会员免费看
587

被折叠的 条评论
为什么被折叠?



