Java 8 Lambda函数编程入门(一)

什么是函数式编程

每个人对函数式编程的理解不尽相同。但其核心是:在思考问题时,使用不可变值和函 数,函数对一个值进行处理,映射成另一个值。

背景

Java是一门面向对象编程语言。面向对象编程语言和函数式编程语言中的基本元素(Basic Values)都可以动态封装程序行为:面向对象编程语言使用带有方法的对象封装行为,函数式编程语言使用函数封装行为。但这个相同点并不明显,因为Java的对象往往比较“重量级”:实例化一个类型往往会涉及不同的类,并需要初始化类里的字段和方法。

随着回调模式和函数式编程风格的日益流行,我们需要在Java中提供一种尽可能轻量级的将代码封装为数据(Model code as data)的方法。匿名内部类并不是一个好的选择,因为:

  1. 语法过于冗余
  2. 匿名类中的this和变量名容易使人产生误解
  3. 类型载入和实例创建语义不够灵活
  4. 无法捕获非final的局部变量
  5. 无法对控制流进行抽象

Lambda表达式基本知识

辨别Lambda表达式

  1. Lambda 表达式不包含参数,使用空括号()表示没有参数。
    Runnable noArguments = () -> System.out.println("Hello World");
  2. Lambda 表达式包含且只包含一个参数,可省略参数的括号。
    ActionListener oneArgument = event -> System.out.println("button clicked");
  3. Lambda 表达式的主体不仅可以是一个表达式,而且也可以是一段代码块,使用大括号 {}将代码块括起来。

    Runnable multiStatement = () -> {
        System.out.print("Hello");
       System.out.println(" World");
    };
  4. Lambda 表达式也可以表示包含多个参数的方法。
    BinaryOperator add = (x, y) -> x + y;

Lambda 变量类型如何识别

在Lambda表达式中无需指定类型,程序依然可以编译。这是因为 javac 根据程序的上下文(方法的签名)在后台推断出了参数的类型。这意味着如果参数类型不言而明,则无需显式指定。即Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。

Lambda 引用值,而不是变量

如果你曾使用过匿名内部类,也许遇到过这样的情况:需要引用它所在方法里的变量。这时,需要将变量声明为final。将变量声明为final,意味着不能为其重复赋值。同时也意味着在使用final变量时,实际上是在使用赋给该变量的一个特定的值。

Java 8虽然放松了这一限制,可以引用非final变量,但是该变量在既成事实上必须是 final。虽然无需将变量声明为final,但在Lambda表达式中,也无法用作非终态变量。如果坚持用作非终态变量,编译器就会报错。(local variables referenced from a lambda expression must be final or effectively final.)

既成事实上的 final 是指只能给该变量赋值一次。换句话说,Lambda 表达式引用的是值, 而不是变量。

Lambda用在哪里

我们知道Lambda表达式的目标类型是函数性接口——每一个Lambda都能通过一个特定的函数式接口与一个给定的类型进行匹配。因此一个Lambda表达式能被应用在与其目标类型匹配的任何地方,lambda表达式必须和函数式接口的抽象函数描述一样的参数类型,它的返回类型也必须和抽象函数的返回类型兼容,并且他能抛出的异常也仅限于在函数的描述范围中。

函数接口

使用只有一个方法的接口来表示某特定方法并反复使用,这种接口称为函数接口。

Java中重要的函数接口:

接口 参数 返回类型
Predicate T boolean
Consumer T void
Function T R
Supplier None T
UnaryOperator T T
BinaryOperator (T,T) T

Java 8中新增的对核心类库的改进主要包括集合类的 API 和新引入的流 (Stream)。

从外部迭代到内部迭代

两者的差异如图:
Java 8 Lambda函数编程入门(一)_第1张图片

Java 8 Lambda函数编程入门(一)_第2张图片

实现机制

通常,在 Java 中调用一个方法,计算机会随即执行操作:比如System.out.println ("Hello World");会在终端上输出一条信息。Stream里的一些方法却略有不同,它们虽是 普通的 Java 方法,但返回的 Stream 对象却不是一个新集合,而是创建新集合的配方。

allArtists.stream()
               .filter(artist -> artist.isFrom("London"));

像 filter 这样只描述 Stream,最终不产生新集合的方法叫作惰性求值方法(lazy);而像 count 这样 最终会从 Stream 产生值的方法叫作及早求值方法(eager)。

判断一个操作是惰性求值还是及早求值很简单:只需看它的返回值。如果返回值是 Stream, 那么是惰性求值;如果返回值是另一个值或为空,那么就是及早求值。使用这些操作的理想方式就是形成一个惰性求值的链,最后用一个及早求值的操作返回想要的结果,这正是 它的合理之处。

常用的流操作

collect(toList())

collect(toList()) 方法由 Stream 里的值生成一个列表,是一个及早求值操作。

map

如果有一个函数可以将一种类型的值转换成另外一种类型,map 操作就可以 使用该函数,将一个流中的值转换成一个新的流。

filter

遍历数据并检查其中的元素时,可尝试使用 Stream 中提供的新方法 filter。

由于此方法和 if 条件语句的功能相同,因此其返回值肯定是 true 或者 false。经过过滤, Stream 中符合条件的,即 Lambda 表达式值为 true 的元素被保留下来。该 Lambda 表达式 的函数接口正是前面章节中介绍过的 Predicate

flatMap

flatMap 方法可用 Stream 替换值,然后将多个 Stream 连接成一个 Stream。

前面已介绍过 map 操作,它可用一个新的值代替 Stream 中的值。但有时,用户希望让 map 操作有点变化,生成一个新的 Stream 对象取而代之。用户通常不希望结果是一连串的流, 此时 flatMap 最能派上用场。

max和min

Stream 上常用的操作之一是求最大值和最小值。Stream API 中的 max 和 min 操作就是解决这一问题的。

查找 Stream 中的最大或最小元素,首先要考虑的是用什么作为排序的指标。

reduce

reduce 操作可以实现从一组值中生成一个值。

int count = Stream.of(1, 2, 3)
                       .reduce(0, (acc, element) -> acc + element);
     assertEquals(6, count);

Lambda 表达式的返回值是最新的 acc,是上一轮 acc 的值和当前元素相加的结果。reducer 的类型是前面已介绍过的 BinaryOperator。


参考资料:
Java 8函数式编程 作者:(英)沃伯顿著
备注:
转载请注明出处:http://blog.csdn.net/wsyw126/article/details/52645402
作者:WSYW126

你可能感兴趣的:(JAVA)