和我一起學Effective Java之類和接口


類和接口

使類和成員的可訪問性最小

信息隱藏(information hiding)/封裝(encapsulation):隱藏模塊內部數據和其他實現細節,通過API和其他模塊通信,不知道其他模塊的內部工作情況。

原因:有效地解除各模塊之間的耦合關系

訪問控制機制(access control):決定類,接口和成員的可訪問性。由聲明的位置和訪問修飾符共同決定。

對於頂層的類和接口,兩種訪問級別:

  • 包級私有的(package-private)
  • 公有的(public)

對於成員(域,方法,嵌套類,嵌套接口),四種訪問級別:

  • 私有的
  • 包級私有的
  • 受保護的
  • 公有的

公有的或受保護的成員是類的導出API的一部分。代表了對某個實現細節的公開承諾。

實例域決不能是公有的。

通過公有final靜態域來暴露常量。

長度非零的數組總是可變的。類具有公有的靜態final數組域或者返回這種域的訪問方法,這幾乎是錯誤的。

總結:

盡可能地降低可訪問性。除了公有靜態final域表示常量的特殊情形外,公有類都不應該包含靜態域。並且要確保公有靜態final域所引用的對象都是不可變的。

在公有類中使用訪問方法而非公有域

//退化類
//沒有封裝
class Point{
    public double x;
    public double y;
}


//退化類應該被包含私有域和公有設值方法的類代替
class Point{
    //私有域
    private double x;
    private double y;
    
    public Point(double x,double y){
        this.x = x;
        this.y = y;
    }
    //公有的getter和setter方法
    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;
    }   
}


若類是包級私有的或是私有的嵌套類,直接暴露數據域並沒有本質的錯誤。

使可變性最小化

不可變類只是其實例不能被修改的類,實例的信息在創建的時候就提供並且固定不變。如String類就是不可變類。

使類成為不可變類的幾條原則:

  • 不提供修改狀態屬性的方法
  • 保證類不會被擴展
  • 使所有的域都是final的
  • 使所有的域都是私有的
  • 對於任何可變組件的互斥訪問

不可變對象本質上是線程安全的,不要求同步。

不可變對象可以被自由地共享。對於頻繁用到的值,提供公有的靜態final常量。以重用現有的實例。

不可變對象為其他對象提供了大量的構件。

注:我覺得下面一段中譯版翻譯有誤:

原文:[76頁第5段]you don't worry about their values changing once they're in the map or set,
which would destory the map or set's invariants.

譯文:一旦不可變對象進入到映射(map)或者集合(set)中,盡管這破壞了映射或者集合的不變性約束,但是也不用擔心它們的值會發生變化。

譯文翻譯得很生硬,破壞映射或集合的不變性約束指的應該是改變映射或集合的值而不是不可變對象進入映射或集合中。

修改后的譯文:若不可變對象進入映射或集合,不會破壞映射或集合的不變性約束(因為它們的值不會發生變化)。

不可變類的缺點:對於每個不同的值都需要一個單獨的對象。

多步操作,每步都會產生一個新的對象。除了最后的結果之外其他對象最終都會丟棄,這就會造成性能問題。

解決:

1>某個多步操作由基本類型提供

2>使用可變的配套類

常見的配套類有StringBuilder類,它是String類的配套類。

       //String
       String str = "";
       long stringStartTime = System.currentTimeMillis();
       for(int i = 0;i<1000000;i++){
        str+=i;
       } 
       long stringEndTime = System.currentTimeMillis();
       System.out.println("String:"+(stringEndTime-stringStartTime));

       //StringBuilder
       StringBuilder sb = new StringBuilder();
       long sbStartTime = System.currentTimeMillis();
       for(int i = 0;i<1000000;i++){
         sb.append(i);
       }
       long sbEndTime = System.currentTimeMillis();
       System.out.println("StringBuilder:"+(sbEndTime-sbStartTime));

運行結果:

上面的例子就是使用String類和使用StringBuilder類執行多步操作,可以看出使用StringBuilder比String類快很多很多。

我們使用Javap -c的命令來查看字節碼:

可以看到str+=i;實質上還是調用的StringBuilder,那照理說應該兩者所花費的時間一樣,為什么在這里差別這么大呢。

個人理解是因為,每次運行到str+=i;都會新創建一個String對象。
下面通過Debug調試來證明我的猜測。

