Item 15 使類和成員的可訪問性最小化
1、一個設計的好的組件應該隱藏它的所有實現細節,清晰地將它的 API 以及具體實現分開。
2、盡可能的使用低級別的訪問級別,降低類和成員的可訪問性。
3、如果一個包私有的頂層類或接口只被一個類使用,考慮將它變為私有的靜態內部類。
3、設計類的時候首先設計你所有的公有 API,然后應該反射性的將其他剩余的類設為private,只有當它真的需要提高訪問級別時,才將它提升為包私有的。
4、protected的類是API的一部分,需要始終保證對它的支持,protected的級別在實際使用中是較少的。
5、如果一個方法覆蓋了超類的一個方法,不能使它的訪問級別比超類中的低。
6、在調試程序時,可以適當提高某些方法的訪問性。
7、public類的實體域通常是非public的。
8、一個類擁有public static final array 是錯誤的,作為替代選擇以下方法:
private static final Thing[] PRIVATE_VALUES = { ... }; public static final List<Thing> VALUES =Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
private static final Thing[] PRIVATE_VALUES = { ... }; public static final Thing[] values() { return PRIVATE_VALUES.clone(); }
9、在Java中,public不在是最高訪問級別,如果它不在導出包中,它將是模塊私有的。
10、public類不應該有public域,除非是充當常量的public static final域。
Item 16 在public類中 使用訪問方法而不是直接使用public域
// 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; } }
1、如果是包私有的或是靜態內部類或是不可變對象則不會有什么大問題。
2、總之、public類不要暴露可變對象,對於不可變對象是否可以暴露存疑,
Item 17 最小化可變性
1、為了確保類的不可變需要保證以下規則:
1)不要提供能改變類的狀態的方法。
2)確保類不能被繼承。
3)使所有域都是final的。
4)使所有域都是private的。
5)確保對於任何可變組件的互斥訪問。
2、例子:
1 // Immutable complex number class 2 public final class Complex { 3 private final double re; 4 private final double im; 5 public Complex(double re, double im) { 6 this.re = re; 7 this.im = im; 8 } 9 10 public double realPart() { return re; } 11 public double imaginaryPart() { return im; } 12 13 public Complex plus(Complex c) { 14 return new Complex(re + c.re, im + c.im); 15 } 16 17 public Complex minus(Complex c) { 18 return new Complex(re - c.re, im - c.im); 19 } 20 21 public Complex times(Complex c) { 22 return new Complex(re * c.re - im * c.im,re * c.im + im * c.re); 23 } 24 25 public Complex dividedBy(Complex c) { 26 double tmp = c.re * c.re + c.im * c.im; 27 return new Complex((re * c.re + im * c.im) / tmp,(im * c.re - re * c.im) / tmp); 28 } 29 30 @Override public boolean equals(Object o) { 31 if (o == this) 32 return true; 33 if (!(o instanceof Complex)) 34 return false; 35 Complex c = (Complex) o; 36 // See page 47 to find out why we use compare instead of == 37 return Double.compare(c.re, re) == 0&&Double.compare(c.im, im) == 0; 38 } 39 40 @Override public int hashCode() { 41 return 31 * Double.hashCode(re) + Double.hashCode(im); 42 } 43 44 @Override public String toString() { 45 return "(" + re + " + " + im + "i)"; 46 } 47 }
這里函數返回了一個新的對象,而不改變原有的操作數,稱為函數式編程。而過程式或是命令式編程則會改變操作數的狀態。
3、一個不可變類的狀態,就是它被剛創建時的類。
4、不可變類是線程安全的,不需要同步。
5、可以盡可能的自由的去復用不可變類。
6、使用靜態工廠使客戶復用對象而不是創建一個新的。
7、可以通過方法return一個不可變類的內部狀態進行復用。
8、不可變類的主要缺點,在於創建對象的開銷。
9、如果一個類的方法的多步操作是基本類型的,那么在中間步驟實際是不需要每次都創建一個新的對象的。
10、使一個類不能被繼承,可以使類的構造器private並添加public靜態工廠:
// Immutable class with static factories instead of constructors public class Complex { private final double re; private final double im; private Complex(double re, double im) { this.re = re; this.im = im; } public static Complex valueOf(double re, double im) { return new Complex(re, im); } ... // Remainder unchanged }
11、實際中亦可適當放松限制以提高性能。
12、對於一些常用的數據進行緩存。
13、如果一個包含可變類的不可變類需要實現Serializable,則必須提供readObject或者readResolve方法,否則會被攻擊者創建一個可變版本。
14、不要一看到getter就反射性的寫setter。
15、小類盡可能是不可變的的,相對的大類是可變的比較好。
16、盡可能使所有的域private final,除非有恰當的理由。
Item 18 比起繼承更支持復合
1、跨包繼承是一件危險的事。
2、與調用方法不同。繼承破壞了封裝性。
3、例子:
// Broken - Inappropriate use of inheritance! 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; } }
錯誤在於 addAll方法在內部實現是基於add的。子類的addAll計算了一次count,然后調用超類addAll,再去調用add,但是add方法已經在子類被覆蓋,其中又計算了一次。一次addAll方法對一個元素會計算兩次。
4、另一個子類脆弱的原因是,超類如果添加了一個新的方法,如果子類不進行相應的覆蓋可能會造成嚴重的安全隱患。
5、如果進行繼承不進行任何覆蓋,只是添加新的方法情況可能會沒有那么糟,但是仍有可能與將來版本超類中的新添加的函數相同而引發沖突。以及其他問題。
6、使用復合。在新的類中添加一個私有域,引用現有類的一個實例。新類中的每個方法都可以調用被包含的現有類實力中對應的方法,並返回它的結果。這種方法稱為轉發。這樣會使類很穩固。
1 // Wrapper class - uses composition in place of inheritance 2 public class InstrumentedSet<E> extends ForwardingSet<E> { 3 private int addCount = 0; 4 public InstrumentedSet(Set<E> s) { 5 super(s); 6 } 7 @Override public boolean add(E e) { 8 addCount++; 9 return super.add(e); 10 } 11 @Override public boolean addAll(Collection<? extends E> c) { 12 addCount += c.size(); 13 return super.addAll(c); 14 } 15 public int getAddCount() { 16 return addCount; 17 } 18 } 19 20 // Reusable forwarding class 21 public class ForwardingSet<E> implements Set<E> { 22 private final Set<E> s; 23 public ForwardingSet(Set<E> s) { this.s = s; } 24 public void clear() { s.clear(); } 25 public boolean contains(Object o) { return s.contains(o); } 26 public boolean isEmpty() { return s.isEmpty(); } 27 public int size() { return s.size(); } 28 public Iterator<E> iterator() { return s.iterator(); } 29 public boolean add(E e) { return s.add(e); } 30 public boolean remove(Object o) { return s.remove(o); } 31 public boolean containsAll(Collection<?> c){ return s.containsAll(c); } 32 public boolean addAll(Collection<? extends E> c){ return s.addAll(c); } 33 public boolean removeAll(Collection<?> c){ return s.removeAll(c); } 34 public boolean retainAll(Collection<?> c){ return s.retainAll(c); } 35 public Object[] toArray() { return s.toArray(); } 36 public <T> T[] toArray(T[] a) { return s.toArray(a); } 37 @Override public boolean equals(Object o){ return s.equals(o); } 38 @Override public int hashCode() { return s.hashCode(); } 39 @Override public String toString() { return s.toString(); } 40 }
這里通過轉發技術,都由私有域s進行處理,因此不會出現上述的問題。
7、包裝技術的缺點在於不支持回調框架。
8、每次使用繼承的時候都要慎重地再次確認是否是 “is-a”的關系,是否真的是子類型。
Item 19 設計被用於繼承的類時需要恰當的文檔說明
1、一個類必須通過文檔說明其中會被覆蓋的 自使用 方法。(也就是一個方法的實現依賴於同個類中的其他方法)。
2、慣例使用 Implementation Requirement 來說明方法的內部工作原理。
3、構造器中不能調用能夠被覆蓋的方法。否則會導致程序出錯。
4、如果設計的將要被繼承的類實現了 Cloneable 或者 Serializable。clone 和 readObject不能調用能被覆蓋的方法。
5、如果設計的將要被繼承的類實現了Serializable,必須把readResolve 或者writeReplace 設為protected而不是private。
6、禁止繼承的兩種方法一是使類final 二是構造器私有化並提供靜態工廠進行代替。
7、為了使更好的子類化,需要一個或多個protected方法。
Item 20 優先考慮接口而不是抽象類
1、兩者最主要的區別在於,子類只能繼承一個父類,卻能實現多個接口。
2、接口是理想的定義混入的辦法,比如Comparable 就被廣泛應用提供標准的比較功能。
3、接口不同於繼承的嚴格關系,可以定義非層次結構的框架。
4、通過包裝器,接口可以提供安全強力的功能增加。
5、考慮在接口中以默認方法的形式提供實現幫助。
6、不允許對Object的方法提供默認方法。
7、結合接口和抽象類的優點可以提供一個 抽象骨架實現類。
8、接口定義類型、提供默認方法。 骨架實現類實現 剩下的基於基本接口方法的 非基本方法。(模版方法模式)
9、管理上對骨架實現類命名為 AbstractInterface 其中Interface為接口名。
10、一個例子,一個靜態工廠方法包含一個 基於 AbstractList 的完整的List實現:
1 // Concrete implementation built atop skeletal implementation 2 static List<Integer> intArrayAsList(int[] a) { 3 Objects.requireNonNull(a); 4 // The diamond operator is only legal here in Java 9 and later 5 // If you're using an earlier release, specify <Integer> 6 return new AbstractList<>() { 7 @Override public Integer get(int i) { 8 return a[i]; // Autoboxing (Item 6) 9 } 10 @Override public Integer set(int i, Integer val) { 11 int oldVal = a[i]; 12 a[i] = val; // Auto-unboxing 13 return oldVal; // Autoboxing 14 } 15 @Override public int size() { 16 return a.length; 17 } 18 }; 19 }
11、即使因為某些原因一個類不能對抽象骨架實現類進行繼承,它也總可以對原接口進行實現。
12、實現了接口的類 可以將 接口方法的調用 傳遞給 它包含的 繼承了骨架實現的 私有內部類的實例。這種技術也被稱為模擬多重繼承。
13、寫一個抽象骨架實現類的步驟:
1)、認真研究接口,決定哪些方法是基本的,而其他方法基於這些基本方法實現。這些基本方法將是 骨架實現類中的抽象方法。
2)、對接口中所有可以被實現的其他方法提供默認方法。不要忘記繼承自Object的方法是例外。
3)、如果基本方法和默認方法包括了這個接口的一切,則就不需要編寫骨架實現類。
4)、否則寫一個骨架實現類實現找個接口,這個接口包含了一切剩余方法的實現,這個類可能包括任何合適的非公有域和方法。
例子:
// Skeletal implementation class 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(); } }
14、 因為骨架實現類用於繼承,因此如上文所述需要一個好的文檔說明。
15、他一個變化是簡單實現,不過他不是抽象的而是該接口最簡單的一種實現。
Item 21 為了后續實現設計接口
1、默認方法的聲明包括一個默認實現,所有實現這個接口,但不實現這個默認方法的類都將使用這個默認方法。
2、許多被加入核心借口的默認方法,都是為了幫助lambdas表達式的使用。
3、並不總是可能添加一個默認方法並保持,已經實現這個接口的類的內部的關系不變。
4、例子:
1 // Default method added to the Collection interface in Java 8 2 default boolean removeIf(Predicate<? super E> filter) { 3 Objects.requireNonNull(filter); 4 boolean result = false; 5 for (Iterator<E> it = iterator(); it.hasNext(); ) { 6 if (filter.test(it.next())) { 7 it.remove(); 8 result = true; 9 } 10 } 11 return result; 12 }
5、現階段添加默認方法,實現這個接口的類可能仍然可以正常編譯,但運行時可能發生錯誤。
6、 設計默認方法不支持用在接口中移除方法或改變現有方法的簽名。
7、除非非常必要,否則不要在已經設計完並被使用的接口中添加默認方法,這可能會使實現類產生不可預知的錯誤,盡量只在新設計的接口中使用這一技術。
Item 22 使用接口只用於定義類型
1、一個常見的錯誤例子:
1 // Constant interface antipattern - do not use! 2 public interface PhysicalConstants { 3 4 static final double AVOGADROS_NUMBER = 6.022_140_857e23; 5 static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23; 6 static final double ELECTRON_MASS = 9.109_383_56e-31; //_相當於,並不對數值有影響,只是更有可讀性。2 7 }
2、正確的方式:如果該數值與類或接口的關系很緊密,直接放入;如果最好被視為枚舉類型,應當視為枚舉類型。或者也可以使用工具類:
// Constant utility class package com.effectivejava.science; 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; }
3、如果經常使用這個數值,如上方式的調用可能非常繁瑣,需要輸入一長串字符,便於化簡可以使用靜態導入功能:
1 // Use of static import to avoid qualifying constants 2 import static com.effectivejava.science.PhysicalConstants.*; 3 public class Test { 4 double atoms(double mols) { 5 return AVOGADROS_NUMBER * mols; 6 } 7 ... 8 // Many more uses of PhysicalConstants justify static import 9 }
Item 23 類層次優先於標簽類
1、標簽類:
1 // Tagged class - vastly inferior to a class hierarchy! 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 Figure(double radius) { 13 shape = Shape.CIRCLE; 14 this.radius = radius; 15 } 16 // Constructor for rectangle 17 Figure(double length, double width) { 18 shape = Shape.RECTANGLE; 19 this.length = length; 20 this.width = width; 21 } 22 double area() { 23 switch(shape) { 24 case RECTANGLE: 25 return length * width; 26 case CIRCLE: 27 return Math.PI * (radius * radius); 28 default: 29 throw new AssertionError(shape); 30 } 31 } 32 }
缺點有許多:多重實現都擠在一個類中;內存增加了許多無用的東西;冗長的;易錯;效率低;只是類層次的拙劣的模仿。
2、將標簽類轉換為類層次:
1)定義一個抽象類,包含所有在標簽類中行為依賴標簽值的方法,往往包含switch,例如上例中的area().
2) 將所有不依賴於標簽值的方法和數值放入這個類。這個類即是根。
3)定義實體子類,新增各自的實現方法,以及實現父類中的抽象類,以對應不同標簽的實現。
// Class hierarchy replacement for a tagged class 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; } }
Item 24 相較於非靜態優先使用靜態成員類
1、一個嵌套類應該只被設計用語服務它的外圍類。
2、共有四種類型的嵌套類:靜態成員類、非靜態成員類、匿名類和局部類。前三種統稱為內部類。
3、靜態成員類的一種常見用法是作為公有的輔助類,用於幫助它的外部類。
4、非靜態成員類隱含着一個外圍類對象的實例 this。正是通過它才能訪問外圍類中的一切。
5、靜態內部類的實例可以不依附於外圍類的實例獨立存在,而非靜態則不行。
6、非靜態成員類的一種常見用法是適配器:
1 // Typical use of a nonstatic member class 2 public class MySet<E> extends AbstractSet<E> { 3 ... // Bulk of the class omitted 4 @Override public Iterator<E> iterator() { 5 return new MyIterator(); 6 } 7 private class MyIterator implements Iterator<E> { 8 ... 9 } 10 }
7、如果成員類不需要訪問它的外部對象,那它就應該是靜態的。
8、private靜態成員類的一種常見用法是代表外圍類所代表的對象的組件。
9、匿名類並不是外圍類的一個成員。
10、除了常量,匿名類不能擁有靜態成員。
11、只有在外圍類非靜態時,匿名類才能擁有外圍類的實例。
12、不能聲明匿名類實現多個接口或者繼承一個類並同時實現接口。
13、匿名類的客戶端無法調用任何成員除非是從其超類繼承獲得。
14、匿名類可以用於實現靜態工廠。
15、如今比起匿名類,lambdas表達式可能更好。
16、局部類可以在任何允許局部變量存在的地方被聲明。
17、局部類有名字可以復用, 只有外圍類非靜態時可以獲得外圍類的實例,不能包含靜態成員,盡可能保持短。
18、如果一個嵌套類不止被外圍類的一個方法所使用,應該使它成為成員類。如果每個成員類實例都需要其外圍對象的引用,那應該是非靜態;否則靜態。如果只有一個地方需要創建實例,並且已經預先定義了類型,使它成為匿名類;否則作為局部類。
Item 25 限制每個資源文件(.java)只擁有一個頂層類。
1、如果不這樣做會因為編譯的順序不同而產生不同的結果。
2、如果需要使用多個類,考慮使用靜態成員類。