溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

夯實(shí)Java基礎(chǔ)系列21:Java8新特性終極指南

發(fā)布時(shí)間:2020-07-14 19:45:06 來(lái)源:網(wǎng)絡(luò) 閱讀:129 作者:黃小斜 欄目:編程語(yǔ)言

本系列文章將整理到我在GitHub上的《Java面試指南》倉(cāng)庫(kù),更多精彩內(nèi)容請(qǐng)到我的倉(cāng)庫(kù)里查看

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

喜歡的話麻煩點(diǎn)下Star哈

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

www.how2playlife.com

<!-- more -->

這是一個(gè)Java8新增特性的總結(jié)圖。接下來(lái)讓我們一次實(shí)踐一下這些新特性吧

夯實(shí)Java基礎(chǔ)系列21:Java8新特性終極指南

Java語(yǔ)言新特性

Lambda表達(dá)式

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

關(guān)于Lambda設(shè)計(jì)的討論占用了大量的時(shí)間與社區(qū)的努力??上驳氖?,最終找到了一個(gè)平衡點(diǎn),使得可以使用一種即簡(jiǎn)潔又緊湊的新方式來(lái)構(gòu)造Lambdas。在最簡(jiǎn)單的形式中,一個(gè)lambda可以由用逗號(hào)分隔的參數(shù)列表、–>符號(hào)與函數(shù)體三部分表示。例如:

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

請(qǐng)注意參數(shù)e的類型是由編譯器推測(cè)出來(lái)的。同時(shí),你也可以通過(guò)把參數(shù)類型與參數(shù)包括在括號(hào)中的形式直接給出參數(shù)的類型:

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

在某些情況下lambda的函數(shù)體會(huì)更加復(fù)雜,這時(shí)可以把函數(shù)體放到在一對(duì)花括號(hào)中,就像在Java中定義普通函數(shù)一樣。例如:

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

Lambda可以引用類的成員變量與局部變量(如果這些變量不是final的話,它們會(huì)被隱含的轉(zhuǎn)為final,這樣效率更高)。例如,下面兩個(gè)代碼片段是等價(jià)的:

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可能會(huì)返回一個(gè)值。返回值的類型也是由編譯器推測(cè)出來(lái)的。如果lambda的函數(shù)體只有一行的話,那么沒(méi)有必要顯式使用return語(yǔ)句。下面兩個(gè)代碼片段是等價(jià)的:

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

語(yǔ)言設(shè)計(jì)者投入了大量精力來(lái)思考如何使現(xiàn)有的函數(shù)友好地支持lambda。

最終采取的方法是:增加函數(shù)式接口的概念。函數(shù)式接口就是一個(gè)具有一個(gè)方法的普通接口。像這樣的接口,可以被隱式轉(zhuǎn)換為lambda表達(dá)式。

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

在實(shí)際使用過(guò)程中,函數(shù)式接口是容易出錯(cuò)的:如有某個(gè)人在接口定義中增加了另一個(gè)方法,這時(shí),這個(gè)接口就不再是函數(shù)式的了,并且編譯過(guò)程也會(huì)失敗。

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

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

@FunctionalInterface
public interface FunctionalDefaultMethods {
void method();

default void defaultMethod() {            
}        

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

下面看一個(gè)例子:

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的兩個(gè)參數(shù)和get的兩個(gè)參數(shù)吻合并且返回類型也要相等,否則報(bào)錯(cuò)
//        static void add(double a,String b) {
//            System.out.println(a + b);
//        }
    }

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

函數(shù)式接口

所謂的函數(shù)式接口就是只有一個(gè)抽象方法的接口,注意這里說(shuō)的是抽象方法,因?yàn)镴ava8中加入了默認(rèn)方法的特性,但是函數(shù)式接口是不關(guān)心接口中有沒(méi)有默認(rèn)方法的。 一般函數(shù)式接口可以使用@FunctionalInterface注解的形式來(lái)標(biāo)注表示這是一個(gè)函數(shù)式接口,該注解標(biāo)注與否對(duì)函數(shù)式接口沒(méi)有實(shí)際的影響, 不過(guò)一般還是推薦使用該注解,就像使用@Override注解一樣。

