Java设计模式(Singleton)

今天上午跟朋友谈了下单例模式因此决定记录一下,相信不管是Android还是Java开发者应该都用的比较多
,主要分为二种第一种是懒汉式顾名思义就是我需要的时候才会去创建比较简单,另外一种是饿汉式。
画张图
详情请参考

单例懒汉式跟饿汉式的优缺点

这里写图片描述
代码如下

package com.example.john.test.util;

/**
 * zm 2016/12/21.
 */

public class LogUtil {
    private  volatile  static LogUtil instance=null;
    private static LogUtil mLogUtils;
    public final int DEBUG=0;
    public final int INFO=1;
    public final int ERROR=2;
    public final  int NOTHTING=3;
    public int level=DEBUG;
    private LogUtil(){

    }

    /**
     * 2)饿汉式是线程安全的,因为虚拟机保证只会装载一次,在装载类的时候是不会发生并发的。
     (3)如何实现懒汉式的线程安全呢
     * @return
     */
    public synchronized  static LogUtil getmLogUtilsInstance(){
        if(mLogUtils==null){
            mLogUtils=new LogUtil();
        }
        return mLogUtils;
    }
  public static LogUtil getInstance2(){
      synchronized (LogUtil.class){
          if(mLogUtils==null){
              mLogUtils=new LogUtil();
          }
          return  mLogUtils;
      }
  }
    /**
     * 同步锁的优化
     * @return
     */
    public static LogUtil getInstance(){
        if(mLogUtils==null) {
            synchronized (LogUtil.class) {
                if (mLogUtils == null) {
                    mLogUtils = new LogUtil();
                }
            }
        }
        return mLogUtils;
    }

    public void debug(String msg){
        if(DEBUG>=level){
            System.out.println("msg:"+msg);
        }
    }
    public void info(String msg){
        if(INFO>=level){
            System.out.println("msg:"+msg);
        }
    }
    public void error(String msg){
        if(ERROR>=level){
            System.out.println("msg:"+msg);
        }
    }
    public static LogUtil getInstance3(){
        //先检查实例是否存在,如果不存在才进入下面的同步块
        if(instance==null){
            //同步块,线程安全地创建实例
            synchronized (LogUtil.class){
                ///再次检查实例是否存在,如果不存在才真正地创建实例
                if(instance==null){
                    mLogUtils=new LogUtil();
                }
            }
        }
        return mLogUtils;
    }

}

在外部调用的话直接上代码

new LogUtil.debug("test");

首先将LogUtil的构造函数私有化,这样就无法使用new关键字来创建LogUtil的实例了。然后使用一个mLogUtils==null私有静态变量来保存实例,并提供一个公有的getInstance方法用于获取LogUtil的实例,在这个方法里面判断如果mLogUtils==null为空,就new出一个新的LogUtil实例,否则就直接返回mLogUtils。这样就可以保证内存当中只会存在一个LogUtil的实例了。单例模式完工!这时打印日志的代码需要改成如下方式:

LogUtil.getInstance().debug("test2");

两种实现方式:

1 懒汉模式(类加载时不初始化)package Singleton;

public class LazySingleton {
//懒汉式单例模式
//比较懒,在类加载时,不创建实例,因此类加载速度快,但运行时获取对象的速度慢

private static LazySingleton intance = null;//静态私用成员,没有初始化

private LazySingleton()
{
    //私有构造函数
}

public static synchronized LazySingleton getInstance()    //静态,同步,公开访问点
{
    if(intance == null)
    {
        intance = new LazySingleton();
    }
    return intance;
}

}造函数定义为私有—-不能在别的类中来获取该类的对象,只能在类自身中得到自己的对象

2)成员变量为static的,没有初始化—-类加载快,但访问类的唯一实例慢,static保证在自身类中获取自身对象

3)公开访问点getInstance: public和synchronized的—–public保证对外公开,同步保证多线程时的正确性(因为类变量不是在加载时初始化的)

2 饿汉式单例模式(在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快)

package Singleton;

public class EagerSingleton {
//饿汉单例模式
//在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快

private static EagerSingleton instance = new EagerSingleton();//静态私有成员,已初始化

private EagerSingleton() 
{
    //私有构造函数
}

public static EagerSingleton getInstance()    //静态,不用同步(类加载时已初始化,不会有多线程的问题)
{
    return instance;
}

}

关键点:

1)私有构造函数

2)静态私有成员--在类加载时已初始化

