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

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


  • 首页

  • 归档

  • 搜索

存储使者,内存驿站-文件操作 文件操作

发表于 2021-11-28

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

文件操作

什么是文件

  • 磁盘上的文件是文件
  • 但是在程序设计中,我们一般谈的文件有两种:程序文件,数据文件(从文件功能的角度来分类的)

程序文件

包括源程序文件(后缀为.c).目标文件(windows环境后缀为.obj),可执行文件(windows环境后缀为.exe)

数据文件

文件内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据文件,或者输出内容的文件

image-20210927131350241

以前所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上,其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上的文件

文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用

文件名包括3部分:文件路径+文件名主干+文件后缀

image-20210920154954187

文件的打开和关闭

文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称**“文件指针”**。

每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE.

image-20210927140127421

1.不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。

image-20210927140347390

2.每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。

3.一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。

1
2
c复制代码//我们可以创建一个FILE*的指针变量
FILE* pf;//文件指针变量

定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。

image-20210927141717048

文件的打开和关闭

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。

ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件

1
2
3
4
c复制代码//打开文件
FILE* fopen(const char* filename,const char* mode);
//关闭文件
int fclose(FILE* stream)

image-20210927143044457

打开方式
文件使用方式 含义 如果指定文件不存在
“r”(只读) **为了输入数据,打开一个已经存在的文本文件 ** 出错
**“w”(只写) ** **为了输出数据,打开一个文本文件 ** 建立一个新的文件
**“a”(追加) ** **向文本文件尾添加数据 ** 建立一个新的文件
“rb”(只读) 为了输入数据,打开一个二进制文件 出错
“wb”(只写) 为了输出数据,打开一个二进制文件 建立一个新的文件
“ab”(追加) 向一个二进制文件尾添加数据 出错
“r+”(读写) 为了读和写,打开一个文本文件 出错
“w+”(读写) 为了读和写,建议一个新的文件 建立一个新的文件
“a+”(读写) 打开一个文件,在文件尾进行读写 建立一个新的文件
“rb+”(读写) 为了读和写打开一个二进制文件 出错
“wb+”(读写) 为了读和写,新建一个新的二进制文件 建立一个新的文件
“ab+”(读写) 打开一个二进制文件,在文件尾进行读和写 建立一个新的文件

我们主要用前三个

image-20210927150002640

image-20210927150925101

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c复制代码#include<stdio.h>

int main()
{
//打开文件
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("打开失败.\n");
perror("fopen");//把错误信息打印到fopen:后面
return 0;
}
//读文件
else
{
printf("打开成功\n");
//关闭文件
fclose(pf);
pf = NULL;
}
return 0;
}
在程序目录下面没有data.txt文件

image-20210927151710788

在程序目录下面有data.txt文件

image-20210927152010218

data放在桌面

image-20210927152650915

那就必须写完整的文件名了

image-20210927153228625

同理写文件

看上面的打开方式可以知道没有文件会新建一个文件,我们在桌面上试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c复制代码#include<stdio.h>

int main()
{
//打开文件
FILE* pf = fopen("C:\\Users\\Administrator\\Desktop\\data.txt", "w");
if (pf == NULL)
{
printf("打开失败.\n");
perror("fopen");//把错误信息打印到fopen:后面
return 0;
}
//写文件
else
{
printf("打开成功\n");
//关闭文件
fclose(pf);
pf = NULL;
}
return 0;
}

image-20210927154402456

注意这个w也不是谁便用的

假如已存在的文件里面有内容,w就是把文件里的内容全部销毁了,变成一个空白文件

image-20210927155758744

文件的顺序读写

image-20210927160334190

fputc写字符函数

image-20210927202304278

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
c复制代码#include<stdio.h>

int main()
{
//打开文件
FILE* pf = fopen("C:\\Users\\Administrator\\Desktop\\data.txt", "w");
if (pf == NULL)
{
printf("打开失败.\n");
perror("fopen");//把错误信息打印到fopen:后面
return 0;
}
//写文件
else
{
fputc('b', pf);
fputc('i', pf);
fputc('t', pf);
//关闭文件
fclose(pf);
pf = NULL;
}
return 0;
}

image-20210927162749323

流

image-20210927165823966

image-20210927170418132

那他适用所有输出流是什么意思呢

image-20210927170532790

是因为他不仅仅可以打印到文件里面去,也可以打印到屏幕里面去

1
2
3
4
5
6
7
8
9
c复制代码#include<stdio.h>

int main()
{
fputc('b', stdout);
fputc('i', stdout);
fputc('t', stdout);
return 0;
}

image-20210927171118486

fgetc读字符函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c复制代码#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("C:\\Users\\Administrator\\Desktop\\data.txt", "r");
if (pf == NULL)
{
perror("fopen");
}
else
{
//读文件
int ch = fgetc(pf);
printf("%c",ch);
ch = fgetc(pf);
printf("%c", ch);
//关闭文件
fclose(pf);
pf = NULL;
}
return 0;
}

image-20210927180055768

同样他适用所有输入流

image-20210927180439618

1
2
3
4
5
6
7
8
9
10
11
12
13
c复制代码#include<stdio.h>

int main()
{
int ch = fgetc(stdin);
printf("%c\n", ch);
ch = fgetc(stdin);
printf("%c\n", ch);
ch = fgetc(stdin);
printf("%c\n", ch);

return 0;
}

image-20210927201044532

注意

fputc(‘b’, stdout)这个函数等价于putchar(‘b’)函数

fgetc(stdin)这个函数等价于getchar函数

fputs写字符串函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
c复制代码#include<stdio.h>

int main()
{
//打开文件
FILE* pf = fopen("C:\\Users\\Administrator\\Desktop\\data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 0;
}
else
{
//写一行数据
fputs("hello bit", pf);
fputs("hello bit\n", pf);
fputs("hello bit", pf);
//关闭文件
fclose(pf);
pf = NULL;
}
return 0;
}

image-20210927205018496

同样适用所有输出流

image-20210927204030540

1
2
3
4
5
6
7
8
9
c复制代码#include<stdio.h>

int main()
{
fputs("hello bit",stdout);
fputs("hello bit\n",stdout);
fputs("hello bit",stdout);
return 0;
}

image-20210927214349880

fgets读字符串函数

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
c复制代码#include<stdio.h>

int main()
{
//打开文件
FILE* pf = fopen("C:\\Users\\Administrator\\Desktop\\data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 0;
}
else
{
//读一行数据
char arr[20] = { 0 };
fgets(arr, 5, pf);
printf("%s\n",arr);
fgets(arr, 5, pf);
printf("%s\n", arr);
//关闭文件
fclose(pf);
pf = NULL;
}
return 0;
}

image-20210927223709225

同理适应所有输入流

image-20210927223852648

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c复制代码#include<stdio.h>

int main()
{
char arr[20] = { 0 };
fgets(arr, 5, stdin);
printf("%s\n", arr);
fgets(arr, 5, stdin);
printf("%s\n", arr);
fgets(arr, 5, stdin);
printf("%s\n", arr);

return 0;
}

image-20210927224709150

fprintf格式化输出函数

image-20210928070048481

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
c复制代码#include<stdio.h>

struct MyStruct
{
int a;
double b;
};

int main()
{
struct MyStruct mystruct = { 20 ,3.14 };
//打开文件
FILE* pf = fopen("C:\\Users\\Administrator\\Desktop\\data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 0;
}
else
{
//写数据
fprintf(pf, "%d %lf", mystruct.a, mystruct.b);
//关闭文件
fclose(pf);
pf = NULL;
}
return 0;
}

image-20210928071302798

同理他适用所有输出流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c复制代码#include<stdio.h>

struct S
{
int a;
double b;
char c;
};

int main()
{
struct S s = { 10,3.25,'a' };
fprintf(stdout, "%d %lf %c", s.a, s.b, s.c);
return 0;
}

image-20210928071951383

fscanf格式化输入函数

image-20210928072440768

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
c复制代码#include<stdio.h>

struct MyStruct
{
int a;
double b;
};

int main()
{
struct MyStruct mystruct = {0};
//打开文件
FILE* pf = fopen("C:\\Users\\Administrator\\Desktop\\data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 0;
}
else
{
//读数据
fscanf(pf, "%d%lf", &(mystruct.a), &(mystruct.b));
printf("%d %lf", mystruct.a, mystruct.b);
//关闭文件
fclose(pf);
pf = NULL;
}
return 0;
}

image-20210928074336753

同理适用所有输入流
1
2
3
4
5
6
7
8
9
10
11
12
13
c复制代码#include<stdio.h>
struct S
{
int a;
double b;
};
int main()
{
struct S s = { 0 };
fscanf(stdin, "%d%lf", &(s.a), &(s.b));
printf("%d %lf", s.a, s.b);
return 0;
}

image-20210928075049270

fwrite二进制输出函数

image-20210928082312323

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
c复制代码#include<stdio.h>

struct MyStruct
{
int a;
double b;
char num[20];
};

int main()
{
struct MyStruct mystruct = {20,3.14,"zhuzhu"};
//打开文件
FILE* pf = fopen("C:\\Users\\Administrator\\Desktop\\data.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 0;
}
else
{
//写文件--二进制的形式写
fwrite(&mystruct, sizeof(struct MyStruct), 1, pf);
//关闭文件
fclose(pf);
pf = NULL;
}
return 0;
}

image-20210928082816543

抱歉的是他只适用于文件流

image-20210928082912972

fread二进制输入

image-20210928083537933

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
c复制代码#include<stdio.h>

struct MyStruct
{
int a;
double b;
char num[20];
};

int main()
{
struct MyStruct mystruct = {0};
//打开文件
FILE* pf = fopen("C:\\Users\\Administrator\\Desktop\\data.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 0;
}
else
{
//读文件--二进制的形式读
fread(&mystruct, sizeof(struct MyStruct), 1, pf);
printf("%d %lf %s", mystruct.a, mystruct.b, mystruct.num);
//关闭文件
fclose(pf);
pf = NULL;
}
return 0;
}

image-20210928084402528

和fwrite一样他只适用于文件流

image-20210928084710001

对比一组函数

scanf/fscanf/sscanf

image-20210928093439868

sscanf

image-20210928092028652

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c复制代码#include<stdio.h>

struct MyStruct
{
int a;
double b;
char num[20];
};