lambda表達(dá)式是如何符合 Java 類型系統(tǒng)的?每個(gè)lambda對(duì)應(yīng)于一個(gè)給定的類型,用一個(gè)接口來(lái)說(shuō)明。而這個(gè)被稱為函數(shù)式接口(functional interface)的接口必須僅僅包含一個(gè)抽象方法聲明。每個(gè)那個(gè)類型的lambda表達(dá)式都將會(huì)被匹配到這個(gè)抽象方法上。因此默認(rèn)的方法并不是抽象的,你可以給你的函數(shù)式接口自由地增加默認(rèn)的方法。

我們可以使用任意的接口作為lambda表達(dá)式,只要這個(gè)接口只包含一個(gè)抽象方法。為了保證你的接口滿足需求,你需要增加@FunctionalInterface注解。編譯器知道這個(gè)注解,一旦你試圖給這個(gè)接口增加第二個(gè)抽象方法聲明時(shí),它將拋出一個(gè)編譯器錯(cuò)誤。

下面舉幾個(gè)例子

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() {
        //下面兩個(gè)是等價(jià)的,都是通過(guò)B接口來(lái)引用一個(gè)方法,而方法可以直接使用::來(lái)作為方法引用
        B b = System.out::println;
        B b1 = a -> Integer.parseInt("s");//這里的a其實(shí)換成別的也行,只是將方法傳給接口作為其方法實(shí)現(xiàn)
        B b2 = Integer::valueOf;//i與方法傳入?yún)?shù)的變量類型一直時(shí),可以直接替換
        B b3 = String::valueOf;
        //B b4 = Integer::parseInt;類型不符,無(wú)法使用

    }
    @FunctionalInterface
    interface C {
        int say(String i);
    }
    public void test3() {
        C c = Integer::parseInt;//方法參數(shù)和接口方法的參數(shù)一樣,可以替換。
        int i = c.say("1");
        //當(dāng)我把C接口的int替換為void時(shí)就會(huì)報(bào)錯(cuò),因?yàn)榉祷仡愋筒灰恢隆?        System.out.println(i);
        //綜上所述,lambda表達(dá)式提供了一種簡(jiǎn)便的表達(dá)方式,可以將一個(gè)方法傳到接口中。
        //函數(shù)式接口是只提供一個(gè)抽象方法的接口,其方法由lambda表達(dá)式注入,不需要寫(xiě)實(shí)現(xiàn)類,
        //也不需要寫(xiě)匿名內(nèi)部類,可以省去很多代碼,比如實(shí)現(xiàn)runnable接口。
        //函數(shù)式編程就是指把方法當(dāng)做一個(gè)參數(shù)或引用來(lái)進(jìn)行操作。除了普通方法以外,靜態(tài)方法,構(gòu)造方法也是可以這樣操作的。
    }
}

請(qǐng)記住如果@FunctionalInterface 這個(gè)注解被遺漏,此代碼依然有效。

方法引用

Lambda表達(dá)式和方法引用

有了函數(shù)式接口之后,就可以使用Lambda表達(dá)式和方法引用了。其實(shí)函數(shù)式接口的表中的函數(shù)描述符就是Lambda表達(dá)式,在函數(shù)式接口中Lambda表達(dá)式相當(dāng)于匿名內(nèi)部類的效果。 舉個(gè)簡(jiǎn)單的例子:

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表達(dá)式
    execute(() -> System.out.println("run"));
}

}

可以看到,相比于使用匿名內(nèi)部類的方式,Lambda表達(dá)式可以使用更少的代碼但是有更清晰的表述。注意,Lambda表達(dá)式也不是完全等價(jià)于匿名內(nèi)部類的, 兩者的不同點(diǎn)在于this的指向和本地變量的屏蔽上。

方法引用可以看作Lambda表達(dá)式的更簡(jiǎn)潔的一種表達(dá)形式,使用::操作符,方法引用主要有三類:

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

指向任意類型實(shí)例方法的方法引用(例如String的length方法,寫(xiě)作String::length);

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

