0%

异常

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
什么是异常?
异常是程序在"编译"或者"执行"的过程中可能出现的问题。
异常是应该尽量提前避免的。
异常可能也是无法做到绝对避免的,异常可能有太多情况了,开发中只能提前干预!!
异常一旦出现了,如果没有提前处理,程序就会退出JVM虚拟机而终止,开发中异常是需要提前处理的。

研究异常并且避免异常,然后提前处理异常,体现的是程序的安全, 健壮性!!!

Java会为常见的代码异常都设计一个类来代表。

异常的体系:
Java中异常继承的根类是:Throwable。

Throwable(根类,不是异常类)
/ \
Error Exception(异常,需要研究和处理)
/ \
编译时异常 RuntimeException(运行时异常)


Error : 错误的意思,严重错误Error,无法通过处理的错误,一旦出现,程序员无能为力了,
只能重启系统,优化项目。
比如内存奔溃,JVM本身的奔溃。这个程序员无需理会。

Exception:才是异常类,它才是开发中代码在编译或者执行的过程中可能出现的错误,
它是需要提前处理的。以便程序更健壮!

Exception异常的分类:
1.编译时异常:继承自Exception的异常或者其子类,编译阶段就会报错,
必须程序员处理的。否则代码编译就不能通过!!

2.运行时异常: 继承自RuntimeException的异常或者其子类,编译阶段是不会出错的,它是在
运行时阶段可能出现,运行时异常可以处理也可以不处理,编译阶段是不会出错的,
但是运行阶段可能出现,还是建议提前处理!!
小结:
异常是程序在编译或者运行的过程中可能出现的错误!!
异常分为2类:编译时异常,运行时异常。
-- 编译时异常:继承了Exception,编译阶段就报错,必须处理,否则代码不通过。
-- 运行时异常:继承了RuntimeException,编译阶段不会报错,运行时才可能出现。
异常一旦真的出现,程序会终止,所以要研究异常,避免异常,处理异常,程序更健壮!!

常见的运行时异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
运行时异常的概念:
继承自RuntimeException的异常或者其子类,
编译阶段是不会出错的,它是在运行时阶段可能出现的错误,
运行时异常编译阶段可以处理也可以不处理,代码编译都能通过!!

1.数组索引越界异常: ArrayIndexOutOfBoundsException。
2.空指针异常 : NullPointerException。
直接输出没有问题。但是调用空指针的变量的功能就会报错!!
3.类型转换异常:ClassCastException。
4.迭代器遍历没有此元素异常:NoSuchElementException。
5.数学操作异常:ArithmeticException。
6.数字转换异常: NumberFormatException。

小结:
运行时异常继承了RuntimeException ,编译阶段不报错,运行时才可能会出现错误!
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 ExceptionDemo {
public static void main(String[] args) {
System.out.println("程序开始。。。。。。");
/** 1.数组索引越界异常: ArrayIndexOutOfBoundsException。*/
int[] arrs = {10 ,20 ,30};
System.out.println(arrs[2]);
// System.out.println(arrs[3]); // 此处出现了数组索引越界异常。代码在此处直接执行死亡!

/** 2.空指针异常 : NullPointerException。直接输出没有问题。但是调用空指针的变量的功能就会报错!! */
String name = null ;
System.out.println(name); // 直接输出没有问题
// System.out.println(name.length()); // 此处出现了空指针异常。代码在此处直接执行死亡!

/** 3.类型转换异常:ClassCastException。 */
Object o = "齐天大圣";
//Integer s = (Integer) o; // 此处出现了类型转换异常。代码在此处直接执行死亡!


/** 5.数学操作异常:ArithmeticException。 */
// int c = 10 / 0 ; // 此处出现了数学操作异常。代码在此处直接执行死亡!


/** 6.数字转换异常: NumberFormatException。 */
String num = "23aa";
Integer it = Integer.valueOf(num); // 此处出现了数字转换异常。代码在此处直接执行死亡!
System.out.println(it+1);

System.out.println("程序结束。。。。。。");
}
}

常见的编译时异常认识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
编译时异常:继承自Exception的异常或者其子类,没有继承RuntimeException
"编译时异常是编译阶段就会报错",
必须程序员编译阶段就处理的。否则代码编译就报错!!

编译时异常的作用是什么:
是担心程序员的技术不行,在编译阶段就爆出一个错误, 目的在于提醒!
提醒程序员这里很可能出错,请检查并注意不要出bug。

编译时异常是可遇不可求。遇到了就遇到了呗。
小结:
编译时异常是编译阶段就会报错的,继承了Exception,编译时
异常是可遇不可求。遇到了就遇到了呗。

编译时异常编译阶段必须处理,否则代码编译不通过!!

异常处理过程解析

异常的产生默认

1
2
3
4
5
6
7
8
9
10
11
12
(自动处理的过程!)
(1)默认会在出现异常的代码那里自动的创建一个异常对象:ArithmeticException。
(2)异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给JVM虚拟机。
(3)虚拟机接收到异常对象后,先在控制台直接输出异常栈信息数据。
(4)直接从当前执行的异常点干掉当前程序。
(5)后续代码没有机会执行了,因为程序已经死亡。

小结:
异常一旦出现,会自动创建异常对象,最终抛出给虚拟机,虚拟机
只要收到异常,就直接输出异常信息,干掉程序!!

默认的异常处理机制并不好,一旦真的出现异常,程序立即死亡!

编译时异常的处理

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
方式一:
编译时异常:编译阶段就会报错,一定需要程序员处理的,否则代码无法通过!!

抛出异常格式:
方法 throws 异常1 , 异常2 , ..{

}
建议抛出异常的方式:代表可以抛出一切异常,
方法 throws Exception{

}
方式一:
在出现编译时异常的地方层层把异常抛出去给调用者,调用者最终抛出给JVM虚拟机。
JVM虚拟机输出异常信息,直接干掉程序,这种方式与默认方式是一样的。
虽然可以解决代码编译时的错误,但是一旦运行时真的出现异常,程序还是会立即死亡!
这种方式并不好!

小结:
编译时异常编译阶段就报错,必须程序员处理。
方式一:出现异常的地方层层抛出,谁都不处理,最终抛出给虚拟机。
这种方式虽然可以解决编译时异常,但是如果异常真的出现了,程序会直接死亡,所以这种方式并不好!


方式二:在出现异常的地方自己处理,谁出现谁处理。
自己捕获异常和处理异常的格式:捕获处理
try{
// 监视可能出现异常的代码!
}catch(异常类型1 变量){
// 处理异常
}catch(异常类型2 变量){
// 处理异常
}...
监视捕获处理异常企业级写法:
try{
// 可能出现异常的代码!
}catch (Exception e){
e.printStackTrace(); // 直接打印异常栈信息
}
Exception可以捕获处理一切异常类型!
小结:
第二种方式,可以处理异常,并且出现异常后代码也不会死亡。
这种方案还是可以的。
但是从理论上来说,这种方式不是最好的,上层调用者不能直接知道底层的执行情况!


方式三: 在出现异常的地方把异常一层一层的抛出给最外层调用者,
最外层调用者集中捕获处理!!(规范做法)
小结:
编译时异常的处理方式三:底层出现的异常抛出给最外层调用者集中捕获处理。
这种方案最外层调用者可以知道底层执行的情况,同时程序在出现异常后也不会立即死亡,这是
理论上最好的方案。
虽然异常有三种处理方式,但是开发中只要能解决你的问题,每种方式都又可能用到!!

运行时异常的处理机制

1
2
3
4
5
6
7
8
9
10
11
12
运行时异常在编译阶段是不会报错,在运行阶段才会出错。
运行时异常在编译阶段不处理也不会报错,但是运行时如果出错了程序还是会死亡
所以运行时异常也建议要处理。

运行时异常是自动往外抛出的,不需要我们手工抛出。

运行时异常的处理规范:直接在最外层捕获处理即可,底层会自动抛出!!

小结:
运行时异常编译阶段不报错,可以处理也可以不处理,建议处理!!
运行时异常可以自动抛出,不需要我们手工抛出。
运行时异常的处理规范:直接在最外层捕获处理即可,底层会自动抛出!!

finally关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
用在捕获处理的异常格式中的,放在最后面。
try{
// 可能出现异常的代码!
}catch(Exception e){
e.printStackTrace();
}finally{
// 无论代码是出现异常还是正常执行,最终一定要执行这里的代码!!
}
try: 1次。
catch:0-N次 (如果有finally那么catch可以没有!!)
finally: 0-1次

finally的作用: 可以在代码执行完毕以后进行资源的释放操作。
什么是资源?资源都是实现了Closeable接口的,都自带close()关闭方法!!

异常的语法注意

1
2
3
4
5
- 运行时异常被抛出可以不处理。可以自动抛出,编译时异常必须处理.按照规范都应该处理!
- 重写方法申明抛出的异常,应该与父类被重写方法申明抛出的异常一样或者范围更小
- 方法默认都可以自动抛出运行时异常! throws RuntimeException可以省略不写!!
- 当多异常处理时,捕获处理,前边的异常类不能是后边异常类的父类。
- 在try/catch后可以追加finally代码块,其中的代码一定会被执行,通常用于资源回收操作。

自定义异常(了解)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
引入:Java已经为开发中可能出现的异常都设计了一个类来代表.
但是实际开发中,异常可能有无数种情况,Java无法为
这个世界上所有的异常都定义一个代表类。
假如一个企业如果想为自己认为的某种业务问题定义成一个异常
就需要自己来自定义异常类.

需求:认为年龄小于0岁,大于200岁就是一个异常。

自定义异常:
自定义编译时异常.
a.定义一个异常类继承Exception.
b.重写构造器。
c.在出现异常的地方用throw new 自定义对象抛出!
编译时异常是编译阶段就报错,提醒更加强烈,一定需要处理!!

自定义运行时异常.
a.定义一个异常类继承RuntimeException.
b.重写构造器。
c.在出现异常的地方用throw new 自定义对象抛出!
提醒不强烈,编译阶段不报错!!运行时才可能出现!!
小结:
自定义异常是程序员自己定义的异常
继承Exception/RuntimeException,重写构造器。
在出现异常的地方用throw new 自定义异常对象抛出!

Git

基础操作指令

命令如下:

  1. clone(克隆): 从远程仓库中克隆代码到本地仓库

  2. checkout (检出):从本地仓库中检出一个仓库分支然后进行修订

  3. add(添加): 在提交前先将代码提交到暂存区

  4. commit(提交): 提交到本地仓库。本地仓库中保存修改的各个历史版本

  5. fetch (抓取) : 从远程库,抓取到本地仓库,不进行任何的合并动作,一般操作比较少。

  6. pull (拉取) : 从远程库拉到本地库,自动进行合并(merge),然后放到到工作区,相当于
    fetch+merge

  7. push(推送) : 修改完成后,需要和团队成员共享代码时,将代码推送到远程仓库

查看修改的状态(status)

作用:查看的修改的状态(暂存区、工作区)
命令形式:git status

添加工作区到暂存区(add)

作用:添加工作区一个或多个文件的修改到暂存区
命令形式:git add 单个文件名|通配符

​ · 将所有修改加入暂存区:git add .

提交暂存区到本地仓库(commit)

作用:提交暂存区内容到本地仓库的当前分支
命令形式:git commit -m ‘注释内容’

查看提交日志(log)

在3.1.3中配置的别名git-log 就包含了这些参数,所以后续可以直接使用指令git-log

作用:查看提交记录
命令形式:git log [option]

options
–all 显示所有分支
–pretty=oneline 将提交信息显示为一行
–abbrev-commit 使得输出的commitId更简短
–graph 以图的形式显示

版本回退

作用:版本切换
命令形式:git reset –hard commitID
commitID 可以使用git-log 或git log 指令查看
如何查看已经删除的记录?
git reflog
这个指令可以看到已经删除的提交记录

添加文件至忽略列表

一般我们总会有些文件无需纳入Git 的管理,也不希望它们总出现在未跟踪文件列表。 通常都是些自动
生成的文件,比如日志文件,或者编译过程中创建的临时文件等。 在这种情况下,我们可以在工作目录
中创建一个名为 .gitignore 的文件(文件名称固定),列出要忽略的文件模式。下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
# no .a files
*.a
# but do track lib.a, even though you're ignoring .a files above
!lib.a
# only ignore the TODO file in the current directory, not subdir/TODO
/TODO
# ignore all files in the build/ directory
build/
# ignore doc/notes.txt, but not doc/server/arch.txt
doc/*.txt
# ignore all .pdf files in the doc/ directory
doc/**/*.pdf

分支

查看本地分支

git branch

创建本地分支

git branch 分支名

切换分支(checkout)

git checkout 分支名

我们还可以直接切换到一个不存在的分支(创建并切换)

git checkout -b 分支名

合并分支(merge)

一个分支上的提交可以合并到另一个分支

git merge 分支名称

删除分支

不能删除当前分支,只能删除其他分支

删除分支时,需要做各种检查

git branch -d b1

不做任何检查,强制删除

git branch -D b1

开发中分支使用原则与流程

在开发中,一般有如下分支使用原则与流程:

master (生产) 分支
线上分支,主分支,中小规模项目作为线上运行的应用对应的分支;

develop(开发)分支
是从master创建的分支,一般作为开发部门的主要开发分支,如果没有其他并行开发不同期上线
要求,都可以在此版本进行开发,阶段开发完成后,需要是合并到master分支,准备上线。

feature/xxxx分支
从develop创建的分支,一般是同期并行开发,但不同期上线时创建的分支,分支上的研发任务完
成后合并到develop分支。

hotfix/xxxx分支,
从master派生的分支,一般作为线上bug修复使用,修复完成后需要合并到master、test、

develop分支。
还有一些其他分支,在此不再详述,例如test分支(用于代码测试)、pre分支(预上线分支)等

解决冲突

当两个分支上对文件的修改可能会存在冲突,例如同时修改了同一个文件的同一行,这时就需要手动解
决冲突,解决冲突步骤如下

  1. 处理文件中冲突的地方

  2. 将解决完冲突的文件加入暂存区(add)

  3. 提交到仓库(commit)

    冲突部分的内容处理如下所示

解决GitBash乱码问题

  1. 打开GitBash执行下面命令

    1
    git config --global core.quotepath false
  2. ${git_home}/etc/bash.bashrc 文件最后加入下面两行

    1
    2
    export LANG="zh_CN.UTF-8"
    export LC_ALL="zh_CN.UTF-8"

Collection集合概述

什么是集合

集合是一个大小可变的容器。
容器中的每个数据称为一个元素。数据==元素。
集合的特点是:类型可以不确定,大小不固定。集合有很多种,不同的集合特点和使用场景不同。

​ 数组:类型和长度一旦定义出来就都固定了。

集合有啥用

在开发中,很多时候元素的个数是不确定的。
而且经常要进行元素的增删该查操作,集合都是非常合适的。
开发中集合用的更多!!

Java中集合的代表是:Collection.
Collection集合是Java中集合的祖宗类。
学习Collection集合的功能,那么一切集合都可以用这些功能!!

Collection集合的体系:
                        Collection<E>(接口)
                  /                                \
             Set<E>(接口)                            List<E>(接口)
            /               \                       /                \
         HashSet<E>(实现类)  TreeSet<>(实现类)     ArrayList<E>(实现类)  LinekdList<>(实现类)
         /
     LinkedHashSet<>(实现类)

集合的特点

Set系列集合:添加的元素是无序,不重复,无索引的。
– HashSet:添加的元素是无序,不重复,无索引的。
– LinkedHashSet:添加的元素是有序,不重复,无索引的。
– TreeSet:不重复,无索引,按照大小默认升序排序!!
List系列集合:添加的元素是有序,可重复,有索引。
– ArrayList:添加的元素是有序,可重复,有索引。底层基于数组存储数据的,查询快,增删慢
– LinekdList:添加的元素是有序,可重复,有索引。底层是基于链表存储数据的,查询慢,增删快

小结:
Collection是集合的祖宗类,Collection集合的功能是一切集合都可以直接使用的。

Collection集合的常用API

Collection是集合的祖宗类,它的功能是全部集合都可以继承使用的,所以要学习它。
Collection API如下:
public boolean add(E e): 把给定的对象添加到当前集合中 。
public void clear() :清空集合中所有的元素。
public boolean remove(E e): 把给定的对象在当前集合中删除。
public boolean contains(Object obj): 判断当前集合中是否包含给定的对象。
public boolean isEmpty(): 判断当前集合是否为空。
public int size(): 返回集合中元素的个数。
public Object[] toArray(): 把集合中的元素,存储到数组中

Collection集合的遍历方式

什么是遍历? 为什么开发中要遍历?
遍历就是一个一个的把容器中的元素访问一遍。
开发中经常要统计元素的总和,找最值,找出某个数据然后干掉等等业务都需要遍历。

Collection集合的遍历方式是全部集合都可以直接使用的,所以我们学习它。
Collection集合的遍历方式有三种:
    (1)迭代器。
    (2)foreach(增强for循环)。
    (3)JDK 1.8开始之后的新技术Lambda表达式(了解)

a.迭代器遍历集合。
    -- 方法:
        public Iterator iterator(): 获取集合对应的迭代器,用来遍历集合中的元素的
        E next():获取下一个元素值!
        boolean hasNext():判断是否有下一个元素,有返回true ,反之。
    --流程:
        1.先获取当前集合的迭代器
            Iterator<String> it = lists.iterator();
        2.定义一个while循环,问一次取一次。
          通过it.hasNext()询问是否有下一个元素,有就通过
          it.next()取出下一个元素。
1
2
3
4
5
6
7
8
9
10
b.foreach(增强for循环)遍历集合。
foreach是一种遍历形式,可以遍历集合或者数组。
foreach遍历集合实际上是迭代器遍历的简化写法。
foreach遍历的关键是记住格式:
for(被遍历集合或者数组中元素的类型 变量名称 : 被遍历集合或者数组){

}
小结:
foreach遍历集合或者数组很方便。
缺点:foreach遍历无法知道遍历到了哪个元素了,因为没有索引。
1
c.JDK 1.8开始之后的新技术Lambda表达式。

常见的数据结构种类

1
2
3
4
5
6
7
8
集合是基于数据结构做出来的,不同的集合底层会采用不同的数据结构。
不同的数据结构,功能和作用是不一样的。

什么是数据结构?
数据结构指的是数据以什么方式组织在一起。
不同的数据结构,增删查的性能是不一样的。
不同的集合底层会采用不同的数据结构,我们要知道集合的底层是基于哪种数据结构存储和操作数据的。
这样才能知道具体场景用哪种集合。

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
数据存储的常用结构有:栈、队列、数组、链表和红黑树
a.队列(queue)
-- 先进先出,后进后出。
-- 场景:各种排队。叫号系统。
-- 有很多集合可以实现队列。

b.栈(stack)
-- 后进先出,先进后出
-- 压栈 == 入栈
-- 弹栈 == 出栈
-- 场景:手枪的弹夹。


c.数组
-- 数组是内存中的连续存储区域。
-- 分成若干等分的小区域(每个区域大小是一样的)
-- 元素存在索引
-- 特点:查询元素快(根据索引快速计算出元素的地址,然后立即去定位)
增删元素慢(创建新数组,迁移元素)

d.链表
-- 元素不是内存中的连续区域存储。
-- 元素是游离存储的。每个元素会记录下个元素的地址。
-- 特点:查询元素慢
增删元素快(针对于首尾元素,速度极快,一般是双链表)

e.红黑树
二叉树:binary tree 永远只有一个根节点,是每个结点不超过2个节点的树(tree) 。
查找二叉树,排序二叉树:小的左边,大的右边,但是可能树很高,性能变差。
为了做排序和搜索会进行左旋和右旋实现平衡查找二叉树,让树的高度差不大于1
红黑树(就是基于红黑规则实现了自平衡的排序二叉树):
树尽量的保证到了很矮小,但是又排好序了,性能最高的树。

红黑树的增删查改性能都好!!!

List集合

ArrayList集合

1
2
3
4
5
6
7
8
9
10
11
List集合继承了Collection集合的全部功能,同时因为List系列集合有索引,
因为List集合多了索引,所以多了很多按照索引操作元素的功能:
ArrayList实现类集合底层基于数组存储数据的,查询快,增删慢!
- public void add(int index, E element): 将指定的元素,添加到该集合中的指定位置上。
- public E get(int index):返回集合中指定位置的元素。
- public E remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素。
- public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回更新前的元素值。
小结:
List系列集合有序,可重复,有索引的。
ArrayList实现类集合底层基于数组存储数据的,查询快,增删慢!!
开发中ArrayList集合用的最多!!

List系列集合的遍历方式有:4种

1
2
3
4
5
6
7
List系列集合多了索引,所以多了一种按照索引遍历集合的for循环。

List遍历方式:
(1)for循环。
(2)迭代器。
(3)foreach。
(4)JDK 1.8新技术。

LinkedList集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
LinkedList也是List的实现类:底层是基于链表的,增删比较快,查询慢!!
LinkedList是支持双链表,定位前后的元素是非常快的,增删首尾的元素也是最快的
所以LinkedList除了拥有List集合的全部功能还多了很多操作首尾元素的特殊功能:
- public void addFirst(E e):将指定元素插入此列表的开头。
- public void addLast(E e):将指定元素添加到此列表的结尾。
- public E getFirst():返回此列表的第一个元素。
- public E getLast():返回此列表的最后一个元素。
- public E removeFirst():移除并返回此列表的第一个元素。
- public E removeLast():移除并返回此列表的最后一个元素。
- public E pop():从此列表所表示的堆栈处弹出一个元素。
- public void push(E e):将元素推入此列表所表示的堆栈。

小结:
LinkedList是支持双链表,定位前后的元素是非常快的,增删首尾的元素也是最快的。
所以提供了很多操作首尾元素的特殊API可以做栈和队列的实现。

如果查询多而增删少用ArrayList集合。(用的最多的)
如果查询少而增删首尾较多用LinkedList集合。

Set集合

HashSet集合

1
2
3
研究两个问题(面试热点):
1)Set集合添加的元素是不重复的,是如何去重复的?
2)Set集合元素无序的原因是什么?

Set系列集合元素去重复的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
集合和泛型都只能支持引用数据类型。

1.对于有值特性的,Set集合可以直接判断进行去重复。
2.对于引用数据类型的类对象,Set集合是按照如下流程进行是否重复的判断。
Set集合会让两两对象,先调用自己的hashCode()方法得到彼此的哈希值(所谓的内存地址)
然后比较两个对象的哈希值是否相同,如果不相同则直接认为两个对象不重复。
如果哈希值相同,会继续让两个对象进行equals比较内容是否相同,如果相同认为真的重复了
如果不相同认为不重复。

Set集合会先让对象调用hashCode()方法获取两个对象的哈希值比较
/ \
false true
/ \
不重复 继续让两个对象进行equals比较
/ \
false true
/ \
不重复 重复了

需求:只要对象内容一样,就希望集合认为它们重复了。重写hashCode和equals方法。

小结:
如果希望Set集合认为两个对象只要内容一样就重复了,必须重写对象的hashCode和equals方法。

Set系列集合元素无序的根本原因。(面试必考)

1
2
3
4
5
6
7
8
9
Set系列集合添加元素无序的根本原因是因为底层采用了哈希表存储元素。

JDK 1.8之前:哈希表 = 数组 + 链表 + (哈希算法)
JDK 1.8之后:哈希表 = 数组 + 链表 + 红黑树 + (哈希算法)
当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

小结:
Set系列集合是基于哈希表存储数据的
它的增删改查的性能都很好!!但是它是无序不重复的!如果不在意当然可以使用!

LinkedHashSet集合

1
2
3
4
5
6
7
8
9
10
11
是HashSet的子类,元素是“有序” 不重复,无索引.

LinkedHashSet底层依然是使用哈希表存储元素的,
但是每个元素都额外带一个链来维护添加顺序!!
不光增删查快,还有序。缺点是多了一个存储顺序的链会占内存空间!!而且不允许重复,无索引。

总结:
如果希望元素可以重复,又有索引,查询要快用ArrayList集合。(用的最多)
如果希望元素可以重复,又有索引,增删要快要用LinkedList集合。(适合查询元素比较少的情况,经常要首尾操作元素的情况)
如果希望增删改查都很快,但是元素不重复以及无序无索引,那么用HashSet集合。
如果希望增删改查都很快且有序,但是元素不重复以及无索引,那么用LinkedHashSet集合。

TreeSet集合

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
TreeSet: 不重复,无索引,按照大小默认升序排序!!
TreeSet集合称为排序不重复集合,可以对元素进行默认的升序排序。

TreeSet集合自自排序的方式:
1.有值特性的元素直接可以升序排序。(浮点型,整型)
2.字符串类型的元素会按照首字符的编号排序。
3.对于自定义的引用数据类型,TreeSet默认无法排序,执行的时候直接报错,因为人家不知道排序规则。

自定义的引用数据类型的排序实现:
对于自定义的引用数据类型,TreeSet默认无法排序
所以我们需要定制排序的大小规则,程序员定义大小规则的方案有2种:
a.直接为对象的类实现比较器规则接口Comparable,重写比较方法(拓展方式)
// 如果程序员认为比较者大于被比较者 返回正数!
// 如果程序员认为比较者小于被比较者 返回负数!
// 如果程序员认为比较者等于被比较者 返回0!

b.直接为集合设置比较器Comparator对象,重写比较方法
// 如果程序员认为比较者大于被比较者 返回正数!
// 如果程序员认为比较者小于被比较者 返回负数!
// 如果程序员认为比较者等于被比较者 返回0!
注意:如果类和集合都带有比较规则,优先使用集合自带的比较规则。

小结:
TreeSet集合对自定义引用数据类型排序,默认无法进行。
但是有两种方式可以让程序员定义大小规则:
a.直接为对象的类实现比较器规则接口Comparable,重写比较方法(拓展方式)
b.直接为集合设置比较器Comparator对象,重写比较方法
注意:如果类和集合都带有比较规则,优先使用集合自带的比较规则。

Collections工具类的使用

1
2
3
4
5
6
7
8
java.utils.Collections:是集合工具类
Collections并不属于集合,是用来操作集合的工具类。
Collections有几个常用的API:
- public static <T> boolean addAll(Collection<? super T> c, T... elements)
给集合对象批量添加元素!
- public static void shuffle(List<?> list) :打乱集合顺序。
- public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。
- public static <T> void sort(List<T> list,Comparator<? super T> ):将集合中元素按照指定规则排序。

引用数据类型的排序

1
2
3
4
5
6
7
8
9
10
11
字符串按照首字符的编号升序排序!

自定义类型的比较方法API:
- public static <T> void sort(List<T> list):
将集合中元素按照默认规则排序。
对于自定义的引用类型的排序人家根本不知道怎么排,直接报错!
如果希望自定义的引用类型排序不报错,可以给类提供比较规则:Comparable。

- public static <T> void sort(List<T> list,Comparator<? super T> c):
将集合中元素按照指定规则排序,自带比较器
注意:如果类有比较规则,而这里有比较器,优先使用比较器。

可变参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
可变参数用在形参中可以接收多个数据。
可变参数的格式:数据类型... 参数名称

可变参数的作用:
传输参数非常灵活,方便。
可以不传输参数。
可以传输一个参数。
可以传输多个参数。
可以传输一个数组。

可变参数在方法内部本质上就是一个数组。
可变参数的注意事项:
1.一个形参列表中可变参数只能有一个!!
2.可变参数必须放在形参列表的最后面!!

Map集合

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
Map集合是另一个集合体系。
Collection是单值集合体系。

Map集合是一种双列集合,每个元素包含两个值。
Map集合的每个元素的格式:key=value(键值对元素)。
Map集合也被称为“键值对集合”。

Map集合的完整格式:{key1=value1 , key2=value2 , key3=value3 , ...}

Map集合有啥用?
1.Map集合存储的信息更加的具体丰富。
Collection: ["苍老师","日本","女","动作演员",23,"广州"]
Map : {name="苍老师" , jiaxiang=小日本 , sex="女" , age = 23 , addr=广州}

2.Map集合很适合做购物车这样的系统。
Map: {娃娃=30 , huawei=1 , iphonex=1}

注意:集合和泛型都只能支持引用数据类型,集合完全可以称为是对象容器,存储都是对象。

Map集合的体系:
Map<K , V>(接口,Map集合的祖宗类)
/ \
TreeMap<K , V> HashMap<K , V>(实现类,经典的,用的最多)
\
LinkedHashMap<K, V>(实现类)

Map集合的特点:
1.Map集合的特点都是由键决定的。
2.Map集合的键是无序,不重复的,无索引的。
Map集合后面重复的键对应的元素会覆盖前面的整个元素!
3.Map集合的值无要求。
4.Map集合的键值对都可以为null。

HashMap:元素按照键是无序,不重复,无索引,值不做要求。
LinkedHashMap:元素按照键是有序,不重复,无索引,值不做要求。

Map集合的常用API(重点中的重点)

1
2
3
4
5
6
- public V put(K key, V value):  把指定的键与指定的值添加到Map集合中。
- public V remove(Object key): 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。
- public V get(Object key) 根据指定的键,在Map集合中获取对应的值。
- public Set<K> keySet(): 获取Map集合中所有的键,存储到Set集合中。
- public Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)。
- public boolean containKey(Object key):判断该集合中是否有此键。

Map集合的遍历方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Map集合的遍历方式有:3种。
(1)“键找值”的方式遍历:先获取Map集合全部的键,再根据遍历键找值。
(2)“键值对”的方式遍历:难度较大。
(3)JDK 1.8开始之后的新技术:Lambda表达式。

a.“键找值”的方式遍历Map集合。
1.先获取Map集合的全部键的Set集合。
2.遍历键的Set集合,然后通过键找值。

b.“键值对”的方式遍历:
1.把Map集合转换成一个Set集合:Set<Map.Entry<K, V>> entrySet();
2.此时键值对元素的类型就确定了,类型是键值对实体类型:Map.Entry<K, V>
3.接下来就可以用foreach遍历这个Set集合,类型用Map.Entry<K, V>

c.JDK 1.8开始之后的新技术:Lambda表达式。

Map集合存储自定义类型

1
2
3
4
5
Map集合的键和值都可以存储自定义类型。

