《Java函数式编程实战》学习总结 第7章 流式重构

第7章 流式重构

7.1 使用Lambda表达式的SOLID原则

SOLID 原则是设计面向对象程序时的一些基本原则。每种原则都对应着一系列潜在的代码异味,并为其提供了解决方案。有很多图书介绍这个主题,因此我不会详细讲解,而是关注如何在 Lambda 表达式的环境下应用其中的三条原则。在 Java 8 中,有些原则通过扩展,已经超出了原来的限制。
7.1.1 单一功能原则

程序中的类或方法只能有一个改变的理由。

计算质数个数,一个方法里塞进了多重职责

java 复制代码
public long countPrimes(int upTo) {
        long tally = 0;
        for (int i = 1; i < upTo; i++) {
            boolean isPrime = true;
            for (int j = 2; j < i; j++) {
                if (i % j == 0) {
                    isPrime = false;
                }
            }
            if (isPrime) {
                tally++;
            }
        }
        return tally;
    }

将 isPrime 重构成另外一个方法后,计算质数个数的方法

java 复制代码
 public long countPrimes(int upTo) {
        long tally = 0;
        for (int i = 1; i < upTo; i++) {
            if (isPrime(i)) {
                tally++;
            }
        }
        return tally;
    }
    private boolean isPrime(int number) {
        for (int i = 2; i < number; i++) {
            if (number % i == 0) {
                return false;
            }
        }
        return true;
    }

并行运行基于集合流的质数计数程序

java 复制代码
public long countPrimes(int upTo) {
        return IntStream.range(1, upTo)
                .parallel()
                .filter(this::isPrime)
                .count();
    }
    private boolean isPrime(int number) {
        return IntStream.range(2, number)
                .allMatch(x -> (number % x) != 0);
    }

7.1.2 开闭原则

软件应该对扩展开放,对修改闭合。

MetricDataGraph 类的方法之一是将代理收集到的各项指标放入该类,API 如例

java 复制代码
 class MetricDataGraph {
        public void updateUserTime(int value);
        public void updateSystemTime(int value);
        public void updateIoTime(int value);
    }

这样的设计意味着每次想往散点图中添加新的时间点,都要修改 MetricDataGraph 类。引入抽象可以解决这个问题,我们使用一个新类TimeSeries 来表示各种时间点。这时,MetricDataGraph 类的公开 API 就得以简化,不必依赖于某项具体指标。

每项具体指标现在可以实现 TimeSeries 接口,在需要时能直接插入。比如,我们可能会有 如 下 类:UserTimeSeriesSystemTimeSeriesIoTimeSeries 。 如 果 要 添 加 新 的, 比如由于虚拟化所浪费的 CPU 时间,则可增加一个新的实现了 TimeSeries 接口的类:StealTimeSeries 。这样,就扩展了 MetricDataGraph 类,但并没有修改它。

java 复制代码
 class MetricDataGraph {
        public void addTimeSeries(TimeSeries values);
    }

高阶函数也展示出了同样的特性:对扩展开放,对修改闭合。前面提到的 ThreadLocal 类就是一个很好的例子。ThreadLocal 有一个特殊的变量,每个线程都有一个该变量的副本并与之交互。该类的静态方法 withInitial 是一个高阶函数,传入一个负责生成初始值的Lambda 表达式。

java 复制代码
   // ThreadLocal 日期格式化器
   // 实现
   ThreadLocal<DateFormat> localFormatter
           = ThreadLocal.withInitial(() -> new SimpleDateFormat());
    // 使用
    DateFormat formatter = localFormatter.get();

    // ThreadLocal 标识符
    // 或者这样实现
    AtomicInteger threadId = new AtomicInteger();
    ThreadLocal<Integer> localId
            = ThreadLocal.withInitial(() -> threadId.getAndIncrement());
    // 使用
    int idForThisThread = localId.get();

7.1.2 依赖反转原则

抽象不应依赖细节,细节应该依赖抽象。

让我们看一个具体的、自动化构建地址簿的例子,实现时使用了依赖反转原则达到上层的解耦。该应用以电子卡片作为输入,使用某种存储机制编写地址簿。显然,可将代码分成如下三个基本模块:

  • 一个能解析电子卡片格式的电子卡片阅读器;
  • 能将地址存为文本文件的地址簿存储模块;
  • 从电子卡片中获取有效信息并将其写入地址簿的编写模块。
    依赖关系:

让我们看一段代码,该段代码从一种假想的标记语言中提取标题,其中标题以冒号( :)结尾。我们的方法先读取文件,逐行检查,滤出标题,然后关闭文件。

解析文件中的标题

但是代码将提取标题和资源管理、文件处理混在一起

java 复制代码
public List<String> findHeadings(Reader input) {
        try (BufferedReader reader = new BufferedReader(input)) {
            return reader.lines()
                    .filter(line -> line.endsWith(":"))
                    .map(line -> line.substring(0, line.length() - 1))
                    .collect(toList());
        } catch (IOException e) {
            throw new HeadingLookupException(e);
        }
    }

剥离了文件处理功能后的业务逻辑

java 复制代码
 public List<String> findHeadings(Reader input) {
        return withLinesOf(input,
                lines -> lines.filter(line -> line.endsWith(":"))
                        .map(line -> line.substring(0, line.length()-1))
                        .collect(toList()),
                HeadingLookupException::new);
    }

    // withLinesOf方法 Reader参数处理文件,handler函数代表了执行的代码,参数error输入输出异常
    private <T> T withLinesOf(Reader input,
                              Function<Stream<String>, T> handler,
                              Function<IOException, RuntimeException> error) {
        try (BufferedReader reader = new BufferedReader(input)) {
            return handler.apply(reader.lines());
        } catch (IOException e) {
            throw error.apply(e);
        }
    }

7.2 使用Lambda表达式的一些重构技巧

7.2.1 不要重复你劳动

增加一个简单的 Order 类来计算用户购买专辑的一些有用属性,如计算音乐家人数、曲目和专辑时长。

Order 类的命令式实现

java 复制代码
 public long countRunningTime() {
        long count = 0;
        for (Album album : albums) {
            for (Track track : album.getTrackList()) {
                count += track.getLength();
            }
        }
        return count;
    }
    public long countMusicians() {
        long count = 0;
        for (Album album : albums) {
            count += album.getMusicianList().size();
        }
        return count;
    }
    public long countTracks() {
        long count = 0;
        for (Album album : albums) {
            count += album.getTrackList().size();
        }
        return count;
    }

使用领域方法重构 Order 类

java 复制代码
public long countFeature(ToLongFunction<Album> function) {
        return albums.stream()
                .mapToLong(function)
                .sum();
    }
    public long countTracks() {
        return countFeature(album -> album.getTracks().count());
    }
    public long countRunningTime() {
        return countFeature(album -> album.getTracks()
                .mapToLong(track -> track.getLength())
                .sum());
    }
    public long countMusicians() {
        return countFeature(album -> album.getMusicians().count());
    }

7.2.2 日志和打印消息

使用 peek 方法记录中间值

java 复制代码
Set<String> nationalities
            = album.getMusicians()
            .filter(artist -> artist.getName().startsWith("The"))
            .map(artist -> artist.getNationality())
            .peek(nation -> System.out.println("Found nationality: " + nation))
            .collect(Collectors.<String>toSet());

7.2.3 合并集合操作

流的扁平化(flatMap)与连接(concat)处理集合

java 复制代码
    List<String> list1 = Arrays.asList("apple", "banana", "cherry", "date");
    List<String> list2 = Arrays.asList("apple", "banana", "cherry", "date");
    // 原始数据 - 嵌套列表
    List<List<String>> nestedList = Arrays.asList(list1,list2);

    // 扁平化操作 - 使用flatMap
    List<String> flatMapped = nestedList.stream().flatMap(List::stream).collect(Collectors.toList());

    // 合并集合 - 使用concat
    List<String> concatResult = Stream.concat(list1.stream(), list2.stream()).collect(Collectors.toList());

7.2.4 集合异步并行处理

CompletableFuture并行处理逻辑,提高处理速度

java 复制代码
public void test() {
        List<String> data1 = Arrays.asList("Alice", "Bob", "Charlie");
        List<Integer> data2 = Arrays.asList(1, 2, 3);

        // 并行处理1
        CompletableFuture<?>[] futures1 = data1.stream().map(
                name -> CompletableFuture.runAsync(() -> 
                cusService.queryCustomerAcquisitionDashboardUpdateTime()))
                .toArray(CompletableFuture[]::new);
        // 并行处理2
        CompletableFuture<?>[] futures2 = data2.stream().map(
                name -> CompletableFuture.runAsync(() -> 
                custService.queryCustomerAcquisitionDashboardUpdateTime()))
                .toArray(CompletableFuture[]::new);
        // 统一等待所有任务完成
        CompletableFuture<Void> future1 = CompletableFuture.allOf(futures1);
        CompletableFuture<Void> future2 = CompletableFuture.allOf(futures2);
        CompletableFuture.allOf(future1, future2).join();
    }