舉個(gè)方法引用的簡(jiǎn)單的例子:

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

//使用方法引用

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

方法引用中還有一種特殊的形式,構(gòu)造函數(shù)引用,假設(shè)一個(gè)類有一個(gè)默認(rèn)的構(gòu)造函數(shù),那么使用方法引用的形式為:

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

//等價(jià)于

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

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

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

//等價(jià)于

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

接口的默認(rèn)方法

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

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

    }
}

默認(rèn)方法出現(xiàn)的原因是為了對(duì)原有接口的擴(kuò)展,有了默認(rèn)方法之后就不怕因改動(dòng)原有的接口而對(duì)已經(jīng)使用這些接口的程序造成的代碼不兼容的影響。 在Java8中也對(duì)一些接口增加了一些默認(rèn)方法,比如Map接口等等。一般來(lái)說(shuō),使用默認(rèn)方法的場(chǎng)景有兩個(gè):可選方法和行為的多繼承。

默認(rèn)方法的使用相對(duì)來(lái)說(shuō)比較簡(jiǎn)單,唯一要注意的點(diǎn)是如何處理默認(rèn)方法的沖突。關(guān)于如何處理默認(rèn)方法的沖突可以參考以下三條規(guī)則:

類中的方法優(yōu)先級(jí)最高。類或父類中聲明的方法的優(yōu)先級(jí)高于任何聲明為默認(rèn)方法的優(yōu)先級(jí)。

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

最后,如果還是無(wú)法判斷,繼承了多個(gè)接口的類必須通過(guò)顯式覆蓋和調(diào)用期望的方法,顯式地選擇使用哪一個(gè)默認(rèn)方法的實(shí)現(xiàn)。那么如何顯式地指定呢:

public class C implements B, A {

    public void hello() {
        B.super().hello();    
    }

}

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

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

默認(rèn)方法與抽象方法不同之處在于抽象方法必須要求實(shí)現(xiàn),但是默認(rèn)方法則沒(méi)有這個(gè)要求。相反,每個(gè)接口都必須提供一個(gè)所謂的默認(rèn)實(shí)現(xiàn),這樣所有的接口實(shí)現(xiàn)者將會(huì)默認(rèn)繼承它(如果有必要的話,可以覆蓋這個(gè)默認(rèn)實(shí)現(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接口用關(guān)鍵字default聲明了一個(gè)默認(rèn)方法notRequired(),Defaulable接口的實(shí)現(xiàn)者之一DefaultableImpl實(shí)現(xiàn)了這個(gè)接口,并且讓默認(rèn)方法保持原樣。Defaulable接口的另一個(gè)實(shí)現(xiàn)者OverridableImpl用自己的方法覆蓋了默認(rèn)方法。

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

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

下面的一小段代碼片段把上面的默認(rèn)方法與靜態(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() );
}

這個(gè)程序的控制臺(tái)輸出如下:

Default implementation
Overridden implementation
在JVM中,默認(rèn)方法的實(shí)現(xiàn)是非常高效的,并且通過(guò)字節(jié)碼指令為方法調(diào)用提供了支持。默認(rèn)方法允許繼續(xù)使用現(xiàn)有的Java接口,而同時(shí)能夠保障正常的編譯過(guò)程。這方面好的例子是大量的方法被添加到j(luò)ava.util.Collection接口中去:stream(),parallelStream(),forEach(),removeIf(),……

盡管默認(rèn)方法非常強(qiáng)大,但是在使用默認(rèn)方法時(shí)我們需要小心注意一個(gè)地方:在聲明一個(gè)默認(rèn)方法前,請(qǐng)仔細(xì)思考是不是真的有必要使用默認(rèn)方法,因?yàn)槟J(rèn)方法會(huì)帶給程序歧義,并且在復(fù)雜的繼承體系中容易產(chǎn)生編譯錯(cuò)誤。更多詳情請(qǐng)參考官方文檔

重復(fù)注解

