..

🧙 Reflection: the black magic of Java

反射(Reflection)是Java语言的一个特性。它允许一个运行中的Java程序来自检,操作程序内部的属性。比如,Java类可以获得所有成员的名字,并且展示它们。

反射的一个实际应用是JavaBeans,使用它可以通过一个构建工具可视化地操作一个软件的组件。这个工具使用反射来在动态加载的过程中获得Java组件(类)的属性。

一个简单的例子

为了展示反射是如何工作的,考虑下面这个简单的例子:

import java.lang.reflect.*;

public class DumpMethods {
  public static void main(String args[])
  {
     try {
        Class c = Class.forName(args[0]);
        Method m[] = c.getDeclaredMethods();
        for (int i = 0; i < m.length; i++)
        System.out.println(m[i].toString());
     }
     catch (Throwable e) {
        System.err.println(e);
     }
  }
}

输出:

public boolean java.util.Stack.empty() public synchronized java.lang.Object java.util.Stack.peek() public synchronized int java.util.Stack.search(java.lang.Object) public java.lang.Object java.util.Stack.push(java.lang.Object) public synchronized java.lang.Object java.util.Stack.pop()

输出的是java.util.Stack的方法名,以及方法的参数类型和返回值。

这段程序使用class.forName加载了指定的类,然后调用getDeclaredMethods方法来获得这个类定义的方法的一个列表。java.lang.reflect.Method是一个类,它代表一个类方法。

开始使用反射

反射相关的类,比如Method,在java.lang.reflect中。为了使用这些类,总共需要三步。

第一步:获得你想要操纵的类的一个java.lang.Class对象。java.lang.Class用来代表一个运行中的Java程序的类或者接口。

一种获得Class对象的方式是使用Class.forName方法。

Class c = Class.forName("java.lang.String");

另一种方式是使用Class c = int.class;或者Class c = Integer.TYPE;来获得基本类型的类信息。

第二步:调用getDeclaredMethods之类的方法,获得这个类的所有方法的一个列表。

第三步:使用反射API来操作第二步获得的信息。

模拟instanceOf

一旦我们拿到了类相关的信息,那么接下来就可以问Class对象一些基本的问题。比如,Class.isInstance方法就可以用来模拟instanceof运算符。

