Java 8 新特性

三木同學發表於2022-06-08

Java 8 (又稱為 jdk 1.8) 是 Java 語言開發的一個主要版本。 Oracle 公司於 2014 年 3 月 18 日釋出 Java 8 ,這個版本包含語言、編譯器、庫、工具和JVM等方面的十多個新特性。 下面就來介紹下語言方面的新特性。

語法相關新特性

預設介面方法

從 Java 8 開始,介面支援定義預設實現方法。所謂的預設方法,就是指介面中定義的抽象方法可以由介面本身提供預設實現,而不一定要實現類去實現這個抽象方法。

比如我定義了一個 Programmer 介面,在 Java 8 之前,我們必須在實現類中實現所有的抽象方法,比如下面的 JavaProgrammer 類。

package com.csx.feature.defaultm;
public interface Programmer {
    /**
     * 程式設計操作
     */
    void coding();

    /**
     * 介紹自己
     * @return
     */
    String introduce();
}

實現類:

package com.csx.feature.defaultm;
public class JavaProgrammer implements Programmer {
    @Override
    public void coding() {
        System.out.println("l am writing a bug...");
    }

    @Override
    public String introduce() {
        return "hi, l am a Java programmer";
    }
}

在 Java 8 中,我們可以使用 default 關鍵字來定義介面中的預設方法實現。比如下面我們將introduce方法定義成一個具有預設實現的方法。

package com.csx.feature.defaultm;
public interface Programmer {

    /**
     * 程式設計操作
     */
    void coding();

    /**
     * 介紹自己
     * @return
     */
    default String introduce(){
        return "hi, l am a C++ programmer";
    }
}

我們再定義一個實現類,就不需要再實現這個方法了(當然,實現類中還是可以實現這個方法的,實現的方法會將介面的預設方法覆蓋)。

package com.csx.feature.defaultm;
public class CJJProgrammer implements Programmer {
    @Override
    public void coding() {
        System.out.println("l am writing a bug.......");
    }
}

上面的實現類 CJJProgrammer 就不再必須要實現 introduce 方法,因為這個方法在介面中已經有預設實現了。

使用介面預設方法的最主要目的是:修改介面後不需要大範圍的修改以前老的實現類

比如說現在給 Programmer 介面新新增一個新的方法 reading():

package com.csx.feature.defaultm;
public interface Programmer {
    
    void reading();
       
    void coding();

    default String introduce(){
        return "hi, l am a C++ programmer";
    }

}

新新增這個方法後,每個實現類中必須也加上這個方法的實現,不然程式碼編譯會報錯。假如之前的實現類很多的話,那麼修改的工作量將是非常大的。這時你就可以為這個方法提供預設實現:

public interface Programmer {
    
    default void reading(){
        System.out.println("reading 11.11 shopping list...");
    }
       
    void coding();

    default String introduce(){
        return "hi, l am a C++ programmer";
    }

}

多個預設方法

考慮這樣的情況,一個類實現了多個介面,且這些介面有相同的預設方法。

public interface Vehicle {
   default void print(){
      System.out.println("我是一輛車!");
   }
}
 
public interface FourWheeler {
   default void print(){
      System.out.println("我是一輛四輪車!");
   }
}

第一個解決方案是建立自己的預設方法,來覆蓋重寫介面的預設方法:

public class Car implements Vehicle, FourWheeler {
   default void print(){
      System.out.println("我是一輛四輪汽車!");
   }
}

第二種解決方案可以使用 super 來呼叫指定介面的預設方法:

public class Car implements Vehicle, FourWheeler {
   public void print(){
      Vehicle.super.print();
   }
}

靜態預設方法

Java 8 的另一個特性是介面可以宣告(並且可以提供實現)靜態方法。

package com.csx.feature.defaultm;
public interface Programmer {
    
    static final String  BLOG = "程式設計師自由之路";
   
