一、序言
Java8 是一個里程碑式的版本,憑藉如下新特性,讓人對其讚不絕口。
- Lambda 表示式給程式碼構建帶來了全新的風格和能力;
- Steam API 豐富了集合操作,擴充了集合的能力;
- 新日期時間 API 千呼萬喚始出來;
隨著對 Java8 新特性理解的深入,會被 Lambda 表示式(包含方法引用)、流式運算的美所迷戀,不由驚歎框架設計的美。
二、方法引用
Lambda 表示式是匿名函式,可以理解為一段可以用引數傳遞的程式碼(程式碼像資料一樣傳遞)。Lambda 表示式的使用需要有函式式介面的支援。
方法引用是對特殊 Lambda 表示式的一種簡化寫法,當 Lambda 體中只呼叫一個方法,此方法滿足函式式介面規範,此時可以使用::
方法引用語法。
從語法表現力角度來講,方法引用 > Lambda表示式 > 匿名內部類
,方法引用是高階版的 Lambda 表示式,語言表達更為簡潔,強烈推薦使用。
方法引用表示式無需顯示宣告被呼叫方法的引數,根據上下文自動注入。方法引用能夠提高 Lambda 表示式語言的優雅性,程式碼更加簡潔。下面以Comparator
排序為例講述如何藉助方法引用構建優雅的程式碼。
(一)方法引用與排序
1、普通資料型別
普通資料型別相對較容易理解。
// 正向排序(方法引用)
Stream.of(11, 15, 11, 12).sorted(Integer::compareTo).forEach(System.out::println);
// 正向排序
Stream.of(11, 15, 11, 12).sorted(Comparator.naturalOrder()).forEach(System.out::println);
// 逆向排序
Stream.of(11, 15, 11, 12).sorted(Comparator.reverseOrder()).forEach(System.out::println);
2、物件資料型別
(1)資料完好
資料完好有兩重含義,一是物件本身不為空;二是待比較物件的屬性值不為空,以此為前提進行排序操作。
// 對集合按照年齡排序(正序排列)
Collections.sort(userList, Comparator.comparingInt(XUser::getAge));
// 對集合按照年齡排序(逆序排列)
Collections.sort(userList, Comparator.comparingInt(XUser::getAge).reversed());
此示例是以Integer
型別展開的,同理Double
型別、Long
型別等數值型別處理方式相同。其中[Comparator]是排序過程中重要的類。
(2)資料缺失
資料缺失的含義是物件本身為空或者待比較物件屬性為空,如果不進行處理,上述排序會出現空指標異常。
最常見的處理方式是通過流式運算中filter
方法,過濾掉空指標資料,然後按照上述策略排序。
userList.stream().filter(e->e.getAge()!=null).collect(Collectors.toList());
3、字串處理
少數開發者在構建實體類時,String
型別遍地開花,在需要運算或者排序的場景下,String 的缺陷逐漸暴露出來。下面講述字串數值
型別排序問題,即不修改資料型別的前提下完成期望的操作。
實體類
public class SUser {
private Integer userId;
private String UserName;
// 本應該是Double型別,錯誤的使用為String型別
private String score;
}
正序、逆序排序
// 對集合按照年齡排序(正序排列)
Collections.sort(userList, Comparator.comparingDouble(e -> new Double(e.getScore())));
資料型別轉換排序時,使用 JDK 內建的 API 並不流暢,推薦使用commons-collection4
包中的排序工具類。瞭解更多,請移步檢視[ComparatorUtils]。
// 對集合按照年齡排序(逆序排列)
Collections.sort(userList, ComparatorUtils.reversedComparator(Comparator.comparingDouble(e -> new Double(e.getScore()))));
小結:通過以排序為例,實現 Comparator 介面、Lambda 表示式、方法引用三種方式相比較,程式碼可讀性逐步提高。
(二)排序器
內建的排序器可以完成大多數場景的排序需求,當排序需求更加精細化時,適時引入第三方框架是比較好的選擇。
1、單列排序
單列排序包含正序和逆序。
// 正序
Comparator<Person> comparator = Comparator.comparing(XUser::getUserName);
// 逆序
Comparator<Person> comparator = Comparator.comparing(XUser::getUserName).reversed();
2、多列排序
多列排序是指當待比較的元素有相等的值時,如何進行下一步排序。
// 預設多列均是正序排序
Comparator<XUser> comparator = Comparator.comparing(XUser::getUserName)
.thenComparing(XUser::getScore);
// 自定義正逆序
Comparator<XUser> comparator = Comparator.comparing(XUser::getUserName,Comparator.reverseOrder())
.thenComparing(XUser::getScore,Comparator.reverseOrder());
三、Steam API
流的操作包含如下三個部分:建立流、中間流、關閉流,篩選
、去重
、對映
、排序
屬於流的中間操作,收集
屬於終止操作。[Stream]是流操作的基礎關鍵類。
(一)建立流
(1)通過集合建立流
// 通過集合建立流
List<String> lists = new ArrayList<>();
lists.stream();
(2)通過陣列建立流
// 通過陣列建立流
String[] strings = new String[5];
Stream.of(strings);
應用較多的是通過集合建立流,然後經過中間操作,最後終止回集合。
(二)中間操作
1、篩選(filter)
篩選是指從(集合)流中篩選滿足條件的子集,通過 Lambda 表示式生產型介面來實現。
// 通過斷言型介面實現元素的過濾
stream.filter(x->x.getSalary()>10);
非空過濾
非空過濾包含兩層內容:一是當前物件是否為空或者非空;二是當前物件的某屬性是否為空或者非空。
篩選非空物件,語法stream.filter(Objects::nonNull)
做非空斷言。
// 非空斷言
java.util.function.Predicate<Boolean> nonNull = Objects::nonNull;
檢視[Objects]類瞭解更詳細資訊。
2、去重(distinct)
去重是指將(集合)流中重複的元素去除,通過 hashcode 和 equals 函式來判斷是否是重複元素。去重操作實現了類似於 HashSet 的運算,對於物件元素流去重,需要重寫 hashcode 和 equals 方法。
如果流中泛型物件使用 Lombok 外掛,使用@Data
註解預設重寫了 hashcode 和 equals 方法,欄位相同並且屬性相同,則物件相等。更多內容可檢視[Lombok 使用手冊]
stream.distinct();
3、對映(map)
取出流中元素的某一列,然後配合收集以形成新的集合。
stream.map(x->x.getEmpId());
filter
和map
操作通常結合使用,取出流中某行某列的資料,建議先行後列
的方式定位。
Optional<MainExportModel> model = data.stream().filter(e -> e.getResId().equals(resId)).findFirst();
if (model.isPresent()) {
String itemName = model.get().getItemName();
String itemType = model.get().getItemType();
return new MainExportVo(itemId, itemName);
}
4、排序(sorted)
傳統的Collectors
類中的排序支援 List 實現類中的一部分排序,使用 stream 排序,能夠覆蓋所有的 List 實現類。
// 按照預設字典順序排序
stream.sorted();
// 按照工資大小排序
stream.sorted((x,y)->Integer.compare(x.getSalary(),y.getSalary()));
(1)函式式介面排序
基於 Comparator 類中函式式方法,能夠更加優雅的實現物件流的排序。
// 正向排序(預設)
pendingPeriod.stream().sorted(Comparator.comparingInt(ReservoirPeriodResult::getPeriod));
// 逆向排序
pendingPeriod.stream().sorted(Comparator.comparingInt(ReservoirPeriodResult::getPeriod).reversed());
(2)LocalDate 和 LocalDateTime 排序
新日期介面相比就介面,使用體驗更加,因此越來越多的被應用,基於日期排序是常見的操作。
// 準備測試資料
Stream<DateModel> stream = Stream.of(new DateModel(LocalDate.of(2020, 1, 1)), new DateModel(LocalDate.of(2019, 1, 1)), new DateModel(LocalDate.of(2021, 1, 1)));
正序、逆序排序
// 正向排序(預設)
stream.sorted(Comparator.comparing(DateModel::getLocalDate)).forEach(System.out::println);
// 逆向排序
stream.sorted(Comparator.comparing(DateModel::getLocalDate).reversed()).forEach(System.out::println);
5、規約(reduce)
對流中的元素按照一定的策略計算。終止操作的底層邏輯都是由 reduce 實現的。
(三)終止操作
收集(collect)將流中的中間(計算)結果儲存到集合中,方便後續進一步使用。為了方便對收集操作的理解,方便讀者掌握收集操作,將收集分為普通收集
和高階收集
。
1、普通收集
(1)收集為List
預設返回的型別為ArrayList
,可通過Collectors.toCollection(LinkedList::new)
顯示指明使用其它資料結構作為返回值容器。
List<String> collect = stream.collect(Collectors.toList());
由集合建立流的收集需注意:僅僅修改流欄位中的內容,沒有返回新型別,如下操作直接修改原始集合,無需處理返回值。
// 直接修改原始集合
userVos.stream().map(e -> e.setDeptName(hashMap.get(e.getDeptId()))).collect(Collectors.toList());
(2)收集為Set
預設返回型別為HashSet
,可通過Collectors.toCollection(TreeSet::new)
顯示指明使用其它資料結構作為返回值容器。
Set<String> collect = stream.collect(Collectors.toSet());
2、高階收集
(1)收集為Map
預設返回型別為HashMap
,可通過Collectors.toCollection(LinkedHashMap::new)
顯示指明使用其它資料結構作為返回值容器。
收集為Map
的應用場景更為強大,下面對這個場景進行詳細介紹。希望返回結果中能夠建立ID
與NAME
之間的匹配關係,最常見的場景是通過ID
批量到資料庫查詢NAME
,返回後再將原資料集中的ID
替換成NAME
。
ID 到 NAME 對映
@Data
public class ItemEntity {
private Integer itemId;
private String itemName;
}
準備集合資料,此部分通常是從資料庫查詢的資料
// 模擬從資料庫中查詢批量的資料
List<ItemEntity> entityList = Stream.of(new ItemEntity(1,"A"), new ItemEntity(2,"B"), new ItemEntity(3,"C")).collect(Collectors.toList());
將集合資料轉化成 ID 與 NAME 的 Map
// 將集合資料轉化成ID與NAME的Map
Map<Integer, String> hashMap = entityList.stream().collect(Collectors.toMap(ItemEntity::getItemId, ItemEntity::getItemName));
ID
與Object
類對映
@Data
public class ItemEntity {
private Integer itemId;
private String itemName;
private Boolean status;
}
將集合資料轉化成 ID 與實體類的 Map
// 將集合資料轉化成ID與實體類的Map
Map<Integer, ItemEntity> hashMap = entityList.stream().collect(Collectors.toMap(ItemEntity::getItemId, e -> e));
其中Collectors
類中的toMap
引數是函式式介面引數,能夠自定義返回值。
public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}
(2)分組收集
流的分組收集操作在記憶體層次模擬了資料庫層面的group by
操作,下面演示流的分組操作。[Collectors]類提供了各種層次的分組操作支撐。
流的分組能力對應資料庫中的聚合函式,目前大部分能在資料庫中操作的聚合函式,都能在流中找到相應的能力。
// 預設使用List作為分組後承載容器
Map<Integer, List<XUser>> hashMap = xUsers.stream().collect(Collectors.groupingBy(XUser::getDeptId));
// 顯示指明使用List作為分組後承載容器
Map<Integer, List<XUser>> hashMap = xUsers.stream().collect(Collectors.groupingBy(XUser::getDeptId, Collectors.toList()));
對映後再分組
Map<Integer, List<String>> hashMap = xUsers.stream().collect(Collectors.groupingBy(XUser::getDeptId,Collectors.mapping(XUser::getUserName,Collectors.toList())));
四、Stream 擴充
(一)集合與物件互轉
將物件包裝成集合的形式和將集合拆解為物件的形式是常見的操作。
1、物件轉集合
返回預設型別的集合例項
/**
* 將單個物件轉化為集合
*
* @param t 物件例項
* @param <T> 物件型別
* @param <C> 集合型別
* @return 包含物件的集合例項
*/
public static <T, C extends Collection<T>> Collection<T> toCollection(T t) {
return toCollection(t, ArrayList::new);
}
使用者自定義返回的集合例項型別
/**
* 將單個物件轉化為集合
*
* @param t 物件例項
* @param supplier 集合工廠
* @param <T> 物件型別
* @param <C> 集合型別
* @return 包含物件的集合例項
*/
public static <T, C extends Collection<T>> Collection<T> toCollection(T t, Supplier<C> supplier) {
return Stream.of(t).collect(Collectors.toCollection(supplier));
}
2、集合轉物件
使用預設的排序規則,注意此處不是指自然順序排序。
/**
* 取出集合中第一個元素
*
* @param collection 集合例項
* @param <E> 集合中元素型別
* @return 泛型型別
*/
public static <E> E toObject(Collection<E> collection) {
// 處理集合空指標異常
Collection<E> coll = Optional.ofNullable(collection).orElseGet(ArrayList::new);
// 此處可以對流進行排序,然後取出第一個元素
return coll.stream().findFirst().orElse(null);
}
上述方法巧妙的解決兩個方面的異常問題:一是集合例項引用空指標異常;二是集合下標越界異常。
(二)其它
1、平行計算
基於流式計算中的並行流,能夠顯著提高大資料下的計算效率,充分利用 CPU 核心數。
// 通過並行流實現資料累加
LongStream.rangeClosed(1,9999999999999999L).parallel().reduce(0,Long::sum);
2、序列陣列
生成指定序列的陣列或者集合。
// 方式一:生成陣列
int[] ints = IntStream.rangeClosed(1, 100).toArray();
// 方式二:生成集合
List<Integer> list = Arrays.stream(ints).boxed().collect(Collectors.toList());
五、其它
(一)新日期時間 API
1、LocalDateTime
// 獲取當前日期(包含時間)
LocalDateTime localDateTime = LocalDateTime.now();
// 獲取當前日期
LocalDate localDate = localDateTime.toLocalDate();
// 獲取當前時間
LocalTime localTime = localDateTime.toLocalTime();
日期格式化
// 月份MM需要大寫、小時字母需要大寫(小寫表示12進位制)
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
// 獲取當前時間(字串)
String dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println("dateTime = " + dateTime);
2、Duration
Duration duration = Duration.between(Instant.now(), Instant.now());
System.out.println("duration = " + duration);
3、獲取當前時間戳
如下方式獲取的是 13 位時間戳,單位是毫秒。
// 方式一
long now = Timestamp.valueOf(LocalDateTime.now()).getTime();
// 方式二
long now = Instant.now().toEpochMilli();
(二)Optional
在[Optional]類出現之前,null
異常幾乎折磨著每一位開發者,為了構建健壯的應用程式,不得不使用繁瑣的if
邏輯判斷來回避空指標異常。解鎖Optional
類,讓你編寫的應用健壯性更上一層樓。
1、先判斷後使用
ifPresent
方法提供了先判斷是否為空,後進一步使用的能力。
2、鏈式取值
鏈式取值是指,層層巢狀物件取值,在上層物件不為空的前提下,才能讀取其屬性值,然後繼續呼叫,取出最終結果值。有時候只關心鏈末端的結果狀態,即使中間狀態為空,直接返回空值。如下提供了一種無 if 判斷,程式碼簡介緊湊的實現方式:
Optional<Long> optional = Optional.ofNullable(tokenService.getLoginUser(ServletUtils.getRequest()))
.map(LoginUser::getUser).map(SysUser::getUserId);
// 如果存在則返回,不存在返回空
Long userId = optional.orElse(null);
六、流的應用
(一)列表轉樹
傳統方式下構建樹形列表需要反覆遞迴呼叫查詢資料庫,效率偏低。對於一棵結點較多的樹,效率更低。這裡提供一種只需呼叫一次資料庫,通過流將列表轉化為樹的解決方式。
/**
* 列表轉樹
*
* @param rootList 列表的全部資料集
* @param parentId 第一級目錄的父ID
* @return 樹形列表
*/
public List<IndustryNode> getChildNode(List<Industry> rootList, String parentId) {
List<IndustryNode> lists = rootList.stream()
.filter(e -> e.getParentId().equals(parentId))
.map(IndustryNode::new).collect(toList());
lists.forEach(e -> e.setChilds(getChildNode(rootList, e.getId())));
return lists;
}