<<java 8 函数式编程>>学习 - 第三章 流

Java 8 对核心类库的改进主要包括集合类的 API 和新引入的流 (Stream)。流使程序员得以站在更高的抽象层次上对集合进行操作.

1.从外部迭代到内部迭代

使用 for 循环计算来自伦敦的艺术家人数:

int count = 0;
for (Artist artist : allArtists) {
    if (artist.isFrom("London")) {
        count++;
    }
}

改进一(外部迭代), 使用迭代器计算来自伦敦的艺术家人数:

int count = 0;
Iterator<Artist> iterator = allArtists.iterator(); 
while(iterator.hasNext()) {
    Artist artist = iterator.next(); 
    if (artist.isFrom("London")) {
        count++; 
    }
}

改进二(内部迭代), 利用stream计算来自伦敦的艺术家人数:

long count = allArtists.stream()
    .filter(artist -> artist.isFrom("London"))
    .count();

Stream 是用函数式编程方式在集合类上进行复杂操作的工具.
上述代码可被分解为两步更简单的操作:

  • 找出所有来自伦敦的艺术家;
  • 计算他们的人数.

每种操作都对应 Stream 接口的一个方法. 为了找出来自伦敦的艺术家, 需要对 Stream 对象进行过滤:filter. 过滤在这里是指"只保留通过某项测试的对象". 测试由一个函数完成, 根据艺术家是否来自伦敦, 该函数返回true或者false. 由于Stream API的函数式编程风格, 我们并没有改变集合的内容, 而是描述出 Stream 里的内容. count() 方法计算给定 Stream 里包含多少个对象.

2.实现机制

上述整个过程被分解为两种更简单的操作: 过滤和计数, 看似有化简为繁之嫌--两次操作(filter和count)是否需要两次循环? 事实上类库设计精妙, 只需对艺术家列表迭代一次.

通常, 在 Java 中调用一个方法,计算机会随即执行操作:比如, System.out.println ("Hello World"); 会在终端上输出一条信息. 这种方式叫做及早求值. 而Stream对象则则使用了另一个概念叫做惰性求值, 直到真正需要时才进行计算.

//只过滤不计数
allArtists.stream()
    .filter(artist -> artist.isFrom("London"));

这行代码并未做什么实际性的工作, filter 只刻画出了 Stream, 但没有产生新的集合.
判断一个操作是惰性求值还是及早求值很简单, 只需看它的返回值: 如果返回值是 Stream, 那么是惰性求值; 如果返回值是另一个值或为空, 那么就是及早求值.

3.常用的流操作

3.1 collect(toList())

collect(toList()) 方法由 Stream 里的值生成一个列表,是一个及早求值操作.

List<String> collected = Stream.of("a", "b", "c").collect(Collectors.toList());
assertEquals(Arrays.asList("a", "b", "c"), collected);

3.2 map

如果有一个函数可以将一种类型的值转换成另外一种类型, map 操作就可以使用该函数, 将一个流中的值转换成一个新的流.

//使用 map 操作将字符串转换为大写形式
List<String> collected = Stream.of("a", "b", "hello")
                                    .map(string -> string.toUpperCase())
                                    .collect(Collectors.toList());
assertEquals(Arrays.asList("A", "B", "HELLO"), collected);

3.3 filter

遍历数据并检查其中的元素时,可尝试使用 Stream 中提供的新方法 filter.

List<String> beginningWithNumbers
                = Stream.of("a", "1abc", "abc1")
                .filter(value -> Character.isDigit(value.charAt(0)))
                .collect(Collectors.toList());
assertEquals(Arrays.asList("1abc"), beginningWithNumbers);

filter 接受一个函数作为参数, 该函数用 Lambda 表达式表示, 其返回值必须是 true 或者 false, 该 Lambda 表达式的函数接口正是之前介绍过的 Predicate.

3.4 flatMap

flatMap 方法可用 Stream 替换值,然后将多个 Stream 连接成一个 Stream.

//假设有一个包含多个列表的流, 现在希望得到所有数字的序列.
List<Integer> together = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4))
                .flatMap(numbers -> numbers.stream())
                .collect(Collectors.toList());
assertEquals(Arrays.asList(1, 2, 3, 4), together);

3.5 max和min

Stream上常用的操作之一是求最大值和最小值. Stream API中的max和min操作足以解决这一问题.

//查找长度最小的元素
List<String> tracks = Arrays.asList("Bakai", "Violets for Your Furs","Time Was");
String shortestTrack = tracks.stream()
        .min(Comparator.comparing(track -> track.length()))
        .get();
assertEquals(tracks.get(1), shortestTrack);

