反射是什么

Reflection is commonly used by programs which require the ability to examine or modify the runtime behavior of applications running in the Java virtual machine. This is a relatively advanced feature and should be used only by developers who have a strong grasp of the fundamentals of the language. With that caveat in mind, reflection is a powerful technique and can enable applications to perform operations which would otherwise be impossible.

以上是Java Reflection 官方文档的简介。反射可以让我们在程序运行时访问到已加载的类字段,方法,以及构造函数相关信息。当然除了访问权限我们甚至可以执行一些原本不太可能的操作。

反射虽好,要谨慎使用,而且不到无可奈何是不建议使用反射的,毕竟是运行时期的产物,编译器没法对反射相关的代码做优化,所以在性能上会有一定的折损。

为什么需要反射

比如,你有一个未知类型的类,如果存在doSomehing方法的话,你想调用它,但是很显然在没有反射的情况下,JVM加载的类的类型你自己其实都是知道的,所以局限于此是无法操作的,这时候需要借鉴反射来解决:

Method method = foo.getClass().getMethod("doSomething", null);
method.invoke(foo, null);

还有像Java中的注解,都离不开反射!正因如此我们需要反射来弥补实现。接下来我们看看反射具体都可以做些什么。

反射用途

Retrieving Class Objects

所有反射操作的入口点都是java.lang.Class。 除了java.lang.reflect.ReflectPermission之外,java.lang.reflect中的所有类都不具有公共构造函数。 要进入这些类,必须在Class上调用适当的方法。 根据代码是否可以访问对象,类的名称,类型或现有的类,有几种获取类的方法。

getClass()

如果可以获取对象的实例,则获取其Class的最简单方法是调用Object.getClass()。 当然,这仅适用于全部继承自Object的引用类型

System.out.println("test".getClass());//输出:class java.lang.String

.class

这时候也许你就要问了,哎呀那8大基本类型怎么办呢?当然有办法啦,针对这种类型无法取得实例的直接通过.class的方式获取:

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

Class c = boolean.class;  // correct 输出boolean

.class语法是获取原始类型的Class的一种更方便且首选的方式。 但是,还有另一种获取类的方法。每个原始类型和void在java.lang中都有一个包装器类,用于将原始类型装箱到引用类型。 每个包装器类都包含一个名为TYPE的字段,该字段等于要包装的原始类型的Class。

Class c = Double.TYPE;

.forName()

如果我们知道一个类的全限定名称,则可以使用静态方法Class.forName()获得相应的Class。 同样这不能用于基本类型。数组类名称可以通过Class.getName()获取。 此语法适用于引用和原始类型。

Class c = Class.forName("reflectstudy.Father");

如上拿到了Father的class信息。

Class cDoubleArray = Class.forName("[D");

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

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

返回Class对象

Class.getSuperclass()

获取父类Class

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

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

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

c中内容包含了两个公共子类Character.SubsetCharacter.UnicodeBlock,一个枚举java.lang.Character.UnicodeScript

Class.getDeclaredClasses()

返回所有类接口,以及在该类中显式声明的枚举。

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

c中内容包含了两个公共子类Character.SubsetCharacter.UnicodeBlock,一个枚举java.lang.Character.UnicodeScript以及私有类Character.CharacterCache

Class.getDeclaringClass()

如果此Class对象表示的类或接口是另一个类的成员,则返回表示在其中声明该类的类的Class对象。 如果此类或接口不是任何其他类的成员,则此方法返回null。 如果此Class对象表示数组类,原始类型或void,则此方法返回null。例如A类有内部类B,那么通过B.class.getDeclaringClass()方法将获取到A的Class对象。

Class.getEnclosingClass()

getDeclaringClass接近,但是在匿名内部上,getEnclosingClass()可以拿到匿名内部类对应的外部类Class对象,而getDeclaringClass不行。

还有其他的几个获取方法不一一介绍了。

访问类成员

获取字段的Class 方法

Class APIList of members?Inherited members?Private members?
getDeclaredField()nonoyes
getField()noyesno
getDeclaredFields()yesnoyes
getFields()yesyesno

获取方法的Class 方法

Class APIList of members?Inherited members?Private members?
getDeclaredMethod()nonoyes
getMethod()noyesno
getDeclaredMethods()yesnoyes
getMethods()yesyesno

获取构造函数的Class 方法

