Java8之Lambda语法

左羽 2020年02月14日 259次浏览

行为参数化概念

所谓行为参数化,就是将行为作为参数传入函数。

比如下面这个接口(通过作者筛选图书):

List<Book> selectBookdByAuth(String authName);

我们想要获取“路遥”的书,我们需要将“路遥”作为参数传入selectBookdByAuth这个函数。

但是,如果我又想根据出版社筛选图书呢?那就又要创建一个根据出版社筛选图书的接口了。那如果我又有需求了呢?要根据图书类别筛选图书……

有没有什么办法把“我想要根据什么筛选图书”作为一个参数呢?这样我们只需要一个筛选图书的接口就可以完成各种筛选图书的功能了。

在这里,“我想要根据什么筛选图书”是就是一个行为,将“我想要根据什么筛选图书”作为参数传入相应的函数,就被称为行为参数化

我们先举个例子(筛选图书的接口):

List<Book> selectBook(Predicate<Book> predicate);

我们暂且不用思考Predicate是什么,当我们创建了这个接口之后,我们就可以将行为作为参数传入该函数了。

例如我想要获取以“路遥”为作者的图书:

List<Book> books = selectBook(boook -> book.getAuth().equals("路遥"));

就可以获取到想要的结果了,再例如我想要获取以“人民邮电出版社”为出版社的图书:

List<Book> books = selectBook(boook -> book.getPress().equals("人民邮电出版社"));

函数式

函数式接口

函数式接口就是只定义一个抽象方法的接口。

例如:

/**
 * 运算函数式接口.
 *
 * @author zuoyu
 *
 **/
public interface Operation {

  /**
   * 用于运算两个int类型的数
   * @param a - 参数一
   * @param b - 参数二
   * @return - 结果
   */
  int opera(int a, int b);
}

对这个接口的简单使用:

@Test
  public void operationTest() {
    Operation operation = (a, b) -> a + b;
    int result = operation.opera(1, 1);
    System.out.println(result);
  }	//result:2

函数描述符

函数式接口的抽象方法的签名(参数、返回值)就是Lambda表达式的签名。其中抽象方法就是函数描述符。

例如上面的int opera(int a, int b);可以接受的Lambda表达式为(a, b) -> a + b,那么其中的参数a和参数b都是int类型,a + b结果也为int,那么这个函数的签名就是(int, int) -> int

再打个比方,例如刚才筛选图书的函数式接口Predicate

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

那么它的函数签名就是T -> boolean,意味着我们将类型T的对象作为参数传入,返回boolean类型。只有符合函数描述符的Lambda表达式才能作为参数传入相应的函数。


使用函数式接口

Java API已经为我们提供了很常用的函数式接口以及其函数描述符,当这些函数式接口不够我们使用的时候我们也可以自己创建。(一定要记住,一个函数式本身并没有什么意义,其意义在于其函数签名。)

拿几个函数式接口细说一下:

Predicate<T>

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

java.util.function.Predicate<T>接口定义了一个名为test的抽象方法,它接受泛型T对象,并返回一个boolean在你需要一个涉及到类型T的布尔表达式时,就可以使用这个函数式接口。

例如你可以写一个过滤List集合元素的方法,将这个函数式接口作为参数:

/**
   * Predicate<T>接口
   *
   * @param list - 集合
   * @param predicate - 筛选条件
   * @param <T> - 类型
   * @return 符合要求的结果
   */
  public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
    List<T> results = new ArrayList<>();
    list.forEach(t -> {
      if (predicate.test(t)) {
        results.add(t);
      }
    });
    return results;
  }

这是一个通用的对List集合进行元素过滤的方法:

  • 从一个图书List集合里获取以“图灵出版社”为出版社的图书:

    List<Book> pressBooks = filter(books, boook -> book.getPress().equals("图灵出版社"));
    
    • 注:在这里,泛型T就是Book类型
  • 从一个苹果集合内获取重量大于1.2的苹果:

    List<Apple> weightApples = filter(apples, apple -> apple.getwWight() > 1.2D);
    
    • 注:在这里泛型T就是Apple类型

Consumer<T>

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

