Thinking in java 太厚了,我不想看,所以先拿EJ開坑。
Effective Java 和 Thinking in java都是java基礎評分超高的書,所以有必要看一看。
Effective Java這本書被java之父推薦,所以特意買了本正版。
我很希望10年前就擁有這本書。可能有人認為我不需要任何Java方面的書籍,但是我需要這本書。
——Java 之父 James Gosling
當然,這本書是2009年出版的,已經過去N多個年頭,所以帶著“批判”的眼光來瞻仰這本巨作。
ps:中文版的翻譯真是爛的可以,英文厲害的強烈推薦看?英文原版,另外,新手還是先多搬磚,或者去看看java核心技術上下兩卷,這本書不適合0基礎。
0x01 靜態工廠方法代替構造器
用靜態工廠方法代替構造器有什么好處呢?
- 靜態工廠方法有方法名稱,可以更確切的針對對象?進行不同的構造如構造素數和正整數,可以在同一個類里放兩個不同的工廠方法,起兩個有意義的名字
- 工廠方法不一定每次都會創建一個新對象,如果配合單例,會有更好的性能提升(這個視場景而定)
- 可以返回子類型的對象,具有更高的靈活性。
- 在創建?參數化實例時,代碼更加簡潔(這個書中使用了泛型作為例子,現在java已經有了類型推斷的能力,所以看情況咯)
//以前
Map<String,List<String>> m = new HashMap<String,List<String>>();
//現在
Map<String,List<String>> m = new HashMap<>();
//工廠
public static <K,V> HashMap<K,V> newIncetance(){
return new HashMap<K,V>();
}
當然工廠方法也有缺點
- 如果類中沒有public 或者protected的構造器,就不能子類化
- 工廠方法和其他靜態方法沒有區別,所以不能作為特殊的方法對待
0x02 構造器
上面提到的?工廠方法和類的構造函數對多個可選參數?時,劣勢就明顯出來了,一個方法中包含多個參數對調用方是非常不友好的,這時候可以考慮使用構造器(Builder)
放上書中的實例
// Builder Pattern
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val)
{ calories = val; return this; }
public Builder fat(int val)
{ fat = val; return this; }
public Builder sodium(int val)
{ sodium = val; return this; }
public Builder carbohydrate(int val)
{ carbohydrate = val; return this; }
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
這樣調用方就可以非常方便的構造出帶有不同屬性的對象了,這些屬性都是可選的,而且可讀性?非常好
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
但是Builder模式?也不是完美的,創建對象前需要先創建Builder對象,而且Builder代碼相對比較多,并不適合參數少的時候使用。但是一個類如果要考慮以后擴展屬性,最好一開始就使用Builder模式,因為?屬性越多,靜態工廠和構造函數就越難控制。
0x03 單例模式
單例模式一般面試被問到的可能性比較高,什么飽漢餓漢的區別,幾種單例模式的寫法,單例模式的好處自然是很多的,對只需要被實例化一次的類,最好使用單例模式
一般單例模式的類的構造器被私有化,并且加了一重或者兩重判斷來保障線程安全,并且有的寫法還在構造器中添加防止反射強制實例化的代碼。
關于單例模式的幾種寫法我就不寫了,網上有。
Effective Java上介紹的單例模式的代碼
// 單例模式靜態成員變量
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
// 靜態工廠方法
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public static Elvis getInstance() { return INSTANCE }
public void leaveTheBuilding() { ... }
}
// 枚舉單例模式
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
另外為了防止反序列化出假冒的單利的對象,書上說要加上這么一句。具體的牽扯到后面內容,坑以后填。
// readResolve method to preserve sigleton property
private Object readResolve() {
// Return the one true Elvis and let the garbage collector
// take care of the Elvis impersonator.
return INSTANCE;
}
0x04 私有構造器強化 禁止工具類被實例化
Math類,Arrays類這些工具類是不應該被實例化的,因為里面的方法都是靜態的。并且沒有實例化的意義,實例化反而會浪費內存。所以編寫這類Java類的時候,最好將構造器私有化。
當實例化這些類的時候,應該有異常拋出。
public class UtilityClass {
// Suppress default constructor for noninstantiability
private UtilityClass() {
throw new AssertionError();
}
... // Remainder omitted
}
0x05 不重復創建對象
書中建議相同功能的對象只需要創建一次就行了,不需要多次創建,另外要盡可能的重用對象,高效運用內存。
String s = new String("stringette"); // 別這么干!
上面的代碼會在常量池創建一個String,再在用構造器在堆里創建一個String,相當于兩次創建,最好是這樣的寫法
String s = "stringette";
另外不變的常亮最好事先加載,不要每次使用對象的時候重新創建。書中用了一個例子,判斷一個人是不是1946年至1964年生的
public class Person {
private final Date birthDate;
// 2B程序員的寫法
public boolean isBabyBoomer() {
// 沒有必要每次都創建Calendar對象
Calendar gmtCal =
Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
Date boomStart = gmtCal.getTime();
gmtCal.set(1964, Calendar.JANUARY, 1, 0, 0, 0);
Date boomEnd = gmtCal.getTime();
return birthDate.compareTo(boomStart) >= 0 &&
birthdate.compareTo(boomEnd) < 0;
}
}
很明顯,下面代碼會好很多,如果你看不出來,請回爐重學java
public class Person {
private final Date birthDate;
/**
* 正經程序員的寫法
*/
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(1964, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_END = gmtCal.getTime();
}
public boolean isBabyBoomer() {
return birthDate.compareTo(boomStart) >= 0 &&
birthdate.compareTo(boomEnd) < 0;
}
}
在這一節,還講到了拆裝箱對程序性能的影響
public static void main(String[] args) {
Long sum = 0;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
因為將long的第一個字母大寫了,導致程序慢了近40秒。
并不是所有創建對象的開銷都很大,但是重復創建是很浪費的,內存就那么大,CPU速度也有上限,無意義的拆裝箱浪費了性能。所以養成好習慣,節約內存。
另外書中說自己維護對象池是個費力不討好的事情,如果能交給GC,請務必交給GC。否則代碼后期維護將是一個很重量級的工作。
0x06 及時丟棄無用對象
首先看一段代碼,是一種棧的實方式
// 這里藏著一個內存泄漏的隱患
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
/**
* 確保至少有一個元素的可用空間,每次到達臨界時讓容量增加一倍。
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copayOf(elements, 2 * size + 1);
}
}
寫C/C++的程序員在對象失去作用的時候,會把對象置空。這個是好辦法,但是java里沒有必要這樣,所以程序員對釋放資源松懈了.
棧彈出元素后,需要解除對這個元素的引用,否則有可能會導致內存泄漏。
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 置空引用
return result;
}
java的緩存是一個內存泄漏的高發地段,因為緩存的對象會被遺忘,最后即使不用也放在緩存中,所以現在的帶緩存的功能都有一個時間機智,一段時間不用后,自動回收。
另外回調函數也有可能造成內存泄漏。如果一個對象注冊了回調,但是還沒等到回調這個對象就被干掉了,這時候,回調的時候就出現了內存泄漏問題。所以注冊回調的時候最好是弱引用。減少內存泄漏的概率
0x07 盡量別使用finalizer方法
書中說了很多,概括一下就是
- 使用這個方法不知道什么時候會被調用,甚至不會執行,容易造成內存泄漏
- 即使被調用了,不一定會讓你得到想要的結果,比如打印異常日志。
- 如果是多線程,分布式系統,容易導致系統崩潰。(線程被鎖住,宕機)
- 嚴重的性能損耗
所以能在對象回收前做完的,不要等到對象失去引用后再做!能不用finalize()方法就不用。
至于好處嘛,finalizer方法的確有兩個優點
當對象的所有者忘記調用前面段落中建議的顯式終止方法時,可以作為保險方法
另一種是對GC不知道的對象進行保險回收操作,比如Native Peer對象。
(FileInputStream
、FileOutputStream
、Timer
和Connection
),都具有終結方法,當它們的close方法未能被調用的情況下,終結方法提供了一層保障。
書上說:
總之,除非是作為安全網,或者是為了終止非關鍵的本地資源,否則請不要使用終結方法。在這些很少見的情況下,既然使用了終結方法,就要記住調用super.finalize。如果終結方法作為安全網,要記得記錄終結方法的非法用法。最后,如果需要把終結方法與公有的非final類關聯起來,請考慮使用終結方法守衛者,以確保即使子類的終結方法未能調用super.finalize,該終結方法也會被執行。
love & peace
轉載請注明出處:https://micorochio.github.io/2017/07/28/reading-effective-java-01/
如若有誤請幫忙指正,謝謝