开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

LeetCode367 有效的完全平方数

发表于 2021-11-17

这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战。

题目描述:

367. 有效的完全平方数 - 力扣(LeetCode) (leetcode-cn.com)

给定一个 正整数 num ,编写一个函数,如果 num 是一个完全平方数,则返回 true ,否则返回 false 。

进阶:不要 使用任何内置的库函数,如 sqrt 。

示例一

1
2
ini复制代码输入: num = 16
输出: true

示例二

1
2
ini复制代码输入: num = 14
输出: false

提示:

  • 1 <= num <= 2^31 - 1

思路分析

暴力

这个没什么好说的,如果 num 是完全平方数, 那么一定存在一个整数 xxx 使得 x✖x=numx ✖ x = numx✖x=num,我们只要从1开始遍历即可。

二分查找

上面的暴力查找我们稍微优化下就可以想到二分查找法,我们知道 1≤x≤num1≤x≤num1≤x≤num,所以我们只要将 1 和 num 作为二分查找的边界即可。

AC代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Kotlin复制代码class Solution {
fun isPerfectSquare(num: Int): Boolean {
var left = 0L
var right = (num / 2).toLong() + 1
var target = num.toLong()
while (left <= right) {
val mind: Long = (left + right) / 2
val square = mind * mind
when {
square == target -> return true
square < target -> left = mind + 1
else -> right = mind - 1
}
}
return false
}
}

总结

看了官解,还有牛顿迭代法,说实话已经看到好几次这个解法了,但一直没有去看下,今天好好看了下,感觉又回到学校在学习数学知识,希望下次能融会贯通用上。

还有神奇的 通项公式法 367.有效的完全平方数——通项公式法 - 有效的完全平方数 - 力扣(LeetCode) (leetcode-cn.com)

参考

有效的完全平方数 - 有效的完全平方数 - 力扣(LeetCode) (leetcode-cn.com)

367.有效的完全平方数——通项公式法 - 有效的完全平方数 - 力扣(LeetCode) (leetcode-cn.com)

【宫水三叶】一题双解 :「二分」&「数学」 - 有效的完全平方数 - 力扣(LeetCode) (leetcode-cn.com)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JDK 17 sealed classes密封类

发表于 2021-11-17

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

欢迎关注公众号OpenCoder,来和我做朋友吧~❤😘😁🐱‍🐉👀

JDK 17 sealed classes密封类

1.简介

  • 密封类(class)和接口(interface)限制哪些其他类或接口可以扩展(extends)或实现(implements)它们。
  • 密封类JDK 15 中作为预览功能提供,并在JDK 16 中作为预览功能进行了改进。
  • 现在在 JDK 17 中,密封类被最终确定,与 JDK 16 没有任何变化。

2.目标

  • 允许类(class)或接口(interface)的作者控制负责实现它的代码。
  • 提供比访问修饰符更具声明性的方式来限制超类的使用。
  • 支持模式匹配(例如switch模式匹配中的案例)

3.动机

为什么要设计密封类(sealed classes)?

  1. 类和接口的继承层次结构

例如,作者想要提供关于常见、已知平面几何图形的一套API

* 图形Shape
    + 圆形Circle
        - 正圆形Round
        - 椭圆形Oval
    + 多边形Polygon
        - 三角形Triangle
        - 四边形Quadrilateral
    + 其他OtherShape
  1. 需求:期望超类(图形Shape)可以被访问、但是又不想被扩展(子类仅限于作者定义的已知的图形)
* Shape 也不能是包访问权限 应该是public:因为期望被开发者访问使用
* Shape 不能是final:否则作者自定的图形不能扩展
  1. 为了解决上述问题,引入sealed classes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public abstract sealed class Shape
permits Circle, Polygon, OtherShape { ... }

public sealed class Circle extends Shape
permits Round, Oval { ... }
public final class Round extends Circle { ... }
public final class Oval extends Circle { ... }

public sealed class Polygon extends Shape
permits Triangle, Quadrilateral { ... }
public final class Triangle extends Polygon { ... }
public final class Quadrilateral extends Polygon { ... }

public non-sealed class OtherShape extends Shape { ... }

4.密封类语法

sealed class 父类 permits 子类1,子类2 ....

​ 表示父类是密封类,指定可以被继承的有子类1,子类2 …

  1. permits指定的子类必须在父类的附近
  • 在同一个模块中(module jdk9新增),(父类在一个命名的模块中)
  • 或在同一包中(package),(父类在一个未命名的模块)
  1. 子类在大小和数量上都很小时,可以同父类定义在一个java文件中
  2. permits指定的子类必须直接继承该父类
  3. permits指定的子类在定义时必须使用以下三种修饰符中的一种
  • final:表示该子类是最终的,不能被继承
  • sealed:表示该子类是密封类,可以被指定的其他类继承
  • non-sealed:表示该子类是非密封类,可以被任意其他类继承

4.1子父类在同一模块中

image-20211103032532538

  • module-info.java
1
2
3
4
java复制代码module sealed_class_demo {
exports org.example.sealed.package1;
exports org.example.sealed.package2;
}
  • 代码
1
2
3
4
5
6
7
8
9
10
11
java复制代码public abstract sealed class SupperClass permits A, B, C {
}

public final class A extends SupperClass {
}

