java反射全解

9,045 阅读32分钟

引言

java中创建对象有几种方式?

1.使用new关键字

2.使用clone方法

3.使用反序列化

4.使用反射

5.使用Unsafe

关于这几种创建对象方式的详解,请看这篇文章 java创建对象的五种方式

接下来主要详细介绍反射相关知识

反射简介

反射之中包含了一个「反」字,所以想要解释反射就必须先从「正」开始解释。
一般情况下,我们使用某个类时必定知道它是什么类,是用来做什么的。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。

Apple apple = new Apple(); //直接初始化,「正射」
apple.setPrice(4);

上面这样子进行类对象的初始化,我们可以理解为「正」。
而反射则是一开始并不知道我要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。
这时候,我们使用 JDK 提供的反射 API 进行反射调用:

Class clz = Class.forName("com.eft.reflect.Apple");
Method method = clz.getMethod("setPrice", int.class);
Constructor constructor = clz.getConstructor();
Object object = constructor.newInstance();
method.invoke(object, 4);

上面两段代码的执行结果,其实是完全一样的。但是其思路完全不一样,第一段代码在未运行时(编译时)就已经确定了要运行的类(Apple),而第二段代码则是在运行时通过字符串值才得知要运行的类(com.eft.reflect.Apple)。

所以说什么是反射?

反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。

官方定义

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

反射机制很重要的一点就是“运行时”,其使得我们可以在程序运行时加载、探索以及使用编译期间完全未知的 .class 文件。换句话说,Java 程序可以加载一个运行时才得知名称的 .class 文件,然后获悉其完整构造,并生成其对象实体、或对其 fields(变量)设值、或调用其 methods(方法)。

通俗概括

反射就是让你可以通过名称来得到对象的信息(如类,属性,方法等) 的技术

核心

Java反射机制是Java语言被视为“准动态”语言的关键性质。Java反射机制的核心就是允许在运行时通过Java Reflection APIs来取得已知名字的class类的内部信息(包括其modifiers(诸如public, static等等)、superclass(例如Object)、实现interfaces(例如Serializable),也包括fields和methods的所有信息),动态地生成此类,并调用其方法或修改其域(甚至是本身声明为private的域或方法)

功能

  • 在运行时判断任意一个对象所属的类;
  • 在运行时构造任意一个类的对象;
  • 在运行时判断任意一个类所具有的成员变量和方法;
  • 在运行时调用任意一个对象的方法;
  • 生成动态代理

反射原理

类加载的流程:


类加载的完整过程如下:
(1)在编译时,Java 编译器编译好 .java 文件之后,在磁盘中产生 .class 文件。.class 文件是二进制文件,内容是只有 JVM 能够识别的机器码。
(2)JVM 中的类加载器读取字节码文件,取出二进制数据,加载到内存中,解析.class 文件内的信息。类加载器会根据类的全限定名来获取此类的二进制字节流;然后,将字节流所代表的静态存储结构转化为方法区的运行时数据结构;接着,在内存中生成代表这个类的 java.lang.Class 对象。
(3)加载结束后,JVM 开始进行连接阶段(包含验证、准备、解析)。经过这一系列操作,类的变量会被初始化。

要想使用反射,首先需要获得待操作的类所对应的 Class 对象。Java 中,无论生成某个类的多少个对象,这些对象都会对应于同一个 Class 对象。这个 Class 对象是由 JVM 生成的,通过它能够获悉整个类的结构。所以,java.lang.Class 可以视为所有反射 API 的入口点。
反射的本质就是:在运行时,把 Java 类中的各种成分映射成一个个的 Java 对象。

简单例子

通过前面引言-使用反射创建对象的例子,我们了解了使用反射创建一个对象的步骤:

获取类的 Class 对象实例

Class clz = Class.forName("com.eft.reflect.Person");

根据 Class 对象实例获取 Constructor 对象

Constructor constructor = clz.getConstructor();

使用 Constructor 对象的 newInstance 方法获取反射类对象

Object personObj = constructor.newInstance();

而如果要调用某一个方法,则需要经过下面的步骤:

  • 获取方法的 Method 对象
Method setNameMethod = clz.getMethod("setName", String.class);
  • 利用 invoke 方法调用方法
setNameMethod.invoke(personObj, "酸辣汤");

通过反射调用方法的测试代码:

Class clz = Person.class;
Method setNameMethod = clz.getMethod("setName", String.class);
// Person person= (Person) clz.getConstructor().newInstance();
Person person= (Person) clz.newInstance();
setNameMethod.invoke(person, "酸辣汤888");//调用setName方法,传入参数

Method getNameMethod = clz.getMethod("getName", null);
String name= (String) getNameMethod.invoke(person,null);//调用getName方法,获取返回值

System.out.println("name:" +name);

运行结果:
name:酸辣汤888

到这里,我们已经能够掌握反射的基本使用。但如果要进一步掌握反射,还需要对反射的常用 API 有更深入的理解

反射API详解

在 JDK 中,反射相关的 API 可以分为下面3个方面:

一、获取反射的 Class 对象

