Java8是Oracle公司在2014年3月发布的版本,是Java5之后最重要的版本,带了了诸多方面的新特性,包括语言、类库、编译器及JVM等新特性。本文重点介绍Java8中语法相关的新特性,主要包括Lambda表达式、Stream API、New Date API、Optional等。
Java8新特性实战 - 图1

Lambda表达式

什么是Lambda

Lambda expression is a new feature which is introduced in Java 8. A lambda expression is an anonymous function. A function that doesn’t have a name and doesn’t belong to any class. The concept of lambda expression was first introduced in LISP programming language.

Lambda这个概念最早起源于LISP语言,Java8中引入了这个特性,Lambda表达式其实是一个匿名函数,这个函数没有名称并且不属于任何类。

为什么需要Lambda

在Java8之前的版本中,如果我们需要实现行为参数化,必须要将特定的行为包装在某个类中,然后将对象传递给具体方法进行执行。匿名内部类的实现如下:

  1. ExecutorService executorService = Executors.newFixedThreadPool(2);
  2. executorService.execute(new Runnable() {
  3. @Override
  4. public void run() {
  5. System.out.println("查询材料库存数!");
  6. }
  7. });
  8. executorService.execute(new Runnable() {
  9. @Override
  10. public void run() {
  11. System.out.println("查询材料门店销量!");
  12. }
  13. });

这种方式写出的代码十分冗长,实际我们想要执行的其实就是run方法中的功能,Java8提供了Lambda表达式来解决这个问题,经过Lambda表达式改造的代码清晰简洁:

  1. ExecutorService executorService = Executors.newFixedThreadPool(2);
  2. executorService.execute(() -> System.out.println("查询材料库存数!"));
  3. executorService.execute(() -> System.out.println("查询材料门店销量!"));

如何使用Lambda

Lambda表达式由参数、箭头和主体组成。Lambda的基本语法如下,有两种基本形式:

  1. // 单行语句
  2. (parameters) -> expression
  3. executorService.execute(() -> System.out.println("查询材料库存数!"));
  4. // 多行语句
  5. (parameters) -> { statements; }
  6. executorService.execute(() -> {
  7. System.out.println("查询材料门店销量!");
  8. System.out.println("查询材料门店销量!");
  9. });

image.png

  • 参数列表:函数式接口中的抽象方法对应的参数列表,前文例子中函数式接口为Runnable接口,抽象方法为run方法,为空参数方法;
  • 箭头:Lambda表达式的标志符号,用来分隔参数列表和Lambda主体;
  • Lambda主体:功能代码块,多行需要使用花括号{};

那么究竟在哪里可以使用Lambda表达式——在函数式接口上使用Lambda表达式,前文示例中的Runnable接口就是一个函数式接口。

  1. @FunctionalInterface
  2. public interface Runnable {
  3. /**
  4. * When an object implementing interface <code>Runnable</code> is used
  5. * to create a thread, starting the thread causes the object's
  6. * <code>run</code> method to be called in that separately executing
  7. * thread.
  8. * <p>
  9. * The general contract of the method <code>run</code> is that it may
  10. * take any action whatsoever.
  11. *
  12. * @see java.lang.Thread#run()
  13. */
  14. public abstract void run();
  15. }

函数式接口

那么什么是函数式接口呢?简单来说,就是只定义了一个抽象方法的接口。Lambda表达式允许直接以内联的方式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。
在Java8中,提供了@FunctionalInterface注解来专门表示函数式接口,该注解不是必须的,但是添加了该注解编译器会进行语法检查,保证接口中只能包含一个抽象方法。

观察下如下接口是否符合函数式接口的定义?

  1. @FunctionalInterface
  2. public interface Runnable {
  3. public abstract void run();
  4. }
  5. @FunctionalInterface
  6. public interface Callable<V> {
  7. V call() throws Exception;
  8. }
  9. @FunctionalInterface
  10. public interface Comparator<T> {
  11. int compare(T o1, T o2);
  12. boolean equals(Object obj);
  13. default Comparator<T> reversed() {
  14. return Collections.reverseOrder(this);
  15. }
  16. ...
  17. public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {
  18. return Collections.reverseOrder();
  19. }
  20. ...
  21. }

为什么Comparator接口也是函数式接口?可以参考FunctionalInterface注解的javadoc:

If an interface declares an abstract method overriding one of the public methods of java.lang.Object, that also does not count toward the interface’s abstract method count since any implementation of the interface will have an implementation from java.lang.Object or elsewhere. Note that instances of functional interfaces can be created with lambda expressions, method references, or constructor references.

如果一个接口中定义了一个抽象方法——重写Object基类中的公有方法,那么这个抽象方法不会被算入接口抽象方法的计数中,因为任何一个这个接口的实现类本来就会通过继承Object基类来实现该方法。

Java8新增了java.util.function包,在function包中引入了一些常用的函数式接口:

函数式接口 函数描述符 原始类型特化 说明
Predicate T->boolean IntPredicate,
LongPredicate, DoublePredicate
断言型接口
Consumer T->void IntConsumer,
LongConsumer, DoubleConsumer
消费型接口
Function T->R IntFunction,
IntToDoubleFunction,
IntToLongFunction,
LongFunction,
LongToDoubleFunction,
LongToIntFunction,
DoubleFunction,
ToIntFunction,
ToDoubleFunction,
ToLongFunction
函数型接口
Supplier ()->T BooleanSupplier,
IntSupplier,
LongSupplier,
DoubleSupplier
供给型接口
UnaryOperator T->T IntUnaryOperator,
LongUnaryOperator,
DoubleUnaryOperator
一元操作型接口
BinaryOperator (T,T)->T IntBinaryOperator,
LongBinaryOperator,
DoubleBinaryOperator
二元操作型接口
BiPredicate (L,R)->boolean 二元断言型接口
BiConsumer (T,U)->void ObjIntConsumer,
ObjLongConsumer,
ObjDoubleConsumer
二元消费型接口
BiFunction (T,U)->R ToIntBiFunction,
ToLongBiFunction,
ToDoubleBiFunction
二元函数型接口
  • Predicate:断言型接口,抽象方法为boolean test(T t),传入一个参数,返回一个布尔值。
  1. // 断言型接口
  2. Predicate<Integer> predicate = t -> t.equals(30);
  3. System.out.println(predicate.test(35));
  4. // 断言型接口原始类型特化
  5. IntPredicate intPredicate = t -> t > 30;
  6. System.out.println(intPredicate.test(25));
  • Consumer:消费型接口,抽象方法为void accept(T t),传入一个参数,没有返回值。
  1. // 消费型接口
  2. // Consumer<String> consumer = t -> System.out.println(t);
  3. Consumer<String> consumer = System.out::println;
  4. consumer.accept("张三");
  • Function:函数型接口,抽象方法为R apply(T t),传入一个参数,返回另一个值。
  1. // 函数型接口
  2. // Function<Integer, String> function = (t) -> String.valueOf(t);
  3. Function<Integer, String> function = String::valueOf;
  4. System.out.println(function.apply(2020));
  • Supplier:供给型接口,抽象方法为T get(),不传入参数,返回一个结果。
  1. // 生产型接口
  2. Supplier<String> supplier = () -> "2020年世界和平!";
  3. System.out.println(supplier.get());
  • UnaryOperator:一元操作型接口,继承自Function接口,传入一个参数,返回该参数。
  1. // 一元操作型接口
  2. // UnaryOperator<String> unaryOperator = t -> t.toUpperCase();
  3. UnaryOperator<Integer> unaryOperator = t -> t + 1;
  4. System.out.println(unaryOperator.apply(99));

方法引用&构造方法引用

方法引用是特殊场景下Lambda表达式的一种简洁写法。如果某个方法刚好满足了Lambda表达式的形式,那么就可以用方法引用来表示Lambda表达式。
方法引用&构造方法引用有四类:

  • 类名::静态方法名——在lambda表达式中,调用了某个类的静态方法;
  • 对象::实例方法名——在lambda表达式中,调用了某个外部对象的实例方法;
  • 类名::实例方法名——在lambda表达式中,调用了lambda参数列表中的对象实例方法;
  • 类名::new——在lambda表达式中,调用了构造方法创建对象;
  1. List<String> nums = Lists.newArrayList("-11", "111", "23", "14", "6", "18");
  2. // 类名::静态方法名,在lambda表达式中,调用了某个类的静态方法
  3. // nums.sort(Comparator.comparing(num -> Integer.valueOf(num)));
  4. nums.sort(Comparator.comparing(Integer::valueOf));
  5. System.out.println("--类名::静态方法名--" + nums);
  6. // 对象::实例方法名,在lambda表达式中,调用了某个外部对象的实例方法
  7. // Supplier<Integer> supplier = () -> nums.size();
  8. Supplier<Integer> supplier = nums::size;
  9. System.out.println(supplier.get());
  10. // 类名::实例方法名,在lambda表达式中,调用了lambda参数列表中的对象实例方法
  11. // nums.sort(Comparator.comparing(num -> num.length()));
  12. nums.sort(Comparator.comparing(String::length));
  13. System.out.println("--类名::实例方法名--" + nums);
  14. // 类名::new,在lambda表达式中,调用了构造方法创建对象
  15. /*
  16. Function<String, BigInteger> function = new Function<String, BigInteger>() {
  17. @Override
  18. public BigInteger apply(String s) {
  19. return new BigInteger(s);
  20. }
  21. };
  22. */
  23. Function<String, BigInteger> function = BigInteger::new;
  24. System.out.println(function.apply("12345678901234567890"));

Stream API

什么是Stream

Stream API是对集合功能的增强,借助于Lambda表达式,能够极大地提高编程效率和程序可读性。Stream处理集合数据时,将要处理的元素看做一种流,流在管道中传输,并且可以在管道的节点上处理,包括筛选、去重、排序、聚合等。元素流在管道中经过中间操作的处理,最后由结束操作得到处理结果。

使用Stream API具有以下优势:

  • 提升性能——Stream会记录下过程操作、并对这些操作进行叠加,最后在一个迭代循环中执行所有叠加的操作,减少迭代次数;
  • 代码简洁——函数式编程风格的代码简洁、意图明确;
  • 多核友好——只需调用parallel()方法,即可实现并行程序,简化编码;

使用Stram API前的编码风格:

  1. List<Staff> staffs = Lists.newArrayList(Staff.builder().name("james").age(35).build(), Staff.builder().name("wade").age(37).build(),
  2. Staff.builder().name("kobe").age(41).build(), Staff.builder().name("rose").age(31).build());
  3. List<Staff> results = Lists.newArrayList();
  4. // 筛选出年龄大于35岁的员工
  5. for (Staff staff : staffs) {
  6. if (staff.getAge() <= 35) {
  7. continue;
  8. }
  9. results.add(staff);
  10. }
  11. System.out.println(results);

使用Stram API后的编码风格:

  1. List<Staff> staffs = Lists.newArrayList(Staff.builder().name("james").age(35).build(), Staff.builder().name("wade").age(37).build(),
  2. Staff.builder().name("kobe").age(41).build(), Staff.builder().name("rose").age(31).build());
  3. // 使用Stream API进行筛选
  4. List<Staff> streamResults = staffs.stream().filter(staff -> staff.getAge() > 35).collect(Collectors.toList());
  5. System.out.println(streamResults);

如何创建Stream

通常创建Stream都是调用集合(Collection)类中的stream()方法或者parallelStream()方法,可以对应生成串行流和并行流。

  1. // 从List创建Stream
  2. Lists.newArrayList(123, 11, 323, 2).stream().map(num -> num * 2).forEach(System.out::println);
  3. // 直接从Stream创建
  4. Stream.of(123, 11, 323, 2).map(num -> num * 2).forEach(System.out::println);

也可以使用IntStream、LongStream、DoubleStream从基本类型创建Stream,基本类型创建的Stream支持一些特殊的结束操作——sum()、average()、max()。

  1. // 通过IntStream直接创建
  2. System.out.println(IntStream.of(123, 11, 323, 2).max().orElse(-1));

Stream和IntStream、LongStream、DoubleStream之间可以相互装换:

  1. // 从Stream转换成IntStream
  2. Stream.of("123", "11", "323", "2").mapToInt(Integer::parseInt).average().ifPresent(System.out::println);
  3. // 从IntStream转换成Stream
  4. IntStream.of(123, 11, 323, 2).mapToObj(num -> "f6" + num).forEach(System.out::println);

常用Stream操作

Stream操作具有如下特点:

  • Stream操作不会修改原始的数据;
  • 操作无状态,不依赖外部变量,在Stream操作内部引用外部非final变量会报错(外部变量默认final,修改之后会报错);
  • Stream中记录中间操作,并对这些操作进行叠加,最后在一个迭代循环中执行所有叠加的操作,生成结果;

根据Stream操作的执行阶段,可以分为两类:

  • 中间操作:总是会惰式执行,调用中间操作只会生成一个标记了该操作的新Stream,中间操作的结果仍然是Stream,可以继续使用Stream API连续调用。中间操作又可以分为有状态操作和无状态操作,有状态操作是指该操作只有拿到所有元素之后才能继续执行,而无状态操作则不受之前元素的影响;
  • 结束操作:会触发实际计算,计算发生时会把所有中间操作以pipeline的方式执行,这样可以减少迭代次数。结束操作的结果通常是一个非Stream结果,计算完成之后Stream就会失效(只能遍历一次);

常用的Stream操作如下图:
image.png

常用的中间操作

  • filter——根据Predicate条件,过滤出符合条件的元素:
  1. // Stream<T> filter(Predicate<? super T> predicate)
  2. // 过滤绝对值开根号大于15的数字
  3. Lists.newArrayList(112, 131, 323, 234, 730, 177, -226, 434)
  4. .stream().filter(num -> Math.sqrt(Math.abs(num)) > 15).forEach(System.out::println);
  • sorted——对集合中的元素进行排序:
  1. // Stream<T> sorted(Comparator<? super T> comparator)
  2. // 字符串装换成数字排序
  3. List<String> nums = Lists.newArrayList("112", "131", "323", "234", "730", "177", "-226", "434");
  4. nums.stream().sorted(Comparator.comparingInt(Integer::parseInt)).forEach(System.out::println);
  • map——对集合中的每个元素按照mapper操作进行转换,转换前后Stream中元素的个数不会改变,但元素的类型取决于转换之后的类型:
  1. // Stream<R> map(Function<? super T,? extends R> mapper)
  2. // map操作:对集合中的每个元素按照mapper操作进行转换
  3. Lists.newArrayList("a1", "a2", "a3", "a4", "a5").stream().map(String::toUpperCase).forEach(System.out::println);
  • flatMap——map方法只能把一个对象转换成另一个对象,如果需要将一个对象转换成多个,需要使用flatMap:
  1. // Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
  2. // flatMap操作:找出所有员工的兴趣爱好
  3. List<Staff> staffs = Lists.newArrayList(Staff.builder().name("张三").age(18).hobbies(Lists.newArrayList("篮球", "足球", "围棋")).build(),
  4. Staff.builder().name("李四").age(27).hobbies(Lists.newArrayList("书法", "围棋", "乒乓球")).build(),
  5. Staff.builder().name("王五").age(33).hobbies(Lists.newArrayList("品茶", "读书", "篮球")).build());
  6. Set<String> hobbies = staffs.stream().map(Staff::getHobbies).flatMap(Collection::stream).collect(Collectors.toSet());
  7. System.out.println(hobbies);

常用的结束操作

  • forEach——对每一个元素的执行指定的action操作:
  1. // void forEach(Consumer<? super T> action)
  2. // forEach操作:对每一个元素的执行指定的action操作
  3. Lists.newArrayList(112, 131, 323, 234, 730, 177, -226, 434).forEach(System.out::println);
  • collect——collect方法接收一个Collector参数,Collector可以将Stream转换成集合,如List、Set或Map。JDK内置了很多常用的Collector,大多数场景下不需要自己实现:
  1. // <R, A> R collect(Collector<? super T, A, R> collector);
  2. // collect操作:将Stream转换成集合,如:List、Set、Map
  3. // Map<String, Staff> staffMap = staffs.stream().collect(Collectors.toMap((Staff::getName), Function.identity()));
  4. Map<String, Staff> staffMap = staffs.stream().collect(Collectors.toMap((Staff::getName), Function.identity(), (oldValue, newValue) -> newValue));
  5. System.out.println(staffMap);

将Stream元素转换成map的时候,需要特别注意:key必须是唯一的,否则会抛出IllegalStateException 。如果想主动规避这个问题,需要我们传入一个merge function,来指定重复的元素映射的方式。也可以使用Collectors.groupingBy(),按照指定key分组的方式来代替:

  1. // collect操作:按照指定key分组
  2. Map<String, List<Staff>> staffsMap = staffs.stream().collect(Collectors.groupingBy((Staff::getName)));
  3. System.out.println(staffsMap);
  • reduce——reduce操作可以实现从一组元素中生成一个值,sum()、max()、min()、count()等都是reduce操作,将他们单独设为函数方便日常使用。redeue方法定义了三种重载形式:

第一种方法声明为:Optional reduce(BinaryOperator accumulator); 参数为累加器,返回值为Optional对象,通过accumulator计算得到一个最终结果,通过Optional对象中返回:

  1. // 实现#号拼接字符串
  2. // 第一次执行时第一个参数是Stream中的第一个元素,第二个参数是Stream参数中的第二个元素
  3. // 后面每次执行的中间结果赋给第一个参数,然后第二个参数为Stream中的下一个元素,依次执行,最后返回一个Optional
  4. Lists.newArrayList("d4", "c3", "a1", "b2", "f5").stream().sorted().reduce((s1, s2) -> {
  5. System.out.println("s1:" + s1);
  6. System.out.println("s2:" + s2);
  7. System.out.println("--------");
  8. return s1 + "#" + s2;
  9. }).ifPresent(System.out::println);
  10. // 执行结果
  11. s1:a1
  12. s2:b2
  13. --------
  14. s1:a1#b2
  15. s2:c3
  16. --------
  17. s1:a1#b2#c3
  18. s2:d4
  19. --------
  20. s1:a1#b2#c3#d4
  21. s2:f5
  22. --------
  23. a1#b2#c3#d4#f5

第二种方法声明为:T reduce(T identity, BinaryOperator accumulator); 新增了一个初始化类型。

  1. // 第一次执行时第一个参数是指定的初始对象,第二个参数是Stream参数中的第一个元素
  2. // 后面每次执行的中间结果赋给第一个参数,然后第二个参数为Stream中的下一个元素,依次执行,最后返回一个和初始值类型相同的结果
  3. System.out.println(Stream.of(1, 2, 3, 4, 5).reduce(10, (p1, p2) -> {
  4. System.out.println("p1:" + p1);
  5. System.out.println("p2:" + p2);
  6. System.out.println("--------");
  7. return p1 + p2;
  8. }));
  9. // 执行结果
  10. p1:10
  11. p2:1
  12. --------
  13. p1:11
  14. p2:2
  15. --------
  16. p1:13
  17. p2:3
  18. --------
  19. p1:16
  20. p2:4
  21. --------
  22. p1:20
  23. p2:5
  24. --------
  25. 25

第三种方法声明为: U reduce(U identity, BiFunction accumulator, BinaryOperator combiner); 在初始对象和累加器基础上,添加了组合器combiner。

  1. // 第三种方式,求单词长度之和,使用串行流和并行流分别执行
  2. System.out.println(Stream.of("d4", "c3", "a1", "b2", "f5").reduce(0, (o1, o2) -> {
  3. String threadName = Thread.currentThread().getName();
  4. System.out.println("BiFunction--" + threadName);
  5. System.out.println("o1:" + o1 + "--" + threadName);
  6. System.out.println("o2:" + o2 + "--" + threadName);
  7. return o1 + o2.length();
  8. }, (o1, o2) -> {
  9. String threadName = Thread.currentThread().getName();
  10. System.out.println("BinaryOperator--" + threadName);
  11. System.out.println("o1:" + o1 + "--" + threadName);
  12. System.out.println("o2:" + o2 + "--" + threadName);
  13. return o1 + o2;
  14. }));
  15. // 执行结果
  16. BiFunction--main
  17. o1:0--main
  18. o2:d4--main
  19. BiFunction--main
  20. o1:2--main
  21. o2:c3--main
  22. BiFunction--main
  23. o1:4--main
  24. o2:a1--main
  25. BiFunction--main
  26. o1:6--main
  27. o2:b2--main
  28. BiFunction--main
  29. o1:8--main
  30. o2:f5--main
  31. 10

执行以上的案例发现BinaryOperator并没有执行,此时的操作与第二种方式类似,我们将Stream转换为并行流再尝试一下:

  1. BiFunction--main
  2. o1:0--main
  3. o2:a1--main
  4. BiFunction--ForkJoinPool.commonPool-worker-3
  5. o1:0--ForkJoinPool.commonPool-worker-3
  6. o2:d4--ForkJoinPool.commonPool-worker-3
  7. BiFunction--ForkJoinPool.commonPool-worker-2
  8. o1:0--ForkJoinPool.commonPool-worker-2
  9. o2:f5--ForkJoinPool.commonPool-worker-2
  10. BiFunction--ForkJoinPool.commonPool-worker-3
  11. o1:0--ForkJoinPool.commonPool-worker-3
  12. o2:b2--ForkJoinPool.commonPool-worker-3
  13. BinaryOperator--ForkJoinPool.commonPool-worker-3
  14. o1:2--ForkJoinPool.commonPool-worker-3
  15. o2:2--ForkJoinPool.commonPool-worker-3
  16. BinaryOperator--ForkJoinPool.commonPool-worker-3
  17. o1:2--ForkJoinPool.commonPool-worker-3
  18. o2:4--ForkJoinPool.commonPool-worker-3
  19. BiFunction--ForkJoinPool.commonPool-worker-1
  20. o1:0--ForkJoinPool.commonPool-worker-1
  21. o2:c3--ForkJoinPool.commonPool-worker-1
  22. BinaryOperator--ForkJoinPool.commonPool-worker-1
  23. o1:2--ForkJoinPool.commonPool-worker-1
  24. o2:2--ForkJoinPool.commonPool-worker-1
  25. BinaryOperator--ForkJoinPool.commonPool-worker-1
  26. o1:4--ForkJoinPool.commonPool-worker-1
  27. o2:6--ForkJoinPool.commonPool-worker-1
  28. 10

发现在并行流中,BinaryOperator执行了,查阅资料发现,为了避免并行竞争,将每个线程的任务单独维护了一个结果,然后通过组合器combiner进行最终结果的合并。

  • match——用来判断某一种规则是否与流对象匹配。所有的匹配操作都是结束操作,只返回一个boolean类型的结果。
  1. // match操作:用来判断某一种规则是否与流对象匹配
  2. boolean anyMatch = staffs.stream().anyMatch((staff) -> staff.getName().startsWith("张"));
  3. System.out.println(anyMatch);
  4. boolean allMatch = staffs.stream().allMatch((staff) -> staff.getAge().equals(34));
  5. System.out.println(allMatch);
  6. boolean noneMatch = staffs.stream().noneMatch((staff) -> staff.getAge().equals(34));
  7. System.out.println(noneMatch);

New Date API

Java8另一项新特性是新的时间和日期API,它们被包含在java.time包中。借助新的时间和日期API可以更简洁地处理时间和日期。

为什么需要New Date API

在Java8之前的时间和日期API有很多缺陷,具体如下:

  • Java的java.util.Date和java.util.Calendar类易用性差,而且不是线程安全的;
  • 对日期的计算方式繁琐,容易出错——月份是从0开始的,从Calendar中获取的月份需要加一才能表示当前月份;

由于以上这些问题,Java社区出现了一些第三方时间日期库——Joda-Time,Java8充分借鉴了Joda库的一些优点,提供了一套新的时间和日期API。

日期&时间类

Java8中常用的日期和时间类主要有LocalDate、LocalTime、LocalDateTime、Instant、Duration和Period。

  • LocalDate、LocalTime、LocalDateTime

LocalDate类表示一个具体的日期,但不包含具体时间,也不包含时区信息。可以通过LocalDate的静态方法of()创建一个实例,LocalDate也包含一些方法用来获取年份、月份、天、星期几等:

  1. // 初始化日期
  2. LocalDate localDate = LocalDate.of(2020, 1, 10);
  3. // 年份 2020
  4. System.out.println(localDate.getYear());
  5. // 年份中第几天 10
  6. System.out.println(localDate.getDayOfYear());
  7. // 月份 JANUARY
  8. Month month = localDate.getMonth();
  9. System.out.println(month);
  10. // 月份中的第几天 10
  11. System.out.println(localDate.getDayOfMonth());
  12. // 一周的第几天:FRIDAY
  13. System.out.println(localDate.getDayOfWeek());
  14. // 月份的天数 31
  15. System.out.println(localDate.lengthOfMonth());
  16. // 是否为闰年 true
  17. System.out.println(localDate.isLeapYear());

LocalTime和LocalDate类似,他们之间的区别在于LocalDate不包含具体时间,而LocalTime包含具体时间:

  1. // 初始化一个时间:17:50:40
  2. LocalTime localTime = LocalTime.of(17, 50, 40);
  3. // 时:17
  4. System.out.println(localTime.getHour());
  5. // 分:50
  6. System.out.println(localTime.getMinute());
  7. // 秒:40
  8. System.out.println(localTime.getSecond());

LocalDateTime类是LocalDate和LocalTime的结合体,可以通过of()方法直接创建,也可以调用LocalDate的atTime()方法或LocalTime的atDate()方法将LocalDate或LocalTime合并成一个LocalDateTime:

  1. LocalDateTime localDateTime = LocalDateTime.of(2020, Month.JANUARY, 10, 17, 50, 40);
  2. LocalDate localDate = LocalDate.of(2020, Month.JANUARY, 10);
  3. LocalTime localTime = LocalTime.of(17, 50, 40);
  4. LocalDateTime combineLocalDateTime = localDate.atTime(localTime);
  5. // LocalDateTime combineLocalDateTime = localTime.atDate(localDate);
  6. // 从LocalDateTime中获取年月日时分秒
  7. System.out.println(combineLocalDateTime.getYear());
  8. System.out.println(combineLocalDateTime.getMonth());
  9. System.out.println(combineLocalDateTime.getDayOfMonth());
  10. System.out.println(combineLocalDateTime.getHour());
  11. System.out.println(combineLocalDateTime.getMinute());
  12. System.out.println(combineLocalDateTime.getSecond());
  13. // LocalDateTime转化成LocalDate或LocalTime
  14. LocalDate transferLocalDate = localDateTime.toLocalDate();
  15. LocalTime transferLocalTime = localDateTime.toLocalTime();
  • Instant——Instant用于表示一个时间戳,可以精确到纳秒,可以使用now()方法创建,也可以通过ofEpochSecond方法创建。
  1. // Instant可以使用now()方法创建,也可以通过ofEpochSecond方法创建
  2. Instant now = Instant.now();
  3. // 2020-01-12T16:16:41.723Z
  4. System.out.println(now);
  5. // ofEpochSecond方法第一个参数表示从1970-01-01 00:00:00开始到现在的秒数
  6. // ofEpochSecond方法第二个参数表示纳秒数,0~999,999,999
  7. Instant instant = Instant.ofEpochSecond(9999, 1000);
  8. // 1970-01-01T02:46:39.000001Z
  9. System.out.println(instant);
  • Duration——Duration表示一个时间段,可以通过Duration.between()或Duration.of()方法创建。
  1. // 使用of创建Duration,统一一个单位设置
  2. Duration duration1 = Duration.of(7, ChronoUnit.DAYS);
  3. Duration duration2 = Duration.of(3000, ChronoUnit.SECONDS);
  4. // 2018-07-03 09:00:00
  5. LocalDateTime start = LocalDateTime.of(2018, Month.JULY, 3, 9, 0, 0);
  6. // 2020-01-13 18:00:00
  7. LocalDateTime end = LocalDateTime.of(2020, Month.JANUARY, 13, 18, 0, 0);
  8. Duration duration = Duration.between(start, end);
  9. // 总天数
  10. System.out.println(duration.toDays());
  11. // 总小时数
  12. System.out.println(duration.toHours());
  13. // 总分钟数
  14. System.out.println(duration.toMinutes());
  15. // 总秒数
  16. System.out.println(duration.getSeconds());
  • Period——Period和Duration类似,不同之处在于Period是以年月日来衡量一个时间段。
  1. // 创建2年3个月6天的范围,年月日单独字段设置
  2. Period period1 = Period.of(2, 3, 6);
  3. // 从2018-07-03到2020-01-13
  4. Period period2 = Period.between(LocalDate.of(2018, 7, 3), LocalDate.of(2020, 1, 13));
  5. System.out.println(period2.getYears());
  6. System.out.println(period2.getMonths());
  7. System.out.println(period2.getDays());

日期操作和格式化

  • 日期操作——常用的日期操作有增减天数、月数,查找本月最后一个周五等操作:
  1. // 2019-12-01
  2. LocalDate date = LocalDate.of(2019, 12, 1);
  3. // 修改日期为2020-01-13 2020-01-13
  4. LocalDate newDate = date.withYear(2020).withMonth(1).withDayOfMonth(13);
  5. System.out.println(newDate);
  6. // 增加一年,减一个月,加十天 2020-12-23
  7. LocalDate localDate = newDate.plusYears(1).minusMonths(1).plus(10, ChronoUnit.DAYS);
  8. System.out.println(localDate);
  9. // 查找本月最后一个周五 2020-01-31
  10. System.out.println(LocalDate.now().with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY)));
  • 日期格式化——新的日期API中提供了一个DateTimeFormatter类用于处理日期格式化操作,日期类中调用format()方法,传入DateTimeFormatter参数:
  1. // 20200113
  2. System.out.println(LocalDateTime.now().format(DateTimeFormatter.BASIC_ISO_DATE));
  3. // 2020-01-13
  4. System.out.println(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
  5. // 11:02:38.148
  6. System.out.println(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME));
  7. // 2020-01-13
  8. System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));

Optional

什么是Optional

在Optional出现之前,Java的NullPointerException问题令人头疼,我们需要手动添加很多判空逻辑:
image.png

为了减少这样的null值判断,Java8借鉴了Guava Optional,提供了新的Optional容器。根据官方文档定义,Optional是一个容器对象,容器中可能包含也可能不包含一个非空对象。如果对象存在,isPresent()将会返回true,get()方法将会返回一个值。

A container object which may or may not contain a non-null value. If a value is present, isPresent() will return true and get() will return the value.

如何使用Optional

  • of、ofNullable——分别为非null值和可为null值创建一个Optional:
  1. // 使用of为非null值创建Optional,ofNullable
  2. String name = "张三";
  3. Integer age = LocalDate.now().isAfter(LocalDate.of(2020, 1, 10)) ? null : 0;
  4. Optional<String> nameOptional = Optional.of(name);
  5. Optional<Integer> ageOptional = Optional.ofNullable(age);
  • isPresent——判断Optional中是否存在值,存在则返回true,不存在则返回false:
  1. // 使用isPresent判断Optional是否存在值
  2. System.out.println(nameOptional.isPresent());
  3. System.out.println(ageOptional.isPresent());
  • ifPresent——如果存在值则执行函数式接口Consumer中的逻辑,否则不操作:
  1. // nameOptional.ifPresent(value -> System.out.println(value));
  2. nameOptional.ifPresent(System.out::println);
  3. ageOptional.ifPresent(System.out::println);
  4. // 执行结果
  5. // 张三
  • get——如果有值直接返回,否则抛出NoSuchElementException异常:
  1. System.out.println(nameOptional.get());
  2. // NoSuchElementException: No value present
  3. // System.out.println(ageOptional.get());
  • orElse、orElseGet、orElseThrow——orElse有值则直接返回,为null时返回参数设置的默认值;orElseGet方法与orElse方法类似,只是提供了一个函数式接口Supplier,用来生成默认值;orElseThrow允许传入一个Lambda表达式,来指定为空时抛出异常信息:
  1. // orElse设置为空时的默认值
  2. System.out.println(nameOptional.orElse("李四"));
  3. System.out.println(ageOptional.orElse(20));
  4. // orElseGet设置为空时的默认值
  5. System.out.println(ageOptional.orElseGet(() -> 20));
  6. // orElseThrow设置为空时抛出的异常
  7. System.out.println(ageOptional.orElseThrow(RuntimeException::new));
  • map、flatMap——map允许传入一个Function对原始值进行转化,生成一个新的值,然后返回Optional;flatMap用法类似,只是传入的lambda表达式要求返回值为Optional:
  1. // 使用map、flatMap映射得到Optional
  2. nameOptional.map(value -> value.replace("三", "四")).ifPresent(System.out::println);
  3. nameOptional.flatMap(value -> Optional.of(value.replace("三", "四"))).ifPresent(System.out::println);
  • filter——通过传入的条件Predicate对原始值进行过滤,然后返回Optional:
  1. // 使用filter对原始值进行过滤
  2. System.out.println(nameOptional.filter(value -> value.length() > 2).isPresent());

使用Optional的注意事项

  • 不要将Optional作为方法参数传递——使用Optional作为方法参数传递,如果使用方法时传递了null,那么这时候就会NullPointerException,我们不得不加上非空判断,这样就违背了引入Optional的初衷;
  1. /**
  2. * 根据名称过滤员工
  3. *
  4. * @param staffs
  5. * @param name
  6. * @param age
  7. * @return
  8. */
  9. public static List<Staff> filterStaffByNameAndAge(List<Staff> staffs, String name, Optional<Integer> age) {
  10. return staffs.stream()
  11. .filter(p -> p.getName().equals(name))
  12. .filter(p -> p.getAge() >= age.orElse(0))
  13. .collect(Collectors.toList());
  14. }
  15. // 使用Optional的注意事项——不要作为方法参数传递
  16. List<Staff> staffs = Lists.newArrayList(Staff.builder().name("张三").age(18).build(),
  17. Staff.builder().name("李四").age(27).hobbies(Lists.newArrayList("书法", "围棋", "乒乓球")).build(),
  18. Staff.builder().name("王五").age(35).hobbies(Lists.newArrayList("读书", "篮球", "爬山")).build());
  19. filterStaffByNameAndAge(staffs, "李四", null);
  • 不要将Optional作为类中的成员变量,因为Optional不支持序列化;
  1. // 使用Optional的注意事项——不要作为类中的字段,不支持序列化
  2. Staff staff = Staff.builder().name("张三").telephoneNumber(Optional.of("12345678900")).build();
  3. try {
  4. // java.io.NotSerializableException: java.util.Optional
  5. ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("object.txt"));
  6. outputStream.writeObject(staff);
  7. } catch (Exception e) {
  8. System.out.println(e.toString());
  9. }

参考文档