JDK8新特性知识点总结


一个简洁的博客网站: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 是完全开源的,可以自由使用。

image-20220425214923084

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());
}

代码分析:

  1. Thread 类需要一个 Runnable 接口作为参数,其中的抽象方法 run 方法是用来执行线程任务的核心
  2. 为了执行 run() 方法,不得不需要 Runnable 的实现类
  3. 为省去定义 Runnable 实现类,不得不使用匿名内部类
  4. 必须覆盖重写抽象的 run 方法,所有的方法名称、参数、返回值不得不重写一遍
  5. 实际上,只在乎方法体的代码
2.2 使用

Lambda 表达式是一个匿名内部类,可以理解为一段可以传递的代码

new Thread(() -> {
    System.out.println("Lambda执行线程:" + Thread.currentThread());
}).start();

优点:简化匿名内部类使用,语法更加简单。

匿名内部类语法冗余,Lambda表达式是简化匿名内部类的一种方式。

2.3 语法规则

省去了面向对象的一些约束

(参数类型 参数名称,...) -> {方法体}
2.4 练习1

无参 Lambda 表达式

  1. 首先创建一个 UserService 方法
public interface UserService {
    void show();
}
  1. 调用该方法
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 表达式

  1. 创建一个 User 实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String name;
    private Integer age;
}
  1. 在主类中创建三个对象加入到 list 集合中
  2. 不适用 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);
    }
}
  1. 可以看到上面 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 原理
  1. 匿名内部类的本质就是在编译的时候生成一个 Class 文件,类名$1.class
// 该代码执行完成之后
// 创建一个线程,执行一段代码
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("子线程执行:" + Thread.currentThread());
    }
}).start();

image-20220425224831219

通过反编译可以看到

package com.lss.jdk8NewFeatures.lambda;

final class Demo01$1 implements Runnable {
    Demo01$1() {
    }

    public void run() {
        System.out.println("子线程执行:" + Thread.currentThread());
    }
}
  1. Lambda 表达式

写有 Lambda 表达式的 class 文件不能反编译工具 Xjad进行查看,可以通过 JDK 自带的工具:javap 对字节码进行反汇编操作。

javap -c -p 文件名.class

-c:表示对代码进行反汇编
-p:显示所有类和成员

image-20220425225927028

在反编译的源码中看到了一个静态方法 lambda$main$0(),在代码体其实就是调用这个方法

匿名内部类在编译的收会产生一个 class 文件

Lambda 表达式在程序运行的时候会形成一个类

  1. 在类中新增一个方法,这个方法的方法体就是 Lambda 表达式中的代码
  2. 还会生成一个匿名内部类,实现接口,重写抽象方法
  3. 在接口中重写方法会调用新生成的方法
2.8 省略写法
  1. 小括号内的参数类型可以省略
  2. 如果小括号里面有且只有一个参数,小括号可以省略
  3. 如果花括号中有且只有一个语句,可以省略花括号,return 关键字以及语句分号。
getShow(() -> System.out.println("调用了UserService接口"));
getStudent( name -> System.out.println(name));

Lambda 使用要求:方法的参数或局部变量必须为接口才能使用 Lambda、接口中有且仅有一个抽象方法(@FunctionalInteface)

Lambda 和 匿名内部类对比

  1. 类型不一样
    • 匿名内部类的类型可以是类、抽象类、接口
    • Lambda表达式需要的类型必须是接口
  2. 方法的数量不一样
    • 匿名内部类所需的接口中的抽象方法的数量是随意的
    • Lambda 表达式所需的接口中只能有一个抽象方法
  3. 实现原理不一样
    • 匿名内部类是在编译后生成一个 Class文件
    • Lambda 表达式是在程序运行的时候动态生成 Class 文件

3. 接口中新增的方法

3.1 接口新增

在 JDK8 中对接口进行了增强,接口中可以有默认方法静态方法

interface 接口名{
    静态常量;
    抽象方法;
    默认方法;
    静态方法;
}
3.2 默认方法
3.2.1 为什么要新增默认方法

在 JDK8 以前的接口中只能由抽象方法和静态常量,会出现一下问题:

如果接口中添加了一个抽象方法,那么它所有的实现类都需要重写这个抽象方法,非常地不利于接口的扩展。

image-20220426085031484

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接口的默认方法

接口中的默认方法有两种使用方式:

  1. 实现类直接调用接口的默认方法
  2. 实现类重写接口的默认方法
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("执行了接口中的静态方法");
    }

}
  1. 默认方法通过实例调用,静态方法通过接口名调用
  2. 默认方法可以继承,实现类可以直接调用接口默认方法,也可以重写接口默认方法
  3. 静态方法不能被继承,实现类不能重写接口的静态方法,只能使用接口名调用

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 包中定义了一些函数式接口,在之后的使用中不一定需要每次都定义一个接口

image-20220426094645757

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 表达式所要实现的方案,已经有其他方法存在相同的方案,那么可以使用方法应用。

