全網(wǎng)最通透的Java8版本特性講解

image
  • 「MoreThanJava」 宣揚(yáng)的是 「學(xué)習(xí),不止 CODE」,本系列 Java 基礎(chǔ)教程是自己在結(jié)合各方面的知識(shí)之后,對(duì) Java 基礎(chǔ)的一個(gè)總回顧,旨在 「幫助新朋友快速高質(zhì)量的學(xué)習(xí)」。
  • 當(dāng)然 不論新老朋友 我相信您都可以 從中獲益。如果覺得 「不錯(cuò)」 的朋友,歡迎 「關(guān)注 + 留言 + 分享」,文末有完整的獲取鏈接,您的支持是我前進(jìn)的最大的動(dòng)力!

特性總覽

以下是 Java 8 中的引入的部分新特性。關(guān)于 Java 8 新特性更詳細(xì)的介紹可參考這里。

  • 接口默認(rèn)方法和靜態(tài)方法
  • Lambda 表達(dá)式
  • 函數(shù)式接口
  • 方法引用
  • Stream
  • Optional
  • Date/Time API
  • 重復(fù)注解
  • 擴(kuò)展注解的支持
  • Base64
  • JavaFX
  • 其它
    • JDBC 4.2 規(guī)范
    • 更好的類型推測(cè)機(jī)制
    • HashMap 性能提升
    • IO/NIO 的改進(jìn)
    • JavaScript 引擎 Nashorn
    • 并發(fā)(Concurrency)
    • 類依賴分析器 jdeps
    • JVM 的 PermGen 空間被移除

一. 接口默認(rèn)方法和靜態(tài)方法

接口默認(rèn)方法

Java 8 中,允許為接口方法提供一個(gè)默認(rèn)的實(shí)現(xiàn)。必須用 default 修飾符標(biāo)記這樣一個(gè)方法,例如 JDK 中的 Iterator 接口:

public interface Iterator<E> {
      boolean hasNext();
      E next();
      default void remove() { throw new UnsupportedOperationExceition("remove"); }
}

這將非常有用!如果你要實(shí)現(xiàn)一個(gè)迭代器,就需要提供 hasNext()next() 方法。這些方法沒有默認(rèn)實(shí)現(xiàn)——它們依賴于你要遍歷訪問的數(shù)據(jù)結(jié)構(gòu)。不過,如果你的迭代器是 只讀 的,那么就不用操心實(shí)現(xiàn) remove() 方法。

默認(rèn)方法也可以調(diào)用其他方法,例如,我們可以改造 Collection 接口,定義一個(gè)方便的 isEmpty() 方法:

public interface Collection {
      int size(); // an abstract method
      default boolean isEmpty() { return size() == 0; }
}

這樣,實(shí)現(xiàn) Collection 的程序員就不用再操心實(shí)現(xiàn) isEmpty() 方法了。

在 JVM 中,默認(rèn)方法的實(shí)現(xiàn)是非常高效的,并且通過字節(jié)碼指令為方法調(diào)用提供了支持。默認(rèn)方法允許繼續(xù)使用現(xiàn)有的 Java 接口,而同時(shí)能夠保障正常的編譯過程。這方面好的例子是大量的方法被添加到java.util.Collection接口中去:stream(),parallelStream(),forEach(),removeIf()等。盡管默認(rèn)方法非常強(qiáng)大,但是在使用默認(rèn)方法時(shí)我們需要小心注意一個(gè)地方:在聲明一個(gè)默認(rèn)方法前,請(qǐng)仔細(xì)思考是不是真的有必要使用默認(rèn)方法。

解決默認(rèn)方法沖突

如果先在一個(gè)接口中將一個(gè)方法定義為默認(rèn)方法,然后又在類或另一個(gè)接口中定義同樣的方法,會(huì)發(fā)生什么?

// 測(cè)試接口 1
public interface TestInterface1 {
    default void sameMethod() { System.out.println("Invoke TestInterface1 method!"); }
}
// 測(cè)試接口 2
public interface TestInterface2 {
    default void sameMethod() { System.out.println("Invoke TestInterface2 method!"); }
}
// 繼承兩個(gè)接口的測(cè)試類
public class TestObject implements TestInterface1, TestInterface2 {

    @Override
    public void sameMethod() {
          // 這里也可以選擇兩個(gè)接口中的一個(gè)默認(rèn)實(shí)現(xiàn)
          // 如: TestInterface1.super.sameMethod();
        System.out.println("Invoke Object method!");
    }
}
// 測(cè)試類
public class Tester {

    public static void main(String[] args) {
        TestObject testObject = new TestObject();
        testObject.sameMethod();
    }
}

測(cè)試輸出:

Invoke Object method!

?? 對(duì)于 Scale 或者 C++ 這些語言來說,解決這種具有 二義性 的情況規(guī)則會(huì)很復(fù)雜,Java 的規(guī)則則簡(jiǎn)單得多:

  1. 類優(yōu)先。如果本類中提供了一個(gè)具體方法符合簽名,則同名且具有相同參數(shù)列表的接口中的默認(rèn)方法會(huì)被忽略;
  2. 接口沖突。如果一個(gè)接口提供了一個(gè)默認(rèn)方法,另一個(gè)接口提供了一個(gè)同名且參數(shù)列表相同的方法 (順序和類型都相同) ,則必須覆蓋這個(gè)方法來解決沖突 (就是??代碼的情況,不覆蓋編譯器不會(huì)編譯..);

Java 設(shè)計(jì)者更強(qiáng)調(diào)一致性,讓程序員自己來解決這樣的二義性似乎也顯得很合理。如果至少有一個(gè)接口提供了一個(gè)實(shí)現(xiàn),編譯器就會(huì)報(bào)告錯(cuò)誤,程序員就必須解決這個(gè)二義性。(如果兩個(gè)接口都沒有為共享方法提供默認(rèn)實(shí)現(xiàn),則不存在沖突,要么實(shí)現(xiàn),要么不實(shí)現(xiàn)..)

?? 我們只討論了兩個(gè)接口的命名沖突?,F(xiàn)在來考慮另一種情況,一個(gè)類繼承自一個(gè)類,同時(shí)實(shí)現(xiàn)了一個(gè)接口,從父類繼承的方法和接口擁有同樣的方法簽名,又將怎么辦呢?

// 測(cè)試接口
public interface TestInterface {
    default void sameMethod() { System.out.println("Invoke TestInterface Method!"); }
}
// 父類
public class Father {
    void sameMethod() { System.out.println("Invoke Father Method!"); }
}
// 子類
public class Son extends Father implements TestInterface {
    @Override
    public void sameMethod() {
        System.out.println("Invoke Son Method!");
    }
}
// 測(cè)試類
public class Tester {
    public static void main(String[] args) { new Son().sameMethod(); }
}

程序輸出:

COPYInvoke Son Method!

