大話 Java 中的 Lambda 表達式丨【奔跑吧!Java】(大話西游)

      網友投稿 1060 2025-04-03

      2014年3月18日,Oracle公司發布了Java SE 8。最近正好抽空整理了Java 8的特性如下:

      接口的默認方法

      Lambda 表達式

      函數式接口

      方法與構造函數引用

      Lambda 作用域

      訪問局部變量

      訪問對象字段與靜態變量

      訪問接口的默認方法

      Date API

      Annotation 注解

      本文將會重點講解Java 8中的Lambda表達式,其他特性將會在后續文章中講解。lambda 表達式,又被成為“閉包”或“匿名方法”。

      背景

      Java 是一門面向對象編程語言。面向對象編程語言和函數式編程語言中的基本元素(Basic Values)都可以動態封裝程序行為:面向對象編程語言使用帶有方法的對象封裝行為,函數式編程語言使用函數封裝行為。但這個相同點并不明顯,因為Java 對象往往比較“重量級”:實例化一個類型往往會涉及不同的類,并需要初始化類里的字段和方法。

      不過有些 Java 對象只是對單個函數的封裝。例如下面這個典型用例:Java API 中定義了一個接口(一般被稱為回調接口),用戶通過提供這個接口的實例來傳入指定行為。

      public interface ActionListener { void actionPerformed(ActionEvent e); }

      這里并不需要專門定義一個類來實現 ActionListener,因為它只會在調用處被使用一次。用戶一般會使用匿名類型把行為內聯(inline):

      button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ui.dazzle(e.getModifiers()); } });

      很多庫都依賴于上面的模式。對于并行 API 更是如此,因為我們需要把待執行的代碼提供給并行 API,并行編程是一個非常值得研究的領域,因為在這里摩爾定律得到了重生:盡管我們沒有更快的 CPU 核心(core),但是我們有更多的 CPU 核心。而串行 API 就只能使用有限的計算能力。

      匿名內部類

      隨著回調模式和函數式編程風格的日益流行,我們需要在Java中提供一種盡可能輕量級的將代碼封裝為數據(Model code as data)的方法。匿名內部類并不是一個好的 選擇,因為:

      語法過于冗余

      匿名類中的 this 和變量名容易使人產生誤解

      類型載入和實例創建語義不夠靈活

      無法捕獲非 final 的局部變量

      無法對控制流進行抽象

      函數式接口

      盡管匿名內部類有著種種限制和問題,但是它有一個良好的特性,它和Java類型系統結合的十分緊密:每一個函數對象都對應一個接口類型。之所以說這個特性是良好的,是因為:

      接口是 Java 類型系統的一部分

      接口天然就擁有其運行時表示(Runtime representation)

      接口可以通過 Javadoc 注釋來表達一些非正式的協定(contract),例如,通過注釋說明該操作應可交換(commutative)

      接口只有一個方法,大多數回調接口都擁有這個特征:比如 Runnable 接口和 Comparator 接口。我們把這些只擁有一個方法的接口稱為 函數式接口。(之前它們被稱為 SAM類型,即 單抽象方法類型(Single Abstract Method))

      實現函數式類型的另一種方式是引入一個全新的 結構化 函數類型,我們也稱其為“箭頭”類型。例如,一個接收 String 和 Object 并返回 int 的函數類型可以被表示為 (String, Object) -> int。我們仔細考慮了這個方式,但出于下面的原因,最終將其否定:

      它會為Java類型系統引入額外的復雜度,并帶來 結構類型(Structural Type) 和 指名類型(Nominal Type) 的混用。(Java 幾乎全部使用指名類型)

      它會導致類庫風格的分歧——一些類庫會繼續使用回調接口,而另一些類庫會使用結構化函數類型

      它的語法會變得十分笨拙,尤其在包含受檢異常(checked exception)之后

      每個函數類型很難擁有其運行時表示,這意味著開發者會受到 類型擦除(erasure) 的困擾和局限。比如說,我們無法對方法 m(T->U) 和 m(X->Y) 進行重載(Overload)

      所以我們選擇了“使用已知類型”這條路——因為現有的類庫大量使用了函數式接口,通過沿用這種模式,我們使得現有類庫能夠直接使用 lambda 表達式。例如下面是 Java SE 7 中已經存在的函數式接口:

      java.lang.Runnable java.util.concurrent.Callable java.security.PrivilegedAction java.util.Comparator java.io.FileFilter java.beans.PropertyChangeListener

      除此之外,Java SE 8中增加了一個新的包:java.util.function,它里面包含了常用的函數式接口,例如:

      Predicate——接收 T 并返回 boolean Consumer——接收 T,不返回值 Function——接收 T,返回 R Supplier——提供 T 對象(例如工廠),不接收值 UnaryOperator——接收 T 對象,返回 T BinaryOperator——接收兩個 T,返回 T

      除了上面的這些基本的函數式接口,我們還提供了一些針對原始類型(Primitive type)的特化(Specialization)函數式接口,例如 IntSupplier 和 LongBinaryOperator。(我們只為 int、long 和 double 提供了特化函數式接口,如果需要使用其它原始類型則需要進行類型轉換)同樣的我們也提供了一些針對多個參數的函數式接口,例如 BiFunction,它接收 T 對象和 U 對象,返回 R 對象。

      lambda表達式

      匿名類型最大的問題就在于其冗余的語法。有人戲稱匿名類型導致了“高度問題”。lambda表達式是匿名方法,它提供了輕量級的語法,從而解決了匿名內部類帶來的“高度問題”。

      (int x, int y) -> x + y () -> 42 (String s) -> { System.out.println(s); }

      第一個 lambda 表達式接收 x 和 y 這兩個整形參數并返回它們的和;第二個 lambda 表達式不接收參數,返回整數 ‘42’;第三個 lambda 表達式接收一個字符串并把它打印到控制臺,不返回值。

      lambda 表達式的語法由參數列表、箭頭符號 -> 和函數體組成。函數體既可以是一個表達式,也可以是一個語句塊:

      大話 Java 中的 Lambda 表達式丨【奔跑吧!JAVA】(大話西游)

      表達式:表達式會被執行然后返回執行結果。

      語句塊:語句塊中的語句會被依次執行,就像方法中的語句一樣——

      return 語句會把控制權交給匿名方法的調用者

      break 和 continue 只能在循環中使用

      如果函數體有返回值,那么函數體內部的每一條路徑都必須返回值

      表達式函數體適合小型 lambda 表達式,它消除了 return 關鍵字,使得語法更加簡潔。

      lambda 表達式也會經常出現在嵌套環境中,比如說作為方法的參數。為了使 lambda 表達式在這些場景下盡可能簡潔,我們去除了不必要的分隔符。不過在某些情況下我們也可以把它分為多行,然后用括號包起來,就像其它普通表達式一樣。

      實戰應用

      Function

      Function 接口有一個參數并且返回一個結果,并附帶了一些可以和其他函數組合的默認方法:andThen和compose。

      @Test public void testFun() { //Function 接口有一個參數并且返回一個結果 Function toInteger = (t) -> Integer.valueOf(t); System.out.println("compose: " + toInteger.andThen(a -> a + 10).compose(str -> str + "1").apply("123")); Function backToString = toInteger.andThen(String::valueOf); Function f = toInteger.compose(backToString); int str = f.apply("123"); System.out.println(str); }

      compose和andThen中定義的Function應用順序正好相反,首先應用compose中的方法,其次才會應用當前Function。

      Supplier

      Supplier 接口返回一個任意范型的值,和Function接口不同的是該接口沒有任何參數。代碼如下:

      @Test public void testSupplier() { //Supplier 接口返回一個任意范型的值,和Function接口不同的是該接口沒有任何參數 Supplier sp = () -> "sp"; System.out.println(sp.get()); }

      如上代碼將會返回一個字符串“sp”,通過get方法獲取到返回的值。

      Predicate

      Predicate 接口只有一個參數,返回boolean類型。該接口包含多種默認方法來將Predicate組合成其他復雜的邏輯(比如:與,或,非):

      @Test public void testPredicate() { Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate(); isEmpty.and(str -> str.equals("test")); System.out.println("tes: " + isEmpty.and(str -> str.equals("test")).test("tes")); }

      如上代碼判斷了字符串是否為空,并應用了與、非操作。

      Consumer

      Consumer 接口表示執行在單個參數上的操作,主要的方法為andThen和accept。

      @Test public void testConsumer() { SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//設置日期格式 Consumer greeter = (p) -> System.out.println("Hello, " + p); greeter.andThen((t) -> System.out.println("now is :" + df.format(new Date()))).accept("Skywalker"); }

      accept表示接收指定的參數執行操作,andThen表示當前操作結束之后附加的操作。

      Comparator

      Comparator接口用于比較, Java 8在此之上添加了多種默認方法,如reversed和thenComparing等。

      @Test public void testComparator() { Comparator comparator = String::compareTo; String str1 = "eeeabc"; String str2 = "bcd"; System.out.println("str比較大小:" + comparator.compare(str1, str2)); System.out.println("str比較大小反轉:" + comparator.reversed().compare(str1, str2)); }

      Optional

      用來防止NullPointerException異常的輔助類型,現在看看這個接口能干什么:

      Optional被定義為一個簡單的容器,其值可能是null或者不是null。在Java 8之前一般某個函數應該返回非空對象但是偶爾卻可能返回了null,而在Java 8中,不推薦你返回null而是返回Optional。

      @Test public void testOptional() { //用來防止NullPointerException異常的輔助類型 List list = Arrays.asList("ab", "bc"); System.out.println(list.stream().findFirst().orElse("null str")); Optional optional = Optional.of("hello"); optional.isPresent(); // true optional.get(); // "hello" optional.orElse("hi"); // "hello" optional.ifPresent((s) -> System.out.println("字符串不為空:" + s)); }

      optional.orElse用來對異常情況返回預設的返回結果。

      Stream

      java.util.Stream 表示能應用在一組元素上一次執行的操作序列。Stream 操作分為中間操作或者最終操作兩種,最終操作返回一特定類型的計算結果,而中間操作返回Stream本身,這樣你就可以將多個操作依次串起來。Stream 的創建需要指定一個數據源,比如 java.util.Collection的子類,List或者Set, Map不支持。

      @Test public void testSort() { List list = Arrays.asList("abe", "abc"); list = list.stream().filter(s -> s.startsWith("a")).sorted().collect(Collectors.toList()); list.stream().forEach(System.out::println); }

      Map

      中間操作map會將元素根據指定的Function接口來依次將元素轉成另外的對象,下面的示例展示了將字符串轉換為大寫字符串。你也可以通過map來講對象轉換成其他類型,map返回的Stream類型是根據你map傳遞進去的函數的返回值決定的。

      @Test public void testMap() { List list = Arrays.asList("abe", "abc"); //map返回的Stream類型是根據傳遞進去的函數的返回值決定 list.stream().map(String::toCharArray).forEach(array -> System.out.println(array.length)); }

      Match

      Stream提供了多種匹配操作,允許檢測指定的Predicate是否匹配整個Stream。所有的匹配操作都是最終操作,并返回一個boolean類型的值。

      @Test public void testMatch() { List list = Arrays.asList("ab", "abc"); boolean anyMatch = list.stream().map(String::toCharArray).anyMatch(array -> array.length == 3); boolean allMatch = list.stream().map(String::toCharArray).allMatch(array -> array.length == 3); boolean noneMatch = list.stream().map(String::toCharArray).noneMatch(array -> array.length == 3); System.out.println("anyMatch:" + anyMatch); System.out.println("allMatch:" + allMatch); System.out.println("noneMatch:" + noneMatch); }

      Reduce

      這是一個最終操作,允許通過指定的函數來講stream中的多個元素規約為一個元素,規越后的結果是通過Optional接口表示的:

      @Test public void testReduce() { List list = Arrays.asList("ab", "abc", "abcd"); Optional reduce = list.stream().reduce((s1, s2) -> s1 + ":" + s2); reduce.ifPresent(s -> System.out.println(s)); }

      ParallelStream

      串行Stream上的操作是在一個線程中依次完成,而并行Stream則是在多個線程上同時執行。

      @Test public void testParallelStream() { int max = 1000000; List values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); } long t0 = System.nanoTime(); long count = values.parallelStream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("sequential sort took: %d ms", millis)); }

      如上所示為并行排序,排序這個Stream耗時明顯低于串行。

      Map方法

      Map類型不支持stream,不過Map提供了一些新的有用的方法來處理一些日常任務。

      @Test public void testMapFun() { Map map = new HashMap<>(); for (int i = 0; i < 10; i++) { map.putIfAbsent(i, "val" + i); } map.forEach((id, val) -> System.out.println(val)); map.computeIfPresent(3, (num, val) -> val + num); System.out.println(map.get(3)); map.computeIfPresent(9, (num, val) -> null); System.out.println(map.containsKey(9)); map.computeIfAbsent(23, num -> "val" + num); System.out.println(map.get(23)); map.putIfAbsent(3, "bam"); System.out.println(map.get(3)); map.remove(3, "val3"); System.out.println(map.get(3)); //Merge時,如果鍵名不存在則插入,否則則對原鍵對應的值做合并操作并重新插入到map中 map.merge(9, "val9", (value, newValue) -> value.concat(newValue)); System.out.println(map.get(9)); map.merge(9, "concat", (value, newValue) -> value.concat(newValue)); System.out.println(map.get(9)); }

      UnaryOperator

      繼承自Function接口,表示對單個操作數的操作,該操作生成與其操作數相同類型的結果。

      @Test public void testUnaryOperator() { UnaryOperator unaryOperator = str -> str + "-test"; System.out.println(unaryOperator.apply("123")); }

      小結

      本文主要介紹了Java8中的Lambda表達式,選擇其中常用的方法進行了簡單的應用講解。Lambda表達式是Java SE 8中一個重要的新特性。Lambda表達式允許你通過表達式來代替功能接口。Lambda表達式就和方法一樣,它提供了一個正常的參數列表和一個使用這些參數的主體(body,可以是一個表達式或一個代碼塊)。

      Lambda表達式還增強了集合庫,包括java.util.function 包以及java.util.stream包。Lambda表達式非常簡潔,大大簡化代碼行數,使代碼在一定程度上變的簡潔干凈,但是同樣的,這可能也會是一個缺點,由于省略了太多東西,代碼可讀性有可能在一定程度上會降低,這個完全取決于你使用lambda表達式的位置所設計的API是否被你的代碼的其他閱讀者所熟悉。

      參考文檔

      Java 8中一些常用的全新的函數式接口

      深入理解Java 8 Lambda(語言篇——lambda,方法引用,目標類型和默認方法)

      【奔跑吧!JAVA】有獎征文火熱進行中:https://bbs.huaweicloud.com/blogs/265241

      API Java 面向對象編程

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:WPS表格從身份證號碼中提取出生年月日(wps表格從身份證提取出生年月日格式)
      下一篇:excel2010怎么拖動單元格?excel拖動單元格方法
      相關文章
      久久亚洲国产最新网站| 亚洲AV无码久久久久网站蜜桃| 亚洲美女视频一区| 国产亚洲视频在线观看网址| 亚洲精品第一国产综合野| 精品亚洲成a人片在线观看少妇| 亚洲国产精品一区二区第一页| 亚洲国产一区视频| 亚洲av中文无码| 亚洲AV日韩精品一区二区三区| 亚洲精品无码专区| 亚洲第一街区偷拍街拍| 亚洲天堂一区二区三区四区| 亚洲精品视频在线免费| 亚洲嫩草影院在线观看| 亚洲白嫩在线观看| 亚洲国产日产无码精品| 亚洲天堂2016| 亚洲人成网站18禁止| 亚洲AV成人片无码网站| 亚洲成a人无码av波多野按摩| 亚洲精品国产高清嫩草影院| 久久亚洲精品无码播放| 亚洲欧洲日产国码av系列天堂 | 中文字幕亚洲无线码| 亚洲午夜久久久影院伊人| 久久亚洲国产中v天仙www| 亚洲AV无码久久| 久久久亚洲裙底偷窥综合| 久久精品国产亚洲av麻豆色欲| 亚洲熟妇无码爱v在线观看| 亚洲乱码在线播放| 亚洲欧美国产日韩av野草社区| 无码天堂亚洲国产AV| 国产精品亚洲高清一区二区| 亚洲动漫精品无码av天堂| 久久精品国产亚洲AV嫖农村妇女| 亚洲国产日韩在线一区| 亚洲精品无码少妇30P| 亚洲欧洲自拍拍偷精品 美利坚| 国产亚洲精品精品国产亚洲综合|