第四章

2021-12-30  本文已影响0人  gcno93

讨论类和接口,以及如何设计出更加有用、健壮和灵活的接口

第十五条 使类和成员的可访问性最小 (优先使用私有)
1.尽可能地使每个类或者成员不被外界访问
2.对于顶层(非嵌套)类和接口,只有两种可能的访问级别,包级私有和公有的,包级私有的不是
导出api的一部分,它可以在以后的发行的版本中进行修改,替换,删除,而不会影响客户端程序
3.访问级别
1.私有的,类内部可以访问
2.包级私有的,一个包下可以访问
3.受保护的,一个包下,和子类,是导出api的一部分
4.公有的,任何地方都可以访问,是导出api的一部分
4.如果方法覆盖了超类的一个方法,子类的访问级别不能低于超类的访问级别
5.公有类的实例域决不能是公有的,包含公有可变域的类通常并不是线程安全的,有一种情况例外,
可以通过公有的静态的final域来暴露这些常量,但是这些常量一般都是不可变的对象或者是基本数据
类型的值
6.长度非零的数组总是可变的,所以让类具有公有的静态的final数组域,或者返回这种域的访问方法,
这是错误的,解决方案有两种:
第一种把公有的数组域变成私有的,提供一个返回不可变的列表的方法Collections.unmodifiableList(Arrays.asList(arrays))。
第二种把公有的数组域变成私有的,提供一个返回数组的一个拷贝的方法 arrays.clone()

第十六条,要在公有类中使用访问方法而非公有域(提供公有的访问方法)
1.如果类可以在它所在的包之外进行访问,就提供访问方法,以保留将来改变该类内部表示法的灵活性
2.如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误

第十七条,使可变性最小化(接近不可改变的类)
一个复数例子 /chapter4/sample17/Complex
1.使类成为不可变的类的五条规则
1.不要提供任何会修改对象状态的方法(设值方法)
2.保证类不会被扩展,final class ,私有构造器 可以防止子类化
3.声明所有的域都是final
4.声明所有域都是私有的,提供公有的final域(15),是可以的,但是不利于以后改变内部的表示法,
可以提供公有的访问方法(16)
5.确保对于任何可变组件的互斥访问,不接受客户端的组件传入,访问,可以在构造器,访问方法和
readObject使用保护性拷贝(88)
2.不可变对象比较简单,只有一种状态,即被创建时的状态
3.不可变对象本质是线程安全的,它们不需要同步,可以自由地被共享,不仅可以共享不可变的对象,还可以
共享它们的内部信息,BigInteger在negate()方法中创建一个新的BigInteger时公用了mag,而没有拷贝mag数组
4.不可变对象为其他对象提供了大量的构件,其他对象维护不可变的对象会比较容易
5.不可变对象无偿提供了失败的原子性,它们状态永远不变,因此不存在临时不一致的可能性
6.不可变类的真正缺点是,对于每个不同的值都需要一个单独的对象,创建这些对象可能代价很高,特别是大型
对象,比如上百万位的BigInteger需要改变它的低位,这项操作消耗的时间和空间与BigInteger成正比
7.多步骤的操作,每个步骤都产生一个对象,除了最后的结果,其他的对象最终都会抛弃,这是性能问题就会
显露,解决方案:
1.如果能够精确地预测出客户端将需要在不可变类上执行那些复杂的多阶段操作,提供一个包级私有的可变
的配套类,比如BigInteger的可变配套类 MutableBigInteger,用于计算
2.提供一个公有的可变的配套类,比如 String 的公有的可变的配套类 StringBuilder
8.让类的所有构造器变成私有的或者包级私有的,提供一静态工厂方法替代公有构造器,这是一个灵活和替换的
好办法
9.BigInteger和BigDecimal 是可以子类化的,对于不可变的类可以子类化,会导致子类可能是可变的,所以需要对它
进行保护性的拷贝(50)
10.把一些开销昂贵的计算结果缓存在非final的域中,多次调用直接返回这个域,因为是不可变的,所以计算的
结果都是一样的,String的hashcode就使用了缓存
11.为不可变类实现serializable接口,并且包含了一个或者多个可变的对象域,就必须提供一个显式的readObject或者
readResolve方法,攻击者可能从不可变的类创建可变的实例(88)伪造字节流,取得不可变域的引用进行修改
12.除非有很好的理由要让类成为可变的类,否则它就应该是不可变;如果类不能被做成不可变的,仍然应该
尽可能的限制它的可变性;除非有令人信服的理由要使域变成非final的,否则就是private final ;构造器应该创建
完全初始化的对象,并建立起所有约束关系