3)公开访问点getInstance-----不需要同步,因为在类加载时已经初始化完毕,也不需要判断null,直接返回

Android单例使用场景

单例在Android开发中的实际使用场景,图片加载框架就是一个很好的例子。我在刚接触Android的时候使用的Android Universal Image Loader就采用了单例,这是因为它需要缓存图片,对缓存的图片集合做各种操作,需要关注单例中的对象状态,而且明显是需要访问资源的。这就很契合单例的特性。同样在热门的EventBus中也采用了单例,因为它内部缓存了各个组件发送过来的event对象,并负责分发出去,各个组件需要向同一个EventBus对象注册自己,才能接收到event事件,肯定是需要全局唯一的对象,所以采用了单例。
EventBus的单例采用的是双重检查加锁单例

static volatile EventBus defaultInstance;

public static EventBus getDefault() {
        if (defaultInstance == null) {
            synchronized (EventBus.class) {
                if (defaultInstance == null) {
                    defaultInstance = new EventBus();
                }
            }
        }
        return defaultInstance;
    }

Retrofit框架静态类构造工具类

在我的一个项目中使用到Retrofit做网络访问,这就需要一个具体的Retrofit对象操作网络。而且最好提供方法得到这个全局唯一的Retrofit对象。一开始我也在纠结是单例还是静态类。因为国内网站上对Retrofit的分析使用不是很多,而且网络上对这单例和静态类的分析争辩实在太多而且混乱。
最后直到看到这篇博客,感觉还是老外靠谱,最后我的项目采用下面的代码实例化Retrofit对象。具体代码是这样的。目前使用没有问题,大家当做使用Retrofit时候的实例化参考吧。(代码依据最新的Retrofit-2.0版本)

public class ServiceGenerator {

    public static final String API_BASE_URL = "http://your.api-base.url";

    private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder();

    private static Retrofit.Builder builder =
            new Retrofit.Builder()
                    .baseUrl(API_BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create());

    public static <S> S createService(Class<S> serviceClass) {
        Retrofit retrofit = builder.client(httpClient.build()).build();
        return retrofit.create(serviceClass);
    }

}

只所以这么写,采用静态类而不是单例,是因为把网络访问看做工具类,只需要拿到Retrofit实例对象做网络操作,ServiceGenerator工具类内部不维护内部变量也不关心内部变量的状态变化

单例开发实际问题

踩坑是每个开发者必须经历的过程,下面说明我在采用单例之后遇到的坑。相信每个初级Android开发者都遇到这样的问题。两个Activity组件之间传递数据,Intent和Bundle只能传递简单的基本类型数据和String对象
(当然也可以传递对象这就需要Parcelable和Serializable接口)。
当需要传递的只是几个值问题不大,但是如果需要传递的数据比较多就感觉代码不简洁而且key值多容易接收出错,传递对象需要对象继承Parcelable接口写大量的重复的模板代码。有没有优雅一点解决办法呢?

用单例对象传递对象的坑

Application传递对象的坑

相信有些人跟当时的我一样看过这样的博客”优雅的用Application传递对象”。当时的我看见这样博客,真实感觉遇到救星一样,感觉一下就解决了组件间传递对象的问题。

长者语:too young too simple sometimes naive
下面来说说如果你真的用Application传递对象会怎么样。原文博客是这样认为的Application由系统提供是全局唯一的对象,并且任何组件都可以访问到。哪就在自定义继承Application的子类里,保存内部变量,由发送的Activity取出内部变量并设值,startActivity之后在接收的Activity中也访问Application对象取出内部变量得到需要传递的对象。就没有复杂的Intent传值了。
但是如果你真的这么做:程序肯定会崩或者是取不到数据。

实际运行情况:
1. 如果你在接收数据的Activity中,按下Home键返回桌面,长时间的没有返回你的App。
2. 系统有可能会在系统内存不足的时候杀掉进程。
3. 当你再从最近程序运行列表进入你的App,系统会默认恢复刚刚离开的状态,直接进入接收数据的Activity中。
4. 然后调用各个生命周期方法回调,其中只要运行到从Application取数据行,程序就会弹出空指针NullPointerException异常导致崩溃。
5. 相信我一定是这样的,如果没有崩溃也只是因为你在内部变量中有默认初始化方法。这样肯定也是取不到想要的数据。
因为整个流程需要很长时间,我们可以使用adb命令杀掉进程adb shell kill,模拟长时间没有回到应用而由系统杀死进程的操作。如果觉得麻烦还可以打开Device Monitor-选中你的应用-使用红色按钮 Stop Process杀死进程。

