Java 8新特性

l1ang_coder發表於2016-08-26

目錄結構

  1. 介紹
  2. Java語言的新特性

    2.1 Lambdas表示式與Functional介面

    2.2 介面的預設與靜態方法

    2.3 方法引用

    2.4 重複註解

    2.5 更好的型別推測機制

    2.6 擴充套件註解的支援

  3. Java編譯器的新特性

    3.1 引數名字

  4. Java 類庫的新特性

    4.1 Optional

    4.2 Streams

    4.3 Date/Time API (JSR 310)

    4.4 JavaScript引擎Nashorn

    4.5 Base64

    4.6 並行(parallel)陣列

    4.7 併發(Concurrency)

  5. 新增的Java工具

    5.1 Nashorn引擎: jjs

    5.2 類依賴分析器: jdeps

  6. Java虛擬機器(JVM)的新特性

1.介紹

毫無疑問,Java 8發行版是自Java 5(發行於2004,已經過了相當一段時間了)以來最具革命性的版本。Java 8 為Java語言、編譯器、類庫、開發工具與JVM(Java虛擬機器)帶來了大量新特性。在這篇教程中,我們將一一探索這些變化,並用真實的例子說明它們適用的場景。

這篇教程由以下幾部分組成,它們分別涉及到Java平臺某一特定方面的內容:

  • Java語言
  • 編譯器
  • 類庫
  • 工具
  • Java執行時(JVM)

2.Java語言的新特性

不管怎麼說,Java 8都是一個變化巨大的版本。你可能認為Java 8耗費了大量的時間才得以完成是為了實現了每個Java程式設計師所期待的特性。在這個小節裡,我們將會涉及到這些特性的大部分。

2.1 Lambda表示式與Functional介面

Lambda表示式(也稱為閉包)是整個Java 8發行版中最受期待的在Java語言層面上的改變,Lambda允許把函式作為一個方法的引數(函式作為引數傳遞進方法中),或者把程式碼看成資料:函式式程式設計師對這一概念非常熟悉。在JVM平臺上的很多語言(Groovy,Scala,……)從一開始就有Lambda,但是Java程式設計師不得不使用毫無新意的匿名類來代替lambda。

關於Lambda設計的討論佔用了大量的時間與社群的努力。可喜的是,最終找到了一個平衡點,使得可以使用一種即簡潔又緊湊的新方式來構造Lambdas。在最簡單的形式中,一個lambda可以由用逗號分隔的引數列表、–>符號與函式體三部分表示。例如:

Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) );

請注意引數e的型別是由編譯器推測出來的。同時,你也可以通過把引數型別與引數包括在括號中的形式直接給出引數的型別:

Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.println( e ) );

在某些情況下lambda的函式體會更加複雜,這時可以把函式體放到在一對花括號中,就像在Java中定義普通函式一樣。例如:

Arrays.asList( "a", "b", "d" ).forEach( e -> {
    System.out.print( e );
    System.out.print( e );
} );

Lambda可以引用類的成員變數與區域性變數(如果這些變數不是final的話,它們會被隱含的轉為final,這樣效率更高)。例如,下面兩個程式碼片段是等價的:

String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach( 
    ( String e ) -> System.out.print( e + separator ) );

和:

final String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach( 
    ( String e ) -> System.out.print( e + separator ) );

Lambda可能會返回一個值。返回值的型別也是由編譯器推測出來的。如果lambda的函式體只有一行的話,那麼沒有必要顯式使用return語句。下面兩個程式碼片段是等價的:

Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> e1.compareTo( e2 ) );

和:

Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> {
    int result = e1.compareTo( e2 );
    return result;
} );

語言設計者投入了大量精力來思考如何使現有的函式友好地支援lambda。最終採取的方法是:增加函式式介面的概念。函式式介面就是一個具有一個方法的普通介面。像這樣的介面,可以被隱式轉換為lambda表示式。java.lang.Runnable與java.util.concurrent.Callable是函式式介面最典型的兩個例子。在實際使用過程中,函式式介面是容易出錯的:如有某個人在介面定義中增加了另一個方法,這時,這個介面就不再是函式式的了,並且編譯過程也會失敗。為了克服函式式介面的這種脆弱性並且能夠明確宣告介面作為函式式介面的意圖,Java 8增加了一種特殊的註解@FunctionalInterface(Java 8中所有類庫的已有介面都新增了@FunctionalInterface註解)。讓我們看一下這種函式式介面的定義:

@FunctionalInterface
public interface Functional {
    void method();
}

需要記住的一件事是:預設方法與靜態方法並不影響函式式介面的契約,可以任意使用:

@FunctionalInterface
public interface FunctionalDefaultMethods {
    void method();
        
    default void defaultMethod() {            
    }        
}

Lambda是Java 8最大的賣點。它具有吸引越來越多程式設計師到Java平臺上的潛力,並且能夠在純Java語言環境中提供一種優雅的方式來支援函數語言程式設計。更多詳情可以參考官方文件

2.2 介面的預設方法與靜態方法

Java 8用預設方法與靜態方法這兩個新概念來擴充套件介面的宣告。預設方法使介面有點像Traits(Scala中特徵(trait)類似於Java中的Interface,但它可以包含實現程式碼,也就是目前Java8新增的功能),但與傳統的介面又有些不一樣,它允許在已有的介面中新增新方法,而同時又保持了與舊版本程式碼的相容性。

預設方法與抽象方法不同之處在於抽象方法必須要求實現,但是預設方法則沒有這個要求。相反,每個介面都必須提供一個所謂的預設實現,這樣所有的介面實現者將會預設繼承它(如果有必要的話,可以覆蓋這個預設實現)。讓我們看看下面的例子:

private interface Defaulable {
    // Interfaces now allow default methods, the implementer may or 
    // may not implement (override) them.
    default String notRequired() { 
        return "Default implementation"; 
    }        
}
        
private static class DefaultableImpl implements Defaulable {
}
    
private static class OverridableImpl implements Defaulable {
    @Override
    public String notRequired() {
        return "Overridden implementation";
    }
}

Defaulable介面用關鍵字default宣告瞭一個預設方法notRequired(),Defaulable介面的實現者之一DefaultableImpl實現了這個介面,並且讓預設方法保持原樣。Defaulable介面的另一個實現者OverridableImpl用自己的方法覆蓋了預設方法。

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

private interface DefaulableFactory {
    // Interfaces now allow static methods
    static Defaulable create( Supplier< Defaulable > supplier ) {
        return supplier.get();
    }
}

下面的一小段程式碼片段把上面的預設方法與靜態方法黏合到一起。

public static void main( String[] args ) {
    Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new );
    System.out.println( defaulable.notRequired() );
        
    defaulable = DefaulableFactory.create( OverridableImpl::new );
    System.out.println( defaulable.notRequired() );
}

這個程式的控制檯輸出如下:

Default implementation
Overridden implementation

在JVM中,預設方法的實現是非常高效的,並且通過位元組碼指令為方法呼叫提供了支援。預設方法允許繼續使用現有的Java介面,而同時能夠保障正常的編譯過程。這方面好的例子是大量的方法被新增到java.util.Collection介面中去:stream(),parallelStream(),forEach(),removeIf(),……

儘管預設方法非常強大,但是在使用預設方法時我們需要小心注意一個地方:在宣告一個預設方法前,請仔細思考是不是真的有必要使用預設方法,因為預設方法會帶給程式歧義,並且在複雜的繼承體系中容易產生編譯錯誤。更多詳情請參考官方文件

2.3 方法引用

方法引用提供了非常有用的語法,可以直接引用已有Java類或物件(例項)的方法或構造器。與lambda聯合使用,方法引用可以使語言的構造更緊湊簡潔,減少冗餘程式碼。

下面,我們以定義了4個方法的Car這個類作為例子,區分Java中支援的4種不同的方法引用。

public static class Car {
    public static Car create( final Supplier< Car > supplier ) {
        return supplier.get();
    }              
        
    public static void collide( final Car car ) {
        System.out.println( "Collided " + car.toString() );
    }
        
    public void follow( final Car another ) {
        System.out.println( "Following the " + another.toString() );
    }
        
    public void repair() {   
        System.out.println( "Repaired " + this.toString() );
    }
}

第一種方法引用是構造器引用,它的語法是Class::new,或者更一般的Class< T >::new。請注意構造器沒有引數。

final Car car = Car.create( Car::new );
final List< Car > cars = Arrays.asList( car );

第二種方法引用是靜態方法引用,它的語法是Class::static_method。請注意這個方法接受一個Car型別的引數。