小结:
Map集合的键和值都可以存储自定义类型。
如果希望Map集合认为自定义类型的键对象重复了,必须重写对象的hashCode()和equals()方法

LinkedHashMap集合

1
2
3
4
5
6
7
8
9
10
11
12
13
LinkedHashMap是HashMap的子类。
-- 添加的元素按照键有序,不重复的。
HashSet集合相当于是HashMap集合的键都不带值。
LinkedHashSet集合相当于是LinkedHashMap集合的键都不带值。

底层原理完全一样,都是基于哈希表按照键存储数据的,
只是HashMap或者LinkedHashMap的键都多一个附属值。


小结:
HashMap集合是无序不重复的键值对集合。
LinkedHashMap集合是有序不重复的键值对集合。
他们都是基于哈希表存储数据,增删改查都很好。

TreeMap集合

1
2
3
4
5
6
7
8
TreeMap集合按照键是可排序不重复的键值对集合。(默认升序)
TreeMap集合按照键排序的特点与TreeSet是完全一样的。
小结:
TreeMap集合和TreeSet集合都是排序不重复集合
TreeSet集合的底层是基于TreeMap,只是键没有附属值而已。
所以TreeMap集合指定大小规则有2种方式:
a.直接为对象的类实现比较器规则接口Comparable,重写比较方法(拓展方式)
b.直接为集合设置比较器Comparator对象,重写比较方法

输出一个字符串中每个字符出现的次数。(经典面试题)

1
2
3
4
5
(1)键盘录入一个字符串。aabbccddaa123。
(2)定义一个Map集合,键是每个字符,值是其出现的次数。 {a=4 , b=2 ,...}
(3)遍历字符串中的每一个字符。
(4)拿着这个字符去Map集合中看是否有这个字符键,有说明之前统计过,其值+1
没有这个字符键,说明该字符是第一次统计,直接存入“该字符=1”
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 MapDemo01 {
public static void main(String[] args) {
// (1)键盘录入一个字符串。aabbccddaa123。
Scanner scanner = new Scanner(System.in);
System.out.print("请您输入一个字符串:");
String datas = scanner.nextLine();

// (2)定义一个Map集合,键是每个字符,值是其出现的次数。
Map<Character , Integer> infos = new HashMap<>(); // {a=2,b=2}

// (3)遍历字符串中的每一个字符。
// datas = aabbccddaa123
for(int i = 0 ; i < datas.length() ; i++ ){
// 取出当前索引的字符
char ch = datas.charAt(i);
// (4)拿着这个字符去Map集合中看是否有这个字符键,有说明之前统计过,其值+1
// 没有这个字符键,说明该字符是第一次统计,直接存入“该字符=1”
if(infos.containsKey(ch)){
// 说明这个字符之前已经出现过,其值+1
infos.put(ch , infos.get(ch) + 1);
}else{
// 说明这个字符第一次出现,直接存入 a=1
infos.put(ch , 1);
}
}
// (5)输出结果
System.out.println("结果:"+infos);

}
}

知识回顾

面向对象

用代码去高度模拟现实世界,以便为人类的业务服务。
Java是一种面向对象的高级编程语言。
高级编程语言:代码风格很像人类的自然语言。
zhubajie.eat(“西瓜”);

面向对象最重要的两个概念:类和对象。

是描述相同事物的共同特征的抽象。 人类。

对象

是具体存在的实例,是真实的。 实例==对象

在代码层面:必须先有类,才能创建出对象。

定义类的格式

修饰符 class 类名{
// 1.成员变量(Field:描述类和对象的属性信息的).
// 2.成员方法(Method:描述类或者对象的行为信息的)
// 3.构造器(Constructor:初始化一个类的对象并返回引用)
// 4.代码块
// 5.内部类
}

注意

​ – 类名的首字母应该大写,满足“驼峰写法”。
​ – 一个Java代码文件中可以定义多个类。但是只能有一个类
​ 是用public修饰的,而且public修饰的类名必须成为代码的文件名称。

类中的成分研究

类中有且仅有五大成分

1.成员变量(Field:描述类和对象的属性信息的).
2.成员方法(Method:描述类或者对象的行为信息的)
3.构造器(Constructor:初始化一个类的对象并返回引用)
4.代码块
5.内部类

注意:只要不是这5大成分放在类下就会报错

构造器的复习

作用

初始化一个类的对象并返回引用。

格式

修饰符 类名(形参){

}

构造器初始化对象的格式

类名 对象名称 = new 构造器;

注意:一个类默认会自带一个无参数构造器,即使不写它也存在,但是如果一个类它写了一个构造器,那么默认的无参数构造器就被覆盖了!!

this关键字的作用

  1. this代表了当前对象的引用
  2. this关键字可以用在实例方法和构造器中
  3. this用在方法中,谁调用这个方法,this就代表谁
  4. this用在构造器,代表了构造器正在初始化的那个对象的引用

static关键字

引入

我们之前定义了很多成员变量(name , age , sex)
其实我们只写了一份,但是发现每个对象都可以用,就说明
Java中这些成员变量或者方法是存在所属性的。
有些是属于对象的,有些是属于类本身的。

Java是通过成员变量是否有static修饰来区分是类的还是属于对象的。

static == 静态 == 修饰的成员(方法和成员变量)属于类本身的。

分类

按照有无static修饰,成员变量和方法可以分为:

成员变量:
(1)静态成员变量(类变量):
有static修饰的成员变量称为静态成员变量也叫类变量,属于类本身的,

直接用类名访问即可。

2)实例成员变量
无static修饰的成员变量称为实例成员变量,属于类的每个对象的

必须用类的对象来访问。

成员方法:
(1)静态方法
有static修饰的成员方法称为静态方法也叫类方法,属于类本身的

直接用类名访问即可

2)实例方法
无static修饰的成员方法称为实例方法,
属于类的每个对象的,必须用类的对象来访问

小结

成员变量有2种
– 有static修饰的属于类叫静态成员变量,与类一起加载一次,直接用类名调用即可。
– 无static修饰的属于类的每个对象的叫实例成员变量,
与类的对象一起加载,对象有多少个,实例成员变量就加载多少份。必须用类的对象调用。

成员方法有2种:

– 有static修饰的属于类叫静态方法,直接用类名调用即可。
– 无static修饰的属于类的每个对象的叫实例方法,必须用类的对象调用

this和super关键字使用总结

this代表了当前对象的引用(继承中指代子类对象):
this.子类成员变量。
this.子类成员方法。
this(…):可以根据参数匹配访问本类其他构造器。(还没有学习)
super代表了父类对象的引用(继承中指代了父类对象空间)
super.父类成员变量。
super.父类的成员方法。
super(…):可以根据参数匹配访问父类的构造器。
拓展:this(…)根据参数匹配访问本类其他构造器。
注意:
this(…)借用本类其他构造器。
super(…)调用父类的构造器。
this(…)和super(…)必须放在构造器的第一行,否则报错!
所以this(…)和super(…)不能同时出现在构造器中!!!

final关键字

final是最终的含义。
final用于修饰:类,方法,变量
1.final修饰类,类不能被继承了。
2.final可以修饰方法,方法就不能被重写了。
3.final修饰变量总规则:变量有且仅能被赋值一次。

拓展:final和abstract的关系?

互斥关系,不能同时修饰类或者同时修饰方法!!

修饰变量

final修饰变量的总规则:有且仅能被赋值一次。

变量有几种?
成员变量
– 静态成员变量:有static修饰,属于类,只加载一份。
– 实例成员变量:无static修饰,属于每个对象,与对象一起加载。
局部变量
– 只能方法中,构造器中,代码块中,for循环中,用完作用范围就消失了。
### 修饰局部变量:
​ – 让值被固定或者说保护起来,执行的过程中防止被修改。

修饰静态成员变量

​ final修饰变量的总规则:有且仅能被赋值一次。
​ final修饰静态成员变量,变量变成了常量。
​ 常量:有public static final修饰,名称字母全部大写,多个单词用下划线连接。

修饰实例成员变量

inal修饰变量的总规则:有且仅能被赋值一次。
拓展:
final修饰实例成员变量可以在哪些地方赋值1次:
1.定义的时候赋值一次。
2.可以在实例代码块中赋值一次。
3.可以在每个构造器中赋值一次。

成员变量的分类和访问

按照有无static修饰成员变量分为:

(1)静态成员变量:

​ 有static修饰,属于类本身与类一起加载一次,直接用类名访问即可。

(2)实例成员变量:无static修饰,属于类的每个对象的,必须先创建对象,再用对象来访问。

​ 成员变量的访问语法:
​ 静态成员变量访问:
​ 类名.静态成员变量。
​ 对象.静态成员变量。(不推荐)
​ 实例成员变量的访问:
​ 对象.实例成员变量。

成员方法的分类和访问

成员方法按照有无static修饰可以分为:

​ (1)静态方法:

有static修饰,属于类,直接用类名访问即可

(2)实例方法:

无static修饰,属于对象的,必须用对象来访问。

成员方法的访问语法:
静态方法的访问格式:
类名.静态方法
对象.静态方法(不推荐)
实例方法的访问格式:
对象.实例方法

小结:

静态方法属于类,有static修饰,直接用类名访问即可。

实例方法属于对象,无static修饰,必须先创建对象,然后用对象来访问。

静态方法也可以被对象共享访问,但是不推荐,因为静态方法直接用类名访问即可。

成员变量和成员方法访问的拓展

面试常考

方法:实例方法,静态方法。
成员变量:实例成员变量,静态成员变量。
8种访问形式的问答:
a.实例方法是否可以直接访问实例成员变量?可以的,因为它们都属于对象。
b.实例方法是否可以直接访问静态成员变量?可以的,静态成员变量可以被共享访问。
c.实例方法是否可以直接访问实例方法? 可以的,实例方法和实例方法都属于对象。 d.实例方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问!


​ a.静态方法是否可以直接访问实例变量? 不可以的,实例变量必须用对象访问!!
​ b.静态方法是否可以直接访问静态变量? 可以的,静态成员变量可以被共享访问。
​ c.静态方法是否可以直接访问实例方法? 不可以的,实例方法必须用对象访问!!
​ d.静态方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问!!

抽象类

抽象类的概述

什么是抽象类

父类知道子类一定要完成某个功能,但是每个子类实现的情况都不一样.
子类都会用自己重写的功能了,父类的该功能就可以定义成抽象的方法。

什么是抽象方法

没有方法体,只有方法签名,必须用abstract修饰的方法就是抽象方法。
拥有抽象方法的类必须定义成抽象类

抽象类:必须用abstract关键字修饰

抽象类的使用

抽象类的作用:为了被子类继承。

抽象类是为了被子类继承,约束子类要重写抽象方法!

注意:一个类继承了抽象类,必须重写完抽象类的全部抽象方法,否则这个类必须定义成抽象类。

抽象类的特征研究和深入

抽象类的特征:有得有失。
有得:抽象类拥有了得到抽象方法的能力。
失去:抽象类失去了创建对象的能力

面试题:抽象类是否有构造器,是否可以创建对象,为什么?
答:抽象类作为类一定有构造器,而且必须有构造器。
提供给子类继承后调用父类构造器使用的。抽象类虽然有构造器,但是抽象类绝对不能创建对象。抽象类中可能存在抽象方法,抽象方法不能执行。抽象在学术上本身意味着不能实例化。

抽象类除了有得有失之外,类的其他成分人家都具备!!抽象类中也可以没有抽象方法!!

抽象类的意义

抽象类存在的意义有两点:
(1)被继承,抽象类就是为了被子类继承,(为了派生子类)否则抽象类将毫无意义。(核心意义)
(2)抽象类体现的是”模板思想”:部分实现,部分抽象。(拓展)
– 可以使用抽象类设计一个模板模式。

抽象类的注意事项和总结

  1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。 理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。

  2. 抽象类一定有而且是必须有构造器,是供子类创建对象时,初始化父类成员使用的。
    理解:子类的构造器中,有默认的super(),需要访问父类构造器。

  3. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

  4. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则子类也必须定义成抽象类。

  5. 抽象类存在的意义是为了被子类继承,抽象类体现的是模板思想。
    理解:抽象类中已经实现的是模板中确定的成员,
    抽象类不确定如何实现的定义成抽象方法,交给具体的子类去实现。

代码块

代码块是类的成分之一:
成员变量,成员方法,构造器,代码块,内部类。

​ 代码块按照有无static修饰分为:
​ 1.静态代码块。
​ 2.实例代码块

​ 静态代码块的格式:
​ static {

​ }

静态代码块

静态代码块特点:
– 必须有static修饰。
– 会与类一起优先加载,且自动触发执行一次。
静态代码块作用:
– 可以在执行类的方法等操作之前先在静态代码块中进行静态资源的初始化操作。(拓展)

实例代码块

实例代码块的格式:

{

}

实例代码块的特点:
– 无static修饰。
– 会与类的对象一起加载,每次创建类的对象的时候,
实例代码块都会被加载且自动触发执行一次。
– 实例代码块的代码在底层实际上是提取到每个构造器中去执行的!

实例代码块的作用:

– 实例代码块可以在创建对象之前进行实例资源的初始化操作。

枚举

枚举的概述和作用

枚举是Java中的一种特殊类型。
枚举的作用:是为了做信息的标志和信息的分类

定义枚举的格式:

​ 修饰符 enum 枚举名称{
​ 第一行都是罗列枚举实例的名称。
​ }

枚举类的编译以后源代码

1
2
3
4
5
6
7
8
9
public final class Season extends java.lang.Enum<Season> {
public static final Season SPRING = new Season();
public static final Season SUMMER = new Season();
public static final Season AUTUMN = new Season();
public static final Season WINTER = new Season();

public static Season[] values();
public static Season valueOf(java.lang.String);
}

枚举的特点:
1.枚举类是用final修饰的,枚举类不能被继承!
2.枚举类默认继承了java.lang.Enum枚举类。
3.枚举类的第一行都是常量,存储都是枚举类的对象。
4.枚举类的第一行必须是罗列枚举类的实例名称。
所以:枚举类相当于是多例设计模式。

常量做信息分类和信息标志:
虽然可以实现可读性,但是入参不受限制!!!

Java建议做信息标志和信息分类应该使用枚举实现:最优雅的方式。
可以实现可读性,而且入参受限制,不能乱输入!!!

内部类

什么是内部类

定义在一个类里面的类就是内部类

内部类有什么用

​ 可以提供更好的封装性, 内部类有更多权限修饰符 , 封装性有更多的控制。
​ 可以体现出组件的思想

内部类的分类

1)静态内部类。
(2)实例内部类。(成员内部类)
(3)局部内部类。
(4)匿名内部类。(重点)

静态内部类

什么是静态内部类?
有static修饰,属于外部类本身,会加载一次。
静态内部类中的成分研究:
类有的成分它都有,静态内部类属于外部类本身,只会加载一次
所以它的特点与外部类是完全一样的,只是位置在别人里面而已。

静态内部类中的成分研究:
类有的成分它都有,静态内部类属于外部类本身,只会加载一次
所以它的特点与外部类是完全一样的,只是位置在别人里面而已。

​ 外部类=宿主
​ 内部类=寄生

静态内部类的访问格式:
外部类名称.内部类名称

静态内部类创建对象的格式:
外部类名称.内部类名称 对象名称 = new 外部类名称.内部类构造器;

静态内部类的访问拓展:
静态内部类中是否可以直接访问外部类的静态成员?可以的,外部类的静态成员只有一份,可以被共享!
静态内部类中是否可以直接访问外部类的实例成员?不可以的,外部类的是成员必须用外部类对象访问!!

小结:
静态内部类属于外部类本身,只会加载一次
所以它的特点与外部类是完全一样的,只是位置在别人里面而已。

实例内部类

​ 什么是实例内部类:
​ 无static修饰的内部类,属于外部类的每个对象的,跟着对象一起加载的。
​ 实例内部类的成分特点:
​ 实例内部类中不能定义静态成员,其他都可以定义。
​ 可以定义常量。
​ 实例内部类的访问格式:
​ 外部类名称.内部类名称。
​ 创建对象的格式:
​ 外部类名称.内部类名称 对象名称 = new 外部类构造器.new 内部构造器;
​ 拓展:

​ 实例内部类中是否可以直接访问外部类的静态成员?可以的,外部类的静态成员可以被共享访问!

实例内部类中是否可以访问外部类的实例成员?可以的,实例内部类属于外部类对象,可以直接访问当前外部类对象的实例成员!
小结:
实例内部类属于外部类对象,需要用外部类对象一起加载,
实例内部类可以访问外部类的全部成员!

局部内部类(几乎不用)

定义在方法中,在构造器中,代码块中,for循环中定义的内部类
就是局部内部类。
局部内部类中的成分特点: 只能定义实例成员,不能定义静态成员
可以定义常量的。
小结:
局部内部类没啥用。

匿名内部类(重点)

什么是匿名内部类?
就是一个没有名字的局部内部类。
匿名内部类目的是为了:简化代码,也是开发中常用的形式。
匿名内部类的格式:
new 类名|抽象类|接口(形参){
方法重写。
}
匿名内部类的特点:
1.匿名内部类是一个没有名字的内部类。
2.匿名内部类一旦写出来,就会立即创建一个匿名内部类的对象返回。
3.匿名内部类的对象的类型相当于是当前new的那个的类型的子类类型。

包和权限修饰符

分门别类的管理各种不同的技术。 企业的代码必须用包区分。便于管理技术,扩展技术,阅读技术。
定义包的格式:package 包名; 必须放在类名的最上面。
一般工具已经帮我们做好了。
包名的命名规范:
一般是公司域名的倒写+技术名称:
http://www.itheima.com => com.itheima.技术名称

​ 包名建议全部用英文,多个单词用”.“连接,必须是合法标识符,不能用关键字
​ ## 注意:
​ 相同包下的类可以直接访问。
​ 不同包下的类必须导包,才可以使用!
​ 导包格式:import 包名.类名;

权限修饰符

权限修饰符:有四种(private -> 缺省 -> protected - > public ) 可以修饰成员变量,修饰方法,修饰构造器,内部类,不同修饰符修饰的成员能够被访问的权限将受到限制!
四种修饰符的访问权限范围:

private 缺省 protected public
本类中
本包下其他类中 X
其他包下的类中 X X X
其他包下的子类中 X X

接口的概述

什么是接口

接口是更加彻底的抽象,接口中全部是抽象方法和常量,没有其他成分。(JDK 1.8之前)

接口有啥用

接口体现的是规范思想,实现接口的类必须重写完接口中全部的抽象方法。

规范 == 约束。
接口称为被实现,实现接口的类称为实现类。

定义接口的格式

修饰符 interface 接口名称{

​ }

interface: 定义接口的关键字

接口中的成分研究(JDK 1.8之前)

  1. 抽象方法

    a.接口中的抽象方法默认会加上public abstract修饰,所以可以省略不写。

  2. 常量

    常量:是指有public static final修饰的成员变量,有且仅能被赋值一次,值不能改变

    常量的名称规范上要求全部大写,多个单词下划线连接。

    常量修饰的public static final 可以省略不写,默认会加上。

小结

  1. 定义接口使用的关键字:interface
  2. 接口中的成分在JDK 1.8之前只能有:常量和抽象方法!!!!
  3. 在接口中常量的修饰符:public static final 可以省略不写,默认会加上。
  4. 在接口中抽象方法的修饰符:public abstract 可以省略不写,默认会加上。

接口的基本实现

接口是用来被类实现的

引入

  1. 类与类是继承关系:一个类只能直接继承一个父类

  2. 类与接口是实现关系:一个类可以实现多个接口

  3. 实现接口的类称为“实现类”

    ​ 子类 继承 父类
    ​ 实现类 实现 接口

实现类实现接口的格式

修饰符 class 实现类名称 implements 接口1,接口2,接口3,….{

}

implements:实现的含义。

接口是可以被多实现的:一个类可以实现多个接口

注意:一个类实现接口必须重写完接口中全部抽象方法,否则这个类必须定义成抽象类!!

接口的多实现

引入

​ 类与类是单继承。
​ 类与接口是多实现

小结

  1. 一个类可以实现多个接口
  2. 一个类如果实现了多个接口,必须重写完全部接口中的全部抽象方法, 否则这个类必须定义抽象类

接口与接口的多继承

引入

  1. 类与类是单继承关系:一个类只能继承一个直接父类
  2. 类与接口是多实现关系:一个类可以实现多个接口
  3. 接口与接口是多继承关系:一个接口可以继承多个接口

JDK 1.8开始(包括JDK1.8)之后接口新增

引入

​ JDK 1.8之前接口中只能是抽象方法,常量。
​ JDK 1.8开始之后接口不再纯洁了。
​ JDK 1.8开始之后接口新增了如下三种方法。

表现

  • 默认方法(就是之前写的普通实例方法)

    – 必须用default修饰,默认会public修饰

    – 必须用接口的实现类的对象来调用。

  • 静态方法

    –可以直接加static修饰

    – 默认会public修饰

    – 注意:接口的静态方法必须用接口的类名本身来调用。

  • 私有方法(就是私有的实例方法): JDK 1.9才开始有的

    – 必须加private修饰。

    – 只能在本类中被其他的默认方法或者私有方法访问。

实现多个接口的使用注意实现

  1. 如果实现了多个接口,多个接口中存在同名的静态方法并不会冲突

    原因是只能通过各自接口名访问静态方法。

  2. 当一个类,既继承一个父类,又实现若干个接口时,(重点)

    父类中的成员方法与接口中的默认方法重名,子类就近选择执行父类的成员方法

  3. 当一个类实现多个接口时,多个接口中存在同名的默认方法

    实现类必须重写这个方法。

  4. 接口中,没有构造器,不能创建对象。(重点)

    接口是更彻底的抽象,连构造器都没有,自然不能创建对象!!

封装

封装的哲学思维

合理隐藏,合理暴露

封装最初的目的

提高代码的安全性和复用性,组件化。

封装的步骤

  1. 成员变量应该私有。用private修饰,只能在本类中直接访问。
  2. 提供成套的getter和setter方法暴露成员变量的取值和赋值

继承

什么是继承

​ 继承是Java中一般到特殊的关系,是一种子类到父类的关系。

​ 例如:学生类继承了人类。 猫类继承了动物类。

​ 被继承的类称为:父类/超类。继承父类的类称为:子类。

继承的特点

  1. 单继承:一个类只能继承一个直接父类

    为什么Java是单继承的?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    答:反证法,假如Java可以多继承,请看如下代码:
    class A{
    public void test(){
    System.out.println("A");
    }
    }
    class B{
    public void test(){
    System.out.println("B");
    }
    }
    class C A , B {
    public static void main(String[] args){
    C c = new C();
    c.test(); // 出现了类的二义性!所以Java不能多继承!!
    }
    }
  2. 多层继承:一个类可以间接继承多个父类。(家谱)

  3. 一个类可以有多个子类

  4. 个类要么默认继承了Object类,要么间接继承了Object类,Object类是Java中的祖宗类!!

  5. 子类继承了一个父类,子类就可以直接得到父类的属性(成员变量)和行为(方法)了

继承的作用

  1. “可以提高代码的复用”,相同代码可以定义在父类中。然后子类直接继承父类,就可以直接使用父类的这些代码了。(相同代码重复利用)
  2. 子类更强大:子类不仅得到了父类的功能,它还有自己的功能.

继承的格式

​ 子类 extends 父类{

​ }

小结

  1. 继承是子类到到父类的一种关系
  2. 子类继承了一个父类,子类就可以直接得到父类的属性和行为了。
  3. 在Java中继承是 “is a” 的关系。Cat extends Animal:猫是一个动物。
  4. 在Java中,子类是更强大的,子类不仅继承了父类的功能,自己还可以定义自己的功能。
  5. 继承的优势可以把相同的代码定义在父类中,子类可以直接继承使用
  6. 这样就可以提高代码的复用性:相同代码只需要在父类中写一次就可以了

子类不能继承父类的内容

引入

  1. 子类继承父类,子类就得到了父类的属性和行为。
  2. 但是并非所有父类的属性和行为等子类都可以继承。

子类不能继承父类的东西

  1. 子类不能继承父类的构造器:子类有自己的构造器。(没有争议的)

  2. 有争议的观点(拓展)

    1. 子类是否可以继承父类的私有成员(私有成员变量,私有成员方法)?

      – 我认为子类是可以继承父类的私有成员的,只是不能直接访问而已。

      – 以后可以暴力去访问继承自父类的私有成员~~~

    2. 子类是否可以继承父类的静态成员?

      – 我认为子类是不能继承父类的静态成员的

      – 子类只是可以访问父类的静态成员,父类的静态成员只有一份可以被子类共享访问。共享并非继承。

继承后-成员变量的访问特点

就近原则

子类有找子类,子类没有找父类,父类没有就报错。

小结

  1. this代表了当前对象的引用,可以用于访问当前子类对象的成员变量。
  2. super代表了父类对象的引用,可以用于访问父类中的成员变量。

继承后-成员方法的访问特点

就近原则

子类有找子类,子类没有找父类,父类没有就报错。

小结

子类对象优先使用子类已有的方法。

方法重写

方法重写的概念

  1. 子类继承了父类,子类就得到了父类的某个方法,但是子类觉得父类的这个方法不好用或者无法满足自己的需求
  2. 子类重写一个与父类申明一样的方法来覆盖父类的该方法,子类的这个方法就进行了方法重写。

方法重写的校验注解: @Override

Java建议在重写的方法上面加上一个@Override注解。
方法一旦加了这个注解,那就必须是成功重写父类的方法,否则报错!
@Override优势:可读性好,安全,优雅!!

方法重写的具体要求

  1. 子类重写方法的名称和形参列表必须与父类被重写方法一样。
  2. 子类重写方法的返回值类型申明要么与父类一样,要么比父类方法返回值类型范围更小。
  3. 子类重写方法的修饰符权限应该与父类被重写方法的修饰符权限相同或者更大。
  4. 子类重写方法申明抛出的异常应该与父类被重写方法申明抛出的异常一样或者范围更小!

方法重写的规范

  1. 加上@Override注解
  2. 建议“申明不变,重新实现”

小结

  1. 方法重写是子类重写一个与父类申明一样的方法覆盖父类的方法
  2. 方法重写建议加上@Override注解
  3. 方法重写的核心要求:方法名称形参列表必须与被重写方法一致!!
  4. 建议“申明不变,重新实现”

super调用父类被重写的方法

super:代表了父类引用。
super可以用在子类的实例方法中调用父类被重写的方法

静态方法和私有方法是否可以被重写(拓展语法)

可以吗? 都不可以.

继承后-构造器的特点

子类的全部构造器默认一定会先访问父类的无参数构造器,再执行子类自己的构造器。

为什么子类构造器会先调用父类构造器?

  1. 子类的构造器的第一行默认有一个super()调用父类的无参数构造器,写不写都存在!
  2. 子类继承父类,子类就得到了父类的属性和行为, 当我们调用子类构造器初始化子类对象数据的时候,必须先调用父类构造器初始化继承自父类的属性和行为

super调用父类构造器

子类的全部构造器默认一定会调用父类的无参数构造器。

super(…):可以根据参数选择调用父类的某个构造器。

小结

可以在子类构造器中通过super(…)根据参数选择调用父类的构造器,以便调用父类构造器初始化继承自父类的数据

多态

多态的形式

​ 父类类型 对象名称 = new 子类构造器;

​ 接口 对象名称 = new 实现类构造器;

​ 父类类型的范围 > 子类类型范围的。

多态的概念

同一个类型的对象,执行同一个行为,在不同的状态下会表现出不同的行为特征

多态的识别技巧

  1. 对于方法的调用:编译看左边,运行看右边
  2. 对于变量的调用:编译看左边,运行看左边

多态的使用前提

  1. 必须存在继承或者实现关系
  2. 必须存在父类类型的变量引用子类类型的对象
  3. 需要存在方法重写

多态的优劣势

优势:

  1. 在多态形式下,右边对象可以实现组件化切换,业务功能也随之改变,便于扩展和维护。可以实现类与类之间的解耦
  2. 实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法.可以传入一切子类对象进行方法的调用,更能体现出多态的扩展性与便利

劣势:

  1. 多态形式下,不能直接调用子类特有的功能。编译看左边!! 左边
  2. 父类中没有子类独有的功能,所以代码在编译阶段就直接报错了!

引用数据类型的自动类型转换

基本数据类型的转换

  1. 小范围类型的变量或者值可以直接赋值给大范围类型的变量。
  2. 大范围类型的变量或者值必须强制类型转换给小范围类型的变量

引用数据类型转换的思想(一样)

父类类型的范围 > 子类类型的范围

​ Animal Cat

语法

  1. 子类类型的对象或者变量可以自动类型转换赋值给父类类型的变量

引用类型的自动类型转换并不能解决多态的劣势

引用类型强制类型转换

语法

父类类型的变量或者对象必须强制类型转换成子类类型的变量,否则报错!

格式

类型 变量名称 = (类型)(对象或者变量)

注意:有继承/实现关系的两个类型就可以进行强制类型转换,编译阶段一定不报错!但是运行阶段可能出现:类型转换异常 ClassCastException

Java建议在进行强制类型转换之前先判断变量的真实类型,再强制类型转换!
变量 instanceof 类型: 判断前面的变量是否是后面的类型或者其子类类型才会返回true,

什么是泛型?

1
2
泛型就是一个标签:<数据类型>
泛型可以在编译阶段约束只能操作某种数据类型

注意:JDK 1.7开始之后,泛型后面的申明可以省略不写!!

泛型和集合都只能支持引用数据类型,不支持基本数据类型!

泛型的好处

1
2
泛型在编译阶段约束了操作的数据类型,从而不会出现类型转换异常。
体现的是Java的严谨性和规范性,数据类型,经常需要进行统一!

自定义泛型类

泛型类的概念

1
使用了泛型定义的类就是泛型类

泛型类的格式

1
2
3
4
修饰符 class 类名<泛型变量>{

}
泛型变量建议使用 E , T , K , V

需求

1
模拟ArrayList集合自定义一个集合MyArrayList集合
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 GenericDemo {
public static void main(String[] args) {
MyArrayList<String> lists = new MyArrayList<String>();

MyArrayList<String> lists1 = new MyArrayList<>();
lists1.add("java");
lists1.add("mysql");
lists1.remove("java");
System.out.println(lists1);
}
}

