1. 程式人生 > >Java 8 特性 – 終極手冊

Java 8 特性 – 終極手冊

原文連結原文作者:,譯者:Justin,校對:郭蕾

1.簡介

毫無疑問,Java 8是自Java  5(2004年)釋出以來Java語言最大的一次版本升級,Java 8帶來了很多的新特性,比如編譯器、類庫、開發工具和JVM(Java虛擬機器)。在這篇教程中我們將會學習這些新特性,並通過真實例子演示說明它們適用的場景

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

  • 語言
  • 編譯器
  • 類庫
  • 開發工具
  • 執行時(Java虛擬機器)

2.Java的新特性

總體來說,Java 8是一個大的版本升級。有人可能會說,Java 8的新特性非常令人期待,但是也要花費大量的時間去學習。這一節我們會講到這些新特性。

2.1 Lambda表示式和函式式介面

Lambda表示式(也叫做閉包)是Java 8中最大的也是期待已久的變化。它允許我們將一個函式當作方法的引數(傳遞函式),或者說把程式碼當作資料,這是每個函數語言程式設計者熟悉的概念。很多基於JVM平臺的語言一開始就支援Lambda表示式,但是Java程式設計師沒有選擇,只能使用匿名內部類來替代Lambda表示式。

Lambda表示式的設計被討論了很久,而且花費了很多的功夫來交流。不過最後取得了一個折中的辦法,得到了一個新的簡明並且緊湊的Lambda表示式結構。最簡單的Lambda表示式可以用逗號分隔的引數列表、->符號和功能語句塊來表示。示例如下:

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

請注意到編譯器會根據上下文來推測引數的型別,或者你也可以顯示地指定引數型別,只需要將型別包在括號裡。舉個例子:

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

如果Lambda的功能語句塊太複雜,我們可以用大括號包起來,跟普通的Java方法一樣,如下:

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

Lambda表示式可能會引用類的成員或者區域性變數(會被隱式地轉變成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裡所有現存的介面都已經加上了@FunctionalInterface)。讓我們看看一個簡單的函式介面定義:

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

我們要記住預設的方法和靜態方法(下一節會具體解釋)不會違反函式介面的約定,例子如下:

@FunctionalInterface
public interface FunctionalDefaultMethods {
    void method();

    default void defaultMethod() {
    }
}

支援Lambda是Java 8最大的賣點,他有巨大的潛力吸引越來越多的開發人員轉到這個開發平臺來,並且在純Java裡提供最新的函數語言程式設計的概念。對於更多的細節,請參考官方文件

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

Java 8增加了兩個新的概念在介面宣告的時候:預設和靜態方法。預設方法和Trait有些類似,但是目標不一樣。預設方法允許我們在接口裡新增新的方法,而不會破壞實現這個介面的已有類的相容性,也就是說不會強迫實現介面的類實現預設方法。

預設方法和抽象方法的區別是抽象方法必須要被實現,預設方法不是。作為替代方式,介面可以提供一個預設的方法實現,所有這個介面的實現類都會通過繼承得倒這個方法(如果有需要也可以重寫這個方法),讓我們來看看下面的例子:

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(),類DefaultableImpl實現了Defaulable介面,沒有對預設方法做任何修改。另外一個類OverridableImpl重寫類預設實現,提供了自己的實現方法。

Java 8 的另外一個有意思的新特性是接口裡可以宣告靜態方法,並且可以實現。例子如下:

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

下面是把介面的靜態方法和預設方法放在一起的示例(::new 是構造方法引用,後面會有詳細描述):

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.util.Collection中新增的額外方法就是最好的例子:stream()parallelStream()forEach()removeIf()

雖然預設方法很強大,但是使用之前一定要仔細考慮是不是真的需要使用預設方法,因為在層級很複雜的情況下很容易引起模糊不清甚至變異錯誤。更多的詳細資訊請參考官方文件

2.3   方法引用

方法引用提供了一個很有用的語義來直接訪問類或者例項的已經存在的方法或者構造方法。結合Lambda表示式,方法引用使語法結構緊湊簡明。不需要複雜的引用。