int main()
{
char arr[20] = { 0 };
struct MyStruct mystruct = {20,3.14,"zhuzhu"};
struct MyStruct tmp = { 0 };
sprintf(arr, "%d %lf %s", mystruct.a, mystruct.b, mystruct.num);
printf("%s",arr);
sscanf(arr, "%d%lf%s", &(tmp.a), &(tmp.b), &(tmp.num));
printf("\n%d\n%lf\n%s\n", tmp.a, tmp.b, tmp.num);
return 0;
}

image-20210928093011839

printf/fprintf/sprintf

image-20210928092201832

sprintf

image-20210928092014648

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c复制代码#include<stdio.h>

struct MyStruct
{
int a;
double b;
char num[20];
};

int main()
{
char arr[20] = { 0 };
struct MyStruct mystruct = {20,3.14,"zhuzhu"};
sprintf(arr, "%d %lf %s", mystruct.a, mystruct.b, mystruct.num);
printf("%s",arr);
return 0;
}

image-20210928091850876

文件的随机读写

fseek

根据文件指针的位置和偏移量来定位文件指针。

image-20210928151756202

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
c复制代码#include<stdio.h>

int main()
{
//打开文件
FILE* pf = fopen("C:\\Users\\Administrator\\Desktop\\data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 0;
}
else
{
//随机读文件
//文件开头向后偏移2
fseek(pf, 2, SEEK_SET);
int ch = fgetc(pf);
printf("%c\n",ch);

//文件当前指针向前移2
fseek(pf, -2, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);

}
return 0;
}

image-20210928152602313

ftell

根据文件指针的位置和偏移量来定位文件指针。

1
2
c复制代码int a = ftell(pf);
printf("%d\n", a);

image-20210928154051054

rewind

让文件指针的位置回到文件的起始位置

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
c复制代码#include<stdio.h>

int main()
{
//打开文件
FILE* pf = fopen("C:\\Users\\Administrator\\Desktop\\data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 0;
}
else
{
//随机读文件
//文件开头向后偏移2
fseek(pf, 2, SEEK_SET);
int ch = fgetc(pf);
printf("%c\n",ch);

//文件当前指针向前移2
fseek(pf, -2, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);

int a = ftell(pf);
printf("%d\n", a);

rewind(pf);
ch = fgetc(pf);
printf("%c\n", ch);
}
return 0;
}

image-20210928154709457

文本文件和二进制文件

根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件

一个数据在内存中是怎么存储的呢?

字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。

image-20210928170707226

我们把10000数据以二进制的形式写到data文件中,并看一下

image-20210928170934248

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c复制代码#include<stdio.h>

int main()
{
int a = 10000;
FILE* pf = fopen("data.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 0;
}
else
{
//把a写入
fwrite(&a, sizeof(int), 1, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}

文件读取结束的判定

fgetc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c复制代码#include<stdio.h>

int main()
{
int ch = 0;
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 0;
}
while ((ch = fgetc(pf))!=EOF)
{
printf("%c ", ch);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}

image-20210928173952985

被错误使用的feof

feof 是文件读取结束了,判断是不是遇到文件末尾而结束的

ferror 文件读取结束了,判断是不是遇到错误后读取结束

牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。

  1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
    例如:
    fgetc 判断是否为 EOF .
    fgets 判断返回值是否为 NULL .
  2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
    例如:
    fread判断返回值是否小于实际要读的个数

文件缓冲区

ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
c复制代码#include <stdio.h>
#include <windows.h>
//VS2013 WIN10环境测试
int main()
{
FILE*pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
//注:fflush 在高版本的VS上不能使用了
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
这里可以得出一个结论:

因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写文件的问题

钱有价,代码无价

我很希望来个01年中美黑客大战,剑指美国,码到成功,体现价值。

本文转载自: 掘金

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

Thymeleaf 模版引擎 Thymeleaf 的基本用法

发表于 2021-11-28

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

Thymeleaf 的基本用法

属于个人整理的文档,大部分内容来源自网络

在这里我们没有打算使用SpringMVC进行整合使用或者说跟Spring Boot 一起使用

我们在这里单独使用Servelet版本-算是为了给一些初学者提供部分代码

Thymeleaf是一款用于渲染XML/XHTML/HTML5内容的模板引擎,类似JSP,Velocity,FreeMaker等,它也可以轻易的与Spring MVC等Web框架进行集成作为Web应用的模板引擎。

Thymeleaf最大的特点是能够直接在浏览器中打开并正确显示模板页面,而不需要启动整个Web应用,但是总是看到说其效率有点低

1
2
html复制代码Thymeleaf 在有网络和无网络的环境下皆可运行,即它可以让美工在浏览器查看页面的静态效果,也可以让程序员在服务器查看带数据的动态页面效果。这是由于它支持 html 原型,然后在 html 标签里增加额外的属性来达到模板+数据的展示方式。浏览器解释 html 时会忽略未定义的标签属性,所以 thymeleaf 的模板可以静态地运行;当有数据返回到页面时,Thymeleaf 标签会动态地替换掉静态内容,使页面动态显示。
Thymeleaf 开箱即用的特性。它提供标准和spring标准两种方言,可以直接套用模板实现JSTL、 OGNL表达式效果,避免每天套模板、改jstl、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。

1.引入提示

在html页面中引入thymeleaf命名空间,即,此时在html模板文件中动态的属性使用th:命名空间修饰 。

1
html复制代码<html lang="en" xmlns:th="http://www.thymeleaf.org">

这样才可以在其他标签里面使用th:*这样的语法.这是下面语法的前提.

2.变量表达式(获取变量值)

1
2
3
4
html复制代码<div th:text="'你是否读过,'+${session.book}+'!!'">
同EL表达式有些相似的效果,如果有数据,被替换
完成前后端分离效果(美工代码)
</div>
1
2
3
4
5
html复制代码代码分析:
1.可以看出获取变量值用$符号,对于javaBean的话使用变量名.属性名方式获取,这点和EL表达式一样
2.它通过标签中的th:text属性来填充该标签的一段内容,意思是$表达式只能写在th标签内部,不然不会生效,上面例子就是使用th:text标签的值替换div标签里面的值,至于div里面的原有的值只是为了给前端开发时做展示用的.这样的话很好的做到了前后端分离.意味着div标签中的内容会被表达式${session.book}的值所替代,无论模板中它的内容是什么,之所以在模板中“多此一举“地填充它的内容,完全是为了它能够作为原型在浏览器中直接显示出来。
3.访问spring-mvc中model的属性,语法格式为“${}”,如${user.id}可以获取model里的user对象的id属性 
4.牛叉的循环<li th:each="book : ${books}" >

3.URL表达式(引入URL)

重点!重点!重点!

  • 引用静态资源文件(CSS使用th:href,js使用使用th:src)

  • href链接URL(使用th:href)

1
2
3
4
5
6
7
8
9
10
11
12
html复制代码代码分析
1.最终解析的href为:    
/seconddemo/    
/seconddemo/usethymeleaf?name=Dear 相对路径,带一个参数   
/seconddemo/usethymeleaf?name=Dear&alis=Dear 相对路径,带多个参数
/seconddemo/usethymeleaf?name=Dear&alis=Dear 相对路径,带多个参数
/seconddemo/usethymeleaf/Dear 相对路径,替换URL一个变量
/seconddemo/usethymeleaf/Dear/Dear 相对路径,替换URL多个变量
2.URL最后的(name=${name})表示将括号内的内容作为URL参数处理,该语法避免使用字符串拼接,大大提高了可读性
3.@{/usethymeleaf}是Context相关的相对路径,在渲染时会自动添加上当前Web应用的Context名字,假设context名字为seconddemo,那么结果应该是/seconddemo/usethymeleaf,即URL中以”/“开头的路径(比如/usethymeleaf将会加上服务器地址和域名和应用cotextpath,形成完整的URL。
4.th:href属性修饰符:它将计算并替换使用href链接URL 值,并放入的href属性中。
5.th:href中可以直接使用静态地址

4.选择或星号表达式

表达式很像变量表达式,不过它们用一个预先选择的对象来代替上下文变量容器(map)来执行*{customer.name}

1
2
3
4
5
6
7
8
9
10
11
12
html复制代码<div th:object="${session.user}">
<p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>

//等价于
<div>
<p>Name: <span th:text="${session.user.firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="${session.user.nationality}">Saturn</span>.</p>
</div>
1
2
html复制代码1.如果不考虑上下文的情况下,两者没有区别;星号语法评估在选定对象上表达,而不是整个上下文,什么是选定对象?就是父标签的值。上面的*{title}表达式可以理解为${book.title}。(父对象)  
2.当然,美元符号和星号语法可以混合使用

小插曲:三和四的变量表达式、URL表达式所对应的属性都可以使用统一的方式:th.attr=“属性名=属性值”的方式来设置,参考第“七.设置属性值”部分

5.文字国际化表达式

j简单看一下就可以,文字国际化表达式允许我们从一个外部文件获取区域文字信息(.properties),用Key索引Value,还可以提供一组参数(可选).

1
2
3
4
5
6
html复制代码#{main.title}    
#{message.entrycreated(${entryId})} 可以在模板文件中找到这样的表达式代码:    
<table>
<th th:text="#{header.address.city}">
<th th:text="#{header.address.country}">
</table>
  1. 表达式支持的语法

  • 字面量(Literals)
+ 文本文字(Text literals): 'one text', 'Another one!',…
+ 数字文本(Number literals): 0, 34, 3.0, 12.3,…
+ 布尔文本(Boolean literals): true, false
+ 空(Null literal): null
+ 文字标记(Literal tokens): one , sometext
  • 文本操作(Text operations)
+ 字符串连接(String concatenation): `+`
+ 文本替换(Literal substitutions): `|The name is ${name}|`



1
2
3
4
5
html复制代码<div th:class="'content'">...</div>
<span th:text="|Welcome to our application, ${user.name}!|">
//Which is equivalent to:
<span th:text="'Welcome to our application, ' + ${user.name} + '!'">
<span th:text="${onevar} + ' ' + |${twovar}, ${threevar}|">
  • 算术运算(Arithmetic operations)
+ 二元运算符(Binary operators): + , - , \* , / , %
+ 减号(Minus sign (unary operator)): -
  • 布尔操作(Boolean operations)
+ Binary operators: and , or
+ Boolean negation (unary operator): ! , not
  • 比较和等价(Comparisons and equality)
+ Comparators: > , < , >= , <= ( gt , lt , ge , le )
+ Equality operators: == , != ( eq , ne )
  • 条件运算符(Conditional operators)三元运算符
+ If-then: (if) ? (then)
+ If-then-else: (if) ? (then) : (else)
+ Default: (value) ?: (defaultvalue)
1
2
3
4
5
6
7
8
9
10
11
html复制代码示例一:    
<h2 th:text="${expression} ? 'Hello' : 'Something else'"></h2>
示例二:       
<!-- IF CUSTOMER IS ANONYMOUS -->    
<div th:if="${customer.anonymous}">        
<div>Welcome, Gues!</div>    
</div>    
<!-- ELSE -->    
<div th:unless="${customer.anonymous}">        
<div th:text=" 'Hi,' + ${customer.name}">Hi, User</div>    
</div>
  • Special tokens:
+ No-Operation: \_
  • switch

  • 循环

渲染列表数据是一种非常常见的场景,例如现在有n条记录需要渲染成一个表格或li列表标签该数据集合必须是可以遍历的,使用th:each标签

1
2
html复制代码代码分析:
循环,在html的标签中,加入th:each=“value:${list}”形式的属性,如可以迭代prods的数据 又如带状态变量的循环:

​ 利用状态变量判断:

7.设置属性值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
html复制代码1. th:attr 
任何属性值,语法格式:th:attr="属性名=属性值,[属性名=属性值]" 
属性值如果是使用表达式的话:通常有URL表达式@{}和变量表达式${}       
但此标签语法不太优雅   
示例:        
th:attr="action=@{/subscribe}" //当然也可以直接使用th:action        
th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" //可直接使用th:src 
th:attr="value=#{subscribe.submit}"//可直接使用th:value       
<input type="checkbox" name="active" th:attr="checked=${user.active}"/>        
设置多个属性在同一时间,有两个特殊的属性可以这样设置: 
th:alt-title 和 th:lang-xmllang        
th:src="@{/images/gtvglogo.png}" th:alt-title="#{logo}"     
2.前置和后置添加属性值  
th:attrappend 和 th:attrprepend       
主要对class和style两个属性        
class="btn" th:attrappend="class=${' ' + cssStyle}"       
转换后:class="btn warning"     
3.还有两个特定的添加属性 
th:classappend 和 th:styleappend       
与上面的attrappend功能一样        
class="row" th:classappend="${prodStat.odd}? 'odd'"         
转换后:奇数行class="row odd",偶数行class="row"

8.内嵌变量Utilities

为了模板更加易用,Thymeleaf还提供了一系列Utility对象(内置于Context中),可以通过#直接访问。

1
2
3
4
5
6
7
8
9
10
properties复制代码dates : java.util.Date的功能方法类    
calendars : 类似#dates,面向java.util.Calendar    
numbers : 格式化数字的功能方法类    
strings : 字符串对象的功能类,contains,startWiths,prepending/appending等等    
objects: 对objects的功能类操作    
bools: 对布尔值求值的功能方法    
arrays:对数组的功能类方法    
lists: 对lists功能类方法    
sets    
maps
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
html复制代码代码示例:    
${#dates.format(dateVar, 'dd/MMM/yyyy HH:mm')}    
${#dates.arrayFormat(datesArray, 'dd/MMM/yyyy HH:mm')}    
${#dates.listFormat(datesList, 'dd/MMM/yyyy HH:mm')}    
${#dates.setFormat(datesSet, 'dd/MMM/yyyy HH:mm')}    
${#dates.createNow()}    
${#dates.createToday()}    
${#strings.isEmpty(name)}    
${#strings.arrayIsEmpty(nameArr)}    
${#strings.listIsEmpty(nameList)}    
${#strings.setIsEmpty(nameSet)}    
${#strings.startsWith(name,'Don')}
// also array*, list* and set*    
${#strings.endsWith(name,endingFragment)}
// also array*, list* and set*    
${#strings.length(str)}     
${#strings.equals(str)}    
${#strings.equalsIgnoreCase(str)}    
${#strings.concat(str)}    
${#strings.concatReplaceNulls(str)}    
${#strings.randomAlphanumeric(count)}//产生随机字符串

9.thymeleaf布局

10.附录

thymeleaf_3.0.5_中文参考手册 提取码:emk0

本文转载自: 掘金

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

java线程池之ScheduledThreadPoolExe

发表于 2021-11-28

java中异步周期任务调度有Timer,ScheduledThreadPoolExecutor等实现,目前单机版的定时调度都是使用ScheduledThreadPoolExecutor去实现,那么它是如何实现周期执行任务的呢?其实它还是利用ThreadPoolExecutor线程池去执行任务,这一点从它是继承自ThreadPoolExecutor救可以看的出来,其实关键在于如何实现任务的周期性调度,

ScheduledThreadPoolExecutor类以及核心函数

首先ScheduledThreadPoolExecutor是实现ScheduledExecutorService接口,它主要定义了四个方法:

  • 周期调度一个Runnable的对象
  • 周期调度一个Callable的对象
  • 固定周期调度Runnable对象 (不管上一次Runnable执行结束的时间,总是以固定延迟时间执行 即 上一个Runnable执行开始时候 + 延时时间 = 下一个Runnable执行的时间点)
  • 以固定延迟调度unnable对象(当上一个Runnable执行结束后+固定延迟 = 下一个Runnable执行的时间点)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arduino复制代码public interface ScheduledExecutorService extends ExecutorService {
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);

public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
}

其次,ScheduledThreadPoolExecutor是继承ThreadPoolExecutor,所以它是借助线程池的能力去执行任务,然后自身去实现周期性调度。从构造方法调用父类的线程池的构造方法,核心线程数是构造方法传入,这里可以看到最大线程数是Integer的最大值即2147483647, 还有等待队列是DelayedWorkQueue,它是实现延时的关键.

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码/**
* Creates a new {@code ScheduledThreadPoolExecutor} with the
* given core pool size.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @throws IllegalArgumentException if {@code corePoolSize < 0}
*/
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}

scheduleAtFixedRate是实现周期性调度的方法,调度任务就是实现Runnable对象,
以及系统的开始延时时间,周期的调度的间隔时间。

  1. 计算初始触发时间和执行周期,并和传入的Runnable对象作为参数封装成 ScheduledFutureTask,然后调用decorateTask装饰Tas(默认实现为空)。
  2. 设置ScheduledFutureTask对象outerTask为t(默认就是它自己)。
  3. 调用delayedExecute延迟执行任务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
** TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (period <= 0)
throw new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(init ialDelay, unit),
unit.toNanos(period));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
  1. 判断线程池状态,如果不是处于running状态,则拒绝该任务。
  2. 将该任务加入父类的延迟队列(实际为初始化的DelayedWorkQueue对象)
  3. 再次判断线程池不是处于running状态,并且,判断是否是处于shutdown状态并且continueExistingPeriodicTasksAfterShutdown标志是否是true(默认是false,表示是否线程次处于shutdown状态下是否继续执行周期性任务),若果为true,则从队列删除任务,false,则确保启动线程来执行周期性任务
1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码private void delayedExecute(RunnableScheduledFuture<?> task) {
if (isShutdown())
reject(task);
else {
super.getQueue().add(task);
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
task.cancel(false);
else
ensurePrestart();
}
}
  1. 获取线程池数量
  2. 如果小于核心线程数,则启动核心线程执行任务,如果线程数为空,则启动非核心线程
1
2
3
4
5
6
7
csharp复制代码void ensurePrestart() {
int wc = workerCountOf(ctl.get());
if (wc < corePoolSize)
addWorker(null, true);
else if (wc == 0)
addWorker(null, false);
}

ScheduledFutureTask的run函数

  1. 获取是否是周期性任务
  2. 判断是否线程池状态是否可以执行任务,如果为true,则取消任务
    3 如果是非周期性任务,则直接调用父类FutureTask的run方法,
    4 如果是周期性任务,则调用FutureTask的runAndReset函数,
    如果该函数返回为true,则调用setNextRunTime设置下一次运行的时间,
    并且还行reExecutePeriodic再次执行周期性任务。
1
2
3
4
5
6
7
8
9
10
11
scss复制代码public void run() {
boolean periodic = isPeriodic();
if (!canRunInCurrentRunState(periodic))
cancel(false);
else if (!periodic)
ScheduledFutureTask.super.run();
else if (ScheduledFutureTask.super.runAndReset()) {
setNextRunTime();
reExecutePeriodic(outerTask);
}
}
  1. 判断线程池是否处于可执行任务的状态,如果为true,则重新将设置下一次运行时间的任务加入父类的等待队列,
  2. 如果线程池处于不可运行任务的状态,则并且从等待队列中移除成功,
    调用任务的取消操作,否则调用ensurePrestart确保启动线程执行任务
1
2
3
4
5
6
7
8
9
scss复制代码void reExecutePeriodic(RunnableScheduledFuture<?> task) {
if (canRunInCurrentRunState(true)) {
super.getQueue().add(task);
if (!canRunInCurrentRunState(true) && remove(task))
task.cancel(false);
else
ensurePrestart();
}
}

DelayedWorkQueue类核心函数

DelayedWorkQueue是继承AbstractQueue,并实现BlockingQueue接口

1
2
scala复制代码static class DelayedWorkQueue extends AbstractQueue<Runnable>
implements BlockingQueue<Runnable> {

核心字段

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// 初始容量为16
private static final int INITIAL_CAPACITY = 16;
// 等待队列,只能保存RunnableScheduledFuture对象
private RunnableScheduledFuture<?>[] queue =
new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
// 锁
private final ReentrantLock lock = new ReentrantLock();
//对俄大小
private int size = 0;
// leader线程,表示最近需要执行的任务的线程。
private Thread leader = null;
// 条件锁
private final Condition available = lock.newCondition();

offer函数:

  1. 将添加的参数转换成RunnableScheduledFuture对象。
  2. 加全局锁。
  3. 获取当前队列的size,如果等于队列的长度,则嗲用grow扩容,增加50%的数组长度。
  4. size加1。
  5. 如果数组为0,则将加入的对象放在索引为0的位置, 然后设置ScheduledFutureTask的heapIndex的索引(便于后续快速删除)。
  6. 调用siftUp做堆的上浮操作,这里是小根堆的操作。
  7. 如果队列中第一个元素是传入的对象,则将laader设置null
  8. 释放锁
  9. 返回true
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
ini复制代码public boolean offer(Runnable x) {
if (x == null)
throw new NullPointerException();
RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
final ReentrantLock lock = this.lock;
lock.lock();
try {
int i = size;
if (i >= queue.length)
grow();
size = i + 1;
if (i == 0) {
queue[0] = e;
setIndex(e, 0);
} else {
siftUp(i, e);
}
if (queue[0] == e) {
leader = null;
available.signal();
}
} finally {
lock.unlock();
}
return true;
}

siftUp主要就是做小根堆的上移操作,从if (key.compareTo(e) >= 0) 看出,如果key大于parent索引的元素,则停止。

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码private void siftUp(int k, RunnableScheduledFuture<?> key) {
while (k > 0) {
int parent = (k - 1) >>> 1;
RunnableScheduledFuture<?> e = queue[parent];
if (key.compareTo(e) >= 0)
break;
queue[k] = e;
setIndex(e, k);
k = parent;
}
queue[k] = key;
setIndex(key, k);
}

poll函数

  1. 加锁
  2. 获取队列中索引为0的云元素,若果为null或者第一个元素的执行时间戳时间大于当前时间则直接返回null,否则调用finishPoll将第一个元素返回.
  3. 释放锁
1
2
3
4
5
6
7
8
9
10
11
12
13
csharp复制代码public RunnableScheduledFuture<?> poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
RunnableScheduledFuture<?> first = queue[0];
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
return finishPoll(first);
} finally {
lock.unlock();
}
}
  1. 将队列size 减 1
  2. 获取队列中队列中最后一个元素,并且设置队列最后一个为null
  3. 最后一个元素不为null,则调用sfitdown进行,将最后一个元素设置到索引为0的位置,将下移操作,重新调整小根堆。
  4. ScheduledFutureTask的heapIndex为-1
1
2
3
4
5
6
7
8
9
ini复制代码private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {
int s = --size;
RunnableScheduledFuture<?> x = queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
setIndex(f, -1);
return f;
}

ScheduledFutureTask的compareTo函数

ScheduledFutureTask实现compareTo方法逻辑

  1. 首先比较是否是同一个对象
  2. 若果是ScheduledFutureTask对象,则比较time的大小,time是下一次执行的任务的时间戳,如果不是,则比较
    getDelay的时间大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码public int compareTo(Delayed other) {
if (other == this) // compare zero if same object
return 0;
if (other instanceof ScheduledFutureTask) {
ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
long diff = time - x.time;
if (diff < 0)
return -1;
else if (diff > 0)
return 1;
else if (sequenceNumber < x.sequenceNumber)
return -1;
else
return 1;
}
long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
}

ScheduledThreadPoolExecutor的take函数就是ThreadPoolExecutor的从任务队列中获取任务,没有任务则一直等待(这里是线程数小于核心线程数的情况)

  1. 加可中断锁
  2. 获取队列中第一个元素的任务,从前面可以知道此任务执行的时间戳最小的任务
  3. 如果第一个任务为空,则再全局的锁的条件锁上等待,
  4. 如果第一个任务不为空,则获取延迟时间,如果延时时间小于0,说明第一个任务已经到时间了,则返回第一个任务。
  5. 如果leader线程不为空,则让线程在全局锁的条件锁上等待
  6. 如果leader为空,则将获取第一个任务的当前线程赋值为leader变量。
  7. 在全局锁的条件锁上等待delay纳秒, 等待结束后,如果当前线程还是等于leader线程,则重置leader为空
  8. 最后判断 leader为空并且第一个任务不为空,则唤醒全局锁上条件锁的等待的线程。
  9. 释放全局锁。
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
ini复制代码public RunnableScheduledFuture<?> take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
RunnableScheduledFuture<?> first = queue[0];
if (first == null)
available.await();
else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
return finishPoll(first);
first = null; // don't retain ref while waiting
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && queue[0] != null)
available.signal();
lock.unlock();
}
}

总结

综合前面所述,线程池从DelayedWorkQueue每次取出的任务就是延迟时间最小的任务, 若果到达时间的任务,则执行任务,否则则用条件锁Conditon的wait进行等待,执行完后,则用signal进行唤醒下一个任务的执行。

本文转载自: 掘金

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

在SpringBoot中优雅地实现策略模式

发表于 2021-11-28

策略模式的简单实现

首先定义一个Strategy接口来表示一个策略:

1
2
3
4
java复制代码public interface Strategy {
String flag();
void process();
}

其中flag方法返回当前策略的唯一标识,process则是该策略的具体执行逻辑。

下面是Strategy接口的两个实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public class StrategyImpl1 implements Strategy {
@Override
public String flag() {
return "s1";
}

@Override
public void process() {
System.out.println("strategy 1");
}
}

public class StrategyImpl2 implements Strategy {
@Override
public String flag() {
return "s2";
}

@Override
public void process() {
System.out.println("strategy 2");
}
}

然后定义一个StrategyRunner接口用来表示策略的调度器:

1
2
3
java复制代码public interface StrategyRunner {
void run(String flag);
}

run方法内部通过判断flag的值来决定具体执行哪一个策略。

下面是一个简单的StrategyRunner:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class StrategyRunnerImpl implements StrategyRunner {
private static final List<Strategy> STRATEGIES = Arrays.asList(new StrategyImpl1(), new StrategyImpl2());
private static final Map<String, Strategy> STRATEGY_MAP;

static {
STRATEGY_MAP = STRATEGIES.stream()
.collect(Collectors.toMap(Strategy::flag, s -> s));
}

@Override
public void run(String flag) {
STRATEGY_MAP.get(flag).process();
}
}

在StrategyRunnerImpl内部,定义了一个STRATEGIES列表来保存所有Strategy实现类的实例,以及一个叫做STRATEGY_MAP的Map来保存flag和Strategy实例之间的对应关系,static块中的代码用于从STRATEGIES列表构造STRATEGY_MAP。这样,在run方法中就可以很方便地获取到指定flag的Strategy实例。

这个实现虽然简单,但是它有个很大的缺点,想象一下,如果我们想添加新的Strategy实现类,我们不仅需要添加新的实现类,还要修改STRATEGIES列表的定义。这样就违反了“对扩展开放,对修改关闭”的原则。

在SpringBoot中实现策略模式

借助于Spring的IOC容器和SpringBoot的自动配置,我们可以以一种更加优雅的方式实现上述策略模式。

首先,我们继续使用StrategyImpl1和StrategyImpl2这两个实现类。不过,为了将它们注册进Spring的IOC容器,需要给他们标注上Component注解:

1
2
3
4
5
6
7
8
9
java复制代码@Component
public class StrategyImpl1 implements Strategy {
...
}

@Component
public class StrategyImpl2 implements Strategy {
...
}

然后,写一个StrategyConfig配置类,用于向容器中注册一个StrategyRunner:

1
2
3
4
5
6
7
8
9
java复制代码@Configuration
public class StrategyConfig {
@Bean
public StrategyRunner strategyRunner(List<Strategy> strategies) {
Map<String, Strategy> strategyMap = strategies.stream()
.collect(Collectors.toMap(Strategy::flag, s -> s));
return flag -> strategyMap.get(flag).process();
}
}

仔细看strategyRunner方法的实现,不难发现,其中的逻辑与之前的StrategyRunnerImpl几乎完全相同,也是根据一个List<Strategy>来构造一个Map<String, Strategy>。只不过,这里的strategies列表不是我们自己构造的,而是通过方法参数传进来的。由于strategyRunner标注了Bean注解,因此参数上的List<Strategy>实际上是在SpringBoot初始化过程中从容器获取的,所以我们之前向容器中注册的那两个实现类会在这里被注入。

这样,我们再也无需操心系统中一共有多少个Strategy实现类,因为SpringBoot的自动配置会帮我们自动发现所有实现类。我们只需编写自己的Strategy实现类,然后将它注册进容器,并在任何需要的地方注入StrategyRunner:

1
2
java复制代码@Autowired
private StrategyRunner strategyRunner;

然后直接使用strategyRunner就行了:

1
2
java复制代码strategyRunner.run("s1");
strategyRunner.run("s2");

控制台输出如下:

1
2
复制代码strategy 1
strategy 2

也就是说,当我们想添加新的Strategy实现类时,直接向容器中注册就行,Spring会自动帮我们“发现”这些策略类,这样就完美地实现了“对扩展开放,对修改关闭”的目标。

优化

到这里,其实还是有优化的空间。我们看到Strategy接口中有flag和process两个方法,实际上flag方法完全可以用注解来指定,这样Strategy接口中就只剩一个方法,看起来更清爽。

首先定义一个StrategyFlag注解,用来指定策略的标识:

1
2
3
4
5
6
java复制代码@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Component
public @interface StrategyFlag {
String value();
}

然后删除Strategy接口中的flag方法,同时改造两个实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public interface Strategy {
void process();
}

@StrategyFlag("s1")
public class StrategyImpl1 implements Strategy {
@Override
public void process() {
System.out.println("strategy 1");
}
}

@StrategyFlag("s2")
public class StrategyImpl2 implements Strategy {
@Override
public void process() {
System.out.println("strategy 2");
}
}

注意,StrategyFlag注解被Component注解标注了,所以打了StrategyFlag注解的类同时也打了Component注解,因此也会被注册到容器中。

最后修改StrategyConfig的实现:

1
2
3
4
5
6
7
8
9
10
java复制代码@Bean
public StrategyRunner strategyRunner(List<Strategy> strategies) {
Map<String, Strategy> strategyMap = strategies.stream()
.collect(Collectors.toMap(
// 获取策略标识
s -> s.getClass().getAnnotation(StrategyFlag.class).value(),
s -> s
));
return flag -> strategyMap.get(flag).process();
}

这里与上一版本的代码基本相同,唯一不同的是s.getClass().getAnnotation(StrategyFlag.class).value()这行代码,通过解析策略类上标注的StrategyFlag注解来获取策略的标识。

进一步优化

到这里,其实已经非常优雅了,我们用一个注解不仅换来了更好的可读性,还减少了一个接口方法。

不过,仔细思考一下,每个策略类都会被标注StrategyFlag注解,所以理论上仅仅依靠StrategyFlag注解就能发现所有的策略类,也就是说,Strategy接口已经不再需要了,所以我们可以大胆删掉Strategy接口。

但是问题来了,假如没有了Strategy接口,那要如何确定具体策略的处理函数呢?

这里有两种方法来实现,一种是依靠自定义注解,另一种是通过解析方法签名。由于通过自定义注解来实现比较简单,所以下面演示一下这种方法。

首先创建一个自定义注解StrategyHandler:

1
2
3
4
java复制代码@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface StrategyHandler {
}

这个注解主要用来标注在方法上,用来标识策略的处理方法。

然后改造两个策略实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@StrategyFlag("s1")
public class Strategy1 {
@StrategyHandler
public void handleStrategy1() {
System.out.println("strategy 1");
}
}

@StrategyFlag("s2")
public class Strategy2 {
@StrategyHandler
public void handleStrategy2() {
System.out.println("strategy 2");
}
}

这里的两个实现类已经被重命名成了Strategy1和Strategy2,而且它们并没有实现任何接口。实际上,这是两个互相独立的、互不相关的类,唯一的共同点是它们都被标注了StrategyFlag注解,都包含一个标注了StrategyHandler接口的方法,而且这个方法是无参数、无返回值的。

接下来是StrategyConfig的实现:

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
java复制代码@Configuration
public class StrategyConfig implements ApplicationContextAware {
private ApplicationContext applicationContext;

@Bean
public StrategyRunner strategyRunner() {
// 从容器中获取所有标注了StrategyFlag注解的类
Map<String, Object> strategyClass = applicationContext.getBeansWithAnnotation(StrategyFlag.class);

// 策略处理函数
Map<String, Function<Void, Void>> strategyHandlers = new HashMap<>();

// 遍历所有策略类
for (Object s : strategyClass.values()) {
// 获取策略标识
String flag = s.getClass().getAnnotation(StrategyFlag.class).value();

// 遍历策略类中的所有方法
for (Method m : s.getClass().getMethods()) {
// 如果方法标注了StrategyHandler注解,则封装成可调用对象加入strategyHandlers
if (m.isAnnotationPresent(StrategyHandler.class)) {
strategyHandlers.put(flag, ignored -> {
try {
m.invoke(s);
return null;
} catch (Exception e) {
throw new RuntimeException(e);
}
});
break;
}
}
}

return flag -> strategyHandlers.get(flag).apply(null);
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 获取ApplicationContext容器对象
this.applicationContext = applicationContext;
}
}

这里的代码稍微有点复杂,但整体的思路还是很清晰的。

由于我们取消了接口,因此不能简单地通过参数注入来获取容器中地所有策略类,而只能通过ApplicationContext中的getBeansWithAnnotation来获取,所以StrategyConfig需要实现ApplicationContextAware来获取容器对象。

获取到所有策略类之后,需要遍历每个策略类的每个方法,一旦发现某个方法被标注了StrategyHandler接口,就放进strategyHandlers中,供将来调用,其中用到了一些Java反射的API。

总结

经过两次优化,我们基于SpringBoot实现了一个比较优雅的策略模式,这种方式无需实现任何接口,只需几个简单的注解就能声明式地指定策略类标识以及策略处理方法,可以很容易地扩展到各种需要使用策略模式的应用场景。

本文转载自: 掘金

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

动态内存管理 动态内存管理

发表于 2021-11-28

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

动态内存管理

为什么存在动态内存分配

我们到现在为止掌握的是什么样的内存开辟方式呢

1
2
3
4
5
6
c复制代码//创建一个变量
int val = 20; //局部变量 在栈空间中开辟4个字节
int g_val = 10; //全局变量 在静态区中开辟4个字节
//创建一个数组
char arr[10] = {0}; //局部区域 在栈空间中开辟10个字节连续的空间
char g_arr[5] = {0};//全局区域 在静态区空间中开辟5个字节的连续空间

image-20210920203300323

但是上述的开辟空间的方式有两个特点:

  1. 空间开辟大小是固定的。
  2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
    但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。 这时候就只能试试动态内存开辟了。

c99是支持变长数组的,但现在很多编译器就不支持c99,连vs都不支持,所以就有动态内存的概念

动态内存函数的介绍

malloc申请空间和free释放空间

c语言提供了一个动态内存开辟的函数

1
c复制代码void* malloc(size_t size);

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

1.如果开辟成功,则返回一个指向开辟好空间的指针。

image-20210920211643597

2.如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。

image-20210920213112488

*3.返回值的类型是 void ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。\

image-20210920214320778

4.如果参数 size 为0,malloc的行为是标准未定义的,取决于编译器。

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
c复制代码#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>

int main()
{
//向内存申请10个整形的空间
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
//把开辟失败的信息打印出来
printf("%s",strerror(errno));
}
else
{
//正常使用空间
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;//在找下标为i的元素
}
for (i = 0; i < 10; i++)//再把每个元素打印出来
{
printf("%d ", *(p + i));
}
}
return 0;
}

image-20210920214641883

那我们可不可以看开辟失败的呢

我们可以用INT_MAX(他是整形最大),一个超级大的数字

image-20210920215213637

image-20210920215421502

image-20210920215536367

有借有还 free释放内存

free函数用来释放动态开辟的内存。
1.如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
2.如果参数 ptr 是NULL指针,则函数什么事都不做。

image-20210920225328673

image-20210920230005418

注意

malloc和free是成对使用的,谁开辟谁释放

calloc申请内存

在内存中开辟一个数组,把元素都改成零

函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。

image-20210921064458843

与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0

image-20210921064517477

realloc调整动态内存的大小

当然我们可以申请空间,但会不会遇到申请的空间不够了,想要增加一些些,大了想要去掉一些些

image-20210921070646055

realloc使用的注意事项

1.如果p指向的空间之后有足够的内存空间可以追加,则直接追加,后返回p

2.如果p指向的空间之后没有足够的内存空间可以追加,则realloc函数会重新找一块新的内存区域,开辟一块满足需求的空间,并且把原来的内存中的数据拷贝回来,释放旧的内存空间,最后返回新开辟的内存空间地址

3.但也有一个大问题,就是开辟INT_MAX,用新的变量ptr来接收realloc返回值

image-20210921080812450

image-20210921091309880

当然realloc也可以直接开辟空间

image-20210921142948715

常见的动态内存错误

1.对NULL指针的解引用操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c复制代码#include<stdio.h>
#include<stdlib.h>

int main()
{
int* p = (int*)malloc(40);//没成功就会有大问题
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}

image-20210921095023767

所以为了防止没有开辟动态内存成功就需要做个判断

image-20210921140318273

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
c复制代码#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>

int main()
{
int* p = (int*)malloc(5*sizeof(int));
if (p == NULL)//这里我的确判断有没有开辟成功了
{
printf("%s", strerror(errno));
}
else
{
int i = 0;
for (i = 0; i < 10; i++)//但是我这里访问10个整型的空间
{
*(p + i) = i;
}
}
free(p);
p = NULL;
return 0;
}

image-20210921141134755

3.对非动态开辟内存使用free释放

1
2
3
4
5
6
7
8
9
c复制代码int main()
{
int a = 0;
int* p = &a;
*p = 20;
free(p);
p = NULL;
return 0;
}

image-20210921141835492

4.使用free释放一块动态内存开辟的一部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c复制代码#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 0;//如果是空指针就直接返回,不干了
}
int i = 0;
for (i = 0; i < 10; i++)
{
*p++ = i;//这个++就是bug的地方
}
//回收空间
free(p);
p = NULL;
return 0;
}

image-20210921145331118

只要p不是指向申请的空间的首地址,其他地方都是错的

5.对同一块动态内存多次释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
c复制代码#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>

int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 0;
}
//使用
//释放
free(p);
//...
free(p);
return 0;
}

image-20210921150827892

6.动态开辟内存忘记释放(内存泄漏)

1
2
3
4
5
6
7
8
9
10
11
12
13
c复制代码#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>

int main()
{
while (1)
{
malloc(100);
}
return 0;
}

image-20210921152924479

几个面试题

题目1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str,"hello world");
printf(str);//这个写法和printf("%s",str);是一样的
}
int main()
{
Test();
return 0;
}

问运行Test函数会有什么样的结果

image-20210921160444529

修改正确

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
c复制代码#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>

void GetMemory(char* *p)
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str,"hello world");
printf(str);//这个写法和printf("%s",str);是一样的
free(str);//用完就释放
str = NULL;
}
int main()
{
Test();
return 0;
}

image-20210921161553669

题目2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}

请问运行Test 函数会有什么样的结果

输出随机值

image-20210921163718477

image-20210921164324364

修改正确

既然是p被销毁了,那我们让他不销毁就可以了延长它的生命周期用static

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码char* GetMemory(void)
{
static char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}

image-20210921165420500

题目3

1
2
3
4
5
6
7
8
9
10
11
c复制代码void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}

这题基本和第一题一样,不过这题就只有内存泄漏的错误

image-20210921184936339

修改正确

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c复制代码#include<stdio.h>
#include<stdlib.h>
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
free(str);//用完就释放,防止内存泄漏
str = NULL;
}
int main()
{
Test();
return 0;
}

image-20210921185134567

题目4

1
2
3
4
5
6
7
8
9
10
11
c复制代码void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}

问题非常大的打印出结果

image-20210921191535581

image-20210921191434742

修改正确

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c复制代码#include<stdio.h>
#include<string.h>
#include<stdlib.h>

void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);//这里考查的是free释放后并没有使str为NULL,所以下面if判断就没有作用,如果使他有作用就让str为NULL
str = NULL;
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}

这道题真正目的就是让你什么都不打印

image-20210921192651227

C/C++程序的内存开辟

image-20210921195028220

C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

本文转载自: 掘金

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

自定义类型 自定义类型

发表于 2021-11-28

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

自定义类型

结构体

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量

声明一个结构体类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
c复制代码//声明一个学生类型,是想通过学生类型来创建学生变量(对象)
//描述学生就得有属性啥的。名字,电话,性别,年龄
struct Stu
{
char name[20];//名字
char tele[12];//电话
char sex[10];//性别
int age;//年龄
};

struct Stu s3;//创建全局结构体变量

int main()
{
struct Stu s1;
struct Stu s2;//创建结构体变量
return 0;
}

image-20210913094556253

特殊声明

在声明结构的时候,可以不完全的声明。

没有结构体标签

匿名结构体类型

要清楚一点匿名结构体是个类型不占用空间的,就好像int一样,他们没有创建一个变量是不会开辟空间的,所以类型不占用空间就没有销毁不销毁这一说,只有有了空间才会有销毁不销毁这一说,类型就好像图纸,变量才是真正要盖的房子

1
2
3
4
5
6
7
c复制代码struct 
{
char name[20];//名字
char tele[12];//电话
char sex[10];//性别
int age;//年龄
}stu;//直接接结构体变量,匿名的时候后面就把变量给创建好,不然之后也用不到这个结构名,因为没有结构体名字怎么创建变量呢

匿名结构体指针类型

1
2
3
4
5
6
7
c复制代码struct 
{
char name[20];//名字
char tele[12];//电话
char sex[10];//性别
int age;//年龄
}* pstu;//这时pstu就变成匿名结构体指针了

image-20210917060233748

结构体自引用

在结构中包含一个类型为该结构本身的成员是否可以呢?

image-20210917061523191

所以节点(Node)就出来了

一块表示数据一块表示地址

1
2
3
4
5
c复制代码struct Node
{
int data; //数据域
struct Node* next; //指针域
};

这就是结构体自引用 自己类型里的变量找到同类型的另外一个对象

注意

image-20210917063320647

所以对于结构体的自引用是不能省略自己的结构体标签,下面就是解决方案

1
2
3
4
5
c复制代码typedef struct Node
{
int data; //数据域
struct Node* next; //指针域
}Node;

结构体变量的定义和初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码struct Stu
{
char name[20];//名字
char tele[12];//电话
char sex[10];//性别
int age;//年龄
};

struct Stu s3;//创建全局结构体变量

int main()
{
struct Stu s1 = {"zhuzhongyuan","13151732661","nan",22};//(定义)创建结构体变量s1并初始化
printf("%s %s %s %d",s1.name,s1.tele,s1.sex,s1.age);
return 0;
}

image-20210917084658329

结构体内存对齐

现在我们深入讨论一个问题:计算结构体的大小。

这也是一个特别热门的考点:结构体内存对齐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c复制代码#include<stdio.h>
//内存对齐
//结构体内存对齐
struct S2
{
int a;
char b;
char c;
};
struct S1
{
char b;
int a;
char c;
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}

image-20210917090210504

结构体内存对齐的规则

  1. 结构体的第一个成员永远放在结构体起始位置偏移量为0的位置
  2. 结构体成员从第二成员开始,总是放在一个对齐数的整数倍处
  3. 对齐数是什么呢是编译器默认的对齐数和变量自身大小的较小值注意一下 linux没有默认对齐数
    vs下默认对齐数是8
  4. 结构体的总大小必须是各个成员的对齐数中最大那个对齐数的整数倍
  5. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

image-20210917093552942

image-20210917094041911

几个练习

image-20210917094956838

image-20210917101912669

为什么存在内存对齐

1.平台原因(移植原因)

不是所有的硬件平台都能访问任意地址上的任意数据,某些平台只能在某些地址某些特定类型的数据,否则抛出硬件异常

2.性能原因

数据结构(尤其是栈)应该尽可能地在自然边界上对齐,原因在于,为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的访问仅需要一次访问

总体来说

结构体内存对齐就是拿空间换取时间的做法,形象的说就是浪费了内存,换来了方便

解决

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。

image-20210917233228992

修改默认对齐数

vs默认对齐数是8

我们可以通过#pragma pack()来修改默认对齐数

image-20210917234827620

默认设置对齐数是2的几次方

offsetof宏的实现

计算结构体中某变量相对于首地址的偏移

offsetof原格式

image-20210918054155802

image-20210918054846243

结构体传参

值传递

image-20210918061503779

址传递

image-20210918063001860

如何选择

1.函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。

2.如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

结论: 结构体传参的时候,要传结构体的地址。

位段

什么是位段

位段的声明和结构体类似,有两个不同

1.位段的成员必须是int,unsiged int 或 signed int

2.位段的成员后面有一个冒号和一个数字

image-20210918065432943

image-20210918070011886

位段的内存分配

  1. 位段的成员可以是int, unsigned int,signed int,或者是char(属于整形家族)类型
  2. 位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段

image-20210918090139913

位段的跨平台问题

  1. int 位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这个也是不确定的。

总结:

跟结构相比,位段可以达到同样的效果,可以很好的节省空间,但是有跨平台的问题存在。

位段的应用

image-20210918091635663

枚举

如果我们没有对枚举常量进行初始化的话,他们是默认加一的

我们常量分为4种

1.字面常量

2.const修饰的常变量

3.#号定义的标识符常量

4.枚举常量

这里我们就讲枚举常量

image-20210918094024475

枚举常量是不可以改的,只能初始化

image-20210918094628627

那枚举怎么用呢

image-20210918095317933

枚举的优点

我们可以使用 #define 定义常量,为什么非要使用枚举? 枚举的优点:

  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
  3. 防止了命名污染(封装)
  4. 便于调试
  5. 使用方便,一次可以定义多个常量

简易计算器

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
c复制代码#include<stdio.h>
enum Option
{
exit,
add,
sub,
mul,
div
};
void menu()
{
printf("*********************\n");
printf("****1.add 2.sub****\n");
printf("****3.mul 4.div****\n");
printf("**** 0.exit ****\n");
printf("*********************\n");
}

int main()
{
int input = 0;
int a = 0;
int b = 0;
int c = 0;
do
{
menu();
printf("请选择:>");
scanf("%d",&input);
printf("请输入两个操作数:>");
scanf("%d%d", &a, &b);
switch (input)
{
case add:
c = a + b;
printf("%d\n", c);
break;
case sub:
c = a - b;
printf("%d\n", c);
break;
case mul:
c = a * b;
printf("%d\n", c);
break;
case div:
if (b == 0)
{
printf("分子不能为0\n");
break;
}
else
{
c = a / b;
printf("%d\n", c);
break;
}
default:
break;
}
} while (input);
return 0;
}

image-20210918104241635

联合(共用体)

联合类型的定义

联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以
联合也叫共用体)。

联合类型的声明

image-20210918105430018

image-20210918105748187

image-20210918110608353

联合的特点

联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)

判断当前机器的大小端存储

之前学过一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
c复制代码#include<stdio.h>
int main()
{
int a = 1; //0x 00 00 00 01
//低 -------------> 高
//01 00 00 00 小端存储
//00 00 00 01 大端存储
//想办法拿到a的第一个字节
char* pc = (char*)&a;
if (*pc == 1)
{
printf("小端存储");
}
else
{
printf("大端存储");
}
return 0;
}

image-20210918111603326

现在学到共用体正好利用他的特殊情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c复制代码#include<stdio.h>
union Un
{
char c;
int i;
};
int main()
{
union Un u = { 0 };
u.i = 1;
if (u.c == 1)
{
printf("小端存储");
}
else
{
printf("大端存储");
}
return 0;
}

image-20210918112419963

联合大小的计算

联合的大小至少是最大成员的大小。
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

image-20210918122833572

本文转载自: 掘金

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

Kafka生产者分区策略

发表于 2021-11-28

前言

查阅了一些资料和看了许多网上的文章,总觉得没有把Kafka生产者分区策略给讲明白,本篇将围绕以下问题步步深入来对文章进行展开。

  • 为什么需要生产者分区策略
  • 生产者分区策略有哪些
  • 不同分区策略有哪些优点和缺点
  • 如何进行自定义分区策略

一、生产者发送消息流程

07.Kafka生产者分区策略01.jpg

说明

(1)新建ProducerRecord对象,包含目标主题和要发送的内容,也可以指定键或分区;

(2)发送ProducerRecord对象时,生产者要把键和值对象序列化成字节数组,这样它们才能在网络上传输;

(3)数据被传给分区器:

  • 如果ProducerRecord对象中指定了分区,那么分区器就不会再做任何事情,直接发送到该分区;
  • 如果发送时未指定,则默认使用key的hash值指定一个分区;
  • 如果发送时未指定消息key,则采用轮询的方式选择一个分区(这就是Kafka默认的分区策略)。

(4)这条记录被添加到一个记录批次里,这个批次里的所有消息会被发送到相同的主题和分区上,有一个独立的线程负责把这些记录批次发送到相应的broker上。

(5)服务器在收到这些消息时会返回一个响应:

  • 如果消息成功写入kafka,就返回一个RecordMetaData对象,它包含了主题和分区信息,以及记录在分区里的偏移量。
  • 如果写入失败,则会返回一个错误。生产者在收到错误之后会尝试重新发送消息,几次之后如果还是失败,就返回错误信息。

ProducerRecord介绍

在发送消息时,需要将 producer 发送的数据封装成一个 ProducerRecord 对象,一个ProducerRecord表示一条待发送的消息记录。

1
2
3
4
5
6
7
8
9
10
11
less复制代码ProducerRecord(@NotNull String topic, Integer partition, Long timestamp, String key, String value, @Nullable Iterable<Header> headers)

ProducerRecord(@NotNull String topic, Integer partition, Long timestamp, String key, String value)

ProducerRecord(@NotNull String topic, Integer partition, String key, String value, @Nullable Iterable<Header> headers)

ProducerRecord(@NotNull String topic, Integer partition, String key, String value)

ProducerRecord(@NotNull String topic, String key, String value)

ProducerRecord(@NotNull String topic, String value)

参数说明

参数 说明
topic 所属主题
partition 所属分区
key 键值
value 消息体
timestamp 时间戳
  • 指明partition的情况下,直接将指明的值作为partition值;
  • 没有指明partition值,但有key的情况下,将key的hash值与topic的partition数目进行取余操作,得到partition值;
  • 即没有partition值又没有key的情况下,第一次调用时随机生成一个整数(后面每次调用在这个整数上自增),将这个值与topic可用的partition总数取余得到partition值,也就是常说的round-robin算法。

总结

因为一个topic可以有多个 Partition组成,在进行传参时Topic是用户自己创建的所以可以必须指定,但是Partition是根据topic名称+有序序号由系统生成的,对于用户而言很难指定到某个具体的Partition。

那么分区器在发送消息的时候怎么知道要发送给哪个分区呢?

这时候生产者分区策略就派上用场了。

通过对上方生产者发送消息流程进行解读我们在前言中提到的第一个问题应该已经有了答案。

下面继续进行第二个问题的探索,生产者分区策略有哪些呢?

二、分区策略

2.1 概述

分区策略:决定生产者将消息发送到哪个分区的算法,Kafka提供了默认的分区策略,也支持自定义的分区策略。

2.2默认分区策略介绍

轮询策略

也称 Round-robin 策略,即顺序分配。

07.Kafka生产者分区策略02.png

说明

比如一个主题下有 3 个分区,那么第一条消息被发送到分区 0,第二条被发送到分区 1,第三条被发送到分区 2,以此类推。当生产第 4 条消息时又会重新开始,即将其分配到分区 0。

总结

  • 轮询策略是 Kafka Java 生产者 API 默认提供的分区策略;
  • 轮询策略的负载均衡表现非常优秀,总能保证消息最大限度地被平均分配到所有分区上,默认情况下它是最合理的分区策略。
随机策略

也称 Randomness 策略。所谓随机就是我们随意地将消息放置到任意一个分区上,如下面这张图所示。

07.Kafka生产者分区策略03.png

如果要实现随机策略版的 partition 方法,很简单,只需要两行代码即可:

1
2
java复制代码List partitions = cluster.partitionsForTopic(topic);
return ThreadLocalRandom.current().nextInt(partitions.size());

先计算出该主题总的分区数,然后随机地返回一个小于它的正整数。本质上看随机策略也是力求将数据均匀地打散到各个分区,但从实际表现来看,它要逊于轮询策略,所以如果追求数据的均匀分布,还是使用轮询策略比较好。事实上,随机策略是老版本生产者使用的分区策略,在新版本中已经改为轮询了。

按消息键保序策略

也称 Key-ordering 策略。

07.Kafka生产者分区策略04.png

说明

  1. Kafka允许为每条消息定义消息键,简称为Key
  2. Key可以是一个有明确业务含义的字符串:客户代码、部门编号、业务ID、用来表征消息的元数据等
  3. 一旦消息被定义了Key,可以保证同一个Key的所有消息都进入到相同的分区里,由于每个分区下的消息处理都是顺序的,所以这个策略被称为按消息键保序策略

实现这个策略的 partition 方法同样简单,只需要下面两行代码即可:

1
2
java复制代码List partitions = cluster.partitionsForTopic(topic);
return Math.abs(key.hashCode()) % partitions.size();

总结

Kafka Java生产者的默认分区策略:

  • 如果指定了Key,采用按消息键保序策略
  • 如果没有指定Key,采用轮询策略

三、自定义分区策略

kafka java api提供了一个接口,用于自定义分区策略:org.apache.kafka.clients.producer.Partitioner

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public interface Partitioner extends Configurable, Closeable {

/**
* Compute the partition for the given record.
*
* @param topic The topic name
* @param key The key to partition on (or null if no key)
* @param keyBytes The serialized key to partition on( or null if no key)
* @param value The value to partition on or null
* @param valueBytes The serialized value to partition on or null
* @param cluster The current cluster metadata
*/
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);

/**
* This is called when partitioner is closed.
*/
public void close();

}

说明

  • partition():计算给定记录的分区

参数说明

参数 说明
topic 需要传递的主题
key 消息中的键值
keyBytes 分区中序列化过后的key,byte数组的形式传递
value 消息的 value 值
valueBytes 分区中序列化后的值数组
cluster 当前集群的原数据
  • close() : 继承了 Closeable 接口能够实现 close() 方法,在分区关闭时调用。
  • onNewBatch(): 表示通知分区程序用来创建新的批次

如何想实现自定义分区策略,直接实现Partitioner接口,重写接口中的方法。

四、总结

  • 轮询策略(默认分区策略)

优点:可以提供非常优秀的负载均衡能力,可以保证消息被平均分配到所有分区上。
缺点:无法保证消息的有序性。

  • 随机策略

优点:消息的分区选择逻辑简单。
缺点:负载均衡能力一般,也无法保证消息的有序性

  • 按消息键保序策略

优点:可以保证相同key的消息被发送到相同的分区,因此可以保证相同key的所有消息之间的顺序性。
缺点:可能会产生数据倾斜 —— 取决于数据中key的分布,以及使用的hash算法。

附参考文章链接地址:

zhongmingmao.me/2019/07/24/…

www.cnblogs.com/yxb-blog/p/…

本文转载自: 掘金

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

大话数据结构--图的常用存储结构

发表于 2021-11-28

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

7.3.2邻接表

为了解决边数相对顶点较少的图,邻接矩阵这种结构会存在大量的空间浪费

如下:

image-20211117201028744

再回忆我们在树中谈存储结构时,讲到了一种孩子表示法,将结点存入数组,并对结点的孩子进行链式存储,不管有多少孩子,也不会存在空间浪费问题。这个思路同样适用于图的存储。我们把这种数组与链表相结合的存储方法称为邻接表 (Adjacency List)。

邻接表的处理办法:

1.图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过数组可以较容易地读取顶点信息,更加方便。另外,对于顶点数组中,每个数据元素还需要存储指向第一个邻接点的指针,以便于查找该顶点的边信息。

2.图中每个顶点Vi的所有邻接点构一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点Vi的边表,有向图则称为顶点Vi作为弧尾的出表。

image-20211117201504503

上图中的V1顶点与V0、V2互为邻接点,则在V1的边表中,adjvex分别为V0的0和V2的2

下面解释什么是边表

顶点表的各个节点由data和firstedge两个域表示,data是数据域,存储顶点的信息,firstedge是指针域,指向边表(因为它是无向图,所以叫边表) 的第一个结点,即此顶点的第一个邻接点。边表结点由adjvex和next两个域组成。adjvex 是邻接点域,存储某顶点的邻接点在顶点表中的下标,next 则存储指向边表中下一个结点的指针。

在有向图中,邻接表的结构是类似的

我们可以建立一个有向图的逆邻接表,即对每个顶点Vi都建立一个链接为v为弧头的表

image-20211117202453595

image-20211117202504430

此时我们很容易就可以算出某个顶点的入度或出度是多少,判断两顶点是否存在弧也很容易实现。

对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可

image-20211117202716960

7.3.3十字链表

那么对于有向图来说,邻接表是有缺陷的。关心了出度问题,想了解入度就必须要遍历整个图才能知道,反之,逆邻接表解决了入度却不了解出度的情况。有没有可能把邻接表与逆邻接表结合起来呢?答案是肯定的,就是把它们整合在一起。这就是我们现在要讲的有向图的一种存储方法:十字链表(Orthogonal List)。

重新定义结点表结点结构

data firstin firstout

其中firstin表示入边表头指针,指向该顶点的入边表中第一个结点, firstout 表示出边表头指针,指向该顶点的出边表中的第一个结点。

重新定义边表结点结构

image-20211117203142673

其中tailvex 是指弧起点在顶点表的下标,headvex 是指弧终点在顶点表中的下标,headlink是指入边表指针域,指向终点相同的下一条边,tallink 是指边表指针域,指向起点相同的下一条边。 如果是网,还可以再增加一个 weight域来存储权值。

实例如下:

image-20211117203324383

以顶点V0来说,firstout指向的是出边表中的第一个结点V3所以tailvex和headvex是03(就是数组下标),那为什么headlink和taillink为空了呢,因为没用终点和V0一样的结点了(指向V3的),那为什么taillink也为空呢,因为没有和V0一样起点的结点(从V0出发)了呀!

图中也有些例子,可以多理解理解

同志们一定好好看看和理解这个图!!!

十字链表的好处就是因为把邻接表和逆邻接表整合在了一起,这样很容易找到以Vi为尾的弧,也容易找到以vi为头的弧,从而更容易求得顶点的出度和入度

缺点就是结构复杂了一点,在有向图的应用中,十字链表是非常好的数据结构模型

7.3.4邻接多重表

十字链表对有向图的存储结构进行了优化

在无向表的应用中,关注的重点是顶点,邻接表是好的选择

如果关注的是边的操作,就需要更简单的方式

对边表结点结构重新定义

image-20211117232920410

其中ivex和jvex是与某条边依附的两个顶点在顶点表中下标。

ilink 指向依附顶点ivex的下一条边

jlink 指向依附顶点jvex的下一条边

下图告诉我们它有4个顶点和5条边,显然,我们就应该先将4个顶点和5条边的边表结点画出来。由于是无向图,所以ivex是0、jvex是1还是反过来都是无所谓的,不过为了绘图方便,都将ivex值设置得与一旁的顶点下标相同。

好,我们来分析这个图

firstedge是指针域,指向边表的第一个结点,ivex和jvex是依附于边的顶点坐标的下标(注意是顶点坐标的下标),比如图中的0和1,那么它们就代表顶点V0和顶点V1中间的那条边

那么ilink和jlink是什么呢?

ilink指的是ivex依附顶点的下一条边

jlink指的是jvex依附顶点下一条边

实例如图所示:

image-20211118080943657

看上面的连线,firstedge指向一条边,顶点下标和ivex的值相同,继续,顶点V0有三个边跟它有关v0v1,v0v2,v0v3

所以连线5,6满足指向下一条依附于顶点的v0的边的目标,ilink指向的结点的jvex一定要和它本身的ivex值相同。连线7就是指v1,v0这条边,它是相当于顶点v1指向v1,v2边后的下一条。v2有三条依附,所以在连线3后就有了8跟9.连线10的就是顶点v3在连线4后下一条边。

图上共5条边所以有10条连线,符合预期

邻接多重表与邻接表的差别,仅仅是在于同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。 这样对边的操作就方便多了,若要删除左图的(V0,V2) 这条边,只需要将右图的⑥⑨的链接指向改为^即 可。各种基本操作的实现也和邻接表是相似的

7.3.5边集数组

边集数组是由两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end) 和权(weight)组成

实例如下:

image-20211118082428193

这个结构很好理解,看图就能看懂

本文转载自: 掘金

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

JVM对象创建与内存分配

发表于 2021-11-28

JVM对象创建与内存分配

前言

在我们创建对象时的一个流程是怎样的,创建的对象又应该在哪里分配给他内存,下面让我们一起来看一下吧。

03JVM对象创建与内存分配机制深度剖析.png

对象创建的流程

在我们创建一个对象时,主要经历以下几个阶段:

未命名文件 (12).png

  • 类加载检查:在分配内存之前,类必须要先加载,因此进行类加载检查。类加载就是将字节码文件读入到JVM。
  • 分配内存:一般对象都是在堆中分配内存。那么内存是如何分配的,有两种分配内存的方法:
+ 指针碰撞:适用于内存是规整的,也就是说已分配与未分配是分开的,在分配内存时,顺序分配下去即可。
+ 空闲列表:适用于内存是不规整的,内存布局比较分散,空闲内存会有一个列表进行维护,分配内存时在列表中找出合适的内存分配即可。以上分配方式在并发环境下,会出现一些问题,试想一下,在同一时间恰有多个对象指向了同一块内存,这块内存到底改分配给谁?针对上述问题,我们也有解决方案:


+ CAS(compare and swap):CAS会保证分配内存的原子性,给对象分配内存之前先看看这个内存跟以前是否一样,一样就分配,不一样就说明被分配出去了。
+ 本地线程缓冲栈:给每个线程分配一小块区域,每个线程只能在自己的区域里面分配内存。
  • 赋默认值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值。
  • 设置对象头:一个对象主要由对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)组成,对象头主要结构如下:

32位对象头

clipboard.png

  • 执行init方法:为属性赋予真正的值。

对象内存分配

说完对象创建的流程,下面我们一起看一下对象是如何进行内存分配的。

未命名文件 (13).png

  • 逃逸分析与栈中分配:一般来说,对象通常是分配在堆中的,但是也有例外的情况,那就是产生了逃逸分析,什么是逃逸分析呢,一个对象在方法中被定义后,永远不会被其他外部方法所引用就是产生了逃逸,对于产生逃逸的对象我们没有必要在堆中给他分配内存,直接在栈中只为它的成员变量分配内存即可,也就是栈中分配,因为不需要被引用。例如下面这个对象:
1
2
3
4
5
csharp复制代码public void test() {
  User user = new User();
  user.setId(1);
  user.setName("zhuge");
}

User就产生了逃逸,因为在这个方法中没有返回任何对象,所以User不会被其他外部对象引用。

  • 对象直接进入老年代:让对象直接进入老年代只有一个目的,为了避免为大对象分配内存时的复制操作而降低效率。对象在以下几种情况会直接进入老年代。
+ 大对象直接进入老年代
+ 长期存活的对象将进入老年代
+ 对象动态年龄判断,一批对象的总大小大于这块Survivor区域内存大小的50%,那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了。
  • 对象在Eden区分配:大多数情况下,对象在新生代中 Eden 区分配。

本文转载自: 掘金

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

第八十九章 SQL命令 WHERE(二) 第八十九章 SQL

发表于 2021-11-28

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

第八十九章 SQL命令 WHERE(二)

相等比较谓词

下面是可用的相等比较谓词:

Predicate Operation
= Equals
<> Does not equal
!= Does not equal

|Is greater than
< |Is less than
= |Is greater than or equal to
<= |Is less than or equal to

例如:

1
2
sql复制代码SELECT Name, Age FROM Sample.Person
WHERE Age < 21

SQL根据排序规则(值的排序顺序)定义了比较操作。
如果两个值以完全相同的方式排序,则它们相等。
如果一个值排在第二个值之后,则该值大于另一个值。
字符串字段排序规则接受字段的默认排序规则。
IRIS默认排序规则不区分大小写。
因此,两个字符串字段值的比较或字符串字段值与字符串文字的比较(默认情况下)是不区分大小写的。
例如,如果Home_State字段值是两个字母的大写字符串:

Expression Value
‘MA’ = Home_State TRUE for values MA.
‘ma’ = Home_State TRUE for values MA.
‘VA’ < Home_State TRUE for values VT, WA, WI, WV, WY.
‘ar’ >= Home_State TRUE for values AK, AL, AR.

然而,请注意,两个字面值字符串的比较是区分大小写的:WHERE 'ma'=' ma'总是FALSE。

BETWEEN谓词

BETWEEN比较操作符允许选择语法BETWEEN lowval和highval指定范围内的数据值。
这个范围包括lowval和highval本身的值。
这相当于一个成对的大于或等于运算符和一个小于或等于运算符。
这个比较如下面的例子所示:

1
2
sql复制代码SELECT Name,Age FROM Sample.Person
WHERE Age BETWEEN 18 AND 21

这将返回Sample中的所有记录。
人表,年龄值介于18和21之间,包括这些值。
注意,必须按升序指定BETWEEN值;
像BETWEEN 21 AND 18这样的谓词将不返回任何记录。

与大多数谓词一样,BETWEEN可以使用NOT逻辑操作符进行倒装,如下例所示:

1
2
3
sql复制代码SELECT Name,Age FROM Sample.Person
WHERE Age NOT BETWEEN 20 AND 55
ORDER BY Age

这将返回Sample中的所有记录。
年龄值小于20或大于55的Person表,不包括这些值。

BETWEEN通常用于一个数值范围,该范围按数字顺序排序。
但是,BETWEEN可以用于任何数据类型的值的排序序列范围。

BETWEEN使用与它所匹配的列相同的排序规则类型。
默认情况下,字符串数据类型排序不区分大小写。

IN和%INLIST谓词

IN谓词用于将一个值匹配到非结构化的一系列项。
它的语法如下:

1
sql复制代码WHERE field IN (item1,item2[,...])

Collation应用于IN比较,就像它应用于相等测试一样。
IN使用字段的默认排序规则。
默认情况下,与字段字符串值的比较不区分大小写。

%INLIST谓词是IRIS扩展,用于将值匹配到 IRIS列表结构的元素。
它的语法如下:

1
sql复制代码WHERE item %INLIST listfield

%INLIST使用EXACT排序。
因此,默认情况下,%INLIST字符串比较是区分大小写的。

使用任何一个谓词,都可以执行相等比较和子查询比较。

Substring谓词

可以使用下面的方法来比较字段值和子字符串:

Predicate Operation
%STARTSWITH 该值必须以指定的子字符串开始。
[ 包含运算符。该值必须包含指定的子字符串。

%STARTSWITH谓词

IRIS %STARTSWITH比较操作符允许对字符串或数字的初始字符执行部分匹配。
下面的示例使用%STARTSWITH。
选择“Name”以“S”开头的记录:

1
2
sql复制代码SELECT Name,Age FROM Sample.Person
WHERE Name %STARTSWITH 'S'

与其他字符串字段比较一样,%STARTSWITH比较使用字段的默认排序规则。
默认情况下,字符串字段不区分大小写。
例如:

1
2
sql复制代码SELECT Name,Home_City,Home_State FROM Sample.Person
WHERE Home_City %STARTSWITH Home_State

包含运算符(()

Contains操作符是左括号符号:[。
它允许将子字符串(字符串或数字)匹配到字段值的任何部分。
比较总是区分大小写的。
下面的示例使用Contains操作符选择Name值中包含“S”的记录:

1
2
sql复制代码SELECT Name, Age FROM Sample.Person
WHERE Name [ 'S'

NULL 谓词

这将检测未定义的值。
可以检测所有空值,或所有非空值。
NULL谓词的语法如下:

1
sql复制代码WHERE field IS [NOT] NULL

NULL谓词条件是可以在WHERE子句中的流字段上使用的少数谓词之一。

EXISTS 谓词

它使用子查询来测试子查询是否计算为空集。

1
2
3
4
sql复制代码SELECT t1.disease FROM illness_tab t1 WHERE EXISTS 
(SELECT t2.disease FROM disease_registry t2
WHERE t1.disease = t2.disease
HAVING COUNT(t2.disease) > 100)

FOR SOME 谓词

WHERE子句的FOR SOME谓词可用于根据一个或多个字段值的条件测试确定是否返回任何记录。
该谓词的语法如下:

1
sql复制代码FOR SOME (table [AS t-alias]) (fieldcondition)

FOR SOME指定字段condition的值必须为true;
至少有一个字段值必须匹配指定的条件。
Table可以是单个表,也可以是逗号分隔的表列表,每个表可以有一个表别名。
Fieldcondition为指定表中的一个或多个字段指定一个或多个条件。
table参数和字段condition参数都必须用括号分隔。

下面的示例展示了如何使用FOR SOME谓词来确定是否返回结果集:

1
2
3
4
sql复制代码SELECT Name,Age AS AgeWithWorkers
FROM Sample.Person
WHERE FOR SOME (Sample.Person) (Age<65)
ORDER BY Age

在上面的示例中,如果至少有一个字段包含的Age值小于指定的Age,则返回所有记录。
否则,不返回任何记录。

FOR SOME %ELEMENT 谓词

WHERE子句的FOR SOME %ELEMENT谓词语法如下:

1
sql复制代码FOR SOME %ELEMENT(field) [AS e-alias] (predicate)

FOR SOME %ELEMENT谓词用指定的谓词子句值匹配字段中的元素。
SOME关键字指定字段中至少有一个元素必须满足指定的谓词条件。
谓词可以包含%VALUE或%KEY关键字。

FOR SOME %ELEMENT谓词是一个集合谓词。

LIKE, %MATCHES, and %PATTERN 谓词

这三个谓词允许执行模式匹配。

  • LIKE允许使用文字和通配符进行模式匹配。
    当希望返回包含已知字面值子字符串的数据值,或在已知序列中包含多个已知子字符串时,请使用LIKE。
    LIKE使用目标的排序规则进行字母大小写比较。
  • %MATCHES允许使用文字、通配符、列表和范围进行模式匹配。
    当您希望返回包含已知字面值子字符串的数据值,或包含一个或多个位于可能字符列表或范围内的字面值字符,或在已知序列中包含多个这样的子字符串时,请使用%MATCHES。
    %MATCHES使用EXACT排序法进行字母大小写比较。
  • %PATTERN允许指定字符类型的模式。
    例如,'1U4L1",".A'(1个大写字母,4个小写字母,一个逗号,后面跟着任意数量的字母字符)。
    如果希望返回包含已知字符类型序列的数据值,请使用%PATTERN。
    %PATTERN可以指定已知的文字字符,但在数据值不重要但这些值的字符类型格式重要时特别有用。

谓词和逻辑操作符

可以使用AND和OR逻辑操作符关联多个谓词。
可以使用括号对多个谓词进行分组。
由于IRIS使用已定义的索引和其他优化来优化WHERE子句的执行,因此无法预测and和OR逻辑运算符链接的谓词的求值顺序。
因此,指定多个谓词的顺序对性能几乎没有影响。
如果希望严格地从左到右计算谓词,可以使用CASE语句。

注意:不能使用OR逻辑运算符将引用表字段的FOR SOME %ELEMENT集合谓词与引用另一个表中的字段的谓词关联起来。
例如,

1
2
sql复制代码WHERE FOR SOME %ELEMENT(t1.FavoriteColors) (%VALUE='purple') 
OR t2.Age < 65

因为这个限制取决于优化器如何使用索引,所以SQL只能在向表添加索引时强制执行这个限制。
强烈建议在所有查询中避免这种类型的逻辑。

本文转载自: 掘金

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

1…145146147…956

开发者博客

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