第15条:使类和成员的可访问性最小化

这一条没什么,其实就是封装的概念,即:隐藏掉内部的细节,对外提供接口。通过 privite、default、protected、public 等关键字来控制访问的范围。

第16条:要在公有中使用访问方法而非公有域

这一点和第15条有所关联,简单的说就是,将类的成员变量私有化,对外提供 get、set 方法。

  • 但是如果类是包级私有的,或者是私有的嵌套类,那么直接暴露出公有的数据域也没什么问题。

第17条:使可变性最小化

如果对象不可变,其实它就可以看作是线程安全的,被多个线程共享也没有关系。

如果类没办法设计成不可变的,那么也应该尽可能的限制它的可变性。

总而言之,坚决不要为每个 get 方法都编写一个对应的 set 方法,除非有着很好的理由要让类变成可变的,否则它就应该是不可变的。

让类不可变的 5 条规则:

  1. 不要提供任何会修改对象状态的方法(即set设值方法)
  2. 保证类不会被扩展。防止子类化、可以声明类为final。
  3. 声明所有的域都是final的。
  4. 声明所有的域都是私有的。
  5. 确保对于任何可变组件的互斥访问。

第18条:复合优先于继承

【设计模式】第一篇:概述、耦合、UML、七大原则,详细分析总结(基于Java)—— (七) 合成复用原则

在设计模式中,存在七大设计原则,其中合成复用原则就是这一点:

定义:在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现

书中的例子就很能说明问题:

示例需求:统计 Set 中曾经新增元素多少个

  1. // Broken - Inappropriate use of inheritance! (Page 87)
  2. public class InstrumentedHashSet<E> extends HashSet<E> {
  3. // The number of attempted element insertions
  4. private int addCount = 0;
  5. public InstrumentedHashSet() {
  6. }
  7. public InstrumentedHashSet(int initCap, float loadFactor) {
  8. super(initCap, loadFactor);
  9. }
  10. @Override public boolean add(E e) {
  11. addCount++;
  12. return super.add(e);
  13. }
  14. @Override public boolean addAll(Collection<? extends E> c) {
  15. addCount += c.size();
  16. return super.addAll(c);
  17. }
  18. public int getAddCount() {
  19. return addCount;
  20. }
  21. public static void main(String[] args) {
  22. InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
  23. s.addAll(List.of("Snap", "Crackle", "Pop"));
  24. System.out.println(s.getAddCount());
  25. }
  26. }

在主函数的测试中,通过 List.of("Snap", "Crackle", "Pop") 添加了三个元素(这个方法是在 Java 9 才有的,低一点的版本可以选择 Arrays.asList 方法代替),但是最终的 addCount 的结果并不是 3 却是 6。

查看 HashSet 的继承体系,发现在 AbstractCollection 中 addAll 方法的定义就是循环调用了 add 方法,所以 addAll 和 add 方法中都进行了计数,所以重复了。

  1. public abstract class AbstractCollection<E> implements Collection<E> {
  2. ...
  3. public boolean addAll(Collection<? extends E> c) {
  4. boolean modified = false;
  5. for (E e : c)
  6. if (add(e))
  7. modified = true;
  8. return modified;
  9. }
  10. ...
  11. }

解决方案如下:

  1. // Reusable forwarding class (Page 90)
  2. public class ForwardingSet<E> implements Set<E> {
  3. private final Set<E> s;
  4. public ForwardingSet(Set<E> s) { this.s = s; }
  5. public void clear() { s.clear(); }
  6. public boolean contains(Object o) { return s.contains(o); }
  7. public boolean isEmpty() { return s.isEmpty(); }
  8. public int size() { return s.size(); }
  9. public Iterator<E> iterator() { return s.iterator(); }
  10. public boolean add(E e) { return s.add(e); }
  11. public boolean remove(Object o) { return s.remove(o); }
  12. public boolean containsAll(Collection<?> c)
  13. { return s.containsAll(c); }
  14. public boolean addAll(Collection<? extends E> c)
  15. { return s.addAll(c); }
  16. public boolean removeAll(Collection<?> c)
  17. { return s.removeAll(c); }
  18. public boolean retainAll(Collection<?> c)
  19. { return s.retainAll(c); }
  20. public Object[] toArray() { return s.toArray(); }
  21. public <T> T[] toArray(T[] a) { return s.toArray(a); }
  22. @Override public boolean equals(Object o)
  23. { return s.equals(o); }
  24. @Override public int hashCode() { return s.hashCode(); }
  25. @Override public String toString() { return s.toString(); }
  26. }
  27. // Wrapper class - uses composition in place of inheritance (Page 90)
  28. public class InstrumentedSet<E> extends ForwardingSet<E> {
  29. private int addCount = 0;
  30. public InstrumentedSet(Set<E> s) {
  31. super(s);
  32. }
  33. @Override public boolean add(E e) {
  34. addCount++;
  35. return super.add(e);
  36. }
  37. @Override public boolean addAll(Collection<? extends E> c) {
  38. addCount += c.size();
  39. return super.addAll(c);
  40. }
  41. public int getAddCount() {
  42. return addCount;
  43. }
  44. public static void main(String[] args) {
  45. InstrumentedSet<String> s = new InstrumentedSet<>(new HashSet<>());
  46. s.addAll(List.of("Snap", "Crackle", "Pop"));
  47. System.out.println(s.getAddCount());
  48. }
  49. }

可以看到,我们在原先的 InstrumentedHashSet<E> 类 和 HashSet<E>类之间,加了一个新的 ForwardingSet<E> 类。

  • InstrumentedHashSet<E> 继承 ForwardingSet<E>ForwardingSet<E> 再去实现 Set<E>

