【整理】Java 8新特性總結

閒語:

  相比於今年三月份才發佈的Java 10 ,發佈已久的Java 8 已經算是老版本了(傳聞Java 11將於9月25日發佈....)。然而不少報道代表:Java 9 和JJava10不是 LTS 版本,和過去的 Java 大版本升級不一樣,它們只有半年左右的開發和維護期。而將來的 Java11,也就是 18.9 LTS,纔是 Java 8 以後第一個 LTS 版本(獲得 Oracle 等商業公司的長期支持服務)。因此Java 8 就成了最新的一次LTS版本升級,這也是爲何Java開發者對Java 8的關注和喜好程度明顯高於Java 10的緣由。在Java Code Geeks上已經有不少介紹Java 8新特性的文章,例如 Playing with Java 8 – Lambdas and ConcurrencyJava 8 Date Time API Tutorial : LocalDateTimeAbstract Class Versus Interface in the JDK 8 Era,本文整理講述的不少內容也是引用於此。
 

介紹

  毫無疑問,Java 8發行版是自Java 5(發行於2004,已通過了至關一段時間了)以來最具革命性的版本。Java 8 爲Java語言、編譯器、類庫、開發工具與JVM(Java虛擬機)帶來了大量新特性。在這篇文章中,咱們將一一探索這些變化,並用真實的例子說明它們適用的場景。主要由如下幾部分組成,它們分別涉及到Java平臺某一特定方面的內容:javascript

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

 

Java語言的新特性

Lambda表達式與Functional接口

Lambda表達式(也稱爲閉包)是整個Java 8發行版中最受期待的在Java語言層面上的改變,Lambda容許把函數做爲一個方法的參數(函數做爲參數傳遞進方法中),或者把代碼當作數據:函數式程序員對這一律念很是熟悉。在JVM平臺上的不少語言(Groovy,Scala,……)從一開始就有Lambda,可是Java程序員不得不使用毫無新意的匿名類來代替lambda。html

關於Lambda設計的討論佔用了大量的時間與社區的努力。可喜的是,最終找到了一個平衡點,使得可使用一種即簡潔又緊湊的新方式來構造Lambdas。java

在最簡單的形式中,一個lambda能夠由用逗號分隔的參數列表、–>符號與函數體三部分表示。例如:git

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中定義普通函數同樣。例如:github

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

Lambda能夠引用類的成員變量與局部變量(若是這些變量不是final的話,它們會被隱含的轉爲final,這樣效率更高)。例如,下面兩個代碼片斷是等價的:spring

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

和:shell

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

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

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

和:apache

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語言環境中提供一種優雅的方式來支持函數式編程。更多詳情能夠參考官方文檔

接口的默認方法和靜態方法

Java 8使用兩個新概念擴展了接口的含義:默認方法和靜態方法。默認方法使得接口有點相似traits,不過要實現的目標不同。默認方法使得開發者能夠在 不破壞二進制兼容性的前提下,往現存接口中添加新的方法,即不強制那些實現了該接口的類也同時實現這個新加的方法。

默認方法和抽象方法之間的區別在於抽象方法須要實現,而默認方法不須要。接口提供的默認方法會被接口的實現類繼承或者覆寫,例子代碼以下:

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類實現了這個接口,同時默認繼承了這個接口中的默認方法;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.util.Collection接口添加新方法,如stream()parallelStream()forEach()removeIf()等等。

儘管默認方法有這麼多好處,但在實際開發中應該謹慎使用:在複雜的繼承體系中,默認方法可能引發歧義和編譯錯誤。若是你想了解更多細節,能夠參考官方文檔

方法引用

方法引用使得開發者能夠直接引用現存的方法、Java類的構造方法或者實例對象。方法引用和Lambda表達式配合使用,使得java類的構造方法看起來緊湊而簡潔,沒有不少複雜的模板代碼。

看下面Car類是不一樣方法引用的例子,能夠幫助讀者區分四種類型的方法引用。

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

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

重複註解

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

 輸出結果:

  filter1

  filter2
 

正如咱們看到的,這裏有個使用@Repeatable( Filters.class )註解的註解類Filter,Filters僅僅是Filter註解的數組,但Java編譯器並不想讓程序員意識到Filters的存在。這樣,接口Filterable就擁有了兩次Filter(並無提到Filter)註解。

同時,反射相關的API提供了新的函數getAnnotationsByType()來返回重複註解的類型(請注意Filterable.class.getAnnotation( Filters.class )經編譯器處理後將會返回Filters的實例)。更多詳情請參考官方文檔

更好的類型推測機制

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()。

擴展註解的支持

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也有小的改動來識別新增的類型註解。

 

Java編譯器的新特性