public final class B extends SupperClass {
}

public final class C extends SupperClass {
}
  • 否则会编译错误

image-20211103032755272

4.2子父类在同一个包中

image-20211103033211779

  • 代码
1
2
3
4
5
6
7
8
9
10
11
java复制代码public abstract sealed class SupperClass permits A, B, C {
}

public final class A extends SupperClass {
}

public final class B extends SupperClass {
}

public final class C extends SupperClass {
}

4.3子父类定义在同一个java文件中

  • 子类在大小和数量上都很小时,可以同父类定义在一个java文件中
  • 代码
1
2
3
4
5
java复制代码public abstract sealed class SupperClass {
final class A extends SupperClass{}
final class B extends SupperClass{}
final class C extends SupperClass{}
}

4.4子类修饰符

  • permits指定的子类在定义时必须使用以下三种修饰符中的一种
  • 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码// 密封类SupperClass 指定子类 A B C
public abstract sealed class SupperClass permits A, B, C {
}

// A不能被继承
public final class A extends SupperClass {
}

// B 继承SupperClass,并指定子类D
public sealed class B extends SupperClass permits D{

}

// D 继承B
public sealed class D extends B{

}

// C 可以被任意其他类继承
public non-sealed class C extends SupperClass {
}

public class Other extends C{}

5.密封接口

  • sealed interface 接口 permits 实现类1,实现类2 ....
  • 代码
1
2
3
4
5
6
7
8
9
10
11
java复制代码public sealed interface Inter permits A,B,C{
}

public final class A implements Inter{
}

public final class B implements Inter{
}

public final class C implements Inter{
}

6.JDK中的密封类

  • ConstantDesc是JDK12 中出现的一组API。定义了一些JVM中已知的符号引用,常量池中的常量描述符
  • JDK17 使用密封类对已知的继承体系做了优化
  • 代码
1
2
3
4
5
6
7
8
9
10
java复制代码public sealed interface ConstantDesc
permits ClassDesc,
MethodHandleDesc,
MethodTypeDesc,
Double,
DynamicConstantDesc,
Float,
Integer,
Long,
String { ... }

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
java复制代码sealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
final class C implements S {}

// 错误
static void switchStatementComplete(S s) {
switch (s) { // 编译错误,缺少类型B,自动提示
case A a :
System.out.println("A");
break;
case C c :
System.out.println("C");
break;
};
}
// 正确
static int testSealedCoverage(S s) {
return switch (s) {
case A a -> 1;
case B b -> 2;
case C c -> 3;
};
}

欢迎关注公众号OpenCoder,来和我做朋友吧~❤😘😁🐱‍🐉👀

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Redis哨兵及集群搭建 哨兵模式(sentinel) re

发表于 2021-11-17

「这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战」。

哨兵模式(sentinel)

Sentinel(哨兵) 是用于监控redis集群中Master状态的工具,是Redis高可用性解决方案,sentinel可以监视一个或多个redis master服务,以及这些master服务的所有从服务;当某个master服务下线时,自动将该master下的某个从服务升级为master服务替代已下线的master服务继续处理请求。一般建议sentinel采取奇数台,因为选举必须超过半数才有效。

哨兵模式搭建

创建sentinel文件夹,将sentinel.conf复制进去,我们准备搭建三个哨兵的场景,再复制两份sentinal配置文件

image.png

接下来对配置文件进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yaml复制代码
bind 0.0.0.0

protected-mode no

port 26379

daemonize yes

sentinel monitor mymaster 127.0.0.1 6378 3 //设置 主节点名称 ip 端口 参加选举的哨兵数

sentinel failover-timeout mymaster 3000 //主从切换超时时间

sentinel down-after-milliseconds mymaster 3000 // sentinel心跳检测3秒内无响应,视为挂掉,开始切换其他从为主。

其他两个也按上述配置,更改个端口号即可

然后进行启动,启动命令如下:

1
2
bash复制代码
./redis-server ../sentinel/sentinel-26377.conf --sentinel

当我们把主节点强制下线,我们来看下从节点的情况

image.png

可以看到经过选举,6378的从节点变为了主节点

image.png

6377从节点指定的主节点变为了6378

当我们把原来的那个主节点进行重启

image.png

可以看到6379自动就变为了从节点

哨兵模式原理

工作方式

  • 每个sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他Sentinel实例发送一个PING命令
  • 如果一个实例距离最后一次有效回复PING命令时间超过own-after-milliseconds选项所指定的值,则这个实例会被Sentine标记为主观下线。
  • 如果一个Master被标记为主观下线,则正在监视这个Master的所有sentinel要以每秒一次的频率确认Master的确进入主观下线状态。
  • 一般情况下,每个Sentinel 会以每10秒一次的频率向它已知的所有Master,Slave发送 INFO 命令
  • 当Master被sentinel标记为客观下线,sentinel向下线的master的所有salve发送info命令的频率会从10秒一次改为1秒一次。
  • 若没有足够数量sentinel同意master下线,master客户下线状态会被移除。

三个定时任务

sentinel在内部有3个定时任务

  • 每10秒每个sentinel会对master和slave执行info命令,这个任务达到两个目的:

发现slave节点