cars.forEach( Car::collide );

第三種方法引用是特定類的任意物件的方法引用,它的語法是Class::method。請注意,這個方法沒有引數。

cars.forEach( Car::repair );

最後,第四種方法引用是特定物件的方法引用,它的語法是instance::method。請注意,這個方法接受一個Car型別的引數

final Car police = Car.create( Car::new );
cars.forEach( police::follow );

執行上面的Java程式在控制檯上會有下面的輸出(Car的例項可能不一樣):

Collided com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Repaired com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Following the com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d

關於方法引用的更多詳情請參考官方文件

2.4 重複註解

自從Java 5引入了註解機制,這一特性就變得非常流行並且廣為使用。然而,使用註解的一個限制是相同的註解在同一位置只能宣告一次,不能宣告多次。Java 8打破了這條規則,引入了重複註解機制,這樣相同的註解可以在同一地方宣告多次。

重複註解機制本身必須用@Repeatable註解。事實上,這並不是語言層面上的改變,更多的是編譯器的技巧,底層的原理保持不變。讓我們看一個快速入門的例子:

package com.javacodegeeks.java8.repeatable.annotations;

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

正如我們看到的,這裡有個使用@Repeatable( Filters.class )註解的註解類Filter,Filters僅僅是Filter註解的陣列,但Java編譯器並不想讓程式設計師意識到Filters的存在。這樣,介面Filterable就擁有了兩次Filter(並沒有提到Filter)註解。

同時,反射相關的API提供了新的函式getAnnotationsByType()來返回重複註解的型別(請注意Filterable.class.getAnnotation( Filters.class )經編譯器處理後將會返回Filters的例項)。

程式輸出結果如下:

filter1
filter2

更多詳情請參考官方文件

2.5 更好的型別推測機制

Java 8在型別推測方面有了很大的提高。在很多情況下,編譯器可以推測出確定的引數型別,這樣就能使程式碼更整潔。讓我們看一個例子:

package com.javacodegeeks.java8.type.inference;

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 >型別的用法。

package com.javacodegeeks.java8.type.inference;

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

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

2.6 擴充套件註解的支援

Java 8擴充套件了註解的上下文。現在幾乎可以為任何東西新增註解:區域性變數、泛型類、父類與介面的實現,就連方法的異常也能新增註解。下面演示幾個例子:

package com.javacodegeeks.java8.annotations;

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

ElementType.TYPE_USE和ElementType.TYPE_PARAMETER是兩個新新增的用於描述適當的註解上下文的元素型別。在Java語言中,註解處理API也有小的改動來識別新增的型別註解。

3. Java編譯器的新特性

3.1 引數名字

很長一段時間裡,Java程式設計師一直在發明不同的方式使得方法引數的名字能保留在Java位元組碼中,並且能夠在執行時獲取它們(比如,Paranamer類庫)。最終,在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() );
        }
    }
}

如果不使用–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>

針對Java 8最新發布的Eclipse Kepler SR2(請檢查這裡的下載說明)提供了非常實用的配置選項,可以通過下圖的配置方式來控制編譯器行為

Java 8新特性終極指南 
圖1. 配置Eclipse工程使之支援Java 8編譯器的新特性——parameters引數

此外,Parameter類有一個很方便的方法isNamePresent()來驗證是否可以獲取引數的名字。

4. Java 類庫的新特性

Java 8 通過增加大量新類,擴充套件已有類的功能的方式來改善對併發程式設計、函數語言程式設計、日期/時間相關操作以及其他更多方面的支援。

4.1 Optional

到目前為止,臭名昭著的空指標異常是導致Java應用程式失敗的最常見原因。以前,為了解決空指標異常,Google公司著名的Guava專案引入了Optional類,Guava通過使用檢查空值的方式來防止程式碼汙染,它鼓勵程式設計師寫更乾淨的程式碼。受到Google Guava的啟發,Optional類已經成為Java 8類庫的一部分。

Optional實際上是個容器:它可以儲存型別T的值,或者僅僅儲存null。Optional提供很多有用的方法,這樣我們就不用顯式進行空值檢測。更多詳情請參考官方文件

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

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類的例項為非空值的話,isPresent()返回true,否從返回false。為了防止Optional為空值,orElseGet()方法通過回撥函式來產生一個預設值。map()函式對當前Optional的值進行轉化,然後返回一個新的Optional例項。orElse()方法和orElseGet()方法類似,但是orElse接受一個預設值而不是一個回撥函式。下面是這個程式的輸出:

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

