夯實Java基礎系列21:Java8新特性終極指南

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫喷面,更多精彩內容請到我的倉庫里查看

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點下Star哈

文章首發(fā)于我的個人博客:

www.how2playlife.com

這是一個Java8新增特性的總結圖证九。接下來讓我們一次實踐一下這些新特性吧

image

Java語言新特性

Lambda表達式

Lambda表達式(也稱為閉包)是整個Java 8發(fā)行版中最受期待的在Java語言層面上的改變蹦掐,Lambda允許把函數(shù)作為一個方法的參數(shù)(函數(shù)作為參數(shù)傳遞進方法中)吊输,或者把代碼看成數(shù)據(jù):函數(shù)式程序員對這一概念非常熟悉。在JVM平臺上的很多語言(Groovy,Scala,……)從一開始就有Lambda咐刨,但是Java程序員不得不使用毫無新意的匿名類來代替lambda。

關于Lambda設計的討論占用了大量的時間與社區(qū)的努力扬霜《瘢可喜的是,最終找到了一個平衡點著瓶,使得可以使用一種即簡潔又緊湊的新方式來構造Lambdas联予。在最簡單的形式中,一個lambda可以由用逗號分隔的參數(shù)列表材原、–>符號與函數(shù)體三部分表示沸久。例如:

Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) );

請注意參數(shù)e的類型是由編譯器推測出來的。同時华糖,你也可以通過把參數(shù)類型與參數(shù)包括在括號中的形式直接給出參數(shù)的類型:

Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.println( e ) );

在某些情況下lambda的函數(shù)體會更加復雜麦向,這時可以把函數(shù)體放到在一對花括號中,就像在Java中定義普通函數(shù)一樣客叉。例如:

Arrays.asList( "a", "b", "d" ).forEach( e -> {
    System.out.print( e );
    System.out.print( e );
} );

Lambda可以引用類的成員變量與局部變量(如果這些變量不是final的話诵竭,它們會被隱含的轉為final,這樣效率更高)兼搏。例如卵慰,下面兩個代碼片段是等價的:

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 ) );

Lambda可能會返回一個值。返回值的類型也是由編譯器推測出來的佛呻。如果lambda的函數(shù)體只有一行的話裳朋,那么沒有必要顯式使用return語句。下面兩個代碼片段是等價的:

Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> e1.compareTo( e2 ) );

和:

Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> {
    int result = e1.compareTo( e2 );
    return result;
} );

語言設計者投入了大量精力來思考如何使現(xiàn)有的函數(shù)友好地支持lambda吓著。

最終采取的方法是:增加函數(shù)式接口的概念鲤嫡。函數(shù)式接口就是一個具有一個方法的普通接口。像這樣的接口绑莺,可以被隱式轉換為lambda表達式暖眼。

java.lang.Runnable與java.util.concurrent.Callable是函數(shù)式接口最典型的兩個例子。

在實際使用過程中纺裁,函數(shù)式接口是容易出錯的:如有某個人在接口定義中增加了另一個方法诫肠,這時司澎,這個接口就不再是函數(shù)式的了,并且編譯過程也會失敗栋豫。

為了克服函數(shù)式接口的這種脆弱性并且能夠明確聲明接口作為函數(shù)式接口的意圖挤安,Java8增加了一種特殊的注解@FunctionalInterface(Java8中所有類庫的已有接口都添加了@FunctionalInterface注解)。讓我們看一下這種函數(shù)式接口的定義:

@FunctionalInterface
public interface Functional {
void method();
}
需要記住的一件事是:默認方法與靜態(tài)方法并不影響函數(shù)式接口的契約丧鸯,可以任意使用:

@FunctionalInterface
public interface FunctionalDefaultMethods {
void method();

default void defaultMethod() {            
}        

}
Lambda是Java 8最大的賣點蛤铜。它具有吸引越來越多程序員到Java平臺上的潛力,并且能夠在純Java語言環(huán)境中提供一種優(yōu)雅的方式來支持函數(shù)式編程骡送。更多詳情可以參考官方文檔昂羡。

下面看一個例子:

public class lambda和函數(shù)式編程 {
    @Test
    public void test1() {
        List names = Arrays.asList("peter", "anna", "mike", "xenia");

        Collections.sort(names, new Comparator<String>() {
            @Override
            public int compare(String a, String b) {
                return b.compareTo(a);
            }
        });
        System.out.println(Arrays.toString(names.toArray()));
    }

    @Test
    public void test2() {
        List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");

        Collections.sort(names, (String a, String b) -> {
            return b.compareTo(a);
        });

        Collections.sort(names, (String a, String b) -> b.compareTo(a));

        Collections.sort(names, (a, b) -> b.compareTo(a));
        System.out.println(Arrays.toString(names.toArray()));
    }

}

    static void add(double a,String b) {
        System.out.println(a + b);
    }
    @Test
    public void test5() {
        D d = (a,b) -> add(a,b);
//        interface D {
//            void get(int i,String j);
//        }
        //這里要求,add的兩個參數(shù)和get的兩個參數(shù)吻合并且返回類型也要相等摔踱,否則報錯
//        static void add(double a,String b) {
//            System.out.println(a + b);
//        }
    }

    @FunctionalInterface
    interface D {
        void get(int i,String j);
    }

