在開發公司產品時,原先使用的是 Java 7,隨著架構越來越龐大,程式碼也越來越冗長,有時候寫起一些邏輯,或是想設計一些封裝時,經常受限於 Java 7 的語法結構,因此最後決定將產品的 Java 版本提升至 Java 8,換成 Java 8 後,不但開發速度變快,Bug 變少了 ,連考試都考了一百分呢! 因此想藉著這篇文章推廣 Java 8 的語法特性,讓還沒轉換至 Java 8,或者是已經轉換至 Java 8 卻較少使用 Java 8 的開發人員,可以一起來使用 Java 8 強大、方便的新功能!
Java 由 Sun 創造,為了讓對 Java 興趣的廠商、組織、開發者與使用者參與定義Java未來的功能與特性,Sun 公司於 1998 年組成了 JCP(Java Community Process),目的是讓 Java 演進由 Sun 非正式地主導,成為全世界數以百計代表成員公開監督的過程。
任何想要提議加入 Java 的功能或特性,必須以 JSR(Java Specification Requests) 正式文件的方式提交,JSR 必須經過 JCP 執行委員會投票通過後才可成為最終標準文件,有興趣的廠商或組織可以根據 JSR 實現產品。
若 JSR 成為最終文件後,必須根據 JSR 實作出免費且開發原始碼的參考實現,稱為 RI (Reference Implementation),並提供 TCK(Technology Compatibility Kit)作為技術相容測試工具箱,方便其它想根據 JSR 實現產品的廠商或組織參考與測試相容性。
在 JSR 所規範的標準之下,各廠商可以各自實作成品,所以同一份 JSR,可以有不同廠商的實作產品,以 Java SE 為例,對於身為開發人員,或使用 Java 開發產品的公司而言,只要是使用相容於標準的 JDK/JRE 開發產品,就可以執行於相容於標準的 JRE 上,而不用擔心跨平台的問題。
2006 年的 JavaOne 大會上,Sun 宣告對 Java 開放原始碼,從 JDK7 b10 開始有了OpenJDK,並於2009年4月15日正式發佈 OpenJDK。Oracle 時代發佈的 JDK7 正式版本,指定了OpenJDK7 為官方參考實作。
簡單來說,OpenJDK 是由 JDK 開源而來,JDK 常見的參考實作之一為 Oracle JDK,隨著 JDK 8 後 Oracle 的商業授權問題(Java依舊免費?)發生,陸續有其他廠商的 OpenJDK Builds 出現。
Java 版本命名規則為:Java [特性版本號碼]u[修補版本號碼],其中特性版本號碼異動代表有JVM、語言、程式庫等各種更新,而修補版本號碼異動則代表有一些安全性修補、效能增強或工具更新等。
Java 8 及更早版本中,安全性修補的釋出會是基於5的倍數,遇偶數加一,例如8u5、8u11、8u25、8u31等,特性釋出版本會是20的倍數,像是8u20、8u40、8u60等,則會包含先前的安全性修補,以及不影響規格書的特性;自 Java 9 開始,安全性修補版本改為每三個月釋出一次,而特性版本改為每六個月釋出一次。
其中特性版本也有區分是否為長期支援(Long-Term-Support, LTS) 版本,如 Java 9、10 都是短期支援版本,在下個小版本釋出之後,上個版本就不再維護,使用者要趕快更換至新版本,而長期支援版本除了顧名思義支援的時間較久外,同時也會包含前幾個特性版本的相關安全性修復,因此對企業而言,通常會採行目前的長期支援版本進行相關的系統開發維運。 目前 Java 的長期版本有 Java 8(Oracle 預計支援至 2030/12) 及 Java 11(Oracle 預計支援至 2026/09)。
Java 目前最新的版本為 Java 14,已引進許多新的語法特性,下一個長期版本則預計為 2021/09 發布的 Java 17。
Java 8 中新增了 java.util.Optional 類別,希望透過這個類別降低程式中發生 NullPointerException 的機率。
在下面的程式中,如果呼叫 getNickName() 時忘了檢查 null,那麼就會直接顯示 null,在這個簡單的例子中並不會怎樣,只是顯示結果令人困惑罷了,但如果後續的執行流程牽涉到至關重要的結果,程式快樂地繼續執行下去,錯誤可能到最後才會呈現發生。
public static void main(String[] args) {
String nickName = getNickName("Duke");
if (nickName == null) {
nickName = "Openhome Reader";
}
out.println(nickName);
}
private static String getNickName(String name) {
Map<String, String> nickNames = new HashMap<>(); // 假裝的鍵值資料庫
nickNames.put("Justin", "caterpillar");
nickNames.put("Monica", "momor");
nickNames.put("Irene", "hamimi");
return nickNames.get(name); // 鍵不存在時會傳回null
}
在日常的開發過程中,我們有時候需要對呼叫其他 method 取得的回傳值進行 null 檢驗,確保回傳值不為 null,而且經常會忘記做這件事情,而導致後續的例外產生,但有時候回傳值又真的有可能為 null,在這些情境下,針對回傳值的檢查顯得複雜又不易維護。
Java 8 中新增的 Optional 類別出現後,這個問題變得簡單許多,上面的程式改寫為 Optional 後,會變成下面的寫法:
public static void main(String[] args) {
Optional<String> nickNameOptional = getNickName("Duke");
String nickName;
if(nickNameOptional.isPresent()) {
nickName = nickNameOptional.get();
} else {
nickName = "Openhome Reader";
}
System.out.println(nickName);
}
private static Optional<String> getNickName(String name) {
Map<String, String> nickNames = new HashMap<>(); // 假裝的鍵值資料庫
nickNames.put("Justin", "caterpillar");
nickNames.put("Monica", "momor");
nickNames.put("Irene", "hamimi");
String nickName = nickNames.get(name);
if(nickName == null) {
return Optional.empty();
} else {
return Optional.of(nickName);
}
}
在 Java 8 Optional 的寫法中,getNickName 這個方法清楚定義了回傳的型別為 Optional,字面上的意思看起來像是「可選擇的字串」
,顧名思義就是代表這個方法回傳的字串可能有值,也可能沒有值,因此後續 main 方法接到回傳值後,可以很清楚的根據回傳值是否存在做後續的處理。而在有使用 Optional 的情境中,如果 getNickName 回傳的型別為 String,就代表這個方法不應該
會回傳 null,而是至少會回傳空字串,因此就不需要針對這個方法的回傳值進行 null check。
除了上述的寫法外,Optional 其實也可以在寫得更精簡一些,如:
public static void main(String[] args) {
Optional<String> nickNameOptional = getNickName("Duke");
System.out.println(nickNameOptional.orElse("Openhome Reader"));
}
private static Optional<String> getNickName(String name) {
Map<String, String> nickNames = new HashMap<>(); // 假裝的鍵值資料庫
nickNames.put("Justin", "caterpillar");
nickNames.put("Monica", "momor");
nickNames.put("Irene", "hamimi");
String nickName = nickNames.get(name);
return Optional.ofNullable(nickName);
}
在 getNickName 方法中,使用 Optional.ofNullable() 包裹住 nickName,如果 nickName 不是 null 就會呼叫 of() 方法,如果 nickName 是 null,則會呼叫 empty() 方法,從上述的程式結果來看,此處會呼叫 empty() 回傳空的 Optional。
而在 main 方法中,則是使用了 Optional.orElse() ,意思為:如果 Optional 不為 empty 則回傳 Optional 中的值,如果 Optional 為 empty 則回傳 orElse 中的值,從上述的程式結果來看,因為從 getNickName 取得的 nickNameOptional 為 empty Optional,因此 nickNameOptional.orElse("Openhome Reader") 實際上會得到 "Openhome Reader"。
Java 8 在介面上也引入了一些新的語言特性:
一般常見 java 的介面應該只會有方法的介面,或者是提供靜態方法讓呼叫端以靜態的方式呼叫,而當有類別實作某個介面時,就必須同時也實作該介面的所有方法介面,如:
public interface InterfaceA {
void foo();
static void bar() {
System.out.println("bar");
}
}
public class ClassA implements InterfaceA {
@Override
void foo() {
System.out.println("foo");
}
}
public class classB {
public void method1() {
// 一般呼叫
InterfaceA a = new ClassA();
a.foo();
// 靜態呼叫
InterfaceA.bar();
}
}
預設方法則允許在介面上直接新增方法的實作,實作該介面的類別可以不實作預設方法,也可以覆寫預設方法,呼叫端呼叫預設方法時就如同一般呼叫一樣簡單,如:
public interface InterfaceA {
void foo();
static void bar() {
System.out.println("bar");
}
default void baz() {
System.out.println("baz");
}
}
public class ClassA implements InterfaceA {
@Override
void foo() {
System.out.println("foo");
}
// 也可覆寫預設方法
// @Override
// void baz() {
// System.out.println("override baz");
// }
}
public class classB {
public void method1() {
// 一般呼叫
InterfaceA a = new ClassA();
a.foo();
// 靜態呼叫
InterfaceA.bar();
// 呼叫預設方法
a.baz();
}
}
在 Java 8 預設方法出現之前,介面與其實作類別之間的耦合度很高,當需要為一個介面新增方法時,所有的實作類別都必須隨之修改新增實作。預設方法可以為介面新增新的方法,而無須修改既有的介面的相關實作,因此不只解決了這個問題,同時也讓既有的舊介面保持了向後相容的彈性。
舉例來說,在 Java 8 中,Collection 或 List 繼承的 Iterable 介面原本沒有forEach() 方法,Java 8 為了增加 Lambda 語法的可用性,所以加入了 forEach() 方法。如果沒有預設方法,則實作此介面的類別將都必須實作forEach()方法,使得在擴展舊介面變得相對困難。
而介面提供靜態方法也是基於同一個目的 - 讓既有的舊介面保持了向後相容的彈性,透過介面新增靜態方法可以讓介面額外的方法,同時又不影響既有的實作類別,與預設方法不同的是,預設方法允許實作類別修改其行為,但靜態方法則不允許實作類別修改其行為。
參考 Java 8 的 Comparator Interface 片段:
public interface Comparator<T> {
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
@SuppressWarnings("unchecked")
public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
}
}
其中 reversed() 方法為預設方法,而 naturalOrder() 為靜態方法,呼叫端呼叫 reversed() 時,預期的是原先的排序會反轉,但有可能受到實作類別的不同而有不同的排序方式,因此這個方法做成預設方法,允許實作類別覆寫。而 naturalOrder 代表的是自然排序,應該不管是任何類別實作時都是預設使用 Object 的自然排序進行排序,不能讓實作類別覆寫,因此做成靜態方法。
在 Java 的世界中,允許多重實作,意即一個類別可以實作多個介面,如:
public interface InterfaceA {
void foo();
}
public interface InterfaceB {
void bar();
}
public class ClassC implements InterfaceA, InterfaceB {
@Override
public void foo() {
System.out.println("foo");
}
@Override
public void bar() {
System.out.println("bar");
}
}
那如果類別實作的多個介面中,恰巧存在 method signature 相同的預設方法呢?
public interface InterfaceA {
void foo();
default void baz() {
System.out.println("baz A");
}
}
public interface InterfaceB {
void bar();
default void baz() {
System.out.println("baz B");
}
}
當有類別同時實作具有相同預設方法的介面時,該類別必須一定要覆寫該預設方法,並可以如繼承般呼叫實作介面的預設方法,如:
public class ClassC implements InterfaceA, InterfaceB {
@Override
public void foo() {
System.out.println("foo");
}
@Override
public void bar() {
System.out.println("bar");
}
@Override
public void baz() {
InterfaceA.super.baz(); // 呼叫 InterfaceA 的 baz 方法
InterfaceB.super.baz(); // 呼叫 InterfaceB 的 baz 方法
System.out.println("ClassC baz"); // 做其他的事
}
}
在不同領域,對於 Lambda 的定義可能不太相同,但概念大致相同:Lambda 為一個函數,可以根據輸入的值,決定輸出的值,但 Lambda 與一般函數不同的是,Lambda 具有匿名性,意即我們不需要為 Lambda 函數命名,僅須關注 Lambda 的 input / logic / output 為何即可。
舉例來說,原先 Java 中的 Runnable 可以這麼實作:
Runnable myRunnable = new Runnable() {
@Override
public void run() {
System.out.println("run me!");
}
};
其中 Runnable 是個介面,並有唯一一個方法介面:void run()
,實作此介面時需要一併實作此方法介面。
從 Lambda 的精神來看,我們僅須關注 Runnable 的 input / logic / output:
Runnable runnbale = () -> System.out.println("run me!");
package com.kuma.demo;
public class LambdaDemo {
public static void main(String[] args) {
Runnable myRunnable = new Runnable() {
@Override
public void run() {
System.out.println("run me!");
}
};
Runnable myLambdaRunnable = () -> System.out.println("run me!");
myRunnable.run();
myLambdaRunnable.run();
}
}
經過編譯後得到的 ByteCode 如下:
public class com.kuma.demo.LambdaDemo {
public com.kuma.demo.LambdaDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/kuma/demo/LambdaDemo$1
3: dup
4: invokespecial #3 // Method com/kuma/demo/LambdaDemo$1."<init>":()V
7: astore_1
8: invokedynamic #4, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
13: astore_2
14: aload_1
15: invokeinterface #5, 1 // InterfaceMethod java/lang/Runnable.run:()V
20: aload_2
21: invokeinterface #5, 1 // InterfaceMethod java/lang/Runnable.run:()V
26: return
}
可以看到不使用 Lambda 時產生的 ByteCode 為:
0: new #2 // class com/kuma/demo/LambdaDemo$1
3: dup
4: invokespecial #3 // Method com/kuma/demo/LambdaDemo$1."<init>":()V
```
而使用 Lambda 時產生的 ByteCode 則僅僅只有
```
8: invokedynamic #4, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
文章前面提到的「只有一個方法的介面」一般被稱為 Single Abstract Method type(SAM),在 Java 8 以前就存在許多這樣的介面,在 Java 8 後,各式各樣的 SAM 更如雨後春筍般冒出,而 SAM 在 Java 8 有了一個更確切的名字,叫做「Functional Interface(函式介面)」。函式介面本質上就是介面,但這個介面要求僅具單一抽象方法,在 Java 8 中常見的 Functional Interface 如下:
public interface Consumer<T> {
void accept(T t);
}
Consumer 的行為是接受一個引數 T,處理後不回傳值
public interface Supplier<T> {
T get();
}
Supplier 的行為是沒有引數,處理後回傳值 T
public interface Function<T, R> {
R apply(T t);
}
Function 的行為是接受一個引數 T,處理後回傳值 R
public interface Predicate<T> {
boolean test(T t);
}
Predicate 的行為與 Function 類似,都是接受一個引數 T,處理後回傳值,差別在於 Predicate 的回傳型別為 boolean
除了上述提到 Function 的函式介面外,Java 8 還提供許多與基本型別操作有關的 Function 函式介面,如:
同時也針對會使用到引述的函式介面(Consumer, Function, Predicate) 等,提供了以 Bi 開頭的相關函式介面(BiConsumer, BiFunction, BiPredicate),讓原先的引數可以變為兩個,內容如下:
public interface BiConsumer<T, U> {
void accept(T t, U u);
}
BiConsumer 的行為是接受兩個引數 T, U,處理後不回傳值
public interface BiFunction<T, U, R> {
R apply(T t, U u);
}
BiFunction 的行為是接受兩個引數 T, U,處理後回傳值 R
public interface BiPredicate<T, U> {
boolean apply(T t, U u);
}
BiPredicate 的行為是接受兩個引數 T, U,處理後回傳 boolean
原則上在一般日常的使用情境中,上述的函式介面應該以充分夠用,如果現在有一個需求是需要提供一個傳入六個引數,回傳一個值的 Function 呢?在這之前應該要先想想,是要傳入六個引數,還是重構一下將六個引數封裝後,改為使用 Function。
如果有這種需求,其實只需要建立一個函式介面如下即可:
@FunctionalInterface
public interface SixParametersFunction<A, B, C, D, E, F, R> {
R apply(A a, B b, C c, D d, E e, F f);
}
Lambda 的語法結構,簡而言之可以表示為:
input -> body
其中 input 和 body 都各有多種寫法,列舉如下:
()
: 沒有引數(X x)
: 一個 X 型別的引數(x)
: 一個引數,省略型別(X x, Y y)
: 兩個引數,分別為 X 和 Y 型別(x, y)
: 兩個引數,省略型別{}
: 什麼都不做{System.out.println("foo")}
: 單行不回傳值x + y
: 單行回傳值{ System.out.println("NO"); System.out.println("NO2"); }
{ x++; y++; return x + y; }
Supplier<String> emptyStringSupplier = () -> "";
Consumer<String> printingConsumer = x -> System.out.println(x);
Function<Integer, Integer> incrementFunction = x -> x++;
Predicate<Integer> evenPredicate = x -> x % 2 == 0;
BiConsumer<String, String> printingAllConsumer = (x,y) -> {
System.out.println(x);
System.out.println(y);
};
BiFunction<Integer, Integer, Integer> sumFunction = (x, y) -> x + y;
BiPredicate<Integer, Integer> allEvenPredicate = (x, y) -> x % 2 == 0 && y % 2 == 0;
而六個引數的函式介面可以用 Lambda 實作如下:
SixParametersFunction<Integer, Integer, Integer, Integer, Integer, Integer, Integer> sumSixFunction = (a,
b, c, d, e, f) -> a + b + c + d + e + f;
在 Java 8 加入了 Lambda 的語法特性後,除了原先 Lambda 的寫法外,還可以進一步改用 Method Reference(方法參考/方法引用)的寫法。
如使用 Lambda 的寫法可以依下列例子改為 MethodReference 的寫法:
List<String> strList = Arrays.asList("A","B","C");
// Lambda
strList.forEach(s -> System.out.print(s));
// MethodReference
strList.forEach(System.out::print);
使用 Method Reference 最大的好處在於語法精簡很多,原先在 Lambda 中的寫法雖然已經比起以往精簡許多,但是 Method Reference 還能在這個基礎上更加精簡,讓開發人員在開發時更能聚焦在商業邏輯而非語法上。
如:
// Lambda
Arrays.sort(stringArray, (o1, o2) -> o1.compareTo(o2));
// MethodReference
Arrays.sort(stringArray, String::compareTo);
List<String> messages = Arrays.asList("hello", "baeldung", "readers!");
// Lambda
messages.forEach(word -> StringUtils.capitalize(word));
// MethodReference
messages.forEach(StringUtils::capitalize);
BicycleComparator bikeFrameSizeComparator = new BicycleComparator();
// Lambda
createBicyclesList().stream()
.sorted((a, b) -> bikeFrameSizeComparator.compare(a, b));
// MethodReference
createBicyclesList().stream()
.sorted(bikeFrameSizeComparator::compare);
public Bicycle(String brand) {
this.brand = brand;
this.frameSize = 0;
}
List<String> bikeBrands = Arrays.asList("Giant", "Scott", "Trek", "GT");
// Lambda
bikeBrands.stream()
.map(brand -> new Bicycle(brand));
// MethodReference
bikeBrands.stream()
.map(Bicycle::new);
createBicyclesList().stream()
.sorted(bikeFrameSizeComparator::compare);
Java 8 新增了一個新的 Stream package 專門用來處理集合(collection),搭配 Lambda 語法,在處理集合時能讓邏輯變得更清楚,效率更好。
在過去沒有 Stream 時,我們操作一個集合時可能會這樣寫:
Set<Integer> numbers = Sets.newHashSet(1, 2, 3, 4);
Set<Integer> oddNumbers = findOddNumbers(numbers);
pubilc Set<Integer> findOddNumbers(Set<Integer> numbers) {
Set<Integer> oddNumbers = Sets.newHashSet();
for(Integer number : numbers) {
if(number % 2 != 0) {
oddNumbers.add(number);
}
}
}
雖然這種寫法已行之有年,但這樣寫應該會有一些問題:
過濾數字
和 加入數字到新容器中
這兩件事Stream 出現後,大幅改善了這些問題,上面那段程式可以用 Stream 語法改寫如下:
Set<Integer> numbers = Sets.newHashSet(1, 2, 3, 4);
Set<Integer> oddNumbers = findOddNumbers(numbers);
pubilc Set<Integer> findOddNumbers(Set<Integer> numbers) {
return numbers.stream()
.filter(number -> number % 2 != 0)
.collect(Collectors.toSet());
}
上面的語法用口語來說可以解釋成:把一個 Collection 的數字轉為 Stream 後,過濾數字留下奇數,並將奇數收集為一個 Set
。
以往在使用迴圈時,開發人員經常混合使用 if 判斷、型別轉換,甚至是很多 continue、break、return 等,由於邏輯複雜,容易使迴圈的內容快速膨脹,造成後續維護上很大的負擔,不但迴圈的內容很難理解,更怕改壞。
改成用 Stream 語法後,最大的好處在於迴圈中的邏輯容易獨立,每個操作彼此獨立,不用擔心改壞其他部份,整體而言語法簡潔許多,可讀性也提高許多。
一般而言,Stream 的語法如下:Collection[.Stream Operation][.Intermediate Operation][.Intermediate Operation]...[.Terminal Operation]
將 Collection Stream 化後,可以進行任意個中間操作(Intermediate Operation),最後可以執行最終操作(Terminal Operation)。
需注意的是,一旦 Stream 進行過最終操作後,Stream 就會被關閉而無法重新使用。
List<Integer> numbers = Lists.newArrayList(1, 2, 3, 4);
Stream<Integer> oddNumberStream = numbers.stream().filter(number -> number % 2 != 0);
long count = oddNumberStream.count();
List<Integer> oddNumbers = oddNumberStream.collect(Collectors.toList());
如上述程式,在 count Stream 後,如果又使用原本的 Stream 進行操作,則會拋出 java.lang.IllegalStateException: stream has already been operated upon or closed
的例外。