綜述
下面以一個常見的需求為例,分析Java 8的函數式編程與常規的面向對象式編程的不同之處。函數式編程和面向對象式編程最根本的不同之處在於,在面向對象的世界,函數功能不能獨立於數據而存在,一個函數功能必須存在於一個包含數據的對象中,服務於特定的數據。也就是說,在面向對象時,對象是編程的最小單元,而一個對象將數據和作用於該數據的函數功能打包成一個整體,數據和函數是不可分割的一部分。此時函數只能為該數據服務,而該數據一般也只能使用定義於其上的函數。而這種函數與數據的結合其實是加深了數據和功能的耦合性。對於編程來說,緊耦合意味着不靈活和低泛化能力。
面向對象的編程中的諸多設計原則如面向接口編程原則,擴展大於繼承的原則等都是為了降低對象間的耦合程度而努力。但面向對象編程是有原罪的,它從出生的那一刻起就將數據和函數綁定到一起。這一點決定了面向對象編程不可能做到徹底的解耦。而函數式編程的目的是將解偶進行到底。他要徹底的解耦。他要做的就是將數據和函數分開,函數是函數,數據是數據,沒必要非得在一起。在函數式編程的世界,函數和數據是對等的位置,都作為最小的單元而出現。我的數據可以選擇A函數來提供服務,也可以選擇B函數來提供服務。我的函數不僅可以給C數據提供服務,也可以給D數據提供服務。我可以按我的需求來隨意組合數據和函數,打破在面向對象式編程的世界中數據與函數綁定規則,擁抱更自由的世界。
在java上談論函數式編程對有些人來說是一件可笑的事情,因為java從本質上說面向對象的,你不管如何給一只猴子化妝,他也變不成人。但是對有些不怎么挑剔的觀眾來說,只要化妝的技術足夠高超,就可以把猴子當作人來看。他們(包括我)認為,通過對java現有技術的組合,可以實現對函數式編程的一些我們喜歡的特性的模擬,享受函數式編程的快樂和便利。基於此,java8推出Function包,可以一定程度上讓我們感受函數式編程的快樂。關於Function包的使用,可以參看Java 8 Consumer、Supplier、Predicate、Function。
言歸正傳,進入今天的主題。我們通過一個常見的需求,來在討論函數式編程和面向對象式編程的思想下,分別會有什么樣的應對方法。
需求案例
傳入一個Long型的id List, 將其轉換成加上特定前綴后綴的id key。如id為1對應的id key為 Number_01_cache_key
面向對象編程
面向對象編程,就需要我們把數據(id List)和對數據的操作(convert函數的邏輯)放到同一個對象中去, 因此像下面這樣的操作是典型的面向對象的編程過程:
DetailReader_Object_way.java
package ObejectProgramming;
import java.util.ArrayList;
import java.util.List;
/**
* @author longxingjian
* Created on 2020-01-14
*/
public class DetailReader_Object_way {
private List<Long> ids;
public DetailReader_Object_way(List<Long> ids){
this.ids = ids;
}
public List<String> convert(){
String format = "Number_%s_CACHE_KEY";
List<String> convertedCacheKeys = new ArrayList<>();
for(Long id:ids)
convertedCacheKeys.add(String.format(format,id));
return convertedCacheKeys;
}
}
如上將對id的轉換邏輯寫到convert操作中,這里面的轉換邏輯只為本類的ids數據服務,且本類的ids數據只能使用convert操作中定義的轉化邏輯。一旦我們的轉換邏輯發生了變更,我們也必須修改業務代碼。
下面是測試函數:
ObjectProgrammingTest.java
package ObejectProgramming;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
/**
* @author longxingjian
* Created on 2020-01-14
*/
public class ObjectProgrammingTest {
@Test
public void objectProgrammingTest(){
List<Long> ids = new ArrayList<>();
ids.add(1L);
ids.add(2L);
DetailReader_Object_way detailReader = new DetailReader_Object_way(ids);
List<String> cacheKeys = detailReader.convert();
for (String s : cacheKeys) {
System.out.println(s);
}
}
}
結果:

函數式編程
函數式編程的目標是解藕數據和函數服務。在java中對象是提供服務的最小單元,因此一個折衷的辦法是,我們定義某一種服務,它只提供類似函數的單一服務,並且以對象傳遞的形式發送給另一個對象使用,好像我們把這個函數服務單獨發送過去了一樣,至於接收者怎么使用,那是他們的事情了。
對於函數式的服務, 在java中由於語言的要求必須以一個對象的形式提供,且我們要求它提供單一的服務,只做一件事,這個功能由java8的Funciton包為我們實現。Function對象的公開方法只有一個apply方法,這個對象作為這種服務的容器來使用。話不多說,來看代碼:
DetailReader_Functional_Way.java
package FunctionalProgramming;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @author longxingjian <longxingjian@kuaishou.com>
* Created on 2020-01-14
*/
public class DetailReader_Functional_Way {
private Function<Long, String> cacheKeyFormatter;
private List<Long> ids;
public DetailReader_Functional_Way(Function<Long, String> function, List<Long> ids) {
this.cacheKeyFormatter = function;
this.ids = ids;
}
public List<String> convert() {
List<String> convertedCacheKeys = ids.stream().map(cacheKeyFormatter).collect(Collectors.toList());
return convertedCacheKeys;
}
}
這個對象雖然也有一個convert方法,但是我們看到這個方法沒有任何的轉換邏輯,它的轉換邏輯是通過調用cacheKeyFormatter來實現的,而cacheKeyFormatter正是與數據ids一起定義在開頭的一個成員,他是一個Function對象,從泛型中可以看出這個函數式對象提供的函數功能是輸入Long型輸出String型。
里看看這個對象是怎么傳入的:
CacheKeyTest.java
package FunctionalProgramming;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
/**
* @author longxingjian
* Created on 2020-01-14
*/
public class CacheKeyTest {
@Test
public void cacheKeyTest() {
List<Long> ids = new ArrayList<>();
ids.add(1L);
ids.add(2L);
DetailReader_Functional_Way detailReader = new DetailReader_Functional_Way(CacheKeyProvider::getCacheKey, ids);
List<String> convertedKeys = detailReader.convert();
for (String key : convertedKeys) {
System.out.println(key);
}
}
}
注意看DetailReader_Functional_Way detailReader = new DetailReader_Functional_Way(CacheKeyProvider::getCacheKey, ids);
這里我們通過函數指針為Function對象傳入了CacheKeyProvider 的成員函數getCacheKey。
來看看CacheKeyProvider對象是如何定義的:
CacheKeyProvider.java
package FunctionalProgramming;
/**
* @author longxingjian
* Created on 2020-01-14
*/
public class CacheKeyProvider {
final static String CACHE_KEY = "Number_%S_CACHE_KEY";
public static String getCacheKey(Long id) {
return String.format(CACHE_KEY, id);
}
}
運行CacheKeyTest.java:

梳理上述過程,我們發現java函數式編程的結果是通過提供一種特殊的對象--只提供一種服務的對象來實現對函數式編程的模擬。這種方式雖然也是在對象層面的解耦,但也實實在在的將數據與操作分離開來。在上面的實現中,我們可以通過在CacheKeyTest.java傳入不同的函數指針而使用不同的函數邏輯,而不需要去修改DetailReader_Functional_Way.java中的業務代碼。在DetailReader_Functional_Way.java中,處理邏輯和被處理的數據是放在對等的位置的。