自從Java 5引入了注解機(jī)制,這一特性就變得非常流行并且廣為使用。然而,使用注解的一個(gè)限制是相同的注解在同一位置只能聲明一次,不能聲明多次。Java 8打破了這條規(guī)則,引入了重復(fù)注解機(jī)制,這樣相同的注解可以在同一地方聲明多次。

重復(fù)注解機(jī)制本身必須用@Repeatable注解。事實(shí)上,這并不是語(yǔ)言層面上的改變,更多的是編譯器的技巧,底層的原理保持不變。讓我們看一個(gè)快速入門(mén)的例子:

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

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

同時(shí),反射相關(guān)的API提供了新的函數(shù)getAnnotationsByType()來(lái)返回重復(fù)注解的類型(請(qǐng)注意Filterable.class.getAnnotation( Filters.class )經(jīng)編譯器處理后將會(huì)返回Filters的實(shí)例)。

程序輸出結(jié)果如下:

filter1
filter2
更多詳情請(qǐng)參考官方文檔

Java編譯器的新特性

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

很長(zhǎng)一段時(shí)間里,Java程序員一直在發(fā)明不同的方式使得方法參數(shù)的名字能保留在Java字節(jié)碼中,并且能夠在運(yùn)行時(shí)獲取它們(比如,Paranamer類庫(kù))。最終,在Java 8中把這個(gè)強(qiáng)烈要求的功能添加到語(yǔ)言層面(通過(guò)反射API與Parameter.getName()方法)與字節(jié)碼文件(通過(guò)新版的javac的–parameters選項(xiàng))中。

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ù)來(lái)編譯這個(gè)類,然后運(yùn)行這個(gè)類,會(huì)得到下面的輸出:

Parameter: arg0
如果使用–parameters參數(shù)來(lái)編譯這個(gè)類,程序的結(jié)構(gòu)會(huì)有所不同(參數(shù)的真實(shí)名字將會(huì)顯示出來(lái)):

Parameter: args

Java 類庫(kù)的新特性

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

Optional

到目前為止,臭名昭著的空指針異常是導(dǎo)致Java應(yīng)用程序失敗的最常見(jiàn)原因。以前,為了解決空指針異常,Google公司著名的Guava項(xiàng)目引入了Optional類,Guava通過(guò)使用檢查空值的方式來(lái)防止代碼污染,它鼓勵(lì)程序員寫(xiě)更干凈的代碼。受到Google Guava的啟發(fā),Optional類已經(jīng)成為Java 8類庫(kù)的一部分。

Optional實(shí)際上是個(gè)容器:它可以保存類型T的值,或者僅僅保存null。Optional提供很多有用的方法,這樣我們就不用顯式進(jìn)行空值檢測(cè)。更多詳情請(qǐng)參考官方文檔。

我們下面用兩個(gè)小例子來(lái)演示如何使用Optional類:一個(gè)允許為空值,一個(gè)不允許為空值。

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

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

        //拋出沒(méi)有該元素的異常
        //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));//當(dāng)值為空時(shí)給與初始值
        System.out.println(optional.orElseGet(() -> new String[]{"a"}));//使用回調(diào)函數(shù)設(shè)置默認(rèn)值
        //即使傳入Optional容器的元素為空,使用optional.isPresent()方法也不會(huì)報(bào)空指針異常
        //所以通過(guò)optional.orElse這種方式就可以寫(xiě)出避免空指針異常的代碼了
        //輸出Optional.empty。
    }
}

如果Optional類的實(shí)例為非空值的話,isPresent()返回true,否從返回false。為了防止Optional為空值,orElseGet()方法通過(guò)回調(diào)函數(shù)來(lái)產(chǎn)生一個(gè)默認(rèn)值。map()函數(shù)對(duì)當(dāng)前Optional的值進(jìn)行轉(zhuǎn)化,然后返回一個(gè)新的Optional實(shí)例。orElse()方法和orElseGet()方法類似,但是orElse接受一個(gè)默認(rèn)值而不是一個(gè)回調(diào)函數(shù)。下面是這個(gè)程序的輸出:

Full Name is set? false
Full Name: [none]
Hey Stranger!
讓我們來(lái)看看另一個(gè)例子:

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ù)式編程風(fēng)格引入到Java中。這是目前為止對(duì)Java類庫(kù)最好的補(bǔ)充,因?yàn)镾tream API可以極大提供Java程序員的生產(chǎn)力,讓程序員寫(xiě)出高效率、干凈、簡(jiǎn)潔的代碼。

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

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

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

我們下面要討論的第一個(gè)問(wèn)題是所有狀態(tài)為OPEN的任務(wù)一共有多少分?jǐn)?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 );

程序在控制臺(tái)上的輸出如下:

Total points: 18

這里有幾個(gè)注意事項(xiàng)。

第一,task集合被轉(zhuǎn)換化為其相應(yīng)的stream表示。然后,filter操作過(guò)濾掉狀態(tài)為CLOSED的task。

下一步,mapToInt操作通過(guò)Task::getPoints這種方式調(diào)用每個(gè)task實(shí)例的getPoints方法把Task的stream轉(zhuǎn)化為Integer的stream。最后,用sum函數(shù)把所有的分?jǐn)?shù)加起來(lái),得到最終的結(jié)果。

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

中間操作返回一個(gè)新的stream對(duì)象。中間操作總是采用惰性求值方式,運(yùn)行一個(gè)像filter這樣的中間操作實(shí)際上沒(méi)有進(jìn)行任何過(guò)濾,相反它在遍歷元素時(shí)會(huì)產(chǎn)生了一個(gè)新的stream對(duì)象,這個(gè)新的stream對(duì)象包含原始stream
中符合給定謂詞的所有元素。

像forEach、sum這樣的最終操作可能直接遍歷stream,產(chǎn)生一個(gè)結(jié)果或副作用。當(dāng)最終操作執(zhí)行結(jié)束之后,stream管道被認(rèn)為已經(jīng)被消耗了,沒(méi)有可能再被使用了。在大多數(shù)情況下,最終操作都是采用及早求值方式,及早完成底層數(shù)據(jù)源的遍歷。

stream另一個(gè)有價(jià)值的地方是能夠原生支持并行處理。讓我們來(lái)看看這個(gè)算task分?jǐn)?shù)和的例子。

stream另一個(gè)有價(jià)值的地方是能夠原生支持并行處理。讓我們來(lái)看看這個(gè)算task分?jǐn)?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 );

這個(gè)例子和第一個(gè)例子很相似,但這個(gè)例子的不同之處在于這個(gè)程序是并行運(yùn)行的,其次使用reduce方法來(lái)算最終的結(jié)果。
下面是這個(gè)例子在控制臺(tái)的輸出:

Total points (all tasks): 26.0
經(jīng)常會(huì)有這個(gè)一個(gè)需求:我們需要按照某種準(zhǔn)則來(lái)對(duì)集合中的元素進(jìn)行分組。Stream也可以處理這樣的需求,下面是一個(gè)例子:

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

這個(gè)例子的控制臺(tái)輸出如下:

{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
讓我們來(lái)計(jì)算整個(gè)集合中每個(gè)task分?jǐn)?shù)(或權(quán)重)的平均值來(lái)結(jié)束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 );

下面是這個(gè)例子的控制臺(tái)輸出:

[19%, 50%, 30%]
最后,就像前面提到的,Stream API不僅僅處理Java集合框架。像從文本文件中逐行讀取數(shù)據(jù)這樣典型的I/O操作也很適合用Stream API來(lái)處理。下面用一個(gè)例子來(lái)應(yīng)證這一點(diǎn)。

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

對(duì)一個(gè)stream對(duì)象調(diào)用onClose方法會(huì)返回一個(gè)在原有功能基礎(chǔ)上新增了關(guān)閉功能的stream對(duì)象,當(dāng)對(duì)stream對(duì)象調(diào)用close()方法時(shí),與關(guān)閉相關(guān)的處理器就會(huì)執(zhí)行。

Stream API、Lambda表達(dá)式與方法引用在接口默認(rèn)方法與靜態(tài)方法的配合下是Java 8對(duì)現(xiàn)代軟件開(kāi)發(fā)范式的回應(yīng)。更多詳情請(qǐng)參考官方文檔。