Class APIList of members?Inherited members?Private members?
getDeclaredConstructor()noN/Ayes
getConstructor()noN/Ano
getDeclaredConstructors()yesN/Ayes
getConstructors()yesN/Ano

注意:构造函数是不可继承的

示例:

//获取所有 public 访问权限的变量
//包括本类声明的和从父类继承的
Field[] fields = Son.class.getFields();

//获取所有本类声明的变量
Field[] fields = mClass.getDeclaredFields();
//获取所有 public 访问权限的方法
//包括自己声明和从父类继承的
Method[] mMethods = Son.class.getMethods();

//获取所有本类的的方法
Method[] mMethods = mClass.getDeclaredMethods();
//获取所有 public 访问权限的构造函数
Constructor[] mConstructor = Son.class.getConstructors();

//获取所有本类的的构造函数
Constructor[] mConstructor =Son.class.getDeclaredConstructors();

以上几个方法我们发现了原来反射还有这么多个我们需要探索的类啊,Field、Method、Constructor,接下来我们就来研究研究这几个类!

Field

获取Field类型

字段分为原始类型或引用类型,还有八种基本类型。引用类型指的是java.lang.Object的所有子类,包括接口,数组和枚举类型。

官方文档示例:

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

在java命令带上args执行输出:

$ 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

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

Field.getGenericType()将在类文件中查询Signature Attribute(如果存在)。 如果该属性不存在,它会退回到Field.getType()上,因为引入泛型并没有更改该属性。

我们在本例中大致知道如何获取一个Field的类型,单单只获取属性可能还无法满足我们的要求,我们希望能够把修饰符一起查出来。

获取修饰符

  • 访问修饰符: public, protected, private
  • 特定于字段的修饰符控制运行时行为:transient, volatile
  • 仅限一个实例的修饰符:static
  • 禁止修改值的修饰符:final
  • 注解

我们既然知道了修饰符有这么多,那么如何拿到这些修饰符嘞?如果在上面行文有注意的话会发现一个getModifiers()的方法,同样在Field类中也有这个方法啦。

public class FieldModifierSpy {
    volatile int share;
    int instance;

    public static void main(String... args) throws ClassNotFoundException {
        Class<?> c = Class.forName("reflectstudy.FieldModifierSpy");
        Field[] f = c.getDeclaredFields();
        for (Field field : f) {
            System.out.println(field.getName() + ":" + Modifier.toString(field.getModifiers()));
        }

    }
}

输出

share:volatile
instance:

当然了仅仅只是拿到属性,修饰符很多时候只能帮助我们来判断,这么强大的功能能不能帮助我们实现修改某个field的值呢?

当然可以了!

获取/重新赋值

官方示例:

package reflectstudy;

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

import static java.lang.System.out;

/**
 * @author chenly
 * @create 2020-05-30 10:44
 */
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));

         OtherBook otherBook=new OtherBook();

         otherBook.setName("Java study");
         otherBook.setContent("Java is a great language");

         Class<?> otherBookClass =otherBook.getClass();
         Field name=otherBookClass.getDeclaredField("name");
         //如果以下这行被注释,那么会抛异常
         //name.setAccessible(true);
         out.println(name.get(otherBook));


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

输出:

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

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

现在我们来关注一下Field的安全性问题,在上面的代码我们关注以下的代码:

OtherBook otherBook=new OtherBook();

otherBook.setName("Java study");
otherBook.setContent("Java is a great language");

Class<?> otherBookClass =otherBook.getClass();
Field name=otherBookClass.getDeclaredField("name");
//如果以下这行被注释,那么会抛异常
//name.setAccessible(true);
out.println(name.get(otherBook));

我们定义了另一个类OtherBook,试图访问OtherBook的私有变量

package reflectstudy;

/**
 * @author chenly
 * @create 2020-05-30 10:57
 */
public class OtherBook {
    private String name;

