Effective Java——創建和銷燬對象

代碼首先是給人看的,所以寫代碼要有寫詩一樣的感覺(哈哈雷軍說的),無論是寫邏輯、中間件或是寫框架都是如此。
想要寫好代碼首先基礎一定要好,所以我最近重新看了Effective Java,雖然這已經不是第一次看了但是還是有很多可以學習的地方。
本系列文章是總結Effective Java文章中我認爲最重點的內容,給很多沒時間看書的朋友以最短的時間看到這本書的精華。

第二章創建和銷燬對象

第1條:考慮用靜態方法代替構造器

優勢

第一大優勢在於,他們有名稱。

這個很好理解,他主要解決的是構造方法的重載問題,如下代碼:

public class CustomDialog {
    //構造一個有標題,有內容,有兩個按鈕的Dialog
    public CustomDialog(String title,
                        String msg,
                        String leftButton,
                        String rightButton,
                        Object leftOnClickListener,
                        Object rightOnClickListener) {
    }
    //構造一個無標題,有內容,有兩個按鈕的Dialog
    public CustomDialog(
            String msg,
            String leftButton,
            String rightButton,
            Object leftOnClickListener,
            Object rightOnClickListener) {
        this(null, msg, leftButton, rightButton, leftOnClickListener, rightOnClickListener);
    }
    //構造一個有標題,有內容,有一個按鈕的Dialog
    public CustomDialog(String title,
                        String msg,
                        String leftButton,
                        Object leftOnClickListener) {
        this(title, msg, leftButton, null, leftOnClickListener, null);
    }
}

如果程序中的公共組件這麼寫,那麼調用這個Dialog組件將是非常麻煩的一件事,每次創建的時候都要看文檔,或者點進去看源碼非常不直觀,如果將代碼修改成如下方式:

public class CustomDialog {
    //構造一個有標題,有內容,有兩個按鈕的Dialog
    public static CustomDialog createCustomDialog(){
        return null;
    }
    //構造一個無標題,有內容,有兩個按鈕的Dialog
    public static CustomDialog createNoTitleCustomDialog(){
        return null;
    }
    //構造一個有標題,有內容,有一個按鈕的Dialog
    public static CustomDialog createSingleButtonCustomDialog(){
        return null;
    }
}
//使用測試代碼
public class MainActivity extends Activity {
    private CustomDialog customDialog;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //很直觀的展示了你創建的Dialog的形式
        customDialog = CustomDialog.createCustomDialog();
        customDialog = CustomDialog.createNoTitleCustomDialog();
        customDialog = CustomDialog.createSingleButtonCustomDialog();
    }

這種靜態方法因爲他有名字,所以創建對象實例的時候非常直觀。查看代碼的人也很清楚就可以看到你創建的Dialog是什麼形式的,是否有標題,是否是單個Button,不用查看文檔或者進入源碼進行查看。

第二大優勢在於,不必在每次調用他們的時候創建一個新的對象。

主要應用於單例模式,如下代碼:

public class SingletonClass {
        private static volatile SingletonClass instance = null;
        public static SingletonClass getInstance() {
            if (null == instance)
                synchronized (SingletonClass.class) {
                    if (null == instance) {
                        instance = new SingletonClass();
                    }
                }
            return instance;
        }
        private SingletonClass() {}
    }
第三大優勢在於,它們可以返回原返回類型的任何子類型的對象。

面向接口編程,創建對象的方法返回接口,如下代碼:

