Java 8的这些新特性,不一样的全新版本(万字长文详细说明)

1995 年 5 月 23 日,Oak 语言改名为 Java,然后有了那句著名的口号——“Write Once,Run Anywhere”。

1996 年 JDK1.0发布了,标志着一个新的时代已经到来。

1998 年 Java 迎来 1.2 版本,有此诞生了我熟知的 J2ME、J2SE 以及 J2EE。ME 主要用于移动端(还记得大明湖畔诺基亚 S40 吗?不小心暴露了年龄),而 SE 做为标准版主要用于桌面程序,EE 则主要针对企业应用所打造,也是我们主要的研究对象。1.2 的发布标志着Java开始普及。

2000 年 1.3 发布,并得到了 Mac OS X 工业标准的支持。

2002 年,也就是中国首次进入世界杯(也是至今唯一的一次,扎铁了老心)的那年。这一年 1.4 问世,这一版本极大的丰富了 Java 的类库,如:XML、Socket、全新的 I/O API、正则、日志、断言等如今我们耳熟能详的功能。

2004 年,我们刚刚经历完非典,Java 也迎来了重要更新,为了突出这次更新的重要性,命名方式从原来的 J2XE 1.X 变成了现在的 JavaXE X,于是有了 JavaSE 5。这一版本增加了泛型、自动拆装箱、循环增强(foreach)、枚举、注解、可变参数等,堪称有史以来最重大的更新,Java 5 应该有姓名!

接下来(2006年)又发布了 Java 6 ,据说这个版本在国内很流行(不要告诉我你们公司还在用 JDK1.6 )。同年,发生了一件大事——Java 开源了!我爱开源!

2009 年 Oracle(就是前段时间裁员 N+6 的那家公司)收购了 Sun , Java 从此跟了后妈,过着寄人篱下的日子。

后来,在 2011 年发布了 Java 7 。

三年后发布了 Java 8 ,为我们带来了 Lambda 表达式、Stream 以及新的日期时间 API 。Java 8 应该也是目前被使用最多的版本。

后来相继发布了 9、10、11、12、13,今年(2020 年)3 月 17 日 Oracle 发布了 JDK 14。

在Java8中产生了许多重大更新。

1、Lambda表达式

Lambda轻松上手,快速传送门:Lambda表达式,你真的不了解一下吗?

2、Stream API

说到Stream便容易想到I/O Stream,而实际上,谁规定“流”就一定是“IO流”呢?在Java 8中,得益于Lambda所带
来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。

2.1 入门介绍

几乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元
素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如:

import java.util.ArrayList;
import java.util.List;
public class Demo01ForEach {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张强");
        list.add("张三丰");
        for (String name : list) {
        	System.out.println(name);
        }
    }
}

这是一段非常简单的集合遍历操作:对集合中的每一个字符串都进行打印输出操作。

循环遍历的弊端 :

Java 8的Lambda让我们可以更加专注于做什么(What),而不是怎么做(How),这点此前已经结合内部类进行
了对比说明。现在,我们仔细体会一下上例代码,可以发现:

  • for循环的语法就是“怎么做”
  • for循环的循环体才是“做什么”

为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从
第一个到最后一个顺次处理的循环。前者是目的,后者是方式。

试想一下,如果希望对集合中的元素进行筛选过滤:

  1. 将集合A根据条件一过滤为子集B;

  2. 然后再根据条件二过滤为子集C。

可以进行多次遍历后筛选:

import java.util.ArrayList;
import java.util.List;
public class Demo02NormalFilter {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张强");
        list.add("张三丰");
        List<String> zhangList = new ArrayList<>();
        //第一次筛选以‘张‘开始的name
        for (String name : list) {
            if (name.startsWith("张")) {
            	zhangList.add(name);
            }
        } 
        //以第一次筛选的结果作为第二次筛选的输入
        List<String> shortList = new ArrayList<>();
        for (String name : zhangList) {
            if (name.length() == 3) {
            	shortList.add(name);
            }
        } 
        for (String name : shortList) {
       		System.out.println(name);
        }
    }
}

这段代码中含有三个循环,每一个作用不同:

  1. 首先筛选所有姓张的人;
  2. 然后筛选名字有三个字的人;
  3. 最后进行对结果进行打印输出。

每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循
环是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使
用另一个循环从头开始。
那,Lambda的衍生物Stream能给我们带来怎样更加优雅的写法呢?

