0%

本节要点:

  • 使用 enum 代替 整型/字符型枚举模式
  • enum 是 final,单例的安全
  • 在 enum 内部使用 abstract 方法使得实例和方法绑定
  • 用 values() 遍历,用 valueOf() 反向索取
  • 使用策略枚举来封装算法

在枚举类加入到 java 大家族之前,为了表达达到枚举的效果,我们使用整形常量来表示,这种表达方式被叫做: int 枚举模式(int enum pattern), 例如:

1
2
3
4
5
6
7
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;

public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;

缺点: 类型不安全 + 描述性不好, 与之类似的还有 String 枚举模式(String enum pattern)。就是用 String 来代替上例中的 int, 这种做法更糟糕,就算拼写错误也能编译通过,很容易引入 bug。

枚举中每个实例都是单例的,是 public static final 的 field。 枚举没有可访问的构造器,所以不能被继承,是真正的 final 类型的 class。enum 提供了一个命名空间,所以不同 enum 中重名是允许的。示例:

1
2
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }

太阳系八大行星枚举示例:

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 enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS(4.869e+24, 6.052e6),
EARTH(5.975e+24, 6.378e6),
MARS(6.419e+23, 3.393e6),
JUPITER(1.899e+27, 7.149e7),
SATURN(5.685e+26, 6.027e7),
URANUS(8.683e+25, 2.556e7),
NEPTUNE(1.024e+26, 2.477e7);
private final double mass; // In kilograms
private final double radius; // In meters
private final double surfaceGravity; // In m / s^2
// Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11;

// Constructor
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}

public double mass() {
return mass;
}

public double radius() {
return radius;
}

public double surfaceGravity() {
return surfaceGravity;
}

public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma}
}
}

枚举中所有的 field 都应该是 final 的。枚举都有 values() 静态方法, 按照声明顺序返回枚举值。

根据枚举类的适用范围制定他的访问权限,如果是普适的,就把他定义成顶层类,比如 math 中控制舍入模式的 RoundingMode 类。

枚举绑定行为的最佳实践:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 普通表示
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;

public double apply(double x, double y) {
switch (this) {
case PLUS:
return x + y;
case MINUS:
return x - y;
case TIMES:
return x * y;
case DIVIDE:
return x / y;
}
throw new AssertionError("Unknown op: " + this);
}
}

缺点:

  • 没有 throw exception 会编译失败
  • 代码脆弱,在添加新操作,如果没有添加 switch 分支的话,新操作不能生效

改进版:

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 enum Operation {
PLUS("+") {
@Override
public double apply(double x, double y) {
return x + y;
}
}, MINUS("-") {
@Override
public double apply(double x, double y) {
return x - y;
}
}, TIMES("*") {
@Override
public double apply(double x, double y) {
return x * y;
}
}, DIVIDE("/") {
@Override
public double apply(double x, double y) {
return x / y;
}
};

private final String symbol;

Operation(String symbol) {
this.symbol = symbol;
}

@Override
public String toString() {
return symbol;
}

public abstract double apply(double x, double y);
}

通过将 apply 方法声明为 abstrct 类型迫使枚举类的每个 field 都必须实现自己的 apply 方法达到绑定的效果,这种做法称为:constant-specific method implementation。

通过使用 values() 方法,可以很方便的实现迭代

1
2
3
4
5
double x = 2.0;
double y = 4.0;
for (Operation op : Operation.values()) {
System.out.printf("%f %s %f = %f%n", x, op , y, op.apply(x, y));
}

如果 enum 的 toString 方法被重写了,可以订制 fromString() 方法实现字符到枚举的转化

1
2
3
4
5
6
7
8
9
10
11
12
// 将枚举的名称和枚举类型配对,存到 map 中
private static final Map<String, Operation> stringToEnum = Stream.of(Operation.values()).collect(Collectors.toMap(Object::toString, e-> e));
// 新增 fromString 方法根据 toString 的值到 map 中取数据
public static Optional<Operation> fromString(String symbol) {
return Optional.ofNullable(stringToEnum.get(symbol));
}

System.out.println(fromString("a"));
System.out.println(fromString("-"));
// output:
// Optional.empty
// Optional[-]

通过 switch 来控制 enum 中的条件选择的例子, 该例用于计算薪资,根据工作日和休息日采取不同的薪资计算。在这个例子中周末工资的理解很有意思,它等于基本工资 + 从一开始就累加的加班工资,这样想的话这个例子理解起来会容易一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;

private static final int MINS_PER_SHIFT = 8 * 60;

int pay(int minutesWorked, int payRate) {
int basePay = minutesWorked * payRate;

int overtimePay;
switch (this) {
// weekends
case SATURDAY:
case SUNDAY:
overtimePay = basePay / 2;
break;
// work day
default:
overtimePay = minutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
}
return basePay + overtimePay;
}
}

在 enum 中使用 switch 有一个弊端, 新添加的类型,比如我想加一个国亲节加班的薪资计算,如果忘了在 switch 中添加相应的分支, 虽然编译能过,然是薪资计算的规则已经出错了。我们通过在该 enum 中添加一个策略枚举来改善它

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
enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);

private final PayType payType;

private static final int MINS_PER_SHIFT = 8 * 60;

PayrollDay(PayType payType) {
this.payType = payType;
} // constructor for weekend

PayrollDay() {
this(PayType.WEEKDAY);
} // constructor for weekday

int pay(int minutesWorked, int payRate) {
return payType.pay(minutesWorked, payRate);
}

private enum PayType {
WEEKDAY {
@Override
int overtimePay(int minsWorked, int payRate) {
return minsWorked <= MINS_PER_SHIFT ? 0 : (minsWorked - MINS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
@Override
int overtimePay(int minsWorked, int payRate) {
return minsWorked * payRate / 2;
}
};

abstract int overtimePay(int minsWorked, int payRate);

private static final int MINS_PER_SHIFT = 8 * 60;

int pay(int minsWorked, int payRate) {
int basePay = minsWorked * payRate;
return basePay + overtimePay(minsWorked, payRate);
}
}
}

所以总结起来就是在枚举内部别用 switch, 在外部鼓励使用。枚举在性能上与 int 相当,但是由于包装成对象形肯定要略差的,但是使用上感觉不出来。所以每当需要一组固定常量,并且在编译时就知道其成员的时候,就应该使用枚举

多个枚举共享行为是可以用策略枚举的形式

枚举中的常量集并不一定要始终保持不变(?不是很清楚怎么理解,没碰到过这种情况)

本节要点:

  • 使用 lambda 代替匿名函数
  • 不要指定 lambda 中的数据类型,除非报错
  • 主要长度,最多三行

名词对照表

EN CN
function type 函数类型
function object 函数对象
function interface 函数接口
type inference 类型推导
raw type 原生类型

自从 java 1.1 发布依赖,如果我们想要创建一个方法对象那么就需要使用到匿名函数

1
2
3
4
5
6
7
8
9
10
11
List<String> list = Arrays.asList("jerry", "tom");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return Integer.compare(o1.length(), o2.length());
}
});
list.forEach(System.out::println);
// output:
// tom
// jerry

这种表述方式可以实现我们的需求,但是实现繁琐并且语义表达不顺畅, 在 java 8 中,我们可以使用 lambda 来代替匿名函数

1
2
3
4
5
6
7
8
9
List<String> list = Arrays.asList("jerry", "tom");
Collections.sort(list, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
list.forEach(System.out::println);

// 甚至可以简写为
Collections.sort(list, Comparator.comparingInt(String::length));

// 或者更甚
list.sort(Comparator.comparingInt(String::length));

再使用 lambda 的时候有一条原则去掉 lambda 中的所有参数类型,除非它能使你的表达更清楚。默认情况下,程序会根据上下文推断出类型,实在不行它会报错的,那个时候你再自己修不迟。

Operator 枚举类优化,可以将参数使用 DoubleBinaryOperator 这个方法接口做优化

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
// 原始代码
enum Operation {
PLUS("+") {
@Override
public double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {
@Override
public double apply(double x, double y) {
return x - y;
}
},
TIMES("*") {
@Override
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE("/") {
@Override
public double apply(double x, double y) {
return 0;
}
};
private final String symbol;

Operation(String symbol) { this.symbol = symbol; }
@Override
public String toString() { return symbol; }
public abstract double apply(double x, double y);
}

通过将上面的 apply() 方法抽象,这个 Operation 的枚举中的行为可以看作是传入两个数,进行计算, 我们将计算抽象,得到如下的简化形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum Operation {
PLUS("+", (x, y) -> x + y),
MINUS("-", (x, y) -> x - y),
TIMES("*", (x, y) -> x * y),
DIVIDE("/", (x, y) -> x / y);
private final String symbol;
private final DoubleBinaryOperator operator;

Operation(String symbol, DoubleBinaryOperator operator) {
this.symbol = symbol;
this.operator = operator;
}

@Override
public String toString() { return symbol; }

public double apply(double x, double y) {
return operator.applyAsDouble(x, y);
}
}

缺点:lambda 没有名字和文档,如果一段算法不能自我描述,或者超出了几行,就别把他放到一个 lambda 函数中。

lambda 注意点:

  • 一个 lambda 一行是最理想的,最多不能超过三行!
  • 绝大多视情况下,使用 lambda 代替匿名函数,但是如果是对抽象类的实现,还是得依靠匿名函数, lambda 并不能完成这样的功能。
  • lambda 不能获取自身引用, 在 lambda 中 this 指代的是外围示例,匿名类中 this 指自己
  • 可能的话,别去序列化 lambda 和 匿名函数
  • lambda 是小函数的最佳表现方式,除非万不得已,不然就别用匿名类实现函数接口

对 java 的 lang 包下的 function 包做一下简要的总结, 写本篇文章时参考的 java 11 的源代码。说实话,我总觉得函数接口的定义,语义上很奇葩,不怎么读的懂,比如源码中 Predicate 的 isEquals 方法是这样定义的

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

简直是看的我一脸的黑人问号啊 ??? 这 TM 什么鬼,有空再研究一下怎么自定义函数接口。

概况

function 包是函数接口的集合,包路径为: java.util.function.*, 接口可以大致分为 5 类

类型 作用 个数
Consumer 接收参数做计算,无返回 8
Supplier 生成数据,对象 5
Predicate 根据参数做返回 Boolean 值的计算 5
Function 接受参数,返会计算值 17
Operator 接受数据并返回计算值 8

示例

Consumer

拿到参数, 运算, 无返回值:

1
Stream.of("a", "b").forEach(System.out::println);

它有一些变种,比如 BiConsumer, Bi 是 Binary 的缩写,表示复数, 两个的意思。这里表示 Consumer 接收两个参数:

1
2
3
4
5
BiConsumer<String, String> biConsumer = (name, action) -> {
System.out.println(String.format("Name: %s is %s ing...", name, action));
};

biConsumer.accept("Jack", "run");

其他变种,比如 DoubleConsumer, 只接受 Double 做参数, 类似的还有 LongConsumer, IntConsumer 等,限制一样:

1
2
3
4
5
6
7
DoubleConsumer consumer = (val) -> {
System.out.println("Val: " + val);
};

consumer.accept(1.0); // output: Val: 1.0
// consumer.accept("test"); - 编译报错
// consumer.accept(1.0L); - 编译报错

还有一类变种,比如 ObjDoubleConsumer, ObjIntConsumer 和 ObjLongConsumer, 表示接收两个参数,但是其中一个是对象类型的:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 设计一个 lambda, 接受 person 对象和 int 值,并用 int 对 person 的年龄 field 赋值

class Person {
String name;
int age;
// 省略 getter/setter/toString 方法
}

ObjIntConsumer<Person> consumer = Person::setAge;
Person p = new Person("Jack");
consumer.accept(p, 30);
System.out.println(p);
// output: Person{name='Jack', age=30}

大多数 *Consumer 接口看书中还有另外一个方法,叫 andThen() 可以达到组合拳的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义两个 lambda, 一个做大写转化,一个做小写转化。 就是他的这个级联的语法总有一种很奇葩的感觉,要先写 andThan 再写 accept 才合法
Consumer<String> consumer01 = (val) -> {
String toUp = val.toUpperCase();
System.out.print(toUp);
};

Consumer<String> consumer02 = (val) -> {
String toUp = val.toLowerCase();
System.out.println(toUp);
};

Stream.of("a", "B").forEach(consumer01.andThen(consumer02).accept);
// output:
// Aa
// Bb

Supplier

生成数据并返回,和工厂方法很像

1
2
3
4
5
Supplier supplier = Math::random;
System.out.println(supplier.get());
System.out.println(supplier.get());
// output: 0.21716622238340733
// output: 0.06868488591912514

和 Consumer 一样,他也有指定返回类型的 type, 像 BooleanSupplier, DoubleSupplier, IntSupplier 和 LongSupplier

Predicate

接受参数,然后再 lambda 中计算,得出一个 Boolean 的结果值

1
2
3
4
// 对准备的 3 个 string 做过滤,输出空字串的个数
long count = Stream.of("a", " ", "B").filter(String::isBlank).count();
System.out.println(count);
// output: 1

filter 中接受的就是 Predicate 类型的表达式,如果计算结果为 true,则保留参数对象,否则过滤掉。

对应的它也有多个变种形式,变种的处理方式和前面的雷同: BiPredicate, DoublePredicate, IntPredicate 和 LongPredicate。

除此之外,大多数的 *Predicate 接口中除了 test() 外还有 and(), negate(), or() 和 isEqual() 方法

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
Predicate<String> checkLength = val -> val.length() > 5;
Predicate<String> startWith = val -> val.startsWith("B");
List<String> ret = Stream.of("Apple", "Banana", "BBC").filter(checkLength.and(startWith)).collect(Collectors.toList());
System.out.println("-------------- Test AND result--------------");
ret.forEach(System.out::println);

ret = Stream.of("Apple", "Banana", "BBC").filter(checkLength.or(startWith)).collect(Collectors.toList());
System.out.println("-------------- Test OR result--------------");
ret.forEach(System.out::println);

ret = Stream.of("Apple", "Banana", "BBC").filter(checkLength.negate()).collect(Collectors.toList());
System.out.println("-------------- Test NEGATE result--------------");
ret.forEach(System.out::println);

ret = Stream.of("Apple", "Banana", "BBC").filter(Predicate.not(checkLength)).collect(Collectors.toList());
System.out.println("-------------- Test NOT result--------------");
ret.forEach(System.out::println);

System.out.println("-------------- Test EQUALS result--------------");
System.out.println(Predicate.isEqual("abc").test("abc"));

// output:
-------------- Test AND result--------------
Banana
-------------- Test OR result--------------
Banana
BBC
-------------- Test NEGATE result--------------
Apple
BBC
-------------- Test NOT result--------------
Apple
BBC
-------------- Test EQUALS result--------------
true

Function

接受参数,计算并返回所得的值

1
2
3
4
5
// 接收一个字符串并将其转化成 integer 类型
Function<String, Integer> func = Integer::valueOf;
int ret = func.apply("100");
System.out.println(ret);
// output: 100

同样,他也有变种,而且特别多

name 作用
BiFunction 接收两个参数, 返回值类型自定
DoubleFunction 接收 Double 参数,返回值自定
IntFunction 接收 Int 参数,返回值自定
LongFunction 接收 Long 参数,返回值自定
DoubleToIntFunction 接收 Double 返回 Int
DoubleToLongFunction 接收 Double 返回 Long
IntToDoubleFunction 接收 Int 返回 Double
IntToLongFunction 接收 Int 返回 Long
LongToDoubleFunction 接收 Long 返回 Double
LongToIntFunction 接收 Long 返回 Int
ToDoubleFunction 接收参数类型自定,返回 Double
ToIntFunction 接收参数类型自定,返回 Int
ToLongFunction 接收参数类型自定,返回 Long
ToIntBiFunction 接收两个参数, 返回 Int
ToLongBiFunction 接收两个参数, 返回 Long
ToDoubleBiFunction 接收两个参数, 返回 Double

相比其他的几个 *Function 接口, BiFunction 和 Function 要更特殊一点,他们除了最基本的 apply() 之外还有一些额外的方法。

Function 和 BiFunction 还有一个相同的 andThen() 方法,他会再前一个返回值的基础上再做计算

1
2
3
4
5
6
7
8
// 对 Stream 中的数进行 x 100 -10 操作
Function<Integer, Integer> funcx100 = val -> val * 100;
Function<Integer, Integer> funcMinus10 = val -> val - 10;
List<Integer> ret = Stream.of(1, 2).map(funcx100.andThen(funcMinus10)).collect(Collectors.toList());
ret.forEach(System.out::println);
// output:
// 90
// 190

初此之外 Function 还有几个特殊的方法, compose() 他是在 apply() 之前执行的,注意泛型返回值的承接

1
2
3
4
5
6
// 设计两个 lambda 函数,将测试字符串中的数字部分抽出来,并格式化
Function<String, Integer> funcComp = val -> {String intVal = val.split(":")[1];
return Integer.valueOf(intVal);};
Function<Integer, String> func = val -> "[" + val + "]";
List<String> ret = Stream.of("Jack:30", "Jerry:18").map(func.compose(funcComp)).collect(Collectors.toList());
ret.forEach(System.out::println);

Function 还有一个 identity() 方法,传入什么返回什么,完全不能领会它有什么用,到是网上一些例子中,可以用来快速生成 map 的用法让人挺印象深刻的

1
2
3
4
Map<String, Integer> map = Stream.of("i", "love", "you").collect(Collectors.toMap(Function.identity(), String::length));
System.out.println(map);
// output:
// {love=4, i=1, you=3}

Operator

一个计算表达式,最基本的类型为 UnaryOperator, 翻译为 一元表达式, 它是 Function 的一个子类, 可以看成是定制版/特殊形式的 Function,只用于计算,看网上的例子貌似是这样

1
2
3
UnaryOperator<Integer> operator = val -> val ^ 2;
System.out.println(operator.apply(4));
// output: 6

与之类似的还有 LongUnaryOperator 表示 long 类型的一元运算, 同理推至 IntUnaryOperator, DoubleUnaryOperator。

DoubleBinaryOperator 两个 Double 类型数据的运算,同理推至 IntBinaryOperator 和 LongBinaryOperator。

在 Operator 一族中,比较特殊的是 BinaryOperator, 他的方法中有两个计算最值的方法 minBy()maxBy()

1
2
3
4
5
6
BinaryOperator<Integer> max = BinaryOperator.maxBy(Comparator.naturalOrder());
System.out.println(max.apply(1,2));

BinaryOperator<Integer> min = BinaryOperator.minBy(Comparator.naturalOrder());
System.out.println(min.apply(1, 2));
//output: 2 1

Java 8 函数式编程读书笔记

第一章 简介

  • Java 8 增加 Lambda 表达式来支持对大型数据的并发操作 - 核实一下
  • 面向对象式对数据进行抽象,函数式编程时对行为进行抽象

第二章 Lambda 表达式

以 Swing 为例,传统做法中监听事件需要如下代码:

1
2
3
4
5
6
7
button.addActionListener(
new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("button clicked");
}
}
);

当我们使用 lambda 表达式时可以简写为 button.addActionListener(event -> System.out.println("button clicked"));

Lambda 表达式中的类型都是由编译器推断出来的,但你也可以显示的声明参数类型。

Lambda 表达式引用的是值,而不是变量。

接口 参数 返回类型 示例
Predicate<T> T boolean 唱片是否发行
Consumer<T> T void 输出一个值
Function<T, R> T R 获取Artist 对象的名字
Supplier<T> None T 工厂方法
UnaryOperator<T> T T 逻辑非(!)
BinaryOperator<T> (T, T) T 求连个数的乘积(*)

在复杂的情况下需要指定泛型类型才能使编译通过

1
2
// 如果省略掉 <Long> 编译报错:Operator'&#x002B;'cannotbeappliedtojava.lang.Object,java.lang.Object.
BinaryOperator<Long> addLongs = (x, y) -> x + y;

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
26
27
28
29
30
// 检测字符串是否以 J 开头
List<String> names = Arrays.asList("Jerry", "Tom");
List<String> ret = names.stream().filter(name -> name.charAt(0) == 'J').collect(Collectors.toList());

// filter 中的部分就是 Predicate 表达式,也可以分开定义写成如下形式
Predicate<String> filterTom = input -> input.equals("Tom");
ret = names.stream().filter(filterTom).collect(Collectors.toList());

// 自定义 predicate 表达式
public class PredicateSamples {
Predicate<String> checkLength = input -> input.length() > 5;
@Test
public void test_checkLength() {
// false
System.out.println(checkLength.test("1234"));
// true
System.out.println(checkLength.test("123456"));
}
}

// 定义方法生成 predicate 作为返回值
static Predicate<String> generatePredicateExpression(String prefix) {
return test -> prefix.startsWith(test);
}

@Test
public void test_generate_expression() {
// true
System.out.println(generatePredicateExpression("jack123").test("jack"));
}

Consumer 对传入的参数做操作,没有返回值,例如可以用它实现打印,断言等操作

1
2
ret.forEach(System.out::print);
names.forEach(name -> Assert.assertEquals(name, "Jerry"));

Function 对传入的对象操作并放回结果

1
2
List<String> ret = names.stream().map(String::toUpperCase).collect(Collectors.toList());
ret.forEach(System.out::println);

Supplier 可以帮你生产数据, 但是只能使用应用于无参的 constructor,不支持传入参数

1
Supplier<User> userSupplier = User::new;

第三章 流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// for 处理集合模板
int count=0;
for (Artist artist : allArtists) {
if (artist.isFrom("London")) {
count++;
}
}
``

外部迭代方式: 通过拿到 iterator, 然后通过 hasNext(), next() 方法完成迭代

```java
int count=0;
Iterator<Artist> iterator = allArtists.iterator();
while (iterator.hasNext()) {
Artistartist = iterator.next();
if (artist.isFrom("London")) {
count++;
}
}

内部迭代:通过 Steam 对集合类进行复杂操作

1
2
// filter:只保留通过某项测试的对象,整个过程被分为两步,过滤和计算
long count = allArtists.stream().filter(artist -> artist.isFrom("London")).count();

filter 中的表达式是惰性求值方法,count 是及早求职方法,惰性求值并不会真正执行

1
2
3
4
5
// 此实例中并不会在控制台打印文字
allArtists.stream().filter(artist -> {
System.out.println("print artist's location: " + artist.location);
return artist.isFrom("London");
});

如果返回值是 Stream 则为 惰性求值;如果返回值是另一个值或为空则是 及早求值。使用这些操作的理想方式就是形成一个惰性求值的链,最后用一个及早求值的操作返回想要的结果,这正是它的合理之处。

常用的流操作

操作 用途 示例
collect 生成集合 Stream.of(“a”, “b”, “c”).collect(Collectors.toList());
map 类型转换 Stream.of(“a”).map(string -> string.toUpperCase()).collect(Collectors.toList());
filter 检查过滤 Stream.of(“a”, “12b”).filter(val -> isDigit(val.charAt(0))).(Collectors.toList());
flatMap 拼接多个 Stream Stream.of(asList(1, 2), asList(3, 4)).flatMap(numbers -> numbers.stream()).collect(toList());
max/min 最值 tracks.stream().min(Comparator.comparing(track -> track.getLength())).get();
reduce 提供计算功能 Stream.of(1,2,3).reduce(0, (acc, ele) - > acc + ele);

第四章 类库

日志优化

1
2
3
4
Logger logger = new Logger();
if (logger.isDebugEnabled()) {
logger.debug("Look at this:" + expensiveOperation());
}

使用 Lambda 优化

1
2
3
4
5
6
7
8
9
Logger logger = new Logger();
logger.debug(() -> "Look at this:" + expensiveOperation());

// 在 Logger 类中添加方法
public void debug(Supplier<String> message) {
if (isDebugEnabled()) {
debug(message.get());
}
}

Supplier -> get(), Predicate -> test, Function -> apply.

如果可以的话,在流中尽量使用对基本类型的操作,而不是封装类型。 mapToInt 之类的操作还提供了很多简便操作得到最值和平均值。

Optional是一个新设计的数据类新来替换 null 值。 使用它有两个目的:

  • Optional 对象鼓励程序员适时检测变量是否为空,以避免代码缺陷
  • 将一个类的 API 中可能为空的值文档化,这比阅读实现代码要简单的很多

第五章 高级集合类和收集器

方法引用语法, artist -> artist.getName() 等价于 Artist::getName, 标准语法为 Classname::methodName. 由此,新建 Artist 对象的代码可以由 (name, nationality) -> new Artist(name, nationality) 简化为 Artist::new, 类似的可以通过 String[]::new 创建新的数组。

stream.collect() 可以生成你想要的集合形式。例如:stream.collect(toCollection(TreeSet::new));

partitioningBy 收集器可用于分流, 与之类似的还有 groupingBy 关键字。

1
2
3
public Map<Boolean, List<Artist>> bandsAndSolo(Stream<Artist> artists) {
return artist.collect(partitioningBy(artist -> artist.isSolo())); // artist -> artist.isSolo() 可替换为 Artist::isSolo
}

字符串流操作示例:

1
String ret = artists.steam().map(Artist::getName).collect(Collectors.joining(",", "[", "]"));

查询并加入 map 的简化操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// before
public Artist getArtist(String name) {
Artist artist = artistCache.get(name);
if (artist == null) {
artist = readArtistFromDB(name);
artistCache.put(name, artist);
}
return artist;
}

// after
public Artist getArtist(String name) {
return artistCache.computeIfAbsent(name, this::readArtistFromDB);
}

通过 forEach 简化 map 的统计操作:

1
2
3
4
5
6
7
8
9
10
11
12
// before
Map<Artist, Integer> countOfAlbums = new HashMap<>();
for(Map.Entry<Artist, List<Album>> entry : albumsByArtist.entrySet()) {
Artist artist = entry.getKey();
List<Album> albums = entry.getValue();
countOfAlbums.put(artist, albums.size());
}
// after
Map<Artist, Integer> countOfAlbums = new HashMap<>();
albumsByArtist.forEach((artist, albums) -> {
countOfAlbums.put(artist, albums.size());
});

第六章 数据并行化

并行化:同一任务拆分,多核执行
并发化:单核多任务

实现上只需要在调用方法时将 .stream() 改为 .parallelStream() 就行了。但是并不是并行了就快,取决于处理量等其他因素。

影响因素:数据大小, 源数据结构, 装箱, 核的数量, 单元处理开销, 底层还是使用了 fork/join 的模式。

数据结构并行性能:ArrayList, 数组, IntStream.range > HashSet, Treeset > LinkedList, Streams.iterate, BufferedReader.lines

为 array 赋初值:

1
2
3
int[] a = new int[100];
Arrays.setAll(a, i->i);
// 输出:0,1,3.。。99

第七章 测试,调试和重构

ThreadLocal 优化:

1
2
3
4
5
6
7
8
// before
ThreadLocal<album> thisAlbum = new ThreadLocal<Album> () {
@Overrride protected Album initialValue() {
return database.lookupCurrentAlbum();
}
}
// after
ThreadLocal<Album> thisAlbum = ThreadLocal.withInitial(() -> database.lookupCurrentAlbum());

可以使用 peek 进行流的调试

第八章 设计和架构的原则

列举了 Lambda 和 设计模式, DSL 的结合的例子,和我看这本书的初衷有点远了,先跳过。

第九章 使用 Lambda 表达式编写并发程序

使用 Vertx 框架结合 Lambda 的知识点,实现一个聊天室,跳过。但是它的这个框架我倒是感觉很有意思,灵感是从 NodeJS 那边来的,支持并发。

工作中遇到的一些例子

如果集合包含范型信息,在没有指定具体的范型类的时候,调用 lambda 会报编译错误

当使用注释掉的语句代替现有的语句时就会报编译错误:Non-static method cannot be referenced from a static contex

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
@Test
public void test4()
{
Set<MyProp> props = new HashSet<>();
MyProp p1 = new MyProp(1);
MyProp p2 = new MyProp(2);
props.add(p1);
props.add(p2);
MyInterface<String> i0 = new MyInterface<>(props); // MyInterface i0 = new MyInterface<>(props);
System.out.println(i0.getList().stream().map(MyProp::getId).collect(Collectors.toList()));
}
private class MyInterface<T> {
private Set props;
MyInterface(Set props) {
this.props = props;
}
public Set<MyProp> getList() { // public Set getList()
return props;
}
}
private static class MyProp {
private final int id;
public MyProp(int id) { this.id = id; }
public int getId() { return id; }
}

找了一下网上的解释,虽然有重现和解决方案,但是对它的底层原因并没有很清楚的解释,以后如果有机会再深入了解 lambda 的语法的话,可以再看看

PS: 个人感觉应该是在没有指定 type 的时候,类型判断有问题

从 List 中抽取属性组成新的集合

List of BeanProperty, BeanProperty 有 getName() 方法,如何通过 lambda 函数抽取

1
List<String> ret = list.stream().map(BeanProperty::getName).collect(Collectors.toList());

Map -> Map 转化

Map<String, List<Obj>> 对 list 中的值进行修改,案例简化为 Map<String, List<String>> 将 list 中的 String 转化为大写

第一步先熟悉 list -> list 转化方式

1
2
3
4
List<String> test = Arrays.asList("a", "c");
List<String> answer = test.stream().map(String::toUpperCase).collect(Collectors.toList());
System.out.println(answer);
//output: [A, C]

熟悉 map 转化方式并结合 list 转化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Map<String, List<String>> origin = new HashMap<>();
origin.put("a", Arrays.asList("a", "n"));
origin.put("b", Arrays.asList("b"));
origin.put("c", Arrays.asList("c"));

System.out.println(origin);

Map<String, List<String>> after = origin.entrySet().stream().
collect(Collectors.toMap(
Map.Entry::getKey, (entry) -> entry.getValue().stream().map(String::toUpperCase).collect(Collectors.toList()))
);
System.out.println(after);
// output:
// {a=[a, n], b=[b], c=[c]}
// {a=[A, N], b=[B], c=[C]}

Collectors.toMap() 怎么使用

函数定义:

1
2
3
4
5
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}

简单理解就是两个参数分别是两个计算式,得到 key 和 value 的对应的值

1
2
List<Class<? extends Number>> list = Arrays.asList(Integer.class, Double.class, Long.class);
Map map = list.stream().collect(Collectors.toMap(Function.identity(), x->0));

Function.identity() 等价于 x -> x

x -> 0 等价于设置常量

Hexo 提供全区搜索功能很方便,在两个 _config.yml 文件下添加配置就行了,一个在 hexo 下,一个在 next 皮肤下。

root -> _config.yml 添加配置

1
2
3
4
5
6
# Config for search service
search:
path: search.xml
field: post
format: html
limit: 10000

root -> themes -> next -> _config.yml

1
2
local_search:
enable: true

Issues

某一天突然发现部分 Post 不能被 search 出来了。 看这个插件的配置信息,建立的索引应该是放在这个 search.xml 文件里了。排查发现是 Splunk 之后一个都失效了。继续排查,是这片文章中有个 ‘Steps:’ 的节点,在编辑器里面查看是没什么问题的,但是贴到其他工具,比如 idea 或者 browser 里面时,他会带一个 [BS] 的前缀。太神奇了。。。所以之前一直没发现。

在 VSCode 里面看结构还是 <p>Steps</p> 但是用 linux cat 时就变成 <pSteps:</p> 所以后面的 search 解析就出问题了。查了下 BS 代表的是退格键 0x008, 也解释了为什么 xml p 标签会少一个尖括号了 ╮( ̄▽ ̄””)╭

2021-05-20

Docker 弹射起步的那篇文章并没有建立索引,搜索不到,只能通过 tag 导航过去。稍微检查了一下,和之前的文章里面混入了特殊字符的问题还不一样,有待查证

搜索引擎使用技巧的那篇文章也不能搜索出来

2021-06-02

jmockit, testng 等关键字也没有建索引,感觉我的忍耐快要到极限了 (#゚Д゚)

2021-06-03

本地启动 server,search 了一下,很多关键字都是可以找出来的。。。难道是远端的部署有问题?!

通过 F12 + network 的 tab, 点击 search 看网络加载。remote 的 search.xml 位于 https://jack-zheng.github.io/hexo/search.xml 这路径下。加载的时候 terminal 会报错,难道我找到了华点?!!

1
2
3
4
This page contains the following errors:
error on line 3463 at column 23: Input is not proper UTF-8, indicate encoding !
Bytes: 0x08 0x20 0xE5 0x88
Below is a rendering of the page up to the first error.

灯噔, 密码正确,删掉了对应的文件,search 正常了,但是我反复看了下那个文件,没看出什么问题啊。。。

终于找到问题所在了!!!关键字没有设置对,害我绕了老大一圈才找到答案。如果早点用关键字 Hexo search.xml Input is not proper UTF-8 的话,早就出结果了。之前一直纠结在 Chrome 等主题那边

用 hexo 插件等关键字发现几个和我一样的案例,很快的解决了问题。。。

解决步骤:在 vscode 中打开你的项目,点搜索,点正则匹配,根据你的世纪情况搜索你要的关键字。比如我报错是 0x08 0x20 0xE5 0x88 就直接搜索 \x08, 把搜索出来的地方都改了就行。

选中出问题的字段直接把它复制黏贴到终端,可以看到 code, ^H如果是 的显示,好神奇

Search issue

又做了一些深入调查,这种字符好像是控制字符,可能是打字的时候无意间打出来的,你可以通过 vscdoe 里面的设置,将它默认显示出来

Code -> Perferences -> Settings -> 搜索 renderControlCharacters 勾选上即可

Q: 什么是反向代理

反向代理是相对正向代理来说的。正向代理隐藏了客户端,反向代理隐藏了服务器端。比如我们搭建 VPS 服务器翻墙就是一个正向代理的例子。我们可以通过代理服务器对 Google 发起请求拿到信息。相对的,www.google.cn 就是一个反向代理,这个域名的背后有很多服务器,但我们不 care,他代表谷歌,这就足够了。

Q: 为什么 BiFunction 可以表示为两个参数的函数接口

这个命名还是很贴切的,在英语中 bi 前缀有两个的意思,来源于拉丁语。类似的单词还有 bifocal(河流分叉),bifurcate(双面显微镜)等。

常用小工具记录

截图

想要截取一下某堂公开课的课件,但是他是嵌入在页面的 scroller bar 组件当中的,截取很不方便,试了一些像 FireShot 之类的 Chrome 扩展,也不能很好的完成任务。最后通过 FastStone Capture 解决了问题

  • FastStone Capture:一个 Windows 平台下的截图工具,功能还挺多,也挺全,不过应该有点年头了,界面很复古。应该是之支持 Win 平台的,20$ 不过送了 30 天的体验,还是很良心的,滚动截图简直优秀
  • 如果光是 Chrome 的话,根本不需要安装插件,Ctrl + Shift + I 调出调试界面, Ctrl + Shift + P 调出 Chrome 终端界面,然后输入关键字 screen 选择 Capture all screen 就可以截取全部网页了。 在选中节点的情况下,也可以通过选择 Capture node screen 截取单个元素的图片,很方便,但是我那种情况并不好使。。。

下载

是不是还在为迅雷的各种广告,各种资源不能下载而烦恼,试试 Motrix 这个开源项目,还不错。已经有官方支持了,不需要什么操作直接下载安装就行,还多平台支持,溜的一批。

树莓派搭建 FastDFS 服务记录

小结写在最前面:此方案可行,稍微有点坑,但是踩踩还是可以过的

小知识

FastDFS 分布式文件存储系统,阿里系程序员余庆开发完成

FastDFS 只负责存储,不提供web支持,所以一般要搭配 nginx 使用

工作模式: client -> (tracker server + storage server), tracker 信息负责管理, storage 负责存储

树莓派的默认账号: pi | raspberry

可以通过终端输入: ssh pi@host 链接

搭建步骤

依赖关系:

1
2
3
4
5
6
7
8
9
10
.
├── FastDFS-nginx-module
├── fastdfs(tracker + storage)
│ ├── GCC
│ ├── libevent
│ └── perl
├── libfastcommon
└── nginx
├── pcre-devel
└── zlib-devel

一些坑

一些包的名字在 RedHat 和 Debain 下的名字不一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apt-get install git gcc make automake autoconf libtool pcre pcre-devel zlib zlib-devel openssl-devel wget vim

E: Unable to locate package gcc-c++
E: Unable to locate package pcre
E: Unable to locate package pcre-devel
E: Unable to locate package zlib
E: Unable to locate package zlib-devel
E: Unable to locate package openssl-devel

gcc-c++ 可以用 g++ 代替,不过好像 gcc 安装之后自动就装好了

pcre,pcre-devel 用 libpcre3 libpcre3-dev 代替

zlib: zlib1g
openssl-devel: libssl-dev

按照教程走,输入 ./make.sh && ./make.sh install 还是报错,说 permission denied. 试试 sudo -i

启动 nginx:

  1. cd 到 root@raspberrypi:/usr/local/nginx/sbin# 路径下运行 ./nginx
  2. 如果是重启就 ./nginx -s reload

wget 下载文件并重命名:

wget -c ‘url’ -O ‘rename’

查看已经上传的图片

根据 storage 配置找到路径,各种 cd 进去可以看到你已经上传的文件,我本地测试的时候是在这里 /home/dfs/data/00/00

1
2
3
4
5
6
7
8
➜  00 ls -al
total 1072
drwxr-xr-x 2 root root 4096 Mar 14 15:10 .
drwxr-xr-x 258 root root 4096 Mar 8 11:38 ..
-rw-r--r-- 1 root root 1024694 Mar 8 11:44 wKgBal5k2qGAQv4CAA-itrfn0m4.tar.gz
-rw-r--r-- 1 root root 20002 Mar 8 12:52 wKgBal5k6pmAa4jlAABOIgA5mas34.jpeg
-rw-r--r-- 1 root root 20002 Mar 8 15:41 wKgBal5lEiyAfaT6AABOIgA5mas88.jpeg
-rw-r--r-- 1 root root 20002 Mar 14 15:10 wKgBal5s8_eAYY_fAABOIgA5mas81.jpeg

测试图片访问可以在 nginx 启动之后访问 host:8888/group1/M00/00/00/wKgBal5s8_eAYY_fAABOIgA5mas81.jpeg 这样的路径查看

记录一些我经常查找的 python 方法作为备忘

Generate random int list, or just a requirement of loop N times

it’s a common requirement and some guys achieve this goal by using Numpy lib, but it’s too heavy. you can do in this way:

1
2
3
4
5
6
import random

for _ in range(10)
print(random.randint(0, 100))

# the _ is from 0 - 9

Get index and val at the same time

1
2
3
4
5
6
7
8
9
10
a = ['a','b','c','d']
for idx, val in enumerate(a):
print(f'idx = {idx}, val = {val}')


# Output:
# idx = 0, val = a
# idx = 1, val = b
# idx = 2, val = c
# idx = 3, val = d

if you want to specify the start index, you can add a second parameter to enumerate func

1
2
3
4
5
6
7
8
9
10
11

# in this case, idx would start from 3
a = ['a','b','c','d']
for idx, val in enumerate(a, 3):
print(f'idx = {idx}, val = {val}')

# Output:
# idx = 3, val = a
# idx = 4, val = b
# idx = 5, val = c
# idx = 6, val = d

Ipython 交互界面重新引入修改后的包

1
2
import importlib
importlib.reload(some_module)

for loop one line mode

1
2
3
4
5
user_ids = [record['login'] for record in resp]

# if you need if condition
list = [1,2,3,4,5,6]
filter = [str(sub + "tt") for sub in list if sub >= 3]

repr Vs str

  • 只重写 str 只定制在 print() 时的输出
  • 只重写 repr print() 和 调用都输出定制内容
  • 重写 str + repr print() 输出 str 定制内容,调用输出 repr 内容
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
class N1:
def __init__(self, data):
self.data = data

def __str__(self):
return 'N1: data=%s' % self.data

class N2:
def __init__(self, data):
self.data = data

def __repr__(self):
return 'N2: data=%s' % self.data

class N3:
def __init__(self, data):
self.data = data

def __repr__(self):
return 'N3 repr: data=%s' % self.data

def __str__(self):
return 'N3 str: data=%s' % self.data

'''output
n1 = N1(1)
# In [30]: n1
# Out[30]: <BinaryTree.N1 at 0x10853fd30>

print(n1)
# N1: data=1

n2 = N2(2)
# In [33]: n2
# Out[33]: N2: data=2

print(n2)
# N2: data=2

n3 = N3(3)
# Out[36]: N3 repr: data=3

print(n3)
# N3 str: data=3
'''

How to print in string formant

1
2
3
'{{ Test-{} }}'.format('output')

# output: { Test-output }

遍历子目录

1
2
3
4
5
6
7
8
9
10
import os

for root, dirs, files in os.walk('.'):
for sub in files:
print('name: %s' %(os.path.join(root, sub)))

# 或者也可以使用 glob
import glob

glob.glob('./**/*.png', recursive=True)

看 you-get 源码时卡在了 import package 这个点,特此记录一下搜索资料的结果

Import Of Python

你在看 python 代码的时候经常会在文件头部发现一串代码,格式类似 import xxx 或者 from xxx import xxx。功能都是一样的,引入代码重复利用。分两种,一种是引入 module,另一种是映入 package。

  • module 简单理解就是组织好的 python 文件
  • package 即使用文件夹形式组织 python 文件,在 package 的更目录下会有一个 __init__.py 文件作为 package 的入口

相对引用

clone 了 rich 的源码通过 python ./styled.py 运行时报错

1
2
3
4
5
(rich-2qeSub0j-py3.7)  i306454@C02TW719HTD5  ~/gitStore/rich/rich   master  python ./styled.py
Traceback (most recent call last):
File "./styled.py", line 3, in <module>
from .measure import Measurement
ImportError: attempted relative import with no known parent package

这是因为对应的文件中采用了相对引用就是类似 from .style import StyleType 的语法,我们可以通过在上一级目录下输入 python -m rich.styled 运行。注意命令没有 .py 后缀

Reference