在之前的章節(jié)中,我們的討論基本都是基于Java 7的,從本節(jié)開(kāi)始,我們探討Java 8的一些特性,主要內(nèi)容包括:
傳遞行為代碼 - Lambda表達(dá)式
函數(shù)式數(shù)據(jù)處理 - 流
組合式異步編程 - CompletableFuture
新的日期和時(shí)間API
本節(jié),我們先討論Lambda表達(dá)式,它是什么?有什么用呢?
Lambda表達(dá)式是Java 8新引入的一種語(yǔ)法,是一種緊湊的傳遞代碼的方式,它的名字來(lái)源于學(xué)術(shù)界的λ演算,具體我們就不探討了。
理解Lambda表達(dá)式,我們先回顧一下接口、匿名內(nèi)部類和代碼傳遞。
通過(guò)接口傳遞代碼
我們?cè)?a target="_blank" style="text-decoration-line: none; color: rgb(51, 153, 255);">19節(jié)介紹過(guò)接口以及面向接口的編程,針對(duì)接口而非具體類型進(jìn)行編程,可以降低程序的耦合性、提高靈活性、提高復(fù)用性。接口常被用于傳遞代碼,比如,在59節(jié),我們介紹過(guò)File的如下方法:
public String[] list(FilenameFilter filter)public File[] listFiles(FilenameFilter filter)
list和listFiles需要的其實(shí)不是FilenameFilter對(duì)象,而是它包含的如下方法:
boolean accept(File dir, String name);
或者說(shuō),list和listFiles希望接受一段方法代碼作為參數(shù),但沒(méi)有辦法直接傳遞這個(gè)方法代碼本身,只能傳遞一個(gè)接口。
再比如,我們?cè)?a target="_blank" style="text-decoration-line: none; color: rgb(51, 153, 255);">53節(jié)介紹過(guò)Collections的一些算法,很多方法都接受一個(gè)參數(shù)Comparator,比如:
public static <T> int binarySearch(List<? extends T> list, T key, Comparator<? super T> c)public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp)public static <T> void sort(List<T> list, Comparator<? super T> c)
它們需要的也不是Comparator對(duì)象,而是它包含的如下方法:
int compare(T o1, T o2);
但是,沒(méi)有辦法直接傳遞方法,只能傳遞一個(gè)接口。
我們?cè)?a target="_blank" style="text-decoration-line: none; color: rgb(51, 153, 255);">77節(jié)介紹過(guò)異步任務(wù)執(zhí)行服務(wù)ExecutorService,提交任務(wù)的方法有:
<T> Future<T> submit(Callable<T> task);<T> Future<T> submit(Runnable task, T result); Future<?> submit(Runnable task);
Callable和Runnable接口也用于傳遞任務(wù)代碼。
通過(guò)接口傳遞行為代碼,就要傳遞一個(gè)實(shí)現(xiàn)了該接口的實(shí)例對(duì)象,在之前的章節(jié)中,最簡(jiǎn)潔的方式是使用匿名內(nèi)部類,比如:
//列出當(dāng)前目錄下的所有后綴為.txt的文件File f = new File("."); File[] files = f.listFiles(new FilenameFilter(){ @Override public boolean accept(File dir, String name) { if(name.endsWith(".txt")){ return true; } return false; } });
將files按照文件名排序,代碼為:
Arrays.sort(files, new Comparator<File>() { @Override public int compare(File f1, File f2) { return f1.getName().compareTo(f2.getName()); } });
提交一個(gè)最簡(jiǎn)單的任務(wù),代碼為:
ExecutorService executor = Executors.newFixedThreadPool(100); executor.submit(new Runnable() { @Override public void run() { System.out.println("hello world"); } });
Lambda表達(dá)式
語(yǔ)法
Java 8提供了一種新的緊湊的傳遞代碼的語(yǔ)法 - Lambda表達(dá)式。對(duì)于前面列出文件的例子,代碼可以改為:
File f = new File("."); File[] files = f.listFiles((File dir, String name) -> { if (name.endsWith(".txt")) { return true; } return false; });
可以看出,相比匿名內(nèi)部類,傳遞代碼變得更為直觀,不再有實(shí)現(xiàn)接口的模板代碼,不再聲明方法,也名字也沒(méi)有,而是直接給出了方法的實(shí)現(xiàn)代碼。Lambda表達(dá)式由->分隔為兩部分,前面是方法的參數(shù),后面{}內(nèi)是方法的代碼。
上面代碼可以簡(jiǎn)化為:
File[] files = f.listFiles((File dir, String name) -> { return name.endsWith(".txt"); });
當(dāng)主體代碼只有一條語(yǔ)句的時(shí)候,括號(hào)和return語(yǔ)句也可以省略,上面代碼可以變?yōu)椋?/p>
File[] files = f.listFiles((File dir, String name) -> name.endsWith(".txt"));
注意,沒(méi)有括號(hào)的時(shí)候,主體代碼是一個(gè)表達(dá)式,這個(gè)表達(dá)式的值就是函數(shù)的返回值,結(jié)尾不能加分號(hào);,也不能加return語(yǔ)句。
方法的參數(shù)類型聲明也可以省略,上面代碼還可以繼續(xù)簡(jiǎn)化為:
File[] files = f.listFiles((dir, name) -> name.endsWith(".txt"));
之所以可以省略方法的參數(shù)類型,是因?yàn)镴ava可以自動(dòng)推斷出來(lái),它知道listFiles接受的參數(shù)類型是FilenameFilter,這個(gè)接口只有一個(gè)方法accept,這個(gè)方法的兩個(gè)參數(shù)類型分別是File和String。
這樣簡(jiǎn)化下來(lái),代碼是不是簡(jiǎn)潔清楚多了?
排序的代碼用Lambda表達(dá)式可以寫(xiě)為:
Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));
提交任務(wù)的代碼用Lambda表達(dá)式可以寫(xiě)為:
executor.submit(()->System.out.println("hello"));
參數(shù)部分為空,寫(xiě)為()。
當(dāng)參數(shù)只有一個(gè)的時(shí)候,參數(shù)部分的括號(hào)可以省略,比如,F(xiàn)ile還有如下方法:
public File[] listFiles(FileFilter filter)
FileFilter的定義為:
public interface FileFilter { boolean accept(File pathname); }
使用FileFilter重寫(xiě)上面的列舉文件的例子,代碼可以為:
File[] files = f.listFiles(path -> path.getName().endsWith(".txt"));
變量引用
與匿名內(nèi)部類類似,Lambda表達(dá)式也可以訪問(wèn)定義在主體代碼外部的變量,但對(duì)于局部變量,它也只能訪問(wèn)final類型的變量,與匿名內(nèi)部類的區(qū)別是,它不要求變量聲明為final,但變量事實(shí)上不能被重新賦值。比如:
String msg = "hello world"; executor.submit(()->System.out.println(msg));
可以訪問(wèn)局部變量msg,但msg不能被重新賦值,如果這樣寫(xiě):
String msg = "hello world"; msg = "good morning"; executor.submit(()->System.out.println(msg));
Java編譯器會(huì)提示錯(cuò)誤。
這個(gè)原因與匿名內(nèi)部類是一樣的,Java會(huì)將msg的值作為參數(shù)傳遞給Lambda表達(dá)式,為L(zhǎng)ambda表達(dá)式建立一個(gè)副本,它的代碼訪問(wèn)的是這個(gè)副本,而不是外部聲明的msg變量。如果允許msg被修改,則程序員可能會(huì)誤以為L(zhǎng)ambda表達(dá)式會(huì)讀到修改后的值,引起更多的混淆。
為什么非要建副本,直接訪問(wèn)外部的msg變量不行嗎?不行,因?yàn)閙sg定義在棧中,當(dāng)Lambda表達(dá)式被執(zhí)行的時(shí)候,msg可能早已被釋放了。如果希望能夠修改值,可以將變量定義為實(shí)例變量,或者,將變量定義為數(shù)組,比如:
String[] msg = new String[]{"hello world"}; msg[0] = "good morning"; executor.submit(()->System.out.println(msg[0]));
與匿名內(nèi)部類比較
從以上內(nèi)容可以看出,Lambda表達(dá)式與匿名內(nèi)部類很像,主要就是簡(jiǎn)化了語(yǔ)法,那它是不是語(yǔ)法糖,內(nèi)部實(shí)現(xiàn)其實(shí)就是內(nèi)部類呢?答案是否定的,Java會(huì)為每個(gè)匿名內(nèi)部類生成一個(gè)類,但Lambda表達(dá)式不會(huì),Lambda表達(dá)式通常比較短,為每個(gè)表達(dá)式生成一個(gè)類會(huì)生成大量的類,性能會(huì)受到影響。
Java利用了Java 7引入的為支持動(dòng)態(tài)類型語(yǔ)言引入的invokedynamic指令、方法句柄(method handle)等,具體實(shí)現(xiàn)比較復(fù)雜,我們就不探討了,感興趣可以參看http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html,我們需要知道的是,Java的實(shí)現(xiàn)是非常高效的,不用擔(dān)心生成太多類的問(wèn)題。
Lambda表達(dá)式不是匿名內(nèi)部類,那它的類型到底是什么呢?是函數(shù)式接口。
函數(shù)式接口
Java 8引入了函數(shù)式接口的概念,函數(shù)式接口也是接口,但只能有一個(gè)抽象方法,前面提及的接口都只有一個(gè)抽象方法,都是函數(shù)式接口。之所以強(qiáng)調(diào)是"抽象"方法,是因?yàn)镴ava 8中還允許定義其他方法,我們待會(huì)會(huì)談到。Lambda表達(dá)式可以賦值給函數(shù)式接口,比如:
FileFilter filter = path -> path.getName().endsWith(".txt"); FilenameFilter fileNameFilter = (dir, name) -> name.endsWith(".txt"); Comparator<File> comparator = (f1, f2) -> f1.getName().compareTo(f2.getName()); Runnable task = () -> System.out.println("hello world");
如果看這些接口的定義,會(huì)發(fā)現(xiàn)它們都有一個(gè)注解@FunctionalInterface,比如:
@FunctionalInterfacepublic interface Runnable { public abstract void run(); }
@FunctionalInterface用于清晰地告知使用者,這是一個(gè)函數(shù)式接口,不過(guò),這個(gè)注解不是必需的,不加,只要只有一個(gè)抽象方法,也是函數(shù)式接口。但如果加了,而又定義了超過(guò)一個(gè)抽象方法,Java編譯器會(huì)報(bào)錯(cuò),這類似于我們?cè)?a target="_blank" style="text-decoration-line: none; color: rgb(51, 153, 255);">85節(jié)介紹的Override注解。
預(yù)定義的函數(shù)式接口
接口列表
Java 8定義了大量的預(yù)定義函數(shù)式接口,用于常見(jiàn)類型的代碼傳遞,這些函數(shù)定義在包java.util.function下,主要的有:
對(duì)于基本類型boolean, int, long和double,為避免裝箱/拆箱,Java 8提供了一些專門(mén)的函數(shù),比如,int相關(guān)的主要函數(shù)有:
這些函數(shù)有什么用呢?它們被大量使用于Java 8的函數(shù)式數(shù)據(jù)處理Stream相關(guān)的類中,關(guān)于Stream,我們下節(jié)介紹。
即使不使用Stream,也可以在自己的代碼中直接使用這些預(yù)定義的函數(shù),我們看一些簡(jiǎn)單的示例。
Predicate示例
為便于舉例,我們先定義一個(gè)簡(jiǎn)單的學(xué)生類Student,有name和score兩個(gè)屬性,如下所示,我們省略了getter/setter方法。
static class Student { String name; double score; public Student(String name, double score) { this.name = name; this.score = score; } }
有一個(gè)學(xué)生列表:
List<Student> students = Arrays.asList(new Student[] { new Student("zhangsan", 89d), new Student("lisi", 89d), new Student("wangwu", 98d) });
在日常開(kāi)發(fā)中,列表處理的一個(gè)常見(jiàn)需求是過(guò)濾,列表的類型經(jīng)常不一樣,過(guò)濾的條件也經(jīng)常變化,但主體邏輯都是類似的,可以借助Predicate寫(xiě)一個(gè)通用的方法,如下所示:
public static <E> List<E> filter(List<E> list, Predicate<E> pred) { List<E> retList = new ArrayList<>(); for (E e : list) { if (pred.test(e)) { retList.add(e); } } return retList; }
這個(gè)方法可以這么用:
// 過(guò)濾90分以上的students = filter(students, t -> t.getScore() > 90);
Function示例
列表處理的另一個(gè)常見(jiàn)需求是轉(zhuǎn)換,比如,給定一個(gè)學(xué)生列表,需要返回名稱列表,或者將名稱轉(zhuǎn)換為大寫(xiě)返回,可以借助Function寫(xiě)一個(gè)通用的方法,如下所示:
public static <T, R> List<R> map(List<T> list, Function<T, R> mapper) { List<R> retList = new ArrayList<>(list.size()); for (T e : list) { retList.add(mapper.apply(e)); } return retList; }
根據(jù)學(xué)生列表返回名稱列表的代碼可以為:
List<String> names = map(students, t -> t.getName());
將學(xué)生名稱轉(zhuǎn)換為大寫(xiě)的代碼可以為:
students = map(students, t -> new Student(t.getName().toUpperCase(), t.getScore()));
Consumer示例
在上面轉(zhuǎn)換學(xué)生名稱為大寫(xiě)的例子中,我們?yōu)槊總€(gè)學(xué)生創(chuàng)建了一個(gè)新的對(duì)象,另一種常見(jiàn)的情況是直接修改原對(duì)象,具體怎么修改通過(guò)代碼傳遞,這時(shí),可以用Consumer寫(xiě)一個(gè)通用的方法,比如:
public static <E> void foreach(List<E> list, Consumer<E> consumer) { for (E e : list) { consumer.accept(e); } }
上面轉(zhuǎn)換為大寫(xiě)的例子可以改為:
foreach(students, t -> t.setName(t.getName().toUpperCase()));
以上這些示例主要用于演示函數(shù)式接口的基本概念,實(shí)際中應(yīng)該使用下節(jié)介紹的流API。
方法引用
基本用法
Lambda表達(dá)式經(jīng)常就是調(diào)用對(duì)象的某個(gè)方法,比如:
List<String> names = map(students, t -> t.getName());
這時(shí),它可以進(jìn)一步簡(jiǎn)化,如下所示:
List<String> names = map(students, Student::getName);
Student::getName這種寫(xiě)法,是Java 8引入的一種新語(yǔ)法,稱之為方法引用,它是Lambda表達(dá)式的一種簡(jiǎn)寫(xiě)方法,由::分隔為兩部分,前面是類名或變量名,后面是方法名。方法可以是實(shí)例方法,也可以是靜態(tài)方法,但含義不同。
我們看一些例子,還是以Student為例,先增加一個(gè)靜態(tài)方法:
public static String getCollegeName(){ return "Laoma School"; }
靜態(tài)方法
對(duì)于靜態(tài)方法,如下語(yǔ)句:
Supplier<String> s = Student::getCollegeName;
等價(jià)于:
Supplier<String> s = () -> Student.getCollegeName();
它們的參數(shù)都是空,返回類型為String。
實(shí)例方法
而對(duì)于實(shí)例方法,它第一個(gè)參數(shù)就是該類型的實(shí)例,比如,如下語(yǔ)句:
Function<Student, String> f = Student::getName;
等價(jià)于:
Function<Student, String> f = (Student t) -> t.getName();
對(duì)于Student::setName,它是一個(gè)BiConsumer,即:
BiConsumer<Student, String> c = Student::setName;
等價(jià)于:
BiConsumer<Student, String> c = (t, name) -> t.setName(name);
通過(guò)變量引用方法
如果方法引用的第一部分是變量名,則相當(dāng)于調(diào)用那個(gè)對(duì)象的方法,比如:
Student t = new Student("張三", 89d); Supplier<String> s = t::getName;
等價(jià)于:
Supplier<String> s = () -> t.getName();
而:
Consumer<String> consumer = t::setName;
等價(jià)于:
Consumer<String> consumer = (name) -> t.setName(name);
構(gòu)造方法
對(duì)于構(gòu)造方法,方法引用的語(yǔ)法是<類名>::new,如Student::new,如下語(yǔ)句:
BiFunction<String, Double, Student> s = (name, score) -> new Student(name, score);
等價(jià)于:
BiFunction<String, Double, Student> s = Student::new;
函數(shù)的復(fù)合
在前面的例子中,函數(shù)式接口都用作方法的參數(shù),其他部分通過(guò)Lambda表達(dá)式傳遞具體代碼給它,函數(shù)式接口和Lambda表達(dá)式還可用作方法的返回值,傳遞代碼回調(diào)用者,將這兩種用法結(jié)合起來(lái),可以構(gòu)造復(fù)合的函數(shù),使程序簡(jiǎn)潔易讀。
下面我們會(huì)看一些例子,在介紹例子之前,我們先需要介紹Java 8對(duì)接口的增強(qiáng)。
接口的靜態(tài)方法和默認(rèn)方法
在Java 8之前,接口中的方法都是抽象方法,都沒(méi)有實(shí)現(xiàn)體,Java 8允許在接口中定義兩類新方法:靜態(tài)方法和默認(rèn)方法,它們有實(shí)現(xiàn)體,比如:
public interface IDemo { void hello(); public static void test() { System.out.println("hello"); } default void hi() { System.out.println("hi"); } }
test()就是一個(gè)靜態(tài)方法,可以通過(guò)IDemo.test()調(diào)用。在接口不能定義靜態(tài)方法之前,相關(guān)的靜態(tài)方法往往定義在單獨(dú)的類中,比如,Collection接口有一個(gè)對(duì)應(yīng)的單獨(dú)的類Collections,在Java 8中,就可以直接寫(xiě)在接口中了,比如Comparator接口就定義了多個(gè)靜態(tài)方法。
hi()是一個(gè)默認(rèn)方法,由關(guān)鍵字default標(biāo)識(shí),默認(rèn)方法與抽象方法都是接口的方法,不同在于,它有默認(rèn)的實(shí)現(xiàn),實(shí)現(xiàn)類可以改變它的實(shí)現(xiàn),也可以不改變。引入默認(rèn)方法主要是函數(shù)式數(shù)據(jù)處理的需求,是為了便于給接口增加功能。
在沒(méi)有默認(rèn)方法之前,Java是很難給接口增加功能的,比如List接口,因?yàn)橛刑喾荍ava JDK控制的代碼實(shí)現(xiàn)了該接口,如果給接口增加一個(gè)方法,則那些接口的實(shí)現(xiàn)就無(wú)法在新版Java 上運(yùn)行,必須改寫(xiě)代碼,實(shí)現(xiàn)新的方法,這顯然是無(wú)法接受的。函數(shù)式數(shù)據(jù)處理需要給一些接口增加一些新的方法,所以就有了默認(rèn)方法的概念,接口增加了新方法,而接口現(xiàn)有的實(shí)現(xiàn)類也不需要必須實(shí)現(xiàn)它。
看一些例子,List接口增加了sort方法,其定義為:
default void sort(Comparator<? super E> c) { Object[] a = this.toArray(); Arrays.sort(a, (Comparator) c); ListIterator<E> i = this.listIterator(); for (Object e : a) { i.next(); i.set((E) e); } }http://www.cnblogs.com/swiftma/p/7123356.html