public class ReflectTest {
    public static void main(String[] args) {
        try {
            Class<?> c = Class.forName(args[0]);
            System.out.println(c.isInstance(0));
            System.out.println(c.isInstance(new ArrayList<>()));
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

编译运行:

javac ReflectTest.java java ReflectTest java.util.Stack false false java ReflectTest java.util.ArrayList false true

得到一个类的方法

反射的一个最基础,也是最重要的用法是用来得到一个类中定义的所有方法。下面是一个例子:

程序首先使用Class.forName获得了A类的一个Class对象,然后通过这个对象拿到了A类的方法(只有一个,也就是f1)。

如果你使用的是getMethods而不是getDeclaredMethods,那么你也可以得到继承自父类的方法。

一旦拿到一个Method对象的列表,展示每个方法的参数类型、异常类型、返回值类型就是小菜一碟了。所有的这些类型,无论是基础类型还是非基础类型,都是用一个Class来表示。

public class ReflectTest {
    public static void main(String[] args) {
        try {
            Class<?> c = Class.forName("ReflectTest$A");

            Method[] methods = c.getDeclaredMethods();
            for (Method m : methods) {
                System.out.println("name = " + m.getName());
                System.out.println("declaring class  = " + m.getDeclaringClass());
                Class<?>[] parameters = m.getParameterTypes();
                for (int j = 0; j < parameters.length; j++) {
                    System.out.println("parameter " + j + " = " + parameters[j].getName());
                }
                Class<?>[] exceptions = m.getExceptionTypes();
                for (int j = 0; j < exceptions.length; j++) {
                    System.out.println("exception " + j + " = " + exceptions[j].getName());
                }
                Class<?> returnType = m.getReturnType();
                System.out.println("return type = " + returnType.getName());
            }
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    public static class A {
        private int f1(Object p, int x) throws NullPointerException {
            if (p == null) {
                throw new NullPointerException();
            }
            return x;
        }
    }
}

编译运行,得到以下输出:

name = f1 declaring class = class ReflectTest$A parameter 0 = java.lang.Object parameter 1 = int exception 0 = java.lang.NullPointerException return type = int

获得构造函数相关信息

有了反射这个黑魔法,我们可以很简单地获得一个类的构造函数。下面是一个例子。

public class ReflectTest {
    ReflectTest() {
    }

    ReflectTest(int i, double d) {
    }

    public static void main(String[] args) {
        try {
            Class<?> c = Class.forName("ReflectTest");

            Constructor<?>[] constructorList = c.getDeclaredConstructors();
            for (Constructor<?> ct : constructorList) {
                System.out.println("name = " + ct.getName());
                System.out.println("declaring class = " + ct.getDeclaringClass());
                Class<?>[] parameterTypes = ct.getParameterTypes();
                for (int i = 0; i < parameterTypes.length; i++) {
                    System.out.println("param #" + i + " " + parameterTypes[i]);
                }
                Class<?>[] exceptionTypes = ct.getExceptionTypes();
                for (int i = 0; i < exceptionTypes.length; i++) {
                    System.out.println("exception #" + i + " " + exceptionTypes[i]);
                }
            }
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

编译运行,得到以下输出:

name = ReflectTest declaring class = class ReflectTest

name = ReflectTest declaring class = class ReflectTest param #0 int param #1 double

获得类的成员

利用反射,我们还可以获得一个类的所有成员。

整体代码与前一个类似。这里用到的一个新特性是Modifer。这个反射类用来表示类成员的修饰语(modifier),比如private int。modifier本身是用数字来表示的,Modifier.toString用来返回一个modifier的字符串表示。

public class ReflectTest {
    private double d;
    public static final int i = 37;
    String s = "testing";

    public static void main(String[] args) {
        try {
            Class<?> c = Class.forName("ReflectTest");

            Field[] fields = c.getDeclaredFields();
            for (Field fld : fields) {
                System.out.println("name = " + fld.getName());
                System.out.println("type = " + fld.getType());
                int mod = fld.getModifiers();
                System.out.println("modifiers = " + Modifier.toString(mod));
            }
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

输出:

name = d type = double modifiers = private

name = i type = int modifiers = public static final

name = s type = class java.lang.String modifiers =

通过名字调用方法

以上的例子展示了如何获得类的相关信息。除此之外,反射还有一些其他的骚操作。比如,使用名字来调用方法。

下面是一个具体的例子。假设一个程序想要调用add方法,但是知道运行时才知道这件事。也就是,这个方法的名字是在运行时确定的。getMethod用来寻找这个类中的一个方法,这个方法的名字是add,有两个int类型的参数。找到我们想调用的方法之后,我们拿到了一个Method类型的对象,为了调用这个方法,我们需要构造这个类的一个实例和这个方法的参数。

public class ReflectTest {
    public int add(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) {
        try {
            Class<?> c = Class.forName("ReflectTest");
            Class<?>[] partTypes = new Class[2];
            // 注意,这里不要写成Integer.class了
            partTypes[0] = Integer.TYPE;
            partTypes[1] = Integer.TYPE;
            Method method = c.getMethod("add", partTypes);
            ReflectTest classObj = new ReflectTest();
            Object[] argList = new Object[2];
            argList[0] = 3;
            argList[1] = 4;
            Object retObj = method.invoke(classObj, argList);
            Integer retVal = (Integer) retObj;
            System.out.println(retVal);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

创建对象

下面是使用反射创建对象的一个例子。首先类似于通过名字和参数类型获得方法,这里我们通过构造函数的参数类型获得一个Constructor,然后创建构造函数的参数,通过对构造器调用newInstance来创建对象。

public class ReflectTest {
    public ReflectTest() {
    }

    public ReflectTest(int a, int b) {
        System.out.println("a = " + a + ", b = " + b);
    }

    public static void main(String[] args) {
        try {
            Class<?> c = Class.forName("ReflectTest");
            Class<?>[] partTypes = new Class[2];
            // 注意,这里不要写成Integer.class了
            partTypes[0] = Integer.TYPE;
            partTypes[1] = Integer.TYPE;
            Constructor<?> ct = c.getConstructor(partTypes);
            Object[] argList = new Object[2];
            argList[0] = 3;
            argList[1] = 4;
            Object retObj = ct.newInstance(argList);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

改变成员变量的值

反射的另一个骚操作是用来改变对象中的成员变量的值。我们可以通过成员变量的名字,得到一个Field,然后通过Field的set方法来改变成员变量的值。(看到这我都震惊了…感觉反射像是从高维空间伸过来的一只手😂)

public class ReflectTest {
    public double d;

    public static void main(String[] args) {
        try {
            Class<?> c = Class.forName("ReflectTest");
            Field fld = c.getField("d");
            ReflectTest obj = new ReflectTest();
            System.out.println("d = " + obj.d);
            fld.setDouble(obj, 3.14);
            System.out.println("d = " + obj.d);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

使用数组

最后一个骚操作:使用反射来创建并操作数组。Java语言中的数组是一种特殊的类,一个数组的引用可以赋值给一个Object引用。

下面是一个简答的例子。在这个例子中,我们创建了一个string的数组,数组长度是10,然后设置了数组中下标为5的元素。

public class ReflectTest {
    public static void main(String[] args) {
        try {
            Class<?> clazz = Class.forName("java.lang.String");
            Object arr = Array.newInstance(clazz, 10);
            Array.set(arr, 5, "this is a test");
            String s = (String) Array.get(arr, 5);
            System.out.println(s);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

总结

Java反射很有用,因为它支持我们在运行时动态地根据名字来获得类的信息,并且支持在一个运行中的程序中操作它们。这个特性非常牛逼,在其他编程语言,比如C, C++, Fortan中都没有。