每一种类型(如:String,Integer,Person...)都会在初次使用时被加载进虚拟机内存的『方法区』中,包含类中定义的属性字段,方法字节码等信息。Java 中使用类 java.lang.Class 来指向一个类型信息,通过这个 Class 对象,我们就可以得到该类的所有内部信息

Class没有公共构造方法。Class对象是在加载类时由Java虚拟机以及通过调用类加载器中的defineClass方法自动构造的。

获取一个 Class 对象的方法主要有以下三种:

使用类字面常量或TYPE字段

  • 类.class,如Person.class
    • 类字面常量不仅可以应用于普通的类,也可以应用于接口、数组以及基本数据类型
    • 这种方式不仅更简单,而且更安全,因为它在编译时就会受到检查,并且根除了对forName方法的调用,所以也更高效,建议使用“.class”的形式
  • Boolean.TYPE,如Integer.TYPE
    • TYPE是基本数据类型的包装类型的一个标准字段,它是一个引用,指向对应的基本数据类型的Class对象

表格两边等价:

boolean.class Boolean.TYPE
char.class Character.TYPE
byte.class Byte.TYPE
short.class Short.TYPE
int.class Integer.TYPE
long.class Long.TYPE
float.class Float.TYPE
double.class Double.TYPE
void.class Void.TYPE

这种方式最直接,但仅能获取到我已知的类的Class对象,也就是工程内用过的类的对象都可以通过类.class方式获取其Class对象,但是这种方式有一个不足就是对于未知的类,或者说不可见的类是不能获取到其Class对象的。

对象.getClass()

如:person.getClass() Java中的祖先类 Object提供了一个方法getClass() 来获取当着实例的Class对象,这种方式是开发中用的最多的方式,同样,它也不能获取到未知的类,比如说某个接口的实现类的Class对象。

API:

public final native Class<?> getClass();

这是一个native方法(一个Native Method就是一个java调用非java代码的接口),并且不允许子类重写,所以理论上所有类型的实例都具有同一个 getClass 方法。

使用:

Integer integer = new Integer(12);
Class clz=integer.getClass();

Class.forName("类全路径")

如:Class.forName("com.eft.xx.Person")
这种方式最常用,可以获取到任何类的Class对象,前提是该类存在,否则会抛出ClassNotFoundException异常。通过这种方式,我们只需要知道类的全路径(完全限定名)即可获取到其Class对象(如果存在的话).

API:

//由于方法区 Class 类型信息由类加载器和类全限定名唯一确定,
//所以想要去找这么一个 Class 就必须提供类加载器和类全限定名,
//这个forName的重载方法允许你传入类加载器和类全限定名来匹配方法区类型信息
//参数说明   name:class名,initialize是否加载static块
public static Class<?> forName(String name, boolean initialize,
                               ClassLoader loader)
    throws ClassNotFoundException{
    ..
    }   

// 这个 forName 方法默认使用调用者的类加载器,将类的.class文件加载到jvm中
//这里传入的initialize为true,会去执行类中的static块
public static Class<?> forName(String className)
    throws ClassNotFoundException {
    return forName0(className, true, ClassLoader.getCallerClassLoader());
}

使用:

Class clz = Class.forName("com.eft.reflect.Person");

二、判断是否为某个类的实例

  • 用 instanceof 关键字
  • 用 Class 对象的 isInstance 方法(Native 方法)
public class InstanceofDemo {
    public static void main(String[] args) {
        ArrayList arrayList = new ArrayList();
        if (arrayList instanceof List) {
            System.out.println("ArrayList is List");
        }
        if (List.class.isInstance(arrayList)) {
            System.out.println("ArrayList is List");
        }
    }
}
//Output:
//ArrayList is List
//ArrayList is List

被不同加载器加载过的类不属于同一种类(即时包名、类名相同),所创建出的对象所属的类也不相同,如下:

ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream("./bean/" + fileName);
                    if (is == null) {
                        return super.loadClass(name);//返回父 类加载器
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (Exception e) {
                    throw new ClassNotFoundException();
                }
            }
        };

Object obj = null;
Class clz=myLoader.loadClass("eft.reflex.bean.Person");
System.out.println("person被自定义类加载器加载了");
obj = clz.newInstance();
System.out.println(obj instanceof Person);


运行结果:
person被自定义类加载器加载完成
person的静态块被调用了
false

原理应该是:jvm会根据instanceof右边的操作符用默认的类加载器去加载该类到方法区,然后根据左操作符对象的对象头中类引用地址信息去查找方法区对应的类,如果找到的类是刚刚加载的类,则结果为true,否则为false。对于这个例子而言,obj对象指向的类在创建对象之前就已经加载到了方法区,而进行instanceof运算时,由于方法区中已经存在的该类并非用此时的默认加载器进行加载,因此jvm认为该类还没有加载,所以右侧操作符指向的类此时才会加载,所以这个例子的结果为false。 如果将括号中的类名改为测试类的类名,结果也是类似的,只不过测试类会在main方法执行之前就会被加载。

三、通过反射获取构造器,并创建实例对象

通过反射创建类对象主要有两种方式:通过 Class 对象的 newInstance() 方法、通过 Constructor 对象的 newInstance() 方法。

