Stream 用來處理集合數(shù)據(jù)的,通過 stream 操作可以實(shí)現(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, "小強(qiáng)", 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, '小強(qiáng)', 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);
至于如何實(shí)現(xiàn) select id, name from user 查詢多字段在下面 collector 收集器會(huì)詳細(xì)講解
條件 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 中的元素,并進(jìn)行累加處理,即歸約 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 動(dòng)態(tài)歸約
A mutable reduction operation accumulates input elements into a mutable result container, such as a
CollectionorStringBuilder, as it processes the elements in the stream
區(qū)別:動(dòng)態(tài)歸約將結(jié)果放進(jìn) Collection StringBuilder 這樣的動(dòng)態(tài)容器中,所以稱為動(dòng)態(tài)歸約。
Stream 接口提供了兩個(gè) 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);
我們只需理解了第一個(gè)方法,第二個(gè)方法就手到擒來了
理解第一個(gè) collect 方法,強(qiáng)烈建議閱讀文檔 動(dòng)態(tài)歸約的定義,下面只簡單的介紹一下它
三個(gè)參數(shù):
- 供給者 supplier:負(fù)責(zé)提供動(dòng)態(tài)容器,例如 Collectors、StringBuilder
- 累加器 accumulator:負(fù)責(zé)將流中的元素做累加處理
- 合并者 combiner :負(fù)責(zé)將兩個(gè)容器的元素合并在一起
在串行流中,combiner 根本沒有執(zhí)行,所以隨便寫點(diǎn)啥滿足參數(shù)對(duì)象就行。
如果說串行流是單線程,那么并行流就是多線程了
舉個(gè)例子:
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));
與其傳遞三個(gè)參數(shù)這么麻煩,還不如直接傳遞一個(gè)對(duì)象呢!
這就是第二個(gè) collect() 方法的由來,使用收集器 Collector 來替代三個(gè)參數(shù)
實(shí)際上,我們一般不需要自己創(chuàng)建 Collector 對(duì)象,Java8 提供了一個(gè) Collectors 類,專門提供收集器 Collector 對(duì)象。畢竟我們平時(shí)能夠使用到的收集操作也就那幾種:轉(zhuǎn)為集合對(duì)象、分組、統(tǒng)計(jì)。
下面以例子演示
在初看 stream 操作的時(shí)候,我被什么創(chuàng)建、中間操作、終止操作、不會(huì)改變?cè)瓕?duì)象給弄暈了,我根本不關(guān)心這些,我的第一想法是怎么將操作后的數(shù)據(jù)導(dǎo)出來,重新變成集合對(duì)象。
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() 方法第二個(gè)參數(shù)累加器 accumulator (c, e) -> c.add(e) 這里,對(duì)流中元素進(jìn)行了遍歷,所以可以把流中元素添加到任意的集合容器中,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 一個(gè)
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 對(duì)象數(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中怎么單獨(dú)使用三元運(yùn)算符?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個(gè) toMap(),它們用來處理不同的問題
兩個(gè)參數(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)用了四個(gè)參數(shù)的 toMap() 方法
例子
Map<Integer, User> map1 = userList.stream()
.collect(Collectors.toMap(e -> e.getId(), Function.identity()));
System.out.println(map1);
// Function.identity() 等價(jià)于 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:你想咋辦就咋辦
三個(gè)參數(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);
}
第三個(gè)參數(shù)
mergeFunction就是用來處理 key 鍵沖突的內(nèi)部也是調(diào)用了四個(gè)參數(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 一個(gè) Map 對(duì)象
四個(gè)參數(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)
以例子具體解釋這兩個(gè)方法
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é)果:
小明,小青,小海,阿刁,小陽,小強(qiáng),小帥,小云
,小明,小青,小海,阿刁,小陽,小強(qiáng),小帥,小云
參數(shù)
identity表示返回結(jié)果的初始值
三參數(shù)的 reducing()
reducing(U identity, Function<? super T,? extends U> mapper, BinaryOperator<U> op)
identity 是初始值,mapper 會(huì)對(duì)元素先進(jìn)行一次處理,然后 op 對(duì)元素進(jìn)行歸約操作
注意: 返回類型要和參數(shù)
identity的一致。
你也許會(huì)納悶,為什么有的返回一個(gè)Optional<String>類型數(shù)據(jù),而有的就返回了String
因?yàn)楹袇?shù)identity的 reduing 方法中返回值有初始值,也就是 identity,所以不會(huì)出現(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ù)量,所以返回值是浮點(diǎn)數(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)
這里不演示了,實(shí)際上你看一下 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
最最激動(dòng)人心的時(shí)候到了,我們要使用分組了!?。?/p>
Map<String, List<User>> map = userList.stream()
.collect(Collectors.groupingBy(User::getGender));
SQL 中的 group by 結(jié)果集中只能包含分組字段和聚合函數(shù)計(jì)算結(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='小強(qiáng)', age=14, gender='男', province='陜西省', city='西安市'}, User{id=106, name='小帥', age=15, gender='男', province='河北省', city='石家莊市'}]
它真的分組了??!這是真正的分組
那怎么對(duì)分組中的元素進(jìn)行操作呢,像 SQL 那樣??
完全不用擔(dān)心,Collectors 提供了三個(gè) 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)
讓我們放飛想象的翅膀,思考一下這幾個(gè)參數(shù)分別有什么用。
downstream ?有 down 就表示有 up。那么誰是 upstream,很明顯是 userList.stream,那么 downstream 就是分組集合的流嘍。猜測 downstream 收集器是對(duì)分組中的元素進(jìn)行歸約操作的,就像是分組 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í)不出所料!這就是證明參數(shù) downstream 確實(shí)是分組集合元素的收集器。
Supplier<M> mapFactory 這函數(shù)式接口方法不會(huì)有參數(shù)傳入,所以不會(huì)操作集合元素;它只是返回一個(gè)變量。同志們,注意觀察三個(gè)方法返回值,前二者都指定了 Map 作為歸約操作的返回類型,而第三個(gè)要我們自己定義,使用 mapFactory 提供返回的數(shù)據(jù)容器
兩參數(shù)的 groupingBy 方法其實(shí)是調(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=小強(qiáng), id=105}, {name=小帥, id=106}]}
partitionBy
實(shí)際上也是分組,只不過 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)計(jì)大于10歲的有多少人,小于等于10歲的有多少人
Map<Boolean, Long> map2 = userList.stream()
.collect(Collectors.partitioningBy(e -> e.getAge() > 10, Collectors.counting()));
第二個(gè)參數(shù) downstream 用來處理分組集合
結(jié)語
Java8 提供的 stream 幾乎是窮盡了所有集合元素能有的操作,起碼是窮盡了我腦海里對(duì)集合元素操作的所有想象
這篇文章也列舉了 stream 絕大部分的功能,盡量寫得通俗易懂,但讀者理解起來可能還是有模糊的地方,這時(shí)建議大家參考 Java8 API 官方文檔 ,多做幾個(gè) Demo 加深理解
不要過度使用
stream 是為了方便集合操作,簡化代碼而推出的,提升代碼執(zhí)行效率并不是它的目的。
雖然,并行流會(huì)對(duì)代碼的執(zhí)行效率有較大的提升(尤其是數(shù)據(jù)量非常大的時(shí)候),但也依賴于計(jì)算機(jī)的CPU配置。
Stream 能實(shí)現(xiàn)的功能,for 循環(huán)都能實(shí)現(xiàn),只是 Stream 代碼一般比較簡潔,可讀性強(qiáng)。但在某些情況下,使用 for 循環(huán)要比 Stream 要簡潔代碼邏輯清晰
舉個(gè)例子:
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 對(duì)象的 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);
在這個(gè)例子中,使用 for 循環(huán)要比 使用 stream 干凈利落的多,代碼邏輯清晰簡明,可讀性也比 stream 好。