确认主从关系

  • 每2秒每个sentinel通过master节点的channel交换信息(pub/sub)。master节点上有一个发布订阅的频道(sentinel:hello)。sentinel节点通过__sentinel__:hello频道进行信息交换(对节点的”看法”和自身的信息),达成共识。
  • 每1秒每个sentinel对其他sentinel和redis节点执行ping操作(相互监控),这个其实是一个心跳检测,是失败判定的依据。

主观下线

所谓主观下线(Subjectively Down, 简称 SDOWN)指的是单个Sentinel实例对服务器做出的下线判断,即单个sentinel认为某个服务下线(有可能是接收不到订阅,之间的网络不通等等原因)。主观下线就是说如果服务器在down-after-milliseconds给定的毫秒数之内, 没有返回 Sentinel 发送的 PING 命令的回复, 或者返回一个错误, 那么 Sentinel 将这个服务器标记为主观下线(SDOWN )。

sentinel会以每秒一次的频率向所有与其建立了命令连接的实例(master,从服务,其他sentinel)发ping命令,通过判断ping回复是有效回复,还是无效回复来判断实例时候在线(对该sentinel来说是“主观在线”)。

sentinel配置文件中的down-after-milliseconds设置了判断主观下线的时间长度,如果实例在down-after-milliseconds毫秒内,返回的都是无效回复,那么sentinel回认为该实例已(主观)下线,修改其flags状态为SRI_S_DOWN。如果多个sentinel监视一个服务,有可能存在多个sentinel的down-after-milliseconds配置不同,这个在实际生产中要注意。

客观下线

客观下线(ODOWN)指的是多个sentinel实例在对同一个服务器做出SDOWN判断,并且通过 sentinel

is-master-down-by-addr 命令互相交流后,得出服务器下线判断,然后会开启failover。

客户下线要满足足够数量的sentinel都将一个服务器标记为主观下线之后,才会被标记我客观下线。

这个数量是由

entinel monitor 这里面的quorum来设置,quorum这个参数是进行客观下线的一个依据,意思是至少有quorum个sentinel主观认为master有故障,才会对master进行下线和故障转移,一般来说quorum的值设置为sentinel个数的二分之一加1,例如3个sentinel就设置2。

选举领头羊

一个redis服务被判断为客观下线,多个监视该服务的sentinel协商,选举一个领头sentinel,对该redis服务进行故障转移,选举领头sentinel遵循以下规则:

1)所有的sentinel都有公平被选举成领头的资格。

2)所有的sentinel都有且只有一次将某个sentinel选举成领头的机会(在一轮选举中),一旦选举某个sentinel为领头,不能更改。

3)sentinel设置领头sentinel是先到先得,一旦当前sentinel设置了领头sentinel,以后要求设置sentinel为领头请求都会被拒绝。

4)每个发现服务客观下线的sentinel,都会要求其他sentinel将自己设置成领头。

5)当一个sentinel(源sentinel)向另一个sentinel(目sentinel)发送is-master-down-by-addr ip port current_epoch runid命令的时候,runid参数是sentinel运行id,就表示源sentinel要求目标sentinel选举其为领头。

6)源sentinel会检查目标sentinel对其要求设置成领头的回复,如果回复的leader_runid和leader_epoch为源sentinel,表示目标sentinel同意将源sentinel设置成领头。

7)如果某个sentinel被半数以上的sentinel设置成领头,那么该sentinel既为领头。

8)如果在限定时间内,没有选举出领头sentinel,暂定一段时间,再选举。

自动故障转移机制

sentinel保存了主节点的所有从节点信息,领头sentinel按照如下规则从服务列表中选出新的主节点:

  • 过滤掉主观下线的节点
  • 选择slave-priority(权重值,可在配置文件中进行设定)最高的节点,如果有则返回没有就继续选择
  • 选择出复制偏移量最大的节点,因为复制偏移量越大数据复制的越完整,如果有就返回,没有就继续。
  • 选择run_id最小的节点。

通过slaveof no one 命令,让选出来的从节点成为主节点;并通过salveof命令让其他节点成为其从节点。当已下线的服务重新上线,sentinel会向其发送slaveof命令,让其成为新的从节点。

redis集群

集群搭建

创建cluster文件夹,将redis的配置文件放入进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arduino复制代码
daemonize yes //开启守护线程

logfile "6379.log"

dbfilename "dump-6379.rdb"

protected-mode no //关闭代表外部网络可以直接访问,开启需配置bind ip或者访问密码

port 6379

bind 0.0.0.0

pidfile /var/run/redis_6379.pid //pidfile文件

cluster-enabled yes //开启集群

cluster-config-file nodes_6379.conf //集群的配置 配置文件首次启动自动生成

cluster-node-timeout 15000 //请求超时,默认15秒

这个配置完成后,再拷贝两个配置文件,修改端口号,其他不变

启动节点

1
2
3
4
5
6
bash复制代码
./redis-server ../cluster/redis-6380-master.conf

./redis-server ../cluster/redis-6381-master.conf

./redis-server ../conf/redis-6379-master.conf

接着要启动集群,启动集群要依赖ruby,所以先安装ruby

安装ruby

wget cache.ruby-lang.org/pub/ruby/2.…

tar -xvf ruby-2.3.1.tar.gz

./configure –prefix=/usr/local/ruby

make

make install

cd /usr/local/ruby

cp bin/ruby /usr/local/bin

