【修煉內功】[Java8] Lambda表達式裏的"陷阱"

本文已收錄 【修煉內功】躍遷之路

clipboard.png

Lambdab表達式帶來的好處就再也不作過多的介紹了,這裏重點介紹幾點,在使用Lambda表達式過程當中可能遇到的"陷阱"html

0x00 Effectively Final

在使用Lambda表達式的過程當中,常常會遇到以下的問題java

labmda1.png

圖中的sayWords爲何必定要是final類型,effectively final又是什麼?git

但,若是改成以下,貌似問題又解決了github

labmda2.png

彷佛,只要對sayWords不作變更就能夠express

若是將sayWords從方法體的變量提到類的屬性中,狀況又會有變化,即便對sayWords有更改,也會編譯經過編程

labmda3.png

難道,就是由於局部變量和類屬性的區別?segmentfault

在Java 8 in Action一書中有這樣一段話oracle

You may be asking yourself why local variables have these restrictions. First, there’s a key difference in how instance and local variables are implemented behind the scenes. Instance variables are stored on the heap, whereas local variables live on the stack. If a lambda could access the local variable directly and the lambda were used in a thread, then the thread using the lambda could try to access the variable after the thread that allocated the variable had deallocated it. Hence, Java implements access to a free local variable as access to a copy of it rather than access to the original variable. This makes no difference if the local variable is assigned to only once—hence the restriction. Second, this restriction also discourages typical imperative programming patterns (which, as we explain in later chapters, prevent easy parallelization) that mutate an outer variable.

首先,要理解Local VariablesInstance Variables在JVM內存中的區別app

Local VariablesThread存儲在Stack棧內存中,而Instance Variables則隨Instance存儲在Heap堆內存中編程語言

  • Local Variables的回收取決於變量的做用域,程序的運行一旦超出變量的做用域,該內存空間便被馬上回收另做他用
  • Instance Variables的回收取決於引用數,當再沒有引用的時候,便會在一個"合適"的時間被JVM垃圾回收器回收

試想,若是Lambda表達式引用了局部變量,而且該Lambda表達式是在另外一個線程中執行,那在某種狀況下該線程則會在該局部變量被收回後(函數執行完畢,超出變量做用域)被使用,顯然這樣是不正確的;但若是Lambda表達式引用了類變量,則該類(屬性)會增長一個引用數,在線程執行完以前,引用數不會歸爲零,也不會觸發JVM對其的回收操做

但這解釋不了圖2的狀況,一樣是局部變量,只是未對sayWords作改動,也是能夠經過編譯的,這裏便要介紹effectively final

Baeldung大神的博文中有這樣一段話

Accessing a non-final variable inside lambda expressions will cause the compile-time error. But it doesn’t mean that you should mark every target variable as final.

According to the 「effectively final」 concept, a compiler treats every variable as final, as long as it is assigned only once.

It is safe to use such variables inside lambdas because the compiler will control their state and trigger a compile-time error immediately after any attempt to change them.

其中提到了 assigned only once,字面理解即是隻賦值了一次,對於這種狀況,編譯器便會 treats variable as final,對於只賦值一次的局部變量,編譯器會將其認定爲effectively final,其實對於effectively final的局部變量,Lambda表達式中引用的是其副本,而該副本的是不會發生變化的,其效果就和final是一致的

Effectively Final更深刻的解釋,能夠參考Why Do Local Variables Used in Lambdas Have to Be Final or Effectively Final?

小結:

  1. Lambda表達式中能夠直接引用Instance Variables
  2. Lambda表達式中引用Local Variables,必須爲finaleffectively final( assigned only once)

0x01 Throwing Exception

Java的異常分爲兩種,受檢異常(Checked Exception)和非受檢異常(Unchecked Exception)

Checked Exception, the exceptions that are checked at compile time. If some code within a method throws a checked exception, then the method must either handle the exception or it must specify the exception using throws keyword.

Unchecked Exception, the exceptions that are not checked at compiled time. It is up to the programmers to be civilized, and specify or catch the exceptions.

簡單的講,受檢異常必須使用try…catch進行捕獲處理,或者使用throws語句代表該方法可能拋出受檢異常,由調用方進行捕獲處理,而非受檢異常則不用。受檢異常的處理是強制的,在編譯時檢測。

lambda-exption-1.jpg

在Lambda表達式內部拋出異常,咱們該如何處理?

Unchecked Exception

首先,看一段示例

public class Exceptional {
    public static void main(String[] args) {
       Stream.of(3, 8, 5, 6, 0, 2, 4).forEach(lambdaWrapper(i -> System.out.println(15 / i)));
    }

    private static Consumer<Integer> lambdaWrapper(IntConsumer consumer) {
        return i -> consumer.accept(i);
    }
}

該段代碼是能夠編譯經過的,但運行的結果是

> 5
> 1
> 3
> 2
> Exception in thread "main" java.lang.ArithmeticException: / by zero
      at Exceptional.lambda$main$0(Exceptional.java:13)
      at Exceptional.lambda$lambdaWrapper$1(Exceptional.java:17)
      at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
      at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
      at Exceptional.main(Exceptional.java:13)

因爲Lambda內部計算時,因爲除數爲零拋出了ArithmeticException異常,致使流程中斷,爲了解決此問題能夠在lambdaWrapper函數中加入try…catch

private static Consumer<Integer> lambdaWrapper(IntConsumer consumer) {
    return i -> {
        try {
            consumer.accept(i);
        } catch (ArithmeticException e) {
            System.err.println("Arithmetic Exception occurred : " + e.getMessage());
        }
    };
}

再次運行

> 5
> 1
> 3
> 2
> Arithmetic Exception occurred : / by zero
> 7
> 3

對於Lambda內部非受檢異常,只須要使用try…catch便可,無需作過多的處理

Checked Exception

一樣,一段示例

public class Exceptional {
    public static void main(String[] args) {
        Stream.of(3, 8, 5, 6, 0, 2, 4).forEach(lambdaWrapper(i -> writeToFile(i)));
    }

    private static Consumer<Integer> lambdaWrapper(IntConsumer consumer) {
        return i -> consumer.accept(i);
    }

    private static void writeToFile(int integer) throws IOException {
        // logic to write to file which throws IOException
    }
}

因爲IOException爲受檢異常,該段將會程序編譯失敗

lambda-exption-2.jpg

按照Unchecked Exception一節中的思路,咱們在lambdaWrapper中使用try…catch處理異常

private static Consumer<Integer> lambdaWrapper(IntConsumer consumer) {
    return i -> {
        try {
            consumer.accept(i);
        } catch (IOException e) {
            System.err.println("IOException Exception occurred : " + e.getMessage());
        }
    };
}

但出乎意料,程序依然編譯失敗

lambda-exption-4.jpg

查看IntConsumer定義,其並未對接口accept聲明異常

@FunctionalInterface
public interface IntConsumer {
    /**
     * Performs this operation on the given argument.
     *
     * @param value the input argument
     */
    void accept(int value);
}

爲了解決此問題,咱們能夠本身定義一個聲明瞭異常的ThrowingIntConsumer

@FunctionalInterface
public interface ThrowingIntConsumer<E extends Exception> {
    /**
     * Performs this operation on the given argument.
     *
     * @param value the input argument
     * @throws E
     */
    void accept(int value) throws E;
}

改造代碼以下

private static Consumer<Integer> lambdaWrapper(ThrowingIntConsumer<IOException> consumer) {
    return i -> {
        try {
            consumer.accept(i);
        } catch (IOException e) {
            System.err.println("IOException Exception occurred : " + e.getMessage());
        }
    };
}

但,若是咱們但願在出現異常的時候終止流程,而不是繼續運行,能夠在獲取到受檢異常後拋出非受檢異常

private static Consumer<Integer> lambdaWrapper(ThrowingIntConsumer<IOException> consumer) {
    return i -> {
        try {
            consumer.accept(i);
        } catch (IOException e) {
            throw new RuntimeException(e.getMessage(), e.getCause());
        }
    };
}

全部使用了ThrowingIntConsumer的地方都須要寫一遍try…catch,有沒有優雅的方式?或許能夠從ThrowingIntConsumer下手

@FunctionalInterface
public interface ThrowingIntConsumer<E extends Exception> {
    /**
     * Performs this operation on the given argument.
     *
     * @param value the input argument
     * @throws E
     */
    void accept(int value) throws E;
    
    /**
     * @return a IntConsumer instance which wraps thrown checked exception instance into a RuntimeException
     */
    default IntConsumer uncheck() {
        return i -> {
            try {
                accept(i);
            } catch (final E e) {
                throw new RuntimeException(e.getMessage(), e.getCause());
            }
        };
    }
}

咱們在ThrowingIntConsumer中定義了一個默認函數uncheck,其內部會自動調用Lambda表達式,並在捕獲到異常後將其轉爲非受檢異常並從新拋出

此時,咱們即可以將lambdaWrapper函數優化以下

private static Consumer<Integer> lambdaWrapper(ThrowingIntConsumer<IOException> consumer) {
    return i -> consumer.accept(i).uncheck();
}

unCheck會將IOException異常轉爲RuntimeException拋出

有沒有更優雅一些的方式?因爲篇幅緣由再也不過多介紹,感興趣的能夠參考 throwing-functionVavr

小結:

  1. Lambda表達式拋出非受檢異常,能夠在Lambda表達式內部或外部直接使用try…catch捕獲處理
  2. Lambda表達式拋出受檢異常,能夠在Lambda表達式內部直接使用try…catch捕獲處理,若是須要在Lambda表達式外部捕獲處理,必須在FunctionalInterface接口上顯式聲明throws

0x02 this pointer

Java中,類(匿名類)中均可以使用this,Lambda表達式也不例外

public class ThisPointer {
    public static void main(String[] args) {
        ThisPointer thisPointer = new ThisPointer("manerfan");
        new Thread(thisPointer.getPrinter()).start();
    }

    private String name;

    @Getter
    private Runnable printer;

    public ThisPointer(String name) {
        this.name = name;
        this.printer = () -> System.out.println(this);
    }

    @Override
    public String toString() {
        return "hello " + name;
    }
}

ThisPointer類的構造函數中,使用Lambda表達式定義了printer屬性,並重寫了類的toString方法

運行後結果

> hello manerfan

ThisPointer類的構造函數中,將printer屬性的定義改成匿名類

public class ThisPointer {
    public static void main(String[] args) {
        ThisPointer thisPointer = new ThisPointer("manerfan");
        new Thread(thisPointer.getPrinter()).start();
    }

    private String name;

    @Getter
    private Runnable printer;

    public ThisPointer(String name) {
        this.name = name;
        this.printer = new Runnable() {
            @Override
            public void run() {
                System.out.println(this);
            }
        };
    }

    @Override
    public String toString() {
        return "hello " + name;
    }
}

從新運行後結果

> ThisPointer$1@782b1823

可見,Lambda表達式及匿名類中的this指向的並非同一內存地址

這裏咱們須要理解,在Lambda表達式中它在詞法上綁定到周圍的類 (定義該Lambda表達式時所處的類),而在匿名類中它在詞法上綁定到匿名類

Java語言規範在15.27.2描述了這種行爲

Unlike code appearing in anonymous class declarations, the meaning of names and the this and super keywords appearing in a lambda body, along with the accessibility of referenced declarations, are the same as in the surrounding context (except that lambda parameters introduce new names).

The transparency of this (both explicit and implicit) in the body of a lambda expression – that is, treating it the same as in the surrounding context – allows more flexibility for implementations, and prevents the meaning of unqualified names in the body from being dependent on overload resolution.

Practically speaking, it is unusual for a lambda expression to need to talk about itself (either to call itself recursively or to invoke its other methods), while it is more common to want to use names to refer to things in the enclosing class that would otherwise be shadowed (this, toString()). If it is necessary for a lambda expression to refer to itself (as if via this), a method reference or an anonymous inner class should be used instead.

那,如何在匿名類中如何作到Lambda表達式的效果,獲取到周圍類this呢?這時候就必須使用qualified this了,以下

public class ThisPointer {
    public static void main(String[] args) {
        ThisPointer thisPointer = new ThisPointer("manerfan");
        new Thread(thisPointer.getPrinter()).start();
    }

    private String name;

    @Getter
    private Runnable printer;

    public ThisPointer(String name) {
        this.name = name;
        this.printer = new Runnable() {
            @Override
            public void run() {
                System.out.println(ThisPointer.this);
            }
        };
    }

    @Override
    public String toString() {
        return "hello " + name;
    }
}

運行結果以下

> hello manerfan

小結:

  1. Lambda表達式中,this在詞法上綁定到周圍的類 (定義該Lambda表達式時所處的類)
  2. 匿名類中,this在詞法上綁定到匿名類
  3. 匿名類中,若是須要引用周圍類this,須要使用qualified this

0x03 其餘

在排查問題的時候,查看異常棧是必不可少的一種方法,其會記錄異常出現的詳細記錄,包括類名、方法名行號等等信息

那,Lambda表達式中的異常棧信息是如何的?

public class ExceptionStack {
    public static void main(String[] args) {
        new ExceptionStack().run();
    }

    private Function<Integer, Integer> divBy100 = divBy(100);

    void run() {
        Stream.of(1, 7, 0, 6).filter(this::isEven).map(this::div).forEach(System.out::println);
    }

    boolean isEven(int i) {
        return 0 == i / 2;
    }

    int div(int i) {
        return divBy100.apply(i);
    }

    Function<Integer, Integer> divBy(int div) {
        return i -> div / i;
    }
}

這裏咱們故意製造了一個ArithmeticException,而且增長了異常的棧深,運行後的異常信息以下

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at ExceptionStack.lambda$divBy$0(ExceptionStack.java:30)
    at ExceptionStack.div(ExceptionStack.java:26)
    at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
    at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)
    at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
    at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
    at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
    at ExceptionStack.run(ExceptionStack.java:18)
    at ExceptionStack.main(ExceptionStack.java:12)

異常信息中的ExceptionStack.lambda$divBy$0 ReferencePipeline$3$1.accept等並不能讓咱們很快地瞭解,具體是類中哪一個方法出現了問題,此類問題在不少編程語言中都存在,也但願JVM有朝一日能夠完全解決

關於Lambda表達式中的"陷阱"不只限於此,也但願你們可以一塊兒來討論


訂閱號

相關文章
相關標籤/搜索