JAVA8新特性用法

枕上~詩書閒發表於2020-12-11

JAVA8新特性各種用法詳解

訪問介面的預設方法

Lambda表示式中是無法訪問到預設方法的,以下程式碼將無法編譯:
複製程式碼 程式碼如下:

Formula formula = (a) -> sqrt( a * 100);
Built-in Functional Interfaces

JDK 1.8 API包含了很多內建的函式式介面,在老Java中常用到的比如Comparator或者Runnable介面,這些介面都增加了@FunctionalInterface註解以便能用在lambda上。
Java 8 API同樣還提供了很多全新的函式式介面來讓工作更加方便,有一些介面是來自Google Guava庫裡的,即便你對這些很熟悉了,還是有必要看看這些是如何擴充套件到lambda上使用的。

Predicate介面

Predicate 介面只有一個引數,返回boolean型別。該介面包含多種預設方法來將Predicate組合成其他複雜的邏輯(比如:與,或,非):
複製程式碼 程式碼如下:

Predicate<String> predicate = (s) -> s.length() > 0;
predicate.test("foo");              
predicate.negate().test("foo");     
Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();

Function 介面

Function 介面有一個引數並且返回一個結果,並附帶了一些可以和其他函式組合的預設方法(compose, andThen):
複製程式碼 程式碼如下:

Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123");     // "123"

Supplier 介面

Supplier 介面返回一個任意範型的值,和Function介面不同的是該介面沒有任何引數
複製程式碼 程式碼如下:

Supplier<Person> personSupplier = Person::new;
personSupplier.get();

Consumer 介面

Consumer 介面表示執行在單個引數上的操作。
複製程式碼 程式碼如下:

Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName)
greeter.accept(new Person("Luke", "Skywalker"))

Comparator 介面

Comparator 是老Java中的經典介面, Java 8在此之上新增了多種預設方法:
複製程式碼 程式碼如下:

Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName)
Person p1 = new Person("John", "Doe")
Person p2 = new Person("Alice", "Wonderland")
comparator.compare(p1, p2)
comparator.reversed().compare(p1, p2)

Optional 介面

Optional 不是函式是介面,這是個用來防止NullPointerException異常的輔助型別,這是下一屆中將要用到的重要概念,現在先簡單的看看這個介面能幹什麼:
Optional 被定義為一個簡單的容器,其值可能是null或者不是null。在Java 8之前一般某個函式應該返回非空物件但是偶爾卻可能返回了null,而在Java 8中,不推薦你返回null而是返回Optional。
複製程式碼 程式碼如下:

Optional<String> optional = Optional.of("bam");
optional.isPresent();           // true
optional.get();                 // "bam"
optional.orElse("fallback");    // "bam"
optional.ifPresent((s) -> System.out.println(s.charAt(0)));     // "b"

Stream 介面

java.util.Stream 表示能應用在一組元素上一次執行的操作序列。Stream 操作分為中間操作或者最終操作兩種,最終操作返回一特定型別的計算結果,而中間操作返回Stream本身,這樣你就可以將多個操作依次串起來。Stream 的建立需要指定一個資料來源,比如 java.util.Collection的子類,List或者Set, Map不支援。Stream的操作可以序列執行或者並行執行。
首先看看Stream是怎麼用,首先建立例項程式碼的用到的資料List:
複製程式碼 程式碼如下:

List<String> stringCollection = new ArrayList<>()
stringCollection.add("ddd2")
stringCollection.add("aaa2")
stringCollection.add("bbb1")
stringCollection.add("aaa1")
stringCollection.add("bbb3")
stringCollection.add("ccc")
stringCollection.add("bbb2")
stringCollection.add("ddd1")

Java 8擴充套件了集合類,可以通過 Collection.stream() 或者 Collection.parallelStream() 來建立一個Stream。下面幾節將詳細解釋常用的Stream操作:

Filter 過濾

過濾通過一個predicate介面來過濾並只保留符合條件的元素,該操作屬於中間操作,所以我們可以在過濾後的結果來應用其他Stream操作(比如forEach)。forEach需要一個函式來對過濾後的元素依次執行。forEach是一個最終操作,所以我們不能在forEach之後來執行其他Stream操作。
複製程式碼 程式碼如下:

stringCollection
   .stream()
   .filter((s) -> s.startsWith("a"))
   .forEach(System.out::println)
// "aaa2", "aaa1"

Sort 排序

排序是一箇中間操作,返回的是排序好後的Stream。如果你不指定一個自定義的Comparator則會使用預設排序。
複製程式碼 程式碼如下:

stringCollection
   .stream()
   .sorted()
   .filter((s) -> s.startsWith("a"))
   .forEach(System.out::println)
// "aaa1", "aaa2"

需要注意的是,排序只建立了一個排列好後的Stream,而不會影響原有的資料來源,排序之後原資料stringCollection是不會被修改的:
複製程式碼 程式碼如下:

System.out.println(stringCollection);

Map 對映

中間操作map會將元素根據指定的Function介面來依次將元素轉成另外的物件,下面的示例展示了將字串轉換為大寫字串。你也可以通過map來講物件轉換成其他型別,map返回的Stream型別是根據你map傳遞進去的函式的返回值決定的。
複製程式碼 程式碼如下:

boolean anyStartsWithA = stringCollection
   .stream()
   .map(String::toUpperCase)
   .sorted((a, b) -> b.compareTo(a))
   .forEach(System.out::println)
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"

Match 匹配

Stream提供了多種匹配操作,允許檢測指定的Predicate是否匹配整個Stream。所有的匹配操作都是最終操作,並返回一個boolean型別的值。
複製程式碼 程式碼如下:

boolean anyStartsWithA = 
   stringCollection
       .stream()
       .anyMatch((s) -> s.startsWith("a"));
System.out.println(anyStartsWithA);      // true
boolean allStartsWithA = 
   stringCollection
       .stream()
       .allMatch((s) -> s.startsWith("a"));
System.out.println(allStartsWithA);      // false
boolean noneStartsWithZ = 
   stringCollection
       .stream()
       .noneMatch((s) -> s.startsWith("z"));
System.out.println(noneStartsWithZ);      // true

Count 計數

計數是一個最終操作,返回Stream中元素的個數,返回值型別是long。
複製程式碼 程式碼如下:

long startsWithB = 
   stringCollection
       .stream()
       .filter((s) -> s.startsWith("b"))
       .count();
System.out.println(startsWithB);    // 3

Reduce 規約

這是一個最終操作,允許通過指定的函式來講stream中的多個元素規約為一個元素,規越後的結果是通過Optional介面表示的:
複製程式碼 程式碼如下:

Optional<String> reduced =
   stringCollection
       .stream()
       .sorted()
       .reduce((s1, s2) -> s1 + "#" + s2)
reduced.ifPresent(System.out::println)
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

並行Streams

前面提到過Stream有序列和並行兩種,序列Stream上的操作是在一個執行緒中依次完成,而並行Stream則是在多個執行緒上同時執行。
下面的例子展示了是如何通過並行Stream來提升效能:
首先我們建立一個沒有重複元素的大表:
複製程式碼 程式碼如下:

int max = 1000000;
List<String> values = new ArrayList<>(max);
for (int i = 0; i < max; i++) {
   UUID uuid = UUID.randomUUID();
   values.add(uuid.toString());
}

然後我們計算一下排序這個Stream要耗時多久,
序列排序:
複製程式碼 程式碼如下:

long t0 = System.nanoTime()
long count = values.stream().sorted().count()
System.out.println(count)
long t1 = System.nanoTime()
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0)
System.out.println(String.format("sequential sort took: %d ms", millis))
// 序列耗時: 899 ms

並行排序:
複製程式碼 程式碼如下:

long t0 = System.nanoTime()
long count = values.parallelStream().sorted().count()
System.out.println(count)
long t1 = System.nanoTime()
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0)
System.out.println(String.format("parallel sort took: %d ms", millis))
// 並行排序耗時: 472 ms

上面兩個程式碼幾乎是一樣的,但是並行版的快了50%之多,唯一需要做的改動就是將stream()改為parallelStream()。

Map

前面提到過,Map型別不支援stream,不過Map提供了一些新的有用的方法來處理一些日常任務。
複製程式碼 程式碼如下:

Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < 10; i++) {
   map.putIfAbsent(i, "val" + i);
}
map.forEach((id, val) -> System.out.println(val));

以上程式碼很容易理解, putIfAbsent 不需要我們做額外的存在性檢查,而forEach則接收一個Consumer介面來對map裡的每一個鍵值對進行操作。
下面的例子展示了map上的其他有用的函式:
複製程式碼 程式碼如下:

map.computeIfPresent(3, (num, val) -> val + num);
map.get(3);             // val33
map.computeIfPresent(9, (num, val) -> null);
map.containsKey(9);     // false
map.computeIfAbsent(23, num -> "val" + num);
map.containsKey(23);    // true
map.computeIfAbsent(3, num -> "bam");
map.get(3);            // val33

接下來展示如何在Map裡刪除一個鍵值全都匹配的項:
複製程式碼 程式碼如下:

map.remove(3, "val3");map.get(3);             
map.remove(3, "val33");map.get(3);

另外一個有用的方法:
複製程式碼 程式碼如下:

map.getOrDefault(42, "not found");  // not found

對Map的元素做合併也變得很容易了:
複製程式碼 程式碼如下:

map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
map.get(9);             // val9
map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
map.get(9);// val9concat

Merge做的事情是如果鍵名不存在則插入,否則則對原鍵對應的值做合併操作並重新插入到map中。

labmel表示式在toMap的時候如果是null的會報錯

null針的判斷
日誌列印的時候最好也是在不是空的時候列印,要不然就null針了

grouping

Stream流的方式能夠讓我們在程式碼層還錦上添花,不用寫的那麼複雜難懂。

像這個grouping就是我們SQL語句裡面的group by 分組嘛
而Java8這裡就可以在程式碼裡面根據不同的東西來分組。

例如:


Map<String, List<User>> collect = list.stream()
        .collect(
                Collectors.groupingBy(
                        User::getName/*, Collectors.counting()*/
                )
        );

相關文章