自主學習報告第一週
Java8新特性總覽
- 接口的默認方法
- Lambda表達式
- 函數式接口
- 方法引用
- 變量作用域
- 訪問局部變量
- 訪問對象字段與靜態變量
- 訪問接口默認方法
- Date API
- Annotation註解
接口的默認方法:從Java8開始,支持在抽象方法中定義非抽象方法,只需用default關鍵字將該方法聲明爲默認方法。
Lambda表達式: Lambda表達式是一個傳遞操作的表達式,實際上就是一個匿名函數,通過Lambda表達式可以省去很多多餘的步驟,簡化代碼,同時使代碼更易於閱讀。
假如我要創建一個線程,該線程用於打印”Hello,world”操作,傳統方法是寫一個類繼承Runable類重寫run方法,創建該類的對象,用這個對象創建一個線程,啓動線程打印“Hello,world”,示例代碼如下:
Class DoWork implement Runnable{
public void run(){
Systen.out.println("Hello,world");
}
}
啓動線程
new Thread(new DoWork()).start();
引入Lambda表達式之後我們有更簡單的方式實現它
new Thread(() -> System.out.println("Hello,world")).start;
通過這種方式我們可以很直觀地將原本涉及多個類的函數寫進一個方法裏面。這裏包含了我們下一節要講的內容:函數式接口。
函數式接口:只包含一個抽象方法的接口我們都稱爲函數式接口。上一節講的Runnable接口就是一個函數式接口,它包含一個run抽象方法。
predicate:接收一個參數,返回一個布爾值。
supplier:不接受任何參數,返回一個結果。
function:接收一個參數,返回一個結果。
comparator:接收兩個參數,返回一個布爾值。
comsumer:接收一個參數,不返回任何值,但可以對該參數對象的內部值進行修改。
下面對這些接口進行簡單的應用,模擬的場景是對工資的扣除個人所得稅操作、顯示信息操作以及工資排序操作,由於本人經驗嚴重不足,對場景的模擬和想象可能不能更加淋漓盡致地發揮函數式接口的作用,請多包涵。
/**
*
*/
package system;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import entity.Personal;
/*
*
* 工資 如果工資>=3500 扣除個稅
*
* predicate & function | predicate &function
*
* 顯示個人信息
*
* consumer
*
* 通過工資高低進行排序
*
* Comparator
*
*/
public class BankSystem {
public static void main(String[] args){
Personal pers = new Personal("Amy", 4000.0);
//通過function可以接受一個參數,返回一個結果,通過這種方法可以將對象讀入,改變對象的某個值再輸出
pers = doWorkAsFunction(pers, n -> n.getFirstSalary() >= 3500, n -> {n.setLastSalary(n.getFirstSalary()*0.8);return n;});
//通過consumer可以讀入一個參數,直接改變該參數的內部值。相對而言function更通用,它可以接收任意值返回任意值,而consumer主要用於改變對象的內部值
// doWorkAsConsumer(pers, n -> n.getFirstSalary() >= 3500, n -> n.setLastSalary(n.getFirstSalary()*0.9));
//展示個人信息
show(pers, n ->
{
System.out.println("name:" + n.getName());
System.out.println("firstSalary:" + n.getFirstSalary());
System.out.println("lastSalary:" + n.getLastSalary());
});
//按照工資高低進行排序
// Stream<Personal> persStream = Stream.of(new Personal("Amy", 1000), new Personal("Aya", 5000), new Personal("Boder", 2000), new Personal("BiBy" , 4000))
// .peek(m -> doWorkAsConsumer(m, n -> n.getFirstSalary() >= 3500, n -> n.setLastSalary(n.getFirstSalary()*0.9)));
Stream<Personal> persStream = Stream.of(new Personal("Amy", 1000), new Personal("Aya", 5000), new Personal("Boder", 2000), new Personal("BiBy" , 4000))
.map(m -> doWorkAsConsumer(m, n -> n.getFirstSalary() >= 3500, n -> n.setLastSalary(n.getFirstSalary()*0.9)));
persStream.sorted((p1, p2) -> Double.compare(p1.getLastSalary(), p2.getLastSalary())).forEach(n -> System.out.println(n.getName() + n.getLastSalary()));
}
public static Personal doWorkAsFunction(Personal pers, Predicate<Personal> perd, Function<Personal, Personal> func){
pers.setLastSalary(pers.getFirstSalary());
if(perd.test(pers)){
pers = func.apply(pers);
}
return pers;
}
public static Personal doWorkAsConsumer(Personal pers, Predicate<Personal> perd, Consumer<Personal> cons){
pers.setLastSalary(pers.getFirstSalary());
if(perd.test(pers)){
cons.accept(pers);
}
return pers;
}
public static void show(Personal pers, Consumer<Personal> coms){
coms.accept(pers);
System.out.println("以上爲您的個人信息");
}
}
從代碼上就可以看出來,Function接口和comsumer接口都可以對對象進行操作,Function接口主要是接受一個參數,返回一個結果,該結果不一定是對象本身,也可以是其他任意的泛型,因此相對於comsumer而言更加通用,而comsumer是針對於改變對象內部的值,因此在後面項目中,假如僅僅是需要改變對象的狀態就使用consumer吧。
展示信息部分明顯是錯誤使用的例子,通過這種方式只是想說明comsumer接受一個參數並不一定要改變該對象的狀態,也可以不對對象進行任何操作,不過這樣做的話comsumer就含無意義,只是增加冗餘代碼而已。
對工資進行排序操作部分的代碼涉及到後面要講的知識點,peek和map,這兩個都可以幫助改變流元素,他們的關係和comsumer和function的關係類似,peek不需要返回值,僅僅將函數應用於每個元素,而map需要返回一個結果,因此在使用map的時候假如刪去doWorkAsXxxx裏的return語句,編譯器會告訴你,不能返回一個void result。
此外包括原有接口,和爲Java8新增的接口,函數式接口還有很多,這裏只是舉例取出幾個比較常用的接口進行說明,其他接口只要瞭解接口用途,熟悉lambda表達式基本用起來都沒有任何問題。
方法引用:方法引用是通過::
引用方法或者構造器。每一個方法引用都是對Lambda表達式的一個簡化。例如String::CompareToIgnoreCase
就相當於(x, y) -> x.String.CompareToIgnoreCase(y)
。
方法引用有以下四種情況:
1.對象::實例方法
2. 類::靜態方法
3. 類::實例方法
4. 類::new
方法引用如何傳參?
這個問題困惑了我很久,看了很多博客,似乎都沒說到點上(或者是隱性說到了,愚鈍的我沒有察覺)。之後看到了某篇學習筆記突然恍然大悟。在糾結這個問題的時候,我們先要明確方法引用是什麼,方法引用不是調用一個方法,而是對Lambda表達式的一個簡化寫法。比如一個Lambda表達式
x -> System.out.println(x)
,通過方法引用可以表達爲System.out::println
,這樣一個表達式單獨存在是沒有任何意義的,我們知道目前Java8的Lambda表達式唯一的作用體現在函數式接口,所以任何一個Lambda表達式都必須顯性或者隱性地賦值給一個函數式接口,像上述的表達式,我們可以賦值給一個接收一個參數,不返回任何結果的函數式接口,consumer正好符合我們的要求,則Consumer cons = System.out::println
,獲得這樣一個接口之後要調用它的accept方法才能執行Lambda表達式裏的語句,調用方法cons.accept(參數)
,通過這一步,問題就迎刃而解了,參數是在最後調用接口方法的時候才傳進去的。跳出慣性思維來看問題就十分簡單,我們寫的Lambda表達式其實就是接口抽象方法的實現,因此參數的傳入就理應由接口抽象方法接收。所以當我們企圖通過::
引用方法的時候,就不得不找到合適的函數式接口。假如開發人員並沒有幫我們設計合適的函數式接口,我們也可以自己寫符合我們自己需求的函數式接口,不過我覺得這樣做並不能簡化開發,除非設計Java的大牛們已經寫好了滿足我們需求的所有函數式接口,否則我覺得這個設計並不能讓代碼看起來更簡潔,或者開發起來更高效,以上也是本人的一點拙見,若有愚鈍之處,請輕噴。此外,假如調用者本身能夠成爲參數,jvm也會幫我們把它作爲參數,這一點很難給出例子,只能在使用的時候體會出來。
下面是對方法引用的幾個小例子
//對象::實例方法
Consumer<String> cons = System.out::println;
cons.accept("Hello,world");
//類::靜態方法
Function<Integer, String> func = String::valueOf;
System.out.println(func.apply(100));
//類::實例方法
ICompare<String> Icom = String::compareToIgnoreCase;
System.out.println(Icom.comparing("aa", "abc"));
其中ICompare是自定義的函數式接口,其語法如下
public interface ICompare <T>{
public int comparing(T a, T b);
}
這些方法引用的方法主體被賦值給一個函數式接口,通過函數式接口可以被隨處調用。
下面是對構造器引用的一個例子
//構造器應用的一個用法
List<String> list = Arrays.asList("Aya", "Amy", "Ada", "Bob");
Object[] persList = list.stream().map(Personal::new).toArray();
for(Object p : persList){
System.out.println(((Personal)p).getName());
}
這裏用的還是上面工資的例子。上面我們在創建多個對象的時候是一個一個手動new出來的,在這裏我們可以直接實例化一個包含我們設定的人物的名字的字符串鏈表,通過map對每一個字符串進行實例化,在這裏即使Personal中有多個構造器,編譯器會幫我們選擇合適的構造器。
訪問局部變量
public static void doWork(String str, int count){
Runnable r = () -> {
for(int i=0; i<count; i++) System.out.println(str);
};
(new Thread(r)).start();
}
通過這種方式,我們訪問了原本並不定義在Lambda表達式內的變量,這樣訪問的約束跟匿名函數訪問的約束一樣,即訪問的變量必須是final變量,在Java8中對匿名函數訪問的變量的約束相對放寬,訪問變量可以不用final聲明,不過即使沒有final聲明,只要在匿名函數或者Lambda表達式中訪問到,該變量就被隱性地加上final聲明,成爲既成事實上的final變量。
像上面這個方法,當str和count被Lambda表達式捕獲,這兩個變量在Lambda表達式的作用域內(doWork方法內)就成爲了既成事實的final變量。在這個區域內這兩個變量一旦改變編譯器都會報出錯誤。有一個很有趣的細節,當我們在Lambda作用域內對str進行這樣的賦值str = str
編譯器也會報錯。說明底層對final變量的檢查是通過是否二次賦值來判斷的。所以即使只要在Lambda作用域內,哪怕是Lambda表達式之後,都會被判斷爲非不可變值變量。
當然,並不是毫無辦法改變final變量的值。上面我們說了,對final變量的檢查是通過檢查是否二次賦值來進行判斷的,那想要改變final變量的值,我們可以不改變變量的引用,而改變變量的狀態。假如我們要改變一個數值型的final變量,我們可以通過把變量放進一個長度爲1的數組,之後再Lambda表達式內不改變變量的引用,而改變數組內第一個索引的值。
public static void doWork(String str, int[] count){
count[0] = 3;
Runnable r = () -> {
for(int i=0; i<count[0]; i++) System.out.println(str);
};
(new Thread(r)).start();
}
假如你要改變字符串你可以將字符串封裝進對象,在Lambda作用域內不改變變量的引用,而改變對象的狀態。
public static void doWork(MyString str, int[] count){
count[0] = 3;
str.setString("Hahaha");
Runnable r = () -> {
for(int i=0; i<count[0]; i++){
System.out.println(str.getString());
}
};
(new Thread(r)).start();
}
通過這些取巧的方式,可以改變final變量的值,但這並不是線程安全的,因此在涉及線程的問題時,請三思而後行。
接口實現和父類繼承的衝突解決
在Java8中,允許在接口中定義默認方法或者靜態方法,就會導致一個問題的重新定義,就是“同名同參函數,誰優先”。
- 當某個類繼承了父類,同時實現了一個接口,父類與該接口存在同名同參函數,此時遵循“類優先”原則,即不論接口中的方法是否有實現,一律以父類方法爲繼承的方法。
- 當某個類同時實現兩個接口,這兩個接口存在同名同參函數,不論如何是否實現,都需要開發人員手動解決這種衝突,即重寫該方法,可以在重寫方法內指明實現那個方法。