其实复合的这个特点,并不体现在这个新的继承关系上,而是在 ForwardingSet<E> 中,没有继承 AbstractCollection<E> 而是在自己的成员变量中定义了private final Set<E> s; ,所以在 InstrumentedHashSet<E> 中调用 add、addAll 方法的时候,是使用自己的成员 s,调用 addAll 的时候,依旧是循环调用 AbstractCollection<E> 中的 add 方法,但是却不会再进入到 InstrumentedSet<E> 中定义的add方法。

  • 这种方式也叫做转发。
  • 其实完全可以在自己的这个 InstrumentedSet<E> 类中进行 复合,但是书中引入了一个新类 ForwardingSet<E> d的意义在于,如果以后你还想增加一个关于 set 的新方法,你就可以直接继承 ForwardingSet 就好了,不必再把 set 原有的方法再新类的方法再转发实现一次。
  • 这种设计手法就是装饰器模式。
  • 但是这种手法不适合回调框架(需要将自身的引用传递给其他对象)

第19条:要么设计继承并提供文档说明,要么禁止继承(待修改)

  • 对于专门为了继承而设计的类, 需要具有良好的文档
  • 类必须通过某种形式提供适当的钩子(hook)
  • 对于为了继承而设计的类,唯一的测试方法就是编写子类
  • 为了允许继承,类还必须遵守其他的一些约束
  • 对于那些并非为了安全地进行子类化而设计和编写文档的类, 要禁止子类化.

第20条:接口优于抽象类

Java-Ideal-Interview —— 3.3.2 抽象类和接口的区别(重要)

第21条:为后代设计接口

如果针对现存的接口进行功能扩展

  • Java8 之前:把所有实现了这个接口的类重新实现新的方法,但是如果这个接口是对外提供的,就会牵扯很广,很麻烦。
  • Java8 之后:8中接口的新特性,允许接口拥有了默认的缺省实现,之前实现这个接口的子类就可以用这个默认实现。

相比之前要相对简单一点,可以考虑采用java8接口的新特性,缺省实现来完成扩展。但是需要注意的是,就算接口拥有缺省实现了,也是一件拥有风险的事情。

例如,java.util.Collection中新增了removeIf,用于函数式编程。但是该书撰写直到出版时,貌似org.apache.commons.collections4.Collection.SynchronizedCollection中,这个类来自Apache Commons类库,但是并没有针对removeIf做同步锁的实装。这样一来,如果在那个版本使用了removeIf,由于java.util.Collection中的removeIf并没有加锁控制删除,如果另一个线程也同时操作了该集合,就会导致ConcurrentModificationException。而这由于是缺省方法来拓展的,编译并不会报错,只有到了真正运行时才会发现。

csdn@微笑小个子

第22条:接口只用于定义类型

接口只用定义类型,也就是说,其他任何目的而顶定义接口都是不恰当的。

例如书中提到的:用接口定义常量,其实这是一种不良的设计。

  • 暴露了实现细节到该类的导出API中。
  • 实现常量接口对于类的用户来说没有价值。
  • 如果以后的发行版本中不需要其中的常量了, 依然必须实现这个接口。
  • 所有子类的命名空间也会被接口中的常量污染。

第23条:类层次优先于标签类

  1. // Tagged class - vastly inferior to a class hierarchy! (Page 109)
  2. class Figure {
  3. enum Shape { RECTANGLE, CIRCLE };
  4. // Tag field - the shape of this figure
  5. final Shape shape;
  6. // These fields are used only if shape is RECTANGLE
  7. double length;
  8. double width;
  9. // This field is used only if shape is CIRCLE
  10. double radius;
  11. // Constructor for circle
  12. // 圆
  13. Figure(double radius) {
  14. shape = Shape.CIRCLE;
  15. this.radius = radius;
  16. }
  17. // Constructor for rectangle
  18. // 矩形
  19. Figure(double length, double width) {
  20. shape = Shape.RECTANGLE;
  21. this.length = length;
  22. this.width = width;
  23. }
  24. double area() {
  25. switch(shape) {
  26. case RECTANGLE:
  27. return length * width;
  28. case CIRCLE:
  29. return Math.PI * (radius * radius);
  30. default:
  31. throw new AssertionError(shape);
  32. }
  33. }
  34. }
  • Figure类 内含 Shape 枚举, 包含圆形和方形,通过构造函数来区分形状,如果再增加其他形状,就会变得很乱。这也就是标签类的形式。
  1. // Class hierarchy replacement for a tagged class (Page 110-11)
  2. abstract class Figure {
  3. abstract double area();
  4. }
  5. // Class hierarchy replacement for a tagged class (Page 110-11)
  6. // 圆
  7. class Circle extends Figure {
  8. final double radius;
  9. Circle(double radius) { this.radius = radius; }
  10. @Override double area() { return Math.PI * (radius * radius); }
  11. }
  12. // Class hierarchy replacement for a tagged class (Page 110-11)
  13. // 矩形
  14. class Rectangle extends Figure {
  15. final double length;
  16. final double width;
  17. Rectangle(double length, double width) {
  18. this.length = length;
  19. this.width = width;
  20. }
  21. @Override double area() { return length * width; }
  22. }
  • 如果使用具有层次结构的继承,就会更加清晰,后期扩展也很舒服。
  1. // Class hierarchy replacement for a tagged class (Page 110-11)
  2. // 正方形
  3. class Square extends Rectangle {
  4. Square(double side) {
  5. super(side, side);
  6. }
  7. }

第24条:优先考虑静态成员类

  • 类、类变量、类方法等均优先使用静态

第25条:限制源文件为单个顶级类

源文件:自己平时开发的 Java 文件

这一点也就是为什么我们从一开始学习 Java 的时候就提到,一个 Java 文件中只能有一个文件名和类名一致的类。