Java8 新特性學習

1、接口中的默認方法

Java8中允許接口中包含具有具體實現的方法,這種方法被稱為「默認方法」,使用default關鍵字修飾。
如:

public interface MyInterface {

    String notDefault();

    default String testDefault() {
        return "Hello Default Method";
    }
}12345678

該接口的實現類中只需要實現抽象方法即可,默認方法可以直接使用。

MyInterface myInterface = new MyInterface() {
    @Override
    public String notDefault() {
        return "這不是一個default方法,實現類需要重寫這個方法";
    }
};

System.out.println(myInterface.notDefault());
System.out.println(myInterface.testDefault());123456789

接口中默認方法的「類優先」原則:

若一個接口中定義了一個默認方法,而另外一個父類或接口中又定義了一個同名的方法時

  • 選擇父類中的方法。如果一個父類中提供了具體的實現,那麼接口中具有相同名稱和參數的默認方法會被忽略。 測試代碼:
public  class MySuperClazz {

    public String testDefault() {
        return "This is default method";
    }
}

public interface MyInterface {

    default String testDefault() {
        return "Hello Default Method";
    }
}

public class MyClazz extends MySuperClazz implements MyInterface{

}
MyClazz myClazz = new MyClazz();
System.out.println(myClazz.testDefault());12345678910111213141516171819

輸出結果:

This is default method1
  • 接口衝突,如果一個父接口提供一個默認方法,而另一個接口也提供了一個具有相同名稱和參數列表的方法(不管方法是否是默認方法),那麼實現類必須覆蓋該方法來解決衝突。

測試代碼:

public class MyClazz /*extends MySuperClazz */implements MyInterface,MyInterface2{

    @Override
    public String testDefault() {
        return "測試兩個接口中同名方法";
    }
}

public interface MyInterface2 {

    default String testDefault() {
        return "Hello Default Method2";
    }
}

public interface MyInterface {

    default String testDefault() {
        return "Hello Default Method";
    }
}123456789101112131415161718192021

測試結果:

測試兩個接口中同名方法1

Java8的接口中也可以聲明靜態方法

public interface MyInterface {

    static void testStatic() {
        System.out.println("靜態方法");
    }
}123456

2、Lambda表達式

上面提到的代碼

MyInterface myInterface = new MyInterface() {
    @Override
    public String notDefault() {
        return "這不是一個default方法,實現類需要重寫這個方法";
    }
};123456

可以通過Lambda表達式改寫成如下方式

MyInterface myInterface = () -> "這不是一個default方法,實現類需要重寫這個方法";1

這種形式,便是java8提供的更加簡潔的方式,即Lambda表達式。相比於jdk1.7之前的代碼,使用Lambda表達式更加的簡短易讀。

Lambda表達式是一種「語法糖」,需要函數式接口的支持。
Lambda表達式的參數列表的數據類型可以省略不寫,因為JVM編譯器可以通過上下文推斷出數據類型,即:類型推斷。Java的編譯器能夠自動識別參數的類型,所以可以省略類型不寫。

Lambda表達式可以分為兩部分:
左側:指定了Lambda表達式需要的所有參數
右側:指定了Lambda體,即lambda表達式要執行的功能

Lambda表達式有以下幾種格式:

  • 語法格式一:無參數,無返回值
格式:
    () -> System.out.println("Hello Lambda");
例:
    Runnable r1 = () -> System.out.println("Hello Lambda");1234
  • 語法格式二:有一個參數,並且無返回值
格式:
     (x) -> System.out.println(x)
     只有一個參數小括號可以不寫
     x -> System.out.println(x)
例:
    Consumer<String> con = (x) -> System.out.println(x);
    Consumer<String> con2 = x -> System.out.println(x);
    con.accept("zhw");
    con2.accept("nb");123456789
  • 語法格式三:有兩個參數以上,並且lambda體中有多條語句,並且有返回值,如果有多條語句必須使用{}
