別翻了,Lambda 表達式入門,看這篇就夠了

今天分享的主題是《Lambda 表達式入門》,這也是之前一些讀者留言強烈要求我寫一寫的,不好意思,讓你們久等了,現在來滿足你們,爲時不晚吧?

01、初識 Lambda

Lambda 表達式描述了一個代碼塊(或者叫匿名方法),可以將其作爲參數傳遞給構造方法或者普通方法以便後續執行。考慮下面這段代碼:

() -> System.out.println("沉默王二")

來從左到右解釋一下,() 爲 Lambda 表達式的參數列表(本例中沒有參數),-> 標識這串代碼爲 Lambda 表達式(也就是說,看到 -> 就知道這是 Lambda),System.out.println("沉默王二") 爲要執行的代碼,即將“沉默王二”打印到標準輸出流。

有點 Java 基礎的同學應該不會對 Runnable 接口感到陌生,這是多線程的一個基礎接口,它的定義如下:

@FunctionalInterface
public interface Runnable
{
   public abstract void run();
}

Runnable 接口非常簡單,僅有一個抽象方法 run();細心的同學會發現一個陌生的註解 @FunctionalInterface,這個註解是什麼意思呢?

我看了它的源碼,裏面有這樣一段註釋:

Note that instances of functional interfaces can be created with lambda expressions, method references, or constructor references.

大致的意思就是說,通過 @FunctionalInterface 標記的接口可以通過 Lambda 表達式創建實例。具體怎麼表現呢?

原來我們創建一個線程並啓動它是這樣的:

public class LamadaTest {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("沉默王二");
            }
        }).start();
    }
}

通過 Lambda 表達式呢?只需要下面這樣:

public class LamadaTest {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("沉默王二")).start();
    }
}

是不是很妙!比起匿名內部類,Lambda 表達式不僅易於理解,更大大簡化了必須編寫的代碼數量。

02、Lambda 語法

每個 Lambda 表達式都遵循以下法則:

( parameter-list ) -> { expression-or-statements }

() 中的 parameter-list 是以逗號分隔的參數。你可以指定參數的類型,也可以不指定(編譯器會根據上下文進行推斷)。Java 11 後,還可以使用 var 關鍵字作爲參數類型,有點 JavaScript 的味道。

-> 相當於 Lambda 的標識符,就好像見到聖旨就見到了皇上。

{} 中的 expression-or-statements 爲 Lambda 的主體,可以是一行語句,也可以多行。

可以通過 Lambda 表達式幹很多事情,比如說

1)爲變量賦值,示例如下:

Runnable r = () -> { System.out.println("沉默王二"); };
r.run();

2)作爲 return 結果,示例如下:

static FileFilter getFilter(String ext)
{
    return (pathname) -> pathname.toString().endsWith(ext);
}

3)作爲數組元素,示例如下:

final PathMatcher matchers[] =
{
        (path) -> path.toString().endsWith("txt"),
        (path) -> path.toString().endsWith("java")
};

4)作爲普通方法或者構造方法的參數,示例如下:

new Thread(() -> System.out.println("沉默王二")).start();

需要注意 Lambda 表達式的作用域範圍。

public static void main(String[] args) {
<span class="token keyword">int</span> limit <span class="token operator">=</span> <span class="token number">10</span><span class="token punctuation">;</span>
Runnable r <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    <span class="token keyword">int</span> limit <span class="token operator">=</span> <span class="token number">5</span><span class="token punctuation">;</span>
    <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">int</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator">&lt;</span> limit<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span>
        System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>i<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>

}

上面這段代碼在編譯的時候會提示錯誤:變量 limit 已經定義過了。

和匿名內部類一樣,不要在 Lambda 表達式主體內對方法內的局部變量進行修改,否則編譯也不會通過:Lambda 表達式中使用的變量必須是 final 的。


這個問題發生的原因是因爲 Java 規範中是這樣規定的:

Any local variable, formal parameter, or exception parameter used but not declared in a lambda expression
must either be declared final or be effectively final (§4.12.4),
or a compile-time error occurs where the use is attempted.

大致的意思就是說,Lambda 表達式中要用到的,但又未在 Lambda 表達式中聲明的變量,必須聲明爲 final 或者是 effectively final,否則就會出現編譯錯誤。

關於 final 和 effectively final 的區別,可能有些小夥伴不太清楚,這裏多說兩句。

final int a;
a = 1;
// a = 2;
// 由於 a 是 final 的,所以不能被重新賦值

int b;
b = 1;
// b 此後再未更改
// b 就是 effectively final

int c;
c = 1;
// c 先被賦值爲 1,隨後又被重新賦值爲 2
c = 2;
// c 就不是 effectively final

明白了 final 和 effectively final 的區別後,我們瞭解到,如果把 limit 定義爲 final,那就無法在 Lambda 表達式中修改變量的值。那有什麼好的解決辦法呢?既能讓編譯器不發出警告,又能修改變量的值。

思前想後,試來試去,我終於找到了 3 個可行的解決方案:

1)把 limit 變量聲明爲 static。

2)把 limit 變量聲明爲 AtomicInteger。

3)使用數組。

下面我們來詳細地一一介紹下。

01)把 limit 變量聲明爲 static

要想把 limit 變量聲明爲 static,就必須將 limit 變量放在 main() 方法外部,因爲 main() 方法本身是 static 的。完整的代碼示例如下所示。

public class ModifyVariable2StaticInsideLambda {
    static int limit = 10;
    public static void main(String[] args) {
        Runnable r = () -> {
            limit = 5;
            for (int i = 0; i < limit; i++) {
                System.out.println(i);
            }
        };
        new Thread(r).start();
    }
}

來看一下程序輸出的結果:

0
1
2
3
4

OK,該方案是可行的。

02)把 limit 變量聲明爲 AtomicInteger

AtomicInteger 可以確保 int 值的修改是原子性的,可以使用 set() 方法設置一個新的 int 值,get() 方法獲取當前的 int 值。

public class ModifyVariable2AtomicInsideLambda {
    public static void main(String[] args) {
        final AtomicInteger limit = new AtomicInteger(10);
        Runnable r = () -> {
            limit.set(5);
            for (int i = 0; i < limit.get(); i++) {
                System.out.println(i);
            }
        };
        new Thread(r).start();
    }
}

來看一下程序輸出的結果:

0
1
2
3
4

OK,該方案也是可行的。

03)使用數組

使用數組的方式略帶一些欺騙的性質,在聲明數組的時候設置爲 final,但更改 int 的值時卻修改的是數組的一個元素。

public class ModifyVariable2ArrayInsideLambda {
    public static void main(String[] args) {
        final int [] limits = {10};
        Runnable r = () -> {
            limits[0] = 5;
            for (int i = 0; i < limits[0]; i++) {
                System.out.println(i);
            }
        };
        new Thread(r).start();
    }
}

來看一下程序輸出的結果:

0
1
2
3
4

OK,該方案也是可行的。

03、Lambda 和 this 關鍵字

Lambda 表達式並不會引入新的作用域,這一點和匿名內部類是不同的。也就是說,Lambda 表達式主體內使用的 this 關鍵字和其所在的類實例相同。

來看下面這個示例。

public class LamadaTest {
    public static void main(String[] args) {
        new LamadaTest().work();
    }
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">work</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">printf</span><span class="token punctuation">(</span><span class="token string">"this = %s%n"</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    Runnable r <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Runnable</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token annotation punctuation">@Override</span>
        <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">run</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">printf</span><span class="token punctuation">(</span><span class="token string">"this = %s%n"</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span><span class="token punctuation">;</span>
    <span class="token keyword">new</span> <span class="token class-name">Thread</span><span class="token punctuation">(</span>r<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">start</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">new</span> <span class="token class-name">Thread</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">&gt;</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">printf</span><span class="token punctuation">(</span><span class="token string">"this = %s%n"</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">start</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

}

Tips:%s 代表當前位置輸出字符串,%n 代表換行符,也可以使用 \n 代替,但 %n 是跨平臺的。

work() 方法中的代碼可以分爲 3 個部分:

1)單獨的 this 關鍵字

System.out.printf("this = %s%n", this);
  • 1

其中 this 爲 main() 方法中通過 new 關鍵字創建的 LamadaTest 對象——new LamadaTest()

2)匿名內部類中的 this 關鍵字

Runnable r = new Runnable()
{
    @Override
    public void run()
    {
        System.out.printf("this = %s%n", this);
    }
};

其中 this 爲 work() 方法中通過 new 關鍵字創建的 Runnable 對象——new Runnable(){...}

3)Lambda 表達式中的 this 關鍵字

其中 this 關鍵字和 1)中的相同。

我們來看一下程序的輸出結果:

this = com.cmower.java_demo.journal.LamadaTest@3feba861
this = com.cmower.java_demo.journal.LamadaTest$1@64f033cb
this = com.cmower.java_demo.journal.LamadaTest@3feba861

符合我們分析的預期。

04、最後

儘管 Lambda 表達式在簡化 Java 編程方面做了很多令人驚訝的努力,但在某些情況下,不當的使用仍然會導致不必要的混亂,大傢伙慎用。

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