JAVA官方文档2024——反射

本文基于官方文档翻译,并补充个人理解部分
全文超长,阅读时间可能超过2小时,建议分开阅读

文章目录

反射API(The Reflection API)

反射的用途

反射通常被那些需要检查或修改运行在JVM上的程序的运行时行为的程序所使用。这是一个相对高级的功能,只应被对语言基础有牢固掌握的开发者使用。考虑到这一点,反射是一个强大的技术,能够使应用程序执行原本不可能完成的操作。

  • 扩展特性

    应用程序可以通过使用其全限定名称创建扩展对象的实例,来利用外部、用户定义的类。

  • 类浏览器和可视化开发环境

    类浏览器需要能够枚举类的成员。可视化开发环境可以从反射中可用的类型信息中受益,帮助开发者编写正确的代码。

  • 调试器和测试工具

    调试器需要能够检查类上的私有成员。测试框架可以利用反射系统地调用类上定义的一组可发现的API,以确保测试套件中有高水平的代码覆盖率。

反射的缺点

反射功能强大,但不应随意使用。如果可能在不使用反射的情况下执行操作,则最好避免使用它。以下是在通过反射访问代码时应记住的几点关注事项。

  • 性能开销

    由于反射涉及动态解析的类型,某些Java虚拟机优化无法执行。因此,反射操作的性能比非反射操作慢,应避免在性能敏感的应用程序中频繁调用的代码段中使用。

  • 安全限制

    反射需要运行时权限,当在安全管理器下运行时可能不存在此权限。这是代码必须在受限安全环境下运行的重要考虑因素,比如在Applet中。

  • 内部细节的暴露

    由于反射允许代码执行在非反射代码中非法的操作,如访问private字段和方法,反射的使用可能导致意外的副作用,可能使代码失效并破坏可移植性。反射代码打破了抽象,因此可能随着平台升级而改变行为。

教程课程

本教程涵盖了使用反射访问和操作类、字段、方法和构造器的常见用途。每一课都包含代码示例、提示和疑难解答信息。

  • 本课展示了获取Class对象的各种方式,并使用它来检查类的属性,包括其声明和内容。

  • 成员

    本课描述了如何使用反射API查找类的字段、方法和构造器。提供了设置和获取字段值、调用方法以及使用特定构造器创建对象新实例的示例。

  • 数组和枚举类型

    本课介绍了两种特殊的类类型:数组,它们在运行时生成,以及enum类型,它们定义了独特的命名对象实例。示例代码展示了如何检索数组的组件类型,以及如何设置和获取具有数组或enum类型的字段。


注意:

本教程中的示例旨在用于实验反射API。因此,异常处理与生产代码中使用的不同。特别是,在生产代码中不建议向用户显示堆栈跟踪。


教程:类

每种类型要么是引用类型,要么是原始类型。类、枚举和数组(所有这些都继承自java.lang.Object,包括java.lang.class)以及接口都是引用类型。引用类型的例子包括java.lang.String、所有原始类型的的包装类(如java.lang.Double)、接口(java.io.Serializable),以及枚举(javax.swing.SortOrder)。原始类型有一组固定集合:booleanbyteshortintlongcharfloatdouble

对于每种类型的对象,Java虚拟机都会实例化一个不可变的java.lang.Class类的实例,该类提供了用于检查对象的运行时属性的方法,包括其成员和类型信息。Class还提供了创建新类和对象的能力。最重要的是,它是所有反射API的入口点。本课涵盖涉及类的最常用的反射操作:

  • 检索类对象:描述了获取Class对象的方法。
  • 检查类修饰符和类型:展示如何访问类声明信息。
  • 发现类成员:说明如何列出类中的构造器、字段、方法和嵌套类。
  • 疑难解答:描述了使用Class时常见的错误。

检索类对象

反射操作的起点是java.lang.Class。除了java.lang.reflect.ReflectPermission之外,java.lang.reflect包下的其他类都没有公共构造函数。要使用这些类,必须在Class上调用适当的方法。根据代码是否可以访问对象、类名、类型或现有的Class,有几种不同的方式来获取Class对象。

使用Object.getClass()

如果有一个对象的实例,那么获取其Class的最简单方法是调用Object.getClass()。当然,这仅适用于所有继承自Object的引用类型。以下是一些示例:

Class c = "foo".getClass();

返回StringClass对象

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

这是与虚拟机关联的唯一的控制台,这个控制台是由静态方法System.console()返回的。通过调用getClass()返回的值是对应于java.io.ConsoleClass对象。

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

A 是枚举E的一个实例;;因此getClass() 返回的Class对象对应于枚举类型E

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

由于数组是Objects(具体来说,它们继承自java.lang.Object),因此也可以在数组实例上调用getClass()方法。返回的Class对象对应于具有byte组件类型的数组。

import java.util.HashSet;
import java.util.Set;

Set<String> s = new HashSet<String>();
Class c = s.getClass();

在这个例子中,java.util.Set是一个接口,指向一个java.util.HashSet类型的对象。由getClass()方法返回的值是对应于java.util.HashSet类的Class对象。

使用.class语法

如果类型可用但没有实例,可以通过在类型名后加上.class来获取Class。这也是获取原始类型Class的最简单方式。

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

注意,语句boolean.getClass()会导致编译时错误,因为boolean是一个原始类型,不能被取消引用。.class语法返回对应于boolean类型的Class对象。

什么是【取消引用】?

原始变量和对象变量是 Java 中可以使用的两种类型的变量。 但是,只有对象变量可以是引用类型。 int 数据类型是原始数据类型而不是对象。

【访问引用所指向的值】就是所谓的取消引用。 不可能取消引用 int,因为它已经是一个值,而不是对其他任何东西的引用。

Class c = java.io.PrintStream.class;

变量c将会是对应于java.io.PrintStream类型的Class对象。

Class c = int[][][].class; // 返回[[[I

.class语法可用于检索对应于给定类型多维数组的Class

Class.forName()

如果已知类的全限定名,可以使用静态方法Class.forName()来获取对应的Class对象。但这种语法不能用于原始类型。

描述数组类名的语法可以用Class.getName()方法。这种语法适用于引用类型和原始类型的数组。

Class c = Class.forName("com.duke.MyLocaleServiceProvider");

这条语句将根据给定的全限定名创建一个类的Class对象。

Class cDoubleArray = Class.forName("[D");
Class cStringArray = Class.forName("[[Ljava.lang.String;");

变量cDoubleArray将包含对应于double原始类型数组的Class对象(即,与double[].class相同)。变量cStringArray将包含对应于二维String数组的Class对象(即,与String[][].class相同)。

个人补充:

Class c = double[].class;
System.out.println(c.getName());
// 输出:[D

Class s = String[][].class;
System.out.println(s.getName());
// 输出[[Ljava.lang.String;

原始类型包装类的TYPE字段

尽管.class语法是获取原始类型Class的更方便且首选的方式,但还有另一种方法。每个原始类型和voidjava.lang中都有一个用于将原始类型转换为引用类型的包装类。每个包装类都包含一个名为TYPE的字段,等于被包装的原始类型的Class

Class c = Double.TYPE;

这是一个包装了原始类型doublejava.lang.Double类,用于在需要Object类时。Double.TYPE的值与double.class完全相同。

【用于在需要Object类时】意思是Double可以作为Object类的子类使用

Class c = Void.TYPE;

Void.TYPE的值与void.class完全相同

返回Classes的方法

存在多个反射API返回Classes,但这些方法只能在直接或间接获得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

  • Class.getDeclaredClasses()

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

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

    Character包含两个公开的成员类Character.SubsetCharacter.UnicodeBlock 以及一个私有Character.CharacterCache.

  • Class.getDeclaringClass()

  • java.lang.reflect.Field.getDeclaringClass()

  • java.lang.reflect.Method.getDeclaringClass()

  • java.lang.reflect.Constructor.getDeclaringClass()

    返回声明这些成员的Class。匿名类(Anonymous Class Declarations )声明不会有声明类(declaring class),但会有包围类(enclosing class)。

    补充:什么是【包围类】

    在Java中,我们可以在一个类(A)的内部定义另一个类(B)。这个类A就是包围类,他把类B围起来了(注意如果有多重包围,只返回第一层包围)。

    所以对于匿名内部类来说

    Predicate<String> predicate = new Predicate<String>() {
     @Override
     public boolean test(String s) {
         return false;
     }
    };
    
    Class<?> aClass = predicate.getClass().getDeclaringClass(); // 返回null
    System.out.println(aClass.getName()); //java.lang.NullPointerException
    

    我们可以修改方法为getEnclosingClass(),这样子就可以获取到匿名类所在的类

    Class<?> aClass = predicate.getClass().getEnclosingClass();
    

    注意,如果使用lambda表达式定义getEnclosingClass()将会返回null,因为lambda表达式不会创建类

    import java.lang.reflect.Field;
    
    Field f = System.class.getField("out"); 
    Class c = f.getDeclaringClass();
    System.out.println(c.getName());
    

    out字段是在System中声明的。

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

    匿名类o的声明类将会是null

  • Class.getEnclosingClass()

    返回类的直接包围类。

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

    枚举类 Thread.State的包围类是Thread

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

    匿名类 o 的包围类是 MyClass

检查类修饰符和类型

类可以声明一个或多个修饰符,这些修饰符影响其运行时行为:

  • 访问修饰符:publicprotectedprivate
  • 要求重写的修饰符:abstract
  • 限制为单个实例的修饰符:static
  • 禁止值修改的修饰符:final
  • 强制严格浮点行为的修饰符:strictfp
  • 注解

并非所有修饰符都适用于所有类,例如接口不能是final,枚举不能是abstractjava.lang.reflect.Modifier包含了所有可能修饰符的声明。它还包含了一些方法,可以用来解析由Class.getModifiers()返回的修饰符集合。

ClassDeclarationSpy示例展示了如何获取类的声明组件,包括修饰符、泛型类型参数、实现的接口以及继承路径。由于Class实现了java.lang.reflect.AnnotatedElement接口,因此也可以查询运行时注解。

import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import static java.lang.System.out;

