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

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


  • 首页

  • 归档

  • 搜索

Java如何生成序列号/订单号 前言 理论知识 Java代码

发表于 2021-11-28

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

前言

今天给大家带来也是比较实用的功能,用Java来生成序列号/订单号,列举几个在我们生活中比较常见的案例:

  1. 订单号
  2. 商品编号
  3. 交易单号
  4. 快递单号

数据存储我是使用的mysql,下面就向大家分享一下是如何实现的~

理论知识

什么是事务?

数据库事务(简称:事务)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。

脏读、不可重复读、幻读

1、脏读:A事务对数据修改但还没有提交到数据库,这个时候B事务来访问,那么B事务对数据就不是最新的,这种现象被成为脏读。

2、不可重复读:A事务多次读取一个数据,这个时候在中途B事务修改了数据,导致A事务多次读到的结果不一致。

3、幻读:A事务在前后两次查询同一个范围的时候、后一次查询看到了前一次查询未看到的行,因为B事务在后一次查询前新增加了一条数据。

mysql的四种隔离级别

按照隔离的级别由低到高,越高的隔离,效率越差,不可重复读,是 MySQL 的默认隔离级别。

事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted) 是 是 是
不可重复读(read-committed) 否 是 是
可重复读(repeatable-read) 否 否 是
串行化(serializable) 否 否 否

1、读未提交:允许别的事务,去读取这个事务为提交之前的数据
缺点:可能会造成脏读、幻读、不可重复读。

2、不可重复读:并发条件下会出现问题,比如:A用户读取数据,随后B用户读出该数据并修改,此时A用户再读取数据时发现前后两次的值不一致
缺点:可能会造成幻读、不可重复读。

3、可重复读:当使用可重复读隔离级别时,在事务执行期间会锁定该事务以任何方式引用的所有行。
缺点: 幻读

4、串行化:不会使用mysql的mvcc机制,在每一个select请求下获得读锁,在每一个update操作下尝试获得写锁。
缺点:效率最差

两种悲观锁

共享锁(S锁):如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。

排它锁(X锁):如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。

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
java复制代码private  String createNewBidNumber() {
//格式说明 CODE20201111xxx CODE+当前年月日+编号(具体长度看需求)
String front="CODE";//前缀
//当前时间编码
Date date = new Date();
String bidDate = new SimpleDateFormat("yyyyMMdd").format(date);
Object bidService=null;//修改为自己的业务代码
if (bidService != null){// 在数据表中查到了,说明现在这个订单不是今天的第一单
String bid = bidService.getXXXX(); //取出ID,也就是业务号
bid = bid.substring(10,13); // 取出后三位数,也就是自动生成的三位数 001
int num = Integer.valueOf(bid);
num ++; // 加1
if(num<10){
String bidNum = String.format("%03d", num);//%03d 只是三位,不足补0
String code = front+bidDate+bidNum;
return code;
}
else if(num<100){
String bidNum = String.format("%03d", num);//num<100,说明是两位数,前面要补一个0
String code = front+bidDate+bidNum;
return code;
}
else {
String bidNum = String.valueOf(num);
String code =front+bidDate+bidNum;
return code;
}
}else {
int number = 1;
String bidNum = "00" + number;
String code = front+bidDate+bidNum;
return code;
}
}

致谢

  1. 数据库mysql之脏读、不可重复读、幻读
  2. 十分钟搞懂MySQL四种事务隔离级别
  3. 事物级别,不可重复读和幻读的区别
  4. 脏读、不可重复读 共享锁、悲观锁 和
    事务五种隔离级别
  5. 关于MySQL可重复读的理解
  6. mysql-serializable-序列化隔离级别-串行化实例场景

本文转载自: 掘金

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

最接近的三数之和

发表于 2021-11-28

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

leetcode 最接近的三数之和

你一个长度为 n 的整数数组 nums 和 一个目标值 target。请你从 nums 中选出三个整数,使它们的和与 target 最接近。

返回这三个数的和。

假定每组输入只存在恰好一个解。

示例 1:

1
2
3
ini复制代码输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。

示例 2:

1
2
ini复制代码输入:nums = [0,0,0], target = 1
输出:0

解题:三数之和要最接近target,就是这个和与target的差值要最小,那就只能算出每个三数之后,然后取最接近target的和,当然如果有等于target的和那就可以直接返回了。首先需要循环数组的每一个数,然后定义两个指针left、right取循环获取其他两个数(从数组两端),然后相加还得总和,有点像暴力循环的感觉,对于无序的数组这样每次都需要循环获取数组每一个数,那如果数组事先有序就好办多了,两个指针就从当前位置index+1至nums.length - 1位置开始相向移动遍历就行,这样慢慢的可以减少遍历元素的次数,算出三个数之和,与之前的和比较,保证获取最小的差值。left、right两个指针只需要移动一个,移动那个需要判断总和的大小,因为数组有序了,如果总和sum比target大,那说明right指针的数需要小一点才行,所以需要移动right指针,可以加一个判断,如果移动后的元素值与移动前的元素值相等,那之后的获取总和判断是没有意义的,所以可以再移动到一个不相等的元素位置就好,反之就移动left指针,最后直到两个指针相撞,就移动index继续循环到数组结尾。

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
scss复制代码class Solution {
public int threeSumClosest(int[] nums, int target) {
// 先排序
Arrays.sort(nums);
// 总和默认先取前三个数
int total = nums[0] + nums[1] + nums[2];
// 二指针循环遍历
int left;
int right;
for (int i = 0; i < nums.length - 1; i++) {
// 左右指针分别指向当前位置之后的两个边界 然后遍历指针相向移动 知道left>=right结束
left = i + 1;
right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
// 如果直接相等 那就是最接近的了
if (sum == target) {
return target;
}
// 保证total取最接近 target 的总和
if (Math.abs(sum - target) < Math.abs(total - target)) {
total = sum;
}
// 看下移动哪个指针
if (sum > target) {
// 数组有序的情况下 总和太大了移动右边指针
right--;
} else {
// 数组有序的情况下 总和太小了移动左边指针
left++;
}
}
}
return total;
}
}

本文转载自: 掘金

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

MySQL高级 - 查询和慢查询日志分析

发表于 2021-11-28

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

SQL性能下降的原因

在日常的运维过程中,经常会遇到DBA将一些执行效率较低的SQL发过来找开发人员分析,当我们拿到这个SQL语句之后,在对这些SQL进行分析之前,需要明确可能导致SQL执行性能下降的原因进行分析,执行性能下降可以体现在以下两个方面:

  • 等待时间长
1
text复制代码1.锁表导致查询一直处于等待状态,后续我们从MySQL锁的机制去分析SQL执行的原理
  • 执行时间长
1
2
3
4
text复制代码1.查询语句写的烂
2.索引失效
3.关联查询太多join
4.服务器调优及各个参数的设置

需要遵守的优化原则

查询优化是一个复杂的工程,涉及从硬件到参数配置、不同数据库的解析器、优化器实现、SQL 语句的执行顺序、索引以及统计信息的采集等等方面

下面给大家介绍几个编写SQL的关键原则,可以帮助我们编写出更加高效的SQL查询

  • 第一条:只返回需要的结果
    • 一定要为查询语句指定WHERE条件,过滤掉不需要的数据行
    • 避免使用select * from, 因为它表示查询表中的所有字段
  • 第二条:确保查询使用了正确的索引
    • 经常出现在WHERE条件中的字段建立索引,可以避免全表扫描
    • 将ORDER BY排序的字段加入到索引中,可以避免额外的排序操作
    • 多表连接查询的关联字段建立索引,可以提高连接查询的性能
    • 将GROUP BY分组操作字段加入到索引中,可以利用索引完成分组
  • 第三条:避免让索引失效
    • 在WHERE子句中对索引字段进行表达式运算或者使用函数都会导致索引失效
    • 使用LIKE匹配时,如果通配符出现在左侧无法使用索引
    • 如果WHERE条件中的字段上创建了索引,尽量设置为NOT NULL

SQL的执行顺序

  • 程序员编写的SQL

image.png

  • MySQL执行的SQL

image.png

  1. FORM子句:左右两个表的笛卡尔积
  2. ON:筛选满足条件的数据
  3. JOIN:如果是inner join那就正常,如果是outer join则会添加回来上面一步过滤掉的一些行
  4. WHERE:对不满足条件的行进行移除,并且不能恢复
  5. GROUP BY:分组后只能得到每组的第一行数据,或者聚合函数的数值
  6. HAVING:对分组后的数据进行筛选
  7. SELECT:执行select操作,获取需要的列
  8. DISTINCT:去重
  9. ORDER BY:排序
  10. LIMIT:取出指定行的记录,并将结果返回
  • 查看下面的SQL分析执行顺序
1
2
3
4
5
6
7
8
9
mysql复制代码select
id,
sex,
count(*) AS num
from
employee
where name is not null
group by sex
order by id
  • 上面的SQL执行执行顺序如下
  1. 首先执行 FROM 子句,从employee表组装数据源的数据
  2. 执行WHERE子句,筛选employee表中所有name不为NULL的数据
  3. 执行GROUP BY子句,按 “性别” 列进行分组
  4. 执行select操作,获取需要的列
  5. 最后执行order by,对最终的结果进行排序

JOIN查询的七种方式

  • 7种JOIN,可以分为四类:内连接 、左连接 、右连接、 全连接

image.png

JOIN查询SQL编写