Date/Time API (JSR 310)

Java 8通過(guò)發(fā)布新的Date-Time API (JSR 310)來(lái)進(jìn)一步加強(qiáng)對(duì)日期與時(shí)間的處理。對(duì)日期與時(shí)間的操作一直是Java程序員最痛苦的地方之一。標(biāo)準(zhǔn)的 java.util.Date以及后來(lái)的java.util.Calendar一點(diǎn)沒(méi)有改善這種情況(可以這么說(shuō),它們一定程度上更加復(fù)雜)。

這種情況直接導(dǎo)致了Joda-Time——一個(gè)可替換標(biāo)準(zhǔn)日期/時(shí)間處理且功能非常強(qiáng)大的Java API的誕生。Java 8新的Date-Time API (JSR 310)在很大程度上受到Joda-Time的影響,并且吸取了其精髓。新的java.time包涵蓋了所有處理日期,時(shí)間,日期/時(shí)間,時(shí)區(qū),時(shí)刻(instants),過(guò)程(during)與時(shí)鐘(clock)的操作。在設(shè)計(jì)新版API時(shí),十分注重與舊版API的兼容性:不允許有任何的改變(從java.util.Calendar中得到的深刻教訓(xùn))。如果需要修改,會(huì)返回這個(gè)類的一個(gè)新實(shí)例。

讓我們用例子來(lái)看一下新版API主要類的使用方法。第一個(gè)是Clock類,它通過(guò)指定一個(gè)時(shí)區(qū),然后就可以獲取到當(dāng)前的時(shí)刻,日期與時(shí)間。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() );

下面是程序在控制臺(tái)上的輸出:

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

我們需要關(guān)注的其他類是LocaleDate與LocalTime。LocaleDate只持有ISO-8601格式且無(wú)時(shí)區(qū)信息的日期部分。相應(yīng)的,LocaleTime只持有ISO-8601格式且無(wú)時(shí)區(qū)信息的時(shí)間部分。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 );

下面是程序在控制臺(tái)上的輸出:

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

下面是程序在控制臺(tái)上的輸出:

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類:在秒與納秒級(jí)別上的一段時(shí)間。Duration使計(jì)算兩個(gè)日期間的不同變的十分簡(jiǎn)單。下面讓我們看一個(gè)這方面的例子。

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

上面的例子計(jì)算了兩個(gè)日期2014年4月16號(hào)與2014年4月16號(hào)之間的過(guò)程。下面是程序在控制臺(tái)上的輸出:

Duration in days: 365
Duration in hours: 8783
對(duì)Java 8在日期/時(shí)間API的改進(jìn)整體印象是非常非常好的。一部分原因是因?yàn)樗⒃凇熬脩?zhàn)殺場(chǎng)”的Joda-Time基礎(chǔ)上,另一方面是因?yàn)橛脕?lái)大量的時(shí)間來(lái)設(shè)計(jì)它,并且這次程序員的聲音得到了認(rèn)可。更多詳情請(qǐng)參考官方文檔。

并行(parallel)數(shù)組

