MENU

The Reflection API

2021 年 01 月 06 日 • 阅读: 4078 • 英语

Uses of Reflection

反射通常用于检测和修改程序的运行时行为。这是一项相对高级的功能,应只被有牢固语言基础的开发者使用。记住这个警告,反射将是一项强大技术,能够让程序执行那些没有它无法执行的操作。

Extensibility Features

应用可以通过类全限定名创建外部自定义类的对象实例作为扩展。

Class Browsers and Visual Development Environments

类浏览器必须有能力枚举类的成员。可视化开发环境可以使用反射提供的类型信息帮助开发者编写正确的代码。

Debuggers and Test Tools

调试器必须能够检测类的私有成员。测试工具可以通过反射对定义在类上的可发现的 API 集合进行系统调用,在测试套件中完成更高等级的代码覆盖。

Drawbacks of Reflection

反射很强大,但不应被随意使用。如果不通过反射可以完成一项操作,那最好避免使用它。通过反射访问代码时,请牢记以下注意事项。

Performance Overhead

因为反射涉及动态类型解析,某些 JVM 优化不会生效。最终,一项操作的反射版本会慢于非反射版本,所以在性能敏感应用的高频调用点,不要使用反射。

Security Restrictions

安全管理系统可能不提供反射需要的运行时权限。对于那些需要运行于安全受限上下文中的代码,比如 Applet,需要着重考虑。

Exposure of Internals

由于反射允许代码执行一些在非反射下的非法操作,比如访问 私有 属性和方法,使用它可能产生未预料的副作用,比如代码功能紊乱或无法移植。反射深入了抽象内部,随着平台升级,内部行为改变,代码可能无法工作。

Trail Lessons

这份教程涵盖了反射的基本使用,包括访问和操作类、属性、方法和构造器。每个部分都包含了样例代码、建议和疑难解答。

Classes

这部分课程介绍多种获得类对象和检测其属性的方法,包括声明和内容。

Members

这部分描述了如何使用反射 API 获得类的属性、方法和构造方法。提供了获得和设置属性值,调用方法和使用特定构造器创建对象实例的代码示例。

Arrays and Enumerated Types

这部分介绍两种特殊类型:运行时生成的 数组,以及用于定义唯一命名对象实例的 枚举 类型。样例代码展示了怎样获得数组的元素类型,以及怎样获得和设置数组和枚举的属性。

备注

这份教程中的示例专门用于测试反射 API。因此异常处理逻辑与生成环境中并不相同。特别地,在生产环境中,不推荐向用户展示堆栈跟踪。

Lesson: Classes

数据分基本和引用类型。类、枚举和数组(它们都继承自 java.lang.Object),还有接口都是引用类型。引用类型的具体例子有 java.lang.String,所有基本类型的包装类,如 java.lang.Doublejava.io.Serializable 接口,还有 javax.swing.SortOrder 枚举类型。基本类型集合则是固定的:boolean, byte, short, int, long, char, floatdouble

对于每种对象类型,Java 虚拟机都实例化了一个不可变的 java.lang.Class,它提供了检测对象运行时属性的方法,包括成员和类型信息。Class 也提供了创建类和对象的能力。最重要的是,它是所有反射 API 的入口。本章介绍与类相关的最常用的反射操作:

Retrieving Class Objects

java.lang.Class 是所有反射操作的入口。除了 java.lang.reflect.ReflectPermissionjava.lang.reflect 中的类都没有公共构造器。要获得这些类,必须调用 Class 的恰当方法。获得一个 Class 有几种方法,取决于代码能否访问一个对象,类名称,一个类型,或一个已有 Class

Object.getClass()

如果存在对象实例,获得其 Class 最简单的方法就是调用 Object.getClass()。当然,这仅限于继承自 Object 的引用类型。下面是一些例子:

Class c = "foo".getClass();

上面的代码返回 StringClass

Class c = System.console().getClass();

静态 方法 System.console() 返回的是一个和虚拟机相关的唯一 console。getClass() 返回的是 java.io.Console 对应的 Class

enum E { A, B }
Class c = A.getClass();

A 是枚举类型 E 的实例。因此 getClass() 返回枚举类型 E 对应的 Class

byte[] bytes = new byte[1024];
Class c3 = bytes.getClass();

因为数组是 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 一致。

The .class Syntax

如果存在类型但没有实例,可以在类型名后附加 ".class" 来获得 Class。这也是最简单的获得基本数据类型 Class 的方法。

boolean b;
Class c = b.getClass();   // compile-time error
Class c = boolean.class;  // correct

注意到 boolean.getClass() 声明将会产生编译时错误,因为 boolean 是基本类型,无法反引用。.class 语法返回 boolean 对应的 Class

Class c = java.io.PrintStream.class;

变量 c 的值是 java.io.PrintStream 对应的 Class

Class c = int[][][].class;

.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)。

TYPE Field for Primitive Type Wrappers

对于基本类型,.class 是更方便和推荐的获取 Class 的方式;但是还有另外一种方式。每种基本类型以及 voidjava.lang 下都有对应的包装类,用于将其打包成对应的引用类型。每种包装类都包含一个名为 TYPE 的属性,它等同于被其包装的基本类型对应的 Class

Class c = Double.TYPE;

当你需要一个 Object 的时候,基本类型,例如 double 会被包装成 java.lang.DoubleDouble.TYPE 的值等同于 double.class

Class c = Void.TYPE;

Void.TYPE 等同于 void.class

Methods that Return Classes

还有许多返回类的反射 API ,但它们只有当你已经直接或间接的获得了 Class 时才能使用。

Class.getSuperclass()

返回给定类的父类。

Class c = javax.swing.JButton.class.getSuperclass();

javax.swing.JButton 的父类是 javax.swing.AbstractButton.

Class.getClasses()

返回所有公共成员类、接口和枚举,包括继承来的。

Class<?>[] c = Character.class.getClasses();

Character 包含两个成员类 Character.SubsetCharacter.UnicodeBlock.(JDK 1.5)。

Class.getDeclaredClasses()

返回本类中显式声明的所有类,接口和枚举。

Class<?>[] c = Character.class.getDeclaredClasses();

Character 包含两个公共成员类 Character.SubsetCharacter.UnicodeBlock,以及一个私有成员类 Character.CharacterCache(JDK 1.5)。

getDeclaringClass()

返回声明自身的 ClassAnonymous Class Declarations 没有声明类但有封闭类。

import java.lang.reflect.Field;

Field f = System.class.getField("out");
Class c = f.getDeclaringClass();

System 声明了 out 属性。

class MyClass {
    static Object o = new Object() {
        public void m() {}
    };
    static Class c = o.getClass().getDeclaringClass();
}

o 定义的匿名类,获取声明它的类会返回 null

Class.getEnclosingClass()

返回类的直接封闭类。

Class c = Thread.State.class.getEnclosingClass();

枚举 Thread.State 的封闭类是 Thread

class MyClass {
    static Object o = new Object() {
        public void m() {}
    };
    static Class c = o.getClass().getEnclosingClass();
}

o 定义的匿名类由 MyClass 关闭。

Examining Class Modifiers and Types

类声明处的一到多个修饰符会影响其运行时行为:

  • 访问控制符:publicprotectedprivate
  • 重写修饰符: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.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[] interfaces = c.getGenericInterfaces();
            if (interfaces.length != 0) {
                for (Type i : interfaces) {
                    out.format("  %s%n", i.toString());
                }
                out.format("%n");
            } else {
                out.format("  -- No Implemented Interfaces --%n%n");
            }

            out.format("Inheritance Path:%n");
            List<Class> l = new ArrayList<>();
            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 e) {
            e.printStackTrace();
        }
    }

    private static void printAncestor(Class<?> c, List<Class> l) {
        Class<?> ancestor = c.getSuperclass();
        if (ancestor != null) {
            l.add(ancestor);
            printAncestor(ancestor, l);
        }
    }
}

下面是几个输出样例:

$ 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 的,编译器会为每个接口添加该修饰符。上述声明包含两个泛化类型参数,KV。演示代码简单地打印了参数名称,但 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 虚拟机定义。特别地,数组实现了 Cloneablejava.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 是受检异常,因为路径中不存在 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(forRemoval=true, since="1.2")

输出显示了 java.security.Identity 是一个过时 API,持有一个 java.lang.Deprecated 注解。反射代码可以用它检测过时 API。

备注

反射不能获得所有注解。只有那些 java.lang.annotation.RetentionPolicyRUNTIME 的才能被获取。三个语言预定义注解 @Deprecated@Override@SuppressWarnings,只有第一个可保留至运行时。

Discovering Class Members

Class 提供了两类,访问属性、函数和构造器的方法:枚举和搜索特定成员的方法。也提供了单独访问类上直接和超类继承成员的方法。下面的表格总结了所有成员定位方法和它们的特征。

定位属性的方法

Class API成员列表?继承成员?私有成员?
getDeclaredField()nonoyes
getField()noyesno
getDeclaredFields()yesnoyes
getFields()yesyesno

定位函数的方法

Class API成员列表?继承成员?私有成员?
getDeclaredMethod()nonoyes
getMethod()noyesno
getDeclaredMethods()yesnoyes
getMethods()yesyesno

定位构造器的方法

Class API成员列表?继承成员?私有成员?
getDeclaredConstructor()noN/A1yes
getConstructor()noN/A1no
getDeclaredConstructors()yesN/A1yes
getConstructors()yesN/A1no

