在Java中應用函數式編程請小心!

jopen 8年前發布 | 14K 次閱讀 Java 函數式編程 Java開發

這并不是要對令人畏懼的函數式編程進行譴責,而是對編程中很容易發生的一些錯誤進行警醒。

高階函數 是函數式編程的關鍵,因此,談論它們會幫助你在派對上成為關注的焦點。

如果你正在寫 JavaScript ,實際上一直在做的就是高階函數。例如:

setTimeout(function() {
    alert('10 Seconds passed');
}, 10000);

上面的 setTimeout() 函數就是一個高階函數。它的參數是一個匿名函數。10秒后,它將會用這個匿名函數作為參數來調用。

我們可以編寫另一個簡單的高階函數,作為結果提供給上面的函數:

var message = function(text) {
    return function() {
        alert(text);
    }
};

setTimeout(message('10 Seconds passed'), 10000);

如果運行上面的程序,將會執行message() 函數,并返回一個匿名函數,這個匿名函數將會輸出傳遞給 message() 函數的text參數。

在函數式編程中,上面是很常見的做法。由高階函數返回的函數被調用時,將會捕捉外部作用域,并且能夠在這個作用域上進行操作。

為什么在 Java 中這種做法很危險?

出于同樣的原因。高階函數(方法)返回的函數(lambda函數)被調用的時候,會捕捉外部作用域,并且能夠在這個作用域上進行操作。

這里給出一個最簡單的例子:

class Test {
    public static void main(String[] args) {
        Runnable runnable = runnable();
        runnable.run(); // Breakpoint here
    }

    static Runnable runnable() {
        return () -> {
            System.out.println("Hello");
        };
    }
}

在上面的邏輯中,如果在 runnable.run() 方法調用處做一個斷點,可以看到在堆棧上無害的lambda實例。生成的一個簡單類,提供了函數式接口的實現:

現在,把這個例子轉換成普通的企業應用程序( 注意注解 )。為了方便博客的書寫,我們做了很大的簡化:

class Test {
    public static void main(String[] args) {
        Runnable runnable = new EnterpriseBean()
            .runnable();
        runnable.run(); // Breakpoint here
    }
}

@ImportantDeclaration
@NoMoreXML({
    @CoolNewValidationStuff("Annotations"),
    @CoolNewValidationStuff("Rock")
})
class EnterpriseBean {
    Object[] enterpriseStateObject =
        new Object[100_000_000];

    Runnable runnable() {
        return () -> {
            System.out.println("Hello");
        };
    }
}

斷點仍放在原來的地方。那么我們在堆棧中又能看到什么呢?

仍然是一個無害的lambda實例:

好吧,為了調試,我們現在添加一些日志:

class Test {
    public static void main(String[] args) {
        Runnable runnable = new EnterpriseBean()
            .runnable();
        runnable.run(); // Breakpoint here
    }
}

@ImportantDeclaration
@NoMoreXML({
    @CoolNewValidationStuff("Annotations"),
    @CoolNewValidationStuff("Rock")
})
class EnterpriseBean {
    Object[] enterpriseStateObject =
        new Object[100_000_000];

    Runnable runnable() {
        return () -> {
            // Some harmless debugging here
            System.out.println("Hello from: " + this);
        };
    }
}

哦哦!

意外地,一個毫無影響的 this 引用強迫Java 編譯器封裝返回的Runnable 類中 EnterpriseBean 的外部類實例:

而且,同時返回的還有 enterpriseStateObject 。這個對象現在不會被垃圾回收,直到調用點釋放 Runnable后才會釋放。

好吧,現在這并不是什么新鮮事,不是嗎?

確實,這并不是什么新鮮事。Java 8 沒有一流的函數,沒關系。通過虛擬的 SAM 類型支持匿名表達式的想法就相當的巧妙,因為在 Java系統中它允許對所有現有的庫更新和lambda-y-fy而并不改變它們。

而且,用一個匿名類,這整件事情就不會那么令人驚訝了。由于良好的Swing 1.0風格ActionListener等,下面的編碼風格通過內部類已經暴露了內部的狀態。

class Test {
    public static void main(String[] args) {
        Runnable runnable = new EnterpriseBean()
            .runnable();
        runnable.run();
    }
}

@ImportantDeclaration
@NoMoreXML({
    @CoolNewValidationStuff("Annotations"),
    @CoolNewValidationStuff("Rock")
})
class EnterpriseBean {
    Object[] enterpriseStateObject =
        new Object[100_000_000];

    Runnable runnable() {
        return new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello from " + EnterpriseBean.this);
            }
        };
    }
}

這里有什么心東西?匿名風格會鼓勵在 Java 中所有地方使用高階函數。一般情況下是挺好的。但是,僅僅當高階函數是一個靜態方法時,其產生的類型不會封裝任何狀態。

然而,通過上面的例子

我們可以看到,在不久的將來,當我們開始擁抱 Java 8 函數式編程風格時,我們在調試過程中將會時不時碰到一些內存泄露等問題。

所以,請謹慎使用并遵循以下規則:

(“Pure”) Higher order functions MUST be static methods in Java!

延伸閱讀

以前封閉實例(enclosing instance)會出現一些問題。可以解 了 一下可怕的 雙花括號反模式 在前20年是怎么給Java開發者帶來痛苦和磨難的。

原文鏈接: jaxenter 翻譯:ImportNew.com -santhy

譯文鏈接:[]

來自: http://www.importnew.com/17292.html

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!