Effective Java 讀書筆記(三):類與接口

1 最小化類和成員的可訪問性

(1)封裝

  • 封裝對組成系統的組件進行解耦,從而容許這些組件獨立開發,測試,優化,使用,理解和修改。
  • 封裝提升了軟件的複用性,由於組件間的耦合度低使得它們不只在開發環境,並且在別的環境也能變得有用。
  • 封裝下降了開發大型系統的風險,由於即便系統不可用了,但這些獨立的組件卻有可能仍可用。

(2)對於成員(域,方法,嵌套類,或者嵌套接口),都有四種可能的訪問級別

  • private:成員只能被聲明它的頂級類訪問。
  • default:成員能夠被聲明它的包下面的全部類訪問。
  • protected:成員能夠被聲明它的類的子類訪問,同時,聲明它的包下面的全部類也能夠訪問它。
  • public:成員能夠在任何地方被訪問。

(3)模塊

  • Java 9在中,做爲模塊系統(module system)引入了兩種額外的隱式訪問級別。
  • 一個模塊就是一組包,就像一個包是一組類同樣。
  • 一個模塊能夠經過在它的模塊聲明(通常包含在module-info.java的源文件中)的導出聲明顯式地導出它的一些包。
  • 模塊的非導出包的公有和受保護成員在模塊外部是沒法訪問的,而在模塊內部,訪問性不受導出聲明的影響。

(4)demo

// Potential security hole!
public static final Thing[] VALUES = { ... };

// 方案1
private static final Thing[] PRIVATE_VALUES = { ... }; 
public static final List<Thing> VALUES =   
        Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES)) ;

// 方案2
private static final Thing[] PRIVATE_VALUES = { ... }; 
public static final Thing[] values() {
    return PRIVATE_VALUES.clone(); 
}

2 在公有類中使用訪問方法,而不是公有域

(1)概述

  • 若是一個類在包外能夠被訪問,就應該提供訪問方法,以此來保留改變類內部展現的靈活性。
  • 若是一個類是包級私有或者是個私有嵌套類,那麼暴露它的數據域也沒有什麼本質上的錯誤

(2)demo

// Degenerate classes like this should not be public!
class Point { 
    public double x; 
    public double y;
}

// Encapsulation of data by accessor methods and mutators
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; 
    }
}
  • 假設須要限制x,y的範圍爲[1,100],第一種方式是沒法實現的,你可能說能夠在外部代碼限制,但實際上這破環了單一職責原則。

3 使可變性最小化

(1)不可變類的5個原則

  • 不要提供修改對象狀態的方法(沒有setter方法)
  • 確保這個類不能被拓展(設置爲final,防止被繼承)
  • 全部域設置爲final,只能賦值一次且沒法修改
  • 全部域設置爲私有
  • 確保對任何可變組件(引用屬性)的互斥訪問:對引用屬性進行保護性拷貝。

(2)demo

// Immutable complex number class 
public final class Complex {
    private final double re; 
    private final double im;
    public Complex(double re, double im) { 
        this.re = re;
        this.im = im;
    }
    public double realPart() { 
        return re; 
    } 
    public double imaginaryPart() { 
        return im; 
    }
    // plus表明相加返回新的對象,函數式編程,狀態不可變。
    // add表明相加返回this對象,命令式編程,狀態可變。
    public Complex plus(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }
    public Complex minus(Complex c) { 
        return new Complex(re - c.re, im - c.im);
    }
    public Complex times(Complex c) {
        return new Complex(re * c.re - im * c.im, re * c.im + im * c.re); 
    }
    public Complex dividedBy(Complex c) {
        double tmp = c.re * c.re + c.im * c.im;
        return new Complex((re * c.re + im * c.im) / tmp, (im * c.re - re * c.im) / tmp);

    }
    @Override 
    public boolean equals(Object o) { 
        if (o == this) return true;
        if (!(o instanceof Complex)) return false;
        Complex c = (Complex) o;
        // See page 47 to find out why we use compare instead of == 
        return Double.compare(c.re, re) == 0 && Double.compare(c.im, im) == 0; 
    }
    @Override 
    public int hashCode() {
        return 31 * Double.hashCode(re) + Double.hashCode(im);
    }
    @Override 
    public String toString() { 
        return "(" + re + " + " + im + "i)";
    } 
}

