本文基于官方文档翻译,并补充个人理解部分
全文超长,阅读时间可能超过2小时,建议分开阅读
文章目录
反射API(The Reflection API)
反射的用途
反射通常被那些需要检查或修改运行在JVM上的程序的运行时行为的程序所使用。这是一个相对高级的功能,只应被对语言基础有牢固掌握的开发者使用。考虑到这一点,反射是一个强大的技术,能够使应用程序执行原本不可能完成的操作。
-
扩展特性
应用程序可以通过使用其全限定名称创建扩展对象的实例,来利用外部、用户定义的类。
-
类浏览器和可视化开发环境
类浏览器需要能够枚举类的成员。可视化开发环境可以从反射中可用的类型信息中受益,帮助开发者编写正确的代码。
-
调试器和测试工具
调试器需要能够检查类上的私有成员。测试框架可以利用反射系统地调用类上定义的一组可发现的API,以确保测试套件中有高水平的代码覆盖率。
反射的缺点
反射功能强大,但不应随意使用。如果可能在不使用反射的情况下执行操作,则最好避免使用它。以下是在通过反射访问代码时应记住的几点关注事项。
-
性能开销
由于反射涉及动态解析的类型,某些Java虚拟机优化无法执行。因此,反射操作的性能比非反射操作慢,应避免在性能敏感的应用程序中频繁调用的代码段中使用。
-
安全限制
反射需要运行时权限,当在安全管理器下运行时可能不存在此权限。这是代码必须在受限安全环境下运行的重要考虑因素,比如在Applet中。
-
内部细节的暴露
由于反射允许代码执行在非反射代码中非法的操作,如访问
private
字段和方法,反射的使用可能导致意外的副作用,可能使代码失效并破坏可移植性。反射代码打破了抽象,因此可能随着平台升级而改变行为。
教程课程
本教程涵盖了使用反射访问和操作类、字段、方法和构造器的常见用途。每一课都包含代码示例、提示和疑难解答信息。
-
本课展示了获取
Class
对象的各种方式,并使用它来检查类的属性,包括其声明和内容。 -
本课描述了如何使用反射API查找类的字段、方法和构造器。提供了设置和获取字段值、调用方法以及使用特定构造器创建对象新实例的示例。
-
本课介绍了两种特殊的类类型:数组,它们在运行时生成,以及
enum
类型,它们定义了独特的命名对象实例。示例代码展示了如何检索数组的组件类型,以及如何设置和获取具有数组或enum
类型的字段。
注意:
本教程中的示例旨在用于实验反射API。因此,异常处理与生产代码中使用的不同。特别是,在生产代码中不建议向用户显示堆栈跟踪。
教程:类
每种类型要么是引用类型,要么是原始类型。类、枚举和数组(所有这些都继承自java.lang.Object
,包括java.lang.class
)以及接口都是引用类型。引用类型的例子包括java.lang.String
、所有原始类型的的包装类(如java.lang.Double
)、接口(java.io.Serializable
),以及枚举(javax.swing.SortOrder
)。原始类型有一组固定集合:boolean
、byte
、short
、int
、long
、char
、float
和double
。
对于每种类型的对象,Java虚拟机都会实例化一个不可变的java.lang.Class
类的实例,该类提供了用于检查对象的运行时属性的方法,包括其成员和类型信息。Class
还提供了创建新类和对象的能力。最重要的是,它是所有反射API的入口点。本课涵盖涉及类的最常用的反射操作:
- 检索类对象:描述了获取
Class
对象的方法。 - 检查类修饰符和类型:展示如何访问类声明信息。
- 发现类成员:说明如何列出类中的构造器、字段、方法和嵌套类。
- 疑难解答:描述了使用
Class
时常见的错误。
检索类对象
反射操作的起点是java.lang.Class
。除了java.lang.reflect.ReflectPermission
之外,java.lang.reflect
包下的其他类都没有公共构造函数。要使用这些类,必须在Class
上调用适当的方法。根据代码是否可以访问对象、类名、类型或现有的Class
,有几种不同的方式来获取Class
对象。
使用Object.getClass()
如果有一个对象的实例,那么获取其Class
的最简单方法是调用Object.getClass()
。当然,这仅适用于所有继承自Object
的引用类型。以下是一些示例:
Class c = "foo".getClass();
返回String
的Class
对象
Class c = System.console().getClass();
这是与虚拟机关联的唯一的控制台,这个控制台是由静态方法System.console()
返回的。通过调用getClass()
返回的值是对应于java.io.Console
的Class
对象。
enum E { A, B }
Class c = A.getClass();
A
是枚举E
的一个实例;;因此getClass()
返回的Class
对象对应于枚举类型E
byte[] bytes = new byte[1024];
Class c = bytes.getClass();
由于数组是Objects
(具体来说,它们继承自java.lang.Object
),因此也可以在数组实例上调用getClass()
方法。返回的Class
对象对应于具有byte
组件类型的数组。
import java.util.HashSet;
import java.util.Set;
Set<String> s = new HashSet<String>();
Class c = s.getClass();
在这个例子中,java.util.Set
是一个接口,指向一个java.util.HashSet
类型的对象。由getClass()
方法返回的值是对应于java.util.HashSet
类的Class
对象。
使用.class
语法
如果类型可用但没有实例,可以通过在类型名后加上.class
来获取Class
。这也是获取原始类型Class
的最简单方式。
boolean b;
Class c = b.getClass(); // compile-time error
Class c = boolean.class; // correct
注意,语句boolean.getClass()
会导致编译时错误,因为boolean
是一个原始类型,不能被取消引用。.class
语法返回对应于boolean
类型的Class
对象。
什么是【取消引用】?
原始变量和对象变量是 Java 中可以使用的两种类型的变量。 但是,只有对象变量可以是引用类型。 int 数据类型是原始数据类型而不是对象。
【访问引用所指向的值】就是所谓的取消引用。 不可能取消引用 int,因为它已经是一个值,而不是对其他任何东西的引用。
Class c = java.io.PrintStream.class;
变量c
将会是对应于java.io.PrintStream
类型的Class
对象。
Class c = int[][][].class; // 返回[[[I
.class
语法可用于检索对应于给定类型多维数组的Class
。
Class.forName()
如果已知类的全限定名,可以使用静态方法Class.forName()
来获取对应的Class
对象。但这种语法不能用于原始类型。
描述数组类名的语法可以用Class.getName()
方法。这种语法适用于引用类型和原始类型的数组。
Class c = Class.forName("com.duke.MyLocaleServiceProvider");
这条语句将根据给定的全限定名创建一个类的Class
对象。
Class cDoubleArray = Class.forName("[D");
Class cStringArray = Class.forName("[[Ljava.lang.String;");
变量cDoubleArray
将包含对应于double
原始类型数组的Class
对象(即,与double[].class
相同)。变量cStringArray
将包含对应于二维String
数组的Class
对象(即,与String[][].class
相同)。
个人补充:
Class c = double[].class; System.out.println(c.getName()); // 输出:[D Class s = String[][].class; System.out.println(s.getName()); // 输出[[Ljava.lang.String;
原始类型包装类的TYPE
字段
尽管.class
语法是获取原始类型Class
的更方便且首选的方式,但还有另一种方法。每个原始类型和void
在java.lang
中都有一个用于将原始类型转换为引用类型的包装类。每个包装类都包含一个名为TYPE
的字段,等于被包装的原始类型的Class
。
Class c = Double.TYPE;
这是一个包装了原始类型double
的java.lang.Double
类,用于在需要Object
类时。Double.TYPE
的值与double.class
完全相同。
【用于在需要
Object
类时】意思是Double可以作为Object类的子类使用
Class c = Void.TYPE;
Void.TYPE
的值与void.class
完全相同
返回Classes
的方法
存在多个反射API返回Classes
,但这些方法只能在直接或间接获得Class
对象后访问。
-
返回给定类的超类。
Class c = javax.swing.JButton.class.getSuperclass();
javax.swing.JButton
的超类是javax.swing.AbstractButton
。 -
返回类的所有公开成员类、接口和枚举,包括继承的成员类。
Class<?>[] c = Character.class.getClasses();`
-
返回类中显式声明的所有类、接口和枚举。
Class<?>[] c = Character.class.getDeclaredClasses();
Character
包含两个公开的成员类Character.Subset
和Character.UnicodeBlock
以及一个私有类Character.CharacterCache
. -
java.lang.reflect.Constructor.getDeclaringClass()
返回声明这些成员的
Class
。匿名类(Anonymous Class Declarations )声明不会有声明类(declaring class),但会有包围类(enclosing class)。补充:什么是【包围类】
在Java中,我们可以在一个类(A)的内部定义另一个类(B)。这个类A就是包围类,他把类B围起来了(注意如果有多重包围,只返回第一层包围)。
所以对于匿名内部类来说
Predicate<String> predicate = new Predicate<String>() { @Override public boolean test(String s) { return false; } }; Class<?> aClass = predicate.getClass().getDeclaringClass(); // 返回null System.out.println(aClass.getName()); //java.lang.NullPointerException
我们可以修改方法为
getEnclosingClass()
,这样子就可以获取到匿名类所在的类Class<?> aClass = predicate.getClass().getEnclosingClass();
注意,如果使用lambda表达式定义
getEnclosingClass()
将会返回null,因为lambda表达式不会创建类import java.lang.reflect.Field; Field f = System.class.getField("out"); Class c = f.getDeclaringClass(); System.out.println(c.getName());
out
字段是在System
中声明的。public class MyClass { static Object o = new Object() { public void m() {} }; static Class<c> = o.getClass().getEnclosingClass(); }
匿名类o的声明类将会是null
-
返回类的直接包围类。
Class c = Thread.State.class().getEnclosingClass();
枚举类
Thread.State
的包围类是Thread
。public class MyClass { static Object o = new Object() { public void m() {} }; static Class<c> = o.getClass().getEnclosingClass(); }
匿名类
o
的包围类是MyClass
。
检查类修饰符和类型
类可以声明一个或多个修饰符,这些修饰符影响其运行时行为:
- 访问修饰符:
public
、protected
和private
- 要求重写的修饰符:
abstract
- 限制为单个实例的修饰符:
static
- 禁止值修改的修饰符:
final
- 强制严格浮点行为的修饰符:
strictfp
- 注解
并非所有修饰符都适用于所有类,例如接口不能是final
,枚举不能是abstract
。java.lang.reflect.Modifier
包含了所有可能修饰符的声明。它还包含了一些方法,可以用来解析由Class.getModifiers()
返回的修饰符集合。
ClassDeclarationSpy
示例展示了如何获取类的声明组件,包括修饰符、泛型类型参数、实现的接口以及继承路径。由于Class
实现了java.lang.reflect.AnnotatedElement
接口,因此也可以查询运行时注解。
import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import static java.lang.System.out;
public class ClassDeclarationSpy {
public static void main(String... args) {
try {
Class<?> c = Class.forName(args[0]);
out.format("Class:%n %s%n%n", c.getCanonicalName());
out.format("Modifiers:%n %s%n%n",
Modifier.toString(c.getModifiers()));
out.format("Type Parameters:%n");
TypeVariable[] tv = c.getTypeParameters();
if (tv.length != 0) {
out.format(" ");
for (TypeVariable t : tv)
out.format("%s ", t.getName());
out.format("%n%n");
} else {
out.format(" -- No Type Parameters --%n%n");
}
out.format("Implemented Interfaces:%n");
Type[] intfs = c.getGenericInterfaces();
if (intfs.length != 0) {
for (Type intf : intfs)
out.format(" %s%n", intf.toString());
out.format("%n");
} else {
out.format(" -- No Implemented Interfaces --%n%n");
}
out.format("Inheritance Path:%n");
List<Class> l = new ArrayList<Class>();
printAncestor(c, l);
if (l.size() != 0) {
for (Class<?> cl : l)
out.format(" %s%n", cl.getCanonicalName());
out.format("%n");
} else {
out.format(" -- No Super Classes --%n%n");
}
out.format("Annotations:%n");
Annotation[] ann = c.getAnnotations();
if (ann.length != 0) {
for (Annotation a : ann)
out.format(" %s%n", a.toString());
out.format("%n");
} else {
out.format(" -- No Annotations --%n%n");
}
// production code should handle this exception more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
private static void printAncestor(Class<?> c, List<Class> l) {
Class<?> ancestor = c.getSuperclass();
if (ancestor != null) {
l.add(ancestor);
printAncestor(ancestor, l);
}
}
}
以下是一些输出样本。请注意,用户输入将以斜体显示。
// jdk1.8控制台进入src使用【全限定名如reflection.ClassDeclarationSpy】执行下面语句
$ java ClassDeclarationSpy java.util.concurrent.ConcurrentNavigableMap
Class:
java.util.concurrent.ConcurrentNavigableMap
Modifiers:
public abstract interface
Type Parameters:
K V
Implemented Interfaces:
java.util.concurrent.ConcurrentMap<K, V>
java.util.NavigableMap<K, V>
Inheritance Path:
-- No Super Classes --
Annotations
-- No Annotations --
源码中 java.util.concurrent.ConcurrentNavigableMap
的实际声明
public interface ConcurrentNavigableMap<K,V>
extends ConcurrentMap<K,V>, NavigableMap<K,V>
值得注意的是,由于这是一个接口,它隐含地声明为abstract
的。编译器会为每一个接口添加这个修饰符。此外,此声明包含了两个泛型类型参数,K
和V
。示例代码仅仅打印了这些参数的名字,但实际上,你可以使用 java.lang.reflect.TypeVariable
中的方法来检索更多关于它们的信息。此外,接口也可以实现其他的接口,如上文。
$ java ClassDeclarationSpy "[Ljava.lang.String;"
Class:
java.lang.String[]
Modifiers:
public abstract final
Type Parameters:
-- No Type Parameters --
Implemented Interfaces:
interface java.lang.Cloneable
interface java.io.Serializable
Inheritance Path:
java.lang.Object
Annotations:
-- No Annotations --
由于数组是运行时对象,所有的类型信息由Java虚拟机定义。具体来说,数组实现了 Cloneable
和 java.io.Serializable
接口,并且它们的直接超类总是 Object
。
$ java ClassDeclarationSpy java.io.InterruptedIOException
Class:
java.io.InterruptedIOException
Modifiers:
public
Type Parameters:
-- No Type Parameters --
Implemented Interfaces:
-- No Implemented Interfaces --
Inheritance Path:
java.io.IOException
java.lang.Exception
java.lang.Throwable
java.lang.Object
Annotations:
-- No Annotations --
从继承路径可以推断出java.io.InterruptedIOException
是一个受检异常(checked exception),因为RuntimeException
并不在其继承链中出现。
$ java ClassDeclarationSpy java.security.Identity
Class:
java.security.Identity
Modifiers:
public abstract
Type Parameters:
-- No Type Parameters --
Implemented Interfaces:
interface java.security.Principal
interface java.io.Serializable
Inheritance Path:
java.lang.Object
Annotations:
@java.lang.Deprecated()
此输出表明java.security.Identity
,,一个已废弃的API,具有java.lang.Deprecated
注解。反射代码可以利用这一点来检测废弃的API。
**注意:**并非所有注解都能通过反射访问。只有那些具有java.lang.annotation.RetentionPolicy
的 RUNTIME
保留策略的注解才可以在运行时访问。在语言中预定义的三个注解 @Deprecated
, @Override
,和@SuppressWarnings
。只有注解在运行时可用。
发现类的成员
Class
提供了两种访问字段、方法和构造器的方法:一种是枚举这些成员的方法,另一种是搜索特定成员的方法。同时,也有专门的方法用于访问直接在类上声明的成员,与搜索父类接口(superinterfaces )和父类(superclasses )中继承成员的方法区分开来。下表总结了所有成员获取(member-locating)方法及其特性。
获取字段(Fields)的Class方法
Class API / 能否获取 | 成员列表? | 继承成员? | 私有成员? |
---|---|---|---|
getDeclaredField() | no | no | yes |
getField() | no | yes | no |
getDeclaredFields() | yes | no | yes |
getFields() | yes | yes | no |
获取方法(Methods)的Class方法
Class API / 能否获取 | 成员列表? | 继承成员? | 私有成员? |
---|---|---|---|
getDeclaredMethod() | no | no | yes |
getMethod() | no | yes | no |
getDeclaredMethods() | yes | no | yes |
getMethods() | yes | yes | no |
获取构造器(Constructors)的Class方法
Class API / 能否获取 | 成员列表? | 继承成员?[1] | 私有成员? |
---|---|---|---|
getDeclaredConstructor() | no | N/A | yes |
getConstructor() | no | N/A | no |
getDeclaredConstructors() | yes | N/A | yes |
getConstructors() | yes | N/A | no |
[1] 构造器不会被继承。
给定一个类名和感兴趣的成员类型声明, ClassSpy
示例使用get*s()
方法来确定所有公共元素的列表,包括任何继承的元素。
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Member;
import static java.lang.System.out;
enum ClassMember {CONSTRUCTOR, FIELD, METHOD, CLASS, ALL}
public class ClassSpy {
public static void main(String... args) {
try {
Class<?> c = Class.forName(args[0]);
out.format("Class:%n %s%n%n", c.getCanonicalName());
Package p = c.getPackage();
out.format("Package:%n %s%n%n",
(p != null ? p.getName() : "-- No Package --"));
for (int i = 1; i < args.length; i++) {
switch (ClassMember.valueOf(args[i])) {
case CONSTRUCTOR:
printMembers(c.getConstructors(), "Constructor");
break;
case FIELD:
printMembers(c.getFields(), "Fields");
break;
case METHOD:
printMembers(c.getMethods(), "Methods");
break;
case CLASS:
printClasses(c);
break;
case ALL:
printMembers(c.getConstructors(), "Constuctors");
printMembers(c.getFields(), "Fields");
printMembers(c.getMethods(), "Methods");
printClasses(c);
break;
default:
assert false;
}
}
// production code should handle these exceptions more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
private static void printMembers(Member[] mbrs, String s) {
out.format("%s:%n", s);
for (Member mbr : mbrs) {
if (mbr instanceof Field)
out.format(" %s%n", ((Field) mbr).toGenericString());
else if (mbr instanceof Constructor)
out.format(" %s%n", ((Constructor) mbr).toGenericString());
else if (mbr instanceof Method)
out.format(" %s%n", ((Method) mbr).toGenericString());
}
if (mbrs.length == 0)
out.format(" -- No %s --%n", s);
out.format("%n");
}
private static void printClasses(Class<?> c) {
out.format("Classes:%n");
Class<?>[] clss = c.getClasses();
for (Class<?> cls : clss)
out.format(" %s%n", cls.getCanonicalName());
if (clss.length == 0)
out.format(" -- No member interfaces, classes, or enums --%n");
out.format("%n");
}
}
这个示例相对紧凑;但是printMembers()
方法却略显笨拙,这是因为java.lang.reflect.Member
接口自反射最早的实现以来就已存在,当引入泛型时,无法对其进行修改以包含更有用的getGenericString()
方法。唯一的选择是如示例所示进行测试和类型转换,或者用printConstructors()
、printFields()
和printMethods()
替换这个方法,或者满足于Member.getName()
相对简略的结果。
以下是一些输出样本及其解释。用户输入将以斜体显示。
$ java ClassSpy java.lang.ClassCastException CONSTRUCTOR
Class:
java.lang.ClassCastException
Package:
java.lang
Constructor:
public java.lang.ClassCastException()
public java.lang.ClassCastException(java.lang.String)
由于构造器不会被继承,因此在直接的超类RuntimeException
和其他超类中定义的异常链机制(chaining mechanism)构造器(那些带有Throwable
参数的构造器)将不会被找到。
什么是【链机制(chaining mechanism)构造器】
子类(派生类)必须重载父类的构造函数,并且显式或隐式的调用父类构造函数,这就是链机制(chaining mechanism)构造器
$ java ClassSpy java.nio.channels.ReadableByteChannel METHOD
Class:
java.nio.channels.ReadableByteChannel
Package:
java.nio.channels
Methods:
public abstract int java.nio.channels.ReadableByteChannel.read
(java.nio.ByteBuffer) throws java.io.IOException
public abstract void java.nio.channels.Channel.close() throws
java.io.IOException
public abstract boolean java.nio.channels.Channel.isOpen()
接口 java.nio.channels.ReadableByteChannel
声明了方法 read()
。剩余的方法继承自父类接口。这段代码很容易修改,只需将get*s()
替换为getDeclared*s()
,就可以仅列出在类中实际声明的方法(无继承的)。
$ java ClassSpy.java ClassMember FIELD METHOD
Class:
ClassMember
Package:
-- No Package --
Fields:
public static final ClassMember ClassMember.CONSTRUCTOR
public static final ClassMember ClassMember.FIELD
public static final ClassMember ClassMember.METHOD
public static final ClassMember ClassMember.CLASS
public static final ClassMember ClassMember.ALL
Methods:
public static ClassMember ClassMember.valueOf(java.lang.String)
public static ClassMember[] ClassMember.values()
public final int java.lang.Enum.hashCode()
public final int java.lang.Enum.compareTo(E)
public int java.lang.Enum.compareTo(java.lang.Object)
public final java.lang.String java.lang.Enum.name()
public final boolean java.lang.Enum.equals(java.lang.Object)
public java.lang.String java.lang.Enum.toString()
public static <T> T java.lang.Enum.valueOf
(java.lang.Class<T>,java.lang.String)
public final java.lang.Class<E> java.lang.Enum.getDeclaringClass()
public final int java.lang.Enum.ordinal()
public final native java.lang.Class<?> java.lang.Object.getClass()
public final native void java.lang.Object.wait(long) throws
java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws
java.lang.InterruptedException
public final void java.lang.Object.wait() hrows java.lang.InterruptedException
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
在这些结果的字段部分,枚举常量被列出。虽然从技术上讲,它们确实被视为字段,但将它们与其它字段区分开来可能是有用的。这个示例可以通过使用 java.lang.reflect.Field.isEnumConstant()
方法来进行修改,以便更好地达到这一目的。在本教程稍后部分的 检查枚举 中的 EnumSpy
示例,包含了一种可能的实现。
在输出的方法部分,可以看到方法名称包含了声明类的名字。因此,toString()
方法是由 Enum
类实现的,而不是从 Object
类继承的。代码可以通过使用 Field.getDeclaringClass()
方法来修改,以更明显地展示这一点。下面的代码片段展示了潜在解决方案的一部分:
if (mbr instanceof Field) {
Field f = (Field)mbr;
out.format(" %s%n", f.toGenericString());
out.format(" -- declared in: %s%n", f.getDeclaringClass());
}
疑难解答
以下示例展示了在反射类时可能遇到的典型错误。
编译器警告:“Note: … uses unchecked or unsafe operations”(使用了未经检查或不安全的操作)
当调用一个方法时,会检查参数值的类型并可能进行转换。 ClassWarning
示例通过调用 getMethod()
方法导致了一个典型的未经检查的转换警告:
import java.lang.reflect.Method;
public class ClassWarning {
void m() {
try {
Class c = ClassWarning.class;
Method m = c.getMethod("m"); // warning
// production code should handle this exception more gracefully
} catch (NoSuchMethodException x) {
x.printStackTrace();
}
}
}
$ javac ClassWarning.java
Note: ClassWarning.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
$ javac -Xlint:unchecked ClassWarning.java
ClassWarning.java:6: warning: [unchecked] unchecked call to getMethod
(String,Class<?>...) as a member of the raw type Class
Method m = c.getMethod("m"); // warning
^
1 warning
许多库方法已经被添加了泛型声明,包括 Class
类中的多个方法。由于 c
被声明为原始类型(没有类型参数),而 getMethod()
方法的对应参数是一个参数化类型,因此会发生未经检查的转换。编译器必须生成一个警告。(参见 The Java Language Specification, Java SE 7 Edition 中的“未经检查的转换” 和 “方法调用转换”)
有两种可能的解决方案。更可取的是修改 c
的声明,以包含适当的泛型类型。在这种情况下,声明应该是:
Class<?> c = warn.getClass();
或者,可以使用预定义的注解@SuppressWarnings
明确地抑制警告,在问题语句之前添加此注解。
Class c = ClassWarning.class;
@SuppressWarnings("unchecked")
Method m = c.getMethod("m");
// warning gone
提示: 作为一般原则,不应忽略警告,因为它们可能表明存在bug。应适当使用参数化声明。如果这不可能(也许是因为应用程序必须与库提供者的代码(library vendor’s code)交互),则使用@SuppressWarnings
注解标记违规行。
当构造器不可访问时抛出 InstantiationException
如果尝试创建类的新实例,但零参数构造器不可见,则 Class.newInstance()
将抛出 InstantiationException
。ClassTrouble
示例演示了由此产生的堆栈跟踪。
class Cls {
private Cls() {}
}
public class ClassTrouble {
public static void main(String... args) {
try {
Class<?> c = Class.forName("Cls");
c.newInstance(); // InstantiationException
// production code should handle these exceptions more gracefully
} catch (InstantiationException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
}
$ java ClassTrouble
java.lang.IllegalAccessException: Class ClassTrouble can not access a member of
class Cls with modifiers "private"
at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:65)
at java.lang.Class.newInstance0(Class.java:349)
at java.lang.Class.newInstance(Class.java:308)
at ClassTrouble.main(ClassTrouble.java:9)
Class.newInstance()
的行为非常类似于 new
关键字,并且会因与 new
失败相同的理由而失败。在反射中,典型的解决策略是利用java.lang.reflect.AccessibleObject
类提供的抑制访问控制检查的能力;但是,这种方法在这里不适用,因为 java.lang.Class
没有继承自 AccessibleObject
。唯一的解决途径是修改代码,使用Constructor.newInstance()
,它确实继承自AccessibleObject
。
提示: 通常,在 成员 章节,出于在 创建新的类对象 部分所述的理由,更倾向于使用 Constructor.newInstance()
。
更多关于使用 Constructor.newInstance() 可能遇到的问题的示例,可以在 成员 教程的 构造器疑难解答 部分找到。
课程:成员
Reflection 定义了一个接口,该接口java.lang.reflect.Member
由 java.lang.reflect.Field
, java.lang.reflect.Method
和java.lang.reflect.Constructor
实现[1]。本课将讨论这些对象。对于每个成员,本课程将描述**用于检索声明和类型信息的关联 API、成员特有的任何操作(例如,设置字段值或调用方法)以及常见遇到的错误。**每个概念都将通过代码示例和相关输出进行说明,这些输出近似于一些预期的反射用途。
[1]补充:Member的实现类一共有5个
其中Executable(可执行的)为抽象类,其子类包括Contructor和Method。还有一个MemberName
注: 根据 Java 语言规范 Java SE 7 Edition( The Java Language Specification, Java SE 7 Edition),类的成员是类主体的可继承部分,包括字段、方法、嵌套类、接口和枚举类型。由于构造函数不可继承,因此它们不是成员。这与java.lang.reflect.Member
的实现类不同。
思考一下为什么构造函数不可继承?
字段(Field)
字段具有类型(type)和值(value)。该 java.lang.reflect.Field
类提供了用于访问类型信息以及设置(setting)和获取(getting)给定对象上字段值的方法。
- 获取字段类型(Obtaining Field Types )介绍如何获取字段的声明类型和泛型类型
- 检索和分析字段修饰符(Retrieving and Parsing Field Modifiers )演示如何获取字段声明的部分内容,例如
public
transient
- 获取和设置字段值(Getting and Setting Field Values)说明了如何访问字段值
- 疑难解答(Troubleshooting )介绍了一些可能导致混淆的常见编码错误
方法(Methods)
方法具有返回值、参数,并且可能会引发异常。该 java.lang.reflect.Method 类提供用于获取参数和返回值的类型信息的方法。它还可用于调用给定对象上的方法。
- 获取方法类型信息(Obtaining Method Type Information)演示如何枚举类中声明的方法并获取类型信息
- 获取方法参数的名称(Obtaining Names of Method Parameters)演示如何检索方法或构造函数参数的名称和其他信息
- 检索和分析方法修饰符(Retrieving and Parsing Method Modifiers)描述了如何访问和解码修饰符以及与方法关联的其他信息
- 调用方法(Invoking Methods )阐释了如何执行方法并获取其返回值
- 疑难解答(Troubleshooting)涵盖查找或调用方法时遇到的常见错误
构造函数(Constructors)
构造函数的反射 API 在java.lang.reflect.Constructor
中定义,并且与方法的反射 API 类似,但有两个主要例外:首先,构造函数没有返回值;其次,构造函数的调用为给定类创建对象的新实例。
- 查找构造函数(Finding Constructors)演示了如何检索具有特定参数的构造函数
- 检索和分析构造函数修饰符(Retrieving and Parsing Constructor Modifiers)演示如何获取构造函数声明的修饰符以及有关构造函数的其他信息
- 创建新的类实例(Creating New Class Instances )演示如何通过调用对象的构造函数来实例化对象的实例
- 疑难解答(Troubleshooting )描述了在查找或调用构造函数时可能遇到的常见错误
字段
字段是具有关联值的类、接口或枚举。 java.lang.reflect.Field
类中的方法可以检索有关字段的信息,例如字段的名称、类型、修饰符和批注。(“类”课程中的“检查类修饰符和类型(Examining Class Modifiers and Types)”一节介绍了如何检索批注。还有一些方法可以动态访问和修改字段的值。以下各节介绍了这些任务:
- 获取字段类型(Obtaining Field Types )介绍如何获取字段的声明类型和泛型类型
- 检索和分析字段修饰符(Retrieving and Parsing Field Modifiers)演示如何获取字段声明的部分内容,例如
public
transient
- 获取和设置字段值(Getting and Setting Field Values)说明了如何访问字段值
- 疑难解答(Troubleshooting )介绍了一些可能导致混淆的常见编码错误
在编写应用程序(如类浏览器)时,找出哪些字段属于特定类可能很有用。类的字段通过调用 Class.getFields()
来标识。该 getFields()
方法返回一个对象数组,Field
其中包含每个可访问的公共字段一个对象。
如果公共字段是以下任一成员,则该字段是可访问的(accessible ):
- 这个类的(this class)
- 这个类的父类的(a superclass of this class)
- 这个类实现的接口的(an interface implemented by this class)
- 这个类实现的接口继承别的接口的(an interface extended from an interface implemented by this class)
字段可以是类(实例)字段,例如 java.io.Reader.lock
,静态字段,例如java.lang.Integer.MAX_VALUE
,也可以是枚举常量,例如 java.lang.Thread.State.WAITING
。
获取字段类型
字段可以是基本类型或引用类型。有八种基本类型:boolean,byte,short,int,long,char,float,double
。引用类型是可以是java.lang.Object
的任何直接或间接的子类,包括接口、数组和枚举类型。
该 FieldSpy
示例打印字段的类型和泛型类型,并给出一个完全限定的二进制类名和字段名。
import java.lang.reflect.Field;
import java.util.List;
public class FieldSpy<T> {
public boolean[][] b = {{ false, false }, { true, true } };
public String name = "Alice";
public List<Integer> list;
public T val;
public static void main(String... args) {
try {
Class<?> c = Class.forName(args[0]);
Field f = c.getField(args[1]);
System.out.format("Type: %s%n", f.getType());
System.out.format("GenericType: %s%n", f.getGenericType());
// production code should handle these exceptions more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
} catch (NoSuchFieldException x) {
x.printStackTrace();
}
}
}
检索此类中三个公共字段的类型( b
、 name
和参数化类型 list
)的示例输出如下。用户输入为$开头的。
$ java FieldSpy FieldSpy b
Type: class [[Z
GenericType: class [[Z
$ java FieldSpy FieldSpy name
Type: class java.lang.String
GenericType: class java.lang.String
$ java FieldSpy FieldSpy list
Type: interface java.util.List
GenericType: java.util.List<java.lang.Integer>
$ java FieldSpy FieldSpy val
Type: class java.lang.Object
GenericType: T
字段 b
的类型是布尔值的二维数组。类型名称的语法如 中 Class.getName()
所述。
字段 val
的类型被报告为 java.lang.Object
,因为泛型是通过类型擦除实现的,该类型擦除会在编译过程中删除有关泛型类型的所有信息。因此 T
被类型变量的上界替换,在本例中为 java.lang.Object
。
Field.getGenericType()
将查阅类文件中的 Signature
属性(如果存在)。如果这个属性不可用,那么它将回退到使用Field.getType()
方法,这个方法并没有因为泛型的引入而改变。在反射中,其他以 getGenericFoo()
命名的方法(其中 Foo
代表某种特定的类型信息)都是以类似的方式实现的。
Signature
属性:在泛型篇中讲过,参数化类型在类型擦除后实际泛型类型会保存在Signature
中,并不会消失。此外:
getGenericType()
方法返回的是GenericArrayType
、ParameterizedType
或TypeVariable
,因为字段可以是多种类型。
检索和解析字段修饰符
有几个修饰符可能是字段声明的一部分:
- 访问修饰符:
public
、protected
和private
- 声明于字段的控制运行时行为的修饰符:
transient
和volatile
- 限制为一个实例的修饰符:
static
- 禁止值修改的修饰符:
final
- 注解(Annotations)
Field.getModifiers()
方法可以用来返回一个整数,这个整数代表了字段声明的修饰符集合。在这个整数中表示修饰符的位(bit)定义在java.lang.reflect.Modifier
类中。
简单来说,Field.getModifiers() 返回的整数包含了字段的所有修饰符信息,比如 public、private、static、final 等,这些修饰符的位标识符可以在 java.lang.reflect.Modifier 类中找到,通过位操作可以解析出具体的修饰符。
该 FieldModifierSpy
示例说明如何搜索具有给定修饰符的字段。它还通过分别调用 Field.isSynthetic()
和Field.isEnumCostant()
来确定定位的字段是合成的(编译器生成的)还是枚举常量。
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import static java.lang.System.out;
enum Spy { BLACK , WHITE }
public class FieldModifierSpy {
volatile int share;
int instance;
class Inner {
}
public static void main(String... args) {
try {
Class<?> c = Class.forName(args[0]);
int searchMods = 0x0;
for (int i = 1; i < args.length; i++) {
searchMods |= modifierFromString(args[i]);
}
Field[] flds = c.getDeclaredFields();
out.format("Fields in Class '%s' containing modifiers: %s%n",
c.getName(),
Modifier.toString(searchMods));
boolean found = false;
for (Field f : flds) {
int foundMods = f.getModifiers();
// Require all of the requested modifiers to be present
if ((foundMods & searchMods) == searchMods) {
out.format("%-8s [ synthetic=%-5b enum_constant=%-5b ]%n",
f.getName(), f.isSynthetic(),
f.isEnumConstant());
found = true;
}
}
if (!found) {
out.format("No matching fields%n");
}
// production code should handle this exception more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
private static int modifierFromString(String s) {
int m = 0x0;
if ("public".equals(s)) m |= Modifier.PUBLIC;
else if ("protected".equals(s)) m |= Modifier.PROTECTED;
else if ("private".equals(s)) m |= Modifier.PRIVATE;
else if ("static".equals(s)) m |= Modifier.STATIC;
else if ("final".equals(s)) m |= Modifier.FINAL;
else if ("transient".equals(s)) m |= Modifier.TRANSIENT;
else if ("volatile".equals(s)) m |= Modifier.VOLATILE;
return m;
}
}
示例输出如下:
$ java FieldModifierSpy FieldModifierSpy volatile
Fields in Class 'FieldModifierSpy' containing modifiers: volatile
share [ synthetic=false enum_constant=false ]
$ java FieldModifierSpy Spy public
Fields in Class 'Spy' containing modifiers: public
BLACK [ synthetic=false enum_constant=true ]
WHITE [ synthetic=false enum_constant=true ]
$ java FieldModifierSpy FieldModifierSpy$Inner final
Fields in Class 'FieldModifierSpy$Inner' containing modifiers: final
this$0 [ synthetic=true enum_constant=false ]
$ java FieldModifierSpy Spy private static final
Fields in Class 'Spy' containing modifiers: private static final
$VALUES [ synthetic=true enum_constant=false ]
请注意,即使某些字段并未在原始代码中声明,它们也会被报告出来。这是因为编译器会生成一些合成字段,这些字段在运行时是必需的。为了检测一个字段是否为合成字段,示例中调用了 Field.isSynthetic()
方法。合成字段的集合依赖于编译器;
然而,常用的一些字段包括用于Inner
类(即,非静态成员类的嵌套类)的 this$0
[1],用于引用最外层封闭类,以及由枚举使用的 $VALUES
,以实现隐式定义的静态方法 values()
。
合成类成员的名称未作规定,并且在所有编译器实现或版本中可能并不相同。
这些以及其他合成字段将包含在 Class.getDeclaredFields()
返回的数组中,但不会被 Class.getField()
标识,因为合成成员通常不是**公共(public)**的。
因为 Field
实现了接口 java.lang.reflect.AnnotatedElement
,所以可以用 java.lang.annotation.RetentionPolicy.RUNTIME
来检索任何运行时注解。有关获取Annotate(注解)的示例,请参阅检查类修饰符和类型(Examining Class Modifiers and Types)一节。
[1] 补充关于
this$0
:这里指的最外层不是最外面的那层,而是类的外面最靠近的一层,下面的例子里可以看到Inner3持有的是Inner2
的对象this$2
,而不是this$0
(FieldModifierSpy
)。另外,该合成字段仅提供给非静态成员类,如果是静态的,将不提供(静态的可以静态访问,不需要通过这个变量访问)。
而且只能通过调试这种方式看到,使用javap -c命令查看也看不到的,说明他是在jvm里面合成的
获取和设置字段值
给定一个类的实例,可以使用反射来设置该类中字段的值。这通常仅在无法以通常方式设置值的特殊情况下执行。由于这种访问通常违反了类的设计意图,因此应谨慎使用。
该 Book
类阐释如何设置 long、array 和 enum 字段类型的值。中 Field
介绍了获取和设置其他基元类型的方法。
import java.lang.reflect.Field;
import java.util.Arrays;
import static java.lang.System.out;
enum Tweedle {DEE, DUM}
public class Book {
public long chapters = 0;
public String[] characters = {"Alice", "White Rabbit"};
public Tweedle twin = Tweedle.DEE;
public static void main(String... args) {
Book book = new Book();
String fmt = "%6S: %-12s = %s%n";
try {
Class<?> c = book.getClass();
Field chap = c.getDeclaredField("chapters");
out.format(fmt, "before", "chapters", book.chapters);
chap.setLong(book, 12);
out.format(fmt, "after", "chapters", chap.getLong(book));
Field chars = c.getDeclaredField("characters");
out.format(fmt, "before", "characters",
Arrays.asList(book.characters));
String[] newChars = {"Queen", "King"};
chars.set(book, newChars);
out.format(fmt, "after", "characters",
Arrays.asList(book.characters));
Field t = c.getDeclaredField("twin");
out.format(fmt, "before", "twin", book.twin);
t.set(book, Tweedle.DUM);
out.format(fmt, "after", "twin", t.get(book));
// production code should handle these exceptions more gracefully
} catch (NoSuchFieldException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
}
}
这是相应的输出:
$ java Book
BEFORE: chapters = 0
AFTER: chapters = 12
BEFORE: characters = [Alice, White Rabbit]
AFTER: characters = [Queen, King]
BEFORE: twin = DEE
AFTER: twin = DUM
注意:**通过反射设置字段的值会产生一定的性能开销,因为必须执行各种操作,例如验证访问权限。**从运行时的角度来看,效果是相同的,并且操作是原子的,就好像值直接在类代码中更改一样。
使用反射可能会导致某些运行时优化丢失。例如,以下代码极有可能由 Java 虚拟机优化:
int x = 1;
x = 2;
x = 3;
等效代码使用 Field.set*()
可能不会优化。
疑难解答
以下是开发人员遇到的一些常见问题,并解释了发生的原因以及如何解决这些问题。
由于不可转换类型导致的 IllegalArgumentException
该FieldTrouble
示例将生成一个IllegalArgumentException
。
尝试使用Field.setInt()
方法为一个引用类型为 Integer
的字段设置一个原始类型(primitive type)的值时,在非反射等价代码 Integer val = 42
中,编译器会将基本类型 42
转换(或装箱)为引用类型 new Integer(42)
,以便其类型检查能够接受该语句。而在使用反射时,类型检查仅在运行时发生,因此没有机会对值进行装箱。
import java.lang.reflect.Field;
public class FieldTrouble {
public Integer val;
public static void main(String... args) {
FieldTrouble ft = new FieldTrouble();
try {
Class<?> c = ft.getClass();
Field f = c.getDeclaredField("val");
f.setInt(ft, 42); // IllegalArgumentException
// production code should handle these exceptions more gracefully
} catch (NoSuchFieldException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
}
}
$ java FieldTrouble
Exception in thread "main" java.lang.IllegalArgumentException: Can not set
java.lang.Object field FieldTrouble.val to (long)42
at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException
(UnsafeFieldAccessorImpl.java:146)
at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException
(UnsafeFieldAccessorImpl.java:174)
at sun.reflect.UnsafeObjectFieldAccessorImpl.setLong
(UnsafeObjectFieldAccessorImpl.java:102)
at java.lang.reflect.Field.setLong(Field.java:831)
at FieldTrouble.main(FieldTrouble.java:11)
若要消除此异常,应将有问题的行替换为以下调用 Field.set(Object obj, Object value)
:
f.set(ft, new Integer(43));
提示: **使用反射来设置或获取字段时,编译器没有机会执行装箱操作。**它只能转换规范中由 Class.isAssignableFrom()
描述的那些相关类型。此示例预期会失败,因为在这种情况下 isAssignableFrom()
会返回 false
,这可以通过编程方式用来验证特定转换是否可行:
Integer.class.isAssignableFrom(int.class) == false
同样,在反射中,从基本类型到引用类型的自动转换也是不可能的。
int.class.isAssignableFrom(Integer.class) == false
非公共字段的 NoSuchFieldException
敏锐的读者可能会注意到,如果前面演示的 FieldSpy
示例用于获取非公共字段的信息,它将失败:
$ java FieldSpy java.lang.String count
java.lang.NoSuchFieldException: count
at java.lang.Class.getField(Class.java:1519)
at FieldSpy.main(FieldSpy.java:12)
提示:Class.getField()
和Class.getFields()
方法返回 Class
对象存在的类、枚举或接口的公共成员字段。若要检索 Class
中声明(但非继承的)的所有字段,请使用 Class.getDeclaredFields()
方法。
修改最终字段时的 IllegalAccessException
如果尝试获取或设置字段 private
的值或其他无法访问的字段,或者设置字段 final
的值(无论其访问修饰符如何),则可能会引发 A IllegalAccessException
。
该 FieldTroubleToo
示例说明了尝试设置final
字段时产生的堆栈跟踪类型。
import java.lang.reflect.Field;
public class FieldTroubleToo {
public final boolean b = true;
public static void main(String... args) {
FieldTroubleToo ft = new FieldTroubleToo();
try {
otherThread(ft); // 其他线程 true
Class<?> c = ft.getClass();
Field f = c.getDeclaredField("b");
f.setAccessible(true); // solution
f.setBoolean(ft, Boolean.FALSE); // IllegalAccessException
System.out.println("反射后直接访问的值:" + ft.b); // true
System.out.println("反射后通过Field获取的值:" + f.getBoolean(ft)); // false
// production code should handle these exceptions more gracefully
} catch (NoSuchFieldException x) {
x.printStackTrace();
} catch (IllegalArgumentException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
}
public static void otherThread(FieldTroubleToo ft) {
Thread thread = new Thread(() -> {
while (true) {
try {
System.out.println(ft.b);
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
}
}
$ java FieldTroubleToo
java.lang.IllegalAccessException: Can not set final boolean field
FieldTroubleToo.b to (boolean)false
at sun.reflect.UnsafeFieldAccessorImpl.
throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:55)
at sun.reflect.UnsafeFieldAccessorImpl.
throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:63)
at sun.reflect.UnsafeQualifiedBooleanFieldAccessorImpl.setBoolean
(UnsafeQualifiedBooleanFieldAccessorImpl.java:78)
at java.lang.reflect.Field.setBoolean(Field.java:686)
at FieldTroubleToo.main(FieldTroubleToo.java:12)
提示: 存在访问限制,该限制会阻止在类初始化后设置 final
字段。但是,Field
类的声明继承了 AccessibleObject
,后者提供了抑制此检查的能力。如果 AccessibleObject.setAccessible()
调用成功,那么后续对此字段值的操作将不会因为这个问题而失败。这可能会产生意外的副作用;例如,有时即使值已被修改,应用程序的某些部分仍将继续使用原始值(请执行上面示例)。AccessibleObject.setAccessible()
仅当安全上下文(security context)允许操作时,才会成功。
方法
方法包含可调用的可执行代码。方法是继承的,在非反射代码中,重载、重写和隐藏等行为由编译器强制执行。相比之下,反射代码使得方法选择可以限制在特定类中,而不考虑其超类。可以访问超类方法,但可以确定其声明类;如果没有反思,这是不可能以编程方式发现的,并且是许多微妙错误的根源。
该 java.lang.reflect.Method
类提供 API 来访问有关方法的修饰符、返回类型、参数、注释和引发的异常的信息。它还用于调用方法。以下各节涵盖了这些主题:
- 获取方法类型信息(Obtaining Method Type Information)演示如何枚举类中声明的方法并获取类型信息
- 获取方法参数的名称(Obtaining Names of Method Parameters)演示如何检索方法或构造函数参数的名称和其他信息
- 检索和分析方法修饰符(Retrieving and Parsing Method Modifiers)描述了如何访问和解码修饰符以及与方法关联的其他信息
- 方法调用(Invoking Methods) 阐释了如何执行方法并获取其返回值
- 疑难解答(Troubleshooting)涵盖查找或调用方法时遇到的常见错误
获取方法类型信息
方法声明包括名称、修饰符、参数、返回类型和可引发异常列表。该 java.lang.reflect.Method
类提供了一种获取此信息的方法。
该 MethodSpy
示例演示如何枚举给定类中所有已声明的方法,并检索给定名称的所有方法的返回、参数和异常类型。
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import static java.lang.System.out;
public class MethodSpy {
private static final String fmt = "%24s: %s%n";
// for the morbidly curious
<E extends RuntimeException> void genericThrow() throws E {
}
public static void main(String... args) {
try {
Class<?> c = Class.forName(args[0]);
Method[] allMethods = c.getDeclaredMethods();
for (Method m : allMethods) {
if (!m.getName().equals(args[1])) {
continue;
}
out.format("%s%n", m.toGenericString());
out.format(fmt, "ReturnType", m.getReturnType());
out.format(fmt, "GenericReturnType", m.getGenericReturnType());
Class<?>[] pType = m.getParameterTypes();
Type[] gpType = m.getGenericParameterTypes();
for (int i = 0; i < pType.length; i++) {
out.format(fmt, "ParameterType", pType[i]);
out.format(fmt, "GenericParameterType", gpType[i]);
}
Class<?>[] xType = m.getExceptionTypes();
Type[] gxType = m.getGenericExceptionTypes();
for (int i = 0; i < xType.length; i++) {
out.format(fmt, "ExceptionType", xType[i]);
out.format(fmt, "GenericExceptionType", gxType[i]);
}
}
// production code should handle these exceptions more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
}
下面是输出,该输出 Class.getConstructor()
是具有参数化类型和可变参数数量的方法的示例。
$ java MethodSpy java.lang.Class getConstructor
public java.lang.reflect.Constructor<T> java.lang.Class.getConstructor
(java.lang.Class<?>[]) throws java.lang.NoSuchMethodException,
java.lang.SecurityException
ReturnType: class java.lang.reflect.Constructor
GenericReturnType: java.lang.reflect.Constructor<T>
ParameterType: class [Ljava.lang.Class;
GenericParameterType: java.lang.Class<?>[]
ExceptionType: class java.lang.NoSuchMethodException
GenericExceptionType: class java.lang.NoSuchMethodException
ExceptionType: class java.lang.SecurityException
GenericExceptionType: class java.lang.SecurityException
这是该方法在源代码中的实际声明:
public Constructor<T> getConstructor(Class<?>... parameterTypes)
首先请注意返回(return)以及参数类型(parameter types)是泛型(generic)的。 Method.getGenericReturnType()
将查阅类文件中的 Signature 属性(如果存在)。如果这个属性不可用,那么它将回退到使用Method.getReturnType()
方法,这个方法并没有因为泛型的引入而改变。在反射中,其他以 getGenericFoo()
命名的方法(其中 Foo
代表某种特定的类型信息)都是以类似的方式实现的。
接下来,注意到最后一个(也是唯一的)参数 parameterType
具有可变长度(具有可变数量的参数),类型为 java.lang.Class
。它表示为 java.lang.Class
类型的一维数组。这可以通过调用 Method.isVarArgs()
方法来区分它与显式为 java.lang.Class
数组的参数。Method.get*Types()
返回值的语法在 Class.getName()
方法的描述中有所说明。
下面的示例演示具有泛型返回类型的方法。
$ java MethodSpy java.lang.Class cast
public T java.lang.Class.cast(java.lang.Object)
ReturnType: class java.lang.Object
GenericReturnType: T
ParameterType: class java.lang.Object
GenericParameterType: class java.lang.Object
该方法 Class.cast()
的泛型返回类型被报告为 java.lang.Object
,因为泛型是通过类型擦除实现的,该类型擦除在编译过程中删除了有关泛型类型的所有信息。 T
擦除后由 Class
声明定义:
public final class Class<T> implements ...
因此 T
被类型变量的上界替换,在本例中为 java.lang.Object
。
最后一个示例演示了具有多个重载的方法的输出。
$ java MethodSpy java.io.PrintStream format
public java.io.PrintStream java.io.PrintStream.format
(java.util.Locale,java.lang.String,java.lang.Object[])
ReturnType: class java.io.PrintStream
GenericReturnType: class java.io.PrintStream
ParameterType: class java.util.Locale
GenericParameterType: class java.util.Locale
ParameterType: class java.lang.String
GenericParameterType: class java.lang.String
ParameterType: class [Ljava.lang.Object;
GenericParameterType: class [Ljava.lang.Object;
public java.io.PrintStream java.io.PrintStream.format
(java.lang.String,java.lang.Object[])
ReturnType: class java.io.PrintStream
GenericReturnType: class java.io.PrintStream
ParameterType: class java.lang.String
GenericParameterType: class java.lang.String
ParameterType: class [Ljava.lang.Object;
GenericParameterType: class [Ljava.lang.Object;
如果发现相同方法名称的多个重载,则它们都由 Class.getDeclaredMethods()
返回。由于 format()
有两个重载(一个有 Locale
一个没),因此两者都让MethodSpy
演示出来了。
注意: Method.getGenericExceptionTypes()
之所以存在, 是因为实际上可以声明具有泛型异常类型的方法。但是,这很少使用,因为无法捕获泛型异常类型。
获取方法参数的名称
您可以使用 方法 java.lang.reflect.Executable.getParameters
获取任何方法或构造函数的形式参数的名称。(类 Method
和Constructor
继承类 Executable
,因此继承该方法 Executable.getParameters
(前面有提到) 。但是,默认情况下, **.class
文件不存储正式参数名称(你在阅读class文件的时候参数名基本都是var1,var2…)。**这是因为许多生成和使用类文件的工具可能不希望包含参数名称 .class
的文件占用更大的静态和动态占用空间。特别是,这些工具必须处理更大的 .class
文件,而 Java 虚拟机 (JVM) 将使用更多的内存。此外,某些参数名称(如 secret
或 password
)可能会公开有关安全敏感方法的信息。
若要将正式参数名称存储在特定 .class
文件中,从而使反射 API 能够检索正式参数名称,请使用 javac
编译器 -parameters
选项编译源文件。
该 MethodParameterSpy
示例演示如何检索给定类的所有构造函数和方法的形式参数的名称。该示例还打印有关每个参数的其他信息。
以下命令打印类 ExampleMethods
. 的构造函数和方法的形式参数名称。
注意:请记住使用 -parameters
编译器选项编译示例 ExampleMethods
:
ExampleMethods类请点击链接进去下载或者复制
java MethodParameterSpy ExampleMethods
此命令打印以下内容:
Number of constructors: 1
Constructor #1
public ExampleMethods()
Number of declared constructors: 1
Declared constructor #1
public ExampleMethods()
Number of methods: 4
Method #1
public boolean ExampleMethods.simpleMethod(java.lang.String,int)
Return type: boolean
Generic return type: boolean
Parameter class: class java.lang.String
Parameter name: stringParam
Modifiers: 0
Is implicit?: false
Is name present?: true
Is synthetic?: false
Parameter class: int
Parameter name: intParam
Modifiers: 0
Is implicit?: false
Is name present?: true
Is synthetic?: false
Method #2
public int ExampleMethods.varArgsMethod(java.lang.String...)
Return type: int
Generic return type: int
Parameter class: class [Ljava.lang.String;
Parameter name: manyStrings
Modifiers: 0
Is implicit?: false
Is name present?: true
Is synthetic?: false
Method #3
public boolean ExampleMethods.methodWithList(java.util.List<java.lang.String>)
Return type: boolean
Generic return type: boolean
Parameter class: interface java.util.List
Parameter name: listParam
Modifiers: 0
Is implicit?: false
Is name present?: true
Is synthetic?: false
Method #4
public <T> void ExampleMethods.genericMethod(T[],java.util.Collection<T>)
Return type: void
Generic return type: void
Parameter class: class [Ljava.lang.Object;
Parameter name: a
Modifiers: 0
Is implicit?: false
Is name present?: true
Is synthetic?: false
Parameter class: interface java.util.Collection
Parameter name: c
Modifiers: 0
Is implicit?: false
Is name present?: true
Is synthetic?: false
该 MethodParameterSpy
示例使用 Parameter
类中的以下方法:
-
getName
:返回参数的名称。如果参数的名称存在,则此方法返回.class
文件提供的名称。否则,此方法将合成以下形式的arg*N*
名称,其中*N*
是声明参数的方法的描述符中参数的索引。例如,假设您在未指定
-parameters
编译器选项的情况下编译了类ExampleMethods
。该示例MethodParameterSpy
将为该方法ExampleMethods.simpleMethod
打印以下内容:public boolean ExampleMethods.simpleMethod(java.lang.String,int) Return type: boolean Generic return type: boolean Parameter class: class java.lang.String Parameter name: arg0 Modifiers: 0 Is implicit?: false Is name present?: false Is synthetic?: false Parameter class: int Parameter name: arg1 Modifiers: 0 Is implicit?: false Is name present?: false Is synthetic?: false
-
getModifiers
:返回一个整数,该整数表示形式参数所具有的各种特征。如果适用于形式参数,则此值是以下值的总和:值(十进制) 值(十六进制) 描述 16 0x0010 形式参数已声明 final
4096 0x1000 形式参数是合成的。或者,您可以调用方法 isSynthetic
。32768 0x8000 该参数在源代码中隐式声明。或者,您可以调用该方法 isImplicit
-
isImplicit
:如果此参数在源代码中隐式声明,则返回true
。有关详细信息,请参阅隐式参数和综合参数( Implicit and Synthetic Parameters )部分。 -
isNamePresent
:如果参数根据.class
文件具有名称,则返回true
。 -
isSynthetic
:如果此参数在源代码中既不是隐式声明也不是显式声明,则返回true
。有关详细信息,请参阅隐式参数和综合参数( Implicit and Synthetic Parameters )部分。
隐式参数和合成参数
如果某些构造不是显式编写的,则会在源代码中隐式声明它们。例如,该 ExampleMethods
示例不包含构造函数。默认构造函数是为其隐式声明的。该 MethodParameterSpy
示例打印有关隐式声明的构造函数的信息 ExampleMethods
:
Number of declared constructors: 1
public ExampleMethods()
请看以下摘录 MethodParameterExamples
:
public class MethodParameterExamples {
public class InnerClass { }
}
该类 InnerClass
是非静态嵌套类( nested class )或内部类。内部类的构造函数也是隐式声明的。但是,此构造函数将包含一个参数。当 Java 编译器编译时 InnerClass
,它会创建一个 .class
文件,该文件表示类似于以下内容的代码:
public class MethodParameterExamples {
public class InnerClass {
final MethodParameterExamples parent;
InnerClass(final MethodParameterExamples this$0) {
parent = this$0;
}
}
}
// 但是实际上你是看不到的,只能通过反射知道有这些
InnerClass
构造函数包含一个参数,其类型是包含 InnerClass
的类,即 MethodParameterExamples
。因此,该示例 MethodParameterExamples
打印以下内容:
public MethodParameterExamples$InnerClass(MethodParameterExamples)
Parameter class: class MethodParameterExamples
Parameter name: this$0
Modifiers: 32784
Is implicit?: true
Is name present?: true
Is synthetic?: false
由于类 InnerClass
的构造函数是隐式声明的,因此其参数也是隐式的。
注意:
- Java 编译器为内部类的构造函数创建一个形式参数,使编译器能够将引用(表示最近的外围类实例)从创建者传递到成员类的构造函数。(说人话就是看代码,把父类引用传递给内部类了)
- 值 32784 表示
InnerClass
构造函数的参数既是最final的 (16) 又是implicit (隐式)的 (32768)。 - Java 编程语言允许使用带有美元符号 (
$
) 的变量名称;但是,按照惯例,变量名称中不使用美元符号。
如果 Java 编译器创建的构造方法与源代码中显式或隐式声明的构造方法不对应,则将它们标记为合成构造,除非它们是类初始化方法。合成构造方法是由编译器生成的构件,这些构件在不同的实现中有所不同。请看以下摘录 MethodParameterExamples
:
public class MethodParameterExamples {
enum Colors {
RED, WHITE;
}
}
当 Java 编译器遇到 enum
构造时,它会创建多个与 .class
文件结构兼容的方法,并提供 enum
构造的预期功能。例如,Java 编译器将为 enum
构造创建一个 .class
文件 Colors
,该文件表示类似于以下内容的代码:
final class Colors extends java.lang.Enum<Colors> {
public final static Colors RED = new Colors("RED", 0);
public final static Colors BLUE = new Colors("WHITE", 1);
private final static values = new Colors[]{ RED, BLUE };
private Colors(String name, int ordinal) {
super(name, ordinal);
}
public static Colors[] values(){
return values;
}
public static Colors valueOf(String name){
return (Colors)java.lang.Enum.valueOf(Colors.class, name);
}
}
Java 编译器为此 enum
构造创建了三个构造函数和方法: Colors(String name, int ordinal)
、 Colors[] values()
和 Colors valueOf(String name)
。方法 values
和 valueOf
被隐式声明。因此,它们的正式参数名称也是隐式声明的。
enum
构造函数 Colors(String name, int ordinal)
是默认构造函数,并且是隐式声明的。但是,此构造函数 ( name
和 ordinal
) 的形式参数不是隐式声明的。因为这些形式参数既不是显式声明的,也不是隐式声明的,它们是合成的。( enum
构造的默认构造函数的形式参数不是隐式声明的,因为不同的编译器不需要就此构造函数的形式达成一致;另一个 Java 编译器可能会为其指定不同的形式参数。当编译器编译使用 enum
常量的表达式时,它们仅依赖于 enum
构造的公共静态字段(这些字段是隐式声明的),而不依赖于它们的构造函数或这些常量的初始化方式。
因此,该示例 MethodParameterExample
打印有关 enum
构造的以下内容 Colors
:
$ javac -parameters MethodParameterExamples.java
enum Colors:
Number of constructors: 0
Number of declared constructors: 1
Declared constructor #1
private MethodParameterExamples$Colors()
Parameter class: class java.lang.String
Parameter name: $enum$name
Modifiers: 4096
Is implicit?: false
Is name present?: true
Is synthetic?: true
Parameter class: int
Parameter name: $enum$ordinal
Modifiers: 4096
Is implicit?: false
Is name present?: true
Is synthetic?: true
Number of methods: 2
Method #1
public static MethodParameterExamples$Colors[]
MethodParameterExamples$Colors.values()
Return type: class [LMethodParameterExamples$Colors;
Generic return type: class [LMethodParameterExamples$Colors;
Method #2
public static MethodParameterExamples$Colors
MethodParameterExamples$Colors.valueOf(java.lang.String)
Return type: class MethodParameterExamples$Colors
Generic return type: class MethodParameterExamples$Colors
Parameter class: class java.lang.String
Parameter name: name
Modifiers: 32768
Is implicit?: true
Is name present?: true
Is synthetic?: false
有关隐式声明的构造(包括反射 API 中显示为隐式的参数)的更多信息,请参阅 Java 语言规范( Java Language Specification)。
检索和解析方法修饰符
有几个修饰符可能是方法声明的一部分:
- 访问修饰符:
public
、protected
和private
- 限制为一个实例的修饰符:
static
- 禁止值修改的修饰符:
final
- 需要覆盖(override)的修饰符:
abstract
- 防止重入的修饰符:
synchronized
- 指示另一种编程语言实现的修饰符:
native
- 修饰符强制严格的浮点行为:
strictfp
- 注解(Annotations)
该 MethodModifierSpy
示例列出具有给定名称的方法的修饰符。它还显示该方法是合成(synthetic )的(编译器生成的)、还是可变参数方法( variable arity),还是桥接方法( bridge method)(编译器生成以支持泛型接口)。
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import static java.lang.System.out;
public class MethodModifierSpy {
private static int count;
private static synchronized void inc() { count++; }
private static synchronized int cnt() { return count; }
public static void main(String... args) {
try {
Class<?> c = Class.forName(args[0]);
Method[] allMethods = c.getDeclaredMethods();
for (Method m : allMethods) {
if (!m.getName().equals(args[1])) {
continue;
}
out.format("%s%n", m.toGenericString());
out.format(" Modifiers: %s%n",
Modifier.toString(m.getModifiers()));
out.format(" [ synthetic=%-5b var_args=%-5b bridge=%-5b ]%n",
m.isSynthetic(), m.isVarArgs(), m.isBridge());
inc();
}
out.format("%d matching overload%s found%n", cnt(),
(cnt() == 1 ? "" : "s"));
// production code should handle this exception more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
}
以下是输出 MethodModifierSpy
产品的几个示例。
$ java MethodModifierSpy java.lang.Object wait
public final void java.lang.Object.wait() throws java.lang.InterruptedException
Modifiers: public final
[ synthetic=false var_args=false bridge=false ]
public final void java.lang.Object.wait(long,int)
throws java.lang.InterruptedException
Modifiers: public final
[ synthetic=false var_args=false bridge=false ]
public final native void java.lang.Object.wait(long)
throws java.lang.InterruptedException
Modifiers: public final native
[ synthetic=false var_args=false bridge=false ]
3 matching overloads found
$ java MethodModifierSpy java.lang.StrictMath toRadians
public static double java.lang.StrictMath.toRadians(double)
Modifiers: public static strictfp
[ synthetic=false var_args=false bridge=false ]
1 matching overload found
$ java MethodModifierSpy MethodModifierSpy inc
private synchronized void MethodModifierSpy.inc()
Modifiers: private synchronized
[ synthetic=false var_args=false bridge=false ]
1 matching overload found
$ java MethodModifierSpy java.lang.Class getConstructor
public java.lang.reflect.Constructor<T> java.lang.Class.getConstructor
(java.lang.Class<T>[]) throws java.lang.NoSuchMethodException,
java.lang.SecurityException
Modifiers: public transient
[ synthetic=false var_args=true bridge=false ]
1 matching overload found
$ java MethodModifierSpy java.lang.String compareTo
public int java.lang.String.compareTo(java.lang.String)
Modifiers: public
[ synthetic=false var_args=false bridge=false ]
public int java.lang.String.compareTo(java.lang.Object)
Modifiers: public volatile
[ synthetic=true var_args=false bridge=true ]
2 matching overloads found
请注意, Class.getConstructor()
方法调用Method.isVarArgs()
返回 true
是因为方法声明如下所示:
public Constructor<T> getConstructor(Class<?>... parameterTypes)
不是这样的:
public Constructor<T> getConstructor(Class<?> [] parameterTypes)
请注意,输出 String.compareTo()
包含两个方法。中 String.java
声明的方法:
public int compareTo(String anotherString);
以及第二种合成或编译器生成的桥接方法。发生这种情况是因为 String
实现了参数化接口 Comparable
。在类型擦除期间,继承方法 Comparable.compareTo()
的参数类型从 java.lang.Object
更改为 java.lang.String
。由于 和 compareTo
Comparable
String
中方法的参数类型在擦除后不再匹配,因此无法进行覆盖。在所有其他情况下,这将产生编译时错误,因为接口未实现。桥接方法的添加避免了这个问题。
Method
实现 java.lang.reflect.AnnotatedElement
.因此,可以检索带有java.lang.annotation.RetentionPolicy.RUNTIME
注解的任何运行时注释。 有关获取注释的示例,请参阅检查类修饰符和类型一节(Examining Class Modifiers and Types)。
方法调用
反射提供了一种在类上调用方法的方法。通常,仅当无法在非反射代码中将类的实例转换为所需的类型时,才需要这样做。方法使用 java.lang.reflect.Method.invoke
调用。第一个参数是要调用此特定方法的对象实例。(如果方法是 static
,第一个参数应该是 null
。后续参数是方法的参数。如果指定方法引发异常,则该方法将由 java.lang.reflect.InvocationTargetException
包装,可以使用异常链接机制 InvocationTargetException.getCause()
的方法获取方法的原始异常。
查找和调用具有特定声明的方法
考虑一个测试场景,它使用反射来调用给定类中的专用测试方法。该 Deet
示例在类中搜索以字符串 “ test
” 开头、具有boolean
返回类型和单个 Locale
参数的 public
方法。然后,它调用每个找到的方法。
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Locale;
import static java.lang.System.out;
import static java.lang.System.err;
public class Deet<T> {
private boolean testDeet(Locale l) {
// getISO3Language() may throw a MissingResourceException
out.format("Locale = %s, ISO Language Code = %s%n", l.getDisplayName(), l.getISO3Language());
return true;
}
private int testFoo(Locale l) { return 0; }
private boolean testBar() { return true; }
public static void main(String... args) {
if (args.length != 4) {
err.format("Usage: java Deet <classname> <langauge> <country> <variant>%n");
return;
}
try {
Class<?> c = Class.forName(args[0]);
Object t = c.newInstance();
Method[] allMethods = c.getDeclaredMethods();
for (Method m : allMethods) {
String mname = m.getName();
if (!mname.startsWith("test")
|| (m.getGenericReturnType() != boolean.class)) {
continue;
}
Type[] pType = m.getGenericParameterTypes();
if ((pType.length != 1)
|| Locale.class.isAssignableFrom(pType[0].getClass())) {
continue;
}
out.format("invoking %s()%n", mname);
try {
m.setAccessible(true);
Object o = m.invoke(t, new Locale(args[1], args[2], args[3]));
out.format("%s() returned %b%n", mname, (Boolean) o);
// Handle any exceptions thrown by method to be invoked.
} catch (InvocationTargetException x) {
Throwable cause = x.getCause();
err.format("invocation of %s failed: %s%n",
mname, cause.getMessage());
}
}
// production code should handle these exceptions more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
} catch (InstantiationException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
}
}
Deet
调用 getDeclaredMethods()
,这将返回类中显式声明的所有方法。此外, Class.isAssignableFrom()
还用于确定定位方法的参数是否与所需的调用兼容。从技术上讲,因为Locale
是 final
类型的,所以通过以下语句是否是 true
可以测试是否兼容:
Locale.class == pType[0].getClass()
但是, Class.isAssignableFrom()
更通用。
$ java Deet Deet ja JP JP
invoking testDeet()
Locale = 日文 (日本,JP), ISO Language Code = jpn
testDeet() returned true
$ java Deet Deet xx XX XX
invoking testDeet()
invocation of testDeet failed: Couldn't find 3-letter language code for xx
首先,请注意,仅 testDeet()
满足代码要求的声明限制。接下来,当传递一个无效参数时 ,testDeet()
会抛出一个未检查(unchecked)错误 java.util.MissingResourceException
。在反射中,在处理已检查的异常和未检查的例外时没有区别。它们都包裹在一个 InvocationTargetException
。
使用可变数量的参数调用方法
Method.invoke()
可用于将可变数量的参数传递给方法。要理解的关键概念是,可变参数方法的实现就像变量参数打包在数组中一样。
该 InvokeMain
示例演示如何在任何类中调用 main()
入口点并在运行时传递确定的一组参数。
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
public class InvokeMain {
public static void main(String... args) {
try {
Class<?> c = Class.forName(args[0]);
Class[] argTypes = new Class[] { String[].class };
Method main = c.getDeclaredMethod("main", argTypes);
String[] mainArgs = Arrays.copyOfRange(args, 1, args.length);
System.out.format("invoking %s.main()%n", c.getName());
main.invoke(null, (Object)mainArgs);
// production code should handle these exceptions more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
} catch (NoSuchMethodException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
} catch (InvocationTargetException x) {
x.printStackTrace();
}
}
}
首先,为了找到main()
方法,代码会在当前环境中搜索一个名为"main"的方法,这个方法存在于某个类中,并且接受一个参数,这个参数是一个String类型的数组。由于main()
方法是静态的,所以在调用Method.invoke()
时,第一个参数会传入null,这是因为静态方法并不属于任何特定的对象实例;第二个参数则是传递给main()方法的参数数组,通常这是程序启动时从操作系统接收的命令行参数。
$ java InvokeMain Deet Deet ja JP JP
invoking Deet.main()
invoking testDeet()
Locale = 日文 (日本,JP), ISO Language Code = jpn
testDeet() returned true
疑难解答
本部分包含开发人员在使用反射查找、调用或获取有关方法的信息时可能遇到的问题的示例。
由于类型擦除导致的 NoSuchMethodException
该 MethodTrouble
示例演示了在类中搜索特定方法的代码时不考虑类型擦除会发生什么情况。
import java.lang.reflect.Method;
public class MethodTrouble<T> {
public void lookup(T t) {}
public void find(Integer i) {}
public static void main(String... args) {
try {
String mName = args[0];
Class cArg = Class.forName(args[1]);
Class<?> c = (new MethodTrouble<Integer>()).getClass();
Method m = c.getMethod(mName, cArg);
System.out.format("Found:%n %s%n", m.toGenericString());
// production code should handle these exceptions more gracefully
} catch (NoSuchMethodException x) {
x.printStackTrace();
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
}
$ java MethodTrouble lookup java.lang.Integer
java.lang.NoSuchMethodException: MethodTrouble.lookup(java.lang.Integer)
at java.lang.Class.getMethod(Class.java:1605)
at MethodTrouble.main(MethodTrouble.java:12)
$ java MethodTrouble lookup java.lang.Object
Found:
public void MethodTrouble.lookup(T)
当使用泛型参数类型声明方法时,编译器会将泛型类型替换为其上限,在本例中,该 T
法的上限为 Object
。因此,当代码搜索 lookup(Integer)
时,找不到任何方法,尽管 MethodTrouble
的实例是按如下方式创建的:
Class<?> c = (new MethodTrouble<Integer>()).getClass(); // 这种方式并不会创建类,但是在类的内部是可以获取到实际类型的
改为Object类后, lookup(Object)
如期搜索成功。
$ java MethodTrouble find java.lang.Integer
Found:
public void MethodTrouble.find(java.lang.Integer)
$ java MethodTrouble find java.lang.Object
java.lang.NoSuchMethodException: MethodTrouble.find(java.lang.Object)
at java.lang.Class.getMethod(Class.java:1605)
at MethodTrouble.main(MethodTrouble.java:12)
在本例中, find()
没有泛型参数,因此搜索的getMethod()
参数类型必须完全匹配。
提示: 搜索方法时,始终传递参数化类型的上限。
调用方法时的 IllegalAccessException
如果尝试调用某个 private
方法或以其他方式无法访问的方法,则会引发 IllegalAccessException
操作。
该 MethodTroubleAgain
示例显示了一个典型的堆栈跟踪,该跟踪是由于尝试调用另一个类中的私有方法而产生的。
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
class AnotherClass {
private void m() {
}
}
public class MethodTroubleAgain {
public static void main(String... args) {
AnotherClass ac = new AnotherClass();
try {
Class<?> c = ac.getClass();
Method m = c.getDeclaredMethod("m");
// m.setAccessible(true); // solution
Object o = m.invoke(ac); // IllegalAccessException
// production code should handle these exceptions more gracefully
} catch (NoSuchMethodException x) {
x.printStackTrace();
} catch (InvocationTargetException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
}
}
引发的异常的堆栈跟踪(stack trace)如下。
$ java MethodTroubleAgain
java.lang.IllegalAccessException: Class MethodTroubleAgain can not access a
member of class AnotherClass with modifiers "private"
at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:65)
at java.lang.reflect.Method.invoke(Method.java:588)
at MethodTroubleAgain.main(MethodTroubleAgain.java:15)
提示: 存在访问限制,这会阻止通过反射调用那些通常无法直接调用的方法(这包括但不限于在另一个类中的private
方法,以及在另一个私有类中的公共方法)。然而,Method
类声明它扩展了AccessibleObject
,后者提供了通过AccessibleObject.setAccessible()
来抑制这种检查的能力。如果它成功了,那么后续对这个方法对象的调用将不会因为这个问题而失败。
Method.invoke() 中的 IllegalArgumentException
Method.invoke()
已被改造为一种可变参数方法。这是一个巨大的便利,但它可能会导致意外行为。该 MethodTroubleToo
示例显示了 Method.invoke()
可能产生令人困惑结果的各种方式。
import java.lang.reflect.Method;
public class MethodTroubleToo {
public void ping() {
System.out.format("PONG!%n");
}
public static void main(String... args) {
try {
MethodTroubleToo mtt = new MethodTroubleToo();
Method m = MethodTroubleToo.class.getMethod("ping");
switch (Integer.parseInt(args[0])) {
case 0:
m.invoke(mtt); // works
break;
case 1:
m.invoke(mtt, null); // works (expect compiler warning)
break;
case 2:
Object arg2 = null;
m.invoke(mtt, arg2); // IllegalArgumentException
break;
case 3:
m.invoke(mtt, new Object[0]); // works
break;
case 4:
Object arg4 = new Object[0];
m.invoke(mtt, arg4); // IllegalArgumentException
break;
default:
System.out.format("Test not found%n");
}
// production code should handle these exceptions more gracefully
} catch (Exception x) {
x.printStackTrace();
}
}
}
$ java MethodTroubleToo 0
PONG!
// m.invoke(mtt);
由于除第一个参数外的所有参数 Method.invoke()
都是可变的,因此当要调用的方法没有参数时,可以省略它们。
$ java MethodTroubleToo 1
PONG!
// m.invoke(mtt, null);
在这种情况下,代码会生成此编译器警告(但可以运行),因为 null
不明确。
$ javac MethodTroubleToo.java
MethodTroubleToo.java:16: warning: non-varargs call of varargs method with
inexact argument type for last parameter;
m.invoke(mtt, null); // works (expect compiler warning)
^
cast to Object for a varargs call
cast to Object[] for a non-varargs call and to suppress this warning
1 warning
无法确定是 null
表示空的参数数组还是第一个参数是 null
。
$ java MethodTroubleToo 2
java.lang.IllegalArgumentException: wrong number of arguments
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke
(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke
(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at MethodTroubleToo.main(MethodTroubleToo.java:21)
// Object arg2 = null;
// m.invoke(mtt, arg2);
尽管参数是 null
,但还是失败了,因为类型是一个 Object
但是 ping()
根本不需要任何参数。
$ java MethodTroubleToo 3
PONG!
// m.invoke(mtt, new Object[0]);
这之所以有效,是因为 new Object[0]
创建了一个类型为Object数量为0的空数组,而对于 varargs 方法,这相当于不传递任何可选参数。
$ java MethodTroubleToo 4
java.lang.IllegalArgumentException: wrong number of arguments
at sun.reflect.NativeMethodAccessorImpl.invoke0
(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke
(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke
(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at MethodTroubleToo.main(MethodTroubleToo.java:28)
// Object arg4 = new Object[0];
// m.invoke(mtt, arg4);
与前面的示例不同,如果空数组存储在 Object
中,则将其视为 Object
。这失败的原因与案例 2 失败的原因相同, ping()
不需要参数。
提示:当声明方法 foo(Object... o)
时,编译器会将传递给 foo()
的所有参数放在 Object
类型的数组中。 foo()
的实现与声明 foo(Object[] o)
相同。了解这一点可能有助于避免上述问题类型。
说人话就是:
foo(Object... o)
和foo(Object[] o)
是一样的。下面代码会提示“方法已存在”。public void foo(Object... vars) {} public void foo(Object[] vars) {}
调用方法失败时的 InvocationTargetException
InvocationTargetException
包装所有在调用方法对象时产生的异常(checked和unchecked)。该 MethodTroubleReturns
示例演示如何获得调用方法时引发的原始异常。
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class MethodTroubleReturns {
private void drinkMe(int liters) {
if (liters < 0)
throw new IllegalArgumentException("I can't drink a negative amount of liquid");
}
public static void main(String... args) {
try {
MethodTroubleReturns mtr = new MethodTroubleReturns();
Class<?> c = mtr.getClass();
Method m = c.getDeclaredMethod("drinkMe", int.class);
m.invoke(mtr, -1);
// production code should handle these exceptions more gracefully
} catch (InvocationTargetException x) {
Throwable cause = x.getCause();
System.err.format("drinkMe() failed: %s%n", cause.getMessage());
} catch (Exception x) {
x.printStackTrace();
}
}
}
$ java MethodTroubleReturns
drinkMe() failed: I can't drink a negative amount of liquid
提示: 如果抛出了一个InvocationTargetException
,则说明方法已经调用完成。诊断问题的方式与直接调用该方法并抛出异常(可通过 getCause()
方法获取)相同。此异常并不表明反射包或其使用存在问题。
说人话就是:方法调用成功,但是方法报错了。反射调用跟你直接调用都是报一样的错的。
构造函数
构造函数用于创建作为类实例的对象。通常,它会在调用方法或访问字段之前执行初始化类所需的操作。构造函数永远不会被继承。
与方法类似,反射提供了 API 来发现和检索类的构造函数,并获取修饰符、参数、注释和抛出的异常等声明信息。也可以使用指定的构造函数创建类的新实例。使用构造函数时使用的关键类是 Class
和 java.lang.reflect.Constructor
。以下各节介绍了涉及构造函数的常见操作:
- 查找构造函数演示了如何检索具有特定参数的构造函数
- 检索和分析构造函数修饰符演示如何获取构造函数声明的修饰符以及有关构造函数的其他信息
- 创建新的类实例演示如何通过调用对象的构造函数来实例化对象的实例
- 疑难解答描述了在查找或调用构造函数时可能遇到的常见错误
查找构造函数
构造函数声明包括名称、修饰符、参数和可引发的异常列表。该 java.lang.reflect.Constructor
类提供了一种获取此信息的方法 。
该 ConstructorSift
示例演示了如何在类声明的构造函数中搜索具有给定类型参数的构造函数。
import java.lang.reflect.Constructor;
import java.lang.reflect.Type;
import static java.lang.System.out;
public class ConstructorSift {
public static void main(String... args) {
try {
Class<?> cArg = Class.forName(args[1]);
Class<?> c = Class.forName(args[0]);
Constructor[] allConstructors = c.getDeclaredConstructors();
for (Constructor ctor : allConstructors) {
Class<?>[] pType = ctor.getParameterTypes();
for (int i = 0; i < pType.length; i++) {
if (pType[i].equals(cArg)) {
out.format("%s%n", ctor.toGenericString());
Type[] gpType = ctor.getGenericParameterTypes();
for (int j = 0; j < gpType.length; j++) {
char ch = (pType[j].equals(cArg) ? '*' : ' ');
out.format("%7c%s[%d]: %s%n", ch,
"GenericParameterType", j, gpType[j]);
}
break;
}
}
}
// production code should handle this exception more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
}
Method.getGenericParameterTypes()
将查阅类文件中的 Signature
属性(如果存在)。如果该属性不可用,那么它将回退到使用 Method.getParameterType()
方法,这个方法没有因为引入泛型而改变。在反射中,其他名称为getGenericFoo()
的方法(其中Foo代表某个特定值)以类似的方式实现。Method.get*Types()
返回值的语法在 Class.getName()
的描述中有详细说明。
以下是 java.util.Formatter
中具有 Locale
参数的所有构造函数的输出。
$ java ConstructorSift java.util.Formatter java.util.Locale
public
java.util.Formatter(java.io.OutputStream,java.lang.String,java.util.Locale)
throws java.io.UnsupportedEncodingException
GenericParameterType[0]: class java.io.OutputStream
GenericParameterType[1]: class java.lang.String
*GenericParameterType[2]: class java.util.Locale
public java.util.Formatter(java.lang.String,java.lang.String,java.util.Locale)
throws java.io.FileNotFoundException,java.io.UnsupportedEncodingException
GenericParameterType[0]: class java.lang.String
GenericParameterType[1]: class java.lang.String
*GenericParameterType[2]: class java.util.Locale
public java.util.Formatter(java.lang.Appendable,java.util.Locale)
GenericParameterType[0]: interface java.lang.Appendable
*GenericParameterType[1]: class java.util.Locale
public java.util.Formatter(java.util.Locale)
*GenericParameterType[0]: class java.util.Locale
public java.util.Formatter(java.io.File,java.lang.String,java.util.Locale)
throws java.io.FileNotFoundException,java.io.UnsupportedEncodingException
GenericParameterType[0]: class java.io.File
GenericParameterType[1]: class java.lang.String
*GenericParameterType[2]: class java.util.Locale
下一个示例输出说明如何搜索String
类型 中的 char[]
参数。
$ java ConstructorSift java.lang.String "[C"
java.lang.String(int,int,char[])
GenericParameterType[0]: int
GenericParameterType[1]: int
*GenericParameterType[2]: class [C
public java.lang.String(char[],int,int)
*GenericParameterType[0]: class [C
GenericParameterType[1]: int
GenericParameterType[2]: int
public java.lang.String(char[])
*GenericParameterType[0]: class [C
在 Class.forName()
中有详细描述可被 Class.getName()
接受的引用类型和基本类型数组的表达语法。需要注意的是,列出的第一个构造器是包私有(package-private
)的,而不是公共(public
)的。之所以返回它,是因为示例代码使用了Class.getDeclaredConstructors()
,而不是仅返回 public
构造器的 Class.getConstructors()
。
此示例表明,搜索可变数量参数的方法(具有可变数量的参数)需要使用数组语法:
$ java ConstructorSift java.lang.ProcessBuilder ";"
public java.lang.ProcessBuilder(java.lang.String[])
*GenericParameterType[0]: class [Ljava.lang.String;
这是源代码中 ProcessBuilder
构造函数的实际声明:
public ProcessBuilder(String... command)
该参数([Ljava.lang.String
)表示为 java.lang.String
类型的一维数组。这可以通过调用 Constructor.isVarArgs()
来与显式定义的 java.lang.String
数组区分开来。
The final example reports the output for a constructor which has been declared with a generic parameter type:
最后的示例展示了一个使用泛型参数类型声明的构造函数的输出:
$ java ConstructorSift java.util.HashMap java.util.Map
public java.util.HashMap(java.util.Map<? extends K, ? extends V>)
*GenericParameterType[0]: java.util.Map<? extends K, ? extends V>
对于构造函数,可以采用与方法类似的方式检索异常类型。有关详细信息,请参阅“获取方法类型信息( Obtaining Method Type Information )”部分中所述的 MethodSpy
示例。
检索和分析构造函数修饰符
由于构造函数在语言中的角色,有意义的修饰符比方法的修饰符少:
- 访问修饰符:
public
、protected
和private
- 注解(Annotations)
该 ConstructorAccess
示例在给定类中搜索具有指定访问修饰符的构造函数。它还显示构造函数是合成的(编译器生成的)还是可变参数的。
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import static java.lang.System.out;
public class ConstructorAccess {
public static void main(String... args) {
try {
Class<?> c = Class.forName(args[0]);
Constructor[] allConstructors = c.getDeclaredConstructors();
for (Constructor ctor : allConstructors) {
int searchMod = modifierFromString(args[1]);
int mods = accessModifiers(ctor.getModifiers());
if (searchMod == mods) {
out.format("%s%n", ctor.toGenericString());
out.format(" [ synthetic=%-5b var_args=%-5b ]%n",
ctor.isSynthetic(), ctor.isVarArgs());
}
}
// production code should handle this exception more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
private static int accessModifiers(int m) {
return m & (Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED);
}
private static int modifierFromString(String s) {
if ("public".equals(s)) return Modifier.PUBLIC;
else if ("protected".equals(s)) return Modifier.PROTECTED;
else if ("private".equals(s)) return Modifier.PRIVATE;
else if ("package-private".equals(s)) return 0;
else return -1;
}
}
没有对应于"package-private
" 访问的显式 Modifier
常量,因此有必要检查是否缺少所有三个访问修饰符以识别"package-private
" 构造函数。
此输出显示 : java.io.File
$ java ConstructorAccess java.io.File private
private java.io.File(java.lang.String,int)
[ synthetic=false var_args=false ]
private java.io.File(java.lang.String,java.io.File)
[ synthetic=false var_args=false ]
合成构造函数很少见;但是,该 SyntheticConstructor
示例说明了可能发生这种情况的典型情况:
public class SyntheticConstructor {
private SyntheticConstructor() {}
class Inner {
// Compiler will generate a synthetic constructor since
// SyntheticConstructor() is private.
Inner() { new SyntheticConstructor(); }
}
}
$ java ConstructorAccess SyntheticConstructor package-private
SyntheticConstructor(SyntheticConstructor$1)
[ synthetic=true var_args=false ]
// 补充下跟private的区别
$ java ConstructorAccess SyntheticConstructor private
private SyntheticConstructor()
[ synthetic=false var_args=false ]
由于内部类的构造函数引用封闭(包围)类的私有构造函数,因此编译器必须生成一个package-private构造函数。参数类型 SyntheticConstructor$1
是任意的,取决于编译器实现。依赖于任何合成或非公共类成员存在的代码可能无法移植。
构造函数实现了 java.lang.reflect.AnnotatedElement
,它提供了在运行时获取带有 java.lang.annotation.RetentionPolicy.RUNTIME
的注解的方法。有关获取注解的示例,请参阅“检查类修饰符和类型( Examining Class Modifiers and Types )”部分。
创建新的类实例
有两种反射方法可用于创建类的实例: java.lang.reflect.Constructor.newInstance()
和Class.newInstance()
。前者是首选,因此在这些示例中使用,因为:
Class.newInstance()
只能调用零参数构造函数,而Constructor.newInstance()
可以调用任何构造函数,而不考虑参数的数量。Class.newInstance()
抛出构造函数引发的任何异常,无论它是checked还是unchecked。Constructor.newInstance()
始终用InvocationTargetException
.Class.newInstance()
要求构造函数可见;Constructor.newInstance()
在某些情况下可以调用private
构造函数。
有时可能想要从对象中获取那些仅在构造后设置(初始化)的内部状态(inner state)。考虑一个场景,其中需要获取 java.io.Console
使用的内部字符集。( Console
字符集存储在私有字段中,不一定与 Java 虚拟机返回默认字符集的 java.nio.charset.Charset.defaultCharset()
方法相同)。该 ConsoleCharset
示例显示了如何实现此目的:
import java.io.Console;
import java.nio.charset.Charset;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import static java.lang.System.out;
public class ConsoleCharset {
public static void main(String... args) {
Constructor[] ctors = Console.class.getDeclaredConstructors();
Constructor ctor = null;
for (int i = 0; i < ctors.length; i++) {
ctor = ctors[i];
if (ctor.getGenericParameterTypes().length == 0)
break;
}
try {
ctor.setAccessible(true);
Console c = (Console) ctor.newInstance();
Field f = c.getClass().getDeclaredField("cs");
f.setAccessible(true);
out.format("Console charset : %s%n", f.get(c));
out.format("Charset.defaultCharset(): %s%n",
Charset.defaultCharset());
// production code should handle these exceptions more gracefully
} catch (InstantiationException x) {
x.printStackTrace();
} catch (InvocationTargetException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
} catch (NoSuchFieldException x) {
x.printStackTrace();
}
}
}
注意:
Class.newInstance()
仅当构造函数具有零参数并且已经可访问时,才会成功。否则,必须 Constructor.newInstance()
使用如上例所示。
UNIX 系统的输出示例:
$ java ConsoleCharset
Console charset : ISO-8859-1
Charset.defaultCharset() : ISO-8859-1
Windows 系统的示例输出:(博主win10)
C:\> java ConsoleCharset
Console charset : x-mswin-936
Charset.defaultCharset(): UTF-8
另一个 Constructor.newInstance()
常见的应用是调用接受参数的构造函数。该 RestoreAliases
示例查找特定的单参数构造函数并调用它:
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static java.lang.System.out;
class EmailAliases {
private Set<String> aliases;
private EmailAliases(HashMap<String, String> h) {
aliases = h.keySet();
}
public void printKeys() {
out.format("Mail keys:%n");
for (String k : aliases)
out.format(" %s%n", k);
}
}
public class RestoreAliases {
private static Map<String, String> defaultAliases = new HashMap<String, String>();
static {
defaultAliases.put("Duke", "duke@i-love-java");
defaultAliases.put("Fang", "fang@evil-jealous-twin");
}
public static void main(String... args) {
try {
Constructor ctor = EmailAliases.class.getDeclaredConstructor(HashMap.class);
ctor.setAccessible(true);
EmailAliases email = (EmailAliases) ctor.newInstance(defaultAliases);
email.printKeys();
// production code should handle these exceptions more gracefully
} catch (InstantiationException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
} catch (InvocationTargetException x) {
x.printStackTrace();
} catch (NoSuchMethodException x) {
x.printStackTrace();
}
}
}
此示例使用 Class.getDeclaredConstructor()
查找具有类型为 java.util.HashMap
的单个参数的构造函数。请注意,传递 HashMap.class
就足够了,因为任何 get*Constructor()
方法的参数只需要一个类(class)用于类型目的(type purposes)。由于类型擦除,以下表达式的计算结果为 true
:
HashMap.class == defaultAliases.getClass()
然后,该示例使用此构造函数的 Constructor.newInstance()
方法创建该类的新实例。
$ java RestoreAliases
Mail keys:
Duke
Fang
疑难解答
开发人员在尝试通过反射调用构造函数时有时会遇到以下问题。
InstantiationException,由于缺少零参数构造函数
该 ConstructorTrouble
示例说明了当代码尝试使用 Class.newInstance()
创建类的新实例,但没有可访问的零参数构造函数时,会发生什么情况,:
public class ConstructorTrouble {
private ConstructorTrouble(int i) {
}
public static void main(String... args) {
try {
Class<?> c = Class.forName("ConstructorTrouble");
Object o = c.newInstance(); // InstantiationException
// production code should handle these exceptions more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
} catch (InstantiationException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
}
}
$ java ConstructorTrouble
java.lang.InstantiationException: ConstructorTrouble
at java.lang.Class.newInstance(Class.java:427)
at ConstructorTrouble.main(ConstructorTrouble.java:8)
Caused by: java.lang.NoSuchMethodException: ConstructorTrouble.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.newInstance(Class.java:412)
... 1 more
提示: InstantiationException
可能发生的原因有很多。在这种情况下,问题在于带有 int
参数的构造函数的存在会阻止编译器生成默认(或零参数)构造函数,并且代码中没有显式的零参数构造函数。请记住, Class.newInstance()
的行为与 new
关键字非常相似,并且当 new
失败时这个方法也会失败。
Class.newInstance() 引发意外异常(Unexpected Exception)
该 ConstructorTroubleToo
示例显示了 Class.newInstance()
中无法解决的问题。也就是说,它可以传递构造函数抛出的任何异常(checked或unchecked)。
import java.lang.reflect.InvocationTargetException;
import static java.lang.System.err;
public class ConstructorTroubleToo {
public ConstructorTroubleToo() {
throw new RuntimeException("exception in constructor");
}
public static void main(String... args) {
try {
Class<?> c = Class.forName("ConstructorTroubleToo");
// Method propagetes any exception thrown by the constructor
// (including checked exceptions).
if (args.length > 0 && args[0].equals("class")) {
Object o = c.newInstance();
} else {
Object o = c.getConstructor().newInstance();
}
// production code should handle these exceptions more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
} catch (InstantiationException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
} catch (NoSuchMethodException x) {
x.printStackTrace();
} catch (InvocationTargetException x) {
x.printStackTrace();
err.format("%n%nCaught exception: %s%n", x.getCause());
}
}
}
$ java ConstructorTroubleToo class
Exception in thread "main" java.lang.RuntimeException: exception in constructor
at ConstructorTroubleToo.<init>(ConstructorTroubleToo.java:7)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at java.lang.Class.newInstance(Class.java:442)
at ConstructorTroubleToo.main(ConstructorTroubleToo.java:16)
这种情况是反射独有的。通常,不可能编写忽略异常检查的代码,因为它不会编译。可以使用 Constructor.newInstance()
而不是 Class.newInstance()
来包装构造函数引发的任何异常。
$ java ConstructorTroubleToo
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at ConstructorTroubleToo.main(ConstructorTroubleToo.java:18)
Caused by: java.lang.RuntimeException: exception in constructor
at ConstructorTroubleToo.<init>(ConstructorTroubleToo.java:7)
... 5 more
Caught exception: java.lang.RuntimeException: exception in constructor
如果抛出 InvocationTargetException
,则该方法调用成功。问题的诊断与直接调用构造函数并引发异常 和 InvocationTargetException.getCause()
获取到的异常相同。此异常并不表示反射包或其使用存在问题。
提示:最好使用 Constructor.newInstance()
代替Class.newInstance()
,因为前面的 API 允许检查和处理构造函数引发的任意异常。
查找或调用正确的构造函数时出现问题
该 ConstructorTroubleAgain
类演示了不正确的代码无法找到或调用预期构造函数的各种方式。
import java.lang.reflect.InvocationTargetException;
import static java.lang.System.out;
public class ConstructorTroubleAgain {
public ConstructorTroubleAgain() {
}
public ConstructorTroubleAgain(Integer i) {
}
public ConstructorTroubleAgain(Object o) {
out.format("Constructor passed Object%n");
}
public ConstructorTroubleAgain(String s) {
out.format("Constructor passed String%n");
}
public static void main(String... args) {
String argType = (args.length == 0 ? "" : args[0]);
try {
Class<?> c = Class.forName("ConstructorTroubleAgain");
if ("".equals(argType)) {
// IllegalArgumentException: wrong number of arguments
Object o = c.getConstructor().newInstance("foo");
} else if ("int".equals(argType)) {
// NoSuchMethodException - looking for int, have Integer
Object o = c.getConstructor(int.class);
} else if ("Object".equals(argType)) {
// newInstance() does not perform method resolution
Object o = c.getConstructor(Object.class).newInstance("foo");
} else {
assert false;
}
// production code should handle these exceptions more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
} catch (NoSuchMethodException x) {
x.printStackTrace();
} catch (InvocationTargetException x) {
x.printStackTrace();
} catch (InstantiationException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
}
}
$ java ConstructorTroubleAgain
Exception in thread "main" java.lang.IllegalArgumentException: wrong number of
arguments
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance
(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance
(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at ConstructorTroubleAgain.main(ConstructorTroubleAgain.java:23)
抛出 IllegalArgumentException
是因为请求了零参数构造函数并尝试传递参数。如果向构造函数传递了错误类型的参数,则会引发相同的异常。
$ java ConstructorTroubleAgain int
java.lang.NoSuchMethodException: ConstructorTroubleAgain.<init>(int)
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getConstructor(Class.java:1825)
at ConstructorTroubleAgain.main(ConstructorTroubleAgain.java:29)
如果开发人员错误地认为反射会自动装箱或拆箱类型,则可能会发生此异常。装箱(将基本类型转换为引用类型)仅在编译期间发生。在反射中没有机会发生此操作,因此在查找构造函数时必须使用特定类型。
$ java ConstructorTroubleAgain Object
Constructor passed Object
在使用newInstance()
方法时,若传入了更具体的String
类型参数,我们或许会期待能够调用接收String
参数的构造器。然而,这已经是不可能的!因为此时找到的构造器已经是那个接收Object
类型参数的构造器。newInstance()
方法并不会尝试进行方法解析;它仅仅作用于已存在的构造器对象上。
解析说明:构造器的顺序很重要
当
newInstance()
被调用时,即使传入了更具体的类型如String
,也不意味着会调用相应的构造器。一旦通过反射获取了一个构造器对象,这个对象就已经绑定到了特定的构造器签名上。这意味着无论之后
newInstance()
调用时传入的参数类型是什么,它都将使用最初绑定的那个构造器签名。
newInstance()
方法不会动态地解析或匹配构造器。它只是简单地操作已经获取的构造器对象,而不考虑实际传入的参数类型是否更加具体或匹配其他可用的构造器签名。
提示: Constructor.newInstance()
和 new
之间最重要的区别在于执行 new
方法时会对参数类型检查、装箱(拆箱)和方法解析。这些在反射中都不会发生,在反射中必须做出明确的选择。
// 拆箱也是支持的 public ConstructorTroubleAgain(int i) { out.println("haha"); } ... new ConstructorTroubleAgain(new Integer(4));
IllegalAccessException 尝试调用不可访问的构造函数时
如果尝试调用私有(private)构造函数或其他无法访问的构造函数,则可能会引发 IllegalAccessException
。该 ConstructorTroubleAccess
示例说明了生成的堆栈跟踪。
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
class Deny {
private Deny() {
System.out.format("Deny constructor%n");
}
}
public class ConstructorTroubleAccess {
public static void main(String... args) {
try {
Constructor c = Deny.class.getDeclaredConstructor();
// c.setAccessible(true); // solution
c.newInstance();
// production code should handle these exceptions more gracefully
} catch (InvocationTargetException x) {
x.printStackTrace();
} catch (NoSuchMethodException x) {
x.printStackTrace();
} catch (InstantiationException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
}
}
$ java ConstructorTroubleAccess
java.lang.IllegalAccessException: Class ConstructorTroubleAccess can not access
a member of class Deny with modifiers "private"
at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:65)
at java.lang.reflect.Constructor.newInstance(Constructor.java:505)
at ConstructorTroubleAccess.main(ConstructorTroubleAccess.java:15)
提示:在Java中,存在一种访问限制,该限制阻止了反射调用那些通常无法通过直接调用访问的构造器(这包括但不限于独立类中的私有private构造器以及独立私有private类中的公共public构造器)。然而,Constructor
类被声明为扩展了AccessibleObject
,这提供了通过AccessibleObject.setAccessible()
来抑制此类访问检查的能力。
课程:数组和枚举类型
从 Java 虚拟机的角度来看,数组和枚举类型(或枚举)都只是类。 Class
中的许多方法都可以用于它们。反射为数组和枚举提供了一些特定的 API。本课使用一系列代码示例来描述如何将这些对象与其他类区分开来并对其进行操作。还检查了各种错误。
数组
数组具有组件类型和长度(不属于该类型的一部分)。数组可以整体操作,也可以按组件操作。反射为后一个目的提供了 java.lang.reflect.Array
类
- 标识数组类型(Identifying Array Types )介绍如何确定类成员是否为数组类型的字段
- 创建新数组(Creating New Arrays)说明了如何使用简单和复杂的组件类型创建数组的新实例
- 获取和设置数组及其组件(Getting and Setting Arrays and Their Components) 演示如何访问数组类型的字段并单独访问数组元素
- 疑难解答(Troubleshooting)涵盖常见错误和编程误解
枚举类型
枚举的处理方式与反射代码中的普通类非常相似。 Class.isEnum(
表示一个Class
是否存在并且是 和 enum
类型。 Class.getEnumConstants()
检索枚举中定义的枚举常量。 java.lang.reflect.Field.isEnumConstant()
指明字段是否为枚举类型。
- 检查枚举(Examining Enums)说明了如何检索枚举的常量以及任何其他字段、构造函数和方法
- 获取和设置具有枚举类型的字段(Getting and Setting Fields with Enum Types) 演示如何设置和获取具有枚举常量值的字段
- 疑难解答(Troubleshooting )描述了与枚举相关的常见错误
数组
数组是引用类型的对象,它包含相同类型的固定数量的组件;数组的长度是不可变的。创建数组的实例需要确定长度和组件类型。每个组件可以是基本类型(如 byte
、 int
或 double
)、引用类型(如 String
、 Object
或 java.nio.CharBuffer
)或数组。多维数组实际上只是包含数组类型组件的数组。
数组是在 Java 虚拟机中实现的。数组上仅有的方法都是继承自 Object
的方法。数组的长度不是其类型的一部分;数组有一个 可通过 java.lang.reflect.Array.getLength()
访问的length
字段。
反射供了用于访问数组类型和数组组件类型、创建新数组以及获取和设置数组组件值的方法。以下各节包括对数组的常见操作示例:
- 识别数组类型(Identifying Array Types) 介绍如何确定类成员是否为数组类型的字段
- 创建新数组(Creating New Arrays)说明了如何使用简单和复杂的组件类型创建数组的新实例
- 获取和设置数组及其组件(Getting and Setting Arrays and Their Components) 演示如何访问数组类型的字段并单独访问数组元素
- 疑难解答(Troubleshooting)包含了常见的错误和编程误解
所有这些操作都可通过java.lang.reflect.Array
中的静态方法实现。
识别数组类型
可以通过调用 Class.isArray()
来识别数组类型。若要获取 Class
请使用此文中的“获取类对象( Retrieving Class Objects)”部分中描述的方法之一。
该 ArrayFind
示例识别指定类中数组类型的字段,并报告每个字段的组件类型。
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import static java.lang.System.out;
public class ArrayFind {
public static void main(String... args) {
boolean found = false;
try {
Class<?> cls = Class.forName(args[0]);
Field[] flds = cls.getDeclaredFields();
for (Field f : flds) {
Class<?> c = f.getType();
if (c.isArray()) {
found = true;
out.format("%s%n"
+ " Field: %s%n"
+ " Type: %s%n"
+ " Component Type: %s%n",
f, f.getName(), c, c.getComponentType());
}
}
if (!found) {
out.format("No array fields%n");
}
// production code should handle this exception more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
}
在Class.getName()
中描述了 Class.get*Type()
返回值的语法。类型名称开头的 ’ [
’ 表示数组的维数(即嵌套深度)。
输出示例如下。用户输入为斜体。基本类型的 byte
数组:
$java ArrayFind java.nio.ByteBuffer
final byte[] java.nio.ByteBuffer.hb
Field: hb
Type: class [B
Component Type: byte
引用类型的 StackTraceElement
数组:
$ java ArrayFind java.lang.Throwable
private java.lang.StackTraceElement[] java.lang.Throwable.stackTrace
Field: stackTrace
Type: class [Ljava.lang.StackTraceElement;
Component Type: class java.lang.StackTraceElement
predefined
是引用类型 java.awt.Cursor
的一维数组, cursorProperties
是引用类型String
的二维数组:
$ java ArrayFind java.awt.Cursor
protected static java.awt.Cursor[] java.awt.Cursor.predefined
Field: predefined
Type: class [Ljava.awt.Cursor;
Component Type: class java.awt.Cursor
static final java.lang.String[][] java.awt.Cursor.cursorProperties
Field: cursorProperties
Type: class [[Ljava.lang.String;
Component Type: class [Ljava.lang.String;
创建新数组
就像在非反射代码中一样,反射支持通过 java.lang.reflect.Array.newInstance()
动态创建任意类型和维度的数组的能力。考虑 ArrayCreator
,一个能够动态创建数组的基本解释器。将要解析的语法如下:
fully_qualified_class_name variable_name[] =
{ val1, val2, val3, ... }
假定 the fully_qualified_class_name
表示具有单个 String
参数的构造函数的类。数组的维数由提供的值数决定。下面的示例将构造一个 fully_qualified_class_name
数组的实例,并使用 val1
、 val2
等给出的实例填充其值(此示例假定熟悉 Class.getConstructor()
和 java.lang.reflect.Constructor.newInstance()
。有关 Constructor
的反射API 的讨论,请参阅此跟踪的“创建新类实例(Creating New Class Instances)”部分。
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.Arrays;
import static java.lang.System.out;
public class ArrayCreator {
private static String s = "java.math.BigInteger bi[] = { 123, 234, 345 }";
private static Pattern p = Pattern.compile("^\\s*(\\S+)\\s*\\w+\\[\\].*\\{\\s*([^}]+)\\s*\\}");
public static void main(String... args) {
Matcher m = p.matcher(s);
if (m.find()) {
String cName = m.group(1);
String[] cVals = m.group(2).split("[\\s,]+");
int n = cVals.length;
try {
Class<?> c = Class.forName(cName);
Object o = Array.newInstance(c, n);
for (int i = 0; i < n; i++) {
String v = cVals[i];
Constructor ctor = c.getConstructor(String.class);
Object val = ctor.newInstance(v);
Array.set(o, i, val);
}
Object[] oo = (Object[])o;
out.format("%s[] = %s%n", cName, Arrays.toString(oo));
// production code should handle these exceptions more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
} catch (NoSuchMethodException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
} catch (InstantiationException x) {
x.printStackTrace();
} catch (InvocationTargetException x) {
x.printStackTrace();
}
}
}
}
$ java ArrayCreator
java.math.BigInteger [] = [123, 234, 345]
上面的例子显示了一种情况,其中可能需要通过反射创建一个数组;也就是说,如果组件类型在运行之前是未知的。在本例中,代码用于 Class.forName()
获取所需组件类型的类,然后调用特定的构造函数来初始化数组的每个组件,然后再设置相应的数组值。
获取和设置数组及其组件
就像在非反射代码中一样,数组字段可以按组件整体或组件进行设置或获取。要一次设置整个数组,请使用 java.lang.reflect.Field.set(Object obj, Object value)
。若要获取整个数组,请使用 Field.get(Object)
。可以使用 java.lang.reflect.Array
中的方法设置或获取单个组件。
Array
提供各种形式(int、float)的 setFoo()
和getFoo()
方法用于设置和获取任何基本类型的组件的方法。例如, int
数组的组件可以使用Array.setInt(Object array, int index, int value)
设置 ,也可使用 Array.getInt(Object array, int index)
获取 。
这些方法支持自动扩展数据类型。因此,可用于设置 int
数组的值, Array.getShort()
因为 16 位 short
可以扩展到 32 位 int
而不会丢失数据;另一方面, Array.setLong()
调用数 int
组将导致抛出 IllegalArgumentException
,因为 64 位 long
无法缩小到 32 位 int
存储而不会丢失信息。无论传递的实际值是否可以在目标数据类型中准确表示,都是如此。 The Java Language Specification, Java SE 7 Edition 的 Widening Primitive Conversion 和Narrowing Primitive Conversion部分包含了有关基本数据放大和缩小转换的完整讨论。
引用类型数组(包括数组的数组)的组件使用 Array.set(Object array, int index, int value)
和 Array.get(Object array, int index)
进行设置和检索。
设置数组类型的字段
GrowBufferedReader
示例展示了如何替换数组类型的字段值。在这个案例中,代码将java.io.BufferedReader
的底层数组替换为一个更大的数组。(这假设原始BufferedReader
的创建是在不可修改的代码中;否则,只需使用接受输入缓冲区大小的替代构造器BufferedReader(java.io.Reader in, int size)
即可轻松实现。)
import java.io.BufferedReader;
import java.io.CharArrayReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Arrays;
import static java.lang.System.out;
public class GrowBufferedReader {
private static final int srcBufSize = 10 * 1024;
private static char[] src = new char[srcBufSize];
static {
src[srcBufSize - 1] = 'x';
}
private static CharArrayReader car = new CharArrayReader(src);
public static void main(String... args) {
try {
BufferedReader br = new BufferedReader(car);
Class<?> c = br.getClass();
Field f = c.getDeclaredField("cb");
// cb is a private field
f.setAccessible(true);
char[] cbVal = char[].class.cast(f.get(br));
char[] newVal = Arrays.copyOf(cbVal, cbVal.length * 2);
if (args.length > 0 && args[0].equals("grow"))
f.set(br, newVal);
for (int i = 0; i < srcBufSize; i++)
br.read();
// see if the new backing array is being used
if (newVal[srcBufSize - 1] == src[srcBufSize - 1])
out.format("Using new backing array, size=%d%n", newVal.length);
else
out.format("Using original backing array, size=%d%n", cbVal.length);
// production code should handle these exceptions more gracefully
} catch (FileNotFoundException x) {
x.printStackTrace();
} catch (NoSuchFieldException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
} catch (IOException x) {
x.printStackTrace();
}
}
}
$ java GrowBufferedReader grow
Using new backing array, size=16384
$ java GrowBufferedReader
Using original backing array, size=8192
请注意,上面的示例使用了数组工具方法 java.util.Arrays.copyOf)
。java.util.Arrays
包含许多方便操作数组的方法。
访问多维数组的元素
多维数组只是嵌套数组。二维数组是数组的数组。三维数组是二维数组的数组,依此类推。该 CreateMatrix
示例说明了如何使用反射创建和初始化多维数组。
import java.lang.reflect.Array;
import static java.lang.System.out;
public class CreateMatrix {
public static void main(String... args) {
Object matrix = Array.newInstance(int.class, 2, 2);
Object row0 = Array.get(matrix, 0);
Object row1 = Array.get(matrix, 1);
Array.setInt(row0, 0, 1);
Array.setInt(row0, 1, 2);
Array.setInt(row1, 0, 3);
Array.setInt(row1, 1, 4);
for (int i = 0; i < 2; i++)
for (int j = 0; j < 2; j++)
out.format("matrix[%d][%d] = %d%n", i, j, ((int[][])matrix)[i][j]);
}
}
$ java CreateMatrix
matrix[0][0] = 1
matrix[0][1] = 2
matrix[1][0] = 3
matrix[1][1] = 4
使用以下代码片段可以获得相同的结果:
Object matrix = Array.newInstance(int.class, 2);
Object row0 = Array.newInstance(int.class, 2);
Object row1 = Array.newInstance(int.class, 2);
Array.setInt(row0, 0, 1);
Array.setInt(row0, 1, 2);
Array.setInt(row1, 0, 3);
Array.setInt(row1, 1, 4);
Array.set(matrix, 0, row0);
Array.set(matrix, 1, row1);
可变参数方法 Array.newInstance(Class componentType, int... dimensions)
提供了一种创建多维数组的便捷方法,但组件仍然需要使用多维数组是嵌套数组的原则进行初始化。(反射不为此提供多个索引的 get
/ set
方法。)
补充:也就是无法一次性set多个下标
疑难解答
以下示例显示了在数组上操作时可能发生的典型错误。
由于不可转换类型导致的 IllegalArgumentException
该 ArrayTroubleAgain
示例将生成一个 IllegalArgumentException
。调用 Array.setInt()
为一个引用类型为 Integer
的组件设置基本类型 int
时,在非反射下等同于 ary[0] = 1
,编译器会将值 1
转换为引用类型 new Integer(1)
,以便其类型检查将接受该语句。而使用反射时,类型检查仅在运行时进行,因此没有机会对值进行装箱(拆箱同理)。
import java.lang.reflect.Array;
import static java.lang.System.err;
public class ArrayTroubleAgain {
public static void main(String... args) {
Integer[] ary = new Integer[2];
try {
Array.setInt(ary, 0, 1); // IllegalArgumentException
// production code should handle these exceptions more gracefully
} catch (IllegalArgumentException x) {
err.format("Unable to box%n");
} catch (ArrayIndexOutOfBoundsException x) {
x.printStackTrace();
}
}
}
$ java ArrayTroubleAgain
Unable to box
若要消除此异常,应将有问题的行替换为以下调用 Array.set(Object array, int index, Object value)
:
Array.set(ary, 0, new Integer(1));
提示: 使用反射设置或获取数组组件时,编译器没有机会执行装箱/拆箱。它只能转换 Class.isAssignableFrom()
规范中描述的相关类型。该示例预计会失败,因为 isAssignableFrom()
将在此测试中返回 false
,该测试可以通过编程方式用于验证是否可以进行特定转换:
Integer.class.isAssignableFrom(int.class) == false
同样在反射中,从基本类型到引用类型的自动转换也是不可能的。
int.class.isAssignableFrom(Integer.class) == false
空数组的 ArrayIndexOutOfBoundsException
该 ArrayTrouble
示例说明了在尝试访问长度为零的数组的元素时将发生的错误:
import java.lang.reflect.Array;
import static java.lang.System.out;
public class ArrayTrouble {
public static void main(String... args) {
Object o = Array.newInstance(int.class, 0);
int[] i = (int[])o;
int[] j = new int[0];
out.format("i.length = %d, j.length = %d, args.length = %d%n",
i.length, j.length, args.length);
Array.getInt(o, 0); // ArrayIndexOutOfBoundsException
}
}
$ java ArrayTrouble
i.length = 0, j.length = 0, args.length = 0
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
at java.lang.reflect.Array.getInt(Native Method)
at ArrayTrouble.main(ArrayTrouble.java:11)
**提示:**可以有没有元素的数组(空数组)。在普通代码中,只有少数情况可以看到它们,但它们可能会在反射中无意中发生。因为不可能设置/获取空数组的值,所以将抛出一个 ArrayIndexOutOfBoundsException
。
IllegalArgumentException(如果尝试缩小范围)
该 ArrayTroubleToo
示例包含失败的代码,因为它尝试执行可能丢失数据的操作:
import java.lang.reflect.Array;
import static java.lang.System.out;
public class ArrayTroubleToo {
public static void main(String... args) {
Object o = new int[2];
Array.setShort(o, 0, (short)2); // widening, succeeds
Array.setLong(o, 1, 2L); // narrowing, fails
}
}
$ java ArrayTroubleToo
Exception in thread "main" java.lang.IllegalArgumentException: argument type
mismatch
at java.lang.reflect.Array.setLong(Native Method)
at ArrayTroubleToo.main(ArrayTroubleToo.java:9)
提示: Array.set*()
和 Array.get*()
方法将执行自动放大转换,但如果尝试缩小转换,则会抛出一个 IllegalArgumentException
。有关扩大和缩小转换的完整讨论,请分别参见 The Java Language Specification, Java SE 7 Edition的 Widening Primitive Conversion 和Narrowing Primitive Conversion 章节。
枚举类型
枚举是一种语言构造,用于定义类型安全枚举(多线程下),当需要一组固定的命名值时,可以使用枚举。所有枚举都隐式扩展 java.lang.Enum
。枚举可以包含一个或多个枚举常量,这些常量定义枚举类型的唯一实例。枚举声明定义了一个枚举类型,该类型与类非常相似,因为它可能具有字段、方法和构造函数等成员(具有一些限制)。
由于枚举是类,因此反射不需要定义一个显式的 java.lang.reflect.Enum
类。唯一特定于枚举的反射 API 包括 Class.isEnum()
、 Class.getEnumConstants()
和 java.lang.reflect.Field.isEnumConstant()
。大多数涉及枚举的反射操作与任何其他类或成员相同。例如,枚举常量作为 public static final
枚举上的字段实现。以下各节介绍如何使用 Class
和 java.lang.reflect.Field
处理枚举。
- 检查枚举(Examining Enums)说明了如何检索枚举的常量以及任何其他字段、构造函数和方法
- 获取和设置具有枚举类型的字段(Getting and Setting Fields with Enum Types) 演示如何设置和获取具有枚举常量值的字段
- 疑难解答(Troubleshooting)描述了与枚举相关的常见错误
有关枚举的简介,请参阅枚举类型(Enum Types)课程。
检查枚举
反射提供了三个特定于枚举的 API:
-
指示此类是否表示枚举类型
-
获取由枚举定义的枚举常量列表,这些常量按声明顺序进行
-
java.lang.reflect.Field.isEnumConstant()
指示此字段是否表示枚举类型的元素
有时需要动态获取枚举常量列表;在非反射代码中,这是通过在枚举上调用隐式声明的静态方法 values()
来实现的。如果枚举类型的实例不可用,则获取可能值列表的唯一方法是调用, Class.getEnumConstants()
因为无法实例化枚举类型。
给定一个完全限定的名称,该 EnumConstants
示例演示如何使用 Class.getEnumConstants()
获取枚举中常量的有序列表。
import java.util.Arrays;
import static java.lang.System.out;
enum Eon { HADEAN, ARCHAEAN, PROTEROZOIC, PHANEROZOIC }
public class EnumConstants {
public static void main(String... args) {
try {
Class<?> c = (args.length == 0 ? Eon.class : Class.forName(args[0]));
out.format("Enum name: %s%nEnum constants: %s%n",
c.getName(), Arrays.asList(c.getEnumConstants()));
if (c == Eon.class)
out.format(" Eon.values(): %s%n",
Arrays.asList(Eon.values()));
// production code should handle this exception more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
}
输出示例如下。用户输入为斜体。
$ java EnumConstants java.lang.annotation.RetentionPolicy
Enum name: java.lang.annotation.RetentionPolicy
Enum constants: [SOURCE, CLASS, RUNTIME]
$ java EnumConstants java.util.concurrent.TimeUnit
Enum name: java.util.concurrent.TimeUnit
Enum constants: [NANOSECONDS, MICROSECONDS,
MILLISECONDS, SECONDS,
MINUTES, HOURS, DAYS]
此示例还显示,返回 Class.getEnumConstants()
的值与调用 values()
枚举类型返回的值相同。
$ java EnumConstants
Enum name: Eon
Enum constants: [HADEAN, ARCHAEAN,
PROTEROZOIC, PHANEROZOIC]
Eon.values(): [HADEAN, ARCHAEAN,
PROTEROZOIC, PHANEROZOIC]
由于枚举是类,因此可以使用本篇的“字段”、“方法”和“构造函数”部分中描述的相同反射 API 获取其他信息。该 EnumSpy
代码演示如何使用这些 API 获取有关枚举声明的其他信息。该示例用Class.isEnum()
限制检查的类集。还用 Field.isEnumConstant()
将枚举常量与枚举声明中的其他字段区分开来(并非所有字段都是枚举常量)。
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Member;
import java.util.List;
import java.util.ArrayList;
import static java.lang.System.out;
public class EnumSpy {
private static final String fmt = " %11s: %s %s%n";
public static void main(String... args) {
try {
Class<?> c = Class.forName(args[0]);
if (!c.isEnum()) {
out.format("%s is not an enum type%n", c);
return;
}
out.format("Class: %s%n", c);
Field[] flds = c.getDeclaredFields();
List<Field> cst = new ArrayList<Field>(); // enum constants
List<Field> mbr = new ArrayList<Field>(); // member fields
for (Field f : flds) {
if (f.isEnumConstant())
cst.add(f);
else
mbr.add(f);
}
if (!cst.isEmpty())
print(cst, "Constant");
if (!mbr.isEmpty())
print(mbr, "Field");
Constructor[] ctors = c.getDeclaredConstructors();
for (Constructor ctor : ctors) {
out.format(fmt, "Constructor", ctor.toGenericString(),
synthetic(ctor));
}
Method[] mths = c.getDeclaredMethods();
for (Method m : mths) {
out.format(fmt, "Method", m.toGenericString(),
synthetic(m));
}
// production code should handle this exception more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
private static void print(List<Field> lst, String s) {
for (Field f : lst) {
out.format(fmt, s, f.toGenericString(), synthetic(f));
}
}
private static String synthetic(Member m) {
return (m.isSynthetic() ? "[ synthetic ]" : "");
}
}
$ java EnumSpy java.lang.annotation.RetentionPolicy
Class: class java.lang.annotation.RetentionPolicy
Constant: public static final java.lang.annotation.RetentionPolicy
java.lang.annotation.RetentionPolicy.SOURCE
Constant: public static final java.lang.annotation.RetentionPolicy
java.lang.annotation.RetentionPolicy.CLASS
Constant: public static final java.lang.annotation.RetentionPolicy
java.lang.annotation.RetentionPolicy.RUNTIME
Field: private static final java.lang.annotation.RetentionPolicy[]
java.lang.annotation.RetentionPolicy. [ synthetic ]
Constructor: private java.lang.annotation.RetentionPolicy()
Method: public static java.lang.annotation.RetentionPolicy[]
java.lang.annotation.RetentionPolicy.values()
Method: public static java.lang.annotation.RetentionPolicy
java.lang.annotation.RetentionPolicy.valueOf(java.lang.String)
输出显示声明 java.lang.annotation.RetentionPolicy
仅包含三个枚举常量。枚举常量公开为 public static final
字段。字段、构造函数和方法由编译器生成。该 $VALUES
字段与 values()
方法的实现有关。
注意:由于各种原因,包括支持枚举类型的演变,枚举常量的声明顺序很重要。 Class.getFields()
和Class.getDeclaredFields()
不保证返回值的顺序与声明源代码中的顺序匹配。如果应用程序需要排序,请使用 Class.getEnumConstants()
。
java.util.concurrent.TimeUnit
的输出表明,更复杂的枚举是可能的。此类包括多个方法以及声明 static final
的非枚举常量的其他字段。
$ java EnumSpy java.util.concurrent.TimeUnit
Class: class java.util.concurrent.TimeUnit
Constant: public static final
...忽略后面N个枚举
获取和设置具有枚举类型的字段
存储枚举的字段将像任何其他引用类型一样设置和检索,使用 Field.set()
和 Field.get()
。有关访问字段的详细信息,请参阅此跟踪的字段部分。
考虑一个需要在运行时动态修改服务器应用程序跟踪级别(TraceLevel)的应用,而通常这种更改在运行时是不允许的。假设已经获得了服务器对象的实例。SetTrace
示例演示了代码如何将枚举类型的字符串表示转换为枚举类型,并获取及设置存储枚举的字段的值。
import java.lang.reflect.Field;
import static java.lang.System.out;
enum TraceLevel { OFF, LOW, MEDIUM, HIGH, DEBUG }
class MyServer {
private TraceLevel level = TraceLevel.OFF;
}
public class SetTrace {
public static void main(String... args) {
TraceLevel newLevel = TraceLevel.valueOf(args[0]);
try {
MyServer svr = new MyServer();
Class<?> c = svr.getClass();
Field f = c.getDeclaredField("level");
f.setAccessible(true);
TraceLevel oldLevel = (TraceLevel)f.get(svr);
out.format("Original trace level: %s%n", oldLevel);
if (oldLevel != newLevel) {
f.set(svr, newLevel);
out.format(" New trace level: %s%n", f.get(svr));
}
// production code should handle these exceptions more gracefully
} catch (IllegalArgumentException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
} catch (NoSuchFieldException x) {
x.printStackTrace();
}
}
}
由于枚举常量是单例, 因此可以使用 ==
和 !=
运算符来比较相同类型的枚举常量。
$ java SetTrace OFF
Original trace level: OFF
$ java SetTrace DEBUG
Original trace level: OFF
New trace level: DEBUG
在实际项目中动态更改log的日志级别可以使用以下代码
// 日志在类中的定义 private final static Logger log = LoggerFactory.getLogger(XXX.class);
方法一:使用反射(缺点是需要对象)
@Autowired private XXX xxx; public void modify() { Class<?> c = xxx.getClass(); Field f = c.getDeclaredField("log"); f.setAccessible(true); Object logInstance = f.get(null); // 因为日志字段通常是static类型的 if (logInstance instanceof Logger) { Method setLevel = logInstance.getClass().getMethod("setLevel", Level.class); setLevel.invoke(logInstance, Level.ERROR); } }
方法二:使用日志自带的Context
public void modify() { LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); Logger logger = loggerContext.getLogger(XXX.class); logger.setLevel(Level.ERROR); }
疑难解答
下面的示例演示使用枚举类型时可能遇到的问题。
IllegalArgumentException 尝试实例化枚举类型时
如前所述,枚举类型的实例化是被禁止的。该 EnumTrouble
示例将尝试此操作。
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import static java.lang.System.out;
enum Charge {
POSITIVE, NEGATIVE, NEUTRAL;
Charge() {
out.format("under construction%n");
}
}
public class EnumTrouble {
public static void main(String... args) {
try {
Class<?> c = Charge.class;
Constructor[] ctors = c.getDeclaredConstructors();
for (Constructor ctor : ctors) {
out.format("Constructor: %s%n", ctor.toGenericString());
ctor.setAccessible(true);
ctor.newInstance();
}
// production code should handle these exceptions more gracefully
} catch (InstantiationException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
} catch (InvocationTargetException x) {
x.printStackTrace();
}
}
}
$ java EnumTrouble
Constructor: private Charge()
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at EnumTrouble.main(EnumTrouble.java:24)
**提示:**在编译时尝试显式实例化一个枚举类型是错误的,因为这会阻止定义的枚举常量保持唯一性。这一限制同样在反射代码中得到强制执行。在使用默认构造器实例化类的代码应该首先调用Class.isEnum()
来判断该类是否为枚举类型。
设置具有不兼容枚举类型的字段时的 IllegalArgumentException
存储枚举的字段应使用相应的枚举类型设置。(实际上,任何类型的字段都必须使用兼容的类型设置。)EnumTroubleToo示例产生了预期的错误。
import java.lang.reflect.Field;
enum E0 { A, B }
enum E1 { A, B }
class ETest {
private E0 fld = E0.A;
}
public class EnumTroubleToo {
public static void main(String... args) {
try {
ETest test = new ETest();
Field f = test.getClass().getDeclaredField("fld");
f.setAccessible(true);
f.set(test, E1.A); // IllegalArgumentException
// production code should handle these exceptions more gracefully
} catch (NoSuchFieldException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
}
}
$ java EnumTroubleToo
Exception in thread "main" java.lang.IllegalArgumentException: Can not set E0
field ETest.fld to E1
at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException
(UnsafeFieldAccessorImpl.java:146)
at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException
(UnsafeFieldAccessorImpl.java:150)
at sun.reflect.UnsafeObjectFieldAccessorImpl.set
(UnsafeObjectFieldAccessorImpl.java:63)
at java.lang.reflect.Field.set(Field.java:657)
at EnumTroubleToo.main(EnumTroubleToo.java:16)
提示: 严格来说,尝试将类型为X的字段设置为类型为Y的值,只有在以下语句为真时才能成功:
X.class.isAssignableFrom(Y.class) == true
代码可以被修改以执行以下测试,以验证类型是否兼容:
if (f.getType().isAssignableFrom(E0.class))
// compatible
else
// expect IllegalArgumentException
反射 API:Trail 的终结
您已经到达了“The Reflection API”路径(Trail)的终点。
如果您对此路线有任何意见或建议,请使用我们的反馈页面告诉我们。