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

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


  • 首页

  • 归档

  • 搜索

java8的新特性之lambda表达式和方法引用

发表于 2021-10-10

1.1. Lambda表达式

通过具体的实例去体会lambda表达式对于我们代码的简化,其实我们不去深究他的底层原理和背景,仅仅从用法上去理解,关注两方面:

  1. lambda表达式是Java8的一个语法糖,用来简化了函数式接口(理解什么是函数式接口)实例的代码量;
  2. 什么是函数式接口,只有在一个接口是函数式接口时候才能使用lambda表达式简化我们的代码;

所以通过以上两个点,我们需要贯彻始终的观念有三点:

  1. 明确函数式接口定义,就是有且只有一个抽象方法的接口就是函数式接口,当然加上@FunctionalInterface 注解更可以确定这个接口是函数式接口;
  2. lambda表达式只能用在函数式接口的实例中,即lambda表达式的语法本质就是函数式接口中那个唯一抽象方法的实现语句;
  3. 因为函数式接口的抽象方法唯一,所以实现(重写)该方法非常明确,不会造成使用了lambda表达式分不清是该接口的哪个方法被重写了,于是我们就可以简化省略各种不必要的语句,比如对数据类型的判断,返回值的判断,大括号之类的,这就是lambda表达式必须在函数式接口中才能使用的原因。

下面我们通过实例,对比没有lambda表达式时候跟有了lambda表达式之后代码的语法糖,以下示例代码包含了lambda表达式的语法规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
typescript复制代码
package com.ethan.lambda;

import org.junit.Test;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Consumer;

/**
* Lambda表达式的使用
*
* 1.语法格式:
* ->:lambda表达式的操作符
* ->左边:lambda的形参列表(其实就是接口中抽象方法的形参列表)
* ->右边:lambda体,(其实就是接口中抽象方法的具体实现)
* 2.lambda表达式的使用,有六种情况
*
* 3.lambda表达式的本质:是作为对应的函数式接口的实例对象!
* 所以记住:
* 1)lambda表达式的返回值都是对应接口的实例对象;
* 2)lambda表达式的语句是对应接口的方法的具体实现;
*/
public class LambdaTest02 {
//情况1:无参,没有返回值
@Test
public void test01(){
Runnable run1 = new Runnable() {
public void run() {
System.out.println("实现Runnable接口的匿名类对象!");
}
};
run1.run();
System.out.println("================*==============");
Runnable run2 = () -> System.out.println("Lambda表达式实现!");
run2.run();
}

//情况2:有一个参数,没有返回值
@Test
public void test02(){
Consumer<String> con1 = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s+"是什么呢?");
}
};
con1.accept("eth");

System.out.println("************************");
Consumer<String> con2 = (String s) -> System.out.println(s+"是什么呢?");
con2.accept("eth和lambda");
}

//情况3:有参数,但是参数的数据类型可以省略,因为编译器可以进行"类型推断"
@Test
public void test03(){

System.out.println("************************");
Consumer<String> con2 = (s) -> System.out.println(s+"是什么呢?");
con2.accept("eth和lambda");

System.out.println("************举例说明类型推断************");
List<String> list = new ArrayList<>();//类型推断ArrayList<这里无需再写数据类型>

int[] arr1 = new int[]{1,2,3};//标准
int[] arr2 = {1,2,3};//进行了类型推断,简化

}

//情况4:若是只有一个参数,参数的小括号可以省略
@Test
public void test04(){

Consumer<String> con1 = (s) -> System.out.println(s+"是什么呢?");
con1.accept("此时有形参的小括号");
System.out.println("***********只有一个参数,可以去掉形参小括号*************");
Consumer<String> con2 = s -> System.out.println(s+"是什么呢?");
con2.accept("此时有形参的小括号");
}

//情况5:Lambda 需要两个或以上的参数,多条执行语句,并且可以有返回值
@Test
public void test05(){
Comparator<String> com1 = new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
System.out.println(o1);
System.out.println(o2);
return o1.compareTo(o2);
}
};
System.out.println(com1.compare("a","c"));
System.out.println("************************");
Comparator<String> com2 = (o1, o2) -> {
System.out.println(o1);
System.out.println(o2);
return o1.compareTo(o2);
};
System.out.println(com1.compare("abc","abcdf"));
}

//情况6:当 Lambda 体只有一条语句时,return 与大括号若有,都可以省略
@Test
public void test06(){
Comparator<String> com = (o1, o2) -> o1.compareTo(o2);
}
}

总结:

  1. 对比了前后的代码,学会lambda表达式的语法;
  2. 初步知道什么是函数式接口;

最重要的一点!!!lambda表达式的本质:是作为对应的函数式接口的实例对象!

所以把握住以下两点进行理解:

  1. lambda表达式的返回值都是对应接口的实例对象;
  2. lambda表达式的语句是对应接口的方法的具体实现;

1.2 函数式(Functional)接口

函数式接口的定义:

  1. 只包含一个抽象方法的接口,称为函数式接口。
  2. 你可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda 表达式抛出一个受检异常(即:非运行时异常),那么该异常需要在目标接口的抽象方法上进行声明)。
  3. 我们可以在一个接口上使用 @FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口。同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。
  4. 在java.util.function包下定义了Java 8 的丰富的函数式接口

如何理解函数式接口

1
2
3
4
markdown复制代码1. Java从诞生日起就是一直倡导“一切皆对象”,在Java里面面向对象(OOP)编程是一切。但是随着python、scala等语言的兴起和新技术的挑战,Java不得不做出调整以便支持更加广泛的技术要求,也即java不但可以支持OOP还可以支持OOF(面向函数编程) 
2. 在函数式编程语言当中,函数被当做一等公民对待。在将函数作为一等公民的编程语言中,Lambda表达式的类型是函数。但是在Java8中,有所不同。在Java8中,Lambda表达式是对象,而不是函数,它们必须依附于一类特别的对象类型——函数式接口。
3. 简单的说,在Java8中,Lambda表达式就是一个函数式接口的实例。这就是Lambda表达式和函数式接口的关系。也就是说,只要一个对象是函数式接口的实例,那么该对象就可以用Lambda表达式来表示。
4. 所以以前用匿名实现类表示的现在都可以用Lambda表达式来写。

1.2.1. Java内置四大核心函数式接口

通过例子对函数式接口和lambda表达式再进行稍微深入的一点理解,慢慢思考消化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
typescript复制代码package com.ethan.lambda;

import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;

/**
*
* java内置的四大核心函数式接口
* 接口名 核心(唯一抽象)方法的作用
* 1.Consumer<T> 对类型为T的对象进行相关操作,包含方法:void accept(T t)
* 2.Supplier<T> 返回类型为T的对象,包含方法: T get()
* 3.Function<T,R> 对类型为T的对象进行操作,并返回结果。结果是R类型的对象。包含方法:R apply(T t)
* 4.Predicate<T> 确定类型为T的对象是否满足某约束条件或者说某种判断规则,并返回boolean值。包含方法: boolean test(T t)
*
*/
public class LambdaTest03 {

/**
* 传统的方式和lambda表达式的对比,传递接口的实例并重写其方法
*/
@Test
public void test01(){
//1.方式1:实例一个接口
Consumer<Double> consumer = new Consumer<Double>() {
@Override
public void accept(Double aDouble) {
System.out.println("方式1:花了" + aDouble + "块钱!");
}
};
happyTime(500,consumer);

System.out.println("============================");
//2.方式2:匿名的形式实例化一个接口
happyTime(500, new Consumer<Double>() {
@Override
public void accept(Double aDouble) {
System.out.println("方式2:花了" + aDouble + "块钱!");
}
});

System.out.println("============================");
//3.方式3:lambda表达式的形式实例化一个接口
happyTime(300,aDouble -> System.out.println("方式3:花了" + aDouble + "块钱!"));
}

/**
* 定义一个方法,其中第二个参数需要传递为一个接口的实例
* @param money
* @param consumer
*/
public void happyTime(double money, Consumer<Double> consumer){
consumer.accept(money);
}
/**
* 传统的方式和lambda表达式的对比,传递接口的实例并重写其方法
*
*/
@Test
public void test02(){

List<String> destList = Arrays.asList("北京","天津","南京","西京","东京","普京","河南","河北","湖南","广东","湖北");
//方式一:仍然以传统的方式实现,不赘述
List<String> strList = filterString(destList, new Predicate<String>() {
@Override
public boolean test(String s) {
return s.contains("京");
}
});
System.out.println(strList);

System.out.println("====================");
//方式二:以lambda表达式实现
List<String> strings = filterString(destList, s -> s.contains("河"));
System.out.println(strings);
}

/**
* 传递一个字符串集合,根据某种规则过滤字符串集合,此规则由Predicate的test方法决定
* @param strList
* @param pre
* @return
*/
public List<String> filterString(List<String> strList, Predicate<String> pre){
List<String> targetList = new ArrayList<>();
for (String s : strList) {
if(pre.test(s)) {
targetList.add(s);
}
}

return targetList;
}
}

1.2.2. Java的其他函数式接口

以下函数式接口是核心四大函数式接口的子接口,其实函数式编程中,函数是一等公民,我们在java中这样理解:

  1. 函数就是函数式接口中的那个唯一的抽象方法;
  2. 其形参参数,我们理解为我们数学中的多元一次方程中的自变量x,y,z,如果有返回值,那么将返回值理解为因变量f(x);
  3. 函数式接口中的唯一的那个抽象方法只需要理解为对一个或多个对象进行相关的操作(这些操作就是我们自己要去写代码实现的,但是为了能够更加简化语法,此时完全可以将这些操作用lambda表达式去实现,扔掉那些不必要的语句,比如数据类型的判断,多余的括号和return关键字)

1.3 方法引用与构造器引用

1.3.1 方法引用

​ 理解方法引用之前,需要注意,其实方法引用需要你对jdk或者你项目中的方法极其熟悉,才能够熟练使用;

​ 建议:真实开发中其实更建议使用lambda表达式,方法引用理解为主。

方法引用的基本理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码
# 1.方法引用的语法格式:类或者对象 :: 方法名
具体分为以下三种情况:
1)对象 :: 非静态方法
2)类 :: 静态方法
3)类 :: 非静态方法(该情况下重点理解非静态方法的调用者(即实例对象)其实是隐藏的形参,或者说非静态方法的形参中隐藏了一个this参数)