下面這是String對象每次添加數字到末尾的GIF動畫,可看到每次str所指向的String對象的value值都不同。

而下面則是StringBuilder對象使用append方法添加數字的GIF動畫,可看到它的值並沒有變化。


確保類的不可變性,類本身不能被子類化:

  • 使類成為final的
  • 類所有的構造器私有的或包級私有的,添加公有靜態工廠方法來代替公有構造器。

不可變類的缺點:在特定情況下存在潛在的性能問題。

復合優於繼承

繼承打破了封裝性(子類依賴父類中特定功能的實現細節)

合理的使用繼承的情況:

  • 在包內使用
  • 父類專門為繼承為設計,並且有很好的文檔說明

只有當子類真正是父類的子類型時,才適合用繼承。

對於兩個類A和B,只有兩者之間存在"is-a"關系,類B才能拓展類A。

繼承機制會把父類API中的所有缺陷傳播到子類中,而復合允許設計新的API來隱藏這些缺陷。

復合(composition):不擴展現有的類,而是在新的類中增加一個私有域,引用現有類的一個實例。

轉發(fowarding):新類中的每個實例方法都可以調用被包含的現有類實例中對應的方法,並返回結果。

public class FowardSet<E> implements Set<E> {

    //引用現有類的實例
    private final Set<E> set;

    public FowardSet(Set<E> set){
        this.set = set;
    }


    /*
     *轉發方法
     */
    @Override
    public int size() {
        return set.size();
    }

    @Override
    public boolean isEmpty() {
        return set.isEmpty();
    }

    @Override
    public boolean contains(Object o) {
        return set.contains(o);
    }

    @NotNull
    @Override
    public Iterator<E> iterator() {
        return set.iterator();
    }

    @NotNull
    @Override
    public Object[] toArray() {
        return set.toArray();
    }

    @NotNull
    @Override
    public <T> T[] toArray(T[] a) {
        return set.toArray(a);
    }

    @Override
    public boolean add(E e) {
        return set.add(e);
    }

    @Override
    public boolean remove(Object o) {
        return set.remove(o);
    }

    @Override
    public boolean containsAll(Collection<?> c) {
        return set.containsAll(c);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        return set.addAll(c);
    }

    @Override
    public boolean retainAll(Collection<?> c) {
        return set.retainAll(c);
    }

    @Override
    public boolean removeAll(Collection<?> c) {
        return set.removeAll(c);
    }

    @Override
    public void clear() {
        set.clear();
    }

    @Override
    public boolean equals(Object obj) {
        return set.equals(obj);
    }

    @Override
    public String toString() {
        return set.toString();
    }

    @Override
    public int hashCode() {
        return set.hashCode();
    }
}

/*
 * 包裝類(wrapper class),采用裝飾者模式
 */
public class InstrumentedSet<E> extends FowardSet<E> {
    private int addCount=0;

    public InstrumentedSet(Set<E> set) {
        super(set);
    }

    @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;
    }
}


上面的例子中,FowardingSet是轉發類,也是被包裝類,而InstrumentedSet是包裝類,它采用的是裝飾者模式,而不是委托模式。

裝飾者模式

委托模式

為什么包裝類不適合用在回調框架中?

包裝類不適合用在回調框架(callback framework)中,會出現SELF問題

在回調框架中,對象把自身的引用傳遞給其他對象,用於后續的調用(回調)

SELF問題:被包裝的對象並不知道它外面的包裝對象,所以它傳遞一個指向自身的引用(this),回調時卻避開了外面的包裝對象。

接口優於抽象類

兩種機制用來定義允許多個實現的類型:接口和抽象類

區別:

  • 現有的類可以很容易被更新,以實現新的接口。
  • 接口是定義混合類型的理想選擇。
  • 接口允許我們構造非層次結構的類型框架。

抽象的骨架實現類(skelletal implementation class):把接口和抽象類的優點結合起來。接口負責定義類型,骨架實現類接管所有與接口實現相關的工作。

Java Collections中為每個重要的集合接口都提供了一個骨架實現。如AbstractCollection,AbstractSet,AbstractList,AbstractMap。

設計得當,骨架實現可以讓程序員很容易就提供我們自己的接口實現。

模擬多重繼承:實現了接口的類可以把對於接口方法的調用轉發到一個內部私有類的實例上

