前言
2014年,Oracle發佈了Java8新版本。這對java來說是一個里程碑式的版本。他最主要的改進就是增加了函數式編程的功能(爲了解決java程序總是冗長的問題),或許會感到奇怪,函數式編程和併發似乎沒什麼關係,但是java中與併發相關的API的實現,卻是以函數式編程的範式來實現的。所以爲了更好的理解這些功能,需要先學習下函數式編程。
java8的函數式編程一些特點
函數作爲一等公民
我理解的,一句話來總結就是函數可以作爲另一個函數的參數或者返回值吧!。如下面一段Javascript代碼:
function f1(){
var n=1;
function f2(){
alert(n);
}
return f2;
}
var result= f1();
result();//1
上述代碼f1返回了f2並賦值給了result,此時result就是一個函數,指向f2,調用result就會打印n的值
無副作用
函數的副作用指的是函數除了返回值外,還修改了函數外部的狀態,不如修改了一個全局變量。可以想象,這樣當系統出現故障時,我們很難判斷問題是由哪一個函數引起的,對調試和追蹤是沒有好處的。如果函數都是顯式函數,那麼函數的執行顯然不會收到外部或者全局信息的干擾,有利於調試和追蹤。所謂顯式函數是指函數與外界交換數據的唯一渠道就是參數和返回值,顯式函數不會讀取或者修改函數的外部狀態。而與之對應的隱式函數還會讀取外部信息或者修改外部信息。
然而 實際上完全的無副作用是不可能是實現的,系統總是需要獲取或者修改外部信息的,同時,模塊之間的交互也極有可能是通過共享變量進行的。因此大多數的函數式編程都允許副作用的存在,如Clojure等,但是與面向對象相比,這種函數調用的副作用,在函數式編程裏,需要進行一些有效的限制。
申明式的(Declarative)
函數式編程是申明式的的編程方式。相對與命令式而言,命令式的·程序者總是喜歡使用大量的 可變對象和指令。我們總是喜歡創建對象和變量,並且修改他們的狀態或值,或者喜歡提供一系列指令要求程序執行。而在函數式編程中,那些細節的指令將會更好的被函數庫所封裝,我們只需要提出我們的要求,申明我們的用意即可。
下面,我們來看下兩種方式的打印數組的代碼:
//命令式
public static void imperative(){
int [] array={1,2,3,4};
for(int i=0;i<array.length;i++){
System.out.println("array[i]");
}
}
申明式
public static void declarative(){
int [] array={1,2,3,4};
Arrays.stream(array).foreach(System.out::println);
}
不變的對象
在函數式編程中幾乎所有的對象都不會被輕易的i修改。下面我麼看個例子來裂解:
static int [] ar={1,2,3};
Arrays.stream(ar).map((x)->x=x+1).foreach(System.out::println);
System.out.println();
Arrays.stream(ar).foreach(System.out::println);
代碼第二行看起來對數組每個元素進行了加一操作,但通過最後一行打印元素時會發現,數組成員並沒有發生變化。
在使用函數式編程的時候,這種狀態幾乎是一種常態,幾乎所有的對象都拒絕被修改。這非常類似與不變模式。
易於並行
由於對象都處於不變狀態,因此函數式編程更加易於並行。不需要同步,也沒有鎖機制,其性能也會比較好。
更少的代碼
不難理解,函數式編程的範式更加緊湊而且簡潔。
函數式編程的基礎
FunctionalInterface註解
java8提出了函數式接口的定義。簡單來說,函數式接口,就是之定義了單一抽象方法的接口。如下面的定義:
@FunctionalInterface
public static interface IntHandler{
void handle(int i);
}
註釋@FunctionalInterface表示這是一個函數式接口,該註釋與@Override註釋類似,不管該方法是否標註了該註釋,只要滿足重載或者函數式接口的定義,編譯器就會把它看做重載方法或者函數式接口。
需要注意的是,並不是函數式接口只能有一個方法,首先接口運行存在實例方法,其次,任何被java.lang.Object實現的方法,都不能視爲抽象方法。
比如下面 也是一個標準的函數式接口:
@FunctionalInterface
public static interface IntHandler{
void handle(int i);
boolean equals(Object obj);
}
接口默認方法
在java8之前,接口只能包含抽象方法。java8之後,接口還可以包含若干個實例方法,是的java8有了類似多繼承的能力。一個對象實例,將擁有來自不同接口的實例方法。
lambda表達式
lambda表達式可以說是函數式編程的核心。lambda表達式就是匿名函數,他是一段沒有函數名的函數體,可以直接作爲參數傳遞給調用者。
下面看一段lambda表達式的使用:
List<Integer> numbers=Arrays.aslist (1,2,3,4);
numbers.forEach((Integer value) -> System.out.println(value));
上述代碼遍歷的輸出列表的元素。
和匿名對象一樣,lambda表達式也可以訪問外部的局部變量,如下:
final int num=2;
Function<Integer,Integer> stringConverter = (from) -> from*num;
//num++;
System.out.println(stringConverter.apply(3));
上述代碼可以編譯通過,輸出結果6,與匿名內部對象一樣,在這種情況外部的num變量必須申明爲final,才能保證lambda表達式合法的訪問它。
但是對lambda表達式而言,即使去掉final,程序依然會編譯通過。但是我們就不能修改num的值了,java8會自動將lambda表達式中用到的變量視爲final。
方法引用
方法引用是java8中提出的用來簡化lambda表達式的一種手段。它通過類名和方法名來定位一個靜態方法和實例方法。
方法引用在java8中使用的非常靈活。總的來說,分爲以下幾種:
- 靜態方法引用:ClassName::methodName
- 實例上的實例方法引用:instanceReference::methodName
- 超類上的實例方法引用: super::methodName
- 類型上的實例方法引用: ClassName::methodName
- 構造方法引用: Class::new
- 數組構造方法引用: TypeName[]::new
總結來說 一般::前面表示類名或者實例名,後半部分表示方法名,構造函數用new表示。
應該容易理解,此處就不在用例子說明了。
java函數式編程的簡單實踐
下面我們以一個java8流的例子。來簡單顯示java函數式編程:
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.function.IntConsumer;
public class Funtionpr {
static int [] arr= {1,2,3,4};
/**
* @param args
*/
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
// for (int i:arr) {
// System.out.println(i);
// }
// Arrays.stream(arr).forEach(new IntConsumer() {
// @Override
// public void accept(int value) {
// System.out.println(value);
// }
// });
Arrays.stream(arr).forEach(( x)->System.out.println(x)
);
}
}