13.使类和成员的可访问性最小化
- 尽可能的使每个类或者成员不被外界访问
- 对于顶层类,接口,只有两种访问级别: 包级私有(
package-private
)和公有(public
) - 对于成员,有四种访问级别(
private
,package-private
,protect
,public
) - 如果一个类只对一个类可见,则应该将其定义为私有的内部类,而没必要
public
的类都应该定义为package private
- 子类的访问级别不允许低于父类的访问级别.
小结
- 应该始终尽可能的降低可访问性
- 除了公有静态
final
域外,公有类
都不应该包含非公有域.
14.在共有类中使用访问方法,而非共有域
示例
class Point {
public double x; //
public double y;
}
- 如果是公有类的时候,应该使用私有成员,并提供setter方法(除非不可变域)
- 包级私有,或内部类,直接暴露则没有本质的错误.
class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public void setX(double x) {
this.x = x;
}
public void setY(double y) {
this.y = y;
}
}
15.使可变性最小
建议
- 不提供可以改变本对象状态的方法
- 保证类不会被扩展(防止子类改变)
- 使所有的域都是
final
的 - 使所有的域都称为
private
的 - 确保对于任何可变组件的互斥访问
示例:
public Complex add(Complex c) {
return new Complex(re + c.re, im + c.im);
}
示例中除了标准的
Object
方法之外,运算操作都是创建新的实例,而不改变原来对象
,这种做法被称为 函数的(functional)做法 .
提示:
- 不可变对象比较简单
- 不可变对象本质上是线程安全的
- 不仅可以共享对象,甚至可以共享不可变对象的内部信息.[
BigInteger
内部的内部数组] - 不可变对象为其他对象提供了 大量的构件(
building blocks
)[map key
和set element
]
缺点
- 不可变对象对于每个不同的值 都需要创建一个单独的对象.
- 由于1的问题.导致多步操作会有性能问题.这种问题的解决办法就是
提供一个公有的可变配套类
,如String
和StringBuilder
.
保持不可变性
为了保持不被子类化,可以使用final
修饰.除此之外,还有一种方法:
- 让类的构造器变成
private/package-private
,添加静态工厂
来代替公有构造器
,见[第一条] - 使用静态工厂方法,具体实现类可以有多个,还能进行
object cache
[第一条]
小结
- 坚决不要为每个
get
方法编写一个 对应的set
方法,除非有很好的理由. - 如果类不可做成
不可变的
,也该尽量限制其可变性
(final 域
) - 当实现
Serializable
,一定要实现readObject/readResolve
方法,或者使用ObjectOutputStream.writeUnshared/ObjectInputStream.readUnshared
案例: TimeTask : 可变,但状态空间被有意的设计的很小.
16.复合优于继承
简介
专为
继承设计的
,包内部 继承
非常安全,是代码重用
的有效手段.
对于具体类
进行跨越包边界
的继承是危险
的.
继承缺点
- 打破
封装性
,子类依赖父类实现细节,父类改变,子类会遭到破坏
比如:HashSet
记录添加元素个数
的方法,addAll
内部调用了add
,导致程序错误.
- 在后续升级版本中,如果父类新增了与子类
相同签名,返回值不同
的方法,子类无法通过编译
,如果新增了签名和返回值都相同
的方法,则会发生覆盖
复合方式
- 新类中增加一个
引用现在类
的私有域
,通过转发
实现
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) {this.s = s;}
public boolean add(E e) {return s.add(e);}
}
缺点
- 不适用于callback frameworks,在回调框架中,需要将自身传递给调用对象来回调.
而被包装起来的对象并不知道 它被包装的 外面的对象, 因此导致回调失败.
继承和符合选择问题
- 继承应该在is-a的场景中使用
- 继承除了会继承父类的API功能,也会继承父类的设计缺陷,而组合则可以隐藏成员类的设计缺陷,通过转发暴露部分
API
17.要么为继承而设计,要么禁用继承
简介
- 一个类必须在文档中说明,每个可重写的方法,在该类的实现中的哪些地方会被调用。调用时机、顺序、结果产生的影响,包括多线程、初始化等情况。
- 被继承类应该通过谨慎选择
protected的方法或成员
,来提供一些hook
,以便能进入到它的内部工作流程中.如:java.util.AbstractList::removeRange
- 构造器不能调用可被覆盖的方法,因为
父类构造函数比子类构造函数先执行
,导致子类重写方法比子类构造器先执行
- 如果实现了
Serializable/Cloneable
,无论是clone
还是readObject
都不可以调用可复写方法
- 如果实现了
Serializable
,readResolve/writeReplace
必须是protected
,而非private
,否则子类会忽略.
总结
- 为继承而设计的类,对这个类会有一些实质性的限制.
- 不是为继承而设计的类,应禁止子类化.
18.接口优于抽象类
简介
Java 语言提供了两种机制: 接口和抽象类,
Java是单继承,多实现的,因此抽象类作为类型定义受到了很大的限制.
- 通过接口,现有类可以很容易的被更新
- 接口是
mixin
(混合类型)的理想选择(提供某些可供选择的行为) - 接口允许我们构建非层次结构的框架(利用接口多继承特性)
public interface Singer{}
public interface Writer{}
public interface SingerWriter extends Singer ,Writer{}
有效的避免了继承导致的
臃肿的类层次
(组合爆炸).
注意事项
- 对每个重要接口都提供一个
抽象的骨架实现类
,把接口和抽象类
的优点结合起来 - 通过把对接口的实现转发到
一个扩展了骨架实现的内部私有实例
上,称作模拟多重实现
,示例:Map.Entry
与AbstractMapEntry
- 骨架类是为了继承而设计的[参考第17条规范]
- 还有一种简单实现,
AbstractMap.SimpleEntry
(可能是基本实现
,可能是空实现
)
区别
- 抽象类演变比接口方便,可以在后续版本中新增新的方法,并提供默认实现,现有实现类都提供这个新的方法,接口则不行
小结
- 接口一旦发行,并广泛实现后,想要改变几乎不可能.
- 通过接口定义类型,可以允许多实现(多继承)
- 但是演进需求大于灵活性、功能性时,抽象类更合适
- 提供接口时,提供一个骨架实现,同时审慎考虑接口设计
19.接口只用于定义类型
简介
接口充当了可以引用这个类的实例类型
Type
.
不良使用
常量接口: 如:
public interface Constants{
static final int AAA = 0;
}
这种接口,会使用户糊涂,并使接口污染,如果这个类被修改了,
意味着,在后续版本中,不再需要这些常量了,它依然必须实现这个接口.
合理方案
- 只在当前类和接口中导出这些常量.
如:Integer.MAX_VALUE
- 不可实例化的工具类中导出常量,配合静态导入(
static import
) - 接口应该 只被用来定义类型,而不是定义常量.
20.类层次优于标签类
概述
标签类,就是在内部定义一个tag
变量,由其控制功能的转换,如下类型
class Figure {
enum Shape {RECTANGLE, CIRCLE};
// Tag field - the shape of this figure
final Shape shape;
}
标签类过于冗长,容易出错
解决方案
通过子类化
解决.为每种标签都定义具体的子类.
示例代码: Figure
abstract class Figure {}
class Rectangle extends Figure {}
class Circle extends Figure {}
好处:
- 代码简单清除,没有样板代码
- 所有域都是
final
的,在初始化构造器时就初始化数据域 - 多个程序员可以独立的扩展类层次结构
建议
当编写包含显示标签域的代码时,应考虑标签是否可以取消.
21.用函数对象表示策略
简介
允许程序把
调用特殊函数的能力
存储起来并传递这种能力.这种机制通常允许函数调用者传入第二个参数
来指定自己的行为.定义一种对象,它的方法
执行其他对象
上的操作,这样的类被称为函数对象
.如Compare
接口的实现类.这种
对象实例
,可以称为其他对象
的具体策略
这种策略类,没有
状态
,没有域
,所有实例都是等价的,因此最好用单例
来实现,如:
//第一种
Arrays.sort(new short[]{}, new Comparator<String>() {
@Override public int compare(String o1, String o2) {
return 0;
}
});
//第二种
class StringLengthComparator implements Comparator<String> {
private StringLengthComparator() {
}
public static final StringLengthComparator INSTANCE = new StringLengthComparator();
@Override public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
}
总结
- 函数指针的主要途径是实现策略模式
- 需要声明一个接口来表示该策略(如
Comparator<T>
,一般是泛型接口) - 当一个具体的策略只使用一次时,使用
匿名内部类
- 当一个具体策略被
重复使用
时,使用单例
来实现
22.优先考虑静态成员类
简介
有四种嵌套类: 静态成员类,非静态成员类,匿名类与局部类.除了第一种,其他三种都被称为内部类.
静态成员
- 静态成员类是最简单的一种嵌套类,可以看成是一种恰好被声明在另一个类的内部的普通类. 和
其他静态成员
规则一致. - 静态成员类的一种常见用法是作为公有的辅助类.仅当与外部类一起使用时才有意义.
- 可以在外围实例之外独立存在,用于表示(封装)外部类的一些成员.
非静态成员
- 每个实例都隐含着外围类的一个实例关联.(内存泄漏一般也是由此引起),持有强引用
- 不可以在外围实例之外独立存在
- 如果内部类不需要引用外部类的成员和方法,则一定要将其定义为
static
,避免空间/时间开销,避免内存泄漏
匿名类
- 没有名字,不是外围类的成员,在使用的时候被声明和实例化
- 有诸多限制,不能
instanceof
,无法扩展一个类或者接口. - 通常像
Runnable
,Thread
在内部类中使用居多
局部类
- 局部类是使用最少的类,在任何可以
声明局部变量地方都可以声明局部变量.
- 是否static取决于其定义的上下文
- 可以在作用域内重复使用
- 不能有static成员
总结
如果成员类每个实例都需要一个指向其外围实例的引用,用非静态
否则,用静态