
5.2 类与对象

在面向对象中类和对象是最基本、最重要的组成单元,那么什么叫类呢?类实际上是表示一个客观世界某类群体的一些基本特征抽象,属于抽象的概念集合。而对象呢?就是表示一个个具体的、可以操作的事物,例如,张三同学、李四账户、王五的汽车,这些都是可以真实使用的事物,那么就可以理解为对象,所以对象表示的是一个个独立的个体。
例如,在现实生活中,人就可以表示为一个类,因为“人”属于一个广义的概念,并不是一个具体个体描述。而某一个具体的人,例如,张三同学,就可以被称为对象,可以通过各种信息完整地描述这个具体的人,如这个人的姓名、年龄、性别等信息,那么这些信息在面向对象的概念中就被称为成员(或者成员属性,实际上就是不同数据类型的变量,所以也被称为成员变量),当然人是可以吃饭、睡觉的,那么这些人的行为在类中就被称为方法。也就是说,如果要使用一个类,就一定有产生对象,每个对象之间是靠各个属性的不同来进行区分的,而每个对象所具备的操作就是类中规定好的方法,类与对象的关系如图5-1所示。
提示:类与对象的简单理解。
在面向对象中有这样一句话可以很好地解释类与对象的区别:“类是对象的模板,而对象是类的实例”,即对象所具备的所有行为都是由类来定义的,按照这种理解方式,在开发中,应该先定义出类的结构,之后再通过对象来使用这个类。

图5-1 类与对象的关系
通过图5-1可以发现,一个类的基本组成单元有两个。
成员属性(field):主要用于保存对象的具体特征。例如,不同的人都有姓名、性别、学历、身高、体重等信息,但是不同的人都有不同的内容定义,而类就需要对这些描述信息进行统一的管理。
方法(method):用于描述功能,例如,跑步、吃饭、唱歌,所有人的对象都有相同的功能。
提示:类与对象的另一种解释。
关于类与对象,初学者在理解上可能存在一定的难度,这里做一个简单的比喻。读者应该都很清楚,如果要想生产出轿车,则首先一定要设计出一个轿车的设计图纸(见图5-2),之后按照此图纸规定的结构生产轿车。这样生产出的轿车结构和功能都是一样的,但是每辆车的具体配置,如各个轿车颜色、是否有天窗等都会存在差一些差异。
在这个实例中,轿车设计图纸实际上就是规定出了轿车应该有的基本组成:包括外形、内部结构、发动机等信息的定义,那么这个图纸就可以称为一个类,显然只有图纸是无法使用的;而通过这个模型产生出的一辆辆的具体轿车是可以被用户使用的,所以就可以称为对象。

图5-2 轿车设计图纸
5.2.1 类与对象的定义

类是由成员属性和方法组成的。成员属性主要定义类的一个具体信息,实际上一个成员属性就是一个变量,而方法是一些操作的行为。但是在程序设计中,定义类也是要按照具体的语法要求来完成的,例如要定义类需要使用class关键字定义,类的定义基础语法如下。

根据给定的类定义结构可以发现,一个类结构本质上就是相关变量与方法的结合体,下面依据此格式定义一个类。
范例:类的定义

本程序定义了一个Person类,里面有两个成员属性name(姓名,String型)、age(年龄,int型),而后又定义了一个tell()方法,该方法可以输出这两个成员属性的内容。
提问:为什么Person类定义的tell()方法没有加上static?
在第4章学习方法定义的时候要求方法前必须加上static,为什么在Person类定义的tell()方法前不加static?
回答:调用形式不同。
在第4章讲解方法的时候是这样要求的:“在主类中定义,并且由主方法直接调用的方法必须加上static”,但是现在的情况有些改变,因为Person类的tell()方法将会由对象调用,与之前的调用形式不同,所以暂时没有加上。读者可以先这样简单理解:如果是由对象调用的方法定义时不加static,如果不是由对象调用的方法才加上static,而关于static关键字的使用,在本章的后面会为读者详细讲解。
一个类定义完成后并不能够被直接使用,因为类描述的只是一个广义的概念,而具体的操作必须通过对象来执行,由于类属于Java引用数据类型,所以对象的定义格式如下。

在Java中引用数据类型是需要进行内存分配的,所以在定义时必须通过关键字new来分配相应的内存空间后才可以使用,此时该对象也被称为“实例化对象”,而一个实例化对象就可以采用以下的方式进行类结构的操作。
对象.成员属性:表示调用类之中的成员属性,可以为其赋值或者获取其保存内容。
对象.方法():表示调用类之中的方法。
范例:通过实例化对象进行类操作

本程序通过关键字new取得了Person类的实例化对象,当获取了实例化对象之后就可以为类中的属性赋值,并且实现类中方法的调用。
提示:关于类中成员属性默认值。
在本书第2章中为读者讲解过数据类型的默认值问题,并且强调过,方法中定义的变量一定要进行初始化,但是在进行类结构定义时可以不为成员变量赋值,这样就会使用默认值进行初始化。
范例:观察类中成员属性默认值


本程序实例化了Person类对象之后并没有为成员属性进行赋值,所以在调用tell()方法输出信息时,name内容为null(String类为引用数据类型),age内容为0(int型默认值)。
5.2.2 对象内存分析

Java中类属于引用数据类型,所有的引用数据类型在使用过程中都要通过关键字new开辟新的内存空间,当对象拥有了内存空间后才可以实现成员属性的信息保存,在引用数据类型操作中最为重要的内存有两块(关系如图5-3所示)。
【heap】堆内存:保存的是对象的具体信息(成员属性),在程序之中堆内存空间的开辟是通过new完成的。
【stack】栈内存:保存的是一块堆内存的地址,即通过地址找到堆内存,而后找到对象内容,但是为了分析简化起见可以简单地理解为对象名称保存在了栈内存之中。