//Mock本地測試數據的類
public class MockDataRespsitoryManager implements IDataRepositoryManager {
    ......
}
//訪問網絡的管理類
public class DataRepositoryManager implements IDataRepositoryManager {
    ......
}
@Singleton
@Module
public class DataRepositoryModule {
     ......
    @Singleton
    @Provides
    public IDataRepositoryManager providerRepositoryManager(Retrofit retrofit, RxCache rxCache) {
        return new DataRepositoryManager(retrofit, rxCache);
    }
    @Singleton
    @Provides
    @MockData
    public IDataRepositoryManager providerMockRepositoryManager(Application application, @Nullable DataRepositoryModule.MockDataConfig mockDataConfig) {
        return new MockDataRespsitoryManager(application,mockDataConfig);
    }
    ......
}
//構造方法傳入的是上兩個方法返回的對象
public class BaseModel {
    protected IDataRepositoryManager repositoryManager;
    public BaseModel(IDataRepositoryManager repositoryManager){
        this.repositoryManager = repositoryManager;
    }
    public void onDestory(){
        this.repositoryManager = null;
    }
}

如上代碼片段BaseModel類的構造方法在正常情況下接收providerRepositoryManager返回的對象鏈接網絡獲取數據,開發前期接收providerMockRepositoryManager返回的對象從本地讀取Mock數據進行調試。

第四大優勢在於,在創建參數化實例的時候,他們使代碼變得更加簡潔。

主要用在創建泛型對象,如下代碼:

public class AClass{
        public static class BClass{
            public static class CClass{
                public static class DClass{}
            }
        }
    }
//普通創建Map集合實例,非常冗長。
Map<AClass.BClass.CClass.DClass,AClass.BClass.CClass.DClass> map = new HashMap<AClass.BClass.CClass.DClass, AClass.BClass.CClass.DClass>();

//應用靜態工廠方法
public static <K,V> HashMap<K,V> newInstance(){
        return new HashMap<K,V>();
}
//應用靜態工廠方法創建泛型對象代碼簡潔了很多。
Map<AClass.BClass.CClass.DClass,AClass.BClass.CClass.DClass> map = newInstance();

缺點

主要缺點在於,類如果不含公有的或者受保護的構造器,就不能被子類化。

還是拿單例模式來舉例子:

public class SingletonClass {
        private static volatile SingletonClass instance = null;
        public static SingletonClass getInstance() {
            if (null == instance)
                synchronized (SingletonClass.class) {
                    if (null == instance) {
                        instance = new SingletonClass();
                    }
                }
            return instance;
        }
        //構造方法是private不是public也不是protected,所以單例不能實現繼承
        private SingletonClass() {}
    }
第二個缺點在於,它們與其他的靜態方法實際上沒有任何區別。

創建對象靜態方法和其他的靜態方法沒有任何區別,所以如果命名不規範,使用者很難找到創建對象的靜態方法。
命名規則例如:

public static Object createXXXX(){
        return null;
}
public static Object factoryXXXX(){
        return null;
}
public static Object newInstance(){
        return null;
}

上面代碼片段只是一種假設,每個項目的命名規範不一樣。只要遵循同一套規範就會使代碼清晰可讀。

第2條:遇到多個構造參數時要考慮用建造器

建造者模式:是將一個複雜的對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。
概念性的東西真是不好理解,簡單解釋爲什麼要用建造者模式,如果創建一個對象需要很多參數,有必傳參數有非必傳參數最好用建造者模式。
如下代碼片段:
代碼源碼點擊這裏
代碼涉及到dagger2知識點擊這裏查看簡介

@Singleton
@Module
public class AppDelegateConfig {

    private final String baseUrl;
    private final File cacheDir;
    private final RetrofitConfig retrofitConfig;
    private final DataRepositoryModule.OkhttpConfig okhttpConfig;
    private final DataRepositoryModule.RxCacheConfig rxCacheConfig;
    private final DataRepositoryModule.MockDataConfig mockDataConfig;
    private final IHttpErrorHandler iHttpErrorHandler;
    private final IHttpResponseHandler iHttpResponseHandler;