cp bin/gem /usr/local/bin

wget rubygems.org/downloads/r…

gem install ./redis-3.3.0.gem

gem list –check redis gem

启动集群

1
2
css复制代码
redis-cli --cluster create 192.168.121.133:6379 192.168.121.133:6380 192.168.121.133:6381 192.168.121.133:6376 192.168.121.133:6377 192.168.121.133:6378 --cluster-replicas 1

最后的这个cluster-replicas 1 代表的是主节点和从节点的比例,比例算法是主节点/从节点,是1,代表主从是1:1,这就表明1个主节点就需要1个从节点,如果是三个主节点就需要三个从节点,集群默认必须要有三个主节点,所以就要再配三个从节点,也就是至少需要6个节点,如果集群没有启动成功就将所有的rdb文件和node开头的conf文件删除,然后重启再开启集群。

集群启动成功后,进入客户端命令后面要加-c

1
2
3
4
5
6
ruby复制代码
./redis-cli -p 6380 -c

127.0.0.1:6379> cluster info 查询集群信息

127.0.0.1:6379> cluster nodes 差群集群有多少节点

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL实战45讲 1sql的执行过程 2日志系统

发表于 2021-11-17

1.sql的执行过程

MySQL的基本逻辑架构

img

Service层(客户端层)大多的跨引擎的功能都在这里,如存储过程。不同的存储引擎共用一个service层。

存储引擎层负责数据的提取和存储。其架构是插件时的,即持 InnoDB、MyISAM、Memory 等多个存储引擎。InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。

每个组件的作用

连接器

当连接上数据库时,会先于连接器交互。它负责跟客户端建立连接,校验密码,获取权限,管理连接等。

数据库里长链接指连接成功后,如果后续有多次请求,则用的是同一个连接。短连接是执行少量的查询便断开。下次查询再重新建立一个。

尽量减少使用长链接。mysql执行中一些数据绑定在连接对象中,因为长连接长时间不断开导致内存占用太多。可以使用定时超时断开,或通过重新初始化链接来清空。

查询缓存

建立连接后。则开始查询缓存。MySQL拿到请求后,会先去查询缓存,是否有相关数据。之前执行的语句及其结果会以key-value的形式,被缓存在内存中。key是查询语句,value是结果。若有,则返回value,若没有,则执行后续的操作,并将结果插入缓存。

但查询缓存命中率太低。因为只要对表有更新,就会让整个表的缓存清空。建议静态表使用。

可以将参数 query_cache_type 设置成 DEMAND,这样对于默认的 SQL 语句都不使用查询缓存。而对于你确定要使用查询缓存的语句,可以用 SQL_CACHE 显式指定,像下面这个语句一样:

1
csharp复制代码mysql> select SQL_CACHE * from T where ID=10;

MySQL 8.0 版本直接将查询缓存的整块功能删掉了

分析器

若没有命中缓存,则开始执行语句。首先分析器会做词法分析,它要知道语句中的单词都是什么意思。如识别关键字,表名,列名等。

然后再进行语法分析,分析sql是否满足语法。

优化器

分析器让MySQL明白你要干什么,优化器则要开始决定怎么做。每个语句不同的执行顺序带来的执行效率是不同的,如有的条件字段有索引等。优化器将会选择一个最优的方案。原则就是:尽可能少的扫描数据库行记录

执行器

通过优化器知道该怎么做,则现在要通过执行器来执行sql语句。

开始执行之前会判断一下你对要操作的表是否有相应的执行权限。到了执行的时候才会进入到数据库引擎,然后执行器也是通过调用数据库引擎的API来进行数据操作的。也因此数据库引擎才会是插件形式的。

1
csharp复制代码select * from T where ID=10;

以上语句为例,字段id没有。其过程如下:

1.调用引擎接口取表的第一行,判断id是否等于10,不是则跳过,是则存在结果集中。

2.调用接口取下一行,重复判断逻辑。直到取到表的最后一行

3.执行器将所有满足条件的行记录组成的结果集返回。

有索引跟上述过程也差不多。其中接口都是引擎定义好的。

2.日志系统

redo log(innoDB特有)

redolog是物理日志,记录的是“在某个数据页做了什么修改”。

当有一条记录更新时,InnoDB引擎先把记录写到redo log 中,redo log 也在磁盘上,这也是一个写磁盘的过程,但是与更新过程不一样的是,更新过程是在磁盘上随机IO,费时。 而写redo log 是在磁盘上顺序IO。效率要高。

同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。

与此类似,InnoDB 的 redo log 是固定大小的,是循环写。比如可以配置为一组 4 个文件,每个文件的大小是 1GB。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。

img

write pos是当前记录的位置,一边写一遍往后推。check point是当前要擦除的位置,也是往后推且循环。当两个点重合,表示redo long满了,将不会执行新的更新操作,擦掉一下记录,并将check point往后推。

擦除记录之前,会将要擦除的记录给写入磁盘中。

redo log保证了在宕机,发生异常时时候保证之前更新的数据不会丢失。这个能力成为carsh-safe

binlog(service层特有,与存储引擎无关)

binlog是逻辑日志,记录是语句的原始逻辑,如“给id为1的a字段加1”

binlog是追加写,是指当binlog文件写到一定的大小时,会换一个文件继续写,不会删除、覆盖之前的记录。

