JDK1.8的新特性

JDK1.8的新特性

1. 前言

JDK1.8已經發布很久了,在很多企業中都已經在使用。並且Spring5、SpringBoot2.0都推薦使用JDK1.8以上版本。所以我們必須與時俱進,擁抱變化。

Jdk8這個版本包含語言、編譯器、庫、工具和JVM等方面的十多個新特性。在本文中我們將學習以下方面的新特性:

2. Lambda表達式

函數式編程

Lambda 表達式,也可稱爲閉包,它是推動 Java 8 發佈的最重要新特性。Lambda 允許把函數作爲一個方法的參數(函數作爲參數傳遞進方法中)。可以使代碼變的更加簡潔緊湊。

2.1 基本語法:

(參數列表) -> {代碼塊}

需要注意:

  • 參數類型可省略,編譯器可以自己推斷
  • 如果只有一個參數,圓括號可以省略
  • 代碼塊如果只是一行代碼,大括號也可以省略
  • 如果代碼塊是一行,且是有結果的表達式,return可以省略

注意:事實上,把Lambda表達式可以看做是匿名內部類的一種簡寫方式。當然,前提是這個匿名內部類對應的必須是接口,而且接口中必須只有一個函數!Lambda表達式就是直接編寫函數的:參數列表、代碼體、返回值等信息,用函數來代替完整的匿名內部類

2.2 用法示例

示例1:多個參數

準備一個集合:

// 準備一個集合
List<Integer> list = Arrays.asList(10, 5, 25, -15, 20);

假設我們要對集合排序,我們先看JDK7的寫法,需要通過匿名內部類來構造一個Comparator

// Jdk1.7寫法
Collections.sort(list,new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1 - o2;
    }
});
System.out.println(list);// [-15, 5, 10, 20, 25]

如果是jdk8,我們可以使用新增的集合API:sort(Comparator c)方法,接收一個比較器,我們用Lambda來代替Comparator 的匿名內部類:

// Jdk1.8寫法,參數列表的數據類型可省略:
list.sort((i1,i2) -> { return i1 - i2;});

System.out.println(list);// [-15, 5, 10, 20, 25]

對比一下Comparator中的compare()方法,你會發現:這裏編寫的Lambda表達式,恰恰就是compare()方法的簡寫形式,JDK8會把它編譯爲匿名內部類。是不是簡單多了!

彆着急,我們發現這裏的代碼塊只有一行代碼,符合前面的省略規則,我們可以簡寫爲:

// Jdk8寫法
// 因爲代碼塊是一個有返回值的表達式,可以省略大括號以及return
list.sort((i1,i2) -> i1 - i2);
示例2:單個參數

還以剛纔的集合爲例,現在我們想要遍歷集合中的元素,並且打印。

先用jdk1.7的方式:

// JDK1.7遍歷並打印集合
for (Integer i : list) {
    System.out.println(i);
}

jdk1.8給集合添加了一個方法:foreach() ,接收一個對元素進行操作的函數:

// JDK1.8遍歷並打印集合,因爲只有一個參數,所以我們可以省略小括號:
list.forEach(i -> System.out.println(i));
實例3:把Lambda賦值給變量

Lambda表達式的實質其實還是匿名內部類,所以我們其實可以把Lambda表達式賦值給某個變量。

// 將一個Lambda表達式賦值給某個接口:
Runnable task = () -> {
    // 這裏其實是Runnable接口的匿名內部類,我們在編寫run方法。
    System.out.println("hello lambda!");
};
new Thread(task).start();

不過上面的用法很少見,一般都是直接把Lambda作爲參數。

示例4:隱式final

Lambda表達式的實質其實還是匿名內部類,而匿名內部類在訪問外部局部變量時,要求變量必須聲明爲final!不過我們在使用Lambda表達式時無需聲明final,這並不是說違反了匿名內部類的規則,因爲Lambda底層會隱式的把變量設置爲final,在後續的操作中,一定不能修改該變量:

正確示範:

// 定義一個局部變量
int num = -1;
Runnable r = () -> {
    // 在Lambda表達式中使用局部變量num,num會被隱式聲明爲final
    System.out.println(num);
};
new Thread(r).start();// -1

錯誤案例:

// 定義一個局部變量
int num = -1;
Runnable r = () -> {
    // 在Lambda表達式中使用局部變量num,num會被隱式聲明爲final,不能進行任何修改操作
    System.out.println(num++);
};
new Thread(r).start();//報錯

