Java8函數式編程

最近使用lambda表達式,感受使用起來很是舒服,箭頭函數極大加強了代碼的表達能力。因而決心花點時間深刻地去研究一下java8的函數式。html

1、lambda表達式

先po一個最經典的例子——線程java

public static void main(String[] args) {
  // Java7
  new Thread(new Runnable() {
    @Override
    public void run() {
      for (int i = 0; i < 100; i++) {
        System.out.println(i);
      }
    }
  }).start();

  // Java8
  new Thread(() -> {
    for (int i = 0; i < 100; i++) {
      System.out.println(i);
    }
  }).start();
}
複製代碼

第一次接觸lambda表達式是在建立線程時,比較直觀的感覺就是lambda表達式至關於匿名類的語法糖,emm~,真甜。不過事實上,lambda表達式並非匿名類的語法糖,並且通過一段時間的使用,感受偏偏相反,在使用上匿名類更像是Java中lambda表達式的載體。api

使用場景

下面的一些使用場景均爲我的的一些體會,可能存在不當或遺漏之處。數組

1. 簡化匿名類的編碼

上面的建立線程就是一個很好簡化編碼的例子,此處就再也不重複。多線程

2. 減小沒必要要的方法建立

在Java中,咱們常常會遇到這樣一種場景,某個方法只會在某處使用且內部邏輯也很簡單,在Java8以前咱們一般都會建立一個方法,可是事實上咱們常常會發現這樣寫着寫着,一個類中的方法可能會變得很是龐雜,嚴重影響閱讀體驗,進而影響編碼效率。可是若是使用lambda表達式,那麼這個問題就能夠很容易就解決掉了。oracle

一個簡單的例子,若是咱們須要在一個函數中屢次打印時間。(這個例子可能有些牽強,可是實際上仍是挺常碰見的)app

public class FunctionMain {
    
    public static void main(String[] args) {
        TimeDemo timeDemo = new TimeDemo();
        timeDemo.createTime = System.currentTimeMillis();
        timeDemo.updateTime = System.currentTimeMillis() + 10000;
        outputTimeDemo(timeDemo);
    }

    private static void outputTimeDemo(TimeDemo timeDemo) {
        Function timestampToDate = timestamp -> {
            DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            return df.format(new Date(timestamp));
        };

        System.out.println(timestampToDate.apply(timeDemo.createTime));
        System.out.println(timestampToDate.apply(timeDemo.updateTime));
    }


    interface Function {
        String apply(long timestamp);
    }
}

class TimeDemo {
    long createTime;
    long updateTime;
}
複製代碼

在這段代碼的outputTimeDemo中咱們能夠看到,對於時間戳轉換的內容,咱們並無額外建立一個方法,而是相似於建立了一個變量來表達。不過,這個時候出現了另外一個問題,雖然咱們少建立了一個方法,可是咱們卻多建立了一個接口Function,總有種因小失大的感受, 不過這個問題,咱們在後面的java.util.function包部分能夠找到答案。jvm

3. 事件處理

一個比較常見的例子就是回調。ide

public static void main(String[] args) {
    execute("hello world", () -> System.out.println("callback"));
}

private static void execute(String s, Callback callback) {
    System.out.println(s);
    callback.callback();
}

@FunctionalInterface
interface Callback {
    void callback();
}
複製代碼

在這裏,能夠發現一點小不一樣,就是Callback多了一個註解@FunctionalInterface,這個註解主要用於編譯期檢查,若是咱們的接口不符合函數式接口的要求,那編譯的時候就會報錯。不加也是能夠正常執行的。函數

4. stream中使用

這個在後面的stream中詳解。

java.util.function包

在以前的例子中,咱們發現使用lambda表達式的時候,常常須要定義一些接口用來輔助咱們的編碼,這樣就會使得本應輕量級的lambda表達式又變得重量級。那是否存在解決方案呢?其實Java8自己已經爲咱們提供了一些常見的函數式接口,就在java.util.function包下面。

接口 描述
Function<T,R> 接受一個輸入參數,返回一個結果
Supplier<T> 無參數,返回一個結果
Consumer<T> 接受一個輸入參數,而且不返回任何結果
BiFunction<T,U,R> 接受兩個輸入參數的方法,而且返回一個結果
BiConsumer<T,U> 接受兩個輸入參數的操做,而且不返回任何結果

此處列出最基本的幾個,其餘的都是在這些的基礎上作了一些簡單的封裝,例如IntFunction<R>就是對Function<T,R>的封裝。上面的這些函數式接口已經能夠幫助咱們處理絕大多數場景了,若是有更復雜的狀況,那就得咱們本身定義接口了。不過遺憾的是在java.util.function下沒找到無參數無返回結果的接口,目前我找到的方案就是本身定義一個接口或者直接使用Runnable接口。

