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

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


  • 首页

  • 归档

  • 搜索

让你的Go代码集健壮和优美于一身

发表于 2021-11-17

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

一. 简介

关于测试工程师,有一个笑话,是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
r复制代码一个测试工程师走进一家酒吧,要了一杯啤酒
一个测试工程师走进一家酒吧,要了一杯咖啡
一个测试工程师走进一家酒吧,要了0.7杯啤酒
一个测试工程师走进一家酒吧,要了-1杯啤酒
一个测试工程师走进一家酒吧,要了2^32杯啤酒
一个测试工程师走进一家酒吧,要了一杯洗脚水
一个测试工程师走进一家酒吧,要了一杯蜥蜴
一个测试工程师走进一家酒吧,要了一份asdfQwer@24dg!&*(@
一个测试工程师走进一家酒吧,什么也没要
一个测试工程师走进家酒吧,又走出去又从窗户进来又从后门出去从下水道钻进来
一个测试工程师走进家酒吧,又走出去又进来又出去又进来又出去,最后在外面把老板打了一顿
一个测试工程师走进一
一个测试工程师走进一家酒吧,要了一杯烫烫烫的锟斤拷
一个测试工程师走进一家酒吧,要了NaN杯Null
1T测试工程师冲进一家酒吧,要了500T啤酒咖啡洗脚水野猫狼牙棒奶茶
1T测试工程师把酒吧拆了
一个测试工程师化装成老板走进一家酒吧,要了500杯啤酒,并且不付钱
一万个测试工程师在酒吧外呼啸而过
一个测试工程师走进一家酒吧,要了一杯啤酒‘;DROPTABLE酒吧
测试工程师们满意地离开了酒吧

这个笑话估计也只有开发才明白其中的笑点与心酸吧。

对于一些刚入门的开发来说,这简直就是噩梦。当初刚入门的时候我的代码也是很多毛病,经不起这样的测试,后来渐渐地经验多了后,代码的健壮性逐渐提升,也明白其中比较重要的就是参数的校验。

参数的使用有通过协议的接口调用(如http、rpc……)、函数调用、库调用等等方式。

其实对于http api的请求来说,现在很多web框架都已经自带了参数校验的功能,基本用起来都挺爽的,也无需多讲。

而对于函数调用这样的常见方式,很多是要靠开发自己去校验参数的。如果仅仅是靠注释,在团队开发过程中,难免会有问题产生。起码我觉得,永远不要相信传过来的参数!

OK,那么来讲讲这次的题目,就是Golang中的参数校验库——validator。

用过Gin的小伙伴儿应该知道其binding参数验证器非常好用,其就是调用了validator。此处呢我们来介绍下validator的基础用法,和在一般场景下的应用案例。

二. 基础用法

1. 介绍

validator包源码在github.com/go-playgrou…。其基于标记实现结构和单个字段的值验证,包含如下关键功能:

  • 使用验证标记或自定义验证程序进行跨字段和跨结构验证
  • Slice、Array和Map都可以允许验证多维字段的任何或者所有级别
  • 能够深入查看映射键和值以进行验证
  • 通过在验证之前确定类型接口的基础类型来处理类型接口
  • 处理自定义字段类型
  • 允许将多个验证映射到单个标记,以便在结构上更轻松地定义验证
  • 提取自定义定义的字段名,例如,可以指定在验证时提取JSON名称,并使其在结果FieldError中可用
  • 可定制的i18n错误消息
  • gin web框架的默认验证器

2. 安装

1
bash复制代码go get github.com/go-playground/validator/v10

3. 导入

1
go复制代码import "github.com/go-playground/validator/v10"

4. 验证规则

此处从官方列举的各个类别中挑选部分举例说明。

1)比较

  • eq:相等
  • gt:大于
  • gte:大于等于
  • lt:小于
  • lte:小于等于
  • ne:不等于

2)字段

此处的字段大部分可以理解为上面的比较的tag跟field拼接而成,而中间有cs的tag为跨struct比较。

  • eqfield(=Field):必须等于Field的值
  • nefield(=Field):必须不等于Field的值
  • gtfield(=Field):必须大于Field的值
  • eqcsfield(=Other.Field):必须等于struct Other中的Field的值

3)网络

  • ip:网络协议地址IP
  • ip4_addr:网络协议地址IPv4
  • mac:mac地址
  • url:url

4)字符

  • ascii:ASCII
  • boolean:Boolean
  • endswith: 以…结尾
  • contains:包含
  • uppercase:大写

5)格式

  • base64:Base64字符串
  • base64url:Base64url字符串
  • email:邮箱字符串
  • json:JSON
  • jwt:JSON Web Token
  • latitude:纬度

6)其它

  • len:长度
  • max:最大值
  • min:最小值
  • required:字段为必须,不可空

7)别名

  • iscolor:hexcolor|rgb|rgba|hsl|hsla
  • country_code:iso3166_1_alpha2|iso3166_1_alpha3|iso3166_1_alpha_numeric

三. 案例

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
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
go复制代码package main

import (
"fmt"

"github.com/go-playground/validator/v10"
)

type User struct {
Name string `validate:"required,lte=10"` // 姓名 非空,长度小于等于10
Age int `validate:"required,gte=18,lte=50"` // 年龄 非空,数字大于等于18,小于等于50
Email string `validate:"required,email"` // 邮箱 非空,格式为email
FavouriteColor string `validate:"iscolor"` // 喜欢的颜色 hexcolor|rgb|rgba|hsl|hsla的别名
Password string `validate:"required,gte=16,lte=22"` // 密码 非空,长度大于等于16,小于等于22
RePassword string `validate:"required,gte=16,lte=22,eqfield=Password"` // 确认密码 非空,长度大于等于16,小于等于22,必须和字段Password相同
Hobbies []Hobby `validate:"lte=5"` // 多个爱好 长度小于等于5
}

type Hobby struct {
Name string `validate:"lte=50"` // 爱好名称 长度小于等于50
}

var validate *validator.Validate

func main() {

validate = validator.New()

// 该函数验证struct
// 不会报错
validateStruct()

// 该函数单度验证字段
// 会报错
validateVariable()

}

func validateStruct() {

hobby := Hobby{
Name: "划水",
}

user := User{
Name: "张三",
Age: 48,
Email: "kuari@justmylife.cc",
FavouriteColor: "#ffffff",
Password: "1234567890123456",
RePassword: "1234567890123456",
Hobbies: []Hobby{hobby},
}

err := validate.Struct(user)
if err != nil {
fmt.Println(err)
}

}

func validateVariable() {

email := "kuari.justmylife.cc" // 此处邮箱地址格式写的是错误的,会导致报错
err := validate.Var(email, "required,email")
if err != nil {
fmt.Println(err)
}

}

2. 自定义验证

自定义验证可以自己创建一个校验的函数:

1
2
3
4
go复制代码// 注册校验函数
func ValidateMyVal(fl validator.FieldLevel) bool {
return fl.Field().String() == "hello,world!"
}

然后将其注册到validate上即可:

1
2
3
4
5
6
7
8
go复制代码validate = validator.New()
validate.RegisterValidation("is-hello", ValidateMyVal)

s := "hello,kuari" // 跟校验函数中的字符串不同,因此此处会报错
err := validate.Var(s, "is-hello")
if err != nil {
fmt.Println(err)
}

自定义校验可以满足开发过程中的特殊场景,通过制定规范的校验标准,可以推进团队的协作和开发效率。

四. 最后

至此便是对于validator的介绍了。本文篇幅较短,管中窥豹而已,基本可以满足简单场景的使用。以及本文的案例也是基于官方的案例改的,让其稍微接地气点。若有兴趣的小伙伴还是建议去完整看一下官方的文档和案例,多样的用法可以满足多样的场景,在满足代码的健壮性的同时也能确保代码的优美。

五. 参考文档

  • go-playground/validator

六. 博客原文

  • Golang如何使用validator校验参数

本文转载自: 掘金

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

Java经典课程设计--在线蛋糕商城销售网站项目【Sprin

发表于 2021-11-17

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

前言:

人类将步入信息时代,网络越来越强烈地介入我们的生活,越来越 贴近我们。这是一个知识经济的时代,信息正在以前所未有的速度膨胀 和爆炸,未来的世界是网络的世界,要让我国在这个信息世界中跟上时 代的步伐,作为 21 世纪主力军的我们,必然要能更快地适应这个高科技 的社会,要具有从外界迅速、及时获取有效科学信息的能力,具有传播 科学信息的能力,这就是科学素质。而网络恰恰适应了这个要求。因此, 网络销售及电子商务应运而生,由此引出了网络蛋糕销售系统,网络蛋 糕销售系统的主要目的是让全国各地的人们在家都能买到自己想吃的蛋 糕。传统的蛋糕销售主要是以店面为基础,要选择合适的门面房,浪费 钱财,销量也不容乐观。而网络蛋糕销售系统可以不用担心门面房问题, 消费群体范围也扩展了。可以大大节省开支,还增加了营业额。它在引 导上的直接性和自身所具有的独到特点,易被接受和采纳,是一种实用 性强的软件工具。