Java 8增加了大量的新方法來(lái)對(duì)數(shù)組進(jìn)行并行處理??梢哉f(shuō),最重要的是parallelSort()方法,因?yàn)樗梢栽诙嗪藱C(jī)器上極大提高數(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()方法來(lái)對(duì)一個(gè)有20000個(gè)元素的數(shù)組進(jìn)行隨機(jī)賦值。然后,調(diào)用parallelSort方法。這個(gè)程序首先打印出前10個(gè)元素的值,之后對(duì)整個(gè)數(shù)組排序。這個(gè)程序在控制臺(tái)上的輸出如下(請(qǐng)注意數(shù)組元素是隨機(jī)生產(chǎn)的):

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

CompletableFuture

在Java8之前,我們會(huì)使用JDK提供的Future接口來(lái)進(jìn)行一些異步的操作,其實(shí)CompletableFuture也是實(shí)現(xiàn)了Future接口, 并且基于ForkJoinPool來(lái)執(zhí)行任務(wù),因此本質(zhì)上來(lái)講,CompletableFuture只是對(duì)原有API的封裝, 而使用CompletableFuture與原來(lái)的Future的不同之處在于可以將兩個(gè)Future組合起來(lái),或者如果兩個(gè)Future是有依賴關(guān)系的,可以等第一個(gè)執(zhí)行完畢后再實(shí)行第二個(gè)等特性。

先來(lái)看看基本的使用方式:

public Future<Double> getPriceAsync(final String product) {
    final CompletableFuture<Double> futurePrice = new CompletableFuture<>();
    new Thread(() -> {
        double price = calculatePrice(product);
        futurePrice.complete(price);  //完成后使用complete方法,設(shè)置future的返回值
    }).start();
    return futurePrice;
}

得到Future之后就可以使用get方法來(lái)獲取結(jié)果,CompletableFuture提供了一些工廠方法來(lái)簡(jiǎn)化這些API,并且使用函數(shù)式編程的方式來(lái)使用這些API,例如:

Fufure<Double> price = CompletableFuture.supplyAsync(() -> calculatePrice(product));
代碼是不是一下子簡(jiǎn)潔了許多呢。之前說(shuō)了,CompletableFuture可以組合多個(gè)Future,不管是Future之間有依賴的,還是沒(méi)有依賴的。

如果第二個(gè)請(qǐng)求依賴于第一個(gè)請(qǐng)求的結(jié)果,那么可以使用thenCompose方法來(lái)組合兩個(gè)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來(lái)組合兩個(gè)CompletableFuture。supplyAsync方法第二個(gè)參數(shù)接受一個(gè)自定義的Executor。 首先使用CompletableFuture執(zhí)行一個(gè)任務(wù),調(diào)用getPrice方法,得到一個(gè)Future,之后使用thenApply方法,將Future的結(jié)果應(yīng)用parse方法, 之后再使用執(zhí)行完parse之后的結(jié)果作為參數(shù)再執(zhí)行一個(gè)applyCount方法,然后收集成一個(gè)CompletableFuture<String>的List, 最后再使用一個(gè)流,調(diào)用CompletableFuture的join方法,這是為了等待所有的異步任務(wù)執(zhí)行完畢,獲得最后的結(jié)果。

注意,這里必須使用兩個(gè)流,如果在一個(gè)流里調(diào)用join方法,那么由于Stream的延遲特性,所有的操作還是會(huì)串行的執(zhí)行,并不是異步的。

再來(lái)看一個(gè)兩個(gè)Future之間沒(méi)有依賴關(guān)系的例子:

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

這里有兩個(gè)異步的任務(wù),使用thenCombine方法來(lái)組合兩個(gè)Future,thenCombine方法的第二個(gè)參數(shù)就是用來(lái)合并兩個(gè)Future方法返回值的操作函數(shù)。

有時(shí)候,我們并不需要等待所有的異步任務(wù)結(jié)束,只需要其中的一個(gè)完成就可以了,CompletableFuture也提供了這樣的方法:

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

Java虛擬機(jī)(JVM)的新特性

PermGen空間被移除了,取而代之的是Metaspace(JEP 122)。JVM選項(xiàng)-XX:PermSize與-XX:MaxPermSize分別被-XX:MetaSpaceSize與-XX:MaxMetaspaceSize所代替。

總結(jié)

更多展望:Java 8通過(guò)發(fā)布一些可以增加程序員生產(chǎn)力的特性來(lái)推進(jìn)這個(gè)偉大的平臺(tái)的進(jìn)步?,F(xiàn)在把生產(chǎn)環(huán)境遷移到Java 8還為時(shí)尚早,但是在接下來(lái)的幾個(gè)月里,它會(huì)被大眾慢慢的接受。毫無(wú)疑問(wèn),現(xiàn)在是時(shí)候讓你的代碼與Java 8兼容,并且在Java 8足夠安全穩(wěn)定的時(shí)候遷移到Java 8。

參考文章

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

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

https://www.jianshu.com/p/4df02599aeb2

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

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

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI