一个简洁的博客网站:http://lss-coding.top,欢迎大家来访
学习娱乐导航页:http://miss123.top/
1. Open JDK 和 Oracle JDK
Java 由 Sun 公司发明,Open JDK 是 Sun 在2006年末把 Java 开源而形成的项目。也就是说 Open JDK 是 JavaSE 平台版的开源和免费实现,它由 SUN 和 Java 社区提供支持,2009年 Oracle 收购了 Sun 公司,自此 Java 的维护方之一的 Sun 也变成了 Oracle。
**关系:**大多数 JDK 都是在 Open JDK 的基础上进一步编写完成的,比如 IBM J9,Oracle JDK 和 Azul Zulu,Azul Zing。Oracle JDK 完全由 Oracle 公司开发,Oracle JDK 是基于 Open JDK 源代码的商业版本。此外,它包含闭源组件。Oracle JDK 根据二进制代码许可协议获得许可,在没有商业许可的情况下,在 2019年1月之后发布的 Oracle Java SE8 的公开更新将无法用于商业或生产用途。但是 Open JDK 是完全开源的,可以自由使用。
Open JDK 官网:http://openjdk.java.net/
2. Lambda 表达式
2.1 分析
public static void main(String[] args) {
// 创建一个线程,执行一段代码
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程执行:" + Thread.currentThread());
}
}).start();
System.out.println("主线程执行:" + Thread.currentThread());
}
代码分析:
- Thread 类需要一个 Runnable 接口作为参数,其中的抽象方法 run 方法是用来执行线程任务的核心
- 为了执行 run() 方法,不得不需要 Runnable 的实现类
- 为省去定义 Runnable 实现类,不得不使用匿名内部类
- 必须覆盖重写抽象的 run 方法,所有的方法名称、参数、返回值不得不重写一遍
- 实际上,只在乎方法体的代码
2.2 使用
Lambda 表达式是一个匿名内部类,可以理解为一段可以传递的代码
new Thread(() -> {
System.out.println("Lambda执行线程:" + Thread.currentThread());
}).start();
优点:简化匿名内部类使用,语法更加简单。
匿名内部类语法冗余,Lambda表达式是简化匿名内部类的一种方式。
2.3 语法规则
省去了面向对象的一些约束
(参数类型 参数名称,...) -> {方法体}
2.4 练习1
无参 Lambda 表达式
- 首先创建一个 UserService 方法
public interface UserService {
void show();
}
- 调用该方法
public class Demo02 {
public static void main(String[] args) {
getShow(() -> {
System.out.println("调用了UserService接口");
});
}
public static void getShow(UserService userService) {
userService.show();
}
}
2.5 练习2
有参 Lambda 表达式
- 创建一个 User 实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String name;
private Integer age;
}
- 在主类中创建三个对象加入到 list 集合中
- 不适用 lambda 进行排序
public class Demo03 {
public static void main(String[] args) {
List<User> list = new ArrayList<>();
list.add(new User("张三", 22));
list.add(new User("李四", 31));
list.add(new User("王五", 11));
Collections.sort(list, new Comparator<User>() {
@Override
public int compare(User o1, User o2) {
return o1.getAge() - o2.getAge();
}
});
list.forEach(System.out::println);
}
}
- 可以看到上面 sort 方法中的 new Comparator… 是一个匿名内部类,可以通过 lambda 进行转换
Collections.sort(list, (User o1, User o2) -> {
return o1.getAge() - o2.getAge();
});
2.6 @FunctionalInterface 注解
在我们的接口中只能有一个方法,否则在调用的时候会报错,使用该注解标识我们的接口为函数时接口,并且该接口中只能有一个抽象方法(方法)
/**
* 标志注解,被该注解修饰的接口只能声明一个抽象方法
**/
@FunctionalInterface
public interface UserService {
void show();
}
2.7 原理
- 匿名内部类的本质就是在编译的时候生成一个 Class 文件,类名$1.class
// 该代码执行完成之后
// 创建一个线程,执行一段代码
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程执行:" + Thread.currentThread());
}
}).start();
通过反编译可以看到
package com.lss.jdk8NewFeatures.lambda;
final class Demo01$1 implements Runnable {
Demo01$1() {
}
public void run() {
System.out.println("子线程执行:" + Thread.currentThread());
}
}
- Lambda 表达式
写有 Lambda 表达式的 class 文件不能反编译工具 Xjad进行查看,可以通过 JDK 自带的工具:javap 对字节码进行反汇编操作。
javap -c -p 文件名.class
-c:表示对代码进行反汇编
-p:显示所有类和成员
在反编译的源码中看到了一个静态方法 lambda$main$0(),在代码体其实就是调用这个方法
匿名内部类在编译的收会产生一个 class 文件
Lambda 表达式在程序运行的时候会形成一个类
- 在类中新增一个方法,这个方法的方法体就是 Lambda 表达式中的代码
- 还会生成一个匿名内部类,实现接口,重写抽象方法
- 在接口中重写方法会调用新生成的方法
2.8 省略写法
- 小括号内的参数类型可以省略
- 如果小括号里面有且只有一个参数,小括号可以省略
- 如果花括号中有且只有一个语句,可以省略花括号,return 关键字以及语句分号。
getShow(() -> System.out.println("调用了UserService接口"));
getStudent( name -> System.out.println(name));
Lambda 使用要求:方法的参数或局部变量必须为接口才能使用 Lambda、接口中有且仅有一个抽象方法(@FunctionalInteface)
Lambda 和 匿名内部类对比
- 类型不一样
- 匿名内部类的类型可以是类、抽象类、接口
- Lambda表达式需要的类型必须是接口
- 方法的数量不一样
- 匿名内部类所需的接口中的抽象方法的数量是随意的
- Lambda 表达式所需的接口中只能有一个抽象方法
- 实现原理不一样
- 匿名内部类是在编译后生成一个 Class文件
- Lambda 表达式是在程序运行的时候动态生成 Class 文件
3. 接口中新增的方法
3.1 接口新增
在 JDK8 中对接口进行了增强,接口中可以有默认方法和静态方法
interface 接口名{
静态常量;
抽象方法;
默认方法;
静态方法;
}
3.2 默认方法
3.2.1 为什么要新增默认方法
在 JDK8 以前的接口中只能由抽象方法和静态常量,会出现一下问题:
如果接口中添加了一个抽象方法,那么它所有的实现类都需要重写这个抽象方法,非常地不利于接口的扩展。
3.2.2 使用
接口中默认方法的语法格式:
interface 接口名{
修饰符 default 返回值类型 方法名{
方法体;
}
}
public class Demo01 {
public static void main(String[] args) {
A b = new B();
System.out.println(b.show2());
A c = new C();
System.out.println(c.show2());
}
}
interface A {
void show();
// 定义一个默认方法
public default String show2() {
return "执行了A接口中的默认方法";
}
}
class B implements A {
@Override
public void show() {
}
}
class C implements A {
@Override
public void show() {
}
// 重写默认方法
@Override
public String show2() {
return "在C类中的重写A接口的默认方法";
}
}
执行了A接口中的默认方法
在C类中的重写A接口的默认方法
接口中的默认方法有两种使用方式:
- 实现类直接调用接口的默认方法
- 实现类重写接口的默认方法
3.3 静态方法
接口中的静态方法就是在接口中定义一个抽象方法通过static 关键字进行修饰,该静态方法不能被实现类重写,只能通过接口名.方法名()调用
public class Demo01 {
public static void main(String[] args) {
A b = new B();
System.out.println(b.show2());
A c = new C();
System.out.println(c.show2());
// 调用接口中的静态方法
A.show3();
}
}
interface A {
void show();
// 定义一个默认方法
public default String show2() {
return "执行了A接口中的默认方法";
}
// 定义一个静态方法
public static void show3() {
System.out.println("执行了接口中的静态方法");
}
}
- 默认方法通过实例调用,静态方法通过接口名调用
- 默认方法可以继承,实现类可以直接调用接口默认方法,也可以重写接口默认方法
- 静态方法不能被继承,实现类不能重写接口的静态方法,只能使用接口名调用
4. 函数式接口
4.1 函数式接口由来
使用 Lambda 表达式的前提是需要有函数式接口,而 Lambda 表达式使用时不关心接口名,抽象方法名。只关心抽象方法的参数列表和返回值类型。因此为了让我们使用 Lambda 表达式更加的方便,在 JDK 中提供了大量常用的函数式接口
public class Demo01 {
public static void main(String[] args) {
fun1((arr -> {
int sum = 0;
for (int i : arr) {
sum += i;
}
return sum;
}));
}
public static void fun1(Operator operator) {
int[] arr = {1,2,3,4};
int sum = operator.getSum(arr);
System.out.println("sum = " + sum);
}
}
@FunctionalInterface
interface Operator{
int getSum(int[] arr);
}
4.2 函数式接口介绍
在 java.util.function 包中定义了一些函数式接口,在之后的使用中不一定需要每次都定义一个接口
4.2.1 Supplier
无参有返回值
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
/**
* Supplier 函数式接口的使用
*/
public class Demo02Supplier {
public static void main(String[] args) {
fun1(() -> {
int arr[] = {11,2,33,5,53,99,2};
// 计算出数组中的最大值
Arrays.sort(arr);
return arr[arr.length - 1];
});
}
public static void fun1(Supplier<Integer> supplier) {
Integer integer = supplier.get();
System.out.println("max = " + integer);
}
}
4.2.2 Consumer
有参无返回值
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
}
/**
* Consumer 函数式接口使用
*/
public class Demo03Consumer {
public static void main(String[] args) {
fun1(s -> {
System.out.println(s.toLowerCase());
});
}
public static void fun1(Consumer<String> consumer) {
consumer.accept("Hello, World");
}
}
4.2.3 Function
有参有返回值,根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
}
public class Demo04Function {
public static void main(String[] args) {
test(msg -> {
return Integer.parseInt("111111");
});
}
public static void test(Function<String,Integer> function) {
Integer apply = function.apply("123");
System.out.println("apply = " + apply);
}
}
在源码中可以看到有一个 andThen 方法,做组合使用
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
public class Demo04Function2 {
public static void main(String[] args) {
test(msg -> {
return Integer.parseInt(msg);
}, msg -> {
return msg * 10;
});
}
public static void test(Function<String,Integer> f1, Function<Integer, Integer> f2) {
// Integer apply = f1.apply("123");
// Integer apply1 = f2.apply(apply);
Integer apply = f1.andThen(f2).apply("123");
System.out.println("i2: " + apply);
}
}
4.2.4 Predicate
有参且返回结果为 boolean
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
}
/**
* 根据输入的值进行判断,返回结果 boolean
*/
public class Demo05Predicate {
public static void main(String[] args) {
test(msg -> {
return msg.length() > 0;
}, "Hello");
}
public static void test(Predicate<String> predicate, String msg){
boolean b = predicate.test(msg);
System.out.println("b: " + b);
}
}
在 Predicate 中的默认方法提供了逻辑关系操作,negate、and、or等
public class Demo05Predicate1 {
public static void main(String[] args) {
test(msg -> {
return msg.contains("H");
}, msg -> {
return msg.contains("W");
});
}
public static void test(Predicate<String> p1, Predicate<String> p2){
// p1 包含 H,同时 p2 包含W
boolean r1 = p1.and(p2).test("Hello");
// p1 包含 H,或者 P2 包含W
boolean r2 = p1.or(p2).test("Hello");
// p1 不包含H
boolean r3 = p1.negate().test("Hello");
System.out.println(r1);
System.out.println(r2);
System.out.println(r3);
}
}
5. 方法的引用
5.1 使用
在 Lambda 表达式中要执行的代码和我们另一个方法的代码是一样的,这样就没有必要重新写,可以”引用“重复代码
public class Demo01 {
public static void main(String[] args) {
// :: 方法引用,JDK8新特性
printSum(Demo01::getTotal);
}
// 求数组元素之和
public static void getTotal(int a[]) {
int sum = 0;
for (int i : a) {
sum += i;
}
System.out.println("数组之和:" + sum);
}
public static void printSum(Consumer<int[]> consumer) {
int[] a = {10, 20, 30, 40, 50, 60};
consumer.accept(a);
}
}
5.2 方法引用格式
符号:::
双冒号为方法引用的运算符,它所在的表达式被称为方法引用
**应用场景:**如果 Lambda 表达式所要实现的方案,已经有其他方法存在相同的方案,那么可以使用方法应用。
常见的引用方式:
- instanceName::methodName 对象名::方法名
- ClassName::staticMethodName 类名::静态方法
- ClassName::methodName 类名::普通方法
- ClassName::new 类名:::new 调用的构造器
- TypeName[]::new String[]::new 调用数组的构造器
5.3 对象名::方法名
最常见用法,如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法
public static void main(String[] args) {
Date date = new Date();
Supplier<Long> supplier = () -> date.getTime();
System.out.println(supplier.get());
// 通过方法引用的方式来进行处理
Supplier<Long> supplier1 = date::getTime;
System.out.println(supplier1.get());
}
注意:被引用的方法参数要和接口中的抽象方法的参数一样;当接口抽象方法有返回值时,被引用的方法也必须有返回值。
5.4 类名::静态方法名
// public static native long currentTimeMillis();
public static void main(String[] args) {
Supplier<Long> supplier = () -> {
return System.currentTimeMillis();
};
System.out.println(supplier.get());
// 通过方法引用实现
Supplier<Long> supplier1 = System::currentTimeMillis;
System.out.println(supplier1.get());
}
5.5 类名::引用实例方法
Java 面向对象中,类名只能调用静态方法,类名引用实例方法是有前提的,实际上是拿第一个参数作为方法的调用者。
public static void main(String[] args) {
Function<String, Integer> function = (s) -> {
return s.length();
};
System.out.println(function.apply("hello"));
// 通过方法引用实现
Function<String, Integer> function1 = String::length;
System.out.println(function1.apply("aaaaaaaaaaaaaaaaaaaa"));
BiFunction<String, Integer, String> function2 = String::substring;
String msg = function2.apply("HelloWorld", 3);
System.out.println(msg);
}
5.6 类名::构造器
由于构造器名称与类名完全一致,所以构造器引用使用 ::new
的格式使用
public static void main(String[] args) {
Supplier<User> supplier = () ->{ return new User(); };
System.out.println(supplier.get());
// 通过方法引用的方式
Supplier<User> supplier1 = User::new;
System.out.println(supplier1.get());
BiFunction<String, Integer, User> function = User::new;
System.out.println(function.apply("张三", 21));
}
5.7 数组::构造器
public static void main(String[] args) {
Function<Integer, String[]> fun1 = (len) -> new String[len];
String[] a1 = fun1.apply(3);
System.out.println("数组长度为:" + a1.length);
// 方法引用方式调用数组的构造器
Function<Integer, String[]> fun2 = String[]::new;
String[] a2 = fun2.apply(8);
System.out.println("数组长度为:" + a2.length);
}
方法引用是对 Lambda 表达式符合特定情况下的一种缩写方式,它使得我们的 Lambda 表达式更加的简洁,也可以理解为 Lambda 表达式的缩写形式,不过需要注意的是方法引用只能引用已经存在的方法。
6. Stream API
6.1 集合处理的弊端
需要对一个集合进行查询的处理,则需要写好几个循环才能实现
- 不适用 Stream
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("李四");
list.add("李晓小");
list.add("张三");
// 1. 查询集合中姓名以 李 开头的
List<String> list1 = new ArrayList<>(); // 记录开头为李的元素
for (String s : list) {
if (s.startsWith("李")) {
list1.add(s);
}
}
// 2. 在开头为李的元素中查找长度为3的
List<String> list2 = new ArrayList<>(); // 记录长度为3的元素列表
for (String s : list1) {
if (s.length() == 3) {
list2.add(s);
}
}
// 3. 输出结果查看
for (String s : list2) {
System.out.println(s);
}
}
- 使用Stream
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("李四");
list.add("李晓小");
list.add("张三");
list.stream() // 获取流
.filter(s -> s.startsWith("李")) // 通过 Lambda 过滤掉开头不是李的元素
.filter(s -> s.length() == 3) // 通过 Lambda 过滤掉长度不为3 的元素
.forEach(System.out::println); // 遍历输出
}
6.2 Stream 流式思想概述
注意:Stream 和 IO 流是没有任何关系的
Stream 流式思想类似于工厂车间的”生产流水线“,Stream 流不是一种数据结构,不保存数据,而是对数据进行加工处理。Stream 可以看作是流水线的一个工序。在流水线上,通过多个工序让一个原料加工成一个商品。
Stream API 能够让我们快速完成许多复杂的操作:筛选、切片、映射、查找、去重、统计、匹配和归约等。
6.3 Stream 获取方式
6.3.1 根据 Collection
java.util.Collection 接口中加入了 default 方法 stream, 也就是说 Collection 接口下的所有的实现都可以通过 stream 方法来获取Stream流。
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.stream();
Set<String> set = new HashSet<>();
set.stream();
Vector vector = new Vector();
vector.stream();
}
注意:Map 接口没有实现 Collection 接口,可以通过 Map 的 key vlaue 集合获取
Map<String, Object> map = new HashMap<>();
Stream<String> stream = map.keySet().stream(); // key
Stream<Object> stream1 = map.values().stream(); // value
Stream<Map.Entry<String, Object>> stream2 = map.entrySet().stream(); // entry
6.3.2 根据 Stream 的 of 方法
开发中不可避免会需要进行对数据中数据的操作,由于数组对象不可能添加默认方法,所有 Stream 接口中提供了静态方法 of
public static void main(String[] args) {
Stream<String> stringStream = Stream.of("1", "2", "3");
int[] ints = {11,22,33};
Stream<int[]> ints1 = Stream.of(ints);
stringStream.forEach(System.out::println);
// 基本数据类型的数组是不行的,输出的数组的地址 [I@5b6f7412
ints1.forEach(System.out::println);
}
6.4 Stream 常用方法
常用 API,分为两种:终结、函数拼接
方法名 | 方法作用 | 返回值类型 | 方法种类 |
---|---|---|---|
count | 统计个数 | long | 终结 |
forEach | 逐一处理 | void | 终结 |
filter | 过滤 | Stream | 函数拼接 |
limit | 取用前几个 | Stream | 函数拼接 |
skip | 跳过前几个 | Stream | 函数拼接 |
map | 映射 | Stream | 函数拼接 |
concat | 组合 | Stream | 函数拼接 |
**终结方法:**返回值类型不再是 Stream 类型的方法,不再支持链式调用。
**非终结方法:**返回值类型仍然是 Stream 类型的方法,支持链式调用。(除了终结方法外,其余方法均为非终结方法)
注意:
- Stream 只能操作一次。
- Stream 方法返回的是新的流。
- Stream 不调用终结方法,中间步骤不会执行。
6.4.1 forEach
遍历流中的数据
void forEach(Consumer<? super T> action);
// 该方法接收一个 Consumer 接口,会将每一个流元素交给函数处理
public static void main(String[] args) {
Stream<String> stringStream = Stream.of("1", "2", "3");
stringStream.forEach(System.out::println);
}
6.4.2 count
统计流中元素的个数
long count();
public static void main(String[] args) {
Stream<String> stringStream = Stream.of("1", "2", "3");
long count = stringStream.count();
System.out.println(count);
}
6.4.3 filter
过滤数据,返回符合条件的数据。从下图可以看出,将一个流转成成另一个子集流
Stream<T> filter(Predicate<? super T> predicate);
该接口接收一个 Predicate 函数式接口参数,作为筛选条件。
public static void main(String[] args) {
Stream<String> stringStream = Stream.of("a1", "a2", "a3");
stringStream
.filter(s -> s.startsWith("a"))
.forEach(System.out::println);
}
6.4.4 limit
可以对流进行截取处理,取前 n 个数据
Stream<T> limit(long maxSize);
public static void main(String[] args) {
Stream<String> stringStream = Stream.of("a1", "a2", "a3","a4", "a5", "a6");
Stream<String> limit = stringStream.limit(2);
limit.forEach(System.out::println);
}
6.4.5 skip
如果希望跳过前面的几个元素,可以使用 skip 获取截取之后的新流。
public static void main(String[] args) {
Stream<String> stringStream = Stream.of("a1", "a2", "a3","a4", "a5", "a6");
stringStream.skip(3).forEach(System.out::println);
}
6.4.6 map
如果需要将流中的元素映射到另一个流中,可以使用 map 方法
<R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper)
该接口需要 Function 函数式接口参数,可以将当前的 T 类型数据转换成另一种类型数据。
public static void main(String[] args) {
Stream.of("1", "2", "3","4", "5", "6")
// .map(msg -> Integer.parseInt(msg))
.map(Integer::parseInt)
.forEach(System.out::println);
}
6.4.7 sorted
如果需要将数据排序,可以使用该方法
Stream<T> sorted();
public static void main(String[] args) {
Stream.of("5", "8", "2","4", "1", "0")
.map(Integer::parseInt)
// .sorted() // 根据自然顺序排序
.sorted((n1, n2 ) -> n2 - n1) // 通过比较器指定对应的排序规则
.forEach(System.out::println);
}
6.4.8 distinct
如果要去掉流元素中的重复元素,可以使用该方法
Stream<T> distinct();
public static void main(String[] args) {
Stream.of("5", "8", "2","4","0", "1", "0")
.distinct()
.forEach(System.out::println);
}
注意:Stream 流中的 distinct 方法对于基本数据类型是可以直接去重的,但是对于自定义类型,需要重写 hasCode 和 equals 方法来判断。
6.4.9 match
如果需要判断数据是否匹配指定的条件,可以使用该方法 match 是终结方法
// 元素是否有一个满足条件
boolean anyMatch(Predicate<? super T> predicate);
// 元素是否都满足条件
boolean allMatch(Predicate<? super T> predicate);
// 元素是否都不满足条件
boolean noneMatch(Predicate<? super T> predicate);
public static void main(String[] args) {
boolean b = Stream.of("5", "8", "2", "4", "0", "1", "0")
.map(Integer::parseInt)
.allMatch(s -> s > 0); // 判断上面集合中的各个数字是否都大于 0
System.out.println(b);
}
6.4.10 find
如果需要找到某些元素或者数据,可以使用该方法实现
Optional<T> findFirst();
Optional<T> findAny();
public static void main(String[] args) {
Optional<Integer> first = Stream.of("5", "8", "2", "4", "1", "1", "1")
.map(Integer::parseInt)
.findFirst();
System.out.println(first.get());
}
6.4.11 max 和 min
如果想要获取最大值和最小值则可以使用该方法
Optional<T> max(Comparator<? super T> comparator);
Optional<T> min(Comparator<? super T> comparator);
public static void main(String[] args) {
Optional<Integer> max = Stream.of("5", "8", "2", "4", "1", "1", "1")
.map(Integer::parseInt)
.max((o1, o2) -> o1 - o2);
Optional<Integer> min = Stream.of("5", "8", "2", "4", "1", "1", "1")
.map(Integer::parseInt)
.min((o1, o2) -> o1 - o2);
System.out.println("最大值:" + max.get());
System.out.println("最小值:" + min.get());
}
6.4.12 reduce 方法
如果需要将所有数据归纳得到一个数据,可以使用该方法
T reduce(T identity, BinaryOperator<T> accumulator);
public static void main(String[] args) {
Stream<Integer> integerStream = Stream.of(2, 4, 6, 1, 9, 4);
// 第一次的时候会将默认值复制给 x
// 之后每次会将上一次的操作结果复制给 x, y就是每次从数据中获取的元素
Integer reduce = integerStream.reduce(0, (x, y) -> {
System.out.println("x=" + x + ",y=" + y);
return x + y;
});
System.out.println(reduce);
}
6.4.13 map 和 reduce 的组合
实际使用的时候都是 map 和 reduct 一块进行使用
public static void main(String[] args) {
// 1. 求出列表中所有用户年龄的总和
List<User> list = new ArrayList<>();
list.add(new User("张三", 20));
list.add(new User("李四", 20));
list.add(new User("王五", 20));
list.add(new User("高成", 20));
Integer sum = list.stream()
.map(User::getAge) // 实现类型数据的转换
.reduce(0,Integer::sum);
System.out.println("所有年龄的总和为:" + sum);
// 2. 求出所有年龄中的最大值
List<User> list1 = new ArrayList<>();
list.add(new User("张三", 20));
list.add(new User("李四", 20));
list.add(new User("王五", 20));
list.add(new User("高成", 22));
Integer sum1 = list.stream()
.map(User::getAge) // 实现类型数据的转换
.reduce(0,Integer::max);
System.out.println("所有年龄最大的为:" + sum1);
// 3. 统计字符 a 出现的次数
Integer reduce = Stream.of("a", "b", "c", "a", "b", "e", "a")
.map(ch -> "a".equals(ch) ? 1 : 0)
.reduce(0, Integer::sum);
System.out.println("字母a出现的次数:" + reduce);
}
6.4.14 mapToInt
Integer 类型占用的内存会比 int 多得多,在 Stream 流操作中会自动进行装箱和拆箱操作, 为了提高代码效率可以先将流中的Integer转换成 int 在操作。
IntStream mapToInt(ToIntFunction<? super T> mapper);
public static void main(String[] args) {
Integer arr[] = {1,2,3,4,5,6};
Stream.of(arr)
.filter(i -> i > 4)
.forEach(System.out::println);
System.out.println("-----------------------");
Stream.of(arr)
.mapToInt(Integer::intValue)
.forEach(System.out::println);
}
6.4.15 concat
如果两个流希望合并称为一个流,那么可以使用 该方法
public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) {
Objects.requireNonNull(a);
Objects.requireNonNull(b);
@SuppressWarnings("unchecked")
Spliterator<T> split = new Streams.ConcatSpliterator.OfRef<>(
(Spliterator<T>) a.spliterator(), (Spliterator<T>) b.spliterator());
Stream<T> stream = StreamSupport.stream(split, a.isParallel() || b.isParallel());
return stream.onClose(Streams.composedClose(a, b));
}
public static void main(String[] args) {
Stream<String> stream1 = Stream.of("a","b","c");
Stream<String> stream2 = Stream.of("x","y","z");
Stream.concat(stream1, stream2).forEach(System.out::println);
}
6.4.16 案例练习
需求:
- 第一个队伍只保留姓名长度为 3 的成员
- 第一个筛选之后只要前 3 个
- 第二个队伍只要姓张的成员
- 第二个队伍筛选之后不要前两个人
- 将两个队伍合并为一个队伍
- 根据姓名创建 User 对象
- 打印整个队伍的User信息
public class Demo18 {
public static void main(String[] args) {
List<String> list1 = Arrays.asList("邓紫棋", "许巍", "郭富城", "周杰伦", "汪峰", "刘德华", "黄百鸣");
List<String> list2 = Arrays.asList("赵磊", "王府井", "张云雷", "郭德纲", "张九南", "张艺兴", "张国荣");
// Stream<String> stream1 = list1
// .stream()
// .filter(s -> s.length() == 3)
// .limit(3);
// System.out.println("----------------------------------");
// Stream<String> stream2 = list2
// .stream()
// .filter(s -> s.startsWith("张"))
// .skip(2);
// Stream.concat(stream1, stream2).map(n -> new User(n)).forEach(System.out::println);
Stream.concat(
list1
.stream()
.filter(s -> s.length() == 3)
.limit(3)
, list2
.stream()
.filter(s -> s.startsWith("张"))
.skip(2)).map(User::new).forEach(System.out::println);
}
}
6.5 Stream 结果收集
6.5.1 结果收集到集合中
@Test
public void test01() {
// 将数据收集到 list中
List<String> list = Stream.of("aa", "bb", "cc", "aa")
.collect(Collectors.toList());
System.out.println(list);
// 将数据收集到 set 中
Set<String> set = Stream.of("aa", "bb", "cc", "aa")
.collect(Collectors.toSet());
System.out.println(set);
// 如果需要获取的类型为具体的实现,比如:ArrayList HashSet
ArrayList<String> arrayList = Stream.of("aa", "bb", "cc", "aa")
.collect(Collectors.toCollection(ArrayList::new));
System.out.println(arrayList);
// 收集到具体实现 HashSet
HashSet<String> hashSet = Stream.of("aa", "bb", "cc", "aa")
.collect(Collectors.toCollection(HashSet::new));
System.out.println(hashSet);
}
6.5.2 结果收集到数组中
Stream 中提供了 toArray 方法来将结果放到一个数组中,返回值类型是 Object[],如果我们要指定返回的类型,那么可以使用另一个重载的 toArray(IntFunction f) 方法。
@Test
public void test02() {
Object[] objects = Stream.of("aa", "bb", "cc", "aa")
.toArray(); // 返回的数组中元素是 Object 类型
System.out.println(Arrays.toString(objects));
// 如果需要指定返回的类型
String[] strings = Stream.of("aa", "bb", "cc", "aa")
.toArray(String[]::new);
System.out.println(Arrays.toString(strings));
}
6.5.3 对流中的数据做聚合计算
当我们使用 Stream 流处理数据后,可以像数据库的聚合函数一样对某个字段进行操作,比如获得最大值、最小值、求和、平均值或者统计数量。
@Test
public void test03() {
// 获取年龄的最大值
Optional<User> maxAge = Stream.of(
new User("张三", 12),
new User("王五", 14),
new User("张三", 12)
).collect(Collectors.maxBy((p1, p2) -> p1.getAge() - p2.getAge()));
System.out.println("最大年龄:" + maxAge.get());
// 获取年龄的最小值
Optional<User> minAge = Stream.of(
new User("张三", 12),
new User("王五", 14),
new User("张三", 12)
).collect(Collectors.minBy((p1, p2) -> p1.getAge() - p2.getAge()));
System.out.println("最小年龄:" + minAge.get());
// 求所有年龄的总和
Integer sumAge = Stream.of(
new User("张三", 12),
new User("王五", 14),
new User("张三", 12)
).collect(Collectors.summingInt(User::getAge));
System.out.println("年龄总和:" + sumAge);
// 求年龄平均值
Double aveAge = Stream.of(
new User("张三", 12),
new User("王五", 14),
new User("张三", 12)
).collect(Collectors.averagingDouble(User::getAge));
System.out.println("平均年龄为:" + aveAge);
// 统计个数
Long count = Stream.of(
new User("张三", 12),
new User("王五", 14),
new User("张三", 12)
).filter(u -> u.getAge() > 10).collect(Collectors.counting());
System.out.println("个数:" + count);
}
6.5.4 对流中的数据做分组操作
当使用 Stream 处理数据后,可以根据某个属性将数据分组
@Test
public void test04() {
// 根据姓名进行分组
Map<String, List<User>> collect = Stream.of(
new User("张三", 12),
new User("李四", 14),
new User("张三", 33),
new User("张三", 12),
new User("李四", 88),
new User("张三", 12),
new User("李四", 19)
).collect(Collectors.groupingBy(User::getName));
System.out.println("根据姓名分组");
collect.forEach((k,v) -> System.out.println("k=" + k + ",v=" +v ));
System.out.println("根据年龄分组");
Map<String, List<User>> collect1 = Stream.of(
new User("张三", 12),
new User("李四", 14),
new User("张三", 33),
new User("张三", 12),
new User("李四", 88),
new User("张三", 12),
new User("李四", 19)
).collect(Collectors.groupingBy(p -> p.getAge() >= 18 ? "成年" : "未成年"));
collect1.forEach((k,v) -> System.out.println("k=" + k + ",v=" +v ));
}
- 多级分组,先根据 name 分组,再根据年龄分组
@Test
public void test05() {
// 根据姓名进行分组
Map<String, Map<String, List<User>>> collect = Stream.of(
new User("张三", 12),
new User("李四", 14),
new User("张三", 33),
new User("张三", 12),
new User("李四", 88),
new User("张三", 12),
new User("李四", 19)
).collect(
Collectors.groupingBy(
User::getName,
Collectors.groupingBy(p -> p.getAge() >= 18 ? "成年" : "未成年")));
collect.forEach((k,v) -> {
System.out.println(k);
v.forEach((k1,v1) -> {
System.out.println("k1=" + k1 + ",v1=" + v1);
});
});
}
6.6.5 对流中的数据做分区操作
Collectors.partitioningBy 会根据值是否为 true, 把集合中的数据分割为两个列表, 一个 true 列表,一个 false 列表
@Test
public void test06() {
// 根据姓名进行分组
Map<Boolean, List<User>> collect = Stream.of(
new User("张三", 12),
new User("李四", 14),
new User("张三", 33),
new User("张三", 12),
new User("李四", 88),
new User("张三", 12),
new User("李四", 19)
).collect(Collectors.partitioningBy(p -> p.getAge() > 18));
collect.forEach((k,v) -> System.out.println("k=" + k + ",v=" + v));
}
6.6.6 对流中的数据进行拼接
Collectors.joining 会根据指定的连接符,将所有的元素连接成一个字符串。
@Test
public void test07() {
// 根据姓名进行分组
String collect = Stream.of(
new User("张三", 12),
new User("李四", 14),
new User("张三", 33),
new User("张三", 12),
new User("李四", 88),
new User("张三", 12),
new User("李四", 19)
).map(User::getName).collect(Collectors.joining("_", "###", "$$$"));
System.out.println(collect);
}
6.6 并行 Stream 流
上面使用的 Stream 流都是串行的,在一个线程上执行。
// 串行流
@Test
public void test01() {
Stream.of(3,2,5,2,1)
.filter( s -> {
System.out.println(Thread.currentThread() + "--" + s);
return s > 3;
}).count();
}
并行流
parallelStream 其实就是一个并行执行的流,它通过默认的 ForkJoinpool, 可以提高读线程的执行速度。
// 获取并行流的两种方式
@Test
public void test02() {
List<Integer> list = new ArrayList<>();
// 通过 list 接口,直接获取并行流
Stream<Integer> integerStream = list.parallelStream();
// 将已有的串行流转换为并行流
Stream<Integer> parallel = Stream.of(1, 2, 3).parallel();
}
@Test
public void test03() {
Stream.of(1, 2, 3,6,2,3,6,9)
.parallel()
.filter(s -> {
System.out.println(Thread.currentThread() + "s=" + s);
return s > 2;
}).count();
}
测试
Long times = 5000000000L;
private long start;
@Before
public void test05() {
start = System.currentTimeMillis();
}
@After
public void test06() {
long end = System.currentTimeMillis();
System.out.println("共执行:" + (end - start) + "ms");
}
@Test
public void test07() {
System.out.println("普通for循环");
// 共执行:1999ms
long sum = 0;
for (long i = 0; i < times; i++) {
sum += i;
}
}
@Test
public void test08() {
System.out.println("串行Stream流");
// 共执行:2523ms
LongStream.range(0, times)
.reduce(0, Long::sum);
}
@Test
public void test09(){
System.out.println("并行Stream流");
// 共执行:1128ms
LongStream.range(0, times)
.parallel()
.reduce(0, Long::sum);
}
通过上面的测试,可以看到 parallelStream 的效率是最高的
Stream 并行处理的过程会分而治之,也就是将一个大的任务切分成了多个小任务,这表示每个任务都是一个线程操作。
6.7 线程安全问题
// 并行流中的线程安全问题
@Test
public void test10(){
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
list.add(i);
}
System.out.println(list.size());
List<Integer> listNew = new ArrayList<>();
list.parallelStream().forEach(listNew::add);
System.out.println(listNew.size());
}
要么执行成功,结果不对,要么会抛出异常。
针对问题解决方案:
- 加同步锁
list.parallelStream().forEach(s -> {
synchronized (obj) {
listNew.add(s);
}
});
- 使用线程安全的容器
@Test
public void test12() {
Vector v = new Vector();
Object obj = new Object();
IntStream.rangeClosed(1 , 1000)
.parallel()
.forEach(i -> {
v.add(i);
});
System.out.println(v.size());
}
- 将线程不安全的容器转换成线程安全的容器
@Test
public void test13() {
List<Integer> listNew = new ArrayList<>();
// 将线程不安全的容器包装成线程安全的容器
List<Integer> synchronizedList = Collections.synchronizedList(listNew);
Object obj = new Object();
IntStream.rangeClosed(1 , 1000)
.parallel()
.forEach(i -> {
synchronizedList.add(i);
});
System.out.println(synchronizedList.size());
}
- 通过 Stream 中的 toArray 方法或者 collect 方法来操作,满足线程安全的要求
@Test
public void test14() {
List<Integer> listNew = new ArrayList<>();
List<Integer> collect = IntStream.rangeClosed(1, 1000)
.parallel()
.boxed()
.collect(Collectors.toList());
System.out.println(collect.size());
}
6.8 Fork/Join 框架
parallelStream 使用的是 Fork/Join 框架。Fork/Join 框架自 JDK7 引入。Fork/Join 框架可以将一个大任务拆分为很多小任务来异步执行。Fork/Join 框架主要包含三个模块:
- 线程池:ForkJoinPool
- 任务对象:ForkJoinTask
- 执行任务的线程:ForkJoinWorkerThread
6.8.1 工作原理-分治法
分治法,ForkJoinPool 主要使用分治法来解决问题。典型的应用比如快速排序算法,ForkJoinPool需要使用相对少的线程来处理大量任务。比如要对 1000万个数据进行排序,那么会将这个任务分割成两个 500万的排序任务和一个针对这两组 500万数据的合并任务。以此类推,对于 500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于 10时,会停止分割,转而使用插入排序对它们进行排序。那么到最后,所有的任务加起来会有大概 20000000+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。
6.8.2 工作原理-工作窃取法
Fork/Join 最核心的地方就是利用了现代硬件设备多核,在一个操作时候会有空闲的 CPU,那么如何利用好这个空闲的 CPU 就成了提高性能的关键,而这里我们要提到的工作窃取(work-stealing)算法就是整个 Fork/Join 框架的核心理念,Fork/Join工作窃取算法是指某个线程从其他队列里窃取任务来执行。
假如我们需要做一个比较大的任务,我们可以把这个人物分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如 A 线程负责处理 A 队列里的任务。但是有线程会先把自己队列里的任务干完,而其他线程对队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
上文中提到 Java8 引入了自动并行化的概念。它能够让一部分 Java 代码自动地以并行的方式执行,也就是我们使用了 ForkJoinPool 的 ParallelStream。
对于 ForkJoinPool 通用线程池的线程数量,通常使用默认值就可以了,即运行时计算机的处理器数量。可以通过设置系统属性:java.util.concurrent.ForkJoinPool.common.parallelism=N(N为线程数量),来调整 ForkJoinPool 的线程数量,可以尝试调整成不同的参数来观察每次的输出结果。
7. Option类
主要解决空指针的问题
Option 是一个没有子类的工具类,Option 是一个可以为 null 的容器对象,它的主要作用就是为了避免 Null检查,防止 NullpointException
public final class Optional<T> {}
7.1 创建 Option 对象
public static void main(String[] args) {
// 1. 通过 of 方法,of 方法是不支持 null的
Optional<String> op1 = Optional.of("张三");
Optional<String> op2 = Optional.of(null);// 报错
// 2. 通过 ofNullable 方法,支持 null
Optional<String> op3 = Optional.ofNullable("张三");
Optional<String> op4 = Optional.ofNullable(null);
// 3. 通过 empty 方法直接创建一个空的 Optional 对象
Optional<String> op5 = Optional.empty();
}
7.2 常用方法
/**
* 常用方法
* get():如果 Optional 有值则返回,否则抛出 NoSuchElementException 异常
* get() 通常和 isPresent 方法一块使用
* isPresent(): 判断是否包含值,包含则返回 true,否则返回 false
* orElse(T t):如果调用对象包含值,返回该值,否则返回 t
* orElseGet(Supplier s):如果调用对象包含值就返回该值,否则返回 Lambda 表达式的返回值
*/
@Test
public void test() {
Optional<String> op1 = Optional.of("张三");
Optional<String> op2 = Optional.empty();
// 获取 Optional 中的值
if (op1.isPresent()) {
String s1 = op1.get();
System.out.println("用户名称:" + s1);
}
if (op2.isPresent()) {
System.out.println(op2.get());
} else {
System.out.println("op2 是一个空的Optional对象");
}
String s3 = op1.orElse("李四");
System.out.println(s3);
String s4 = op2.orElse("王五");
System.out.println(s4);
String s5 = op2.orElseGet(() -> "没有值");
System.out.println(s5);
}
@Test
public void test02() {
Optional<String> op1 = Optional.of("张三");
Optional<String> op2 = Optional.empty();
// 如果存在值就做一些什么操作
op1.ifPresent(s -> System.out.println("有值:" + s));
}
/*
自定义方法,将 User 对象中的 name 转换为大写并返回
*/
@Test
public void test03() {
User user = new User("zhangsan", 88);
Optional<User> op = Optional.of(user);
String nameForObject = getNameForObject(op);
System.out.println(nameForObject);
}
/*
根据 User 对象,将 name 转换为大写并返回
通过 Optional 方式实现
*/
public String getNameForObject(Optional<User> op) {
if (op.isPresent()) {
String res = op.map(User::getName)
.map(String::toUpperCase)
.orElse("空值");
return res;
}
return null;
}
// 原始写法
/**
* 根据 User 对象,将name转换成大写并且返回
*/
public String nameToUpper(User user) {
if (user != null) {
String name = user.getName();
if (name != null) {
return name;
} else return "空值";
} else {
return "空值";
}
}
8. 新时间日期 API
8.1 旧版问题
- 设计不合理,在 java.util 和 java.sql 的包中都有日期类,java.util.Date 同时包含日期和时间的,而 java.sql.Date 仅仅包含日期,此外用于格式化和解析的类在 java.text 包下。
- 非线程安全,java.util.Date 是非线程安全的,所有的日期类都是可变的
- 时区处理麻烦,日期类并不提供国际化,没有时区支持
8.2 新版日期 API 介绍
JDK8 中增加了一套全新的日期时间 API,设计合理,线程安全。新的日期及时间API 位于 java.time 包中,下面是一些关键类
- LocalDate:表示日期,包含年月日,格式为 2019-10-16
- LocalTime:表示时间,包含时分秒格式为 16.38.54. 158549300
- LocalDateTime:表示日期时间,包含年月日,时分秒,格式为 2018-09-06T15:33:56.750
- DateTimeFormatter:日期时间格式化类
- Instant:时间戳,表示一个特定的时间瞬间
- Duration:用于计算 2 个时间(LocalTime,时分秒)的距离
- Period:用于计算 2 个日期(LocalDate,年月日)的距离
- ZonedDateTime:包含时区的时间
Java 中使用的历法是 ISO 8601 日历系统,它是世界民用历法,也就是我们常说的公历。平年有 365天,闰年有 366天。此外 Java8 还提供了4套其他历法,分别是:
- ThaiBuddhistDate:泰国佛教历
- MinguoDate:中国民国历
- JapaneseDate:日本历
- HijrahDate:伊斯兰历
8.3 日期时间常用操作
- 对日期的操作
public static void main(String[] args) {
// 创建指定日期
LocalDate date1 = LocalDate.of(2022,4,1);
System.out.println(date1);
// 获得当前日期
LocalDate now = LocalDate.now();
System.out.println(now);
// 获得日期中的相关信息
System.out.println("年份:" + now.getYear());
System.out.println("月份:" + now.getMonth().getValue());
System.out.println("日:" + now.getDayOfMonth());
System.out.println("星期:" + now.getDayOfWeek().getValue());
}
- 对时间的操作
@Test
public void test1() {
// 获得指定的时间
LocalTime time = LocalTime.of(6,30,00,00);
System.out.println(time);
// 获得当前的时间
LocalTime time1 = LocalTime.now();
System.out.println(time1);
// 获得当前时间的相关信息
System.out.println(time1.getHour());
System.out.println(time1.getMinute());
System.out.println(time1.getSecond());
System.out.println(time1.getNano());
}
- 日期时间
@Test
public void test2() {
// 获得指定的日期时间
LocalDateTime dateTime = LocalDateTime.of(LocalDate.now(), LocalTime.now());
System.out.println(dateTime);
// 获得当前的日期时间
LocalDateTime dateTime1 = LocalDateTime.now();
System.out.println(dateTime1);
}
8.4 日期时间的修改
@Test
public void test3() {
LocalDateTime dateTime = LocalDateTime.now();
// 修改日期时间,对日期时间的修改,对已存在的 LocalDate 对象,创建了它的模板,并不会修改原来的信息
LocalDateTime updDateTime = dateTime.withYear(1998);
System.out.println("now: " + dateTime);
System.out.println("upd: " + updDateTime);
// 在当前日期的基础上,加上或者减去指定时间
System.out.println("两天后:" + dateTime.plusDays(5));
// 减去10年
System.out.println("减去10年:" + dateTime.minusYears(10));
}
// 日期时间的比较
@Test
public void test04() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime date = LocalDateTime.of(2022,1,3,1,3,4);
// 在 JDK8 中要实现日期的比较 isAfter、isBefore、isEqual 通过这几个方法来直接比较
System.out.println(now.isAfter(date));
System.out.println(now.isEqual(date));
}
注意:在进行日期时间的修改的时候,原来的 LocalDate 对象是不会被修改的,每次操作都是返回一个新的 LocalDate 对象,所以在多线程场景下是线程安全的。
8.5 格式化
@Test
public void test05() {
LocalDateTime now = LocalDateTime.now();
// 指定格式 使用系统默认的格式
DateTimeFormatter isoLocalDateTime = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
// 将日期转换成字符
String format = now.format(isoLocalDateTime);
System.out.println("系统默认转换 = " + format);
// 通过 ofPattern 方法指定特定的格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String format1 = formatter.format(now);
System.out.println("自定义格式 = " + format1);
// 解析日期
LocalDateTime parse = LocalDateTime.parse("1999-01-01 22:22:22", formatter);
System.out.println("parse = " + parse);
}
8.6 计算日期时间差
JDK8 中提供了两个工具类 Duration/Period:计算日期时间差
- Duration:用来计算两个时间差(LocalTime)
- Period:用来计算两个日期差(LocalDate)
public static void main(String[] args) {
// 计算时间差
LocalTime now = LocalTime.now();
LocalTime time = LocalTime.of(22, 40, 12);
System.out.println("now = " + now);
System.out.println("time = " + time);
// 通过 Duration 计算时间差
Duration duration = Duration.between(now, time);
System.out.println(duration.toDays());
System.out.println(duration.toHours());
System.out.println(duration.toMinutes());
System.out.println(duration.toMillis());
}
@Test
public void test01() {
// 计算日期差
LocalDate nowDate = LocalDate.now();
LocalDate date = LocalDate.of(2001,03,20);
Period period = Period.between(date, nowDate);
System.out.println(period.getYears());
System.out.println(period.getMonths());
System.out.println(period.getDays());
}
8.7 时间校正器
有时候我们可能需要如下调整:将日期调整到 下个月的第一天 等操作。这是我们可以通过时间校正器获得更好的效果。
- TemporalAdjuster 时间校正器
- TemporalAdjusters 通过该类的静态方法提供了大量TemporalAdjuster的常用操作,简化操作
@Test
public void test02() {
LocalDateTime now = LocalDateTime.now();
// 将当前的日期调整到下个月的一号
TemporalAdjuster adjuster = (temporal -> {
LocalDateTime dateTime = (LocalDateTime) temporal;
LocalDateTime nextMonth = dateTime.plusMonths(1).withDayOfMonth(1);
System.out.println("nextMonth = " + nextMonth);
return nextMonth;
});
// LocalDateTime nextMonth = now.with(adjuster);
LocalDateTime nextMonth = now.with(TemporalAdjusters.firstDayOfNextMonth());
System.out.println(nextMonth);
}
8.8 日期时间的时区
Java8 中加入了对时区的支持,LocalDate、LocalTime、LocalDateTime 是不带时区的,带时区的日期时间类分别为:ZonedDate、ZonedTime、ZonedDateTime。
其中每个时区都对应着 ID, ID 的格式为 “区域/城市”,例如:Asia/Shanghai 等。
ZonedId:该类中包含了所有的时区信息。
public static void main(String[] args) {
// 获得所有时区id
ZoneId.getAvailableZoneIds().forEach(System.out::println);
// 获得当前时间,中国时区,东八区时区,比标准时间早8小时
LocalDateTime now = LocalDateTime.now();
System.out.println("now = " + now);
// 获得标准时间
ZonedDateTime bzsj = ZonedDateTime.now(Clock.systemUTC());
System.out.println("bzsj = " + bzsj);
// 使用计算机默认时区,创建日期时间
ZonedDateTime now1 = ZonedDateTime.now();
System.out.println("now1 = " + now1);
// 使用指定的时区创建日期时间
ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("America/Marigot"));
System.out.println("now2 = " + now2);
}
新日期API 优势
- 新版日期时间 API 中,日期和时间对象是不可变,操作日期不会影响原来的值,而是生成一个新的实例
- 提供不同的两种方式,有效的区分了人和机器的操作
- TemporalAdjuster 可以更精确的操作日期,还可以自定义日期调整器
- 线程安全
**学习参考视频:**https://www.bilibili.com/video/BV1HV411W78K?p=1