JDK8提升开发效率有哪些

第06章 JDK8以上提高开发 效率有哪些
1)JAVA现状&版本介绍
Java自1.0版本发布的这18年来,java8中新增的功能可以说 是最大变化的一次。基于函数式编程的思想,它本身并没有 去处掉原有的任何东西,所以就算升级使用了Java8,但是仍 然有很多人在基于之前的开发习惯进行代码编写,并没有使 用到Java8的诸多新功能。
同时也有一些人,在对Java8的新功能做了一些了解之后,认 为其改变了编写代码的一些方式,所以可能会有抵触情绪, 并未在项目中进行使用。
但是Java8提供的新功能本身是很强大的,通过这些新功能或 特性,可以帮助程序员们编写出更清晰、更简洁的代码。
本章节从使用与本质出发,同各位逐步的去学习并掌握Java8 各种新功能与特性的使用与原理。
2)接口的默认方法和静态方法
众所周知,在接口中定义的普通方法都是抽象方法,方法前
面默认都会添加public abstract,不能有方法实现,需要
在接口的实现类中对方法进行具体实现。
1 public interface MyDSService {
2
3 //抽象方法
4 String abstractMethod();
5 }
1 public class MyDSServiceImpl implements
MyDSService {
2
3 public String abstractMethod() {
4 return “execute abstract method”;
5 }
6 }
但是Java8开始允许在接口中定义默认方法和静态方法,对于
这两种方法,可以直接在接口对其进行实现,无需再在接口
实现类中进行实现。
默认方法:扩展方法,在方法前面需通过default修饰。不
能直接通过接口调用,必须通过接口实现类的实例对象进行
方法调用
接口实现类对象.默认方法()
静态方法:类方法,在方法前面需通过static修饰。可以直
接通过接口调用。
接口.静态方法()
方法定义:
1 public interface MyDSService {
2
3 //抽象方法
4 String abstractMethod();
5
6 //默认方法
7 default String defaultMethod(){
8 return “execute default method”;
9 }
10
11 //静态方法
12 static String staticMethod(){
13 return “execute static method”;
14 }
15 }
方法调用:
1 public class DSDemo {
2
3 public static void main(String[] args) {
4
5 //接口实现类实例对象
6 MyDSServiceImpl myDSService = new
MyDSServiceImpl();
7
8 //调用默认方法
9 String defaultMethodResult =
myDSService.defaultMethod();
10
System.out.println(defaultMethodResult);
11
12 //调用静态方法
13
System.out.println(MyDSService.staticMethod
());
14
15 }
16 }
方法重写:
1 //方法重写
2 MyDSService service = new MyDSService() {
3 @Override
4 public String commonMethod() {
5 return null;
6 }
7
8 @Override
9 public String defaultMethod() {
10 return null;
11 }
12 };
普通方法必须实现,默认方法可以选择性重写,静态方法无
法重写。
3)Lambda表达式
3.1)代码书写的不断演进
以查询学生信息为例。
1 public class Student {
2
3 private int id;
4
5 private String name;

38 this.name = name;
39 this.sex = sex;
40 }
41
42 @Override
43 public String toString() {
44 return “Student{” +
45 “id=” + id +
46 “, name=’” + name + ‘’’ +
47 “, sex=’” + sex + ‘’’ +
48 ‘}’;
49 }
50 }
1 public class StudentDemo {
2
3 //通过形参传递姓名
4 public static Student
getStudentInfo(List studentList){
5
6 for (Student student : studentList)
{
7 if (“张
三”.equals(student.getName())){
8 return student;
9 }
10 }
11
12 return null;
13 }
14
15 public static void main(String[] args) {
16
17 List students = new
ArrayList<>();
18 students.add(new Student(1,“张
三”,“M”));
19
20 Student studentInfo =
getStudentInfo(students);
21 }
22 }
这种方式实现起来没有任何的问题,但是如果一旦需求发生
改变,比方说现在还要查询出名字是李四、王五、赵六等。
如果还按照上述的方式进行实现的话,就需要编写大量的判
断。
现在可以直接将名字作为方法的形参进行传递,通过方法形
参指定查询条件。
1 public class StudentDemo {
2
3 //通过形参传递姓名
4 public static Student
getStudentInfo(List
studentList,String name){
5
6 for (Student student : studentList)
{
7 if
(name.equals(student.getName())){
8 return student;
9 }
10 }
11
12 return null;
13 }
14
15 public static void main(String[] args) {
16
17 List students = new
ArrayList<>();
18 students.add(new Student(1,“张
三”,“M”));
19 students.add(new Student(2,“李
四”,“M”));
20 students.add(new Student(3,“王
五”,“M”));
21
22 Student studentInfo =
getStudentInfo(students,“张三”);
23 }
24 }
通过这种方式,不管查询哪个学生,只要在调用这个方法
时,执行名称条件即可,避免编写大量的冗余代码。
但是现在如果又有一个条件,根据性别来查询学生信息,此
时可能又会定义方法,把性别作为方法形参进行传递,这对
功能本身的实现是没有问题的,但是此时因为一个条件的出
现,又要定义一个新的方法,就有点没必要了。
此时可以考虑将姓名和性别作为同一个方法形参进行传递,
然后通过一个标记来进行标识。
1
2
3 public class StudentDemo {
4
5 //传递多个查询条件
6 public static Student
getStudentInfo(List
studentList,String value,String flag){
7
8 for (Student student : studentList)
{
9 if (“name”.equals(flag)){
10 if
(value.equals(student.getName())){
11 return student;
12 }
13 }
14
15 if (“sex”.equals(flag)){
16 if
(value.equals(student.getSex())){
17 return student;
18 }
19 }
20
21 }
22
23 return null;
24 }
25
26 public static void main(String[] args) {
27
28 List students = new
ArrayList<>();
29 students.add(new Student(1,“张
三”,“M”));
30 students.add(new Student(2,“李
四”,“M”));
31 students.add(new Student(3,“王
五”,“M”));
32
33 Student studentInfo =
getStudentInfo(students,“张三”,“name”);
34 Student studentInfo1 =
getStudentInfo(students,“M”,“sex”);
35 }
36 }
这种方式从实现功能的角度来说没有问题,但是这种解决方
案非常的差劲。首先,代码读起来感觉非常不好,其次,这
种方案并不能完美的应对需求的改变,比方说,又要按照年
龄、住址等条件进行查询呢? 可想而知,此时你的功能代码
会接收很多的条件参数,同时也会出现大量的条件判断。不
管从可阅读性、性能、可扩展性上都非常的不友好。
因此现在需要一种能够避免定义很多方法形参的方式来应对
不断变化的需求。基于抽象的思想,现在可以在接口中一个
抽象方法,然后让不同类去实现接口中的抽象方法即可.
1 public interface StudentService {
2
3 Student getStudentInfo(List
studentList,Student student);
4 }
1 public class StudentNameServiceImpl
implements StudentService {
2 @Override
3 public Student
getStudentInfo(List studentList,
Student student) {
4
5 for (Student s : studentList) {
6 if
(s.getName().equals(student.getName())){
7 return s;
8 }
9 }
10 return null;
11 }
12 }
1 public class StudentSexServiceImpl
implements StudentService {
2 @Override
3 public Student
getStudentInfo(List studentList,
Student student) {
4 for (Student s : studentList) {
5 if
(s.getSex().equals(student.getSex())){
6 return s;
7 }
8 }
9 return null;
10 }
11 }
通过上述这种方式,当要根据不同条件进行查询时,只要调
用StudentService不同的实现类即可。
但是这种方法非常啰嗦,需要根据条件不声明多个不同的
StudentService接口的实现类。对于这个问题的解决,可以 通过匿名类进行解决。通过匿名类可以让代码变得非常简 洁。
1 public static void main(String[] args) {
2
3 StudentService studentService = new
StudentService() {
4 @Override
5 public Student
getStudentInfo(List studentList,
Student student) {
6 for (Student s : studentList) {
7 if
(s.getName().equals(student.getName())){
8 return s;
9 }
10 }
11 return null;
12 }
13 };
14 }
通过匿名类,在代码中就不需要重复的声明不同的实现类
了。但是匿名类最大的问题就是不利于阅读。如果代码中充
斥着大量匿名类的话,会让人非常的费解。
3.2)Lambda表达式介绍
Lambda表达式是Java8中非常重要的一个新特性,其基于函
数式编程的思想,支持将代码作为方法参数进行使用。可以
把Lambda表达式理解为通过一种更加简洁的方式表示可传 递的匿名函数。
它本身没有名称,而且不像方法那样属于某一个类,但是可
以有参数列表、代码体、返回值。使用了Lambda表达式之 后就不需要再去编写匿名类了。
3.2.1)Lambda使用规范
Lambda基础格式
1 (参数列表) -> {
2 方法体
3 }
参数列表:即匿名方法的形参
-> :Lambda运算符
方法体:用于执行业务逻辑。可以是单一语句,也可以是语
句块。如果是单一语句,可以省略花括号。当需要返回值,
如果方法体中只有一条语句,可以省略return,会自动根据 结果进行返回。
1)没有参数的Lambda表达式
1 ()->new Student();
2)只有一个参数的Lambda表达式
1 x -> {
2 System.out.println(x);
3 return x;
4 }
3)有多个参数的Lambda表达式
1 (int x,int y) ->{
2 System.out.println(x);
3 System.out.println(x);
4 return x+y;
5 }
上述可以进行简写,因为在Lambda中,参数列表中参数的 数据类型可以交给JVM根据上下文进行推断。所以可以不用定 义类型。
1 (x,y) ->{
2 System.out.println(x);
3 System.out.println(y);
4 return x+y;
5 }
4)一个参数和仅一条语句的Lambda表达式
1 x -> 3+x;
5)多个参数和仅一条语句的Lambda表达式
1 (x,y) -> x+y;
3.2.2)Lambda使用对比
遍历集合
1 public static void main(String[] args) {
2 String[] language = {“c”, “c++”,
3 “c#”,
4 “java”,“python”,
5 “go”,“hive”,
6 “php”};
7 List languageList =
Arrays.asList(language);
8
9 //旧的循环方式
10 for (String s : languageList) {
11 System.out.println(s+",");
12 }
13
14 //lambda循环
15 languageList.forEach(s->
System.out.println(s+","));
16 }
使用Lambda替换匿名内部类使用
1 //匿名内部类
2 Runnable runnable = new Runnable() {
3 @Override
4 public void run() {
5 System.out.println(“Hello world !”);
6 }
7 };
8
9 //使用Lambda
10 Runnable runnable1 = ()->
System.out.println(“hello itcast”);
实现Lambda实现集合排序
1 public static void main(String[] args) {
2
3 String[] language = {“c”, “c++”,
4 “c#”,
5 “java”,“python”,
6 “go”,“hive”,
7 “php”};
8
9 //旧的循环比较方式
10 Arrays.sort(language,new
Comparator(){
11 @Override
12 public int compare(String o1, String
o2) {
13 return (o1.compareTo(o2));
14 }
15 });
16
17 //lambda循环比较
18 Arrays.sort(language,(o1,o2)->
(o1.compareTo(o2)));
19 }
3.3)Lambda表达式底层原理解析
定义一个使用Lambda表达式的方法
1 public class SourceDemo {
2
3 public void demo(){
4
5 String[] language = {“c”, “c++”,
6 “c#”,
7
“java”,“python”,
8 “go”,“hive”,
9 “php”};
10
11 List list =
Arrays.asList(language);
12
13 list.forEach(s->
System.out.println(s));
14 }
15 }
将当前.java文件编译生成.class文件,执行命令后,会在当前 文件夹生成对应的.class文件
1 javac SourceDemo.java
将.class文件进行反编译,查看文件内容
1 javap -p SourceDemo.class
生成内容如下
1 Compiled from “SourceDemo.java”
2 public class
com.itheima.lambda.source.SourceDemo {
3 public
com.itheima.lambda.source.SourceDemo();
4 public void demo();
5 private static void
lambda$demoKaTeX parse error: Expected 'EOF', got '}' at position 24: …ang.String); 6 }̲ 此时可以发现,代码中执行La…&Lambda$1,内部类里的调用方法块 并不是动态生成的,只是在原class里已经编译生成了一个静 态的方法,内部类只需要调用该静态方法。
4)函数式接口
4.1)简介
在Java8中为了让现在有的函数能够更加友好的使用Lambda 表达式,因此引入了函数式接口这个概念。其是一个仅有一 个抽象方法的普通接口。如果声明多个抽象方法则会报错。 但是默认方法和静态方法在此接口中可以定义多个。
要想自定义一个函数式接口的话,需要在接口上添加
@FunctionalInterface。
1 @FunctionalInterface
2 public interface MyFunctionalInterface {
3
4 void execute();
5 }
1 public class MyTest {
2
3 public void demo(MyFunctionalInterface
functionalInterface){
4
5 functionalInterface.execute();
6
7 }
8
9 @Test
10 public void exec(){
11
12 //jdk8之前
13 demo(new MyFunctionalInterface() {
14 @Override
15 public void execute() {
16 System.out.println(“jdk8
before”);
17 }
18 });
19
20 //使用lambda表达式
21 demo(()-> System.out.println(“jdk8
latter”));
22 }
23 }
4.2)常见应用
在Java8的类库设计中,已经引入了几个函数式接口: Predicate、Consumer、Function、Supplier
4.2.1)Predicate使用
Predicate接口是Java8定义的一个函数式接口,属于 java.util.function包下,用于进行判断操作,内部定义一个 抽象方法test、三个默认方法and,negate,or、一个静态方 法isEqual
将符合条件的学生放入集合中,并将集合最终返回
1 package
com.itheima.functionInterface.predicate;
2
3 import com.itheima.lambda.Student;
4
5 import java.util.ArrayList;
6 import java.util.List;
7 import java.util.function.Predicate;
8
9 public class MyPredicateDemo {
10
11 public static List
filter(List studentList,
Predicate predicate){
12 ArrayList list = new
ArrayList<>();
13
14 studentList.forEach(s->{
15 if (predicate.test(s)){
16 list.add(s);
17 }
18 });
19 return list;
20 }
21
22 public static void main(String[] args) {
23 List students = new
ArrayList<>();
24 students.add(new Student(1,“张
三”,“M”));
25 students.add(new Student(2,“李
四”,“M”));
26 students.add(new Student(3,“王
五”,“F”));
27
28 List result =
filter(students, (s) ->
s.getSex().equals(“F”));
29
30
System.out.println(result.toString());
31 }
32 }
4.2.2)Consumer使用
Consumer也是JDK8提供的函数式接口,用于进行获取数据 的操作,其内部定义了一个抽象方法accept、一个默认方法 andThen。
对于accept()方法来说,它接受一个泛型T对象。如果现在需 要访问类型T对象,并对其进行某些操作的话,就可以使用这 个接口。
1 package
com.itheima.functionInterface.consumer;
2
3 import java.util.ArrayList;
4 import java.util.List;
5 import java.util.function.Consumer;
6
7 public class MyConsumerDemo {
8
9 public static void foreach(List
arrays, Consumer consumer){
10
11 arrays.forEach(s-

consumer.accept(s));
12 }
13
14 public static void main(String[] args) {
15
16 List arrays = new
ArrayList<>();
17 arrays.add(“java”);
18 arrays.add(“python”);
19 arrays.add(“go”);
20 arrays.add(“hive”);
21
22 foreach(arrays,(s)->
System.out.print(s+","));
23 }
24 }
4.2.3)Function使用
Function主要用于进行类型转换的操作。内部提供一个抽象 方法apply、两个默认方法compose,andThen、一个静态 方法identity
对于apply方法,它接收一个泛型T对象,并返回一个泛型R的
对象。
1 package
com.itheima.functionInterface.function;
2
3 import java.util.function.Function;
4
5 public class MyFunctionDemo {
6
7 public static Integer convert(String
value, Function<String,Integer> function){
8
9 return function.apply(value);
10 }
11
12 public static void main(String[] args) {
13
14 String value = “黑马程序员,666”;
15 Integer result = convert(value,(s) -
{Integer.parseInt(s) + 222;}
16 );
17
18 System.out.println(result);
19 }
20 }
4.2.4)Supplier使用
Supplier也是用来进行值获取操作,内部只有一个抽象方法 get
1 public class MySupplierDemo {
2
3 public static Integer
getMin(Supplier supplier){
4 return supplier.get();
5 }
6
7 public static void main(String[] args) {
8
9 // 创建数组
10 int[] arr =
{100,20,50,30,99,101,-50};
11
12 Integer result = getMin(() -> {
13 int min = arr[0];
14 for (int i : arr) {
15 if (i<min){
16 min =i;
17 }
18 }
19 return min;
20 });
21
22 System.out.println(result);
23 }
24 }
5)类型检查&推断
根据之前的例子演示,大家会发现,在Lambda表达式中对 于数据类型是可以不用声明的,lambda直接就能进行正确的 运算,那么对于这一点,Lambda表达式内部又是如何来进 行实现的呢?
其实对于Lambda表达式的类型推断,它是对Java7中的目标 类型推断进行的再次扩展。在Java7中引入了菱形操作符的概 念,它可以是代码在编译时自动推断出泛型参数的类型。
1 public class Demo1 {
2
3 public static void demo(){
4
5 //全量声明
6 Map<String,String> map = new
HashMap<String, String>();
7
8 //菱形运算符
9 Map<String,String> map1 = new
HashMap<>();
10 }
11 }
第二种书写代码的方式,也是我们在开发中经常使用的。不
用明确声明泛型类型,代码在进行编译时,可以自动的进行
类型推断。
那么在Java8中对于这种使用方式又进一步扩展,可以省略 Lambda表达式中的所有参数类型。在编译时根据Lambda表 达式的上下文信息推断出参数的正确类型。这就是所谓的类 型推断。
以Predicate函数式接口为例。其内部有一个test方法。
那么对于之前编写的测试代码内部,对于Lambda表达式的
类型检查推断的过程是什么样子的呢?
1 public class MyPredicateDemo {
2
3 public static List
filter(List studentList,
Predicate predicate){
4
5 ArrayList list = new
ArrayList<>();
6
7 studentList.forEach(s->{
8 if (predicate.test(s)){
9 list.add(s);
10 }
11 });
12 return list;
13 }
14
15 public static void main(String[] args) {
16 List students = new
ArrayList<>();
17 students.add(new Student(1,“张
三”,“M”));
18 students.add(new Student(2,“李
四”,“M”));
19 students.add(new Student(3,“王
五”,“F”));
20
21 List result =
filter(students, (s) ->
s.getSex().equals(“F”));
22
23
System.out.println(result.toString());
24 }
25 }
但是到现在为止,对于Lambda表达式都只用到了方法体里 面的参数,但Lambda其实是支持使用外部定义的变量的。
1 public void demo(){
2
3 int port = 8086;
4 Runnable runnable = ()->
System.out.println(port);
5 }
在这段代码中,在Lambda表达式内部引用了外部变量。但 是当在Lambda方法体内使用外部变量时,其必须声明为 final。上述代码虽然没有显示的声明,但是在Java8它自动的 会对需要为final的变量进行转换。
但是对于类型检查推断,有一些人也是持反对意见的,认为
这么编写代码不利于阅读。这种问题只能说仁者见仁智者见
智了。
6)方法引用
方法引用更近一步的优化了Lambda的使用。它让代码感觉 更加的自然。我们可以直接使用::来简化Lambda表达式的 使用。其使用语法如下:
1 类名或实例名::方法名
1 public class MyDemo {
2
3 public static void main(String[] args) {
4
5 List students = new
ArrayList<>();
6 students.add(new Student(2,“张
三”,“M”));
7 students.add(new Student(1,“李
四”,“M”));
8 students.add(new Student(3,“王
五”,“F”));
9
10 /students.sort((s1,s2)-
s1.getId().compareTo(s2.getId()));
11
12 System.out.println(students);
/
13
14
15
students.sort(Comparator.comparing(Student:
:getId));
16
17 System.out.println(students);
18 }
19 }
7)Stream流使用
7.1)概述
流操作是Java8提供一个重要新特性,它允许开发人员以声明 性方式处理集合,其核心类库主要改进了对集合类的API和新 增Stream操作。Stream类中每一个方法都对应集合上的一种 操作。将真正的函数式编程引入到Java中,能让代码更加简
洁,极大地简化了集合的处理操作,提高了开发的效率和生
产力。
同时stream不是一种数据结构,它只是某种数据源的一个视 图,数据源可以是一个数组,Java容器或I/O channel等。 在Stream中的操作每一次都会产生新的流,内部不会像普通 集合操作一样立刻获取值,而是惰性取值,只有等到用户真 正需要结果的时候才会执行。
7.2)集合操作演进对比
此处以操作学生集合为例,查询年龄小于20岁的学生,并且根 据年龄进行排序,得到学生姓名,生成新集合
1 public class Demo1 {
2
3 public static void main(String[] args) {
4
5 //java7 查询年龄小于20岁的学生,并且根据
年龄进行排序,得到学生姓名,生成新集合
6 List studentList = new
ArrayList<>();
7 studentList.add(new Student(1,“张
三”,“M”,19));
8 studentList.add(new Student(1,“李
四”,“M”,18));
9 studentList.add(new Student(1,“王
五”,“F”,21));
10 studentList.add(new Student(1,“赵
六”,“F”,20));
11
12 //条件筛选
13 List result = new
ArrayList<>();
14 for (Student student : studentList)
{
15 if (student.getAge() < 20){
16 result.add(student);
17 }
18 }
19
20 //排序
21 Collections.sort(result, new
Comparator() {
22 @Override
23 public int compare(Student o1,
Student o2) {
24 return
Integer.compare(o1.getAge(),o2.getAge());
25 }
26 });
27
28 //获取学生姓名
29 List nameList = new
ArrayList<>();
30 for (Student student : result) {
31 nameList.add(student.getName());
32 }
33
34
System.out.println(nameList.toString());
35
36 }
37 }
上述代码中产生了两个垃圾变量声明,result和nameList,
他们两个作用就是作为一个中间数据存储容器。并且整体的
操作需要产生多次遍历。
如何通过Java8的Stream流式处理的话,又会变成什么样子 呢?
1 public class Demo2 {
2
3 public static void main(String[] args) {
4
5 //java8 查询年龄小于20岁的学生,并且根据
年龄进行排序,得到学生姓名,生成新集合
6 List studentList = new
ArrayList<>();
7 studentList.add(new Student(1,“张
三”,“M”,19));
8 studentList.add(new Student(1,“李
四”,“M”,18));
9 studentList.add(new Student(1,“王
五”,“F”,21));
10 studentList.add(new Student(1,“赵
六”,“F”,20));
11
12 List result =
studentList.stream()
13 .filter(s -> s.getAge() < 20) //
过滤出年龄小于20岁的学生
14
.sorted(Comparator.comparing(Student::getAge
)) //对结果进行排序
15 .map(Student::getName) // 提取出
结合中的name属性
16 .collect(Collectors.toList());//
转换成一个新的集合
17
18 System.out.println(result);
19 }
20 }
通过上述代码的执行,可以发现无需再去定义过多的冗余变
量。我们可以将多个操作组成一个调用链,形成数据处理的
流水线。在减少代码量的同时也更加的清晰易懂。
并且对于现在调用的方法,本身都是一种高层次构件,与线
程模型无关。因此在并行使用中,开发者们无需再去操心线
程和锁了。Stream内部都已经做好了。
1 如果刚接触流操作的话,可能会感觉不太舒服。其实理解流操
作的话可以对比数据库操作。把流的操作理解为对数据库中数
据的查询操作
2 集合 = 数据表
3 元素 = 表中的每条数据
4 属性 = 每条数据的列
5 流API = sql查询
6 以上面案例为例,转换为sql的话
7 SELECT name FROM student WHERE age < 20
ORDER BY age;
7.3)流实现思想
通过刚才的实例已经演示了Stream流的一个基本使用实例, 其以更高层次的抽象思想,完成了对于集合的复杂操作。
那么它本身就是将集合的操作由外部迭代转换为了内部迭
代。
7.3.1)外部迭代
在java8之前,在使用集合时,无非就是在集合上进行迭代, 然后处理每一个元素。以统计学生总数为例
1 public class Demo3 {
2
3 //统计学生总数
4 public static void main(String[] args) {
5
6
7 List studentList = new
ArrayList<>();
8 studentList.add(new Student(1,“张
三”,“M”,19));
9 studentList.add(new Student(1,“李
四”,“M”,18));
10 studentList.add(new Student(1,“王
五”,“F”,21));
11 studentList.add(new Student(1,“赵
六”,“F”,20));
12
13 int count = 0;
14
15 for (Student student : studentList)
{
16 count++;
17 }
18
19 System.out.println(count);
20 }
21 }
这种方式对于功能实现没有任何问题,但是它也存在一些问
题:
1)for 循环是串行的,而且必须按照集合中元素的顺序进行 依次处理,要想改造成并行的话,需要修改每个for循环;
2)使用是及早求值,返回的是另一个值或空。使用性能上存 在一点点的瑕疵;
3)易读性不好,如果for中嵌套大量循环与功能代码,阅读 起来简直是灾难;
根据上图的说明,所有的集合迭代所及都是在我们自己编写
的代码中,所以这种显式的方式称之为外部迭代。其主要关
注于数据本身。并且一般都是串行的。
7.3.2)内部迭代
而内部迭代来说,它所操作的就是不是一个集合了,而是一
个流。它会将所有的操作融合在流中,由其在内部进行处
理,这种隐式的方式称之为内部迭代。
并且内部迭代支持并行处理,更利于集合操作的性能优化。
其关注与对数据的计算。
7.4)流操作详解
Stream流接口中定义了许多对于集合的操作方法,总的来说
可以分为两大类:中间操作和终端操作。
中间操作:会返回一个流,通过这种方式可以将多个中间
操作连接起来,形成一个调用链,从而转换为另外一个
流。除非调用链最后存在一个终端操作,否则中间操作对
流不会进行任何结果处理。 终端操作:会返回一个具体的结果,如boolean、list、 integer等。
7.4.1)筛选
对于集合的操作,经常性的会涉及到对于集中符合条件的数 据筛选,Stream中对于数据筛选两个常见的API:filter(过 滤)、distinct(去重)
7.4.1.1)基于filter()实现数据过滤
7.4.1.1.1)使用案例
该方法会接收一个返回boolean的函数作为参数,最终返回 一个包括所有符合条件元素的流。
获取所有年龄20岁以下的学生
1 public class FilterDemo {
2
3 public static void main(String[] args) {
4
5 List studentList = new
ArrayList<>();
6 studentList.add(new Student(1,“张
三”,“M”,19,true));
7 studentList.add(new Student(1,“李
四”,“M”,18,false));
8 studentList.add(new Student(1,“王
五”,“F”,21,true));
9 studentList.add(new Student(1,“赵
六”,“F”,20,false));
10
11 //java7写法
12 List resultList = new
ArrayList<>();
13 for (Student student : studentList)
{
14 if (student.getAge() < 20){
15 resultList.add(student);
16 }
17 }
18
19 //java8Stream写法
20 Stream studentStream =
studentList.stream()
21 .filter(s->s.getAge() < 20);
22 List list =
studentStream.collect(Collectors.toList());
23 }
24 }
获取所有及格学生的信息
1 public class FilterDemo {
2
3 public static void main(String[] args) {
4
5 List studentList = new
ArrayList<>();
6 studentList.add(new Student(1,“张
三”,“M”,19,true));
7 studentList.add(new Student(1,“李
四”,“M”,18,false));
8 studentList.add(new Student(1,“王
五”,“F”,21,true));
9 studentList.add(new Student(1,“赵
六”,“F”,20,false));
10
11 //java7写法
12 List resultList = new
ArrayList<>();
13 for (Student student : studentList)
{
14 if (student.getIsPass()){
15 resultList.add(student);
16 }
17 }
18
19 //java8Stream写法
20 Stream studentStream =
studentList.stream()
21 .filter(Student::getIsPass);
22
23 List list =
studentStream.collect(Collectors.toList());
24 }
25 }
7.4.1.1.2)源码解析
此处可以看到filter方法接收了Predicate函数式接口。
7.4.1.2)基于distinct实现数据去重
7.4.1.2.1)使用案例
在java7之前对集合中的内容去重,有多种的实现方式,如通 过set去重、遍历后赋值给另一个集合
1 public class DistinctDemo {
2
3 public static void main(String[] args) {
4
5 //对数据模2后结果去重
6 List numberList =
Arrays.asList(1, 2, 3, 4, 1, 2, 3, 4);
7
8 List integers =
demo1(numberList);
9 System.out.println(integers);
10 List result =
demo2(numberList);
11 System.out.println(result);
12
13
14 }
15
16 /**
17 * java7 将一个集合的值赋给另一个集合
18 * @param numberList
19 * @return
20 /
21 public static List
demo1(List numberList){
22 List resultList = new
ArrayList<>();
23 for (Integer number : numberList) {
24 if (number % 2 ==0){
25 resultList.add(number);
26 }
27 }
28
29 List newList = new
ArrayList<>();
30
31 for (Integer number : numberList) {
32 if (!newList.contains(number)){
33 newList.add(number);
34 }
35 }
36
37 return newList;
38 }
39
40 /
*
41 * java7 利用set去重
42 * @param numberList
43 * @return
44 /
45 public static List
demo2(List numberList){
46 List newList = new
ArrayList();
47 Set set = new HashSet();
48 for (Integer number : numberList) {
49 if (number % 2 0){
50 if (set.add(number)){
51 newList.add(number);
52 }
53 }
54 }
55
56 return newList;
57 }
58 }
1 /
*
2 * java8 distinct使用
3 * @param numberList
4 * @return
5 */
6 public static List
demo3(List numberList){
7
8 List result =
numberList.stream()
9 .filter(n -> n % 2 == 0)
10 .distinct()
11 .collect(Collectors.toList());
12 return result;
13 }
刚才只是一个基础数值型的去重,那么要对集合中对象去
重,distinct可以做到么?
1 public class DistinctDemo2 {
2
3 public static void main(String[] args) {
4
5 List studentList = new
ArrayList<>();
6 studentList.add(new Student(1,“张
三”,“M”,19,true));
7 studentList.add(new Student(1,“李
四”,“M”,18,false));
8 studentList.add(new Student(1,“王
五”,“F”,21,true));
9 studentList.add(new Student(1,“赵
六”,“F”,20,false));
10
11 studentList.add(new Student(1,“张
三”,“M”,19,true));
12 studentList.add(new Student(1,“李
四”,“M”,18,false));
13 studentList.add(new Student(1,“王
五”,“F”,21,true));
14 studentList.add(new Student(1,“赵
六”,“F”,20,false));
15
16 List result =
studentList.stream().distinct().collect(Coll
ectors.toList());
17
18
System.out.println(result.toString());
19
20 }
21 }
此时运行可以发现,其并没有对集合中的对象进行去重,那
这是因为什么呢?难道说做不到么? 其实不是的,要对对象
进行去重的话,还需要在对象内部重写,hashCode()和 equals()方法才可以实现去重。
7.4.1.2.2)源码解析
对于现在方法的执行来说,其方法调用已经形成了一个调用
链。
根据其源码,我们可以知道在distinct()内部是基于 LinkedHashSet对流中数据进行去重,并最终返回一个新的 流。
7.4.2)切片
7.4.2.1)基于limit()实现数据截取
该方法会返回一个不超过给定长度的流。
7.4.2.1.1)使用案例
1 public class LimitDemo {
2
3 public static void main(String[] args) {
4
5 List numberList =
Arrays.asList(1, 2, 3, 4, 1, 2, 3, 4);
6
7 List collect =
numberList.stream().limit(5).collect(Collect
ors.toList());
8 System.out.println(collect);
9 }
10 }
7.4.2.1.2)源码分析
对于limit方法的实现,它会接收截取的长度,如果该值小于 0,则抛出异常,否则会继续向下调用SliceOps.makeRef()。 该方法中this代表当前流,skip代表需要跳过元素,比方说本 来应该有4个元素,当跳过元素值为2,会跳过前面两个元 素,获取后面两个。maxSize代表要截取的长度
在makeRef方法中的unorderedSkipLimitSpliterator()中接 收了四个参数Spliterator,skip(跳过个数)、limit(截取个 数)、sizeIfKnown(已知流大小)。如果跳过个数小于已知流大 小,则判断跳过个数是否大于0,如果大于则取截取个数或已 知流大小-跳过个数的两者最小值,否则取已知流大小-跳过个 数的结果,作为跳过个数。
最后对集合基于跳过个数和截取个数进行切割。
7.4.2.2)基于skip()实现数据跳过
7.4.2.2.1)使用案例
刚才已经基于limit完成了数据截取,但是limit对于数据截取 是从前往后截取几个。如果现在对结果只获取后几个怎么办 呢?此时就需要使用skip()。其与limit()的使用是相辅相成 的。
1 public class SkipDemo {
2
3 public static void main(String[] args) {
4
5 List numberList =
Arrays.asList(1, 2, 3, 4, 1, 2, 3, 4);
6
7 List collect =
numberList.stream().limit(5).skip(2).collect
(Collectors.toList());
8 System.out.println(collect);
9 }
10 }
7.4.2.2.2)源码分析
在skip方法中接收的n代表的是要跳过的元素个数,如果n小 于0,抛出非法参数异常,如果n等于0,则返回当前流。如果 n小于0,才会调用makeRef()。同时指定limit参数为-1.
此时可以发现limit和skip都会进入到该方法中,在确定limit 值时,如果limit<0,则获取已知集合大小长度-跳过的长度。最 终进行数据切割。
7.4.3)映射
在对集合进行操作的时候,我们经常会从某些对象中选择性
的提取某些元素的值,就像编写sql一样,指定获取表中特定 的数据列
1 #指定获取特定列
2 SELECT name FROM student;
在Stream API中也提供了类似的方法,map()。它接收一个 函数作为方法参数,这个函数会被应用到集合中每一个元素 上,并最终将其映射为一个新的元素。
7.4.3.1)使用案例
1 public class MapDemo {
2
3 public static void main(String[] args) {
4
5 List studentList = new
ArrayList<>();
6 studentList.add(new Student(1,“张
三”,“M”,19,true));
7 studentList.add(new Student(2,“李
四”,“M”,18,false));
8 studentList.add(new Student(3,“王
五”,“F”,21,true));
9 studentList.add(new Student(4,“赵
六”,“F”,20,false));
10
11 //获取每一个学生的名字,并形成一个新的集合
12 List nameList =
studentList.stream()
13 .map(Student::getName)
14 .collect(Collectors.toList());
15
16 System.out.println(nameList);
17 }
18 }
1 public class MapDemo {
2
3 public static void main(String[] args) {
4
5 List studentList = new
ArrayList<>();
6 studentList.add(new Student(1,“张
三”,“M”,19,true));
7 studentList.add(new Student(1,“李
四”,“M”,18,false));
8 studentList.add(new Student(1,“王
五”,“F”,21,true));
9 studentList.add(new Student(1,“赵
六”,“F”,20,false));
10
11 //获取每一个学生的名字,并形成一个新的集合
12 List nameList =
studentList.stream()
13 .map(Student::getName)
14 .map(String::length) //也可以继续
向下获取每一个名称的长度
15 .collect(Collectors.toList());
16
17 System.out.println(nameList);
18 }
19 }
7.4.3.2)源码分析
将当前流给定函数中的元素,包含到一个新的流中进行返
回。
其会接收一个Function函数式接口,内部接收一个
内部对Function函数式接口中的apply方法进行实现,接收一
个对象,返回另外一个对象,并把这个内容存入当前流中,
最后返回。
7.4.4)匹配
在日常开发中,有时还需要判断集合中某些元素是否匹配对 应的条件,如果有的话,在进行后续的操作。在Stream API 中也提供了相关方法供我们进行使用,如anyMatch、 allMatch等。他们对应的就是&&和||运算符。
7.4.1.1)基于anyMatch()判断条件至少匹配一个元素
anyMatch()主要用于判断流中是否至少存在一个符合条件的 元素,它会返回一个boolean值,并且对于它的操作,一般 叫做短路求值
7.4.1.1.1)短路求值
对于集合的一些操作,我们无需处理整个集合就能得到结
果,比方说通过&&或者||连接一个判断条件,这就是短路。
对于流来说,某些操作不用操作整个流就可以得到结果。
7.4.1.1.2)使用案例
1 public class AnyMatchDemo {
2
3 public static void main(String[] args) {
4 List studentList = new
ArrayList<>();
5 studentList.add(new Student(1,“张
三”,“M”,19,true));
6 studentList.add(new Student(2,“李
四”,“M”,18,false));
7 studentList.add(new Student(3,“王
五”,“F”,21,true));
8 studentList.add(new Student(4,“赵
六”,“F”,20,false));
9
10 if (studentList.stream().anyMatch(s-
s.getAge()<20)){
11
12 System.out.println(“集合中有符合条
件的学生”);
13 }
14 }
15 }
根据上述例子可以看到,当流中只要有一个符合条件的元
素,则会立刻中止后续的操作,立即返回一个布尔值,无需
遍历整个流。
7.4.1.1.3)源码解析
内部实现会调用makeRef(),其接收一个Predicate函数式接 口,并接收一个枚举值,该值代表当前操作执行的是ANY。
如果test()抽象方法执行返回值MatchKind中any的 stopOnPredicateMatches,则将stop中断置为true,value 也为true。并最终进行返回。无需进行后续的流操作。
7.4.1.2)基于allMatch()判断条件是否匹配所有元素
allMatch()的工作原理与anyMatch()类似,但是anyMatch执 行时,只要流中有一个元素符合条件就会返回true,而 allMatch会判断流中是否所有条件都符合条件,全部符合才 会返回true。
7.4.1.2.1)使用案例
1 public class AllMatchDemo {
2
3 public static void main(String[] args) {
4
5 List studentList = new
ArrayList<>();
6 studentList.add(new Student(1,“张
三”,“M”,19,true));
7 studentList.add(new Student(2,“李
四”,“M”,18,true));
8 studentList.add(new Student(3,“王
五”,“F”,21,true));
9 //studentList.add(new Student(4,“赵
六”,“F”,20,true));
10 studentList.add(new Student(4,“赵
六”,“F”,20,false));
11
12 if
(studentList.stream().allMatch(Student::getI
sPass)){
13 System.out.println(“所有学生合
格”);
14 }else {
15 System.out.println(“有学生不合
格”);
16 }
17 }
18 }
7.4.5)查找
对于集合操作,有时需要从集合中查找中符合条件的元素, Stream中也提供了相关的API,findAny()和findFirst(),他俩 可以与其他流操作组合使用。findAny用于获取流中随机的某 一个元素,findFirst用于获取流中的第一个元素。至于一些 特别的定制化需求,则需要自行实现。
7.4.5.1)基于findAny()查找元素
7.4.5.1.1)使用案例
findAny用于获取流中随机的某一个元素,并且利用短路在找 到结果时,立即结束。
1 public class FindAnyTest {
2
3 public static void main(String[] args) {
4 List studentList = new
ArrayList<>();
5
6 studentList.add(new Student(1,“张
三”,“M”,18,true));
7 studentList.add(new Student(2,“李
四”,“M”,19,true));
8 studentList.add(new Student(3,“王
五”,“F”,21,true));
9 studentList.add(new Student(4,“赵六
1”,“F”,15,false));
10 studentList.add(new Student(5,“赵六
2”,“F”,16,false));
11 studentList.add(new Student(6,“赵六
3”,“F”,17,false));
12 studentList.add(new Student(7,“赵六
4”,“F”,18,false));
13 studentList.add(new Student(8,“赵六
5”,“F”,19,false));
14
15 Optional optional =
studentList.stream().filter(s -> s.getAge()
< 20).findAny();
16 if (optional.isPresent()){
17
System.out.println(optional.get());
18 }
19 }
20 }
上面的例子中返回了Optional,该类会在后续详细讲解,现
在把它理解为封装着查询结果的对象即可。
根据上述例子的运行结果,可以发现。它最终获取到的是第
一个符合条件的元素,如果让它循环多次的话,是什么结果
呢?
1 public class FindAnyTest {
2
3 public static void main(String[] args) {
4 List studentList = new
ArrayList<>();
5
6 studentList.add(new Student(1,“张
三”,“M”,18,true));
7 studentList.add(new Student(2,“李
四”,“M”,19,true));
8 studentList.add(new Student(3,“王
五”,“F”,21,true));
9 studentList.add(new Student(4,“赵六
1”,“F”,15,false));
10 studentList.add(new Student(5,“赵六
2”,“F”,16,false));
11 studentList.add(new Student(6,“赵六
3”,“F”,17,false));
12 studentList.add(new Student(7,“赵六
4”,“F”,18,false));
13 studentList.add(new Student(8,“赵六
5”,“F”,19,false));
14
15 for(int i=0;i<100;i++){
16
17 Optional optional =
studentList.stream().filter(s -> s.getAge()
< 20).findAny();
18 if (optional.isPresent()){
19
System.out.println(optional.get());
20 }
21 }
22 }
23 }
根据这个例子的演示,可以发现,循环了一百次,但是每次
还是返回的是符合条件的第一个元素,那这就违背了之前的
介绍,这是为什么呢?
7.4.5.1.2)源码解析
根据这一段源码介绍,findAny对于同一数据源的多次操作会
返回不同的结果。但是,我们现在的操作是串行的,所以在
数据较少的情况下,一般会返回第一个结果,但是如果在并
行的情况下,那就不能确保返回的是第一个了。这种设计主
要是为了获取更加高效的性能。并行操作后续会做详细介
绍。
传递参数,指定不必须获取第一个元素。
在该方法中,主要用于判断对于当前的操作执行并行还是串
行。
在该方法中的wrapAndCopyInto()内部做的会判断流中是否 存在符合条件的元素,如果有的话,则会进行返回。结果最 终会封装到Optional中的IsPresent中。
根据上面的解析可以,得知,当为串行流且数据较少时,获
取的结果一般为流中第一个元素,但是当为并流行的时候,
则会随机获取。请看下面测试:
1 public class FindAnyTest {
2
3 public static void main(String[] args) {
4 List studentList = new
ArrayList<>();
5
6 studentList.add(new Student(1,“张
三”,“M”,18,true));
7 studentList.add(new Student(2,“李
四”,“M”,19,true));
8 studentList.add(new Student(3,“王
五”,“F”,21,true));
9 studentList.add(new Student(4,“赵六
1”,“F”,15,false));
10 studentList.add(new Student(5,“赵六
2”,“F”,16,false));
11 studentList.add(new Student(6,“赵六
3”,“F”,17,false));
12 studentList.add(new Student(7,“赵六
4”,“F”,18,false));
13 studentList.add(new Student(8,“赵六
5”,“F”,19,false));
14
15 for(int i=0;i<100;i++){
16
17 Optional optional =
studentList.parallelStream().filter(s ->
s.getAge() < 20).findAny();
18 if (optional.isPresent()){
19
System.out.println(optional.get());
20 }
21 }
22 }
23 }
此处使用了并行流,根据结果可以看到,是随机进行获取
的。
7.4.5.2)基于findFirst()查找元素
findFirst使用原理与findAny类似,但不管是在并行还是串 行,指定返回流中的第一个元素。
7.4.5.2.1)使用案例
1 public class FindFirstTest {
2
3 public static void main(String[] args) {
4
5 List studentList = new
ArrayList<>();
6
7 studentList.add(new Student(1,“张
三”,“M”,18,true));
8 studentList.add(new Student(2,“李
四”,“M”,19,true));
9 studentList.add(new Student(3,“王
五”,“F”,21,true));
10 studentList.add(new Student(4,“赵六
1”,“F”,15,false));
11 studentList.add(new Student(5,“赵六
2”,“F”,16,false));
12 studentList.add(new Student(6,“赵六
3”,“F”,17,false));
13 studentList.add(new Student(7,“赵六
4”,“F”,18,false));
14 studentList.add(new Student(8,“赵六
5”,“F”,19,false));
15
16 for(int i=0;i<100;i++){
17 Optional optional =
studentList
18 .stream()
19 //.parallelStream()
20 .filter(s -> s.getAge() <

21 .findFirst();
22
23 if (optional.isPresent()){
24
System.out.println(optional.get());
25 }
26 }
27 }
28 }
此处不管使用串行流还是并行流,得到的都是流中的第一个
元素。
7.4.6)归约
到现在截止,对于流的终端操作,我们返回的有boolean、 Optional和List。但是在集合操作中,我们经常会涉及对元素 进行统计计算之类的操作,如求和、求最大值、最小值等, 从而返回不同的数据结果。
7.4.6.1)基于reduce()进行累积求和
7.4.6.1.1)使用案例
当求总和时,在java7中的写法思想,一般是如下面样子的:
1 public class ReduceTest {
2
3 public static void main(String[] args) {
4
5 List numbers = new
ArrayList<>();
6 numbers.add(1);
7 numbers.add(2);
8 numbers.add(3);
9 numbers.add(4);
10 numbers.add(5);
11 numbers.add(6);
12 numbers.add(7);
13
14 int sum =0;
15
16 for (Integer number : numbers) {
17 sum+=number;
18 }
19 System.out.println(sum);
20 }
21 }
对numbers数组中的每一个元素进行取出,反复迭代求出最 终的结果,这种操作就是一个归约操作:将多个值经过操作 或计算来得到一个最终结果。
那么通过Stream可以如何实现上述操作呢?如下面代码所
示:
1 public class ReduceTest {
2
3 public static void main(String[] args) {
4
5 List numbers = new
ArrayList<>();
6 numbers.add(1);
7 numbers.add(2);
8 numbers.add(3);
9 numbers.add(4);
10 numbers.add(5);
11 numbers.add(6);
12 numbers.add(7);
13
14 Integer reduce =
numbers.stream().reduce(0, (a, b) -> a + b);
15 System.out.println(reduce);
16 }
17 }
在上述代码中,在reduce里的第一个参数声明为初始值,第 二个参数接收一个lambda表达式,代表当前流中的两个元 素,它会反复相加每一个元素,直到流被归约成一个最终结 果。执行流程如下图所示:
那对于上述代码,能不能再次优化呢?当然可以,Integer类 中提供了sum方法,用于对两个数求和,这里我们可以直接 基于lambda方法调用的形式来使用
1 Integer reduce = numbers.stream().reduce(0,
Integer::sum);
那这种方式能不能被优化呢?同样也可以,reduce方法还有 一个重载方法,不需要初始化值,会返回一个Optional对 象。
对于这种操作,适用于如果流中没有任何元素的话,则也就
没有结果了,需要进行判断,防止空值的出现,所以会返回
Optional用于判断是否有结果。更加推荐使用这种方式进行 归约操作
1 Optional optional =
numbers.stream().reduce(Integer::sum);
2 if (optional.isPresent()){
3 System.out.println(optional.get());
4 }else {
5 System.out.println(“数据有误”);
6 }
7.4.6.1.2)源码分析
1)两个参数的reduce方法
identity相当于初始化值。
accumulator相当于当前做的操作
在上述方法中,对于流中元素的操作,当执行第一个元素, 会进入begin方法,将初始化的值给到state,state就是最后 的返回结果。并执行accept方法,对state和第一个元素根据 传入的操作,对两个值进行计算。并把最终计算结果赋给 state。
当执行到流中第二个元素,直接执行accept方法,对state和 第二个元素对两个值进行计算,并把最终计算结果赋给 state。后续依次类推。
可以按照下述代码进行理解
1 T result = identity;
2 for (T element : this stream){
3 result = accumulator.apply(result, element)
4 }
5 return result;
2)一个参数的reduce方法
只接收当前操作。
在这部分实现中,对于匿名内部类中的empty相当于是一个 开关,state相当于结果。
对于流中第一个元素,首先会执行begin()将empty置为 true,state为null。接着进入到accept(),判断empty是否为 true,如果为true,则将empty置为false,同时state置为当 前流中第一个元素,当执行到流中第二个元素时,直接进入 到accpet(),判断empty是否为true,此时empty为false,则 会执行apply(),对当前state和第二个元素进行计算,并将结 果赋给state。后续依次类推。
当整个流操作完之后,执行get(), 如果empty为true,则返回 一个空的Optional对象,如果为false,则将最后计算完的 state存入Optional中。
可以按照下述代码进行理解:
1 boolean flag = false;
2 T result = null;
3 for (T element : this stream) {
4 if (!flag) {
5 flag = true;
6 result = element;
7 }else{
8 result = accumulator.apply(result,
element);
9 }
10 }
11 return flag ? Optional.of(result) :
Optional.empty();
7.4.6.2)获取流中元素的最大值
7.4.6.2.1)使用案例
Integer除了提供sum()外,还提供了其他的操作,如获取最 大值,获取最小值等。基于上述reduce的讲解,可以做如下 实现:
1 public class MaxDemo {
2
3 public static void main(String[] args) {
4 List numbers = new
ArrayList<>();
5 numbers.add(1);
6 numbers.add(2);
7 numbers.add(3);
8 numbers.add(4);
9 numbers.add(5);
10 numbers.add(6);
11 numbers.add(7);
12
13 Optional optional =
numbers.stream().reduce(Integer::max)
14
15 if (optional.isPresent()){
16 Integer integer =
optional.get();
17 System.out.println(integer);
18 }
19 }
20 }
上述方法实现原理与进行sum时一致。那么对于这种写法能 不能进一步简写呢?答案是可以的。在strema中,也直接提 供了求最大值的方法,实现方式如下:
1 Optional optional =
numbers.stream().max(Integer::compareTo);
7.4.6.2.2)源码解析
该方法会接收Comparator函数式接口。
其内部实现原理很简单,循环的对流中两个元素进行比较。
7.4.6.3)获取流中元素的最小值
7.4.6.3.1)使用案例
1 public class MinDemo {
2
3 public static void main(String[] args) {
4
5 List numbers = new
ArrayList<>();
6 numbers.add(1);
7 numbers.add(2);
8 numbers.add(3);
9 numbers.add(4);
10 numbers.add(5);
11 numbers.add(6);
12 numbers.add(7);
13
14 Optional min =
numbers.stream().min(Integer::compareTo);
15 if (min.isPresent()){
16 System.out.println(min.get());
17 }
18 }
19 }
7.4.6.3.2)源码解析
接收Comparator函数式接口
7.5)构建流
到现在为止,已经基于流对集合中的数据进行了很多的处理 演示,大家已经体会到了Stream的好用之处。但是,现在大 家可能会有一个疑问,难道Stream只能在集合基础上才能操 作么?当然不是,我们还可以基于值、数组甚至文件来构建 流,完成流操作。
7.5.1)基于值创建流
7.5.1.1)使用案例
在Stream中提供了一个静态方法of,它可以接收任意数量参 数,显式的创建一个流。并且会根据传入的参数类型,构建 不同泛型的流。
1 public class OfTest {
2
3 public static void main(String[] args) {
4
5 Stream stringStream =
Stream.of(“1”, “2”, “3”);
6
7 Stream stream =
Stream.of(“1”, “2”, 3,true,new St());
8 stream.forEach(d->
System.out.println(d));
9 }
10 }
11
12 class St {}
7.5.1.2)源码解析
根据上述源码,其内部就是基于Arrays中的stream方法将传
入的多个参数转换为数组,然后创建流,并遍历数组,将每
一个元素放入流中。
7.5.2)基于数组创建流
对于基于数组构建流,在上述源码中,已经使用到了,就是
通过Arrays.stream进行完成
1 Integer[] numbers = new Integer[]
{1,2,3,4,5,6};
2 Stream integerStream =
Arrays.stream(numbers);
7.5.3)基于文件创建流
之前对于Stream的操作,操作的都是集合或者数组。但是现 在也可以通过Stream完成对于文件的操作。在Java中提供了 Files类,该类中提供了一些对于文件操作的相关方法。可以 看下面对于Files类中部分方法截图

