我学习学习编程技术最大的体会,就是感觉像是在吃热豆腐,慢慢来,是豆腐总会凉凉的。

为什么要有反射?

最直接的原因是,在java之后出现了动态的语言,如python,js,这种动态的语言的类型的检查是在运行的时候做的,程序在运行的时候,能够改变程序结构或是变量类型,java学习别的语言的动态性,引入了反射机制,使得java成为了一门准动态语言,使得java在编程的时候有更大的灵活性,如spring的依赖反转(ioc),便是依赖反射机制的。

应用范围:java 的反射在框架中有着大量的应用。比如开发人员日常接触到的IDE,(集成开发环境)便运用了反射,每当我们敲入点号的时候,IDE变根据点号之前的内容,动态展示可以访问的字段或者是方法;再比如java的调试器,能够在调试过程中枚举某一个对象所有字段的值。

java在没有反射之前,因为java是强类型的语言,所有的参数,变量,返回值,属性都必须在编译期间就确定他的类型(所有的静态的语言的特点就是在编译时候,就必须确定变量等的类型),而对于弱类型的语言不需要确定其类型,比如PHP,声明一个变量的时候不需要指定该变量是什么类型的,在运行的时候会自动根据值来确定该变量的类型。

如果我们想在运行期间才确定变量,或者是返回值的类型,我们需要使用到反射机制。

反射的作用:

 (1)在运行期间,确定某个类型,动态的创建一个对象
 (2)在运行期间,获取某个对象的类型的全部信息
 (3)在运行期间,动态的为某个对象的某个属性赋值,获取或者某个属性值
 (4)在运行期间,动态的调用某个对象的某个方法

 反射的根源(java.lang.Class)

首先,我们来看看java8的API:

public final class Class<T>
extends Object
implements Serializable, GenericDeclaration, Type, AnnotatedElement

Instances of the class Class represent classes and interfaces in a running Java application。

Class类的实例(对象)代表着,正在java应用程序中运行的类或者是接口。

我们知道所有的类在加载到JVM之前,都需要被编译成字节码文件,即.class文件,在这个.class文件中有这个类的相关的属性,构造,方法等信息。而这些类的信息被JVM加载之后,将会以类的Class对象存在的。这有点抽象,我们可以把.class文件比作是一个箱子,而把类的这些信息比作是一些玩具,这些玩具装入了这个箱子里,等到这个箱子被JVM加载到内存的时候,JVM根据这个箱子,或者是这个箱子的信息,产生一个这个箱子的编号,从此之后用这个编号就可以获得这个箱子。这个箱子我们人可以认为就是一个该类的Class对象。可能真实的情况不是这样的,但是这将有助于你理解Class类的对象。

由此,可以看出,每一个被加载到JVM中的类的存在形式,表面是类文件(即字节码文件)实际上是该类的Class类对象。接着API:

An enum is a kind of class and an annotation is a kind of interface. Every array also belongs to a class that is reflected as a Class object that is shared by all arrays with the same element type and number of dimensions. The primitive Java types (boolean, byte, char, short, int, long, float, and double), and the keyword void are also represented as Class objects.

总结一下:

Java中任意类型,在运行期间都对应一个唯一的Class类对象。

 Java的类型:
        8种基本数据类型和void
        引用数据类型:类(class,enum)、接口(interface,@interface)、数组([])

也就是说,所有类运行时(字节码加载到JVM),都对应着唯一的一个Class类对象(在方法区中)。接着:

Class has no public constructor. Instead Class objects are constructed automatically by the Java Virtual Machine as classes are loaded and by calls to the defineClass method in the class loader.

Class类没有公共的构造方法,Class类对象是在类加载时有JVM及通过调用调用类加载器中的defineClass方法自动构造的。

这句话,说明我们无法new 一个 Class类的对象,只能通过某种方式去获取这个Class对象。因为一个类型只有一个Class类的对象,而且在加载到JVM时候必须有且仅有一个Class对象,那么JVM自动给我们创建以此来确保唯一也帮我们省事,如果我们需要使用这个类的Class对象,java提供获取方式给我们,接下来我们来认识一个获取Class类对象的方法。一共有四种方法,我们依次来说:

类名/型.class

适用于所有的类型。

上文我们说到加载类时,JVM自动帮我们创建了该类Class对象。那么JVM总得找一个地方放,放在哪里?放在了class里。我们可以class想象成一个静态的属性,但是其实他不是,因为你翻遍任何一个类的源代码,都找不到这样的一个属性。我们获得了class,也就获得了该类的信息,还记得上文说到的箱子的编号么?获得class,就获得了该类的Class对象,即获得了“箱子编号”,获得了箱子,箱子里有类的信息。(一旦有类的信息,我们留能够创建该类的实例了

我们来使用类名.class来获取Class对象玩玩:

package com.isea.java;

import java.io.Serializable;
import java.lang.annotation.ElementType;

public class TestClass {
    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println("void的类对象:" + void.class);//void的类对象:void
        System.out.println("Object的类对象:" + Object.class);//Object的类对象:class java.lang.Object

        System.out.println(int[].class);//class [I
        System.out.println(int[][].class);//class [[I

        System.out.println(Override.class);//interface java.lang.Override
        System.out.println(ElementType.class);//class java.lang.annotation.ElementType
        System.out.println(Serializable.class);//interface java.io.Serializable
        System.out.println(TestClass.class);//class com.isea.java.TestClass
    }
}

对象名.getClass()

适用于引用数据类型,必须有对象对象才能用,如类,接口,枚举,数据等的对象都可以得到,它对应的运行时的类型。

package com.isea.java;

public class TestClass {
    public static void main(String[] args) {
       Object obj = new String();
       Class<? extends Object> c1 = obj.getClass();
        System.out.println(c1);//class java.lang.String
    }
}

该方法,是Object下的方法:放回调用this的类对象

public final native Class<?> getClass();

Class.forName("类型的全名称")

适用于类,接口,枚举,注解,基本数据类型,数组是不能使用这个方法得到的。

package com.isea.java;

public class TestClass {
    public static void main(String[] args) throws ClassNotFoundException {
        Class c1 = String.class;
        Class c2 = new String().getClass();
        Class c3 = Class.forName("java.lang.String");
        System.out.println(c1 == c2);//true
        System.out.println(c1 == c3);//true
        System.out.println(c2 == c3);//true 证明一个类对应的Class对象是唯一的
    }
}

类加载器对象.loadClass("类型的全名称")

适用于类,接口,枚举,注解。所谓的类加载器,我们可以简单的理解为用来加载类的东西。这种方式,我们首先要得到类加载器的对象,介绍其中一种方式:

ClassLoarder.getSystemClassLoader()

这种方法返回系统类加载器,就是默认的类加载器对象。源码如下:

//return  The system <tt>ClassLoader</tt> for delegation or <tt>null</tt> if none
    @CallerSensitive
    public static ClassLoader getSystemClassLoader() {
        initSystemClassLoader();
        if (scl == null) {
            return null;
        }
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkClassLoaderPermission(scl, Reflection.getCallerClass());
        }
        return scl;
    }

例子:

package com.isea.java;

public class TestClass {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        Class<?> c1 = loader.loadClass("java.lang.Object");
        System.out.println(c1);//class java.lang.Object
    }
}

至此,我们介绍完了所有的获取Class类对象的方式,获取Class类对象是使用反射的第一步。获取到Class对象之后,我们便可以创建这个对象,使用Class类下面的这个方法能够获得该类的一个实例,:

//return  a newly allocated instance of the class represented by this object
public T newInstance() throws InstantiationException, IllegalAccessException{
            //....省略
}

注意:

调用该方法的对象对应的类要含有public类型的无参构造

反射应用一:运行期间动态创建任意类型对象

我们先来看看,如果不使用反射,假如在编译期间,我们就已经知道类型:

package com.isea.java;
public class Student {

}

package com.isea.java;

public class TestClass {
    public static void main(String[] args){
        Student student = new Student();
    }
}

以上的例子中,我们已经知道有了Student类,我们直接创建该对象,这样做虽然非常的简洁,但是Student类和我们的TestClass直接发生了联系(耦合)一旦我们修改了Student类,我们就得修改TestClass中的代码。

再来看另外的一种场景,假如我们在编译期间不确定某个类型,只知道这个某个接口的实现类,但是我们能够在配置文件中获取类名。(广泛使用在web和框架中)来看下面的例子:

配置文件dao.properties:

dao = com.isea.dao.StudentDaoImp

接口:

package com.isea.java;

public interface IStudentDao {
    void test();
}

测试代码:

public class TestClass {
    public static void main(String[] args) throws Exception {
        Properties properties = new Properties();
        properties.load(new FileInputStream("dao.properties"));//加载的是项目于根目录src(目录下)文件下
        String name = properties.getProperty("dao");

        Class c = Class.forName(name);

        IStudentDao iStudentDao = (IStudentDao) c.newInstance();
        iStudentDao.test();
    }
}

以上代码,在编译期间是不会报错的,但是在运行的时候回报如下的异常:

Exception in thread "main" java.lang.ClassNotFoundException:
 com.isea.dao.StudentDaoImp

找不到该类!以上的这段代码利用了反射通过编译,但是并不表示运行期间这个类型可以不存在。代码中我们并没有出现我们要创建的类型(即从配置文件读取的文件类型)。假如我们创建这个类:

package com.isea.dao;

import com.isea.java.IStudentDao;
public class StudentDaoImp implements IStudentDao {
    @Override
    public void test() {
        System.out.println("Just for test...");
    }
}

这样代码就能够正常的运行。这个时候,我们编写的测试类没有直接和StudentDao发生任何的关系(解耦合)。实际上,这个例子中的获得Class对象,并获得这个对象的实例都将由一些框架完成,如javaWeb中Servelet对象都是TomCat创建的,一些接口的实现类都交由Spring等框架来做,可以理解为框架帮助我们管理写在配置文件中的那些类,获取类的信息,创建该类实例等。

本例子模拟了框架中对于反射的应用,值得把玩,框架提前在不知道要创建什么类的情况下通过反射,获取相关要创建类的信息(其实没有,但是能够通过编译)在运行之前,我们需要编写配置文件,将要创建的类告诉框架;框架并能够帮我们创建我们写在配置文件中的类的对象。

这个过程容易发生

ClassNotFoundException

排错

配置文件中(包.类名)是否写错了;对应的路径下是否存在.class文件

 这是第一种方式,我们通过在获得了class对象之后,直接通过class对象newInstance,创建该类的实例。还有一种创建对象的方式,先获取该类的构造器对象,然后在创建构造器对象,newInstance。下面来演示一下(在获取了class对象之后,上文中提到的 c)获取构造器对象的方法有多个:

c.getConstructors()       获得所有的公众的构造器

c.getDeclaredConstructors()    获得所有的声明的构造器,包括私有的

c.getConstructor(parameterType)  获得一个指定类型的公众的构造器

c.getDeclaredConstructor(parameterType)  获得一个指定类型的公众的构造器、参数不写都不写,得到的是无参构造;

返回值是Constructor类型;参数的类型是,class;

得到构造器对象之后,如果使用该构造器对象创建该类的对象:

Constructor constructor = c.getDeclaredConstructor();
IStudentDao iStudentDao = (IStudentDao) constructor.newInstance();

如果得到的构造器对象不是public的,在创建对象时,会发生如下的一异常:

Exception in thread "main" java.lang.IllegalAccessException:
Class com.isea.java.TestClass can not access a member of class com.isea.dao.StudentDaoImp with modifiers "private"

在创建对象之前,我们需要设置访问权限:

constructor.setAccessible(true);

