<>学习 - 第三章 流

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

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

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

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

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

int count = 0;
Iterator 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 collected = Stream.of("a", "b", "c").collect(Collectors.toList());
assertEquals(Arrays.asList("a", "b", "c"), collected);

3.2 map

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

//使用 map 操作将字符串转换为大写形式
List 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 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 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 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 members;
    private String nationality;

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

    public Artist(String name, List 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 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 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 getMusicians();

    // TODO: test
    public default Stream 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 tracks;
    private List musicians;

    public Album(String name, List tracks, List 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 getTracks() {
        return tracks.stream();
    }

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

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

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

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

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

}

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

//遗留代码, 使用for循环
public Set findLongTracks(List albums) { 
    Set 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 findLongTracks(List albums) {
    Set 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 findLongTracks(List albums) { 
    Set 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 findLongTracks(List albums) {
    Set 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 findLongTracks(List 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 numbers) {
    return numbers.reduce(0, (acc, x) -> acc + x);
}

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

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

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

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

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

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

e.只用 reduce 和 Lambda 表达式写出实现 Stream 上的 map 操作的代码, 如果不想返回 Stream, 可以返回一个 List.

public static  List map(Stream stream, Function mapper) {
    return stream.reduce(new ArrayList(), (acc, x) -> {
        List newAcc = new ArrayList<>(acc);
        newAcc.add(mapper.apply(x));
        return newAcc;
    }, (List left, List right) -> {
        List newleft = new ArrayList<>(left);
        newleft.addAll(right);
        return newleft;
    });
}

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

public static  List filter(Stream stream, Predicate mapper) {
    return stream.reduce(new ArrayList(), (acc, x) -> {
        if (mapper.test(x)) {
            List newAcc = new ArrayList<>(acc);
            newAcc.add(x);
            return newAcc;
        }
        return acc;
    }, (List left, List right) -> {
        List newleft = new ArrayList<>(left);
        newleft.addAll(right);
        return newleft;
    });
}

你可能感兴趣的:(<>学习 - 第三章 流)