Java8新特性-Stream API

Java8中有两大最为重要的改变。第一个是 Lambda 表达式;另外一 个则是 Stream API(java.util.stream.*)。

Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。

使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。

什么是 Stream

是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。“集合讲的是数据,流讲的是计算!”

注意:
①Stream 自己不会存储元素。
②Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
③Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行

Stream 的操作三个步骤

  • 创建 Stream:一个数据源(如:集合、数组),获取一个流
  • 中间操作一个中间操作链,对数据源的数据进行处理
  • 终止操作(终端操作)一个终止操作,执行中间操作链,并产生结果

创建stream

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
//1. 创建 Stream
@Test
public void test1(){
//1. Collection 提供了两个方法 stream() 与 parallelStream()
List<String> list = new ArrayList<>();
Stream<String> stream = list.stream(); //获取一个顺序流
Stream<String> parallelStream = list.parallelStream(); //获取一个并行流

//2. 通过 Arrays 中的 stream() 获取一个数组流
Integer[] nums = new Integer[10];
Stream<Integer> stream1 = Arrays.stream(nums);

//3. 通过 Stream 类中静态方法 of()
Stream<Integer> stream2 = Stream.of(1,2,3,4,5,6);

//4. 创建无限流
//迭代,如果不加limit,则会一直创建偶数,供forEach消费
Stream<Integer> stream3 = Stream.iterate(0, (x) -> x + 2).limit(10);
stream3.forEach(System.out::println);

//生成
Stream<Double> stream4 = Stream.generate(Math::random).limit(2);
stream4.forEach(System.out::println);

}

2.中间操作

  • filter:接收Lambda,从流中排除某些操作;
  • limit:截断流,使其元素不超过给定对象
  • skip(n):跳过元素,返回一个扔掉了前n个元素的流,若流中元素不足n个,则返回一个空流,与limit(n)互补
  • distinct:筛选,通过流所生成元素的hashCode()和equals()去除重复元素。

映射:

  • map–接收Lambda,将元素转换成其他形式或提取信息。接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
  • flatMap–接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流

测试前构造数据集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Employee {

private int id;
private String name;
private int age;
private double salary;
// get/set/constructor...
}
//定义一个数据集合
List<Employee> emps = Arrays.asList(
new Employee(102, "李四", 59, 6666.66),
new Employee(101, "张三", 18, 9999.99),
new Employee(103, "王五", 28, 3333.33),
new Employee(104, "赵六", 8, 7777.77),
new Employee(104, "赵六", 8, 7777.77),
new Employee(104, "赵六", 8, 7777.77),
new Employee(105, "田七", 38, 5555.55)
);

2.1filter

接收Lambda,从流中排除某些操作

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test2(){
//所有的中间操作不会做任何的处理
// filter方法传递一个Predicate
Stream<Employee> stream = emps.stream()
.filter((e) -> {
System.out.println("测试中间操作");
return e.getAge() <= 35;
});

//只有当做终止操作时,所有的中间操作会一次性的全部执行,称为“惰性求值”
stream.forEach(System.out::println);
}

多个中间操作可以连接起来形成一个流水线,除非流水 线上触发终止操作,否则中间操作不会执行任何的处理! 而在终止操作时一次性全部处理,称为“惰性求值”。

2.2 limit

截断流,使其元素不超过给定对象

1
2
3
4
5
6
7
8
@Test
public void test3(){
emps.stream()
.filter((e) -> {
return e.getSalary() >= 5000;
}).limit(3)
.forEach(System.out::println);
}

2.3 skip

跳过元素,返回一个扔掉了前n个元素的流,若流中元素不足n个,则返回一个空流,与limit(n)互补

1
2
3
4
5
6
7
@Test
public void test5(){
emps.parallelStream()
.filter((e) -> e.getSalary() >= 5000)
.skip(2)
.forEach(System.out::println);
}

2.4 distinct

筛选,通过流所生成元素的hashCode()和equals()去除重复元素

distinct去重是通过hashCode()和equals(),因此要重写Employee的这两个方法

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

private int id;
private String name;
private int age;
private double salary;
// get/set/constructor...

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + id;
result = prime * result + ((name == null) ? 0 : name.hashCode());
long temp;
temp = Double.doubleToLongBits(salary);
result = prime * result + (int) (temp ^ (temp >>> 32));
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Employee other = (Employee) obj;
if (age != other.age)
return false;
if (id != other.id)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (Double.doubleToLongBits(salary) != Double.doubleToLongBits(other.salary))
return false;
return true;
}
}

@Test
public void test6(){
emps.stream()
.distinct()
.forEach(System.out::println);
}

2.5.映射

  • map——接收 Lambda , 将元素转换成其他形式或提取信息。接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。

  • flatMap——接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流

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
@Test
public void test1(){
// 获取值包含name的stream
Stream<String> str = emps.stream()
.map((e) -> e.getName());

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

//将strList流里面的字符串转成大写
List<String> strList = Arrays.asList("aaa", "bbb", "ccc", "ddd", "eee");
Stream<String> stream = strList.stream()
.map(String::toUpperCase);
stream.forEach(System.out::println);

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

//stream2流中存的是stream流,stream流存的是Character
Stream<Stream<Character>> stream2 = strList.stream()
.map(TestStreamAPI1::filterCharacter);// {a,a,a},{b,b,b},{c,c,c}
//流里面有流,遍历就很麻烦
stream2.forEach((sm) -> {
sm.forEach(System.out::println);
});

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

// flatMap接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流
Stream<Character> stream3 = strList.stream()
.flatMap(TestStreamAPI1::filterCharacter);// {a,a,a,b,b,b,c,c,c}
//一个流,直接遍历就可以
stream3.forEach(System.out::println);
}

2.6.排序

  • sorted()——自然排序
  • sorted(Comparator com)——定制排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void test2(){
emps.stream()
.map(Employee::getName)
.sorted()
.forEach(System.out::println);

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

// 年龄相同则按姓名排序
emps.stream()
.sorted((x, y) -> {
if(x.getAge() == y.getAge()){
return x.getName().compareTo(y.getName());
}else{
return Integer.compare(x.getAge(), y.getAge());
}
}).forEach(System.out::println);
}

3.终止操作

  • allMatch–检查是否匹配所有元素
  • anyMatch–检查是否至少匹配一个元素
  • noneMatch–检查是否没有匹配所有元素
  • findFirst–返回第一个元素
  • findAny–返回当前流中的任意元素
  • count–返回流中元素的总个数
  • max–返回流中最大值
  • min–返回流中最小值

这些方面在Stream类中都有说明,这里不一一举例,只对allMatch、max各举一例进行说明。

给employee增加一个Status枚举属性,完整代码入戏

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

private int id;
private String name;
private int age;
private double salary;
private Status status;

public Employee() {
}

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

public Employee(String name, int age) {
this.name = name;
this.age = age;
}

public Employee(int id, String name, int age, double salary) {
this.id = id;
this.name = name;
this.age = age;
this.salary = salary;
}

public Employee(int id, String name, int age, double salary, Status status) {
this.id = id;
this.name = name;
this.age = age;
this.salary = salary;
this.status = status;
}

public Status getStatus() {
return status;
}

public void setStatus(Status status) {
this.status = status;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public double getSalary() {
return salary;
}

public void setSalary(double salary) {
this.salary = salary;
}

public String show() {
return "测试方法引用!";
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + id;
result = prime * result + ((name == null) ? 0 : name.hashCode());
long temp;
temp = Double.doubleToLongBits(salary);
result = prime * result + (int) (temp ^ (temp >>> 32));
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Employee other = (Employee) obj;
if (age != other.age)
return false;
if (id != other.id)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (Double.doubleToLongBits(salary) != Double.doubleToLongBits(other.salary))
return false;
return true;
}

@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", age=" + age + ", salary=" + salary + ", status=" + status
+ "]";
}

public enum Status {
FREE, BUSY, VOCATION;
}

}
//定义一个数据集合
List<Employee> emps = Arrays.asList(
new Employee(102, "李四", 59, 6666.66, Status.BUSY),
new Employee(101, "张三", 18, 9999.99, Status.FREE),
new Employee(103, "王五", 28, 3333.33, Status.VOCATION),
new Employee(104, "赵六", 8, 7777.77, Status.BUSY),
new Employee(104, "赵六", 8, 7777.77, Status.FREE),
new Employee(104, "赵六", 8, 7777.77, Status.FREE),
new Employee(105, "田七", 38, 5555.55, Status.BUSY)
);

3.1 allMatch & anyMatch & noneMatch

  • allMatch——检查是否匹配所有元素
  • anyMatch——检查是否至少匹配一个元素
  • noneMatch——检查是否没有匹配的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test1(){
// 检查是否匹配所有元素
boolean bl = emps.stream()
.allMatch((e) -> e.getStatus().equals(Status.BUSY));
System.out.println(bl);

// 检查是否至少匹配一个元素
boolean bl1 = emps.stream()
.anyMatch((e) -> e.getStatus().equals(Status.BUSY));
System.out.println(bl1);

// 检查是否没有匹配的元素
boolean bl2 = emps.stream()
.noneMatch((e) -> e.getStatus().equals(Status.BUSY));
System.out.println(bl2);
}

3.2 findFirst & findAny

  • findFirst——返回第一个元素
  • findAny——返回当前流中的任意元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test2(){
// stream串行流,返回第一个元素
Optional<Employee> op = emps.stream()
.sorted((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()))
.findFirst();
System.out.println(op.get());

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

// parallelStream并行流,返回当前流中的任意元素
Optional<Employee> op2 = emps.parallelStream()
.filter((e) -> e.getStatus().equals(Status.FREE))
.findAny();
System.out.println(op2.get());
}

Optional类是Java8为了解决null值判断问题,借鉴google guava类库的Optional类而引入的一个同名Optional类,使用Optional类可以避免显式的null值判断(null的防御性检查),避免null导致的NPE(NullPointerException)。

3.3 count & max & min

  • count——返回流中元素的总个数
  • max——返回流中最大值
  • min——返回流中最小值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void test3(){
// 返回流中元素的总个数
long count = emps.stream()
.filter((e) -> e.getStatus().equals(Status.FREE))
.count();
System.out.println(count);

// 返回流中最大值
Optional<Double> op = emps.stream()
.map(Employee::getSalary)
.max(Double::compare);
System.out.println(op.get());

// 返回流中最小值
Optional<Employee> op2 = emps.stream()
.min((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()));
System.out.println(op2.get());
}

最后注意以下:流进行了终止操作后,不能再次使用

3.4 归约reduce

reduce(T identity, BinaryOperator) / reduce(BinaryOperator) ——可以将流中元素反复结合起来,得到一个值

reduce方法需要传的参数是一个BIFunction(BinaryOperator 继承 BIFunction)

1
2
3
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T>
{ ... }

计算工资总和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void test1(){
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);

Integer sum = list.stream()
.reduce(0, (x, y) -> x + y);

System.out.println(sum);

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

// 计算工资总和
Optional<Double> op = emps.stream()
.map(Employee::getSalary)
.reduce(Double::sum);

System.out.println(op.get());
}

3.5 收集collect

collect——将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法

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
@Test
public void test3(){
// 收集到list集合中
List<String> list = emps.stream()
.map(Employee::getName)
.collect(Collectors.toList());
list.forEach(System.out::println);

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

// 收集到set集合中
Set<String> set = emps.stream()
.map(Employee::getName)
.collect(Collectors.toSet());
set.forEach(System.out::println);

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

// 收集到hashset集合中
HashSet<String> hs = emps.stream()
.map(Employee::getName)
.collect(Collectors.toCollection(HashSet::new));
hs.forEach(System.out::println);
}

@Test
public void test4(){
// 收集最最大值值
Optional<Double> max = emps.stream()
.map(Employee::getSalary)
.collect(Collectors.maxBy(Double::compare));
System.out.println(max.get());

// 收集最小值
Optional<Employee> op = emps.stream()
.collect(Collectors.minBy((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())));
System.out.println(op.get());

//收集工资总和
Double sum = emps.stream()
.collect(Collectors.summingDouble(Employee::getSalary));
System.out.println(sum);

// 收集工资平均值
Double avg = emps.stream()
.collect(Collectors.averagingDouble(Employee::getSalary));
System.out.println(avg);

// 收集员工总数
Long count = emps.stream()
.collect(Collectors.counting());
System.out.println(count);

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

DoubleSummaryStatistics dss = emps.stream()
.collect(Collectors.summarizingDouble(Employee::getSalary));

System.out.println(dss.getMax());
}

3.6 分组

按照状态分组

1
2
3
4
5
6
7
@Test
public void test5(){
// 按照状态分组
Map<Status, List<Employee>> map = emps.stream()
.collect(Collectors.groupingBy(Employee::getStatus));
System.out.println(map);
}

多级分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test6(){
Map<Status, Map<String, List<Employee>>> map = emps.stream()
.collect(Collectors.groupingBy(Employee::getStatus, Collectors.groupingBy((e) -> {
if(e.getAge() >= 60)
return "老年";
else if(e.getAge() >= 35)
return "中年";
else
return "成年";
})));

System.out.println(map);
}

分区

1
2
3
4
5
6
7
8
@Test
public void test7(){
// 工资大于等于5000一个区,小于5000一个区
Map<Boolean, List<Employee>> map = emps.stream()
.collect(Collectors.partitioningBy((e) -> e.getSalary() >= 5000));

System.out.println(map);
}

3.7 连接joining

1
2
3
4
5
6
7
8
@Test
public void test8(){
String str = emps.stream()
.map(Employee::getName)
.collect(Collectors.joining("," , "----", "----"));

System.out.println(str);
}

Java8新特性-方法引用

方法引用的使用场景

  我们用Lambda表达式来实现匿名方法。但有些情况下,我们用Lambda表达式仅仅是调用一些已经存在的方法,除了调用动作外,没有其他任何多余的动作,在这种情况下,我们倾向于通过方法名来调用它,而Lambda表达式可以帮助我们实现这一要求,它使得Lambda在调用那些已经拥有方法名的方法的代码更简洁、更容易理解。方法引用可以理解为Lambda表达式的另外一种表现形式。

方法引用的分类

类型 语法 对应的Lambda表达式
对象:实例方法引用 inst::instMethod (args) -> inst.instMethod(args)
类名:实例方法引用 类名::instMethod (inst,args) -> 类名.instMethod(args)
类名:静态方法引用 类名::staticMethod (args) -> 类名.staticMethod(args)
类名:构建方法引用 类名::new (args) -> new 类名(args)

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
// 获取对象属性的值:有返回值
@Test
public void test2(){
// lombda表达式方式
Employee emp = new Employee(101, "张三", 18, 9999.99);
Supplier<String> sup = () -> emp.getName();
System.out.println(sup.get());

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

// 方法表达式
Supplier<String> sup2 = emp::getName;
System.out.println(sup2.get());
}
// 输出字符串:无返回值
@Test
public void test1(){
// lombda表达式方式
PrintStream ps = System.out;
Consumer<String> con = (str) -> ps.println(str);
con.accept("Hello World!");

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

// 方法表达式
Consumer<String> con2 = ps::println;
con2.accept("Hello Java8!");
Consumer<String> con3 = System.out::println;
}

2.类:实例方法引用

这种方式需要注意的是,若Lambda 的参数列表的第一个参数,是实例方法的调用者,第二个参数(或无参)是实例方法的参数时,则可以使用格式: ClassName::MethodName

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
//类名 :: 实例方法名
@Test
public void test2(){
// x是exquals实例方法的调用者,y是实例方法的参数
BiPredicate<String, String> bp = (x, y) -> x.equals(y);
System.out.println(bp.test("abcde", "abcde"));

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

BiPredicate<String, String> bp2 = String::equals;
System.out.println(bp2.test("abc", "abc"));

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

// e是show实例方法的调用者,y是实例方法的参数
Function<Employee, String> fun = (e) -> e.show();
System.out.println(fun.apply(new Employee()));

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

Function<Employee, String> fun2 = Employee::show;
System.out.println(fun2.apply(new Employee()));

}

public class Employee {
public Employee() {
}
public String show() {
return "测试方法引用!";
}
}

3.类:静态方法引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void test3(){
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);

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

Comparator<Integer> com2 = Integer::compare;
}

@Test
public void test4(){
BiFunction<Double, Double, Double> fun = (x, y) -> Math.max(x, y);
System.out.println(fun.apply(1.5, 22.2));

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

BiFunction<Double, Double, Double> fun2 = Math::max;
System.out.println(fun2.apply(1.2, 1.5));
}

4.类名:构建方法引用

构造器的参数列表,需要与函数式接口中参数列表保持一致

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
@Test
public void test6(){
Supplier<Employee> sup = () -> new Employee();
System.out.println(sup.get());

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

Supplier<Employee> sup2 = Employee::new;
System.out.println(sup2.get());
}

@Test
public void test7(){
Function<String, Employee> fun = Employee::new;

BiFunction<String, Integer, Employee> fun2 = Employee::new;
}
// 同样也适用于数组
@Test
public void test8(){
Function<Integer, String[]> fun = (args) -> new String[args];
String[] strs = fun.apply(10);
System.out.println(strs.length);

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

Function<Integer, Employee[]> fun2 = Employee[] :: new;
Employee[] emps = fun2.apply(20);
System.out.println(emps.length);
}

Java8新特性-四大核心函数式接口

总结一下java8中的新特性内置的四大核心函数式接口

函数式接口在java中是指:有且仅有一个抽象方法的接口

函数式接口,即适用于函数式编程场景的接口。而java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。

@FunctionalInterface

@FunctionalInterface标注在一个接口上,说明这个接口是一个函数式接口。

那么关于函数式接口,有如下特点:

  • 有且只有一个抽象方法
  • 可以有多个静态方法
  • 可以有多个default方法(默认方法)
  • 可以有多个Object的public的抽象方法

消费型接口Consumer:

源码

1
2
3
4
5
6
7
8
9
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);

default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}

Consumer有参数,无返回值

Consumer有两个方法:accept()抽象方法, andThen()非抽象方法

使用示例:

1
2
3
4
5
6
7
8
9
//Consumer<T> 消费型接口 :
@Test
public void test1(){
happy(10000, (m) -> System.out.println("消费:" + m + "元"));
}

public void happy(double money, Consumer<Double> con){
con.accept(money);
}

供给型接口Supplier:

源码

1
2
3
4
5
@FunctionalInterface
public interface Supplier<T> {
// 无输入参数,提供一个创建好的对象,即结果
T get();
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Supplier<T> 供给型接口 :
@Test
public void test2(){
List<Integer> numList = getNumList(10, () -> (int)(Math.random() * 100));

for (Integer num : numList) {
System.out.println(num);
}
}
//需求:产生指定个数的整数,并放入集合中
public List<Integer> getNumList(int num, Supplier<Integer> sup){
List<Integer> list = new ArrayList<>();

for (int i = 0; i < num; i++) {
Integer n = sup.get();
list.add(n);
}

return list;
}

函数型接口Function:

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@FunctionalInterface
public interface Function<T, R> {
// 我给你一个参数,你帮我处理一下,给我返回另一个参数。
R apply(T t);

// 返回一个组合函数,首先将入参应用到before函数,再将before函数结果应用到该函数中
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}

// 返回一个组合函数,该函数结果应用到after函数中
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}

// 返回输入参数
static <T> Function<T, T> identity() {
return t -> t;
}
}

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Function<T, R> 函数型接口:
@Test
public void test3(){
String newStr = strHandler("\t\t\t 这是一个字符串", (str) -> str.trim());
System.out.println(newStr);

String subStr = strHandler("这是一个字符串", (str) -> str.substring(2, 5));
System.out.println(subStr);
}

//需求:用于处理字符串
public String strHandler(String str, Function<String, String> fun){
return fun.apply(str);
}

断言型接口:Predicate

源码:

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
@FunctionalInterface
public interface Predicate<T> {
// 给一个参数T,返回boolean类型的结果
boolean test(T t);

default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}

default Predicate<T> negate() {
return (t) -> !test(t);
}

default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}

static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}

Predicate默认实现的三个重要方法and,or和negate,这三个方法对应了java的三个连接符号&&、||和!,isEqual这个方法的返回类型也是Predicate,所以我们也可以把它作为函数式接口进行使用。我们可以当做==操作符来使用。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//Predicate<T> 断言型接口:
@Test
public void test4(){
List<String> list = Arrays.asList("Hello", "atguigu", "Lambda", "www", "ok");
List<String> strList = filterStr(list, (s) -> s.length() > 3);

for (String str : strList) {
System.out.println(str);
}
}

//需求:将满足条件的字符串,放入集合中
public List<String> filterStr(List<String> list, Predicate<String> pre){
List<String> strList = new ArrayList<>();

for (String str : list) {
if(pre.test(str)){
strList.add(str);
}
}

return strList;
}

Java8新特性-Lambda表达式

总结一下java8中的新特性lambda表达式

1 匿名函数

Lambda是一个匿名函数,可以理解为一段可以传递的代码(将代码像数据一样传递);可以写出更简洁、更灵活的代码;作为一种更紧凑的代码风格,是Java语言表达能力得到提升。

有一个需求:获取公司中年龄小于 35 的员工信息获取公司中工资大于 5000 的员工信息

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

private int id;
private String name;
private int age;
private double salary;
// get/set/constructor...
}
List<Employee> emps = Arrays.asList(
new Employee(101, "张三", 18, 9999.99),
new Employee(102, "李四", 59, 6666.66),
new Employee(103, "王五", 28, 3333.33),
new Employee(104, "赵六", 8, 7777.77),
new Employee(105, "田七", 38, 5555.55)
);

该需求最直接的实现方式,遍历筛选符合条件的员工

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

// 获取小于 35 的员工信息
public List<Employee> filterEmployeeAge(List<Employee> emps){
List<Employee> list = new ArrayList<>();

for (Employee emp : emps) {
if(emp.getAge() <= 35){
list.add(emp);
}
}
return list;
}
// 获取工资大于5000的员工信息
public List<Employee> filterEmployeeSalary(List<Employee> emps){
List<Employee> list = new ArrayList<>();

for (Employee emp : emps) {
if(emp.getSalary() >= 5000){
list.add(emp);
}
}

return list;
}

如果又增加一种筛选条件,岂不是又要增加一个方法,且很多代码都是重复的,我们来进行优化

优化方式一:策略模式改造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 抽象接口
public interface MyPredicate<T> {
public boolean test(T t);
}
// 按年龄筛选策略类
public class FilterEmployeeForAge implements MyPredicate<Employee>{

@Override
public boolean test(Employee t) {
return t.getAge() <= 35;
}
}
// 按工资筛选策略类
public class FilterEmployeeForSalary implements MyPredicate<Employee> {
@Override
public boolean test(Employee t) {
return t.getSalary() >= 5000;
}
}
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
//优化方式一:策略设计模式
@Test
public void test4(){
List<Employee> list = filterEmployee(emps, new FilterEmployeeForAge());
// 获取小于 35 的员工信息
for (Employee employee : list) {
System.out.println(employee);
}

System.out.println("------------------------------------------");
// 获取工资大于5000的员工信息
List<Employee> list2 = filterEmployee(emps, new FilterEmployeeForSalary());
for (Employee employee : list2) {
System.out.println(employee);
}
}
public List<Employee> filterEmployee(List<Employee> emps, MyPredicate<Employee> mp){
List<Employee> list = new ArrayList<>();

for (Employee employee : emps) {
if(mp.test(employee)){
list.add(employee);
}
}

return list;
}

优化方式二:匿名内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//优化方式二:匿名内部类
@Test
public void test5(){
List<Employee> list = filterEmployee(emps, new MyPredicate<Employee>() {
@Override
public boolean test(Employee t) {
return t.getId() <= 103;
}
});

for (Employee employee : list) {
System.out.println(employee);
}
}

优化方式三:Lambda 表达式

1
2
3
4
5
6
7
8
9
10
11
//优化方式三:Lambda 表达式
@Test
public void test6(){
List<Employee> list = filterEmployee(emps, (e) -> e.getAge() <= 35);
list.forEach(System.out::println);

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

List<Employee> list2 = filterEmployee(emps, (e) -> e.getSalary() >= 5000);
list2.forEach(System.out::println);
}

优化方式四:Stream API

Stream API也是java8的新特性,为了保持这个例子的完整性,我也还是放在了这里,可以跳过,在了解完Stream API可以再回来看这个简单例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test7(){
emps.stream()
.filter((e) -> e.getAge() <= 35)
.forEach(System.out::println);

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

emps.stream()
.map(Employee::getName)
.limit(3)
.sorted()
.forEach(System.out::println);
}

从上面的演变过程如下:

垃圾代码 --> 策略模式 --> 匿名内部类 --> Lambda表达式 --> Stream API

可以看出,lambda没有一句废话,直奔主题(我们的筛选条件),为我们减少了很多工作量

那么lambda语法如何使用呢?

2.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
// 1.无参数,无返回值
@Test
public void test01(){
Runnable runnable = () -> {
System.out.println("Hello Lambda");
};
}
// 2.有一个参数,无返回值
@Test
public void test02(){
Consumer<String> consumer = (a) -> System.out.println(a);
consumer.accept("我觉得还行!");
}
// 3.有一个参数,无返回值 (小括号可以省略不写)
@Test
public void test03(){
Consumer<String> consumer = a -> System.out.println(a);
consumer.accept("我觉得还行!");
}
// 4。有两个及以上的参数,有返回值,并且 Lambda 体中有多条语句
@Test
public void test04(){
Comparator<Integer> comparator = (a, b) -> {
System.out.println("比较接口");
return Integer.compare(a, b);
};
}
// 5.有两个及以上的参数,有返回值,并且 Lambda 体中只有1条语句 (大括号 与 return 都可以省略不写)
@Test
public void test05(){
Comparator<Integer> comparator = (a, b) -> Integer.compare(a, b);
}

Lambda 表达式的参数列表的数据类型可以省略不写,因为JVM编译器通过上下文推断出,数据类型,即“类型推断”:(Integer a, Integer b) -> Integer.compare(a, b);

3.Lambda 表达式与函数式接口

函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

函数式接口可以被隐式转换为 lambda 表达式。

MyFun接口是一个函数式接口,它接受一个输入Integer参数 num,返回一个Integer结果。

使用@FunctionalInterface将这个接口定义为函数式接口

1
2
3
4
5
// 增加@FunctionalInterface注解
@FunctionalInterface
public interface MyFun {
public Integer getValue(Integer num);
}

需求:对一个数进行运算

1
2
3
4
5
6
7
8
9
10
11
12
//需求:对一个数进行运算
@Test
public void test6(){
Integer num = operation(100, (x) -> x * x);
System.out.println(num);

System.out.println(operation(200, (y) -> y + 200));
}

public Integer operation(Integer num, MyFun mf){
return mf.getValue(num);
}

Docker系列-docker-compose安装

1.简介

Compose 项目是 Docker 官方的开源项目,负责实现对 Docker 容器集群的快速编排。其代码目前在 https://github.com/docker/compose 上开源。

我们知道使用一个 Dockerfile 模板文件,可以让用户很方便的定义一个单独的应用容器。然而,在日常工作中,经常会碰到需要多个容器相互配合来完成某项任务的情况。例如要实现一个 Web 项目,除了 Web 服务容器本身,往往还需要再加上后端的数据库服务容器,甚至还包括负载均衡容器等。

Compose 恰好满足了这样的需求。它允许用户通过一个单独的 docker-compose.yml 模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。

Compose 中有两个重要的概念:

  • 服务 (service):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例。
  • 项目 (project):由一组关联的应用容器组成的一个完整业务单元,在 docker-compose.yml 文件中定义。

Compose 的默认管理对象是项目,通过子命令对项目中的一组容器进行便捷地生命周期管理。

Compose 项目由 Python 编写,实现上调用了 Docker 服务提供的 API 来对容器进行管理。因此,只要所操作的平台支持 Docker API,就可以在其上利用 Compose 来进行编排管理。

2.安装

linux系统命令行方式安装

1
2
sudo curl -L "https://github.com/docker/compose/releases/download/1.25.5/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

windows系统安装docker后自带DockerCompose

卸载

1
sudo` `rm` `/usr/local/bin/docker-compose

Docker系列-docker安装

Docker 的安装和使用有一些前提条件,主要体现在体系架构和内核的支持上。对于体系架构,除了 Docker 一开始就支持的 X86-64 ,其他体系架构的支持则一直在不断地完善和推进中。

Docker 分为 CEEE 两大版本。 CE 即社区版(免费,支持周期 7 个月), EE 即企业版,强调安全,付费使用,支持周期 24 个月。

我们在安装前可以参看官方文档获取最新的 Docker 支持情况,官方文档在这里:

https://docs.docker.com/install/

Docker 对于内核支持的功能,即内核的配置选项也有一定的要求(比如必须开启 CgroupNamespace 相关选项,以及其他的网络和存储驱动等), Docker 源码中提供了一个检测脚本来检测和指导内核的配置,脚本链接在这里:

https://raw.githubusercontent.com/docker/docker/master/contrib/check-config.sh

在满足前提条件后,安装就变得非常的简单了。

Docker CE 的安装请参考官方文档:

这里我们以 CentOS7 作为本文的演示。

1
2
3
4
5
6
7
8
9
10
11
12
#更新yum   
sudo yum update
#安装依赖包
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
#设置镜像仓库为国内的阿里云仓库
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
#安装docker
sudo yum install -y docker-ce
#启动docker
sudo systemctl start docker
#设置开机启动docker
sudo systemctl enable docker

Docker 官方为了简化安装流程,提供了一套便捷的安装脚本,CentOS 系统上可以使用这套脚本安装

1
2
curl -fsSL get.docker.com -o get-docker.sh
sh get-docker.sh

安装完成后,运行下面的命令,验证是否安装成功:

1
2
3
docker version
or
docker info

返回docker的版本相关信息,证明 docker 安装成功

Sonarquber-docker安装教程

安装准备

本文安装 Sonarqube是基于docker-compose的安装教程,安装 Sonarqube前需安装docker和docker-compose

docker安装和docker-compose安装见链接:

环境准备

因为sonarqube采用elasticsearch作为检索后台服务,在Linux下面部署应用的时候,有时候会遇上Socket/File: Can’t open so many files的问题;这个值也会影响服务器的最大并发数,其实Linux是有文件句柄限制的,而且Linux默认不是很高,一般都是1024,生产服务器用其实很容易就达到这个数量

如果想查看和修改相关配置,参考linux中/etc/security/limits.conf配置文件说明

如果已改过可以不用改了

1
2
3
4
# 创建容器映射路径
sudo mkdir -p /home/sonar/postgres/{postgresql,data}
sudo mkdir -p /home/sonar/sonarqube/{extensions,logs,data,conf}
sudo chmod -R 777 /home/sonar/* # 启动容器映射路径权限问题
1
2
3
# 拉取docker 镜像,拉取镜像较慢,可以使用阿里云镜像站或者清华大学镜像站。
docker pull postgres:12.3
docker pull sonarqube:7.9.2-community
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
# 编辑docker-compose.yml文件
version: '3'
services:
postgres:
image: postgres:12.3
restart: always
container_name: postgres
ports:
- 5432:5432
volumes:
- /home/sonar/postgres/postgresql:/var/lib/postgresql
- /home/sonar/postgres/data:/var/lib/postgresql/data
- /etc/localtime:/etc/localtime:ro
environment:
TZ: Asia/Shanghai
POSTGRES_USER: sonar
POSTGRES_PASSWORD: sonar
POSTGRES_DB: sonar

sonar:
image: sonarqube:7.9.2-community
container_name: sonar
depends_on:
- postgres
volumes:
- /home/sonar/sonarqube/extensions:/opt/sonarqube/extensions
- /home/sonar/sonarqube/logs:/opt/sonarqube/logs
- /home/sonar/sonarqube/data:/opt/sonarqube/data
- /home/sonar/sonarqube/conf:/opt/sonarqube/conf
# 设置与宿主机时间同步
- /etc/localtime:/etc/localtime:ro
ports:
- 59000:9000
command:
# 内存设置
- -Dsonar.ce.javaOpts=-Xmx2048m
- -Dsonar.web.javaOpts=-Xmx2048m
# 设置服务代理路径
- -Dsonar.web.context=/
# 此设置用于集成gitlab时,回调地址设置
- -Dsonar.core.serverBaseURL=https://sonarqube.example.com
environment:
TZ: Asia/Shanghai
SONARQUBE_JDBC_USERNAME: sonar
SONARQUBE_JDBC_PASSWORD: sonar
SONARQUBE_JDBC_URL: jdbc:postgresql://postgres:5432/sonar

服务部署

1
2
# -d 服务后台运行
docker-compose up -d

查看容器运行情况

1
2
3
4
5
$ sudo docker-compose -f docker-compose.yml ps
Name Command State Ports
---------------------------------------------------------------------------
postgres docker-entrypoint.sh postgres Up 0.0.0.0:5432->5432/tcp
sonar ./bin/run.sh -Dsonar.ce.ja ... Up 0.0.0.0:9000->9000/tcp

浏览器输入http://ip:9000,ip为sonar服务器ip,账号密码为:admin/admin

更多精彩内容:mrxccc

设计模式-单例模式介绍+8种实现方式

关于单例模式,思想很简单,就是确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,减少内存开支

主要有两大实现方式:懒汉式、饿汉式

  • 饿汉式:在类加载时就完成了初始化,所以类加载比较慢,但获取对象的速度快。
  • 懒汉式:在类加载时不初始化,等到第一次被使用时才初始化。

打一个很形象的比喻,有两个很饿的老汉,一个很懒、一个很勤快,懒汉要别人把饭做好送到嘴边的才肯吃;饿汉呢就是拿着碗等着饭熟,饭一熟立马盛饭开吃

那么在程序里面,懒汉式是用的最多,因为它可以减少性能开销。但是懒汉式在单线程环境中没有任何问题,一旦处于多线程环境,那么它是线程不安全的,而如何保证线程安全,各个语言又会有不同的处理方式,本文以JAVA语言为例,来进行介绍单例模式思想实现单例模式的8种方式

首先我们还是以一个非常简单例子为例,介绍单例模式的思想,如果了解了单例模式,可以跳过这小节

1.我是皇帝我独苗

自从秦始皇确立了皇帝这个位置以后,同一时期基本上就只有一个人孤零零地坐在这个位置。这种情况下臣民们也好处理,大家叩拜、谈论的时候只要提及皇帝,每个人都知道指的是谁,而不用在皇帝前面加上特定的称呼,如张皇帝、李皇帝。这一个过程反应到设计领域就是,要求一个类只能生成一个对象(皇帝),所有对象对它的依赖都是相同的,因为只有一个对象,大家对它的脾气和习性都非常了解,建立健壮稳固的关系,我们把皇帝这种特殊职业通过程序来实现。

皇帝每天要上朝接待臣子、处理政务,臣子每天要叩拜皇帝,皇帝只能有一个,也就是一个类只能产生一个对象,该怎么实现呢?对象产生是通过new关键字完成的(当然也有其他方式,比如对象复制、反射等),这个怎么控制呀,但是大家别忘记了构造函数,使用new关键字创建对象时,都会根据输入的参数调用相应的构造函数,如果我们把构造函数设置为private私有访问权限不就可以禁止外部创建对象了吗?臣子叩拜唯一皇帝的过程类图

只有两个类,Emperor代表皇帝类,Minister代表臣子类,关联到皇帝类非常简单。

1
2
3
4
5
6
7
8
9
10
11
12
public class Emperor {
private static final Emperor emperor =new Emperor(); //初始化一个皇帝
private Emperor(){
//世俗和道德约束你,目的就是不希望产生第二个皇帝}
public static Emperor getInstance(){
return emperor;
}
//皇帝发话了
public static void say(){
System.out.println("我就是皇帝某某某....");
}
}

通过定义一个私有访问权限的构造函数,避免被其他类new出来一个对象,而Emperor自己则可以new一个对象出来,其他类对该类的访问都可以通过getInstance获得同一个对象。

皇帝有了,臣子要出场

1
2
3
4
5
6
7
8
public class Minister {
public static void main(String[] args) {
for(int day=0;day<3;day++){
Emperor emperor=Emperor.getInstance();emperor.say();
}
//三天见的皇帝都是同一个人,荣幸吧!
}
}

臣子参拜皇帝的运行结果如下所示。

1
2
3
4
5
我就是皇帝某某某....

我就是皇帝某某某....

我就是皇帝某某某....

臣子天天要上朝参见皇帝,今天参拜的皇帝应该和昨天、前天的一样(过渡期的不考虑,别找茬哦),大臣磕完头,抬头一看,嗨,还是昨天那个皇帝,老熟人了,容易讲话,这就是单例模式。

2 单例模式的定义

单例模式(Singleton Pattern)是一个比较简单的模式,其定义如下:

Ensure a class has only one instance, and provide a global point of access to it.(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)

单例模式的通用类图如下图所示

Singleton类称为单例类,通过使用private的构造函数确保了在一个应用中只产生一个实例,并且是自行实例化的(在Singleton中自己使用new Singleton())

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
private static final Singleton singleton = new Singleton();
//限制产生多个对象
private Singleton(){
}
//通过该方法获得实例对象
public static Singleton getSingleton(){
return singleton;
}
//类中其他方法,尽量是static
public static void doSomething(){
}
}

3 单例模式的应用

3.1 单例模式的优点

● 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。

● 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM垃圾回收机制)。

● 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。

● 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

3.2 单例模式的缺点

● 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。

● 单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。

● 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。

3.3 单例模式的使用场景

在一个系统中,要求一个类有且仅有一个对象,如果出现多个对象就会出现“不良反应”,可以采用单例模式,具体的场景如下:

● 要求生成唯一序列号的环境;

● 在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的;

● 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;

● 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。

4 实现单例模式的8种方式

介绍之前我们先看看java种类的加载顺序

类加载(classLoader)机制一般遵从下面的加载顺序

如果类还没有被加载:

  • 先执行父类的静态代码块和静态变量初始化,静态代码块和静态变量的执行顺序跟代码中出现的顺序有关。
  • 执行子类的静态代码块和静态变量初始化。
  • 执行父类的实例变量初始化
  • 执行父类的构造函数
  • 执行子类的实例变量初始化
  • 执行子类的构造函数

同时,加载类的过程是线程私有的,别的线程无法进入。

如果类已经被加载:

静态代码块静态变量不再重复执行,再创建类对象时,只执行与实例相关的变量初始化和构造方法。

static关键字

一个类中如果有成员变量或者方法被static关键字修饰,那么该成员变量或方法将独立于该类的任何对象。它不依赖类特定的实例,被类的所有实例共享,只要这个类被加载,该成员变量或方法就可以通过类名去进行访问,它的作用用一句话来描述就是,不用创建对象就可以调用方法或者变量,这简直就是为单例模式的代码实现量身打造的。

4.1 静态常量(饿汉模式)

这一种是使用静态常量的方式创建实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SingletonTest01 {
public static void main(String[] args) {
SingletonClass01 c1 = SingletonClass01.getInstance();
SingletonClass01 c2 = SingletonClass01.getInstance();
System.out.println(c1 == c2);
}
}
class SingletonClass01 {

// 1. 构造器私有化
private SingletonClass01() {

}

// 2.本类内部创建对象实例
private final static SingletonClass01 instance = new SingletonClass01();

// 3. 提供一个公有的静态方法,返回实例对象
public static SingletonClass01 getInstance() {
return instance;
}
}

4.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
public class SingletonTest02 {
public static void main(String[] args) {
SingletonClass02 c1 = SingletonClass02.getInstance();
SingletonClass02 c2 = SingletonClass02.getInstance();
System.out.println(c1 == c2);
}
}

class SingletonClass02 {

//1. 构造器私有化, 外部不能new
private SingletonClass02() {

}

//2.本类内部创建对象实例
private static SingletonClass02 instance;

static { // 在静态代码块中,创建单例对象
instance = new SingletonClass02();
}

//3. 提供一个公有的静态方法,返回实例对象
public static SingletonClass02 getInstance() {
return instance;
}

}

4.3 线程不安全(懒汉式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class SingletonTest03 {
public static void main(String[] args) {
// System.out.println("单线程创建实例=======");
// SingletonClass03 c1 = SingletonClass03.getInstance();
// SingletonClass03 c2 = SingletonClass03.getInstance();
// System.out.println(c1 == c2);
System.out.println("多线程创建实例=======");
//创建10个线程, 在每个 线程中打印单例对象
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//调用Singleton.getInstance()返回单例对象,打印会输出对象的哈希码
System.out.println(SingletonClass03.getInstance());
}
}).start();
}
//程序运行后,输出单例的哈希码都相同,说明是同一个对象
}
}

class SingletonClass03 {
private static SingletonClass03 instance;
//1. 构造器私有化, 外部不能new
private SingletonClass03() {
}

//提供一个静态的公有方法,当使用到该方法时,才去创建 instance
//即懒汉式
public static SingletonClass03 getInstance() {
if (instance == null) {
instance = new SingletonClass03();
}
return instance;
}
}

多线程环境测试结果

1
2
3
4
5
6
7
8
9
10
11
多线程创建实例=======
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@4bd8a307
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207

这种是线程不安全的单例模式,从多线程的测试结果可以看出,在创建实例时创建了新的对象,这样的单例模式就是线程不安全单例模式,不推荐该方式

4.4 线程安全,同步方法(懒汉式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class SingletonTest04 {
public static void main(String[] args) {
// System.out.println("单线程创建实例=======");
// SingletonClass04 c1 = SingletonClass04.getInstance();
// SingletonClass04 c2 = SingletonClass04.getInstance();
// System.out.println(c1 == c2);

System.out.println("多线程创建实例=======");
//创建10个线程, 在每个 线程中打印单例对象
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//调用Singleton.getInstance()返回单例对象,打印会输出对象的哈希码
System.out.println(SingletonClass04.getInstance());
}
}).start();
}
//程序运行后,输出单例的哈希码都相同,说明是同一个对象
}
}

// 懒汉式(线程安全,同步方法)
class SingletonClass04 {
private static SingletonClass04 instance;
//1. 构造器私有化, 外部不能new
private SingletonClass04() {}

//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
//即懒汉式
public static synchronized SingletonClass04 getInstance() {
if(instance == null) {
instance = new SingletonClass04();
}
return instance;
}
}

这种是线程安全的单例模式,特点就是给获取实例的方法getInstance()加了一个synchronized关键字,加上锁使其成为一个同步方法,保证了同一时刻只能有一个线程访问并获得实例,但是缺点也很明显,因为synchronized是修饰整个方法,每个线程访问都要进行同步,而其实这个方法只执行一次实例化代码就够了,每次都同步方法显然效率低下,不推荐

4.5双重检查(懒汉式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class SingletonTest05 {
public static void main(String[] args) {
SingletonClass05 c1 = SingletonClass05.getInstance();
SingletonClass05 c2 = SingletonClass05.getInstance();
System.out.println(c1 == c2);
System.out.println("多线程创建实例=======");
//创建10个线程, 在每个 线程中打印单例对象
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//调用Singleton.getInstance()返回单例对象,打印会输出对象的哈希码
System.out.println(SingletonClass05.getInstance());
}
}).start();
}
//程序运行后,输出单例的哈希码都相同,说明是同一个对象
}
}

// 懒汉式(线程安全,同步方法)
class SingletonClass05 {
// volatile禁止重排序
private static volatile SingletonClass05 instance;

private SingletonClass05() {}

//提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题
//同时保证了效率, 推荐使用
public static SingletonClass05 getInstance() {
if(instance == null) {
synchronized (SingletonClass05.class) {
if(instance == null) {
instance = new SingletonClass05();
}
}

}
return instance;
}
}

这种写法用了两个if判断,也就是Double-Check,并且同步的不是方法,而是代码块,效率较高,是对第四种写法的改进。为什么要做两次判断呢?这是为了线程安全考虑,还是那个场景,对象还没实例化,两个线程A和B同时访问静态方法并同时运行到第一个if判断语句,这时线程A先进入同步代码块中实例化对象,结束之后线程B也进入同步代码块,如果没有第二个if判断语句,那么线程B也同样会执行实例化对象的操作了。

给对象实例加上volatile保证变量的可见性,是为了防止指令重排,这里介绍指令重排,有点偏移文章重点,暂不介绍,关于什么是指令重排可以网上搜一下相关文章

4.6 静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class SingletonTest06 {
public static void main(String[] args) {
// System.out.println("单线程创建实例=======");
// SingletonClass06 c1 = SingletonClass06.getInstance();
// SingletonClass06 c2 = SingletonClass06.getInstance();
// System.out.println(c1 == c2);
System.out.println("多线程创建实例=======");
//创建10个线程, 在每个 线程中打印单例对象
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//调用Singleton.getInstance()返回单例对象,打印会输出对象的哈希码
System.out.println(SingletonClass06.getInstance());
}
}).start();
}
//程序运行后,输出单例的哈希码都相同,说明是同一个对象
}
}

// 静态内部类完成:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存
// 推荐使用
class SingletonClass06 {

//构造器私有化
private SingletonClass06() {}

//写一个静态内部类,该类中有一个静态属性 Singleton
private static class SingletonInstance {
private final static SingletonClass06 INSTANCE = new SingletonClass06();
}

//提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE

public static SingletonClass06 getInstance() {
return SingletonInstance.INSTANCE;
}
}

这是很多开发者推荐的一种写法,这种静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成对象的实例化。

同时,因为类的静态属性只会在第一次加载类的时候初始化,也就保证了SingletonInstance中的对象只会被实例化一次,并且这个过程也是线程安全的。

推荐使用

4.7枚举

这种写法在《Effective JAVA》中大为推崇,它可以解决两个问题:

1)线程安全问题。因为Java虚拟机在加载枚举类的时候会使用ClassLoader的方法,这个方法使用了同步代码块来保证线程安全。

2)避免反序列化破坏对象,因为枚举的反序列化并不通过反射实现。

推荐使用

4.8 静态内部类+防止反射破坏单例

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
public class SingletonTest08 {
public static void main(String[] args) throws Exception {
Class clazz = SingletonClass08.class;
Constructor constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);
Object c1 = constructor.newInstance();
Object c2 = SingletonClass08.getInstance();
System.out.println(c1 == c2);
}
}

// 静态内部类完成:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存
// 推荐使用
class SingletonClass08 {

// 构造器私有化
private SingletonClass08() {
if (SingletonInstance.INSTANCE != null) {
throw new RuntimeException("不允许创建多个实例");
}
}

// 写一个静态内部类,该类中有一个静态属性 Singleton
private static class SingletonInstance {
private static final SingletonClass08 INSTANCE = new SingletonClass08();
}

// 提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
public static SingletonClass08 getInstance() {
return SingletonInstance.INSTANCE;
}
}

网上的大部分文章介绍单例模式都没有介绍到这一种,这里使用了抛异常的方式防止反射进行创建多个示例

5 总结

至此,关于单例模式的介绍和场景使用已经介绍完了,有任何疑问和沟通讨论的可以给博主留言哦

其他设计模式介绍:

设计模式-工厂方法模式

设计模式-抽象工厂模式

设计模式-建造者模式

更多精彩内容:mrxccc

设计模式-建造者模式

1.变化是永恒的

首先,我们由一个例子来进入入今天的主题

又是一个周三,快要下班了,老大突然拉住我,喜滋滋地告诉我:“××公司很满意我们做的模型,又签订了一个合同,把奔驰、宝马的车辆模型都交给我们公司制作了,不过这次又额外增加了一个新需求:汽车的启动、停止、喇叭声音、引擎声音都由客户自己控制,他想什么顺序就什么顺序,这个没问题吧?”

那任务又是一个时间紧、工程量大的项目,为什么是“又”呢?因为基本上每个项目都是如此,我该怎么来完成这个任务呢?

首先,我们分析一下需求,奔驰、宝马都是一个产品,它们有共有的属性,××公司关心的是单个模型的运行过程:奔驰模型A是先有引擎声音,然后再响喇叭;奔驰模型B是先启动起来,然后再有引擎声音,这才是××公司要关心的。那到我们老大这边呢,就是满足人家的要求,要什么顺序就立马能产生什么顺序的模型出来。我就负责把老大的要求实现出来,而且还要是批量的,也就是说××公司下单订购宝马A车模,我们老大马上就找我“生产一个这样的车模,启动完毕后,喇叭响一下”,然后我们就准备开始批量生产这些模型。由我生产出N多个奔驰和宝马车辆模型,这些车辆模型都有run()方法,但是具体到每一个模型的run()方法中间的执行任务的顺序是不同的,老大说要啥顺序,我就给啥顺序,最终客户买走后只能是既定的模型。

好,需求还是比较复杂,我们先一个一个地解决,先从找一个最简单的切入点——产品类,每个车都是一个产品,如图所示。

类图比较简单,在CarModel中我们定义了一个setSequence方法,车辆模型的这几个动作要如何排布,是在这个ArrayList中定义的。然后run()方法根据sequence定义的顺序完成指定的顺序动作,我们先看CarModel源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public abstract class CarModel {
//这个参数是各个基本方法执行的顺序
private ArrayList<String> sequence = new ArrayList<String>();
//模型是启动开始跑了
protected abstract void start();
//能发动,还要能停下来,那才是真本事
protected abstract void stop();
//喇叭会出声音,是滴滴叫,还是哔哔叫
protected abstract void alarm();
//引擎会轰隆隆地响,不响那是假的
protected abstract void engineBoom();
//那模型应该会跑吧,别管是人推的,还是电力驱动,总之要会跑
final public void run() {
//循环一边,谁在前,就先执行谁
for(int i=0;i<this.sequence.size();i++){
String actionName = this.sequence.get(i);
if(actionName.equalsIgnoreCase("start")){
this.start(); //启动汽车
}else if(actionName.equalsIgnoreCase("stop")){
this.stop(); //停止汽车
}else if(actionName.equalsIgnoreCase("alarm")){
this.alarm(); //喇叭开始叫了
}else if(actionName.equalsIgnoreCase("engine boom")){
//如果是engine boom关键字
this.engineBoom(); //引擎开始轰鸣
}
}
}
//把传递过来的值传递到类内
final public void setSequence(ArrayList sequence){
this.sequence = sequence;
}
}

CarModel的设计原理是这样的,setSequence方法是允许客户自己设置一个顺序,是要先启动响一下喇叭再跑起来,还是要先响一下喇叭再启动。对于一个具体的模型永远都固定的,但是对N多个模型就是动态的了。在子类中实现父类的基本方法,run()方法读取sequence,然后遍历sequence中的字符串,哪个字符串在先,就先执行哪个方法。

两个实现类分别实现父类的基本方法,奔驰模型、宝马模型如代码清单如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    // 奔驰模型
public class BenzModel extends CarModel {
@Override
protected void alarm() {
System.out.println("奔驰车的喇叭声音是这个样子的...");
}
@Override
protected void engineBoom() {
System.out.println("奔驰车的引擎是这个声音的...");
}
@Override
protected void start() {
System.out.println("奔驰车跑起来是这个样子的...");
}
@Override
protected void stop() {
System.out.println("奔驰车应该这样停车...");
}
}
// 宝马模型
public class BMWModel extends CarModel {
@Override
protected void alarm() {
System.out.println("宝马车的喇叭声音是这个样子的...");
}
@Override
protected void engineBoom() {
System.out.println("宝马车的引擎是这个声音的...");
}
@Override
protected void start() {
System.out.println("宝马车跑起来是这个样子的...");
}
@Override
protected void stop() {
System.out.println("宝马车应该这样停车...");
}
}

两个产品的实现类都完成,我们来模拟一下××公司的要求:生产一个奔驰模型,要求跑的时候,先发动引擎,然后再挂挡启动,然后停下来,不需要喇叭。这个需求很容易满足,我们增加一个场景类实现该需求,如代码清单如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Client {
public static void main(String[] args) {
/*
* 客户告诉XX公司,我要这样一个模型,然后XX公司就告诉我老大
* 说要这样一个模型,这样一个顺序,然后我就来制造
*/
BenzModel benz = new BenzModel();
//存放run的顺序
ArrayList<String> sequence = new ArrayList<String>();
sequence.add("engine boom"); //客户要求,run的时候先发动引擎
sequence.add("start"); //启动起来
sequence.add("stop"); //开了一段就停下来
//我们把这个顺序赋予奔驰车
benz.setSequence(sequence);
benz.run();
}
}

运行结果如下所示:

1
2
3
4
5
奔驰车的引擎是这个声音的...

奔驰车跑起来是这个样子的...

奔驰车应该这样停车...

看,我们组装了这样的一辆汽车,满足了××公司的需求。

但是想想我们的需求,汽车的动作执行顺序是要能够随意调整的。我们只满足了一个需求,还有下一个需求呀,然后是第二个宝马模型,只要启动、停止,其他的什么都不要;第三个模型,先喇叭,然后启动,然后停止;第四个……直到把你逼疯为止,那怎么办?我们就一个一个地来写场景类满足吗?不可能了,那我们要想办法来解决这个问题,有了!我们为每种模型产品模型定义一个建造者,你要啥顺序直接告诉建造者,由建造者来建造,类图如下

增加了一个CarBuilder抽象类,由它来组装各个车模,要什么类型什么顺序的车辆模型,都由相关的子类完成。首先编写CarBuilder代码

1
2
3
4
5
6
public abstract class CarBuilder {
//建造一个模型,你要给我一个顺序要求,就是组装顺序
public abstract void setSequence(ArrayList<String> sequence);
//设置完毕顺序后,就可以直接拿到这个车辆模型
public abstract CarModel getCarModel();
}

很简单,每个车辆模型都要有确定的运行顺序,然后才能返回一个车辆模型

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BenzBuilder extends CarBuilder {
private BenzModel benz = new BenzModel();

@Override
public CarModel getCarModel() {
return this.benz;
}

@Override
public void setSequence(ArrayList<String> sequence) {
this.benz.setSequence(sequence);
}
}

非常简单实用的程序,给定一个汽车的运行顺序,然后就返回一个奔驰车,简单了很多。宝马车的组装与此相同

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BMWBuilder extends CarBuilder {
private BMWModel bmw = new BMWModel();

@Override
public CarModel getCarModel() {
return this.bmw;
}

@Override
public void setSequence(ArrayList<String> sequence) {
this.bmw.setSequence(sequence);
}
}

两个组装者都完成了,我们再来看看××公司的需求如何满足,修改一下场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Client {
public static void main(String[] args) {
/*
* 客户告诉XX公司,我要这样一个模型,然后XX公司就告诉我老大
* 说要这样一个模型,这样一个顺序,然后我就来制造
*/
//存放run的顺序
ArrayList<String> sequence = new ArrayList<String>();
sequence.add("engine boom"); //客户要求,run时候时候先发动引擎
sequence.add("start"); //启动起来
sequence.add("stop"); //开了一段就停下来
//要一个奔驰车:
BenzBuilder benzBuilder = new BenzBuilder();
//把顺序给这个builder类,制造出这样一个车出来
benzBuilder.setSequence(sequence);
//制造出一个奔驰车
BenzModel benz = (BenzModel)benzBuilder.getCarModel();
//奔驰车跑一下看看
benz.run();
}
}

运行结果如下所示:

1
2
3
4
5
奔驰车的引擎是这个声音的...

奔驰车跑起来是这个样子的...

奔驰车应该这样停车...

那如果我再想要个同样顺序的宝马车呢?很简单,再次修改一下场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Client {
public static void main(String[] args) {
//存放run的顺序
ArrayList<String> sequence = new ArrayList<String>();
sequence.add("engine boom"); //客户要求,run的时候先发动引擎
sequence.add("start"); //启动起来
sequence.add("stop"); //开了一段就停下来
//要一个奔驰车:
BenzBuilder benzBuilder = new BenzBuilder();
//把顺序给这个builder类,制造出这样一个车出来
benzBuilder.setSequence(sequence);
//制造出一个奔驰车
BenzModel benz = (BenzModel)benzBuilder.getCarModel();
//奔驰车跑一下看看
benz.run();
//按照同样的顺序,我再要一个宝马
BMWBuilder bmwBuilder = new BMWBuilder();
bmwBuilder.setSequence(sequence);
BMWModel bmw = (BMWModel)bmwBuilder.getCarModel();
bmw.run();
}
}

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
奔驰车的引擎是这个声音的...

奔驰车跑起来是这个样子的...

奔驰车应该这样停车...

宝马车的引擎是这个声音的...

宝马车跑起来是这个样子的...

宝马车应该这样停车...

看,同样运行顺序的宝马车也生产出来了,而且代码是不是比刚开始直接访问产品类(Procuct)简单了很多。我们在做项目时,经常会有一个共识:需求是无底洞,是无理性的,不可能你告诉它不增加需求就不增加,这4个过程(start、stop、alarm、engine boom)按照排列组合有很多种,××公司可以随意组合,它要什么顺序的车模我就必须生成什么顺序的车模,客户可是上帝!那我们不可能预知他们要什么顺序的模型呀,怎么办?封装一下,找一个导演,指挥各个事件的先后顺序,然后为每种顺序指定一个代码,你说一种我们立刻就给你生产处理,好方法,厉害!我们先修改一下类图

类图看着复杂了,但还是比较简单,我们增加了一个Director类,负责按照指定的顺序生产模型,其中方法说明如下:

  • getABenzModel方法

组建出A型号的奔驰车辆模型,其过程为只有启动(start)、停止(stop)方法,其他的引擎声音、喇叭都没有。

  • getBBenzModel方法

组建出B型号的奔驰车,其过程为先发动引擎(engine boom),然后启动,再然后停车,没有喇叭。

  • getCBMWModel方法

组建出C型号的宝马车,其过程为先喇叭叫一下(alarm),然后启动,再然后是停车,引擎不轰鸣。

  • getDBMWModel方法

组建出D型号的宝马车,其过程就一个启动,然后一路跑到黑,永动机,没有停止方法,没有喇叭,没有引擎轰鸣。

其他的E型号、F型号……可以有很多,启动、停止、喇叭、引擎轰鸣这4个方法在这个类中可以随意地自由组合。

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
public class Director {
private ArrayList<String> sequence = new ArrayList();
private BenzBuilder benzBuilder = new BenzBuilder();
private BMWBuilder bmwBuilder = new BMWBuilder();
/*
* A类型的奔驰车模型,先start,然后stop,其他什么引擎、喇叭一概没有
*/
public BenzModel getABenzModel(){
//清理场景,这里是一些初级程序员不注意的地方
this.sequence.clear();
//ABenzModel的执行顺序
this.sequence.add("start");
this.sequence.add("stop");
//按照顺序返回一个奔驰车
this.benzBuilder.setSequence(this.sequence);
return (BenzModel)this.benzBuilder.getCarModel();
}
/*
* B型号的奔驰车模型,是先发动引擎,然后启动,然后停止,没有喇叭
*/
public BenzModel getBBenzModel(){
this.sequence.clear();
this.sequence.add("engine boom");
this.sequence.add("start");
this.sequence.add("stop");
this.benzBuilder.setSequence(this.sequence);
return (BenzModel)this.benzBuilder.getCarModel();
}
/*
* C型号的宝马车是先按下喇叭(炫耀嘛),然后启动,然后停止
*/
public BMWModel getCBMWModel(){
this.sequence.clear();
this.sequence.add("alarm");
this.sequence.add("start");
this.sequence.add("stop");
this.bmwBuilder.setSequence(this.sequence);
return (BMWModel)this.bmwBuilder.getCarModel();
}
/*
* D类型的宝马车只有一个功能,就是跑,启动起来就跑,永远不停止
*/
public BMWModel getDBMWModel(){
this.sequence.clear();
this.sequence.add("start");
this.bmwBuilder.setSequence(this.sequence);
return (BMWModel)this.benzBuilder.getCarModel();
}
/*
* 这里还可以有很多方法,你可以先停止,然后再启动,或者一直停着不动,静态的嘛
* 导演类嘛,按照什么顺序是导演说了算
*/
}

顺便说一下,大家看一下程序中有很多this调用。这个我一般是这样要求项目组成员的,如果你要调用类中的成员变量或方法,需要在前面加上this关键字,不加也能正常地跑起来,但是不清晰,加上this关键字,我就是要调用本类中的成员变量或方法,而不是本方法中的一个变量。还有super方法也是一样,是调用父类的成员变量或者方法,那就加上这个关键字,不要省略,这要靠约束,还有就是程序员的自觉性,他要是死不悔改,那咱也没招。

注意 上面每个方法都有一个this.sequence.clear(),估计你一看就明白。ArrayList和HashMap如果定义成类的成员变量,那你在方法中的调用一定要做一个clear的动作,以防止数据混乱。

有了这样一个导演类后,我们的场景类就更容易处理了,××公司要A类型的奔驰车1万辆,B类型的奔驰车100万辆,C类型的宝马车1000万辆,D类型的不需要,非常容易处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BatchClient {
public static void main(String[] args) {
Director director = new Director();
// 1万辆A类型的奔驰车
for (int i = 0; i < 10000; i++) {
director.getABenzModel().run();
}
// 100万辆B类型的奔驰车
for (int i = 0; i < 1000000; i++) {
director.getBBenzModel().run();
}
// 1000万辆C类型的宝马车
for (int i = 0; i < 10000000; i++) {
director.getCBMWModel().run();
}
}
}

2 建造者模式的定义

建造者模式(Builder Pattern)也叫做生成器模式,其定义如下:

Separate the construction of a complex object from its representation so that the same construction process can create different representations.(将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。)

建造者模式的通用类图如图11-4所示。

图11-4 建造者模式通用类图

在建造者模式中,有如下4个角色:

  • Product产品类

通常是实现了模板方法模式,也就是有模板方法和基本方法,这个参考第10章的模板方法模式。例子中的BenzModel和BMWModel就属于产品类。

1
2
3
4
5
public class Product {
public void doSomething(){
//独立业务处理
}
}
  • Builder抽象建造者

规范产品的组建,一般是由子类实现。例子中的CarBuilder就属于抽象建造者。

1
2
3
4
5
6
public abstract class Builder {    
//设置产品的不同部分,以获得不同的产品
public abstract void setPart();
//建造产品
public abstract Product buildProduct();
}
  • ConcreteBuilder具体建造者

实现抽象类定义的所有方法,并且返回一个组建好的对象。例子中的BenzBuilder和BMWBuilder就属于具体建造者。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ConcreteProduct extends Builder {
private Product product = new Product();
//设置产品零件
public void setPart(){
/*
* 产品类内的逻辑处理
*/
}
//组建一个产品
public Product buildProduct() {
return product;
}
}
  • Director导演类

负责安排已有模块的顺序,然后告诉Builder开始建造,在上面的例子中就是我们的老大,××公司找到老大,说我要这个或那个类型的车辆模型,然后老大就把命令传递给我,我和我的团队就开始拼命地建造,于是一个项目建设完毕了。

1
2
3
4
5
6
7
8
9
10
11
public class Director {
private Builder builder = new ConcreteProduct();
//构建不同的产品
public Product getAProduct(){
builder.setPart();
/*
* 设置不同的零件,产生不同的产品
*/
return builder.buildProduct();
}
}

3 建造者模式的应用

11.3.1 建造者模式的优点

● 封装性

使用建造者模式可以使客户端不必知道产品内部组成的细节,如例子中我们就不需要关心每一个具体的模型内部是如何实现的,产生的对象类型就是CarModel。

● 建造者独立,容易扩展

BenzBuilder和BMWBuilder是相互独立的,对系统的扩展非常有利。

● 便于控制细节风险

由于具体的建造者是独立的,因此可以对建造过程逐步细化,而不对其他的模块产生任何影响。

3.2 建造者模式的使用场景

● 相同的方法,不同的执行顺序,产生不同的事件结果时,可以采用建造者模式。

● 多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时,则可以使用该模式。

● 产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能,这个时候使用建造者模式非常合适。

● 在对象创建过程中会使用到系统中的一些其他对象,这些对象在产品对象的创建过程中不易得到时,也可以采用建造者模式封装该对象的创建过程。该种场景只能是一个补偿方法,因为一个对象不容易获得,而在设计阶段竟然没有发觉,而要通过创建者模式柔化创建过程,本身已经违反设计的最初目标。

3.3 建造者模式的注意事项

建造者模式关注的是零件类型和装配工艺(顺序),这是它与工厂方法模式最大不同的地方,虽然同为创建类模式,但是注重点不同。

4 建造者模式的扩展

已经不用扩展了,因为我们在汽车模型制造的例子中已经对建造者模式进行了扩展,引入了模板方法模式。可能大家会比较疑惑,为什么在其他介绍设计模式的书籍上创建者模式并不是这样说的?读者请注意,建造者模式中还有一个角色没有说明,就是零件,建造者怎么去建造一个对象?是零件的组装,组装顺序不同对象效能也不同,这才是建造者模式要表达的核心意义,而怎么才能更好地达到这种效果呢?引入模板方法模式是一个非常简单而有效的办法。

大家看到这里估计就开始犯嘀咕了,这个建造者模式和工厂模式非常相似呀,是的,非常相似,但是记住一点你就可以游刃有余地使用了:建造者模式最主要的功能是基本方法的调用顺序安排,也就是这些基本方法已经实现了,通俗地说就是零件的装配,顺序不同产生的对象也不同;而工厂方法则重点是创建,创建零件是它的主要职责,组装顺序则不是它关心的。

参考书籍:《设计模式之禅》

源码:设计模式

其他设计模式介绍:

设计模式-工厂方法模式

设计模式-抽象工厂模式

设计模式-建造者模式

更多精彩内容:mrxccc

从服务化到云原生-配置

配置(Configguration)对于每个工程师来说都不陌生,相信没有哪个系统是不提供配置参数的。

本文讲解由单机应用的本地配置向分布式应用的配置中心演进过程,以及配置中心的一些核心概念

1 本地配置

在集中式系统架构的单机应用时代,配置大多数通过属性文件的形式存储以Key=Value的形态出现。当然也有使用XML或YAML等更加复杂的方式进行配置(比如Spring的配置文件application.xml)但开发工程师更倾向于将他们归类为代码部分,真正可以动态修改的配置应该是简单、易于理解的、易于修改的。

在单机时代,配置文件就够用了。运维工程师如果想修改配置,登录生产机器,用VIM本文编辑,然后重启应用,或者用定时任务从新加载配置文件就可以生效。

2 配置集中化

服务器增加导致运维工作量增加,分布式系统很难使用本地配置。采用集中化的方式,也就是散落在每台服务器上运维操作集中于一点统一处理,然后程序通过远程通信或异步消息分发到各个服务器。对系统配置进行修改是运维工程师的重要工作之一,所以对配置进行统一管理是大势所趋。

本地配置存在的问题:

  •  配置工作量大;
  •  配置修改遗漏,导致各个节点配置环境不一致;
  •  各个节点配置不一致的时间差长(各个服务器操作时间不同);
  •  配置修改无法动态生效(定时重新加载或重启应用);
  •  直接修改配置文本信息产生错误难于校验;

配置中心能解决上述问题,还能提供额外的便利:

  •  配置工作量少,单点修改,全局生效;
  •  配置修改不容易遗漏;
  •  各个节点配置不一致的时间差比较短;
  •  配置信息可以像业务数据一样被持久化保存,方便恢复,方便搭建环境,方便最终修改历史;
  • 多个系统上线时,配置检查、沟通协调容易;
  •  配置信息放置于应用程序之外,更容易保持应用的无状态化,为容器化和微服务化部署方案提供了强有力的支持。

3 配置值中心和注册中心

很多人认为配置中心和注册中心是可以相互替换的两个同义词,因为他们的使用场景非相似。另外,当前很多开源产品,如zookeeper、etcd等,都同时支持这两种场景,这也更加容易让人误以为他们就是同一事物。但事实上,他们还是有着本质区别的。

注册中心与配置中心的关注点不完全相同。注册中心用于分布式系统的服务质量,多用于管理运行在当前集群中的服务的状态,需要随时进行动态更新。而配置中心则不然,它关注的是配置本身,相比于状态,配置是更加静态和具象的事物。配置的三个要素是快速传播、变更稀疏、环境相关

快速传播:分布式场景下,各个服务节点都需要得到一致的数据,无论是配置还是状态,一旦发生改变往往要求集群中的所有节点同时感知变更

变更稀疏:配置发生变更的情况非常少,因此配置中心对读性能进行优化,而对写要求稍低

环境相关:一般耳熟能详的系统环境有开发环境、测试环境、线上环境,不同的环境对应着不同的配置,但注册中心所关注的应用程序的运行状态和环境是没有任何关系

3.1 读性能

采用配置中心方案后,就必须要考虑远程调用导致的性能下降和配置中心本身的单节点访问压力的问题

解决方案:缓存。

因为配置信息是可穷举的,不可能是海量的,所以一次性杯加载进本地缓存是非常方便的

缓存分为位于配置中心的集中式缓存和位于应用端的本地缓存

  • 集中式缓存:配置信息读远大于写。每次读取磁盘影响性能,缓存到内存。

    • 优点:能访问最新的数据,数据一致性好,提升访问效率;
    • 缺点:没有缓解配置中心的访问压力。
  • 本地缓存:客户端缓存,尽量访问本地,只有在配置发生变化的时候才读配置中心,更新缓存。

    • 优点:减少远程调用,提升访问效率,缓解了配置中心压力。
    • 缺点:数据存在多份,可能不一致。

补充:

缓存击穿:缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞,缓存还有可能降低效率;

3.2 变更实时性

如果使用本地缓存,数据就会存在多个副本,配置中心数据发生变更时,如何将配置信息实时通知给应用客户端。

一般有两种方式:监听实时同步

  • 监听:配置中心的客户端都需要与配置中心建立长连接。配置变化时,配置变化了,主动推送各个客户端,客户端更新缓存。
    • 优点:实时性高;
    • 缺点: 长连接比较消耗系统资源。并且长连接一旦断了,还要从新连接,容错等。

保持长连接有效的方法是:心跳监听服务,一旦发现连接不可用则销毁连接建立新连接。为了保证应用客户端能正确接收到信息变更请求,也需要让客户端给予反馈,不反馈就一直发。客户端要实现幂等性(在应答式通信系统中,可能存存在多发请求的情况; 比如kafka重复消费数据)。 

  • 实时同步:客户端主动定时去询问配置中心。如果发现缓存和配置中心不一致,就更新缓存。这样使用短连接就可以。
  • 优点:节省连接资源,降低服务中心的压力。
  • 缺点:间隔时间长,则配置更新不及时;间隔时间短,则配置中心压力过大,并且做很多无用功。

其他方法;设置缓存失效时间。 

3.3 可用性

配置中心是整个分布式系统的核心,一旦配置中心不可用,整个系统将会受到极大影响

那么如何提升配置中心的可用性呢?服务冗余、缓存

  • 服务冗余:做服务集群,数据备份

    • 1 基于主节点提供服务:主从模式,主节点提供服务,从节点冗余数据备份
    • 2 基于对等节点提供服务:节点对等,在访问量非常大的情况下可以有效分流(但对于配置中心,有点小题大做)
  • 缓存:也是服务冗余的一种,但只是冗余数据,而不是整个服务

    • 优点:提升读取配置信息的性能,可以在配置中心节点全失效时提供应急使用,也叫离线模式。
    • 缺点:缓存更新不了了。

 

3.4 数据一致性

分布式架构下,数据一致性如何保证

一致性三种方案

  • ACID(强一致性)

  • BASE:BASE是Basically Available(基本可用,响应时间上有损失,功能有损失Soft state(软状态,软状态是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同的数据副本之间进行数据同步的过程存在延时Eventually consistent(最终一致性,最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状三个短语的简写。 其核心思 想是即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方法来使系统达到最终一致性。最终一致性是一种特殊的弱一致性:系统能够保证在没有其他新的更新操作的情况下,数据最终一定能够达到一致的状态,因此所有客户端对系统的数据访问都能够获取到最新的值。同时,在没有发生故障的前提下,数据到达一致状态的时间延迟,取决于网络延迟、系统负载和数据复制方案设计等因素。

  • 状态机:状态机是表示实体的状态根据条件转移的数学模型。通过状态机模型,系统可以判断当前不一致状态,以及如何校正不一致状态到一致状态;

  • 基于状态机的数据一致性算法是:ZAB和Raft

  • 成熟的产品有zookeeper(基于ZAB算法);etcd(基于Raft算法)和Consul(基于Raft算法)

那么配置中心选择哪种方案更加合适呢?

配置中心并不需要ACID的事物,也不会有类似于关系型数据库那一复杂跨表的关联操作;对于BASE的最终一致性的柔性事物场景而言,一致性状态没有时间的保证,因此也不适合用于处理相对敏感的配置信息;通过状态机保证数据一致性的处理方式,无论是在一致性还是性能上,都更加适合配置中心。