我又回來了
已經很久沒有寫過博客了。
因爲前段時間我感到自己之前寫的博客毫無深度,像是一個產品的說明書,而這樣的博客會將一項技術看作黑匣子——你不需要知道這個技術的原理或源代碼的實現邏輯,你只需要按照接口的說明調接口去完成你要實現的功能就夠了。
這樣將某一項IT技術看作黑匣子,以簡單的利用它的功能實現自己想要的功能爲目標的想法在實際工作中是合理的,因爲實際工作是講究效率的,沒有那麼多時間讓你撥開面紗悟其內核。但是,作爲一名開發者,豈能止步於此?至少在工作之外的時間中,將那個“黑匣子”打開看看它的內部,在下次使用它的時候,讓這個“黑匣子”在你的手裏可以由自己完全定製,並以這個流行的技術爲鑑,擇其善者而從之,則其不善者而改之。如此,不斷進步。
我一直想寫有深度的技術博客,但我可能一直都對深度這個概念有些誤解,到底何爲深度?之前的我一直將深度這個詞和複雜聯繫在一起,認爲只有將複雜的東西吃透了,才叫深度;但是我現在才意識到,自己對複雜這個概念也有誤解——到底何爲複雜?Java基礎簡單嗎?肯定會有不少人質疑:Java基礎不簡單嗎?幾個基本變量、控制語句、類、接口、集合等等,都是很簡單的東西。但是如果我問你,這些東西都是怎麼實現的?應該會有很多人啞口無言。我認爲簡單與複雜的關係,一方面是相對的,即之前看上去複雜的東西掌握了之後就會看上去簡單;另一方面就是深度,即之前看上去簡單的東西追求深度之後就會看上去複雜。而簡單的東西是怎麼實現的,就是深度。
我將重拾博客,寫一些有深度的技術博客,這樣,也好在這個碎片化的時代,讓自己的技術不那麼碎片化,不停留於片面,養成深度思考的習慣。
關於Java的新功能的博客,網絡上還真的不少,但是不夠系統也不夠全面,我將這些東西整理後寫成博客,爲想要對Java8的新特性學習的朋友提供參考,共同學習。
我將連續寫Java8相比之前版本的新功能,由此成爲一個系列——話說Java8還真是一個呈上啓下的版本。
行爲參數化
所謂行爲參數化,就是將行爲作爲參數傳入函數。
比如下面這個接口(通過作者篩選圖書):
List<Book> selectBookdByAuth(String authName);
我們想要獲取“路遙”的書,我們需要將“路遙”作爲參數傳入selectBookdByAuth
這個函數。
但是,如果我又想根據出版社篩選圖書呢?那就又要創建一個根據出版社篩選圖書的接口了。那如果我又有需求了呢?要根據圖書類別篩選圖書……
有沒有什麼辦法把“我想要根據什麼篩選圖書”作爲一個參數呢?這樣我們只需要一個篩選圖書的接口就可以完成各種篩選圖書的功能了。
在這裏,“我想要根據什麼篩選圖書”是就是一個行爲,將“我想要根據什麼篩選圖書”作爲參數傳入相應的函數,就被稱爲行爲參數化
。
我們先舉個例子(篩選圖書的接口):
List<Book> selectBook(Predicate<Book> predicate);
我們暫且不用思考 Predicate
是什麼,當我們創建了這個接口之後,我們就可以將行爲作爲參數傳入該函數了。
例如我想要獲取以“路遙”爲作者的圖書:
List<Book> books = selectBook(boook -> book.getAuth().equals("路遙"));
就可以獲取到想要的結果了,再例如我想要獲取以“人民郵電出版社”爲出版社的圖書:
List<Book> books = selectBook(boook -> book.getPress().equals("人民郵電出版社"));
函數式
函數式接口
函數式接口就是隻定義一個抽象方法的接口。
例如:
/**
* 運算函數式接口.
*
* @author zuoyu
*
**/
public interface Operation {
/**
* 用於運算兩個int類型的數
* @param a - 參數一
* @param b - 參數二
* @return - 結果
*/
int opera(int a, int b);
}
對這個接口的簡單使用:
@Test
public void operationTest() {
Operation operation = (a, b) -> a + b;
int result = operation.opera(1, 1);
System.out.println(result);
} //result:2
函數描述符
函數式接口的抽象方法的簽名(參數、返回值)就是Lambda表達式的簽名。其中抽象方法就是函數描述符。
例如上面的int opera(int a, int b);
可以接受的Lambda表達式爲(a, b) -> a + b
,那麼其中的參數a
和參數b
都是int類型,a + b
結果也爲int,那麼這個函數的簽名就是(int, int) -> int
。
再打個比方,例如剛纔篩選圖書的函數式接口Predicate
:
public interface Predicate<T> {
boolean test(T t);
}
那麼它的函數簽名就是T -> boolean
,意味着我們將類型T
的對象作爲參數傳入,返回boolean
類型。只有符合函數描述符的Lambda表達式才能作爲參數傳入相應的函數。
使用函數式接口
Java API已經爲我們提供了很常用的函數式接口以及其函數描述符,當這些函數式接口不夠我們使用的時候我們也可以自己創建。(一定要記住,一個函數式本身並沒有什麼意義,其意義在於其函數簽名。)
拿幾個函數式接口細說一下:
Predicate<T>
public interface Predicate<T> {
boolean test(T t);
}
java.util.function.Predicate<T>
接口定義了一個名爲test
的抽象方法,它接受泛型T
對象,並返回一個boolean
。在你需要一個涉及到類型T的布爾表達式時,就可以使用這個函數式接口。
例如你可以寫一個過濾List
集合元素的方法,將這個函數式接口作爲參數:
/**
* Predicate<T>接口
*
* @param list - 集合
* @param predicate - 篩選條件
* @param <T> - 類型
* @return 符合要求的結果
*/
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> results = new ArrayList<>();
list.forEach(t -> {
if (predicate.test(t)) {
results.add(t);
}
});
return results;
}
這是一個通用的對List
集合進行元素過濾的方法:
-
從一個圖書List集合裏獲取以“圖靈出版社”爲出版社的圖書:
List<Book> pressBooks = filter(books, boook -> book.getPress().equals("圖靈出版社"));
- 注:在這裏,泛型
T
就是Book
類型
- 注:在這裏,泛型
-
從一個蘋果集合內獲取重量大於1.2的蘋果:
List<Apple> weightApples = filter(apples, apple -> apple.getwWight() > 1.2D);
- 注:在這裏泛型
T
就是Apple
類型
- 注:在這裏泛型
Consumer<T>
public interface Consumer<T> {
void accept(T t);
}
java.util.function.Consumer<T>
定義了一個名叫accept
的抽象方法,它接受泛型T的對象,沒有返回(void)
。如果你需要訪問類型爲T
的對象,並對其執行某些操作,就可以使用這個接口。
例如我要對一個List
集合內的每個元素執行行爲操作:
/**
* 對集合進行任何行爲操作
*/
public static <T> void action(List<T> list, Consumer<T> consumer) {
for (T t : list) {
consumer.accept(t);
}
}
這是一個通用的對List
集合進行元素行爲操作的方法:
-
對圖書集合內的元素進行打印:
action(books, book -> System.out.println(book.toString()));
- 注:在這裏,泛型
T
就是Book
類型
- 注:在這裏,泛型
-
將蘋果集合內的每個蘋果的重量增加0.5:
action(apples, apple -> { apple.setWeight(apple.getWeight() + 1.00D); });
- 注:在這裏泛型
T
就是Apple
類型
- 注:在這裏泛型
Function
public interface Function<T, R> {
R apply(T t);
}
java.util.function.Function<T, R>
接口定義了一個叫做apply
的方法,它接受一個泛型T
的對象,並返回一個泛型R
的對象。如果你需要定一個Lambda用於輸入對象的信息映射到輸出,你便可以利用這個接口來完成。
例如我要對一個List
集合內所有對象的某一個屬性進行提取:
/**
* 對集合內對象的某一元素進行提取
*/
public static <T, R> List<R> function(List<T> list, Function<T, R> function) {
List<R> rList = new ArrayList<>();
for (T t : list) {
R r = function.apply(t);
rList.add(r);
}
return rList;
}
這是一個通用的對List
集合的對象操作的方法:
-
對圖書集合內的所有書的書名進行提取:
List<String> booksName = function(books, book -> book.getName());
- 注:在這裏,泛型
T
是Book
類型,泛型R
是String
類型
- 注:在這裏,泛型
-
對蘋果集合內的所有蘋果的重量進行提取,並增加0.5:
List<Double> appleWeight = function(apples, apple -> apple.getWeight() + 0.5D );
- 注:在這裏,泛型
T
是Apple
類型,泛型R
是Double
類型
- 注:在這裏,泛型
Function的原始類型化
衆所周知,在Java中的泛型只能綁定到引用類型上,不能綁定在原始類型上。在Java中有一個將原始類型轉換爲對應的引用類型的機制——裝箱;相反的將引用類型轉換爲原始類型的機制——拆箱。這一系列操作都是Java自動完成的,但是這個機制是要付出代價的——
裝箱:把原始數據類型包裹起來,並保存到堆裏。所以,裝箱後需要更多的內存來保存,並需要額外的內存用來搜索並獲取被包裹的原始值。
Java8爲避免這個現象對其所提供的函數式接口帶來了一個專門版本,在數據的輸入和輸出都是原始類型時避免自動裝箱的操作,以此節省內存。
例如我要根據下標獲取一個蘋果對象:
IntFunction<Apple> appleIntFunction = (int i) -> apples.get(i);
Apple apple = appleIntFunction.apply(2);
我們來看一下IntFunction
接口:
public interface IntFunction<R> {
R apply(int value);
}
java.util.function.IntFunction<R>
接口的參數爲int
原始類型,返回一個R
類型,與我們想要完成相同功能的java.util.function.Function<T, R>
接口相比較,避免了必須傳入Integer
類型的自動裝箱操作。
再比如我要獲取一個double
隨機數的2倍數:
IntToDoubleFunction intToDoubleFunction = (int i) -> Math.random() * i;
double random = intToDoubleFunction.applyAsDouble(2);
我們看一下IntToDoubleFunction
接口:
public interface IntToDoubleFunction {
double applyAsDouble(int value);
}
上面的功能如果我們使用java.util.function.Function<T, R>
接口來實現這個功能,需要將接口寫成Function<Integer, Double>
,輸入Integer
類型並輸出Double
類型;相對於java.util.function.IntToDoubleFunction
接口,輸入int
類型輸出double
類型,省去了自動裝箱。
Function的變種函數:
IntFunction<R>
IntToDoubleFunction
IntToLongFunction
LongFunction<R>
LongToDoubleFunction
LongToIntFunction
DoubleFunction<R>
ToIntFunction<T>
ToDoubleFunction<T>
ToLongFunction<T>
其他函數式接口
JavaAPI自帶的函數接口還有不少,爲的是我們日常使用。當然也有不能滿足我們需求的時候,比如我要輸入三個參數,那就需要自己定義接口了。還是那句話,函數接口本身並無意義,其意義在於其函數簽名(參數數量與返回類型)。
JavaAPI自帶的函數式接口(不一一細說了):
函數式接口 | 函數描述符 |
---|---|
Predicate<T> |
T -> boolean |
Consumer<T> |
T -> void
|
Function<T, R> |
T -> R
|
Supplier<T> |
(void ) -> T
|
UnaryOperator<T> |
T -> T
|
BinaryOperator<T> |
(T , T ) -> T
|
BiPredicate<L, R> |
(L , R ) -> boolean
|
BiConsumer<T, U> |
(T , U ) -> void
|
BitFunction<T, U, R> |
(T , U ) -> R
|
- 注:以上函數式接口都有其原始類型化的變種。
方法引用
方法引用可以重複的使用現有的方法定義,可以將其理解爲Lambda的簡化方式。
例如,我要根據蘋果的重量對其從小到大排序:
apples.sort((apple1, apple2) -> apple1.getWeight().compareTo(apple2.getWeight()));
上面是Lambda表達式的寫法,那麼換作方法引用的方式可以簡化代碼:
apples.sort(Comparator.comparing(Apple::getWeight));
相對於Lambda表達式的寫法,方法引用的寫法在這裏意思更加清晰直觀。
方法引用主要有三類:
- 指向靜態方法的方法引用(例:
Integer
的parseInt()
方法可以直接寫成Integer::parseInt
)。 - 指向任意類型實例方法的方法引用(例:
String
的length()
方法可以直接寫成String::length
)。 - 指向現有對象的實例方法的方法引用(例:假設有一個局部變量
book
指向用於存放Book
類型的對象,它有一個實例方法getName()
,你可以直接寫成book::getName
)。
複合Lambda表達式
Java8API中的函數式接口都提供了複合方法,即通過這些方法把多個簡單的Lambda表達式複合成複雜的表達式。
謂詞複合
謂詞接口包含三個方法:
negate
(否定)、and
(並且)、or
(或)。
這幾個謂詞類似布爾語句之間的關係,舉個例子:
-
比如我現在有三種對圖書的篩選方案:
- 篩選出以“路遙”爲作者的圖書的邏輯接口:
Predicate<Books> bookPredicateByAuth = book -> book.getAuth().equals("路遙"));
- 篩選出以“人民郵電出版社”爲出版社的圖書的邏輯接口:
Predicate<Books> bookPredicateByPress = boook -> book.getPress().equals("人民郵電出版社"));
- 篩選出印刷時間在2010年之後的圖書的邏輯接口:
Predicate<Books> bookPredicateByPrintingData = boook -> book.getPrintingData.before(new SimpleDateFormat("yyyy-MM-dd").parse("2010-01-01"););
-
那麼我現在想要篩選出以“路遙”爲作者的,印刷時間在2010年之後的圖書,不要以“人民郵電出版社”出版的,可以該邏輯接口這麼寫:
Predicate<Books> bookPredicate = bookPredicateByAuth.and(bookPredicateByPrintingData).negate(bookPredicateByPress);
-
進行篩選:
List<Book> books = filter(books, bookPredicate));
函數複合
函數複合的接口方法有兩個:
andThen
和compose
。
andThen
方法會返回一個函數,它先對輸入應用一個給定的函數,再對輸出應用另一個函數。
compose
方法把給定的函數作用compose
的參數裏面給的那個函數,然後再把函數本身用於結果。
這兩個函數的作用就是函數套函數,舉個例子:
-
我現在有兩個函數:
- 第一個函數爲兩數相加的函數:
Function<Integer, Integer> add = x -> x + 1;
- 第二個爲兩數相乘的函數:
Function<Integer, Integer> multiply = x -> x * 2;
-
如果想要先算加法再算乘法,達到
multiply(add())
的效果(只是可以這樣理解,實際不是這樣的結構):Function<Integer, Integer> function = add.andThen(multiply); function.apply(1); // result: 4
-
如果想要先算乘法再算加法,達到
add(multiply)
的效果(只是可以這樣理解,實際不是這樣的結構):Function<Integer, Integer> function = add.compose(multiply); function.apply(1); // result: 3
結尾
轉載請表明出處左羽博客
我的郵箱:[email protected]
下期咱們聊一聊Java8的stream流