使用示例

public static void main(String[] args) {
    Function<Integer, Integer> f = x -> x + 1;
    System.out.println(f.apply(1));

    BiFunction<Integer, Integer, Integer> g = (x, y) -> x + y;
    System.out.println(g.apply(1, 2));
}
複製代碼

lambda表達式和匿名類的區別

lambda表達式雖然使用時和匿名類很類似,可是仍是存在那麼一些區別。

1. this指向不一樣

lambda表達式中使用this指向的是外部的類,而匿名類中使用this則指向的是匿名類自己。

public class FunctionMain {

    private String test = "test-main";

    public static void main(String[] args) {
        new FunctionMain().output();
    }

    private void output() {
        Function f = () -> {
            System.out.println("1:-----------------");
            System.out.println(this);
            System.out.println(this.test);
        };
        f.outputThis();

        new Function() {
            @Override
            public void outputThis() {
                System.out.println("2:-----------------");
                System.out.println(this);
                System.out.println(this.test);
            }
        }.outputThis();
    }

    interface Function {
        String test = "test-function";

        void outputThis();
    }
}
複製代碼

如上面這段代碼,輸出結果以下

image-20190417113153242

因此若是想使用lambda表達式的同時去訪問原類中的變量、方法的是作不到的。

2. 底層實現不一樣

編譯

從編譯結果來看,二者的編譯結果徹底不一樣。

首先是匿名類的方式,代碼以下:

import java.util.function.Function;

public class ClassMain {

    public static void main(String[] args) {
        Function<Integer, Integer> f = new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer integer) {
                return integer + 1;
            }
        };
        System.out.println(f.apply(1));
    }
}
複製代碼

編譯後的結果以下:

image-20190418185503781

能夠看到ClassMain在編譯後生成了兩個class,其中ClassMain$1.class就是匿名類生成的class。


那麼接下來,咱們再來編譯一下lambda版本的。代碼和編譯結果以下:

import java.util.function.Function;

public class FunctionMain {

    public static void main(String[] args) {
        Function<Integer, Integer> f = x -> x + 1;
        System.out.println(f.apply(1));
    }
}
複製代碼

image-20190418185243575

在這裏咱們能夠看到FunctionMain並無生成第二個class文件。

字節碼

更進一步,咱們打開他們的字節碼來尋找更多的細節。首先依然是匿名類的方式

image-20190418190414990

在Code-0這一行,咱們能夠看到匿名類的方式是經過new一個類來實現的。


接下來是lambda表達式生成的字節碼,

image-20190418190726628

在lambda表達式的字節碼中,咱們能夠看到咱們的lambda表達式被編譯成了一個叫作lambda$main$0的靜態方法,接着經過invokedynamic的方式進行了調用。

3. lambda表達式只能替代部分匿名類

lambda表達式想要替代匿名類是有條件的,即這個匿名類實現的接口必須是函數式接口,即只能有一個抽象方法的接口。

性能

因爲沒有實際測試過lambda表達式的性能,且我使用lambda更可能是基於編碼簡潔度的考慮,所以本文就不探討性能相關問題。

關於lambda表達式和匿名類的性能對比能夠參考官方ppt www.oracle.com/technetwork…

2、Stream API

Stream API是Java8對集合類的補充與加強。它主要用來對集合進行各類便利的聚合操做或者批量數據操做。

1. 建立流

在進行流操做的第一步是建立一個流,下面介紹幾種常見的流的建立方式

從集合類建立流

若是已經咱們已經有一個集合對象,那麼咱們能夠直接經過調用其stream()方法獲得對應的流。以下

List<String> list = Arrays.asList("hello", "world", "la");
list.stream();
複製代碼

利用數組建立流

String[] strArray = new String[]{"hello", "world", "la"};
Stream.of(strArray);
複製代碼

利用可變參數建立流

Stream.of("hello", "world", "la");
複製代碼

根據範圍建立數值流

IntStream.range(0, 100);         // 不包含最後一個數
IntStream.rangeClosed(0, 99);    // 包含最後一個數
複製代碼

BufferReader.lines()

對於BufferReader而言,它的lines方法也一樣能夠建立一個流

File file = new File("/Users/cayun/.m2/settings.xml");
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
br.lines().forEach(System.out::println);
br.close();
複製代碼

2. 流操做

在Stream API中,流的操做有兩種:Intermediate和Terminal

Intermediate:一個流能夠後面跟隨零個或多個 intermediate 操做。其目的主要是打開流,作出某種程度的數據映射/過濾,而後返回一個新的流,交給下一個操做使用。這類操做都是惰性化的(lazy),就是說,僅僅調用到這類方法,並無真正開始流的遍歷。 Terminal:一個流只能有一個 terminal 操做,當這個操做執行後,流就被使用「光」了,沒法再被操做。因此這一定是流的最後一個操做。Terminal 操做的執行,纔會真正開始流的遍歷,而且會生成一個結果,或者一個 side effect。

