天天看點

Java8——快速入門手冊(學習筆記)

github博文傳送門

Java8特性學習筆記

  Java8中新增了許多的新特性,在這裡本人研究學習了幾個較為常用的特性,在這裡與大家進行分享。(這裡推薦深入了解Java 8用于了解基礎知識)本文分為以下幾個章節:

  • Lambda 表達式
  • 方法引用
  • 預設方法
  • 函數接口
  • Function
  • Stream
  • Optional API
  • Date Time API

Lambda表達式

Lambda 表達式,也可稱為閉包。Lambda 允許把函數作為一個方法的參數(函數作為參數傳遞進方法中)。使用 Lambda 表達式可以使代碼變的更加簡潔緊湊。 Lambda表達式可以替代以前廣泛使用的内部匿名類,各種回調,比如事件響應器、傳入Thread類的Runnable等。

Lambda文法

lambda 表達式的文法格式如下:

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

Lambda表達式的特征

  • 類型聲明(可選):可以不需要聲明參數類型,編譯器會識别參數值。
  • 參數圓括号(可選):在單個參數時可以不使用括号,多個參數時必須使用。
  • 大括号和return關鍵字(可選):如果隻有一個表達式,則可以省略大括号和return關鍵字,編譯器會自動的傳回值;相對的,在使用大括号的情況下,則必須指明傳回值。

Lambda表達式例子

這裡以常用的list排序功能為例:

private static List<People> peopleList = new ArrayList<People>();
{
	peopleList.add(new People("a",17));
	peopleList.add(new People("b",16));
	peopleList.add(new People("c",19));
	peopleList.add(new People("d",15));
}

@Test
public void testLambda(){
    System.out.println("排序前:"+peopleList);

    //第一種,傳統匿名Compartor接口排序
    Collections.sort(peopleList, new Comparator<People>() {
        @Override
        public int compare(People o1, People o2) {
            return o1.getAge().compareTo(o2.getAge());
        }
    });
    System.out.println("匿名接口方法——排序後:"+peopleList);

    //第二種,使用Lambda表達式來代替匿名接口方法
    //1.聲明式,不使用大括号,隻可以寫單條語句
    Collections.sort(peopleList,(People a,People b)->a.getAge().compareTo(b.getAge()));
    System.out.println("Lambda表達式1、排序:"+peopleList);;
    //2.不聲明式,使用大括号,可以寫多條語句
    Collections.sort(peopleList,(a,b)->{
        System.out.print("——————————————");
        return a.getAge().compareTo(b.getAge());
    });
    System.out.println();
    System.out.println("Lambda表達式2、排序:"+peopleList);

    //第三種,使用Lambda表達式調用類的靜态方法
    Collections.sort(peopleList,(a,b)->People.sortByName(a,b));
    System.out.println("Lambda表達式調用靜态方法:"+peopleList);

    //第四種,使用Lambda表達式調用類的執行個體方法
    Collections.sort(peopleList,(a,b)->new People().sortByAge(a,b));
    System.out.println("Lambda表達式調用執行個體方法:"+peopleList);
}
           

對應的運作結果:

Java8——快速入門手冊(學習筆記)

(注意:在Lambda表達式中隻能對final的對象進行操作,聲明的對象也為final)

有的朋友應該已經觀察到了,Lambda 表達式與C中的函數指針,JavaScript的匿名function均有些相似。其實,Lambda表達式本質上是一個匿名的方法,隻不過它的目标類型必須是“函數接口(functional interface)”,這是Java8引入的新概念,在接下來會進行更加詳細的介紹。

方法引用

在一些Lambda中可能隻是單純的調用方法,比如前例中的三、四,在這種情況下,就可以使用方法引用的方式來提高可讀性。

方法引用的種類

  • 類靜态方法引用

    Class::staticMethodName

  • 某個對象的方法引用

    instance::instanceMethodName

  • 特定類的任意對象的方法引用:

    Class::method

  • 構造方法引用:

    Class::new

方法引用的例子

@Test
public void testMethodReference() {
//第一種,引用類的靜态方法
Collections.sort(peopleList, People::sortByName);
System.out.println("引用類的靜态方法:" + peopleList);

//第二種,引用類的執行個體方法
Collections.sort(peopleList, new People()::sortByAge);
System.out.println("引用類的執行個體方法:" + peopleList);

//第三種,特定類的方法調用()
Integer[] a = new Integer[]{3, 1, 2, 4, 6, 5};
Arrays.sort(a, Integer::compare);
System.out.println("特定類的方法引用:" + Arrays.toString(a));

//第四種,引用類的構造器
Car car = Car.create(Car::new);
System.out.println("引用類的構造器:" + car);
}
           