(3)優點

  • 不可變對象天生就是線程安全的,它們不要求同步。
  • 不可變對象能夠自由地被共享
  • 不可變類能夠提供靜態工廠將頻繁被請求的實例緩存起來,從而避免重複建立現有實例。(Integer的-128~127)
  • 永遠都不用進行保護性拷貝(下降內存佔用)
  • 不只能夠共享不可變對象,它們的內部信息也能夠被共享。(BigInteger的實現符號使用int,數據使用int[],取反操做不須要clone數組)
  • 不可變對象爲其它對象提供了大量的構件
  • 不可變對象提供了免費的失敗原子機制(根本不會改變)

(4)缺點

  • 對於每一個不一樣的值都須要一個對應的對象

(5)設計原則

  • 類應該都是不可變的,除非有個很好的理由須要它們是可變的。
  • 若是一個類不能作成不可變,那就儘量限制它的可變性。(如:CountDownLatch,當計數變爲0就不能再使用了。)
  • 構造器應該徹底初始化對象,並創建好不變性。

4 組合優於繼承

(1)繼承

  • 繼承違反了封裝原則:將來父類升級,將致使子類的失效(相同簽名不一樣返回致使編譯不經過,相同簽名且重寫了父類將來的方法將致使不可預測的錯誤)。
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;
    }
}

// jdk7
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(Arrays.asList({"Snap", "Crackle", "Pop"})); // 結果爲3

// jdk9
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(Arrays.asList({"Snap", "Crackle", "Pop"})); // 結果爲6

// jdk升級後,HashSet內部addAll方法依賴於add方法

(2)組合

// 包裝類:裝飾者模式
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;
    }
} 

// 轉發類:組合模式
// Reusable forwarding class
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(); 
    }
}
  • Guava就爲全部的集合接口提供了轉發類,無需本身實現轉發類。
  • SELF問題:包裝者對象不適用於回調框架,由於在回調框架裏,對象須要將自身引用傳給別的對象,以便別的對象在後續進行調用。由於被包裝對象並不知道它的包裝者,它將一個引用傳給它本身(this)同時回調也避開了包裝者。
  • 只有當類B和類A是「is-a」的關係時,類B才應該繼承類A。

5 若要設計繼承,則提供文檔說明,不然禁止繼承

  • 必須在這個類的文檔裏爲可覆蓋方法說明它的自用性(self-use):對於每一個公有方法或受保護方法,文檔裏都必須指明這個方法調用了哪些可覆蓋方法,是以什麼順序調用的,每一個調用的結果是如何影響接下來的處理過程。
  • 測試一個用於被繼承的類的惟一方式是編寫子類:經驗代表,三個子類就足以測試一個可擴展的類了。並且這些子類應該由父類做者以外的人來編寫。
  • 一個類容許被繼承時:
    • 構造器必定不能調用可覆蓋方法:構造器裏調用不可覆蓋的方法,即私有方法,final方法和靜態方法,則是安全的。
    • 實現Cloneable接口或Serializable接口時,clone方法或readObject方法,都不該該直接或間接地調用一個可覆蓋方法。
    • 實現Serializable接口且擁有readResolve方法或writeReplace方法時,須要設置爲受保護的,而不是私有的。
  • 防止子類化:
    • final修飾類
    • 構造器設置爲私有並提供公有的靜態工廠建立方法

6 接口優於抽象類

(1)接口優點

  • 現有類能夠很容易地被改造以實現一個新的接口。
  • 接口是定義混合類型(mixins)的理想選擇。
  • 接口構造非層次結構的框架。
  • 經過包裝者類模式,使用接口使得安全地加強類的功能成爲可能。html

  • 混合模式demojava

public interface Singer { 
    AudioClip sing(Song s);
}
public interface Songwriter {
    Song compose(int chartPosition); 
}

// 既是歌手又是做曲人
public interface SingerSongwriter extends Singer, Songwriter { 
    AudioClip strum();
    void actSensitive();
}

(2)接口

  • 域只容許用public static修飾
  • 方法:
    • private static修飾
    • public static修飾
    • public default修飾
public interface Interface {
    public static int test = 0;

    public static void publicTest() {
        privateTest();
    }

    private static void privateTest() {
        System.out.println(test);
    }

    public default void defaultTest() {
        System.out.println("defaultTest");
    }
    
