2

Java8引入了与此前完全不同的函数式编程方法,通过Lambda表达式和StreamAPI来为Java下的函数式编程提供动力。本文是Java8新特性的第二篇,旨在展示Stream API是如何简化程序编写并且体现出函数式编程的美感的,在Stream的世界里面,数据在概念上不再只是内存中存储的字节,而更像是流动的活跃生物。

最近在读这本图灵的新书:Java 8 in Action ,本书作者的目的在于向Java程序员介绍Java8带来的新特性,鼓励使用新特性来完成更简洁有力的Java编程。本系列的文章的主要思路也来源于本书。

内部迭代的引入

我们上回说到Java 8函数式编程的基础其实就是“策略对象的传递”,换言之就是行为的参数化,在实现上就是匿名类来实现接口定义的抽象方法,从而把匿名类的对象进行传递就形成了行为参数化,上篇对这个过程的推演进行了详细的描述。
和实现了函数式接口的匿名类对象等价的Lambda表达式作为参数,即可简洁高效地完成上述工作。
行为参数化虽然可以让我们只传递策略对象本身,但是每次都书写相同的迭代逻辑并不优雅,那么我们如果要从中解放出来,就应该把这些迭代逻辑从显式的外部移到API的内部,让我们在调用API的过程中就可以完成这些繁琐重复的迭代,这样一来:

内部迭代 + 行为参数化 = 优雅的函数式编程

我们来回顾下上次我们提到的写法:

我们在上文介绍的这个函数式接口(其实也就是API中的Predicate<T>接口):

interface ChooseStrategy<T>{
    public Boolean test(T t);
}

同时我们需要一个泛化的filter方法:

    public static <T> List<T> filter(List<T> ts, ChooseStrategy<T> strategy){
        ArrayList<T> resList = new ArrayList<>();
        for (T t
                : ts) {
            if(strategy.test(t))
                resList.add(t);
        }
        return resList;
    }
List<Apple> redApples = filter(apples, apple -> "red".equals(apple.getColor()));

在这个过程中,apple -> "red".equals(apple.getColor())就是参数化的行为,亦即ChooseStrategy<T>函数式接口匿名实现类的对象,实现的test方法即为Lambda表达式描述的那样。而这个filter方法就是我们提到的内部迭代,因为在我们真正调用的时候并未显式地再做迭代,而是把迭代隐藏在filter方法内部。
类似的,其实API只要定义几个这样的通用内部迭代方法,比如用于筛选的filter操作、用于映射的map操作、用于规约的reduce操作,就可以完成我们上文提到的优雅的函数式编程方法。但是,Java8并未停步于此,而是开发了一整套全新的Stream API接口,通过这个接口,不仅定义了一系列的内部迭代操作,还将数据流化,利于更便捷地再多核机器上并行执行程序。

什么是Stream流呢

流在JavaIO的概念里面实际上不是新的概念,在JavaIO的世界里面,文件、网络等IO设备上的数据如果想要进入内存或者从内存写进IO就开辟一个IO和内存的通道,这个通道就是JavaIO提到的流。在感官上,IO流和我们在Java8 StreamAPI里面看到的Stream流其实比较接近--数据通道。

在Stream流的世界里,数据在概念上不再是存在内存中的字节(之所以说是概念上,是因为数据处理显然无法脱离内存而存在),而更像是流动在管道中的活跃生物。这些活跃的数据生物排队依次进入处理单元,而经过的处理单元可以在函数式的编程作用下形成一关关的流水线依次对数据生物进行处理,而在并行程序中数据生物甚至可以被发送到在不同的处理器核心上去。这样的理念无疑是非常令人振奋的。

Stream API定义的一系列处理单元其实就是我们上面提到的内部迭代操作:

有了这些内部迭代操作,我们就彻底从无聊冗余的外部迭代中解放出来了。

下面以filter为例来看看StreamAPI定义的操作和我们上面自己写的filter操作有什么区别:

先来看我们自己写的:

List<Apple> redApples = filter(apples, apple -> "red".equals(apple.getColor()));

我们处理的就是内存中的List。

List<Apple> redApples = apples.stream().filter(apple -> "red".equals(apple.getColor())).collect(Collectors.toList());