讓我們來看看另一個例子:

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!

更多詳情請參考官方文件

4.2 Stream

最新新增的Stream API(java.util.stream) 把真正的函數語言程式設計風格引入到Java中。這是目前為止對Java類庫最好的補充,因為Stream API可以極大提供Java程式設計師的生產力,讓程式設計師寫出高效率、乾淨、簡潔的程式碼。

Stream API極大簡化了集合框架的處理(但它的處理的範圍不僅僅限於集合框架的處理,這點後面我們會看到)。讓我們以一個簡單的Task類為例進行介紹:

public class Streams  {
    private enum Status {
        OPEN, CLOSED
    };
    
    private static final class Task {
        private final Status status;
        private final Integer points;

        Task( final Status status, final Integer points ) {
            this.status = status;
            this.points = points;
        }
        
        public Integer getPoints() {
            return points;
        }
        
        public Status getStatus() {
            return status;
        }
        
        @Override
        public String toString() {
            return String.format( "[%s, %d]", status, points );
        }
    }
}

Task類有一個分數的概念(或者說是偽複雜度),其次是還有一個值可以為OPEN或CLOSED的狀態.讓我們引入一個Task的小集合作為演示例子:

final Collection< Task > tasks = Arrays.asList(
    new Task( Status.OPEN, 5 ),
    new Task( Status.OPEN, 13 ),
    new Task( Status.CLOSED, 8 ) 
);

我們下面要討論的第一個問題是所有狀態為OPEN的任務一共有多少分數?在Java 8以前,一般的解決方式用foreach迴圈,但是在Java 8裡面我們可以使用stream:一串支援連續、並行聚集操作的元素。

// Calculate total points of all active tasks using sum()
final long totalPointsOfOpenTasks = tasks
    .stream()
    .filter( task -> task.getStatus() == Status.OPEN )
    .mapToInt( Task::getPoints )
    .sum();
        
System.out.println( "Total points: " + totalPointsOfOpenTasks );

程式在控制檯上的輸出如下:

Total points: 18

這裡有幾個注意事項。第一,task集合被轉換化為其相應的stream表示。然後,filter操作過濾掉狀態為CLOSED的task。下一步,mapToInt操作通過Task::getPoints這種方式呼叫每個task例項的getPoints方法把Task的stream轉化為Integer的stream。最後,用sum函式把所有的分數加起來,得到最終的結果。

在繼續講解下面的例子之前,關於stream有一些需要注意的地方(詳情在這裡).stream操作被分成了中間操作與最終操作這兩種。

中間操作返回一個新的stream物件。中間操作總是採用惰性求值方式,執行一個像filter這樣的中間操作實際上沒有進行任何過濾,相反它在遍歷元素時會產生了一個新的stream物件,這個新的stream物件包含原始stream
中符合給定謂詞的所有元素。

像forEach、sum這樣的最終操作可能直接遍歷stream,產生一個結果或副作用。當最終操作執行結束之後,stream管道被認為已經被消耗了,沒有可能再被使用了。在大多數情況下,最終操作都是採用及早求值方式,及早完成底層資料來源的遍歷。

stream另一個有價值的地方是能夠原生支援並行處理。讓我們來看看這個算task分數和的例子。

// Calculate total points of all tasks
final double totalPoints = tasks
   .stream()
   .parallel()
   .map( task -> task.getPoints() ) // or map( Task::getPoints ) 
   .reduce( 0, Integer::sum );
    
System.out.println( "Total points (all tasks): " + totalPoints );

這個例子和第一個例子很相似,但這個例子的不同之處在於這個程式是並行執行的,其次使用reduce方法來算最終的結果。
下面是這個例子在控制檯的輸出:

Total points (all tasks): 26.0

經常會有這個一個需求:我們需要按照某種準則來對集合中的元素進行分組。Stream也可以處理這樣的需求,下面是一個例子:

// Group tasks by their status
final Map< Status, List< Task > > map = tasks
    .stream()
    .collect( Collectors.groupingBy( Task::getStatus ) );
System.out.println( map );

這個例子的控制檯輸出如下:

{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}

讓我們來計算整個集合中每個task分數(或權重)的平均值來結束task的例子。

