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

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

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

喜歡的話麻煩點下Star哈git

文章首發於個人我的博客:程序員

www.how2playlife.comgithub

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

image

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註解)。讓咱們看一下這種函數式接口的定義:

@FunctionalInterfacepublic interface Functional {void method();}須要記住的一件事是:默認方法與靜態方法並不影響函數式接口的契約,能夠任意使用:

@FunctionalInterfacepublic 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 implementationOverridden 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的實例)。

程序輸出結果以下:

filter1filter2更多詳情請參考官方文檔

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? falseFull 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? trueFirst Name: TomHey 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: 365Duration 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 552378Sorted: 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 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方法第二個參數接受一個自定義的Executor。 首先使用CompletableFuture執行一個任務,調用getPrice方法,獲得一個Future,以後使用thenApply方法,將Future的結果應用parse方法, 以後再使用執行完parse以後的結果做爲參數再執行一個applyCount方法,而後收集成一個CompletableFuture<String>的List, 最後再使用一個流,調用CompletableFuture的join方法,這是爲了等待全部的異步任務執行完畢,得到最後的結果。複製代碼

注意,這裏必須使用兩個流,若是在一個流裏調用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技術學習資源: 一些程序員學習技術的資源大禮包,關注公衆號後,後臺回覆關鍵字 「資料」 便可免費無套路獲取。

相關文章
相關標籤/搜索