還記得我們說過的方法調(diào)用的過程嗎 (先找本類的方法找不到再?gòu)母割愓??加上這里提到的 “類優(yōu)先” 原則 (本類中有方法則直接調(diào)用),這很容易理解!

千萬不要讓一個(gè)默認(rèn)方法重新定義 Object 類中的某個(gè)方法。例如,不能為 toString()equals() 定義默認(rèn)方法,盡管對(duì)于 List 之類的接口這可能很有吸引力,但由于 類優(yōu)先原則,這樣的方法絕對(duì)無法超越 Object.toString() 或者 Object.equals()

接口靜態(tài)方法

Java 8 中,允許在接口中增加靜態(tài)方法 (允許不構(gòu)建對(duì)象而直接使用的具體方法)。理論上講,沒有任何理由認(rèn)為這是不合法的,只是這有違將接口作為抽象規(guī)范的初衷

例子:

public interface StaticInterface {
    static void method() {
        System.out.println("這是Java8接口中的靜態(tài)方法!");
    }
}

調(diào)用:

public class Main {
    public static void main(String[] args) {
        StaticInterface.method(); // 輸出 這是Java8接口中的靜態(tài)方法!
    }
}

目前為止,通常的做法都是將靜態(tài)方法放在 伴隨類 (可以理解為操作繼承接口的實(shí)用工具類) 中。在標(biāo)準(zhǔn)庫中,你可以看到成對(duì)出現(xiàn)的接口和實(shí)用工具類,如 Collection/ CollectionsPath/ Paths

Java 11 中,Path 接口就提供了一個(gè)與之工具類 Paths.get() 等價(jià)的方法 (該方法用于將一個(gè) URI 或者字符串序列構(gòu)造成一個(gè)文件或目錄的路徑)

COPYpublic interface Path {
    public static Path of(String first, String... more) { ... }
    public static Path of(URI uri) { ... }
}

這樣一來,Paths 類就不再是必要的了。類似地,如果實(shí)現(xiàn)你自己的接口時(shí),沒有理由再額外提供一個(gè)帶有實(shí)用方法的工具類。

?? 另外,在 Java 9 中,接口中的方法可以是 private。private 方法可以是靜態(tài)方法或?qū)嵗椒āS捎谒接蟹椒ㄖ荒茉诮涌诒旧淼姆椒ㄖ惺褂?,所以它們的用法很有限,只能作為接口中其他方法的輔助方法。

二. Lambda 表達(dá)式

Lambda表達(dá)式 (也稱為閉包) 是整個(gè) Java 8 發(fā)行版中最受期待的在 Java 語言層面上的改變,Lambda 允許把函數(shù)作為一個(gè)方法的參數(shù),即 行為參數(shù)化,函數(shù)作為參數(shù)傳遞進(jìn)方法中。

什么是 Lambda 表達(dá)式

我們知道,對(duì)于一個(gè) Java 變量,我們可以賦給一個(gè) 「值」。

image

如果你想把 「一塊代碼」 賦給一個(gè) Java 變量,應(yīng)該怎么做呢?

比如,我想把右邊的代碼塊,賦值給一個(gè)叫做 blockOfCode 的 Java 變量:

image

在 Java 8 之前,這個(gè)是做不到的,但是 Java 8 問世之后,利用 Lambda 特性,就可以做到了。

image

當(dāng)然,這個(gè)并不是一個(gè)很簡(jiǎn)潔的寫法,所以為了讓這個(gè)賦值操作變得更加優(yōu)雅,我們可以移除一些沒有必要的聲明。

image

這樣,我們就成功的非常優(yōu)雅的把「一塊代碼」賦給了一個(gè)變量。而「這塊代碼」,或者說「這個(gè)被賦給一個(gè)變量的函數(shù)」,就是一個(gè) Lambda 表達(dá)式。

但是這里仍然有一個(gè)問題,就是變量 blockOfCode 的類型應(yīng)該是什么?

在 Java 8 里面,所有的 Lambda 的類型都是一個(gè)接口,而 Lambda 表達(dá)式本身,也就是「那段代碼」,需要是這個(gè)接口的實(shí)現(xiàn)。這是理解 Lambda 的一個(gè)關(guān)鍵所在,簡(jiǎn)而言之就是,Lambda 表達(dá)式本身就是一個(gè)接口的實(shí)現(xiàn)。直接這樣說可能還是有點(diǎn)讓人困擾,我們繼續(xù)看看例子。我們給上面的 blockOfCode 加上一個(gè)類型:

image

這種只有一個(gè)接口函數(shù)需要被實(shí)現(xiàn)的接口類型,我們叫它「函數(shù)式接口」。

為了避免后來的人在這個(gè)接口中增加接口函數(shù)導(dǎo)致其有多個(gè)接口函數(shù)需要被實(shí)現(xiàn),變成「非函數(shù)接口」,我們可以在這個(gè)上面加上一個(gè)聲明 @FunctionalInterface, 這樣別人就無法在里面添加新的接口函數(shù)了:

image

這樣,我們就得到了一個(gè)完整的 Lambda 表達(dá)式聲明:

image

Lambda 表達(dá)式的作用

Lambda 最直觀的作用就是使代碼變得整潔.。

我們可以對(duì)比一下 Lambda 表達(dá)式和傳統(tǒng)的 Java 對(duì)同一個(gè)接口的實(shí)現(xiàn):

image

這兩種寫法本質(zhì)上是等價(jià)的。但是顯然,Java 8 中的寫法更加優(yōu)雅簡(jiǎn)潔。并且,由于 Lambda 可以直接賦值給一個(gè)變量,我們就可以直接把 Lambda 作為參數(shù)傳給函數(shù), 而傳統(tǒng)的 Java 必須有明確的接口實(shí)現(xiàn)的定義,初始化才行。

有些情況下,這個(gè)接口實(shí)現(xiàn)只需要用到一次。傳統(tǒng)的 Java 7 必須要求你定義一個(gè)“污染環(huán)境”的接口實(shí)現(xiàn) MyInterfaceImpl,而相較之下 Java 8 的 Lambda, 就顯得干凈很多。

三. 函數(shù)式接口

上面我們說到,只有一個(gè)接口函數(shù)需要被實(shí)現(xiàn)的接口類型,我們叫它「函數(shù)式接口」。Lambda 表達(dá)式配合函數(shù)式接口能讓我們代碼變得干凈許多。

Java 8 API 包含了很多內(nèi)建的函數(shù)式接口,在老 Java 中常用到的比如Comparator或者Runnable接口,這些接口都增加了@FunctionalInterface注解以便能用在Lambda上。

Java 8 API 同樣還提供了很多全新的函數(shù)式接口來讓工作更加方便,有一些接口是來自 Google Guava 庫里的,即便你對(duì)這些很熟悉了,還是有必要看看這些是如何擴(kuò)展到 Lambda 上使用的。

1 - Comparator(比較器接口)

