Java8之lambda表达式

作者: 苦寒行 | 来源:发表于2018-02-02 14:16 被阅读58次

    在这篇文章中您主要可以看到以下内容:

    • 对lambda表达式产生需求的原因
    • lambda表达式语法
    • 函数式接口
    • 一些例子

    前言

    最近因为实习,需要了解一些java8的相关知识,主要是大佬们提供的接口比较频繁得使用了函数式接口,通过一段时间得学习和使用后,将一些笔记和思考整理出来。在学习过程中主要使用得书籍是Java8 in action,当然还参考了大量的博客。关于java8大概会整理4篇文章,第一篇是关于lambda表达式的,第二篇将会是关于流的,第三篇将会是java8提供的其他语法,第四篇将会是对java8函数式编程的思考,这大概与书籍的目录相对应,但是我会尽量避免大量使用书籍中的语句,所以文章不会面面俱到,关于一些细节可以参考Java8 in action书籍。同样,因为水平所限,难免会有错误或者理解片面的地方,欢迎留言讨论。文章中的例子在以下地址可以看到完整代码github地址

    对lambda表达式产生需求的原因

    java8 in action中有这样一个例子:有个果农,需要根据一些要求对自己的产品做筛选,比如选取重量大于500g的苹果(额,虽然我不是太清楚一个苹果有多重,但是这并不影响我们对需求的描述。 ),或者选出颜色为淡黄色的香蕉。当然也可能根据苹果的颜色或者香蕉的重量进行筛选。那么我们如何实现这个需求呢,使用java7我们可能提供以下代码:

    // 以判断苹果的属性为例
    class Apple {
      private String color;
      private int weight;
    
      Apple(String color, int weight) {
        this.color = color;
        this.weight = weight;
      }
      // getter、setter.....
    }
    // 然后需要一个通用的判断接口
    interface FruitsPredicate<T> {
      boolean test(T t);
    }
    // 然后是决策
    public static <T> List<T> predicateFruit(List<T> fruits, FruitsPredicate<T> predicate) {
        List<T> resultList = new ArrayList<>();
        for (T t : fruits) {
          if (predicate.test(t)) resultList.add(t);
        }
        return resultList;
      }
    // 看看筛选苹果重量的具体实现
    result =
            Fruits.predicateFruit(
                appleList,
                new FruitsPredicate<Apple>() {
                  @Override
                  public boolean test(Apple apple) {
                    return apple.getWeight() > 500 ? true : false;
                  }
                });
    

    这是一个策略模式的例子,我们注意到,为了满足不同的需求,我们可以提供不同的代码,比如某一天果农需要按照颜色来对苹果进行分类,则可以做如下修改:

    // 判断苹果颜色
    result =
        Fruits.predicateFruit(
            appleList,
            new FruitsPredicate<Apple>() {
              @Override
              public boolean test(Apple apple) {
                return "red".equals(apple.getColor()) ? true : false;
              }
            });
    

    可以注意到,在判断苹果重量和判断苹果颜色的例子中,我们使用了两个匿名类,而这两个匿名类仅仅只有两行代码不同,那为什么我们不能只提供这两行代码!毕竟不想偷懒的程序员不是好程序员~~~,可是在java8之前是不能实现的,因为java中除了基础数据类型,一切都是对象,能够进行传递的只有这两类数据,即使我们想要传递方法,额,你可以选择guava或者其他第三方库,但是使用java官方的库是不能实现的。为了解决这个问题,java在借鉴scala和guava及大量其他语言或者第三方库的优点后,推出了java8,内置提供了对lambda表达式的支持。
    经过lambda改造之后的代码将会变成这样:

    result = Fruits.predicateFruit(appleList, apple -> apple.getWeight() > 500 ? true : false);
    result = Fruits.predicateFruit(appleList, apple -> "red".equals(apple.getColor()) ? true : false);
    

    看起来是不是简洁了很多!

    Lambda

    Lambda与类和基础类型的区别

    不管lambda底层实现,我们可以完全认为lambda就是一个方法,而且这是一个可以传递的方法,也就是说你可以将这个方法保存在一个变量中,也可以直接传递给另外的方法使用,想想都很激动!这就意味了不必为了封装几行代码而创建一个类,让这个类的实例来进行流通。java的类可是重量级的啊(c++的类相对来说就简单的多)!

    Lambda和函数式接口

    既然lambda表达式可以像变量一样保存、传递,那么需要用什么东西来保存呢?答案就是函数式接口,其实函数式接口只是个高大上的名词而已,简单来说函数式接口就是只定义了一个方法的接口,当然,这里的一个指的是在除去接口中默认方法之后剩余的数量。

    Lambda语法中的坑

    首先看一个lambda表达式:

    apple -> apple.getWeight() > 500 ? true : false
    

    lambda表达式是一个方法,所以它具有方法应该具有的一切内容,参数列表、函数主体、甚至是异常列表。通常参数列表并不强制要求指明参数类型,指明或者不指明类型都有好处和坏处,所以就全凭喜好,只是在使用的过程中不指明的情况要多见些,这和类型推断机制有关,这会在后面介绍。
    关于函数主体是否需要带大括号的问题,如果主体只有一个表达式,那么是不需要大括号的,同时默认return该表达式的值。但是如果函数主体不止一个表达式或者由一个或者多个语句组成,那么就需要带上大括号,且此时如果有必要需要显示提供return语句。通常来说不带大括号的情况应该占据多数,并不推荐lambda表达式中包含复杂的逻辑。

    常见函数式接口

    函数签名

    函数签名是由函数参数的类型和返回值类型组成的,如果函数接口的函数签名和lambda表达式的函数签名相同,那么就说明这两者类型匹配。简单来说就像不能将Integer对象赋值给声明为String的变量一样,不能将两个函数签名不同的lambda表达式赋值给函数式接口。比如下面函数的签名:

    apple -> apple.getWeight() > 500 ? true : false
    

    (Apple)->(boolean)
    因为参数apple的类型是Apple而返回类型是一个boolean,所以这个lambda表达式的函数签名就如上所示。

    Predicate

    下面将要介绍一些java8内置的常见的函数接口,它们都在java.util.function包中。
    这是一个主要用来做判断的函数接口,他的声明如下:

    @FunctionalInterface
    public interface Predicate<T>{
      boolean test(T t);
    }
    

    函数的签名是(T)->boolean,也就是说他接受一个参数,经过判断,返回一个boolean表示判断的结果,至于如何判断,就需要我们用lambda表达式进行说明,比如:

    apple -> apple.getWeight() > 500 ? true : false
    

    这个函数的函数签名和Predicate的函数签名是相同的,所以可以将该lambda表达式赋值给Predicate类型的变量:

    Predicate<Apple>  p = apple -> apple.getWeight() > 500 ? true : false;
    

    下面是一个具体的使用例子:

    public class FInterface {
      // Predicate
      public static <T> String predicateTest(T t, Predicate<T> p) {
        if (p.test(t)) return "yes";
        return "no";
      }
    
      public static void main(String[] args) {
        System.out.println(FInterface.predicateTest(5, integer -> integer > 3));
      }
    }
    

    同时,Predicate也支持谓词符合,即and(与)、or(或)、negate(非),一个具体的例子如下:

     FInterface.predicateTest(
                5, ((Predicate<Integer>) integer -> integer > 3).and(integer -> integer < 5)));
    

    Consumer

    声明如下:

    @FunctionalInterface
    public interface Consumer<T>{
      void accept(T t);
    }
    

    函数签名:(T)->(void)
    对于返回值为void的函数来说,有个特殊的兼容规则:如果一个lambda表达式的主体是一个表达式,那么他和一个返回值是void的函数接口兼容,即使这个表达式有返回值。
    具体的使用例子:

    public static <T> void  consumerTest(List<T> listT, Consumer<T> c){
        for (T t : listT) {
          c.accept(t);
        }
      }
    // main
    FInterface.consumerTest(Arrays.asList(1, 2, 3, 4), integer -> System.out.println(integer));
    

    Supplier

    声明如下:

    @FunctionalInterface
        public interface Supplier<T> {
            T get();
        }
    

    函数签名:()->(T)
    使用例子:

    // Supplier
      public static <T> T supplierTest(Supplier<T> s) {
        return s.get();
      }
    System.out.println(FInterface.supplierTest(() -> "test"));
    

    Function

    声明如下:

    @FunctionalInterface
    public interface Function<T, R>{
    R apply(T t);
    }
    

    函数签名:(T)->(R)
    具体使用例子:

    public static <T> void  consumerTest(List<T> listT, Consumer<T> c){
        for (T t : listT) {
          c.accept(t);
        }
      }
    // 银魂废材王子的叫声~~~~~
    System.out.println(FInterface.functionTest("啊~~~~~",(String s)->s+"哦~~~~~~"));
    

    同时,函数也支持复合,分别有以下两种复合方式:
    (g o f)(x):andThen()
    (f o g)(x):compose()

    装箱(Boxing)与拆箱(Unboxing)

    大量的函数式接口将参数都声明为泛型,但因为泛型的实现原理,不能支持基础数据类型(可以简单理解为泛型T就是Object的代名词,只是运行时动态绑定,Object只能指向引用类型而不能指向基础类型,原因是引用类型和值类型存储机制不同),所以当基础数据类型使用这些函数接口的时候,默认都有一个装箱(将基础数据类型转化为对于的对象)和拆箱(将对象转化为基础数据类型)操作,这一系列操作十分影响性能,特别是在数据量较大或者操作链较长的情况下,所以java8还内置类针对基础数据类型的相应函数接口,这些函数接口就是在相应的函数接口上加上相应的原始类型前缀。比如DoublePredicate、IntConsumer等。

    方法引用和构造函数Lambda化

    方法引用是lambda表达式的语法糖,可以很方便的将现有方法转化为lambda表达式,进而进行方法传递,一个例子如下:

    public class MethodReference {
      public static void consumerMR(List<String> sl, Consumer<String> c) {
        for (String s : sl) {
          c.accept(s);
        }
      }
    
      public static void main(String[] args) {
        MethodReference.consumerMR(Arrays.asList("test", "string"), System.out::print);
      }
    }
    

    在代码中我们使用了System.out的静态方法,并利用方法引用将其传递给了Consumer。方法引用的标志就是连续的两个冒号"::",方法引用有三种方式,第一种就是向上面的引用存在类的静态方法,第二种是引用lambda表达式传递对象的已有方法,第三种是引用lambda表达式外部已经声明的对象的方法。
    在这之中对于构造函数的lambda化比较有意思,如果使用默认的构造函数,即构造函数的签名是()->(T),则这和Supplier接口的签名匹配,所以下面的代码是合法的:

    Supplier<T> s = T::new;
    

    当然,如果不是默认构造函数,则需要寻找合适的函数接口(系统提供或者自己声明)。

    闭包

    闭包详细的定义请问度娘(数学闭包和计算机闭包没有本质的联系),简单来说就是一个函数和与之相关联的数据组成的实体,函数可传递,数据不仅包含函数自己的数据域,还包含自己数据域之外的数据,也就是说函数可以访问自己数据域之外的数据。根据上述描述,java lambda表达式并没有完全的实现闭包的含义,主要问题出在如何访问函数之外的数据域,因为函数外数据域存储位置不同导致生命周期不同以至于无法保证在任意时刻可以访问任意变量,java的lambda表达式在处理这个问题的时候采用了和匿名函数访问外部变量相同的方式,即默认将访问的变量置为final。

    常见函数接口


    常见函数接口

    有些函数接口在原有的一元变量的基础上扩展而来,从而支持二元变量,至于其他的需求,可以自己定义接口。

    相关文章

      网友评论

        本文标题:Java8之lambda表达式

        本文链接:https://www.haomeiwen.com/subject/vitdzxtx.html