effectiveJava學習筆記:類和介面(一)
一、使類和成員的可訪問性最小化
對於類
對於類,只有public和package-private兩種訪問級別。package-private是預設的,也就是預設的。
1.對於頂層的類來說,只有包級私有和公有兩種可能,區別是包級私有意味著只能在當前包中使用,不會成為匯出api的一部分,而公有意味著匯出api,你有責任去永遠支援它。所以,為了使訪問最小化,能包級私有就應該宣告為包級私有。
2.對於包級私有類來說,如果只在某一個類中被使用,那麼就直接讓這個包級私有類成為這個類的巢狀類,這樣就能讓訪問級別再次縮小。
對於成員
成員包括域,方法,巢狀類和巢狀介面
訪問級別有私有的,包級私有的,受保護的和公有的四種。
1.例項域絕對不能是公有的,宣告例項域是公有的,相當於限制了對儲存在這個域中的值進行限制的能力,破壞了封裝性。
而靜態域 也只有在提供常量的抽象類中,通過公有的靜態final域來暴露。
Employee類新增一個例項域id和一個靜態域nextld:
class Employee
{
private int id;
private static int nextId=1;
}
常量的抽象類中通過公有的靜態final域來暴露 public final static int i = 1;
2. 設計類時,應當把所有的其他成員都變成私有的。
只有當同一個包中另一個類真正需要訪問一個成員的時候,才應該刪除private修飾符,把該成員變成包級私有的。
其實這兩者都是類的實現的一部分,不會影響到他的api。
3.如果對於公有類的成員,訪問級別從包級私有變成保護級別時,要額外小心,因為保護的成員是匯出api的一部分,必須得到永久支援。
4.方法覆蓋了超類中的一個方法,子類中的訪問級別就不允許低於父類的訪問級別。這個規則限制了方法的可訪問性的能力,保證可以使用超類的地方都可以使用到子類。
二、在公有類中使用訪問方法而非公有域
我們不能這樣做,
Class Point {
public double x;
public double y;
}
Point類的資料域是可以直接被訪問的,這樣的類沒有提供封裝。
1、如果不改變API,就無法改變它的資料表示法(比如,使用一個比double更高精度的類來表示x和y)
2、也無法強加任何約束條件(比如以後我們可能會希望x和y不會超過某個值)。
下面是我們推薦使用的方法。使用私有域和公有訪問方法的公有類是比較合適的。在它所在的包的外部訪問時,提供訪問方法,以保留將來改變該類的內部表示法的靈活性。
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; }
}
讓公有類暴露域不是好辦法,但如果域是不可變的,這種做法的危害會較小。
public final class Time {
private static final int HOURS_PER_DAY = 24;
private static final int MINUTES_PER_HOURS = 60;
public final int hour;// hour是不可變域
public final int minute;// minute是不可變域
public Time(int hour, int minute) {
// 無法改變類的表示法,但是可以強加約束條件
if (hour < 0 || hour > HOURS_PER_DAY)
throw new IllegalArgumentException("Hour: " + hour);
if (minute < 0 || minute >= MINUTES_PER_HOUR)
throw new IllIllegalArgumentException("Minute: " + minute);
this.hour = hour;
this.minute = minte;
}
}
總之。公有類永遠都不應該暴露可變的域。
雖然還是有問題,但是讓公有類暴露不可變的域其危害比較小。但是,有時候會需要用包級私有的或者私有的巢狀類來暴露域,無論這個類是可變還是不可變的。
三、使可變性最小化
不可變類是它的例項不能被修改的類。每個例項中所有的資訊,必須在建立的時候提供,並在其整個物件週期內固定不變。
為了使類成為不可變的,一般遵循以下幾個原則:
- 不要提供任何會修改物件狀態的方法(改變物件屬性的方法,也稱為mutator,也就是set方法)。
- 保證類不會被擴充套件。防止惡意的子類假裝物件的狀態已經改變,一般是將該類設定為final。
- 使所有的域都為final。通過系統的強制方法,清晰的表示你的意圖。
- 使所有的域都為private。防止通過繼承獲取訪問被域引用的可變物件的許可權,實際上用final修飾的public域也足夠滿足這個條件,但是不建議這麼做 ,為以後的版本的維護作考慮。
- 確保對於任何可變元件的互斥訪問。如果類具有指向可變物件的域,則必須確保該類的客戶端無法獲取指向這些物件的引用。並且, 永遠不要用客戶端提供的物件引用來初始化這樣的域,也不要從任何方法(access)中返回該物件的引用。在構造器中和訪問方法中,請使用保護性拷貝(defensive copy)。(保護性拷貝,比如在構造器中,需要傳遞某個物件進行初始化,那麼初始化的時候不要使用這個物件的引用,因為外部是可以修改這個引用中的資料的。因此初始化的時候,應該使用這個引用中的資料重新初始化這個物件,可參考39條)PS:沒怎麼看懂,以後再看。。。
下面給出一個關於不可變類的例子:
// Immutable class - pages 76-78
package effectiveJava.Chapter4.Item15;
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 static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);
// Accessory with no corresponding mutators
public double realPart() {
return re;
}
public double imaginaryPart() {
return im;
}
public Complex add(Complex c) {
return new Complex(re + c.re, im + c.im);
}
public Complex subtract(Complex c) {
return new Complex(re - c.re, im - c.im);
}
public Complex multiply(Complex c) {
return new Complex(re * c.re - im * c.im, re * c.im + im * c.re);
}
public Complex divide(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 43 to find out why we use compare instead of ==
return Double.compare(re, c.re) == 0 && Double.compare(im, c.im) == 0;
}
@Override
public int hashCode() {
int result = 17 + hashDouble(re);
result = 31 * result + hashDouble(im);
return result;
}
private int hashDouble(double val) {
long longBits = Double.doubleToLongBits(re);
return (int) (longBits ^ (longBits >>> 32));
}
@Override
public String toString() {
return "(" + re + " + " + im + "i)";
}
}
以上是一個關於複數的類,其中部分方法,如:加減乘除,可以返回新的物件,而不是修改當前的物件。很多不可變類都使用了這種方法,這是一種常見的函式式(functional)做法,因為它們返回的是函式的結果,對物件進行運算的結果,而不改變這些物件。
使用:
事實上,不可變物件非常簡單,它只有一種狀態,建立時的狀態。只要你在構造器中能保證這個類的約束關係,並遵守以上幾條原則,那麼在該物件的整個生命週期裡,永遠都不會再發生改變,維護人員也不需要過額外的時間來維護它。
另外,可變的物件擁有任意複雜的狀態空間。如果文件中沒有對其精確的描述,那麼要可靠的使用一個可變類是非常困難的。
由於不可變物件本身的特點,它本質上就是執行緒安全的,不需要對其進行同步。因為不可變物件的狀態永遠不發生改變,所以當多個執行緒同時訪問這個物件的時候,對其不會有任何影響。基於這一點,不可變類應該儘量鼓勵重用現有的例項,而不是new一個新的例項。方法之一就是,對於頻繁用到的值,使用public static final。如下:
public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);
不可變類還可以提供一些靜態工廠(見第1條),將頻繁請求的例項儲存起來,所有基本型別的包裝類和BigInteger都有這樣的工廠。靜態工廠還可以代替公有構造器,使得以後可以有新增快取的靈活性。
永遠不需要對不可變物件進行保護性拷貝,因為不可變物件內部的資料不可變,沒有保護性拷貝的必要。
不可變物件唯一的缺點是,對於每個不同的值,都需要一個單獨的物件。如果是一個大型物件,那麼建立這種物件的代價可能很高。如果你執行一個多步驟操作,然而除了最後的結果之外,其他的物件都被拋棄了,此時效能將會是一個比較大的問題。有兩種常用的方法:
- 將多步操作作為一個安全的基本操作提供,這樣就免除了中間的多步物件。
- 如果無法精準的預測客戶端將會在不可變的類上執行那些複雜的多步操作,不如提供一個公有的可變配套類。StringBuilder就是一個很好的例子。
奇妙的方法
除了使類成為final這種方法之外,還有另外一種更加靈活的方法來實現不可變類。讓類的所以構造器都變成私有的,並新增靜態工廠來代替公有的構造器。
package com.ligz.three;
/**
* @author ligz
*/
public class Complex {
private final double re;
private final double im;
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
public static Complex valueOf(double re, double im) {
return new Complex(re, im);
}
public static void main(String[] args) {
final Complex c= Complex.valueOf(1, 2);
}
}
這種方法最大的好處是它允許多個包級私有類的實現,並且在後續的維護中擴充套件靜態工廠的功能。例如,你想新增一個通過極座標生成複數的功能。如果通過構造器,可能會顯得非常凌亂,但是隻用新增第二個靜態工廠即可。
public static Complex valueOfPolar(double r, double theta) {
return new Complex(r * Math.cos(theta), r * Math.sin(theta));
}
總結
開頭所提到的幾個原則,其實強硬了,為了提高效能,往往有所放鬆。事實上應該是,沒有一個方法能夠對物件的狀態產生外部可見的改變(在初始化之後,自身還是可以改變自身內部的資料),許多不可變物件擁有一個或多個非final的域,在第一次請求的時候,將一些昂貴的計算結果快取在這些域裡,如果將來還有同樣的計算,直接將快取的資料返回即可。因為物件是不可變的,因此如果相同的計算再次執行,一定會返回相同的結果。例如,延遲初始化(lazy initialization)就是一個很好的例子。
總之,不要為每個get方法都寫一個set方法,除非有必要。換句話說,除非有很好的理由讓類成為可變的類,否則就應該是不可變。儘管它存在一些效能問題,但是你總可以找到讓一些較小的類成為不可變類。
構造器應該建立完整的構造方法,並建立起所有的關係約束。不應該在構造器或者靜態工廠外,再提供公有的初始化方法,或者是重新初始化方法。