雅客

往事随风


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

深入理解单例模式

发表于 2018-07-21 | 分类于 设计模式 |
字数统计: 1,612 | 阅读时长 ≈ 8
  1. 为什么静态内部类的单例模式是最推荐的?
  2. 如何在反射的情况下保证单例
  3. 如何在反序列化中保证单例

为了回答上面三个问题,下面将逐步进行分析

1. 饿汉式——线程安全的单例模式
1
2
3
4
5
6
7
8
9
public class Singleton1 {
private static Singleton1 instance = new Singleton1();

private Singleton1(){}

public static Singleto1 getInstnance() {
return instance;
}
}

优点:线程安全;缺点:类加载的时候已经实例化,浪费空间

2. 懒汉式
(1)懒汉式V1
1
2
3
4
5
6
7
8
9
10
11
12
13
public class LazySingleton1() {
private static LazySingleton1 instance;

private LazySingleton1(){}

public static LazySingleton1 getInstance() {
if (instance == null) {
instance = new LazySingleton1();
}

return instance;
}
}

线程不安全,给方法加锁

1
2
3
4
5
6
public static synchronized LazySingleton1 getInstance() {
if (instance == null) {
instance = new LazySingleton1();
}
return instance;
}

由于将synchronized关键字加在方法上会造成线程阻塞(同步),在性能上会大打折扣。所以使用双重校验锁(在保证线程安全的同时提高了性能。)

1
2
3
4
5
6
7
8
9
10
public static LazySingleton1 getInstance() {
if (instance == null) {
synchronized (LazySingleton1.class) {
if (instance == null) {
instance = new LazySingleton1();
}
}
}
return instance;
}
(2) 懒汉式V2

内部类加载机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class OuterTest {

static {
System.out.println("load outer class...");
}

static class StaticInnerTest {
static {
System.out.println("load static inner class...");
}

static void staticInnerMethod() {
System.out.println("static inner method...");
}
}

public static void main(String[] args) {
OuterTest outerTest = new OuterTest();
System.out.println("===========//=========");
OuterTest.StaticInnerTest.staticInnerMethod();
}
}

输出:
load outer class...
===========//=========
load static inner class...
static inner method...

说明:1. 加在一个类时,其内部类不会被同时加载。2. 一个类被加载,当且仅当某个静态成员(静态域,构造器,静态方法等)被调用时发生。


 回到第一版的双重校验锁。其实不管性能如何优越,还是使用了synchronized关键字,那么对性能始终是有影响的(有兴趣的话可以了解一下synchronized的内存模型与底层原理)。所以,下面给出了改良版本——使用静态内部类的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class LazySingleton2 {
private LazySingleton2() {
}

static class SingletonHolder {
private static final lazySingleton2 instance = new LazySingleton2();
}

public static LazySingleton2 getInstance() {
return SingletonHolder.instance;
}
}

class MyThread extends Thread{
@Override
public void run() {
System.out.println(LazySingleton2.getInstance().hashCode());
}
}

class Run{
public static void main(String[] args) {

MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();

t1.start();
t2.start();
t3.start();
}
}

结果:
1385166630
1385166630
1385166630

 由于对象实例化是在内部类加载的时候构建的,因此是线程安全的(在方法中创建对象才存在并发问题,静态内部类随着方法的调用而被加载,只加载一次,不存在线程安全问题)。

 在getInstance()方法中没有使用synchronized关键字,因此没有造成多余的性能损耗。当LazySingleton2类加载的时候,其静态内部类SingletonHolder并没有被加载,因此instance对象没有被构建。

 而我们在调用LazySingleton2.getInstance()方法时,内部类SingletonHolder被加载,此时单例对象才被构建。因此,这种写法节约空间,达到了懒加载的目的。

(3) 懒汉式V3

