《Java 8 in Action》Chapter 4:引入流

1. 流简介

流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理。让我们来看一个实例返回低热量(<400)的菜肴名称:

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
Java7版本:
List<Dish> lowCaloricDishes = new ArrayList<>();
// 用累加器筛选元素
for(Dish d: menu){
if(d.getCalories() < 400){
lowCaloricDishes.add(d);
}
}
// 用匿名类对菜肴排序
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
public int compare(Dish d1, Dish d2){
return Integer.compare(d1.getCalories(), d2.getCalories());
}
});
// 处理排序后的菜名列表
List<String> lowCaloricDishesName = new ArrayList<>();
for(Dish d: lowCaloricDishes){
lowCaloricDishesName.add(d.getName());
}
Java8版本:
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
List<String> lowCaloricDishesName = menu.stream()
.filter(d -> d.getCalories() < 400) // 选出400卡路里以下的菜肴
.sorted(comparing(Dish::getCalories)) // 按照卡路里排序
.map(Dish::getName) // 提取菜肴名称
.collect(toList()); // 将所有的名称保存在List中
利用多核架构并行执行,只需要把stream()换成parallelStream()

Java 8中的Stream API特性:

  • 声明性——更简洁,更易读
  • 可复合——更灵活
  • 可并行——性能更好

流定义:

  • 元素序列——就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序 值。
  • 源——流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集 合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
  • 数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可并行执行。
  • 流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。这让我们下一章中的一些优化成为可能,如延迟和短路。流水线的操作可以看作对数据源进行数据库式查询。
  • 内部迭代——与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。

2. 流与集合

集合与流之间的差异就在于什么时候进行计算。集合是一个内存中的数据结构,它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中。相比之下,流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。集合和流的另一个关键区别在于它们遍历数据的方式。

2.1 只能遍历一次

和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。以下代码会抛出一个异常,说流已被消费掉了:

1
2
3
4
5
6
7
8
9
List<String> title = Arrays.asList(“Java8”,”In”, “Action”);
Stream<String> s = title.stream();
s.forEach(System.out::println);
s.forEach(System.out::println);

Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
at com.lujiahao.learnjava8.chapter4.StreamAndCollection.main(StreamAndCollection.java:16)

2.2 外部迭代与内部迭代

使用Collection接口需要用户去做迭代(比如用for-each),这称为外部迭代。相反,Streams库使用内部迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
集合:用for-each循环外部迭代
List<String> names = new ArrayList<>();
for(Dish d: menu){
names.add(d.getName());
}

集合:用背后的迭代器做外部迭代
List<String> names = new ArrayList<>();
Iterator<String> iterator = menu.iterator();
while(iterator.hasNext()) {
Dish d = iterator.next();
names.add(d.getName());
}

流:内部迭代
List<String> names = menu.stream()
.map(Dish::getName)
.collect(toList());

3. 流操作

java.util.stream.Stream中的Stream接口定义了许多操作。它们可以分为两大类。可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。
中间操作:除非流水线上触发一个终端操作,否则中间操作不会执行任何处理。
终端操作:会从流的流水线生成结果。其结果是任何不是流的值。

流的使用一般包括三件事:

  • 一个数据源(如集合)来执行一个查询;
  • 一个中间操作链,形成一条流的流水线;
  • 一个终端操作,执行流水线,并能生成结果。

流的流水线背后的理念类似于构建器模式。

常见流操作:

4. 小结

以下是你应从本章中学到的一些关键概念。

  • 流是“从支持数据处理操作的源生成的一系列元素”。
  • 流利用内部迭代:迭代通过filter、map、sorted等操作被抽象掉了。
  • 流操作有两类:中间操作和终端操作。
  • filter和map等中间操作会返回一个流,并可以链接在一起。可以用它们来设置一条流水线,但并不会生成任何结果
  • forEach和count等终端操作会返回一个非流的值,并处理流水线以返回结果。
  • 流中的元素是按需计算的。

资源获取

  • 公众号回复 : Java8 即可获取《Java 8 in Action》中英文版!

Tips

  • 欢迎收藏和转发,感谢你的支持!(๑•̀ㅂ•́)و✧
  • 欢迎关注我:后端小哥,专注后端开发,希望和你一起进步!

《Java 8 in Action》Chapter 3:Lambda表达式

1. Lambda简介

可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

  • 匿名——我们说匿名,是因为它不像普通的方法那样有一个明确的名称:写得少而想得多!
  • 函数——我们说它是函数,是因为Lambda函数不像方法那样属于某个特定的类。但和方法一样,Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。
  • 传递——Lambda表达式可以作为参数传递给方法或存储在变量中。
  • 简洁——无需像匿名类那样写很多模板代码。

2. Lambda写法

(parameters) -> expression 或 (parameters) -> { statements; }
eg:(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
Lambda表达式有三个部分:

  • 参数列表——这里它采用了Comparator中compare方法的参数,两个Apple。
  • 箭头——箭头->把参数列表与Lambda主体分隔开。
  • Lambda主体——比较两个Apple的重量。表达式就是Lambda的返回值了。

3. 函数式接口和函数描述符

函数式接口就是只定义一个抽象方法的接口。接口上标有@FunctionalInterface表示该接口会设计成 一个函数式接口,如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。接口现在还可以拥有默认方法(即在类没有对方法进行实现时, 其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。

函数式接口的抽象方法的签名就是Lambda表达式的签名。我们将这种抽象方法叫作:函数描述符。例如,Runnable接口可以看作一个什么也不接受什么也不返回(void)的函数的签名,因为它只有一个叫作run的抽象方法,这个方法什么也不接受,什么也不返回(void)。

4. 三种常用的函数式接口

4.1 Predicate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Represents a predicate (boolean-valued function) of one argument.
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #test(Object)}.
* @param <T> the type of the input to the predicate
* @since 1.8
*/
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
}

Predicate的英文示意是:谓词。
Predicate接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。

4.2 Consumer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Represents an operation that accepts a single input argument and returns no
* result. Unlike most other functional interfaces, {@code Consumer} is expected
* to operate via side-effects.
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #accept(Object)}.
* @param <T> the type of the input to the operation
* @since 1.8
*/
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
* @param t the input argument
*/
void accept(T t);
}

Consumer的英文示意是:消费者。
Consumer接口定义了一个名叫accept的抽象方法,它接受泛型T对象,并没有返回任何值。

