重構:不可變物件
1. 什麼是不可變物件
不可變物件也稱之為值物件,《Effective Java》一書中給出這樣的定義:不可變物件是指每個物件中包含的所有資訊都必須在建立該物件時提供,並在物件的整個生命週期內固定不變。比如下面這段程式碼:
public class ImmutableObject {
private int value;
public ImmutableObject(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
}
由於ImmutableObject不提供任何setter方法,並且成員變數value是基本資料型別,getter方法返回的是value的拷貝,所以一旦ImmutableObject例項被建立後,該例項的狀態無法再進行更改,因此該類具備不可變性。
實際上Java平臺類庫中包含很多不可變的類,其中就有String
、基本型別的包裝類、BigInteger
和BigDecimal
等等。同時在日常工作中,我們也有很多地方可以使用不可變物件,比如:家庭地址類、座標類。還有Map
中的複合鍵。
2. 如何建立不可變物件
為了使類成為不可變,要遵循下面的幾條規則:
(1)所有的成員變數都必須是private
final
修飾。
(2)不提供修改原有物件狀態的方法,比如setter
方法。
(3)通過構造器初始化所有成員變數,引用型別的成員變數必須進行深拷貝。
(4)getter
方法不能對外洩露this引用以及成員變數的引用。
(5)保證類不會被擴充套件,一般做法是宣告這個類成為final
的。
(6)重寫equals
和hashCode
方法。
下面我們仿造BigDecimal
自己寫一個不可變物件類Complex
。程式碼如下:
public final class Complex {
private final double realPart;
private final double imaginaryPart;
public Complex(double realPart, double imaginaryPart) {
this.realPart = realPart;
this.imaginaryPart = imaginaryPart;
}
public double getRealPart() {
return realPart;
}
public double getImaginaryPart() {
return imaginaryPart;
}
public Complex plus(Complex complex) {
return new Complex(
getRealPart() + complex.getRealPart(),
getImaginaryPart() + complex.getImaginaryPart());
}
public Complex minus(Complex complex) {
return new Complex(
getRealPart() - complex.getRealPart(),
getImaginaryPart() - complex.getImaginaryPart());
}
public Complex times(Complex complex) {
double tempRealPart = getRealPart() * complex.getRealPart() - getImaginaryPart() * complex.getImaginaryPart();
double tempImaginaryPart = getRealPart() * complex.getImaginaryPart() + getImaginaryPart() * complex.getRealPart();
return new Complex(tempRealPart, tempImaginaryPart);
}
public Complex divided(Complex complex) {
double tempRealPart = getRealPart() * complex.getRealPart() + getImaginaryPart() * complex.getImaginaryPart();
double tempImaginaryPart = getImaginaryPart() * complex.getRealPart() - getRealPart() * complex.getImaginaryPart();
double temp = complex.getRealPart() * complex.getRealPart() + complex.getImaginaryPart() * complex.getImaginaryPart();
return new Complex(tempRealPart / temp, tempImaginaryPart / temp);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Complex complex = (Complex) o;
return Double.compare(complex.realPart, realPart) == 0 &&
Double.compare(complex.imaginaryPart, imaginaryPart) == 0;
}
@Override
public int hashCode() {
return Objects.hash(realPart, imaginaryPart);
}
@Override
public String toString() {
return "Complex{" +
"realPart=" + realPart +
", imaginaryPart=" + imaginaryPart +
'}';
}
}
這個類表示一個複數,除了提供標準的Object方法,它還提供了針對實部(realPart)和虛部(imaginaryPart)的訪問方法,以及4種基本的演算法運算:加法、減法、乘法和除法。需要注意的是這些算術運算都是返回一個新的Complex
物件,而不是修改原有物件。
3. 不可變物件的優缺點
3.1 優點
(1)不可變物件比較簡單:不可變物件只有一種狀態,即被建立時的狀態。
(2)不可變物件本質上是執行緒安全的,它們不要求同步。當多個執行緒併發訪問這樣的物件時,它們不會遭到破壞。
(3)不可變物件可以被自由地共享,我們應該充分利用這種優勢,鼓勵客戶端儘可能地重現現有的例項。而要做到這一點,一個很簡單的方法就是對於頻繁用到的值,為它們提供共有的靜態final
常量。BigDecimal
類中就有這樣的案例:
public static final BigDecimal ZERO = zeroThroughTen[0];
public static final BigDecimal ONE = zeroThroughTen[1];
3.2 缺點
(1)對於每個不同的值都需要一個單獨的物件,建立這些物件的代價可能很高,特別是大型的物件。