1. 程式人生 > >夯實Java基礎系列21:Java8新特性終極指南

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

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

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

喜歡的話麻煩點下Star哈

文章首發於我的個人部落格:

www.how2playlife.com

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

Java語言新特性

Lambda表示式

Lambda表示式(也稱為閉包)是整個Java 8發行版中最受期待的在Java語言層面上的改變,Lambda允許把函式作為一個方法的引數(函式作為引數傳遞進方法中),或者把程式碼看成資料:函式式程式設計師對這一概念非常熟悉。在JVM平臺上的很多語言(Groovy,Scala,……)從一開始就有Lambda,但是Java程式設計師不得不使用毫無新意的匿名類來代替lambda。

關於Lambda設計的討論佔用了大量的時間與社群的努力。可喜的是,最終找到了一個平衡點,使得可以使用一種即簡潔又緊湊的新方式來構造Lambdas。在最簡單的形式中,一個lambda可以由用逗號分隔的引數列表、–>符號與函式體三部分表示。例如:

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

請注意引數e的型別是由編譯器推測出來的。同時,你也可以通過把引數型別與引數包括在括號中的形式直接給出引數的型別:

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

在某些情況下lambda的函式體會更加複雜,這時可以把函式體放到在一對花括號中,就像在Java中定義普通函式一樣。例如:

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的函式體只有一行的話,那麼沒有必要顯式使用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;
} );

語言設計者投入了大量精力來思考如何使現有的函式友好地支援lambda。

最終採取的方法是:增加函式式介面的概念。函式式介面就是一個具有一個方法的普通介面。像這樣的介面,可以被隱式轉換為lambda表示式。

java.lang.Runnable與java.util.concurrent.Callable是函式式介面最典型的兩個例子。

在實際使用過程中,函式式介面是容易出錯的:如有某個人在介面定義中增加了另一個方法,這時,這個介面就不再是函式式的了,並且編譯過程也會失敗。

為了克服函式式介面的這種脆弱性並且能夠明確宣告介面作為函式式介面的意圖,Java8增加了一種特殊的註解@FunctionalInterface(Java8中所有類庫的已有介面都添加了@FunctionalInterface註解)。讓我們看一下這種函式式介面的定義:

@FunctionalInterface
public interface Functional {
void method();
}
需要記住的一件事是:預設方法與靜態方法並不影響函式式介面的契約,可以任意使用:

@FunctionalInterface
public interface FunctionalDefaultMethods {
void method();

default void defaultMethod() {            
}        

}
Lambda是Java 8最大的賣點。它具有吸引越來越多程式設計師到Java平臺上的潛力,並且能夠在純Java語言環境中提供一種優雅的方式來支援函數語言程式設計。更多詳情可以參考官方文件。

下面看一個例子:

public class lambda和函數語言程式設計 {
    @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的兩個引數和get的兩個引數吻合並且返回型別也要相等,否則報錯
//        static void add(double a,String b) {
//            System.out.println(a + b);
//        }
    }

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

函式式介面

所謂的函式式介面就是隻有一個抽象方法的介面,注意這裡說的是抽象方法,因為Java8中加入了預設方法的特性,但是函式式介面是不關心介面中有沒有預設方法的。 一般函式式介面可以使用@FunctionalInterface註解的形式來標註表示這是一個函式式介面,該註解標註與否對函式式介面沒有實際的影響, 不過一般還是推薦使用該註解,就像使用@Override註解一樣。

lambda表示式是如何符合 Java 型別系統的?每個lambda對應於一個給定的型別,用一個介面來說明。而這個被稱為函式式介面(functional interface)的介面必須僅僅包含一個抽象方法宣告。每個那個型別的lambda表示式都將會被匹配到這個抽象方法上。因此預設的方法並不是抽象的,你可以給你的函式式介面自由地增加預設的方法。

我們可以使用任意的介面作為lambda表示式,只要這個介面只包含一個抽象方法。為了保證你的介面滿足需求,你需要增加@FunctionalInterface註解。編譯器知道這個註解,一旦你試圖給這個介面增加第二個抽象方法宣告時,它將丟擲一個編譯器錯誤。

下面舉幾個例子