3. 函數式接口

經過前面的學習,相信大家對於Lambda表達式已經有了初步的瞭解。總結一下:

  • Lambda表達式是接口的匿名內部類的簡寫形式
  • 接口必須滿足:內部只有一個函數

其實這樣的接口,我們稱爲函數式接口,我們學過的RunnableComparator都是函數式接口的典型代表。但是在實踐中,函數接口是非常脆弱的,只要有人在接口裏添加多一個方法,那麼這個接口就不是函數接口了,就會導致編譯失敗。Java 8提供了一個特殊的註解@FunctionalInterface來克服上面提到的脆弱性並且顯示地表明函數接口。而且jdk8版本中,對很多已經存在的接口都添加了@FunctionalInterface註解,例如Runnable接口:

在這裏插入圖片描述

另外,Jdk8默認提供了一些函數式接口供我們使用:

3.1 Function類型接口

@FunctionalInterface
public interface Function<T, R> {
	// 接收一個參數T,返回一個結果R
    R apply(T t);
}

Function代表的是有參數,有返回值的函數。還有很多類似的Function接口:

接口名 描述
BiFunction<T,U,R> 接收兩個T和U類型的參數,並且返回R類型結果的函數
DoubleFunction<R> 接收double類型參數,並且返回R類型結果的函數
IntFunction<R> 接收int類型參數,並且返回R類型結果的函數
LongFunction<R> 接收long類型參數,並且返回R類型結果的函數
ToDoubleFunction<T> 接收T類型參數,並且返回double類型結果
ToIntFunction<T> 接收T類型參數,並且返回int類型結果
ToLongFunction<T> 接收T類型參數,並且返回long類型結果
DoubleToIntFunction 接收double類型參數,返回int類型結果
DoubleToLongFunction 接收double類型參數,返回long類型結果

看出規律了嗎?這些都是一類函數接口,在Function基礎上衍生出的,要麼明確了參數不確定返回結果,要麼明確結果不知道參數類型,要麼兩者都知道。

3.2 Consumer系列

@FunctionalInterface
public interface Consumer<T> {
	// 接收T類型參數,不返回結果
    void accept(T t);
}

Consumer系列與Function系列一樣,有各種衍生接口,這裏不一一列出了。不過都具備類似的特徵:那就是不返回任何結果。

3.3 Predicate系列

@FunctionalInterface
public interface Predicate<T> {
	// 接收T類型參數,返回boolean類型結果
    boolean test(T t);
}

Predicate系列參數不固定,但是返回的一定是boolean類型。

3.4 Supplier系列

@FunctionalInterface
public interface Supplier<T> {
	// 無需參數,返回一個T類型結果
    T get();
}

Supplier系列,英文翻譯就是“供應者”,顧名思義:只產出,不收取。所以不接受任何參數,返回T類型結果。

4. 方法引用

方法引用使得開發者可以將已經存在的方法作爲變量來傳遞使用。方法引用可以和Lambda表達式配合使用。

4.1 語法:

總共有四類方法引用:

語法 描述
類名::靜態方法名 類的靜態方法的引用
類名::非靜態方法名 類的非靜態方法的引用
實例對象::非靜態方法名 類的指定實例對象的非靜態方法引用
類名::new 類的構造方法引用

4.2 示例

首先我們編寫一個集合工具類,提供一個方法:

	package com.pbteach;
    public class CollectionUtil{
        /**
         * 利用function將list集合中的每一個元素轉換後形成新的集合返回
         * @author www.pbteach.com攀博網
         * @param list 要轉換的源集合
         * @param function 轉換元素的方式
         * @param <T> 源集合的元素類型
         * @param <R> 轉換後的元素類型
         * @return
         * 
         */
        public static <T,R> List<R> convert(List<T> list, Function<T,R> function){
            List<R> result = new ArrayList<>();
            list.forEach(t -> result.add(function.apply(t)));
            return result;
        }
    }

可以看到這個方法接收兩個參數:

  • List<T> list:需要進行轉換的集合
  • Function<T,R>:函數接口,接收T類型,返回R類型。用這個函數接口對list中的元素T進行轉換,變爲R類型

接下來,我們看具體案例:

4.2.1 類的靜態方法引用

List<Integer> list = Arrays.asList(1000, 2000, 3000);

我們需要把這個集合中的元素轉爲十六進制保存,需要調用Integer.toHexString()方法:

public static String toHexString(int i) {
    return toUnsignedString0(i, 4);
}

這個方法接收一個 i 類型,返回一個String類型,可以用來構造一個Function的函數接口:

我們先按照Lambda原始寫法,傳入的Lambda表達式會被編譯爲Function接口,接口中通過Integer.toHexString(i)對原來集合的元素進行轉換:

// 通過Lambda表達式實現
List<String> hexList = CollectionUtil.convert(list, i -> Integer.toHexString(i));
System.out.println(hexList);// [3e8, 7d0, bb8]

上面的Lambda表達式代碼塊中,只有對Integer.toHexString()方法的引用,沒有其它代碼,因此我們可以直接把方法作爲參數傳遞,由編譯器幫我們處理,這就是靜態方法引用:

// 類的靜態方法引用
List<String> hexList = CollectionUtil.convert(list, Integer::toHexString;
System.out.println(hexList);// [3e8, 7d0, bb8]

4.2.2 類的非靜態方法引用

接下來,我們把剛剛生成的String集合hexList中的元素都變成大寫,需要藉助於String類的toUpperCase()方法:

public String toUpperCase() {
    return toUpperCase(Locale.getDefault());
}

這次是非靜態方法,不能用類名調用,需要用實例對象,因此與剛剛的實現有一些差別,我們接收集合中的每一個字符串s。但與上面不同然後s不是toUpperCase()的參數,而是調用者:

// 通過Lambda表達式,接收String數據,調用toUpperCase()
List<String> upperList = CollectionUtil.convert(hexList, s -> s.toUpperCase());
System.out.println(upperList);// [3E8, 7D0, BB8]

因爲代碼體只有對toUpperCase()的調用,所以可以把方法作爲參數引用傳遞,依然可以簡寫:

// 類的成員方法
List<String> upperList = CollectionUtil.convert(hexList, String::toUpperCase);
System.out.println(upperList);// [3E8, 7D0, BB8]

4.2.3 指定實例的非靜態方法引用

下面一個需求是這樣的,我們先定義一個數字Integer num = 2000,然後用這個數字和集合中的每個數字進行比較,比較的結果放入一個新的集合。比較對象,我們可以用IntegercompareTo方法:

public int compareTo(Integer anotherInteger) {
    return compare(this.value, anotherInteger.value);
}

先用Lambda實現,

List<Integer> list = Arrays.asList(1000, 2000, 3000);

// 某個對象的成員方法
Integer num = 2000;
List<Integer> compareList = CollectionUtil.convert(list, i -> num.compareTo(i));
System.out.println(compareList);// [1, 0, -1]

與前面類似,這裏Lambda的代碼塊中,依然只有對num.compareTo(i)的調用,所以可以簡寫。但是,需要注意的是,這次方法的調用者不是集合的元素,而是一個外部的局部變量num,因此不能使用 Integer::compareTo,因爲這樣是無法確定方法的調用者。要指定調用者,需要用 對象::方法名的方式:

// 某個對象的成員方法
Integer num = 2000;
List<Integer> compareList = CollectionUtil.convert(list, num::compareTo);
System.out.println(compareList);// [1, 0, -1]

4.2.4 構造函數引用

最後一個場景:把集合中的數字作爲毫秒值,構建出Date對象並放入集合,這裏我們就需要用到Date的構造函數:

/**
  * @param   date   the milliseconds since January 1, 1970, 00:00:00 GMT.
  * @see     java.lang.System#currentTimeMillis()
  */
public Date(long date) {
    fastTime = date;
}

我們可以接收集合中的每個元素,然後把元素作爲Date的構造函數參數:

// 將數值類型集合,轉爲Date類型
List<Date> dateList = CollectionUtil.convert(list, i -> new Date(i));
// 這裏遍歷元素後需要打印,因此直接把println作爲方法引用傳遞了
dateList.forEach(System.out::println);

上面的Lambda表達式實現方式,代碼體只有new Date()一行代碼,因此也可以採用方法引用進行簡寫。但問題是,構造函數沒有名稱,我們只能用new關鍵字來代替:

// 構造方法
List<Date> dateList = CollectionUtil.convert(list, Date::new);
dateList.forEach(System.out::println);

注意兩點:

  • 上面代碼中的System.out::println 其實是 指定對象System.out的非靜態方法println的引用
  • 如果構造函數有多個,可能無法區分導致傳遞失敗

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

Java 8使用兩個新概念擴展了接口的含義:默認方法和靜態方法。

5.1 默認方法

默認方法使得開發者可以在 不破壞二進制兼容性的前提下,往現存接口中添加新的方法,即不強制那些實現了該接口的類也同時實現這個新加的方法。

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

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類也實現了這個接口,但覆寫了該接口的默認方法,並提供了一個不同的實現。

5.2 靜態方法

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 ) {
    // 調用接口的靜態方法,並且傳遞DefaultableImpl的構造函數引用來構建對象
    Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new );
    System.out.println( defaulable.notRequired() );
	// 調用接口的靜態方法,並且傳遞OverridableImpl的構造函數引用來構建對象
    defaulable = DefaulableFactory.create( OverridableImpl::new );
    System.out.println( defaulable.notRequired() );
}