3.6 reduce

reduce 操作可以实现从一组值中生成一个值. 在上述例子中用到的 count、min 和 max 方 法, 因为常用而被纳入标准库中. 事实上, 这些方法都是 reduce 操作.
下例展示这一过程, Lambda 表达式就是 reducer, 它执行求和操作, 有两个参数: 传入 Stream 中的当前元素和 acc. 将两个参数相加, acc 是累加器, 保存着当前的累加结果.

//使用 reduce 求和
int count = Stream.of(1, 2, 3)
                .reduce(0, (acc, element) -> acc + element);
assertEquals(6, count);

4.重构遗留代码

为了进一步阐释如何重构遗留代码, 本节将举例说明如何将一段使用循环进行集合操作的代码, 重构成基于 Stream 的操作.
下面是跟专辑相关的一组基础类:

package com.chyun.java8.lambda.base;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toList;

/**
 * Domain class for a popular music artist.
 *
 * @author Richard Warburton
 */
public final class Artist {

    private String name;
    private List<Artist> members;
    private String nationality;

    public Artist(String name, String nationality) {
        this(name, Collections.emptyList(), nationality);
    }

    public Artist(String name, List<Artist> members, String nationality) {
        Objects.requireNonNull(name);
        Objects.requireNonNull(members);
        Objects.requireNonNull(nationality);
        this.name = name;
        this.members = new ArrayList<>(members);
        this.nationality = nationality;
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @return the members
     */
    public Stream<Artist> getMembers() {
        return members.stream();
    }

    /**
     * @return the nationality
     */
    public String getNationality() {
        return nationality;
    }

    public boolean isSolo() {
        return members.isEmpty();
    }

    public boolean isFrom(String nationality) {
        return this.nationality.equals(nationality);
    }

    @Override
    public String toString() {
        return getName();
    }

    public Artist copy() {
        List<Artist> members = getMembers().map(Artist::copy).collect(toList());
        return new Artist(name, members, nationality);
    }

}
package com.chyun.java8.lambda.base;

import java.util.stream.Stream;

import static java.util.stream.Stream.concat;

public interface Performance {

    public String getName();

    public Stream<Artist> getMusicians();

    // TODO: test
    public default Stream<Artist> getAllMusicians() {
        return getMusicians().flatMap(artist -> {
            return concat(Stream.of(artist), artist.getMembers());
        });
    }

}
package com.chyun.java8.lambda.base;

public final class Track {

    private final String name;
    private final int length;

    public Track(String name, int length) {
        this.name = name;
        this.length = length;
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @return the length of the track in milliseconds.
     */
    public int getLength() {
        return length;
    }

    public Track copy() {
        return new Track(name, length);
    }

}
package com.chyun.java8.lambda.base;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;

import static java.util.Collections.unmodifiableList;
import static java.util.stream.Collectors.toList;

/**
 *
 * @author richard
 */
public final class Album implements Performance {

    private String name;
    private List<Track> tracks;
    private List<Artist> musicians;

