mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-25 04:36:41 +08:00
1397 lines
39 KiB
HTML
1397 lines
39 KiB
HTML
<!DOCTYPE html>
|
||
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
|
||
<head>
|
||
|
||
<head>
|
||
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
|
||
<link rel="icon" href="/static/favicon.png">
|
||
|
||
<title>06 Stream如何提高遍历集合效率?.md.html</title>
|
||
|
||
<!-- Spectre.css framework -->
|
||
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
|
||
<!-- theme css & js -->
|
||
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
|
||
</head>
|
||
|
||
|
||
|
||
<body>
|
||
|
||
|
||
|
||
<div class="book-container">
|
||
|
||
<div class="book-sidebar">
|
||
|
||
<div class="book-brand">
|
||
|
||
<a href="/">
|
||
|
||
<img src="/static/favicon.png">
|
||
|
||
<span>技术文章摘抄</span>
|
||
|
||
</a>
|
||
|
||
</div>
|
||
|
||
<div class="book-menu uncollapsible">
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="../">上一级</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/00 开篇词你为什么需要学习并发编程?.md.html">00 开篇词你为什么需要学习并发编程?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/01 如何制定性能调优标准?.md.html">01 如何制定性能调优标准?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/02 如何制定性能调优策略?.md.html">02 如何制定性能调优策略?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/03 字符串性能优化不容小觑,百M内存轻松存储几十G数据.md.html">03 字符串性能优化不容小觑,百M内存轻松存储几十G数据.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/04 慎重使用正则表达式.md.html">04 慎重使用正则表达式.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/05 ArrayList还是LinkedList?使用不当性能差千倍.md.html">05 ArrayList还是LinkedList?使用不当性能差千倍.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
<a class="current-tab" href="/专栏/Java并发编程实战/06 Stream如何提高遍历集合效率?.md.html">06 Stream如何提高遍历集合效率?.md.html</a>
|
||
|
||
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/07 深入浅出HashMap的设计与优化.md.html">07 深入浅出HashMap的设计与优化.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/08 网络通信优化之IO模型:如何解决高并发下IO瓶颈?.md.html">08 网络通信优化之IO模型:如何解决高并发下IO瓶颈?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/09 网络通信优化之序列化:避免使用Java序列化.md.html">09 网络通信优化之序列化:避免使用Java序列化.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/10 网络通信优化之通信协议:如何优化RPC网络通信?.md.html">10 网络通信优化之通信协议:如何优化RPC网络通信?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/11 答疑课堂:深入了解NIO的优化实现原理.md.html">11 答疑课堂:深入了解NIO的优化实现原理.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/12 多线程之锁优化(上):深入了解Synchronized同步锁的优化方法.md.html">12 多线程之锁优化(上):深入了解Synchronized同步锁的优化方法.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/13 多线程之锁优化(中):深入了解Lock同步锁的优化方法.md.html">13 多线程之锁优化(中):深入了解Lock同步锁的优化方法.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/14 多线程之锁优化(下):使用乐观锁优化并行操作.md.html">14 多线程之锁优化(下):使用乐观锁优化并行操作.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/15 多线程调优(上):哪些操作导致了上下文切换?.md.html">15 多线程调优(上):哪些操作导致了上下文切换?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/16 多线程调优(下):如何优化多线程上下文切换?.md.html">16 多线程调优(下):如何优化多线程上下文切换?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/17 并发容器的使用:识别不同场景下最优容器.md.html">17 并发容器的使用:识别不同场景下最优容器.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/18 如何设置线程池大小?.md.html">18 如何设置线程池大小?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/19 如何用协程来优化多线程业务?.md.html">19 如何用协程来优化多线程业务?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/20 磨刀不误砍柴工:欲知JVM调优先了解JVM内存模型.md.html">20 磨刀不误砍柴工:欲知JVM调优先了解JVM内存模型.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/21 深入JVM即时编译器JIT,优化Java编译.md.html">21 深入JVM即时编译器JIT,优化Java编译.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/22 如何优化垃圾回收机制?.md.html">22 如何优化垃圾回收机制?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/23 如何优化JVM内存分配?.md.html">23 如何优化JVM内存分配?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/24 内存持续上升,我该如何排查问题?.md.html">24 内存持续上升,我该如何排查问题?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/25 答疑课堂:模块四热点问题解答.md.html">25 答疑课堂:模块四热点问题解答.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/26 单例模式:如何创建单一对象优化系统性能?.md.html">26 单例模式:如何创建单一对象优化系统性能?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/27 原型模式与享元模式:提升系统性能的利器.md.html">27 原型模式与享元模式:提升系统性能的利器.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/28 如何使用设计模式优化并发编程?.md.html">28 如何使用设计模式优化并发编程?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/29 生产者消费者模式:电商库存设计优化.md.html">29 生产者消费者模式:电商库存设计优化.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/30 装饰器模式:如何优化电商系统中复杂的商品价格策略?.md.html">30 装饰器模式:如何优化电商系统中复杂的商品价格策略?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/31 答疑课堂:模块五思考题集锦.md.html">31 答疑课堂:模块五思考题集锦.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/32 MySQL调优之SQL语句:如何写出高性能SQL语句?.md.html">32 MySQL调优之SQL语句:如何写出高性能SQL语句?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/33 MySQL调优之事务:高并发场景下的数据库事务调优.md.html">33 MySQL调优之事务:高并发场景下的数据库事务调优.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/34 MySQL调优之索引:索引的失效与优化.md.html">34 MySQL调优之索引:索引的失效与优化.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/35 记一次线上SQL死锁事故:如何避免死锁?.md.html">35 记一次线上SQL死锁事故:如何避免死锁?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/36 什么时候需要分表分库?.md.html">36 什么时候需要分表分库?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/37 电商系统表设计优化案例分析.md.html">37 电商系统表设计优化案例分析.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/38 数据库参数设置优化,失之毫厘差之千里.md.html">38 数据库参数设置优化,失之毫厘差之千里.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/39 答疑课堂:MySQL中InnoDB的知识点串讲.md.html">39 答疑课堂:MySQL中InnoDB的知识点串讲.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/41 如何设计更优的分布式锁?.md.html">41 如何设计更优的分布式锁?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/42 电商系统的分布式事务调优.md.html">42 电商系统的分布式事务调优.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/43 如何使用缓存优化系统性能?.md.html">43 如何使用缓存优化系统性能?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/44 记一次双十一抢购性能瓶颈调优.md.html">44 记一次双十一抢购性能瓶颈调优.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/加餐 什么是数据的强、弱一致性?.md.html">加餐 什么是数据的强、弱一致性?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/加餐 推荐几款常用的性能测试工具.md.html">加餐 推荐几款常用的性能测试工具.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/答疑课堂:模块三热点问题解答.md.html">答疑课堂:模块三热点问题解答.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java并发编程实战/结束语 栉风沐雨,砥砺前行!.md.html">结束语 栉风沐雨,砥砺前行!.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
|
||
<div class="sidebar-toggle-inner"></div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<script>
|
||
|
||
function add_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.add('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function remove_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function sidebar_toggle() {
|
||
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let content = document.querySelector('.off-canvas-content')
|
||
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
|
||
sidebar_toggle.classList.remove('extend')
|
||
|
||
sidebar.classList.remove('hide')
|
||
|
||
content.classList.remove('extend')
|
||
|
||
} else { // hide
|
||
|
||
sidebar_toggle.classList.add('extend')
|
||
|
||
sidebar.classList.add('hide')
|
||
|
||
content.classList.add('extend')
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
function open_sidebar() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.add('show')
|
||
|
||
overlay.classList.add('show')
|
||
|
||
}
|
||
|
||
function hide_canvas() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.remove('show')
|
||
|
||
overlay.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
<div class="off-canvas-content">
|
||
|
||
<div class="columns">
|
||
|
||
<div class="column col-12 col-lg-12">
|
||
|
||
<div class="book-navbar">
|
||
|
||
<!-- For Responsive Layout -->
|
||
|
||
<header class="navbar">
|
||
|
||
<section class="navbar-section">
|
||
|
||
<a onclick="open_sidebar()">
|
||
|
||
<i class="icon icon-menu"></i>
|
||
|
||
</a>
|
||
|
||
</section>
|
||
|
||
</header>
|
||
|
||
</div>
|
||
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
|
||
overflow-x: auto;
|
||
|
||
overflow-y: hidden;">
|
||
|
||
<div class="book-post">
|
||
|
||
<p id="tip" align="center"></p>
|
||
|
||
<div><h1>06 Stream如何提高遍历集合效率?</h1>
|
||
|
||
<p>你好,我是刘超。</p>
|
||
|
||
<p>上一讲中,我在讲 List 集合类,那我想你一定也知道集合的顶端接口 Collection。在 Java8 中,Collection 新增了两个流方法,分别是 Stream() 和 parallelStream()。</p>
|
||
|
||
<p>通过英文名不难猜测,这两个方法肯定和 Stream 有关,那进一步猜测,是不是和我们熟悉的 InputStream 和 OutputStream 也有关系呢?集合类中新增的两个 Stream 方法到底有什么作用?今天,我们就来深入了解下 Stream。</p>
|
||
|
||
<h2>什么是 Stream?</h2>
|
||
|
||
<p>现在很多大数据量系统中都存在分表分库的情况。</p>
|
||
|
||
<p>例如,电商系统中的订单表,常常使用用户 ID 的 Hash 值来实现分表分库,这样是为了减少单个表的数据量,优化用户查询订单的速度。</p>
|
||
|
||
<p>但在后台管理员审核订单时,他们需要将各个数据源的数据查询到应用层之后进行合并操作。</p>
|
||
|
||
<p>例如,当我们需要查询出过滤条件下的所有订单,并按照订单的某个条件进行排序,单个数据源查询出来的数据是可以按照某个条件进行排序的,但多个数据源查询出来已经排序好的数据,并不代表合并后是正确的排序,所以我们需要在应用层对合并数据集合重新进行排序。</p>
|
||
|
||
<p>在 Java8 之前,我们通常是通过 for 循环或者 Iterator 迭代来重新排序合并数据,又或者通过重新定义 Collections.sorts 的 Comparator 方法来实现,这两种方式对于大数据量系统来说,效率并不是很理想。</p>
|
||
|
||
<p>Java8 中添加了一个新的接口类 Stream,他和我们之前接触的字节流概念不太一样,Java8 集合中的 Stream 相当于高级版的 Iterator,他可以通过 Lambda 表达式对集合进行各种非常便利、高效的聚合操作(Aggregate Operation),或者大批量数据操作 (Bulk Data Operation)。</p>
|
||
|
||
<p>Stream 的聚合操作与数据库 SQL 的聚合操作 sorted、filter、map 等类似。我们在应用层就可以高效地实现类似数据库 SQL 的聚合操作了,而在数据操作方面,Stream 不仅可以通过串行的方式实现数据操作,还可以通过并行的方式处理大批量数据,提高数据的处理效率。</p>
|
||
|
||
<p><strong>接下来我们就用一个简单的例子来体验下 Stream 的简洁与强大。</strong></p>
|
||
|
||
<p>这个 Demo 的需求是过滤分组一所中学里身高在 160cm 以上的男女同学,我们先用传统的迭代方式来实现,代码如下:</p>
|
||
|
||
<pre><code>Map<String, List<Student>> stuMap = new HashMap<String, List<Student>>();
|
||
|
||
for (Student stu: studentsList) {
|
||
|
||
if (stu.getHeight() > 160) { // 如果身高大于 160
|
||
|
||
if (stuMap.get(stu.getSex()) == null) { // 该性别还没分类
|
||
|
||
List<Student> list = new ArrayList<Student>(); // 新建该性别学生的列表
|
||
|
||
list.add(stu);// 将学生放进去列表
|
||
|
||
stuMap.put(stu.getSex(), list);// 将列表放到 map 中
|
||
|
||
} else { // 该性别分类已存在
|
||
|
||
stuMap.get(stu.getSex()).add(stu);// 该性别分类已存在,则直接放进去即可
|
||
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>我们再使用 Java8 中的 Stream API 进行实现:</p>
|
||
|
||
<p>\1. 串行实现</p>
|
||
|
||
<pre><code>Map<String, List<Student>> stuMap = stuList.stream().filter((Student s) -> s.getHeight() > 160) .collect(Collectors.groupingBy(Student ::getSex));
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>\2. 并行实现</p>
|
||
|
||
<pre><code>Map<String, List<Student>> stuMap = stuList.parallelStream().filter((Student s) -> s.getHeight() > 160) .collect(Collectors.groupingBy(Student ::getSex));
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>通过上面两个简单的例子,我们可以发现,Stream 结合 Lambda 表达式实现遍历筛选功能非常得简洁和便捷。</p>
|
||
|
||
<h2>Stream 如何优化遍历?</h2>
|
||
|
||
<p>上面我们初步了解了 Java8 中的 Stream API,那 Stream 是如何做到优化迭代的呢?并行又是如何实现的?下面我们就透过 Stream 源码剖析 Stream 的实现原理。</p>
|
||
|
||
<h3>1.Stream 操作分类</h3>
|
||
|
||
<p>在了解 Stream 的实现原理之前,我们先来了解下 Stream 的操作分类,因为他的操作分类其实是实现高效迭代大数据集合的重要原因之一。为什么这样说,分析完你就清楚了。</p>
|
||
|
||
<p>官方将 Stream 中的操作分为两大类:中间操作(Intermediate operations)和终结操作(Terminal operations)。中间操作只对操作进行了记录,即只会返回一个流,不会进行计算操作,而终结操作是实现了计算操作。</p>
|
||
|
||
<p>中间操作又可以分为无状态(Stateless)与有状态(Stateful)操作,前者是指元素的处理不受之前元素的影响,后者是指该操作只有拿到所有元素之后才能继续下去。</p>
|
||
|
||
<p>终结操作又可以分为短路(Short-circuiting)与非短路(Unshort-circuiting)操作,前者是指遇到某些符合条件的元素就可以得到最终结果,后者是指必须处理完所有元素才能得到最终结果。操作分类详情如下图所示:</p>
|
||
|
||
<p><img src="assets/ea8dfeebeae8f05ae809ee61b3bf3094.jpg" alt="img" /></p>
|
||
|
||
<p>我们通常还会将中间操作称为懒操作,也正是由这种懒操作结合终结操作、数据源构成的处理管道(Pipeline),实现了 Stream 的高效。</p>
|
||
|
||
<h3>2.Stream 源码实现</h3>
|
||
|
||
<p>在了解 Stream 如何工作之前,我们先来了解下 Stream 包是由哪些主要结构类组合而成的,各个类的职责是什么。参照下图:</p>
|
||
|
||
<p><img src="assets/fc256f9f8f9e3224aac10b2ee8940e00.jpg" alt="img" /></p>
|
||
|
||
<p>BaseStream 和 Stream 为最顶端的接口类。BaseStream 主要定义了流的基本接口方法,例如,spliterator、isParallel 等;Stream 则定义了一些流的常用操作方法,例如,map、filter 等。</p>
|
||
|
||
<p>ReferencePipeline 是一个结构类,他通过定义内部类组装了各种操作流。他定义了 Head、StatelessOp、StatefulOp 三个内部类,实现了 BaseStream 与 Stream 的接口方法。</p>
|
||
|
||
<p>Sink 接口是定义每个 Stream 操作之间关系的协议,他包含 begin()、end()、cancellationRequested()、accpt() 四个方法。ReferencePipeline 最终会将整个 Stream 流操作组装成一个调用链,而这条调用链上的各个 Stream 操作的上下关系就是通过 Sink 接口协议来定义实现的。</p>
|
||
|
||
<h3>3.Stream 操作叠加</h3>
|
||
|
||
<p>我们知道,一个 Stream 的各个操作是由处理管道组装,并统一完成数据处理的。在 JDK 中每次的中断操作会以使用阶段(Stage)命名。</p>
|
||
|
||
<p>管道结构通常是由 ReferencePipeline 类实现的,前面讲解 Stream 包结构时,我提到过 ReferencePipeline 包含了 Head、StatelessOp、StatefulOp 三种内部类。</p>
|
||
|
||
<p>Head 类主要用来定义数据源操作,在我们初次调用 names.stream() 方法时,会初次加载 Head 对象,此时为加载数据源操作;接着加载的是中间操作,分别为无状态中间操作 StatelessOp 对象和有状态操作 StatefulOp 对象,此时的 Stage 并没有执行,而是通过 AbstractPipeline 生成了一个中间操作 Stage 链表;当我们调用终结操作时,会生成一个最终的 Stage,通过这个 Stage 触发之前的中间操作,从最后一个 Stage 开始,递归产生一个 Sink 链。如下图所示:</p>
|
||
|
||
<p><img src="assets/f548ce93fef2d41b03274295aa0a0419.jpg" alt="img" /></p>
|
||
|
||
<p><strong>下面我们再通过一个例子来感受下 Stream 的操作分类是如何实现高效迭代大数据集合的。</strong></p>
|
||
|
||
<pre><code>List<String> names = Arrays.asList(" 张三 ", " 李四 ", " 王老五 ", " 李三 ", " 刘老四 ", " 王小二 ", " 张四 ", " 张五六七 ");
|
||
|
||
|
||
|
||
String maxLenStartWithZ = names.stream()
|
||
|
||
.filter(name -> name.startsWith(" 张 "))
|
||
|
||
.mapToInt(String::length)
|
||
|
||
.max()
|
||
|
||
.toString();
|
||
|
||
</code></pre>
|
||
|
||
<p>这个例子的需求是查找出一个长度最长,并且以张为姓氏的名字。从代码角度来看,你可能会认为是这样的操作流程:首先遍历一次集合,得到以“张”开头的所有名字;然后遍历一次 filter 得到的集合,将名字转换成数字长度;最后再从长度集合中找到最长的那个名字并且返回。</p>
|
||
|
||
<p>这里我要很明确地告诉你,实际情况并非如此。我们来逐步分析下这个方法里所有的操作是如何执行的。</p>
|
||
|
||
<p>首先 ,因为 names 是 ArrayList 集合,所以 names.stream() 方法将会调用集合类基础接口 Collection 的 Stream 方法:</p>
|
||
|
||
<pre><code> default Stream<E> stream() {
|
||
|
||
return StreamSupport.stream(spliterator(), false);
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>然后,Stream 方法就会调用 StreamSupport 类的 Stream 方法,方法中初始化了一个 ReferencePipeline 的 Head 内部类对象:</p>
|
||
|
||
<pre><code> public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
|
||
|
||
Objects.requireNonNull(spliterator);
|
||
|
||
return new ReferencePipeline.Head<>(spliterator,
|
||
|
||
StreamOpFlag.fromCharacteristics(spliterator),
|
||
|
||
parallel);
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>再调用 filter 和 map 方法,这两个方法都是无状态的中间操作,所以执行 filter 和 map 操作时,并没有进行任何的操作,而是分别创建了一个 Stage 来标识用户的每一次操作。</p>
|
||
|
||
<p>而通常情况下 Stream 的操作又需要一个回调函数,所以一个完整的 Stage 是由数据来源、操作、回调函数组成的三元组来表示。如下图所示,分别是 ReferencePipeline 的 filter 方法和 map 方法:</p>
|
||
|
||
<pre><code> @Override
|
||
|
||
public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) {
|
||
|
||
Objects.requireNonNull(predicate);
|
||
|
||
return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE,
|
||
|
||
StreamOpFlag.NOT_SIZED) {
|
||
|
||
@Override
|
||
|
||
Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) {
|
||
|
||
return new Sink.ChainedReference<P_OUT, P_OUT>(sink) {
|
||
|
||
@Override
|
||
|
||
public void begin(long size) {
|
||
|
||
downstream.begin(-1);
|
||
|
||
}
|
||
|
||
|
||
|
||
@Override
|
||
|
||
public void accept(P_OUT u) {
|
||
|
||
if (predicate.test(u))
|
||
|
||
downstream.accept(u);
|
||
|
||
}
|
||
|
||
};
|
||
|
||
}
|
||
|
||
};
|
||
|
||
}
|
||
|
||
@Override
|
||
|
||
@SuppressWarnings("unchecked")
|
||
|
||
public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
|
||
|
||
Objects.requireNonNull(mapper);
|
||
|
||
return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
|
||
|
||
StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
|
||
|
||
@Override
|
||
|
||
Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
|
||
|
||
return new Sink.ChainedReference<P_OUT, R>(sink) {
|
||
|
||
@Override
|
||
|
||
public void accept(P_OUT u) {
|
||
|
||
downstream.accept(mapper.apply(u));
|
||
|
||
}
|
||
|
||
};
|
||
|
||
}
|
||
|
||
};
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>new StatelessOp 将会调用父类 AbstractPipeline 的构造函数,这个构造函数将前后的 Stage 联系起来,生成一个 Stage 链表:</p>
|
||
|
||
<pre><code> AbstractPipeline(AbstractPipeline<?, E_IN, ?> previousStage, int opFlags) {
|
||
|
||
if (previousStage.linkedOrConsumed)
|
||
|
||
throw new IllegalStateException(MSG_STREAM_LINKED);
|
||
|
||
previousStage.linkedOrConsumed = true;
|
||
|
||
previousStage.nextStage = this;// 将当前的 stage 的 next 指针指向之前的 stage
|
||
|
||
|
||
|
||
this.previousStage = previousStage;// 赋值当前 stage 当全局变量 previousStage
|
||
|
||
this.sourceOrOpFlags = opFlags & StreamOpFlag.OP_MASK;
|
||
|
||
this.combinedFlags = StreamOpFlag.combineOpFlags(opFlags, previousStage.combinedFlags);
|
||
|
||
this.sourceStage = previousStage.sourceStage;
|
||
|
||
if (opIsStateful())
|
||
|
||
sourceStage.sourceAnyStateful = true;
|
||
|
||
this.depth = previousStage.depth + 1;
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>因为在创建每一个 Stage 时,都会包含一个 opWrapSink() 方法,该方法会把一个操作的具体实现封装在 Sink 类中,Sink 采用(处理 -> 转发)的模式来叠加操作。</p>
|
||
|
||
<p>当执行 max 方法时,会调用 ReferencePipeline 的 max 方法,此时由于 max 方法是终结操作,所以会创建一个 TerminalOp 操作,同时创建一个 ReducingSink,并且将操作封装在 Sink 类中。</p>
|
||
|
||
<pre><code> @Override
|
||
|
||
public final Optional<P_OUT> max(Comparator<? super P_OUT> comparator) {
|
||
|
||
return reduce(BinaryOperator.maxBy(comparator));
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>最后,调用 AbstractPipeline 的 wrapSink 方法,该方法会调用 opWrapSink 生成一个 Sink 链表,Sink 链表中的每一个 Sink 都封装了一个操作的具体实现。</p>
|
||
|
||
<pre><code> @Override
|
||
|
||
@SuppressWarnings("unchecked")
|
||
|
||
final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
|
||
|
||
Objects.requireNonNull(sink);
|
||
|
||
|
||
|
||
for ( @SuppressWarnings("rawtypes") AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
|
||
|
||
sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
|
||
|
||
}
|
||
|
||
return (Sink<P_IN>) sink;
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>当 Sink 链表生成完成后,Stream 开始执行,通过 spliterator 迭代集合,执行 Sink 链表中的具体操作。</p>
|
||
|
||
<pre><code> @Override
|
||
|
||
final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
|
||
|
||
Objects.requireNonNull(wrappedSink);
|
||
|
||
|
||
|
||
if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
|
||
|
||
wrappedSink.begin(spliterator.getExactSizeIfKnown());
|
||
|
||
spliterator.forEachRemaining(wrappedSink);
|
||
|
||
wrappedSink.end();
|
||
|
||
}
|
||
|
||
else {
|
||
|
||
copyIntoWithCancel(wrappedSink, spliterator);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>Java8 中的 Spliterator 的 forEachRemaining 会迭代集合,每迭代一次,都会执行一次 filter 操作,如果 filter 操作通过,就会触发 map 操作,然后将结果放入到临时数组 object 中,再进行下一次的迭代。完成中间操作后,就会触发终结操作 max。</p>
|
||
|
||
<p>这就是串行处理方式了,那么 Stream 的另一种处理数据的方式又是怎么操作的呢?</p>
|
||
|
||
<h3>4.Stream 并行处理</h3>
|
||
|
||
<p>Stream 处理数据的方式有两种,串行处理和并行处理。要实现并行处理,我们只需要在例子的代码中新增一个 Parallel() 方法,代码如下所示:</p>
|
||
|
||
<pre><code>List<String> names = Arrays.asList(" 张三 ", " 李四 ", " 王老五 ", " 李三 ", " 刘老四 ", " 王小二 ", " 张四 ", " 张五六七 ");
|
||
|
||
|
||
|
||
String maxLenStartWithZ = names.stream()
|
||
|
||
.parallel()
|
||
|
||
.filter(name -> name.startsWith(" 张 "))
|
||
|
||
.mapToInt(String::length)
|
||
|
||
.max()
|
||
|
||
.toString();
|
||
|
||
</code></pre>
|
||
|
||
<p>Stream 的并行处理在执行终结操作之前,跟串行处理的实现是一样的。而在调用终结方法之后,实现的方式就有点不太一样,会调用 TerminalOp 的 evaluateParallel 方法进行并行处理。</p>
|
||
|
||
<pre><code> final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) {
|
||
|
||
assert getOutputShape() == terminalOp.inputShape();
|
||
|
||
if (linkedOrConsumed)
|
||
|
||
throw new IllegalStateException(MSG_STREAM_LINKED);
|
||
|
||
linkedOrConsumed = true;
|
||
|
||
|
||
|
||
return isParallel()
|
||
|
||
? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags()))
|
||
|
||
: terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>这里的并行处理指的是,Stream 结合了 ForkJoin 框架,对 Stream 处理进行了分片,Splititerator 中的 estimateSize 方法会估算出分片的数据量。</p>
|
||
|
||
<p>ForkJoin 框架和估算算法,在这里我就不具体讲解了,如果感兴趣,你可以深入源码分析下该算法的实现。</p>
|
||
|
||
<p>通过预估的数据量获取最小处理单元的阀值,如果当前分片大小大于最小处理单元的阀值,就继续切分集合。每个分片将会生成一个 Sink 链表,当所有的分片操作完成后,ForkJoin 框架将会合并分片任何结果集。</p>
|
||
|
||
<h2>合理使用 Stream</h2>
|
||
|
||
<p>看到这里,你应该对 Stream API 是如何优化集合遍历有个清晰的认知了。Stream API 用起来简洁,还能并行处理,那是不是使用 Stream API,系统性能就更好呢?通过一组测试,我们一探究竟。</p>
|
||
|
||
<p>我们将对常规的迭代、Stream 串行迭代以及 Stream 并行迭代进行性能测试对比,迭代循环中,我们将对数据进行过滤、分组等操作。分别进行以下几组测试:</p>
|
||
|
||
<ul>
|
||
|
||
<li>多核 CPU 服务器配置环境下,对比长度 100 的 int 数组的性能;</li>
|
||
|
||
<li>多核 CPU 服务器配置环境下,对比长度 1.00E+8 的 int 数组的性能;</li>
|
||
|
||
<li>多核 CPU 服务器配置环境下,对比长度 1.00E+8 对象数组过滤分组的性能;</li>
|
||
|
||
<li>单核 CPU 服务器配置环境下,对比长度 1.00E+8 对象数组过滤分组的性能。</li>
|
||
|
||
</ul>
|
||
|
||
<p>由于篇幅有限,我这里直接给出统计结果,你也可以自己去验证一下,具体的测试代码可以在<a href="https://github.com/nickliuchao/stream">Github</a>上查看。通过以上测试,我统计出的测试结果如下(迭代使用时间):</p>
|
||
|
||
<ul>
|
||
|
||
<li>常规的迭代 <Stream 并行迭代 <Stream 串行迭代</li>
|
||
|
||
<li>Stream 并行迭代 < 常规的迭代 <Stream 串行迭代</li>
|
||
|
||
<li>Stream 并行迭代 < 常规的迭代 <Stream 串行迭代</li>
|
||
|
||
<li>常规的迭代 <Stream 串行迭代 <Stream 并行迭代</li>
|
||
|
||
</ul>
|
||
|
||
<p>通过以上测试结果,我们可以看到:在循环迭代次数较少的情况下,常规的迭代方式性能反而更好;在单核 CPU 服务器配置环境中,也是常规迭代方式更有优势;而在大数据循环迭代中,如果服务器是多核 CPU 的情况下,Stream 的并行迭代优势明显。所以我们在平时处理大数据的集合时,应该尽量考虑将应用部署在多核 CPU 环境下,并且使用 Stream 的并行迭代方式进行处理。</p>
|
||
|
||
<p>用事实说话,我们看到其实使用 Stream 未必可以使系统性能更佳,还是要结合应用场景进行选择,也就是合理地使用 Stream。</p>
|
||
|
||
<h2>总结</h2>
|
||
|
||
<p>纵观 Stream 的设计实现,非常值得我们学习。从大的设计方向上来说,Stream 将整个操作分解为了链式结构,不仅简化了遍历操作,还为实现了并行计算打下了基础。</p>
|
||
|
||
<p>从小的分类方向上来说,Stream 将遍历元素的操作和对元素的计算分为中间操作和终结操作,而中间操作又根据元素之间状态有无干扰分为有状态和无状态操作,实现了链结构中的不同阶段。</p>
|
||
|
||
<p>**在串行处理操作中,**Stream 在执行每一步中间操作时,并不会做实际的数据操作处理,而是将这些中间操作串联起来,最终由终结操作触发,生成一个数据处理链表,通过 Java8 中的 Spliterator 迭代器进行数据处理;此时,每执行一次迭代,就对所有的无状态的中间操作进行数据处理,而对有状态的中间操作,就需要迭代处理完所有的数据,再进行处理操作;最后就是进行终结操作的数据处理。</p>
|
||
|
||
<p>**在并行处理操作中,**Stream 对中间操作基本跟串行处理方式是一样的,但在终结操作中,Stream 将结合 ForkJoin 框架对集合进行切片处理,ForkJoin 框架将每个切片的处理结果 Join 合并起来。最后就是要注意 Stream 的使用场景。</p>
|
||
|
||
<h2>思考题</h2>
|
||
|
||
<p>这里有一个简单的并行处理案例,请你找出其中存在的问题。</p>
|
||
|
||
<pre><code>// 使用一个容器装载 100 个数字,通过 Stream 并行处理的方式将容器中为单数的数字转移到容器 parallelList
|
||
|
||
List<Integer> integerList= new ArrayList<Integer>();
|
||
|
||
|
||
|
||
for (int i = 0; i <100; i++) {
|
||
|
||
integerList.add(i);
|
||
|
||
}
|
||
|
||
|
||
|
||
List<Integer> parallelList = new ArrayList<Integer>() ;
|
||
|
||
integerList.stream()
|
||
|
||
.parallel()
|
||
|
||
.filter(i->i%2==1)
|
||
|
||
.forEach(i->parallelList.add(i));
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div>
|
||
|
||
<div style="float: left">
|
||
|
||
<a href="/专栏/Java并发编程实战/05 ArrayList还是LinkedList?使用不当性能差千倍.md.html">上一页</a>
|
||
|
||
</div>
|
||
|
||
<div style="float: right">
|
||
|
||
<a href="/专栏/Java并发编程实战/07 深入浅出HashMap的设计与优化.md.html">下一页</a>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
|
||
</div>
|
||
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997163c8a93d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
|
||
</body>
|
||
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
|
||
<script>
|
||
|
||
window.dataLayer = window.dataLayer || [];
|
||
|
||
|
||
|
||
function gtag() {
|
||
|
||
dataLayer.push(arguments);
|
||
|
||
}
|
||
|
||
|
||
|
||
gtag('js', new Date());
|
||
|
||
gtag('config', 'G-NPSEEVD756');
|
||
|
||
var path = window.location.pathname
|
||
|
||
var cookie = getCookie("lastPath");
|
||
|
||
console.log(path)
|
||
|
||
if (path.replace("/", "") === "") {
|
||
|
||
if (cookie.replace("/", "") !== "") {
|
||
|
||
console.log(cookie)
|
||
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
setCookie("lastPath", path)
|
||
|
||
}
|
||
|
||
|
||
|
||
function setCookie(cname, cvalue) {
|
||
|
||
var d = new Date();
|
||
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
|
||
var expires = "expires=" + d.toGMTString();
|
||
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
|
||
}
|
||
|
||
|
||
|
||
function getCookie(cname) {
|
||
|
||
var name = cname + "=";
|
||
|
||
var ca = document.cookie.split(';');
|
||
|
||
for (var i = 0; i < ca.length; i++) {
|
||
|
||
var c = ca[i].trim();
|
||
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
|
||
}
|
||
|
||
return "";
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
</html>
|
||
|