函数式接口

函数式接口(Functional Interface)是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

为保证方法数量不多不少,java8提供了一个专用注解@FunctionalInterface,这样,当接口中声明的 抽象方法 大于或小于一个时就会报错。

Java 8 API 包含了很多内建的函数式接口,在Java8之前 中常用到的比如Comparator或者Runnable接口,这些接口都增加了@FunctionalInterface注解以便能用在Lambda上。

如Runnable接口类:

Java8新特性 - 图1

同时只要接口只定义了一个抽象方法,那它就还是一个函数式接口,哪怕这个接口有好多默认的方法实现,如Comparator接口类:

Java8新特性 - 图2

@FunctionalInterface

这个注解有以下三点:

  1. 注解只能标记在 有且仅有一个抽象方法 的接口上。JDK8接口中的静态方法和默认方法,都不算是抽象方法。
  2. 接口默认继承java.lang.Object,所以如果接口显示声明覆盖了Object中方法,那么也不算抽象方法。
  3. 该注解不是必须的,如果一个接口符合 函数式接口 定义,那么加不加该注解都没有影响。加上该注解能够更好地让编译器进行检查。如果编写的不是函数式接口,但是加上了@FunctionInterface,那么编译器会报错。

下面是是JDK注解的描述:

  1. package java.lang;
  2. import java.lang.annotation.*;
  3. /**
  4. * An informative annotation type used to indicate that an interface
  5. * type declaration is intended to be a <i>functional interface</i> as
  6. * defined by the Java Language Specification.
  7. * [用于指示接口类型声明是由Java语言规范定义的功能接口的信息性注释类型。]
  8. *
  9. * Conceptually, a functional interface has exactly one abstract
  10. * method. Since {@linkplain java.lang.reflect.Method#isDefault()
  11. * default methods} have an implementation, they are not abstract. If
  12. * an interface declares an abstract method overriding one of the
  13. * public methods of {@code java.lang.Object}, that also does
  14. * <em>not</em> count toward the interface's abstract method count
  15. * since any implementation of the interface will have an
  16. * implementation from {@code java.lang.Object} or elsewhere.
  17. * [从概念上讲,功能接口只有一个抽象方法。因为默认方法有一个实现,所以它们不是抽象的。
  18. * 如果一个接口声明了一个抽象方法,那么它将覆盖java.lang的一个公共方法。
  19. * Object,它也不计入接口的抽象方法计数,因为接口的任何实现都有来自java.lang.Object或其他地方的现。]
  20. *
  21. * <p>Note that instances of functional interfaces can be created with
  22. * lambda expressions, method references, or constructor references.
  23. * [注意,函数接口的实例可以用lambda表达式、方法引用或构造函数引用创建。]
  24. *
  25. * <p>If a type is annotated with this annotation type, compilers are
  26. * required to generate an error message unless:
  27. *
  28. * <ul>
  29. * <li> The type is an interface type and not an annotation type, enum, or class.
  30. * <li> The annotated type satisfies the requirements of a functional interface.
  31. * </ul>
  32. * 如果一个类型被注释为该注释类型,编译器需要生成错误消息,除非:
  33. * - 该类型是接口类型,而不是注释类型、枚举或类。
  34. * - 带注释的类型满足功能接口的需求。
  35. *
  36. * <p>However, the compiler will treat any interface meeting the
  37. * definition of a functional interface as a functional interface
  38. * regardless of whether or not a {@code FunctionalInterface}
  39. * annotation is present on the interface declaration.
  40. * [然而,编译器将把任何符合函数接口定义的接口视为函数接口,而不管接口声明中是否存在FunctionalInterface注释。]
  41. *
  42. * jdk version 1.8
  43. */
  44. @Documented
  45. @Retention(RetentionPolicy.RUNTIME)
  46. @Target(ElementType.TYPE)
  47. public @interface FunctionalInterface {}

简而言之一句话,@FunctionalInterface标记在接口上,函数式接口 是指仅仅只包含一个抽象方法的接口,大于小于一个都是不OK的。比如:

  • 正常
  1. package com.chen.exercises4;
  2. @FunctionalInterface
  3. public interface FunctionInterface {
  4. public void eat();
  5. }

Java8新特性 - 图3

  • 没有抽象方法

Java8新特性 - 图4

错误信息:

No target method found . 没有找到目标方法

  • 两个抽象方法

Java8新特性 - 图5

错误信息:

Multiple non-overriding abstract methods found in interface com.chen.exercises4.FunctionInterface

在interface中发现多个非覆盖的抽象方法