    private String content;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

setAccessible方法可以取消Java的权限控制检查如果没有设置为true,那么私有变量是无法被访问的!会抛出如下的异常

java.lang.IllegalAccessException: Class reflectstudy.Book can not access a member of class reflectstudy.OtherBook with modifiers "private"
    at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
    at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
    at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
    at java.lang.reflect.Field.get(Field.java:390)
    at reflectstudy.Book.main(Book.java:51)

所以为了想要访问到私有成员变量,就需要设置为true!同样修改的话也是需要设置为true

out.format(fmt, "before", "name", otherBook.getName());
name.set(otherBook, "new Java Study");
out.format(fmt, "after", "name", name.get(otherBook));

输出:

BEFORE:  name         = Java study
 AFTER:  name         = new Java Study

一般来说我们做了如此僭越之举,必须要圆回去即在完成我们获取或者修改Field操作之后需要将其重新设置为setAccessible(false),保证其安全性。

Method

获取方法类型信息

这个其实和Field的方法差不多

package reflectstudy;

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

import static java.lang.System.out;

/**
 * @author chenly
 * @create 2020-05-30 11:35
 */
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) {
        Method[] allMethods = MethodSpy.class.getDeclaredMethods();
        for (Method m : allMethods) {
            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]);
            }
            out.println("----------------------");
        }
    }
}

输出