函數(shù)式接口

所謂的函數(shù)式接口就是只有一個抽象方法的接口虐先,注意這里說的是抽象方法,因為Java8中加入了默認方法的特性派敷,但是函數(shù)式接口是不關心接口中有沒有默認方法的蛹批。 一般函數(shù)式接口可以使用@FunctionalInterface注解的形式來標注表示這是一個函數(shù)式接口,該注解標注與否對函數(shù)式接口沒有實際的影響篮愉, 不過一般還是推薦使用該注解腐芍,就像使用@Override注解一樣。

lambda表達式是如何符合 Java 類型系統(tǒng)的试躏?每個lambda對應于一個給定的類型猪勇,用一個接口來說明。而這個被稱為函數(shù)式接口(functional interface)的接口必須僅僅包含一個抽象方法聲明颠蕴。每個那個類型的lambda表達式都將會被匹配到這個抽象方法上泣刹。因此默認的方法并不是抽象的,你可以給你的函數(shù)式接口自由地增加默認的方法犀被。

我們可以使用任意的接口作為lambda表達式椅您,只要這個接口只包含一個抽象方法。為了保證你的接口滿足需求寡键,你需要增加@FunctionalInterface注解掀泳。編譯器知道這個注解,一旦你試圖給這個接口增加第二個抽象方法聲明時西轩,它將拋出一個編譯器錯誤员舵。

下面舉幾個例子

public class 函數(shù)式接口使用 {
    @FunctionalInterface
    interface A {
        void say();
        default void talk() {

        }
    }
    @Test
    public void test1() {
        A a = () -> System.out.println("hello");
        a.say();
    }

    @FunctionalInterface
    interface B {
        void say(String i);
    }
    public void test2() {
        //下面兩個是等價的,都是通過B接口來引用一個方法藕畔,而方法可以直接使用::來作為方法引用
        B b = System.out::println;
        B b1 = a -> Integer.parseInt("s");//這里的a其實換成別的也行固灵,只是將方法傳給接口作為其方法實現(xiàn)
        B b2 = Integer::valueOf;//i與方法傳入?yún)?shù)的變量類型一直時,可以直接替換
        B b3 = String::valueOf;
        //B b4 = Integer::parseInt;類型不符劫流,無法使用

    }
    @FunctionalInterface
    interface C {
        int say(String i);
    }
    public void test3() {
        C c = Integer::parseInt;//方法參數(shù)和接口方法的參數(shù)一樣,可以替換。
        int i = c.say("1");
        //當我把C接口的int替換為void時就會報錯祠汇,因為返回類型不一致仍秤。
        System.out.println(i);
        //綜上所述,lambda表達式提供了一種簡便的表達方式可很,可以將一個方法傳到接口中诗力。
        //函數(shù)式接口是只提供一個抽象方法的接口,其方法由lambda表達式注入我抠,不需要寫實現(xiàn)類苇本,
        //也不需要寫匿名內部類,可以省去很多代碼菜拓,比如實現(xiàn)runnable接口瓣窄。
        //函數(shù)式編程就是指把方法當做一個參數(shù)或引用來進行操作。除了普通方法以外纳鼎,靜態(tài)方法俺夕,構造方法也是可以這樣操作的。
    }
}

請記住如果@FunctionalInterface 這個注解被遺漏贱鄙,此代碼依然有效劝贸。

方法引用

Lambda表達式和方法引用

有了函數(shù)式接口之后,就可以使用Lambda表達式和方法引用了逗宁。其實函數(shù)式接口的表中的函數(shù)描述符就是Lambda表達式映九,在函數(shù)式接口中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表達式的更簡潔的一種表達形式管挟,使用::操作符轿曙,方法引用主要有三類:

指向靜態(tài)方法的方法引用(例如Integer的parseInt方法,寫作Integer::parseInt)僻孝;

指向任意類型實例方法的方法引用(例如String的length方法导帝,寫作String::length);

指向現(xiàn)有對象的實例方法的方法引用(例如假設你有一個本地變量localVariable用于存放Variable類型的對象穿铆,它支持實例方法getValue您单,那么可以寫成localVariable::getValue)。

舉個方法引用的簡單的例子:

Function<String, Integer> stringToInteger = (String s) -> Integer.parseInt(s);

//使用方法引用

Function<String, Integer> stringToInteger = Integer::parseInt;

方法引用中還有一種特殊的形式荞雏,構造函數(shù)引用虐秦,假設一個類有一個默認的構造函數(shù)平酿,那么使用方法引用的形式為:

