Effective Java——创建和销毁对象

Android.jpg

代码首先是给人看的,所以写代码要有写诗一样的感觉(哈哈雷军说的),无论是写逻辑、中间件或是写框架都是如此。
想要写好代码首先基础一定要好,所以我最近重新看了Effective Java,虽然这已经不是第一次看了但是还是有很多可以学习的地方。
本系列文章是总结Effective Java文章中我认为最重点的内容,给很多没时间看书的朋友以最短的时间看到这本书的精华。

第二章创建和销毁对象

目录.png

第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
            }
        };
        ......
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容