Java 8新特性:學習如何使用Lambda表達式(二)

2021-03-06 享學課堂online

接著上一篇文章繼續談談Lambda表達式。

目錄

介紹

我們為什麼需要lambdas?

Lambdas的語法

功能接口

方法參考

構造函數參考

可變範圍

默認方法

結論

方法參考

有時你已經有了一個適合你需求的方法,你想將它傳遞給其他一些方案。例如,假設您希望在單擊按鈕時只列印事件對象。你可以這樣寫

button.setOnAction(event -> System.out.println(event));

將 println方法傳遞給 setOnAction方法更直觀。以下示例顯示了它:

button.setOnAction(System.out::println);

System.out::println是一個方法引用,類似於lambda表達式。我們可以在這裡用方法引用替換lambda。

想像一下,你想要忽略一個字母大小寫的排序字符串。您可以編寫如下代碼:

Arrays.sort(strs, String::compareToIgnoreCase)

運算符 ::將方法名稱與對象或類的名稱分開。主要有三種情況:

在前兩種情況下,方法引用等效於帶有方法參數的lambda表達式。如上所示, System.out::println相當於 x->System.out.println(x)。同樣, Math::pow相當於 (x,y)->Math.pow(x,y)。在第三種情況下,第一個參數成為方法的目標。例如, String::compareToIgnoreCase與...相同 (x,y)->x.compareToIgnoreCase(y)。

眾所周知,類可以有多個具有相同名稱的重載方法。在這種情況下,編譯器將嘗試從上下文中找到要選擇的內容。例如,該 Math.max方法有兩個版本,一個用於int,一個用於double值。調用哪一個取決於 Math::max轉換的功能接口的方法籤名。方法引用不是單獨存在的。與幕後的lambdas類似,它們總是變成功能接口的實例。

您可能想到是否可以使用 this在方法參考中捕獲參數。是的你可以。例如, this::equals相當於 x->this.equals(x)。它也可以使用 super。當我們使用 super::instanceMethod它成為目標並調用給定方法的基類版本。這是一個非常真實的例子:

class Speaker {

public void speak() {

System.out.println("Hello, world!");

}

}

class ConcurrentSpeaker extends Speaker {

public void speak() {

Thread t = new Thread(super::speak);

t.start();

}

}

當線程啟動時, run調用方法並 super::speak執行,調用 speak其基類的方法。請注意,在內部類中,您可以 this將封閉類的引用,捕獲為 EnclosingClass.this::method或 EnclosingClass.super::method。

構造函數參考

構造函數引用與方法引用相同,只是方法名稱為 new。例如, Button::new是類的構造函數引用 Button。將調用哪個構造函數取決於上下文。想像一下,您想要將字符串列錶轉換為按鈕數組。在這種情況下,您應該在每個字符串上調用構造函數。它可能是這樣的:

List<String> strs = ...;

Stream<Button> stream = strs.stream().map(Button::new);

List<Button> buttons = stream.collect(Collectors.toList());

有關 stream的更多信息,您可以查看文檔。在這種情況下,最重要的是該方法為每個列表元素調用構造函數。有多個構造函數,但編譯器選擇帶有參數的構造函數,因為從上下文中可以明顯看出應該調用帶有字符串的構造函數。map``collect``map``Button(String)``Button``String

還可以為數組類型創建構造函數引用。例如, int數組的構造函數引用是 int[]::new。它需要一個參數:數組的長度。它等同於lambda表達式 x->newint[x]。

數組的構造函數引用對於超越Java的限制很有用。創建泛型類型的數組是不可能的 T。表達式 newT[n]不正確,因為它將替換為 newObject[n]。對於圖書館作者來說這是一個問題。想像一下,我們想擁有一系列按鈕。有一種方法 toArray在類 Stream返回的數組 Object:

Object[] buttons = stream.toArray();

但那不是你想要的。用戶不想要 Objects,只有按鈕。該庫使用構造函數引用解決了這個問題。你應該傳遞 Button[]::new給方法 toArray:

Button[] buttons = stream.toArray(Button[]::new);

該 toArray方法調用此構造函數以獲取所需類型的數組,然後在填充後返回此數組。

可變範圍

通常,您希望從lambda表達式中的封閉範圍訪問變量。考慮以下代碼:

public static void repeatText(String text, int count) {

Runnable r = () -> {

for (int i = 0; i < count; i++) {

System.out.println(text);

Thread.yield();

}

};

new Thread(r).start();

}

看下面這個調用:

repeatText("Hi!", 2000); // Prints Hi 2000 times in a separate thread

注意變量 count和 text沒有在lambda表達式中定義; 這些是封閉方法的參數。

如果仔細觀察這段代碼,你會發現幕後有某種魔力。該 repeatText方法可以在lambda表達式的代碼運行之前返回,而那時參數 count和 text變量將消失,但它們仍然可用於lambda。秘密是什麼?

要了解發生了什麼,我們需要提高對lambda表達式的理解。lambda表達式由三個部分組成:

代碼塊

參數

自由變量不是參數,也沒有在lambda中定義

在我們的案例中有兩個自由變量, text和 count。表示lambda的數據結構必須存儲它們的值,「嗨!」 他們說這些值是由lambda表達式捕獲的。(如何完成取決於實現。例如,實現可以使用一個方法將lambda表達式轉換為對象,並將自由變量的值複製到對象的實例變量中。)

有一個特殊的術語「封閉」; 它是一個代碼塊和自由變量的值。Lambda用一種方便的語法表示Java中的閉包。順便說一句,內部類總是封閉的。

因此,lambda表達式可以捕獲封閉範圍中變量的值,只是有一些限制。你無法更改這些捕獲變量的值。以下代碼不正確:

public static void repeatText(String text, int count) {

Runnable r = () -> {

while (count > 0) {

count--; // Error: Can't modify captured variable

System.out.println(text);

Thread.yield();

}

};

new Thread(r).start();

}

這種限制是合理的,因為lambda表達式中的變量變量不是線程安全的。想像一下,我們有一系列並發任務,每個任務都更新一個共享計數器。

int matchCount = 0;

for (Path p : files)

new Thread(() -> { if (p has some property) matchCount++; }).start();

// Illegal to change matchCount

如果這段代碼是正確的,那將是非常非常糟糕的。increment運算符 ++不是原子的,如果多個線程同時執行此代碼,則無法控制結果。

內部類也可以從外部類中捕獲值。在Java 8之前,內部類只能訪問 final局部變量。此規則已擴展為與lambda表達式匹配。現在,內部類可以處理其值不會發生變化的任何變量(實際上是 final變量)。

不要指望編譯器捕獲所有並發訪問錯誤。您應該知道,此修改規則僅適用於局部變量。如果我們使用外部類的實例或靜態變量,則不會出現錯誤,結果是未定義的。

您也可以修改共享對象,即使它不健全。例如,

List<Path> matchedObjs = new ArrayList<>();

for (Path p : files)

new Thread(() -> { if (p has some property) matchedObjs.add(p); }).start();

// Legal to change matchedObjs, but unsafe

如果你仔細觀察,你會發現變量 matchedObjs實際上是final的。(有效的final變量是一個在初始化後再無變化的變量。)在此代碼中, matchedObjs始終引用同一個對象。但是,該變量 matchedObjs已被修改,並且它不是線程安全的。在多線程環境中運行此代碼的結果是不可預測的。

這種多線程任務有安全的機制。例如,您可以使用線程安全計數器和集合,流來收集值。

對於內部類,有一種解決方法允許lambda表達式更新封閉的本地範圍中的計數器。想像一下,你使用長度為1的數組,如下所示:

int[] counts = new int[1];

button.setOnAction(event -> counts[0]++);

很明顯,這段代碼不是線程安全的。對於按鈕回調,這沒關係,但一般來說,在使用這個技巧之前你應該三思而後行。

lambda的主體與嵌套塊具有相同的範圍。名稱衝突和陰影的規則是相同的。您不能在lambda中聲明與封閉範圍中的變量同名的參數或局部變量。

Path first = Paths.get("/usr/local");

Comparator<String> comp =

(first, second) -> Integer.compare(first.length(), second.length());

// Error: Variable first already defined

在方法中不能有兩個具有相同名稱的局部變量。因此,您也不能在lambda表達式中引入這些變量。同樣的規則適用於lambda。在 thislambda中使用關鍵字時,請參考 this創建lambda的方法。我們考慮一下這段代碼

public class Application() {

public void doSomething() {

Runnable r = () -> { ...; System.out.println(this.toString()); ... };

...

}

}

在此示例中 <code>this.toString()調用對象的 toString方法 Application,而不是 Runnable實例。this在lambda表達式中使用沒有什麼特別之處。lambda表達式的範圍嵌套在 doSomething方法中,並且在該方法中的任何位置都具有相同的含義。

默認方法

最後,讓我們談談與lambdas沒有直接關係的新功能,它也非常有趣 - 默認方法。

在許多程式語言中,函數表達式與其集合庫集成在一起。這通常會導致代碼比使用循環的等效代碼更簡單,更容易理解。我們考慮一下代碼:

for (int i = 0; i < strList.size(); i++)

System.out.println(strList.get(i));

我們可以改進這段代碼。該庫可以提供一種方便的方法 forEach,將函數應用於每個元素。所以你可以簡化代碼

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

如果庫是從頭開始設計的,那麼一切都還可以,但是如果它是很久以前在Java中創建的呢?如果 Collection接口添加了一個新方法,例如 forEach,那麼定義自己實現此接口的類的每個應用程式都將無法工作,直到它實現該方法。它在Java中並不好。

Java中的這個問題通過允許具有特定實現的接口方法(命名為默認方法)來解決。您可以將此類方法安全地添加到現有接口。現在我們將更密切地研究默認方法。在Java 8 中,使用您將在下面看到的技巧將該 forEach方法添加到 Iterable基本接口 Collection中。

考慮這個界面:

interface Person {

long getId();

default String getFirstName() { return "Jack"; }

}

接口中有兩種方法:getId抽象方法, getFirstName默認方法。當然,實現此接口的具體類必須提供實現 getId,但是它可以選擇使用 getFirstName或覆蓋它的默認實現。

這種技術使用提供接口的經典模式和實現其大部分或全部方法的抽象類來停止,例如 Collection/AbstractCollection或 WindowListener/WindowAdapter。現在,您可以在界面中實現所需的方法。

如果具有相同籤名的方法在一個接口中定義為默認方法,然後再在基類或另一個接口中定義,會發生什麼?其他語言(如C ++)具有解決此類歧義的複雜規則。幸運的是,Java中的規則要簡單得多。我們來看看它們:

基礎類獲勝。如果基類包含具體方法,則會忽略具有相同籤名的默認方法。

接口發生衝突。如果基接口具有默認方法,並且另一個接口包含具有相同籤名的方法(默認與否),則必須通過覆蓋該方法手動解決衝突。

讓我們仔細看看第二條規則。想像一下使用 getFirstName方法的另一個接口:

interface Naming {

default String getFirstName() { return getClass().getName() + "_" + hashCode(); }

}

如果您嘗試創建一個實現它們的類,會發生什麼?

class Student implements Person, Naming {

...

}

該類繼承兩種不同的方法 getFirstName通過接口提供的 Person和 Naming。Java編譯器報告錯誤而不是選擇其中一個錯誤,而是由程式設計師決定是否解決了歧義。只需 getFirstName在類中實現一個方法 Student。然後在該方法中,您可以選擇兩種衝突方法中的一種,如下所示:

class Student implements Person, Naming {

public String getFirstName() { returnPerson.super.getFirstName (); }

...

}

現在讓我們考慮一下 Naming接口不提供默認實現的情況 getFirstName:

interface Naming {

String getFirstName();

}

Student該類是否會從 Person接口繼承默認方法?Java設計者決定採用統一性,儘管這種方式可能是合理的。無論接口究竟如何衝突。如果至少一個接口包含實現,則編譯器報告衝突,並且程式設計師必須手動解決該問題。

如果沒有任何接口提供該方法的默認實現,則沒有問題。最後一個類可以實現該方法,也可以不實現該方法。在第二種情況下,類本身仍然是抽象的。

我剛剛描述了兩個接口之間的名稱衝突。現在讓我們考慮一個繼承另一個類並實現接口的類,從兩者繼承相同的方法。例如,假設它 Person是一個類, Naming是一個接口,類 Student定義為:

class Student extends Person implements Naming { ... }

在這種情況下,簡單地忽略界面中的任何默認方法。在我們的示例中, Student繼承了 getFirstName方法 Person, Naming接口是否提供默認實現 getFirstName並不重要。這是行動中的「階級勝利」規則。此規則可確保與Java 7的兼容性。如果將默認方法添加到接口,則它對舊代碼沒有影響。但請記住,您永遠不能創建覆蓋類中某個方法的默認方法 Object。舉例來說,你不能重新定義默認的方法 toString或者 equals,雖然它可能是如接口有用 List。作為規則的結果,這種方法永遠不會贏得反對 Object.toString或 Object.equals。

默認方法允許您向現有接口添加新功能,並確保與為這些接口的舊版本編寫的代碼的二進位兼容性。特別是,使用默認方法,您可以添加接受lambda表達式作為現有接口參數的方法。

結論

我已經將lambda表達式描述為有史以來對編程模型的最大升級 - 甚至可能比泛型更大。我們可以使用lambdas編寫一些非常好的代碼。

我希望本文能夠一瞥Java 8為我們提供一些新變化。

推薦閱讀:

今日課題:


↑點擊圖片直接跳轉觀看免費直播課程↑

↑點擊圖片直接跳轉觀看免費直播課程↑

相關焦點

  • Java 8新特性:學習如何使用Lambda表達式(一)
    我將分為兩篇系列文章來描述了使用Java 8的新特性 - lambda表達式。
  • Java8 lambda表達式
    如何在正確的場合使用lambda表達式?下面有幾個使用lambda表達式的例子①展示了在沒有參數的情況下如何使用lambda,可以使用一對空的小括號來表示沒有參數,這是一個實現了Runnable的lambda的表達式,該接口只有一個方法run(),該方法不接受任何參數,且返回void.
  • Java lambda表達式
    Java lambda表達式是Java 8新特性。它是步入Java函數式編程的第一步。因此,Java lambda表達式是創建時不屬於任何類的函數。它可以像一個對象一樣傳遞,並按要求執行。在Java 8中,您可以使用Java lambda表達式添加事件監聽器,如下所示:StateOwner stateOwner = new StateOwner();stateOwner.addStateListener(    (oldState, newState) -> System.out.println("State
  • java8 之 lambda 表達式簡介
    Lambda 表達式是 Java8 最重要也最令人期待的特性,它使得 Java 初步具有了函數式編程的能力。
  • 【Java 8系列】Lambda 表達式,你學廢了嗎
    1.前言Lambda 表達式,也可稱為閉包,它是推動 Java 8 發布的最重要新特性。Lambda 允許把函數作為一個方法的參數(函數作為參數傳遞進方法中)。使用Lambda 表達式可以使代碼變的更加簡潔緊湊。Lambda 表達式是一種匿名函數(對 Java 而言這並不完全正確,但現在姑且這麼認為),簡單地說,它是沒有聲明的方法,也即沒有訪問修飾符、返回值聲明和名字。1.1 為什麼 Java 需要 Lambda 表達式?
  • Effective java-Lambda使用
    為了能根據時代發展,java 8中引入了lambda表達式。促進了java的函數編程,大大提升了開發效率。lambda表達式的出現,改變了Java開發者的編程習慣,但lambda應該如何更好的使用呢? effective java 中給出了說明。
  • 深度解讀 深入淺出 Java 8 Lambda 表達式
    簡而言之,在 Java 裡將普通的方法或函數像參數一樣傳值並不簡單,為此,Java 8 增加了一個語言級的新特性,名為 Lambda 表達式。為什麼 Java 需要 Lambda 表達式?如果忽視註解(Annotations)、泛型(Generics)等特性,自 Java 語言誕生時起,它的變化並不大。
  • 面試中一定會問的JAVA8新特性——Lambda表達式
    從張汝京的大陸IC夢說起 :(一)張汝京的IC經歷Lambda表達式(也稱為閉包)是整個Java 8發行版中最受期待的在Java語言層面上的改變,Lambda允許把函數作為一個方法的參數(函數作為參數傳遞進方法中),或者把代碼看成數據:函數式程式設計師對這一概念非常熟悉。
  • 自從學會Java中的lambda表達式和函數式編程技巧,再也不用加班了!
    題記本文將分享如何在Java程序中使用lambda表達式和函數式編程技巧在Java SE 8之前,匿名類通常用於將功能傳遞給方法。這種做法混淆了原始碼,使代碼難以理解***。Java 8通過引入lambda來解決這個問題。本教程首先介紹lambda的語言特性,然後詳細的介紹了如何使用lambda表達式並根據目標類型進行函數式編程。
  • Java 8裡面 lambda 的最佳實踐
    為了編寫並行處理這些大數據的類庫,需要在語言層面上修改現有的Java:增加lambda表達式。當然,這樣做是有代價的,程式設計師必須學習如何編寫和閱讀包含lambda表達式的代碼,但是,這不是一樁賠本的買賣。與手寫一大段複雜的、線程安全 的代碼相比,學習一點新語法和一些新習慣容易很多。開發企業級應用時,好的類庫和框架極大地降低了開發時間和成本,也掃清了開發易用且高效的類庫的障礙。
  • Lambda 表達式的 10 個示例
    作為開發人員,我發現學習和掌握lambda表達式的最佳方法就是勇於嘗試,儘可能多練習流API和lambda表達式,用於對列表(Lists)和集合(Collections)數據進行提取、過濾和排序。本文分享在代碼中最有用的10個lambda表達式的使用方法,這些例子都短小精悍,將幫助你快速學會lambda表達式。
  • Java中Lambda表達式的5種不同語法
    主體,由單個表達式或語句塊組成。這使代碼更整潔,更像真正的lambda表達式。3. Lambda表達式中的多行代碼如果代碼不能一行編寫,則可以將其括在{}中。現在,該代碼應明確包含return語句。, is, a]4.推斷類型的單參數當可以推斷類型時,對於單參數lambda表達式可以省略括號。
  • 【外文翻譯】外國友人寫得很不錯的Java Lambda表達式入門教程,我終於翻譯好給大家啦!!!
    Lambda 表達式是 Java SE 8 中一個重要的新特性。lambda 表達式允許你通過表達式來代替功能接口。lambda 表達式就和方法一樣, 它提供了一個正常的參數列表和一個使用這些參數的主體 (body, 可以是一個表達式或一個代碼塊)。Lambda 表達式還增強了集合庫。
  • Java 8 Lambda 的實現原理及源碼剖析
    為了支持函數式編程,Java 8引入了Lambda表達式,那麼在Java 8中到底是如何實現Lambda表達式的呢?
  • JAVA8 新特性詳解
    但是我們看到了defaultMethod,說明實現類可以直接調用接口中的default方法;那麼如何使用接口中的static方法呢???函數式接口可以使用Lambda表達式,lambda表達式會被匹配到這個抽象方法上我們可以將lambda表達式當作任意只包含一個抽象方法的接口類型,確保你的接口一定達到這個要求,你只需要給你的接口添加 @FunctionalInterface 註解,編譯器如果發現你標註了這個註解的接口有多於一個抽象方法的時候會報錯的
  • Lambda 表達式,簡潔優雅就是生產力!
    比如,我想把右邊那塊代碼,賦給一個叫做aBlockOfCode的Java變量:在Java 8之前,這個是做不到的。但是Java 8問世之後,利用Lambda特性,就可以做到了。當然,這個並不是一個很簡潔的寫法。
  • 你真的了解java的lambda嗎?- java lambda用法與源碼分析
    我們注意到Runnable有個註解 @FunctionalInterface,它是jdk8才引入,它的含義是函數接口。它是lambda表達式的協議註解,這個註解非常重要,後面做源碼分析會專門分析它的官方注釋,到時候一目了然。/* @jls 4.3.2.
  • 【C++基礎】C++11 lambda 表達式解析
    C++11 新增了很多特性,lambda 表達式是其中之一,如果你想了解的 C++11 完整特性,建議去看看C++標準。本文作為 5 月的最後一篇博客,將介紹 C++11 的 lambda 表達式。很多語言都提供了 lambda 表達式,如 Python,Java 8。
  • Java8中lambda表達式的語法,別人都會的,你還不會嗎?「一」
    lambda表達式JSR-335首次定義了在Java中使用lambda表達式的基本規範,當前的實現就是針對JSR-335規範的。 lambda表達式是一種緊湊的、傳遞行為的方式。Lambda表達式本質上是為了解決方便的將代碼作為數據傳遞的難題。
  • Java Lambda 使用教程
    三、高級集合類及收集器3.1 轉換成值3.2 轉換成塊3.3 數據分組3.4 字符串拼接四、總結一、引言java8最大的特性就是引入Lambda表達式,即函數式編程,可以將行為進行傳遞。總結就是:使用不可變值與函數,函數對不可變值進行處理,映射成另一個值。二、java重要的函數式接口1、什麼是函數式接口函數接口是只有一個抽象方法的接口,用作 Lambda 表達式的類型。使用@FunctionalInterface註解修飾的類,編譯器會檢測該類是否只有一個抽象方法或接口,否則,會報錯。可以有多個默認方法,靜態方法。