文章目录
关于Steam
举个例子
java
List<Integer> list = Arrays.asList(1, 2, 3);
// map将int的流,转换为string 的流, 然后对流进行过滤,最后收集到一个list
List<String> list2=list.stream().map(x -> x + "123").filter(x -> x.startsWith("11")).collect(Collectors.toList());
Java 8中的Stream API是一种全新的处理集合数据的方式,它提供了一种非常便捷的方法来对集合中的元素进行筛选、排序、聚合等操作。下面是一些Stream API的特点和应用场景,以及其关键特性:
特点:
- Stream是一种延迟计算的集合,它只有在真正需要使用时才会执行计算操作,可以极大地提高效率。
- Stream可以处理大量数据,其内部使用了多线程的技术,可以自动并行化处理。
- Stream可以实现非常复杂的操作,比如filter、map、reduce等等。
应用场景:
- 数据处理:Stream API可以用于处理大量的数据,比如从数据库或者文件中读取数据,并对其进行处理。
- 并发编程:Stream API内部使用了多线程技术,可以实现并发编程,提高程序的执行效率。
- 功能扩展:Stream API提供了大量的中间操作和终止操作,可以方便地对集合中的元素进行筛选、排序、聚合等操作。
关键特性:
- 流(Stream):Stream是一个数据流,可以看做是一种集合,但是它并不会存储数据,而是通过函数式编程的方式来对数据进行处理。
- 中间操作:Stream提供了大量的中间操作,比如filter、map、distinct、sorted、limit等等,这些中间操作会返回一个新的Stream。
- 终止操作:Stream提供了一些终止操作,比如forEach、count、reduce、collect等等,这些操作会触发Stream的执行,返回一个结果。
- 并行流:Stream提供了并行流的功能,可以利用多线程的技术来提高程序的执行效率。可以通过parallelStream()方法获取一个并行流。
- 支持函数式编程:Stream API使用函数式编程的方式来处理数据,可以大大简化程序的编写过程。
总之,Stream API是Java 8中非常重要的一个新特性,它可以让我们以一种更加简洁、高效的方式来处理集合中的数据,应用场景非常广泛。
对于java转C++的小伙伴来说,这个api实在太好用了,完全离不开它,能否在c++中也实现一套类似的api呢
实现思路
java的实现分析, 详细参考:Java中牛X的Stream流水线操作是怎样实现的 这里简单说明一下:
如下java代码:
java
list.stream().map(x->x+"123").filter(x->x.startsWith("test")).collect(Collectors.toList());
抽象来看 创建了一些列的StatelessOp/StateFullOp ,通过类似链表的方式串联了起来,最终执行的时候,遍历链表将 StatelessOp/StateFullOp 中用户定义操作,通过Sink这个类,一层层wapper,最终在收集的时候执行了这个wrapper。
类似如下过程:

本质来说就是通过一层一层的回调函数的方式将多个操作串联了起来。 那么C++也可以通过回调函数的方式直接实现一个简易的类似java 中Stream的API。
也可以通过生产消费者模型来解释,本质都是一样的:
有AB两个模块,A负责生产数据,B负责消费数据,B不关心A怎么生产,A不关心B怎么消费,可能需要先过滤,转换,或者聚合什么的,这种情况下,传统的做法就是在A提供一个接口,注册一个回调函数,B负责调用,当A生产出数据的时候,调用B注册的回调函数进行消费。
思路打开,如果我们 有ABCD。。。 N 个模块呢,一级级注册回调函数,将这些回调函数级联起来,是不是就很像Stream的api呢,而且由于都是回调函数天生是懒加载 的或者说天生支持反应式编程中的背压机制,就是说会根据消费者的消费速度去生产数据。
stream API 的本质就是注册回调函数,并且在合适的时候触发这个回调函数的调用
接口定义
c++
template<typename T>
class Flow {
protected:
/**
* 关键函数定义:
* 此函数的实现定义了何时调用入参的回调函数(数据的生产),当流生产出数据时就调用入参的回调函数,
* 例如:对于一个vector的流就是循环调用回调函数
* 对于一个无限流,那么就是死循环调用回调函数
*/
std::function<void(std::function<void(T)>)> consume;
public:
explicit Flow(const std::function<void(std::function<void(T)>)> c) : consume(c) {}
}
关键在于 这个成员变量函数,consume: 此函数的实现定义了何时调用入参的回调函数(数据的生产),当流生产出数据时就调用入参的回调函数
将一个Vector转换成流:
c++
static Flow<T> of(std::vector<T> vector) {
return Flow<std::string>([=](const std::function<void(std::string)> &c) {
for (const auto &item: vector) {
c(item);
}
});
}
map
对于map其实就是消费一个流,并且 产生一个新的流。
C++
template<typename R>
Flow<R> map(std::function<R(T)> dataMapFun) {
Flow<T> mapFLow = Flow([=](std::function<void(R)> c) {
//定义了上一个流消费消费逻辑,消费的同时转换数据,产生下一个流
this->consume([=](T data) {
c(dataMapFun(data)); //生产数据
});
});
return mapFLow;
}
记住我们对一个conume函数的定义,调用入参的回调函数即是生产数据
flatmap
c++
template<typename R>
Flow<R> flatMap(std::function<Flow<R>(T)> function) {
return Flow([=](std::function<void(R)> c) {
consume([=](T data) {
Flow<R> flow = function(data);
flow.consume(c);
});
});
}
是不是很简单,只要牢记我们对一个conume函数的定义,调用入参的回调函数即是生产数据
takeWhile
c++
Flow<T> takeWhile(std::function<bool(T)> predicate) {
return Flow(
[=](std::function<void(T)> c) {
consumeTillStop([=](T data) {
if (predicate(data)) {
stop();
}
c(data);
});
}
);
}
void stop() {
throw StopException{};
}
void consumeTillStop(std::function<void(T)> consumer) {
try {
consume(consumer);
} catch (StopException exception) {
}
}
因为都是回调函数,我们只能通过异常通知生产者停止生产
filter
c++
Flow<T> filter(std::function<bool(T)> predicate) {
return Flow(
[=](std::function<void(T)> c) {
consume([=](T data) {
if (predicate(data)) {
c(data);
}
});
}
);
}
sorted
c++
Flow<T> sorted(std::function<bool(T, T)> comparator) {
auto list = this->toVector();
std::sort(list.begin(), list.end(), comparator);
return Flow::of(list);
}
toVector
类似java中toList,这里是最终的操作,不要产生一个新的流了。
c++
std::vector<T> toVector() {
std::vector<T> list;
this->consume([&list](T data) {
list.push_back(data);
});
return list;
}
扩展
上面只给出了部分api的实现,按照这个思路stream中所有api都是可以实现的,上面还给出了java9中才支持的takeWhile。
上面也只给出了Vector转换为流,只要牢记我们那个关键函数的定义 consume:当流生产出数据时就调用入参的回调函数那么就可以实现所有集合的流式操作
那么它仅限于此么?当然不是
并行流
并发多线程调用回调函数,那么这个流就变成了并发的了
文件流
当读出文件时,就调用回调函数,那么它就变成了文件流,而且是基于回调函数的,不必读取所有的文件内容,读一部分,处理一部分,占用内存十分的小
二元流
看看下面这个函数的定义,它又变成了二元流了,当然N元的也不是不可以,是吧
C++
template<typename T1,typename T2>
class Flow {
protected:
/**
* 关键函数定义:
* 此函数的实现定义了何时调用入参的回调函数(数据的生产),当流生产出数据时就调用入参的回调函数,
* 例如:对于一个vector的流就是循环调用回调函数
* 对于一个无限流,那么就是死循环调用回调函数
*/
std::function<void(std::function<void(T1,T2)>)> consume;
}
业务流
最后回到生成者消费者模型,仔细想想也满足上述模型,而且是背压的,少了一个存放数据队列
总结
我们通过级联回调函数的方式实现了java中stream相关api,比起java jdk的实现要简单很多,但是该有的功能都有了。
这些代码的实现我觉得正是 业务逻辑与控制逻辑的分离的体现,api的调用者只关心逻辑,不关系怎么去控制。比如filter你只需要告诉我过滤的的条件是什么,怎么样过滤不需要关心。
也正是封装易变点思想的体现,通过上述api的封装,对于map ,filter等等操作,不用重复去写for循环,if语句,只需要告诉api的逻辑是什么,也就是传入的func
也是函数式编程思想 的体现,无状态的,函数作为参数传递,惰性求值和并性处理等
代码demo
C++
#include <vector>
#include <list>
#include <set>
#include <map>
#include <functional>
/**
* c++ 版本流的实现,通过回调函数的方式实现类似java中stream 的api,目前是demo版本,支持部分功能,看需要可能会支持并发流,二元流等功能
*
* 由于是回调函数实现,天然是懒加载,被压的方式
*
* @tparam T 流的类型
*/
template<typename T>
class Flow {
private:
void stop() {
throw StopException{};
}
void consumeTillStop(std::function<void(T)> consumer) {
try {
consume(consumer);
} catch (StopException exception) {
}
}
protected:
/**
* 关键函数定义:
* 此函数的实现定义了何时调用入参的回调函数(数据的生产),当流生产出数据时就调用入参的回调函数,
* 例如:对于一个vector的流就是循环调用回调函数
* 对于一个无限流,那么就是死循环调用回调函数
*
*
*/
std::function<void(std::function<void(T)>)> consume;
public:
explicit Flow(const std::function<void(std::function<void(T)>)> c) : consume(c) {}
using StopException = std::exception;
void forEach(std::function<void(T)> consumer) {
this->consume(consumer);
}
/**
* 流的转换,消费一个流,产生一个新的流,
*
* 定义了上一个流的消费逻辑,以及本流的产生逻辑
* @tparam R 新的流的类型
* @param dataMapFun
* @return
*/
template<typename R>
Flow<R> map(std::function<R(T)> dataMapFun) {
Flow<T> mapFLow = Flow([=](std::function<void(R)> c) {
consume([=](T data) {
c(dataMapFun(data));
});
});
return mapFLow;
}
template<typename R>
Flow<R> flatMap(std::function<Flow<R>(T)> function) {
return Flow([=](std::function<void(R)> c) {
consume([=](T data) {
Flow<R> flow = function(data);
flow.consume(c);
});
});
}
Flow<T> takeWhile(std::function<bool(T)> predicate) {
return Flow(
[=](std::function<void(T)> c) {
consumeTillStop([=](T data) {
if (predicate(data)) {
stop();
}
c(data);
});
}
);
}
Flow<T> filter(std::function<bool(T)> predicate) {
return Flow(
[=](std::function<void(T)> c) {
consume([=](T data) {
if (predicate(data)) {
c(data);
}
});
}
);
}
Flow<T> dropWhile(std::function<bool(T)> predicate) {
return Flow(
[=](std::function<void(T)> c) {
bool drop = false;
consume([=, &drop](T data) {
if (drop) {
c(data);
}
if (!drop) {
drop = predicate(data);
}
});
}
);
}
Flow<T> skip(int n) {
return Flow(
[=](std::function<void(T)> c) {
int count = n;
consume([=, &count](T data) {
if (count <= 0) {
c(data);
} else {
count--;
}
});
}
);
}
Flow<T> peek(std::function<void(T)> consumer) {
return Flow(
[=](std::function<void(T)> c) {
consume([=](T data) {
consumer(data);
c(data);
});
}
);
}
Flow<T> sorted(std::function<bool(T, T)> comparator) {
auto list = this->toVector();
std::sort(list.begin(), list.end(), comparator);
return Flow::of(list);
}
Flow<T> sorted() {
auto list = this->toVector();
std::sort(list.begin(), list.end());
return Flow::of(list);
}
std::vector<T> toVector() {
std::vector<T> list;
this->consume([&list](T data) {
list.push_back(data);
});
return list;
}
template<typename K>
std::unordered_map<K, std::vector<T>> groupBy(std::function<K(T)> keyFun) {
std::unordered_map<K, std::vector<T>> map;
consume([=, &map](T data) {
K key = keyFun(data);
auto iter = map.find(key);
if (iter != map.end()) {
iter->second.push_back(data);
} else {
std::vector<T> vector;
vector.push_back(data);
map.insert(std::make_pair(key, vector));
}
});
return map;
}
std::set<T> toSet() {
std::set<T> set;
this->consume([&set](T data) {
set.insert(data);
});
return set;
}
static Flow<T> of(std::vector<T> vector) {
return Flow<std::string>([=](const std::function<void(std::string)> &c) {
for (const auto &item: vector) {
c(item);
}
});
}
};
c++
std::vector<std::string> vec;
vec.push_back("12_3");
vec.push_back("12");
vec.push_back("11");
vec.push_back("a");
vec.push_back("b");
Flow<std::string> strFlow = Flow<std::string>::of(vec);
std::cout << "demo1<<<<<<<<<<<<<<<<<<:" << std::endl;
std::vector<std::string> mapVec = strFlow
.map<std::string>([=](const std::string &data) { return data + "123"; })
.peek([=](const std::string &data) { std::cout << "peek:" + data << std::endl; })
.skip(3)
.toVector();
tips
给上面的那些函数起个好名字,可能会让java的同学感到更有亲切感
c++
using Consumer = std::function<void(T)>;
using Function = std::function<T(T)>;
using Predicate = std::function<bool(T)>;
using Comparator = std::function<bool(T,T)>;