    static String blogName(){
        return BLOG;
    }
    
    void coding();

    default String introduce(){
        return "hi, l am a C++ programmer";
    }
}

Lambda 表示式

Lambda 表示式,也可稱為閉包,它是推動 Java 8 釋出的最重要新特性。Lambda 允許把函式作為一個方法的引數(函式作為引數傳遞進方法中)。

使用 Lambda 表示式可以使程式碼變的更加簡潔緊湊,其本質是一個Java語法糖,具體內容可以參考我的部落格Java中的語法糖中關於Lambda的章節。

Lambda 表示式的語法如下:


(p1) -> exp;
或者
(p1,p2) -> {
    exp1;
    exp2;
}

以下是lambda表示式的重要特徵:

  • 可選型別宣告:不需要宣告引數型別,編譯器可以統一識別引數值。
  • 可選的引數圓括號:一個引數無需定義圓括號,但多個引數需要定義圓括號。
  • 可選的大括號:如果主體包含了一個語句,就不需要使用大括號。
  • 可選的返回關鍵字:如果主體只有一個表示式返回值則編譯器會自動返回值,大括號需要指定明表示式返回了一個數值。

lambda 表示式的最大作用是簡化了匿名內部類的使用,是的程式碼看起來更加簡潔。(注意:Lambda 表示式並不能提升程式碼執行的效率)。
lambda 表示式還有一個作用就是推動了 Java 中的函式化程式設計,使得可以將一個函式作為引數傳給方法。(之前必須傳一個物件的引用給方法,然後再通過這個物件引用呼叫具體的方法)。

變數作用域

lambda 表示式只能引用不被修改的外層區域性變數,否則會編譯錯誤。

int num = 1;  
Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
s.convert(2);
// 這個 num 變數被 lambda 表示式引用,又被修改了,所以會編譯報錯。
num = 5;

其實如果lambda表示式引用了外層的區域性變數,編譯器會自動將這個變數設定成final修飾。

final int num = 1;  
Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
s.convert(2);
// final變數賦值後不能修改,所以會編譯報錯。
// num = 5;

函式式介面

函式式介面(Functional Interface)就是一個有且僅有一個抽象方法,但是可以有多個非抽象方法的介面。函式式介面可以被隱式轉換為 lambda 表示式。

@FunctionalInterface
interface GreetingService {
    // 非抽象介面可以有多個
    static void sayBye(){
        System.out.println("bye");
    }

    void sayMessage(String message);
}

那麼就可以使用Lambda表示式來表示該介面的一個實現(注:JAVA 8 之前一般是用匿名類實現的):

GreetingService greetService1 = message -> System.out.println("Hello " + message);

從 Java 8 開始,很多之前的介面,都被調整成函式式介面:

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

同時,Java 8 也增加了很多新的函式式介面,主要在java.util.function這個包下面。常用的有:

  • Predicate:接受一個輸入引數,返回一個布林值結果。

  • Supplier:無引數,返回一個結果。

  • Consumer:代表了接受一個輸入引數並且無返回的操作

在實踐中,函式式介面非常脆弱:只要某個開發者在該介面中新增一個函式,則該介面就不再是函式式介面進而導致編譯失敗。為了克服這種程式碼層面的脆弱性,並顯式說明某個介面是函式式介面,Java 8 提供了一個特殊的註解@FunctionalInterface(Java 庫中的所有相關介面都已經帶有這個註解了)。

不過有一點需要注意,預設方法和靜態方法不會破壞函式式介面的定義,因此如下的程式碼是合法的。

@FunctionalInterface
interface GreetingService {
    // 非抽象介面可以有多個
    static void sayBye(){
        System.out.println("bye");   
    }
   
   default void sayHi() {            
        System.out.println("bye");   
    }        
  
    void sayMessage(String message);
}

方法引用

在學習lambda表示式之後,我們通常使用lambda表示式來建立匿名方法。然而,有時候我們僅僅是呼叫了一個已存在的方法。如下:

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

