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

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


  • 首页

  • 归档

  • 搜索

AspNet Core部署:早知道,还是docker!以及

发表于 2021-11-27

前言

AspNetCore技术栈在我们团队里的使用也有一段时间了,之前的部署方式一直是本地编译之后上传可执行文件到服务器,使用supervisor来管理进程这种很原始的方式。

参考之前的文章:zhuanlan.zhihu.com/p/203298625

对于小项目来说尚可,够用,但是存在几个问题:

  1. 每次更新花费的时间太长了,无论是Framework-Dependent还是Self-Contained,都要上传很大的文件~
  2. 更新的时候需要在supervisor里把进程停掉,不然无法覆盖
  3. 每次更新都是手动操作,一点也不geek

鉴于之前使用Django的项目里docker用得非常愉快,而且到处在宣传.netcore新技术对docker的官方支持有多好多好,于是我这也不能落后,必须上docker部署啊!

更关键的一点理由是,最近搞了个新的小项目,用到了Redis,但服务器上没装Redis,作为一个被docker惯坏的人,我也不可能去安装配置这些东西~

那就开始吧,直接从微软官方文档开始(MSDN真是好东西啊)

开始

事实上,我现在已经开始尝鲜使用 .net6.0 来新建项目了,默认的项目模板中就带有docker配置,完全是傻瓜式的,不用自己写什么配置文件,上传到服务器里就是docker build一把梭(或者是docker-compose up更好)

没有的也没事,把VS升级到最新的2022版本(其他版本应该也有,请自测),项目右键添加就能选择Docker支持了~

image.png

其实Rider也可以,并且我在此前也是一直使用Rider开发的,但截至本文编写时,Rider的2021.3版本还没推出,尚未支持 .net6.0 ,因此我没有使用Rider测试自动添加docker支持~

image.png

不想使用傻瓜式生成dockerfile也行,下面给出我部署的这个项目的dockerfile,可以参考一下。(项目名称为:DataMiddlePlatform)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bash复制代码#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["DataMiddlePlatform/DataMiddlePlatform.csproj", "DataMiddlePlatform/"]
RUN dotnet restore "DataMiddlePlatform/DataMiddlePlatform.csproj"
COPY . .
WORKDIR "/src/DataMiddlePlatform"
RUN dotnet build "DataMiddlePlatform.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "DataMiddlePlatform.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "DataMiddlePlatform.dll"]

其实dockerfile基本可以不管的,因为 .net6.0 项目生成的时候,都会有个dockerfile,在本项目中我只修改了docker-compose.yml文件,因为要增加一个Redis容器~

以下是我的docker-compose.yml文件代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yml复制代码version: '3.4'

services:
redis:
image: redis
expose:
- 6379

web:
image: ${DOCKER_REGISTRY-}web
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=https://+:443;http://+:80
build:
context: .
dockerfile: DataMiddlePlatform/Dockerfile
depends_on:
- redis
ports:
- "15002:80"
- "15003:443"

不多解释了,docker-compose的用法详见官方文档~

环境切换

之前我们在docker-compose.yml里添加了Redis容器,并在web镜像里添加了依赖,所以在Web容器里访问Redis是用redis:6379这样的地址,不像本地开发时一样使用localhost:6379,如果在Django里就要在settings.py里根据环境变量判断了(我的Django-Starter框架集成了自动识别)

而AspNetCore的基础设施做得很完善,默认就有development、staging、production这三个环境,每个环境有对应的appsettings.json配置文件,所以只要在配置文件里区分不同的Redis地址就好了,如果是其他数据库的配置也同理,真是方便啊~!

到本项目中就是,在appsettings.Development.json文件里,Redis的连接地址是"Connection": "127.0.0.1:6379",本地开发时使用本地安装的Redis服务。

然后appsettings.json文件里,使用"Connection": "redis:6379",docker部署的时候,使用docker里的Redis服务~

完美

部署

既然写完了dockerfile和docker-compose.yml,那部署这块也没啥好说的,就把整个代码文件夹上传到服务器,之后一行命令搞定:docker-compose up~

因为这AspNetCore本身性能就很可以了,小项目都不用nginx来提供静态文件服务,方便得很~!

要更新服务的话,目前就是上传代码之后执行docker-compose up --build重新构建,因为只需要上传代码文件,这样下来每次更新的速度快多了,而且可以写脚本在git commit后一键执行更新,也…算是自动了吧…hhh

小结

微服务是发展趋势,应用从旧的部署方式到容器化是很重要的一步,虽然我们团队的Java还处在一个jar包丢上去supervisor运行的阶段,(吐槽:后面甚至有项目都没有挂supervisor,直接shell里执行起来,什么时候挂了都不知道)

经过这段时间的实践下来,AspNetCore还是很可靠的扛住了不低的并发,也有比较完善的生态(虽然第三方库比不上Java和Python,但完完全全够好用了),开发效率高(但学习门槛也不低,至少比Django难),综合优缺点下来,加上我自己对这框架的掌握也还很粗浅,所以只能小范围推广了~

我对于AspNetCore目前还处在摸索阶段,好用是好用,就是在工作中用得还不够多,不够熟悉,加上之前折腾Django的经历让我尝到了动态语言开发的甜头,所以不会短时间把团队的技术栈全部迁移到NetCore上去,毕竟会C#的人还是不够多,新招进来的应届生来现学也有不低的门槛,项目进度也禁不起这种折腾…

我还是很喜欢微软的这套技术栈,它真的很好用,目前以及后续的小项目毫无疑问都会选择这套东西,但对于提高多少生产力,我目前是没多少底气的,接下来要调研一番构建自动化、部署自动化方面的技术,哎,小团队的基础设施不足真是痛苦…… 难怪现在Serverless开始流行起来了,低代码平台也起来了,程序员终究要自己砸自己的饭碗…

参考资料

  • 托管和部署 ASP.NET Core | Microsoft Docs
  • 在 Docker 容器中托管 ASP.NET Core | Microsoft Docs
  • .NET 微服务。 适用于容器化 .NET 应用程序的体系结构 | Microsoft Docs

本文转载自: 掘金

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

REST-Assured,接口自动化的 "瑞士军刀"- 初识

发表于 2021-11-27

REST-Assured 简介

REST-Assured 是一套基于 Java 语言实现的开源 REST API 测试框架,由作者 Johan Haleby 开发并维护,目前该项目在 GitHub 上已收获 4.9K star

image.png

从官方描述可以看到 REST-Assured 使得通过 Java 语言测试 REST API 变得更加简单和容易

REST-Assured 除了语法简洁之外,强大的解析功能(支持 XML,JSON)也是其成为如今企业首选的接口自动化框架原因之一。【软件测试资料合集】

REST-Assured 初体验

Step 1)安装 JDK

Step 2)安装 IDE,推荐 Intellij IDEA

Step 3)安装 Maven,设置 Maven 镜像

引入 REST-Assured 依赖

1.创建 Maven 工程

2.POM.xml 添加 REST-Assured 依赖坐标

1
2
3
4
5
6
xml复制代码  <dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>4.3.0</version>
<scope>test</scope>
</dependency>

3.创建 Java Class,静态导入 REST-Assured 类路径(官方推荐使用,编写脚本时更加有效率)

1
arduino复制代码  import static io.restassured.RestAssured.*;

4.第一个 get 请求

1
2
3
4
5
scss复制代码  given().
when().
get("http://httpbin.org/get?phone=13323234545&password=123456").
then().
log().body();

类似于行为驱动开发(Behaviour Driven Development-BDD)中的定义的结构 Given-When-Then,Given:在某场景下,When:发生什么事件,Then:产生了什么结果。而 REST-Assured 借鉴了这一套描述可以使得语法更加简洁:

  • given 设置测试预设(包括请求头、请求参数、请求体、cookies 等等)
  • when 所要执行的操作(GET/POST 请求)
  • then 解析结果、断言

所以我们很容易想到这条 case 的作用:发送 get 请求,log()表示输出响应结果信息,body()输出响应体内容。

如果要输出响应的所有信息,使用 log().all()即可。

param 参数设置

我们会注意到上面这条 case 参数和 URL 是拼接在一起的,REST-Assured 可以让每部分(URL,参数,请求头)分开来,确保我们的代码有更好的可读性,在 given 中配置 queryParam 查询参数:

1
2
3
4
5
6
7
scss复制代码  given().
queryParam("mobilephone","13323234545").
queryParam("password","123456").
when().
get("http://httpbin.org/get").
then().
log().body();

而且我们还能采用更加智能的方式:given 中指定 param,此时 REST-Assured 将会自动根据 Http 方法决定参数类型(GET 方法将会自动使用查询参数,POST 方法将会自动使用表单参数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scss复制代码  //GET方法将会自动使用查询参数
given().
param("mobilephone","13323234545").
param("password","123456").
when().
get("http://httpbin.org/get").
then().
log().body();
//POST方法将会自动使用表单参数
given().
param("mobilephone","13323234545").
param("password","123456").
when().
post("http://httpbin.org/post").
then().
log().body();

Cookies 设置

如果想要在请求中携带 Cookies 信息,REST-Assured 给我们提供了非常方便的方式:

1
2
3
4
5
6
scss复制代码  given().
cookie("cookieName","cookieValue").
when().
post("http://httpbin.org/post").
then().
log().body();

或者是指定多对 cookie:

1
2
3
4
5
6
scss复制代码  given().
cookie("cookieName","cookieValue1","cookieValue2").
when().
post("http://httpbin.org/post").
then().
log().body();

Header 设置

1
2
3
4
5
6
scss复制代码  given().
header("headerName","value").
when().
post("http://httpbin.org/post").
then().
log().body();

Content Type 设置

1
2
3
4
5
6
7
8
scss复制代码  given().
contentType("application/json").
//或者指定下面的形式
//contentType(ContentType.JSON).
when().
post("http://httpbin.org/post").
then().
log().body();

Request Body 设置

1
2
3
4
5
6
less复制代码  given().
body("{\"mobilephone\":\"13323234545\",\"password\":\"123456\"}").
when().
post("http://httpbin.org/post").
then().
log().body();

REST-Assured 还支持可以将 Java 对象序列化为 JSON 或者 XML,比如:

1 ) 通过 contentType 指定为 JSON,将 HashMap 序列化为 JSON

1
2
3
4
5
6
7
8
9
10
scss复制代码  HashMap<String,String> hashMap= new HashMap<String,String>();
hashMap.put("firstName","jack");
hashMap.put("lastName","tom");
given().
contentType(ContentType.JSON).
body(hashMap).
when().
post("http://httpbin.org/post").
then().
log().body();

2 )通过 contentType 指定为 JSON,将 Message 对象序列化为 JSON

Message.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
scss复制代码  public class Message {
private String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}


Message message = new Message();
message.setMessage("tester");
given().
contentType(ContentType.JSON).
body(message).
when().
post("http://httpbin.org/post").
then().
log().body();

3 ) 通过 contentType 指定为 XML,将 Message 对象序列化为 XML

在类前面加注解 XmlRootElement:

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码  @XmlRootElement
public class Message {}


Message message = new Message();
message.setMessage("tester");
given().
contentType(ContentType.XML).
body(message).
when().
post("http://httpbin.org/post").
then().
log().body();

校验响应数据

支持校验状态码, cookies, 响应头, content type 和响应体

1
2
3
4
5
6
7
8
9
10
erlang复制代码  given().
when().
post("http://httpbin.org/post").
then().
assertThat().statusCode(200);
//assertThat().cookie("cookieName","cookieValue");
//assertThat().header("headName","headerValue");
//assertThat().contentType(ContentType.JSON);
//assertThat().body(equalTo("something"));
//equalTo是hamcrest所带断言,hamcrest有提供非常丰富的断言方式

本文带着大家了解 REST-Assured 的基本结构和语法,当然,REST-Assured 的功能远不止这些,比如其内置的 JsonPath 解析和 XmlPath 解析以及 hamcrest 断言都是十分强大的功能,后续再给大家详细介绍。【软件测试资料合集】

本文转载自: 掘金

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

【Spring Boot 快速入门】二十、Spring Bo

发表于 2021-11-27

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

前言

  在很多后台管理系统中,有明确的权限和角色的管控,当然也少不了操作日志的记录。本文将基于Spring 的AOP特性开发一个日志记录功能。下面记录一下整个开发工程

快速开始

  使用Spring的AOP特性,首先了解AOP是什么,AOP在程序开发过程中是指面向切面编程,通过预编译和动态代理实现程序功能。AOP中主要有切点、切面、连接点、目标群、通知、织入方式等。通知类型常用的有前置通知、环绕通知、后置通知等,在日志记录的过程中一般使用环绕通知。具体的AOP的相关概念大家不熟悉的可以去查询一下。

版本信息

  本次Spring Boot 基于AOP注解实现日志记录功能,主要版本信息如下:

1
2
3
sql复制代码Spring Boot 2.3.0.RELEASE
aspectjweaver 1.9.6
maven 3

  主要引入的依赖是aspectjweaver,如果aspectjweaver 和Spring Boot 版本不一致,可能会报找不到切点等相关的异常,可以替换位相关版本即可解决。

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>

基础信息

  实现日志记录功能,主要是将操作日志记录到数据库中或者搜索引擎中,方便查询。在日志中需要记录操作人、操作时间、请求的参数、请求的ip、请求的连接、操作类型等信息。本次示例的建表SQL如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
less复制代码CREATE TABLE `log_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`model` varchar(255) DEFAULT NULL COMMENT '模块',
`log_type` tinyint(4) DEFAULT NULL COMMENT '类型0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=登录,9=清空数据,10查询',
`url` varchar(255) DEFAULT NULL COMMENT '请求链接',
`method` varchar(255) DEFAULT NULL COMMENT '请求方法',
`class_name` varchar(255) DEFAULT NULL COMMENT '类名',
`method_name` varchar(255) DEFAULT NULL COMMENT '方法名',
`params` varchar(500) DEFAULT NULL COMMENT '请求参数',
`ip` varchar(255) DEFAULT NULL COMMENT 'ip地址',
`user_id` int(11) DEFAULT NULL COMMENT '操作人id',
`user_name` varchar(255) DEFAULT NULL COMMENT '操作人',
`sys_info` varchar(255) DEFAULT NULL COMMENT '系统信息',
`create_user` varchar(200) CHARACTER SET utf8 DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_user` varchar(200) CHARACTER SET utf8 DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`data_state` tinyint(4) NOT NULL DEFAULT '1' COMMENT '0 删除 1未删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;

  本次将操作类型分为11类包登录、退出、增删改查、导入导出、清空等相关日志操作类别。

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
scss复制代码@Getter
@AllArgsConstructor
public enum LogTypeEnum {
/**
* 0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=登录,9=清空数据,10查询
* */
OTHER(0,"其它"),
ADD(1,"新增"),
UPDATE(2,"修改"),
DEL(3,"删除"),
AUTH(4,"授权"),
EXPORT(5,"导出"),
IMPORT(6,"导入"),
QUIT(7,"强退"),
GENERATE_CODE(8,"登录"),
CLEAR(9,"清空"),
QUERY(10,"查询"),
;


@EnumValue
private int value;

private String desc;
}

Log注解

  当数据初始化完成之后,就是编写一个自定义的Log注解。本次使用的注解主要是针对方法进行注解。具体如下:

  • @Target:注解的目标位置,主要可以有接口、类、枚举、字段、方法、构造函数、包等位置。可以根据需要进行配置。日志使用的本次基于方法注解。
  • @Retention:是指注解保留的位置,可以在源码中、类中、运行中。本次日志操作记录肯定是在运行中使用,所以选择RUNTIME。
  • @Documented:字面意思文档,也就是说明该注解将被包含在javadoc中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 模块
* */
String model() default "";

/**
* 操作
* */
LogTypeEnum logType() default LogTypeEnum.OTHER;
}

LogAspect

  定义一个日志的LogAspect切面。在类中需要使用@Aspect和
@Component指明这是一个切面方法在运行中进行扫描包。需要注意的是这个方法中需要定义切面和通知类型。最后根据注解和请求参数中的信息,查询到操作日志需要的信息。由于本次无需登录直接接口请求,所以操作人和操作id在演示中使用了默认值。本次示例只提取了部分操作日志信息,在项目中需要加入的日志信息多,可以根据需求进行修改。

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
61
62
63
64
65
66
67
68
69
70
scss复制代码@Aspect
@Component
public class LogAspect {

@Resource
private LogInfoMapper logInfoMapper;

/**
* @ClassName logPointCut
* @Description:切点信息
* @Author JavaZhan @公众号:Java全栈架构师
* @Version V1.0
**/
@Pointcut("@annotation(com.example.demo.log.Log)")
public void logPointCut(){

}

/**
* @ClassName aroundForLog
* @Description:环绕通知
* @Author JavaZhan @公众号:Java全栈架构师
* @Version V1.0
**/
@Around("logPointCut()")
public Object aroundForLog(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
Object result = point.proceed();
saveSmsLog(point);
return result;
}


/**
* @ClassName saveLogInfo
* @Description: 保存操作日志
* @Author JavaZhan @公众号:Java全栈架构师
* @Version V1.0
**/
private void saveLogInfo(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
LogInfo logInfo = new LogInfo();
logInfo.setClassName(joinPoint.getTarget().getClass().getName());
logInfo.setMethodName(signature.getName());
Log log = method.getAnnotation(Log.class);
if(log != null){
logInfo.setModel(log.model());
logInfo.setLogType(log.logType().getValue());
}
Object[] args = joinPoint.getArgs();
try{
String params = JSONObject.toJSONString(args);
logInfo.setParams(params);
}catch (Exception e){

}
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
logInfo.setIp(IpUtils.getIpAddr(request));
logInfo.setUrl(request.getServletPath());
logInfo.setMethod(request.getMethod());
logInfo.setUserId(123);
logInfo.setUserName("admin");
logInfo.setCreateTime(new Date());
logInfo.setDataState(1);
//保存操作日志
logInfoMapper.insert(logInfo);
}
}

测试日志

  本示例将基于常用的接口进行测试。在Controller方法中,我们调用自定义注解,根据方法的实际使用含义指定方法的model信息和操作类型信息即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
less复制代码@RequestMapping("user")
@Controller
public class UserController {

@Resource
private UserService userService;

@RequestMapping("getAllUser")
@ResponseBody
@Log(model = "查询用户列表",logType = LogTypeEnum.QUERY)
public List<User> getAllUser(){
return userService.getAllUser();
}


@RequestMapping("getUserById")
@ResponseBody
@Log(model = "获取指定用户",logType = LogTypeEnum.QUERY)
public User getUserById(Integer id ){
return userService.getById(id);
}
}

调用接口:http://127.0.0.1:8888/user/getAllUser 返回的数据信息如下。
图片.png
查看日志表中,可以看到已经新增一条查询用户列表的日志信息。
图片.png
下面访问其他接口:http://127.0.0.1:8888/user/getUserById?id=1 返回的数据信息如下。

图片.png
查看日志表中,可以看到已经新增一条获取指定用户的日志信息。
图片.png

结语

  好了,以上就是Spring Boot 基于AOP注解实现日志记录功能的示例,感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

  作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

本文转载自: 掘金

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

归并排序以及Master公式 一、概念 二、排序过程 三、M

发表于 2021-11-27

图片

一、概念

归并排序(Merge Sort)是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法的一个非常典型的应用。

将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。

二、排序过程

1、归并操作,指的是将两个顺序序列合并成一个顺序序列的方法。

如:数组 {6,202,100,301,38,8,1}

初始状态:6,202,100,301,38,8,1

第一次归并后:{6,202},{100,301},{8,38},{1};

第二次归并后:{6,100,202,301},{1,8,38};

第三次归并后:{1,6,8,38,100,202,301};

2、归并操作步骤如下:

(1)申请空间,大小为两个已经排好序的序列之和,该空间用来做辅助数组

(2)设定两个指针,最初位置分别为两个已经排序序列的起始位置

(3)比较两个指针所指向的元素,选择相对小的元素放入到合并空间(相等选择左组),并移动指针到下一位置。

重复步骤3直到某一指针越界。将另一序列剩下的所有元素直接复制到合并序列(辅助数组)尾,将辅助数组数据拷贝回原数组。

图片

三、Master公式估计时间复杂度

Master公式:分析递归函数的时间复杂度,要求子问题规模一致

形如:

T(N) = a * T(N/b) + O(N^d)(其中a、b、d都是常数)的递归函数,可以直接通过Master公式来确定时间复杂度

1)如果log(b,a) < d,时间复杂度为O(N^d)

2)如果log(b,a) > d,时间复杂度为O(N^log(b,a))

3)如果log(b,a) == d,时间复杂度为O(N^d * logN)

根据Master公式可得 T(N) =2 * T(N/2) + O(N)

因为每次都是拆分为两个子问题,每个子问题占总规模的一半,且合并过程的时间复杂度是O(N)

可得归并排序的时间复杂度是O(N*logN)。

四、代码实现

1、传统的递归方法实现

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
java复制代码    /**
     * 1.递归方法实现
     */
    public static void mergeSort1(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        process(arr, 0, arr.length - 1);
    }

    private static void process(int[] arr, int l, int r) {
        if (l == r) {
            return;
        }
        int mid = l + ((r - l) >> 1);
        // 左组递归
        process(arr, l, mid);
        // 右组递归
        process(arr, mid + 1, r);
        // 合并
        merge(arr, l, mid, r);
    }

    private static void merge(int[] arr, int l, int m, int r) {
        // 辅助数组
        int[] help = new int[r - l + 1];
        int i = 0;

        // 左组位置
        int pL = l;
        // 右组位置
        int pR = m + 1;

        while (pL <= m && pR <= r) {
            // 谁小拷贝谁,相等的拷贝左组
            help[i++] = arr[pL] <= arr[pR] ? arr[pL++] : arr[pR++];
        }
        // pL和pR有且只有一个会越界,也就是下面两个while只有一个会执行
        while (pL <= m) {
            help[i++] = arr[pL++];
        }
        while (pR <= r) {
            help[i++] = arr[pR++];
        }
        // 拷贝回原数组
        for (int j = 0; j < help.length; j++) {
            arr[l + j] = help[j];
        }
    }

2、迭代方式实现

定义一个步长,初始值为1。从0位置的数开始,每两个步长的数merge完后拷贝回原数组,步长*2;再从0位置的数开始,每两个步长的数merge完后拷贝回原数组,步长*2……直到(步长 > 数组长度 / 2)

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
java复制代码    /**
     * 2.迭代方式实现归并排序
     * <p>
     * 步长调整次数 复杂度是logN,每次调整步长都会遍历一遍整个数组 复杂度是N,整个的时间复杂度是O(N*logN)
     */
    public static void mergeSort2(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        int N = arr.length;
        // 步长初始值
        int mergeSize = 1;

        // 步长不能超过数组长度
        while (mergeSize < N) {
            // 当前左组的第一个位置
            int L = 0;
            // 左组也不能超过数组长度
            while (L < N) {
                // 左组最后一个位置
                int M = L + mergeSize - 1;
                // 如果左组最后一个位置越界,表明左组都不够则不需要merge
                if (M >= N) {
                    break;
                }

                // 右组的最后一个位置
                // 右组第一个位置是 M + 1,满足个数要求则右组大小是mergeSize,所以最后一个位置是 (M + 1) + mergeSize - 1
                // 不满足则是数组的最大位置 N - 1
                int R = Math.min(M + mergeSize, N - 1);

                // 目前 左组:L......M,右组:M+1......R
                merge(arr, L, M, R);

                // 下一次左组的位置(所以才需要判断L < N)
                L = R + 1;
            }

            // 防止溢出
            // 当步长很靠近N的最大值时,乘以2扩大步长后,步长就溢出了
            // 不能取 >=,假设最大值是9,mergeSize = 4时就不会扩大步长了,但是mergeSize = 8时还有一次merge,所以不能取 =
            if (mergeSize > N / 2) {
                break;
            }

            // 步长每次扩大2倍
            mergeSize <<= 1;
        }

    }

本文转载自: 掘金

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

Linux之Poll机制理解 Linux内核实现过程 pol

发表于 2021-11-27

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

最近看了一下Linux Poll 机制的实现,看了韦老师的分析文档,总结如下:

int poll(struct pollfd *fds,nfds_t nfds, int timeout);

总的来说,Poll机制会判断fds中的文件是否可读,如果可读则会立即返回,返回的值就是可读fd的数量,如果不可读,那么就进程就会休眠timeout这么长的时间,然后再来判断是否有文件可读,如果有,返回fd的数量,如果没有,则返回0.

Linux内核实现过程

在内核中大致上实现过程:

当应用程序调用poll函数的时候,会调用到系统调用sys_poll函数,该函数最终调用do_poll函数,do_poll函数中有一个死循环,在里面又会利用do_pollfd函数去调用驱动中的poll函数(fds中每个成员的字符驱动程序都会被扫描到),驱动程序中的Poll函数的工作有两个,一个就是调用poll_wait 函数,把进程挂到等待队列中去(这个是必须的,你要睡眠,必须要在一个等待队列上面,否则到哪里去唤醒你呢。另一个是确定相关的fd是否有内容可 读,如果可读,就返回1,否则返回0,如果返回1 ,do_poll函数中的count++,
然后 do_poll函数然后判断三个条件(if (count ||!timeout || signal_pending(current)))如果成立就直接跳出,如果不成立,就睡眠timeout个jiffes这么长的时间(调用schedule_timeout实现睡眠),如果在这段时间内没有其他进程去唤醒它,那么第二次执行判断的时候就会跳出死循环。如果在这段时间内有其他进程唤醒它,那么也可以跳出死循环返回(例如我们可以利用中断处理函数去唤醒它,这样的话一有数据可读,就可以让它立即返回)。

poll机制分析

所有的系统调用,基于都可以在它的名字前加上“sys_”前缀,这就是它在内核中对应的函数。比如系统调用open、read、write、poll,与之对应的内核函数为:sys_open、sys_read、sys_write、sys_poll。

内核框架

对于系统调用poll或select,它们对应的内核函数都是sys_poll。分析sys_poll,即可理解poll机制。

  1. sys_poll函数位于fs/select.c文件中,代码如下:
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++复制代码asmlinkagelong sys_poll(struct pollfd __user *ufds, unsigned int nfds,

                 long timeout_msecs)

{

         s64 timeout_jiffies;




         if (timeout_msecs > 0) {

#ifHZ > 1000

             /* We can only overflow if HZ >1000 */

             if (timeout_msecs / 1000 >(s64)0x7fffffffffffffffULL / (s64)HZ)

                 timeout_jiffies = -1;

             else

#endif

                 timeout_jiffies =msecs_to_jiffies(timeout_msecs);

         } else {

             /* Infinite (< 0) or no (0)timeout */

             timeout_jiffies = timeout_msecs;

         }




         return do_sys_poll(ufds,nfds, &timeout_jiffies);

}

它对超时参数稍作处理后,直接调用do_sys_poll。

  1. do_sys_poll函数也位于位于fs/select.c文件中,我们忽略其他代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c++复制代码intdo_sys_poll(struct pollfd __user *ufds, unsigned int nfds, s64 *timeout)

{

……

poll_initwait(&table);

……

         fdcount = do_poll(nfds, head,&table, timeout);

……

}

poll_initwait函数非常简单,它初始化一个poll_wqueues变量table:

poll_initwait> init_poll_funcptr(&pwq->pt, __pollwait); > pt->qproc = qproc;

即table->pt->qproc= __pollwait,__pollwait将在驱动的poll函数里用到。

  1. do_sys_poll函数位于fs/select.c文件中,代码如下:
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
c++复制代码static int do_poll(unsigned int nfds,  struct poll_list *list,

            struct poll_wqueues *wait, s64 *timeout)

{

……

  for (;;){

……

                  if(do_pollfd(pfd, pt)) {

                           count++;

                           pt = NULL;

                   }

……

       if(count || !*timeout || signal_pending(current))

           break;

       count= wait->error;

       if(count)

           break;



       if(*timeout < 0) {

           /*Wait indefinitely */

           __timeout= MAX_SCHEDULE_TIMEOUT;

       }else if (unlikely(*timeout >= (s64)MAX_SCHEDULE_TIMEOUT-1)) {

           /*

           * Wait for longer than MAX_SCHEDULE_TIMEOUT. Do it in

           * a loop

           */

           __timeout= MAX_SCHEDULE_TIMEOUT - 1;

           *timeout-= __timeout;

       }else {

           __timeout= *timeout; 
*timeout= 0;

       }



       __timeout= schedule_timeout(__timeout); // 休眠时间由应用提供

       if(*timeout >= 0)

           *timeout+= __timeout;

   }

   __set_current_state(TASK_RUNNING);

   returncount;

}

分析其中的代码,可以发现,它的作用如下:

① 从02行可以知道,这是个循环,它退出的条件为:

a. 09行的3个条件之一(count非0,超时、有信号等待处理)

count顺0表示04行的do_pollfd至少有一个成功。

b. 11、12行:发生错误

② 重点在do_pollfd函数,后面再分析

③ 第30行,让本进程休眠一段时间,注意:应用程序执行poll调用后,如果①②的条件不满足,进程就会进入休眠。那么,谁唤醒呢?除了休眠到指定时间被系统唤醒外,还可以被驱动程序唤醒──记住这点,这就是为什么驱动的poll里要调用poll_wait的原因,后面分析。

  1. do_pollfd函数位于fs/select.c文件中,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
c++复制代码static inline unsigned int do_pollfd(struct pollfd*pollfd, poll_table *pwait)

{

……

             if(file->f_op && file->f_op->poll)

                     mask= file->f_op->poll(file, pwait);

……

}

可见,它就是调用我们的驱动程序里注册的poll函数。

驱动程序

驱动程序里与poll相关的地方有两处:一是构造file_operation结构时,要定义自己的poll函数。二是通过poll_wait来调用上面说到的__pollwait函数,pollwait的代码如下:

1
2
3
4
5
6
7
8
9
c++复制代码staticinline void poll_wait(struct file * filp, wait_queue_head_t * wait_address,poll_table *p)

{

         if (p && wait_address)

             p->qproc(filp, wait_address, p);

}

p->qproc就是__pollwait函数,从它的代码可知,它只是把当前进程挂入我们驱动程序里定义的一个队列里而已。它的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
c++复制代码staticvoid __pollwait(struct file *filp, wait_queue_head_t *wait_address,

                         poll_table *p)

{

         struct poll_table_entry *entry =poll_get_entry(p);

         if (!entry)

             return;

         get_file(filp);

         entry->filp = filp;

         entry->wait_address = wait_address;

         init_waitqueue_entry(&entry->wait,current);

         add_wait_queue(wait_address,&entry->wait);

}

执行到驱动程序的poll_wait函数时,进程并没有休眠,我们的驱动程序里实现的poll函数是不会引起休眠的。让进程进入休眠,是前面分析的do_sys_poll函数的30行“__timeout = schedule_timeout(__timeout)”。

poll_wait只是把本进程挂入某个队列,应用程序调用poll > sys_poll> do_sys_poll > poll_initwait,do_poll > do_pollfd > 我们自己写的poll函数后,再调用schedule_timeout进入休眠。如果我们的驱动程序发现情况就绪,可以把这个队列上挂着的进程唤醒。可见,poll_wait的作用,只是为了让驱动程序能找到要唤醒的进程。即使不用poll_wait,我们的程序也有机会被唤醒:chedule_timeout(__timeout),只是休眠__time_out这段时间。

总结一下poll机制

  1. poll > sys_poll > do_sys_poll >poll_initwait,poll_initwait函数注册一下回调函数__pollwait,它就是我们的驱动程序执行poll_wait时,真正被调用的函数。
  2. 接下来执行file->f_op->poll,即我们驱动程序里自己实现的poll函数

它会调用poll_wait把自己挂入某个队列,这个队列也是我们的驱动自己定义的;

它还判断一下设备是否就绪。

  1. 如果设备未就绪,do_sys_poll里会让进程休眠一定时间,这个时间是应用提供的“超时时间”
  2. 进程被唤醒的条件有2:一是上面说的“一定时间”到了,二是被驱动程序唤醒。驱动程序发现条件就绪时,就把“某个队列”上挂着的进程唤醒,这个队列,就是前面通过poll_wait把本进程挂过去的队列。
  3. 如果驱动程序没有去唤醒进程,那么chedule_timeout(__timeou)超时后,会重复2、3动作1次,直到应用程序的poll调用传入的时间到达, 然后返回。

本文转载自: 掘金

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

深度剖析数据在内存中的存储-初了解

发表于 2021-11-27

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

最近,想复习一下C语言,所以笔者将会在掘金每天更新一篇关于C语言的文章! 各位初学C语言的大一新生,以及想要复习C语言/C++知识的不要错过哦! 夯实基础,慢下来就是快!

数据类型介绍

1
2
3
4
5
6
7
8
arduino复制代码char        //字符数据类型
short      //短整型
int        //整形
long        //长整型
longlong  //更长的整形
float      //单精度浮点数
double      //双精度浮点数
//C语言有没有字符串类型?->没有

类型的意义:

1.使用这个类型开辟内存空间的大小(大小决定了使用范围)。

2.如何看待内存空间的视角。


类型的基本归类:

整形家族:

1
2
3
4
5
6
7
8
9
10
11
12
arduino复制代码char
unsignedchar
signedchar
short
unsignedshort [int]
signedshort [int]
int
unsignedint
signedint
long
unsignedlong [int]
signedlong [int]

浮点数家族:

1
2
arduino复制代码float
double

构造类型:

1
2
3
4
shell复制代码>数组类型
>结构体类型struct
>枚举类型enum
>联合类型union

指针类型

1
2
3
4
arduino复制代码int*pi;
char*pc;
float*pf;
void*pv;

空类型:

1
2
arduino复制代码void表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型。

原码反码补码的概念

计算机中的有符号数有三种表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位三种表示方法各不相同。


原码

直接将二进制按照正负数的形式翻译成二进制就可以。

反码

将原码的符号位不变,其他位依次按位取反就可以得到了。

补码

反码+1就得到补码。


正负数的计算规则是不相同的

正数的原、反、补码都相同。
对于整形来说:数据存放内存中其实存放的是补码。


为什么内存中存放的都是补码

为什么呢?
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同
时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需
要额外的硬件电路。


今天就先到这吧~感谢你能看到这里!希望对你有所帮助!欢迎老铁们点个关注订阅这个专题! 同时欢迎大佬们批评指正!

本文转载自: 掘金

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

Netty 零拷贝机制分析(一)

发表于 2021-11-27

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

前言

零拷贝机制(Zero-Copy)是在操作数据时不需要将数据从一块内存区域复制到另一块内存区域的技术,这样就避免了内存的拷贝,使得可以提高CPU的。零拷贝机制是一种操作数据的优化方案,通过避免数据在内存中拷贝达到的提高CPU性能的方案。

  1. 什么是零拷贝机制?

场景:我们以文件服务器下载文件为例, 服务器将硬盘中的数据通过网络发送到客户端?

image.png

image.png
如图所示,整个过程可以分为4步。

1、操作系统通过DMA传输将硬盘中的数据复制到内核缓冲区

2、操作系统执行read方法将内核缓冲区的数据复制到用户空间

3、操作系统执行write方法将用户空间的数据复制到内核socket缓冲区

4、操作系统通过DMA传输将内核socket缓冲区数据复制给网卡发送数据

  1. 操作系统零拷贝?

我们可以看到,操作系统从磁盘拷贝数据到内核空间, 在从内核空间拷贝到用户空间,然后在拷贝到内核的socket内核空间,然后拷贝到网卡传输。
感觉有点多次一举, 那么操作系统为什么会这么设计?

因为对于操作系统来说,可能有多个应用程序会同时使用这些数据,并有可能进行修改,如果让大家都使用同一份内核空间的数据就会产生冲突。因此,操作系统设计为:每个应用程序想使用这些数据都必须复制一份到自己的用户空间,这样就不会互相影响了。所以这个机制在碰到数据不需要做修改的场景时就产生了浪费,数据本来可以呆在内核缓冲区不动,没必要再多此一举拷贝一次到用户空间。

mmap优化

为了避免上面的这种浪费,最开始采用了mmap调用的方式来进行优化。
mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。

image.png
现在,你只需要从内核缓冲区拷贝到 Socket 缓冲区即可,这将减少一次内存拷贝(从 4 次变成了 3 次),但不减少上下文切换次数。

sendfile优化

Linux 2.1 版本 提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换。

image.png

如上图,我们进行 sendFile 系统调用时,数据被 DMA 引擎从文件复制到内核缓冲区,然后调用 write 方法时,从内核缓冲区进入到 Socket,这时,是没有上下文切换的,因为都在内核空间。

sendfile再次优化

Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。具体如下图:
image.png
原来的3次拷贝,现在只需要2 次拷贝:第一次使用 DMA 引擎从文件拷贝到内核缓冲区,第二次从内核缓冲区将数据拷贝到网络协议栈;内核缓存区只会拷贝一些 offset 和 length 信息到 SocketBuffer,基本无消耗。

零拷贝定义:
“Zero-copy” describes computer operations in which the CPU does not perform the task of copying data from one memory area to another.

从用户空间到socket缓冲两次复制。但是明明还有两次数据的复制,为什么要叫“零拷贝”呢?这是因为从操作系统的角度来说,数据没有从内存复制到内存的过程,也就没有了CPU参与的过程, 所以对于操作系统来说就是零拷贝了。

mmap和sendfile区别

mmap 和 sendFile 的区别。

  1. mmap 适合小数据量读写,sendFile 适合大文件传输。
  2. mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝。
  3. sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。

实际场景中rocketMQ 在消费消息时,使用了 mmap。kafka 使用了 sendFile。

Netty中的零拷贝

本文转载自: 掘金

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

盒式布局

发表于 2021-11-27

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

行型盒式布局

1.Box类的静态方法createHorizontalBox()可以获得一个具有行型盒式布局的盒式容器

• Box box;

• box=Box.createHorizontalBox();
2.行型盒式布局容器中添加的组件的上沿在同一水平线上

在这里插入图片描述

列型盒式布局

1.Box类的静态方法createVerticalBox()可以获得一
个具有列型盒式布局的盒式容器

• Box box;

• box=Box.createVerticalBox();

2.列型盒式布局容器中添加的组件的左沿在同一
垂直线上
在这里插入图片描述

盒式布局管理器

1.如果想控制盒式布局容器中组件之间的距离,
就需要使用水平支撑或垂直支撑

2.Box类调用静态方法createHorizontalStrut(int width)
可以得到一个水平支撑,高度为0,宽度是width

3.Box类调用静态方法createVertialStrut(int height)
可以得到一个垂直支撑,高度为height ,宽度为0

边框布局管理器

BorderLayout(边框布局管理器)是 Window、JFrame 和 JDialog 的默认布局管理器。边框布局管理器将窗口分为 5 个区域:North、South、East、West 和 Center。其中,North 表示北,将占据面板的上方;Soufe 表示南,将占据面板的下方;East表示东,将占据面板的右侧;West 表示西,将占据面板的左侧;中间区域 Center 是在东、南、西、北都填满后剩下的区域,如图 1 所示。

image.png

提示:边框布局管理器并不要求所有区域都必须有组件,如果四周的区域(North、South、East 和 West 区域)没有组件,则由 Center 区域去补充。如果单个区域中添加的不只一个组件,那么后来添加的组件将覆盖原来的组件,所以,区域中只显示最后添加的一个组件。

BorderLayout 布局管理器的构造方法如下所示。

  • BorderLayout():创建一个 Border 布局,组件之间没有间隙。
  • BorderLayout(int hgap,int vgap):创建一个 Border 布局,其中 hgap 表示组件之间的横向间隔;vgap 表示组件之间的纵向间隔,单位是像素。

本文转载自: 掘金

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

SpringBoot集成Swagger(九)给你的Swagg

发表于 2021-11-27

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


相关文章

Java随笔记:Java随笔记


前言

  • 其实讲到这里,关于Swagger基本功能的介绍就结束了。
  • 不知道大家对Swagger的页面是怎么看的?我看起来反正不大舒服。
  • 所以,今天给大家介绍几款Swagger的皮肤!!
  • 皮肤的使用非常简单,只需简单的引入依赖即可。

一、bootstrap-ui

  • 引入依赖:
  • 1
    2
    3
    4
    5
    6
    xml复制代码 <!-- 引入swagger-bootstrap-ui包 /doc.html-->
           <dependency>
               <groupId>com.github.xiaoymin</groupId>
               <artifactId>swagger-bootstrap-ui</artifactId>
               <version>1.9.1</version>
           </dependency>
  • 重启项目,访问:http://localhost:8080/doc.html

  • image-20211127182019892.png

  • 以markdown形式展示文档,将文档的请求地址、类型、请求参数、示例、响应参数分层次依次展示,接口文档一目了然,方便开发者对接。

二、swagger-mg-ui

  • 引入依赖:

  • 1
    2
    3
    4
    5
    xml复制代码        <dependency>
               <groupId>com.zyplayer</groupId>
               <artifactId>swagger-mg-ui</artifactId>
               <version>1.0.6</version>
           </dependency>
  • 重启项目,访问:http://127.0.0.1:8080/document.html

  • image-20211127183140495.png

  • ui支持多种树形菜单展示方式,但我觉得所有请求的颜色都是一样的,反而有点让人误导,不是一目了然!!

三、knife4j

  • 引入依赖:

  • 1
    2
    3
    4
    5
    xml复制代码        <dependency>
               <groupId>com.github.xiaoymin</groupId>
               <artifactId>knife4j-spring-ui</artifactId>
               <version>2.0.6</version>
           </dependency>
  • 重启项目,访问:http://127.0.0.1:8080/doc.html

  • image-20211127184003573.png
  • 关于这个得多说几句。
  • knife4j 2.0.6及以上版本,Spring Boot的版本必须大于等于2.2.x,且springfox版本要对应;
  • 2.0.6及以上版本,使用@EnableSwagger2WebMvc注解开启,而2.0.6之前版本是使用@EnableSwagger2注解,和swagger-bootstrap-ui是一样的。

总结

  • 其实还有一种皮肤swagger-ui-layer,但是由于此项目已经停止维护,不兼容最新的SpringBoot和Swagger。所以在此就不列举出来了!
  • 上面介绍了三种皮肤,以我的经验看来,最常用的肯定是knife4j,这也是我平常在使用的风格。
  • 大家可以根据自己的喜欢来选择皮肤!
  • 以上都是个人所言,如有不对,欢迎指出。
  • 如果有其他的皮肤,也欢迎大家指出,我会补充上去!
  • Swagger系列到此结束啦~ 明日开新坑!

路漫漫其修远兮,吾必将上下求索~

如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧~hahah

本文转载自: 掘金

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

翻译翻译,什么叫类与对象! 类与对象

发表于 2021-11-27

类与对象

  1. 面向过程和面向对象

大一刚开始学编程的时候,老师说一定要了解面向过程开发和面向对象开发。

我当时心想:“学校还能分配个对象给我?让我天天面向她?”后来发现是我多想了。

我于是下意识去百度一下:什么是面向对象?

我发现百度的解释跟我大学老师一样:高深莫测,不能通俗易懂。

后来我头悬梁锥刺股去学,终于弄懂了。

下面我会用通俗易懂的小案例来告诉大家这两者的区别。


案例一:打王者荣耀

面向过程:

    1. 打开王者荣耀的 app
    1. 登录账号
    1. 选择5V5的游戏模式
    1. 选择英雄
    1. 开始游戏
    1. 游戏结束

面向对象:

    1. 创建三个对象:人、手机、王者荣耀游戏
    1. 给人对象增加方法:打开手机、操作王者荣耀
    1. 给手机对象增加方法:运行王者荣耀
    1. 给王者荣耀对象增加方法:登录、设置游戏模式、设置英雄、符文、开始游戏、判定游戏输赢等。
    1. 人打开手机
    1. 手机运行王者荣耀
    1. 人玩王者荣耀游戏
    1. 王者荣耀通过人的操作判定输赢

案例二:把大象装进冰箱

面向过程:

    1. 打开冰箱门
    1. 把大象装进去
    1. 关闭冰箱门

面向对象:

    1. 创建两个对象:冰箱、大象
    1. 给冰箱新增方法:开门()、装大象()、关门()
    1. 冰箱开门:冰箱.开门()
    1. 冰箱装大象:冰箱.装大象(大象)
    1. 冰箱关门:冰箱.关门()

案例三:洗衣机洗衣服

面向过程:

    1. 打开洗衣机
    1. 把脏衣服放进去
    1. 倒点洗衣液
    1. 拧开水龙头加水
    1. 开始洗衣服
    1. 洗完衣服关掉洗衣机

面向对象:

    1. 创建两个对象:人、洗衣机
    1. 给人增加方法:操作洗衣机、放脏衣服、打开水龙头、加洗衣液
    1. 给洗衣机增加方法:洗衣服
    1. 人打开洗衣机:人.打开洗衣机()
    1. 人放入脏衣服:人.放脏衣服()
    1. 人加洗衣液:人.加入洗衣液(洗衣液)
    1. 人打开水龙头:人.打开水龙头()
    1. 洗衣机洗衣服:洗衣机.洗衣服()

1.1 面向过程

通过以上例子我们知道面向过程是把一件事情分成好几个步骤去做,强调做事的过程。

比如要想学会降龙十八掌,需要先学第一式,再学第二式……最后学第十八式,每一式与每一式之间的关联性很强,只有依次学完每一式,才能练成。

优点:性能比面向对象高,因为面向对象还要先创建对象,比较消耗计算机的内存,所以更适合小型项目。

缺点:每一个步骤之间的关联性太强,耦合度高,不容易扩展。

1.2 面向对象

通过以上例子我们知道面向对象是强调对象的重要性,把所有的东西都看成对象,把跟他相关的东西都封装在一起。就好比用洗衣机洗衣服那个案例:

1
2
3
4
5
6
7
8
9
java复制代码人{
打开洗衣机()
放脏衣服()
加入洗衣液(洗衣液)
}

冰箱{
洗衣服()
}

把一件事情的完成分成一个个对象,给对象赋予一定的功能,然后由对象之间分工协作。

优点:代码可复用、容易维护和开发、可扩展性强(比如洗衣服不想用洗衣液了可以换成洗衣粉),所以更适合大型项目。

缺点:比面向过程的性能低

总结:

  • 面向过程:强调过程。
  • 面向对象:强调对象、分工和协作。
  1. 类与对象

类:类是一个抽象的概念,在现实世界中不存在。

类本质上是在现实世界中具有共同特征的事物,通过提取这些共同特征形成的概念叫做“类”,比如人类、动物类、汽车类、鸟类等。

类其实就是一个模板,类中描述的是所有对象的“共同特征”。

对象:对象是一个具体的概念,在现实世界中真实存在的。比如李白、爱迪生、贝克汉姆都是对象,他们都属于人类。对象就是通过类创建出的真实的个体。

2.1 抽象

将具有相同特征的对象抽取共同特征的过程叫做“抽象”。

2.2 实例化

对象还有一个别名叫做“实例”,通过类创建对象的过程叫做“实例化”。

  1. 类的定义

我们都知道类具有相同的特征,特征包含静态的和动态的。

鸟类的静态特征是长着一双翅膀,动态特征是会飞。狗类的静态特征是嗅觉灵敏,动态特征是会“汪汪”叫……

所以,类 = 属性 +方法

3.1 java 定义类的语法格式

1
2
3
4
5
css复制代码[修饰符] class 类名 { 
属性 + 方法
}

注:这里修饰符可以省略,后面会讲解。

案例一:用 java 代码创建人类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class Person {
// 姓名
private String name;
// 年龄
private int age;
// 性别 0-女 1-男
private int sex;
// 身份证
private String idCard;
// 吃饭方法
public void eat(){
System.out.printf("人要吃饭");
}
}

在上面的例子中,属性以“变量”的形式存在。为什么呢?

因为属性是静态特征,属性包含的是数据,比如年龄18岁,性别男,身高180。所以在 java 程序中有关属性的数据只能存在于变量中。

案例二:用 java 代码创建动物类

1
2
3
4
5
6
7
8
9
java复制代码public class Animal {

// 体重
private int weight;

public void sleep(){
System.out.println("动物要睡觉");
}
}

注:在 java 中凡是用 class 定义的类型都是引用类型,他的类型就是类名本身。

3.2 实例变量

实例变量:把对象共同的静态特征抽取出来存放在类中的变量里面,比如人类中的年龄、性别、身高等。

上面类中的变量就是实例变量,因为对象又叫做实例,所以实例变量就是对象级别的变量。

  1. 对象的创建和使用

4.1 创建对象

我们学会定义类之后,该如何创建对象呢?

很简单,new 一下。

语法格式:

1
arduino复制代码类名 变量名 = new 类名();

我们 new 出来的对象也需要用一个变量来接收,例如:

1
2
3
java复制代码Student student = new Student();
People people = new People();
Animal animal = new Animal();

4.2 使用对象

创建对象之后,我们怎样使用对象?怎样获取对象的属性?怎样访问对象的方法?

对象.属性 对象.方法

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复制代码public class Person {
// 姓名
private String name;
// 年龄
private int age;
// 性别 0-女 1-男
private int sex;
// 身份证
private String idCard;
// 吃饭方法
public void eat(){
System.out.printf("人要吃饭");
}

public static void main(String[] args) {
Person person1 = new Person();
System.out.println("1号人类姓名:"+person1.name);
System.out.println("1号人类年龄:"+person1.age);
System.out.println("1号人类身份证:"+person1.idCard);
person1.eat();
System.out.println("-----------------------");
Person person2 = new Person();
System.out.println("2号人类姓名:"+person2.name);
System.out.println("2号人类年龄:"+person2.age);
System.out.println("2号人类身份证:"+person2.idCard);
person2.eat();
}
}

运行结果:

我们都知道一个类可以创建很多对象,但是上面人类创建的对象的年龄为什么是0?身份证为什么是null?

这是因为在 java 中,我们在创建对象的时候如果没有给变量手动赋值,系统会对实例变量默认赋值。默认值如下所示:

数据类型 默认值
byte 0
short 0
int 0
long 0L
float 0.0f
double 0.0
引用类型 null
  1. 画个内存图

为什么要学习 JVM 内存图?

因为 JVM 内存图可以加深你对 Java 运行机制的理解。

Java 虚拟机在运行 java 程序时,会将自己管理的内存划分为几个区域,每个区域都有自己的用途,他们的创建时间和销毁时间也不一样。

JVM 内存很复杂,这里我们主要关注三个区域:栈、堆、方法区。

栈:主要存放方法信息,比如 main 方法。

堆:主要存放对象实例,你可以想象成这里“堆满了对象”。

方法区:这里主要用来存储类的信息、静态变量、常量以及编译器编译后的代码。

前面的例子中,我们创建了两个 Person 类的对象 person1 和 person2。

person1 和 person2 实际上存储的是堆中对象的内存地址:ox00001 和 ox00002,所以他们分别指向了堆中的两个对象。

所以当我们访问对象的实例变量时,先根据对象变量存储的内存地址找到该对象,再获取该对象的实例变量存储的数据。

  1. 空指针异常

有时候我们会遇到空指针异常,例如

1
2
3
4
5
6
7
8
java复制代码public class Student {
// 姓名
private String name;
public static void main(String[] args) {
Student student = null;
System.out.println("学生姓名:"+student.name);
}
}

运行结果:

为什么会有空指针异常?通过下面的内存图就能明白:

你新建了空的对象变量 student,它里面没有存任何对象的内存地址。所以当你去访问它的实例变量时,它找不到堆中的对象,就会抛出空指针异常。

  1. 构造方法

构造方法是啥?通过“构造”的字面意思隐隐约约感觉它是造东西用的。可是造啥呢?

对,造对象用的。

当你 new 对象的时候是通过调用构造方法来完成对象的创建,以及对象属性的初始化操作。

也就是说在创建对象之前会先调用构造方法。

注:

    1. 当类中没有提供任何构造方法,系统默认提供一个无参数的构造方法。
    1. 当类中手动的提供了构造方法,那么系统将不再默认提供无参数构造方法。

7.1 定义构造方法

语法格式:

1
2
3
ini复制代码[修饰符列表] 构造方法名(形式参数列表){
方法体;
}

注:

  1. 构造方法名和类名一致
  2. 构造方法没有返回值
  3. 一个类中可以定义多个构造方法,这些构造方法其实是方法的重载

例如:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class Student {
// 姓名
private String name = "一颗雷布斯";

public Student() {
System.out.println("我是一个没有参数的构造方法");
}

public static void main(String[] args) {
Student student = new Student();
}
}

运行结果:

7.2 定义多个构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class Student {
// 姓名
private String name;

public Student() {
System.out.println("我是构造方法1");
}
public Student(String name) {
System.out.println("我是构造方法2");
}
public static void main(String[] args) {
Student student1 = new Student();
Student student2 = new Student("一颗雷布斯");
}
}

运行结果:

7.3 使用构造方法初始化实例变量

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class Student {
// 姓名
private String name;

public Student() {
System.out.println("我是构造方法1");
}
public Student(String name) {
System.out.println("我是构造方法2");
this.name = name;
}
public static void main(String[] args) {
Student student = new Student("一颗雷布斯");
System.out.println("姓名:"+student.name);
}
}

运行结果:

7.4 this

上面的例子中我们使用了 this 关键字,那么为什么要用 this?

我们先把上面例子中的构造方法改一下:

1
2
3
java复制代码   public Student(String name) {
name = name;
}

看完是不是懵逼了?哪个是我要传递的参数?哪个是对象的实例变量?

所以这里 this 用来区分局部变量和实例变量。

注:

    1. this 是一个关键字,是一个引用,保存了当前对象的内存地址。
    1. this 出现在实例方法中代表的是当前对象。
    1. this 不能使用在静态方法中。
    1. 当用来区分局部变量和实例变量时,this 不能省略。
    1. this 既可以出现在构造方法中,也可以出现在实例方法中。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class Student {
// 姓名
private String name;

public Student(String name) {
this.name = name;
}
public void printName(){
System.out.println(this.name);
}
public static void main(String[] args) {
Student student = new Student("一颗雷布斯");
student.printName();
}
}

运行结果:

  1. 封装

封装是面向对象的三大特征之一,但是什么是封装?为什么要封装?

我们先看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class Student {
String name;
String phone;

public Student(String name, String phone) {
this.name = name;
this.phone = phone;
}
}

public class StudentTest {
public static void main(String[] args) {
Student student = new Student("一颗雷不斯","18888888888");
System.out.println("电话:"+student.phone);
student.phone = "110";
System.out.println("电话:"+student.phone);
}
}

运行结果:

上面的 Student 类没有进行封装,其中的姓名和电话都是对外暴露的,很不安全,很容易就被篡改了。

所以为了保证数据的安全性,需要对类进行封装。

那 java 语言如何做封装?

    1. 属性必须私有化,即使用 private 关键字修饰,只有本类中才能访问。
    1. 对外提供 set 和 get方法。外部程序只能通过调用该对象的 set 方法设置值,调用该对象的 get 方法获取值。
    1. set 和 get 方法的修饰符必须是 public, 也就是公共的,在其他类中也能访问。

封装之后的例子:

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
java复制代码public class Student {
private String name;
private String phone;

public Student(String name, String phone) {
this.name = name;
this.phone = phone;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getPhone() {
return phone;
}

public void setPhone(String phone) {
this.phone = phone;
}
}

发现错误提示:不能访问私有属性

改为通过 get 方法获取值、set 方法设置值:

1
2
3
4
5
6
7
8
java复制代码public class StudentTest {
public static void main(String[] args) {
Student student = new Student("一颗雷不斯","18888888888");
System.out.println("电话:"+student.getPhone());
student.setPhone("119");
System.out.println("电话:"+student.getPhone());
}
}

运行结果:

  1. 面向对象三大特征

  • 封装
  • 继承
  • 多态

这三个特征互相关联,任何一个面向对象的编程语言都包括这三个特征。后面会接着讲解继承和多态。

本文转载自: 掘金

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

1…150151152…956

开发者博客

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