# 2.方法引用的使用情景:
当发现需要实现Lambda体的操作(即我们要写的lambda表达式),
其他的接口(java8开始接口可以有默认实现的方法了)或者类已经有实现相同功能的方法
此时就可以使用方法引用!!!其他情况下目前不能使用。
也就是说,使用方法引用的前提条件:
1)在能够使用lambda表达式的前提下
2)某个方法的参数类型和参数个数,返回值类型以及方法体都跟我们要写的lambda表达式一致(适用于情况1和2)

# 3.方法引用的理解:
本质上就是lambda表达式,而lambda表达式就是作为函数式接口的一个实例对象,所以方法引用,也是函数式接口的实例对象。
可以将方法引用理解为在我们需要实现lambda表达式时候,
将已经存在的其他类的某个方法(注意这个方法的功能与我们要实现的lambda表达式的功能一致)调用过来(拿来主义),替代我们要写的lambda表达式
可以这样认为:在函数(方法)的层面上消除冗余重复的方法体

总结:

  • 1
    复制代码 JDK在Lambda表达式的基础上提出了方法引用的概念,允许我们复用当前项目(或JDK源码)中已经存在的且逻辑相同的方法。
  • 1
    复制代码 即,如果已经存在某个方法能完成你的需求,那么你连Lambda表达式都别写了,直接引用这个方法吧。

示例中理解方法引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
csharp复制代码package com.ethan.methodReferences;

public class MethodRefTest {
/**
* 情况一:对象 :: 实例方法
* 我们知道Consumer是函数式接口,有唯一一个抽象方法void accept(T t)
* 我们又发现PrintStream中的void println(T t)方法,不论是返回值类型,形参类型和形参个数
* 都跟Consumer接口的void accept(T t)方法一样,唯一不同的是方法名不一样,并且假如我们要实现void accept(T t)方法
* 如果实现的功能(方法体)又跟PrintStream中的void println(T t)方法实现的一样,那么此时我们就可以使用方法引用
* 在函数(方法)层面去减少相同功能方法的重复书写,直接调用PrintStream中的void println(T t)方法来代替我们要实现void accept(T t)方法
* 对于一个方法的完整定义来说,这两个方法的方法名不一样无关大雅,但是其他的定义都完全一致,实现的功能完全一致。所以可以替换。
* 这就是方法引用的意义,从函数方法层面去消除重复代码,实现相同功能。
*/
@Test
public void test1() {
//lambda表达式
Consumer<String> con1 = s -> System.out.println(s);
con1.accept("北京");
System.out.println("======================");
//方法引用
Consumer<String> con2 = System.out::println;
con2.accept("beijing");

}

/**
* Supplier中的T get()
* Employee中的String getName()
*/
@Test
public void test2() {
Employee emp = new Employee(11,"ETHAN",22,30000);
Supplier<String> sup1 = ()-> emp.getName();
System.out.println(sup1.get());
System.out.println("======================");
//方法引用
Supplier<String> sup2 = emp::getName;
System.out.println(sup2.get());

}

/**
* 情况二:类 :: 静态方法
* Comparator中的int compare(T t1,T t2)
* Integer中的int compare(T t1,T t2)
*/
@Test
public void test3() {

Comparator<Integer> com1 = (t1, t2)-> Integer.compare(t1,t2);
System.out.println(com1.compare(12, 22));
System.out.println("========================");

/**
* 这里有个疑惑,我们说方法引用的条件是接口的抽象方法的返回值跟形参类型和个数都要一致,
* 但是尝试Comparator接口的int compare(T t1,T t2)方法,引用Integer的compareTo方法仍然成功
* 情况3会解惑
*/
//Comparator<Integer> com2 = Integer ::compareTo;
Comparator<Integer> com2 = Integer ::compare;
System.out.println(com2.compare(13,111));
}



/**
* Function中的R apply(T t)
* Math中的Long round(Double d)
*/
@Test
public void test4() {
Function<Double,Long> fun1 = aDouble -> Math.round(aDouble);
System.out.println(fun1.apply(12.3));
System.out.println("========================");
Function<Double,Long> fun2 = Math::round;
System.out.println(fun2.apply(12.6));
}



/**
* 情况三:类 :: 实例方法
* 这种情况比较少见,一般其实自己写lambda表达式就挺好的。
*
* Comparator中的int compare(T t1,T t2)
* String中的int t1.compareTo(t2)
* 上面跟我们一开始理解的方法引用的适用条件:
* 即某个方法的参数类型和参数个数,返回值类型以及方法体都跟我们要写的lambda表达式一致(适用于以下情况1和2)
* 不一致!!!如何理解:
* 首先我们要有一个概念,非静态方法的形参其实是隐含了一个形参变量,那就是我们这个非静态方法的实例对象this
* 所以Comparator中的int comapre(T t1,T t2)可以将String中的int t1.compareTo(t2)进行方法引用,就是因为
* String中的int t1.compareTo(t2)方法其实隐含了一个形参this(其实就是t1),
* 那么加上的this就符合方法引用的条件,
* Comparator中的int compare(T t1,T t2)
* String中的int t1.compareTo(this(t1),t2)(this其实也就是Comparator中的int comapre(T t1,T t2)的t1形参。
*
*
* 这种情况下,可以这样理解:
* 方法引用的方法的调用者t1,其实就是函数式接口的抽象方法中的第一个形参。
*/
@Test
public void test5() {


Comparator<String> c1 = (s1,s2)-> s1.compareTo(s2);
System.out.println(c1.compare("abr","abc"));

System.out.println("-==============================");
Comparator<String> c2 = String::compareTo;
System.out.println(c2.compare("abr","abc"));
}



/**
*
* BiPredicate中的boolean test(T t1, T t2);
* String中的boolean t1.equals(t2)
*/
@Test
public void test6() {
BiPredicate<String,String> bp1 = (s1,s2)->s1.equals(s2);
System.out.println(bp1.test("aka","aba"));
System.out.println("==============================");
BiPredicate<String,String> bp2 = String::equals;
System.out.println(bp2.test("aka","aka"));
}



/**
* Function中的R apply(T t)
* Employee中的String getName();
*
* 变形理解:
* Function中的R apply(T t) ================================> Function中的R apply(T t);
* Employee中的emp.getName() ===getName隐藏了一个形参this(emp)==> Employee中的String getName(Employee emp);
*/
@Test
public void test7() {
Employee emp = new Employee(11,"ETHAN",22,30000);
Function<Employee,String> fun1 = employee -> employee.getName();
System.out.println(fun1.apply(emp));
System.out.println("==============================");
Function<Employee,String> fun2 = Employee::getName;
System.out.println(fun2.apply(emp));
}

}


//省略 get/set 构造器
class Employee {

private int id;
private String name;
private int age;
private double salary;
}

1.3.2 构造器引用和数组引用

一、构造器引用

格式: ClassName::new

与函数式接口相结合,自动与函数式接口中方法兼容。

  • 和方法引用类似,函数式接口的抽象方法的形参列表和构造器的形参列表一致。即对应抽象方法形参列表的构造器必须存在。
  • 抽象方法的返回值类型即为构造器所属类的类型

二、数组引用

格式: type[] :: new

  • 将数组看作一个特殊的类,即与构造器引用类型了。
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
typescript复制代码package com.ethan.methodReferences;

import org.junit.Test;

import java.util.Arrays;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;


public class ConstructorRefTest {


/**
* 构造器引用
* Supplier中的T get()
* Employee中的 new Employee();
*/
@Test
public void test1(){
//最基础的方式
Supplier<Employee> sup1 = new Supplier<Employee>() {
@Override
public Employee get() {
return new Employee();
}
};
System.out.println(sup1.get());

System.out.println("========================");
//lambda表达式的方式
Supplier<Employee> sup2 = ()-> new Employee();
System.out.println(sup2.get());

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

//构造器引用的方式
Supplier<Employee> sup3 = Employee::new;
System.out.println(sup3.get());

}

//Function中的R apply(T t)
@Test
public void test2(){
Function<Integer,Employee> fun1 = id-> new Employee(id);
System.out.println(fun1.apply(1001));
System.out.println("========================");
Function<Integer,Employee> fun2 = Employee::new;
System.out.println(fun1.apply(1002));

}

//BiFunction中的R apply(T t,U u)
@Test
public void test3(){
BiFunction<Integer,String,Employee> bf1 = (id,name)->new Employee(id,name);
System.out.println(bf1.apply(1001,"ethan"));

System.out.println("=====================");
BiFunction<Integer,String,Employee> bf2 = Employee::new;
System.out.println(bf2.apply(1002,"ethal"));
}

//数组引用
//Function中的R apply(T t)
@Test
public void test4(){
Function<Integer,String[]> fun1 = len -> new String[len];
String[] strArr1 = fun1.apply(5);
System.out.println(Arrays.toString(strArr1));
System.out.println("=====================");
Function<Integer,String[]> fun2 = String[] ::new;
String[] strArr2 = fun2.apply(10);
System.out.println(Arrays.toString(strArr2));

}
}

本文转载自: 掘金

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

马拉车算法,其实并不难!!!

发表于 2021-10-10

要说马拉车算法,必须说说这道题,查找最长回文子串,马拉车算法是其中一种解法,狠人话不多,直接往下看:

题目描述

给你一个字符串 s,找到 s 中最长的回文子串。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
txt复制代码示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

示例 2:
输入:s = "cbbd"
输出:"bb"

示例 3:
输入:s = "a"
输出:"a"

示例 4:
输入:s = "ac"
输出:"a"

马拉车算法

这是一个奇妙的算法,是1957年一个叫Manacher的人发明的,所以叫Manacher‘s Algorithm,主要是用来查找一个字符串的最长回文子串,这个算法最大的贡献是将时间复杂度提升到线性,前面我们说的动态规划的时间复杂度为 O(n2)。

前面说的中心拓展法,中心可能是字符也可能是字符的间隙,这样如果有 n 个字符,就有 n+n+1 个中心:

为了解决上面说的中心可能是间隙的问题,我们往每个字符间隙插入”#“,为了让拓展结束边界更加清晰,左边的边界插入”^“,右边的边界插入 “$“:

S 表示插入”#“,”^“,”$“等符号之后的字符串,我们用一个数组P表示S中每一个字符能够往两边拓展的长度:

比如 P[8] = 3,表示可以往两边分别拓展3个字符,也就是回文串的长度为 3,去掉 # 之后的字符串为aca:

P[11]= 4,表示可以往两边分别拓展4个字符,也就是回文串的长度为 4,去掉 # 之后的字符串为caac:

假设我们已经得知数组P,那么我们怎么得到回文串?

用 P 的下标 index ,减去 P[i](也就是回文串的长度),可以得到回文串开头字符在拓展后的字符串 S 中的下标,除以2,就可以得到在原字符串中的下标了。

那么现在的问题是:如何求解数组P[i]

其实,马拉车算法的关键是:它充分利用了回文串的对称性,用已有的结果来帮助计算后续的结果。

假设已经计算出字符索引位置 P 的最大回文串,左边界是PL,右边界是PR:

那么当我们求因为一个位置 i 的时候,i 小于等于 PR,其实我们可以找到 i 关于 P 的对称点 j:

那么假设 j 为中心的最长回文串长度为 len,并且在 PL 到 P 的范围内,则 i 为中心的最长回文串也是如此:

以 i 为中心的最长回文子串长度等于以 j 为中心的最长回文子串的长度

但是这里有两个问题:

  • 前一个回文字符串P,是哪一个?
  • 有哪些特殊情况?特殊情况怎么处理?

(1) 前一个回文字符串 P,是指的前面计算出来的右边界最靠右的回文串,因为这样它最可能覆盖我们现在要计算的 i 为中心的索引,可以尽量重用之前的结果的对称性。

也正因为如此,我们在计算的时候,需要不断保存更新 P 的中心和右边界,用于每一次计算。

(2) 特殊情况其实就是当前 i 的最长回文字符串计算不能再利用 P 点的对称,例如:

  1. 以 i 的回文串的右边界超出了 P 的右边界 PR:

这种情况的解决方案是:超过的部分,需要按照中心拓展法来一一拓展。

  1. i 不在 以 P 为中心的回文串里面,只能按照中心拓展法来处理。

具体的代码实现如下:

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
java复制代码    // 构造字符串
public String preProcess(String s) {
int n = s.length();
if (n == 0) {
return "^$";
}
String ret = "^";
for (int i = 0; i < n; i++)
ret = ret + "#" + s.charAt(i);
ret = ret + "#$";
return ret;
}

// 马拉车算法
public String longestPalindrome(String str) {
String S = preProcess(str);
int n = S.length();
// 保存回文串的长度
int[] P = new int[n];
// 保存边界最右的回文中心以及右边界
int center = 0, right = 0;
// 从第 1 个字符开始
for (int i = 1; i < n - 1; i++) {
// 找出i关于前面中心的对称
int mirror = 2 * center - i;
if (right > i) {
// i 在右边界的范围内,看看i的对称点的回文串长度,以及i到右边界的长度,取两个较小的那个
// 不能溢出之前的边界,否则就得中心拓展
P[i] = Math.min(right - i, P[mirror]);
} else {
// 超过范围了,中心拓展
P[i] = 0;
}

// 中心拓展
while (S.charAt(i + 1 + P[i]) == S.charAt(i - 1 - P[i])) {
P[i]++;
}

// 看看新的索引是不是比之前保存的最右边界的回文串还要靠右
if (i + P[i] > right) {
// 更新中心
center = i;
// 更新右边界
right = i + P[i];
}

}

// 通过回文长度数组找出最长的回文串
int maxLen = 0;
int centerIndex = 0;
for (int i = 1; i < n - 1; i++) {
if (P[i] > maxLen) {
maxLen = P[i];
centerIndex = i;
}
}
int start = (centerIndex - maxLen) / 2;
return str.substring(start, start + maxLen);
}

至于算法的复杂度,空间复杂度借助了大小为n的数组,为O(n),而时间复杂度,看似是用了两层循环,实则不是 O(n2),而是 O(n),因为绝大多数索引位置会直接利用前面的结果以及对称性获得结果,常数次就可以得到结果,而那些需要中心拓展的,是因为超出前面结果覆盖的范围,才需要拓展,拓展所得的结果,有利于下一个索引位置的计算,因此拓展实际上较少。

【作者简介】:

秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。

剑指Offer全部题解PDF

2020年我写了什么?

开源编程笔记

本文转载自: 掘金

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

MySql基础知识总结(索引篇)

发表于 2021-10-10

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

一、MySQL三层逻辑架构

MySQL的存储引擎架构将查询处理与数据的存储/提取相分离。下面是MySQL的逻辑架构图:

三层.png

1、第一层负责连接管理、授权认证、安全等等。
每个客户端的连接都对应着服务器上的一个线程。服务器上维护了一个线程池,避免为每个连接都创建销毁一个线程。当客户端连接到MySQL服务器时,服务器对其进行认证。可以通过用户名和密码的方式进行认证,也可以通过SSL证书进行认证。登录认证通过后,服务器还会验证该客户端是否有执行某个查询的权限。

2、第二层负责解析查询
编译SQL,并对其进行优化(如调整表的读取顺序,选择合适的索引等)。对于SELECT语句,在解析查询前,服务器会先检查查询缓存,如果能在其中找到对应的查询结果,则无需再进行查询解析、优化等过程,直接返回查询结果。存储过程、触发器、视图等都在这一层实现。

3、第三层是存储引擎
存储引擎负责在MySQL中存储数据、提取数据、开启一个事务等等。存储引擎通过API与上层进行通信,这些API屏蔽了不同存储引擎之间的差异,使得这些差异对上层查询过程透明。存储引擎不会去解析SQL。

二、对比InnoDB与MyISAM

1、 存储结构
MyISAM:每个MyISAM在磁盘上存储成三个文件。分别为:表定义文件、数据文件、索引文件。第一个文件的名字以表的名字开始,扩展名指出文件类型。.frm文件存储表定义。数据文件的扩展名为.MYD (MYData)。索引文件的扩展名是.MYI (MYIndex)。

InnoDB:所有的表都保存在同一个数据文件中(也可能是多个文件,或者是独立的表空间文件),InnoDB表的大小只受限于操作系统文件的大小,一般为2GB。

2、 存储空间
MyISAM: MyISAM支持支持三种不同的存储格式:静态表(默认,但是注意数据末尾不能有空格,会被去掉)、动态表、压缩表。当表在创建之后并导入数据之后,不会再进行修改操作,可以使用压缩表,极大的减少磁盘的空间占用。

InnoDB: 需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引。

3、 可移植性、备份及恢复
MyISAM:数据是以文件的形式存储,所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作。

InnoDB:免费的方案可以是拷贝数据文件、备份 binlog,或者用 mysqldump,在数据量达到几十G的时候就相对痛苦了。

4、 事务支持
MyISAM:强调的是性能,每次查询具有原子性,其执行数度比InnoDB类型更快,但是不提供事务支持。

InnoDB:提供事务支持事务,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。

5、 AUTO_INCREMENT
MyISAM:可以和其他字段一起建立联合索引。引擎的自动增长列必须是索引,如果是组合索引,自动增长可以不是第一列,他可以根据前面几列进行排序后递增。

InnoDB:InnoDB中必须包含只有该字段的索引。引擎的自动增长列必须是索引,如果是组合索引也必须是组合索引的第一列。

6、 表锁差异
MyISAM: 只支持表级锁,用户在操作myisam表时,select,update,delete,insert语句都会给表自动加锁,如果加锁以后的表满足insert并发的情况下,可以在表的尾部插入新的数据。

InnoDB: 支持事务和行级锁,是innodb的最大特色。行锁大幅度提高了多用户并发操作的新能。但是InnoDB的行锁,只是在WHERE的主键是有效的,非主键的WHERE都会锁全表的。

7、 全文索引
MyISAM:支持 FULLTEXT类型的全文索引

InnoDB:不支持FULLTEXT类型的全文索引,但是innodb可以使用sphinx插件支持全文索引,并且效果更好。

8、表主键
MyISAM:允许没有任何索引和主键的表存在,索引都是保存行的地址。

InnoDB:如果没有设定主键或者非空唯一索引,就会自动生成一个6字节的主键(用户不可见),数据是主索引的一部分,附加索引保存的是主索引的值。

9、表的具体行数
MyISAM: 保存有表的总行数,如果select count() from table;会直接取出出该值。

InnoDB: 没有保存表的总行数,如果使用select count(*) from table;就会遍历整个表,消耗相当大,但是在加了wehre条件后,myisam和innodb处理的方式都一样。

10、CRUD操作
MyISAM:如果执行大量的SELECT,MyISAM是更好的选择。

InnoDB:如果你的数据执行大量的INSERT或UPDATE,出于性能方面的考虑,应该使用InnoDB表。

11、 外键
MyISAM:不支持

InnoDB:支持

三、sql优化简介

1、什么情况下进行sql优化

性能低、执行时间太长、等待时间太长、连接查询、索引失效。

2、sql语句执行过程

(1)编写过程
select distinct ... from ... join ... on ... where ... group by ... having ... order by ... limit ...

(2)解析过程
from ... on ... join ... where ... group by ... having ... select distinct ... order by ... limit ...

3、sql优化就是优化索引

索引相当于书的目录。

索引的数据结构是B+树。

四、索引
1、索引的优势
(1)提高查询效率(降低IO使用率)

(2)降低CPU使用率

比如查询order by age desc,因为B+索引树本身就是排好序的,所以再查询如果触发索引,就不用再重新查询了。

2、索引的弊端
(1)索引本身很大,可以存放在内存或硬盘上,通常存储在硬盘上。

(2)索引不是所有情况都使用,比如①少量数据②频繁变化的字段③很少使用的字段

(3)索引会降低增删改的效率

3、索引的分类
(1)单值索引

(2)唯一索引

(3)联合索引

(4)主键索引

备注:唯一索引和主键索引唯一的区别:主键索引不能为null

4、创建索引

alter table user add INDEX user_index_username_password (username,password)

4.png

5、MySQL索引原理 -> B+树
MySQL索引的底层数据结构是B+树

B+Tree是在B-Tree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是用B+Tree实现其索引结构。

B-Tree结构图中每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。

B+Tree相对于B-Tree有几点不同:

非叶子节点只存储键值信息。
所有叶子节点之间都有一个链指针。
数据记录都存放在叶子节点中。
将上一节中的B-Tree优化,由于B+Tree的非叶子节点只存储键值信息,假设每个磁盘块能存储4个键值及指针信息,则变成B+Tree后其结构如下图所示:

3.png

通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对B+Tree进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。

可能上面例子中只有22条数据记录,看不出B+Tree的优点,下面做一个推算:

InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为〖10〗^3)。也就是说一个深度为3的B+Tree索引可以维护10^3 * 10^3 * 10^3 = 10亿 条记录。

实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在24层。MySQL的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要13次磁盘I/O操作。

数据库中的B+Tree索引可以分为聚集索引(clustered index)和辅助索引(secondary index)。上面的B+Tree示例图在数据库中的实现即为聚集索引,聚集索引的B+Tree中的叶子节点存放的是整张表的行记录数据。辅助索引与聚集索引的区别在于辅助索引的叶子节点并不包含行记录的全部数据,而是存储相应行数据的聚集索引键,即主键。当通过辅助索引来查询数据时,InnoDB存储引擎会遍历辅助索引找到主键,然后再通过主键在聚集索引中找到完整的行记录数据。

五、如何触发联合索引

1、对user表建立联合索引username、password

2.png

2、触发联合索引

(1)使用联合索引的全部索引键可触发联合索引

5.png

(2)使用联合索引的全部索引键,但是用or连接的,不可触发联合索引

11111.png

(3)单独使用联合索引的左边第一个字段时,可触发联合索引

7.png

(4)单独使用联合索引的其它字段时,不可触发联合索引

单独.png

六、分析sql的执行计划—explain

explain可以模拟sql优化执行sql语句。

1、explan使用简介

(1)用户表

8.png

(2)部门表

9.png

(3)未触发索引

10.png

(4)触发索引

11.png

(5)结果分析

explain中第一行出现的表是驱动表。

  1. 指定了联接条件时,满足查询条件的记录行数少的表为[驱动表]
  2. 未指定联接条件时,行数少的表为[驱动表]

对驱动表直接进行排序就会触发索引,对非驱动表进行排序不会触发索引。

2、explain查询结果简介
(1)id:SELECT识别符。这是SELECT的查询序列号。

(2)select_type:SELECT类型:

SIMPLE: 简单SELECT(不使用UNION或子查询)
PRIMARY: 最外面的SELECT
UNION:UNION中的第二个或后面的SELECT语句
DEPENDENT UNION:UNION中的第二个或后面的SELECT语句,取决于外面的查询
UNION RESULT:UNION的结果
SUBQUERY:子查询中的第一个SELECT
DEPENDENT SUBQUERY:子查询中的第一个SELECT,取决于外面的查询
DERIVED:导出表的SELECT(FROM子句的子查询)
(3)table:表名

(4)type:联接类型

system:表仅有一行(=系统表)。这是const联接类型的一个特例。
const:表最多有一个匹配行,它将在查询开始时被读取。因为仅有一行,在这行的列值可被优化器剩余部分认为是常数。const用于用常数值比较PRIMARY KEY或UNIQUE索引的所有部分时。
eq_ref:对于每个来自于前面的表的行组合,从该表中读取一行。这可能是最好的联接类型,除了const类型。它用在一个索引的所有部分被联接使用并且索引是UNIQUE或PRIMARY KEY。eq_ref可以用于使用= 操作符比较的带索引的列。比较值可以为常量或一个使用在该表前面所读取的表的列的表达式。
ref:对于每个来自于前面的表的行组合,所有有匹配索引值的行将从这张表中读取。如果联接只使用键的最左边的前缀,或如果键不是UNIQUE或PRIMARY KEY(换句话说,如果联接不能基于关键字选择单个行的话),则使用ref。如果使用的键仅仅匹配少量行,该联接类型是不错的。ref可以用于使用=或<=>操作符的带索引的列。
ref_or_null:该联接类型如同ref,但是添加了MySQL可以专门搜索包含NULL值的行。在解决子查询中经常使用该联接类型的优化。
index_merge:该联接类型表示使用了索引合并优化方法。在这种情况下,key列包含了使用的索引的清单,key_len包含了使用的索引的最长的关键元素。
unique_subquery:该类型替换了下面形式的IN子查询的ref:value IN (SELECT primary_key FROMsingle_table WHERE some_expr);unique_subquery是一个索引查找函数,可以完全替换子查询,效率更高。
index_subquery:该联接类型类似于unique_subquery。可以替换IN子查询,但只适合下列形式的子查询中的非唯一索引:value IN (SELECT key_column FROM single_table WHERE some_expr)
range:只检索给定范围的行,使用一个索引来选择行。key列显示使用了哪个索引。key_len包含所使用索引的最长关键元素。在该类型中ref列为NULL。当使用=、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN或者IN操作符,用常量比较关键字列时,可以使用range
index:该联接类型与ALL相同,除了只有索引树被扫描。这通常比ALL快,因为索引文件通常比数据文件小。
all:对于每个来自于先前的表的行组合,进行完整的表扫描。如果表是第一个没标记const的表,这通常不好,并且通常在它情况下很差。通常可以增加更多的索引而不要使用ALL,使得行能基于前面的表中的常数值或列值被检索出。
(5)possible_keys:possible_keys列指出MySQL能使用哪个索引在该表中找到行。注意,该列完全独立于EXPLAIN输出所示的表的次序。这意味着在possible_keys中的某些键实际上不能按生成的表次序使用。

(6)key:key列显示MySQL实际决定使用的键(索引)。如果没有选择索引,键是NULL。要想强制MySQL使用或忽视possible_keys列中的索引,在查询中使用FORCE INDEX、USE INDEX或者IGNORE INDEX。

(7)key_len:key_len列显示MySQL决定使用的键长度。如果键是NULL,则长度为NULL。注意通过key_len值我们可以确定MySQL将实际使用一个多部关键字的几个部分。

(8)ref:ref列显示使用哪个列或常数与key一起从表中选择行。

(9)rows:rows列显示MySQL认为它执行查询时必须检查的行数。