例:
    Comparator<Integer> com = (x,y) -> {
        System.out.println("語法格式三");
        return Integer.compare(x,y);
    };12345
  • 語法格式四:lambda體重只有一條語句,並有返回值,大括號可以省略,return可以省略
Comparator<Integer> com = (x,y) -> Integer.compare(x,y);1

Lambda表達式的範圍:

對於lambdab表達式外部的變量,其訪問權限的粒度與匿名對象的方式非常類似。
你能夠訪問局部對應的外部區域的局部final變量,以及成員變量和靜態變量。
注意:final關鍵字可以省略,但是編譯的時候依然被隱式的當成final變量來處理。

方法引用:

若Lambda體中的內容有方法已經實現了,我們可以使用「方法引用」,(可以理解為方法引用是Lambda表達式的另一種表現形式)

注意:
1、 Lambda體中調用方法的參數列表與返回值類型,要與函數式接口中抽象方法的參數列表和返回值類型保持一致
2、若lambda參數列表中的第一個參數是 實例方法的調用者,而第二個參數是實例方法的參數時,可以使用ClassName::method的形式

方法引用的三種語法格式:

  • 對象::實例方法名
  • 類::靜態方法名
  • 類::實例方法名

構造器引用

格式:

  • ClassName::new

注意:
需要調用的構造器的參數列表要與函數式接口中抽象方法的參數保持一致

3、函數式接口

上文提到的Lambda表達式需要函數式接口的支持那什麼是函數式接口呢?所謂的函數式接口就是必須有且僅有一個抽象方法聲明的接口,Lambda表達式必須要與抽象方法的聲明相匹配,由於默認方法不是抽象的,所以可以在函數式接口中任意添加默認方法。

定義函數式接口的時候可以使用@FunctionalInterface註解,編譯器會判斷定義的接口是否滿足函數是接口的條件,如果不滿足,編譯器會拋出異常。

@FunctionalInterface
public interface MyFunction<T,R> {

    R get(T t1,T t2);
}12345

Java8內置的函數式接口:

Consumers

Consumer<T> : 消費型接口,Consumer代表了在一個輸入參數上需要進行的操作。
    void accept(T t); 

Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));12345

Suppliers

Supplier<T>  : 供給型接口,口產生一個給定類型的結果。與Function不同的是,Supplier沒有輸入參數。

Supplier<Person> personSupplier = Person::new;
personSupplier.get(); // new Person1234

Functions

Function<T,R>  : 函數型接口,接受一個參數(T),並返回單一的結果(R)。默認方法可以將多個函數串在一起(compse, andThen)。

Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123"); // "123"12345

Predicates

Predicate<T>  : 段言型接口,是一個布爾類型的函數,該函數只有一個參數。Predicate接口包含了多種默認方法,用於處理複雜的邏輯動詞(and, or,negate)

Predicate<String> predicate = (s) -> s.length() > 0;
predicate.test("foo"); // true
predicate.negate().test("foo"); // false
Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();123456789

4、Stream 數據流

數據流表示元素的序列,並支持不同種類的操作來執行元素上的計算。數據流是單體(Monad),並且在Java8函數式編程中起到重要作用。

流(Stream)到底是什麼:是數據渠道,用於操作數據源(集合,數組等)所生成的元素序列「集合講的是數據,流講的是計算」。

注意:

  1. Stream自己不會存儲元素
  2. Stream不會改變源對象。相反,他們會返回一個持有結果的新的Stream
  3. Stream操作是延遲執行的。這意味着他們會等到需要結果的時候才會執行。

Stream操作的三個步驟:

  • 創建數據流
  • 銜接操作
  • 終止操作

Stream數據流的操作過程圖:
這裡寫圖片描述

數據流操作要麼是銜接操作(創建過程也可以看成銜接操作),要麼是終止操作,數據流的操作大多數是無狀態的而且是無干擾的,即當一個函數不修改數據流的底層數據源時,它就是無干擾的;當一個函數的操作不依賴於外部作用域中的任何操作過程中的可變的變量或狀態時,它就是無狀態的。

創建數據流:

創建Stream的五種方式:
1、 可以通過Collection系列集合中的stream()或者parallelStream()方法獲取串行流或者並行流。

List<String> list = new ArrayList<>();
Stream<String> stream = list.stream();12

2、 通過Arrays中的靜態方法stream()方法獲取數組流

 Arrays.asList("a1", "a2", "a3").stream();1

3、 通過Stream類中的靜態方法of()方法獲取流,可以從一系列對象引用中創建數據流

Stream<String> stream = Stream.of(「aa」,」bb」,」cc」);1

4、 創建無限流

//迭代
Stream<Integer> stream = Stream.iterate(0,(x) -> x+ 2);
Stream.limit(10).forEach(System.out::println)
//生成
Stream.generate(()->Math.random()).forEach(System.out::println);12345

5、java8提供了一些特殊種類的流,用於處理基本數據類型int,long,double。分別為IntStream,LongStream,DoubleStream

前四種方式創建的數據流稱為「對象數據流」,第五種方式創建的流稱為「基本數據流」,基本數據流和對象數據流用法相似,但有一些不同,基本數據流使用的是特殊的lambda表達式,如:IntFunction而不是Function,IntPredicate而不是Predicate。同時基本數據流還支持一些額外的聚合終止操作sum()和average()等。有些時候需要進行對象數據流和基本數據流的轉換。轉換方式如下:

對象數據流轉換成基本數據流:

mapToInt() 、 mapToLong() 和mapToDouble()

例:
Stream.of("a1", "a2", "a3")
        .map(s -> s.substring(1))
        .mapToInt(Integer::parseInt)
        .max()
        .ifPresent(System.out::println); 12345678

基本數據流轉換成對象數據流:

mapToObj()

例:
IntStream.range(1, 4)
        .mapToObj(i -> "a" + i)
        .forEach(System.out::println);123456

銜接操作:

多個銜接操作可以連接起來形成 一個流水線,除非流水線上出發終止操作,否則銜接操作不會執行任何處理,而在終止操作是一次性全部處理,稱為「惰性求值」或「延遲執行」,延遲性是銜接操作的一個重要特性。

例:
Stream.of("d2", "a2", "b1", "b3", "c")
        .filter(s -> {
            System.out.println("filter: " + s);
            return true;
        });123456

上面這段代碼並不會向控制台打印任何東西,因為並沒有終止操作,銜接操作只有在終止操作調用時才會被執行。

銜接操作主要有以下幾種方式:

篩選與切片

filter—接收Lambda,從六中排除某些元素
limit—截斷流,使其元素不超過給定數量
skip(n)—跳過元素,返回一個扔掉了前n個元素的流。若流中元素不足n個,則返回一個空流。與limit互補
distinct—篩選,通過流所生成元素的hashCode()和equals()去除重複元素

內部迭代:迭代操作有Stream API完成
外部迭代:自己編寫迭代操作1234567

映射

map—接收Lambda,將元素轉換成其他形式或提取信息。接收一個函數操作參數,該函數會應用到每個元素上,並將其映射成一個新的元素。

flatMap—接收一個函數作為參數,將流中的每個值都換成另一個流,然後把所有流鏈接成一個流123

排序

sorted()—自然排序
sorted(Comparator com)—定製排序12

終止操作:

在數據流中,只有執行了終止操作時,銜接操作才會一次性執行。終止操作主要有以下幾種方式:

查找與匹配

allMatch—檢查是否匹配所有元素
anyMatch—檢查是否至少匹配一個元素
noneMatch—檢查是否沒有匹配所有元素
findFirst—返回當前流中的第一個元素
findAny—返迴流中元素的任意元素
count—返迴流中元素的總個數
max—返迴流中的最大值
min—返迴流中的最小值12345678

歸約

ruduce(T identity,BinaryOperator) / ruduce(BinaryOperator) –可以將流中元素反覆結合起來,組合為單一結果。1

收集

collect—將流轉換為其他形式。接收一個Collector接口的實現,用於給Stream中元素做匯總的方法,將流中的元素存放在不同類型的結果中。Java8通過內置的 Collectors 類支持多種內置的收集器。