1构造器无法继承。

ClassSpy 示例中,给定类名和想要获取的成员,使用 get*s() 方法查询公有元素列表,包括继承成员。

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;

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(), "Constructors");
                        printMembers(c.getFields(), "Fields");
                        printMembers(c.getMethods(), "Methods");
                        printClasses(c);
                        break;
                    default:
                        assert false;
                }
            }

        } catch (ClassNotFoundException x) {
            x.printStackTrace();
        }
    }

    private static void printClasses(Class<?> c) {
        out.format("Classes:%n");
        Class<?>[] cs = c.getClasses();
        for (Class<?> clz : cs) {
            out.format("  %s%n", clz.getCanonicalName());
        }
        if (cs.length == 0) {
            out.format(" -- No member interfaces, classes, or enums --%n");
        }
        out.format("%n");
    }

    private static void printMembers(Member[] ms, String s) {
        out.format("%s:%n", s);
        for (Member m : ms) {
            if (m instanceof Field) {
                out.format("  %s%n", ((Field) m).toGenericString());
            } else if (m instanceof Constructor) {
                out.format("  %s%n", ((Constructor<?>) m).toGenericString());
            } else if (m instanceof Method) {
                out.format(" %s%n", ((Method) m).toGenericString());
            }
        }
        if (ms.length == 0) {
            out.format("  -- No %s --%n", s);
        }
        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 和其他父类定义的异常链机制构造器(含 Throwable 参数的那些)。

$ 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 boolean java.nio.channels.Channel.isOpen()
 public abstract void java.nio.channels.Channel.close() throws java.io.IOException

java.nio.channels.ReadableByteChannel 接口定义了 read() 方法。剩余方法继承自父接口。通过把 get*s() 换成 getDeclared*s(),代码可以很容易地修改成只列出本类定义的方法。

$ java ClassSpy 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.values()
 public static ClassMember ClassMember.valueOf(java.lang.String)
 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 final int java.lang.Enum.hashCode()
 public int java.lang.Enum.compareTo(java.lang.Object)
 public final int java.lang.Enum.compareTo(E)
 public static <T extends java.lang.Enum<T>> T java.lang.Enum.valueOf(java.lang.Class<T>,java.lang.String)
 public final java.util.Optional<java.lang.Enum$EnumDesc<E>> java.lang.Enum.describeConstable()
 public final java.lang.Class<E> java.lang.Enum.getDeclaringClass()
 public final int java.lang.Enum.ordinal()
 public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
 public final void java.lang.Object.wait() throws java.lang.InterruptedException
 public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
 public final native java.lang.Class<?> java.lang.Object.getClass()
 public final native void java.lang.Object.notify()
 public final native void java.lang.Object.notifyAll()

属性部分列出了枚举常量。将这些技术属性与其他属性分开很有用。可以使用 java.lang.reflect.Field.isEnumConstant() 达到这个目的。后续教程 Examining EnumsEnumSpy 示例包含了一个可能实现。

在方法部分,可以看到方法名包含了声明它的类名。因此,toString()Enum 实现的,并非继承自 Object。可以使用 Field.getDeclaringClass() 改进代码,让它的输出更加明显。下面是可能的代码片段。

if (m instanceof Field) {
    Field f = (Field)m;
    out.format("  %s%n", f.toGenericString());
    out.format("  -- declared in: %s%n", f.getDeclaringClass());
}

Troubleshooting

下面是在类上反射会遇到的典型错误。

Compile Warning: "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 e) {
            e.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 被声明为 raw 类型(没有类型参数),而 getMethod() 对应的参数是参数化的,所以会发生未检查转换。编译器将依据规则产生警告。(见 The Java Language Specification, Java SE 7 EditionUnchecked ConversionMethod Invocation Conversion 章节。)

有两种可能的解决方案。更推荐的是修改 c 的声明,让它包含恰当的泛化类型。本例中,声明应该是:

Class<?> c = warn.getClass();

作为替代,在问题语句前显式加上预定义注解 @SuppressWarnings 可以抑制警告。

Class c = ClassWarning.class;
@SuppressWarnings("unchecked")
Method m = c.getMethod("m");
// warning gone

备注

作为通用原则,警告不该被忽略,因为它们可能暗示了潜在的 bug。使用参数化声明是恰当的做法。如果条件不允许(也许因为应用必须和库供应商代码交互),再用 @SuppressWarnings 注解那行烦人代码。

InstantiationException when the Constructor is Not Accessible

使用 Class.newInstance() 创建类实例时,如果类的无参构造器不可见,则会抛出 InstantiationException

ClassTrouble 示例展示了生成的堆栈跟踪。

class Cls {
    private Cls() {
    }
}

public class ClassTrouble {
    public static void main(String[] args) {
        Class<?> c = null;
        try {
            c = Class.forName("Cls");
            c.newInstance(); // InstantiationException

            // production code should handle these exceptions more gracefully
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.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 关键字很像,后者也会因同样原因失败。反射中典型的解决方法是使用 java.lang.reflect.AccessibleObject 类,它提供了抑制访问控制检查的能力;但是,此处无法使用它,因为 java.lang.Class 没有继承 AccessibleObject。唯一的解决方案是修改代码使用 Constructor.newInstance(),它继承了 AccessibleObject

备注

总结,推荐使用 Constructor.newInstance(),原因见 Members 课程的 Creating New Class Instances 章节。

使用 Constructor.newInstance() 的潜在问题示例见 Members 课程的 Constructor Troubleshooting 章节。

Lesson: Members

反射定义了一个接口 java.lang.reflect.Member,它有三个实现类 java.lang.reflect.Fieldjava.lang.reflect.Method,java.lang.reflect.Constructor。这些对象都将在本节讨论。课程描述了每种成员的 API,包括获取声明和类型信息,成员独有操作(如,设置属性值或调用方法)以及常见错误。每个概念都会有示例代码和相应输出,这些样例接近反射的实际用法。

备注

根据 The Java Language Specification, Java SE 7 Edition,类 成员 是类体继承的组件,包括属性,方法,内部类,接口和枚举类型。由于构造器无法继承,它们不是成员。这不同于 java.lang.reflect.Member 类的实现。

Fields

属性包含类型和值。对于给定对象,java.lang.reflect.Field 类提供了访问其属性的类型信息,设置和获取属性值的方法。

Methods

方法包含返回值,参数,可能会抛出异常。java.lang.reflect.Method 类提供了获得函数参数和返回值信息的方法。它也可用于调用给定对象的方法。

Constructors

构造器相关的反射 API 定义在 java.lang.reflect.Constructor,它们和函数相似,但有两点主要不同:首先,构造器没有返回值;其次,调用构造器会创建给定类的新对象实例。

Fields

属性 是一个带有关联值的类,接口或枚举。java.lang.reflect.Field 类中的方法可以获取属性信息,如名称,类型,修饰符和注解。(Classes 课程的 Examining Class Modifiers and Types 章节介绍了获取注解的方法)有些方法还能动态访问和修改属性的值。

例如要编写类浏览器,找出属性属于哪个类可能非常有用。可以通过调用 Class.getFields() 确定类的属性,它的返回值是 Field 数组,包含了可访问的所有公有属性。

可访问的公有属性来自以下对象:

  • 本类
  • 父类
  • 父接口
  • 父接口的父接口

属性可以是类(实例)属性,如 java.io.Reader.lock,静态属性,如 java.lang.Integer.MAX_VALUE,或者枚举常量,如 java.lang.Thread.State.WAITING

Obtaining Field Types

属性要么是基本类型要么是引用类型。有 8 种基本类型:boolean, byte, short, int, long, char, floatdouble。引用类型是 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.printf("Type: %s%n", f.getType());
            System.out.printf("GenericType: %s%n", f.getGenericType());

            // production code should handle these exceptions more gracefully
        } catch (ClassNotFoundException | NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}

下面是获取三个公共属性的样例输出(bname 和参数化类型 list)。

$ java FieldSpy b
Type: class [[Z
GenericType: class [[Z

$ java FieldSpy name
Type: class java.lang.String
GenericType: class java.lang.String

$ java FieldSpy list
Type: interface java.util.List
GenericType: java.util.List<java.lang.Integer>

$ java FieldSpy val
Type: class java.lang.Object
GenericType: T

属性 b 的类型是 boolean 二维数组。它的类型名语法由 Class.getName() 定义。

属性 val 的类型为 java.lang.Object,因为泛型是通过类型 擦除 实现的,编译时会去除所有泛化类型。因此,T 被类型变量的上界取代,本例中,它是 java.lang.Object

如果存在的泛化类型,Field.getGenericType() 会查询类文件中的签名属性。如果属性不存在,则返回 Field.getType(),它在泛型引入后没有任何改变。反射中的其它形如 getGenericFoo 的方法用于获取 Foo 的值,与此类似。

Retrieving and Parsing Field Modifiers

属性声明上可以包含如下修饰符:

  • 访问控制符:publicprotectedprivate
  • 控制运行时行为的属性特定修饰符:transientvolatile
  • 单例修饰符:static
  • 禁止修改符:final
  • 注解

方法 Field.getModifiers() 返回代表属性声明上修饰符集合的整数。整数位与修饰符的对应关系定义在 java.lang.reflect.Modifier

示例 FieldModifierSpy 展示了通过修饰符搜索属性。Field.isSynthetic()Field.isEnumConstant() 能分别判断属性是否是运行时生成的或是一个枚举常量。

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

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[] fds = c.getDeclaredFields();
            System.out.printf("Fields in Class '%s' containing modifiers: %s%n",
                    c.getName(),
                    Modifier.toString(searchMods));

            boolean found = false;
            for (Field f : fds) {
                int foundMods = f.getModifiers();
                // Require all of the requested modifiers to be present
                if ((foundMods & searchMods) == searchMods) {
                    System.out.printf("%-8s [ synthetic=%-5b enum_constant=%-5b ]%n",
                            f.getName(),
                            f.isSynthetic(),
                            f.isEnumConstant());
                    found = true;
                }
            }

            if (!found) {
                System.out.printf("No matching fields%n");
            }

            // production code should handle this exception more gracefully
        } catch (ClassNotFoundException e) {
            e.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 volatile
Fields in Class 'FieldModifierSpy' containing modifiers:  volatile
share    [ synthetic=false enum_constant=false ]

$ java Spy public
Fields in Class 'Spy' containing modifiers:  public
BLACK    [ synthetic=false enum_constant=true  ]
WHITE    [ synthetic=false enum_constant=true  ]

$ java FieldModifierSpy\$Inner final
Fields in Class 'FieldModifierSpy$Inner' containing modifiers:  final
this$0   [ synthetic=true  enum_constant=false ]

$ java Spy private static final
Fields in Class 'Spy' containing modifiers:  private static final
$VALUES  [ synthetic=true  enum_constant=false ]

注意输出中的部分属性没有在源码中声明,这是因为编译器会生成一些运行时需要的 合成 属性。示例中通过调用 Field.isSynthetic() 确定属性是否是合成属性。合成属性集依赖编译器,其名称在不同编译器和版本实现下可能不同;但常用属性包括内部类(如非静态成员内部类)的 this.$0 指向最外层封闭类,枚举中的 $VALUES 用于实现隐式定义的静态方法 values()Class.getDeclaredFields() 返回的数组中包含合成属性,但 Class.getField() 不包含,因为合成属性不都是 public 的。

由于 Field 实现了 java.lang.reflect.AnnotatedElement 接口,所以能得到任何 java.lang.annotation.RetentionPolicy.RUNTIME 类型的运行时注解。示例见 Examining Class Modifiers and Types

Getting and Setting Field Values

对于给定类实例,可以使用反射设置其属性值。通常只有无法通过常规方法设置时才使用它,因为这种访问通常违背类的设计意图,应最大程度的谨慎使用。

Book 类说明了如何为 long,array 和 enum 类型属性设值。获取和设置其他基本类型值的方法见 Field

import java.lang.reflect.Field;
import java.util.Arrays;

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();
            System.out.printf(fmt, "before", "chapters", book.chapters);
            Field chap = c.getDeclaredField("chapters");
            chap.setLong(book, 12);
            System.out.printf(fmt, "after", "chapters", chap.getLong(book));

            System.out.printf(fmt, "before", "characters", Arrays.asList(book.characters));
            Field chars = c.getDeclaredField("characters");
            String[] newChars = {"Queen", "King"};
            chars.set(book, newChars);
            System.out.printf(fmt, "after", "characters", Arrays.asList((String[]) chars.get(book)));

            System.out.printf(fmt, "before", "twin", book.twin);
            Field t = c.getDeclaredField("twin");
            t.set(book, Tweedle.DUM);
            System.out.printf(fmt, "after", "twin", t.get(book));

            // production code should handle these exceptions more gracefully
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.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*() 的等价代码则不会。

Troubleshooting

IllegalArgumentException due to Inconvertible Types

下面解释了一些常见错误的原因和解决方法。

FieldTrouble 示例将产生一个 IllegalArgumentExceptionField.setInt() 被调用来为引用类型属性 Integer 设置基本类型值。在等价的非反射代码中,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 exception more gracefully
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

$ java FieldTrouble
java.lang.IllegalArgumentException: Can not
    set java.lang.Integer field FieldTrouble.val to (int)42
        at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl
            .throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:167)
        at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl
            .throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:191)
        at java.base/jdk.internal.reflect.UnsafeObjectFieldAccessorImpl
            .setInt(UnsafeObjectFieldAccessorImpl.java:114)
        at java.base/java.lang.reflect.Field.setInt(Field.java:978)
        at FieldTrouble.main(FieldTrouble.java:12)

要消除这个异常,问题行应被下面的 Field.set(Object obj, Object value) 调用取代:

f.set(ft, new Integer(42));

备注

使用反射获取和设置属性时,编译器没有机会自动拆装箱。它仅能转换 Class.isAssignableFrom() 规定的相关类型。异常是预期行为,因为下行代码中 isAssignableFrom() 会返回 false,该测试可用于编程式检测特定转换能否完成:

Integer.class.isAssignableFrom(int.class) == false

类似地,基本类型到引用类型的自动转换在反射中也无法完成。

int.class.isAssignableFrom(Integer.class) == false

NoSuchFieldException for Non-Public Fields

敏锐的读者会注意到如果使用前面的 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.getDeclaredFields()

IllegalAccessException when Modifying Final Fields

IllegalAccessException 将被抛出,如果用户试图获取或设置 private 或其他不可访问属性,又或是设置 final 属性(无论它的访问修饰符是什么)。

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 {
            Class<?> c = ft.getClass();
            Field f = c.getDeclaredField("b");
//            f.setAccessible(true); // solution
            f.setBoolean(ft, Boolean.FALSE); // IllegalAccessException
            System.out.println("f.getBoolean(ft.b) = " + f.getBoolean(ft));

            // production code should handle this exception more gracefully
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

$ java FieldTroubleToo
java.lang.IllegalAccessException: Can not 
    set final boolean field FieldTroubleToo.b to (boolean)false
        at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl
            .throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:76)
        at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl
            .throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:84)
        at java.base/jdk.internal.reflect.UnsafeQualifiedBooleanFieldAccessorImpl
            .setBoolean(UnsafeQualifiedBooleanFieldAccessorImpl.java:96)
        at java.base/java.lang.reflect.Field.setBoolean(Field.java:830)
        at FieldTroubleToo.main(FieldTroubleToo.java:13)

备注

存在一个访问限制,阻止 final 属性在类初始化后被设值。但是 Field 继承了 AccessibleObject,所以具备抑制访问检查的能力。

如果 AccessibleObject.setAccessible() 成功,那么后续对该属性值的操作都不会因为访问控制失败。这可能会带来非预期效果;比如,有时应用的其它部分会继续使用原始值,即便它的值已经被改变。

只有安全上下文允许,AccessibleObject.setAccessible() 才会成功。

Methods

方法 是可被调用的可执行代码,它是可继承的。在非反射代码中,像重载,重写和隐藏这样的行为由编译器强制保证。相反,反射代码能将选定方法限制在特定类内,无需考虑父类。虽然继承方法会被一并访问,但提供了获取它声明类的 API;没有反射,将不能编程式发现这些信息,这也是许多微妙问题的根源。

Obtaining Method Type Information

方法声明包含名称,修饰符,参数,返回值,以及可抛异常列表。java.lang.reflect.Method 类提供了获取这些信息的方法。

MethodSpy 示例阐述了如何枚举特定类的所有声明方法,以及特定方法名所有方法的返回值,参数和异常类型。

import java.lang.reflect.Method;
import java.lang.reflect.Type;

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;
                }

                System.out.printf("%s%n", m.toGenericString());

                System.out.printf(fmt, "ReturnType", m.getReturnType());
                System.out.printf(fmt, "GenericReturnType", m.getGenericReturnType());

                Class<?>[] pType = m.getParameterTypes();
                Type[] gpType = m.getGenericParameterTypes();
                for (int i = 0; i < pType.length; i++) {
                    System.out.printf(fmt, "ParameterType", pType[i]);
                    System.out.printf(fmt, "GenericParameterType", gpType[i]);
                }

                Class<?>[] xType = m.getExceptionTypes();
                Type[] gxType = m.getGenericExceptionTypes();
                for (int i = 0; i < xType.length; i++) {
                    System.out.printf(fmt, "ExceptionType", xType[i]);
                    System.out.printf(fmt, "GenericExceptionType", gxType[i]);
                }
            }

            // production code should handle this exception more gracefully
        } catch (ClassNotFoundException e) {
            e.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)
    throws NoSuchMethodException, SecurityException

首先注意返回值和参数是泛化类型。Method.getGenericReturnType() 会查询类文件中的签名属性,如果存在的话。如果属性不存在,则退而返回 Method.getReturnType(),该方法在泛型引入后没有改变。其它用于在反射中获取 Foo,名似 getGenericFoo() 的方法与其有相似实现。

其次,注意最后(也是唯一)一个参数,parameterType,它是可变数量的 java.lang.Class,代表 java.lang.Class 类型的一维数组。可以通过调用 Method.isVarArgs() 区分显式声明的数组类型。Method.get*Type() 的返回值格式由 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

输出的返回值类型是 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.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;
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;

Class.getDeclaredMethods() 会返回所有重载方法。由于 format() 有两个重载(一个有 Locale,另一个没有),它们都被程序输出。

备注

尽管 Method.getGenericExceptionTypes() 能够获取方法的泛化异常类型,但这很少使用,因为泛化异常类型无法捕获。

Obtaining Names of Method Parameters

通过 java.lang.reflect.Executable.getParameters,你可以获得任何方法和构造器的形式参数名(MethodConstructor 类都继承了 Executable 类。)然而,.class 文件默认不存储形式参数名。这是因为许多生成和消费类文件的工具不想获得包含参数名的占据更大静态和动态空间的 .class 文件。如果是那样,这些工具就得处理大文件,JVM 也会占用更大内存。其次,一些参数名,比如 secretpassword,可能暴露安全敏感的方法信息。

想要在特定 .class 文件中存储形式参数名,进而能让反射 API 获取它们,你需要在编译源文件时,为 javac 加上 -parameters 选项。

MethodParameterSpy 示例演示了如何获取给定类上所有构造器和方法的形式参数。示例也打印了每个参数的其他信息。

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class MethodParameterSpy {
    private static final String fmt = "%24s: %s%n";

    // for the morbidly curious
    <E extends RuntimeException> void genericThrow() throws E {
    }

    public static void printClassConstructors(Class c) {
        Constructor[] allConsts = c.getConstructors();
        System.out.printf(fmt, "Number of constructors", allConsts.length);
        for (Constructor currConst : allConsts) {
            printConstructor(currConst);
        }
        Constructor[] allDeclConsts = c.getDeclaredConstructors();
        System.out.printf(fmt, "Number of declared constructors", allDeclConsts.length);
        for (Constructor currDeclConst : allDeclConsts) {
            printConstructor(currDeclConst);
        }
    }

    private static void printConstructor(Constructor c) {
        System.out.printf("%s%n", c.toGenericString());
        Parameter[] params = c.getParameters();
        System.out.printf(fmt, "Number of parameters", params.length);
        for (Parameter p : params) {
            printParameter(p);
        }
        System.out.println();
    }

    private static void printParameter(Parameter p) {
        System.out.printf(fmt, "Parameter class", p.getType());
        System.out.printf(fmt, "Parameter name", p.getName());
        System.out.printf(fmt, "Modifiers", p.getModifiers());
        System.out.printf(fmt, "Is implicit?", p.isImplicit());
        System.out.printf(fmt, "Is name present?", p.isNamePresent());
        System.out.printf(fmt, "Is synthetic?", p.isSynthetic());
    }

    public static void main(String[] args) {
        try {
            printClassConstructors(Class.forName(args[0]));
            printClassMethods(Class.forName(args[0]));
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    private static void printClassMethods(Class<?> c) {
        Method[] allMethods = c.getDeclaredMethods();
        System.out.printf(fmt, "Number of methods", allMethods.length);
        for (Method m : allMethods) {
            printMethod(m);
        }
    }

    private static void printMethod(Method m) {
        System.out.printf("%s%n", m.toGenericString());
        System.out.printf(fmt, "Return type", m.getReturnType());
        System.out.printf(fmt, "Generic return type", m.getGenericReturnType());

        for (Parameter p : m.getParameters()) {
            printParameter(p);
        }
        System.out.println();
    }
}

下行命令使用 ExampleMethods 类演示,记得编译时加上 -parameters 选项:

import java.util.Collection;
import java.util.List;

public class ExampleMethods<T> {
    public boolean simpleMethod(String stringParam, int intParam) {
        System.out.println("String: " + stringParam + ", integer: " + intParam);
        return true;
    }

    public int varArgsMethods(String... manyStrings) {
        return manyStrings.length;
    }

    public boolean methodWithList(List<String> listParam) {
        return listParam.isEmpty();
    }

    public <T> void genericMethod(T[] a, Collection<T> c) {
        System.out.println("Length of array: " + a.length);
        System.out.println("Size of collection: " + c.size());
    }
}
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 类的如下方法:

  • getType:返回一个 Class,表示参数的声明类型。
  • getName:返回参数名称。如果存在,则返回 .class 文件提供的名称。否则,自动生成 argsN 形式的名字,N 代表参数声明顺序索引。

例如,如果你编译 ExampleMethods 时没有加 -parameters 参数,则 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:返回一个整数,代表形式参数持有的多个属性。它的值是下表值之和,如果该修饰符用在了形参上的话。
值(十进制)值(十六进制)描述
160x0010形参是 final
40960x1000形参是生成的。替代方法是调用 isSynthetic
327680x8000形参是源码中隐式声明的。替代方法是调用 isImplicit
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 构造器包含一个封闭自己的参数类型 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 编译器生成的结构如果不和源码中显式或隐式声明的一致,则标记为 synthetic,除非它们是类初始化方法。合成结构是编译器产生的,它们可能有许多不同实现。考虑下面 MethodParameterExamples 的摘录:

public class MethodParameterExamples {
    enum Colors {
        RED, WHITE
    }
}

当编译器遇到 enum 结构时,它会创建几个方法,这些方法兼容 .class 文件结构,并且提供 enum 结构预期的功能。例如,Java 编译器为Colors 创建的 .class 文件,内容就像下面一样:

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);
    }
}

