首页 文章 精选 留言 我的

精选列表

搜索[玩转Lighthouse],共1675篇文章
优秀的个人博客,低调大师

Zadig 快速体验,玩转本地安装!

啥是本地安装? 听过 Zadig 支持All in One 安装、基于 Kubernetes 安装、基于 Helm 安装等多种安装方式,怎么又来了个本地安装?(这么多安装方式谁听起来头不大) 别急,先看一段对话: (本对话内容基于真实场景模拟,如有雷同,实属巧合) 目标人群 需要在本地快速体验上手,不需要数据持久化保存、不需要生产环境使用。 开源项目好奇宝宝: 开源的云原生持续交付平台?赶紧让我下载安装看看你有什么花活,安装太麻烦我就放弃了。 云原生开发工程师: 虽然你有 Helm 安装,可是做云原生工程师,日常修改 YAML 已经吐了,实在不想看那么多安装的参数,我只想在自己的电脑快速体验下,我有 Docker for desktop 可以启动集群,能不能来个一键脚本给个痛快? 技术 Leader: 团队最近在做云原生持续交付平台的调研选型,听说 Zadig 很强啊,在圈内也很火,赶紧让哪个谁谁谁快速安装了解下,是否适合团队使用,调研后再决定是否上生产。 实施工程师(技术 Leader の 小弟): Leader 需要这边安装体验下 Zadig,尽快出个报告,可是申请集群资源好麻烦啊,层层审批估计疫情到时候都结束了。好烦啊,自己电脑搞个虚拟机弄个集群或者安装个 minikube 不知道能不能行? 实施工程师(技术 Leader の 小弟): Leader 需要这边安装体验下 Zadig,尽快出个报告,可是申请集群资源好麻烦啊,层层审批估计疫情到时候都结束了。好烦啊,自己电脑搞个虚拟机弄个集群或者安装个 minikube 不知道能不能行? 没关系,以上统统安排! Zadig 作为一款开源云原生持续交付产品,支持多种安装方式,每种安装方式又适用于不同的使用场景,例如基于 Helm 命令的安装方式,适用于生产使用,而且对集群资源有一定要求。而对于资源无法满足要求但又对 Zadig 感兴趣的大量开发者来说,如何实现快速体验?成为我们团队需要关注和解决的一个问题。 于是我们推出了本地安装,帮助新人在本机尝鲜和快速体验 Zadig。 如何进行本地安装? 前提 使用 minikube、KinD 等工具在本地拉起一套 K8s 集群,参考如下: a.安装 minikube[1] b.安装 docker-desktop[2] c. 更多工具请参考其官方安装文档 确保本地 K8s 集群满足至少 4C8G 的资源配置,版本满足 v1.16~v1.22。 第一步:安装 Zadig 在本地集群中执行以下脚本: 若安装成功后需要集成外部系统(比如:代码源),请确保使用的IP 地址可外网访问。 1 export IP=<本机 IP 地址>2 export PORT=<任意一合法的 K8s Node Port>3 curl -SsL https://download.koderover.com/install?type=quickstart | bash 安装成功后系统会自动初始化登录账号和密码。 第二步:访问 Zadig 小贴士: 如果使用的是 KinD 拉起的集群,由于其自身特性,需要打通本机端口到 K8s 集群 NodePort 服务的通路,参考命令如下: 1 kubectl -n zadig port-forward svc/gateway-proxy 32000:80 访问 IP: PORT,使用默认账号密码 admin/zadig 登录成功后,即可愉快玩耍了~ One More Thing 我们计划在后续的更新中,支持内置的 demo 项目,在本地安装成功后即可直接体验工作流、环境、服务部署等功能,缩短从配置到使用的路径,做到开箱即用,降低体验 Zadig 的门槛。 参考链接 [1]https://minikube.sigs.k8s.io/docs/start/ [2]https://www.docker.com/products/docker-desktop/ Zadig,让工程师更专注创造!

优秀的个人博客,低调大师

玩转 HelloGitHub 的新姿势