下面来看一下借助Java 8的Stream API,如果优雅的写代码:

import java.util.ArrayList;
import java.util.List;
public class Demo03StreamFilter {
public static void main(String[] args) {
  List<String> list = new ArrayList<>();
  list.add("张无忌");
  list.add("周芷若");
  list.add("赵敏");
  list.add("张强");
  list.add("张三丰");
  list.stream()
      .filter(s ‐> s.startsWith("张"))
      .filter(s ‐> s.length() == 3)
      .forEach(System.out::println);
  }
}

直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤姓张、过滤长度为3、逐一打印。代码
中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。

2.2、什么是Stream流

流(Stream) 到底是什么呢?是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。“集合讲的是数据,流讲的是计算! ”

注意:

  1. Stream不会存储元素。
  2. Stream不会改变元对象,相反,他们会返回一个持有结果的新Strean【类似建造者模式】。
  3. Stream操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。

Stream的操作三部曲

  • 创建Stream:一个数据源(如:集合、数组),获取一个流。
  • 中间操作:一个中间操作链,对数据源的数据进行处理。
  • 终止操作:一个终止操作,执行中间操作链,得到结果。

在这里插入图片描述

2.3、Stream的创建

①、通过Collection接口获取

  • default Stream stream() : 返回一个顺序流
  • default Stream parallelStream() : 返回一个并行流
//1.通过Collection系列集合提供的Stream()[串行流]方法或者parallelStream()[并行流]
        List<Integer>list=new ArrayList<>();
        Stream<Integer> stream1 = list.stream();

②、由数组创建流

Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:

  • static Stream stream(T[] array): 返回一个流
 //2.通过Arrays中的静态方法Stream()获取数组流,各种数组转为Stream
        int[] arr={1,2,3,4};
        IntStream stream2=  Arrays.stream(arr);

③、由值创建流

可以使用静态方法 Stream.of(), 通过显示值创建一个流。它可以接收任意数量的参数。

  • public static Stream of(T… values) : 返回一个流
 Stream<String> stream3 = Stream.of("a", "b");

④、由函数创建流:创建无限流

可以使用静态方法 Stream.iterate() 和Stream.generate(), 创建无限流。 【流的初始大小未固定】

  • ①、迭代

    public static Stream iterate(final T seed, finalUnaryOperator f)

  • ②、生成

    public static Stream generate(Supplier s)

/**
         * public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
         * 参数 seed:初始值
         * UnaryOperator<T> f函数式接口,单参数有返回值
         *
         */
		//①迭代
        Stream<Integer> stream4 = Stream.iterate(0, (x) -> x + 2);
        stream4.limit(10)
                .forEach(System.out::println);
        //②生成
        Stream.generate(()->Math.random())
                .limit(10)
                .forEach(System.out::println);

2.4、Stream的中间操作

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”

①、筛选与切片

  • filter-接受lambda表达式,从流中排除某些元素;
  • limit-截断流,试元素不超过给定的数量;
  • skip(n)-跳过元素,返回一个扔掉了前n个元素的流。若流中的元素不足n个,则返回一个空流,与limit(n)互补;
  • distinct-筛选,通过流所生成的hashCode()和equls()去掉重复元素。
 @Test
    public void test01(){
        list.stream()
                .filter((x)->Integer.parseInt(x.getAge())>50)
                .limit(1)
                .forEach(System.out::println);
        //终止操作:最后一次性执行全部内容,并不会先执行某句话再执行
    }
    @Test
    public void test02(){
        list.stream()
                .skip(2)//跳过前两个
                .distinct()//去重   其中对象得重写hashcode和equls
                .forEach(System.out::println);
    }

说明:比如filter(Predicate<? super T> predicate)需要传递的是一个函数式接口,而上述代码使用lambda表达式去实现。

筛选与切片

②、映射

  • flatMap(Function<? super T, ? extends Stream<? extends R>> mapper):【多个流合并为一个流】把流中的元素一个个加入到当前的流中。
  • map(Function<? super T, ? extends R> mapper):函数会对每一个元素进行映射得到一个新的元素。

下面为了测试这两种的区别,我们自定义一个返回Stream流的方法:

@Test
    public void test04(){
        List<String>str=Arrays.asList("aaa","bbb","ccc");
        //直接使用map map本地得到的就是一个新的Stream流,而map执行多次得到多个流,最终Stream流存放的依然是			Stream流【Stream流中存放的是Stream流】
        //map:不会合并流,仅仅是对元素输入函数进行映射,得到一个个Stream加入到当前的流中
        Stream<Stream<Character>> sm = str.stream().map(TestStream::filterCharter);//直接调用此类的方法
        sm.forEach((ssmm)->{
            ssmm.forEach(System.out::println);
        });
        //flatMap:将返回的流进行合并,得到一个流
        str.stream()
                .flatMap(TestStream::filterCharter)//lambda表达式的类::静态方法调用 直接调用此类的方法
                .forEach(System.out::println);
    }
   //将字符串切割得到字符
    public static Stream<Character>filterCharter(String str){
        List<Character> list=new ArrayList<>();
        for (Character c:str.toCharArray()) {
            list.add(c);
        }
        return list.stream();
    }

映射

③、排序

 List<Employee> list= Arrays.asList(
            new Employee("张三","15"),
            new Employee("李四","45"),
            new Employee("王五","35"),
            new Employee("王五","35"),
            new Employee("王六","35"),
            new Employee("小刘","65")
    );
    /**
     * 排序:
     * sorted()-自然排序-(Comparable)
     * sorted(Comparator com)-定制排序
     */
    @Test
    public void test05(){
        List<String>str=Arrays.asList("ddd","aaaaa","bbb","ccccc");
        str.stream()
                .sorted()
                .forEach(System.out::println);
        list.stream()
                .sorted((x,y)->{
                    if(x.getAge().equals(y.getAge())){
                        return  -x.getName().compareTo(y.getName());
                    }else{
                        return  -(Integer.parseInt(x.getAge())-Integer.parseInt(y.getAge()));
                    }
                })
                .forEach(System.out::println);
    }

排序

2.4、Stream的终止操作

终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如: List、 Integer,甚至是 void 。

①、查找与匹配

  • allMatch-检查是否匹配所有元素
  • anyMatch-检查是否至少匹配一个元素
  • noneMatch-检查是否没有匹配所有元素
  • findFirst-返回第一个元素
  • findAny-返回当前流中的任意元素
  • count-返回流中元素的总个数
  • max-返回流中最大值
  • min-返回流中的最小值

是不是已经感觉到迷糊了,这么多!别急,一个小案例就清晰了。

 List<Student> list= Arrays.asList(
           new Student("张三",18, Student.Status.FREE),
           new Student("李四",13, Student.Status.BUSY),
           new Student("李四",13, Student.Status.BUSY),
           new Student("王五",11, Student.Status.BUSY),
           new Student("刘六",55, Student.Status.VOCATION),
           new Student("王麻子",77, Student.Status.FREE)
    );
//注:Student.Status是一个enum
    @Test
    public void test01(){
        //是否匹配所有的
        boolean match = list.stream()
                .allMatch((t) -> Student.Status.BUSY.equals(t.getStatus()));
        System.out.println(match);//false
        //至少有一个匹配
        boolean match1 = list.stream()
                .anyMatch((t) -> Student.Status.BUSY.equals(t.getStatus()));
        System.out.println("至少有一个元素与之匹配"+match1);//至少有一个元素与之匹配true
        //没有匹配的元素
        boolean match2 = list.stream()
                .noneMatch((t) -> Student.Status.BUSY.equals(t.getStatus()));
        System.out.println("没有与之匹配的元素:"+match2);//没有与之匹配的元素:false
        //避免空指针异常,将结果封装到Optional,一旦有可能为空就封装到Optional中
        Optional<Student> op = list.stream()
                .sorted((x, y) ->Integer.compare(x.getAge(),y.getAge()))
                .findFirst();
        System.out.println("排序后拿到第一个元素:"+op.get());//Student{name='王五', age=11, Status=BUSY}
        //随便找到一个空闲状态的人,先过滤出来,再随便找一个
        Optional<Student> any = list.stream()
                .filter((e) -> e.getStatus().equals(Student.Status.FREE))
                .findAny();
        System.out.println("随便-找到一个status为空闲的人"+any.get());//Student{name='张三', age=18, Status=FREE}
        System.out.println("人员总数"+list.stream().count());//人员总数6
        Optional<Student> max = list.stream()
                .max((x, y) -> Integer.compare(x.getAge(), y.getAge()));
        System.out.println("获取年龄最大的:"+max.get());//Student{name='王麻子', age=77, Status=FREE}
    }

下面对上述方法的具体描述:

查找与匹配

查找与匹配

②、归约

  • reduce(T identity,BinaryOperator)
    T identity 初始值
    BinaryOperator->继承BiFunction<T,U, R>
    将identity作为起始值,做为x,再从流中取出一个元素作为y;
  • reduce(BinaryOperator)–可以将流中元素反复结合起来,得到一个值
  @Test
    public void test02(){
        List<Integer>list=Arrays.asList(1,2,3,4,5,6,7,8);
        Integer sum = list.stream()
                .reduce(0, (x, y) -> x + y);//初始值,一开始x=初始值,再从流中拿到第一个值作为y
        System.out.println(sum);//36
        /**
         * 下列包含Lambda表达式中的方法方法引用的 类::实例方法
         * 此种情况较为特殊:
         *      若lambda参数列表中的第一个参数是实例方法的调用者,
         *      而第二个参数是实例方法的参数时或者没有第二个参数,
         *      可以使用ClassName::method
         */
        Optional<Integer> op = this.list.stream()
                .map((x) -> x.getAge())
                .reduce(Integer::sum);
        System.out.println(op.get());//187
        Optional<Integer> op2 = this.list.stream()
                .map(Student::getAge)
                .reduce(Integer::sum);
        System.out.println(op2.get());//187
    }

归约

③、收集

Collector 接口中方法的实现决定了如何对流执行收集操作(如收集到 List、 Set、 Map)。但是 Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例, 具体方法与实例如下表:

收集
收集

  • collect-将流转换为其他形式。接受一个Collector接口的实现,用于Stream中元素做汇总的方法
 List<Student> list= Arrays.asList(
           new Student("张三",18, Student.Status.FREE),
           new Student("李四",13, Student.Status.BUSY),
           new Student("李四",13, Student.Status.BUSY),
           new Student("王五",11, Student.Status.BUSY),
           new Student("刘六",55, Student.Status.VOCATION),
           new Student("王麻子",77, Student.Status.FREE)
    );
//需求:将当前学生的名字提取出后放在一个集合里面
    @Test
    public void test03(){
        System.out.println("-----------将结果收集到list集合并返回--------------------");
        List<String> list1 = list.stream()
                .map(Student::getName)
                .collect(Collectors.toList());
        list1.forEach(System.out::println);
        System.out.println("-----------将结果收集到set集合并返回-----------------------");
        Set<String> set1 = list.stream()
                .map(Student::getName)
                .collect(Collectors.toSet());
        set1.stream().forEach(System.out::println);
        System.out.println("-----------自定义返回的集合类型---------------------------");
        HashSet<String> set2 = list.stream()
                .map(Student::getName)
                .collect(Collectors.toCollection(HashSet::new));
        set2.forEach(System.out::println);
        System.out.println("-----------将结果收集为总数数量---------------------------");
        Long number = list.stream()
                .collect(Collectors.counting());
        System.out.println(number);
        System.out.println("-----------得到结果的平均值---------------------------");
        Double av = list.stream()
                .collect(Collectors.averagingInt(value -> value.getAge()));
        System.out.println(av);
        System.out.println("-----------得到结果的总和---------------------------");
        Integer sum = list.stream()
                .collect(Collectors.summingInt(Student::getAge));
        System.out.println(sum);
        System.out.println("-----------得到结果的最大值---------------------------");
        Optional<Student> student = list.stream()
                .collect(Collectors.maxBy((o1, o2) -> Double.compare(o1.getAge(), o2.getAge())));
        System.out.println(student.get());
        System.out.println("-----------连接字符串---------------------------");
        String s = list.stream()
                .map(Student::getName)
                .collect(Collectors.joining("中间连接符,","首","尾"));
        System.out.println(s);

    }

需求一:按照状态进行分组 对应sql的分组

@Test
    public void test04(){
        Map<Student.Status, List<Student>> map = list.stream()
                .collect(Collectors.groupingBy(Student::getStatus));
        System.out.println(map);
    }
//结果:
{VOCATION=[Student{name='刘六', age=55, Status=VOCATION}], 
BUSY=[Student{name='李四', age=13, Status=BUSY}, Student{name='李四', age=13, Status=BUSY}, Student{name='王五', age=11, Status=BUSY}], 
FREE=[Student{name='张三', age=18, Status=FREE}, Student{name='王麻子', age=77, Status=FREE}]}

需求二:多级分组

注意:分组后一般返回的是一个map,其中key作为分组的依据,value就是分组后的值

 /**
     *
     * Collectors.groupingBy(Function,Collectors)
     * 分组后,对已分组的数据进行二次分组
     */
    @Test
    public void test05(){
        Map<Student.Status, Map<String, List<Student>>> map = list.stream()
                .collect(Collectors.groupingBy(Student::getStatus, Collectors.groupingBy(o -> {
                    if (((Student) o).getAge() <= 18) {
                        return "青年";
                    } else if (((Student) o).getAge() <= 50) {
                        return "中年";
                    } else {
                        return "老年";
                    }
                })));
        System.out.println(map);
    }
//结果:
{BUSY={青年=[Student{name='李四', age=13, Status=BUSY}, Student{name='李四', age=13, Status=BUSY}, Student{name='王五', age=11, Status=BUSY}]},
VOCATION={老年=[Student{name='刘六', age=55, Status=VOCATION}]}, 
FREE={青年=[Student{name='张三', age=18, Status=FREE}], 老年=[Student{name='王麻子', age=77, Status=FREE}]}}

分片和分区:根据处理的结果ture/false进行分区

 @Test
    public void test06(){
        Map<Boolean, List<Student>> map = list.stream()
                .collect(Collectors.partitioningBy(o -> o.getAge() > 50));
        System.out.println(map);
    }
//结果:
{false=[Student{name='张三', age=18, Status=FREE}, Student{name='李四', age=13, Status=BUSY}, Student{name='李四', age=13, Status=BUSY}, Student{name='王五', age=11, Status=BUSY}], 
true=[Student{name='刘六', age=55, Status=VOCATION}, Student{name='王麻子', age=77, Status=FREE}]}

3、新的时间日期API

3.1、LocalDate、 LocalTime、 LocalDateTime

LocalDate、 LocalTime、 LocalDateTime 类的实例是不可变的对象【线程安全】,分别表示使用 ISO-8601日历系统的日期、时间、日期和时间。它们提供了简单的日期或时间,并不包含当前的时间信息。也不包含与时区相关的信息。 新的日期类将统一放在java.time包下。

注意:三者使用的方式完全相同,只是一个表示日期,一个表示时间,一个表示时间和日期

下面让我们快速入门吧!

 @Test
    public void test01(){
        System.out.println("---------------------获取当前系统时间----------------------------");
        LocalDateTime dateTime1 = LocalDateTime.now();
        System.out.println(dateTime1);
        System.out.println("---------------------指定时间,年月日时分秒----------------------------");
        LocalDateTime dateTime2 = LocalDateTime.of(2015, 10, 16, 13, 22, 33);
        System.out.println(dateTime2);
        //时间运算,添加一日
        System.out.println("---------------------日期运算,当前系统时间往后添加一日-----------------");
        LocalDateTime dateTime3 = dateTime1.plusDays(1);
        System.out.println(dateTime3);
        System.out.println("---------------------日期运算,当前系统时间往后减少一个月---------------");
        LocalDateTime dateTime4 = dateTime1.minusMonths(1);
        System.out.println(dateTime4);
        //获取单独的年月日
        //可以直接获取值,也可以获取年月日对象
        System.out.println("年:"+dateTime1.getYear());
        System.out.println("月:"+dateTime1.getMonthValue());
        System.out.println("日:"+dateTime1.getDayOfMonth());
        System.out.println("时:"+dateTime1.getHour());
        System.out.println("分:"+dateTime1.getMinute());
        System.out.println("秒:"+dateTime1.getSecond());
    }
输出:
---------------------获取当前系统时间---------------------------------
2020-08-13T22:36:07.123
---------------------指定时间,年月日时分秒----------------------------
2015-10-16T13:22:33
---------------------日期运算,当前系统时间往后添加一日-----------------
2020-08-14T22:36:07.123
---------------------日期运算,当前系统时间往后减少一个月---------------
2020-07-13T22:36:07.123
年:2020
月:8
日:13
时:22
分:36
秒:7

新的日期API

①、Instant时间戳

Instant 时间戳 给机器读的用于“时间戳”的运算。它是以Unix元年(传统的设定为UTC时区1970年1月1日午夜时分)开始所经历的描述进行运算。

 @Test
    public void test02(){
        Instant instant = Instant.now();//默认获取的是UTC时区为基础的
        System.out.println("默认获取的是UTC时区为基础的时间:"+instant);
        //对时区做一个偏移量运算
        OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8));
        System.out.println("带偏移量的时区时间:"+offsetDateTime);

        //输出时间格式为毫秒格式
        System.out.println("输出时间格式为毫秒格式"+instant.toEpochMilli());
        //相较1970年的时间
        System.out.println("相较1970年的时间 向后添加60秒"+Instant.ofEpochSecond(60));//1970-01-01T00:01:00Z
    }
输出:
    默认获取的是UTC时区为基础的时间:2020-08-15T02:49:41.828Z
	带偏移量的时区时间:2020-08-15T10:49:41.828+08:00
	输出时间格式为毫秒格式1597459781828
	相较1970年的时间 向后添加60秒1970-01-01T00:01:00Z

②、Duration 和 Period

  • Duration:计算两个时间之间的间隔。
  • Period:计算两个日期之间的间隔。
@Test
    public void test03() throws InterruptedException {
        Instant instant1 = Instant.now();
        Thread.sleep(100);
        Instant instant2 = Instant.now();
        //计算两个时间戳之间的间隔
        Duration duration = Duration.between(instant1, instant2);
        //获取毫秒
        System.out.println(duration.toMillis());//119
        //获取纳秒
        System.out.println(duration.getNano());//119000000
        //获取秒
        System.out.println(duration.getSeconds());//0
        //获取纳秒第二种方式
        System.out.println(duration.toNanos());//119000000

        LocalDateTime dateTime1 = LocalDateTime.now();
        Thread.sleep(1);
        LocalDateTime dateTime2 = LocalDateTime.now();
        System.out.println(duration.between(dateTime1,dateTime2).toMillis());//1
    }
  @Test
    public void test04(){
        LocalDate date1 = LocalDate.of(2018,1,1);
        LocalDate date2 = LocalDate.now();
        Period period = Period.between(date1, date2);
        System.out.println(period);//P2Y5M13D
        //直接输出格式不是很明显
        System.out.println("相差几年:"+period.getYears());//
        System.out.println("相差多少月:"+period.getMonths());//
        System.out.println("相差多少天:"+period.getDays());//
    }
输出:
    P2Y7M14D
	相差几年:2
	相差多少月:7
	相差多少天:14

③、日期的操纵

  • TemporalAdjuster : 时间校正器。有时我们可能需要获取例如:将日期调整到“下个周日”等操作。
  • TemporalAdjusters : 该类通过静态方法提供了大量的常用 TemporalAdjuster 的实现。

方法public LocalDate with(TemporalAdjuster adjuster) ,传入接口TemporalAdjuster 对日期进行调整,我们查看TemporalAdjuster 接口的源码:

@FunctionalInterface
public interface TemporalAdjuster {
     Temporal adjustInto(Temporal temporal);
}

可以发现其是一个函数式接口

  @Test
    public void test05(){
        //下一个周五是啥时候
        LocalDate date = LocalDate.now();
        //下一个周五是啥时候
        LocalDate date1 = date.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
        System.out.println("下一个周五是啥时候"+date1);

        //自定义指定时间,获取当前时间距离周六的天数,
        LocalDate date2 = date.with(t -> {
            //LocalDate实现了接口Temporal,所以可以实现强转
            LocalDate lt = (LocalDate) t;
            //获取今天是周几
            //lt.plusDays():向当前日期添加几天
            DayOfWeek dayOfWeek = lt.getDayOfWeek();
            if (dayOfWeek.equals(DayOfWeek.SATURDAY)) {
                return lt.plusDays(0);//其实这一句是多余的
            } else {
                return lt.plusDays(DayOfWeek.SATURDAY.getValue() - dayOfWeek.getValue());
            }
        });
        System.out.println("前时间距离周六的时间"+date2);
    }
输出:
    下一个周五是啥时候2020-08-21
	前时间距离周六的时间2020-08-15

说明:DayOfWeek是java.time包下的一个enum类型,其中定义好了周一~周日的枚举值。

④、解析与格式化

java.time.format.DateTimeFormatter 类:该类提供了三种格式化方法:

  • 预定义的标准格式
  • 语言环境相关的格式
  • 自定义的格式
 @Test
    public void test07(){
        System.out.println("-------------------------使用自带的各种格式---------------");
        DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME;

        LocalDateTime dateTime = LocalDateTime.now();
        String strtime = formatter.format(dateTime);
        System.out.println(strtime);
        System.out.println("-------------------------自定义日期格式---------------");
        DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
        String strtime2 = formatter2.format(dateTime);
        System.out.println(strtime2);
        System.out.println("--------------------把字符串解析成为时间LocalDateTime---");
        LocalDateTime dateTime2 = dateTime.parse(strtime2, formatter2);
        System.out.println(dateTime2);
    }
输出:
   -------------------------使用自带的各种格式---------------
	2020-08-15T11:14:51.74
	-------------------------自定义日期格式---------------
	2020年08月15日 11:14:51
	--------------------把字符串解析成为时间LocalDateTime---
	2020-08-15T11:14:51 

注意:根据字符串解析LocalDateTime时,其formatter需保持一致,否则会报错java.time.format.DateTimeParseException

⑤、时区的处理

Java8 中加入了对时区的支持,带时区的时间为分别为: ZonedDate、 ZonedTime、 ZonedDateTime

其中每个时区都对应着 ID,地区ID都为 “{区域}/{城市}”的格式例如 : Asia/Shanghai 等。

ZoneId:该类中包含了所有的时区信息

  • getAvailableZoneIds() : 可以获取所有时区时区信息
  • of(id) : 用指定的时区信息获取 ZoneId 对象
@Test
    public void test08(){
        System.out.println("---------------获取支持的所有时区----------------------");
        Set<String> ids = ZoneId.getAvailableZoneIds();
        ids.forEach(System.out::println);
        System.out.println("---------------获取指定时区地区的时间信息---------------");
        LocalDateTime now = LocalDateTime.now(ZoneId.of("Europe/London"));
        System.out.println(now);//2020-06-15T08:18:27.396
        System.out.println("---------------获取带时区信息的时间,包含了与UTC标准的时差---");
        LocalDateTime now1 = LocalDateTime.now();
        ZonedDateTime zone = now1.atZone(ZoneId.of("Europe/London"));
        System.out.println(zone);//2020-06-15T15:18:27.515+01:00[Europe/London]
    }
输出:
    ---------------获取支持的所有时区----------------------
    Asia/Aden
    America/Cuiaba
    Etc/GMT+9
    Etc/GMT+8
    Asia/Shanghai
    等等.....
    ---------------获取指定时区地区的时间信息---------------
    2020-08-15T04:22:09.009
    ---------------获取带时区信息的时间,包含了与UTC标准的时差---
    2020-08-15T11:22:09.029+01:00[Europe/London]

⑥、与传统时器处理与转换

在这里插入图片描述

4、接口中的默认方法和静态方法

我们都知道,当实体类(非抽象类)A继承了某个接口B,那么类A必须重写接口中的方法。此模式在springboot 1.X的版本中均使用此模式,其解决方式是在接口和实体类中添加了一层:抽象类,使之抽象类继承接口,实体类再去继承抽象类。比如:

在springboot 1.X版本时,当需要对springmvc进行扩展时,相关配置需要继承抽象类WebMvcConfigureAdapter:

@Deprecated
public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {
    /**
	 * {@inheritDoc}
	 * <p>This implementation is empty.
	 */
	@Override
	public void configurePathMatch(PathMatchConfigurer configurer) {
	}
    ...WebMvcConfigurer接口的其他方法
}   

进入源码我们可以发现其对WebMvcConfigurer实现,但是均没有具体实现。

当springboot 2.X版本时,此时我们进入接口WebMvcConfigurer中查看:

public interface WebMvcConfigurer {

	/**
	 * Helps with configuring HandlerMappings path matching options such as trailing slash match,
	 * suffix registration, path matcher and path helper.
	 * Configured path matcher and path helper instances are shared for:
	 * <ul>
	 * <li>RequestMappings</li>
	 * <li>ViewControllerMappings</li>
	 * <li>ResourcesMappings</li>
	 * </ul>
	 * @since 4.0.3
	 */
	default void configurePathMatch(PathMatchConfigurer configurer) {
	}
    @Nullable
	default MessageCodesResolver getMessageCodesResolver() {
		return null;
	}
    ...等等...
}

对比一下:此接口中定义方法与我们平时定义的有何不同?

  • 接口中的方法前多了一个关键字default,而且对其进行了实现。

最后插入一下springboot中对于springmvc扩展时的部分配置:

@Configuration
//@EnableWebMvc
public class MyMvcConfig implements WebMvcConfigurer {
    //需要什么方法,从写什么方法

    /**
     * 配置嵌入式servlet容器
     * @return
     */
//    @Bean //一定要将这个定制器加入到容器中
    public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer() {
            //定制嵌入式的Servlet容器相关的规则
       /* return new WebServerFactoryCustomizer<ConfigurableWebServerFactory>() {
            @Override
            public void customize(ConfigurableWebServerFactory factory) {
                factory.setPort(8081);
            }
        };*/
        return (factory)->factory.setPort(8081);
    }
    /**
     * 自定义一个controller
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        //返回的路径也是经过thymeleaf模板引擎的
        //这个相当于在controller中映射了一个@RequestMapping()
        registry.addViewController("/page03").setViewName("thymeleaf01");
    }
    @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }
    /**
     * 自定义一个视图解析器
     * 将被ContentNegotiatingViewResolver自动加载
     */
    @Bean
    public ViewResolver myViewResolver(){
       return new MyViewResolver();
    }
    private static class MyViewResolver implements ViewResolver{
        @Override
        public View resolveViewName(String viewName, Locale locale) throws Exception {
            return null;
        }
    }

    /**
     *注册拦截器
     */
   /* @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
           .excludePathPatterns(Arrays.asList("/index.html","/","/user/login","/assert/**","/css/**","/js/**","/img/**","/webjars/**"))
                .addPathPatterns("/**");
    }*/
}

好了,上述听着可能比较繁琐,而我们比较直观的看见的是:当接口中的方法使用default修饰后,其实现类不强制去实现,也只是为了引入default关键字,那么接口中的默认方法和静态方法有啥注意事项呢?

接口默认方法的” 类优先” 原则 :

若一个接口中定义了一个默认方法,而另外一个父类或接口中又定义了一个同名的方法时 :

  • 选择父类中的方法。如果一个父类提供了具体的实现,那么接口中具有相同名称和参数的默认方法会被忽略。
  • 接口冲突。如果一个父接口提供一个默认方法,而另一个接口也提供了一个具有相同名称和参数列表的方法(不管方法是否是默认方法), 那么必须覆盖该方法来解决冲突 。

好了,下面使用一个例子来说明上述两条规则:

定义一个接口Myfun:

/**
 * 在以前接口中只能有:全局静态常量和抽象方法
 * 在java8中添加默认方法、静态方法
 */
public interface Myfun {
    default String getName(){
        return "这是接口Myfun!";
    }
    public static void shwo(){
        System.out.println("这是Myfun接口中的静态方法");
    }
}

定义一个实体类TestDefault,其下有一个与接口Myfun同名方法:

public class TestDefault  {
    public  String getName(){
        return "这是类testDefault!";
    }
}

定义一个实现类MyClass去继承第接口和实现类:

public class MyClass extends TestDefault implements Myfun{
}

此时若调用MyClass的getName()方法,那么调用的是接口中的方法,或者是实体类终的方法呢???

public class TestDefaultInterface {
    /**
     * 类MyClass同时继承了TestDefault、Myfun
     * 在类TestDefault和接口Myfun均存在方法getName()
     * 问题:当子孙类同时继承时,调用getName()方法时,调用的是谁的方法?
     */
   
     //注意:当继承的多个接口中存在同名的默认方法时,那么实现类中必须对此方法进行重写
    
    public static void main(String[] args) {
        //调用的是类中的方法
        System.out.println(new MyClass().getName());
        //接口直接调用静态方法执行
        Myfun.shwo();
    }
}
结果:
    这是类testDefault!
    这是Myfun接口中的静态方法

5、Optional类

Optional 类(java.util.Optional) 是一个容器类,代表一个值存在或不存在,原来用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念。并且可以避免空指针异常。

常用方法:

  • Optional.of(T t) : 创建一个 Optional 实例
  • Optional.empty() : 创建一个空的 Optional 实例
  • Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则创建空实例
  • isPresent() : 判断是否包含值
  • orElse(T t) : 如果调用对象包含值,返回该值,否则返回t
  • orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回 s 获取的值
  • map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回 Optional.empty()
  • flatMap(Function mapper):与 map 类似,要求返回值必须是Optional