编译器为枚举创建了三个方法(包括构造器):Colors(String name, int ordinal), Colors[] values(), 和 Colors valueOf(String name)valuesvalueOf 是隐式声明的,因此它们的形参名也是隐式声明的。

Colors(String name, int ordinal) 是隐式声明的默认构造器。但是,它的形参(nameordinal并非 隐式声明。由于这些参数既不是显式也不是隐式声明的,所以它们是合成的。(enum 默认构造器的形参并非隐式声明,因为不同编译器对构造器格式没有共识,其他编译器可能指定不同的形参。当编译器编译含 enum 常量的表达式时,它们只依赖枚举结构隐式声明的的公有静态字段,而不依赖它们的构造器或者怎样初始化这些常量。)

所以,MethodParameterExample 打印的枚举 Colors 信息如下:

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

阅读 Java Language Specification 了解更多隐式声明结构信息,包括被反射 API 标记为隐式的参数。

Retrieving and Parsing Method Modifiers

方法声明上可以包含如下修饰符:

  • 访问控制符:public, protected, 和 private
  • 单例限制符:static
  • 值禁止修改符:final
  • 强制重写符:abstract
  • 重入禁止符:synchronized
  • 其他语言实现指示符:native
  • 严格浮点型行为强制符:strictfp
  • 注解

MethodModifierSpy 示例实现了列举给定名称的方法修饰符。它也能显示方法是否是合成的(编译器生成),可变参数,或桥接方法(编译器生成以支持泛型接口)。

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

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;
                }
                System.out.printf("%s%n", m.toGenericString());
                System.out.printf("  Modifiers:  %s%n", Modifier.toString(m.getModifiers()));
                System.out.printf("  [ synthetic=%-5b var_args=%-5b bridge=%-5b ]%n",
                        m.isSynthetic(),
                        m.isVarArgs(),
                        m.isBridge());
                inc();
            }
            System.out.printf("%d matching overload%s found%n",
                    cnt(), (cnt() == 1 ? "" : "s"));

            // production code should handle this exception more gracefully
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