    public abstract void abstractTest();
}

public class InterfaceImpl implements Interface {
    public void extendTest() {
        defaultTest();
    }
    
    @Override
    public void abstractTest() {
        System.out.println("abstractTest");
    }

    public static void main(String[] args) {
        InterfaceImpl extend = new InterfaceImpl();
        extend.extendTest();
        extend.defaultTest();
        
        System.out.println(Interface.test);
        Interface.publicTest();
    }
}

(3)接口與實現類結合使用:模板類

  • 模板實現類在基本接口方法上實現剩餘的非基本接口方法(模板方法模式)。編程

  • demo1:繼承模板類數組

// AbstractList是模板類,基於它能夠很是容易實現新的類
// 適配器模式:int[] -> List<Integer>
static List<Integer> intArrayAsList(int[] a) {
    Objects.requireNonNull(a);
    return new AbstractList<>() {
        @Override 
        public Integer get(int i) { 
            return a[i]; 
        }
        @Override 
        public Integer set(int i, Integer val) { 
            int oldVal = a[i];
            a[i] = val; 
            return oldVal; 
        }
        @Override 
        public int size() {
            return a.length; 
        }
    };
}
  • demo2:模板類編寫
// Map.Entry最主要的兩個方法:getKey和getValue(必需實現) setValue(可選,只有可修改的Map才須要實現)
// hashCode、equals和toString方法能夠由模板類提供默認實現
public abstract class AbstractMapEntry<K,V> implements Map.Entry<K,V> {
    // Entries in a modifiable map must override this method 
    @Override 
    public V setValue(V value) {
        throw new UnsupportedOperationException(); 
    }
    
    // Implements the general contract of Map.Entry.equals 
    @Override 
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Map.Entry)) return false;
        Map.Entry<?,?> e = (Map.Entry) o;
        return Objects.equals(e.getKey(), getKey()) && Objects.equals(e.getValue(), getValue());
    }
    
    // Implements the general contract of Map.Entry.hashCode 
    @Override 
    public int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }
    
    @Override 
    public String toString() { 
        return getKey() + "=" + getValue();
    } 
}

7 接口拓展

  • java8以後,jdk中的集合相關接口都添加了lambda的支持,可是這隻對繼承模式的實現類有效,對於採用組合模式的拓展類可能致使問題(裝飾者模式)。好比:原本是須要加鎖的,可是新加入的default方法並無加鎖,就可能致使併發問題。
  • 因此說對於接口的拓展(添加默認方法或者添加抽象方法)都須要慎重,實在沒有好的實現不如新建立一個接口去進行拓展,避免由於疏忽和形成不可預測的錯誤。

8 接口只用來定義類型

  • 經過常量接口模式來使用接口是很糟糕的:若是一個非final類實現了一個常量接口,那麼它的全部子類的命名空間都將被接口的常量污染。
public interface PhysicalConstants {
    static final double AVOGADROS_NUMBER = 6.022_140_857e23;
    static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
    static final double ELECTRON_MASS = 9.109_383_56e-31;
}
  • 替代方案
public class PhysicalConstants {
    private PhysicalConstants() { } // Prevents instantiation
    public static final double AVOGADROS_NUMBER = 6.022_140_857e23;
    public static final double BOLTZMANN_CONST =1.380_648_52e-23;
    public static final double ELECTRON_MASS = 9.109_383_56e-31;
}

9 優先使用類層次,而不是標籤類

  • 標籤類
    • 混亂的模板代碼:枚舉、標籤域和switch語句等
    • 標籤類實例負擔許多不相關的域,致使內存佔用增長。
    • switch-case可能致使運行時錯誤
class Figure {
    enum Shape { RECTANGLE, CIRCLE };
    
    // Tag field - the shape of this figure
    final Shape shape;
    
    // These fields are used only if shape is RECTANGLE
    double length;
    double width;
    
    // This field is used only if shape is CIRCLE
    double radius;
    
    // Constructor for circle
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    } 
    
    // Constructor for rectangle
    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    } 
    
    double area() {
        switch(shape) {
            case RECTANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError(shape);
        }
    }
}
  • 類層次
    • 代碼清晰簡單
    • 域都不可變
    • 消除運行時失敗
abstract class Figure {
    abstract double area();
}