而Stream API会将内存中的List先流化为Stream再做处理。下图展示了这两种方式的区别:

如果是流水线式的处理,有时就会更加清晰,Stream化的数据始终是以流的形式进行处理的:

像执行SQL一样编写查询:使用Stream流来进行函数式编程

其实我们使用SQL来进行数据库查询的时候,就是一种典型的函数式查询。我们不需要显式地去说明如何在表中的一条条数据中间迭代,而只是使用SQL内建的关键词即可完成查询,SQL的内建关键词会在查询的过程中执行内部迭代,它的具体操作对于编写SQL的我们来说是透明的,这也就是函数式编程最终达到的效果--隐藏细节。
使用Stream API也可以完成这一点,复杂的内部迭代过程隐去,我们只需要编写最核心的操作策略,也就是这些内部迭代操作需要传入的Lambda表达式:策略对象参数。
为了表示Stream API如何使用,仍然不能免俗地引入一个例子,本例来源于Java 8 in Action一书。为了说明使用Stream API带来的简洁,我们将试图模拟使用SQL来对照这些例子。

有几个执行交易的交易员。交易员的定义:

class Trader {
    private final String name;
    private final String city;

    public Trader(String n, String c) {
        this.name = n;
        this.city = c;
    }

    public String getName() {
        return this.name;
    }

    public String getCity() {
        return this.city;
    }

    public String toString() {
        return "Trader:" + this.name + " in " + this.city;
    }
}

有几个这些交易员参与的交易。交易的定义:

class Transaction {
    private final Trader trader;
    private final int year;
    private final int value;

    public Transaction(Trader trader, int year, int value) {
        this.trader = trader;
        this.year = year;
        this.value = value;
    }

    public Trader getTrader() {
        return trader;
    }

    public int getYear() {
        return year;
    }

    public int getValue() {
        return value;
    }

    @Override
    public String toString() {
        return "Transaction{" +
                "trader=" + trader +
                ", year=" + year +
                ", value=" + value +
                '}';
    }
}

给定几个交易员和几场交易:

Trader raoul = new Trader("Raoul", "Cambridge");
Trader mario = new Trader("Mario", "Milan");
Trader alan = new Trader("Alan", "Cambridge");
Trader brian = new Trader("Brian", "Cambridge");
List<Transaction> transactions = Arrays.asList(
      new Transaction(brian, 2011, 300),
      new Transaction(raoul, 2012, 1000),
      new Transaction(raoul, 2011, 400),
      new Transaction(mario, 2012, 710),
      new Transaction(mario, 2012, 700),
      new Transaction(alan, 2012, 950)
);

你的经理让你进行几个查询。
为了我们说明Stream API编程和SQL查询的相似之处,我们模拟这样的数据表来匹配上面的数据(以MySQL数据库为例):
TRADER表:

Transaction表:

我们需要进行的查询:

(1) 找出2011年发生的所有交易,并按交易额排序(从低到高)。
//找出2011年发生的所有交易,并按交易额排序(从低到高)。
List<Transaction> resTransactions = transactions.stream()
                .filter(tr -> tr.getYear() == 2011)
                .sorted(Comparator.comparing(Transaction::getValue))
                .collect(Collectors.toList());
//打印出每一个交易
resTransactions.forEach(tr -> System.out.println(tr));

如果是数据表,可用这样的SQL来做类似的操作:

SELECT 
    *
FROM
    Transaction ts
WHERE
    ts.year = 2011
ORDER BY ts.value

不难看出,Stream API里的内部迭代操作和我们熟悉的SQL中的内建关键词存在着对应的联系,我们可以使用诸如:filter方法对应where关键词,sorted方法对应ORDER BY关键词。通过Stream API的内部迭代方法,我们确实可以实现只说明操作而不显式说明操作的具体做法,即可完成我们的任务,正如SQL实现的那样。

(2) 交易员都在哪些不同的城市工作过?
//交易员都在哪些不同的城市工作过?
List<String> cities = transactions.stream().map(Transaction::getTrader).map(Trader::getCity).distinct().collect(Collectors.toList());
//打印出每一个城市
cities.forEach(c -> System.out.println(c));

如果是数据表,可用这样的SQL来做类似的操作:

SELECT 
    distinct td.city
FROM
    Transaction ts,
    Trader td
