Java反射机制浅析

概念
  Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

  Class类与java.lang.reflect类库一起对反射的概念进行了支持,该类库包含了Field、Method以及Constructor类(每个类都实现了Member接口)。这些类型的对象是由JVM在运行时创建的,用以表示未知类里对应的成员。这样开发人员就可以使用Constructor创建新的对象,用get()和set()方法读取和修改与Field对象关联的字段,用invoke()方法调用与Method对象关联的方法。另外,还可以调用getFields()、getMethods()、getConstructors()等很便利的方法,以返回表示字段、方法以及构造器的对象的数组。这样,匿名对象的类信息就能在运行时被完全确认下来,而在编译时不需要知道任何事情。

Class 

  类是程序的一部分,每个类都有一个Class对象。换言之,每当编写并且编译一个新类,就会产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中),它包含了与类有关的信息。为了生成这个类的对象,运行这个对象的虚拟机(JVM)将使用被称为“类加载器(ClassLoader)”的子系统。下面测试类的一些最基本信息。

public class ClassInfo {
 
    /**
    * @description 输出不同格式类名
    * @param clazz
    */
    public static void printName(Class clazz) {
        System.out.println(“getName: ” + clazz.getName());
        System.out.println(“getCanonicalName: ” + clazz.getCanonicalName());
        System.out.println(“getSimpleName: ” + clazz.getSimpleName());
    }
   
    /**
    * @description 输出类的父类和接口
    * @param clazz
    */
    public static void printClassIntf(Class clazz) {
        Class superClass = clazz.getSuperclass();
        Class[] interfaces = clazz.getInterfaces();
        if(superClass != null) {
            System.out.print(clazz.getSimpleName() + ” extends ” + superClass.getSimpleName());
        }
        if(interfaces.length > 0) {
            System.out.print(” implements “);
            for(int i = 0; i < interfaces.length - 1; i++) {
                System.out.print(interfaces[i].getSimpleName() + “, “);
            }
            System.out.println(interfaces[interfaces.length – 1].getSimpleName());
        }
    }
}

测试类 测试用例:ArrayList

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
 
public class ClassInfoTest {
   
    private Class clazz;
   
    private String className = “java.util.ArrayList”;
   
