Java基礎20:Java8新特性終極指南

Java基礎20:Java8新特性終極指南

毫無疑問,Java 8發行版是自Java 5(發行於2004,已通過了至關一段時間了)以來最具革命性的版本。Java 8 爲Java語言、編譯器、類庫、開發工具與JVM(Java虛擬機)帶來了大量新特性。在這篇教程中,咱們將一一探索這些變化,並用真實的例子說明它們適用的場景。html

本文由如下幾部分組成,它們分別涉及到Java平臺某一特定方面的內容:java

Java語言 編譯器 類庫 工具 Java運行時(JVM)git

本文參考http://www.importnew.com/11908.html程序員

具體代碼在個人GitHub中能夠找到github

https://github.com/h2pl/MyTechsql

喜歡的話麻煩點一下星哈謝謝。數據庫

文章首發於個人我的博客:express

https://h2pl.github.io/2018/05/06/javase20編程

更多關於Java後端學習的內容請到個人CSDN博客上查看:後端

https://blog.csdn.net/a724888

 

 

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

 

Java語言新特性

Lambda表達式

Lambda表達式(也稱爲閉包)是整個Java 8發行版中最受期待的在Java語言層面上的改變

Lambda容許把函數做爲一個方法的參數(函數做爲參數傳遞進方法中),或者把代碼當作數據:

使用Lambda 表達式可使代碼變的更加簡潔緊湊

lambda 表達式的語法格式以下:

(parameters) -> expression或(parameters) ->{statements; }

如下是lambda表達式的重要特徵:

· 可選類型聲明:不須要聲明參數類型,編譯器能夠統一識別參數值。

· 可選的參數圓括號:一個參數無需定義圓括號,但多個參數須要定義圓括號。

· 可選的大括號:若是主體包含了一個語句,就不須要使用大括號。

· 可選的返回關鍵字:若是主體只有一個表達式返回值則編譯器會自動返回值,大括號須要指定明表達式返回了一個數值。

lambda 表達式的局部變量能夠不用聲明爲 final,可是必須不可被後面的代碼修改(即隱性的具備final 的語義)

 

關於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體的操做,已經有實現的方法了,可使用方法引用! (實現抽象方法的參數列表,必須與方法引用方法的參數列表保持一致! ) 方法引用:使用操做符 「::」 將方法名和對象或類的名字分隔開來。 以下三種主要使用狀況:

 對象::實例方法

 類::靜態方法

 類::實例方法

構造器引用

ClassName::new

數組引用

type[]::new

有了函數式接口以後,就可使用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);
class Car {
    @FunctionalInterface
    public interface Supplier<T> {
        T get();
    }

    //Supplier是jdk1.8的接口,這裏和lamda一塊兒使用了
    public static Car create(final Supplier<Car> supplier) {
        return supplier.get();
    }

    public static void collide(final Car car) {
        System.out.println("Collided " + car.toString());
    }

    public void follow(final Car another) {
        System.out.println("Following the " + another.toString());
    }

    public void repair() {
        System.out.println("Repaired " + this.toString());
    }

    public static void main(String[] args) {
        //構造器引用:它的語法是Class::new,或者更通常的Class< T >::new實例以下:
         Car car  = Car.create(Car::new);
         Car car1 = Car.create(Car::new);
         Car car2 = Car.create(Car::new);
         Car car3 = new Car();
        List<Car> cars = Arrays.asList(car,car1,car2,car3);
        System.out.println("===================構造器引用========================");
        //靜態方法引用:它的語法是Class::static_method,實例以下:
        cars.forEach(Car::collide);
        System.out.println("===================靜態方法引用========================");
        //特定類的任意對象的方法引用:它的語法是Class::method實例以下:
        cars.forEach(Car::repair);
        System.out.println("==============特定類的任意對象的方法引用================");
        //特定對象的方法引用:它的語法是instance::method實例以下:
        final Car police = Car.create(Car::new);
        cars.forEach(police::follow);
        System.out.println("===================特定對象的方法引用===================");

    }
}

 

接口的默認方法

Java 8 新增了接口的默認方法。

簡單說,默認方法就是接口能夠有實現方法,並且不須要實現類去實現其方法。

咱們只需在方法名前面加個default關鍵字便可實現默認方法。

爲何要有這個特性?

