本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連接 html
在以前的章節中,咱們的討論基本都是基於Java 7的,從本節開始,咱們探討Java 8的一些特性,主要內容包括:java
本節,咱們先討論Lambda表達式,它是什麼?有什麼用呢?git
Lambda表達式是Java 8新引入的一種語法, 是一種緊湊的傳遞代碼的方式,它的名字來源於學術界的λ演算,具體咱們就不探討了。程序員
理解Lambda表達式,咱們先回顧一下接口、匿名內部類和代碼傳遞。github
咱們在19節介紹過接口以及面向接口的編程,針對接口而非具體類型進行編程,能夠下降程序的耦合性、提升靈活性、提升複用性。接口常被用於傳遞代碼,好比,在59節,咱們介紹過File的以下方法:算法
public String[] list(FilenameFilter filter)
public File[] listFiles(FilenameFilter filter)
複製代碼
list和listFiles須要的其實不是FilenameFilter對象,而是它包含的以下方法:編程
boolean accept(File dir, String name);
複製代碼
或者說,list和listFiles但願接受一段方法代碼做爲參數,但沒有辦法直接傳遞這個方法代碼自己,只能傳遞一個接口。swift
再好比,咱們在53節介紹過Collections的一些算法,不少方法都接受一個參數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對象,而是它包含的以下方法:微信
int compare(T o1, T o2);
複製代碼
可是,沒有辦法直接傳遞方法,只能傳遞一個接口。
咱們在77節介紹過異步任務執行服務ExecutorService,提交任務的方法有:
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
複製代碼
Callable和Runnable接口也用於傳遞任務代碼。
經過接口傳遞行爲代碼,就要傳遞一個實現了該接口的實例對象,在以前的章節中,最簡潔的方式是使用匿名內部類,好比:
//列出當前目錄下的全部後綴爲.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());
}
});
複製代碼
提交一個最簡單的任務,代碼爲:
ExecutorService executor = Executors.newFixedThreadPool(100);
executor.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello world");
}
});
複製代碼
Java 8提供了一種新的緊湊的傳遞代碼的語法 - Lambda表達式。對於前面列出文件的例子,代碼能夠改成:
File f = new File(".");
File[] files = f.listFiles((File dir, String name) -> {
if (name.endsWith(".txt")) {
return true;
}
return false;
});
複製代碼
能夠看出,相比匿名內部類,傳遞代碼變得更爲直觀,再也不有實現接口的模板代碼,再也不聲明方法,也名字也沒有,而是直接給出了方法的實現代碼。Lambda表達式由->分隔爲兩部分,前面是方法的參數,後面{}內是方法的代碼。
上面代碼能夠簡化爲:
File[] files = f.listFiles((File dir, String name) -> {
return name.endsWith(".txt");
});
複製代碼
當主體代碼只有一條語句的時候,括號和return語句也能夠省略,上面代碼能夠變爲:
File[] files = f.listFiles((File dir, String name) -> name.endsWith(".txt"));
複製代碼
注意,沒有括號的時候,主體代碼是一個表達式,這個表達式的值就是函數的返回值,結尾不能加分號;,也不能加return語句。
方法的參數類型聲明也能夠省略,上面代碼還能夠繼續簡化爲:
File[] files = f.listFiles((dir, name) -> name.endsWith(".txt"));
複製代碼
之因此能夠省略方法的參數類型,是由於Java能夠自動推斷出來,它知道listFiles接受的參數類型是FilenameFilter,這個接口只有一個方法accept,這個方法的兩個參數類型分別是File和String。
這樣簡化下來,代碼是否是簡潔清楚多了?
排序的代碼用Lambda表達式能夠寫爲:
Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));
複製代碼
提交任務的代碼用Lambda表達式能夠寫爲:
executor.submit(()->System.out.println("hello"));
複製代碼
參數部分爲空,寫爲()。
當參數只有一個的時候,參數部分的括號能夠省略,好比,File還有以下方法:
public File[] listFiles(FileFilter filter)
複製代碼
FileFilter的定義爲:
public interface FileFilter {
boolean accept(File pathname);
}
複製代碼
使用FileFilter重寫上面的列舉文件的例子,代碼能夠爲:
File[] files = f.listFiles(path -> path.getName().endsWith(".txt"));
複製代碼
與匿名內部類相似,Lambda表達式也能夠訪問定義在主體代碼外部的變量,但對於局部變量,它也只能訪問final類型的變量,與匿名內部類的區別是,它不要求變量聲明爲final,但變量事實上不能被從新賦值。好比:
String msg = "hello world";
executor.submit(()->System.out.println(msg));
複製代碼
能夠訪問局部變量msg,但msg不能被從新賦值,若是這樣寫:
String msg = "hello world";
msg = "good morning";
executor.submit(()->System.out.println(msg));
複製代碼
Java編譯器會提示錯誤。
這個緣由與匿名內部類是同樣的,Java會將msg的值做爲參數傳遞給Lambda表達式,爲Lambda表達式創建一個副本,它的代碼訪問的是這個副本,而不是外部聲明的msg變量。若是容許msg被修改,則程序員可能會誤覺得Lambda表達式會讀到修改後的值,引發更多的混淆。
爲何非要建副本,直接訪問外部的msg變量不行嗎?不行,由於msg定義在棧中,當Lambda表達式被執行的時候,msg可能早已被釋放了。若是但願可以修改值,能夠將變量定義爲實例變量,或者,將變量定義爲數組,好比:
String[] msg = new String[]{"hello world"};
msg[0] = "good morning";
executor.submit(()->System.out.println(msg[0]));
複製代碼
從以上內容能夠看出,Lambda表達式與匿名內部類很像,主要就是簡化了語法,那它是否是語法糖,內部實現其實就是內部類呢?答案是否認的, Java會爲每一個匿名內部類生成一個類,但Lambda表達式不會,Lambda表達式一般比較短,爲每一個表達式生成一個類會生成大量的類,性能會受到影響。
Java利用了Java 7引入的爲支持動態類型語言引入的invokedynamic指令、方法句柄(method handle)等,具體實現比較複雜,咱們就不探討了,感興趣能夠參看cr.openjdk.java.net/~briangoetz…,咱們須要知道的是,Java的實現是很是高效的,不用擔憂生成太多類的問題。
Lambda表達式不是匿名內部類,那它的類型究竟是什麼呢?是 函數式接口。
Java 8引入了函數式接口的概念, 函數式接口也是接口,但只能有一個抽象方法,前面說起的接口都只有一個抽象方法,都是函數式接口。之因此強調是"抽象"方法,是由於Java 8中還容許定義其餘方法,咱們待會會談到。Lambda表達式能夠賦值給函數式接口,好比:
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");
複製代碼
若是看這些接口的定義,會發現它們都有一個註解@FunctionalInterface,好比:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
複製代碼
@FunctionalInterface用於清晰地告知使用者,這是一個函數式接口,不過,這個註解不是必需的,不加,只要只有一個抽象方法,也是函數式接口。但若是加了,而又定義了超過一個抽象方法,Java編譯器會報錯,這相似於咱們在85節介紹的Override註解。
Java 8定義了大量的預約義函數式接口,用於常見類型的代碼傳遞,這些函數定義在包java.util.function下,主要的有:
對於基本類型boolean, int, long和double,爲避免裝箱/拆箱,Java 8提供了一些專門的函數,好比,int相關的主要函數有:
這些函數有什麼用呢?它們被大量使用於Java 8的函數式數據處理Stream相關的類中,關於Stream,咱們下節介紹。
即便不使用Stream,也能夠在本身的代碼中直接使用這些預約義的函數,咱們看一些簡單的示例。
爲便於舉例,咱們先定義一個簡單的學生類Student,有name和score兩個屬性,以下所示,咱們省略了getter/setter方法。
static class Student {
String name;
double score;
public Student(String name, double score) {
this.name = name;
this.score = score;
}
}
複製代碼
有一個學生列表:
List<Student> students = Arrays.asList(new Student[] {
new Student("zhangsan", 89d),
new Student("lisi", 89d),
new Student("wangwu", 98d) });
複製代碼
在平常開發中,列表處理的一個常見需求是過濾,列表的類型常常不同,過濾的條件也常常變化,但主體邏輯都是相似的,能夠藉助Predicate寫一個通用的方法,以下所示:
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;
}
複製代碼
這個方法能夠這麼用:
// 過濾90分以上的
students = filter(students, t -> t.getScore() > 90);
複製代碼
列表處理的另外一個常見需求是轉換,好比,給定一個學生列表,須要返回名稱列表,或者將名稱轉換爲大寫返回,能夠藉助Function寫一個通用的方法,以下所示:
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;
}
複製代碼
根據學生列表返回名稱列表的代碼能夠爲:
List<String> names = map(students, t -> t.getName());
複製代碼
將學生名稱轉換爲大寫的代碼能夠爲:
students = map(students, t -> new Student(t.getName().toUpperCase(), t.getScore()));
複製代碼
在上面轉換學生名稱爲大寫的例子中,咱們爲每一個學生建立了一個新的對象,另外一種常見的狀況是直接修改原對象,具體怎麼修改經過代碼傳遞,這時,能夠用Consumer寫一個通用的方法,好比:
public static <E> void foreach(List<E> list, Consumer<E> consumer) {
for (E e : list) {
consumer.accept(e);
}
}
複製代碼
上面轉換爲大寫的例子能夠改成:
foreach(students, t -> t.setName(t.getName().toUpperCase()));
複製代碼
以上這些示例主要用於演示函數式接口的基本概念,實際中應該使用下節介紹的流API。
Lambda表達式常常就是調用對象的某個方法,好比:
List<String> names = map(students, t -> t.getName());
複製代碼
這時,它能夠進一步簡化,以下所示:
List<String> names = map(students, Student::getName);
複製代碼
Student::getName
這種寫法,是Java 8引入的一種新語法,稱之爲 方法引用,它是Lambda表達式的一種簡寫方法,由::分隔爲兩部分,前面是類名或變量名,後面是方法名。方法能夠是實例方法,也能夠是靜態方法,但含義不一樣。
咱們看一些例子,仍是以Student爲例,先增長一個靜態方法:
public static String getCollegeName(){
return "Laoma School";
}
複製代碼
對於靜態方法,以下語句:
Supplier<String> s = Student::getCollegeName;
複製代碼
等價於:
Supplier<String> s = () -> Student.getCollegeName();
複製代碼
它們的參數都是空,返回類型爲String。
而對於實例方法,它第一個參數就是該類型的實例,好比,以下語句:
Function<Student, String> f = Student::getName;
複製代碼
等價於:
Function<Student, String> f = (Student t) -> t.getName();
複製代碼
對於Student::setName,它是一個BiConsumer,即:
BiConsumer<Student, String> c = Student::setName;
複製代碼
等價於:
BiConsumer<Student, String> c = (t, name) -> t.setName(name);
複製代碼
若是方法引用的第一部分是變量名,則至關於調用那個對象的方法,好比:
Student t = new Student("張三", 89d);
Supplier<String> s = t::getName;
複製代碼
等價於:
Supplier<String> s = () -> t.getName();
複製代碼
而:
Consumer<String> consumer = t::setName;
複製代碼
等價於:
Consumer<String> consumer = (name) -> t.setName(name);
複製代碼
對於構造方法,方法引用的語法是<類名>::new,如Student::new,以下語句:
BiFunction<String, Double, Student> s = (name, score) -> new Student(name, score);
複製代碼
等價於:
BiFunction<String, Double, Student> s = Student::new;
複製代碼
在前面的例子中,函數式接口都用做方法的參數,其餘部分經過Lambda表達式傳遞具體代碼給它,函數式接口和Lambda表達式還可用做方法的返回值,傳遞代碼回調用者,將這兩種用法結合起來,能夠構造複合的函數,使程序簡潔易讀。
下面咱們會看一些例子,在介紹例子以前,咱們先須要介紹Java 8對接口的加強。
在Java 8以前,接口中的方法都是抽象方法,都沒有實現體,Java 8容許在接口中定義兩類新方法:靜態方法和默認方法,它們有實現體,好比:
public interface IDemo {
void hello();
public static void test() {
System.out.println("hello");
}
default void hi() {
System.out.println("hi");
}
}
複製代碼
test()就是一個靜態方法,能夠經過IDemo.test()調用。在接口不能定義靜態方法以前,相關的靜態方法每每定義在單獨的類中,好比,Collection接口有一個對應的單獨的類Collections,在Java 8中,就能夠直接寫在接口中了,好比Comparator接口就定義了多個靜態方法。
hi()是一個默認方法,由關鍵字default標識,默認方法與抽象方法都是接口的方法,不一樣在於,它有默認的實現,實現類能夠改變它的實現,也能夠不改變。引入默認方法主要是函數式數據處理的需求,是爲了便於給接口增長功能。
在沒有默認方法以前,Java是很難給接口增長功能的,好比List接口,由於有太多非Java JDK控制的代碼實現了該接口,若是給接口增長一個方法,則那些接口的實現就沒法在新版Java 上運行,必須改寫代碼,實現新的方法,這顯然是沒法接受的。函數式數據處理須要給一些接口增長一些新的方法,因此就有了默認方法的概念,接口增長了新方法,而接口現有的實現類也不須要必須實現它。
看一些例子,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);
}
}
複製代碼
Collection接口增長了stream方法,其定義爲:
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
複製代碼
須要說明的是,即便能定義方法體了,接口與抽象類仍是不同的,接口中不能定義實例變量,而抽象類能夠。
瞭解了靜態方法和默認方法,咱們看一些利用它們實現複合函數的例子。
Comparator接口定義了以下靜態方法:
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));
}
複製代碼
這個方法是什麼意思呢?它用於構建一個Comparator,好比,在前面的例子中,對文件按照文件名排序的代碼爲:
Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));
複製代碼
使用comparing方法,代碼能夠簡化爲:
Arrays.sort(files, Comparator.comparing(File::getName));
複製代碼
這樣,代碼的可讀性是否是大大加強了?comparing方法爲何能達到這個效果呢?它構建並返回了一個符合Comparator接口的Lambda表達式,這個Comparator接受的參數類型是File,它使用了傳遞過來的函數代碼keyExtractor將File轉換爲String進行比較。像comparing這樣使用複合方式構建並傳遞代碼並不容易閱讀和理解,但調用者很方便,也很容易理解。
Comparator還有不少默認方法,咱們看兩個:
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
default Comparator<T> thenComparing(Comparator<? super T> other) {
Objects.requireNonNull(other);
return (Comparator<T> & Serializable) (c1, c2) -> {
int res = compare(c1, c2);
return (res != 0) ? res : other.compare(c1, c2);
};
}
複製代碼
reversed返回一個新的Comparator,按原排序逆序排。thenComparing也是一個返回一個新的Comparator,在原排序認爲兩個元素排序相同的時候,使用提供的other Comparator進行比較。
看一個使用的例子,將學生列表按照分數倒序排(高分在前),分數同樣的,按照名字進行排序,代碼以下所示:
students.sort(Comparator.comparing(Student::getScore)
.reversed()
.thenComparing(Student::getName));
複製代碼
這樣,代碼是否是很容易讀?
在java.util.function包中的不少函數式接口裏,都定義了一些複合方法,咱們看一些例子。
Function接口有以下定義:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
複製代碼
先將T類型的參數轉化爲類型R,再調用after將R轉換爲V,最後返回類型V。
還有以下定義:
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
複製代碼
對V類型的參數,先調用before將V轉換爲T類型,再調用當前的apply方法轉換爲R類型返回。
Consumer, Predicate等都有一些複合方法,它們大量被用於下節介紹的函數式數據處理API中,具體咱們就不探討了。
本節介紹了Java 8中的一些新概念,包括Lambda表達式、函數式接口、方法引用、接口的靜態方法和默認方法等。
最重要的變化是,傳遞代碼變的簡單了,函數變爲了代碼世界的一等公民,能夠方便的被做爲參數傳遞,被做爲返回值,被複合利用以構建新的函數,看上去,這些只是語法上的一些小變化,但利用這些小變化,卻能使得代碼更爲通用、更爲靈活、更爲簡潔易讀,這,大概就是函數式編程的奇妙之處吧。
下一節,咱們來探討Java 8引入的函數式數據處理API,它們大大簡化了常見的集合數據操做。
(與其餘章節同樣,本節全部代碼位於 github.com/swiftma/pro…,位於包shuo.laoma.java8.c91下)
未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),從入門到高級,深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。