java.util.function.Consumer<T>定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void)。如果你需要访问类型为T的对象,并对其执行某些操作,就可以使用这个接口。

例如我要对一个List集合内的每个元素执行行为操作:

 /**
   * 对集合进行任何行为操作
   */
  public static <T> void action(List<T> list, Consumer<T> consumer) {
    for (T t : list) {
      consumer.accept(t);
    }
  }

这是一个通用的对List集合进行元素行为操作的方法:

  • 对图书集合内的元素进行打印:

    action(books, book -> System.out.println(book.toString()));
    
    • 注:在这里,泛型T就是Book类型
  • 将苹果集合内的每个苹果的重量增加0.5:

    action(apples, apple -> {
          apple.setWeight(apple.getWeight() + 1.00D);
        });
    
    • 注:在这里泛型T就是Apple类型

Function<T, R>

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

java.util.function.Function<T, R>接口定义了一个叫做apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。如果你需要定一个Lambda用于输入对象的信息映射到输出,你便可以利用这个接口来完成。

例如我要对一个List集合内所有对象的某一个属性进行提取:

 /**
   * 对集合内对象的某一元素进行提取
   */
  public static <T, R> List<R> function(List<T> list, Function<T, R> function) {
    List<R> rList = new ArrayList<>();
    for (T t : list) {
      R r = function.apply(t);
      rList.add(r);
    }
    return rList;
  }

这是一个通用的对List集合的对象操作的方法:

  • 对图书集合内的所有书的书名进行提取:

    List<String> booksName = function(books, book -> book.getName());
    
    • 注:在这里,泛型TBook类型,泛型RString类型
  • 对苹果集合内的所有苹果的重量进行提取,并增加0.5:

    List<Double> appleWeight = function(apples, apple ->
            apple.getWeight() + 0.5D
        );
    
    • 注:在这里,泛型TApple类型,泛型RDouble类型

Function<T, R>的原始类型化

众所周知,在Java中的泛型只能绑定到引用类型上,不能绑定在原始类型上。在Java中有一个将原始类型转换为对应的引用类型的机制——装箱;相反的将引用类型转换为原始类型的机制——拆箱。这一系列操作都是Java自动完成的,但是这个机制是要付出代价的——

**装箱:**把原始数据类型包裹起来,并保存到堆里。所以,装箱后需要更多的内存来保存,并需要额外的内存用来搜索并获取被包裹的原始值。

Java8为避免这个现象对其所提供的函数式接口带来了一个专门版本,在数据的输入和输出都是原始类型时避免自动装箱的操作,以此节省内存。

例如我要根据下标获取一个苹果对象:

IntFunction<Apple> appleIntFunction = (int i) -> apples.get(i);
Apple apple = appleIntFunction.apply(2);

我们来看一下IntFunction接口:

public interface IntFunction<R> {
    R apply(int value);
}

java.util.function.IntFunction<R>接口的参数为int原始类型,返回一个R类型,与我们想要完成相同功能的java.util.function.Function<T, R>接口相比较,避免了必须传入Integer类型的自动装箱操作。

再比如我要获取一个double随机数的2倍数:

IntToDoubleFunction intToDoubleFunction = (int i) -> Math.random() * i;
double random = intToDoubleFunction.applyAsDouble(2);

我们看一下IntToDoubleFunction接口:

public interface IntToDoubleFunction {
    double applyAsDouble(int value);
}

上面的功能如果我们使用java.util.function.Function<T, R>接口来实现这个功能,需要将接口写成Function<Integer, Double>,输入Integer类型并输出Double类型;相对于java.util.function.IntToDoubleFunction接口,输入int类型输出double类型,省去了自动装箱。

Function<T, R>的变种函数:

  • IntFunction<R>
  • IntToDoubleFunction
  • IntToLongFunction
  • LongFunction<R>
  • LongToDoubleFunction
  • LongToIntFunction
  • DoubleFunction<R>
  • ToIntFunction<T>
  • ToDoubleFunction<T>
  • ToLongFunction<T>

其他函数式接口

JavaAPI自带的函数接口还有不少,为的是我们日常使用。当然也有不能满足我们需求的时候,比如我要输入三个参数,那就需要自己定义接口了。还是那句话,函数接口本身并无意义,其意义在于其函数签名(参数数量与返回类型)。

JavaAPI自带的函数式接口(不一一细说了):

函数式接口函数描述符
Predicate<T>T -> boolean
Consumer<T>T -> void
Function<T, R>T -> R
Supplier<T>(void) -> T
UnaryOperator<T>T -> T
BinaryOperator<T>(T, T) -> T
BiPredicate<L, R>(L, R) -> boolean
BiConsumer<T, U>(T, U) -> void
BitFunction<T, U, R>(T, U) -> R
  • **注:**以上函数式接口都有其原始类型化的变种。

方法引用

方法引用可以重复的使用现有的方法定义,可以将其理解为Lambda的简化方式。

例如,我要根据苹果的重量对其从小到大排序:

apples.sort((apple1, apple2) -> apple1.getWeight().compareTo(apple2.getWeight()));

上面是Lambda表达式的写法,那么换作方法引用的方式可以简化代码:

apples.sort(Comparator.comparing(Apple::getWeight));

相对于Lambda表达式的写法,方法引用的写法在这里意思更加清晰直观。

方法引用主要有三类:

  1. 指向静态方法的方法引用(例:IntegerparseInt()方法可以直接写成Integer::parseInt)。
  2. 指向任意类型实例方法的方法引用(例:Stringlength()方法可以直接写成String::length)。
  3. 指向现有对象的实例方法的方法引用(例:假设有一个局部变量book指向用于存放Book类型的对象,它有一个实例方法getName(),你可以直接写成book::getName)。

复合Lambda表达式

Java8API中的函数式接口都提供了复合方法,即通过这些方法把多个简单的Lambda表达式复合成复杂的表达式。

谓词复合

谓词接口包含三个方法:negate(否定)、and(并且)、or(或)。

这几个谓词类似布尔语句之间的关系,举个例子:

  • 比如我现在有三种对图书的筛选方案:

    • 筛选出以“路遥”为作者的图书的逻辑接口:
    Predicate<Books> bookPredicateByAuth = book -> book.getAuth().equals("路遥"));
    
    • 筛选出以“人民邮电出版社”为出版社的图书的逻辑接口:
    Predicate<Books> bookPredicateByPress = boook -> book.getPress().equals("人民邮电出版社"));
    
    • 筛选出印刷时间在2010年之后的图书的逻辑接口:
    Predicate<Books> bookPredicateByPrintingData = boook -> book.getPrintingData.before(new SimpleDateFormat("yyyy-MM-dd").parse("2010-01-01"););
    
  • 那么我现在想要筛选出以“路遥”为作者的,印刷时间在2010年之后的图书,不要以“人民邮电出版社”出版的,可以该逻辑接口这么写:

    Predicate<Books> bookPredicate = bookPredicateByAuth.and(bookPredicateByPrintingData).negate(bookPredicateByPress);
    
  • 进行筛选:

    List<Book> books = filter(books, bookPredicate));
    

函数复合

函数复合的接口方法有两个:andThencompose

andThen方法会返回一个函数,它先对输入应用一个给定的函数,再对输出应用另一个函数。

compose方法把给定的函数作用compose的参数里面给的那个函数,然后再把函数本身用于结果。

这两个函数的作用就是函数套函数,举个例子:

  • 我现在有两个函数:

    • 第一个函数为两数相加的函数:
    Function<Integer, Integer> add = x -> x + 1;
    
    • 第二个为两数相乘的函数:
    Function<Integer, Integer> multiply = x -> x * 2;
    
  • 如果想要先算加法再算乘法,达到multiply(add())的效果(只是可以这样理解,实际不是这样的结构):

    Function<Integer, Integer> function = add.andThen(multiply);
    function.apply(1);  // result: 4
    
  • 如果想要先算乘法再算加法,达到add(multiply)的效果(只是可以这样理解,实际不是这样的结构):

    Function<Integer, Integer> function = add.compose(multiply);
    function.apply(1);  // result: 3
    

结尾