天天看點

你要知道的Java8 匿名内部類、函數式接口、lambda表達式與Stream API都在這裡

轉載自:

https://blog.csdn.net/u014205968/article/details/71484312

https://blog.csdn.net/u014205968/article/details/71484374

轉載請注明出處 http://blog.csdn.net/u014205968/article/details/71484312

本文主要講解Java8 Stream API,但是要講解這一部分需要匿名内部類、lambda表達式以及函數式接口的相關知識,本文将分為兩篇文章來講解上述内容,讀者可以按需查閱。

Java 匿名内部類、lambda表達式與函數式接口

Java Stream API

本文是該系列博文的第一篇Java 匿名内部類、lambda表達式與函數式接口,主要講解Java的匿名内部類、lambda表達式以及函數式接口,第二篇文章Java Stream API主要講解Java Stream API。

匿名内部類

匿名内部類适用于那些隻需要使用一次的類,比如設計模式下的指令模式,往往通過定義一系列接口進行調用,有時有的指令隻會執行一次就不再執行,這個時候如果單獨定義一個類就顯得過于複雜并且編譯會生成這個類的.class檔案,不利于管理,這種情況下使用匿名内部類就能夠很好的簡化程式設計并且不會編譯生成單獨的.class檔案。

接下來看一個例子:

interface Programmer

{

void listLanguages();

void introduceMyself();

}

class MyProgrammer implements Programmer

{

public void listLanguages() {

System.out.println(“Objective-C Swift Python Go Java”);

}

public void introduceMyself() {
    System.out.println("My Name is Jiaming Chen");
}
           

}

public class HelloWorld

{

static void info(Programmer programmer)

{

programmer.listLanguages();

programmer.introduceMyself();

}

public static void main(String[] args)
{   
    info(new MyProgrammer());
}
           

}

上面這個例子為了執行info函數定義了一個實作了Programmer接口的類MyProgrammer,如果它隻執行一次這樣就顯得過于複雜,如果采用匿名内部類就會在很大程度上簡化程式設計,首先介紹一下匿名内部類的基礎文法:

new 需要實作的接口() | 父類構造器()

{

//需要實作的方法或重載父類的方法

}

匿名内部類的文法很簡單,必須要實作一個接口或者繼承一個類,可以看到使用了new關鍵詞,是以在建立匿名内部類的同時會建立一個該類的執行個體,并且隻能建立一個執行個體,建立完成後這個匿名内部類就不能再使用,是以,匿名内部類不能是抽象類,由于匿名内部類沒有類名是以也不能定義構造函數,但是可以在定義匿名内部類的時候調用父類的有參構造器也可以定義初始化塊用于初始化父類的成員變量。下面這個栗子是将上述代碼修改為匿名内部類的實作方式:

class MyProgrammer implements Programmer

{

public void listLanguages() {

System.out.println(“Objective-C Swift Python Go Java”);

}

public void introduceMyself() {
    System.out.println("My Name is Jiaming Chen");
}
           

}

public class HelloWorld

{

static void info(Programmer programmer)

{

programmer.listLanguages();

programmer.introduceMyself();

}

public static void main(String[] args)
{   
    info(new Programmer(){
        public void listLanguages() {
            System.out.println("Objective-C Swift Python Go Java");
        }

        public void introduceMyself() {
            System.out.println("My Name is Jiaming Chen");
        }
    });
}
           

}

通過對比發現,使用匿名内部類比重新定義一個新類更加簡潔,在建立匿名内部類的時候可以調用父類的有參構造函數,栗子如下:

abstract class Programmer

{

protected String name;

public Programmer(String name)
{
    this.name = name;
}

public abstract void listLanguages();
public abstract void introduceMyself();
           

}

public class HelloWorld

{

static void info(Programmer programmer)

{

programmer.listLanguages();

programmer.introduceMyself();

}

public static void main(String[] args)
{   
    int age = 22;
    Programmer p = new Programmer("Jiaming Chen"){
        public void listLanguages()
        {
            System.out.println("Objective-C Swift Python Go Java");
        }
        public void introduceMyself()
        {
            System.out.println("My Name is " + this.name + " and I'm " + age + " years old.");
            //age = 2;
        }
    };
    info(p);
    //age = 23;
}
           

}

上述栗子首先定義了一個抽象父類,并且該抽象父類隻有一個構造函數,是以在建立匿名内部類的時候需要顯示調用該構造函數,這樣就可以在匿名内部類内部使用父類定義的成員變量了,匿名内部類也可以使用外部變量,在Java8中上述栗子中的age會自動聲明為final類型,這稱為effectively final,隻要匿名内部類通路了一個局部變量,這個局部變量無論是否被final修飾它都會自動被聲明為final類型,不允許任何地方進行修改,Java與其他語言相比在閉包内通路外部變量的局限更大,因為隻能是final類型,比如OC在block内部也可以捕獲外部變量,swift也有一套閉包值捕獲機制,都可以對捕獲的值進行修改或權限的設定,Java的局限性太大。

lambda表達式

Java8引入了lambda表達式,在其他語言中,比如python、swift都支援lambda表達式,這個特性用起來也非常友善和簡潔。接下來舉一個常見的對一個清單進行排序的例子:

class MyComparator implements Comparator

{

public int compare(String o1, String o2)

{

return o1.compareTo(o2);

}

}

public class HelloWorld

{

public static void main(String[] args)

{

ArrayList list = new ArrayList<>();

list.add(“Objective-C”);

list.add(“Swift”);

list.add(“Python”);

list.add(“Golang”);

list.add(“Java”);

list.sort(new MyComparator());

list.forEach(System.out::println);

}

}

在開發中經常會對一個集合類型進行排序操作,調用集合的sort方法時需要傳入一個實作了Comparator接口的參數,是以上述栗子就定義了一個類MyComparator并且這個類實作了Comparator接口,是以可以建立一個MyComparator的對象用于排序操作,不難發現這樣做非常複雜需要重新定義一個全新的類,經過前文的介紹這裡完全可以用匿名内部類來代替,關于最後一行代碼list.forEach(System.out::println);在後文會介紹,這裡先賣個關子,它的作用就是周遊整個集合并輸出。接下來看一下使用匿名内部類實作的方式:

public class HelloWorld

{

public static void main(String[] args)

{

ArrayList list = new ArrayList<>();

list.add(“Objective-C”);

list.add(“Swift”);

list.add(“Python”);

list.add(“Golang”);

list.add(“Java”);

list.sort(new Comparator(){

public int compare(String o1, String o2) {

return o1.compareTo(o2);

}

});

list.forEach(System.out::println);

}

}

結果同上,顯而易見,采用了匿名内部類更加的友善了,代碼簡潔明了,那有沒有再簡介一點的辦法呢?答案當然是肯定的,那就是使用lambda表達式,栗子如下:

public class HelloWorld

{

public static void main(String[] args)

{

ArrayList list = new ArrayList<>();

list.add(“Objective-C”);

list.add(“Swift”);

list.add(“Python”);

list.add(“Golang”);

list.add(“Java”);

list.sort((String o1, String o2)->{

return o1.compareTo(o2);

});

list.forEach(System.out::println);

}

}

通過上述代碼發現,sort函數傳入的參數是一個lambda表達式,整個代碼非常類似前文中我們實作的compare函數,但是我們不需要再寫new Comparator<String()這樣繁瑣的代碼,也不需要寫函數名,是以lambda表達式可以很好的替代匿名内部類,不難發現lambda表達式由三部分組成:

首先是參數清單,這個參數清單與要實作的方法的參數清單一緻,因為系統已經知道了你需要實作哪一個方法是以可以省略形參類型,當隻有一個參數時也可以省略圓括号,但是當沒有形參時圓括号不可以省略

接下來是一個->符号,該符号用于分隔形參清單與函數體,該符号不允許省略。

最後就是代碼體了,如果代碼體隻有一行代碼就可以省略掉花括号,并且如果方法需要有傳回值連return關鍵詞都可以省略,系統會自動将這一行代碼的結果傳回。

通過上面的講解,我們就可以寫一個更加簡潔lambda表達式了,栗子如下:

public class HelloWorld

{

public static void main(String[] args)

{

ArrayList list = new ArrayList<>();

list.add(“Objective-C”);

list.add(“Swift”);

list.add(“Python”);

list.add(“Golang”);

list.add(“Java”);

list.sort((s1, s2)->s1.compareTo(s2));

list.forEach(System.out::println);

}

}

上面的代碼我們省略了形參的類型,由于隻有一行我們同時省略了花括号和return語句,整個代碼相比使用匿名内部類更加簡潔了。到這裡有同學可能會問了,lambda表達式是怎麼知道我們實作的是接口的哪一個方法?

lambda表達式的類型也被稱為目标類型 target type,該類型必須是函數式接口 Functional Interface,函數式接口代表有且隻有一個抽象方法,但是可以包含多個預設方法或類方法的接口,是以使用lambda表達式系統一定知道我們實作的接口的哪一個方法,因為實作的接口有且隻有一個抽象方法供我們實作。

函數式接口可以使用注釋@FunctionalInterface來要求編譯器在編譯時進行檢查,是否隻包含一個抽象方法。Java提供了大量的函數式接口這樣就能使用lambda表達式簡化程式設計。lambda表達式的目标類型必須是函數式接口,lambda表達式也隻能為函數式接口建立對象因為lambda表達式隻能實作一個抽象方法。

前文介紹了在使用lambda表達式時,如果代碼體隻有一行代碼可以省略花括号,如果有傳回值也可以省略return關鍵詞,不僅如此,lambda表達式在隻有一條代碼時還可以引用其他方法或構造器并自動調用,可以省略參數傳遞,代碼更加簡潔,引用方法的文法需要使用::符号。lambda表達式提供了四種引用方法和構造器的方式:

引用對象的方法 類::執行個體方法

引用類方法 類::類方法

引用特定對象的方法 特定對象::執行個體方法

引用類的構造器 類::new

舉個栗子:

public class HelloWorld

{

public static void main(String[] args)

{

ArrayList list = new ArrayList<>();

list.add(“Objective-C”);

list.add(“Swift”);

list.add(“Python”);

list.add(“Golang”);

list.add(“Java”);

//list.sort((s1, s2)->s1.compareTo(s2));

list.sort(String::compareTo);

list.forEach(System.out::println);

}

}

對比上述兩行代碼,第一個sort函數傳入了一個lambda表達式用于實作Comparator接口的compare函數,由于該實作隻有一條代碼,是以可以省略花括号以及return關鍵字。第二個sort方法則直接引用了對象的執行個體方法,文法規則為類::執行個體方法,系統會自動将函數式接口實作的方法的所有參數中的第一個參數作為調用者,接下來的參數依次傳入引用的方法中即自動進行s1.compareTo(s2)的方法調用,明顯第二個sort函數調用更加簡潔明了。

最後一行代碼list.forEach(System.out::println);則引用了類方法,集合類的執行個體方法forEach接收一個Consumer接口對象,該接口是一個函數式接口,隻有一個抽象方法void accept(T t);,是以可以使用lambda表達式進行調用,這裡引用System.out的類方法println,引用文法類::類方法,系統會自動将實作的函數式接口方法中的所有參數都傳入該類方法并進行自動調用。

再舉一個栗子:

@FunctionalInterface

interface Index

{

int index(String subString);

}

@FunctionalInterface

interface Generator

{

String generate();

}

public class HelloWorld

{

static int getIndex(Index t, String subString)

{

return t.index(subString);

}

static String generateString(Generator g)
{
    return g.generate();
}

public static void main(String[] args)
{   
    String str = "Hello World";
    System.out.println(getIndex(str::indexOf, "e"));
    System.out.println(generateString(String::new).length());
}
           

}

這個栗子似乎沒有任何實際意義,就是為了示範引用特定對象的執行個體方法和引用類的構造器。接口Index和Generator都是函數式接口,是以可以使用lambda表達式。對于getIndex方法需要傳入一個實作Index接口的對象和一個子串,在調用時首先定義了一個字元串Hello World,然後引用了這個對象的執行個體方法indexOf,這個時候系統會自動将這個特定對象作為調用者然後将所有的參數是以傳入該實力方法。

引用構造器的方法也很簡單類::new,不再贅述。

總結

本文主要講解匿名内部類、lambda表達式、函數式接口以及lambda表達式的方法引用和構造器引用,通過幾個例子逐漸深入,逐漸簡化代碼的編寫,可以發現Java8提供的lambda表達式是如此的強大。接下來的一篇文章會對Java8新增的Stream API進行講解,Stream的流式API支援并行,對傳統程式設計方式進行了改進,可以編寫出更簡潔明了的高性能代碼。有興趣的讀者可以閱讀Java Stream API。

你要知道的Java8 匿名内部類、函數式接口、lambda表達式與Stream API都在這裡

轉載請注明出處 http://blog.csdn.net/u014205968/article/details/71484374

本文主要講解Java8 Stream API,但是要講解這一部分需要匿名内部類、lambda表達式以及函數式接口的相關知識,本文将分為兩篇文章來講解上述内容,讀者可以按需查閱。

Java 匿名内部類、lambda表達式與函數式接口

Java Stream API

本文是本系列文章的第二篇,主要講解Stream API,在學習Stream API之前要求讀者有一定的lambda表達式基礎,如果相關知識不了解可以參考本系列文章的第一篇Java 匿名内部類、lambda表達式與函數式接口。

Stream API

Java8新增的stream功能非常強大,這裡的stream和Java IO中的stream是完全不同概念的兩個東西。本文要講解的stream是能夠對集合對象進行各種串行或并發聚集操作,Stream API依賴于前一篇文講解的

lambda表達式,隻有當兩者結合時才能極大的提高程式設計效率并且代碼更易了解和維護。Stream API支援串行和并發的集合操作,這也是響應了現在多核處理器的需求,Stream API的并發采用的是我們熟悉的fork/join模式,手動編寫并行代碼很複雜也很容易出錯,但是采用Stream API來進行集合對象上的并發操作你不需要編寫任何多線程代碼就能夠輕而易舉的實作并發操作,進而提高代碼的運作效率,也極大的簡化了程式設計難度。

聚集操作

在實際開發中,我們經常對一個集合内的對象進行一系列的操作,比如排序、查找、過濾、重組、資料統計等操作,通常情況下我們可能會采用for循環周遊的方式來逐一進行操作,這樣的代碼即複雜又難以維護,如果對性能有要求再進行多線程代碼的編寫就更加的複雜了,同時也更容易出錯。

下面舉一個栗子:

class User

{

private String userID;

private boolean isVip;

private int balance;

public User(String userID, boolean isVip, int balance)
{
    this.userID = userID;
    this.isVip = isVip;
    this.balance = balance;
}

public boolean isVip()
{
    return this.isVip;
}

public String getUserID()
{
    return this.userID;
}

public int getBalance()
{
    return this.balance;
}
           

}

public class HelloWorld

{

public static void main(String[] args)

{

ArrayList users = new ArrayList<>();

users.add(new User(“2017001”, false, 0));

users.add(new User(“2017002”, true, 36));

users.add(new User(“2017003”, false, 98));

users.add(new User(“2017004”, false, 233));

users.add(new User(“2017005”, true, 68));

users.add(new User(“2017006”, true, 599));

users.add(new User(“2017007”, true, 1023));

users.add(new User(“2017008”, false, 9));

users.add(new User(“2017009”, false, 66));

users.add(new User(“2017010”, false, 88));

//普通實作方式
    ArrayList<User> tempArray = new ArrayList<>();
    ArrayList<String> idArray = new ArrayList<>(3);
    for (User user: users)
    {
        if (user.isVip())
        {
            tempArray.add(user);
        }
    }
    tempArray.sort(new Comparator<User>(){
        public int compare(User o1, User o2) {
            return o2.getBalance() - o1.getBalance();
        }
    });
    for (int i = 0; i < 3; i++)
    {
        idArray.add(tempArray.get(i).getUserID());
    }
    for (int i = 0; i < idArray.size(); i++)
    {
        System.out.println(idArray.get(i));
    }

    //Stream API實作方式
    //也可以使用parallelStream方法擷取一個并發的stream,提高計算效率
    Stream<User> stream = users.stream();
    List<String> array = stream.filter(User::isVip).sorted((t1, t2) -> t2.getBalance() - t1.getBalance()).limit(3).map(User::getUserID).collect(Collectors.toList());
    array.forEach(System.out::println);
}
           

}

上述代碼首先定義了一個使用者類,這個類儲存使用者是否是VIP、使用者ID以及使用者的餘額,假如現在有一個需求,将VIP中餘額最高的三個使用者的ID找出來,傳統的思路一般就是建立一個臨時的list,然後逐一判斷,将所有的VIP使用者加入到這個臨時的list中,然後調用集合類的sort方法根據餘額排序,最後再周遊三次擷取餘額最高的三個使用者的ID等資訊。這樣的方法看似簡單,但代碼寫出來即混亂也不好看,如果使用者量非常大,有幾千萬甚至幾個億,這樣周遊的方式效率就會特别低,如果手工加上多線程的并發操作,代碼就更加複雜了。

上述代碼的第二部分使用Stream API的方式來計算,首先通過集合類擷取了一個普通的stream,如果資料量大可以使用parallelStream方法擷取一個并發的stream,這樣接下來的計算程式員不需要編寫任何多線程代碼系統會自動進行多線程計算。擷取了stream以後首先調用filter方法找到是否為VIP使用者然後對VIP使用者進行排序操作,接下來限制隻擷取三個使用者的資訊,然後将使用者映射為使用者ID,最後将該stream轉換為集合類,兩種實作方式的結果完全一樣,但是明顯的采用Stream API的代碼更加簡潔易懂。

Stream API的編寫大量依賴lambda表達式以及lambda表達式的引用方法和引用構造器,如果您對這一塊不了解可以查閱文章Java 匿名内部類、lambda表達式與函數式接口。

如何使用Stream

A sequence of elements supporting sequential and parallel aggregate operations

1

上面是Java文檔中定義的Stream,可以看出,Stream就是元素的集合,并且可以采用串行或并行的方式進行聚集操作。在使用時我們可以将Stream了解為一個疊代器,隻不過這個疊代器更加進階,能夠對其中的每一個元素進行我們規定的計算。

當我們要使用Stream API時,首先需要建立一個Stream對象,可以通過集合類的執行個體方法stream或parallelStream來擷取一個普通的串行stream或是并行stream。也可以使用Stream、IntStream、LongStream或DoubleStream建立一個Stream對象,Stream是一個比較通用的流,可以代表任何引用資料類型,其他的則是指特定類型的流。最常用的就是通過一個集合類型來擷取相應類型的Stream。

流的操作分為中間操作 Intermediate和結束操作 Terminal:

中間操作(Intermediate):一個流可以采用鍊式調用的方式進行數個中間操作,主要目的就是打開流然後對這個流進行各種過濾、映射、聚集、統計操作等,如上述代碼中的filter、map操作等。每一個操作結束後都會傳回一個新的流,并且這些操作都是lazy的,也就是在進行結束操作時才會真正的進行計算,一次周遊就計算出所有結果。

結束操作(Terminal):一個流隻能執行一個結束操作,當執行了結束操作以後這個流就不能再被執行,也就是說不能再次進行中間操作或結束操作,是以結束操作一定是流的最後一個操作,如上述代碼中的collect方法。當開始執行結束操作的時候才會對流進行周遊并且隻一次周遊就計算出所有結果。

Stream的建立

通過集合類建立

通過集合建立Stream的方法是我們最常用的,集合類的執行個體方法stream和parallelStream可以擷取相應的流。

ArrayList users = new ArrayList<>();

users.add(new User(“2017001”, false, 0));

users.add(new User(“2017002”, true, 36));

users.add(new User(“2017003”, false, 98));

Stream stream = users.stream();

通過數組構造

String[] str = {“Hello World”, “Jiaming Chen”, “Zhouhang Cheng”};

Stream stream = Stream.of(str);

通過單個元素構造

Stream stream = Stream.of(1, 2, 3, 4);

Stream與Array和Collection的轉換

一般我們都會對Stream進行結束操作,用于擷取一個數組或是集合類,通過數組和集合類建立Stream前文已經介紹了,這裡介紹通過Stream擷取數組或集合類。

String[] str = {“Hello World”, “Jiaming Chen”, “Zhouhang Cheng”};

Stream stream = Stream.of(str);

String[] strArray = stream.toArray(String[]::new);

List strList = stream.collect(Collectors.toList());

ArrayList strArrayList = stream.collect(Collectors.toCollection(ArrayList::new));

Set strSet = stream.collect(Collectors.toSet());