class MyArrayList<E>{

private ArrayList lists = new ArrayList();

public void add(E e){
lists.add(e);
}

public void remove(E e){
lists.remove(e);
}
@Override
public String toString() {
return lists.toString();
}
}

泛型类的核心思想

1
是把出现泛型变量的地方全部替换成传输的真实数据类型。

自定义泛型方法

什么是泛型方法?

1
定义了泛型的方法就是泛型方法

泛型方法的定义格式

1
2
3
修饰符 <泛型变量> 返回值类型 方法名称(形参列表){

}

注意:方法定义了是什么泛型变量,后面就只能用什么泛型变量。

泛型类的核心思想

1
是把出现泛型变量的地方全部替换成传输的真实数据类型。

需求

1
给你任何一个类型的数组,都能返回它的内容。
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 class GenericDemo {
public static void main(String[] args) {
Integer[] nums = {10 , 20 , 30 , 40 , 50};
String rs1 = arrToString(nums);
System.out.println(rs1);

String[] names = {"贾乃亮","王宝绿","陈羽凡"};
String rs2 = arrToString(names);
System.out.println(rs2);
}

public static <T> String arrToString(T[] nums){
StringBuilder sb = new StringBuilder();
sb.append("[");
if(nums!=null && nums.length > 0){
for(int i = 0 ; i < nums.length ; i++ ){
T ele = nums[i];
sb.append(i == nums.length-1 ? ele : ele+", ");
}
}
sb.append("]");
return sb.toString();
}
}

泛型接口

什么是泛型接口

1
使用了泛型定义的接口就是泛型接口

泛型接口的格式

1
2
3
修饰符 interface 接口名称<泛型变量>{

}

需求

1
教务系统,提供一个接口可约束一定要完成数据(学生,老师)的增删改查操作
1
2
3
4
5
6
public interface Data<E> {
void add(E stu);
void delete(E stu);
void update(E stu);
E query(int id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 操作学生数据
public class StudentData implements Data<Student> {
@Override
public void add(Student stu) {
System.out.println("添加学生!");
}

@Override
public void delete(Student stu) {
System.out.println("删除学生!");
}

@Override
public void update(Student stu) {

}

@Override
public Student query(int id) {
return null;
}
}
1
2
public class Student {
}
1
2
3
4
5
6
7
8
9
10
11
public class GenericDemo {
public static void main(String[] args) {
StudentData data = new StudentData();
data.add(new Student());
data.delete(new Student());

TeacherData data1 = new TeacherData();
data1.add(new Teacher());
data1.delete(new Teacher());
}
}

泛型通配符

通配符

1
2
?可以用在使用泛型的时候代表一切类型。
E , T , K , V是在定义泛型的时候使用代表一切类型。

注意:虽然BMW和BENZ都继承了Car,但是ArrayList和ArrayList与ArrayList没有关系的!泛型没有继承关系!

泛型的上下限

1
2
? extends Car : 那么?必须是Car或者其子类。(泛型的上限)
? super Car :那么?必须是Car或者其父类。(泛型的下限。不是很常见)

需求

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
public class GenericDemo {
public static void main(String[] args) {
ArrayList<BMW> bmws = new ArrayList<>();
bmws.add(new BMW());
bmws.add(new BMW());
bmws.add(new BMW());
run(bmws);

ArrayList<BENZ> benzs = new ArrayList<>();
benzs.add(new BENZ());
benzs.add(new BENZ());
benzs.add(new BENZ());
run(benzs);

ArrayList<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
dogs.add(new Dog());
// run(dogs); // 就进不来了!
}

// 定义一个方法,可以让很多汽车一起进入参加比赛
public static void run(ArrayList<? extends Car> cars){

}
}

class Car{
}
class BMW extends Car{

}
class BENZ extends Car{

}
class Dog{

}

设计模式概述

软件设计模式的产生背景

“设计模式”最初并不是出现在软件设计中,而是被用于建筑领域的设计中。

1977年美国著名建筑大师、加利福尼亚大学伯克利分校环境结构中心主任克里斯托夫·亚历山大(Christopher Alexander)在他的著作《建筑模式语言:城镇、建筑、构造》中描述了一些常见的建筑设计问题,并提出了 253 种关于对城镇、邻里、住宅、花园和房间等进行设计的基本模式。

1990年软件工程界开始研讨设计模式的话题,后来召开了多次关于设计模式的研讨会。直到1995 年,艾瑞克·伽马(ErichGamma)、理査德·海尔姆(Richard Helm)、拉尔夫·约翰森(Ralph Johnson)、约翰·威利斯迪斯(John Vlissides)等 4 位作者合作出版了《设计模式:可复用面向对象软件的基础》一书,在此书中收录了 23 个设计模式,这是设计模式领域里程碑的事件,导致了软件设计模式的突破。这 4 位作者在软件开发领域里也以他们的“四人组”(Gang of Four,GoF)著称。

软件设计模式的概念

软件设计模式(Software Design Pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。

学习设计模式的必要性

设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。

正确使用设计模式具有以下优点。

  • 可以提高程序员的思维能力、编程能力和设计能力。
  • 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
  • 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。

设计模式分类

  • 创建型模式

    用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。GoF(四人组)书中提供了单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式。

  • 结构型模式

    用于描述如何将类或对象按某种布局组成更大的结构,GoF(四人组)书中提供了代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。

  • 行为型模式

    用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。GoF(四人组)书中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。

UML图

统一建模语言(Unified Modeling Language,UML)是用来设计软件的可视化建模语言。它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息。

UML 从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等 9 种图。

类图概述

类图(Class diagram)是显示了模型的静态结构,特别是模型中存在的类、类的内部结构以及它们与其他类的关系等。类图不显示暂时性的信息。类图是面向对象建模的主要组成部分。

类图的作用

  • 在软件工程中,类图是一种静态的结构图,描述了系统的类的集合,类的属性和类之间的关系,可以简化了人们对系统的理解;
  • 类图是系统分析和设计阶段的重要产物,是系统编码和测试的重要模型。

类图表示法

类的表示方式

在UML类图中,类使用包含类名、属性(field) 和方法(method) 且带有分割线的矩形来表示,比如下图表示一个Employee类,它包含name,age和address这3个属性,以及work()方法。

属性/方法名称前加的加号和减号表示了这个属性/方法的可见性,UML类图中表示可见性的符号有三种:

  • +:表示public

  • -:表示private

  • #:表示protected

属性的完整表示方式是: 可见性 名称 :类型 [ = 缺省值]

方法的完整表示方式是: 可见性 名称(参数列表) [ : 返回类型]

注意:

1,中括号中的内容表示是可选的

2,也有将类型放在变量名前面,返回值类型放在方法名前面

举个栗子:

上图Demo类定义了三个方法:

  • method()方法:修饰符为public,没有参数,没有返回值。
  • method1()方法:修饰符为private,没有参数,返回值类型为String。
  • method2()方法:修饰符为protected,接收两个参数,第一个参数类型为int,第二个参数类型为String,返回值类型是int。

类与类之间关系的表示方式

关联关系

关联关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生、师傅和徒弟、丈夫和妻子等。关联关系是类与类之间最常用的一种关系,分为一般关联关系、聚合关系和组合关系。我们先介绍一般关联。

关联又可以分为单向关联,双向关联,自关联。

单向关联

在UML类图中单向关联用一个带箭头的实线表示。上图表示每个顾客都有一个地址,这通过让Customer类持有一个类型为Address的成员变量类实现。

双向关联

从上图中我们很容易看出,所谓的双向关联就是双方各自持有对方类型的成员变量。

在UML类图中,双向关联用一个不带箭头的直线表示。上图中在Customer类中维护一个List<Product>,表示一个顾客可以购买多个商品;在Product类中维护一个Customer类型的成员变量表示这个产品被哪个顾客所购买。

自关联

自关联在UML类图中用一个带有箭头且指向自身的线表示。上图的意思就是Node类包含类型为Node的成员变量,也就是“自己包含自己”。

聚合关系

聚合关系是关联关系的一种,是强关联关系,是整体和部分之间的关系。

聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。

在 UML 类图中,聚合关系可以用带空心菱形的实线来表示,菱形指向整体。下图所示是大学和教师的关系图:

组合关系

组合表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系。

在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。例如,头和嘴的关系,没有了头,嘴也就不存在了。

在 UML 类图中,组合关系用带实心菱形的实线来表示,菱形指向整体。下图所示是头和嘴的关系图:

依赖关系

依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。

在 UML 类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。下图所示是司机和汽车的关系图,司机驾驶汽车:

继承关系

继承关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系。

在 UML 类图中,泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现泛化关系。例如,Student 类和 Teacher 类都是 Person 类的子类,其类图如下图所示:

实现关系

实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。

在 UML 类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。例如,汽车和船实现了交通工具,其类图如图 9 所示。

软件设计原则

在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据6条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。

开闭原则

对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。

想要达到这样的效果,我们需要使用接口和抽象类。

因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。

下面以 搜狗输入法 的皮肤为例介绍开闭原则的应用。

【例】搜狗输入法 的皮肤设计。

分析:搜狗输入法 的皮肤是输入法背景图片、窗口颜色和声音等元素的组合。用户可以根据自己的喜爱更换自己的输入法的皮肤,也可以从网上下载新的皮肤。这些皮肤有共同的特点,可以为其定义一个抽象类(AbstractSkin),而每个具体的皮肤(DefaultSpecificSkin和HeimaSpecificSkin)是其子类。用户窗体可以根据需要选择或者增加新的主题,而不需要修改原代码,所以它是满足开闭原则的。

里氏代换原则

里氏代换原则是面向对象设计的基本原则之一。

里氏代换原则:任何基类可以出现的地方,子类一定可以出现。通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。

下面看一个里氏替换原则中经典的一个例子

【例】正方形不是长方形。

在数学领域里,正方形毫无疑问是长方形,它是一个长宽相等的长方形。所以,我们开发的一个与几何图形相关的软件系统,就可以顺理成章的让正方形继承自长方形。

代码如下:

长方形类(Rectangle):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Rectangle {
private double length;
private double width;

public double getLength() {
return length;
}

public void setLength(double length) {
this.length = length;
}

public double getWidth() {
return width;
}

public void setWidth(double width) {
this.width = width;
}
}

正方形(Square):

由于正方形的长和宽相同,所以在方法setLength和setWidth中,对长度和宽度都需要赋相同值。

1
2
3
4
5
6
7
8
9
10
11
12
public class Square extends Rectangle {

public void setWidth(double width) {
super.setLength(width);
super.setWidth(width);
}

public void setLength(double length) {
super.setLength(length);
super.setWidth(length);
}
}

类RectangleDemo是我们的软件系统中的一个组件,它有一个resize方法依赖基类Rectangle,resize方法是RectandleDemo类中的一个方法,用来实现宽度逐渐增长的效果。

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
public class RectangleDemo {

public static void resize(Rectangle rectangle) {
while (rectangle.getWidth() <= rectangle.getLength()) {
rectangle.setWidth(rectangle.getWidth() + 1);
}
}

//打印长方形的长和宽
public static void printLengthAndWidth(Rectangle rectangle) {
System.out.println(rectangle.getLength());
System.out.println(rectangle.getWidth());
}

public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setLength(20);
rectangle.setWidth(10);
resize(rectangle);
printLengthAndWidth(rectangle);

System.out.println("============");

Rectangle rectangle1 = new Square();
rectangle1.setLength(10);
resize(rectangle1);
printLengthAndWidth(rectangle1);
}
}

我们运行一下这段代码就会发现,假如我们把一个普通长方形作为参数传入resize方法,就会看到长方形宽度逐渐增长的效果,当宽度大于长度,代码就会停止,这种行为的结果符合我们的预期;假如我们再把一个正方形作为参数传入resize方法后,就会看到正方形的宽度和长度都在不断增长,代码会一直运行下去,直至系统产生溢出错误。所以,普通的长方形是适合这段代码的,正方形不适合。
我们得出结论:在resize方法中,Rectangle类型的参数是不能被Square类型的参数所代替,如果进行了替换就得不到预期结果。因此,Square类和Rectangle类之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立,正方形不是长方形。

如何改进呢?此时我们需要重新设计他们之间的关系。抽象出来一个四边形接口(Quadrilateral),让Rectangle类和Square类实现Quadrilateral接口

依赖倒转原则

高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

下面看一个例子来理解依赖倒转原则

【例】组装电脑

现要组装一台电脑,需要配件cpu,硬盘,内存条。只有这些配置都有了,计算机才能正常的运行。选择cpu有很多选择,如Intel,AMD等,硬盘可以选择希捷,西数等,内存条可以选择金士顿,海盗船等。

类图如下:

代码如下:

希捷硬盘类(XiJieHardDisk):

1
2
3
4
5
6
7
8
9
10
11
public class XiJieHardDisk implements HardDisk {

public void save(String data) {
System.out.println("使用希捷硬盘存储数据" + data);
}

public String get() {
System.out.println("使用希捷希捷硬盘取数据");
return "数据";
}
}

Intel处理器(IntelCpu):

1
2
3
4
5
6
public class IntelCpu implements Cpu {

public void run() {
System.out.println("使用Intel处理器");
}
}

金士顿内存条(KingstonMemory):

1
2
3
4
5
6
public class KingstonMemory implements Memory {

public void save() {
System.out.println("使用金士顿作为内存条");
}
}

电脑(Computer):

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
public class Computer {

private XiJieHardDisk hardDisk;
private IntelCpu cpu;
private KingstonMemory memory;

public IntelCpu getCpu() {
return cpu;
}

public void setCpu(IntelCpu cpu) {
this.cpu = cpu;
}

public KingstonMemory getMemory() {
return memory;
}

public void setMemory(KingstonMemory memory) {
this.memory = memory;
}

public XiJieHardDisk getHardDisk() {
return hardDisk;
}

public void setHardDisk(XiJieHardDisk hardDisk) {
this.hardDisk = hardDisk;
}

public void run() {
System.out.println("计算机工作");
cpu.run();
memory.save();
String data = hardDisk.get();
System.out.println("从硬盘中获取的数据为:" + data);
}
}

测试类(TestComputer):

测试类用来组装电脑。

1
2
3
4
5
6
7
8
9
10
public class TestComputer {
public static void main(String[] args) {
Computer computer = new Computer();
computer.setHardDisk(new XiJieHardDisk());
computer.setCpu(new IntelCpu());
computer.setMemory(new KingstonMemory());

computer.run();
}
}

上面代码可以看到已经组装了一台电脑,但是似乎组装的电脑的cpu只能是Intel的,内存条只能是金士顿的,硬盘只能是希捷的,这对用户肯定是不友好的,用户有了机箱肯定是想按照自己的喜好,选择自己喜欢的配件。

根据依赖倒转原则进行改进:

代码我们只需要修改Computer类,让Computer类依赖抽象(各个配件的接口),而不是依赖于各个组件具体的实现类。

类图如下:

image-20191229173554296

电脑(Computer):

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 Computer {

private HardDisk hardDisk;
private Cpu cpu;
private Memory memory;

public HardDisk getHardDisk() {
return hardDisk;
}

public void setHardDisk(HardDisk hardDisk) {
this.hardDisk = hardDisk;
}

public Cpu getCpu() {
return cpu;
}

public void setCpu(Cpu cpu) {
this.cpu = cpu;
}

public Memory getMemory() {
return memory;
}

public void setMemory(Memory memory) {
this.memory = memory;
}

public void run() {
System.out.println("计算机工作");
}
}

面向对象的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度。

接口隔离原则

客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。

下面看一个例子来理解接口隔离原则

【例】安全门案例

我们需要创建一个黑马品牌的安全门,该安全门具有防火、防水、防盗的功能。可以将防火,防水,防盗功能提取成一个接口,形成一套规范。类图如下:

上面的设计我们发现了它存在的问题,黑马品牌的安全门具有防盗,防水,防火的功能。现在如果我们还需要再创建一个传智品牌的安全门,而该安全门只具有防盗、防水功能呢?很显然如果实现SafetyDoor接口就违背了接口隔离原则,那么我们如何进行修改呢?看如下类图:

代码如下:

AntiTheft(接口):

1
2
3
public interface AntiTheft {
void antiTheft();
}

Fireproof(接口):

1
2
3
public interface Fireproof {
void fireproof();
}

Waterproof(接口):

1
2
3
public interface Waterproof {
void waterproof();
}

HeiMaSafetyDoor(类):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class HeiMaSafetyDoor implements AntiTheft,Fireproof,Waterproof {
public void antiTheft() {
System.out.println("防盗");
}

public void fireproof() {
System.out.println("防火");
}


public void waterproof() {
System.out.println("防水");
}
}

ItcastSafetyDoor(类):

1
2
3
4
5
6
7
8
9
public class ItcastSafetyDoor implements AntiTheft,Fireproof {
public void antiTheft() {
System.out.println("防盗");
}

public void fireproof() {
System.out.println("防火");
}
}

迪米特法则

迪米特法则又叫最少知识原则。

只和你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。

其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。

迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。

下面看一个例子来理解迪米特法则

【例】明星与经纪人的关系实例

明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如和粉丝的见面会,和媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则。

类图如下:

image-20191229173554296

代码如下:

明星类(Star)

1
2
3
4
5
6
7
8
9
10
11
public class Star {
private String name;

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

public String getName() {
return name;
}
}

粉丝类(Fans)

1
2
3
4
5
6
7
8
9
10
11
public class Fans {
private String name;

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

public String getName() {
return name;
}
}

媒体公司类(Company)

1
2
3
4
5
6
7
8
9
10
11
public class Company {
private String name;

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

public String getName() {
return name;
}
}

经纪人类(Agent)

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
public class Agent {
private Star star;
private Fans fans;
private Company company;

public void setStar(Star star) {
this.star = star;
}

public void setFans(Fans fans) {
this.fans = fans;
}

public void setCompany(Company company) {
this.company = company;
}

public void meeting() {
System.out.println(fans.getName() + "与明星" + star.getName() + "见面了。");
}

public void business() {
System.out.println(company.getName() + "与明星" + star.getName() + "洽淡业务。");
}
}

合成复用原则

合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

通常类的复用分为继承复用和合成复用两种。

继承复用虽然有简单和易实现的优点,但它也存在以下缺点:

  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
  2. 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  3. 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:

  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  2. 对象间的耦合度低。可以在类的成员位置声明抽象。
  3. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

下面看一个例子来理解合成复用原则

【例】汽车分类管理程序

汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。类图如下:

image-20191229173554296

从上面类图我们可以看到使用继承复用产生了很多子类,如果现在又有新的动力源或者新的颜色的话,就需要再定义新的类。我们试着将继承复用改为聚合复用看一下。

image-20191229173554296

创建者模式

创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。

这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。

创建型模式分为:

  • 单例模式
  • 工厂方法模式
  • 抽象工程模式
  • 原型模式
  • 建造者模式

单例设计模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例模式的结构

单例模式的主要有以下角色:

  • 单例类。只能创建一个实例的类
  • 访问类。使用单例类

单例模式的实现

单例设计模式分类两种:

饿汉式:类加载就会导致该单实例对象被创建	

懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
  1. 饿汉式-方式1(静态变量方式)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * 饿汉式
    * 静态变量创建类的对象
    */
    public class Singleton {
    //私有构造方法
    private Singleton() {}

    //在成员位置创建该类的对象
    private static Singleton instance = new Singleton();

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
    return instance;
    }
    }

    说明:

    该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
    
  2. 饿汉式-方式2(静态代码块方式)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
    * 恶汉式
    * 在静态代码块中创建该类对象
    */
    public class Singleton {

    //私有构造方法
    private Singleton() {}

    //在成员位置创建该类的对象
    private static Singleton instance;

    static {
    instance = new Singleton();
    }

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
    return instance;
    }
    }

    说明:

    该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是对着类的加载而创建。所以和饿汉式的方式1基本上一样,当然该方式也存在内存浪费问题。
    
  3. 懒汉式-方式1(线程不安全)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    /**
    * 懒汉式
    * 线程不安全
    */
    public class Singleton {
    //私有构造方法
    private Singleton() {}

    //在成员位置创建该类的对象
    private static Singleton instance;

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {

    if(instance == null) {
    instance = new Singleton();
    }
    return instance;
    }
    }

    说明:

    从上面代码我们可以看出该方式在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。
    
  4. 懒汉式-方式2(线程安全)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    /**
    * 懒汉式
    * 线程安全
    */
    public class Singleton {
    //私有构造方法
    private Singleton() {}

    //在成员位置创建该类的对象
    private static Singleton instance;

    //对外提供静态方法获取该对象
    public static synchronized Singleton getInstance() {

    if(instance == null) {
    instance = new Singleton();
    }
    return instance;
    }
    }

    说明:

    该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。
    
  5. 懒汉式-方式3(双重检查锁)

    再来讨论一下懒汉模式中加锁的问题,对于 getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式

    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 class Singleton {

    //私有构造方法
    private Singleton() {}

    private static Singleton instance;

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
    //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
    if(instance == null) {
    synchronized (Singleton.class) {
    //抢到锁之后再次判断是否为null
    if(instance == null) {
    instance = new Singleton();
    }
    }
    }
    return instance;
    }
    }

    双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。

    要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性。

    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 class Singleton {

    //私有构造方法
    private Singleton() {}

    private static volatile Singleton instance;

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
    //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
    if(instance == null) {
    synchronized (Singleton.class) {
    //抢到锁之后再次判断是否为空
    if(instance == null) {
    instance = new Singleton();
    }
    }
    }
    return instance;
    }
    }

    小结:

    添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。

  6. 懒汉式-方式4(静态内部类方式)

    静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /**
    * 静态内部类方式
    */
    public class Singleton {

    //私有构造方法
    private Singleton() {}

    private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
    }

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
    return SingletonHolder.INSTANCE;
    }
    }

    说明:

    第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder
    

    并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。

    小结:

    静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
    
  7. 枚举方式

    枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
    * 枚举方式
    */
    public enum Singleton {
    INSTANCE;

    public void doSomething() {
    System.out.println("doSomething");
    }
    }

    说明:

    枚举方式属于恶汉式方式。
    

存在的问题

问题演示

破坏单例模式:

使上面定义的单例类(Singleton)可以创建多个对象,枚举方式除外。有两种方式,分别是序列化和反射。

  • 序列化反序列化

    Singleton类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Singleton implements Serializable {

    //私有构造方法
    private Singleton() {}

    private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
    }

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
    return SingletonHolder.INSTANCE;
    }
    }

    Test类:

    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 Test {
    public static void main(String[] args) throws Exception {
    //往文件中写对象
    //writeObject2File();
    //从文件中读取对象
    Singleton s1 = readObjectFromFile();
    Singleton s2 = readObjectFromFile();

    //判断两个反序列化后的对象是否是同一个对象
    System.out.println(s1 == s2);
    }

    private static Singleton readObjectFromFile() throws Exception {
    //创建对象输入流对象
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\Think\\Desktop\\a.txt"));
    //第一个读取Singleton对象
    Singleton instance = (Singleton) ois.readObject();

    return instance;
    }

    public static void writeObject2File() throws Exception {
    //获取Singleton类的对象
    Singleton instance = Singleton.getInstance();
    //创建对象输出流
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\Think\\Desktop\\a.txt"));
    //将instance对象写出到文件中
    oos.writeObject(instance);
    }
    }

    上面代码运行结果是false,表明序列化和反序列化已经破坏了单例设计模式。

  • 反射

    Singleton类:

    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 Singleton {

    //私有构造方法
    private Singleton() {}

    private static volatile Singleton instance;

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {

    if(instance != null) {
    return instance;
    }

    synchronized (Singleton.class) {
    if(instance != null) {
    return instance;
    }
    instance = new Singleton();
    return instance;
    }
    }
    }

    Test类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class Test {
    public static void main(String[] args) throws Exception {
    //获取Singleton类的字节码对象
    Class clazz = Singleton.class;
    //获取Singleton类的私有无参构造方法对象
    Constructor constructor = clazz.getDeclaredConstructor();
    //取消访问检查
    constructor.setAccessible(true);

    //创建Singleton类的对象s1
    Singleton s1 = (Singleton) constructor.newInstance();
    //创建Singleton类的对象s2
    Singleton s2 = (Singleton) constructor.newInstance();

    //判断通过反射创建的两个Singleton对象是否是同一个对象
    System.out.println(s1 == s2);
    }
    }

    上面代码运行结果是false,表明序列化和反序列化已经破坏了单例设计模式

注意:枚举方式不会出现这两个问题。

问题的解决

  • 序列化、反序列方式破坏单例模式的解决方法

    在Singleton类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。

    Singleton类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class Singleton implements Serializable {

    //私有构造方法
    private Singleton() {}

    private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
    }

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
    return SingletonHolder.INSTANCE;
    }

    /**
    * 下面是为了解决序列化反序列化破解单例模式
    */
    private Object readResolve() {
    return SingletonHolder.INSTANCE;
    }
    }

    源码解析:

    ObjectInputStream类

    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
    public final Object readObject() throws IOException, ClassNotFoundException{
    ...
    // if nested read, passHandle contains handle of enclosing object
    int outerHandle = passHandle;
    try {
    Object obj = readObject0(false);//重点查看readObject0方法
    .....
    }

    private Object readObject0(boolean unshared) throws IOException {
    ...
    try {
    switch (tc) {
    ...
    case TC_OBJECT:
    return checkResolve(readOrdinaryObject(unshared));//重点查看readOrdinaryObject方法
    ...
    }
    } finally {
    depth--;
    bin.setBlockDataMode(oldMode);
    }
    }

    private Object readOrdinaryObject(boolean unshared) throws IOException {
    ...
    //isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类,
    obj = desc.isInstantiable() ? desc.newInstance() : null;
    ...
    // 在Singleton类中添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true
    if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
    // 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量
    // 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。
    Object rep = desc.invokeReadResolve(obj);
    ...
    }
    return obj;
    }
  • 反射方式破解单例的解决方法

    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 Singleton {

    //私有构造方法
    private Singleton() {
    /*
    反射破解单例模式需要添加的代码
    */
    if(instance != null) {
    throw new RuntimeException();
    }
    }

    private static volatile Singleton instance;

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {

    if(instance != null) {
    return instance;
    }

    synchronized (Singleton.class) {
    if(instance != null) {
    return instance;
    }
    instance = new Singleton();
    return instance;
    }
    }
    }

    说明:

    这种方式比较好理解。当通过反射方式调用构造方法进行创建创建时,直接抛异常。不运行此中操作。
    

JDK源码解析-Runtime类

Runtime类就是使用的单例设计模式。

  1. 通过源代码查看使用的是哪儿种单例模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
    * Returns the runtime object associated with the current Java application.
    * Most of the methods of class <code>Runtime</code> are instance
    * methods and must be invoked with respect to the current runtime object.
    *
    * @return the <code>Runtime</code> object associated with the current
    * Java application.
    */
    public static Runtime getRuntime() {
    return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
    ...
    }

    从上面源代码中可以看出Runtime类使用的是饿汉式(静态属性)方式来实现单例模式的。

  2. 使用Runtime类中的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class RuntimeDemo {
    public static void main(String[] args) throws IOException {
    //获取Runtime类对象
    Runtime runtime = Runtime.getRuntime();

    //返回 Java 虚拟机中的内存总量。
    System.out.println(runtime.totalMemory());
    //返回 Java 虚拟机试图使用的最大内存量。
    System.out.println(runtime.maxMemory());

    //创建一个新的进程执行指定的字符串命令,返回进程对象
    Process process = runtime.exec("ipconfig");
    //获取命令执行后的结果,通过输入流获取
    InputStream inputStream = process.getInputStream();
    byte[] arr = new byte[1024 * 1024* 100];
    int b = inputStream.read(arr);
    System.out.println(new String(arr,0,b,"gbk"));
    }
    }

工厂模式

概述

需求:设计一个咖啡店点餐系统。

设计一个咖啡类(Coffee),并定义其两个子类(美式咖啡【AmericanCoffee】和拿铁咖啡【LatteCoffee】);再设计一个咖啡店类(CoffeeStore),咖啡店具有点咖啡的功能。

具体类的设计如下:

在java中,万物皆对象,这些对象都需要创建,如果创建的时候直接new该对象,就会对该对象耦合严重,假如我们要更换对象,所有new对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则。如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可,达到了与对象解耦的目的;所以说,工厂模式最大的优点就是:解耦

在本教程中会介绍三种工厂的使用

  • 简单工厂模式(不属于GOF的23种经典设计模式)
  • 工厂方法模式
  • 抽象工厂模式

简单工厂模式

简单工厂不是一种设计模式,反而比较像是一种编程习惯。

结构

简单工厂包含如下角色:

  • 抽象产品 :定义了产品的规范,描述了产品的主要特性和功能。
  • 具体产品 :实现或者继承抽象产品的子类
  • 具体工厂 :提供了创建产品的方法,调用者通过该方法来获取产品。

实现

现在使用简单工厂对上面案例进行改进,类图如下:

工厂类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class SimpleCoffeeFactory {

public Coffee createCoffee(String type) {
Coffee coffee = null;
if("americano".equals(type)) {
coffee = new AmericanoCoffee();
} else if("latte".equals(type)) {
coffee = new LatteCoffee();
}
return coffee;
}
}

工厂(factory)处理创建对象的细节,一旦有了SimpleCoffeeFactory,CoffeeStore类中的orderCoffee()就变成此对象的客户,后期如果需要Coffee对象直接从工厂中获取即可。这样也就解除了和Coffee实现类的耦合,同时又产生了新的耦合,CoffeeStore对象和SimpleCoffeeFactory工厂对象的耦合,工厂对象和商品对象的耦合。

后期如果再加新品种的咖啡,我们势必要需求修改SimpleCoffeeFactory的代码,违反了开闭原则。工厂类的客户端可能有很多,比如创建美团外卖等,这样只需要修改工厂类的代码,省去其他的修改操作。

优缺点

优点:

封装了创建对象的过程,可以通过参数直接获取对象。把对象的创建和业务逻辑层分开,这样以后就避免了修改客户代码,如果要实现新产品直接修改工厂类,而不需要在原代码中修改,这样就降低了客户代码修改的可能性,更加容易扩展。

缺点:

增加新产品时还是需要修改工厂类的代码,违背了“开闭原则”。

扩展

静态工厂

