Stream 用來處理集合數(shù)據(jù)的,通過 stream 操作可以實現(xiàn) SQL 的擁有的大部分查詢功能
下面借助例子迂求,演示 stream 操作
Java userList 列表
private List<User> userList = Arrays.asList(
new User(101, "小明", 10, "男", "青海省", "西寧市"),
new User(102, "小青", 12, "女", "寧夏回族自治區(qū)", "銀川市"),
new User(103, "小海", 8, "男", "西藏自治區(qū)", "拉薩市"),
new User(108, "阿刁", 18, "女", "西藏自治區(qū)", "拉薩市"),
new User(104, "小陽", 9, "女", "新疆維吾爾自治區(qū)", "烏魯木齊市"),
new User(105, "小強", 14, "男", "陜西省", "西安市"),
new User(106, "小帥", 15, "男", "河北省", "石家莊市"),
new User(107, "小云", 15, "女", "河北省", "石家莊市")
);
MySQL user 表數(shù)據(jù)
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) PRIMARY KEY,
`name` varchar(20),
`age` int(2),
`gender` varchar(10),
`province` varchar(100),
`city` varchar(100)
) ;
INSERT INTO `user` VALUES (101, '小明', 10, '男', '青海省', '西寧市');
INSERT INTO `user` VALUES (102, '小青', 12, '女', '寧夏回族自治區(qū)', '銀川市');
INSERT INTO `user` VALUES (103, '小海', 8, '男', '西藏自治區(qū)', '拉薩市');
INSERT INTO `user` VALUES (104, '小陽', 9, '女', '新疆維吾爾自治區(qū)', '烏魯木齊市');
INSERT INTO `user` VALUES (105, '小強', 14, '男', '陜西省', '西安市');
INSERT INTO `user` VALUES (106, '小帥', 15, '男', '河北省', '石家莊市');
INSERT INTO `user` VALUES (107, '小云', 15, '女', '河北省', '石家莊市');
查詢字段 select - map
// select id from user
userList.stream()
.map(e -> e.getId())
.forEach(System.out::println);
至于如何實現(xiàn) select id, name from user
查詢多字段在下面 collector 收集器會詳細講解
條件 where - filter
// select * from user where age<10
userList.stream()
.filter(e-> e.getAge() < 10)
.forEach(System.out::println);
// select * from user where age<10 and gender='男'
userList.stream()
.filter(e->e.getAge() < 10)
.filter(e->e.getGender()=="男")
.forEach(System.out::println);
最值顾稀、總和霸褒、數(shù)量万哪、均值(max, min, sum, count, average)
// select max(age), min(age), sum(age), count(age), avg(age) from user
// max
Optional<Integer> maxAge = userList.stream()
.map(e -> e.getAge())
.max(Comparator.comparingInt(x -> x));
// 等同于
// Optional<Integer> maxAge = userList.stream()
// .map(e -> e.getAge())
// .max((x, y) -> x-y);
// min
Optional<Integer> minAge = userList.stream()
.map(e -> e.getAge())
.min(Comparator.comparingInt(x -> x));
// sum
Optional<Integer> sumAge = userList.stream()
.map(e -> e.getAge())
.reduce((e, u) -> e + u);
// count
long count = userList.stream()
.map(e -> e.getAge())
.count();
// 平均值=總和/數(shù)量
排序 order by - sorted
// select * from user order by age
userList.stream()
.sorted(Comparator.comparingInt(User::getAge))
.forEach(System.out::println);
分頁 limit - skip嗅绰、limit
// select * from user limit 5
userList.stream()
.limit(5)
.forEach(System.out::println);
// select * from user limit 5, 5
userList.stream()
.skip(5)
.limit(5)
.forEach(System.out::println);
// select * from user order by age limit 1
userList.stream()
.sorted(Comparator.comparingInt(User::getAge))
.limit(1)
.forEach(System.out::println);
// 或者
Optional<User> minAgeUser = userList.stream()
.sorted(Comparator.comparingInt(User::getAge))
.findFirst();
是否存在 exists - anymatch
// select exists(select * from user where name='小海')
// 有沒有名字叫“小海”的用戶
boolean exists0 = userList.stream()
.anyMatch(e -> e.getName().equals("小海"));
// select not exists(select * from user where name='小海')
// 是不是沒有名字叫“小合蜃澹”的用戶
boolean exists1 = userList.stream()
.noneMatch(e -> e.getName().equals("小海"));
// 是不是所有用戶年齡都小于10歲
boolean exists2 = userList.stream()
.allMatch(e -> e.getAge() < 10);
收集操作 collect
收集操作就是遍歷 stream 中的元素呵燕,并進行累加處理,即歸約 reduction
A reduction operation (also called a fold) takes a sequence of input elements and combines them into a single summary result by repeated application of a combining operation, such as finding the sum or maximum of a set of numbers, or accumulating elements into a list.
前面提到的 max()
min()
count()
reduce()
都屬于 reduction operation
但 collect()
又和前面這幾種歸約操作有所區(qū)別件相,它是 Mutable reduction 動態(tài)歸約
A mutable reduction operation accumulates input elements into a mutable result container, such as a
Collection
orStringBuilder
, as it processes the elements in the stream
區(qū)別:動態(tài)歸約將結(jié)果放進 Collection
StringBuilder
這樣的動態(tài)容器中再扭,所以稱為動態(tài)歸約。
Stream 接口提供了兩個 collect() 方法
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
<R, A> R collect(Collector<? super T, A, R> collector);
我們只需理解了第一個方法夜矗,第二個方法就手到擒來了
理解第一個 collect 方法泛范,強烈建議閱讀文檔 動態(tài)歸約的定義,下面只簡單的介紹一下它
三個參數(shù):
- 供給者 supplier:負責提供動態(tài)容器侯养,例如 Collectors敦跌、StringBuilder
- 累加器 accumulator:負責將流中的元素做累加處理
- 合并者 combiner :負責將兩個容器的元素合并在一起
在串行流中,combiner 根本沒有執(zhí)行逛揩,所以隨便寫點啥滿足參數(shù)對象就行柠傍。
如果說串行流是單線程,那么并行流就是多線程了
舉個例子:
ArrayList<String> strings = new ArrayList<>();
for (T element : stream) {
strings.add(element.toString());
}
// 等同于
ArrayList<String> strings = stream.collect(() -> new ArrayList<>(),
(c, e) -> c.add(e.toString()),
(c1, c2) -> c1.addAll(c2));
與其傳遞三個參數(shù)這么麻煩辩稽,還不如直接傳遞一個對象呢惧笛!
這就是第二個 collect()
方法的由來,使用收集器 Collector 來替代三個參數(shù)
實際上逞泄,我們一般不需要自己創(chuàng)建 Collector 對象患整,Java8 提供了一個 Collectors 類,專門提供收集器 Collector 對象喷众。畢竟我們平時能夠使用到的收集操作也就那幾種:轉(zhuǎn)為集合對象各谚、分組、統(tǒng)計到千。
下面以例子演示
在初看 stream 操作的時候昌渤,我被什么創(chuàng)建、中間操作憔四、終止操作膀息、不會改變原對象給弄暈了,我根本不關(guān)心這些了赵,我的第一想法是怎么將操作后的數(shù)據(jù)導出來潜支,重新變成集合對象。
toCollection
不使用收集器的情況下:
List<User> subUserList1 = userList.stream()
.filter(e -> e.getAge() < 10)
.filter(e -> e.getGender() == "男")
.collect(() -> new ArrayList<>(),
(c, e) -> c.add(e),
(c1, c2) -> c1.addAll(c2));
在 collect() 方法第二個參數(shù)累加器 accumulator (c, e) -> c.add(e)
這里柿汛,對流中元素進行了遍歷冗酿,所以可以把流中元素添加到任意的集合容器中,List、Set已烤、Map 等等
使用 Collectors 工具類提供的收集器:
// toList()
List<User> list = userList.stream()
.filter(e -> e.getAge() < 10)
.filter(e -> e.getGender() == "男")
.collect(Collectors.toList());
// toSet()
Set<User> set = userList.stream()
.filter(e -> e.getAge() < 10)
.filter(e -> e.getGender() == "男")
.collect(Collectors.toSet());
// toCollection()鸠窗,想要返回什么容器,就 new 一個
ArrayList<User> collection = userList.stream()
.filter(e -> e.getAge() < 10)
.filter(e -> e.getGender() == "男")
.collect(Collectors.toCollection(
() -> new ArrayList<>()
));
這里插播一條新聞:如何將流轉(zhuǎn)為數(shù)組胯究?
Stream
提供了方法 toArray()
Object[] toArray();
<A> A[] toArray(IntFunction<A[]> generator);
小試牛刀:
Object[] nameArray = userList.stream()
.map(e -> e.getName())
.toArray();
Arrays.stream(nameArray)
.forEach(System.out::println);
// 轉(zhuǎn)為 User 對象數(shù)組
User[] users = userList.stream()
.filter(e -> e.getGender() == "女")
.toArray(User[]::new);
Arrays.stream(users)
.forEach(System.out::println);
toStringBuilder
不使用收集器的情況下:
StringBuilder joinName = userList.stream()
.map(e -> e.getName())
.collect(StringBuilder::new,
(s, e) -> s = s.length() > 0 ? s.append("-" + e) : s.append(e),
(s1, s2) -> s1.append(s2)
);
誰能告訴我在Java中怎么單獨使用三元運算符稍计?s = s.length() > 0 ? s.append("-" + e) : s.append(e)
我想把 s =
省略掉,但 Java 中不行
使用 Collectors
類提供的收集器:
String joinName1 = userList.stream()
.map(e -> e.getName())
.collect(Collectors.joining());
String joinName2 = userList.stream()
.map(e -> e.getName())
.collect(Collectors.joining("-"));
String joinName3 = userList.stream()
.map(e -> e.getName())
.collect(Collectors.joining("-", "[", "]"));
至于 Collectors.joining() 參數(shù)分別代表什么含義裕循,看一下它們的參數(shù)名稱臣嚣,就明白了
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter, // 分隔符
CharSequence prefix, // 前綴
CharSequence suffix) // 后綴
toMap
在 Collectors 中一共有3個 toMap()
,它們用來處理不同的問題
兩個參數(shù)的 toMap
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}
參數(shù)
keyMapper
用來獲取key;valueMapper
用來獲取 value它的內(nèi)部調(diào)用了四個參數(shù)的 toMap() 方法
例子
Map<Integer, User> map1 = userList.stream()
.collect(Collectors.toMap(e -> e.getId(), Function.identity()));
System.out.println(map1);
// Function.identity() 等價于 e -> e
// select id, name, gender from user
Map<Integer, Map<String, Object>> map2 = userList.stream()
.collect(Collectors.toMap(e -> e.getId(), e -> {
Map<String, Object> map = new HashMap<>();
map.put("gender", e.getGender());
map.put("name", e.getName());
map.put("id", e.getId());
return map;
}));
System.out.println(map2);
你:如果 key 沖突了咋辦剥哑?
Java8:你想咋辦就咋辦
三個參數(shù)的 toMap
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction) {
return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}
第三個參數(shù)
mergeFunction
就是用來處理 key 鍵沖突的內(nèi)部也是調(diào)用了四個參數(shù)的 toMap() 方法
例子
// 如果 key 沖突硅则,那么將沖突的 value 值拼接在一起
Map<String, String> map3 = userList.parallelStream()
.collect(Collectors.toMap(
e -> e.getGender(),
e -> e.getName(),
(o1, o2) -> o1 + ", " + o2
)
);
System.out.println(map3);
你:我想自己 new 一個 Map 對象
四個參數(shù)的 toMap
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier)
參數(shù)
mapSupplier
用來提供返回容器
例子
LinkedHashMap<String, String> map4 = userList.parallelStream()
.collect(Collectors.toMap(e -> e.getGender(), e -> e.getName(), (o1, o2) -> o1 + ", " + o2, LinkedHashMap::new));
System.out.println(map4);
reducing
單參數(shù)和兩參數(shù)的 reducing()
Collector<T, ?, Optional<T>> reducing(BinaryOperator<T> op)
Collector<T, ?, U> reducing(U identity, Function<? super T,? extends U> mapper, BinaryOperator<U> op)
以例子具體解釋這兩個方法
Optional<String> names1 = userList.stream()
.map(User::getName)
.collect(Collectors.reducing((e1, e2) -> e1 + "," + e2));
System.out.println(names1.get());
// 等同于
String names2 = userList.stream()
.collect(Collectors.reducing(
"", (e) -> e.getName(), (e1, e2) -> e1 + "," + e2)
);
System.out.println(names2);
輸出結(jié)果:
小明,小青,小海,阿刁,小陽,小強,小帥,小云
,小明,小青,小海,阿刁,小陽,小強,小帥,小云
參數(shù)
identity
表示返回結(jié)果的初始值
三參數(shù)的 reducing()
reducing(U identity, Function<? super T,? extends U> mapper, BinaryOperator<U> op)
identity
是初始值,mapper
會對元素先進行一次處理株婴,然后 op
對元素進行歸約操作
注意: 返回類型要和參數(shù)
identity
的一致怎虫。
你也許會納悶,為什么有的返回一個Optional<String>
類型數(shù)據(jù)困介,而有的就返回了String
因為含有參數(shù)identity
的 reduing 方法中返回值有初始值大审,也就是 identity,所以不會出現(xiàn)空的情況
下面Collectors 提供的一些常用歸約收集器
// minBy座哩、maxBy
Optional<User> minAgeUser = userList.stream()
.collect(Collectors.minBy((o1, o2) -> o1.getAge() - o2.getAge()));
// counting
Long count = userList.stream()
.collect(Collectors.counting());
// summingInt徒扶、summingLong、summingDouble根穷、
Integer sumAge = userList.stream()
.collect(Collectors.summingInt(User::getAge));
// averagingInt姜骡、averagingLong、averagingDouble
// 平均值內(nèi)部是總值/數(shù)量屿良,所以返回值是浮點數(shù) dobule
Double avgAge = userList.stream()
.collect(Collectors.averagingInt(User::getAge));
你也許覺得每次都要執(zhí)行一遍 minBy圈澈、maxBy、counting尘惧、summingXxx康栈、averagingXxx 這些太麻煩了,有沒有一次執(zhí)行就獲取所有這些方法結(jié)果褥伴?
有的。這就是 summarizingXxx
Collector<T, ?, IntSummaryStatistics> summarizingInt(ToIntFunction<? super T> mapper)
Collector<T, ?, LongSummaryStatistics> summarizingLong(ToLongFunction<? super T> mapper)
Collector<T, ?, DoubleSummaryStatistics> summarizingDouble(ToDoubleFunction<? super T> mapper)
這里不演示了漾狼,實際上你看一下 XxxSummaryStatistics
這些類就明白了重慢,比如
public class IntSummaryStatistics implements IntConsumer {
private long count;
private long sum;
private int min = Integer.MAX_VALUE;
private int max = Integer.MIN_VALUE;
...
}
group by
最最激動人心的時候到了,我們要使用分組了Q吩辍K契狻!
Map<String, List<User>> map = userList.stream()
.collect(Collectors.groupingBy(User::getGender));
SQL 中的 group by
結(jié)果集中只能包含分組字段和聚合函數(shù)計算結(jié)果,這段代碼比它更加全面
我們使用如下語句輸出結(jié)果
map.keySet().stream()
.forEach((e) -> {
System.out.println(e + "=" + map.get(e));
});
顯示結(jié)果:
女=[User{id=102, name='小青', age=12, gender='女', province='寧夏回族自治區(qū)', city='銀川市'}, User{id=108, name='阿刁', age=18, gender='女', province='西藏自治區(qū)', city='拉薩市'}, User{id=104, name='小陽', age=9, gender='女', province='新疆維吾爾自治區(qū)', city='烏魯木齊市'}, User{id=107, name='小云', age=15, gender='女', province='河北省', city='石家莊市'}]
男=[User{id=101, name='小明', age=10, gender='男', province='青海省', city='西寧市'}, User{id=103, name='小海', age=8, gender='男', province='西藏自治區(qū)', city='拉薩市'}, User{id=105, name='小強', age=14, gender='男', province='陜西省', city='西安市'}, User{id=106, name='小帥', age=15, gender='男', province='河北省', city='石家莊市'}]
它真的分組了:搜俊囚戚!這是真正的分組
那怎么對分組中的元素進行操作呢,像 SQL 那樣轧简?驰坊?
完全不用擔心,Collectors 提供了三個 groupBy 方法返回分組收集器
Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T,? extends K> classifier)
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T,? extends K> classifier,
Collector<? super T,A,D> downstream)
Collector<T, ?, M> groupingBy(Function<? super T,? extends K> classifier,
Supplier<M> mapFactory,
Collector<? super T,A,D> downstream)
讓我們放飛想象的翅膀哮独,思考一下這幾個參數(shù)分別有什么用拳芙。
downstream ?有 down 就表示有 up皮璧。那么誰是 upstream舟扎,很明顯是 userList.stream
,那么 downstream 就是分組集合的流嘍悴务。猜測 downstream 收集器是對分組中的元素進行歸約操作的睹限,就像是分組 SQL 語句字段中的聚合操作一樣。
// select gender, count(*) from user group by gender
Map<String, Long> map2 = userList.stream()
.collect(Collectors.groupingBy(User::getGender, Collectors.counting()));
System.out.println(map2);
輸出結(jié)果確實不出所料讯檐!這就是證明參數(shù) downstream
確實是分組集合元素的收集器羡疗。
Supplier<M> mapFactory
這函數(shù)式接口方法不會有參數(shù)傳入,所以不會操作集合元素裂垦;它只是返回一個變量顺囊。同志們,注意觀察三個方法返回值蕉拢,前二者都指定了 Map 作為歸約操作的返回類型特碳,而第三個要我們自己定義,使用 mapFactory
提供返回的數(shù)據(jù)容器
兩參數(shù)的 groupingBy
方法其實是調(diào)用了三參數(shù)的 groupingBy
方法(而單參數(shù) groupingBy
調(diào)用了兩參數(shù)的 groupingBy
)
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
Collector<? super T, A, D> downstream) {
return groupingBy(classifier, HashMap::new, downstream);
}
groupingBy
經(jīng)常使用 Collectors.mapping()
處理分組集合
Map<String, List<Map<String, Object>>> map4 = userList.stream()
.collect(Collectors.groupingBy(
User::getGender,
Collectors.mapping(e -> {
Map<String, Object> m = new HashMap<>();
m.put("name", e.getName());
m.put("id", e.getId());
return m;
}, Collectors.toList())
));
System.out.println(map4);
輸出結(jié)果:
{女=[{name=小青, id=102}, {name=阿刁, id=108}, {name=小陽, id=104}, {name=小云, id=107}], 男=[{name=小明, id=101}, {name=小海, id=103}, {name=小強, id=105}, {name=小帥, id=106}]}
partitionBy
實際上也是分組晕换,只不過 partitionBy 是按照布爾值(真假)來分組
Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) {
return partitioningBy(predicate, toList());
}
Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,
Collector<? super T, A, D> downstream)
例子:大于10歲為一組午乓,小于等于10的為一組
Map<Boolean, List<User>> map1 = userList.stream()
.collect(Collectors.partitioningBy(e -> e.getAge() > 10));
例子:統(tǒng)計大于10歲的有多少人,小于等于10歲的有多少人
Map<Boolean, Long> map2 = userList.stream()
.collect(Collectors.partitioningBy(e -> e.getAge() > 10, Collectors.counting()));
第二個參數(shù) downstream
用來處理分組集合
結(jié)語
Java8 提供的 stream 幾乎是窮盡了所有集合元素能有的操作闸准,起碼是窮盡了我腦海里對集合元素操作的所有想象
這篇文章也列舉了 stream 絕大部分的功能益愈,盡量寫得通俗易懂,但讀者理解起來可能還是有模糊的地方夷家,這時建議大家參考 Java8 API 官方文檔 蒸其,多做幾個 Demo 加深理解
不要過度使用
stream 是為了方便集合操作,簡化代碼而推出的库快,提升代碼執(zhí)行效率并不是它的目的摸袁。
雖然,并行流會對代碼的執(zhí)行效率有較大的提升(尤其是數(shù)據(jù)量非常大的時候)义屏,但也依賴于計算機的CPU配置靠汁。
Stream 能實現(xiàn)的功能蜂大,for 循環(huán)都能實現(xiàn),只是 Stream 代碼一般比較簡潔蝶怔,可讀性強奶浦。但在某些情況下,使用 for 循環(huán)要比 Stream 要簡潔代碼邏輯清晰
舉個例子:
private List<Order> orderList = Arrays.asList(
new Order(103),
new Order(106),
new Order(107),
new Order(104),
new Order(102),
new Order(103),
new Order(102),
new Order(101),
new Order(104),
new Order(102),
new Order(105)
);
// 現(xiàn)根據(jù) userId 設(shè)置 Order 對象的 name 屬性
// 使用 stream
List<Order> newOrderList = orderList.stream()
.map(o -> userList.stream()
.filter(u -> u.getId() == o.getUserId())
.findFirst()
.map(u -> {
o.setUserName(u.getName());
return o;
})
.orElse(o))
.collect(Collectors.toList());
newOrderList.stream().forEach(System.out::println);
// 使用 for 循環(huán)
for (Order o : orderList) {
for (User u : userList) {
if (o.getUserId() == u.getId()) {
o.setUserName(u.getName());
break;
}
}
}
orderList.stream().forEach(System.out::println);
在這個例子中踢星,使用 for 循環(huán)要比 使用 stream 干凈利落的多澳叉,代碼邏輯清晰簡明,可讀性也比 stream 好斩狱。