创建表插入数据

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
mysql复制代码---部门表
DROP TABLE IF EXISTS `t_dept`;
CREATE TABLE `t_dept` (
`id` varchar(40) NOT NULL,
`name` varchar(40) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

---员工表
DROP TABLE IF EXISTS `t_emp`;
CREATE TABLE `t_emp` (
`id` varchar(40) NOT NULL,
`name` varchar(40) DEFAULT NULL,
`age` int(3) DEFAULT NULL,
`deptid` varchar(40) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `deptid` (`deptid`),
CONSTRAINT `deptid` FOREIGN KEY (`deptid`) REFERENCES `t_dept` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--插入部门数据
INSERT INTO `t_dept` VALUES ('1', '研发部');
INSERT INTO `t_dept` VALUES ('2', '人事部');
INSERT INTO `t_dept` VALUES ('3', '财务部');

--插入员工数据
INSERT INTO `t_emp` VALUES ('1', '赵四', 23, '1');
INSERT INTO `t_emp` VALUES ('2', '刘能', 25, '2');
INSERT INTO `t_emp` VALUES ('3', '广坤', 27, '1');
INSERT INTO `t_emp` VALUES ('4', '玉田', 43, NULL);

内连接

image.png

1
mysql复制代码SELECT * FROM t_emp e INNER JOIN t_dept d ON e.deptid = d.id

左连接

image.png

1
mysql复制代码SELECT * FROM t_emp e LEFT JOIN t_dept d ON e.deptid = d.id

左连接去重叠部分

image.png

1
2
mysql复制代码SELECT * FROM t_emp e LEFT JOIN t_dept d ON e.deptid = d.id
WHERE e.deptid IS NULL;

右连接

image.png

1
mysql复制代码SELECT * FROM t_emp e RIGHT JOIN t_dept d ON e.deptid = d.id

右连接去重叠部分

image.png

1
2
mysql复制代码SELECT * FROM t_emp e RIGHT JOIN t_dept d ON e.deptid = d.id
WHERE e.id IS NULL;

全连接

image.png

1
2
3
mysql复制代码SELECT * FROM t_emp e LEFT JOIN t_dept d ON e.deptid = d.id
UNION
SELECT * FROM t_emp e RIGHT JOIN t_dept d ON e.deptid = d.id

MySQL UNION操作符用于连接两个以上的SELECT语句的结果组合到一个结果集合中。多个SELECT语句会删除重复的数据。

各自独有

image.png

1
2
3
4
5
6
7
mysql复制代码SELECT * FROM t_emp e LEFT JOIN t_dept d ON e.deptid = d.id
WHERE e.deptid IS NULL

UNION

SELECT * FROM t_emp e RIGHT JOIN t_dept d ON e.deptid = d.id
WHERE e.id IS NULL

慢查询日志分析

慢查询介绍

MySQL的慢查询,全名是慢查询日志,是MySQL提供的一种日志记录,用来记录在MySQL中响应时间超过阈值的语句。

默认情况下,MySQL数据库并不启动慢查询日志,需要手动来设置这个参数。

如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。

慢查询日志支持将日志记录写入文件和数据库表。

慢查询参数

  1. 执行下面的语句
1
mysql复制代码SHOW VARIABLES LIKE "%query%" ;
  1. MySQL 慢查询的相关参数解释:
  • slow_query_log:是否开启慢查询日志, 1表示开启, 0表示关闭。
  • slow-query-log-fifile:新版(5.6及以上版本)MySQL数据库慢查询日志存储路径。
  • long_query_time: 慢查询阈值,当查询时间多于设定的阈值时,记录日志。

慢查询配置方式

  1. 默认情况下slow_query_log的值为OFF,表示慢查询日志是禁用的
1
2
3
4
5
6
7
mysql复制代码mysql> SHOW VARIABLES LIKE '%slow_query_log%';
+---------------------+-----------------------------------+
| Variable_name | Value |
+---------------------+-----------------------------------+
| slow_query_log | OFF |
| slow_query_log_file | /var/lib/mysql/localhost-slow.log |
+---------------------+-----------------------------------+
  1. 可以通过设置slow_query_log的值来开启
1
2
3
4
5
6
7
8
9
mysql复制代码mysql> set global slow_query_log=1;

mysql> SHOW VARIABLES LIKE '%slow_query_log%';
+---------------------+-----------------------------------+
| Variable_name | Value |
+---------------------+-----------------------------------+
| slow_query_log | ON |
| slow_query_log_file | /var/lib/mysql/localhost-slow.log |
+---------------------+-----------------------------------+
  1. 使用set global slow_query_log=1开启了慢查询日志只对当前数据库生效,MySQL重启后则会失效。如果要永久生效,就必须修改配置文件my.cnf(其它系统变量也是如此)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
lua复制代码-- 编辑配置
vim /etc/my.cnf

-- 添加如下内容
slow_query_log =1
slow_query_log_file=/var/lib/mysql/lagou-slow.log

-- 重启MySQL
service mysqld restart

mysql> SHOW VARIABLES LIKE '%slow_query_log%';
+---------------------+-------------------------------+
| Variable_name | Value |
+---------------------+-------------------------------+
| slow_query_log | ON |
| slow_query_log_file | /var/lib/mysql/lagou-slow.log |
+---------------------+-------------------------------+
  1. 那么开启了慢查询日志后,什么样的SQL才会记录到慢查询日志里面呢?这个是由参数long_query_time控制,默认情况下long_query_time的值为10秒
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql复制代码mysql> show variables like 'long_query_time';
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+

mysql> set global long_query_time=1;
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like 'long_query_time';
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+
  1. 我修改了变量long_query_time,但是查询变量long_query_time的值还是10,难道没有修改到呢?注意:使用命令set global long_query_time=1修改后,需要重新连接或新开一个会话才能看到修改值。
1
2
3
4
5
6
mysql复制代码mysql> show variables like 'long_query_time';
+-----------------+----------+
| Variable_name | Value |
+-----------------+----------+
| long_query_time | 1.000000 |
+-----------------+----------+
  1. log_output参数是指定日志的存储方式。log_output=’FILE’表示将日志存入文件,默认值是’FILE’。 log_output=’TABLE’表示将日志存入数据库,这样日志信息就会被写入到mysql.slow_log表中。
1
2
3
4
5
6
mysql复制代码mysql> SHOW VARIABLES LIKE '%log_output%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_output | FILE |
+---------------+-------+

MySQL数据库支持同时两种日志存储方式,配置的时候以逗号隔开即可,如:log_output=’FILE,TABLE’。日志记录到系统的专用日志表中,要比记录到文件耗费更多的系统资源,因此对于需要启用慢查询日志,又需要能够获得更高的系统性能,那么建议优先记录到文件

  1. 系统变量log-queries-not-using-indexes:未使用索引的查询也被记录到慢查询日志中(可选项)。如果调优的话,建议开启这个选项。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql复制代码mysql> show variables like 'log_queries_not_using_indexes';
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| log_queries_not_using_indexes | OFF |
+-------------------------------+-------+

mysql> set global log_queries_not_using_indexes=1;
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like 'log_queries_not_using_indexes';
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| log_queries_not_using_indexes | ON |
+-------------------------------+-------+
1 row in set (0.00 sec)

慢查询测试

  1. 执行test_index.sql脚本,导入测试表
  2. 执行下面的SQL,执行超时 (超过1秒) 我们去查看慢查询日志
1
2
mysql复制代码SELECT * FROM test_index WHERE
hobby = '20009951' OR hobby = '10009931' OR hobby = '30009931';
  1. 日志内容

我们得到慢查询日志后,最重要的一步就是去分析这个日志。我们先来看下慢日志里到底记录了哪些内容。

如下是慢日志里其中一条SQL的记录内容,可以看到有时间戳,用户,查询时长及具体的SQL等

1
2
3
4
5
6
7
mysql复制代码==> lagou-slow.log <==
# User@Host: root[root] @ [192.168.52.1] Id: 4
# Query_time: 1.681371 Lock_time: 0.000089 Rows_sent: 3 Rows_examined: 5000000
SET timestamp=1604307746;
select * from test_index where
hobby = '20009951' or hobby = '10009931' or hobby = '30009931' LIMIT 0, 1000;
# Time: 2020-11-02T09:02:26.052231Z

本文转载自: 掘金

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

Maven settingsxml 详解 概述 元素详解

发表于 2021-11-28

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

概述

作用

用来设置Maven参数的配置文件,在Maven中提供了一个settings.xml文件来定义Maven的全局配置信息。我们通过这个文件来定义本地仓库、远程仓库和联网使用的代理信息等配置。

文件位置

一般存在于两个位置:

  • 全局配置:Maven的安装目录的conf子目录下面(${M2_HOME}/conf/settings.xml)
  • 用户目录的的.m2子目录下面({user.home}/.m2/settings.xml)。当前用户的独享配置。

当我们使用一些工具时(IDEA),可以直接指定settings.xml文件的位置。

配置文件优先级

局部配置高于全局配置

配置优先级从高到低:pom.xml> user settings > global settings

如果这些文件同时存在,在应用配置时,会合并它们的内容,如果有重复的配置,优先级高的配置会覆盖优先级低的。

Maven依赖搜索顺序

当我们执行Maven命令时,maven开始按照以下顺序查找依赖库:

  • 步骤 1:在本地仓库搜索,如果找不到,执行步骤 2,找到了则执行其他操作
  • 步骤 2:在中央仓库搜索,如果找不到,并且有一个或多个远程仓库已经设置,则执行步骤 4,如果找到了则下载到本地仓库中引用。
  • 步骤 3:如果远程仓库没有被设置, 将简单的停滞处理并抛出错误(无法找到依赖的文件)。
  • 步骤 4:在一个或多个远程仓库中搜索依赖的文件, 如果找到则下载到本地仓库已被将来引用, 否则将停止处理并抛出错误(无法找到依赖的文件)。

元素详解

顶级元素

LocalRepository

作用:该值表示构建系统本地仓库的路径。

默认值:${user.home}/.m2/repository

1
xml复制代码<localRepository>${user.home}/.m2/repository</localRepository>

InteractiveMode

作用:表示maven是否需要和用户交互以获得输入。

默认值:true

1
xml复制代码<interactiveMode>true</interactiveMode>

offline

作用:表示maven是否需要在离线模式下运行。

默认值:false

当由于网络设置原因或者安全因素,构建服务器不能连接远程仓库的时候,该配置就十分有用。

1
xml复制代码<offline>false</offline>

pluginGroups

作用:当插件的组织id(groupId)没有显式提供时,供搜寻插件groupId的列表。

默认值:默认情况下,maven会自动把org.apache.maven.plugins和org.codehaus.mojo添加到pluginGroups下。

1
2
3
4
xml复制代码<pluginGroups>
<pluginGroup>com.your1.plugins</pluginGroup>
<pluginGroup>com.your2.plugins</pluginGroup>
</pluginGroups>

proxies

作用:用来配置不同的代理, 多代理 profiles 可以应对笔记本或移动设备的工作环境: 通过简单的设置 profile id 就可以很容易的更换整个代理配置

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
xml复制代码<!-- 可以配置多个 -->
<proxies>

<!-- 代理元素包含配置代理时需要的信息 -->
<proxy>

<!-- 代理的唯一定义符, 用来区分不同的代理元素 -->
<id>optional</id>
<!-- 该代理是否是激活的那个。true则激活代理。当我们声明了一组代理, 而某个时候只需要激活一个代理的时候, 该元素就可以派上用处 -->
<active>true</active>
<!-- 代理的协议 -->
<protocol>http</protocol>
<!-- 代理服务器认证的登录名 -->
<username>proxyuser</username>
<!-- 代理服务器认证登录密码 -->
<password>proxypass</password>
<!-- 代理的主机名 -->
<host>proxy.host.net</host>
<!-- 代理的端口 -->
<port>80</port>
<!-- 不该被代理的主机名列表。该列表的分隔符由代理服务器指定;例子中使用了竖线分隔符, 使用逗号分隔也很常见 -->
<nonProxyHosts>local.net|some.host.com</nonProxyHosts>
</proxy>

</proxies>

servers

作用:进行远程服务器访问时所需的授权配置信息。通过系统唯一的 server id 进行唯一关联

注意:您应该指定用户名/密码或私钥/密码,因为这些配对是一起使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码<servers>
<server>
<!-- 服务的唯一定义符, 用来区分不同的代理元素 -->
<id>deploymentRepo</id>
<!-- 鉴权用户名 -->
<username>repouser</username>
<!-- 鉴权密码 -->
<password>repopwd</password>
</server>

<server>
<id>siteServer</id>
<!-- 鉴权时的私钥位置 -->
<privateKey>/path/to/private/key</privateKey>
<!-- 鉴权时的私钥密码 -->
<passphrase>optional; leave empty if not used.</passphrase>
</server>

</servers>

mirrors

作用:用于替代指定远程仓库的镜像服务器配置,例如当您无法连接上国外的仓库是, 可以指定连接到国内的镜像服务器,同时还可以缓解镜像仓库的压力

注意:pom.xml 和 setting.xml 中配置的仓库和镜像优先级关系(``mirror优先级高于repository`)

1
复制代码repository(setting.xml) < repository(pom.xml) < mirror(setting.xml)

mirror匹配顺序:

  • 多个mirror按照id字母顺序进行排列,与编写顺序无关
  • 在第一个mirror找不到artifact,不会继续找下一个镜像
  • 只有当mirror无法链接的时候,才会尝试找下一个镜像,类似容灾备份
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
xml复制代码<mirrors>

<mirror>
<!--该镜像的唯一标识符。id用来区分不同的mirror元素。 -->
<id>mirrorId</id>
<!--用来表示该mirror是关联的哪一个仓库,其值为其关联仓库的id。 -->
<mirrorOf>repositoryId</mirrorOf>
<!-- 镜像名称, 无特殊作用, 可视为简述 -->
<name>Human Readable Name for this Mirror.</name>
<!-- 镜像地址 -->
<url>http://my.repository.com/repo/path</url>
</mirror>


<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>

mirrorOf配置语法:

  • *:匹配所有远程仓库。相当于一个拦截器,它会拦截远程仓库的相关请求,把请求里的远程仓库地址,重定向到mirror里配置的地址。
  • external:* : 匹配除 localhost、使用 file:// 协议外的所有远程仓库
  • repo1,repo2:匹配仓库 repo1 和 repo2
  • *,!repo1: 匹配所有远程仓库, repo1 除外

profiles

作用:构建方法的配置清单, maven 将根据不同环境参数来使用这些构建配置。

注意:settings.xml 中的 profile元素是 pom.xml中 profile元素的裁剪版本。

  • settings.xml负责的是整体的构建过程, pom.xml负责单独的项目对象构建过程。
  • settings.xml 只包含了id, activation, repositories, pluginRepositories 和 properties 元素。
  • 如果 settings中的 profile 被激活, 它的值会覆盖任何其它定义在 pom.xml中或 profile.xml中的相同 id 的 profile。

查看当前激活的 profile

1
cmd复制代码mvn help:active-profiles
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
xml复制代码<profiles>

<profile>
<!-- 该配置的唯一标识符 -->
<id>jdk-1.4</id>

<!--自动触发profile的条件逻辑。Activation是profile的开启钥匙。-->
<!--如POM中的profile一样,profile的力量来自于它能够在某些特定的环境中自动使用某些特定的值;这些环境通过activation元素指定。-->
<!--activation元素并不是激活profile的唯一方式。settings.xml文件中的activeProfile元素可以包含profile的id。-->
<!--profile也可以通过在命令行,使用-P标记和逗号分隔的列表来显式的激活(如,-P test)。 -->
<activation>
<!--profile默认是否激活的标识 -->
<activeByDefault>false</activeByDefault>
<!--activation有一个内建的java版本检测,如果检测到jdk版本与期待的一样,profile被激活。 -->
<jdk>1.4</jdk>
</activation>


<!--远程仓库列表,它是Maven用来填充构建系统本地仓库所使用的一组远程项目。 -->
<repositories>

<repository>
<!--远程仓库唯一标识 -->
<id>jdk14</id>
<!--远程仓库名称 -->
<name>Repository for JDK 1.4 builds</name>
<!--远程仓库URL,按protocol://hostname/path形式 -->
<url>http://www.myhost.com/maven/jdk14</url>
<!--用于定位和排序构件的仓库布局类型-可以是default(默认)或者legacy(遗留)。-->
<layout>default</layout>
<!-- 快照策略 -->
<snapshotPolicy>always</snapshotPolicy>
</repository>
</repositories>
</profile>



<profile>
<id>env-dev</id>
<!--如果Maven检测到某一个属性(其值可以在POM中通过${名称}引用),其拥有对应的名称和值,Profile就会被激活。-->
<!--如果值字段是空的,那么存在属性名称字段就会激活profile,否则按区分大小写方式匹配属性值字段 -->
<activation>
<property>
<name>target-env</name>
<value>dev</value>
</property>
</activation>

<properties>
<!-- 如果这个profile被激活,那么属性${tomcatPath}就可以被访问了 -->
<tomcatPath>/path/to/tomcat/instance</tomcatPath>
</properties>
</profile>

</profiles>

activeProfiles

作用:手动激活profiles的列表,按照profile被应用的顺序定义activeProfile

说明:

  • 任何在activeProfile中定义的profile id,不论环境设置如何,其对应的 profile都会被激活
  • 如果没有匹配的profile,则什么都不会发生。
  • 如果运行过程中找不到这样一个profile,Maven则会像往常一样运行。
1
2
3
4
xml复制代码<activeProfiles>
<activeProfile>alwaysActiveProfile</activeProfile>
<activeProfile>anotherAlwaysActiveProfile</activeProfile>
</activeProfiles>

特殊说明

私服说明

私服的配置推荐用profile配置而不是mirror

实际应用

实际应用中,经常使用的是<localRepository>、<servers>、<mirrors>、<profiles>有限几个节点,其他节点使用默认值足够应对大部分的应用场景。

本文转载自: 掘金

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

异步编程Future掌控未来

发表于 2021-11-28

Callable

有了Runnable,为什么还要Callable?

我们先来看下Callable的接口:

1
2
3
4
5
6
7
8
9
java复制代码public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}

第一点是不能返回值,对于 Runnable 而言,它不能返回一个返回值,虽然可以利用其他的一些办法,比如在 Runnable 方法中写入日志文件或者修改某个共享的对象的办法,来达到保存线程执行结果的目的,但这种解决问题的行为千曲百折,属于曲线救国,效率着实不高。

实际上,在很多情况下执行一个线程时,我们都希望能得到执行的任务的结果,也就是说,我们是需要得到返回值的,比如请求网络、查询数据库等。我们看接口中的V就代表返回值。

第二点是不能抛异常,我们看下Callable接口定义的时候throw了Exception,而 Runnable是没有的,Runnable只能这样写,在里面try catch掉:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码Runnable runnable = new Runnable() {
       /**
        *  run方法上无法声明 throws 异常,且run方法内无法 throw 出 checked Exception,除非使用try catch进行处理
        */
       @Override
       public void run() {
           try {
               throw new IOException();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
}

最后对比一下Runnable和Callable

  • 方法名,Callable 规定的执行方法是 call(),而 Runnable 规定的执行方法是 run();
  • 返回值,Callable 的任务执行后有返回值,而 Runnable 的任务执行后是没有返回值的;
  • 抛出异常,call() 方法可抛出异常,而 run() 方法是不能抛出受检查异常的;
  • 和 Callable 配合的有一个 Future 类,通过 Future 可以了解任务执行情况,或者取消任务的执行,还可获取任务执行的结果,这些功能都是 Runnable 做不到的,Callable 的功能要比 Runnable 强大。

Future

下面就来介绍一下Future,上面说到Callable是可以返回值的,那这个返回值怎么拿呢?就是通过 Future 类的 get 方法来获取 。

因此,Future 就相当于一个存储器,它存储了 Callable 的 call 方法的任务结果。除此之外,我们还可以通过 Future 的 isDone 方法来判断任务是否已经执行完毕了,还可以通过 cancel 方法取消这个任务,或限时获取任务的结果等,总之 Future 的功能比较丰富。

如何创建Future

一种是通过线程池,之前在讲线程池的时候也提到过, 《线程池源码精讲》

1
2
3
4
java复制代码   ExecutorService service = Executors.newFixedThreadPool(10);
   Future<Integer> future = service.submit(new CallableTask());
//阻塞获得结果
Integer rs = future.get();

还有一种是通过FutureTask创建

1
2
3
4
5
java复制代码     FutureTask<Integer> integerFutureTask = new FutureTask<>(new CallableTask());
//启动线程
     new Thread(integerFutureTask).start();
//阻塞获得结果
Integer rs=integerFutureTask.get();

有了宏观上的认识,我们来看下Future里面的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)

        throws InterruptedException, ExecutionException, TimeoutException;

}

get() 方法

  1. 最常见的就是当执行 get 的时候,任务已经执行完毕了,可以立刻返回,获取到任务执行的结果。
  2. 任务还没有结果,如果任务还没开始或在进行中,我们去调用 get 的时候,都会把当前的线程阻塞,直到任务完成再把结果返回回来。
  3. 任务执行过程中抛出异常,一旦这样,我们再去调用 get 的时候,就会抛出 ExecutionException 异常,不管我们执行 call 方法时里面抛出的异常类型是什么,在执行 get 方法时所获得的异常都是 ExecutionException。
  4. 任务被取消了,如果任务被取消,我们用 get 方法去获取结果时则会抛出 CancellationException。
  5. 任务超时, get 方法有一个重载方法,就是带延迟参数的,调用了这个带延迟参数的 get 方法之后,如果 call 方法在规定时间内正常顺利完成了任务,那么 get 会正常返回;但是如果到达了指定时间依然没有完成任务,get 方法则会抛出 TimeoutException,代表超时了。

isDone() 方法

该方法是用来判断当前这个任务是否执行完毕了。需要注意的是,这个方法如果返回 true,则代表执行完成了,如果返回 false 则代表还没完成。

但这里如果返回 true,并不代表这个任务是成功执行的,比如说任务执行到一半抛出了异常。那么在这种情况下,对于这个 isDone 方法而言,它其实也是会返回 true 的,因为对它来说,虽然有异常发生了,但是这个任务在未来也不会再被执行,它确实已经执行完毕了。所以 isDone 方法在返回 true 的时候,不代表这个任务是成功执行的,只代表它执行完毕了。

cancel()方法

  1. 当任务还没有开始执行时,一旦调用 cancel,这个任务就会被正常取消,未来也不会被执行,那么 cancel 方法返回 true。
  2. 如果任务已经完成,或者之前已经被取消过了,那么执行 cancel 方法则代表取消失败,返回 false。因为任务无论是已完成还是已经被取消过了,都不能再被取消了。
  3. 当这个任务正在执行,这个时候执行 cancel 方法是不会直接取消这个任务的,而是会根据我们传入的参数做判断。cancel 方法是必须传入一个参数,该参数叫作 mayInterruptIfRunning,它是什么含义呢?如果传入的参数是 true,执行任务的线程就会收到一个中断的信号,正在执行的任务可能会有一些处理中断的逻辑,进而停止,这个比较好理解。如果传入的是 false 则代表不中断正在运行的任务,也就是说,本次 cancel 不会有任何效果,同时 cancel 方法会返回 false。

isCancelled() 方法

判断是否被取消,它和 cancel 方法配合使用,比较简单。

下面看下FutureTask的类图:
image.png

我们看了上面的代码其实也能猜到,既然 futureTask 能丢到 Thread 类里面去执行,那它肯定继承了Runnable接口,实现了run方法;既然能够调用get()方法,肯定是继承了Future接口,与上面的类图吻合。
image.png

我们看下源码,看下run()方法,很简单,里面执行的逻辑就是Callable里面的call方法,最终将计算出来的结果保存到outcome里面去,然后唤醒阻塞的线程。
image.png

看下get()方法,很简单,如果任务结束完成了,直接把outcome里的值返回,否则加入到阻塞队列,类似于AQS。《ReentrantLock介绍及AQS源码精讲》
image.png
最后看下流程图:

image.png

CompletableFuture

上面介绍了Future/Callable的使用和原理,下面介绍下CompletableFuture。

CompletableFuture对Future做了改进,主要是在get()方法上,主线程如果需要依赖该任务执行结果继续后续操作时,不再需要等待,而是可以直接传入一个回调对象,当异步任务执行完成后,自动调用该回调对象,相当于实现了异步回调通知功能。

除此之外,CompletableFuture还提供了非常强大的功能,比如对于回调对象的执行,可以放到非任务线程中执行,也能用任务线程执行;提供了函数式编程能力,简化了异步编程的复杂性;提供了多个CompletableFuture的组合与转化功能。

看下类图,实现了CompletionStage和Future接口。
image.png
Future就是上面讲的Future,里面有5个方法。CompletionStage表示任务执行的一个阶段,每个异步任务都会返回一个新的CompletionStage对象,我们可以针对多个CompletionStage对象进行串行、并行或者聚合的方式来进行后续下一阶段的操作,简单来说,就是实现异步任务执行后的自动回调功能。

CompletableFuture的构建

CompletableFuture提供了四个静态方法来构建一个异步事件,方法如下。

  1. supplyAsync(Supplier supplier):带有返回值的异步执行方法,传入一个函数式接口,返回一个新的CompletableFuture对象。默认使用ForkJoinPool.commonPool()作为线程池执行异步任务。
  2. supplyAsync(Supplier supplier,Executor executor):带有返回值的异步执行方法,多了一个Executor参数,表示使用自定义线程池来执行任务。
  3. runAsync(Runnable runnable):不带返回值的异步执行方法,传入一个Runnable,返回一个新的CompletableFuture对象。默认使用ForkjoinPool.commonPool()作为线程池执行异步任务。
  4. runAsync(Runnable runnable,Executor executor):不带返回值的异步执行方法, 多了一个Executor参数,表示使用自定义线程池来执行任务。

下面看下CompletableFuture的简单用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture cf1 = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName() + ":异步执行一个任务");
});
//通过阻塞获取执行结果
System.out.println(cf1.get());


CompletableFuture cf2 = CompletableFuture.supplyAsync(() -> "Hello World").thenAccept(result -> {
System.out.println(result);
});
//继续做其他事情
//...
}

cf1就是执行一个任务,用的是默认ForkJoinPool的线程池,不带返回值,cf1.get()是阻塞获取值,因为不带返回值,所以获取的是null。

cf2是执行一个带返回值的任务,里面就干一件事return hello world,此时主线程可以继续往下执行做其他事情,待任务执行完以后,thenAccept方法接收到返回的hello world,然后打印出来。

image.png

CompletableFuture方法介绍

我们可以看下CompletableFuture类里面有38个方法,十分的多,下面和大家分类介绍一下。
image.png

获取结果方法

CompletableFuture类实现了Future接口,所以它开始可以像Future那样主动通过阻塞或者轮询的方式来获得执行结果。

  • get(),基于阻塞的方式获取异步任务执行结果。
  • get (long timeout, TimeUnit unit),通过带有超时时间的阻塞方式获取异步执行结果。
  • join(),和 get() 方法的作用相同,唯一不同的点在于 get() 方法允许被中断,也就是会抛出InterruptedException ,但是join()不允许被中断。
  • getNow(T valueIfAbsent),这个方法有点特殊,如果当前任务已经执行完成,则返回执行结果,否则返回传递进去的参数 valueIfAbsent 。

在CompletableFuture类中还有一个比较有意思的方法 complete(T value) ,它表示完成完成计算,
也就是把 value 设置为CompletableFuture的返回值并且唤醒在上述方法阻塞的线程。

我们看下下面的例子,就是创建两个线程t1和t2,线程里面通过completableFuture.get()方法阻塞,当我们调用cf.complete(“Finish”)方法的时候,相当于往里面赋值了,get()方法取到值了,才能继续往下走。

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
java复制代码public class CompleteExample {

static class ClientThread implements Runnable {

private CompletableFuture completableFuture;

public ClientThread(CompletableFuture completableFuture) {
this.completableFuture = completableFuture;
}

@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + ":" +
completableFuture.get()); //阻塞
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}

}

public static void main(String[] args) {
CompletableFuture cf = new CompletableFuture();
new Thread(new ClientThread(cf), "t1").start();
new Thread(new ClientThread(cf), "t2").start();
//执行某段逻辑
cf.complete("Finish");
//exception
//cf.completeExceptionally(e);
}
}

image.png

纯消费类型的方法

纯消费类型的方法,指依赖上一个异步任务的结果作为当前函数的参数进行下一步计算,它的特点是不返回新的计算值,这类的方法都包含 Accept 这个关键字。在CompletionStage中包含9个Accept关键字的方法,这9个方法又可以分为三类:依赖单个CompletionStage任务完成,依赖两个CompletionStage任务都完成,依赖两个CompletionStage中的任何一个完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码//当前线程同步执行
public CompletionStage<Void> thenAccept(Consumer<? super T> action);
//使用ForkJoinPool.commonPool线程池执行action
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action);
//使用自定义线程池执行action
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T>
action,Executor executor);
public <U> CompletionStage<Void> thenAcceptBoth(CompletionStage<? extends U>
other,BiConsumer<? super T, ? super U> action);
public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<?
extends U> other,BiConsumer<? super T, ? super U> action);
public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<?
extends U> other,BiConsumer<? super T, ? super U> action,Executor executor);
public CompletionStage<Void> acceptEither(CompletionStage<? extends T>
other,Consumer<? super T> action);
public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T>
other,Consumer<? super T> action);
public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T>
other,Consumer<? super T> action,Executor executor);

thenAccept上面演示过了,下面演示下thenAcceptBoth() 方法,当task1和task2都返回值以后,然后再一起打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class AcceptExample {

public static void main(String[] args) throws ExecutionException, InterruptedException {

CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "AcceptBot");
CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Message");

task1.thenAcceptBoth(task2, (r1, r2) -> {
System.out.println("result: " + r1 + r2);
});
}

}

image.png

有返回值类型的方法

有返回值类型的方法,就是用上一个异步任务的执行结果进行下一步计算,并且会产生一个新的有返回值的CompletionStage对象。

在CompletionStage中,定义了9个带有返回结果的方法,同样也可以分为三个类型:依赖单个CompletionStage任务完成,依赖两个CompletionStage任务都完成,依赖两个CompletionStage中的任何一个完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public <U> CompletionStage<U> thenApply(Function<? super T,? extends U> fn);
public <U> CompletionStage<U> thenApplyAsync(Function<? super T,? extends U>
fn);
public <U> CompletionStage<U> thenApplyAsync(Function<? super T,? extends U>
fn,Executor executor);
public <U,V> CompletionStage<V> thenCombine(CompletionStage<? extends U>
other,BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends
U> other,BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends
U> other,BiFunction<? super T,? super U,? extends V> fn,Executor executor);
public <U> CompletionStage<U> applyToEither(CompletionStage<? extends T>
other,Function<? super T, U> fn);
public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends
T> other,Function<? super T, U> fn);
public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends
T> other,Function<? super T, U> fn,Executor executor);

thenApply() 方法

这新建一个任务return hello,thenApply在拿到值以后再和world拼接,然后再返回值,然后通过get获取到值。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class ApplyExample {

public static void main(String[] args) throws ExecutionException, InterruptedException {

CompletableFuture cf = CompletableFuture.supplyAsync(() -> "hello ").thenApply(r -> {
return r + "world";
});
System.out.println(cf.get());

}

}

image.png
thenCombineAsync() 方法

thenCombineAsync的作用就是将task1和task2的值都拿到以后返回值。

1
2
3
4
5
6
7
8
9
10
java复制代码public class CombineDemo {

public static void main(String[] args) throws ExecutionException, InterruptedException {

CompletableFuture cf = CompletableFuture.supplyAsync(() -> "Combine")
.thenCombineAsync(CompletableFuture.supplyAsync(() -> "Message"), (r1, r2) -> r1 + r2);
System.out.println(cf.get());
}

}

image.png

不消费也不返回的方法

也是9个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public CompletionStage<Void> thenRun(Runnable action);
public CompletionStage<Void> thenRunAsync(Runnable action);
public CompletionStage<Void> thenRunAsync(Runnable action,Executor
executor);
public CompletionStage<Void> runAfterBoth(CompletionStage<?> other,Runnable
action);
public CompletionStage<Void> runAfterBothAsync(CompletionStage<?>
other,Runnable action);
public CompletionStage<Void> runAfterBothAsync(CompletionStage<?>
other,Runnable action,Executor executor);
public CompletionStage<Void> runAfterEither(CompletionStage<?>
other,Runnable action);
public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?>
other,Runnable action);
public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?>
other,Runnable action,Executor executor);

这里新建两个任务,一个是return Both,一个是return Message,在都执行结束以后,因为run是不消费也不返回的,所以入参为0,不需要你们的参数,也不返回,所以没有return。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class RunExample {

public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture cxf = CompletableFuture.supplyAsync(() -> "Both")
.runAfterBoth(CompletableFuture.supplyAsync(() -> "Message"), () -> {
System.out.println("Done");
});
System.out.println(cxf.get());
}

}

image.png

多任务组合

1
2
3
4
5
6
java复制代码public <U> CompletionStage<U> thenCompose(Function<? super T, ? extends
CompletionStage<U>> fn);
public <U> CompletionStage<U> thenComposeAsync(Function<? super T, ? extends
CompletionStage<U>> fn);
public <U> CompletionStage<U> thenComposeAsync(Function<? super T, ? extends
CompletionStage<U>> fn,Executor executor);

异常处理

异常处理一共三个方法

  • whenComplete:表示当任务执行完成后,会触发的方法,它的特点是,不论前置的CompletionStage任务是正常执行结束还是出现异常,都能够触发特定的 action 方法。
  • handle:表示前置任务执行完成后,不管前置任务执行状态是正常还是异常,都会执行handle中的fn 函数,它和whenComplete的作用几乎一致,不同点在于,handle是一个有返回值类型的方法。
  • exceptionally:接受一个 fn 函数,当上一个CompletionStage出现异常时,会把该异常作为参数传递到 fn 函数。

这里写在一个例子里面,具体用哪种类型,小伙伴按照具体场景具体选取。
image.png
最后CompletableFuture里面的方法十分的多,本文介绍了几个,抛砖引玉,更多的是小伙伴在实际开发过程中慢慢的用,熟能生巧,有些方法缺少应用场景也很难举出例子来,以及这些方法里面传的参数都是函数式接口,java8新特性lambda表达式,这个也是需要学会的,否则会看不懂。感谢收看~

本文转载自: 掘金

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

力扣刷题笔记 → 5939 半径为 k 的子数组平均值

发表于 2021-11-28

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

题目

给你一个下标从 0 开始的数组 nums ,数组中有 n 个整数,另给你一个整数 k 。

半径为 k 的子数组平均值 是指:nums 中一个以下标 i 为 中心 且 半径 为 k 的子数组中所有元素的平均值,即下标在 i - k 和 i + k 范围(含 i - k 和 i + k)内所有元素的平均值。如果在下标 i 前或后不足 k 个元素,那么 半径为 k 的子数组平均值 是 -1 。

构建并返回一个长度为 n 的数组 avgs,其中 avgs[i] 是以下标 i 为中心的子数组的 半径为 k 的子数组平均值 。

x 个元素的 平均值 是 x 个元素相加之和除以 x ,此时使用截断式 整数除法 ,即需要去掉结果的小数部分。

  • 例如,四个元素 2、3、1 和 5 的平均值是 (2 + 3 + 1 + 5) / 4 = 11 / 4 = 3.75,截断后得到 3 。

示例

image.png

1
2
3
4
5
6
7
8
9
ini复制代码输入: nums = [7,4,3,9,1,8,5,2,6], k = 3
输出: [-1,-1,-1,5,4,4,-1,-1,-1]
解释:
- avg[0]、avg[1] 和 avg[2] 是 -1 ,因为在这几个下标前的元素数量都不足 k 个。
- 中心为下标 3 且半径为 3 的子数组的元素总和是:7 + 4 + 3 + 9 + 1 + 8 + 5 = 37 。
使用截断式 整数除法,avg[3] = 37 / 7 = 5 。
- 中心为下标 4 的子数组,avg[4] = (4 + 3 + 9 + 1 + 8 + 5 + 2) / 7 = 4 。
- 中心为下标 5 的子数组,avg[5] = (3 + 9 + 1 + 8 + 5 + 2 + 6) / 7 = 4 。
- avg[6]、avg[7] 和 avg[8] 是 -1 ,因为在这几个下标后的元素数量都不足 k 个。
1
2
3
4
5
ini复制代码输入: nums = [100000], k = 0
输出: [100000]
解释:
- 中心为下标 0 且半径 0 的子数组的元素总和是:100000 。
avg[0] = 100000 / 1 = 100000 。
1
2
3
4
ini复制代码输入: nums = [8], k = 100000
输出: [-1]
解释:
- avg[0] 是 -1 ,因为在下标 0 前后的元素数量均不足 k 。

提示

  • n == nums.length
  • 1 <= n <= 10^5
  • 0 <= nums[i], k <= 10^5

解题思路

滑动窗口

要计算一段长度为k的子数组平均值,滑动窗口无疑是一个很好的方式,固定窗口长度k * 2 + 1,统计出区间总和,再求得平均值avg之后填入到中心位置对应索引处即可。然后向右滑动,增加右边元素,减去左边元素,继续重复前面的求和取平均值步骤。

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
java复制代码class Solution {
public int[] getAverages(int[] nums, int k) {
// 边界判断
if(k == 0){
return nums;
}
int n = nums.length;
int[] ans = new int[n];
Arrays.fill(ans, -1);
if(n < k * 2 + 1){
return ans;
}

// 由于n的取值范围比较大,这里需要采用long类型来统计区间和
long sum = 0;
int left = 0, right = 0;
// 初始化窗口区间和,这里要预留最右边一位数
while(right - left < k * 2){
sum += nums[right++];
}

while(right < n){
// 加上最右边的元素
sum += nums[right++];
// 求平均值,将其赋值给中间点
ans[left + k] = (int)(sum / (k * 2 + 1));
// 减去最左边的元素
sum -= nums[left++];
}

return ans;
}
}

复杂度分析

  • 时间复杂度:O(N)O(N)O(N)
  • 空间复杂度:O(N)O(N)O(N)

最后

文章有写的不好的地方,请大佬们不吝赐教,错误是最能让人成长的,愿我与大佬间的距离逐渐缩短!

如果觉得文章对你有帮助,请 点赞、收藏、关注、评论 一键四连支持,你的支持就是我创作最大的动力!!!

题目出处:

本文转载自: 掘金

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

【Spring源码】-Spring中的循环依赖

发表于 2021-11-28

什么是循环依赖?

很简单,就是A对象依赖了B对象,B对象依赖了A对象。

比如:

1
2
3
4
5
6
7
8
9
java复制代码// A依赖了B
class A{
public B b;
}

// B依赖了A
class B{
public A a;
}

那么循环依赖是个问题吗?

如果不考虑Spring,循环依赖并不是问题,因为对象之间相互依赖是很正常的事情。

比如

1
2
3
4
5
java复制代码A a = new A();
B b = new B();

a.b = b;
b.a = a;

这样,A,B就依赖上了。

但是,在Spring中循环依赖就是一个问题了,为什么?
因为,在Spring中,一个对象并不是简单new出来了,而是会经过一系列的Bean的生命周期,就是因为Bean的生命周期所以才会出现循环依赖问题。当然,在Spring中,出现循环依赖的场景很多,有的场景Spring自动帮我们解决了,而有的场景则需要程序员来解决,下文详细来说。

要明白Spring中的循环依赖,得先明白Spring中Bean的生命周期。

Bean的生命周期

这里不会对Bean的生命周期进行详细的描述,只描述一下大概的过程。

Bean的生命周期指的就是:在Spring中,Bean是如何生成的?

被Spring管理的对象叫做Bean。Bean的生成步骤如下:

  1. Spring扫描class得到BeanDefinition
  2. 根据得到的BeanDefinition去生成bean
  3. 首先根据class推断构造方法
  4. 根据推断出来的构造方法,反射,得到一个对象(暂时叫做原始对象)
  5. 填充原始对象中的属性(依赖注入)
  6. 如果原始对象中的某个方法被AOP了,那么则需要根据原始对象生成一个代理对象
  7. 把最终生成的代理对象放入单例池(源码中叫做singletonObjects)中,下次getBean时就直接从单例池拿即可

可以看到,对于Spring中的Bean的生成过程,步骤还是很多的,并且不仅仅只有上面的7步,还有很多很多,比如Aware回调、初始化等等,这里不详细讨论。

可以发现,在Spring中,构造一个Bean,包括了new这个步骤(第4步构造方法反射)。

得到一个原始对象后,Spring需要给对象中的属性进行依赖注入,那么这个注入过程是怎样的?

比如上文说的A类,A类中存在一个B类的b属性,所以,当A类生成了一个原始对象之后,就会去给b属性去赋值,此时就会根据b属性的类型和属性名去BeanFactory中去获取B类所对应的单例bean。如果此时BeanFactory中存在B对应的Bean,那么直接拿来赋值给b属性;如果此时BeanFactory中不存在B对应的Bean,则需要生成一个B对应的Bean,然后赋值给b属性。

问题就出现在第二种情况,如果此时B类在BeanFactory中还没有生成对应的Bean,那么就需要去生成,就会经过B的Bean的生命周期。

那么在创建B类的Bean的过程中,如果B类中存在一个A类的a属性,那么在创建B的Bean的过程中就需要A类对应的Bean,但是,触发B类Bean的创建的条件是A类Bean在创建过程中的依赖注入,所以这里就出现了循环依赖:

ABean创建–>依赖了B属性–>触发BBean创建—>B依赖了A属性—>需要ABean(但ABean还在创建过程中)

从而导致ABean创建不出来,BBean也创建不出来。

这是循环依赖的场景,但是上文说了,在Spring中,通过某些机制帮开发者解决了部分循环依赖的问题,这个机制就是三级缓存。

三级缓存

三级缓存是通用的叫法。

一级缓存为:singletonObjects

二级缓存为:earlySingletonObjects

三级缓存为:singletonFactories

先稍微解释一下这三个缓存的作用,后面详细分析:

  • singletonObjects中缓存的是已经经历了完整生命周期的bean对象。
  • earlySingletonObjects比singletonObjects多了一个early,表示缓存的是早期的bean对象。早期是什么意思?表示Bean的生命周期还没走完就把这个Bean放入了earlySingletonObjects。
  • singletonFactories中缓存的是ObjectFactory,表示对象工厂,表示用来创建早期bean对象的工厂。

解决循环依赖思路分析

先来分析为什么缓存能解决循环依赖。

上文分析得到,之所以产生循环依赖的问题,主要是:

A创建时—>需要B—->B去创建—>需要A,从而产生了循环

image.png

那么如何打破这个循环,加个中间人(缓存)
image.png

A的Bean在创建过程中,在进行依赖注入之前,先把A的原始Bean放入缓存(提早暴露,只要放到缓存了,其他Bean需要时就可以从缓存中拿了),放入缓存后,再进行依赖注入,此时A的Bean依赖了B的Bean,如果B的Bean不存在,则需要创建B的Bean,而创建B的Bean的过程和A一样,也是先创建一个B的原始对象,然后把B的原始对象提早暴露出来放入缓存中,然后在对B的原始对象进行依赖注入A,此时能从缓存中拿到A的原始对象(虽然是A的原始对象,还不是最终的Bean),B的原始对象依赖注入完了之后,B的生命周期结束,那么A的生命周期也能结束。

因为整个过程中,都只有一个A原始对象,所以对于B而言,就算在属性注入时,注入的是A原始对象,也没有关系,因为A原始对象在后续的生命周期中在堆中没有发生变化。

从上面这个分析过程中可以得出,只需要一个缓存就能解决循环依赖了,那么为什么Spring中还需要singletonFactories呢?

这是难点,基于上面的场景想一个问题:如果A的原始对象注入给B的属性之后,A的原始对象进行了AOP产生了一个代理对象,此时就会出现,对于A而言,它的Bean对象其实应该是AOP之后的代理对象,而B的a属性对应的并不是AOP之后的代理对象,这就产生了冲突。

B依赖的A和最终的A不是同一个对象。

AOP就是通过一个BeanPostProcessor来实现的,这个BeanPostProcessor就是AnnotationAwareAspectJAutoProxyCreator,它的父类是AbstractAutoProxyCreator,而在Spring中AOP利用的要么是JDK动态代理,要么CGLib的动态代理,所以如果给一个类中的某个方法设置了切面,那么这个类最终就需要生成一个代理对象。

一般过程就是:A类—>生成一个普通对象–>属性注入–>基于切面生成一个代理对象–>把代理对象放入singletonObjects单例池中。

而AOP可以说是Spring中除开IOC的另外一大功能,而循环依赖又是属于IOC范畴的,所以这两大功能想要并存,Spring需要特殊处理。

如何处理的,就是利用了第三级缓存singletonFactories。

首先,singletonFactories中存的是某个beanName对应的ObjectFactory,在bean的生命周期中,生成完原始对象之后,就会构造一个ObjectFactory存入singletonFactories中。这个ObjectFactory是一个函数式接口,所以支持Lambda表达式:() -> getEarlyBeanReference(beanName, mbd, bean)

上面的Lambda表达式就是一个ObjectFactory,执行该Lambda表达式就会去执行getEarlyBeanReference方法,而该方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}

该方法会去执行SmartInstantiationAwareBeanPostProcessor中的getEarlyBeanReference方法,而这个接口下的实现类中只有两个类实现了这个方法,一个是AbstractAutoProxyCreator,一个是InstantiationAwareBeanPostProcessorAdapter,它的实现如下:

1
2
3
4
5
java复制代码// InstantiationAwareBeanPostProcessorAdapter
@Override
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
return bean;
}
1
2
3
4
5
6
7
java复制代码// AbstractAutoProxyCreator
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.put(cacheKey, bean);
return wrapIfNecessary(bean, beanName, cacheKey);
}

在整个Spring中,默认就只有AbstractAutoProxyCreator真正意义上实现了getEarlyBeanReference方法,而该类就是用来进行AOP的。上文提到的AnnotationAwareAspectJAutoProxyCreator的父类就是AbstractAutoProxyCreator。

那么getEarlyBeanReference方法到底在干什么?
首先得到一个cachekey,cachekey就是beanName。
然后把beanName和bean(这是原始对象)存入earlyProxyReferences中
调用wrapIfNecessary进行AOP,得到一个代理对象。

那么,什么时候会调用getEarlyBeanReference方法呢?回到循环依赖的场景中

image.png

左边文字:
这个ObjectFactory就是上文说的labmda表达式,中间有getEarlyBeanReference方法,注意存入singletonFactories时并不会执行lambda表达式,也就是不会执行getEarlyBeanReference方法

右边文字:
从singletonFactories根据beanName得到一个ObjectFactory,然后执行ObjectFactory,也就是执行getEarlyBeanReference方法,此时会得到一个A原始对象经过AOP之后的代理对象,然后把该代理对象放入earlySingletonObjects中,注意此时并没有把代理对象放入singletonObjects中,那什么时候放入到singletonObjects中呢?

我们这个时候得来理解一下earlySingletonObjects的作用,此时,我们只得到了A原始对象的代理对象,这个对象还不完整,因为A原始对象还没有进行属性填充,所以此时不能直接把A的代理对象放入singletonObjects中,所以只能把代理对象放入earlySingletonObjects,假设现在有其他对象依赖了A,那么则可以从earlySingletonObjects中得到A原始对象的代理对象了,并且是A的同一个代理对象。

当B创建完了之后,A继续进行生命周期,而A在完成属性注入后,会按照它本身的逻辑去进行AOP,而此时我们知道A原始对象已经经历过了AOP,所以对于A本身而言,不会再去进行AOP了,那么怎么判断一个对象是否经历过了AOP呢?会利用上文提到的earlyProxyReferences,在AbstractAutoProxyCreator的postProcessAfterInitialization方法中,会去判断当前beanName是否在earlyProxyReferences,如果在则表示已经提前进行过AOP了,无需再次进行AOP。

对于A而言,进行了AOP的判断后,以及BeanPostProcessor的执行之后,就需要把A对应的对象放入singletonObjects中了,但是我们知道,应该是要把A的代理对象放入singletonObjects中,所以此时需要从earlySingletonObjects中得到代理对象,然后入singletonObjects中。

整个循环依赖解决完毕。

总结

至此,总结一下三级缓存:

  1. singletonObjects:缓存经过了完整生命周期的bean
  2. earlySingletonObjects:缓存未经过完整生命周期的bean,如果某个bean出现了循环依赖,就会提前把这个暂时未经过完整生命周期的bean放入earlySingletonObjects中,这个bean如果要经过AOP,那么就会把代理对象放入earlySingletonObjects中,否则就是把原始对象放入earlySingletonObjects,但是不管怎么样,就是是代理对象,代理对象所代理的原始对象也是没有经过完整生命周期的,所以放入earlySingletonObjects我们就可以统一认为是未经过完整生命周期的bean。
  3. singletonFactories:缓存的是一个ObjectFactory,也就是一个Lambda表达式。在每个Bean的生成过程中,经过实例化得到一个原始对象后,都会提前基于原始对象暴露一个Lambda表达式,并保存到三级缓存中,这个Lambda表达式可能用到,也可能用不到,如果当前Bean没有出现循环依赖,那么这个Lambda表达式没用,当前bean按照自己的生命周期正常执行,执行完后直接把当前bean放入singletonObjects中,如果当前bean在依赖注入时发现出现了循环依赖(当前正在创建的bean被其他bean依赖了),则从三级缓存中拿到Lambda表达式,并执行Lambda表达式得到一个对象,并把得到的对象放入二级缓存((如果当前Bean需要AOP,那么执行lambda表达式,得到就是对应的代理对象,如果无需AOP,则直接得到一个原始对象))。
  4. 其实还要一个缓存,就是earlyProxyReferences,它用来记录某个原始对象是否进行过AOP了。

反向分析一下singletonFactories

为什么需要singletonFactories?假设没有singletonFactories,只有earlySingletonObjects,earlySingletonObjects是二级缓存,它内部存储的是为经过完整生命周期的bean对象,Spring原有的流程是出现了循环依赖的情况下:

  1. 先从singletonFactories中拿到lambda表达式,这里肯定是能拿到的,因为每个bean实例化之后,依赖注入之前,就会生成一个lambda表示放入singletonFactories中
  2. 执行lambda表达式,得到结果,将结果放入earlySingletonObjects中

那如果没有singletonFactories,该如何把原始对象或AOP之后的代理对象放入earlySingletonObjects中呢?何时放入呢?

首先,将原始对象或AOP之后的代理对象放入earlySingletonObjects中的有两种:

  1. 实例化之后,依赖注入之前:如果是这样,那么对于每个bean而言,都是在依赖注入之前会去进行AOP,这是不符合bean生命周期步骤的设计的。
  2. 真正发现某个bean出现了循环依赖时:按现在Spring源码的流程来说,就是getSingleton(String beanName, boolean allowEarlyReference)中,是在这个方法中判断出来了当前获取的这个bean在创建中,就表示获取的这个bean出现了循环依赖,那在这个方法中该如何拿到原始对象呢?更加重要的是,该如何拿到AOP之后的代理对象呢?难道在这个方法中去循环调用BeanPostProcessor的初始化后的方法吗?不是做不到,不太合适,代码太丑。最关键的是在这个方法中该如何拿到原始对象 还是得需要一个Map,预习把这个Bean实例化后的对象存在这个Map中,那这样的话还不如直接用第一种方案,但是第一种又直接打破了Bean生命周期的设计。

所以,我们可以发现,现在Spring所用的singletonFactories,为了调和不同的情况,在singletonFactories中存的是lambda表达式,这样的话,只有在出现了循环依赖的情况,才会执行lambda表达式,才会进行AOP,也就说只有在出现了循环依赖的情况下才会打破Bean生命周期的设计,如果一个Bean没有出现循环依赖,那么它还是遵守了Bean的生命周期的设计的。

本文转载自: 掘金

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

HashMap 中的 resize() 方法详解

发表于 2021-11-28

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

  1. 前言

我们平常使用HashMap 的时候 ,应该使用比较多的就是 put 和 get 方法,好学的小伙伴,点进去 put 方法的源码,会发现在某些情况下 它会调用 resize()。此外,我们还可以发现在HashMap 中其他地方 调用这个 resize()方法的 也不少。那我们就来讨论下这个方法主要是干啥的。

官方翻译:初始化或加倍表的大小。如果为空,则按照字段阈值中持有的初始容量目标分配。否则,因为我们使用的是2的幂展开,所以每个容器中的元素必须保持在相同的索引,或者在新表中以2的偏移量幂移动。

  1. 源码

源码还是比较长,我们还是一段一段的看
首先说一下,这些hashmap 自带的属性,和静态常量的含义我在前面 java HashMap 详解 – (默认常量和构造函数) 中已经讲过了,不懂的可以去看看。

2.1 第一部分

图片.png

  • 定义一个变量 oldTab,保存原先 hashMap 中的 table。
  • 定义变量 oldCap,保存 oldTab 的长度(如果 oldTab == null ,则返回0)
  • 定义变量 oldThr,保存 原先的 threshold
  • 定义变量 newCap(保存 扩容后的table 的长度);newThr(扩容后的 threshold 的值),先把他们两初始化为0
  • 当 oldCap > 0 时
+ 如果 oldCap >= MAXIMUM\_CAPACITY,则说明无法扩容了,则把 threshold置为 Integer.MAX\_VALUE;并返回 oldTab;
+ 或者当,newCap = oldCap*2 且 newCap < MAXIMUM\_CAPACITY(最大容量) && oldCap >= 默认初始化容量 16; newThr = oldThr << 1(即 newThr = 2* oldThr),即扩容两倍
  • 或者当 oldThr > 0 ,说明它初始化容量时设置过阈值,则直接把 oldThr 赋给 newCap 即可。
  • 否则 则按默认的初始化规则,newCap = 16; newThr = 0.75*16 (进到这里则说明,原先的map,初始化时没有指定 initialCapactity 大小,所以没有给 threshold 赋值,即它为 0)
  • 当 newThr == 0时,则重新计算 newThr的值
+ 用 newCap \* 负载因子,得到 float 类型的变量 ft,当(newCap和ft均小于 MAXIMUM\_CAPACITY )时,把ft 赋给 newThr,否则把 Integer.MAX\_VALUE 赋给 newThr。
  • 处理完后,把 newThr 赋给 threshold.

2.2 第二部分

2021-11-26_172600.jpg

  • 定义一个新表 newTable,它的长度就是扩容后的长度。
  • 使当前table 指向这个newTable
  • 当旧表不为null (这里主要考虑数据复制)
    • 遍历旧表,且当该元素不为null时 进入if判断
    • 当 e.next == null ,说明当前这个节点就只要它一个元素,我们只需要把它重新分配位置即可,(位置的具体计算方式,元素的hash值 & 扩容后的table长度-1 ),因为前面说过newCap 是2的幂数,所以newCap-1 的二进制形式是(1,11,111,1111 …)这种形式的,与当前元素hash值去 &运算,最终结果就是 0—(newCap-1),刚好保证随机分配到hashMap的各个位置中去。
    • 或者,当该节点是一个 treeNode类型,则说明这个节点下的元素已经树华,会将树容器中的节点拆分为较低的树容器和较高的树容器,如果太小则取消树化,变成链表。具体可以看 java HashMap 详解 – (默认常量和构造函数) ,后续这个方法我在细讲。
    • 否则的话说明这个节点下面是一个链表。
    • lohead : 低位首节点, loTail : 低位尾节点
    • hiHead : 高位首节点, hiTail : 高位尾节点 , next : 保存下一节点。
    • 遍历链表,当 e = next不为null,一直遍历
      • 首先用next 保存 e.next
      • 这里 使用的是 : e.hash & oldTab的长度 (前面说过 oldTable 的长度 也是2的幂数,所以这里的二进制形式是 0,10,100,1000,10000….)当它的结果为0 时,则说明它的hash值小于老数组的长度,即他再次分配还是会在原先老数组的位置上(即 低位数组)。所以如果低位尾结点为null ,就把它赋给 低位头结点,否则就赋给低位尾结点的next。最后使得 低位尾结点 = e。
      • 这里 当 e.hash & oldTab的长度 不为0,则说明这个元素分配的位置必在,新数组的高位上,所以当高位尾结点为null,直接把该元素赋给高位头结点,否则赋给高位尾结点的next,最后把高位尾结点元素置为 e。
    • 当低位尾结点不为null,则把 它的next 置为null,把它的头结点直接放到,新数组索引为j的位置上。
    • 当高位尾结点不为null,也把它的 next 置为 null,因为它是高位,索引把它放到 新数组索引 j+oldCap的位置上
  • 最后返回这个新数组即可。

本文转载自: 掘金

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

学会使用表驱动编程模式后,领导了终于合了我的PR

发表于 2021-11-28

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

引子

刚入行时,对测试驱动开发(TDD)还没有任何概念, 写代码也时直接梭哈到底(Leetcode 刷题除外)。那会,领导那我写一个时间操作相关的工具方法并提到要写好测试用例,如获取指定时间当天的0点和23点59分59秒之类的。我心想这么简单,直接梭哈上手并写好测试用例,直接提了PR,没想到第一次PR就被打回了。

为啥呢?其中一个获取零点时间的测试用例我是这样子写的:

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
go复制代码//GetZeroTimeOfDay 获取目标时间的零点时间
func GetZeroTimeOfDay(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
}


func parseTime(timeStr string) (t time.Time, err error) {
t, err = time.Parse("2006-01-02 15:04:05", timeStr)
return
}

func Test_GetZeroTime_HardCode(t *testing.T) {
correctTime, err := parseTime("2021-11-28 00:00:00" )
assert.Empty(t, err)
t1, err := parseTime("2021-11-28 10:00:51")
assert.Empty(t, err)
assert.Equal(t, GetZeroTimeOfDay(t1), correctTime)
t2, err := parseTime("2021-11-28 12:00:51")
assert.Empty(t, err)
assert.Equal(t, GetZeroTimeOfDay(t2), correctTime)
t3, err := parseTime("2021-11-28 00:00:00")
assert.Empty(t, err)
assert.Equal(t, GetZeroTimeOfDay(t3), correctTime)
t4, err := parseTime("2021-11-28 23:59:59")
assert.Empty(t, err)
assert.Equal(t, GetZeroTimeOfDay(t4), correctTime)
}

相信读者们都看出问题,这不是硬编码测试吗?这明显的问题也让领导指出了,并让我去了解一下表驱动这一编程模式, 对测试用例进行改造。

表驱动

什么是表驱动?

表驱动模式是一种(scheme)–从表里查找信息而不使用逻辑语句(if 和 case)。

引用自《代码大全2》 第18章

在《代码大全2》中举了一个简单的例子来说明表驱动编程模式实现方式。

如果我们需要编写一个方法,用来获取每个月的天数, 硬编码梭哈的话就会产生如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码function GetDaysPerMonth(month) {
if (month == 1) {
return 31
} else if (month == 2) {
// 不考虑闰年
return 28
} else if (month == 3) {
return 31
} else if (month == 4) {
return 30
} else if (month == 5) {
return 31
}
....
}

使用表驱动编程模式的进行改成本质上就是使用一个哈希表/数组来存储条件和结果的映射关系。

改造后的代码如下所示, result数组的下标的含义是(月份数-1), 当然你也可以直接使用哈希表来存储月份和天书的映射关系。

1
2
3
4
js复制代码const result = [31, 28, 31, 30, 31...]
function GetDaysPerMonth(month) {
return result[month-1]
}

使用表驱动模式改造测试用例

根据表驱动编程模式的思想,我们需要对测试用例做如下改造:

  • 将所有需要测试数据集中保存到一个容器中哈希表或者数组皆可。
  • 设计一个测试骨架,读取测试数据并跑完所有定义好的测试用例

如以下代码所示, 我们定义好一个结构体, 其中ExpectTime表示正确的时间, TimeStr数组中保存我们需要测试的数据。

1
2
3
4
go复制代码type TestCase struct {
TimeStr []string
ExpectTime string
}

定义好测试用例的结构之后,我们来编写测试骨架,其主要逻辑就是读取表中的数据, 并调用目标方法将取得的运算结果和表中定义的正确结果进行比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码//Test_GetZeroTime_TableDriven  使用表驱动进行测试
func Test_GetZeroTime_TableDriven(t *testing.T) {
testCases := []TestCase{
{
TimeStr: []string{
"2021-11-28 10:00:51",
"2021-11-28 12:00:51",
"2021-11-28 00:00:00",
"2021-11-28 23:59:59",
},
ExpectTime: "2021-11-28 00:00:00",
},
}
for _, testCase := range testCases{
expectTime, err := parseTime(testCase.ExpectTime)
assert.Empty(t, err)
for _, timeStr := range testCase.TimeStr {
testTime, err := parseTime(timeStr)
assert.Empty(t, err)
result := GetZeroTimeOfDay(testTime)
assert.Equal(t, result, expectTime)
}
}
}

输出结果如下所示:
image.png

更进一步,从文件中读取测试数据

那么,还有进一步优化的空间吗?(这样子问,肯定是有)

虽然使用表驱动改造了测试用例, 但是要新增测试用例还是得在代码中进行修改, 那么如果我们将测试用例提取出来呢?然后直接读取测试用例文件得数据,之后再跑一遍测试用例,岂不美哉?

定义testdata.json文件用来存储测试用例得数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
json复制代码{
"ZeroTimeTestCase": [
{
"TimeStr": [
"2021-11-28 10:00:51",
"2021-11-28 12:00:51",
"2021-11-28 00:00:00",
"2021-11-28 23:59:59"
],
"ExpectTime": "2021-11-28 00:00:00"
},
{
"TimeStr": [
"2021-01-28 10:11:51",
"2021-01-28 12:00:51",
"2021-01-28 17:00:00",
"2021-01-28 23:59:57"
],
"ExpectTime": "2021-01-28 00:00:00"
}
]
}

改造后得测试用例代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码type TestData struct{
ZeroTimeTestCase []TestCase
}

func Test_GetZeroTime_ReadFile(t *testing.T) {
bytes, err := os.ReadFile("./testdata.json")
assert.Empty(t, err)
data := &TestData{}
err = json.Unmarshal(bytes, data)
assert.Empty(t, err)
for _, testCase := range data.ZeroTimeTestCase {
expectTime, err := parseTime(testCase.ExpectTime)
assert.Empty(t, err)
for _, timeStr := range testCase.TimeStr {
testTime, err := parseTime(timeStr)
assert.Empty(t, err)
result := GetZeroTimeOfDay(testTime)
assert.Equal(t, result, expectTime)
}
}
}

注意: 读取测试文件时需要使用相对路径

运行所有测试用例, 输出如下所示:

image.png

总结

  • 使用表驱动编程模式来设计测试用例可以大幅度提升你编写测试用例得速度,并且减少硬编码, 此方法也同样适用于业务逻辑开发中。
  • 多编写测试用例, 有助于代码健康
  • 多阅读《代码大全2》有好处,虽然名字起得不咋地,但干货真的多

完整代码 -> 在这儿

如果本文对你有所帮助,欢迎点赞,关注,收藏, 谢谢各位老铁。

Golang文章推荐

代码规范相关:

  • # 什么是圈复杂度?快来看大佬们是如何实现判断代码复杂度检测功能?
  • # Githook实践以代码规范检测插件golangci-lint为例
  • Gin系列*
  • # Gin 中间件的实现原理

本文转载自: 掘金

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

202111-28更文-leetcode152:乘积最大子数

发表于 2021-11-28

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

leetcode152:乘积最大子数组

前文

本文为本人在leetcode中解题的思路,并非代表最佳解决方案。

题目信息

给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

1
2
3
makefile复制代码输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。

解题思路分析

根据题目信息所述,我们这里有一个数组,同时可能存储正整数与负整数。由于该题目要求为计算连续乘积最大的子数组,由于存在正数与负数的两种情况,因此需要对正数负数分别进行记录。本题主要的解题方向是采取动态规划的思路,对于每个节点位置,均获取至今为止的连续数组与最小连续数组。通过mx与mn变量,也就是max和min变量,分别记录至今为止最后一个连续周期内的最大值与最小值。考虑到正负的角度,所以需要获取最小值乘以当前节点、最大值乘以当前节点与result中较大的值作至今为止的最大数组乘积结果。而上述的处理过程,也就是动态规划中对应的状态转移方程的内容。随着dp数组的遍历,即可得到每个位置的最大值。由于我们对于中间节点的数据没有要求,因此采用变量进行存储,优化该算法的空间复杂度。至此,该问题解决完毕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码public int maxProduct(int[] nums) {
if(nums.length == 0){
return 0;
}else if(nums.length == 1){
return nums[0];
}
int max = nums[0];
int min = nums[0];
int result = nums[0];
for (int i = 1; i < nums.length; i++) {
int mx = max;
int mn = min;
max = Math.max(mx * nums[i],Math.max(nums[i],mn * nums[i]));
min = Math.min(mn * nums[i],Math.min(nums[i],mx * nums[i]));
result = Math.max(max,result);
}
return result;
}

复杂度分析

  • 时间复杂度 o(n)
  • 空间复杂度 o(1)

后记

  • 千古兴亡多少事?悠悠。不尽长江滚滚流。

本文转载自: 掘金

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

1…133134135…956

开发者博客

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