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循環的循環體才是「做什麼」
為什麼使用循環?因為要進行遍歷。但循環是遍歷的唯一方式嗎?遍歷是指每一個元素逐一進行處理,而並不是從
第一個到最後一個順次處理的循環。前者是目的,後者是方式。
試想一下,如果希望對集合中的元素進行篩選過濾:
-
將集合A根據條件一過濾為子集B;
-
然後再根據條件二過濾為子集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);
}
}
}
這段程式碼中含有三個循環,每一個作用不同:
- 首先篩選所有姓張的人;
- 然後篩選名字有三個字的人;
- 最後進行對結果進行列印輸出。
每當我們需要對集合中的元素進行操作的時候,總是需要進行循環、循環、再循環。這是理所當然的么?不是。循
環是做事情的方式,而不是目的。另一方面,使用線性循環就意味著只能遍歷一次。如果希望再次遍歷,只能再使
用另一個循環從頭開始。
那,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) 到底是什麼呢?是數據渠道,用於操作數據源(集合、數組等)所生成的元素序列。「集合講的是數據,流講的是計算! 」
注意:
- Stream不會存儲元素。
- Stream不會改變元對象,相反,他們會返回一個持有結果的新Strean【類似建造者模式】。
- 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
①、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