1. 程式人生 > >[一] java8 函數語言程式設計入門 什麼是函數語言程式設計 函式介面概念 流和收集器基本概念

[一] java8 函數語言程式設計入門 什麼是函數語言程式設計 函式介面概念 流和收集器基本概念

本文是針對於java8引入函數語言程式設計概念以及stream流相關的一些簡單介紹

什麼是函數語言程式設計?

java程式設計師第一反應可能會理解成類的成員方法一類的東西 此處並不是這個含義,更接近是數學上的函式 看一下百度百科中關於函式的說明 函式的定義:
給定一個數集A,假設其中的元素為x。 現對A中的元素x施加對應法則f,記作f(x),得到另一數集B。假設B中的元素為y。 則y與x之間的等量關係可以用y=f(x)表示。 我們把這個關係式就叫函式關係式,簡稱函式。 函式概念含有三個要素:定義域A、值域C和對應法則f。 其中核心是對應法則f,它是函式關係的本質特徵。
image_5b790d18_713d_thumb[1] 對應於程式設計來說,當然不是完全的數學上的函式定義 所謂函數語言程式設計我們可以理解為:
通過對應法則f(x) 對指定的x 進行處理,對映成另外一個值 而且不會對x本身產生變動
所謂不會對x產生變動,你可以理解為無副作用,或者說副作用不會被察覺 副作用你可以理解為解題過程中對資料的修改 說起來好像很囉嗦,但是如果有人告訴你 通過sin(x) 計算後, x的值被改變了,你不會覺得異常奇怪麼 函數語言程式設計就是把函式的一些特性應用於程式語言之中 注意:
函數語言程式設計不是某一種語言,也不是某個API
他是一種方法論,是一種程式設計正規化,有它自有的一些特性和規定 語言中引入函數語言程式設計,也就是用語言本身定義了函數語言程式設計的一些特性和規定
函數語言程式設計最重要的基礎是λ演算,而且λ演算的函式可以接受函式當作輸入(引數)和輸出(返回值)。 它一套用於研究函式定義、函式應用和遞迴的形式系統 我們只需要知道λ演算是一種形式的匿名函式,並且接收一個引數作為輸入 (可以柯里化進行引數轉換多引數函式轉換為單引數) 有興趣的可以去探究下λ演算

函數語言程式設計有下列特性

閉包和高階函式 閉包就是能夠讀取其他函式內部變數的函式,是個不太好理解的概念
此處我們僅僅理解成 函式可以當做值進行傳遞並且可以使用變數儲存 是"第一等公民" 一等公民或者一等型別的含義就是指可以跟值一樣的地位,作為引數傳遞或者儲存於變數中  高階函式是指可以用另一個函式(間接地,用一個表示式) 作為其輸入引數,比如 f(g(x))=g(x)+1 的形式 惰性計算 表示式不是在繫結到變數時立即計算,而是在求值程式需要產生表示式的值時進行計算 你可以理解為流水線上每一個節點都只是做了一系列的設定,並沒有立刻去計算數值 沒有副作用 副作用是指在運算過程中,修改了函式內部區域性變數以外的其他變數的狀態,比如你修改了類成員變數 沒有副作用也就意味著不產生運算以外的其他結果,不修改系統的變數 引用透明性 如果提供同樣的輸入,那麼函式總是返回同樣的結果 也就是說表示式的值不依賴於可以改變值的全域性狀態,比如不依賴成員變數的值

為什麼要使用函數語言程式設計?

