java反射全解

引言

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

1.使用new关键字

2.使用clone方法

3.使用反序列化

4.使用反射

5.使用Unsafe

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

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

反射简介

反射之中包含了一个「反」字,因此想要解释反射就必须先从「正」开始解释。
通常状况下,咱们使用某个类时一定知道它是什么类,是用来作什么的。因而咱们直接对这个类进行实例化,以后使用这个类对象进行操做。数据库

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)。api

因此说什么是反射?数组

反射就是在运行时才知道要操做的类是什么,而且能够在运行时获取类的完整构造,并调用对应的方法。缓存

官方定义

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

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

通俗归纳

反射就是让你能够经过名称来获得对象的信息(如类,属性,方法等) 的技术数据结构

核心

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个方面:

1、获取反射的 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");
复制代码

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

  • 用 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方法执行以前就会被加载。

3、经过反射获取构造器,并建立实例对象

经过反射建立类对象主要有两种方式:经过 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建立对象的完整例子:详见上面的使用反射建立对象-调用类对象的构造方法

4、经过反射获取类的属性、方法

使用反射能够获取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/…

相关文章
相关标签/搜索