一些调用输出如下。

$ java MethodModifierSpy java.lang.Object wait
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 void java.lang.Object.wait() 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 strictfp 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 static synchronized void MethodModifierSpy.inc()
  Modifiers:  private static 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<?>...)
        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.Object)
  Modifiers:  public volatile
  [ synthetic=true  var_args=false bridge=true  ]
public int java.lang.String.compareTo(java.lang.String)
  Modifiers:  public
  [ synthetic=false var_args=false bridge=false ]
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);

第二个是 synthetic 或者是编译器生成的 bridge 方法。出现这个方法的原因是 String 实现了参数化接口 Comparable。在类型擦除过程中,继承方法 Comparable.compareTo() 的参数类型从 java.lang.Object 变成了 java.lang.String。由于擦除后 ComparableString 中的 compareTo 参数类型不再匹配,重写无法发生。在所有其它情况下,这会导致编译时错误,因为接口没有被实现。附加的桥接方法避免了这个问题。

Method 实现了 java.lang.reflect.AnnotatedElement。因此任何运行时注解 java.lang.annotation.RetentionPolicy.RUNTIME 都能被获取。获取注解的示例见 Examining Class Modifiers and Types

Invoking Methods

泛型提供了调用类上方法的能力。通常,只有在非反射代码中无法将类实例强转为期望类型时才有必要使用。使用 java.lang.reflect.Method.invoke() 调用方法。它的第一个参数是待调用方法对象的实例。(如果是 static 方法,则第一个参数应该是 null。)后续是待调用方法的参数。如果底层方法抛出异常,它将被 java.lang.reflect.InvocationTargetException 包裹。方法的原始异常可以通过异常链机制的 InvocationTargetException.getCause() 方法获得。

Finding and Invoking a Method with a Specific Declaration

考虑使用反射调用给定类上私有测试方法的测试套件。示例 Deet 会搜索类上以字符串 "test" 开头的 public 方法,它的返回值类型是 boolean,唯一参数类型是 Locale。接着它会调用每个匹配方法。

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Locale;

public class Deet {
    private boolean testDeet(Locale l) {
        // getISO3Language() may throw a MissingResourceException
        System.out.printf("Locale = %s, ISO Languages Code = %s%n",
                l.getDisplayName(), l.getISO3Language());
        return true;
    }

    private int testFoo(Locale locale) {
        return 0;
    }

    private boolean testBar() {
        return true;
    }