首先,以前的接口是個雙刃劍,好處是面向抽象而不是面向具體編程,缺陷是,當須要修改接口時候,須要修改所有實現該接口的類,目前的java 8以前的集合框架沒有foreach方法,一般能想到的解決辦法是在JDK裏給相關的接口添加新的方法及實現。然而,對於已經發布的版本,是無法在給接口添加新方法的同時不影響已有的實現。因此引進的默認方法。他們的目的是爲了解決接口的修改與現有的實現不兼容的問題

 

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 類是一個能夠爲null的容器對象。若是值存在則isPresent()方法會返回true,調用get()方法會返回該對象。

Optional 是個容器:它能夠保存類型T的值,或者僅僅保存null。Optional提供不少有用的方法,這樣咱們就不用顯式進行空值檢測。

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 是 Java8 中處理集合的關鍵抽象概念,它能夠指定你但願對 集合進行的操做,能夠執行很是複雜的查找、過濾和映射數據等操做。 使用Stream API 對集合數據進行操做,就相似於使用 SQL 執行的數據庫查詢。也可使用 Stream API 來並行執行操做。簡而言之, Stream API 提供了一種高效且易於使用的處理數據的方式

 

集合講的是數據, Stream講的是計算

什麼是 Stream? Stream(流)是一個來自數據源的元素隊列並支持聚合操做(數據渠道,用於操做數據源(集合、數組等)所生成的元素序列 )

元素:是特定類型的對象,造成一個隊列。Java中的Stream並不會存儲元素,而是按需計算。

數據源 :流的來源。能夠是集合,數組,I/O channel,產生器generator等。

聚合操做: 相似SQL語句同樣的操做,好比filter, map, reduce, find,match, sorted等。

和之前的Collection操做不一樣,Stream操做還有兩個基礎的特徵:

Pipelining::中間操做都會返回流對象自己。這樣多個操做能夠串聯成一個管道,如同流式風格(fluent style)。這樣作能夠對操做進行優化,好比延遲執行(laziness)和短路( short-circuiting)。

內部迭代:之前對集合遍歷都是經過Iterator或者For-Each的方式,顯式的在集合外部進行迭代,這叫作外部迭代。Stream提供了內部迭代的方式,經過訪問者模式(Visitor)實現。

注意:

①Stream 本身不會存儲元素

②Stream 不會改變源對象。相反,他們會返回一個持有結果的新Stream。

③Stream 操做是延遲執行的。這意味着他們會等到須要結果的時候才執行

Stream 的操做三個步驟

建立 Stream 一個數據源(如: 集合、數組), 獲取一個流

建立 Stream方式一:經過集合 (Collection)

default Stream<E> stream() : 返回一個順序流 default Stream<E> parallelStream() : 返回一個並行流

建立 Stream方式二:經過數組

Arrays 的靜態方法 stream() 能夠獲取數組流: static <T> Stream<T> stream(T[] array): 返回一個流

建立 Stream方式三:經過Stream的of()

public static<T> Stream<T> of(T... values) : 返回一個流

 

中間操做 一箇中間操做鏈,對數據源的數據進行處理

終止操做(終端操做) 一個終止操做,執行中間操做鏈,併產生結果

 

並行流與串行流

並行流就是把一個內容分紅多個數據塊,並用不一樣的線程分別處理每一個數據塊的流。

Java 8 中將並行進行了優化,咱們能夠很容易的對數據進行並 行操做。 Stream API 能夠聲明性地經過 parallel()sequential() 在並行流與順序流之間進行切換。

生成流 在Java 8中,集合接口有兩個方法來生成流:

stream() −爲集合建立串行流。

parallelStream() − 爲集合建立並行流。

public static void main(String[] args) {
    List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
    List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
}

5.3 forEach Stream 提供了新的方法 'forEach' 來迭代流中的每一個數據。如下代碼片斷使用forEach 輸出了10個隨機數:

Random random = new Random();
random.ints().limit(10).forEach(System.out::println);

5.4 map map 方法用於映射每一個元素到對應的結果,如下代碼片斷使用 map 輸出了元素對應的平方數:

List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
// 獲取對應的平方數
List<Integer> squaresList = numbers.stream().map(i -> i * i).distinct().collect(Collectors.toList());

5.5 filter filter 方法用於經過設置條件過濾出元素。如下代碼片斷使用filter 方法過濾出空字符串:

List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// 獲取空字符串的數量
int count = (int) strings.stream().filter(string -> string.isEmpty()).count();

5.6 limit limit 方法用於獲取指定數量的流。如下代碼片斷使用 limit 方法打印出 10 條數據:

Random random = new Random();
random.ints().limit(10).forEach(System.out::println);

