本章阐述一些指导原则,可以帮助你更好的利用这些元素,设计出更加有用、健壮和灵活的类和接口。
13. 使类和成员的可访问性最小化
信息隐藏(信息封装):的概念:
设计良好的模块会隐藏所有的实现细节,把它的API与它的实现清晰的隔离开来。然后模块之间只通过他们的API进行通信,一个模块不需要知道其他模型的内部工作情况。
这是软件设计的基本原则之一。信息隐藏可以有效的解除组成系统的各模块之间的耦合关系,使这块模块可以独立的开发、测试、优化、使用、理解和修改。
优势:- 可以并行开发,加快开发速度;
- 可以调节性能,提高每个模块的性能;
- 提高了可重用性;
- 降低了构建大型系统的风险;
访问控制机制(access control)决定了类、接口和成员的可访问性(accessibility),应当尽可能的使每个类或者成员不被外界访问。
公有的API将会在之后的发型版本中被维护,不能被修改、替换、删除等。所以降低不必要的公有类访问很重要。
如果一个包级私有的顶层类(接口)只在某一个类中用到,就应该把它做成那个类的内部类,这样它的可以被范围会更小(从包级缩小为类内部)。
成员(成员变量、方法、类、接口)的四种可访问类别:
private
:类内部才能使用default(package-private)
:包级私有的。声明该成员的包内部的任何类都可以访问这个成员。protected
:声明该成员的类的子类可以访问该成员、并且该成员的包内部的任何类也可以访问。public
:在任何地方都可以访问该成员。
子类中方法的可访问级别不许低于父类的访问级别,这样可以确保任何可以使用父类的地方也可以使用子类的实例。
实例域决不能是公有的。
如果域是非final的、或者是一个指向可变对象的final引用,一旦成为公有的,就放弃了对该域进行限制的能力、也放弃了强制这个域不可变的能力。
包含公有可变域的类并不是线程安全的。类具有公有静态final数组域,或者返回这种域的访问方法,这几乎总是错误的,是一个常见的安全漏洞来源。
应该尽可能降低可访问性,避免把任何散乱的类、接口和成员变成API的一部分。
除了公有静态final域以外,公有类都不应该包含公有域,并要确保公有静态域所引用的对象都不是可变的。
14. 在公有类中使用访问方法而非公有域
如果类是包级私有的,或者是私有的内部类,直接暴露它的数据域并没有本质的错误。这种访问方法更清晰、视觉不混乱。
公有类永远都不应该暴露可变的域。但是让公有类暴露不可变的域其危害较小。
15. 使可变性最小化
不可变类是其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。
使类成为不可变,遵循下面五条规则:
- 不要提供任何会修改对象状态的方法,如setter方法
- 保证类不会被扩展,即不能被子类化
- 使所有的域都是final的。
- 保证所有的域都是private的。
- 确保对于任何可变组件的互斥访问。 对于是引用对象的域,不要暴露给用户。
使用函数方式的编程方法达成不可变性。进行运算创建并返回新的对象,而不是修改这个实例。
不可变对象本质上是线程安全的,它们不要求同步。不可变对象可以被自由的共享。最好的方法,对于常用的值,提供公有的静态final常量。更进一步,可以提供静态工厂,把频繁用到的值缓存起来。
不应该为不可变类提供clone方法或拷贝构造器。
不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。
使类不能被子类化的两种方法:
- 使用final修饰类
- 构造器私有化,提供公有的静态工厂方法
坚决不要为每个get方法编写一个相应的set方法,除非有很好的理由让类成为可变的类,否则就应该是不可变的。
如果类不能被做成是不可变的,仍然应该尽可能的限制它的可变性。除非有令人信服的理由要使域变成非final的,否则要使每个域都是final的。
16. 复合优先于继承
继承(inheritance)是实现代码重用的有力手段,但他并非永远是完成这项工作的最佳工具。
在包的内部使用继承是非常安全的,因为这时子类和实例都处于同一个程序员的控制下。对于专门为了继承而设计、并且有很好的文档说明的类,也是安全的。
对于普通的具体类进行跨包的继承,则是危险的。继承打破了封装性。
子类依赖于超类中特定功能的实现细节,超类可能会随着发行版本的不同而有所变化,子类就会有可能出错。除非超类是为了继承而设计的。使用复合(composition),装饰器模式,来提供额外的功能。
只有当子类真正是超类的子类型时,才适合用继承。即
is - a
的关系。如果不是,A本质上不是B的一部分,只是它的实现细节而已。如果在适合于使用复合的地方使用了继承,则会不必要的暴露实现细节。因为客户端可以直接访问父类的API,会造成可能的问题。
在决定使用继承而不是复合之前,应该确定父类的API是否有缺陷,如果有,考虑使用复合来隐藏这些缺陷。
17. 要么为继承而设计,并提供文档说明,要么就禁止继承
努力思考,决定应该暴露哪些受保护的方法和域。
构造器决不能调用可被覆盖的方法。
那些并非为了安全的进行子类化而设计和编写文档的类,就禁止子类化。
18. 接口优于抽象类
Java提供了两种机制用来定义允许多个实现的类型:接口和抽象类。
抽象类允许包含某些方法的实现,而接口则不允许。
类必须成为抽象类的一个子类才能实现由抽象类定义的类型,而接口可以在任意类层次进行实现,又因为Java只允许单继承,所有抽象类受到了极大的限制。
现有的类可以很容易被更新,以实现新的接口。
使用抽象类来扩展类,就可能会推高类的层次结构;而接口可以很简单的扩展该类。接口是定义混合类型的理想选择。
类除了实现它的基本类型之外,还可以实现这个mixni类型,以表明它提供了某些可供选择的行为。
这样的mixin接口允许任选的功能可以被混合到类型的主要功能中;抽象类不能被定义为mixin类型,同样也是因为他们不能被更新到现有的类中:类不可能有一个以上的父类,类层次结构中也没有适当的地方来插入mixin。接口允许我们构造非层次结构的类型框架
使用类的话会造成组合爆炸(combinatorial explosion)
。
在包装类中,接口使得安全的增强类的功能成为可能。骨架实现类
对每个重要接口都提供一个抽象的骨架实现类,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但骨架实现类接管了所有与接口实现的工作。被称为AbstractInterface
,Interface是指接口的名字。
骨架类可以使程序员很容易提供他们自己的接口实现。
举例:AbstractCollection
、AbstractSet
、AbstractList
、AbstractMap
。- 骨架实现的美妙之处在于作为抽象类提供了实现上的帮助,但摒弃了抽象类的严格限制:
- 对于接口的实现来讲,扩展骨架实现是个很显然的选择,但并不是必须的,类也可以直接实现接口。
- 实现了这个接口的类可以把对于接口方法的调用转发到一个内部私有类上,这个内部私有类扩展了骨架实现类。这种方法被称为
模拟多重继承(simulated multiple inheritance)
,包装类技术,具有多重继承的有点,也避免了相应的缺陷。
- 骨架实现的美妙之处在于作为抽象类提供了实现上的帮助,但摒弃了抽象类的严格限制:
抽象类的优势:抽象类的演变比接口的演变要容易的多。
在后续发行版本中,在抽象类中可以随意添加具体方法,而接口不行。所有在设计之初就要保证接口是正确的。
接口通常是定义允许多个实现类型的最佳途径;
当演变的容易性大于灵活性时,可以考虑使用抽象类来定义类型;
当导出一个重要接口时,就应该考虑使用骨架实现类;
谨慎的设计公有接口,编写多个实现进行测试,保证接口的正确性。
19. 接口只用于定义类型
当类实现接口时,接口就可以充当引用这个类实例的类型(type)。为了其他任何目的而定义接口是不恰当的。
常量接口(constant interface),这种模式是对接口的不良使用。
实现常量接口,会把这样的实现细节泄露到该类的导出的API中。对用户来讲没什么价值;在将来的发行版本中,依然必须实现这个接口,确保兼容性。使用不可实例化的工具类来封装这些常量,并使用静态导入来引用这些常量。
20. 类层次优于标签类
标签类:带有两种甚至更多种风格的实例的类,并包含表示实例风格的标签域。即一个类表示了多种类型,这种情况下应该使用抽象类或接口抽取出共同性,使用类层次来清晰化代码。
21. 用函数对象表示策略
Java没有提供函数指针,但是可以用对象引用实现同样的功能。
函数对象:它的方法执行了其他对象(这些对象被显示的传递给这些方法)上的操作。
例子参考Comparator
。
在设计具体的策略类时,还需要定义一个策略接口,并且可以使用泛型来加大范围。
具体的策略类往往使用匿名类声明。但如果它被经常调用,就可以做成
static final
的,减少类的创建和回收,还可以为它定义一个有意义的名称。具体的策略类还可以是宿主类的内部类。
22. 优先考虑静态成员类
嵌套类(nested class)是指被定义在另一个类的内部的类嵌套类存在的目的应该只是为它的外围类提供服务。
嵌套类有四种:
- 静态成员类
- 非静态成员类
- 匿名类
- 局部类
除了第一种以外,其他三种都被成为内部类。
静态成员类
就是一个普通类,被声明在了类内部。
它可以访问外围类的所有成员,包括私有成员;
如果静态成员类被声明为私有的,则只能被外围类方位,否则遵守访问限制的规则。
静态成员类的常见用法是作为公有的辅助类,仅当与它的外围类一起使用时才有意义。非静态成员类
非静态成员类的每个实例都隐含着外围类的一个实例;可以调用外围类的方法、成员。
当非静态成员类的实例被创建时,它和外围实例之间的关联关系也随之被建立起来,这种冠以以后不能被修改;外围类的某个方法调用了非静态成员类的构造方法时,这种关系也被建立起来。- 如果成员类不要求访问外围实例,那么就把它做成静态成员类。
因为非静态成员类都要持有一个外围类的引用,会多消耗时间和空间,并容易导致内存泄露。
- 如果成员类不要求访问外围实例,那么就把它做成静态成员类。
匿名类
在使用时被声明和实例化。局部类
在任何可以声明局部变量的地方,都可以声明局部类。
如果一个嵌套类需要在单个方法之外仍是可见的,或者它太长了,不适合于放在方法内部,就应该使用成员类。
如果成员类的每个实例都需要一个指向外围类实例的引用,就要把成员类做成非静态的,否则,做成静态的。
如果是在方法内部的嵌套类,若只需要在一个地方创建实例、并且已经有一个预置的类型说明这个类,就做成匿名类;否则,做成局部类。