    public static void main(String[] args) {
        if (args.length != 4) {
            System.err.println("Usage: java Deet <classname> <language> <country> <variant>");
            return;
        }

        try {
            Class<?> c = Class.forName(args[0]);
            Object t = c.getDeclaredConstructor().newInstance();

            Method[] allMethods = c.getDeclaredMethods();
            for (Method m : allMethods) {
                String name = m.getName();
                if (!name.startsWith("test") ||
                        m.getGenericReturnType() != boolean.class) {
                    continue;
                }
                Type[] pType = m.getGenericParameterTypes();
                if (pType.length != 1 ||
                        Locale.class.isAssignableFrom(pType[0].getClass())) {
                    continue;
                }
                try {
                    System.out.printf("invoking %s()%n", name);
                    m.setAccessible(true);
                    Object o = m.invoke(t, new Locale(args[1], args[2], args[3]));
                    System.out.printf("%s() returned %b%n", name, o);
                    // Handle any exceptions thrown by method to be invoked.
                } catch (InvocationTargetException e) {
                    Throwable cause = e.getCause();
                    System.err.printf("invocation of %s failed: %s%n",
                            name, cause.getMessage());
                }

            }
            // production code should handle these exception more gracefully
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Deet 调用了 getDeclaredMethods(),它会返回所有类中显式声明的方法。其次,Class.isAssignableFrom() 被用来确定方法参数是否与预期调用兼容。技术上,代码可以判断下面的声明是否为 true,因为 Localefinal 的。

Locale.class == pType[0].getClass()

Class.isAssignableFrom() 更通用。

$ java Deet Deet ja JP JP
invoking testDeet()
Locale = Japanese (Japan,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() 的参数非法时,它会抛出非受检异常 java.util.MissingResourceException。在反射中,处理受检与非受检异常没有区别。它们都被 InvocationTargetException 包裹。

Invoking Methods with a Variable Number of Arguments

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.printf("invoking %s.main()%n", c.getName());
            main.invoke(null, (Object) mainArgs);

            // production code should handle these exception more gracefully
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

首先,为了找到 main(),代码搜索了类中名为 main,含有唯一 String 数组类型参数的方法。由于 main()static 的,Method.invoke() 的第一个参数是 null。第二个参数是需要传递的参数数组。

$ java InvokeMain Deet Deet ja JP JP
invoking Deet.main()
invoking testDeet()
Locale = Japanese (Japan,JP), ISO Language Code = jpn
testDeet() returned true

Troubleshooting

本节包含开发者在使用反射定位,调用和获取方法信息时可能遇到的常见问题示例。

NoSuchMethodException Due to Type Erasure

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 cArgs = Class.forName(args[1]);
            Class<?> c = (new MethodTrouble<Integer>()).getClass();
            Method m = c.getMethod(mName, cArgs);
            System.out.printf("Found:%n  %s%n", m.toGenericString());

            // production code should handle these exceptions more gracefully
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }
}

$ java MethodTrouble lookup java.lang.Integer
java.lang.NoSuchMethodException: MethodTrouble.lookup(java.lang.Integer)
    at java.base/java.lang.Class.getMethod(Class.java:2201)
    at MethodTrouble.main(MethodTrouble.java:15)

$ java MethodTrouble lookup java.lang.Object
Found:
  public void MethodTrouble.lookup(T)

当方法含泛化类型参数时,编译器会将泛化类型替换为其上界,本例中,T 的上界是 Object。因此,代码找不到 lookup(Integer),尽管 MethodTrouble 实例是像下面这样创建的:

Class<?> c = (new MethodTrouble<Integer>()).getClass();

但能够搜索到 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.base/java.lang.Class.getMethod(Class.java:2201)
    at MethodTrouble.main(MethodTrouble.java:22)

上例中,find() 没有泛化类型参数,所以 getMethod() 使用的类型参数可以精确匹配签名。

备注

搜索带有泛化类型参数的方法时,应总是使用类型参数的上界。

IllegalAccessException when Invoking a Method

尝试调用 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 exception more gracefully
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

抛出异常的堆栈跟踪如下:

$ java MethodTroubleAgain
java.lang.IllegalAccessException: class MethodTroubleAgain
    cannot access a member of class AnotherClass with modifiers "private"
        at java.base/jdk.internal.reflect
            .Reflection.newIllegalAccessException(Reflection.java:385)
        at java.base/java.lang.reflect
            .AccessibleObject.checkAccess(AccessibleObject.java:693)
        at java.base/java.lang.reflect.Method.invoke(Method.java:556)
        at MethodTroubleAgain.main(MethodTroubleAgain.java:24)

备注

访问限制阻止反射调用正常情况下直接调用无法访问的方法。(这包括但不限于它类私有方法和私有类公有方法。)然而,Method 继承了 AccessibleObjectAccessibleObject.setAccessible() 提供了抑制检查的能力。如果成功抑制,后续对该方法对象的调用将不会因访问控制失败。

IllegalArgumentException from Method.invoke()

Method.invoke() 被改进为可变数量参数方法。这带来了极大便利,但也会导致意想不到的行为。MethodTroubleToo 示例展示了多种 Method.invoke() 可能导致异常的方式。

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class MethodTroubleToo {
    public void ping() {
        System.out.println("PONG!");
    }

    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 (except 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.println("Test not found");
            }

            // production code should handle these exceptions more gracefully
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

$ java MethodTroubleToo 0
PONG!

由于 Method.invoke() 除了第一个参数都是可选的,如果被调方法没有参数时,该方法的后续参数可以省略。

$ java MethodTroubleToo 1
PONG!

case 1 调用会产生编译期警告,因为 null 具有二义性。

$ javac MethodTroubleToo.java
MethodTroubleToo.java:19: 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 代表空数组还是第一个参数元素。

$ java MethodTroubleToo 2
Exception in thread "main" java.lang.IllegalArgumentException:
    wrong number of arguments
        at java.base/jdk.internal.reflect
            .NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect
            .NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
        at java.base/jdk.internal.reflect
            .DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:564)
        at MethodTroubleToo.main(MethodTroubleToo.java:23)

尽管参数是 null,调用还是失败了,因为它的类型 Object,而 ping() 不需要参数。

$ java MethodTroubleToo 3
PONG!

因为 new Object[0] 创建了一个空数组,对于可变参数方法,这等同于没有传递任何可选参数。

$ java MethodTroubleToo 4
Exception in thread "main" java.lang.IllegalArgumentException: 
    wrong number of arguments
        at java.base/jdk.internal.reflect
            .NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect
            .NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
        at java.base/jdk.internal.reflect
            .DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:564)
        at MethodTroubleToo.main(MethodTroubleToo.java:30)

与上例不同,本例将空数组存储在 Object 中,那么它就会被看作 Object。此处的失败原因与 case 2 相同,ping() 不需要任何参数。

备注

当方法被声明成 foo(Object... o) 形式时,编译器会将所有传给它的参数放进 Object[] 中,就像被声明为 foo(Object[] o) 一样。理解这点可能有助于避免上面阐释的类型问题。

InvocationTargetException when Invoked Method Faills

当调用方法对象产生异常时,InvocationTargetException 将会包裹它。(无论异常是否受检。)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 e) {
            Throwable cause = e.getCause();
            System.err.printf("drinkMe() failed: %s%n", cause.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

$ java MethodTroubleReturns
drinkMe() failed: I can't drink a negative amount of liquid

备注

如果抛出 InvocationTargetException,则方法已经被调用。问题症状如同直接调用方法,随后抛出 getCause() 得到的异常。该异常表明问题不在反射包或它的使用。

Constructors

构造器被用来创建类对象实例。通常,它会执行方法调用或域访问前的类初始化操作。构造器不会被继承。

类似于方法,反射提供了接口,发现和获取类构造器,获得声明信息,如修饰符、参数、注解,以及异常。可以使用特定构造器创建类实例。和构造器相关的核心类有 Classjava.lang.reflect.Constructor

Finding Constructors

构造器声明包含名称、修饰符、参数,以及可抛异常列表。java.lang.reflect.Constructor 类提供了获取这些信息的方法。

示例 ConstructorSift 阐述了如何搜索类上声明的包含给定类型参数的构造器。

import java.lang.reflect.Constructor;
import java.lang.reflect.Type;

public class ConstructorShit {
    public static void main(String[] args) {
        try {
            Class<?> cArgs = 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(cArgs)) {
                        System.out.println(ctor.toGenericString());

                        Type[] gpType = ctor.getGenericParameterTypes();
                        for (int j = 0; j < gpType.length; j++) {
                            char ch = pType[j].equals(cArgs) ? '*' : ' ';
                            System.out.printf("%7c%s[%d]: %s%n", ch, "GenericParameterType", j, gpType[j]);
                        }
                        break;
                    }
                }
            }

            // production code should handle this exception more gracefully
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Method.getGenericParameterTypes() 会查询类文件中的签名属性,如果存在的话。如果属性不存在,它会退而返回 Method.getParameterType(),该方法在泛型引入后没有改变。反射中其它名如 getGenericFoo() 用于获取 Foo 的方法,有类似实现。Method.get*Types() 返回值 toString() 的格式定义在 Class.getName()

下面是 java.util.Formatter 类包含 Locale 参数构造器的输出。

$ java ConstructorSift java.util.Formatter java.util.Locale
public java.util.Formatter(java.lang.String,java.nio.charset.Charset,java.util.Locale) throws java.io.IOException
       GenericParameterType[0]: class java.lang.String
       GenericParameterType[1]: class java.nio.charset.Charset
      *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.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.io.File,java.nio.charset.Charset,java.util.Locale) throws java.io.IOException
       GenericParameterType[0]: class java.io.File
       GenericParameterType[1]: class java.nio.charset.Charset
      *GenericParameterType[2]: 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
private java.util.Formatter(java.nio.charset.Charset,java.util.Locale,java.io.File) throws java.io.FileNotFoundException
       GenericParameterType[0]: class java.nio.charset.Charset
      *GenericParameterType[1]: class java.util.Locale
       GenericParameterType[2]: class java.io.File
private java.util.Formatter(java.util.Locale,java.lang.Appendable)
      *GenericParameterType[0]: class java.util.Locale
       GenericParameterType[1]: interface java.lang.Appendable
public java.util.Formatter(java.io.OutputStream,java.nio.charset.Charset,java.util.Locale)
       GenericParameterType[0]: class java.io.OutputStream
       GenericParameterType[1]: class java.nio.charset.Charset
      *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

下例展示了如何搜索 String 中包含 char[] 的构造器。

$ java ConstructorSift java.lang.String "[C"
java.lang.String(char[],int,int,java.lang.Void)
      *GenericParameterType[0]: class [C
       GenericParameterType[1]: int
       GenericParameterType[2]: int
       GenericParameterType[3]: class java.lang.Void
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(),而非 Class.getConstructors(),后者只会返回 public 构造器。

下例展示了搜索可变数量参数需要使用数组格式:

$ java ConstructorSift java.lang.ProcessBuilder "[Ljava.lang.String;"
public java.lang.ProcessBuilder(java.lang.String...)
      *GenericParameterType[0]: class [Ljava.lang.String;

下面是 ProcessBuilder 构造器源码的真实声明:

public ProcessBuilder(String... command)

参数被表示为 java.lang.String 的一维数组格式。调用 Constructor.isVarArgs() 可以区分显示声明的 java.lang.String 数组。

最后的示例展示了包含泛化类型参数的构造器:

$ 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 示例。

Retrieving and Parsing Constructor Modifiers

由于构造器在语言中的角色,可用于它的修饰符少于普通方法:

  • 访问控制符:publicprotectedprivate
  • 注解

ConstructorAccess 示例搜索了给定类上包含特定访问控制符的构造器。它也输出了构造器是否是 synthetic 以及是否包含可变数量参数。

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;

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) {
                    System.out.println(ctor.toGenericString());
                    System.out.printf("  [ synthetic=%-5b var_args=%-5b ]%n", ctor.isSynthetic(), ctor.isVarArgs());
                }
            }

            // production code should handle this exception more gracefully
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    private static int accessModifiers(int m) {
        return m & (Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED);
    }

    private static int modifierFromString(String s) {
        switch (s) {
            case "public":
                return Modifier.PUBLIC;
            case "protected":
                return Modifier.PROTECTED;
            case "private":
                return Modifier.PRIVATE;
            case "package-private":
                return 0;
            default:
                return -1;
        }
    }
}

