在java的世界里,万物皆对象。多个具有相同特征的实例可以抽象出一个类,而所有类也可以再进一步抽象化,就得到一个描述类结构信息的类,称为Class类。Class类和其他类的区别是:其他类的对象由自己创建,而Class类的对象是由jvm创建。
图片.png
任何一个类都是Class类的一个实例对象,该如何得到Class类的实例对象?
图片.png
运行结果为:
图片.png
第一种方式通过Foo类的一个隐含的静态成员获取Foo类对应的Class对象。
第二种方式通过Foo类的实例对象调用getClass()方法获取Foo类对应的Class对象。
第三种方式通过Class类的静态成员函数forName()指定Foo类获取Foo类对应的Class对象。
上面三种取得Foo类对应的Class对象的方式,c1、c2和c3引用指向JVM内存中method区的同一地址,所以它们输出true。
这里放栈对应堆的图。类里切成了每一个对象
得到Foo类对应的Class对象,我们可以做很多事情 。
比如我们可以使用Foo类对应的Class对象创建Foo类对象。
图片.png
运行结果为:
图片.png
或者可以使用Foo类对应的Class对象得到Foo类的成员变量,成员方法和构造函数等信息。
代码Demo:使用Foo类对应的Class对象得到Foo类自己声明的成员方法。
图片.png
运行结果:
图片.png
代码Demo:使用Foo类对应的Class对象得到Foo类自己声明的成员变量。
图片.png
运行结果:
图片.png
代码Demo:使用Foo类对应的Class对象得到Foo类自己声明的构造函数。
图片.png
运行结果:
图片.png
类的一个变量就是一个Field对象,类的一个成员函数就是一个Method对象,类的一个构造函数就是一个Constructor对象。Field类、Method类和Constructor类存在的意义不仅局限于打印一个类包含了哪些变量、成员函数以及构造函数,还可以访问私有变量和调用私有成员函数,以及使用Constructor对象实例出类的一个对象。
Demo:访问私有变量。
图片.png
运行结果:
图片.png
Demo:调用私有成员函数。
图片.png
运行结果:
图片.png
Demo:使用Constructor对象实例出类的一个对象。
图片.png
运行结果:
图片.png
到这里,我们就能够得到反射机制的定义了。
反射:JAVA反射机制是在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
前文说过Class对象是由jvm产生的,那jvm产生某个类的Class对象的时候怎么也得首先得到这个类的相关信息吧。记录这个类相关信息的文件就是编译后产生的.class文件。即取得类对应Class对象的前提是jvm将.class文件加载进内存区,这个在程序运行的过程将.class文件读进jvm内存区的机制称为类加载机制。下面我们来研究一下类加载机制。
图片.png
类加载过程包括:加载、校验、准备、解析和初始化五个阶段,其中校验、准备和解析并称为链接阶段。校验和解析与本文主题关系不大会一笔带过,着重介绍加载、准备和初始化。
加载:
“加载”是“类加载”(Class Loading)过程的一个阶段。在加载阶段,虚拟机需要完成以下3件事情:
1)通过一个类的全限定名(com.imooc.reflect.Person)来获取定义此类的二进制字节流(.class文件)。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中实例化一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证:
验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。
准备:
当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。这里需要注意两个关键点,即内存分配的对象以及初始化的类型。
内存分配的对象。Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。
例如下面的代码在准备阶段,只会为 age属性分配内存,而不会为 name属性分配内存。
public static int age= 3;
public String name= "温佰培";
初始化的类型。在准备阶段,JVM 会为类变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。
例如下面的代码在准备阶段之后,age的值将是 0,而不是 3。
public static int age= 3;
但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,number 的值将是 3,而不是 0。
public static final int number = 3;
解析:
解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。
初始化:
类的初始化阶段是类加载过程的最后一步,这一步会执行“类初始化方法”。
类初始化方法:编译器会按照其出现顺序,收集类变量的赋值语句、静态代码块,最终组成类初始化方法。
Demo:
图片.png
在上面这个Demo中,Book类经过编译后就会得到如下类初始化方法。
图片.png
虚拟机规范则是严格规定了一下几种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
(1)用new实例化一个类时、读取或者设置类的静态字段时、执行静态方法的时候。
(2)用Class.forName(String className);来加载类的时候,也会执行初始化动作。
(3)初始化一个类的时候,如果他的父亲还没有被初始化,则先去初始化其父亲。
(4)当jvm启动时,用户需要指定一个要执行的主类(包含static void main(String[] args)的那个类),则jvm会先去初始化这个类。
Demo:
Grandpa类定义:
图片.png
Father类定义:
图片.png
功能类定义:
图片.png
运行结果:
图片.png
分析:
这里放张分析图
功能类Test类包含static void main(String[] args),故先找到一个名为Test.class的二进制文件,将Test的类信息加载到运行时数据区的方法区内。然后链接和初始化,在初始化阶段执行类初始化方法。
JVM找到Test的主函数入口,开始执行main函数。
main函数的第一条命令是Class clazz = Class.forName("com.imooc.reflect.Father");但是这时候方法区中没有Father类的信息,所以JVM马上加载Father类,把Father类的类型信息放到方法区中。然后就链接。当进展到初始化这个阶段的时候发现Father的父类Grandpa没有初始化,这时候Father先不执行类初始化方法,转而去加载Grandpa。
Grandpa类加载、链接和初始化,初始化阶段执行Grandpa的类初始化方法。
再进行Father类的初始化,执行Father类的类初始化方法。
类加载机制在工厂模式中的应用:
先来看一个简单工厂模式的demo:
图片.png
上面写法的缺点是当我们再添加一个子类的时候,就需要修改工厂类了。如果我们添加太多的子类的时候,改动就会很多。另外,如果删除“苹果类”,由于在编译时刻就需要加载所有可能使用到的类,所以编译是不能通过的。但是,程序运行的时候不一定会用到“苹果类”,可能只用到“橙子类”,程序也就成功运行了。所以目前我们的需求是:我们只想程序运行时用到哪个类时缺少该类才抛出错误,不缺少则不报错,我们该怎么做?
答案是用反射(类加载)机制实现工厂模式:
图片.png
现在就算我们添加任意多个子类的时候,工厂类都不需要修改。而且,删除掉苹果类编译也能成功通过。只不过程序运行起来需要创建苹果类时会抛出错误罢了。
在上面用了反射(类加载)机制的简单工厂模式中,对于客户端(hello类),它失去创建对象的控制权,创建对象的控制权转移到了Factory类。这种现象就叫做控制反转(IOC)。Factory类就是IOC容器,它为你创建对象。Spring框架设计时的理念也是控制反转,以降低组件间的耦合。
下面看一个应用Spring框架的Demo:
Book类
图片.png
功能测试类BeanTest:
图片.png
BookBean.xml
图片.png
运行结果:
图片.png
分析:
ApplicationContext ctx = new ClassPathXmlApplicationContext("BookBean.xml");可以理解成是一个IOC容器,是它帮我们初始化了Book类并且实例化出一个Book对象。
拓展:
the normol block of Book type.100
the constructor of Book type.100
上面两行是由对象初始化方法生成的。
什么是对象初始化方法?
编译器编译过程中,按照出现顺序收集成员变量的赋值语句、普通代码块,最后收集构造函数的代码,最终组成对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。
IOC容器是怎么帮我们实例化Book类对象的?看Spring框架源码
图片.png
若这时候类定义里需要注入其他对象呢?这个对象依旧由IOC容器创建并由IOC容器“注入”到该类里面,这个过程称为依赖注入(DI)。
Demo:
被注入对象的类Injected:
图片.png
注入对象类Injecting:
图片.png
DITest.XML
图片.png
运行代码:
图片.png
运行结果:
图片.png
分析:
图片.png
可以直观地看到, 在Injected类的定义中,需要有一个合作对象injecting,但是并没有类似于“Injecting injecting = new Injecting();”的赋值操作。而现在事实是:程序运行过程中,我们通过injected.getInjecting();方法得到了合作对象injecting。那是谁向Injected对象注入了合作对象injecting呢?答案是IOC容器。
IOC容器生成了injected对象和injecting对象,并且为injeted类对象注入了一个injecting对象,构建两个对象之间的依赖关系。这种思想称为Dependency Injection(依赖注入)。
在这个Demo中,我们失去了以下几种权利:1、如上文所说,失去了创建Injected和Injecting对象的主动权。2、构建Injected对象和它的依赖对象Injecting对象之间的这种依赖关系的时机的主动权失去了。这些权利通通都转交给IOC,这样的目的是让对象间的依赖关系从代码层面抽离出来,转而让DITest.xml去描述对象间的依赖关系。
网友评论