5.7 sorted sorted 方法用於對流進行排序。如下代碼片斷使用 sorted 方法對輸出的 10 個隨機數進行排序:

Random random = new Random();
random.ints().limit(10).sorted().forEach(System.out::println);

5.8 並行(parallel)程序 parallelStream 是流並行處理程序的代替方法。如下實例咱們使用parallelStream 來輸出空字符串的數量:

List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
// 獲取空字符串的數量
int count = (int) strings.parallelStream().filter(string -> string.isEmpty()).count();
咱們能夠很容易的在順序運行和並行直接切換。

5.9 Collectors Collectors 類實現了不少歸約操做,例如將流轉換成集合和聚合元素。Collectors可用於返回列表或字符串:

List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
System.out.println("篩選列表: " + filtered);
String mergedString = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(", "));
System.out.println("合併字符串: " + mergedString);

5.10 統計 另外,一些產生統計結果的收集器也很是有用。它們主要用於int、double、long等基本類型上,它們能夠用來產生相似以下的統計結果。

List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
IntSummaryStatistics stats = numbers.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("列表中最大的數 : " + stats.getMax());
System.out.println("列表中最小的數 : " + stats.getMin());
System.out.println("全部數之和 : " + stats.getSum());
System.out.println("平均數 : " + stats.getAverage());

5.11 Stream 完整實例 將如下代碼放入Java8Tester.java 文件中:

Java8Tester.java文件

public class Java8Tester {
    public static void main(String args[]) {
        System.out.println("使用 Java 7: ");
        // 計算空字符串
        List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
        System.out.println("列表: " + strings);
        long count = getCountEmptyStringUsingJava7(strings);
        System.out.println("空字符數量爲: " + count);
        count = getCountLength3UsingJava7(strings);
        System.out.println("字符串長度爲 3 的數量爲: " + count);
        // 刪除空字符串
        List<String> filtered = deleteEmptyStringsUsingJava7(strings);
        System.out.println("篩選後的列表: " + filtered);
        // 刪除空字符串,並使用逗號把它們合併起來
        String mergedString = getMergedStringUsingJava7(strings, ", ");
        System.out.println("合併字符串: " + mergedString);
        List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
        // 獲取列表元素平方數
        List<Integer> squaresList = getSquares(numbers);
        System.out.println("平方數列表: " + squaresList);
        List<Integer> integers = Arrays.asList(1, 2, 13, 4, 15, 6, 17, 8, 19);
        System.out.println("列表: " + integers);
        System.out.println("列表中最大的數 : " + getMax(integers));
        System.out.println("列表中最小的數 : " + getMin(integers));
        System.out.println("全部數之和 : " + getSum(integers));
        System.out.println("平均數 : " + getAverage(integers));
        System.out.println("隨機數: ");
        // 輸出10個隨機數
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            System.out.println(random.nextInt());
        }
        System.out.println("使用 Java 8: ");
        System.out.println("列表: " + strings);
        count = strings.stream().filter(string -> string.isEmpty()).count();
        System.out.println("空字符串數量爲: " + count);
        count = strings.stream().filter(string -> string.length() == 3).count();
        System.out.println("字符串長度爲 3 的數量爲: " + count);
        filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
        System.out.println("篩選後的列表: " + filtered);
        mergedString = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(", "));
        System.out.println("合併字符串: " + mergedString);
        squaresList = numbers.stream().map(i -> i * i).distinct().collect(Collectors.toList());
        System.out.println("Squares List: " + squaresList);
        System.out.println("列表: " + integers);
        IntSummaryStatistics stats = integers.stream().mapToInt((x) -> x).summaryStatistics();
        System.out.println("列表中最大的數 : " + stats.getMax());
        System.out.println("列表中最小的數 : " + stats.getMin());
        System.out.println("全部數之和 : " + stats.getSum());
        System.out.println("平均數 : " + stats.getAverage());
        System.out.println("隨機數: ");
        random.ints().limit(10).sorted().forEach(System.out::println);
        // 並行處理
        count = strings.parallelStream().filter(string -> string.isEmpty()).count();
        System.out.println("空字符串的數量爲: " + count);
    }
private static int getCountEmptyStringUsingJava7(List<String> strings) {
    int count = 0;
    for (String string : strings) {
        if (string.isEmpty()) {
            count++;
        }
    }
    return count;
}

private static int getCountLength3UsingJava7(List<String> strings) {
    int count = 0;
    for (String string : strings) {
        if (string.length() == 3) {
            count++;
        }
    }
    return count;
}