上面的代碼分别将流轉換為數組、List、ArrayList和Set類型,具體的參數可以檢視官方API文檔。

Stream 常用方法

filter

filter的栗子前面已經舉過了,filter函數需要傳入一個實作Predicate函數式接口的對象,該接口的抽象方法test接收一個參數并傳回一個boolean值,為true則保留,false則剔除,前文舉的栗子就是判斷是否為VIP使用者,如果是就保留,不是就剔除。

原理如圖所示:

filter

map、flatMap

map的栗子前面已經舉過了,map函數需要傳入一個實作Function函數式接口的對象,該接口的抽象方法apply接收一個參數并傳回一個值,可以了解為映射關系,前文舉的栗子就是将每一個使用者映射為一個userID。

原理如圖所示:

map

map方法是一個一對一的映射,每輸入一個資料也隻會輸出一個值。

flatMap方法是一對多的映射,對每一個元素映射出來的仍舊是一個Stream,然後會将這個子Stream的元素映射到父集合中,栗子如下:

Stream<List> inputStream = Stream.of(Arrays.asList(1), Arrays.asList(2, 3), Arrays.asList(4, 5, 6));

List integerList = inputStream.flatMap((childList) -> childList.stream()).collect(Collectors.toList());

//将一個“二維數組”flat為“一維數組”

integerList.forEach(System.out::println);

limit、skip

limit用于限制擷取多少個結果,與資料庫中的limit作用類似,skip用于排除前多少個結果。

sorted

sorted的栗子前面也舉過了,sorted函數需要傳入一個實作Comparator函數式接口的對象,該接口的抽象方法compare接收兩個參數并傳回一個整型值,作用就是排序,與其他常見排序方法一緻。

distinct

distinct用于剔除重複,與資料庫中的distinct用法一緻。

findFirst

findFirst方法總是傳回第一個元素,如果沒有則傳回空,它的傳回值類型是Optional類型,接觸過swift的同學應該知道,這是一個可選類型,如果有第一個元素則Optional類型中儲存的有值,如果沒有第一個元素則該類型為空。

Stream stream = users.stream();

Optional userID = stream.filter(User::isVip).sorted((t1, t2) -> t2.getBalance() - t1.getBalance()).limit(3).map(User::getUserID).findFirst();

userID.ifPresent(uid -> System.out.println(“Exists”));

min、max

min可以對整型流求最小值,傳回OptionalInt。

max可以對整型流求最大值,傳回OptionalInt。

這兩個方法是結束操作,隻能調用一次。

allMatch、anyMatch、noneMatch

allMatch:Stream中全部元素符合傳入的predicate傳回 true

anyMatch:Stream中隻要有一個元素符合傳入的predicate傳回 true

noneMatch:Stream中沒有一個元素符合傳入的predicate傳回 true

reduce

reduce方法用于組合Stream元素,它可以提供一個初始值然後按照傳入的計算規則依次和Stream中的元素進行計算,是以上文介紹的min、max都可以看做是reduce的一種實作。

舉個栗子:

IntStream is = IntStream.range(0, 10);

System.out.println(is.reduce(0, Integer::sum));

IntStream intStream = IntStream.range(0, 10);

System.out.println(intStream.reduce((o1, o2) -> o1 + o2));

Stream stream = Stream.of(“Hello”, “World”, “Jiaming”, “Chen”);

System.out.println(stream.reduce("", String::concat));

第一個IntStream調用的reduce方法設定了一個初始值,是以最終reduce計算的結果一定有值,該方法調用Integer的類方法sum用于計算Stream的總和。

第二個IntStream調用reduce方法時沒有設定初始值,是以最終reduce計算的結果不一定有值,是以傳回值類型是Optional類型,沒有提供初始值時會自動将第一個和第二個元素先進行計算,但有可能不存在第一個或第二個元素,是以傳回值是Optional類型。

Stream API的性能

這篇文章詳細測試了Stream API的性能Java Stream API性能測試。

總的來說,對于複雜計算并且擁有多核CPU來說,使用Stream API進行并發計算速度最快,也推薦使用。對于計算比較簡單,手工外部疊代性能更加。單核CPU盡量不要使用并發的Stream API計算。如果沒有太高的性能要求,想要編寫出簡潔的代碼還是推薦使用Stream API。