第一种:通过 Class 对象的 newInstance() 方法。

  • public T newInstance()
    • 要求被调用的构造函数是可见,否则会抛出IllegalAccessException xxx can not access a member of class eft.reflex.Singleton with modifiers "private"的异常
    • 只能够调用无参的构造函数,即默认的构造函数

第二种:通过 Constructor 对象的 newInstance() 方法
这个操作涉及到的几个api如下:

  • public Constructor<?>[] getConstructors() //获取类对象的所有可见的构造函数
  • public Constructor<?>[] getDeclaredConstructors()//获取类对象的所有的构造函数

注意: 1.getConstructors和getDeclaredConstructors获取的构造器数组无序,所以不要通过索引来获取指定的构造方法 2.getXXXX 与getDeclaredXXXX 区别是,带Declared的方法不会返回父类成员,但会返回私有成员;不带Declared的方法恰好相反下面类似的方法不赘述

  • public Constructor getConstructor(Class<?>... parameterTypes)
    • 获取指定的可见的构造函数,参数为:指定构造函数的参数类型数组
    • 如果该构造函数不可见或不存在,会抛出 NoSuchMethodException 异常

使用举例:

Class p = Person.class;
Constructor constructor1 = p.getConstructor();//获取没有任何参数的构造函数
Constructor constructor = p.getConstructor(String.class,int.class);//获取Person(String name,int age)这个构造函数
  • public Constructor getDeclaredConstructor(Class<?>... parameterTypes)
    • 获取指定的构造函数,参数为:指定构造函数的参数类型数组
    • 无论构造函数可见性如何,均可获取

使用举例:

Class p = Person.class;
Constructor constructor = p.getDeclaredConstructor(String.class,int.class);//获取Person(String name,int age)这个构造函数
  • Constructor的setAccessible和newInstance方法
//关闭访问检查,需要先将此设置为true才可通过反射访问不可见的构造器
//但编译器不允许使用普通的代码该字段,因为仅适用于反射
public void setAccessible(boolean flag) 

//创建对象,使用可变长度的参数,但是在调用构造函数时必须为每一个参数提供一个准确的参量.
public T newInstance(Object ... initargs)
    
使用举例:
//假设Person有个 private Person(String name){}的构造方法
Constructor constructor = Person.class.getConstructor(String.class);
constructor.setAccessible(true);
Person person = (Person)constructor.newInstance("酸辣汤");

使用Constructor创建对象的完整例子:详见上面的使用反射创建对象-调用类对象的构造方法

四、通过反射获取类的属性、方法

使用反射可以获取Class对象的一系列属性和方法,接下来列举下Class类中相关的API

类名

  • public String getName() //获取类全路径名(返回的是虚拟机里面的class的表示)
  • public String getCanonicalName()//获取类全路径名(返回的是更容易理解的表示)
  • public String getSimpleName() //获取不包含包名的类名

那么以上三者区别是?举个栗子
普通类名:

Class clz=Person.class;
System.out.println(clz);
System.out.println(clz.toString());
System.out.println(clz.getName());
System.out.println(clz.getCanonicalName());
System.out.println(clz.getSimpleName());

运行结果:
class reflex.Person
class reflex.Person//Class里面重写了toString方法,并且在里面调用了getName()方法
reflex.Person
reflex.Person
Person

数组:

Class clz=Person[][].class;
System.out.println(clz.getName());
System.out.println(clz.getCanonicalName());
System.out.println(clz.getSimpleName());

运行结果:
[[Lreflex.Person; 
reflex.Person[][]
Person[][]

修饰符

  • public native int getModifiers(); //获取修饰符

修饰符被包装进一个int内,每一个修饰符都是一个标志位(置位或清零)。可以使用java.lang.reflect.Modifier类中的以下方法来检验修饰符:

Modifier.isAbstract(int mod)
    Modifier.isFinal(int mod)
    Modifier.isInterface(int mod)
    Modifier.isNative(int mod)
    Modifier.isPrivate(int mod)
    Modifier.isProtected(int mod)
    Modifier.isPublic(int mod)v
    Modifier.isStatic(int mod)
    Modifier.isStrict(int mod)//如果mod包含strictfp(strict float point (精确浮点))修饰符,则为true; 否则为:false。
    Modifier.isSynchronized(int mod)
    Modifier.isTransient(int mod)
    Modifier.isVolatile(int mod)

使用举例:

Class clz= Person.class;
int modifier = clz.getModifiers();
System.out.println("修饰符是否为public:" + Modifier.isPublic(modifier));

运行结果:
true

Modifier内部用&运算做判断,如

public static boolean isPublic(int var0) {
    return (var0 & 1) != 0;
}

包信息

  • public Package getPackage()  //获取包信息

从Package对象中你可以访问诸如名字等包信息。您还可以访问类路径上这个包位于JAR文件中Manifest这个文件中指定的信息。例如,你可以在Manifest文件中指定包的版本号。可以在java.lang.Package中了解更多包类信息。

父类

  • public native Class<? super T> getSuperclass(); //获取直接父类

父类的Class对象和其它Class对象一样是一个Class对象,可以继续使用反射

实现的接口

  • public native Class<?>[] getInterfaces(); //获取实现的接口列表
  • 一个类可以实现多个接口。因此返回一个Class数组。在Java反射机制中,接口也由Class对象表示。
  • 注意:只有给定类声明实现的接口才会返回。例如,如果类A的父类B实现了一个接口C,但类A并没有声明它也实现了C,那么C不会被返回到数组中。即使类A实际上实现了接口C,因为它的父类B实现了C。

为了得到一个给定的类实现接口的完整列表,需要递归访问类和其超类

  • public Type[] getGenericInterfaces() //getGenericInterface返回包括泛型的类型

字段

  • public Field[] getFields() //获取所有可见的字段信息,Field数组为类中声明的每一个字段保存一个Field 实例
  • public Field[] getDeclaredFields()//获取所有的字段信息
  • public Field getField(String name) //通过字段名称获取字符信息,该字段必须可见,否则抛出异常
  • public Field getDeclaredField(String name) //通过字段名称获取可见的字符信息

关于Field
  • public String getName() //获取字段名字
  • public Class<?> getType() //获取一个字段的类型

使用举例:

Class clz = Person.class;
Field field = clz.getDeclaredField("name");

System.out.println("获取字段名称:" + field.getName());
System.out.println("获取字段类型:" +field.getType());

运行结果:
获取字段名称:name
获取字段类型:class java.lang.String
  • public Object get(Object obj) //获取字段的值
  • public void set(Object obj, Object value)//设置字段的值,

注意:

  1. 如果获取的字段不可见,则再通过set和get访问之前,必须先使用 setAccessible(true) 设置为可访问
  2. 如果是静态字段,obj传入null,而不是具体的对象;不过,如果传具体的对象也是能正常操作的

使用举例:

Person person= (Person) clz.newInstance();
field.setAccessible(true);//设置为可访问
field.set(person, "酸辣汤");//通过set方法设置字段值
System.out.println("通过get获取的值:"+field.get(person));

运行结果:
通过get获取的值:酸辣汤

当反射遇到final修饰的字段

看如下例子:

public class FinalTest {

    public static void main(String[] args )throws Exception {
        Field nameField = OneCity.class.getDeclaredField("name");

        nameField.setAccessible(true);
        nameField.set(null, "Shenzhen");
        System.out.println(OneCity.getName());

    }
}

class OneCity {
    private static final String name = new String("Beijing");

    public static String getName() {
        return name;
    }
}

输出结果:

Exception in thread "main" java.lang.IllegalAccessException: Can not set static final java.lang.String field eft.reflex.OneCity.name to java.lang.String

那么该如何用反射来修改它的值?

这时候我们要做一个更彻底的反射 — 对 Java 反射包中的类进行自我反射。Field 对象有个一个属性叫做 modifiers, 它表示的是属性是否是 public, private, static, final 等修饰的组合。这里把这个 modifiers 也反射出来,进而把 nameField 的 final 约束也去掉了,回到了上面的状况了。完整代码是这样的:

public class FinalTest {

    public static void main(String[] args )throws Exception {
        Field nameField = OneCity.class.getDeclaredField("name");

        Field modifiersField = Field.class.getDeclaredField("modifiers"); //①
        modifiersField.setAccessible(true);
        modifiersField.setInt(nameField, nameField.getModifiers() & ~Modifier.FINAL); //②

        nameField.setAccessible(true); //这个同样不能少,除非上面把 private 也拿掉了,可能还得 public
        nameField.set(null, "Shenzhen");
        System.out.println(OneCity.getName()); //输出 Shenzhen

    }
}

class OneCity {
    private static final String name = new String("Beijing");

    public static String getName() {
        return name;
    }
}

在 ① 处把 Field 的 modifiers 找到,它也是个私有变量,所以也要 setAccessible(ture)。接着在 ② 处把 nameField 的 modifiers 值改掉,是用的按位取反 ~ 再按位与 ~ 操作把 final 从修饰集中剔除掉,其他特性如 private, static 保持不变。再想一下 modifierField.setInt() 可以把 private 改为 public, 如此则修改 name 时无需 setAccessible(true) 了。

通过把属性的 final 去掉, 就成功把 name 改成了 Shenzhen。

注意上面为何把 OneCity 的 name 赋值为 new String(“Beijing”), 这是为了不让 Java 编译器内联 name 到 getName() 方法中,而使 getName() 的方法体为 return “Beijing”,造成 getName() 永远输出 ”Beijing” 。

方法

  • public Method[] getMethods() 获取所有可见的方法
  • public Method[] getDeclaredMethods() 获取所有的方法,无论是否可见
  • public Method getMethod(String name, Class<?>... parameterTypes)
    • 通过方法名称、参数类型获取方法
    • 如果你想访问的方法不可见,会抛出异常
    • 如果你想访问的方法没有参数,传递 null作为参数类型数组,或者不传值)
  • public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
    • 通过方法名称、参数类型获取方法
    • 如果你想访问的方法没有参数,传递 null作为参数类型数组,或者不传值)

关于Method
  • public Class<?>[] getParameterTypes() //获取方法的所有参数类型
  • public Class<?> getReturnType() //获取方法的返回值类型
  • public Object invoke(Object obj, Object... args)//调用方法
    • obj:想要调用该方法的对象;args:方法的具体参数,必须为每个参数提供一个准确的参量
    • 如果方法是静态的,这里obj传入null
    • 如果方法没有参数,args传null或者不传