在Java8中,我們可以直接通過方法引用來簡寫lambda表示式中已經存在的方法。

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

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

方法引用是用來直接訪問類或者例項的已經存在的方法或者構造方法。方法引用提供了一種引用而不執行方法的方式,它需要由相容的函式式介面構成的目標型別上下文。計算時,方法引用會建立函式式介面的一個例項。我們需要把握的重點是:函式引用只是簡化Lambda表示式的一種手段而已。

當Lambda表示式中只是執行一個方法呼叫時,不用Lambda表示式,直接通過方法引用的形式可讀性更高一些。方法引用是一種更簡潔易懂的Lambda表示式。

注意方法引用是一個特殊的Lambda表示式,其中方法引用的操作符是雙冒號"::"。

具體關於方法引用的內容,請參考這篇文章

下面就舉個列子:

方法引用的標準形式是:類名::方法名。(注意:只需要寫方法名,不需要寫括號

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

型別 示例
引用靜態方法 ContainingClass::staticMethodName
引用某個物件的例項方法 containingObject::instanceMethodName
引用某個型別的任意物件的例項方法 ContainingType::methodName
引用構造方法 ClassName::new

1、靜態方法引用

組成語法格式:ClassName::staticMethodName

注意:

  • 靜態方法引用比較容易理解,和靜態方法呼叫相比,只是把 . 換為 ::
  • 在目標型別相容的任何地方,都可以使用靜態方法引用。

例子:

  String::valueOf 等價於lambda表示式 (s) -> String.valueOf(s);

  Math::pow 等價於lambda表示式 (x, y) -> Math.pow(x, y);

2、特定例項物件的方法引用

這種語法與用於靜態方法的語法類似,只不過這裡使用物件引用而不是類名。****例項方法引用又分以下三種型別:

  • 例項上的例項方法引用

    組成語法格式:instanceReference::methodName

  • 超類上的例項方法引用

    組成語法格式:super::methodName

    方法的名稱由methodName指定,通過使用super,可以引用方法的超類版本。

  • 型別上的例項方法引用

    組成語法格式:ClassName::methodName (會先建立一個物件??)

3、任意物件(屬於同一個類)的例項方法引用

如下示例,這裡引用的是字串陣列中任意一個物件的compareToIgnoreCase方法。

String[] stringArray = { "Barbara", "James", "Mary", "John", "Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);

4、構造方法引用

構造方法引用又分構造方法引用和陣列構造方法引用。

a.構造方法引用(也可以稱作構造器引用)

組成語法格式:Class::new

建構函式本質上是靜態方法,只是方法名字比較特殊,使用的是new 關鍵字

例子:String::new, 等價於lambda表示式 () -> new String()

b.陣列構造方法引用

組成語法格式:TypeName[]::new

例子:int[]::new 是一個含有一個引數的構造器引用,這個引數就是陣列的長度。等價於lambda表示式 x -> new int[x]。

假想存在一個接收int引數的陣列構造方法

IntFunction<int[]> arrayMaker = int[]::new;
int[] array = arrayMaker.apply(10) // 建立陣列 int[10]

支援重複註解並拓寬註解的應用場景

自從Java 5中引入註解以來,這個特性開始變得非常流行,並在各個框架和專案中被廣泛使用。不過,註解有一個很大的限制是:在同一個地方不能多次使用同一個註解。Java 8打破了這個限制,引入了重複註解的概念,允許在同一個地方多次使用同一個註解。

在Java 8中使用@Repeatable註解定義重複註解,實際上,這並不是語言層面的改進,而是編譯器做的一個trick,底層的技術仍然相同。

Java 8拓寬了註解的應用場景。現在,註解幾乎可以使用在任何元素上:區域性變數、介面型別、超類和介面實現類,甚至可以用在函式的異常定義上。

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

ElementType.TYPE_USER和ElementType.TYPE_PARAMETER是Java 8新增的兩個註解,用於描述註解的使用場景。Java 語言也做了對應的改變,以識別這些新增的註解。

具體支援哪些使用場景,建議檢視ElementType這個類。

工具相關新特性

Stream API

Java 8 API新增了一個新的抽象稱為流Stream,可以讓你以一種宣告的方式處理資料。

Stream 使用一種類似用 SQL 語句從資料庫查詢資料的直觀方式來提供一種對 Java 集合運算和表達的高階抽象。

Stream API可以極大提高Java程式設計師的生產力,讓程式設計師寫出高效率、乾淨、簡潔的程式碼。

這種風格將要處理的元素集合看作一種流, 流在管道中傳輸, 並且可以在管道的節點上進行處理, 比如篩選, 排序,聚合等。

元素流在管道中經過中間操作(intermediate operation)的處理,最後由最終操作(terminal operation)得到前面處理的結果。

關於 Stream API 的具體使用方式,我之前寫過一篇文章詳細介紹過。點選談談集合.Stream API前往閱讀。

Optional 類

Java應用中最常見的bug就是空值異常。在Java 8之前,Google Guava引入了Optionals類來解決NullPointerException,從而避免原始碼被各種null檢查汙染,以便開發者寫出更加整潔的程式碼。Java 8也將Optional加入了官方庫。

Optional僅僅是一個容易:存放T型別的值或者null。它提供了一些有用的介面來避免顯式的null檢查,可以參考Java 8官方文件瞭解更多細節。

關於這個類的具體使用方式,可以參考我整理的這篇文章

另外,建議大家看看流行的開源框架中,這些新特性是怎麼使用的。我們在沒有熟練掌握這些新功能之前,不妨模仿下這些框架的用法,也不失為一種好的學習方法。

時間API

Java 8引入了新的Date-Time API(JSR 310)來改進時間、日期的處理。時間和日期的管理一直是最令Java開發者痛苦的問題。java.util.Date和後來的java.util.Calendar一直沒有解決這個問題(甚至令開發者更加迷茫)。

因為上面這些原因,誕生了第三方庫Joda-Time,可以替代Java的時間管理API。Java 8中新的時間和日期管理API深受Joda-Time影響,並吸收了很多Joda-Time的精華。新的java.time包包含了所有關於日期、時間、時區、Instant(跟日期類似但是精確到納秒)、duration(持續時間)和時鐘操作的類。新設計的API認真考慮了這些類的不變性(從java.util.Calendar吸取的教訓),如果某個例項需要修改,則返回一個新的物件。

關於時間API的詳細使用,可以參考我之前整理的文章

Nashorn JavaScript引擎

Java 8提供了新的Nashorn JavaScript引擎,使得我們可以在JVM上開發和執行JS應用。Nashorn JavaScript引擎是javax.script.ScriptEngine的另一個實現版本,這類Script引擎遵循相同的規則,允許Java和JavaScript互動使用,例子程式碼如下:

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

Base64

對Base64編碼的支援已經被加入到Java 8官方庫中,這樣不需要使用第三方庫就可以進行Base64編碼,例子程式碼如下:

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

新的Base64API也支援URL和MINE的編碼解碼。
(Base64.getUrlEncoder() / Base64.getUrlDecoder(), Base64.getMimeEncoder() / Base64.getMimeDecoder())。

並行陣列

Java8版本新增了很多新的方法,用於支援並行陣列處理。最重要的方法是parallelSort(),可以顯著加快多核機器上的陣列排序。下面的例子論證了parallexXxx系列的方法:

package com.javacodegeeks.java8.parallel.arrays;
 
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
 
public class ParallelArrays {
    public static void main( String[] args ) {
        long[] arrayOfLong = new long [ 20000 ];        
 
        Arrays.parallelSetAll( arrayOfLong, 
            index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
        Arrays.stream( arrayOfLong ).limit( 10 ).forEach( 
            i -> System.out.print( i + " " ) );
        System.out.println();
 
        Arrays.parallelSort( arrayOfLong );        
        Arrays.stream( arrayOfLong ).limit( 10 ).forEach( 
            i -> System.out.print( i + " " ) );
        System.out.println();
    }
}

上述這些程式碼使用parallelSetAll()方法生成20000個隨機數,然後使用parallelSort()方法進行排序。這個程式會輸出亂序陣列和排序陣列的前10個元素。

併發相關新特性

基於新增的lambda表示式和steam特性,為Java 8中為java.util.concurrent.ConcurrentHashMap類新增了新的方法來支援聚焦操作;另外,也為java.util.concurrentForkJoinPool類新增了新的方法來支援通用執行緒池操作。

Java 8還新增了新的java.util.concurrent.locks.StampedLock類,用於支援基於容量的鎖——該鎖有三個模型用於支援讀寫操作(可以把這個鎖當做是java.util.concurrent.locks.ReadWriteLock的替代者)。

java.util.concurrent.atomic包中也新增了不少工具類,列舉如下:

  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator
  • LongAdder

Java命令列工具相關

Nashorn引擎:jjs

jjs是一個基於標準Nashorn引擎的命令列工具,可以接受js原始碼並執行。例如,我們寫一個func.js檔案,內容如下:

function f() { 
     return 1; 
}; 
 
print( f() + 1 );

可以在命令列中執行這個命令:jjs func.js,控制檯輸出結果是:

2

如果需要了解細節,可以參考官方文件

類依賴分析器:jdeps

jdeps是一個相當棒的命令列工具,它可以展示包層級和類層級的Java類依賴關係,它以.class檔案、目錄或者Jar檔案為輸入,然後會把依賴關係輸出到控制檯。

我們可以利用jedps分析下Spring Framework庫,為了讓結果少一點,僅僅分析一個JAR檔案:org.springframework.core-3.0.5.RELEASE.jar

jdeps org.springframework.core-3.0.5.RELEASE.jar

這個命令會輸出很多結果,我們僅看下其中的一部分:依賴關係按照包分組,如果在classpath上找不到依賴,則顯示"not found".

org.springframework.core-3.0.5.RELEASE.jar -> 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

更多的細節可以參考官方文件

編譯器相關特性

1. 獲取方法的引數名稱

為了在執行時獲得Java程式中方法的引數名稱,老一輩的Java程式設計師必須使用不同方法,例如Paranamer liberary。Java 8終於將這個特性規範化,在語言層面(使用反射API和Parameter.getName()方法)和位元組碼層面(使用新的javac編譯器以及-parameters引數)提供支援。

package com.javacodegeeks.java8.parameter.names;
 
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
 
public class ParameterNames {
    public static void main(String[] args) throws Exception {
        Method method = ParameterNames.class.getMethod( "main", String[].class );
        for( final Parameter parameter: method.getParameters() ) {
            System.out.println( "Parameter: " + parameter.getName() );
        }
    }
}

在Java 8中這個特性是預設關閉的,因此如果不帶-parameters引數編譯上述程式碼並執行,則會輸出如下結果:

Parameter: arg0

如果帶-parameters引數,則會輸出如下結果(正確的結果):

Parameter: args

如果你使用Maven進行專案管理,則可以在maven-compiler-plugin編譯器的配置項中配置-parameters引數:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.1</version>
    <configuration>
        <compilerArgument>-parameters</compilerArgument>
        <source>1.8</source>
        <target>1.8</target>
    </configuration>
</plugin>

JVM的新特性

使用Metaspace(JEP 122)代替持久代(PermGen space)。在JVM引數方面,使用-XX:MetaSpaceSize和-XX:MaxMetaspaceSize代替原來的-XX:PermSize和-XX:MaxPermSize。

參考

相關文章