课时4:对象内存分析
摘要:接下来对对象实例化操作展开初步分析。在整个课程学习中,对象使用环节往往是最棘手的问题所在。
因为在Java里,类属于引用数据类型,引用数据类型的最大难点在于需进行内存管理,且在执行相关操作时,内存关系会随之发生变化。此次针对之前程序中的内存关系做简要分析,鉴于完整分析过程颇为繁杂,先进行简单剖析。
以如下程序作为主要分析对象,即程序中创建实例化对象,进而进行属性设置以及方法调用的操作。
public class JavaDemo{ public static void main(String args[]){ Person per=new Person();//声明并实例化对象 per.name="张三"; per.age=18; per.tell(); //进行方法的调用
进行内存分析时,先给出两块基础的内存空间,分别是堆内存和栈内存。在学习方法递归时,曾接触过“Stack Overflow”(栈溢出)这一概念,其涉及栈出现的问题,且与后续要深入解释的栈相关联。
堆内存保存的是对象的具体信息。例如,有个人叫王建,说“王建干活”,干活的是王建这个人本身;说“王建去死”,也是这个人本身面临相应情况,王建是真实存在的实体,即便其改名,本质上还是那个人去干活。所以,堆内存用于存放对象具体信息。
栈内存类似整形变量,保存的是一块堆内存的地址,通过该地址能找到堆内存,进而获取对象的具体内容。
需注意,此概念后续还会深入分析,先建立基本印象即可。所有堆内存地址都有相应的地址编号,不同堆内存的编号不同。栈内存在整个处理过程中,其内容与对象信息相关,后续会围绕此进一步深入探讨。
在整个实际开发过程中,要进行对象真实且完整操作信息的保存。需明确,栈内存中实际存放的是对应内存块的地址数据,通过该地址能够找到对应的堆内存,进而借助堆内存开展完整的操作。
在此有个关键要点需牢记,在程序里,堆内存空间的开辟是通过“new”关键字来完成的。在整个项目中,“new”是内存分析里既令人头疼又极为重要的关键字。明确二者关系后,来看后续如何进一步处理。
在清楚上述对应关系后,针对之前的程序展开分析。为此,通过画图来梳理逻辑,一步步呈现出来。鉴于后续预计会进行诸多内存分析,需做好相应心理准备。
public class JavaDemo{ public static void main(String args[]){ Person per=new Person();//声明并实例化对象 per.name="张三"; per.age=18; per.tell(); //进行方法的调用
回到代码中,首先明确堆内存与栈内存的关系。第一点,堆内存依靠“new”来开辟。很清晰的是,当程序中出现“new”这个关键字时,基本就能确定对于当前程序而言,要在此处开辟一个堆内存空间,将其称作“开辟新的堆内存”。在Java中,“new”属于内存开辟的最高级别语句,如同拥有绝对权威,任何情况下只要出现“new”就必须开辟内存空间。
第二个问题,堆内存存放的是具体的数据。以“Person”为例,“Person”中有哪些数据呢?有两个,一是“name”,二是“age”。若代码中未给“name”赋值,其值为空;“age”的值为0。由此可得出结论,在定义保存时,在相应内存里就存有“name”和“age”这两个数据。
并且,所有的堆内存都有对应的物理内存地址,这是必然存在的。确定好这些语句相关内容后,再来探讨“new Person”这个操作能实现什么功能,以“Person”的本质来讲,其在整个代码中实际定义了什么内容。通过画图来进一步呈现,这是堆内存中保存的具体数据,前面已对此进行过分析。
然而,当下的问题在于为何要存储这些数据,是由类来定义这些数据的,其实就是person。
通过画图进一步阐释,因这部分内容很关键。在整个程序代码里,“new Person”直接决定了程序中要保存的数据内容。若换用其他类,比如“Dog”类,里面存储的就是与“Dog”相关的数据,二者互不相关。
至此,对程序的第一波分析已完成,程序按从左到右的顺序执行,这部分代码确定好后,接下来要考虑右边的内容,即栈内存。
已知栈内存应存放堆内存的地址,不过在这个程序中,发现其在栈内存里存了与“Person”相关的内容。正常而言,此处应存放地址,因只有通过地址才能引用对应的堆内存空间。为便于分析简化,可简单理解为对象的名称保存在了栈内存之中,这是一种简化理解方式。
要知道对象是程序中的元素,虽说此处告知有个“p”,其本应存放的是地址,但可简化理解为“p”描述的是栈内存中保存的一个名字。
如此,整个过程得以区分开。从整体的访问结构来看,能够得出相应的内存分析图,应能明白其中意思。这里存在引用关系,整个过程涉及到了栈内存,这便是第一行语句执行后呈现的效果。
接下来看第二个语句,即“per.name=张三”。若出现这样的语句,问题就比较清晰了。将相关代码复制出来放置在此处,之前的第一个图稍显复杂,第二个图恢复正常设计,不然不好描述。
来看“per.name”,严格来讲,“per”描述的是一个栈内存地址。而“per.name”指的是该地址所对应堆内存中的“name”属性。
所以此时意味着要将堆内存中的“name”赋值为“张三”。
同样地,对于代码“p.age=18”,这相当于把堆内存中的数据进行了改变,最终输出的结果就是“张三18”。若不信,可再次运行代码验证,确实会输出“张三18”,如此便完成了最基础的内存分析处理。
对于对象实例化,之前已分析过,有两种语法。一种是之前使用过的声明并实例化对象,另一种是分步完成操作。
下面针对分步的内存操作进行分析,来看操作代码,定义程序代码如下:
public class JavaDemo{ public static void main(String args[]){ Person per=new Person();//声明并实例化对象 per.name="张三"; per.age=18; per.tell(); //进行方法的调用 Person per=null;声明化对象 per.=new Person();实例化对象
第一个步骤称作声明对象,第二个步骤称作实例化对象,现在是分步操作,与之前的区别在于不是一步完成,而是分两步进行。不过结果是一样的,毕竟最终都得到了对象。那这两步具体是如何进行的呢?下面通过内存分析来梳理操作过程。
我们现在回到这个程序继续进行内存分析。
首先来看第一句“Person per=null;”,当出现“per=null”时,要知道,程序中堆内存和栈内存是始终需要关注的,这是关键所在。此时给了“null”,这里并没有“new”关键字,那就表明只是开辟了一个栈内存空间,并没有具体的指向。
而当执行第二句语法“per=new Person();”时,之前讲过,只要看到“new”关键字,不用思考,它肯定是用来开辟空间的,这几乎是一种条件反射了。一旦开辟空间,属性都是默认值,像这里的“name”默认是空值,不过千万不要忘记,在内存分析中,内存都是有地址的。
实际上这里一旦使用“new”开辟了新空间,这个新空间的值也一定会有一个地址保存在栈内存中,大家应该能明白这个意思吧。后续的赋值操作,和之前讲的那种情况并没有本质区别,最终输出的结果肯定是姓名为“张三”,年龄为“18”,这就形成了分步处理,这样就能看出两种操作方式的差别了。
但需要特别注意的一点是,所有的对象在调用类中的属性或方法时,必须要在实例化完成之后才可以执行。下面把错误代码展示出来,大家看一下,在整个代码中,
per.name="张三"; per.age=18; per.tell(); //进行方法的调用
只是声明了对象,并没有对对象进行实例化,所以此时是无法调用属性或方法的。关键在于,我们要清楚这种无法调用所带来的问题是什么。
对代码进行编译并再次执行,会出现两个问题。
1.Exception in thread"main"java lang NullPointer Exception
2.favaDemo.java:IH
在Java代码中,查看第11行per.name=null,这里存在没有对对象进行实例化就使用的情况。我们说这时就会出错,随后,程序中出现的“NullPointerException”(空指针异常),明确描述的就是一种空指向异常。
也就是说,在没有对对象进行实例化,也就是没有在堆内存开辟相应空间时,就会产生这样的问题,并且只有引用数据类型才会存在此类问题。比如在后续的开发过程中,“NullPointerException”会一直伴随着开发工作,同时一定要牢记,只有引用数据类型会出现“NullPointerException”,而基本数据类型是不存在这类问题的,因为基本数据类型本身就是实际的数值。
所以说,这是一个基础的内存分析结构,大家一定要清楚这个结构,以后切记一点,对象必须在实例化之后才能够使用,否则是无法使用的。