4.3 Function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Represents a function that accepts one argument and produces a result.
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #apply(Object)}.
* @param <T> the type of the input to the function
* @param <R> the type of the result of the function
* @since 1.8
*/
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
* @param t the function argument
* @return the function result
*/
R apply(T t);
}

Function的英文示意是:功能。
Function接口定义了一个名叫apply的抽象方法,它接受泛型T对象,并返回一个泛型R的对象。

Java还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操作是自动完成的。但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。Java 8为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。

4.4 Java 8中的常用函数式接口

5. 类型检查、类型推断以及限制

5.1 类型检查

Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。

类型检查过程可以分解为如下所示。

  • 首先,你要找出filter方法的声明。
  • 第二,要求它是Predicate(目标类型)对象的第二个正式参数。
  • 第三,Predicate是一个函数式接口,定义了一个叫作test的抽象方法。
  • 第四,test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean.
  • 最后,filter的任何实际参数都必须匹配这个要求。

这段代码是有效的,因为我们所传递的Lambda表达式也同样接受Apple为参数,并返回一个 boolean。请注意,如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必 须与之匹配。有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要它 们的抽象方法签名能够兼容。比如,前面提到的Callable和PrivilegedAction,这两个接口都代表着什么也不接受且返回一个泛型T的函数。 因此,下面两个赋值是有效的:

1
2
Callable<Integer> c = () -> 42;
PrivilegedAction<Integer> p = () -> 42;

特殊的void兼容规则
如果一个Lambda的主体是一个语句表达式, 它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。
例如,以下两行都是合法的,尽管List的add方法返回了一个 boolean,而不是Consumer上下文(T -> void)所要求的void:

1
2
3
4
// Predicate返回了一个boolean 
Predicate<String> p = s -> list.add(s);
// Consumer返回了一个void
Consumer<String> b = s -> list.add(s);

5.2 类型推断

Java编译器会从上下文(目标类型)推断出用什么函数式接 口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。

1
2
3
4
// 没有类 型推断
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
// 有类型推断
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

5.3 使用局部变量

Lambda表达式 也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。 它们被 称作捕获Lambda。
Lambda捕获的局部变量必须显式声明为final, 或事实上是final。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。

  • 第一,实例变量和局部变量背后的实现有一 个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。
  • 第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(我们会在以后的各章中 解释,这种模式会阻碍很容易做到的并行处理)。

6. 方法引用

方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷 写法,方法引用看作针对仅仅涉及单一方法的Lambda的语法糖。目标引用放在分隔符::前,方法的名称放在后面。方法引用主要有三类:

  • (1) 指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt)。
  • (2) 指向任意类型实例方法的方法引用(例如String的length方法,写作 String::length)。
  • (3) 指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction 用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensive- Transaction::getValue)。
1
2
3
4
5
6
7
对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用: ClassName::new。它的功能与指向静态方法的引用类似。
Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();

这就等价于:
Supplier<Apple> c1 = () -> new Apple(); // 利用默认构造函数创建 Apple的Lambda表达式
Apple a1 = c1.get(); // 调用Supplier的get方法 将产生一个新的Apple

7. 复合Lambda表达式的有用方法

7.1 比较器复合

1
2
3
4
5
6
7
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
// 逆序 按重量递 减排序
inventory.sort(comparing(Apple::getWeight).reversed());
// 比较器链 按重量递减排序;两个苹果一样重时,进一步按国家排序
inventory.sort(comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry));

7.2 谓词复合

1
2
3
4
5
6
7
8
// 产生现有Predicate 对象redApple的非
Predicate<Apple> notRedApple = redApple.negate();
// 链接两个谓词来生成另 一个Predicate对象 一个苹果既是红色又比较重
Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);
// 链接Predicate的方法来构造更复杂Predicate对象 表达要么是重(150克以上)的红苹果,要么是绿苹果
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150)
.or(a -> "green".equals(a.getColor()));
请注意,and和or方法是按照在表达式链中的位置,从左向右确定优 先级的。因此,a.or(b).and(c)可以看作(a || b) && c。

7.3 函数复合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。 比如,
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
int result = h.apply(1);
数学上会写作g(f(x))或(g o f)(x)
这将返回4

compose方法,先把给定的函数用作compose的参数里面给的那个函 数,然后再把函数本身用于结果。
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1);
数学上会写作f(g(x))或(f o g)(x)
这将返回3

8. 小结

以下是你应从本章中学到的关键概念。

  • Lambda表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常的列表。
  • Lambda表达式让你可以简洁地传递代码。
  • 函数式接口就是仅仅声明了一个抽象方法的接口。
  • 只有在接受函数式接口的地方才可以使用Lambda表达式。
  • Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
  • Java 8自带一些常用的函数式接口,放在java.util.function包里,包括Predicate、Function<T,R>、Supplier、Consumer和BinaryOperator,如表3-2所述。
  • 为了避免装箱操作,对Predicate和Function<T, R>等通用函数式接口的原始类型特化:IntPredicate、IntToLongFunction等。
  • 环绕执行模式(即在方法所必需的代码中间,你需要执行点儿什么操作,比如资源分配 和清理)可以配合Lambda提高灵活性和可重用性。
  • Lambda表达式所需要代表的类型称为目标类型。
  • 方法引用让你重复使用现有的方法实现并直接传递它们。
  • Comparator、Predicate和Function等函数式接口都有几个可以用来结合Lambda表达式的默认方法。

资源获取

  • 公众号回复 : Java8 即可获取《Java 8 in Action》中英文版!

Tips

  • 欢迎收藏和转发,感谢你的支持!(๑•̀ㅂ•́)و✧
  • 欢迎关注我:后端小哥,专注后端开发,希望和你一起进步!

《Java 8 in Action》Chapter 2:通过行为参数化传递代码

你将了解行为参数化,这是Java 8非常依赖的一种软件开发模式,也是引入 Lambda表达式的主要原因。行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味 着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用。本章通过筛选苹果这个实际需求来一步步引出Lambda表达式,同时我也会把代码贴出来,读完你会看到代码是如何一步一步的向Lambda转化。多代码来袭,保护我方ADC!!

代码演化

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
package com.lujiahao.learnjava8.chapter2;

import java.util.ArrayList;
import java.util.List;

/**
* 筛选绿色苹果
* @author lujiahao
* @date 2019-02-19 18:28
*/
public class FilterAppleV0 {
public static void main(String[] args) {
List<Apple> appleList = DataUtil.generateApples();
List<Apple> greenAppleList = new ArrayList<>();

for (Apple apple : appleList) {
if ("green".equals(apple.getColor())) {
greenAppleList.add(apple);
}
}

System.out.println("原集合:" + appleList);
System.out.println("绿苹果集合:" + greenAppleList);
}
}

