文章摘自公衆號importNew:
http://mp.weixin.qq.com/s?__biz=MjM5NzMyMjAwMA==&mid=2651478449&idx=1&sn=4946a62a03f6d4fec988d238cec5244f&chksm=bd2535ce8a52bcd84ea86458f3a5138f1291d914a658d796556528b354c31826262bec60457c&mpshare=1&scene=23&srcid=0608xxRVS5JLuijaiPY6FaxZ#rd
有興趣的同學可以關注這個公衆號。
Java8是2014年發佈的,至今也已經有快三年的時間了,之前雖然有學習過,但是學的比較零散,不成系統,而且也沒有覆蓋到Java8所有的特性。 由於公司已經使用了JDK1.8,所以工作中能使用Java8的機會還是很多的,因此決定來系統地學習一下Java8的新特性,這是對我最近學習Java8的一些記錄, 以備在有些細節記不太清的時候可以查詢。
先來一個概覽,上圖是我整理的Java8中的新特性,總的來看,大致上可以分成這麼幾個大塊。
函數式接口
所謂的函數式接口就是隻有一個抽象方法的接口,注意這裏說的是抽象方法,因爲Java8中加入了默認方法的特性,但是函數式接口是不關心接口中有沒有默認方法的。 一般函數式接口可以使用@FunctionalInterface註解的形式來標註表示這是一個函數式接口,該註解標註與否對函數式接口沒有實際的影響, 不過一般還是推薦使用該註解,就像使用@Override註解一樣。JDK1.8中提供了一些函數式接口如下:
上表中的原始類型特化指的是爲了消除自動裝箱和拆箱的性能開銷,JDK1.8提供的針對基本類型的函數式接口。
Lambda表達式和方法引用
有了函數式接口之後,就可以使用Lambda表達式和方法引用了。其實函數式接口的表中的函數描述符就是Lambda表達式,在函數式接口中Lambda表達式相當於匿名內部類的效果。 舉個簡單的例子:
public class TestLambda {
public static void execute(Runnable runnable) {
runnable.run();
}
public static void main(String[] args) {
//Java8之前
execute(new Runnable() {
@Override
public void run() {
System.out.println("run");
}
});
//使用Lambda表達式
execute(() -> System.out.println("run"));
}
}
可以看到,相比於使用匿名內部類的方式,Lambda表達式可以使用更少的代碼但是有更清晰的表述。注意,Lambda表達式也不是完全等價於匿名內部類的, 兩者的不同點在於this的指向和本地變量的屏蔽上。
Lambda表達式還可以複合,把幾個Lambda表達式串起來使用:
Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150).or(a -> “green”.equals(a.getColor()));
上面這行代碼把兩個Lambda表達式串了起來,含義是選擇重量大於150或者綠色的蘋果。
方法引用可以看作Lambda表達式的更簡潔的一種表達形式,使用::操作符,方法引用主要有三類:
-
指向靜態方法的方法引用(例如Integer的parseInt方法,寫作Integer::parseInt);
-
指向任意類型實例方法的方法引用(例如String的length方法,寫作String::length);
-
指向現有對象的實例方法的方法引用(例如假設你有一個本地變量localVariable用於存放Variable類型的對象,它支持實例方法getValue,那麼可以寫成localVariable::getValue)。
舉個方法引用的簡單的例子:
Function<String, Integer> stringToInteger = (String s) -> Integer.parseInt(s);
//使用方法引用
Function<String, Integer> stringToInteger = Integer::parseInt;
方法引用中還有一種特殊的形式,構造函數引用,假設一個類有一個默認的構造函數,那麼使用方法引用的形式爲:
Supplier<SomeClass> c1 = SomeClass::new;
SomeClass s1 = c1.get();
//等價於
Supplier<SomeClass> c1 = () -> new SomeClass();
SomeClass s1 = c1.get();
如果是構造函數有一個參數的情況:
Function<Integer, SomeClass> c1 = SomeClass::new;
SomeClass s1 = c1.apply(100);
//等價於
Function<Integer, SomeClass> c1 = i -> new SomeClass(i);
SomeClass s1 = c1.apply(100);
Stream
Stream可以分成串行流和並行流,並行流是基於Java7中提供的ForkJoinPool來進行任務的調度,達到並行的處理的目的。 集合是我們平時在進行Java編程時非常常用的API,使用Stream可以幫助更好的來操作集合。Stream提供了非常豐富的操作,包括篩選、切片、映射、查找、匹配、歸約等等, 這些操作又可以分爲中間操作和終端操作,中間操作會返回一個流,因此我們可以使用多箇中間操作來作鏈式的調用,當使用了終端操作之後,那麼這個流就被認爲是被消費了, 每個流只能有一個終端操作。
//篩選後收集到一個List中
List<Apple> vegetarianMenu = apples.stream().filter(Apple::isRed).collect(Collectors.toList());
//篩選加去重
List<Integer> numbers = Arrays.asList(1,2,1,3,3,2,4);
numbers.stream().filter(i -> i % 2 == 0).distinct().forEach(System.out::println);
以上都是一些簡單的例子,Stream提供的API非常豐富,可以很好的滿足我們的需求。
與函數式接口類似,Stream也提供了原始類型特化的流,比如說IntStream等:
//maoToInt轉化爲一個IntStream
int count = list.stream().mapToInt(list::getNumber).sum();
並行流與串行流的區別就在於將stream改成parallelStream,並行流會將流的操作拆分,放到線程池中去執行,但是並不是說使用並行流的性能一定好於串行的流, 恰恰相反,可能大多數時候使用串行流會有更好的性能,這是因爲將任務提交到線程池,執行完之後再合併,這些本身都是有不小的開銷的。關於並行流其實還有非常多的細節, 這裏做一個拋磚引玉,有興趣的同學可以在網上自行查找一些資料來學習。
默認方法
默認方法出現的原因是爲了對原有接口的擴展,有了默認方法之後就不怕因改動原有的接口而對已經使用這些接口的程序造成的代碼不兼容的影響。 在Java8中也對一些接口增加了一些默認方法,比如Map接口等等。一般來說,使用默認方法的場景有兩個:可選方法和行爲的多繼承。
默認方法的使用相對來說比較簡單,唯一要注意的點是如何處理默認方法的衝突。關於如何處理默認方法的衝突可以參考以下三條規則:
-
類中的方法優先級最高。類或父類中聲明的方法的優先級高於任何聲明爲默認方法的優先級。
-
如果無法依據第一條規則進行判斷,那麼子接口的優先級更高:函數簽名相同時,優先選擇擁有最具體實現的默認方法的接口。即如果B繼承了A,那麼B就比A更具體。
-
最後,如果還是無法判斷,繼承了多個接口的類必須通過顯式覆蓋和調用期望的方法,顯式地選擇使用哪一個默認方法的實現。那麼如何顯式地指定呢:
public class C implements B, A {
public void hello() {
B.super().hello();
}
}
使用X.super.m(..)顯式地調用希望調用的方法。
Optional
如果一個方法返回一個Object,那麼我們在使用的時候總是要判斷一下返回的結果是否爲空,一般是這樣的形式:
if (a != null) {
//do something...
}
但是簡單的情況還好,如果複雜的情況下每一個都要去檢查非常麻煩,而且寫出來的代碼也不好看、很臃腫,但是如果不檢查就很容易遇到NullPointerException, Java8中的Optional就是爲此而設計的。
Optional一般使用在方法的返回值中,如果使用Optional來包裝方法的返回值,這就表示方法的返回值可能爲null,需要使用Optional提供的方法來檢查,如果爲null,還可以提供一個默認值。
//創建Optional對象
Optional<String> opt = Optional.empty();
//依據一個非空值創建Optional
Optional<String> opt = Optional.of("hello");
//可接受null的Optional
Optional<String> opt = Optional.ofNullable(null);
除了以上這些方法外,Optional還提供了以下方法:
CompletableFuture
在Java8之前,我們會使用JDK提供的Future接口來進行一些異步的操作,其實CompletableFuture也是實現了Future接口, 並且基於ForkJoinPool來執行任務,因此本質上來講,CompletableFuture只是對原有API的封裝, 而使用CompletableFuture與原來的Future的不同之處在於可以將兩個Future組合起來,或者如果兩個Future是有依賴關係的,可以等第一個執行完畢後再實行第二個等特性。
先來看看基本的使用方式:
public Future<Double> getPriceAsync(final String product) {
final CompletableFuture<Double> futurePrice = new CompletableFuture<>();
new Thread(() -> {
double price = calculatePrice(product);
futurePrice.complete(price); //完成後使用complete方法,設置future的返回值
}).start();
return futurePrice;
}
得到Future之後就可以使用get方法來獲取結果,CompletableFuture提供了一些工廠方法來簡化這些API,並且使用函數式編程的方式來使用這些API,例如:
Fufure<Double> price = CompletableFuture.supplyAsync(() -> calculatePrice(product));
代碼是不是一下子簡潔了許多呢。之前說了,CompletableFuture可以組合多個Future,不管是Future之間有依賴的,還是沒有依賴的。 如果第二個請求依賴於第一個請求的結果,那麼可以使用thenCompose方法來組合兩個Future
public List<String> findPriceAsync(String product) {
List<CompletableFutute<String>> priceFutures = tasks.stream()
.map(task -> CompletableFuture.supplyAsync(() -> task.getPrice(product),executor))
.map(future -> future.thenApply(Work::parse))
.map(future -> future.thenCompose(work -> CompletableFuture.supplyAsync(() -> Count.applyCount(work), executor)))
.collect(Collectors.toList());
return priceFutures.stream().map(CompletableFuture::join).collect(Collectors.toList());
}
上面這段代碼使用了thenCompose來組合兩個CompletableFuture。supplyAsync方法第二個參數接受一個自定義的Executor。 首先使用CompletableFuture執行一個任務,調用getPrice方法,得到一個Future,之後使用thenApply方法,將Future的結果應用parse方法, 之後再使用執行完parse之後的結果作爲參數再執行一個applyCount方法,然後收集成一個CompletableFuture<String>的List, 最後再使用一個流,調用CompletableFuture的join方法,這是爲了等待所有的異步任務執行完畢,獲得最後的結果。
注意,這裏必須使用兩個流,如果在一個流裏調用join方法,那麼由於Stream的延遲特性,所有的操作還是會串行的執行,並不是異步的。
再來看一個兩個Future之間沒有依賴關係的例子:
Future<String> futurePriceInUsd = CompletableFuture.supplyAsync(() -> shop.getPrice(“price1”))
.thenCombine(CompletableFuture.supplyAsync(() -> shop.getPrice(“price2”)), (s1, s2) -> s1 + s2);
這裏有兩個異步的任務,使用thenCombine方法來組合兩個Future,thenCombine方法的第二個參數就是用來合併兩個Future方法返回值的操作函數。
有時候,我們並不需要等待所有的異步任務結束,只需要其中的一個完成就可以了,CompletableFuture也提供了這樣的方法:
//假設getStream方法返回一個Stream<CompletableFuture<String>>
CompletableFuture[] futures = getStream(“listen”).map(f -> f.thenAccept(System.out::println)).toArray(CompletableFuture[]::new);
//等待其中的一個執行完畢
CompletableFuture.anyOf(futures).join();
使用anyOf方法來響應CompletableFuture的completion事件。
新的時間和日期API
Java8之前的時間和日期API並不好用,而且在線程安全性等方面也存在問題,一般會藉助一些開源類庫來解決時間處理的問題。在JDK1.8中新加入了時間和日期的API, 藉助這些新的API基本可以不再需要開源類庫的幫助來完成時間的處理了。
Java8中加入了LocalDateTime, LocalDate, LocalTime, Duration, Period, Instant, DateTimeFormatter等等API,來看一些使用這些API的簡單的例子:
//創建日期
LocalDate date = LocalDate.of(2017,1,21); //2017-01-21
int year = date.getYear() //2017
Month month = date.getMonth(); //JANUARY
int day = date.getDayOfMonth(); //21
DayOfWeek dow = date.getDayOfWeek(); //SATURDAY
int len = date.lengthOfMonth(); //31(days in January)
boolean leap = date.isLeapYear(); //false(not a leap year)
//時間的解析和格式化
LocalDate date = LocalDate.parse(“2017-01-21”);
LocalTime time = LocalTime.parse(“13:45:20”);
LocalDateTime now = LocalDateTime.now();
now.format(DateTimeFormatter.BASIC_ISO_DATE);
//合併日期和時間
LocalDateTime dt1 = LocalDateTime.of(2017, Month.JANUARY, 21, 18, 7);
LocalDateTime dt2 = LocalDateTime.of(localDate, time);
LocalDateTime dt3 = localDate.atTime(13,45,20);
LocalDateTime dt4 = localDate.atTime(time);
LocalDateTime dt5 = time.atDate(localDate);
//操作日期
LocalDate date1 = LocalDate.of(2014,3,18); //2014-3-18
LocalDate date2 = date1.plusWeeks(1); //2014-3-25
LocalDate date3 = date2.minusYears(3); //2011-3-25
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS); //2011-09-25
可以發現,新的時間和日期API都是不可變的,並且是線程安全的,之前使用的比如SimpleDateFormat不是線程安全的, 現在可以使用DateTimeFormatter來代替,DateTimeFormatter是線程安全的。
以上只是Java8提供的新時間和日期API的一部分,更多的內容可以參考官網文檔,有了這些API,相信完全可以不再依賴開源的類庫來進行時間的處理。
小結
以上只是對Java8的新特性進行了一個非常簡單的介紹,由於近年來函數式編程很火,Java8也受函數式編程思想的影響,吸收了函數式編程好的地方, 很多新特性都是按照函數式編程來設計的。關於Java8還有非常多的細節沒有提到,這些需要我們自行去學習,推薦一本學習Java8非常好的書籍——《Java8實戰》, 看完這本書對Java8的使用可以有一個比較清楚的瞭解。