Effective Java(第三版) 学习笔记 - 第四章 类和接口 Rule15~Rule19
目录
Rule15 使类和成员的可访问性最小化
这个感觉没什么好说的,对应Java的封装特性。只对外提供外部需要的内容,隐藏内部具体实现,从而达到解耦的目的。
四种访问控制:
- 私有的(private)
- 包级私有(package-private)也叫缺省
- 受保护的(protected)
- 公有的(public)
※ 迪米特法则 相关
Rule16 要在公有类中使用访问方法而非公有域
与Rule15相关,简单而言就一句话,私有化类属性、对外提供get/set方法进行操作
※ 如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误。
※ 迪米特法则 相关
Rule17 使可变性最小化
如何定义一个不可变类:
- 不要提供任何会修改对象状态的方法(即set设值方法)
- 保证类不会被扩展。防止子类化、可以声明类为final。
- 声明所有的域都是final的。
- 声明所有的域都是私有的。
- 确保对于任何可变组件的互斥访问。
如果对象是不可变的,那么本质上他们是线程安全的,不需要同步,也可以自由的被共享。如果类不能被做成不可变的,那也仍然应该尽可能的限制它的可变性。
不可变对象的缺点:对于不同的值,都需要一个对象与之对应。
Rule18 复合优先于继承
如果看过设计模式相关的内容,就会知道所有的设计模式遵循的原则是:
- 开闭原则:对扩展开放,对修改关。
- 里氏替换原则:这是针对继承的原则。防止子类重写了父类定义的方法之后,改变了父类原先定义该方法的功能初衷。
- 依赖倒置原则:面向接口编程,而不是面向实现编程。高层不应该依赖低层实现。
- 单一职责原则:一个类甚至一个方法,只做好自己的事情。
- 接口隔离原则:一个接口只提供与自己相关的方法,接口上的单一职责。
- 迪米特法则(最少知识原则):只与自己认识的对象进行交流,不与陌生人沟通,如果需要经由中间人转述。
- 合成复用原则:在使用继承之前,优先考虑时候使用复合就可以满足功能需求,减少继承。
本书的Rule18其实就是设计模式里面的,合成复用原则。为什么不推荐优先考虑继承来做业务实现,因为继承打破了封装性,不利于最终的高内聚、低耦合。Java8中接口的定义中可以有具体的default实现了,想一想,如此一来,是不是可以替代一些abstract类的作用。
直接借用书中的用例:
如果需要一个新功能,统计Set中曾经新增元素多少个?
// Broken - Inappropriate use of inheritance! (Page 87)
public class InstrumentedHashSet<E> extends HashSet<E> {
// The number of attempted element insertions
private int addCount = 0;
public InstrumentedHashSet() {
}
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
public static void main(String[] args) {
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("Snap", "Crackle", "Pop"));
System.out.println(s.getAddCount());
}
}
预想得到的是3,但是如果执行的话发现,打印的是6。为什么?因为在HashSet的继承体系往上追溯的话就发现,AbstractCollection<E>类中定义的addAll的具体实现是循环调用add方法。所以上文中的计数,在addAll中计数了一遍,在add方法中又被重复计数了一遍。
public abstract class AbstractCollection<E> implements Collection<E> {
...
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
...
}
如果要按照合成复用原则来实现,我们应该如何打破set的继承体系,避免重复计数呢。
// Reusable forwarding class (Page 90)
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s = s; }
public void clear() { s.clear(); }
public boolean contains(Object o) { return s.contains(o); }
public boolean isEmpty() { return s.isEmpty(); }
public int size() { return s.size(); }
public Iterator<E> iterator() { return s.iterator(); }
public boolean add(E e) { return s.add(e); }
public boolean remove(Object o) { return s.remove(o); }
public boolean containsAll(Collection<?> c)
{ return s.containsAll(c); }
public boolean addAll(Collection<? extends E> c)
{ return s.addAll(c); }
public boolean removeAll(Collection<?> c)
{ return s.removeAll(c); }
public boolean retainAll(Collection<?> c)
{ return s.retainAll(c); }
public Object[] toArray() { return s.toArray(); }
public <T> T[] toArray(T[] a) { return s.toArray(a); }
@Override public boolean equals(Object o)
{ return s.equals(o); }
@Override public int hashCode() { return s.hashCode(); }
@Override public String toString() { return s.toString(); }
}
// Wrapper class - uses composition in place of inheritance (Page 90)
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}
@Override public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
public static void main(String[] args) {
InstrumentedSet<String> s = new InstrumentedSet<>(new HashSet<>());
s.addAll(List.of("Snap", "Crackle", "Pop"));
System.out.println(s.getAddCount());
}
}
可能有同学看了之后还是不太理解,为什么重新自定义了一个ForwardingSet<E>类、也还是实现了Set<E>接口,然后InstrumentedSet<E>也继承了ForwardingSet<E>类,最终打印的却是3。
可能会问,ForwardingSet中,针对add、addAll也没做什么特殊处理,InstrumentedSet中间,add、addAll方法不也是调用了父类super方法么?
为了方便理解,我们在原书的用例上稍微改造下
/**
* @author xuweijsnj
*/
public class InstrumentedSet<E> implements Set<E> {
private int addCount = 0;
private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s = s; }
public void clear() { s.clear(); }
public boolean contains(Object o) { return s.contains(o); }
public boolean isEmpty() { return s.isEmpty(); }
public int size() { return s.size(); }
public Iterator<E> iterator() { return s.iterator(); }
public boolean add(E e) {
addCount++;
return s.add(e);
}
public boolean remove(Object o) { return s.remove(o); }
public boolean containsAll(Collection<?> c)
{ return s.containsAll(c); }
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return s.addAll(c);
}
public boolean removeAll(Collection<?> c)
{ return s.removeAll(c); }
public boolean retainAll(Collection<?> c)
{ return s.retainAll(c); }
public Object[] toArray() { return s.toArray(); }
public <T> T[] toArray(T[] a) { return s.toArray(a); }
@Override public boolean equals(Object o)
{ return s.equals(o); }
@Override public int hashCode() { return s.hashCode(); }
@Override public String toString() { return s.toString(); }
public int getAddCount() {
return addCount;
}
public static void main(String[] args) {
InstrumentedSet<String> s = new InstrumentedSet<>(new HashSet<>());
s.addAll(List.of("Snap", "Crackle", "Pop"));
System.out.println(s.getAddCount());
s.forEach(System.out::println);
}
}
打印结果---
3
Pop
Crackle
Snap
可以发现,去除InstrumentedSet<E>的继承,直接实现Set<E>,最终的结果是预想的,同时元素也加入到了Set<E>中。其实,复合和InstrumentedSet<E>继承ForwardingSet<E>没有什么直接的关系。主要的原因是InstrumentedSet<E>并没有继承AbstractCollection<E>类,而是在自身中定义了private final Set<E> s; ,add、addAll等系列方法的实现,是直接调用内部属性s来完成的,而s的是HashSet<E>的声明是在InstrumentedSet<E>的构造函数中完成的。addAll还是利用AbstractCollection<E>类中定义的循环调用add方法来实现的,但是不会进入InstrumentedSet<E>中定义的add方法,而被重复计数。
这样的手法叫做转发,类中的方法叫做转发方法。
那么书中给出的ForwardingSet用作给InstrumentedSet继承有什么作用或者说意义呢?如果你还需要另一个关于Set的新增功能,但是与addCount计数无关,这个时候你就可以直接继承ForwardingSet,实现新的功能就行了,而不需要再次把set自有的方法在新类中再次转发实现一边。这也符合依赖倒置原则。
如果按设计模式来看待,这样的设计手法,其实就是装饰器(Decorator)或者叫修饰者模式。比继承灵活,可以在不改变原有对象的情况下,增加一些新的扩展功能。
但是需要注意一点的是,这种手法不适合回调框架,因为回调框架需要将自身的引用传递给其他对象,而经过了包装类包装之后,被包装的类并不知道他外面的包装对象。
/**
* @author xuweijsnj
*/
public interface Callback {
public void callback();
}
/**
* @author xuweijsnj
*/
public class A implements Callback {
private B b;
public A(B b) {
this.b = b;
}
public void ask() {
new Thread(() -> {b.apply(this);}, "线程A").start();
}
@Override
public void callback() {
System.out.println("处理A中回调");
}
public static void main(String[] args) {
new C(new A(new B())).ask();
}
}
/**
* @author xuweijsnj
*/
public class B {
public void apply(Callback c) {
System.out.println("接受请求");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
c.callback();
}
}
/**
* @author xuweijsnj
*/
public class C implements Callback {
private A a;
public C(A a) {
this.a = a;
}
public void ask() {
a.ask();
}
@Override
public void callback() {
a.callback();
System.out.println("处理C中回调");
}
}
----打印结果
接受请求
处理A中回调
你会发现,用装饰器修饰了具备回调功能时,并没有打印预想的“处理C中回调”。
Rule19 要么设计继承并提供文档说明,要么禁止继承
直接用书中的代码样例直观感受下
// Class whose constructor invokes an overridable method. NEVER DO THIS! (Page 95)
public class Super {
// Broken - constructor invokes an overridable method
public Super() {
overrideMe();
}
public void overrideMe() {
}
}
// Demonstration of what can go wrong when you override a method called from constructor (Page 96)
public final class Sub extends Super {
// Blank final, set by constructor
private final Instant instant;
Sub() {
instant = Instant.now();
}
// Overriding method invoked by superclass constructor
@Override public void overrideMe() {
System.out.println(instant);
}
public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
}
----打印结果
null
2021-06-11T08:56:29.633780300Z
第一次打印时,是父类Super的构造函数中调用overrideMe触发,但是当时子类Sub还没有执行构造函数对instant进行初始化。
该规则的结果很简单:
专门为了继承而设计类,构造器不能调用可被覆盖的方法。准确的说,clone 和 readObject 方法这种行为上和构造器类似的方法都不可以调用可被重写的方法。
同时需要编写具体的子类来进行测试,书写子类化实现文档(希望你先把日常的代码注释写好)。
本文技术菜鸟个人学习使用,如有不正欢迎指出修正。xuweijsnj