Comparator是老Java中的經(jīng)典接口, Java 8 在此之上添加了多種默認(rèn)方法。源代碼及使用示例如下:

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}
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);             // > 0
comparator.reversed().compare(p1, p2);  // < 0

2 - Consumer(消費(fèi)型接口)

Consumer 接口表示執(zhí)行在單個(gè)參數(shù)上的操作。源代碼及使用示例如下:

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}
Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));

更多的Consumer接口

  • BiConsumer:void accept(T t, U u);: 接受兩個(gè)參數(shù)的二元函數(shù)
  • DoubleConsumer:void accept(double value);: 接受一個(gè)double參數(shù)的一元函數(shù)
  • IntConsumer:void accept(int value);: 接受一個(gè)int參數(shù)的一元函數(shù)
  • LongConsumer:void accept(long value);: 接受一個(gè)long參數(shù)的一元函數(shù)
  • ObjDoubleConsumer:void accept(T t, double value);: 接受一個(gè)泛型參數(shù)一個(gè)double參數(shù)的二元函數(shù)
  • ObjIntConsumer:void accept(T t, int value);: 接受一個(gè)泛型參數(shù)一個(gè)int參數(shù)的二元函數(shù)
  • ObjLongConsumer:void accept(T t, long value);: 接受一個(gè)泛型參數(shù)一個(gè)long參數(shù)的二元函數(shù)

3 - Supplier(供應(yīng)型接口)

Supplier 接口是不需要參數(shù)并返回一個(gè)任意范型的值。其簡(jiǎn)潔的聲明,會(huì)讓人以為不是函數(shù)。這個(gè)抽象方法的聲明,同 Consumer 相反,是一個(gè)只聲明了返回值,不需要參數(shù)的函數(shù)。也就是說 Supplier 其實(shí)表達(dá)的不是從一個(gè)參數(shù)空間到結(jié)果空間的映射能力,而是表達(dá)一種生成能力,因?yàn)槲覀兂R姷膱?chǎng)景中不止是要consume(Consumer)或者是簡(jiǎn)單的map(Function),還包括了 new 這個(gè)動(dòng)作。而 Supplier 就表達(dá)了這種能力。源代碼及使用示例如下:

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
Supplier<Person> personSupplier = Person::new;
personSupplier.get();   // new Person

更多Supplier接口

  • BooleanSupplier:boolean getAsBoolean();: 返回boolean的無參函數(shù)
  • DoubleSupplier:double getAsDouble();: 返回double的無參函數(shù)
  • IntSupplier:int getAsInt();: 返回int的無參函數(shù)
  • LongSupplier:long getAsLong();: 返回long的無參函數(shù)

4 - Predicate(斷言型接口)

Predicate 接口只有一個(gè)參數(shù),返回 boolean 類型。該接口包含多種默認(rèn)方法來將 Predicate 組合成其他復(fù)雜的邏輯(比如:,,)。Streamfilter 方法就是接受 Predicate 作為入?yún)⒌?。這個(gè)具體在后面使用 Stream 的時(shí)候再分析深入。源代碼及使用示例如下:

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
Predicate<String> predicate = (s) -> s.length() > 0;
predicate.test("foo");            // true
predicate.negate().test("foo");     // false
Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();

更多的Predicate接口

  • BiPredicate:boolean test(T t, U u);: 接受兩個(gè)參數(shù)的二元斷言函數(shù)
  • DoublePredicate:boolean test(double value);: 入?yún)閐ouble的斷言函數(shù)
  • IntPredicate:boolean test(int value);: 入?yún)閕nt的斷言函數(shù)
  • LongPredicate:boolean test(long value);: 入?yún)閘ong的斷言函數(shù)

5 - Function(功能型接口)

Function 接口有一個(gè)參數(shù)并且返回一個(gè)結(jié)果,并附帶了一些可以和其他函數(shù)組合的默認(rèn)方法(compose, andThen)。源代碼及使用示例如下:

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123");     // "123"

更多的Function接口

  • BiFunction :R apply(T t, U u);: 接受兩個(gè)參數(shù),返回一個(gè)值,代表一個(gè)二元函數(shù);
  • DoubleFunction :R apply(double value);: 只處理double類型的一元函數(shù);
  • IntFunction :R apply(int value);: 只處理int參數(shù)的一元函數(shù);
  • LongFunction :R apply(long value);: 只處理long參數(shù)的一元函數(shù);
  • ToDoubleFunction:double applyAsDouble(T value);: 返回double的一元函數(shù);
  • ToDoubleBiFunction:double applyAsDouble(T t, U u);: 返回double的二元函數(shù);
  • ToIntFunction:int applyAsInt(T value);: 返回int的一元函數(shù);
  • ToIntBiFunction:int applyAsInt(T t, U u);: 返回int的二元函數(shù);
  • ToLongFunction:long applyAsLong(T value);: 返回long的一元函數(shù);
  • ToLongBiFunction:long applyAsLong(T t, U u);: 返回long的二元函數(shù);
  • DoubleToIntFunction:int applyAsInt(double value);: 接受double返回int的一元函數(shù);
  • DoubleToLongFunction:long applyAsLong(double value);: 接受double返回long的一元函數(shù);
  • IntToDoubleFunction:double applyAsDouble(int value);: 接受int返回double的一元函數(shù);
  • IntToLongFunction:long applyAsLong(int value);: 接受int返回long的一元函數(shù);
  • LongToDoubleFunction:double applyAsDouble(long value);: 接受long返回double的一元函數(shù);
  • LongToIntFunction:int applyAsInt(long value);: 接受long返回int的一元函數(shù);

6 - Operator

Operator 其實(shí)就是 Function,函數(shù)有時(shí)候也叫作算子。算子在Java8中接口描述更像是函數(shù)的補(bǔ)充,和上面的很多類型映射型函數(shù)類似。算子 Operator 包括:UnaryOperatorBinaryOperator。分別對(duì)應(yīng)單(一)元算子和二元算子。

算子的接口聲明如下:

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
    static <T> UnaryOperator<T> identity() {
        return t -> t;
    }
}
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {

    public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
    }

    public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
    }
}

Operator只需聲明一個(gè)泛型參數(shù) T 即可。對(duì)應(yīng)的使用示例如下:

UnaryOperator<Integer> increment = x -> x + 1;
System.out.println("遞增:" + increment.apply(2)); // 輸出 遞增:3

BinaryOperator<Integer> add = (x, y) -> x + y;
System.out.println("相加:" + add.apply(2, 3)); // 輸出 相加:5

BinaryOperator<Integer> min = BinaryOperator.minBy((o1, o2) -> o1 - o2);
System.out.println("最小值:" + min.apply(2, 3)); // 輸出 最小值:2