这种之所以称之为实习生版本,是因为此种写法比较初级,所有代码在一个方法中实现。没有进行方法的抽取,不符合面向对象的理念,希望大家在编码工作时避免这种写法。

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
package com.lujiahao.learnjava8.chapter2;

import java.util.ArrayList;
import java.util.List;

/**
* 筛选绿色苹果
* @author lujiahao
* @date 2019-02-19 18:30
*/
public class FilterAppleV1 {

public static void main(String[] args) {
List<Apple> appleList = DataUtil.generateApples();
System.out.println("原集合:" + appleList);

List<Apple> filterGreenApples = filterGreenApples(appleList);
System.out.println("绿苹果集合:" + filterGreenApples);
}

/**
* 筛选绿色苹果
* @param appleList
* @return
*/
public static List<Apple> filterGreenApples(List<Apple> appleList) {
List<Apple> resultList = new ArrayList<>();
for (Apple apple : appleList) {
if ("green".equals(apple.getColor())) {
resultList.add(apple);
}
}
return resultList;
}
}

此版本对筛选绿色苹果的方法进行了简单的抽取,相较于上个版本有了很大的提升。然而,如果需求方改变想法,想筛选红色的苹果。复制filterGreenApples() 方法并将其中的绿色筛选条件改为红色,确实可以实现。但是,这样有太多重复的模板代码,不是良好的编码规范。因此,我们将筛选条件颜色进一步抽象化。

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
37
38
package com.lujiahao.learnjava8.chapter2;

import java.util.ArrayList;
import java.util.List;

/**
* 需要判断的属性作为参数传入
* @author lujiahao
* @date 2019-02-19 18:30
*/
public class FilterAppleV2 {

public static void main(String[] args) {
List<Apple> appleList = DataUtil.generateApples();
System.out.println("原集合:" + appleList);

List<Apple> filterGreenApples = filterApples(appleList, "green");
System.out.println("筛选绿色苹果:" + filterGreenApples);

List<Apple> filterRedApples = filterApples(appleList, "red");
System.out.println("筛选红色苹果:" + filterRedApples);
}

/**
* 筛选特定颜色苹果
* @param appleList
* @return
*/
public static List<Apple> filterApples(List<Apple> appleList, String color) {
List<Apple> resultList = new ArrayList<>();
for (Apple apple : appleList) {
if (color.equals(apple.getColor())) {
resultList.add(apple);
}
}
return resultList;
}
}

满足了颜色的筛选条件,然而需求方又灵光一闪,筛选大于150克的苹果。无论是复制filterApples() 方法,还是增加重量作为参数传入,都是不推荐的编码习惯。第一种方法复制了大部分的代码来实现遍历,它打破了DRY(Don’t Repeat Yourself)的软件工程原则;第二种方法并不能考虑到所有情况,并且每次修改都对原有代码产生了影响,无法做到修改对外封闭的原则。

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
54
55
56
57
package com.lujiahao.learnjava8.chapter2;

import java.util.ArrayList;
import java.util.List;

/**
* 行为参数化
* @author lujiahao
* @date 2019-02-19 18:30
*/
public class FilterAppleV3 {

public static void main(String[] args) {
List<Apple> appleList = DataUtil.generateApples();
System.out.println("原集合:" + appleList);

List<Apple> filterGreenApples = filterApples(appleList, new AppleGreenColorPredicate());
System.out.println("筛选绿色苹果:" + filterGreenApples);

List<Apple> filterHeavyApples = filterApples(appleList, new AppleHeavyWeightPredicate());
System.out.println("筛选重量大于150苹果:" + filterHeavyApples);
}

/**
* 筛选绿色苹果
* @param appleList
* @return
*/
public static List<Apple> filterApples(List<Apple> appleList, ApplePredicate predicate) {
List<Apple> resultList = new ArrayList<>();
for (Apple apple : appleList) {
// 谓词对象封装了条件
if (predicate.filter(apple)) {
resultList.add(apple);
}
}
return resultList;
}

public interface ApplePredicate {
boolean filter(Apple apple);
}

public static class AppleHeavyWeightPredicate implements ApplePredicate {
@Override
public boolean filter(Apple apple) {
return apple.getWeight() > 150;
}
}

public static class AppleGreenColorPredicate implements ApplePredicate {
@Override
public boolean filter(Apple apple) {
return "green".equals(apple.getColor());
}
}
}

我们对苹果的所有属性进行更高一个层次的抽象建模,通过定义ApplePredicate 接口,AppleHeavyWeightPredicate 和 AppleGreenColorPredicate 分别实现该接口来达到进行不同的筛选功能。客户端调用中创建不同的实现类,对于filterApple() 方法而言,是传入了不同的行为,即行为参数化。行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。
其原理如下图所示:

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
42
43
44
45
46
47
48
49
50
51
52
53
package com.lujiahao.learnjava8.chapter2;

import java.util.ArrayList;
import java.util.List;

/**
* 使用匿名类
* @author lujiahao
* @date 2019-02-19 18:30
*/
public class FilterAppleV4 {

public static void main(String[] args) {
List<Apple> appleList = DataUtil.generateApples();
System.out.println("原集合:" + appleList);

List<Apple> filterGreenApples = filterApples(appleList, new ApplePredicate() {
@Override
public boolean filter(Apple apple) {
return "green".equals(apple.getColor());
}
});
System.out.println("筛选绿色苹果:" + filterGreenApples);

List<Apple> filterHeavyApples = filterApples(appleList, new ApplePredicate() {
@Override
public boolean filter(Apple apple) {
return apple.getWeight() > 150;
}
});
System.out.println("筛选重量大于150苹果:" + filterHeavyApples);
}

/**
* 筛选绿色苹果
* @param appleList
* @return
*/
public static List<Apple> filterApples(List<Apple> appleList, ApplePredicate predicate) {
List<Apple> resultList = new ArrayList<>();
for (Apple apple : appleList) {
// 谓词对象封装了条件
if (predicate.filter(apple)) {
resultList.add(apple);
}
}
return resultList;
}

public interface ApplePredicate {
boolean filter(Apple apple);
}
}

当每次有新的查询需求提出,都要新建一个实现类,随着条件越来越多,实现类的数量也在急剧上升。此时,通过使用匿名内部类的方式,来减少实现类过多的模板代码。然而,匿名内部类并非完美,第一,它往往很笨重,因为它占用了很多空间;第二,很多程序员觉得它用起来很让人费解。

6.使用 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
package com.lujiahao.learnjava8.chapter2;

import java.util.ArrayList;
import java.util.List;

/**
* 使用Lambda表达式
* @author lujiahao
* @date 2019-02-19 18:30
*/
public class FilterAppleV5 {

public static void main(String[] args) {
List<Apple> appleList = DataUtil.generateApples();
System.out.println("原集合:" + appleList);

List<Apple> filterGreenApples = filterApples(appleList, (Apple apple) -> "green".equals(apple.getColor()));
System.out.println("筛选绿色苹果:" + filterGreenApples);

List<Apple> filterHeavyApples = filterApples(appleList, (Apple apple) -> apple.getWeight() > 150);
System.out.println("筛选重量大于150苹果:" + filterHeavyApples);
}

/**
* 筛选绿色苹果
* @param appleList
* @return
*/
public static List<Apple> filterApples(List<Apple> appleList, ApplePredicate predicate) {
List<Apple> resultList = new ArrayList<>();
for (Apple apple : appleList) {
// 谓词对象封装了条件
if (predicate.filter(apple)) {
resultList.add(apple);
}
}
return resultList;
}

public interface ApplePredicate {
boolean filter(Apple apple);
}
}

不得不承认这代码看上去比先前干净很多,而且它看起来更像是在陈述问题本身,更加通俗易懂。

7.List 类型抽象化

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
package com.lujiahao.learnjava8.chapter2;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.*;

/**
* List类型抽象话
*
* @author lujiahao
* @date 2019-02-19 18:30
*/
public class FilterAppleV6 {

public static void main(String[] args) {
List<Apple> appleList = DataUtil.generateApples();
System.out.println("原集合:" + appleList);

List<Apple> filterGreenApples = filter(appleList, (Apple apple) -> "green".equals(apple.getColor()));
System.out.println("筛选绿色苹果:" + filterGreenApples);

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

List<Integer> numberList = Arrays.asList(1, 2, 3);
System.out.println("原集合:" + numberList);

List<Integer> numbers = filter(numberList, (Integer i) -> i % 2 == 0);
System.out.println("能被2整除的数:" + numbers);
}

/**
* 筛选绿色苹果
*/
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> resultList = new ArrayList<>();
for (T t : list) {
// 谓词对象封装了条件
if (predicate.filter(t)) {
resultList.add(t);
}
}
return resultList;
}

public interface Predicate<T> {
boolean filter(T t);
}
}

在通往抽象的路上,我们还可以更进一步。目前,filterApples方法还只适用于Apple。还可以将List类型抽象化,从而支持所有类型。

8.演化小结

这一路演化中我们可以看出代码是如何一步一步转化的更加简洁更加优雅,对此我们进行总结:

实例

1.用 Comparator 排序

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
package com.lujiahao.learnjava8.chapter2;

import java.util.Comparator;
import java.util.List;

/**
* 用 Comparator 排序
* @author lujiahao
* @date 2019-03-02 18:34
*/
public class ComparatorDemo {
public static void main(String[] args) {
List<Apple> appleList = DataUtil.generateApples();
System.out.println("原集合:" + appleList);

appleList.sort(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
});
System.out.println("按重量升序:" + appleList);

appleList.sort((Apple a1, Apple a2) -> a1.getColor().compareTo(a2.getColor()));
System.out.println("按颜色字典排序:" + appleList);
}
}

2.用 Runnable 执行代码块

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
package com.lujiahao.learnjava8.chapter2;

/**
* 用 Runnable 执行代码块
* @author lujiahao
* @date 2019-03-02 18:42
*/
public class RunnableDemo {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello Java 8!");
}
});
t.start();

Thread t1 = new Thread(() -> System.out.println("Hello Lambda!"));
t1.start();

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

3.GUI 事件处理

1
2
3
4
5
6
7
8
Button button = new Button(“Send”);
button.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
lable.setText(“Send!!”);
}
}

button.setOnAction((ActionEvent event) -> lable.setText(“Send!!”));

小猿之前搞安卓开发的,各种控件的监听都是这个样子,想想以前各种代码啊啊啊~

总结

以下是你应从本章中学到的关键概念。

  • 行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。
  • 行为参数化可让代码更好地适应不断变化的要求,减轻未来的工作量。
  • 传递代码,就是将新行为作为参数传递给方法。但在Java 8之前这实现起来很啰嗦。为接口声明许多只用一次的实体类而造成的啰嗦代码,在Java 8之前可以用匿名类来减少。
  • Java API包含很多可以用不同行为进行参数化的方法,包括排序、线程和GUI处理。

资源获取

  • 公众号回复 : Java8 即可获取《Java 8 in Action》中英文版!

Tips

  • 欢迎收藏和转发,感谢你的支持!(๑•̀ㅂ•́)و✧
  • 欢迎关注我:后端小哥,专注后端开发,希望和你一起进步!

《Java 8 in Action》Chapter 1:为什么要关心Java 8

自1998年 JDK 1.0(Java 1.0) 发布以来,Java 已经受到了学生、项目经理和程序员等一大批活跃用户的欢迎。这一语言极富活力,不断被用在大大小小的项目里。从 Java 1.1(1997年) 一直到 Java 7(2011年),Java 通过增加新功能,不断得到良好的升级。Java 8 则是在2014年3月发布的。Java 8 所做的改变,在许多方面比 Java 历史上任何一次改变都深远,而且极大的提高了 Java 代码的简洁性。

1. lambda 表达式

本文通过筛选苹果的需求引入 Java 8 ,对 inventory 中的苹果按照重量进行排序。
Java 8 之前的版本:

1
2
3
4
5
Collections.sort(inventory, new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});

Java 8 版本:

1
inventory.sort(comparing(Apple::getWeight));

通过对比我们不难发现,使用 Java 8 可以编写更为简洁的代码,而且代码读起来更接近问题的描述。

2. 方法引用

在 Java 8 之前类(Class)是Java中的一等公民,Java8中将方法和lambda增加为一等公民。方法和lambda作为一等公民,是Java8中方法引用的基础。除了允许(命名)函数成为一等值外,Java 8还体现了更广义的将函数作为值的思想,包括 Lambda1(或匿名函数)。

筛选一个目录中的所有隐藏文件,Java 8 之前版本:

1
2
3
4
5
File[] hiddenFiles = new File(“.”).listFiles(new FileFilter() {
public boolean accept (File file) {
return file.isHidden();
}
}

Java 8 版本:

1
File[] hiddenFiles = new File(".").listFiles(File::isHidden);

3. 流

在Java8之前,遍历处理集合元素,你得用for-each循环一个个去迭代元素,然后再处理元素。我们把这种数据迭代的方法称为外部迭代。相反,有了Stream API,你根本用不着操心循环的事情。数据处理完全是在库内部进行的。我们把这种思想叫作内部迭代。

Java 8 中对于大数据量的集合,用Stream API(java.util.stream)解决了:集合处理时的套路和晦涩,以及难以利用多核这两个问题。

如下展示 Java 8 中使用 Stream API 并行处理数据:

1
2
import static java.util.stream.Collectors.toList;
List<Apple> heavyApples = inventory.parallelStream().filter((Apple a) -> a.getWeight() > 150) .collect(toList());

4. 默认方法

Java 8中加入默认方法主要是为了支持库设计师,让他们能够写出更容易改进的接口。同时,普通开发者也可以在接口中使用默认方法,在实现类没有实现方法时提供方法内容,进一步方便开发。

1
2
List<Apple> heavyApples1 = inventory.stream().filter((Apple a) -> a.getWeight() > 150).collect(toList());
List<Apple> heavyApples2 = inventory.parallelStream().filter((Apple a) -> a.getWeight() > 150).collect(toList());

在Java 8之前,List并没有stream或parallelStream方法,它实现 的Collection接口也没有。Java 8 给接口设计者提供了一个扩充接口的方式,而不会破坏现有的代码。Java 8在接口声明 中使用新的default关键字来表示这一点。这样就实现了改变已发布的接口而不破坏已有的实现。

总结

本章主要总结Java 8 的主要变化(Lambda表达式、方法引用、流和默认方法),为后面更进一步学习打下坚实基础。

资源获取

  • 公众号回复 : Java8 即可获取《Java 8 in Action》中英文版!

Tips

  • 欢迎收藏和转发,感谢你的支持!(๑•̀ㅂ•́)و✧
  • 欢迎关注我:后端小哥,专注后端开发,希望和你一起进步!

阿里开源项目上新,Flutter Go 请查收!

前言

新年伊始,阿里开源项目上新了!此次是阿里拍卖前端团队带来的 Fluttr Go ,针对于时下很火的跨平台移动解决方案推出的实例APP项目,对于Flutter初学者,学习掌握此项目是极其有益的。

Flutter 是什么?

2018年6月21日Google发布Flutter首个release预览版,作为Google 大力推出的一种全新的响应式,跨平台,高性能的移动开发框架。Flutter是一个跨平台的移动UI框架,旨在帮助开发者使用一套代码开发高性能、高保真的Android和iOS应用。

flutter优点主要包括:

  • 开源、跨平台
  • Hot Reload、响应式框架、及其丰富的控件以及开发工具
  • 灵活的界面设计以及控件组合
  • 借助可以移植的GPU加速的渲染引擎以及高性能ARM代码运行时已达到高质量的用户体验

Flutter Go 的由来

  • 帮助开发者快速上手 Flutter
  • Flutter学习资料太少,对于英文不好的同学相对来说比较困难
  • 官网文档示例不够健全,不够直观
  • 各个 widget 的用法各异,属性纷繁,要运行一个 widget 的 demo 往往要到处翻阅各种资料

Flutter Go 的优势

  • 详解常用widget多达 140+
  • 配套 Demo 详解 widget 常规用法
  • 集中整合 widget 案例,一个 APP 搞定所有常用 widget 的用法
  • 持续迭代 ‘追新’ 官方版本

app 预览

后记

跨平台技术蓝海已经出现,未来跨平台开发甚至多端统一的开发模式与语言势必成为主流。而我们大家一般都是只拥有一个技术栈,这就需要我们努力扩展知识宽度广度的同时,也要多涉猎时下新兴技术,紧追技术的时代大潮!

Tips

本文同步发表在公众号,欢迎大家关注!😁 各位大佬点点广告,万分感谢!!!

谷歌推出12款新官方主题,赶快给你的Chrome换换样子!

前言

新年伊始,开工大吉!新的一年,各位大佬准备好码起来了吗!俗话说得好,工欲善其事必先利其器,今天介绍下谷歌官方出品的浏览器主题,让我们的 Chrome 浏览器更加赏心悦目!

随着 macOS Mojave 的发布,黑暗模式不再仅是手机专属,PC 端迎来了黑暗模式主题。小编本人就在第一时间更新系统,黑暗模式绝对的科技感十足!
![](https://raw.githubusercontent.com/lujiahao0708/PicRepo/master/2019/20190213%E8%B0%B7%E6%AD%8C%E6%8E%A8%E5%87%BA12%E6%AC%BE%E6%96%B0%E5%AE%98%E6%96%B9%E4%B8%BB%E9%A2%98%EF%BC%8C%E7%BB%99%E4%BD%A0%E7%9A%84Chrome%E6%8D%A2%E6%8D%A2%E6%A0%B7%E5%AD%90%EF%BC%81/macOS Mojave.png)

Chrome 新主题

为了满足用户对黑暗模式的需求,Google Chrome 团队在新年期间为大众提供了全新的主题模式。这套 Chrome 谷歌浏览器官方新主题共包含 12 款,其中备受欢迎的黑暗模式主题(Just Black),极大的满足了倾向于夜间工作的用户群体。
![](https://raw.githubusercontent.com/lujiahao0708/PicRepo/master/2019/20190213%E8%B0%B7%E6%AD%8C%E6%8E%A8%E5%87%BA12%E6%AC%BE%E6%96%B0%E5%AE%98%E6%96%B9%E4%B8%BB%E9%A2%98%EF%BC%8C%E7%BB%99%E4%BD%A0%E7%9A%84Chrome%E6%8D%A2%E6%8D%A2%E6%A0%B7%E5%AD%90%EF%BC%81/Just Black.png)

除了备受欢迎的黑暗模式外,还有一系列主题可选择,包括 Slate、Oceanic、Ultra Violet、Classic Blue、Banana、Black&White、Honeysuckle、Rose、Serenity、Sea Foam、Marsala、高对比色彩、以及 Pretty in Pink 等。大家可以依据个人喜好选择自己中意的主题。

截止小编发稿是,本次新主题中好评度较高的的几款如下:
![](https://raw.githubusercontent.com/lujiahao0708/PicRepo/master/2019/20190213%E8%B0%B7%E6%AD%8C%E6%8E%A8%E5%87%BA12%E6%AC%BE%E6%96%B0%E5%AE%98%E6%96%B9%E4%B8%BB%E9%A2%98%EF%BC%8C%E7%BB%99%E4%BD%A0%E7%9A%84Chrome%E6%8D%A2%E6%8D%A2%E6%A0%B7%E5%AD%90%EF%BC%81/Ultra Violet.png)
![](https://raw.githubusercontent.com/lujiahao0708/PicRepo/master/2019/20190213%E8%B0%B7%E6%AD%8C%E6%8E%A8%E5%87%BA12%E6%AC%BE%E6%96%B0%E5%AE%98%E6%96%B9%E4%B8%BB%E9%A2%98%EF%BC%8C%E7%BB%99%E4%BD%A0%E7%9A%84Chrome%E6%8D%A2%E6%8D%A2%E6%A0%B7%E5%AD%90%EF%BC%81/Black & White.png)


小编个人选择了 Slate 主题。(我感觉 Just Black 主题太黑了😁,纯个人观点)

恢复默认主题

  1. 主题刚刚安装后,顶部会有一个撤销按钮,如果感觉并不满意当前的主题,可以撤销恢复默认主题。

  2. 如果用户在使用任何主题一段时间后,若要恢复成默认主题,可以单击菜单>设置>外观,然后单击“ 重置为默认值 ”,即可恢复。

    获得主题

    有兴趣朋友可以访问 Chrome 网上应用商店,搜索主题背景,进入主题页面来查看及获得主题。

Tips

本文同步发表在公众号,欢迎大家关注!😁 各位大佬点点广告,万分感谢!!!

Mac iTerm2 配置 rz/sz 上传下载功能

使用mac的同学要进行远程服务器文件的上传下载,推荐使用 sz 和 rz 命令,下文为iTerm2配置的方法。

1. 安装lrzsz

1
brew install lrzsz

2. 下载脚本

https://github.com/mmastrac/iterm2-zmodem

3. 复制脚本

将脚本iterm2-send-zmodem.shiterm2-recv-zmodem.sh复制到/usr/local/bin/目录中即可

1
cp iterm2-recv-zmodem.sh iterm2-send-zmodem.sh /usr/local/bin/

4. 配置 iTerm2

打开 iTerm2,按⌘+,打开Perfences,选择Profiles标签页,在Profiles标签页下选择Advanced标签页,编辑Triggers

Regular expression: rz waiting to receive.\*\*B0100
Action: Run Silent Coprocess
Parameters: /usr/local/bin/iterm2-send-zmodem.sh
Instant: checked

Regular expression: \*\*B00000000000000
Action: Run Silent Coprocess
Parameters: /usr/local/bin/iterm2-recv-zmodem.sh
Instant: checked

图示 :

5. 远程服务器安装lrzsz

CentOS安装方法 : yum -y install lrzsz

6. 使用方法

  1. 本地上传文件到远程服务器

    1. 登录到远程服务器,在远端服务器上输入 rz ,回车
    2. 弹框中选择本地要上传的文件
    3. 确定后等待上传完成
  2. 远程服务器下载文件到本地

    1. 在远程服务器输入 sz filename filename1 ... filenameN
    2. 弹框中选择本地的存储目录
    3. 确定后等待下载完成

至此,我们就可以使用这项黑科技了!

Tips

本文同步发表在公众号,欢迎大家关注!😁 各位大佬点点广告,万分感谢!!!

《深入理解Java虚拟机》第六章 类文件结构

6.1 概述

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。

6.2 无关性的基石

Java虚拟机有两个无关性,即平台无关性和语言无关性。字节码(ByteCode) 是构成平台无关性的基石。在 Java 发展之初,设计者就曾经考虑过并实现了让其他语言运行在 Java 虚拟机之上的可能性,由此 Java 规范拆分成了 Java语言规范《The Java Language Specification》及 Java 虚拟机规范《The Java Virtual Machine Specification》。

In the future, we will consider bounded extensions to the Java virtual machine to provide better support for the other languages.

  • 语言无关性是指虚拟机并不止执行 Java程序,也考虑让其支持其他语言(Groovy/Scala/Kotlin等)的运行。

  • “一次编写,到处运行”。Java的平台无关性即体现在此处,可以在多个平台上运行。

6.3 Class 类文件的结构

Class 文件是一组以8位字节为基础单位的二进制流。Class 文件采用一种类似于 C 语言结构体的伪结构来存储数据 :

  • 无符号数
    • 基本的数据类型
    • u1 / u2 / u4 / u8 分别代表1个字节 / 2个字节 / 4个字节 / 8个字节
    • 可以用来描述数字 / 索引引用 / 数量值或者按照UTF-8编码构成字符串值
    • 复合数据类型
    • 由多个无符号数或者其他表作为数据项构成,习惯以“_info”结尾
    • 用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表

6.3.1 魔数与 Class 文件的版本

每个 Class 文件的头4个字节称为魔数(Magic Number)。唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。

紧接着魔数的4个字节是 Class 文件的版本号 : 第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。

将HelloWorld.java编译成 Class 文件后,使用 Synalyze It 16进制编辑工具查看

也可以使用命令查看 Class 文件的版本号

1
2
3
4
5
6
7
8
9
10
# javap -v HelloWorld.class 
Classfile /Users/lujiahao/HelloWorld.class
Last modified 2019-1-12; size 430 bytes
MD5 checksum f6fad4e65a952d7f01272063b971c2f8
Compiled from "HelloWorld.java"
public class HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
...

同时查看本机 JDK 版本 :

1
2
3
4
# java -version  
java version "1.8.0_161"
Java(TM) SE Runtime Environment (build 1.8.0_161-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.161-b12, mixed mode)

通过上述方式我们可以看到 Class 文件的前9个字节的含义。下面是 Class 文件的版本号汇总 :

发布版本号 内部版本号(十六进制) 内部版本号(十进制)
1.5 31 49
1.6 32 50
1.7 33 51
1.8 34 52

Tips

6.3.2 常量池

6.3.3 访问标志

6.3.4 类索引、父类索引与接口索引集合

6.3.5 字段表集合

6.3.6 方法表集合

6.3.7 属性表集合


欢迎大家关注😁

《深入理解Java虚拟机》第二章 Java 内存区域与内存溢出异常

著名数学家华罗庚先生说:“读一本书要越读越薄。”书越读越薄的过程,就是在多次重复阅读中不断删除冗余信息的过程,浓缩的主要办法是:列提纲与写梗概。前者必须在认真读的基础上,理清文章的脉络,然后逐段概括内容;后者也必须反复阅读,掌握课文要点,将内容加以高度浓缩。浓缩法就是博学反约,厚积薄发,把厚书读薄,又把薄书积厚的读书方法。

2.1 概述

从概念上介绍 Java 虚拟机内存的各个区域,讲解这些区域的作用、服务对象以及其中可能产生的问题,这是翻越虚拟机内存管理这堵围墙的第一步。

2.2 运行时数据区域

JVM 内存结构的布局和相应的控制参数 :

2.2.1 程序计数器

  • 一块较小的内存空间,固定宽度的整数的存储空间
  • 线程私有
  • 当前线程所执行的字节码的行号指示器
  • Java 虚拟机规范中唯一一个没有规定 OutOfMemoryError 的区域
  • 如果线程正在执行 Java 方法,存储的是正在执行的虚拟机字节码指令的地址;如果正在执行 Native 方法,其值为空(Undefined)。

经典问题扩展 : Java 程序计数器为什么不规定 OutOfMemoryError ?

2.2.2 Java 虚拟机栈(Java Virtual Machine Stacks)

  • 线程私有,生命周期与线程相同
  • 运行 Java 方法( 字节码 ) 服务
  • 描述的是 Java 方法执行的内存模型 : 栈帧(Stack Frame),包含:局部变量表、操作数栈、动态链接、方法出口等
  • StackOverflowError 异常:如果线程请求的栈深度大于虚拟机所允许的深度,抛出此异常
  • OutOfMemoryError 异常:如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,抛出此异常

2.2.3 本地方法栈(Native Method Stack)

  • 线程私有,生命周期与线程相同
  • 运行 Native方法服务
  • 与Java虚拟机栈相似,也会抛出StackOverflowError异常和OutOfMemoryError异常

2.2.4 Java 堆(Java Heap)

  • JVM 所管理的内存中最大的一块,垃圾回收的主要操作区域
  • 所有线程共享,虚拟机启动时创建
  • 所有对象实例以及数组都要在堆上分配(非绝对,JIT 和逃逸分析技术发展)
  • 物理上不连续的内存空间,逻辑上是连续的
  • 划分为:新生代( Eden空间、From Survivor空间、To Survivor空间 (分配比例 8:1:1) )和老年代
  • 控制参数
    • -Xms 设置堆的最小空间大小
    • -Xmx 设置堆的最大空间大小
    • -XX:NewSize 设置新生代最小空间大小
    • -XX:MaxNewSize 设置新生代最小空间大小。
  • OutOfMemoryError异常:如果堆中没有内存完成实例分配,并且堆也无法扩展,抛出此异常

2.2.5 方法区(Method Area)

  • 所有线程共享
  • Java 虚拟机规范把方法区描述为堆的一个逻辑部分,别名非堆 Non-Heap,包含:类信息、常量、静态变量、即时编译器编译后的代码等数据
  • HotSpot 虚拟机称为“永久代”(Permanent Generation)
  • 回收效率并不高
  • OutOfMemoryError异常:当方法区无法满足内存分配需求时,抛出此异常

2.2.6 运行时常量池(Runtime Constant Pool)

  • 方法区一部分,所有线程共享
  • 存储编译期生成的各种字面量和符号引用
  • OutOfMemoryError 异常:方法区一部分,受到方法区内存限制,当常量池无法再申请到内存时,抛出此异常
  • 扩展 深入解析String#intern

2.2.7 直接内存(Direct Memory)

  • 并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域
  • OutOfMemoryError 异常:配置虚拟机参数时,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现异常

2.3 HotSpot 虚拟机对象探秘

2.3.1 对象的创建

  1. 类加载检查:new 类名,根据 new 的参数在常量池中定位一个类的符号引用,如果没有找到这个符号引用,说明类还未加载,则进行类的加载/解析和初始化。
  2. 虚拟机为对象分配内存(位于堆中)
  3. 将分配的内存初始化为零值(不包括对象头),如果使用 TLAB ,这一过程可以提前至 TLAB 分配时进行
  4. 调用对象的<init>方法

堆内存分配两种方式:指针碰撞(Bump the Pointer) : Java 堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离。例如:Serial、ParNew 等收集器。空闲列表(Free List) : Java 堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。例如:CMS 这种基于 Mark-Sweep 算法的收集器。

堆内存分配并发解决方案:对分配内存空间的动作进行同步处理,实际上虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。本地线程分配缓冲 TLAB (Thread Local Allocation Buffer),把内存分配的动作按照线程划分为在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定。

2.3.2 对象的内存布局

  • 对象头( Header )
    • 对象自身运行时数据 ( Mark Word ),包含:哈希码 / GC分代年龄 / 锁状态标志 / 线程持有的锁 / 偏向线程ID / 偏向时间戳
    • 类型指针:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
  • 实例数据( Instance Data )
    • 对象真正存储的有效信息
    • 程序代码中定义的各种类型的字段内容
    • HotSpot 虚拟机默认分配策略:longs/doubles/ints/shorts/chars/bytes/booleans/oops(Oridinary Object Pointers)
  • 对齐填充( Padding ),并不是必然存在的,仅仅起着占位符的作用。

2.3.2 对象的访问定位

  • 使用句柄:Java 堆中分配一块内存,reference 中存储的就是对象句柄地址,使用句柄来访问的最大好处就是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中实例数据地址,reference 本身不用改变。如下图所示:
  • 直接指针:Java 堆中分配一块内存,reference 中存储的就是对象实例地址,HostSpot 使用此种方式,节省了一次指针定位的时间开销,提升了速度。如下图所示:

2.4 实战:OutOfMemoryError 异常

  • MacBook Pro Retina, 2.6 GHz Intel Core i7, 16 GB 2133 MHz LPDDR3, OS X Yosemite
  • JDK 1.8

2.4.1 Java 堆溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Java 堆内存溢出
* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* @author lujiahao
* @date 2018-12-21 14:47
*/
public class HeapOOM {
static class OOMObject {

}

public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
System.out.println(list.size());
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid1439.hprof ...
Heap dump file created [28440089 bytes in 0.115 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at OutOfMemory.HeapOOM.main(HeapOOM.java:20)

Process finished with exit code 1

2.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
/**
* 虚拟机栈和本地方法栈溢出
* VM Args: -Xss128k
* @author lujiahao
* @date 2018-12-21 17:02
*/
public class JavaVMStackSOF {
private int stackLength = 1;

public void stackLeak() {
stackLength++;
stackLeak();
}

public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
1
2
3
4
设置128k,启动会报下面的问题
The stack size specified is too small, Specify at least 160k
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
1
2
3
4
5
修改为161k,实现效果
stack length:7738
Exception in thread "main" java.lang.StackOverflowError
at OutOfMemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:22)
at OutOfMemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:22)
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
/**
* 创建线程导致内存溢出 危险!!! 可能导致死机,我就不轻易尝试了
* VM Args: -Xss2M
* @author lujiahao
* @date 2018-12-21 17:11
*/
public class JavaVMStackOOM {
private void dontStop() {
while (true) {

}
}

public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}

public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}

2.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
/**
* 运行时常量池导致内存溢出
* VM Args: -XX:PermSize=10m -XX:MaxPermSize=10M
* jdk1.6
*
* 使用新版本的jdk会输出:
* Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=10m; support was removed in 8.0
* Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=10M; support was removed in 8.0
*
* @author lujiahao
* @date 2018-12-21 17:33
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
// 使用List保持炸常量池引用,避免Full GC回收常量池行为
List<String> list = new ArrayList<>();
// 10MB的PermSize在Integer范围内足够产生OOM了
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 借助CGLib使方法区出现内存溢出
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
* @author lujiahao
* @date 2018-12-21 17:40
*/
public class JavaMethodAreaOOM {
static class OOMObject{}
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(objects, args);
}
});
enhancer.create();
}
}
}