图5-3 内存结构
提示:关于方法信息的保存。
类中所有的成员属性都是每个对象私有的,而类中的方法是所有对象共有的,方法的信息会保存在“全局方法区”这样的公共内存之中。
程序中每当使用了关键字new都会为指定类型的对象进行堆内存空间的开辟,在堆内存中会保存有相应的成员属性信息。这样当对象调用类中方法进成员属性信息时,会从对象对应的堆内存中获取相应的内容,以下面的程序为例进行类引用数据类型的使用分析。
范例:类引用数据类型使用分析

本程序最为重要的内存操作为对象的实例化以及属性赋值操作,内存分配流程如图5-4所示。

图5-4 对象实例化与成员属性赋值内存操作
从图5-4读者可以发现,实例化对象一定需要对应的内存空间,而内存空间的开辟需要通过关键字new来完成。每一个对象在刚刚实例化之后,里面的所有成员属性的内容都是其对应数据类型的默认值,只有设置了成员属性的内容之后,成员属性才可以替换为用户所设置的数据。
提示:关于后续内存图描述。
所有的堆内存都会有相应的内存地址,同时栈内存会保存堆内存的地址数值。后续的讲解中为了方便读者理解程序,将采用简单的描述形式,即栈内存中保存的是对象名称。
在进行对象定义时除了在声明时实例化之外,也可以采用先定义对象,再通过关键字new实例化方式完成。
范例:对象实例化处理

本程序分两步实现了Person类对象的实例化操作,程序的内存关系如图5-5所示。

图5-5 内存分析
注意:对象使用前首先必须进行实例化操作。
引用数据类型在使用之前进行实例化操作,如果在开发之中出现了以下代码,那么肯定会在程序运行时产生异常。
范例:产生异常的代码

这个异常信息表示的是NullPointerException(空指向异常),这种异常只会在引用数据类型上产生,并且只要是进行项目的开发,都有可能会出现此类异常,而此类异常出现的唯一解决方法就是:查找引用数据类型,并观察其是否被正确实例化。
5.2.3 对象引用传递分析

类是一种引用数据类型,而引用数据类型的核心本质在于堆内存和栈内存的分配与指向处理。在程序开发中,不同的栈内存可以指向同一块的堆内存空间(相当于为同一块堆内存设置不同的对象名称),这样就形成了对象的引用传递过程。
提示:引用传递的简单理解。
首先所有的读者一定要清楚一件事情:程序来源于生活,只是对生活的更理性抽象。本着这个原则对于对象引用传递可以换种简单的方式来理解。
例如,现在有一位逍遥自在的小伙子叫“张麻蛋”,在村里人都叫他的乳名“麻雷子”,江湖都叫他“麻子哥”,有一天张麻蛋出去办事结果不小心被车撞断了腿,导致了骨折,而此时“麻雷子”与“麻子哥”也一定会骨折,也就是说一个人有多个不同的名字(栈内存不同),但是不同的对象名称可以指向同一个实体(堆内存),这实际上就是引用传递的本质。
范例:引用传递

本程序中重要的代码为“Person per2=per1”,该程序代码的核心意义在于,将per1对象堆内存的地址赋值给per2,这样就相当于两个不同的栈内存都指向了同一块堆内存空间,程序的内存结构如图5-6所示。

图5-6 引用传递内存分析
在实际的项目开发中,引用传递使用最多的情况是结合方法来使用,即可以通过方法的参数接收引用对象,也可以通过方法返回一个引用对象。
范例:通过方法实现引用传递


本程序定义了change()方法,并且在方法上接收了Person类型的引用对象,这样当通过change()方法的temp对象进行属性修改的时候将会影响到原始对象内容,程序的内存分析如图5-7所示。

图5-7 基于方法实现引用传递
5.2.4 引用传递与垃圾产生分析

引用传递的本质意义在于,一块堆内存空间可以被不同的栈内存所引用,每一块栈内存都会保存有堆内存的地址信息,并且只允许保存一个堆内存地址信息,即如果一块栈内存已经存在有其他堆内存的引用,当需要改变引用指向时就需要丢弃已有的引用实体更换为新的引用实体。
范例:引用传递与垃圾产生


本程序实例化了两个Person类对象(per1和per2),并且分别为这两个对象进行赋值,但是由于发生了引用传递“per2=per1”,所以per2将丢弃原始的引用实体(产生垃圾),将引用指向per1的实体,这样当执行“per2.age=80”语句时修改的就是per1对象的内容。本程序内存关系如图5-8所示。
通过图5-8可以发现本程序中的一个问题,per1和per2两个栈内存都各自保存着一块堆内存空间指向,而每一块栈内存只能够保留一块堆内存空间的地址。所以当程序发生了引用传递(per2=per1)时,per2首先要断开已有的堆内存连接,而后才能够指向新的堆内存(per1所指向的堆内存)。但是由于此时per2原本指向的堆内存空间没有任何栈内存对其进行引用,该内存空间就将成为垃圾空间,所有的垃圾空间将等待GC(Garbage Collection,垃圾收集器)不定期地进行回收释放。

图5-8 垃圾产生分析
提示:开发之中尽量减少垃圾产生。
虽然Java本身提供了自动垃圾收集机制(提供有自动GC和手动GC处理),但是在代码编写中,如果产生了过多的垃圾,也会对程序的性能带来影响,所以在开发人员编写代码的过程之中,应该尽量减少无用对象的产生,避免垃圾的产生。