更多的Operator接口

  • LongUnaryOperator:long applyAsLong(long operand);: 對(duì)long類型做操作的一元算子
  • IntUnaryOperator:int applyAsInt(int operand);: 對(duì)int類型做操作的一元算子
  • DoubleUnaryOperator:double applyAsDouble(double operand);: 對(duì)double類型做操作的一元算子
  • DoubleBinaryOperator:double applyAsDouble(double left, double right);: 對(duì)double類型做操作的二元算子
  • IntBinaryOperator:int applyAsInt(int left, int right);: 對(duì)int類型做操作的二元算子
  • LongBinaryOperator:long applyAsLong(long left, long right);: 對(duì)long類型做操作的二元算子

7 - 其他函數(shù)式接口

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.security.PrivilegedAction
  • java.io.FileFilter
  • java.nio.file.PathMatcher
  • java.lang.reflect.InvocationHandler
  • java.beans.PropertyChangeListener
  • java.awt.event.ActionListener
  • javax.swing.event.ChangeListener

四. 方法引用

1 - 概述

在學(xué)習(xí)了 Lambda 表達(dá)式之后,我們通常使用 Lambda 表達(dá)式來創(chuàng)建匿名方法。然而,有時(shí)候我們僅僅是調(diào)用了一個(gè)已存在的方法。如下:

Arrays.sort(strArray, (s1, s2) -> s1.compareToIgnoreCase(s2));

在 Java 8 中,我們可以直接通過方法引用來簡(jiǎn)寫 Lambda 表達(dá)式中已經(jīng)存在的方法。

Arrays.sort(strArray, String::compareToIgnoreCase);

這種特性就叫做方法引用(Method Reference)。

方法引用是用來直接訪問類或者實(shí)例的已經(jīng)存在的方法或者構(gòu)造方法。方法引用提供了一種引用而不執(zhí)行方法的方式,它需要由兼容的函數(shù)式接口構(gòu)成的目標(biāo)類型上下文。計(jì)算時(shí),方法引用會(huì)創(chuàng)建函數(shù)式接口的一個(gè)實(shí)例。當(dāng) Lambda 表達(dá)式中只是執(zhí)行一個(gè)方法調(diào)用時(shí),不用 Lambda 表達(dá)式,直接通過方法引用的形式可讀性更高一些。方法引用是一種更簡(jiǎn)潔易懂的 Lambda 表達(dá)式。

注意: 方法引用是一個(gè) Lambda 表達(dá)式,其中方法引用的操作符是雙冒號(hào)::。

2 - 分類

方法引用的標(biāo)準(zhǔn)形式是:類名::方法名。(注意:只需要寫方法名,不需要寫括號(hào))

有以下四種形式的方法引用:

  • 引用靜態(tài)方法: ContainingClass::staticMethodName
  • 引用某個(gè)對(duì)象的實(shí)例方法: containingObject::instanceMethodName
  • 引用某個(gè)類型的任意對(duì)象的實(shí)例方法:ContainingType::methodName
  • 引用構(gòu)造方法: ClassName::new

3 - 示例

使用示例如下:

public class Person {

    String name;

    LocalDate birthday;

    public Person(String name, LocalDate birthday) {
        this.name = name;
        this.birthday = birthday;
    }

    public LocalDate getBirthday() {
        return birthday;
    }

    public static int compareByAge(Person a, Person b) {
        return a.birthday.compareTo(b.birthday);
    }

    @Override
    public String toString() {
        return this.name;
    }
}

測(cè)試類:

public class MethodReferenceTest {

    @Test
    public static void main() {
        Person[] pArr = new Person[] {
            new Person("003", LocalDate.of(2016,9,1)),
            new Person("001", LocalDate.of(2016,2,1)),
            new Person("002", LocalDate.of(2016,3,1)),
            new Person("004", LocalDate.of(2016,12,1))
        };

        // 使用匿名類
        Arrays.sort(pArr, new Comparator<Person>() {
            @Override
            public int compare(Person a, Person b) {
                return a.getBirthday().compareTo(b.getBirthday());
            }
        });

        //使用lambda表達(dá)式
        Arrays.sort(pArr, (Person a, Person b) -> {
            return a.getBirthday().compareTo(b.getBirthday());
        });

        //使用方法引用,引用的是類的靜態(tài)方法
        Arrays.sort(pArr, Person::compareByAge);
    }

}

五. Stream 流操作

流是 Java8 中 API 的新成員,它允許你以 聲明式 的方式處理數(shù)據(jù)集合(通過查詢語句來表達(dá),而不是臨時(shí)編寫一個(gè)實(shí)現(xiàn))。這有點(diǎn)兒像是我們操作數(shù)據(jù)庫一樣,例如我想要查詢出熱量較低的菜品名字我就可以像下面這樣:

COPYSELECT name FROM dishes WHERE calorie < 400;

您看,我們并沒有對(duì)菜品的什么屬性進(jìn)行篩選(比如像之前使用迭代器一樣每個(gè)做判斷),我們只是表達(dá)了我們想要什么。那么為什么到了 Java 的集合中,這樣做就不行了呢?

另外一點(diǎn),如果我們想要處理大量的數(shù)據(jù)又該怎么辦?是否是考慮使用多線程進(jìn)行并發(fā)處理呢?如果是,那么可能編寫的關(guān)于并發(fā)的代碼比使用迭代器本身更加的復(fù)雜,而且調(diào)試起來也會(huì)變得麻煩。

基于以上的幾點(diǎn)考慮,Java 設(shè)計(jì)者在 Java 8 版本中 (真正把函數(shù)式編程風(fēng)格引入到 Java 中),引入了流的概念,來幫助您節(jié)約時(shí)間!并且有了 Lambda 的參與,流操作的使用將更加順暢!

1 - 流操作特點(diǎn)

特點(diǎn)一:內(nèi)部迭代

就現(xiàn)在來說,您可以把它簡(jiǎn)單的當(dāng)成一種高級(jí)的迭代器(Iterator),或者是高級(jí)的 for 循環(huán),區(qū)別在于,前面兩者都是屬于外部迭代,而流采用內(nèi)部迭代。

image

上圖簡(jiǎn)要說明了內(nèi)部迭代與外部迭代的差異,我們?cè)倥e一個(gè)生活中實(shí)際的例子(引自《Java 8 實(shí)戰(zhàn)》),比如您想讓您兩歲的孩子索菲亞把她的玩具都收到盒子里面去,你們之間可能會(huì)產(chǎn)生如下的對(duì)話:

  • 你:“索菲亞,我們把玩具收起來吧,地上還有玩具嗎?”
  • 索菲亞:“有,球?!?/li>
  • 你:“好,把球放進(jìn)盒子里面吧,還有嗎?”
  • 索菲亞:“有,那是我的娃娃?!?/li>
  • 你:“好,把娃娃也放進(jìn)去吧,還有嗎?”
  • 索菲亞:“有,有我的書。”
  • 你:“好,把書也放進(jìn)去,還有嗎?”
  • 索菲亞:“沒有了。”
  • 你:“好,我們收好啦?!?/li>