根据上述截图,可以看到,在该类中部分方法返回值就是
Stream,如:newDirectoryStream、list、lines等。
7.5.3.1)使用案例
1 public class FilesDemo {
2
3 public static void main(String[] args)
throws IOException {
4
5 //获取指定目录下的所有子文件路径
6
//Files.list(Paths.get(“D:\workspace\ithe
ima\course-JDK8\demo”)).forEach(path ->
System.out.println(path));
7
8
9
Files.list(Paths.get(“D:\workspace\itheim
a\course-JDK8\demo”)).forEach(path -> {
10
11 try {
12 System.out.println(path);
13 //读取每一个文件中的内容
14
Files.lines(path).forEach(content->
System.out.println(content));
15 } catch (IOException e) {
16 e.printStackTrace();
17 }
18 });
19 }
20 }
7.6)收集器
通过之前的学习,对于Stream的常用api都已经学习完了,
但是迄今为止,对于数据的返回,我们返回的都是一些简单
的数据类型。那现在我们要做一些复杂的数据返回,应该怎
么做呢? 比方说返回一个Map<String,Integer>或者 Map<String,List>。
那么对于这些内容,就需要通过收集器来实现了。 首先先以
一个案例为例,现在要通过性别对学生集合中的数据进行分
组,不通过Lambda实现,如下所示:
1 public class CollectDemo {
2
3 public static void main(String[] args) {
4
5 List studentList = new
ArrayList<>();
6 studentList.add(new Student(1,“张
三”,“M”,19,true));
7 studentList.add(new Student(2,“李
四”,“M”,18,false));
8 studentList.add(new Student(3,“王
五”,“F”,21,true));
9 studentList.add(new Student(4,“赵
六”,“F”,20,false));
10
11 Map<String,List> map = new
HashMap<>();
12
13 for (Student student : studentList)
{
14
15 String sex = student.getSex();
16
17 if (map.get(sex)==null){
18
19 List list = new
ArrayList<>();
20 list.add(student);
21 map.put(sex,list);
22
23 }else{
24
25 List list =
map.get(sex);
26 list.add(student);
27 }
28 }
29
30 System.out.println(map);
31 }
32 }
对于这种代码,是我们日常开发中经常编写的内容,但是这
么简单的事,现在需要编写这么多的代码,而且这种代码读
起来也比较难受。
现在就可以通过lambda对上述代码进行改造,如下所示:
1 Map<Integer, List> map =
studentList.stream().collect(Collectors.group
ingBy(Student::getAge));
只需要一句话,就可以把上述复杂的内容搞定。这就是收集
器。
7.6.1)收集器简介
通过使用收集器,可以让代码更加方便的进行简化与重用。 其内部主要核心是通过Collectors完成更加复杂的计算转换, 从而获取到最终结果。并且Collectors内部提供了非常多的常 用静态方法,直接拿来就可以了。比方说:toList。
7.6.2)Collectors常用方法使用
7.6.2.1)通过counting()统计集合总数
对于集合总数的统计,如果通过之前的编码思路,需要多行
代码才能实现。但现在通过收集器的话,一行足以搞定。如
下所示:
1 Long collect =
studentList.stream().collect(Collectors.count
ing());
counting()中内部做的调用reducing()进行数据汇总,源码如
下:
对于上述代码,根据之前的学习,也可以更进一步简写,如
下所示:
1 long count = studentList.stream().count();
7.6.2.2)通过maxBy()与minBy()获取最大值最小值
1)获取年龄最大的学生
1 //获取年龄最大的学生
2 Optional optional =
studentList.stream().collect(Collectors.maxBy
(Comparator.comparing(Student::getAge)));
3 if (optional.isPresent()){
4 Student student = optional.get();
5 System.out.println(student);
6 }
maxBy()中做的同样是调用reducing方法在内部进行数据比 较,源码如下:
对于上述代码,也可以更进一步优化
1 Optional studentOptional =
studentList.stream().max(Comparator.comparing
(Student::getAge));
2)获取年龄最小的学生
1 Optional studentOptional =
studentList.stream().min(Comparator.comparing
(Student::getAge));
7.6.2.3)通过summingInt()进行数据汇总
1 Integer collect =
studentList.stream().collect(Collectors.summi
ngInt(Student::getAge));
其内部收集过程是在遍历流时,会把每一个学生都映射为其
中的年龄,然后把每一个年龄进行累加。基于这个思路,对
于上述代码,也可以更进一步简化
1 int sum =
studentList.stream().mapToInt(Student::getAge
).sum();
7.6.2.4)通过averagingInt()进行平均值获取
1 Double collect =
studentList.stream().collect(Collectors.avera
gingInt(Student::getAge));
其内部是在收集过程中,对所有年龄进行累加,最后除以平
均值。对于上述代码也可以换另外一种写法完成。
1 OptionalDouble average =
studentList.stream().mapToDouble(Student::get
Age).average();
2 if (average.isPresent()){
3 double asDouble = average.getAsDouble();
4 System.out.println(asDouble);
5 }
这种方式虽然代码写多了一些,但是可以防止空值的出现。
7.6.2.5)复杂结果返回
到此截止,已经通过收集器完成了汇总、求和、求最大最小
值、求平均值的操作。但是值得注意的是,这些操作每一次
都是返回单独的一个值,但是日常开发中,经常需要获取多
种内容,那这种需求应该如何完成?Collectors也提供了相关 静态方法进行解决,这三个方法可以,返回的都是收集器。 其内部已经包含了多种结果内容
如下所示:
1 IntSummaryStatistics collect =
studentList.stream().collect(Collectors.summa
rizingInt(Student::getAge));
上述方法返回了IntSummaryStatistics类,其内部提供了相 关getter方法用于获取汇总值、总和、最大值最小值等方 法,直接调用即可
1 long count = collect.getCount();
2 long sum = collect.getSum();
3 int max = collect.getMax();
4 int min = collect.getMin();
5 double average = collect.getAverage();
对于另外的summarizingDouble()和summarizingLong()使
用方式都是相同的。只不过他们适用于收集属性数据类型为
double和long而已。
7.6.2.6)通过joining()进行数据拼接
1 String collect =
studentList.stream().map(Student::getName).co
llect(Collectors.joining());
这种方式相当于将流中每一个元素的name属性获取映射,内 部通过StringBuilder来把每一个映射的值进行拼接。
但是这种方式的数据拼接,没有任何的分隔符,因此最后的
数据结果可读性不好。joining()对于这个问题,提供了一个重
载方法,用于接收元素之间的分隔符。
1 String collect =
studentList.stream().map(Student::getName).co
llect(Collectors.joining(","));
7.6.3)分组
7.6.3.1)分组简介
在数据库操作中,经常会通过group by对查询结果进行分 组。同时在日常开发中,也经常会涉及到这一类操作,如通 过性别对学生集合进行分组。如果通过普通编码的方式需要 编写大量代码且可读性不好。
对于这个问题的解决,java8也提供了简化书写的方式。通过 Collectors。groupingBy()即可。
1 Map<Integer, List> map =
studentList.stream().collect(Collectors.group
ingBy(Student::getAge))
7.6.3.2)多级分组
7.6.3.2.1)多级分组使用
刚才已经使用groupingBy()完成了分组操作,但是只是通过 单一的age进行分组,那现在如果需求发生改变,还要按照是 否及格进行分组,能否实现?答案是可以的。对于 groupingBy()它提供了两个参数的重载方法,用于完成这种 需求。
这个重载方法在接收普通函数之外,还会再接收一个 Collector类型的参数,其会在内层分组(第二个参数)结果,传 递给外层分组(第一个参数)作为其继续分组的依据。
1 Map<Integer, Map<String, List>>
collect =
studentList.stream().collect(Collectors.group
ingBy(Student::getAge,
Collectors.groupingBy(student -> {
2 if (student.getIsPass()) {
3 return “pass”;
4 } else {
5 return “not pass”;
6 }
7 })));
8
9 System.out.println(collect);
最终得到的结果如下:
1 {18={not pass=[Student{id=2, name=‘李四’,
sex=‘M’, age=18}], pass=[Student{id=1,
name=‘张三’, sex=‘M’, age=18}]}, 20={not
pass=[Student{id=4, name=‘赵六’, sex=‘F’,
age=20}], pass=[Student{id=3, name=‘王五’,
sex=‘F’, age=20}]}}
对于这个结果外层分组产生的map生成了两个值:18、20。 同时他们各自的值又是一个map,key是二级分组的返回值, 值是流中元素的具体值。对于这种多级分组操作,可以扩展 至无限层级。
7.6.3.2.2)多级分组变形
在日常开发中,我们很有可能不是需要返回一个数据集合,
还有可能对数据进行汇总操作,比方说对于年龄18岁的通过 的有多少人,未及格的有多少人。因此,对于二级分组收集 器传递给外层分组收集器的可以任意数据类型,而不一定是 它的数据集合。
1 //根据年龄进行分组,获取并汇总人数
2 Map<Integer, Long> collect =
studentList.stream().collect(Collectors.group
ingBy(Student::getAge,
Collectors.counting()));
3
4 //根据年龄与是否及格进行分组,并汇总人数
5 Map<Integer, Map<Boolean, Long>> collect =
studentList.stream().collect(Collectors.group
ingBy(Student::getAge,
Collectors.groupingBy(Student::getIsPass,
Collectors.counting())));
当需求变的更加复杂,要根据年龄与是否集合进行分组,并
获取每组中分数最高的学生
1 Map<Integer, Map<Boolean, Student>> collect =
studentList.stream().collect(
2 Collectors.groupingBy(Student::getAge,
3
Collectors.groupingBy(Student::getIsPass,
4
Collectors.collectingAndThen(
5
Collectors.maxBy(
6
Comparator.comparing(Student::getScore)),
Optional::get))));
7
8 System.out.println(collect);
在对收集器的使用中,将多个收集器嵌套起来使用非常常
见。通过这些收集器的嵌套,可以完成很多较为复杂的业务
逻辑实现。
7.6.4)自定义收集器
7.6.4.1)介绍
到此截止,已经通过Collectors中提供的静态方法,完成了诸 多的收集器操作,虽然它本身提供了诸多方法,但是不一定 能够覆盖日常开发中的所有需求,因此,有时还需要我们根 据自身的业务去自定义收集器的实现。
对于自定义收集器实现,可以对Collector接口中的方法进行
实现。
根据源码,Collector接口需要三个参数。T:流中要收集的元 素类型、A:累加器的类型、R:收集的结果类型。
如想自定义收集器,需要实现Collector接口中的五个方法: supplier、accumulator、finisher、combiner、 characteristics
supplier:用于创建一个容器,在调用它时,需要创建一个 空的累加器实例,供后续方法使用。
accumulator:基于supplier中创建的累加容器,进行累加操
作。
finisher:当遍历完流后,在其内部完成最终转换,返回一个 最终结果。
combiner:用于在并发情况下,将每个线程的容器进行合
并。
characteristics:用于定义收集器行为,如是否可以并行或使 用哪些优化。其本身是一个枚举,内部有三个值,分别为:
CONCURRENT:表明收集器是并行的且只会存在一个中间容
器。
UNORDERED:表明结果不受流中顺序影响,收集是无序 的。
IDENTITY_FINISH:表明累积器的结果将会直接作为归约的 最终结果,跳过finisher()。
7.6.4.2)实现
自定义收集器,返回所有合格的学员
1 public class MyCollector implements
Collector<Student,
List,List> {
2
3
4 @Override
5 public Supplier<List>
supplier() {
6 return ArrayList::new;
7 }
8
9 @Override
10 public BiConsumer<List,
Student> accumulator() {
11
12
13 return (studentList,student)->{
14 if (student.getIsPass()){
15 studentList.add(student);
16 }
17 };
18 }
19
20 @Override
21 public BinaryOperator<List>
combiner() {
22 return null;
23 }
24
25 @Override
26 public Function<List,
List> finisher() {
27 return Function.identity();
28 }
29
30 @Override
31 public Set
characteristics() {
32 return
EnumSet.of(Characteristics.IDENTITY_FINISH,C
haracteristics.UNORDERED);
33 }
34 }
使用自定义收集器
1 public class MyCollectorTest {
2
3 public static void main(String[] args) {
4
5 List studentList = new
ArrayList<>();
6 studentList.add(new Student(1,“张
三”,“M”,19,true));
7 studentList.add(new Student(2,“李
四”,“M”,18,true));
8 studentList.add(new Student(3,“王
五”,“F”,21,true));
9 studentList.add(new Student(4,“赵
六”,“F”,20,false));
10
11 List list =
studentList.stream().collect(new
MyCollector());
12 System.out.println(list);
13 }
14 }
8)数据并行化
通过前面内容的学习,已经通过Stream提供的相关方法完成 了很多操作,但是对于之前的操作,都是基于串行的方式来 执行的。为了让数据处理更加高效,Java8对于Stream也提 供了并行的操作方式,在Java7之前如果要对数据并行处理, 需要开发人员做的事情很多,如数据如何进行分块、开启多 少个线程、哪个线程负责哪部分数据、出现线程竞争怎么办 等等的问题。
Java8对于数据并行化处理的实现非常简单,直接调用一个 parallelStream()就可以开启并行化处理。本节的关注点不在 于如何修改代码,而是要理解为什么要开启并行处理以及在 何时使用不同的处理方式能让程序获得最佳的性能。
8.1)并行与并发的区别
在说到并行的时候,相信很多人都会想到并发的概念。那么
并行和并发两者一字之差,有什么区别呢?
并行:多个任务在同一时间点发生,并由不同的cpu进行处
理,不互相抢占资源
并发:多个任务在同一时间点内同时发生了,但由同一个cpu
进行处理,互相抢占资源
当在大量数据处理上,数据并行化可以大量缩短任务的执行
时间,将一个数据分解成多个部分,然后并行处理,最后将
多个结果汇总,得到最终结果。
8.2)并行流使用
对于并行流的使用,只需要改变一个方法调用就可以实现,
让你获得并行的操作。
1 public class MaxDemo {
2
3 public static void main(String[] args) {
4 List numbers = new
ArrayList<>();
5 numbers.add(1);
6 numbers.add(2);
7 numbers.add(3);
8 numbers.add(4);
9 numbers.add(5);
10 numbers.add(6);
11 numbers.add(7);
12
13 int sum =
numbers.parallelStream().mapToInt(i ->
i).sum();
14 System.out.println(sum);
15 }
16 }
当将stream()切换为parallelStream()后,则完成了串行转换
为并行的实现。
8.3)并行流原理介绍
对于并行流,其在底层实现中,是沿用了Java7提供的 fork/join分解合并框架进行实现。fork根据cpu核数进行数据 分块,join对各个fork进行合并。实现过程如下所示:
假设现在的求和操作是运行在一台4核的机器上。
8.4)使用注意事项
对于并行流,一定不要陷入一个误区:并行一定比串行快。 并行在不同的情况下它不一定是比串行快的。影响并行流性 能主要存在5个因素:
1)数据大小:输入数据的大小,直接影响了并行处理的性 能。因为在并行内部实现中涉及到了fork/join操作,它本身 就存在性能上的开销。因此只有当数据量很大,使用并行处 理才有意义。
2)源数据结构:fork时会对源数据进行分割,数据源的特性 直接影响了fork的性能。
1 ArrayList、数组或IntStream.range,可分解性最佳,
因为他们都支持随机读取,因此可以被任意分割。
2
3 HashSet、TreeSet,可分解性一般,其虽然可被分解,但
因为其内部数据结构,很难被平均分解。
4
5 LinkedList、Streams.iterate、
BufferedReader.lines,可分解性极差,因为他们长度未
知,无法确定在哪里进行分割。
3)装箱拆箱
尽量使用基本数据类型,避免装箱拆箱。
4)CPU核数
fork的产生数量是与可用CPU核数相关,可用的核数越多, 获取的性能提升就会越大。
5)单元处理开销
花在流中每个元素的时间越长,并行操作带来的性能提升就
会越明显。
8.5)性能测试
对于Stream的应用已经基本介绍完了,它简单易用,可读性 好。但是它的性能到底如何呢? 这个问题在网络上也有非常 多的人在进行讨论。那现在我们可以对他通过基本类型、对 象类型和复杂对象来对普通for循环、串行流和并行流进行分 别的性能测试。
此处使用常见线上服务器配置。12核24线程,96G内存的配
置。
8.5.1)基本类型
从上图可知,对于基本数据类型Stream串行的性能开销是普 通for循环的两倍左右。同时Stream并行的性能比普通for循 环和串行都要好。
1 Stream串行>for循环>Stream并行
8.5.2)对象
根据上图可知,当操作对象类型时,Stream串行的性能开销 仍高于普通for循环一倍左右。同时Stream并行的性能比普通 for循环和串行都要好。
1 Stream串行>for循环>Stream并行
8.5.3)复杂对象
根据上图可知,当操作复杂对象时,普通for循环的性能开销 会高于Stream串行,同时仍然是Stream并行性能最优
1 for循环>Stream串行>Stream并行
8.5.4)结论
根据上述测试可知,对于简单操作,如果环境机是多核的
话,建议使用Stream并行,同时在不考虑核数的情况下,普 通for循环性能要明显高于Stream串行,相差两倍左右。
对于复杂操作,推荐使用Stream API操作。
9)异步编程 CompleTableFuture
在现在软件开发的环境下,经常需要考虑如何能够设计出性
能更加优异的系统。就像上一章提到的数据并行化,就是充
分利用多核处理器结合并行操作来让代码执行效率更加优
异。第二种方式就是让功能方法能够并行执行。
比方说现在一个新闻类app,需要给用户推荐文章信息。那
么在计算文章的方法中,它会调用方法查询文章的点赞数、
转发数和评论数。
这三个方法相互之间是没有什么联系,各自完成自己的事
情,那么就会考虑开启多个线程,让他们能够在各自的子线
程中完成自己的任务。
在开发中对于松耦合服务,需要让他们尽量的并行执行,避
免阻塞出现,从而最大化程序的吞吐量。
9.1)同步API与异步API
9.1.1)同步API
同步API就是传统的方法调用,以串行的形式实现。比方当调 用多个方法时,第一个方法执行时,其他方法会产生等待, 当第一个方法执行完之后,取得返回结果后,后续的方法在 继续逐一执行。对于这种调用方式,业界内一般称之为阻塞 式调用。
以获取评分最高文章为例:
1 public class SyncDemo {
2
3 static Random random = new Random();
4
5 //接收文章名称,获取并计算文章分数
6 public static int getArticleScore(String
aname){
7
8 int a =
calculateArticleScore(aname);
9 int b =
calculateArticleScore(aname);
10 int c =
calculateArticleScore(aname);
11
12 doSomeThingElse();
13
14 return a+b+c;
15 }
16
17 private static void doSomeThingElse() {
18
19 System.out.println(“exec other
things”);
20 }
21
22 //计算文章分数
23 public static int
calculateArticleScore(String aname){
24
25 //模拟延迟
26 otherService();
27
28 return random.nextInt(100);
29 }
30
31 //模拟服务调用延迟
32 public static void otherService(){
33 try {
34 Thread.sleep(3000L);
35 } catch (InterruptedException e) {
36 throw new RuntimeException(e);
37 }
38 }
39
40
41 public static void main(String[] args) {
42
System.out.println(getArticleScore(“demo”))
;
43 }
44 }
在上述代码中,定义了getArticleScore(),在这个方法内部, 会调用计算文章分数方法calculateArticleScore(),在 calculateArticleScore()中其可能会查询数据库、调用其他服 务(如关注服务、点赞服务等)用于计算文章分数,因为当 前没有其他服务,所以定义otherService(),用于模拟其他服 务的长时间运行,在方法内部延迟三秒钟,接着返回一个100 以内的随机整数,代表文章分数。
此时执行效果,方法每次在执行的时候,为了等待同步的完
成,每次都要等待九秒钟,想象一下,线上服务出现这种情
况的话,那是绝对不能够接受的!!!!并且现在服务器绝
大多数都是多核服务器,这种调用方式也会极大的浪费服务
器的性能!!!
9.1.2)异步API
异步API采用并行的形式完成方法执行,当第一个方法在执行 并且未执行完时,开始另外的线程去执行其他的方法,这种 方式称之为非阻塞式调用。值得注意的是,每一个任务线程 都会将自己的结果返回给调用方,要么通过回调,要么通过
调用方再次执行一个“等待->结束”方法。
9.2)Future接口介绍
此时有的同学会说,对于任务并行需求,直接通过多线程实 现不就可以了, 要注意,对于多线程的实现,java提供了三 种方式:继承Thread类、实现Runnable接口和实现Callable 接口。但是业务代码在执行时会考虑执行顺序的问题,直接 基于这些方式实现多线程会出现两个问题:
1)要想控制线程执行顺序,会通过join()等待线程结束,那 这样的话又回归到了阻塞式调用的思路上,违背了并行的需 求。 另外还可以通过wait()、notify()、notifyAll()结合状态变 量实现,但实现起来过于复杂。
2)线程执行完之后,要想获取线程执行结果,还要用过共享 变量或线程间通信等方式来获取,同样过于复杂。
为了解决上述问题,Java5中推出了Future,其初衷就是用 于构建复杂并行操作。内部方法在返回时,不是返回一个 值,而是返回Future对象。
其本质是在执行主业务的同时,异步的执行其他分业务,从
而利用原本需要同步执行时的等待时间去执行其他的业务,
当需要获取其结果时,再进行获取。
Java官网对于Future的描述:
1 Future表示异步计算的结果。 提供了一些方法来检查计算是
否完成,等待其完成以及检索计算结果。 只有在计算完成后
才可以使用get方法检索结果,必要时将其阻塞,直到准备就
绪为止。 取消通过cancel方法执行。 提供了其他方法来确
定任务是正常完成还是被取消。 一旦计算完成,就不能取消
计算。
在Future接口中有五个抽象方法:
cancel():取消任务, 取消成功返回true;入参 mayInterruptIfRunning表示是否允许取消正在执行中的任 务。
isCancelled():返回布尔值,代表是否取消成功。
isDone():返回布尔值,代表是否执行完毕。
get():返回Future对象,获取执行结果,如果任务没有完成 会阻塞到任务完成再返回。
get(long timeout, TimeUnit unit):获取执行结果并设置超 时时间,如果超时则抛出TimeoutException
9.2)Future应用
Future的使用通常需要配合ExecutorService和Callable一起
使用,使用示例如下:
1 public class FutureAsyncDemo {
2
3 static Random random = new Random();
4
5 static ExecutorService executor =
Executors.newCachedThreadPool();
6
7 //接收文章名称,获取并计算文章分数
8 public static int getArticleScore(String
aname){
9
10
11 Future futureA =
executor.submit(new
CalculateArticleScoreA());
12 Future futureB =
executor.submit(new
CalculateArticleScoreA());
13 Future futureC =
executor.submit(new
CalculateArticleScoreA());
14
15 doSomeThingElse();
16
17
18 Integer a = null;
19 try {
20 a = futureA.get();
21 } catch (InterruptedException e) {
22 futureA.cancel(true);
23 e.printStackTrace();
24 } catch (ExecutionException e) {
25 futureA.cancel(true);
26 e.printStackTrace();
27 }
28
29 Integer b = null;
30 try {
31 b = futureB.get();
32 } catch (InterruptedException e) {
33 futureB.cancel(true);
34 e.printStackTrace();
35 } catch (ExecutionException e) {
36 futureB.cancel(true);
37 e.printStackTrace();
38 }
39
40 Integer c = null;
41 try {
42 c = futureC.get();
43 } catch (InterruptedException e) {
44 futureC.cancel(true);
45 e.printStackTrace();
46 } catch (ExecutionException e) {
47 futureC.cancel(true);
48
49
50
51
52
53
54
55
56
57 e.printStackTrace();
}
executor.shutdown();
return a+b+c;
}
private static void doSomeThingElse() {
System.out.println(“exec other
things”);
58 }
59
60
61 public static void main(String[] args) {
62
System.out.println(getArticleScore(“demo”))
;
63 }
64 }
65
66 class CalculateArticleScoreA implements
Callable{
67
68 @Override
69 public Integer call() throws Exception {
70 //业务代码
71 Random random = new Random();
72 TimeUnit.SECONDS.sleep(3);
73
System.out.println(Thread.currentThread().g
etName());
74 return random.nextInt(100);
75 }
76 }
执行结果
1 exec other things
2 pool-1-thread-1
3 pool-1-thread-3
4 pool-1-thread-2
5 159
上述方法改造了calculateArticleScore(),在其内部基于线程 池调用重写了Callable接口中的call(),并在call()中对具体业 务完成编码,并且让其在执行时睡三秒钟。根据结果可以看 到,先调用了计算文章分数方法,其内部开启了子线程去执 行任务,并且子线程在执行时,并没有阻塞主线程的执行。 当主线程需要结果时,在通过返回的Future来获取子任务中 的返回值。
9.3)Future并行变串行问题解析
刚才已经基于Future演示了并行执行的效果,已经达到了期 望,但是在使用的过程中,其实还有个坑需要说明。对于 Future的使用,如稍加不注意,就会让并行变为串行。
示例代码如下:
1 public class FutureAsyncDemo {
2
3 static ExecutorService executor =
Executors.newCachedThreadPool();
4
5 //接收文章名称,获取并计算文章分数
6 public static int getArticleScore(String
aname){
7
8
9 Future futureA =
executor.submit(new
CalculateArticleScoreA());
10 Future futureB =
executor.submit(new
CalculateArticleScoreB());
11 Future futureC =
executor.submit(new
CalculateArticleScoreC());
12
13 doSomeThingElse();
14
15
16 Integer a = 0;
17 try {
18 a = futureA.get();
19 } catch (InterruptedException e) {
20 futureA.cancel(true);
21 e.printStackTrace();
22 } catch (ExecutionException e) {
23 futureA.cancel(true);
24 e.printStackTrace();
25 }
26
27 Integer b = 0;
28 try {
29 b = futureB.get();
30 } catch (InterruptedException e) {
31 futureB.cancel(true);
32 e.printStackTrace();
33
34
35
36
37
38
39
40
41 } catch (ExecutionException e) {
futureB.cancel(true);
e.printStackTrace();
}
Integer c = 0;
try {
c = futureC.get();
} catch (InterruptedException e) {

42 futureC.cancel(true);
43 e.printStackTrace();
44 } catch (ExecutionException e) {
45 futureC.cancel(true);
46 e.printStackTrace();
47 }
48
49 executor.shutdown();
50 return a+b+c;
51 }
52
53 private static void doSomeThingElse() {
54
55 System.out.println(“exec other
things”);
56 }
57
58
59
60 public static void main(String[] args) {
61
System.out.println(getArticleScore(“demo”))
;
62 }
63 }
64
65 class CalculateArticleScoreA implements
Callable{
66
67 @Override
68 public Integer call() throws Exception {
69 Random random = new Random();
70 TimeUnit.SECONDS.sleep(10);
71
System.out.println(Thread.currentThread().g
etName());
72 return random.nextInt(100);
73 }
74 }
75
76 class CalculateArticleScoreB implements
Callable{
77
78 @Override
79 public Integer call() throws Exception {
80 Random random = new Random();
81 TimeUnit.SECONDS.sleep(20);
82
System.out.println(Thread.currentThread().g
etName());
83 return random.nextInt(100);
84 }
85 }
86
87 class CalculateArticleScoreC implements
Callable{
88
89 @Override
90 public Integer call() throws Exception {
91 Random random = new Random();
92 TimeUnit.SECONDS.sleep(30);
93
System.out.println(Thread.currentThread().g
etName());
94 return random.nextInt(100);
95 }
96 }
上述代码加计算得分方法复制出来两份,各自休眠10秒、20 秒、30秒。当方法返回Future之后,调用get()进行值获取 时,发现每次调用时都需要进行等待。这样可以发现,之前 的并行现在变成了串行了!!!! 这个问题为什么会产生 呢?需要看一下Future中对于get()的介绍
根据源码可知,当调用get()时,其会等待对应方法执行完毕 后,才会返回结果,否则会一直等待。因为这个设定,所以 上述代码则出现并行变串行的效果。
对于这个问题的解决,可以调用get()的重载,get(long timeout, TimeUnit unit)。设置等待的时长,如果超时则抛 出TimeoutException。
使用示例如下:
1 public class FutureAsyncDemo {
2
3 static Random random = new Random();
4
5 static ExecutorService executor =
Executors.newCachedThreadPool();
6
7 //接收文章名称,获取并计算文章分数
8 public static int
getArticleScore(String aname){
9
10
11 Future futureA =
executor.submit(new
CalculateArticleScoreA());
12 Future futureB =
executor.submit(new
CalculateArticleScoreB());
13 Future futureC =
executor.submit(new
CalculateArticleScoreC());
14
15 doSomeThingElse();
16
17
18 Integer a = 0;
19 try {
20 a = futureA.get();
21
22 } catch (InterruptedException e) {
23 futureA.cancel(true);
24 e.printStackTrace();
25 } catch (ExecutionException e) {
26 futureA.cancel(true);
27 e.printStackTrace();
28 }
29
30
31 Integer b = 0;
32 try {
33 b = futureB.get(3L,
TimeUnit.SECONDS);
34
35 } catch (TimeoutException e) {
36 e.printStackTrace();
37 }
38 catch (InterruptedException e) {
39 futureB.cancel(true);
40 e.printStackTrace();
41 } catch (ExecutionException e) {
42 futureB.cancel(true);
43 e.printStackTrace();
44 }
45
46
47 Integer c = 0;
48 try {
49 c = futureC.get();
50 } catch (InterruptedException e) {
51 futureC.cancel(true);
52 e.printStackTrace();
53 } catch (ExecutionException e) {
54 futureC.cancel(true);
55
56
57
58
59
60
61
62
63
64 e.printStackTrace();
}
executor.shutdown();
return a+b+c;
}
private static void doSomeThingElse() {

65 System.out.println(“exec other
things”);
66 }
67
68
69
70 public static void main(String[] args)
{
71
System.out.println(getArticleScore(“demo”)
);
72 }
73 }
74
75 class CalculateArticleScoreA implements
Callable{
76
77 @Override
78 public Integer call() throws Exception
{
79 Random random = new Random();
80 TimeUnit.SECONDS.sleep(10);
81
System.out.println(Thread.currentThread().
getName());
82 return random.nextInt(100);
83 }
84 }
85
86 class CalculateArticleScoreB implements
Callable{
87
88 @Override
89 public Integer call() throws Exception
{
90 Random random = new Random();
91 TimeUnit.SECONDS.sleep(20);
92
System.out.println(Thread.currentThread().
getName());
93 return random.nextInt(100);
94 }
95 }
96
97 class CalculateArticleScoreC implements
Callable{
98
99 @Override
100 public Integer call() throws Exception
{
101 Random random = new Random();
102 TimeUnit.SECONDS.sleep(30);
103
System.out.println(Thread.currentThread().
getName());
104 return random.nextInt(100);
105 }
106 }
在上述方法中,对于B的get()设置了超时时间三秒钟,如果当 调用其获取返回值时,如果超过三秒仍然没有返回结果,则 抛出超时异常,接着方法会再次向下运行。
对于Future来说,它能够支持任务并发执行,对于任务结果
的获取顺序是按照提交的顺序获取,在使用的过程中建议通 过CPU高速轮询的方式获取任务结果,但这种方式比较耗费 资源。不建议使用
9.4)FutureTask源码解析
FutureTask是RunnableFuture接口的实现类,并且在 RunnableFuture接口中继承了Runnable+Future。所以 FutureTask可以像Future一样交给Executor执行,也可以直 接由Runnable中的run执行。其出现的初衷就是为了弥补 Thread的不足。
其基本实现原理与Future类似,内部通过状态转换判断完成 任务执行校验与执行,另外,FutureTask还可以确保即使调 用了多次run方法,它都只会执行一次Runnable或者Callable 任务,或者通过cancel取消FutureTask的执行等待。
根据源码的介绍,FutureTask定义了一些其内部需要使用到
的成员变量。
1 state:通过volatile修饰,所以多线程可以获取它的最新
值,取值范围0-6,对应下面的六个常量值。
2
3 callable:FutureTask可以直接接收Callable类型执行
任务,当运行后消失。
4
5 outcome:任务执行后或异常抛出后的返回信息。
6
7 runner:用于记录执行任务的线程。
8
9 waiters:等待task执行结果的线程队列。
其内部还有两个构造方法,用于初始化任务
当传入类型是Callable,如果不为null,则将其赋给成员变
量,并且设置当前任务状态为新建。
当传入类型是Runnable,其会将runnable转换为callable并 赋给成员变量,同时设置任务状态为新建。
9.4.1)get()源码分析
对比有参和无参get(),其基本逻辑都是相同的,首先会判断 当前任务的状态, 如果为完成中,则调用awaitDone()执行 线程等待,返回时调用report()。但是在调用awaitDone()是 传入的参数是不同的。其源码如下:
1)根据传入的布尔值timed,计算等待时间deadline,如果 为true,等待时间为当前时间+传入的等待时间,如果为 false,等待时间为0。
2)判断线程是否中断,如果线程中断,将当前线程从等待队 列waiters中移除,并抛出中断异常。
3)否则判断当前任务状态,如果任务状态值大于完成中(相 当于已完成),并且等待队列不为null,则将等待线程节点的 线程置为null,返回state状态值。
4)否则如果task状态为完成中,调用Thread.yield()将线程
从执行状态变为可执行状态。
5)否则如果等待线程节点为null,初始化等待线程节点。
6)否则如果当前等待线程节点q还未成功进入等待队列 waiters,进入线程等待队列。
7)否则判断timed,如果为true,判断当前是否超时,如果 已经超时,将当前等待线程节点从waiters中移出,返回任务 状态,如果还未超时,调用LockSupport.parkNanos方法阻 塞当前线程。如果为false,调用LockSupport.park方法,阻 塞当前线程,并再次进入到循环中。
对于get()来说,其内部基于自旋锁思想实现,内部维护一个 等待线程队列waiters,如果任务还未执行完毕,调用get方 法的线程会先进入等待队列自旋等待,知道任务状态变为已 完成状态或者等待时间超过超时时间或者线程中断才会跳出 循环,内部不会一直进行循环,而是采用线程阻塞方式来节 省开销。
9.4.2)run()源码分析
1)如果任务状态不是new或者不在等待队列,代表当前任务
已执行,直接返回。
2)否则判断如果callable不等于null并且状态为new,则调 用c.call()调起任务,接着执行set(),设置call方法返回结果以 及任务状态。
3)最终设置当前运行当前任务的线程为null,如果任务状态
为中断中或已中断,则调用 handlePossibleCancellationInterrupt(),其内部会调用 Thread.yield()将线程从执行状态变为可执行状态。
9.4.3)set()源码分析
set()主要用于设置返回结果,同时调用finishCompletion()操 作等待队列waiters中的等待线程
9.4.4)cancel()源码分析
1)判断任务状态是否为NEW并且如果传入参数为true,设置 任务状态为正在中断,如果为false,设置任务状态为已取 消。当设置失败返回false。
2)判断入参,如果为true,中断线程,并设置任务状态为已 中断。
3)最后执行finishCompletion(),清除和唤醒等待队列 waiters中的等待线程,返回true,程序结束。
9.4.5)小结
根据run(),FutureTask有3种状态:未启动、已启动、已完 成。在完成中又分为正常、取消、中断。
对于FutureTask的方法执行示意图(方法和Future接口基本 是一样的,这里就不过多描述了)
当FutureTask处于未启动或已启动状态时,如果此时我们 执行FutureTask.get()方法将导致调用线程阻塞;当 FutureTask处于已完成状态时,执行FutureTask.get()方 法将导致调用线程立即返回结果或者抛出异常。 当FutureTask处于未启动状态时,执行
FutureTask.cancel()方法将导致此任务永远不会执行。当 FutureTask处于已启动状态时,执行cancel(true)方法将 以中断执行此任务线程的方式来试图停止任务,如果任务 取消成功,cancel(…)返回true;但如果执行cancel(false) 方法将不会对正在执行的任务线程产生影响(让线程正常执 行到完成),此时cancel(…)返回false。当任务已经完成, 执行cancel(…)方法将返回false。
FutureTask获取任务结果顺序无法确定,当向Executor提交 多个任务并且希望获得它们完成之后的结果,需要循环获取 task,并调用get方法去获取task执行结果,但是如果task还 未完成,获取结果的线程将阻塞直到task完成,由于不知道 哪个task优先执行完毕,使用这种方式效率不会很高。所以 不建议使用。
9.5)CompletionService应用与解析
9.5.1)介绍
为了解决FutureTask的问题,JDK1.5还提出了 CompletionService,它是一个接口,其实现类为 ExecutorCompletionService。它整合了Executor和 BlockingQueue的功能,在多任务执行时,对于结果的获取 时按照任务完成的先后顺序来获取。在JDK8之前,它是最好 的结果方案,推荐使用。
9.5.2)源码解析
9.5.2.1)CompletionService源码解析
在CompletionService定义了五个方法submit(Callable task)、submit(Runnable task, V result)、take()、poll()、 poll(long timeout, TimeUnit unit)。
submit(Callable task):接收Callable类型的任务,返回 Future对象,当任务完成后,可以选择执行或轮询。
submit(Runnable task, V result):接收Runnable 类型的 任务,并且还要接收w完成后的返回结果,返回Future对 象,当任务完成后,可以选择执行或轮询。
take():获取并移除处于已完成状态的任务,如果不存在已 完成的任务,则等待。
poll():获取并移除已完成状态的task,如果不存在已完成的 任务,则返回null。
poll(long timeout, TimeUnit unit):获取并移除已完成状 态的task,如果在指定等待时间内不存在已完成的任务,则 返回null。
9.5.2.2) ExecutorCompletionService源码解析
ExecutorCompletionService是CompletionService接口的实
现类,首先其内部成员变量有三个
executor:用于执行任务的线程池,必须指定。
aes:用于创建待执行的任务。
completionQueue:是基于链表结构的阻塞队列 LinkedBlockingQueue,用于存储已完成的任务。
1)submit()
用于提交任务,内部完成很简单,将传入参数转换为
Future,并放入队列中。
2)take()&poll()
这两个方法都是从队列中获取已完成的任务,不过task()在获 取时,如果任务还没有完成则会等待。而poll()在获取时,如 果任务还没完成则不会等待,直接返回null。
9.5.3)应用案例
1 public class CompletionServiceDemo {
2
3 static ExecutorService executor =
Executors.newCachedThreadPool();
4
5 public static void main(String[] args) {
6
7 CompletionService
completionService = new
ExecutorCompletionService<>(executor);
8
9 completionService.submit(new
CalculateArticleScoreA());
10 completionService.submit(new
CalculateArticleScoreDemoB());
11 completionService.submit(new
CalculateArticleScoreDemoC());
12
13 doSomeThingElse();
14
15 Integer result = 0;
16 for(int i=0;i<3;i++){
17 try {
18
result+=completionService.take().get();
19 } catch (InterruptedException e)
{
20 e.printStackTrace();
21 } catch (ExecutionException e) {
22 e.printStackTrace();
23 }
24 }
25
26 System.out.println(result);
27
28 executor.shutdown();
29
30 }
31
32 private static void doSomeThingElse() {
33
34 System.out.println(“exec other
things”);
35 }
36 }
37
38
39
40 class CalculateArticleScoreDemoA implements
Callable {
41
42 @Override
43 public Integer call() throws Exception {
44 //业务代码
45 Random random = new Random();
46 TimeUnit.SECONDS.sleep(3);
47
System.out.println(Thread.currentThread().g
etName());
48 return random.nextInt(100);
49 }
50 }
51
52 class CalculateArticleScoreDemoB implements
Callable {
53
54 @Override
55 public Integer call() throws Exception {
56 //业务代码
57 Random random = new Random();
58 TimeUnit.SECONDS.sleep(3);
59
System.out.println(Thread.currentThread().g
etName());
60 return random.nextInt(100);
61 }
62 }
63
64 class CalculateArticleScoreDemoC implements
Callable {
65
66 @Override
67 public Integer call() throws Exception {
68 //业务代码
69 Random random = new Random();
70 TimeUnit.SECONDS.sleep(3);
71
System.out.println(Thread.currentThread().g
etName());
72 return random.nextInt(100);
73 }
74 }
9.6)Future接口局限性
截止现在已经对异步编程进行了相关的实现,但是有一些需 求,按照现在的实现是无法做到的。如:
1)将两个相互独立的异步计算合并为一个,同时第二个异步 计算依赖于第一个异步计算的结果。
2)等待Future集合中所有任务都完成。
3)仅等待Future集合中最快结束的任务完成,并返回结果。
4)当Future的完成事件发生时会发起通知,并能使用Future 计算的结果进行下一步操作,而不是简单的阻塞。
对于这些问题,使用Future中提供的方法是无法完成的。 Java8通过CompletableFuture对这些问题进行了解决,其内 部实现思路仍旧是依赖于Stream流式编程模式来完成。
9.7)CompletableFuture应用与源码 解析
CompletableFuture是Java1.8提供的一个新类,其实现了 Future与CompletionStage两个接口。提供了诸多API扩展功 能,可以通过Stream形式简化异步编程的复杂度,同时提供 通过回调方式处理计算结果。
9.7.1)异步任务创建
在CompletableFuture中提供了四个静态方法用于创建异步 任务
1 runAsync(Runnable runnable)
2 runAsync(Runnable runnable,Executor executor)
3 supplyAsync(Supplier supplier)
4 supplyAsync(Supplier supplier,Executor
executor)
9.7.1.1)runAsync()应用与源码解析
根据源码可知,runAsync()分为一个参数和两个参数,并且 其内部都会调用asyncRunStage().
在该方法内部会创建异步任务,并把任务放入线程池中。并
且runAsync()是没有返回值的。
根据源码可知,当传入Executor会使用指定线程池执行,如 果没有传入则使用默认ForkJoinPool.commonPool()执行, 值得注意的是,commonPool中都是守护线程,主线程执行 完,子线程也就over了。因此建议当任务非常耗时,使用自 定义线程池。
1 //未设置Executor
2 public class Demo1 {
3
4 public static void main(String[] args) {
5
6 CompletableFuture future =
CompletableFuture.runAsync(() -> {
7 try {
8 TimeUnit.SECONDS.sleep(3);
9 System.out.println(“child
run”);
10 } catch (InterruptedException e)
{
11 e.printStackTrace();
12 }
13 });
14
15 System.out.println(“main end”);
16
17 }
18 }
根据结果可以看到,子线程没有来得及打印,主线程就结束
了。
1 //设置自定义线程池
2 public class Demo1 {
3
4 public static void main(String[] args) {
5
6 ExecutorService executor =
Executors.newFixedThreadPool(100);
7
8 CompletableFuture future =
CompletableFuture.runAsync(() -> {
9 try {
10 TimeUnit.SECONDS.sleep(3);
11 System.out.println(“child
run”);
12 } catch (InterruptedException e)
{
13 e.printStackTrace();
14 }
15 },executor);
16
17 System.out.println(“main end”);
18
19 executor.shutdown();
20
21 }
22 }
根据结果可以看到,就算主线程执行完,但并不会关闭子线
程,子线程结果一样可以输出
9.7.1.2)supplyAsync()应用与源码解析
根据源码可知,supplyAsync()分为一个参数和两个参数,并 且其内部都会调用asyncSupplyStage().
现在可知,其实supplyAsync()与runAsync()内部原理类似, 但supplyAsync()有返回值。
1 public class Demo1 {
2
3 public static void main(String[] args) {
4
5 ExecutorService executor =
Executors.newFixedThreadPool(100);
6
7 CompletableFuture future =
CompletableFuture.supplyAsync(() -> {
8 try {
9 TimeUnit.SECONDS.sleep(3);
10 System.out.println(“child
run”);
11
12 } catch (InterruptedException e)
{
13 e.printStackTrace();
14 }
15 return 123;
16 },executor);
17
18 System.out.println(“main end”);
19 try {
20 Integer integer = future.get();
21 System.out.println(integer);
22 } catch (InterruptedException e) {
23 e.printStackTrace();
24 } catch (ExecutionException e) {
25 e.printStackTrace();
26 }
27
28 executor.shutdown();
29
30 }
31 }
9.7.2)异步计算结果触发回调
当异步任务结束或者抛出异常时,还要根据结果来完成特定
的操作,对于这种需求CompletableFuture也提供了方法进
行实现
1 public CompletableFuture
whenComplete(BiConsumer<? super T,? super
Throwable> action)
2
3 public CompletableFuture
whenCompleteAsync(BiConsumer<? super T,?
super Throwable> action)
4
5 public CompletableFuture
whenCompleteAsync(BiConsumer<? super T,?
super Throwable> action, Executor executor)
6
7 public CompletableFuture
exceptionally(Function<Throwable,? extends T>
fn)
9.7.2.1)whenComplete()与whenCompleteAsync()使用
与源码解析
根据源码可知,上述三个方法都会接收BiConsumer并调用 uniWhenCompleteStage,BiConsumer用于定义后续处理 业务,处理正常计算结果或异常情况。在 uniWhenCompleteStage主要用于判断任务执行状态以及构 建任务。
值得注意的一点是,whenComplete是在当前任务线程中继 续执行指定的特定处理,而whenCompleteAsync会将指定 的特定交给线程池另开启一个线程来执行。
1 public class Demo1 {
2
3 public static void main(String[] args) {
4
5 ExecutorService executor =
Executors.newFixedThreadPool(100);
6
7
System.out.println(Thread.currentThread().g
etName());
8
9 CompletableFuture future =
CompletableFuture.supplyAsync(() -> {
10 try {
11 System.out.println(“异步任务
线程:”+Thread.currentThread().getName());
12 TimeUnit.SECONDS.sleep(3);
13 System.out.println(“child
run”);
14
15 } catch (InterruptedException e)
{
16 e.printStackTrace();
17 }
18 return 123;
19 },executor);
20
21 System.out.println(“main end”);
22
23 try {
24 future.whenComplete(new
BiConsumer<Integer, Throwable>() {
25 @Override
26 public void accept(Integer
integer, Throwable throwable) {
27 System.out.println(“结果
触发任务线
程:”+Thread.currentThread().getName());
28 System.out.println(“特定
任务执行”);
29 }
30 });
31 }catch (Exception e){
32 e.printStackTrace();
33 }
34
35 executor.shutdown();
36
37 }
38 }
执行后结果
1 main
2 main end
3 异步任务线程:pool-1-thread-1
4 child run
5 结果触发任务线程:pool-1-thread-1
6 特定任务执行
根据执行结果可知,异步任务结束后,当使用
whenComplete()时,后续的特定处理任务使用的线程与异步 任务线程相同。
当使用whenCompleteAsync(),并指定线程池后
1 public class Demo1 {
2
3 public static void main(String[] args) {
4
5 ExecutorService executor =
Executors.newFixedThreadPool(100);
6
7
System.out.println(Thread.currentThread().g
etName());
8
9 CompletableFuture future =
CompletableFuture.supplyAsync(() -> {
10 try {
11 System.out.println(“异步任务
线程:”+Thread.currentThread().getName());
12 TimeUnit.SECONDS.sleep(3);
13 System.out.println(“child
run”);
14
15 } catch (InterruptedException e)
{
16 e.printStackTrace();
17 }
18 return 123;
19 },executor);
20
21 System.out.println(“main end”);
22
23 try {
24 future.whenCompleteAsync(new
BiConsumer<Integer, Throwable>() {
25 @Override
26 public void accept(Integer
integer, Throwable throwable) {
27 System.out.println(“结果
触发任务线
程:”+Thread.currentThread().getName());
28 System.out.println(“特定
任务执行”);
29 executor.shutdown();
30 }
31 },executor);
32 }catch (Exception e){
33 e.printStackTrace();
34 }
35 }
36 }
运行结果
1 main
2 main end
3 异步任务线程:pool-1-thread-1
4 child run
5 结果触发任务线程:pool-1-thread-2
6 特定任务执行
根据结果可知,后续的处理任务在线程池中又开启了一个新
的线程进行使用。
9.7.2.2) exceptionally()使用与源码解析
exceptionally()与上述两个方法类似,都是用于当异步任务结 束后,执行特定处理,但不同的是,上述两个方法即可以处 理正常的返回结果也可以处理异常,而exceptionally()只对异 常进行处理,且其使用的是主线程。
根据源码可知,其内部实现原理与上述两个方法类似,都是
判断任务是否执行完,如果执行完构建一个后续任务进行执
行。
1 public class Demo1 {
2
3 public static void main(String[] args) {
4
5 ExecutorService executor =
Executors.newFixedThreadPool(100);
6
7
System.out.println(Thread.currentThread().g
etName());
8
9 CompletableFuture future =
CompletableFuture.supplyAsync(() -> {
10 try {
11 System.out.println(“异步任务
线程:”+Thread.currentThread().getName());
12 int i=1/0;
13 TimeUnit.SECONDS.sleep(3);
14 System.out.println(“child
run”);
15
16 } catch (InterruptedException e)
{
17 e.printStackTrace();
18 }
19 return 123;
20 },executor);
21
22 System.out.println(“main end”);
23
24 future.exceptionally(new
Function<Throwable, Integer>() {
25 @Override
26 public Integer apply(Throwable
throwable) {
27 System.out.println(“异常结果
触发任务线
程:”+Thread.currentThread().getName());
28 System.out.println(“异步任务
执行失败:”+throwable.getMessage());
29 return null;
30 }
31 });
32 }
33 }
返回结果
1 main
2 main end
3 异步任务线程:pool-1-thread-1
4 结果触发任务线程:pool-1-thread-2
5 特定任务执行
6 异常结果触发任务线程:main
7 异步任务执行失败:java.lang.ArithmeticException:
/ by zero
根据返回结果可以看到,当异步任务出现异常之后,
whenCompleteAsync与exceptionally都被触发了,但 exceptionally使用的是当前主线程。
9.7.3)多任务依赖执行
在异步编程中,有时会涉及到异步任务间存在依赖关系,如
第二个任务的执行需要依赖与第一个任务的执行结果。对于
这种需求,CompletableFuture中也提供了方法实现
9.7.3.1)thenApply()使用
该方法会接收一个Function,用于声明后续要执行的业务, 其中T代表上一个方法的执行结果,fn代表当前任务的结果数 据类型,最终其会映射到CompletableFuture中的结果数据 类型。
thenApply()使用与刚才的方法内部实现原理都是类似的,此 处重点关注对于该方法的使用
1 public class ThenApplyDemo {
2
3 public static void main(String[] args) {
4
5 ExecutorService executor =
Executors.newFixedThreadPool(100);
6
7 CompletableFuture future =
CompletableFuture.supplyAsync(() -> {

handle()的使用效果与thenApply()类似,但不同的是 thenApply()只能处理任务的正常返回结果,一旦出现异常则 无法进行后续处理。而handle()即可以处理正常结果,也可 以处理异常结果。
1 public class ThenApplyDemo {
2
3 public static void main(String[] args) {
4
5 ExecutorService executor =
Executors.newFixedThreadPool(100);
6
7 CompletableFuture future =
CompletableFuture.supplyAsync(() -> {
8
9 int value = new
Random().nextInt(100);
10 int i=1/0;
11 System.out.println(value);
12 return value;
13
14 }, executor)
15 .handle(new BiFunction<Integer,
Throwable, Integer>() {
16 @Override
17 public Integer apply(Integer
integer, Throwable throwable) {
18 int result=1;
19 if (throwable == null){
20 result = integer *
10;
21
System.out.println(result);
22
23 }else {
24
System.out.println(throwable.getMessage());
25 }
26 return result;
27 }
28 });
29
30 try {
31 Integer result = future.get();
32 System.out.println(result);
33 } catch (InterruptedException e) {
34 e.printStackTrace();
35 future.cancel(true);
36 } catch (ExecutionException e) {
37 e.printStackTrace();
38 future.cancel(true);
39 }finally {
40 executor.shutdown();
41 }
42
43
44 } }

根据上述代码的运行可以看到,当第一个任务出现异常后,
第二个任务会对该异常进行后续的处理,完成串性操作。
9.7.3.3)thenAccept()使用
当将多个任务连接起来执行时,有时最终是不需要返回结
果,CompletableFuture中也提供了方法实现。
thenAccept()使用与上述方法类似,接收任务执行结果,并 使用,但其没有结果返回。
1 public class ThenAcceptDemo {
2
3 public static void main(String[] args) {
4
5 ExecutorService executor =
Executors.newFixedThreadPool(100);
6
7 CompletableFuture.supplyAsync(() ->
{
8
9 int value = new
Random().nextInt(100);
10 System.out.println(value);
11 return value;
12 }).thenAcceptAsync(new
Consumer() {
13 @Override
14 public void accept(Integer
integer) {
15 System.out.println(“接收上一
个任务结果为:” + integer);
16 }
17 },executor);
18
19 executor.shutdown();
20 }
21 }
9.7.3.4)thenRun()使用
thenRun()与thenAccept()使用基本相同,都是不会进行结果 返回,但不同的是,thenRun()不关心方法是否有结果,只要 它完成,就会触发其执行。
1 public class ThenRunDemo {
2
3 public static void main(String[] args) {
4
5 ExecutorService executorService =
Executors.newFixedThreadPool(100);
6
7 CompletableFuture.supplyAsync(()->{
8
9 int i = new
Random().nextInt(100);
10 return i;
11 }).thenRun(()->
System.out.println(“run方法执行”));
12
13 executorService.shutdown();
14 }
15 }
9.7.4)两任务合并执行
9.7.4.1)两个任务全部完成触发
在进行多异步任务执行时,有时不光要让任务之间串联执
行,有时还要将多个任务执行结果进行合并处理,
CompletableFuture中也提供了一些方法实现。
9.7.4.1.1)thenCombine()使用
当两个异步任务都执行完毕后,它可以将两个任务进行合
并,获取到两个任务的执行结果,进行合并处理,最后会有
返回值。
根据源码其内部都执行了biApplyStage()
内部会判断当前要执行的函数是否为null,或者任务有没有执 行完。如果为true,则抛出空指针异常。接着会构建一个新 的任务,将任务放入栈中,线程池会为其分配线程让其执 行。
1 public class ThenCombineDemo {
2
3 public static void main(String[] args)
throws ExecutionException,
InterruptedException {
4
5 ExecutorService executorService =
Executors.newFixedThreadPool(100);
6
7 CompletableFuture future1 =
CompletableFuture.supplyAsync(()->{
8
System.out.println(“future1:”+Thread.curre
ntThread().getName());
9 return “hello”;
10 },executorService);
11
12
13 CompletableFuture future2 =
CompletableFuture.supplyAsync(()-> {
14
System.out.println(“future2:”+Thread.curre
ntThread().getName());
15 return “itheima”;
16 },executorService);
17
18 CompletableFuture result =
future1.thenCombineAsync(future2, (f1,f2)->
{
19
20
System.out.println(“result:”+Thread.curren
tThread().getName());
21 return f1+" "+f2;
22 },executorService);
23
24 System.out.println(result.get());
25
26 executorService.shutdown();
27 }
28 }
返回结果
1 future1:pool-1-thread-1
2 future2:pool-1-thread-2
3 result:pool-1-thread-3
4 hello itheima
根据返回结果可以看到,这一系列操作相当于构建了三个异
步任务进行执行。
9.4.7.1.2)thenAcceptBoth()使用
thenAcceptBoth()使用与thenCombine()类似,当两个任务 执行完,获取两个任务的结果进行特定处理,但 thenAcceptBoth()没有返回值
其内部都调用了biAcceptStage(),其内部实现原理与上述方 法类似。
1 public class ThenAcceptBothDemo {
2
3 public static void main(String[] args) {
4
5 ExecutorService executorService =
Executors.newFixedThreadPool(100);
6
7 CompletableFuture future1 =
CompletableFuture.supplyAsync(() -> {
8 int f1 = new
Random().nextInt(100);
9 System.out.println(“f1 value:”

  • f1);
    10 return f1;
    11 },executorService);
    12
    13 CompletableFuture future2 =
    CompletableFuture.supplyAsync(() -> {
    14 int f2 = new
    Random().nextInt(100);
    15 System.out.println(“f2 value:”
  • f2);
    16 return f2;
    17 },executorService);
    18
    19 future1.thenAcceptBoth(future2,
    (f1,f2)-> System.out.println(f1+f2));
    20
    21 executorService.shutdown();
    22
    23 }
    24 }
    9.4.7.1.3)runAfterBoth()使用
    当两个任务执行完毕,触发特定任务处理,但不要两个异步
    任务结果,且不会进行值返回。
    1 public class RunAfterBothDemo {
    2
    3 public static void main(String[] args) {
    4
    5 ExecutorService executorService =
    Executors.newFixedThreadPool(100);
    6
    7 CompletableFuture future1 =
    CompletableFuture.supplyAsync(() -> {
    8 int f1 = new
    Random().nextInt(100);
    9 System.out.println(“f1 value:”
  • f1);
    10 return f1;
    11 },executorService);
    12
    13 CompletableFuture future2 =
    CompletableFuture.supplyAsync(() -> {
    14 try {
    15 TimeUnit.SECONDS.sleep(5);
    16 } catch (InterruptedException e)
    {
    17 e.printStackTrace();
    18 }
    19 int f2 = new
    Random().nextInt(100);
    20 System.out.println(“f2 value:”
  • f2);
    21 return f2;
    22 },executorService);
    23
    24 future1.runAfterBothAsync(future2,
    ()-> {
    25 System.out.println(“两个任务都已执
    行完”);
    26 executorService.shutdown();
    27 },executorService);
    28
    29 }
    30 }
    9.4.7.2)两个任务任意一个完成触发
    9.4.7.2.1)applyToEither()使用
    当两个任务异步任务执行,谁先执行完,就以谁的结果为
    准,完成后续的业务处理,并且会进行结果值返回。
    1 public class ApplyToEitherDemo {
    2
    3 public static void main(String[] args)
    throws ExecutionException,
    InterruptedException {
    4 ExecutorService executorService =
    Executors.newFixedThreadPool(100);
    5
    6 CompletableFuture future1 =
    CompletableFuture.supplyAsync(()->{
    7 try {
    8 TimeUnit.SECONDS.sleep(5);
    9 } catch (InterruptedException e)
    {
    10 e.printStackTrace();
    11 }
    12
    System.out.println(“future1:”+Thread.curre
    ntThread().getName());
    13 return “hello”;
    14 },executorService);
    15
    16
    17 CompletableFuture future2 =
    CompletableFuture.supplyAsync(()-> {
    18 try {
    19 TimeUnit.SECONDS.sleep(3);
    20 } catch (InterruptedException e)
    {
    21 e.printStackTrace();
    22 }
    23
    System.out.println(“future2:”+Thread.curre
    ntThread().getName());
    24 return “itheima”;
    25 },executorService);
    26
    27 CompletableFuture result =
    future1.applyToEitherAsync(future2, (value)-

{
28
29
System.out.println(“result:”+Thread.curren
tThread().getName());
30 return value;
31 },executorService);
32
33 System.out.println(result.get());
34
35 executorService.shutdown();
36 }
37 }
9.4.7.2.2)acceptEither()使用
acceptEither()的使用效果与applyToEither()类似,但 acceptEither()没有返回值
其内部调用了orAcceptStage()
1 public class AcceptEitherDemo {
2
3 public static void main(String[] args)
throws ExecutionException,
InterruptedException {
4 ExecutorService executorService =
Executors.newFixedThreadPool(100);
5
6 CompletableFuture future1 =
CompletableFuture.supplyAsync(()->{
7 try {
8 TimeUnit.SECONDS.sleep(5);
9 } catch (InterruptedException e)
{
10 e.printStackTrace();
11 }
12
System.out.println(“future1:”+Thread.curre
ntThread().getName());
13 return “hello”;
14 },executorService);
15
16
17 CompletableFuture future2 =
CompletableFuture.supplyAsync(()-> {
18 try {
19 TimeUnit.SECONDS.sleep(3);
20 } catch (InterruptedException e)
{
21 e.printStackTrace();
22 }
23
System.out.println(“future2:”+Thread.curre
ntThread().getName());
24 return “itheima”;
25 },executorService);
26
27
28 future1.acceptEitherAsync(future2,
(value)-> {
29
System.out.println(“result:”+Thread.curren
tThread().getName());
30 System.out.println(value);
31 executorService.shutdown();
32 },executorService);
33 }
34 }
9.4.7.2.3)runAfterEither()使用
当两个任务执行,只要有一个任务执行完,则触发特定处理
执行,无需使用异步任务的执行结果,且特定处理不会进行
值的返回。
1 public class RunAfterEitherDemo {
2
3 public static void main(String[] args)
throws ExecutionException,
InterruptedException {
4 ExecutorService executorService =
Executors.newFixedThreadPool(100);
5
6 CompletableFuture future1 =
CompletableFuture.supplyAsync(()->{
7 try {
8 TimeUnit.SECONDS.sleep(5);
9 } catch (InterruptedException e)
{
10 e.printStackTrace();
11 }
12
System.out.println(“future1:”+Thread.curre
ntThread().getName());
13 return “hello”;
14 },executorService);
15
16
17 CompletableFuture future2 =
CompletableFuture.supplyAsync(()-> {
18 try {
19 TimeUnit.SECONDS.sleep(3);
20 } catch (InterruptedException e)
{
21 e.printStackTrace();
22 }
23
System.out.println(“future2:”+Thread.curre
ntThread().getName());
24 return “itheima”;
25 },executorService);
26
27
28 future1.runAfterEitherAsync(future2,
()-> System.out.println(“其中一个任务处理完成
了”),executorService);
29 }
30 }
9.7.5)多任务组合执行
刚才的操作异步任务的数量,只能局限在两个,现在如果需
要有任意多个异步任务进行组合操作的话,
CompletableFuture中也提供了对应方法进行实现
1 public static CompletableFuture
allOf(CompletableFuture<?>… cfs)
2
3 public static CompletableFuture
anyOf(CompletableFuture<?>… cfs)
9.7.5.1)allOf()使用与源码分析
当一个特定业务处理任务的执行需要一组异步任务完成后才 能执行的话,就可以通过allOf()实现。适用场景:假设现在有 一个Z任务,它的执行需要[A,B,C,D,E,F]这一组异步任务全部 执行完才能触发。
其内部调用了andTree(),传递任意多个异步任务。其内部会
基于二分查找法,将一个数组构建成一个二叉树,并且同时
将两个任务添加到栈中执行。
判断任务何时添加到栈中,其内部又会调用biRelay()传递两 个任务,如果a,b都结束了才继续往下。
使用示例如下:
1 public class AllOfDemo {
2
3 public static void main(String[] args) {
4
5 ExecutorService executorService =
Executors.newFixedThreadPool(100);
6
7 CompletableFuture future1 =
CompletableFuture.supplyAsync(() -> {
8 int f1 = new
Random().nextInt(100);
9 System.out.println(“f1 value:”

  • f1);
    10 return f1;
    11 },executorService);
    12
    13 CompletableFuture future2 =
    CompletableFuture.supplyAsync(() -> {
    14 try {
    15 TimeUnit.SECONDS.sleep(5);
    16 } catch (InterruptedException e)
    {
    17 e.printStackTrace();
    18 }
    19 int f2 = new
    Random().nextInt(100);
    20 System.out.println(“f2 value:”
  • f2);
    21 return f2;
    22 },executorService);
    23
    24 CompletableFuture future3 =
    CompletableFuture.supplyAsync(() -> {
    25 try {
    26 TimeUnit.SECONDS.sleep(5);
    27 } catch (InterruptedException e)
    {
    28 e.printStackTrace();
    29 }
    30 int f3 = new
    Random().nextInt(100);
    31 System.out.println(“f3 value:”
  • f3);
    32 return f3;
    33 },executorService);
    34
    35 List<CompletableFuture>
    list = new ArrayList<>();
    36 list.add(future1);
    37 list.add(future2);
    38 list.add(future3);
    39
    40 CompletableFuture all =
    CompletableFuture.allOf(list.toArray(new
    CompletableFuture[]{}));
    41
    42
    43 all.thenRunAsync(()->{
    44 AtomicReference result
    = new AtomicReference<>(0);
    45
    list.parallelStream().forEach(future->{
    46
    47 try {
    48 Integer value =
    future.get();
    49 result.updateAndGet(v ->
    v + value);
    50
    System.out.println(result);
    51 } catch
    (InterruptedException e) {
    52 e.printStackTrace();
    53 } catch (ExecutionException
    e) {
    54 e.printStackTrace();
    55 }
    56 });
    57 });
    58
    59 }
    60 }
    9.7.5.2)anyOf()使用与源码分析
    anyOf()与allOf()类似,但不同的是,使用anyOf()时,当一组 异步任务中,只要有一个执行完毕,则会被触发,利用该特 性可以用来获取最快的那个线程结果。
    其内部实现原理与allOf()都是一样的,内部也会基于二分查找 构建一个二叉树。
    使用示例如下:
    1 public class AnyOfDemo {
    2
    3 public static void main(String[] args) {
    4
    5 ExecutorService executorService =
    Executors.newFixedThreadPool(100);
    6
    7 CompletableFuture future1 =
    CompletableFuture.supplyAsync(() -> {
    8 int f1 = new
    Random().nextInt(100);
    9 System.out.println(“f1 value:”
  • f1);
    10 return f1;
    11 },executorService);
    12
    13
    14 CompletableFuture future2 =
    CompletableFuture.supplyAsync(() -> {
    15 try {
    16 TimeUnit.SECONDS.sleep(5);
    17 } catch (InterruptedException e)
    {
    18 e.printStackTrace();
    19 }
    20 int f2 = new
    Random().nextInt(100);
    21 System.out.println(“f2 value:”
  • f2);
    22 return f2;
    23 },executorService);
    24
    25
    26 CompletableFuture future3 =
    CompletableFuture.supplyAsync(() -> {
    27 try {
    28 TimeUnit.SECONDS.sleep(5);
    29 } catch (InterruptedException e)
    {
    30 e.printStackTrace();
    31 }
    32 int f3 = new
    Random().nextInt(100);
    33 System.out.println(“f3 value:”
  • f3);
    34 return f3;
    35 },executorService);
    36
    37
    38 List<CompletableFuture>
    list = new ArrayList<>();
    39 list.add(future1);
    40 list.add(future2);
    41 list.add(future3);
    42
    43
    44 CompletableFuture future =
    CompletableFuture.anyOf(list.toArray(new
    CompletableFuture[]{}));
    45
    46
    47 future.thenRunAsync(()->{
    48 try {
    49 System.out.println(“有一个任
    务执行完了,其值为:”+future.get());
    50 } catch (InterruptedException e)
    {
    51 e.printStackTrace();
    52 } catch (ExecutionException e) {
    53 e.printStackTrace();
    54 }
    55 });
    56
    57 executorService.shutdown();
    58 }
    59 }
    运行结果
    1 f1 value:84
    2 有一个任务执行完了,其值为:84
    3 f2 value:87
    4 f3 value:88
    根据结果可知,当多个异步计算,只要有一个结束了,则触
    发回调处理。
    9.7.6)小结
    1)当一个操作需要依赖与一个或多个比较耗时的操作时,可 以通过异步任务改善程序性能,加快响应速度。
    2)在功能实现时,根据当前需求,应该尽量的使用异步 API。
    3)将同步API封装到CompletableFuture,以异步形式执
    行。
    4)结合自身业务确定异步任务何时结束,是全部执行完毕还 是只要有一个首先完成就结束。
    5)CompletableFuture提供了回调操作,当任务执行完毕可 以通过回调触发后续特定任务处理。
    10)StampedLock锁
    StampedLock类是在JDK8引入的一把新锁,其是对原有 ReentrantReadWriteLock读写锁的增强,增加了一个乐观读 模式,内部提供了相关API不仅优化了读锁、写锁的访问,也 可以让读锁与写锁间可以互相转换,从而更细粒度的控制并 发。
    10.1)ReentrantReadWriteLock回顾
    读写锁适用于读多写少的场景,内部有写锁和读锁。
    读锁是一把共享锁,当一个线程持有某一个数据的读锁
    时,其他线程也可以对这条数据进行读取,但是不能写。
    写锁是一把独占锁,一个线程持有某一个数据的写锁时,
    其他线程是不可以获取到这条数据的写锁和读锁的。
    对于锁升级来说,当一个线程在没有释放读锁的情况下,
    就去申请写锁,是不支持的。
    对于锁降级来说,当一个线程在没有释放写锁的情况下,
    去申请读锁,是支持的。
    另外在使用读写锁时,还容易出现写线程饥饿的问题。主要 是因为读锁和写锁互斥。比方说:当线程 A 持有读锁读取数 据时,线程 B 要获取写锁修改数据就只能到队列里排队。此 时又来了线程 C 读取数据,那么线程 C 就可以获取到读锁, 而要执行写操作线程 B 就要等线程 C 释放读锁。由于该场景 下读操作远远大于写的操作,此时可能会有很多线程来读取 数据而获取到读锁,那么要获取写锁的线程 B 就只能一直等 待下去,最终导致饥饿。
    对于写线程饥饿问题,可以通过公平锁进行一定程度的解
    决,但是它是以牺牲系统吞吐量为代价的。
    10.2)StampedLock特点
    1)获取锁的方法,会返回一个票据(stamp),当该值为0 代表获取锁失败,其他值都代表成功。
    2)释放锁的方法,都需要传递获取锁时返回的票据,从而控 制是同一把锁。
    3)StampedLock是不可重入的,如果一个线程已经持有了
    写锁,再去获取写锁就会造成死锁。
    4)StampedLock提供了三种模式控制读写操作:写锁、悲
    观读锁、乐观读锁
    1 写锁:
    2 使用类似于ReentrantReadWriteLock,是一把独占
    锁,当一个线程获取该锁后,其他请求线程会阻塞等待。 对
    于一条数据没有线程持有写锁或悲观读锁时,才可以获取到写
    锁,获取成功后会返回一个票据,当释放写锁时,需要传递获
    取锁时得到的票据。
    1 悲观读锁:
    2 使用类似于ReentrantReadWriteLock,是一把共享
    锁,多个线程可以同时持有该锁。当一个数据没有线程获取写
    锁的情况下,多个线程可以同时获取到悲观读锁,当获取到后
    会返回一个票据,并且阻塞线程获取写锁。当释放锁时,需要
    传递获取锁时得到的票据。
    1 乐观读锁:
    2 这把锁是StampedLock新增加的。可以把它理解为是一
    个悲观锁的弱化版。当没有线程持有写锁时,可以获取乐观读
    锁,并且返回一个票据。值得注意的是,它认为在获取到乐观
    读锁后,数据不会发生修改,获取到乐观读锁后,其并不会阻
    塞写入的操作。
    3 那这样的话,它是如何保证数据一致性的呢? 乐观读锁
    在获取票据时,会将需要的数据拷贝一份,在真正读取数据
    时,会调用StampedLock中的API,验证票据是否有效。如
    果在获取到票据到使用数据这期间,有线程获取到了写锁并修
    改数据的话,则票据就会失效。 如果验证票据有效性时,当
    返回true,代表票据仍有效,数据没有被修改过,则直接读
    取原有数据。当返回flase,代表票据失效,数据被修改过,
    则重新拷贝最新数据使用。
    4 乐观读锁使用与一些很短的只读代码,它可以降低线程
    之间的锁竞争,从而提高系统吞吐量。但对于读锁获取数据结
    果必须要进行校验。
    5)在StampedLock中读锁和写锁可以相互转换,而在 ReentrantReadWriteLock中,写锁可以降级为读锁,而读锁 不能升级为写锁。
    10.3)源码解析
    10.3.1)实现原理解析
    10.3.1.1)实例化
    1)StampedLock是基于CLH自旋锁实现,锁会维护一个等待 线程链表队列,所有没有成功申请到锁的线程都以FIFO的策 略记录到队列中,队列中每个节点代表一个线程,节点保存 一个标记位,判断当前线程是否已经释放锁。
    当一个线程试图获取锁时,首先取得当前队列的尾部节点作
    为其前序节点,并判断前序节点是否已经释放锁,如果前序
    节点没有释放锁,则当前线程还不能执行,进入自旋等待。
    如果前序节点已经释放锁,则当前线程执行。
    2)首先需要先了解一些StampedLock类的常量值,方便与 后面源码的理解。
    另外还有两个很重要的属性:state、readerOverFlow
    1 state:
    2 当前锁的状态,是由写锁占用还是由读锁占用。其中
    long的倒数第八位是1,则表示由写锁占用(0000
    0001),前七位由读锁占用(1-126)。
    3 readerOverFlow:
    4 当读锁的数量超过了范围,通过该值进行记录。
    3)当实例化StampedLock时,会设置节点状态值为 ORIGIN(0000 0000)。
    10.3.1.2)获取锁过程分析
    假设现在有四个线程:ThreadA获取写锁、ThreadB获取读 锁、ThreadC获取读锁、ThreadD获取写锁。
    1)ThreadA获取写锁
    该方法用于获取写锁,如果当前读锁和写锁都未被使用的 话,则获取成功并更新state,返回一个long值,代表当前写 锁的票据,如果获取失败,则调用acquireWrite()将写锁放入 等待队列中。
    因为当前还没有任务线程获取到锁,所以ThreadA获取写锁 成功。
    2)ThreadB获取读锁
    该方法用于获取读锁,如果写锁未被占用,则获取成功,返 回一个long值,并更新state,如果有写锁存在,则调用 acquireRead(),将当前线程包装成一个WNODE放入等待队 列,线程会被阻塞。
    因为现在ThreadA已经获取到了写锁并且没有释放,所以 ThreadB在获取读锁时,一定会阻塞,被包装成WNode进入 等待队列中。
    在acquireRead()内部会进行两次for循环进行自旋尝试获取 锁,每次for循环次数由CPU核数决定,进入到该方法后,首 先第一次自旋会尝试获取读锁,获取成功,则直接返回。否 则,ThreadB会初始化等待队列,并创建一个WNode,作为 队头放入等待队列,其内部模式为写模式,线程对象为null, status为0【初始化】。同时还会将当前线程ThreadB包装为 WNode放入等待队列的队尾中,其内部模式为读模式, thread为当前ThreadB对象,status为0。
    当进入到第二次自旋后,还是先尝试获取读锁,如果仍没有
    获取到,则将前驱节点的状态设置为-1【WAITING】,用于 代表当前ThreadB已经进入等待阻塞。
    3)ThreadC获取读锁
    ThreadC在获取读锁时,其过程与ThreadB类似,因为 ThreadA的写锁没有释放,ThreadC也会进入等待队列。但 与ThreadB不同的是,ThreadC不会占用等待队列中的一个 新节点,因为其前面的ThreadB也是一个读节点,它会赋值 给用于表达ThreadB的WNode中的cowait属性,实际上构成 一个栈。
    4)ThreadD获取写锁
    由于ThreadA的写锁仍然没有释放,当ThreadD调用 writeLock()获取写锁时,内部会调用acquireWrite()
    acquireWrite()内部的逻辑和acquireRead()类似,也会进
    行两次自旋。第一次自旋会先尝试获取写锁,获取成功则直
    接返回,获取失败,则会将当前线程TheadD包装成WNode 放入等待队列并移动队尾指针,内部属性模式为写模式, thread为ThreadD对象,status=0【初始化】。
    当进入到第二次自旋,仍然会尝试获取写锁,如果获取不
    到,会修改其前驱节点状态为-1【等待】,并阻塞当前线
    程。
    10.3.1.3)释放锁过程分析 1)ThreadA释放写锁
    当要释放写锁时,需要调用unlockWrite(),其内部首先会 判断,传入的票据与获取锁时得到的票据是否相同,不同的 话,则抛出异常。如果相同先修改state,接着调用 release(),唤醒等待队列中的队首节点【即头结点whead的 后继节点】
    在release()中,它会先将头结点whead的状态修改从-1变为 0,代表要唤醒其后继节点,接着会判断头结点whead的后继 节点是否为null或者其后继节点的状态是否为1【取消】。 如 果不是,则直接调用unpark()唤醒队首节点,如果是的话, 再从队尾开始查找距离头结点最近的状态<=0【WAITING或 初始化】的节点。
    当ThreadB被唤醒后,它会从cowait中唤醒栈中的所有线
    程,因为读锁是一把共享锁,允许多线程同时占有。
    当所有的读锁都被唤醒后,头结点指针会后移,指向
    ThreadB这个WNode,并将原有的头结点移出等待队列
    此时ThreadC已经成为了孤立节点,最终会被GC。最终队列
    结构:
    2)ThreadB和ThreadC释放读锁
    读锁释放需要调用unlockRead(),其内部先判断票据是否正 确,接着会对读锁数量进行扣减,当读锁数量为0,会调用 release()唤醒队首节点
    其内部同样会先将头结点状态从-1该为0,标识要唤醒后继节

    当ThreadD被唤醒获取到写锁后,头结点指针会后移指向 ThreadD,并原有头部节点移出队列。
    10.3.2)乐观读锁解析
    在ReentrantReadWriteLock中,只有写锁和读锁的概念,但
    是在读多写少的环境下,容易出现写线程饥饿问题,虽然能
    够通过公平锁解决,但会造成系统吞吐量降低。
    乐观读锁只需要获取,不需要释放。在获取时,只要没有线
    程获取写锁,则可以获取到乐观读锁,同时将共享数据储存
    到局部变量中。同时在获取到乐观读锁后,并不会阻塞其他
    线程对共享数据进行修改。
    因为就会造成当使用共享数据时,出现数据不一致的问题。
    因为在使用乐观读锁时,要反复的对数据进行校验。
    10.4)使用示例
    此处引用Oracle官方案例。https://docs.oracle.com/javase/ 8/docs/api/java/util/concurrent/locks/StampedLock.html
    1 class Point {
    2
    3 //定义共享数据
    4 private double x, y;
    5
    6 //实例化锁
    7 private final StampedLock sl = new
    StampedLock();
    8
    9 //写锁案例
    10 void move(double deltaX, double deltaY)
    {
    11
    12 //获取写锁
    13 long stamp = sl.writeLock();
    14
    15 try {
    16 x += deltaX;
    17 y += deltaY;
    18 } finally {
    19 //释放写锁
    20 sl.unlockWrite(stamp);
    21 }
    22 }
    23
    24 //使用乐观读锁案例
    25 double distanceFromOrigin() {
    26
    27 long stamp = sl.tryOptimisticRead();
    //获得一个乐观读锁
    28
    29 double currentX = x, currentY = y;
    //将两个字段读入本地局部变量
    30
    31 if (!sl.validate(stamp)) { //检查发出
    乐观读锁后同时是否有其他写锁发生?
    32
    33 stamp = sl.readLock(); //如果有,
    我们再次获得一个读悲观锁
    34 try {
    35 currentX = x; // 将两个字段读
    入本地局部变量
    36 currentY = y; // 将两个字段读
    入本地局部变量
    37 } finally {
    38 sl.unlockRead(stamp);
    39 }
    40 }
    41 return Math.sqrt(currentX * currentX
  • currentY * currentY);
    42 }
    43
    44 //使用悲观读锁并锁升级案例
    45 void moveIfAtOrigin(double newX, double
    newY) {
    46
    47 // 获取悲观读锁
    48 long stamp = sl.readLock();
    49
    50 try {
    51 while (x == 0.0 && y == 0.0) {
    //循环,检查当前状态是否符合
    52
    53 //锁升级,将读锁转为写锁
    54 long ws =
    sl.tryConvertToWriteLock(stamp);
    55
    56 //确认转为写锁是否成功
    57 if (ws != 0L) {
    58 stamp = ws; //如果成功 替
    换票据
    59 x = newX; //进行状态改变
    60 y = newY; //进行状态改变
    61 break;
    62 }
    63 else { //如果不成功
    64 sl.unlockRead(stamp); //
    显式释放读锁
    65 stamp = sl.writeLock();
    //显式直接进行写锁 然后再通过循环再试
    66 }
    67 }
    68 } finally {
    69 //释放读锁或写锁
    70 sl.unlock(stamp);
    71 }
    72 }
    73 }
    11)Optional
    在日常开发中,NullPointerException相信所有人都见过,
    不管你是刚入行的萌新还是骨灰级玩家,对于它都是耳熟能
    详的。它的出现可以说无处不在,总是能在各种场景下出
    现。那么对于如何防止它的出现,我们平时都是被动的采用
    各种非空校验,但是它还是经常能出现在我们的视线中。
    1 public String getCompanyName(Student
    student){
    2
    3 if (student != null){
    4 Job job = student.getJob();
    5
    6 if (job != null){
    7 Company company =
    job.getCompany();
    8
    9 if (company != null){
    10 String name =
    company.getName();
    11
    12 return name;
    13 }else {
    14 return “no company”;
    15 }
    16 }else {
    17 return “no job”;
    18 }
    19 }else {
    20 return “no student”;
    21 }
    22
    23 }
    对于上述这段代码,相信大家平时工作类似的代码经常会有
    出现。每一次在获取到一个对象时都进行一个null的判断,然
    后才继续后续的实现。但是这种方式很不好,首先会存在大
    量的if-else判断嵌套,导致代码的可读性和扩展性极差。
    此时,有的同学可能就会这么改造,如下所示:
    1 public String getCompanyName(Student
    student){
    2
    3 if (student == null){
    4 return “no student”;
    5 }
    6
    7 Job job = student.getJob();
    8 if (job == null){
    9 return “no job”;
    10 }
    11
    12 Company company = job.getCompany();
    13 if (company == null){
    14 return “no company”;
    15 }
    16
    17 return company.getName();
    18 }
    这种判断已经有意识的避免了大量嵌套判断,但是同样存在
    多个不同的判断点,代码维护同样困难。
    那么有没有一种方式可以优雅的解决这些问题呢?
    11.1)简介
    为了防止空指针异常的出现,Java8中引入了一个新类 Optional,对于它之前我们已经进行了简单的实现。其本质 就是通过Optional类对值进行封装, 当有值的时候,会把该 值封装到Optional类中。如果没有值的话,则会在该类封装 一个Empty
    11.2)应用
    在Optional类中基于函数式接口提供了一些用于操作值的方 法。
    11.2.1)创建Optional对象
    要创建Optional,该类提供了三种方法操作,分别为: empty()、of()、ofNullable()。使用方式如下所示:
    1 Optional studentOptional =
    Optional.empty();
    2
    3 Optional studentOptional =
    Optional.of(student);
    4
    5 Optional studentOptional =
    Optional.ofNullable(student);
    可以看到这三个方法,都会返回Optional对象,那么三者之
    间有什么区别呢?根据源码分析如下:
    根据源码可知,empty()会直接返回一个空的Optional实例, 内部不会存在任何值。
    根据源码可知,of()会返回一个存在值的Optional对象,并且 该值不允许null的存在。如果调用该方法时传入参数是null, 则立刻抛出NullPointerException,而不是等到你用这个对 象时才抛出,相当于进行了立即检查。
    根据源码可知,ofNullable()同样也会返回一个存在值的 Optional对象,但是它和of()最大的不同在于,它会对传入的 值进行判断,如果传入的值为null,其会调用empty()返回一 个不包含内容的Optional,如果不为null,则会调用of()返回 一个包含内容的Optional
    11.2.2)基于Optional对象获取值
    有了Optional对象之后,就需求获取其内部的值了, Optional类也提供了多种方法用于值的获取。
    11.2.2.1)isPresent()与ifPresent()应用&源码解析
    Optional类中提供了两个方法用于判断Optional是否有值, 分别是isPresent()和ifPresent(Consumer<? super T> consumer)。其一般与ofNullable()搭配使用,因为of()在创 建时已经完成了判断,而empty()只是单纯了实例化了一个 Optional对象。
    根据源码可知,isPresent()内部非常简单,就是判断这个值 是否为null。
    根据源码可知,该方法在执行时,接收一个consumer函数式 接口,如果value不为null,则通过consumer中的accept方 法获取该值。
    使用示例如下:
    1 public class PresentDemo {
    2
    3 public static void
    getStudentName(Student student){
    4
    5 Optional studentOptional =
    Optional.ofNullable(student);
    6
    7 if (studentOptional.isPresent()){
    8 //存在
    9 System.out.println(“student存
    在”);
    10 }else {
    11 System.out.println(“student不存
    在”);
    12 }
    13 }
    14
    15 public static void main(String[] args) {
    16
    17 Student student = new
    Student(1,“zhangsan”,“M”);
    18 getStudentName(student);
    19 }
    20 }
    1 Optional studentOptional =
    Optional.ofNullable(student);
    2
    3 studentOptional.ifPresent(s->
    System.out.println(“学生存在”));
    11.2.2.2) get()应用&源码解析
    get()的使用非常简单,但不安全,因为其在获取值的时候, 如果值存在,则直接返回封装在Optional中的值,如果不存 在,则抛出NoSuchElementException。因此它的使用前提 是已经确定Optional中有值,否则没有使用意义。
    使用示例如下:
    1 Optional studentOptional =
    Optional.ofNullable(student);
    2
    3 if (studentOptional.isPresent()){
    4 Student result = studentOptional.get();
    5 }
    11.2.2.3)orElseThrow()应用&源码解析
    该方法与get()类似,都是用于取值,但是当Optional中没有 值时,get()会直接抛出NoSuchElementException,这样的 话,就存在了一定的局限性,因为有时可能需要抛出自定义
    异常。此时就可以使用orElseThrow(),它在取值时,如果 Optional中没有值时,可以抛出自定义异常。
    1 public class MyException extends Throwable {
    2
    3 public MyException() {
    4 super();
    5 }
    6
    7 public MyException(String message) {
    8 super(message);
    9 }
    10
    11 @Override
    12 public String getMessage() {
    13 return “exception message”;
    14 }
    15 }
    1 public class OrElseThrowDemo {
    2
    3 public static void
    getStudentInfo(Student student) {
    4
    5 Optional studentOptional =
    Optional.ofNullable(student);
    6
    7 try {
    8 Student student1 =
    studentOptional.orElseThrow(MyException::new
    );
    9 } catch (MyException e) {
    10 e.printStackTrace();
    11 }
    12 }
    13
    14
    15 public static void main(String[] args) {
    16
    17 Student student = null;
    18
    19 getStudentInfo(student);
    20 }
    21 }
    11.2.2.4)map()应用&源码解析
    当Option中有值的话,经常性的一个操作就是从值中获取它 的某一个属性,实例如下:
    1 if(job != null){
    2 String name = job.getName();
    3 }
    对于这种需求,可以通过map()完成,它的使用思路与 Stream中的map类似,只不过一个是转换Stream的泛型, 一个是转换Optional的泛型。
    使用示例如下:
    1 if (studentOptional.isPresent()){
    2
    3 Optional nameOptional =
    studentOptional.map(Student::getName);
    4
    5 }
    11.2.2.5)flatMap()应用&源码解析
    刚才已经通过map()获取了学生的姓名,操作非常简单。但是 当产生链路获取时,map可以使用么? 如:学生->工作->公 司->公司名称。
    现在可能大家脑袋里已经有了一个想法,就是通过map(),代
    码结构如下:
    1 studentOptional.map(Student::getJob).map(Job:
    :getCompany).map(Company::getName);
    但是这段代码是无法通过编译的。因为根据map的学习,每 一次在调用的时候,都会对Optional的泛型进行改变,最终 产生多层Optional嵌套的结构。如下图所示:
    对于这个问题的解决,Optional类中提供了另外一个获取值 的方法flatMap()。它本身用于多层调用,同时对于结果它不 会形成多个Optional,而是将结果处理成最终的一个类型的 Optional。但是通过flatMap获取的返回值必须是Optional类 型。而map则没有这个限制。
    使用示例如下:
    1 Optional nameOptional =
    studentOptional.flatMap(Student::getJob).flat
    Map(Job::getCompany).map(Company::getName);
    11.2.2.6)filter()应用&源码解析
    在获取某个对象中的属性值时,经常会根据特定的条件进行
    获取。之前的编码方法通常为:
    1 Company company = optional().get();
    2 if(“itheima”.equals(company.getName)){
    3 sout(company);
    4 }
    Optional类中也提供了数据过滤的方法filter()来实现这个需 求。其会根据传入的条件进行判断,如果匹配则返回一个 Optional对象并包含对应的值,否则返回一个空值的 Optional
    使用示例如下:
    1 Optional company =
    companyOptional.filter(c ->
    “itheima”.equals(c.getName()));
    11.2.2.7)orElse()应用&源码解析
    在取值的时候,如果值不存在,有时我们会考虑返回一个默
    认值。该需求就可以通过orElse()实现。
    其内部会判断值是否为null,如果不为null,则返回该值,如 果为null,则返回传入的默认值。
    使用示例如下:
    1 public class Demo1 {
    2
    3 public static void
    getCompanyName(Student student) {
    4
    5 Optional studentOptional =
    Optional.ofNullable(student);
    6
    7 if (studentOptional.isPresent()) {
    8
    9 String value =
    studentOptional.flatMap(Student::getJob).fla
    tMap(Job::getCompany).map(Company::getName).
    orElse(“default value”);
    10
    11 System.out.println(value);
    12 }
    13 }
    14
    15
    16 public static void main(String[] args) {
    17
    18 Company company = new Company();
    19 //company.setName(“itheima”);
    20 Optional companyOptional =
    Optional.of(company);
    21
    22 Job job = new Job();
    23 job.setName(“pm”);
    24 job.setCompany(companyOptional);
    25 Optional jobOptional =
    Optional.of(job);
    26
    27 Student s1 = new Student();
    28 s1.setName(“张三”);
    29 s1.setJob(jobOptional);
    30
    31
    32 getCompanyName(s1);
    33
    34
    35 }
    36 }
    11.2.2.8)orElseGet()应用&源码解析
    orElseGet()也是用于当Optional中没有值时,返回默认值的 方法。但是它与orElse()的区别在于,它是延迟加载的。只有 当Optional中没有值是才会被调用。
    区别示例如下:
    1)当公司名称不存在
    1 public class Demo1 {
    2
    3 public static void
    getCompanyName(Student student) {
    4
    5 Optional studentOptional =
    Optional.ofNullable(student);
    6
    7 if (studentOptional.isPresent()) {
    8
    9 String value1 =
    studentOptional.flatMap(Student::getJob).fla
    tMap(Job::getCompany).map(Company::getName).
    orElse(get(“a”));
    10
    11 String value2 =
    studentOptional.flatMap(Student::getJob).fla
    tMap(Job::getCompany).map(Company::getName).
    orElseGet(()->get(“b”));
    12 System.out.println("a:
    "+value1);
    13 System.out.println("b:
    "+value2);
    14
    15 }
    16 }
    17
    18 public static String get(String name) {
    19 System.out.println(name + “执行了方
    法”);
    20 return “exec”;
    21 }
    22
    23
    24 public static void main(String[] args) {
    25
    26 Company company = new Company();
    27 //company.setName(“itheima”);
    28 Optional companyOptional =
    Optional.of(company);
    29
    30 Job job = new Job();
    31 job.setName(“pm”);
    32 job.setCompany(companyOptional);
    33 Optional jobOptional =
    Optional.of(job);
    34
    35 Student s1 = new Student();
    36 s1.setName(“张三”);
    37 s1.setJob(jobOptional);
    38
    39
    40 getCompanyName(s1);
    41
    42
    43 }
    44 }
    执行结果
    1 a执行了方法
    2 b执行了方法
    3 a: exec
    4 b: exec
    根据上述结果可知,当公司名称不存在时,orElse()与 orElseGet()都被执行了。
    2)公司名称存在
    1 public class Demo1 {
    2
    3 public static void
    getCompanyName(Student student) {
    4
    5 Optional studentOptional =
    Optional.ofNullable(student);
    6
    7 if (studentOptional.isPresent()) {
    8
    9 String value1 =
    studentOptional.flatMap(Student::getJob).fla
    tMap(Job::getCompany).map(Company::getName).
    orElse(get(“a”));
    10
    11 String value2 =
    studentOptional.flatMap(Student::getJob).fla
    tMap(Job::getCompany).map(Company::getName).
    orElseGet(()->get(“b”));
    12 System.out.println(“a:
    “+value1);
    13 System.out.println(“b:
    “+value2);
    14
    15 }
    16 }
    17
    18 public static String get(String name) {
    19 System.out.println(name + “执行了方
    法”);
    20 return “exec”;
    21 }
    22
    23
    24 public static void main(String[] args) {
    25
    26 Company company = new Company();
    27 company.setName(“itheima”);
    28 Optional companyOptional =
    Optional.of(company);
    29
    30 Job job = new Job();
    31 job.setName(“pm”);
    32 job.setCompany(companyOptional);
    33 Optional jobOptional =
    Optional.of(job);
    34
    35 Student s1 = new Student();
    36 s1.setName(“张三”);
    37 s1.setJob(jobOptional);
    38
    39
    40 getCompanyName(s1);
    41
    42
    43 }
    44 }
    执行结果如下:
    1 a执行了方法
    2 a: itheima
    3 b: itheima
    根据上述结果可知,当公司名称存在时,orElseGet()不会被 执行,而orElse()会被执行。因此可知,只有当Optional值为 不存在时,orElseGet()才会被执行。
    在使用时,更加推荐使用orElseGet(),因为它使用延迟调用
    所以性能更加优异。
    12)日期时间新方式
    在日常开发中,对于日期操作是非常常见的,但是对于有经 验的开发人员来说Java8之前的日期操作是有较大问题的。比 方说SimpleDateFormat。但是在Java8之后提出了 DateTimeFormatter用于解决之前的问题。
    12.1)SimpleDateFormat的那些坑
    SimpleDateFormat本身是线程不安全的,在多线程环境下, 如果多个线程使用同一个类解析日期,最后的结果是无法预 期的。同时继承了它的DateFormat类也不是线程安全的。
    12.1.1)效果演示
    在单线程下
    1 public class SimpleDateFormatDemo {
    2
    3 private static final SimpleDateFormat
    simpleDateFormat = new
    SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”);
    4
    5 public static String format(Date date){
    6 return
    simpleDateFormat.format(date);
    7 }
    8
    9 public static Date parse(String date){
    10 try {
    11 return
    simpleDateFormat.parse(date);
    12 } catch (ParseException e) {
    13 e.printStackTrace();
    14 }
    15 return null;
    16 }
    17
    18 public static void main(String[] args) {
    19
    20 Date date = new Date();
    21 String format = format(date);
    22 Date parse = parse(format);
    23
    24 System.out.println(format);
    25 System.out.println(parse);
    26 }
    27 }
    输出的结果是没有任务问题的
    1 2020-05-12 11:45:11
    2 Tue May 12 11:45:11 CST 2020
    但是当在多线程下时,问题就会出现了。
    1 public class SimpleDateFormatDemo {
    2
    3 private static final SimpleDateFormat
    simpleDateFormat = new
    SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”);
    4
    5 public static String format(Date date){
    6 return
    simpleDateFormat.format(date);
    7 }
    8
    9 public static Date parse(String date)
    throws ParseException {
    10
    11 return simpleDateFormat.parse(date);
    12
    13 }
    14
    15 public static void main(String[] args) {
    16
    17 ExecutorService executorService =
    Executors.newFixedThreadPool(100);
    18 Date date = new Date();
    19 for(int i=0;i<30;i++){
    20 executorService.execute(()->{
    21 for(int j=0;j<10;j++){
    22 String format =
    format(date);
    23 Date parse = null;
    24 try {
    25 parse =
    parse(format);
    26 } catch (ParseException
    e) {
    27 e.printStackTrace();
    28 }
    29
    System.out.println(parse);
    30 }
    31 });
    32 }
    33
    34 executorService.shutdown();
    35 }
    36 }
    根据返回结果,可以发现有多线程执行时,要么结果值不 对,要么转换出现问题。
    对于问题的出现,是因为将SimpleDateFormat定义为 static,所以在多线程下它的实例会被多线程共享,线程之间 相互读取时间,所以才出现时间差异和其他的那些异常。
    在Java8之前的解决方案,对于SimpleDateFormat不会通过 static进行修饰,而是在使用时每次都新创建一个实例,但是 这种方式会造成频繁的垃圾回收。或者使用Synchronized加 锁,但是会造成线程阻塞。或者把它放入到ThreadLocal 中。但是这些方式用起来都感觉有一些复杂。
    12.2)DateTimeFormatter
    Java8对于日期时间操作提供了一些新类供我们进行使用,现 在可以通过DateTimeFormatter来替换掉 SimpleDateFormat。
    首先根据源码介绍,该类是不可变和线程的。该类中提供了
    很多方法用于替换SimpleDateFormat。
    基于DateTimeFormatter改造
    1 public class DateTimeFormatterDemo {
    2
    3 private static final DateTimeFormatter
    dateTimeFormatter =
    DateTimeFormatter.ofPattern(“yyyy-MM-dd
    HH:mm:ss”);
    4
    5 public static String
    format(LocalDateTime date){
    6 return
    dateTimeFormatter.format(date);
    7 }
    8
    9 public static LocalDateTime parse(String
    date) {
    10 return
    LocalDateTime.parse(date,dateTimeFormatter);
    11 }
    12
    13 public static void main(String[] args) {
    14
    15 ExecutorService executorService =
    Executors.newFixedThreadPool(100);
    16
    17 for(int i=0;i<30;i++){
    18
    19 executorService.execute(()->{
    20
    21 for(int j=0;j<10;j++){
    22
    23 String format =
    format(LocalDateTime.now());
    24
    25 LocalDateTime parse =
    parse(format);
    26
    System.out.println(parse);
    27 }
    28 });
    29 }
    30
    31 executorService.shutdown();
    32 }
    33 }
    12.3)LocalDate、LocalTime、 LocalDateTime介绍
    12.3.1)LocalDate介绍
    根据源码可知,该类是一个不可变,线程安全的类。其内部
    了提供了若干用于操作日期的方法
    12.3.2)LocalTime介绍
    LocalTime类可以用来操作时间
    12.3.3)LocalDateTime介绍
    LocalDateTime类可以用来操作日期+时间。
    13)JDK11新特性
    到此截止,对于JDK8的一些重要更新内容已经介绍完了,现 在Java已经更新到14的版本,但是长期支持版本只有8和11。 那这里再来简单介绍下JDK11都做了应用层面的哪些更新
    1)变量类型推断
    在JS中,不管类型是什么,我们都是使用var来进行变量声明 的。但是之前对于Java的使用,都会提到它是强类型语言, 变量声明需要定义特定类型。但是在JDK11中对JDK8的类型 推断思路又得以延伸,使用JS的方式,通过var定义局部变 量,它会根据右边的表达式推断变量类型
    1 var text = “hello itheima”;
    2 sout(text);
    2)新增字符串方法
    3)新增创建集合的方式
    JDK11提供了通过of()和copyOf()创建集合的方式,但是创建 的集合长度不可变,不能进行任何修改操作,of()会直接创 建,而copyOf()首先会判断传入的集合是否为不可变集合, 是的话直接返回,不是的话,调用of()创建新集合并返回。
    1 var list = List.of(“hello”,“itheima”);
    2 var arrayList = new ArrayList();
    3 var list2 = List.copyOf(arrayList);
    4)Files类增强
    在Files类中新增两个方法:writeString和readString。可以 把String内容写入文件或者把整个文件以String读出
    1 Files.writeString(
    2 Path.of(”./”, “demo.txt”),
    3 “hello,itheima”,
    4 “utf-8”);
    5 String info = Files.readString(
    6 Paths.get(”./demo.txt”),
    7 “utf-8”);
    5)HTTP Client Api
    平时我们要去访问HTTP资源,大多数时间我们都是通过第三 方完成的,虽然在JDK标准类库中有一个 HttpURLConnection,但是也不太好用。
    在JDK9就提出了HTTP Client Api,经过9,10两个版本的改 进,在11中正式发布。其同时支持同步请求和异步请求。
    1 var request = HttpRequest.newBuilder()
    2 .uri(URI.create(“http://www.itcast.cn”))
    3 .POST()
    4 .build();
    5 var client = HttpClient.newHttpClient();
    6 // 同步
    7 HttpResponse response =
    client.send(request,
    HttpResponse.BodyHandlers.ofString());
    8 System.out.println(response.body());
    9 // 异步
    10 client.sendAsync(request,
    HttpResponse.BodyHandlers.ofString())
    11 .thenApply(HttpResponse::body)
    12 .thenAccept(System.out::println);
    6)更方便的编译运行
    1 #之前
    2 javac Demo.java
    3 java Demo
    4
    5 #现在
    6 java Demo.java
    14)理解拷贝算法
    在日常开发中,拷贝对象的操作是非常常见的,主要是为了
    在新的上下文中复用现有对象中的数据。但是稍加不注意,
    就会出现BUG,比如说:当把一个对象的所有成员变量拷贝 到另一个对象上,然后对它进行操作,但是,大家如果对于 拷贝操作不是很理解的话,经常会出现一个问题就是,当将 一个对象中的成员变量进行修改之后,发现另一个对象中的 成员变量也被修改了。从而造成BUG问题的出现。
    14.1)浅拷贝
    14.1.1)介绍
    浅拷贝会创建一个新对象,新对象属性值有着原对象属性值
    的准确拷贝。
    当属性是基本类型,则直接进行值传递,是两份不同的内
    容,当对其中一个修改,不会影响另外一个。
    当属性是引用类型,则拷贝的是该引用类型的内存地址。
    所以就会出现开篇说到的问题,当其中一个对象改变了内
    存地址,则另外一个对象也会发生改变,因为他们两个共
    用同一个内存地址。
    14.1.2)实现
    1 public class Person {
    2
    3 private String name;
    4
    5 private int age;
    6
    7 public String getName() {
    8 return name;
    9 }
    10
    11 public void setName(String name) {
    12 this.name = name;
    13 }
    14
    15 public int getAge() {
    16 return age;

10 return name;
11 }
12
13 public void setName(String name) {
14 this.name = name;
15 }
16
17 public int getAge() {
18 return age;
19 }
20
21 public void setAge(int age) {
22 this.age = age;
23 }
24
25 public Person getPerson() {
26 return person;
27 }
28
29 public void setPerson(Person person) {
30 this.person = person;
31 }
32
33
34
35
36
37
38
39
40
41
42 @Override
public String toString() {
return “Student{” +
this.hashCode()+
“name=’” + name + ‘’’ +
“, age=” + age +
“, person=” + person +
‘}’;
}

43 @Override
44 protected Object clone(){
45 try {
46 return super.clone();
47 } catch (CloneNotSupportedException
e) {
48 e.printStackTrace();
49 return null;
50 }
51 }
52 }
1 public class ShallowCopyDemo {
2
3 public static void main(String[] args) {
4
5 Person person = new
Person(“zhangsan”,18);
6
7 Student s1 = new Student();
8 s1.setPerson(person);
9 s1.setName(“lisi”);
10 s1.setAge(19);
11
12 //拷贝
13 Student s2 = (Student) s1.clone();
14 s2.setPerson(person);
15 s2.setName(“wangwu”);
16 s2.setAge(20);
17
18 Person person1 = s2.getPerson();
19 person1.setName(“zhaoliu”);
20
21
System.out.println(“s1:”+s1.toString());
22
System.out.println(“s2:”+s2.toString());
23
24 }
25 }
1 s1:Student{21685669name=‘lisi’, age=19,
person=Person{2133927002name=‘zhaoliu’,
age=18}}
2
3 s2:Student{1836019240name=‘wangwu’, age=20,
person=Person{2133927002name=‘zhaoliu’,
age=18}}
根据运行结果,从s1拷贝出来的s2,两者是不同的两个对 象,当对两者基本类型修改时,互不影响,相互独立。但对 引用类型修改时,修改了一个,则另外一个也会发生修改。
14.2)深拷贝
14.2.1)介绍
通过浅拷贝的演示,可以发现,其天然的缺陷就是存在数据
安全问题,当修改s2中的person时,s1中的person也被修改 了,因为他们两个都指向同一个内存地址。对于这个问题, 可以使用深拷贝进行解决。
当属性是基本类型时,其效果与浅拷贝一样。
当属性是引用类型时,会为其单独开辟一块内存空间,让
两者存在于不同的内容空间,从而实现相互独立。
14.2.2)实现
14.2.2.1)基于Cloneable实现
引用类型的类上需要实现Cloneable接口,并重写clone()
1 public class Person implements Cloneable{
2
3 private String name;
4
5 private int age;
6
7 public String getName() {
8 return name;
9 }
10
11 public void setName(String name) {
12 this.name = name;
13 }
14
15 public int getAge() {
16 return age;
17 }

在拷贝对象上,重写clone()方法,拿到拷贝后产生的新对 象,然后对新对象中的引用类型通过clone()重新赋值,从而 实现对引用类型深拷贝
1 public class Student implements Cloneable{
2
3 private String name;
4
5 private int age;
6
7 private Person person;
8
9 public String getName() {
10 return name;
11 }
12
13 public void setName(String name) {
14 this.name = name;
15 }
16
17 public int getAge() {
18 return age;
19 }
20
21 public void setAge(int age) {
22
23
24
25
26
27
28
29 this.age = age;
}
}

public Person getPerson() {
return person;
public void setPerson(Person person) {

30 this.person = person;
31 }
32
33 @Override
34 public String toString() {
35 return “Student{” +
36 this.hashCode()+
37 “name=’” + name + ‘’’ +
38 “, age=” + age +
39 “, person=” + person +
40 ‘}’;
41 }
42
43 @Override
44 protected Object clone(){
45 try {
46 Student student = (Student)
super.clone();
47 student.person = (Person)
person.clone();
48 return student;
49 } catch (CloneNotSupportedException
e) {
50 e.printStackTrace();
51 return null;
52 }
53 }
54 }
1 public class DeepCopy {
2
3 public static void main(String[] args) {
4
5 Person person = new
Person(“zhangsan”,18);
6
7 Student s1 = new Student();
8 s1.setPerson(person);
9 s1.setName(“lisi”);
10 s1.setAge(19);
11
12 //拷贝
13 Student s2 = (Student) s1.clone();
14 //Student s2 = s1;
15 s2.setName(“wangwu”);
16 s2.setAge(20);
17
18 Person person1 = s2.getPerson();
19 person1.setName(“zhaoliu”);
20
21
System.out.println(“s1:”+s1.toString());
22
System.out.println(“s2:”+s2.toString());
23 }
24 }
运行结果
1 s1:Student{21685669name=‘lisi’, age=19,
person=Person{2133927002name=‘zhangsan’,
age=18}}
2
3 s2:Student{1836019240name=‘wangwu’, age=20,
person=Person{325040804name=‘zhaoliu’,
age=18}}
此时可以发现,引用类型没有因为一个改变而影响另一个,
因为两个现在是不同的两个对象。
15.2.2.2)基于序列化实现
通过clone()的方式虽然能够实现,但是问题就是,如果是多 级嵌套对象的话,通过这种方式实现来就过于繁琐了。每一 个引用类型都是去实现Cloneable接口重写clone()方法。
现在也可以通过对象序列化与反序列化方式来实现深拷贝
1 public class Person{
2
3 private String name;
4
5 private int age;
6
7 public String getName() {
8 return name;
9 }
10
11 public void setName(String name) {
12 this.name = name;
13 }
14
15 public int getAge() {
16 return age;
17 }
18
19 public void setAge(int age) {
20 this.age = age;
21 }
22
23 public Person() {

24 }
25
26 public Person(String name, int age) {
27 this.name = name;
28 this.age = age;
29 }
30
31 @Override
32 public String toString() {
33 return “Person{” +
34 this.hashCode()+
35 “name=’” + name + ‘’’ +
36 “, age=” + age +
37 ‘}’;
38 }
39 }
改造拷贝类,添加构造方法,让引用类型作为构造方法的参
数使用
1 public class Student implements Serializable
{
2
3 private String name;
4
5 private int age;
6
7 private Person person;
8
9 public String getName() {
10 return name;
11 }
12
13 public void setName(String name) {
14 this.name = name;
15 }
16
17 public int getAge() {
18 return age;
19 }
20
21 public void setAge(int age) {
22 this.age = age;
23 }
24
25 public Person getPerson() {
26 return person;
27 }
28
29 public void setPerson(Person person) {
30 this.person = person;
31 }
32
33 @Override
34 public String toString() {
35
36
37
38
39
40
41
42
43
44
45 return “Student{” +
this.hashCode()+
“name=’” + name + ‘’’ +
“, age=” + age +
“, person=” + person +
‘}’;
}
}