由于不存在显式代表 package-privateModifier,所以只有检测出不存在其他三种访问控制符时,才是 package-private 的。

以下输出展示了 java.io.File 的私有构造器:

$ java ConstructorAccess java.io.File private
private java.io.File(java.lang.String,java.io.File)
  [ synthetic=false var_args=false ]
private java.io.File(java.lang.String,int)
  [ 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 ]

备注

从 JDK 11 开始,合成方法和构造器被 Nest-Based Access Control 取代。

由于内部类构造器引用了封闭类的私有构造器,编译器必须生成一个包级别构造器。参数类型 SyntheticConstructor$1 是任意的,它依赖编译器实现。依赖任何合成或非公有类成员的代码可能是不可移植的。

构造器实现了 java.lang.reflect.AnnotatedElement,它提供了获得 java.lang.annotation.RetentionPolicy.RUNTIME 运行时注解的方法。示例见 Examining Class Modifiers and Types 章节。

Creating New Class Instances

有两个创建类实例的反射方法:java.lang.reflect.Constructor.newInstance()Class.newInstance()。更推荐使用第一个,因为:

有时,开发者想获取对象创建后设置的内部状态。考虑获取 java.io.Console 使用的内部字符集的场景。(Console 字符集存储在私有变量中,并且无需与 Java 虚拟机默认字符集 java.nio.charset.Charset.defaultCharset() 一致。)ConsoleCharset 示例展示了如何获得它:

import java.io.Console;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.Charset;

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);
            System.out.printf("Console charset         :  %s%n", f.get(c));
            System.out.printf("Charset.defaultCharset():  %s%n", Charset.defaultCharset());

            // production code should handle these exceptions more gracefully
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}

备注

只有构造器无参并且已经可访问,Class.newInstance() 才会成功。否则就必须像上面那样使用 java.lang.reflect.Constructor.newInstance()

UNIX 系统输出如下:

$ java ConsoleCharset
Console charset          :  ISO-8859-1
Charset.defaultCharset() :  ISO-8859-1

Windows 系统输出如下:

C:\> java ConsoleCharset
Console charset          :  IBM437
Charset.defaultCharset() :  windows-1252

java.lang.reflect.Constructor.newInstance() 的另一个常见用例是调用含参构造器。RestoreAliases 示例找出了特定的单参数构造器,并且调用了它:

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Set;

class EmailAliases {
    private final Set<String> aliases;

    private EmailAliases(HashMap<String, String> h) {
        aliases = h.keySet();
    }

    public void printKeys() {
        System.out.println("Mail keys:");
        for (String k : aliases) {
            System.out.printf(" %s%n", k);
        }
    }
}

public class RestoreAliases {
    private static final HashMap<String, String> defaultAliases = new HashMap<>();

    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 (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

上例使用 Class.getDeclaredConstructor() 寻找包含 java.util.HashMap 类型单参数的构造器。注意传递 HashMap.class 是足够的,因为任何 get*Constructor() 方法需要的类仅用于类型解析。由于 type erasure,下面的表达式结果为 true

HashMap.class == defaultAliases.getClass()

示例随后通过 java.lang.reflect.Constructor.newInstance() 创建了一个类实例。

$ java RestoreAliases
Mail keys:
  Duke
  Fang

Troubleshooting

InstantiationException Due to Missing Zero-Argument Constructor

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.newInstance0(Class.java:340)
        at java.lang.Class.newInstance(Class.java:308)
        at ConstructorTrouble.main(ConstructorTrouble.java:7)

备注

发生 InstantiationException 异常的原因有很多。本例的问题在于,int 参数构造器阻止了编译器生成默认构造器(无参),而代码中又不存在显式声明的无参构造器。记住 Class.newInstance() 的行为非常像 new 关键字,任何 new 失败的地方,它也会失败。

Class.newInstance() Throws Unexpected Exception

ConstructorTroubleToo 示例展示了 Class.newInstance() 中一个不可解决的问题。即,它抛出所有构造器抛出的异常,无论该异常是否受检。

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 propagates 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:6)
        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 java.lang.Class.newInstance0(Class.java:355)
        at java.lang.Class.newInstance(Class.java:308)
        at ConstructorTroubleToo.main(ConstructorTroubleToo.java:15)

这种情形是反射独有的。正常情况下不可能编写出忽略受检异常的代码,因为它无法通过编译。要包裹任何构造器抛出的异常,可以使用 Constructor.newInstance()

$ java ConstructorTroubleToo
java.lang.reflect.InvocationTargetException
        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 ConstructorTroubleToo.main(ConstructorTroubleToo.java:17)
Caused by: java.lang.RuntimeException: exception in constructor
        at ConstructorTroubleToo.<init>(ConstructorTroubleToo.java:6)
        ... 5 more


Caught exception: java.lang.RuntimeException: exception in constructor

如果 InvocationTargetException 被抛出,则方法已经被调用。问题诊断应该和直接调用构造器抛出 InvocationTargetException.getCause() 返回的异常一样。此异常表示问题与反射包或使用反射无关。

备注

更推荐使用 Constructor.newInstance() 而非 Class.newInstance(),因为前者允许检测和处理构造器抛出的任意异常。

Problems Locating or Invoking the Corrent Constructor

ConstructorTroubleAgain 类阐释了多种会导致无法定位和调用预期构造器的问题代码。

import java.lang.reflect.InvocationTargetException;

public class ConstructorTroubleAgain {
    public ConstructorTroubleAgain() {
    }

    public ConstructorTroubleAgain(Integer i) {
    }

    public ConstructorTroubleAgain(Object o) {
        System.out.println("Constructor passed Object");
    }

    public ConstructorTroubleAgain(String s) {
        System.out.println("Constructor passed String");
    }

    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 e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.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:2706)
        at java.lang.Class.getConstructor(Class.java:1657)
        at ConstructorTroubleAgain.main(ConstructorTroubleAgain.java:26)

上面的异常可能由于开发者误解性地认为反射会自动拆装箱。装箱(将基本类型转为引用类型)只在编译时发生。反射时没有解决机会拆装箱,所以定位构造器时必须使用确切类型。

$ java ConstructorTroubleAgain Object
Constructor passed Object

上例传递给 newInstance() 的参数类型是更具体的 String,你可能期待接收 String 参数的构造器被调用。然而,太晚了!发现的已经是接收 Object 的构造器。newInstance() 不再尝试方法解析,它只简单在已有构造器上完成操作。

备注

Constructor.newInstance()new 最重要的不同在于,后者执行方法参数类型检测,自动装箱,以及方法解析。而这些在反射中都不会发生,必须显式选择。

IllegalAccessException When Attempting to Invoke an Inaccessible Constructor

尝试调用私有或其他不可访问构造器时可能抛出 IllegalAccessExceptionConstructorTroubleAccess 示例阐释了导致该异常的堆栈跟踪。

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

class Deny {
    private Deny() {
        System.out.println("Deny constructor");
    }
}

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 exception more gracefully
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.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:13)