两阶段提交

当执行更新操作时。两日志的流程如下:

1.将更新后的数据更新到内存中,同时将这次的更新操作写入redo log中,此时redo log处于prepare状态。并告知执行器执行完成,并可以随时提交事务。

2.执行器生成这个操作的binlog,并把binlog写入磁盘。

3.执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。

img

\

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java中wait和sleep方法的不同 Java中wait

发表于 2021-11-17

学习笔记:

Java中wait和sleep方法的不同

最大的不同是在等待时wait会释放锁,而sleep一直持有锁。Wait通常被用于线程间交互,sleep通常被用于暂停执行。

直接了解的深入一点吧: 40.jpg 在Java中线程的状态一共被分成6种:

  • 初始态:NEW 创建一个Thread对象,但还未调用start()启动线程时,线程处于初始态。
  • 运行态:RUNNABLE 在Java中,运行态包括就绪态和运行态。 就绪态该状态下的线程已经获得执行所需的所有资源,只要CPU分配执行权就能运行。所有就绪态的线程存放在就绪队列中。 运行态获得CPU执行权,正在执行的线程。由于一个CPU同一时刻只能执行一条线程,因此每个CPU每个时刻只有一条运行态的线程。
  • 阻塞态 当一条正在执行的线程请求某一资源失败时,就会进入阻塞态。而在Java中,阻塞态专指请求锁失败时进入的状态。由一个阻塞队列存放所有阻塞态的线程。处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。PS:锁、IO、Socket等都资源。
  • 等待态 当前线程中调用wait、join、park函数时,当前线程就会进入等待态。也有一个等待队列存放所有等待态的线程。线程处于等待态表示它需要等待其他线程的指示才能继续运行。进入等待态的线程会释放CPU执行权,并释放资源(如:锁)
  • 超时等待态 当运行中的线程调用sleep(time)、wait、join、parkNanos、parkUntil时,就会进入该状态;它和等待态一样,并不是因为请求不到资源,而是主动进入,并且进入后需要其他线程唤醒;进入该状态后释放CPU执行权 和 占有的资源。与等待态的区别:到了超时时间后自动进入阻塞队列,开始竞争锁。
  • 终止态 线程执行结束后的状态。

注意:

  • wait()方法会释放CPU执行权 和 占有的锁。
  • sleep(long)方法仅释放CPU使用权,锁仍然占用;线程被放入超时等待队列,与yield相比,它会使线程较长时间得不到运行。
  • yield()方法仅释放CPU执行权,锁仍然占用,线程会被放入就绪队列,会在短时间内再次执行。
  • wait和notify必须配套使用,即必须使用同一把锁调用;
  • wait和notify必须放在一个同步块中调用wait和notify的对象必须是他们所处同步块的锁对象。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL 按照主键更新报Deadlock错误原因分析

发表于 2021-11-17

这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战

问题描述

报错信息很简单在执行update操作语句的时候报错。

Deadlock found when trying to get lock; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock;

lQLPDhrgKxz8mX3NAe_NBb6wVHF5PgQasK8BnIUznEBYAA_1470_495.png

MySQL锁介绍

MySQL有三种锁的级别:页级、表级、行级

image.png

  1. 行级锁在使用的时候并不是直接锁掉这行记录,而是锁索引
  2. 如果一条sql用到了主键索引(mysql主键自带索引), mysql会锁住主键索引;
  3. 如果一条sql操作了非主键索引,mysql会先锁住非主键索引,再锁定主键索引.

原因分析

报错的原因很简单,我们在按照主键更新表记录的时候,SQL抛了一个死锁的错误。
那就很奇怪按照主键索引更新按道理不会产生错误, 我们观察报错的时间几次都是在凌晨3点左右的时候抛的异常。 3点的时候我们会将这个表的数据做备份,执行delete删除过期的数据,删除数据的时候索引。

我们先看第一个更新的SQL, 一点毛病没有,先查询出来主键ID,然后更新记录

image.png

然后看一眼这张表的索引, 也很正常

image.png

排除更新代码问题,那么肯定是有其他地方更新非主键索引导致的行锁级别提升导致死锁。注意时间点每次都是凌晨3点钟, 是有一个定时任务备份历史记录的操作。

image.png

我们可以看到send_time 是非主键索引,当定时任务更新的时候,这个时候在操作更新操作会导致死锁。

参考

最后推荐一个Github维护的一个解决死锁案例汇总的一个项目。
github.com/aneasystone…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

两数之和->三数之和->四数之和 LeetCode1 两数之

发表于 2021-11-17

这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战

LeetCode1 两数之和题解

题目

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。

举个例子:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

解题思路

思路一 暴力解法

显而易见经过两层的循环嵌套,就能把答案的两个值找出来,比较简单,时间复杂度显而易见就是O(n^2),不过这个方法比较耗时,提交了以后会超时。

思路二 哈希表法

因为哈希表查找值的时间复杂度是O(1),所以可以利用哈希表通过key-value的方式快速查找需要的值。首先遍历一遍数组,将数组值和对应下标输入进hashmap中,之后再次遍历一次数组,对每一个数组元素nums[i],在hashmap里寻找target-nums[i],如果找到则找到题目答案,否则无解。

代码展示

思路一 暴力解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public int[] twoSum(int[] nums, int target) {
int a[] = new int[2];
a[0] = a[1] = 0;
for(int i=0;i< nums.length;i++)
{
for(int j=i+1;j< nums.length;j++)
{
if(nums[i]+nums[j]==target)
{
a[0] = i;
a[1] = j;
return a;
}
}
}

return a;
}

思路二 哈希表法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public int[] twoSum(int[] nums, int target) {
HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
for(int i=0;i< nums.length;i++)
{
map.put(nums[i],i);
}
for(int i=0;i< nums.length;i++)
{
int comp = target - nums[i];
if(map.containsKey(comp)&&map.get(comp)!=i)
{
int a[] = new int[2];
a[0] = i;
a[1] = map.get(comp);
return a;
}
}
return new int[2];
}

LeetCode15 三数之和题解

题目

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。

注意:答案中不可以包含重复的三元组。

举个例子:
给定数组 nums = [-1, 0, 1, 2, -1, -4],

满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]

解题思路

思路一 暴力解法

很简单能想出来用三层循环嵌套可以暴力地解出来答案,但是时间复杂度是O(n^3),所以没有尝试。

思路二 单层循环+双指针

为了防止输出的答案包含重复的三元组,先对原数组进行排序,可以使用自带的快速排序函数,时间复杂度为O(nlogn)。之后遍历数组,对于每个数组值nums[i],只需要在下标i之后(在下标i之后进行寻找是为了防止重复三元组出现)找到两个数组值nums[a]和nums[b](b>a>i),使得满足nums[a]+nums[b]==0-nums[i]。将当前数组值nums[i]、i+1(左指针)、nums.length-1(右指针)以及数组nums输入到函数Twosum()中寻找需要的nums[a]和nums[b]。

在函数Twosum()中是通过左右双指针寻找两个值和为 -nums[i] ,由于nums数组是有序的(从小到大),因此当nums[left]+num[right]>-nums[i] 时,将右指针左移,同理当nums[left]+num[right]<-nums[i] 时,将左指针右移。当检测到nums[left]+num[right]=-nums[i],此时左右指针指向的数组值即为一个答案,将他们和nums[i]放进list中,同时需要移动左右指针,以左指针为例,若nums[left+1]==nums[left],则需要右移左指针排除重复值,右指针同理。最后将左右指针各自让中间移动一次进行下一次的寻找。

举个例子

初始数组

对于数组[-1,0,1,2,-1,4],需要三个数字使他们和为0,首先对其进行排序。

i=0

当i=0时,即nums[i]=-4,在此数组中剩余的数中不存在两个数满足和为4。

i=1

当i=1时,即nums[i]=-1,left指针指向第二个 -1,right指针指向2,此时满足条件,-1+2 = 0-(-1)

i=-1

因为left指针右侧和right指针左侧不存在重复值,因此直接各向中间移动一次,此时left指向0,而right指向1,此时满足条件,0+1 = 0-(-1)

i=3

因为当i=2时发现nums[1]==nums[2],因此跳过i=2的情况,进入到i=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
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
java复制代码    public static List<List<Integer>> Twosum(int start,int end,int nums[],int value)
{
List<List<Integer>> list = new ArrayList<>();
while(start<end)
{
if(start<end)
{
int a = nums[start];
int b = nums[end];
while(nums[start]+nums[end]>(0-value)&&start<end)
{
end--;
}
while(nums[start]+nums[end]<(0-value)&&start<end)
{
start++;
}
if(nums[start]+nums[end]==(0-value)&&start<end)
{
List<Integer> alist = new ArrayList<>();
alist.add(value);
alist.add(nums[start]);
alist.add(nums[end]);
list.add(alist);
//防止重复
while(start!= nums.length-1&&nums[start+1]==nums[start])
start++;
while(end!=0&&nums[end]==nums[end-1])
end--;
if(start>=end)
break;
start++;
end--;
}
}

}
return list;
}



public static List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);

for(int i=0;i< nums.length;i++)
{
if(i>0&&nums[i]==nums[i-1])
continue;
List<List<Integer>> alist = Twosum(i+1, nums.length-1,nums,nums[i]);
for (int j=0;j<alist.size();j++)
list.add(alist.get(j));

}



return list;
}

LeetCode18 四数之和题解(附伪代码步骤)

题目

给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。

举个例子:
给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。
满足要求的四元组集合为:
[
[-1, 0, 0, 1],
[-2, -1, 1, 2],
[-2, 0, 0, 2]
]

解题思路

前面写过了LeetCode第1题“两数之和”以及LeetCode第15题“三数之和”,这道题就比较简单了,思路与“三数之和”题思路类似

首先对原数组进行排序,用这种方法防止结果出现重复的四元组。之后遍历nums数组,在遍历到第i个元素nums[i]时,将第i+1的元素到最后一个元素组成一个新数组,在这个新数组中寻找三个数使得满足他们的和等于target - nums[i],之后的步骤就可以调用三数之和的函数进行寻找。
不过需要注意的是,为了防止出现重复的四元组,在遍历nums数组时,如果nums[i]==nums[i-1]时,需要跳过此次遍历。

伪代码步骤

  • step1:对nums数组进行排序
  • step2:遍历nums数组
  • step3:如果当前数组值等于前一个数组值,那么跳过此次循环
  • step4:将当前数组值之后的数组值组成一个新的数组
  • step5:调用threesum()函数进行检索是否有满足条件的三元组
  • step6:将找到的三元组分别与当前值组成四元组

代码展示

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
java复制代码public static List<List<Integer>> Twosum(int start,int end,int nums[],int value,int target)
{
List<List<Integer>> list = new ArrayList<>();
while(start<end)
{
if(start<end)
{
int a = nums[start];
int b = nums[end];
while(nums[start]+nums[end]>(target)&&start<end)
{
end--;
}
while(nums[start]+nums[end]<(target)&&start<end)
{
start++;
}
if(nums[start]+nums[end]==(target)&&start<end)
{
List<Integer> alist = new ArrayList<>();
alist.add(value);
alist.add(nums[start]);
alist.add(nums[end]);
list.add(alist);
//防止重复
while(start!= nums.length-1&&nums[start+1]==nums[start])
start++;
while(end!=0&&nums[end]==nums[end-1])
end--;
if(start>=end)
break;
start++;
end--;
}
}

}
return list;
}



public static List<List<Integer>> threeSum(int[] nums,int target) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);

for(int i=0;i< nums.length;i++)
{
if(i>0&&nums[i]==nums[i-1])
continue;
List<List<Integer>> alist = Twosum(i+1, nums.length-1,nums,nums[i],target - nums[i]);
for (int j=0;j<alist.size();j++)
list.add(alist.get(j));

}
return list;
}

public static List<List<Integer>> fourSum(int[] nums,int target)
{
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);

for (int i=0;i< nums.length-2;i++)
{
if(i>0&&nums[i]==nums[i-1])
continue;
int a[] = new int[nums.length-i-1];
for (int j = 0;j<a.length;j++)
{
a[j] = nums[i+j+1];
}
List<List<Integer>> alist = threeSum(a,target-nums[i]);
for (int j=0;j<alist.size();j++)
{
List<Integer> list = new ArrayList<>();
list.add(nums[i]);
for (int t=0;t<alist.get(j).size();t++)
{
list.add(alist.get(j).get(t));
}
res.add(list);
}
}
return res;
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

「过程宏」part1

发表于 2021-11-17

「这是我参与11月更文挑战的第 10 天,活动详情查看:2021最后一次更文挑战」


构建过程宏,要在cargo.toml里面设置一些参数,这是必须的。一般来说,过程宏必须是一个库,或者作为工程的子库,不能单独作为一个源文件存在,至少目前不行。

所以在 cargo new [proc-lib] --lib 中一般需要标明:

1
2
3
toml复制代码[lib]
proc-macro = true
path = "src/lib.rs"

从分类上,可以分为大致3类:

  • proc-macro
  • proc-macro-derive
  • proc-macro-attribute

大致说一下这几种的形态:

格式

简略过一下这几个宏的格式,在后面会具体讲他们的使用范围:

proc-macro

1
2
3
4
rust复制代码#[proc_macro]
pub fn my_proc_macro(input: TokenStream) -> TokenStream{
// ...
}

可以看出函数式的过程宏只接受一个形参,而且必须是pub的。

proc_macro_derive

1
2
3
4
rust复制代码#[proc_macro_derive(MyDerive)]
pub fn my_proc_macro_derive(input: TokenStream) -> TokenStream{
// ...
}

proc_macro_derive 表明了这是继承宏,还定义了新的继承宏的名字 MyDerive。 熟悉rust编程的,都应该知道有个继承宏,一直用得到,就是 Debug。这是标准库里的,可以帮助调试和显示。

proc_macro_attribute

1
2
3
4
rust复制代码#[proc_macro_attribute]
pub fn my_attribute_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
// ...
}

可以看到这里的形参是两个,使用的关键字是 proc_macro_attribute。 关于例子,熟悉python的人应该知道修饰器吧,其实本质就是函数(闭包)可以作为一个对象来返回。 比如我需要一个修饰器来测量一个调用函数的运行时间。

示例

使用代码:

1
2
3
4
rust复制代码#[handler(blog(::hxl::com))]
fn fake(a: i32) {
println!("hello proc-derive -> fake func args: {}", a);
}

宏代码:

1
2
3
4
5
6
rust复制代码#[proc_macro_attribute]
pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream {
eprintln!("attr: {:#?}", attr);
eprintln!("item: {:#?}", item);
item
}

cargo check 我们可以在终端中看到:

可以看到目前 attr/item 其实都只是被识别成一个个标志符,没有任何意义,因为要把这些标志符组合有很大的难度。所以需要引入:syn/quote

syn & quote

1
2
3
4
5
6
7
8
9
10
rust复制代码
#[proc_macro_attribute]
pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream {
let args: AttributeArgs = parse_macro_input!(attr as AttributeArgs);
eprintln!("args: {:#?}", args);

let body = parse_macro_input!(item as Item);
eprintln!("body: {:#?}", body);
quote!(#body).into()
}
  1. parse_macro_input! -> 是将 TokenStream 转换为语法树
    • 当识别成 AttributeArgs,输出的是 Vec<NestedMeta>
    • 当识别成 Item,输出的是 Item
  2. quote! -> 可以将语法树及其节点转换为 TokenStream
    • #body_ast 并不属于 Rust 表达式,是 quote! 自己实现的一种自定义语法
    • 返回值是 proc_macro2::TokenStream,通过 into() 才能转换,这个有历史原因
    • quote/syn/proc_macro2 目的都是为了更方便处理和编辑以及生成 proc_macro::TokenStream

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Spring Boot 快速入门】十八、Spring Bo

发表于 2021-11-17

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」

前言

  在操作关系数据库管理系统里,经常会遇到锁的问题,在数据库中有行锁、页锁和表锁。在Java的开发过程中,经常会遇到悲观锁和乐观锁。乐观锁和悲观锁对于理解Java多线程、并发和数据库来说至关重要。下面和大家已起聊聊关于Mybatis_Plus乐观锁。

数据库锁

  锁是数据库中的一个重要的概念,在大数据高并发的情况下,如果同一条数据被多个线程读取,可能会出现幻读、脏读、误读的情况。所以引入了锁。在程序员操作方面,可以根据锁的使用分类悲观锁和乐观锁。

  悲观锁总是认为最坏的情况会出现,当前的数据可能被修改,从而在读取数据的时候就会把资源或者数据进行锁定,其他线程使用这个数据或者资源的时候就会阻塞等待,直到悲观锁将锁释放之后,其他资源才可以使用。

  乐观锁与悲观锁正好相反,乐观锁总是认为资源和数据不会被修改,在读取数据和资源时不会进行加锁。但是乐观锁是在写入操作的时候会判断当前数据和资源是否修改过。乐观锁的实现方案一般来说有两种: 版本号机制 和 CAS实现 。

  在我们的项目中,使用Mybatis_Plus敏捷开发,近期学习了一下基于Mybatis_Plus的乐观锁,下面快速开始熟悉吧。

快速开始

版本介绍

  本次将基于Spring Boot 搭建一个学习Mybatis_Plus的乐观锁的Demo。开发环境如下:

1
2
3
js复制代码JDK1.8
SpringBoot 2.3.0.RELEASE
mybatis-plus 3.3.0

  Mybatis_Plus的乐观锁的实现原理是基于版本号机制进行控制的。当在取出一条数据后,将对该条数据进行更新修改操作,会获取当前数据的version版本号,在更新时带上version版本号信息,执行更新时,会判断当前的version版本号与数据库中的版本号是否一致,一致则更新成功version版本号+1,如果version版本号信息不一致,则证明数据被修改过,更新数据失败。了解了原理,下面快速开始。

引入依赖

本次学习使用的依赖包如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
js复制代码 <dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>3.4.0</version>
</dependency>

配置乐观锁

在项目中配置乐观锁的拦截器信息如下:

1
2
3
4
5
6
7
8
9
10
js复制代码@Component
public class MybatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}

指定版本号

  在我们数据库的实体对象中指定一个版本号字段,并用注解@Version注释该字段。@Version注释的字段类型支持:int,Integer,long,Long,Date,Timestamp,LocalDateTime,在整数类型下newVersion = oldVersion + 1会自动递增版本号,需要注意的是仅支持updateById()和update(entity, wrapper)方法。
本次建立一个User对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码@ApiModel(value = "com-example-demo-module-User")
@TableName("user")
public class User {

@TableId(value = "id", type = IdType.INPUT)
private Integer id;
@TableField(value = "name")
private String name;
@TableField(value = "age")
private int age;
@Version
private int version;
}

单元测试

编写完如上基本配置之后,就可以进行简单的单元测试了。首先我们创建一个用户信息。

1
2
3
4
5
6
7
js复制代码    @Test
void TestUser(){
User user = new User();
user.setName("测试1");
user.setAge(12);
userMapper.insert(user);
}

可以看到用户“测试1”已经新增成功,当前的用户id是19。

图片.png

编写单元测试二,本次更新2次id为19的用户信息。

1
2
3
4
5
6
7
8
9
10
11
js复制代码     @Test
void TestUser(){
User user = userService.getById(19);
System.out.println(user);
//开始更新一次
userService.updateByTest(user);
user.setName("测试32222");
userService.updateById(user);
User user1 = userService.getById(user.getId());
System.out.println("第二个更新的方法:" +user1);
}

运行结果如下,可以看到版本号已经进行了自动更新:
图片.png

1
2
3
js复制代码User{id=19, name='测试32222', age=12, version=0}
第一个更新的方法:User{id=19, name='123456', age=12, version=1}
第二个更新的方法:User{id=19, name='测试32222', age=12, version=2}

结语

  好了,以上就是Spring Boot 集成Mybatis_Plus的乐观锁,感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

  作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

状态模式

发表于 2021-11-17

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

概述

【例】通过按钮来控制一个电梯的状态,一个电梯有开门状态,关门状态,停止状态,运行状态。每一种状态改变,都有可能要根据其他状态来更新处理。例如,如果电梯门现在处于运行时状态,就不能进行开门操作,而如果电梯门是停止状态,就可以执行开门操作。

类图如下:

代码如下:

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
java复制代码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
java复制代码//抽象状态类
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,缺点:

  • 状态模式的使用必然会增加系统类和对象的个数。
  • 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
  • 状态模式对”开闭原则”的支持并不太好。

使用场景

  • 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
  • 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…303304305…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%