如此一来,我们便能够根据这个构造器对象创建对象。另外,我们在写代码的时候,所有的类尽量的显示化一个无参的构造函数

反射应用二:动态的设置/访问某个对象的属性

先来看一下使用的场景:我们需要动态的获取Student对象的属性

package com.isea.java;
public class Student {
    private int id;
    private String name;
    private char gender;
    
    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }
    public Student() {
    }
}

第一步:获取class对象;第二步根据class对象,创建该类的对象。第三步是我们需要看的:

c.getFields()            获取公共的属性

c.getDeclaredFields()     获取所有声明的属性(包括私有)

c.getField(name)        获取指定的公共的属性

c.getDeclaredField(name)     获取指定的声明的属性(包括私有)

根据属性名区分,获取哪个属性;返回的类型是Field类型,

注意,我们这里是使用 c(class类对象)来获得属性的,而不是使用该类的对象。

我们通过以上的几种方式获得了属性,便可以进行get和set,如果该类的属性是私有的,在set和get之前,我们需要设置他的访问权限

package com.isea.java;
import java.lang.reflect.Field;

public class TestClass {
    public static void main(String[] args) throws Exception {
        Class c = Class.forName("com.isea.java.Student");
        Student student = (Student) c.newInstance();

        Field field = c.getDeclaredField("name");
        field.setAccessible(true);

        field.set(student,"isea_you");

        Object value = field.get(student);
        System.out.println(value);//isea_you
    }
}

关于Filed类型的组成?周知,一个类的属性,有访问权限,返回值,属性名等组成,可推测Field也是如此。

总结一下:

  1. 获取Class类对象
  2. 创建实例对象
  3. 获取属性值Filed
  4. 如果非公属性,setAccessible(true)
  5. Fielfd对象.set() ,Field对象.get()

反射应用三:动态的调用方法

和前面的步骤一样,获取Class对象,创建该类的对象;第三步:

c..getMethods()                             /获取所有的公共方法

c.getDeclaredMethods()              获取所有声明的方法

c.getMethord(name,paramaterType)      获取指定的公共的方法

c.getDeclaredMethod(name,paramaterType)    获取指定的声明的方法

返回值类型是:Methord类型。同一个类中有方法名相同的方法,因此,需要传入方法的形参列表进行区别。

举一个例子:

package com.isea.java;
public class Num {
    public int getMax(int x,int y){
        return x > y ? x : y;
    }
    public double getMax(double x,double y){
        return x > y ? x : y;
    }
}
package com.isea.java;
import java.lang.reflect.Method;

public class TestClass {
    public static void main(String[] args) throws Exception {
        Class c = Class.forName("com.isea.java.Num");
        Num instance = (Num) c.newInstance();
        Method method = c.getDeclaredMethod("getMax", int.class, int.class);
        Object n = method.invoke(instance,3,9);
        System.out.println(n);//9
    }
}

为什么,我们在得到了该类的对象之后,不适用对象 “.” 的方式去调用方法,而是使用反射?因为我们并不知道项目中需要什么类,以至于这个类我们还没有创建,我们只是知道这个类全路径,和这个类应该有的方法。我们能够通过反射拿到该类的方法,并调用该类的方法,这在编译期间,这个类实际上并不存在。

反射应用四, 动态获取类型信息

首先,我们新建一个用来测试的User类,我们从配置文件中读取这个类的全路径

package com.isea.java;

import java.io.Serializable;

public class User implements Serializable,Comparable {
    private static final long serialVersion = 1l;
    private String username;
    private String password;
    private String email;

    public User(String username, String password, String email) {
        this.username = username;
        this.password = password;
        this.email = email;
    }