List<Person> filtered = persons.stream()
                                .filter(p -> p.name.startsWith("P"))
                                .collect(Collectors.toList());
System.out.println(filtered); // [Peter, Pamela]123456

5、並行流與串行流

串行流:在一個線程上執行一個內容

Arrays.asList("a1", "a2", "a3")
        .stream()
        .findFirst()
        .ifPresent(System.out::println);1234

並行流:就是把一個內容分成多個數據快,並用不同的線程分別處理每個數據塊的流。並行流能夠在多個線程上執行操作。主要是為了解決處理大量元素時的性能問題。

Arrays.asList("a1", "a2", "a3")
        .stream()
        .findFirst()
        .parallel()
        .ifPresent(System.out::println);
--------------------------------------------------------------------
Arrays.asList("a1", "a2", "a3")
        .parallelStream()
        .findFirst()
        .ifPresent(System.out::println);12345678910

Stream API 可以聲明性的通過parallel()與sequential()在並行流與串行流之間進行切換。
在一個串行流中使用parallel()切換為並行流,在一個並行流中使用sequential()切換為串行流。

Java8對並行流有了更好的支持
並行流使用公共的 ForkJoinPool ,由 ForkJoinPool.commonPool() 方法提供。底層線程池的大小最大為五個線程 – 取決於CPU的物理核數。

Fork/Join框架:在必要的情況下,講一個大任務,進行拆分(fork)成若干個小任務(拆分到不可拆分時),再將一個個小人物運算的結果進行匯總(join)。
這裡寫圖片描述

採用「工作竊取」算法(work-stealing),Java8之前這種框架使用的並不多,主要是因為這種方法實現起來過於麻煩,而Java8將實現的方法變得非常簡單。

「工作竊取」算法:算法是指某個線程從其他隊列里竊取任務來執行

對於常見的一個大型任務,我們可以把這個大的任務切割成很多個小任務,然後這些小任務會放在不同的隊列中,每一個隊列都有一個相應的的工作執行線程來執行,當一個線程所需要執行的隊列中,任務執行完之後,這個線程就會被閑置,為了提高線程的利用率,這些空閑的線程可以從其他的任務隊列中竊取一些任務,來避免使自身資源浪費,這種在自己線程閑置,同時竊取其他任務隊列中任務執行的算法,就是工作竊取算法

6、Optional類

Optional類是一個容器類,代表一個值存在或不存在,可以避免空指針異常。Optional不是一個函數式接口,而是一個精巧的工具接口,用來方式NullPointException異常。Optional是一個簡單的值容器,這個值可以是null,也可以是non-null,為了不直接返回null,我們在Java 8中就返回一個Optional類,使用Optional類可以避免使用if進行null檢查,這種檢查的過程交由底層自動處理。主要有如下一些方法:

of(T t) : 創建一個Optional實例
empty() : 創建一個空的Optional實例
ofNullable(T t) :  若t不為null,創建Optional實例,否則創建空的Optional實例
isPresent() : 判斷是否包含值
orElse(T t) : 如果調用對象包含值,返回該值,否則返回t
orElseGet(Suppplier s) : 如果調用對象包含值,返回該值,否則返回s獲取的值
map(Function f) : 如果有值對其處理,並返回處理後的Optional,否則返回Optional.empty()
flatMap(Function mapper) : 與map類似,要求返回值必須是Optional12345678

7、JAVA8全新的時間包

Java 8 包含了全新的時間日期API,這些功能都放在了java.time包下。主要有如下一些方法:

LocalDate,LocalTime,LocalDateTime : 時間
Instant : 時間戳(以Unix元年:1970年1月一日 00:00:00到某個時間時間的毫秒值)
Duration : 計算兩個「時間」之間的間隔
Period : 計算兩個「日期」之間的間隔
TemporalAdjust :時間校正器
DateTimeFormatter :格式化時間/日期
ZonedDate,ZonedTime,ZonedDateTime : 時區