流使程序员得以站在更高的抽象层次上对集合进行操作。
从外部迭代到内部迭代
外部迭代
for循环属于是外部迭代,其工作原理:首先调用iterator方法,产生一个新的Iterator对象,进而控制整个迭代过程,这就是外部迭代。迭代过程通过显式调用Iterator对象的hasNext和next方法完成迭代。
下面两段代码是等效的,for的底层原理就是对迭代器进行封装。
int count=0;
Iterator<Artist> iterator=allArtists.iterator();
while(iterator.hasNext()) {
Artist artist=iterator.next();
if (artist.isFrom("London")) {
count++;
}
}
int count=0;
for (Artist artist : allArtists) {
if (artist.isFrom("London")) {
count++;
}
}
内部迭代
在Java中,可以通过调用stream()方法,返回内部迭代中的相应接口——Stream。
long count=allArtists.stream()
.filter(artist-> artist.isFrom("London"))
.count();
上面计算来自伦敦的艺术家人数时,每种操作都对应Stream接口的一个方法。
为了找出来自伦敦的艺术家,需要对Stream对象进行过滤:filter,过滤在这里是指“只保留通过某项测试的对象”。测试由一个函数完成,根据艺术家是否来自伦敦,该函数返回true或者false。
count()方法计算给定Stream里包含多少个对象。
求值方法
惰性求值方法:返回值是Stream,只描述Stream,最终不产生新集合的方法,例如filter、map。
及早求值方法:返回值是另一个值或为空,最终会从Stream产生值的方法,例如count、collect。
// 不会输出任何信息
allArtists.stream()
.filter(artist-> {
System.out.println(artist.getName());
return artist.isFrom("London");
});
// 输出艺术家姓名
long count=allArtists.stream()
.filter(artist-> {
System.out.println(artist.getName());
return artist.isFrom("London");
})
.count();
判断一个操作是惰性求值还是及早求值很简单:只需看它的返回值。如果返回值是Stream,那么是惰性求值;如果返回值是另一个值或为空,那么就是及早求值。
在进行流操作时,惰性求值和及早求值常常组合在一起使用,使用这些操作的理想方式就是形成一个惰性求值的链,最后用一个及早求值的操作返回想要的结果。
在涉及集合操作(如过滤,转换等,使用惰性求值方法)的场景中,通过惰性求值,我们能把这些操作串联起来,形成一条操作链,但只执行一次迭代(计算),即在真正需要结果的地方。这样能大大提高计算效率。
常用的流操作
of
Stream的of方法使用一组初始值生成新的Stream
List<String> collected=Stream.of("a", "b", "c");
collect(toList())
collect(toList())方法由Stream里的值生成一个列表,是一个及早求值操作。
List<String> collected=Stream.of("a", "b", "c")
.collect(Collectors.toList());
// 断言成功,collected=Arrays.asList("a", "b", "c")
assertEquals(Arrays.asList("a", "b", "c"), collected);
这段程序展示了如何使用collect(toList())方法从Stream中生成一个列表。
很多Stream操作都是惰性求值,因此调用Stream上一系列方法之后,还需要最后再调用一个类似collect的及早求值方法。
map
返回一个新流,新流由给定函数(入参
mapper,Function类型的函数式接口)对旧流的元素处理后的结果组成;这是一个惰性求值操作;
参数:mapper,是一个
Function类型的函数式接口(即Lambda表达式),会对旧流中的每一个元素进行处理;返回值:一个新流。
// 对旧流中的每一个元素进行大写转换
List<String> collected=Stream.of("a", "b", "hello")
.map(string-> string.toUpperCase())
.collect(toList());
// 断言成功,collected=asList("A", "B", "HELLO")
assertEquals(asList("A", "B", "HELLO"), collected);
传给map的Lambda表达式必须是Function接口的一个实例。下面是Function函数接口的实现。
......
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
......
}
filter
返回一个新流,新流由使给定函数(入参
predicate,Predicate类型的函数式接口)返回true的旧流元素组成;这是一个惰性求值操作;
参数:predicate ,是一个函数式接口(即Lambda表达式),应用于旧流中的每一个元素,以确定是否应包含该元素;
返回值:一个新流。
// 如果字符串首字母为数字,则返回true,该字符串会被保留下来。
List<String> beginningWithNumbers
=Stream.of("a", "1abc", "abc1")
.filter(value-> isDigit(value.charAt(0)))
.collect(toList());
// 断言成功,beginningWithNumbers=asList("1abc")
assertEquals(asList("1abc"), beginningWithNumbers);
filter和map很像,filter接受一个函数作为参数,该函数用Lambda表达式表示。
如果输入的元素使Lambda表达式返回值为true,那么该元素会被保留下来。
若要重构遗留代码,for循环中的if条件语句就是一个很强的信号,可用filter方法替代。
下面是Predicate函数接口的实现。
......
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
......
}
flatMap
flatMap方法不仅可以用于替换Stream中的值(map的功能),还能将多个Stream连接成一个Stream。
接下来我们展现下flatMap将多个流连接成一个流的功能。
List<Integer> together=Stream.of(asList(1, 2), asList(3, 4))
.flatMap(numbers-> numbers.stream())
.collect(toList());
// 断言成功,together=asList(1, 2, 3, 4)
assertEquals(asList(1, 2, 3, 4), together);
max和min
查找Stream中的最大或最小元素,首先要考虑的是用什么作为排序的指标。
以查找专辑中的最短曲目为例,排序的指标就是曲目的长度。为了让Stream对象按照曲目长度进行排序,需要传给它一个Comparator对象。
List<Track> tracks=asList(new Track("Bakai", 524),
new Track("Violets for Your Furs", 378),
new Track("Time Was", 451));
Track shortestTrack=tracks.stream()
.min(Comparator.comparing(track-> track.getLength()))
.get();
// 断言成功,shortestTrack=tracks.get(1)
assertEquals(tracks.get(1), shortestTrack);
reduce
reduce操作可以实现从一组值中生成一个值。count、min和max这些方法都是reduce操作。
reduce模式:reduce= initialValue+reducer,
下面代码展示了使用reduce进行求和的过程,其中initialValue=0,reducer是 (acc, element)-> acc+element),其是一个Lambda表达式,
它执行求和操作,Lambda表达式入参:当前元素element和acc,Lambda表达式返回值类型是:BinaryOperator(上一讲的【如何理解下面的问题#2】对该函数式接口做过介绍),
Lambda表达式将两个参数相加,acc是累加器,保存着当前的累加结果。
int count=Stream.of(1, 2, 3)
.reduce(0, (acc, element)-> acc+element);
// 断言成功,count=6
assertEquals(6, count);
下面代码是展开reduce操作的过程,accumulator是一个BinaryOperator类型的累加器,入参:累加器acc和当前元素element。
BinaryOperator<Integer> accumulator=(acc, element)-> acc+element;
int count=accumulator.apply(
accumulator.apply(
accumulator.apply(0, 1),
2),
3);
如何理解下面的问题
Stream API并不会改变集合的内容,而是描述出Stream里的内容
在使用Java 8的Stream API进行编程时,我们并不会直接操作或修改原始数据集合,而是通过描述我们想要如何处理这些数据,然后让Stream API帮助我们实现这些处理。
来看一个简单的例子:
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squaredNums = nums.stream()
.map(x -> x * x)
.collect(Collectors.toList());
在上述代码中,我们并没有改变原有的nums列表的内容,而是创建了一个新的squaredNums列表来存放对原有列表中每个元素平方后的结果。在处理过程中,我们只是描述了我们想对原始列表中的每个元素做什么(即将每个元素平方),然后Stream API就会帮助我们完成这个操作。
因此,Stream的处理方式被称为函数式编程风格。它的好处在于,原始数据集合保持不变,有助于并发处理,以提高程序的性能。而且,处理过程的描述方式使得代码更易读、更易维护。