這正是你每天都要對(duì) Java 集合做的事情。你外部迭代了一個(gè)集合,顯式地取出每個(gè)項(xiàng)目再加以處理,但是如果你只是跟索菲亞說:“把地上所有玩具都放進(jìn)盒子里”,那么索菲亞就可以選擇一手拿娃娃一手拿球,或是選擇先拿離盒子最近的那個(gè)東西,再拿其他的東西。

采用內(nèi)部迭代,項(xiàng)目可以透明地并行處理,或者用優(yōu)化的順序進(jìn)行處理,要是使用 Java 過去的外部迭代方法,這些優(yōu)化都是很困難的。

這或許有點(diǎn)雞蛋里挑骨頭,但這差不多就是 Java 8 引入流的原因了——Streams 庫的內(nèi)部迭代可以自動(dòng)選擇一種是和你硬件的數(shù)據(jù)表示和并行實(shí)現(xiàn)。

特點(diǎn)二:只能遍歷一次

請(qǐng)注意,和迭代器一樣,流只能遍歷一次。當(dāng)流遍歷完之后,我們就說這個(gè)流已經(jīng)被消費(fèi)掉了,你可以從原始數(shù)據(jù)那里重新獲得一條新的流,但是卻不允許消費(fèi)已消費(fèi)掉的流。例如下面代碼就會(huì)拋出一個(gè)異常,說流已被消費(fèi)掉了:

List<String> title = Arrays.asList("Wmyskxz", "Is", "Learning", "Java8", "In", "Action");
Stream<String> s = title.stream();
s.forEach(System.out::println);
s.forEach(System.out::println);
// 運(yùn)行上面程序會(huì)報(bào)以下錯(cuò)誤
/*
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
    at java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
    at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
    at Test1.main(Tester.java:17)
*/

特點(diǎn)三:方便的并行處理

Java 8 中不僅提供了方便的一些流操作(比如過濾、排序之類的),更重要的是對(duì)于并行處理有很好的支持,只需要加上 .parallel() 就行了!例如我們使用下面程序來說明一下多線程流操作的方便和快捷,并且與單線程做了一下對(duì)比:

COPYpublic class StreamParallelDemo {

    /** 總數(shù) */
    private static int total = 100_000_000;

    public static void main(String[] args) {
        System.out.println(String.format("本計(jì)算機(jī)的核數(shù):%d", Runtime.getRuntime().availableProcessors()));

        // 產(chǎn)生1000w個(gè)隨機(jī)數(shù)(1 ~ 100),組成列表
        Random random = new Random();
        List<Integer> list = new ArrayList<>(total);

        for (int i = 0; i < total; i++) {
            list.add(random.nextInt(100));
        }

        long prevTime = getCurrentTime();
        list.stream().reduce((a, b) -> a + b).ifPresent(System.out::println);
        System.out.println(String.format("單線程計(jì)算耗時(shí):%d", getCurrentTime() - prevTime));

        prevTime = getCurrentTime();
        // 只需要加上 .parallel() 就行了
        list.stream().parallel().reduce((a, b) -> a + b).ifPresent(System.out::println);
        System.out.println(String.format("多線程計(jì)算耗時(shí):%d", getCurrentTime() - prevTime));

    }

    private static long getCurrentTime() {
        return System.currentTimeMillis();
    }
}

以上程序分別使用了單線程流和多線程流計(jì)算了一千萬個(gè)隨機(jī)數(shù)的和,輸出如下:

本計(jì)算機(jī)的核數(shù):8
655028378
單線程計(jì)算耗時(shí):4159
655028378
多線程計(jì)算耗時(shí):540

并行流的內(nèi)部使用了默認(rèn)的 ForkJoinPool 分支/合并框架,它的默認(rèn)線程數(shù)量就是你的處理器數(shù)量,這個(gè)值是由 Runtime.getRuntime().availableProcessors() 得到的(當(dāng)然我們也可以全局設(shè)置這個(gè)值)。我們也不再去過度的操心加鎖線程安全等一系列問題。

2 - 一些重要方法說明

  • stream: 返回?cái)?shù)據(jù)流,集合作為其源
  • parallelStream: 返回并行數(shù)據(jù)流, 集合作為其源
  • filter: 方法用于過濾出滿足條件的元素
  • map: 方法用于映射每個(gè)元素對(duì)應(yīng)的結(jié)果
  • forEach: 方法遍歷該流中的每個(gè)元素
  • limit: 方法用于減少流的大小
  • sorted: 方法用來對(duì)流中的元素進(jìn)行排序
  • anyMatch: 是否存在任意一個(gè)元素滿足條件(返回布爾值)
  • allMatch: 是否所有元素都滿足條件(返回布爾值)
  • noneMatch: 是否所有元素都不滿足條件(返回布爾值)
  • collect: 方法是終端操作,這是通常出現(xiàn)在管道傳輸操作結(jié)束標(biāo)記流的結(jié)束

3 - 一些使用示例

Filter 過濾

stringCollection
    .stream()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);

Sort 排序

stringCollection
    .stream()
    .sorted()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);

Map 映射

stringCollection
    .stream()
    .map(String::toUpperCase)
    .sorted((a, b) -> b.compareTo(a))
    .forEach(System.out::println);

Match 匹配

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 計(jì)數(shù)

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

Reduce 歸約

這是一個(gè)最終操作,允許通過指定的函數(shù)來將 stream 中的多個(gè)元素規(guī)約為一個(gè)元素,規(guī)越后的結(jié)果是通過 Optional 接口表示的。代碼如下:

Optional<String> reduced = stringCollection
        .stream()
        .sorted()
        .reduce((s1, s2) -> s1 + "#" + s2);
reduced.ifPresent(System.out::println);

想了解更多請(qǐng)參考:https://www.wmyskxz.com/2019/08/03/java8-liu-cao-zuo-ji-ben-shi-yong-xing-neng-ce-shi/

六. Optional

到目前為止,臭名昭著的空指針異常是導(dǎo)致 Java 應(yīng)用程序失敗的最常見原因。以前,為了解決空指針異常,Google公司著名的 Guava 項(xiàng)目引入了 Optional 類,Guava 通過使用檢查空值的方式來防止代碼污染,它鼓勵(lì)程序員寫更干凈的代碼。受到 Google Guava 的啟發(fā),Optional類已經(jīng)成為 Java 8 類庫的一部分。

Optional 實(shí)際上是個(gè)容器:它可以保存類型 T 的值,或者僅僅保存 null。Optional 提供很多有用的方法,這樣我們就不用顯式進(jìn)行空值檢測(cè)。

我們下面用兩個(gè)小例子來演示如何使用 Optional 類:一個(gè)允許為空值,一個(gè)不允許為空值。

Optional<String> fullName = Optional.ofNullable(null);
System.out.println("Full Name is set? " + fullName.isPresent());
System.out.println("Full Name: " + fullName.orElseGet(() -> "[none]"));
System.out.println(fullName.map(s -> "Hey " + s + "!").orElse("Hey Stranger!"));

