《Java 8 实战》Ch3: Lambda表达式(上):基础运用

李文轩 2019-04-21


3.1 Overview

  • Lambda表达式可以简单得理解为简洁地表示可以传递匿名函数的一种方式:没有名称,但是有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

    • 简洁:代码一般比匿名类少,主要少在模版代码中。
    • 传递:Lambda表达式可以作为参数传递给方法或者存储在变量中。
    • 匿名:像匿名类一样没有名称
    • 函数:像函数一样有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表;但不像方法那样属于某个特定的类。
  • 主要表现形式为:

    • (parameters) -> expression

    或者

    • (parameters) -> { statement; }
  • 其实和普通的方法一样,有一个参数列表和方法主体;不同点在于 Lambda表达式用箭头(→) 区分这两者。

  • 例子:

    //参数是一个字符串,返回此字符串的长度
    //Lamdba 没有return语句,因为已经隐含了return
    (String s) -> s.length()
    
    //参数是一个Apple的对象,返回一个布尔值代表Apple的重量是否大于150
    (Apple apple) -> apple.getWeight() > 150
    
    //参数是两个int类型,而没有返回值(void返回)。
    //Lambda是可以包含多行语句
    (int x, int y) -> {
      System.out.println("result: ");
      System.out.println("x+y);
    }
    
    //没有参数,返回一个int值
    () -> 42
    
  • 用Lambda表达式的前后对比

    //这里用第二章的Apple类做例子,创建一个Apple类型的Comparator
    
    //前
    Comparator<Apple> byweight = new Comparator<Apple>() {
      public int compare(Apple a1, Apple a2){
        return a1.getWeight().compareTo(a2.getWeight());
      }
    }
    
    //后
    Comparator<Apple> byWeight = 
     (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
    
    

3.2 Lambda表达式的具体用法

  1. 函数式接口

    • 即一个定义且只定义了一个抽象方法的接口

    • 一些Java自带的函数式接口

      //java.util.Comparator
      public interface Comparator<T> {
        int compare(T o1, T o2);
      }
      
      //java.lang.Runnable
      public interface Runnable {
        void run();
      }
      
      //java.awt.event.ActionListener
      public interface ActionListener extends EvenListener{
        void actionPerformed(ActionEvent e);
      }
      
      //java.util.concurrent.Callable
      public interface Callable<V>{
        V call();
      }
      
      //java.security.PrivilegedAction
      public interface PrivilegedAction<V> {
        V run();
      }
      
      
    • Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。虽然匿名类也可以达成目的,但是用Lambda表达式比较简洁和更易读。

        //用3种方法打印“Hello World” 来对比匿名类和Lambda表达式
      
        //使用Lambda
        Runnable r1 = () -> System.out.println("Hello world 1");
      
        //使用匿名类
        Runnable r2 = new Runnable(){
          public void run(){
              System.out.println("Hello World 2");
          }
        }
      
        public static void process(Runnable r){
          r.run();
        }
      
        public static void main(String[] args){
          //打印"Hello world 1"
          process(r1);
      
          //打印"Hello world 2"
          process(r2);
      
          //利用直接传递的Lambda,打印"Hello world 3"
          process( () -> System.out.println("Hello World 3")); 
        }
      
      
  2. 函数描述符

    • 函数式接口的抽象方法的签名就是Lambda表达式的签名,函数描述符是这个签名的抽象。
    • Runnable为例,它的函数描述符为 () -> void,因为它不接受任何参数,也不返回(void)
    • 上面 Comparator<Apple>的描述符为 (Apple, Apple) -> int,即接受两个Apple对象,返回一个int值。
    • @FunctionalInterface,这个标注用于表示该接口会设计成一个函数式接口。

3.3 用Lambda简化环绕执行

  • 环绕执行主要出现在需要某些资源处理的行为中,处理文件或者数据库。

  • 环绕执行模式基本上是打开一个资源,做一些处理,关闭资源。这些开头和结尾关闭的阶段总是很类似,而且是围绕这执行处理的重要代码。

  • 简化的目标就是重用这些环绕的代码

      //环绕执行的一个例子
      //try-with-resource 已经简化了显式地关闭资源的行为
    
      public static String processFile() throws IOException {
        //用try语句开启资源,并在它结束时隐式地关闭资源
        try (BufferedReader br = 
                new BufferedReader(new FileReader("data.txt"))){
            //重要的资源处理代码 
            return br.readline();
        }
      }
    
    
  • 用Lambda优化环绕执行模式并使其行为参数化

    1. 行为参数化

      • 在上面的代码中,处理资源的代码有局限性。为了更灵活地设计环绕执行模式,我们可以将处理资源的行为参数化。

      • 理想方式是让 processFile方法接受一个函数, (BufferedReader) -> String

          //理想的processFile方法的运用方式
          String result = processFile( (BufferedReader br) -> br.readLine());
        
        
    2. 函数式接口

      • 因为Lambda只能用在接受函数式接口的方法里,我们需要一个签名是 (BufferedReader) -> String 的函数式接口,并且要抛出 IOException异常

          @FunctionalInterface
          public interface BufferedReaderProcessor {
            String process(BufferedReader br) throws IOException;
          }
        
        
      • 我们也同时需要改变 processFile的方法参数,以至于它能接受这个接口

          public static String processFile(BufferedReaderProcessor) throws IOException{
            ...
          }
        
        
    3. 执行一个行为

      • processFile方法里,将重要的处理资源的代表,替换成 BufferedReaderProcessor的process,就可以把处理资源的行为和整个框架分离。

      • 处理资源的行为将取决于我们传递给 processFileBufferedReaderProcessor的实例

          public static String processFile(BufferedReaderProcessor) throws IOException{
            try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
                return p.process(br);
            }
          }
        
        
    4. 传递Lambda

      • 用Lambda表达式定义行为

          //从文件里取第一行
          String oneLine = processFile((BufferedReader br -> br.readLine());
        
          //从文件里取前两行
          String twoLine = processFile((BufferedReader br -> br.readLine() + br.readLine());
        
        

3.4 Java API的常见函数式接口

  1. Predicate

    • java.util.function.Predicate<T>接口定义了一个 test的抽象方法

    • 签名为 (T) -> boolean

    • 需要表达一个涉及类型T的布尔表达式的时候可以使用

        @FunctionalInterface
        public interface Predicate<T>{
          boolean test(T t);
        }
      
        public static <T> List<T> filter(List<T> list, Predicate<T> p) {
          List<T> results = new ArrayList<>();
          //遍历list,每个元素都通过p的test方法,每次调用都会获得一个布尔值,它决定会不会将元素放进返回的list里。
          for(T s: list){
              if(p.test(s)){
                  result.add(s);
              }
          }
          return results;
        }
      
      
    • 使用:

        //用lambda表达式赋予函数给一个Predicate的对象
        Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
      
        //通过变量传递行为
        List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate)
      
      
    • Predicate有诸如and和or的方法,下一节会谈到

  2. Consumer

    • java.util.function.Consumer<T>接口定义了一个 accept的抽象方法

    • 签名为 (T) -> void

    • 需要访问一个类型T的对象,并对其执行操作(不返回)的时候可以使用

        @FunctionalInterface
        public interface Consumer<T>{
          void accpet(T t);
        }
      
        public static <T> void forEach(List<T> list, Consumer<T> c){
          for(T i: list){
              //遍历list,每个元素都通过c的accept方法,accept方法会有运用这些元素的行为
              c.accept(i);
          }
        }
      
      
    • 使用:

        //遍历list,并打印每个数字
        //通过Lambda传递 Consumer的实例(行为)
        forEach( Array.asList(1,2,3,4,5), (Integer i) -> System.out.println(i));
      
      
  3. Function

    • java.util.function.Function<T,R>接口定义了一个 apply的抽象方法

    • 签名为 (T) -> R

    • 需要将输入对象的信息映射到输出的时候可以使用(比如说,把字符串映射为它的长度)

        @FunctionalInterface
        public interface Function<T,R>{
          R apply(T t);
        }
      
        public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
          List<R> result = new ArrayList<>();
      
          for(T s: list){
              //遍历list,每个元素都通过f的apply方法,apply方法会有映射这些元素的行为
              result.add(f.apply(s));
          }
          return result;
        }
      
      
    • 使用:

        //遍历list,并将每个字符串输入到f的apply
        //这里的apply实例为,输入字符串返回其长度。
        List<Integer> l = map(Arrays.asList("lambdas", "in", "action"), 
                                                  (String s) -> s.length()
                                               );
      
      

原始类型特化

  • 上述的函数式接口都是泛性的;若是要输入原始类型的数据时,我们不能避免得用到Java的自动装箱自动拆箱的机制。自动将引用类型(Byte、Integer等)换成对应的原始类型(byte、int)为自动拆箱,自动从原始类型到引用类型为自动装箱。

  • 如果要实现函数式接口是没有问题的,但是会有性能上的缺陷。详情请查询相关阅读,这里不展开了。

  • Java 8 带来了一些其函数式接口的专门的版本,以便在输入和输出时,都是原始类型,避免自动装箱和拆箱。

      //这里用Predicate的专门应对输入是integer原始类型的一个版本
      public interface IntPredicate{
        boolean test(int t);
      }
    
      //无装箱
      IntPredicate evenNumbers = (int i) -> i % 2 == 0;
      evenNumbers.test(1000); //返回true
    
      //有装箱
      Predicate<T> oddNumbers = (Integer i) -> i % 2 == 1;
      oddNumbers.test(1000); //返回false
    
    
  • 这个链接有这些版本的详细的表格

java.util.function (Java Platform SE 8 )


Reference:

[1] Fusco, Mario, and Alan Mycroft. Java 8 in Action: Lambdas, Streams, and Functional-Style Programming. Manning, 2015.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,084评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,623评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,450评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,322评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,370评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,274评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,126评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,980评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,414评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,599评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,773评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,470评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,080评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,713评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,852评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,865评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,689评论 2 354

推荐阅读更多精彩内容