github博文傳送門
Java8特性學習筆記
Java8中新增了許多的新特性,在這裡本人研究學習了幾個較為常用的特性,在這裡與大家進行分享。(這裡推薦深入了解Java 8用于了解基礎知識)本文分為以下幾個章節:
- Lambda 表達式
- 方法引用
- 預設方法
- 函數接口
- Function
- Stream
- Optional API
- Date Time API
Lambda表達式
Lambda 表達式,也可稱為閉包。Lambda 允許把函數作為一個方法的參數(函數作為參數傳遞進方法中)。使用 Lambda 表達式可以使代碼變的更加簡潔緊湊。 Lambda表達式可以替代以前廣泛使用的内部匿名類,各種回調,比如事件響應器、傳入Thread類的Runnable等。
Lambda文法
lambda 表達式的文法格式如下:
(parameters) -> expression
或
(parameters) ->{ statements; }
Lambda表達式的特征
- 類型聲明(可選):可以不需要聲明參數類型,編譯器會識别參數值。
- 參數圓括号(可選):在單個參數時可以不使用括号,多個參數時必須使用。
- 大括号和return關鍵字(可選):如果隻有一個表達式,則可以省略大括号和return關鍵字,編譯器會自動的傳回值;相對的,在使用大括号的情況下,則必須指明傳回值。
Lambda表達式例子
這裡以常用的list排序功能為例:
private static List<People> peopleList = new ArrayList<People>();
{
peopleList.add(new People("a",17));
peopleList.add(new People("b",16));
peopleList.add(new People("c",19));
peopleList.add(new People("d",15));
}
@Test
public void testLambda(){
System.out.println("排序前:"+peopleList);
//第一種,傳統匿名Compartor接口排序
Collections.sort(peopleList, new Comparator<People>() {
@Override
public int compare(People o1, People o2) {
return o1.getAge().compareTo(o2.getAge());
}
});
System.out.println("匿名接口方法——排序後:"+peopleList);
//第二種,使用Lambda表達式來代替匿名接口方法
//1.聲明式,不使用大括号,隻可以寫單條語句
Collections.sort(peopleList,(People a,People b)->a.getAge().compareTo(b.getAge()));
System.out.println("Lambda表達式1、排序:"+peopleList);;
//2.不聲明式,使用大括号,可以寫多條語句
Collections.sort(peopleList,(a,b)->{
System.out.print("——————————————");
return a.getAge().compareTo(b.getAge());
});
System.out.println();
System.out.println("Lambda表達式2、排序:"+peopleList);
//第三種,使用Lambda表達式調用類的靜态方法
Collections.sort(peopleList,(a,b)->People.sortByName(a,b));
System.out.println("Lambda表達式調用靜态方法:"+peopleList);
//第四種,使用Lambda表達式調用類的執行個體方法
Collections.sort(peopleList,(a,b)->new People().sortByAge(a,b));
System.out.println("Lambda表達式調用執行個體方法:"+peopleList);
}
對應的運作結果:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsISM9AnYldnJwAzN9c3Pn5Gcu4WNHNWdZtmYuZ1aZxkUwwEd5ITW1lEWk5WMXFWdrJDT29GRjBjUIF2Lc12bj5SYphXa5VWen5WY35iclN3Ztl2Lc9CX6MHc0RHaiojIsJye.png)
(注意:在Lambda表達式中隻能對final的對象進行操作,聲明的對象也為final)
有的朋友應該已經觀察到了,Lambda 表達式與C中的函數指針,JavaScript的匿名function均有些相似。其實,Lambda表達式本質上是一個匿名的方法,隻不過它的目标類型必須是“函數接口(functional interface)”,這是Java8引入的新概念,在接下來會進行更加詳細的介紹。
方法引用
在一些Lambda中可能隻是單純的調用方法,比如前例中的三、四,在這種情況下,就可以使用方法引用的方式來提高可讀性。
方法引用的種類
- 類靜态方法引用
Class::staticMethodName
- 某個對象的方法引用
instance::instanceMethodName
- 特定類的任意對象的方法引用:
Class::method
- 構造方法引用:
Class::new
方法引用的例子
@Test
public void testMethodReference() {
//第一種,引用類的靜态方法
Collections.sort(peopleList, People::sortByName);
System.out.println("引用類的靜态方法:" + peopleList);
//第二種,引用類的執行個體方法
Collections.sort(peopleList, new People()::sortByAge);
System.out.println("引用類的執行個體方法:" + peopleList);
//第三種,特定類的方法調用()
Integer[] a = new Integer[]{3, 1, 2, 4, 6, 5};
Arrays.sort(a, Integer::compare);
System.out.println("特定類的方法引用:" + Arrays.toString(a));
//第四種,引用類的構造器
Car car = Car.create(Car::new);
System.out.println("引用類的構造器:" + car);
}
public static Car create(Supplier<Car> supplier){
return supplier.get();
}
預設方法
在Java8之前的時代,為已存在接口增加一個通用的實作是十分困難的,接口一旦釋出之後就等于定型,如果這時在接口内增加一個方法,那麼就會破壞所有實作接口的對象。
預設方法(之前被稱為 虛拟擴充方法 或 守護方法)的目标即是解決這個問題,使得接口在釋出之後仍能被逐漸演化。
預設方法(defalut)
public interface vehicle {
default void print(){
System.out.println("我是一輛車!");
}
}
靜态方法(static)
public interface vehicle {
static void blowHorn() {
System.out.println("按喇叭!!!");
}
}
注:靜态方法與預設方法均可以有多個,預設方法可以被覆寫。
函數接口
“函數接口(functional interface)”,就是除去預設方法以及繼承的抽象方法,隻有顯式聲明一個抽象方法的接口。它使用@FunctionalInterface注解在類上進行标注,也可以省略,Java會自動識别。接下來介紹一些常見的函數接口:
java.util.function.Predicate
該接口包含方法boolean test(T t),該接口一般用于條件的檢測,内部包含三個預設方法:and、or、negate、,即與或非,用于各式的條件判斷。例:
Predicate<Integer> predicate = x -> x > 3;
predicate.test(10);//true
predicate.negate().test(10);//false
predicate.or(x -> x < 1).and(x -> x > -1).negate().test(-1);//true
注意:在這裡與或非的判斷順序是從左到右的,調用的順序會影響結果。
java.util.Comparator
Comparator是Java中的經典接口,在排序中較為常用。Java8在此之上添加了一些新的預設方法,來豐富該接口的功能。例:
Integer[] a = new Integer[]{3, 1, 2, 4, 6, 5};
Comparator<Integer> comparator = Integer::compare;
Arrays.sort(a, comparator);
System.out.println("升序:" + Arrays.toString(a));
Arrays.sort(a,comparator.reversed());
System.out.println("降序:"+Arrays.toString(a));
結果
升序:[1, 2, 3, 4, 5, 6]
降序:[6, 5, 4, 3, 2, 1]
java.util.function.Supplier
該類隻包含方法:
T get();
Supplier接口是在1.8中新出現的函數接口,用于支援函數式程式設計。它用于傳回一個任意泛型的執行個體對象,與工廠的功能類似。
java.util.function.Consumer
該接口表示一個接受單個輸入參數并且沒有傳回值的操作。不像其他函數式接口,Consumer接口期望執行修改内容的操作。例如 ,我們需要一個批量修改People的方法,利用Predicate和Consumer就可以這麼寫
在People内增加updateMany方法:
public static List updateMany(List<People> peopleList, Predicate<People> predicate, Consumer<People> consumer) {
for (int i = 0; i < peopleList.size(); i++) {
if (predicate.test(peopleList.get(i))) {
consumer.accept(peopleList.get(i));
}
}
return peopleList;
}
調用:
//批量修改,将age<18的對象的age改為18
People.updateMany(peopleList,
p -> p.getAge() < 18,
p -> p.setAge(18));
System.out.println("修改後的結果:" + peopleList);
通過這種方式,可以将内部的判斷邏輯與修改代碼放至外部調用,而将for、if等語句封裝至内部,提高代碼的可讀性。
其他的還有一些函數接口,如Runnable,InvocationHandler等,在這裡就不闡述了。有興趣的大家可以自行查詢資料。Stream、Function、Optional也是函數接口,将在下面進行詳細介紹。
Function
說明
Java8提供的java.util.function包的核心函數接口有4個。
- 函數型T ->R,完成參數類型T向結果類型R的轉換和資料處理。核心函數接口Function
- 判斷型T ->boolean,核心函數接口Predicate
- 消費型T ->void,核心函數接口Consumer
- 供給型void->T,核心函數接口Supplier
Function接口是為Java8提供了函數式程式設計的基礎,apply方法與Consumer的accept方法功能類似,但是提供了傳回及類型轉換的可能,功能更加強大;再通過andThen與compose方法可以使Function組成Function功能鍊,進行多級資料處理及轉換。
主要方法
- R apply(T t) – 将Function對象應用到輸入的參數上,然後傳回計算結果。
- default Function<T,V> andThen(Function<? super R,? extends V> after) 傳回一個先執行目前函數對象apply方法再執行after函數對象apply方法的函數對象。
- default Function<T,V> compose(Function<? super V,? extends T> before)傳回一個先執行before函數對象apply方法再執行目前函數對象apply方法的函數對象。
- static Function<T,T> identity() 傳回一個執行了apply()方法之後隻會傳回輸入參數的函數對象。
方法詳解
apply:
R apply(T t);
接收類型:T
傳回類型:R
類型轉換:T→R
Function接口的核心方法,可以執行任意的操作,且具有傳回值。接收一個T類型的對象,在經過處理後,傳回一個R類型的對象。主要功能為類型轉換及資料處理。
compose:
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
接收類型:Function<? super V, ? extends T>
傳回類型:Function<V, R>
類型轉換:(V+)→(T-)→T→R
apply執行順序:before→this
此處“V+”指代“? super V”,表示包含V在内的V的任意父類;"T-"指代“? extends T”,表示包含T在内的T的任意子類。compose方法傳回一個Function<V,R>,這個Function先執行before的apply方法,将V+類型的資料轉換為T-類型,再将T-作為參數傳遞給this的apply方法,将T類型轉換為R類型。
通過compose方法,可以在某個Function執行之前插入一個Function執行。由于傳回類型依舊為Function,可以重複調用compose方法形成方法鍊。
andThen:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
接收類型:Function<? super R, ? extends V>
傳回類型:Function<T, V>
類型轉換:T→R→(R+)→(V-)
apply執行順序:this→after
此處“R+”指代“? super R”,表示包含R在内的R的任意父類;"V-"指代“? extends V”,表示包含V在内的V的任意子類。andThen方法傳回一個Function<T,V>,這個Function先執行this的apply方法,将T類型的資料轉換為R類型,再将R作為參數傳遞給after的apply方法,将R+類型轉換為V-類型。
通過andThen方法,可以在某個Function執行之後插入一個Function執行。由于傳回類型依舊為Function,可以重複調用andThen方法形成方法鍊。
identity:
static <T> Function<T, T> identity() {
return t -> t;
}
接收類型:無
傳回類型:Function<T, T>
類型轉換:T→T
該方法的說明是:傳回一個函數,它總是傳回輸入參數。調用該方法可以得到一個傳回輸入參數的Funtion,這個Function就可以單純的用來做資料處理,而不用類型轉換。
Stream
Java8中提供了Stream API,即流式處理。可以通過将List、Set、Array等對象轉換成流進行操作。Stream内的流操作分為兩種:中間操作和最終操作,中間操作會傳回一個全新的Stream對象,意味着你的操作不會影響最初的流;最終操作會将流進行轉換或者操作,傳回非Stream的對象。Stream可以替代傳統的循環操作,從線程上差別,Stream分為串行(Stream)和并行(parallelStream),關于Stream的性能分析可以檢視這篇文章《Stream性能分析》。下面來看下Strea内的一些方法:
中間操作
- distinct
去除Stream中重複的對象,并傳回一個流。(使用對象的equals方法)Stream<T> distinct();
- skip
跳過Stream中的前n個對象,将其他對象傳回一個Stream。如果n超過了Stream中對象的個數,則會傳回一個空的Stream。Stream<T> skip(long n);
- limit
截取Stream的前maxSize個對象,并形成一個新Stream。Stream<T> limit(long maxSize);
- filter
根據給定的predicate來過濾對象,傳回滿足條件的對象構成的Stream。Stream<T> filter(Predicate<? super T> predicate);
- map
通過給定的mapper,将T類型的流轉換為R類型的Stream。<R> Stream<R> map(Function<? super T, ? extends R> mapper);
- flatMap
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
flatMap也是将Stream進行轉換,flatMap與map的差別在于 flatMap是将一個Stream中的每個值都轉成一個個Stream,然後再将這些流扁平化成為一個Stream。
例(轉自:Java8 新特性之流式資料處理):
假設我們有一個字元串數組String[] strs = {"java8", "is", "easy", "to", "use"};,我們希望輸出構成這一數組的所有非重複字元,那麼我們可能首先會想到如下實作:
在執行map操作以後,我們得到是一個包含多個字元串(構成一個字元串的字元數組)的流,此時執行distinct操作是基于在這些字元串數組之間的對比,是以達不到我們希望的目的,此時的輸出為:List<String[]> distinctStrs = Arrays.stream(strs) .map(str -> str.split("")) // 映射成為Stream<String[]> .distinct() .collect(Collectors.toList());
distinct隻有對于一個包含多個字元的流進行操作才能達到我們的目的,即對Stream進行操作。此時flatMap就可以達到我們的目的:[j, a, v, a, 8] [i, s] [e, a, s, y] [t, o] [u, s, e]
flatMap将由map映射得到的Stream<String[]>,轉換成由各個字元串數組映射成的流Stream,再将這些小的流扁平化成為一個由所有字元串構成的大流Steam,進而能夠達到我們的目的。List<String> distinctStrs = Arrays.stream(strs) .map(str -> str.split("")) // 映射成為Stream<String[]> .flatMap(Arrays::stream) // 扁平化為Stream<String> .distinct() .collect(Collectors.toList());
- sorted
sorted方法可以對Stream進行排序。排序的對象必須實作Comparable,如果沒實作會抛出ClassCastException;不提供comparator時,則會調用compareTo方法。Stream<T> sorted(); Stream<T> sorted(Comparator<? super T> comparator);
- peek
Stream<T> peek(Consumer<? super T> action);
對流中的每個對象執行提供的action操作。
在Stack中,peek用于檢視一個對象。在流中也是一樣,用于在流循環時,根據給定的action進行檢視對象。雖然可以進行元素修改操作,但不建議。
- 綜合例:
輸出:Integer[] a = new Integer[]{3, 1, 2, 5, 11, 4, 6, 5, 3, 1}; List<Integer> aList = Arrays.stream(a) .distinct() .skip(1) .filter((e) -> e < 6) .peek(e -> System.out.println("循環1次")) .limit(4) .sorted() .collect(Collectors.toList()); System.out.println(aList);
循環1次 循環1次 循環1次 循環1次 [1, 2, 4, 5]
最終操作
- 聚合
- max & min
根據給定的comparator傳回Stream中的max或min。Optional<T> min(Comparator<? super T> comparator); Optional<T> max(Comparator<? super T> comparator);
- count
傳回Stream中對象的個數。long count();
- 比對
- anyMatch & allMatch & noneMatch
根據給定的predicate判斷Stream是否比對條件。boolean anyMatch(Predicate<? super T> predicate); boolean allMatch(Predicate<? super T> predicate); boolean noneMatch(Predicate<? super T> predicate);
- collect
<R, A> R collect(Collector<? super T, A, R> collector);
根據給定的collector對Stream中的元素進行操作,傳回複雜資料結構的對象。用于将Stream中的對象轉換成我們想要的結構,如list、map、set等。
前例中就使用collect(Collectors.toList())将Stream中的對象轉換成List。
- reduce
Optional<T> reduce(BinaryOperator<T> accumulator); T reduce(T identity, BinaryOperator<T> accumulator);
如果我們不知希望單純的傳回List這樣的類型,而是希望将整個Stream經過一些操作後,規約成一個對象傳回,就可以用到規約操作。reduce方法有兩個參數,其中accumulator代表着規約的操作,即用何種的方式進行參數化處理;identity則是accumulator的辨別值(具體用處暫不明)。
例:求和
Integer[] a = new Integer[]{3, 1, 2, 5, 11, 4, 6, 5, 3, 1}; int sum = Arrays.stream(a) .distinct() .filter((e) -> e < 6) .reduce(0, (x, y) -> x + y);//或.reduce(0, Integer::sum); System.out.println(sum);//15
- toArray
将Stream中的對象傳回成一個Object數組。Object[] toArray();
- forEach
顧名思義,對Stream中每個元素進行action操作,與peek類似,但forEach是一個最終操作,一般在結束時檢視對象使用。void forEach(Consumer<? super T> action);
- findFirst & findAny
Optional<T> findFirst(); Optional<T> findAny();
findFirst可以傳回Stream中第一個對象,并将它封裝在Optional中。
findAny則不是傳回第一個對象,而是任意一個對象。在順序Stream中findFirst和findAny的結果是一緻的,但在并行Stream中,findFirst存在着限制,故在并行Stream中需要使用findAny(findAny源碼注釋中寫的是some element?)。同樣将對象封裝在Optional中。
Optional API
在java8之前的程式設計中,我們總是需要進行if(obj=null)來防止NullPointException,而在java8後,提供了Optional類,它一方面用于防止NullPotinException的判斷,另一方面則為流式程式設計與函數式變成提供了更好的支援;Optional是一個包含對象的容器,它可以包含null值。在Optional類中封裝了許多的方法,來讓我們更好的處理我們的代碼。接下來看看Optional中幾個常用的方法:
- empty & of & ofNullable
首先,Optioanl的構造方法是私有的,隻能通過以上三個靜态方法來擷取Optional的執行個體。empty方法會傳回Optional中的常量EMPTY對象,一般在compare時使用,注意這裡的EMPTY是單例的而且為常量;一般我們需要構造一個Optional,使用of或ofNullable方法,of方法會将我們的傳值構造一個新的Optional傳回,而ofNullable則在接收null時傳回EMPTY執行個體。public static <T> Optional<T> empty(){...} public static <T> Optional<T> of(T value) {return new Optional<T>(value);} public static <T> Optional<T> ofNullable(T value){return value == null ? empty() : of(value);}
- isPresent
isPresent方法用于判斷Optional包含的value是否為null,第一種方法傳回一個boolean;第二種方法則根據判斷,為null則什麼都不執行,不為null則執行一個consumer操作。public boolean isPresent() {return value != null;} public void ifPresent(Consumer<? super T> consumer) {if (value != null)consumer.accept(value);}
- map & flatMap
map與flatMap與Stream中用法與功能大緻相同,都是轉換及合并轉換,不再贅述。public<U> Optional<U> map(Function<? super T, ? extends U> mapper){...} public<U> Optional<U> flatMap(Function<? super T, Optional<U> mapper){...}
- get
get方法用于擷取value。需要注意的是,如果value為null,則會抛出NoSuchElementException。public T get() {...}
- filter
filter方法也是擷取value,它可以傳入一個predicate,用于判斷value是否滿足條件。如果value為null,則會傳回this;如果predicate.test為true,則傳回this,否則會傳回EMPTY。public Optional<T> filter(Predicate<? super T> predicate) {...}
- orElse & orElseGet & orElseGet
這三個方法都用于擷取value,同時可以在valuenull的情況下做出不同的操作。orElse可以傳入一個other,當valuenull時則傳回null;orElseGet則是使用Supplier,為null時調用get方法;orElseThrow則是接收一個Supplier包含某種異常的exceptionSupplier,為null時則會調用get方法抛出一個異常。public T orElse(T other) {return value != null ? value : other;} public T orElseGet(Supplier<? extends T> other) {return value != null ? value : other.get();} public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X{...}
Date Time API
Java8使用新的日期時間API覆寫舊的日期時間API的,處理了以下缺點。
- 線程安全 - java.util.Date不是線程安全的,是以開發者必須在使用日期處理并發性問題。新的日期時間API是不可變的,并且沒有setter方法。
- 設計問題 - 預設的開始日期為1900年,月的開始月份為0而不是1,沒有統一。不直接使用方法操作日期。新的API提供了這樣操作實用方法。
- 時區處理困難 - 開發人員必須編寫大量的代碼來處理時區的問題。新的API設計開發為這些特定領域提供了幫助。
JAVA8引入了java.time包,一個新的日期時間API。限于篇幅與精力問題,這裡不對java.time進行過多的介紹,這裡推薦幾篇個人覺得不錯的博文以供研究:
Java 類庫的新特性之日期時間API (Date/Time API )
Java 8 之 java.time 包