第十八条,复合优先于继承
1.在同一个包下的继承是非常安全的,都在同一个程序员的控制下,一些为了继承设计的类并且有好的文档说明,
也是非常安全的,对于普通的具体类进行跨包边界的继承是非常危险的
2.与方法调用不同的是,继承打破了封装性,子类依赖于其超类中特定功能的实现,超类发生改变,尽管子类
代码不变,也有可能遭到破坏
3.当子类基于父类需要满足一些先决条件,比如父类的所有添加的方法都需要覆盖,但是如果未来父类添加了
新的添加的方法,而子类没有覆盖,则导致调用这个新的添加的方法,把元素加进子类中
4.当子类不覆盖父类的方法,只是添加新的方法,但是如果未来父类提供了一个和你新增的方法签名,只是
返回值不一样,则子类编译错误,如果方法签名和返回值一样,则覆盖了父类的方法
5.新类使用一个私有域引用现有类的一个实例,这种设计称为复合,新类可以在方法里面调用现有类的方法
这种方法被称为转发方法,不管现有类添加新的方法,也不会影响新类
6.这种复合就像装饰了一个类,装饰者模式的一种形式,也称为包装类,注意的一点,包装类不适合用于回调
框架(self问题,不是很清楚)
7.A和B确实是is-a关系的时候,才适合用继承,Stack 不是向量 扩展了vector 和Properties不是散列 扩展了hashTable
这两个违反了这条,不推荐

第十九条,要么设计继承并提供文档,要么禁止继承
1.该类必须有文档说明它可覆盖的方法的自用性,那些情况下会调用可覆盖的方法
2.@implSpec 描述该方法的内部工作,可以查看AbstractCollection的remove(),覆盖iterator会影响remove
3.一个好的api文档应该描述一个给定的方法做了什么,而不是描述他是如何做到的,但是为了安全的子类化
你必须描述清楚那些有可能未定义的实现细节
4.类必须以精心挑选的受保护的方法的形式,提供适当的钩子,以便进入其内部工作
5.对于为了继承而设计的类每,唯一的测试方法就是编写子类,一般编写3个子类就足以测试一个扩展类
,在发布类之前先编写子类对类进行测试,因为受保护犯法,和域中的实现策略,你都做出了承诺,之后
的修改将会非常困难
6.构造器不能调用可被覆盖的方法,无论是直接的还是间接的,子类的方法会先于子类的构造器运行
7.无论是clone还是readObject,都不可以调用可覆盖的方法,不管是直接的还是间接的方式,对于readObject方法,
覆盖方法将在子类的状态被反序列化之前被先被运行,而对于clone方法,覆盖方法则是在子类的clone方法有机
会修正被克隆对象的状态之前先被运行
8.为了继承而设计的类实现serialized接口,必须使readResolve或者writeReplace成为受保护的方法
9.对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化,禁止子类化的方式,
可以final class或者私有构造器,提供一个公有的静态方法,也可以使用复合进行扩展