參數名稱

爲了在運行時得到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>

 

Java 類庫的新特性

Java 8 經過增長大量新類,擴展已有類的功能的方式來改善對併發編程、函數式編程、日期/時間相關操做以及其餘更多方面的支持。

Optional類

到目前爲止,臭名昭著的空指針異常是致使Java應用程序失敗的最多見緣由。之前,爲了解決空指針異常,Google公司著名的Guava項目引入了Optional類,Guava經過使用檢查空值的方式來防止代碼污染,它鼓勵程序員寫更乾淨的代碼。受到Google Guava的啓發,Optional類已經成爲Java 8類庫的一部分。

  • Optional 類的引入很好的解決空指針異常。
  • Optional 類是一個能夠爲null的容器對象。若是值存在則isPresent()方法會返回true,調用get()方法會返回該對象。
  • 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!" ) );

-----
輸出結果:
  Full Name is set? false
  Full Name: [none]
  Hey Stranger!

若是Optional類的實例爲非空值的話,isPresent()返回true,否從返回false。爲了防止Optional爲空值,orElseGet()方法經過回調函數來產生一個默認值。map()函數對當前Optional的值進行轉化,而後返回一個新的Optional實例。orElse()方法和orElseGet()方法相似,可是orElse接受一個默認值而不是一個回調函數。

讓咱們來看看另外一個例子: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!

更多詳情請參考官方文檔

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

---------
輸出結果:
  Total points (all tasks): 26.0

這個例子和第一個例子很類似,但這個例子的不一樣之處在於這個程序是並行運行的,其次使用reduce方法來算最終的結果。

常常會有這個一個需求:咱們須要按照某種準則來對集合中的元素進行分組。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對現代軟件開發範式的迴應。更多詳情請參考官方文檔

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

-------
輸出結果:
  2018-09-25T10:06:53.963Z
  1537870014196

 

咱們須要關注的其餘類是LocaleDate與LocalTime。LocaleDate只持有ISO-8601格式且無時區信息的日期部分。相應的,LocaleTime只持有ISO-8601格式且無時區信息的時間部分。LocaleDate與LocalTime均可以從Clock中獲得。

// Get the local date and local time
final Clock clock = Clock.systemUTC();

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

---------
輸出結果:
   2018-09-25
    2018-09-25
    18:08:32.543
    10:08:32.543

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

--------
輸出結果:
    2018-09-25T18:12:09.071
    2018-09-25T10:12:09.071
 

若是你須要特定時區的日期/時間,那麼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 );

--------
輸出結果:
  
    2018-09-25T18:14:15.541+08:00[Asia/Shanghai]
    2018-09-25T10:14:15.541Z
    2018-09-25T03:14:15.545-07:00[America/Los_Angeles]

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

final LocalDateTime from = LocalDateTime.of( 2017, Month.September, 25, 0, 0, 0 );
final LocalDateTime to = LocalDateTime.of( 2018, Month.September, 25, 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() );

-----------
輸出結果:
    Duration in days: 365
    Duration in hours: 8783 

上面的例子計算了兩個日期2017年9月25號與2018年9月25號之間的過程。

 

對Java 8在日期/時間API的改進總體印象是很是很是好的。一部分緣由是由於它創建在「久戰殺場」的Joda-Time基礎上,另外一方面是由於用來大量的時間來設計它,而且此次程序員的聲音獲得了承認。更多詳情請參考官方文檔

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

Base64

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

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())。

並行(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 [ 5000 ];       
         
        Arrays.parallelSetAll( arrayOfLong,
            index -> ThreadLocalRandom.current().nextInt( 500000 ) );
        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();
    }
}

-----------------
輸出結果:
    43246 429105 479150 498970 478123 404198 273038 114468 488657 438767 
   111 347 552 581 731 852 1118 1252 1545 1735 

上面的代碼片斷使用了parallelSetAll()方法來對一個有5000個元素的數組進行隨機賦值。而後,調用parallelSort方法。這個程序首先打印出前10個元素的值,以後對整個數組排序。這個程序在控制檯上的輸出以下(請注意數組元素是隨機生產的):

併發(Concurrency)

在新增Stream機制與lambda的基礎之上,在java.util.concurrent.ConcurrentHashMap中加入了一些新方法來支持彙集操做。同時也在java.util.concurrent.ForkJoinPool類中加入了一些新方法來支持共有資源池(common pool)。

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

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

  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator
  • LongAdder

更多詳情請參考官方文檔

 

新的Java工具

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

Nashorn引擎: jjs

jjs是個基於Nashorn引擎的命令行工具。它接受一些JavaScript源代碼爲參數,而且執行這些源代碼。例如,咱們建立一個具備以下內容的func.js文件:

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