本文不会涉及太多技术细节和源码,请放心食用 大家好,我是 HelloGitHub 的老荀,好久不见啊! 我在完成 HelloZooKeeper 系列之后,就很少“露面了”。但是我对开源和 HelloGitHub 的热情并没有丝毫的减少。这不,逮着个机会就来输出一波,防止被大家遗忘😂。 这次带来的是我写的一款在终端浏览 HelloGitHub 的工具:hg-tui,让你双手不离开键盘就能畅游在 HG 的开源世界。功能如下: 色彩丰富、平铺展示 关键字搜索月刊往期的项目 类 Vim 的快捷键操作方式 一键直达开源项目首页 支持 Linux、macOS、Windows 地址:https://github.com/kaixinbaba/hg-tui 下面我将分享自己发起这个开源项目的缘起、构思、再到开发的全部过程,最后分享一下,我通过做这个项目对开源的一些感悟。 一、起因 我本职是做 Java 开发,但架不住 Rust 太有意思了!所以最近在学 Rust 恰好前段时间看到 HG 讲解 tui.rs 的文章。 看完后手痒得厉害,就写了一篇 tui.rs 入门文章,但感觉还不过瘾就想写一个项目练手。 因为我平时经常上 HelloGitHub 找开源项目,所以就决定用 tui.rs 做一个终端浏览 HelloGitHub 官网的工具。 官网:https://hellogithub.com/ 二、构思 首先我希望这个应用能有以下功能: 有搜索框,可以按关键词搜索 HelloGitHub 中的任意项目 通过表格按列展示搜索结果 既然是终端应用,那操作方式肯定是使用键盘方式,快捷键我采用了一些大家熟知的 Vim 快捷键 浏览项目的途中,可以随时在浏览器中打开当前浏览的项目 有了这些主要功能点的思路,下面就要想想怎么设计一个界面了,我本职工作后端一碰到画界面就头疼,几经周折大概把界面设计成了这样: 又因为是 TUI 界面层级不能太深,所以再多弄个详情页面(用来浏览文字明细)或者弹窗页面(提示消息)就差不多了。 我又想到了 GitHub 为每一种编程语言都设计了一种颜色,我也可以把这些颜色应用在我的项目里,让整个终端界面看起来没那么单调,色彩更丰富。效果如下: 主界面: 详情页: 弹窗提示: 最后为了向 TUI 妥协,按期数或类别搜索,我是通过使用搜索前缀来和普通关键词搜索作出区别。 上面展示的这些差不多已经是这个项目的全部了 三、开发 3.1 技术选型 要实现上述的那些功能,就要从 Rust 的生态中选择合适的库了 下面这些是我在这个项目中使用到的: 基础设施:anyhow、thiserror、lazy_static、better-panic 绘制 UI:tui、crossterm HTTP client:reqwest 缓存:cached HTML 解析:nipper 工具:regex、crossbeam-channel 命令行:clap 虽然 Rust 还是编程界的小学生(2011 年启动),但是经过了这些年的发展,生态已经逐渐完善,工具库已经很丰富了。再加上 Rust 是系统级的语言,值得投入时间学习! 3.2 项目结构 项目结构规划(非全部) src ├── app.rs // 统一管理整个应用的状态 ├── cli.rs // 命令行解析 ├── draw.rs // 绘制 UI ├── events.rs // UI 事件、输入事件、通知 ├── fetch.rs // HTTP 请求 ├── main.rs // 入口 ├── parse.rs // HTML 解析 ├── utils.rs // 工具 └── widget // 自定义组件 ├── ... 合理的分文件(目录)开发,可以让每个功能模块 高内聚、低耦合,并且可以很容易地分开进行单元测试。 当然这些文件也不是在项目之初就已经一股脑地建立好的,都是在完善功能的路上一点点添加进来的~ 3.3 主要代码 因为是基于 tui.rs 开发的应用,所以主流程肯定是遵循该库的设计的,首先需要定义一个 App 用来保存整个项目的状态信息。 pub struct App { /// 用户输入框 pub input: InputState, /// 内容展示 pub content: ContentState, /// 弹窗提示 pub popup: PopupState, /// 状态栏 pub statusline: StatusLineState, /// 模式 pub mode: AppMode, /// 项目明细子页面 pub project_detail: ProjectDetailState, ... } 每一个状态字段,其实就是对应一个自定义组件.要在 tui.rs 中实现自定义组件(实现方式也是我自己的理解)也很简单只要三步,我以 Input 组件为例。 /// 用户输入框组件,组件本身没有字段,是一个无状态的对象 /// 无状态对象只关心 UI 怎么绘制,不存储数据 pub struct Input {} /// 组件的状态,每一个字段就是组件需要存储的数据 #[derive(Debug)] pub struct InputState { input: String, active: bool, pub mode: SearchMode, } /// 最后为 Input 组件实现 StatefulWidget trait impl StatefulWidget for Input { type State = InputState; // 指定关联类型为 InputState /// area 绘制的区域 /// buf 缓冲区(可以直接写入字符串,如果要高度定制的话,可以理解为画笔) /// state 从这个变量中直接取绘制过程中需要的数据 fn render(self, area: Rect, buf: &amp;mut Buffer, state: &amp;mut Self::State) { // 具体绘制的逻辑 ... } } 只要是面向用户的应用,都会处理各种各样的用户输入(事件)。Rust 中一般都使用 channel 来解耦处理各种各样的事件,再利用 Rust 强大的枚举支持,定义各种各样的事件(用户输入和非用户输入)即可。 /// 定义事件枚举 #[derive(Debug, Clone)] pub enum HGEvent { /// 用户事件(键盘事件) UserEvent(KeyEvent), /// 应用内部组件的通知事件 NotifyEvent(Notify), } #[derive(Debug, Clone, PartialEq)] pub enum Notify { /// 重绘界面 Redraw, /// 退出应用 Quit, /// 弹出窗口展示消息 Message(Message), /// tick,比如一些数据需要每隔一段时间自动更新的(比如:显示的时间) Tick, } /// 弹窗的消息,分为 错误、警告、提示 #[derive(Debug, Clone, PartialEq)] pub enum Message { Error(String), Warn(String), Tips(String), } 为了区分用户事件和通知,我使用了两个不同的 channel 分别处理这两类: lazy_static! { /// 因为通知队列希望被应用内部共享,所以使用了 lazy_static 方便使用 pub static ref NOTIFY: (Sender<hgevent>, Receiver<hgevent>) = bounded(1024); } 又因为不同的事件处理,并不应该互相阻塞,所以整个应用采用了最基础的多线程模型来提高性能,这里使用的也是标准库的多线程。 pub fn handle_key_event(event_app: Arc<mutex<app>&gt;) { let (sender, receiver) = unbounded(); ... std::thread::spawn(move || loop { // 单独一个线程接收用户事件 if let Ok(Event::Key(event)) = crossterm::event::read() { sender.send(HGEvent::UserEvent(event)).unwrap(); } }); std::thread::spawn(move || loop { // 单独一个线程处理用户事件 if let Ok(HGEvent::UserEvent(key_event)) = receiver.recv() { ... } }); } 其他剩下的就是业务逻辑,完整的代码可以直接看仓库 https://github.com/kaixinbaba/hg-tui 四、心路历程 一开始我做 hg-tui 项目的时候,仅仅是为了做个实际的项目把玩一下 tui.rs 这个框架,做好之后问题层出不穷,但我深知没有与生俱来的完美,只有不断的迭代才能让它越来越好,经过 100 多次的提交后,现在用着感觉顺手多了。毕竟作者是项目的第一个用户,自己用着不舒服其他人就更不喜欢了! 我想着既然要让别人用,一定要容易安装。接着我做了基于 GitHub Action 自动编译和发布,支持 Windows、Linux、macOS 直接下载就能用。 我还做了对 homebrew 安装的支持,但因为 Star 数不够没有收录到 homecore 要求:30 forks、30 watchers、75 stars 希望大家看到这里的话能给个 star✨ 地址:https://github.com/kaixinbaba/hg-tui 五、最后 hg-tui 它从出生那一刻起,体内流淌的就是开源的血。 它很小甚至是微不足道,我本不想开源,但蛋蛋的一段话让我改变了主意:开源不是完结,仅仅只是开始。 一个开源项目可能只是作者的一个灵光乍现,也可能只是为了解决自己实际工作生活中的小小痛点,没准用完就丢到角落里了。但开源出来或许就能找到有相同需求的人,从而延续这个项目的生命,或许这就是开源的本意吧。 以上就是我做这个项目的全部心得和收获,如果你们对 hg-tui 有什么建议和问题,欢迎给我提 issue 最后,如果你喜欢本文和项目的话,欢迎点赞和 Star 爱你们哟~

优秀的个人博客,低调大师

在 BatchCompute 上玩转 Blender 渲染

本篇主要是介绍如何基于 BatchCompute 的 App,提交 Blender 渲染作业。 1. 准备工作 (1) 开通服务 开通批量计算服务(BatchCompute): https://help.aliyun.com/document_detail/127644.html 开通对象存储服务(OSS): https://oss.console.aliyun.com 开通MNS服务: https://mns.console.aliyun.com 开通容器服务: https://cr.console.aliyun.com 如果已经开通,请忽略此步骤。 (2) 地域的选择 本篇例子所有阿里云服务都需要使用相同的地域。 本篇例子使用地域: 华南1(深圳) (3) 准备OSS Bucket 请到OSS控制台 创建一个Bucket。 本篇例子假设创建的 buc