小结:只要接口只定义了一个抽象方法,那它就是一个函数式接口,还有在上述Java Api中都有个@FunctionalInterface注解,这表示着该接口会设计成一个函数式接口,不符合规范的话,就会编译报错。

Lambda表达式

什么是Lambda表达式

  1. @Test
  2. public void LambdaTest1(){
  3. List<Person> personList = new ArrayList<>();
  4. personList.add(new Person("1","张三","13"));
  5. personList.add(new Person("2","李四","12"));
  6. personList.add(new Person("3","王五","14"));
  7. for (Person person : personList) {
  8. System.out.println(JSONUtil.toJsonStr(person));
  9. }
  10. //jdk8之前版本的传统操作
  11. personList.sort(new Comparator<Person>() {
  12. @Override
  13. public int compare(Person o1, Person o2) {
  14. return o1.getAge().compareTo(o2.getAge());
  15. }
  16. });
  17. //Lambda表达式写法
  18. personList.sort((Person p1,Person p2) -> p1.getAge().compareTo(p2.getAge()));
  19. System.out.println("-----------------------------------");
  20. for (Person person : personList) {
  21. System.out.println(JSONUtil.toJsonStr(person));
  22. }
  23. }
  1. {"name":"张三","id":"1","age":"13"}
  2. {"name":"李四","id":"2","age":"12"}
  3. {"name":"王五","id":"3","age":"14"}
  4. -----------------------------------
  5. {"name":"李四","id":"2","age":"12"}
  6. {"name":"张三","id":"1","age":"13"}
  7. {"name":"王五","id":"3","age":"14"}

Lambda表达式组成

我们可以把Lambda组成部分分为三部分,以上面排序的例子为例如下图:

  1. (Person p1,Person p2) -> p1.getAge().compareTo(p2.getAge())

Java8新特性 - 图6

  1. 参数列表:本例中是两个Person对象的参数,采用的是Comparator接口中compare方法的参数。
  2. 箭头->把参数列表和主体分隔为两个部分。
  3. 主体:本例中是把比较口罩品牌的表达式作为Lambda表达式的返回。主体可以修改成另外一种写法,含义是一样的:
  1. personList.sort((Person o1,Person o2) -> {
  2. return o1.getAge().compareTo(o2.getAge());
  3. });

Lambda表达式语法

Lambda表达式有两种基本语法,分别如下:

  1. (参数列表) -> 表达式
  2. (参数列表) -> { 多条语句 }

示例分析(在哪使用Lambda表达式)

  1. (Person p1,Person p2) -> p1.getAge().compareTo(p2.getAge())

这里使用的sort方法的参数类型是Comparator<T>,我们就是把Lambda表达式作为Comparator<T>传入sort方法中的。Comparator<T>就是一个函数式接口,函数式接口在上面已经讲过了这里在加单说下:函数式接口就是有且仅有一个抽象方法的接口。

上面提到的Comparator<T>接口,虽然有很多默认方法,但有且仅有一个抽象方法compare,所以它仍然是一个函数式接口。

我们就可以直接使用Lambda表达式为函数式接口提供实现了,并且还可以把整个Lambda表达式作为函数式接口的实例。比如上面提到的Runnable接口,我们就是这样直接赋值:

  1. Runnable runnable = () -> {
  2. System.out.println("测试……");
  3. };

怎样使用Lambda表达式

从上面Runnable接口实例的例子中,可以看出:Runnable接口的run方法没有入参没有返回,该方法的签名是() -> void;Lambda表达式同样的也没有入参没有返回,该表达式的签名是() -> void

也就是说:函数式接口的抽象方法的签名和Lambda表达式的签名必须一致。

再比如,按照年龄给Person列表进行排序的例子,Comparator<T>接口的compare方法的签名是(T ,T) -> int,Lambda表达式的签名同样也是(T ,T) -> int

Lambda类型推断

编译器可以通过函数式接口推断出Lambda表达式的参数类型,所以在编写Lambda表达式时,可以省略参数类型。比如:

  1. personList.sort((Person p1,Person p2) -> p1.getAge().compareTo(p2.getAge()));

简化为:

  1. personList.sort((p1,p2) -> p1.getAge().compareTo(p2.getAge()));

另外,当Lambda表达式只有一个参数的时候,不仅可以省略参数类型,还可以省略到参数名称两边的括号,比如:

  1. (Person p1) -> p1.getAge()

简化为

  1. p1 -> p1.getAge()

方法引用

什么是方法引用?