(10)Extra:该列包含MySQL解决查询的详细信息。

    • Distinct:MySQL发现第1个匹配行后,停止为当前的行组合搜索更多的行。
    • Not exists:MySQL能够对查询进行LEFT JOIN优化,发现1个匹配LEFT JOIN标准的行后,不再为前面的的行组合在该表内检查更多的行。
    • range checked for each record (index map: #):MySQL没有发现好的可以使用的索引,但发现如果来自前面的表的列值已知,可能部分索引可以使用。对前面的表的每个行组合,MySQL检查是否可以使用range或index_merge访问方法来索取行。
    • Using filesort:MySQL需要额外的一次传递,以找出如何按排序顺序检索行。通过根据联接类型浏览所有行并为所有匹配WHERE子句的行保存排序关键字和行的指针来完成排序。然后关键字被排序,并按排序顺序检索行。
    • Using index:从只使用索引树中的信息而不需要进一步搜索读取实际的行来检索表中的列信息。当查询只使用作为单一索引一部分的列时,可以使用该策略。
    • Using temporary:为了解决查询,MySQL需要创建一个临时表来容纳结果。典型情况如查询包含可以按不同情况列出列的GROUP BY和ORDER BY子句时。
    • Using where:WHERE子句用于限制哪一个行匹配下一个表或发送到客户。除非你专门从表中索取或检查所有行,如果Extra值不为Using where并且表联接类型为ALL或index,查询可能会有一些错误。
    • Using sort_union(…), Using union(…), Using intersect(…):这些函数说明如何为index_merge联接类型合并索引扫描。
    • Using index for group-by:类似于访问表的Using index方式,Using index for group-by表示MySQL发现了一个索引,可以用来查询GROUP BY或DISTINCT查询的所有列,而不要额外搜索硬盘访问实际的表。并且,按最有效的方式使用索引,以便对于每个组,只读取少量索引条目。
      通过相乘EXPLAIN输出的rows列的所有值,你能得到一个关于一个联接如何的提示。这应该粗略地告诉你MySQL必须检查多少行以执行查询。当你使用max_join_size变量限制查询时,也用这个乘积来确定执行哪个多表SELECT语句。

🍅 简介:Java领域优质创作者🏆、Java架构师奋斗者💪

🍅 有兴趣的可以加小编微信,一起学习一起进步guo_rui_

🍅 迎欢点赞 👍 收藏 ⭐留言 📝

「欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章」。

本文转载自: 掘金

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

听说你还在自己做重复劳动?看我一键生成错误码映射

发表于 2021-10-10

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

大家在工作中定义错误码的时候都是如何处理的? xdm 可以评论区交流交流

我看到过有的是这样定义错误码的:

1
2
3
4
5
go复制代码m := make(map[int]string)
m[0] = "OK"
m[1] = "链接失败"
m[2] = "文件类型错误"
...

还看到过这样定义错误码的:

1
2
3
4
5
6
7
8
9
10
go复制代码type myErr struct {
code int
err error
extMsg interface{}
}

myOk := myErr{0,errors.New("PROCESS OK"),"xxx"}
myOk := myErr{1,errors.New("文件类型错误"),"xxx"}
myOk := myErr{2,errors.New("连接数据库失败"),"xxx"}
...

也有见到过这样做的:

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码const (
ERR_OK = 0
ERR_CONN_REFUSE = 1
ERR_FILE_NOT = 2
)


var m = map[int]string{
ERR_OK: "OK",
ERR_CONN_REFUSE: "链接被拒绝",
ERR_FILE_NOT: "文件不存在",
}

现在有一个更好的方法来实现我们工作中错误码的映射

引入 go generate

咱们引入 go generate ,可以只用定义错误码和写注释,就可以达到,当我们调用错误码的时候,能够正确的输出我们想要的错误信息

举个例子:

我们先建立如下目录,将错误码文件 errcode.go,放在一个单独的包里面

1
2
3
4
5
shell复制代码.
├── go.mod
├── main.go
└── mycodes
└── errcode.go

我们还需要运用 stringer 工具,来辅助我们完成这个目标

1
shell复制代码go get golang.org/x/tools/cmd/stringer

我们来看看上述文件的内容:

./mycodes/errcode.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
shell复制代码/*
____ ___ _____ ___________
\ \/ / / \\__ ___/
\ / / \ / \ | |
/ \ / Y \| |
/___/\ \\____|__ /|____|
\_/ \/
createTime:2021/10/10
createUser:Administrator
*/
package mycodes

type ErrCode int64 //错误码
const (
ERR_CODE_OK ErrCode = 0 // PROCESS OK
ERR_CODE_INVALID_PARAMS ErrCode = 1 // 参数无效
ERR_CODE_TIMEOUT ErrCode = 2 // 超时
ERR_CODE_FILE_NOT_EXIST ErrCode = 3 // 文件不存在
ERR_CODE_CONN_REFUSE ErrCode = 4 // 连接被拒绝
ERR_CODE_NET_ABNORMAL ErrCode = 5 // 网络异常
)

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
shell复制代码/*
____ ___ _____ ___________
\ \/ / / \\__ ___/
\ / / \ / \ | |
/ \ / Y \| |
/___/\ \\____|__ /|____|
\_/ \/
createTime:2021/10/10
createUser:Administrator
*/
package main

import (
"fmt"
"myerr/mycodes"
)

func main() {
fmt.Println(mycodes.ERR_CODE_CONN_REFUSE)
}

我们在 main.go 统计目录下初始化一下 go 的 模块, go mod init myerr

go.mod

1
2
3
shell复制代码module myerr

go 1.15

开始演示

我们直接在 main.go 的同级目录下执行 go run main.go,输出如下:

1
shell复制代码4

是 ERR_CODE_CONN_REFUSE 对应的枚举值 4 ,可是我们期望的课不是这个,我们是期望能直接输出错误码对应的错误信息

使用 stringer

手动在 mycodes 目录下使用刚才安装的 stringer 工具

1
shell复制代码stringer -linecomment -type ErrCode

此处的 ErrCode 是错误码的类型 , 执行上述语句后,mycodes 生成了一个文件 errcode_string.go ,现在目录结构是这样的

1
2
3
4
5
6
shell复制代码.
├── go.mod
├── main.go
└── mycodes
├── errcode.go
└── errcode_string.go

看看 errcode_string.go 内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
shell复制代码// Code generated by "stringer -linecomment -type ErrCode"; DO NOT EDIT.

package mycodes

import "strconv"

const _ErrCode_name = "PROCESS OK参数无效超时文件不存在连接被拒绝网络异常"

var _ErrCode_index = [...]uint8{0, 10, 22, 28, 43, 58, 70}

func (i ErrCode) String() string {
if i < 0 || i >= ErrCode(len(_ErrCode_index)-1) {
return "ErrCode(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _ErrCode_name[_ErrCode_index[i]:_ErrCode_index[i+1]]
}

我们可以看出 stringer 工具,将我们的错误码信息映射的字符串全部合并起来放在 _ErrCode_name 常量中,且有 _ErrCode_index 来作为每一个错误码映射字符串的索引值 ,最终便能实现错误码和字符串的映射,这个就很简单吧

效果展示

此时,我们仍然在 main.go 的同级目录下执行 go run main.go,输出如下:

1
shell复制代码连接被拒绝

显示的正式我们期望的错误信息

stringer 工具

我们来看看 stringer 工具的帮助,在来详细学习一波

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
shell复制代码# stringer -h
Usage of stringer:
stringer [flags] -type T [directory]
stringer [flags] -type T files... # Must be a single package
For more information, see:
http://godoc.org/golang.org/x/tools/cmd/stringer
Flags:
-linecomment
use line comment text as printed text when present
-output string
output file name; default srcdir/<type>_string.go
-trimprefix prefix
trim the prefix from the generated constant names
-type string
comma-separated list of type names; must be set

可以看到大的用法有 2 种:

1
2
shell复制代码 stringer [flags] -type T [directory]
stringer [flags] -type T files... # Must be a single package

第一种可以在类型 T 后面加上目录

第二种可以指定类型 T 后面指定明确的文件,但是这种方式必须是在一个单独的包里面

参数如下:

  • -linecomment

使用行注释文本作为打印文本

  • -output string

输出文件名称;默认源目录下的 / <类型> _string.go,所以我们可以看到例子中我们的输出文件在 mycodes 下的 errcode_string.go

  • -trimprefix prefix

从生成的常量名称中修剪前缀

  • -type string

以逗号分隔的类型名称列表,这个参数是必须要设置的

go generate

刚才我们是在命令行中,使用 stringer 工具来生成的,那么我们要把这些东西放入项目代码中就需要使用 go generate 工具了

先大致了解一下 go generate 是个啥玩意

go generate 是 go 自带的一个工具,我们可以通过在命令行中查看到:

1
shell复制代码# go

咱们查看一下帮助文档 go help generate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
shell复制代码# go help generate
...

Go generate scans the file for directives, which are lines of
the form,

//go:generate command argument...

...

Go generate sets several variables when it runs the generator:

$GOARCH
The execution architecture (arm, amd64, etc.)
$GOOS
The execution operating system (linux, windows, etc.)
$GOFILE
The base name of the file.
$GOLINE
The line number of the directive in the source file.
$GOPACKAGE
The name of the package of the file containing the directive.
$DOLLAR
A dollar sign.

上面这些是 go generate 使用时候的环境变量

  • $GOARCH

体系架构(arm、amd64 等)

  • $GOOS

当前的 OS 环境(linux、windows 等)

  • $GOFILE

当前处理中的文件名

  • $GOLINE

当前命令在文件中的行号

  • $GOPACKAGE

当前处理文件的包名

go generate命令格式

1
shell复制代码go generate [-run regexp] [-n] [-v] [-x] [command] [build flags] [file.go... | packages]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
shell复制代码参数说明如下:
-run
正则表达式匹配命令行,仅执行匹配的命令;

-v
输出被处理的包名和源文件名;

-n
显示不执行命令;

-x
显示并执行命令;

command
可以是在环境变量 PATH 中的任何命令。

generate 用法

上面帮助文档有体现,我们可以使用 //go:generate command argument... 来讲 generate 工具用起来

实际案例

我们来简单的尝试一下

我们在刚才的 main.go 中加入 generate 的语句,使用 generate 执行,ls -alh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
shell复制代码/*
____ ___ _____ ___________
\ \/ / / \\__ ___/
\ / / \ / \ | |
/ \ / Y \| |
/___/\ \\____|__ /|____|
\_/ \/
createTime:2021/10/10
createUser:Administrator
*/
package main

//go:generate ls -alh

import (
"fmt"
"myerr/mycodes"
)

func main() {
fmt.Println(mycodes.ERR_CODE_CONN_REFUSE)
}

在 main.go 同级目录下执行 go generate 看效果

1
2
3
4
5
6
7
shell复制代码# go generate
total 20K
drwxr-xr-x 3 root root 4.0K Oct 10 17:30 .
drwxr-xr-x 11 root root 4.0K Oct 10 16:25 ..
-rw-r--r-- 1 root root 22 Oct 10 16:02 go.mod
-rw-r--r-- 1 root root 346 Oct 10 17:30 main.go
drwxr-xr-x 2 root root 4.0K Oct 10 17:13 mycodes

果然是调用 ls -alh 成功了

go generate + stringer 的使用

那么我们就把刚才我们实践的 stringer 工具也加进去玩玩

此时目录是这样的

1
2
3
4
5
shell复制代码.
├── go.mod
├── main.go
└── mycodes
└── errcode.go

main.go 是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
shell复制代码/*
____ ___ _____ ___________
\ \/ / / \\__ ___/
\ / / \ / \ | |
/ \ / Y \| |
/___/\ \\____|__ /|____|
\_/ \/
createTime:2021/10/10
createUser:Administrator
*/
package main

//go:generate stringer -linecomment -type ErrCode ./mycodes

import (
"fmt"
"myerr/mycodes"
)

func main() {
fmt.Println(mycodes.ERR_CODE_CONN_REFUSE)
}

没错我们加入了 //go:generate stringer -linecomment -type ErrCode ./mycodes

直接执行 go generate -x 来看效果吧

1
2
shell复制代码# go generate -x
stringer -linecomment -type ErrCode ./mycodes

errcode_string.go 又生成了

1
2
3
4
5
6
shell复制代码.
├── go.mod
├── main.go
└── mycodes
├── errcode.go
└── errcode_string.go

执行 go run main.go 当然必须是我们想要的东西啦

1
2
shell复制代码# go run main.go
连接被拒绝

go generate 的使用规范

  • 运行go generate命令时,才会执行特殊注释后面的命令
  • 特殊注释必须以//go:generate开头,双斜线后面没有空格
  • 该特殊注释必须在 .go 源码文件中
  • 每个源码文件可以包含多个 generate 特殊注释
  • 当go generate命令执行出错时,将终止程序的运行

最后说说 go generate 还能干些啥

go generate 能干的事情还真不少,只要是能够在 path 下面能找到的可执行程序,都可以放在 //go:generate 后面玩,一般使用 go generate 会有如下场景:

  • protobuf:从 protocol buffer 定义文件(.proto)生成 .pb.go 文件 , 这种情况 grpc 通信的时候常用
  • yacc:从 .y 文件生成 .go 文件
  • HTML:将 HTML 文件嵌入到 go 源码
  • bindata:将形如 JPEG 这样的文件转成 go 代码中的字节数组
  • Unicode:从 UnicodeData.txt 生成 Unicode 表

工具要用用起来才能体现它的价值

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

本文转载自: 掘金

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

简单工厂模式——接口和抽象类

发表于 2021-10-10

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。


哈喽,大家好,我是一条~

国庆正是弯道超车的时候,向大家推荐一本个人觉得写的非常好的书——《redis深度历险 核心原理与应用实践》。

我们接着聊设计模式,新同学可以先看一下《23种设计模式的一句话通俗解读》全面的了解一下设计模式,形成一个整体的框架,再逐个击破。

往期回顾:原型模式、建造者模式

今天我们一块看一下简单工厂模式,其实他不属于23种设计模式,但为了更好的理解后面的工厂方法和抽象工厂,我们还是需要先了解一下。

image-20211004190949835

定义

官方定义

定义一个工厂类,他可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。

通俗解读

我们不必关心对象的创建细节,只需要根据不同参数获取不同产品即可。

难点:写好我们的工厂。

结构图

代码演示

本文源码:简单工厂模式源码 提取码: qr5h

目录结构

建议跟着一条学设计模式的小伙伴都建一个maven 工程,并安装lombok依赖和插件。

并建立如下包目录,便于归纳整理。

pom如下

1
2
3
4
5
xml复制代码    <dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>

开发场景

汽车制造工厂,既可以生产跑车,也可以生产SUV,未来还会生产新能源汽车。

代码实现

1.创建抽象产品Car

1
2
3
4
java复制代码public abstract class Car {
public String color;
abstract void run();
}

2.创建具体产品

SuvCar

1
2
3
4
5
6
7
8
9
10
java复制代码public class SuvCar extends Car{
public SuvCar(){
this.color="green";
}

@Override
public void run() {
System.out.println("SuvCar running---------");
}
}

SportsCar

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

public SportsCar(){
this.color="red";
}

@Override
public void run() {
System.out.println("SportsCar running-------");
}
}

3.创建静态工厂

在简单工厂模式中用于被创建实例的方法通常为静态方法,因此简单工厂模式又被成为静态工厂方法(Static Factory Method)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码/**
* 简单/静态工厂,少数产品
*/
public class CarFactory {

public static Car getCar(String type){
if (type.equals("sport")){
return new SportsCar();
}else if (type.equals("suv")){
return new SuvCar();
}else {
return null;
}
}
}

4.编写测试类

1
2
3
4
5
6
7
java复制代码public class MainTest {
public static void main(String[] args) {
SportsCar sport = (SportsCar) CarFactory.getCar("sport");
sport.run();
System.out.println(sport.color);
}
}

5.输出结果

接口和抽象类

补充一个知识:

接口和抽象类有什么区别?

什么时候用接口,什么时候用抽象类?

接口和抽象类有什么区别?

  • 接口是针对方法的整合,抽象类是针对子类的整合。
  • 人有男人,女人,人是抽象类。人可以吃东西,狗也可以吃东西,吃东西这个动作是接口。
  • 接口可以多继承,抽象类不行。
  • 接口中基本数据类型为static, 而抽类象不是。
  • 抽象类有构造器,方法可以实现,除了不能被实例化,和普通类没有区别,接口不是。

什么时候用接口,什么时候用抽象类?

  • 当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。
  • 再简单点说,有属性定义的时候,用抽象类,只有方法的时候,用接口。

应用场景

  • 工厂类负责创建对的对象比较少,因为不会造成工厂方法中的业务逻辑过于复杂
  • 客户端只知道传入工厂类的参数,对如何创建对象不关心
  • 由于简单工厂很容易违反高内聚责任分配原则,因此一般只在很简单的情况下应用。

总结

优点

  • 通过工厂类,无需关注生产的细节,只需要传递对应参数即可。
  • 可以引入配置文件,在不修改客户端代码的情况下更换和添加新的具体产品类。

缺点

  • 违背开闭原则,扩展不易。
  • 工厂类职责过重,一旦异常,系统瘫痪。
  • 无法动态的增加产品,扩展困难。

问题:在不修改的工厂的前提下,怎么生产新能源汽车?下一节的工厂方法模式给大家讲解。

本文转载自: 掘金

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

【c++常用遍历算法】

发表于 2021-10-10

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

常用遍历算法

【算法简介】:

for_each; // 遍历容器

transform; //搬运容器中的元素到另一个容器中

for_each(demo1.cpp)

【功能】:

1
markdown复制代码     实现遍历容器

【函数原型】:

`for_each(iterator begin, iterator end,_func);`
1
2
3
4
5
6
7
arduino复制代码    // 遍历算法,遍历容器中的元素

// begin 容器的开始迭代器

// end 容器的结束迭代器

// _func 函数或者函数对象

【demo】:

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
c++复制代码// demo1.cpp
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

// 定义一个普通函数
void print01(int v)
{
    cout << "v = " << v << endl;
}

// 定义函数对象
class MyPrint
{
public:
    void operator()(int v)
    {
        cout << "v = " << v << endl;
    }
};

// for_each算法基本用法
void test01()
{
    vector<int> v;
// 向容器中添加元素
    for (int i = 0; i < 10; i++)
    {
        v.push_back(i);
    }
    // 遍历算法
// 使用普通函数进行遍历
    for_each(v.begin(), v.end(), print01);
    cout << endl;
// 使用函数对象进行遍历
    for_each(v.begin(), v.end(), MyPrint());
}

int main()
{
    test01();
    return 0;
}

注意:for_each在实际开发中是最常用的遍历算法

transform(demo2.cpp)

【功能】:

1
复制代码   搬运容器到另一个容器中

【函数原型】:

transform(iterator begin1,iterator end1,iterator begin2,_func);

1
2
3
4
5
6
7
arduino复制代码    // begin1 源容器的开始迭代器

// end1 源容器的结束迭代器

// begin2 目标容器开始迭代器

// _func 函数或者函数对象

【demo】:

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
c++复制代码// demo2.cpp
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

// 定义函数对象
class Transform
{
public:
    int operator()(int v)
    {
        return v;
    }
};

// 定义函数对象
class MyPrint
{
public:
    void operator()(int v)
    {
        cout << v << endl;
    }
};

void test01()
{
    vector<int> v;
    for (int i = 0; i < 5; i++)
    {
        v.push_back(i);
    }
    vector<int> vTarget;
// 提前开辟空间,空间大小与源空间大小相同
    vTarget.resize(v.size());
    transform(v.begin(), v.end(), vTarget.begin(), Transform());
// 遍历目标容器
for_each(vTarget.begin(), vTarget.end(), MyPrint());
}

int main()
{
    test01();
    return 0;
}

注意:搬运的目标容器必须提前开辟空间,否则无法正常搬运

本文转载自: 掘金

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

SSM 图书管理系统:后端具体实现(下)

发表于 2021-10-10

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

前言

前一篇文章 图书管理系统实战(上)中,我们已经编写了 pojo、dao 层以及配置 dao 层对应的 mapper,从现在开始,我们开始编写 service 层和 controller 层。

service 层

预约业务操作码

在正式编写 service 层之前,我们先定义一个预约图书操作返回码的数据字段,用于反馈给客户信息;

返回码 说明
1 预约成功
0 预约失败
-1 预约重复
-2 系统异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码package com.cunyu.utils;

import com.cunyu.dto.AppointDto;
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
* @author : cunyu
* @version : 1.0
* @className : AppointStateEnum
* @date : 2020/7/24 10:50
* @description : 定义预约业务的数据字典
*/

@Getter
@AllArgsConstructor
public enum AppointStateEnum {

SUCCESS(1, "预约成功"), FAILURE(0, "预约失败"), REPEAT(-1, "预约重复"), SYSTEMERROR(-2, "系统异常");

private int state;
private String stateInfo;

/**
* @param stat 状态码
* @return
* @description 获取状态码对应 enum
* @date 2020/7/24 10:57
* @author cunyu1943
* @version 1.0
*/
public static AppointStateEnum stateOf(int stat) {
for (AppointStateEnum state : values()
) {
if (stat == state.getState()) {
return state;
}
}
return null;
}
}

数据传输层

定义好预约业务的数据字典之后,新建一个数据传输类用来传输我们的预约结果;

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复制代码package com.cunyu.dto;

import com.cunyu.pojo.Appointment;
import com.cunyu.utils.AppointStateEnum;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* @author : cunyu
* @version : 1.0
* @className : AppointDto
* @date : 2020/7/24 10:46
* @description : 用于数据传输,封装
*/

@Data
@NoArgsConstructor
public class AppointDto {
private int bookId;
// 状态码
private int state;
// 状态信息
private String stateInfo;
// 预约成功的对象
private Appointment appointment;

// 预约失败的构造器
public AppointDto(int bookId, AppointStateEnum appointStateEnum) {
this.bookId = bookId;
this.state = appointStateEnum.getState();
this.stateInfo = appointStateEnum.getStateInfo();
}

// 预约成功的构造器
public AppointDto(int bookId, AppointStateEnum appointStateEnum, Appointment appointment) {
this.bookId = bookId;
this.state = appointStateEnum.getState();
this.stateInfo = appointStateEnum.getStateInfo();
this.appointment = appointment;
}
}

service 业务代码编写

BookService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
java复制代码package com.cunyu.service;

import com.cunyu.dto.AppointDto;
import com.cunyu.pojo.Book;

import java.util.List;

/**
* @author : cunyu
* @version : 1.0
* @className : BookService
* @date : 2020/7/24 10:44
* @description : Book 业务接口
*/


public interface BookService {

/**
* @param bookId 图书 ID
* @return 对应 ID 的图书
* @description 根据图书 id 查询图书
* @date 2020/7/24 11:41
* @author cunyu1943
* @version 1.0
*/
Book getById(int bookId);

/**
* @param
* @return 所有图书的列表
* @description 获取图书列表
* @date 2020/7/24 11:41
* @author cunyu1943
* @version 1.0
*/
List<Book> getList();

/**
* @param bookId 图书 id
* @param studentId 学生 Id
* @return
* @description 返回预约结果
* @date 2020/7/24 11:39
* @author cunyu1943
* @version 1.0
*/
AppointDto appoint(int bookId, int studentId);
}

BookServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
java复制代码package com.cunyu.service.impl;

import com.cunyu.dao.AppointmentDao;
import com.cunyu.dao.BookDao;
import com.cunyu.dto.AppointDto;
import com.cunyu.pojo.Appointment;
import com.cunyu.pojo.Book;
import com.cunyu.service.BookService;
import com.cunyu.utils.AppointStateEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
* @author : cunyu
* @version : 1.0
* @className : BookServiceImpl
* @date : 2020/7/24 11:43
* @description : Book 业务接口实现类
*/

@Service
public class BookServiceImpl implements BookService {
// 依赖注入
@Autowired
private BookDao bookDao;

@Autowired
private AppointmentDao appointmentDao;

public Book getById(int bookId) {
return bookDao.queryById(bookId);
}

public List<Book> getList() {
return bookDao.queryAll(0, 3);
}

public AppointDto appoint(int bookId, int studentId) {
AppointDto appointDto = null;
try {
// 减库存
int update = bookDao.reduceNumber(bookId);
if (update <= 0) {
System.out.println(AppointStateEnum.FAILURE);
} else {
// 执行预约操作
int insert = appointmentDao.insertAppointment(bookId, studentId);
if (insert <= 0) {
System.out.println(AppointStateEnum.REPEAT);
} else {
Appointment appointment = appointmentDao.queryByKeyWithBook(bookId, studentId);
appointDto = new AppointDto(bookId, AppointStateEnum.SUCCESS, appointment);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return appointDto;
}
}

测试

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
java复制代码package com.cunyu.service.impl;

import com.cunyu.service.BookService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
* @author : cunyu
* @version : 1.0
* @className : BookServiceImplTest
* @date : 2020/7/24 11:53
* @description : BookServiceImpl 测试类
*/

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring/spring-*.xml")
public class BookServiceImplTest {
@Autowired
private BookService bookService;

@Test
public void testAppoint() {
int bookId = 1;
int studentId = 18301343;
System.out.println(bookService.appoint(bookId, studentId));
}
}

下图是我们测试后数据库中的数据,说明此时我们的 service 层接口测试成功。

封装结果

既然我们的 service 层接口和实现类都编写好了,我们就需要将结果进行封装成 json 格式,方便我们传到 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
java复制代码package com.cunyu.dto;

import lombok.Data;
import lombok.NoArgsConstructor;

/**
* @author : cunyu
* @version : 1.0
* @className : ResultDto
* @date : 2020/7/24 12:11
* @description : 封装结果为 json
*/

@Data
@NoArgsConstructor
public class ResultDto<T> {
// 是否预约成功
private boolean success;
// 预约成功返回的数据
private T data;
// 错误信息
private String error;

// 预约成功的构造器
public ResultDto(boolean success, T data) {
this.success = success;
this.data = data;
}

// 预约失败的构造器
public ResultDto(boolean success, String error) {
this.success = success;
this.error = error;
}
}

controller 层

编写好 service 层之后,我们就剩下最后的 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
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
java复制代码package com.cunyu.controller;

import com.cunyu.dto.AppointDto;
import com.cunyu.dto.ResultDto;
import com.cunyu.pojo.Book;
import com.cunyu.service.BookService;
import com.cunyu.utils.AppointStateEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
* @author : cunyu
* @version : 1.0
* @className : BookController
* @date : 2020/7/24 12:20
* @description : Book controller 层
*/

@Controller
@RequestMapping("/book")
public class BookController {
@Autowired
private BookService bookService;

// url:ip:port:/book/list
@GetMapping("/list")
private String list(Model model) {
List<Book> bookList = bookService.getList();
model.addAttribute("bookList", bookList);
return "list";
}

@GetMapping(value = "/{bookId}/detail")
private String detail(@PathVariable("bookId") Integer bookId, Model model) {
if (bookId == null) {
return "redirect:/book/list";
}

Book book = bookService.getById(bookId);
if (book == null) {
return "forward:/book/list";
}

model.addAttribute("book", book);
return "detail";
}

//ajax 传递 json 数据到前端
@RequestMapping(value = "/{bookId}/appoint", method = RequestMethod.POST, produces = {"application/json; charset=utf-8"})
@ResponseBody
private ResultDto<AppointDto> appoint(@PathVariable("bookId") Integer bookId, @RequestParam("studentId") Integer studentId) {
if (studentId == null || studentId.equals("")) {
return new ResultDto<>(false, "学号不能为空");
}
AppointDto appointDto = null;
try {
appointDto = bookService.appoint(bookId, studentId);
} catch (Exception e) {
e.printStackTrace();
}
return new ResultDto<AppointDto>(true, appointDto);
}
}

前端

好了,我们的后台就开发完毕了,接下来就可以去编写前端页面了。然后启动 Tomcat,访问对应 url 即可。

list.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
jsp复制代码<%--
Created by IntelliJ IDEA.
User: cunyu
Date: 2020/7/23
Time: 9:47
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>图书列表页</title>
</head>
<body>
<h1>${bookList}</h1>
</body>
</html>

detail.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
jsp复制代码<%--
Created by IntelliJ IDEA.
User: cunyu
Date: 2020/7/23
Time: 10:02
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>图书详情页</title>
</head>
<body>
<h1>${book.name}</h1>
<h2>${book.bookId}</h2>
<h2>${book.number}</h2>
</body>
</html>

总结

到此,我们的后台所有服务都写好了,SSM 框架整合配置,与应用实例部分已经结束,前端部分就简单写了个数据展示页面。

感兴趣的小伙伴可以接着去实现前哦 ~

本文转载自: 掘金

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

SSM 图书管理系统:后端具体实现(上)

发表于 2021-10-10

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

前言

前两篇文章已经讲过了如何准备项目框架以及如何整合 SSM,今天就来具体看看如何实现吧。

以下附上前两篇文章的传送门:

SSM 图书管理系统:项目框架结构搭建

SSM 图书管理系统:整合 SSM

准备数据库

新建数据库 bookmanager,然后创建两张表:图书表 book 和 预约图书表 appointment;

1
2
sql复制代码-- 建数据库
CREATE DATABASE `bookmanager`;
1
2
3
4
5
6
7
8
9
10
sql复制代码-- 创建图书表
CREATE TABLE `book` (
`book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '图书ID',
`name` varchar(100) NOT NULL COMMENT '图书名称',
`number` int(11) NOT NULL COMMENT '馆藏数量',
PRIMARY KEY (`book_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='图书表';

-- 插入数据
INSERT INTO `book`(`book_id`, `name`, `number`) VALUES (1, "Effective Java", 10),(2, "算法", 10),(3, "MySQL 必知必会", 10);
1
2
3
4
5
6
7
8
sql复制代码-- 创建预约图书表
CREATE TABLE `appointment` (
`book_id` int(11) NOT NULL COMMENT '图书ID',
`student_id` int(11) NOT NULL COMMENT '学号',
`appoint_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '预约时间' ,
PRIMARY KEY (`book_id`, `student_id`),
INDEX `idx_appoint_time` (`appoint_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='预约图书表';

实体类编写

数据库准备好之后,就可以给对应表创建实体类,创建实体类之前,我们可以在 pom.xml 中引入 lombok 依赖,减少代码的编写;

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>

Book.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码package com.cunyu.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* @author : cunyu
* @version : 1.0
* @className : Book
* @date : 2020/7/23 15:53
* @description : Book 实体类
*/

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
private int bookId;
private String name;
private int number;
}

Appointment.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
java复制代码package com.cunyu.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
* @author : cunyu
* @version : 1.0
* @className : Appointment
* @date : 2020/7/23 15:57
* @description : Appointment 实体类
*/

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Appointment {
private int bookId;
private int studentId;
private Date appointTime;
private Book book;
}

dao 接口类编写

BookDao.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
java复制代码package com.cunyu.dao;

import com.cunyu.pojo.Book;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
* @InterfaceName : BookDao
* @Author : cunyu
* @Date : 2020/7/23 16:02
* @Version : 1.0
* @Description : Book 接口
**/

public interface BookDao {

/**
* @param bookId 图书 id
* @return 对应 id 的图书
* @description 根据图书 id 查找对应图书
* @date 2020/7/23 16:04
* @author cunyu1943
* @version 1.0
*/
Book queryById(@Param("bookId") int bookId);

/**
* @param offset 查询起始位置
* @param limit 查询条数
* @return 查询出的所有图书列表
* @description 查询所有图书
* @date 2020/7/23 16:08
* @author cunyu1943
* @version 1.0
*/
List<Book> queryAll(@Param("offset") int offset, @Param("limit") int limit);

/**
* @param bookId 图书 id
* @return 更新的记录行数
* @description 借阅后更新馆藏
* @date 2020/7/23 16:09
* @author cunyu1943
* @version 1.0
*/
int reduceNumber(@Param("bookId") int bookId);
}

AppointmentDao.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码package com.cunyu.dao;

import com.cunyu.pojo.Appointment;
import org.apache.ibatis.annotations.Param;

/**
* @InterfaceName : AppointmentDao
* @Author : cunyu
* @Date : 2020/7/23 16:03
* @Version : 1.0
* @Description : Appointment 接口
**/

public interface AppointmentDao {

/**
* @param bookId 图书 id
* @param studentId 学生 id
* @return 插入的行数
* @description 插入预约图书记录
* @date 2020/7/23 16:13
* @author cunyu1943
* @version 1.0
*/
int insertAppointment(@Param("bookId") int bookId, @Param("studentId") int studentId);

/**
* @param bookId 图书 id
* @param studentId 学生 id
* @return
* @description 通过主键查询预约图书记录,并且携带图书实体
* @date 2020/7/23 16:16
* @author cunyu1943
* @version 1.0
*/
Appointment queryByKeyWithBook(@Param("bookId") int bookId, @Param("studentId") int studentId);
}

mapper 编写

编写好 dao 接口之后,并不需要我们自己去实现,MyBatis 会给我们动态实现,但是需要我们配置相应的 mapper。在 src/main/resources/mapper 下新建 BookDao.xml 和 AppointmentDao.xml,用于对应上面的 dao 接口;

BookDao.xml

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复制代码<?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.cunyu.dao.BookDao">
<select id="queryById" resultType="Book" parameterType="int">
SELECT book_id, name, number
FROM book
WHERE book_id = #{bookId}
</select>

<select id="queryAll" resultType="Book">
SELECT *
FROM book
ORDER BY book_id
LIMIT #{offset},#{limit}
</select>

<update id="reduceNumber">
UPDATE book
SET number = number - 1
WHERE book_id = #{bookId}
AND number > 0
</update>
</mapper>

AppointmentDao.xml

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
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.cunyu.dao.AppointmentDao">
<insert id="insertAppointment">
<!-- ignore 主键冲突,报错 -->
INSERT ignore INTO appointment (book_id, student_id) VALUES (#{bookId}, #{studentId})
</insert>

<select id="queryByKeyWithBook" resultType="Appointment">
<!-- 告知MyBatis 把结果映射到 Appointment 的同时映射 Book 属性 -->
SELECT
appointment.book_id,
appointment.student_id,
appointment.appoint_time,
book.book_id "book.book_id",
book.`name` "book.name",
book.number "book.number"
FROM
appointment
INNER JOIN book ON appointment.book_id = book.book_id
WHERE
appointment.book_id = #{bookId}
AND appointment.student_id = #{studentId}
</select>
</mapper>

测试

经过 准备数据库 -> 实体类编写 -> 接口类编写 -> mapper 配置 这一套流程之后,我们就可以进行模块化测试了,看看我们的接口是否成功实现。

BookDaoTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
java复制代码package com.cunyu.dao;

import com.cunyu.pojo.Book;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.List;

/**
* @author : cunyu
* @version : 1.0
* @className : BookDaoTest
* @date : 2020/7/23 18:02
* @description : BookDao 测试类
*/

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring/spring-*.xml")
public class BookDaoTest {
// 自动注入
@Autowired
private BookDao bookDao;

@Test
public void testQueryById() {
int bookId = 1;
Book book = bookDao.queryById(bookId);
System.out.println("ID 对应的图书信息:" + book);
}

@Test
public void testQueryAll() {
List<Book> bookList = bookDao.queryAll(0, 3);
System.out.println("所有图书信息:");
for (Book book : bookList
) {
System.out.println(book);
}

}

@Test
public void testReduceNumber() {
int bookId = 3;
int update = bookDao.reduceNumber(bookId);
System.out.println("update = " + update);
}
}

运行两次测试后,数据库的结果如下图:

AppointmentDaoTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码package com.cunyu.dao;

import com.cunyu.pojo.Appointment;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
* @author : cunyu
* @version : 1.0
* @className : AppointmentDaoTest
* @date : 2020/7/23 18:21
* @description : AppointmentDao 测试
*/

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring/spring-*.xml")
public class AppointmentDaoTest {

@Autowired
AppointmentDao appointmentDao;

@Test
public void testInsertAppointment() {
int bookId = 2;
int studentId = 18301333;
int insert = appointmentDao.insertAppointment(bookId, studentId);
System.out.println("Insert = " + insert);
}

@Test
public void testQueryByKeyWithBook(){
int bookId = 2;
int studentId = 18301333;
Appointment appointment=appointmentDao.queryByKeyWithBook(bookId,studentId);
System.out.println(appointment);
System.out.println(appointment.getBook());
}
}

预约后,appointment 表中插入记录;

总结

至此,我们做的工作总结下来主要有如下几点:

  1. 设计数据库
  2. 创建实体类
  3. 编写 dao 接口类
  4. 编写 dao 接口对应 mapper,交由 MyBatis 动态实现
  5. 对 dao 接口方法实现进行测试

好了,图书管理系统第一阶段到此就结束了,下一步我们就可以对其进行优化,并编写 service 层和 controller 层代码了。

本文转载自: 掘金

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

听说还不知道这几个 Goland 技巧

发表于 2021-10-10

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

很多人使用 Goland 有很长时间的,却没有好好利用上 Goland 工具带给我们的遍历,今天咱们就来解锁一下新技巧

过去我们使用 Goland 就是简单的配置一下 go 的 proxy ,以及配置一下 ssh ,与服务器进行文件的上传和下载,其余的技巧也就没有费心去尝试挖掘和使用了,有没有同感的童鞋

1 指哪打哪

进入自己编辑的文件,左边文件树就会自动指定到对应的文件

  • 点击设置图标
  • 勾选 Always Select Opened File

来个例子

例如我在 点击我的 color.go 文件,左侧的文件树,会马上指定到我现在正在查看的 color.go

2 自动生成单测文件

例如我们写了一个函数 func MyAdd(a, b int) int

开始生成单测

  • 点击代码
  • 点击生成
  • 点击自己需要生成的单测条件,即可生成单测文件

剩下的,我们只需要填写单测数据的各种情况即可,此处用到的是 go test 的 子测试 ,要是对单测感兴趣的 xdm 可以查看历史文章 Go test 单元测试用起来

解释一下图中生成单测的几种情况

  • Empty test files

创建一个空的单测文件

  • Test for selection

根据自己光标勾选的函数来生产单测文件中的单测函数

  • Tests for file

根据整个文件来生成单测文件,文件中的所有方法都会有对应的单测方法

  • Tests for package

根据整个包来生成单测文件,文件中的所有方法都会有对应的单测方法

3 生成函数代码模板

我们在工作中,有很多函数名字不同,但是内部的结果可以说是完全相同的,那么这种代码,我们一般怎么做?

你会告诉我,直接 C V 不就好了吗

可是我会告诉你,咱们可以使用生成函数代码的方式来实现

举个例子

  • 点击文件 - 设置
  • 搜索 Live Templates ,找到代码模板

  • 点击窗格右上角的 + 号,点击 Live Templates 添加一个函数模板

  • 填写好缩写,描述,模板内容,应用范围,若有变量则编辑变量
  • 应用 ,确定

咱们在代码中输入缩写的时候,就可以选择生成我们的模板了,以后写相同的业务代码就可以不用 C V 了,直接快捷一键生成模板不香吗?

4 注释

文件注释

文件注释,咱们可以自定义文件头

  • 文件 – 设置 – 文件和代码模板 – Go File
  • 设置自己的文件注释,还可以使用变量

查看效果

函数注释

  • 文件 - 设置 - 插件
  • 搜索 Goanno ,安装
  • 应用

1
2
3
4
5
6
7
shell复制代码如何使用
1.在函数上方点击快捷键(control + commend + /)
2.右键 -> Generate -> Goanno
功能
1.普通函数
2.接口中的函数
3.支持自定义模版

例如我们的函数要加上注释,我们可以 右键 -> Generate -> Goanno

自定义函数注释

当然这个工具也是很灵活的,支持咱们自己定义函数注释的模板

  • 工具 - Goanno Setting
  • 根据我们的喜好自定义模板 提交即可

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

本文转载自: 掘金

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

RocketMQ消息"推送"浅析(下)

发表于 2021-10-10
上文分析了消息推送核心机制和主要过程。本文的目的是破除迷信,笔者试图从源码上修正一些市面上大多数文章对Rocket MQ推送模式的错误理解。

从几张截图开始

在这之前我曾经搜索过这么类似的几个问题--"Rocket MQ推模式弊端",我们看看大家的回答。


我截取了一些:

image.png

image.png

image.png

破除谣言

上述他们的理解中存在不少错误,我分别驳斥之:
  • Push模式由MQ主动将消息推送给消费者

在笔者的RocketMQ消息”推送”浅析(上)上已经分析过,Push并不会主动推送消息,而是被动的接收到拉取请求之后讲消息告知消费者

  • Push过程中,不会考虑消费者处理能力

拉取之前目前的源码中存在三个条件的流量控制,目的就是为了考虑消费者的消费能力

  • 要求消费端有很强的消费能力,消费端缓冲区可能会溢出

默认情况下Client每个Consumer订阅的每个Queue暂存的消息占用内存不能超过100M,且可以调控,其实真的没有那么容易溢出

看一下代码

上文中我们介绍了拉取消息是由PullMessageService服务线程决定的,但是最终的落脚点都是DefaultMQPushConsumerImpl#pullMessage()。笔者分析一下ta的为了保护Consumer端都做了哪些努力:
  • 消费累计数量的控制
  1. 计算当前拉取请求对象–PullRequest对应的processQueue已经暂存的消息数量
  2. pullThresholdForQueue是队列级别的流量控制阈值,每个消息队列默认最多缓存1000条消息在Consumer端,判断两者大小,很显然如果暂存的消息已经超过限制,则不会处理该拉取请求,而是将这个请求重新入队
  3. 等待50ms之后再次处理,假设50ms后依然大于阈值则一直重复1,2步骤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public void pullMessage(PullRequest pullRequest) {
/* 当前ProcessQueue暂存的消息总数 */
long cachedMessageCount = processQueue.getMsgCount().get();

if (
cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()
) {
/* 放弃本次拉取任务,等待50ms,pullRequest又入队了 */
executePullRequestLater(pullRequest,
PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL
);
return;
}
}
1
2
3
复制代码pullThresholdForQueue这个阈值是可以调整的,
DefaultMQPushConsumerImpl暴露了setPullThresholdForQueue方法,
支持用户根据自身情况定制,默认等于1000
  • 占用内存的控制
  1. 计算当前拉取请求对象–PullRequest对应的processQueue已经暂存的消息占用内存总数
  2. pullThresholdSizeForQueue限制队列级别的缓存消息大小,每个消息队列默认最多缓存100M消息,判断两者大小,很显然如果暂存的消息已经超过限制,则不会处理该拉取请求,而是将这个请求重新入队
  3. 等待50ms之后再次处理,假设50ms后依然大于阈值则一直重复1,2步骤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public void pullMessage(PullRequest pullRequest) {
/* 当前ProcessQueue暂存消息占用内存空间(MB) */
long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);

/* 当前消息堆积超过100M,触发流控*/
if (
cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()
) {
/* 放弃本次拉取任务,等待50ms,pullRequest又入队了 */
this.executePullRequestLater(pullRequest,
PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL
);
return;
}
}
1
2
3
4
复制代码同样pullThresholdSizeForQueue这个阈值是也是可以调整的,
DefaultMQPushConsumerImpl暴露了setPullThresholdSizeForQueue
方法,
支持用户根据自身情况定制,默认等于100
  • 消费偏移量之差大小的控制
这个是难以理解的,因为刚刚接触Rocket MQ的时候,我先入为主的以为,需要将ProcessQueue中的暂存的消息消费完毕之后才会继续拉取,其实不然,只要没有触发Consumer端的流量控制,PullMessageService会一直按照自己的节奏进行消息拉取。


每次消息消费进度的上报则是ProcessQueue中目前暂存消息的最小的偏移量,这个偏移量则决定了消费端重启之后,需要从何处开始拉取,而PullRequest对象中维护的下一次拉取偏移量--nextOffset是针对当前Consumer拉取行为每次计算得出的。


正常情况下,明明已经限制了暂存消息不能超过1000条,而拉取顺序又是从前到后的,怎么也不可能偏移量之差等于2000啊。


但是你自己思考假设出现了如此情况呢:如果offset=1的消息所在的消费线程死锁,那么这个消息就会一直没有ACK,而其他消息都在正常消费,就会导致一直可以正常拉取消息,偏偏这个时候消费端重启,而你其实已经消费了大量消息,但是消息偏移量却是1,这就会带来超级大量的重复消费,这个对我们而言是不可接受的。有了此项限制就保证了即使出现了死锁现象,不至于重复消费太多也就是2000来个,瑕不掩瑜。
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public void pullMessage(PullRequest pullRequest) {
if (!this.consumeOrderly) {
/* 当前 ProcessQueue 中队列最大偏移量与最小偏移量间距不得超过2000 */
if (processQueue.getMaxSpan() > this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan()) {
/* 放弃本次拉取任务,等待50ms,pullRequest又入队了 */
executePullRequestLater(pullRequest,
PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL
);
return;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public long getMaxSpan() {
try {
this.lockTreeMap.readLock().lockInterruptibly();
try {
/**
* lastKey()当前暂存消息的最大偏移量
* firstKey()当前暂存消息的最小偏移量
*/
if (!this.msgTreeMap.isEmpty()) {
return this.msgTreeMap.lastKey()
- this.msgTreeMap.firstKey();
}
} finally {
this.lockTreeMap.readLock().unlock();
}
} catch (InterruptedException e) {
log.error("getMaxSpan exception", e);
}

return 0;
}

总结一下

综上所述,Rocket MQ的推送模式请放心使用,根本不会出现文章开头所说的各种尴尬情况,不会破坏系统的稳定性。

本文转载自: 掘金

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

1…501502503…956

开发者博客

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