除此之外,還有一種叫作short-circuiting的操做

對於一個 intermediate 操做,若是它接受的是一個無限大(infinite/unbounded)的 Stream,但返回一個有限的新 Stream。 對於一個 terminal 操做,若是它接受的是一個無限大的 Stream,但能在有限的時間計算出結果。

常見的流操做能夠以下歸類:

Intermediate map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered

Terminal forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

Short-circuiting anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

常見的流操做詳解

1. forEach

forEach能夠說是最多見的操做了,甚至對於List等實現了Collection接口的類能夠不建立stream而直接使用forEach。簡單地說,forEach就是遍歷並執行某個操做。

Stream.of("hello", "world", "a", "b").forEach(System.out::println);
複製代碼

2. map

map也一樣是一個很是高頻的流操做,用來將一個集合映射爲另外一個集合。下面代碼展現了將[1,2,3,4]映射爲[1,4,9,16]

IntStream.rangeClosed(1, 4).map(x -> x * x).forEach(System.out::println);
複製代碼

除此以外,還有一個叫作flatMap的操做,這個操做在映射的基礎上又作了一層扁平化處理。這個概念可能比較難理解,那舉個例子,咱們須要將["hello", "world"]轉換成[h,e,l,l,o,w,o,r,l,d],能夠嘗試一下使用map,那你會驚訝地發現,可能結果不是你想象中的那樣。若是不信能夠執行下面這段代碼,就會發現map與flatMap之間的區別了,

Stream.of("hello", "world").map(s -> s.split("")).forEach(System.out::println);
System.out.println("--------------");
Stream.of("hello", "world").flatMap(s -> Stream.of(s.split(""))).forEach(System.out::println);
複製代碼

3. filter

filter則實現了過濾的功能,若是隻須要[1,2,3,4,5]中的奇數,能夠以下,

IntStream.rangeClosed(1, 5).filter(x -> x % 2 == 1).forEach(System.out::println);
複製代碼

4. sorted和distinct

其中sorted表示排序,distinct表示去重,簡單的示例以下:

Integer[] arr = new Integer[]{5, 1, 2, 1, 3, 1, 2, 4};    // 千萬不要用int
Stream.of(arr).sorted().forEach(System.out::println);
Stream.of(arr).distinct().forEach(System.out::println);
Stream.of(arr).distinct().sorted().forEach(System.out::println);
複製代碼

5. collect

在流操做中,咱們每每需求是從一個List獲得另外一個List,而不是直接經過forEach來打印。那麼這個時候就須要使用到collect了。依然是以前的例子,將[1,2,3,4]轉換成[1,4,9,16]。

List<Integer> list1= Stream.of(1, 2, 3, 4).map(x -> x * x).collect(Collectors.toList());
        
// 對於IntStream生成的流須要使用mapToObj而不是map
List<Integer> list2 = IntStream.rangeClosed(1, 4).mapToObj(x -> x * x).collect(Collectors.toList());
複製代碼

3. 補充

並行流

除了普通的stream以外還有parallelStream,區別比較直觀,就是stream是單線程執行,parallelStream爲多線程執行。parallelStream的建立及使用基本與stream相似,

List<Integer> list = Arrays.asList(1, 2, 3, 4);
// 直接建立一個並行流
list.parallelStream().map(x -> x * x).forEach(System.out::println);
// 或者將一個普通流轉換成並行流
list.stream().parallel().map(x -> x * x).forEach(System.out::println);
複製代碼

不過因爲是並行執行,parallelStream並不保證結果順序,一樣因爲這個特性,若是能使用findAny就儘可能不要使用findFirst。

使用parallelStream時須要注意的一點是,多個parallelStream之間默認使用的是同一個線程池,因此IO操做盡可能不要放進parallelStream中,不然會阻塞其餘parallelStream。

3、Optional

Optional的引入是爲了解決空指針異常的問題,事實上在Java8以前,Optional在不少地方已經較爲普遍使用了,例如scala、谷歌的Guava庫等。

在實際生產中咱們常常會遇到以下這種狀況,

public class FunctionMain {

    public static void main(String[] args) {
        Person person = new Person();
        String result = null;
        if (person != null) {
            Address address = person.address;
            if (address != null) {
                Country country = address.country;
                if (country != null) {
                    result = country.name;
                }
            }
        }
        System.out.println(result);
    }
}

class Person {
    Address address;
}

class Address {
    Country country;
}

class Country {
    String name;
}
複製代碼

往往寫到這樣的代碼,做爲編碼者必定都會頭皮發麻,滿心地不想寫,可是卻不得不寫。這個問題若是使用Optional,或許你就能找到你想要的答案了。