使用举例:

Person类里有这么一个方法:
private void testMethod(String param){
    System.out.println("调用了testMethod方法,参数是:"+param);
}



//通过反射调用方法
Class clz = Person.class;
Method method=clz.getDeclaredMethod("testMethod",String.class);
method.setAccessible(true);
method.invoke(clz.newInstance(),"我是具体的参数值");

运行结果:
调用了testMethod方法,参数是:我是具体的参数值

关于Method更多API自行查看源码

使用Java反射可以在运行时检查类的方法并调用它们。这可以用来检测一个给定的类有哪些get和set方法。可以通过扫描一个类的所有方法并检查每个方法是否是get或set方法。
下面是一段用来找到类的get和set方法的代码:

public static void printGettersSetters(Class aClass){

        Method[]methods = aClass.getMethods();

        for(Methodmethod : methods){
           if(isGetter(method))System.out.println("getter: " + method);
           if(isSetter(method))System.out.println("setter: " + method);
        }
}

public staticboolean isGetter(Method method){
        if(!method.getName().startsWith("get"))      return false;
        if(method.getParameterTypes().length!= 0)   return false; 
        if(void.class.equals(method.getReturnType())return false;
        return true;

}

public staticboolean isSetter(Method method){
    if(!method.getName().startsWith("set"))return false;
    if(method.getParameterTypes().length!= 1) return false;
    return true;
}

注解

  • public Annotation[] getAnnotations() //获取当前成员所有的注解,不包括继承的;(since jdk1.5)
  • public Annotation[] getDeclaredAnnotations()//获取包括继承的所有注解;(since jdk1.5)

关于注解,下回详解

反射与数组

数组:定义多个类型相同的变量
我们都知道,数组是一种特殊的类型,它本质上由虚拟机在运行时动态生成,所以在反射这种类型的时候会稍有不同。
因为数组类直接由虚拟机运行时动态创建,所以你不可能从一个数组类型的 Class 实例中得到构造方法,编译器根本没机会为类生成默认的构造器。于是你也不能以常规的方法通过 Constructor 来创建一个该类的实例对象。
如果你非要尝试使用 Constructor 来创建一个新的实例的话,那么运行时程序将告诉你无法匹配一个构造器。像这样:

Class<String[]> cls = String[].class;
Constructor constructor = cls.getConstructor();
String[] strs = (String[]) constructor.newInstance();

程序会抛出 NoSuchMethodException的异常,告诉你Class 实例中根本找不到一个无参的构造器

那我们要怎么动态创建一个数组??
Java 中有一个类 java.lang.reflect.Array 提供了一些静态的方法用于动态的创建和获取一个数组类型

  • public static Object newInstance(Class<?> componentType, int length)
    • //创建一个一维数组,componentType 为数组元素类型,length 数组长度
  • public static Object newInstance(Class<?> componentType, int... dimensions)
    • //可变参数 dimensions,指定多个维度的单维度长度
  • public static native void set(Object array, int index, Object value)
    • 把数组array索引位置为index的设为value值
  • public static native Object get(Object array, int index)
    • 获得数组array的index位置上的元素

补充下,Class类中获取组件类型的API:

  • public native Class<?> getComponentType();
    • 如果class是数组类型, 获取其元素的类型,如果是非数组,则返回null

一维数组实例

//用反射来定义一个int类型,3长度的数组
int[] intArray = (int[]) Array.newInstance(int.class, 3);

Array.set(intArray, 0, 123);
Array.set(intArray, 1, 456);
Array.set(intArray, 2, 789);

System.out.println("intArray[0] = " + Array.get(intArray, 0));
System.out.println("intArray[1] = " + Array.get(intArray, 1));
System.out.println("intArray[2] = " + Array.get(intArray, 2));

//获取类对象的一个数组
Class stringArrayClass = Array.newInstance(int.class, 0).getClass();
System.out.println("is array: " + stringArrayClass.isArray());


//获取数组的组件类型
String[] strings = new String[3];
Class stringArrayClass2 = strings.getClass();
Class stringArrayComponentType = stringArrayClass2.getComponentType();
System.out.println(stringArrayComponentType);

运行结果:
intArray[0] = 123
intArray[1] = 456
intArray[2] = 789
is array: true
class java.lang.String

多维数组:

// 创建一个三维数组,每个维度长度分别为5,10,15
int[] dims = new int[] { 5, 10,15 };
Person[][][] array = (Person[][][]) Array.newInstance(Person.class, dims); // 可变参数,也可以这样写:Object array = Array.newInstance(Integer.TYPE, 5,10,15);

Class<?> classType0 = array.getClass().getComponentType();    // 返回数组元素类型
System.out.println("三维数组元素类型:"+classType0);    // 三维数组的元素为二维数组

Object arrayObject = Array.get(array, 2);// 获得三维数组中索引为2的元素,返回的是一个二维数组
System.out.println("二维数组元素类型:"+arrayObject.getClass().getComponentType());

Object oneObject = Array.get(arrayObject, 0);// 获得二维数组中索引为0的数组,返回的是一个一维数组
System.out.println("一维数组元素类型:"+oneObject.getClass().getComponentType());

Array.set(oneObject,14,new Person("酸辣汤",18));//设置以为数组索引为3的位置的元素

System.out.println("未被设置元素的位置:"+array[0][0][0]);
System.out.println("已被设置元素的位置:"+array[2][0][14]);

运行结果:
三维数组元素类型:class [[Left.reflex.bean.Person;
二维数组元素类型:class [Left.reflex.bean.Person;
一维数组元素类型:class eft.reflex.bean.Person
person的静态块被调用了
未被设置元素的位置:null
已被设置元素的位置:Person{name='酸辣汤', age=18}

反射与泛型

泛型是 Java 编译器范围内的概念,它能够在程序运行之前提供一定的安全检查,而反射是运行时发生的,也就是说如果你反射调用一个泛型方法,实际上就绕过了编译器的泛型检查了。我们看一段代码:

ArrayList<Integer> list = new ArrayList<>();
list.add(23);
//list.add("fads");编译不通过

Class<?> cls = list.getClass();
Method add = cls.getMethod("add",Object.class);
add.invoke(list,"hello");
for (Object obj:list){
    System.out.println(obj);
}

运行结果:
23
hello

最终你会发现我们从整型容器中取出一个字符串,因为虚拟机只管在运行时从方法区找到 ArrayList 这个类的类型信息并解析出它的 add 方法,接着执行这个方法。它不像一般的方法调用,调用之前编译器会检测这个方法存在不存在,参数类型是否匹配等,所以没了编译器的这层安全检查,反射地调用方法更容易遇到问题。

使用反射来获取泛型信息

在实际应用中,为了获得和泛型有关的信息,Java就新增了几种类型来代表不能被归一到Class类中的类型,但又和基本数据类型齐名的类型,通常使用的是如下两个:

  • GenericType: 表示一种元素类型是参数化的类型或者类型变量的数组类型。@since 1.5
  • ParameterizedType: 表示一种参数化的类型。    @since 1.5

为什么要引入这两种呢,实际上,在通过反射获得成员变量时,Field类有一个方法是getType,可以获得该字段的属性,但是这种属性如果是泛型就获取不到了,所以才引入了上面两种类型。

实例:

public class Person {
    ...
    private Map<String,Integer> map;     
    ...
}

Class<Person> clazz = Person.class;
Field f = clazz.getDeclaredField("map");

//通过getType方法只能获得普通类型
System.out.println("map的类型是:" + f.getType()); //打印Map

//1. 获得f的泛型类型
Type gType = f.getGenericType();

//2.如果gType是泛型类型对像
if(gType instanceof ParameterizedType)
{
    ParameterizedType pType = (ParameterizedType)gType;
    //获取原始类型
    Type rType = pType.getRawType();
    System.out.println("原始类型是: " + rType);

    //获得泛型类型的泛型参数
    Type[] gArgs = pType.getActualTypeArguments();
    //打印泛型参数
    for(int i=0; i < gArgs.length; i ++)
    {
        System.out.println("第"+ i +"个泛型类型是:" + gArgs[i]);
    }
}
else {
    System.out.println("获取泛型信息失败");

}

运行结果:
map的类型是:interface java.util.Map
原始类型是: interface java.util.Map
第0个泛型类型是:class java.lang.String
第1个泛型类型是:class java.lang.Integer

反射源码与性能开销

只列举个别方法的源码,其他的有兴趣可以自行查看源码(大部分都是native方法)

调用invoke()方法

获取到Method对象之后,调用invoke方法的流程如下:

可以看到,调用Method.invoke之后,会直接去调MethodAccessor.invoke。MethodAccessor就是上面提到的所有同名method共享的一个实例,由ReflectionFactory创建。创建机制采用了一种名为inflation的方式(JDK1.4之后):如果该方法的累计调用次数<=15,会创建出NativeMethodAccessorImpl,它的实现就是直接调用native方法实现反射;如果该方法的累计调用次数>15,会由java代码创建出字节码组装而成的MethodAccessorImpl。(是否采用inflation和15这个数字都可以在jvm参数中调整)
以调用MyClass.myMethod(String s)为例,生成出的MethodAccessorImpl字节码翻译成Java代码大致如下:

public class GeneratedMethodAccessor1 extends MethodAccessorImpl {    
   public Object invoke(Object obj, Object[] args)  throws Exception {
       try {
           MyClass target = (MyClass) obj;
           String arg0 = (String) args[0];
           target.myMethod(arg0);
       } catch (Throwable t) {
           throw new InvocationTargetException(t);
       }
   }
}

至于native方法的实现,由于比较深入本文就不探讨了

直接调用方法与通过反射调用方法对比

    public static void main(String[] args) throws Exception {
       // directCall();//直接调用
        reflectCall();//反射调用
    }

    public static void target(int i) {
    }

    //直接调用
    private static void directCall() {
        long current = System.currentTimeMillis();
        for (int i = 1; i <= 2_000_000_000; i++) {
            if (i % 100_000_000 == 0) {
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }

            MethodTest.target(128);
        }
    }

    //反射调用同一个方法
    private static void reflectCall() throws Exception {
        Class<?> klass = Class.forName("eft.reflex.MethodTest");
        Method method = klass.getMethod("target", int.class);

        long current = System.currentTimeMillis();
        for (int i = 1; i <= 2_000_000_000; i++) {
            if (i % 100_000_000 == 0) {
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }

            method.invoke(null, 128);
        }
    }
    
运行结果:
直接调用结果:
...
121
126
105
115
100 (取最后5个值,作为预热后的峰值性能)

反射调用结果:
...
573
581
593
557
594 (取最后5个值,作为预热后的峰值性能)

结果分析:普通调用作为性能基准,大约100多秒,通过反射调用的耗时大约为基准的4倍

为何反射会带来性能开销?

先看下使用反射调用的字节码文件:

63: aload_1                         // 加载Method对象
64: aconst_null                     // 静态方法,反射调用的第一个参数为null
65: iconst_1
66: anewarray                       // 生成一个长度为1的Object数组
69: dup
70: iconst_0
71: sipush        128
74: invokestatic Integer.valueOf    // 将128自动装箱成Integer
77: aastore                         // 存入Object数组
78: invokevirtual Method.invoke     // 反射调用

可以看出反射调用前的两个动作

  • Method.invoke是一个变长参数方法,最后一个参数在字节码层面会是Object数组
    • Java编译器会在方法调用处生成一个长度为入参数量的Object数组,并将入参一一存储进该数组
  • Object数组不能存储基本类型,Java编译器会对传入的基本类型进行自动装箱

上述两个步骤会带来性能开销和GC

如何降低开销?

    1. 增加启动JVM参数:-Djava.lang.Integer.IntegerCache.high=128,减少装箱

    经测试,峰值性能:280.4ms,为基准耗时的2.5倍

    1. 减少自动生成Object数组,测试代码如下:
 private static void reflectCall() throws Exception {
        Class<?> klass = Class.forName("eft.reflex.MethodTest");
        Method method = klass.getMethod("target", int.class);

        // 在循环外构造参数数组
        Object[] arg = new Object[1];
        arg[0] = 128;

        long current = System.currentTimeMillis();
        for (int i = 1; i <= 2_000_000_000; i++) {
            if (i % 100_000_000 == 0) {
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }

            method.invoke(null, 128);
        }
    }

字节码:

80: aload_2                         // 加载Method对象
81: aconst_null                     // 静态方法,反射调用的第一个参数为null
82: aload_3
83: invokevirtual Method.invoke     // 反射调用,无anewarray指令

经测试,峰值性能:312.4ms,为基准耗时的2.8倍

    1. 关闭inflation机制
    • -Dsun.reflect.noInflation=true,关闭Inflation机制,反射调用在一开始便会直接使用动态实现,而不会使用委派实现或者本地实现 (即一开始invoke方法就使用java实现的而不使用native方法)
    • 关闭权限校验:每次反射调用都会检查目标方法的权限
// -Djava.lang.Integer.IntegerCache.high=128
// -Dsun.reflect.noInflation=true
public static void main(String[] args) throws Exception {
        Class<?> klass = Class.forName("eft.reflex.MethodTest");
        Method method = klass.getMethod("target", int.class);
        // 关闭权限检查
        method.setAccessible(true);

        long current = System.currentTimeMillis();
        for (int i = 1; i <= 2_000_000_000; i++) {
            if (i % 100_000_000 == 0) {
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }

            method.invoke(null, 128);
        }
    }

峰值性能:186.2ms,为基准耗时的1.7倍

反射优缺点

优点

1.增加程序的灵活性,避免将程序写死到代码里。

例:定义了一个接口,实现这个接口的类有20个,程序里用到了这个实现类的地方有好多地方,如果不使用配置文件手写的话,代码的改动量很大,因为每个地方都要改而且不容易定位,如果你在编写之前先将接口与实现类的写在配置文件里,下次只需改配置文件,利用反射(java API已经封装好了,直接用就可以用 Class.newInstance())就可完成

2.代码简洁,提高代码的复用率,外部调用方便

缺点

  • 性能开销 - 由于反射涉及动态解析的类型,因此无法执行某些 Java 虚拟机优化。因此,反射操作的性能要比非反射操作的性能要差,应该在性能敏感的应用程序中频繁调用的代码段中避免。
  • 破坏封装性 - 反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。
  • 模糊程序内部逻辑 - 程序人员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术,因而会带来维护问题。反射代码比相应的直接代码更复杂。
  • 内部曝光 - 由于反射允许代码执行在非反射代码中非法的操作,例如访问私有字段和方法,所以反射的使用可能会导致意想不到的副作用,这可能会导致代码功能失常并可能破坏可移植性。反射代码打破了抽象,因此可能会随着平台的升级而改变行为。

Java反射可以访问和修改私有成员变量,那封装成private还有意义么?

既然小偷可以访问和搬走私有成员家具,那封装成防盗门还有意义么?这是一样的道理,并且Java从应用层给我们提供了安全管理机制——安全管理器,每个Java应用都可以拥有自己的安全管理器,它会在运行阶段检查需要保护的资源的访问权限及其它规定的操作权限,保护系统免受恶意操作攻击,以达到系统的安全策略。所以其实反射在使用时,内部有安全控制,如果安全设置禁止了这些,那么反射机制就无法访问私有成员。

反射是否真的会让你的程序性能降低?

1.反射大概比直接调用慢50~100倍,但是需要你在执行100万遍的时候才会有所感觉
2.判断一个函数的性能,你需要把这个函数执行100万遍甚至1000万遍
3.如果你只是偶尔调用一下反射,请忘记反射带来的性能影响
4.如果你需要大量调用反射,请考虑缓存。
5.你的编程的思想才是限制你程序性能的最主要的因素

开发中使用反射的场景

工厂模式:Factory类中用反射的话,添加了一个新的类之后,就不需要再修改工厂类Factory了,如下例子

数据库JDBC中通过Class.forName(Driver).来获得数据库连接驱动

开发通用框架 - 反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 JavaBean、Filter 等),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射——运行时动态加载需要加载的对象。

动态代理 - 在切面编程(AOP)中,需要拦截特定的方法,通常,会选择动态代理方式。这时,就需要反射技术来实现了。

注解 - 注解本身仅仅是起到标记作用,它需要利用反射机制,根据注解标记去调用注解解释器,执行行为。如果没有反射机制,注解并不比注释更有用。

可扩展性功能 - 应用程序可以通过使用完全限定名称创建可扩展性对象实例来使用外部的用户定义类。

使用反射的工厂模式举例:

//用反射机制实现工厂模式:
interface fruit{
    public abstract void eat();
}
class Apple implements fruit{
    public void eat(){
        System.out.println("Apple");
    }
}
class Orange implements fruit{
    public void eat(){
        System.out.println("Orange");
    }
}
class Factory{
    public static fruit getInstance(String ClassName){
        fruit f=null;
        try{
            f=(fruit)Class.forName(ClassName).newInstance();
        }catch (Exception e) {
            e.printStackTrace();
        }
        return f;
    }
}

客户端:
class hello{
    public static void main(String[] a){
        fruit f=Factory.getInstance("Reflect.Apple");
        if(f!=null){
            f.eat();
        }
    }
}

反射与内省

内省(自省):
内省基于反射实现,也就是对反射的再次包装,主要用于操作JavaBean,通过内省 可以获取bean的getter/setter
通俗地说:javaBean 具有的自省机制可以在不知道javaBean都有哪些属性的情况下,设置他们的值。核心也是反射机制

一般在开发框架时,当需要操作一个JavaBean时,如果一直用反射来操作,显得很麻烦;所以sun公司开发一套API专门来用来操作JavaBean

内省是 Java 语言对 Bean 类属性、事件的一种缺省处理方法。例如类 A 中有属性 name, 那我们可以通过 getName,setName 来得到其值或者设置新的值。通过 getName/setName 来访问 name 属性,这就是默认的规则。 Java 中提供了一套 API 用来访问某个属性的 getter/setter 方法,通过这些 API 可以使你不需要了解这个规则(但你最好还是要搞清楚),这些 API 存放于包 java.beans 中。
一般的做法是通过类 Introspector 来获取某个对象的 BeanInfo 信息,然后通过 BeanInfo 来获取属性的描述器( PropertyDescriptor ),通过这个属性描述器就可以获取某个属性对应的 getter/setter 方法,然后我们就可以通过反射机制来调用这些方法。下面我们来看一个例子,这个例子把某个对象的所有属性名称和值都打印出来:

package introspector;
//这些api都是在java.beans下(rt.jar包下)
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;

public class IntrospectorDemo{
    String name;
    public static void main(String[] args) throws Exception{
        IntrospectorDemo demo = new IntrospectorDemo();
        // 如果不想把父类的属性也列出来的话,
        //那 getBeanInfo 的第二个参数填写父类的信息
        BeanInfo bi = Introspector.getBeanInfo(demo.getClass(), Object. class );//Object类是所有Java类的根父类
        PropertyDescriptor[] props = bi.getPropertyDescriptors();//获得属性的描述器
        for ( int i=0;i<props.length;i++){
            System.out.println("获取属性的Class对象:"+props[i].getPropertyType());
            props[i].getWriteMethod().invoke(demo, "酸辣汤" );//获得setName方法,并使用invoke调用
            System.out.println("读取属性值:"+props[i].getReadMethod().invoke(demo, null ));
        }
    }

    public String getName() {
        return name;
    }

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

运行结果:
获取属性的Class对象:class java.lang.String
读取属性值:酸辣汤

JDK内省类库:PropertyDescriptor类:
 PropertyDescriptor类表示JavaBean类通过存储器导出一个属性。主要方法:
1. getPropertyType(),获得属性的Class对象;
2. getReadMethod(),获得用于读取属性值的方法;(如上面的获取getName方法)
3.getWriteMethod(),获得用于写入属性值的方法;(如上面的获取setName方法)
4.hashCode(),获取对象的哈希值;
5. setReadMethod(Method readMethod),设置用于读取属性值的方法;
6. setWriteMethod(Method writeMethod),设置用于写入属性值的方法。

Apache开发了一套简单、易用的API来操作Bean的属性——BeanUtils工具包。

参考资料

zhuanlan.zhihu.com/p/34168509

fanyilun.me/2015/10/29/…

zhongmingmao.me/2018/12/20/…