背景
自从lambda表达式成为Java语言的一部分之后,Java集合(Collections)API就面临着大幅变化。而 JSR 355(规定了 Java lambda 表达式的标准)的正式启用更是使得 Java 集合 API 变的过时不堪。尽管我们可以从头实现一个新的集合框架(比如“Collection II”),但取代现有的集合框架是一项非常艰难的工作,因为集合接口渗透了 Java 生态系统的每个角落,将它们一一换成新类库需要相当长的时间。因此,我们决定采取演化的策略(而非推倒重来)以改进集合 API:
- 为现有的接口(例如 Collection,List 和 Stream)增加扩展方法;
- 在类库中增加新的 流(stream,即
java.util.stream.Stream)抽象以便进行聚集(aggregation)操作; - 改造现有的类型使之可以提供流视图(stream view);
- 改造现有的类型使之可以容易的使用新的编程模式,这样用户就不必抛弃使用以久的类库,例如 ArrayList 和 HashMap(当然这并不是说集合 API 会常驻永存,毕竟集合 API 在设计之初并没有考虑到 lambda 表达式。我们可能会在未来的 JDK 中添加一个更现代的集合类库)。
除了上面的改进,还有一项重要工作就是提供更加易用的并行(Parallelism)库。尽管 Java 平台已经对并行和并发提供了强有力的支持,然而开发者在实际工作(将串行代码并行化)中仍然会碰到很多问题。因此,我们希望 Java 类库能够既便于编写串行代码也便于编写并行代码,因此我们把编程的重点从具体执行细节(how computation should be formed)转移到抽象执行步骤(what computation should be perfomed)。除此之外,我们还需要在将并行变的 容易(easier)和将并行变的 不可见(invisible)之间做出抉择,我们选择了一个折中的路线:提供 显式(explicit)但 非侵入(unobstrusive)的并行。(如果把并行变的透明,那么很可能会引入不确定性(nondeterminism)以及各种数据竞争(data race)问题)
内部迭代和外部迭代(Internal vs external iteration)
集合类库主要依赖于 外部迭代(external iteration)。Collection 实现 Iterable 接口,从而使得用户可以依次遍历集合的元素。比如我们需要把一个集合中的形状都设置成红色,那么可以这么写:
for (Shape shape : shapes) {
shape.setColor(RED);
}
这个例子演示了外部迭代:for-each 循环调用 shapes 的 iterator() 方法进行依次遍历。外部循环的代码非常直接,但它有如下问题:
Java 的 for 循环是串行的,而且必须按照集合中元素的顺序进行依次处理;
集合框架无法对控制流进行优化,例如通过排序、并行、短路(short-circuiting)求值以及惰性求值改善性能。
尽管有时 for-each 循环的这些特性(串行,依次)是我们所期待的,但它对改善性能造成了阻碍。
我们可以使用 内部迭代(internal iteration)替代外部迭代,用户把对迭代的控制权交给类库,并向类库传递迭代时所需执行的代码。下面是前例的内部迭代代码:
shapes.forEach(s -> s.setColor(RED));
尽管看起来只是一个小小的语法改动,但是它们的实际差别非常巨大。用户把对操作的控制权交还给类库,从而允许类库进行各种各样的优化(例如乱序执行、惰性求值和并行等等)。总的来说,内部迭代使得外部迭代中不可能实现的优化成为可能。
外部迭代同时承担了 做什么(把形状设为红色)和 怎么做(得到 Iterator 实例然后依次遍历)两项职责,而内部迭代只负责 做什么,而把 怎么做 留给类库。通过这样的职责转变:用户的代码会变得更加清晰,而类库则可以进行各种优化,从而使所有用户都从中受益。
什么是流
流是Java API的新成员,它允许你以声明性方式处理数据(通过查询语句来表达,而不是临时编写一个实现)。简短的定义就是”从支持数据处理操作的源生成的元素序列”。进一步剖析:
- 元素序列——就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储的访问元素(如ArrayList和LinkList)。但流的目的在于表达计算,比如你前面见到的filter、sorted和map。集合讲的是数据,流讲的是计算。
- 源——流会使用一个提供数据的源,如集合、数据或输入/输出资源。请注意,从有序集合生成流时会保留原有的顺序。从来列表生成的流,其元素顺序与列表一致。
- 数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可以并行执行。
此外,流操作有两个重要的特点。 - 流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线,流水线的操作可以看做对数据源进行数据库试查询
内部迭代——与使用迭代器显示迭代的集合不同,流的迭代操作是在背后进行的。
此外,流还可以通明地并行处理,你无需写任何多线程代码。
我们先来看一下流的使用体验一下流带来的便利:
首先是Java 7中的集合遍历处理数据代码:
List<Dish> lowCaloricDishes = new ArrayList<>();
for(Dish d: menu){
if(d.getCalories() < 400){
lowCaloricDishes.add(d);
}
}
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
public int compare(Dish d1, Dish d2){
return Integer.compare(d1.getCalories(), d2.getCalories());
}
});
List<String> lowCaloricDishesName = new ArrayList<>();
for(Dish d: lowCaloricDishes){
lowCaloricDishesName.add(d.getName());
}
如果用Java 8来实现上面的操作呢?我们来看下代码:
List<String> lowCaloricDishesName =
menu.stream()
.filter(d -> d.getCalories() < 400)
.sorted(comparing(Dish::getCalories))
.map(Dish::getName)
.collect(toList());
可以看到代码很简洁,对于数据的处理也是一口气直截了当,极大的提高了开发效率。如果数据量很大想要提升效率,那么就可以使用多和架构并行执行这段代码,只需要将stream换成parallelStream,如下
List<String> lowCaloricDishesName =
menu.parallelStream()
.filter(d -> d.getCalories() < 400)
.sorted(comparing(Dishes::getCalories))
.map(Dish::getName)
.collect(toList());
在这里我们只是了解一下并行架构。我们再来看一下stream带来的好处:
- 代码是以声明式方式写的:说明想要完成什么而不是说明如何实现一个操作,利用lamdba表达式是你的代码简单明了,不需要写过多的for循环。
- 你可以把几个基础操作链接起来,来表达复杂的数据处理流水线,同时保持代码清晰可读。filter的结果被传递给了sort方法,在传给map方法,最后传给collect方法。
需要注意流只能遍历一次
List<String> title = Arrays.asList("Java8", "In", "Action");
Stream<String> s = title.stream();
s.forEach(System.out::println);
s.forEach(System.out::println);//执行第二次时会抛出异常
流中的操作
Stream中的stream接口定义了很多操作。他们可以分为两大类:可以连起来的流操作成为中间操作,如filter、map和limit可以连成一条流水线;关闭流的操作称为中断操作:如collect出发流水线执行并关闭它。
中间操作:
- filter——接受Lambda,从流中排出某些元素
- map——接受一个lambda,将元素转换成其他形式或提取信息
- limit——截断流,使其元素不超过给定数量。
- sorted——排序
- distinct——过滤重复元素
操作 | 类型 | 返回类型 | 操作参数 | 函数描述符 |
---|---|---|---|---|
filter | 中间 | Stream< T > | Predicate< T > | T -> boolean |
map | 中间 | Stream< T > | Function< T, R > | T -> R |
limit | 中间 | Stream< T > | ||
sorted | 中间 | Stream< T > | Compartator< T > | (T, T) -> int |
distinct | 中间 | Stream | ||
skip(n) | 中间 | Stream< T > |
注意:limit与skip互补
中断操作
操作 | 类型 | 返回类型 | 使用的类型/函数式接口 | 目的 |
---|---|---|---|---|
forEach | 终端 | Consumer | 消费流中的每个元素并对其应用Lambda。这一操作返回void | |
count | 终端 | long | 返回流中元素的个数。这一操作返回long | |
collect | 终端 | R | Collector | 把流归约成一个集合,比如List、Map甚至Integer。 |
anyMatch | 终端 | boolean | ||
noneMatch | 终端 | boolean | ||
allMatch | 终端 | boolean | ||
findAny | 终端 | Optinal< T > | ||
findFirst | 终端 | Optinal | ||
reduce | 终端 | Optinal | BinaryOperator |
怎么判断是终端操作?
答:看最后一个操作返回类型是否为stream类型。
流中的映射
一个非常常见的数据处理套路就是从某些对象中选择信息。比如在sql里,你可以从表中选择一列。Stream API也可以通过map和flatMap方法提供了类似的工具。
流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的席位差别在于它是“创建一个新版本”而不是去“修改”)
String[] arrayOfWords = {"Goodbye", "world"};
Stream<String> streamOfWords = Arrays.stream(arrayOfWords);
words.stream().map(word -> word.split("")).map(Arrays::stream).distinct().collect(toList()); //在这里每个数组都会变成一个单独的流
流的扁平化
使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流
List< String > uniqueCharacters = words.stream().map(w -> w.split("")).flatMap(Arrays::stream).distinct().collect(toList()); //在这里每个生成流扁平化为单个流
简而言之就是flatmap方法让你把一个流中的每个值都转换成另一个流。
查找和匹配
StreamAPI通过allMatch、anyMatch、noneMatch、findFirst和findAny方法提供了这样的工具。
1.检查为此是否至少匹配一个元素
if(menu.stream().anyMatch(Dish::isVegetarian)){
System.out.println("The menu is (somewhat) vegetarian friendly!!");
}
anyMatch方法返回一个boolean,因此是一个最终操作。
2.检查谓词是否至少匹配所有元素
allMatch方法的工作原理和anyMatch类似,但它会看看流中的元素是否都能匹配给定的谓词。
boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);
noneMatch
和allMatch相对的是noneMatch。它可以确保流中没有任何元素与给定的谓词匹配。
boolean isHealthy = menu.stream().noneMatch(d -> d.getCalories() >= 1000);
查找元素
findAny方法将返回当前流中的任意元素。
Optional<Dish> dish = menu.stream().filter(Dish::isVegetarian).findAny(); //使用optinal是因为可能什么元素都没找到
查找第一个元素
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree = someNumbers.stream().map(x -> x * x).filter(x -> x % 3 == 0).findFirst(); // 9
何时使用findFirst和findAny
如果并不关心返回的元素是哪个则用findAny,在并行的情况下。findAnyx效率高限制少
归约
1.求和
reduce: 一个初始值,这里是0; 一个BinaryOperator来将两个元素结合起来产生一个新值,这里我们用的是
lambda (a, b) -> a + b
int product = numbers.stream().reduce(1, (a, b) -> a * b);
int sum = numbers.stream().reduce(0, Integer::sum);
reduce还有一个重载的变体,它不接受初始值,但是会返回一个Optional对象:
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));
并行求和:只需要将stream改为parallelStream
原始类型流特化
java 8 引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和LongStream,分别将六中的元素特化为int,long和double,从而避免了暗含的装箱成本
1.映射到数值流
将流转换为特化版本的常用方法是mapToInt、 mapToDouble和mapToLong。这些方法和前面说的map方法的工作方式一样,只是它们返回的是一个特化流,而不是Stream。
例如,你可以像下面这样用mapToInt对menu中的卡路里求和:
int calories = menu.stream().mapToInt(Dish::getCalories).sum();
2.转换回对象流
同样,一旦有了数值流,你可能会想把它转换回非特化流。
IntStream intStream = menu.stream().mapToInt(Dish::getCalories); //将Stream转换为数值流
Stream stream = intStream.boxed(); //将数值流转换为Stream
3.默认值OptinalInt
对于三种原始流特化,也分别有一个Optional原始类型特化版本: OptionalInt、 OptionalDouble和OptionalLong。
例如,要找到IntStream中的最大元素,可以调用max方法,它会返回一个OptionalInt:
OptionalInt maxCalories = menu.stream().mapToInt(Dish::getCalories).max();
现在,如果没有最大值的话,你就可以显式处理OptionalInt去定义一个默认值了:
int max = maxCalories.orElse(1);
4.数值范围
比如,假设你想要生成1和100之间的所有数字。Java 8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围:range和rangeClosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range是不包含结束值的,而rangeClosed则包含结束值。
IntStream evenNumbers = IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0); 这里生产1到100中的50个偶数,而使用range则只有49个,不包括100
构建流
1.由值创建流
可以使用静态方法Stream.of创建一个流。它接受任意数量的参数。如:
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
你可以使用empty得到一个空流,如下所示:
Stream<String> emptyStream = Stream.empty();
2.由数组创建流
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
3.由文件生成流
long uniqueWords = 0;
try(Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){
uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
.distinct()
.count();
}
catch(IOException e){
}
该方法中流会自动关闭
4.由函数生成流:创建无限流
Stream API提供了两个静态方法来从函数生成流: Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。
1.迭代
Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println);
2.生成
与iterate方法类似, generate方法也可让你按需生成一个无限流。但generate不是依次对每个新生成的值应用函数的。它接受一个Supplier类型的Lambda提供新的值。我们先来看一个简单的用法:
Stream.generate(Math::random).limit(5).forEach(System.out::println);