Java8 新特性简介
Java8是2014年發布的,至今也已經有快三年的時間了,之前雖然有學習過,但是學的比較零散,不成系統,而且也沒有覆蓋到Java8所有的特性。 由于公司已經使用了JDK1.8,所以工作中能使用Java8的機會還是很多的,因此決定來系統地學習一下Java8的新特性,這是對我最近學習Java8的一些記錄, 以備在有些細節記不太清的時候可以查詢。
先來一個概覽,上圖是我整理的Java8中的新特性,總的來看,大致上可以分成這么幾個大塊。
函數式接口
所謂的函數式接口就是只有一個抽象方法的接口,注意這里說的是抽象方法,因為Java8中加入了默認方法的特性,但是函數式接口是不關心接口中有沒有默認方法的。 一般函數式接口可以使用@FunctionalInterface注解的形式來標注表示這是一個函數式接口,該注解標注與否對函數式接口沒有實際的影響, 不過一般還是推薦使用該注解,就像使用@Override注解一樣。JDK1.8中提供了一些函數式接口如下:
| Predicate<T> | T -> boolean | IntPredicate, LongPredicate, DoublePredicate |
| Consumer<T> | T -> void | IntConsumer, LongConsumer, DoubleConsumer |
| Function<T,R> | T -> R | IntFunction<R>, IntToDoubleFunction, IntToLongFunction, LongFunction<R>, LongToDoubleFunction, LongToIntFunction, DoubleFunction<R>, ToIntFunction<T>, ToDoubleFunction<T>, ToLongFunction<T> |
| Supplier<T> | () -> T | BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier |
| UnaryOperator<T> | T -> T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator |
| BinaryOperator<T> | (T,T) -> T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator |
| BiPredicate<L,R> | (L,R) -> boolean | ? |
| BiConsumer<T,U> | (T,U) -> void | ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T> |
| BiFunction<T,U,R> | (T,U) -> R | ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U> |
上表中的原始類型特化指的是為了消除自動裝箱和拆箱的性能開銷,JDK1.8提供的針對基本類型的函數式接口。
Lambda表達式和方法引用
有了函數式接口之后,就可以使用Lambda表達式和方法引用了。其實函數式接口的表中的函數描述符就是Lambda表達式,在函數式接口中Lambda表達式相當于匿名內部類的效果。 舉個簡單的例子:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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表達式串起來使用:
| 1 | Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150).or(a -> “green”.equals(a.getColor())); |
上面這行代碼把兩個Lambda表達式串了起來,含義是選擇重量大于150或者綠色的蘋果。
方法引用可以看作Lambda表達式的更簡潔的一種表達形式,使用::操作符,方法引用主要有三類:
舉個方法引用的簡單的例子:
| 1 2 3 4 | Function<String, Integer> stringToInteger = (String s) -> Integer.parseInt(s); //使用方法引用 Function<String, Integer> stringToInteger = Integer::parseInt; |
方法引用中還有一種特殊的形式,構造函數引用,假設一個類有一個默認的構造函數,那么使用方法引用的形式為:
| 1 2 3 4 5 6 7 | Supplier<SomeClass> c1 = SomeClass::new; SomeClass s1 = c1.get(); //等價于 Supplier<SomeClass> c1 = () -> new SomeClass(); SomeClass s1 = c1.get(); |
如果是構造函數有一個參數的情況:
| 1 2 3 4 5 6 7 | 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提供了非常豐富的操作,包括篩選、切片、映射、查找、匹配、歸約等等, 這些操作又可以分為中間操作和終端操作,中間操作會返回一個流,因此我們可以使用多個中間操作來作鏈式的調用,當使用了終端操作之后,那么這個流就被認為是被消費了, 每個流只能有一個終端操作。
| 1 2 3 4 5 6 | //篩選后收集到一個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非常豐富,可以很好的滿足我們的需求。
| filter | 中間 | Stream<T> | Predicate<T> | T -> boolean |
| distinct | 中間 | Stream<T> | ? | ? |
| skip | 中間 | Stream<T> | long | ? |
| limit | 中間 | Stream<T> | long | ? |
| map | 中間 | Stream<R> | Function<T,R> | T -> R |
| flatMap | 中間 | Stream<R> | Function<T, Stream<R>> | T -> Stream<R> |
| sorted | 中間 | Stream<R> | Comparator<T> | (T,T) -> int |
| anyMatch | 終端 | boolean | Predicate<T> | T -> boolean |
| noneMatch | 終端 | boolean | Predicate<T> | T -> boolean |
| allMatch | 終端 | boolean | Predicate<T> | T -> boolean |
| findAny | 終端 | Optional<T> | ? | ? |
| findFirst | 終端 | Optional<T> | ? | ? |
| forEach | 終端 | void | Consumer<T> | T -> void |
| collect | 終端 | R | Collector<T,A,R> | ? |
| reduce | 終端 | Optional<T> | BinaryOperator<T> | (T,T) -> T |
| count | 終端 | long | ? | ? |
與函數式接口類似,Stream也提供了原始類型特化的流,比如說IntStream等:
| 1 2 | //maoToInt轉化為一個IntStream int count = list.stream().mapToInt(list::getNumber).sum(); |
并行流與串行流的區別就在于將stream改成parallelStream,并行流會將流的操作拆分,放到線程池中去執行,但是并不是說使用并行流的性能一定好于串行的流, 恰恰相反,可能大多數時候使用串行流會有更好的性能,這是因為將任務提交到線程池,執行完之后再合并,這些本身都是有不小的開銷的。關于并行流其實還有非常多的細節, 這里做一個拋磚引玉,有興趣的同學可以在網上自行查找一些資料來學習。
默認方法
默認方法出現的原因是為了對原有接口的擴展,有了默認方法之后就不怕因改動原有的接口而對已經使用這些接口的程序造成的代碼不兼容的影響。 在Java8中也對一些接口增加了一些默認方法,比如Map接口等等。一般來說,使用默認方法的場景有兩個:可選方法和行為的多繼承。
默認方法的使用相對來說比較簡單,唯一要注意的點是如何處理默認方法的沖突。關于如何處理默認方法的沖突可以參考以下三條規則:
| 1 2 3 4 5 6 7 | public class C implements B, A { ????public void hello() { ????????B.super().hello();??? ????} } |
使用X.super.m(..)顯式地調用希望調用的方法。
Optional
如果一個方法返回一個Object,那么我們在使用的時候總是要判斷一下返回的結果是否為空,一般是這樣的形式:
| 1 2 3 | if (a != null) { ????//do something... } |
但是簡單的情況還好,如果復雜的情況下每一個都要去檢查非常麻煩,而且寫出來的代碼也不好看、很臃腫,但是如果不檢查就很容易遇到NullPointerException, Java8中的Optional就是為此而設計的。
Optional一般使用在方法的返回值中,如果使用Optional來包裝方法的返回值,這就表示方法的返回值可能為null,需要使用Optional提供的方法來檢查,如果為null,還可以提供一個默認值。
| 1 2 3 4 5 6 7 8 | //創建Optional對象 Optional<String> opt = Optional.empty(); //依據一個非空值創建Optional Optional<String> opt = Optional.of("hello"); //可接受null的Optional Optional<String> opt = Optional.ofNullable(null); |
除了以上這些方法外,Optional還提供了以下方法:
| empty | 返回一個空的Optional實例 |
| filter | 如果值存在并且滿足提供的謂詞,就返回包括該值的Optional對象;否則返回一個空的Optional對象 |
| flatMap | 如果值存在,就對該值執行提供的mapping函數調用,返回一個Optional類型的值,否則就返回一個空的Optional對象 |
| get | 如果該值存在,將該值用Optional封裝返回,否則拋出一個NoSuchElementException異常 |
| ifPresent | 如果值存在,就執行使用該值的方法調用,否則返回false |
| isPresent | 如果值存在就返回true,否則返回false |
| map | 如果值存在,就對該值執行提供的mapping函數調用 |
| of | 將指定值用Optional封裝之后返回,如果該值為null,拋出一個NullPointerException異常 |
| ofNullable | 將指定值用Optional封裝之后返回,如果該值為null,則返回一個空的Optional對象 |
| orElse | 如果有值則將其返回,否則返回一個默認值 |
| orElseGet | 如果有值則將其返回,否則返回一個由指定的Supplier接口生成的值 |
| orElseThrow | 如果有值則將其返回,否則拋出一個由指定的Supplier接口生成的異常 |
CompletableFuture
在Java8之前,我們會使用JDK提供的Future接口來進行一些異步的操作,其實CompletableFuture也是實現了Future接口, 并且基于ForkJoinPool來執行任務,因此本質上來講,CompletableFuture只是對原有API的封裝, 而使用CompletableFuture與原來的Future的不同之處在于可以將兩個Future組合起來,或者如果兩個Future是有依賴關系的,可以等第一個執行完畢后再實行第二個等特性。
先來看看基本的使用方式:
| 1 2 3 4 5 6 7 8 | 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,例如:
| 1 | Fufure<Double> price = CompletableFuture.supplyAsync(() -> calculatePrice(product)); |
代碼是不是一下子簡潔了許多呢。之前說了,CompletableFuture可以組合多個Future,不管是Future之間有依賴的,還是沒有依賴的。 如果第二個請求依賴于第一個請求的結果,那么可以使用thenCompose方法來組合兩個Future
| 1 2 3 4 5 6 7 8 9 | 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之間沒有依賴關系的例子:
| 1 2 | Future<String> futurePriceInUsd = CompletableFuture.supplyAsync(() -> shop.getPrice(“price1”)) ????????????????????????????????????.thenCombine(CompletableFuture.supplyAsync(() -> shop.getPrice(“price2”)), (s1, s2) -> s1 + s2); |
這里有兩個異步的任務,使用thenCombine方法來組合兩個Future,thenCombine方法的第二個參數就是用來合并兩個Future方法返回值的操作函數。
有時候,我們并不需要等待所有的異步任務結束,只需要其中的一個完成就可以了,CompletableFuture也提供了這樣的方法:
| 1 2 3 4 | //假設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的簡單的例子:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | //創建日期 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的使用可以有一個比較清楚的了解。
總結
以上是生活随笔為你收集整理的Java8 新特性简介的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于wampserver和Eclipse
- 下一篇: java美元兑换,(Java实现) 美元