您當前的位置:首頁 > 文化

Java函數語言程式設計學習筆記(一)

作者:由 Viscent大千 發表于 文化時間:2018-06-13

1.

此函式非彼函式

在弄清楚什麼是函式程式設計之前,有必要先弄清楚究竟什麼是函式這個問題。在面向物件程式設計中,我們經常將方法稱為函式,那麼方法與函式究竟是否是同一個東西的不同稱呼呢?函數語言程式設計中的“函式”是指數學意義上的函式,不是程式語言中的“函式”。數學上的函式(Function)可以看成一個小機器,給這個機器提供一定的原材料(輸入引數),它就會加工出(輸出)一定的產品(返回值),如圖1所示。

圖1

函式示意圖

Java函數語言程式設計學習筆記(一)

圖片來自:https://en。wikipedia。org/wiki/Function_(mathematics)

方法往往是有副作用(Side Effect)的,即一個方法的呼叫往往意味著物件狀態的變化,而這種變化可能使得用同樣的引數多次呼叫同一個方法,每次呼叫的返回值可能各不相同。而數學上的函式是沒有副作用的(Side Effect Free),因此只要使用同樣的引數呼叫同一個函式,那麼不管呼叫這個函式多少次,每次呼叫的返回值總是相同的。例如,對於一個計算矩形面積的函式f(x,y)=x*y(x和y代表矩形的長和寬),那麼無論呼叫多少次f(3,4),每次的返回值總是12。

從“無副作用”的角度來看,一個方法只要對其的呼叫不會產生副作用,那麼它就是一個函式,否則它就不是函式。例如,String。charAt(int)這個方法就是一個函式。System。currentTimeMillis()這個方法就不是函式,因為對其的每次呼叫的返回值都可能不相同。

2.

函數語言程式設計:函式作為“一等公民”

面向物件程式設計是Java平臺在Java 8之前就已經支援的一種程式設計正規化(Paradigm),Java平臺從Java 8開始支援另外一種程式設計正規化——函數語言程式設計(Functional Programming)。面向物件程式設計中,物件是“一等公民”。所謂“一等公民”就是指可以作為“值”的語言元素。“值”既可以賦值給變數,也可以作為方法呼叫的引數進行傳遞以及作為方法的返回值。例如,我們可以將“1024”賦值給一個int型變數,也可以將其作為方法呼叫的引數,或者作為一個方法的返回值。同樣,物件(確切的說是對物件的引用,相當於指標)也可以作為一個變數(引用型變數)值或者作為一個方法呼叫的引數,或者作為一個方法的返回值(返回一個物件的方法我們通常稱之為工廠方法)。

在面向物件程式設計中,方法(或者函式)並非“一等公民”,因為方法在這裡就不是一個值。而在函數語言程式設計中,函式翻身成為“一等公民”,因此在這裡函式可以作為一個值賦值給一個變數,可以作為函式呼叫的引數(即用函式作為引數去呼叫一個函式),可以作為函式的返回值(即一個函式的返回值是另外一個函式)。

3.

函數語言程式設計的優勢

3.1.

編寫更為簡潔的程式碼

函數語言程式設計使得我們能夠編寫更為簡潔的程式碼。下面看一個例子。假設有個服務(如清單1所示),其啟動動作(即Service。start()方法)比較耗時,因此我們希望用非同步的方式去啟動這個服務,以避免主執行緒被阻塞。

清單1

一個啟動比較耗時的服務

public

class

SomeTimeConsumingService

implements

Service

{

@Override

public

void

start

()

{

// 模擬啟動動作的耗時

try

{

Thread

sleep

5000

);

}

catch

InterruptedException

e

{

}

}

}

為此,我們只需要建立一個專門的執行緒(工作者執行緒),並在該執行緒的run方法中呼叫Service。start()即可,如清單2所示。

清單2

以非同步方式啟動一個耗時服務(面向物件程式設計)

public

class

ServiceStarter

{

public

static

void

main

String

[]

args

{

Thread

serviceStarter

final

SomeTimeConsumingService

service

=

new

SomeTimeConsumingService

();

//建立Runnable例項以呼叫Service。start()

serviceStarter

=

new

Thread

new

Runnable

()

{

@Override

public

void

run

()

{

service

start

();

}

});

//啟動執行緒

serviceStarter

start

();

}

}

清單2中我們建立的匿名Runnable例項的目的僅僅是為了在工作者執行緒中呼叫Service。start()而已。如果我們能夠直接把Service。start()方法作為一個引數傳遞給Thread類的構造器(姑且將構造器看做一種特殊的方法),那麼這裡我們也就省卻了建立Runnable例項,從而使程式碼更加簡潔。在Java 8之後我們的確可以這麼做,如清單3所示。

清單3

以非同步方式啟動一個耗時服務(函數語言程式設計)

public

class

ServiceStarterFP

{

public

static

void

main

String

[]

args

{

Thread

serviceStarter

final

SomeTimeConsumingService

service

=

new

SomeTimeConsumingService

();

serviceStarter

=

new

Thread

service

::

start

);

//直接將start方法作為引數傳遞給構造器

serviceStarter

start

();

}

}

這裡,“service::start”是一個方法引用(Method Reference),它表示物件service的start方法。可見,我們將一個方法作為一個引數傳遞給了Thread類的構造器,這使得清單3相比清單2中相應程式碼要簡潔一些。讀者也許在想,Thread類的構造器的引數的型別是java。lang。Runnable,何以我們能夠將一個方法(作為“值”)作為其引數傳入呢?這樣為何不會出現型別不匹配(相容)的問題呢?後續的筆記中我們會解釋這一點。

從這個例子我們可以看到,Java 8中方法已然躋身作為函式的“一等公民”行列,因此方法可以作為引數進行傳遞。顯然Service。start()並不是真正意義上函式,因為它是有副作用的(至少它有可能是有副作用的,因為它是用來啟動一個服務的)。儘管如此,這個方法還是享受到了函式的“一等公民”待遇(作為值傳遞)。由此可見,Java 8中一個方法可以看做是一個函式,但是Java並不強制這個方法必須是無副作用的。

3.2.

函數語言程式設計為併發程式設計提供了便利

函數語言程式設計為併發程式設計提供了便利。函式是無副作用的,這就意味著這函式必須是無狀態(Stateless)。無狀態是多執行緒程式設計和函數語言程式設計的共同好友。我們知道,在多執行緒程式設計中,如果多個執行緒之間存在共享可變狀態(Shared Mutable State),那麼為了確保執行緒安全我們往往需要藉助鎖,而鎖除了其開銷較大之外還存在可能導致死鎖等問題。相反,如果多個執行緒之間不存在共享狀態(或者僅存在只讀狀態),那麼這些執行緒是可以並行(Parallel)的。這不僅有利於提高系統的併發性,也使得程式碼更為簡單。函式也是類似,既然函式是(必須是)無狀態的,那麼多個執行緒同時執行一個函式的時候也就無需加鎖,這既簡化了程式碼又有利於提供系統的併發性。

4.

作者簡介

黃文海,著有《Java多執行緒程式設計實戰指南(核心篇)》、《Java多執行緒程式設計實戰指南(設計模式篇)》。

5.

參考資料

1.Raoul-Gabriel Urma等。Java 8實戰。人民郵電出版社,2016

2.黃文海。Java多執行緒程式設計實戰指南(核心篇)。電子工業出版社,2017

3.黃文海。Java多執行緒程式設計實戰指南(設計模式篇)。電子工業出版社,2015

標簽: 函式  程式設計  方法  Java  start