4.5 封装与private
扫码看视频
封装是面向对象的基本特性之一。想想你使用的手机、观看的电视、驾驶的汽车,你都是通过这些对象预先定义好的接口来操作它们(通过说明书告知你)的,但这些对象内部的状态和内部的工作机制你知道吗?不知道,也不需要知道,作为用户,我们只需要知道按下一个按键,对象能做出正确的响应就行了。这就是对象的封装!屏蔽复杂的实现细节,通过公开、暴露的方法来操作对象,使得对象的操作简单又高效。
前面我们讲述了类的设计,在类中定义了数据成员和方法,当实例化一个类的对象后,通过该对象可以任意访问类中的数据成员和方法,这显然不符合真实的对象设计。在现实世界中,对象是独立的、自治的个体,不能任意去修改某个对象的状态,比如你的父母希望你长高1厘米,他们不能直接去修改你的身高,合理的方式是通过调用你的吃饭方法,给你传入牛奶、鸡蛋、牛肉等,调用你的运动方法,然后你的身体内部的工作流程经过计算,在条件满足后,身高增长1厘米。
为了对外屏蔽一些类的实现细节,我们需要用到private关键字,这是一个访问说明符,用于说明类中的成员是私有的,对外不可以访问。
通常来说,类中的成员变量都应该声明为private,以防止外部直接访问这些变量,例如,代码4.20中的Animal类的成员变量height和weight,我们就应该将它们声明为private,如代码4.21所示。
上述代码在编译时会提示如图4-3所示的错误信息。
图4-3 在类的外部访问其私有成员变量报错
现在,Animal的成员变量height和weight只能在调用Animal的构造方法时通过传入参数来改变,如果希望在对象构造完毕后,还有机会修改这两个变量,以及能够得到它们的值,那么可以为它们提供访问器方法。
所谓访问器方法,就是针对私有的成员变量提供一对get/set方法,方法的命名一般是get/set + 成员变量名大写的首字母 + 成员变量名剩余字母。例如,针对Animal类的成员变量height和weight,提供的访问器方法如代码4.22所示。
有的读者可能要问,为何要多此一举,而提供两个方法来完成成员变量的读写操作,还不如放开权限,让用户直接访问成员变量。要注意,这一切都是为了访问控制!如果让用户可以直接修改对象的状态,那么对象的状态将变得不可预知,也破坏了我们前面所说的封装,虽然这里看到的访问器方法实现很简单,只是简单地取值和赋值,但是不要忘了,这毕竟是方法,所以在需要的时候,我们完全可以在访问器方法内部添加额外的代码来进行如逻辑验证、权限判断等操作,只有在满足条件时才允许用户访问。
此外,访问器方法并没有要求说一定要有,这根据你的类的设计需求来决定。也没有说必须要成对提供,如果你的成员变量是只写访问,那么提供一个setXxx方法即可;如果是只读访问,那么提供一个getXxx方法即可。
题外话 如果一个类满足以下的条件,则称之为JavaBean组件。
(1)是一个公开的(public)类。
(2)有一个默认的构造方法,也就是不带参数的构造方法(在实例化JavaBean对象时,需要调用默认的构造方法)。
(3)提供setXxx方法和getXxx方法(访问器方法)来让外部程序设置和获取JavaBean的属性。
(4)实现java.io.Serializable或者java.io.Externalizable接口,以支持序列化。
属性(Property)是JavaBean组件内部状态的抽象表示,外部程序使用属性来设置和获取JavaBean组件的状态。为了让外部程序能够知道JavaBean提供了哪些属性,JavaBean的编写者必须遵循标准的命名方式。
例如,类中有一个实例变量为username,有一对方法getName和setName用于获取和设置实例变量username的值,那么属性名为name。也就是说,JavaBean的属性和实例变量不是一个概念,属性和实例变量也不是一一对应的关系,属性可以不依赖于任何的实例变量而存在,而是JavaBean组件内部状态的抽象表示。
例如:
如果没有setInfo方法,那么info就是一个只读的属性,且没有对应任何的实例变量。
get/set命名方式有一个例外,那就是对于boolean类型的属性应该使用is/set命名方式(也可以使用get/set命名方式),例如,有一个boolean类型的属性married,它所对应的方法如下:
除了成员变量一般都声明为private,很多时候,我们也将有些方法声明为private,这些方法大多是用于辅助完成某个功能的,它们并不是提供给外部用户调用的,而是用于内部逻辑的辅助实现。声明为private的方法(即私有的方法),也不能在类的外部被访问,我们看代码4.23。
在Fish类中定义了一个私有的方法log,它负责在每次swim方法被调用的时候都做一个记录。在Fish类的内部,可以随意访问log方法,但是在类的外部,就不能被访问了。
上述代码在main方法中提出了一个问题,这个问题也是很多新手没有搞明白的,特别是从C++转而学习Java的读者。按理说,fh.log()这句代码相当于是在类的外部访问了私有方法,在编译时理应报错,但实际情况却是没有错误。这里容易让人迷惑的地方主要是main方法,main方法作为程序的入口方法,在Java程序中必须要放到一个类中,即使该方法跟这个类没有任何成员关系。上述代码的main方法在Fish类中,你也可以将它看成是Fish类的一个静态方法,fh.log()的调用仍然是在Fish类的内部完成的,所以编译时并不会报错。如果你将main方法移到Animal类中,在编译时,就会提示如图4-4所示的错误信息。
图4-4 在类的外部访问私有的成员方法报错
读者可以思考一下,在子类中能不能访问父类中的私有方法?能不能覆盖私有方法?要注意,私有的成员只能在该类的内部被访问,子类也算类的外部,所以是不能访问的,自然也就不能被覆盖了。
我们看代码4.24,其在编译时,会提示如图4-5所示的错误。
图4-5 子类访问父类的私有方法报错
关于更多的访问说明符,请读者参看第5章。