如果 Optional 類的實(shí)例為非空值的話,isPresent() 返回 true,否從返回 false。為了防止 Optional 為空值,orElseGet() 方法通過回調(diào)函數(shù)來產(chǎn)生一個(gè)默認(rèn)值。map() 函數(shù)對(duì)當(dāng)前 Optional 的值進(jìn)行轉(zhuǎn)化,然后返回一個(gè)新的 Optional 實(shí)例。orElse() 方法和 orElseGet() 方法類似,但是 orElse 接受一個(gè)默認(rèn)值而不是一個(gè)回調(diào)函數(shù)。下面是這個(gè)程序的輸出:

Full Name is set? false
Full Name: [none]
Hey Stranger!

讓我們來看看另一個(gè)例子:

Optional<String> firstName = Optional.of("Tom");
System.out.println("First Name is set? " + firstName.isPresent());
System.out.println("First Name: " + firstName.orElseGet(() -> "[none]"));
System.out.println(firstName.map(s -> "Hey " + s + "!").orElse("Hey Stranger!"));
System.out.println();

下面是程序的輸出:

First Name is set? true
First Name: Tom
Hey Tom!

Lambda 配合 Optinal 優(yōu)雅解決 null

這里假設(shè)我們有一個(gè) person object,以及一個(gè) person object 的 Optional wrapper:

image

Optional<T> 如果不結(jié)合 Lambda 使用的話,并不能使原來繁瑣的 null check 變的簡(jiǎn)單。

image

只有當(dāng) Optional<T> 結(jié)合 Lambda 一起使用的時(shí)候,才能發(fā)揮出其真正的威力!

我們現(xiàn)在就來對(duì)比一下下面四種常見的 null 處理中,Java 8 的 Lambda + Optional<T> 和傳統(tǒng) Java 兩者之間對(duì)于 null 的處理差異。

情況一:存在則繼續(xù)

image

情況二:存在則返回,無則返回不存在

image

情況三:存在則返回,無則由函數(shù)產(chǎn)生

image

情況四:奪命連環(huán) null 檢查

image

由上述四種情況可以清楚地看到,Optional<T> + Lambda 可以讓我們少寫很多 ifElse 塊。尤其是對(duì)于情況四那種奪命連環(huán) null 檢查,傳統(tǒng) Java 的寫法顯得冗長(zhǎng)難懂,而新的 Optional<T> +Lambda 則清新脫俗,清楚簡(jiǎn)潔。

七. Data/Time API

Java 8 在包 java.time下包含了一組全新的時(shí)間日期API。新的日期API和開源的 Joda-Time庫差不多,但又不完全一樣,下面的例子展示了這組新API里最重要的一些部分:

1 - Clock 時(shí)鐘

Clock類提供了訪問當(dāng)前日期和時(shí)間的方法,Clock 是時(shí)區(qū)敏感的,可以用來取代 System.currentTimeMillis() 來獲取當(dāng)前的微秒數(shù)。某一個(gè)特定的時(shí)間點(diǎn)也可以使用 Instant 類來表示,Instant 類也可以用來創(chuàng)建老的 java.util.Date 對(duì)象。代碼如下:

Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();
Instant instant = clock.instant();
Date legacyDate = Date.from(instant);   // legacy java.util.Date

2 - Timezones 時(shí)區(qū)

在新 AP I中時(shí)區(qū)使用 ZoneId 來表示。時(shí)區(qū)可以很方便的使用靜態(tài)方法 of 來獲取到。時(shí)區(qū)定義了到 UTS 時(shí)間的時(shí)間差,在 Instant 時(shí)間點(diǎn)對(duì)象到本地日期對(duì)象之間轉(zhuǎn)換的時(shí)候是極其重要的。代碼如下:

System.out.println(ZoneId.getAvailableZoneIds());
// prints all available timezone ids
ZoneId zone1 = ZoneId.of("Europe/Berlin");
ZoneId zone2 = ZoneId.of("Brazil/East");
System.out.println(zone1.getRules());
System.out.println(zone2.getRules());
// ZoneRules[currentStandardOffset=+01:00]
// ZoneRules[currentStandardOffset=-03:00]

3 - LocalTime 本地時(shí)間

LocalTime定義了一個(gè)沒有時(shí)區(qū)信息的時(shí)間,例如 晚上 10 點(diǎn),或者 17:30:15。下面的例子使用前面代碼創(chuàng)建的時(shí)區(qū)創(chuàng)建了兩個(gè)本地時(shí)間。之后比較時(shí)間并以小時(shí)和分鐘為單位計(jì)算兩個(gè)時(shí)間的時(shí)間差。代碼如下:

LocalTime now1 = LocalTime.now(zone1);
LocalTime now2 = LocalTime.now(zone2);
System.out.println(now1.isBefore(now2));  // false
long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);
System.out.println(hoursBetween);       // -3
System.out.println(minutesBetween);     // -239

LocalTime提供了多種工廠方法來簡(jiǎn)化對(duì)象的創(chuàng)建,包括解析時(shí)間字符串。代碼如下:

LocalTime late = LocalTime.of(23, 59, 59);
System.out.println(late);       // 23:59:59
DateTimeFormatter germanFormatter = DateTimeFormatter
        .ofLocalizedTime(FormatStyle.SHORT)
        .withLocale(Locale.GERMAN);
LocalTime leetTime = LocalTime.parse("13:37", germanFormatter);
System.out.println(leetTime);   // 13:37

4 - LocalData 本地日期

LocalDate 表示了一個(gè)確切的日期,比如 2014-03-11。該對(duì)象值是不可變的,用起來和 LocalTime 基本一致。下面的例子展示了如何給 Date 對(duì)象加減天/月/年。另外要注意的是這些對(duì)象是不可變的,操作返回的總是一個(gè)新實(shí)例。代碼如下:

LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays(2);
LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();

System.out.println(dayOfWeek);    // FRIDAY

從字符串解析一個(gè) LocalDate 類型和解析 LocalTime 一樣簡(jiǎn)單。代碼如下:

DateTimeFormatter germanFormatter = DateTimeFormatter
        .ofLocalizedDate(FormatStyle.MEDIUM)
        .withLocale(Locale.GERMAN);
LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
System.out.println(xmas);   // 2014-12-24

5 - LocalDateTime 本地日期時(shí)間

LocalDateTime同時(shí)表示了時(shí)間和日期,相當(dāng)于前兩節(jié)內(nèi)容合并到一個(gè)對(duì)象上了。LocalDateTimeLocalTime還有LocalDate一樣,都是不可變的。LocalDateTime提供了一些能訪問具體字段的方法。代碼如下:

LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);
DayOfWeek dayOfWeek = sylvester.getDayOfWeek();
System.out.println(dayOfWeek);      // WEDNESDAY
Month month = sylvester.getMonth();
System.out.println(month);          // DECEMBER
long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
System.out.println(minuteOfDay);    // 1439