public class 函式式介面使用 {
    @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其實換成別的也行,只是將方法傳給介面作為其方法實現
        B b2 = Integer::valueOf;//i與方法傳入引數的變數型別一直時,可以直接替換
        B b3 = String::valueOf;
        //B b4 = Integer::parseInt;型別不符,無法使用

    }
    @FunctionalInterface
    interface C {
        int say(String i);
    }
    public void test3() {
        C c = Integer::parseInt;//方法引數和介面方法的引數一樣,可以替換。
        int i = c.say("1");
        //當我把C介面的int替換為void時就會報錯,因為返回型別不一致。
        System.out.println(i);
        //綜上所述,lambda表示式提供了一種簡便的表達方式,可以將一個方法傳到介面中。
        //函式式介面是隻提供一個抽象方法的介面,其方法由lambda表示式注入,不需要寫實現類,
        //也不需要寫匿名內部類,可以省去很多程式碼,比如實現runnable介面。
        //函數語言程式設計就是指把方法當做一個引數或引用來進行操作。除了普通方法以外,靜態方法,構造方法也是可以這樣操作的。
    }
}

請記住如果@FunctionalInterface 這個註解被遺漏,此程式碼依然有效。

方法引用

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表示式的更簡潔的一種表達形式,使用::操作符,方法引用主要有三類:

指向靜態方法的方法引用(例如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);

介面的預設方法

Java 8 使我們能夠使用default 關鍵字給介面增加非抽象的方法實現。這個特性也被叫做 擴充套件方法(Extension Methods)。如下例所示:

public class 介面的預設方法 {
    class B implements A {
//        void a(){}實現類方法不能重名
    }
    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();

    }
}

預設方法出現的原因是為了對原有介面的擴充套件,有了預設方法之後就不怕因改動原有的介面而對已經使用這些介面的程式造成的程式碼不相容的影響。 在Java8中也對一些介面增加了一些預設方法,比如Map介面等等。一般來說,使用預設方法的場景有兩個:可選方法和行為的多繼承。

預設方法的使用相對來說比較簡單,唯一要注意的點是如何處理預設方法的衝突。關於如何處理預設方法的衝突可以參考以下三條規則:

類中的方法優先順序最高。類或父類中宣告的方法的優先順序高於任何宣告為預設方法的優先順序。

如果無法依據第一條規則進行判斷,那麼子介面的優先順序更高:函式簽名相同時,優先選擇擁有最具體實現的預設方法的介面。即如果B繼承了A,那麼B就比A更具體。

最後,如果還是無法判斷,繼承了多個介面的類必須通過顯式覆蓋和呼叫期望的方法,顯式地選擇使用哪一個預設方法的實現。那麼如何顯式地指定呢:

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

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

Java 8用預設方法與靜態方法這兩個新概念來擴充套件介面的宣告。預設方法使介面有點像Traits(Scala中特徵(trait)類似於Java中的Interface,但它可以包含實現程式碼,也就是目前Java8新增的功能),但與傳統的介面又有些不一樣,它允許在已有的介面中新增新方法,而同時又保持了與舊版本程式碼的相容性。

預設方法與抽象方法不同之處在於抽象方法必須要求實現,但是預設方法則沒有這個要求。相反,每個介面都必須提供一個所謂的預設實現,這樣所有的介面實現者將會預設繼承它(如果有必要的話,可以覆蓋這個預設實現)。讓我們看看下面的例子:

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介面的實現者之一DefaultableImpl實現了這個介面,並且讓預設方法保持原樣。Defaulable介面的另一個實現者OverridableImpl用自己的方法覆蓋了預設方法。

Java 8帶來的另一個有趣的特性是介面可以宣告(並且可以提供實現)靜態方法。例如:

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

這個程式的控制檯輸出如下:

Default implementation
Overridden implementation
在JVM中,預設方法的實現是非常高效的,並且通過位元組碼指令為方法呼叫提供了支援。預設方法允許繼續使用現有的Java介面,而同時能夠保障正常的編譯過程。這方面好的例子是大量的方法被新增到java.util.Collection介面中去:stream(),parallelStream(),forEach(),removeIf(),……

儘管預設方法非常強大,但是在使用預設方法時我們需要小心注意一個地方:在宣告一個預設方法前,請仔細思考是不是真的有必要使用預設方法,因為預設方法會帶給程式歧義,並且在複雜的繼承體系中容易產生編譯錯誤。更多詳情請參考官方文件

重複註解

自從Java 5引入了註解機制,這一特性就變得非常流行並且廣為使用。然而,使用註解的一個限制是相同的註解在同一位置只能宣告一次,不能宣告多次。Java 8打破了這條規則,引入了重複註解機制,這樣相同的註解可以在同一地方宣告多次。

重複註解機制本身必須用@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,Filters僅僅是Filter註解的陣列,但Java編譯器並不想讓程式設計師意識到Filters的存在。這樣,介面Filterable就擁有了兩次Filter(並沒有提到Filter)註解。