2.4.4 本机直接内存溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 本机直接内存溢出(使用unsafe分配本机内存)
* VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
* @author lujiahao
* @date 2018-12-21 17:51
*/
public class DirectMemoryOOM {
public static final int _1MB = 1024 * 1024;

public static void main(String[] args) throws Exception{
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}

欢迎大家关注😁

还在用if else?策略模式了解一下!

小编在公司负责的就是订单取消业务,老系统中各种类型订单取消都是通过if else 判断不同的订单类型进行不同的逻辑。在经历老系统的折磨和产品需求的不断变更,小编决定进行一次大的重构:消灭 if else。

接下来就向大家介绍下是如何消灭 if else。

1. if else模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class CancelOrderService {

public void process(OrderDTO orderDTO) {
int serviceType = orderDTO.getServiceType();
if (1 == serviceType) {
System.out.println("取消即时订单");
} else if (2 == serviceType) {
System.out.println("取消预约订单");
} else if (3 == serviceType) {
System.out.println("取消拼车订单");
}
}
}

若干个月再来看就是这样的感觉

2. 策略模式

2.1 策略模式实现的Service

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class CancelOrderStrategyService {

@Autowired
private StrategyContext context;

public void process(OrderDTO orderDTO) {
OrderTypeEnum orderTypeEnum = OrderTypeEnum.getByCode(orderDTO.getServiceType());
AbstractStrategy strategy = context.getStrategy(orderTypeEnum);
strategy.process(orderDTO);
}
}

简洁的有点过分了是不是!!!

2.2 各种类型策略实现及抽象策略类

下面选取了即时订单和预约订单的策略.

1
2
3
4
5
6
7
8
@Service
@OrderTypeAnnotation(orderType = OrderTypeEnum.INSTANT)
public class InstantOrderStrategy extends AbstractStrategy {
@Override
public void process(OrderDTO orderDTO) {
System.out.println("取消即时订单");
}
}
1
2
3
4
5
6
7
8
@Service
@OrderTypeAnnotation(orderType = OrderTypeEnum.BOOKING)
public class BookingOrderStrategy extends AbstractStrategy {
@Override
public void process(OrderDTO orderDTO) {
System.out.println("取消预约订单");
}
}
1
2
3
public abstract class AbstractStrategy {
abstract public void process(OrderDTO orderDTO);
}

2.3 策略类型注解

每个策略中增加了注解OrderTypeAnnotation,以标注适用于不同类型的策略内容.

1
2
3
4
5
6
7
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface OrderTypeAnnotation {
OrderTypeEnum orderType();
}

2.4 策略处理器类StrategyProcessor和策略上下文StrategyContext

其中最为核心的为StrategyProcessor 策略处理器类和StrategyContext 策略上下文,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class StrategyProcessor implements BeanFactoryPostProcessor {

private static final String STRATEGY_PACKAGE = "com.lujiahao.strategy";

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
Map<OrderTypeEnum, Class> handlerMap = Maps.newHashMapWithExpectedSize(3);
ClassScanner.scan(STRATEGY_PACKAGE, OrderTypeAnnotation.class).forEach(clazz -> {
OrderTypeEnum type = clazz.getAnnotation(OrderTypeAnnotation.class).orderType();
handlerMap.put(type, clazz);
});

StrategyContext context = new StrategyContext(handlerMap);
configurableListableBeanFactory.registerSingleton(StrategyContext.class.getName(), context);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class StrategyContext {
private Map<OrderTypeEnum, Class> strategyMap;

public StrategyContext(Map<OrderTypeEnum, Class> strategyMap) {
this.strategyMap = strategyMap;
}

public AbstractStrategy getStrategy(OrderTypeEnum orderTypeEnum) {
if (orderTypeEnum == null) {
throw new IllegalArgumentException("not fond enum");
}

if (CollectionUtils.isEmpty(strategyMap)) {
throw new IllegalArgumentException("strategy map is empty,please check you strategy package path");
}

Class clazz = strategyMap.get(orderTypeEnum);
if (clazz == null) {
throw new IllegalArgumentException("not fond strategy for type:" + orderTypeEnum.getCode());
}

return (AbstractStrategy) SpringBeanUtils.getBean(clazz);
}
}
  • 首先会扫描指定包中标有@OrderTypeAnnotation的类
  • 将符合类的对应的枚举值作为key,对应的类作为value,保存在策略Map中
  • 初始化StrategyContext,并注册到spring容器中,同时将策略Map传入其中

我们使用了枚举作为Map中的key,相信大家很少有人这样操作过,不过可以放心操作.通过下面两篇文章解答大家的疑问.

3. 总结

策略模式极大的减少if else等模板代码,在提升代码可读性的同时,也大大增加代码的灵活性,添加新的策略即可以满足业务需求.
本人在我司业务中对策略模式的应用得到了很好的验证,从此再也不用担心产品改需求.
用策略模式一时爽,一直用一直爽😏!

4. 代码

完整代码


欢迎大家关注😁