只要附加上時(shí)區(qū)信息,就可以將其轉(zhuǎn)換為一個(gè)時(shí)間點(diǎn)Instant對(duì)象,Instant時(shí)間點(diǎn)對(duì)象可以很容易的轉(zhuǎn)換為老式的java.util.Date。代碼如下:

Instant instant = sylvester
        .atZone(ZoneId.systemDefault())
        .toInstant();
Date legacyDate = Date.from(instant);
System.out.println(legacyDate);     // Wed Dec 31 23:59:59 CET 2014

格式化LocalDateTime和格式化時(shí)間和日期一樣的,除了使用預(yù)定義好的格式外,我們也可以自己定義格式。代碼如下:

DateTimeFormatter formatter =
    DateTimeFormatter
        .ofPattern("MMM dd, yyyy - HH:mm");
LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter);
String string = formatter.format(parsed);
System.out.println(string);     // Nov 03, 2014 - 07:13

java.text.NumberFormat不一樣的是新版的DateTimeFormatter是不可變的,所以它是線程安全的。

八. 重復(fù)注解

自從 Java 5 引入了注解機(jī)制,這一特性就變得非常流行并且廣為使用。然而,使用注解的一個(gè)限制是相同的注解在同一位置只能聲明一次,不能聲明多次。Java 8 打破了這條規(guī)則,引入了重復(fù)注解機(jī)制,這樣相同的注解可以在同一地方聲明多次。

重復(fù)注解機(jī)制本身必須用 @Repeatable 注解。事實(shí)上,這并不是語言層面上的改變,更多的是編譯器的技巧,底層的原理保持不變。讓我們看一個(gè)快速入門的例子:

import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

public class RepeatingAnnotations {

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Filters {
        Filter[] value();
    }

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Repeatable(Filters.class)
    public @interface Filter {
        String value();
    };

    @Filter("filter1")
    @Filter("filter2")
    public interface Filterable {
    }

    public static void main(String[] args) {
        for(Filter filter: Filterable.class.getAnnotationsByType(Filter.class)) {
            System.out.println(filter.value());
        }
    }

}

正如我們看到的,這里有個(gè)使用 @Repeatable(Filters.class) 注解的注解類 Filter,Filters僅僅是 Filter 注解的數(shù)組,但Java編譯器并不想讓程序員意識(shí)到 Filters 的存在。這樣,接口 Filterable 就擁有了兩次 Filter(并沒有提到Filter)注解。