public Student() {

46 public Student(String name, int age,
Person person) {
47 this.name = name;
48 this.age = age;
49 this.person = person;
50 }
51 }
1 public class DeepCopy {
2
3 public static void main(String[] args)
throws IOException, ClassNotFoundException {
4
5 Person person = new
Person(“zhangsan”,18);
6
7 Student s1 = new
Student(“lisi”,19,person);
8
9 ByteArrayOutputStream baos = new
ByteArrayOutputStream();
10 ObjectOutputStream oos = new
ObjectOutputStream(baos);
11 oos.writeObject(s1);
12 oos.flush();
13
14 ObjectInputStream ois = new
ObjectInputStream(new
ByteArrayInputStream(baos.toByteArray()));
15
16 Student s2 = (Student)
ois.readObject();
17 s2.setName(“wangwu”);
18 s2.setAge(20);
19 Person person1 = s2.getPerson();
20 person1.setName(“zhaoliu”);
21
22
System.out.println(“s1:”+s1.toString());
23
System.out.println(“s2:”+s2.toString());
24
25 }
26 }
运行结果
1 s1:Student{1265094477name=‘lisi’, age=19,
person=Person{1846274136name=‘zhangsan’,
age=18}}
2
3 s2:Student{668386784name=‘wangwu’, age=20,
person=Person{1329552164name=‘zhaoliu’,
age=18}}
根据结果可以看到,通过这种效果一样可以实现深拷贝。
14.3)小结
深拷贝与浅拷贝的差异性,主要表现在对于引用类型的操作
上,当使用浅拷贝,拷贝前与拷贝后对象的引用类型指向的
都是同一个内存地址,当修改一个,另一个也会发生改变。
而深拷贝,会让两者存在于不同的内存空间,从而实现两者
的相互独立,互不影响。
浅拷贝消耗资源较低,但会造成数据不安全。深拷贝解决的
数据安全的问题,但消耗大。
在决定使用深拷贝还是浅拷贝时,主要的思考点不在于效率
问题,而是取决于当前的业务逻辑。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值