- 前面一篇 Java 8 的 groupingBy 能否产生空的 Map 分组 是提出来的思考,本篇就是上一篇的答案。
由于在 Java 8 中用 Collectors.groupingBy 对 List 进行分组时每个组里都必须存在元素,也就是Stream<Person> stream = Stream.of(new Person("Tom", "male"), new Person("Jerry", "male"));
只能得到结果
System.out.println(stream.collect(Collectors.groupingBy(person -> person.gender)));{male=[Tom, Jerry]}
而无法表示存在其他 gender 的可能性,并且 female=[] 的情况,即想要结果{male=[Tom, Jerry], female=[]}
如果想得到以上的结果该当如何呢? stream.collect() 接受一个 Collector, Collectors 中只是定义了许多常用的 Collector 实现,如果不够用的话我们可以实现自己的 Collector. 下面就来定义一个 GroupingWithKeys, 它需要实现 java.util.stream.Collector 接口,有五个接口方法. 事成之后我们写 Read More - 我们在 Java 8 之前用 for-loop 对 List 进行分组的时候,可能会要求产生空的分组。例如对 List<Person> 按性能进行分组,即使给定的 List<Person> 中全是 male, 我们也想得到 Map 包含两个 Key
{male=[person1, person2], female=[]}
而不只是{male=[person1, person2]}
这两种表示略有区别,第一种方式暗示着有另一种可能性。看看 for-loop 如何对 List<Person> 进行分组。
类 Person 代码 Read More - 在 Java 8 之前如果我们要找到集合中第一个匹配元素,要使用外部循环,如下面方法 findFirstMatch() 如果找到一个大于 3 的数字立即返回它,否则返回 null
public Integer findFirstMatch() {
因为在 for 循环中找到第一个大于 3 的数字是 4, 并且立即返回,所以不管集合 integers 再大,也不会遍历整个集合。
List<Integer> integers = Arrays.asList(1, 4, 2, 5, 6, 3);
for(int i: integers) {
if(i > 3) return i;
}
return null;
}
注:不要纠结于上面示例方法的实际用途,实际上集体和匹配条件都该通过参数传入方法的,这里只作演示循环。
那么我们来到 Java 8 之后用 Stream API 该如何实现,翻遍了 Stream API, 能过滤元素的操作也就是 filter 方法,于是尝试这样的写法 Read More - 可能会把捕获外部变量的 Lambda 表达式称为闭包,那么 Java 8 的 Lambda 可以捕获什么变量呢?
- 捕获实例或静态变量是没有限制的(可认为是通过 final 类型的局部变量 this 来引用前两者)
- 捕获的局部变量必须显式的声明为 final 或实际效果的的 final 类型
回顾一下我们在 Java 8 之前,匿名类中如果要访问局部变量的话,那个局部变量必须显式的声明为 final,例如下面的代码放在 Java 7 中是编译不过的
Java 7 要求 version 这个局部变量必须是 final 类型的,否则在匿名类中不可引用。
Read More - 由于 Java 对集合的函数式操作并非原生态的,必须对得到的 stream() 进行过滤等操作,之后还是一个 stream(),一般我们最后返回给调用者需还原为相应的集合。这无法与 Scala 的
for ... yield操作相比。例如下面在使用 Stream API 过滤获得所有大于 3 的数字之后,方法的返回值还应该还原为 List<Integer>, 这个需求非常自然1List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); 2List<Integer> greaterThan3(list) { 3 Stream<Integer> streamOfInteger = list.stream().filter( i -> i > 3); 4 return streamOfInteger.ToIntegerList......; 5}
我们这儿的问题就是如何把上面的 streamOfInteger 转换为 List<Integer>, 有以下几种办法 Read More - Java 8 的 Lambda 表达式的实现方式还是基于已有的字节码指令,由 Lambda 表达式的方法签名结合上下文并经由 SAM 推断出正确的类型来。Java 8 的 Lambda 完整书写格式是
(type parameter1 [type parameter2, ...type parametern]) -> { statements here }
这种标准格式所表示的就是方法签名。
虽不及其他语言的 Lambda 表达式,像 Swift, Scala 可省略参数部分,可用默认参数名$0,$1, 或_, 但 Java 8 的 Lambda 还是可以进行酌情简化- 参数类型总是可省略 --
(x, y) -> { x + y; } - 参数为一个时,参数括号可省略 --
x -> { System.out.println(x); } - 语句为一条时,可省略大括号, 并且语句后不要分号 --
x -> System.out.println(x) - 上面更进一步,如果是单条 return 语句,必须把 return 关键字去掉 --
x -> "Hello " + x - 就差一点,参数部分总是不能省,无参必须写成
() -> System.out.println("hi") - Java 8 中若要近似的实现无参数部分写法,那就是方法引用了 --
System.out::println
- 参数类型总是可省略 --
阅读完递归这一章我仍然无法理解递归为何被作为函数式编程的一个特性。只是递归有时候会简洁,但没弄好却更让人犯迷糊,用循环的话就四平八稳,总是比较好理解。当然有些情况用循环去思考确实难于处理。递归方法内部调用方法本身通常也比较慢,因为需要频繁的保存现场,创建栈帧,恢复现场,所以受内存限制,递归到达一定深度容易导致堆栈溢出。
章中还说到递归的一个优点在于它只需处理输入的值,而循环则需关注整个集合本身。这个该如何理解呢?在每一次递归调用的时候传入的都是当前需要被处理的数据。
先来比较下循环变换成递归的一个简单示例,用 Scala 实现 Read More- Java 5 引入了泛型,这是一次重大的改进,从此集合中的东西不需要每次显式的去转型。不过 Java 5 还不具备类型推断的能力,所以声明泛型必须写成
List<String> list = new ArrayList<String>();
一直到 Java 6 也是如此。自 Java 7 起泛型增强为可根据声明类型进行推断,所以 Java 7 中可以这么写
List<String> list = new ArrayList<>(); //<> 中的参数可省略,如果类型参数多, 或多层嵌套时很省事
或
List<String> list = Collections.emptyList(); //见 Java 泛型 -- 依据声明的变量类型自动推断
Java 8 开始对泛型类型推断又进一步增强:可根据方法上下文进行推断,例如下面的代码在 Java 7 下编译不过1List<String> list = new ArrayList<>(); 2list.addAll(new ArrayList<>()); //根据 list.addAll() 上下文推断要创建的类型是 new ArrayList<String>()
Read More 
不可变性(Immutable--不是不让谁变性) 在设计模式中似乎不那么显眼,但是它在函数式编程却起着举足轻重的意义了。我们都知道 Java 的 String 是设计为 Immutable 的,对 String 改变都会产生新的 String 实例,这有助于共享常量池中字符串与并发。
这里再回顾上一篇关于纯虚函数(输入决定输出,无副作用函数,非 C++ 中可重载无 Body 的函数),由此可知,Immutable 也有助于实现纯虚函数,因为一个不可变实例被传入到某个函数中,该实例的内部状态是无法被修改的,也就是说该函数是无副作用的。
先来感受一下可变类型在并发环境下的一个弊端,看个例子,可变的 Customer 类有三个属性 name, email 和 age, 它还有一个更新方法 updateNameAndEmail() Read More
函数式编程第二式,纯函数 (Pure Functions). 何谓纯函数,纯函数就是数据库的函数一样没有副作用,不修改对象的内部状态,或者说只进行计算。给定什么输入,永远得到相同的输出,即输出只依赖于输入。
纯函数有什么好处呢?极其容易测试,只需要给定一组输入,对输出进行断言。如果是带副作用的方法,它修改了某个私有的属性,很难进行状态判定。
Scala 基于统一访问原则,属性与方法不需要那么明晰,所以属性与方法名是不能重名的。它对有无副作用存在这么一个约定:
空括号方法 (empty-paren method), 如 def width() {...} 它是有副作的。这样调用 obj.width()
无参方法 (parameterless method), 如 def width {...}, 它是无副作的。这样调用 obj.width, 这和使用属性一致形式
Java 也有类似的约定,get 开头的或 getter 方法往往是无副作用的。要是有人偏偏在 getName() 方法里修改了对像的属性而引入了 Bug,那只会让人唏嘘不已。
方法的副作用一般有哪些呢?- 输出内容到屏幕
- 写数据到文件或数据库
- 修改了对象的属性