Supplier<SomeClass> c1 = SomeClass::new;
SomeClass s1 = c1.get();

//等價于

Supplier<SomeClass> c1 = () -> new SomeClass();
SomeClass s1 = c1.get();

如果是構造函數(shù)有一個參數(shù)的情況:

Function<Integer, SomeClass> c1 = SomeClass::new;
SomeClass s1 = c1.apply(100);

//等價于

Function<Integer, SomeClass> c1 = i -> new SomeClass(i);
SomeClass s1 = c1.apply(100);

接口的默認方法

Java 8 使我們能夠使用default 關鍵字給接口增加非抽象的方法實現(xiàn)。這個特性也被叫做 擴展方法(Extension Methods)悦陋。如下例所示:

public class 接口的默認方法 {
    class B implements A {
//        void a(){}實現(xiàn)類方法不能重名
    }
    interface A {
        //可以有多個默認方法
        public default void a(){
            System.out.println("a");
        }
        public default void b(){
            System.out.println("b");
        }
        //報錯static和default不能同時使用
//        public static default void c(){
//            System.out.println("c");
//        }
    }
    public void test() {
        B b = new B();
        b.a();

    }
}

默認方法出現(xiàn)的原因是為了對原有接口的擴展蜈彼,有了默認方法之后就不怕因改動原有的接口而對已經使用這些接口的程序造成的代碼不兼容的影響。 在Java8中也對一些接口增加了一些默認方法俺驶,比如Map接口等等幸逆。一般來說,使用默認方法的場景有兩個:可選方法和行為的多繼承暮现。

默認方法的使用相對來說比較簡單还绘,唯一要注意的點是如何處理默認方法的沖突。關于如何處理默認方法的沖突可以參考以下三條規(guī)則:

類中的方法優(yōu)先級最高栖袋。類或父類中聲明的方法的優(yōu)先級高于任何聲明為默認方法的優(yōu)先級拍顷。

如果無法依據(jù)第一條規(guī)則進行判斷,那么子接口的優(yōu)先級更高:函數(shù)簽名相同時栋荸,優(yōu)先選擇擁有最具體實現(xiàn)的默認方法的接口菇怀。即如果B繼承了A,那么B就比A更具體晌块。

最后爱沟,如果還是無法判斷,繼承了多個接口的類必須通過顯式覆蓋和調用期望的方法匆背,顯式地選擇使用哪一個默認方法的實現(xiàn)呼伸。那么如何顯式地指定呢:

public class C implements B, A {
 
    public void hello() {
        B.super().hello();    
    }
 
}

使用X.super.m(..)顯式地調用希望調用的方法。

Java 8用默認方法與靜態(tài)方法這兩個新概念來擴展接口的聲明钝尸。默認方法使接口有點像Traits(Scala中特征(trait)類似于Java中的Interface括享,但它可以包含實現(xiàn)代碼,也就是目前Java8新增的功能)珍促,但與傳統(tǒng)的接口又有些不一樣铃辖,它允許在已有的接口中添加新方法,而同時又保持了與舊版本代碼的兼容性猪叙。

默認方法與抽象方法不同之處在于抽象方法必須要求實現(xiàn)娇斩,但是默認方法則沒有這個要求。相反穴翩,每個接口都必須提供一個所謂的默認實現(xiàn)犬第,這樣所有的接口實現(xiàn)者將會默認繼承它(如果有必要的話,可以覆蓋這個默認實現(xiàn))芒帕。讓我們看看下面的例子:

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";
    }
}

Defaulable接口用關鍵字default聲明了一個默認方法notRequired()歉嗓,Defaulable接口的實現(xiàn)者之一DefaultableImpl實現(xiàn)了這個接口,并且讓默認方法保持原樣背蟆。Defaulable接口的另一個實現(xiàn)者OverridableImpl用自己的方法覆蓋了默認方法鉴分。

Java 8帶來的另一個有趣的特性是接口可以聲明(并且可以提供實現(xiàn))靜態(tài)方法哮幢。例如:

private interface DefaulableFactory {
    // Interfaces now allow static methods
    static Defaulable create( Supplier< Defaulable > supplier ) {
        return supplier.get();
    }
}

下面的一小段代碼片段把上面的默認方法與靜態(tài)方法黏合到一起。

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() );
}

這個程序的控制臺輸出如下:

Default implementation
Overridden implementation
在JVM中冠场,默認方法的實現(xiàn)是非常高效的家浇,并且通過字節(jié)碼指令為方法調用提供了支持。默認方法允許繼續(xù)使用現(xiàn)有的Java接口碴裙,而同時能夠保障正常的編譯過程。這方面好的例子是大量的方法被添加到java.util.Collection接口中去:stream()点额,parallelStream()舔株,forEach(),removeIf()还棱,……

