接口中的默認方法和靜態方法
先考慮一個問題,如何向Java中的集合庫中增加方法?例如在Java 8中向Collection接口中添加了一個forEach方法。
如果在Java 8之前,對于接口來說,其中的方法必須都為抽象方法,也就是說接口中不允許有接口的實現,那么就需要對每個實現Collection接口的類都需要實現一個forEach方法。
但這就會造成在給接口添加新方法的同時影響了已有的實現,所以Java設計人員引入了接口默認方法,其目的是為了解決接口的修改與已有的實現不兼容的問題,接口默認方法可以作為庫、框架向前兼容的一種手段。
默認方法就像一個普通Java方法,只是方法用default關鍵字修飾。
下面來舉一個簡單的例子
public interface Person {
//默認方法
default String getName(String name) {
return name;
}
}
///////////////////////////////////////////////////////////////////////
public class Student implements Person {
}
//////////////////////////////////////////////////////////////////////
public class Test {
public static void main(String[] args) {
Person p = new Student();
String name = p.getName("小李");
System.out.println(name);
}
}
我們定義了一個Person接口,其中getName是一個默認方法。接著編寫一個實現類,可以從結果中看到,雖然Student是空的,但是仍然可以實現getName方法。
顯然默認接口的出現打破了之前的一些基本規則,使用時要注意幾個問題。
考慮如果接口中定義了一個默認方法,而另外一個父類或者接口中又定義了一個同名的方法,該選擇哪個?
1.選擇父類中的接口。如果一個父類提供了具體的實現方法,那么接口中具有相同名稱和參數的默認方法會被忽略。
2.接口沖突。如果一個父接口提供了一個默認方法,而另一個接口也提供了具有相同名稱和參數類型的方法(不管該方法是否是默認方法),那么必須通過覆蓋方法來解決。
記住一個原則,就是“類優先”,即當類和接口都有一個同名方法時,只有父類中的方法會起作用。
“類優先”原則可以保證與Java 7的兼容性。如果你再接口中添加了一個默認方法,它對Java 8以前編寫的代碼不會產生任何影響。
下面來說說靜態方法。
靜態方法就像一個普通Java靜態方法,但方法的權限修飾只能是public或者不寫。
默認方法和靜態方法使Java的功能更加豐富。
在Java 8中Collection接口中就添加了四個默認方法,stream()、parallelStream()、forEach()和removeIf()。Comparator接口也增加了許多默認方法和靜態方法。
函數式接口和Lambda表達式
函數式接口(Functional Interface)是只包含一個方法的抽象接口。
比如Java標準庫中的java.lang.Runnable,java.util.concurrent.Callable就是典型的函數式接口。
在Java 8中通過@FunctionalInterface注解,將一個接口標注為函數式接口,該接口只能包含一個方法。
@FunctionalInterface注解不是必須的,只要接口只包含一個方法,虛擬機會自動判斷該接口為函數式接口。
一般建議在接口上使用@FunctionalInterface注解進行聲明,以免他人錯誤地往接口中添加新方法,如果在你的接口中定義了第二個抽象方法的話,編譯器會報錯。
函數式接口是為Java 8中的lambda而設計的,lambda表達式的方法體其實就是函數接口的實現。
為什么要使用lambda表達式?
“lambda表達式”是一段可以傳遞的代碼,因為他可以被執行一次或多次。我們先回顧一下之前在Java中一直使用的相似的代碼塊。
當我們在一個線程中執行一些邏輯時,通常會將代碼放在一個實現Runnable接口的類的run方法中,如下所示:
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 10; i++)
System.out.println("Without Lambda Expression");
}}).start();
然后通過創建實例來啟動一個新的線程。run方法內包含了一個新線程中需要執行的代碼。
再來看另一個例子,如果想利用字符串長度排序而不是默認的字典順序排序,就需要自定義一個實現Comparator接口的類,然后將對象傳遞給sort方法。
class LengthComparator implements Comparator<String> {
@Override
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
}
Arrays.sort(strings, new LengthComparator());
按鈕回調是另一個例子。將回調操作放在了一個實現了監聽器接口的類的一個方法中。
JButton button = new JButton("click");
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Without Lambda Expression");
}
});
這三個例子中,出現了相同方式,一段代碼被傳遞給其他調用者——一個新線程、是一個排序方法或者是一個按鈕。這段代碼會在稍后被調用。
在Java中傳遞代碼并不是很容易,不可能將代碼塊到處傳遞。你不得不構建一個類的對象,由他的某個方法來包含所需的代碼。
而lambda表達式實際上就是代碼塊的傳遞的實現。其語法結構如下:
(parameters) -> expression 或者 (parameters) -> {statements;}
括號里的參數可以省略其類型,編譯器會根據上下文來推導參數的類型,你也可以顯式地指定參數類型,如果沒有參數,括號內可以為空。
方法體,如果有多行功能語句用大括號括起來,如果只有一行功能語句則可以省略大括號。
new Thread(() -> {
for (int i = 0; i < 100; i++)
System.out.println("Lambda Expression");
}).start();
Comparator<String> c = (s1, s2) -> Integer.compare(s1.length(), s2.length());
button.addActionListener(e -> System.out.println("Lambda Expression"));
可以看到lambda表達式使代碼變得簡單,代替了匿名內部類。
下面來說一下方法引用,方法引用是lambda表達式的一種簡寫形式。 如果lambda表達式只是調用一個特定的已經存在的方法,則可以使用方法引用。
使用“::”操作符將方法名和對象或類的名字分隔開來。以下是四種使用情況:
- 對象::實例方法
- 類::靜態方法
- 類::實例方法
- 類::new
Arrays.sort(strings, String::compareToIgnoreCase);
// 等價于
Arrays.sort(strings, (s1, s2) -> s1.compareToIgnoreCase(s2));
上面的代碼就是第三種情況,對lambda表達式又一次進行了簡化。
Stream API
當處理集合時,通常會迭代所有元素并對其中的每一個進行處理。例如,我們希望統計一個字符串類型數組中,所有長度大于3的元素。
String[] strArr = { "Java8", "new", "feature", "Stream", "API" };
int count = 0;
for (String s : strArr) {
if (s.length() > 3)
count++;
}
通常我們都會使用這段代碼來統計,并沒有什么錯誤,只是它很難被并行計算。這也是Java8引入大量操作符的原因,在Java8中,實現相同功能的操作符如下所示:
long count = Stream.of(strArr).filter(w -> w.length() > 3).count();
stream方法會為字符串列表生成一個Stream。filter方法會返回只包含字符串長度大于3的一個Stream,然后通過count方法計數。
一個Stream表面上與一個集合很類似,允許你改變和獲取數據,但實際上卻有很大區別:
Stream自己不會存儲元素。元素可能被存儲在底層的集合中,或者根據需要產生出來。
Stream操作符不會改變源對象。相反,他們返回一個持有新結果的Stream。
-
Stream操作符可能是
延遲執行
的。意思是它們會等到需要結果的時候才執行。
Stream相對于循環操作有更好的可讀性。并且可以并行計算:
long count = Arrays.asList(strArr).parallelStream().filter(w -> w.length() > 3).count();
只需要把stream方法改成parallelStream,就可以讓Stream去并行執行過濾和統計操作。
Stream遵循“做什么,而不是怎么去做”的原則。只需要描述需要做什么,而不用考慮程序是怎樣實現的。
Stream很像Iterator,單向,只能遍歷一遍。但是Stream可以只通過一行代碼就實現多線程的并行計算。
當使用Stream時,會有三個階段:
- 創建一個Stream。
- 在一個或多個步驟中,將初始Stream轉化到另一個Stream的中間操作。
- 使用一個終止操作來產生一個結果。該操作會強制他之前的延遲操作立即執行。在這之后,該Stream就不會在被使用了。
從著三個階段來看,對應著三種類型的方法,首先是Stream的創建方法。
// 1. Individual values
Stream stream = Stream.of("a", "b", "c");
// 2. Arrays
String [] strArray = new String[] {"a", "b", "c"};
stream = Stream.of(strArray);
stream = Arrays.stream(strArray);
// 3. Collections
List<String> list = Arrays.asList(strArray);
stream = list.stream();
中間操作包括:map (mapToInt, flatMap 等)、 filter、distinct、sorted、peek、limit、skip、parallel、sequential、unordered。
終止操作包括:forEach、forEachOrdered、toArray、reduce、collect、min、max、count、anyMatch、allMatch、noneMatch、findFirst、findAny、iterator。