第三章:Java集合类库的批量数据操作
引入批量数据操作的目的是应用lambda函数来实现包含并行操作在内的多种数据处理功能,而支持并行数据操作是其关键内容。这个并行操作是在Java7 java.util.concurrency的Fork/Join机制上实现的。
批量操作接口
正如最初在变更说明书上说的,引入批量操作接口的目的是:
给Java集合类库增加批量操作数据的支持。通常称这种批量数据操作为 “Java中的filter/map/reduce”。批量数据操作有串行(在当前线程上)和并行(使用多线程)两种操作模式。一般用Lambda函数来定义对数据的操作。
由于Lambda表达式已经应用到Java语言和新集合API中,因此我们可以更加高效地利用底层平台的并行特性。
流式API
JDK中已经增加了一个新包java.util.stream,使我们能够使用Java8集合类库执行类似filter/map/reduce的操作。
这个流式API使我们能在数据流之上编写串行或者并行的操作,如下所示:
List persons = .. //串行操作 Stream stream=persons.stream(); //并行操作 Stream parallelStream=persons.parallelStream(); |
处理一条数据流有点像迭代,只是一条数据流只能遍历一次,然后就结束了,然而数据流也可能是无尽的,这通常是说流是“断断续续”的-我们不能提前知道有多少流元素要处理。
java.util.stream.Stream接口是批量数据操作的入口。我们在拿到流式接口的引用后,就可以使用集合类库做些有趣的事情了。
关于数据流API要特别注意一点,就是在数据处理过程中不会改动源数据。这是考虑到数据源可能根本不存在,或者是由于原始数据还要在代码的其它地方使用。
Stream源码
数据流接口可以使用多种数据源来处理数据,使用这些流式方法扩展标准JDK类库,可以获得更好的数据处理体验。
毫无疑问,首选的用于流式操作的数据源是集合(collections),如下所示:
List list; Stream stream=list.stream(); |
另外,还有一种有趣的数据源是所谓的生成器(generators),如下所示:
Random random=new Random(); Stream randomNumbers=Stream.generate(random::nextInt); |
有几种工具方法可以设置操作多大范围的数据:
IntStream range =IntStream.range(0, 50, 10); range.forEach(System.out::println);// 0, 10, 20, 30, 40 |
标准类库中也已经存在一些类可以充当数据源。例如, Random类已经扩展了一些有用的方法,如下所示:
newRandom() .ints()// 随机生成一条的整数数据流 .limit(10)// 我们只要10个随机整数 .forEach(System.out::println); |
中间操作
中间操作用来描述在数据流之上执行的转换操作(可以理解为一种映射操作)。filter() 和 map()是不错的中间操作的例子,它们的返回值是Stream类型,因此可以允许链式执行多个中间操作。
以下是一些有用的中间操作:
- filter 排除所有不满足条件的元素,具体条件通过Predicate接口来定义;
- map 执行元素的映射转换,具体的映射方式使用Function接口定义;
- flatMap 通过另外一种 Stream接口将每个流元素转换成零个或者更多流元素
- peek 对遇到的每个流元素执行一些操作。
- distinct 根据流元素的equals(..)结果排除所有重复的元素
- sorted 使后续操作中的流元素强制按Comparator定义的比较逻辑排列。
- limit 使后续操作只能看到有限数量的元素。
- substream 使后续操作只能看到某个范围内的元素(使用索引)。
中间操作中的一些,如sorted, distinct 和 limit等是有状态的,有状态的意思是这些操作返回的数据流结果依赖之前进行的操作的结果。另外,正如Javadoc上讲的,所有中间操作是“延迟执行(lazy)”的。接下来让我们更详细的了解一些中间操作。
Filter
数据流过滤是我们需要做的初始且固有的操作。Stream接口中有一个filter(..)方法,它以SAM类型的Predicate接口为参数,Predicate接口使我们能够使用Lambda表达式来定义过滤规则:
List persons = ... Stream personsOver18 =persons.stream().filter(p ->p.getAge()>18); |
Map
Map操作允许我们使用一个Function接口,Function接口接收一种类型的参数,然后返回其他类型。首先,我们来看看在传统方式下,使用匿名内部类是怎么定义Map操作的:
Stream students =persons.stream() .filter(p ->p.getAge()>18) .map(new Function<Person, Student>(){ @Override
public Student apply(Person person){ return new Student(person); }
}
); |
现在把上面的实现改用Lambda表达式语法,代码如下:
Stream map =persons.stream() .filter(p ->p.getAge()>18) .map(person ->new Student(person)); |
既然作为map(..)方法参数的Lambda表达式仅仅是使用了参数(person),而没有用此参数做其他操作,那么我们可以更进一步地把Lambda表达式改写为方法引用:
Stream map =persons.stream() .filter(p ->p.getAge()>18) .map(Student::new); |
终结操作
数据流处理过程通常包含下面几个步骤:
1. 从某个数据源头获取到数据流;
2. 执行像filter,map等等这样的一个或者多个中间操作;
3. 执行一个终结操作.
终结操作必须是最后一个在数据流上执行的操作。一旦执行了终结操作,数据流就“消耗完了”,不可再用了。
现有如下一些可用的终结操作类型:
- reducers ,如reduce(..), count(..), findAny(..), findFirst(..),可以终结数据流处理过程。根据意图,终结操作可以是“短路”操作(不用完整的遍历所有数据流)。例如,findFirst(..)在一遇到匹配的元素就会马上终结数据流的处理过程。
- collectors,就像其名字表示的,用来把处理过的元素收集到一个结果集中。
- forEach 对数据流中的每一个元素执行某个操作。
- iterators ,如果上面的操作都不能满足我们的需求,那么还是采用iterators这种传统的集合操作方式。
其中最有趣的终结操作类型是所谓的“收集器(collectors)”:
收集器
虽然抽象数据流本质上是连续的,而且我们可以定义数据流上的操作,但是要获得最终的结果,我们需要以某种方式收集到数据。数据流API提供了一些所谓的“终结”操作,而collect() 方法就是终结操作的其中一个,它使我们能够收集结果数据。
List students =persons.stream() .filter(p ->p.getAge()>18) .map(Adult::new) .collect(new Collector<Student, List>(){ ... }); |
幸好你在大多数情况下不需要自己实现Collector接口。为了方便起见,已经实现了一个Collectors工具类:
List students =persons.stream() .filter(p ->p.getAge()>18) .map(Student::new) .collect(Collectors.toList()); |
那万一我们想使用特定的收集逻辑来收集结果,可以像下面这样做:
List students =persons.stream() .filter(p ->p.getAge()>18) .map(Student::new) .collect(Collectors.toCollection(ArrayList::new)); |
并行和串行
新式数据流API 的一个有意思的特性是它不要求从头到尾都一定是并行操作或者串行操作。一开始并发地处理数据,然后切换到串行处理,并回到处理流程中的任意步骤,这是可以实现的,如下所示:
List students =persons.stream() .parallel() .filter(p ->p.getAge()>18)// 并发地执行过滤操作 .sequential() .map(Student::new) .collect(Collectors.toCollection(ArrayList::new)); |
数据处理流程中的并发操作会自治地管理自身,不需要我们来处理并发问题,这简直太酷了。
总结和一则告别漫画
我们在本文中讲了不久就要发布的java8的三个的主题:
1. Lambd表达式
2. Default 方法
3. Java集合的批量数据操作
就像我们看到的,Lambda表达式极大地提升的代码可读性并使Java语言更加具有表现力,尤其当我们使用新增的数据流API时。相应地,Default方法对API升级至关重要,它用来把Lambda表达式集成到集合API中,为我们使用提供便利。然而,所有新特性的终极目标却是引入并行类库和无缝地利用多核硬件的优势。
长话短说,JVM本身是一项伟大的工程,无论你信不信,Java这个平台依然会活力无限。这些新的变化,将使我们能够以更加高效的方式充分利用Java平台和Java语言,另外也会绝好地给予批评Java的人们更多的东西去琢磨琢磨。
ANTON ARHIPOV,ZeroTurnaround公司的JRebel产品负责人。他热爱Java,vim和IntelliJ。职业兴趣包括编程语言、中间件和建模。Anton喜爱喝茶但不喝咖啡。他的tweeter账号是@antonarhipov,也可以在 LinkedIn上找到他。