盡管默認方法非常強大载慈,但是在使用默認方法時我們需要小心注意一個地方:在聲明一個默認方法前,請仔細思考是不是真的有必要使用默認方法珍手,因為默認方法會帶給程序歧義办铡,并且在復雜的繼承體系中容易產生編譯錯誤。更多詳情請參考官方文檔

重復注解

自從Java 5引入了注解機制琳要,這一特性就變得非常流行并且廣為使用寡具。然而,使用注解的一個限制是相同的注解在同一位置只能聲明一次稚补,不能聲明多次童叠。Java 8打破了這條規(guī)則,引入了重復注解機制课幕,這樣相同的注解可以在同一地方聲明多次厦坛。

重復注解機制本身必須用@Repeatable注解。事實上乍惊,這并不是語言層面上的改變杜秸,更多的是編譯器的技巧,底層的原理保持不變润绎。讓我們看一個快速入門的例子:

package com.javacodegeeks.java8.repeatable.annotations;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
public class RepeatingAnnotations {
    @Target( ElementType.TYPE )
    @Retention( RetentionPolicy.RUNTIME )
    public @interface Filters {
        Filter[] value();
    }
     
    @Target( ElementType.TYPE )
    @Retention( RetentionPolicy.RUNTIME )
    @Repeatable( Filters.class )
    public @interface Filter {
        String value();
    };
     
    @Filter( "filter1" )
    @Filter( "filter2" )
    public interface Filterable {        
    }
     
    public static void main(String[] args) {
        for( Filter filter: Filterable.class.getAnnotationsByType( Filter.class ) ) {
            System.out.println( filter.value() );
        }
    }
}

正如我們看到的撬碟,這里有個使用@Repeatable( Filters.class )注解的注解類Filter,F(xiàn)ilters僅僅是Filter注解的數(shù)組凡橱,但Java編譯器并不想讓程序員意識到Filters的存在小作。這樣,接口Filterable就擁有了兩次Filter(并沒有提到Filter)注解稼钩。

同時顾稀,反射相關的API提供了新的函數(shù)getAnnotationsByType()來返回重復注解的類型(請注意Filterable.class.getAnnotation( Filters.class )經編譯器處理后將會返回Filters的實例)。

程序輸出結果如下:

filter1
filter2
更多詳情請參考官方文檔

Java編譯器的新特性

方法參數(shù)名字可以反射獲取

很長一段時間里坝撑,Java程序員一直在發(fā)明不同的方式使得方法參數(shù)的名字能保留在Java字節(jié)碼中静秆,并且能夠在運行時獲取它們(比如粮揉,Paranamer類庫)。最終抚笔,在Java 8中把這個強烈要求的功能添加到語言層面(通過反射API與Parameter.getName()方法)與字節(jié)碼文件(通過新版的javac的–parameters選項)中扶认。

package com.javacodegeeks.java8.parameter.names;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class ParameterNames {
public static void main(String[] args) throws Exception {
Method method = ParameterNames.class.getMethod( "main", String[].class );
for( final Parameter parameter: method.getParameters() ) {
System.out.println( "Parameter: " + parameter.getName() );
}
}
}
如果不使用–parameters參數(shù)來編譯這個類,然后運行這個類殊橙,會得到下面的輸出:

Parameter: arg0
如果使用–parameters參數(shù)來編譯這個類辐宾,程序的結構會有所不同(參數(shù)的真實名字將會顯示出來):

Parameter: args

Java 類庫的新特性

Java 8 通過增加大量新類,擴展已有類的功能的方式來改善對并發(fā)編程膨蛮、函數(shù)式編程叠纹、日期/時間相關操作以及其他更多方面的支持。

Optional

到目前為止敞葛,臭名昭著的空指針異常是導致Java應用程序失敗的最常見原因袜瞬。以前了赌,為了解決空指針異常,Google公司著名的Guava項目引入了Optional類,Guava通過使用檢查空值的方式來防止代碼污染景鼠,它鼓勵程序員寫更干凈的代碼民假。受到Google Guava的啟發(fā)开泽,Optional類已經成為Java 8類庫的一部分廓鞠。

Optional實際上是個容器:它可以保存類型T的值,或者僅僅保存null儒飒。Optional提供很多有用的方法谬莹,這樣我們就不用顯式進行空值檢測。更多詳情請參考官方文檔桩了。

我們下面用兩個小例子來演示如何使用Optional類:一個允許為空值附帽,一個不允許為空值。

public class 空指針Optional {
    public static void main(String[] args) {

        //使用of方法井誉,仍然會報空指針異常
//        Optional optional = Optional.of(null);
//        System.out.println(optional.get());

        //拋出沒有該元素的異常
        //Exception in thread "main" java.util.NoSuchElementException: No value present
//        at java.util.Optional.get(Optional.java:135)
//        at com.javase.Java8.空指針Optional.main(空指針Optional.java:14)
//        Optional optional1 = Optional.ofNullable(null);
//        System.out.println(optional1.get());
        Optional optional = Optional.ofNullable(null);
        System.out.println(optional.isPresent());
        System.out.println(optional.orElse(0));//當值為空時給與初始值
        System.out.println(optional.orElseGet(() -> new String[]{"a"}));//使用回調函數(shù)設置默認值
        //即使傳入Optional容器的元素為空蕉扮,使用optional.isPresent()方法也不會報空指針異常
        //所以通過optional.orElse這種方式就可以寫出避免空指針異常的代碼了
        //輸出Optional.empty。
    }
}

