Effective Java(第三版) 学习笔记 - 第四章 类和接口 Rule15~Rule19

Effective Java(第三版) 学习笔记 - 第四章 类和接口 Rule15~Rule19

目录

Rule15 使类和成员的可访问性最小化

Rule16 要在公有类中使用访问方法而非公有域

Rule17 使可变性最小化

Rule18 复合优先于继承

Rule19 要么设计继承并提供文档说明,要么禁止继承


Rule15 使类和成员的可访问性最小化

这个感觉没什么好说的,对应Java的封装特性。只对外提供外部需要的内容,隐藏内部具体实现,从而达到解耦的目的。

四种访问控制:

  • 私有的(private)
  • 包级私有(package-private)也叫缺省
  • 受保护的(protected)
  • 公有的(public)

※ 迪米特法则 相关

 

Rule16 要在公有类中使用访问方法而非公有域

与Rule15相关,简单而言就一句话,私有化类属性、对外提供get/set方法进行操作

※ 如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误。

※ 迪米特法则 相关

 

Rule17 使可变性最小化

如何定义一个不可变类:

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

如果对象是不可变的,那么本质上他们是线程安全的,不需要同步,也可以自由的被共享。如果类不能被做成不可变的,那也仍然应该尽可能的限制它的可变性。

不可变对象的缺点:对于不同的值,都需要一个对象与之对应。

 

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

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值