這段代碼的輸出結果如下:

Default implementation
Overridden implementation

由於JVM上的默認方法的實現在字節碼層面提供了支持,因此效率非常高。默認方法允許在不打破現有繼承體系的基礎上改進接口。該特性在官方庫中的應用是:給java.util.Collection接口添加新方法,如stream()parallelStream()forEach()removeIf()等等。

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

6. Optional

Java應用中最常見的bug就是空值異常。

Optional僅僅是一個容器,可以存放T類型的值或者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;如果Optional實例持有nullorElseGet()方法可以接受一個lambda表達式生成的默認值;map()方法可以將現有的Optional實例的值轉換成新的值;orElse()方法與orElseGet()方法類似,但是在持有null的時候返回傳入的默認值,而不是通過Lambda來生成。

上述代碼的輸出結果如下:

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

再看下另一個簡單的例子:

Optional< String > firstName = Optional.of( "www.pbteach.com" );
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: www.pbteach.com
Hey www.pbteach.com!

如果想了解更多的細節,請參考官方文檔。

7. Streams

新增的Stream API(java.util.stream)將生成環境的函數式編程引入了Java庫中。這是目前爲止最大的一次對Java庫的完善,以便開發者能夠寫出更加有效、更加簡潔和緊湊的代碼。

Steam 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類有一個points屬性,另外還有兩種狀態: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集合中一共有多少個OPEN狀態的?計算出它們的points屬性和。在Java 8之前,要解決這個問題,則需要使用foreach循環遍歷task集合;但是在Java 8中可以利用steams解決:包括一系列元素的列表,並且支持順序和並行處理。

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

這裏有很多知識點值得說。首先,tasks集合被轉換成steam表示;其次,在steam上的filter操作會過濾掉所有CLOSEDtask;第三,mapToInt操作基於tasks集合中的每個task實例的Task::getPoints方法將task流轉換成Integer集合;最後,通過sum方法計算總和,得出最後的結果。

在學習下一個例子之前,還需要記住一些steams(點此更多細節)的知識點。Steam之上的操作可分爲中間操作和晚期操作。

中間操作會返回一個新的steam——執行一箇中間操作(例如filter)並不會執行實際的過濾操作,而是創建一個新的steam,並將原steam中符合條件的元素放入新創建的steam。

晚期操作(例如forEach或者sum),會遍歷steam並得出結果或者附帶結果;在執行晚期操作之後,steam處理線已經處理完畢,就不能使用了。在幾乎所有情況下,晚期操作都是立刻對steam進行遍歷。

steam的另一個價值是創造性地支持並行處理(parallel processing)。對於上述的tasks集合,我們可以用下面的代碼計算所有task的points之和:

// 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方法並行處理所有的task,並使用reduce方法計算最終的結果。控制檯輸出如下:

Total points(all tasks): 26.0

對於一個集合,經常需要根據某些條件對其中的元素分組。利用steam提供的API可以很快完成這類任務,代碼如下:

// 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]]}

最後一個關於tasks集合的例子問題是:如何計算集合中每個任務的點數在集合中所佔的比重,具體處理的代碼如下:

// 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%]

最後,正如之前所說,Steam API不僅可以作用於Java集合,傳統的IO操作(從文件或者網絡一行一行得讀取數據)可以受益於steam處理,這裏有一個小例子:

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對軟件開發的現代範式的響應。

8. 並行數組

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個元素。上述例子的代碼輸出的結果是:

Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378 
Sorted: 39 220 263 268 325 607 655 678 723 793
發佈了125 篇原創文章 · 獲贊 24 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章