1. Lambda表達式
函數(shù)式接口
偎血,如OnClickListener接口只有一個方法颇玷,Java中大多數(shù)回調(diào)接口都有這個特征:比如Runnable和Comparator就缆;我們把這些只擁有一個方法的接口稱之為函數(shù)式接口
违崇。
Lambda表達式(也稱為閉包)允許把函數(shù)作為一個方法的參數(shù)(函數(shù)作為參數(shù)傳遞進方法中)羞延,或者把代碼看成數(shù)據(jù):函數(shù)式程序員對這一概念非常熟悉。
Lambda表達式是匿名方法入愧。
(int x, int y) -> x + y //接收x和y兩個整形參數(shù)并返回他們的和
() -> 66 //不接收任何參數(shù)直接返回66
(String name) -> {System.out.println(name);} //接收一個字符串然后打印出來
(View view) -> {view.setText("lalala");} //接收一個View對象并調(diào)用setText方法
Lambda表達式語法由參數(shù)列表
棺蛛、->
和函數(shù)體
組成旁赊。函數(shù)體既可以是一個表達式也可以是一個代碼塊终畅。
-
表達式:表達式會被執(zhí)行然后返回結(jié)果。它簡化掉了
return
關(guān)鍵字杖狼。 - 代碼塊:顧名思義就是一坨代碼蝶涩,和普通方法中的語句一樣绿聘。
目標類型--上下文推導
通過前面的例子我們可以看到斜友,lambda表達式?jīng)]有名字垃它,那我們怎么知道它的類型呢国拇?答案是通過上下文推導而來的酱吝。
OnClickListener listener = (View v) -> {v.setText("lalala");};
Runnable runnable = () -> doSomething(); //這個表達式是Runnable類型的
Callback callback = () -> doSomething(); //這個表達式是Callback類型的
編譯器利用lambda表達式所在的上下文所期待的類型來推導表達式的類型务热,這個被期待的類型被稱為目標類型
山孔。lambda表達式只能出現(xiàn)在目標類型為函數(shù)式接口
的上下文中捷雕。
Lambda表達式的類型和目標類型的方法簽名必須一致,編譯器會對此做檢查濒憋,一個lambda表達式要想賦值給目標類型
T
則必須滿足下面所有的條件:
T
是一個函數(shù)式接口- lambda表達式的參數(shù)必須和
T
的方法參數(shù)在數(shù)量凛驮、類型和順序上一致(一一對應)- lambda表達式的返回值必須和
T
的方法的返回值一致或者是它的子類- lambda表達式拋出的異常和
T
的方法的異常一致或者是它的子類
由于目標類型是知道lambda表達式的參數(shù)類型辐烂,所以我們沒必要把已知的類型重復一遍纠修。也就是說lambda表達式的參數(shù)類型可以從目標類型獲瓤鄄荨:
//編譯器可以推導出s1和s2是String類型
Comparator<String> c = (s1, s2) -> s1.compareTo(s2);
//當表達式的參數(shù)只有一個時括號也是可以省略的
button.setOnClickListener(v -> v.setText("lalala"));
作用域--this仍指向外部環(huán)境
在內(nèi)部類中使用變量名和this非常容易出錯辰妙。內(nèi)部類通過繼承得到的成員變量(包括來說object的)可能會把外部類的成員變量覆蓋掉甫窟,未做限制的this引用會指向內(nèi)部類自己而非外部類粗井。
而lambda表達式的語義就十分簡單:基于詞法作用域的理念浇衬,它不會從父類中繼承任何變量耘擂,也不用引入新的作用域。lambda表達式的參數(shù)及函數(shù)體里面的變量和它外部環(huán)境的變量具有相同的語義(this關(guān)鍵字也是一樣)秩霍。
變量捕獲
在Java7中铃绒,編譯器對內(nèi)部類中引用的外部變量(即捕獲的變量)要求非常嚴格:如果捕獲的變量沒有被聲明為final
就會產(chǎn)生一個編譯錯誤匿垄。但是在Java8中放寬了這一限制--對于lambda表達式和內(nèi)部類椿疗,允許在其中捕獲那些符合有效只讀的局部變量(如果一個局部變量在初始化后從未被修改過届榄,那么它就是有效只讀)。
Runnable getRunnable(String name){
String hello = "hello";
return () -> System.out.println(hello + "," + name);
}
對于this
的引用以及通過this
對未限定字段的引用和未限定方法的調(diào)用本質(zhì)上都屬于使用final
局部變量。包含此類引用的lambda表達式相當于捕獲了this
實例贤壁。在其他情況下埠忘,lambda對象不會保留任何對this
的應用莹妒。
這個特性對內(nèi)存管理是極好的:要知道在java中一個非靜態(tài)內(nèi)部類會默認持有外部類實例的強引用旨怠,這往往會造成內(nèi)存泄露鉴腻。而在lambda表達式中如果沒有捕獲外部類成員則不會保留對外部類實例的引用拘哨。
不過盡管Java8放寬了對捕獲變量的語法限制倦青,但試圖修改捕獲變量的行為是被禁止的产镐,比如下面這個例子就是非法的:
int sum = 0;
list.forEach(i -> {sum += i;});
為什么要禁止這種行為呢踢步?因為這樣的lambda表達式很容易引起race condition获印,可以使用Stream API
來實現(xiàn)這種行為。
2. 方法引用
lambda表達式允許我們定義一個匿名方法唆缴,并以函數(shù)式接口的方式使用它面徽。Java8能夠在已有的方法上實現(xiàn)同樣的特性趟紊。
方法引用和lambda表達式擁有相同的特性(他們都需要一個目標類型霎匈,并且需要被轉(zhuǎn)化為函數(shù)式接口的實例),不過我們不需要為方法引用提供方法體唧躲,我們可以直接通過方法名引用已有方法弄痹。
以下面的代碼為例肛真,假設我們要按照userName
排序
class User{
private String userName;
public String getUserName() {
return userName;
}
...
}
List<User> users = new ArrayList<>();
Comparator<User> comparator = Comparator.comparing(u -> u.getUserName());
Collections.sort(users, comparator);
我們可以用方法引用替換上面的lambda表達式
Comparator<User> comparator = Comparator.comparing(User::getUserName);
這里的User::getUserName
被看做是lambda表達式的簡寫形式蚓让。盡管方法引用不一定會把代碼變得更緊湊历极,但它擁有更明確的語義--如果我們想要調(diào)用的方法擁有一個名字衷佃,那么我們就可以通過方法名調(diào)用它氏义。
方法引用有很多種惯悠,它們的語法如下:
- 靜態(tài)方法引用:ClassName::methodName
- 實例上的實例方法引用:instanceReference::methodName
- 超類上的實例方法引用:super::methodName
- 類型上的實例方法引用:ClassName::methodName
- 構(gòu)造方法引用:Class::new
- 數(shù)組構(gòu)造方法引用:TypeName[]::new
方法引用提供了非常有用的語法筒严,可以直接引用已有Java類或?qū)ο螅▽嵗┑姆椒ɑ驑?gòu)造器。與lambda聯(lián)合使用嘀掸,方法引用可以使語言的構(gòu)造更緊湊簡潔睬塌,減少冗余代碼歇万。
下面揩晴,我們以定義了4個方法的Car這個類作為例子,區(qū)分Java中支持的4種不同的方法引用贪磺。
`public` `static` `class` `Car {`` ``public` `static` `Car create( ``final` `Supplier< Car > supplier ) {`` ``return` `supplier.get();`` ``} `` ` ` ``public` `static` `void` `collide( ``final` `Car car ) {`` ``System.out.println( ``"Collided "` `+ car.toString() );`` ``}`` ` ` ``public` `void` `follow( ``final` `Car another ) {`` ``System.out.println( ``"Following the "` `+ another.toString() );`` ``}`` ` ` ``public` `void` `repair() { `` ``System.out.println( ``"Repaired "` `+ ``this``.toString() );`` ``}``}`
`Collided com.javacodegeeks.java8.method.references.MethodReferences$Car``@7a81197d``Repaired com.javacodegeeks.java8.method.references.MethodReferences$Car``@7a81197d``Following the com.javacodegeeks.java8.method.references.MethodReferences$Car``@7a81197d`
關(guān)于方法引用的更多詳情請參考官方文檔硫兰。
3. 默認方法
在Java中一個接口一旦發(fā)布就已經(jīng)被定型,除非我們能夠一次性的更新所有該接口的實現(xiàn)寒锚,否者在接口的添加新方法將會破壞現(xiàn)有接口的實現(xiàn)劫映。默認方法就是為了解決這一問題的,這樣接口在發(fā)布之后依然能夠繼續(xù)演化泳赋。
默認方法就是向接口增加新的行為。它是一種新的方法:接口方法可以是抽象的或者是默認的喇喉。默認方法擁有默認實現(xiàn)祖今,接口實現(xiàn)類通過繼承得到該默認實現(xiàn)。默認方法不是抽象的拣技,所以我們可以放心的向函數(shù)式接口里增加默認方法千诬,而不用擔心函數(shù)式接口單抽象方法的限制。
public interface Iterator<E> {
boolean hasNext();
E next();
//默認方法
default void remove() {
throw new UnsupportedOperationException("remove");
}
//默認方法
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
和其他方法一樣膏斤,默認方法也可以被繼承徐绑。
除了上面看到的默認方法,Java8中還允許我們在接口中定義靜態(tài)方法莫辨。這使得我們可以從接口中直接調(diào)用它相關(guān)的輔助方法傲茄,而不是從其它的輔助類中調(diào)用(如Collections)。在做集合中元素比較的時候衔掸,我們一般需要使用靜態(tài)輔助方法生成實現(xiàn)Comparator的比較器烫幕,在Java8中我們可以直接把該靜態(tài)方法定義在Comparator接口中:
//生成實現(xiàn)Comparator的比較器
public static <T, U extends Comparable<? super U>>
Comparator<T> comparing(Function<T, U> keyExtractor) {
return (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
4. Stream-集合功能增強
Stream作為Java8的新特性俺抽,主要用于對集合對象進行各種非常便利高效的聚合和大批量數(shù)據(jù)的操作敞映。結(jié)合Lambda表達式可以極大的提高開發(fā)效率和代碼可讀性。
假設我們需要把一個集合中的所有形狀設置成紅色磷斧,那么我們可以這樣寫
//以前的寫法振愿,外部迭代
for (Shape shape : shapes){
shape.setColor(RED)
}
如果使用Java8擴展后的集合框架則可以這樣寫:
//java8 stream 捷犹,內(nèi)部迭代
shapes.foreach(s -> s.setColor(RED));
什么是Stream
像一個更高級的Interator
,Stream提供了強大的數(shù)據(jù)集合操作功能冕末,并被深入整合到現(xiàn)有的集合類和其它的JDK類型中萍歉。流的操作可以被組合成流水線(Pipeline)。
//只改變藍色的變?yōu)榧t色
shapes.stream()//生成該集合元素的流
.filter(s -> s.getColor() == BLUE)//只包含藍色形狀的流
.forEach(s -> s.setColor(RED));
//把藍色的形狀提取到新的List里
List<Shape> blue = shapes.stream()
.filter(s -> s.getColor() == BLUE)
.collect(Collectors.toList());//把其接收的元素聚集到一起(這里是List)
//`collect()`方法的參數(shù)被用來指定如何進行聚集操作,`toList()`以把元素輸出到List中
如果每個形狀都被保存在Box
里档桃,然后我們想知道哪個盒子至少包含一個藍色形狀枪孩,我們可以這么寫:
Set<Box> hasBlueShape = shapes.stream()
.filter(s -> s.getColor() == BLUE)
.map(s -> s.getContainingBox())//shape 里面有個getContainingBox()
.collect(Collectors.toSet());
map()
操作通過映射函數(shù)(這里的映射函數(shù)接收一個形狀,然后返回包含它的盒子)對輸入流里面的元素進行依次轉(zhuǎn)換藻肄,然后產(chǎn)生新流蔑舞。
如果我們需要得到藍色物體的總重量,我們可以這樣表達:
int sum = shapes.stream()
.filter(s -> s.getColor() == BLUE)
.mapToInt(s -> s.getWeight())//shape 里面有個getWeight()嘹屯,返回值是int類型
.sum();
Stream vs Collection
流(Stream)和集合(Collection)的區(qū)別:
- Collection主要用來對元素進行管理和訪問攻询;
- Stream并不支持對其元素進行直接操作和直接訪問,而只支持通過聲明式操作在其之上進行運算后得到結(jié)果州弟;
- Stream不存儲值
- 對Stream的操作會產(chǎn)生一個結(jié)果钧栖,但是Stream并不會改變數(shù)據(jù)源;
- 大多數(shù)Stream的操作(filter,map,sort等)都是以惰性的方式實現(xiàn)的婆翔。這使得我們可以使用一次遍歷完成整個流水線操作,并可以用短路操作提供更高效的實現(xiàn)拯杠。
惰性求值 vs 急性求值
filter()
和map()
這樣的操作既可以被急性求值,也可以被惰性求值啃奴,在實際中進行惰性運算可以帶來很多好處阴挣。
惰性運算(延遲運算)實際中可以帶來很多好處,比如說纺腊,如果我們進行惰性過濾畔咧,我們就可以把過濾和流水線里的其它操作混合在一起,從而不需要對數(shù)據(jù)進行多遍遍歷揖膜。相類似的誓沸,如果我們在一個大型集合里搜索第一個滿足某個條件的元素,我們可以在找到后直接停止壹粟,而不是繼續(xù)處理整個集合拜隧。(這一點對無限數(shù)據(jù)源是很重要,惰性求值對于有限數(shù)據(jù)源起到的是優(yōu)化作用趁仙,但對無限數(shù)據(jù)源起到的是決定作用洪添,沒有惰性求值,對無限數(shù)據(jù)源的操作將無法終止)
對于filter()
和map()
這樣的操作雀费,我們很自然的會把它當成是惰性求值操作干奢,不過它們是否真的是惰性取決于它們的具體實現(xiàn)。另外盏袄,像sum()
這樣生成值的操作和forEach()
這樣產(chǎn)生副作用的操作都是天然急性求值忿峻,因為它們必須要產(chǎn)生具體的結(jié)果薄啥。
我們拿下面這段代碼舉例:
int sum = shapes.stream()
.filter(s -> s.getColor() == BLUE)
.mapToInt(s -> s.getWeight())
.sum();
這里的filter()
和map()
都是惰性的,這就意味著在調(diào)用sum()
之前不會從數(shù)據(jù)源中提取任何元素逛尚。在sum()
操作之后才會把filter()
垄惧、map()
和sum()
放在對數(shù)據(jù)源一次遍歷中。這樣可以大大減少維持中間結(jié)果所帶來的開銷绰寞。
例
假設我們有一個房源庫項目到逊,這個房源庫中有一系列的小區(qū),每個小區(qū)都有小區(qū)名和房源列表滤钱,每套房子又有價格蕾管、面積等屬性。現(xiàn)在我們需要篩選出含有100平米以上房源的小區(qū)菩暗,并按照小區(qū)名排序掰曾。
我們先來看看不用Streams API如何實現(xiàn):
List<Community> result = new ArrayList<>();
for (Community community : communities) {
for (House house : community.houses) {
if (house.area > 100) {
result.add(community);
break;
}
}
}
Collections.sort(result, new Comparator<Community>() {
@Override
public int compare(Community c1, Community c2) {
return c1.name.compareTo(c2.name);
}
});
return result;
如果使用Streams API:
return communities.stream()
.filter(c -> c.houses.stream().anyMatch(h -> h.area>100))
.sorted(Comparator.comparing(c -> c.name))
.collect(Collectors.toList());
以后所有的集合操作,全都都要轉(zhuǎn)成stream操作MM拧?跆埂!
5. 重復注解 -- 編碼中不會直接用到
自從Java 5引入了注解機制佑稠,這一特性就變得非常流行并且廣為使用秒梅。然而,使用注解的一個限制是相同的注解在同一位置只能聲明一次舌胶,不能聲明多次捆蜀。Java 8打破了這條規(guī)則,引入了重復注解機制幔嫂,這樣相同的注解可以在同一地方聲明多次辆它。
重復注解機制本身必須用@Repeatable注解。事實上履恩,這并不是語言層面上的改變锰茉,更多的是編譯器的技巧,底層的原理保持不變切心。讓我們看一個快速入門的例子:
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public class RepeatingAnnotations {
/*注解類*/
/*Filters僅僅是Filter注解的數(shù)組飒筑,但程序員不會顯示的調(diào)用該注解,Java編譯器并不想讓程序員意識到Filters的存在*/
@Target( ElementType.TYPE )
@Retention( RetentionPolicy.RUNTIME )
public @interface Filters {
Filter[] value();
}
@Target( ElementType.TYPE )
@Retention( RetentionPolicy.RUNTIME )
//重復該注解時绽昏,自動替換為Filters注解
@Repeatable( Filters.class )
public @interface Filter {
String value();
}
@Filter( "filter1" )
@Filter( "filter2" )
public interface Filterable {
}
public static void main(String[] args) {
for( Filter filter: Filterable.class.getAnnotationsByType( Filter.class ) ) {
System.out.println( filter.value() );
}
}
}
正如我們看到的协屡,這里有個使用@Repeatable( Filters.class )注解的注解類Filter,F(xiàn)ilters僅僅是Filter注解的數(shù)組全谤,但Java編譯器并不想讓程序員意識到Filters的存在肤晓。這樣,接口Filterable就擁有了兩次Filter(并沒有提到Filters)注解。
同時材原,反射相關(guān)的API提供了新的函數(shù)getAnnotationsByType()來返回重復注解的類型(請注意Filterable.class.getAnnotation( Filters.class )經(jīng)編譯器處理后將會返回Filters的實例)沸久。
程序輸出結(jié)果如下:
filter1
filter2
6. Android Studio中應用java8
Jack(Java Android Compiler Kit)
要想在Android項目中使用Java8的新特性季眷,要采用新的Jack(Java Android Compiler Kit)編譯余蟹。新的 Android 工具鏈將 Java 源語言編譯成 Android 可讀取的 Dalvik 可執(zhí)行文件字節(jié)碼,且有其自己的 .jack 庫格式子刮,在一個工具中提供了大多數(shù)工具鏈功能:重新打包威酒、壓縮、模糊化以及 Dalvik 可執(zhí)行文件分包挺峡。
以下是構(gòu)建 Android Dalvik 可執(zhí)行文件可用的兩種工具鏈的對比:
- 舊版 javac 工具鏈:
javac (.java --> .class) --> dx (.class --> .dex)
- 新版 Jack 工具鏈:
Jack (.java --> .jack --> .dex)
配置
為了在項目中使用Java8葵孤,我們還需要項目module中的gradle.build文件中加入如下代碼:
android {
defaultConfig {
****
jackOptions {
enabled true
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
如果你項目的minSdkVersion>=24,還可以使用Stream API橱赠。