使用静态内部类的懒加载方式虽然具有很多优良特性,但是在反射的作用下,单例结构还是会被破坏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class LazySingleton2Test {

public static void main(String[] args) {
//创建第一个实例
LazySingleton2 instance1 = LazySingleton2.getInstance();

//通过反射创建第二个实例
LazySingleton2 instance2 = null;

try {
Class<LazySingleton2> clazz = LazySingleton2.class;
Constructor<LazySingleton2> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
instance2 = constructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
//检验两个实例的hashcode值
System.out.println("Instance1's hashcode:" + instance1.hashCode());
System.out.println("Instance2's hashcode:" + instance2.hashCode());
}
}

结果:
Instance1's hashcode:21685669
Instance2's hashcode:2133927002

由于上面的问题,所以诞生了第三版的懒汉式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class LazySingleton3 {

private static boolean initialized = false;

private LazySingleton3() {
synchronized (LazySingleton3.class) {
if (initialized == false) {
initialized = !initialized;
} else {
throw new RuntimeException("单例已经被破坏!");
}
}
}

static class SingletonHolder {
private static final LazySingleton3 instance = new LazySingleton3();
}

public static LazySingleton3 getInstance() {
return SingletonHolder.instance;
}
}

经过测试后输出为:
java.lang.reflect.InvocationTargetException
trueat sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
trueat sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
trueat sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
trueat java.lang.reflect.Constructor.newInstance(Constructor.java:423)
trueat com.yuangh.concurrent4.demo3.LazySingleton2Test.main(LazySingleton2Test.java:22)
Caused by: java.lang.RuntimeException: 单例已经被破坏!
trueat com.yuangh.concurrent4.demo3.LazySingleton3.<init>(LazySingleton3.java:16)
true... 5 more
Exception in thread "main" java.lang.NullPointerException
trueat com.yuangh.concurrent4.demo3.LazySingleton2Test.main(LazySingleton2Test.java:28)
Instance1's hashcode:1836019240

这就保证了反射无法破坏其单例性。

(4) 懒汉式V4

在分布式系统中,有些情况下需要在单例中实现Serializable接口。这样就可以在文件系统中存储它的状态并且在稍后的某一时间点取出。

将 public class LazySingleton3 修改为 public class LazySingleton3 implements Serializable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class LazySingleton3Test {

public static void main(String[] args) {
try {
//将对象序列化到文件
LazySingleton3 instance1 = LazySingleton3.getInstance();
ObjectOutput out = null;
out = new ObjectOutputStream(new FileOutputStream("filename.ser"));
out.writeObject(instance1);
out.close();

//从文件反序列化到对象
ObjectInput in = new ObjectInputStream(new FileInputStream("filename.ser"));
LazySingleton3 instance2 = (LazySingleton3) in.readObject();
in.close();
System.out.println("instance1's hashcode:" + instance1.hashCode());
System.out.println("instance2's hashcode:" + instance2.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
}

}

得到结果为:
instance1's hashcode:856419764
instance2's hashcode:1480010240

 显然,出现了两个实例类,说明破坏了单例模式。我们需要提供 readResolve() 方法的实现。readResolve()代替了从流中读取对象。这就确保了在序列化和反序列化的过程中没人可以创建新的实例。下面是懒汉式第四个版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class LazySingleton4 implements Serializable {

private static boolean initialized = false;

private LazySingleton4() {
synchronized (LazySingleton3.class) {
if (initialized == false) {
initialized = !initialized;
} else {
throw new RuntimeException("单例已经被破坏!");
}
}
}

static class SingletonHolder {
private static final LazySingleton4 instance = new LazySingleton4();
}

public static LazySingleton4 getInstance() {
return SingletonHolder.instance;
}

private Object readResolve() {
return getInstance();
}
}

测试后输出结果为:
instance1's hashcode:856419764
instance2's hashcode:856419764
总结

 在项目开发中根据情况选择相应的版本使用即可,并没有强制要求使用哪个版本。当然,一般来说,第二个版本(懒汉式V2)是使用的比较多的。

配置centos_minimal

发表于 2018-07-21 | 分类于 Linux |
字数统计: 1,790 | 阅读时长 ≈ 7

我只想说,我绝望了一次,不想再绝望第二次

说在前面
  • 工作环境:VMware® Workstation 12 Pro 12.5.6 build-5528349
  • linux版本:CentOS-7-x86_64-Minimal-1611.iso

VMware安装镜像不多赘述,傻瓜操作!

万事第一步,把网络先连上

说句废话啊,CentOS-7-x86_64-Minimal,见名知意,其实就是精简版本的centos(DVD 版的镜像文件有4G多,精简版本的只有600多M),关于这个精简啊,真的只能说无语,好多命令都不能用(比如:nano, ifconfig),我们需要自己配置与安装很多东西。其中配置网络就是万事第一步,没有网络,寸步难行哦。

  • 第一步:选中虚拟当前虚拟机右键设置——在弹出的窗口中选择网络适配器——在网络连接选项中选择 NAT模式——确定退出
  • 第二步:激活centos中的网卡(ONBOOT=yes,默认是no),这就是不能联网的原因(下面的操作都是root权限)

    1. cd /etc/sysconfig/network-scripts/ 命令进入,通过 ls 命令可以查看下面的文件

      image

    2. vi ifcfg-ens33 进入后编辑该文件,编辑如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      iTYPE=Ethernet
      BOOTPROTO=dhcp
      DEFROUTE=yes
      PEERDNS=yes
      PEERROUTES=yes
      IPV4_FAILURE_FATAL=no
      IPV6INIT=no
      IPV6_AUTOCONF=no
      IPV6_DEFROUTE=no
      IPV6_PEERDNS=no
      IPV6_PEERROUTES=no
      IPV6_FAILURE_FATAL=no
      IPV6_ADDR_GEN_MODE=stable-privacy
      NAME=ens33
      UUID=1c4e0233-8fca-47f7-9cf5-5aa94e0319fa
      DEVICE=ens33
      ONBOOT=yes
    3. service network restart 命令敲一遍,相当重启一下服务,然后 ping www.baidu.com,我们惊奇的发现连上网络了。

  • 第三步:其实前两步走完不出意外的话基本上就连上网络了,出了意外的话就自行百度啦。这一步就是给虚拟机配置一个静态IP,用于在客户端使用 xshell 等工具进行连接,就相当于把虚拟机当做一个服务器,然后客户端只需要连接服务端的一个固定IP就可以访问服务器了。其实看上面的配置文件中的 BOOTPROTO=dhcp,这个的意思就是说是一个动态IP,下面进行配置:

    1. 进入文件的方式命令和前一步一样,进入后配置如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      iTYPE=Ethernet
      BOOTPROTO=static
      DEFROUTE=yes
      PEERDNS=yes
      PEERROUTES=yes
      IPV4_FAILURE_FATAL=no
      IPV6INIT=no
      IPV6_AUTOCONF=no
      IPV6_DEFROUTE=no
      IPV6_PEERDNS=no
      IPV6_PEERROUTES=no
      IPV6_FAILURE_FATAL=no
      IPV6_ADDR_GEN_MODE=stable-privacy
      NAME=ens33
      UUID=1c4e0233-8fca-47f7-9cf5-5aa94e0319fa
      DEVICE=ens33
      ONBOOT=yes

      IPADDR=192.168.32.201
      NETMASK=255.255.255.0
      GATEWAY=192.168.32.2
      DNS1=8.8.8.8
      DNS2=8.8.4.4

      BOOTPROTO=static 设置为静态IP,IPADDR 是自己的配置的ip,前三段不变,第四段改为自己设置的数字,NETMASK 是
      子网掩码, GATEWAY 是网关。DNS1和DNS2是域名系统,这是基本配置(复制就行)。说一下怎么查看这几个配置:在VMware中点击编辑——点击虚拟网络编辑器——点击更改设置——选择VMnet8后点击NAT设置,就可以查看到基本配置。

      image

    2. service network restart 重启网络服务,使用 ip addr 就可以查看自己的网络配置了(无法使用 ifconfig命令,需要安装)。

      image

装点工具——先配置一个软件源

网络连接上之后可以干的事情就多了,前面已经说了,好多命令是不能用的,这时就需要我们把它装上。但是,装软件的话是需要软件源的,centos默认的软件源下载很慢,这就需要我们配置一个软件源,我配置的是阿里的软件源。

在开始之前先介绍一个命令 yum :yellowdog updater modified , 这老家伙就是用了下载各种工具的boss,还是很牛叉的!对于我们这个精简版本来说,他需要经常上场。

  • 第一步:备份原来的默认源(其实就是重命名) mv /etc/yum.repos.d/CentOS.repo.backup
  • 第二步:curl -o /etc/yum.repos.d/ali.repo http://mirrors.aliyun.com/repo/Centos-7.repo 使用这个命令就可以把阿里的源配置为自己的默认源。ls 命令查看如下:

image

  • 第三步就是装工具了(工具就会从我们刚刚配置的源进行下载)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
       //基本名介绍:

    $>yum list //列出所有软件包
    $>yum list installed //列出已经安装的软件包
    $>yum list installed | grep nano //列出已经安装的软件包
    $>yum search nano //在yum的软件源中搜索软件
    $>yum remove nano //卸载软件
    $>yum -y install nano //直接安装,不需要yes确认.
    $>yum list installed | grep nano //查看是否安装了Nano

    $>sudo yum install --downloadonly --downloaddir=/home/centos/rpms nano //只下载, 不安装

    $>sudo yum reinstall --downloadonly --downloaddir=/home/centos/rpms nano//重新下载,不安装

    $>sudo yum localinstall xxx.rpm //从本地rpm文件直接安装软件

    $>yum -y install nano //直接安装

    //基本工具安装(建议下面的都安装):
    $>yum -y install net-tools //安装网络工具(可以使用 ifconfig 命令了)
    $>yum -y install gcc //安装gcc编译工具
    $>yum -y install kenel-devel
    $>yum -y install mkisofs //用于制作 .iso 文件

    其余的用到了假如有提示的话再安装即可。
共享文件夹——安装虚拟机增强工具

共享文件夹必不可少,这可是重中之重哦。桌面版的还好,这个minmal版的就要麻烦一些,所以上面建议安装的工具都要装上,要不然会很麻烦。、

 其实假如使用VMware的虚拟机增强工具,点击安装或更新的话他会自动挂载,如果使用centos的桌面版的话,我们直接可以在桌面上看到,但是minimal版的话由于只能使用命令,所以相对麻烦一些,我相对习惯于使用手动挂载,就是把这个工具制作成一个 .iso 镜像文件,然后挂载到minimal中。

下面介绍一下如何制作一个 .iso 文件,其实这就是一个归档打包的过程(root权限)

1
$>mkisofs -r -o linux.iso /home/centos/linux	//-r : 保留原文件,-o:输出的iso文件名

挂载vmware安装目录下的linux.iso(虚拟机增强工具镜像文件)文件:

  1. $> mkdir /mnt/cdrom //在mnt文件夹下新建cdrom文件夹
  2. 插入镜像:打开设置——选项——共享文件夹(总是启用)——添加一个共享文件夹
  3. $> mount /dev/cdrom /mnt/cdrom //挂载设备
  4. $> cp /mnt/cdrom/* /homn/centos/linux //将cdrom中的文件复制到Linux目录下

下面就是关键部分了,其实到这里的话就很easy了:

  1. $> cd /home/centos/linux //进入linux文件夹

image

  1. $> tar -xzf VMwareTools-10.1.6-5214329.tar.gz 解压后会出现 vmware-tools-distrib,进入该文件夹

image

  1. $> ./vmware-install.pl 执行该文件, 后面出现的询问都闭着眼睛回车,一直到最后出现 Enjoy, 那么恭喜你,成功了。下面就可以查看共享文件夹了:cd /mnt/hgfs/ 下面就是你的共享文件夹了。
  • 分享一个链接,一个命令大全:
    https://www.centoschina.cn/
  • 推荐书籍:
    《Linux命令行与shell脚本编程大全》

线程基础知识

发表于 2018-05-05 | 分类于 Java线程 |
字数统计: 4,252 | 阅读时长 ≈ 17

进程与线程

  • 什么是进程: 我们先看看比较官方的表述,进程是操作系统结构的基础,是一次程序的执行,是一个程序及其数据在处理机上顺序执行时所发生的活动;是程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。

 其实关于进程的官方定义也是很好懂,但是我们都不太喜欢过于官方的东西,还是来点比较实在解释比较好。进程就是我们打开的一个个应用程序,每一个应用程序都有属于自己的一个端口号,操作系统为每一个应用程序分配内存空间后,端口号成为了应用程序在任务队列的标识(就好比是程序在内存中的地址,独一无二),只要应用程序没有被关闭,操作系统没有回收应用程序所占的空间,则程序一直占有内存。这也就是为什么有些时候打开应用程序时我们会遇到端口已经被占用的问题(最好的例子,你在IDE中打开了Tomcat,那么在外部就不能重复打开),因为在同一个时刻内只能有一个应用程序独占同一段内存空间,这就是进程。再通俗一点,就是任务管理器里面的进程队列:

  • 什么是线程: 刚开始听到线程的时候可能会感觉有点懵,脑袋有点转不过弯来,就拿学习Java的过程来说吧,刚开始学习啥是面向对象啊,异常啊,泛型啊,集合啊什么的,虽然开始接触时会有点难以理解,但是慢慢的,随着学习的深入,总是有迹可循的。但是线程这部分知识给我的感觉很独特,那就是学了感觉懂一点,但是总觉得抓不住精髓,所以往往是无从下手。后来,好好磨了一下这部分知识,感觉抓到一点头绪了,现在我就说说我眼中的线程:前面介绍了进程,我们知道了进程就好比一个个的应用程序,我们可以同时打开多个应用程序,边听歌边打游戏,操作系统则对这些应用进行调度,对于这一个个的进程,操作系统就是掌握他们生杀大权的上帝。理解了这个,我们来理解线程就会很容易了,线程就好比进程中一个个独立的子任务,进程就好比操作系统,是所有线程的上帝。以次类推,在一个进程中,就拿QQ来说,我们可以边聊天,边下载QQ文件,边发送表情,这些功能就好比一个个的线程,并同时在后台默默运行,应用程序则进行总的调度。

 补充一点:在开始学习Java的时候(或者是C, C++等),我们都知道程序的入口是main,其实学习了线程后我们将了解到,main其实也是一个线程,它有一个特殊的名字:主线程(main线程),所以说,我们原先编写的程序大多是单线程的程序(只用到了主线程)。而Java中的多线程的编程,就是为了让我们掌握关于多任务多功能并行处理的能力,现在我们就进入多线程编程之旅。

创建线程的四种方式

 在我们没学习多线程之前,main线程就是默认线程,程序的所有运算都在主线程上执行,main线程是为我们提供好的程序入口(main方法),不需要我们去创建。但假如我们需要其他的子线程去帮助我们执行其他的任务,那么就需要创建新的线程,下面就介绍四种创建线程的方式:

第一种——继承Thread类

  • Thread类简介: Thread类是JDK1.0时就提供的一个线程类,它为Java提供了多线程编程的能力,每一个Java类只需要继承Thread类,并重写其中的run()方法,那么该类就是一个新的子线程。程序示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class MyThread extends Thread {

//重写run()方法
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
int time = (int) (Math.random() * 1000);
Thread.sleep(time);
System.out.println("run=" + Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

class Run {
public static void main(String[] args) {

try {
MyThread myThread = new MyThread();
myThread.setName("myThread");
myThread.start();
for (int i = 0; i < 10; i++) {
int time = (int) (Math.random() * 1000);
Thread.sleep(time);
System.out.println("main=" + Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

第二种——实现Runnable接口

 继承Thread类固然是一种简单的创建线程的方式,但是由于Java中的单继承机制,使得继承了Thread类就不能再继承其他的类,这是不利于扩展的。所以可以使用另一种方式,实现Runnable接口。Runnable接口是一个公共协议,里面只提供了一个run()方法。其实,Thread类也同样实现了Runnable接口,所以通过继承Thread类得到线程和实现Runnable接口得到线程没有区别,只是实现Runnable接口更容易扩展。程序示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
第一种方法
*/
public class MyRunnable implements Runnable{

@Override
public void run() {
System.out.println("运行中!");
}
}

class Run{
public static void main(String[] args) {
//new一个线程对象
Runnable runnable = new MyRunnable();
//将线程对象传给Thread的构造器
Thread thread = new Thread(runnable);
//启动线程
thread.start()
}
}

/**
第二种方法:使用匿名内部类
*/
class Run{
public static void main(String[] args) {
new Thread(new Runnable(){
@Override
public void run() {
System.out.println("运行中!");
}
}).start();
}
}

第三种——实现Callable接口

 上面的两种创建线程方式是我们比较熟悉的,其实一直到JDK 1.5之前,创建线程的也只有上面的两种。在JDK 1.5 之后,Java对线程做出了更加丰富的扩充,添加了一个java.util.concurrent的包(多线程编程者的福音),里面提供了更多更加丰富,功能更加完善的方法,其中在创建线程方面也做了扩充,而实现Callable接口创建线程就是其中一种方法。下面是代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ThreadDemo implements Callable<Integer> {
//可以抛出异常,有返回值
@Override
public Integer call() throws Excetion{
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}

//Callable需要FutureTask类的支持来获取返回值
class CallableDemo {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();

//使用FutureTask,接收运算结果
FutureTask<Integer> result = new FutureTask<>(td);

new Thread(result).start();

//接收线程运算后的结果
Integer sum = 0;
try {
//分线程运算完成后执行,得到线程运算的结果
sum = result.get();
System.out.println(sum);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}

 通过对比实现Runnable和实现Callable,我们会发现比较有趣的地方:

  • 实现Runnable接口,run()方法没有返回值,且run()方法不能抛出异常
  • 实现Callable接口,call()方法有返回值,且可以抛出异常

 其实这两个方法就设计的就很有意思,run翻译过来就是执行的意思,也就是说我给你一个任务,你只要负责把它做完,其他的不用你操心,做完了也不用告诉我(不叫你哔哔你就别哔哔)。而call翻译过来有打电话的意思,打电话肯定要有回音啊(你必须哔哔),而且占线了(对不起,电话暂时无法接通…)是不是要说一声啊(抛异常)。

第四种——通过线程池来创建线程

 其实上面提供的几种创建线程的方法在项目开发中是不常用的,因为假如出现这种情况:如果并发的线程数量特别多,并且每个线程只执行很短的时间就被销毁,这样频繁的创建和销毁线程会大大降低系统的效率。在JDK 1.5之前遇到这样的问题我们也只能干瞪眼,但是在JDK 1.5之后,我们却有能力对这种情况作出改善,那就是创建一个线程池(Executor, java.util.concurrent包下的Executor接口),下面只介绍如何通过线程池去创建线程,关于线程池的详细构造与说明请看另一篇笔记。下面是程序示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ThreadA implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}

class TestThreadPool {
public static void main(String[] args) {

//使用Executors工具类创建线程池
//ExecutorService pool = Executors.newFixedThreadPool(5);

//使用ThreadPoolExecutor创建线程
ExecutorService pool = new ThreadPoolExecutor(10, 20, 3,
TimeUnit.SECONDS, new LinkedBlockingDeque<>(10));

ThreadA t = new ThreadA();

//执行
pool.execute(t);

//关闭线程池
pool.shutdown();
}
}

 其实准确来说,线程池不能说成是创建线程的方式,而是提供了一个线程队列,避免了额外的创建于开销。这有点向JDBC中的数据库连接池,池子里面已经准备好了一些数据库连接,需要的时候就从池子里面取就行了。

Thread中的API介绍

 通过上面的介绍,我们已经知道了什么是线程,如何去创建一个线程,下面就介绍一下Thread类中的几个重要API(关于java.util.concurrent的包中的类与API后续笔记会有说明,但是其实很多方法都是Thread中方法的衍生或补充)进行介绍。

  • start(): 使当前线程开始执行,Java虚拟机调用该线程的run()方法

  • sleep(long millis): 在指定的毫秒数内让正在执行的线程休眠(暂停执行)。注意,这是一个静态方法,直接使用 Thread.sleep() 调用,并且该方法会抛出InterruptedException(中断异常)

  • currentThread(): 放回当前正在执行的线程对象的引用。这是一个静态方法,使用Thread.currentThread()调用。

  • getId(): 返回该线程的标识符。线程ID是一个正的long值,在创建线程时生成,并且是唯一的,终生不变。在线程终止时,该线程ID可以被重新使用。

  • getState(): 返回该线程的状态。线程由如下几种状态:

    1. NEW: 线程实例化后还未执行start()方法时的状态
    2. RUNNABLE: 线程进入运行状态
    3. TERMINATED: 线程被销毁时的状态
    4. BLOCKED: 某一线程等待锁
    5. WAITING: 线程执行了Object.wait()方法后出现的状态
  • setName(): 设置当前线程的名字

  • getName(): 返回当前线程的名字

  • isAlive(): 测试当前线程是否处于活动状态(如果线程已经启动且尚未终止,则为活动状态)

  • setPriority(int newPriority): 更改线程的优先级。下面对线程优先级进行说明:

  在Java中,线程的优先级分为1~10个等级,如果优先级小于1或大于10,则抛出IllegalArgumentExceptin()异常。在JDK中使用三个常量定义了三个优先级的值:

- public final static int MIN_PRIORITY = 1;
- public final static int NORM_PRIORITY=5;
- public final static int MAX_PRIORITY=10;

- 高优先级的线程总是大部分先执行完,但不代表高优先级的线程都执行完
- 并非先被main线程调用就会先执行完
- 当线程的优先级等级差距很大时,谁先执行与调用顺序无关

  注意:不要把线程的优先级与运行结果的顺序作为衡量标准,线程优先级与打印顺序无关,这说明线程的优先级还具有一定的随机性

  • getPriority(int newPriority): 返回线程的优先级

  • interrupted(): 测试当前线程是否已经中断(静态方法)。线程的中断状态由该方法清除,如果两次调用该方法,第二次将返回false(假如第一次调用时当前线程处于中断状态,在返回true的同时把中断状态清除,第二次调用时将返回false)

  • isInterrupted(): 测试当前线程是否已经中断(不是静态方法,不会清除中断状态)

  • interrupt(): 中断当前线程

  • yield(): 暂停当前正在执行的线程对象,并执行其他线程(放弃当前的CPU资源,将它让给其他任务去占用CPU时间,但放弃时间不确定)

  • join(): 等待线程终止。理解:主线程等待子线程终止。main是主线程,在main线程中创建子线程thread,并在main线程中调用thread.join(),那么main线程要等待thread线程执行后再执行。

  具体解释:在很多情况下,主线程生成并启动了子线程,如果子线程里要进行大量的耗时运算,主线程往往将在子线程之前结束,但是如果主线程处理完其他的事物后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这时就需要用到join()方法。join()方法具有使线程排队运行的作用,有些类似于同步的运行效果。join与synchronized的区别是:join在内部使用wait()方法进行等待,而synchronized关键字使用的是“对象监视器”原理做为同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//下面给出的是join()方法的示例
public class JoinThread extends Thread{

@Override
public void run() {
try{
int secondValue = (int) (Math.random() * 10000);
System.out.println(secondValue);
Thread.sleep(secondValue);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
class Test{
public static void main(String[] args) {
try{
JoinThread joinThread = new JoinThread();
joinThread.start();
joinThread.join();
System.out.println("当joinThread对象执行完毕后再执行!");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}

this和Thread.currentThread()

 刚开始学习线程的时候可能会对this和Thread.currentThread()有点迷,傻傻分不清这两个到底有什么区别,现在我就来详细解释一下这两个的区别和联系,先看一段代码,通过结果来分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class CountOperate extends Thread {

public CountOperate() {
System.out.println("-----cbegin---");

//构造器里面有一个主线程main,其他线程由主线程产生,所以当前线程为main
System.out.println("Thread.currentThread.getName: " + Thread.currentThread().getName());

System.out.println("Thread.currentThread.isAlive: " + Thread.currentThread().isAlive());

//表示当前线程对象
System.out.println("this.getName: " + this.getName());

System.out.println("this.isAlive: " + this.isAlive());

System.out.println("----cend----");

System.out.println();
}

@Override
public void run() {
System.out.println("-----rbegin---");

System.out.println("Thread.currentThread.getName: " + Thread.currentThread().getName());

System.out.println("Thread.currentThread.isAlive: " + Thread.currentThread().isAlive());

System.out.println("this.getName: " + this.getName());

System.out.println("this.isAlive: " + this.isAlive());

System.out.println("----rend----");
}
}

class Run1 {
public static void main(String[] args) {
//生产一个线程对象
CountOperate c = new CountOperate();
//将前面生产的一个线程对象作为构造参数传给Thread再生产一个线程对象
Thread thread = new Thread(c);

System.out.println("main begin thread isAlive=" + thread.isAlive());

thread.setName("A");
thread.start();

System.out.println("main end thread isAlive=" + thread.isAlive());
}
}

结果:
-----cbegin---
Thread.currentThread.getName: main
Thread.currentThread.isAlive: true
this.getName: Thread-0
this.isAlive: false
----cend----

main begin thread isAlive=false
main end thread isAlive=true
-----rbegin---
Thread.currentThread.getName: A
Thread.currentThread.isAlive: true
this.getName: Thread-0
this.isAlive: false
----rend----

 this是什么: 如果线程类是继承java.lang.Thread,那么线程类就可以使用this关键字去调用继承自父类的Thread方法,this代表当前的线程对象.

 Thread.currentThread(): Thread.currentThread()可以获取当前线程的引用,一般是在没有线程对象又需要获得线程信息时可以通过Thread.currentThread()获得当前代码段所在线程的引用。

 this和Thread.currentThread()的区别:

  • 在构造器中,this代表的是当前线程对象(通过构造器生成的线程对象),但是由于线程对象正在生产,还没有start,所以线程状态为false。但是在构造器中,Thread.currentThread()表示的是mian线程(main线程是主线程,其实并非所有的线程都是我们手动开启,还有一些线程是JVM自动开启的,比如垃圾回收线程,主线程等),所以线程状态为true。

  • 在run方法中,this和Thread.currentThread()代表的都是当前线程对象的引用。由于线程已经start,所以线程状态就是true

参考书籍

  • 《Java多线程编程核心技术》

面向对象六大原则——开闭原则

发表于 2018-04-28 | 分类于 面向对象六大原则 |
字数统计: 932 | 阅读时长 ≈ 3

什么是开闭原则(Open Close Principle, OCP)

 开闭原则是Java中最基础的设计原则,它指导我们如何建立一个稳定的,灵活的系统。

  • 定义:一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。
  • 为什么使用开闭原则: 在程序的生命周期内,因为变化,升级和维护等原因需要对程序原有的代码进行修改时,可能会给代码引入错误,增加项目开发测试的复杂度,也可能会使我们不得不对整个功能进行重构,而且还要对原有的代码进行测试。

开闭原则——我是你们的爸爸

 开闭原则是一个非常基础的原则,其他的五个原则都是开闭原则的具体,也就是说其他的五个原则是指导设计的工具和方法,而开闭原则才是它们的精神领袖。从另一个角度说,开闭原则就是抽象类,其他五大原则是具体的实现类,开闭原则是一种纲领性的框架,五大原则在这个框架里添砖加瓦。所以这么说吧,只要我们遵守好其他的五大原则,那么我们设计的软件自然就遵守了开闭原则,现在我们再好好回顾一下其他五大原则:

  • 单一职责原则:应该有且仅有一个原因引起类的变更(一个接口或一个类只有一个原则,它就只负责一件事)
  • 里式替换原则:子类型必须能替换掉它们的基类型
  • 依赖倒置原则:
    1. 高层模块不应该依赖低层模块,两者都应该依赖其抽象
    • 抽象不应该依赖细节
    • 细节应该依赖抽象
  • 接口隔离原则:
    1. 客户端不应该依赖它不需要的接口
    • 类间的依赖关系应该建立在最小的接口上
  • 迪米特法则:只与直接朋友进行通信

 简单总结上面的五大原则就是:单一职责原则告诉我们实现类要职责单一;里式替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向抽象编程;接口隔离原则告诉我们设计接口要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则告诉我们:要对修改关闭,对扩展开放。其实只要我们想一想,前面的五大原则一直反复强调的,几乎每一个原则都在强调的宗旨是什么:解耦,单一,高内聚——这不就是开闭原则的精神纲领吗。

把开闭原则应用于实际项目中,我们需要注意至关重要的一点:抽象约束
 抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多的可能性,可以跟随需求的变化而变化。因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义:

  • 通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法
  • 参数类型、引用对象尽量使用接口或者抽象类,而不是实现类
  • 抽象层尽量保持稳定,一旦确定即不允许修改

参考书籍与链接

  • 《设计模式之禅》
  • https://blog.csdn.net/zhengzhb/article/details/7296944

面向对象六大原则——迪米特法则

发表于 2018-04-28 | 分类于 面向对象六大原则 |
字数统计: 1,742 | 阅读时长 ≈ 7

什么是迪米特法则(Law of Demeter, LoD)

 迪米特法则也可以称为最少知识法则(Least Knowledge Principle, LKP)。它们都描述了一个规则:一个对象应该对其他对象有最少的了解。通俗来说,一个类应该对自己需要耦合或调用的类知道最少,也就是对于被依赖的类,向外公开的方法应该尽可能的少。

 迪米特法则还有一种解释:Only talk to your immediate friends,只与直接朋友进行通信。关于朋友给出如下解释:两个对象之间的耦合关系称之为朋友,通常有依赖,关联,聚合,组成等。而直接朋友通常表现为关联,聚合和组成关系,即两个对象之间联系更为紧密,通常以成员变量,方法参数和返回值的形式出现。

LoD实例演示

 迪米特法则强调了下面两点:

  • 从被依赖者的角度:只暴露应该暴露的方法或属性,即编写相关的类时确定方法和属性的权限
  • 从依赖者的角度来看,只依赖应该依赖的对象

先举例演示第一点:当我们按下计算机的按钮的时候,计算机会指行一系列操作:保存当前任务,关闭相关服务,接着关闭显示屏,最后关闭电源,这些操作完成则计算机才算关闭。如下是代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//计算机类
public class Computer{

public void saveCurrentTask(){
//do something
}
public void closeService(){
//do something
}
public void closeScreen(){
//do something
}

public void closePower(){
//do something
}

public void close(){
saveCurrentTask();
closeService();
closeScreen();
closePower();
}
}

//人
public class Person{
private Computer c;

...

public void clickCloseButton(){
//现在你要开始关闭计算机了,正常来说你只需要调用close()方法即可,
//但是你发现Computer所有的方法都是公开的,该怎么关闭呢?于是你写下了以下关闭的流程:
c.saveCurrentTask();
c.closePower();
c.close();

//亦或是以下的操作
c.closePower();

//还可能是以下的操作
c.close();
c.closePower();
}

}

 观察上面的代码我们发现了什么问题:对于人来说,我期待的结果只是按下关闭电钮然后计算机“啪”的给我关了,而不是需要我去小心的去保存当前正在执行的任务等等。在上面的代码中,c是一个完全暴露的对象,它的方法是完全公开的,对于Person来说,手里面就如同多出了好几把钥匙,至于具体用哪一把他不知道,所以只能一把一把的去试一遍,显然这样的设计是不对的。

 根据迪米特法则的第一点:从被依赖者的角度,只暴露应该暴露的方法。在本例中,应该暴露的方法就是close(),关于计算机的其他操作不是依赖者应该关注的问题,应该对依赖者关闭,重新设计如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//计算机类
public class Computer{

private void saveCurrentTask(){
//do something
}
private void closeService(){
//do something
}
private void closeScreen(){
//do something
}

private void closePower(){
//do something
}

public void close(){
saveCurrentTask();
closeService();
closeScreen();
closePower();
}
}

//人
public class Person{
private Computer c;
...

public void clickCloseButton(){
c.close();
}

}

现在举例演示第二点:在我们生活中会有这样的情况,比如张三去找李四帮忙做一件事,对于李四来说这件事也很棘手,李四也做不了,但是李四有一个好哥们王五却能完成这件事,所以李四就把这件事交给王五去办(在本例中,张三和王五是不认识的)。现在我们暂定张三为A,李四为B,王五为C,代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//张三找李四办事
public class A {
truepublic String name;
truepublic A(String name) {
truetruethis.name = name;
true}
truepublic B getB(String name) {
truetruereturn new B(name);
true}
truepublic void work() {
truetrueB b = getB("李四");
truetrueC c = b.getC("王五");
truetruec.work();
true}
}

//李四办不了于是去找王五
public class B {
trueprivate String name;
truepublic B(String name) {
truetruethis.name = name;
true}
truepublic C getC(String name) {
truetruereturn new C(name);
true}
}

//对于王五来说so easy,办得妥妥的
public class C {
truepublic String name;
truepublic C(String name) {
truetruethis.name = name;
true}
truepublic void work() {
truetrueSystem.out.println(name + "把这件事做好了");
true}
}

public class Client {
truepublic static void main(String[] args) {
truetrueA a = new A("张三");
truetruea.work();
true}
}
结果:王五把事情做好了

 上面的设计输出答案是正确的,王五确实把事情办妥了。但是我们仔细看业务逻辑确发现这样做事不对的。张三和王五互相不认识,那为什么代表张三的A类中会有代表李四的C类呢?这样明显是违背了迪米特法则的。现在我们对上面的代码进行重构,根据迪米特法则的第二点:从依赖者的角度来看,只依赖应该依赖的对象。在本例中,张三只认识李四,那么只能依赖李四。重构后代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//张三认识李四,只依赖李四
public class A {
truepublic String name;
truepublic A(String name) {
truetruethis.name = name;
true}
truepublic B getB(String name) {
truetruereturn new B(name);
true}
truepublic void work() {
truetrueB b = getB("李四");
truetrueb.work();
true}
}

//李四依赖王五
public class B {
trueprivate String name;
truepublic B(String name) {
truetruethis.name = name;
true}
truepublic C getC(String name) {
truetruereturn new C(name);
true}

truepublic void work(){
truetrueC c = getC("王五");
truetruec.work();
true}
}

//王五把事情办得妥妥的
public class C {
truepublic String name;
truepublic C(String name) {
truetruethis.name = name;
true}
truepublic void work() {
truetrueSystem.out.println(name + "把这件事做好了");
true}
}

public class Client {
truepublic static void main(String[] args) {
truetrueA a = new A("张三");
truetruea.work();
true}
}
结果:王五把事情做好了

总结

 迪米特法则的目的是让类之间解耦,降低耦合度,提高类的复用性。但是设计原则并非有利无弊,使用迪米特法则会产生大量的中转类或跳转类,导致系统复杂度提高。在实际的项目中,需要适度的考虑这个原则,不能因为套用原则而反而使项目设计变得复杂。

参考书籍与链接

  • 《设计模式之禅》
  • https://tianweili.github.io/2015/02/12/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%85%AD%E5%A4%A7%E5%8E%9F%E5%88%99-%E8%BF%AA%E7%B1%B3%E7%89%B9%E6%B3%95%E5%88%99/
  • https://blog.csdn.net/zhengzhb/article/details/7296930
  • https://www.jianshu.com/p/30931aab5ea0

面向对象六大原则——接口隔离原则

发表于 2018-04-27 | 分类于 面向对象六大原则 |
字数统计: 2,166 | 阅读时长 ≈ 8

什么是接口隔离原则(Interface Segregation Principle, ISP)

 接口对于Java开发者来说都不陌生,它几乎存在于每一个Java程序中,是抽象的代名词。在讲接口隔离原则之前,先说说接口,接口分为以下两种:

  • 实例接口(Object Interface): 在Java中声明一个类,然后用new关键字产生一个实例,是对一个类型的事物的描述,这就是一种接口。或许我们乍一看会有点懵,怎么和我们原来学习的接口不一样呢,其实我们这样想,我们都知道,在Java中有一个Class类,表示正在运行的类和接口,换句话说每一个正在运行时的类或接口都是Class类的对象,这是一种向上的抽象。接口是一种更为抽象的定义,类是一类相同事物的描述集合,那为什么不可以抽象为一个接口呢?
  • 类接口(Class Interface): 这就是我们经常使用的用interface定义的接口

 这里插一句,接口隔离原则中所说的接口并不是狭意的在Java中用interface定义的接口,而是一种更为宽泛的概念,可以是接口,抽象类或者实体类。

 接口隔离原则定义如下:

  • 客户端不应该依赖它不需要的接口
  • 类间的依赖关系应该建立在最小的接口上

 其实通俗来理解就是,不要在一个接口里面放很多的方法,这样会显得这个类很臃肿不堪。接口应该尽量细化,一个接口对应一个功能模块,同时接口里面的方法应该尽可能的少,使接口更加轻便灵活。或许看到接口隔离原则这样的定义很多人会觉得和单一职责原则很像,但是这两个原则还是有着很鲜明的区别。接口隔离原则和单一职责原则的审视角度是不同的,单一职责原则要求类和接口职责单一,注重的是职责,是业务逻辑上的划分,而接口隔离原则要求方法要尽可能的少,是在接口设计上的考虑。例如一个接口的职责包含10个方法,这10个方法都放在一个接口中,并且提供给多个模块访问,各个模块按照规定的权限来访问,并规定了“不使用的方法不能访问”,这样的设计是不符合接口隔离原则的,接口隔离原则要求“尽量使用多个专门的接口”,这里专门的接口就是指提供给每个模块的都应该是单一接口(即每一个模块对应一个接口),而不是建立一个庞大臃肿的接口来容纳所有的客户端访问。

接口隔离原则的使用

 在说接口隔离原则之前,我们先说一个没有使用该原则的例子,然后通过前后对比,来看看有什么不同之处。大家一听到“美女”这个字眼,会想到什么呢?别激动啊,我没啥意思,只是想给美女来定一个通用的标准:面貌,身材与气质,一般来说,长得好看的,身材不错的,气质出众的都可以称为美女。假如我现在是一个星探,下面我要设计一个找美女的类图:

 定义了一个IPettyGirl接口,声明所有的美女都应该有goodLooking,niceFigure,greatTemperament。还定义了一个抽象类AbstractSearcher,其作用就是搜索美女并显示其信息。下面是接口的定义与实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//美女接口
public interface IPettyGirl {
//要有姣好的面孔
public void goodLooking();
//要有好身材
public void niceFigure();
//要有气质
public void greatTemperament();
}

//接口的实现类
public class PettyGirl implements IPettyGirl {
private String name;

public PettyGirl(String name){
this.name= name;
}

//脸蛋漂亮
public void goodLooking() {
System.out.println(this.name + "---脸蛋很漂亮!");
}

//气质要好
public void greatTemperament() {
System.out.println(this.name + "---气质非常好!");
}

//身材要好
public void niceFigure() {
System.out.println(this.name + "---身材非常棒!");
}
}

美女有了,就需要星探出马找美女了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//星探的抽象
public abstract class AbstractSearcher {

protected IPettyGirl pettyGirl;

public AbstractSearcher(IPettyGirl pettyGirl){
this.pettyGirl = pettyGirl;
}
//搜索美女, 列出美女信息
public abstract void show();
}

//实现类
public class Searcher extends AbstractSearcher{

public Searcher(IPettyGirl pettyGirl){
super(pettyGirl);
}

//展示美女的信息
public void show(){
System.out.println("--------美女的信息如下: ---------------");
//展示面容
super.pettyGirl.goodLooking();
//展示身材
super.pettyGirl.niceFigure();
//展示气质
super.pettyGirl.greatTemperament();
}
}

//下面是客户端代码
public class Client {
//搜索并展示美女信息
public static void main(String[] args) {
//定义一个美女
IPettyGirl xiaoMei = new PettyGirl("小美");
AbstractSearcher searcher = new Searcher(yanYan);
searcher.show();
}
}

 OK,找美女的过程开发完毕,总体来说还是不错的,因为只要按照我们设计的原则来,那么找到的都是美女。但是这样的设计是最优的吗?现在考虑这样一种情况,由于人们审美的提高,或许颜值不一定是我们关注的主要因素,也许某个女生虽然颜值不是太高,但是气质很好,也可以把她称为美女。也有可能某些人喜欢身材匀称的,有的人觉得骨感一点好。也就是是说,美女的定义是可以宽泛话的,并非一成不变的。就如同我们的设计,必须符合我们定好的原则那才是美女,显然这是说不通的。所以上面的设计是有问题的,显然IPrettyGirl这个接口过于庞大了,根据接口隔离原则,星探AbstractSearcher应该依赖于具有部分特质的女孩子,但上面的设计却把这些特质都封装起来,放到一个接口中,这样就造成了封装过度,不容易扩展。


 现在我们找到了问题的原因,那就该接口隔离原则上场了。把原IPrettyGirl接口拆分为两个接口,一种是外形美女IGoodBodyGirl(相貌一流,身材不错,但气质可能差点),另一种是气质美女IGreatTemperamentGirl(也许外形条件不出众,但是谈吐优雅得体,气质很好)。下面是设计类图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public interface IGoodBodyGirl {
//要有姣好的面孔
public void goodLooking();
//要有好身材
public void niceFigure();
}

public interface IGreatTemperamentGirl {
//要有气质
public void greatTemperament();
}

public class PettyGirl implements IGoodBodyGirl,IGreatTemperamentGirl {
private String name;

public PettyGirl(String _name){
this.name=_name;
}

//脸蛋漂亮
public void goodLooking() {
System.out.println(this.name + "---脸蛋很漂亮!");
}

//气质要好
public void greatTemperament() {
System.out.println(this.name + "---气质非常好!");
}

//身材要好
public void niceFigure() {
System.out.println(this.name + "---身材非常棒!");
}
}

OK,现在经过重新设计,程序变得更加灵活,这就是接口隔离原则的强大之处。

ISP的几个使用原则

  • 根据接口隔离原则拆分接口时,首先必须满足单一职责原则: 没有哪个设计可以十全十美的考虑到所有的设计原则,有些设计原则之间就可能出现冲突,就如同单一职责原则和接口隔离原则,一个考虑的是接口的职责的单一性,一个考虑的是方法设计的专业性(尽可能的少),必然是会出现冲突。在出现冲突时,尽量以单一职责为主,当然这也要考虑具体的情况。
  • 提高高内聚: 提高接口,类,模块的处理能力,减少对外的交互。比如你给杀手提交了一个订单,要求他在一周之内杀一个人,一周后杀手完成了任务,这种不讲条件完成任务的表现就是高内聚。具体来说就是:要求在接口中尽量少公布public方法,接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险就越小,也有利于降低成本。
  • 定制服务: 单独为一个个体提供优良服务(只提供访问者需要的方法)。
  • 接口设计要有限度: 根据经验判断

参考书籍

  • 《设计模式之禅》

面向对象六大原则——依赖倒置原则

发表于 2018-04-26 | 分类于 面向对象六大原则 |
字数统计: 1,886 | 阅读时长 ≈ 7

什么是依赖倒置原则(Dependence Inversion Principle, DIP)

 依赖倒置原则的包含如下的三层含义:

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象
  • 抽象不应该依赖细节
  • 细节应该依赖抽象

 每一个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是低层模块(一般是接口,抽象类),原子逻辑的组装就是高层模块。在Java语言中,抽象就是指接口和或抽象类,两者都不能被直接实例化。细节就是实现类,实现接口或继承抽象类而产生的类就是细节,可以被直接实例化。下面是依赖倒置原则在Java语言中的表现:

  • 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的
  • 接口或抽象类不依赖于实现类
  • 实现类依赖于接口或抽象类

更为精简的定义:面向接口编程(Object-Oriented Design, OOD)

DIP的好处: 采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。

DIP的使用

 现在我们先不考虑依赖倒置原则,看一下如下的设计:

从上面的类图中可以看出,司机类和奔驰车类都属于细节,并没有实现或继承抽象,它们是对象级别的耦合。通过类图可以看出司机有一个drive()方法,用来开车,奔驰车有一个run()方法,用来表示车辆运行,并且奔驰车类依赖于司机类,用户模块表示高层模块,负责调用司机类和奔驰车类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Driver {
//司机的主要职责就是驾驶汽车
public void drive(Benz benz){
benz.run();
}
}

public class Benz {
//汽车肯定会跑
public void run(){
System.out.println("奔驰汽车开始运行...");
}
}

//高层模块
public class Client {
public static void main(String[] args) {
Driver xiaoLi = new Driver();
Benz benz = new Benz();
//小李开奔驰车
xiaoLi.drive(benz);
}
}

 这样的设计乍一看好像也没有问题,小李只管开着他的奔驰车就好。但是假如有一天他不想开奔驰了,想换一辆宝马车玩玩怎么办呢?我们当然可以新建一个宝马车类,也给它弄一个run()方法,但问题是,这辆车有是有了,但是小李却不能开啊。因为司机类里面并没有宝马车的依赖,所以小李空看着宝马车在那儿躺着,自己却没有钥匙,你说郁不郁闷呢?

1
2
3
4
5
6
public class BMW {
//宝马车当然也可以开动了
public void run(){
System.out.println("宝马汽车开始运行...");
}
}

 上面的设计没有使用依赖倒置原则,我们已经郁闷的发现,模块与模块之间耦合度太高,生产力太低,只要需求一变就需要大面积重构,说明这样的设计是不合理。现在我们引入依赖倒置原则,重新设计的类图如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//将司机模块抽象为一个接口
public interface IDriver {
//是司机就应该会驾驶汽车
public void drive(ICar car);
}

public class Driver implements IDriver{
//司机的主要职责就是驾驶汽车
public void drive(ICar car){
car.run();
}
}

//将汽车模块抽象为一个接口:可以是奔驰汽车,也可以是宝马汽车
public interface ICar {
//是汽车就应该能跑
public void run();
}

public class Benz implements ICar{
//汽车肯定会跑
public void run(){
System.out.println("奔驰汽车开始运行...");
}
}

public class BMW implements ICar{
//宝马车当然也可以开动了
public void run(){
System.out.println("宝马汽车开始运行...");
}
}

//高层模块
public class Client {
public static void main(String[] args) {
IDriver xiaoLi = new Driver();
ICar benz = new Benz();
//小李开奔驰车
xiaoLi.drive(benz);
}
}

 在新增低层模块时,只修改了高层模块(业务场景类),对其他低层模块(Driver类)不需要做任何修改,可以把”变更”的风险降低到最低。在Java中,只要定义变量就必然有类型,并且可以有两种类型:表面类型和实际类型,表面类型是在定义时赋予的类型,实际类型是对象的类型。就如上面的例子中,小李的表面类型是IDriver,实际类型是Driver。

 抽象是对实现的约束,是对依赖者的一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其目的就是保证所有的细节不脱离契约的范畴,确保约束双方按照规定好的契约(抽象)共同发展,只要抽象这条线还在,细节就脱离不了这个圈圈。就好比一场篮球比赛,已经定好了规则,大家如果按照规则来打球,那么会很愉快。但是假如大家脱离了规则,那么也许比赛就无法顺利进行。

DIP的几种写法

  • 接口声明依赖对象: 在接口的方法中声明依赖对象,就如上面的例子。

  • 构造函数传递依赖对象: 在类中通过构造函数声明依赖对象(好比Spring中的构造器注入),采用构造器注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//将司机模块抽象为一个接口
public interface IDriver {
public void drive();
}

public class Driver implements IDriver{
private ICar car;

//注入
public void Driver(ICar car){
this.car = car;
}

public void drive(ICar car){
this.car.run();
}
}
  • Setter方法传递依赖对象: 在抽象中设置Setter方法声明依赖对象(Spring中的方法注入)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface IDriver{
//注入依赖
public void setCar(ICar car);

public void drive();
}

public class Driver implements IDriver{
private ICar car;

public void setCar(ICar car){
this.car = car;
}

public void drive(){
this.car.run();
}
}

深入理解

 依赖倒置原则的本质就是通过抽象(抽象类或接口)使各个类或模块实现彼此独立,不互相影响,实现模块间的松耦合。在项目中使用这个规则需要以下原则;

  • 每个类尽量都要有接口或抽象类,或者抽象类和接口都有: 依赖倒置原则的基本要求,有抽象才能依赖倒置
  • 变量的表面类型尽量是接口或者抽象类
  • 任何类都不应该从具体类派生
  • 尽量不要重写基类已经写好的方法(里式替换原则)
  • 结合里式替换原则来使用: 结合里式替换原则和依赖倒置原则我们可以得出一个通俗的规则,接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。

一句话:依赖倒置原则的核心就是面向抽象(抽象类或者接口)编程

参考书籍

  • 《设计模式之禅》

面向对象六大原则——单一职责原则

发表于 2018-04-26 | 分类于 面向对象六大原则 |
字数统计: 1,282 | 阅读时长 ≈ 4

什么是单一职责原则(Single Responsibility Principle, SRP)

 在讲解什么是单一职责原则之前,我们先说一个例子,吊一下口味:我们在做项目的时候,会接触到用户,机构,角色管理这些模块,基本上使用的都是RBAC模型(Role-Based Access Control,基于角色的访问控制, 通过分配和取消角色来完成用户权限的授予和取消,使动作主体(用户)与资源的行为(权限)分离)。现在假设这样一种场景,我们把用户管理,修改用户信息,增加机构,增加角色等维护信息写到一个接口中进行管理,类图如下:

分析上面的类图我们会发现,这样的设计是非常不合理的,用户的属性和用户的行为是两种不同的业务模式,把它们都写在一个类中显然不行。我们应该把用户的信息抽取成一个BO(Business Object, 业务对象), 把行为抽取成一个Biz(Business Logic, 业务逻辑), 重新设计的类图如下:

SRP在类或接口中的使用

 SRP的原话是:There should never be more than one reason for a class to change.翻译过来其实也很好懂:应该有且仅有一个原因引起类的变更。看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
上面的的类图对应的接口入下
*/
public interface IPhone{
//拨通电话
public void dial(String phoneNumber);

//通话
public void chat(Object o);

//挂断电话
public void hangup();
}

在看到这个接口的时候,我们都会认为这样的设计是没有问题的,拨通电话,通话,挂断电话写在同一个接口里面并没有什么错。但是,我们仔细分析,这个接口真的没有问题吗?单一职责原则要求一个接口或类只有一个原因引起变化,也就是说一个接口或一个类只有一个原则,它就只负责一件事。 但我们分析上面这个接口,却发现它包含了两个职责:一个时协议管理,一个是数据传送。dial()和hangup()两个方法实现的是协议管理,分别是拨通电话和挂机。chat()实现的是数据传送,把我们说的话转换成模拟信号或数字信号传递给对方,然后再把对方传递过来的信号还原成我们听得懂的语言。这里的协议接通和数据传送的变化都会引起该接口或实现类的变化。我们想一想,这两个职责会相互影响吗?不管是什么协议,协议接通只负责将电话接通就行,而数据传输只需要传输数据,不必要去管协议是如何接通的。所以通过分析,IPhone接口包含了两个职责,而且这两个职责的变化不互相影响,这就可以考虑分成两个接口,类图如下:

观察上面的类图,我们发现这样的设计会比原来笼统的设计优雅的多,现在的设计在职责上比原来更加分明,让人一眼就能看出这个接口负责的是什么。也许有人会问,Phone这个类实现了两个接口,又把两个职责融合在了一个类中,那么是不是就有两个原因引起了它的变化了呢?别忘了,我们是面向接口编程,我们对外公布的是接口(API),并非实现类,给你提供了模板,在接口层面已经为你明确了职责,那么具体的实现怎么弄就需要开发者去考虑了。

SRP也适用于方法

 其实,单一职责原则不仅适用于类,接口,同样适用于方法中。这要举一个例子了,比如我们做项目的时候会遇到修改用户信息这样的功能模块,我们一般的想法是将用户的所有数据都接收过来,比如用户名,信息,密码,家庭地址等等,然后统一封装到一个User对象中提交到数据库,我们一般都是这么干的,就如下面这样:

 其实这样的方法是不可取的,因为职责不明确,方法不明确,你到底是要修改密码,还是修改用户名,还是修改地址,还是都要修改?这样职责不明确的话在与其他项目成员沟通的时候会产生很多麻烦,正确的设计如下:

SRP的优点

  • 类的复杂性降低,对于实现什么职责都有清晰明确的定义。
  • 可读性提高。
  • 可维护性提高。
  • 变更引起的风险降低,一个接口的修改只对相应的实现类有影响,对其他接口无影响,这对系统的扩展性,维护性都有非常大的帮助。

参考书籍

  • 《设计模式之禅》

面向对象六大原则——里式替换原则

发表于 2018-04-26 | 分类于 面向对象六大原则 |
字数统计: 2,560 | 阅读时长 ≈ 10

说说继承

 继承是面向对象三大特性之一,是一种非常优秀的语言机制,它有如下有点:

  • 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性
  • 提高代码的重用性
  • 子类可以形似父类
  • 提高代码的可扩展性
  • 提高产品或项目的开放性

 继承有它的优点,但是也有一些致命的缺点:

  • 继承具有侵入性,只要子类继承了父类,那么子类必须拥有父类的所有属性和方法
  • 降低了代码的灵活性
  • 增强了耦合性。当父类中发生方法,属性的修改时需要考虑子类是否修改,而且在缺乏规范的情况下,还可能发生大段的代码重构

 正如前面所说,继承是面向对象非常优良的特性,使用继承有利也有弊,如何将继承的利最大化,弊最小化呢(这就是为什么说在开发时多用组合,少用继承),解决方案就是引入里式替换原则。

举例说说继承的缺点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//需要完成一个两数相减的功能,由类A来负责
class A{
public int func1(int a, int b){
return a-b;
}
}

public class Client{
public static void main(String[] args){
A a = new A();
System.out.println("100-50="+a.func1(100, 50));
System.out.println("100-80="+a.func1(100, 80));
}
}
//结果:
100-50=50
100-80=20

//现在增加一个功能:完成两数相加,然后再与100求和,由类B来负责
class B extends A{
public int func1(int a, int b){
return a+b;
}

public int func2(int a, int b){
return func1(a,b)+100;
}
}

public class Client{
public static void main(String[] args){
B b = new B();
System.out.println("100-50="+b.func1(100, 50));
System.out.println("100-80="+b.func1(100, 80));
System.out.println("100+20+100="+b.func2(100, 20));
}
}
结果:
100-50=150
100-80=180
100+20+100=220

 我们发现原本运行正常的相减功能发生了错误。原因就是类B在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类B重写后的方法,造成原本运行正常的功能出现了错误。在本例中,引用基类A完成的功能,换成子类B之后,发生了异常。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。(违反了里式替换原则)

什么是里式替换原则(LiskovSubstitution Principle, LSP)

  • 第一种定义:如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型。

  • 第二种定义:子类型必须能替换掉它们的基类型

理解: 第二种定义相对来说更易于理解一些,通俗来说就是:只要父类出现的地方子类就可以出现,而且提换为子类也不会出现任何的错误和异常。但是反过来是不行的,有子类出现的地方,父类未必能替换。

LSP的深层含义

 里式替换原则为良好的继承定义了一个规范,它包含四个深层含义:

  • 子类必须完全实现父类的方法, 但不能覆盖(重写)父类的非抽象方法:这个规则相对来说是很好理解的,我们定义了一个接口或抽象类,我们必须在子类中完全实现所有的抽象方法,其实这时我们已经使用了里式替换原则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class A{
public abstract void run();
public abstract void fly();
public void walk(){
....
}
}
class B extends A{

@Override
public void run(){...}

@Override
public void fly(){...}
}

public calss test{
public static void main(String[] args){
A a = new B();
a.run();
a.fly();
a.walk();
}
}
  • 子类可以增加自己特有的方法

  • 当子类的方法重载父类的方法时,子类方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Father {
public Collection doSomething(HashMap map) {
System.out.println("父类被执行...");
return map.values();
}
}

public class Son extends Father {
// 放大输入参数类型
public Collection doSomething(Map map) {
System.out.println("子类被执行...");
return map.values();
}
}

public class Test {

public static void invoker() {
// 父类存在的地方,子类就应该能够存在
// Father f = new Father();
Son son = new Son();
HashMap map = new HashMap();
son.doSomething(map);
}

public static void main(String[] args){
invoker();
}

}
两个输出结果都是:父类被执行...

//假如将父类和子类的参数类型调换
则 f.doSomething(map) 输出结果为:父类被执行
son.doSomething(map) 输出结果为:子类被执行

 解释如下:在上面的例子中,子类中的doSomething(Map map)和父类中的doSomething(HashMap map)两个方法构成重载(并不是重写,因为参数列表不同,子类继承父类那么相应的父类方法就存在于子类的生命周期中,所以构成重载),而子类方法的形参范围比父类方法的形参范围要大。其实我们可以想一想,子类方法的形参范围比父类方法的形参范围要大,则子类代替父类传递参数到调用者中,子类的方法将永远不会被执行,这其实和里式交换原则是想符合的,父类的空间必须是子类的子区间,那么子类才能替换父类。而假如父类方法的形参范围大于子类方法的形参范围,子类方法在没有重写父类方法的前提下被执行了,这会引起业务逻辑的混乱,因为在实际应用中父类一般是抽象类,子类是实现类,你传递了一个这样的实现类就会“歪曲”父类的意图,引起一堆意想不到的逻辑混乱,所以子类中方法的前置条件必须与超类中被覆写的方法的前置条相同或更宽松。

  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更加严格: 如父类要求返回List,那么子类就应该返回List的实现ArrayList,父类是采用泛型,那么子类则不能采用泛型,而是具体的返回。

加深理解

  • 其实通俗说来,里式替换原则就是:子类可以扩展父类的功能,但不能改变父类原有的功能
  • 当继承不能满足里式替换原则时应该进行重构:
    • 把冲突的派生类与基类的公共部分提取出来作为一个抽象基类,然后分别继承这个类。
    • 改变继承关系:从父子关系变为委托关系
  • 在类中调用其他类时务必要使用父类或接口, 如果不能使用父类或接口, 则说明类的设计已经违背了LSP原则
  • 如果子类不能完整地实现父类的方法, 或者父类的某些方法在子类中已经发生“畸变”, 则建议断开父子继承关系, 采用依赖、 聚集、 组合等关系代替继承

多态与LSP是否矛盾

 在学习Java里面的多态时,我们知道多态的前提就是要有子类继承父类并且子类重写父类的方法。那这是否和LSP矛盾呢?因为LSP要求我们只可以扩展父类的功能,但不能改变父类原有的功能,也就是不能对父类原有的方法进行重写,只能去实现父类的方法或重载。下面是我在知乎上找到的一种比较合理的解释:

  • 里式替换原则是针对继承而言的,如果继承是为了实现代码的重用,也 就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。子类只能通过添加新的方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时就可以使用子类对象将父类对象替换掉。
  • 如果继承的目的是为了多态,而多态的前提就是子类重写父类的方法,为了符合LSP,我们应该将父类重新定义为抽象类,并定义抽象方法,让子类重新定义这些方法。由于父类是抽象类,所以父类不能被实例化,也就不存在可实例化的父类对象在程序里,就不存在子类替换父类时逻辑不一致的可能。

不符合LSP最常见的情况就是:父类和子类都是非抽象类,且父类的方法被子类重新定义,这样实现继承会造成子类和父类之间的强耦合,将不相关的属性和方法搅和在一起,不利于程序的维护和扩展。所以总结一句:尽量不要从可实例化的父类中继承,而是要使用基于抽象类和接口的继承(也就是面向接口和抽象编程)

参考书籍与网站

  • 《设计模式之禅》
  • https://blog.csdn.net/zhengzhb/article/details/7281833
  • https://blog.csdn.net/tjiyu/article/details/76551307

适配器模式——Adapter

发表于 2018-04-24 | 分类于 设计模式 |
字数统计: 2,541 | 阅读时长 ≈ 11

说说啥是适配器

 适配器模式是设计模式中比较好理解的设计模式之一。适配器,通俗来说就有点像生活中插座的转接头,你有一个三孔插座,但是你的电视插头却是两孔的,这时加上一个转接头就能让插头正常工作。其实,适配器模式的思想也就源于此,在面向对象的代码中,有很多可复用的类(经过反复测试可用),但是有时候我们去使用的时候却必须去改动一些地方,这就需要我们重新去测试,如此反复浪费时间与效率,于是大佬们就想出了一种设计模式——适配器模式,我们去做一个转接头,把那些可复用的类包装成目标对象不就可以了吗?于是,适配器模式应运而生。

适配器模式的定义

 Convert the interface of a class into another interface clients expect.Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.(将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。)Adapter模式又被称为Wrapper模式(包装器),它有以下两种:

  • 类适配器模式(使用继承的适配器) 类图如下:

  • 对象适配器模式(使用委托的适配器) 类图如下:

对适配器中各个登场角色的分析

  • Target对象:该对象定义把其他类转化为何种接口,也就是我们的期望接口。如下面案例中的IUserInfo接口就是目标对象
  • Adaptee源角色:被适配的对象,它是已经存在的,运转良好的类或对象。如下面案例中的IOuterUser接口
  • Adapter适配器角色:适配器模式的核心角色,通过类继承或类关联的方式将源角色转换为目标角色
  • Client角色:该角色负责使用Target角色所定义的方法进行具体处理

案例引入(来自《设计模式之禅》)——类适配器模式

 某公司做了一个人力资源管理项目,共分为三大模块:人员信息管理,薪酬管理,职位管理。其中,人员信息管理的对象是所有员工的所有信息(指在职的员工,离职退休的员工不考虑),人员信息管理类图如下:

接口设计如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//人员信息管理模块接口(包含员工的基本信息)
public interface IUserInfo {
//获得用户姓名
public String getUserName();
//获得家庭地址
public String getHomeAddress();
//手机号码
public String getMobileNumber();
//办公电话
public String getOfficeTelNumber();
//职位
public String getJobPosition();
//获得家庭电话
public String getHomeTelNumber();
}

 上面代码是信息管理模块的接口设计,具体实现类没有给出。现在遇到了一个问题,公司需要从劳动服务公司引进一部分员工解决公司劳动力不足问题,就需要将他们的基本信息比如:人员信息,工资情况,福利情况等同步到本公司的人力资源管理系统中来(人力资源部门要求我们的系统同步劳动服务公司这部分员工的信息),但是经过调研发现,劳动服务公司的人员对象和本公司系统的对象不相同,劳动服务公司人员信息管理类图如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//接口设计
public interface IOuterUser {
//基本信息, 比如名称、 性别、 手机号码等
public Map<String, String> getUserBaseInfo();

//工作区域信息
public Map<String, String> getUserOfficeInfo();

//用户的家庭信息
public Map<String, String> getUserHomeInfo();
}

//接口的实现类
public class OuterUser implements IOuterUser {
/*
* 用户的基本信息
*/
public Map<String, String> getUserBaseInfo() {
HashMap<String, String> baseInfoMap = new HashMap<>();
baseInfoMap.put("userName", "这个员工叫混世魔王...");
baseInfoMap.put("mobileNumber", "这个员工电话是...");
return baseInfoMap;
}

/
**
员工的家庭信息
*/
public Map getUserHomeInfo() {
HashMap<String, String> homeInfo = new HashMap<>();
homeInfo.put("homeTelNumbner", "员工的家庭电话是...");
homeInfo.put("homeAddress", "员工的家庭地址是...");
return homeInfo;
}

/
**
员工的工作信息, 比如, 职位等
*/
public Map<String, String> getUserOfficeInfo() {
HashMap officeInfo = new HashMap();
officeInfo.put("jobPosition","这个人的职位是BOSS...");
officeInfo.put("officeTelNumber", "员工的办公电话是...");
return officeInfo;
}
}

 分析如上设计我们发现:劳动服务公司将人员信息分为了三部分:基本信息,办公信息和个人家庭信息,并且都放到HashMap中。现在的问题是,本公司的人员信息管理系统如何和劳服公司的系统进行交互呢?这时可以进行这样的转化,先拿到对方的数据对象,然后转化为我们自己的数据对象,中间加一层数据转换处理,类图如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//适配器类OuterUserInfo的实现如下
public class OuterUserInfo extends OuterUser implements IUserInfo{

private Map<String, String> baseInfo = super.getUserBaseInfo();//员工的基本信息
private Map<String, String> homeInfo = super.getUserHomeInfo(); //员工的家庭信息
private Map<String, String> officeInfo = super.getUserOfficeInfo(); //员工的工作信息

/*
* 家庭地址
*/
public String getHomeAddress() {
String homeAddress = this.homeInfo.get("homeAddress");
System.out.println(homeAddress);
return homeAddress;
}

/*
*家庭电话号码
*/
public String getHomeTelNumber() {
String homeTelNumber = this.homeInfo.get("homeTelNumber");
System.out.println(homeTelNumber);
return homeTelNumber;
}

/*
*职位信息
*/
truepublic String getJobPosition() {
String jobPosition = this.officeInfo.get("jobPosition");
System.out.println(jobPosition);
return jobPosition;
}

/*
*手机号码
*/
truepublic String getMobileNumber() {
String mobileNumber = this.baseInfo.get("mobileNumber");
System.out.println(mobileNumber);
return mobileNumber;
}

/*
*办公电话
*/
truepublic String getOfficeTelNumber() {
String officeTelNumber = this.officeInfo.get("officeTelNumbe");
System.out.println(officeTelNumber);
return officeTelNumber;
}

/*
*员工的名称
*/
truepublic String getUserName() {
String userName = this.baseInfo.get("userName");
System.out.println(userName);
return userName;
}

}

//主类如下
public class Client {

public static void main(String[] args) {

//1.没有与外系统共享时
IUserInfo girl = new UserInfo();

girl.getMobileNumber();

//2.与外系统共享
IUserInfo girl2 = new OuterUserInfo();

girl2.getMobileNumber();

}

}

这就是适配器的强大之处,通过使用适配器,我们几乎不用对原来的系统和要包装的系统做任何修改,就能将两个系统很完美的拼接在一起,只要在主类修改一句话就能解决问题。

使用对象适配器模式

 我们还是使用上面的案例,我们想一想,假如劳动服务公司给的员工信息接口是分开的,比如基本信息一个接口,家庭信息一个接口等有多个接口的情况,我们还能像上面那样做吗?当然不行,因为Java是不支持多继承的,我们可以使用委托(也就是一种关联关系)来达到目的,这就是适配器模式的另一种方式:对象适配器模式。类图如下:

各个接口和实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public interface IOuterUserBaseInfo {
//基本信息, 比如名称、 性别、 手机号码等
public Map<String, String> getUserBaseInfo();
}

public interface IOuterUserHomeInfo {
//用户的家庭信息
public Map<String, String> getUserHomeInfo();
}

public interface IOuterUserOfficeInfo {
//工作区域信息
public Map<String, String> getUserOfficeInfo();
}


public class OuterUserBaseInfo implements IOuterUserBaseInfo {
/*
* 用户的基本信息
*/
public Map<String, String> getUserBaseInfo() {
HashMap<String, String> baseInfoMap = new HashMap<>();
baseInfoMap.put("userName", "这个员工叫混世魔王...");
baseInfoMap.put("mobileNumber", "这个员工电话是...");
return baseInfoMap;
}

public class OuterUserHomeInfo implements IOuterUserHomeInfo {
/*
* 员工的家庭信息
*/
public Map<String, String> getUserHomeInfo() {
HashMap<>String, String> homeInfo = new HashMap<>();
homeInfo.put("homeTelNumbner", "员工的家庭电话是...");
homeInfo.put("homeAddress", "员工的家庭地址是...");
return homeInfo;
}
}

public class OuterUserOfficeInfo implements IOuterUserOfficeInfo {
/*
* 员工的工作信息, 比如, 职位等
*/
public Map<String, String> getUserOfficeInfo() {
HashMap<String, String> officeInfo = new HashMap<>();
officeInfo.put("jobPosition","这个人的职位是BOSS...");
officeInfo.put("officeTelNumber", "员工的办公电话是...");
return officeInfo;
}
}
}

下面是适配器及主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class OuterUserInfo implements IUserInfo {

//源目标对象
private IOuterUserBaseInfo baseInfo = null; //员工的基本信息
private IOuterUserHomeInfo homeInfo = null; //员工的家庭信息
private IOuterUserOfficeInfo officeInfo = null; //工作信息

//数据处理
private Map<String, String> baseMap = null;
private Map<String, String> homeMap = null;
private Map<String, String> officeMap = null;

//构造函数传递对象
public OuterUserInfo(IOuterUserBaseInfo baseInfo,IOuterUserHomeInfo homeInfo,
IOuterUserOfficeInfo officeInfo){
this.baseInfo = baseInfo;
this.homeInfo = homeInfo;
this.officeInfo = officeInfo;
//数据处理
this.baseMap = this.baseInfo.getUserBaseInfo();
this.homeMap = this.homeInfo.getUserHomeInfo();
this.officeMap = this.officeInfo.getUserOfficeInfo();
}

//家庭地址
public String getHomeAddress() {
String homeAddress = this.homeMap.get("homeAddress");
System.out.println(homeAddress);
return homeAddress;
}

//家庭电话号码
public String getHomeTelNumber() {
String homeTelNumber = this.homeMap.get("homeTelNumber");
System.out.println(homeTelNumber);
return homeTelNumber;
}

//职位信息
public String getJobPosition() {
String jobPosition = this.officeMap.get("jobPosition");
System.out.println(jobPosition);
return jobPosition;
}

//手机号码
public String getMobileNumber() {
String mobileNumber = this.baseMap.get("mobileNumber");
System.out.println(mobileNumber);
return mobileNumber;
}

//办公电话
public String getOfficeTelNumber() {
String officeTelNumber= this.officeMap.get("officeTelNumber"
System.out.println(officeTelNumber);
return officeTelNumber;
}

//员工的名称
public String getUserName() {
String userName = this.baseMap.get("userName");
System.out.println(userName);
return userName;
}
}


//主类如下:
public class Client {
public static void main(String[] args) {
//外系统的人员信息
IOuterUserBaseInfo baseInfo = new OuterUserBaseInfo();
IOuterUserHomeInfo homeInfo = new OuterUserHomeInfo();
IOuterUserOfficeInfo officeInfo = new OuterUserOfficeInfo();
//传递三个对象
IUserInfo girl = new OuterUserInfo(baseInfo,homeInfo,officeInfo)

girl.getMobileNumber();
}

适配器模式的优点

  • 适配器模式可以让两个没有任何关系的类在一起运行
  • 增加了类的透明性
  • 提高了类的复用度:源角色在原有的系统中还可以正常使用,在目标角色中也可以充当新的演员
  • 具有很高的灵活性

注意: 在使用适配器模式时,项目一定要遵循依赖倒置原则和里式替换原则。

参考书籍

  • 《设计模式之禅》
  • 《图解设计模式》
12
雅客

雅客

一切并非毫无意义

16 日志
6 分类
18 标签
RSS
GitHub
© 2018 雅客
本站访客数:
博客全站共35.1k字