2. 創(chuàng)建和銷毀對象
2.1 考慮用靜態(tài)方法代替構(gòu)造器
what
// 靜態(tài)方法
public static Boolean valueOf(boolean b){
return b ? Boolean.TRUE : Boolean.FALSE;
}
// demo1
Boolean a1 = new Boolean(true);
Boolean a2 = new Boolean(true);
Boolean b1 = Boolean.valueOf(true);
Boolean b2 = Boolean.valueOf(true);
if (a1 == a2){
System.out.println("==========1"); //NO
}
if (b1 == b2){
System.out.println("==========2"); //OK
}
// demo2
Map<String,List<String>> m = new HashMap<String,List<String>>();
Map<String,List<String>> m = HashMap.newInstance();
public static <K,V> HashMap<K,V> newInstance(){
return new HashMap<K,V>();
}
why
靜態(tài)方法相比構(gòu)造器優(yōu)點:
- 有名稱
- 不必每次調(diào)用時創(chuàng)建一個新的對象(此時可以用==代替equals凑兰,從而提升性能筝家,demo1)
- 可以返回原返回類型的任何子類型對象媳瞪,從而隱藏實現(xiàn)類使api更簡潔(服務(wù)提供者框架)
- 創(chuàng)建參數(shù)化類型實例時抒痒,利用類型推導(dǎo)(type inference)代碼更簡潔
缺點:
- 類如果沒有公有的或受保護(hù)的構(gòu)造器,就無法被子類化
2.2 使用構(gòu)建器代替多個參數(shù)的構(gòu)造器
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
private int calories = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) {
calories = val;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
}
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).build();
}
}
2.3 使用私有構(gòu)造器或枚舉強化Singleton屬性
what
Singleton指僅被實例化一次的類该默,常依賴代表那些本質(zhì)上唯一的組件案糙。
好的單例模式要做到:
- 防止反射,序列化送粱,高并發(fā)導(dǎo)致的單例失斖使蟆;
- 能便捷的切換成多例模式
HOW
2.4 通過私有構(gòu)造器強化不可實例化的能力
防止缺省的構(gòu)造器導(dǎo)致類仍可以實例化抗俄,可以主動實現(xiàn)個無參構(gòu)造器脆丁,并在其中返回異常,這樣做的缺點是導(dǎo)致該類無法子類化
2.5 避免創(chuàng)建不必要的對象
- 重用不可變對象
String s = "a"
代替String s = new String("a")
- 重用已知不會被修改的可變對象动雹,如date
class Person {
private final Date birthDate;
public Person(Date birthDate) {
// Defensive copy - see Item 39
this.birthDate = new Date(birthDate.getTime());
}
// Other fields, methods
/**
* The starting and ending dates of the baby boom.
*/
private static final Date BOOM_START;
private static final Date BOOM_END;
static {
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_START = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_END = gmtCal.getTime();
}
public boolean isBabyBoomer() {
return birthDate.compareTo(BOOM_START) >= 0
&& birthDate.compareTo(BOOM_END) < 0;
}
}
- 優(yōu)先使用基本類型槽卫,而不是裝箱類型
- 除非是重量級的大對象,如數(shù)據(jù)庫連接池胰蝠,不然沒必要去刻意維護(hù)個對象池來實現(xiàn)重用歼培,因為小對象的創(chuàng)建回收是廉價的震蒋,相比維護(hù)對象池而言(代碼亂,占用內(nèi)存)
2.6 清除過期的對象引用
為什么清除:內(nèi)存泄露躲庄,占用內(nèi)存查剖,導(dǎo)致機(jī)器性能越來越慢,甚至內(nèi)存溢出
清空對象引用應(yīng)該是一種例外噪窘,而不是規(guī)范行為笋庄,應(yīng)當(dāng)最小化變量的作用域。
哪些情況會導(dǎo)致過期引用
自己管理內(nèi)存的類
-
緩存:一旦把對象引用放到緩存里效览,就容易被遺忘无切。
只要緩存之外存在對某個鍵的引用,該鍵才有意義丐枉,考慮使用WeakHashMap實現(xiàn)該緩存
監(jiān)聽器和其他回調(diào):如注冊回調(diào)卻沒有顯式取消回調(diào)哆键。確保回調(diào)可以垃圾被垃圾回收的最佳方法是只保存弱引用(weak reference)瘦锹,例如保存成WeakHashMap的鍵
2.7 避免使用finalizer()
3. 對所有對象都通用的方法
4. 類和接口
4.1 使類和成員的可訪問性最小化
信息隱藏:模塊之間只通過他們的api通信籍嘹,一個模塊不需要知道其他模塊的實現(xiàn)細(xì)節(jié)
實例域不能是公有的,否則即放棄了對存儲在這個域中的值進(jìn)行限制的能力弯院,也因此包含公有可變域的類不是線程安全的
長度非零的數(shù)組總是可變的辱士,不要返回它或者將其聲明為公有的靜態(tài)final數(shù)組域。處理辦法是聲明成私有的听绳,然后返回不可變數(shù)組颂碘,或者數(shù)據(jù)的拷貝(clone())
4.2 在公有類中使用訪問方法而不是公有域
如果公有類暴露了數(shù)據(jù)域,那今后想改也晚了椅挣,應(yīng)該調(diào)用它的代碼已經(jīng)遍布客戶端
4.3 使可變性最小化
what
不可變類是其實例不能被修改的類头岔,實例中所有信息都在創(chuàng)建時提供,如String,基本類型的包裝類,BigInteger,BigDecimal.
不可變對象只有一個狀態(tài)鼠证,即被創(chuàng)建時的狀態(tài)
不可變對象是線程安全的峡竣,不需要同步,故可被自由的共享量九,進(jìn)而永遠(yuǎn)不需要保護(hù)性拷貝
不可變對象的缺點是每個不同的值都需要一個單獨的對象
How
為使類成為不可變适掰,遵循5條原則:
4.4 復(fù)合優(yōu)于繼承
繼承的缺點:
- 打破了封裝性,在子類的實現(xiàn)依賴父類的某個特性時會是個問題荠列,其功能可能隨父類發(fā)行版本的變化而失效类浪,如HashSet中addAll()是調(diào)用add()實現(xiàn)的,所以統(tǒng)計add()元素個數(shù)時弯予,只能重寫add()方法
- 把超類api中的缺陷傳遞到子類
導(dǎo)致問題的另一個原因戚宦,在超類在后續(xù)的發(fā)行方法中會新增加方法:
- 子類有方法與新加的方法簽名相同但返回類型不同,則編譯報錯
- 子類有方法與新加的方法簽名且返回類型相同锈嫩,其實現(xiàn)可能不會遵循該新方法的約定
包裝類(復(fù)合受楼,把現(xiàn)有的類變成新類的組件;新類方法中調(diào)用超類中對應(yīng)方法并返回其結(jié)果垦搬,稱為轉(zhuǎn)發(fā)方法)的缺點:
- 不適用在回調(diào)框架(callback framework),可能會導(dǎo)致SELF問題艳汽,因為被包裝起來的對象并不知道它外面的包裝對象
只有A IS B 時猴贰,才使用繼承
4.5 接口優(yōu)于抽象類
區(qū)別:
- 抽象類允許包含某些方法的實現(xiàn)而接口不允許;
- 為實現(xiàn)抽象類定義的類型河狐,必須成為抽象類的子類
抽象類的弊端:
- 破壞類層次米绕,某個類一旦實習(xí)抽象類,其所有后代都要擴(kuò)展這個新的抽象類馋艺,無論這個后代是否合適
- 不能Mixin栅干,接口可以實現(xiàn)多個,但類只能繼承一個
- 無法構(gòu)建非層次結(jié)構(gòu)的類型框架
抽象類優(yōu)點:
- 可以增加已實現(xiàn)的方法
骨架實現(xiàn)(skeletal implementtation):使用抽象類實現(xiàn)接口,亦被稱作abstractInterface捐祠,可以使程序員很容易提供自己的接口實現(xiàn),對于重要的接口碱鳞,最好提供對應(yīng)的骨架實現(xiàn)類。
通過對你導(dǎo)出的每個重要接口都提供一個抽象的骨架實現(xiàn)類(skeletal implementation)類踱蛀,把接口和抽象類的優(yōu)點結(jié)合起來窿给。接口的作用仍然是定義類型,但是骨架實現(xiàn)類接管了所有與接口實現(xiàn)相關(guān)的工作(可以只實現(xiàn)需要的方法)率拒。
// 應(yīng)用骨架類實現(xiàn)自己的List
public class IntArrays {
static List<Integer> intArrayAsList(final int[] a) {
if (a == null)
throw new NullPointerException();
return new AbstractList<Integer>() {
public Integer get(int i) {
return a[i]; // Autoboxing (Item 5)
}
@Override
public Integer set(int i, Integer val) {
int oldVal = a[i];
a[i] = val; // Auto-unboxing
return oldVal; // Autoboxing
}
public int size() {
return a.length;
}
};
}
public static void main(String[] args) {
int[] a = new int[10];
for (int i = 0; i < a.length; i++)
a[i] = i;
List<Integer> list = intArrayAsList(a);
Collections.shuffle(list);
System.out.println(list);
}
}
4.6 優(yōu)先考慮靜態(tài)成員類
嵌套類:
- 定義在另一個類內(nèi)部的類崩泡,目的在于為其外圍類服務(wù),如果其可能用在其他類猬膨,應(yīng)該是頂層類角撞。
- 分類:靜態(tài)成員類、內(nèi)部類(非靜態(tài)成員類勃痴,匿名類靴寂,局部類)
[圖片上傳失敗...(image-c90921-1553657651829)]
5. 泛型
5.1 不要在新代碼中使用原生態(tài)類型
每個泛型都會定義一個原生態(tài)類型(Raw Type),即不帶任何實際類型參數(shù)的泛型名稱召耘。例如,與List<String>對應(yīng)的原生態(tài)類型時List褐隆。原生態(tài)類型就像從類型聲明中刪除了所有泛型信息一樣污它。實際上,原生態(tài)類型List與java平臺沒有泛型之前的接口類型List完全一樣庶弃。
List<Object>是個參數(shù)化類型衫贬,表示可以包含任何對象類型的一個集合;List<?>是一個通配符類型歇攻,表示只能包含某種未知對象類型的一個集合固惯;List是原生態(tài)類型镇辉,它脫離泛型系統(tǒng)忽肛。前兩種是安全的汛骂,可以在編譯時就拋出錯誤图张,最后一種不可以适袜,它是不安全的
原因
- 無法在編譯時就發(fā)現(xiàn)類型錯誤
- 可讀性差
java之所以支持原生態(tài)類型是未來兼容疫萤。
5.2 消除非受檢警告
如果不是確定沒關(guān)系钓丰,不要置之不理梦鉴,其可能有潛在的CLassCastException風(fēng)險
5.3 列表優(yōu)于數(shù)組
數(shù)組是協(xié)變的娜庇,如果sub是super的子類型匕得,那sub[]就是super[]的子類型,總是在運行時才發(fā)現(xiàn)類型錯誤当娱;
Object[] objectArr = new Long[1];
objectArr[0] = "11";// 運行時報錯
List<Object> objectList = new List<Long>();
//無法通過編譯,因為List不是協(xié)變的溃槐,List<Long>不會成為List<Object>的子類型
數(shù)組是具體化(reified)的科吭,它會在運行時才檢查元素類型約束;而泛型通過擦除(crasure)實現(xiàn),只在編譯時強化類型信息,并在運行時丟棄類型信息(JVM中并沒有泛型)混萝。
總之,數(shù)組是協(xié)變且可具體化的,泛型是不可變且可擦除的雄坪。因此數(shù)組提供了運行時的類型安全绳姨,但沒有編譯時的類型安全,泛型則相反阔挠。所以如果你將泛型和數(shù)組混用飘庄,并在編譯時得到警告或錯誤,用列表代替數(shù)組购撼。
6. 枚舉和注解
6.1 用enum代替int常量
枚舉:通過公有的靜態(tài)final域為每個枚舉常量導(dǎo)出實例的類跪削,是真正的final。
優(yōu)點:
- 可讀性更好
- 實例受控迂求,是單例的泛型化碾盐,不會被擴(kuò)展或創(chuàng)建新的實例
- 編譯時的類型安全
- 有獨立的命名空間,所以不同枚舉類型可以有同名常量
- 可添加方法和域揩局,并實現(xiàn)接口
缺點:裝載和初始化枚舉時會占用空間和時間成本
應(yīng)用:
當(dāng)想給常量綁定對應(yīng)的行為時:
- 使用抽象方法毫玖,(防止新加的常量沒有重寫對應(yīng)的行為)枚舉中的抽象方法必須被它所有常量中的具體方法所覆蓋;
- 當(dāng)多個常量共享一個行為時,考慮策略枚舉(私有的嵌套枚舉類)
- 抽象方法可定義在接口中付枫,再由枚舉類實現(xiàn)之烹玉,提高擴(kuò)展性
// 常量綁定抽象方法
public enum Operation {
PLUS("+") {
double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {
double apply(double x, double y) {
return x - y;
}
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
abstract double apply(double x, double y);
// Test program to perform all operations on given operands
public static void main(String[] args) {
double x = Double.parseDouble("1");
double y = Double.parseDouble("1");
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
}
// 枚舉綁定行為
public enum Planet {
MERCURY(3.302e+23, 2.439e6), VENUS(4.869e+24, 6.052e6);
private final double mass; // In kilograms
private final double radius; // In meters
private final double surfaceGravity; // In m / s^2
// Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11;
// Constructor
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}
public double mass() {
return mass;
}
public double radius() {
return radius;
}
public double surfaceGravity() {
return surfaceGravity;
}
public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
public static void main(String[] args) {
double earthWeight = Double.parseDouble("1000");
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
}
}
// 策略枚舉
enum PayrollDay {
MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDNESDAY(
PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY), FRIDAY(PayType.WEEKDAY), SATURDAY(
PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) {
this.payType = payType;
}
double pay(double hoursWorked, double payRate) {
return payType.pay(hoursWorked, payRate);
}
// The strategy enum type
private enum PayType {
WEEKDAY {
double overtimePay(double hours, double payRate) {
return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT)
* payRate / 2;
}
},
WEEKEND {
double overtimePay(double hours, double payRate) {
return hours * payRate / 2;
}
};
private static final int HOURS_PER_SHIFT = 8;
abstract double overtimePay(double hrs, double payRate);
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
return basePay + overtimePay(hoursWorked, payRate);
}
}
public static void main(String[] args) {
System.out.println(PayrollDay.FRIDAY.pay(2,0.3));;
System.out.println(PayrollDay.SUNDAY.pay(2,0.3));;
}
}
7. 方法
7.1 檢查參數(shù)有效性
7.2 必要時進(jìn)行保護(hù)性拷貝
// 不安全
public Date start() {
return start;
}
public Date end() {
return end;
}
public Period(Date start, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(start + " after " + end);
this.start = start;
this.end = end;
}
// 攻擊1
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Modifies i
// 攻擊2
start = new Date();
end = new Date();
p = new Period(start, end);
p.end().setYear(78); // Modifies internals of p!
System.out.println(p);
// 安全
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(start +" after "+ end);
}
注意:
- 保護(hù)性拷貝是在參數(shù)有效性檢查之前進(jìn)行,因為有效性是針對檢查之后的對象
- 對參數(shù)類型可以被不可信任方子類化的參數(shù)阐滩,不要使用clone()進(jìn)行保護(hù)性拷貝二打。因為對于非final對象如Date,clone()方法無法保證返回java.util.Date對象掂榔,它可能返回的是惡意的子類實例
- 只要有可能继效,使用不可變對象作為對象組件,這樣就不需要保護(hù)性拷貝了
- 保護(hù)性拷貝可能會有性能損失衅疙,如果確定客戶端可以信任莲趣,則在文檔中指明客戶端不可修改受到影響的組件
7.3 關(guān)于方法簽名的設(shè)計
避免參數(shù)超過四個的三種方法:
- 拆成多個子方法
- 創(chuàng)建輔助類,保存參數(shù)的分組
- 使用Builder模式
對于參數(shù)類型饱溢,優(yōu)先使用接口而不是類喧伞,如用Map而不是HashMap;
8. 通用程序設(shè)計
8.1 需要精確的答案绩郎,不要使用float和double
System.out.println(1.03 - .42);//0.6100000000000001
System.out.println();
System.out.println(1.00 - 9 * .10);//0.09999999999999998
System.out.println();
使用BigDecimal,int或long進(jìn)行貨幣運算
需要十進(jìn)制小數(shù)點--BigDecimal
不超過9位十進(jìn)制數(shù)字--int
不超過18位十進(jìn)制數(shù)字--long
9. 異常
9.1 只針對異常情況使用異常
JVM在處理異常情況時可能性能會更低
9.2 對可恢復(fù)的情況使用受檢異常,對編譯錯誤使用運行時異常
9.3 盡量使用標(biāo)準(zhǔn)的異常
9.4 拋出與所在類層次對應(yīng)的異常
異常轉(zhuǎn)義:高層的實現(xiàn)應(yīng)該捕獲低層的異常潘鲫,同時拋出可以按照高層抽象進(jìn)行解釋的異常
9.5 在異常的構(gòu)造器中保存異常的細(xì)節(jié)信息
10. 并發(fā)
10.1 同步訪問共享的可變數(shù)據(jù)
正確的使用同步,可以保證沒有任何方法會看到對象處于不一致的狀態(tài)肋杖。
將可變數(shù)據(jù)限制在單個線程中溉仑,不然每個讀或者寫操作必須執(zhí)行同步
參考:Java并發(fā)編程:volatile關(guān)鍵字解析
10.2 避免過度同步
在一個被同步的區(qū)域內(nèi)部,不要調(diào)用那些外來的方法状植,如果被設(shè)計成要被覆蓋的方法浊竟,或者客戶端以函數(shù)對象形式提供的方法,因為你無法控制它們津畸。比如我在遍歷一個List振定,在同步的代碼塊內(nèi)是沒問題,但如果調(diào)用了一個外部方法肉拓,導(dǎo)致其回調(diào)刪除了List中的一個元素后频,就會報錯。
在一個被同步的區(qū)域內(nèi)部暖途,盡量少做事情卑惜,把耗時的操作移到外面去
10.3 executor和task優(yōu)于線程
現(xiàn)在關(guān)鍵的抽象不再是Thread,而是工作單元驻售,稱作任務(wù)(task)露久,有Runnable及其近親Callable兩種。執(zhí)行任務(wù)的通用機(jī)制是executor service欺栗。
executor framework 也有個替代java.util.Timer的機(jī)制抱环,即ScheduledThreadPoolExecutor .Timer是單線程的壳快,如果其唯一線程拋出未被捕獲的異常,timer就會停止運行镇草。而executor 支持多線程,并可從拋出為受檢異常的任務(wù)中恢復(fù)瘤旨。
10.4 并發(fā)工具優(yōu)于wait和notify
沒理由在新代碼中使用梯啤,如果不得不使用wait和notify:
- 始終應(yīng)該使用wait循環(huán)模式來調(diào)用wait方法(即要在while循環(huán)內(nèi)部調(diào)用,調(diào)用wait前后測試條件的成立與否)
- 一般用notifyAll而不是notify存哲,它可以喚醒所有需要被喚醒的線程因宇,非目標(biāo)的線程會在檢查等待條件后繼續(xù)等待。除非等待狀態(tài)的所有線程都在等待同一個條件祟偷,而每次只喚醒其中一個察滑。
10.5 線程安全的文檔化
當(dāng)一個類的實例或靜態(tài)方法被并發(fā)調(diào)用時,這個類的行為如何修肠,是該類與客戶端建立的約定的重要組成成分贺辰。
線程安全性是有多個級別的,一個類必須在文檔中清楚說明所支持的安全級別嵌施。
- 無條件線程安全類:必須把鎖對象私有化饲化,防止被客戶端訪問
- 有條件線程安全類:文檔中必須指明哪個調(diào)用序列需要外部同步,還要指明為了執(zhí)行這些序列吗伤,必須獲得哪些鎖
10.6 慎用延遲初始化
延遲初始化降低了初始化類或者創(chuàng)建實例的開銷吃靠,但增加了訪問被延遲初始化域的開銷。
要明確是否延遲足淆,唯一的辦法是測量類在用和不用延遲時的性能差別巢块。
如果多個線程會共享同一個延遲初始化的域,那必須做好同步:
- 對靜態(tài)域的初始化:
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
static FieldType getField() {
return FieldHolder.field;
}
- 對只初始化一次的實例域的初始化:
// 雙重檢查
private volatile FieldType field4;
FieldType getField4() {
FieldType result = field4;
if (result == null) { // First check (no locking)
synchronized (this) {
result = field4;
if (result == null) // Second check (with locking)
field4 = result = computeFieldValue();
}
}
return result;
}
- 對重復(fù)初始化的實例域的初始化:
// 單檢查模式巧号,如果放寬到每個線程可以也初始化一次實例族奢,并且實例是不可變對象,就刪去volatile
private volatile FieldType field5;
private FieldType getField5() {
FieldType result = field5;
if (result == null)
field5 = result = computeFieldValue();
return result;
}
10.7 不要依賴于線程調(diào)度器
線程調(diào)度器:當(dāng)有多個線程可以運行時裂逐,它決定哪些線程將會運行歹鱼,以及運行多長時間。不同的操作系統(tǒng)采用的策略可能大相徑庭卜高,依賴于線程調(diào)度器的程序得滤,很可能是不可移植的。
進(jìn)而不要依賴Thread.yield或者線程優(yōu)先級赏僧,這些措施僅僅是對調(diào)度器做些暗示匿情。可以使用線程優(yōu)先級提高已能正常工作的程序的質(zhì)量薪缆,但不能用來修正一個原本不能工作的程序秧廉。
要編寫健壯的伞广、響應(yīng)良好的、可移植的多線程程序疼电,最好的辦法是確苯莱可運行線程的平均數(shù)量不明顯多于處理器數(shù)量。注意蔽豺,可運行線程的平均數(shù)量不等于線程總數(shù)量区丑,因為等待的線程并不是可運行的。
10.8 避免使用線程組
11. 序列化
序列化:把一個對象編碼成字節(jié)流
反序列化:從字節(jié)流編碼中重新構(gòu)建對象
一旦對象序列化后修陡,就可以從一臺虛擬機(jī)傳遞到另一臺虛擬機(jī)上沧侥,或者存儲到磁盤,供后續(xù)反序列化使用魄鸦。
參考:
Effective Java--序列化--你以為只要實現(xiàn)Serializable接口就行了嗎
對Java Serializable(序列化)的理解和總結(jié)
11.1 謹(jǐn)慎實現(xiàn)Serializable接口
實現(xiàn)Serializable接口的缺點
1. 類被發(fā)布后宴杀,改變類的靈活性變小
如果一個類實現(xiàn)了Serializable接口,它的字節(jié)流編碼也變成了它導(dǎo)出API的一部分拾因,它的子類都等價于實現(xiàn)了序列化旺罢,以后如果想要改變這個類的內(nèi)部表示法,可能導(dǎo)致序列化形式不兼容盾致。
如果被序列化的類沒有顯示的指定serialVersionUID標(biāo)識(序列版本UID)主经,系統(tǒng)會自動根據(jù)這個類來調(diào)用一個復(fù)雜的運算過程生成該標(biāo)識。此標(biāo)識是根據(jù)類名稱庭惜、接口名稱罩驻、所有公有和受保護(hù)的成員名稱生成的一個64位的Hash字段,若我們改變了這些信息护赊,如增加一個方法惠遏,自動產(chǎn)生的序列版本UID就會發(fā)生變化,等價于客戶端用這個類的舊版本序列化一個類骏啰,而用新版本進(jìn)行反序列化节吮,從而導(dǎo)致程序失敗,類兼容性遭到破壞判耕。
2. 更容易引發(fā)Bug和安全漏洞
一般對象是由構(gòu)造器創(chuàng)建的透绩,而序列化也是一種對象創(chuàng)建機(jī)制,反序列化也可以構(gòu)造對象壁熄。由于反序列化機(jī)制中沒有顯式的構(gòu)造器帚豪,開發(fā)者一般很容易忽略它的存在。
構(gòu)造器創(chuàng)建對象有它的約束條件:不允許攻擊者訪問正在構(gòu)造過程中的對象內(nèi)部信息草丧,而用默認(rèn)的反序列化機(jī)制構(gòu)造對象過程中狸臣,很容易遭到非法訪問,使構(gòu)造出來的對象昌执,并不是原始對象烛亦,引發(fā)程序Bug和其他安全問題诈泼。
3. 隨著類發(fā)行新版本,相關(guān)測試負(fù)擔(dān)加重
當(dāng)一個可序列化的類被修改后煤禽,需要檢查“在新版中序列化一個實例铐达,在舊版本中反序列化”及“在舊版本中序列化一個實例,在新版本反序列化”是否正常檬果,當(dāng)發(fā)布版本增多時娶桦,這種測試量冪級增加。如果開發(fā)者早期進(jìn)行了良好的序列化設(shè)計汁汗,就可能不需要這些測試。
4. 開銷大
序列化對象時栗涂,不僅會序列化當(dāng)前對象本身知牌,還會對該對象引用的其他對象也進(jìn)行序列化。如果一個對象包含的成員變量是容器類等并深層引用時(對象是鏈表形式)斤程,此時序列化開銷會很大角寸,這時必須要采用其他一些手段處理。
無參的構(gòu)造器
若父類沒有實現(xiàn)Serializable忿墅,而子類需要序列化扁藕,需要父類有一個無參的構(gòu)造器,子類要負(fù)責(zé)序列化(反序列化)父類的域疚脐,子類要先序列化自身亿柑,再序列化父類的域。
至于為什么父類要有無參構(gòu)造器棍弄,因為父類沒有實現(xiàn)Serializable接口時望薄,虛擬機(jī)不會序列化父對象,而一個Java對象的構(gòu)造必須先有父對象呼畸,才有子對象痕支,反序列也是構(gòu)造對象的一種方法,所以反序列化時蛮原,為了構(gòu)造父對象卧须,只能調(diào)用父類的無參構(gòu)造函數(shù)作為默認(rèn)的父對象。
11.2 使用自定義的序列化形式
一個理想的序列化形式儒陨,應(yīng)該只包含該對象所表示的邏輯數(shù)據(jù)花嘶,而邏輯數(shù)據(jù)與物理表示法相互獨立。
若一個對象的物理表示法等同于它的邏輯內(nèi)容框全,則可能適合使用默認(rèn)的序列化形式察绷。一般而言,只有自定義的和默認(rèn)的形式基本相同津辩,才考慮使用默認(rèn)的拆撼。如一個表示人名的Name類容劳,從邏輯角度一個名字由姓和名組成,而Name中亦只有firstName和lastName兩個字段闸度,故是可以采用默認(rèn)形式的竭贩。
若一個對象的物理表示法與邏輯數(shù)據(jù)內(nèi)容有實質(zhì)性區(qū)別時,如下面的類:
public final class StringList implements Serializable {
private int size = 0;
private Entry head = null;
private static class Entry implements Serializable {
String data;
Entry next;
Entry previous;
}
}
該類邏輯上是一個字符串序列莺禁,但物理意義是雙向鏈表形式留量,使用默認(rèn)序列化有以下4個缺點:
a) 該類導(dǎo)出API被束縛在該類的內(nèi)部表示法上,鏈表類也變成了公有API的一部分哟冬,若將來內(nèi)部表示法發(fā)生變化楼熄,仍需要接受鏈表形式的輸入,并產(chǎn)生鏈?zhǔn)叫问降妮敵觥?br>
b) 消耗過多空間:像上面的例子浩峡,序列化既表示了鏈表中的每個項可岂,也表示了所有鏈表關(guān)系,而這是不必要的翰灾。這樣使序列化過于龐大缕粹,把它寫到磁盤中或網(wǎng)絡(luò)上發(fā)送都很慢;
c) 消耗過多時間:序列化邏輯并不了解對象圖的拓?fù)潢P(guān)系纸淮,所以它必須要經(jīng)過一個圖遍歷過程平斩。
d) 引起棧溢出:默認(rèn)的序列化過程要對對象圖執(zhí)行一遍遞歸遍歷,這樣的操作可能會引起棧溢出咽块。
對于StringList類绘面,可以用treansient修飾head和size變量控制其序列化,自定義writeObject,readObject進(jìn)行序列化糜芳。
具體改進(jìn)如下:
public final class StringList implements Serializable {
private transient int size = 0;
private transient Entry head = null;
//此類不再實現(xiàn)Serializable接口
private static class Entry {
String data;
Entry next;
Entry previous;
}
private final void add(String s) {
size++;
Entry entry = new Entry();
entry.data = s;
head.next = entry;
}
/**
* 自定義序列化
* @param s
* @throws IOException
*/
private void writeObject(ObjectOutputStream s) throws IOException{
s.defaultWriteObject();
s.writeInt(size);
for (Entry e = head; e != null; e = e.next) {
s.writeObject(e.data);
}
}
/**
* 自定義反序列化
* @param s
* @throws IOException
*/
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException{
s.defaultReadObject();
size = s.readInt();
for (Entry e = head; e != null; e = e.next) {
add((String) s.readObject());
}
}
}
(2) 如果對象狀態(tài)需要同步飒货,則對象序列化也需要同步
如果選擇使用了默認(rèn)序列化形式,就要使用下列的writeObject方法
private synchronized void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
}
如果把同步放在writeObject中峭竣,就必須確保它遵守與其他動作相同的鎖排列(lock-ordering)約束條件塘辅,否則由遭遇資源排列(resource-ordering)死鎖的危險
用來兩個private方法來實現(xiàn)自身序列化,這兩個函數(shù)為什么會被調(diào)用到皆撩?
writeObject: 用來處理對象的序列化扣墩,如果聲明該方法,它會被ObjectOutputStream調(diào)用扛吞,而不是默認(rèn)的序列化進(jìn)程呻惕;
readObject: 和writeObject相對應(yīng),用來處理對象的反序列化滥比。
ObjectOutputStream使用反射getPrivateMethod來尋找默認(rèn)序列化的類是否聲明了這兩個方法亚脆,所以這兩個方法必須聲明為private提供ObjectOutputStream使用。虛擬機(jī)會先試圖調(diào)用對象里的writeObject, readObject方法盲泛,進(jìn)行用戶自定義序列化和反序列化濒持,若沒有這樣的方法键耕,就會使用默認(rèn)的ObjectOutputSteam的defaultWriteObject及ObjectInputStream里的defaultReadObject方法。
關(guān)鍵字transient
(1) transient關(guān)鍵字作用是阻止變量的序列化柑营,在變量聲明前加上此關(guān)鍵字屈雄,在被反序列化時,transient的變量值被設(shè)為初始值官套,如int型是0, 對象型是null酒奶;
(2) transient關(guān)鍵字只能修飾變量,而不能修飾方法和類奶赔;
(3) 靜態(tài)變量不管是否被transient修飾惋嚎,均不能被序列化;
(4)defaultWriteObject被調(diào)用時站刑,未被標(biāo)記transient的實例域都會被序列化瘸彤,所以可以加transient的都加上
11.3 保護(hù)性地編寫readObject方法
readObject實際上相當(dāng)于另一個構(gòu)造器(不嚴(yán)格地說,用字節(jié)流作唯一參數(shù)的構(gòu)造器)笛钝,也需要檢查參數(shù)有效性,必要時作保護(hù)性拷貝愕宋。
如下面的類:
public final class Period implements Serializable {
private Date start;
private Date end;
public Period(Date start, Date end) {
this.start = new Date(start.getTime());//保護(hù)性拷貝
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException("start bigger end");
}
}
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
start = new Date(start.getTime());//保護(hù)性拷貝
end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException("start bigger end");
}
}
}
readObject方法不可以調(diào)用可以被覆蓋的方法玻靡,因為被覆蓋的方法將在子類的狀態(tài)被反序列化之前先運行,這樣程序很可能會crash.
11.4 對于實例控制中贝,枚舉類型優(yōu)先于readObsolve
采用readObsolve方法實現(xiàn)單例序列化
對于下面的單例
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { }
public static Elvis getINSTANCE() {
return INSTANCE;
}
}
通過序列化工具囤捻,可以將一個類的單例的實例對象寫到磁盤再讀回來,從而有效獲得一個實例邻寿。如果想要單例實現(xiàn)Serializable蝎土,任何readObject方法,不管顯示還是默認(rèn)的绣否,它會返回一個新建的實例誊涯,這個新建實例不同于該類初始化時創(chuàng)建的實例。從而導(dǎo)致單例獲取失敗蒜撮。但序列化工具可以讓開發(fā)人員通過readResolve來替換readObject中創(chuàng)建的實例暴构,即使構(gòu)造方法是私有的。在反序列化時段磨,新建對象上的readResolve方法會被調(diào)用取逾,返回的對象將會取代readObject中新建的對象。
具體方法是在類中添加如下方法就可以保證類的Singleton屬性:
//該方法忽略了被反序列化的對象苹支,只返回該類初始化時創(chuàng)建的那個Elvis實例
private Object readResolve() {
return INSTANCE;
}
由于Elvis實例的序列化形式不需要包含任何實際的數(shù)據(jù)砾隅,因此該類的所有的類成員(field)、帶有對象引用類型的實例域都應(yīng)該被transient修飾债蜜。
采用枚舉實現(xiàn)單例序列化
采用readResolve的一些缺點:
- readResolve的可訪問性需要控制好晴埂,否則很容易出問題究反。如果readResolve方法是受保護(hù)或是公有的,且子類沒有覆蓋它邑时,序列化的子類實例進(jìn)行反序列化時奴紧,就會產(chǎn)生一個超類實例,這時可能導(dǎo)致ClassCastException異常晶丘。
-
readResolve需要類的所有實例域都用transient來修飾,除非它們都是基本數(shù)據(jù)類型黍氮,否則可能被攻擊。
而將一個可序列化的實例受控類用枚舉實現(xiàn)浅浮,可以保證除了聲明的常量外沫浆,不會有別的實例。
所以如果一個單例需要序列化滚秩,最好用枚舉來實現(xiàn):
public enum Elvis implements Serializable {
INSTANCE;
private String[] favriteSongs = {"test", "abc"};//如果不是枚舉专执,需要將該變量用transient修飾
}
11.5 考慮用序列化代理代替序列化實例
public final class Period implements Serializable {
private final Date start;
private final Date end;
/**
* @param start
* the beginning of the period
* @param end
* the end of the period; must not precede start
* @throws IllegalArgumentException
* if start is after end
* @throws NullPointerException
* if start or end is null
*/
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(start + " after " + end);
}
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
public String toString() {
return start + " - " + end;
}
// Serialization proxy for Period class - page 312
private static class SerializationProxy implements Serializable {
private final Date start;
private final Date end;
SerializationProxy(Period p) {
this.start = p.start;
this.end = p.end;
}
private static final long serialVersionUID = 234098243823485285L; // Any
// number
// will
// do
// (Item
// 75)
// readResolve method for Period.SerializationProxy - Page 313
private Object readResolve() {
return new Period(start, end); // Uses public constructor
}
}
// writeReplace method for the serialization proxy pattern - page 312
private Object writeReplace() {
return new SerializationProxy(this);
}
// readObject method for the serialization proxy pattern - Page 313
private void readObject(ObjectInputStream stream)
throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
}
public class MutablePeriod {
// A period instance
public final Period period;
// period's start field, to which we shouldn't have access
public final Date start;
// period's end field, to which we shouldn't have access
public final Date end;
public MutablePeriod() {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
// Serialize a valid Period instance
out.writeObject(new Period(new Date(), new Date()));
/*
* Append rogue "previous object refs" for internal Date fields in
* Period. For details, see "Java Object Serialization
* Specification," Section 6.4.
*/
byte[] ref = { 0x71, 0, 0x7e, 0, 5 }; // Ref #5
bos.write(ref); // The start field
ref[4] = 4; // Ref # 4
bos.write(ref); // The end field
// Deserialize Period and "stolen" Date references
ObjectInputStream in = new ObjectInputStream(
new ByteArrayInputStream(bos.toByteArray()));
period = (Period) in.readObject();
start = (Date) in.readObject();
end = (Date) in.readObject();
} catch (Exception e) {
throw new AssertionError(e);
}
}
public static void main(String[] args) {
MutablePeriod mp = new MutablePeriod();
Period p = mp.period;
Date pEnd = mp.end;
// Let's turn back the clock
pEnd.setYear(78);
System.out.println(p);
// Bring back the 60s!
pEnd.setYear(69);
System.out.println(p);
}
}