主要功能说明

用户角色: 包含以下功能:查看所有蛋糕,用户登录和注册,查看蛋糕详情,提交订单,查看我的订单,查看我的购物车,确认收货,评价等功能。

管理员: 管理员登录,蛋糕分类管理,蛋糕管理,用户管理,订单管理。

功能截图:

登陆注册:

首页功能:蛋糕商品浏览 购买 加入购物车等


主要代码实现:

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
java复制代码package com.smzy.controller;

import com.smzy.pojo.User;
import com.smzy.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Controller
@RequestMapping("/admin")
public class UserController {
@Autowired
private UserService userService;

@RequestMapping("/listUser")
public String findAll(Model model) {
List<User> users = userService.findAll();
model.addAttribute("users",users);
return "admin/listUser";
}
@RequestMapping("/editUser")
public String edit(Model model ,Integer id) {
User user = userService.get(id);
model.addAttribute("user",user);
return "admin/editUser";
}

@RequestMapping("/updateUser")
public String update(Integer id,String password) {
userService.updatePassword(id,password);
return "redirect:listUser";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码package com.smzy.service;

import com.smzy.pojo.User;

import java.util.List;

public interface UserService {

List<User> findAll();

User get(Integer id);

void updatePassword (Integer id ,String password);

User get(String name,String password);

boolean isExist(String name);

void add(User user);
}
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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
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
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">

<context:component-scan base-package="com.smzy.controller"/>
<mvc:annotation-driven/>
<mvc:default-servlet-handler/>

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>

<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/*"/>
<bean class="com.smzy.interceptor.LoginInterceptor"/>
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/*"/>
<bean class="com.smzy.interceptor.OtherInterceptor"/>
</mvc:interceptor>

</mvc:interceptors>
</beans>

数据库表设计:

**数据库名: **ssm_dangao_shop

**文档版本: **V1.0.0

**文档描述: **数据库表设计描述

表admin

编号 名称 数据类型 长度 小数位 允许空值 主键 默认值 说明
1 id int 10 0 N Y 管理员id
2 name varchar 255 0 N N 管理员用户名
3 password varchar 255 0 N N 管理员密码

表category

编号 名称 数据类型 长度 小数位 允许空值 主键 默认值 说明
1 id int 10 0 N Y 唯一索引ID
2 name varchar 255 0 N N 分类名
3 img_url varchar 255 0 N N 分类详情图地址

表orders

编号 名称 数据类型 长度 小数位 允许空值 主键 默认值 说明
1 id int 10 0 N Y 唯一索引id
2 order_code varchar 255 0 N N 订单号
3 address varchar 255 0 N N 收货地址
4 receiver varchar 255 0 N N 收货人姓名
5 phone varchar 255 0 N N 手机号码
6 user_message varchar 255 0 N N 用户备注的信息
7 create_date datetime 19 0 N N 订单创建时间
8 pay_date datetime 19 0 Y N 订单支付时间
9 delivery_date datetime 19 0 Y N 发货日期
10 confirm_date datetime 19 0 Y N 确认收货日期
11 user_id int 10 0 Y N 对应的用户id
12 status varchar 255 0 N N 订单状态

表order_item

编号 名称 数据类型 长度 小数位 允许空值 主键 默认值 说明
1 id int 10 0 N Y 唯一索引id
2 product_id int 10 0 N N 对应产品id
3 order_id int 10 0 Y N 对应订单id
4 user_id int 10 0 N N 对应用户id
5 number int 10 0 Y N 对应产品购买的数量

表product

编号 名称 数据类型 长度 小数位 允许空值 主键 默认值 说明
1 id int 10 0 N Y 唯一索引id
2 name varchar 255 0 N N 产品的名称
3 sub_title varchar 255 0 Y N 小标题
4 price float 13 0 Y N 价格
5 sale int 10 0 Y N 销量
6 stock int 10 0 Y N 库存
7 description varchar 2000 0 N N 商品描述
8 brand varchar 255 0 N N 品牌
9 category_id int 10 0 Y N 对应的分类id

表product_image

编号 名称 数据类型 长度 小数位 允许空值 主键 默认值 说明
1 id int 10 0 N Y 唯一索引id
2 product_id int 10 0 Y N 产品ID

表property

编号 名称 数据类型 长度 小数位 允许空值 主键 默认值 说明
1 id int 10 0 N Y 唯一索引id
2 name varchar 255 0 Y N 属性名称
3 category_id int 10 0 N N 对应的分类id

表property_value

编号 名称 数据类型 长度 小数位 允许空值 主键 默认值 说明
1 id int 10 0 N Y 唯一索引id
2 product_id int 10 0 N N 对应产品id
3 property_id int 10 0 N N 对应属性id
4 value varchar 255 0 Y N 具体的属性值

表review

编号 名称 数据类型 长度 小数位 允许空值 主键 默认值 说明
1 id int 10 0 N Y 唯一索引id
2 content varchar 4000 0 Y N 评价内容
3 user_id int 10 0 N N 对应的用户id
4 product_id int 10 0 N N 对应的产品id
5 createDate datetime 19 0 Y N 评价时间

表user

编号 名称 数据类型 长度 小数位 允许空值 主键 默认值 说明
1 id int 10 0 N Y 唯一索引id
2 name varchar 255 0 N N 用户名称
3 password varchar 255 0 N N 用户密码
4 email varchar 255 0 N N 邮箱
5 registTime timestamp 19 0 N N CURRENT_TIMESTAMP 注册时间

大家点赞、收藏、关注、评论啦 、
打卡 文章 更新 104/ 365天

本文转载自: 掘金

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

操作系统系列 -- Linux 常用命令

发表于 2021-11-17
类型 各命令具体作用
文件目录操作 ls 命令:查看 linux 文件夹包含的文件、查看文件权限、查看目录信息; cd 命令:最基本的命令语句,其他的命令都是建立在使用 cd 命令上的,用于切换当前目录至 dirName; pwd 命令:查看”当前工作目录”的完整路径; mkdir 命令:用来创建指定的名称的目录,要求创建目录的用户在当前目录中具有写权限,并且指定的目录名不能是当前目录中已有的目录; rm 命令:删除一个目录中的一个或多个文件或目录,如果没有使用 -r 选项,则 rm 不会删除目录。如果使用 rm 来删除文件,通常仍可以将该文件恢复原状; rmdir 命令:该命令从一个目录中删除一个或多个子目录项,删除某目录时也必须具有对父目录的写权限; mv 命令:(move (rename) files)可以用来移动文件或者重命名文件。当第二个参数类型是文件时,mv 命令完成文件重命名。当第二个参数是已存在的目录名称时,源文件或目录参数可以有多个,mv 命令将各参数指定的源文件均移至目标目录中; cp 命令:将源文件复制至目标文件,或将多个源文件复制至目标目录; touch 命令:更改文档或目录的日期时间,包括存取时间和更改时间; cat 命令:用来显示文件内容/将几个文件连接起来显示/从标准输入读取内容并显示,它常与重定向符号配合使用; nl 命令:输出的文件内容自动的加上行号!其默认的结果与 cat -n 有点不太一样, nl 可以将行号做比较多的显示设计,包括位数与是否自动补齐 0 等等的功能 more 命令:more 命令和 cat 的功能一样都是查看文件里的内容,但有所不同的是 more 可以按页来查看文件的内容,还支持直接跳转行等功能; less 命令:less 与 more 类似,但使用 less 可以随意浏览文件,而 more 仅能向前移动,却不能向后移动,而且 less 在查看之前不会加载整个文件; head 命令:用来显示档案的开头至标准输出中,默认 head 命令打印其相应文件的开头 10 行; tail 命令:显示指定文件末尾内容,不指定文件时,作为输入信息进行处理。常用查看日志文件;
文件查找 which 命令:在 PATH 变量指定的路径中,搜索某个系统命令的位置,并且返回第一个搜索结果; whereis 命令:定位可执行文件、源代码文件、帮助文件在文件系统中的位置; locate 命令:快速搜寻档案系统内是否有指定的档案; find 命令:沿着文件层次结构向下遍历,匹配符合条件的文件,并执行相应的操作
文件打包上传和下载 tar 命令:用来压缩和解压文件。tar本身不具有压缩功能,他是调用压缩功能实现的; gzip 命令:使用广泛的压缩程序,文件经它压缩过后,其名称后面会多出”.gz”的扩展名;
文件权限设置 chmod 命令:用于改变 linux 系统文件或目录的访问权限; chgrp 命令:可采用群组名称或群组识别码的方式改变文件或目录的所属群组; chown 命令:通过chown改变文件的拥有者和群组;
磁盘存储 df 命令:显示指定磁盘文件的可用空间; du 命令:显示每个文件和目录的磁盘使用空间;
性能监控和优化 top 命令:显示当前系统正在执行的进程的相关信息,包括进程ID、内存占用率、CPU占用率等; free 命令:显示系统使用和空闲的内存情况,包括物理内存、交互区内存(swap)和内核缓冲区内存 vmstat 命令:用来显示虚拟内存的信息; lostat 命令:查看CPU、网卡、tty设备、磁盘、CD-ROM 等等设备的活动情况, 负载信息; lsof 命令:查看你进程开打的文件,打开文件的进程,进程打开的端口(TCP、UDP);
网络命令 ipconfig 命令:查看和配置网络设备; route 命令:创建一个静态路由让指定一个主机或者一个网络通过一个网络接口,如eth0; ping 命令:确定网络和各外部主机的状态;跟踪和隔离硬件和软件问题;测试、评估和管理网络; traceroute 命令:追踪网络数据包的路由途径,预设数据包大小是40Bytes,用户可另行设置; netstat 命令:显示与IP、TCP、UDP和ICMP协议相关的统计数据,一般用于检验本机各端口的网络连接情况; telstat 命令:开启终端机阶段作业,并登入远端主机;
其他命令 ln 命令:为某一个文件在另外一个位置建立一个同步的链接。当我们需要在不同的目录,用到相同的文件时,我们不需要在每一个需要的目录下都放一个必须相同的文件,我们只要在某个固定的目录,放上该文件,然后在 其它的目录下用ln命令链接(link)它就可以,不必重复的占用磁盘空间; diff 命令:比较单个文件或者目录内容; grep 命令:强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行打印出来; wc 命令:;用来显示文件所包含的行、字和字节数 ps 命令:用来显示当前进程的状态; watch 命令:可以将命令的输出结果输出到标准输出设备,多用于周期性执行命令/定时执行命令; at 命令:在一个指定的时间执行一个指定任务,只能执行一次。(需开启atd进程); crontab 命令:在固定的间隔时间执行指定的系统指令或 shell script脚本。时间间隔的单位可以是分钟、小时、日、月、周及以上的任意组合。(需开启crond服务);

命令的参数及使用实例见45 个常用Linux 命令,让你轻松玩转Linux 或 Linux 常用命令学习 - 菜鸟教程 或 27个常用的Linux 命令 - 简书

本文转载自: 掘金

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

LeetCode3 无重复字符的最长子串 题目信息

发表于 2021-11-17

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

题目信息

1
2
c复制代码给定一个字符串 s ,
请你找出其中不含有重复字符的 最长子串 的长度。

示例1:

1
2
3
java复制代码输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例2:

1
2
3
java复制代码输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例3:

1
2
3
4
java复制代码输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

示例4:

1
2
java复制代码输入: s = ""
输出: 0

解题思路:双指针扫描

假设下面的字符串是题目给出的字符串。

1
2
3
4
5
6
7
8
c复制代码定义结果res代表最长子串的长度。
定义双指针i,j起初都在0号下标位置,遍历整个字符串,
接着定义哈希表map,键值对为Character,Integer。
表示当前字符出现的次数。每次遇到一个字符,就将它放到map中,
直到该字符在map中的value值大于1,
这时候就说明存在重复的字符了。
接着将当前j下标的字符放进map中,value值为当前j下标的value值减1;
接着j++;更新res。最后返回res。

在这里插入图片描述


实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码    public int lengthOfLongestSubstring(String s) {
HashMap<Character,Integer> hashMap=new HashMap<> ();
int res=0;
for(int i=0, j=0; i<s.length ();i++){
hashMap.put (s.charAt (i), hashMap.getOrDefault (s.charAt (i), 0)+1);
while (hashMap.get (s.charAt (i))>1){
hashMap.put (s.charAt (j), hashMap.get (s.charAt (j))-1);
j++;
}
res=Math.max (res,i-j+1);
}
return res;
}

本文转载自: 掘金

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

5go基础入门-字符(byte、rune)、字符串(str

发表于 2021-11-17

前言

什么是字符?什么是字符串?

1
2
3
4
5
6
7
8
9
go复制代码// 输出一句话
fmt.Println("ab吃饭")
/*
输出结果:
ab吃饭
*/

// 字符
a := 'a'

我们先看一下这句话 “ab吃饭”,这一句话就是字符串,这一串字符由四个字符组成,分别是 ‘a’,’b’,’吃’,’饭’;所以单独拿出来,每一个字就是一个字符,字符类型单引号引用,并且只包含一个字符;而字符串,顾名思义就是可以多个字符串联在一起,双引号引用;(我们可以把字符串理解成是一个由双引号组成的容器,把它看做是长方形凹槽;可以把字符理解成乒乓球,一个字符就是一个乒乓球;我们可以往凹槽里放入乒乓球)

[ ‘a’, ‘b’, ‘吃’, ‘饭’ ]

  • 字符: 单引号组成、内容只有一个字符(它就是一个乒乓球,对应一个字符,所以内容必然是有值的,内容为空时会报错)
  • 字符串: 双引号组成(它只是一个容器,乒乓球放不放都行,所以里面没内容也不会报错)

字符 byte

我们在前面的篇章中说过,我们的一切数据,都是以二进制的形式存在于内存当中的,所以我们所说的字符类型也是如此;我们前面讲过 uint8 类型的整数,他是无符号,8个比特位(1个字符)的整数,所表示的范围是 0到255,一共256个整数;那这个 uint8 跟我们讲的 byte有什么关系呢?

还记得上一章我们讲的浮点数吗?虽然我们直观看到的是小数,但是实际上他是用IEEE754标准把浮点数转换成了二进制数据之后,再存储在内存中的;也就是说 我们的 byte 字符型数据也是通过某种方式,转换成了二进制,再存储进内存;所以我们在讲解 byte、rune、string之前,先把 ‘字符’ 转 ‘二进制’ 的方式讲明白,后面我们就能很简单理解这三种类型。

编码是信息从一种形式或格式转换为另一种形式的过程,也称为计算机编程语言的代码简称编码。这是什么意思呢?

我们计算机中有很多本字典,我们拿 ASCII 这本字典做个列子, 我们键盘上的 26个大写字母、26个小写字母、符号、数字0-9等这些字符都在ASCII这个字典中,每个字符在字典中都有一个位置,字符 ‘a’ 在 ASCII字典中的位置是 97,所以我们把 ‘a’ 转成 97,再把97转成二进制,然后存储到内存中;同理,我们拿到内存中的二进制值,再通过二进制转换成97,我们在 ASCII 字典中取出位置97的字符,我们就得到了字符 ‘a’。是不是既简单又易懂,所以我们把这种方式成为 编码(信息从一种形式或格式转为另一种形式的过程)。

ASCII(美国信息交换标准代码) 这本比较早的字典用于显示新英语和其他西欧语言,这本字典的大小是一个字节,也就是256个位置,范围是0-255,刚好和uint8一样,无符号256个数字;我们上面已经说过, uint8和byte大小和存储都是一样的,无符号256个数字,只是名称不一样;之所以byte是字符型,是因为byte中存储的数字其实是对应字典中的位置的,为了在字面上可以更直观的表达出字符型,所以没有使用 uint8 这个类型来存储,而是创建多一种名称不同,而实际与uint8一样的类型 byte。

其所我们的 byte 类型就是对应 ASCII 编码表的一种类型,他存储的是ASCII编码表的字符位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码// 创建一个字符型 'a'
var a byte = 'a'
fmt.Println(a)
/*
输出结果:
97
*/

var b byte = ''
/* 报错 empty rune literal or unescaped ' */

var c byte = '措'
/* 报错 constant 25514 overflows byte */

我们 fmt.Println 函数打印 a 变量的值,结果是 97,而非 ‘a’,由此而知我们 a 变量存储的值实际上是 97,而我们所看到的 ‘a’,其实是计算机在编码表中取出来显示的符号。

变量 b 报错,上面说过 byte 中存储的实际上就是 ASCII 编码标准中的字符编号(位置),字符不能为空,所以报错。

变量 c 报错,byte 是一个字节 8 个bit,无符号 整型,256个数字,字符 ‘措’ 是 Unicode 编码标准(下文讲述)中的字符,他在 Unicode 标准中的字符编号(位置)是 25514, byte 所能存储的最大数字是 255,而我们把数字 25514 存储到 byte 类型中就会报错 “constant 25514 overflows byte”,就是溢出,放不下那么大的数字。

到这里我们应该明白 byte 类型了,我们下面做个小总结:

  • byte 1字节,无符号,256个正数,范围0-255
  • ASCII 编码表,256个字符,对应 byte,只适用美国使用电脑
  • 编码表字符与计算机二进制之间的转换,我们把这个过程称之为 编码

字符 rune

上面有一个重点,大家应该也注意到了就是, ASCII 编码,他主要用于 新英语和其他西欧语言的转换。常用的 符号、字母、数字的确可以使用 ASCII 编码就能满足,但如果是显示我们的中文呢?我们的中文汉字呢?只有256个怎么够我们使用?

汉字的数量并没有准确数字,大约将近十万个,日常所使用的汉字只有几千字。据统计,1000个常用字能覆盖约92%的书面资料,2000字可覆盖98%以上,3000字则已到99%,简体与繁体的统计结果相差不大。(百度百科)

所以 ASCII 编码只是适用于美国、西欧等国家,我们国家如果要使用电脑并且显示中文,就要有一个包含中文的编码标准,所以我们上面说了 技术机中有很多本字典;为了让电脑显示中文,我国家又发布了 GB2312-80 标准、GBK 编码标准、GB18030编码标准等;如果每个拥有自己独特字符的国家都去发布一份自己的标准,就会出现一种问题,比如我们国家电脑程序使用的是GBK进行编码的,那么程序和数据到别的国家的电脑上运行时就会出现乱码的问题,因为两台电脑使用的不是同一个编码标准。

为了解决上述问题,有一个国际组织提出创建 Unicode 编码标准(统一码、万国码、单一码),由世界各国合作把自己国家的字符都添加到这个编码标准中,这个标准有 1114112 码位,可以容纳全世界所有国家的字符,最多使用4个字节进行存储,前面部分兼容 ASCII 标准中的字符,然后再加上各国自己的字符;另外 Unicode 标准中使用 UTF-8 格式去编码时,中文是使用 3 个字节的,而我们的 Golang 是默认使用 UTF-8 格式的,我们编程的时候,如果我们的程序仅仅只用到中文、中文符号、英文、英文符号(使用的都是中国人),那么使用GBK进行编码也是可以的,因为GBK标准中,汉字字符的编码是使用 2 个字符的,比 Unicode UTF-8 要节省内存空间。(我们平常所说的 UTF-8,实际上说的是 使用Unicode编码标准UTF-8转换格式)

我们这里简单提一嘴UTF-8(UCS)格式,这里代表的是以1个byte1个byte地去处理,这样可以对不同范围的字符使用不同长度编码;比如Unicode前面部分 ASCII 字符,用一个字节就可以表示,这个时候我们 UTF-8 格式就能根据字符串度使用一个字节去处理;又比如某个汉字字符需要三个字节才能表示,这个时候 UTF-8 格式就用三个字节去处理;这样可以通俗的理解了吧?以前老版的Unicode只有两种编码格式 UCS2 和 UCS4 就是用2个字节表达或者用4个字节表达,后面使用了 UTF 的处理方式才真正让 Unicode 名副其实的称为万国码。 这里只简单的提一嘴,编码方面详细的知识如果有兴趣的可以自行查阅关于计算机编码方面的资料进行学习,这里就不再详细说了,后续找个时间再专门写一篇编码相关的文章。

言归正传 rune 字符型

我们讲 byte 字符型的时候说他其实就是 uint8类型,只是类型的名称不同,存储的数据都是一样的。
而我们的 rune 字符类型,4个字节、有符号、整型,是不是跟我们的 int32 一样?实际上 rune 等价于 int32 ,只是类型名称不同。rune 存储的是 Unicode 编码标准中的字符编号,Unicode 最大用 4 个字节来表示字符,而 rune 使用的是 4 个字节,意味着 rune 可以存储以及表示出 Unicode 中的所有字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码// rune
var a rune = 'a'
fmt.Println(a)
/*
输出结果:
97
*/

var b rune = '措'
fmt.Println(b)
/*
输出结果:
25514
*/

所以 rune 能存储 ‘措’ 字符,自然也能存储下占用1个字节的 ‘a’字符,rune 跟 byte一样不能为空,单引号内一定要有内容。如果我们只是表示 ASCII 编码标准中的字符,直接使用 byte 类型就可以了,没必要使用 rune 类型,该类型是直接使用4个字节的,内存使用的是 byte 的四倍。

说白了, rune 就是一个空间更大容器,可以放得下编号更大字符, ‘措’ 字符的 编号是数字 25514,而我们的 rune 的空间是4个字节32个bit,存储数字 25514 绰绰有余。

有byte的铺垫,rune应该很容易看到,到此我们对 rune 做过一个小总结:

  • rune 4个字节 32bit、对应 int32 类型
  • Unicode 万国码、统一码、最大使用4个字节表示一个字符、有多种编码格式(UTF-8、UTF-16、UTF-32)
  • UTF-8 golang默认使用的编码方式、UTF-8格式使用3个字节表示1个汉字字符

字符串 string

字符串顾明思意,就是多个字符串联起来,使用双引号表示。

1
2
3
4
5
6
7
8
9
go复制代码// string 类型
var a string = "a措"
fmt.Println(a)
fmt.Println("长度:", len(a))
/**
输出结果
a措
长度:4
**/

打印出内容 a措 的时候,是不是还能理解,当使打印出 长度:4 的时候是不是就有点懵了?有 byte 和 rune 的铺垫,我们再讲一下 string 字符串就能很简单的理解了。

我们上面篇章说过 Go 语言默认使用 Unicode UTF-8 编码的,UTF-8 格式 对不同范围的字符使用不同长度编码,变量 a 字符串中有两个字符 ‘a’ 和 ‘措’,字符 ‘a’ 所在的范围用1个字节就可以存储了;上面也说过 汉字字符,在使用 UTF-8 格式时,一个汉字字符使用3个字节表示。我们把这两个字符看做乒乓球,’a’ 乒乓球的大小是1byte, ‘措’ 乒乓球的大小是3个byte,而我们的双引号就是一个凹槽容器,这时我们往凹槽容器依次放入乒乓球 ‘a’,再放入乒乓球 ‘措’ ,我们代码中获取长度的 len() 函数,其实是获取变量的 字节数量,那么我们的 ‘a’ 字符1byte,而 ‘措’ 字符3byte,所以打印的结果是 4。这能明白了吧,字符串之所以叫字符串,就是把字符串联起来。

而我们比喻的这个 凹槽容器,其实他也是一种类型,我们叫他 数组,但这是下一篇的内容,我们把它看做是一个容器就可以了。

“a措” → [ ‘a’, ‘措’ ] → [ b1, b2, b3, b4 ]

  • “a措” 编码后我们所看到的字符串内容
  • [ ‘a’, ‘措’ ] 我们所理解的 凹槽中存放乒乓球 的样子
  • [ b1, b2, b3, b4 ] string类型变量 a 中实际的值,其中 b1 是存储 ‘a’ 字符编号的字节, b2 b3 b4 是用来存储 ‘措’ 字符所用的字节;这个 [ ] 中括号就是我们说的 数组,中括号内的 b1 b2 b3 b4 就是数组中的元素(值),数组中有4个byte字节类型的元素,那么他的大小就是 4,所以 len() 函数获取的是这个数组元素的数量,而我们数组中元素的类型都是字节型,所以我们称他为 字节型数组,也就是 byte[];这里就先这样简单的了解一下数组,下一章我们再详细的去说。

上面代码打印出 a 变量的长度是 4,这个我们已经可以理解了,但是我们该怎么用代码输出 字符 的数量呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码a := "a措"

// 使用 utf8 包的函数
b := utf8.RuneCountInString(a)
fmt.Println(a)
/*
输出结果:
2
*/

// 把字符串转换成 rune类型的数组
c := []rune(a)
fmt.Println(len(c))
/*
输出结果:
2
*/
  • utf8.RuneCountInString(a) 函数使用 utf-8 的规则统计出 a 变量的字符数量,把统计出的数量 2,赋值给 b 变量,然后打印出来 输出结果:2
  • b := [] rune(a) 把 a字符串 转换成 rune类型数组,这个rune类型的数组可以理解成数组的内容只存储 rune字符,然后再赋值给 c 变量,所以此时的 c变量 [ ‘a’, ‘措’ ]
  • len(b) 函数获取数组中的元素数量,这是一个 rune字符类型的数组,里面有两个元素,所以结果是 2,打印出来 输出结果:2

我们字符串中所看到的内容是经过编码后显示出来的内容,实际上字符串的实际值就是一个 byte[] 数组,把每个字符通过 UTF-8 格式编码得到字节放到这个数组中,然后再把这数组的内容存储到内存中;反之,我们拿到这个数组的内容,再通过 UTF-8 编码又能再得到对应的字符,然后再把字符组成成字符串显示出来。

至此我们的字符串部分也讲完了,我们做一个小总结:

  • 字符串 多个字符串联在一起
  • 字符串的实际值是 byte[] 数组,当字符串为空时是允许的(a := “”),因为实际值是[ ],表示一个没有元素的数组,所以不会报错
  • Unicode UTF-8 格式把得到的字符转换成编号,并存储在字节中,然后再把字节存入数组中,这样 byte[] 就能跟 string 字符串通过使用 UTF-8 格式相互转换了。

至此,本章内容 byte字符、rune字符、string字符串,三种类型都讲解完成。
(java中的字符和字符串本质上也是一样的,不过java9之前String使用char[]数组去存储字符串,java9之后才使用了byte[]数组存储字符串,java中的char是固定的两个字节表示不了4个字节的字符,这点和go有一定的差别,有兴趣自行了解)

bool 布尔类型

这个是最简单的一种数据类型,该类型只有固定的两个值,就是关键字 true 和 false ,默认是 false。

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
go复制代码// 布尔变量
t := true
f := false
fmt.Println(t)
fmt.Println(f)
/*
输出结果:
true
false
*/

// 创建 a变量 赋值0
var a int32 = 0

// 条件不成立时,t=false
t = a == 1
// 条件成立时,f=true
f = a == 0
fmt.Println(t)
fmt.Println(f)
/*
输出结果:
false
true
*/

a == 1 两个等于号,是对比的意思,就是对比左右两边的值是否相等; a = 1 一个等于号,是赋值,之前说过; ! 感叹号,就是取反的意思, 0 != 1 这个条件的意思就是 0 不等于 1,所以这个条件是成立的,所以是 true;这里只简单提一嘴,后面会详细讲述这些符号,这里先知道怎么使用的就可以了。

bool类型的值是固定的 true 和 false,我们赋值的条件如果成立值为 true(1 == 1),如果条件不成立值为 false(0 > 1)。

进入我们的类型讲解环节,go 语言中的 bool 布尔类型使用1个字节存储,用 0 表示 false,用 1 表示 true,所以在内存中 false(0000 0000)、true(0000 0001),但我们的go语言做了特殊处理,虽然他们的二进制数对应我们的十进制数就是 0 和 1,而我们的 bool 类型只做了 true 和 false 的转换,并且限制了我们使用 整数 对比 bool 类型( t == 0 会报错)。所以我们的 bool 类型和其他语言是有差别的,不能用数值与 bool直接对比( java语言中 true == 1 是允许的)。

bool 类型:

  • 存储 使用 1 byte ,存储固定值 0 和 1
  • 值 固定值 true(1) 和 false(0),虽然 bool 类型存储的实际值是 0 和 1 ,但不能使用数值与 bool 类型比对

到这里我们的基本数据类型已经全部讲完了,下一章开始内容 数组。

本文转载自: 掘金

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

java 递归实现权限树(菜单树) 一、 权限树的问题由来

发表于 2021-11-17

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

完整示例代码地址如下:
github.com/Dr-Water/sp…

一、 权限树的问题由来

  1. 在开发中难免遇到一个有多级菜单结构树,或者多级部门的结构树,亦或是省市区县的多级结构,数据结构类似如下的json数据:
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
json复制代码[
{
"id": "1",
"name": "主菜单1",
"pid": "0",
"menuChildren": [
{
"id": "4",
"name": "子菜单1.1",
"pid": "1",
"menuChildren": [
{
"id": "6",
"name": "子菜单1.1.1",
"pid": "4",
"menuChildren": []
},
{
"id": "9",
"name": "子菜单1.1.2",
"pid": "4",
"menuChildren": []
}
]
},
{
"id": "5",
"name": "子菜单1.2",
"pid": "1",
"menuChildren": []
}
]
},
{
"id": "2",
"name": "主菜单2",
"pid": "0",
"menuChildren": [
{
"id": "7",
"name": "子菜单2.1",
"pid": "2",
"menuChildren": []
},
{
"id": "8",
"name": "子菜单2.2",
"pid": "2",
"menuChildren": []
}
]
},
{
"id": "3",
"name": "主菜单3",
"pid": "0",
"menuChildren": []
}
]

二、 解决方案

目前的解决方案主要有以下两种方案:

  • 方案一:后端把所有需要的数据以一个大list返回前端,前端进行操作,把数据搞成树状结构
  • 方案二: 后端在后端返回数据之前把数据搞成已经有层次结构的数据,方案二也分为两种解决方法
    • 方法一:次性将数据查询出来,在java程序中进行树状结构的构建
    • 方法二: 第一次将最高层次的数据查询出来,然后多次循环查询数据库将子数据查询出来

由于博主的前端水平有限,目前只能用后端的实现方式,再加上每次查询数据库的开销比较大,所以本文使用方案二的方法一进行验证

实现步骤

以菜单的结构树为例

  1. 准备mysql数据库的基础数据
    在这里插入图片描述
  2. java的实体类:
1
2
3
4
5
6
7
8
9
java复制代码@Data
@NoArgsConstructor
public class Menu implements Serializable {

private String id;
private String name;
private String pid;
private List<Menu> menuChildren;
}
  1. java的dao层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Mapper
public interface MenuDao {

/**
* 根据父类id查询子类菜单
* @param pid
* @return
*/
List<Menu> selectByPid(Integer pid);

/**
* 查询所有的菜单
* @return
*/
List<Menu> selectAll();

/**
* 查询除了一级菜单以外的菜单
* @return
*/
List<Menu> selectAllNotBase();
}
  1. mapper文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ratel.shiro.dao.MenuDao">


<select id="selectByPid" resultType="com.ratel.shiro.entity.Menu">
SELECT * FROM menu WHERE pid=#{pid}
</select>

<select id="selectAll" resultType="com.ratel.shiro.entity.Menu">
SELECT * FROM menu
</select>

<select id="selectAllNotBase" resultType="com.ratel.shiro.entity.Menu">
SELECT * FROM menu where pid!= 0
</select>
</mapper>
  1. Controller层(由于是查询操作,并且没有复杂的操作,偷个懒就不写service层)
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
java复制代码@RestController
@RequestMapping("mymenu")
public class MenuController {

@Autowired
private MenuDao menuDao;


@RequestMapping("/getMenuTree")
public List<Menu> getMenuTree(){
List<Menu> menusBase = menuDao.selectByPid(0);
List<Menu> menuLNotBase = menuDao.selectAllNotBase();
for (Menu menu : menusBase) {
List<Menu> menus = iterateMenus(menuLNotBase, menu.getId());
menu.setMenuChildren(menus);
}
return menusBase;
}


/**
*多级菜单查询方法
* @param menuVoList 不包含最高层次菜单的菜单集合
* @param pid 父类id
* @return
*/
public List<Menu> iterateMenus(List<Menu> menuVoList,String pid){
List<Menu> result = new ArrayList<Menu>();
for (Menu menu : menuVoList) {
//获取菜单的id
String menuid = menu.getId();
//获取菜单的父id
String parentid = menu.getPid();
if(StringUtils.isNotBlank(parentid)){
if(parentid.equals(pid)){
//递归查询当前子菜单的子菜单
List<Menu> iterateMenu = iterateMenus(menuVoList,menuid);
menu.setMenuChildren(iterateMenu);
result.add(menu);
}
}
}
return result;
}
}
  1. 启动程序用postman进行测试:
    在这里插入图片描述
    返回的json数据如下:
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
java复制代码[
{
"id": "1",
"name": "主菜单1",
"pid": "0",
"menuChildren": [
{
"id": "4",
"name": "子菜单1.1",
"pid": "1",
"menuChildren": [
{
"id": "6",
"name": "子菜单1.1.1",
"pid": "4",
"menuChildren": []
},
{
"id": "9",
"name": "子菜单1.1.2",
"pid": "4",
"menuChildren": []
}
]
},
{
"id": "5",
"name": "子菜单1.2",
"pid": "1",
"menuChildren": []
}
]
},
{
"id": "2",
"name": "主菜单2",
"pid": "0",
"menuChildren": [
{
"id": "7",
"name": "子菜单2.1",
"pid": "2",
"menuChildren": []
},
{
"id": "8",
"name": "子菜单2.2",
"pid": "2",
"menuChildren": []
}
]
},
{
"id": "3",
"name": "主菜单3",
"pid": "0",
"menuChildren": []
}
]

参考链接:
java递归 处理权限管理菜单树或分类
一次性搞定权限树遍历——–权限树后台遍历的通用解决方案
(java后台)用户权限的多级菜单遍历方法
java 用递归实现球上下级(牵涉到对上级的去重)
java递归获取某个父节点下面的所有子节点
java递归算法总结

本文转载自: 掘金

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

spring声明式事务 Transactional 不回滚

发表于 2021-11-17

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

本文是基于springboot完成测试测试代码地址如下:
github.com/Dr-Water/sp…

一、 spring 事务原理

一、Spring事务原理
在使用JDBC事务操作数据库时,流程如下:

1
2
3
4
5
6
7
8
9
java复制代码//获取连接 
1.Connection con = DriverManager.getConnection()
//开启事务
2.con.setAutoCommit(true/false);
3.执行CRUD
//提交事务/回滚事务
4. con.commit() / con.rollback();
//关闭连接
5. conn.close();

Spring本身并不提供事务,而是对JDBC事务通过AOP做了封装,隐藏了2和4的操作,简化了JDBC的应用。

spring对JDBC事务的封装,是通过AOP动态代理来实现的,在调用目标方法(也就是第3步)前后会通过代理类来执行事务的开启、提交或者回滚操作。
spring事务使用的两个不可忽略点:

  1. 注意关键词 “动态代理”,这意味着要生成一个代理类,那么我们就不能在一个类内直接调用事务方法,否则无法代理,
  2. 而且该事务方法必须是public,如果定义成 protected、private 或者默认可见性,则无法调用!

一@Transactional 应该加到什么地方,如果加到Controller会回滚吗?

  1. @Transactional 最好加到service层,加到Controller层也是生效的,但是为了规范起见,还是加到service层上。
  2. 下载代码并启动难项目进行验证:主要代码如下:
    Controller层代码如下:
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
java复制代码 @Autowired
private TransactionalService transactionalService;

@Autowired
private UserDao userDao;

@Autowired
private JwtUserDao jwtUserDao;

/**
* 测试@Transactional 注解加到service层事务是否回滚
*/
@RequestMapping("/tx")
public void serviceTX(){
transactionalService.controllerTX();
}
/**
* 测试@Transactional 注解加到Controller层事务是否回滚
*/
@Transactional(rollbackFor = Exception.class)
@RequestMapping("/ctx2")
public void cTX2(){
userDao.update();
System.out.println(2/0);
jwtUserDao.update();
}

/**
* 测试@Transactional 注解加到Controller层事务是否回滚
* 这里在Controller层为了方便直接调用了dao层,在实际开发中dao层即可在Controller层调用也可以在service层调用,
* 比如service层只是直接调用dao层一个方法,此外没有任何操作,那么这时候完全不用写service层的方法,直接在Controller调用dao层即可,
* 当然如果公司有规范,必须严格按照mvc的模式进行开发,则另说
*/
@Transactional(rollbackFor = Exception.class)
@RequestMapping("/ctx2")
public void cTX2(){
userDao.update();
//手动抛出一个RuntimeException
System.out.println(2/0);
jwtUserDao.update();
}

service层的主要代码

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码 @Autowired
private UserDao userDao;

@Autowired
private JwtUserDao jwtUserDao;

@Transactional(rollbackFor = Exception.class)
public void controllerTX(){
userDao.update();
//手动抛出一个RuntimeException
System.out.println(2/0);
jwtUserDao.update();
}

dao层sql语句如下:

1
2
3
4
5
6
7
java复制代码 <update id="update">
UPDATE jwt_user SET username ='wangwuupdate' WHERE user_id= 2
</update>

<update id="update">
UPDATE user SET username ='zsupdate' WHERE id= 2
</update>

数据库原始数据:
在这里插入图片描述
在这里插入图片描述
浏览器中输入:http://localhost:8081/tx/tx,由于本次使用测试代码进行统一的异常处理所以浏览器的返回数据如下:
在这里插入图片描述
控制台输出如下:
在这里插入图片描述
查看数据库中的数据并没有被修改
浏览器中输入:http://localhost:8081/tx/ctx2,
在这里插入图片描述
在这里插入图片描述
查看数据库中的数据并没有被修改
由此可以得出 :@Transactional 加到Controller层也是生效的,但是为了规范起见,还是加到service层上。

问题二、 @Transactional 注解中用不用加rollbackFor = Exception.class 这个属性值

spring的api doc中有折磨一句描述:
在这里插入图片描述
红框中的内容如下:

1
java复制代码rolling back on RuntimeException and Error but not on checked exceptions

==大致意思就默认情况下,当程序发生 RuntimeException 和 Error 的这两种异常的时候事务会回滚,但是如果发生了checkedExcetions ,如fileNotfundException 则不会回滚,所以 rollbackFor = Exception.class 这个一定要加!==
验证如下:
浏览器输入:http://localhost:8081/tx/ctx3
控制台输出如下:
在这里插入图片描述
这时候查看数据库中的数据并没有被修改
浏览器输入:http://localhost:8081/tx/ctx4
在这里插入图片描述
这时候查看数据库数据已经被修改:
在这里插入图片描述
在这里插入图片描述

问题三:事务调用嵌套问题具体结果如下代码:

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
java复制代码/**
* 同类中在方法a中调用b
* a没有事务,b有 ,异常发生在b中 不会回滚
*/
@RequestMapping("/a1")
public void a1(){
transactionalService.a1();
}

/**
* 同类中在方法a中调用b
* a没有事务,b有 ,异常发生在a中 不会回滚
*/
@RequestMapping("/a2")
public void a2(){
transactionalService.a2();
}
/**
* 同类中在方法a中调用b
* a有事务,b没有 ,异常发生在b中 会回滚
*/
@RequestMapping("/a3")
public void a3(){
transactionalService.a3();
}
/**
* 同类中在方法a中调用b
* a有事务,b没有 ,异常发生在a中 会回滚
*/
@RequestMapping("/a4")
public void a4(){
transactionalService.a4();
}
/**
* 同类中在方法a中调用b
* a有事务,b也有 ,异常发生在b中 会回滚
*/
@RequestMapping("/a5")
public void a5(){
transactionalService.a5();
}
/**
* 同类中在方法a中调用b
* a有事务,b也有 ,异常发生在a中 会回滚
*/
@RequestMapping("/a6")
public void a6(){
transactionalService.a6();
}


/**
*a类中调用b类中的方法
* a中有事务,b中也有 会回滚
*
*/
@RequestMapping("/b5")
public void b5(){
transactionalService.b5();
}

/**
*a类中调用b类中的方法
* a中有事务,b中没有 会回滚
*
*/
@RequestMapping("/b6")
public void b6(){
transactionalService.b6();
}

/**
*a类中调用b类中的方法
* a没有事务,b中有 不会回滚
*
*/
@RequestMapping("/b7")
public void b7(){
transactionalService.b7();
}

/**
*a类中调用b类中的方法
* a没有事务,b中没有 不会回滚
*
*/
@RequestMapping("/b8")
public void b8(){
transactionalService.b8();
}

总结:如果在a方法中调用b方法不管是不是a和b是不是在同一个类中,只要a方法中没有事务,则发生异常的时候不会回滚,即:当a无事务时,则a和b均没有事务,当a有事务时,b如果有事务,则b事务会加到a事务中,二者为同一事务!

四、总结

在springboot中默认是开启事务的,在service层的方法加上@Transactional(rollbackFor = Exception.class) 注解即可实现事务 如果在方法a中调用方法b 如果要实现事务,则只需要在方法上加上@Transactional(rollbackFor = Exception.class) 即可!
如果业务需要,一定要抛出checked异常的话,可以通过rollbackFor属性指定异常类型即可。有兴趣的可以动手验证一下,这里不再赘述。

五、 参考链接

Spring Boot中的事务管理
深入理解 Spring 之 SpringBoot 事务原理
声明式事务不回滚@Transactional的避坑正确使用
Spring声明式事务不回滚问题
spring 事务应用误区总结:那些导致事务不回滚的坑
你的Spring事务为什么不会自动回滚,包含异常的分类
Java异常之checked与unchecked

本文转载自: 掘金

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

基于Guava API实现异步通知和事件回调

发表于 2021-11-17

本文节选自《设计模式就该这样学》

1 基于Java API实现通知机制

当小伙伴们在社区提问时,如果有设置指定用户回答,则对应的用户就会收到邮件通知,这就是观察者模式的一种应用场景。有些小伙伴可能会想到MQ、异步队列等,其实JDK本身就提供这样的API。我们用代码来还原这样一个应用场景,首先创建GPer类。

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复制代码
/**
* JDK提供的一种观察者的实现方式,被观察者
*/
public class GPer extends Observable{
private String name = "GPer生态圈";
private static GPer gper = null;
private GPer(){}

public static GPer getInstance(){
if(null == gper){
gper = new GPer();
}
return gper;
}
public String getName() {
return name;
}
public void publishQuestion(Question question){
System.out.println(question.getUserName() + "在" + this.name + "上提交了一个问题。");
setChanged();
notifyObservers(question);
}
}

然后创建问题Question类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码
public class Question {
private String userName;
private String content;

public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}
}

接着创建老师Teacher类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码
public class Teacher implements Observer {

private String name;

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

public void update(Observable o, Object arg) {
GPer gper = (GPer)o;
Question question = (Question)arg;
System.out.println("======================");
System.out.println(name + "老师,你好!\n" +
"您收到了一个来自" + gper.getName() + "的提问,希望您解答。问题内容如下:\n" +
question.getContent() + "\n" + "提问者:" + question.getUserName());
}
}

最后编写客户端测试代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码
public static void main(String[] args) {
GPer gper = GPer.getInstance();
Teacher tom = new Teacher("Tom");
Teacher jerry = new Teacher("Jerry");

gper.addObserver(tom);
gper.addObserver(jerry);

//用户行为
Question question = new Question();
question.setUserName("张三");
question.setContent("观察者模式适用于哪些场景?");

gper.publishQuestion(question);
}

运行结果如下图所示。

file

2 基于Guava API轻松落地观察者模式

笔者向大家推荐一个实现观察者模式的非常好用的框架,API使用也非常简单,举个例子,首先引入Maven依赖包。

1
2
3
4
5
6
java复制代码
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
</dependency>

然后创建侦听事件GuavaEvent。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码
/**
* Created by Tom
*/
public class GuavaEvent {
@Subscribe
public void subscribe(String str){
//业务逻辑
System.out.println("执行subscribe方法,传入的参数是:" + str);
}

}

最后编写客户端测试代码。

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

/**
* Created by Tom
*/
public class GuavaEventTest {
public static void main(String[] args) {
EventBus eventbus = new EventBus();
GuavaEvent guavaEvent = new GuavaEvent();
eventbus.register(guavaEvent);
eventbus.post("Tom");
}

}

3 使用观察者模式设计鼠标事件响应API

再来设计一个业务场景,帮助小伙伴们更好地理解观察者模式。在JDK源码中,观察者模式的应用也非常多。例如java.awt.Event就是观察者模式的一种,只不过Java很少被用来写桌面程序。我们用代码来实现一下,以帮助小伙伴们更深刻地了解观察者模式的实现原理。首先,创建EventListener接口。

1
2
3
4
5
6
7
8
java复制代码
/**
* 观察者抽象
* Created by Tom.
*/
public interface EventListener {

}

创建Event类。

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
java复制代码

/**
* 标准事件源格式的定义
* Created by Tom.
*/
public class Event {
//事件源,动作是由谁发出的
private Object source;
//事件触发,要通知谁(观察者)
private EventListener target;
//观察者的回应
private Method callback;
//事件的名称
private String trigger;
//事件的触发事件
private long time;

public Event(EventListener target, Method callback) {
this.target = target;
this.callback = callback;
}

public Object getSource() {
return source;
}

public Event setSource(Object source) {
this.source = source;
return this;
}

public String getTrigger() {
return trigger;
}

public Event setTrigger(String trigger) {
this.trigger = trigger;
return this;
}

public long getTime() {
return time;
}

public Event setTime(long time) {
this.time = time;
return this;
}

public Method getCallback() {
return callback;
}

public EventListener getTarget() {
return target;
}

@Override
public String toString() {
return "Event{" +
"source=" + source +
", target=" + target +
", callback=" + callback +
", trigger='" + trigger + '\'' +
", time=" + time +
'}';
}
}

创建EventContext类。

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
java复制代码
/**
* 被观察者的抽象
* Created by Tom.
*/
public abstract class EventContext {
protected Map<String,Event> events = new HashMap<String,Event>();

public void addListener(String eventType, EventListener target, Method callback){
events.put(eventType,new Event(target,callback));
}

public void addListener(String eventType, EventListener target){
try {
this.addListener(eventType, target,
target.getClass().getMethod("on"+toUpperFirstCase(eventType), Event.class));
}catch (NoSuchMethodException e){
return;
}
}

private String toUpperFirstCase(String eventType) {
char [] chars = eventType.toCharArray();
chars[0] -= 32;
return String.valueOf(chars);
}

private void trigger(Event event){
event.setSource(this);
event.setTime(System.currentTimeMillis());

try {
if (event.getCallback() != null) {
//用反射调用回调函数
event.getCallback().invoke(event.getTarget(), event);
}
}catch (Exception e){
e.printStackTrace();
}
}

protected void trigger(String trigger){
if(!this.events.containsKey(trigger)){return;}
trigger(this.events.get(trigger).setTrigger(trigger));
}
}

然后创建MouseEventType接口。

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
java复制代码
/**
* Created by Tom.
*/
public interface MouseEventType {
//单击
String ON_CLICK = "click";

//双击
String ON_DOUBLE_CLICK = "doubleClick";

//弹起
String ON_UP = "up";

//按下
String ON_DOWN = "down";

//移动
String ON_MOVE = "move";

//滚动
String ON_WHEEL = "wheel";

//悬停
String ON_OVER = "over";

//失去焦点
String ON_BLUR = "blur";

//获得焦点
String ON_FOCUS = "focus";
}

创建Mouse类。

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
java复制代码
/**
* 具体的被观察者
* Created by Tom.
*/
public class Mouse extends EventContext {

public void click(){
System.out.println("调用单击方法");
this.trigger(MouseEventType.ON_CLICK);
}

public void doubleClick(){
System.out.println("调用双击方法");
this.trigger(MouseEventType.ON_DOUBLE_CLICK);
}

public void up(){
System.out.println("调用弹起方法");
this.trigger(MouseEventType.ON_UP);
}

public void down(){
System.out.println("调用按下方法");
this.trigger(MouseEventType.ON_DOWN);
}

public void move(){
System.out.println("调用移动方法");
this.trigger(MouseEventType.ON_MOVE);
}

public void wheel(){
System.out.println("调用滚动方法");
this.trigger(MouseEventType.ON_WHEEL);
}

public void over(){
System.out.println("调用悬停方法");
this.trigger(MouseEventType.ON_OVER);
}

public void blur(){
System.out.println("调用获得焦点方法");
this.trigger(MouseEventType.ON_BLUR);
}

public void focus(){
System.out.println("调用失去焦点方法");
this.trigger(MouseEventType.ON_FOCUS);
}
}

创建回调方法MouseEventLisenter类。

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
java复制代码
/**
* 观察者
* Created by Tom.
*/
public class MouseEventListener implements EventListener {


public void onClick(Event e){
System.out.println("===========触发鼠标单击事件==========" + "\n" + e);
}

public void onDoubleClick(Event e){
System.out.println("===========触发鼠标双击事件==========" + "\n" + e);
}

public void onUp(Event e){
System.out.println("===========触发鼠标弹起事件==========" + "\n" + e);
}

public void onDown(Event e){
System.out.println("===========触发鼠标按下事件==========" + "\n" + e);
}

public void onMove(Event e){
System.out.println("===========触发鼠标移动事件==========" + "\n" + e);
}

public void onWheel(Event e){
System.out.println("===========触发鼠标滚动事件==========" + "\n" + e);
}

public void onOver(Event e){
System.out.println("===========触发鼠标悬停事件==========" + "\n" + e);
}

public void onBlur(Event e){
System.out.println("===========触发鼠标失去焦点事件==========" + "\n" + e);
}

public void onFocus(Event e){
System.out.println("===========触发鼠标获得焦点事件==========" + "\n" + e);
}

}

最后编写客户端测试代码。

1
2
3
4
5
6
7
8
9
10
11
java复制代码
public static void main(String[] args) {
EventListener listener = new MouseEventListener();

Mouse mouse = new Mouse();
mouse.addListener(MouseEventType.ON_CLICK,listener);
mouse.addListener(MouseEventType.ON_MOVE,listener);

mouse.click();
mouse.move();
}

关注『 Tom弹架构 』回复“设计模式”可获取完整源码。

【推荐】Tom弹架构:30个设计模式真实案例(附源码),挑战年薪60W不是梦

本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。关注『 Tom弹架构 』可获取更多技术干货!

本文转载自: 掘金

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

Go小知识 基本语法 Go小知识 基本语法

发表于 2021-11-17

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

Go小知识 | 基本语法

变量定义

使用var 关键字

