目錄:
Java8新特性概要Lamda表達式函數(shù)接口方法引用Stream1 Stream工作方式2 不同類型的streams3 操作的順序4 重復使用Stream5 一些重要的操作總結(jié)
Java 8 在 2014年3月18日進行了發(fā)布,相較之前的版本,有了許多的改變,無論是從語法上還是類庫上,都有了很多的變化,更加有利于程序的編寫,本文用于此次小組分享,從其中個人認為的較重要的幾個方面進行闡述。 總體來說,有了以下幾個方面: 1. Lamda表達式 2. 函數(shù)接口 3. 類庫的增加 4. 工具類
Lamda表達式也被稱之為閉包(closures),利用該特性,我們可以將一個函數(shù)當做方法的參數(shù)進行傳遞,也就是將代碼當做數(shù)據(jù)來看待。 由于以上原因,有人將Lamda表達式當做是匿名內(nèi)部類的語法糖(Syntactic suger)來看待,但從虛擬機實現(xiàn)角度來看,并不是如此,因為Lamda表達式在編譯的時候,并不會生成xxx$1.class的匿名類,而是通過動態(tài)綁定,在運行的時候在調(diào)用,因此避免了在編譯時生成從而影響jvm的加載速度。 Lamda表達式?jīng)]有名稱,但是有參數(shù)列表,函數(shù)體,返回類型并且能夠拋出異常,語法如下形式:
(parameters) -> {statements}(parameters) -> statements(parameters) -> exPRession舉例:
() -> Math.PI * 2.0 (String s) -> s.length() (int i0, int i1) -> i0 + i1 (int x, int y) -> { return x + y; }使用:
//1. 省略類型(i, j) ->{System.out.println(i + j)};//2. 參數(shù)數(shù)量為1時,省略括號//行數(shù)體只有1行時,可以省略大括號i -> arrayList::add//3. 函數(shù)體多行的時候需要用大括號包圍(String idStr) -> {Long id = Long.valueOf(idStr);try { TEliteUser user = eliteAdapter.getUserById(id); } catch (Exception e) { log.error("", e) } userList.add(user); };//4. 函數(shù)體只有一行且有返回值得可以省略return,此時大括號需要一并省略(i, j) -> i - j;//5. 用于Lamda的變量不可改變int portNumber = 1337; Runnable r = () -> System.out.println(portNumber); // OK // 編譯錯誤 // Local variable portNumber defined in an enclosing scope must be final or effectively final int portNumber = 1337; Runnable r = () -> System.out.println(portNumber); // NG portNumber = 1338; // 通過數(shù)組實現(xiàn) final int[] wrappedNumber = new int[] { 1337 }; Runnable r = () -> System.out.println(wrappedNumber[0]); // OK wrappedNumber[0] = 1338;其中所說的Lamda表達式中所引用的必須是不可變的類型,在編譯器實現(xiàn)時是通過隱式的方式將類變量或局部變量進行轉(zhuǎn)換的。也就是以下兩種方法是等效的:
//隱式String separator = ",";Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.print( e + separator ) );//顯式final String separator = ",";Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.print( e + separator ) );還有一點是,在使用Lamda表達式操作集合時,是無法對集合進行元素的刪除的,否則會在產(chǎn)生RunTime Exception:
//Exception in thread "main" java.util.ConcurrentModificationException List<String> names = new ArrayList<String>(){{ add("Zhao"); add("Qian"); add("Sun"); }}; names.forEach(name -> { if (Objects.equals(name, "Sun")); names.remove(name); });當然,這并不是Lamda表達式的問題,使用foreach的話也會遇到同樣的問題,在要對集合進行修改的時候,請使用iterator迭代器進行。
為了使Lamda表達式與原有功能友好兼容,增加了函數(shù)接口:只有一個方法的接口(比如java.lang.Runnable和java.util.concurrent.Callable)。通過函數(shù)接口,接口能夠隱式的轉(zhuǎn)換為Lamda表達式。 為了確保函數(shù)接口中只有一個方法,java8中增加了一個注釋@FunctionalInterface來確保這點。 Java現(xiàn)有接口中均已添加該注釋,比如Runnable函數(shù):
@FunctionalInterfacepublic interface Runnable { public abstract void run();}不過需要注意的是,默認方法和靜態(tài)方法并不會違背函數(shù)接口。比如Java8中引入的Consumer接口的定義:
@FunctionalInterfacepublic interface Consumer<T> { void accept(T t); default Consumer<T> andThen(Consumer<? super T> after) { Objects.requireNonNull(after); return (T t) -> { accept(t); after.accept(t); }; }}關(guān)于接口的默認方法和靜態(tài)方法: 在關(guān)于java的討論中,抽象類和接口之間的相似性和區(qū)別一直是一個很經(jīng)典的問題。而有趣的是,Oracle在java8中向接口中引入了默認方法和靜態(tài)方法,以此來縮小接口和抽象類的區(qū)別。 * 默認方法: 默認方法允許我們在接口里添加新的方法,并不會破壞與實現(xiàn)該接口之前代碼的兼容性。也就是并不要求實現(xiàn)該接口的類實現(xiàn)該方法。使用默認方法只需要在方法前加上default關(guān)鍵字。 * 靜態(tài)方法: Java8同樣在接口中定義了靜態(tài)方法,使用關(guān)鍵字static來進行修飾,默認是public修飾符,所以可以省略,使用方法與在class中定義靜態(tài)方法相同。建立了方法與接口之間的聯(lián)系。 以下的例子同時包含了默認方法和靜態(tài)方法:
private interface Defaulable { // Interfaces now allow default methods, the implementer may or // may not implement (override) them. default String notRequired() { return "Default implementation"; }}private static class DefaultableImpl implements Defaulable {}private static class OverridableImpl implements Defaulable { @Override public String notRequired() { return "Overridden implementation"; }}private interface DefaulableFactory { // Interfaces now allow static methods static Defaulable create( Supplier< Defaulable > supplier ) { return supplier.get(); }}public static void main( String[] args ) { Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new ); System.out.println( defaulable.notRequired() ); defaulable = DefaulableFactory.create( OverridableImpl::new ); System.out.println( defaulable.notRequired() );}為了使Lamda表達式更加簡潔,Java8同樣引入了方法引用。方法引用提供了一個很有用的語義來直接訪問類或?qū)嵗姆椒ā?比如我們定義了如下的類:
public class Car {//Supplier<T>為Java8引入的函數(shù)接口,不接受參數(shù),返回T public static Car create( final Supplier< Car > supplier ) { return supplier.get(); } public static void collide( final Car car ) { System.out.println( "Collided " + car.toString() ); } public void follow( final Car another ) { System.out.println( "Following the " + another.toString() ); } public void repair() { System.out.println( "Repaired " + this.toString() ); }}以下兩種方式是等價的:
//原始final Car car = Car.create(() -> {return new Car();});//方法引用final Car car = Car.create(Car::new);可見,方法引用使得Lamda表達式簡潔很多。方法引用具有四種類型:Class::new, Class::static_method, Class::method以及instance::method。例子如下:
//Class::newList<Car> cars = Arrays.asList(Car.create(Car::new));Car car = Car.create(Car::new);//Class::static_methodcars.forEach(Car::collide);//Class::methodcars.forEach(Car::repair);//instance::methodcars.forEach(car::follow);引入了Stream API(java.util.stream),從而與Lamda共同組成了Java的函數(shù)式編程,目的是簡化,整潔復雜的代碼編寫,從而調(diào)高生產(chǎn)率。 Stream旨在簡化基于集合的操作,專注于對集合對象進行各種便利、高效的聚合操作,或者批量數(shù)據(jù)操作。在原來的對集合操作時,只能通過對集合Iterator或者foreach循環(huán)來進行便利操作,非常笨拙,而通過Stream和函數(shù)編程,能夠極大的簡化該過程。
以下例子展示了stream的工作方式:
List<String> myString = Arrays.asList("a1", "a2", "c", "c2", "c1");myString.stream().filter(s -> s.startsWith("c")).map(String::toUpperCase).sorted().forEach(System.out::println);stream的操作分為兩種,要不是中間操作,要不是終點操作。中間操作返回一個新的Stream。這些中間操作是延遲的,執(zhí)行一個中間操作比如filter實際上不會真的做過濾操作,而是創(chuàng)建一個新的Stream,當這個新的Stream被遍歷的時候,它里頭會包含有原來Stream里符合過濾條件的元素。而終點操作則不返回或者返回一個非stream的結(jié)果。在以上例子中,filter, map, sorted均是中間操作,而forEach則是終點操作。以上的對于stream的操作,我們稱之為操作管道(Operation pipeline)。在stream上所有的操作可以通過查看javadoc來進行查看。在一下的文章中,我們會就其中最重要幾個函數(shù)進行介紹。 需要注意的是,一般stream操作均會和Lamda表達式,函數(shù)接口和方法引用等結(jié)合起來,并且是非引用(non-interfering)的。
stream可以是不同的來源的數(shù)據(jù),不夠大部分的時候我們用它來處理集合的問題。通過stream()和parallelStream()來分別構(gòu)造同步或異步的stream。 我們可以通過如下的形式來構(gòu)建同步的stream,異步parallelStream只是在實現(xiàn)的線程上有所區(qū)別。
//通過集合的stream方法List<String> myString = Arrays.asList("a1", "a2", "c", "c2", "c1");myString.stream().findFirst().ifPresent(System.out::println);//通過Stream.of()方法Stream.of("a1", "a2", "c", "c2","c1").findFirst().ifPresent(System.out::println);需要注意的是,對于不同的原生數(shù)據(jù)類型(Primitive DataType),Stream也有相對應(yīng)的數(shù)據(jù)類型,IntStream,LongStream, DoubleStream等,與Stream一樣,他們都是BaseStream<T,BaseStream<T>>的實現(xiàn)。 原生類型的Stream與普通對象的區(qū)別有一下幾點(以IntStream為例): 1. IntFunction代替Function<T,R>(接受一個T類型參數(shù),返回R類型參數(shù)),IntPredicate代替Predictae<T>(接受一個T類型參數(shù),返回boolean值)… 2. 支持一些附加的終點操作,比如sum()或者average()。
Arrays.stream(new int[] {1, 2, 3}) .map(n -> 2 * n + 1).average().ifPresent(System.out::println);3.普通Stream<T>與原生類型Stream之間的轉(zhuǎn)換通過mapToInt(Function<T, Integer> mapper)和mapToObj(<Interger, T> mapper)轉(zhuǎn)換。
//regular steam to intStreamStream.of("a1", "a2", "a3").map(s -> s.substring(1)).mapToInt(Integer::parseInt) .max().ifPresent(System.out::println);// intStream to regular Stream IntStream.range(1, 4).mapToObj(i -> "a" + i).forEach(System.out::println);//first Stream<Double> to int, then int to regularStream.of(1.0, 2.0, 3.0).mapToInt(Double::intValue).mapToObj(i -> "a" + i).forEach(System.out::println);對于stream來講,操作管道的順序是串行,垂直的(vertically),而不是水平的(horizontally)。為了理解這句話,我們看一下這個例子:
Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> { System.out.println("filter: " + s); return true; }).forEach(s -> System.out.println("forEach: " + s));//結(jié)果是:filter: d2forEach: d2filter: a2forEach: a2filter: b1forEach: b1filter: b3forEach: b3filter: cforEach: c由此可見,操作的順序是一個接著一個元素順序進行的,也就是當”d2”元素全部操作完成后,”a2”才會繼續(xù)進行。 這樣順序的一個好處是,可以減少進行判斷的次數(shù):
Stream.of("d2", "a2", "b1", "b3", "c") .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .anyMatch(s -> { System.out.println("anyMatch: " + s); return s.startsWith("A"); }); //結(jié)果:map: d2anyMatch: D2map: a2anyMatch: A2對于anyMatch來講,當匹配到a2后,就不在進行后續(xù)的操作,從而減少了操作的次數(shù)。 由以上分析我們可以看出操作管道的順序會影響到計算的性能,比如以下這個例子:
//order1Stream.of("d2", "a2", "b1", "b3", "c") .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .filter(s -> { System.out.println("filter: " + s); return s.startsWith("A"); }) .forEach(s -> System.out.println("forEach: " + s));//結(jié)果// map: d2// filter: D2// map: a2// filter: A2// forEach: A2// map: b1// filter: B1// map: b3// filter: B3// map: c// filter: C//order2Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return s.startsWith("a"); }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .forEach(s -> System.out.println("forEach: " + s));//結(jié)果// filter: d2// filter: a2// map: a2// forEach: A2// filter: b1// filter: b3// filter: c可見,當我們更換了map和filter的操作順序后,執(zhí)行的次數(shù)也發(fā)生了變化,所以說順序會影響整個操作管道執(zhí)行的性能。
Stream的終結(jié)是以終點操作來標識結(jié)束的,也就是一旦調(diào)用了終點方法,那么這個stream就會關(guān)閉,不能再次使用。
Stream<String> stream =Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> s.startsWith("a"));stream.anyMatch(s -> true); // okstream.noneMatch(s -> true); // exception解決方法就是每次重新構(gòu)造新的stream操作鏈來進行。比如我們可以構(gòu)造一個stream supplier來每次獲得新的stream:
Supplier<Stream<String>> streamSupplier = () -> Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> s.startsWith("a"));streamSupplier.get().anyMatch(s -> true); // okstreamSupplier.get().noneMatch(s -> true); // ok所有Stream操作管道支持的操作均在javadoc中列出。在上面我們已經(jīng)使用了包括map,filter在內(nèi)的一些操作,在下面我們將會介紹包括collect,reduce這兩個操作。
CollectCollect是一個非常常用的終點操作,能夠?qū)tream轉(zhuǎn)換為各種集合,比如List, Map或者Set。Collect接受Collector為參數(shù),而Collector由四部分組成:Supplier,Accumulator,Combiner,F(xiàn)inisher。雖然Collector很復雜,但是我們可以通過框架類Collector來獲得,在大多數(shù)情況下并不需要我們手動實現(xiàn)。 比如構(gòu)返回一個List, Set, Map只需:
//返回ListList<Person> filtered = persons.stream().filter(p -> p.name.startsWith("P")).collect(Collectors.toList());System.out.println(filtered);//返回SetSet<Person> filtered = persons.stream().filter(p -> p.name.startsWith("P")).collect(Collectors.toSet());System.out.println(filtered);//返回MapMap<Integer, List<Person>> personsByAge = persons.stream().collect(Collectors.groupingBy(p -> p.age));personsByAge.forEach((age, p) -> System.out.format("age %s: %s/n", age, p));//結(jié)果// age 18: [Max]// age 23: [Peter, Pamela]// age 12: [David]Collectors能夠做的功能遠遠不止這些,比如還能夠計算平均值:
Double averageAge = persons.stream().collect(Collectors.averagingInt(p -> p.age));System.out.println(averageAge);計算統(tǒng)計數(shù)據(jù)summaryStatistics:
IntSummaryStatistics ageSummary =persons.stream().collect(Collectors.summarizingInt(p -> p.age));System.out.println(ageSummary);// IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23}連接字符串:
String phrase = persons.stream().filter(p -> p.age >= 18).map(p -> p.name).collect(Collectors.joining(" and ", "In Germany ", " are of legal age."));System.out.println(phrase);// In Germany Max and Peter and Pamela are of legal age.對于映射到Map來講,需要傳遞三個函數(shù)接口:分別是key和value的Function,以及value合并的BinaryOperation:
Map<Integer, String> map = persons.stream().collect(Collectors.toMap( p -> p.age, p -> p.name, (name1, name2) -> name1 + ";" + name2));System.out.println(map);// {18=Max, 23=Peter;Pamela, 12=David}ReduceReduce操作正如名字所言,是將stream中的各個數(shù)據(jù)結(jié)合為一個最終結(jié)果的方法。總共有三種reduce操作:
Optional<T> reduce(BinaryOperator<T> accumulator); T reduce(T identity, BinaryOperator<T> accumulator); <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);我們分別來看一下。 第一種通過accumulator將stream中的所有元素均生成一個最終數(shù)據(jù),如下所示:
persons.stream().reduce((p1, p2) -> p1.age > p2.age ? p1 : p2).ifPresent(System.out::println);以上例子返回的是依據(jù)age得到的結(jié)果,函數(shù)接口使用的是BinaryOperator<Person>,該函數(shù)接口接受兩個相同類型的參數(shù),同時返回該類型的參數(shù)。返回的數(shù)據(jù)是Optional<Person>類型。 第二種接受一個變量identity和同樣的accumulator。identity用于保存累加的結(jié)果。比如我們可以通過以下的方式來獲取一個新的累加的人:
Person result = persons.stream().reduce(new Person("", 0), (p1, p2) -> { p1.age += p2.age; p1.name += p2.name; return p1; });System.out.format("name=%s; age=%s", result.name, result.age);// name=MaxPeterPamelaDavid; age=76第三種接受三個變量,一個identity用于保存累加結(jié)果,一個函數(shù)接口accumulator用于計算累加方式,還有一個函數(shù)接口combiner用于計算兩個accumulator計算得到的值。也就是說,combiner函數(shù)接口主要是用于parallel并行方法的。
Integer ageSum = persons.stream().reduce(0, (sum, p) -> sum += p.age, (sum1, sum2) -> sum1 + sum2);System.out.println(ageSum); // 76為了印證以上的說法,可以如下測試:
Integer ageSum = persons.stream().reduce(0, (sum, p) -> { System.out.format("accumulator: sum=%s; person=%s/n", sum, p); return sum += p.age; }, (sum1, sum2) -> { System.out.format("combiner: sum1=%s; sum2=%s/n", sum1, sum2); return sum1 + sum2; });// accumulator: sum=0; person=Max// accumulator: sum=18; person=Peter// accumulator: sum=41; person=Pamela// accumulator: sum=64; person=David可見,串行的時候并沒有用到combiner函數(shù)接口,而當采用parallelStream()轉(zhuǎn)換為并行時,又有如下的結(jié)果:
Integer ageSum = persons.parallelStream() .reduce(0, (sum, p) -> { System.out.format("accumulator: sum=%s; person=%s/n", sum, p); return sum += p.age; }, (sum1, sum2) -> { System.out.format("combiner: sum1=%s; sum2=%s/n", sum1, sum2); return sum1 + sum2; });// accumulator: sum=0; person=Pamela// accumulator: sum=0; person=David// accumulator: sum=0; person=Max// accumulator: sum=0; person=Peter// combiner: sum1=18; sum2=23// combiner: sum1=23; sum2=12// combiner: sum1=41; sum2=35java8無論是Lamda表達式,函數(shù)接口,方法引用還是Stream類庫的加入,目的都是將函數(shù)編程的便利引入到j(luò)ava中來,而這些特性的加入也是的編程更加的簡潔與便利。而這些內(nèi)容還需要我們在以后的實踐中不斷的去試錯與嘗試,才能夠深入體會到其中真諦。 最后感謝以下資源的貢獻。
Java8特性官方頁1 Java8 features tutorial2 Java8特性,終極手冊3 深入淺出Lamda表達式4 Java8默認方法5 Java8 Stream Tutorial6 Javadoc7
新聞熱點
疑難解答