深入淺出講解Java8函數式編程
什麼是函數式編程
函數式編程就是一種抽象程度很高的編程范式,純粹的函數式編程語言編寫的函數沒有變量,因此,任意一個函數,隻要輸入是確定的,輸出就是確定的,這種純函數我們稱之為沒有副作用。而允許使用變量的程序設計語言,由於函數內部的變量狀態不確定,同樣的輸入,可能得到不同的輸出,因此,這種函數是有副作用的。 函數式編程的一個特點就是,允許把函數本身作為參數傳入另一個函數,還允許返回一個函數! 函數式編程最早是數學傢阿隆佐·邱奇研究的一套函數變換邏輯,又稱Lambda Calculus(λ-Calculus),所以也經常把函數式編程稱為Lambda計算。
Java8內置瞭一些常用的方法接口FunctionalInterface
這種接口隻定義瞭一個抽象方法,並且用@FunctionalInterface註解標記,如Predicate,Consumer,Function,Supplier,Comparator等等,這些都屬於java.util.function包中
@FunctionalInterface public interface Predicate<T> { boolean test(T t); } @FunctionalInterface public interface Consumer<T> { void accept(T t); } // 省略不貼瞭
他們的特點是定義瞭函數的入參以及返回值,當使用時傳入滿足函數接口定義的表達式,即可通過編譯器檢查,下面會介紹函數接口和對應的4種使用方式
通過一個示例來看看使用函數式和不使用的區別,需求是要有一個函數,傳入一個List<Integer>
,篩選出單數的項,另一個則篩選出雙數的項,先看看不使用函數式的寫法
// 篩選出單數的方法 public static List<Integer> filterSingular(List<Integer> list) { List<Integer> result = new ArrayList<>(); for (Integer item : list) { if (item % 2 != 0) { result.add(item); } } return result; } // 篩選出雙數的方法 public static List<Integer> filterEven(List<Integer> list) { List<Integer> result = new ArrayList<>(); for (Integer item : list) { if (item % 2 == 0) { result.add(item); } } return result; }
定義方法後調用,預期效果輸出[1,3,5,7]和[2,4,5]
List<Integer> targetList = new ArrayList<Integer>() { { this.add(1); this.add(2); this.add(3); this.add(4); this.add(5); this.add(6); this.add(7); } }; List<Integer> singularList = filterSingular(targetList); List<Integer> evenList = filterEven(targetList); System.out.println(singularList); System.out.println(evenList);
但其實這兩個篩選函數,唯一區別隻是判斷條件的不同,這時候就可以將這個條件抽象成一個函數接口去編寫,Predicate接口的test定義文章開頭就有,傳入一個泛型類型,返回一個boolean,改寫下filter的代碼
public static List<Integer> filter(List<Integer> list,Predicate<Integer> predicate) { List<Integer> result = new ArrayList<>(); for (Integer item : list) { if (predicate.test(item)) { result.add(item); } } return result; }
將函數改造成瞭除瞭傳入目前List外,還要傳入一個實現瞭Predicate接口的實例對象,隻需要傳入滿足函數定義入參和出參,就能通過編譯,下面介紹4種這個函數的使用方式
- 使用傳統的匿名內部類,在java8之前隻能這麼操作
List<Integer> singularList = filter(targetList, new Predicate<Integer>() { @Override public boolean test(Integer integer) { return integer % 2 != 0; } }); System.out.println(singularList);
- 使用lambda表達式格式如下()->{},()的是方法列表,->{}是方法體,由於目前隻有一個參數,並且參數類型是可以推斷出來的,所以類型和()可以不寫,方法體隻有一句,{}也可以不寫,不推薦在方法體中寫過長的代碼,應保證可讀性
List<Integer> singularList2 = filter(targetList, integer -> integer % 2 != 0); // 下面是完整寫法 // List<Integer> singularList3 = filter(targetList, (Integer integer) -> { // return integer % 2 != 0; // });
可以使用的原因,lambda表達式滿足傳入Integer返回一個boolean的抽象操作,可以自動轉化為函數接口
- 靜態方法引用,這裡定義瞭一個靜態方法,也可以自動的轉化為函數接口,使用時需要用雙冒號語法
private static boolean integerWithSingular (Integer haha){ return haha % 2 != 0; }
使用靜態方法引用,Cn是所在類名,這種方式對比lambda表達式可以讓可讀性進一步提高,因為方法有名字,可以通過名字去判斷在執行什麼操作,並且更適合編寫更多的邏輯
List<Integer> singularList3 = filter(targetList, Cn::integerWithSingular);
- 實例方法,因為任何實例方法,第一個參數永遠都是一個隱藏的指針this指向當前實例,由於上面例子泛型傳入的是Integer類型,需要改寫下預期才能演示,先聲明一個類,並且有一個實例方法是完成傳入Test類型返回boolean的映射
public class Test { private long id; public Test(long id) { this.id = id; } private boolean integerWithSingular(){ return this.id % 2 != 0; } }
將filter函數的Integer類型全換成Test類型
public static List<Test> filter(List<Test> list, Predicate<Test> predicate) { List<Test> result = new ArrayList<>(); for (Test item : list) { if (predicate.test(item)) { result.add(item); } } return result; }
下面的調用中,傳入類名::實例方法名
實現的效果是等價的
ArrayList<Test> targetList = new ArrayList<Test>() { { this.add(new Test(1)); this.add(new Test(2)); } }; filter(targetList,Test::integerWithSingular);
任何隻包含一個抽象方法的接口都可以被自動轉換成函數接口,自己定義的接口沒有標註@FunctionalInterface標註也可以
用的比較多的函數接口
- Consumer 輸入一個對象,輸出是空的,相當於消費掉傳入的對象,ArrayList的forEach方法使用瞭Consumer
// ArrayList的forEach方法源碼 @Override public void forEach(Consumer<? super E> action) { Objects.requireNonNull(action); final int expectedModCount = modCount; @SuppressWarnings("unchecked") final E[] elementData = (E[]) this.elementData; final int size = this.size; for (int i=0; modCount == expectedModCount && i < size; i++) { action.accept(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } }
- Function更加接近於函數的定義,用於將一個類型變換成另一個類型,如數學中的函數把X變成Y,函數接口的定義如下,還是以剛才編寫的Test類為理解,再編寫一個map方法
public static String map(Test test, Function<Test, String> function) { return function.apply(test); }
隻要滿足傳入一個Test類型,返回一個String類型的東西都可以被自動轉換
map(new Test(1),test -> "name"); // 如果Test類型還有一個屬性為String的name和對應的getter方法,可以寫成下面這種實例方法引用 // map(new Test(2), Test::getName);
- Supplier和Consumer是對立者,Consumer消費,Supplier提供,從虛空中提供一個東西
public static Object create(Supplier<Object> supplier){ return supplier.get(); }
隻要滿足憑空冒出一個東西的條件即可
create(Object::new); // new的作用也是從虛無創造出一個對象,所以可以這麼寫 create(() -> "supplier"); create(() -> new Test(1));
最後再介紹函數式編程在排序中的使用
// Collections.sort的靜態方法定義 public static <T> void sort(List<T> list, Comparator<? super T> c) { list.sort(c); } // Comparator.comparing的靜態方法定義 // 理解成需要傳入一個T類型映射到U類型的形式即可 // 對應著示例就是傳入一個Test,返回一個實現瞭Comparable接口的對象(如Integer,String...) public static <T, U extends Comparable<? super U>> Comparator<T> comparing( Function<? super T, ? extends U> keyExtractor) { Objects.requireNonNull(keyExtractor); return (Comparator<T> & Serializable) (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2)); }
下面是爽快時間
// 使用簡短的代碼就能實現按對象中某個字段去排序 public static void main(String[] args) { ArrayList<Test> tests = new ArrayList<Test>() { { this.add(new Test(2, "abc")); this.add(new Test(1, "efg")); } }; // 現在Test實例的id字段排序,再將數組反轉,然後再按照name字段排序 Collections.sort(tests, Comparator.comparing(Test::getId) .reversed() .thenComparing(Test::getName)); System.out.println(tests); }
其他的函數接口就不再贅述,隻要搞懂原理,就能輕松上手使用
總結
到此這篇關於Java8函數式編程的文章就介紹到這瞭,更多相關Java8函數式編程內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!