第12条:使类和成员的可访问能力最小化
要想区别一个良好的模块与一个设计不好的模块,最重要的因素是,这个模块对于外部的其他模块而言,是否隐藏了内部的数据和其他的实现细节。一个设计良好的模块会隐藏所有的实现细节,把它的api与实现清晰的隔离开来,然后模块之间只通过他们的api进行通信,一个模块不需要知道其他模块的内部工作情况。(被称为信息隐藏,或者封装)。
信息隐藏之所以非要重要,理由源于:他可以有效的解除一个系统中各个模块之间的耦合关系,使得这些模块可以被独立的开发,测试,优化,使用,理解,修改。这样可以加速系统开发速度,因为这些模块可以被并行的开发。它也可以减轻维护的负担,因为程序员可以很快理解这些模块并且调试的时候不伤害其他模块。
经验表明,你应该尽可能得使每一个类或成员不被外界访问。
对于顶层的(非嵌套的)类和接口,只有两种可能得访问级别:包级私有的和公有的。如果一个类或者接口能够被做成包级私有的,那么它就应该被做成包级私有的。通过把一个类或接口做成包级私有的,它实际上成了这个包的实现的一部分,而不是该包导出的api的一部分,并且在以后的发行版本你可以对它进行修改,替换或者去除,而无需担心会伤害到现有的客户。如果你把它做成公有的,你就有义务永远支持它,以保持前容性。
如果一个包级私有的顶层类或接口只是在某一个类的内部被调用到,那么你应该考虑它成为后者的一个私有嵌套类。这样可以进一步降低它的可访问性。
当你仔细设计了一个类的公有api之后,接下去应该把所有其他的成员都变私有的。
子类中的方法的访问级别低于超类中的访问级别是不被允许的,这样可以确保子类的实例可以被用在任何可使用超类的实例的场合。如果违反了这条规则,编译子类的时候会报错。
接口中的所有方法都隐含着公有访问级别。
公有类应该尽可能得少的包含公有的域。包含公有可变的域的类不是线程安全的。
通过公有的静态final域来暴露类的常量是允许的。这些域要么包含原语类型的值,要么包含指向非可变对象的引用,如果一个final域包含一个指向可变对象的引用,那么它具备非final域的所有缺点,虽然引用本身不能被修改,但是它引用的对象可以被修改,这对导致灾难性的后果。
非零长度的数组总是可变的,所以具有公有的静态final数组域几乎总是错误的。
除了公有静态final域的特殊情况,公有类不应该包含公有域,并且确保公有静态final域引用的对象是不可变的对象。
第13条:支持非可变性
一个非可变类是一个简单的类,它的实例不能被修改。每个实例中包含的所有信息都必须在该实例被创建出来的时候就提供出来,并且在对象的整个生存期内固定不变。
非可变类存在有许多理由:非可变类比可变类更加易于设计,实现和使用。它不容易出错,更加安全。
为了使一个类称为非可变类,遵循下面5条规则:
1.不要提供任何会修改对象的方法
2.保证没有可被子类改写的方法。可以防止子类破坏该类的不可变行为。一般做法是使这个类称为final的。
3.使所有的域都是final的
4.使所有的域都称为私有的。这样可以防止客户直接修改域中的信息。
5.保证对于任何可变组件的互斥访问。如果你的类具有指向可变对象的域,则必须确保该类的客户无法获得指向这些对象的引用。
非可变对象比较简单,一个非可变对象可以只有一个状态,即最初被创建时刻的状态。
非可变对象本质上是线程安全的,他们不要求同步。
非可变对象可以被自由的共享。
你不仅可以共享非可变对象,甚至也可以共享它们的内部信息。
非可变对象真正唯一的缺点是,对于每一个不同的值都要求一个单独的对象。
总而言之,坚决不要为每个get方法编写一个相应的set方法。除非有很好的理由要让一个类称为可变类,否者就应该是非可变的。
第14条:复合优先于继承
在一个包的内部使用继承是非常安全的,在那儿子类和超类的实现在同一个程序的控制之下,对于专门为了继承而设计,并且具有很好的文档说明的类,使用继承也是非常安全的,然而,对普通的具体类进行跨越包边界的继承,则是非常危险的。
与方法调用不同的是,继承打破了封装性。一个子类依赖于其超类中特定功能的实现细节。超类的实现可能会随着发行版本的不同而有变化,如果真的发生变化,则子类可能被打破,即使它的代码完全没有改变。因而,一个子类必须要跟着其超类的更新而发展,除非超类是专门为了扩展而设计的,并且具有很好的文档说明。
只有当子类真正是超类的子类型的时候,继承才是合适的。换句话说,对于两个类a和b,只有当两者之间确实存在is-a关系的时候,类b才应该扩展类a。
第15条:要么专门为继承而设计,并给出文档说明,要么机制继承。
首先,该类的文档必须精确的描述了改写每一个方法所带来的影响。
一个类必须通过某种形式提供适当的钩子,以便能够进入到它内部工作流程中,这样的形式可以是精心选择的受保护方法,也可以是保护域。
构造函数一定不能调用可被改写的方法,无论是直接的还是间接的。如果违反了这条规定,很可能导致程序失败。超类的构造在子类构造函数之前运行,所以,子类中改写版本的方法将会在子类的构造函数运行之前就先被调用。如果该改写版本的方法依赖于子类构造函数所执行的初始化工作,那么该方法将不会如预期般的执行。
在继承体系中类实现cloneable或者serializable接口,无论是clone还是readObject,都不能调用一个可改写的方法,不管是直接的方式,还是间接的方式。
两种办法可以禁止子类化,比较容易的办法是把这个类声明为final的,另一种办法是把所有的构造函数变成私有的,或者包级私有的,并且增加一些公有的静态方法。
一个合理的办法是确保这个类永远不会调用它的可改写的方法,并且在这文档中说明这一点。
第16条:接口优于抽象类
已有的类可以很容易被更新,以实现新的接口。你所需要做的是增加要求的方法,然后在类的声明上增加一个implements子句。一般的,要更新一个已有的类,使他扩展一个新的抽象类,这往往不可能得。如果你希望让两个类扩展同一个抽象类,那么你必须把抽象类放到类的层次的上部,以便这两个类的一个祖先称为它的子类,不幸的是这样做会间接的伤害到类层次,强迫这个公共的祖先的所有后代类都扩展这个新的抽象类,而不管它对于这个后代类是否合适。
接口是定义混合类型的理想选择。实现接口表明它提供了某些可供选择的行为。抽象类不适合被用于混合类型,理由是一个类不可能有一个以上的父类。
接口使得我们可以构造出非层次结构的类型框架。(可以同时实现多个接口)。
接口使得安全的增强一个类的功能成为可能。(包装模式)。
你可以把接口和抽象类的优点结合起来,对于你期望导出的每一个重要接口,都提供一个抽象的骨架实现。接口的作用仍然定义类型,但是骨架的实现类负责所有的与接口的实现相关工作。Collections Framework提供了一个骨架实现,包括AbstractColletion,AbstractSet等。
如果设计合理的话,利用骨架实现,程序员可以很容易的提供他们的自己的接口实现。
骨架实现的优美在于,他们为抽象类提供了实现上的帮助,但又没有强加“抽象类被用作类型定义时候”所特有的严格限制。如果一个预先已经定义好的类无法扩展骨架实现类,那么,这个类总可以手工实现这个接口。尽管如此,骨架实现类仍然能够有助于接口的实现。实现了这个接口的类可以把对于接口方法的调用,转发到一个内部私有类的实例上,而这个内部私有类扩展了骨架实现类。这项技术被称为模拟多重继承。
骨架实现类不是为了继承的目的而设计的。
抽象类比接口的一个明显优势:抽象类的演化比接口的演化要容易的多。如果在后续版本中,你希望在抽象类中增加一个新方法,那么你总可以增加一个具体方法,它包含了一个合理的默认实现。然后,该抽象类的所有已有实现类都自动提供这个新方法。
要在一个公有的接口中增加一个方法,而不打破现有的,已经在使用的接口的所有程序,这是不可能得。在此之前实现了接口的类将会漏掉新增加的方法,所以根本就不能编译通过。
因此,设计公有接口非常谨慎。一旦一个接口被公开发行,并且已经广泛实现,在想改变这个接口是不可能得,你必须在第一次设计的时候保证接口的正确。
接口通常是定义具有多个实现的类型的最佳途径,但是当演化的容易性比灵活和功能更为重要的时候,应该使用抽象类。
第17条:接口只是被用于定义类型。
实现常量接口模式是对接口的不良使用。一个类要在内部使用某些常量,这纯粹是实现细节。实现常量接口,会导致把这样的实现细节泄露到该类的导出api。如果将来发行版本中不需要常量了,为了兼容性那么它仍必须实现这个接口。
导出常量的几种选择方案:
1.把这些常量添加到这个类中
2.使用枚举类
3.使用一个不可实例化的工具类
第18条:优先考虑静态成员类。
嵌套类存在的目的应该只是为他的外围类提供服务。如果一个嵌套类可能会用于其它的某个环境中,那么它应该是顶层类。
嵌套类的四种:静态成员类,非静态成员类,匿名类,局部类。
静态类可以访问外围类的所有成员,包括私有的。于静态成员一样遵守可访问性规划。
非静态成员类的实例都隐含着与外围类的一个外围实例。在没有外围实例的情况要想创建非静态成员类的实例是不可能得。
如果你声明的成员类不要求访问外围实例,那么请记住把static修饰符放到成员类的声明中使他成为一个静态类,而不是一个非静态类。非静态类实例维护指向外围对象的引用需要耗时间和空间。如果没有外围实例的情况下要分配实例化则不能使用非静态成员类。
私有静态成员类的一种通常用法是用来代表外围类的组件。(Map和Entry关系)。
如果匿名类出现在一个非静态环境中,则它有一个外围实例。
匿名类的一个通常用法是创建一个函数对象,比如Comparator实例.
Arrays.sort(args,new Comparator(){
public int compare(Object o1,Object o2){
}
匿名类的另一个用法创建过程对象,如Runnable或者TimerTask实例。
匿名类的第三个用法是在一个静态工厂方法的内部。
匿名类的第三个用法是复杂的类型安全枚举类型。
局部类在任何可以声明局部变量的地方都可以声明局部类,并且局部类也遵守同样的作用域规则。
})
要想区别一个良好的模块与一个设计不好的模块,最重要的因素是,这个模块对于外部的其他模块而言,是否隐藏了内部的数据和其他的实现细节。一个设计良好的模块会隐藏所有的实现细节,把它的api与实现清晰的隔离开来,然后模块之间只通过他们的api进行通信,一个模块不需要知道其他模块的内部工作情况。(被称为信息隐藏,或者封装)。
信息隐藏之所以非要重要,理由源于:他可以有效的解除一个系统中各个模块之间的耦合关系,使得这些模块可以被独立的开发,测试,优化,使用,理解,修改。这样可以加速系统开发速度,因为这些模块可以被并行的开发。它也可以减轻维护的负担,因为程序员可以很快理解这些模块并且调试的时候不伤害其他模块。
经验表明,你应该尽可能得使每一个类或成员不被外界访问。
对于顶层的(非嵌套的)类和接口,只有两种可能得访问级别:包级私有的和公有的。如果一个类或者接口能够被做成包级私有的,那么它就应该被做成包级私有的。通过把一个类或接口做成包级私有的,它实际上成了这个包的实现的一部分,而不是该包导出的api的一部分,并且在以后的发行版本你可以对它进行修改,替换或者去除,而无需担心会伤害到现有的客户。如果你把它做成公有的,你就有义务永远支持它,以保持前容性。
如果一个包级私有的顶层类或接口只是在某一个类的内部被调用到,那么你应该考虑它成为后者的一个私有嵌套类。这样可以进一步降低它的可访问性。
当你仔细设计了一个类的公有api之后,接下去应该把所有其他的成员都变私有的。
子类中的方法的访问级别低于超类中的访问级别是不被允许的,这样可以确保子类的实例可以被用在任何可使用超类的实例的场合。如果违反了这条规则,编译子类的时候会报错。
接口中的所有方法都隐含着公有访问级别。
公有类应该尽可能得少的包含公有的域。包含公有可变的域的类不是线程安全的。
通过公有的静态final域来暴露类的常量是允许的。这些域要么包含原语类型的值,要么包含指向非可变对象的引用,如果一个final域包含一个指向可变对象的引用,那么它具备非final域的所有缺点,虽然引用本身不能被修改,但是它引用的对象可以被修改,这对导致灾难性的后果。
非零长度的数组总是可变的,所以具有公有的静态final数组域几乎总是错误的。
除了公有静态final域的特殊情况,公有类不应该包含公有域,并且确保公有静态final域引用的对象是不可变的对象。
第13条:支持非可变性
一个非可变类是一个简单的类,它的实例不能被修改。每个实例中包含的所有信息都必须在该实例被创建出来的时候就提供出来,并且在对象的整个生存期内固定不变。
非可变类存在有许多理由:非可变类比可变类更加易于设计,实现和使用。它不容易出错,更加安全。
为了使一个类称为非可变类,遵循下面5条规则:
1.不要提供任何会修改对象的方法
2.保证没有可被子类改写的方法。可以防止子类破坏该类的不可变行为。一般做法是使这个类称为final的。
3.使所有的域都是final的
4.使所有的域都称为私有的。这样可以防止客户直接修改域中的信息。
5.保证对于任何可变组件的互斥访问。如果你的类具有指向可变对象的域,则必须确保该类的客户无法获得指向这些对象的引用。
非可变对象比较简单,一个非可变对象可以只有一个状态,即最初被创建时刻的状态。
非可变对象本质上是线程安全的,他们不要求同步。
非可变对象可以被自由的共享。
你不仅可以共享非可变对象,甚至也可以共享它们的内部信息。
非可变对象真正唯一的缺点是,对于每一个不同的值都要求一个单独的对象。
总而言之,坚决不要为每个get方法编写一个相应的set方法。除非有很好的理由要让一个类称为可变类,否者就应该是非可变的。
第14条:复合优先于继承
在一个包的内部使用继承是非常安全的,在那儿子类和超类的实现在同一个程序的控制之下,对于专门为了继承而设计,并且具有很好的文档说明的类,使用继承也是非常安全的,然而,对普通的具体类进行跨越包边界的继承,则是非常危险的。
与方法调用不同的是,继承打破了封装性。一个子类依赖于其超类中特定功能的实现细节。超类的实现可能会随着发行版本的不同而有变化,如果真的发生变化,则子类可能被打破,即使它的代码完全没有改变。因而,一个子类必须要跟着其超类的更新而发展,除非超类是专门为了扩展而设计的,并且具有很好的文档说明。
只有当子类真正是超类的子类型的时候,继承才是合适的。换句话说,对于两个类a和b,只有当两者之间确实存在is-a关系的时候,类b才应该扩展类a。
第15条:要么专门为继承而设计,并给出文档说明,要么机制继承。
首先,该类的文档必须精确的描述了改写每一个方法所带来的影响。
一个类必须通过某种形式提供适当的钩子,以便能够进入到它内部工作流程中,这样的形式可以是精心选择的受保护方法,也可以是保护域。
构造函数一定不能调用可被改写的方法,无论是直接的还是间接的。如果违反了这条规定,很可能导致程序失败。超类的构造在子类构造函数之前运行,所以,子类中改写版本的方法将会在子类的构造函数运行之前就先被调用。如果该改写版本的方法依赖于子类构造函数所执行的初始化工作,那么该方法将不会如预期般的执行。
在继承体系中类实现cloneable或者serializable接口,无论是clone还是readObject,都不能调用一个可改写的方法,不管是直接的方式,还是间接的方式。
两种办法可以禁止子类化,比较容易的办法是把这个类声明为final的,另一种办法是把所有的构造函数变成私有的,或者包级私有的,并且增加一些公有的静态方法。
一个合理的办法是确保这个类永远不会调用它的可改写的方法,并且在这文档中说明这一点。
第16条:接口优于抽象类
已有的类可以很容易被更新,以实现新的接口。你所需要做的是增加要求的方法,然后在类的声明上增加一个implements子句。一般的,要更新一个已有的类,使他扩展一个新的抽象类,这往往不可能得。如果你希望让两个类扩展同一个抽象类,那么你必须把抽象类放到类的层次的上部,以便这两个类的一个祖先称为它的子类,不幸的是这样做会间接的伤害到类层次,强迫这个公共的祖先的所有后代类都扩展这个新的抽象类,而不管它对于这个后代类是否合适。
接口是定义混合类型的理想选择。实现接口表明它提供了某些可供选择的行为。抽象类不适合被用于混合类型,理由是一个类不可能有一个以上的父类。
接口使得我们可以构造出非层次结构的类型框架。(可以同时实现多个接口)。
接口使得安全的增强一个类的功能成为可能。(包装模式)。
你可以把接口和抽象类的优点结合起来,对于你期望导出的每一个重要接口,都提供一个抽象的骨架实现。接口的作用仍然定义类型,但是骨架的实现类负责所有的与接口的实现相关工作。Collections Framework提供了一个骨架实现,包括AbstractColletion,AbstractSet等。
如果设计合理的话,利用骨架实现,程序员可以很容易的提供他们的自己的接口实现。
骨架实现的优美在于,他们为抽象类提供了实现上的帮助,但又没有强加“抽象类被用作类型定义时候”所特有的严格限制。如果一个预先已经定义好的类无法扩展骨架实现类,那么,这个类总可以手工实现这个接口。尽管如此,骨架实现类仍然能够有助于接口的实现。实现了这个接口的类可以把对于接口方法的调用,转发到一个内部私有类的实例上,而这个内部私有类扩展了骨架实现类。这项技术被称为模拟多重继承。
骨架实现类不是为了继承的目的而设计的。
抽象类比接口的一个明显优势:抽象类的演化比接口的演化要容易的多。如果在后续版本中,你希望在抽象类中增加一个新方法,那么你总可以增加一个具体方法,它包含了一个合理的默认实现。然后,该抽象类的所有已有实现类都自动提供这个新方法。
要在一个公有的接口中增加一个方法,而不打破现有的,已经在使用的接口的所有程序,这是不可能得。在此之前实现了接口的类将会漏掉新增加的方法,所以根本就不能编译通过。
因此,设计公有接口非常谨慎。一旦一个接口被公开发行,并且已经广泛实现,在想改变这个接口是不可能得,你必须在第一次设计的时候保证接口的正确。
接口通常是定义具有多个实现的类型的最佳途径,但是当演化的容易性比灵活和功能更为重要的时候,应该使用抽象类。
第17条:接口只是被用于定义类型。
实现常量接口模式是对接口的不良使用。一个类要在内部使用某些常量,这纯粹是实现细节。实现常量接口,会导致把这样的实现细节泄露到该类的导出api。如果将来发行版本中不需要常量了,为了兼容性那么它仍必须实现这个接口。
导出常量的几种选择方案:
1.把这些常量添加到这个类中
2.使用枚举类
3.使用一个不可实例化的工具类
第18条:优先考虑静态成员类。
嵌套类存在的目的应该只是为他的外围类提供服务。如果一个嵌套类可能会用于其它的某个环境中,那么它应该是顶层类。
嵌套类的四种:静态成员类,非静态成员类,匿名类,局部类。
静态类可以访问外围类的所有成员,包括私有的。于静态成员一样遵守可访问性规划。
非静态成员类的实例都隐含着与外围类的一个外围实例。在没有外围实例的情况要想创建非静态成员类的实例是不可能得。
如果你声明的成员类不要求访问外围实例,那么请记住把static修饰符放到成员类的声明中使他成为一个静态类,而不是一个非静态类。非静态类实例维护指向外围对象的引用需要耗时间和空间。如果没有外围实例的情况下要分配实例化则不能使用非静态成员类。
私有静态成员类的一种通常用法是用来代表外围类的组件。(Map和Entry关系)。
如果匿名类出现在一个非静态环境中,则它有一个外围实例。
匿名类的一个通常用法是创建一个函数对象,比如Comparator实例.
Arrays.sort(args,new Comparator(){
public int compare(Object o1,Object o2){
}
匿名类的另一个用法创建过程对象,如Runnable或者TimerTask实例。
匿名类的第三个用法是在一个静态工厂方法的内部。
匿名类的第三个用法是复杂的类型安全枚举类型。
局部类在任何可以声明局部变量的地方都可以声明局部类,并且局部类也遵守同样的作用域规则。
})