關注做什麼 更接近於自然語言 任務分解後你的解題思路將是如何呼叫各個不同的函式,要做什麼 不在關注於函式內部的細節本身去思考怎麼做
假設有這麼一組Student學生型別的List資料,學生有性別男女
如果在Java程式碼中,你會如何解題? 虛擬碼:
List<Student> 男List ;
for(int i=0;i<studentList.length;i++){//studentList 為學生列表,其中有男有女
if(studentList[i].性別 == 男){
    男List.add(List[i])
}
你迴圈遍歷列表,找到符合條件的學生,然後把他加入另外一個列表,這可能是一種常見的解題思路
假設有個Student 學生表,每條記錄都有一個性別字段值為男女
如果是在資料庫中查詢呢,一種可能的解法是這樣子的
select
*
from
student
where
sex='';
他們的主要區別是什麼? 一個最直觀的差別就是: java程式碼中是你自己去迴圈資料項,你自己處理每一項資料,找出符合你要求的資料 SQL查詢中,你只是傳入通知條件where  sex='男';  ,資料庫在自己內部進行了迴圈,幫我們找出來符合要求的資料 這就是外部迴圈和內部迴圈,這是一種思維方式的轉變 外部迴圈,需要程式設計師自己去關注每一個數據項 內部迴圈,程式設計師只需要關注結果 內部迴圈以及函式呼叫 也將我們從如何做中解放出來,讓我們不再關注資料項迴圈的細節本身,僅僅關注於此次呼叫的結果 不管是什麼方式進行思考程式設計,你都會將你的任務進行分解 劃分為更小的子任務 但是不同的是: 如何做的思維下,你還需要思考在每個子任務中,每一個細節是怎麼處理的,比如迴圈中進行條件判斷 這其實還是往計算機的思維傾斜的一種思考方式,這是指令式或者命令式的程式設計模式 做什麼的思維下,你不在關注每個子任務的內部細節,只在乎結果也就是"做什麼" 每個子任務內部的細節是函式自己內部的事情,這更加符合人的思維習慣 內部迴圈不也是函數語言程式設計的一種表現形式麼 函式本身如同一個黑盒一般,有輸入有輸出,我們不關心內部的實現細節,僅僅在乎輸入和輸出 內部迴圈也是如此,我們告訴他我們想要的結果行為,他返回給我們結果 比如SQL中 where   sex='男';  這就是對我們行為的描述(不要把它理解成篩選條件) 我們將行為像引數一樣傳遞給了資料庫軟體,資料庫執行查詢操作,根據的是我們給定的行為 這就是行為引數化的魅力所在 行為引數化也是一種思維模式,只要能把行為像引數一樣進行傳遞  就是行為引數化 有人可能已經想到了匿名內部類
new Thread(new Runnable() {
@Override
public void run() {
System.out.println();
}
}).start();
的確這也是一種行為引數化,但是顯然,這段程式碼還不夠簡潔純粹,因為方法的外層還套了一層物件 Java8中的行為引數化,傳遞的將是更加純粹的行為,而不再需要藉助一個匿名物件的形式,而且,Lambda表示式不會像內部類一樣生成一個類 傳遞的是方法本身,方法中的程式碼本身 那麼行為引數化,不也就是函數語言程式設計中的閉包特性麼 更加易於併發程式設計 函數語言程式設計的準則是沒有副作用不依賴外部的資料,也不改變外部資料的值。 我們知道執行緒安全的根本在於共享資料,如果沒有任何的資料共享,那麼很多的併發/執行緒安全問題都將迎刃而解 所以說這一特性正好滿足了多核並行程式設計的需求,所以很顯然能夠簡化並行程式的開發 函數語言程式設計程式碼簡潔 函數語言程式設計大量使用函式,減少了程式碼的重複,就如同你呼叫別人的方法一樣不是麼,一行就得到了結果

Java8 對於函數語言程式設計的支援

程式語言把函數語言程式設計的概念引入,也就是使自身支援函數語言程式設計的特性,換句話說也就是 在語言內部可以使用一系列的型別或者關鍵字或者符號組合等進行表示 Java主要涉及這三個核心概念
  • 函式介面(FunctionalInterface)
  • 流(Stream)
  • 收集器(Collector)
函式介面 既然函數語言程式設計要求函式可以是同值一樣的一等公民用於引數化傳遞,那麼必須要有表示函式的型別 先說一下函式式介面的註解 註解@FunctionalInterface   描述了什麼是一個函式式介面
public @interface FunctionalInterface {}
image_5b790d18_4458_thumb[1] 上面的註釋也就相當於是函式式介面的定義: 一個函式式介面只能有一個抽象方法,default方法有實現,所以不是抽象方法  如果一個介面聲明瞭一個覆蓋Object  public公有方法的抽象方法,也不算是抽象方法 所以說:函式式介面,有且僅有一個抽象方法,覆蓋Object的public方法不計算在內(如果是覆蓋Object的protected那麼會計數的)  比如 java.lang.Runnable、java.util.Comparator是典型的函式式介面
函式介面是一個介面,有且只有一個唯一的抽象方法
介面上定義了函式的型別引數 抽象方法的方法簽名限定了函式(函式式介面的抽象方法的簽名稱為函式描述符) 所以說一個函式介面,只能描述一種型別的函式 比如 Function<T, R>      這個函式介面 image_5b790d18_5ba3_thumb[1] 他表示形如
R function(T){     ....   return R }
他的型別引數是T  R,呼叫方法apply 輸入為T   輸出為R 作用為轉換一個物件為不同型別的物件 所有這種形式的函式都是這個函式介面型別 比如
public static void main(String[] args){
Function<String,Boolean> function = (String x)->x.equals("true");
System.out.println(function.apply("1"));
System.out.println(function.apply("true"));
}
image_5b790d18_906_thumb[1] 至此,Java中已經有了用於表示函式的型別了,也就是可以定義一個函式或者返回一個函式,或者把函式當做一個引數值進行傳遞了 以賦值運算子的形式來類比的話就是 比如 int i = 1; 等號左邊的型別已經有了就是函式介面 但是右邊,也就是行為引數化這個行為到底如何表示呢?也即是上面的1 的位置 在java中可以使用
  • Lambda表示式((String x)->x.equals("true"))
  • 方法引用(String::length)
兩種形式進行表示 比如
public static void main(String[] args){
Function<String,Integer> function = String::length;
System.out.println(function.apply("1"));
System.out.println(function.apply("true"));
}
image_5b790d18_316d_thumb[1] 既然每一種函式型別都需要存在指定形式的函式介面,想要使用Lambda-匿名函式或者方法引用,自然需要定義函式介面 函式型別的說法可能不太準確,函式式介面的抽象方法的簽名稱為函式描述符 其實說的也都還是方法簽名  方法簽名唯一的標識了一個函式 Java8 也已經給我們預置了一些常用的函式介面型別   已經定義一套能夠描述常見函式描述符的函式介面 比如上面提到的 function  就是其中一種 另外還有其他一些,後面再說,我們已經可以在Java中表示一個函式,並且對函式進行呼叫

流,流動,流水,java中早就已經有了IO流,形象的表達了資料在程式中的處理與流動 Java8中的Stream流則更傾向於流水線的含義 每個節點有各自獨立的功能目的,根據你的目的(做什麼),將各個獨立的功能目的節點拼接成一整個的完整的流水線 資料在此流水線上進行加工處理,最終得出結果 通過告知Stream "做什麼" 來進行資料操作和處理 你不在需要關注內部的細節,Stream通過內部迭代進行資料項的篩選查詢,找到符合條件的資料  流(Stream)是Java8對函數語言程式設計的重要支撐。大部分函式式工具都圍繞Stream展開 也可以說Stream類是Java8 關於函數語言程式設計定義的一些列函式集合 由此可以看得出來,Stream的重要性   想要使用Java進行函數語言程式設計,僅僅使用Lambda表示式是不夠的,必須有足夠的函式,Lambda表示式只有跟stream一起使用才能顯示其真實的威力
集合是一種資料結構用於儲存資料 Stream不是一種資料結構,是對於資料的一種新的檢視,用於資料的計算,提供了一系列的API用於呼叫
概括的說 Stream就是函數語言程式設計中程式語言提供出來的庫方法集合,而引數基本上都是函式 所以才說,Lambda表示式只有跟stream一起使用才能顯示其真實的威力 image_5b790d18_3c54_thumb[1] 常用的Stream呼叫流程 image_5b790d18_5d3e_thumb[1] 1.獲得Stream 想要使用Stream的一些特性,顯然你必須把你的資料集轉換生成為Stream,這沒有Stream何談使用? 2.設定行為型別 也就是操作型別 這句話有些模糊不清,其實就是你需要設定想幹什麼 到底是篩選資料?轉換資料?求和還是怎樣? 你可以類比為SQL查詢中到底是SELECT 還是UPDATE 或者DELETE? 這就是行為的型別 為了更快理解的話,你可以片面的理解為呼叫Stream類的方法 我們舉例說明 比如你經常讓同學幫你買東西 買東西就是行為型別,是去買東西,既不是幫你開車也不是陪你看電影 這就是行為的型別 Stream中有一系列的API可以幫助我們達到這個目的 比如 filter  map等等 3. 確定行為引數 也就是操作內容 行為引數也就是基於已經設定的行為型別下,你具體要以什麼樣子的行為去執行 你篩選資料篩選什麼樣子的資料? 轉換資料,轉換為什麼形式? 類比為SQL查詢中就是查詢條件,查詢  男生?查詢 女生? 這就是行為的具體方式 還是剛才的例子,你經常讓同學幫你買東西,那到底買什麼?買礦泉水還是買麵包?這就是確定行為引數 Java8中使用方法引用或者Lambda-匿名函式  或者方法引用來表示行為引數 4.行為的屬性 既然是流水線式的工作方式,那麼當前的工作結束後或許結束了或許是進入到流水線的下一環節 當然最終他肯定還是會結束掉的 這就又涉及到繫結行為方法的屬性種類  到底是中間的操作(可以繼續傳遞給流水線下一步)  還是結束的終端操作 中間操作的返回結果還是一個Stream  你仍舊可以對他進行上述類似的過程 終端操作則一般會將流進行收集整理成指定的資料結構 這基本上是一個常用的Stream使用流程  流程處理雖然很簡單,但是強大之處在於中間操作處理後仍舊是流 這就意味著你可以按照需要進行無數的變換組合以達到你想要的效果

收集器

Stream結合Lambda表示式可以對於資料進行各種各樣的操作 但是Stream 終歸是Stream ,它並不是一種資料結構,不管經過了多少處理,他終歸是再次返回到程式碼中具體的其他資料型別中 把Stream類比做資料項處理的流水線的話 中間操作就是流水線上的一個個的功能操作節點 而收集器就是在某些結束操作中用於將資料進行轉換的工具 在Java中關於收集器有幾個關鍵的概念 1. Stream中的collect 方法是收集器的呼叫者
<R, A> R collect(Collector<? super T, A, R> collector);
2. Collector 介面 定義了收集器
public interface Collector<T, A, R> {
3. 收集器工廠Collectors  用於預置一些收集器
public final class Collectors
比如   .....collect(Collectors.toList());  就是把一個處理後的流轉換為List 總結: Java8 構建了三個主要概念,函式介面,流,收集器 有了函式介面  函式擁有了型別也就是可以像值一樣作為引數進行傳遞,作為返回值,或者使用變數進行表示 使用Lambda-匿名函式或者方法引用來表示行為引數  也就是函式的值 Stream是Java8 提供的函數語言程式設計的"庫函式" 預定了一些常用的操作模式,通過Lambda表示式結合使用 收集器用於把Stream處理後的資料進行打包整理成你需要的資料結構