    /**
    * forName()是获取Class对象的引用的一种方法。
    * 它是用一个包含目标类的文本名的String作输入参数,返回的是一个Class对象的引用。
    */
    @Before
    public void before() {
        try {
            clazz = Class.forName(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
   
    @After
    public void after() {
        clazz = null;
    }
   
    @Test
    public void testGetName() {
        ClassInfo.printName(clazz);
    }
   
    @Test
    public void testPrintClassIntf() {
        ClassInfo.printClassIntf(clazz);
    }
 
}

测试结果

getName: java.util.ArrayList
getCanonicalName: java.util.ArrayList
getSimpleName: ArrayList
ArrayList extends AbstractList implements List, RandomAccess, Cloneable, Serializable

Constructor
  Constructor类是对Java普通类中的构造器的抽象。通过Class类的getConstructors()方法可以取得表示构造器的对象的数组,通过getConstructor(Class… parameterTypes)可以取得指定参数类型的构造器。通过newInstance(Object… initargs)方法可以构建一个实例对象。需要注意的是:newInstance()方法的参数要和getConstructor()方法参数相对应。例如 getConstructor(String.class) — getInstance(“Jack”)。

  以下的测试都是假设我们从磁盘上或者网络中获取一个类的字节,得知这个类的包名(reflcet)和类名(Reflect)和相关字段名称和方法名称,并通过热加载已经加载到工程中。

  method.invoke()会在下文Method中讲到。

 待测类

package reflect;
 
/**
 * @description 运行时获取的类
 * @author Administrator
 */
public class Reflect {
   
    public int id;
    private String name;
   
    public Reflect() {
        this.name = “Tom”;
    }
   
    public Reflect(String name) {
        this.name = name;
    }
   
    public Reflect(int id, String name) {
        this.id = id;
        this.name = name;
    }
   
    public void setName(String name) {
        this.name = name;
    }
   
    public String getName() {
        return name;
    }
   
    @SuppressWarnings(“unused”)
    private void setId(int id) {
        this.id = id;
    }
   
    public int getId() {
        return id;
    }
   
    @Override
    public String toString() {
        return “id:” + id + “, name:” + name;
    }
 
}

 测试类

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
 
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
 
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
 
public class ReflectTest {
   
    Class clazz;
   
    /**
    * className = “包名.类名”
    */
    String className = “reflect.Reflect”;
   
    @Before
    public void before() {
        try {
            clazz = Class.forName(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
   
    @After
    public void after() {
        clazz = null;
    }
   
    @Test
    public void testConstructor() {
        try {
            /**
            * 获取无参构造器
            */
            Constructor constructor = clazz.getConstructor();
            Object obj = constructor.newInstance();
            Method method = clazz.getMethod(“getName”);
            assertThat((String)method.invoke(obj), containsString(“Tom”));
            /**
            * 获取带参构造器
            */
            constructor = clazz.getConstructor(String.class);
            obj = constructor.newInstance(“Jack”);
            assertThat((String)method.invoke(obj), containsString(“Jack”));
            /**
            * 获取多个参数构造器
            */
            constructor = clazz.getConstructor(int.class, String.class);
            obj = constructor.newInstance(6, “Rose”);
            method = clazz.getMethod(“toString”);
            assertThat((String)method.invoke(obj), allOf(containsString(“id:6”), containsString(“name:Rose”)));
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
       
    }
   
}

Field

  Field类是对Java普通类中的属性或者称字段的抽象。通过Class类的getFields()方法可以取得表示字段的对象的数组,通过getField(String name)获取给定名称的字段的对象,如果字段修饰符为private或protected,则getField()方法会抛出java.lang.NoSuchFieldException异常。对于非公有的属性的设定,可以使用getDeclaredField()方法,并调用setAccessible(true),使属性可获得。

测试方法

@Test
public void testField() {
    try {
        /**
        * Class类的newInstance()方法会调用默认构造函数创建一个实例对象
        */
        Object obj = clazz.newInstance();
        Method method = clazz.getMethod(“getName”);
        assertThat((String)method.invoke(obj), containsString(“Tom”));
        /**
        * 设定private属性的值
        */
        Field field = clazz.getDeclaredField(“name”);
        field.setAccessible(true);
        field.set(obj, “Jack”);
        assertThat((String)method.invoke(obj), containsString(“Jack”));
        /**
        * 设定public属性的值
        */
        field = clazz.getField(“id”);
        field.setInt(obj, 9);
        method = clazz.getMethod(“getId”);
        assertThat(String.valueOf(method.invoke(obj)), containsString(“9”));
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (SecurityException e) {
        e.printStackTrace();
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    }
}

Method

  Method类是对Java普通类中的方法的抽象。通过Class类的getMethods()方法可以取得表示方法的对象的数组,通过getMethod(String name, Class… parameterTypes)方法可以取得指定方法名称以及方法参数类型的方法的对象,如果方法修饰符为private或protected,getMethod()方法会抛出java.lang.NoSuchMethodException异常。对于非公有的方法,可以通过getDeclaredMethod()方法,并调用setAccessible(true),使方法可获得。调用method.invoke(Object obj, Object… args)方法,实现obj对象对方法method的调用,参数为args。和构造器同样的道理,getMethod()方法和invoke方法要相对应。例如getMethod(“setName”, String.class) — invoke(obj, “Rose”)。

测试方法

@Test
public void testMethod() {
    try {
        /**
        * 调用无参公有方法
        */
        Object obj = clazz.newInstance();
        Method method1 = clazz.getMethod(“getName”);
        assertThat((String)method1.invoke(obj), containsString(“Tom”));
        /**
        * 调用带参公有方法
        */
        Method method2 = clazz.getMethod(“setName”, String.class);
        method2.invoke(obj, “Jack”);
        assertThat((String)method1.invoke(obj), containsString(“Jack”));
        /**
        * 调用带参私有方法
        */
        Method method3 = clazz.getDeclaredMethod(“setId”, int.class);
        method3.setAccessible(true);
        method3.invoke(obj, 5);
        Method method = clazz.getMethod(“getId”);
        assertThat(String.valueOf(method.invoke(obj)), containsString(“5”));
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (SecurityException e) {
        e.printStackTrace();
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
}

类方法提取器
  当开发者学习一个类(比如:ArrayList)时,通过浏览实现了类定义的源代码或是其JDK文档,只能找到在这个类定义中被定义或被覆盖的方法。但对开发者来说,可能有数十个更有用的方法都是继承自基类的。要找出这些方法可能很乏味且费时。幸运的是,反射机制提供了一个方法,使开发者能够编写可以自动展示完整接口的简单工具。工作方式如下:

/**
 * @description 类方法提取器
 * @param clazz
 */
public static void printClassInfo(Class clazz) {
    Pattern pattern = Pattern.compile((“\\w+\\.”));
    Constructor[] constructors = clazz.getConstructors();
    Method[] methods = clazz.getMethods();
    Field[] fields = clazz.getDeclaredFields();
    for(Field field : fields) {
        System.out.println(pattern.matcher(field.toGenericString()).replaceAll(“”));
    }
    for(Constructor constructor : constructors) {
        System.out.println(pattern.matcher(constructor.toGenericString()).replaceAll(“”));
    }
    for(Method method : methods) {
        System.out.println(pattern.matcher(method.toGenericString()).replaceAll(“”));
    }
}

 测试方法 测试用例:ArrayList

@Test
public void testPrintClassInfo() {
    try {
        ClassInfo.printClassInfo(Class.forName(“java.util.ArrayList”));
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

 测试结果:

private static final long serialVersionUID
private static final int DEFAULT_CAPACITY
private static final Object[] EMPTY_ELEMENTDATA
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA
transient Object[] elementData
private int size
private static final int MAX_ARRAY_SIZE
public ArrayList(Collection)
public ArrayList()
public ArrayList(int)
public boolean add(E)
public void add(int,E)
public boolean remove(Object)
public E remove(int)
public E get(int)
public Object clone()
public int indexOf(Object)
public void clear()
public boolean contains(Object)
public boolean isEmpty()
public Iterator iterator()
public int lastIndexOf(Object)
public void replaceAll(UnaryOperator)
public int size()
public List subList(int,int)
public T[] toArray(T[])
public Object[] toArray()
public Spliterator spliterator()
public boolean addAll(int,Collection)
public boolean addAll(Collection)
public void forEach(Consumer)
public E set(int,E)
public void ensureCapacity(int)
public void trimToSize()
public ListIterator listIterator(int)
public ListIterator listIterator()
public boolean removeAll(Collection)
public boolean removeIf(Predicate)
public boolean retainAll(Collection)
public void sort(Comparator)
public boolean equals(Object)
public int hashCode()
public String toString()
public boolean containsAll(Collection)
public final void wait() throws InterruptedException
public final void wait(long,int) throws InterruptedException
public final native void wait(long) throws InterruptedException
public final native Class getClass()
public final native void notify()
public final native void notifyAll()
public default Stream stream()
public default Stream parallelStream()

JVM
  反射机制并没有什么神奇之处。当通过反射与一个未知类型的对象打交道时,JVM只是简单的检查这个对象。因此,那个类的.class文件对于JVM来说必须是可获取的:要么在本地机器上,要么可以通过网络取得。对已反射机制来说,.class文件在编译时时不可获取的 所以是在运行时打开和检查.class文件。

应用
  假设开发者从磁盘文件,或者网络连接中获取一串字节,并且被告知这些字节代表了一个类。既然这个类在程序编译后很久才出现,若想使用这个类,就需要采用发射机制。热加载就属于这种场景。

  在运行时获取类的信息的另一个场景,开发者希望提供在跨网络的远程平台上创建和运行对象的能力。这被称为远程方法调用(RMI) 它允许一个Java程序将对象分布到多台机器上。

  在自己写框架时候,开发者肯定会用到反射,很简单的例子就是事件总线和注解框架。

总结

  反射很灵活,在日常开发中,慎用少用反射,反射会牺牲部分性能。在写框架时,不避讳反射,在关键时利用反射助自己一臂之力。