Java 8函数式编程实战(二)Stream 流
- Stream流
- 案例分析
- 常用流操作
- 概念回顾
- 迭代
- 求值
- 高阶函数
- 纯函数
- 进阶练习
Stream流
案例分析
常用流操作
编写一个求和函数,计算流中所有数之和。例如,int addUp(Stream< Integer > numbers);
//定义求和方法
public static int addUp(Stream<Integer> numbers) {
return numbers.reduce(0, (acc, x) -> acc + x);
}
举例:计算空集合、非空集合的Test方法,并进行断言
@Test
public void addsEmptyList() {
int result = addUp(Stream.empty());
assertEquals(0, result);
}
@Test
public void addsListWithValues() {
int result = addUp(Stream.of(1, 3, -2));
assertEquals(2, result);
}
上述代码Lambda表达式就是reducer,它执行求和操作,有两个参数:传入Stream中的当前元素和acc。将两个参数相加,acc是累加器,保存着当前的累加结果。当然如果你去看Strea源码,其实还有两外两种reduce方法的定义,如下所示:
@Test
public void addsListWithValues2(){
List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
int result = ints.stream().reduce((acc, x) -> acc + x).get();
System.out.println("ints sum is:" + result);
}
@Test
public void addsListWithValues3(){
List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
//与方式一相比设置了累加器的初始值
int result = ints.stream().reduce(0, (acc, x) -> acc + x);
assertEquals(55, result);
}
@Test
public void addsListWithValues4(){
List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
//与方式一、二相比可以改变返回值的数据类型
long result = ints.stream().reduce(0L,(a,b) -> a + b, (a,b)-> 0L );
assertEquals(55L, result);
}
为了更方便的理解整个求和的过程,我们可以将reduce操作展开,如下所示:
@Test
public void expandedReduce() {
// BEGIN expanded_reduce
BinaryOperator<Integer> accumulator = (acc, element) -> acc + element;
int count = accumulator.apply(
accumulator.apply(
accumulator.apply(0, 1),
2),
3);
// END expanded_reduce
assertEquals(6, count);
//进一步拆解
Integer apply01 = accumulator.apply(0, 1);
Integer apply02 = accumulator.apply(apply01, 2);
Integer apply03 = accumulator.apply(apply02, 3);
System.out.println(apply01);
System.out.println(apply02);
System.out.println(apply03);
}
通过这个例子我们大概对reduce的工作原理有了一定的了解,也对函数式编程有了一定的认识,那么函数式编程跟命令式编程的区别是什么呢?接下来我们通过个例子对比一下
@Test
public void countUsingReduceFor() {
// BEGIN count_using_reduce_for
int acc = 0;
for (Integer element : asList(1, 2, 3)) {
acc = acc + element;
}
assertEquals(6, acc);
// END count_using_reduce_for
}
编写一个函数,接受艺术家列表作为函数,返回一个字符串列表,其中包含艺术家的姓名和国籍;
public static List<String> getNamesAndOrigins(List<Artist> artists) {
return artists.stream()
.flatMap(artist -> Stream.of(artist.getName(), artist.getNationality()))
.collect(toList());
}
@Test
public void extractsNamesAndOriginsOfArtists() {
List<String> namesAndOrigins = getNamesAndOrigins(SampleData.getThreeArtists());
assertEquals(asList("John Coltrane", "US", "John Lennon", "UK", "The Beatles", "UK"), namesAndOrigins);
}
这个例子用到了 flatMap 方法,它解决的是:1.可将Stream替换值,2.然后将多个Stream连接成一个Stream。在分析flatMap操作之前,我们先说一下map操作,如下:
- 使用map操作将字符串转换成为大写形式
@Test
public void mapToUpperCase() {
// BEGIN map_to_uppercase
List<String> collected = Stream.of("a", "b", "hello")
.map(string -> string.toUpperCase()) // <1>
.collect(toList());
assertEquals(asList("A", "B", "HELLO"), collected);
// END map_to_uppercase
}
想必你看过map操作,就能明白flatMap操作也可以用一个新的值代替Stream中的值,不同的是,flatMap可以生成新的Stream对象取而代之。除此之外flatMap最终返回的结果不是一个个Stream流,而是将多个流进行了合并。下面我们通过一个案例,更好的说明下:
- 包含多个列表的Stream
@Test
public void flatMapCharacters() {
// BEGIN flatmap_characters
List<Integer> together = Stream.of(asList(1, 2), asList(3, 4))
.flatMap(numbers -> numbers.stream())
.collect(toList());
assertEquals(asList(1, 2, 3, 4), together);
// END flatmap_characters
}
这个例子可以更好的看出,通过调用stream方法,将每个列表转换成Stream对象,其余部分由flatMap方法处理,将两个Stream对象合并成一个输出。
编写一个函数,接受专辑列表作为参数,返回一个由最多包含3首歌曲的专辑组成的列表;
public static List<Album> getAlbumsWithAtMostThreeTracks(List<Album> input) {
return input.stream()
.filter(album -> album.getTrackList().size() <= 3)
.collect(toList());
}
@Test
public void findsShortAlbums() {
List<Album> input = asList(manyTrackAlbum, sampleShortAlbum, aLoveSupreme);
List<Album> result = getAlbumsWithAtMostThreeTracks(input);
assertEquals(asList(sampleShortAlbum, aLoveSupreme), result);
}
这个例子其实就是介绍filter操作的用法,它可以遍历数据并检查其中的元素,如果你已经很熟悉这个概念了,也可以直接看下个问题。啊哈!你还没有跳过,那么请一起看下面这个例子:假设要找出一组字符串中以数字开头的字符串,该怎么做呢?
- 命令式编程,使用for循环
@Test
public void imperativeStringsWithNumbers() {
// BEGIN strings_numbers_for
List<String> beginningWithNumbers = new ArrayList<>();
for (String value : asList("a", "1abc", "abc1")) {
if (isDigit(value.charAt(0))) {
beginningWithNumbers.add(value);
}
}
assertEquals(asList("1abc"), beginningWithNumbers);
// END strings_numbers_for
}
- 函数式编程,使用filter
@Test
public void functionalStringsWithNumbers() {
// BEGIN strings_numbers_filter
List<String> beginningWithNumbers
= Stream.of("a", "1abc", "abc1")
.filter(value -> isDigit(value.charAt(0)))
.collect(toList());
assertEquals(asList("1abc"), beginningWithNumbers);
// END strings_numbers_filter
}
概念回顾
迭代
迭代。修改如下代码,将外部迭代转换成内部迭代;
外部迭代:最传统的方法是用Iterator,当然还以用for i、增强for循环等等。这一类方法叫做外部迭代,意为显式地进行迭代操作,即集合中的元素访问是由一个处于集合外部的东西来控制的,在这里控制着循环的东西就是迭代器。请看下这个例子
public static int countBandMembersExternal(List<Artist> artists) {
// BEGIN COUNT_MEMBERS_EXTERNAL
int totalMembers = 0;
for (Artist artist : artists) {
Stream<Artist> members = artist.getMembers();
totalMembers += members.count();
}
// END COUNT_MEMBERS_EXTERNAL
return totalMembers;
}
- 可能这个例子不是很直观,那我们换个写法
public static int countBandMembersExternal2(List<Artist> artists) {
// BEGIN COUNT_MEMBERS_EXTERNAL
int totalMembers = 0;
for (Iterator<Artist> artistIterator = artists.iterator(); artistIterator.hasNext(); ) {
Stream<Artist> members = artistIterator.next().getMembers();
totalMembers += members.count();
}
// END COUNT_MEMBERS_EXTERNAL
return totalMembers;
}
- 如果这还不直观,那看这个
public static int countBandMembersExternal3(List<Artist> artists) {
// BEGIN COUNT_MEMBERS_EXTERNAL
int totalMembers = 0;
Iterator<Artist> artistIterator = artists.iterator();
while (artistIterator.hasNext()){
Stream<Artist> members = artistIterator.next().getMembers();
totalMembers += members.count();
}
return totalMembers;
}
内部迭代:顾名思义,这种方式的遍历将在集合内部进行,我们不会显式地去控制这个循环。无需关心遍历元素的顺序,我们只需要定义对其中每一个元素进行什么样的操作。请看下面的例子
public static int countBandMembersInternal(List<Artist> artists) {
return (int) artists.stream()
.flatMap(artist -> artist.getMembers()).count();
}
- 当然我们也可以用开篇提到的reduce操作,请看下面的例子
public static int countBandMembersInternal(List<Artist> artists) {
return artists.stream()
.map(artist -> artist.getMembers().count())
.reduce(0L, Long::sum)
.intValue();
}
求值
求值。根据Stream方法的签名,判断其是惰性求值还是及早求值;
a. boolean anyMatch(Predicate <? super T> predicate)
b. Stream<T> limit(long maxSize)
再说答案之前,首先解释一下概念。
及早求值:最终会从Stream产生值的方法。
惰性求值:只是描述Stream,最终不产生新集合的方法就做惰性求值方法
判断依据也很简单,只需看它的返回值,如果返回值是Stream那么它是惰性求值。如果返回值是另一个值或者为空,那么就是及早求值。那么本题的答案就很明显了,a为及早求值;b为惰性求值
为什么要区分惰性求值和及早求值?可以思考一下,欢迎在评论区讨论
高阶函数
高阶函数。下面的Stream函数是高阶函数吗?为什么?
a. boolean anyMatch(Predicate <? super T> predicate)
b. Stream<T> limit(long maxSize)
老样子,再说答案之前,先解释一下盖面。
高阶函数:是指接受另一个函数作为参数,或返回一个函数的函数。判断以及同样简单,只需看函数签名就够了。如果函数的参数列表里包含函数接口,或该函数返回一个函数接口,那么该函数就是高阶函数。本题答案也就显而易见,a是,因为接受参数为函数,b不是。
纯函数
纯函数。下面的Lambda表达式有无副作用,或者说它们是否更改了程序状态?
a. x -> x + 1
b. 示例代码如下所示:该代码传入forEach方法的lambda表达式
AtomicInteger count = new AtomicInteger(0);
List<String> origins = album.musicians().forEach(musician -> count.incAndGet();)
这个留作是讨论题,欢迎评论区留言~
进阶练习
计算一个字符串中小写字母的个数;
public static int countLowercaseLetters(String string) {
return (int) string.chars()
.filter(Character::isLowerCase)
.count();
}
- 看到这个实现方法,是不是有些疑惑?流对象从哪里来呢?这我们就得看下String源码,如下
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {...
- 这里我们发现String实现了CharSequence接口,该接口定义了chars()方法,返回值是 IntStream
public default IntStream chars() {
class CharIterator implements PrimitiveIterator.OfInt {
int cur = 0;
public boolean hasNext() {
return cur < length();
}
public int nextInt() {
if (hasNext()) {
return charAt(cur++);
} else {
throw new NoSuchElementException();
}
}
@Override
public void forEachRemaining(IntConsumer block) {
for (; cur < length(); cur++) {
block.accept(charAt(cur));
}
}
}
return StreamSupport.intStream(() ->
Spliterators.spliterator(
new CharIterator(),
length(),
Spliterator.ORDERED),
Spliterator.SUBSIZED | Spliterator.SIZED | Spliterator.ORDERED,
false);
}
- 而IntStream也继承了BaseStream,它也有Stream类有的方法
public interface IntStream extends BaseStream<Integer, IntStream> {...
@Test
public void noLowercaseLettersInAnEmptyString() {
assertEquals(0, countLowercaseLetters(""));
}
@Test
public void countsLowercaseLetterExample() {
assertEquals(3, countLowercaseLetters("aBcDeF"));
}
@Test
public void suppoertsNoLowercaseLetters() {
assertEquals(0, countLowercaseLetters("ABCDEF"));
}
在一个字符串列表中,找出包含最多小写字母的字符串。对于空列表,返回Optional<String>对象;
@Test
public void findsMostLowercaseString2() {
List<String> stringList = Arrays.asList("a", "abc", "ABCde");
Optional<String> optional = stringList.stream()
.max(Comparator.comparingLong((x)-> x.chars().filter(Character::isLowerCase).count()));
assertEquals(Optional.of("abc"), optional);
}
只用reduce和Lambda表达式写出实现Stream上的map操作的代码,如果不想返回Stream,可以返回一个list;
public static <I, O> List<O> map(Stream<I> stream, Function<I, O> mapper) {
return stream.reduce(new ArrayList<O>(), (acc, x) -> {
// We are copying data from acc to new list instance. It is very inefficient,
// but contract of Stream.reduce method requires that accumulator function does
// not mutate its arguments.
// Stream.collect method could be used to implement more efficient mutable reduction,
// but this exercise asks to use reduce method.
List<O> newAcc = new ArrayList<>(acc);
newAcc.add(mapper.apply(x));
return newAcc;
}, (List<O> left, List<O> right) -> {
// We are copying left to new list to avoid mutating it.
List<O> newLeft = new ArrayList<>(left);
newLeft.addAll(right);
return newLeft;
});
}
private <I, O> void assertMapped(Function<I, O> mapper, List<I> input, List<O> expectedOutput) {
List<O> output = map(input.stream(), mapper);
assertEquals(expectedOutput, output);
List<O> parallelOutput = map(input.parallelStream(), mapper);
assertEquals(expectedOutput, parallelOutput);
}
@Test
public void emptyList() {
assertMapped(Function.<Object>identity(), Collections.<Object>emptyList(), Collections.<Object>emptyList());
}
@Test
public void identityMapsToItself() {
assertMapped((Integer x) -> x, asList(1, 2, 3), asList(1, 2, 3));
}
@Test
public void incrementingNumbers() {
assertMapped((Integer x) -> x + 2, asList(1, 2, 3), asList(3, 4, 5));
}
只用reduce和Lambda表达式写出实现Stream上的filter操作的代码,如果不想返回Stream,可以返回一个list;
public class FilterUsingReduceTest {
public static <I> List<I> filter(Stream<I> stream, Predicate<I> predicate) {
List<I> initial = new ArrayList<>();
return stream.reduce(initial,
(List<I> acc, I x) -> {
if (predicate.test(x)) {
// We are copying data from acc to new list instance. It is very inefficient,
// but contract of Stream.reduce method requires that accumulator function does
// not mutate its arguments.
// Stream.collect method could be used to implement more efficient mutable reduction,
// but this exercise asks to use reduce method explicitly.
List<I> newAcc = new ArrayList<>(acc);
newAcc.add(x);
return newAcc;
} else {
return acc;
}
},
FilterUsingReduceTest::combineLists);
}
private static <I> List<I> combineLists(List<I> left, List<I> right) {
// We are copying left to new list to avoid mutating it.
List<I> newLeft = new ArrayList<>(left);
newLeft.addAll(right);
return newLeft;
}
private <I> void assertFiltered(Predicate<I> predicate, List<I> input, List<I> expectedOutput) {
List<I> output = FilterUsingReduce.filter(input.stream(), predicate);
assertEquals(expectedOutput, output);
List<I> parallelOutput = FilterUsingReduce.filter(input.parallelStream(), predicate);
assertEquals(expectedOutput, parallelOutput);
}
@Test
public void emptyList() {
assertFiltered(x -> false, Collections.<Object>emptyList(), Collections.<Object>emptyList());
}
@Test
public void trueReturnsEverything() {
assertFiltered((Integer x) -> true, asList(1, 2, 3), asList(1, 2, 3));
}
@Test
public void falseRemovesEverything() {
assertFiltered((Integer x) -> false, asList(1, 2, 3), asList());
}
@Test
public void filterPartOfList() {
assertFiltered((Integer x) -> x > 2, asList(1, 2, 3), asList(3));
}
}