本篇文章主要内容:构造方法Constructor
空指针异常
当实例变量是一个引用
方法调用时参数的传递问题
难点解惑
构造方法Constructor
什么是构造方法?构造方法怎么定义?构造方法怎么调用?构造方法有什么作用?构造方法可以重载吗?接下来学习一下。
构造方法是类中特殊的方法,通过调用构造方法来完成对象的创建,以及对象属性的初始化操作。
构造方法怎么定义,请看以下的语法格式:[修饰符列表]构造方法名(形式参数列表){构造方法体;
}
① 构造方法名和类名一致。
② 构造方法用来创建对象,以及完成属性初始化操作。
③ 构造方法返回值类型不需要写,写上就报错,包括void也不能写。
④ 构造方法的返回值类型实际上是当前类的类型。
⑤ 一个类中可以定义多个构造方法,这些构造方法构成方法重载。
怎么调用构造方法呢,语法格式是:new构造方法名(实际参数列表);接下来,看以下代码:调用无参数构造方法
以上程序运行结果如下图所示:调用无参数构造方法的运行结果
以上程序的输出结果中虽然第一行看不懂,但起码说明程序是能够正常执行的。我们看到以上Date类当中的代码,并没有发现任何构造方法,为什么可以调用呢?接下来我们来测试一下,把构造方法显示的定义出来:
运行结果如下图所示:测试无参数构造方法
通过以上程序执行结果确实看到了“newDate()”确实调用了Date类当中的无参数构造方法。再看以下程序:
编译报错了,错误信息如下图所示:编译器错误信息提示
通过以上的测试,得出这样一个结论(这是java中语法的规定,记住就行):当一个类没有显示的定义任何构造方法的时候,系统默认提供无参数构造方法,当显示的定义构造方法之 后,系统则不再提供无参数构造方法。无参数构造方法又叫做缺省构造器,或者默认构造方法。一般在开发中为了方便编程,建议程序员手动的将无参数构造方法写上,因为不写无参数构造 方法的时候,这个默认的构造方法很有可能就不存在了,另外也是因为无参数构造方法使用的 频率较高。例如以下代码:
运行结果如下图所示:调用所有构造方法
通过以上的测试可以看出一个类当中可以定义多个构造方法,构造方法是支持重载机制的,具体调用哪个构造方法,那要看调用的时候传递的实际参数列表符合哪个构造方法了。构造方法虽然在返回值类型方面不写任何类型,但它执行结束之后实际上会返回该对象在堆内存当中的内存地址,这个时候可以定义变量接收对象的内存地址,这个变量就是之前所学的“引用”,请看以下代码:
运行结果如下图所示:构造方法的返回值
以上程序中time1,time2,time3,time4都是引用,输出这些引用的结果是“Date@xxxxx”,对于这个结果目前可以把它等同看做是对象的内存地址(严格来说不是真实的对象内存地址)。通过这个引用就可以访问对象的内存了,例如以下代码:
运行结果如下图所示:通过“引用”访问属性
为什么无论通过哪个构造方法创建Date对象,最终的结果都是“0年0月0日”呢?
这是因为所有的构造方法在执行过程中没有给对象的属性手动赋值,系统则自动赋默认值,实际上大部分情况下我们需要在构造方法中手动的给属性赋值,这本来就是构造方法的主要的职责, 要不然重载多次构造方法就没有意义了,以上的代码应该这样写,请看:
运行结果如下图所示:通过构造方法给属性赋值
为什么第一个日期输出的是“0年0月0日”?这是因为调用的是无参数构造方法创建的第一个日期对象,在无参数构造方法中没有给属性赋值,则系统赋默认值,所以年月日都是0。
那为什么第二个日期输出的是“2008年0月0日”呢?这是因为调用的是“public Date(intyear1){year=year1; }”构造方法创建的第二个日期对象,在这个构造方法当中只是显示的给Date对象的year属性赋值,month和day仍然是系统赋默认值,所以month和day都是0。第三个日期对象是“2008年8月8日”,这是因为在这个对应的构造方法中显示的为year,month,day三个属性都赋值了。
在编写以上构造方法的时候需要注意变量名的问题,请看下图:java 遵循就近原则
通过以上内容的学习得知,构造方法的作用是专门用来创建对象同时给属性赋值的,它的语法很简单,比普通方法还要简单,因为构造方法名和类名一致,还不需要写返回值类型,使用new就可以调用了。在一个类当中可以同时定义多个构造方法,它们之间构成重载关系。这样就做到了在java中你想要什么就new什么,每一次new都会在堆内存中创建对象,并且对象内部的实例变量被初始化了。
一定要注意,实例变量没有手动赋值的时候系统会默认赋值,但不管是手动赋值还是系统赋默认值,都是在构造方法执行的时候才会进行赋值操作,类加载的时候并不会初始化实例变量的空间,那是因为实例变量是对象级别的变量,没有对象,哪来实例变量,这也是为什么实例变量不能采用“类名”去访问的原因。
编译报错了:实例变量不能直接采用“类名”访问
空指针异常
当一个空的引用去访问实例变量会出现什么问题吗?请看以下代码:
运行结果如下图所示:空指针异常演示
java.lang.NullPointerException被称为空指针异常,在java编程当中属于很常见的异常,接下来研究一下以上程序执行过程的内存图是如何变化的。请看下图:Balloon ball = new Balloon("红色" , "氢气");
ball = null;
以上程序语法正确,编译通过,因为程序在编译阶段检测出“引用ball”属于Balloon类型,在Balloon类中有color属性,所以编译器允许通过ball引用去访问color属性,例如以上代码的ball.color。但是程序在运行阶段会通过ball引用查找堆内存当中的对象,因为color是实例变量,该变量存储在java对象内部,当ball=null执行之后表示“引用ball”不再保存java对象的内存地址,换句话说通过ball引用已经无法找到堆内存当中的java对象了,对于程序来说这个时候就没有办法正常访问了,这种情况下就会发生空指针异常。就好比一个小孩儿放风筝,通过拽线来操控风筝,结果线断了,再拽风筝线的时候,已经无法再操控风筝了,这对于小孩儿来说是一种异常。而java程序中把这种异常叫做NullPointerException。
总之,当一个“空的引用”去访问“对象相关/实例相关”数据的时候,此时一定会发生空指针异常。
当实例变量是一个引用
在以上内容学习的过程当中,其实大家已经接触过实例变量是引用的情况,不知道吧!例如在Student学生类当中有一个属性“Stringname;”,这个属性/实例变量name描述的是学生的姓名,name变量的数据类型是String类型,String类型不属于基本数据类型的范畴,也就是说String类型属于引用数据类型,换句话说String类型应该对应一个String.class文件才对,String是一个类,和我们自己定义的类没什么区别,是这样吗?一起来看看JDK的java源代码:jdk java 源代码位置
String 类源代码位置
String 类源代码
通过查看源代码得知,其实String是一个class,和我们定义的类没有区别,它和基本数据类型还是不一样的(int i=10,i变量是基本数据类型,i变量中存储的就是10),也就是说Stringname=“zhangsan”,实际上name变量中存储的并不是”zhangsan”这个字符串,因为name是一个引用,那name中必然存储的是”zhangsan”字符串对象的内存地址。
因为之前我们说过引用的概念,什么是引用:引用就是一个变量,只不过该变量中存储的是java对象的内存地址。也就是说,以前所讲内容中使用内存图描述字符串的时候都是有偏差的。我们来修正一下, 请看代码:
以上程序的运行结果请看下图:运行结果
将以上内存结构图画出来,请看:修正字符串对象的内存
通过上图可以看到,Student对象当中的name这个“属性/实例变量”是一个引用,保存的不是”zhangsan”字符串,而是字符串对象的内存地址。(按照实际来说,字符串”zhangsan”是在方法区的字符串常量池当中,这个后期再继续进行修正)。接下来,我们再来看看当属性是其他类型引用的时候,请看代码:
运行结果如下图所示:运行结果
以上程序main 方法的内存结构图如下所示:Date d = new Date(1983 , 5 , 6);
Vip v = new Vip(123 , "jack" , d);
通过以上内容的学习大家掌握当对象的属性是一个引用的时候内存图是怎样的了吗?其实只要记住一点,任何“引用”当中存储一定是对象的内存地址,“引用”不一定只是以局部变量的形式存在,例如以上程序,其中Vip类当中的birth属性就是一个“引用”,它是一个实例变量。当一个对象的属性是引用的时候应该如何访问这个引用所指向的对象呢?这里其实有一个规律,大家记住就行:类当中有什么就可以“.”什么,例如:Vip类中有birth属性,那么就可以v.birth,那么v.birth是Date类型,Date类当中有year属性,那么就可以v.birth.year,你懂了吗?
方法调用时参数的传递问题(理解)
方法在调用的时候参数是如何传递的呢?其实在调用的时候参数传递给方法,这个过程就是赋值的过程,参数传递和“赋值规则”完全相同,只不过参数传递在代码上看不见“=”运算符。我们先来深入的研究一下“赋值规则”吧!
在以上程序当中,有两个疑问,第一个:a 赋值给 b,a 把什么给了 b?第二个:bird1 赋值给 bird2, bird1 把什么给了 bird2?
其实 a,b,bird1,bird2 就是 4 个普通的变量,唯一的区别只是 a 和 b 都是基本数据类型的变量,bird1 和 bird2 都是引用数据类型的变量(或者说都是引用),a 变量中保存的那个“值”是 10, bird1 变量中保存的那个“值”是 0x8888(java 对象内存地址),本质上来说 10 和 0x8888 都是“值”, 只不过一个“值”是整数数字,另一个“值”是 java 对象的内存地址,大家不要把内存地址特殊化, 它也是一个普通的值。
那么“赋值”是什么意思呢,顾名思义,赋值就是把“值”赋上去。a 赋值给 b,本质上不是把 a 给了 b,而是把 a 变量中保存的“值 10”复制了一份给了 b。bird1 赋值给 bird2 本质上不是把 bird1 给了 bird2,而是把 bird1 变量中保存的“值 0x8888”复制了一份给了 bird2。请看以下内存图的变化:
赋值原理图
通过以上内存图我们可以看出“赋值”运算的时候实际上和变量的数据类型无关,无论是基本数据类型还是引用数据类型,一律都是将变量中保存的“值”复制一份,然后将复制的这个“值”赋上去。他们的区别在于,如果是基本数据类型则和堆内存当中的对象无关,如果是引用数据类型由于传递的这个值是java对象的内存地址,所以会导致两个引用指向同一个堆内存中的java对象,通过任何一个引用去访问堆内存当中的对象,此对象内存都会受到影响。我们来验证一下,让a++,a应该变成了11,但是b不会变,让bird1.name = “波利”,然后输出bird2.name的结果肯定也是”波利”,请看代码以及运行结果:
运行结果如下图所示:赋值原理测试
上面我就提到了,方法调用时参数的传递和赋值运算符的原理完全相同,那么请大家根据以上内容所学,画出以下程序的内存图,以及推算它们的执行结果:
难点解惑
对于初学者来说,本章节内容是比较艰难的一个章节,主要是因为涉及到程序执行过程中内存的变化。本章节中要想搞明白所有的难点,只需要搞定一点就行,那就是亲手画出每个程序执行时的内存图。
在这里我要提醒大家的有两点:先要记住Java虚拟机内存管理这块有哪些内存空间,每一个空间中都存储什么;
代码要遵循一行一行逐行执行,每执行一行则需要在内存方面 发生一些变化。只要大家能把握以上两点,对于空指针异常、实例变量为引用类型、方法调用时参数传递问题等迎刃而解。
小结
通过本章节内容的学习,需要大家掌握的是一个类定义好之后,怎么创建对象,对象创建之后怎么去使用这个对象,代码要会写。另外还需要掌握构造方法怎么定义、怎么调用。需要理解构造方法在开发中的作用。
在本章节内容中大家尤其要对Java虚拟机内存管理这块要有深入的理解。要知道Java虚拟机中的栈和堆各自存储什么,要能够画出程序执行过程中内存的变化。要知道空指针异常是如何发生的,为什么会出现空指针。
除以上所描述之外,大家还需要掌握方法调用时参数是如何传递的,一定要弄明白这个知识点,在以后的开发中会频繁的进行方法的调用,而方法调用时需要传递参数,参数传递时内存是如何变化的,弄明白这些有助于你对程序运行结果的把控。
最后附Java零基础视频教程给大家,配合学习效果更佳!!