    private AppDelegateConfig(Builder builder){
        baseUrl = builder.baseUrl;
        cacheDir = builder.cacheDir;
        retrofitConfig = builder.retrofitConfig;
        okhttpConfig = builder.okhttpConfig;
        rxCacheConfig = builder.rxCacheConfig;
        mockDataConfig = builder.mockDataConfig;
        iHttpErrorHandler = builder.iHttpErrorHandler;
        iHttpResponseHandler = builder.iHttpResponseHandler;
    }
    @Singleton
    @Provides
    public String providerBaseUrl() {
        return baseUrl;
    }
    @Singleton
    @Provides
    public File providerCacheDir() {
        return cacheDir;
    }
    @Singleton
    @Provides
    @Nullable
    public RetrofitConfig providerRetrofitConfig() {
        return retrofitConfig;
    }
    @Singleton
    @Provides
    @Nullable
    public DataRepositoryModule.OkhttpConfig providerOkhttpConfig() {
        return okhttpConfig;
    }
    @Singleton
    @Provides
    @Nullable
    public DataRepositoryModule.RxCacheConfig providerRxCacheConfig() {
        return rxCacheConfig;
    }
    @Singleton
    @Provides
    @Nullable
    public DataRepositoryModule.MockDataConfig providerMockDataConfig(){
        if(null == mockDataConfig){
             return new DefaultMockDataConfig();
        }
        return mockDataConfig;
    }
    @Singleton
    @Provides
    public IHttpErrorHandler providerIHttpErrorHandler(Application application) {
        if(null == iHttpErrorHandler){
            return new DefaultHttpErrorHandler(application);
        }
        return iHttpErrorHandler;
    }
    @Singleton
    @Provides
    public IHttpResponseHandler providerIHttpResponseHandler() {
        if(null == iHttpResponseHandler){
            return new DefaultHttpResponseHandler();
        }
        return iHttpResponseHandler;
    }

    public static class Builder{

        private String baseUrl;
        private File cacheDir;
        private RetrofitConfig retrofitConfig;
        private DataRepositoryModule.OkhttpConfig okhttpConfig;
        private DataRepositoryModule.RxCacheConfig rxCacheConfig;
        private DataRepositoryModule.MockDataConfig mockDataConfig;
        private IHttpErrorHandler iHttpErrorHandler;
        private IHttpResponseHandler iHttpResponseHandler;

        public Builder(String baseUrl, File cacheDir){
            this.baseUrl = baseUrl;
            this.cacheDir = cacheDir;
        }
        public Builder setRetrofitConfig(RetrofitConfig retrofitConfig) {
            this.retrofitConfig = retrofitConfig;
            return this;
        }
        public Builder setOkhttpConfig(DataRepositoryModule.OkhttpConfig okhttpConfig) {
            this.okhttpConfig = okhttpConfig;
            return this;
        }
        public Builder setRxCacheConfig(DataRepositoryModule.RxCacheConfig rxCacheConfig) {
            this.rxCacheConfig = rxCacheConfig;
            return this;
        }
        public Builder setIHttpErrorHandler(IHttpErrorHandler iHttpErrorHandler){
            this.iHttpErrorHandler = iHttpErrorHandler;
            return this;
        }

        public Builder setiHttpResponseHandler(IHttpResponseHandler iHttpResponseHandler) {
            this.iHttpResponseHandler = iHttpResponseHandler;
            return this;
        }
        public Builder setMockDataConfig(DataRepositoryModule.MockDataConfig mockDataConfig) {
            this.mockDataConfig = mockDataConfig;
            return this;
        }
        public AppDelegateConfig builder(){
            return new AppDelegateConfig(this);
        }
    }
}

AppDelegateConfig appDelegateConfig = new AppDelegateConfig
                .Builder("baseUrl", new File("cacheDir"))
                .setOkhttpConfig(...)
                .setMockDataConfig(...)
                .setIHttpErrorHandler(...)
                .setiHttpResponseHandler(...)
                .setRetrofitConfig(...)
                .setRxCacheConfig(...)
                .builder();

如上代碼片段構建AppDelegateConfig類實例需要8個參數其中baseUrlcacheDir是必傳參數。
如果直接用構造器傳遞參數的方式來創建實例弊端是:

  1. 構造器參數冗長而且不直觀必須參照着源碼或者文檔來填寫參數,而且容易出錯,假如參數之間String類型較多容易填錯 。
  2. 在衆多參數中,必傳參數不明顯。