private static List<String> deleteEmptyStringsUsingJava7(List<String> strings) {
    List<String> filteredList = new ArrayList<String>();
    for (String string : strings) {
        if (!string.isEmpty()) {
            filteredList.add(string);
        }
    }
    return filteredList;
}

private static String getMergedStringUsingJava7(List<String> strings, String separator) {
    StringBuilder stringBuilder = new StringBuilder();
    for (String string : strings) {
        if (!string.isEmpty()) {
            stringBuilder.append(string);
            stringBuilder.append(separator);
        }
    }
    String mergedString = stringBuilder.toString();
    return mergedString.substring(0, mergedString.length() - 2);
}

private static List<Integer> getSquares(List<Integer> numbers) {
    List<Integer> squaresList = new ArrayList<Integer>();
    for (Integer number : numbers) {
        Integer square = new Integer(number.intValue() * number.intValue());
        if (!squaresList.contains(square)) {
            squaresList.add(square);
        }
    }
    return squaresList;
}

private static int getMax(List<Integer> numbers) {
    int max = numbers.get(0);
    for (int i = 1; i < numbers.size(); i++) {
        Integer number = numbers.get(i);
        if (number.intValue() > max) {
            max = number.intValue();
        }
    }
    return max;
}

private static int getMin(List<Integer> numbers) {
    int min = numbers.get(0);
    for (int i = 1; i < numbers.size(); i++) {
        Integer number = numbers.get(i);
        if (number.intValue() < min) {
            min = number.intValue();
        }
    }
    return min;
}

private static int getSum(List numbers) {
    int sum = (int) (numbers.get(0));
    for (int i = 1; i < numbers.size(); i++) {
        sum += (int) numbers.get(i);
    }
    return sum;
}

private static int getAverage(List<Integer> numbers) {
    return getSum(numbers) / numbers.size();
}
}
執行以上腳本,輸出結果爲:

使用Java7:
列表:[abc,, bc, efg, abcd,, jkl]
空字符數量爲:2
字符串長度爲3的數量爲:3
篩選後的列表:[abc, bc, efg, abcd, jkl]
合併字符串: abc, bc, efg, abcd, jkl
平方數列表:[9,4,49,25]
列表:[1,2,13,4,15,6,17,8,19]
列表中最大的數:19
列表中最小的數:1
全部數之和:85
平均數:9
隨機數:
-393170844
-963842252
447036679
-1043163142
-881079698
221586850
-1101570113
576190039
-1045184578
1647841045
使用Java8:
列表:[abc,, bc, efg, abcd,, jkl]
空字符串數量爲:2
字符串長度爲3的數量爲:3
篩選後的列表:[abc, bc, efg, abcd, jkl]
合併字符串: abc, bc, efg, abcd, jkl
SquaresList:[9,4,49,25]
列表:[1,2,13,4,15,6,17,8,19]
列表中最大的數:19
列表中最小的數:1
全部數之和:85
平均數:9.444444444444445
隨機數:
-1743813696
-1301974944
-1299484995
-779981186
136544902
555792023
1243315896
1264920849
1472077135
1706423674

空字符串的數量爲:2

 

Date/Time API (JSR 310)

在舊版的Java 中,日期時間API 存在諸多問題,其中有:

· 非線程安全 − java.util.Date 是非線程安全的,全部的日期類都是可變的,這是Java日期類最大的問題之一。

· 設計不好 − Java的日期/時間類的定義並不一致,在java.util和java.sql的包中都有日期類,此外用於格式化和解析的類在java.text包中定義。java.util.Date同時包含日期和時間,而java.sql.Date僅包含日期,將其歸入java.sql包並不合理。另外這兩個類都有相同的名字,這自己就是一個很是糟糕的設計。

· 時區處理麻煩 − 日期類並不提供國際化,沒有時區支持,所以Java引入了java.util.Calendar和java.util.TimeZone類,但他們一樣存在上述全部的問題。

Java 8 在 java.time 包下提供了不少新的 API。如下爲兩個比較重要的 API:

· Local(本地) − 簡化了日期時間的處理,沒有時區的問題。

· Zone(時區) − 經過制定的時區處理日期時間。

新的java.time包涵蓋了全部處理日期(localDateTime),時間,日期/時間,時區(zone),時刻(instants),過程(Duration)與時鐘(clock)的操做。

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 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的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。

相關文章
相關標籤/搜索