下面我們用Car 這個類來做示例,Car這個類有不同的方法定義。讓我們來看看java 8支援的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 );

執行這些例子我們將會在控制檯得到如下資訊(Car的例項可能會不一樣):

Collided [email protected]a81197d
Repaired [email protected]a81197d
Following the [email protected]a81197d

關於方法引用更多的示例和詳細資訊,請參考官方文件

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

我們可以看到,註釋Filter被@Repeatable( Filters.class )註釋。Filters 只是一個容器,它持有Filter, 編譯器盡力向程式設計師隱藏它的存在。通過這樣的方式,Filterable介面可以被Filter註釋兩次。

另外,反射的API提供一個新方法getAnnotationsByType() 來返回重複註釋的型別(請注意Filterable.class.getAnnotation( Filters.class )將會返回編譯器注入的Filters例項)。

程式的輸出將會是這樣:

filter1
filter2

更多詳細資訊請參考官方文件

2.5   更好的型別推斷

Java 8在型別推斷方面改進了很多,在很多情況下,編譯器可以推斷引數的型別,從而保持程式碼的整潔。讓我們看看例子:

package com.javacodegeeks.java8.type.inference;

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擴充套件了註解可以使用的範圍,現在我們幾乎可以在所有的地方:區域性變數、泛型、超類和介面實現、甚至是方法的Exception宣告。一些例子如下:

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

Java 8 新增加了兩個註解的程式元素型別ElementType.TYPE_USE ElementType.TYPE_PARAMETER ,這兩個新型別描述了可以使用註解的新場合。註解處理API(Annotation Processing API)也做了一些細微的改動,來識別這些新新增的註解型別。

3.Java編譯器的新特性

3.1 引數名字

很長時間以來,Java程式設計師想盡辦法把引數名字儲存在java位元組碼裡,並且讓這些引數名字在執行時可用。Java 8 終於把這個需求加入到了Java語言(使用反射API和Parameter.getName() 方法)和位元組碼裡(使用java編譯命令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() );
}
}
}

如果你編譯這個class的時候沒有新增引數–parameters執行的時候你會得到這個結果:

Parameter: arg0

編譯的時候添加了–parameters引數的話,執行結果會不一樣:

Parameter: args

對於有經驗的Maven使用者,–parameters引數可以新增到maven-compiler-plugin的配置部分:

<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>

最新版的Eclipse Kepler SR2 提供了編譯設定項,如下圖所示:

01.ECLIPSE-JAVA-COMPILER

Picture 1. Configuring Eclipse projects to support new Java 8 compiler –parameters argument.

額外的,有一個方便的方法Parameter.isNamePresent() 來驗證引數名是不是可用。

4.Java  庫的新特性

Java 8 新添加了很多類,並且擴充套件了很多現有的類來更好地支援現代併發、函數語言程式設計、日期\時間等等。

4.1 Optional

著名的NullPointerException 是引起系統失敗最常見的原因。很久以前Google Guava專案引入了Optional作為解決空指標異常的一種方式,不贊成程式碼被null檢查的程式碼汙染,期望程式設計師寫整潔的程式碼。受Google Guava的鼓勵,Optional 現在是Java 8庫的一部分。

Optional 只是一個容器,它可以儲存一些型別的值或者null。它提供很多有用的方法,所以沒有理由不顯式地檢查null。請參照java 8的文件檢視詳細資訊。

讓我們看看兩個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。方法orElseGet提供了回退機制,當Optional的值為空時接受一個方法返回預設值。map()方法轉化Optional當前的值並且返回一個新的Optional例項。orElse方法和orElseGet類似,但是它不接受一個方法,而是接受一個預設值。上面程式碼執行結果如下:

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集合類)。讓我們從一個簡單的類Task開始來看看Stream的用法。

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

第一個問題是所有的開放的Task的點數是多少?在java 8 之前,通常的做法是用foreach迭代。但是Java8裡頭我們會用Stream。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操作會過濾掉所有關閉的Task,接下來使用Task::getPoints 方法取得每個Task例項的點數,mapToInt方法會把Task Stream轉換成Integer Stream,最後使用Sum方法將所有的點數加起來得到最終的結果。