  • var a,b,c bool
  • var s1,s2 string = “hello”, “world”
  • 可放在函数内,或直接放在包内
  • 使用 var() 集中定义变量

编译器类型推断

  • var a, b, i, s1, s2 = true, false, 3, “hello”, “world”

使用 := 定义变量

  • a, b, i, s1, s2 := true, false, 3, “hello”, “world”
  • 只能在函数内使用
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
go复制代码package main

import "fmt"

// 包全局不能使用 := 定义变量
var (
aa = 3
ss = "kkk"
bb = true
)
// 变量空值
func variableZeroValue () {
var a int
var s string
fmt.Printf("%d %q\n",a,s)
}
// 变量赋值
func variableInitialValue () {
var a, b int = 3,4
var s string = "abc"
fmt.Println(a,b,s)
}
// 类型推断
func variableTypeDeduction () {
var a,b,c,s = 3,4,true,"def"
fmt.Println(a,b,c,s )
}
// := 定义变量
func variableShorter () {
a,b,c,s := 3,4,true,"def"
b = 5 // 改变值
fmt.Println(a,b,c,s )
}
func main(){
fmt.Println("Hello GoLang")
variableZeroValue()
variableInitialValue()
variableTypeDeduction()
variableShorter()
fmt.Println(aa,ss,bb)
}
1
2
3
4
5
6
bash复制代码Hello GoLang
0 ""
3 4 abc
3 4 true def
3 5 true def
3 kkk true

内建变量类型