抽象類的演變比接口的演變容易多了:在后續的發行版本中,在抽象類中增加新的方法,始終可以增加具體的方法,它包含合理的默認實現。該抽象類的所有現有的實現都將提供這個新的方法。對於接口,這樣做不行。

接口是定義允許多個實現的類型的最佳途徑。當是否容易演變比靈活性和功能更為重要的時候,應該使用抽象類。

接口只用於定義類型

不應該使用接口來定義常量,可以使用枚舉類型或者是不可實例化的工具類來定義常量。

/**
 * 使用接口定義常量,不推薦使用
 * 細節的實現,會被泄露到導出到API中
 * 並且代表一種承諾,類為保證二進制兼容性,需要一直實現接口即使它不再需要使用這些常量了
 */
public interface MathConstants {
    
    static final double PI = 3.14159265;
    
}


/**
 * 使用枚舉類型定義常量
 */
public enum MathConstantsWithEnum {
       PI(3.14159265);
       private double value;
       private MathConstantsWithEnum(double value){
           this.value = value;
       }
}


/**
 * 使用不可實例化的幫助類來定義常量
 */
public class MathConstantsUtil {
    private MathConstantsUtil(){}
    public static final double PI = 3.14159265;
}


import static com.xyz.johntsai.effectivejava.MathConstantsUtil.*;
/**
 * 靜態導入機制,避免用類名來修飾常量名
 */
public class TestStaticImport {
    public static double getArea(double r){
        return PI*r*r;
    }
}

類層次優於標簽類

/**
 * 標簽類
 * 缺點:過於冗長,易錯,效率低下,不易拓展
 */
public class Figure {
    enum Shape{

        RECTANGLE,CIRCLE
    }

    final Shape shape;

    //用於三角形
    double width;
    double length;

    //用於圓形
    double radius;

    //三角形的構造器
    Figure(double length,double width){
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }

    Figure(double radius){
        shape = Shape.CIRCLE;
        this.radius  =radius;
    }

    double getArea(){
        switch (shape){
            case RECTANGLE:
                return this.length*this.width;
            case CIRCLE:
                return Math.PI*this.radius*this.radius;
            default:
                throw new AssertionError();
        }
    }
}

/**
 * 類層次代替標簽類
 */
 
 /**
  *Figure類是類層次的根
  */
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 width;
    final double length;

    Rectangle(int width,int length){
        this.length = length;
        this.width = width;
    }

    @Override
    double area() {
        return width*length;
    }
}

用函數對象表示策略

策略模式

Strategy pattern

        String [] array = {"a","aa"};
        //匿名內部類
        Arrays.sort(array, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o1.length()-o2.length();
            }
        });

        //JDK1.8 支持lambda
        Arrays.sort(array,(s1,s2)->s1.length()-s2.length());
        
        //多次重復使用 用私有的靜態成員類實現,並通過靜態final成員導出
        Arrays.sort(array,Host.STRING_COMPARATOR);
        
        
        class Host{
    private static class StrLengthComparator implements Comparator<String>,Serializable{

        @Override
        public int compare(String o1, String o2) {
            return o1.length()-o2.length();
        }
    }

    public static final Comparator<String> STRING_COMPARATOR = new StrLengthComparator();
}

優先考慮靜態成員類

什么時候使用嵌套類,局部類,匿名類和lambda表達式?

局部類:定義在代碼塊中(大括號里面有0個或多個語句),一般定義在方法內部

public class LocalClassExample {

    static String regularExpression = "[^0-9]";


    public static void validatePhoneNumber(String phoneNumber1,String phoneNumber2){

        final int length = 10;

        //局部類(Local class)
        class PhoneNumber{
            String formattedPhoneNumber = null;
            PhoneNumber(String phoneNumber){
                String currentNumber = phoneNumber.replaceAll(regularExpression,"");
                formattedPhoneNumber = currentNumber.length()==length?currentNumber:null;
            }
            public String getNumber(){
                return formattedPhoneNumber;
            }
        }

        PhoneNumber number1 = new PhoneNumber(phoneNumber1);
        PhoneNumber number2 = new PhoneNumber(phoneNumber2);

        if(number1.getNumber()==null)
            System.out.println("First number is invalid");
        else
            System.out.println("First number is"+number1.getNumber());
        if(number2.getNumber()==null)
            System.out.println("Second number is invalid");
        else
            System.out.println("Second number is"+number2.getNumber());
    }

    public static void main(String[] args) {
        validatePhoneNumber("1234567890","456-7890");
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM