Java JVM(五):JDK8 新特性

        Java8 是一個重大改變的版本。比如說增加了Lambda表達式,集合的流式操作,函數式接口,接口默認方法 等。

一. 函數式接口
        如果一個接口定義唯一一個抽象方法,那麼這個接口就可以成爲函數式接口,如 Runnable,Callable 接口。
        可以在函數式接口前加一個 @FunctionalInterface註解,非必須。在接口中添加了@FunctionalInterface,只允許有一個抽象方法,否則編譯器也報錯。Java8 中 Runnable 接口就是一個函數式接口,如下:
@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

        像Runnable,Callable等有且只有一個抽象方法的接口,一般稱爲單一抽象方法接口,我們平常使用它的時候,比較習慣於使用一個內部類,這至少也要幾行的代碼,如下:
public class AnonymousInnerClassTest {
  public static void main(String[] args) {
    new Thread(new Runnable() {
      @Override
      public void run() {
        System.out.println("A thread created and running ...");
      }
    }).start();
  }
}

        如果我們聲明爲函數式接口,再結合Lambda表達式,那麼一行代碼就可以搞定,比較方便。如下代碼:
/*
* Implementing the interface by creating an
* anonymous inner class versus using 
* lambda expression.
*/
public class SimpleFunInterfaceTest {
  public static void main(String[] args) {
    carryOutWork(new SimpleFuncInterface() {
      @Override
      public void doWork() {
        System.out.println("Do work in SimpleFun impl...");
      }
    });

    carryOutWork(() -> System.out.println("Do work in lambda exp impl..."));
  }
  public static void carryOutWork(SimpleFuncInterface sfi){
    sfi.doWork();
  }
}

輸出爲:
Do work in SimpleFun impl...
Do work in lambda exp impl...

        當然,一個接口也可以繼承另外一個函數式接口,只要它不再聲明抽象方法(可以使用默認方法),那麼它也是一個函數式接口。
例如:
@FunctionalInterface
public interface ComplexFunctionalInterface extends SimpleFuncInterface {
  default public void doSomeWork(){
    System.out.println("Doing some work in interface impl...");
  }
  default public void doSomeOtherWork(){
    System.out.println("Doing some other work in interface impl...");
  }
}


總結:好處在哪裏?
         結合Lambda表達式,比之前更加的簡便。

二.lambda 表達式
        lambda 表達式是一個匿名方法,可由三部分組成: 參數列表,箭頭( ->),表達式或者語句塊。利用Lambda表達式,可以把一個幾行的語句變成一個非常簡單的語句。
        lambda表達式我們也可以把它當做一種類型,像基本類型,引用類型一樣,我們可以把它當做是一種目標類型,這種目標類型就是函數式接口。一個lambda表達式必須對應至少一個目標類型,也就是必須對應一種函數接口。
        一個lambda表達式只有在轉型成一個函數接口後才能被當做Object來進行使用,所以說我們要當Object來使用必須先轉型。

Function:
在java.util.function包中 預定義了很多函數式接口以避免用戶重複定義。最典型的就是Function:
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
    ......
}

這個接口代表一個函數,接受一個 T 類型的參數,並且返回一個 R 類型的返回值。

Consumer:
另一個預定義函數接口叫做 Consumer,跟 Function 的唯一不同是它沒有返回值。
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
    ......
}

Predicate:
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
    ......
}

用於判斷某項條件是否滿足。經常用來進行篩選操作。

綜合來說:
        一個Lambda表達式就是定義了一個匿名方法,只不過這個方法必須符合至少一個函數接口。

用處:
        lambda表達式主要用於替換以前廣泛使用的匿名內部類,各種回調,事件響應器等。
例如:
Thread oldSchool = new Thread( new Runnable () {
        @Override
        public void run() {
            System.out.println("This is from an anonymous class.");
        }
    });

Thread gaoDuanDaQiShangDangCi = new Thread( () -> {
        System.out.println("This is from an anonymous method (lambda exp).");
    });

        在這裏,比如說第二個線程中的lambda表達式,你並不需要顯示地轉換爲一個Runnable,因爲Java可以根據上下文自動推斷出一個Thread 的構造函數接受一個Runnable 參數,而傳入的 lambda表達式正好符合其 run() 函數,所以 Java編譯器推斷它爲 Runnable。

        從形式上來看, lambda 表達式只是節省了幾行代碼,但是從目前來說,lambda表達式可以配合 "集合類批處理操作"的內部迭代和並行處理;另外,長期來看,Java 也好像是要向函數式編程語言這個方面來引導。

三.集合的批處理和流式操作:
3.1 批處理
        集合的批處理和流式操作再配合Lambda表達式應該算是Java8 中最重要的改進,主要是希望在對集合的操作中,可以充分利用現代多核 CPU 來進行並行計算。
        我們之前 Java8 之前的集合操作都是外部迭代的,也就是寫一些 for 循環這種。那麼現在的集合類都有一個forEach()方法,來對元素實現迭代,這個forEach方法就在Iterable接口中,Collection接口繼承了它,同時,Map接口也有一個forEach方法,forEach() 方法可以接受一個函數式接口Consumer作爲參數,所以說可以使用lambda表達式。

3.2 流式操作
        Java8爲集合類引入了另外一個重要概念:流。一個流通常以一個集合類實例作爲其數據源,然後在其上定義各種操作。流的 API 設計使用了管道。對一個流的操作會返回另一個流。如同 IO 的API 或者 StringBuffer的 append 方法那樣,從而多個不同的操作可以在一個語句串起來。看下面的方法:
List<Shape> shapes = ...
shapes.stream()
    .filter(s -> s.getColor() == BLUE)
    .forEach(s -> s.setColor(RED));

        首先調用 stream() 方法,以集合類對象 shapes 裏面的元素作爲數據源,生成一個流。然後在這個流上調用 filter 方法,挑出藍色的,返回另外一個流。最後調用forEach() 方法將這些藍色的物體噴成紅色。(forEach方法不再返回流,而是一個終端方法,類似於StringBuffer在調用若干append之後的那個toString)
        filter 方法的參數是 Predicate 類型。forEach方法的參數是Consumer 類型,它們都是函數式接口,所以可以使用lambda表達式。
        還有一個方法叫做parallelStream(),它和Stream()一樣,只不過要指明並行處理,希望能充分利用現代CPU的多核特性。
例子: 給出一個String 類型的數組,找出其中所有不重複的素數
public void distinctPrimary(String... numbers) {
    List<String> l = Arrays.asList(numbers);
    List<Integer> r = l.stream()
        .map(e -> new Integer(e))
        .filter(e -> Primes.isPrime(e))
        .distinct()
         .collect(Collectors.toList());
    System.out.println("distinctPrimary result is: " + r);
}

第一步:傳入一系列String(假設都是合法的數字),轉成一個List,然後調用stream()方法生成流。

第二步:調用流的map方法把每個元素由String轉成Integer,得到一個新的流。map方法接受一個Function類型的參數,上面介紹了,Function是個函數接口,所以這裏用λ表達式。

第三步:調用流的filter方法,過濾那些不是素數的數字,並得到一個新流。filter方法接受一個Predicate類型的參數,上面介紹了,Predicate是個函數接口,所以這裏用λ表達式。

第四步:調用流的distinct方法,去掉重複,並得到一個新流。這本質上是另一個filter操作。

第五步:用collect方法將最終結果收集到一個List裏面去。collect方法接受一個Collector類型的參數,這個參數指明如何收集最終結果。在這個例子中,結果簡單地收集到一個List中。我們也可以用Collectors.toMap(e->e, e->e)把結果收集到一個Map中,它的意思是:把結果收到一個Map,用這些素數自身既作爲鍵又作爲值。toMap方法接受兩個 Function類型的參數,分別用以生成鍵和值,Function是個函數接口,所以這裏都用λ表達式。

你可能會覺得在這個例子裏,List l被迭代了好多次,map,filter,distinct都分別是一次循環,效率會不好。實際並非如此。這些返回另一個Stream的方法都是“懶 (lazy)”的,而最後返回最終結果的collect方法則是“急(eager)”的。在遇到eager方法之前,lazy的方法不會執行。

當遇到eager方法時,前面的lazy方法纔會被依次執行。而且是管道貫通式執行。這意味着每一個元素依次通過這些管道。例如有個元素“3”,首 先它被map成整數型3;然後通過filter,發現是素數,被保留下來;又通過distinct,如果已經有一個3了,那麼就直接丟棄,如果還沒有則保 留。這樣,3個操作其實只經過了一次循環。


除collect外其它的eager操作還有forEach,toArray,reduce等。
下面來看一下也許是最常用的收集器方法,groupingBy:
//給出一個String類型的數組,找出其中各個素數,並統計其出現次數
public void primaryOccurrence(String... numbers) {
    List<String> l = Arrays.asList(numbers);
    Map<Integer, Integer> r = l.stream()
        .map(e -> new Integer(e))
        .filter(e -> Primes.isPrime(e))
        .collect( Collectors.groupingBy(p->p, Collectors.summingInt(p->1)) );
    System.out.println("primaryOccurrence result is: " + r);
}

注意這一行:
Collectors.groupingBy(p->p, Collectors.summingInt(p->1))

它的意思是:把結果收集到一個Map中,用統計到的各個素數自身作爲鍵,其出現次數作爲值。

下面是一個reduce的例子:
//給出一個String類型的數組,求其中所有不重複素數的和
public void distinctPrimarySum(String... numbers) {
    List<String> l = Arrays.asList(numbers);
    int sum = l.stream()
        .map(e -> new Integer(e))
        .filter(e -> Primes.isPrime(e))
        .distinct()
        .reduce(0, (x,y) -> x+y); // equivalent to .sum()
    System.out.println("distinctPrimarySum result is: " + sum);
}

reduce方法用來產生單一的一個最終結果。
流有很多預定義的reduce操作,如sum(),max(),min()等。


再舉個現實世界裏的栗子比如:
// 統計年齡在25-35歲的男女人數、比例
public void boysAndGirls(List<Person> persons) {
    Map<Integer, Integer> result = persons.parallelStream().filter(p -> p.getAge()>=25 && p.getAge()<=35).
        collect(
            Collectors.groupingBy(p->p.getSex(), Collectors.summingInt(p->1))
    );
    System.out.print("boysAndGirls result is " + result);
    System.out.println(", ratio (male : female) is " + (float)result.get(Person.MALE)/result.get(Person.FEMALE));
}


Ps:
        給Java 集合類增加批量數據的支持,通常稱這種批量數據操作爲 "Java 中的 filter/map/reduce"。批量操作有並行和串行兩種操作模式。我們期望可以利用底層平臺的並行特性。
        JDK 中已經增加了一個新包:java.util.stream包,能夠使java8 集合類庫執行類似 filter/map/reduce 的操作。這個流式API 能夠使我們在數據流之上編寫串行或者並行的操作。比如說:
List persons = ..
 //串行操作
Stream stream=persons.stream();
 //並行操作
Stream parallelStream=persons.parallelStream();

四. 接口默認方法
        Java8 可以在 interface 中有方法的實現,也稱爲默認方法。那麼 interface 中 有了方法的實現,也就是說,可以實現多繼承,(當然之前利用一些方法也可以實現多繼承,但是Java 一直不提倡多繼承)。
        那麼爲什麼現在加入了默認方法實現多繼承呢?因爲 interface 太過依賴他們的實現類了,要往 interface中加入一個方法就必須修改所有的實現類。那麼Java8 中加入一些特性可能需要修改一些核心類,但是很多核心類不僅僅JDK中實現,很多第三方庫中也會有實現,那麼一改動可能就會出現兼容性問題。那麼默認方法就可以很好的處理這個問題。
        代碼示例:
interface ITest {
	public default void sayHello(){
		System.out.println("Hello");
	}
}
public class Test implements ITest{
	public static void main(String[] args) {
		new Test().sayHello();
	}
}

注意,需要加一個 default。


參考:
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章