同時,反射相關的API提供了新的函式getAnnotationsByType()來返回重複註解的型別(請注意Filterable.class.getAnnotation( Filters.class )經編譯器處理後將會返回Filters的例項)。

程式輸出結果如下:

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

Java編譯器的新特性

方法引數名字可以反射獲取

很長一段時間裡,Java程式設計師一直在發明不同的方式使得方法引數的名字能保留在Java位元組碼中,並且能夠在執行時獲取它們(比如,Paranamer類庫)。最終,在Java 8中把這個強烈要求的功能新增到語言層面(通過反射API與Parameter.getName()方法)與位元組碼檔案(通過新版的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引數來編譯這個類,然後執行這個類,會得到下面的輸出:

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

Parameter: args

Java 類庫的新特性

Java 8 通過增加大量新類,擴充套件已有類的功能的方式來改善對併發程式設計、函數語言程式設計、日期/時間相關操作以及其他更多方面的支援。

Optional

到目前為止,臭名昭著的空指標異常是導致Java應用程式失敗的最常見原因。以前,為了解決空指標異常,Google公司著名的Guava專案引入了Optional類,Guava通過使用檢查空值的方式來防止程式碼汙染,它鼓勵程式設計師寫更乾淨的程式碼。受到Google Guava的啟發,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"}));//使用回撥函式設定預設值
        //即使傳入Optional容器的元素為空,使用optional.isPresent()方法也不會報空指標異常
        //所以通過optional.orElse這種方式就可以寫出避免空指標異常的程式碼了
        //輸出Optional.empty。
    }
}

如果Optional類的例項為非空值的話,isPresent()返回true,否從返回false。為了防止Optional為空值,orElseGet()方法通過回撥函式來產生一個預設值。map()函式對當前Optional的值進行轉化,然後返回一個新的Optional例項。orElse()方法和orElseGet()方法類似,但是orElse接受一個預設值而不是一個回撥函式。下面是這個程式的輸出:

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) 把真正的函數語言程式設計風格引入到Java中。這是目前為止對Java類庫最好的補充,因為Stream API可以極大提供Java程式設計師的生產力,讓程式設計師寫出高效率、乾淨、簡潔的程式碼。

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

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

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

我們下面要討論的第一個問題是所有狀態為OPEN的任務一共有多少分數?在Java 8以前,一般的解決方式用foreach迴圈,但是在Java 8裡面我們可以使用stream:一串支援連續、並行聚集操作的元素。

// 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操作過濾掉狀態為CLOSED的task。

下一步,mapToInt操作通過Task::getPoints這種方式呼叫每個task例項的getPoints方法把Task的stream轉化為Integer的stream。最後,用sum函式把所有的分數加起來,得到最終的結果。

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

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

像forEach、sum這樣的最終操作可能直接遍歷stream,產生一個結果或副作用。當最終操作執行結束之後,stream管道被認為已經被消耗了,沒有可能再被使用了。在大多數情況下,最終操作都是採用及早求值方式,及早完成底層資料來源的遍歷。

stream另一個有價值的地方是能夠原生支援並行處理。讓我們來看看這個算task分數和的例子。

stream另一個有價值的地方是能夠原生支援並行處理。讓我們來看看這個算task分數和的例子。

// 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分數(或權重)的平均值來結束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集合框架。像從文字檔案中逐行讀取資料這樣典型的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()方法時,與關閉相關的處理器就會執行。

Stream API、Lambda表示式與方法引用在介面預設方法與靜態方法的配合下是Java 8對現代軟體開發正規化的迴應。更多詳情請參考官方文件。

Date/Time API (JSR 310)

Java 8通過釋出新的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包涵蓋了所有處理日期,時間,日期/時間,時區,時刻(instants),過程(during)與時鐘(clock)的操作。在設計新版API時,十分注重與舊版API的相容性:不允許有任何的改變(從java.util.Calendar中得到的深刻教訓)。如果需要修改,會返回這個類的一個新例項。

讓我們用例子來看一下新版API主要類的使用方法。第一個是Clock類,它通過指定一個時區,然後就可以獲取到當前的時刻,日期與時間。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格式且無時區資訊的日期部分。相應的,LocaleTime只持有ISO-8601格式且無時區資訊的時間部分。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的改進整體印象是非常非常好的。一部分原因是因為它建立在“久戰殺場”的Joda-Time基礎上,另一方面是因為用來大量的時間來設計它,並且這次程式設計師的聲音得到了認可。更多詳情請參考官方文件。

