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