在开发中也有一部分人将工厂类中的创建对象的功能定义为静态的,这个就是静态工厂模式,它也不是23种设计模式中的。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class SimpleCoffeeFactory {

public static Coffee createCoffee(String type) {
Coffee coffee = null;
if("americano".equals(type)) {
coffee = new AmericanoCoffee();
} else if("latte".equals(type)) {
coffee = new LatteCoffee();
}
return coffe;
}
}

工厂方法模式

针对上例中的缺点,使用工厂方法模式就可以完美的解决,完全遵循开闭原则。

概念

定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其工厂的子类。

结构

工厂方法模式的主要角色:

  • 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
  • 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。

实现

使用工厂方法模式对上例进行改进,类图如下:

代码如下:

抽象工厂:

1
2
3
4
public interface CoffeeFactory {

Coffee createCoffee();
}

具体工厂:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LatteCoffeeFactory implements CoffeeFactory {

public Coffee createCoffee() {
return new LatteCoffee();
}
}

public class AmericanCoffeeFactory implements CoffeeFactory {

public Coffee createCoffee() {
return new AmericanCoffee();
}
}

咖啡店类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CoffeeStore {

private CoffeeFactory factory;

public CoffeeStore(CoffeeFactory factory) {
this.factory = factory;
}

public Coffee orderCoffee(String type) {
Coffee coffee = factory.createCoffee();
coffee.addMilk();
coffee.addsugar();
return coffee;
}
}

从以上的编写的代码可以看到,要增加产品类时也要相应地增加工厂类,不需要修改工厂类的代码了,这样就解决了简单工厂模式的缺点。

工厂方法模式是简单工厂模式的进一步抽象。由于使用了多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。

优缺点

优点:

  • 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程;
  • 在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则;

缺点:

  • 每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。

抽象工厂模式

前面介绍的工厂方法模式中考虑的是一类产品的生产,如畜牧场只养动物、电视机厂只生产电视机、传智播客只培养计算机软件专业的学生等。

这些工厂只生产同种类产品,同种类产品称为同等级产品,也就是说:工厂方法模式只考虑生产同等级的产品,但是在现实生活中许多工厂是综合型的工厂,能生产多等级(种类) 的产品,如电器厂既生产电视机又生产洗衣机或空调,大学既有软件专业又有生物专业等。

本节要介绍的抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族,下图所示横轴是产品等级,也就是同一类产品;纵轴是产品族,也就是同一品牌的产品,同一品牌的产品产自同一个工厂。

概念

是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。

抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。

结构

抽象工厂模式的主要角色如下:

  • 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
  • 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它 同具体工厂之间是多对一的关系。

实现

现咖啡店业务发生改变,不仅要生产咖啡还要生产甜点,如提拉米苏、抹茶慕斯等,要是按照工厂方法模式,需要定义提拉米苏类、抹茶慕斯类、提拉米苏工厂、抹茶慕斯工厂、甜点工厂类,很容易发生类爆炸情况。其中拿铁咖啡、美式咖啡是一个产品等级,都是咖啡;提拉米苏、抹茶慕斯也是一个产品等级;拿铁咖啡和提拉米苏是同一产品族(也就是都属于意大利风味),美式咖啡和抹茶慕斯是同一产品族(也就是都属于美式风味)。所以这个案例可以使用抽象工厂模式实现。类图如下:

代码如下:

抽象工厂:

1
2
3
4
5
6
public interface DessertFactory {

Coffee createCoffee();

Dessert createDessert();
}

具体工厂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//美式甜点工厂
public class AmericanDessertFactory implements DessertFactory {

public Coffee createCoffee() {
return new AmericanCoffee();
}

public Dessert createDessert() {
return new MatchaMousse();
}
}
//意大利风味甜点工厂
public class ItalyDessertFactory implements DessertFactory {

public Coffee createCoffee() {
return new LatteCoffee();
}

public Dessert createDessert() {
return new Tiramisu();
}
}

如果要加同一个产品族的话,只需要再加一个对应的工厂类即可,不需要修改其他的类。

优缺点

优点:

当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。

缺点:

当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。

使用场景

  • 当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等。

  • 系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。

  • 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。

如:输入法换皮肤,一整套一起换。生成不同操作系统的程序。

模式扩展

简单工厂+配置文件解除耦合

可以通过工厂模式+配置文件的方式解除工厂对象和产品对象的耦合。在工厂类中加载配置文件中的全类名,并创建对象进行存储,客户端如果需要对象,直接进行获取即可。

第一步:定义配置文件

为了演示方便,我们使用properties文件作为配置文件,名称为bean.properties

1
2
american=com.itheima.pattern.factory.config_factory.AmericanCoffee
latte=com.itheima.pattern.factory.config_factory.LatteCoffee

第二步:改进工厂类

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
public class CoffeeFactory {

private static Map<String,Coffee> map = new HashMap();

static {
Properties p = new Properties();
InputStream is = CoffeeFactory.class.getClassLoader().getResourceAsStream("bean.properties");
try {
p.load(is);
//遍历Properties集合对象
Set<Object> keys = p.keySet();
for (Object key : keys) {
//根据键获取值(全类名)
String className = p.getProperty((String) key);
//获取字节码对象
Class clazz = Class.forName(className);
Coffee obj = (Coffee) clazz.newInstance();
map.put((String)key,obj);
}
} catch (Exception e) {
e.printStackTrace();
}
}

public static Coffee createCoffee(String name) {

return map.get(name);
}
}

静态成员变量用来存储创建的对象(键存储的是名称,值存储的是对应的对象),而读取配置文件以及创建对象写在静态代码块中,目的就是只需要执行一次。

JDK源码解析-Collection.iterator方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Demo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("令狐冲");
list.add("风清扬");
list.add("任我行");

//获取迭代器对象
Iterator<String> it = list.iterator();
//使用迭代器遍历
while(it.hasNext()) {
String ele = it.next();
System.out.println(ele);
}
}
}

对上面的代码大家应该很熟,使用迭代器遍历集合,获取集合中的元素。而单列集合获取迭代器的方法就使用到了工厂方法模式。我们看通过类图看看结构:

Collection接口是抽象工厂类,ArrayList是具体的工厂类;Iterator接口是抽象商品类,ArrayList类中的Iter内部类是具体的商品类。在具体的工厂类中iterator()方法创建具体的商品类的对象。

另:

1,DateForamt类中的getInstance()方法使用的是工厂模式;

2,Calendar类中的getInstance()方法使用的是工厂模式;

原型模式

概述

用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。

结构

原型模式包含如下角色:

  • 抽象原型类:规定了具体原型对象必须实现的的 clone() 方法。
  • 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
  • 访问类:使用具体原型类中的 clone() 方法来复制新的对象。

接口类图如下:

实现

原型模式的克隆分为浅克隆和深克隆。

浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。

深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。

Java中的Object类中提供了 clone() 方法来实现浅克隆。 Cloneable 接口是上面的类图中的抽象原型类,而实现了Cloneable接口的子实现类就是具体的原型类。代码如下:

Realizetype(具体的原型类):

1
2
3
4
5
6
7
8
9
10
11
12
public class Realizetype implements Cloneable {

public Realizetype() {
System.out.println("具体的原型对象创建完成!");
}

@Override
protected Realizetype clone() throws CloneNotSupportedException {
System.out.println("具体原型复制成功!");
return (Realizetype) super.clone();
}
}

PrototypeTest(测试访问类):

1
2
3
4
5
6
7
8
public class PrototypeTest {
public static void main(String[] args) throws CloneNotSupportedException {
Realizetype r1 = new Realizetype();
Realizetype r2 = r1.clone();

System.out.println("对象r1和r2是同一个对象?" + (r1 == r2));
}
}

案例

用原型模式生成“三好学生”奖状

同一学校的“三好学生”奖状除了获奖人姓名不同,其他都相同,可以使用原型模式复制多个“三好学生”奖状出来,然后在修改奖状上的名字即可。

类图如下:

代码如下:

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 Citation implements Cloneable {
private String name;

public void setName(String name) {
this.name = name;
}

public String getName() {
return (this.name);
}

public void show() {
System.out.println(name + "同学:在2020学年第一学期中表现优秀,被评为三好学生。特发此状!");
}

@Override
public Citation clone() throws CloneNotSupportedException {
return (Citation) super.clone();
}
}

//测试访问类
public class CitationTest {
public static void main(String[] args) throws CloneNotSupportedException {
Citation c1 = new Citation();
c1.setName("张三");

//复制奖状
Citation c2 = c1.clone();
//将奖状的名字修改李四
c2.setName("李四");

c1.show();
c2.show();
}
}

使用场景

  • 对象的创建非常复杂,可以使用原型模式快捷的创建对象。
  • 性能和安全要求比较高。

扩展(深克隆)

将上面的“三好学生”奖状的案例中Citation类的name属性修改为Student类型的属性。代码如下:

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
//奖状类
public class Citation implements Cloneable {
private Student stu;

public Student getStu() {
return stu;
}

public void setStu(Student stu) {
this.stu = stu;
}

void show() {
System.out.println(stu.getName() + "同学:在2020学年第一学期中表现优秀,被评为三好学生。特发此状!");
}

@Override
public Citation clone() throws CloneNotSupportedException {
return (Citation) super.clone();
}
}

//学生类
public class Student {
private String name;
private String address;

public Student(String name, String address) {
this.name = name;
this.address = address;
}

public Student() {
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}
}

//测试类
public class CitationTest {
public static void main(String[] args) throws CloneNotSupportedException {

Citation c1 = new Citation();
Student stu = new Student("张三", "西安");
c1.setStu(stu);

//复制奖状
Citation c2 = c1.clone();
//获取c2奖状所属学生对象
Student stu1 = c2.getStu();
stu1.setName("李四");

//判断stu对象和stu1对象是否是同一个对象
System.out.println("stu和stu1是同一个对象?" + (stu == stu1));

c1.show();
c2.show();
}
}

运行结果为:

说明:

stu对象和stu1对象是同一个对象,就会产生将stu1对象中name属性值改为“李四”,两个Citation(奖状)对象中显示的都是李四。这就是浅克隆的效果,对具体原型类(Citation)中的引用类型的属性进行引用的复制。这种情况需要使用深克隆,而进行深克隆需要使用对象流。代码如下:
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 CitationTest1 {
public static void main(String[] args) throws Exception {
Citation c1 = new Citation();
Student stu = new Student("张三", "西安");
c1.setStu(stu);

//创建对象输出流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\Think\\Desktop\\b.txt"));
//将c1对象写出到文件中
oos.writeObject(c1);
oos.close();

//创建对象出入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\Think\\Desktop\\b.txt"));
//读取对象
Citation c2 = (Citation) ois.readObject();
//获取c2奖状所属学生对象
Student stu1 = c2.getStu();
stu1.setName("李四");

//判断stu对象和stu1对象是否是同一个对象
System.out.println("stu和stu1是同一个对象?" + (stu == stu1));

c1.show();
c2.show();
}
}

运行结果为:

注意:Citation类和Student类必须实现Serializable接口,否则会抛NotSerializableException异常。

建造者模式

概述

将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。

  • 分离了部件的构造(由Builder来负责)和装配(由Director负责)。 从而可以构造出复杂的对象。这个模式适用于:某个对象的构建过程复杂的情况。
  • 由于实现了构建和装配的解耦。不同的构建器,相同的装配,也可以做出不同的对象;相同的构建器,不同的装配顺序也可以做出不同的对象。也就是实现了构建算法、装配算法的解耦,实现了更好的复用。
  • 建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节。

结构