程序崩溃的这主要原因就是:

系统会恢复之前离开的状态,直接进入某个Activity组件而不是再依次打开Activity,这样你的发送数据的Activity没有运行也就不会向Application中传值,自然也取不到值。
所以千万不要相信”优雅的用Application传递对象”这写博客,这是个坑!实际情况复杂得多,真使用起来还有很多问题。
指出这个问题原文是dont-store-data-in-the-application-object中文翻译的博客在这,大家可以点击查看会有详细说明。
EventBus的坑

当时也是在写一个项目,觉得Intent传递数据太麻烦,根据Appliaction可以传递数据的思路,其实自己也可以写个单例用来保存全局数据,各个组件取出实现组件间传递数据。然后很网络上搜索,发现EventBus同样实现了这样的思路,EventBus本身就是采用了单例模式。上篇博客的伏笔就在这。

EventBus: Android 事件发布/订阅框架,通过解耦发布者和订阅者简化 Android 事件传递
由一个组件发送事件,另一个组件向EventBus注册然后响应的方法就会得到数据。这里面也有坑啊。
当然我没有说EventBus有问题,只是使用不当会导致Crash程序崩溃。
当时项目是就是按照标准的EventBus使用流程写的代码,没有问题。还是上文的情况,按下Home键长时间没有返回应用,再次进入程序Crash。
原因还是一样的:

系统恢复离开的现场,直接运行接收数据的Activity,而没有运行到发送数据的Activity组件,取不到数据,因为根本就没有数据发送。
顺带提一句,

用Kill App这个方法能够检查出App中很多意想不到的问题 解决办法

用单例传递数据实质是用内存存储数据,然后全局方法。但是内存是很容易被虚拟机回收的。我们要解决的就是怎么样保存数据,持久化数据。
其实也没有什么好的解决方案。

还是直接将数据通过intent传递给 Activity 。 使用官方推荐的几种方式将数据持久化到磁盘上,再取数据。
在使用数据的时候总是要对变量的值进行非空检查,这样还是取不到数据
使用EventBus传递数据时采用onSaveInstanceState(Bundle
outState)方法保存数据,使用onCreate(Bundle savedInstanceState)等待恢复取值。
包装Activity跳转方法

针对第一项,我提供一个简单的包装跳转方法,简化Inten传递数据的代码逻辑

public class MyActivity extends AppCompatActivity{
    //Intent的key值
    protected static final String TYPE_KEY = "TYPE_KEY";
    protected static final String TYPE_TITLE = "TYPE_TITLE";

    //接收的数据
    public String mKey;
    public String mTitle;

    //包装的跳转方法 
public static void launch(Activity activity, String key, String title) {
        Intent intent = new Intent(activity, BoardDetailActivity.class);
        intent.putExtra(TYPE_TITLE, title);
        intent.putExtra(TYPE_KEY, key);
        activity.startActivity(intent);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //获取数据
         mKey = getIntent().getStringExtra(TYPE_KEY);
        mTitle = getIntent().getStringExtra(TYPE_TITLE);}
 }

使用代码,就一行
MyActivity.launch(this, key, title);

整个的逻辑是,在跳转的组件中实现类方法,把传递值的key值以成员类变量的形式写定在Activity中,需要传递的数据放入Intent中,简化调用方的使用代码。

onSaveInstanceState保存数据

onSaveInstanceState()方法的调用时机是:

只要某个Activity是做入栈并且非栈顶时(启动跳转其他Activity或者点击Home按钮),此Activity是需要调用onSaveInstanceState的,
如果Activity是做出栈的动作(点击back或者执行finish),是不会调用onSaveInstanceState的。
这正是上文我们程序Crash的场景,产生问题的关键操作点。
所有我们需要做的就是在onSaveInstanceState回调方法中保存数据,等待数据恢复。
代码没什么好贴的就是outState.putParcelable(KEY, mData);,然后在OnCreate中取savedInstanceState中的数据。
提示被put的数据需要实现Parcelable接口,如果不想写大量的模板代码可以使用Android Parcelable Code Generator插件快捷成成代码。

参考博文
郭神的博客 http://blog.csdn.net/guolin_blog/article/details/886064
懒汉式单例跟饿汉式优缺点 http://blog.csdn.net/u014284952/article/details/26138355
单例模式 http://www.cnblogs.com/kkgreen/archive/2011/09/05/2166868.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值