public static Car create(Supplier<Car> supplier){
	return supplier.get();
}
           

預設方法

在Java8之前的時代,為已存在接口增加一個通用的實作是十分困難的,接口一旦釋出之後就等于定型,如果這時在接口内增加一個方法,那麼就會破壞所有實作接口的對象。

預設方法(之前被稱為 虛拟擴充方法 或 守護方法)的目标即是解決這個問題,使得接口在釋出之後仍能被逐漸演化。

預設方法(defalut)

public interface vehicle {
   default void print(){
      System.out.println("我是一輛車!");
   }
}
           

靜态方法(static)

public interface vehicle {
   static void blowHorn() {
      System.out.println("按喇叭!!!");
   }
}
           

注:靜态方法與預設方法均可以有多個,預設方法可以被覆寫。

函數接口

“函數接口(functional interface)”,就是除去預設方法以及繼承的抽象方法,隻有顯式聲明一個抽象方法的接口。它使用@FunctionalInterface注解在類上進行标注,也可以省略,Java會自動識别。接下來介紹一些常見的函數接口:

java.util.function.Predicate

該接口包含方法boolean test(T t),該接口一般用于條件的檢測,内部包含三個預設方法:and、or、negate、,即與或非,用于各式的條件判斷。例:

Predicate<Integer> predicate = x -> x > 3;

predicate.test(10);//true
predicate.negate().test(10);//false
predicate.or(x -> x < 1).and(x -> x > -1).negate().test(-1);//true
           

注意:在這裡與或非的判斷順序是從左到右的,調用的順序會影響結果。

java.util.Comparator

Comparator是Java中的經典接口,在排序中較為常用。Java8在此之上添加了一些新的預設方法,來豐富該接口的功能。例:

Integer[] a = new Integer[]{3, 1, 2, 4, 6, 5};
Comparator<Integer> comparator = Integer::compare;

Arrays.sort(a, comparator);
System.out.println("升序:" + Arrays.toString(a));

Arrays.sort(a,comparator.reversed());
System.out.println("降序:"+Arrays.toString(a));
           

結果

升序:[1, 2, 3, 4, 5, 6]
降序:[6, 5, 4, 3, 2, 1]
           

java.util.function.Supplier

該類隻包含方法:

T get();

Supplier接口是在1.8中新出現的函數接口,用于支援函數式程式設計。它用于傳回一個任意泛型的執行個體對象,與工廠的功能類似。

java.util.function.Consumer

該接口表示一個接受單個輸入參數并且沒有傳回值的操作。不像其他函數式接口,Consumer接口期望執行修改内容的操作。例如 ,我們需要一個批量修改People的方法,利用Predicate和Consumer就可以這麼寫

在People内增加updateMany方法:

public static List updateMany(List<People> peopleList, Predicate<People> predicate, Consumer<People> consumer) {
    for (int i = 0; i < peopleList.size(); i++) {
        if (predicate.test(peopleList.get(i))) {
            consumer.accept(peopleList.get(i));
        }
    }
    return peopleList;
}
           

調用:

//批量修改,将age<18的對象的age改為18
People.updateMany(peopleList,
        p -> p.getAge() < 18,
        p -> p.setAge(18));
System.out.println("修改後的結果:" + peopleList);
           

通過這種方式,可以将内部的判斷邏輯與修改代碼放至外部調用,而将for、if等語句封裝至内部,提高代碼的可讀性。

其他的還有一些函數接口,如Runnable,InvocationHandler等,在這裡就不闡述了。有興趣的大家可以自行查詢資料。Stream、Function、Optional也是函數接口,将在下面進行詳細介紹。

Function

說明

Java8提供的java.util.function包的核心函數接口有4個。

  • 函數型T ->R,完成參數類型T向結果類型R的轉換和資料處理。核心函數接口Function
  • 判斷型T ->boolean,核心函數接口Predicate
  • 消費型T ->void,核心函數接口Consumer
  • 供給型void->T,核心函數接口Supplier

Function接口是為Java8提供了函數式程式設計的基礎,apply方法與Consumer的accept方法功能類似,但是提供了傳回及類型轉換的可能,功能更加強大;再通過andThen與compose方法可以使Function組成Function功能鍊,進行多級資料處理及轉換。

主要方法

  • R apply(T t) – 将Function對象應用到輸入的參數上,然後傳回計算結果。
  • default Function<T,V> andThen(Function<? super R,? extends V> after) 傳回一個先執行目前函數對象apply方法再執行after函數對象apply方法的函數對象。
  • default Function<T,V> compose(Function<? super V,? extends T> before)傳回一個先執行before函數對象apply方法再執行目前函數對象apply方法的函數對象。
  • static Function<T,T> identity() 傳回一個執行了apply()方法之後隻會傳回輸入參數的函數對象。

方法詳解

apply:
R apply(T t); 
           

接收類型:T

傳回類型:R

類型轉換:T→R

Function接口的核心方法,可以執行任意的操作,且具有傳回值。接收一個T類型的對象,在經過處理後,傳回一個R類型的對象。主要功能為類型轉換及資料處理。

compose:
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
    Objects.requireNonNull(before);
    return (V v) -> apply(before.apply(v));
}
           

接收類型:Function<? super V, ? extends T>

傳回類型:Function<V, R>

類型轉換:(V+)→(T-)→T→R

apply執行順序:before→this

此處“V+”指代“? super V”,表示包含V在内的V的任意父類;"T-"指代“? extends T”,表示包含T在内的T的任意子類。compose方法傳回一個Function<V,R>,這個Function先執行before的apply方法,将V+類型的資料轉換為T-類型,再将T-作為參數傳遞給this的apply方法,将T類型轉換為R類型。

通過compose方法,可以在某個Function執行之前插入一個Function執行。由于傳回類型依舊為Function,可以重複調用compose方法形成方法鍊。

andThen:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t));
}
           

接收類型:Function<? super R, ? extends V>

傳回類型:Function<T, V>

類型轉換:T→R→(R+)→(V-)

apply執行順序:this→after

此處“R+”指代“? super R”,表示包含R在内的R的任意父類;"V-"指代“? extends V”,表示包含V在内的V的任意子類。andThen方法傳回一個Function<T,V>,這個Function先執行this的apply方法,将T類型的資料轉換為R類型,再将R作為參數傳遞給after的apply方法,将R+類型轉換為V-類型。

通過andThen方法,可以在某個Function執行之後插入一個Function執行。由于傳回類型依舊為Function,可以重複調用andThen方法形成方法鍊。

identity:
static <T> Function<T, T> identity() {
	return t -> t;
}
           

接收類型:無

傳回類型:Function<T, T>

類型轉換:T→T

該方法的說明是:傳回一個函數,它總是傳回輸入參數。調用該方法可以得到一個傳回輸入參數的Funtion,這個Function就可以單純的用來做資料處理,而不用類型轉換。

Stream

Java8中提供了Stream API,即流式處理。可以通過将List、Set、Array等對象轉換成流進行操作。Stream内的流操作分為兩種:中間操作和最終操作,中間操作會傳回一個全新的Stream對象,意味着你的操作不會影響最初的流;最終操作會将流進行轉換或者操作,傳回非Stream的對象。Stream可以替代傳統的循環操作,從線程上差別,Stream分為串行(Stream)和并行(parallelStream),關于Stream的性能分析可以檢視這篇文章《Stream性能分析》。下面來看下Strea内的一些方法:

中間操作

  • distinct
    Stream<T> distinct();
               
    去除Stream中重複的對象,并傳回一個流。(使用對象的equals方法)
  • skip
    Stream<T> skip(long n);
               
    跳過Stream中的前n個對象,将其他對象傳回一個Stream。如果n超過了Stream中對象的個數,則會傳回一個空的Stream。
  • limit
    Stream<T> limit(long maxSize);
               
    截取Stream的前maxSize個對象,并形成一個新Stream。
  • filter
    Stream<T> filter(Predicate<? super T> predicate);
               
    根據給定的predicate來過濾對象,傳回滿足條件的對象構成的Stream。
  • map
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
               
    通過給定的mapper,将T類型的流轉換為R類型的Stream。
  • flatMap
    <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
               

    flatMap也是将Stream進行轉換,flatMap與map的差別在于 flatMap是将一個Stream中的每個值都轉成一個個Stream,然後再将這些流扁平化成為一個Stream。

    例(轉自:Java8 新特性之流式資料處理):

    假設我們有一個字元串數組String[] strs = {"java8", "is", "easy", "to", "use"};,我們希望輸出構成這一數組的所有非重複字元,那麼我們可能首先會想到如下實作:

    List<String[]> distinctStrs = Arrays.stream(strs)
                        .map(str -> str.split(""))  // 映射成為Stream<String[]>
                        .distinct()
                        .collect(Collectors.toList());
               
    在執行map操作以後,我們得到是一個包含多個字元串(構成一個字元串的字元數組)的流,此時執行distinct操作是基于在這些字元串數組之間的對比,是以達不到我們希望的目的,此時的輸出為:
    [j, a, v, a, 8]
    [i, s]
    [e, a, s, y]
    [t, o]
    [u, s, e]
               
    distinct隻有對于一個包含多個字元的流進行操作才能達到我們的目的,即對Stream進行操作。此時flatMap就可以達到我們的目的:
    List<String> distinctStrs = Arrays.stream(strs)
                                    .map(str -> str.split(""))  // 映射成為Stream<String[]>
                                    .flatMap(Arrays::stream)  // 扁平化為Stream<String>
                                    .distinct()
                                    .collect(Collectors.toList());
               
    flatMap将由map映射得到的Stream<String[]>,轉換成由各個字元串數組映射成的流Stream,再将這些小的流扁平化成為一個由所有字元串構成的大流Steam,進而能夠達到我們的目的。
  • sorted
    Stream<T> sorted();
    Stream<T> sorted(Comparator<? super T> comparator);
               
    sorted方法可以對Stream進行排序。排序的對象必須實作Comparable,如果沒實作會抛出ClassCastException;不提供comparator時,則會調用compareTo方法。
  • peek
    Stream<T> peek(Consumer<? super T> action);
               

    對流中的每個對象執行提供的action操作。

    在Stack中,peek用于檢視一個對象。在流中也是一樣,用于在流循環時,根據給定的action進行檢視對象。雖然可以進行元素修改操作,但不建議。

  • 綜合例:
    Integer[] a = new Integer[]{3, 1, 2, 5, 11, 4, 6, 5, 3, 1};
    	List<Integer> aList = Arrays.stream(a)
        .distinct()
        .skip(1)
        .filter((e) -> e < 6)
        .peek(e -> System.out.println("循環1次"))
        .limit(4)
        .sorted()
        .collect(Collectors.toList());
    System.out.println(aList);
               
    輸出:
    循環1次
    循環1次
    循環1次
    循環1次
    [1, 2, 4, 5]
               

最終操作

  • 聚合
    • max & min
    Optional<T> min(Comparator<? super T> comparator);
    Optional<T> max(Comparator<? super T> comparator);
               
    根據給定的comparator傳回Stream中的max或min。
    • count
    long count();
               
    傳回Stream中對象的個數。
  • 比對
    • anyMatch & allMatch & noneMatch
    boolean anyMatch(Predicate<? super T> predicate);
    boolean allMatch(Predicate<? super T> predicate);
    boolean noneMatch(Predicate<? super T> predicate);
               
    根據給定的predicate判斷Stream是否比對條件。
    • collect
    <R, A> R collect(Collector<? super T, A, R> collector);
               

    根據給定的collector對Stream中的元素進行操作,傳回複雜資料結構的對象。用于将Stream中的對象轉換成我們想要的結構,如list、map、set等。

    前例中就使用collect(Collectors.toList())将Stream中的對象轉換成List。

    • reduce
    Optional<T> reduce(BinaryOperator<T> accumulator);
    T reduce(T identity, BinaryOperator<T> accumulator);
               

    如果我們不知希望單純的傳回List這樣的類型,而是希望将整個Stream經過一些操作後,規約成一個對象傳回,就可以用到規約操作。reduce方法有兩個參數,其中accumulator代表着規約的操作,即用何種的方式進行參數化處理;identity則是accumulator的辨別值(具體用處暫不明)。

    例:求和

    Integer[] a = new Integer[]{3, 1, 2, 5, 11, 4, 6, 5, 3, 1};
    int sum = Arrays.stream(a)
    		.distinct()
    		.filter((e) -> e < 6)
    		.reduce(0, (x, y) -> x + y);//或.reduce(0, Integer::sum);
    System.out.println(sum);//15
               
    • toArray
    Object[] toArray();
               
    将Stream中的對象傳回成一個Object數組。
    • forEach
    void forEach(Consumer<? super T> action);
               
    顧名思義,對Stream中每個元素進行action操作,與peek類似,但forEach是一個最終操作,一般在結束時檢視對象使用。
    • findFirst & findAny
    Optional<T> findFirst();
    Optional<T> findAny();
               

    findFirst可以傳回Stream中第一個對象,并将它封裝在Optional中。

    findAny則不是傳回第一個對象,而是任意一個對象。在順序Stream中findFirst和findAny的結果是一緻的,但在并行Stream中,findFirst存在着限制,故在并行Stream中需要使用findAny(findAny源碼注釋中寫的是some element?)。同樣将對象封裝在Optional中。

Optional API

在java8之前的程式設計中,我們總是需要進行if(obj=null)來防止NullPointException,而在java8後,提供了Optional類,它一方面用于防止NullPotinException的判斷,另一方面則為流式程式設計與函數式變成提供了更好的支援;Optional是一個包含對象的容器,它可以包含null值。在Optional類中封裝了許多的方法,來讓我們更好的處理我們的代碼。接下來看看Optional中幾個常用的方法:

  • empty & of & ofNullable
    public static <T> Optional<T> empty(){...}
    public static <T> Optional<T> of(T value) {return new Optional<T>(value);}
    public static <T> Optional<T> ofNullable(T value){return value == null ? empty() : of(value);}
               
    首先,Optioanl的構造方法是私有的,隻能通過以上三個靜态方法來擷取Optional的執行個體。empty方法會傳回Optional中的常量EMPTY對象,一般在compare時使用,注意這裡的EMPTY是單例的而且為常量;一般我們需要構造一個Optional,使用of或ofNullable方法,of方法會将我們的傳值構造一個新的Optional傳回,而ofNullable則在接收null時傳回EMPTY執行個體。
  • isPresent
    public boolean isPresent() {return value != null;}
    public void ifPresent(Consumer<? super T> consumer) {if (value != null)consumer.accept(value);}
               
    isPresent方法用于判斷Optional包含的value是否為null,第一種方法傳回一個boolean;第二種方法則根據判斷,為null則什麼都不執行,不為null則執行一個consumer操作。
  • map & flatMap
    public<U> Optional<U> map(Function<? super T, ? extends U> mapper){...}
    public<U> Optional<U> flatMap(Function<? super T, Optional<U> mapper){...}
               
    map與flatMap與Stream中用法與功能大緻相同,都是轉換及合并轉換,不再贅述。
  • get
    public T get() {...}
               
    get方法用于擷取value。需要注意的是,如果value為null,則會抛出NoSuchElementException。
  • filter
    public Optional<T> filter(Predicate<? super T> predicate) {...}
               
    filter方法也是擷取value,它可以傳入一個predicate,用于判斷value是否滿足條件。如果value為null,則會傳回this;如果predicate.test為true,則傳回this,否則會傳回EMPTY。
  • orElse & orElseGet & orElseGet
    public T orElse(T other) {return value != null ? value : other;}
    public T orElseGet(Supplier<? extends T> other) {return value != null ? value : other.get();}
    public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X{...} 
               
    這三個方法都用于擷取value,同時可以在valuenull的情況下做出不同的操作。orElse可以傳入一個other,當valuenull時則傳回null;orElseGet則是使用Supplier,為null時調用get方法;orElseThrow則是接收一個Supplier包含某種異常的exceptionSupplier,為null時則會調用get方法抛出一個異常。

Date Time API

Java8使用新的日期時間API覆寫舊的日期時間API的,處理了以下缺點。

  • 線程安全 - java.util.Date不是線程安全的,是以開發者必須在使用日期處理并發性問題。新的日期時間API是不可變的,并且沒有setter方法。
  • 設計問題 - 預設的開始日期為1900年,月的開始月份為0而不是1,沒有統一。不直接使用方法操作日期。新的API提供了這樣操作實用方法。
  • 時區處理困難 - 開發人員必須編寫大量的代碼來處理時區的問題。新的API設計開發為這些特定領域提供了幫助。

JAVA8引入了java.time包,一個新的日期時間API。限于篇幅與精力問題,這裡不對java.time進行過多的介紹,這裡推薦幾篇個人覺得不錯的博文以供研究:

Java 類庫的新特性之日期時間API (Date/Time API )

Java 8 之 java.time 包