WHERE ts.traderid = td.id

本例中,两个map操作得到每个交易的交易员的城市,最终效果等同于多表联合查询加上SELECT城市得到每个交易的交易员城市。distinct方法对应distinct关键字。

(3) 查找所有来自于剑桥的交易员,并按姓名排序。
//查找所有来自于剑桥的交易员,并按姓名排序。
List<Trader> traders = transactions.stream().map(Transaction::getTrader).distinct().filter(trader -> "Cambridge".equals(trader.getCity())).sorted(Comparator.comparing(Trader::getName)).collect(Collectors.toList());
///打印出每一个交易员
traders.forEach(trader -> System.out.println(trader));

如果是数据表,可用这样的SQL来做类似的操作:

SELECT 
    distinct td.*
FROM
    Transaction ts,
    Trader td
WHERE ts.traderid = td.id
AND td.city = 'Cambridge'
ORDER BY td.name
(4) 返回所有交易员的姓名字符串,按字母顺序排序。
//返回所有交易员的姓名字符串,按字母顺序排序。
String nameStr = transactions.stream().map(transaction -> transaction.getTrader().getName()).distinct().sorted().reduce("",(n1,n2)->n1+","+n2);
//打印出字符串
System.out.println(nameStr);

如果是数据表,可用这样的SQL来做类似的操作:

SELECT 
    GROUP_CONCAT(DISTINCT td.name)
FROM
    Transaction ts,
    Trader td
WHERE
    ts.traderid = td.id
ORDER BY td.name

reduce方法在SQL中的对应情形不唯一,因为SQL未对归约操作作出统一的函数定义,而是给出了一系列的工具方法,这样做对于实用性有帮助,因为SQL其实并不会太多做这样的操作,归约操作更应在前端进行。

(5) 有没有交易员是在米兰工作的?
//有没有交易员是在米兰工作的?
Boolean yes = transactions.stream().map(transaction -> transaction.getTrader().getCity()).anyMatch(city->"Milan".equals(city));
//打印结果
System.out.println(yes);

如果是数据表,可用这样的SQL来做类似的操作:

SELECT 
    *
FROM
    Trader
WHERE
    EXISTS( SELECT 
            td.id
        FROM
            Transaction ts,
            Trader td
        WHERE
            ts.traderid = td.id
                AND td.city = 'Milan')
(6) 打印生活在剑桥的交易员的所有交易额。
//打印生活在剑桥的交易员的所有交易额。
transactions.stream().filter(transaction -> "Cambridge".equals(transaction.getTrader().getCity())).forEach(transaction -> System.out.println(transaction.getValue()));

如果是数据表,可用这样的SQL来做类似的操作:

SELECT 
    ts.value
FROM
    Transaction ts,
    Trader td
WHERE
    ts.traderid = td.id
AND
    td.city = 'Cambridge'
(7) 所有交易中,最高的交易额是多少?
//所有交易中,最高的交易额是多少?
Optional<Integer> maxValue = transactions.stream().map(Transaction::getValue).reduce(Integer::max);
//查看是否存在(在无交易的时候会有用)
System.out.println(maxValue.isPresent());
System.out.println(maxValue.get());

如果是数据表,可用这样的SQL来做类似的操作:

SELECT 
    max(ts.value)
FROM
    Transaction ts

max对应了reduce的归约操作。

(8) 找到交易额最小的交易。
//找到交易额最小的交易。
Optional<Transaction> minValueTrasaction = transactions.stream().reduce((tr1, tr2) -> {
  if (tr1.getValue() > tr2.getValue()) return tr2;
  else return tr1;
});
//查看是否存在(在无交易的时候会有用)
System.out.println(minValueTrasaction.isPresent());
System.out.println(minValueTrasaction.get());

如果是数据表,可用这样的SQL来做类似的操作:

SELECT 
    ts.*
FROM
    Transaction ts
WHERE
    ts.value = (SELECT 
            MAX(ts.value)
        FROM
            Transaction ts)

这就是Stream API的核心做法,拥有了这一系列的操作,内部迭代得以在高效的Stream流的作用下组成流水线,使得函数式编程更方便地实现。


JinhaoPlus
1.5k 声望92 粉丝

扎瓦程序员


引用和评论

0 条评论