Java反射机制的原理以及创建类实例的三种方式是什么?

一、什么是Java反射机制

当程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言。我们认为java并不是动态语言,但是它却有一个非常突出的动态相关机制,俗称:反射。

IT行业里这么说,没有反射也就没有框架,现有的框架都是以反射为基础。在实际项目开发中,用的最多的是框架,填的最多的是类,反射这一概念就是将框架和类揉在一起的调和剂。所以,反射才是接触项目开发的敲门砖!

二、反射的应用及原理

我们可能听过,Java编写的程序,一次编译,到处运行。这也是Java程序为什么是无关平台的所在,原因在于,java的源代码会被编译成.class文件字节码,只要装有Java虚拟机JVM的地方(Java提供了各种不同平台上的虚拟机制,第一步由Java IDE进行源代码编译,得到相应类的字节码.class文件,第二步,Java字节码由JVM执行解释给目标计算机,第三步,目标计算机将结果呈现给我们计算机用户;因此,Java并不是编译机制,而是解释机制),.class文件畅通无阻。

Java的反射机制,操作的就是这个.class文件,首先加载相应类的字节码(运行eclipse的时候,.class文件的字节码会加载到内存中),随后解剖(反射 reflect)出字节码中的构造函数、方法以及变量(字段),或者说是取出,我们先来定义一个类Animal,里面定义一些构造函数,方法,以及变量:

package com.reflect;

public class Animal {

    public String name = "Dog";
    private int age = 30;

    //默认无参构造函数
    public Animal() {
        System.out.println("Animal");
    }

    //带参数的构造函数
    public Animal(String name, int age) {
        System.out.println(name + "," + age);
    }

    //公开 方法  返回类型和参数均有
    public String sayName(String name) {
        return "Hello," + name;
    }

}

我们再定义一个测试类:ReflectTest.java

package com.reflect;

public class ReflectTest {
    public static void main(String[] args) {
        
        Animal animal = new Animal();
        System.out.println(animal.name);
        
    }
}

我们运行一下我们的项目,会发现如下:

Java反射机制的原理以及创建类实例的三种方式是什么?

我们借助javap命令查看一下,这个Animal.class里面的内容是什么:

D:\Tools\intellij idea\ideaIU_workspace\LeetCode\target\classes\com\reflect>java
p -c Animal.class
Compiled from "Animal.java"
public class com.reflect.Animal {
  public java.lang.String name;

  public com.reflect.Animal();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":
()V
       4: aload_0
       5: ldc           #2                  // String Dog
       7: putfield      #3                  // Field name:Ljava/lang/String;
      10: aload_0
      11: bipush        30
      13: putfield      #4                  // Field age:I
      16: getstatic     #5                  // Field java/lang/System.out:Ljava/
io/PrintStream;
      19: ldc           #6                  // String Animal
      21: invokevirtual #7                  // Method java/io/PrintStream.printl
n:(Ljava/lang/String;)V
      24: return

  public com.reflect.Animal(java.lang.String, int);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":
()V
       4: aload_0
       5: ldc           #2                  // String Dog
       7: putfield      #3                  // Field name:Ljava/lang/String;
      10: aload_0
      11: bipush        30
      13: putfield      #4                  // Field age:I
      16: getstatic     #5                  // Field java/lang/System.out:Ljava/
io/PrintStream;
      19: new           #8                  // class java/lang/StringBuilder
      22: dup
      23: invokespecial #9                  // Method java/lang/StringBuilder."<
init>":()V
      26: aload_1
      27: invokevirtual #10                 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      30: ldc           #11                 // String ,
      32: invokevirtual #10                 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      35: iload_2
      36: invokevirtual #12                 // Method java/lang/StringBuilder.ap
pend:(I)Ljava/lang/StringBuilder;
      39: invokevirtual #13                 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
      42: invokevirtual #7                  // Method java/io/PrintStream.printl
n:(Ljava/lang/String;)V
      45: return

  public java.lang.String sayName(java.lang.String);
    Code:
       0: new           #8                  // class java/lang/StringBuilder
       3: dup
       4: invokespecial #9                  // Method java/lang/StringBuilder."<
init>":()V
       7: ldc           #14                 // String Hello,
       9: invokevirtual #10                 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      12: aload_1
      13: invokevirtual #10                 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      16: invokevirtual #13                 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
      19: areturn
}

我们发现,字节码里面包含了类Animal的构造函数、变量以及方法,但注意,全都是public类型的,我们的定义的类的私有变量 private int age =30 哪去了?当然,既然是类的私有部分,肯定不会暴露在外面的,但是不阻碍我们通过反射获得字节码中的私有成员(本篇只举例说明私有变量(字段field),其他私有类成员同理)。

我们的类Animal在Anima.java中定义,但在Animal.class文件中,我们的Animal类阐述如下:

public class com.reflect.Animal

下面,我们来写一段demo,来演示一下,如何使用反射机制,将.class文件中的类加载出来,并解剖出字节码中对应类的相关内容(构造函数、属性、方法):

ReflectTest.java:

package com.reflect;

import java.lang.reflect.Constructor;

public class ReflectTest {
    public static void main(String[] args) throws Exception {

        //1、加载类,指定类的完全限定名:包名+类名
        Class c1 = Class.forName("com.reflect.Animal");
        System.out.println(c1); //打印c1,发现值和字节码中的类的名称一样

        //2、解刨(反射)类c1的公开构造函数,且参数为null
        Constructor ctor1 = c1.getConstructor();

        //3、构造函数的用途,就是创建类的对象(实例)的
        //除了私有构造函数外(单列模式,禁止通过构造函数创建类的实例,保证一个类只有一个实例)
        //ctor1.newInstance()默认生成一个Object对象,我们需要转化成我们要的Animal类对象
        Animal a1 = (Animal) ctor1.newInstance();

        //4、证明一下a1确实是Animal的实例,我们通过访问类中的变量来证明
        System.out.println(a1.name);

    }
}
输出结果:
class com.reflect.Animal
Animal
Dog

我们接着走,获得类中的变量(字段)和方法,两种方式,一个是getXXX,一个是getDeclaredXXX,二者是有区别的,下面demo注释的很详细,并且,我们使用反射出的字段和方法,去获取相应实例的字段值和唤起方法(相当于执行某实例的方法),我们看下完整版demo:

加强版的 ReflectTest.java

package com.reflect;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflectTest {
    public static void main(String[] args) throws Exception {

        System.out.println("A(无参构造函数)--加载类、反射类的构造函数、利用构造函数new一个Animal实例instance--");

        //1、加载类,指定类的完全限定名:包名+类名
        Class c1 = Class.forName("com.reflect.Animal");
        System.out.println(c1); //打印c1,发现值和字节码中的类的名称一样

        //2、解刨(反射)类c1的公开构造函数,且参数为null
        Constructor ctor1 = c1.getConstructor();

        //3、构造函数的用途,就是创建类的对象(实例)的
        //除了私有构造函数外(单列模式,禁止通过构造函数创建类的实例,保证一个类只有一个实例)
        //ctor1.newInstance()默认生成一个Object对象,我们需要转化成我们要的Animal类对象
        Animal a1 = (Animal) ctor1.newInstance();

        //4、证明一下a1确实是Animal的实例,我们通过访问类中的变量来证明
        System.out.println(a1.name);

        System.out.println("A(有参构造函数)--加载类、反射类的构造函数、利用构造函数new一个Animal实例instance--");

        //2.b、 解刨(反射)类c1的公开构造函数,参数为string和int
        Constructor ctor2 = c1.getConstructor(String.class, int.class);
        Animal a2 = (Animal) ctor2.newInstance("cat", 20);

        System.out.println("B--获得本类中的所有的字段----------------------------");

        //5、获得类中的所有的字段	包括public、private和protected,不包括父类中申明的字段
        Field[] fields = c1.getDeclaredFields();
        for (Field field : fields) {
            System.out.println(field);
        }

        System.out.println("C--获得本类中的所有公有的字段,并获得指定对象的字段值-----");

        //6、获得类中的所有的公有字段
        fields = c1.getFields();
        for (Field field : fields) {
            System.out.println(field + ", 字段值 = " + field.get(a1));
            //注意:私有变量值,无法通过field.get(a1)进行获取值
            //通过反射类中的字段name,修改name的值(注意,原值在类中name="Dog")
            //如果,字段名称等于"name",且字段类型为String,我们就修改字段的值,也就是类中变量name的值
            if (field.getName() == "name" && field.getType().equals(String.class)) {
                String name_new = (String) field.get(a1); //记得转换一下类型
                name_new = "哈士奇"; //重新给name赋值
                field.set(a1, name_new); //设置当前实例a1的name值,使修改后的值生效
            }
        }

        System.out.println("利用反射出的字段,修改字段值,修改后的name = "+a1.name);
        System.out.println("D--获取本类中的所有的方法--------------------");

        //7、获取本类中所有的方法 包括public、private和protected,不包括父类中申明的方法
        Method[] methods = c1.getDeclaredMethods();
        for (Method m : methods) {
            System.out.println(m);//我们在类Animal中只定义了一个public方法,sayName
        }

        System.out.println("E--获取本类中的所有的公有方法,包括父类中和实现接口中的所有public方法-----------");

        //8、获取类中所有公有方法,包括父类中的和实现接口中的所有public 方法
        methods = c1.getMethods();
        for (Method m : methods) {
            System.out.println(m);//我们在类Animal中只定义了一个public方法,sayName
        }

        System.out.println("F--根据方法名称和参数类型获取指定方法,并唤起方法:指定所属对象a1,并给对应参数赋值-----------");

        //9、唤起Method方法(执行) getMethod:第一个参数是方法名,后面跟方法参数的类
        Method sayName = c1.getMethod("sayName", String.class);
        System.out.println(sayName.invoke(a1, "riemann"));

    }
}

输出结果:

A(无参构造函数)--加载类、反射类的构造函数、利用构造函数new一个Animal实例instance--
class com.reflect.Animal
Animal
Dog
A(有参构造函数)--加载类、反射类的构造函数、利用构造函数new一个Animal实例instance--
cat,20
B--获得本类中的所有的字段----------------------------
public java.lang.String com.reflect.Animal.name
private int com.reflect.Animal.age
C--获得本类中的所有公有的字段,并获得指定对象的字段值-----
public java.lang.String com.reflect.Animal.name, 字段值 = Dog
利用反射出的字段,修改字段值,修改后的name = 哈士奇
D--获取本类中的所有的方法--------------------
public java.lang.String com.reflect.Animal.sayName(java.lang.String)
E--获取本类中的所有的公有方法,包括父类中和实现接口中的所有public方法-----------
public java.lang.String com.reflect.Animal.sayName(java.lang.String)
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
F--根据方法名称和参数类型获取指定方法,并唤起方法:指定所属对象a1,并给对应参数赋值-----------
Hello,riemann

反射的机制,无非就是先加载对应字节码中的类,然后,根据加载类的信息,一点点的去解剖其中的内容,不管你是public的还是private的,亦或是本类的还是来自原继承关系或者实现接口中的方法,我们java的反射技术 reflect,均可以将其从字节码中拉回到现实,不仅可以得到字段的名字,我们还可以获得字段的值和修改字段的值,不仅可以得到方法的申明我们还可以拿到方法的定义和唤起方法(执行方法),当然,你会有一个这样的疑惑?

为什么new一个对象那么简单,非要用反射技术中的newInstance?

为什么,我可以直接对象a1. 变量访问变量,却非要用反射那么费劲的获得name字段呢?

为什么,我几行代码就能搞定的事情,非要用反射呢?

Java反射机制的原理以及创建类实例的三种方式是什么?

ok,解密答案之前,我们先来思考一个问题?

假设我们定义了很多类,有Animal、Person、Car… ,如果我想要一个Animal实例,那我就new Animal(),如果另一个人想要一个Person实例,那么他需要new Person(),当然,另一个说,我只要一个Car实例,于是它要new Car()…这样一来就导致,每个用户new的对象需求不相同,因此他们只能修改源代码,并重新编译才能生效。这种将new的对象写死在代码里的方法非常不灵活,因此,为了避免这种情况的方法,Java提供了反射机制,典型的应用如下:

Java反射机制的原理以及创建类实例的三种方式是什么?

比如,在Spring中,我们经常看到:

Java反射机制的原理以及创建类实例的三种方式是什么?

针对上述的配置,我们Spring是怎么帮助我们实例化对象,并放到容器中去了呢? 没错,就是通过反射!!!!

我们看下,下面的伪代码实现过程:

		//解析<bean .../>元素的id属性得到该字符串值为"sqlSessionFactory" 
	    String idStr = "sqlSessionFactory";  
	    //解析<bean .../>元素的class属性得到该字符串值为"org.mybatis.spring.SqlSessionFactoryBean"  
	    String classStr = "org.mybatis.spring.SqlSessionFactoryBean";  
	    //利用反射知识,通过classStr获取Class类对象  
	    Class cls = Class.forName(classStr);  
	    //实例化对象  
	    Object obj = cls.newInstance();  
	    //container表示Spring容器  
	    container.put(idStr, obj);  
		
	    //当一个类里面需要用另一类的对象时,我们继续下面的操作
	    
	    //解析<property .../>元素的name属性得到该字符串值为“dataSource”  
	    String nameStr = "dataSource";  
	    //解析<property .../>元素的ref属性得到该字符串值为“dataSource”  
	    String refStr = "dataSource";  
	    //生成将要调用setter方法名  
	    String setterName = "set" + nameStr.substring(0, 1).toUpperCase()  
	            + nameStr.substring(1);  
	    //获取spring容器中名为refStr的Bean,该Bean将会作为传入参数  
	    Object paramBean = container.get(refStr);  
	    //获取setter方法的Method类,此处的cls是刚才反射代码得到的Class对象  
	    Method setter = cls.getMethod(setterName, paramBean.getClass());  
	    //调用invoke()方法,此处的obj是刚才反射代码得到的Object对象  
	    setter.invoke(obj, paramBean); 

是不是很熟悉,虽然是伪代码,但是和我们本篇讲的反射机制的使用是相同的,现在知道我们的反射机制用在哪了吧,没错就是我们经常提到的Java web框架中,里面就用到了反射机制,只要在代码或配置文件中看到类的完全限定名(包名+类名),其底层原理基本上使用的就是Java的反射机制。

因此,如果你不做框架的话,基本上是用不到反射机制的,我们大多时候是使用框架的一方,而反射机制都已经在底层实现过了,因此,我们不必担心,我们会写那么复杂的代码。但是,我们必须要理解这种机制的存在!

三、创建类实例的三种方式

JavaBean

public class Person implements China{
      private String name;
      private int age ;
      private char sex ;

      public Person() {
           super ();
     }

      public Person(String name, int age, char sex) {
           super ();
           this .name = name;
           this .age = age;
           this .sex = sex;
     }

      public String getName() {
           return name ;
     }

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

      public int getAge() {
           return age ;
     }

      public void setAge(int age) {
           this .age = age;
     }

      public char getSex() {
           return sex ;
     }

      public void setSex(char sex) {
           this .sex = sex;
     }
      public void eat()
     {
          System. out .println("吃了" );
     }

      @Override
      public String toString() {
           return "Person [name=" + name + ", age=" + age + ", sex=" + sex + "]" ;
     }

      @Override
      public void sayChina() {
           // TODO Auto-generated method stub
          System. out .println("作者:" + AUTHOR + "国籍:"+ NATIONAL );
     }

      @Override
      public String sayHello(String name, int age, char sex) {
           // TODO Auto-generated method stub
           return "姓名:" + name + "年龄:"+ age + "性别:" + sex;
     }

}
public class ClassDemo {

     public static void main(String[] args) {
          Person p1 = new Person("小明" ,20,'男' );
          Person p2 = new Person("小红" ,23,'女' );

           //创建Class对象的方式一:(对象.getClass()),获取person类中的字节码文件
           Class class1 = p1.getClass();
          System. out.println(p1.getClass().getName());
           Class class2 = p2.getClass();
          System. out.println(class1 == class2 );

          System. out.println("==============================" );
           //创建Class对象的方式二:(类.class:需要输入一个明确的类,任意一个类型都有一个静态的class属性)
           Class class3 = Person.class;
          System. out.println(class1 == class2);

          System. out.println("==============================" );
           //创建Class对象的方式三:(forName():传入时只需要以字符串的方式传入即可)
           //通过Class类的一个forName(String className)静态方法返回一个Class对象,className必须是全路径名称;
           //Class.forName()有异常:ClassNotFoundException

           Class class4 = null;
           try {
              class4 = Class.forName("cn.itcast.Person");
          } catch (ClassNotFoundException e) {
               // TODO Auto-generated catch block
              e.printStackTrace();
          }
          System. out.println(class4 == class3);
     }
}

注意:在开发中一般使用第三种方法,因为第三种接收的是一个字符串路径,将来可以通过配置文件获取,通用性好;