public class ClassDeclarationSpy {
    public static void main(String... args) {
	try {
	    Class<?> c = Class.forName(args[0]);
	    out.format("Class:%n  %s%n%n", c.getCanonicalName());
	    out.format("Modifiers:%n  %s%n%n",
		       Modifier.toString(c.getModifiers()));

	    out.format("Type Parameters:%n");
	    TypeVariable[] tv = c.getTypeParameters();
	    if (tv.length != 0) {
		out.format("  ");
		for (TypeVariable t : tv)
		    out.format("%s ", t.getName());
		out.format("%n%n");
	    } else {
		out.format("  -- No Type Parameters --%n%n");
	    }

	    out.format("Implemented Interfaces:%n");
	    Type[] intfs = c.getGenericInterfaces();
	    if (intfs.length != 0) {
		for (Type intf : intfs)
		    out.format("  %s%n", intf.toString());
		out.format("%n");
	    } else {
		out.format("  -- No Implemented Interfaces --%n%n");
	    }

	    out.format("Inheritance Path:%n");
	    List<Class> l = new ArrayList<Class>();
	    printAncestor(c, l);
	    if (l.size() != 0) {
		for (Class<?> cl : l)
		    out.format("  %s%n", cl.getCanonicalName());
		out.format("%n");
	    } else {
		out.format("  -- No Super Classes --%n%n");
	    }

	    out.format("Annotations:%n");
	    Annotation[] ann = c.getAnnotations();
	    if (ann.length != 0) {
		for (Annotation a : ann)
		    out.format("  %s%n", a.toString());
		out.format("%n");
	    } else {
		out.format("  -- No Annotations --%n%n");
	    }

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

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

以下是一些输出样本。请注意,用户输入将以斜体显示。

// jdk1.8控制台进入src使用【全限定名如reflection.ClassDeclarationSpy】执行下面语句
$ java ClassDeclarationSpy java.util.concurrent.ConcurrentNavigableMap
Class:
  java.util.concurrent.ConcurrentNavigableMap

Modifiers:
  public abstract interface

Type Parameters:
  K V

Implemented Interfaces:
  java.util.concurrent.ConcurrentMap<K, V>
  java.util.NavigableMap<K, V>

Inheritance Path:
  -- No Super Classes --

Annotations
  -- No Annotations --

源码中 java.util.concurrent.ConcurrentNavigableMap 的实际声明

public interface ConcurrentNavigableMap<K,V>
    extends ConcurrentMap<K,V>, NavigableMap<K,V>

值得注意的是,由于这是一个接口,它隐含地声明为abstract的。编译器会为每一个接口添加这个修饰符。此外,此声明包含了两个泛型类型参数,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是一个受检异常(checked exception),因为RuntimeException 并不在其继承链中出现。

$ java ClassDeclarationSpy java.security.Identity
Class:
  java.security.Identity

Modifiers:
  public abstract

Type Parameters:
  -- No Type Parameters --

Implemented Interfaces:
  interface java.security.Principal
  interface java.io.Serializable

Inheritance Path:
  java.lang.Object

Annotations:
  @java.lang.Deprecated()

此输出表明java.security.Identity,,一个已废弃的API,具有java.lang.Deprecated注解。反射代码可以利用这一点来检测废弃的API。


**注意:**并非所有注解都能通过反射访问。只有那些具有java.lang.annotation.RetentionPolicyRUNTIME 保留策略的注解才可以在运行时访问。在语言中预定义的三个注解 @Deprecated, @Override,和@SuppressWarnings 。只有注解在运行时可用。


发现类的成员

Class提供了两种访问字段、方法和构造器的方法:一种是枚举这些成员的方法,另一种是搜索特定成员的方法。同时,也有专门的方法用于访问直接在类上声明的成员,与搜索父类接口(superinterfaces )和父类(superclasses )中继承成员的方法区分开来。下表总结了所有成员获取(member-locating)方法及其特性。

获取字段(Fields)的Class方法

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

获取方法(Methods)的Class方法

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

获取构造器(Constructors)的Class方法

Class API / 能否获取成员列表?继承成员?[1]私有成员?
getDeclaredConstructor()noN/Ayes
getConstructor()noN/Ano
getDeclaredConstructors()yesN/Ayes
getConstructors()yesN/Ano

[1] 构造器不会被继承。

给定一个类名和感兴趣的成员类型声明, ClassSpy 示例使用get*s()方法来确定所有公共元素的列表,包括任何继承的元素。

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

import static java.lang.System.out;

enum ClassMember {CONSTRUCTOR, FIELD, METHOD, CLASS, ALL}

public class ClassSpy {

    public static void main(String... args) {
        try {
            Class<?> c = Class.forName(args[0]);
            out.format("Class:%n  %s%n%n", c.getCanonicalName());

            Package p = c.getPackage();
            out.format("Package:%n  %s%n%n",
                    (p != null ? p.getName() : "-- No Package --"));

            for (int i = 1; i < args.length; i++) {
                switch (ClassMember.valueOf(args[i])) {
                    case CONSTRUCTOR:
                        printMembers(c.getConstructors(), "Constructor");
                        break;
                    case FIELD:
                        printMembers(c.getFields(), "Fields");
                        break;
                    case METHOD:
                        printMembers(c.getMethods(), "Methods");
                        break;
                    case CLASS:
                        printClasses(c);
                        break;
                    case ALL:
                        printMembers(c.getConstructors(), "Constuctors");
                        printMembers(c.getFields(), "Fields");
                        printMembers(c.getMethods(), "Methods");
                        printClasses(c);
                        break;
                    default:
                        assert false;
                }
            }

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

    private static void printMembers(Member[] mbrs, String s) {
        out.format("%s:%n", s);
        for (Member mbr : mbrs) {
            if (mbr instanceof Field)
                out.format("  %s%n", ((Field) mbr).toGenericString());
            else if (mbr instanceof Constructor)
                out.format("  %s%n", ((Constructor) mbr).toGenericString());
            else if (mbr instanceof Method)
                out.format("  %s%n", ((Method) mbr).toGenericString());
        }
        if (mbrs.length == 0)
            out.format("  -- No %s --%n", s);
        out.format("%n");
    }

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

这个示例相对紧凑;但是printMembers()方法却略显笨拙,这是因为java.lang.reflect.Member接口自反射最早的实现以来就已存在,当引入泛型时,无法对其进行修改以包含更有用的getGenericString()方法。唯一的选择是如示例所示进行测试和类型转换,或者用printConstructors()printFields()printMethods()替换这个方法,或者满足于Member.getName()相对简略的结果。

以下是一些输出样本及其解释。用户输入将以斜体显示。

$ java ClassSpy java.lang.ClassCastException CONSTRUCTOR
Class:
  java.lang.ClassCastException

Package:
  java.lang

Constructor:
  public java.lang.ClassCastException()
  public java.lang.ClassCastException(java.lang.String)

由于构造器不会被继承,因此在直接的超类RuntimeException 和其他超类中定义的异常链机制(chaining mechanism)构造器(那些带有Throwable 参数的构造器)将不会被找到。

什么是【链机制(chaining mechanism)构造器】

子类(派生类)必须重载父类的构造函数,并且显式或隐式的调用父类构造函数,这就是链机制(chaining mechanism)构造器

$ java ClassSpy java.nio.channels.ReadableByteChannel METHOD
Class:
  java.nio.channels.ReadableByteChannel

Package:
  java.nio.channels

Methods:
  public abstract int java.nio.channels.ReadableByteChannel.read
    (java.nio.ByteBuffer) throws java.io.IOException
  public abstract void java.nio.channels.Channel.close() throws
    java.io.IOException
  public abstract boolean java.nio.channels.Channel.isOpen()

接口 java.nio.channels.ReadableByteChannel 声明了方法 read()。剩余的方法继承自父类接口。这段代码很容易修改,只需将get*s()替换为getDeclared*s(),就可以仅列出在类中实际声明的方法(无继承的)。

$ java ClassSpy.java ClassMember FIELD METHOD
Class:
  ClassMember

Package:
  -- No Package --

Fields:
  public static final ClassMember ClassMember.CONSTRUCTOR
  public static final ClassMember ClassMember.FIELD
  public static final ClassMember ClassMember.METHOD
  public static final ClassMember ClassMember.CLASS
  public static final ClassMember ClassMember.ALL

Methods:
  public static ClassMember ClassMember.valueOf(java.lang.String)
  public static ClassMember[] ClassMember.values()
  public final int java.lang.Enum.hashCode()
  public final int java.lang.Enum.compareTo(E)
  public int java.lang.Enum.compareTo(java.lang.Object)
  public final java.lang.String java.lang.Enum.name()
  public final boolean java.lang.Enum.equals(java.lang.Object)
  public java.lang.String java.lang.Enum.toString()
  public static <T> T java.lang.Enum.valueOf
    (java.lang.Class<T>,java.lang.String)
  public final java.lang.Class<E> java.lang.Enum.getDeclaringClass()
  public final int java.lang.Enum.ordinal()
  public final native java.lang.Class<?> java.lang.Object.getClass()
  public final native void java.lang.Object.wait(long) throws
    java.lang.InterruptedException
  public final void java.lang.Object.wait(long,int) throws
    java.lang.InterruptedException
  public final void java.lang.Object.wait() hrows java.lang.InterruptedException
  public final native void java.lang.Object.notify()
  public final native void java.lang.Object.notifyAll()

在这些结果的字段部分,枚举常量被列出。虽然从技术上讲,它们确实被视为字段,但将它们与其它字段区分开来可能是有用的。这个示例可以通过使用 java.lang.reflect.Field.isEnumConstant() 方法来进行修改,以便更好地达到这一目的。在本教程稍后部分的 检查枚举 中的 EnumSpy 示例,包含了一种可能的实现。

在输出的方法部分,可以看到方法名称包含了声明类的名字。因此,toString() 方法是由 Enum 类实现的,而不是从 Object 类继承的。代码可以通过使用 Field.getDeclaringClass() 方法来修改,以更明显地展示这一点。下面的代码片段展示了潜在解决方案的一部分:

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

疑难解答

以下示例展示了在反射类时可能遇到的典型错误。

编译器警告:“Note: … uses unchecked or unsafe operations”(使用了未经检查或不安全的操作)

当调用一个方法时,会检查参数值的类型并可能进行转换。 ClassWarning 示例通过调用 getMethod() 方法导致了一个典型的未经检查的转换警告:

import java.lang.reflect.Method;

public class ClassWarning {
    void m() {
	try {
	    Class c = ClassWarning.class;
	    Method m = c.getMethod("m");  // warning

        // production code should handle this exception more gracefully
	} catch (NoSuchMethodException x) {
    	    x.printStackTrace();
    	}
    }
}
$ javac ClassWarning.java
Note: ClassWarning.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
$ javac -Xlint:unchecked ClassWarning.java
ClassWarning.java:6: warning: [unchecked] unchecked call to getMethod
  (String,Class<?>...) as a member of the raw type Class
Method m = c.getMethod("m");  // warning
                      ^
1 warning

许多库方法已经被添加了泛型声明,包括 Class 类中的多个方法。由于 c 被声明为原始类型(没有类型参数),而 getMethod() 方法的对应参数是一个参数化类型,因此会发生未经检查的转换。编译器必须生成一个警告。(参见 The Java Language Specification, Java SE 7 Edition 中的“未经检查的转换” 和 “方法调用转换”)

有两种可能的解决方案。更可取的是修改 c 的声明,以包含适当的泛型类型。在这种情况下,声明应该是:

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

或者,可以使用预定义的注解@SuppressWarnings明确地抑制警告,在问题语句之前添加此注解。

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

提示: 作为一般原则,不应忽略警告,因为它们可能表明存在bug。应适当使用参数化声明。如果这不可能(也许是因为应用程序必须与库提供者的代码(library vendor’s code)交互),则使用@SuppressWarnings注解标记违规行。


当构造器不可访问时抛出 InstantiationException

如果尝试创建类的新实例,但零参数构造器不可见,则 Class.newInstance() 将抛出 InstantiationExceptionClassTrouble 示例演示了由此产生的堆栈跟踪。

class Cls {
    private Cls() {}
}

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

        // production code should handle these exceptions more gracefully
	} catch (InstantiationException x) {
	    x.printStackTrace();
	} catch (IllegalAccessException x) {
	    x.printStackTrace();
	} catch (ClassNotFoundException x) {
	    x.printStackTrace();
	}
    }
}
$ java ClassTrouble
java.lang.IllegalAccessException: Class ClassTrouble can not access a member of
  class Cls with modifiers "private"
        at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:65)
        at java.lang.Class.newInstance0(Class.java:349)
        at java.lang.Class.newInstance(Class.java:308)
        at ClassTrouble.main(ClassTrouble.java:9)

Class.newInstance() 的行为非常类似于 new 关键字,并且会因与 new 失败相同的理由而失败。在反射中,典型的解决策略是利用java.lang.reflect.AccessibleObject 类提供的抑制访问控制检查的能力;但是,这种方法在这里不适用,因为 java.lang.Class 没有继承自 AccessibleObject。唯一的解决途径是修改代码,使用Constructor.newInstance() ,它确实继承自AccessibleObject


提示: 通常,在 成员 章节,出于在 创建新的类对象 部分所述的理由,更倾向于使用 Constructor.newInstance()


更多关于使用 Constructor.newInstance() 可能遇到的问题的示例,可以在 成员 教程的 构造器疑难解答 部分找到。

课程:成员

Reflection 定义了一个接口,该接口java.lang.reflect.Memberjava.lang.reflect.Fieldjava.lang.reflect.Methodjava.lang.reflect.Constructor 实现[1]。本课将讨论这些对象。对于每个成员,本课程将描述**用于检索声明和类型信息的关联 API、成员特有的任何操作(例如,设置字段值或调用方法)以及常见遇到的错误。**每个概念都将通过代码示例和相关输出进行说明,这些输出近似于一些预期的反射用途。

[1]补充:Member的实现类一共有5个

image-20240718110345180

其中Executable(可执行的)为抽象类,其子类包括Contructor和Method。还有一个MemberName

image-20240718110445058


注: 根据 Java 语言规范 Java SE 7 Edition( The Java Language Specification, Java SE 7 Edition),类的成员是类主体的可继承部分,包括字段、方法、嵌套类、接口和枚举类型。由于构造函数不可继承,因此它们不是成员。这与java.lang.reflect.Member的实现类不同。

思考一下为什么构造函数不可继承?


字段(Field)

字段具有类型(type)和值(value)。该 java.lang.reflect.Field 类提供了用于访问类型信息以及设置(setting)和获取(getting)给定对象上字段值的方法。

方法(Methods)

方法具有返回值、参数,并且可能会引发异常。该 java.lang.reflect.Method 类提供用于获取参数和返回值的类型信息的方法。它还可用于调用给定对象上的方法

构造函数(Constructors)

构造函数的反射 API 在java.lang.reflect.Constructor中定义,并且与方法的反射 API 类似,但有两个主要例外:首先,构造函数没有返回值;其次,构造函数的调用为给定类创建对象的新实例

字段

字段是具有关联值的类、接口或枚举。 java.lang.reflect.Field类中的方法可以检索有关字段的信息,例如字段的名称、类型、修饰符和批注。(“类”课程中的“检查类修饰符和类型(Examining Class Modifiers and Types)”一节介绍了如何检索批注。还有一些方法可以动态访问和修改字段的值。以下各节介绍了这些任务:

在编写应用程序(如类浏览器)时,找出哪些字段属于特定类可能很有用。类的字段通过调用 Class.getFields()来标识。该 getFields()方法返回一个对象数组,Field 其中包含每个可访问的公共字段一个对象。

如果公共字段是以下任一成员,则该字段是可访问的(accessible ):

  • 这个类的(this class)
  • 这个类的父类的(a superclass of this class)
  • 这个类实现的接口的(an interface implemented by this class)
  • 这个类实现的接口继承别的接口的(an interface extended from an interface implemented by this class)

字段可以是类(实例)字段,例如 java.io.Reader.lock ,静态字段,例如java.lang.Integer.MAX_VALUE,也可以是枚举常量,例如 java.lang.Thread.State.WAITING

获取字段类型

字段可以是基本类型或引用类型。有八种基本类型:boolean,byte,short,int,long,char,float,double。引用类型是可以是java.lang.Object的任何直接或间接的子类,包括接口、数组和枚举类型

FieldSpy 示例打印字段的类型和泛型类型,并给出一个完全限定的二进制类名和字段名。

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

public class FieldSpy<T> {
    public boolean[][] b = {{ false, false }, { true, true } };
    public String name  = "Alice";
    public List<Integer> list;
    public T val;

    public static void main(String... args) {
	try {
	    Class<?> c = Class.forName(args[0]);
	    Field f = c.getField(args[1]);
	    System.out.format("Type: %s%n", f.getType());
	    System.out.format("GenericType: %s%n", f.getGenericType());

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

检索此类中三个公共字段的类型( bname 和参数化类型 list )的示例输出如下。用户输入为$开头的。

$ java FieldSpy FieldSpy b
Type: class [[Z
GenericType: class [[Z
$ java FieldSpy FieldSpy name
Type: class java.lang.String
GenericType: class java.lang.String
$ java FieldSpy FieldSpy list
Type: interface java.util.List
GenericType: java.util.List<java.lang.Integer>
$ java FieldSpy FieldSpy val
Type: class java.lang.Object
GenericType: T

字段 b 的类型是布尔值的二维数组。类型名称的语法如 中 Class.getName() 所述。

字段 val 的类型被报告为 java.lang.Object ,因为泛型是通过类型擦除实现的,该类型擦除会在编译过程中删除有关泛型类型的所有信息。因此 T 被类型变量的上界替换,在本例中为 java.lang.Object

Field.getGenericType()将查阅类文件中的 Signature 属性(如果存在)。如果这个属性不可用,那么它将回退到使用Field.getType()方法,这个方法并没有因为泛型的引入而改变。在反射中,其他以 getGenericFoo() 命名的方法(其中 Foo 代表某种特定的类型信息)都是以类似的方式实现的。

Signature 属性:在泛型篇中讲过,参数化类型在类型擦除后实际泛型类型会保存在Signature 中,并不会消失。

此外:getGenericType()方法返回的是GenericArrayTypeParameterizedTypeTypeVariable,因为字段可以是多种类型。

检索和解析字段修饰符

有几个修饰符可能是字段声明的一部分:

  • 访问修饰符: publicprotectedprivate
  • 声明于字段的控制运行时行为的修饰符: transientvolatile
  • 限制为一个实例的修饰符: static
  • 禁止值修改的修饰符: final
  • 注解(Annotations)

Field.getModifiers()方法可以用来返回一个整数,这个整数代表了字段声明的修饰符集合。在这个整数中表示修饰符的位(bit)定义在java.lang.reflect.Modifier 类中。

简单来说,Field.getModifiers() 返回的整数包含了字段的所有修饰符信息,比如 public、private、static、final 等,这些修饰符的位标识符可以在 java.lang.reflect.Modifier 类中找到,通过位操作可以解析出具体的修饰符。

FieldModifierSpy示例说明如何搜索具有给定修饰符的字段。它还通过分别调用 Field.isSynthetic()Field.isEnumCostant() 来确定定位的字段是合成的(编译器生成的)还是枚举常量。

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import static java.lang.System.out;

enum Spy { BLACK , WHITE }

public class FieldModifierSpy {
	volatile int share;
	int instance;

	class Inner {
	}

	public static void main(String... args) {
		try {
			Class<?> c = Class.forName(args[0]);
			int searchMods = 0x0;
			for (int i = 1; i < args.length; i++) {
				searchMods |= modifierFromString(args[i]);
			}

			Field[] flds = c.getDeclaredFields();
			out.format("Fields in Class '%s' containing modifiers:  %s%n",
					c.getName(),
					Modifier.toString(searchMods));
			boolean found = false;
			for (Field f : flds) {
				int foundMods = f.getModifiers();
				// Require all of the requested modifiers to be present
				if ((foundMods & searchMods) == searchMods) {
					out.format("%-8s [ synthetic=%-5b enum_constant=%-5b ]%n",
							f.getName(), f.isSynthetic(),
							f.isEnumConstant());
					found = true;
				}
			}

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

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

    private static int modifierFromString(String s) {
		int m = 0x0;
		if ("public".equals(s))           m |= Modifier.PUBLIC;
		else if ("protected".equals(s))   m |= Modifier.PROTECTED;
		else if ("private".equals(s))     m |= Modifier.PRIVATE;
		else if ("static".equals(s))      m |= Modifier.STATIC;
		else if ("final".equals(s))       m |= Modifier.FINAL;
		else if ("transient".equals(s))   m |= Modifier.TRANSIENT;
		else if ("volatile".equals(s))    m |= Modifier.VOLATILE;
		return m;
    }
}

示例输出如下:

$ java FieldModifierSpy FieldModifierSpy volatile
Fields in Class 'FieldModifierSpy' containing modifiers:  volatile
share    [ synthetic=false enum_constant=false ]

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

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

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

请注意,即使某些字段并未在原始代码中声明,它们也会被报告出来。这是因为编译器会生成一些合成字段,这些字段在运行时是必需的。为了检测一个字段是否为合成字段,示例中调用了 Field.isSynthetic() 方法。合成字段的集合依赖于编译器;

然而,常用的一些字段包括用于Inner类(即,非静态成员类的嵌套类)的 this$0 [1],用于引用最外层封闭类,以及由枚举使用的 $VALUES,以实现隐式定义的静态方法 values()

合成类成员的名称未作规定,并且在所有编译器实现或版本中可能并不相同。

这些以及其他合成字段将包含在 Class.getDeclaredFields() 返回的数组中,但不会被 Class.getField() 标识,因为合成成员通常不是**公共(public)**的。

因为 Field 实现了接口 java.lang.reflect.AnnotatedElement ,所以可以用 java.lang.annotation.RetentionPolicy.RUNTIME 来检索任何运行时注解。有关获取Annotate(注解)的示例,请参阅检查类修饰符和类型(Examining Class Modifiers and Types)一节。

[1] 补充关于 this$0:这里指的最外层不是最外面的那层,而是类的外面最靠近的一层,下面的例子里可以看到Inner3持有的是Inner2的对象this$2,而不是this$0FieldModifierSpy)。另外,该合成字段仅提供给非静态成员类,如果是静态的,将不提供(静态的可以静态访问,不需要通过这个变量访问)。

image-20240718162012867

而且只能通过调试这种方式看到,使用javap -c命令查看也看不到的,说明他是在jvm里面合成的

image-20240718162239848

获取和设置字段值

给定一个类的实例,可以使用反射来设置该类中字段的值。这通常仅在无法以通常方式设置值的特殊情况下执行。由于这种访问通常违反了类的设计意图,因此应谨慎使用。

Book 类阐释如何设置 long、array 和 enum 字段类型的值。中 Field 介绍了获取和设置其他基元类型的方法。

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

import static java.lang.System.out;

enum Tweedle {DEE, DUM}

public class Book {
    public long chapters = 0;
    public String[] characters = {"Alice", "White Rabbit"};
    public Tweedle twin = Tweedle.DEE;

    public static void main(String... args) {
        Book book = new Book();
        String fmt = "%6S:  %-12s = %s%n";

        try {
            Class<?> c = book.getClass();

            Field chap = c.getDeclaredField("chapters");
            out.format(fmt, "before", "chapters", book.chapters);
            chap.setLong(book, 12);
            out.format(fmt, "after", "chapters", chap.getLong(book));

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

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

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

这是相应的输出:

$ java Book
BEFORE:  chapters     = 0
 AFTER:  chapters     = 12
BEFORE:  characters   = [Alice, White Rabbit]
 AFTER:  characters   = [Queen, King]
BEFORE:  twin         = DEE
 AFTER:  twin         = DUM

注意:**通过反射设置字段的值会产生一定的性能开销,因为必须执行各种操作,例如验证访问权限。**从运行时的角度来看,效果是相同的,并且操作是原子的,就好像值直接在类代码中更改一样。

使用反射可能会导致某些运行时优化丢失。例如,以下代码极有可能由 Java 虚拟机优化:

int x = 1;
x = 2;
x = 3;

等效代码使用 Field.set*() 可能不会优化。

疑难解答

以下是开发人员遇到的一些常见问题,并解释了发生的原因以及如何解决这些问题。

由于不可转换类型导致的 IllegalArgumentException

FieldTrouble示例将生成一个IllegalArgumentException

尝试使用Field.setInt()方法为一个引用类型为 Integer 的字段设置一个原始类型(primitive type)的值时,在非反射等价代码 Integer val = 42 中,编译器会将基本类型 42 转换(或装箱)为引用类型 new Integer(42),以便其类型检查能够接受该语句。而在使用反射时,类型检查仅在运行时发生,因此没有机会对值进行装箱

import java.lang.reflect.Field;

public class FieldTrouble {
    public Integer val;

    public static void main(String... args) {
	FieldTrouble ft = new FieldTrouble();
	try {
	    Class<?> c = ft.getClass();
	    Field f = c.getDeclaredField("val");
  	    f.setInt(ft, 42);               // IllegalArgumentException

        // production code should handle these exceptions more gracefully
	} catch (NoSuchFieldException x) {
	    x.printStackTrace();
 	} catch (IllegalAccessException x) {
 	    x.printStackTrace();
	}
    }
}
$ java FieldTrouble
Exception in thread "main" java.lang.IllegalArgumentException: Can not set
  java.lang.Object field FieldTrouble.val to (long)42
        at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException
          (UnsafeFieldAccessorImpl.java:146)
        at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException
          (UnsafeFieldAccessorImpl.java:174)
        at sun.reflect.UnsafeObjectFieldAccessorImpl.setLong
          (UnsafeObjectFieldAccessorImpl.java:102)
        at java.lang.reflect.Field.setLong(Field.java:831)
        at FieldTrouble.main(FieldTrouble.java:11)

若要消除此异常,应将有问题的行替换为以下调用 Field.set(Object obj, Object value)

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

提示: **使用反射来设置或获取字段时,编译器没有机会执行装箱操作。**它只能转换规范中由 Class.isAssignableFrom() 描述的那些相关类型。此示例预期会失败,因为在这种情况下 isAssignableFrom() 会返回 false这可以通过编程方式用来验证特定转换是否可行:

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

同样,在反射中,从基本类型到引用类型的自动转换也是不可能的。

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

非公共字段的 NoSuchFieldException

敏锐的读者可能会注意到,如果前面演示的 FieldSpy示例用于获取非公共字段的信息,它将失败:

$ java FieldSpy java.lang.String count
java.lang.NoSuchFieldException: count
        at java.lang.Class.getField(Class.java:1519)
        at FieldSpy.main(FieldSpy.java:12)

提示:Class.getField()Class.getFields()方法返回 Class 对象存在的类、枚举或接口的公共成员字段。若要检索 Class 中声明(但非继承的)的所有字段,请使用 Class.getDeclaredFields()方法。


修改最终字段时的 IllegalAccessException

如果尝试获取或设置字段 private 的值或其他无法访问的字段,或者设置字段 final 的值(无论其访问修饰符如何),则可能会引发 A IllegalAccessException

FieldTroubleToo 示例说明了尝试设置final字段时产生的堆栈跟踪类型。

import java.lang.reflect.Field;

public class FieldTroubleToo {
    public final boolean b = true;

    public static void main(String... args) {
        FieldTroubleToo ft = new FieldTroubleToo();
        try {
			otherThread(ft); // 其他线程 true
            Class<?> c = ft.getClass();
            Field f = c.getDeclaredField("b");
            f.setAccessible(true);  // solution
            f.setBoolean(ft, Boolean.FALSE);   // IllegalAccessException
			System.out.println("反射后直接访问的值:" + ft.b); // true
			System.out.println("反射后通过Field获取的值:" + f.getBoolean(ft)); // false
            // production code should handle these exceptions more gracefully
        } catch (NoSuchFieldException x) {
            x.printStackTrace();
        } catch (IllegalArgumentException x) {
            x.printStackTrace();
        } catch (IllegalAccessException x) {
            x.printStackTrace();
        }
    }

	public static void otherThread(FieldTroubleToo ft) {
		Thread thread = new Thread(() -> {
			while (true) {
				try {
					System.out.println(ft.b);
					Thread.sleep(5 * 1000);
				} catch (InterruptedException e) {
					throw new RuntimeException(e);
				}
			}
		});
		thread.start();
	}
}
$ java FieldTroubleToo
java.lang.IllegalAccessException: Can not set final boolean field
  FieldTroubleToo.b to (boolean)false
        at sun.reflect.UnsafeFieldAccessorImpl.
          throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:55)
        at sun.reflect.UnsafeFieldAccessorImpl.
          throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:63)
        at sun.reflect.UnsafeQualifiedBooleanFieldAccessorImpl.setBoolean
          (UnsafeQualifiedBooleanFieldAccessorImpl.java:78)
        at java.lang.reflect.Field.setBoolean(Field.java:686)
        at FieldTroubleToo.main(FieldTroubleToo.java:12)

提示: 存在访问限制,该限制会阻止在类初始化后设置 final 字段。但是,Field 类的声明继承了 AccessibleObject ,后者提供了抑制此检查的能力。如果 AccessibleObject.setAccessible() 调用成功,那么后续对此字段值的操作将不会因为这个问题而失败。这可能会产生意外的副作用;例如,有时即使值已被修改,应用程序的某些部分仍将继续使用原始值(请执行上面示例)AccessibleObject.setAccessible()仅当安全上下文(security context)允许操作时,才会成功。


方法

方法包含可调用的可执行代码。方法是继承的,在非反射代码中,重载、重写和隐藏等行为由编译器强制执行。相比之下,反射代码使得方法选择可以限制在特定类中,而不考虑其超类。可以访问超类方法,但可以确定其声明类;如果没有反思,这是不可能以编程方式发现的,并且是许多微妙错误的根源。

java.lang.reflect.Method 类提供 API 来访问有关方法的修饰符、返回类型、参数、注释和引发的异常的信息。它还用于调用方法。以下各节涵盖了这些主题:

获取方法类型信息

方法声明包括名称、修饰符、参数、返回类型和可引发异常列表。该 java.lang.reflect.Method 类提供了一种获取此信息的方法。

MethodSpy 示例演示如何枚举给定类中所有已声明的方法,并检索给定名称的所有方法的返回、参数和异常类型。

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

import static java.lang.System.out;

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

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

    public static void main(String... args) {
        try {
            Class<?> c = Class.forName(args[0]);
            Method[] allMethods = c.getDeclaredMethods();
            for (Method m : allMethods) {
                if (!m.getName().equals(args[1])) {
                    continue;
                }
                out.format("%s%n", m.toGenericString());

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

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

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

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

下面是输出,该输出 Class.getConstructor()是具有参数化类型和可变参数数量的方法的示例。

$ java MethodSpy java.lang.Class getConstructor
public java.lang.reflect.Constructor<T> java.lang.Class.getConstructor
  (java.lang.Class<?>[]) throws java.lang.NoSuchMethodException,
  java.lang.SecurityException
              ReturnType: class java.lang.reflect.Constructor
       GenericReturnType: java.lang.reflect.Constructor<T>
           ParameterType: class [Ljava.lang.Class;
    GenericParameterType: java.lang.Class<?>[]
           ExceptionType: class java.lang.NoSuchMethodException
    GenericExceptionType: class java.lang.NoSuchMethodException
           ExceptionType: class java.lang.SecurityException
    GenericExceptionType: class java.lang.SecurityException

这是该方法在源代码中的实际声明:

public Constructor<T> getConstructor(Class<?>... parameterTypes)

首先请注意返回(return)以及参数类型(parameter types)是泛型(generic)的。 Method.getGenericReturnType() 将查阅类文件中的 Signature 属性(如果存在)。如果这个属性不可用,那么它将回退到使用Method.getReturnType()方法,这个方法并没有因为泛型的引入而改变。在反射中,其他以 getGenericFoo() 命名的方法(其中 Foo 代表某种特定的类型信息)都是以类似的方式实现的。

接下来,注意到最后一个(也是唯一的)参数 parameterType 具有可变长度(具有可变数量的参数),类型为 java.lang.Class。它表示为 java.lang.Class 类型的一维数组。这可以通过调用 Method.isVarArgs() 方法来区分它与显式为 java.lang.Class 数组的参数。Method.get*Types() 返回值的语法在 Class.getName() 方法的描述中有所说明。

下面的示例演示具有泛型返回类型的方法。

$ java MethodSpy java.lang.Class cast
public T java.lang.Class.cast(java.lang.Object)
              ReturnType: class java.lang.Object
       GenericReturnType: T
           ParameterType: class java.lang.Object
    GenericParameterType: class java.lang.Object

该方法 Class.cast() 的泛型返回类型被报告为 java.lang.Object ,因为泛型是通过类型擦除实现的,该类型擦除在编译过程中删除了有关泛型类型的所有信息。 T擦除后由 Class 声明定义:

public final class Class<T> implements ...

因此 T 被类型变量的上界替换,在本例中为 java.lang.Object

最后一个示例演示了具有多个重载的方法的输出。

$ java MethodSpy java.io.PrintStream format
public java.io.PrintStream java.io.PrintStream.format
  (java.util.Locale,java.lang.String,java.lang.Object[])
              ReturnType: class java.io.PrintStream
       GenericReturnType: class java.io.PrintStream
           ParameterType: class java.util.Locale
    GenericParameterType: class java.util.Locale
           ParameterType: class java.lang.String
    GenericParameterType: class java.lang.String
           ParameterType: class [Ljava.lang.Object;
    GenericParameterType: class [Ljava.lang.Object;
public java.io.PrintStream java.io.PrintStream.format
  (java.lang.String,java.lang.Object[])
              ReturnType: class java.io.PrintStream
       GenericReturnType: class java.io.PrintStream
           ParameterType: class java.lang.String
    GenericParameterType: class java.lang.String
           ParameterType: class [Ljava.lang.Object;
    GenericParameterType: class [Ljava.lang.Object;

如果发现相同方法名称的多个重载,则它们都由 Class.getDeclaredMethods() 返回。由于 format() 有两个重载(一个有 Locale 一个没),因此两者都让MethodSpy演示出来了。


注意: Method.getGenericExceptionTypes()之所以存在, 是因为实际上可以声明具有泛型异常类型的方法。但是,这很少使用,因为无法捕获泛型异常类型


获取方法参数的名称

您可以使用 方法 java.lang.reflect.Executable.getParameters获取任何方法或构造函数的形式参数的名称。(类 MethodConstructor继承类 Executable ,因此继承该方法 Executable.getParameters前面有提到) 。但是,默认情况下, **.class 文件不存储正式参数名称(你在阅读class文件的时候参数名基本都是var1,var2…)。**这是因为许多生成和使用类文件的工具可能不希望包含参数名称 .class 的文件占用更大的静态和动态占用空间。特别是,这些工具必须处理更大的 .class 文件,而 Java 虚拟机 (JVM) 将使用更多的内存。此外,某些参数名称(如 secretpassword )可能会公开有关安全敏感方法的信息。

若要将正式参数名称存储在特定 .class 文件中,从而使反射 API 能够检索正式参数名称,请使用 javac 编译器 -parameters 选项编译源文件。

MethodParameterSpy 示例演示如何检索给定类的所有构造函数和方法的形式参数的名称。该示例还打印有关每个参数的其他信息。

以下命令打印类 ExampleMethods. 的构造函数和方法的形式参数名称。

注意:请记住使用 -parameters 编译器选项编译示例 ExampleMethods

ExampleMethods类请点击链接进去下载或者复制

java MethodParameterSpy ExampleMethods

此命令打印以下内容:

Number of constructors: 1

Constructor #1
public ExampleMethods()

Number of declared constructors: 1

Declared constructor #1
public ExampleMethods()

Number of methods: 4

Method #1
public boolean ExampleMethods.simpleMethod(java.lang.String,int)
             Return type: boolean
     Generic return type: boolean
         Parameter class: class java.lang.String
          Parameter name: stringParam
               Modifiers: 0
            Is implicit?: false
        Is name present?: true
           Is synthetic?: false
         Parameter class: int
          Parameter name: intParam
               Modifiers: 0
            Is implicit?: false
        Is name present?: true
           Is synthetic?: false

Method #2
public int ExampleMethods.varArgsMethod(java.lang.String...)
             Return type: int
     Generic return type: int
         Parameter class: class [Ljava.lang.String;
          Parameter name: manyStrings
               Modifiers: 0
            Is implicit?: false
        Is name present?: true
           Is synthetic?: false

Method #3
public boolean ExampleMethods.methodWithList(java.util.List<java.lang.String>)
             Return type: boolean
     Generic return type: boolean
         Parameter class: interface java.util.List
          Parameter name: listParam
               Modifiers: 0
            Is implicit?: false
        Is name present?: true
           Is synthetic?: false

Method #4
public <T> void ExampleMethods.genericMethod(T[],java.util.Collection<T>)
             Return type: void
     Generic return type: void
         Parameter class: class [Ljava.lang.Object;
          Parameter name: a
               Modifiers: 0
            Is implicit?: false
        Is name present?: true
           Is synthetic?: false
         Parameter class: interface java.util.Collection
          Parameter name: c
               Modifiers: 0
            Is implicit?: false
        Is name present?: true
           Is synthetic?: false

MethodParameterSpy 示例使用 Parameter 类中的以下方法:

  • getType :返回一个 Class 对象,该对象标识参数的声明类型。

  • getName :返回参数的名称。如果参数的名称存在,则此方法返回 .class 文件提供的名称。否则,此方法将合成以下形式的 arg*N* 名称,其中 *N* 是声明参数的方法的描述符中参数的索引。

    例如,假设您在未指定 -parameters 编译器选项的情况下编译了类 ExampleMethods 。该示例 MethodParameterSpy 将为该方法 ExampleMethods.simpleMethod 打印以下内容:

    public boolean ExampleMethods.simpleMethod(java.lang.String,int)
                 Return type: boolean
         Generic return type: boolean
             Parameter class: class java.lang.String
              Parameter name: arg0
                   Modifiers: 0
                Is implicit?: false
            Is name present?: false
               Is synthetic?: false
             Parameter class: int
              Parameter name: arg1
                   Modifiers: 0
                Is implicit?: false
            Is name present?: false
               Is synthetic?: false
    
  • getModifiers:返回一个整数,该整数表示形式参数所具有的各种特征。如果适用于形式参数,则此值是以下值的总和:

    值(十进制)值(十六进制)描述
    160x0010形式参数已声明 final
    40960x1000形式参数是合成的。或者,您可以调用方法 isSynthetic
    327680x8000该参数在源代码中隐式声明。或者,您可以调用该方法 isImplicit
  • isImplicit :如果此参数在源代码中隐式声明,则返回 true 。有关详细信息,请参阅隐式参数和综合参数( Implicit and Synthetic Parameters )部分。

  • isNamePresent :如果参数根据 .class 文件具有名称,则返回 true

  • isSynthetic :如果此参数在源代码中既不是隐式声明也不是显式声明,则返回 true 。有关详细信息,请参阅隐式参数和综合参数( Implicit and Synthetic Parameters )部分。

隐式参数和合成参数

如果某些构造不是显式编写的,则会在源代码中隐式声明它们。例如,该 ExampleMethods 示例不包含构造函数。默认构造函数是为其隐式声明的。该 MethodParameterSpy 示例打印有关隐式声明的构造函数的信息 ExampleMethods

Number of declared constructors: 1
public ExampleMethods()

请看以下摘录 MethodParameterExamples

public class MethodParameterExamples {
    public class InnerClass { }
}

该类 InnerClass 是非静态嵌套类( nested class )或内部类。内部类的构造函数也是隐式声明的。但是,此构造函数将包含一个参数。当 Java 编译器编译时 InnerClass ,它会创建一个 .class 文件,该文件表示类似于以下内容的代码:

public class MethodParameterExamples {
    public class InnerClass {
        final MethodParameterExamples parent;
        InnerClass(final MethodParameterExamples this$0) {
            parent = this$0; 
        }
    }
}
// 但是实际上你是看不到的,只能通过反射知道有这些 

InnerClass 构造函数包含一个参数,其类型是包含 InnerClass 的类,即 MethodParameterExamples 。因此,该示例 MethodParameterExamples 打印以下内容:

public MethodParameterExamples$InnerClass(MethodParameterExamples)
         Parameter class: class MethodParameterExamples
          Parameter name: this$0
               Modifiers: 32784
            Is implicit?: true
        Is name present?: true
           Is synthetic?: false

由于类 InnerClass 的构造函数是隐式声明的,因此其参数也是隐式的。

注意:

  • Java 编译器为内部类的构造函数创建一个形式参数,使编译器能够将引用(表示最近的外围类实例)从创建者传递到成员类的构造函数。(说人话就是看代码,把父类引用传递给内部类了)
  • 值 32784 表示 InnerClass 构造函数的参数既是最final的 (16) 又是implicit (隐式)的 (32768)。
  • Java 编程语言允许使用带有美元符号 ( $ ) 的变量名称;但是,按照惯例,变量名称中不使用美元符号。

如果 Java 编译器创建的构造方法与源代码中显式或隐式声明的构造方法不对应,则将它们标记为合成构造,除非它们是类初始化方法。合成构造方法是由编译器生成的构件,这些构件在不同的实现中有所不同。请看以下摘录 MethodParameterExamples

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

当 Java 编译器遇到 enum 构造时,它会创建多个与 .class 文件结构兼容的方法,并提供 enum 构造的预期功能。例如,Java 编译器将为 enum 构造创建一个 .class 文件 Colors ,该文件表示类似于以下内容的代码:

final class Colors extends java.lang.Enum<Colors> {
    public final static Colors RED = new Colors("RED", 0);
    public final static Colors BLUE = new Colors("WHITE", 1);
 
    private final static values = new Colors[]{ RED, BLUE };
 
    private Colors(String name, int ordinal) {
        super(name, ordinal);
    }
 
    public static Colors[] values(){
        return values;
    }
 
    public static Colors valueOf(String name){
        return (Colors)java.lang.Enum.valueOf(Colors.class, name);
    }
}

Java 编译器为此 enum 构造创建了三个构造函数和方法: Colors(String name, int ordinal)Colors[] values()Colors valueOf(String name) 。方法 valuesvalueOf 被隐式声明。因此,它们的正式参数名称也是隐式声明的。

enum 构造函数 Colors(String name, int ordinal) 是默认构造函数,并且是隐式声明的。但是,此构造函数 ( nameordinal ) 的形式参数不是隐式声明的。因为这些形式参数既不是显式声明的,也不是隐式声明的,它们是合成的。( enum 构造的默认构造函数的形式参数不是隐式声明的,因为不同的编译器不需要就此构造函数的形式达成一致;另一个 Java 编译器可能会为其指定不同的形式参数。当编译器编译使用 enum 常量的表达式时,它们仅依赖于 enum 构造的公共静态字段(这些字段是隐式声明的),而不依赖于它们的构造函数或这些常量的初始化方式。

因此,该示例 MethodParameterExample 打印有关 enum 构造的以下内容 Colors

$ javac -parameters MethodParameterExamples.java  

enum Colors:

Number of constructors: 0

Number of declared constructors: 1

Declared constructor #1
private MethodParameterExamples$Colors()
         Parameter class: class java.lang.String
          Parameter name: $enum$name
               Modifiers: 4096
            Is implicit?: false
        Is name present?: true
           Is synthetic?: true
         Parameter class: int
          Parameter name: $enum$ordinal
               Modifiers: 4096
            Is implicit?: false
        Is name present?: true
           Is synthetic?: true

Number of methods: 2

Method #1
public static MethodParameterExamples$Colors[]
    MethodParameterExamples$Colors.values()
             Return type: class [LMethodParameterExamples$Colors;
     Generic return type: class [LMethodParameterExamples$Colors;

Method #2
public static MethodParameterExamples$Colors
    MethodParameterExamples$Colors.valueOf(java.lang.String)
             Return type: class MethodParameterExamples$Colors
     Generic return type: class MethodParameterExamples$Colors
         Parameter class: class java.lang.String
          Parameter name: name
               Modifiers: 32768
            Is implicit?: true
        Is name present?: true
           Is synthetic?: false

有关隐式声明的构造(包括反射 API 中显示为隐式的参数)的更多信息,请参阅 Java 语言规范( Java Language Specification)。

检索和解析方法修饰符

有几个修饰符可能是方法声明的一部分:

  • 访问修饰符: publicprotectedprivate
  • 限制为一个实例的修饰符: static
  • 禁止值修改的修饰符: final
  • 需要覆盖(override)的修饰符: abstract
  • 防止重入的修饰符: synchronized
  • 指示另一种编程语言实现的修饰符: native
  • 修饰符强制严格的浮点行为: strictfp
  • 注解(Annotations)

MethodModifierSpy 示例列出具有给定名称的方法的修饰符。它还显示该方法是合成(synthetic )的(编译器生成的)、还是可变参数方法( variable arity),还是桥接方法( bridge method)(编译器生成以支持泛型接口)。

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import static java.lang.System.out;

public class MethodModifierSpy {

    private static int count;
    private static synchronized void inc() { count++; }
    private static synchronized int cnt() { return count; }

    public static void main(String... args) {
	try {
	    Class<?> c = Class.forName(args[0]);
	    Method[] allMethods = c.getDeclaredMethods();
	    for (Method m : allMethods) {
		if (!m.getName().equals(args[1])) {
		    continue;
		}
		out.format("%s%n", m.toGenericString());
		out.format("  Modifiers:  %s%n",
			   Modifier.toString(m.getModifiers()));
		out.format("  [ synthetic=%-5b var_args=%-5b bridge=%-5b ]%n",
			   m.isSynthetic(), m.isVarArgs(), m.isBridge());
		inc();
	    }
	    out.format("%d matching overload%s found%n", cnt(),
		       (cnt() == 1 ? "" : "s"));

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

以下是输出 MethodModifierSpy 产品的几个示例。

$ java MethodModifierSpy java.lang.Object wait
public final void java.lang.Object.wait() throws java.lang.InterruptedException
  Modifiers:  public final
  [ synthetic=false var_args=false bridge=false ]
public final void java.lang.Object.wait(long,int)
  throws java.lang.InterruptedException
  Modifiers:  public final
  [ synthetic=false var_args=false bridge=false ]
public final native void java.lang.Object.wait(long)
  throws java.lang.InterruptedException
  Modifiers:  public final native
  [ synthetic=false var_args=false bridge=false ]
3 matching overloads found

$ java MethodModifierSpy java.lang.StrictMath toRadians
public static double java.lang.StrictMath.toRadians(double)
  Modifiers:  public static strictfp
  [ synthetic=false var_args=false bridge=false ]
1 matching overload found

$ java MethodModifierSpy MethodModifierSpy inc
private synchronized void MethodModifierSpy.inc()
  Modifiers: private synchronized
  [ synthetic=false var_args=false bridge=false ]
1 matching overload found

$ java MethodModifierSpy java.lang.Class getConstructor
public java.lang.reflect.Constructor<T> java.lang.Class.getConstructor
  (java.lang.Class<T>[]) throws java.lang.NoSuchMethodException,
  java.lang.SecurityException
  Modifiers: public transient
  [ synthetic=false var_args=true bridge=false ]
1 matching overload found

$ java MethodModifierSpy java.lang.String compareTo
public int java.lang.String.compareTo(java.lang.String)
  Modifiers: public
  [ synthetic=false var_args=false bridge=false ]
public int java.lang.String.compareTo(java.lang.Object)
  Modifiers: public volatile
  [ synthetic=true  var_args=false bridge=true  ]
2 matching overloads found

请注意, Class.getConstructor() 方法调用Method.isVarArgs() 返回 true 是因为方法声明如下所示:

public Constructor<T> getConstructor(Class<?>... parameterTypes)

不是这样的:

public Constructor<T> getConstructor(Class<?> [] parameterTypes)

请注意,输出 String.compareTo() 包含两个方法。中 String.java 声明的方法:

public int compareTo(String anotherString);

以及第二种合成或编译器生成的桥接方法。发生这种情况是因为 String 实现了参数化接口 Comparable 。在类型擦除期间,继承方法 Comparable.compareTo() 的参数类型从 java.lang.Object 更改为 java.lang.String 。由于 和 compareTo Comparable String 中方法的参数类型在擦除后不再匹配,因此无法进行覆盖。在所有其他情况下,这将产生编译时错误,因为接口未实现。桥接方法的添加避免了这个问题。

Method 实现 java.lang.reflect.AnnotatedElement .因此,可以检索带有java.lang.annotation.RetentionPolicy.RUNTIME注解的任何运行时注释。 有关获取注释的示例,请参阅检查类修饰符和类型一节(Examining Class Modifiers and Types)。

方法调用

反射提供了一种在类上调用方法的方法。通常,仅当无法在非反射代码中将类的实例转换为所需的类型时,才需要这样做。方法使用 java.lang.reflect.Method.invoke 调用。第一个参数是要调用此特定方法的对象实例。(如果方法是 static ,第一个参数应该是 null 。后续参数是方法的参数。如果指定方法引发异常,则该方法将由 java.lang.reflect.InvocationTargetException包装,可以使用异常链接机制 InvocationTargetException.getCause()的方法获取方法的原始异常。

查找和调用具有特定声明的方法

考虑一个测试场景,它使用反射来调用给定类中的专用测试方法。该 Deet 示例在类中搜索以字符串 “ test ” 开头、具有boolean返回类型和单个 Locale 参数的 public 方法。然后,它调用每个找到的方法。

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Locale;
import static java.lang.System.out;
import static java.lang.System.err;

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

    private int testFoo(Locale l) { return 0; }
    private boolean testBar() { return true; }

    public static void main(String... args) {
	if (args.length != 4) {
	    err.format("Usage: java Deet <classname> <langauge> <country> <variant>%n");
	    return;
	}

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

	    Method[] allMethods = c.getDeclaredMethods();
	    for (Method m : allMethods) {
		String mname = m.getName();
		if (!mname.startsWith("test")
		    || (m.getGenericReturnType() != boolean.class)) {
		    continue;
		}
 		Type[] pType = m.getGenericParameterTypes();
 		if ((pType.length != 1)
		    || Locale.class.isAssignableFrom(pType[0].getClass())) {
 		    continue;
 		}

		out.format("invoking %s()%n", mname);
		try {
		    m.setAccessible(true);
		    Object o = m.invoke(t, new Locale(args[1], args[2], args[3]));
		    out.format("%s() returned %b%n", mname, (Boolean) o);

		// Handle any exceptions thrown by method to be invoked.
		} catch (InvocationTargetException x) {
		    Throwable cause = x.getCause();
		    err.format("invocation of %s failed: %s%n",
			       mname, cause.getMessage());
		}
	    }

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

Deet 调用 getDeclaredMethods(),这将返回类中显式声明的所有方法。此外, Class.isAssignableFrom() 还用于确定定位方法的参数是否与所需的调用兼容。从技术上讲,因为Localefinal 类型的,所以通过以下语句是否是 true 可以测试是否兼容:

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

但是, Class.isAssignableFrom()更通用。

$ java Deet Deet ja JP JP
invoking testDeet()
Locale = 日文 (日本,JP), ISO Language Code = jpn
testDeet() returned true

$ java Deet Deet xx XX XX
invoking testDeet()
invocation of testDeet failed: Couldn't find 3-letter language code for xx

首先,请注意,仅 testDeet() 满足代码要求的声明限制。接下来,当传递一个无效参数时 ,testDeet()会抛出一个未检查(unchecked)错误 java.util.MissingResourceException。在反射中,在处理已检查的异常和未检查的例外时没有区别。它们都包裹在一个 InvocationTargetException

使用可变数量的参数调用方法

Method.invoke()可用于将可变数量的参数传递给方法。要理解的关键概念是,可变参数方法的实现就像变量参数打包在数组中一样。

InvokeMain 示例演示如何在任何类中调用 main() 入口点并在运行时传递确定的一组参数。

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

public class InvokeMain {
    public static void main(String... args) {
	try {
	    Class<?> c = Class.forName(args[0]);
	    Class[] argTypes = new Class[] { String[].class };
	    Method main = c.getDeclaredMethod("main", argTypes);
  	    String[] mainArgs = Arrays.copyOfRange(args, 1, args.length);
	    System.out.format("invoking %s.main()%n", c.getName());
	    main.invoke(null, (Object)mainArgs);

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

首先,为了找到main()方法,代码会在当前环境中搜索一个名为"main"的方法,这个方法存在于某个类中,并且接受一个参数,这个参数是一个String类型的数组。由于main()方法是静态的,所以在调用Method.invoke()时,第一个参数会传入null,这是因为静态方法并不属于任何特定的对象实例;第二个参数则是传递给main()方法的参数数组,通常这是程序启动时从操作系统接收的命令行参数。

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

疑难解答

本部分包含开发人员在使用反射查找、调用或获取有关方法的信息时可能遇到的问题的示例。

由于类型擦除导致的 NoSuchMethodException

MethodTrouble 示例演示了在类中搜索特定方法的代码时不考虑类型擦除会发生什么情况。

import java.lang.reflect.Method;

public class MethodTrouble<T>  {
    public void lookup(T t) {}
    public void find(Integer i) {}

    public static void main(String... args) {
        try {
            String mName = args[0];
            Class cArg = Class.forName(args[1]);
            Class<?> c = (new MethodTrouble<Integer>()).getClass();
            Method m = c.getMethod(mName, cArg);
            System.out.format("Found:%n  %s%n", m.toGenericString());

            // production code should handle these exceptions more gracefully
        } catch (NoSuchMethodException x) {
            x.printStackTrace();
        } catch (ClassNotFoundException x) {
            x.printStackTrace();
        }
    }
}
$ java MethodTrouble lookup java.lang.Integer
java.lang.NoSuchMethodException: MethodTrouble.lookup(java.lang.Integer)
        at java.lang.Class.getMethod(Class.java:1605)
        at MethodTrouble.main(MethodTrouble.java:12)
        
$ java MethodTrouble lookup java.lang.Object
Found:
  public void MethodTrouble.lookup(T)

当使用泛型参数类型声明方法时,编译器会将泛型类型替换为其上限,在本例中,该 T 法的上限为 Object 。因此,当代码搜索 lookup(Integer) 时,找不到任何方法,尽管 MethodTrouble 的实例是按如下方式创建的:

Class<?> c = (new MethodTrouble<Integer>()).getClass(); // 这种方式并不会创建类,但是在类的内部是可以获取到实际类型的

改为Object类后, lookup(Object) 如期搜索成功。

$ java MethodTrouble find java.lang.Integer
Found:
  public void MethodTrouble.find(java.lang.Integer)
  
$ java MethodTrouble find java.lang.Object
java.lang.NoSuchMethodException: MethodTrouble.find(java.lang.Object)
        at java.lang.Class.getMethod(Class.java:1605)
        at MethodTrouble.main(MethodTrouble.java:12)

在本例中, find() 没有泛型参数,因此搜索的getMethod()参数类型必须完全匹配。


提示: 搜索方法时,始终传递参数化类型的上限。


调用方法时的 IllegalAccessException

如果尝试调用某个 private 方法或以其他方式无法访问的方法,则会引发 IllegalAccessException 操作。

MethodTroubleAgain示例显示了一个典型的堆栈跟踪,该跟踪是由于尝试调用另一个类中的私有方法而产生的。

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

class AnotherClass {
    private void m() {
    }
}

public class MethodTroubleAgain {
    public static void main(String... args) {
        AnotherClass ac = new AnotherClass();
        try {
            Class<?> c = ac.getClass();
            Method m = c.getDeclaredMethod("m");
//  	    m.setAccessible(true);      // solution
            Object o = m.invoke(ac);    // IllegalAccessException

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

引发的异常的堆栈跟踪(stack trace)如下。

$ java MethodTroubleAgain
java.lang.IllegalAccessException: Class MethodTroubleAgain can not access a
  member of class AnotherClass with modifiers "private"
        at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:65)
        at java.lang.reflect.Method.invoke(Method.java:588)
        at MethodTroubleAgain.main(MethodTroubleAgain.java:15)

提示: 存在访问限制,这会阻止通过反射调用那些通常无法直接调用的方法(这包括但不限于在另一个类中的private方法,以及在另一个私有类中的公共方法)。然而,Method类声明它扩展了AccessibleObject,后者提供了通过AccessibleObject.setAccessible()来抑制这种检查的能力。如果它成功了,那么后续对这个方法对象的调用将不会因为这个问题而失败。


Method.invoke() 中的 IllegalArgumentException

Method.invoke() 已被改造为一种可变参数方法。这是一个巨大的便利,但它可能会导致意外行为。该 MethodTroubleToo示例显示了 Method.invoke() 可能产生令人困惑结果的各种方式。

import java.lang.reflect.Method;

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

    public static void main(String... args) {
        try {
            MethodTroubleToo mtt = new MethodTroubleToo();
            Method m = MethodTroubleToo.class.getMethod("ping");

            switch (Integer.parseInt(args[0])) {
                case 0:
                    m.invoke(mtt);                 // works
                    break;
                case 1:
                    m.invoke(mtt, null);           // works (expect compiler warning)
                    break;
                case 2:
                    Object arg2 = null;
                    m.invoke(mtt, arg2);           // IllegalArgumentException
                    break;
                case 3:
                    m.invoke(mtt, new Object[0]);  // works
                    break;
                case 4:
                    Object arg4 = new Object[0];
                    m.invoke(mtt, arg4);           // IllegalArgumentException
                    break;
                default:
                    System.out.format("Test not found%n");
            }

            // production code should handle these exceptions more gracefully
        } catch (Exception x) {
            x.printStackTrace();
        }
    }
}
$ java MethodTroubleToo 0
PONG!  

// m.invoke(mtt);

由于除第一个参数外的所有参数 Method.invoke() 都是可变的,因此当要调用的方法没有参数时,可以省略它们。

$ java MethodTroubleToo 1
PONG!

// m.invoke(mtt, null); 

在这种情况下,代码会生成此编译器警告(但可以运行),因为 null 不明确。

$ javac MethodTroubleToo.java
MethodTroubleToo.java:16: warning: non-varargs call of varargs method with
  inexact argument type for last parameter;
 		m.invoke(mtt, null);           // works (expect compiler warning)
 		              ^
  cast to Object for a varargs call
  cast to Object[] for a non-varargs call and to suppress this warning
1 warning

无法确定是 null 表示空的参数数组还是第一个参数是 null

$ java MethodTroubleToo 2
java.lang.IllegalArgumentException: wrong number of arguments
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke
          (NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke
          (DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at MethodTroubleToo.main(MethodTroubleToo.java:21)
        
// Object arg2 = null;
// m.invoke(mtt, arg2);

尽管参数是 null ,但还是失败了,因为类型是一个 Object 但是 ping() 根本不需要任何参数。

$ java MethodTroubleToo 3
PONG!

// m.invoke(mtt, new Object[0]);

这之所以有效,是因为 new Object[0] 创建了一个类型为Object数量为0空数组,而对于 varargs 方法,这相当于不传递任何可选参数。

$ java MethodTroubleToo 4
java.lang.IllegalArgumentException: wrong number of arguments
        at sun.reflect.NativeMethodAccessorImpl.invoke0
          (Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke
          (NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke
          (DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at MethodTroubleToo.main(MethodTroubleToo.java:28)
        
// Object arg4 = new Object[0];
// m.invoke(mtt, arg4);

与前面的示例不同,如果空数组存储在 Object 中,则将其视为 Object。这失败的原因与案例 2 失败的原因相同, ping() 不需要参数。


提示:当声明方法 foo(Object... o) 时,编译器会将传递给 foo() 的所有参数放在 Object类型的数组中。 foo() 的实现与声明 foo(Object[] o) 相同。了解这一点可能有助于避免上述问题类型。

说人话就是:foo(Object... o)foo(Object[] o)是一样的。下面代码会提示“方法已存在”。

public void foo(Object... vars) {}
public void foo(Object[] vars) {}

调用方法失败时的 InvocationTargetException

InvocationTargetException 包装所有在调用方法对象时产生的异常(checked和unchecked)。该 MethodTroubleReturns 示例演示如何获得调用方法时引发的原始异常。

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

public class MethodTroubleReturns {
    private void drinkMe(int liters) {
	if (liters < 0)
	    throw new IllegalArgumentException("I can't drink a negative amount of liquid");
    }

    public static void main(String... args) {
	try {
	    MethodTroubleReturns mtr  = new MethodTroubleReturns();
 	    Class<?> c = mtr.getClass();
   	    Method m = c.getDeclaredMethod("drinkMe", int.class);
	    m.invoke(mtr, -1);

        // production code should handle these exceptions more gracefully
	} catch (InvocationTargetException x) {
	    Throwable cause = x.getCause();
	    System.err.format("drinkMe() failed: %s%n", cause.getMessage());
	} catch (Exception x) {
	    x.printStackTrace();
	}
    }
}
$ java MethodTroubleReturns
drinkMe() failed: I can't drink a negative amount of liquid

提示: 如果抛出了一个InvocationTargetException,则说明方法已经调用完成。诊断问题的方式与直接调用该方法并抛出异常(可通过 getCause()方法获取)相同。此异常并不表明反射包或其使用存在问题。

说人话就是:方法调用成功,但是方法报错了。反射调用跟你直接调用都是报一样的错的。


构造函数

构造函数用于创建作为类实例的对象。通常,它会在调用方法或访问字段之前执行初始化类所需的操作。构造函数永远不会被继承

与方法类似,反射提供了 API 来发现和检索类的构造函数,并获取修饰符、参数、注释和抛出的异常等声明信息。也可以使用指定的构造函数创建类的新实例。使用构造函数时使用的关键类是 Classjava.lang.reflect.Constructor 。以下各节介绍了涉及构造函数的常见操作:

  • 查找构造函数演示了如何检索具有特定参数的构造函数
  • 检索和分析构造函数修饰符演示如何获取构造函数声明的修饰符以及有关构造函数的其他信息
  • 创建新的类实例演示如何通过调用对象的构造函数来实例化对象的实例
  • 疑难解答描述了在查找或调用构造函数时可能遇到的常见错误

查找构造函数

构造函数声明包括名称、修饰符、参数和可引发的异常列表。该 java.lang.reflect.Constructor 类提供了一种获取此信息的方法 。

ConstructorSift 示例演示了如何在类声明的构造函数中搜索具有给定类型参数的构造函数。

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

import static java.lang.System.out;

public class ConstructorSift {
    public static void main(String... args) {
        try {
            Class<?> cArg = Class.forName(args[1]);

            Class<?> c = Class.forName(args[0]);
            Constructor[] allConstructors = c.getDeclaredConstructors();
            for (Constructor ctor : allConstructors) {
                Class<?>[] pType = ctor.getParameterTypes();
                for (int i = 0; i < pType.length; i++) {
                    if (pType[i].equals(cArg)) {
                        out.format("%s%n", ctor.toGenericString());

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

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

Method.getGenericParameterTypes() 将查阅类文件中的 Signature属性(如果存在)。如果该属性不可用,那么它将回退到使用 Method.getParameterType() 方法,这个方法没有因为引入泛型而改变。在反射中,其他名称为getGenericFoo()的方法(其中Foo代表某个特定值)以类似的方式实现。Method.get*Types()返回值的语法在 Class.getName()的描述中有详细说明。

以下是 java.util.Formatter 中具有 Locale 参数的所有构造函数的输出。

$ java ConstructorSift java.util.Formatter java.util.Locale
public
java.util.Formatter(java.io.OutputStream,java.lang.String,java.util.Locale)
throws java.io.UnsupportedEncodingException
       GenericParameterType[0]: class java.io.OutputStream
       GenericParameterType[1]: class java.lang.String
      *GenericParameterType[2]: class java.util.Locale
public java.util.Formatter(java.lang.String,java.lang.String,java.util.Locale)
throws java.io.FileNotFoundException,java.io.UnsupportedEncodingException
       GenericParameterType[0]: class java.lang.String
       GenericParameterType[1]: class java.lang.String
      *GenericParameterType[2]: class java.util.Locale
public java.util.Formatter(java.lang.Appendable,java.util.Locale)
       GenericParameterType[0]: interface java.lang.Appendable
      *GenericParameterType[1]: class java.util.Locale
public java.util.Formatter(java.util.Locale)
      *GenericParameterType[0]: class java.util.Locale
public java.util.Formatter(java.io.File,java.lang.String,java.util.Locale)
throws java.io.FileNotFoundException,java.io.UnsupportedEncodingException
       GenericParameterType[0]: class java.io.File
       GenericParameterType[1]: class java.lang.String
      *GenericParameterType[2]: class java.util.Locale

下一个示例输出说明如何搜索String 类型 中的 char[] 参数。

$ java ConstructorSift java.lang.String "[C"
java.lang.String(int,int,char[])
       GenericParameterType[0]: int
       GenericParameterType[1]: int
      *GenericParameterType[2]: class [C
public java.lang.String(char[],int,int)
      *GenericParameterType[0]: class [C
       GenericParameterType[1]: int
       GenericParameterType[2]: int
public java.lang.String(char[])
      *GenericParameterType[0]: class [C

Class.forName() 中有详细描述可被 Class.getName() 接受的引用类型和基本类型数组的表达语法。需要注意的是,列出的第一个构造器是包私有package-private)的,而不是公共public)的。之所以返回它,是因为示例代码使用了Class.getDeclaredConstructors(),而不是仅返回 public 构造器的 Class.getConstructors()

此示例表明,搜索可变数量参数的方法(具有可变数量的参数)需要使用数组语法:

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

这是源代码中 ProcessBuilder 构造函数的实际声明:

public ProcessBuilder(String... command)

该参数([Ljava.lang.String)表示为 java.lang.String 类型的一维数组。这可以通过调用 Constructor.isVarArgs()来与显式定义的 java.lang.String 数组区分开来。

The final example reports the output for a constructor which has been declared with a generic parameter type:
最后的示例展示了一个使用泛型参数类型声明的构造函数的输出:

$ java ConstructorSift java.util.HashMap java.util.Map
public java.util.HashMap(java.util.Map<? extends K, ? extends V>)
      *GenericParameterType[0]: java.util.Map<? extends K, ? extends V>

对于构造函数,可以采用与方法类似的方式检索异常类型。有关详细信息,请参阅“获取方法类型信息( Obtaining Method Type Information )”部分中所述的 MethodSpy 示例。

检索和分析构造函数修饰符

由于构造函数在语言中的角色,有意义的修饰符比方法的修饰符少:

  • 访问修饰符: publicprotectedprivate
  • 注解(Annotations)

ConstructorAccess 示例在给定类中搜索具有指定访问修饰符的构造函数。它还显示构造函数是合成的(编译器生成的)还是可变参数的。

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

import static java.lang.System.out;

public class ConstructorAccess {
    public static void main(String... args) {
        try {
            Class<?> c = Class.forName(args[0]);
            Constructor[] allConstructors = c.getDeclaredConstructors();
            for (Constructor ctor : allConstructors) {
                int searchMod = modifierFromString(args[1]);
                int mods = accessModifiers(ctor.getModifiers());
                if (searchMod == mods) {
                    out.format("%s%n", ctor.toGenericString());
                    out.format("  [ synthetic=%-5b var_args=%-5b ]%n",
                            ctor.isSynthetic(), ctor.isVarArgs());
                }
            }

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

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

    private static int modifierFromString(String s) {
        if ("public".equals(s)) return Modifier.PUBLIC;
        else if ("protected".equals(s)) return Modifier.PROTECTED;
        else if ("private".equals(s)) return Modifier.PRIVATE;
        else if ("package-private".equals(s)) return 0;
        else return -1;
    }
}

没有对应于"package-private" 访问的显式 Modifier常量,因此有必要检查是否缺少所有三个访问修饰符以识别"package-private" 构造函数。

此输出显示 : java.io.File

$ java ConstructorAccess java.io.File private
private java.io.File(java.lang.String,int)
  [ synthetic=false var_args=false ]
private java.io.File(java.lang.String,java.io.File)
  [ synthetic=false var_args=false ]

合成构造函数很少见;但是,该 SyntheticConstructor 示例说明了可能发生这种情况的典型情况:

public class SyntheticConstructor {
    private SyntheticConstructor() {}
    class Inner {
	// Compiler will generate a synthetic constructor since
	// SyntheticConstructor() is private.
	Inner() { new SyntheticConstructor(); }
    }
}
$ java ConstructorAccess SyntheticConstructor package-private
SyntheticConstructor(SyntheticConstructor$1)
  [ synthetic=true  var_args=false ]

// 补充下跟private的区别
$ java ConstructorAccess SyntheticConstructor private
private SyntheticConstructor()
  [ synthetic=false var_args=false ]

由于内部类的构造函数引用封闭(包围)类私有构造函数,因此编译器必须生成一个package-private构造函数。参数类型 SyntheticConstructor$1 是任意的,取决于编译器实现。依赖于任何合成或非公共类成员存在的代码可能无法移植。

构造函数实现了 java.lang.reflect.AnnotatedElement ,它提供了在运行时获取带有 java.lang.annotation.RetentionPolicy.RUNTIME 的注解的方法。有关获取注解的示例,请参阅“检查类修饰符和类型( Examining Class Modifiers and Types )”部分。

创建新的类实例

有两种反射方法可用于创建类的实例: java.lang.reflect.Constructor.newInstance()Class.newInstance()。前者是首选,因此在这些示例中使用,因为:

  • Class.newInstance() 只能调用零参数构造函数,而 Constructor.newInstance() 可以调用任何构造函数,而不考虑参数的数量
  • Class.newInstance() 抛出构造函数引发的任何异常,无论它是checked还是unchecked。 Constructor.newInstance() 始终用 InvocationTargetException .
  • Class.newInstance() 要求构造函数可见Constructor.newInstance() 在某些情况下可以调用 private 构造函数。

有时可能想要从对象中获取那些仅在构造后设置(初始化)的内部状态(inner state)。考虑一个场景,其中需要获取 java.io.Console 使用的内部字符集。( Console 字符集存储在私有字段中,不一定与 Java 虚拟机返回默认字符集的 java.nio.charset.Charset.defaultCharset()方法相同)。该 ConsoleCharset 示例显示了如何实现此目的:

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

import static java.lang.System.out;

public class ConsoleCharset {
    public static void main(String... args) {
        Constructor[] ctors = Console.class.getDeclaredConstructors();
        Constructor ctor = null;
        for (int i = 0; i < ctors.length; i++) {
            ctor = ctors[i];
            if (ctor.getGenericParameterTypes().length == 0)
                break;
        }

        try {
            ctor.setAccessible(true);
            Console c = (Console) ctor.newInstance();
            Field f = c.getClass().getDeclaredField("cs");
            f.setAccessible(true);
            out.format("Console charset         :  %s%n", f.get(c));
            out.format("Charset.defaultCharset():  %s%n",
                    Charset.defaultCharset());

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

注意:

Class.newInstance() 仅当构造函数具有零参数并且已经可访问时,才会成功。否则,必须 Constructor.newInstance() 使用如上例所示。


UNIX 系统的输出示例:

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

Windows 系统的示例输出:(博主win10)

C:\> java ConsoleCharset
Console charset         :  x-mswin-936
Charset.defaultCharset():  UTF-8

另一个 Constructor.newInstance() 常见的应用是调用接受参数的构造函数。该 RestoreAliases 示例查找特定的单参数构造函数并调用它:

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

import static java.lang.System.out;

class EmailAliases {
    private Set<String> aliases;

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

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

public class RestoreAliases {

    private static Map<String, String> defaultAliases = new HashMap<String, String>();

    static {
        defaultAliases.put("Duke", "duke@i-love-java");
        defaultAliases.put("Fang", "fang@evil-jealous-twin");
    }

    public static void main(String... args) {
        try {
            Constructor ctor = EmailAliases.class.getDeclaredConstructor(HashMap.class);
            ctor.setAccessible(true);
            EmailAliases email = (EmailAliases) ctor.newInstance(defaultAliases);
            email.printKeys();

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

此示例使用 Class.getDeclaredConstructor() 查找具有类型为 java.util.HashMap 的单个参数的构造函数。请注意,传递 HashMap.class 就足够了,因为任何 get*Constructor() 方法的参数只需要一个类(class)用于类型目的(type purposes)。由于类型擦除,以下表达式的计算结果为 true

HashMap.class == defaultAliases.getClass()

然后,该示例使用此构造函数的 Constructor.newInstance() 方法创建该类的新实例。

$ java RestoreAliases
Mail keys:
  Duke
  Fang

疑难解答

开发人员在尝试通过反射调用构造函数时有时会遇到以下问题。

InstantiationException,由于缺少零参数构造函数

ConstructorTrouble 示例说明了当代码尝试使用 Class.newInstance() 创建类的新实例,但没有可访问的零参数构造函数时,会发生什么情况,:

public class ConstructorTrouble {
    private ConstructorTrouble(int i) {
    }

    public static void main(String... args) {
        try {
            Class<?> c = Class.forName("ConstructorTrouble");
            Object o = c.newInstance();  // InstantiationException

            // production code should handle these exceptions more gracefully
        } catch (ClassNotFoundException x) {
            x.printStackTrace();
        } catch (InstantiationException x) {
            x.printStackTrace();
        } catch (IllegalAccessException x) {
            x.printStackTrace();
        }
    }
}
$ java ConstructorTrouble
java.lang.InstantiationException: ConstructorTrouble
	at java.lang.Class.newInstance(Class.java:427)
	at ConstructorTrouble.main(ConstructorTrouble.java:8)
Caused by: java.lang.NoSuchMethodException: ConstructorTrouble.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.newInstance(Class.java:412)
	... 1 more

提示: InstantiationException 可能发生的原因有很多。在这种情况下,问题在于带有 int 参数的构造函数的存在会阻止编译器生成默认(或零参数)构造函数,并且代码中没有显式的零参数构造函数。请记住, Class.newInstance() 的行为与 new关键字非常相似,并且当 new 失败时这个方法也会失败。


Class.newInstance() 引发意外异常(Unexpected Exception)

ConstructorTroubleToo 示例显示了 Class.newInstance() 中无法解决的问题。也就是说,它可以传递构造函数抛出的任何异常(checked或unchecked)。

import java.lang.reflect.InvocationTargetException;

import static java.lang.System.err;

public class ConstructorTroubleToo {
    public ConstructorTroubleToo() {
        throw new RuntimeException("exception in constructor");
    }

    public static void main(String... args) {
        try {
            Class<?> c = Class.forName("ConstructorTroubleToo");
            // Method propagetes any exception thrown by the constructor
            // (including checked exceptions).
            if (args.length > 0 && args[0].equals("class")) {
                Object o = c.newInstance();
            } else {
                Object o = c.getConstructor().newInstance();
            }

            // production code should handle these exceptions more gracefully
        } catch (ClassNotFoundException x) {
            x.printStackTrace();
        } catch (InstantiationException x) {
            x.printStackTrace();
        } catch (IllegalAccessException x) {
            x.printStackTrace();
        } catch (NoSuchMethodException x) {
            x.printStackTrace();
        } catch (InvocationTargetException x) {
            x.printStackTrace();
            err.format("%n%nCaught exception: %s%n", x.getCause());
        }
    }
}
$ java ConstructorTroubleToo class
Exception in thread "main" java.lang.RuntimeException: exception in constructor
	at ConstructorTroubleToo.<init>(ConstructorTroubleToo.java:7)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at java.lang.Class.newInstance(Class.java:442)
	at ConstructorTroubleToo.main(ConstructorTroubleToo.java:16)

这种情况是反射独有的。通常,不可能编写忽略异常检查的代码,因为它不会编译。可以使用 Constructor.newInstance() 而不是 Class.newInstance() 来包装构造函数引发的任何异常。

$ java ConstructorTroubleToo
java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at ConstructorTroubleToo.main(ConstructorTroubleToo.java:18)
Caused by: java.lang.RuntimeException: exception in constructor
	at ConstructorTroubleToo.<init>(ConstructorTroubleToo.java:7)
	... 5 more


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

如果抛出 InvocationTargetException ,则该方法调用成功。问题的诊断与直接调用构造函数并引发异常InvocationTargetException.getCause()获取到的异常相同。此异常并不表示反射包或其使用存在问题。


提示:最好使用 Constructor.newInstance() 代替Class.newInstance() ,因为前面的 API 允许检查和处理构造函数引发的任意异常。


查找或调用正确的构造函数时出现问题

ConstructorTroubleAgain 类演示了不正确的代码无法找到或调用预期构造函数的各种方式。

import java.lang.reflect.InvocationTargetException;

import static java.lang.System.out;

public class ConstructorTroubleAgain {
    public ConstructorTroubleAgain() {
    }

    public ConstructorTroubleAgain(Integer i) {
    }

    public ConstructorTroubleAgain(Object o) {
        out.format("Constructor passed Object%n");
    }

    public ConstructorTroubleAgain(String s) {
        out.format("Constructor passed String%n");
    }

    public static void main(String... args) {
        String argType = (args.length == 0 ? "" : args[0]);
        try {
            Class<?> c = Class.forName("ConstructorTroubleAgain");
            if ("".equals(argType)) {
                // IllegalArgumentException: wrong number of arguments
                Object o = c.getConstructor().newInstance("foo");
            } else if ("int".equals(argType)) {
                // NoSuchMethodException - looking for int, have Integer
                Object o = c.getConstructor(int.class);
            } else if ("Object".equals(argType)) {
                // newInstance() does not perform method resolution
                Object o = c.getConstructor(Object.class).newInstance("foo");
            } else {
                assert false;
            }

            // production code should handle these exceptions more gracefully
        } catch (ClassNotFoundException x) {
            x.printStackTrace();
        } catch (NoSuchMethodException x) {
            x.printStackTrace();
        } catch (InvocationTargetException x) {
            x.printStackTrace();
        } catch (InstantiationException x) {
            x.printStackTrace();
        } catch (IllegalAccessException x) {
            x.printStackTrace();
        }
    }
}
$ java ConstructorTroubleAgain
Exception in thread "main" java.lang.IllegalArgumentException: wrong number of
  arguments
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance
          (NativeConstructorAccessorImpl.java:39)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance
          (DelegatingConstructorAccessorImpl.java:27)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
        at ConstructorTroubleAgain.main(ConstructorTroubleAgain.java:23)

抛出 IllegalArgumentException 是因为请求了零参数构造函数并尝试传递参数。如果向构造函数传递了错误类型的参数,则会引发相同的异常。

$ java ConstructorTroubleAgain int
java.lang.NoSuchMethodException: ConstructorTroubleAgain.<init>(int)
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getConstructor(Class.java:1825)
	at ConstructorTroubleAgain.main(ConstructorTroubleAgain.java:29)

如果开发人员错误地认为反射会自动装箱或拆箱类型,则可能会发生此异常。装箱(将基本类型转换为引用类型)仅在编译期间发生。在反射中没有机会发生此操作,因此在查找构造函数时必须使用特定类型。

$ java ConstructorTroubleAgain Object
Constructor passed Object

在使用newInstance()方法时,若传入了更具体的String类型参数,我们或许会期待能够调用接收String参数的构造器。然而,这已经是不可能的!因为此时找到的构造器已经是那个接收Object类型参数的构造器。newInstance()方法并不会尝试进行方法解析;它仅仅作用于已存在的构造器对象上。

解析说明:构造器的顺序很重要

newInstance()被调用时,即使传入了更具体的类型如String,也不意味着会调用相应的构造器。

一旦通过反射获取了一个构造器对象,这个对象就已经绑定到了特定的构造器签名上。这意味着无论之后newInstance()调用时传入的参数类型是什么,它都将使用最初绑定的那个构造器签名。

newInstance()方法不会动态地解析或匹配构造器。它只是简单地操作已经获取的构造器对象,而不考虑实际传入的参数类型是否更加具体或匹配其他可用的构造器签名。


提示: Constructor.newInstance()new 之间最重要的区别在于执行 new 方法时会对参数类型检查、装箱(拆箱)和方法解析。这些在反射中都不会发生,在反射中必须做出明确的选择。

// 拆箱也是支持的
public ConstructorTroubleAgain(int i) {
    out.println("haha");
}
...
new ConstructorTroubleAgain(new Integer(4));

IllegalAccessException 尝试调用不可访问的构造函数时

如果尝试调用私有(private)构造函数或其他无法访问的构造函数,则可能会引发 IllegalAccessException 。该 ConstructorTroubleAccess 示例说明了生成的堆栈跟踪。

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

class Deny {
    private Deny() {
		System.out.format("Deny constructor%n");
    }
}

public class ConstructorTroubleAccess {
    public static void main(String... args) {
	try {
	    Constructor c = Deny.class.getDeclaredConstructor();
//  	    c.setAccessible(true);   // solution
	    c.newInstance();

        // production code should handle these exceptions more gracefully
	} catch (InvocationTargetException x) {
	    x.printStackTrace();
	} catch (NoSuchMethodException x) {
	    x.printStackTrace();
	} catch (InstantiationException x) {
	    x.printStackTrace();
	} catch (IllegalAccessException x) {
	    x.printStackTrace();
	}
    }
}
$ java ConstructorTroubleAccess
java.lang.IllegalAccessException: Class ConstructorTroubleAccess can not access
  a member of class Deny with modifiers "private"
        at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:65)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:505)
        at ConstructorTroubleAccess.main(ConstructorTroubleAccess.java:15)

提示:在Java中,存在一种访问限制,该限制阻止了反射调用那些通常无法通过直接调用访问的构造器(这包括但不限于独立类中的私有private构造器以及独立私有private类中的公共public构造器)。然而,Constructor类被声明为扩展了AccessibleObject,这提供了通过AccessibleObject.setAccessible()来抑制此类访问检查的能力。


课程:数组和枚举类型

从 Java 虚拟机的角度来看,数组和枚举类型(或枚举)都只是类。 Class 中的许多方法都可以用于它们。反射为数组和枚举提供了一些特定的 API。本课使用一系列代码示例来描述如何将这些对象与其他类区分开来并对其进行操作。还检查了各种错误。

数组

数组具有组件类型和长度(不属于该类型的一部分)。数组可以整体操作,也可以按组件操作。反射为后一个目的提供了 java.lang.reflect.Array

枚举类型

枚举的处理方式与反射代码中的普通类非常相似。 Class.isEnum( 表示一个Class 是否存在并且是 和 enum 类型。 Class.getEnumConstants() 检索枚举中定义的枚举常量。 java.lang.reflect.Field.isEnumConstant()指明字段是否为枚举类型。


数组

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

数组是在 Java 虚拟机中实现的。数组上仅有的方法都是继承自 Object 的方法。数组的长度不是其类型的一部分;数组有一个 可通过 java.lang.reflect.Array.getLength() 访问的length 字段。

反射供了用于访问数组类型和数组组件类型、创建新数组以及获取和设置数组组件值的方法。以下各节包括对数组的常见操作示例:

所有这些操作都可通过java.lang.reflect.Array中的静态方法实现。

识别数组类型

可以通过调用 Class.isArray() 来识别数组类型。若要获取 Class 请使用此文中的“获取类对象( Retrieving Class Objects)”部分中描述的方法之一。

ArrayFind 示例识别指定类中数组类型的字段,并报告每个字段的组件类型。

import java.lang.reflect.Field;
import java.lang.reflect.Type;

import static java.lang.System.out;

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

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

Class.getName()中描述了 Class.get*Type() 返回值的语法。类型名称开头的 ’ [ ’ 表示数组的维数(即嵌套深度)。

image-20240720140708223

输出示例如下。用户输入为斜体。基本类型的 byte 数组:

$java ArrayFind java.nio.ByteBuffer
final byte[] java.nio.ByteBuffer.hb
           Field: hb
            Type: class [B
  Component Type: byte

引用类型的 StackTraceElement 数组:

$ java ArrayFind java.lang.Throwable
private java.lang.StackTraceElement[] java.lang.Throwable.stackTrace
           Field: stackTrace
            Type: class [Ljava.lang.StackTraceElement;
  Component Type: class java.lang.StackTraceElement

predefined 是引用类型 java.awt.Cursor 的一维数组, cursorProperties 是引用类型String 的二维数组:

$ java ArrayFind java.awt.Cursor
protected static java.awt.Cursor[] java.awt.Cursor.predefined
           Field: predefined
            Type: class [Ljava.awt.Cursor;
  Component Type: class java.awt.Cursor
static final java.lang.String[][] java.awt.Cursor.cursorProperties
           Field: cursorProperties
            Type: class [[Ljava.lang.String;
  Component Type: class [Ljava.lang.String;

创建新数组

就像在非反射代码中一样,反射支持通过 java.lang.reflect.Array.newInstance() 动态创建任意类型和维度的数组的能力。考虑 ArrayCreator ,一个能够动态创建数组的基本解释器。将要解析的语法如下:

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

假定 the fully_qualified_class_name 表示具有单个 String 参数的构造函数的类。数组的维数由提供的值数决定。下面的示例将构造一个 fully_qualified_class_name 数组的实例,并使用 val1val2 等给出的实例填充其值(此示例假定熟悉 Class.getConstructor()java.lang.reflect.Constructor.newInstance() 。有关 Constructor 的反射API 的讨论,请参阅此跟踪的“创建新类实例(Creating New Class Instances)”部分。

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

public class ArrayCreator {
    private static String s = "java.math.BigInteger bi[] = { 123, 234, 345 }";
    private static Pattern p = Pattern.compile("^\\s*(\\S+)\\s*\\w+\\[\\].*\\{\\s*([^}]+)\\s*\\}");

    public static void main(String... args) {
        Matcher m = p.matcher(s);

        if (m.find()) {
            String cName = m.group(1);
            String[] cVals = m.group(2).split("[\\s,]+");
            int n = cVals.length;

            try {
                Class<?> c = Class.forName(cName);
                Object o = Array.newInstance(c, n);
                for (int i = 0; i < n; i++) {
                    String v = cVals[i];
                    Constructor ctor = c.getConstructor(String.class);
                    Object val = ctor.newInstance(v);
                    Array.set(o, i, val);
                }

                Object[] oo = (Object[])o;
                out.format("%s[] = %s%n", cName, Arrays.toString(oo));

            // production code should handle these exceptions more gracefully
            } catch (ClassNotFoundException x) {
                x.printStackTrace();
            } catch (NoSuchMethodException x) {
                x.printStackTrace();
            } catch (IllegalAccessException x) {
                x.printStackTrace();
            } catch (InstantiationException x) {
                x.printStackTrace();
            } catch (InvocationTargetException x) {
                x.printStackTrace();
            }
        }
    }
}
$ java ArrayCreator
java.math.BigInteger [] = [123, 234, 345]

上面的例子显示了一种情况,其中可能需要通过反射创建一个数组;也就是说,如果组件类型在运行之前是未知的。在本例中,代码用于 Class.forName() 获取所需组件类型的类,然后调用特定的构造函数来初始化数组的每个组件,然后再设置相应的数组值。

获取和设置数组及其组件

就像在非反射代码中一样,数组字段可以按组件整体或组件进行设置或获取。要一次设置整个数组,请使用 java.lang.reflect.Field.set(Object obj, Object value) 。若要获取整个数组,请使用 Field.get(Object)。可以使用 java.lang.reflect.Array 中的方法设置或获取单个组件。

Array 提供各种形式(int、float)的 setFoo()getFoo()方法用于设置和获取任何基本类型的组件的方法。例如, int 数组的组件可以使用Array.setInt(Object array, int index, int value) 设置 ,也可使用 Array.getInt(Object array, int index)获取 。

这些方法支持自动扩展数据类型。因此,可用于设置 int 数组的值, Array.getShort() 因为 16 位 short 可以扩展到 32 位 int 而不会丢失数据;另一方面, Array.setLong() 调用数 int 组将导致抛出 IllegalArgumentException,因为 64 位 long 无法缩小到 32 位 int 存储而不会丢失信息。无论传递的实际值是否可以在目标数据类型中准确表示,都是如此。 The Java Language Specification, Java SE 7 Edition Widening Primitive ConversionNarrowing Primitive Conversion部分包含了有关基本数据放大和缩小转换的完整讨论。

引用类型数组(包括数组的数组)的组件使用 Array.set(Object array, int index, int value)Array.get(Object array, int index) 进行设置和检索。

设置数组类型的字段

GrowBufferedReader示例展示了如何替换数组类型的字段值。在这个案例中,代码将java.io.BufferedReader的底层数组替换为一个更大的数组。(这假设原始BufferedReader的创建是在不可修改的代码中;否则,只需使用接受输入缓冲区大小的替代构造器BufferedReader(java.io.Reader in, int size)即可轻松实现。)

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

import static java.lang.System.out;

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

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

    private static CharArrayReader car = new CharArrayReader(src);

    public static void main(String... args) {
        try {
            BufferedReader br = new BufferedReader(car);

            Class<?> c = br.getClass();
            Field f = c.getDeclaredField("cb");

            // cb is a private field
            f.setAccessible(true);
            char[] cbVal = char[].class.cast(f.get(br));

            char[] newVal = Arrays.copyOf(cbVal, cbVal.length * 2);
            if (args.length > 0 && args[0].equals("grow"))
                f.set(br, newVal);

            for (int i = 0; i < srcBufSize; i++)
                br.read();

            // see if the new backing array is being used
            if (newVal[srcBufSize - 1] == src[srcBufSize - 1])
                out.format("Using new backing array, size=%d%n", newVal.length);
            else
                out.format("Using original backing array, size=%d%n", cbVal.length);

            // production code should handle these exceptions more gracefully
        } catch (FileNotFoundException x) {
            x.printStackTrace();
        } catch (NoSuchFieldException x) {
            x.printStackTrace();
        } catch (IllegalAccessException x) {
            x.printStackTrace();
        } catch (IOException x) {
            x.printStackTrace();
        }
    }
}
$ java GrowBufferedReader grow
Using new backing array, size=16384
$ java GrowBufferedReader
Using original backing array, size=8192

请注意,上面的示例使用了数组工具方法 java.util.Arrays.copyOf)java.util.Arrays 包含许多方便操作数组的方法。

访问多维数组的元素

多维数组只是嵌套数组。二维数组是数组的数组。三维数组是二维数组的数组,依此类推。该 CreateMatrix 示例说明了如何使用反射创建和初始化多维数组。

import java.lang.reflect.Array;
import static java.lang.System.out;

public class CreateMatrix {
    public static void main(String... args) {
        Object matrix = Array.newInstance(int.class, 2, 2);
        Object row0 = Array.get(matrix, 0);
        Object row1 = Array.get(matrix, 1);

        Array.setInt(row0, 0, 1);
        Array.setInt(row0, 1, 2);
        Array.setInt(row1, 0, 3);
        Array.setInt(row1, 1, 4);

        for (int i = 0; i < 2; i++)
            for (int j = 0; j < 2; j++)
                out.format("matrix[%d][%d] = %d%n", i, j, ((int[][])matrix)[i][j]);
    }
}
$ java CreateMatrix
matrix[0][0] = 1
matrix[0][1] = 2
matrix[1][0] = 3
matrix[1][1] = 4

使用以下代码片段可以获得相同的结果:

Object matrix = Array.newInstance(int.class, 2);
Object row0 = Array.newInstance(int.class, 2);
Object row1 = Array.newInstance(int.class, 2);

Array.setInt(row0, 0, 1);
Array.setInt(row0, 1, 2);
Array.setInt(row1, 0, 3);
Array.setInt(row1, 1, 4);

Array.set(matrix, 0, row0);
Array.set(matrix, 1, row1);

可变参数方法 Array.newInstance(Class componentType, int... dimensions) 提供了一种创建多维数组的便捷方法,但组件仍然需要使用多维数组是嵌套数组的原则进行初始化。(反射不为此提供多个索引的 get / set 方法。)

补充:也就是无法一次性set多个下标

疑难解答

以下示例显示了在数组上操作时可能发生的典型错误。

由于不可转换类型导致的 IllegalArgumentException

ArrayTroubleAgain 示例将生成一个 IllegalArgumentException。调用 Array.setInt()为一个引用类型为 Integer的组件设置基本类型 int 时,在非反射下等同于 ary[0] = 1 ,编译器会将值 1 转换为引用类型 new Integer(1) ,以便其类型检查将接受该语句。而使用反射时,类型检查仅在运行时进行,因此没有机会对值进行装箱(拆箱同理)

import java.lang.reflect.Array;
import static java.lang.System.err;

public class ArrayTroubleAgain {
    public static void main(String... args) {
	Integer[] ary = new Integer[2];
	try {
	    Array.setInt(ary, 0, 1);  // IllegalArgumentException

        // production code should handle these exceptions more gracefully
	} catch (IllegalArgumentException x) {
	    err.format("Unable to box%n");
	} catch (ArrayIndexOutOfBoundsException x) {
	    x.printStackTrace();
	}
    }
}
$ java ArrayTroubleAgain
Unable to box

若要消除此异常,应将有问题的行替换为以下调用 Array.set(Object array, int index, Object value)

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

提示: 使用反射设置或获取数组组件时,编译器没有机会执行装箱/拆箱。它只能转换 Class.isAssignableFrom() 规范中描述的相关类型。该示例预计会失败,因为 isAssignableFrom() 将在此测试中返回 false ,该测试可以通过编程方式用于验证是否可以进行特定转换:

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

同样在反射中,从基本类型到引用类型的自动转换也是不可能的。

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

空数组的 ArrayIndexOutOfBoundsException

ArrayTrouble 示例说明了在尝试访问长度为零的数组的元素时将发生的错误:

import java.lang.reflect.Array;
import static java.lang.System.out;

public class ArrayTrouble {
    public static void main(String... args) {
        Object o = Array.newInstance(int.class, 0);
        int[] i = (int[])o;
        int[] j = new int[0];
        out.format("i.length = %d, j.length = %d, args.length = %d%n",
                   i.length, j.length, args.length);
        Array.getInt(o, 0);  // ArrayIndexOutOfBoundsException
    }
}
$ java ArrayTrouble
i.length = 0, j.length = 0, args.length = 0
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
        at java.lang.reflect.Array.getInt(Native Method)
        at ArrayTrouble.main(ArrayTrouble.java:11)

**提示:**可以有没有元素的数组(空数组)。在普通代码中,只有少数情况可以看到它们,但它们可能会在反射中无意中发生。因为不可能设置/获取空数组的值,所以将抛出一个 ArrayIndexOutOfBoundsException


IllegalArgumentException(如果尝试缩小范围)

ArrayTroubleToo 示例包含失败的代码,因为它尝试执行可能丢失数据的操作:

import java.lang.reflect.Array;
import static java.lang.System.out;

public class ArrayTroubleToo {
    public static void main(String... args) {
        Object o = new int[2];
        Array.setShort(o, 0, (short)2);  // widening, succeeds
        Array.setLong(o, 1, 2L);         // narrowing, fails
    }
}
$ java ArrayTroubleToo
Exception in thread "main" java.lang.IllegalArgumentException: argument type
  mismatch
        at java.lang.reflect.Array.setLong(Native Method)
        at ArrayTroubleToo.main(ArrayTroubleToo.java:9)

提示: Array.set*()Array.get*() 方法将执行自动放大转换,但如果尝试缩小转换,则会抛出一个 IllegalArgumentException 。有关扩大和缩小转换的完整讨论,请分别参见 The Java Language Specification, Java SE 7 EditionWidening Primitive ConversionNarrowing Primitive Conversion 章节。


枚举类型

枚举是一种语言构造,用于定义类型安全枚举(多线程下),当需要一组固定的命名值时,可以使用枚举。所有枚举都隐式扩展 java.lang.Enum 。枚举可以包含一个或多个枚举常量,这些常量定义枚举类型的唯一实例。枚举声明定义了一个枚举类型,该类型与类非常相似,因为它可能具有字段、方法和构造函数等成员(具有一些限制)。

由于枚举是类,因此反射不需要定义一个显式的 java.lang.reflect.Enum 类。唯一特定于枚举的反射 API 包括 Class.isEnum()Class.getEnumConstants()java.lang.reflect.Field.isEnumConstant() 。大多数涉及枚举的反射操作与任何其他类或成员相同。例如,枚举常量作为 public static final 枚举上的字段实现。以下各节介绍如何使用 Classjava.lang.reflect.Field 处理枚举。

有关枚举的简介,请参阅枚举类型(Enum Types)课程。

检查枚举

反射提供了三个特定于枚举的 API:

有时需要动态获取枚举常量列表;在非反射代码中,这是通过在枚举上调用隐式声明的静态方法 values() 来实现的。如果枚举类型的实例不可用,则获取可能值列表的唯一方法是调用, Class.getEnumConstants() 因为无法实例化枚举类型。

给定一个完全限定的名称,该 EnumConstants 示例演示如何使用 Class.getEnumConstants()获取枚举中常量的有序列表。

import java.util.Arrays;
import static java.lang.System.out;

enum Eon { HADEAN, ARCHAEAN, PROTEROZOIC, PHANEROZOIC }

public class EnumConstants {
    public static void main(String... args) {
	try {
	    Class<?> c = (args.length == 0 ? Eon.class : Class.forName(args[0]));
	    out.format("Enum name:  %s%nEnum constants:  %s%n",
		       c.getName(), Arrays.asList(c.getEnumConstants()));
	    if (c == Eon.class)
		out.format("  Eon.values():  %s%n",
			   Arrays.asList(Eon.values()));

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

输出示例如下。用户输入为斜体。

$ java EnumConstants java.lang.annotation.RetentionPolicy
Enum name:  java.lang.annotation.RetentionPolicy
Enum constants:  [SOURCE, CLASS, RUNTIME]
$ java EnumConstants java.util.concurrent.TimeUnit
Enum name:  java.util.concurrent.TimeUnit
Enum constants:  [NANOSECONDS, MICROSECONDS, 
                  MILLISECONDS, SECONDS, 
                  MINUTES, HOURS, DAYS]

此示例还显示,返回 Class.getEnumConstants() 的值与调用 values() 枚举类型返回的值相同。

$ java EnumConstants
Enum name:  Eon
Enum constants:  [HADEAN, ARCHAEAN, 
                  PROTEROZOIC, PHANEROZOIC]
Eon.values():  [HADEAN, ARCHAEAN, 
                PROTEROZOIC, PHANEROZOIC]

由于枚举是类,因此可以使用本篇的“字段”、“方法”和“构造函数”部分中描述的相同反射 API 获取其他信息。该 EnumSpy 代码演示如何使用这些 API 获取有关枚举声明的其他信息。该示例用Class.isEnum() 限制检查的类集。还用 Field.isEnumConstant() 将枚举常量与枚举声明中的其他字段区分开来(并非所有字段都是枚举常量)。

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

import static java.lang.System.out;

public class EnumSpy {
    private static final String fmt = "  %11s:  %s %s%n";

    public static void main(String... args) {
        try {
            Class<?> c = Class.forName(args[0]);
            if (!c.isEnum()) {
                out.format("%s is not an enum type%n", c);
                return;
            }
            out.format("Class:  %s%n", c);

            Field[] flds = c.getDeclaredFields();
            List<Field> cst = new ArrayList<Field>();  // enum constants
            List<Field> mbr = new ArrayList<Field>();  // member fields
            for (Field f : flds) {
                if (f.isEnumConstant())
                    cst.add(f);
                else
                    mbr.add(f);
            }
            if (!cst.isEmpty())
                print(cst, "Constant");
            if (!mbr.isEmpty())
                print(mbr, "Field");

            Constructor[] ctors = c.getDeclaredConstructors();
            for (Constructor ctor : ctors) {
                out.format(fmt, "Constructor", ctor.toGenericString(),
                        synthetic(ctor));
            }

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

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

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

    private static String synthetic(Member m) {
        return (m.isSynthetic() ? "[ synthetic ]" : "");
    }
}
$ java EnumSpy java.lang.annotation.RetentionPolicy
Class:  class java.lang.annotation.RetentionPolicy
     Constant:  public static final java.lang.annotation.RetentionPolicy
                  java.lang.annotation.RetentionPolicy.SOURCE 
     Constant:  public static final java.lang.annotation.RetentionPolicy
                  java.lang.annotation.RetentionPolicy.CLASS 
     Constant:  public static final java.lang.annotation.RetentionPolicy 
                  java.lang.annotation.RetentionPolicy.RUNTIME 
        Field:  private static final java.lang.annotation.RetentionPolicy[] 
                  java.lang.annotation.RetentionPolicy. [ synthetic ]
  Constructor:  private java.lang.annotation.RetentionPolicy() 
       Method:  public static java.lang.annotation.RetentionPolicy[]
                  java.lang.annotation.RetentionPolicy.values() 
       Method:  public static java.lang.annotation.RetentionPolicy
                  java.lang.annotation.RetentionPolicy.valueOf(java.lang.String) 

输出显示声明 java.lang.annotation.RetentionPolicy 仅包含三个枚举常量。枚举常量公开为 public static final 字段。字段、构造函数和方法由编译器生成。该 $VALUES 字段与 values() 方法的实现有关。


注意:由于各种原因,包括支持枚举类型的演变,枚举常量的声明顺序很重要。 Class.getFields()Class.getDeclaredFields()不保证返回值的顺序与声明源代码中的顺序匹配。如果应用程序需要排序,请使用 Class.getEnumConstants()


java.util.concurrent.TimeUnit的输出表明,更复杂的枚举是可能的。此类包括多个方法以及声明 static final 的非枚举常量的其他字段。

$ java EnumSpy java.util.concurrent.TimeUnit
Class:  class java.util.concurrent.TimeUnit
     Constant:  public static final 
...忽略后面N个枚举

获取和设置具有枚举类型的字段

存储枚举的字段将像任何其他引用类型一样设置和检索,使用 Field.set()Field.get() 。有关访问字段的详细信息,请参阅此跟踪的字段部分。

考虑一个需要在运行时动态修改服务器应用程序跟踪级别(TraceLevel)的应用,而通常这种更改在运行时是不允许的。假设已经获得了服务器对象的实例。SetTrace示例演示了代码如何将枚举类型的字符串表示转换为枚举类型,并获取及设置存储枚举的字段的值。

import java.lang.reflect.Field;
import static java.lang.System.out;

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

class MyServer {
    private TraceLevel level = TraceLevel.OFF;
}

public class SetTrace {
    public static void main(String... args) {
	TraceLevel newLevel = TraceLevel.valueOf(args[0]);

	try {
	    MyServer svr = new MyServer();
	    Class<?> c = svr.getClass();
	    Field f = c.getDeclaredField("level");
	    f.setAccessible(true);
	    TraceLevel oldLevel = (TraceLevel)f.get(svr);
	    out.format("Original trace level:  %s%n", oldLevel);

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

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

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

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

在实际项目中动态更改log的日志级别可以使用以下代码

// 日志在类中的定义
private final static Logger log = LoggerFactory.getLogger(XXX.class);

方法一:使用反射(缺点是需要对象)

@Autowired
private XXX xxx;

public void modify() {
    Class<?> c = xxx.getClass();
    Field f = c.getDeclaredField("log");
    f.setAccessible(true);
    Object logInstance = f.get(null); // 因为日志字段通常是static类型的
    if (logInstance instanceof Logger) {
        Method setLevel = logInstance.getClass().getMethod("setLevel", Level.class);
        setLevel.invoke(logInstance, Level.ERROR);
    }
}

方法二:使用日志自带的Context

public void modify() {
    LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
    Logger logger = loggerContext.getLogger(XXX.class);
    logger.setLevel(Level.ERROR);
}

疑难解答

下面的示例演示使用枚举类型时可能遇到的问题。

IllegalArgumentException 尝试实例化枚举类型时

如前所述,枚举类型的实例化是被禁止的。该 EnumTrouble 示例将尝试此操作。

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

import static java.lang.System.out;

enum Charge {
    POSITIVE, NEGATIVE, NEUTRAL;

    Charge() {
        out.format("under construction%n");
    }
}

public class EnumTrouble {

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

            Constructor[] ctors = c.getDeclaredConstructors();
            for (Constructor ctor : ctors) {
                out.format("Constructor: %s%n", ctor.toGenericString());
                ctor.setAccessible(true);
                ctor.newInstance();
            }

            // production code should handle these exceptions more gracefully
        } catch (InstantiationException x) {
            x.printStackTrace();
        } catch (IllegalAccessException x) {
            x.printStackTrace();
        } catch (InvocationTargetException x) {
            x.printStackTrace();
        }
    }
}
$ java EnumTrouble
Constructor: private Charge()
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at EnumTrouble.main(EnumTrouble.java:24)

**提示:**在编译时尝试显式实例化一个枚举类型是错误的,因为这会阻止定义的枚举常量保持唯一性。这一限制同样在反射代码中得到强制执行。在使用默认构造器实例化类的代码应该首先调用Class.isEnum()来判断该类是否为枚举类型。


设置具有不兼容枚举类型的字段时的 IllegalArgumentException

存储枚举的字段应使用相应的枚举类型设置。(实际上,任何类型的字段都必须使用兼容的类型设置。)EnumTroubleToo示例产生了预期的错误。

import java.lang.reflect.Field;

enum E0 { A, B }
enum E1 { A, B }

class ETest {
    private E0 fld = E0.A;
}

public class EnumTroubleToo {
    public static void main(String... args) {
	try {
	    ETest test = new ETest();
	    Field f = test.getClass().getDeclaredField("fld");
	    f.setAccessible(true);
 	    f.set(test, E1.A);  // IllegalArgumentException

        // production code should handle these exceptions more gracefully
	} catch (NoSuchFieldException x) {
	    x.printStackTrace();
	} catch (IllegalAccessException x) {
	    x.printStackTrace();
	}
    }
}
$ java EnumTroubleToo
Exception in thread "main" java.lang.IllegalArgumentException: Can not set E0
  field ETest.fld to E1
        at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException
          (UnsafeFieldAccessorImpl.java:146)
        at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException
          (UnsafeFieldAccessorImpl.java:150)
        at sun.reflect.UnsafeObjectFieldAccessorImpl.set
          (UnsafeObjectFieldAccessorImpl.java:63)
        at java.lang.reflect.Field.set(Field.java:657)
        at EnumTroubleToo.main(EnumTroubleToo.java:16)

提示: 严格来说,尝试将类型为X的字段设置为类型为Y的值,只有在以下语句为真时才能成功:

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

代码可以被修改以执行以下测试,以验证类型是否兼容:

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

反射 API:Trail 的终结

您已经到达了“The Reflection API”路径(Trail)的终点。

如果您对此路线有任何意见或建议,请使用我们的反馈页面告诉我们。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值