章节目录:
一、Java8 新特性
Java 8 (又称为 jdk 1.8) 是
Java
语言开发的一个主要版本。Oracle
公司于 2014 年 3 月 18 日发布Java 8
,它支持函数式编程,新的JavaScript
引擎,新的日期API
,新的Stream API
等。
1.1 新增的主要特性
-
Lambda 表达式:
Lambda
允许把函数作为一个方法的参数(函数作为参数传递进方法中)。 -
方法引用:提供了非常有用的语法,可以直接引用已有
Java
类或对象(实例)的方法或构造器。与lambda
联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。 -
默认方法:默认方法就是一个在接口里面有了一个实现的方法。
-
新工具: 新的编译工具,如:
Nashorn
引擎jjs
、 类依赖分析器jdeps
。 -
Stream API: 新添加的
Stream API
(java.util.stream
) 把真正的函数式编程风格引入到Java
中。 -
Date Time API: 加强对日期与时间的处理。
-
Optional 类:
Optional
类已经成为Java 8
类库的一部分,用来解决空指针异常。 -
Nashorn,JavaScript 引擎:
Java 8
提供了一个新的Nashorn javascript
引擎,它允许我们在JVM
上运行特定的javascript
应用。 -
更多新特性,请参考官方文档:《What’s New in JDK 8》:https://www.oracle.com/java/technologies/javase/8-whats-new.html
二、Lambda 表达式
Lambda
是一个匿名函数,我们可以把Lambda
表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使
Java
的语言表达能力得到了提升。
2.1 从匿名类到 Lambda 的转换
- 写法比较:
@Test
public void test01() {
// 1.匿名内部类写法
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println(" hello world ");
}
};
r1.run();
// 2.Lambda表达式写法(更加简洁)
Runnable r2 = () -> System.out.println(" hello world ");
r2.run();
}
- 作为参数传递:
@Test
public void test02() {
// 1.使用匿名内部类作为方法参数传递
TreeSet<String> set1 = new TreeSet<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return Integer.compare(o1.length(), o2.length());
}
});
// 2.使用Lambda作为方法参数传递
TreeSet<String> set2 = new TreeSet<>((o1, o2) -> Integer.compare(o1.length(), o2.length()));
}
2.2 为什么要使用 Lambda ?
- 假设我们有一个需求,查询薪资大于5000的员工信息(代码示例如下):
public class LambdaSample {
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Employee {
private long id;
private String name;
private int age;
private double salary;
@Override
public String toString() {
return " id=" + id +
" ,name=" + name +
" ,age=" + age +
" ,salary=" + salary;
}
}
/**
* 员工列表。
*/
List<Employee> emps = Arrays.asList(
new Employee(1, "张三", 17, 3500),
new Employee(2, "李四", 28, 5200),
new Employee(3, "王五", 39, 6800),
new Employee(4, "赵六", 40, 7100)
);
/**
* 过滤薪资大于5000的员工。
*
* @param emps 员工列表
* @return {@link List}<{@link Employee}>
*/
public List<Employee> filterEmployeeSalary(List<Employee> emps) {
List<Employee> filterList = new ArrayList<>();
for (Employee emp : emps) {
if (emp.getSalary() > 5000) {
filterList.add(emp);
}
}
return filterList;
}
/**
* 调用方法进行测试。
*/
@Test
public void test01() {
for (Employee employee : filterEmployeeSalary(emps)) {
System.out.println(employee.toString());
// id=2 ,name=李四 ,age=28 ,salary=5200.0
// id=3 ,name=王五 ,age=39 ,salary=6800.0
// id=4 ,name=赵六 ,age=40 ,salary=7100.0
}
}
}
- 此时,我们新增了一个需求,查询年龄大于30岁的员工。(一般的思路是:会新增一个专门过滤年龄大于30岁员工的方法):
/**
* 过滤年龄大于30岁的员工。
*
* @param emps 员工列表
* @return {@link List}<{@link Employee}>
*/
public List<Employee> filterEmployeeAge(List<Employee> emps) {
List<Employee> filterList = new ArrayList<>();
for (Employee emp : emps) {
if (emp.getAge() > 30) {
filterList.add(emp);
}
}
return filterList;
}
/**
* 调用方法进行测试。
*/
@Test
public void test02() {
for (Employee employee : filterEmployeeAge(emps)) {
System.out.println(employee);
// id=3 ,name=王五 ,age=39 ,salary=6800.0
// id=4 ,name=赵六 ,age=40 ,salary=7100.0
}
}
-
思考:后续,如果新增此类需求都需要增加新的方法?(查询姓氏为“李”的员工,查询年龄小于35岁的员工,查询薪资小于4500的员工…)
-
现在我们通过引入
Lambda
来实现该需求并与上述实现思路进行对比(代码示例如下): -
提示:关于具体的语法及规则后续章节会进行阐述,此处只需要关注代码量及书写风格即可。
/**
* 声明一个函数式接口。
*/
@FunctionalInterface
public interface IPredicate<T> {
/**
* 自定义规则匹配。
*
* @param t t
* @return boolean
*/
boolean match(T t);
}
/**
* 通用过滤方法。
*
* @param emps 员工列表
* @param ipr 函数接口实现
* @return {@link List}<{@link Employee}>
*/
public List<Employee> filterEmployee(List<Employee> emps, IPredicate<Employee> ipr) {
List<Employee> filterList = new ArrayList<>();
for (Employee emp : emps) {
if (ipr.match(emp)) {
filterList.add(emp);
}
}
return filterList;
}
@Test
public void test03() {
List<Employee> empsByName = filterEmployee(emps, e -> e.getName().startsWith("李"));
empsByName.forEach(System.out::println);
// id=2 ,name=李四 ,age=28 ,salary=5200.0
System.out.println("------------------------------------------");
List<Employee> empsByAge = filterEmployee(emps, e -> e.getAge() < 35);
empsByAge.forEach(System.out::println);
// id=1 ,name=张三 ,age=17 ,salary=3500.0
// id=2 ,name=李四 ,age=28 ,salary=5200.0
System.out.println("------------------------------------------");
List<Employee> empsBySalary = filterEmployee(emps, e -> e.getSalary() < 4500);
empsBySalary.forEach(System.out::println);
// id=1 ,name=张三 ,age=17 ,salary=3500.0
}
- 总结:我们可以看出,用 Lambda 进行需求实现,使得代码更加简洁、灵活。
2.3 Lambda 语法
Lambda
表达式在Java
语言中引入了一个新的语法元素和操作符。这个操作符为 “->
” , 该操作符被称为Lambda
操作符或箭头操作符。它将Lambda
分为两个部分:- 左侧:指定了
Lambda
表达式需要的所有参数。 - 右侧:指定了
Lambda 体
,即Lambda
表达式要执行的功能。
- 左侧:指定了
@Test
public void testLambda() {
// 语法格式一:无参数,无返回值。
Runnable r1 = () -> System.out.println("Hello Lambda!");
// 语法格式二:有一个参数,并且无返回值。
Consumer<String> fun = (args) -> System.out.println(args);
// 语法格式三:若只有一个参数,小括号可以省略不写。
Consumer<String> fun1 = args -> System.out.println(args);
// 语法格式四:有两个以上的参数,有返回值,并且 Lambda 体中有多条语句。
Comparator<Integer> com = (x, y) -> {
System.out.println(" 有两个以上参数,且有返回值。 ");
return Integer.compare(x, y);
};
// 语法格式五:若 Lambda 体中只有一条语句, return 和 大括号都可以省略不写。
Comparator<Integer> com1 = (x, y) -> Integer.compare(x, y);
// 语法格式六:Lambda 表达式的参数列表的数据类型可以省略不写,因为JVM编译器通过上下文推断出数据类型,即“类型推断”。
// 此处参数列表的Integer可以省略。
Comparator<Integer> com2 = (Integer x, Integer y) -> Integer.compare(x, y);
}
三、函数式接口
3.1 相关概述
- 只包含一个抽象方法的接口,称为函数式接口。
- 你可以通过
Lambda
表达式来创建该接口的对象。(若Lambda
表达式抛出一个受检异常,那么该异常需要在目标接口的抽象方法上进行声明)。 - 我们可以在任意函数式接口上使用
@FunctionalInterface
注解,这样做可以检查它是否是一个函数式接口,同时javadoc
也会包含一条声明,说明这个接口是一个函数式接口。
3.2 四大核心函数式接口及使用
- 说明:
函数式接口 | 参数类型 | 返回类型 | 用途 |
---|---|---|---|
Consumer<T> 消费型接口 | T | void | 对类型为T的对象应用操作。包含方法:void accept(T t); |
Supplier<T> 供给型接口 | 无 | T | 返回类型为T的对象。包含方法:T get(); |
Function<T, R> 函数型接口 | T | R | 对类型为T的对象应用操作,并返回结果,结果是R类型的对象。包含方法:R apply(T t); |
Predicate<T> 断定型接口 | T | boolean | 确定类型为T的对象是否满足某约束,并返回boolean 值。包含方法boolean test(T t); |
- 代码示例:
/**
* Java8 内置的四大核心函数式接口。
*/
public class FunctionalInterfaceSample {
/* *
* Consumer<T> : void accept(T t);
* 消费型接口 :该接口对应的方法类型为接收一个参数,没有返回值。
* 此处需求 :打印传入的消费金额参数。
*/
public void spend(double money, Consumer<Double> cons) {
cons.accept(money);
}
@Test
public void testConsumer() {
spend(10.0, money -> System.out.println("一共消费" + money + "元"));
// 一共消费10.0元
}
/* *
* Supplier<T> : T get();
* 供给型接口 : 该接口对应的方法类型不接受参数,但是提供一个返回值。
* 此处需求 :产生指定个数的随机整数,并放入集合中。
*/
public List<Integer> saveRandomNum(int num, Supplier<Integer> sup) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < num; i++) {
Integer n = sup.get();
list.add(n);
}
return list;
}
@Test
public void testSupplier() {
List<Integer> nums = saveRandomNum(3, () -> (int) (Math.random() * 100));
nums.forEach(System.out::println);
// 77 23 79
}
/* *
* Function<T, R> : R apply(T t);
* 函数型接口 :对类型为T的对象应用操作,并返回结果,结果是R类型的对象。
* 此处需求 :用于处理字符串并返回新的对象。
*/
public String handleString(String str, Function<String, String> fun) {
return fun.apply(str);
}
@Test
public void testFunction() {
String str = handleString("hello", String::toUpperCase);
System.out.println(str);
// HELLO
}
/* *
* Predicate<T> : boolean test(T t);
* 断言型接口 :对应的方法接收一个参数,返回一个boolean类型值,多见于判断和过滤。
* 此处需求 :将满足条件的数字,放入集合中。
*/
public List<Integer> filterNum(List<Integer> nums, Predicate<Integer> pre) {
List<Integer> list = new ArrayList<>();
for (Integer num : nums) {
if (pre.test(num)) {
list.add(num);
}
}
return list;
}
@Test
public void testPredicate() {
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> filteredNums = filterNum(nums, num -> num > 3);
filteredNums.forEach(System.out::println);
// 4 5 6
}
}
3.3 其他函数接口
函数式接口 | 参数类型 | 返回类型 | 用途 |
---|---|---|---|
BiFunction<T, U, R> | T, U | R | 对类型为 T, U 参数应用操作, 返回 R 类型的结果。包含方法为R apply(T t, U u); |
UnaryOperator<T> (Function子接口) | T | T | 对类型为T的对象进行一元运算, 并返回T类型的结果。包含方法为T apply(T t); |
BinaryOperator<T> (BiFunction 子接口) | T, T | T | 对类型为T的对象进行二元运算,并返回T类型的结果。包含方法为T apply(T t1, T t2); |
BiConsumer<T, U> | T, U | void | 对类型为T, U 参数应用操作。包含方法为void accept(T t, U u); |
ToIntFunction<T> ToLongFunction<T> ToDoubleFunction<T> | T | int long double | 分别计算 int 、 long 、double、值的函数。 |
IntFunction<R> LongFunction<R> DoubleFunction<R> | int long double | R | 参数分别为int、long、double 类型的函数。 |
四、方法与构造器引用
4.1 方法引用
-
当要传递给
Lambda体
的操作,已经有实现的方法了,可以使用方法引用。 -
注意:实现抽象方法的参数列表,必须与方法引用方法的参数列表保持一致!
-
使用方式:使用操作符 “
::
” 将方法名和对象或类的名字分隔开来。 -
如下三种主要使用情况:
对象::实例方法
类::静态方法
类::实例方法
public class MethodRef {
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Employee {
private long id;
private String name;
public static String staticMethod() {
return "调用静态方法";
}
public String instanceMethod() {
return "调用实例方法";
}
@Override
public String toString() {
return " id=" + id +
" ,name=" + name;
}
}
@Test
public void test01() {
// 1.对象::实例方法
Employee employee = new Employee(1, "张三");
Supplier<String> name = employee::getName;
System.out.println(name.get());
// 张三
// 2.类::静态方法
Supplier<String> stm = Employee::staticMethod;
System.out.println(stm.get());
// 调用静态方法
// 3.类::实例方法
Function<Employee, String> inm = Employee::instanceMethod;
System.out.println(inm.apply(new Employee()));
// 调用实例方法
}
}
4.2 构造器引用
- 构造器引用格式:
ClassName::new
- 数组引用格式:
type[]::new
- 与函数式接口相结合,自动与函数式接口中方法兼容,可以把构造器引用赋值给定义的方法,与构造器参数列表要与接口中抽象方法的参数列表一致!
@Test
public void test02() {
// 一、构造器引用:类名::new
BiFunction<Long, String, Employee> employee = Employee::new;
Employee e = employee.apply(1L, "张三");
System.out.println(e.getName());
// 张三
// 二、数组引用:类型[]::new;
Function<Integer, String[]> fun = String[]::new;
String[] strs = fun.apply(10);
System.out.println(strs.length);
// 10
}
五、Stream API
5.1 相关概述
Java 8 API
添加了一个新的抽象称为流Stream
,可以让你以一种声明的方式处理数据。Stream
使用一种类似用SQL
语句从数据库查询数据的直观方式来提供一种对Java
集合运算和表达的高阶抽象。Stream API
可以极大提高Java
程序员的生产力,让程序员写出高效率、干净、简洁的代码。- 这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。
- 元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。
5.2 什么是 Stream ?
Stream
(流)是一个来自数据源的元素队列并支持聚合操作。“集合讲的是数据,流则讲的是计算。”
-
注意:
Stream
自己不会存储元素。Stream
不会改变源对象。相反,它们会返回一个持有结果的新Stream
。Stream
操作是延迟执行的,这意味着它们会等到需要结果的时候才执行。
-
操作步骤:
- 创建 Stream:一个数据源(如:集合、数组),获取一个流。
- 中间操作:对数据源的数据进行处理。
- 终止操作(终端操作):执行中间操作链,并产生结果。
Pipelining
...............................
: v
+----------------------+ +--------+ +--------+ +-----+ +-------------------+
| elements(DataSource) | --> | filter | --> | sorted | --> | map | --> | collect(Endpoint) |
+----------------------+ +--------+ +--------+ +-----+ +-------------------+
5.3 测试类准备
public class StreamAPISample {
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Employee {
private long id;
private String name;
private int age;
private double salary;
@Override
public String toString() {
return " id=" + id +
" ,name=" + name +
" ,age=" + age +
" ,salary=" + salary;
}
}
/**
* 员工列表。
*/
List<Employee> emps = Arrays.asList(
new Employee(1, "张三", 17, 3500),
new Employee(1, "张三", 17, 3500),
new Employee(2, "李四", 28, 5200),
new Employee(3, "王五", 39, 6800),
new Employee(4, "赵六", 51, 5700),
new Employee(5, "田七", 40, 7100),
new Employee(6, "陈八", 40, 9000)
);
}
5.4 筛选与切片
/**
* 中间操作 - 筛选与切片:
* filter——接收 Lambda ,从流中排除某些元素。
* limit——截断流,使其元素不超过给定数量。
* skip(n) —— 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补。
* distinct——筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素。
*/
@Test
public void test01() {
// 第一步:创建流。
Stream<Employee> stream = emps.stream();
// 第二步:执行中间操作。
Stream<Employee> stream1 = stream
// 2.1 元素过滤(只保留40岁以下员工)。
.filter(e -> e.getAge() < 40)
// 2.2 截断流(只保留4个元素)。
.limit(4)
// 2.3 去重。
.distinct();
stream1.forEach(System.out::println);
// id=1 ,name=张三 ,age=17 ,salary=3500.0
// id=2 ,name=李四 ,age=28 ,salary=5200.0
// id=3 ,name=王五 ,age=39 ,salary=6800.0
}
5.5 映射
/**
* 中间操作 - 映射:
* map——接收 Lambda ,将元素转换成其他形式或提取信息。接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
* flatMap——接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。
*/
@Test
public void test02() {
List<String> strList = Arrays.asList("aa", "bb", "cc", "dd", "ee");
// 1.通过map映射,将接收的元素转为大写并映射成新元素。
Stream<String> stream = strList.stream().map(String::toUpperCase);
stream.forEach(System.out::println);
// AA BB CC DD EE
}
5.6 排序
/**
* 中间操作 - 排序:
* sorted()——自然排序。
* sorted(Comparator com)——定制排序。
*/
@Test
public void test03() {
// 1.自然排序。
emps.stream()
// 映射元素的age值。
.map(Employee::getAge)
// 根据年龄大小进行自然排序。
.sorted()
.forEach(System.out::println);
// 17 17 28 39 40 40 51
System.out.println("------------------------------------");
// 2.定制排序。
emps.stream()
.sorted((x, y) -> {
// 若年龄相同,则根据姓名比较排序。
if (x.getAge() == y.getAge()) {
return x.getName().compareTo(y.getName());
} else {
return Integer.compare(x.getAge(), y.getAge());
}
}).distinct().forEach(System.out::println);
// id=1 ,name=张三 ,age=17 ,salary=3500.0
// id=2 ,name=李四 ,age=28 ,salary=5200.0
// id=3 ,name=王五 ,age=39 ,salary=6800.0
// id=5 ,name=田七 ,age=40 ,salary=7100.0
// id=6 ,name=陈八 ,age=40 ,salary=9000.0
// id=4 ,name=赵六 ,age=51 ,salary=5700.0
}
5.7 匹配与统计
/**
* 终止操作 - 匹配/统计:
* allMatch——检查是否匹配所有元素。
* anyMatch——检查是否至少匹配一个元素。
* noneMatch——检查是否没有匹配的元素。
* findFirst——返回第一个元素。
* findAny——返回当前流中的任意元素。
* count——返回流中元素的总个数。
* max——返回流中最大值。
* min——返回流中最小值。
*/
@Test
public void test04() {
// 1.使用 anyMatch() 进行元素匹配。
System.out.println(emps.stream()
.anyMatch(e -> "李四".equals(e.getName())));
// true
// 2.使用 count() 对过滤结果进行统计。
System.out.println(emps.stream()
.filter(e -> "张三".equals(e.getName()))
.count());
// 2
// 3.使用 map() 将薪资建立新的映射,再使用 max() 得到流中的最大值。
System.out.println(emps.stream()
.map(Employee::getSalary)
.max(Double::compare));
// 9000.0
}
5.8 流终止操作
/**
* 终止操作 -归约:
* reduce(T identity, BinaryOperator) / reduce(BinaryOperator) ——可以将流中元素反复结合起来,得到一个值。
* collect——将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法。
* groupingBy——根据条件进行分组。
* partitioningBy——分区。
*/
@Test
public void test05() {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 1.使用 reduce() 将集合元素进行累加。
Integer sum = list.stream().reduce(0, Integer::sum);
System.out.println(sum);
// 55
System.out.println("------------------------------------");
// 2.使用 collect() 将接收的结果汇总。
emps.stream()
.map(Employee::getName)
// 将结果汇总成list。
.collect(Collectors.toList())
.forEach(System.out::println);
// 张三 张三 李四 王五 赵六 田七 陈八
System.out.println("------------------------------------");
emps.stream()
.map(Employee::getName)
// 将结果汇总成set。
.collect(Collectors.toSet())
.forEach(System.out::println);
// 李四 陈八 张三 王五 赵六 田七
System.out.println("------------------------------------");
// 3.使用 groupingBy() 进行分组。
Map<Double, Map<String, List<Employee>>> collect = emps.stream()
// 根据薪资进行分组。
.collect(Collectors.groupingBy(Employee::getSalary, Collectors.groupingBy(e -> {
// 根据具体的薪资水平线进行再次分组。
if (e.getSalary() < 4000) {
return "低薪资水平";
} else {
return "高薪资水平";
}
})));
Set<Map.Entry<Double, Map<String, List<Employee>>>> entries = collect.entrySet();
for (Map.Entry<Double, Map<String, List<Employee>>> entry : entries) {
System.out.println(entry.getValue());
// {高薪资水平=[ id=6 ,name=陈八 ,age=40 ,salary=9000.0]}
// {高薪资水平=[ id=2 ,name=李四 ,age=28 ,salary=5200.0]}
// {高薪资水平=[ id=4 ,name=赵六 ,age=51 ,salary=5700.0]}
// {高薪资水平=[ id=3 ,name=王五 ,age=39 ,salary=6800.0]}
// {高薪资水平=[ id=5 ,name=田七 ,age=40 ,salary=7100.0]}
// {低薪资水平=[ id=1 ,name=张三 ,age=17 ,salary=3500.0, id=1 ,name=张三 ,age=17 ,salary=3500.0]}
}
System.out.println("------------------------------------");
// 使用 partitioningBy() 进行分区。
Map<Boolean, List<Employee>> map = emps.stream()
.collect(Collectors.partitioningBy((e) -> e.getSalary() >= 6000));
for (Map.Entry<Boolean, List<Employee>> entry : map.entrySet()) {
System.out.println(entry.getValue());
// [ id=1 ,name=张三 ,age=17 ,salary=3500.0, id=1 ,name=张三 ,age=17 ,salary=3500.0, id=2 ,name=李四 ,age=28 ,salary=5200.0, id=4 ,name=赵六 ,age=51 ,salary=5700.0]
// [ id=3 ,name=王五 ,age=39 ,salary=6800.0, id=5 ,name=田七 ,age=40 ,salary=7100.0, id=6 ,name=陈八 ,age=40 ,salary=9000.0]
}
}
六、并行流与 Fork/Join 框架
6.1 什么是并行流?
- 并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。
Java 8
中将并行进行了优化,我们可以很容易的对数据进行并行操作。Stream API
可以声明性地通过parallel()
与sequential()
在并行流与顺序流之间进行切换。
6.2 Fork/Join 框架概述
-
Fork/Join
框架:就是在必要的情况下,将一个大任务进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行汇总(join)。 -
采用 “工作窃取”模式(work-stealing):当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。
-
相较于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上。在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行,这种方式减少了线程的等待时间,提升了性能。
-
使用场景:大数据计算。
-
示意图:
6.3 Fork/Join 与并行流的使用
- 实现
RecursiveTask<V>
接口:
/**
* 实现 RecursiveTask 接口。
*/
public class ForkJoinCalculate extends RecursiveTask<Long> {
/**
* 起始值。
*/
private final long start;
/**
* 结束值。
*/
private final long end;
/**
* 任务拆分阈值。
*/
private static final long THRESHOLD = 10_0000L;
/**
* 带参构造器。
*
* @param start 起始值
* @param end 结束值
*/
public ForkJoinCalculate(long start, long end) {
this.start = start;
this.end = end;
}
/**
* 实现具体的计算方法。
*
* @return {@link Long}
*/
@Override
protected Long compute() {
// 1.小于阈值直接进行计算。
if ((end - start) <= THRESHOLD) {
long sum = 0;
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
// 2.超过阈值则进行 fork/join。
// 从中间值进行任务拆分。
long middle = (start + end) / 2;
ForkJoinCalculate left = new ForkJoinCalculate(start, middle);
left.fork();
ForkJoinCalculate right = new ForkJoinCalculate(middle + 1, end);
right.fork();
return left.join() + right.join();
}
}
}
- 方式对比:
@Slf4j
public class ForkJoinSample {
private static final long TASK_LENGTH = 10_0000_0000L;
/**
* 方式一:使用普通的for循环方式实现。
*/
@Test
public void test01() {
long start = System.currentTimeMillis();
long sum = 0L;
for (long i = 0L; i <= TASK_LENGTH; i++) {
sum += i;
}
long end = System.currentTimeMillis();
log.info("使用普通实现方式:计算结果={},总计执行耗时={}ms", sum, (end - start));
}
/**
* 方式二:使用 fork/join 进行实现。
* 注意:由于此处实现的计算需求过于简单,因此导致 fork/join 的任务分配和管理带来的开销(线程上下文切换)远超于并行计算带来的提升。
* 故:此处性能提升并不能很直观地展现出来,我们只需要关注如何使用即可。
*/
@Test
public void test2() {
long start = System.currentTimeMillis();
ForkJoinPool pool = new ForkJoinPool();
ForkJoinCalculate task = new ForkJoinCalculate(0L, TASK_LENGTH);
long sum = pool.invoke(task);
long end = System.currentTimeMillis();
log.info("使用 fork/join 方式:计算结果={},总计执行耗时={}ms", sum, (end - start));
//
}
/**
* 方式三:使用 parallel() 并行流进行实现。
*/
@Test
public void test3() {
long start = System.currentTimeMillis();
long sum = LongStream.rangeClosed(0L, TASK_LENGTH)
.parallel()
.sum();
long end = System.currentTimeMillis();
log.info("使用并行流方式:计算结果={},总计执行耗时={}ms", sum, (end - start));
}
// [main] INFO ForkJoinSample - 使用普通实现方式:计算结果=500000000500000000,总计执行耗时=236ms
// [main] INFO ForkJoinSample - 使用 fork/join 方式:计算结果=500000000500000000,总计执行耗时=150ms
// [main] INFO ForkJoinSample - 使用并行流方式:计算结果=500000000500000000,总计执行耗时=83ms
}
七、Date Time API
7.1 相关概述
Java 8
通过发布新的Date-Time API
(JSR 310
)来进一步加强对日期与时间的处理。- 在旧版的
Java
中,日期时间API
存在诸多问题,其中有:- 非线程安全 −
java.util.Date
是非线程安全的,所有的日期类都是可变的,这是Java
日期类最大的问题之一。 - 设计很差 −
Java
的日期/时间类的定义并不一致,在java.util
和java.sql
的包中都有日期类,此外用于格式化和解析的类在java.text
包中定义。java.util.Date
同时包含日期和时间,而java.sql.Date
仅包含日期,将其纳入java.sql
包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。 - 时区处理麻烦 − 日期类并不提供国际化,没有时区支持,因此
Java
引入了java.util.Calendar
和java.util.TimeZone
类,但他们同样存在上述所有的问题。
- 非线程安全 −
Java 8
在 java.time 包下提供了很多新的API
。以下为两个比较重要的API
:- Local(本地) − 简化了日期时间的处理,没有时区的问题。
- Zoned(时区) − 通过制定的时区处理日期时间。
- 新的
java.time
包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants
),过程(during
)与时钟(clock
)的操作。
7.2 LocalDate、LocalTime、LocalDateTime
LocalDate
、LocalTime
、LocalDateTime
类的实例是不可变的对象,分别表示使用ISO-8601
日历系统的日期、时间、日期和时间。- 它们提供了简单的日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。
- 注:
ISO-8601
日历系统是国际标准化组织制定的现代公民的日期和时间的表示法。
/* *
* LocalDate、LocalTime、LocalDateTime:
* now()——静态方法,根据当前时间创建对象。
* of()——静态方法,根据指定日期/时间创建对象。
* plusDays(), plusWeeks(),plusMonths(), plusYears()——向当前 LocalDate 对象添加几天、几周、几个月、几年。
* minusDays(), minusWeeks(),minusMonths(), minusYears()——从当前 LocalDate 对象减去几天、几周、几个月、几年。
* withDayOfMonth(),withDayOfYear(),withMonth(),withYear()——将月份天数、年份天数、月份、年份修改为指定的值并返回新的 LocalDate 对象。
* getDayOfMonth()——获得月份天数(1-31)。
* getDayOfYear()——获得年份天数(1-366)
* getDayOfWeek()——获得星期几(返回一个 DayOfWeek枚举值)
* getMonth()——获得月份, 返回一个 Month 枚举值。
* getMonthValue()——获得月份(1-12)。
* getYear()——获得年份.
* until()——获得两个日期之间的 Period 对象,或者指定 ChronoUnit 的数字。
* isBefore(), isAfter()——比较两个 LocalDate。
* isLeapYear()——判断是否是闰年。
*/
@Test
public void test01() {
// 1.now() 静态方法,根据当前时间创建对象
LocalDateTime ldt = LocalDateTime.now();
System.out.println("当前时间:" + ldt);
// 当前时间:2022-07-31T10:52:46.074569100
// 2.静态方法,根据指定日期/时间创建对象
LocalDateTime ld2 = LocalDateTime.of(2022, 7, 31, 10, 30, 10);
System.out.println("创建指定日期/时间:" + ld2);
// 创建指定日期/时间:2022-07-31T10:30:10
}
7.3 Instant、Duration、Period
- 用于“时间戳”的运算:它是以
Unix
元年(传统的设定为UTC
时区1970年1月1日午夜时分)开始所经历的描述进行运算。
/**
* Instant : 时间戳。 (使用 Unix 元年 1970年1月1日 00:00:00 所经历的毫秒值)。
* Duration : 用于计算两个“时间”间隔。
* Period : 用于计算两个“日期”间隔。
*/
@Test
public void test02() {
// 1.计算程序执行耗时。
// 获取当前时间戳。
Instant start = Instant.now();
try {
// 线程睡眠。
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Instant end = Instant.now();
System.out.println("代码段执行耗时为:" + Duration.between(start, end).toMillis() + "ms");
// 代码段执行耗时为:1001ms
System.out.println("----------------------------------");
// 2.计算当前时间距离指定时间的间隔时间。
LocalDate ld1 = LocalDate.now();
LocalDate ld2 = LocalDate.of(2020, 2, 8);
Period pe = Period.between(ld2, ld1);
System.out.println("距离2020年2月8号已经过去:" + pe.getYears() + "年" + pe.getMonths() + "月" + pe.getDays() + "天。");
// 距离2020年2月8号已经过去:2年5月23天。
}
7.4 时间校正器
/* *
* TemporalAdjuster : 时间校正器。有时我们可能需要获取例如:将日期调整到“下个周日”等操作。
* TemporalAdjusters: 该类通过静态方法提供了大量的常用 TemporalAdjuster 的实现。
*/
@Test
public void test03() {
System.out.println("当前日期为:" + LocalDate.now());
// 当前日期为:2022-07-31
LocalDate nextSunDay = LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.SUNDAY));
System.out.println("下个周日的日期为:" + nextSunDay);
// 下个周日的日期为:2022-08-07
}
7.5 解析与格式化
java.time.format.DateTimeFormatter
类,该类提供了三种格式化方法:- 预定义的标准格式
- 语言环境相关的格式
- 自定义的格式
/**
* DateTimeFormatter : 解析和格式化日期或时间。
*/
@Test
public void test04() {
// 指定格式。
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss E");
LocalDateTime ldt = LocalDateTime.now();
// 将当前时间按照指定格式进行输出。
String strDate = ldt.format(dtf);
System.out.println(strDate);
// 2022年07月31日 11:35:33 周日
}
7.6 时区的处理
Java8
中加入了对时区的支持,带时区的时间为分别为:ZonedDate
、ZonedTime
、ZonedDateTime
- 其中每个时区都对应着
ID
,地区ID
都为 “{区域}/{城市}
”的格式,例如 :Asia/Shanghai
等。
ZoneId
:该类中包含了所有的时区信息:getAvailableZoneIds()
: 可以获取所有时区时区信息。of(id)
: 用指定的时区信息获取ZoneId
对象。
/**
* ZonedDate、ZonedTime、ZonedDateTime : 带时区的时间或日期。
*/
@Test
public void test05() {
LocalDateTime ldt = LocalDateTime.now(ZoneId.of("Asia/Shanghai"));
System.out.println("此时上海时区的时间为:" + ldt);
// 此时上海时区的时间为:2022-07-31T12:15:46.811099200
ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("America/New_York"));
System.out.println("此时纽约的时间为:" + zdt);
// 此时纽约的时间为:2022-07-31T00:15:46.822102400-04:00[America/New_York]
// 查询所有可用的时区ID。
Set<String> set = ZoneId.getAvailableZoneIds();
set.forEach(System.out::println);
}
八、接口中的默认方法
public interface IInterface {
/**
* 默认方法,使用 default 进行修饰。
* 接口的默认方法采取”类优先”原则:
* 如果一个父类提供了具体的实现,那么接口中具有相同名称和参数的默认方法会被忽略。
* 如果一个父接口提供一个默认方法,而另一个接口也提供了一个具有相同名称和参数列表的方法(不管方法是否是默认方法),那么必须覆盖该方法来解决冲突。
*
* @return {@link String}
*/
default String getHello() {
return "hello world!";
}
/**
* Java8 中,接口中允许添加静态方法。
*
* @return {@link String}
*/
static String show() {
return "hello java8";
}
}
九、Optional 类
- Optional<T> 类(
java.util.Optional
) 是一个容器类,代表一个值存在或不存在,原来用null
表示一个值不存在,现在Optional
可以更好的表达这个概念,并且可以避免空指针异常。 Optional
提供一种类型级解决方案来表示可选值而不是空引用。- 常用方法:
方法名 | 用途 |
---|---|
Optional.of(T t) | 创建一个 Optional 实例 |
Optional.empty() | 创建一个空的 Optional 实例 |
Optional.ofNullable(T t) | 若 t 不为 null,创建 Optional 实例,否则创建空实例 |
isPresent() | 判断是否包含值 |
orElse(T t) | 如果调用对象包含值,返回该值,否则返回t |
orElseGet(Supplier s) | 如果调用对象包含值,返回该值,否则返回 s 获取的值 |
map(Function f) | 如果有值对其处理,并返回处理后的Optional,否则返回 Optional.empty() |
flatMap(Function mapper) | 与 map 类似,要求返回值必须是Optional |
- 代码示例:
public class OptionalSample {
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Classes {
private Student student;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Student {
private long id;
private String name;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class NewClasses {
/**
* 注意:Optional 不能被序列化。
*/
private Optional<Student> students = Optional.empty();
}
/**
* 旧的实现方式:
*/
@Test
public void test01() {
// 此处构造一个班级的实例。
Student stu = new Student(1, "jan");
Classes cla = new Classes(stu);
// 在过去,我们想要拿到班级的学生信息,为避免取值过程中发生空指针异常,我们需要做一些非空判断...
if (null != cla) {
Student student = cla.getStudent();
if (null != student) {
String name = student.getName();
System.out.println(name);
// jan
}
}
}
/**
* 使用 Optional 实现:
*/
@Test
public void test02() {
// 情况一:若传入的实例不为 null,创建 Optional 实例,否则创建空实例。
Optional<Student> student = Optional.ofNullable(new Student(1, "jan"));
Optional<NewClasses> newClasses = Optional.ofNullable(new NewClasses(student));
System.out.println(getStudentName(newClasses));
// jan
// 情况二:传入空值。
Optional<Student> student1 = Optional.ofNullable(null);
Optional<NewClasses> newClasses1 = Optional.ofNullable(new NewClasses(student1));
System.out.println(getStudentName(newClasses1));
// tony
}
/**
* 获取学生名字的方法。
*
* @param newClasses 持有 Optional 容器构造的班级类。
* @return {@link String}
*/
public String getStudentName(Optional<NewClasses> newClasses) {
// orElse()如果调用对象包含值,返回该值,否则返回创建的新对象。
return newClasses.orElse(new NewClasses())
.getStudents()
.orElse(new Student(2, "tony"))
.getName();
}
}
十、结束语
“-------怕什么真理无穷,进一寸有一寸的欢喜。”
微信公众号搜索:饺子泡牛奶。