要区别设计良好的模块与设计不好的模块,最后重要的因素在于,这个模块对于外部的其他模块而言,是否隐藏其内部了数据和其他实现细节。设计良好的模块会隐藏所有的实现细节,把它的API与它的实现清晰地隔离开来。然后,模块之间只通过它们的API进行通信,一个模块不要知道其他模块的内部工作情况。这个概念被称为信息隐藏或封装,也是软件设计的基本原则之一。
封装有效地解除组成系统的各模块之间的耦合关系,使得这些模块可以独立地开发、测试与维护,还提高了软件的可重用性。
Java里的封装是通过访问控制符来加以限制的。
第一规则很简单:尽可能地使每个类或者成员不被外界访问,即使用尽可能最小的访问级别。
对于顶层类(非嵌套的)类和接口,只有两种可能的访问级别:包级私有的和公有的。如果使用pulbic修饰符声明了顶层类或者接口,那它就是公有的;否则,它将是包级私有的。如果顶层类或者接口能够被做成包级私有的,它就应该被做成包级私有,这样类或者是接口就成了这个包的实现的一部分,而不是该包导出的API的一部分,在以后的版本中,可以对它进行修改、替换、或者删除,而无需担心会影响到现有的客户端程序。
如果一个包级私有的顶层类(或者接口)只是在某一个类的内部用到,就应该考虑使它成为唯一使用它的那个类的私有嵌套类。这样可以将它的可访问范围从包中的所有类缩小到了使用它的那个类。然而,降低不必要公有类的可访问性,比降低包级私有的顶层类的更重要得多:因为公有类是包的API的一部分,而包级私有的顶层类只是这个包的实现的一部分,包级是我们可以掌控的一部分。
对于成员(域、方法、嵌套类和嵌套接口)有四种可能的访问级别,下面按照可访问性的递增顺序罗列出来:
1、私有的(PRivate):只有在声明该成员的顶层类内部才可以访问这个成员。
2、包级私有的:声明该成员的包内部的任何的任何类都可以访问这个成员。这也是“缺省”访问级别,如果没有为成员指定访问修饰符,就采用这个访问级别。
3、受保护的(protected):声明该成员的类的子类可以访问这个成员(但有一些限制[JLS,6.6.2]),并且声明该成员的包内部的任何类也可以访问这个成员。
4、公有的(public):在任何地方都可以访问该成员。
只有当同一个包内的另一个类真正需要访问一个成员的时候,你才应该删除private修饰符,使该成员变成包私有的。
私有成员和包级私有成员都是一个类的实现中的一部分,一般不会影响它的导出的API。然而,如果这个类实现了Serializable接口(见第74和75),这些域就有可能会被“泄漏”到导出的API中。
对于公有类的成员,当访问级别从包级私有变成保护级别时,会大大增强可访问性。受保护的成员是类的导出的API的一部分,必须永远得到支持。导出的类的受保护成员也代表了该类对于某个实现细节的公开承诺(见第17条)。受保护的成员应该尽量少用。
如果方法覆盖了超类中的一个方法,子类中的访问级别就不允许低于超类中的访问级别。这样就可以确保任何可使用超类的实例的地方也都可以使用子类的实例。
如果一个类实现了一个接口,那么接口中所有的类方法在这个类中也都必须被声明为公有的。之所以如此,是因为接口中的所有方法都隐含着公有访问级别。
为了测试而将一个公有类的私有成员变成包级别私有的,这还可以接受,但是如果要将访问级别提高到超过包访问级别,这就无法接受了。换包话说,不能为了测试,而将类、接口、或成员变成包的导出的API的一部分。幸运的是,也没有必要这么做,因为可以让测试作为被测试的包的一部分来运行,从而能够访问它的包级私有的元素。
实例域决不能是公有的(见第14条)。如果域是非final的,或者即是final但指向的却是可变对象,那么一旦使这个域成为公有的,就放弃了对存储在这个域中的值进行限制的能力;这意味着,你也放弃了强制这个域不可变的能力。因此,包含公有可变域的类并不是线程安全的。即使域是final的,并且引用不可变的对象,当把这个域变成公有的时候,也就放弃了“切换换到一种新的内部了数据表示法”(比如将这个字段删除掉,或者使用多个字段来表示等)的灵活性。
public final修饰的域要么包含基本类型的值,要么包含指向不可变对象的引用(见第15条)(同样的建议也适用于静态域)。如果final域包含可变对象的引用,它更具有非final域的所有缺点,虽然引用本身不能被修改,但是它所引用的对象却可以被修改。
长度非零的数组总是可变的,所以,类具有公有的静态final数组域,或者返回这种域的访问方法(这与方法返回的是局部数组是不一样的,因为这个是共享的,而返回的局部数组是单个线程共享的),这几乎总是错误的。如果类具有这样的域或者访问方法,客户端将能够修改数组中的内容,这是安全漏洞的一个常见根源:
public static final Thing[] VALUES={…};
修正这个问题有两种方法,可以使公有数组变成私有的,并增加一个公有的不可变列表:
private static final Thing[] PRIVATE_VALUES={…};
public static final LIST VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
另一种方法是,可以使用数组变成私有的,并添加一个公有方法,它返回私有的数组的一个备份:
private static final Thing[]PRIVATE_VALUES ={…};
public static final Thing[] values(){
return PRIVATE_VALUES.clone();
}
但依我个人看,上面的克隆还是不起根本作用(如果数组元素是基本类型的,就没有问题):虽然各个元素引用已被复制,但是它们所引用的对象却是同一份,如果指向的对象是可变的,则还是可以通过引用去修改它们所指向对象的问题,所以上面的修改也有潜在的问题。
从上面分析来看public static final最好修改的是基本类型的变量,或者是不可变的类。
总而言之,你应该始终尽量可能地降低可访问性。除了公有静态final域的特殊情形之外,公有类都不应该包含公有域。并且要确保公有静态final域所引用的对象都是不可变的。
14、 在公有类中使用访问方法而非公有域如果类可以在它所在的包的外部进行访问,就将域设为私有的并提供域的访问与设置公有方法,以保留将来改变该类的内部表示法的灵活性。如果公有类暴露了它的数据域,要想在将来改变其内部表示法是不可能的,因为公有类的客户端已经遍布各处了。另外,如果不采取这种方式,则当域被访问的时候,无法彩任何辅助的行动。
如果类是包级私有的(虽然将域暴露了,但也只仅限于包含该类的包中,还在你可控的范围之内),或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误——假设这些数据域确实描述了该类所提供的抽象。比如LinkedList中的Entry静态内部类,就暴露了所有的成员给外类了,这样用起来更简洁方便:
publicclassLinkedList{
//...
privatestaticclassEntry {
Object element;
Entry next;
Entry previous;
//...
}
}
让公有类直接暴露域虽然从来都不是种好办法,但是如果域是不可变的,这种做法危害就比较小一些(只是不能改变其指向,但指向的内容是否安全就不好说了)。如果不改变类的API,就无法改变这种类的表示法,当域被读取的时候,你也无法采取辅助的行动。
总之,公有类永远不应该暴露可变的域。虽然还是有问题,但是让公有类暴露不可变域其危害比较小。但是,有时候会需要使包级私有的或者私有的嵌套灰来暴露域,无论这个类是可变还是不可变的。
15、 使可变性最小化不可变类是其实例不能被修改的类(不只是类前加上了final就可以了)。每个实例中包含的所有信息都必须在创建该实例时候就提供,并在对象的整个生命周期内固定不变。
Java平台类库上有很多不可变的类,其中有String、基本类型的包装类、BigInteger和BigDecimal。
存在不可变内的许多理由:不可变类比可变类更加易于设计、实现和使用。它们不容易出错,且更加安全。
为使类成为不可变,要遵循以下5条规则:
1、不要提供任何会修改对象状态(属性)的方法。
2、保证类不会被扩展。一般做法是使这个类成为final的,另外作法就是让类所有构造器都变成私有的或者是包级私有的。
3、使用有的域都是final的(一般是针对非静态变量)。通过这种加上final修饰的强制方式,这可以清楚地表明你的意图:确保该域在使用前得到正确的初始化。而且,如果一个指向新创建的实例的引用在缺乏同步机制(一般不可变对象的访问是不需要同步的,因为状态一旦确定,就不会再更改)的情况下,从一个线程切换另一个线程就必需确保实例的正确状态(比如刚创建完这个实例,但还没来得及将工作内存中的数据存回到主内存中时就会有问题,但如果是加上了final修饰符后,则不会出现使用前final域还会初始化完成的情况,这样一定能保证构造器调用完后final域也会随之初始化完成。虽然以前很早的虚拟机上会出现构造器执行完成后final域未初始化完成的问题,但现已JMM已修复),正如果内存模型中所述那样[JLS 17.5]。
4、使用所有的域都成为私有的。这样可以防止客户端获得访问被域引用的可变对象的权限,并防止客户端直接修改这些对象。虽然从技术上讲,允许不可变的类具有公有的final域,只要这些域包含基本类型的值或都指向不可变对象的引用,但是不建议这样做,因为这样会使得在以后的版本中无法以再改变内部的表示法。
5、确保对于任何可变域的互斥访问。如果类具有指向可变对象域,则必须确保该类的客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象的引用(即进出都不行)。在构造器、访问方法、readObject方法(见76条)中请使用保护性拷贝技术。
下面是一个不可变复数(具有实部和虚部)类的例子:
//复数
publicfinalclassComplex {
privatefinaldoublere;//实部
privatefinaldoubleim;//虚部
//私有的,让它无法扩展
privateComplex(doublere,doubleim) {
this.re = re;
this.im = im;
}
//静态工厂方法
publicstaticComplex valueOf(doublere,doubleim) {
returnnewComplex(re, im);
}
publicstaticComplex valueOfPolar(doubler,doubletheta) {
returnnewComplex(r * Math.cos(theta), r * Math.
新闻热点
疑难解答