备注

存在访问限制阻止反射调用正常情况下直接调用无法访问的构造器。(这包括但不限于,它类私有构造器和其他私有类的公有构造器。)但是,Constructor 继承了 AccessibleObject,后者提供了抑制检查的能力,方法是 AccessibleObject.setAccessible()

Lesson: Arrays and Enumerated Types

从 Java 虚拟机角度看,数组和枚举也仅是类,Class 上的许多方法同样适用于它们,反射为数组和枚举提供了一些独有 API。本章使用一系列代码示例描述如何区分它们和其他类,如何操作它们,同时介绍可能遇到的常见错误。

Arrays

数组包含组件类型和长度(长度不是类型的一部分)。数组可以被整体或逐组件操纵。反射提供了 java.lang.reflect.Array 类完成以下操作。

Enumerated Types

反射对待枚举非常像其他普通类型。Class.isEnum() 用于判断 Class 是否是枚举类型。Class.getEnumConstants() 用于获取定义于枚举内的常量。java.lang.reflect.Field.isEnumConstant() 表明属性是否是枚举类型。

Arrays

数组是引用类型对象,它包含固定数量的相同类型元素,数组的长度是不可变的。创建数组实例需要知道长度和元素类型。每个元素可以是基本类型(例如,byteintdouble),引用类型(如,StringObjectjava.nio.CharBuffer),也可以是数组。多维数组实际上就是包含数组元素的数组。

数组在 Java 虚拟机中实现,它仅有的方法都继承自 Object,长度不是数组类型的一部分,该属性可以通过 java.lang.reflect.Array.getLength() 获得。

java.lang.reflect.Array 提供了访问数组类型和数组元素类型,创建数组,获取和设置数组元素值的方法,它们都是 static 的。

Identifying Array Types

Class.isArray() 标识了类型是否是数组。获取数组 Class 对象的方法在前面的 Retrieving Class Objects 章节已经介绍。

ArrayFind 示例识别了特定类上的数组属性,并且报告了它们的元素类型。

import java.lang.reflect.Field;

public class ArrayFind {
    public static void main(String[] args) {
        boolean found = false;
        try {
            Class<?> cls = Class.forName(args[0]);
            Field[] fields = cls.getDeclaredFields();
            for (Field f : fields) {
                Class<?> c = f.getType();
                if (c.isArray()) {
                    found = true;
                    System.out.printf("%s%n"
                                    + "           Field: %s%n"
                                    + "            Type: %s%n"
                                    + " Component Types: %s%n",
                            f, f.getName(), c, c.getComponentType());
                }
            }
            if (!found) {
                System.out.println("No array fields");
            }

            // production code should handle this exception more gracefully
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Class.get*Type() 的返回值格式描述在 Class.getName() 里。类型名开始的 '[' 数量代码数组的维数。

基本类型 byte 数组的输出如下:

$java ArrayFind java.nio.ByteBuffer
final byte[] java.nio.ByteBuffer.hb
           Field: hb
            Type: class [B
 Component Types: 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;

Creating New Arrays

就像非反射代码,反射也提供了创建任何类型和大小数组的能力,方法是 java.lang.reflect.Array.newInstance()

考虑 ArrayCreator 示例,一个能够动态创建数组的基本解释器。要被解析的格式如下:

fully_qualified_class_name variable_name[] = 
    { val1, val2, val3, ... }

假定 fully_qualified_class_name 代表一个类,它有一个以 String 为参数的构造器。数组的大小由提供值的数量决定。下面的示例将会创建一个 fully_qualified_class_name 数组,并且使用给定实例 val1val2 等填充它的值。(本例假定读者熟悉 Class.getConstructor()java.lang.reflect.Constructor.newInstance()。有关反射 API 中 Constructor 的讨论见本教程的 Creating New Class Instances。)

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ArrayCreator {
    private static final String s = "java.math.BigInteger bi[] = { 123, 234, 345 }";
    private static final 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;
                System.out.printf("%s[] = %s%n", cName, Arrays.toString(oo));

                // production code should handle these exceptions more gracefully
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }
}

$ java ArrayCreator
java.math.BigInteger [] = [123, 234, 345]

上例展示了一个通过反射创建数组的案例,此时数组元素类型直到运行时才能确定。代码使用 Class.forName() 获得元素类型,随后调用特定构造器初始化每个数组元素,最后把它们设置到数组对象上。

Getting and Setting Arrays and Their Components

和非反射代码一样,反射也可以设置和获取整个数组或它的每个元素。java.lang.reflect.Field.set(Object obj, Object value) 方法用于设置整个数组。Field.get(Object) 用于获取整个数组。java.lang.reflect.Array 中的方法能够设置和获取每个元素。

java.lang.reflect.Array 提供了 setFoo()getFoo() 形式的方法用于设置和获取任何基本类型元素。例如,int 数组的元素可以通过 Array.setInt(Object array, int index, int value) 设置,通过 Array.getInt(Object array, int index) 获取。

这些方法支持数据类型拓宽。因此,Array.setShort() 可以被用来设置 int 型数组元素,因为 16 位的 short 可以拓宽为 32 位的 int 而不损失精度。相反,调用 Array.setLong() 设置 int 型数组将导致 IllegalArgumentException,因为 64 位的 long 不能收窄至 32 的 int 却不丢失信息。在类型上确实是这样,无论被传递的实际参数是否能准确表示目标类型。The Java Language Specification, Java SE 7 Edition 中的 Widening Primitive ConversionNarrowing Primitive Conversion 章节包含了拓宽和收窄转换的完整讨论。

引用类型数组(包括数组的数组)的元素可以通过 Array.set(Object array, int index, int value)Array.get(Object array, int index) 设置和获取。

Setting a Field of Type Array

GrowBufferedReader 示例阐释了如何替换数组类型属性的值。本例中,代码将 java.io.BufferedReader 的支持数组替换为更大的一个。(假定原始 BufferedReader 数组创建的代码不可修改,否则就可以简单地使用另一接收输入缓冲区大小的构造器 BufferedReader(java.io.Reader in, int size)。)

import java.io.BufferedReader;
import java.io.CharArrayReader;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Arrays;

public class GrowBufferedReader {
    private static final int srcBufSize = 10 * 1024;
    private static final char[] src = new char[srcBufSize];

    static {
        src[srcBufSize - 1] = 'x';
    }

    private static final 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[]) 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]) {
                System.out.printf("Using new backing array, size=%d%n", newVal.length);
            } else {
                System.out.printf("Using original backing array, size=%d%n", cbVal.length);
            }

            // production code should handle these exception more gracefully
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.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 包含许多简便的数组操作方法。

Accessing Elements of a Multidimensional Array

简单来说多维数组就是嵌套数组。二维数组是数组的数组。三维数组是二维数组的数组,以此类推。CreateMatrix 示例阐释了如何通过反射创建和初始化一个多维数组。

import java.lang.reflect.Array;

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++) {
                System.out.printf("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。)

Troubleshooting

IllegalArgumentException due to Inconvertible Types

下面的示例展示了数组操作上可能遇到的典型错误。

ArrayTroubleAgain 示例将产生一个 IllegalArgumentExceptionArray.setInt() 被调用,使用基本类型 int 设置引用类型 Integer 元素的值。在等价的非反射代码 ary[0] = 1 中,编译器会转换(或者叫装箱) 1 的值成为引用类型 new Integer(1),所以类型检查接受这样的声明。而使用反射时,类型检测只发生在运行时,没有机会完成数值装箱。

import java.lang.reflect.Array;

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 e) {
            System.out.println("Unable to box");
        } catch (ArrayIndexOutOfBoundsException e) {
            e.printStackTrace();
        }
    }
}

$ java ArrayTroubleAgain
Unable to box

要消除这个异常,问题行应该被以下 Array.set(Object array, int index, Object value) 调用取代:

Array.set(ary, 0, new Integer(1));

备注

使用反射设置和获取数组元素时,编译器没有机会完成自动拆装箱。它仅可转换那些 Class.isAssignableFrom() 返回 true 的类型。本例的失败是预期的,因为下行测试里的 isAssignalbeFrom() 会返回 false,这可以用来编程式判断特定转换能否完成。

Integer.class.isAssignableFrom(int.class) == false

类似地,在反射中也不能自动将引用类型转为基本类型。

int.class.isAssignableFrom(Integer.class) == false

ArrayIndexOutOfBoundsException for Empty Arrays

ArrayTrouble 示例阐释了访问零长数组时发生的错误:

import java.lang.reflect.Array;

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];
        System.out.printf("i.length = %d, j.length = %d, args.length = %d%n",
                i.length, j.length, args.length);
        Array.getInt(o, 0); // ArrayIndexOutOfBoundException
    }
}

$ 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 if Narrowing is Attempted

ArrayTroubleToo 示例尝试执行可能丢失数据的操作,最终失败了:

import java.lang.reflect.Array;

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, 0, 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 EditionWidening Primitive ConversionNarrowing Primitive Conversion 章节。

Enumerated Types