如果Optional類的實例為非空值的話颗圣,isPresent()返回true喳钟,否從返回false。為了防止Optional為空值在岂,orElseGet()方法通過回調函數(shù)來產生一個默認值奔则。map()函數(shù)對當前Optional的值進行轉化,然后返回一個新的Optional實例蔽午。orElse()方法和orElseGet()方法類似易茬,但是orElse接受一個默認值而不是一個回調函數(shù)。下面是這個程序的輸出:

Full Name is set? false
Full Name: [none]
Hey Stranger!
讓我們來看看另一個例子:

Optional< String > firstName = Optional.of( "Tom" );
System.out.println( "First Name is set? " + firstName.isPresent() );        
System.out.println( "First Name: " + firstName.orElseGet( () -> "[none]" ) ); 
System.out.println( firstName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
System.out.println();

下面是程序的輸出:

First Name is set? true
First Name: Tom
Hey Tom!

Stream

最新添加的Stream API(java.util.stream) 把真正的函數(shù)式編程風格引入到Java中。這是目前為止對Java類庫最好的補充抽莱,因為Stream API可以極大提供Java程序員的生產力范抓,讓程序員寫出高效率、干凈食铐、簡潔的代碼匕垫。

Stream API極大簡化了集合框架的處理(但它的處理的范圍不僅僅限于集合框架的處理,這點后面我們會看到)虐呻。讓我們以一個簡單的Task類為例進行介紹:

Task類有一個分數(shù)的概念(或者說是偽復雜度)象泵,其次是還有一個值可以為OPEN或CLOSED的狀態(tài).讓我們引入一個Task的小集合作為演示例子:

final Collection< Task > tasks = Arrays.asList(
    new Task( Status.OPEN, 5 ),
    new Task( Status.OPEN, 13 ),
    new Task( Status.CLOSED, 8 ) 
);

我們下面要討論的第一個問題是所有狀態(tài)為OPEN的任務一共有多少分數(shù)?在Java 8以前铃慷,一般的解決方式用foreach循環(huán)单芜,但是在Java 8里面我們可以使用stream:一串支持連續(xù)、并行聚集操作的元素犁柜。

// Calculate total points of all active tasks using sum()
final long totalPointsOfOpenTasks = tasks
    .stream()
    .filter( task -> task.getStatus() == Status.OPEN )
    .mapToInt( Task::getPoints )
    .sum();
         
System.out.println( "Total points: " + totalPointsOfOpenTasks );

程序在控制臺上的輸出如下:

Total points: 18

這里有幾個注意事項。

第一堂淡,task集合被轉換化為其相應的stream表示馋缅。然后,filter操作過濾掉狀態(tài)為CLOSED的task绢淀。

下一步萤悴,mapToInt操作通過Task::getPoints這種方式調用每個task實例的getPoints方法把Task的stream轉化為Integer的stream。最后皆的,用sum函數(shù)把所有的分數(shù)加起來覆履,得到最終的結果。

在繼續(xù)講解下面的例子之前费薄,關于stream有一些需要注意的地方(詳情在這里).stream操作被分成了中間操作與最終操作這兩種硝全。

中間操作返回一個新的stream對象。中間操作總是采用惰性求值方式楞抡,運行一個像filter這樣的中間操作實際上沒有進行任何過濾伟众,相反它在遍歷元素時會產生了一個新的stream對象,這個新的stream對象包含原始stream
中符合給定謂詞的所有元素召廷。

像forEach凳厢、sum這樣的最終操作可能直接遍歷stream,產生一個結果或副作用竞慢。當最終操作執(zhí)行結束之后先紫,stream管道被認為已經被消耗了,沒有可能再被使用了筹煮。在大多數(shù)情況下遮精,最終操作都是采用及早求值方式,及早完成底層數(shù)據(jù)源的遍歷寺谤。

stream另一個有價值的地方是能夠原生支持并行處理仑鸥。讓我們來看看這個算task分數(shù)和的例子吮播。

stream另一個有價值的地方是能夠原生支持并行處理。讓我們來看看這個算task分數(shù)和的例子眼俊。

// Calculate total points of all tasks
final double totalPoints = tasks
   .stream()
   .parallel()
   .map( task -> task.getPoints() ) // or map( Task::getPoints ) 
   .reduce( 0, Integer::sum );
     
System.out.println( "Total points (all tasks): " + totalPoints );

這個例子和第一個例子很相似意狠,但這個例子的不同之處在于這個程序是并行運行的,其次使用reduce方法來算最終的結果疮胖。
下面是這個例子在控制臺的輸出:

Total points (all tasks): 26.0
經常會有這個一個需求:我們需要按照某種準則來對集合中的元素進行分組环戈。Stream也可以處理這樣的需求,下面是一個例子:

// Group tasks by their status
final Map< Status, List< Task > > map = tasks
    .stream()
    .collect( Collectors.groupingBy( Task::getStatus ) );
System.out.println( map );

這個例子的控制臺輸出如下:

{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
讓我們來計算整個集合中每個task分數(shù)(或權重)的平均值來結束task的例子澎灸。

// Calculate the weight of each tasks (as percent of total points) 
final Collection< String > result = tasks
    .stream()                                        // Stream< String >
    .mapToInt( Task::getPoints )                     // IntStream
    .asLongStream()                                  // LongStream
    .mapToDouble( points -> points / totalPoints )   // DoubleStream
    .boxed()                                         // Stream< Double >
    .mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream
    .mapToObj( percentage -> percentage + "%" )      // Stream< String> 
    .collect( Collectors.toList() );                 // List< String > 
         
System.out.println( result );

下面是這個例子的控制臺輸出:

[19%, 50%, 30%]
最后院塞,就像前面提到的,Stream API不僅僅處理Java集合框架性昭。像從文本文件中逐行讀取數(shù)據(jù)這樣典型的I/O操作也很適合用Stream API來處理拦止。下面用一個例子來應證這一點。

final Path path = new File( filename ).toPath();
try( Stream< String > lines = Files.lines( path, StandardCharsets.UTF_8 ) ) {
    lines.onClose( () -> System.out.println("Done!") ).forEach( System.out::println );
}

對一個stream對象調用onClose方法會返回一個在原有功能基礎上新增了關閉功能的stream對象糜颠,當對stream對象調用close()方法時汹族,與關閉相關的處理器就會執(zhí)行。

Stream API其兴、Lambda表達式與方法引用在接口默認方法與靜態(tài)方法的配合下是Java 8對現(xiàn)代軟件開發(fā)范式的回應顶瞒。更多詳情請參考官方文檔。

Date/Time API (JSR 310)

Java 8通過發(fā)布新的Date-Time API (JSR 310)來進一步加強對日期與時間的處理元旬。對日期與時間的操作一直是Java程序員最痛苦的地方之一榴徐。標準的 java.util.Date以及后來的java.util.Calendar一點沒有改善這種情況(可以這么說,它們一定程度上更加復雜)匀归。

這種情況直接導致了Joda-Time——一個可替換標準日期/時間處理且功能非常強大的Java API的誕生坑资。Java 8新的Date-Time API (JSR 310)在很大程度上受到Joda-Time的影響,并且吸取了其精髓朋譬。新的java.time包涵蓋了所有處理日期盐茎,時間,日期/時間徙赢,時區(qū)字柠,時刻(instants),過程(during)與時鐘(clock)的操作狡赐。在設計新版API時窑业,十分注重與舊版API的兼容性:不允許有任何的改變(從java.util.Calendar中得到的深刻教訓)。如果需要修改枕屉,會返回這個類的一個新實例常柄。

讓我們用例子來看一下新版API主要類的使用方法。第一個是Clock類,它通過指定一個時區(qū)西潘,然后就可以獲取到當前的時刻卷玉,日期與時間。Clock可以替換System.currentTimeMillis()與TimeZone.getDefault()喷市。

// Get the system clock as UTC offset 
final Clock clock = Clock.systemUTC();
System.out.println( clock.instant() );
System.out.println( clock.millis() );

下面是程序在控制臺上的輸出:

2014-04-12T15:19:29.282Z
1397315969360

我們需要關注的其他類是LocaleDate與LocalTime相种。LocaleDate只持有ISO-8601格式且無時區(qū)信息的日期部分。相應的品姓,LocaleTime只持有ISO-8601格式且無時區(qū)信息的時間部分寝并。LocaleDate與LocalTime都可以從Clock中得到。

// Get the local date and local time
final LocalDate date = LocalDate.now();
final LocalDate dateFromClock = LocalDate.now( clock );
         
System.out.println( date );
System.out.println( dateFromClock );
         
// Get the local date and local time
final LocalTime time = LocalTime.now();
final LocalTime timeFromClock = LocalTime.now( clock );
     
System.out.println( time );
System.out.println( timeFromClock );

下面是程序在控制臺上的輸出:

2014-04-12
2014-04-12
11:25:54.568
15:25:54.568

下面是程序在控制臺上的輸出:

2014-04-12T11:47:01.017-04:00[America/New_York]
2014-04-12T15:47:01.017Z
2014-04-12T08:47:01.017-07:00[America/Los_Angeles]

最后腹备,讓我們看一下Duration類:在秒與納秒級別上的一段時間衬潦。Duration使計算兩個日期間的不同變的十分簡單。下面讓我們看一個這方面的例子植酥。

// Get duration between two dates
final LocalDateTime from = LocalDateTime.of( 2014, Month.APRIL, 16, 0, 0, 0 );
final LocalDateTime to = LocalDateTime.of( 2015, Month.APRIL, 16, 23, 59, 59 );

final Duration duration = Duration.between( from, to );
System.out.println( "Duration in days: " + duration.toDays() );
System.out.println( "Duration in hours: " + duration.toHours() );

上面的例子計算了兩個日期2014年4月16號與2014年4月16號之間的過程镀岛。下面是程序在控制臺上的輸出:

Duration in days: 365
Duration in hours: 8783
對Java 8在日期/時間API的改進整體印象是非常非常好的。一部分原因是因為它建立在“久戰(zhàn)殺場”的Joda-Time基礎上友驮,另一方面是因為用來大量的時間來設計它哎媚,并且這次程序員的聲音得到了認可。更多詳情請參考官方文檔喊儡。

并行(parallel)數(shù)組

Java 8增加了大量的新方法來對數(shù)組進行并行處理〉揪荩可以說艾猜,最重要的是parallelSort()方法,因為它可以在多核機器上極大提高數(shù)組排序的速度捻悯。下面的例子展示了新方法(parallelXxx)的使用匆赃。

package com.javacodegeeks.java8.parallel.arrays;
 
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
 
public class ParallelArrays {
    public static void main( String[] args ) {
        long[] arrayOfLong = new long [ 20000 ];        
         
        Arrays.parallelSetAll( arrayOfLong, 
            index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
        Arrays.stream( arrayOfLong ).limit( 10 ).forEach( 
            i -> System.out.print( i + " " ) );
        System.out.println();
         
        Arrays.parallelSort( arrayOfLong );     
        Arrays.stream( arrayOfLong ).limit( 10 ).forEach( 
            i -> System.out.print( i + " " ) );
        System.out.println();
    }
}

上面的代碼片段使用了parallelSetAll()方法來對一個有20000個元素的數(shù)組進行隨機賦值。然后今缚,調用parallelSort方法算柳。這個程序首先打印出前10個元素的值,之后對整個數(shù)組排序姓言。這個程序在控制臺上的輸出如下(請注意數(shù)組元素是隨機生產的):

Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378
Sorted: 39 220 263 268 325 607 655 678 723 793

CompletableFuture

在Java8之前瞬项,我們會使用JDK提供的Future接口來進行一些異步的操作,其實CompletableFuture也是實現(xiàn)了Future接口何荚, 并且基于ForkJoinPool來執(zhí)行任務囱淋,因此本質上來講,CompletableFuture只是對原有API的封裝餐塘, 而使用CompletableFuture與原來的Future的不同之處在于可以將兩個Future組合起來妥衣,或者如果兩個Future是有依賴關系的,可以等第一個執(zhí)行完畢后再實行第二個等特性。

先來看看基本的使用方式:

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,并且使用函數(shù)式編程的方式來使用這些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方法第二個參數(shù)接受一個自定義的Executor。 首先使用CompletableFuture執(zhí)行一個任務俱箱,調用getPrice方法国瓮,得到一個Future,之后使用thenApply方法狞谱,將Future的結果應用parse方法乃摹, 之后再使用執(zhí)行完parse之后的結果作為參數(shù)再執(zhí)行一個applyCount方法,然后收集成一個CompletableFuture<String>的List跟衅, 最后再使用一個流孵睬,調用CompletableFuture的join方法,這是為了等待所有的異步任務執(zhí)行完畢伶跷,獲得最后的結果掰读。

注意,這里必須使用兩個流叭莫,如果在一個流里調用join方法蹈集,那么由于Stream的延遲特性,所有的操作還是會串行的執(zhí)行雇初,并不是異步的拢肆。

再來看一個兩個Future之間沒有依賴關系的例子:

Future<String> futurePriceInUsd = CompletableFuture.supplyAsync(() -> shop.getPrice(“price1”))
                                    .thenCombine(CompletableFuture.supplyAsync(() -> shop.getPrice(“price2”)), (s1, s2) -> s1 + s2);

這里有兩個異步的任務,使用thenCombine方法來組合兩個Future靖诗,thenCombine方法的第二個參數(shù)就是用來合并兩個Future方法返回值的操作函數(shù)郭怪。

有時候,我們并不需要等待所有的異步任務結束呻畸,只需要其中的一個完成就可以了移盆,CompletableFuture也提供了這樣的方法:

//假設getStream方法返回一個Stream<CompletableFuture<String>>
CompletableFuture[] futures = getStream(“l(fā)isten”).map(f -> f.thenAccept(System.out::println)).toArray(CompletableFuture[]::new);
//等待其中的一個執(zhí)行完畢
CompletableFuture.anyOf(futures).join();
使用anyOf方法來響應CompletableFuture的completion事件。

Java虛擬機(JVM)的新特性

PermGen空間被移除了伤为,取而代之的是Metaspace(JEP 122)咒循。JVM選項-XX:PermSize與-XX:MaxPermSize分別被-XX:MetaSpaceSize與-XX:MaxMetaspaceSize所代替据途。

總結

更多展望:Java 8通過發(fā)布一些可以增加程序員生產力的特性來推進這個偉大的平臺的進步。現(xiàn)在把生產環(huán)境遷移到Java 8還為時尚早叙甸,但是在接下來的幾個月里颖医,它會被大眾慢慢的接受。毫無疑問裆蒸,現(xiàn)在是時候讓你的代碼與Java 8兼容熔萧,并且在Java 8足夠安全穩(wěn)定的時候遷移到Java 8。

參考文章

https://blog.csdn.net/shuaicihai/article/details/72615495

https://blog.csdn.net/qq_34908167/article/details/79286697

http://www.reibang.com/p/4df02599aeb2

https://www.cnblogs.com/yangzhilong/p/10973006.html

https://www.cnblogs.com/JackpotHan/p/9701147.html

微信公眾號

Java技術江湖

如果大家想要實時關注我更新的文章以及分享的干貨的話僚祷,可以關注我的公眾號【Java技術江湖】一位阿里 Java 工程師的技術小站佛致,作者黃小斜,專注 Java 相關技術:SSM辙谜、SpringBoot俺榆、MySQL、分布式装哆、中間件罐脊、集群、Linux蜕琴、網絡萍桌、多線程,偶爾講點Docker凌简、ELK上炎,同時也分享技術干貨和學習經驗,致力于Java全棧開發(fā)雏搂!

Java工程師必備學習資源: 一些Java工程師常用學習資源反症,關注公眾號后,后臺回復關鍵字 “Java” 即可免費無套路獲取畔派。

我的公眾號

個人公眾號:黃小斜

作者是跨考軟件工程的 985 碩士,自學 Java 兩年润绵,拿到了 BAT 等近十家大廠 offer线椰,從技術小白成長為阿里工程師。

作者專注于 JAVA 后端技術棧尘盼,熱衷于分享程序員干貨憨愉、學習經驗、求職心得和程序人生卿捎,目前黃小斜的CSDN博客有百萬+訪問量配紫,知乎粉絲2W+,全網已有10W+讀者午阵。

黃小斜是一個斜杠青年躺孝,堅持學習和寫作享扔,相信終身學習的力量,希望和更多的程序員交朋友植袍,一起進步和成長惧眠!

關注公眾號【黃小斜】后回復【原創(chuàng)電子書】即可領取我原創(chuàng)的電子書《菜鳥程序員修煉手冊:從技術小白到阿里巴巴Java工程師》

程序員3T技術學習資源: 一些程序員學習技術的資源大禮包,關注公眾號后于个,后臺回復關鍵字 “資料” 即可免費無套路獲取氛魁。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市厅篓,隨后出現(xiàn)的幾起案子秀存,更是在濱河造成了極大的恐慌,老刑警劉巖羽氮,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件或链,死亡現(xiàn)場離奇詭異,居然都是意外死亡乏苦,警方通過查閱死者的電腦和手機株扛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來汇荐,“玉大人洞就,你說我怎么就攤上這事∠铺裕” “怎么了旬蟋?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長革娄。 經常有香客問我倾贰,道長,這世上最難降的妖魔是什么拦惋? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任匆浙,我火速辦了婚禮,結果婚禮上厕妖,老公的妹妹穿的比我還像新娘首尼。我一直安慰自己,他們只是感情好言秸,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布软能。 她就那樣靜靜地躺著,像睡著了一般举畸。 火紅的嫁衣襯著肌膚如雪查排。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天抄沮,我揣著相機與錄音跋核,去河邊找鬼岖瑰。 笑死,一個胖子當著我的面吹牛了罪,可吹牛的內容都是我干的锭环。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼泊藕,長吁一口氣:“原來是場噩夢啊……” “哼辅辩!你這毒婦竟也來了?” 一聲冷哼從身側響起娃圆,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤玫锋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后讼呢,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體撩鹿,經...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年悦屏,在試婚紗的時候發(fā)現(xiàn)自己被綠了节沦。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡础爬,死狀恐怖甫贯,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情看蚜,我是刑警寧澤叫搁,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站供炎,受9級特大地震影響渴逻,放射性物質發(fā)生泄漏。R本人自食惡果不足惜音诫,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一惨奕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧竭钝,春花似錦墓贿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽队伟。三九已至穴吹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間嗜侮,已是汗流浹背港令。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工啥容, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人顷霹。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓咪惠,卻偏偏與公主長得像,于是被迫代替她去往敵國和親淋淀。 傳聞我的和親對象是個殘疾皇子遥昧,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內容