  • bool, string4
  • (u)int, (u)int8,(u)int16,(u)int32,(u)int64,unitptr(指针) u无符号
  • byte,rune (go 语言字符型32位, 类似于char)
  • float32,float64,complex64,complex128
1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码package main

import (
"fmt"
"math/cmplx"
)
func euler() {
c := 3 + 4i
fmt.Println(cmplx.Abs(c))
}
func main(){
fmt.Println("Hello GoLang")
euler()
}
1
2
bash复制代码Hello GoLang
5

强制类型转换

类型转换是强制的, go 语言中之后强制类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码package main

import (
"fmt"
"math"
)

func triangle () {
var a,b int = 3,4
var c int
c = int(math.Sqrt(float64(a*a + b*b))) // 强制类型转换
fmt.Println(c)
}
func main(){
fmt.Println("Hello GoLang")
triangle()
}

常量的定义

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码package main

import (
"fmt"
"math"
)

func consts() {
const filename = "abc.txt"
const a,b = 3, 4
var c int
c = int(math.Sqrt(a*a+b*b))
fmt.Println(filename,c)
}

func main(){
consts()
}
1
bash复制代码abc.txt 5

枚举类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码package main

import (
"fmt"
)

func enums () {
const(
cpp = 0
java = 1
python = 2
golang = 3
)
fmt.Println(cpp, java, python, golang)
}

func main(){
enums() // 0 1 2 3
}

iota 自增值

自增值枚举类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码package main

import (
"fmt"
)

func enums () {
const(
cpp = iota //自增值
_
java
python
golang
)
fmt.Println(cpp, java, python, golang)
}

func main(){
enums() // 0 2 3 4
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码// 定义 b,kb,mb,gb,tb,pb
package main

import (
"fmt"
)

func enums () {
const(
b = 1<< (10*iota)
kb
mb
gb
tb
pb
)
fmt.Println(b,kb,mb,gb ,tb,pb)
}

func main(){
enums() // 1 1024 1048576 1073741824 1099511627776 1125899906842624
}

本文转载自: 掘金

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

java递归算法的理解,经典算法,优缺点 一、 什么是递归

发表于 2021-11-17

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

一、 什么是递归

  • 递归就是一个程序或函数在其中定义或说明有之间或者间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个原问题相似的规模较小的问题来求解,递归策略只需要少量的程序就可以描述出解题过程所需要的多次重复计算,大大的减少了程序的代码量,递归的能力在于用有限的语句来定义对象的无限集合,一般来说,递归需要边界条件,递归前进段和递归返回段,当边界条件不满足时,递归前进,当边界条件满足时,递归返回。 ==总结一句话:就是自己调用自己,但是要有递归出口,要不就陷入死循环了==
  • 递归通俗理解:
    假设你在一个电影院,你想知道自己坐在哪一排,但是前面人很多,你懒得去数了,于是你问前一排的人「你坐在哪一排?」,这样前面的人 (代号 A) 回答你以后,你就知道自己在哪一排了——只要把 A 的答案加一,就是自己所在的排了,不料 A 比你还懒,他也不想数,于是他也问他前面的人 B「你坐在哪一排?」,这样 A 可以用和你一模一样的步骤知道自己所在的排(==自己调用自己==)。然后 B 也如法炮制,==直到他们这一串人问到了最前面的一排(或者说问到了知道自己是哪一排的人,预示着调用结束 递归的出口)==,第一排的人告诉问问题的人「我在第一排」,最后大家就都知道自己在哪一排了

二、 递归的优缺点

2.1 递归的优点

代码更简洁清晰,可读性好

2.2 递归的缺点

由于递归需要系统堆栈,所以空间消耗要比非递归代码要大很多。而且,如果递归深度太大,可能系统撑不住。

三、递归使用的注意事项

使用递归需要满足以下两种条件:

  1. 有反复执行的过程(自己调用自己)
  2. 有跳出反复执行过程的条件(递归出口)

四、经典的递归算法(实际应用)

4.1 求阶乘 ,例如求5的阶乘 代码实现如下:

求5的阶乘的数学公式:5×4×3×2×1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码   @Test
public void t1() {
System.out.println(jieCheng(5));

}
public int jieCheng(int a) {
//a>1 为递归出口
if (a > 1) {
//jiecheng(a-1) 为自己调用自己
return a * jieCheng(a - 1);
} else {
return a;
}
}

4.2 求 1+2+3+4+…+100的和

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
java复制代码  @Test
public void t2() {
// 结果为5050
// System.out.println(sum(100));
System.out.println(sum2(1, 100));
}

/**
* 方法一
*计算从1到 max 的一次加1的和
* @param max
* @return
*/
public int sum(int max) {
if (max > 1) {
return max + sum(max - 1);
} else {
return max;
}

}

/**
* 方法二
* 计算从min 到 max的一次加1 的和
* 这相较方法1更有普适性
* @param min
* @param max
* @return
*/
public int sum2(int min, int max) {
if (min < max) {
return min + sum2(min += 1, max);
} else {
return min;
}

}

4.3 有一列数 1,1,2,3,5,8,13,21,34…,求用递归算出第30个数是多少

本道题的变种:

  1. 兔子在出生两个月后,就有繁殖能力,一对兔子每个月能生出一对小兔子来。如果所有兔子都不死,那么一年以后可以繁殖多少对兔子?
  2. 斐波那契数列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Test
public void t3() {
// 结果: 832040
System.out.println(count(30));
}


public int count(int i) {
// 递归出口
if (i == 1 || i == 2) {
return 1;
} else {
// 自己调用自己
return count(i - 1) + count(i - 2);
}
}

4.4 爬楼梯算法:已知一个楼梯有n个台阶,每次可以选择迈上一个或者两个台阶,求走完一共有多少种不同的走法

分析一下这个算法:
A:如果有0个台阶,那么有0种走法,这个不用多说;
B:如果有1个台阶,那么有1种走法;
C:如果有2个台阶,那么有2种走法(一次走1个,走两次;一次走两个);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码  @Test
public void t4(){
// 结果:8
System.out.println(climbStairs(5));
}
public int climbStairs(int n ){
// 递归的出口
if(n<=0){
return 0;
}
// 递归的出口
if(n==1){
return 1;
}
// 递归的出口
if(n==2){
return 2;
}
// 自己调用自己
return climbStairs(n-1)+climbStairs(n-2);

}

4.5 汉诺塔

汉诺塔问题是由很多放置在三个塔座上的盘子组成的一个古老的难题,如下图所示,所有的盘子的直径是不同的。并且盘子中央都有一个洞使得它刚好可以放在塔座上,所有的盘子刚开始都是在a座上,这个难题的目标是将左右的盘子都从塔座a,移到塔座c上,每次只可以移动一个盘子,并且任何一个盘子都不可以放置在比它小的盘子上,
在这里插入图片描述
这个问题可以先从简单的方面想,然后一步一步出发,假设有2个盘子,盘子的大小我按照阿拉伯数字命名。从小到大,如果a上面有两个盘子,分别是1,2那么我们只需要把1的盘子移到b上面,然后把2的盘子移到c上面,最后把b上面的盘子1移动到c上面就可以了,这样两个盘子的问题就解决了,
如果是三个盘子呢?我们一样的来命名1,2,3,假设先把1的盘子移动到c的上面,然后把2的盘子移动到b上面,这样目前就是a上面的是3,b上面的是2,c上面的是1,然后将c上面的盘子移动到b上面,继续把a的盘子移动到c上面,这样的话,目前就是b上面有1,2,c上面的有3,现在答案已经很清楚了,将b上面的盘子移动到a上面,然后第二个盘子移动到c上面,最后a的盘子移动到c上面,这样问题就解决了,
但是如果有四个,五个,n个盘子呢?这个时候递归的思想就很好的解决这样的问题了,当只有两个盘子的时候,我们只需要将b塔座作为中介,将盘子1放到中介b上面,然后将盘子2放到c上面,最后将b上面的盘子1移动到c盘就可以了

所以无论多少盘子,我们都将其看做只有两个盘子,假设n个盘子在a的上面,我们将其看做只有两个盘子,只有(n-1)和n这两个盘子,
先将a上面的n-1的哪一个盘子放到塔座b上面,然后将第n的盘子放到目标塔上面,
然后a的上面为空,看做中间塔,b上面的有n-1个盘子,将第n-2以下的盘子看成一个盘子,放到中间a塔上面,然后将第n-1的盘子放到c上面,
这是发现a上面有n-2个盘子,b上面为空,按照上面的方式以此类推,直到全部放到冰箱里面

简单来说:

  1. 从初始塔a上移动到包含n-1个盘子到c上面
  2. 将a上面剩下的一个盘子,放到c上面
  3. 然后假设b上面有n-1个盘子,然后将它看成为n继续将看成n的盘子中间的n-1个盘子放到a上面
  4. 将b上面剩下的那个盘子放到c上面。
    …
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码 public static void main(String[] args) {

move(3,"A","B","C");
}

/**
* 汉诺塔问题
*
* @param dish 盘子个数(也表示名称)
* @param from 初始塔座
* @param temp 中介塔座
* @param to 目标塔座
*/
public static void move(int dish, String from, String temp, String to) {
if (dish == 1) {//圆盘只有一个的时候 将其从a移动到c
System.out.println("将盘子" + dish + "从塔座" + from + "移动到目标塔座" + to);
}else {
move(dish-1,from,to,temp);//a为初始塔座,b为目标塔座,c为中介塔座
System.out.println("将盘子"+dish+"从塔座"+from+"移动到目标"+to);//把a上面的最下面的一个盘子移到c上面
move(dish-1,temp,from,to);//b为初始塔座,c为目标塔座,a为中介塔座
}
}

五、递归在实际项目中的应用

java 递归实现权限树(菜单树)点击打开

参考链接:
java递归算法(一)——详解以及几个经典示例 包含分治算法
Java实现简单的递归操作
Java递归解决“九连环”公式

本文转载自: 掘金

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

1…308309310…956

开发者博客

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