enum 是一个用来定义类型安全枚举值的语言结构,它在需要固定集合命名值时被使用。所有枚举隐式继承自 java.lang.Enum。枚举可以包含一到多个 enum 常量,它们是该枚举类型的唯一实例。枚举声明非常类似类声明,都包含属性,方法和构造器(有一些限制)等成员。

因为枚举是类,所以反射无需显式定义 java.lang.reflect.Enum。枚举独有的 API 有 Class.isEnum()Class.getEnumConstants()java.lang.reflect.Field.isEnumConstant()。大多数枚举有关的反射操作和其他类或成员相同。例如,枚举常量是枚举上 public static final 的属性。下面的章节介绍如何使用 Classjava.lang.reflect.Field 操作枚举类型。

详细的枚举介绍见 Enum Types

Examining Enums

反射提供了三个枚举特有 API:

Class.isEnum()

表明该类是否是枚举类型

Class.getEnumConstants()

以声明顺序返回枚举上定义的枚举常量列表

java.lang.reflect.Field.isEnumConstant()

表明属性是否是枚举类型元素

有时需要动态获取枚举常量列表,在非反射代码中,可以显式调用枚举的静态方法 values()。如果无法获得枚举类型实例,获得枚举常量列表的唯一方式是调用 Class.getEnumConstants(),因为无法实例化一个枚举类型。

给定全限定名,EnumConstants 示例展示了如何通过 Class.getEnumConstants() 获取枚举上有序的常量列表。

import java.util.Arrays;

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]);
            System.out.printf("Enum name:  %s%nEnum constants:  %s%n",
                    c.getName(), Arrays.asList(c.getEnumConstants()));
            if (c == Eon.class) {
                System.out.printf("  Eon.values():  %s%n",
                        Arrays.asList(Eon.values()));
            }

            // production code should handle these exceptions more gracefully
        } catch (ClassNotFoundException e) {
            e.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 获得,详见教程的 FieldsMethodsConstructors 部分。EnumSpy 示例阐述了如何使用上述 API 获取枚举声明上的其他信息。示例使用 Class.isEnum() 限定检测类集合。使用 java.lang.reflect.Field.isEnumConstant() 区分枚举常量和枚举声明中的其他属性(并非所有属性都是枚举常量)。

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

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()) {
                System.out.printf("%s is not an enum type%n", c);
            }
            System.out.printf("Class:  %s%n", c);

            Field[] flds = c.getDeclaredFields();
            List<Field> cst = new ArrayList<>(); // enum constants
            List<Field> mbr = new ArrayList<>(); // 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) {
                System.out.printf(fmt, "Constructor", ctor.toGenericString(), synthetic(ctor));
            }

            Method[] mths = c.getDeclaredMethods();
            for (Method m : mths) {
                System.out.printf(fmt, "Method", m.toGenericString(), synthetic(m));
            }

            // production code should handle these exception more gracefully
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }

    private static String synthetic(Member m) {
        return m.isSynthetic() ? "[ synthetic ]" : "";
    }

    private static void print(List<Field> lst, String s) {
        for (Field f : lst) {
            System.out.printf(fmt, s, f.toGenericString(), synthetic(f));
        }
    }
}

$ 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 java.util.concurrent.TimeUnit
                  java.util.concurrent.TimeUnit.NANOSECONDS
     Constant:  public static final java.util.concurrent.TimeUnit
                  java.util.concurrent.TimeUnit.MICROSECONDS
     Constant:  public static final java.util.concurrent.TimeUnit
                  java.util.concurrent.TimeUnit.MILLISECONDS
     Constant:  public static final java.util.concurrent.TimeUnit
                  java.util.concurrent.TimeUnit.SECONDS
     Constant:  public static final java.util.concurrent.TimeUnit
                  java.util.concurrent.TimeUnit.MINUTES
     Constant:  public static final java.util.concurrent.TimeUnit
                  java.util.concurrent.TimeUnit.HOURS
     Constant:  public static final java.util.concurrent.TimeUnit
                  java.util.concurrent.TimeUnit.DAYS
        Field:  static final long java.util.concurrent.TimeUnit.C0
        Field:  static final long java.util.concurrent.TimeUnit.C1
        Field:  static final long java.util.concurrent.TimeUnit.C2
        Field:  static final long java.util.concurrent.TimeUnit.C3
        Field:  static final long java.util.concurrent.TimeUnit.C4
        Field:  static final long java.util.concurrent.TimeUnit.C5
        Field:  static final long java.util.concurrent.TimeUnit.C6
        Field:  static final long java.util.concurrent.TimeUnit.MAX
        Field:  private static final java.util.concurrent.TimeUnit[] 
                  java.util.concurrent.TimeUnit. [ synthetic ]
  Constructor:  private java.util.concurrent.TimeUnit()
  Constructor:  java.util.concurrent.TimeUnit
                  (java.lang.String,int,java.util.concurrent.TimeUnit)
                  [ synthetic ]
       Method:  public static java.util.concurrent.TimeUnit
                  java.util.concurrent.TimeUnit.valueOf(java.lang.String)
       Method:  public static java.util.concurrent.TimeUnit[] 
                  java.util.concurrent.TimeUnit.values()
       Method:  public void java.util.concurrent.TimeUnit.sleep(long) 
                  throws java.lang.InterruptedException
       Method:  public long java.util.concurrent.TimeUnit.toNanos(long)
       Method:  public long java.util.concurrent.TimeUnit.convert
                  (long,java.util.concurrent.TimeUnit)
       Method:  abstract int java.util.concurrent.TimeUnit.excessNanos
                  (long,long)
       Method:  public void java.util.concurrent.TimeUnit.timedJoin
                  (java.lang.Thread,long) throws java.lang.InterruptedException
       Method:  public void java.util.concurrent.TimeUnit.timedWait
                  (java.lang.Object,long) throws java.lang.InterruptedException
       Method:  public long java.util.concurrent.TimeUnit.toDays(long)
       Method:  public long java.util.concurrent.TimeUnit.toHours(long)
       Method:  public long java.util.concurrent.TimeUnit.toMicros(long)
       Method:  public long java.util.concurrent.TimeUnit.toMillis(long)
       Method:  public long java.util.concurrent.TimeUnit.toMinutes(long)
       Method:  public long java.util.concurrent.TimeUnit.toSeconds(long)
       Method:  static long java.util.concurrent.TimeUnit.x(long,long,long)

Getting and Setting Fields with Enum Types

获取和设置存储枚举的属性和其他任何引用类型相同,都是使用 Field.set()Field.get()。更多属性访问信息,见前面的 Fields 章节。

考虑应用需要动态修改服务端应用的跟踪级别,而正常情况下不允许在运行时修改。假定无法获得服务端对象实例。SetTrace 示例展示了代码如何将表示枚举的 String 转译为枚举类型,之后获取储存枚举的属性,为其设置新值。

import java.lang.reflect.Field;

enum TraceLevel {OFF, LOW, MEDIUM, HIGH, DEBUG}

class MyServer {
    private final 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);
            System.out.printf("Original trace level:  %s%n", oldLevel);

            if (oldLevel != newLevel) {
                f.set(svr, newLevel);
                System.out.printf("  New trace level:  %s%n", f.get(svr));
            }

            // production code should handle these exceptions more gracefully
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

因为枚举常量是单例,==!= 运算符可以用来比较相同类型的枚举常量。

$ java SetTrace OFF
Original trace level:  OFF

$ java SetTrace DEBUG
Original trace level:  OFF
    New  trace level:  DEBUG

Troubleshooting

下面是使用枚举类型时经常遇到的问题。

IllegalArgumentException When Attempting to Instantiate an Enum Type

就像之前提及的,枚举类型的初始化是被禁止的。EnumTrouble 示例做了一个测试。

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

enum Charge {
    POSITIVE, NEGATIVE, NEUTRAL;

    Charge() {
        System.out.println("under construction");
    }
}

public class EnumTrouble {
    public static void main(String[] args) {
        Class<?> c = Charge.class;

        try {
            Constructor<?>[] ctors = c.getDeclaredConstructors();
            for (Constructor<?> ctor : ctors) {
                System.out.printf("Constructor: %s%n", ctor.toGenericString());
                ctor.setAccessible(true);
                ctor.newInstance();
            }

            // production code should handle these exception more gracefully
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.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:511)
        at EnumTrouble.main(EnumTrouble.java:21)

备注

显式初始化枚举类型将导致编译时错误,因为那将导致枚举常量不唯一,这种限制在反射中也同样存在。尝试实例化类型之前,应首先调用 Class.isEnum(),确定它是否是枚举类型。

IllegalArgumentException when Setting a Field with an Incompatible Enum Type

存储枚举的属性被不恰当的枚举类型设值。(实际上,任何 类型的属性都必须使用兼容属性设值。)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 exception more gracefully
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.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:17)

备注

严格来说,用类型为 Y 的值设置类型为 X 的属性,只有下面这种情形成立才能成功:

X.class.isAssignableFrom(Y.class) == true

代码可以这样修改以检测类型是否兼容:

if (f.getType().isAssignableFrom(E0.class)) {
    // compatible
} else {
    // expect IllegalArgumentException
}

The Reflection API: End of Trail

恭喜你已经学完了反射 API 课程,如果有任何评论和建议请前往 feedback page 告诉我们。

本文译自 Trail: The Reflection API,译者 LOGI。

TG 大佬群 QQ 大佬群

返回文章列表 文章二维码
本页链接的二维码
打赏二维码
添加新评论

Loading captcha...