並行(parallel)陣列

Java 8增加了大量的新方法來對陣列進行並行處理。可以說,最重要的是parallelSort()方法,因為它可以在多核機器上極大提高陣列排序的速度。下面的例子展示了新方法(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個元素的陣列進行隨機賦值。然後,呼叫parallelSort方法。這個程式首先打印出前10個元素的值,之後對整個陣列排序。這個程式在控制檯上的輸出如下(請注意陣列元素是隨機生產的):

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也是實現了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

如果第二個請求依賴於第一個請求的結果,那麼可以使用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

注意,這裡必須使用兩個流,如果在一個流裡呼叫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事件。

Java虛擬機器(JVM)的新特性

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

總結

更多展望:Java 8通過釋出一些可以增加程式設計師生產力的特性來推進這個偉大的平臺的進步。現在把生產環境遷移到Java 8還為時尚早,但是在接下來的幾個月裡,它會被大眾慢慢的接受。毫無疑問,現在是時候讓你的程式碼與Java 8相容,並且在Java 8足夠安全穩定的時候遷移到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

微信公眾號

Java技術江湖

如果大家想要實時關注我更新的文章以及分享的乾貨的話,可以關注我的公眾號【Java技術江湖】一位阿里 Java 工程師的技術小站,作者黃小斜,專注 Java 相關技術:SSM、SpringBoot、MySQL、分散式、中介軟體、叢集、Linux、網路、多執行緒,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!

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

個人公眾號:黃小斜

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

作者專注於 JAVA 後端技術棧,熱衷於分享程式設計師乾貨、學習經驗、求職心得和程式人生,目前黃小斜的CSDN部落格有百萬+訪問量,知乎粉絲2W+,全網已有10W+讀者。

黃小斜是一個斜槓青年,堅持學習和寫作,相信終身學習的力量,希望和更多的程式設計師交朋友,一起進步和成長!

關注公眾號【黃小斜】後回覆【原創電子書】即可領取我原創的電子書《菜鳥程式設計師修煉手冊:從技術小白到阿里巴巴Java工程師》

程式設計師3T技術學習資源: 一些程式設計師學習技術的資源大禮包,關注公眾號後,後臺回覆關鍵字 “資料” 即可免費無套路獲取。

相關推薦

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

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視 https://github.com/h2pl/Java-Tutorial 喜歡的話麻煩點下Star哈 文章首發於我的個人部落格: www.how2playlife.com 這是一個Java8新增特性的總

夯實Java基礎系列4一文了解final關鍵字的特性、使用方法,以及實現原理

目錄 final使用 final變數 final修飾基本資料型別變數和引用 final類 final關鍵字的知識點 final關鍵字的最佳實踐 final的用法 關於空白final final記憶體分配 使用final修飾方法會提高速度和效率嗎 使用final修飾變數會讓變數的值不能被改變嗎; 如何保

夯實Java基礎系列5Java檔案和Java包結構

目錄 Java中的包概念 包的作用 package 的目錄結構 設定 CLASSPATH 系統變數 常用jar包 java軟體包的型別 dt.jar rt.jar *.java檔案的奧祕 *.Java檔案簡介 為什麼一個java原始檔中只能有一個public類? Main方法 外部類的訪問許可權

夯實Java基礎系列6一文搞懂抽象類和介面,從基礎到面試題,揭祕其本質區別!

目錄 抽象類介紹 為什麼要用抽象類 一個抽象類小故事 一個抽象類小遊戲 介面介紹 介面與類相似點: 介面與類的區別: 介面特性 抽象類和介面的區別 介面的使用: 介面最佳實踐:設計模式中的工廠模式 介面與抽象類的本質區別是什麼? 基本語法區別 設計思想區別 如何回答面試題:介面和抽象類的區別?

夯實Java基礎系列7一文讀懂Java 程式碼塊和執行順序

目錄 Java中的構造方法 構造方法簡介 構造方法例項 例 1 例 2 Java中的幾種構造方法詳解 普通構造方法 預設構造方法 過載構造方法 java子類構造方法呼叫父類構造方法 Java中的程式碼塊簡介 Java程式碼塊使用 區域性程式碼塊 構造程式碼塊 靜態程式碼塊 Java程式碼塊、

夯實Java基礎系列9深入理解Class類和Object類

目錄 Java中Class類及用法 Class類原理 如何獲得一個Class類物件 使用Class類的物件來生成目標類的例項 Object類 類構造器public Object(); registerNatives()方法; Clone()方法實現淺拷貝 getClass()方法 equals()方法

夯實Java基礎系列10深入理解Java中的異常體系

目錄 為什麼要使用異常 異常基本定義 異常體系 初識異常 異常和錯誤 異常的處理方式 "不負責任"的throws 糾結的finally throw : JRE也使用的關鍵字 異常呼叫鏈 自定義異常 異常的注意事項 當finally遇上return JAVA異常常見面試題 參考文章 微信公眾號 Java技術

夯實Java基礎系列11深入理解Java中的回撥機制

目錄 模組間的呼叫 多執行緒中的“回撥” Java回撥機制實戰 例項一 : 同步呼叫 例項二:由淺入深 例項三:Tom做題 參考文章

夯實Java基礎系列12深入理解Java中的反射機制

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視 https://github.com/h2pl/Java-Tutorial 喜歡的話麻煩點下Star哈 文章首發於我的個人部落格: www.how2playlife.com 列舉(enum)型別是Java

夯實Java基礎系列19一文搞懂Java集合類框架,以及常見面試題

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視 https://github.com/h2pl/Java-Tutorial 喜歡的話麻煩點下Star哈 文章首發於我的個人部落格: www.how2playlife.com 本文參考 https://ww

夯實Java基礎系列22一文讀懂Java序列化和反序列化

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視 https://github.com/h2pl/Java-Tutorial 喜歡的話麻煩點下Star哈 文章首發於我的個人部落格: www.how2playlife.com 本文參考 http://www

夯實Java基礎系列23一文讀懂繼承、封裝、多型的底層實現原理

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視 https://github.com/h2pl/Java-Tutorial 喜歡的話麻煩點下Star哈 文章首發於我的個人部落格: www.how2playlife.com 從JVM結構開始談多型 Jav

Java基礎系列16使用JSONObject和JSONArray解析和構造json字串

轉自:https://www.zifangsky.cn/561.html 一 介紹 在Java開發中,我們通常需要進行XML文件或JSON字串的構造和解析。當然在Java Web開發中有一些第三方外掛是可以自動完成Java物件和json之間的轉換的,比

Java基礎系列1Java面向物件

該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,並上手進行實戰,接著瞭解每個Java知識點背後的實現原理,更完整地瞭解整個Java技術體系,形成自己的知識框架。   概述: Java是面向物件的程式設計語言,Java語言提供了定義類、成員變數、方法等最基本的功能。類可被認為是一種自

Java基礎系列4抽象類與介面的前世今生

該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,並上手進行實戰,接著瞭解每個Java知識點背後的實現原理,更完整地瞭解整個Java技術體系,形成自己的知識框架。   1、抽象類:   當編寫一個類時,常常會為該類定義一些方法,這些方法用以描述該類的行為方式,那麼這些方法都

Java基礎系列5Java程式碼的執行順序

該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,並上手進行實戰,接著瞭解每個Java知識點背後的實現原理,更完整地瞭解整個Java技術體系,形成自己的知識框架。   一、構造方法 構造方法(或建構函式)是類的一種特殊方法,用來初始化類的一個新的物件。Java 中的每個類都

Java基礎系列6深入理解Java異常體系

該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,並上手進行實戰,接著瞭解每個Java知識點背後的實現原理,更完整地瞭解整個Java技術體系,形成自己的知識框架。   前言: Java的基本理念是“結構不佳的程式碼不能執行”。 “異常”這個詞有“我對此感到意外”的意思。問題出現了,你

Java基礎入門之jdk1.8特性

Lamda 表示式(目標型別) 簡介 語法糖,也叫糖衣語法 指的是計算機中 新增某種語法 這種語法 ,能使程式設計師更加方便的使用語言開發程式,同時,增強了程式碼的可讀性 避免了出錯的機會,但是,這種語法對於語言的功能並且有增強 例如: 泛型 自動裝箱拆箱 增強

JAVA秒會技術之Java8特性】利用流快速處理集合的常見操作

例子1:對集合進行排序 List<Integer> list = Lists.newArrayList(1,1,2,2,5,3,4,6,6,5,2,7); list.sort(null); list.forEach(e -> System.out.prin

夯實基礎系列Java 基礎總結

前言 大學期間接觸 Java 的時間也不短了,不論學習還是實習,都讓我發覺基礎的重要性。網際網路發展太快了,各種框架各種技術更新迭代的速度非常快,可能你剛好掌握了一門技術的應用,它卻已經走在淘汰的邊緣了。 而學習新技術總要付出一定的時間成本,那麼怎麼降低時間成本呢?那就是打好基礎,技術再怎麼革新,底層的東西也