开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Java面向对象关键字extends继承详解 一、 问题引出

发表于 2021-11-19

这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战」 @TOC

@TOC

一、 问题引出

面向对象的编程思想使得代码中创建的类更加具体,他们都有各自的属性,方法。有的时候一些客观事物之前存在一些联系,那么他们在代码中的具体类也存在一些联系。
例如:设计一个动物类

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class Animal {
public String name;
public int age;
public Animal(String name) {
this.name = name;
}
public Animal(int age) {
this.age = age;
}
public void eat(){
System.out.println(this.name+"吃东西");
}
}

这个动物类有自己的name,age属性和eat方法
我们又想创建一个猫类和狗类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码class cat {
public String name;
public int age;
public cat(String name) {
this.name = name;
}
public cat(int age) {
this.age = age;
}
public void eat(){
System.out.println(this.name+"吃饭");
}
}
class dog{
public String name;
public int age;

public dog(int age) {
this.age = age;
}

public dog(String name) {
this.name = name;
}
public void eat(){
System.out.println(this.name+"干饭");
}
}

由于大家都是碳基生物,你要吃饭我也要吃饭,你有名字我也有名字,也都有自己的年龄,我们在创建其他类的时候还需要在写一遍他们的属性name,age,和方法eat。并且,从逻辑上来说猫和狗都属于动物。这就造成了大量重复的代码,那有没有什么办法能让我不用把这些属性。方法在写一遍呢?
在这里插入图片描述

继承:我来啦!!!

继承作为面向对象编程的一个非常重要的关键字,在C++,和Java当中都可以使用它来减少代码冗余。顾名思义,它可以让一个子类继承另一个父类,就像儿子继承爸爸的财产一样,继承后的子类可以拥有父类的方法,属性,这样每次在定义动物类完之后,在想要定义狗或者猫类的时候就不用再把多余的代码写一遍了。我们来看看效果。


二、继承extends

2.1 继承的用法

子类extends父类即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码class cat extends animal{
public cat(String name) {
super(name);
}
public cat(int age){
super(age);
}
public void catchMouce(){
System.out.println(this.name+"抓老鼠");
}
}
class dog extends animal{
public dog(String name) {
super(name);
}

public dog(int age) {
super(age);
}
public void defence(){
System.out.println(this.name+"看家");
}
}

在上面的代码中,cat,dog被称为子类、派生类,而animal被称为父类或者超类,extends英文意思为扩展,这里可以理解成继承,例如我们写的cat类有了抓老鼠的方法,dog类有了看家的方法。继承可以让子类拥有父类public修饰的属性和方法,cat、dog类就继承了animal类的name、age属性以及eat方法。

2.2 基本语法

使用 extends 指定父类.
子类会继承父类的所有 public 的字段和方法.
对于父类的 private 的字段和方法, 子类中是无法访问的.
子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用
子类继承父类之后,还可以声明自己特有的属性和方法,实现功能的拓展。

2.3继承的好处

1:减少代码的冗余,提高复用性。
2:便于功能的拓展
3:为多态提供了前提。

2.4继承性

Java各种类互相继承的属性称作继承性,继承性有一些规定。
1:一个类可以被多个子类继承。
2:Java的单继承性:一个子类只能继承一个父类。比如一个儿子只能有一个亲爸爸,一个爸爸可以有好几个儿子。在C++,和python中支持多继承
3:如果一个类没有说明一个类的父类的话,那么此类继承于java.lang.Object类
4:所以的类(除了java.lang.Object)都间接或直接的继承java.lang.Object类。那么就意味这所有的类都具有java.lang.Object类声明的功能。


在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

istio 原理简介

发表于 2021-11-19

前导

由于 istio 自 1.5 版本以后架构上有了较大变化,控制面从多组件变成了单体的 istiod 组件,所以下文会先介绍 1.5 之前的架构,再介绍 1.5 之后的,是一个由繁到简的过程。

istio 1.5 之前架构

图片

Istio 的架构分为控制平面和数据平面

  • 数据平面:由一组智能代理(Envoy)以 sidecar 模式部署,协调和控制所有服务之间的网络通信。
  • 控制平面:负责管理和配置代理路由流量,以及在运行时执行的政策。

图片

可以看到控制面(control plane )组件众多,下图是 1.1 版本所包含的组件:

图片

istio 工作原理

我们先按照 1.5 版本之前的架构描述

Sidecar 注入 (envoy)

图片

详细的注入过程可以参考:blog.yingchi.io/posts/2020/…

图片

连接 (pilot)

图片

控制 && 观测 (mixer telemetry、mixer policy)

图片

保护(citadel)

图片

配置

Galley 原来仅负责进行配置验证,1.1 后升级为整个控制面的配置管理中心,除了继续提供配置验证功能外,Galley 还负责配置的管理和分发,Galley 使用 网格配置协议 (Mesh Configuration Protocol) 和其他组件进行配置的交互。

提供 istio 中的配置管理服务,验证 Istio 的 CRD 资源的合法性

istio 各组件功能及作用

  • istio-polit: 服务发现,向数据平面下发规则,包括 VirtualService、DestinationRule、Gateway、ServicEntry 等流量治理规则,也包括认证授权等安全规则。
  • istio-telemetry: 专门收集遥测数据的 mixer 服务组件。
  • Istio-policy: 另外一个 mixer 服务,可以对接如配额、授权、黑白名单等不同的控制后端,对服务间的访问进行控制。
  • Istio-citadel: 核心安全组件,提供了自动生成、分发、轮换与撤销秘钥和证书的功能。
  • Istio-galley: 配置管理的组件,验证配置信息的格式和内容的正确性,并将这些配置信息提供给管理面的 Pilot 和 Mixer 使用。
  • Istio-sidecar-injector: 负责自动注入的组件。
  • Istio-proxy: 数据面的轻量代理。
  • Istio-ingressgateway: 入口处的 gateway。

istio 1.5 之后架构

图片

之前版本的 istio 对组件进行了很好的解耦,组件们各司其职,当然也带来了组件比较多的问题。可以看到新版本将众多组件包装在了一起叫 istiod

所以新版本 istio 核心组件就只剩下一个:istiod

参考

  • www.infoq.cn/article/dtf…
  • blog.yingchi.io/posts/2020/…
  • istio.io/

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

NIO网络编程(十)—— 零拷贝技术

发表于 2021-11-19

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

传统的IO分析

如果要将一个文件从本地磁盘通过网络传输到另一台主机上,传统的IO会通过如下的代码,这段代码的步骤就是读取文件、将文件内容存到字节数组中、通过socket发送字节数组。

1
2
3
4
5
6
7
8
java复制代码File f = new File("helloword/data.txt");
RandomAccessFile file = new RandomAccessFile(file, "r");

byte[] buf = new byte[(int)f.length()];
file.read(buf);

Socket socket = ...;
socket.getOutputStream().write(buf);

这段代码的实际工作流程可以通过下面这张图看到:

在这里插入图片描述

  • 第一步:Java 本身并不具备 IO 读写能力,因此 read 方法调用后,要从 Java 程序的用户态切换至内核态,去调用操作系统(Kernel)的方法的读能力,将数据先读入内核缓冲区(磁盘数据不能直接就读入用户缓冲区)。
  • 第二步:从内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即 byte[] buf)。
  • 第三步:调用 write 方法,这时将数据从用户缓冲区(byte[] buf)写入 socket 缓冲区。
  • 第四步:接下来要向网卡写数据,这项能力 Java 也不具备,因此又需要从用户态切换至内核态,调用操作系统的写能力,将 socket 缓冲区的数据写入网卡。

可以看到虽然代码不长,但是中间环节较多,同时可以看到 JAVA 的 IO 实际不是物理设备级别的读写,而是缓存的复制,底层的真正读写是操作系统来完成的,分析一下这一系列步骤:

  • 用户态与内核态的切换发生了 3 次
  • 数据拷贝了共 4 次

NIO优化

可以使用nio的buffer,需要注意的是必须使用DirectByteBuf去分配buffer,因为使用ByteBuffer.allocate()分配buffer,底层对应 HeapByteBuffer,使用的还是 Java 内存,而使用ByteBuffer.allocateDirect() 底层对应DirectByteBuffer,直接使用的就是操作系统内存,这个内存有一个特点:操作系统可以访问,Java也可以访问

通过这个改进后,工作流程变成了下图:

在这里插入图片描述

大部分步骤与上一版相同,唯有一点不同的是:刚刚说到使用 DirectByteBuffer可以将堆外内存映射到 JVM 内存中来直接访问使用,因此可以将内核缓冲区和用户缓冲区当作同一块内存,变相地减少了一次数据的拷贝

  • 用户态与内核态的切换次数没有变化,还是发生了 3 次
  • 数据拷贝减少了一次,共 3 次

零拷贝技术

可以使用零拷贝技术对这一过程进一步优化,此外需要注意的是:零拷贝指的是数据无需拷贝到 JVM 内存中,而不是不进行拷贝。

零拷贝技术1

第一种零拷贝技术底层采用了 linux 2.1后提供的sendFile方法,在Java中对应的是两个 channel 调用 transferTo/transferFrom方法拷贝数据,需要注意的是:这两个方法这fileChannel里有,在SocketChannel没有

img

  • 这个方法改进的地方是,它不需要向directBuffer中传输数据了,可以直接从内核缓冲区发送到socket缓冲区,中间不经过Java了,减少了两次用户态与内核态的切换
  • Java首先调用transferTo方法,从 Java 程序的用户态切换至内核态,将数据读入内核缓冲区
  • 之后数据从内核缓冲区传输到 socket 缓冲区
  • 最后将 socket 缓冲区的数据写入网卡

分析一下这种方法:

  • 只发生了1次用户态与内核态的切换
  • 数据拷贝了 3 次

零拷贝技术2

linux 2.4 对上述方法再次进行了优化

img

  • 可以直接将数据内容从内核缓冲区发送到网卡,只会将一些 offset 和 length 信息拷入 socket 缓冲区,几乎无消耗
  • Java 调用transferTo方法后,要从 Java 程序的用户态切换至内核态,将数据读入内核缓冲区
  • 可以将 内核缓冲区的数据直接写入网卡

零拷贝技术的特点

  • 更少的用户态与内核态的切换
  • 不利用 cpu 计算,减少 cpu 缓存伪共享
  • 需要注意的是零拷贝适合小文件传输

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

关于nodejs服务端开发hmr的方案

发表于 2021-11-19

背景

我们常用nodemon做node.js进程管理工具,当内容发生变化时候即reload,严格来说不算hmr。

本文主要是总结下当前node.js开发方式。和聊下hmr的几种做法

注:本文以koa来举例,尽量不涉及到框架,只是为了简化。

主流做法

主要是区分是否使用typescrpt, 目前使用typescript开发nodejs应用已经非常普遍,但typescript无法直接在node中运行,需要先进行编译,既要编译,又要hmr,怎么实现呢?

1
2
3
4
5
6
7
go复制代码// 非typescript版本
1 全局安装nodemon; `npm install nodemon -g`
2 修改启动脚本;`"start": "nodemon main.js"`
// typescript版本
1 全局安装ts-node、nodemon; `npm install ts-node nodemon -g`
2 安装typescript和koa的依赖; `npm install --save-dev koa @types/koa typescript`
3 配置nodemon监听目录管理进程,ts-node执行typescript文件; `nodemon --watch src/**/* -e ts,tsx --watch main.ts --exec ts-node main.ts`

node-hmr/hot-module-require

在进一步了解服务端hmr之前,需要先了解下commonjs/esm的执行逻辑(更加详细的细节请查看我关于模块化的介绍)。不管是commonjs还是esm,在执行代码之前,会先处理模块之间的引用关系,再缓存到内存中,我们实际执行的是已缓存的代码(代码已执行后,修改文件内容不会影响已缓存内容)。

所以需要实现服务端hmr的关键在于:如何更新缓存内容。

这两个插件实现的基本流程:

1
2
3
javascript复制代码1 通过chokidar监听文件变化;  
2 通过node.js通过require暴露的缓存对象require.cache, 删除具体的缓存内容 `delete require.cache[moduleId]`;
3 替换新的内容;

但这里有一些需要注意的地方,例如:

1 修改require.cache[moduleId]并不只是删除后添加新的引用而已,还涉及到被多处引用等问题,所有关联的parent节点都需要清理。

2 不同框架的处理方式有些不一样,例如koa框架,在初始化时候会将所有中间件通过compose函数绑定在一起(类似链表的数据结构),如果修改了其中的某个中间件,很难去找到对应的节点修改。目前像node-hmr这个插件只是将中间件重新reload,并且分离http服务。避免重启http服务:

1
2
3
4
5
6
7
8
9
10
ini复制代码const hmr = require('node-hmr');

let callback;
hmr(() => {
const app = require('./app');
callback = app.callback();
});

const server = http.createServer((req, res) => callback(req, res));
server.listen(3000);

相关工具

nodemon

nodemon主要实现两个功能,监听文件变化(通过chokidar插件实现,后面有介绍),重启应用;

其实我一开始以为重启node.js应用是很简单的事情,因为平时就是ctrl+c,或者用nodemon或者是pm2这样的进程管理工具。其实不是这么简单的事情,特别是还需要cross platform.

nodemon重启流程如下(因为我是mac环境,所以只聊下mac下重启流程):

1
2
3
ini复制代码1 通过pstree获取所有子进程,并关闭所有子进程;  
2 关闭主进程;
3 再次启动服务;

关于重启进程涉及过多,我后面会有一篇更详细的文章说明。

ts-node

ts-node是typescript的执行引擎,可以通过ts-node命令直接执行typescript命令而不需要编译为js再执行;

1
2
3
4
arduino复制代码// 安装
npm install -g ts-node
// 执行
ts-node *.ts

思考

1 客户端(浏览器)hmr和服务端hmr有什么区别?

先看下两者的执行流程:

1
2
rust复制代码客户端hmr流程:监听文件变化->将更新内容通知到框架->框架内部实现的render方法执行修改并渲染出来; 
服务端hmr流程:监听文件变化->修改被缓存的文件内容;

其实两者的区别在于客户端需要框架配合将内容实时渲染出来,而服务端只需要修改内容,等请求进来后按照新的内容执行即可;

参考文档

1 Node.js 也能 HMR 热更新: zhuanlan.zhihu.com/p/260441242

2 ts-node: typestrong.org/ts-node/

3 node-hmr: github.com/serhiinkh/n…

4 hot-module-require: github.com/imcuttle/ho…

5 nodejs.cn/api/modules…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

C# 使用Timer和ProgressBar控件制作一个倒计

发表于 2021-11-19

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

前言

博主前几天发现一个好玩的控件,Timer时间控件和ProgressBar进度条控件,于是就打算做一个倒计时的计时器,C#窗体应用是一个比较好玩的语言,你要在里面找到乐趣,因为这门语言你做的就能马上感觉到,有一种所见即所得,博主这个篇文章只是简单讲解一下,启发作用,博主只是做了一个简单的倒计时器,你可以根据自己的想法做一个计时器或者类似电子手表一样的时钟,初学者做完你还是会有成就感的觉得这么语言还阔以。

每日一遍,快乐一天!!

6a04b428gy1fy7ab1pyrqg20c80bw417

开头展示效果:吸引你往后看哈哈哈

我的作品2 00_00_00-00_00_30

1.创建窗体应用文件并设计界面

创建一个窗体类,不会的童鞋看之前的文章啦🙌🙌🙌

image-20211119094237180

1.1 创建并拖ComboBOX控件设置属性

在这里我们设置静态的,不输入的ComboBOX,对于comboBOX显示样式有三个属性

image-20211119094814220

1
2
3
ini复制代码Simple是文本可编辑,下拉列表总可见;
DropDown是默认样式,文本可编辑,下拉需用户点击箭头;
DropDownList是文本不可编辑,下拉需用户点击箭头;需要注意的是DropDownList形式直接使用comboBox1.Text = "值";形式来赋值结果会显示为空,

1.2 设置进度条ProgressBar控件

我们在使用进度条控件是需要设置进度条最大值,步进量,Value属性博主在调用里面置零了为了方便重复调用。

image-20211119095259657

1
2
3
4
5
6
vbnet复制代码需要注意ProgressBar控件的几个属性
Maximum属性:用来设置或返回进度条能够显示的最大值,默认值为100。
Minimum属性:用来设置或返回进度条能够显示的最小值,默认值为0。
MarqueeAnimationSpeed属性:这个属性经常以毫秒为单位,显示加载的速度
Step属性:用来设置或返回一个值,该值用来决定每次调用PerformStep 方法时,  Value属性增加的幅度。例如,如果要复制一组文件,则可将 Step 属性的值设置为 1,并将 Maximum 属性的值设置为要复制的文件总数。在复制每个文件时,可以调用PerformStep方法按Step属性的值增加进度栏
Value属性:这个属性是用来显示控件的进度的,如果是0则进度为0,如果是100,则进度为100%;

1.3 设置时间Timer控件

Timer控件需要设置中断值,博主设置1秒,就是1秒停一下实现倒计时效果,先要打开Timer控件才能用哦,使用start打开哦

image-20211119095543651

1
2
3
4
scss复制代码reset () :停止正在运行的计时器,重置currentCount=0, 再次调用 start() 后,将运行计时器实例,运行次数为指定的重复次数 
start () :如果计时器尚未运行,则启动计时器
stop () : 停止计时器。 如果在调用 stop() 后调用 start(),则将继续运行计时器实例,运行次数为剩余的 重复次数(由 repeatCount 属性设置)
Interval :属性指定窗体上 Timer 事件之间的间隔(以毫秒为单位)//中止作用

image-20211119095848134

1.4 对剩余时间代码分析

博主改变了剩余时间的变化效果,每次选择都会改变剩余时间,默认为0秒。

image-20211119100248118

1.5 对Timer控件代码分析

image-20211119143208050

1.6 对倒计时按钮代码触发做代码处理

image-20211119143509269

1.6 整体代码及运行效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
csharp复制代码using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace 倒计时
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
int count = 0;//记录次数,每次相当一秒,把time减去count实现倒计时
int time = 0;//全局变量用来得到我们comboBox的值
private void Form1_Load(object sender, EventArgs e)//赋值
{
int i;
for(i=1;i<100;i++)//给comboBox赋值1-99秒
{
comboBox1.Items.Add(i.ToString() + "秒");//追加到Item里面
}
}

private void timer1_Tick(object sender, EventArgs e)//时间控件
{

count++;//每次加一
label3.Text = (time - count).ToString() + "秒";//获取倒计时总秒数减去已经过去的秒数,实现倒计时
progressBar1.Value = count;
if(count==time)//当我们的count等于time说明剩余0秒
{
timer1.Stop();//结束停止时间控件
System.Media.SystemSounds.Asterisk.Play();//播放系统提示音,不需要可以不写
MessageBox.Show("时间到了!");//时间到了,出现弹窗
}
}

private void button1_Click(object sender, EventArgs e)
{
count = 0;
progressBar1.Value = 0;//设置进度条为0,因为如果我们执行下一次需要把之前的已经到100%的进度条回到0
string str = comboBox1.Text;
time = Convert.ToInt32(str.Substring(0,(str.Length-1)));//获取选择的秒数
// MessageBox.Show(time.ToString());
progressBar1.Maximum = time;//设置进度条最大值也就是100%为我们的timer值
timer1.Start();//开始了就会循环调用timer函数,直到停止
}

private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
label3.Text = comboBox1.Text;//修改剩余时间的默认值
}
}
}

梅开二度效果展示:

我的作品2 00_00_00-00_00_30

总结

博主这篇文章主要用了两个控件,一个是进度条progressBar控件另一个是时间Timer控件,实现的效果还蛮好玩,对于初学者学习窗体应用是一个很好的入门小程序,最主要感觉没什么难度,如果使用其他语言可能比较难,哈哈哈,对了提一下,我们Timer控件使用比较多哦,在窗体应用里面,类似循环吧,好处是可以停止效果,比如你需要做什么接收处理,就可以利用这个控件每多少秒执行一次,因为它可以中断。好了,创作不易点赞关注评论收藏哦。

p56

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【力扣-二叉树】14合并二叉树(617) 617 合并二

发表于 2021-11-19

「这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战」

617. 合并二叉树

题目描述

给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。

你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
makefile复制代码输入: 
Tree 1 Tree 2
1 2
/ \ / \
3 2 1 3
/ \ \
5 4 7
输出:
合并后的树:
3
/ \
4 5
/ \ \
5 4 7

注意: 合并必须从两个树的根节点开始。

递归法

前序遍历 – 递归法

  • 1、递归函数的参数与返回值
    • 参数:两个树的节点
    • 返回值:新树的节点
  • 2、递归终止条件
    • 两个树的节点一个为空一个不为空,返回不为空的节点
    • 两个树的节点都为空,返回空
    • 两个树的节点都不为空,返回新节点
  • 3、单层遍历的逻辑
    • 根节点
    • 左节点
    • 右节点

代码

合并后的树使用新的空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
c++复制代码// 递归法
class Solution
{
public:
TreeNode *mergeTrees(TreeNode *root1, TreeNode *root2)
{
return traversal(root1, root2);
}

private:
TreeNode *traversal(TreeNode *root1, TreeNode *root2)
{

// 判断是否有节点为空
// 返回不为空的节点
// 全为空的时候返回NULL
if (root1 == NULL && root2 != NULL)
{
return root2;
}
else if (root1 != NULL && root2 == NULL)
{
return root1;
}
else if (root1 == NULL && root2 == NULL)
{
return NULL;
}
// 全都非空
// 定义新的节点,节点值为两个节点值的和
TreeNode *node = new TreeNode(root1->val + root2->val);

// 左
node->left = traversal(root1->left, root2->left);
// 右
node->right = traversal(root1->right, root2->right);

return node;
}
};

原地合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c++复制代码class Solution{
public:
TreeNode *mergeTrees(TreeNode *root1,TreeNode *root2){
// 如果第一棵树节点为空,则返回第二棵树的节点
if(root1 ==NULL){
return root2;
}
if(root2 == NULL){
return root1;
}
// 在第一棵树上进行原地修改
// 中
root1->val += root2->val;
// 左
root1->left = mergeTrees(root1->left,root2->left);
// 右
root1->right = mergeTrees(root1->right,root2->right);


return root1;
}
};

迭代法

思路同递归

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
c++复制代码class Solution {
public:
TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
if (t1 == NULL) return t2;
if (t2 == NULL) return t1;
queue<TreeNode*> que;
que.push(t1);
que.push(t2);
while(!que.empty()) {
TreeNode* node1 = que.front();
que.pop();
TreeNode* node2 = que.front();
que.pop();


// 此时两个节点一定不为空,val相加
node1->val += node2->val;

// 如果两棵树左节点都不为空,加入队列
if (node1->left != NULL && node2->left != NULL) {
que.push(node1->left);
que.push(node2->left);
}
// 如果两棵树右节点都不为空,加入队列
if (node1->right != NULL && node2->right != NULL) {
que.push(node1->right);
que.push(node2->right);
}

// 当t1的左节点 为空 t2左节点不为空,就赋值过去
if (node1->left == NULL && node2->left != NULL) {
node1->left = node2->left;
}
// 当t1的右节点 为空 t2右节点不为空,就赋值过去
if (node1->right == NULL && node2->right != NULL) {
node1->right = node2->right;
}
}
return t1;
}
};

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【力扣-二叉树】13、最大二叉树 654 最大二叉树

发表于 2021-11-19

「这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战」

654. 最大二叉树

题目描述

给定一个不含重复元素的整数数组 nums 。一个以此数组直接递归构建的 最大二叉树 定义如下:

  1. 二叉树的根是数组 nums 中的最大元素。
  2. 左子树是通过数组中 最大值左边部分 递归构造出的最大二叉树。
  3. 右子树是通过数组中 最大值右边部分 递归构造出的最大二叉树。

返回有给定数组 nums 构建的 最大二叉树 。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码输入: nums = [3,2,1,6,0,5]
输出: [6,3,5,null,2,0,null,null,1]
解释: 递归调用如下所示:
- [3,2,1,6,0,5] 中的最大值是 6 ,左边部分是 [3,2,1] ,右边部分是 [0,5] 。
- [3,2,1] 中的最大值是 3 ,左边部分是 [] ,右边部分是 [2,1] 。
- 空数组,无子节点。
- [2,1] 中的最大值是 2 ,左边部分是 [] ,右边部分是 [1] 。
- 空数组,无子节点。
- 只有一个元素,所以子节点是一个值为 1 的节点。
- [0,5] 中的最大值是 5 ,左边部分是 [0] ,右边部分是 [] 。
- 只有一个元素,所以子节点是一个值为 0 的节点。
- 空数组,无子节点。

示例 2:

1
2
ini复制代码输入: nums = [3,2,1]
输出: [3,null,2,null,1]

解析

类似于上一题(13、从中序与后序遍历序列构造二叉树(106))

  • 1、如果数组为空,则返回空
  • 2、找到数组中的最大数,作为根节点
  • 3、将剩余的数组分为左数组和右数组
  • 4、递归构造二叉树(处理左数组和右数组)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
c++复制代码class Solution
{
public:
TreeNode *constructMaximumBinaryTree(vector<int> &nums)
{
return traversal(nums);
}

TreeNode *traversal(vector<int> &nums)
{
// 判断如果数组为空,则返回NULL
if (nums.size() == 0)
{
return NULL;
}
// 2、查找最大值所在的位置,构造根节点
int maxIndex = 0, maxVal = MIN_INT;
for (int i = 0; i < nums.size(); i++)
{
if (nums[i] > maxVal)
{
maxVal = nums[i];
maxIndex = i;
}
}
TreeNode *root = new TreeNode(maxVal);
// 3、根据根节点的所在位置,将数组分为左右数组
vector<int> leftSub(nums.begin(), nums.begin() + maxIndex);
vector<int> rightSub(nums.begin() + maxIndex + 1, nums.end());

// 4、递归处理左右数组
root->left = traversal(leftSub);
root->right = traversal(rightSub);

return root;
}
};

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

『面试の神』Java如何实现DistinctBy?

发表于 2021-11-19

「这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战」。

1.前言

Hello 大家好,我是l拉不拉米,在列表中搜索不同元素是我们程序员通常面临的常见任务之一。从包含 Streams 的 Java 8 开始,我们有了一个新的 API 来使用函数式方法处理数据。

在本文中,我们将展示4种使用列表中对象的特定属性过滤集合的方法。

  1. 使用Stream API

Stream API 提供了 distinct() 方法,该方法基于 Object 类的 equals() 方法返回列表的不同元素。

但是,如果我们想按特定属性进行过滤,它会变得不那么灵活。我们的替代方案之一是编写一个过滤器来维护状态。

2.1.使用状态过滤器

解决方案之一是实现有状态的 Predicate:

1
2
3
4
5
6
java复制代码public static <T> Predicate<T> distinctByKey( 
Function<? super T, ?> keyExtractor) {

Map<Object, Boolean> seen = new ConcurrentHashMap<>();
return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}

为了测试它,我们将使用以下具有属性 age、email 和 name 的 Person 类:

1
2
3
4
5
6
java复制代码public class Person { 
private int age;
private String name;
private String email;
// getters and setters
}

按名称获取新的过滤集合

1
2
3
java复制代码List<Person> personListFiltered = personList.stream()
.filter(distinctByKey(p -> p.getName()))
.collect(Collectors.toList());
  1. 使用 Eclipse Collections

Eclipse Collections 是一个Java类库,它提供了在 Java 中处理流和集合的附加方法。

3.1.使用 ListIterate.distinct()

ListIterate.distinct() 方法允许我们使用各种 HashingStrategies 过滤流。这些策略可以使用 lambda 表达式或方法引用来定义。

如果我们想按人名过滤:

1
2
java复制代码List<Person> personListFiltered = ListIterate
.distinct(personList, HashingStrategies.fromFunction(Person::getName));

或者,如果我们要使用的属性是原始属性(int、long、double),我们可以使用这样的专用函数:

1
2
java复制代码List<Person> personListFiltered = ListIterate.distinct( 
personList, HashingStrategies.fromIntFunction(Person::getAge));

3.2. Maven 依赖

1
2
3
4
5
xml复制代码<dependency> 
<groupId>org.eclipse.collections</groupId>
<artifactId>eclipse-collections</artifactId>
<version>8.2.0</version>
</dependency>
  1. 使用 Vavr (Javaslang)

这是 Java 8 的函数库,提供不可变数据和函数控制结构。

4.1.使用 List.distinctBy

为了过滤列表,该类提供了自己的 List 类,该类具有 distinctBy() 方法,允许我们按其包含的对象的属性进行过滤:

1
2
3
java复制代码List<Person> personListFiltered = List.ofAll(personList) 
.distinctBy(Person::getName)
.toJavaList();

4.2. Maven 依赖

1
2
3
4
5
xml复制代码<dependency> 
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.9.0</version>
</dependency>
  1. 使用 StreamEx

该库为 Java 8 流处理提供了有用的类和方法。

5.1.使用 StreamEx.distinct

在提供的类中是 StreamEx,它具有 distinct 方法,我们可以向该方法发送对要区分的属性的引用:

1
2
3
java复制代码List<Person> personListFiltered = StreamEx.of(personList) 
.distinct(Person::getName)
.toList();

5.2. Maven 依赖

1
2
3
4
5
xml复制代码<dependency> 
<groupId>one.util</groupId>
<artifactId>streamex</artifactId>
<version>0.6.5</version>
</dependency>

6.最后

创作不易,如果觉得这篇文章对您有所帮助,还请多多关注,多多点赞!!感谢!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【力扣-二叉树】12、从中序与后序遍历序列构造二叉树(106

发表于 2021-11-19

「这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战」

106. 从中序与后序遍历序列构造二叉树

题目描述

根据一棵树的中序遍历与后序遍历构造二叉树。

注意: 你可以假设树中没有重复的元素。

例如,给出

1
2
ini复制代码中序遍历 inorder = [9,3,15,20,7]
后序遍历 postorder = [9,15,7,20,3]

返回如下的二叉树:

1
2
3
4
5
markdown复制代码    3
/ \
9 20
/ \
15 7

解析

递归法

  • 步骤:
    • 1、判断数组大小是否为0,为0则表示空节点
    • 2、不为空,取后序遍历数组的最后一个元素(作为根节点)
    • 3、在中序遍历中找到后序遍历的最后一个元素
    • 4、切割中序数组,将中序数组分为中序左数组,中序右数组
    • 5、切割后序数组,得到后序左数组,后序右数组
    • 6、递归处理左区间和右区间

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
c++复制代码class Solution
{
public:
TreeNode *buildTree(vector<int> &inorder, vector<int> &postorder)
{
return traversal(inorder, postorder);
}

private:
TreeNode *traversal(vector<int> &inorder, vector<int> &postorder)
{
// 1、判断数组大小是否为0,为0则表示空节点,直接返回空
if (inorder.size() == 0)
{
return NULL;
}
// 取后序遍历的最后一个元素作为根节点
int rootVal = postorder[postorder.size() - 1];
// 设置根节点
TreeNode *root = new TreeNode(rootVal);
int splitPos;
// 3、查找中序遍历的分割点
for (splitPos = 0; splitPos < inorder.size(); splitPos++)
{
if (inorder[splitPos] == rootVal)
{
break;
}
}

// 4、切割中序数组,分为左数组和右数组
vector<int> inLeft(inorder.begin(), inorder.begin() + splitPos);
vector<int> inRight(inorder.begin() + splitPos + 1, inorder.end());

// 5、切割后序数组

// 首先舍去最后一个元素
postorder.resize(postorder.size() - 1);
// 后序数组的切割根据已有条件来获得
// 后序左数组的大小与中序左数组的大小相同,以此来找到切割点
vector<int> postLeft(postorder.begin(), postorder.begin() + inLeft.size());
vector<int> postRight(postorder.begin() + inLeft.size(), postorder.end());

// 6、递归处理左区间和右区间
root->left = traversal(inLeft, postLeft);
root->right = traversal(inRight, postRight);

return root;
}
};

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

录制快、回放稳,爱奇艺iOS云录制回放平台技术实践 二、落地

发表于 2021-11-19

众所周知,移动APP的周期短、迭代速度快,测试人员在保证新功能正常运行的情况下需要回归大量的历史功能,自动化回归便成为重要的回归手段之一。iOS端自动化由于以下问题,一直很难在业务线广泛开展。

  • 落地成本高:执行环境必须使用mac设备,硬件成本高,本地服务&设备难以跨团队共享。
  • 学习成本大:除了Python、node等基础编程环境外,还需要使用xcode,要有一定的OC基础。
  • 二次开发难:设备驱动如Appium WDA、Facebook-WDA,部分功能接口响应耗时长、稳定性不高,且基于OC语言编写,二次开发难度较大,难以针对业务特性定制。

基于上述情况,爱奇艺在云真机基础之上重点优化设备驱动,并将设备共享、远程租用、自动化脚本管理、任务调度、可视化报告等能力组合,推出了爱奇艺iOS云录制回放平台。该平台通过设备和服务共享可以有效降低业务线自动化投入,使测试人员更加专注于测试用例设计。通过项目实践和不断调优,平台在易用性和自动化执行效率等方面有显著提升,现已对接主站等多条业务线,覆盖视频播放、视频编辑、feed流等复杂场景,成为爱奇艺业务群质量保障中不可缺少的一环。本文将介绍爱奇艺iOS云录制回放平台的系统架构以及实现细节。

一、方案设计

1.1 基本流程

录制回放基本流程图

在录制过程中,云IDE实时监听鼠标点击、滑动事件,同时获取手机页面的DOM树,根据用户操作的坐标查找最匹配的元素节点,如果用户选择OCR和AI方式,则识别手机截图中的文本和已知的AI元素,确认元素之后会转换为自动化脚本并保存。

在回放时,首先拉取待执行脚本集合,进行脚本解析,通过脚本指定的方法查找元素并执行相应的操作,如点击、滑动等。任务结束后会生成测试报告,包括用例执行步骤、日志、截图等信息。

1.2 录制篇

  • 功能多样

录制页面采用Web化IDE设计,集成了设备选择、脚本管理、实时画面和脚本实时生成四大功能,支持脚本在线编辑和调试,并持久化到服务端,相对于其他竞品的录制页面,更方便、灵活。

  • 设备列表:展示可用的手机列表,支持设备实时切换;
  • 脚本管理:对录制的脚本进行管理,可以按照业务、用例集等不同维度归档;
  • 脚本编辑:在该IDE内操作APP会自动生成Python脚本,支持在线编辑和多机调试;
  • 手机画面:实时展示手机画面,同步监听用户操作事件,展示画面元素操作选择。

录制页面

  • 流畅的操作体验

除了功能多样外,流畅的操作体验也是好产品的重要评价指标之一,其性能直接影响平台用户体验和操作效率。我们调研了市面上主流的WDA,如Appium WDA、Facebook WDA等,他们的设计主要是用于自动化执行,设计思路优先保证case执行的稳定性,因此相关接口响应耗时比较长,这样的设计严重影响用户操作体验并不适合作为录制使用,考虑到开源的WDA都是用OC语言编写,学习成本比较高,我们决定使用SWIFT语言自行开发WDA,重点在DOM树获取速度、点击响应耗时和画面帧率方面进行优化。

在DOM树获取方面,区分层级页面的元素结构并加以剔除,有效降低元素查找速度。

针对点击响应慢的问题,我们优化原有接口调用逻辑,包括去除复杂的同步等待机制等,响应速度有了很大提升。

在屏幕获取方面则是通过WDA实现截图,重点优化图片压缩算法和传输效率,保证画面清晰度的同时达到每秒20帧以上的画面渲染效果,有效解决画面卡顿的问题。

优化前后数据对比如下:

远程录制画面渲染的效果图如下:

远程录制画面渲染效果图
  • 丰富的元素识别方式

写脚本容易,维护脚本不易,编写自动化脚本过程中,选择合适的元素获取方式才能从根本保证脚本的稳定性。相信很多同学都会有过这种经历:首次写完自动化执行成功率很高,但是随着功能迭代,自动化脚本成功率逐渐降低。测试同学被动地陷入自动化相关的维护里,自动化不仅没有起到保障质量的作用,反而成为了测试同学的负担。为了尽可能的适配各种场景,爱奇艺提供了多种元素定位方式,以适用不同的业务场景和兼容性需求。

元素识别方式分类

原生方式

iOS的原生支持方式,包括predicate、accessibility ID、坐标等方式,利用元素的type、name、label等属性定位,其中predicate支持比较、范围运算符等,这种方式适用于元素定义比较规范的场景。

XPath方式

路径定位方式,iOS原生系统并不支持此种方式,业界开源的工具定位速度很慢,通常在3-30秒,我们通过路径优化获取算法,XPath获取速度降低到1秒以内并且提高了多机回放的兼容性,优化前后数据对比如下:

图像识别

依托于爱奇艺自研的AI和机器学习技术,支持对UI界面截图进行OCR识别和图标识别,使得Android端和iOS端使用同一套UI自动化测试脚本成为可能,实现跨平台脚本能力,目前这种方式多用于图标和icon的识别场景。

1.3 回放篇

稳定的脚本加上完善的任务调度服务才能实现自动化任务的高效运转。iOS云平台回放调度服务从设备筛选、任务触发、任务执行和问题回溯等四个方面着力打造满足业务线需求的平台。

系统设计图

  • 设备筛选

手机设备作为自动化任务执行的最终载体,是自动化任务启动的第一步,其丰富程度和筛选的灵活性决定服务的应用范围。平台支持用户从机型、分辨率、系统、运营商等多个维度进行选择。为避免手机本身异常影响自动化任务执行,在自动化任务开始之前系统会进行设备环境检查,只将任务分配到检查通过的手机设备上执行。

  • 任务触发

任务触发无缝对接CICD,支持定时/手动/指定条件触发,增加用例集概念,支持以业务线/用户个人维度对执行脚本进行管理,并指定用例并行或串行执行。例如,需要验证兼容性可以选择相同用例多设备并发执行,在用例较多时可选择多设备分布式执行以提升执行效率。

  • 任务执行

自动化执行过程中,我们重点处理以下几类情况,以保证自动化任务稳定运行:

(1)实时监控并处理系统弹窗和App内各种弹窗等等,以保证任务执行过程不被弹窗干扰,弹窗处理有效率>90%。

(2)测试关键过程信息随时存取,同时启动崩溃检测,与bug项目协同管理,发现异常自动转为可视化bug。

(3)任务结束时自动进行清理环境,包括卸载App、清除本地执行过程数据等,支持异常任务支持选择相同条件设备进行重试。

  • 问题定位

对App异常情况提供足够的信息辅助开发修复问题形成闭环才是自动化最终的目的,可视化报告必不可少,它可以帮助用户快速了解整个任务的执行情况、定位异常。

爱奇艺iOS云录制回放提供的测试报告包含任务概况、设备信息、用例执行步骤、执行日志等内容,让我们对每个用例的执行过程都了如指掌,快速还原崩溃前后场景。

基于云端设备管理模式,可以快速跳转到对应设备进行场景回放和远程调试。

任务报告图

二、落地效果

录制回放系统极大地降低了iOS自动化的编写和运行成本,目前已应用于爱奇艺内部的多条业务线中,日均执行次数100+,发现多例功能性bug和崩溃。在日常的版本准入、回归和CICD流程中发挥着重要的作用。

平台提供的各类元素识别方法在不同系统手机上兼容性良好,平均执行成功率稳定在98%以上,常规版本迭代中脚本维护的成本低,单次维护半小时以内。

三、未来展望

当前自动化测试用例仍然需要测试同学进行多次调优才能达到最佳的执行效果,智能化测试用例生成是一个重要的探索方向,未来我们会不断地丰富元素的识别手段和生成方式,使用例生成更加智能、App自动化运行更稳定。

在问题定位方面,第一阶段还是以提供执行场景信息、人工定位和判断为主,后续我们会持续跟进各类问题产生原因并生成知识库,结合知识库实现失败场景的初步定位和问题智能分发,进一步降低自动化跟进和维护成本。

录制回放作为自动化的基础平台,除支持基本的功能验证和UI验证外,未来会与爱奇艺内部各平台打通,支持同步启动流量录制、PingBack检测、数据Mock等,助力各种专项测试。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…279280281…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%