前言Java8 是 Oracle 公司在 2014 年 3 ⽉发布的版本,是 Java5 之后最重要的版本,带来了诸多⽅⾯的新特性,包括语⾔、类库、编译器及 JVM 等新特性。本⽂重点介绍 Java8 中语法相关的新特性,主要包括 Lambda 表达式
、Stream API
、New Date API
、Optional
等。
Lambda 表达式 什么是 LambdaLambda 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 2 3 4 5 6 7 8 9 10 11 12 13 14 ExecutorService executorService = Executors.newFixedThreadPool(2 );executorService.execute(new Runnable () { @Override public void run () { System.out.println("查询材料库存数!" ); } }); executorService.execute(new Runnable () { @Override public void run () { System.out.println("查询材料门店销量!" ); } });
这种方式写出的代码十分冗长,实际我们想要执行的其实就是 run 方法中的功能,Java8 提供了 Lambda 表达式来解决这个问题,经过 Lambda 表达式改造的代码清晰简洁:
1 2 3 4 ExecutorService executorService = Executors.newFixedThreadPool(2 );executorService.execute(() -> System.out.println("查询材料库存数!" )); executorService.execute(() -> System.out.println("查询材料门店销量!" ));
如何使用 LambdaLambda 表达式由参数、箭头和主体组成。Lambda 的基本语法如下,有两种基本形式:
1 2 3 4 5 6 7 8 9 10 (parameters) -> expression executorService.execute(() -> System.out.println("查询材料库存数!" )); (parameters) -> { statements; } executorService.execute(() -> { System.out.println("查询材料门店销量!" ); System.out.println("查询材料门店销量!" ); });
参数列表:函数式接口
中的抽象方法对应的参数列表,前文例子中函数式接口为 Runnable 接口,抽象方法为 run 方法,为空参数方法; 箭头:Lambda 表达式的标志符号,用来分隔参数列表和 Lambda 主体; Lambda 主体:功能代码块,多行需要使用 花括号 {}
; 那么究竟在哪里可以使用 Lambda 表达式——在函数式接口上使用 Lambda 表达式,前文示例中的 Runnable 接口就是一个函数式接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @FunctionalInterface public interface Runnable { public abstract void run () ; }
函数式接⼝那么什么是函数式接口呢?简单来说,就是 只定义了一个抽象方法的接口
。Lambda 表达式允许直接以内联的方式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。
在 Java8 中,提供了 @FunctionalInterface
注解来专门表示函数式接口,该注解不是必须的,但是添加了该注解编译器会进行语法检查,保证接口中只能包含一个抽象方法。
观察下如下接口是否符合函数式接口的定义?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @FunctionalInterface public interface Runnable { public abstract void run () ; } @FunctionalInterface public interface Callable <V> { V call () throws Exception; } @FunctionalInterface public interface Comparator <T> { int compare (T o1, T o2) ; boolean equals (Object obj) ; default Comparator<T> reversed () { return Collections.reverseOrder(this ); } ... public static <T extends Comparable <? super T>> Comparator<T> reverseOrder () { return Collections.reverseOrder(); } ... }
为什么 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> T -> boolean IntPredicate, LongPredicate, DoublePredicate 断言型接口 Consumer<T> T -> void IntConsumer, LongConsumer, DoubleConsumer 消费型接口 Function<T, R> T -> R IntFunction<R>, IntToDoubleFunction, IntToLongFunction, LongFunction<R>, LongToDoubleFunction, LongToIntFunction, DoubleFunction<R>, ToIntFunction<T>, ToDoubleFunction<T>, ToLongFunction<T> 函数型接口 Supplier<T> () -> T BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier 供给型接口 UnaryOperator<T> T -> T IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator 一元操作型接口 BinaryOperator<T> (T, T) -> T IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator 二元操作型接口 BiPredicate<L, R> (L, R) -> boolean 二元断言型接口 BiConsumer<T, U> (T, U) -> void ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T> 二元消费型接口 BiFunction<T, U, R> (T, U) -> R ToIntBiFunction<T, U>, ToLongBiFunction<T, U>, ToDoubleBiFunction<T, U> 二元函数型接口
Predicate<T>
:断言型接口,抽象方法为 boolean test(T t)
,传入一个参数,返回一个布尔值。1 2 3 4 5 6 7 Predicate<Integer> predicate = t -> t.equals(30 ); System.out.println(predicate.test(35 )); IntPredicate intPredicate = t -> t > 30 ;System.out.println(intPredicate.test(25 ));
Consumer<T>
:消费型接口,抽象方法为 void accept(T t)
,传入一个参数,没有返回值。1 2 3 4 Consumer<String> consumer = System.out::println; consumer.accept("张三" );
Function<T,R>
:函数型接口,抽象方法为 R apply(T t)
,传入一个参数,返回另一个值。1 2 3 4 Function<Integer, String> function = String::valueOf; System.out.println(function.apply(2020 ));
Supplier<T>
:供给型接口,抽象方法为 T get()
,不传入参数,返回一个结果。1 2 3 Supplier<String> supplier = () -> "2020年世界和平!" ; System.out.println(supplier.get());
UnaryOperator<T>
:一元操作型接口,继承自 Function<T, T>
接口,传入一个参数,返回该参数。1 2 3 4 UnaryOperator<Integer> unaryOperator = t -> t + 1 ; System.out.println(unaryOperator.apply(99 ));
⽅法引⽤&构造⽅法引⽤方法引用是特殊场景下 Lambda 表达式的一种简洁写法。如果某个方法刚好满足了 Lambda 表达式的形式,那么就可以用方法引用来表示 Lambda 表达式。
方法引用&构造方法引用有四类:
类名::静态方法名
——在 lambda 表达式中,调用了某个类的静态方法;对象::实例方法名
——在 lambda 表达式中,调用了某个外部对象的实例方法;类名::实例方法名
——在 lambda 表达式中,调用了 lambda 参数列表中的对象实例方法;类名::new
——在 lambda 表达式中,调用了构造方法创建对象;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 List<String> nums = Lists.newArrayList("-11" , "111" , "23" , "14" , "6" , "18" ); nums.sort(Comparator.comparing(Integer::valueOf)); System.out.println("--类名::静态方法名--" + nums); Supplier<Integer> supplier = nums::size; System.out.println(supplier.get()); nums.sort(Comparator.comparing(String::length)); System.out.println("--类名::实例方法名--" + nums); Function<String, BigInteger> function = BigInteger::new ; System.out.println(function.apply("12345678901234567890" ));
Stream API 什么是 StreamStream API 是对集合功能的增强,借助于 Lambda 表达式,能够极大地提高编程效率和程序可读性。Stream 处理集合数据时,将要处理的元素看做一种流,流在管道中传输,并且可以在管道的节点上处理,包括筛选、去重、排序、聚合等。元素流在管道中经过中间操作的处理,最后由结束操作得到处理结果。
使用 Stream API 具有以下优势:
提升性能——Stream 会记录下过程操作、并对这些操作进行叠加,最后在一个迭代循环中执行所有叠加的操作,减少迭代次数; 代码简洁——函数式编程风格的代码简洁、意图明确; 多核友好——只需调用 parallel()方法,即可实现并行程序,简化编码; 使用 Stram API 前的编码风格:
1 2 3 4 5 6 7 8 9 10 11 List<Staff> staffs = Lists.newArrayList(Staff.builder().name("james" ).age(35 ).build(), Staff.builder().name("wade" ).age(37 ).build(), Staff.builder().name("kobe" ).age(41 ).build(), Staff.builder().name("rose" ).age(31 ).build()); List<Staff> results = Lists.newArrayList(); for (Staff staff : staffs) { if (staff.getAge() <= 35 ) { continue ; } results.add(staff); } System.out.println(results);
使用 Stram API 后的编码风格:
1 2 3 4 5 List<Staff> staffs = Lists.newArrayList(Staff.builder().name("james" ).age(35 ).build(), Staff.builder().name("wade" ).age(37 ).build(), Staff.builder().name("kobe" ).age(41 ).build(), Staff.builder().name("rose" ).age(31 ).build()); List<Staff> streamResults = staffs.stream().filter(staff -> staff.getAge() > 35 ).collect(Collectors.toList()); System.out.println(streamResults);
如何创建 Stream通常创建 Stream 都是调用集合(Collection)类中的 stream()方法或者 parallelStream()方法,可以对应生成串行流和并行流。
1 2 3 4 Lists.newArrayList(123 , 11 , 323 , 2 ).stream().map(num -> num * 2 ).forEach(System.out::println); Stream.of(123 , 11 , 323 , 2 ).map(num -> num * 2 ).forEach(System.out::println);
也可以使用 IntStream、LongStream、DoubleStream 从基本类型创建 Stream,基本类型创建的 Stream 支持一些特殊的结束操作——sum()、average()、max()。
1 2 System.out.println(IntStream.of(123 , 11 , 323 , 2 ).max().orElse(-1 ));
Stream 和 IntStream、LongStream、DoubleStream 之间可以相互装换:
1 2 3 4 Stream.of("123" , "11" , "323" , "2" ).mapToInt(Integer::parseInt).average().ifPresent(System.out::println); 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 操作如下图:
Stream 操作分类 中间操作 无状态 unordered(), filter(), map(), mapToInt(), mapToLong(), mapToDobule(), flatMap(), flatMapToInt(), flatMapToLong(), flatMapToDobule(), peek() 有状态 distinct(), sorted(), limit(), skip() 结束操作 非短路操作 forEach(), forEachOrdered(), toArray(), reduce(), collect(), max(), min(), count() 短路操作 anyMatch(), allMatch(), noneMatch(), findFirst(), findAny()
常⽤的中间操作filter——根据 Predicate 条件,过滤出符合条件的元素: 1 2 3 4 5 Lists.newArrayList(112 , 131 , 323 , 234 , 730 , 177 , -226 , 434 ) .stream().filter(num -> Math.sqrt(Math.abs(num)) > 15 ).forEach(System.out::println);
1 2 3 4 5 List<String> nums = Lists.newArrayList("112" , "131" , "323" , "234" , "730" , "177" , "-226" , "434" ); nums.stream().sorted(Comparator.comparingInt(Integer::parseInt)).forEach(System.out::println);
map——对集合中的每个元素按照 mapper 操作进行转换,转换前后 Stream 中元素的个数不会改变,但元素的类型取决于转换之后的类型: 1 2 3 4 Lists.newArrayList("a1" , "a2" , "a3" , "a4" , "a5" ).stream().map(String::toUpperCase).forEach(System.out::println);
flatMap——map 方法只能把一个对象转换成另一个对象,如果需要将一个对象转换成多个,需要使用 flatMap: 1 2 3 4 5 6 7 8 List<Staff> staffs = Lists.newArrayList(Staff.builder().name("张三" ).age(18 ).hobbies(Lists.newArrayList("篮球" , "足球" , "围棋" )).build(), Staff.builder().name("李四" ).age(27 ).hobbies(Lists.newArrayList("书法" , "围棋" , "乒乓球" )).build(), Staff.builder().name("王五" ).age(33 ).hobbies(Lists.newArrayList("品茶" , "读书" , "篮球" )).build()); Set<String> hobbies = staffs.stream().map(Staff::getHobbies).flatMap(Collection::stream).collect(Collectors.toSet()); System.out.println(hobbies);
常用的结束操作forEach——对每一个元素的执行指定的 action 操作: 1 2 3 4 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 2 3 4 5 6 Map<String, Staff> staffMap = staffs.stream().collect(Collectors.toMap((Staff::getName), Function.identity(), (oldValue, newValue) -> newValue)); System.out.println(staffMap);
将 Stream 元素转换成 map 的时候,需要特别注意:key 必须是唯一的,否则会抛出 IllegalStateException 。如果想主动规避这个问题,需要我们传入一个 merge function,来指定重复的元素映射的方式。也可以使用 Collectors.groupingBy(),按照指定 key 分组的方式来代替:
1 2 3 Map<String, List<Staff>> staffsMap = staffs.stream().collect(Collectors.groupingBy((Staff::getName))); System.out.println(staffsMap);
reduce——reduce 操作可以实现从一组元素中生成一个值,sum()、max()、min()、count()等都是 reduce 操作,将他们单独设为函数方便日常使用。redeue 方法定义了三种重载形式: 第一种方法声明为:Optional<T> reduce(BinaryOperator<T> accumulator);
参数为累加器,返回值为 Optional 对象,通过 accumulator 计算得到一个最终结果,通过 Optional 对象中返回:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Lists.newArrayList("d4" , "c3" , "a1" , "b2" , "f5" ).stream().sorted().reduce((s1, s2) -> { System.out.println("s1:" + s1); System.out.println("s2:" + s2); System.out.println("--------" ); return s1 + "#" + s2; }).ifPresent(System.out::println); s1:a1 s2:b2 -------- s1:a1#b2 s2:c3 -------- s1:a1#b2#c3 s2:d4 -------- s1:a1#b2#c3#d4 s2:f5 -------- a1#b2#c3#d4#f5
第二种方法声明为:T reduce(T identity, BinaryOperator<T> accumulator);
新增了一个初始化类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 System.out.println(Stream.of(1 , 2 , 3 , 4 , 5 ).reduce(10 , (p1, p2) -> { System.out.println("p1:" + p1); System.out.println("p2:" + p2); System.out.println("--------" ); return p1 + p2; })); p1:10 p2:1 -------- p1:11 p2:2 -------- p1:13 p2:3 -------- p1:16 p2:4 -------- p1:20 p2:5 -------- 25
第三种方法声明为:<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
在初始对象和累加器基础上,添加了组合器 combiner。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 System.out.println(Stream.of("d4" , "c3" , "a1" , "b2" , "f5" ).reduce(0 , (o1, o2) -> { String threadName = Thread.currentThread().getName(); System.out.println("BiFunction--" + threadName); System.out.println("o1:" + o1 + "--" + threadName); System.out.println("o2:" + o2 + "--" + threadName); return o1 + o2.length(); }, (o1, o2) -> { String threadName = Thread.currentThread().getName(); System.out.println("BinaryOperator--" + threadName); System.out.println("o1:" + o1 + "--" + threadName); System.out.println("o2:" + o2 + "--" + threadName); return o1 + o2; })); BiFunction--main o1:0 --main o2:d4--main BiFunction--main o1:2 --main o2:c3--main BiFunction--main o1:4 --main o2:a1--main BiFunction--main o1:6 --main o2:b2--main BiFunction--main o1:8 --main o2:f5--main 10
执行以上的案例发现 BinaryOperator 并没有执行,此时的操作与第二种方式类似,我们将 Stream 转换为并行流再尝试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 BiFunction--main o1:0 --main o2:a1--main BiFunction--ForkJoinPool.commonPool-worker-3 o1:0 --ForkJoinPool.commonPool-worker-3 o2:d4--ForkJoinPool.commonPool-worker-3 BiFunction--ForkJoinPool.commonPool-worker-2 o1:0 --ForkJoinPool.commonPool-worker-2 o2:f5--ForkJoinPool.commonPool-worker-2 BiFunction--ForkJoinPool.commonPool-worker-3 o1:0 --ForkJoinPool.commonPool-worker-3 o2:b2--ForkJoinPool.commonPool-worker-3 BinaryOperator--ForkJoinPool.commonPool-worker-3 o1:2 --ForkJoinPool.commonPool-worker-3 o2:2 --ForkJoinPool.commonPool-worker-3 BinaryOperator--ForkJoinPool.commonPool-worker-3 o1:2 --ForkJoinPool.commonPool-worker-3 o2:4 --ForkJoinPool.commonPool-worker-3 BiFunction--ForkJoinPool.commonPool-worker-1 o1:0 --ForkJoinPool.commonPool-worker-1 o2:c3--ForkJoinPool.commonPool-worker-1 BinaryOperator--ForkJoinPool.commonPool-worker-1 o1:2 --ForkJoinPool.commonPool-worker-1 o2:2 --ForkJoinPool.commonPool-worker-1 BinaryOperator--ForkJoinPool.commonPool-worker-1 o1:4 --ForkJoinPool.commonPool-worker-1 o2:6 --ForkJoinPool.commonPool-worker-1 10
发现在并行流中,BinaryOperator 执行了,查阅资料发现,为了避免并行竞争,将每个线程的任务单独维护了一个结果,然后通过组合器 combiner 进行最终结果的合并。
match——用来判断某一种规则是否与流对象匹配。所有的匹配操作都是结束操作,只返回一个 boolean 类型的结果。 1 2 3 4 5 6 7 boolean anyMatch = staffs.stream().anyMatch((staff) -> staff.getName().startsWith("张" ));System.out.println(anyMatch); boolean allMatch = staffs.stream().allMatch((staff) -> staff.getAge().equals(34 ));System.out.println(allMatch); boolean noneMatch = staffs.stream().noneMatch((staff) -> staff.getAge().equals(34 ));System.out.println(noneMatch);
New Date APIJava8 另一项新特性是新的时间和日期 API,它们被包含在 java.time 包中。借助新的时间和日期 API 可以更简洁地处理时间和日期。
为什么需要 New Date API在 Java8 之前的时间和日期 API 有很多缺陷,具体如下:
由于以上这些问题,Java 社区出现了一些第三方时间日期库——Joda-Time,Java8 充分借鉴了 Joda 库的一些优点,提供了一套新的时间和日期 API。
日期&时间类Java8 中常用的日期和时间类主要有 LocalDate、LocalTime、LocalDateTime、Instant、Duration 和 Period。
LocalDate、LocalTime、LocalDateTime LocalDate 类表示一个具体的日期,但不包含具体时间,也不包含时区信息。可以通过 LocalDate 的静态方法 of() 创建一个实例,LocalDate 也包含一些方法用来获取年份、月份、天、星期几等:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 LocalDate localDate = LocalDate.of(2020 , 1 , 10 );System.out.println(localDate.getYear()); System.out.println(localDate.getDayOfYear()); Month month = localDate.getMonth();System.out.println(month); System.out.println(localDate.getDayOfMonth()); System.out.println(localDate.getDayOfWeek()); System.out.println(localDate.lengthOfMonth()); System.out.println(localDate.isLeapYear());
LocalTime 和 LocalDate 类似,他们之间的区别在于 LocalDate 不包含具体时间,而 LocalTime 包含具体时间:
1 2 3 4 5 6 7 8 LocalTime localTime = LocalTime.of(17 , 50 , 40 );System.out.println(localTime.getHour()); System.out.println(localTime.getMinute()); System.out.println(localTime.getSecond());
LocalDateTime 类是 LocalDate 和 LocalTime 的结合体,可以通过 of()方法直接创建,也可以调用 LocalDate 的 atTime() 方法或 LocalTime 的 atDate() 方法将 LocalDate 或 LocalTime 合并成一个 LocalDateTime:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 LocalDateTime localDateTime = LocalDateTime.of(2020 , Month.JANUARY, 10 , 17 , 50 , 40 );LocalDate localDate = LocalDate.of(2020 , Month.JANUARY, 10 );LocalTime localTime = LocalTime.of(17 , 50 , 40 );LocalDateTime combineLocalDateTime = localDate.atTime(localTime);System.out.println(combineLocalDateTime.getYear()); System.out.println(combineLocalDateTime.getMonth()); System.out.println(combineLocalDateTime.getDayOfMonth()); System.out.println(combineLocalDateTime.getHour()); System.out.println(combineLocalDateTime.getMinute()); System.out.println(combineLocalDateTime.getSecond()); LocalDate transferLocalDate = localDateTime.toLocalDate();LocalTime transferLocalTime = localDateTime.toLocalTime();
Instant——Instant 用于表示一个时间戳,可以精确到纳秒,可以使用 now() 方法创建,也可以通过 ofEpochSecond() 方法创建。 1 2 3 4 5 6 7 8 9 10 Instant now = Instant.now();System.out.println(now); Instant instant = Instant.ofEpochSecond(9999 , 1000 );System.out.println(instant);
Duration——Duration 表示一个时间段,可以通过 Duration.between() 或 Duration.of() 方法创建。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Duration duration1 = Duration.of(7 , ChronoUnit.DAYS);Duration duration2 = Duration.of(3000 , ChronoUnit.SECONDS);LocalDateTime start = LocalDateTime.of(2018 , Month.JULY, 3 , 9 , 0 , 0 );LocalDateTime end = LocalDateTime.of(2020 , Month.JANUARY, 13 , 18 , 0 , 0 );Duration duration = Duration.between(start, end);System.out.println(duration.toDays()); System.out.println(duration.toHours()); System.out.println(duration.toMinutes()); System.out.println(duration.getSeconds());
Period——Period 和 Duration 类似,不同之处在于 Period 是以年月日来衡量一个时间段。 1 2 3 4 5 6 7 8 Period period1 = Period.of(2 , 3 , 6 );Period period2 = Period.between(LocalDate.of(2018 , 7 , 3 ), LocalDate.of(2020 , 1 , 13 ));System.out.println(period2.getYears()); System.out.println(period2.getMonths()); System.out.println(period2.getDays());
日期操作和格式化日期操作——常用的日期操作有增减天数、月数,查找本月最后一个周五等操作: 1 2 3 4 5 6 7 8 9 10 11 LocalDate date = LocalDate.of(2019 , 12 , 1 );LocalDate newDate = date.withYear(2020 ).withMonth(1 ).withDayOfMonth(13 );System.out.println(newDate); LocalDate localDate = newDate.plusYears(1 ).minusMonths(1 ).plus(10 , ChronoUnit.DAYS);System.out.println(localDate); System.out.println(LocalDate.now().with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY)));
日期格式化——新的日期 API 中提供了一个 DateTimeFormatter 类用于处理日期格式化操作,日期类中调用 format() 方法,传入 DateTimeFormatter 参数: 1 2 3 4 5 6 7 8 System.out.println(LocalDateTime.now().format(DateTimeFormatter.BASIC_ISO_DATE)); System.out.println(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE)); System.out.println(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME)); System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd" )));
Optional 什么是 Optional在 Optional 出现之前,Java 的 NullPointerException 问题令人头疼,我们需要手动添加很多判空逻辑:
为了减少这样的 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.
如何使用 Optionalof、ofNullable——分别为非 null 值和可为 null 值创建一个 Optional: 1 2 3 4 5 String name = "张三" ;Integer age = LocalDate.now().isAfter(LocalDate.of(2020 , 1 , 10 )) ? null : 0 ;Optional<String> nameOptional = Optional.of(name); Optional<Integer> ageOptional = Optional.ofNullable(age);
isPresent——判断 Optional 中是否存在值,存在则返回 true,不存在则返回 false: 1 2 3 System.out.println(nameOptional.isPresent()); System.out.println(ageOptional.isPresent());
ifPresent——如果存在值则执行函数式接口 Consumer 中的逻辑,否则不操作: 1 2 3 4 5 6 nameOptional.ifPresent(System.out::println); ageOptional.ifPresent(System.out::println);
get——如果有值直接返回,否则抛出 NoSuchElementException 异常: 1 2 3 System.out.println(nameOptional.get());
orElse、orElseGet、orElseThrow——orElse 有值则直接返回,为 null 时返回参数设置的默认值;orElseGet 方法与 orElse 方法类似,只是提供了一个函数式接口 Supplier,用来生成默认值;orElseThrow 允许传入一个 Lambda 表达式,来指定为空时抛出异常信息: 1 2 3 4 5 6 7 System.out.println(nameOptional.orElse("李四" )); System.out.println(ageOptional.orElse(20 )); System.out.println(ageOptional.orElseGet(() -> 20 )); System.out.println(ageOptional.orElseThrow(RuntimeException::new ));
map、flatMap——map 允许传入一个 Function 对原始值进行转化,生成一个新的值,然后返回 Optional;flatMap 用法类似,只是传入的 lambda 表达式要求返回值为 Optional: 1 2 3 nameOptional.map(value -> value.replace("三" , "四" )).ifPresent(System.out::println); nameOptional.flatMap(value -> Optional.of(value.replace("三" , "四" ))).ifPresent(System.out::println);
filter——通过传入的条件 Predicate 对原始值进行过滤,然后返回 Optional: 1 2 System.out.println(nameOptional.filter(value -> value.length() > 2 ).isPresent());
使用 Optional 的注意事项不要将 Optional 作为方法参数传递——使用 Optional 作为方法参数传递,如果使用方法时传递了 null,那么这时候就会 NullPointerException,我们不得不加上非空判断,这样就违背了引入 Optional 的初衷; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static List<Staff> filterStaffByNameAndAge (List<Staff> staffs, String name, Optional<Integer> age) { return staffs.stream() .filter(p -> p.getName().equals(name)) .filter(p -> p.getAge() >= age.orElse(0 )) .collect(Collectors.toList()); } List<Staff> staffs = Lists.newArrayList(Staff.builder().name("张三" ).age(18 ).build(), Staff.builder().name("李四" ).age(27 ).hobbies(Lists.newArrayList("书法" , "围棋" , "乒乓球" )).build(), Staff.builder().name("王五" ).age(35 ).hobbies(Lists.newArrayList("读书" , "篮球" , "爬山" )).build()); filterStaffByNameAndAge(staffs, "李四" , null );
不要将 Optional 作为类中的成员变量,因为 Optional 不支持序列化; 1 2 3 4 5 6 7 8 9 10 Staff staff = Staff.builder().name("张三" ).telephoneNumber(Optional.of("12345678900" )).build();try { ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream ("object.txt" )); outputStream.writeObject(staff); } catch (Exception e) { System.out.println(e.toString()); }
参考文档欢迎关注「端小强的博客 」微信公众号,会不定期分享日常学习和工作经验,欢迎大家关注交流。