public static void reflectstudy.MethodSpy.main(java.lang.String[])
              ReturnType: void
       GenericReturnType: void
           ParameterType: class [Ljava.lang.String;
    GenericParameterType: class [Ljava.lang.String;
----------------------
<E> void reflectstudy.MethodSpy.genericThrow() throws E
              ReturnType: void
       GenericReturnType: void
           ExceptionType: class java.lang.RuntimeException
    GenericExceptionType: E
----------------------

当我们拿到方法信息之后,我们自然而然想能不能像Field那样能够操作这个方法呢?

当然可以!

执行方法

package reflectstudy;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * @author chenly
 * @create 2020-05-30 11:48
 */
public class MethodInvoke {
    private final List<String> list = new ArrayList();

    private void add(String name) {
        list.add("baidu");
        list.add("tencent");
        list.add(name);
    }

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException,
            IllegalAccessException {
        Method method = MethodInvoke.class.getDeclaredMethod("add", String.class);
        MethodInvoke obj = new MethodInvoke();
        method.invoke(obj,"bilibili");
        System.out.println(obj.list);
    }
}
//输出 [baidu, tencent, bilibili]

以上是一个非常刻意的小例子,在field中我们提到了安全性,那么在Method是否也有这个问题呢?

package reflectstudy;

/**
 * @author chenly
 * @create 2020-05-30 11:54
 */
public class OtherMethodInvoke {
    private String getUpperName(String name){
        return name.toUpperCase();
    }
}

MethodInvoke的main方法下试图访问OtherMethodInvokegetUpperName私有方法

OtherMethodInvoke otherMethodInvoke=new OtherMethodInvoke();
Method otherMethod=otherMethodInvoke.getClass().getDeclaredMethod("getUpperName",String.class);
//otherMethod.setAccessible(true);
Object upper=otherMethod.invoke(otherMethodInvoke,"dilidilixiu");
System.out.println(upper);

不出所料,抛异常了:

Exception in thread "main" java.lang.IllegalAccessException: Class reflectstudy.MethodInvoke can not access a member of class reflectstudy.OtherMethodInvoke with modifiers "private"
    at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
    at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
    at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
    at java.lang.reflect.Method.invoke(Method.java:491)
    at reflectstudy.MethodInvoke.main(MethodInvoke.java:32)

取消注释之后就能够执行,同样别忘了执行之后调用setAccessible方法设置为false。

Constructor

获取构造函数

我们在上面获取构造函数的Class 方法小节中提到了几个方法,这里不再赘述。具体使用见下节。

创建实例

最常见的操作莫过于我们可以通过反射来创建实例,有两种方式 :java.lang.reflect.Constructor.newInstance()Class.newInstance()

package reflectstudy;

import java.util.Map;

/**
 * @author chenly
 * @create 2020-05-30 15:32
 */
public class EmailAliases {
   public EmailAliases() {
   }

   public EmailAliases(Map<String, String> keyValue) {
      this.keyValue = keyValue;
   }

   private EmailAliases(String name) {
      this.name = name;
   }

   public EmailAliases(String name, Map<String, String> keyValue) {
      this.name = name;
      this.keyValue = keyValue;
   }

   private String name;

   private Map<String, String> keyValue;

   public String getName() {
      return name;
   }

   public void setName(String name) {
      this.name = name;
   }

   public Map<String, String> getKeyValue() {
      return keyValue;
   }

   public void setKeyValue(Map<String, String> keyValue) {
      this.keyValue = keyValue;
   }
}
package reflectstudy;

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

/**
 * @author chenly
 * @create 2020-05-30 15:34
 */
public class FindConstructor {
   public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException,
         InvocationTargetException, InstantiationException, ClassNotFoundException {
      Constructor ctor = EmailAliases.class.getDeclaredConstructor(String.class);
      ctor.setAccessible(true);
      EmailAliases email = (EmailAliases)ctor.newInstance("小明");
      ctor.setAccessible(false);
      System.out.println(email.getName());

      Constructor ctorMap = EmailAliases.class.getDeclaredConstructor(Map.class);
      Map<String,String> map=new HashMap<>();
      map.put("key","value");
      map.put("test","hello");
      EmailAliases mapEmail = (EmailAliases)ctorMap.newInstance(map);
      System.out.println(mapEmail.getKeyValue());


      EmailAliases emailOfNoneConstructor =(EmailAliases)Class.forName("reflectstudy.EmailAliases").newInstance();
      emailOfNoneConstructor.setName("hello");

   }
}

特殊的数组与枚举反射

对于JVM来说,万物皆类,所以Class的很多方法对他们都是有效的,反射为数组和枚举提供了一些特定的API。

数组

验证数组

想要验证一个field是不是一个数组,我们可以通过isArray()来判断。

   Class<?> cls = Class.forName("java.nio.ByteBuffer");
   Field[] flds = cls.getDeclaredFields();
   for (Field f : flds) {
      Class<?> c = f.getType();
      if (c.isArray()) {
         //do something
      }
   }

   // production code should handle this exception more gracefully

}

创建数组/赋值

跟非反射的使用很类似,创建实例,再往里面塞值,只是要指定位置以及实例对象。

public static void main(String[] args) throws ClassNotFoundException {
        Class<?> cls = Class.forName("java.lang.String");
        Object array = Array.newInstance(cls, 10);
        //往数组里添加内容
        Array.set(array, 0, "Golang");
        Array.set(array, 1, "Java");
        Array.set(array, 2, "Python");
        Array.set(array, 3, "Scala");
        Array.set(array, 4, "JavaScript");
        //获取某一项的内容
        System.out.println(Array.get(array, 3));
    }

如果遇到多维数组,需要拆分成一维数组进行赋值,例如

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

枚举

验证枚举

跟数组的验证是一样的,提供了一个API:Class.isEnum()

获取枚举值/赋值

getEnumConstants获取枚举值,使用Field.set()和Field.get(),可以获取枚举以及设置枚举类型的值。

package reflectstudy;

import com.sun.deploy.trace.TraceLevel;

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

import static java.lang.System.out;
import static reflectstudy.Eon.HADEAN;

/**
 * @author chenly
 * @create 2020-05-30 16:13
 */

enum Eon {HADEAN, ARCHAEAN, PROTEROZOIC, PHANEROZOIC}

class MyServer {
    private Eon level = Eon.HADEAN;
}

public class EnumConstants {
    Eon econEnum = HADEAN;

    public static void main(String... args) throws ClassNotFoundException, NoSuchFieldException,
            IllegalAccessException {
        Class<?> c = Class.forName("reflectstudy.Eon");
        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()));
        }

        out.println("判断是否为枚举类型:" + EnumConstants.class.getDeclaredField("econEnum")
                .isEnumConstant());

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

        f.set(svr, Eon.PHANEROZOIC);
        out.format("    New  Eon:  %s%n", f.get(svr));

    }
}

输出

Enum name:  reflectstudy.Eon
Enum constants:  [HADEAN, ARCHAEAN, PROTEROZOIC, PHANEROZOIC]
  Eon.values():  [HADEAN, ARCHAEAN, PROTEROZOIC, PHANEROZOIC]
判断是否为枚举类型:false
Original Eon:  HADEAN
    New  Eon:  PHANEROZOIC

isEnumConstant如果你细心的话可能会发现这个方法,你在类里怎么用貌似都无法得到true,因为你需要在一个枚举类里使用,获取枚举类型的field,那么他才会是true。

结语

反射是双刃剑,它可以突破Java的访问权限,做一些奇妙的事,但是也可能引起严重安全问题,在使用反射的时候你需要清楚地知道自己在做什么。在能够不使用反射尽量不使用,JVM帮助我们优化了很多操作,性能相当优秀,但是在反射这种在运行时处理的无法得到JVM的照拂,所以追求极致性能的话要特别注意!

参照

https://docs.oracle.com/javase/tutorial/reflect/TOC.html