class Circle extends Figure {
    final double radius;
    Circle(double radius) { 
        this.radius = radius; 
    }
    @Override 
    double area() { 
        return Math.PI * (radius * radius); 
    }
} 

class Rectangle extends Figure {
    final double length;
    final double width;
    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    } 
    @Override 
    double area() { 
        return length * width;
    }
}

10 優先考慮靜態成員類

(1)嵌套類

  • 嵌套類是定義在另外一個類中的類。
  • 一個嵌套類的存在應該是爲了服務它的外圍類。
  • 若是一個嵌套類還能夠用於別的上下文,那麼它就應該是個頂層類
  • 分類:
    • 靜態成員類
    • 非靜態成員類
    • 匿名類
    • 局部類

(2)靜態成員類

  • 可視爲普通類
  • 可訪問外圍類的全部成員(包括private修飾的)
  • 通常做爲一個公有的輔助類,只有與它的外圍類一塊兒用時纔有意義。
public class Calculator{
    public static enum Operation{
        PLUS,MINUS;
    }
}
  • 私有靜態成員類一般被用來展現表明外圍類對象的組件。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>{
    static class Node<K,V> implements Map.Entry<K,V>{}
}
  • demo
public class OuterClass {
    private static int outTest = 1;

    public static class InnerClass {
        private int inTest;

        public void test() {
            System.out.println(outTest);
        }

    }

    public static void main(String[] args) {
        InnerClass innerClass = new OuterClass.InnerClass();
        innerClass.test();
    }
}

(3)非靜態成員類

  • 非靜態成員類的每一個實例都是隱式地和它的外圍類的實例關聯在一塊兒。(內存佔用比較高,由於它擁有外圍類實例的引用)
  • 在非靜態成員類的實例方法內部,你能夠調用外圍實例的方法或經過標識了this的構造來獲取外圍實例的引用。
  • 非靜態成員類的一個一般的用法是,定義一個適配器 ,而這個適配器讓外圍類的實例被當作是某個不相關類的實例。
// 如:List -> Iterator
public class MySet<E> extends AbstractSet<E> {
    // Bulk of the class omitted
    @Override 
    public Iterator<E> iterator() {
        return new MyIterator();
    } 
    private class MyIterator implements Iterator<E> {
       
    }
}
  • demo
public class OuterClass {
    private int outTest = 1;

    public class InnerClass {
        private int inTest;

        public void test() {
            System.out.println(outTest);
            // System.out.println(OuterClass.this.outTest); 
        }

        public OuterClass getOuterClass(){
            return OuterClass.this;
        }
    }

    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass();
        InnerClass innerClass = outerClass.new InnerClass();
        innerClass.test();
    }
}

(4)匿名類

  • 匿名類能夠沒有名字。
  • 匿名類並非它的外圍類的一個成員。
  • 匿名類出如今靜態的上下文當中,也不能擁有除了常量型變量的任何的靜態成員,這些常量型變量是final的基本類型或初始化常量表達式的字符串屬性。
  • 匿名類須要在聲明的時候初始化。
  • 匿名類沒法使用instanceof來判斷。
  • 匿名類沒法繼承或實現其餘接口了。
  • 匿名類的常見用法是實現靜態工廠方法,參見第6節的demo1。
  • 匿名類是建立小的函數對象和處理對象的首選方式(如今被lambda取代)
public static void main(String[] args) {
    try(AutoCloseable myCloseable = new AutoCloseable() {
        @Override
        public void close() {
            System.out.println("close success!");
        }
    };) {
        System.out.println("use myCloseable");
    } catch (Exception e) {
        e.printStackTrace();
    }
}

(5)局部類

  • 局部類與局部變量同樣,只存在局部做用域。
public static void main(String[] args) {
    class A{
        private int test = 1;
    };
    A a = new A();
    System.out.println(a.test);;
}

(6)用途

  • 若是一個嵌套類必須在方法外部可見,或者放在方法內部會顯得太長時,就使用成員類。
  • 若是成員類的實例須要擁有該類的外圍類的引用,就將其作成非靜態;否則,就將其作成靜態。
  • 假設一個類應當在方法內部,若你須要只從一個地方建立實例並且已經存在一個類型能說明這個類的特徵,那麼將其作成匿名類;不然,就將其作成局部類。

參考:緩存

相關文章
相關標籤/搜索