我大概在脑中规划了一下自己的技术成长路线,包括从基本的编程语言到数据库、网络编程这些,其中设计模式是一块,因为我本人有大概两年的工作经验,所以多少了解一些常用的设计模式,这里借着《研磨设计模式》这本书系统的学习一下,也避免长期学习《Java编程思想》显得枯燥乏味。《Java编程思想》只是本人对基础知识的一个回归。同时也由于时间有限,短期只针对常用的设计模式进行学习研究。个人觉得《研磨设计模式》这本书写的还是比较详细简单易懂,有读者需要的话可以留邮箱我发给你。
一、场景问题
考虑这样一个问题,在一个项目中要连接MySQL数据库,连接的地址以及口令都写在了配置文件中,一般的有xml格式或者是properties格式,那么我们要读取配置文件应该怎样读呢?
package com.chenxyt.java.test;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class AppConfig {
//定义两个用来存储配置文件内容的字符串
private String parameterA;
private String parameterB;
//访问对象的私有数据域
public String getParameterA() {
return parameterA;
}
public String getParameterB() {
return parameterB;
}
//构造方法
public AppConfig(){
readConfig();
}
//读取配置文件并赋值给存储字符串
private void readConfig(){
//获取一个properties对象的引用
Properties p = new Properties();
//输入流
InputStream in = null;
try{
//输入流获取配置文件
in = AppConfig.class.getResourceAsStream("appConfig.properties");
//输入流加载到properties对象
p.load(in);
//将配置文件的内容赋值到成员变量
this.parameterA=p.getProperty("url");
this.parameterB=p.getProperty("port");
}catch(IOException e){
//读取配置文件异常
e.printStackTrace();
}finally{
try {
//发生异常之后也要关闭输入流所以写在finally块中
in.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
在AppConfig.java的目录下编写配置文件appConfig.properties
url=127.0.0.1
port=3306
编写一个用于测试读取配置文件的客户端类Client.java
package com.chenxyt.java.practice;
import com.chenxyt.java.test.AppConfig;
public class Client{
public static void main(String[] args) {
AppConfig ac1 = new AppConfig();
System.out.println("paramA:" + ac1.getParameterA());
System.out.println("paramB:" + ac1.getParameterB());
}
}
运行结果:
package com.chenxyt.java.practice;
import com.chenxyt.java.test.AppConfig;
public class Client{
public static void main(String[] args) {
AppConfig ac1 = new AppConfig();
AppConfig ac2 = new AppConfig();
System.out.println("paramA:" + ac1.getParameterA());
System.out.println("paramB:" + ac1.getParameterB());
System.out.println("paramA:" + ac2.getParameterA());
System.out.println("paramB:" + ac2.getParameterB());
}
}
运行结果如下:
如图所示,我们每创建一个对象,对象内部的私有数据域就会被使用,这浪费了很多的资源。同时创建实例化对象也会占用大量的系统资源。事实上对于AppConfig这种类,在运行的时候获取一次资源就可以了。因为配置文件这些内容都是固定的。
二、解决方案
package com.chenxyt.java.test;
public class Singleton{
//定义一个存放单例对象的变量
private Singleton uniqueInstance = null;
//私有化构造函数,保证实例个数
private Singleton(){
//---处理业务,给对象的私有域赋值
}
//加锁保证线程安全 提供公共的获取实例方法 设置成静态方法,保证不用对象就可以调用
public synchronized static Singleton getInstance(){
//懒汉式设计,如果实例不存在则初始化
if(uniqueInstance==null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
package com.chenxyt.java.test;
public class Singleton{
//定义一个存放实例的变量
private Singleton uniqueInstance = new Singleton();
//私有化构造函数,保证实例个数
private Singleton(){
//---处理业务,给对象的私有域赋值
}
//提供公共的调用方法 由于只返回实例,所以不存在线程不安全问题
public Singleton getInstance(){
return uniqueInstance;
}
}
package com.chenxyt.java.test;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class Singleton {
//先创建一个实例
private static Singleton uniqueInstance = new Singleton();
public static Singleton getInstance(){
//返回唯一的类实例
return uniqueInstance;
}
//私有化构造方法,收回创建实例的权限
private Singleton(){
readConfig();
}
//建立两个成员变量存储配置文件的内容
private String parameterA;
private String parameterB;
//获取私有成员变量的方法
public String getParameterA() {
return parameterA;
}
public String getParameterB() {
return parameterB;
}
//读取配置文件并赋值给存储字符串
private void readConfig(){
//获取一个properties对象的引用
Properties p = new Properties();
//输入流
InputStream in = null;
try{
//输入流获取配置文件
in = Singleton.class.getResourceAsStream("appConfig.properties");
//输入流加载到properties对象
p.load(in);
//将配置文件的内容赋值到成员变量
this.parameterA=p.getProperty("url");
this.parameterB=p.getProperty("port");
}catch(IOException e){
//读取配置文件异常
e.printStackTrace();
}finally{
try {
//发生异常之后也要关闭输入流所以写在finally块中
in.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
客户端的调用方式也要相应改变一下:
package com.chenxyt.java.practice;
import com.chenxyt.java.test.Singleton;
public class Client{
public static void main(String[] args) {
Singleton sl = Singleton.getInstance();
System.out.println("paraA:" + sl.getParameterA());
System.out.println("paraB:" + sl.getParameterB());
}
}
运行结果如下:
![](https://i-blog.csdnimg.cn/blog_migrate/18ffb25ebb9e76b90d4be7c06e63aa37.png)
package com.chenxyt.java.practice;
import com.chenxyt.java.test.Singleton;
public class Client{
public static void main(String[] args) {
Singleton sl1 = Singleton.getInstance();
Singleton sl2 = Singleton.getInstance();
if(sl1==sl2){
System.out.println("单例模式成功!只产生了一个实例!");
}else{
System.out.println("单例模式失败!我们是两个不同的实例!");
}
}
}
运行结果如下:
![](https://i-blog.csdnimg.cn/blog_migrate/f8d8b3882c529321a4893a9edebf862d.png)
三、模式讲解
private Singleton(){
}
b.提供获取实例的方法:既然我们收回了创建实例的权限,那么我们就要为所有使用者提供一个统一的方法来获取实例,因此接下来就是提供一个全局方点来获取实例:
public Singleton getInstance(){
}
c.把获取实例的方法变成静态:如上我们提供了一个全局的获取实例的方法,这时存在一个问题,这个方法是一个实例方法,也就是没有实例我们没办法调用,而我们恰恰是要通过这个方法获取实例,这时候就陷入了一个死循环。因此,我们可以通过将这个方法设置为static静态方法,然后直接通过类访问这个方法获取实例。static的作用我在前边文章专门学习过,附带一个传送门
Java编程思想学习笔记二(1):static关键字的四种用法
public static Singleton getInstance(){
}
d.定义存储实例的属性:方法定义好了,实例怎么样创建呢?这里我们暂且忽略了线程安全的问题。如果我们直接返回一个new的实例可以不呢?答案显然是不可以的这样做跟直接new是一样的,反而比new更复杂了一些。所以我们要定义一个成员变量,然后返回这个成员变量,由于我们需要在一个static方法中使用这个变量,因此这个变量要被设置为static静态的。
private static Singleton instance = null;
e.实现控制实例的创建:我们在getInstance()方法中实现对实例的创建控制,如果存在则返回,不存在则重新创建一个然后返回。
public static Singleton getInstance(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
4.饿汉式的实现:饿汉式与懒汉式的区别在于,饿汉式在程序开始定义变量的时候就已经初始化了,然后在getInstance()方法中直接进行了返回。这里有一个很明显的区别在于
private static Singleton instance = null;
饿汉式:
private static Singleton instance = new Singleton();
区别在于:饿汉式的存储变量用到了static的特性!其实static基本就符合了单例设计模式的思想,因为:
这不正是static要实现的功能吗?!
四、单例模式的延迟加载
if(instance==null){
instance=new Singleton();
}
现在要使用instance实例了,看一下有没有,没有的话没办法了,只能创建了。
五、单例模式的缓存思想
缓存思想是一个典型的使用空间换时间的概念。我们使用Map作为简单的缓存来重新写一下懒汉式的单例模式
package com.chenxyt.java.test;
import java.util.HashMap;
import java.util.Map;
public class Singleton{
//定义键值对的key
private final static String DEFAULT_KEY = "SingletonKey";
//定义用来缓存的map
private static Map<String,Singleton> map = new HashMap<String,Singleton>();
//私有化构造函数,保证实例个数
private Singleton(){
//---处理业务,给对象的私有域赋值
}
//提供公共的调用方法 由于只返回实例,所以不存在线程不安全问题
public Singleton getInstance(){
Singleton instance = map.get(DEFAULT_KEY);
//缓存中没有就新创建一个然后放到map中
if(instance == null){
instance = new Singleton();
map.put(DEFAULT_KEY, new Singleton());
}
return instance;
}
}
上述代码中实际上就是用Map代替了原来的null,判断map对应的key是否有值,如果有则返回,没有就创建一个新的然后加到map中去。
六、单例模式的优缺点
1.时间和空间:
2.线程安全:
package com.chenxyt.java.test;
public class Singleton{
//定义一个用来存储变量的值
private static Singleton instance = null;
//私有化构造函数,保证实例个数
private Singleton(){
//---处理业务,给对象的私有域赋值
}
//提供公共的调用方法 由于只返回实例,所以不存在线程不安全问题
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class) {
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
这里之所有再嵌套一个if判断,是因为假设高并发的情况A跟B都进入第一个if了,那么如果不判断最终还是会创建两个实例。
package com.chenxyt.java.practice;
public class Singleton {
//定义静态内部类 只有在使用时才被加载
private static class SingletonHolder{
//由JVM控制线程安全
private static Singleton instance = new Singleton();
}
//私有化构造方法
private Singleton(){
//---
}
//提供对外的获取实例的方法
public Singleton getInstance(){
return SingletonHolder.instance;
}
}
七、单例和枚举
package com.chenxyt.java.practice;
enum Singleton{
uniqueInstance;
public void doSomething(){
//===
}
}
八、总结
单例模式是较为常用的一种设计模式,掌握单例模式的应用场景以及掌握懒汉式、饿汉式的写法与区别,还有更高级别的使用内部类或者枚举形式的实现。同时也了解懒加载和缓存的设计思想。