------
命令行中執行:jjs func.js
------
輸出結果:1

jjs補充

Java 中調用 JavaScript

使用 ScriptEngineManager, JavaScript 代碼能夠在 Java 中執行,實例以下:

import javax.script.ScriptEngineManager;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
 
public class Java8Tester {
   public static void main(String args[]){
   
      ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
      ScriptEngine nashorn = scriptEngineManager.getEngineByName("nashorn");
        
      String name = "JackpotHan";
      Integer result = null;
      
      try {
         nashorn.eval("print('" + name + "')");
         result = (Integer) nashorn.eval("10 + 2");
         
      }catch(ScriptException e){
         System.out.println("執行腳本錯誤: "+ e.getMessage());
      }
      
      System.out.println(result.toString());
   }
}

-----------
輸出結果:
    $ javac Java8Tester.java 
    $ java Java8Tester
    JackpotHan
    12


JavaScript 中調用 Java

如下實例演示瞭如何在 JavaScript 中引用 Java 類,建立test.js文件:

var BigDecimal = Java.type('java.math.BigDecimal');

function calculate(amount, percentage) {

   var result = new BigDecimal(amount).multiply(
   new BigDecimal(percentage)).divide(new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_EVEN);
   
   return result.toPlainString();
}

var result = calculate(1537933632887,13.14);
print(result);

---------
控制檯執行:
$ jjs test.js
輸出結果:202084479361.35

更多詳情請參考官方文檔

類依賴分析器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

更多詳情請參考官方文檔

 

Java虛擬機(JVM)的新特性

元空間(MetaSpace)

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

JDK8 HotSpot JVM 將移除永久區,使用本地內存來存儲類元數據信息並稱之爲:元空間(Metaspace),這與Oracle JRockit 和IBM JVM’s很類似,以下圖所示

這意味着不會再有java.lang.OutOfMemoryError: PermGen問題,也再也不須要你進行調優及監控內存空間的使用……但請等等,這麼說還爲時過早。在默認狀況下,這些改變是透明的,接下來咱們的展現將使你知道仍然要關注類元數據內存的佔用。請必定要牢記,這個新特性也不能神奇地消除類和類加載器致使的內存泄漏。

java8中metaspace總結以下:

  PermGen 空間的情況

    這部份內存空間將所有移除。

    JVM的參數:PermSize 和 MaxPermSize 會被忽略並給出警告(若是在啓用時設置了這兩個參數)。

  Metaspace 內存分配模型

    大部分類元數據都在本地內存中分配。

    用於描述類元數據的「klasses」已經被移除。

  Metaspace 容量

    默認狀況下,類元數據只受可用的本地內存限制(容量取決因而32位或是64位操做系統的可用虛擬內存大小)。

    新參數(MaxMetaspaceSize)用於限制本地內存分配給類元數據的大小。若是沒有指定這個參數,元空間會在運行時根據須要動態調整。

  Metaspace 垃圾回收

    對於僵死的類及類加載器的垃圾回收將在元數據使用達到「MaxMetaspaceSize」參數的設定值時進行。

    適時地監控和調整元空間對於減少垃圾回收頻率和減小延時是頗有必要的。持續的元空間垃圾回收說明,可能存在類、類加載器致使的內存泄漏或是大小設置不合適。

  Java 堆內存的影響

    一些雜項數據已經移到Java堆空間中。升級到JDK8以後,會發現Java堆 空間有所增加。

  Metaspace 監控

    元空間的使用狀況能夠從HotSpot1.8的詳細GC日誌輸出中獲得。

    Jstat 和 JVisualVM兩個工具,在使用b75版本進行測試時,已經更新了,可是仍是能看到老的PermGen空間的出現。

前面已經從理論上充分說明,下面讓咱們經過「泄漏」程序進行新內存空間的觀察……

PermGen vs. Metaspace 運行時比較

爲了更好地理解Metaspace內存空間的運行時行爲,

將進行如下幾種場景的測試:

  1. 使用JDK1.7運行Java程序,監控並耗盡默認設定的85MB大小的PermGen內存空間。
  2. 使用JDK1.8運行Java程序,監控新Metaspace內存空間的動態增加和垃圾回收過程。
  3. 使用JDK1.8運行Java程序,模擬耗盡經過「MaxMetaspaceSize」參數設定的128MB大小的Metaspace內存空間。

首先創建了一個模擬PermGen OOM的代碼

public class ClassA {
     public void method(String name) {
      // do nothing
     }
}

上面是一個簡單的ClassA,把他編譯成class字節碼放到D:/classes下面,測試代碼中用URLClassLoader來加載此類型上面類編譯成class