常见的引用方式:

  1. instanceName::methodName 对象名::方法名
  2. ClassName::staticMethodName 类名::静态方法
  3. ClassName::methodName 类名::普通方法
  4. ClassName::new 类名:::new 调用的构造器
  5. 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 集合处理的弊端

需要对一个集合进行查询的处理,则需要写好几个循环才能实现

  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);
    }
}
  1. 使用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 能够让我们快速完成许多复杂的操作:筛选、切片、映射、查找、去重、统计、匹配和归约等。

image-20220426114732876

image-20220426114743337

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 类型的方法,支持链式调用。(除了终结方法外,其余方法均为非终结方法)

注意:

  1. Stream 只能操作一次。
  2. Stream 方法返回的是新的流。
  3. 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

过滤数据,返回符合条件的数据。从下图可以看出,将一个流转成成另一个子集流

image-20220426135407491

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

image-20220426135909000

如果希望跳过前面的几个元素,可以使用 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)

image-20220426140208792

该接口需要 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

image-20220426143234302

如果需要找到某些元素或者数据,可以使用该方法实现

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 方法

image-20220426143828330

如果需要将所有数据归纳得到一个数据,可以使用该方法

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);
}

image-20220426144302949

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

image-20220426145940874

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 案例练习

需求:

  1. 第一个队伍只保留姓名长度为 3 的成员
  2. 第一个筛选之后只要前 3 个
  3. 第二个队伍只要姓张的成员
  4. 第二个队伍筛选之后不要前两个人
  5. 将两个队伍合并为一个队伍
  6. 根据姓名创建 User 对象
  7. 打印整个队伍的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);
    }

}

image-20220426154250226

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);
}

image-20220426155819722

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);
}

image-20220426161453080

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 ));
}

image-20220426162158168

  • 多级分组,先根据 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);
        });
    });
}

image-20220426162841446

6.6.5 对流中的数据做分区操作

Collectors.partitioningBy 会根据值是否为 true, 把集合中的数据分割为两个列表, 一个 true 列表,一个 false 列表

image-20220426163054228

@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));
}

image-20220426163312600

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);
}

image-20220426163623008

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();
}

image-20220426164155986

并行流

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();
}

image-20220426164732370

测试

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());
}

image-20220426171858508

image-20220426171908001

要么执行成功,结果不对,要么会抛出异常。

针对问题解决方案:

  1. 加同步锁
list.parallelStream().forEach(s -> {
    synchronized (obj) {
        listNew.add(s);
    }
});
  1. 使用线程安全的容器
@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());
}
  1. 将线程不安全的容器转换成线程安全的容器
@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());
}
  1. 通过 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 框架主要包含三个模块:

  1. 线程池:ForkJoinPool
  2. 任务对象:ForkJoinTask
  3. 执行任务的线程:ForkJoinWorkerThread

image-20220426203933960

6.8.1 工作原理-分治法

分治法,ForkJoinPool 主要使用分治法来解决问题。典型的应用比如快速排序算法,ForkJoinPool需要使用相对少的线程来处理大量任务。比如要对 1000万个数据进行排序,那么会将这个任务分割成两个 500万的排序任务和一个针对这两组 500万数据的合并任务。以此类推,对于 500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于 10时,会停止分割,转而使用插入排序对它们进行排序。那么到最后,所有的任务加起来会有大概 20000000+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。

image-20220426204401175

6.8.2 工作原理-工作窃取法

Fork/Join 最核心的地方就是利用了现代硬件设备多核,在一个操作时候会有空闲的 CPU,那么如何利用好这个空闲的 CPU 就成了提高性能的关键,而这里我们要提到的工作窃取(work-stealing)算法就是整个 Fork/Join 框架的核心理念,Fork/Join工作窃取算法是指某个线程从其他队列里窃取任务来执行。

image-20220426204941101

假如我们需要做一个比较大的任务,我们可以把这个人物分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如 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);
}

image-20220426213713238

@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 旧版问题
  1. 设计不合理,在 java.util 和 java.sql 的包中都有日期类,java.util.Date 同时包含日期和时间的,而 java.sql.Date 仅仅包含日期,此外用于格式化和解析的类在 java.text 包下。
  2. 非线程安全,java.util.Date 是非线程安全的,所有的日期类都是可变的
  3. 时区处理麻烦,日期类并不提供国际化,没有时区支持
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 日期时间常用操作
  1. 对日期的操作
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());
}
  1. 对时间的操作
@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());
}
  1. 日期时间
@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);
}

image-20220426225939588

8.6 计算日期时间差

JDK8 中提供了两个工具类 Duration/Period:计算日期时间差

  1. Duration:用来计算两个时间差(LocalTime)
  2. 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());
}

image-20220427084817653

@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);
}

image-20220427093159967

新日期API 优势

  1. 新版日期时间 API 中,日期和时间对象是不可变,操作日期不会影响原来的值,而是生成一个新的实例
  2. 提供不同的两种方式,有效的区分了人和机器的操作
  3. TemporalAdjuster 可以更精确的操作日期,还可以自定义日期调整器
  4. 线程安全

**学习参考视频:**https://www.bilibili.com/video/BV1HV411W78K?p=1

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值