// Calculate the weight of each tasks (as percent of total points) 
final Collection< String > result = tasks
    .stream()                                        // Stream< String >
    .mapToInt( Task::getPoints )                     // IntStream
    .asLongStream()                                  // LongStream
    .mapToDouble( points -> points / totalPoints )   // DoubleStream
    .boxed()                                         // Stream< Double >
    .mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream
    .mapToObj( percentage -> percentage + "%" )      // Stream< String> 
    .collect( Collectors.toList() );                 // List< String > 
        
System.out.println( result );

下面是這個例子的控制檯輸出:

[19%, 50%, 30%]

最後,就像前面提到的,Stream API不僅僅處理Java集合框架。像從文字檔案中逐行讀取資料這樣典型的I/O操作也很適合用Stream API來處理。下面用一個例子來應證這一點。

final Path path = new File( filename ).toPath();
try( Stream< String > lines = Files.lines( path, StandardCharsets.UTF_8 ) ) {
    lines.onClose( () -> System.out.println("Done!") ).forEach( System.out::println );
}

對一個stream物件呼叫onClose方法會返回一個在原有功能基礎上新增了關閉功能的stream物件,當對stream物件呼叫close()方法時,與關閉相關的處理器就會執行。

Stream API、Lambda表示式方法引用介面預設方法與靜態方法的配合下是Java 8對現代軟體開發正規化的迴應。更多詳情請參考官方文件

4.3 Date/Time API (JSR 310)

Java 8通過釋出新的Date-Time API (JSR 310)來進一步加強對日期與時間的處理。對日期與時間的操作一直是Java程式設計師最痛苦的地方之一。標準的 java.util.Date以及後來的java.util.Calendar一點沒有改善這種情況(可以這麼說,它們一定程度上更加複雜)。

這種情況直接導致了Joda-Time——一個可替換標準日期/時間處理且功能非常強大的Java API的誕生。Java 8新的Date-Time API (JSR 310)在很大程度上受到Joda-Time的影響,並且吸取了其精髓。新的java.time包涵蓋了所有處理日期,時間,日期/時間,時區,時刻(instants),過程(during)與時鐘(clock)的操作。在設計新版API時,十分注重與舊版API的相容性:不允許有任何的改變(從java.util.Calendar中得到的深刻教訓)。如果需要修改,會返回這個類的一個新例項。

讓我們用例子來看一下新版API主要類的使用方法。第一個是Clock類,它通過指定一個時區,然後就可以獲取到當前的時刻,日期與時間。Clock可以替換System.currentTimeMillis()與TimeZone.getDefault()。

// Get the system clock as UTC offset 
final Clock clock = Clock.systemUTC();
System.out.println( clock.instant() );
System.out.println( clock.millis() );

下面是程式在控制檯上的輸出:

2014-04-12T15:19:29.282Z
1397315969360

我們需要關注的其他類是LocaleDate與LocalTime。LocaleDate只持有ISO-8601格式且無時區資訊的日期部分。相應的,LocaleTime只持有ISO-8601格式且無時區資訊的時間部分。LocaleDate與LocalTime都可以從Clock中得到。

// Get the local date and local time
final LocalDate date = LocalDate.now();
final LocalDate dateFromClock = LocalDate.now( clock );
        
System.out.println( date );
System.out.println( dateFromClock );
        
// Get the local date and local time
final LocalTime time = LocalTime.now();
final LocalTime timeFromClock = LocalTime.now( clock );
        
System.out.println( time );
System.out.println( timeFromClock );

下面是程式在控制檯上的輸出:

2014-04-12
2014-04-12
11:25:54.568
15:25:54.568

LocaleDateTime把LocaleDate與LocaleTime的功能合併起來,它持有的是ISO-8601格式無時區資訊的日期與時間。下面是一個快速入門的例子。

// Get the local date/time
final LocalDateTime datetime = LocalDateTime.now();
final LocalDateTime datetimeFromClock = LocalDateTime.now( clock );
        
System.out.println( datetime );
System.out.println( datetimeFromClock );

下面是程式在控制檯上的輸出:

2014-04-12T11:37:52.309
2014-04-12T15:37:52.309

如果你需要特定時區的日期/時間,那麼ZonedDateTime是你的選擇。它持有ISO-8601格式具具有時區資訊的日期與時間。下面是一些不同時區的例子:

// Get the zoned date/time
final ZonedDateTime zonedDatetime = ZonedDateTime.now();
final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now( clock );
final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of( "America/Los_Angeles" ) );
        
System.out.println( zonedDatetime );
System.out.println( zonedDatetimeFromClock );
System.out.println( zonedDatetimeFromZone );

下面是程式在控制檯上的輸出:

2014-04-12T11:47:01.017-04:00[America/New_York]
2014-04-12T15:47:01.017Z
2014-04-12T08:47:01.017-07:00[America/Los_Angeles]

最後,讓我們看一下Duration類:在秒與納秒級別上的一段時間。Duration使計算兩個日期間的不同變的十分簡單。下面讓我們看一個這方面的例子。

// Get duration between two dates
final LocalDateTime from = LocalDateTime.of( 2014, Month.APRIL, 16, 0, 0, 0 );
final LocalDateTime to = LocalDateTime.of( 2015, Month.APRIL, 16, 23, 59, 59 );

final Duration duration = Duration.between( from, to );
System.out.println( "Duration in days: " + duration.toDays() );
System.out.println( "Duration in hours: " + duration.toHours() );

上面的例子計算了兩個日期2014年4月16號與2014年4月16號之間的過程。下面是程式在控制檯上的輸出:

Duration in days: 365
Duration in hours: 8783

對Java 8在日期/時間API的改進整體印象是非常非常好的。一部分原因是因為它建立在“久戰殺場”的Joda-Time基礎上,另一方面是因為用來大量的時間來設計它,並且這次程式設計師的聲音得到了認可。更多詳情請參考官方文件

4.4 JavaScript引擎Nashorn

Nashorn,一個新的JavaScript引擎隨著Java 8一起公諸於世,它允許在JVM上開發執行某些JavaScript應用。Nashorn就是javax.script.ScriptEngine的另一種實現,並且它們倆遵循相同的規則,允許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

我們在後面的Java新工具章節會再次談到Nashorn。

4.5 Base64

在Java 8中,Base64編碼已經成為Java類庫的標準。它的使用十分簡單,下面讓我們看一個例子:

package com.javacodegeeks.java8.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 );
    }
}

程式在控制檯上輸出了編碼後的字元與解碼後的字元:

QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
Base64 finally in Java 8!

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

4.6 並行(parallel)陣列

Java 8增加了大量的新方法來對陣列進行並行處理。可以說,最重要的是parallelSort()方法,因為它可以在多核機器上極大提高陣列排序的速度。下面的例子展示了新方法(parallelXxx)的使用。

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個元素的值,之後對整個陣列排序。這個程式在控制檯上的輸出如下(請注意陣列元素是隨機生產的):

Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378 
Sorted: 39 220 263 268 325 607 655 678 723 793

4.7 併發(Concurrency)

在新增Stream機制與lambda的基礎之上,在java.util.concurrent.ConcurrentHashMap中加入了一些新方法來支援聚集操作。同時也在java.util.concurrent.ForkJoinPool類中加入了一些新方法來支援共有資源池(common pool)(請檢視我們關於Java 併發的免費課程)。

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

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

  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator
  • LongAdder

5. 新的Java工具

Java 8也帶來了一些新的命令列工具。在這節裡我們將會介紹它們中最有趣的部分。

5.1 Nashorn引擎: jjs

jjs是個基於Nashorn引擎的命令列工具。它接受一些JavaScript原始碼為引數,並且執行這些原始碼。例如,我們建立一個具有如下內容的func.js檔案:

function f() { 
     return 1; 
}; 

print( f() + 1 );

我們可以把這個檔案作為引數傳遞給jjs使得這個檔案可以在命令列中執行:

jjs func.js

下面是程式在控制檯上的輸出:

2

更多詳情請參考官方文件

5.2 類依賴分析器jdeps

jdeps是一個很有用的命令列工具。它可以顯示Java類的包級別或類級別的依賴。它接受一個.class檔案,一個目錄,或者一個jar檔案作為輸入。jdeps預設把結果輸出到系統輸出(控制檯)上。

下面我們檢視現階段較流行的Spring框架類庫的依賴報告,為了簡化這個例子,我們只分析一個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

更多詳情請參考官方文件

6. Java虛擬機器(JVM)的新特性

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


譯文連結: http://www.importnew.com/11908.html

相關文章