/**
 * 模擬PermGen OOM
 * @author benhail
 */
public class OOMTest {
    public static void main(String[] args) {
        try {
            //準備url
            URL url = new File("D:/classes").toURI().toURL();
            URL[] urls = {url};
            //獲取有關類型加載的JMX接口
            ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();
            //用於緩存類加載器
            List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();
            while (true) {
                //加載類型並緩存類加載器實例
                ClassLoader classLoader = new URLClassLoader(urls);
                classLoaders.add(classLoader);
                classLoader.loadClass("ClassA");
                //顯示數量信息(共加載過的類型數目,當前還有效的類型數目,已經被卸載的類型數目)
                System.out.println("total: " + loadingBean.getTotalLoadedClassCount());
                System.out.println("active: " + loadingBean.getLoadedClassCount());
                System.out.println("unloaded: " + loadingBean.getUnloadedClassCount());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

虛擬機器參數設置以下:-verbose -verbose:gc

設置-verbose參數是爲了獲取類型加載和卸載的信息

設置-verbose:gc是爲了獲取垃圾收集的相關信息

JDK 1.7 @64-bit – PermGen 耗盡測試

Java1.7的PermGen默認空間爲85 MB(或者能夠經過-XX:MaxPermSize=XXXm指定)

能夠從上面的JVisualVM的截圖看出:當加載超過6萬個類以後,PermGen被耗盡。咱們也能經過程序和GC的輸出觀察耗盡的過程。

程序輸出(摘取了部分)

......
[Loaded ClassA from file:/D:/classes/]
total: 64887
active: 64887
unloaded: 0
[GC 245041K->213978K(536768K), 0.0597188 secs]
[Full GC 213978K->211425K(644992K), 0.6456638 secs]
[GC 211425K->211425K(656448K), 0.0086696 secs]
[Full GC 211425K->211411K(731008K), 0.6924754 secs]
[GC 211411K->211411K(726528K), 0.0088992 secs]
...............
java.lang.OutOfMemoryError: PermGen space

JDK 1.8 @64-bit – Metaspace大小動態調整測試

Java的Metaspace空間:不受限制 (默認)

從上面的截圖能夠看到,JVM Metaspace進行了動態擴展,本地內存的使用由20MB增加到646MB,以知足程序中不斷增加的類數據內存佔用需求。咱們也能觀察到JVM的垃圾回收事件—試圖銷燬僵死的類或類加載器對象。可是,因爲咱們程序的泄漏,JVM別無選擇只能動態擴展Metaspace內存空間。程序加載超過10萬個類,而沒有出現OOM事件。

JDK 1.8 @64-bit – Metaspace 受限測試

Java的Metaspace空間:128MB(-XX:MaxMetaspaceSize=128m)

能夠從上面的JVisualVM的截圖看出:當加載超過2萬個類以後,Metaspace被耗盡;與JDK1.7運行時很是類似。咱們也能經過程序和GC的輸出觀察耗盡的過程。另外一個有趣的現象是,保留的原生內存佔用量是設定的最大大小兩倍之多。這可能代表,若是可能的話,可微調元空間容量大小策略,來避免本地內存的浪費。

從Java程序的輸出中看到以下異常。

[Loaded ClassA from file:/D:/classes/]
total: 21393
active: 21393
unloaded: 0
[GC (Metadata GC Threshold) 64306K->57010K(111616K), 0.0145502 secs]
[Full GC (Metadata GC Threshold) 57010K->56810K(122368K), 0.1068084 secs]
java.lang.OutOfMemoryError: Metaspace

在設置了MaxMetaspaceSize的狀況下,該空間的內存仍然會耗盡,進而引起「java.lang.OutOfMemoryError: Metadata space」錯誤。由於類加載器的泄漏仍然存在,而一般Java又不但願無限制地消耗本機內存,所以設置一個相似於MaxPermSize的限制看起來也是合理的。

小結:

  • 以前無論是否是須要,JVM都會吃掉那塊空間……若是設置得過小,JVM會死掉;若是設置得太大,這塊內存就被JVM浪費了。理論上說,如今你徹底能夠不關注這個,由於JVM會在運行時自動調校爲「合適的大小」;
  • 提升Full GC的性能,在Full GC期間,Metadata到Metadata pointers之間不須要掃描了,別小看這幾納秒時間;
  • 隱患就是若是程序存在內存泄露,像OOMTest那樣,不停的擴展metaspace的空間,會致使機器的內存不足,因此仍是要有必要的調試和監控。

參考資料

  https://www.oracle.com/technetwork/java/javase/8-whats-new-2157071.html

   http://www.importnew.com/11908.html

   https://www.baeldung.com/java8

相關文章
相關標籤/搜索