5.2 类与对象
类与对象是面向对象中最为核心的两个组成元素。只有掌握了类与对象的定义及使用之后才可以更好地掌握面向对象编程的特点,所以本节将为读者完整地分析类与对象的关系以及相关操作说明。
5.2.1 类与对象的关系
图5-1 类与对象的关系
在面向对象中类和对象是最基本、最重要的组成单元。类实际上是表示一个客观世界某类群体的一些基本特征抽象。对象是表示一个个具体的东西。例如,在现实生活中,人就可以表示为一个类,因为人本身属于一种广义的概念,并不是一个具体的。而某一个具体的人,就可以称为对象,可以通过各种信息完整的描述这个具体的人,如这个人的姓名、年龄、性别等信息,这些信息在面向对象的概念中就称为属性,当然人是可以吃饭、睡觉的,那么这些人的行为在类中就称为方法。也就是说如果要使用一个类,就一定有产生对象,每个对象之间是靠各个属性的不同来进行区分的,而每个对象所具备的操作就是类中规定好的方法。类与对象的关系如图5-1所示。
提示
类与对象的另一种解释。
关于类与对象,初学者在理解上是存在一定难度的,在此处笔者再为各位读者做一个简单的比喻,读者应该都很清楚,如果要想生产出汽车,则首先一定要设计出一个汽车的设计图纸,之后按照此图纸规定的结构生产汽车。这样生产出的汽车结构和功能都是一样的,但是每辆车的具体内容,如各个汽车颜色、是否有天窗等都会存在一些差异。
在这个实例中,汽车设计图纸实际上就是规定出了汽车应该有的基本组成:包括外型、内部结构、发动机等信息的定义,那么这个图纸就可以称为一个类,显然只有图纸是无法使用的;而通过这个模型产生出的一辆辆的汽车是可以被用户使用的,所以就可以称其为对象。
5.2.2 类的定义
从类的概念中可以了解到,类是由属性和方法组成的。属性中定义类需要的一个个的具体信息,实际上一个属性就是一个变量,而方法是一些操作的行为,但是在程序设计中,定义类也是要按照具体的语法要求完成的,类的定义语法如下:
【格式5-1 类的定义】
下面根据以上的格式定义一个Person类。
提示
属性也可以称为变量
在类中的属性实际上也就是相当于一个个的变量,有时候也称之为Field(成员)。
【例5.1】定义Person类
以上程序中在Person类中定义了name、age两个属性,分别表示人的姓名和年龄,之后定义了一个tell方法,此方法的功能就是打印这两个属性的内容。
提示
类中定义方法的补充说明。
读者可以发现,此处的方法与之前讲解的方法定义有些区别,并没有加上“static”关键字,这是因为此时定义的方法将要由对象调用,而不像之前那样与主方法定义在一个类中且由主方法直接调用。
图5-2 Person的类图表示
类定义完成之后,可以表示出类的定义如图5-2所示。
图5-2所示的图形分为3个层次:
(1)第1层表示类的名称,类的名称与之前一样要求开头首字母大写;
(2)第2层表示属性的定义,按照“访问权限属性名称:属性类型”的格式定义,在本类中因为声明属性处没有写任何的访问权限,所以前面暂时不加任何的符号;
(3)第3层表示类中方法的定义,按照“访问权限方法名称():方法返回值”的格式定义,在本类中方法的声明处加上了public(此为访问权限,表示任何地方都可以访问),所以使用“+”表示。另外如果方法中有传递的参数,则方法定义格式为:“访问权限方法名称(参数名称:参数类型,参数名称:参数类型,……):方法返回值”。
在实际的开发中,以上的图形出现较多,本书会在后续的部分陆续介绍各种常见图形的画法。
5.2.3 对象的创建及使用
当创建好了Person类之后,使用这个类,则需要通过对象,下面给出了对象的语法格式:
【格式5-2 对象的创建】
以上格式的对象产生分为声明对象与实例化对象两步。当然也可以直接通过以下的方式一步完成。
细心的读者可以发现,以上的格式与之前的数组定义的格式很相似,因为类和数组都属于引用数据类型,只要是引用数据类型的使用格式都可以使用上面的定义样式。
【例5.2】对象创建了解了上述的格式之后,下面就来看一下创建对象的具体范例
以上程序在主方法中实例化了一个Person对象,对象名称为per。与之前的数组开辟空间的道理一样,对象的实例化也是要划分堆、栈空间的,具体的内存分配如图5-3所示。
从图5-3中可以发现,所有的对象名称都在栈内存中保存,而对象的具体内容则保存在对应的堆内存之中,必须使用new关键字才能开辟堆内存空间,堆内存里保存的属性信息,在此时,因为per对象刚刚被实例化完,所以对象里面的属性内容都是默认值,字符串的默认值是null,整数的默认值是0。
提示
栈中存放的是堆空间的地址。
在本书中对于引用关系,为了让读者理解起来更加容易,所以在讲解时会以“将对象名称保存在栈内存之中”这样的方式表示栈的作用,实际上更准确的说法是“在栈内存中实际上保存的是堆内存空间的访问地址”。
如果要使用对象访问类中的某个属性或方法可以使用如下的语法实现:
【格式5-3 访问对象中的属性或方法】
【例5.3】为对象的属性设置内容,同时调用tell()方法把内容输出
程序执行结果:
本程序中为属性赋值,并通过类中提供的方法把内容直接输出,属性赋值完之后的内存如图5-4所示。
图5-3 对象的实例化过程
图5-4 为对象中的属性赋值
注意
对象使用前必须实例化。
读者必须引起注意的是,如果一个对象要被使用,则对象必须被实例化(在本书后面的部分将直接把已经实例化好的对象,称为实例化对象),如果一个对象没有被实例化而直接调用了对象里的属性或方法的,如下代码所示:
则程序运行时会出现以下的异常:
此异常是开发中最常见的异常,也会始终伴随着每位开发人员,在此读者一定要记住,此问题只会出现在引用数据类型中,只要使用了未实例化的对象则肯定会出现此异常。
5.2.4 创建多个对象
在开发中,每个类都可以产生多个实例化对象,只要在实例化对象时使用了关键字new,那么就表示开辟新的堆内存空间,而每一个实例化对象都将会占据自己的堆、栈内存空间。
【例5.4】创建两个对象
程序执行结果:
由程序的运行结果可以发现,程序分别实例化了两个Person对象,那么也就意味着per1和per2对象分别指向各自的堆内存空间,如图5-5所示。
图5-5 实例化两个对象
类属于引用数据类型,而且从数组的使用上也可以发现,引用数据类型就是指一段堆内存空间可以同时被多个栈内存所指向。下面来看一个引用传递的简单范例。
【例5.5】对象引用传递
程序执行结果:
从程序运行结果可以发现,两个对象的输出内容是一样的,实际上所谓的引用传递,就是将一个堆内存空间的使用权给多个栈内存空间,每个栈内存空间都可以修改堆内存的内容,此程序的内存图如图5-6所示。
图5-6 对象引用传递的内存分配图
从图5-6中可以发现内存空间的堆、栈指向变化,程序最后由per2将age的值修改为33,所以最终结果per1中的age属性值也就是33了。
综上所述引用传递的本质就是在于不同的栈内存空间将指向同一块堆内存(一个栈内存只能够保存一个堆内存空间的地址),这样不同的栈内存将指向同一块堆内存,并且都可以对指向的堆内存空间数据进行修改。
【例5.6】对象引用传递—观察垃圾的产生
程序执行结果:
本程序最大的特点是在于发生引用传递前per2对象已经有了指向的堆内存空间,内存关系图如图5-7所示。
图5-7 垃圾对象的产生
图5-7 垃圾对象的产生(续)
提示
关于垃圾空间的释放。
Java本身提供垃圾收集机制(简称GC,Garbage Collection),会不定期的释放不用的内存空间,只要对象不使用了,就会等待GC释放空间。
从上面程序中必须明确地知道一点:一个栈内存空间只能指向一个堆内存空间,如果要想再指向其他的堆内存空间,则必须先断开已有的指向才能分配新的指向。