建造者模式很容易的解決了這兩個弊端:

  1. 參數通過單獨的方法進行設置,方法的名字描述了該參數的意思非常直觀例如setOkhttpConfig設置Okhttp配置。不需要的參數不調用setXXX方法保證了代碼的整潔。由於每個參數都是一個方法來配置,也不容易出錯。
  2. 必傳參數通過Builder建造者的構造方法傳入,保證必須設置。

第3條:用私有構造器或者枚舉類型強化Singleton屬性

1.單例模式要將構造方法設置成私有的private,代碼在上文中已經多次提到這裏就不再贅述了。
2.通過枚舉來實現單例模式,如下代碼:

public enum Singleton {
      //定義一個枚舉的元素,它就是 Singleton 的一個實例
     INSTANCE;  
     public void doSomeThing() {  
         // do something...
     }  
 }

1)線程安全
2)防止序列化
3)防止反射攻擊
這篇文章分析的很透徹:請點擊

第4條:通過私有構造器強化不可實例的能力

主要說的就是工具類(utility class),例如jdk自帶的java.util.Arraysjava.util.Collections等他們類中全都是靜態方法和靜態常量,實例化和子類化對他們一點意義都沒有,所以通過私有化構造方法來控制他們不能被實例化或子類化。

public class Arrays {
    // Suppresses default constructor, ensuring non-instantiability.
    private Arrays() {}
    ......
}

public class Collections {
    // Suppresses default constructor, ensuring non-instantiability.
    private Collections() {
    }
    ......
}

第5條:避免創建不必要的對象

防止創建不必要的對象,浪費內存也減慢了程序的執行速度。
介紹了幾個常見的例子:

  1. 創建字符串
String s = new String("stringette");
//應該優化成
String s = "stringette";

第一行代碼每次執行的時候都創建一個新的String實例,非常沒有必要。
第二行代碼通過虛擬機進行優化,在同一臺虛擬機中運行代碼,只要他們包含相同的字符串字面常量,該對象實例就不會重新創建會被重用。

  1. DateUtils 工具類
public class DateUtils {
    private DateUtils(){}
    //根據pattern來格式化輸入的millis
    public static String dateFormat(long millis, String pattern) {
        //假如這個工具類用來列表中,這個對象會非常頻繁的創建,造內存泄漏,程序運行減慢
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern);
        Date date = new Date(millis);
        String dateString = simpleDateFormat.format(date);
        return dateString;
    }
}
//改進版的時間工具類
public class DateUtils {
    //靜態對象只在類加載的時候創建一次,有效的解決了上面頻繁創建對象造成的內存泄漏問題
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat();
    private DateUtils(){}
    //根據pattern來格式化輸入的millis
    public static String dateFormat(long millis, String pattern) {
        simpleDateFormat.applyPattern(pattern);
        Date date = new Date(millis);
        String dateString = simpleDateFormat.format(date);
        return dateString;
    }
}
  1. 自動裝箱
public class TestClass {
        public static void main(String[] args) {
            Long sum = 0L;
            for (long i = 0; i < Integer.MAX_VALUE; i++) {
                sum += i;
            }
            System.out.println(sum);
        }
    }

這段代碼的結果是沒有問題的,但是由於Java的自動裝箱機制,創造出了大約Integer.MAX_VALUE個多餘的Long實例,造成內存泄漏,程序運行減慢。
所以要優先使用基本類型進行計算,要當心無意識的自動裝箱。

第6條:消除過期的對象引用

只要類是自己管理內存,程序員就應該警惕內存泄漏問題。
Java 內存回收就不再這篇文章進行討論,簡單的理解是一個對象沒有任何的強引用這個對象就會在內存回收的時刻被回收掉
如下代碼例子:

public static 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 obj) {
            ensureCapacity();
            elements[size++] = obj;
        }
        //彈出元素會造成內存泄漏,由於棧中的每個對象都是強引用的,
        //雖然在這個地方將元素取出size也進行了收縮,但是彈出對象的強引用還是一直由elements數組進行持有,
        //在垃圾回收的時候取法將彈出的對象進行銷燬造成了內存泄漏
        public Object pop() {
            if (0 == size) {
                throw new EmptyStackException();
            }
            return elements[--size];
        }
        private void ensureCapacity() {
            if (elements.length == size) {
                elements = Arrays.copyOf(elements, 2 * size + 1);
            }
        }
    }
//改進版的void pop(); 方法
//在彈出元素的同時,收縮列表,將列表中對這個元素的強引用設置成null,
//這樣這個彈出的對象在列表中就沒有強引用了,他的聲明週期完全取決於外部如何用它,
public Object pop() {
            if (0 == size) {
                throw new EmptyStackException();
            }
            Object obj = elements[--size];
            elements[size] = null;
            return obj;
}

第7條:避免使用終結方法

終結方法(finalizer)通常是不可預測的,也是很危險的,一般情況下是不必須要的。根據經驗:應該避免使用。

建議:所有需要銷燬的對象都必須顯示的調用終止方法.
例如:InputStreamclose方法。

缺點:

  1. 終結方法不能保證及時執行。
  2. 可移植性問題。由於終結方法的調用以來jvm垃圾回收算法,所以不同的jvm很可能會有不同的結果。
  3. 終結方法執行的線程(也就是GC的線程)有可能比你應用程序的任何線程優先級都低。假如利用終結方法來釋放內存,程序都已經OutOfMemoryError死掉了,終結方法還沒有調用。
  4. 終結方法不會拋出異常。終結者方法中如果有異常則不會打印出任何信息,且終結方法會停止,這樣對找bug來說真是難上加難。
  5. 使用終結方法有非常嚴重的性能損失。書上舉例說:正常創建和銷燬一個簡單對象時間大約爲5.6ns。增加一個終結方法使時間增加到2400ns。慢了大約430倍。

既然java提供了終結方法那麼它肯定是有用途的。

用途:

  1. 安全網。當程序中有對象忘記調用顯示終結方法,例如InputStream 忘記調用close方法。這個方法可以當做安全網,在這個方法中顯示調用close,雖然不能保證終結方法及時的調用,但是遲一點調用總比永遠不釋放要好得多。如果終結方法中發現資源還未被終止,則應該在日誌中增加一條警告。這代表程序中的bug應該得到修復。還要考慮終結方法會影響到性能上面提到過,是否值得這麼做。
  2. 本地對象。在本書中作者用了本地對等體的名詞,其實就是Java編程思想中的本地對象,也就是java調用C或C++ malloc出來的內存,這些內存如果不顯示的調用free將無法銷燬,java的GC機制也無法銷燬這些對象,所以需要在終結方法中調用free來釋放內存。
    其實這些本地對象的銷燬一定要在程序中顯示調用釋放方法。

下面代碼是如何正確的編寫終結方法:

  1. 終結方法必須調用父類的終結方法,否則當類繼承的情況下父類就永遠不可能調用終結方法了。這個代碼使用try{}finally{}代碼塊在finally{}中調用super.finalize();保證就算髮生異常也一定會執行父類的終結方法。
  2. 爲了防止粗心大意忘記調用父類的終結方法,有了第二種寫法,終結方法守護。如下第二段代碼用了一個匿名Object內部類來實現終結方法,在這個內部類的void finalize()方法中去調用Foo類需要結束的對象,由於這個匿名Object類沒有父類所以寫不寫super.finalize();也無所謂。
@Override
protected void finalize() throws Throwable {
        try {
            //Finalize subclass state
        } finally {
            super.finalize();
        }
}

public class Foo{
        private final Object finalizeGuardian = new Object(){
            @Override
            protected void finalize() throws Throwable {
                //Finalize subclass state
            }
        };
        ......
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章