优秀的个人博客,低调大师

玩转mybatis中的类型转换

1.场景 日常java开发中经常有这种需求,用0或者1这些代码(不局限于数字)来表示某种状态。比如用0表示女性,用1来表示男性。而且写入数据库可能是一个标识,从数据库读取又还原为具体的说明。而且一般情况下为了更好理解或者消除魔法值,通常的处理方案是定义一个枚举: 有些枚举是这样定义的 public enum GenderType{ FEMALE,MALE,UNKNOWN } 那么通常很多人会这么入库(java伪代码) if(GenderType.MALE){ // 写入 1 }else if(GenderType.FEMALE){ // 写入 0 }else{ //也可能是泰国回来的 那就 2 } 读取的时候要么同样按照上面的再反向处理一次或者使用数据库sql语法case when 来直接写入DTO CASE gender WHEN 1 THEN '男' WHEN 0 THEN '女' ELSE '未知' END 这种处理方式看起来不是很优雅。而且多了很多的判断和处理逻辑,和我们的业务并不是非常相关。所以我们可以选择更好的处理方式。 2.Mybatis中的TypeHandler 如果你ORM框架用的是Mybatis。那么将很容易通过TypeHandler<T>接口解决这个问题。 2.1 TypeHandler 分析 public interface TypeHandler<T> { void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; T getResult(ResultSet rs, String columnName) throws SQLException; T getResult(ResultSet rs, int columnIndex) throws SQLException; T getResult(CallableStatement cs, int columnIndex) throws SQLException; } 源码分析: setParameter 方法 通过 传入的T类型写你自己的逻辑,选择调用 PreparedStatement 对象的某个set方法将数据写入数据库。此方法用来写库。 getResult(ResultSet rs, String columnName) 通过字段名来读库并转换为T类型。 getResult(ResultSet rs, int columnIndex) 通过字段索引来读库并转换为T类型。 getResult(CallableStatement cs, int columnIndex) 调用存储过程来获取结果并转换为T类型。 2.2 EnumOrdinalTypeHandler 我们发现TypeHandler有一个实现类EnumOrdinalTypeHandler。字面意思是可以通过枚举的序号来处理类型。 @Override public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException { ps.setInt(i, parameter.ordinal()); } 我们先不考虑setNull的情况。通过此方法我们发现确实存入的是枚举的顺序值(顺序从0开始),拿上面的例子来说 如果是GenderType.FEMALE是0,如果是GenderType.MALE是1,但是当GenderType.UNKNOWN时存入的是2。取的时候也是自然反向处理为具体的GenderType枚举。 2.3 EnumTypeHandler 我们还发现有另外一个枚举类型处理器。它的set方法是这样的: @Override public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException { if (jdbcType == null) { ps.setString(i, parameter.name()); } else { ps.setObject(i, parameter.name(), jdbcType.TYPE_CODE); // see r3589 } } 我们不考虑jdbcType问题发现都是将Enum.name()的值写入数据库。拿上面的例子来说 如果是GenderType.FEMALE是FEMALE,如果是GenderType.MALE是MALE,但是当GenderType.UNKNOWN时存入的是UNKNOWN。读库是通过Enum.valueOf(Class<T> enumType,String name)来进行反转操作。 2.4 自定义TypeHandler 如果说我们的枚举类型或者说我们使用其他方式来处理类别转换怎么办?当然Mybatis不会帮你干这么具体的事情。需要你自己来实现了。我们还拿枚举作为例子,然后模仿上面的两种TypeHandler。 还是拿开始的例子来说通常我个人比较喜欢这么定义枚举: public enum GenderTypeEnum { /** * female. */ FEMALE(0, "女"), /** * male. */ MALE(1,"男"), /** * unknown. */ UNKNOWN(2, "未知"); private int value; private String description; GenderType(int value, String description) { this.value = value; this.description = description; } public int value() { return this.value; } public String description() { return this.description; } } 通过继承BaseTypeHandler实现该抽象类的3个钩子方法就行了: @MappedTypes({GenderTypeEnum.class}) @MappedJdbcTypes({JdbcType.INTEGER}) public class GenderTypeEnumTypeHandler extends BaseTypeHandler<GenderTypeEnum> { @Override public void setNonNullParameter(PreparedStatement ps, int i, GenderTypeEnum parameter, JdbcType jdbcType) throws SQLException { if (jdbcType == null) { ps.setInt(i, parameter.value()); } else { // see r3589 ps.setObject(i, parameter.value(), jdbcType.TYPE_CODE); } } @Override public GenderTypeEnum getNullableResult(ResultSet rs, String columnName) throws SQLException { return getGenderType(rs.getInt(columnName)); } @Override public GenderTypeEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return getGenderType(rs.getInt(columnIndex)); } @Override public GenderTypeEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return getGenderType(cs.getInt(columnIndex)); } private GenderTypeEnum getGenderType(int value) { Class<GenderTypeEnum> genderTypeClass = GenderTypeEnum.class; return Arrays.stream(genderTypeClass.getEnumConstants()) .filter(genderType -> genderType.value() == value) .findFirst().orElse(GenderTypeEnum.UNKNOWN); } } TypeHandler 实现写好了,那么如何让其发挥作用呢?我们接着往下走。 2.5 TypeHandler的核心要点 TypeHandler作用是javaType和jdbcType相互转换。所以在声明一个TypeHandler的时候一定要明确该TypeHandler处理的这两种类型。这是必须要明确的原则。MyBatis不会通过窥探数据库元信息来决定使用哪种JDBC类型,所以你必须在参数和结果映射中指明何种类型的字段,使其能够绑定到正确的类型处理器上。MyBatis直到语句被执行时才清楚数据类型。 通过上述例子中的@MappedJdbcTypes和@MappedTypes来进行绑定类型转换关系,也可以通过xml的typeHandler元素中的jdbcType或者javaType来指定。如果同时指定,xml的优先级要高。 注意有可能你会覆盖内置的TypeHandler。所以自定义时一定要去了解Mybatis提供的一些默认处理器。避免对其他业务的影响。所以使用自定义TypeHandler很重要的一个原则就是一定要声明JavaType和JdbcType.上面这些虽然比较生涩但是对于使用好TypeHandler非常重要。接下来我们来讲讲具体的配置。 2.6 免注册TypeHandler 我们这里只讲xml中的配置: 一种在rultMap元素中声明一般用来查询。一定要注意2.5中的一些原则。 <resultMap id="StudentMap" type="cn.felord.mybatis.entity.Student"> <id column="student_id" property="studentId"/> <result column="student_name" property="studentName"/> <result column="gender" property="genderType" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/> <result column="age" property="age"/> </resultMap> 然后是在插入、更新语句中使用。它们都是相同的,这里只举一个插入例子。 <insert id="saveStu"> insert into student (student_name, gender, age) values (#{studentName}, #{genderType,javaType=cn.felord.mybatis.enums.GenderTypeEnum,jdbcType=INTEGER,typeHandler=cn.felord.mybatis.type.GenderTypeEnumTypeHandler}, #{age}) </insert> 如果注册了别名都可以使用别名。上面的好处就是不用在TypeHandlerRegistry中进行注册。 2.7 注册TypeHandler 在配置中声明注册TypeHandler,然后Mybatis根据两种类型会自动匹配。所以这里还是要强调2.5中的核心要点。 如果你是xml配置需要在Configuration配置文件中的<typeHandlers>标签中进行声明式注册 <typeHandlers> <typeHandler jdbcType="JdbcType枚举存在的枚举" javaType="typeAliases的别名或者全限定类名" handler="类全限定名"/> <package name="指定所有typeHandler所在的包的包名"/> </typeHandlers> javaConfig 方式 ,第一你可以通过SqlSessionFactory对象取到Configuration对象将typeHandler注册进去。如果你使用mybatis-spring组件,可以在SqlSessionFactoryBean 的setTypeHandlersPackage方法中配置typeHandler的集中包路径,那么框架将会自动扫描并注册他们。springboot中对应的配置属性是mybatis.typeHandlersPackage。 如果你注册了TypeHandler。在Mapper.xml中只需要声明jdbcType和javaType,无需再声明具体的typeHandler。Mybatis会自动通过jdbcType、javaType来映射到具体注册的TypeHandler上去 。就像下面的例子 <insert id="saveAutomaticStu"> insert into student (student_name, gender, age) values (#{studentName}, #{genderType,javaType=cn.felord.mybatis.enums.GenderTypeEnum,jdbcType=INTEGER}, #{age}) </insert> 3.总结 今天我们学习了mybatis开发中如何通过使用类型处理器进行类型的转换处理,如何处理枚举,如何自定义处理器并使用它。相信对你在java开发过程中会有很大的帮助。相关的代码在我的码云仓库中:https://gitee.com/felord/mybatis-test.git 多多关注我的公众号:码农小胖哥,可以得到更加及时的资讯和反馈。

优秀的个人博客,低调大师

自己动手玩转深度学习项目

自从2012年AlexNet网络在ImageNet挑战赛上取得巨大成功之后,计算机视觉和深度学习等领域再一次迎来研究热潮。计算机视觉,从字面意义上理解就是让计算机等机器也具备人类视觉,研究让机器进行图像分类、目标检测等。在这近十年里,该领域取得的成就让人大吃一惊,有些研究已经超越了人类的表现水平。对于想入门或者从事深度学习领域的工作者而言,一般是从计算机视觉入手,网上有很多资料去介绍理论方面的知识,进行相应的实践也必不可少。本文总结一些在计算机视觉和深度学习领域的一些实践项目,供读者针对自己的感兴趣点挑选并实践。如果不熟悉上述术语,可以从下面的文章中了解更多的相关信息: 一份“新奇”的深度学习介绍 两个月探索深度学习和计算机视觉 从神经科学到计算机视觉 Andrew Ng在Coursera上的计算机视觉课程 下面进行正文介绍: 1.使用Ope

优秀的个人博客,低调大师

用Python玩转时序数据

时间序列是日常生活中最常见的数据类型之一。股票价格、销售信息、气候数据、能源使用,甚至个人身高体重都是可以用来定期收集的数据样本。几乎每个数据科学家在工作中都会遇到时间序列,能够有效地处理这些数据是数据科学领域之中的一项非常重要的技能。 本文简要介绍了如何从零开始使用Python中的时间序列。这包括对时间序列的简单定义,以及对利用pandas访问伦敦市居民智能电表所获取数据的处理。可以点击此处获取本文中所使用的数据。还提供了一些我认为有用的代码。 让我们从基础开始,时间序列的定义是这样的: 时间序列是按时间的顺序进行索引、排列或者绘制的数据点的集合。最常见的定义是,一个时间序列是在连续的相同间隔的时间点上取得的序列,因此它是一个离散时间数据的序列。 时间序列数据是围绕相对确定的时间戳而组织的。因此,与随机样本相比,可能包含我们将要尝试提取

优秀的个人博客,低调大师

玩转vue的slot内容分发

vue的内容分发非常适合“固定部分+动态部分”的组件的场景,固定部分可以是结构固定,也可以是逻辑固定,比如下拉loading,下拉loading只是中间内容是动态的,而拉到底部都会触发拉取更多内容的操作,因此我们可以把下拉loading做成一个有slot的插件。 单个Slot 在children这个标签里面放Dom,Vue不会理你,也就是不会显示,类似React:this.props.children。 //父 <children> <span>12345</span>//这边不会显示 </children> //子 components: { children: { template: "<button>为了明确作用范围,所以使用button标签</button>" } } 你需要写成这样 children: { template: "<button><slot></slot>为了明确作用范围,所以使用button标签</button>" } 注意这边 slot 相当于一个坑,等着父组件给填上,这边 slot 代表的就是上面的 span 多个Slot 这边需要加name属性,说白了,多个Slot就不像上面单个,需要有个对应关系。 父-> slot="name1" 子-> <slot name="name1" //父 <children> <span slot="name1">12345</span> </children> //子 components: { children: { template: "<button> <slot name="name1"></slot> button标签 </button>" } } 这边写了一个name1,如果有多个,就插多个,比较简单。 使用场景 “下拉加载更多”的场景在移动端相对来说出现得比较多。我们知道下拉触底都要监听触底事件,触底的操作也相同(去后台拉取数据),分页算法也相同,因此我们会想到把它做成一个组件,重用这些相同的地方,让其他地方可以共用这个组件,从而减少代码量。 然而,下拉loading并不是一个可以完全重用的组件,因为列表里面的内容不同,空白页(没有内容时)的内容也可能不同,如果要做成组件,那么就要考虑到这方面的“不同”,因此我们想到利用vue的内容分发slot来做。下面是本人在开发的时候做的一个下拉loading,大家可以参考下。 组件代码: <template> <div> <slot name="list" v-if="total > 0"></slot> <slot name="empty" v-else></slot> </div> </template> <script> import Toast from 'lib/xl-toast' import Tool from 'tool/tool' export default { data() { return { page: 1, isLoading: false, busy: false, isFirstLoad: false } }, props: { pageSize: { default: 10 // 每页展示多少条数据 }, total: { default: 0 // 总共多少条记录 } }, computed: { totalPage() { return Math.ceil(this.total / this.pageSize) } }, created() { this.getList() }, mounted() { this.addScrollListener() }, methods: { addScrollListener() { // 添加监听滚动操作,用到函数防抖 this.scrollFn = Tool.throttle(this.onScroll, 30, 30) document.addEventListener('scroll', this.scrollFn, false) }, getList() { // 正在拉取数据或者没有数据了,则取消滚动监听 if(this.isLoading || this.isFirstLoad && (this.page > this.totalPage)) { document.removeEventListener('scroll', this.scrollFn, false) return } this.busy = true this.isLoading = true // 通知父组件去拉取更多数据 this.$emit("getList", this.page, () => { this.isFirstLoad = true this.isLoading = false this.page++ }, () => { Toast.show('网络错误,请稍后重试') this.total = 0 this.isLoading = false }) }, reset() { // 重新拉取数据 this.page = 1 this.total = 0 this.isLoading = false this.isFirstLoad = false this.addScrollListener() this.getList() }, onScroll() { // 到底拉取更多数据 if(Tool.touchBottom()) { this.getList() } } } } </script> 前端全栈学习交流圈:866109386,面向1-3经验年前端开发人员,帮助突破技术瓶颈,提升思维能力,群内有大量PDF可供自取,更有干货实战项目视频进群免费领取。 总之,遇到一些有想对比较固定的部分,包括js操作或者结构固定,又有一些动态的部分,我们应该就应该考虑到使用:组件+slot。 意向不到的slot另类用法 我在做需求的时候,做了一个组件,该组件分为上下两个部分,这两个部分耦合度很高(不然我怎么把它当成一个组件呢哈哈哈),如下图所示: 本来C区域是一个组件,然后产品突然说,需要把这两个部分分开,把A移到C1的位置,C1移到A的位置(心里感觉到憋屈)。 这里我的第一个想法就是拆开来做成两个组件,但是问题来了,之前这两部分的耦合度很高,如果强制把它拆开成两个组件,那么这两个组件之间的交互必然会多很多。比如,C1改变了某个东西会影响到C2,那么C1需要触发事件通知父组件,父组件再调用C2的某个方法来更新状态。这种跨组件之间的通讯在组件之间频繁交互的情况下,将会是噩梦,而我这边却需要频繁的交互,所以如果把它拆分为两个组件,那么工作量和复杂度将会大大的增加。当然,你可以想到通过Event Hub的方式来实现两个组件之间的交互,但是根本问题还是没有实质性得得到解决。 那么,有什么方法可以做到不拆分成两个组件又能移动位置的方法呢,答案就是slot。以我的例子为例,把A和B作为C的内容分发,原来是这样的: <A></A> <B></B> <C></C> 改为slot以后是这样的 <C> <A slot="c1"></A> <B slot="c2"></B> </C> 这样就能做到不把C模块拆分,又能调整位置了,以最小的代价完成需求~~。 总结 vue的slot不仅可以用来内容分发,还可以用来做位置调整。如果在需要拆分组件来做位置调整,又不想因为拆分耦合度很高的组件,可以考虑使用slot来进行位置调整。一点愚见,希望对大家有所帮助。

优秀的个人博客,低调大师

iOS 开发之玩转专场动画

有空了再造个轮子,先放个链接 WWDC 2013 Session笔记 - iOS7中的ViewController切换 几句代码快速集成自定义转场效果+ 全手势驱动 TransitionAnimation 学习笔记 iOS --- 一张图看懂转场动画 iOS自定义转场动画实战讲解 iOS自定义转场动画实战讲解 iOS 视图控制器转场详解 iOS 视图控制器转场详解 iOS 转场动画探究(一) iOS 转场动画探究(二) iOS 转场动画详解 iOS自定义转场动画

优秀的个人博客,低调大师

玩转Kafka的生产者

上篇文章学习kafka的基本安装和基础概念,本文主要是学习kafka的常用API。其中包括生产者和消费者, 多线程生产者,多线程消费者,自定义分区等,当然还包括一些避坑指南。 准备工作 kafka版本:2.11-1.1.1 操作系统:centos7 java:jdk1.8 有了以上这些条件就OK了,具体怎么安装和启动Kafka这里就不强调了,可以看上一篇文章。 新建一个maven工程,需要的依赖如下: <dependency><groupId>org.apache.kafka</groupId><artifactId>kafka_2.11</artifactId><version>1.1.1</version></dependency><dependency><groupId>org.apache.kafka</groupId><artifactId>kafka-clients</artifactId><version>1.1.1</version></dependency> 主题管理 kafka的核心就是主题,学会使用kafka的脚本创建主题,也需要学习使用Java API来创建主题。 Kafka将zookeeper的操作封装成一个ZkUtils类,通过AdminUtils类来调用ZkUtils,来实现Kafka中元数据的操作。 下面一个例子是使用AdminUtils来创建主题,并同时创建指定大小的分区数。 // 连接配置 private static final String ZK_CONNECT = "10.0.90.53:2181";// session过期时间 private static final int SEESSION_TIMEOUT = 30 * 1000;// 连接超时时间 private static final int CONNECT_TIMEOUT = 30 * 1000;/** * 创建主题 * * @param topic 主题名称 * @param partition 分区数 * @param repilca 副本数 * @param properties 配置信息 */ public static void createTopic(String topic, int partition, int repilca, Properties properties) { ZkUtils zkUtils = null;try {// 创建zkutil zkUtils = ZkUtils.apply(ZK_CONNECT, SEESSION_TIMEOUT, CONNECT_TIMEOUT, JaasUtils.isZkSecurityEnabled());if (!AdminUtils.topicExists(zkUtils, topic)) {//主题不存在,则创建主题 AdminUtils.createTopic(zkUtils, topic, partition, repilca, properties, AdminUtils.createTopic$default$6()); } } catch (Exception e) { e.printStackTrace(); } finally { zkUtils.close(); } } 执行该方法,创建主题, 在centos7中查看之前创建的主题: bin/kafka-topics.sh --list --zookeeper localhost:2181 删除主题: /** * 删除主题 * * @param topic */ public static void deleteTopic(String topic){ ZkUtils zkUtils = null; try { zkUtils = ZkUtils.apply(ZK_CONNECT, SEESSION_TIMEOUT, CONNECT_TIMEOUT, JaasUtils.isZkSecurityEnabled()); AdminUtils.deleteTopic(zkUtils,topic); } catch (Exception e) { e.printStackTrace(); } finally { zkUtils.close(); } } 生产者API 在掌握了创建和删除主题之后,接下来,学习Kafka的生产者API。 Kafka中的生产者,通过KafkaProducer这个类来实现的,在介绍这个类的使用之前,首先介绍kafka的配置项,这也是实际生产中比较关心的。 消息发送流程 实例化生产者时,有三个配置是必须指定的: bootstrap.servers:配置连接代理列表,不必包含Kafka集群的所有代理地址,当连接上一个代理后,会从集群元数据信息中获取其他存活的代理信息。但为了保证能够成功连上Kafka集群,在多代理集群的情况下,建议至少配置两个代理。 (由于电脑配置有限,本文实验的是单机情况)key.serializer: 用于序列化消息Key的类 value.serializer:用于序列化消息值(Value)的类 向Kafka发送一个消息,基本上要经过以下的流程: 1.配置Properties对象,这个是必须的 2.实例化KafkaProducer对象 3.实例化ProducerRecord对象,每条消息对应一个ProducerRecord对象 4.调用KafkaProducer的send方法,发送消息。发送消息有两种,一种是带回调函数的(如果发送消息有异常,会在回调函数中返回),另一种是不带回调函数的。 KafkaProducer默认是异步发送消息,首先它会将消息缓存到消息缓冲区中,当缓存区累积到一定数量时,将消息封装成一个 RecordBatch,统一发送消息。也就是说,发送消息实质上分为两个阶段,第一将消息发送到消息缓冲区,第二执行网络I/O操作 5.关闭KafkaProducer,释放连接的资源。 了解以上的流程,那么接下来就实现Java版本的API。 代码实例 第一步: 新建一个消息实体类,模拟支付订单消息,包含消息的ID,商家名称,创建时间,备注。 public class OrderMessage {// 订单ID private String id;// 商家名称 private String sName;// 创建时间 private long createTime;// 备注 private String remake;public String getId() {return id; }public void setId(String id) {this.id = id; }public String getsName() {return sName; }public void setsName(String sName) {this.sName = sName; }public long getCreateTime() {return createTime; }public void setCreateTime(long createTime) {this.createTime = createTime; }public String getRemake() {return remake; }public void setRemake(String remake) {this.remake = remake; }@Override public String toString() {return "OrderMessage{" +"id='" + id + '\'' +", sName='" + sName + '\'' +", createTime=" + createTime +", remake='" + remake + '\'' +'}'; } } 第二步: 这里简单的发送一个消息demo,按照上面的流程,生产者例子如下: package kafka.producer;import kafka.OrderMessage;import org.apache.kafka.clients.producer.KafkaProducer;import org.apache.kafka.clients.producer.ProducerConfig;import org.apache.kafka.clients.producer.ProducerRecord;import java.util.Properties;import java.util.UUID;/** * kafka生产者 */public class ProducerSimpleDemo {static Properties properties = new Properties();//主题名称 static String topic = "myTopic";//生产者 static KafkaProducer<String, String> producer = null;//生产者配置 static {properties.put(org.apache.kafka.clients.producer.ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "10.0.90.53:9092");properties.put(org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");producer = new KafkaProducer<>(properties); }public static void main(String args[]) throws Exception {sendMsg(); }/** * 发送消息 * * @throws Exception */ public static void sendMsg() throws Exception { ProducerRecord<String, String> record = null;try {// 循环发送一百条消息 for (int i = 0; i < 10; i++) {// 构造待发送的消息 OrderMessage orderMessage = new OrderMessage(); orderMessage.setId(UUID.randomUUID().toString());long timestamp = System.nanoTime(); orderMessage.setCreateTime(timestamp); orderMessage.setRemake("remind"); orderMessage.setsName("test");// 实例化ProducerRecord record = new ProducerRecord<String, String>(topic, timestamp + "", orderMessage.toString());producer.send(record, (metadata, e) -> {// 使用回调函数 if (null != e) { e.printStackTrace(); }if (null != metadata) { System.out.println(String.format("offset: %s, partition:%s, topic:%s timestamp:%s", metadata.offset(), metadata.partition(), metadata.topic(), metadata.timestamp())); } }); } } catch (Exception e) { e.printStackTrace(); } finally {producer.close(); } } } 运行,结果就出现了,异常。 异常记录: 2018-07-30 18:05:10.755 DEBUG 10272 --- [ad | producer-1] o.apache.kafka.common.network.Selector : Connection with localhost/127.0.0.1 disconnected java.net.ConnectException: Connection refused: no further information at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) ~[na:1.8.0_111] at sun.nio.ch.SocketChannelImpl.finishConnect(Unknown Source) ~[na:1.8.0_111] at org.apache.kafka.common.network.PlaintextTransportLayer.finishConnect(PlaintextTransportLayer.java:51) ~[kafka-clients-0.10.1.1.jar:na] at org.apache.kafka.common.network.KafkaChannel.finishConnect(KafkaChannel.java:73) ~[kafka-clients-0.10.1.1.jar:na] at org.apache.kafka.common.network.Selector.pollSelectionKeys(Selector.java:323) [kafka-clients-0.10.1.1.jar:na] at org.apache.kafka.common.network.Selector.poll(Selector.java:291) [kafka-clients-0.10.1.1.jar:na] at org.apache.kafka.clients.NetworkClient.poll(NetworkClient.java:260) [kafka-clients-0.10.1.1.jar:na] at org.apache.kafka.clients.producer.internals.Sender.run(Sender.java:236) [kafka-clients-0.10.1.1.jar:na] at org.apache.kafka.clients.producer.internals.Sender.run(Sender.java:148) [kafka-clients-0.10.1.1.jar:na] at java.lang.Thread.run(Unknown Source) [na:1.8.0_111] 可以看到报错第一句显示:Connection with localhost/127.0.0.1 disconnected 但是可以看到自己的配置是正确的。 这里需要在kafka中修改部分配置: 在配置kafka中,首先需要修改kafka的配置server.properties中的 advertised.listeners=PLAINTEXT://:your.host.name:9092 翻译过来就是hostname和端口是用来建议给生产者和消费者使用的。 如果没有设置,将会使用 listeners的配置,如果listeners也没有配置,将使用java.net.InetAddress.getCanonicalHostName()来获取这个hostname和port,对于ipv4,基本就是localhost了。 "PLAINTEXT"表示协议,可选的值有PLAINTEXT和SSL,hostname可以指定IP地址,也可以用"0.0.0.0"表示对所有的网络接口有效,如果hostname为空表示只对默认的网络接口有效 也就是说如果你没有配置advertised.listeners,就使用listeners的配置通告给消息的生产者和消费者,这个过程是在生产者和消费者获取源数据(metadata)。 修改之后: advertised.listeners=PLAINTEXT://10.0.90.53:9092 需要注意的是,如果Kafka有多个节点,那么需要每个节点都按照这个节点的实际hostname和port情况进行设置。 修改完毕,重启Kafka服务,开启消费者,接受消息,在服务器中输入: bin/kafka-console-consumer.sh --zookeeper localhost:2181 --topic myTopic --from-beginning 可以看到服务器中的消费者: 成功接收到消息。之前提到过在生产者有回调函数,来看看回调函数的输出: offset: 0, partition:0, topic:myTopic timestamp:1533199115840 offset: 1, partition:0, topic:myTopic timestamp:1533199115850 offset: 2, partition:0, topic:myTopic timestamp:1533199115850 offset: 3, partition:0, topic:myTopic timestamp:1533199115850 offset: 4, partition:0, topic:myTopic timestamp:1533199115850 offset: 5, partition:0, topic:myTopic timestamp:1533199115850 offset: 6, partition:0, topic:myTopic timestamp:1533199115850 offset: 7, partition:0, topic:myTopic timestamp:1533199115852 offset: 8, partition:0, topic:myTopic timestamp:1533199115852 offset: 9, partition:0, topic:myTopic timestamp:1533199115852 打印出了偏移值,分区,主题,和时间戳。说明发送成功了。到此就完成第一个Helloworld操作了。 我们可以看到回调函数返回的消息,怎么都在一个分区中呢?下面来研究分区器。 自定义分区器 Kafka在底层摒弃了Java堆缓存机制,采用了操作系统级别的页缓存,同时将随机写操作改为顺序写,再结合Zero-Copy的特性极大地改善了IO性能。 这个在单机上的提高,对于集群,Kafka使用了分区,将topic的消息分散到多个分区上,并保存在不同的机器上。 但是是否分区越多,效率越高呢?也不尽然! 1.每个分区在底层文件系统都有属于自己的一个目录。该目录下通常会有两个文件: base_offset.log和base_offset.index。Kafak的controller和ReplicaManager会为每个broker都保存这两个文件句柄(file handler)。很明显,如果分区数越多,所需要保持打开状态的文件句柄数也就越多,最终可能会突破你的ulimit -n的限制。 2.消费者和生产者都会为分区缓存消息,分区越多,缓存的消息就越多,占用的内存就越大。 3.降低高可用,Kafka是通过高可用来实现高可用性的。我们知道在集群中往往会有一个leader,假设集群中有10个Kafka进程,1个leader,9个follwer,如果一个leader挂了,那么就会重新选出一个leader,如果集群中有10000个分区,那么将要花费很长的时间,这对于高可用是有损耗的。 本身kafka有自己的分区策略的,如果未指定,就会使用默认的分区策略: Kafka根据传递消息的key来进行分区的分配,即hash(key) % numPartitions。如果Key相同的话,那么就会分配到统一分区。 Kafka提供了自定义的分区器,只要实现Partitioner接口即可,下面是自定义分区的例子: package kafka.partition;import org.apache.kafka.clients.producer.Partitioner;import org.apache.kafka.common.Cluster;import java.util.Map;/** * 自定义分区器 */public class PartitionUtil implements Partitioner {// 分区数 private static final Integer PARTITION_NUM = 6;@Override public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {if (null == key){return 0; } String keyValue = String.valueOf(key);// key取模 int partitionId = (int) (Long.valueOf(key.toString())%PARTITION_NUM);return partitionId; }@Override public void close() { }@Override public void configure(Map<String, ?> configs) { } } 还是刚才分区的代码,只要在之前的配置中加上 properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, PartitionUtil.class.getName()); 运行生产者,回调函数打印如下: offset: 3, partition:5, topic:MyOrder timestamp:1533205894785 offset: 5, partition:3, topic:MyOrder timestamp:1533205893202 offset: 6, partition:3, topic:MyOrder timestamp:1533205894784 offset: 2, partition:2, topic:MyOrder timestamp:1533205894785 offset: 4, partition:1, topic:MyOrder timestamp:1533205894785 offset: 5, partition:1, topic:MyOrder timestamp:1533205894785 offset: 5, partition:0, topic:MyOrder timestamp:1533205894784 offset: 6, partition:0, topic:MyOrder timestamp:1533205894784 offset: 7, partition:0, topic:MyOrder timestamp:1533205894785 offset: 8, partition:0, topic:MyOrder timestamp:1533205894786 分区成功了,在实际生产过程中,可以根据项目的实际需要进行分区设计。 线程池生产者 在实际生产过程中,通常消息数量是比较多的,就可以考虑使用线程池。 使用线程池发送消息时,要考虑两点:1.需要结合实际情况,合理设计线程池的大小;2.使用线程池时,消息的发送是无序的,如果对消息的顺序有要求,不建议使用。 如果使用线程池,建议是只实例化一个KafkaProducer对象,这样性能最好。代码如下: 首先写一个线程类: package kafka.producer;import org.apache.kafka.clients.producer.KafkaProducer;import org.apache.kafka.clients.producer.ProducerRecord;/** * 生产者线程 */public class ProducerThread implements Runnable {private KafkaProducer<String, String> producer = null;private ProducerRecord<String, String> record = null;public ProducerThread(KafkaProducer<String, String> producer, ProducerRecord<String, String> record) {this.producer = producer;this.record = record; }@Override public void run() {producer.send(record, (metadata, e) -> {if (null != e) { e.printStackTrace(); }if (null != metadata) { System.out.println("消息发送成功 : "+String.format("offset: %s, partition:%s, topic:%s timestamp:%s", metadata.offset(), metadata.partition(), metadata.topic(), metadata.timestamp())); } }); } } 接着完成启动类,启动类中自定义了一个线程池,这里还是有一些遐思,就是没有自定义,线程创建工厂,没有指定创建的线程名称,在实际生产中,最好是自定义线程工厂。 代码如下: package kafka.producer;import kafka.OrderMessage;import kafka.partition.PartitionUtil;import org.apache.kafka.clients.producer.*;import org.apache.kafka.clients.producer.ProducerConfig;import java.util.Properties;import java.util.UUID;import java.util.concurrent.*;/** * 线程池生产者 * * @author tangj * @date 2018/7/29 20:15 */public class ProducerDemo {static Properties properties = new Properties();static String topic = "MyOrder";static KafkaProducer<String, String> producer = null;// 核心池大小 static int corePoolSize = 5;// 最大值 static int maximumPoolSize = 20;// 无任务时存活时间 static long keepAliveTime = 60;// 时间单位 static TimeUnit timeUnit = TimeUnit.SECONDS;// 阻塞队列 static BlockingQueue blockingQueue = new LinkedBlockingQueue();// 线程池 static ExecutorService service = null;static {// 配置项 properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "10.0.90.53:9092");properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, PartitionUtil.class.getName());properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");producer = new KafkaProducer<>(properties);// 初始化线程池 service = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, blockingQueue); }public static void main(String args[]) throws Exception {for (int i = 0; i < 6; i++) {service.submit(createMsgTask()); } }/** * 生产消息 * * @return */ public static ProducerThread createMsgTask() { OrderMessage orderMessage = new OrderMessage(); orderMessage.setId(UUID.randomUUID().toString());long timestamp = System.nanoTime(); orderMessage.setCreateTime(timestamp); orderMessage.setRemake("rem"); orderMessage.setsName("test"); ProducerRecord<String, String> record = new ProducerRecord<String, String>(topic, timestamp + "", orderMessage.toString()); ProducerThread task = new ProducerThread(producer, record);return task; } }

优秀的个人博客,低调大师

面试 9:Java 玩转冒泡排序

面试 9:用 Java 实现冒泡排序 南尘的朋友们,新的一周好,原本打算继续讲链表考点算法的,这里姑且是卡一段。虽然在我们 Android 开发中,很少涉及到排序算法,因为基本官方都帮我们封装好了,但排序算法也是非常重要的,在面试中 归并排序 和 快速排序 一直为高频考点,但在学习它们之前,我们必须得先把三大基础算法学会,毕竟层层递进,方得始终嘛。 冒泡排序 冒泡排序恐怕是我们计算机专业课程上以第一个接触到的排序算法,也算是一种入门级的排序算法。它的基本思想是:两两比较相邻记录的关键字,如何反序则交换,直到没有反序的记录为止。 冒泡排序算法原理: 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。 针对所有的元素重复以上的步骤,除了最后一个。 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。 一次比较过程如图所示: 图片来源于网络 我们通常容易想到最简单的实现代码: public class Test09 { private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } private static void printArr(int[] arr) { for (int anArr : arr) { System.out.print(anArr + " "); } } private static void bubbleSort(int[] arr) { if (arr == null) return; for (int i = 0; i < arr.length - 1; i++) { for (int j = i + 1; j < arr.length; j++) { if (arr[i] > arr[j]) swap(arr, i, j); } } } public static void main(String[] args) { int[] arr = {6, 4, 2, 1, 8, 3, 7, 9, 5}; bubbleSort(arr); printArr(arr); } } 严格地讲,上面的算法并不是冒泡排序,因为 它完全不符合两两相邻比较。它更应该是最最简单的就交换排序而已。它的思路是让每一个关键字,都和它后面的每一个关键字比较,如果大则交换,这样第一位置的关键字在一次循环后一定变成最小值。 我们不妨来看看正宗的冒泡排序算法。 public class Test09 { private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } private static void printArr(int[] arr) { for (int anArr : arr) { System.out.print(anArr + " "); } } private static void bubbleSort(int[] arr) { if (arr == null) return; for (int i = 0; i < arr.length - 1; i++) { for (int j = 1; j < arr.length - i; j++) { if (arr[j - 1] > arr[j]) { swap(arr, j - 1, j); } } } } public static void main(String[] args) { int[] arr = {6, 4, 2, 1, 8, 3, 7, 9, 5}; bubbleSort(arr); printArr(arr); } } 上述代码是否完美了呢?答案是否定的,我们假设待排序的序列是 {2,1,3,4,5,6,7,8,9},也就是说,除了第一和第二个关键字需要交换外,别的都应该是正常的顺序,当 i = 1 时,交换了 2 和 1 的位置,此时已经有序,但是算法依然不依不挠地将 i = 2 到 9 以及每一个内循环都执行了一遍,尽管没有交换数据,但之后的大量比较还是大大的多余了。所以我们完全可以设置一个标记位 isSort,当我们比较一次后都没有交换,则代表数组已经有序了,此时直接退出循环即可。 既然思路已经确定,那代码自然是很信手拈来了。 public class Test09 { private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } private static void printArr(int[] arr) { for (int anArr : arr) { System.out.print(anArr + " "); } } private static void bubbleSort(int[] arr) { if (arr == null) return; // 定义一个标记 isSort,当其值为 true 的时候代表已经有序。 boolean isSort; for (int i = 0; i < arr.length - 1; i++) { isSort = true; for (int j = 1; j < arr.length - i; j++) { if (arr[j - 1] > arr[j]) { swap(arr, j - 1, j); isSort = false; } } if (isSort) break; } } public static void main(String[] args) { int[] arr = {6, 4, 2, 1, 8, 3, 7, 9, 5}; bubbleSort(arr); printArr(arr); } } Perfect 的代码,但冒泡排序在数组长度较大的时候,效率真的很低下,所以在实际生产中,我们也很少使用这种算法。 冒泡排序时间空间复杂度及算法稳定性分析 对于长度为 n 的数组,冒泡排序需要经过 n(n-1)/2 次比较,最坏的情况下,即数组本身是倒序的情况下,需要经过 n(n-1)/2 次交换,所以其 冒泡排序的算法时间平均复杂度为 O(n²)。空间复杂度为 O(1)。 可以想象一下:如果两个相邻的元素相等是不会进行交换操作的,也就是两个相等元素的先后顺序是不会改变的。如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个元素相邻起来,最终也不会交换它俩的位置,所以相同元素经过排序后顺序并没有改变。 所以冒泡排序是一种稳定排序算法。所以冒泡排序是稳定排序。这也正是算法稳定性的定义: 排序算法的稳定性:通俗地讲就是能保证排序前两个相等的数据其在序列中的先后位置顺序与排序后它们两个先后位置顺序相同。 冒泡排序总结: 冒泡排序的算法时间平均复杂度为 O(n²)。 空间复杂度为 O(1)。 冒泡排序为稳定排序。 考虑到不少读者说每天代码量太多的问题,我们今天就只讲冒泡排序的 Java 实现,我们明天将带来三大简单排序的另外两种,选择排序 和 插入排序。 文章参考来源:https://juejin.im/post/5a96d6b15188255efc5f8bbd

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册