一、序言
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;
}