建造者(Builder)模式包含如下角色:

  • 抽象建造者类(Builder):这个接口规定要实现复杂对象的那些部分的创建,并不涉及具体的部件对象的创建。

  • 具体建造者类(ConcreteBuilder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,提供产品的实例。

  • 产品类(Product):要创建的复杂对象。

  • 指挥者类(Director):调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。

类图如下:

实例

创建共享单车

生产自行车是一个复杂的过程,它包含了车架,车座等组件的生产。而车架又有碳纤维,铝合金等材质的,车座有橡胶,真皮等材质。对于自行车的生产就可以使用建造者模式。

这里Bike是产品,包含车架,车座等组件;Builder是抽象建造者,MobikeBuilder和OfoBuilder是具体的建造者;Director是指挥者。类图如下:

具体的代码如下:

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
//自行车类
public class Bike {
private String frame;
private String seat;

public String getFrame() {
return frame;
}

public void setFrame(String frame) {
this.frame = frame;
}

public String getSeat() {
return seat;
}

public void setSeat(String seat) {
this.seat = seat;
}
}

// 抽象 builder 类
public abstract class Builder {

protected Bike mBike = new Bike();

public abstract void buildFrame();
public abstract void buildSeat();
public abstract Bike createBike();
}

//摩拜单车Builder类
public class MobikeBuilder extends Builder {

@Override
public void buildFrame() {
mBike.setFrame("铝合金车架");
}

@Override
public void buildSeat() {
mBike.setSeat("真皮车座");
}

@Override
public Bike createBike() {
return mBike;
}
}

//ofo单车Builder类
public class OfoBuilder extends Builder {

@Override
public void buildFrame() {
mBike.setFrame("碳纤维车架");
}

@Override
public void buildSeat() {
mBike.setSeat("橡胶车座");
}

@Override
public Bike createBike() {
return mBike;
}
}

//指挥者类
public class Director {
private Builder mBuilder;

public Director(Builder builder) {
mBuilder = builder;
}

public Bike construct() {
mBuilder.buildFrame();
mBuilder.buildSeat();
return mBuilder.createBike();
}
}

//测试类
public class Client {
public static void main(String[] args) {
showBike(new OfoBuilder());
showBike(new MobikeBuilder());
}
private static void showBike(Builder builder) {
Director director = new Director(builder);
Bike bike = director.construct();
System.out.println(bike.getFrame());
System.out.println(bike.getSeat());
}
}

注意:

上面示例是 Builder模式的常规用法,指挥者类 Director 在建造者模式中具有很重要的作用,它用于指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类,但是有些情况下需要简化系统结构,可以把指挥者类和抽象建造者进行结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 抽象 builder 类
public abstract class Builder {

protected Bike mBike = new Bike();

public abstract void buildFrame();
public abstract void buildSeat();
public abstract Bike createBike();

public Bike construct() {
this.buildFrame();
this.BuildSeat();
return this.createBike();
}
}

说明:

这样做确实简化了系统结构,但同时也加重了抽象建造者类的职责,也不是太符合单一职责原则,如果construct() 过于复杂,建议还是封装到 Director 中。

优缺点

优点:

  • 建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在指挥者类中对整体而言可以取得比较好的稳定性。
  • 在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
  • 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
  • 建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。符合开闭原则。

缺点:

造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。

使用场景

建造者(Builder)模式创建的是复杂对象,其产品的各个部分经常面临着剧烈的变化,但将它们组合在一起的算法却相对稳定,所以它通常在以下场合使用。

  • 创建的对象较复杂,由多个部件构成,各部件面临着复杂的变化,但构件间的建造顺序是稳定的。
  • 创建复杂对象的算法独立于该对象的组成部分以及它们的装配方式,即产品的构建过程和最终的表示是独立的。

模式扩展

建造者模式除了上面的用途外,在开发中还有一个常用的使用方式,就是当一个类构造器需要传入很多参数时,如果创建这个类的实例,代码可读性会非常差,而且很容易引入错误,此时就可以利用建造者模式进行重构。

重构前代码如下:

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
public class Phone {
private String cpu;
private String screen;
private String memory;
private String mainboard;

public Phone(String cpu, String screen, String memory, String mainboard) {
this.cpu = cpu;
this.screen = screen;
this.memory = memory;
this.mainboard = mainboard;
}

public String getCpu() {
return cpu;
}

public void setCpu(String cpu) {
this.cpu = cpu;
}

public String getScreen() {
return screen;
}

public void setScreen(String screen) {
this.screen = screen;
}

public String getMemory() {
return memory;
}

public void setMemory(String memory) {
this.memory = memory;
}

public String getMainboard() {
return mainboard;
}

public void setMainboard(String mainboard) {
this.mainboard = mainboard;
}

@Override
public String toString() {
return "Phone{" +
"cpu='" + cpu + '\'' +
", screen='" + screen + '\'' +
", memory='" + memory + '\'' +
", mainboard='" + mainboard + '\'' +
'}';
}
}

public class Client {
public static void main(String[] args) {
//构建Phone对象
Phone phone = new Phone("intel","三星屏幕","金士顿","华硕");
System.out.println(phone);
}
}

上面在客户端代码中构建Phone对象,传递了四个参数,如果参数更多呢?代码的可读性及使用的成本就是比较高。

重构后代码:

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
public class Phone {

private String cpu;
private String screen;
private String memory;
private String mainboard;

private Phone(Builder builder) {
cpu = builder.cpu;
screen = builder.screen;
memory = builder.memory;
mainboard = builder.mainboard;
}

public static final class Builder {
private String cpu;
private String screen;
private String memory;
private String mainboard;

public Builder() {}

public Builder cpu(String val) {
cpu = val;
return this;
}
public Builder screen(String val) {
screen = val;
return this;
}
public Builder memory(String val) {
memory = val;
return this;
}
public Builder mainboard(String val) {
mainboard = val;
return this;
}
public Phone build() {
return new Phone(this);}
}
@Override
public String toString() {
return "Phone{" +
"cpu='" + cpu + '\'' +
", screen='" + screen + '\'' +
", memory='" + memory + '\'' +
", mainboard='" + mainboard + '\'' +
'}';
}
}

public class Client {
public static void main(String[] args) {
Phone phone = new Phone.Builder()
.cpu("intel")
.mainboard("华硕")
.memory("金士顿")
.screen("三星")
.build();
System.out.println(phone);
}
}

重构后的代码在使用起来更方便,某种程度上也可以提高开发效率。从软件设计上,对程序员的要求比较高。

创建者模式对比

工厂方法模式VS建造者模式

工厂方法模式注重的是整体对象的创建方式;而建造者模式注重的是部件构建的过程,意在通过一步一步地精确构造创建出一个复杂的对象。

我们举个简单例子来说明两者的差异,如要制造一个超人,如果使用工厂方法模式,直接产生出来的就是一个力大无穷、能够飞翔、内裤外穿的超人;而如果使用建造者模式,则需要组装手、头、脚、躯干等部分,然后再把内裤外穿,于是一个超人就诞生了。

抽象工厂模式VS建造者模式

抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式则是不需要关心构建过程,只关心什么产品由什么工厂生产即可。

建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品。

如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完整的汽车。

结构型模式

结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。

由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。

结构型模式分为以下 7 种:

  • 代理模式
  • 适配器模式
  • 装饰者模式
  • 桥接模式
  • 外观模式
  • 组合模式
  • 享元模式

代理模式

概述

由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK代理和CGLib代理两种。

结构

代理(Proxy)模式分为三种角色:

  • 抽象主题(Subject)类: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。
  • 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
  • 代理(Proxy)类 : 提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。

静态代理

我们通过案例来感受一下静态代理。

【例】火车站卖票

如果要买火车票的话,需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦。而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。这个例子其实就是典型的代理模式,火车站是目标对象,代售点是代理对象。类图如下:

代码如下:

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
//卖票接口
public interface SellTickets {
void sell();
}

//火车站 火车站具有卖票功能,所以需要实现SellTickets接口
public class TrainStation implements SellTickets {

public void sell() {
System.out.println("火车站卖票");
}
}

//代售点
public class ProxyPoint implements SellTickets {

private TrainStation station = new TrainStation();

public void sell() {
System.out.println("代理点收取一些服务费用");
station.sell();
}
}

//测试类
public class Client {
public static void main(String[] args) {
ProxyPoint pp = new ProxyPoint();
pp.sell();
}
}

从上面代码中可以看出测试类直接访问的是ProxyPoint类对象,也就是说ProxyPoint作为访问对象和目标对象的中介。同时也对sell方法进行了增强(代理点收取一些服务费用)。

JDK动态代理

接下来我们使用动态代理实现上面案例,先说说JDK提供的动态代理。Java中提供了一个动态代理类Proxy,Proxy并不是我们上述所说的代理对象的类,而是提供了一个创建代理对象的静态方法(newProxyInstance方法)来获取代理对象。

代码如下:

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
//卖票接口
public interface SellTickets {
void sell();
}

//火车站 火车站具有卖票功能,所以需要实现SellTickets接口
public class TrainStation implements SellTickets {

public void sell() {
System.out.println("火车站卖票");
}
}

//代理工厂,用来创建代理对象
public class ProxyFactory {

private TrainStation station = new TrainStation();

public SellTickets getProxyObject() {
//使用Proxy获取代理对象
/*
newProxyInstance()方法参数说明:
ClassLoader loader : 类加载器,用于加载代理类,使用真实对象的类加载器即可
Class<?>[] interfaces : 真实对象所实现的接口,代理模式真实对象和代理对象实现相同的接口
InvocationHandler h : 代理对象的调用处理程序
*/
SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(station.getClass().getClassLoader(),
station.getClass().getInterfaces(),
new InvocationHandler() {
/*
InvocationHandler中invoke方法参数说明:
proxy : 代理对象
method : 对应于在代理对象上调用的接口方法的 Method 实例
args : 代理对象调用接口方法时传递的实际参数
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

System.out.println("代理点收取一些服务费用(JDK动态代理方式)");
//执行真实对象
Object result = method.invoke(station, args);
return result;
}
});
return sellTickets;
}
}

//测试类
public class Client {
public static void main(String[] args) {
//获取代理对象
ProxyFactory factory = new ProxyFactory();

SellTickets proxyObject = factory.getProxyObject();
proxyObject.sell();
}
}

使用了动态代理,我们思考下面问题:

  • ProxyFactory是代理类吗?

    ProxyFactory不是代理模式中所说的代理类,而代理类是程序在运行过程中动态的在内存中生成的类。通过阿里巴巴开源的 Java 诊断工具(Arthas【阿尔萨斯】)查看代理类的结构:

    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
    82
    83
    package com.sun.proxy;

    import com.itheima.proxy.dynamic.jdk.SellTickets;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.lang.reflect.UndeclaredThrowableException;

    public final class $Proxy0 extends Proxy implements SellTickets {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $Proxy0(InvocationHandler invocationHandler) {
    super(invocationHandler);
    }

    static {
    try {
    m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
    m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
    m3 = Class.forName("com.itheima.proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]);
    m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
    return;
    }
    catch (NoSuchMethodException noSuchMethodException) {
    throw new NoSuchMethodError(noSuchMethodException.getMessage());
    }
    catch (ClassNotFoundException classNotFoundException) {
    throw new NoClassDefFoundError(classNotFoundException.getMessage());
    }
    }

    public final boolean equals(Object object) {
    try {
    return (Boolean)this.h.invoke(this, m1, new Object[]{object});
    }
    catch (Error | RuntimeException throwable) {
    throw throwable;
    }
    catch (Throwable throwable) {
    throw new UndeclaredThrowableException(throwable);
    }
    }

    public final String toString() {
    try {
    return (String)this.h.invoke(this, m2, null);
    }
    catch (Error | RuntimeException throwable) {
    throw throwable;
    }
    catch (Throwable throwable) {
    throw new UndeclaredThrowableException(throwable);
    }
    }

    public final int hashCode() {
    try {
    return (Integer)this.h.invoke(this, m0, null);
    }
    catch (Error | RuntimeException throwable) {
    throw throwable;
    }
    catch (Throwable throwable) {
    throw new UndeclaredThrowableException(throwable);
    }
    }

    public final void sell() {
    try {
    this.h.invoke(this, m3, null);
    return;
    }
    catch (Error | RuntimeException throwable) {
    throw throwable;
    }
    catch (Throwable throwable) {
    throw new UndeclaredThrowableException(throwable);
    }
    }
    }

    从上面的类中,我们可以看到以下几个信息:

    • 代理类($Proxy0)实现了SellTickets。这也就印证了我们之前说的真实类和代理类实现同样的接口。
    • 代理类($Proxy0)将我们提供了的匿名内部类对象传递给了父类。
  • 动态代理的执行流程是什么样?

    下面是摘取的重点代码:

    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
    //程序运行过程中动态生成的代理类
    public final class $Proxy0 extends Proxy implements SellTickets {
    private static Method m3;

    public $Proxy0(InvocationHandler invocationHandler) {
    super(invocationHandler);
    }

    static {
    m3 = Class.forName("com.itheima.proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]);
    }

    public final void sell() {
    this.h.invoke(this, m3, null);
    }
    }

    //Java提供的动态代理相关类
    public class Proxy implements java.io.Serializable {
    protected InvocationHandler h;

    protected Proxy(InvocationHandler h) {
    this.h = h;
    }
    }

    //代理工厂类
    public class ProxyFactory {

    private TrainStation station = new TrainStation();

    public SellTickets getProxyObject() {
    SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(station.getClass().getClassLoader(),
    station.getClass().getInterfaces(),
    new InvocationHandler() {

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    System.out.println("代理点收取一些服务费用(JDK动态代理方式)");
    Object result = method.invoke(station, args);
    return result;
    }
    });
    return sellTickets;
    }
    }


    //测试访问类
    public class Client {
    public static void main(String[] args) {
    //获取代理对象
    ProxyFactory factory = new ProxyFactory();
    SellTickets proxyObject = factory.getProxyObject();
    proxyObject.sell();
    }
    }

执行流程如下:

1. 在测试类中通过代理对象调用sell()方法
2. 根据多态的特性,执行的是代理类($Proxy0)中的sell()方法
3. 代理类($Proxy0)中的sell()方法中又调用了InvocationHandler接口的子实现类对象的invoke方法
4. invoke方法通过反射执行了真实对象所属类(TrainStation)中的sell()方法

CGLIB动态代理

同样是上面的案例,我们再次使用CGLIB代理实现。

如果没有定义SellTickets接口,只定义了TrainStation(火车站类)。很显然JDK代理是无法使用了,因为JDK动态代理要求必须定义接口,对接口进行代理。

CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。

CGLIB是第三方提供的包,所以需要引入jar包的坐标:

1
2
3
4
5
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>

代码如下:

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
//火车站
public class TrainStation {

public void sell() {
System.out.println("火车站卖票");
}
}

//代理工厂
public class ProxyFactory implements MethodInterceptor {

private TrainStation target = new TrainStation();

public TrainStation getProxyObject() {
//创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数
Enhancer enhancer =new Enhancer();
//设置父类的字节码对象
enhancer.setSuperclass(target.getClass());
//设置回调函数
enhancer.setCallback(this);
//创建代理对象
TrainStation obj = (TrainStation) enhancer.create();
return obj;
}

/*
intercept方法参数说明:
o : 代理对象
method : 真实对象中的方法的Method实例
args : 实际参数
methodProxy :代理对象中的方法的method实例
*/
public TrainStation intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("代理点收取一些服务费用(CGLIB动态代理方式)");
TrainStation result = (TrainStation) methodProxy.invokeSuper(o, args);
return result;
}
}

//测试类
public class Client {
public static void main(String[] args) {
//创建代理工厂对象
ProxyFactory factory = new ProxyFactory();
//获取代理对象
TrainStation proxyObject = factory.getProxyObject();

proxyObject.sell();
}
}

三种代理的对比

  • jdk代理和CGLIB代理

    使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的类或者方法进行代理,因为CGLib原理是动态生成被代理类的子类。

    在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。所以如果有接口使用JDK动态代理,如果没有接口使用CGLIB代理。

  • 动态代理和静态代理

    动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。

    如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题

优缺点

优点:

  • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
  • 代理对象可以扩展目标对象的功能;
  • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;

缺点:

  • 增加了系统的复杂度;

使用场景

  • 远程(Remote)代理

    本地服务通过网络请求远程服务。为了实现本地到远程的通信,我们需要实现网络通信,处理其中可能的异常。为良好的代码设计和可维护性,我们将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节。

  • 防火墙(Firewall)代理

    当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器。

  • 保护(Protect or Access)代理

    控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。

适配器模式

概述

如果去欧洲国家去旅游的话,他们的插座如下图最左边,是欧洲标准。而我们使用的插头如下图最右边的。因此我们的笔记本电脑,手机在当地不能直接充电。所以就需要一个插座转换器,转换器第1面插入当地的插座,第2面供我们充电,这样使得我们的插头在当地能使用。生活中这样的例子很多,手机充电器(将220v转换为5v的电压),读卡器等,其实就是使用到了适配器模式。

定义:

将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。

适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。

结构

适配器模式(Adapter)包含以下主要角色:

  • 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
  • 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
  • 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。

类适配器模式

实现方式:定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。

【例】读卡器

现有一台电脑只能读取SD卡,而要读取TF卡中的内容的话就需要使用到适配器模式。创建一个读卡器,将TF卡中的内容读取出来。

类图如下:

代码如下:

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
//SD卡的接口
public interface SDCard {
//读取SD卡方法
String readSD();
//写入SD卡功能
void writeSD(String msg);
}

//SD卡实现类
public class SDCardImpl implements SDCard {
public String readSD() {
String msg = "sd card read a msg :hello word SD";
return msg;
}

public void writeSD(String msg) {
System.out.println("sd card write msg : " + msg);
}
}

//电脑类
public class Computer {

public String readSD(SDCard sdCard) {
if(sdCard == null) {
throw new NullPointerException("sd card null");
}
return sdCard.readSD();
}
}

//TF卡接口
public interface TFCard {
//读取TF卡方法
String readTF();
//写入TF卡功能
void writeTF(String msg);
}

//TF卡实现类
public class TFCardImpl implements TFCard {

public String readTF() {
String msg ="tf card read msg : hello word tf card";
return msg;
}

public void writeTF(String msg) {
System.out.println("tf card write a msg : " + msg);
}
}

//定义适配器类(SD兼容TF)
public class SDAdapterTF extends TFCardImpl implements SDCard {

public String readSD() {
System.out.println("adapter read tf card ");
return readTF();
}

public void writeSD(String msg) {
System.out.println("adapter write tf card");
writeTF(msg);
}
}

//测试类
public class Client {
public static void main(String[] args) {
Computer computer = new Computer();
SDCard sdCard = new SDCardImpl();
System.out.println(computer.readSD(sdCard));

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

SDAdapterTF adapter = new SDAdapterTF();
System.out.println(computer.readSD(adapter));
}
}

类适配器模式违背了合成复用原则。类适配器是客户类有一个接口规范的情况下可用,反之不可用。

对象适配器模式

实现方式:对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。

【例】读卡器

我们使用对象适配器模式将读卡器的案例进行改写。类图如下:

代码如下:

类适配器模式的代码,我们只需要修改适配器类(SDAdapterTF)和测试类。

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
//创建适配器对象(SD兼容TF)
public class SDAdapterTF implements SDCard {

private TFCard tfCard;

public SDAdapterTF(TFCard tfCard) {
this.tfCard = tfCard;
}

public String readSD() {
System.out.println("adapter read tf card ");
return tfCard.readTF();
}

public void writeSD(String msg) {
System.out.println("adapter write tf card");
tfCard.writeTF(msg);
}
}

//测试类
public class Client {
public static void main(String[] args) {
Computer computer = new Computer();
SDCard sdCard = new SDCardImpl();
System.out.println(computer.readSD(sdCard));

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

TFCard tfCard = new TFCardImpl();
SDAdapterTF adapter = new SDAdapterTF(tfCard);
System.out.println(computer.readSD(adapter));
}
}

注意:还有一个适配器模式是接口适配器模式。当不希望实现一个接口中所有的方法时,可以创建一个抽象类Adapter ,实现所有方法。而此时我们只需要继承该抽象类即可。

5.2.5 应用场景

  • 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
  • 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。

JDK源码解析

Reader(字符流)、InputStream(字节流)的适配使用的是InputStreamReader。

InputStreamReader继承自java.io包中的Reader,对他中的抽象的未实现的方法给出实现。如:

1
2
3
4
5
6
7
public int read() throws IOException {
return sd.read();
}

public int read(char cbuf[], int offset, int length) throws IOException {
return sd.read(cbuf, offset, length);
}

如上代码中的sd(StreamDecoder类对象),在Sun的JDK实现中,实际的方法实现是对sun.nio.cs.StreamDecoder类的同名方法的调用封装。类结构图如下:

从上图可以看出:

  • InputStreamReader是对同样实现了Reader的StreamDecoder的封装。
  • StreamDecoder不是Java SE API中的内容,是Sun JDK给出的自身实现。但我们知道他们对构造方法中的字节流类(InputStream)进行封装,并通过该类进行了字节流和字符流之间的解码转换。

结论:

从表层来看,InputStreamReader做了InputStream字节流类到Reader字符流之间的转换。而从如上Sun JDK中的实现类关系结构中可以看出,是StreamDecoder的设计实现在实际上采用了适配器模式。

5.3 装饰者模式

概述

我们先来看一个快餐店的例子。

快餐店有炒面、炒饭这些快餐,可以额外附加鸡蛋、火腿、培根这些配菜,当然加配菜需要额外加钱,每个配菜的价钱通常不太一样,那么计算总价就会显得比较麻烦。

使用继承的方式存在的问题:

  • 扩展性不好

    如果要再加一种配料(火腿肠),我们就会发现需要给FriedRice和FriedNoodles分别定义一个子类。如果要新增一个快餐品类(炒河粉)的话,就需要定义更多的子类。

  • 产生过多的子类

定义:

指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。

结构

装饰(Decorator)模式中的角色:

  • 抽象构件(Component)角色 :定义一个抽象接口以规范准备接收附加责任的对象。
  • 具体构件(Concrete Component)角色 :实现抽象构件,通过装饰角色为其添加一些职责。
  • 抽象装饰(Decorator)角色 : 继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
  • 具体装饰(ConcreteDecorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

案例

我们使用装饰者模式对快餐店案例进行改进,体会装饰者模式的精髓。

类图如下:

代码如下:

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//快餐接口
public abstract class FastFood {
private float price;
private String desc;

public FastFood() {
}

public FastFood(float price, String desc) {
this.price = price;
this.desc = desc;
}

public void setPrice(float price) {
this.price = price;
}

public float getPrice() {
return price;
}

public String getDesc() {
return desc;
}

public void setDesc(String desc) {
this.desc = desc;
}

public abstract float cost(); //获取价格
}

//炒饭
public class FriedRice extends FastFood {

public FriedRice() {
super(10, "炒饭");
}

public float cost() {
return getPrice();
}
}

//炒面
public class FriedNoodles extends FastFood {

public FriedNoodles() {
super(12, "炒面");
}

public float cost() {
return getPrice();
}
}

//配料类
public abstract class Garnish extends FastFood {

private FastFood fastFood;

public FastFood getFastFood() {
return fastFood;
}

public void setFastFood(FastFood fastFood) {
this.fastFood = fastFood;
}

public Garnish(FastFood fastFood, float price, String desc) {
super(price,desc);
this.fastFood = fastFood;
}
}

//鸡蛋配料
public class Egg extends Garnish {

public Egg(FastFood fastFood) {
super(fastFood,1,"鸡蛋");
}

public float cost() {
return getPrice() + getFastFood().getPrice();
}

@Override
public String getDesc() {
return super.getDesc() + getFastFood().getDesc();
}
}

//培根配料
public class Bacon extends Garnish {

public Bacon(FastFood fastFood) {

super(fastFood,2,"培根");
}

@Override
public float cost() {
return getPrice() + getFastFood().getPrice();
}

@Override
public String getDesc() {
return super.getDesc() + getFastFood().getDesc();
}
}

//测试类
public class Client {
public static void main(String[] args) {
//点一份炒饭
FastFood food = new FriedRice();
//花费的价格
System.out.println(food.getDesc() + " " + food.cost() + "元");

System.out.println("========");
//点一份加鸡蛋的炒饭
FastFood food1 = new FriedRice();

food1 = new Egg(food1);
//花费的价格
System.out.println(food1.getDesc() + " " + food1.cost() + "元");

System.out.println("========");
//点一份加培根的炒面
FastFood food2 = new FriedNoodles();
food2 = new Bacon(food2);
//花费的价格
System.out.println(food2.getDesc() + " " + food2.cost() + "元");
}
}

好处:

  • 饰者模式可以带来比继承更加灵活性的扩展功能,使用更加方便,可以通过组合不同的装饰者对象来获取具有不同行为状态的多样化的结果。装饰者模式比继承更具良好的扩展性,完美的遵循开闭原则,继承是静态的附加责任,装饰者则是动态的附加责任。

  • 装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。

5.3.4 使用场景

  • 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。

    不能采用继承的情况主要有两类:

    • 第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;
    • 第二类是因为类定义不能继承(如final类)
  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。

  • 当对象的功能要求可以动态地添加,也可以再动态地撤销时。

JDK源码解析

IO流中的包装类使用到了装饰者模式。BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter。

我们以BufferedWriter举例来说明,先看看如何使用BufferedWriter

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo {
public static void main(String[] args) throws Exception{
//创建BufferedWriter对象
//创建FileWriter对象
FileWriter fw = new FileWriter("C:\\Users\\Think\\Desktop\\a.txt");
BufferedWriter bw = new BufferedWriter(fw);

//写数据
bw.write("hello Buffered");

bw.close();
}
}

使用起来感觉确实像是装饰者模式,接下来看它们的结构:

小结:

BufferedWriter使用装饰者模式对Writer子实现类进行了增强,添加了缓冲区,提高了写数据的效率。

代理和装饰者的区别

静态代理和装饰者模式的区别:

  • 相同点:
    • 都要实现与目标类相同的业务接口
    • 在两个类中都要声明目标对象
    • 都可以在不修改目标类的前提下增强目标方法
  • 不同点:
    • 目的不同
      装饰者是为了增强目标对象
      静态代理是为了保护和隐藏目标对象
    • 获取目标对象构建的地方不同
      装饰者是由外界传递进来,可以通过构造方法传递
      静态代理是在代理类内部创建,以此来隐藏目标对象

桥接模式

概述

现在有一个需求,需要创建不同的图形,并且每个图形都有可能会有不同的颜色。我们可以利用继承的方式来设计类的关系:

我们可以发现有很多的类,假如我们再增加一个形状或再增加一种颜色,就需要创建更多的类。

试想,在一个有多种可能会变化的维度的系统中,用继承方式会造成类爆炸,扩展起来不灵活。每次在一个维度上新增一个具体实现都要增加多个子类。为了更加灵活的设计系统,我们此时可以考虑使用桥接模式。

定义:

将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。

结构

桥接(Bridge)模式包含以下主要角色:

  • 抽象化(Abstraction)角色 :定义抽象类,并包含一个对实现化对象的引用。
  • 扩展抽象化(Refined Abstraction)角色 :是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
  • 实现化(Implementor)角色 :定义实现化角色的接口,供扩展抽象化角色调用。
  • 具体实现化(Concrete Implementor)角色 :给出实现化角色接口的具体实现。

案例

【例】视频播放器

需要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Mac、Linux等)上播放多种格式的视频文件,常见的视频格式包括RMVB、AVI、WMV等。该播放器包含了两个维度,适合使用桥接模式。

类图如下:

代码如下:

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
//视频文件
public interface VideoFile {
void decode(String fileName);
}

//avi文件
public class AVIFile implements VideoFile {
public void decode(String fileName) {
System.out.println("avi视频文件:"+ fileName);
}
}

//rmvb文件
public class REVBBFile implements VideoFile {

public void decode(String fileName) {
System.out.println("rmvb文件:" + fileName);
}
}

//操作系统版本
public abstract class OperatingSystemVersion {

protected VideoFile videoFile;

public OperatingSystemVersion(VideoFile videoFile) {
this.videoFile = videoFile;
}

public abstract void play(String fileName);
}

//Windows版本
public class Windows extends OperatingSystem {

public Windows(VideoFile videoFile) {
super(videoFile);
}

public void play(String fileName) {
videoFile.decode(fileName);
}
}

//mac版本
public class Mac extends OperatingSystemVersion {

public Mac(VideoFile videoFile) {
super(videoFile);
}

public void play(String fileName) {
videoFile.decode(fileName);
}
}

//测试类
public class Client {
public static void main(String[] args) {
OperatingSystem os = new Windows(new AVIFile());
os.play("战狼3");
}
}

好处:

  • 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。

    如:如果现在还有一种视频文件类型wmv,我们只需要再定义一个类实现VideoFile接口即可,其他类不需要发生变化。

  • 实现细节对客户透明

使用场景

  • 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
  • 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
  • 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。

外观模式

概述

有些人可能炒过股票,但其实大部分人都不太懂,这种没有足够了解证券知识的情况下做股票是很容易亏钱的,刚开始炒股肯定都会想,如果有个懂行的帮帮手就好,其实基金就是个好帮手,支付宝里就有许多的基金,它将投资者分散的资金集中起来,交由专业的经理人进行管理,投资于股票、债券、外汇等领域,而基金投资的收益归持有者所有,管理机构收取一定比例的托管管理费用。

定义:

又名门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。

外观(Facade)模式是“迪米特法则”的典型应用

结构

外观(Facade)模式包含以下主要角色:

  • 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
  • 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。

案例

【例】智能家电控制

小明的爷爷已经60岁了,一个人在家生活:每次都需要打开灯、打开电视、打开空调;睡觉时关闭灯、关闭电视、关闭空调;操作起来都比较麻烦。所以小明给爷爷买了智能音箱,可以通过语音直接控制这些智能家电的开启和关闭。类图如下:

代码如下:

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
82
83
//灯类
public class Light {
public void on() {
System.out.println("打开了灯....");
}

public void off() {
System.out.println("关闭了灯....");
}
}

//电视类
public class TV {
public void on() {
System.out.println("打开了电视....");
}

public void off() {
System.out.println("关闭了电视....");
}
}

//控制类
public class AirCondition {
public void on() {
System.out.println("打开了空调....");
}

public void off() {
System.out.println("关闭了空调....");
}
}

//智能音箱
public class SmartAppliancesFacade {

private Light light;
private TV tv;
private AirCondition airCondition;

public SmartAppliancesFacade() {
light = new Light();
tv = new TV();
airCondition = new AirCondition();
}

public void say(String message) {
if(message.contains("打开")) {
on();
} else if(message.contains("关闭")) {
off();
} else {
System.out.println("我还听不懂你说的!!!");
}
}

//起床后一键开电器
private void on() {
System.out.println("起床了");
light.on();
tv.on();
airCondition.on();
}

//睡觉一键关电器
private void off() {
System.out.println("睡觉了");
light.off();
tv.off();
airCondition.off();
}
}

//测试类
public class Client {
public static void main(String[] args) {
//创建外观对象
SmartAppliancesFacade facade = new SmartAppliancesFacade();
//客户端直接与外观对象进行交互
facade.say("打开家电");
facade.say("关闭家电");
}
}

好处:

  • 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
  • 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。

缺点:

  • 不符合开闭原则,修改很麻烦

使用场景

  • 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
  • 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
  • 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。

源码解析

使用tomcat作为web容器时,接收浏览器发送过来的请求,tomcat会将请求信息封装成ServletRequest对象,如下图①处对象。但是大家想想ServletRequest是一个接口,它还有一个子接口HttpServletRequest,而我们知道该request对象肯定是一个HttpServletRequest对象的子实现类对象,到底是哪个类的对象呢?可以通过输出request对象,我们就会发现是一个名为RequestFacade的类的对象。

RequestFacade类就使用了外观模式。先看结构图:

为什么在此处使用外观模式呢?

定义 RequestFacade 类,分别实现 ServletRequest ,同时定义私有成员变量 Request ,并且方法的实现调用 Request  的实现。然后,将 RequestFacade上转为 ServletRequest  传给 servlet 的 service 方法,这样即使在 servlet 中被下转为 RequestFacade ,也不能访问私有成员变量对象中的方法。既用了 Request ,又能防止其中方法被不合理的访问。

组合模式

概述

对于这个图片肯定会非常熟悉,上图我们可以看做是一个文件系统,对于这样的结构我们称之为树形结构。在树形结构中可以通过调用某个方法来遍历整个树,当我们找到某个叶子节点后,就可以对叶子节点进行相关的操作。可以将这颗树理解成一个大的容器,容器里面包含很多的成员对象,这些成员对象即可是容器对象也可以是叶子对象。但是由于容器对象和叶子对象在功能上面的区别,使得我们在使用的过程中必须要区分容器对象和叶子对象,但是这样就会给客户带来不必要的麻烦,作为客户而已,它始终希望能够一致的对待容器对象和叶子对象。

定义:

又名部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。

结构

组合模式主要包含三种角色:

  • 抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
  • 树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。
  • 叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。

5.6.3 案例实现

【例】软件菜单

如下图,我们在访问别的一些管理系统时,经常可以看到类似的菜单。一个菜单可以包含菜单项(菜单项是指不再包含其他内容的菜单条目),也可以包含带有其他菜单项的菜单,因此使用组合模式描述菜单就很恰当,我们的需求是针对一个菜单,打印出其包含的所有菜单以及菜单项的名称。

要实现该案例,我们先画出类图:

代码实现:

不管是菜单还是菜单项,都应该继承自统一的接口,这里姑且将这个统一的接口称为菜单组件。

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 abstract class MenuComponent {

protected String name;
protected int level;

//添加菜单
public void add(MenuComponent menuComponent){
throw new UnsupportedOperationException();
}

//移除菜单
public void remove(MenuComponent menuComponent){
throw new UnsupportedOperationException();
}

//获取指定的子菜单
public MenuComponent getChild(int i){
throw new UnsupportedOperationException();
}

//获取菜单名称
public String getName(){
return name;
}

public void print(){
throw new UnsupportedOperationException();
}
}

这里的MenuComponent定义为抽象类,因为有一些共有的属性和行为要在该类中实现,Menu和MenuItem类就可以只覆盖自己感兴趣的方法,而不用搭理不需要或者不感兴趣的方法,举例来说,Menu类可以包含子菜单,因此需要覆盖add()、remove()、getChild()方法,但是MenuItem就不应该有这些方法。这里给出的默认实现是抛出异常,你也可以根据自己的需要改写默认实现。

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 Menu extends MenuComponent {

private List<MenuComponent> menuComponentList;

public Menu(String name,int level){
this.level = level;
this.name = name;
menuComponentList = new ArrayList<MenuComponent>();
}

@Override
public void add(MenuComponent menuComponent) {
menuComponentList.add(menuComponent);
}

@Override
public void remove(MenuComponent menuComponent) {
menuComponentList.remove(menuComponent);
}

@Override
public MenuComponent getChild(int i) {
return menuComponentList.get(i);
}

@Override
public void print() {

for (int i = 1; i < level; i++) {
System.out.print("--");
}
System.out.println(name);
for (MenuComponent menuComponent : menuComponentList) {
menuComponent.print();
}
}
}

Menu类已经实现了除了getName方法的其他所有方法,因为Menu类具有添加菜单,移除菜单和获取子菜单的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MenuItem extends MenuComponent {

public MenuItem(String name,int level) {
this.name = name;
this.level = level;
}

@Override
public void print() {
for (int i = 1; i < level; i++) {
System.out.print("--");
}
System.out.println(name);
}
}

MenuItem是菜单项,不能再有子菜单,所以添加菜单,移除菜单和获取子菜单的功能并不能实现。

组合模式的分类

在使用组合模式时,根据抽象构件类的定义形式,我们可将组合模式分为透明组合模式和安全组合模式两种形式。

  • 透明组合模式

    透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中 MenuComponent 声明了 addremovegetChild 方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。

    透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)

  • 安全组合模式

    在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点 Menu 类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。

优点

  • 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
  • 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
  • 在组合模式中增加新的树枝节点和叶子节点都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
  • 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。

使用场景

组合模式正是应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。比如:文件目录显示,多级目录呈现等树形结构数据的操作。

享元模式

概述

定义:

运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。

结构

享元(Flyweight )模式中存在以下两种状态:

  1. 内部状态,即不会随着环境的改变而改变的可共享部分。
  2. 外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。

享元模式的主要有以下角色:

  • 抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
  • 具体享元(Concrete Flyweight)角色 :它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
  • 非享元(Unsharable Flyweight)角色 :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
  • 享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

案例实现

【例】俄罗斯方块

下面的图片是众所周知的俄罗斯方块中的一个个方块,如果在俄罗斯方块这个游戏中,每个不同的方块都是一个实例对象,这些对象就要占用很多的内存空间,下面利用享元模式进行实现。

先来看类图:

代码如下:

俄罗斯方块有不同的形状,我们可以对这些形状向上抽取出AbstractBox,用来定义共性的属性和行为。

1
2
3
4
5
6
7
public abstract class AbstractBox {
public abstract String getShape();

public void display(String color) {
System.out.println("方块形状:" + this.getShape() + " 颜色:" + color);
}
}

接下来就是定义不同的形状了,IBox类、LBox类、OBox类等。

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 IBox extends AbstractBox {

@Override
public String getShape() {
return "I";
}
}

public class LBox extends AbstractBox {

@Override
public String getShape() {
return "L";
}
}

public class OBox extends AbstractBox {

@Override
public String getShape() {
return "O";
}
}

提供了一个工厂类(BoxFactory),用来管理享元对象(也就是AbstractBox子类对象),该工厂类对象只需要一个,所以可以使用单例模式。并给工厂类提供一个获取形状的方法。

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 BoxFactory {

private static HashMap<String, AbstractBox> map;

private BoxFactory() {
map = new HashMap<String, AbstractBox>();
AbstractBox iBox = new IBox();
AbstractBox lBox = new LBox();
AbstractBox oBox = new OBox();
map.put("I", iBox);
map.put("L", lBox);
map.put("O", oBox);
}

public static final BoxFactory getInstance() {
return SingletonHolder.INSTANCE;
}

private static class SingletonHolder {
private static final BoxFactory INSTANCE = new BoxFactory();
}

public AbstractBox getBox(String key) {
return map.get(key);
}
}

优缺点和使用场景

1,优点

  • 极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能
  • 享元模式中的外部状态相对独立,且不影响内部状态

2,缺点:

为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂

3,使用场景:

  • 一个系统有大量相同或者相似的对象,造成内存的大量耗费。
  • 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
  • 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。

JDK源码解析

Integer类使用了享元模式。我们先看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo {
public static void main(String[] args) {
Integer i1 = 127;
Integer i2 = 127;

System.out.println("i1和i2对象是否是同一个对象?" + (i1 == i2));

Integer i3 = 128;
Integer i4 = 128;

System.out.println("i3和i4对象是否是同一个对象?" + (i3 == i4));
}
}

运行上面代码,结果如下:

为什么第一个输出语句输出的是true,第二个输出语句输出的是false?通过反编译软件进行反编译,代码如下:

1
2
3
4
5
6
7
8
9
10
public class Demo {
public static void main(String[] args) {
Integer i1 = Integer.valueOf((int)127);
Integer i2 Integer.valueOf((int)127);
System.out.println((String)new StringBuilder().append((String)"i1\u548ci2\u5bf9\u8c61\u662f\u5426\u662f\u540c\u4e00\u4e2a\u5bf9\u8c61\uff1f").append((boolean)(i1 == i2)).toString());
Integer i3 = Integer.valueOf((int)128);
Integer i4 = Integer.valueOf((int)128);
System.out.println((String)new StringBuilder().append((String)"i3\u548ci4\u5bf9\u8c61\u662f\u5426\u662f\u540c\u4e00\u4e2a\u5bf9\u8c61\uff1f").append((boolean)(i3 == i4)).toString());
}
}

上面代码可以看到,直接给Integer类型的变量赋值基本数据类型数据的操作底层使用的是 valueOf() ,所以只需要看该方法即可

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
public final class Integer extends Number implements Comparable<Integer> {

public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];

static {
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}

private IntegerCache() {}
}
}

可以看到 Integer 默认先创建并缓存 -128 ~ 127 之间数的 Integer 对象,当调用 valueOf 时如果参数在 -128 ~ 127 之间则计算下标并从缓存中返回,否则创建一个新的 Integer 对象。

行为型模式

行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。

行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。

行为型模式分为:

  • 模板方法模式
  • 策略模式
  • 命令模式
  • 职责链模式
  • 状态模式
  • 观察者模式
  • 中介者模式
  • 迭代器模式
  • 访问者模式
  • 备忘录模式
  • 解释器模式

以上 11 种行为型模式,除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式。

模板方法模式

概述

在面向对象程序设计过程中,程序员常常会遇到这种情况:设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但某些步骤的具体实现还未知,或者说某些步骤的实现与具体的环境相关。

例如,去银行办理业务一般要经过以下4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。

定义:

定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。

结构

模板方法(Template Method)模式包含以下主要角色:

  • 抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。

    • 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。

    • 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:

      • 抽象方法(Abstract Method) :一个抽象方法由抽象类声明、由其具体子类实现。

      • 具体方法(Concrete Method) :一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。

      • 钩子方法(Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。

        一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。

  • 具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。

案例实现

【例】炒菜

炒菜的步骤是固定的,分为倒油、热油、倒蔬菜、倒调料品、翻炒等步骤。现通过模板方法模式来用代码模拟。类图如下:

代码如下:

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
public abstract class AbstractClass {

public final void cookProcess() {
//第一步:倒油
this.pourOil();
//第二步:热油
this.heatOil();
//第三步:倒蔬菜
this.pourVegetable();
//第四步:倒调味料
this.pourSauce();
//第五步:翻炒
this.fry();
}

public void pourOil() {
System.out.println("倒油");
}

//第二步:热油是一样的,所以直接实现
public void heatOil() {
System.out.println("热油");
}

//第三步:倒蔬菜是不一样的(一个下包菜,一个是下菜心)
public abstract void pourVegetable();

//第四步:倒调味料是不一样
public abstract void pourSauce();


//第五步:翻炒是一样的,所以直接实现
public void fry(){
System.out.println("炒啊炒啊炒到熟啊");
}
}

public class ConcreteClass_BaoCai extends AbstractClass {

@Override
public void pourVegetable() {
System.out.println("下锅的蔬菜是包菜");
}

@Override
public void pourSauce() {
System.out.println("下锅的酱料是辣椒");
}
}

public class ConcreteClass_CaiXin extends AbstractClass {
@Override
public void pourVegetable() {
System.out.println("下锅的蔬菜是菜心");
}

@Override
public void pourSauce() {
System.out.println("下锅的酱料是蒜蓉");
}
}

public class Client {
public static void main(String[] args) {
//炒手撕包菜
ConcreteClass_BaoCai baoCai = new ConcreteClass_BaoCai();
baoCai.cookProcess();

//炒蒜蓉菜心
ConcreteClass_CaiXin caiXin = new ConcreteClass_CaiXin();
caiXin.cookProcess();
}
}

注意:为防止恶意操作,一般模板方法都加上 final 关键词。

6.1.3 优缺点

优点:

  • 提高代码复用性

    将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。

  • 实现了反向控制

    通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。

缺点:

  • 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
  • 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。

适用场景

  • 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
  • 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。

6.1.5 JDK源码解析

InputStream类就使用了模板方法模式。在InputStream类中定义了多个 read() 方法,如下:

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 abstract class InputStream implements Closeable {
//抽象方法,要求子类必须重写
public abstract int read() throws IOException;

public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}

public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}

int c = read(); //调用了无参的read方法,该方法是每次读取一个字节数据
if (c == -1) {
return -1;
}
b[off] = (byte)c;

int i = 1;
try {
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
}

从上面代码可以看到,无参的 read() 方法是抽象方法,要求子类必须实现。而 read(byte b[]) 方法调用了 read(byte b[], int off, int len) 方法,所以在此处重点看的方法是带三个参数的方法。

在该方法中第18行、27行,可以看到调用了无参的抽象的 read() 方法。

总结如下: 在InputStream父类中已经定义好了读取一个字节数组数据的方法是每次读取一个字节,并将其存储到数组的第一个索引位置,读取len个字节数据。具体如何读取一个字节数据呢?由子类实现。

策略模式

6.2.1 概述

先看下面的图片,我们去旅游选择出行模式有很多种,可以骑自行车、可以坐汽车、可以坐火车、可以坐飞机。

作为一个程序猿,开发需要选择一款开发工具,当然可以进行代码开发的工具有很多,可以选择Idea进行开发,也可以使用eclipse进行开发,也可以使用其他的一些开发工具。

定义:

该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

结构

策略模式的主要角色如下:

  • 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
  • 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
  • 环境(Context)类:持有一个策略类的引用,最终给客户端调用。

案例实现

【例】促销活动

一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,由促销员将促销活动展示给客户。类图如下:

代码如下:

定义百货公司所有促销活动的共同接口

1
2
3
public interface Strategy {
void show();
}

定义具体策略角色(Concrete Strategy):每个节日具体的促销活动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//为春节准备的促销活动A
public class StrategyA implements Strategy {

public void show() {
System.out.println("买一送一");
}
}

//为中秋准备的促销活动B
public class StrategyB implements Strategy {

public void show() {
System.out.println("满200元减50元");
}
}

//为圣诞准备的促销活动C
public class StrategyC implements Strategy {

public void show() {
System.out.println("满1000元加一元换购任意200元以下商品");
}
}

定义环境角色(Context):用于连接上下文,即把促销活动推销给客户,这里可以理解为销售员

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SalesMan {                        
//持有抽象策略角色的引用
private Strategy strategy;

public SalesMan(Strategy strategy) {
this.strategy = strategy;
}

//向客户展示促销活动
public void salesManShow(){
strategy.show();
}
}

优缺点

1,优点:

  • 策略类之间可以自由切换

    由于策略类都实现同一个接口,所以使它们之间可以自由切换。

  • 易于扩展

    增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“

  • 避免使用多重条件选择语句(if else),充分体现面向对象设计思想。

2,缺点:

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
  • 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。

使用场景

  • 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
  • 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
  • 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。
  • 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。

JDK源码解析

Comparator 中的策略模式。在Arrays类中有一个 sort() 方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class Arrays{
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (c == null) {
sort(a);
} else {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}
}

Arrays就是一个环境角色类,这个sort方法可以传一个新策略让Arrays根据这个策略来进行排序。就比如下面的测试类。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class demo {
public static void main(String[] args) {

Integer[] data = {12, 2, 3, 2, 4, 5, 1};
// 实现降序排序
Arrays.sort(data, new Comparator<Integer>() {
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
System.out.println(Arrays.toString(data)); //[12, 5, 4, 3, 2, 2, 1]
}
}

这里我们在调用Arrays的sort方法时,第二个参数传递的是Comparator接口的子实现类对象。所以Comparator充当的是抽象策略角色,而具体的子实现类充当的是具体策略角色。环境角色类(Arrays)应该持有抽象策略的引用来调用。那么,Arrays类的sort方法到底有没有使用Comparator子实现类中的 compare() 方法吗?让我们继续查看TimSort类的 sort() 方法,代码如下:

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
class TimSort<T> {
static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,
T[] work, int workBase, int workLen) {
assert c != null && a != null && lo >= 0 && lo <= hi && hi <= a.length;

int nRemaining = hi - lo;
if (nRemaining < 2)
return; // Arrays of size 0 and 1 are always sorted

// If array is small, do a "mini-TimSort" with no merges
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
binarySort(a, lo, hi, lo + initRunLen, c);
return;
}
...
}

private static <T> int countRunAndMakeAscending(T[] a, int lo, int hi,Comparator<? super T> c) {
assert lo < hi;
int runHi = lo + 1;
if (runHi == hi)
return 1;

// Find end of run, and reverse range if descending
if (c.compare(a[runHi++], a[lo]) < 0) { // Descending
while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
runHi++;
reverseRange(a, lo, runHi);
} else { // Ascending
while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
runHi++;
}

return runHi - lo;
}
}

上面的代码中最终会跑到 countRunAndMakeAscending() 这个方法中。我们可以看见,只用了compare方法,所以在调用Arrays.sort方法只传具体compare重写方法的类对象就行,这也是Comparator接口中必须要子类实现的一个方法。

命令模式

概述

日常生活中,我们出去吃饭都会遇到下面的场景。

定义:

将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理。

6.3.2 结构

命令模式包含以下主要角色:

  • 抽象命令类(Command)角色: 定义命令的接口,声明执行的方法。
  • 具体命令(Concrete Command)角色:具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
  • 实现者/接收者(Receiver)角色: 接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
  • 调用者/请求者(Invoker)角色: 要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。

案例实现

将上面的案例用代码实现,那我们就需要分析命令模式的角色在该案例中由谁来充当。

服务员: 就是调用者角色,由她来发起命令。

资深大厨: 就是接收者角色,真正命令执行的对象。

订单: 命令中包含订单。

类图如下:

代码如下:

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
public interface Command {
void execute();//只需要定义一个统一的执行方法
}

public class OrderCommand implements Command {

//持有接受者对象
private SeniorChef receiver;
private Order order;

public OrderCommand(SeniorChef receiver, Order order){
this.receiver = receiver;
this.order = order;
}

public void execute() {
System.out.println(order.getDiningTable() + "桌的订单:");
Set<String> keys = order.getFoodDic().keySet();
for (String key : keys) {
receiver.makeFood(order.getFoodDic().get(key),key);
}

try {
Thread.sleep(100);//停顿一下 模拟做饭的过程
} catch (InterruptedException e) {
e.printStackTrace();
}


System.out.println(order.getDiningTable() + "桌的饭弄好了");
}
}

public class Order {
// 餐桌号码
private int diningTable;

// 用来存储餐名并记录份数
private Map<String, Integer> foodDic = new HashMap<String, Integer>();

public int getDiningTable() {
return diningTable;
}

public void setDiningTable(int diningTable) {
this.diningTable = diningTable;
}

public Map<String, Integer> getFoodDic() {
return foodDic;
}

public void setFoodDic(String name, int num) {
foodDic.put(name,num);
}
}

// 资深大厨类 是命令的Receiver
public class SeniorChef {

public void makeFood(int num,String foodName) {
System.out.println(num + "份" + foodName);
}
}

public class Waitor {

private ArrayList<Command> commands;//可以持有很多的命令对象

public Waitor() {
commands = new ArrayList();
}

public void setCommand(Command cmd){
commands.add(cmd);
}

// 发出命令 喊 订单来了,厨师开始执行
public void orderUp() {
System.out.println("美女服务员:叮咚,大厨,新订单来了.......");
for (int i = 0; i < commands.size(); i++) {
Command cmd = commands.get(i);
if (cmd != null) {
cmd.execute();
}
}
}
}

public class Client {
public static void main(String[] args) {
//创建2个order
Order order1 = new Order();
order1.setDiningTable(1);
order1.getFoodDic().put("西红柿鸡蛋面",1);
order1.getFoodDic().put("小杯可乐",2);

Order order2 = new Order();
order2.setDiningTable(3);
order2.getFoodDic().put("尖椒肉丝盖饭",1);
order2.getFoodDic().put("小杯雪碧",1);

//创建接收者
SeniorChef receiver=new SeniorChef();
//将订单和接收者封装成命令对象
OrderCommand cmd1 = new OrderCommand(receiver, order1);
OrderCommand cmd2 = new OrderCommand(receiver, order2);
//创建调用者 waitor
Waitor invoker = new Waitor();
invoker.setCommand(cmd1);
invoker.setCommand(cmd2);

//将订单带到柜台 并向厨师喊 订单来了
invoker.orderUp();
}
}

优缺点

1,优点:

  • 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
  • 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
  • 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
  • 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。

2,缺点:

  • 使用命令模式可能会导致某些系统有过多的具体命令类。
  • 系统结构更加复杂。

6.3.5 使用场景

  • 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
  • 系统需要在不同的时间指定请求、将请求排队和执行请求。
  • 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。

JDK源码解析

Runable是一个典型命令模式,Runnable担当命令的角色,Thread充当的是调用者,start方法就是其执行方法

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
//命令接口(抽象命令角色)
public interface Runnable {
public abstract void run();
}

//调用者
public class Thread implements Runnable {
private Runnable target;

public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();

group.add(this);

boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}

private native void start0();
}

会调用一个native方法start0(),调用系统方法,开启一个线程。而接收者是对程序员开放的,可以自己定义接收者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* jdk Runnable 命令模式
* TurnOffThread : 属于具体
*/
public class TurnOffThread implements Runnable{
private Receiver receiver;

public TurnOffThread(Receiver receiver) {
this.receiver = receiver;
}
public void run() {
receiver.turnOFF();
}
}
1
2
3
4
5
6
7
8
9
10
11
/**
* 测试类
*/
public class Demo {
public static void main(String[] args) {
Receiver receiver = new Receiver();
TurnOffThread turnOffThread = new TurnOffThread(receiver);
Thread thread = new Thread(turnOffThread);
thread.start();
}
}

责任链模式

概述

在现实生活中,常常会出现这样的事例:一个请求有多个对象可以处理,但每个对象的处理条件或权限不同。例如,公司员工请假,可批假的领导有部门负责人、副总经理、总经理等,但每个领导能批准的天数不同,员工必须根据自己要请假的天数去找不同的领导签名,也就是说员工必须记住每个领导的姓名、电话和地址等信息,这增加了难度。这样的例子还有很多,如找领导出差报销、生活中的“击鼓传花”游戏等。

定义:

又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。

结构

职责链模式主要包含以下角色:

  • 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
  • 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
  • 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。

案例实现

现需要开发一个请假流程控制系统。请假一天以下的假只需要小组长同意即可;请假1天到3天的假还需要部门经理同意;请求3天到7天还需要总经理同意才行。

类图如下:

代码如下:

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//请假条
public class LeaveRequest {
private String name;//姓名
private int num;//请假天数
private String content;//请假内容

public LeaveRequest(String name, int num, String content) {
this.name = name;
this.num = num;
this.content = content;
}

public String getName() {
return name;
}

public int getNum() {
return num;
}

public String getContent() {
return content;
}
}

//处理者抽象类
public abstract class Handler {
protected final static int NUM_ONE = 1;
protected final static int NUM_THREE = 3;
protected final static int NUM_SEVEN = 7;

//该领导处理的请假天数区间
private int numStart;
private int numEnd;

//领导上面还有领导
private Handler nextHandler;

//设置请假天数范围 上不封顶
public Handler(int numStart) {
this.numStart = numStart;
}

//设置请假天数范围
public Handler(int numStart, int numEnd) {
this.numStart = numStart;
this.numEnd = numEnd;
}

//设置上级领导
public void setNextHandler(Handler nextHandler){
this.nextHandler = nextHandler;
}

//提交请假条
public final void submit(LeaveRequest leave){
if(0 == this.numStart){
return;
}

//如果请假天数达到该领导者的处理要求
if(leave.getNum() >= this.numStart){
this.handleLeave(leave);

//如果还有上级 并且请假天数超过了当前领导的处理范围
if(null != this.nextHandler && leave.getNum() > numEnd){
this.nextHandler.submit(leave);//继续提交
} else {
System.out.println("流程结束");
}
}
}

//各级领导处理请假条方法
protected abstract void handleLeave(LeaveRequest leave);
}

//小组长
public class GroupLeader extends Handler {
public GroupLeader() {
//小组长处理1-3天的请假
super(Handler.NUM_ONE, Handler.NUM_THREE);
}

@Override
protected void handleLeave(LeaveRequest leave) {
System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。");
System.out.println("小组长审批:同意。");
}
}

//部门经理
public class Manager extends Handler {
public Manager() {
//部门经理处理3-7天的请假
super(Handler.NUM_THREE, Handler.NUM_SEVEN);
}

@Override
protected void handleLeave(LeaveRequest leave) {
System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。");
System.out.println("部门经理审批:同意。");
}
}

//总经理
public class GeneralManager extends Handler {
public GeneralManager() {
//部门经理处理7天以上的请假
super(Handler.NUM_SEVEN);
}

@Override
protected void handleLeave(LeaveRequest leave) {
System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。");
System.out.println("总经理审批:同意。");
}
}

//测试类
public class Client {
public static void main(String[] args) {
//请假条来一张
LeaveRequest leave = new LeaveRequest("小花",5,"身体不适");

//各位领导
GroupLeader groupLeader = new GroupLeader();
Manager manager = new Manager();
GeneralManager generalManager = new GeneralManager();

groupLeader.setNextHandler(manager);//小组长的领导是部门经理
manager.setNextHandler(generalManager);//部门经理的领导是总经理
//之所以在这里设置上级领导,是因为可以根据实际需求来更改设置,如果实战中上级领导人都是固定的,则可以移到领导实现类中。

//提交申请
groupLeader.submit(leave);
}
}

优缺点

1,优点:

  • 降低了对象之间的耦合度

    该模式降低了请求发送者和接收者的耦合度。

  • 增强了系统的可扩展性

    可以根据需要增加新的请求处理类,满足开闭原则。

  • 增强了给对象指派职责的灵活性

    当工作流程发生变化,可以动态地改变链内的成员或者修改它们的次序,也可动态地新增或者删除责任。

  • 责任链简化了对象之间的连接

    一个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。

  • 责任分担

    每个类只需要处理自己该处理的工作,不能处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。

2,缺点:

  • 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
  • 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
  • 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。

源码解析

在javaWeb应用开发中,FilterChain是职责链(过滤器)模式的典型应用,以下是Filter的模拟实现分析:

  • 模拟web请求Request以及web响应Response

    1
    2
    3
    4
    5
    6
    7
    public interface Request{

    }

    public interface Response{

    }
  • 模拟web过滤器Filter

    1
    2
    3
    public interface Filter {
    public void doFilter(Request req,Response res,FilterChain 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
    public class FirstFilter implements Filter {
    @Override
    public void doFilter(Request request, Response response, FilterChain chain) {

    System.out.println("过滤器1 前置处理");

    // 先执行所有request再倒序执行所有response
    chain.doFilter(request, response);

    System.out.println("过滤器1 后置处理");
    }
    }

    public class SecondFilter implements Filter {
    @Override
    public void doFilter(Request request, Response response, FilterChain chain) {

    System.out.println("过滤器2 前置处理");

    // 先执行所有request再倒序执行所有response
    chain.doFilter(request, response);

    System.out.println("过滤器2 后置处理");
    }
    }
  • 模拟实现过滤器链FilterChain

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class FilterChain {

    private List<Filter> filters = new ArrayList<Filter>();

    private int index = 0;

    // 链式调用
    public FilterChain addFilter(Filter filter) {
    this.filters.add(filter);
    return this;
    }

    public void doFilter(Request request, Response response) {
    if (index == filters.size()) {
    return;
    }
    Filter filter = filters.get(index);
    index++;
    filter.doFilter(request, response, this);
    }
    }
  • 测试类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Client {
    public static void main(String[] args) {
    Request req = null;
    Response res = null ;

    FilterChain filterChain = new FilterChain();
    filterChain.addFilter(new FirstFilter()).addFilter(new SecondFilter());
    filterChain.doFilter(req,res);
    }
    }

状态模式

概述

【例】通过按钮来控制一个电梯的状态,一个电梯有开门状态,关门状态,停止状态,运行状态。每一种状态改变,都有可能要根据其他状态来更新处理。例如,如果电梯门现在处于运行时状态,就不能进行开门操作,而如果电梯门是停止状态,就可以执行开门操作。

类图如下:

代码如下:

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
public interface ILift {
//电梯的4个状态
//开门状态
public final static int OPENING_STATE = 1;
//关门状态
public final static int CLOSING_STATE = 2;
//运行状态
public final static int RUNNING_STATE = 3;
//停止状态
public final static int STOPPING_STATE = 4;

//设置电梯的状态
public void setState(int state);

//电梯的动作
public void open();
public void close();
public void run();
public void stop();
}

public class Lift implements ILift {
private int state;

@Override
public void setState(int state) {
this.state = state;
}

//执行关门动作
@Override
public void close() {
switch (this.state) {
case OPENING_STATE:
System.out.println("电梯关门了。。。");//只有开门状态可以关闭电梯门,可以对应电梯状态表来看
this.setState(CLOSING_STATE);//关门之后电梯就是关闭状态了
break;
case CLOSING_STATE:
//do nothing //已经是关门状态,不能关门
break;
case RUNNING_STATE:
//do nothing //运行时电梯门是关着的,不能关门
break;
case STOPPING_STATE:
//do nothing //停止时电梯也是关着的,不能关门
break;
}
}

//执行开门动作
@Override
public void open() {
switch (this.state) {
case OPENING_STATE://门已经开了,不能再开门了
//do nothing
break;
case CLOSING_STATE://关门状态,门打开:
System.out.println("电梯门打开了。。。");
this.setState(OPENING_STATE);
break;
case RUNNING_STATE:
//do nothing 运行时电梯不能开门
break;
case STOPPING_STATE:
System.out.println("电梯门开了。。。");//电梯停了,可以开门了
this.setState(OPENING_STATE);
break;
}
}

//执行运行动作
@Override
public void run() {
switch (this.state) {
case OPENING_STATE://电梯不能开着门就走
//do nothing
break;
case CLOSING_STATE://门关了,可以运行了
System.out.println("电梯开始运行了。。。");
this.setState(RUNNING_STATE);//现在是运行状态
break;
case RUNNING_STATE:
//do nothing 已经是运行状态了
break;
case STOPPING_STATE:
System.out.println("电梯开始运行了。。。");
this.setState(RUNNING_STATE);
break;
}
}

//执行停止动作
@Override
public void stop() {
switch (this.state) {
case OPENING_STATE: //开门的电梯已经是是停止的了(正常情况下)
//do nothing
break;
case CLOSING_STATE://关门时才可以停止
System.out.println("电梯停止了。。。");
this.setState(STOPPING_STATE);
break;
case RUNNING_STATE://运行时当然可以停止了
System.out.println("电梯停止了。。。");
this.setState(STOPPING_STATE);
break;
case STOPPING_STATE:
//do nothing
break;
}
}
}

public class Client {
public static void main(String[] args) {
Lift lift = new Lift();
lift.setState(ILift.STOPPING_STATE);//电梯是停止的
lift.open();//开门
lift.close();//关门
lift.run();//运行
lift.stop();//停止
}
}

问题分析:

  • 使用了大量的switch…case这样的判断(if…else也是一样),使程序的可阅读性变差。
  • 扩展性很差。如果新加了断电的状态,我们需要修改上面判断逻辑

定义:

对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。

结构

状态模式包含以下主要角色。

  • 环境(Context)角色:也称为上下文,它定义了客户程序需要的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理。
  • 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为。
  • 具体状态(Concrete State)角色:实现抽象状态所对应的行为。

案例实现

对上述电梯的案例使用状态模式进行改进。类图如下:

代码如下:

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
//抽象状态类
public abstract class LiftState {
//定义一个环境角色,也就是封装状态的变化引起的功能变化
protected Context context;

public void setContext(Context context) {
this.context = context;
}

//电梯开门动作
public abstract void open();

//电梯关门动作
public abstract void close();

//电梯运行动作
public abstract void run();

//电梯停止动作
public abstract void stop();
}

//开启状态
public class OpenningState extends LiftState {

//开启当然可以关闭了,我就想测试一下电梯门开关功能
@Override
public void open() {
System.out.println("电梯门开启...");
}

@Override
public void close() {
//状态修改
super.context.setLiftState(Context.closeingState);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().close();
}

//电梯门不能开着就跑,这里什么也不做
@Override
public void run() {
//do nothing
}

//开门状态已经是停止的了
@Override
public void stop() {
//do nothing
}
}

//运行状态
public class RunningState extends LiftState {

//运行的时候开电梯门?你疯了!电梯不会给你开的
@Override
public void open() {
//do nothing
}

//电梯门关闭?这是肯定了
@Override
public void close() {//虽然可以关门,但这个动作不归我执行
//do nothing
}

//这是在运行状态下要实现的方法
@Override
public void run() {
System.out.println("电梯正在运行...");
}

//这个事绝对是合理的,光运行不停止还有谁敢做这个电梯?!估计只有上帝了
@Override
public void stop() {
super.context.setLiftState(Context.stoppingState);
super.context.stop();
}
}

//停止状态
public class StoppingState extends LiftState {

//停止状态,开门,那是要的!
@Override
public void open() {
//状态修改
super.context.setLiftState(Context.openningState);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().open();
}

@Override
public void close() {//虽然可以关门,但这个动作不归我执行
//状态修改
super.context.setLiftState(Context.closeingState);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().close();
}

//停止状态再跑起来,正常的很
@Override
public void run() {
//状态修改
super.context.setLiftState(Context.runningState);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().run();
}

//停止状态是怎么发生的呢?当然是停止方法执行了
@Override
public void stop() {
System.out.println("电梯停止了...");
}
}

//关闭状态
public class ClosingState extends LiftState {

@Override
//电梯门关闭,这是关闭状态要实现的动作
public void close() {
System.out.println("电梯门关闭...");
}

//电梯门关了再打开,逗你玩呢,那这个允许呀
@Override
public void open() {
super.context.setLiftState(Context.openningState);
super.context.open();
}


//电梯门关了就跑,这是再正常不过了
@Override
public void run() {
super.context.setLiftState(Context.runningState);
super.context.run();
}

//电梯门关着,我就不按楼层
@Override
public void stop() {
super.context.setLiftState(Context.stoppingState);
super.context.stop();
}
}

//环境角色
public class Context {
//定义出所有的电梯状态
public final static OpenningState openningState = new OpenningState();//开门状态,这时候电梯只能关闭
public final static ClosingState closeingState = new ClosingState();//关闭状态,这时候电梯可以运行、停止和开门
public final static RunningState runningState = new RunningState();//运行状态,这时候电梯只能停止
public final static StoppingState stoppingState = new StoppingState();//停止状态,这时候电梯可以开门、运行


//定义一个当前电梯状态
private LiftState liftState;

public LiftState getLiftState() {
return this.liftState;
}

public void setLiftState(LiftState liftState) {
//当前环境改变
this.liftState = liftState;
//把当前的环境通知到各个实现类中
this.liftState.setContext(this);
}

public void open() {
this.liftState.open();
}

public void close() {
this.liftState.close();
}

public void run() {
this.liftState.run();
}

public void stop() {
this.liftState.stop();
}
}

//测试类
public class Client {
public static void main(String[] args) {
Context context = new Context();
context.setLiftState(new ClosingState());

context.open();
context.close();
context.run();
context.stop();
}
}

优缺点

1,优点:

  • 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
  • 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。

2,缺点:

  • 状态模式的使用必然会增加系统类和对象的个数。
  • 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
  • 状态模式对”开闭原则”的支持并不太好。

使用场景

  • 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
  • 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。

观察者模式

概述

定义:

又被称为发布-订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。

结构

在观察者模式中有如下角色:

  • Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。
  • ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
  • Observer:抽象观察者,是观察者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。
  • ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。

案例实现

【例】微信公众号

在使用微信公众号时,大家都会有这样的体验,当你关注的公众号中有新内容更新的话,它就会推送给关注公众号的微信用户端。我们使用观察者模式来模拟这样的场景,微信用户就是观察者,微信公众号是被观察者,有多个的微信用户关注了程序猿这个公众号。

类图如下:

代码如下:

定义抽象观察者类,里面定义一个更新的方法

1
2
3
public interface Observer {
void update(String message);
}

定义具体观察者类,微信用户是观察者,里面实现了更新的方法

1
2
3
4
5
6
7
8
9
10
11
12
public class WeixinUser implements Observer {
// 微信用户名
private String name;

public WeixinUser(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + "-" + message);
}
}

定义抽象主题类,提供了attach、detach、notify三个方法

1
2
3
4
5
6
7
8
9
10
11
public interface Subject {
//增加订阅者
public void attach(Observer observer);

//删除订阅者
public void detach(Observer observer);

//通知订阅者更新消息
public void notify(String message);
}

微信公众号是具体主题(具体被观察者),里面存储了订阅该公众号的微信用户,并实现了抽象主题中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SubscriptionSubject implements Subject {
//储存订阅公众号的微信用户
private List<Observer> weixinUserlist = new ArrayList<Observer>();

@Override
public void attach(Observer observer) {
weixinUserlist.add(observer);
}

@Override
public void detach(Observer observer) {
weixinUserlist.remove(observer);
}

@Override
public void notify(String message) {
for (Observer observer : weixinUserlist) {
observer.update(message);
}
}
}

客户端程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Client {
public static void main(String[] args) {
SubscriptionSubject mSubscriptionSubject=new SubscriptionSubject();
//创建微信用户
WeixinUser user1=new WeixinUser("孙悟空");
WeixinUser user2=new WeixinUser("猪悟能");
WeixinUser user3=new WeixinUser("沙悟净");
//订阅公众号
mSubscriptionSubject.attach(user1);
mSubscriptionSubject.attach(user2);
mSubscriptionSubject.attach(user3);
//公众号更新发出消息给订阅的微信用户
mSubscriptionSubject.notify("传智黑马的专栏更新了");
}
}

优缺点

1,优点:

  • 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。
  • 被观察者发送通知,所有注册的观察者都会收到信息【可以实现广播机制】

2,缺点:

  • 如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会耗时
  • 如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统崩溃

使用场景

  • 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
  • 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时。

6.6.6 JDK中提供的实现

在 Java 中,通过 java.util.Observable 类和 java.util.Observer 接口定义了观察者模式,只要实现它们的子类就可以编写观察者模式实例。

1,Observable类

Observable 类是抽象目标类(被观察者),它有一个 Vector 集合成员变量,用于保存所有要通知的观察者对象,下面来介绍它最重要的 3 个方法。

  • void addObserver(Observer o) 方法:用于将新的观察者对象添加到集合中。

  • void notifyObservers(Object arg) 方法:调用集合中的所有观察者对象的 update方法,通知它们数据发生改变。通常越晚加入集合的观察者越先得到通知。

  • void setChange() 方法:用来设置一个 boolean 类型的内部标志,注明目标对象发生了变化。当它为true时,notifyObservers() 才会通知观察者。

2,Observer 接口

Observer 接口是抽象观察者,它监视目标对象的变化,当目标对象发生变化时,观察者得到通知,并调用 update 方法,进行相应的工作。

【例】警察抓小偷

警察抓小偷也可以使用观察者模式来实现,警察是观察者,小偷是被观察者。代码如下:

小偷是一个被观察者,所以需要继承Observable类

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 Thief extends Observable {

private String name;

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

public void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void steal() {
System.out.println("小偷:我偷东西了,有没有人来抓我!!!");
super.setChanged(); //changed = true
super.notifyObservers();
}
}

警察是一个观察者,所以需要让其实现Observer接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Policemen implements Observer {

private String name;

public Policemen(String name) {
this.name = name;
}
public void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}

@Override
public void update(Observable o, Object arg) {
System.out.println("警察:" + ((Thief) o).getName() + ",我已经盯你很久了,你可以保持沉默,但你所说的将成为呈堂证供!!!");
}
}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
public class Client {
public static void main(String[] args) {
//创建小偷对象
Thief t = new Thief("隔壁老王");
//创建警察对象
Policemen p = new Policemen("小李");
//让警察盯着小偷
t.addObserver(p);
//小偷偷东西
t.steal();
}
}

中介者模式

概述

一般来说,同事类之间的关系是比较复杂的,多个同事类之间互相关联时,他们之间的关系会呈现为复杂的网状结构,这是一种过度耦合的架构,即不利于类的复用,也不稳定。例如在下左图中,有六个同事类对象,假如对象1发生变化,那么将会有4个对象受到影响。如果对象2发生变化,那么将会有5个对象受到影响。也就是说,同事类之间直接关联的设计是不好的。

如果引入中介者模式,那么同事类之间的关系将变为星型结构,从下右图中可以看到,任何一个类的变动,只会影响的类本身,以及中介者,这样就减小了系统的耦合。一个好的设计,必定不会把所有的对象关系处理逻辑封装在本类中,而是使用一个专门的类来管理那些不属于自己的行为。

定义:

又叫调停模式,定义一个中介角色来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。

结构

中介者模式包含以下主要角色:

  • 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。

  • 具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。

  • 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。

  • 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。

案例实现

【例】租房

现在租房基本都是通过房屋中介,房主将房屋托管给房屋中介,而租房者从房屋中介获取房屋信息。房屋中介充当租房者与房屋所有者之间的中介者。

类图如下:

代码如下:

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
//抽象中介者
public abstract class Mediator {
//申明一个联络方法
public abstract void constact(String message,Person person);
}

//抽象同事类
public abstract class Person {
protected String name;
protected Mediator mediator;

public Person(String name,Mediator mediator){
this.name = name;
this.mediator = mediator;
}
}

//具体同事类 房屋拥有者
public class HouseOwner extends Person {

public HouseOwner(String name, Mediator mediator) {
super(name, mediator);
}

//与中介者联系
public void constact(String message){
mediator.constact(message, this);
}

//获取信息
public void getMessage(String message){
System.out.println("房主" + name +"获取到的信息:" + message);
}
}

//具体同事类 承租人
public class Tenant extends Person {
public Tenant(String name, Mediator mediator) {
super(name, mediator);
}

//与中介者联系
public void constact(String message){
mediator.constact(message, this);
}

//获取信息
public void getMessage(String message){
System.out.println("租房者" + name +"获取到的信息:" + message);
}
}

//中介机构
public class MediatorStructure extends Mediator {
//首先中介结构必须知道所有房主和租房者的信息
private HouseOwner houseOwner;
private Tenant tenant;

public HouseOwner getHouseOwner() {
return houseOwner;
}

public void setHouseOwner(HouseOwner houseOwner) {
this.houseOwner = houseOwner;
}

public Tenant getTenant() {
return tenant;
}

public void setTenant(Tenant tenant) {
this.tenant = tenant;
}

public void constact(String message, Person person) {
if (person == houseOwner) { //如果是房主,则租房者获得信息
tenant.getMessage(message);
} else { //反正则是房主获得信息
houseOwner.getMessage(message);
}
}
}

//测试类
public class Client {
public static void main(String[] args) {
//一个房主、一个租房者、一个中介机构
MediatorStructure mediator = new MediatorStructure();

//房主和租房者只需要知道中介机构即可
HouseOwner houseOwner = new HouseOwner("张三", mediator);
Tenant tenant = new Tenant("李四", mediator);

//中介结构要知道房主和租房者
mediator.setHouseOwner(houseOwner);
mediator.setTenant(tenant);

tenant.constact("需要租三室的房子");
houseOwner.constact("我这有三室的房子,你需要租吗?");
}
}

优缺点

1,优点:

  • 松散耦合

    中介者模式通过把多个同事对象之间的交互封装到中介者对象里面,从而使得同事对象之间松散耦合,基本上可以做到互补依赖。这样一来,同事对象就可以独立地变化和复用,而不再像以前那样“牵一处而动全身”了。

  • 集中控制交互

    多个同事对象的交互,被封装在中介者对象里面集中管理,使得这些交互行为发生变化的时候,只需要修改中介者对象就可以了,当然如果是已经做好的系统,那么就扩展中介者对象,而各个同事类不需要做修改。

  • 一对多关联转变为一对一的关联

    没有使用中介者模式的时候,同事对象之间的关系通常是一对多的,引入中介者对象以后,中介者对象和同事对象的关系通常变成双向的一对一,这会让对象的关系更容易理解和实现。

2,缺点:

当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。

使用场景

  • 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解。
  • 当想创建一个运行于多个类之间的对象,又不想生成新的子类时。

迭代器模式

概述

定义:

提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。

结构

迭代器模式主要包含以下角色:

  • 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合元素以及创建迭代器对象的接口。

  • 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。

  • 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、next() 等方法。

  • 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。

案例实现

【例】定义一个可以存储学生对象的容器对象,将遍历该容器的功能交由迭代器实现,涉及到的类如下:

代码如下:

定义迭代器接口,声明hasNext、next方法

1
2
3
4
public interface StudentIterator {
boolean hasNext();
Student next();
}

定义具体的迭代器类,重写所有的抽象方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class StudentIteratorImpl implements StudentIterator {
private List<Student> list;
private int position = 0;

public StudentIteratorImpl(List<Student> list) {
this.list = list;
}

@Override
public boolean hasNext() {
return position < list.size();
}

@Override
public Student next() {
Student currentStudent = list.get(position);
position ++;
return currentStudent;
}
}

定义抽象容器类,包含添加元素,删除元素,获取迭代器对象的方法

1
2
3
4
5
6
7
public interface StudentAggregate {
void addStudent(Student student);

void removeStudent(Student student);

StudentIterator getStudentIterator();
}

定义具体的容器类,重写所有的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StudentAggregateImpl implements StudentAggregate {

private List<Student> list = new ArrayList<Student>(); // 学生列表

@Override
public void addStudent(Student student) {
this.list.add(student);
}

@Override
public void removeStudent(Student student) {
this.list.remove(student);
}

@Override
public StudentIterator getStudentIterator() {
return new StudentIteratorImpl(list);
}
}

优缺点

1,优点:

  • 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式。
  • 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计。
  • 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足 “开闭原则” 的要求。

2,缺点:

增加了类的个数,这在一定程度上增加了系统的复杂性。

使用场景

  • 当需要为聚合对象提供多种遍历方式时。
  • 当需要为遍历不同的聚合结构提供一个统一的接口时。
  • 当访问一个聚合对象的内容而无须暴露其内部细节的表示时。

JDK源码解析

迭代器模式在JAVA的很多集合类中被广泛应用,接下来看看JAVA源码中是如何使用迭代器模式的。

1
2
3
4
5
List<String> list = new ArrayList<>();
Iterator<String> iterator = list.iterator(); //list.iterator()方法返回的肯定是Iterator接口的子实现类对象
while (iterator.hasNext()) {
System.out.println(iterator.next());
}

看完这段代码是不是很熟悉,与我们上面代码基本类似。单列集合都使用到了迭代器,我们以ArrayList举例来说明

  • List:抽象聚合类
  • ArrayList:具体的聚合类
  • Iterator:抽象迭代器
  • list.iterator():返回的是实现了 Iterator 接口的具体迭代器对象

具体的来看看 ArrayList的代码实现

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 ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

public Iterator<E> iterator() {
return new Itr();
}

private class Itr implements Iterator<E> {
int cursor; // 下一个要返回元素的索引
int lastRet = -1; // 上一个返回元素的索引
int expectedModCount = modCount;

Itr() {}

//判断是否还有元素
public boolean hasNext() {
return cursor != size;
}

//获取下一个元素
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
...
}

这部分代码还是比较简单,大致就是在 iterator 方法中返回了一个实例化的 Iterator 对象。Itr是一个内部类,它实现了 Iterator 接口并重写了其中的抽象方法。

注意:

当我们在使用JAVA开发的时候,想使用迭代器模式的话,只要让我们自己定义的容器类实现`java.util.Iterable`并实现其中的iterator()方法使其返回一个 `java.util.Iterator` 的实现类就可以了。

访问者模式

概述

定义:

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。

结构

访问者模式包含以下主要角色:

  • 抽象访问者(Visitor)角色:定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素类个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变。
  • 具体访问者(ConcreteVisitor)角色:给出对每一个元素类访问时所产生的具体行为。
  • 抽象元素(Element)角色:定义了一个接受访问者的方法(accept),其意义是指,每一个元素都要可以被访问者访问。
  • 具体元素(ConcreteElement)角色: 提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
  • 对象结构(Object Structure)角色:定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素(Element),并且可以迭代这些元素,供访问者访问。

案例实现

【例】给宠物喂食

现在养宠物的人特别多,我们就以这个为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食。

  • 访问者角色:给宠物喂食的人
  • 具体访问者角色:主人、其他人
  • 抽象元素角色:动物抽象类
  • 具体元素角色:宠物狗、宠物猫
  • 结构对象角色:主人家

类图如下:

代码如下:

创建抽象访问者接口

1
2
3
4
5
public interface Person {
void feed(Cat cat);

void feed(Dog dog);
}

创建不同的具体访问者角色(主人和其他人),都需要实现 Person接口

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 class Owner implements Person {

@Override
public void feed(Cat cat) {
System.out.println("主人喂食猫");
}

@Override
public void feed(Dog dog) {
System.out.println("主人喂食狗");
}
}

public class Someone implements Person {
@Override
public void feed(Cat cat) {
System.out.println("其他人喂食猫");
}

@Override
public void feed(Dog dog) {
System.out.println("其他人喂食狗");
}
}

定义抽象节点 – 宠物

1
2
3
public interface Animal {
void accept(Person person);
}

定义实现Animal接口的 具体节点(元素)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Dog implements Animal {

@Override
public void accept(Person person) {
person.feed(this);
System.out.println("好好吃,汪汪汪!!!");
}
}

public class Cat implements Animal {

@Override
public void accept(Person person) {
person.feed(this);
System.out.println("好好吃,喵喵喵!!!");
}
}

定义对象结构,此案例中就是主人的家

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Home {
private List<Animal> nodeList = new ArrayList<Animal>();

public void action(Person person) {
for (Animal node : nodeList) {
node.accept(person);
}
}

//添加操作
public void add(Animal animal) {
nodeList.add(animal);
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
Home home = new Home();
home.add(new Dog());
home.add(new Cat());

Owner owner = new Owner();
home.action(owner);

Someone someone = new Someone();
home.action(someone);
}
}

优缺点

1,优点:

  • 扩展性好

    在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。

  • 复用性好

    通过访问者来定义整个对象结构通用的功能,从而提高复用程度。

  • 分离无关行为

    通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。

2,缺点:

  • 对象结构变化很困难

    在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。

  • 违反了依赖倒置原则

    访问者模式依赖了具体类,而没有依赖抽象类。

使用场景

  • 对象结构相对稳定,但其操作算法经常变化的程序。

  • 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。

扩展

访问者模式用到了一种双分派的技术。

1,分派:

变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如 Map map = new HashMap() ,map变量的静态类型是 Map ,实际类型是 HashMap 。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派。

静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。

动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写支持动态分派。

2,动态分派:

通过方法的重写支持动态分派。

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
public class Animal {
public void execute() {
System.out.println("Animal");
}
}

public class Dog extends Animal {
@Override
public void execute() {
System.out.println("dog");
}
}

public class Cat extends Animal {
@Override
public void execute() {
System.out.println("cat");
}
}

public class Client {
public static void main(String[] args) {
Animal a = new Dog();
a.execute();

Animal a1 = new Cat();
a1.execute();
}
}

上面代码的结果大家应该直接可以说出来,这不就是多态吗!运行执行的是子类中的方法。

Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。

3,静态分派:

通过方法重载支持静态分派。

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 Animal {
}

public class Dog extends Animal {
}

public class Cat extends Animal {
}

public class Execute {
public void execute(Animal a) {
System.out.println("Animal");
}

public void execute(Dog d) {
System.out.println("dog");
}

public void execute(Cat c) {
System.out.println("cat");
}
}

public class Client {
public static void main(String[] args) {
Animal a = new Animal();
Animal a1 = new Dog();
Animal a2 = new Cat();

Execute exe = new Execute();
exe.execute(a);
exe.execute(a1);
exe.execute(a2);
}
}

运行结果:

这个结果可能出乎一些人的意料了,为什么呢?

重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。

4,双分派:

所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时区别,还要根据参数的运行时区别。

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
public class Animal {
public void accept(Execute exe) {
exe.execute(this);
}
}

public class Dog extends Animal {
public void accept(Execute exe) {
exe.execute(this);
}
}

public class Cat extends Animal {
public void accept(Execute exe) {
exe.execute(this);
}
}

public class Execute {
public void execute(Animal a) {
System.out.println("animal");
}

public void execute(Dog d) {
System.out.println("dog");
}

public void execute(Cat c) {
System.out.println("cat");
}
}

public class Client {
public static void main(String[] args) {
Animal a = new Animal();
Animal d = new Dog();
Animal c = new Cat();

Execute exe = new Execute();
a.accept(exe);
d.accept(exe);
c.accept(exe);
}
}

在上面代码中,客户端将Execute对象做为参数传递给Animal类型的变量调用的方法,这里完成第一次分派,这里是方法重写,所以是动态分派,也就是执行实际类型中的方法,同时也将自己this作为参数传递进去,这里就完成了第二次分派,这里的Execute类中有多个重载的方法,而传递进行的是this,就是具体的实际类型的对象。

说到这里,我们已经明白双分派是怎么回事了,但是它有什么效果呢?就是可以实现方法的动态绑定,我们可以对上面的程序进行修改。

运行结果如下:

双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了。

备忘录模式

概述

备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原,很多软件都提供了撤销(Undo)操作,如 Word、记事本、Photoshop、IDEA等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在 浏览器 中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。

定义:

又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。

结构

备忘录模式的主要角色如下:

  • 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
  • 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
  • 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。

备忘录有两个等效的接口:

  • 窄接口:管理者(Caretaker)对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口(narror Interface),这个窄接口只允许他把备忘录对象传给其他的对象。
  • 宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide Interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。

案例实现

【例】游戏挑战BOSS

游戏中的某个场景,一游戏角色有生命力、攻击力、防御力等数据,在打Boss前和后一定会不一样的,我们允许玩家如果感觉与Boss决斗的效果不理想可以让游戏恢复到决斗之前的状态。

要实现上述案例,有两种方式:

  • “白箱”备忘录模式
  • “黑箱”备忘录模式

“白箱”备忘录模式

备忘录角色对任何对象都提供一个接口,即宽接口,备忘录角色的内部所存储的状态就对所有对象公开。类图如下:

代码如下:

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//游戏角色类
public class GameRole {
private int vit; //生命力
private int atk; //攻击力
private int def; //防御力

//初始化状态
public void initState() {
this.vit = 100;
this.atk = 100;
this.def = 100;
}

//战斗
public void fight() {
this.vit = 0;
this.atk = 0;
this.def = 0;
}

//保存角色状态
public RoleStateMemento saveState() {
return new RoleStateMemento(vit, atk, def);
}

//回复角色状态
public void recoverState(RoleStateMemento roleStateMemento) {
this.vit = roleStateMemento.getVit();
this.atk = roleStateMemento.getAtk();
this.def = roleStateMemento.getDef();
}

public void stateDisplay() {
System.out.println("角色生命力:" + vit);
System.out.println("角色攻击力:" + atk);
System.out.println("角色防御力:" + def);
}

public int getVit() {
return vit;
}

public void setVit(int vit) {
this.vit = vit;
}

public int getAtk() {
return atk;
}

public void setAtk(int atk) {
this.atk = atk;
}

public int getDef() {
return def;
}

public void setDef(int def) {
this.def = def;
}
}

//游戏状态存储类(备忘录类)
public class RoleStateMemento {
private int vit;
private int atk;
private int def;

public RoleStateMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = def;
}

public int getVit() {
return vit;
}

public void setVit(int vit) {
this.vit = vit;
}

public int getAtk() {
return atk;
}

public void setAtk(int atk) {
this.atk = atk;
}

public int getDef() {
return def;
}

public void setDef(int def) {
this.def = def;
}
}

//角色状态管理者类
public class RoleStateCaretaker {
private RoleStateMemento roleStateMemento;

public RoleStateMemento getRoleStateMemento() {
return roleStateMemento;
}

public void setRoleStateMemento(RoleStateMemento roleStateMemento) {
this.roleStateMemento = roleStateMemento;
}
}

//测试类
public class Client {
public static void main(String[] args) {
System.out.println("------------大战Boss前------------");
//大战Boss前
GameRole gameRole = new GameRole();
gameRole.initState();
gameRole.stateDisplay();

//保存进度
RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
roleStateCaretaker.setRoleStateMemento(gameRole.saveState());

System.out.println("------------大战Boss后------------");
//大战Boss时,损耗严重
gameRole.fight();
gameRole.stateDisplay();
System.out.println("------------恢复之前状态------------");
//恢复之前状态
gameRole.recoverState(roleStateCaretaker.getRoleStateMemento());
gameRole.stateDisplay();

}
}

分析:白箱备忘录模式是破坏封装性的。但是通过程序员自律,同样可以在一定程度上实现模式的大部分用意。

“黑箱”备忘录模式

备忘录角色对发起人对象提供一个宽接口,而为其他对象提供一个窄接口。在Java语言中,实现双重接口的办法就是将备忘录类设计成发起人类的内部成员类。

RoleStateMemento 设为 GameRole 的内部类,从而将 RoleStateMemento 对象封装在 GameRole 里面;在外面提供一个标识接口 MementoRoleStateCaretaker 及其他对象使用。这样 GameRole 类看到的是 RoleStateMemento 所有的接口,而RoleStateCaretaker 及其他对象看到的仅仅是标识接口 Memento 所暴露出来的接口,从而维护了封装型。类图如下:

代码如下:

窄接口Memento,这是一个标识接口,因此没有定义出任何的方法

1
2
public interface Memento {
}

定义发起人类 GameRole,并在内部定义备忘录内部类 RoleStateMemento(该内部类设置为私有的)

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/游戏角色类
public class GameRole {
private int vit; //生命力
private int atk; //攻击力
private int def; //防御力

//初始化状态
public void initState() {
this.vit = 100;
this.atk = 100;
this.def = 100;
}

//战斗
public void fight() {
this.vit = 0;
this.atk = 0;
this.def = 0;
}

//保存角色状态
public Memento saveState() {
return new RoleStateMemento(vit, atk, def);
}

//回复角色状态
public void recoverState(Memento memento) {
RoleStateMemento roleStateMemento = (RoleStateMemento) memento;
this.vit = roleStateMemento.getVit();
this.atk = roleStateMemento.getAtk();
this.def = roleStateMemento.getDef();
}

public void stateDisplay() {
System.out.println("角色生命力:" + vit);
System.out.println("角色攻击力:" + atk);
System.out.println("角色防御力:" + def);

}

public int getVit() {
return vit;
}

public void setVit(int vit) {
this.vit = vit;
}

public int getAtk() {
return atk;
}

public void setAtk(int atk) {
this.atk = atk;
}

public int getDef() {
return def;
}

public void setDef(int def) {
this.def = def;
}

private class RoleStateMemento implements Memento {
private int vit;
private int atk;
private int def;

public RoleStateMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = def;
}

public int getVit() {
return vit;
}

public void setVit(int vit) {
this.vit = vit;
}

public int getAtk() {
return atk;
}

public void setAtk(int atk) {
this.atk = atk;
}

public int getDef() {
return def;
}

public void setDef(int def) {
this.def = def;
}
}
}

负责人角色类 RoleStateCaretaker 能够得到的备忘录对象是以 Memento 为接口的,由于这个接口仅仅是一个标识接口,因此负责人角色不可能改变这个备忘录对象的内容

1
2
3
4
5
6
7
8
9
10
11
12
//角色状态管理者类
public class RoleStateCaretaker {
private Memento memento;

public Memento getMemento() {
return memento;
}

public void setMemento(Memento memento) {
this.memento = memento;
}
}

客户端测试类

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 Client {
public static void main(String[] args) {
System.out.println("------------大战Boss前------------");
//大战Boss前
GameRole gameRole = new GameRole();
gameRole.initState();
gameRole.stateDisplay();

//保存进度
RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
roleStateCaretaker.setMemento(gameRole.saveState());

System.out.println("------------大战Boss后------------");
//大战Boss时,损耗严重
gameRole.fight();
gameRole.stateDisplay();
System.out.println("------------恢复之前状态------------");
//恢复之前状态
gameRole.recoverState(roleStateCaretaker.getMemento());
gameRole.stateDisplay();
}
}

优缺点

1,优点:

  • 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
  • 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
  • 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。

2,缺点:

  • 资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。

使用场景

  • 需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能。

  • 需要提供一个可回滚操作的场景,如 Word、记事本、Photoshop,idea等软件在编辑时按 Ctrl+Z 组合键,还有数据库中事务操作。

解释器模式

概述

如上图,设计一个软件用来进行加减计算。我们第一想法就是使用工具类,提供对应的加法和减法的工具方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//用于两个整数相加
public static int add(int a,int b){
return a + b;
}

//用于两个整数相加
public static int add(int a,int b,int c){
return a + b + c;
}

//用于n个整数相加
public static int add(Integer ... arr) {
int sum = 0;
for (Integer i : arr) {
sum += i;
}
return sum;
}

上面的形式比较单一、有限,如果形式变化非常多,这就不符合要求,因为加法和减法运算,两个运算符与数值可以有无限种组合方式。比如 1+2+3+4+5、1+2+3-4等等。

显然,现在需要一种翻译识别机器,能够解析由数字以及 + - 符号构成的合法的运算序列。如果把运算符和数字都看作节点的话,能够逐个节点的进行读取解析运算,这就是解释器模式的思维。

定义:

给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。

在解释器模式中,我们需要将待解决的问题,提取出规则,抽象为一种“语言”。比如加减法运算,规则为:由数值和+-符号组成的合法序列,“1+3-2” 就是这种语言的句子。

解释器就是要解析出来语句的含义。但是如何描述规则呢?

文法(语法)规则:

文法是用于描述语言的语法结构的形式规则。

1
2
3
4
expression ::= value | plus | minus
plus ::= expression ‘+’ expression
minus ::= expression ‘-’ expression
value ::= integer

注意: 这里的符号“::=”表示“定义为”的意思,竖线 | 表示或,左右的其中一个,引号内为字符本身,引号外为语法。

上面规则描述为 :

表达式可以是一个值,也可以是plus或者minus运算,而plus和minus又是由表达式结合运算符构成,值的类型为整型数。

抽象语法树:

在计算机科学中,抽象语法树(AbstractSyntaxTree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

用树形来表示符合文法规则的句子。

结构

解释器模式包含以下主要角色。

  • 抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()。

  • 终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。

  • 非终结符表达式(Nonterminal Expression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。

  • 环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。

  • 客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。

案例实现

【例】设计实现加减法的软件

代码如下:

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
//抽象角色AbstractExpression
public abstract class AbstractExpression {
public abstract int interpret(Context context);
}

//终结符表达式角色
public class Value extends AbstractExpression {
private int value;

public Value(int value) {
this.value = value;
}

@Override
public int interpret(Context context) {
return value;
}

@Override
public String toString() {
return new Integer(value).toString();
}
}

//非终结符表达式角色 加法表达式
public class Plus extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;

public Plus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}

@Override
public int interpret(Context context) {
return left.interpret(context) + right.interpret(context);
}

@Override
public String toString() {
return "(" + left.toString() + " + " + right.toString() + ")";
}
}

///非终结符表达式角色 减法表达式
public class Minus extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;

public Minus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}

@Override
public int interpret(Context context) {
return left.interpret(context) - right.interpret(context);
}

@Override
public String toString() {
return "(" + left.toString() + " - " + right.toString() + ")";
}
}

//终结符表达式角色 变量表达式
public class Variable extends AbstractExpression {
private String name;

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

@Override
public int interpret(Context ctx) {
return ctx.getValue(this);
}

@Override
public String toString() {
return name;
}
}

//环境类
public class Context {
private Map<Variable, Integer> map = new HashMap<Variable, Integer>();

public void assign(Variable var, Integer value) {
map.put(var, value);
}

public int getValue(Variable var) {
Integer value = map.get(var);
return value;
}
}

//测试类
public class Client {
public static void main(String[] args) {
Context context = new Context();

Variable a = new Variable("a");
Variable b = new Variable("b");
Variable c = new Variable("c");
Variable d = new Variable("d");
Variable e = new Variable("e");
//Value v = new Value(1);

context.assign(a, 1);
context.assign(b, 2);
context.assign(c, 3);
context.assign(d, 4);
context.assign(e, 5);

AbstractExpression expression = new Minus(new Plus(new Plus(new Plus(a, b), c), d), e);

System.out.println(expression + "= " + expression.interpret(context));
}
}

优缺点

1,优点:

  • 易于改变和扩展文法。

    由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。

  • 实现文法较为容易。

    在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂。

  • 增加新的解释表达式较为方便。

    如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合 “开闭原则”。

2,缺点:

  • 对于复杂文法难以维护。

    在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护。

  • 执行效率较低。

    由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。

使用场景

  • 当语言的文法较为简单,且执行效率不是关键问题时。

  • 当问题重复出现,且可以用一种简单的语言来进行表达时。

  • 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候。

自定义Spring框架

spring使用回顾

自定义spring框架前,先回顾一下spring框架的使用,从而分析spring的核心,并对核心功能进行模拟。

  • 数据访问层。定义UserDao接口及其子实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public interface UserDao {
    public void add();
    }

    public class UserDaoImpl implements UserDao {

    public void add() {
    System.out.println("userDaoImpl ....");
    }
    }
  • 业务逻辑层。定义UserService接口及其子实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public interface UserService {
    public void add();
    }

    public class UserServiceImpl implements UserService {

    private UserDao userDao;

    public void setUserDao(UserDao userDao) {
    this.userDao = userDao;
    }

    public void add() {
    System.out.println("userServiceImpl ...");
    userDao.add();
    }
    }
  • 定义UserController类,使用main方法模拟controller层

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class UserController {
    public static void main(String[] args) {
    //创建spring容器对象
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
    //从IOC容器中获取UserService对象
    UserService userService = applicationContext.getBean("userService", UserService.class);
    //调用UserService对象的add方法
    userService.add();
    }
    }
  • 编写配置文件。在类路径下编写一个名为ApplicationContext.xml的配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="userService" class="com.itheima.service.impl.UserServiceImpl">
    <property name="userDao" ref="userDao"></property>
    </bean>

    <bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"></bean>

    </beans>

    代码运行结果如下:

通过上面代码及结果可以看出:

  • userService对象是从applicationContext容器对象获取到的,也就是userService对象交由spring进行管理。
  • 上面结果可以看到调用了UserDao对象中的add方法,也就是说UserDao子实现类对象也交由spring管理了。
  • UserService中的userDao变量我们并没有进行赋值,但是可以正常使用,说明spring已经将UserDao对象赋值给了userDao变量。

上面三点体现了Spring框架的IOC(Inversion of Control)和DI(Dependency Injection, DI)

spring核心功能结构

Spring大约有20个模块,由1300多个不同的文件构成。这些模块可以分为:

核心容器、AOP和设备支持、数据访问与集成、Web组件、通信报文和集成测试等,下面是 Spring 框架的总体架构图:

核心容器由 beans、core、context 和 expression(Spring Expression Language,SpEL)4个模块组成。

  • spring-beans和spring-core模块是Spring框架的核心模块,包含了控制反转(Inversion of Control,IOC)和依赖注入(Dependency Injection,DI)。BeanFactory使用控制反转对应用程序的配置和依赖性规范与实际的应用程序代码进行了分离。BeanFactory属于延时加载,也就是说在实例化容器对象后并不会自动实例化Bean,只有当Bean被使用时,BeanFactory才会对该 Bean 进行实例化与依赖关系的装配。
  • spring-context模块构架于核心模块之上,扩展了BeanFactory,为它添加了Bean生命周期控制、框架事件体系及资源加载透明化等功能。此外,该模块还提供了许多企业级支持,如邮件访问、远程访问、任务调度等,ApplicationContext 是该模块的核心接口,它的超类是 BeanFactory。与BeanFactory不同,ApplicationContext实例化后会自动对所有的单实例Bean进行实例化与依赖关系的装配,使之处于待用状态。
  • spring-context-support模块是对Spring IoC容器及IoC子容器的扩展支持。
  • spring-context-indexer模块是Spring的类管理组件和Classpath扫描组件。
  • spring-expression 模块是统一表达式语言(EL)的扩展模块,可以查询、管理运行中的对象,同时也可以方便地调用对象方法,以及操作数组、集合等。它的语法类似于传统EL,但提供了额外的功能,最出色的要数函数调用和简单字符串的模板函数。EL的特性是基于Spring产品的需求而设计的,可以非常方便地同Spring IoC进行交互。

bean概述

Spring 就是面向 Bean 的编程(BOP,Bean Oriented Programming),Bean 在 Spring 中处于核心地位。Bean对于Spring的意义就像Object对于OOP的意义一样,Spring中没有Bean也就没有Spring存在的意义。Spring IoC容器通过配置文件或者注解的方式来管理bean对象之间的依赖关系。

spring中bean用于对一个类进行封装。如下面的配置:

1
2
3
4
<bean id="userService" class="com.itheima.service.impl.UserServiceImpl">
<property name="userDao" ref="userDao"></property>
</bean>
<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"></bean>

为什么Bean如此重要呢?

  • spring 将bean对象交由一个叫IOC容器进行管理。
  • bean对象之间的依赖关系在配置文件中体现,并由spring完成。

Spring IOC相关接口分析

BeanFactory解析

Spring中Bean的创建是典型的工厂模式,这一系列的Bean工厂,即IoC容器,为开发者管理对象之间的依赖关系提供了很多便利和基础服务,在Spring中有许多IoC容器的实现供用户选择,其相互关系如下图所示。

其中,BeanFactory作为最顶层的一个接口,定义了IoC容器的基本功能规范,BeanFactory有三个重要的子接口:ListableBeanFactory、HierarchicalBeanFactory和AutowireCapableBeanFactory。但是从类图中我们可以发现最终的默认实现类是DefaultListableBeanFactory,它实现了所有的接口。

那么为何要定义这么多层次的接口呢?

每个接口都有它的使用场合,主要是为了区分在Spring内部操作过程中对象的传递和转化,对对象的数据访问所做的限制。例如,

  • ListableBeanFactory接口表示这些Bean可列表化。
  • HierarchicalBeanFactory表示这些Bean 是有继承关系的,也就是每个 Bean 可能有父 Bean
  • AutowireCapableBeanFactory 接口定义Bean的自动装配规则。

这三个接口共同定义了Bean的集合、Bean之间的关系及Bean行为。最基本的IoC容器接口是BeanFactory,来看一下它的源码:

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 interface BeanFactory {

String FACTORY_BEAN_PREFIX = "&";

//根据bean的名称获取IOC容器中的的bean对象
Object getBean(String name) throws BeansException;
//根据bean的名称获取IOC容器中的的bean对象,并指定获取到的bean对象的类型,这样我们使用时就不需要进行类型强转了
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
Object getBean(String name, Object... args) throws BeansException;
<T> T getBean(Class<T> requiredType) throws BeansException;
<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;

<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);
<T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);

//判断容器中是否包含指定名称的bean对象
boolean containsBean(String name);
//根据bean的名称判断是否是单例
boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;
@Nullable
Class<?> getType(String name) throws NoSuchBeanDefinitionException;
String[] getAliases(String name);
}

在BeanFactory里只对IoC容器的基本行为做了定义,根本不关心你的Bean是如何定义及怎样加载的。正如我们只关心能从工厂里得到什么产品,不关心工厂是怎么生产这些产品的。

BeanFactory有一个很重要的子接口,就是ApplicationContext接口,该接口主要来规范容器中的bean对象是非延时加载,即在创建容器对象的时候就对象bean进行初始化,并存储到一个容器中。

要知道工厂是如何产生对象的,我们需要看具体的IoC容器实现,Spring提供了许多IoC容器实现,比如:

  • ClasspathXmlApplicationContext : 根据类路径加载xml配置文件,并创建IOC容器对象。
  • FileSystemXmlApplicationContext :根据系统路径加载xml配置文件,并创建IOC容器对象。
  • AnnotationConfigApplicationContext :加载注解类配置,并创建IOC容器。

BeanDefinition解析

Spring IoC容器管理我们定义的各种Bean对象及其相互关系,而Bean对象在Spring实现中是以BeanDefinition来描述的,如下面配置文件

1
2
3
4
<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"></bean>

bean标签还有很多属性:
scope、init-method、destory-method等。

其继承体系如下图所示。

BeanDefinitionReader解析

Bean的解析过程非常复杂,功能被分得很细,因为这里需要被扩展的地方很多,必须保证足够的灵活性,以应对可能的变化。Bean的解析主要就是对Spring配置文件的解析。这个解析过程主要通过BeanDefinitionReader来完成,看看Spring中BeanDefinitionReader的类结构图,如下图所示。

看看BeanDefinitionReader接口定义的功能来理解它具体的作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface BeanDefinitionReader {

//获取BeanDefinitionRegistry注册器对象
BeanDefinitionRegistry getRegistry();

@Nullable
ResourceLoader getResourceLoader();

@Nullable
ClassLoader getBeanClassLoader();

BeanNameGenerator getBeanNameGenerator();

/*
下面的loadBeanDefinitions都是加载bean定义,从指定的资源中
*/
int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException;
int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException;
int loadBeanDefinitions(String location) throws BeanDefinitionStoreException;
int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException;
}

BeanDefinitionRegistry解析

BeanDefinitionReader用来解析bean定义,并封装BeanDefinition对象,而我们定义的配置文件中定义了很多bean标签,所以就有一个问题,解析的BeanDefinition对象存储到哪儿?答案就是BeanDefinition的注册中心,而该注册中心顶层接口就是BeanDefinitionRegistry。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface BeanDefinitionRegistry extends AliasRegistry {

//往注册表中注册bean
void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException;

//从注册表中删除指定名称的bean
void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;

//获取注册表中指定名称的bean
BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;

//判断注册表中是否已经注册了指定名称的bean
boolean containsBeanDefinition(String beanName);

//获取注册表中所有的bean的名称
String[] getBeanDefinitionNames();

int getBeanDefinitionCount();
boolean isBeanNameInUse(String beanName);
}

继承结构图如下:

从上面类图可以看到BeanDefinitionRegistry接口的子实现类主要有以下几个:

  • DefaultListableBeanFactory

    在该类中定义了如下代码,就是用来注册bean

    1
    private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
  • SimpleBeanDefinitionRegistry

    在该类中定义了如下代码,就是用来注册bean

    1
    private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(64);

创建容器

ClassPathXmlApplicationContext对Bean配置资源的载入是从refresh()方法开始的。refresh()方法是一个模板方法,规定了 IoC 容器的启动流程,有些逻辑要交给其子类实现。它对 Bean 配置资源进行载入,ClassPathXmlApplicationContext通过调用其父类AbstractApplicationContext的refresh()方法启动整个IoC容器对Bean定义的载入过程。

自定义SpringIOC

现要对下面的配置文件进行解析,并自定义Spring框架的IOC对涉及到的对象进行管理。

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="userService" class="com.itheima.service.impl.UserServiceImpl">
<property name="userDao" ref="userDao"></property>
</bean>
<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"></bean>
</beans>

定义bean相关的pojo类

PropertyValue类

用于封装bean的属性,体现到上面的配置文件就是封装bean标签的子标签property标签数据。

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
public class PropertyValue {

private String name;
private String ref;
private String value;

public PropertyValue() {
}

public PropertyValue(String name, String ref,String value) {
this.name = name;
this.ref = ref;
this.value = value;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getRef() {
return ref;
}

public void setRef(String ref) {
this.ref = ref;
}

public String getValue() {
return value;
}

public void setValue(String value) {
this.value = value;
}
}

MutablePropertyValues类

一个bean标签可以有多个property子标签,所以再定义一个MutablePropertyValues类,用来存储并管理多个PropertyValue对象。

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
public class MutablePropertyValues implements Iterable<PropertyValue> {

private final List<PropertyValue> propertyValueList;

public MutablePropertyValues() {
this.propertyValueList = new ArrayList<PropertyValue>();
}

public MutablePropertyValues(List<PropertyValue> propertyValueList) {
this.propertyValueList = (propertyValueList != null ? propertyValueList : new ArrayList<PropertyValue>());
}

public PropertyValue[] getPropertyValues() {
return this.propertyValueList.toArray(new PropertyValue[0]);
}

public PropertyValue getPropertyValue(String propertyName) {
for (PropertyValue pv : this.propertyValueList) {
if (pv.getName().equals(propertyName)) {
return pv;
}
}
return null;
}

@Override
public Iterator<PropertyValue> iterator() {
return propertyValueList.iterator();
}

public boolean isEmpty() {
return this.propertyValueList.isEmpty();
}

public MutablePropertyValues addPropertyValue(PropertyValue pv) {
for (int i = 0; i < this.propertyValueList.size(); i++) {
PropertyValue currentPv = this.propertyValueList.get(i);
if (currentPv.getName().equals(pv.getName())) {
this.propertyValueList.set(i, new PropertyValue(pv.getName(),pv.getRef(), pv.getValue()));
return this;
}
}
this.propertyValueList.add(pv);
return this;
}

public boolean contains(String propertyName) {
return getPropertyValue(propertyName) != null;
}
}

BeanDefinition类

BeanDefinition类用来封装bean信息的,主要包含id(即bean对象的名称)、class(需要交由spring管理的类的全类名)及子标签property数据。

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 BeanDefinition {
private String id;
private String className;

private MutablePropertyValues propertyValues;

public BeanDefinition() {
propertyValues = new MutablePropertyValues();
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getClassName() {
return className;
}

public void setClassName(String className) {
this.className = className;
}

public void setPropertyValues(MutablePropertyValues propertyValues) {
this.propertyValues = propertyValues;
}

public MutablePropertyValues getPropertyValues() {
return propertyValues;
}
}

定义注册表相关类

BeanDefinitionRegistry接口

BeanDefinitionRegistry接口定义了注册表的相关操作,定义如下功能:

  • 注册BeanDefinition对象到注册表中
  • 从注册表中删除指定名称的BeanDefinition对象
  • 根据名称从注册表中获取BeanDefinition对象
  • 判断注册表中是否包含指定名称的BeanDefinition对象
  • 获取注册表中BeanDefinition对象的个数
  • 获取注册表中所有的BeanDefinition的名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface BeanDefinitionRegistry {

//注册BeanDefinition对象到注册表中
void registerBeanDefinition(String beanName, BeanDefinition beanDefinition);

//从注册表中删除指定名称的BeanDefinition对象
void removeBeanDefinition(String beanName) throws Exception;

//根据名称从注册表中获取BeanDefinition对象
BeanDefinition getBeanDefinition(String beanName) throws Exception;

boolean containsBeanDefinition(String beanName);

int getBeanDefinitionCount();

String[] getBeanDefinitionNames();
}

SimpleBeanDefinitionRegistry类

该类实现了BeanDefinitionRegistry接口,定义了Map集合作为注册表容器。

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 SimpleBeanDefinitionRegistry implements BeanDefinitionRegistry {

private Map<String, BeanDefinition> beanDefinitionMap = new HashMap<String, BeanDefinition>();

@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) {
beanDefinitionMap.put(beanName,beanDefinition);
}

@Override
public void removeBeanDefinition(String beanName) throws Exception {
beanDefinitionMap.remove(beanName);
}

@Override
public BeanDefinition getBeanDefinition(String beanName) throws Exception {
return beanDefinitionMap.get(beanName);
}

@Override
public boolean containsBeanDefinition(String beanName) {
return beanDefinitionMap.containsKey(beanName);
}

@Override
public int getBeanDefinitionCount() {
return beanDefinitionMap.size();
}

@Override
public String[] getBeanDefinitionNames() {
return beanDefinitionMap.keySet().toArray(new String[1]);
}
}

定义解析器相关类

BeanDefinitionReader接口

BeanDefinitionReader是用来解析配置文件并在注册表中注册bean的信息。定义了两个规范:

  • 获取注册表的功能,让外界可以通过该对象获取注册表对象。
  • 加载配置文件,并注册bean数据。
1
2
3
4
5
6
7
public interface BeanDefinitionReader {

//获取注册表对象
BeanDefinitionRegistry getRegistry();
//加载配置文件并在注册表中进行注册
void loadBeanDefinitions(String configLocation) throws Exception;
}

XmlBeanDefinitionReader类

XmlBeanDefinitionReader类是专门用来解析xml配置文件的。该类实现BeanDefinitionReader接口并实现接口中的两个功能。

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
public class XmlBeanDefinitionReader implements BeanDefinitionReader {

private BeanDefinitionRegistry registry;

public XmlBeanDefinitionReader() {
this.registry = new SimpleBeanDefinitionRegistry();
}

@Override
public BeanDefinitionRegistry getRegistry() {
return registry;
}

@Override
public void loadBeanDefinitions(String configLocation) throws Exception {

InputStream is = this.getClass().getClassLoader().getResourceAsStream(configLocation);
SAXReader reader = new SAXReader();
Document document = reader.read(is);
Element rootElement = document.getRootElement();
//解析bean标签
parseBean(rootElement);
}

private void parseBean(Element rootElement) {

List<Element> elements = rootElement.elements();
for (Element element : elements) {
String id = element.attributeValue("id");
String className = element.attributeValue("class");
BeanDefinition beanDefinition = new BeanDefinition();
beanDefinition.setId(id);
beanDefinition.setClassName(className);
List<Element> list = element.elements("property");
MutablePropertyValues mutablePropertyValues = new MutablePropertyValues();
for (Element element1 : list) {
String name = element1.attributeValue("name");
String ref = element1.attributeValue("ref");
String value = element1.attributeValue("value");
PropertyValue propertyValue = new PropertyValue(name,ref,value);
mutablePropertyValues.addPropertyValue(propertyValue);
}
beanDefinition.setPropertyValues(mutablePropertyValues);

registry.registerBeanDefinition(id,beanDefinition);
}
}
}

IOC容器相关类

BeanFactory接口

在该接口中定义IOC容器的统一规范即获取bean对象。

1
2
3
4
5
6
public interface BeanFactory {
//根据bean对象的名称获取bean对象
Object getBean(String name) throws Exception;
//根据bean对象的名称获取bean对象,并进行类型转换
<T> T getBean(String name, Class<? extends T> clazz) throws Exception;
}

ApplicationContext接口

该接口的所以的子实现类对bean对象的创建都是非延时的,所以在该接口中定义 refresh() 方法,该方法主要完成以下两个功能:

  • 加载配置文件。
  • 根据注册表中的BeanDefinition对象封装的数据进行bean对象的创建。
1
2
3
4
public interface ApplicationContext extends BeanFactory {
//进行配置文件加载并进行对象创建
void refresh() throws IllegalStateException, Exception;
}

AbstractApplicationContext类

  • 作为ApplicationContext接口的子类,所以该类也是非延时加载,所以需要在该类中定义一个Map集合,作为bean对象存储的容器。

  • 声明BeanDefinitionReader类型的变量,用来进行xml配置文件的解析,符合单一职责原则。

    BeanDefinitionReader类型的对象创建交由子类实现,因为只有子类明确到底创建BeanDefinitionReader哪儿个子实现类对象。

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
public abstract class AbstractApplicationContext implements ApplicationContext {

protected BeanDefinitionReader beanDefinitionReader;
//用来存储bean对象的容器 key存储的是bean的id值,value存储的是bean对象
protected Map<String, Object> singletonObjects = new HashMap<String, Object>();

//存储配置文件的路径
protected String configLocation;

public void refresh() throws IllegalStateException, Exception {

//加载BeanDefinition
beanDefinitionReader.loadBeanDefinitions(configLocation);

//初始化bean
finishBeanInitialization();
}

//bean的初始化
private void finishBeanInitialization() throws Exception {
BeanDefinitionRegistry registry = beanDefinitionReader.getRegistry();
String[] beanNames = registry.getBeanDefinitionNames();

for (String beanName : beanNames) {
BeanDefinition beanDefinition = registry.getBeanDefinition(beanName);
getBean(beanName);
}
}
}

注意:该类finishBeanInitialization()方法中调用getBean()方法使用到了模板方法模式。

ClassPathXmlApplicationContext类

该类主要是加载类路径下的配置文件,并进行bean对象的创建,主要完成以下功能:

  • 在构造方法中,创建BeanDefinitionReader对象。
  • 在构造方法中,调用refresh()方法,用于进行配置文件加载、创建bean对象并存储到容器中。
  • 重写父接口中的getBean()方法,并实现依赖注入操作。
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
public class ClassPathXmlApplicationContext extends AbstractApplicationContext{

public ClassPathXmlApplicationContext(String configLocation) {
this.configLocation = configLocation;
//构建XmlBeanDefinitionReader对象
beanDefinitionReader = new XmlBeanDefinitionReader();
try {
this.refresh();
} catch (Exception e) {
}
}

//根据bean的id属性值获取bean对象
@Override
public Object getBean(String name) throws Exception {

//return singletonObjects.get(name);
Object obj = singletonObjects.get(name);
if(obj != null) {
return obj;
}

BeanDefinitionRegistry registry = beanDefinitionReader.getRegistry();
BeanDefinition beanDefinition = registry.getBeanDefinition(name);
if(beanDefinition == null) {
return null;
}
String className = beanDefinition.getClassName();
Class<?> clazz = Class.forName(className);
Object beanObj = clazz.newInstance();
MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();
for (PropertyValue propertyValue : propertyValues) {
String propertyName = propertyValue.getName();
String value = propertyValue.getValue();
String ref = propertyValue.getRef();
if(ref != null && !"".equals(ref)) {

Object bean = getBean(ref);
String methodName = StringUtils.getSetterMethodNameByFieldName(propertyName);
Method[] methods = clazz.getMethods();
for (Method method : methods) {
if(method.getName().equals(methodName)) {
method.invoke(beanObj,bean);
}
}
}

if(value != null && !"".equals(value)) {
String methodName = StringUtils.getSetterMethodNameByFieldName(propertyName);
Method method = clazz.getMethod(methodName, String.class);
method.invoke(beanObj,value);
}
}
singletonObjects.put(name,beanObj);
return beanObj;
}

@Override
public <T> T getBean(String name, Class<? extends T> clazz) throws Exception {

Object bean = getBean(name);
if(bean != null) {
return clazz.cast(bean);
}
return null;
}
}

自定义Spring IOC总结

使用到的设计模式

  • 工厂模式。这个使用工厂模式 + 配置文件的方式。
  • 单例模式。Spring IOC管理的bean对象都是单例的,此处的单例不是通过构造器进行单例的控制的,而是spring框架对每一个bean只创建了一个对象。
  • 模板方法模式。AbstractApplicationContext类中的finishBeanInitialization()方法调用了子类的getBean()方法,因为getBean()的实现和环境息息相关。
  • 迭代器模式。对于MutablePropertyValues类定义使用到了迭代器模式,因为此类存储并管理PropertyValue对象,也属于一个容器,所以给该容器提供一个遍历方式。

spring框架其实使用到了很多设计模式,如AOP使用到了代理模式,选择JDK代理或者CGLIB代理使用到了策略模式,还有适配器模式,装饰者模式,观察者模式等。

符合大部分设计原则

整个设计和Spring的设计还是有一定的出入

spring框架底层是很复杂的,进行了很深入的封装,并对外提供了很好的扩展性。而我们自定义SpringIOC有以下几个目的:

  • 了解Spring底层对对象的大体管理机制。
  • 了解设计模式在具体的开发中的使用。
  • 以后学习spring源码,通过该案例的实现,可以降低spring学习的入门成本。

环境声明(重要!!!)

  1. 本⽂档所有实验、环境、⼯具、软件均基于CentOS 7.4 64bit Linux操作系统进⾏
  2. 笔者是腾讯云服务器,VMwave CentOS7 也ok.

安装配置Linux系统环境

Cmake⼯具安装

预先工具

1
2
3
yum install gcc gcc-c++

yum install openssl-devel

获取源码包cmake-3.17.1.tar.gz上传到/root

进入

1
cd /usr/local

创建

1
mkdir cmake && cd camke

解压到指定目录

1
tar zxf /root/cmake-3.17.1.tar.gz C ./
1
2
3
4
5
6
7
8
9
10
11
 cd cmake-3.17.1

./bootstrap

make && make install

which is cmake

ln -s /usr/local/bin/cmake /usr/sbin/

cmake -version

GIT⼯具安装

⽅式⼀:通过包管理器安装

1
yum install git

yum remove git # 通过源码安装前先卸载,

⽅法⼆:通过源码编译安装

  1. 准备Git安装包

    将下载好的安装包git-2.26.2.tar.gz 直接放在了root ⽬录下
    然后将其本地解压,得到git-2.26.2 ⽬录:

    1
    tar -zxvf git-2.26.2.tar.gz
  2. 提前安装可能所需的依赖

1
yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel gcc-c++ perl-ExtUtils-MakeMaker
  1. 编译安装Git

    1
    2
    3
    4
    5
    [root@localhost ~]# cd git-2.26.2/
    [root@localhost git-2.26.2]# make configure
    [root@localhost git-2.26.2]# ./configure --prefix=/usr/local/git
    [root@localhost git-2.26.2]# make profix=/usr/local/git
    [root@localhost git-2.26.2]# make install
  2. make configure 可能失败?

    1
    根据提示  yum install install autoconfautomake libtool
  3. 将Git加⼊环境变量

1
2
 
vim /etc/profile

尾部加⼊Git 的bin 路径配置即可

1
2
export GIT_HOME=/usr/local/git
export PATH=$PATH:$GIT_HOME/bin

最后执⾏ source /etc/profile 使环境变量⽣效即可

  1. 查看安装结果

    1
    git --version

    安装后的版本

JDK(JAVA环境)安装

准备JDK安装包

我这⾥下载的是jdk-8u161-linux-x64.tar.gz 安装包,并将其直接放在了root ⽬录下

卸载已有的OPENJDK(如果有)

⾸先查找已经安装的OpenJDK 包

1
rpm -qa | grep java

OpenJDK 包

java 开头的安装包均卸载即可

1
2
yum -y remove java-1.7.0-openjdk-1.7.0.141-2.6.10.5.el7.x86_64
yum -y remove java-1.8.0-openjdk-1.8.0.131-11.b12.el7.x86_64

创建⽬录并解压

  1. 在/usr/local/ 下创建java ⽂件夹并进⼊

1
2
3
cd /usr/local/
mkdir java
cd java
  1. 将上⾯准备好的JDK 安装包解压到/usr/local/java 中即可
1
tar -zxvf /root/jdk-8u161-linux-x64.tar.gz -C ./

配置JDK环境变量

编辑/etc/profile ⽂件,在⽂件尾部加⼊如下JDK 环境配置即可

1
2
3
4
JAVA_HOME=/usr/local/java/jdk1.8.0_161
CLASSPATH=$JAVA_HOME/lib/
PATH=$PATH:$JAVA_HOME/bin
export PATH JAVA_HOME CLASSPATH

然后执⾏如下命令让环境变量⽣效

1
source /etc/profile

验证JDK安装结果

输⼊如下命令即可检查安装结果:

1
2
java -version
javac

Java 多版本管理工具

解决自由切换Linux上Java版本

jEnv官网

其实官网的说明文档很清晰。按照官网说明安装即可实现

接下来我将我的安装记录下来(Centos7.4)

  1. Linux
1
git clone https://github.com/jenv/jenv.git ~/.jenv
  1. 配置jENV环境变量(.bash)

    1
    2
    echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.bash_profile
    echo 'eval "$(jenv init -)"' >> ~/.bash_profile
  2. 设置JDK

    比如本人电脑上有jdk1.8 和jdk11

    磁盘位置: /usr/local/java/jdk-11.0.11

    执行add指令:

    1
    jenv add /usr/local/java/jdk-11.0.11
  3. 通过jenv查看jdk版本列表

    1
    jenv versions

  4. 使用jEnv切换到JDK11

    1
    jenv local jdk-11

Node环境安装

准备node安装包

我这⾥下载的是node-v12.16.3-linux-x64.tar.xz 安装包,并将其直接放在了root ⽬录下

创建⽬录并解压

  1. 在/usr/local/ 下创建node ⽂件夹并进⼊

    1
    2
    3
    cd /usr/local/
    mkdir node
    cd node
  2. 将Node 的安装包解压到/usr/local/node 中即可

    1
    tar -xJvf /root/node-v12.16.3-linux-x64.tar.xz -C ./

配置NODE系统环境变量

编辑~/.bash_profile ⽂件,在⽂件末尾追加如下信息:

1
2
# Nodejs
export PATH=/usr/local/node/node-v12.16.3-linux-x64/bin:$PATH

刷新环境变量,使之⽣效即可:

1
source ~/.bash_profile

检查安装结果

1
2
3
node -v
npm version
npx -v

Python3环境安装

CentOS 7.4 默认⾃带了⼀个Python2.7 环境:

准备Pytohn3安装包并解压

我这⾥下载的是Python-3.8.3.tgz 安装包,并将其直接放在了/root ⽬录下

1
tar zxvf Python-3.8.3.tgz

安装相关预备环境

1
yum install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gcc make

编译并安装

这⾥指定了安装⽬录为/usr/local/python3 ,有需要可以⾃定义

1
2
3
cd Python-3.8.3/
./configure prefix=/usr/local/python3
make && make install

添加软链接

我们还需要将刚刚安装⽣成的⽬录/usr/local/python3 ⾥的python3 可执⾏⽂件做⼀份软链接,链
接到/usr/bin 下,⽅便后续使⽤python3

1
2
ln -s /usr/local/python3/bin/python3 /usr/bin/python3
ln -s /usr/local/python3/bin/pip3 /usr/bin/pip3

许多时候,shell识别不出,都是没有在usr/bim找到相应的软链接(相当于在windowns配置环境变量)

验证安装

1
python3

1
python

maven项⽬构建和管理⼯具安装

准备MAVEN安装包并解压

这⾥下载的是apache-maven-3.8.5-bin.tar.gz 安装包,并将其放置于提前创建好的/opt/maven
⽬录下。

1
tar zxvf apache-maven-3.8.5-bin.tar.gz

配置MAVEN加速镜像源

编辑修改 /opt/maven/apache-maven-3.8.5/conf/settings.xml
⽂件,在 标签对⾥添加如下内容即可:

1
2
3
4
5
6
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>

跟windowns上配置是一样的

具体位置(ctrl+F 搜索,建议使用Notebook++等文本工具)

配置环境变量

编辑修改/etc/profile ⽂件,在⽂件尾部添加如下内容

1
2
export MAVEN_HOME=/opt/maven/apache-maven-3..8..5
export PATH=$MAVEN_HOME/bin:$PATH

接下来来刷新环境变量,让maven 环境的路径配置⽣效,

1
source /etc/profile

检验安装结果

1
mvn –v

MySQL数据库部署和安装

⾸先准备安装包

这⾥下载的是mysql-5.7.30-linux-glibc2.12-x86_64.tar.gz 安装包,并将其直接放在了root
⽬录下

卸载系统⾃带的MARIADB(如果有)

⾸先查询已安装的Mariadb 安装包:

1
rpm -qa|grep mariadb

将其均卸载之:

1
2
3
4
yum -y remove mariadb-server-5.5.56-2.el7.x86_64
yum -y remove mariadb-5.5.56-2.el7.x86_64
yum -y remove mariadb-devel-5.5.56-2.el7.x86_64
yum -y remove mariadb-libs-5.5.56-2.el7.x86_64

解压MYSQL安装包

1
2
tar -zxvf /root/mysql-5.7.30-linux-glibc2.12-x86_64.tar.gz -C /usr/local/
mv mysql-5.7.30-linux-glibc2.12-x86_64 mysql

创建MYSQL⽤户和⽤户组

1
2
groupadd mysql
useradd -g mysql mysql

修改MYSQL⽬录的归属⽤户

1
chown -R mysql:mysql ./

准备MYSQL的配置⽂件

在/etc ⽬录下新建my.cnf ⽂件

写⼊如下简化配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[mysql]
# 设置mysql客户端默认字符集
default-character-set=utf8
socket=/var/lib/mysql/mysql.sock
[mysqld]
skip-name-resolve
#设置3306端⼝
port = 3306
socket=/var/lib/mysql/mysql.sock
# 设置mysql的安装⽬录
basedir=/usr/local/mysql
# 设置mysql数据库的数据的存放⽬录
datadir=/usr/local/mysql/data
# 允许最⼤连接数
max_connections=200
# 服务端使⽤的字符集默认为8⽐特编码的latin1字符集
character-set-server=utf8
# 创建新表时将使⽤的默认存储引擎
default-storage-engine=INNODB
lower_case_table_names=1
max_allowed_packet=16M

同时使⽤如下命令创建/var/lib/mysql ⽬录,并修改权限:

1
2
mkdir /var/lib/mysql
chmod 777 /var/lib/mysql

开始安装MYSQL

执⾏如下命令正式开始安装:

1
2
cd /usr/local/mysql
./bin/mysqld --initialize --user=mysql --basedir=/usr/local/mysql --datadir=/usr/local/mysql/data

注意:记住上⾯打印出来的root 的密码,后⾯⾸次登陆需要使⽤

复制启动脚本到资源⽬录

执⾏如下命令复制:

1
cp ./support-files/mysql.server /etc/init.d/mysqld

并修改/etc/init.d/mysqld ,修改其basedir 和datadir 为实际对应⽬录:

1
2
basedir=/usr/local/mysql
datadir=/usr/local/mysql/data

设置MYSQL系统服务并开启⾃启

⾸先增加mysqld 服务控制脚本执⾏权限:

1
chmod +x /etc/init.d/mysqld

同时将mysqld 服务加⼊到系统服务:

1
chkconfig --add mysqld

最后检查mysqld 服务是否已经⽣效即可:

1
chkconfig --list mysqld

这样就表明mysqld 服务已经⽣效了,在2、3、4、5运⾏级别随系统启动⽽⾃动启动,以后可以直接使
⽤service 命令控制mysql 的启停。

启动MYSQL

1
service mysqld start

将MYSQL 的BIN ⽬录加⼊PATH 环境变量

编辑 ~/.bash_profile ⽂件,在⽂件末尾处追加如下信息:

1
export PATH=$PATH:/usr/local/mysql/bin

最后执⾏如下命令使环境变量⽣效

1
source ~/.bash_profile

⾸次登陆MYSQL

以root 账户登录mysql ,使⽤上⽂安装完成提示的密码进⾏登⼊

1
mysql -u root -p

接下来修改ROOT账户密码

在mysql的命令⾏执⾏如下命令即可,密码可以换成你想⽤的密码即可

1
2
alter user user() identified by "你的密码";
flush privileges;

设置远程主机登录

1
2
3
mysql> use mysql;
mysql> update user set user.Host='%' where user.User='root';
mysql> flush privileges;

最后利⽤FinaShell等⼯具进⾏测试即可

Redis缓存安装部署

⾸先准备REDIS安装包

这⾥下载的是redis-5.0.8.tar.gz 安装包,并将其直接放在了root ⽬录下

解压安装包

  1. 在/usr/local/ 下创建redis ⽂件夹并进⼊
1
2
3
cd /usr/local/
mkdir redis
cd redis
  1. 将Redis 安装包解压到/usr/local/redis 中即可

    1
    tar zxvf /root/redis-5.0.8.tar.gz -C ./

编译并安装

1
2
cd redis-5.0.8/
make && make install

将REDIS 安装为系统服务并后台启动

进⼊utils ⽬录,并执⾏如下脚本即可:

1
2
cd utils/
./install_server.sh

此处我全部选择的默认配置即可,有需要可以按需定制

查看REDIS服务启动情况

1
systemctl status redis_6379.service

启动REDIS客户端并测试

设置允许远程连接

编辑redis 配置⽂件

1
vim /etc/redis/6379.conf

将 bind 127.0.0.1 修改为 0.0.0.0

然后重启Redis 服务即可

1
systemctl restart redis_6379.service

设置访问密码

编辑redis 配置⽂件

1
vim /etc/redis/6379.conf

找到如下内容:

1
#requirepass foobared

去掉注释,将foobared 修改为⾃⼰想要的密码,保存即可

1
requirepass ******

保存,重启Redis 服务即可

1
systemctl restart redis_6379.service

这样后续的访问需要先输⼊密码认证通过⽅可:

消息队列RABBITMQ安装部署

⾸先安装ERLANG环境

为RabbitMQ 需要erlang 环境的⽀持,所以必须先安装erlang

执⾏如下命令来安装其对应的yum repo

RabbitMQ官方手册

1
curl -s https://packagecloud.io/install/repositories/rabbitmq/erlang/script.rpm | sudo bash

接下来执⾏如下命令正式安装erlang 环境:

1
yum install erlang-22.3.3-1.el7.x86_64

有可能要执行,看情况

接下来可以直接执⾏如下命令,测试erlang 是否安装成功

1
erl

安装RABBITMQ

依然是安装其对应的yum repo

1
curl -s https://packagecloud.io/install/repositories/rabbitmq/rabbitmqserver/script.rpm.sh | sudo bash

然后执⾏如下命令正式安装rabbitmq :

1
yum install rabbitmq-server-3.8.3-1.el7.noarch

设置RABBITMQ开机启动

1
chkconfig rabbitmq-server on

启动RABBITMQ服务

1
systemctl start rabbitmq-server.service

开启WEB可视化管理插件:

1
rabbitmq-plugins enable rabbitmq_management

访问可视化管理界⾯:

浏览器输⼊: 你的服务器IP:15672

能看到⽹⻚登录⼊⼝即可

我们可以在后台先添加⼀个⽤户/密码对:

1
2
rabbitmqctl add_user [username] [passwrod]
rabbitmqctl set_user_tags [username] administrator

然后登录⽹⻚即可:

应⽤服务器Tomcat安装部署

准备安装包

这⾥使⽤的是8.5.55 版: apache-tomcat-8.5.55.tar.gz ,直接将其放在了/root ⽬录下

解压并安装

在/usr/local/ 下创建tomcat ⽂件夹并进⼊

1
2
3
cd /usr/local/
mkdir tomcat
cd tomcat

将Tomcat 安装包解压到/usr/local/tomcat 中即可

1
tar -zxvf /root/apache-tomcat-8.5.55.tar.gz -C ./

启动Tomcat

直接进apache-tomcat-8.5.55 ⽬录,执⾏其中bin ⽬录下的启动脚本即可

1
2
cd bin/
./startup.sh

这时候浏览器访问: 你的主机IP:8080

配置快捷操作和开机启动

⾸先进⼊/etc/rc.d/init.d ⽬录,创建⼀个名为tomcat 的⽂件,并赋予执⾏权限

1
2
3
[root@localhost ~]# cd /etc/rc.d/init.d/
[root@localhost init.d]# touch tomcat
[root@localhost init.d]# chmod +x tomcat

接下来编辑tomcat ⽂件,并在其中加⼊如下内容

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
#chkconfig:- 20 90
#description:tomcat
#processname:tomcat
TOMCAT_HOME=/usr/local/tomcat/apache-tomcat-8.5.55
case $1 in
start) su root $TOMCAT_HOME/bin/startup.sh;;
stop) su root $TOMCAT_HOME/bin/shutdown.sh;;
*) echo "require start|stop" ;;
esac

一定要以UNIX格式保存,这就是要用NoteBook++的好处

这样后续对于Tomcat的开启和关闭只需要执⾏如下命令即可:

1
2
service tomcat start
service tomcat stop

提醒:运行service tomcat start,大概率会报如下错:

Tomcat Neither the JAVA_HOME nor the JRE_HOME environment variable is defined 或者 找不到tomcat文件

解决:inux解决方法:
编辑文件 /usr/local/tomcat/bin/catalina.sh (根据你自己的jdk路径进行修改) 在文件的正文开头,即正式代码前,大概在99行添加如下代码

1
2
export JAVA_HOME=/usr/local/your_jdk
export JRE_HOME=/usr/local/your_jdk/jre

注意:Windows下请修改对应的 catalina.bat

再一个注意:

Ubuntu/liunx 安装JDK10以上版本没有JRE目录的存在解决方法

​ 解决:需要在java根目录下,使用终端执行

1
sudo bin/jlink --module-path jmods --add-modules java.desktop --output jre 

最后加⼊开机启动即可:

1
2
chkconfig --add tomcat
chkconfig tomcat on

WEB服务器NGINX安装部署

⾸先安装包并解压

这⾥下载的是nginx-1.20.1.tar.gz 安装包,并将其直接放在了root ⽬录下

  1. 在/usr/local/ 下创建nginx ⽂件夹并进⼊
1
2
3
cd /usr/local/
mkdir nginx
cd nginx
  1. 将Nginx 安装包解压到/usr/local/nginx 中即可

    1
    tar zxvf /root/nginx-1.20.1.tar.gz -C ./

预先安装额外的依赖

1
2
yum -y install pcre-devel
yum -y install openssl openssl-devel

编译安装NGINX

1
2
3
cd nginx-1.20.1
./configure
make && make install

安装完成后,Nginx的可执⾏⽂件位置位于

1
/usr/local/nginx/sbin/nginx

启动NGINX

直接执⾏如下命令即可

1
[root@localhost sbin]# /usr/local/nginx/sbin/nginx

如果想停⽌Nginx服务,可执⾏:

1
/usr/local/nginx/sbin/nginx -s stop

如果修改了配置⽂件后想重新加载Nginx,可执⾏:

1
/usr/local/nginx/sbin/nginx -s reload

注意其配置⽂件位于:

1
/usr/local/nginx/conf/nginx.conf

特别注意.conf文件位置,开发经常要用到

浏览器验证启动情况

DOCKER环境安装

安装DOCKER

1
yum install -y docker

开启DOCKER服务

1
systemctl start docker.service

查看docker状态

1
systemctl status docker.service

查看安装版本

1
docker version

设置开机启动

1
systemctl enable docker.service

配置DOCKER镜像下载加速

直接编辑配置⽂件:

ps:如果没有,请参考阿里云镜像加速器

1
vim /etc/docker/daemon.json

加完加速地址后,重新加载配置⽂件,重启docker 服务即可

1
2
systemctl daemon-reload
systemctl restart docker.service

一、主语:

概念:句子叙述的主体。

可做主语:

​ 名词(都可做主语)、代词(人称代词+指示代词等,为避免重复而代替名词)、数词(基数词+序数词)、动词不定式、动名词、句子、其他。

不可做主语:

​ 谓语动词、形容词、介词、副词等
ps:英语规则死板,说一是一,说二是二。

二、谓语:

概念:

​ 用来说明主语的动作或状态的词语,分为动作型谓语+状态型谓语两大类。

动作型谓语:

​ 由动词构成,表动作/心理行为的词语。包括:简单谓语+复合谓语

(1)简单谓语:

​ 实义动词+动词短语
​ 实义动词:有实际意义的词,用来区分系动词、助动词、情态动词。
​ 动词短语(复合动词):由几个词构成但实际只是表示一个动作。如:look after “照顾”。
例句:
​ I work every day.(work是简单谓语)

​ I run every day. (run)

​ She looks after her brother. (look after)

(2)复合谓语:

​ 简单谓语+其他词语共同做谓语。包括3类:

A. 情态动词+实义动词/动词短语原形

​ 例句:I can speak Cantonese(can speak是复合谓语)

B. 助动词+实义动词/动词短语原形

​ 例句:I will buy a new tomorrow. (will buy是复合谓语)

C. 助动词+其他动词形式(be+doing或者have/has+done)

​ 例句:I am working now. (am working是复合谓语)

状态型谓语:

​ 由系动词(“be”8种形态)+表语构成
​ 例句:I am happy. (am happy是状态型谓语)

三、表语:

概念:

​ 属于谓语范畴,因为系动词+表语构成谓语,所以系动词和表语不可分开,简单理解为:系动词后面的词是表语。

可做表语:

​ 名词、形容词、介词短语(这三类画重点)、名词所有格、动词不定式、动名词、代词、数词、分词、副词、句子都可以。
​ 例句:I am rich .(rich是表语)
​ I am Nick. (Nick)