Java反射学习笔记(二)

通过反射获取方法信息

除了类,基本类型(int, double, boolean etc)和Java关键字也有其对应的类类型(Class Type):

  • 获取基本类型的类类型(Class Type):
    1
    Class c1 = int.class;
  • 获取关键字的类类型(Class Type):
    1
    Class c2 = void.class;

    tips: Java类外的关键字(如package)没有类类型。extends, implements也没有类类型。

此外,Java还提供了获取类的方法的方法:

  • 获取所有的public方法:
    1
    Method ms1[] = c1.getMethods();
  • 获取所有自己声明的方法(不包括从父类继承来的方法):
    1
    Method ms2[] = c1.getDeclaredMethods();
    Method类提供了获取方法的参数列表的类型对应的类类型(Class Type)的方法,即如果参数类型为int,则返回int.class:
    1
    2
    Method m = ms1[0];
    Class paramTypes[] = m.getParameterTypes();
    还可以获取返回类型的类类型(Class Type):
    1
    Class returnType = m.getReturnType();

    tips: 通过Constructor类可以获取类的构造方法,与Method类的用法一样。

如此,我们可以根据一个对象,获取它的类的所有方法信息(包括private方法, 构造方法)。代码见:github

获取成员变量构造函数信息

成员变量也有对应的类类型(Class Type),用Field表示。

  • 获取所有成员变量:
    1
    Fields fs[] = c.getDeclaredFields();
  • 获取Public的成员变量:
    1
    Fields fs[] = c.getFields();
  • 获取成员变量的信息
    1
    2
    3
    4
    5
    6
    Field field = fs[0];
    Class fieldType = field.getType();
    //获取成员变量的名称
    String fieldName = field.getName();
    //获取成员变量的类型(字符串表示)
    String typeName = fieldType.getName();
    如此,我们可以根据一个对象,获得它的类的所有成员变量。(包括私有变量)

    方法反射的基本操作

    现在,假如Foo类中有个print方法:
    1
    2
    3
    public void print(int a, int b){
    System.out.print(a + b);
    }
    在上一部分中我们知道了如何获取一个类的所有方法。如何获取一个特定的方法呢?Class类提供了getMethod()方法来实现这个需求:
    1
    2
    3
    Foo foo = new Foo();
    Class c = foo.getClass();
    Method method = c.getMethod("print", new Class[]{int.class, int.class});
    这样就获取到了这个方法。同理,可以用getDeclaredMethod()来获取private的方法。
    如果我们想调用foo对象的print方法,正常的思路是:
    1
    foo.print(1,2);
    另一种思路是通过print方法来“调用”foo对象。
    1
    Object o = method.invoke(foo, 1, 2);

    o的类型由返回值的类型决定,如果没有返回值则为null, 否则和返回值类型一样。注意要做强制类型转换。

这两种方法操作的结果是一样的。
方法反射的意义是什么呢?我们知道,通过方法反射我们可以获得private的方法,当我们想调用private的方法时,比如某些私有的构造方法,我们便可以通过调用方法对象的setAccessible方法,使得这些私有方法可以为我们所用。此外,方法反射在框架中的应用较为常见,由于笔者还没有研究过框架源码,所以只展开到这里,有兴趣的读者可以自行研究。

思考

我们知道,面向对象的特性之一就是封装。使用private关键字,是为了将不想暴露在外的变量或者方法对外隐藏起来。而反射却提供了获取私有变量和私有方法的能力,这不是违背了面向对象的封装思想吗?其实,当我们深入研究反射的机制后,会发现并不是如此。为了便于理解,我们暂时把这个问题放到一边,先去思考一个泛型的例子:

1
2
3
4
5
6
7
8
ArrayList list1 = new ArrayList();
ArrayList<String> list2 = new ArrayList<String>();

//list1可以添加任意类型的对象,list2中只能添加String对象
//list1和list2的类类型(Class Type)是否相等呢?
Class c1 = list1.getClass();
Class c2 = list2.getClass();
System.out.print(c1 == c2);

打印结果为true,大家可以自行验证。
这个结果说明,list1和list2编译之后得到的.class文件是相同的,编译之后的集合是去泛型化的。如果我们绕过编译,那么就可以向list2中添加非String对象了。思路还是用method.invoke():

1
2
3
Method method = c2.getMethod("add", Object.class);
method.invoke(list2, 100);

通过打印list2的内容可得知,整数100现在可以加进list2中了。而这是在编译之后进行的操作,对原来的add()方法不造成任何影响。如果你用常规的list2.add()方法,依然不能将非String对象添加进list2中。到这里结论已经出来了,反射给我们提供了一种在运行时动态修改.class文件的能力,并不对源文件造成影响,提高了Java的灵活性。

Author: kpt

Permalink: http://kpt.ink/2019/11/22/java-reflection-2/

文章默认使用 CC BY-NC-SA 4.0 协议进行许可,使用时请注意遵守协议。