注:本文是《Effective Java》學習的筆記。
剛看完《java編程思想》這本書。看到網上又推薦了一個叫《EffectiveJava》第三版的書就入手了。
這本書意在更有效的java編程。提供了多條不同使用時的建議。
一: 創建和銷燬對象(與創建型設計模式有關)
1.用靜態工廠方法代替構造器。
此處的靜態工廠方法與設計模式中的工廠方法不是一個。此處所指的是除了公有public的構造方法來創建對象的另一種創建對象的形式。
下面是靜態工廠方法與構造器的一些不同點、
(1)名稱清晰易懂。構造方法都是類名加參數這種。靜態工廠方法就是可以有方法名來起到理解的作用。這個作用也不是特別大。
(2)不必每次調用時都創建一個新的對象。像傳統的構造器是每次都創建一個新的實例。而這個靜態方法可以像單例模式那樣工作。即多次調用返回相同對象。
(3)靜態工廠方法可以返回原返回類型的任何子類型。
(4)靜態工廠方法返回的對象可以隨每次調用而發生變化。取決於靜態工廠方法的參數值。
(5)方法返回的對象鎖屬的類,在編寫包含該靜態工廠方法的類時可以不存在。就像上面那個demo似的。
Demo: 如果你想創建一個fruit類,這個fruit類有三個子類apple,orange,banana。這時你可以通過入參來創建對象而返回類型是fruit來保證你的程序不會出錯。這個入參可以是類名然後通過反射來返回實例。
tips"個人總結:這個靜態工廠方法在使用單例模式和繼承關係時常常被使用。比如要寫一套根據不同類型進行不同計算的服務,這就很好的能使用到工廠模式。
2.遇到多個構造器參數時要考慮使用構造器。
當有多個成員變量時,一個多參的構造器會很麻煩,這時通常使用JavaBean的方式setter創建對象時設置成員變量的值。但是使用setter時,就肯定不是一次的操作,此時可能會出現線程不安全。如果在方法裏面new對象會避免這種情況。因爲方法中的棧引用是不被多線程共享的。
還有一種創建方式是建造者模式Builder。這個在之前的Okhttp工具類中使用過。原理是通過靜態內部類不斷返回當前對象,最後創建一個類對象來實現隨意多參構造。下面是一個簡單的例子。
package com.zy.girl.effective;
public class ConBuilderFact {
private final int a;
private final int b;
private final int c;
public static class Builder {
private final int a;
private int b;
private int c;
public Builder(int a) {
this.a = a;
}
public Builder bCon(int b) {
this.b = b;
return this;
}
public Builder cCon(int c) {
this.c = c;
return this;
}
public ConBuilderFact build() {
return new ConBuilderFact(this);
}
}
public ConBuilderFact(Builder builder) {
this.a = builder.a;
this.b = builder.b;
this.c = builder.c;
}
public String toString(){
return "a="+a+"b="+b+"c="+c;
}
public static void main(String[] args){
ConBuilderFact con = new ConBuilderFact.Builder(1).bCon(2).build();
System.out.println(con);
}
}
tips"個人總結: 對於參數多的類可以使用Builder生成器模式來創建對象。像OkHttp這個Util,需要設置多個參數。這樣能更安全更好的賦參。因爲setter要注意線程安全的問題。當然setter是在方法中的new出來的對象處使用時沒問題的。方法中的對象是局部變量。而多個構造函數比較麻煩但是也是線程安全的。像po,vo這種類使用setter就好了。
3.用私有構造器或者枚舉類型來強化Singleton屬性。
singleton通常被用來作爲一個無狀態的對象。或者那些本質上唯一的系統組件。
對於單例模式有多種實現方式,要權衡線程安全與懶加載而衍生出不同的形式。需要私有化構造方法,以防止外部調用。
在使用單例模式時除了線程安全和懶加載需要考慮之外,還需要考慮反射和序列化會不會打破單例的平衡。
防止setAccessible()方法反射調用私有構造。需要在構造函數中創建第二個對象時拋異常。
public class Singleton {
private final static Singleton SINGLETON = new Singleton();
private Singleton(){}
public static Singleton getSingleton(){
return SINGLETON;
}
}
上述就在反射時打破了平衡,可以這麼改。當然下面只是一個簡單的寫法。
對於反序列化是不調用構造函數的。需要創建 readResolve()方法。在方法中返回這個SINGLETON來保證反序列化是也是那個實例。 最好的一種方法實現單例模式是單元素的枚舉。
public enum SingletonEnum {
INSTANCE;
public static SingletonEnum getInstance(){
return INSTANCE;
}
}
枚舉是完全不讓多次實例化的方法。
tips"個人總結:單例模式創建對象最好使用枚舉的方法。除了需要非Enum繼承的情況下使用其他方法。
4.通過私有構造器強化不可實例化的能力。
這一條是在編寫像Util,Collections這些工具類時保證不再外部被實例化。私有構造方法來實現。下面是Objects的源碼。
在設計時可以在構造方法中拋出一個異常,這樣可以保證外部不能通過反射調用,當然這不是必須的。因爲這並不像單例模式那樣敏感。他只是爲了內部不訪問。訪問沒有意義。訪問也沒啥影響。
tips"個人總結:以後編寫工具類時構造設成私有的就好了。
5.優先考慮依賴注入來引用資源。
通過構造器將需要的對象注入進去。
public class Inject {
private final Apple apple;
public Inject(Apple apple){
this.apple = Objects.requireNonNull(apple);
}
}
tips"個人總結:不要用單例或靜態工具類來實現依賴多個底層資源的類。而應該將這些類通過構造器傳入。像常使用的spring框架。這樣更加靈活可變。
6.避免創建不必要的對象
String s = new String("aaaa"); ------修改成 String s = "aaaa";
多次使用也使用的是方法區中的字符串常量不會每次都創建對象。
對於同時提供了靜態工廠方法和構造器的類要優先考慮使用工廠方法。因爲構造器每次調用只要不拋異常就會創建對象。而工廠方法在一定程度上編碼後會做到對象的複用。
對於昂貴的對象最後將他重用。在String的matches正則匹配的時候會創建一個Pattern對象。而這個對象只用了一次就垃圾回收了。這時候需要改成可複用的。
private static final Pattern pat = Pattern.compile("^sad$"); 類加載時候創建這個實例
pat.matcher(s).matches(); 返回bool判斷是否匹配。這樣每次調用時使用的是一個Pattern實例。所以這個String 實例的matches方法是很耗性能的。
對於自動裝箱也會很耗性能。對於sum定義成包裝類會很慢。定義成long基本類型會快好多倍。
對於像數據庫連接池這樣的對象可以使用數據庫連接池來重用,其他的小對象還是直接創建就行,因爲小對象開銷也不是很大。
7.消除過期的對象引用
對於不會再使用的對象要置爲null 。以棧爲例。
public class StackTest {
private Object[] elements;
private int size = 0;
public StackTest() {
elements = new Object[16];
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return elements[size--]; //此時只是返回但是位置處還是有元素存在,而這個元素是邏輯上被幹掉的。此時內存泄漏
}
public Object popChange() { //將pop的元素置null。修復了內存泄漏。
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[size--];
elements[size--] = null;
return result;
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
清空對象引用並不是一個規範的行爲。最好的方式是讓包含該引用的變量結束其生命週期。比如在一個方法中聲明的局部變量,方法返回時生命週期便結束了。
對於緩存可能會存在內存泄漏的問題。所以最好在緩存中設置引用存放的時間。常使用的是外部緩存像redis此時就把對象放到redis裏了,不需要考慮這個內存泄漏的問題。
內存泄漏的第三個常見來源是監聽器和其他回調。確保回調立即被當作垃圾回收的最佳方法就是隻保存他們的弱引用。例如使用WeakHashMap
tips:最好的方法就是藉助heap剖析工具。
8.避免使用終結方法和清除方法。
不要主動使用清除方法來垃圾回收清除對象。對於需要關閉的資源使用try-with-resources來關閉實現AutoCloseable的類。
9.使用try-with-resources 代替try-finally
這個try-with-resources是JDK1.7加入的。作用在資源的關閉。這個解決了try-finally塊異常問題。
如果try和finally都發生異常,肯定是拋finally中的異常。那try中的異常就會被覆蓋掉。
而使用try-with-resources出現了第一個異常,在發生異常就會被限制住,以做到只有一個異常,這樣爲改錯提供了方便。同時這個也使代碼更加簡潔。這個try包圍的()既是需要關閉資源的類,這裏面的類實現了AutoCloseable。此時不需要寫finally語句。
try(InputStream is = new FileInputStream("");
OutputStream os = new FileOutputStream("");
){
is.read();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
如下是這個try-with-resources 編譯後的代碼。
try {
InputStream is = new FileInputStream("");
Throwable var4 = null;
try {
OutputStream os = new FileOutputStream("");
Throwable var6 = null;
try {
is.read();
} catch (Throwable var33) {
var6 = var33;
throw var33;
} finally {
if (os != null) {
if (var6 != null) {
try {
os.close();
} catch (Throwable var32) {
var6.addSuppressed(var32);
}
} else {
os.close();
}
}
}
} catch (Throwable var35) {
var4 = var35;
throw var35;
} finally {
if (is != null) {
if (var4 != null) {
try {
is.close();
} catch (Throwable var31) {
var4.addSuppressed(var31);
}
} else {
is.close();
}
}
}
} catch (FileNotFoundException var37) {
var37.printStackTrace();
} catch (IOException var38) {
var38.printStackTrace();
}