封装的意义是:尽量避免向外部暴露实现细节,只提供个别接口让使用方调用,降低耦合性。这样做的话,当自身的逻辑发生变化时,不会破坏使用方的逻辑,或是强制使用方修改自身的逻辑,而是只需要修改自身的代码就可以了。
假如某天汽车这个类,它的启动方法需要增加一个连通电路4的调用,如果没有封装,那么作为使用者的我们肯定要骂娘,甚至会劝说维护汽车的生产商,不要改代码,你改了我也得改。由此可以看出,封装是多么的重要。
下面来看下 Java 为了能够实现封装,提供了哪些语法和特性。
属性和方法的访问限定符public:所有人都可以访问。
protected:只有子类和同一个包下才可以访问。
package private (默认,即什么都不写): 只有同一个包下的才可以访问。
private:只有自己才可以访问。
基本上都是字面意思,有几个点比较重要:从上面可以看出,Java 的封装是以「包」展开的,即可以做到一个包之间的类可以互相访问,外部却无法访问。
同一个包指的是一个类的包语句,例如 package com.java.test 完全一致。包与包之间没有所谓的嵌套和父子关系,同一个包就是同一个包,虽然从包的文件目录上来看有层级关系,但是上层级的仍然无法访问到下层级的包。
Java Bean 约定
JavaBean 是 Java 业界流传非常广的一种约定写法:属性都是用 private 限定符。
getter/setter 的方法名具有规范:布尔属性使用 isXXX 和 setXXX
非布尔属性使用 getXXX 和 setXXX
这样做,就可以防止外界直接修改类中的属性,例如 person.age = 18 这种写法。
你可能会觉得:外界直接修改不是更方便吗? 就像文章开头举的例子,如果某天你要实现一个需求:「Person 类的 name 长度不得超过 10」,那就意味着你需要找到所有直接修改属性的地方,检查修改一遍,如果你提前使用了封装,你只需要关心 Person 类内部的 setName 即可。
值得一提的是:一些 JSON 转换库,比如 FastJSON。 它转换对象的时候,是看 getter/setter 方法,而不是看属性名,比如你有 3 个属性,只有 2 个属性有 getter/setter 方法,那么转换出来的对象也只会有 2 个属性。
设计模式-静态工厂方法
我们可以把 Person 类的构造器函数声明为 private 的,然后再对外提供静态工厂方法,这是一种设计模式,关于好处和坏处网上介绍得也比较多,这里只是随便举个用途的例子,比如我们这里就实现了,如果外界传入一个空的 name,我们就返回一个 InValidPerson。
设计模式-builder
一个类可能会有非常多的字段构成,这个时候,我们用传统的构造器生成的方式,就会发现可读性非常查,而且会搞错(IDEA 里面可能有属性名提示,但是在 code review 的时候,是完全没有的)。
所以就有了一种「builder 模式」,我们可以下载 Builder Generator 这个插件。然后生成一个 PersonBuilder 类。
是不是可读性一下子高了非常多呢。
类的访问限定符public:所有人都可以访问。
package private (默认,即什么都不写): 只有同一个包下的才可以访问。
想象一下你在写一个工具库,你可能有一些类,只是为了在工具库中使用,你不希望外界有人直接调用它们,那么你就可以把那些类不写访问限定符,把要供别人调用的类声明为 public 的。
突破类的访问限定符
有一种办法是可以突破 package private 的类的,即在非同一个包下,调用这个包中的非 public 的类。首先我们要创建出一个和这个包完全相同的包路径(这里只是随便挑选了一个第三方库),来创造一个「同一个包下」的环境。
然后我们就可以在这个包路径下的类中导入非 public 的类了。
返回值需要是 Object,而不是那个类的类型
这种做法仅限于学习,无论在什么情况下,都不应该这样使用。
JDK9 的模块化系统
JDK9 推出了「模块化系统」,那么为什么要有这个功能呢?
其实这个功能主要是针对补足类访问限定符的缺陷。
我们现在有:com.sdk.test
com.sdk.test.utils
这两个包,那么作为开发者来说,我们肯定是希望 Main 是 public 的,而 utils 里面的类是非 public 的。看上去它们好像是在一个包中,但是上文也说明了「包之间没有父子关系」,所以 Main 类中无法引用 com.sdk.test.utils 包中的非 public 的类。
模块系统就是为了解决这些问题,但是它太新了,以至于还没有大范围使用。