Java二十一篇:Java8

圣誕節(jié)快樂
大家好,我是小劉!
函數(shù)式編程
眾所周知,JDK8引入了函數(shù)式編程。什么是函數(shù)式編程呢?為何需要函數(shù)式編程呢?
認知決定高度。首先函數(shù)式編程是與面向對象編程一個層級的概念。
任何Java程序員都不可能不知道面向對象編程OOP。OOP的口號是“萬物皆對象”。什么是對象呢?就是現(xiàn)實中一個東西在編程領域的投射。對象有屬性,有方法。屬性表示數(shù)據(jù),方法表示行為。對象可以用來表示任何事物,非常強大。既然如此,為何又需要函數(shù)式編程呢?
什么是函數(shù)
函數(shù)式編程來自數(shù)學。有點(gaozhong)數(shù)學功底的你一定不會忘記這個東東:

數(shù)學中的函數(shù)表示的是什么呢?其實就是一個計算過程。編程領域也是一樣的, 函數(shù)表示了一個計算過程,比如,加減乘除,取余等等。就是我們java的方法的作用。
你肯定要問,那java中有方法了啊,為何又弄個什么函數(shù)式編程出來呢?問題在于,java中的方法是“二等公民”,它只能依附于對象存在,而不能獨立存在。
JS中的函數(shù)與Java中的方法比較
這方面我們可以把java與js放在一起比較。在js中,函數(shù)是一等公民,你可以直接聲明函數(shù),使用函數(shù)。如下例所示。
//將函數(shù)定義為變量
var add = function(a,b){
?return a+b;
}
var minus = function(a,b){
?return a-b;
}
//定義一個計算函數(shù),第一個參數(shù)是個函數(shù),注意我們已經開始傳遞函數(shù)了
function calc(fn,a,b){
?return fn(a,b);//調用函數(shù)fn,將a,b傳入
}
//調用calc
//傳入add函數(shù)作為第一個參數(shù)
var r1 = calc(add,10,5);
console.log("r1:"+r1)
//傳入minus函數(shù)作為第一個參數(shù)
var r2 = calc(minus,10,5);
console.log("r2:"+r2)
在這里我們也看到,函數(shù)的好處是可以封裝一段算法,一個計算過程,一個行為。這是對象無法做到的。但是好處卻很顯然,calc的第一個參數(shù)是個函數(shù),在不改變calc方法的前提下,可以非常容易的擴展出各種算法。只需要提供不同函數(shù)實現(xiàn)即可。因為函數(shù)封裝了算法。
這在java中是無法直接實現(xiàn)的。必須繞個彎。我們需要將函數(shù)放在一個接口中。代碼如下:
package com.woniuxy.test;
/**
* 表示一個計算接口,這個接口存在的唯一用處就是存放calc方法
*/
publicinterface Fn {
? ?//計算函數(shù)的抽象方法
? ?double calc(double a,double b);
}
//實現(xiàn)加法
package com.woniuxy.test;
publicclass Add implements Fn {
? ?@Override
? ?public double calc(double a, double b) {
? ? ? ?return a+b;
? ?}
}
//實現(xiàn)減法
package com.woniuxy.test;
publicclass Minus implements Fn {
? ?@Override
? ?public double calc(double a, double b) {
? ? ? ?return a-b;
? ?}
}
//計算器類
package com.woniuxy.test;
publicclass Calculator {
? ?/**
? ? * 接受Fn接口作為參數(shù),根據(jù)多態(tài)性,F(xiàn)n可以傳入Add和Minus
? ? */
? ?public double calc(Fn fn, double a, double b){
? ? ? ?return fn.calc(a,b);
? ?}
}
最后我們來組裝一下,代碼與上面的js代碼就非常接近了。
//調用測試
package com.woniuxy.test;
publicclass App {
? ?public static void main(String[] args) {
? ? ? ?//聲明函數(shù)實例
? ? ? ?Fn add = new Add();
? ? ? ?Fn minus = new Minus();
? ? ? ?
? ? ? ?Calculator c = new Calculator();
? ? ? ?//調用
? ? ? ?double r1 = c.calc(add,10,5);
? ? ? ?System.out.println("r1:"+r1);
? ? ? ?
? ? ? ?double r2 = c.calc(minus,10,5);
? ? ? ?System.out.println("r2:"+r2);
? ?}
}
我們來看,同樣的計算邏輯,為何js為java要簡單呢?因為java是oop的,基于對象。所以必須現(xiàn)有Fn接口才能有calc方法、必須現(xiàn)有Add類才能有calc方法的實現(xiàn)。方法必須依賴于對象。而JavaScript中函數(shù)是“一等公民”,可以直接創(chuàng)建。
引入函數(shù)式編程
為了簡化這類代碼,JDK8引入了函數(shù)式編程,終于Java也可以直接使用函數(shù)啦。
這里我們先給出BiFunction 這個接口,這個接口表示一個接受兩個參數(shù)的函數(shù),有三個泛型分別表示第一個參數(shù)的類型和第二個參數(shù)的類型以及返回值類型。所以可以直接用BiFunction來表示我們的這里的加減乘除的函數(shù)。
//修改Calculator.java,接受BiFunction作為第一個參數(shù)
? ?public double calc2(BiFunction<Double,Double,Double> fn,double a,double b){
? ? ? ?return fn.apply(a,b);
? ?}
//不再創(chuàng)建Fn、Add和Minus了,直接用BiFunction表示操作
publicclass App {
? ?public static void main(String[] args) {
? ? ? ?//直接用BiFunction封裝加法運算
? ? ? ?BiFunction<Double,Double,Double> add =new BiFunction<Double, Double, Double>() {
? ? ? ? ? ?@Override
? ? ? ? ? ?public Double apply(Double a, Double b) {
? ? ? ? ? ? ? ?return a+b;
? ? ? ? ? ?}
? ? ? ?};
? ? ? ?//直接用BiFunction封裝減法運算
? ? ? ?BiFunction<Double,Double,Double> minus =new BiFunction<Double, Double, Double>() {
? ? ? ? ? ?@Override
? ? ? ? ? ?public Double apply(Double a, Double b) {
? ? ? ? ? ? ? ?return a-b;
? ? ? ? ? ?}
? ? ? ?};
? ?}
}
顯然一下子少了3個類,比剛才簡單了一些,但是?BiFunction<Double,Double,Double>
?仍然看著頭大呀。有簡化的辦法嗎?
引入lambda表達式
是了,就是jdk8引入的lambda表達式。先看效果
//直接用BiFunction封裝加法運算
BiFunction<Double,Double,Double> add = (a, b) -> a+b;
//直接用BiFunction封裝減法運算
BiFunction<Double,Double,Double> minus = (a, b) -> a-b;
Calculator c = new Calculator();
//傳入函數(shù)
double r1 = c.calc2(add,10,5);
System.out.println("r1:"+r1);
double r2 = c.calc2(minus,10,5);
System.out.println("r2:"+r2);
感覺如何?已經無限接近js的代碼簡潔程度了。
lambda基礎
在Java程序中,我們經常遇到一大堆單方法接口,即一個接口只定義了一個方法:
Comparator
Runnable
Callable
以Comparator為例,我們想要調用Arrays.sort()時,可以傳入一個Comparator實例,以匿名類方式編寫如下:
String[] array = ...
Arrays.sort(array, new Comparator<String>() {
? ?public int compare(String s1, String s2) {
? ? ? ?return s1.compareTo(s2);
? ?}
});
上述寫法非常繁瑣。從Java 8開始,我們可以用Lambda表達式替換單方法接口。改寫上述代碼如下:
public class Main {
? ?public staticvoid main(String[] args) {
? ? ? ?String[] array = newString[] { "Apple", "Orange", "Banana", "Lemon" };
? ? ? ?Arrays.sort(array, (s1, s2) -> {
? ? ? ? ? ?return s1.compareTo(s2);
? ? ? ?});
? ? ? ?System.out.println(String.join(", ", array));
? ?}
}
觀察Lambda表達式的寫法,它只需要寫出方法定義:
(s1, s2) -> {
? ?return s1.compareTo(s2);
}
其中,參數(shù)是(s1, s2),參數(shù)類型可以省略,因為編譯器可以自動推斷出String類型。-> { ... }表示方法體,所有代碼寫在內部即可。Lambda表達式沒有class定義,因此寫法非常簡潔。
如果只有一行return xxx的代碼,完全可以用更簡單的寫法:
Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));
返回值的類型也是由編譯器自動推斷的,這里推斷出的返回值是int,因此,只要返回int,編譯器就不會報錯。
方法引用
方法引用通過方法的名字來指向一個方法。
方法引用可以使語言的構造更緊湊簡潔,減少冗余代碼。
方法引用使用一對冒號?::?。
類型語法對應的Lambda表達式靜態(tài)方法引用類名::staticMethod(args) -> 類名.staticMethod(args)實例方法引用inst::instMethod(args) -> inst.instMethod(args)對象方法引用類名::instMethod(inst,args) -> 類名.instMethod(args)構建方法引用類名::new(args) -> new 類名(args)
No Magic
代碼是清晰了,但是你可能沒弄明白是怎么回事。
BiFunction<Double,Double,Double> add =new BiFunction<Double, Double, Double>() {
? ? ? ? ? ?@Override
? ? ? ? ? ?public Double apply(Double a, Double b) {
? ? ? ? ? ? ? ?return a+b;
? ? ? ? ? ?}
? ? ? ?};
//被轉換成了
BiFunction<Double,Double,Double> add = (a, b) -> a+b;
這是怎么做到的呢?
道理其實很簡單。BiFunction接口只有一個方法apply,而BiFunction存在的意義就是為這個方法提供載體。換言之,我們使用BiFunction接口就是奔著apply方法去的。既然如此,為何不直接把那個方法表示出來呢?jdk就提供了一種簡潔的表示法,稱為lambda表達式,直接表示出了接口里的方法。
反過來想一下,因為接口只有一個方法,所以非常明確的定位到這個方法。這里就是apply方法。并不會混淆。但是為何a,b參數(shù)都沒有類型了呢?當然是為了簡化代碼,但其實是參數(shù)類型可以進行推斷出來的。我們通過反射能夠得到BiFunction的泛型參數(shù),根據(jù)約定就可以知道a和b的類型了。有了這些約定,jdk可以獲取到足夠的信息,自動將lambda表達式轉換為匿名內部類。
到這里,我們了解到JDK函數(shù)式編程的優(yōu)勢了。同樣的封裝計算過程(Add、Minus),傳統(tǒng)的方式和函數(shù)式編程的差別是非常大的。
一些實例
事已至此,我們不如來看幾個例子,感受下函數(shù)式編程的驚人魅力。請留意,lambda表達式都是在封裝算法。
package com.woniuxy.examples;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
publicclass Sorting {
? privatestatic ?List<Person> personList = Arrays.asList(new Person(20,"Dido")
? ? ? ? ? ?,new Person(15,"Guava")
? ? ? ? ? ?,new Person(30,"Alina")
? ? ? ? ? ?,new Person(28,"Crack")
? ?);
? ?public static void main(String[] args) {
? ? ? ?//0)封裝sqrt,雖然有點多此一舉
? ? ? ?Function<Double,Double> sqrt = (number)->Math.sqrt(number);
? ? ? ?System.out.println(sqrt.apply(9.0));;
? ? ? ?//1)封裝排序算法
? ? ? ?//按年齡排序
? ? ? ?Collections.sort(personList,(p1,p2)-> p1.getAge()-p2.getAge());
? ? ? ?System.out.println(personList);
? ? ? ?//按姓名排序
? ? ? ?Collections.sort(personList,(p1,p2)-> p1.getName().compareTo(p2.getName()));
? ? ? ?System.out.println(personList);
? ? ? ?//2)封裝選擇算法
? ? ? ?//選出18歲及以上的人
? ? ? ?List<Person> adultList = personList.stream()//這里用到了List的StreamAPI
? ? ? ? ? ? ? ?.filter((person -> person.getAge()>=18))//lambda表達式
? ? ? ? ? ? ? ?.collect(Collectors.toList());
? ? ? ?System.out.println(adultList);
? ? ? ?//3)封裝Runnable里的run方法,媽媽再也不用擔心我寫Runnable累死了
? ? ? ?new Thread(()->{
? ? ? ? ? ?for (int i = 0; i < 10; i++) {
? ? ? ? ? ? ? ?System.out.println(Thread.currentThread().getName()+":"+i);
? ? ? ? ? ?}
? ? ? ?}).start();
? ?}
}
class Person{
? ?privateint age;
? ?private String name;
? ?public Person(int age, String name) {
? ? ? ?this.age = age;
? ? ? ?this.name = name;
? ?}
//...省略setter、getter
? ?@Override
? ?public String toString() {
? ? ? ?return"Person{" +
? ? ? ? ? ? ? ?"age=" + age +
? ? ? ? ? ? ? ?", name='" + name + '\'' +
? ? ? ? ? ? ? ?'}';
? ?}
}
Stream
Java從8開始,不但引入了Lambda表達式,還引入了一個全新的流式API:Stream API。它位于java.util.stream包中。
劃重點:這個Stream不同于java.io的InputStream和OutputStream,它代表的是任意Java對象的序列。兩者對比如下:
java.iojava.util.stream存儲順序讀寫的byte或char順序輸出的任意Java對象實例用途序列化至文件或網(wǎng)絡內存計算/業(yè)務邏輯
有同學會問:一個順序輸出的Java對象序列,不就是一個List容器嗎?
再次劃重點:這個Stream和List也不一樣,List存儲的每個元素都是已經存儲在內存中的某個Java對象,而Stream輸出的元素可能并沒有預先存儲在內存中,而是實時計算出來的。
換句話說,List的用途是操作一組已存在的Java對象,而Stream實現(xiàn)的是惰性計算,兩者對比如下:
java.util.Listjava.util.stream元素已分配并存儲在內存可能未分配,實時計算用途操作一組已存在的Java對象惰性計算
Stream看上去有點不好理解,但我們舉個例子就明白了。
如果我們要表示一個全體自然數(shù)的集合,顯然,用List是不可能寫出來的,因為自然數(shù)是無限的,內存再大也沒法放到List中:
List<BigInteger> list = ??? // 全體自然數(shù)?
但是,用Stream可以做到。寫法如下:
Stream<BigInteger> naturals = createNaturalStream(); // 全體自然數(shù)
我們先不考慮createNaturalStream()這個方法是如何實現(xiàn)的,我們看看如何使用這個Stream。
首先,我們可以對每個自然數(shù)做一個平方,這樣我們就把這個Stream轉換成了另一個Stream:
Stream<BigInteger> naturals = createNaturalStream(); // 全體自然數(shù)
Stream<BigInteger> streamNxN = naturals.map(n -> n.multiply(n)); // 全體自然數(shù)的平方
因為這個streamNxN也有無限多個元素,要打印它,必須首先把無限多個元素變成有限個元素,可以用limit()方法截取前100個元素,最后用forEach()處理每個元素,這樣,我們就打印出了前100個自然數(shù)的平方:
Stream<BigInteger> naturals = createNaturalStream();
naturals.map(n -> n.multiply(n)) // 1, 4, 9, 16, 25...
? ? ? ?.limit(100)
? ? ? ?.forEach(System.out::println);
我們總結一下Stream的特點:它可以“存儲”有限個或無限個元素。這里的存儲打了個引號,是因為元素有可能已經全部存儲在內存中,也有可能是根據(jù)需要實時計算出來的。
Stream的另一個特點是,一個Stream可以輕易地轉換為另一個Stream,而不是修改原Stream本身。
最后,真正的計算通常發(fā)生在最后結果的獲取,也就是惰性計算。
Stream<BigInteger> naturals = createNaturalStream(); // 不計算
Stream<BigInteger> s2 = naturals.map(BigInteger::multiply); // 不計算
Stream<BigInteger> s3 = s2.limit(100); // 不計算
s3.forEach(System.out::println); // 計算
惰性計算的特點是:一個Stream轉換為另一個Stream時,實際上只存儲了轉換規(guī)則,并沒有任何計算發(fā)生。
小結
1.JDK8函數(shù)式編程提供了一種直接封裝函數(shù)的方式,即提供了一系列預定義的Function接口,提供了封裝函數(shù)所需的功能。并通過lambda表達式簡化了函數(shù)的編寫方式。
2.Stream API的特點是:
Stream API提供了一套新的流式處理的抽象序列;
Stream API支持函數(shù)式編程和鏈式操作;
Stream可以表示無限序列,并且大多數(shù)情況下是惰性求值的。
3.Java Steam API的使用我們下一篇來介紹,掌握了Java8的這種寫法之后,讓我們的代碼看起來更加的邏輯清楚,代碼更加優(yōu)雅了。
4.java8引入的新特性是為了簡化代碼,也是引入函數(shù)式編程融入大環(huán)境。
5.好好學習java8 的新特性,現(xiàn)在有一些企業(yè)都在使用java11的版本了(我們公司技就是用的java11),你不會java8的東西還不會吧!不會吧!
