閱讀108 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Java 8 特性 – 終極手冊(一)

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

我們可以看到,注釋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!

更多詳細信息請參考官方文檔

最後更新:2017-05-24 09:31:42

  上一篇:go  Java 8 特性 – 終極手冊(二)
  下一篇:go  Maven入門指南(二)