在我們看下一個例子之前,我們要記住一些關於Stream的說明。Stream操作被分為中間操作和終點操作。

中間操作返回一個新的Stream。這些中間操作是延遲的,執行一箇中間操作比如filter實際上不會真的做過濾操作,而是建立一個新的Stream,當這個新的Stream被遍歷的時候,它裡頭會包含有原來Stream裡符合過濾條件的元素。

終點操作比如說forEach或者sum會遍歷Stream從而產生最終結果或附帶結果。終點操作執行完之後,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 );

這個例子跟上面那個非常像,除了這個例子裡使用了parallel()方法       並且計算最終結果的時候使用了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日期時間API(JSR310

Java 8引入了新的日期時間API(JSR 310)改進了日期時間的管理。日期和時間管理一直是Java開發人員最痛苦的問題。java.util.Date和後來的java.util.Calendar一點也沒有改變這個情況(甚至讓人們更加迷茫)。

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

讓我們看看一些關鍵的類和用法示例。第一個類是Clock,Clock使用時區來訪問當前的instant, date和time。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

其他類我們看看LocalTime和LocalDate。LocalDate只儲存有ISO-8601日期系統的日期部分,有時區資訊,相應地,LocalTime只儲存ISO-8601日期系統的時間部分,沒有時區資訊。LocalDate和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

LocalDateTime類合併了LocalDate和LocalTime,它儲存有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年5月16日)之間的持續時間(基於天數和小時)輸出如下:

Duration in days: 365
Duration in hours: 8783

對於Java 8的新日期時間的總體印象還是比較積極的。一部分是因為有經歷實戰的Joda-Time的基礎,還有一部分是因為日期時間終於被認真對待而且聽取了開發人員的聲音。關於更多的詳細資訊,請參考官方文件

4.4   Nashorn javascript引擎

Java 8提供了一個新的Nashorn javascript引擎,它允許我們在JVM上執行特定的javascript應用。Nashorn javascript引擎只是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
4.5   Base64

對Base64的支援最終成了Java 8標準庫的一部分,非常簡單易用:

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!

新的Base64API也支援URL和MINE的編碼解碼。

(Base64.getUrlEncoder() / Base64.getUrlDecoder()Base64.getMimeEncoder() / Base64.getMimeDecoder()).

4.6   並行陣列

Java 8新增加了很多方法支援並行的陣列處理。最重要的大概是parallelSort()這個方法顯著地使排序在多核計算機上速度加快。下面的小例子演示了這個新的方法(parallelXXX)的行為。

</pre>
<pre class="brush:java">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();
    }
}</pre>
<pre>

這一小段程式碼使用parallelSetAll() t方法填充這個長度是2000的陣列,然後使用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   併發

在新增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 8 提供了一些新的命令列工具,在這節裡我們將會介紹它們中最有趣的部分。

5.1  Nashorn引擎:jjs

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

</span>
<pre>
function f() {
return 1;
};

print( f() + 1 );

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

<span style="font-size: 13px;">jjs func.js</span>

輸出結果如下

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.

</pre>
<pre class="brush:java">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</pre>
<pre>

更多的詳細資訊請參考官方文件

6. JVM的新特性

JVM記憶體永久區已經被metaspace替換(JEP 122)。JVM引數 -XX:PermSize 和 –XX:MaxPermSizeXX:MetaSpaceSize 和 -XX:MaxMetaspaceSize代替

7. 結論

更多展望:Java 8通過釋出一些可以增加程式設計師生產力的特性來推進這個偉大的平臺的進步。現在把生產環境遷移到Java 8還為時尚早,但是在接下來的幾個月裡,它會被大眾慢慢的接受。毫無疑問,現在是時候讓你的程式碼與Java 8相容,並且在Java 8足夠安全穩定的時候遷移到Java 8。

如果你喜歡這篇文章,請訂閱我們的郵件列表來檢視每週的更新以及免費贈送的白皮書。對於更高階的教程,請檢視我們的[JCG學院][JCG]。

我們歡迎你對Java 8中激動人心的特性進行評論!

8. 資源

下面一些文章從不同層面上深度討論了Java 8的特性