第二十条,接口优于抽象类(接口很好的组合,可以提供抽象的骨架实现类)
1.现有的类可以很容易被更新,以实现新的接口,但是无法更新现有的类来扩展新的抽象类,只能把新的抽象类
放到顶层,但这会破坏类的层次
2.接口是定义mixin(混合类型)的理想选择,mixin:类除了实现它的基本类型之外,还可以实现这个mixin类型,
抽象类却不能,因为类层次没有适当的地方插入mixin
3.接口允许构造非层次结构的类型框架,继承的话,会导致组合爆炸
4.接口使得安全地增强类的功能成为可能
5.对接口提供一个抽象的骨架实现类,可以把接口和抽象类的优点结合起来,AbstractInterface,比如AbstractSet,
AbstractList,AbstractMap,对于骨架类,好的文档是必须的@implSpec,例子代码/chapter4/sample20/AbstractMapEntry

第二十一条,为后代设计接口(java8缺省方法)
1.java8 的缺省方法(default method)可以给现有的接口提供新的方法,但是这样的操作还是充满风险的
2.举了一个例子,apache.commons包的一个集合,它使用了同步的操作,而java8在collection接口提供了removeIf的
缺省方法,removeIf根据条件删除元素,如果客户端调用removeIf,另一个线程进行修改集合,可能就会导致
异常
3.有了缺省方法,接口的现有实现就不会出现编译时没有报错或警告,运行时却失败的情况
4.虽然缺省方法已经是java平台的组成部分,但谨慎设计接口仍然是至关重要,因此测试每一个新的接口,
最起码实现3个实例,这样可以帮助你及时发现接口的缺陷,或许接口程序发布之后也能纠正,但是千万
不要指望它啦

第二十二条,接口只用于定义类型(用接口来定义常量是错误的)
1.当类实现接口时,接口就充当可以引用这个类的实例的类型,因此,类实现了接口,就表明客户端
可以对这个类的实例实施某些动作,为了任何其他目的而定义接口是不合适的
2.用接口来定义常量是错误的,它会把实现细节泄露到该类的导出api中,尽管以后不用和这个接口的常量了,
也不能去掉这个常量接口,为了兼容;它还会污染实现类的命名空间;应该创建一个常量的类来处理常量,
使用静态导入去掉类名调用;也可以使用枚举

第二十三条,类层次优于标签类(当编写便签类时,思考是否可以转成类层次)
1.标签类过于冗长、容易出错,并且效率低下,例子代码/chapter4/sample23/Figure
2.子类型化,可以定义出能表示多种风格的对象,也就是类层次
3.提供一个抽象类,给每个标签定义一个子类,纠正了标签类的所有缺点 //chapter4/sample23/CorrectFigure

第二十四条,静态成员类优于非静态成员类(嵌套类,非静态成员类会与关联外围类简立关系)
1.嵌套类是指定义在另一个类内部的类,主要是服务与外围类;四种嵌套类:
1.静态成员类
可以看作是一个普通类,只是碰巧被声明在另一个类的内部,他可以访问外围类的所有成员,包括私有的
静态成员类是外围类的一个静态成员,也可以使用访问修饰符
2.非静态成员类
非静态成员类的每一个实例都隐含与外围类的一个外围实例相关联,当非静态成员类的实例被创建的时候,
他和外围类的关联关系也随之被简立,外围类方法里面调用非静态成员类的构造器,关联关系就会简立,也
可以手工创建 instance.new MemberClass()。这种关联关系需要消耗非静态成员类实例的空间,并增加构造的时间。
可以访问外围了的方法,或者获取外围类的this
3.匿名类
没有名字的类,可以出现在代码中任何允许表达式的地方,当且仅当匿名类出现在非静态的环境中,它
才有外围实例。但是即使他们出现在静态坏境中,也不可能拥有任何静态成员,而是拥有常数变量;
无法扩展一个类,除了从超类继承得到之外,匿名类的客户端无法调用任何成员
4.局部类
在任何可以声明局部变量的地方都可以声明局部类,局部类有名字,可以重复利用;在非静态环境
中定义的时候,才有外围实例,不能包含静态成员,建议不要太长,会影响可读性
2.如果声明成员类不要求访问外围实例,就要始终把修饰符static放在它的声明中

第二十五,限制源文件为单个顶级类(一般不会犯,建议使用内部类)

上一篇下一篇

猜你喜欢

热点阅读