Java8 新特性
Java 8 的革新之路
自 1995 年首次發布以來,Java 已經成為世界上最廣泛使用的編程語言之一。隨著時間的推移,Java 經歷了多次版本更新,其中最具里程碑意義的便是 Java 8 的發布。這個版本引入了許多重大變革,包括 Lambda 表達式、Stream API 和函數式編程等新特性。這些變化不僅提升了 Java 開發者的生產力,還使得 Java 在性能、簡潔性和可讀性方面達到了新的高度。
本文將深入探討 Java 8 的主要新特性,并通過示例代碼和實際應用案例幫助你理解這些特性的優勢和使用場景。無論你是 Java 初學者還是經驗豐富的開發者,這篇文章都將為你提供一個全面了解 Java 8 新特性的平臺,幫助你在日常開發中更高效地利用這些功能。
讓我們一起踏上 Java 8 的革新之旅,探索如何通過這些新特性提高我們的代碼質量和開發效率。
主要內容
- Lambda 表達式(核心)
- 函數式接口
- 方法引用與構造器引用
- Stream API
- 新時間日期 API
- 接口中默認方法與靜態方法
- 其他新特性
新特性簡介
- 速度更快
- 代碼更少(簡潔,增加了新的語法 Lambda 表達式)
- 強大的 Stream API
- 便于并行
- 可以最大化減少空指針異常 Optional – 因為這個類的方法
為什么便于運行?
三點原因:
Stream API
可以讓我們輕松地對集合進行并行處理。Lambda表達式
可以讓我們以一種簡潔的方式定義匿名函數,這種方式可以使代碼更加簡潔。- 方式引用已經存在的方法或構造函數,可以避免我們重復編寫相似的代碼。
綜上所述,Java8 新特性便于并行的原因是,它們可以讓我們以一種簡潔的方式處理集合數據,同時充分利用多核 CPU 的優勢,從而提高程序的執行效率。
1、Lambda 表達式
何為 Lambda 表達式?
Lambda 是一個匿名函數,作為一種更緊湊的代碼風格,使 Java 的語言表達能力得到了提升,可以寫出更簡潔、更靈活的代碼。
代碼示例
Runnable
接口,從匿名內部類到 Lambda 的轉換**(無參)**
// 匿名內部類
Runnable r1 = new Runnable() {@Overridepublic void run() {System.out.println("Hello World!");}
};// Lambda 表達式
Runnable r1 = () -> System.out.println("Hello Lambda!");
Comparator
接口,參數傳遞 – 從匿名內部類到 Lambda 的轉換**(有參)**
// 使用匿名內部類作為參數傳遞
TreeSet<String> ts = new TreeSet<>(new Comparator<String>() {@Overridepublic int compare(String o1, String o2) {return Interger.compare(o1.length(), o2.length());}
});// 使用 Lambda 表達式作為參數傳遞
TreeSet<String> ts2 = new TreeSet<>((o1, o2) -> Integer.compare(o1.length(), o2.length()) // 升序排序
);
Listener
接口
JButton button = new JButton();
button.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {e.getItem();
}
});// Lambda
button.addItemListener(e -> e.getItem());
語法
-> 箭頭操作符簡介
Lambda 表達式在 Java 語言中引入了一個新的語法元素和操作符。這個操作符為 “->”,該操作符被稱為 Lambda 操作符或箭頭操作符。
它將 Lambda 分為兩個部分:
- 左側:指定了 Lambda 表達式需要的所有參數
- 右側:指定了 Lambda 體,即 Lambda 表達式要執行的功能
六種格式
格式一:
無參,無返回值,Lambda 體只需要一條語句即可
Runnable r1 = () -> System.out.println("Hello Lambda!");
格式二:
Lambda 需要一個參數
Consumer<String> fun = (args) -> System.out.println(args);
格式三:
Lambda 只需要一個參數時,參數的小括號可以省略
Consumer<String> fun = args -> System.out.println(args);
格式四:
Lambda 需要兩個參數,并且有返回值時
BinaryOperator<Long> bo = (x, y) -> {System.out.println("實現函數接口方法!");return x + y;
};
格式五:
當 Lambda 體只有一條語句時,return 與大括號可以省略
BinaryOperator<Long> bo = (x, y) -> x + y;
格式六:
帶參數的數據類型 – 數據類型可以省略(如上所示),因為可由編譯器推斷得出,稱為“類型推斷”
BinaryOperator<Long> bo = (Long x, Long y) -> {System.out.println("實現函數接口方法!");return x + y;
};
類型推斷
所謂的“類型推斷”是指:Lambda 表達式中的參數類型依賴于上下文環境,都是由編譯器推斷得出的。
在 Lambda 表達式中無需指定類型,程序依然可以編譯,這是因為 javac 根據程序的上下文,在后臺推斷出了參數的類型。
總結
大致可分為三種類型:
- 格式一為一種 – 無需參數,無返回值,Lambda 體一條語句即可
- 格式二、三為一種 – 需要一個參數,無返回值,可省略參數小括號,也是一條語句即可
- 格式四、五、六為一種 – 需要兩種參數,有返回值,可以省略大括號與 return
參數類型均可省略,推薦使用一、二、五
2、函數式接口
何為函數式接口?
-
函數式接口是指:只包含一個抽象方法的接口,該接口的對象可以通過 Lambda 表達式來創建。
(若 Lambda 表達式拋出一個受檢異常,那么該異常需要在目標接口的抽象方法上進行聲明)
-
一般建議函數式接口上使用
@FunctionalInterface
注解修飾, 這樣做可以檢查它是否是一個函數式接口,同時 javadoc 也會包含一條聲明,說明這個接口是一個函數式接口
@FunctionalInterface
注解:用于標識該接口是函數式接口,防止在接口中添加多余的抽象方法。不是所有的函數式接口都有
@FunctionalInterface
注解修飾,但是建議所有函數式接口都應該加上該注解,因為這有助于編譯器檢查該接口是否符合函數式接口的要求,即只有一個抽象方法。如果該注解標注在有多個抽象方法的接口上,則編譯器會報錯,提示該接口不符合函數式接口的定義。同時,加上該注解也可以讓其他開發者更容易地理解該接口的用途和特點。
什么是抽象方法?
抽象方法指的是:沒有實現代碼的方法,只有方法聲明(包括方法名、返回值類型、參數列表和異常列表)。在 Java 中,如果一個類中包含了抽象方法,那么這個類必須被聲明為抽象類,因為抽象方法無法被直接調用。只有當一個類繼承了抽象類并實現了抽象方法,才能創建該類的對象并調用該方法。
抽象方法的語法格式為:在方法聲明中使用 abstract
關鍵字修飾方法,方法體中不包含實現代碼。例如:
public abstract void draw();
抽象方法的作用是:為了讓子類實現自己的方法邏輯,從而實現多態性和代碼復用。在設計模式中也經常使用抽象方法來定義基礎框架和算法流程,具體實現由子類來完成。
在函數式接口中,由于該接口只包含一個抽象方法,因此該方法默認為抽象方法,不需要顯式使用 abstract
關鍵字進行修飾。
代碼示例:
@FunctionalInterface
public interface MyInterface {void myMethod();
}
MyInterface myInterface = () -> System.out.println("Hello World!");
myInterface.myMethod(); // 輸出:Hello World!
自定義函數式接口
只要方法的參數是函數式接口都可以用 Lambda 表達式
@FunctionalInterface
public interface MyNumber {double getValue();
}// 在函數式接口中使用泛型
public interface MyFunc<T> {T getValue(T t);
}// 作為參數傳遞 Lambda 表達式
public String toUpperString(MyFunc<String) mf, String str) {return mf.getValue(str);
}String newStr = toUpperString((str) -> str.toUpperCase(), "abcdef"); // 該 Lambda 表達式的作用是將字符串轉換成大寫字母并返回
System.out.println(newStr); // "ABCDEF"
作為參數傳遞 Lambda 表達式:
為了將 Lambda 表達式作為參數傳遞,接收 Lambda 表達式的參數類型必須是與該 Lambda 表達式兼容的函數式接口的類型。
例如一個計算器接口,可以定義如下的函數式接口:
@FunctionalInterface
public interface Calculator {int calculate(int x, int y);
}
在該接口中,定義了一個抽象方法 calculate(int x, int y)
,接受兩個整型參數 x
和 y
,返回一個整型結果。該接口標記了 @FunctionalInterface
注解,表示該接口是一個函數式接口。 然后,可以使用 Lambda 表達式來創建該接口的實例,例如:
Calculator add = (x, y) -> x + y;
Calculator subtract = (x, y) -> x - y;
Calculator multiply = (x, y) -> x * y;
Calculator divide = (x, y) -> x / y;
在這個例子中,分別使用 Lambda 表達式創建了加法、減法、乘法、除法四種計算器操作的實例,這些實例都是 Calculator
函數式接口的實例。 然后,可以使用這些實例進行計算,例如:
int result = add.calculate(2, 3); // 加法運算,結果為 5
在這個例子中,使用加法操作的實例 add
對參數 2 和 3 進行了加法運算,并將結果賦值給 result
變量。
總之,自定義函數式接口可以根據具體的業務需求定義,然后使用 Lambda 表達式來創建該接口的實例,從而實現對業務邏輯的封裝和復用。
Java 內置四大核心函數式接口
四種:消費型接口、供給型接口、函數型接口、斷定型接口
1. Consumer 接口 – 消費型接口
Consumer
接口定義了一個接受一個泛型參數并返回 void 的操作。該接口有一個抽象方法 accept(T t)
,接收一個泛型參數 T
,表示該操作的輸入參數,無返回值。
源碼解析:
package java.util.function;import java.util.Objects;@FunctionalInterface
public interface Consumer<T> {/*** 對給定的參數執行操作** @param t 要執行操作的參數*/void accept(T t);/*** 返回一個組合了多個Consumer的新Consumer,表示執行順序為該Consumer接口先執行,然后執行after中的Consumer接口** @param after 要執行的另一個Consumer接口* @return 組合后的新Consumer接口*/default Consumer<T> andThen(Consumer<? super T> after) {Objects.requireNonNull(after);return (T t) -> {accept(t);after.accept(t);};}
}
在這個源碼中,首先使用 @FunctionalInterface
注解標記了該接口是一個函數式接口。然后定義了一個抽象方法accept(T t)
,表示對給定的參數執行操作,并無返回值。接著定義了一個默認方法 andThen(Consumer<? super T> after)
,用于組合多個 Consumer
實例,表示執行順序為該 Consumer
接口先執行,然后執行 after
中的Consumer
接口。
總之,Consumer
接口可以用于對給定的參數執行操作,例如打印、修改狀態等。該接口提供了一個默認方法 andThen
可以用于組合多個 Consumer
實例,從而實現更加復雜的操作。
應用場景:
Consumer
接口可以用于對集合進行遍歷、對異步任務結果進行處理、以及對對象狀態進行修改等場景中。使用Consumer
接口可以使代碼更加簡潔、易于維護,增強代碼的可讀性和可重用性。
以下是一些常見的場景:
-
集合遍歷:可以使用
Consumer
接口對集合中的每個元素進行操作。例如對一個字符串列表中的每個字符串進行大寫轉換并輸出:
List<String> list = Arrays.asList("apple", "banana", "orange");
Consumer<String> toUpperCase = s -> System.out.println(s.toUpperCase());
list.forEach(toUpperCase);
在這個例子中,定義了一個字符串列表 list
,其中包含三個字符串元素。然后定義了一個 Consumer
實例 toUpperCase
,用于將字符串轉換為大寫并輸出。接著使用 forEach
方法遍歷 list
中的每個元素,并將 toUpperCase
實例作為參數傳入,實現對每個字符串的大寫轉換和輸出操作。
-
異步任務處理:可以使用
Consumer
接口對異步任務的結果進行處理。例如對一個異步任務的結果進行打印:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello, World!");
Consumer<String> printResult = s -> System.out.println(s);
future.thenAccept(printResult); // "Hello, World!"
在這個例子中,使用 CompletableFuture
創建一個異步任務,該任務返回一個字符串 "Hello, World!"
。然后定義了一個 Consumer
實例 printResult
,用于打印異步任務的結果。使用 thenAccept
方法注冊了一個 Consumer 類型的回調函數,該回調函數的作用是在 future
對象執行完畢后,將結果傳遞給 printResult
Lambda 表達式并輸出。表示當異步任務完成時對結果進行處理,體現了異步任務執行完成后的回調機制。
-
狀態修改:可以使用
Consumer
接口對對象的狀態進行修改。例如修改一個訂單對象的狀態:
public class Order {private String status;// getter和setter方法// ...
}
Consumer<Order> updateStatus = o -> o.setStatus("已發貨");
Order order = new Order();
updateStatus.accept(order);
在這個例子中,定義了一個 Order
類,其中包含一個狀態屬性 status
。然后定義了一個 Consumer
實例 updateStatus
,用于將訂單狀態修改為 "已發貨"
。接著創建了一個訂單對象 order
,并將 updateStatus
實例作為參數傳入,實現對訂單狀態的修改。
2. Supplier 接口 – 供給型接口
Supplier
接口產生給定泛型類型的結果。與 Function 接口不同,Supplier 接口不接受參數。
該接口有一個抽象方法 get()
,用于返回一個泛型參數 T
,表示該操作的結果。
源碼解析:
package java.util.function;@FunctionalInterface
public interface Supplier<T> {/*** 獲取一個結果** @return 表示操作結果的泛型參數T*/T get();
}
在這個源碼中,首先使用 @FunctionalInterface
注解標記了該接口是一個函數式接口。然后定義了一個抽象方法 get()
,用于獲取一個結果并返回一個泛型參數 T
。該方法沒有參數,僅返回一個表示操作結果的泛型參數。
總之,Supplier
接口主要用于提供一個泛型參數類型的結果,例如獲取當前時間、生成隨機數、從數據庫中獲取數據等。Supplier
接口的 get()
方法沒有參數,返回一個表示操作結果的泛型參數,可以與其它函數式接口進行組合使用,實現更加復雜的操作。
應用場景:
用于提供一個泛型參數類型的結果,例如生成隨機數、從數據庫中獲取數據、獲取當前時間等場景中。
- 生成隨機數
Supplier<Integer> randomSupplier = () -> new Random().nextInt(100);
int randomNum = randomSupplier.get();
- 從數據庫中獲取數據
public class SupplierExample {public static void main(String[] args) {Supplier<List<String>> supplier = () -> {List<String> list = new ArrayList<>();try {ResultSet resultSet = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "password").createStatement().executeQuery("select * from user");while (resultSet.next()) {list.add(resultSet.getString("name"));}} catch (SQLException e) {e.printStackTrace();}return list;};List<String> result = supplier.get();System.out.println(result.stream().collect(Collectors.joining(", ")));}
}
- 獲取當前時間
Supplier<LocalDateTime> timeSupplier = LocalDateTime::now;
LocalDateTime currentTime = timeSupplier.get();
3. Function 接口 – 函數型接口
Function
接口接收一個參數并生成結果。默認方法可用于將多個函數鏈接在一起(compose, andThen):
package java.util.function;import java.util.Objects;@FunctionalInterface
public interface Function<T, R> {//將Function對象應用到輸入的參數上,然后返回計算結果。R apply(T t);//將兩個Function整合,并返回一個能夠執行兩個Function對象功能的Function對象。default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {Objects.requireNonNull(before);return (V v) -> apply(before.apply(v));}//返回一個由原始Function執行完后再執行after Function的新Functiondefault <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {Objects.requireNonNull(after);return (T t) -> after.apply(apply(t));}//接收任意類型參數,并返回相同類型參數的Function//作用:快速創建一個無需處理參數的Function實例static <T> Function<T, T> identity() {return t -> t;}
}
應用場景:
Function
接口主要用于將一個類型的值轉化為另一個類型的值。它接受一個參數并返回一個結果,通常用于在數據處理、轉換或映射過程中使用。
- 數據轉換和映射
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123"); // "123"
- 數據過濾和篩選
List<String> list = Arrays.asList("apple", "banana", "orange", "grape");
Function<String, Boolean> filterFunction = s -> s.startsWith("a");
List<String> filteredList = list.stream().filter(filterFunction::apply).collect(Collectors.toList());
System.out.println(filteredList); // [apple]
- 數據處理和轉換
List<String> list = Arrays.asList("apple", "banana", "orange", "grape");
Function<String, Integer> mapFunction = String::length;
List<Integer> lengthList = list.stream().map(mapFunction::apply).collect(Collectors.toList());
System.out.println(lengthList); // [5, 6, 6, 5]
4. Predicate 接口 – 斷定型接口
Predicate 接口是只有一個參數的返回布爾類型值的 斷言型 接口。該接口包含多種默認方法來將 Predicate 組合成其他復雜的邏輯(比如:與,或,非):
源碼解析:
package java.util.function;
import java.util.Objects;@FunctionalInterface
public interface Predicate<T> {// 該方法是接收一個傳入類型,返回一個布爾值.此方法應用于判斷.boolean test(T t);//and方法與關系型運算符"&&"相似,兩邊都成立才返回truedefault Predicate<T> and(Predicate<? super T> other) {Objects.requireNonNull(other);return (t) -> test(t) && other.test(t);}// 與關系運算符"!"相似,對判斷進行取反default Predicate<T> negate() {return (t) -> !test(t);}//or方法與關系型運算符"||"相似,兩邊只要有一個成立就返回truedefault Predicate<T> or(Predicate<? super T> other) {Objects.requireNonNull(other);return (t) -> test(t) || other.test(t);}// 該方法接收一個Object對象,返回一個Predicate類型.用于判斷兩個對象是否相等static <T> Predicate<T> isEqual(Object targetRef) {return (null == targetRef)? Objects::isNull: object -> targetRef.equals(object);}
應用場景:
Predicate
接口可以用于對集合進行過濾、對方法參數進行校驗、以及實現復雜的邏輯判斷等場景中。使用Predicate
接口可以使代碼更加簡潔、易于維護,增強代碼的可讀性和可重用性。
以下是一些常見的場景:
- 集合過濾:可以使用
Predicate
接口對集合進行過濾,例如篩選出所有大于等于 10 的整數:
List<Integer> list = Arrays.asList(1, 5, 10, 15, 20);
Predicate<Integer> predicate = i -> i >= 10;
List<Integer> filteredList = list.stream().filter(predicate).collect(Collectors.toList());
在這個例子中,首先創建了一個整型列表 list
,然后定義了一個 Predicate
實例 predicate
,用于判斷一個整數是否大于等于 10。接著使用 stream
對列表進行流操作,在流操作中使用 filter
方法傳入 predicate
實例對整數進行過濾,最后將過濾結果收集到一個新的列表中。
- 參數校驗:可以使用
Predicate
接口對方法參數進行校驗,例如校驗一個字符串是否為空:
public void doSomething(String str) {Predicate<String> isEmpty = s -> s == null || s.length() == 0;if (isEmpty.test(str)) {throw new IllegalArgumentException("字符串不能為空");}// ...
}
在這個例子中,定義了一個 Predicate
實例 isEmpty
,用于判斷一個字符串是否為空。在方法中,對傳入參數str
進行校驗,如果為空則拋出異常。
- 復雜邏輯判斷:可以使用
Predicate
接口對多個條件進行組合,實現更加復雜的邏輯判斷。例如判斷一個字符串是否包含數字和大寫字母:
String str = "Hello, World! 123";
Predicate<String> containsDigit = s -> s.matches(".*\\d.*");
Predicate<String> containsUpperCase = s -> s.matches(".*[A-Z].*");
if (containsDigit.and(containsUpperCase).test(str)) {System.out.println("字符串符合要求");
}
在這個例子中,定義了兩個 Predicate
實例 containsDigit
和 containsUpperCase
,分別用于判斷一個字符串是否包含數字和大寫字母。在判斷中使用 and
方法將兩個實例組合起來,表示字符串必須同時包含數字和大寫字母才符合要求。最后使用 test
方法對字符串進行判斷,如果符合要求則輸出相應信息。
小結
-
Consumer 接口
-
抽象方法
void accept(T t);
接收一個泛型參數T
,表示該操作的輸入參數,無返回值; -
一個默認方法
andThen(Consumer<? super T> after)
,用于組合多個Consumer
實例,表示執行順序為- 該Consumer
接口先執行,然后執行after
中的Consumer
接口。
-
-
Supplier 接口
- 抽象方法
T get;
用于返回一個泛型參數T
,表示該操作的結果。
- 抽象方法
-
Function 接口
-
抽象方法
R apply(T t);
接收參數T
然后返回計算結果; -
兩個默認方法(compose, andThen),用于將多個函數鏈接在一起;
-
一個靜態方法
identity()
,用于快速創建一個無需處理參數的 Function 實例。
-
-
Predicate 接口
-
抽象方法
boolean test(T t);
返回一個布爾值,應用于判斷。 -
包含多種默認方法來將 Predicate 組合成其他復雜的邏輯(比如:與(and),或(or),取反(negate))
-
一個靜態方法
isEqual
,用于判斷兩個對象是否相等
-
其他接口
3、方法引用與構造器引用
概念
使用操作符 “::” 將方法名和對象或類的名字分隔開來。
使用場景
當要傳遞給 Lambda 體的操作,已經有實現的方法了,可以使用方法引用!(實現抽象方法的參數列表,必須與方法引用方法的參數列表保持一致!)
(一)方法引用
1. 類 :: 靜態方法
類 :: 靜態方法(ClassName::staticMethodName
)
Function<String, Integer> strToInt = Integer::parseInt;
int result = strToInt.apply("123");
2. 對象 :: 實例方法
對象 :: 實例方法(instance::instanceMethodName
)
List<String> list = Arrays.asList("apple", "banana", "orange", "grape");
list.forEach(System.out::println);
3. 類 :: 實例方法
類 :: 實例方法(ClassName::instanceMethodName
)
List<String> list = Arrays.asList("apple", "banana", "orange", "grape");
list.stream().map(StringUtils::toUppercase).forEach(System.out::println);
(二)構造器引用
格式:ClassName::new
與函數式接口相結合,自動與函數式接口中方法兼容。可以把構造器引用賦值給定義的方法,與構造器參數列表要與接口中抽象方法的參數列表一致。
Function<String, Person> createPerson = Person::new;
Person person = createPerson.apply("Tom");
(三)數組引用
格式:type[] :: new
// 創建一個長度為 10 的數組
Function<Integer, String[]> createArray = String[]::new;
String[] array = createArray.apply(10);
// 將其中所有元素設置為 0
Arrays.stream(arr).forEach(int[]::setAll);
4、強大的 Stream API
概念
Stream 是 Java8 中處理集合的關鍵抽象概念,它可以指定你希望對集合進行的操作,可以執行非常復雜的查找、過濾和映射數據等操作。 使用 Stream API 對集合數據進行操作,就類似于使用 SQL 執行的數 據庫查詢。也可以使用 Stream API 來并行執行操作。簡而言之, Stream API 提供了一種高效且易于使用的處理數據的方式。
流(Stream)到底是什么?
是數據渠道,用于操作數據源(集合、數組等)所生成的元素序列。
“集合講的是數據,流講的是計算!”
注意:
- Stream 自己不會存儲元素。
- Stream 不會改變源對象。相反,他們會返回一個持有結果的新 Stream。
- Stream 操作是延遲執行的。這意味著他們會等到需要結果的時候才執行。
操作三步驟
(一)創建 Stream
順序流與并行流
Java8 中的 Collection 接口被擴展,提供了兩個獲取流的方法:
-
default Stream stream() : 返回一個順序流
-
default Stream parallelStream() : 返回一個并行流
并行流就是把一個內容分成多個數據塊,并用不同的線程分別處理每個數據塊的流。
Java8 中將并行進行了優化,我們可以很容易的對數據進行并行操作。Stream API 可以聲明性地通過 parallel() 與 sequential() 在并行流與順序流之間進行切換。
數組流
Java8 中的 Arrays 的靜態方法 stream() 可以獲取數組流:
static Stream stream(T[] array): 返回一個流
重載形式,能夠處理對應基本類型的數組:
- public static IntStream stream(int[] array)
- public static LongStream stream(long[] array)
- public static DoubleStream stream(double[] array)
由值創建流
可以使用靜態方法 Stream.of(), 通過顯示值創建一個流。它可以接收任意數量的參數。
public static Stream of(T… values) : 返回一個流
無限流
由函數創建流。可以使用靜態方法 Stream.iterate() 和 Stream.generate(), 創建無限流。
-
迭代
public static Stream iterate(final T seed, final UnaryOperator f)
-
生成
public static Stream generate(Supplier s)
(二)Stream 的中間操作
什么是中間操作?
中間操作是對 Stream 對象進行加工處理的操作,用于對 Stream 中的元素進行處理、篩選、過濾、排序、去重等操作。中間操作可以使用鏈式調用的方式進行連續操作,**每次操作都會返回一個新的 Stream 對象,**這樣可以構建出一條操作流水線。
惰性求值
多個中間操作可以連接起來形成一個流水線,除非流水線上觸發終止操作,否則中間操作不會執行任何的處理!而在終止操作時一次性全部處理,稱為“惰性求值” 。
API
篩選與切片
方法 | 描述 |
---|---|
filter(Predicate) | 接收 Lambda ,從流中排除某些元素 |
distinct() | 篩選,通過流所生成元素的 hashCode() 和 equals() 去除重復元素 |
limit(long maxSize) | 截斷流,使其元素不超過給定數量。 |
skip(long n) | 跳過元素,返回一個扔掉了前 n 個元素的流。若流中元素不足 n 個,則返回一個空流。與 limit(n) 互補 |
映射
方法 | 描述 |
---|---|
map(Function f) | 接收一個函數作為參數,該函數會被應用到每個元素上,并將其映射成一個新的元素。 |
mapToDouble(ToDoubleFunction f) | 接收一個函數作為參數,該函數會被應用到每個元素上,產生一個新的 DoubleStream。 |
mapToInt(ToIntFunction f) | 接收一個函數作為參數,該函數會被應用到每個元素上,產生一個新的 IntStream。 |
mapToLong(ToLongFunction f) | 接收一個函數作為參數,該函數會被應用到每個元素上,產生一個新的 LongStream。flatMap(Function f) 接收一個函數作為參數,將流中的每個值都換成另 一個流,然后把所有流連接成一個流。 |
排序
方法 | 描述 |
---|---|
sorted() | 產生一個新流,其中按自然順序排序 |
sorted(Comparator comp) | 產生一個新流,其中按比較器順序排序 |
(三)終止操作
何為終止操作?
- 終止操作是對 Stream 對象執行最終操作的操作,用于觸發 Stream 的處理流程,并返回最終的結果。
- 終止操作可以是有返回值的,例如
collect()
方法,也可以是沒有返回值的,例如forEach()
方法。 - 終端操作會從流的流水線生成結果。其結果可以是任何不是流的值,例如:List、Integer,甚至是 void。
API
查找與匹配
方法 | 描述 |
---|---|
allMatch(Predicate p) | 檢查是否匹配所有元素 |
anyMatch(Predicate p) | 檢查是否至少匹配一個元素 |
noneMatch(Predicate p) | 檢查是否沒有匹配所有元素 |
findFirst() | 返回第一個元素 |
count() | 返回流中元素總數 |
max(Comparator c) | 返回流中最大值 |
min(Comparator c) | 返回流中最小值 |
forEach(Consumer) | 內部迭代(使用 Collection 接口需要用戶去做迭代,稱為外部迭代。相反,Stream API 使用內部迭代一一它幫你把迭代做了) |
歸約
方法 | 描述 |
---|---|
reduce(T iden,BinaryOperator) | 可以將流中元素反復結合起來,得到一個值返口 T |
reduce(BinaryOperator b) | 可以將流中元素反復結合起來,得到一個值。返 Optional<T> |
**備注:**map 和 reduce 的連接通常稱為 map-reduce 模式,因 Google 用它來進行網絡搜索而出名
收集
方法 | 描述 |
---|---|
collect(Collector c) | 將流轉換為其他形式。接收一個 Collector 接口的實現,用于給 Stream 中元素做匯總的方法 |
Collector 接口中方法的實現決定了如何對流執行收集操作(如收 集到 List、Set、Map)。
但是 Collectors 實用類提供了很多靜態 方法,可以方便地創建常見收集器實例,具體方法與實例如下表:
ForK/Join 框架
什么是 Fork/Join 框架?
**Fork/Join 框架:**就是在必要的情況下,將一個大任務,進行拆分 (fork) 成若干個小任務(拆到不可再拆時),再將一個個的小任務運算的結果進行 join 匯總。
并行流:
parallelStream
可多線程執行,是基于 ForkJoin 框架實現的
forEach()
用到的就是多線程
Fork/Join 框架與傳統線程池的區別
采用“工作竊取”模式(work-stealing):當執行新的任務時它可以將其拆分分成更小的任務執行,并將小任務加到線程隊列中,然后再從一個隨機線程的隊列中竊取一個并把它放在自己的隊列中繼續執行。
相對于一般的線程池實現,fork/join 框架的優勢體現在對其中包含的任務的處理方式上。
-
在一般的線程池中,如果一個線程正在執行的任務由于某些原因無法繼續運行,那么該線程會處于等待狀態。
-
而在 fork/join 框架實現中,如果某個子問題由于等待另外一個子問題的完成而無法繼續運行。那么處理該子問題的線程會主動尋找其他尚未運行的子問題來執行,這種方式是動態的,減少了線程的等待時間,并提高了性能。
-
Fork/Join 框架提供了一些特殊的方法,比如
fork()
和join()
等,用于處理分治任務,簡化了代碼的編寫和管理。
5、新時間日期 API
LocalDate、LocalTime、LocalDateTime
三種類的實例都是不可變的對象。
LocalDate
– 表示使用 ISO-8601 日歷系統的日期LocalTime
– 表示使用 ISO-8601 日歷系統的時間LocalDateTime
– 表示使用 ISO-8601 日歷系統的日期和時間
LocalDateTime.class //日期+時間 format: yyyy-MM-ddTHH:mm:ss.SSS
LocalDate.class //日期 format: yyyy-MM-dd
LocalTime.class //時間 format: HH:mm:ss
注:ISO-8601 日歷系統是國際標準化組織制定的現代公民的日期和時間的表示。
各種方法如下:
instant 時間戳
用于“時間戳”的運算。它是以 Unix 元年(傳統的設定為 UTC 時區 1970 年 1 月 1 日午夜時分)開始所經歷的描述進行運算。
Duration 和 Period
-
Duration:
用于計算兩個**“時間”間隔**import java.time.Duration; import java.time.LocalDateTime; public class DurationExample {public static void main(String[] args) {//獲取當前時間LocalDateTime start = LocalDateTime.now();System.out.println("開始時間:" + start);//模擬程序執行try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}//獲取執行結束時間LocalDateTime end = LocalDateTime.now();System.out.println("結束時間:" + end);//計算時間差Duration duration = Duration.between(start, end);System.out.println("程序執行時間:" + duration.toMillis() + " 毫秒");} }
-
Period:
用于計算兩個**“日期”間隔**import java.time.LocalDate; import java.time.Period; public class PeriodExample {public static void main(String[] args) {//獲取當前日期LocalDate start = LocalDate.now();System.out.println("開始日期:" + start);//模擬時間間隔LocalDate end = start.plusDays(30);System.out.println("結束日期:" + end);//計算日期差Period period = Period.between(start, end);System.out.println("日期間隔:" + period.getDays() + " 天");} }
操作日期的類
TemporalAdjuster:
時間校正器。
例如,有時我們可能需要獲取:將日期調整到“下個周日”
等操作。
TemporalAdjusters:
該類通過靜態方法提供了大量的常用TemporalAdjuster
的實現,方便開發者對日期進行各種調整操作。
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
public class TemporalAdjusterExample {public static void main(String[] args) {//獲取當前日期LocalDate date = LocalDate.now();System.out.println("當前日期:" + date);//獲取本月的第一天LocalDate firstDayOfMonth = date.with(TemporalAdjusters.firstDayOfMonth());System.out.println("本月第一天:" + firstDayOfMonth);//獲取本月的最后一天LocalDate lastDayOfMonth = date.with(TemporalAdjusters.lastDayOfMonth());System.out.println("本月最后一天:" + lastDayOfMonth);//獲取下一個周二LocalDate nextTuesday = date.with(TemporalAdjusters.next(DayOfWeek.TUESDAY));System.out.println("下一個周二:" + nextTuesday);}
}
日期時間格式化和解析的類
java.time.format.DateTimeFormatter
類:
該類提供了三種格式化方法:
1. 預定義的標準格式
DateTimeFormatter
類提供了一些預定義的標準格式,例如 ISO_DATE
、ISO_TIME
、ISO_DATE_TIME
等等,這些標準格式已經定義好了日期時間的格式,開發者可以直接使用。
2. 語言環境相關的格式
DateTimeFormatter
類還提供了一些與語言環境相關的格式,例如 ofLocalizedDate(FormatStyle style)
、ofLocalizedTime(FormatStyle style)
、ofLocalizedDateTime(FormatStyle style)
等等,這些格式會根據不同的語言環境自動適配不同的日期時間格式。
3. 自定義的格式
例如 ofPattern(String pattern)
方法可以傳入一個字符串參數,用于定義自己想要的日期時間格式,"yyyy-MM-dd HH:mm:ss"
就是一個自定義的日期時間格式。
時區的處理
Java8 中加入了對時區的支持,帶時區的時間為分別為:ZonedDate
、ZonedTime
、ZonedDateTime
其中每個時區都對應著 ID,地區 ID 都為 “{區域}/{城市}”的格式 例如 :Asia/Shanghai 等
ZoneId
:該類中包含了所有的時區信息getAvailableZoneIds()
: 可以獲取所有時區時區信息of(id)
: 根據指定的時區信息獲取ZoneId
對象
講一下與傳統日期的轉換
Java8 中的日期和時間 API 與傳統的日期類(如 java.util.Date
和 java.util.Calendar
)之間存在一些差異,因此在進行日期轉換時需要注意以下幾點:
-
java.util.Date
和java.util.Calendar
類是可變的,而 Java8 中的日期和時間類是不可變的,因此在進行轉換時需要特別注意。 -
java.util.Date
類的精度是毫秒級別,而 Java8 中的日期和時間類的精度可以達到納秒級別,因此在進行轉換時需要注意精度的損失。 -
Java8 中的日期和時間類之間可以相互轉換,例如可以將
LocalDateTime
對象轉換為Instant
對象,但是需要注意轉換后的時區和精度問題。以下是一個簡單的 Java8 和傳統日期類(
java.util.Date
和java.util.Calendar
)之間的轉換示例:
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Calendar;
import java.util.Date;
public class DateConversionExample {public static void main(String[] args) {// Java 8日期類轉換為傳統日期類LocalDateTime dateTime = LocalDateTime.now();Date date = Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());Calendar calendar = Calendar.getInstance();calendar.setTime(date);System.out.println("Java 8日期類轉換為傳統日期類:" + calendar.getTime());// 傳統日期類轉換為Java 8日期類calendar = Calendar.getInstance();calendar.set(2022, Calendar.APRIL, 28);date = calendar.getTime();dateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());System.out.println("傳統日期類轉換為Java 8日期類:" + dateTime);// Java 8日期類之間的轉換dateTime = LocalDateTime.now();Instant instant = dateTime.atZone(ZoneId.systemDefault()).toInstant();dateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());System.out.println("Java 8日期類之間的轉換:" + dateTime);}
}
6、接口中的默認方法與靜態方法
概念
Java8 中允許接口中包含具有具體實現的方法,該方法稱為 “默認方法”,默認方法使用 default
關鍵字修飾;并允許添加靜態方法。
接口默認方法的“類優先”原則:
如果一個類實現了多個接口,而這些接口中定義了同名的默認方法,那么編譯器就會產生歧義,因為它不知道應該使用哪個默認方法。為了解決這個問題,Java 8 引入了“類優先”(Class Priority)的原則,即如果一個類繼承了另一個類并且實現了一個接口,而這兩者中都定義了同名的默認方法,那么類中的方法優先于接口中的方法。
注意:在使用“類優先”原則時,如果類中的方法不是完全覆蓋了接口中的方法,那么編譯器會報錯,此時需要在類中顯式地調用接口中的方法。
interface MyInterface {default void myMethod() {System.out.println("Default method in interface");}
}
class MyClass {public void myMethod() {System.out.println("Method in class");}
}
class MyImplementation extends MyClass implements MyInterface {
}
public class DefaultMethodExample {public static void main(String[] args) {MyImplementation obj = new MyImplementation();obj.myMethod(); // 輸出 "Method in class"}
}
7、其他新特性
Optional 類?
Optional 類 (java.util.Optional) 是一個容器類,代表一個值存在或不存在,原來用 null 表示一個值不存在,現在 Optional 可以更好的表達這個概念。并且 可以避免空指針異常。
常用方法:
- Optional.of(T t): 創建一個 Optional 實例
- Optional.empty(): 創建一個空的 Optional 實例
- Optional.ofNullable(T t): 若 t 不為 null,創建 Optional 實例,否則創建空實例
- isPresent(): 判斷是否包含值
- orElse(T t): 如果調用對象包含值,返回該值,否則返回 t
- orElseGet(Supplier s): 如果調用對象包含值,返回該值,否則返回 s 獲取的值
- map(Function f): 如果有值對其處理,并返回處理后的 Optional,否則返回 Optional.empty()
- flatMap(Function mapper): 與 map 類似,要求返回值必須是 Optional
重復注解與類型注解
Java8 對注解處理提供了兩點改進:可重復的注解及可用于類型的注解。
-
可重復的注解
在Java 8之前,同一個注解不能在同一個地方重復使用,這在某些情況下會導致代碼的冗余。Java8 引入了可重復的注解,它允許同一個注解在同一個地方重復使用。這種注解需要使用
@Repeatable
元注解來標注。舉個例子,假設我們有一個
@Author
注解,用于表示文章的作者。在 Java8 之前,我們無法在同一個類或方法上使用多次@Author
注解,而必須定義多個注解,如@Author1
、@Author2
等。在 Java8 中,我們可以使用可重復的注解來解決這個問題,示例代碼如下:@Repeatable(Authors.class) @interface Author {String name(); } @interface Authors {Author[] value(); } @Authors({@Author(name = "Alice"),@Author(name = "Bob") }) public class Article {// ... }
-
可用于類型的注解
Java8 還引入了一種新的注解類型 —— 類型注解(
Type Annotation
),它可以用于注解類型(如類、接口、枚舉、注解等)上。這種注解需要使用新的元注解@Target
來標注,同時需要指定ElementType.TYPE_USE
作為注解的目標類型。舉個例子,假設我們有一個
@NotNull
注解,用于表示一個類型不為null
。在 Java8 之前,我們只能將它應用到方法的參數上,而不能將其應用到類型上。在 Java8 中,我們可以使用類型注解將其應用到類型上,示例代碼如下:interface NotNull {} class Example<@NotNull T> {public void foo(List<@NotNull String> strings) {} }
8、Java 8 實戰
推薦參考此篇文章:
https://javaguide.cn/java/new-features/java8-common-new-features.html
9、peek()
peek()
方法是 Stream API 中的一個中間操作,它允許你在不改變數據的情況下,對流中的元素進行某種操作。peek()
方法接收一個 Consumer 接口的實現,對流中的每個元素執行該操作,并返回一個新的流,其中包含與原始流相同的元素。
import java.util.stream.Stream;public class Main {public static void main(String[] args) {Stream.of(1, 2, 3, 4, 5).filter(n -> n % 2 == 0).peek(System.out::println).collect(Collectors.toList());}
}
在這個示例中,我們創建了一個包含 1 到 5 的整數流,然后使用 filter()
方法過濾出偶數。接下來,我們使用 peek()
方法打印每個元素,最后使用 collect()
方法將流轉換為列表。輸出結果如下:
2
4
返回一個新的流有啥必要?
返回一個新的流在某些情況下是有必要的,因為這樣可以保留原始流的狀態。當你對一個流進行操作時,例如過濾、映射或排序等,原始流不會受到影響,因為這些操作都是惰性的,只有在需要時才會執行。
然而,如果你在 forEach
方法中對元素進行了修改,那么這些修改會影響到原始流。這是因為 forEach
方法返回的是一個 void
,它不能直接返回一個新的流。但是,你仍然可以在 forEach
方法中對原始流進行操作,然后使用 .collect()
方法將流轉換為列表或其他集合類型。
總之,返回一個新的流并不總是必要的,但它在某些情況下是有用的,特別是當你需要保留原始流的狀態時。
應用場景
peek()
方法主要用于調試。它可以用于查看流中的元素,但不會改變流的狀態。例如,你可以使用 peek()
方法來查看一個整數流中的偶數,但不會將它們從流中刪除。這個方法在多線程的場景下也很有用,因為它可以用于讀取隊列頭部元素。
總之,peek()
方法主要用于調試和多線程場景下讀取隊列頭部元素。
html](https://javaguide.cn/java/new-features/java8-common-new-features.html)