lambda函数if_lambda表达式速度如何呢?看完这篇文章你就明白了
雖然Java 8已經出了好幾年了,但是很多朋友可能對于其中的一些特性還是不太了解。甚至對lambda表達式這個特性可能會產生誤解,誤認為lambda表達式會影響程序的速度。其中也不乏很多誤人子弟的自媒體傳播這些錯誤的觀點。
今天我看到一篇自媒體推送的文章,號稱用Java字節碼分析為什么lambda表達式速度慢,但是其中漏洞百出,搞得我忍不住寫了這么一篇文章,為一些受到誤導的朋友糾正一個概念:lambda表達式和普通的循環一樣,不會影響到程序速度,大家可以放心使用。
因為頭條壓縮圖片的緣故,所以對于小段代碼,我用高亮代碼圖片的形式貼出。對于大段代碼,直接貼代碼,可能會影響大家的閱讀體驗。也希望頭條能夠允許上傳高清圖片,讓大家的閱讀體驗更好一下。
lambda表達式是什么
可能還有一些朋友對lambda表達式還是不太清楚,所以我先介紹一下lambda表達式的概念。簡單來說lambda表達式就是匿名函數,在一些支持匿名函數的語言中,用不用lambda表達式其實不是那么重要。但是因為Java不支持匿名函數,所以lambda表達式可以極大的簡化這些場合的代碼。
先來看看一個例子。假如我們需要在一個新線程中運行代碼,可能需要創建一個新的Runnable對象。此處使用了Java的一項特性匿名內部類,創建了一個新的臨時的Runnable對象。但是代碼如你所見非常難看,大段的縮進和方括號,非常影響閱讀。
如果換成了lambda表達式的實現,如你所見,代碼非常干凈整潔。
這種形如(a,b)->{ ..... }的表達式就是lambda表達式。上面已經提到過了,lambda表達式其實就是匿名函數,箭頭前面的括號內部的就是函數的參數列表;箭頭后面的括號內部的就是方法體,假如方法體只有一行語句或者表達式,方法體的括號可以省略。
lambda表達式參數的類型也不需要寫明,編譯器會自動從前面的類型中推斷。在上面的例子中,因為Runnable中的run函數沒有參數,所以lambda表達式自然也不需要參數。你可能會想到,假如類型中有多個函數怎么辦呢?這時候編譯器無法推斷,程序就會報錯。這也是Java lambda表達式的一個限制,前面的接口類型中只能有一個函數聲明。
很多古板的程序員不喜歡這個特性,認為它會影響到程序的可讀性。但是實際情況恰恰相反,合理的利用lambda表達式,不僅不會污染代碼的可讀性,反而會大大加強可讀性。lambda表達式這個特性,已經被現在很多新的編程語言吸收,足見其流行程度。
錯誤的測試方法
很多朋友可能對lambda表達式的運行速度產生疑問,會不會用了這種寫法,程序的運行速度就會變慢呢?這種擔心也是完全多余的,Java作為一門經典的企業級應用開發語言,Oracle對每個新添加的特性都是小心翼翼的。既然這個特性被添加到Java語言中,那么足以說明Oracle對其進行了深刻的優化,運行速度絕對是有保證的,就算比普通循環慢一點,也不會慢到哪里去。
可能有些人用了錯誤的測試方法對lambda表達式進行了測試,發現速度不如普通的for循環,然后就得出結論:lambda表達式運行速度慢。這種測試是完全不負責任的。下面的代碼就是一種錯誤的測試方法,測試結果:lambda表達式用時150毫秒,而普通循環用時7毫秒。因此得出結論:lambda表達式慢。大家可以看看代碼,然后想想問題在哪里。
public class LambdaTest { public static int N = 1_0000_0000; static List list = IntStream.range(0, N).boxed().collect(Collectors.toList()); public static void main(String[] args) { long start, end; start = System.currentTimeMillis(); lambdaTest(); end = System.currentTimeMillis(); System.out.println("lambda:" + (end - start)); start = System.currentTimeMillis(); loopTest(); end = System.currentTimeMillis(); System.out.println("loop:" + (end - start)); } static void lambdaTest() { list.forEach(i -> { }); } static void loopTest() { for (int i = 0; i < list.size(); i++) { } }}好了不賣關子了,直接說結論吧。上面測試方法的問題在于,兩種測試方法實際上根本不對等。lambda表達式的測試中,雖然方法體是空的,但是程序執行的時候,仍然會取出每一個元素,然后再應用空的方法。而循環測試中,真的只是執行了一個空循環,什么也沒干。因此這種方法測出來的結論,完全不能證明lambda表達式比空循環慢。
公平的測試方法應該是怎么樣的呢?對于循環,一樣要加上取元素和應用空方法的操作。為此在空循環中增加了一部分代碼。這樣測出來的結果,lambda表達式和普通循環一樣都是150毫秒左右,存在幾毫秒的誤差。這次的結果可以反映真實情況了,那就是兩者沒有什么速度差別。大家可以自己運行代碼試試。
更加實際的測試
不管怎么說,用空的方法來測試lambda表達式和普通循環并不具有實際意義。所以我換了一種更加實際的方法,來看看lambda相較于普通的循環有沒有優勢所在。
首先準備一個用戶類,這里用到了lombok自動生成各種工具方法,為我們節約時間。
然后準備一個隨機類,準備用來生成10萬個隨機用戶,來進行下一步的操作。
接下來就是測試代碼了。測試代碼其實也很簡單,隨機生成一千萬個用戶,然后進行簡單的篩選操作,選出來所有ID大于1000且為偶數,用戶名以字母a開頭的用戶。兩種測試結果輸出各自的篩選結果數量,以保證結果是相同的。因為這次的測試比較復雜,所以可以看出實際的差異。在我的電腦上,lambda表達式耗時100毫秒左右,而循環耗時80毫秒左右。可見lambda表達式雖然比循環慢一點,但是差距很小,在千萬次循環的級別僅差幾十毫秒,對程序的運行基本沒有什么影響。
public class LambdaTest { public static int N = 1000_0000; static List list; public static void main(String[] args) { init(); long start, end; start = System.currentTimeMillis(); lambdaTest(); end = System.currentTimeMillis(); System.out.println("lambda:" + (end - start)); start = System.currentTimeMillis(); loopTest(); end = System.currentTimeMillis(); System.out.println("loop:" + (end - start)); } static void init() { list = new ArrayList<>(); for (int i = 0; i < N; i++) { list.add(new User(MyRandom.randomId(), MyRandom.randomUsername())); } } static void lambdaTest() { List r = list.stream() .filter(e -> e.getName().startsWith("a")) .filter(e -> e.getId() % 2 == 0) .filter(e -> e.getId() > 1000) .collect(Collectors.toList()); System.out.println(r.size()); } static void loopTest() { List r = new ArrayList<>(); for (User user : list) { if (user.getName().startsWith("a") && user.getId() % 2 == 0 && user.getId() > 1000) { r.add(user); } } System.out.println(r.size()); }}這次的測試算是一個比較實際的測試了,生成一千萬個用戶并對其屬性進行檢查,過濾出符合條件的用戶。測試的數量是一千萬,但是測試結果相差并不大??梢娖鋵峫ambda表達式并不怎么影響程序的運行速度。值得注意的是,這個測試數據完全是保存在內存上的,而一般情況下數據都是從數據庫中加載出來的。這時候程序的瓶頸在數據庫的IO上,就算程序本身速度相差幾十毫秒,相較于數據庫的延遲完全可以忽略不計。
我們的原則是不進行過早的優化。寫程序的時候,該怎么寫就怎么寫,lambda這種好用的新特性,該用的時候就應該用,不要害怕它影響性能。等到程序寫完,需要優化的時候,老老實實的跑profile,查看程序的瓶頸究竟在哪里。一般情況下程序問題都在數據庫IO、算法不夠高效或者是內存泄露上,我還真沒聽說過哪個程序寫的非常完美,就是被lambda表達式的速度拖后腿的。實際上,雖然很多程序員都擔心lambda表達式的速度,但是他們的程序完全優秀到需要扣lambda表達式細節的這種程度。
反過來說適當的時候應用這些新特性,反而會增加代碼的可讀性。就拿上面這個例子來說,通過三次filter方法過濾程序,最后用collect方法得到結果,這種流式函數調用不僅非常簡單易讀,而且十分優雅。反觀循環版本中的查找操作,只能通過if判斷簡單粗暴的進行。這還是一個簡單的例子,假如查找操作比較復雜,帶了十幾個查詢條件的話,那么循環版本的代碼就會變成可讀性的災難。
這里還有一個細節值得注意。為了最高效的運行,循環版本的代碼只能在一個if中不斷的增加判斷條件。而lambda表達式版本則是流式調用了三次filter語句,但是它們的運行結果相差不大。相信你應該也猜到原因了:lambda表達式和流類庫內部做了特殊的優化,就算是多個過濾條件,也會保證僅僅循環一次。因此放心大膽的使用lambda表達式吧!它是編寫代碼的利器!
lambda表達式,更加強大
寫到這里,本文的內容應該是差不多了。但是我猜很多朋友看了以后,會說“你說了這么多,lambda表達式不還是比循環慢嘛。說來說去,我還是要繼續用循環”。在這里我想說明一下,我的觀點是:lambda表達式雖然比循環慢那么一點點,但是帶來的便利性和優化空間,遠遠不是普通循環可以比擬的。
上面的例子用了一千萬次的循環,才得到了幾十毫秒的差距。而實際情況中,幾千次或者幾萬次的循環,差距便會忽略不計。而且如果加上數據庫等外部數據源的讀寫延遲,程序的這點運行速度完全就不值一提了。所有擔心lambda表達式的朋友基本都是杞人憂天。而lambda表達式帶來的方便確實實實在在的。更重要的是,普通循環的優化非常困難,基本要重寫整個代碼,在這之中很容易發生錯誤。而lambda表達式的優化則簡單許多。
上面的例子恰好是一個適合并行化的例子,優化方法很簡單,多加一行parallel()方法調用即可。并行化是另外一個非常復雜的主題,但是在這個例子中,第一數據量大(一千萬之多),第二數據易于分割和和合并(ArrayList可以用下標直接定位中間的元素),第三操作都是只讀的(不會影響到數據集本身),所以正好適合并行化。并行化之后,lambda表達式的運行速度已經和循環相差無幾了(僅差幾毫秒左右)。而普通代碼的并行化,我想這就不是一般程序員可以輕松寫出來的東西了。
好了,本文終于寫完了,其實本來準備反駁一下錯誤觀點就結束的,結果不知不覺寫了這么多。如果大家覺得本文不錯的話,歡迎點贊、評論、轉發,創作不易,還請大家多多支持!在此先謝謝各位了。
總結
以上是生活随笔為你收集整理的lambda函数if_lambda表达式速度如何呢?看完这篇文章你就明白了的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 驱动开发起步
- 下一篇: 为什么我们的软件不及印度