    public static long getSerialVersion() {
        return serialVersion;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public int compareTo(Object o) {
        return 0;
    }
}

配置配置信息:

type = com.isea.java.User

获取关于这个类的相关信息:

package com.isea.java;

import java.io.FileInputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Properties;

public class TestClass {
    public static void main(String[] args) throws Exception {
        Properties properties = new Properties();
        properties.load(new FileInputStream("bean.properties"));
        Class c = Class.forName(properties.getProperty("type"));
//        User instance = (User) c.newInstance();
//      获取包名类型信息

        Package p = c.getPackage();
        System.out.println("包名:" + p.getName());

        String name  = c.getName();
        System.out.println("类名:" + name);

        int modify = c.getModifiers();
        System.out.println("修饰符:" + modify);
        System.out.println(Modifier.toString(modify));

//      获取父类信息
        Class superclass = c.getSuperclass();
        System.out.println("父类:" + superclass.getName());

        Class[] interfaces = c.getInterfaces();
        System.out.println("该类接口:");
        for (Class inter :
                interfaces) {
            System.out.println("\t" + inter.getName());
        }

//      该类的属性
        Field[] fields = c.getDeclaredFields();
        System.out.println("该类的属性");
        for (Field field:
            fields ) {
            System.out.println("\t修饰符:" + Modifier.toString(field.getModifiers()));
            System.out.println("\t数据类型:" + field.getType().getName());
            System.out.println("\t属性名:" + field.getName());
            System.out.println();
        }

//      该类的构造器
        Constructor[] constructors = c.getDeclaredConstructors();
        System.out.println("该类的构造器:");
        for (Constructor constructor :
                constructors) {
            System.out.println("\t修饰符:" + Modifier.toString(constructor.getModifiers()));
            System.out.println("\t构造器名:" + constructor.getName());
            System.out.println("\t构造器的形参列表:");
            Class[] paramType = constructor.getParameterTypes();
            for (Class param :
                    paramType) {
                System.out.println("\t\t" + param);
            }
        }

        Method[] methods = c.getDeclaredMethods();
        for (Method method :
                methods) {
            System.out.println("\t修饰符:" + Modifier.toString(method.getModifiers()));
            System.out.println("\t返回值类型:" + method.getReturnType().getName());
            System.out.println("\t方法名:" + method.getName());

            System.out.println("\t方法的形参列表:");
            Class[] parameterTypes = method.getParameterTypes();
            for (Class param :
                    parameterTypes) {
                System.out.println("\t\t" + param);
                System.out.println();
            }
        }
    }
}

最后所有的类型打印:

包名:com.isea.java
类名:com.isea.java.User
修饰符:1
public
父类:java.lang.Object
该类接口:
	java.io.Serializable
	java.lang.Comparable
该类的属性
	修饰符:private static final
	数据类型:long
	属性名:serialVersion

	修饰符:private
	数据类型:java.lang.String
	属性名:username

	修饰符:private
	数据类型:java.lang.String
	属性名:password

	修饰符:private
	数据类型:java.lang.String
	属性名:email

该类的构造器:
	修饰符:public
	构造器名:com.isea.java.User
	构造器的形参列表:
		class java.lang.String
		class java.lang.String
		class java.lang.String
	修饰符:public
	返回值类型:int
	方法名:compareTo
	方法的形参列表:
		class java.lang.Object

	修饰符:public
	返回值类型:java.lang.String
	方法名:getPassword
	方法的形参列表:
	修饰符:public
	返回值类型:java.lang.String
	方法名:getUsername
	方法的形参列表:
	修饰符:public
	返回值类型:java.lang.String
	方法名:getEmail
	方法的形参列表:
	修饰符:public
	返回值类型:void
	方法名:setEmail
	方法的形参列表:
		class java.lang.String

	修饰符:public
	返回值类型:void
	方法名:setUsername
	方法的形参列表:
		class java.lang.String

	修饰符:public static
	返回值类型:long
	方法名:getSerialVersion
	方法的形参列表:
	修饰符:public
	返回值类型:void
	方法名:setPassword
	方法的形参列表:
		class java.lang.String

演示完毕。此外,我们也能通过上面的方式获得泛型的信息,这里就不在演示了。

曾经有一个朋友总是说,人生苦短,及时行乐。可是我知道他并不快乐,或许这就是人生。