    public Album(String name, List<Track> tracks, List<Artist> musicians) {
        Objects.requireNonNull(name);
        Objects.requireNonNull(tracks);
        Objects.requireNonNull(musicians);

        this.name = name;
        this.tracks = new ArrayList<>(tracks);
        this.musicians = new ArrayList<>(musicians);
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @return the tracks
     */
    public Stream<Track> getTracks() {
        return tracks.stream();
    }

    /**
     * Used in imperative code examples that need to iterate over a list
     */
    public List<Track> getTrackList() {
        return unmodifiableList(tracks);
    }

    /**
     * @return the musicians
     */
    public Stream<Artist> getMusicians() {
        return musicians.stream();
    }

    /**
     * Used in imperative code examples that need to iterate over a list
     */
    public List<Artist> getMusicianList() {
        return unmodifiableList(musicians);
    }

    public Artist getMainMusician() {
        return musicians.get(0);
    }

    public Album copy() {
        List<Track> tracks = getTracks().map(Track::copy).collect(toList());
        List<Artist> musicians = getMusicians().map(Artist::copy).collect(toList());
        return new Album(name, tracks, musicians);
    }

}

假定选定一组专辑, 找出其中所有长度大于 1 分钟的曲目名称.

//遗留代码, 使用for循环
public Set<String> findLongTracks(List<Album> albums) { 
    Set<String> trackNames = new HashSet<>();
    for(Album album : albums) {
        for (Track track : album.getTrackList()) { 
            if (track.getLength() > 60) {
                String name = track.getName();
                trackNames.add(name);
            }
        } 
    }
    return trackNames;
}

第一步要修改的是 for 循环. 首先使用 Stream 的 forEach 方法替换掉 for 循环.

public Set<String> findLongTracks(List<Album> albums) {
    Set<String> trackNames = new HashSet<>();
    albums.stream()
        .forEach(album -> {
            album.getTracks()
                    .forEach(track -> {
                        if (track.getLength() > 60) {
                            String name = track.getName();
                            trackNames.add(name);
                        }
                    });
        });
    return trackNames; 
}

第二步, 将内部的 forEach 方法用Stream代替.

public Set<String> findLongTracks(List<Album> albums) { 
    Set<String> trackNames = new HashSet<>(); 
    albums.stream()
        .forEach(album -> {
            album.getTracks()
                    .filter(track -> track.getLength() > 60)
                    .map(track -> track.getName())
                    .forEach(name -> trackNames.add(name));
        });
    return trackNames;
}

第三步,用flatMap替换第一个foreach.

public Set<String> findLongTracks(List<Album> albums) {
    Set<String> trackNames = new HashSet<>();
    albums.stream()
            .flatMap(album -> album.getTracks())
            .filter(track -> track.getLength() > 60)
            .map(track -> track.getName())
            .forEach(name -> trackNames.add(name));
    return trackNames;
}

第四步, 用collect()方法替换最后的forEach.

public Set<String> findLongTracks(List<Album> albums) {
    return albums.stream()
            .flatMap(album -> album.getTracks())
            .filter(track -> track.getLength() > 60)
            .map(track -> track.getName())
            .collect(Collectors.toSet());
}

5.练习

a.编写一个求和函数, 计算流中所有数之和;

public int addUp(Stream<Integer> numbers) {
    return numbers.reduce(0, (acc, x) -> acc + x);
}

b.编写一个函数, 接受艺术家列表作为参数, 返回一个字符串列表, 其中包含艺术家的姓名和国籍;

public static List<String> getNamesAndOrigins(List<Artist> artists) {
    return artists.stream().flatMap(artist -> Stream.of(artist.getName(), artist.getNationality()))
            .collect(Collectors.toList());
}

c.修改如下代码,将外部迭代转换成内部迭代;

int totalMembers = 0;
for (Artist artist : artists) {
    Stream<Artist> members = artist.getMembers();
    totalMembers += members.count();
}
artists.stream().map(artist -> artist.getMembers().count()).reduce(0L, Long::sum).intValue();

d.在一个字符串列表中, 找出包含最多小写字母的字符串.

public static Optional<String> mostLowercaseString(List<String> strings) {
    return strings.stream().max(Comparator.comparing(string -> (int) string.chars().filter(Character::isLowerCase).count()));
}

e.只用 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) -> {
        List<O> newAcc = new ArrayList<>(acc);
        newAcc.add(mapper.apply(x));
        return newAcc;
    }, (List<O> left, List<O> right) -> {
        List<O> newleft = new ArrayList<>(left);
        newleft.addAll(right);
        return newleft;
    });
}

此处关于reduce可以参考https://segmentfault.com/q/1010000004944450.
f.只用 reduce 和 Lambda 表达式写出实现 Stream 上的 filter 操作的代码, 如果不想返回 Stream, 可以返回一个 List.

public static <O> List<O> filter(Stream<O> stream, Predicate<O> mapper) {
    return stream.reduce(new ArrayList<O>(), (acc, x) -> {
        if (mapper.test(x)) {
            List<O> newAcc = new ArrayList<>(acc);
            newAcc.add(x);
            return newAcc;
        }
        return acc;
    }, (List<O> left, List<O> right) -> {
        List<O> newleft = new ArrayList<>(left);
        newleft.addAll(right);
        return newleft;
    });
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • Jav8中,在核心类库中引入了新的概念,流(Stream)。流使得程序媛们得以站在更高的抽象层次上对集合进行操作。...
    仁昌居士阅读 3,559评论 0 6
  • 第一章 为什么要关心Java 8 使用Stream库来选择最佳低级执行机制可以避免使用Synchronized(同...
    谢随安阅读 1,441评论 0 4
  • 01 何小萍和刘峰在片首最先出现,画面是刘峰将新入伍的何小萍接到部队。 快进门时,刘峰提起何小萍的父亲正在劳动改教...
    说心理话阅读 1,074评论 0 2
  • 五万元
    十七k阅读 121评论 0 0
  • 数量与质量 数量与质量能否兼得不能一概而论。 人和人不同,世界上确有很多天才,能够一直兼顾数量与质量,这是我等普通...
    zzzzzzzzzzzzzzc阅读 220评论 0 2