这篇文章主要介绍了 Java 函数式编程(二): 集合的使用, 本文着重讲解了遍历列表的一些方法, 需要的朋友可以参考下
第二章: 集合的使用
我们经常会用到各种集合, 数字的, 字符串的还有对象的它们无处不在, 哪怕操作集合的代码要能稍微优化一点, 都能让代码清晰很多在这章中, 我们探索下如何使用 lambda 表达式来操作集合我们用它来遍历集合, 把集合转化成新的集合, 从集合中删除元素, 把集合进行合并
遍历列表
遍历列表是最基本的一个集合操作, 这么多年来, 它的操作也发生了一些变化我们使用一个遍历名字的小例子, 从最古老的版本介绍到现在最优雅的版本
用下面的代码我们很容易创建一个不可变的名字的列表:
- final List<String> friends =
- Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
- System.out.println(friends.get(i));
- }
下面这是最常见的一种遍历列表并打印的方法, 虽然也最一般:
- for (int i = 0; i < friends.size(); i++) {
- System.out.println(friends.get(i));
- }
我把这种方式叫做自虐型写法又啰嗦又容易出错我们得停下来好好想想,"是 i < 还是 i<= 呢?" 这只有当我们需要操作具体某个元素的时候才有意义, 不过即便这样, 我们还可以使用坚持不可变原则的函数式风格来实现, 这个我们很快会讨论到
Java 还提供了一种相对先进的 for 结构
- collections / fpij / Iteration.java
- for (String name: friends) {
- System.out.println(name);
- }
在底层, 这种方式的迭代是使用 Iterator 接口来实现的, 调用了它的 hasNext 和 next 方法 这两种方式都属于外部迭代器, 它们把如何做和想做什么揉到了一起我们显式的控制迭代, 告诉它从哪开始到哪结束; 第二个版本则在底层通过 Iterator 的方法来做这些显式的操作下, 还可以用 break 和 continue 语句来控制迭代 第二个版本比第一个少了点东西如果我们不打算修改集合的某个元素的话, 它的方式比第一个要好不过这两种方式都是命令式的, 在现在的 Java 中应该摒弃这种方式 改成函数式原因有这几个:
1.for 循环本身是串行的, 很难进行并行化
2. 这样的循环是非多态的; 所得即所求我们直接把集合传给 for 循环, 而不是在集合上调用一个方法 (支持多态) 来执行特定的操作
3. 从设计层面来说, 这样 写的代码违反了 Tell,Don't Ask 的原则 我们请求执行一次迭代, 而不是把迭代留给底层库来执行
是时候从老的命令式编程转换到更优雅的内部迭代器的函数式编程了使用内部迭代器后我们把很多具体操作都扔给了底层方法库来执行, 你可以更专注于具体的业务需求底层的函数会负责进行迭代的我们先用一个内部迭代器来枚举一下名字列表
Iterable 接口在 JDK8 中得到加强, 它有一个专门的名字叫 forEach, 它接收一个 Comsumer 类型的参数如名字所说, Consumer 的实例正是通过它的 accept 方法消费传递给它的对象的我们用一个很熟悉的匿名内部类的语法来使用下这个 forEach 方法:
- friends.forEach(new Consumer<String>() { public void accept(final String name) {
- System.out.println(name); }
- });
我们调用了 friends 集合上的 forEach 方法, 给它传递了一个 Consumer 的匿名实现这个 forEach 方法从对集合中的每一个元素调用传入的 Consumer 的 accept 方法, 让它来处理这个元素在这个示例中我们只是打印了一下它的值, 也就是这个名字 我们来看下这个版本的输出结果, 和上两个的结果 是一样的:
- Brian
- Nate
- Neal
- Raju
- Sara
- Scott
我们只改了一个地方: 我们抛弃了过时的 for 循环, 使用了新的内部迭代器好处是, 我们不用指定如何迭代这个集合, 可以更专注于如何处理每一个元素缺点是, 代码看起来更啰嗦了这简直要把新的编码风格带来的喜悦冲的一干二净了所幸的是, 这个很容易改掉, 这正是 lambda 表达式和新的编译器的威力大展身手的时候了我们再做一点修改, 把匿名内部类换成 lambda 表达式
friends.forEach((final String name) - >System.out.println(name));
这样看起来就好多了代码更少了, 不过我们先来看下这是什么意思这个 forEach 方法是一个高阶函数, 它接收一个 lambda 表达式或者代码块, 来对列表中的元素进行操作在每次调用的时候 , 集合中的元素会绑定到 name 这个变量上底层库托管了 lambda 表达式调用的活它可以决定延迟表达式的执行, 如果合适的话还可以进行并行计算 这个版本的输出也和前面的一样
- Brian
- Nate
- Neal
- Raju
- Sara
- Scott
内部迭代器的版本更为简洁而且, 使用它的话我们可以更专注每个元素的处理操作, 而不是怎么去遍历这可是声明式的
不过这个版本还有缺陷一旦 forEach 方法开始执行了, 不像别的两个版本, 我们没法跳出这个迭代 (当然有别的方法能搞定这个) 因此, 这种写法在需要对集合里的每个元素处理的时候比较常用后面我们会介绍到一些别的函数可以让我们控制循环的过程
lambda 表达式的标准语法, 是把参数放到 () 里面, 提供类型信息并使用逗号分隔参数 Java 编译器为了解放我们, 还能自动进行类型推导不写类型当然更方便了, 工作少了, 世界也清静了下面是上一个版本去掉了参数类型之后的:
friends.forEach((name) - >System.out.println(name));
在这个例子里, Java 编译器通过上下文分析, 知道 name 的类型是 String 它查看被调用方法 forEach 的签名, 然后分析参数里的这个函数式接口接着它会分析这个接口里的抽象方法, 查看参数的个数及类型即便这个 lambda 表达式接收多个参数, 我们也一样能进行类型推导, 不过这样的话所有参数都不能带参数类型; 在 lambda 表达式中, 参数类型要么全不写, 要写的话就得全写
Java 编译器对单个参数的 lambda 表达式会进行特殊处理: 如果你想进行类型推导的话, 参数两边的括号可以省略掉
friends.forEach(name - >System.out.println(name));
这里有一点小警告: 进行类型推导的参数不是 final 类型的在前面显式声明类型例子中, 我们同时也把参数标记为 final 的这样能防止你在 lambda 表达式中修改参数的值通常来说, 修改参数的值是个坏习惯, 这样容易引起 BUG, 因此标记成 final 是个好习惯不幸的是, 如果我们想使用类型推导的话, 我们就得自己遵守规则不要修改参数, 因为编译器可不再为我们保驾护航了
走到这步可费了老劲了, 现在代码量确实少了一点不过这还不算最简我们来体验下最后这个极简版的
friends.forEach(System.out: :println);
在上面的代码中我们用到了一个方法引用我们用方法名就可以直接替换整个的代码了在下节中我们会深入探讨下这个, 不过现在我们先来回忆下 Antoine de Saint-Exupéry 的一句名言: 完美不是无法再增添加什么, 而是无法再去掉什么
lambda 表达式让我们能够简洁明了的进行集合的遍历下一节我们会讲到它如何使我们在进行删除操作和集合转化的时候, 也能够写出如此简洁的代码
来源: http://www.phperz.com/article/16/0717/235771.html