Java8特性大全(最新版)

Java知識圖譜發表於2022-02-13

一、序言

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());

filtermap操作通常結合使用,取出流中某行某列的資料,建議先行後列的方式定位。

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的應用場景更為強大,下面對這個場景進行詳細介紹。希望返回結果中能夠建立IDNAME之間的匹配關係,最常見的場景是通過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));

IDObject類對映

@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);

六、流的應用

(一)列表轉樹

傳統方式下構建樹形列表需要反覆遞迴呼叫查詢資料庫,效率偏低。對於一棵結點較多的樹,效率更低。這裡提供一種只需呼叫一次資料庫,通過流將列表轉化為樹的解決方式。

image-20211014133305884
/**
 * 列表轉樹
 *
 * @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;
}

相關文章