在学习了 Lambda 表达式之后,我们通常使用 Lambda 表达式来创建匿名方法。然而,有时候我们仅仅是调用了一个已存在的方法。如下。

  1. Arrays.sort((personList, (o1,o2) -> o1.compareToIgnoreCase(o2));

在 Java 8 中,我们可以直接通过方法引用来简写 Lambda 表达式中已经存在的方法。

  1. Arrays.sort((personList, String::compareToIgnoreCase);

这种特性就叫做方法引用(Method Reference)。

方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,可以让我们重复使用现用方法的定义,做为某些Lambda表达式的另一种更简洁的写法。它需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用会创建函数式接口的一个实例。当 Lambda 表达式中只是执行一个方法调用时,不用 Lambda 表达式,直接通过方法引用的形式可读性更高一些。方法引用是一种更简洁易懂的 Lambda 表达式。

当你需要方法引用时,目标引用放在分隔符::前,方法的名称放在分隔符::后。比如,上面的Person::getAge,就是引用了Person中定义的getAge方法。方法名称后不需要加括号,因为我们并没有实际调用它。方法引用提高了代码的可读性,也使逻辑更加清晰。

方法引用的构建

可以构建方法引用的场景的有四种:

  1. 静态方法
    指向静态方法的引用,语法:类名::静态方法名,类名放在分隔符::前,:静态方法名放在分隔符::后。比如:
  1. (String str) -> Integer.parseInt(str)

使用方法引用以后,可以简写为:

  1. 内部对象实例方法
    指向Lambda表达式内部对象的实例方法的引用,语法:类名::实例方法名,类名放在分隔符::前,:实例方法名放在分隔符::后。比如:
  1. (Person person) -> person.getAge()

使用方法引用以后,可以简写为:

  1. 外部对象是实例方法
    指向Lambda表达式外部对象的实例方法的引用,语法:实例名::实例方法名,类名放在分隔符::前,:实例方法名放在分隔符::后。比如:
  1. String type = "TEST";
  2. Predicate<String> predicate = (String str) -> type.equals(str);
  3. System.out.println(predicate.test("TEST"));

其中,type是一个Lambda表达式外部的局部变量,使用方法引用以后,可以简写为:
如果对于Predicate接口还不熟悉,没关系,以后的文章会介绍到,这里暂且知道它的抽象方法的签名是(T) -> boolean就可以了。

  1. 构造方法
    指向构造方法的引用,语法:类名::new,类名放在分隔符::前,new放在分隔符::后。比如:
  1. (String name, String age) -> new Person(name, age)

使用方法引用以后,可以简写为:

案例总结:

  1. 因为(char[] array) -> new String(array)是一个构造方法的Lambda表达式,此种方法引用的语法是:类名::new,所以正确答案是:String::new。
  2. 因为(String str) -> str.length()是一个内部对象的实例方法的Lambda表达式,此种方法引用的语法是:类名::实例方法名,所以正确答案是:String::length。
  3. 因为(String type) -> mask.setType(type)中的mask是一个Mask对象的局部变量,它是一个包含外部对象的Lambda表达式,此种方法引用的语法是:实例名::实例方法名,所以正确答案是mask::setType。
  4. 因为(String str) -> System.out.println(str)是一个静态方法的Lambda表达式,此种方法引用的语法是:类名::静态方法名,所以正确答案是System.out::println。

Stream

Stream在Java中极大增强了集合对象的功能,专注于对集合对象进行方便、高效的聚合操作。另外可以配合Lambda表达式,让代码更加容易理解。另外Stream提供串行和并行两种操作方式,并行操作可以很方便的写出高性能的并发程序。

它允许你以 声明式 的方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。这有点儿像是我们操作数据库一样,例如我想要查询出热量较低的菜品名字我就可以像下面这样:

  1. select name,age from person where age >= 15

和之前使用迭代器一样每个做判断相比,我们只是表达了我们想要什么。那么为什么到了 Java 的集合中,这样做就不行了呢?另外,如果我们想要处理大量的数据又该怎么办?是否是考虑使用多线程进行并发处理呢?如果是,那么可能编写的关于并发的代码比使用迭代器本身更加的复杂,而且调试起来也会变得麻烦。基于以上的几点考虑,Java 设计者在 Java 8 版本中 (真正把函数式编程风格引入到 Java 中),引入了流的概念,来帮助您节约时间!并且配合 Lambda 使用将更加顺畅!

那如何使用流呐?

使用一个Stream流,一般分为三个步骤:

  1. 获取数据源
  2. 中间操作(Intermediate)
  3. 终端操作(Terminal)

中间操作:一个流可以有0或多个中间操作,对数据进行转换、过滤等操作,一个接着一个,这些操作是lazy的,中间操作是还没有开始真正的遍历。

终端操作:一个流只能有一个终端操作,使用终端操作之后就会返回结果,不能再使用这个流了。终端操作时,才真正开始遍历。

在Stream中一个流的多次中间操作不是每一次都进行一次遍历的,中间操作是lazy 的,多个中间操作是最终聚合到终端操作的时候进行的,只进行一次遍历循环。可以理解为每个中间操作被当做一个判断条件加入到终端操作循环中,完成每个元素的数据转换。

Stream流操作的特点

  1. 特点一:内部迭代
  2. 特点二:只能遍历一次
  3. 特点三:方便的并行处理

常用的方法

  • stream: 返回数据流,集合作为其源
  • parallelStream: 返回并行数据流, 集合作为其源
  • filter: 方法用于过滤出满足条件的元素
  • map: 方法用于映射每个元素对应的结果
  • forEach: 方法遍历该流中的每个元素
  • limit: 方法用于减少流的大小
  • sorted: 方法用来对流中的元素进行排序
  • anyMatch: 是否存在任意一个元素满足条件(返回布尔值)
  • allMatch: 是否所有元素都满足条件(返回布尔值)
  • noneMatch: 是否所有元素都不满足条件(返回布尔值)
  • collect: 方法是终端操作,这是通常出现在管道传输操作结束标记流的结束

常用实例

  • stream() − 为集合创建串行流。
  • parallelStream() − 为集合创建并行流。
  1. Filter 过滤
    filter 方法用于通过设置的条件过滤出元素。以下代码片段使用 filter 方法过滤 a 字符开头的数据
  1. @Test
  2. public void streamTest2(){
  3. List<String> strings = Arrays.asList("aaa", "bbb", "ccc", "abc", "bce","cff", "");
  4. Stream<String> stream = strings.stream().filter((s) -> s.startsWith("a"));
  5. stream.forEach(System.out::println);
  6. }
  1. Sort 排序
  1. @Test
  2. public void streamTest2(){
  3. List<String> strings = Arrays.asList("aaa", "bbb", "ccc", "abc", "bce","cff", "");
  4. Stream<String> stream = strings.stream().filter((s) -> s.startsWith("a")).sorted();
  5. stream.forEach(System.out::println);
  6. }
  1. Map映射
  1. @Test
  2. public void streamTest2(){
  3. List<String> strings = Arrays.asList("aaa", "bbb", "ccc", "abc", "bce","cff", "");
  4. Stream<String> stream = strings.stream().map(String::toUpperCase).sorted();
  5. stream.forEach(System.out::println);
  6. }
  7. //打印结果
  8. AAA
  9. ABC
  10. BBB
  11. BCE
  12. CCC
  13. CFF
  1. forEach
    Stream 提供了新的方法 forEach 来迭代流中的每个数据。以下代码片段使用 forEach 输出了10个随机数:
  1. @Test
  2. public void streamTest2(){
  3. List<String> strings = Arrays.asList("aaa", "bbb", "ccc", "abc", "bce","cff", "");
  4. Stream<String> stream = strings.stream().map(String::toUpperCase);
  5. stream.forEach(System.out::println);
  6. }
  1. limit
    limit 方法用于获取指定数量的流。 以下代码片段使用 limit 方法打印出 10 条数据:
  1. Random random = new Random();
  2. random.ints().limit(10).forEach(System.out::println);
  1. Match匹配
  1. @Test
  2. public void streamTest2(){
  3. List<String> strings = Arrays.asList("aaa", "bbb", "ccc", "abc", "bce","cff", "");
  4. boolean anyStartsWithA = strings.stream().anyMatch((s) -> s.startsWith("a"));
  5. System.out.println(anyStartsWithA); // true
  6. boolean allStartsWithA = strings.stream().allMatch((s) -> s.startsWith("a"));
  7. System.out.println(allStartsWithA); // false
  8. boolean noneStartsWithZ = strings.stream().noneMatch((s) -> s.startsWith("z"));
  9. System.out.println(noneStartsWithZ);// true
  10. }
  1. Count 计数
  1. @Test
  2. public void streamTest2(){
  3. List<String> strings = Arrays.asList("aaa", "bbb", "ccc", "abc", "bce","cff", "");
  4. long count = strings.stream().filter((s) -> s.startsWith("a")).count();
  5. System.out.println(count);
  6. }

Optional

Java 应用程序失败的最常见的报错原因,那就不得不说空指针异常了。以前为了解决空指针异常,Google公司著名的 Guava 项目引入了 Optional 类,Guava 通过使用检查空值的方式来防止代码污染,它鼓励程序员写更干净的代码。

Optional 实际上是个容器:它可以保存类型 T 的值,或者仅仅保存 null。Optional 提供很多方法,这样我们就不用显式进行空值检测。

我们下面用个小例子来演示如何使用 Optional 类:

  1. @Test
  2. public void OptionalTest2(){
  3. Person person = getPerson(null);
  4. //实际开发中这里要使用person对象,肯定是要判断是否为null的,如下
  5. if (person != null) {
  6. System.out.println("name is :"+person.getName());
  7. }
  8. }
  9. public Person getPerson(String id){
  10. if (id == null){
  11. return null;
  12. }else{
  13. return new Person("1","TEST","15");
  14. }
  15. }

但是很多时候,我们可能会忘记写 if (person!= null) —— 不会到什么情况下就会导致程序 NullPointerException 哇塞,画面太美,简直不敢继续想下去。

其实Optional主要作用就是解决一层一层的if判断导致代码污染,从而使代码中可以省去了ifelse对null对象的判断。

Optional这是一个可以包含或者不包含非 null 值的容器。如果值存在则 isPresent()方法会返回 true,调用 get() 方法会返回该对象。

Java8新特性 - 图7

构造一个 Optional

Optional的有两个构造方法,都被private修饰。

  1. //无参:无参构造方法用来初始化EMPTY。
  2. private Optional() {
  3. this.value = null;
  4. }
  5. //有参:有参构造方法用来初始化非null对象。
  6. private Optional(T value) {
  7. this.value = Objects.requireNonNull(value);
  8. }

因为构造方法被修饰为私有的,Optional想要实例化对象只能通过类方法调用。Optional提供三个类方法。

  • of:返回 value 非null的Optional对象
  • ofNullable:value的值根据参数是否为null返回对应的Optional对象
  • empty:返回 value为null的Optional对象

1.Optional.of(T value),该方法通过一个非 nullvalue 来构造一个 Optional,返回的 Optional 包含了 value 这个值。对于该方法,传入的参数一定不能为 null,否则便会抛出 NullPointerException

2.Optional.ofNullable(T value),该方法和 of 方法的区别在于,传入的参数可以为 null —— 该方法会判断传入的参数是否为 null,如果为 null 的话,返回的就是 Optional.empty()

3.Optional.empty(),该方法用来构造一个空的 Optional,即该 Optional 中不包含值 —— 其实底层实现还是 如果 **Optional** 中的 value 为 **null** 则该 **Optional** 为不包含值的状态,然后在 API 层面将 Optional 表现的不能包含 null 值,使得 Optional 只存在 包含值不包含值 两种状态。

  1. public static<T> Optional<T> empty() {
  2. Optional<T> t = (Optional<T>) EMPTY;
  3. return t;
  4. }
  5. public static <T> Optional<T> of(T value) {
  6. return new Optional<>(value);
  7. }
  8. public static <T> Optional<T> ofNullable(T value) {
  9. return value == null ? empty() : of(value);
  10. }

Optional实例方法介绍

方法 参数类型 返回类型 说明
get T value值为null抛出NoSuchElementException异常
isPresent boolean value值为null则返回false
ifPresent Consumer<? super T> void 如果Optional实例有值则为其调用consumer,否则不做处理
filter Predicate<? super T> Optional<T> 如果值存在并且满足提供的谓词,就返回包括该值的Optional对象;否则返回一个空的Optional对象
map Function<? super T, ? extends U> Optional<U> 如果值存在,就对该值执行提供的mapping函数调用,返回Optional<U>
对象
flatMap Function<? super T, Optional<U>> Optional<U> 如果值存在,就对该值执行提供的mapping函数调用,返回非null Optional对象
orElse T T 如果有值则将其返回,否则返回一个默认值
orElseGet Supplier<? extends T> T 如果有值则将其返回,否则返回一个由指定的Supplier接口生成的值
orElseThrow Supplier<? extends X> <X extends Throwable> 如果有值则将其返回,否则抛出一个由指定的Supplier接口生成的异常

Optinal使用和配合Lambda解决NPE

这里我们有一个person object,以及一个 person object 的 Optional wrapper:Optional<T> 如果不结合 Lambda 使用的话,并不能使原来繁琐的 null check 变的简单。如下示例:

  1. @Test
  2. public void OptionalTest3(){
  3. Person person = getPerson("true");
  4. //传统判断
  5. if (person != null){
  6. System.out.println(person);
  7. }else{
  8. System.out.println("获取数据失败,值为null");
  9. }
  10. //Optional方式判断
  11. Optional<Person> optionalPerson = Optional.ofNullable(person);
  12. if (optionalPerson.isPresent()){
  13. System.out.println(optionalPerson.get());
  14. }else{
  15. System.out.println("获取数据失败,值为null");
  16. }
  17. }
  18. public Person getPerson(String id){
  19. if (id == null){
  20. return null;
  21. }else{
  22. return new Person("1","TEST","15");
  23. }
  24. }

只有当 Optional<T> 结合 Lambda 一起使用的时候,才能知道代码竟然可以这样简洁明了。参考示例如下

情况一:存在继续

  1. @Test
  2. public void OptionalTest3(){
  3. Person person = getPerson("true");
  4. //传统判断
  5. if (person != null) {
  6. System.out.println(person);
  7. }
  8. //Optional方式判断
  9. Optional<Person> optionalPerson = Optional.ofNullable(person);
  10. optionalPerson.ifPresent(System.out::println);
  11. }

情况二:存在则返回,无则返回不存在

  1. @Test
  2. public void OptionalTest3(){
  3. Person person = getPerson(null);
  4. //传统判断
  5. if (person != null){
  6. System.out.println(JSONUtil.toJsonStr(person));
  7. }else{
  8. System.out.println("获取数据失败,值为null");
  9. }
  10. //Optional方式判断
  11. Optional<Person> optionalPerson = Optional.ofNullable(person);
  12. // orElse 如果有值则将其返回,否则返回一个默认值
  13. Person person1 = optionalPerson.orElse(new Person("4","optionalTest","15"));
  14. System.out.println(JSONUtil.toJsonStr(person1));
  15. }

情况三:存在则返回,吴则由函数生成

  1. @Test
  2. public void OptionalTest3(){
  3. Person person = getPerson(null);
  4. //传统判断
  5. if (person != null){
  6. System.out.println(JSONUtil.toJsonStr(person));
  7. }else{
  8. System.out.println(JSONUtil.toJsonStr(getPersonFunction()));
  9. }
  10. //Optional方式判断
  11. Optional<Person> optionalPerson = Optional.ofNullable(person);
  12. Person person1 = optionalPerson.orElseGet(()->getPersonFunction());
  13. System.out.println(JSONUtil.toJsonStr(person1));
  14. }
  15. public Person getPerson(String id){
  16. if (id == null){
  17. return null;
  18. }else{
  19. return new Person("1","TEST","15");
  20. }
  21. }
  22. public Person getPersonFunction(){
  23. return new Person("5","getPersonFunction","15");
  24. }

情况四:连续NULL检查

  1. @Test
  2. public void OptionalTest3(){
  3. Person person = getPerson("true");
  4. //传统判断
  5. if (person != null){
  6. if (person.getName() != null) {
  7. System.out.println(person.getName().toUpperCase());
  8. }else{
  9. System.out.println("null");
  10. }
  11. }else{
  12. System.out.println("null");
  13. }
  14. //Optional方式判断
  15. Optional<Person> optionalPerson = Optional.ofNullable(person);
  16. String s = optionalPerson.map(p -> p.getName()).map(name -> name.toUpperCase()).orElse(null);
  17. System.out.println(s);
  18. }
  19. public Person getPerson(String id){
  20. if (id == null){
  21. return null;
  22. }else{
  23. return new Person("1","TEST","15");
  24. }
  25. }

传统 Java 的写法显得冗长难懂,而新的 Optional<T> +Lambda 则清新脱俗,清楚简洁。非常推荐大家使用这种方式开发

Date/Time API

Java8之前的日期API

在Java 8之前,所有关于时间和日期的API都存在各种使用方面的缺陷,主要有:

  1. Java的java.util.Date和java.util.Calendar类易用性差,不支持时区,而且他们都不是线程安全的;
  2. 用于格式化日期的类DateFormat被放在java.text包中,它是一个抽象类,所以我们需要实例化一个SimpleDateFormat对象来处理日期格式化,并且DateFormat也是非线程安全,这意味着如果你在多线程程序中调用同一个DateFormat对象,会得到意想不到的结果;
  3. 对日期的计算方式繁琐,而且容易出错,因为月份是从0开始的,从Calendar中获取的月份需要加一才能表示当前月份;

由于以上这些问题,出现了一些第三方的日期处理框架,例如Joda-Time,date4j等开源项目。但是,Java需要一套标准的用于处理时间和日期的框架,于是Java 8中引入了新的日期API。新的日期API是JSR-310规范的实现,Joda-Time框架的作者正是JSR-310的规范的倡导者,所以能从Java 8的日期API中看到很多Joda-Time的特性。

Java8中日期新特性

Java 8一个新增的重要特性就是引入了新的时间和日期API,它们被包含在java.time包中。借助新的时间和日期API可以以更简洁的方法处理时间和日期;

Java 8 中新增了日期时间 API 用来加强对日期时间的处理,其中包括了 LocalDate,LocalTime,LocalDateTime,ZonedDateTime 等等。他们都在 java.time 包下。

LocalDate本地日期

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

  1. @Test
  2. public void LocalDateTest(){
  3. //获取当前时间
  4. LocalDate now = LocalDate.now();
  5. System.out.println("current time:"+now);
  6. // 初始化一个日期:2021-10-28
  7. LocalDate localDateOf = LocalDate.of(2021,10,28);
  8. System.out.println("LocalDate.of:"+localDateOf);
  9. //年份 2021
  10. int year = localDateOf.getYear();
  11. //月份 OCTOBER
  12. Month month = localDateOf.getMonth();
  13. //月份中的第几天 28
  14. int dayOfMonth = localDateOf.getDayOfMonth();
  15. //一周的第几天 THURSDAY
  16. DayOfWeek dayOfWeek = localDateOf.getDayOfWeek();
  17. //月份的天数 31
  18. int i = localDateOf.lengthOfMonth();
  19. //是否为瑞年 false
  20. boolean leapYear = localDateOf.isLeapYear();
  21. }

LocalTime本地时间

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

  1. @Test
  2. public void LocalTimeTest(){
  3. //当前时间
  4. LocalTime now = LocalTime.now();
  5. System.out.println("current time:"+now);
  6. //初始化一个时间:14:30:00
  7. LocalTime localTime = LocalTime.of(14, 30, 00);
  8. //时 14
  9. int hour = localTime.getHour();
  10. //分 30
  11. int minute = localTime.getMinute();
  12. //秒 00
  13. int second = localTime.getSecond();
  14. }

LocalDateTime本地日期时间

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

  1. @Test
  2. public void LocalDateTimeTest(){
  3. LocalDate localDate = LocalDate.of(2021, Month.OCTOBER, 28);
  4. LocalTime localTime = LocalTime.of(14, 30, 00);
  5. //LocalDate和LocalTime的结合体,获取年月日时分秒
  6. LocalDateTime localDateTime = LocalDateTime.of(2021, Month.OCTOBER, 28, 14, 30, 00);
  7. //时分秒和当前年月日合并
  8. LocalDateTime dateTime = localDate.atTime(localTime);
  9. //LocalDateTime转换为LocalDate
  10. LocalDate date = localDateTime.toLocalDate();
  11. //LocalDateTime转换为LocalTime
  12. LocalTime time = localDateTime.toLocalTime();
  13. }

Instant

  1. Instant用于表示一个时间戳,它与我们常使用的System.currentTimeMillis()有些类似,不过Instant可以精确到纳秒(Nano-Second),System.currentTimeMillis()方法只精确到毫秒(Milli-Second);
  2. 如果查看Instant源码,发现它的内部使用了两个常量,seconds表示从1970-01-01 00:00:00开始到现在的秒数,nanos表示纳秒部分(nanos的值不会超过999,999,999);
  3. Instant除了使用now()方法创建外,还可以通过ofEpochSecond方法创建:

日期的操作

Java 8中的日期/时间类都是不可变的,这是为了保证线程安全;新的 日期/时间 类也提供了方法用于创建对象的可变版本,比如增加一天或者减少一天:

  1. @Test
  2. public void updateDateTimeTest(){
  3. // 2021-10-28
  4. LocalDate date = LocalDate.now();
  5. // 修改为 2020-10-28
  6. LocalDate date1 = date.withYear(2020);
  7. // 修改为 2021-09-28
  8. LocalDate date2 = date.withMonth(9);
  9. // 修改为 2021-10-29
  10. LocalDate date3 = date.withDayOfMonth(29);
  11. // 增加一年 2022-10-28
  12. LocalDate date4 = date.plusYears(1);
  13. // 减少两个月 2021-08-28
  14. LocalDate date5 = date.minusMonths(2);
  15. // 增加5天 2021-11-02
  16. LocalDate date6 = date.plus(5, ChronoUnit.DAYS);
  17. }

上面例子中对于日期的操作比较简单,但是有些时候我们要面临更复杂的时间操作,比如将时间调到下一个工作日,或者是下个月的最后一天,这时候我们可以使用with()方法的另一个重载方法,它接收一个TemporalAdjuster参数,可以使我们更加灵活的调整日期:

  1. //注意,这里需要引入这个包:
  2. import static java.time.temporal.TemporalAdjusters.*;
  3. // 返回下一个距离当前时间最近的星期日
  4. LocalDate date7 = date.with(nextOrSame(DayOfWeek.SUNDAY));
  5. // 返回本月最后一个星期六
  6. LocalDate date9 = date.with(lastInMonth(DayOfWeek.SATURDAY));
方法名 描述
dayOfWeekInMonth 返回同一个月中每周的第几天
firstDayOfMonth 返回当月的第一天
firstDayOfNextMonth 返回下月的第一天
firstDayOfNextYear 返回下一年的第一天
firstDayOfYear 返回本年的第一天
firstInMonth 返回同一个月中第一个星期几
lastDayOfMonth 返回当月的最后一天
lastDayOfNextMonth 返回下月的最后一天
lastDayOfNextYear 返回下一年的最后一天
lastDayOfYear 返回本年的最后一天
lastInMonth 返回同一个月中最后一个星期几
next / previous 返回后一个/前一个给定的星期几
nextOrSame / previousOrSame 返回后一个/前一个给定的星期几,如果这个值满足条件,直接返回

如果上面表格中列出的方法不能满足你的需求,你还可以创建自定义的TemporalAdjuster接口的实现,TemporalAdjuster也是一个函数式接口,所以我们可以使用Lambda表达式:

  1. @FunctionalInterface
  2. public interface TemporalAdjuster {
  3. Temporal adjustInto(Temporal temporal);
  4. }

比如给定一个日期,计算该日期的下一个工作日(不包括星期六和星期天):

  1. @Test
  2. public void TemporalAdjusterTest(){
  3. //2021-10-28
  4. LocalDate date = LocalDate.now();
  5. LocalDate with = date.with(temporal -> {
  6. // 当前日期
  7. DayOfWeek dayOfWeek = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
  8. // 正常情况下,每次增加一天
  9. int dayToAdd = 1;
  10. // 如果是星期五,增加三天
  11. if (dayOfWeek == DayOfWeek.FRIDAY) {
  12. dayToAdd = 3;
  13. }
  14. // 如果是星期六,增加两天
  15. if (dayOfWeek == DayOfWeek.SATURDAY) {
  16. dayToAdd = 2;
  17. }
  18. return temporal.plus(dayToAdd, ChronoUnit.DAYS);
  19. });
  20. System.out.println(with);
  21. }

日期的格式化

新的日期API中提供了一个DateTimeFormatter类用于处理日期格式化操作,它被包含在java.time.format包中,Java 8的日期类有一个format()方法用于将日期格式化为字符串,该方法接收一个DateTimeFormatter类型参数:

  1. @Test
  2. public void DateTimeFormatterTest(){
  3. //2021-10-28
  4. LocalDateTime localDateTime = LocalDateTime.now();
  5. //20211028
  6. String formatDate1 = localDateTime.format(DateTimeFormatter.BASIC_ISO_DATE);
  7. //2021-10-28
  8. String formatDate2 = localDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE);
  9. //15:36:33.197
  10. String formatDate3 = localDateTime.format(DateTimeFormatter.ISO_LOCAL_TIME);
  11. //2021-10-28
  12. String formatDate4 = localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
  13. }

日期类也支持将一个字符串解析成一个日期对象,例如:

  1. @Test
  2. public void strFormatDate(){
  3. String dateTime1 = "2021-10-28";
  4. String dateTime2 = "2021-10-28 15:41:35";
  5. //2017-01-05
  6. LocalDate date = LocalDate.parse(dateTime1, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
  7. //2021-10-28T15:41:35
  8. LocalDateTime dateTime = LocalDateTime.parse(dateTime2, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
  9. }

Base64

在 Java 8 中,Base64 编码已经成为 Java 类库的标准。它的使用十分简单,下面让我们看一个例子:

  1. import java.nio.charset.StandardCharsets;
  2. import java.util.Base64;
  3. public class Base64s {
  4. public static void main(String[] args) {
  5. final String text = "Base64 finally in Java 8!";
  6. final String encoded = Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.UTF_8));
  7. System.out.println(encoded);
  8. final String decoded = new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8);
  9. System.out.println(decoded);
  10. }
  11. }

程序在控制台上输出了编码后的字符与解码后的字符:

  1. QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
  2. Base64 finally in Java 8!

Base64 类同时还提供了对 URL、MIME 友好的编码器与解码器(Base64.getUrlEncoder() / Base64.getUrlDecoder(), Base64.getMimeEncoder() / Base64.getMimeDecoder())。