同時(shí),反射相關(guān)的API提供了新的函數(shù)getAnnotationsByType()來返回重復(fù)注解的類型(請(qǐng)注意Filterable.class.getAnnotation(Filters.class)`經(jīng)編譯器處理后將會(huì)返回Filters的實(shí)例)。

九. 擴(kuò)展注解的支持

Java 8 擴(kuò)展了注解的上下文。現(xiàn)在幾乎可以為任何東西添加注解:局部變量、泛型類、父類與接口的實(shí)現(xiàn),就連方法的異常也能添加注解。下面演示幾個(gè)例子:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collection;

public class Annotations {

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER })
    public @interface NonEmpty {
    }

    public static class Holder<@NonEmpty T> extends @NonEmpty Object {
        public void method() throws @NonEmpty Exception {
        }
    }

    @SuppressWarnings("unused")
    public static void main(String[] args) {
        final Holder<String> holder = new @NonEmpty Holder<String>();
        @NonEmpty Collection<@NonEmpty String> strings = new ArrayList<>();
    }

}

十. Base64

在 Java 8 中,Base64 編碼已經(jīng)成為 Java 類庫的標(biāo)準(zhǔn)。它的使用十分簡(jiǎn)單,下面讓我們看一個(gè)例子:

import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class Base64s {

    public static void main(String[] args) {
        final String text = "Base64 finally in Java 8!";

        final String encoded = Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.UTF_8));
        System.out.println(encoded);

        final String decoded = new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8);
        System.out.println(decoded);
    }

}

程序在控制臺(tái)上輸出了編碼后的字符與解碼后的字符:

QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
Base64 finally in Java 8!

Base64 類同時(shí)還提供了對(duì) URL、MIME 友好的編碼器與解碼器(Base64.getUrlEncoder() / Base64.getUrlDecoder(), Base64.getMimeEncoder() / Base64.getMimeDecoder())。

十一. JavaFX

JavaFX是一個(gè)強(qiáng)大的圖形和多媒體處理工具包集合,它允許開發(fā)者來設(shè)計(jì)、創(chuàng)建、測(cè)試、調(diào)試和部署富客戶端程序,并且和Java一樣跨平臺(tái)。從Java8開始,JavaFx已經(jīng)內(nèi)置到了JDK中。關(guān)于JavaFx更詳細(xì)的文檔可參考JavaFX中文文檔。

十二. 其它

1. JDBC4.2規(guī)范

JDBC4.2主要有以下幾點(diǎn)改動(dòng):

  • 增加了對(duì)REF Cursor的支持
  • 修改返回值大小范圍(update count)
  • 增加了java.sql.DriverAction接口
  • 增加了java.sql.SQLType接口
  • 增加了java.sql.JDBCtype枚舉
  • 對(duì)java.time包時(shí)間類型的支持

2. 更好的類型推測(cè)機(jī)制

Java 8 在類型推測(cè)方面有了很大的提高。在很多情況下,編譯器可以推測(cè)出確定的參數(shù)類型,這樣就能使代碼更整潔。讓我們看一個(gè)例子:

public class Value<T> {

    public static<T> T defaultValue() {
        return null;
    }

    public T getOrDefault(T value, T defaultValue) {
        return (value != null) ? value : defaultValue;
    }

}

這里是Value<String>類型的用法。

public class TypeInference {

    public static void main(String[] args) {
        final Value<String> value = new Value<>();
        value.getOrDefault("22", Value.defaultValue());
    }

}

Value.defaultValue()的參數(shù)類型可以被推測(cè)出,所以就不必明確給出。在Java 7中,相同的例子將不會(huì)通過編譯,正確的書寫方式是Value.<String>defaultValue()。

3. HashMap性能提升

Java 8 中,HashMap 內(nèi)部實(shí)現(xiàn)又引入了紅黑樹,使得 HashMap 的總體性能相較于 Java 7 有比較明顯的提升。以下是對(duì) Hash 均勻和不均勻的情況下的性能對(duì)比

Hash較均勻的情況

Hash較均勻時(shí)的性能對(duì)比

Hash極不均勻的情況

Hash極不均勻時(shí)的性能對(duì)比

想要了解更多 HashMap 的童鞋戳這里吧:傳送門

4. IO/NIO 的改進(jìn)

Java 8 對(duì)IO/NIO也做了一些改進(jìn)。主要包括:改進(jìn)了java.nio.charset.Charset的實(shí)現(xiàn),使編碼和解碼的效率得以提升,也精簡(jiǎn)了jre/lib/charsets.jar包;優(yōu)化了String(byte[], *)構(gòu)造方法和String.getBytes()方法的性能;還增加了一些新的IO/NIO方法,使用這些方法可以從文件或者輸入流中獲取流(java.util.stream.Stream),通過對(duì)流的操作,可以簡(jiǎn)化文本行處理、目錄遍歷和文件查找。

新增的 API 如下:

  • BufferedReader.line(): 返回文本行的流Stream<String>
  • File.lines(Path, Charset): 返回文本行的流Stream<String>
  • File.list(Path): 遍歷當(dāng)前目錄下的文件和目錄
  • File.walk(Path, int, FileVisitOption): 遍歷某一個(gè)目錄下的所有文件和指定深度的子目錄
  • File.find(Path, int, BiPredicate, FileVisitOption...): 查找相應(yīng)的文件

下面就是用流式操作列出當(dāng)前目錄下的所有文件和目錄:

Files.list(new File(".").toPath()).forEach(System.out::println);

5. JavaScript 引擎 Nashorn

Java 8 提供了一個(gè)新的Nashorn javascript引擎,它允許我們?cè)?JVM 上運(yùn)行特定的 javascript 應(yīng)用。Nashorn javascript 引擎只是javax.script.ScriptEngine另一個(gè)實(shí)現(xiàn),而且規(guī)則也一樣,允許Java和JavaScript互相操作。這里有個(gè)小例子:

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");

System.out.println(engine.getClass().getName());
System.out.println("Result:" + engine.eval("function f(){return 1;}; f() + 1;"));

輸出如下:

jdk.nashorn.api.scripting.NashornScriptEngine
Result: 2

6. 并發(fā)(Concurrency)

在新增Stream機(jī)制與Lambda的基礎(chǔ)之上,在java.util.concurrent.ConcurrentHashMap中加入了一些新方法來支持聚集操作。同時(shí)也在java.util.concurrent.ForkJoinPool類中加入了一些新方法來支持共有資源池(common pool)(請(qǐng)查看我們關(guān)于Java 并發(fā)的免費(fèi)課程)。

新增的java.util.concurrent.locks.StampedLock類提供一直基于容量的鎖,這種鎖有三個(gè)模型來控制讀寫操作(它被認(rèn)為是不太有名的java.util.concurrent.locks.ReadWriteLock類的替代者)。

java.util.concurrent.atomic包中還增加了下面這些類:

  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator
  • LongAdder

7. 類依賴分析器jdeps

Jdeps是一個(gè)功能強(qiáng)大的命令行工具,它可以幫我們顯示出包層級(jí)或者類層級(jí)java類文件的依賴關(guān)系。它接受class文件、目錄、jar文件作為輸入,默認(rèn)情況下,jdeps會(huì)輸出到控制臺(tái)。

作為例子,讓我們看看現(xiàn)在很流行的 Spring 框架的庫的依賴關(guān)系報(bào)告。為了讓報(bào)告短一些,我們只分析一個(gè) jar: org.springframework.core-3.0.5.RELEASE.jar.

jdeps org.springframework.core-3.0.5.RELEASE.jar這個(gè)命令輸出內(nèi)容很多,我們只看其中的一部分,這些依賴關(guān)系根絕包來分組,如果依賴關(guān)系在classpath里找不到,就會(huì)顯示 not found。

C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar
   org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar)
      -> java.io
      -> java.lang
      -> java.lang.annotation
      -> java.lang.ref
      -> java.lang.reflect
      -> java.util
      -> java.util.concurrent
      -> org.apache.commons.logging                         not found
      -> org.springframework.asm                            not found
      -> org.springframework.asm.commons                    not found
   org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar)
      -> java.lang
      -> java.lang.annotation
      -> java.lang.reflect
      -> java.util

8. JVM 的 PermGen 空間被移除

PermGen空間被移除了,取而代之的是Metaspace(JEP 122)。JVM 選項(xiàng)-XX:PermSize-XX:MaxPermSize分別被-XX:MetaSpaceSize-XX:MaxMetaspaceSize所代替。

區(qū)別:

  1. 元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存
  2. 默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制
  3. 也可以通過-XX:MetaspaceSize指定元空間大小

參考資料

  1. 「MoreThanJava」Day 7:接口詳解 - https://www.wmyskxz.com/2020/08/13/morethanjava-day-7-jie-kou-xiang-jie/
  2. 【知乎問題】Lambda 表達(dá)式 有何用處?如何使用? | @Mingqi - https://www.zhihu.com/question/20125256
  3. Java8新特性及使用(一) | 閃爍之狐 - http://blinkfox.com/2018/11/13/hou-duan/java/java8-xin-te-xing-ji-shi-yong-yi/#toc-heading-21
  4. Java8新特性及使用(二) | 閃爍之狐 - http://blinkfox.com/2018/11/14/hou-duan/java/java8-xin-te-xing-ji-shi-yong-er/

文章推薦

  1. 這都JDK15了,JDK7還不了解? - https://www.wmyskxz.com/2020/08/18/java7-ban-ben-te-xing-xiang-jie/
  2. 你記筆記嗎?關(guān)于最近知識(shí)管理工具革新潮心臟有話要說 - https://www.wmyskxz.com/2020/08/16/ni-ji-bi-ji-ma-guan-yu-zui-jin-zhi-shi-guan-li-gong-ju-ge-xin-chao-xin-zang-you-hua-yao-shuo/
  3. 黑莓OS手冊(cè)是如何詳細(xì)闡述底層的進(jìn)程和線程模型的? - https://www.wmyskxz.com/2020/07/31/hao-wen-tui-jian-hei-mei-os-shou-ce-shi-ru-he-xiang-xi-chan-shu-di-ceng-de-jin-cheng-he-xian-cheng-mo-xing-de/
  4. 「MoreThanJava」系列文集 - https://www.wmyskxz.com/categories/MoreThanJava/
  • 本文已收錄至我的 Github 程序員成長(zhǎng)系列 【More Than Java】,學(xué)習(xí),不止 Code,歡迎 star:https://github.com/wmyskxz/MoreThanJava
  • 個(gè)人公眾號(hào) :wmyskxz,個(gè)人獨(dú)立域名博客:wmyskxz.com,堅(jiān)持原創(chuàng)輸出,下方掃碼關(guān)注,2020,與您共同成長(zhǎng)!
image

非常感謝各位人才能 看到這里,如果覺得本篇文章寫得不錯(cuò),覺得 「我沒有三顆心臟」有點(diǎn)東西 的話,求點(diǎn)贊,求關(guān)注,求分享,求留言!

創(chuàng)作不易,各位的支持和認(rèn)可,就是我創(chuàng)作的最大動(dòng)力,我們下篇文章見!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容