Optional的基本操做

1. 建立Optional

Optional.empty();          // 建立一個空Optional
Optional.of(T value);      // 不接受null,會報NullPointerException異常
Optional.ofNullable(T value);     // 能夠接受null
複製代碼

2. 獲取結果

get();                                   // 返回裏面的值,若是值爲null,則拋異常
orElse(T other);                         // 有值則返回值,null則返回other
orElseGet(Supplier other);               // 有值則返回值,null則由提供的lambda表達式生成值
orElseThrow(Supplier exceptionSupplier); // 有值則返回值,null則拋出異常
複製代碼

3. 判斷是否爲空

isPresent();       // 判斷是否爲空
複製代碼

到這裏,咱們可能會開始考慮怎麼用Optional解決引言中的問題了,因而思考半天,寫出了這樣一段代碼,

public static void main(String[] args) {
    Person person = new Person();
    String result = null;
    Optional<Person> per = Optional.ofNullable(person);
    if (per.isPresent()) {
        Optional<Address> address = Optional.ofNullable(per.get().address);
        if (address.isPresent()) {
            Optional<Country> country = Optional.ofNullable(address.get().country);
            if (country.isPresent()) {
                result = Optional.ofNullable(country.get().name).orElse(null);
            }
        }
     }
     System.out.println(result);
}
複製代碼

啊嘞嘞,感受不只沒有使得代碼變得簡單,反而變得更加複雜了。那麼很顯然這並非Optional的正確使用方法。接下來的部分纔是Optional的正確使用方式。

4. 鏈式方法

在Optional中也有相似於Stream API中的鏈式方法map、flatMap、filter、ifPresent。這些方法纔是Optional的精髓。此處以最典型的map做爲例子,能夠看看map的源碼

public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else {
        return Optional.ofNullable(mapper.apply(value));
    }
}
複製代碼

源碼很簡單,能夠看到對於null狀況仍然返回null,不然返回處理結果。那麼此再來思考一下引言的問題,那就能夠很簡單地改寫成以下的寫法,

public static void main(String[] args) {
    Person person = new Person();
    String result = Optional.ofNullable(person)
            .map(per -> per.address)
            .map(address -> address.country)
            .map(country -> country.name).orElse(null);
    System.out.println(result);
}
複製代碼

哇哇哇,相比原先的null寫法真真是舒服太多了。

map與flatMap的區別

這二者的區別,一樣使用一個簡單的例子來解釋一下吧,

public class FunctionMain {

    public static void main(String[] args) {
        Person person = new Person();
        String name = Optional.ofNullable(person).flatMap(p -> p.name).orElse(null);
        System.out.println(name);
    }
}

class Person {
    Optional<String> name;
}
複製代碼

在這裏使用的不是map而是flatMap,稍微觀察一下,能夠發現Person中的name再也不是String類型,而是Optional<String>類型了,若是使用map的話,那map的結果就是Optional<Optional<String>>了,很顯然不是咱們想要的,flatMap就是用來將最終的結果扁平化(簡單地描述,就是消除嵌套)的。

至於filter和ifPresent用法相似,就再也不敘述了。

4、其餘一些函數式概念在Java中的實現

因爲我的目前爲止也只是初探函數式階段,不少地方瞭解也很少,此處只列舉兩個。(注意:下面的部分應用函數與柯里化對應的是scala中的概念,其餘語言中可能略有誤差)

部分應用函數(偏應用函數)

部分應用函數指的是對於一個有n個參數的函數f,可是咱們只提供m個參數給它(m < n),那麼咱們就能夠獲得一個部分應用函數,簡單地描述一下,以下

f(x,y,z) = x + y + z
g(x,y) = f(x,y,3)

在這裏g就是f的一個部分應用函數。

BiFunction<Integer, Integer, Integer> f = (x, y) -> x + y;
Function<Integer, Integer> g = x -> f.apply(1, x);
System.out.println(g.apply(2));
複製代碼

柯里化

柯里化就是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數並且返回結果的新函數的技術。換個描述,以下

f(x, y, z)  \rightarrow  f(x)(y)(z)

Java中對柯里化的實現以下,

Function<Integer, Function<Integer, Integer>> f = x -> y -> x + y;
System.out.println(f.apply(1).apply(2));
複製代碼

由於Java限制,咱們不得不寫成f.apply(1).apply(2)的形式,不過視覺上的體驗與直接寫成f(1)(2)相差就很大了。

柯里化與部分應用函數感受很相像,不過由於我的幾乎未使用過這二者,所以此處就不發表更多看法。

參考

[1] java.util.stream 庫簡介
[2] Java 8 中的 Streams API 詳解
[3] 瞭解、接受和利用Java中的Optional(類)
[4] 維基百科-柯里化
[5] 維基百科-λ演算

相關文章
相關標籤/搜索