Java 8函数式编程实战(二)Stream 流

Stream流

案例分析

常用流操作

  1. 编写一个求和函数,计算流中所有数之和。例如,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
    }
  1. 编写一个函数,接受艺术家列表作为函数,返回一个字符串列表,其中包含艺术家的姓名和国籍;
    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对象合并成一个输出。

  1. 编写一个函数,接受专辑列表作为参数,返回一个由最多包含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();)

这个留作是讨论题,欢迎评论区留言~

进阶练习

  1. 计算一个字符串中小写字母的个数;
    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"));
    }
  1. 在一个字符串列表中,找出包含最多小写字母的字符串。对于空列表,返回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);
    }
  1. 只用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));
    }
  1. 只用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));
    }

    

}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

健身的IT鸟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值