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

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


  • 首页

  • 归档

  • 搜索

Nodejs应用接入Skywalking实现APM监控

发表于 2021-11-26

Node.js应用接入Skywalking实现APM监控

1:笔者使用的windows,请先自行安装启动Skywalking,下载地址:,下载完成,解压启动Skywalking后,访问http://localhost:8080/(默认配置),到此安装启动 Skywalking 成功。

2:Node.js应用接入,Skywalking 官方提供新的库来接入,原来的模块是 SkyAPM-nodejs 已经不用了,使用新的库 skywalking-backend-js ,官方要求 SkyWalking backend (OAP) 8.0+ and NodeJS >= 10. 支持已下框架和模块,更多信息查看 README.md

Library Plugin Name
built-in http and https module http / https
Express express
Axios axios
MySQL mysql
MySQL mysql2
PostgreSQL pg
pg-cursor pg-cursor
MongoDB mongodb
Mongoose mongoose
RabbitMQ amqplib
Redis ioredis

3:接入代码示例:完整代码地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
javascript复制代码require('make-promises-safe')
const {default: agent} = require('skywalking-backend-js');
agent.start({ //引用
serviceName: 'my-service-name',
serviceInstance: 'my-service-instance-name',
// collectorAddress: 'http://localhost:8080',
})

var express = require('express')
var app = express();
var port = process.env.PORT || 3001;

var routes = require('./api/routes');
routes(app);
app.listen(port, function() {
console.log('Server started on port: ' + port);
});

4:
示例代码中定义了两个路由:

  1. http://localhost:3001/about
  2. http://localhost:3001/distance/:zipcode1/:zipcode2

运行启动,浏览器访问你的服务路由,可多访问几遍会多一些数据 ,然后刷新 http://localhost:8080/ ,数据可能会有延迟,稍等一些[PS:如果等了很久还是没有数据,检查下你查看的时间区间是否选对]

正常如下:

1637912085(1).jpg

1637912201(1).jpg

从上图可以看到:应用已接入到skywalking监控了,点击各个选项可查看各个功能,链路追踪,性能监控分析等等,redis,mysql,http 的链路都有,可以清楚的看到Node.js应用各个链路所花费的时间,可以更好的监控排查问题。skywalking更多的功能用户自行探索了。

后续:websocket , kafka, rpc , GraphQL 等等Node.js探针是否支持还有待探究。
skywalking-backend-js 也在计划 V0.4.0 版本了,目前已经看到合并了几个PR了,是改Bug的内容和一些小细节,没看到新功能。期待新功能。。。。

本文转载自: 掘金

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

Python爬虫实战,pyecharts模块,Python实

发表于 2021-11-26

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

前言

利用Python实现大江大河评论数据可视化。废话不多说。

让我们愉快地开始吧~

开发工具

Python版本: 3.6.4

相关模块:

requests模块

proxy2808

pandas模块

pyecharts模块;

以及一些Python自带的模块。

环境搭建

安装Python并添加到环境变量,pip安装需要的相关模块即可。

因为豆瓣反爬还是比较严重的

反爬

2808PROXY提供的代理服务

2808PROXY提供的代理服务

没有用代理的话基本就没戏了

分析网页

虽然评论有两万多条,但是豆瓣在登陆的情况下,也只是放出500条数据。

本次只获取全部评论以及差评评论标签页下的数据,合计约为900多条。

网页分析

然后便是获取用户的注册时间。

900多个用户,900多个请求。

我相信不用代理,绝对Game Over。

4.jpg

获取数据

评论及用户信息获取的部分代码

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
python复制代码import time
import requests
import proxy2808
from bs4 import BeautifulSoup

USERNAME = '用户名'
PASSWORD = '密码'

headers = {
'Cookie': '你的Cookie值',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'
}


def get_comments(page, proxy_url_secured):
"""
评论获取
"""
# 热门评论获取
url = 'https://movie.douban.com/subject/26797690/comments?start=' + str(page) + '&limit=20&sort=new_score&status=P'
# 好评获取
# url = 'https://movie.douban.com/subject/26797690/comments?start=' + str(page) + '&limit=20&sort=new_score&status=P&percent_type=h'
# 一般评论获取
# url = 'https://movie.douban.com/subject/26797690/comments?start=' + str(page) + '&limit=20&sort=new_score&status=P&percent_type=m'
# 差评获取
# url = 'https://movie.douban.com/subject/26797690/comments?start=' + str(page) + '&limit=20&sort=new_score&status=P&percent_type=l'
# 使用2808proxy代理
response = requests.get(url=url, headers=headers, proxies={'http': proxy_url_secured, 'https': proxy_url_secured})
soup = BeautifulSoup(response.text, 'html.parser')
for div in soup.find_all(class_='comment-item'):
time.sleep(3)

获取全部评论标签页下的数据(500条)。

数据1

红框部分为用户的注册时间。

假设我能爬取所有评论,那么水军估计要被我逮到了。

个人理解,水军就是过多的新注册用户…

然而豆瓣并没有给我们这个机会。

获取差评标签页的数据(482条)。

数据2

看看给差评的用户注册时间。

相较好评的用户注册时间,有那么点意思了。

注册时间相对都比较晚。

分析情感

评论的情感分析使用百度的自然语言处理。

情感分析

下面利用网站做个示例。

示例

具体的可以去官网看文档,这里只是简述一番。

通过你的百度账号登陆百度的AI开发平台,新建自然语言处理项目。

获取「API Key」及「Secret Key」后。

调用情感倾向分析接口,得到情感结果。

部分代码

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
python复制代码import urllib.request
import pandas
import json
import time


def get_access_token():
"""
获取百度AI平台的Access Token
"""
# 使用你的API Key及Secret Key
host = 'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=[API Key]&client_secret=[Secret Key]'
request = urllib.request.Request(host)
request.add_header('Content-Type', 'application/json; charset=UTF-8')
response = urllib.request.urlopen(request)
content = response.read().decode('utf-8')
rdata = json.loads(content)
return rdata['access_token']


def sentiment_classify(text, acc):
"""
获取文本的感情偏向(消极 or 积极 or 中立)
参数:
text:str 本文
"""
raw = {"text":"内容"}
raw['text'] = text
data = json.dumps(raw).encode('utf-8')
# 情感倾向分析接口
host = "https://aip.baidubce.com/rpc/2.0/nlp/v1/sentiment_classify?charset=UTF-8&access_token=" + acc
request = urllib.request.Request(url=host, data=data)
request.add_header('Content-Type', 'application/json')
response = urllib.request.urlopen(request)
content = response.read().decode('utf-8')
rdata = json.loads(content)
return rdata

情感分析结果如下。

9.jpg

总的来说5星评分的结果多为正向(2)的。

当然也出现了一些负向(0)的结果。

不过还是在可接受范围内。

结果

1星评分的评论情感倾向多为负向。

这里把正向的用红框圈出来了,大家可以自行体会。

毕竟机器的识别水平有限,想达到100%识别,可能性几乎为0。

数据可视化

评论日期分布情况

全部短评-500评论日期分布

全部差评-500评论日期分布

热评随着电视剧的开播,便慢慢没有什么变化。

而差评却在后头有一些波动。

评论时间分布情况

全部短评-500评论日间分布

全部差评-500评论日间分布

大部分评论都是在晚上评论的,符合常态。

评论评分情况

全部短评-500评论评分情况

全部差评-500评论日间分布

全部短评的5星评分占大头。

全部差评的1星和2星占大头。

评论情感分析情况

全部短评-500评论情感分析情况

全部差评-500评论情感分析

其中「2」代表积极的,「1」代表中性的,「-2」代表消极的。

全部短评的正向结果占大头。

全部短评的排序是基于点赞数而来的。

所以对于整部剧,大家还是比较认可的。

评论用户注册时间

全部短评-用户注册时间分布

全部差评-用户注册时间分布

生成评论词云

好评词云

好评词云

差评词云

差评词云

本文转载自: 掘金

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

从零开始学java - 第二十六天 java9 新特性

发表于 2021-11-26

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

今天继续~

java9 新特性

模块系统

  • 一个包的容器,是java9最大的变化之一
  • 模块的代码被组织成多个包,每个包中包含Java类和接口
  • 模块的数据为一些资源文件和其他的静态信息

REPL(JShell)

  • 此英文的意思为交互式编程环境
  • 它允许你无需使用类或者方法包装来执行Java,可直接输入表达式并查看执行结果
  • 类似PythonIDE

改进的Javadoc

  • java9的Javadoc命令中的-html5参数可以让生成的文档支持HTML5

多版本兼容jar包

  • 多版本兼容可以实现在使用不同版本的Java时会动态的使用不同版本的class

集合工厂方法

  • Java9的List,Set和Map接口中,新的静态工厂方法可以创建这些集合的不可变实例

新的方法创建集合

1
2
3
java复制代码static <E> List<E> of(E e1,E e2,E e3);
static <E> Set<E> of(E e1,E e2,E e3);
static <K,V> Map<K,V> of(K k1,V v1,K k2,V v2,K k3,V v3);

私有接口方法

  • 在Java9中,一个接口能定义如下几种变量/方法:
    • 常量
    • 抽象方法
    • 默认方法
    • 静态方法
    • 私有方法
    • 私有静态方法
  • 接口中存在的私有方法就可以不用非要被实例化出来

改进的进程API

  • Java9向Process API添加了一个名为ProcessHandle的接口来增强Java.lang.Process类
  • ProcessHandle接口的实例标识一个本地进程,它允许查询进程状态并管理进程

改进的Stream API

  • Java9新添加了一些遍历的方法,使流处理更容易,并使用收集器编写复杂的查询

takeWhile方法

  • 使用一个断言作为参数,如果流的执行中与断言相符合的话返回断言前的东西,否则返回一个空的Stream
1
java复制代码default Stream<T> takeWhile(Predicate<? super T>predicate)
1
2
3
4
5
6
7
java复制代码import java.util.stream.Stream;

public class Test{
public static void main(String[] args){
Stream.of("1","2","3","","5").takeWhile(s->!s.isEmpty()).forEach(System.out::print);
}
}
  • 输出123
    ps:流执行1~5,4为空,isEmpty()判断是否为空,加上!取反,所有不为空的放过,为空的拦下

dropWhile方法

  • 与takeWhile作用相反,碰到空的字符串时开始进行输出
1
java复制代码default Stream<T> dropWhile(Predicate<? super T>predicate)
  • 比如上面的例子如果改成dropWhile就只会输出5

iterate方法

  • 可与for循环进行相对应的理解
1
java复制代码static <T> Stream<T> iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next)
1
2
3
4
5
6
java复制代码java.util.stream.IntStream;
public class Test {
public static void main(String[] args) {
IntStream.iterate(3, x -> x < 10, x -> x+ 3).forEach(System.out::println);
}
}

ps:代码输出3,6,9

ofNullable方法

  • 可以防止元素为null的异常,如果不是null正常返回,如果是null返回空
1
java复制代码static <T> Stream<T> ofNullable(T t)

今天就学到这里,明天继续Java9的新特性,晚安~

本文转载自: 掘金

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

Java实现LeetCode 题号:211 - 220

发表于 2021-11-26

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

LeetCode习题集
有些题可能直接略过了,整理一下之前刷leetcode

  1. 添加与搜索单词 - 数据结构设计

设计一个支持以下两种操作的数据结构:

1
2
arduino复制代码void addWord(word)
bool search(word)

search(word) 可以搜索文字或正则表达式字符串,字符串只包含字母 . 或 a-z 。 . 可以表示任何一个字母。

示例:

1
2
3
4
5
6
7
erlang复制代码addWord("bad")
addWord("dad")
addWord("mad")
search("pad") -> false
search("bad") -> true
search(".ad") -> true
search("b..") -> true

说明:

你可以假设所有单词都是由小写字母 a-z 组成的。

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
java复制代码class WordDictionary {

Map<Integer,Set<String>> map = new HashMap<>();//根据字符串长度分开存放
public WordDictionary() {

}
public void addWord(String word) {
int length = word.length();
if(map.get(length)!=null){
map.get(length).add(word);
}else{
Set<String> set = new HashSet<>();
set.add(word);
map.put(length, set);
}
}
public boolean search(String word) {
Set<String> set = map.get(word.length());
if(set==null){ //不存在该长度的字符串,直接返回false;
return false;
}
if(set.contains(word)) return true;
char[] chars = word.toCharArray();
P:for(String s : set){
if(word.length()!=s.length()){
continue;
}
char[] cs = s.toCharArray();
for(int i = 0; i< cs.length; i++){//逐个字符对比
if(chars[i] != '.' && chars[i] != cs[i]){
continue P;
}
}
set.add(word);
return true;
}
return false;
}
}

/**
* Your WordDictionary object will be instantiated and called as such:
* WordDictionary obj = new WordDictionary();
* obj.addWord(word);
* boolean param_2 = obj.search(word);
*/
  1. 单词搜索 II

给定一个二维网格 board 和一个字典中的单词列表 words,找出所有同时在二维网格和字典中出现的单词。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。

示例:

输入:

1
2
3
4
5
6
css复制代码words = ["oath","pea","eat","rain"] and board =
[ ['o','a','a','n'],
['e','t','a','e'],
['i','h','k','r'],
['i','f','l','v']
]

输出: ["eat","oath"]
说明:
你可以假设所有输入都由小写字母 a-z 组成。

提示:

你需要优化回溯算法以通过更大数据量的测试。你能否早点停止回溯?
如果当前单词不存在于所有单词的前缀中,则可以立即停止回溯。什么样的数据结构可以有效地执行这样的操作?散列表是否可行?为什么? 前缀树如何?如果你想学习如何实现一个基本的前缀树,请先查看这个问题: 实现Trie(前缀树)。

1
2
3
go复制代码	首先构建一个字典树,然后在dfs的时候加入字典树,以某个字符串结尾的可以减少搜索次数
这里为什么不用开头用结尾呢, `(●ˇ∀ˇ●)`
结尾在这里就是我搜索完一个,我可以直接比较,如果没有的话,我再返回上一个,
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
java复制代码class TrieNode {
private static final int ALPHABET_SIZE = 26;

TrieNode[] children = new TrieNode[ALPHABET_SIZE];
// 判断这个前缀是不是某个字符串的结尾
boolean isEndOfWord = false;
TrieNode() {
isEndOfWord = false;
for (int i = 0; i < ALPHABET_SIZE; i++)
children[i] = null;
}
}

class Trie {
public TrieNode root;
/** Initialize your data structure here. */
public Trie() {
root = new TrieNode();
}
/** Inserts a word into the trie. */
public void insert(String word) {
TrieNode curNode = root;
int index;
for (int i = 0; i < word.length(); i++) {
index = word.charAt(i) - 'a';
if (curNode.children[index] == null) {
curNode.children[index] = new TrieNode();
}
curNode = curNode.children[index];
}
curNode.isEndOfWord = true;
}
}
class Solution {
public List<String> findWords(char[][] board, String[] words) {
List<String> result = new ArrayList<>();
if (words == null || words.length == 0 || board == null || board.length == 0 || board[0].length == 0)
return result;

Trie trie = new Trie();
for (String temp : words)
trie.insert(temp);

TrieNode root = trie.root;
boolean[][] visited = new boolean[board.length][board[0].length];
Set<String> tempResult = new HashSet<>();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (root.children[board[i][j] - 'a'] != null ) {
dfs(board, visited, i, j, root.children[board[i][j] - 'a'], tempResult, sb);
}
}
}

// 需要把tempResult这个set拷贝到真正的result List中进行返回
Iterator<String> iterator = tempResult.iterator();
while (iterator.hasNext()) {
result.add(iterator.next());
}
return result;
}

private void dfs(char[][] board, boolean[][] visited, int startIInBoard, int startJInBoard
, TrieNode curNode, Set<String> resultSet, StringBuilder curStrBuilder) {
curStrBuilder.append(board[startIInBoard][startJInBoard]);
visited[startIInBoard][startJInBoard] = true;
if (curNode.isEndOfWord) {
resultSet.add(curStrBuilder.toString());
}
// 向上搜索, 如果上面的格子没有被搜索过的话
if (startIInBoard > 0 && !visited[startIInBoard - 1][startJInBoard]
&& curNode.children[board[startIInBoard - 1][startJInBoard] - 'a'] != null) {
dfs(board, visited,startIInBoard - 1, startJInBoard
, curNode.children[board[startIInBoard - 1][startJInBoard] - 'a'], resultSet, curStrBuilder);
}
// 向下搜索
if (startIInBoard < board.length - 1 && !visited[startIInBoard + 1][startJInBoard]
&& curNode.children[board[startIInBoard + 1][startJInBoard] - 'a'] != null) {
dfs(board, visited,startIInBoard + 1, startJInBoard
, curNode.children[board[startIInBoard + 1][startJInBoard] - 'a'], resultSet, curStrBuilder);
}
// 向左搜索
if (startJInBoard > 0 && !visited[startIInBoard][startJInBoard - 1]
&& curNode.children[board[startIInBoard][startJInBoard - 1] - 'a'] != null) {
dfs(board, visited, startIInBoard, startJInBoard - 1
, curNode.children[board[startIInBoard][startJInBoard - 1] - 'a'], resultSet, curStrBuilder);
}
// 向右搜索
if (startJInBoard < board[0].length - 1 && !visited[startIInBoard][startJInBoard + 1]
&& curNode.children[board[startIInBoard][startJInBoard + 1] - 'a'] != null) {
dfs(board, visited, startIInBoard, startJInBoard + 1
, curNode.children[board[startIInBoard][startJInBoard + 1] - 'a'], resultSet, curStrBuilder);
}
// 恢复现场
curStrBuilder.setLength(curStrBuilder.length() - 1);
visited[startIInBoard][startJInBoard] = false;
}
}
  1. 最短回文串

给定一个字符串 s,你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串。

示例 1:

输入: “aacecaaa”
输出: “aaacecaaa”
示例 2:

输入: “abcd”
输出: “dcbabcd”

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
java复制代码class Solution {
public static String shortestPalindrome(String s) {
StringBuilder r = new StringBuilder(s).reverse();
String str = s + "#" + r;
int[] next = next(str);
//如果是回文串 1234321 # 1234321
//next[str.length]=7,r.substring(0,0)=""输出原字符串
//如果 123432 # 234321 next[str.length]=5
//r.substring(0,6-5),只需要第一位
String prefix = r.substring(0, r.length() - next[str.length()]);
return prefix + s;
}

// next数组
//KMP的next[j]=x就是0~x-1与 j-x~j-1 的元素是相同的
//大概是这样
private static int[] next(String P) {
int[] next = new int[P.length() + 1];
next[0] = -1;
int k = -1;
int i = 1;
//next【k】保存的是我上次相等的时候
//不相等的时候我就从我上一次相等的时候就行匹配
//i是快指针,k是慢指针
while (i < next.length) {
if (k == -1 || P.charAt(k) == P.charAt(i - 1)) {
next[i++] = ++k;
} else {
k = next[k];
}
}
return next;
}
}
  1. 数组中的第K个最大元素

在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
示例 2:

输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4
说明:

你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。

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
java复制代码class Solution {
public int findKthLargest(int[] nums, int k) {
if(nums.length < 2){
return nums.length == 1 ? nums[0] : -1;
}
return countingSort(nums, k);
}

private int countingSort(int[] nums, int k){
int max = 0;
int min = 0;
for(int i = 0; i < nums.length; i++){
if(nums[i] > max){
max = nums[i];
}

if(nums[i] < min){
min = nums[i];
}
}

int length = (max - min) + 1;
int[] newArray = new int[length];
//这个数组记录的是与最小的值相差的某位的数量
for(int i = 0; i < nums.length; i++){
newArray[nums[i] - min]++;
}

int j = 0;
for(int i = newArray.length - 1; i >= 0; i--){
//这样最大值就出来了,先从最大值开始
if(newArray[i] > 0){
j = newArray[i] + j;
if(j >= k){
return i + min;
}
}
}
return -1;
}
}
  1. 组合总和 III

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:

所有数字都是正整数。
解集不能包含重复的组合。
示例 1:

输入: k = 3, n = 7
输出: [[1,2,4]]
示例 2:

输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码class Solution {
//结果集
public List<List<Integer>> lists = new ArrayList<>();
//临时集
public List<Integer> list = new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
//回溯入口
backTracking(list, 0, 1, k, n, 0);
return lists;
}
public void backTracking(List<Integer> list, int index, int c, int k, int n, int sum){
//回溯出口
//保证是k个数字的累加,且,和为指定值
if(index == k && sum == n){
//满足添加入集合
List<Integer> currentList = new ArrayList<>();
currentList.addAll(list);
lists.add(currentList);
return;
}
//剪枝操作
if(sum > n){
return;
}
//回溯条件
for(int i = c; i <= 9; ++i){
list.add(i);
sum += i;
backTracking(list, index + 1, i + 1, k, n, sum);
list.remove(list.size() - 1);
sum -= i;
}
}
}
  1. 存在重复元素

给定一个整数数组,判断是否存在重复元素。

如果任何值在数组中出现至少两次,函数返回 true。如果数组中每个元素都不相同,则返回 false。

示例 1:

输入: [1,2,3,1]
输出: true
示例 2:

输入: [1,2,3,4]
输出: false
示例 3:

输入: [1,1,1,3,3,4,3,2,4,2]
输出: true

1
arduino复制代码Java自带集合,set不存放相同元素的内容,如果添加相同内容会返回false,并且不会添加进去
1
2
3
4
5
6
7
8
java复制代码class Solution {
public boolean containsDuplicate(int[] nums) {
Set<Integer> res = new HashSet<Integer>();
for(int i:nums)
res.add(i);
return res.size()<nums.length?true:false;
}
}
  1. 天际线问题

城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。现在,假设您获得了城市风光照片(图A)上显示的所有建筑物的位置和高度,请编写一个程序以输出由这些建筑物形成的天际线(图B)。
在这里插入图片描述
在这里插入图片描述

Buildings Skyline Contour

每个建筑物的几何信息用三元组 [Li,Ri,Hi] 表示,其中 Li 和 Ri 分别是第 i 座建筑物左右边缘的 x 坐标,Hi 是其高度。可以保证 0 ≤ Li, Ri ≤ INT_MAX, 0 < Hi ≤ INT_MAX 和 Ri - Li > 0。您可以假设所有建筑物都是在绝对平坦且高度为 0 的表面上的完美矩形。

例如,图A中所有建筑物的尺寸记录为:[ [2 9 10], [3 7 15], [5 12 12], [15 20 10], [19 24 8] ] 。

输出是以 [ [x1,y1], [x2, y2], [x3, y3], … ] 格式的“关键点”(图B中的红点)的列表,它们唯一地定义了天际线。关键点是水平线段的左端点。请注意,最右侧建筑物的最后一个关键点仅用于标记天际线的终点,并始终为零高度。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。

例如,图B中的天际线应该表示为:[ [2 10], [3 15], [7 12], [12 0], [15 10], [20 8], [24, 0] ]。

说明:

任何输入列表中的建筑物数量保证在 [0, 10000] 范围内。
输入列表已经按左 x 坐标 Li 进行升序排列。
输出列表必须按 x 位排序。
输出天际线中不得有连续的相同高度的水平线。例如 […[2 3], [4 5], [7 5], [11 5], [12 7]…] 是不正确的答案;三条高度为 5 的线应该在最终输出中合并为一个:[…[2 3], [4 5], [12 7], …]

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
java复制代码class Solution {
// 线段树
public List<List<Integer>> getSkyline(int[][] buildings) {
int len = buildings.length;
if (len == 0) return new ArrayList<>();
return segment(buildings, 0, len - 1);
}

private List<List<Integer>> segment(int[][] buildings, int l, int r) {
// 创建返回值
List<List<Integer>> res = new ArrayList<>();

// 找到树底下的结束条件 -> 一个建筑物
if (l == r) {
res.add(Arrays.asList(buildings[l][0], buildings[l][2])); // 左上端坐标
res.add(Arrays.asList(buildings[l][1], 0)); // 右下端坐标
return res;
}

int mid = l + (r - l) / 2; // 取中间值

// 左边递归
List<List<Integer>> left = segment(buildings, l, mid);

// 右边递归
List<List<Integer>> right = segment(buildings, mid + 1, r);

// 左右合并

// 创建left 和 right 的索引位置
int m = 0, n = 0;
// 创建left 和 right 目前的高度
int lpreH = 0, rpreH = 0;
// 两个坐标
int leftX, leftY, rightX, rightY;
while (m < left.size() || n < right.size()) {

// 当有一边完全加入到res时,则加入剩余的那部分
if (m >= left.size()) res.add(right.get(n++));
else if (n >= right.size()) res.add(left.get(m++));

else { // 开始判断left 和 right
leftX = left.get(m).get(0); // 不会出现null,可以直接用int类型
leftY = left.get(m).get(1);
rightX = right.get(n).get(0);
rightY = right.get(n).get(1);
//看我这两个矩形谁靠左
if (leftX < rightX) {
//左面还比以前高,就加左面
if (leftY > rpreH) res.add(left.get(m));
//左面比右面高,我要加入左面点的以及以前右面的的高度,因为我马上就有新高度了2,10
else if (lpreH > rpreH) res.add(Arrays.asList(leftX, rpreH));
// 用我左面的高替换我以前右面的高
lpreH = leftY;
m++;
} else if (leftX > rightX) {
if (rightY > lpreH) res.add(right.get(n));
else if (rpreH > lpreH) res.add(Arrays.asList(rightX, lpreH));
rpreH = rightY;
n++;
} else { // left 和 right 的横坐标相等
if (leftY >= rightY && leftY != (lpreH > rpreH ? lpreH : rpreH)) res.add(left.get(m));
else if (leftY <= rightY && rightY != (lpreH > rpreH ? lpreH : rpreH)) res.add(right.get(n));
lpreH = leftY;
rpreH = rightY;
m++;
n++;
}
}
}
return res;
}

}
  1. 存在重复元素 II

给定一个整数数组和一个整数 k,判断数组中是否存在两个不同的索引 i 和 j,使得 nums [i] = nums [j],并且 i 和 j 的差的绝对值最大为 k。

示例 1:

输入: nums = [1,2,3,1], k = 3
输出: true
示例 2:

输入: nums = [1,0,1,1], k = 1
输出: true
示例 3:

输入: nums = [1,2,3,1,2,3], k = 2
输出: false

1
markdown复制代码	滑动窗口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码class Solution {
public boolean containsNearbyDuplicate(int[] nums, int k) {
Set<Integer> set = new HashSet<>();

for (int i = 0; i < nums.length; i++) {
if(set.contains(nums[i])){
return true;
}
set.add(nums[i]);
if(set.size() == k+1){
set.remove(nums[i - k]);
}

}

return false;
}
}
  1. 存在重复元素 III

给定一个整数数组,判断数组中是否有两个不同的索引 i 和 j,使得 nums [i] 和 nums [j] 的差的绝对值最大为 t,并且 i 和 j 之间的差的绝对值最大为 ķ。

示例 1:

输入: nums = [1,2,3,1], k = 3, t = 0
输出: true
示例 2:

输入: nums = [1,0,1,1], k = 1, t = 2
输出: true
示例 3:

输入: nums = [1,5,9,1,5,9], k = 2, t = 3
输出: false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码class Solution {
public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
// 滑动窗口结合查找表,此时滑动窗口即为查找表本身(控制查找表的大小即可控制窗口大小)
TreeSet<Long> set = new TreeSet<>();
for (int i = 0; i < nums.length; i++) {
// 边添加边查找
// 查找表中是否有大于等于 nums[i] - t 且小于等于 nums[i] + t 的值
Long ceiling = set.ceiling((long) nums[i] - (long) t);
if (ceiling != null && ceiling <= ((long) nums[i] + (long) t)) {
return true;
}
// 添加后,控制查找表(窗口)大小,移除窗口最左边元素
set.add((long) nums[i]);
if (set.size() == k + 1) {
set.remove((long) nums[i - k]);
}
}
return false;
}
}

本文转载自: 掘金

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

LeetCode 328 奇偶链表【c++/java详细题

发表于 2021-11-26

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

1、题目

给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。

请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1)O(1)O(1),时间复杂度应为 O(nodes)O(nodes)O(nodes),nodesnodesnodes 为节点总数。

示例 1:

1
2
rust复制代码输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL

示例 2:

1
2
rust复制代码输入: 2->1->3->5->6->4->7->NULL 
输出: 2->3->6->7->1->5->4->NULL

说明:

  • 应当保持奇数节点和偶数节点的相对顺序。
  • 链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。

2、思路

(链表) O(n)O(n)O(n)

根据节点编号的奇偶性,我们可以将奇数节点和偶数节点分离成奇数链表和偶数链表,然后将偶数链表连接在奇数链表之后,合并后的链表即为结果链表。


具体过程如下:

1、从前往后遍历整个链表,遍历时维护四个指针:奇数链表头结点,奇数链表尾节点,偶数链表头结点,偶数链表尾节点。


2、遍历时将位置编号是奇数的节点插在奇数链表尾节点后面,将位置编号是偶数的节点插在偶数链表尾节点后面。

  • 具体可以先定义一个p指针,让p指针向指向链表第三个节点,即p = head->next->next。
  • 奇数链表尾节点oddTail的next指针指向p节点,并后移一位,即oddTail = oddTail->next = p。
  • p指针后移一位。
  • 偶数链表尾节点evenTail的next指针指向p节点, 并后移一位,即evenTail = evenTail->next = p。
  • p指针后移一位,重复上述过程。

最终我们就将奇偶链表分离开来,如下图所示:


3、遍历完整个链表后,将偶数链表头结点插在奇数链表尾节点后面即可。

1
2
c复制代码oddTail->next  = evenHead;  //将偶数链表头结点插在奇数链表尾节点后面
evenTail->next = nullptr; //偶数链表尾节点指向null


时间复杂度分析: 整个链表只遍历一次,所以时间复杂度是O(n)O(n)O(n),遍历时只记录了常数个额外的指针,所以额外的空间复杂度是 O(1)O(1)O(1)。

3、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
c复制代码/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* oddEvenList(ListNode* head) {
if(!head || !head->next) return head;
auto oddHead = head, oddTail = oddHead; //定义4个链表指针
auto evenHead = head->next, evenTail = evenHead;

for(ListNode* p = head->next->next; p;)
{
oddTail = oddTail->next = p;
p = p->next;
if(p) //当前节点不为空
{
evenTail = evenTail->next = p;
p = p->next;
}
}
oddTail->next = evenHead; //将偶数链表头结点插在奇数链表尾节点后面
evenTail->next = nullptr;
return oddHead;
}
};

4、java代码

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
java复制代码/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode oddEvenList(ListNode head) {
if(head == null|| head.next == null) return head;
ListNode oddHead = head, oddTail = oddHead; //定义4个链表指针
ListNode evenHead = head.next, evenTail = evenHead;

for(ListNode p = head.next.next; p!=null;)
{
oddTail = oddTail.next = p;
p = p.next;
if(p != null)
{
evenTail = evenTail.next = p;
p = p.next;
}
}
oddTail.next = evenHead; //将偶数链表头结点插在奇数链表尾节点后面
evenTail.next = null;
return oddHead;
}
}

原题链接: 328. 奇偶链表
在这里插入图片描述

本文转载自: 掘金

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

spring 事务

发表于 2021-11-26

事务的特性

1
2
3
4
markdown复制代码 - 原子性: 原子性是事务的最小执行单位,事务的原子性确保动作要么全部执行完毕,要么就都不执行
- 一致性:确保数据的一致性,一致性其实就是原子性的升华,逻辑差不多,但又不完全相同
- 隔离性:并发访问下,事务之间互不干预,每个事务的独立的
- 持久性:一个事务被提交之后,在数据库中是持久的,并不会因为数据库故障等原因丢失

spring事务管理接口

1
2
3
markdown复制代码 - PlatformTransactionManager 平台事务管理器
- TransactionDefinition 事务定义信息(传播行为、事务隔离级别、只读、超时、回滚规则)
- TransactionStatus 事务状态

PlatformTransactionManager接口介绍

spring 并不直接管理事务,而是提供多种事务管理器,具体的实现交于JTA等持久化机制所提供的平台框架去实现

spring事务管理器的接口是:org.springframework.transaction.PlatformTransactionManager,为各个平台提供事务管理器,具体的实现就需要平台自己实现,

image.png

PlatformTransactionManager提供了三个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public interface PlatformTransactionManager {

//根据传播行为返回当前活动的事务或者创建一个新的事务返回
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;

//提交目前事务的状态
void commit(TransactionStatus status) throws TransactionException;


//对执行的事务进行回滚
void rollback(TransactionStatus status) throws TransactionException;

}

TransactionDefinition介绍

TransactionDefinition中定义了事务的传播行为、隔离级别、超时、只读、回滚规则。 TransactionDefinition中有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
ini复制代码public interface TransactionDefinition {

//事务的传播行为常量
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
//事务的隔离级别
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;
int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED;
int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ;
int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE;

//事务默认超时时间
int TIMEOUT_DEFAULT = -1;


//返回事务的传播行为
int getPropagationBehavior();

//事务的隔离级别
int getIsolationLevel();

//获取事务的超时时间
int getTimeout();

//事务是否只读
boolean isReadOnly();

//返回事务的名称
@Nullable
String getName();

}

TransactionStatus介绍

TransactionStatus中有五个方法,用于来获取事务的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
csharp复制代码public interface TransactionStatus extends SavepointManager, Flushable {

//是否是一个新的事务
boolean isNewTransaction();

//是否有恢复点
boolean hasSavepoint();

//设置为只回滚
void setRollbackOnly();

//是否为只回滚
boolean isRollbackOnly();

//是否已完成
boolean isCompleted();

}

事务的传播行为

事务的传播行为是为了解决业务层之间互相调用的事务问题,当事务方法被另一个事务方法调用时,是在现有的事务中运行,还是创建一个新的事务来运行,在TransactionDefinition中定义了7个传播行为的常量

支持当前事务

  • PROPAGATION_REQUIRED :如果当前存在事务,则加入该事务,如果不存在事务,则创建一个新的事务运行。
  • PROPAGATION_SUPPORTS :如果当前存在事务,则加入该事务,如果不存在事务,则以非事务的方式运行。
  • PROPAGATION_MANDATORY :如果当前存在事务,则加入该事务,如果不存在事务,则抛出异常。(MANDATORY:强制性)

不支持当前事务

  • PROPAGATION_REQUIRES_NEW :创建一个新的事务运行,如果当前存在事务的话,则将当前事务挂起。
  • PROPAGATION_NOT_SUPPORTED :以非事务的方式运行,如果当前存在事务的话,则将当前事务挂起。
  • PROPAGATION_NEVER :以非事务的方式运行,如果当前存在事务的话,则抛出异常

其他情况

  • PROPAGATION_NESTED :如果当前存在事务的话,则创建一个嵌套事务来运行,可以理解为子事务,多个子事务之间互不影响,外部事务回滚子事务不会回滚,子事务回滚外部事务也会回滚,子事务不可以单独提交,他依赖于外部事务,只有通过外部事务的提交,才可以提交子事务

事务的隔离级别

事务的的隔离级别定义了再并发访问下受其他事务所影响的程度,先让我们来看看并发事务下会出现哪些问题

  • 脏读: 当一个事务获取数据并修改了数据,但还未提交到数据库中,此时另一个事务获取到了数据, 然后使用了该数据,因为这个数据是还未提交到数据库中,因此第二个事务读取到的数据就是”脏数据”,基于脏数据的操作结果可能是不正确的
  • 丢失修改: 一个事务在获取数据时,另一个事务也获取了该数据,第一个事务修改了数据并提交到数据库中,另一个事务也修改了数据并提交到数据库中,此时第二个事务修改的数据会覆盖第一个事务修改的数据,造成第一个事务的修改丢失
  • 不可重复读: 当一个事务多次读取数据时,在此事务还没有结束时,另一个事务在此事务两次读取数据之间获取了该数据并进行了修改,此时此数据前后读取到的数据时不一致的,因此成为不可重复读
  • 幻读: 幻读和不可重复读类似,在事务一读取多条数据时,此时事务二向数据库中插入了新数据,在事务一后面的查阅中会多出一些原本不存在的数据,就好像出现幻觉一样,一次称为幻读

不可重复读和幻读的区别
不可重复读的重点在于修改,幻读的重点在于新增或者删除

例1: 当事务1中的A用户去读取的自己的工资为1000的操作还没有完成时,事务2中的B用户将A用户的工资改为2000,导致A在读取自己的工资为2000,这就称为不可重复读

例2:在事务1中去查询工资大于2000的员工,第一次查询为只有4人,此时事务二新增一条工资大于2000的员工数据,此时事务1再次读取数据变成了5条,这称为幻读

spring 事务隔离级别

  • ISOLATION_DEFAULT: 使用数据库自带的默认隔离级别,mysql默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别
  • ISOLATION_READ_UNCOMMITTED: 最低的隔离级别,允许读取未提交的数据变更,可能会产生脏读、不可重复读、幻读等问题
  • ISOLATION_READ_COMMITTED: 只允许读取已提交的数据,可以阻止脏读,可能会产生不可重复读、幻读等问题
  • ISOLATION_REPEATABLE_READ: 对多次读取的结果必须一致,除非时同一个事务所修改的,可以阻止脏读、不可重复读,可能会产生幻读等问题
  • ISOLATION_SERIALIZABLE: 最高隔离级别,每个事务必须逐个执行,根本不可能产生脏读、丢失修改、不可重复读、幻读等问题,但是严重影响了程序性能,实际开发中基本不用这个隔离级别

事务超时属性

事务超时属性定义了事务最大的执行时长,超过执行时长事务会自动回滚,其单位为秒

事务只读属性

事务只读属性标记着对事务性资源只读操作或读写操作,所谓的事务性资源就是数据源、JMS资源以及自定义的事务性资源,如果确定对事务性资源只读操作,那么可以将事务只读属性标记为true,以提高事务处理性能

回滚规则

回滚规则定义了哪些异常会回滚,哪些异常不会回滚,默认情况下只有运行时异常才会回滚,我们可以自定义那些异常回滚,也可以自定义哪些异常不回滚

spring 支持两种方式的事务管理

  • 编程式事务管理

编程式事务管理在实际开发中,几乎不会用到,大多数情况下我们通过注解式事务管理,我们可以使用TransactionTemplate和PlatformTransactionManager来手动管理事务
TransactionTemplate方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typescript复制代码@Resource
private TransactionTemplate transactionTemplate;

@PostMapping("test1")
public Object test1() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
userService.addException(); //业务代码
} catch (Exception e) {
status.setRollbackOnly(); //回滚
}
}
});
return "执行完毕";
}

PlatformTransactionManager方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码@Resource
private PlatformTransactionManager transactionManager;

@PostMapping("test2")
public Object test2() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

try {
userService.addException();
transactionManager.commit(status); //手动提交事务
} catch (Exception e) {
transactionManager.rollback(status); //回滚事务
}
return "执行完毕";
}

声明式事务管理

1
2
3
4
5
6
7
8
9
10
11
less复制代码@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
@Override
public void test() {
add();
try {
addException();
} catch (Exception e) {
log.error("事务回滚了");
}

}

本文转载自: 掘金

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

JVM知识点总结

发表于 2021-11-26

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

1、Object类下有哪些方法

(1)clone方法

保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。

(2)getClass方法

final方法,获得运行时类型。

(3)toString方法

该方法用得比较多,一般子类都有覆盖。

(4)finalize方法

该方法用于释放资源。因为无法确定该方法什么时候被调用,很少使用。

(5)equals方法

该方法是非常重要的一个方法。一般equals和==是不一样的,但是在Object中两者是一样的。子类一般都要重写这个方法。

(6)hashCode方法

该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。
一般必须满足obj1.equals(obj2)==true。可以推出obj1.hash- Code()==obj2.hashCode(),但是hashCode相等不一定就满足equals。不过为了提高效率,应该尽量使上面两个条件接近等价。

(7)wait方法

wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。

调用该方法后当前线程进入睡眠状态,直到以下事件发生。

(1)其他线程调用了该对象的notify方法。

(2)其他线程调用了该对象的notifyAll方法。

(3)其他线程调用了interrupt中断该线程。

(4)时间间隔到了。

此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。

(8)notify方法

该方法唤醒在该对象上等待的某个线程。

(9)notifyAll方法

该方法唤醒在该对象上等待的所有线程。

2、JVM 的主要组成部分及其作用

JVM包含两个子系统和两个组件。

两个子系统为:Class loader(类装载)、Execution engine(执行引擎);

两个组件为:Runtime data area(运行时数据区)、Native Interface(本地接口)。

  • Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
  • Execution engine(执行引擎):执行classes中的指令。
  • Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
  • Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。

作用 :首先通过编译器把 Java 代码转换成字节码,类加载器(Class。Loader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

3、JVM内存模型划分

jvm内存模型共分为虚拟机栈、本地方法栈、堆、方法区和程序计数栈五个部分。

(1)程序计数器(线程私有)

每个线程都有一个独立的程序计数器,计数器所记录的是虚拟机字节码指令当前的地址。

(2)虚拟机栈(线程私有):

每个线程对应一个虚拟机栈,栈中的每一个栈帧对应一个方法。它的生命周期与线程相同。每个方法被执行时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接和方法返回地址等信息。每个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

1)局部变量表

存放了编译器可知的各种基本数据类型(int,long,short,double,float,char,byte,boolean)、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。同时在编译器就确定了局部变量表的最大容量。

2)操作数栈

虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。

3)动态链接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连 接。

4)方法返回地址

当方法被执行后,退出方法有两种:遇到返回字节码指令或者产生异常,并且异常没有在该方法体内得到处理。但无论哪种退出方式都需要返回被调用位置,正常退出时,一般使用栈帧中保存的地址,异常退出时则由异常处理表来确定返回地址。

3.本地方法栈(线程私有)

和虚拟机栈类似,但主要为虚拟机使用到的Native方法服务。

4.Java堆(线程共享)

所有线程共享,在虚拟机创建时启动,用于存放对象的实例。 堆是JVM内存占用最大,管理最复杂的一个区域。唯一的途径就是存放对象实例:所有的对象实例以及数组都在堆上进行分配。

5.方法区(线程共享)

所有方法线程共享的一块内存区域,用于存储已经被虚拟机加载的类信息,常量,静态变量等。

4、描述深克隆和浅克隆区别

浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,

深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,

使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。

5、JAVA堆栈的区别

  • 物理地址

堆的物理地址分配对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩)

栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。

  • 内存分别

堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。

栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。

  • 存放的内容

堆存放的是对象的实例和数组。因此该区更关注的是数据的存储

栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。

  • 程序的可见度

堆对于整个应用程序都是共享、可见的。

栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。

6、描述新生代、老年代、持久代区别

  • 年轻代

所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

  • 年老代

在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

  • 持久代

用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

7、描述强引用、软引用、弱引用、虚引用区别

强引用:发生 gc 的时候不会被回收。(GC的主要作用就是自动释放逻辑堆里实例对象所占的内存)

软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。

弱引用:有用但不是必须的对象,在下一次GC时会被回收。

虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。

  1. 如何判断一个对象应该被回收?

一般有两种方法来判断:

引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;

可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。

  1. 描述类的生命周期

1、加载:查找,并且加载类的二进制数据

2、连接

3、验证:确保类被正确加载

4、准备:为类的静态变量分配内存,并且初始化为默认值

5、解析:把类中的符号引用转化为直接引用

6、初始化:为类的静态变量赋予正确的初始值。

本文转载自: 掘金

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

Spring Boot自动识别打包环境,不再修改配置文件

发表于 2021-11-26

gitee链接

autoPackageDemo,自动识别打包环境

Spring Boot版本:2.3.4.RELEASE

Maven项目

场景

当我们打包项目需要切换环境的时候,通常是在application.yml中修改指定环境:

1
2
3
4
yaml复制代码spring:
profiles:
#   active: dev # 开发环境
  active: pro # 生产环境

目的

我们希望能避免频繁的修改配置文件,改成在打包指令中添加指定环境的方式,像这样:

1
2
3
yaml复制代码spring:
profiles:
  active: @activatedProperties@ # 自动识别环境

打包指令:

mvn clean package -Dmaven.test.skip=true -P pro

实现

只需要修改pom.xml就可以了

pom.xml

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
​
   <groupId>com.cc</groupId>
   <artifactId>autoPackageDemo</artifactId>
   <version>1.0.0</version>
​
   <parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>2.3.4.RELEASE</version>
   </parent>
​
   <dependencies>
       <!--springboot启动器-->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>junit</groupId>
           <artifactId>junit</artifactId>
           <scope>test</scope>
       </dependency>
   </dependencies>
​
   <profiles>
       <profile>
           <id>dev</id>
           <properties>
               <activatedProperties>dev</activatedProperties>
           </properties>
           <activation>
               <!--默认情况下使用dev开发配置-->
               <activeByDefault>true</activeByDefault>
           </activation>
       </profile>
       <!--指定可以用来打包的配置文件1-->
       <profile>
           <id>pro</id>
           <properties>
               <activatedProperties>pro</activatedProperties>
           </properties>
       </profile>
       <!--有其他环境的话就继续添加-->
       <!--...-->
   </profiles>
​
   <build>
       <!--指定打包的配置文件2-->
       <resources>
           <resource>
               <directory>src/main/resources</directory>
               <filtering>true</filtering>
           </resource>
       </resources>
​
       <!--指定包名-->
       <finalName>app</finalName>
       <plugins>
           <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
           </plugin>
       </plugins>
   </build>
</project>

配置文件:

application.yml:

1
2
3
4
5
yaml复制代码server:
port: 8888
spring:
profiles:
  active: @activatedProperties@ # 自动识别环境

application-dev.yml:

1
makefile复制代码myvalue: dev

application-pro.yml:

1
makefile复制代码myvalue: pro

测试

新建接口来测试下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码package com.cc.controller;
​
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
​
@RestController
public class TestController {
   @Value("${myvalue}")
   private String myvalue;
​
   @GetMapping("/env")
   public String env() {
       return "当前的启动环境是:" + myvalue;
  }
}

结果:

  • 在编译器中启动的时候默认是dev环境,请求结果是:
+ 当前的启动环境是:dev
  • 指定pro环境打包的jar包,请求结果是:
+ 当前的启动环境是:pro

注:打包后,如果编译器运行紊乱,尝试执行maven clean以及maven install清理旧缓存。

本文转载自: 掘金

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

6go基础入门-判断(if)、循环(for)、指针(ptr

发表于 2021-11-26

前言

在前面的篇章中,我们已经把基本数据类型都讲完,我们接下来的几个篇章将进入讲述跟容器有点类似的数据类型,还记的我们上一篇说的数组吗?我们上一篇把它比作是一个 凹槽容器,接下来我们要讲的就是 数组 这个类型,为了让我们更好的去学习和理解数组,我们先学习一下在学数组时需要使用到的知识。
(判断、循环、指针都是非常简单的东西,相比于前面的知识,简直就是小菜一碟)

判断(if)

格式:
if 条件 {
…代码
}

是不是很简单,就是当条件成立 时,就执行花括号中的代码,来我们直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码var a int8 = 1
var c int8 = 1
var b bool = false

if a == c {
fmt.Println("条件成立1")
}

if b {
fmt.Println("条件成立2")
}
/*
输出结果:
条件成立1
*/

我们变量 a 和 c 的值都是 1,所以使用 == 对比两个变量的值都是 1,所以条件成立,结果为 true,然后执行花括号中的代码,打印出 条件成立1。

而我们的变量 b 是 bool 布尔类型,并且值是 false,所以当 if 接收到 b 变量之后,判断值是 false,所以不会执行花括号里面的内容。

是不是很好理解?我在举个通俗的例子,我们做白日梦的时候:
==如果,我中了5000万彩票,我就回家养猪。==

  • “如果” 对应的就是 if
  • “我中了5000万” 这个假设对应我们的 条件
  • “就回家养猪” 我们的条件满足(中了5000万),才能这么任性的辞职回家养猪

所以代码也是来源于生活的,我们平常还会这样说:

==我买的彩票 如果,中了5000万,就回家养猪;如果,中了500万,就回家养鱼;都没中,就继续上班写代码吧。== 用代码表达出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码// 彩票中奖金额
var a int32 = 5000

if a == 5000 {
// 中了5000 万,养猪
fmt.Println("回家养猪")
} else if a == 500 {
// 中了 500 万,养鱼
fmt.Println("回家养鱼")
} else {
// 以上都没有中,秃头
fmt.Println("码代码搬砖秃头")
}

if a > 500 {
fmt.Println("我是单独的if")
}
/*
输出结果:
回家养猪
我是单独的if
*/

else 是关键字,用来串联条件,意思是 其它, else if 顾明思意就是 其它 的 判断,通俗的话来说就是 多个假设,我们代码里面叫 多条件,顺序从上往下进行判断,我们执行第一个判断 a == 5000,条件成立,所以 回家养猪。

这里要注意,多条件下(用 else 关键字串联起来),一旦判断条件成立,执行花括号内的代码后,不会再往下执行余下的判断。但 if a > 500 是独立的判断,与上面的一堆并无关联

最后面的 else { … } 又是什么意思呢?这个超简单,就是当前面的 if 条件都不成立时,就会执行 else { … } 的代码。(通俗的讲,就是 彩票没中5000万也没中500万,这个时候我们还是继续敲代码敲到秃头吧)

在后面的篇章中,我们会把学的知识都慢慢的运用起来,这里我们先把它理解。


循环

循环,这个更简单,更容易理解,都说代码来源于生活,生活中的循环就是重复的去做某件事;你在原地转十个圈 == 原地转圈的动作你循环了十次,这两个都是一个意思,可以理解吧? 是不是超级简单?既然理解了,接下来我们就讲讲该怎么让一段代码,重复执行10次。

我们需要在代码中使用 for 关键字

for 条件 { …代码 }

跟我们的 if 非常的像, 只要条件满足,就执行花括号中的代码; for 与 if 的不同点就在于 循环,只要 for 的条件成立,就会再次重复执行for,直到 条件不成立 才会结束执行。看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码// 创建变量a,赋值0
var a int32 = 0

// 判断a是否小于10
for a < 10 {
// 打印a变量的值(Print 不换行打印到控制台)
fmt.Print(a)
// a变量的值加1
a = a + 1
}
// for 执行完毕后继续往下执行代码
fmt.Println()
fmt.Println("for结束了,执行了我")

/*
输出结果:
0123456789
for结束了,执行了我
*/

是不是超级简单, 我们创建了一个 a 变量赋值0,然后执行 for 代码,判断 a 变量是否小于10,第一次执行 a 的值是0,所以条件成立,执行花括号中的代码打印a变量,再让a的值加1,花括号代码执行完毕;开始第二轮循环,继续判断a是否小于10,然后继续执行花括号……;直到 a 的值被加到了10,for 继续执行条判断 a 是否小于 10,条件不成立,所以不再执行 for 代码,进而代码往下执行 fro 或括号外的代码,输出 “for结束了,执行了我”。

非常简单是不是,就三个点:

  • 创建一个变量
  • 判断变量的值
  • 修改变量的值

我们的 for 循环有一种简化的写法,同样可以达到一样的效果:
for 判断前执行; 条件判断; 花括号代码执行后执行 {…}
( a = a + 1 就是 a 变量自身加1, a++ 是它的简写,意思是一样的,自身加1; a– 就是自身减1)

1
2
3
4
go复制代码// 初始化变量a赋值0 | 条件判断a小于10 | {...} 花括号代码执行后a变量累加1
for a:=0; a < 10; a++ {
fmt.Print(a)
}

这个写法跟之前的 for 代码片段是一样的,只要理解了它的格式和执行顺序就超容易理解; for 1; 2; 3 {…} 格式分解:

  • 1 在执行条件判断之前会执行该代码片段
  • 2 判断条件
  • 3 {…} 执行完花括号内的代码之后就会执行该代码片段
1
2
3
go复制代码for true {
fmt.Println("我会一直输出这句话,知道内存不足程序崩掉")
}

各单位注意: 如果条件一直都是 true,就会一直循环操作,这样就会形成了死循环,就是出不来了,你在本地转圈转到死为止(直到程序内存溢出,程序就会卡死、崩掉)。

你看我们能不能做这样两个功能:

  • 功能1: 循环10次,当 a 变量的值为 5 时,输出一句话“斗牛大陆”,并且不再执行本次循环的代码,立马开始下一轮循环。(通俗讲功能1:让你连续吃10顿饭,吃到第6顿的第一个菜时,剩下的菜都不吃了,立马开始吃第7顿饭。使用 continue)
  • 功能2:循环10次,当 a 变量的值为 5 时,立马结束循环,后面的循环不再执行。(通俗讲功能2:让你连续吃10顿饭,吃到第6顿的第一个菜时,后面都不吃了,后面的第7、8、9、10顿都不吃了。使用 break)

上代码:

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
go复制代码
// a == 5 时,那顿饭剩下的菜就不吃了,立马开始下一顿
for a:=0; a < 10; a++ {
if a == 5 {
fmt.Print("斗牛大陆")
continue
}
fmt.Print(a)
}
/*
输出结果:
01234斗牛大陆6789
*/

// a == 5 时,剩下的菜会之后的每一顿饭都不吃了
for a:=0; a < 10; a++ {
if a == 5 {
break
}
fmt.Print(a)
}
/*
输出结果:
01234
*/

是不是超级简单,只需要使用两个关键字就能完成上面的两个功能:

  • continue 跳出本次循环,立马执行下一轮循环
  • break 跳出整个整个for循环,立马开始执行for循环外的代码。

我们这里对 for 的内容暂时告一段落,它还有一种用处,就是把 容器 内的元素一个个取出来,这里先不讲,我们下面讲数组的时候会用到。不知不觉已经讲了三种 for了,总结一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码// 第一种
a := 0
for a < 10 {
fmt.Println(a)
a++
}

// 第二种
for b:=0; b<10; b++ {
fmt.Println(b)
}

// 第三种 死循环
for true {
fmt.Println("直到天荒地老")
}

指针(ptr)

指南针的那根针也叫指针,风水大师那罗盘上的跟针也是指针,指针的意思就是用来做指向的,指南针指向的是南边,风水大师的罗盘指向的也是南边,我们机械手表的时分秒针指向的是方向;所以指针的共同作用就是 指向。我们代码中的指针也是如此,也是指向的意思。

我们代码中的指针是用来指向内存的;之前篇章中,我说过程序运行的一切数据都是存储在内存中的,内存就像一个空间一个房间,我们的数据就像是房间里面的物品。既然这个指针是指向内存的,那我们先把内存简单的讲一下。

内存 (虚拟内存)
我们计算机中有一个叫内存条的配件,内存条上有很多个电容,电容通电代表这个电容存储的是1,电容不通电代表存储0,这是内存条的物理存储方式。cpu会把每个电容在内存条上的位置记录下来并存储在寄存器中,我们程序把数据存储进内存的时候,会先把数据转换成二进制(0和1组成的数字),然后cpu会根据数据的大小,分配对应数据大小的内存空间(一组电容的地址)给程序的数据,分别根据0和1给对应的电容进行通电。这就是CPU和内存条的关联,这个内存上的电容,我们可以称为物理内存。那 虚拟内存呢?这个超级好理解,就是CPU寄存器上存储的电容位置,我们把它称为虚拟内存(这里只做简单的结束,有兴趣的去自行学习)。内存指的就是所有电容,我们计算机存储数据时 CPU 把每8个电容的地址组织成一组,我们称之为字节,1个字节其实就是8个电容的地址组成的,所以我们说 1个byte 占 8个bit,指的就是这个意思。我们讲的这些跟指针又有什么关系呢?

1
go复制代码var a byte = 1

我们创建一个类型为 byte 的 a 变量,赋值1(var a byte = 1),看看都发生了些什么:

  • 程序创建变量 a ,并向系统申请一个存储 byte 类型数据的内存地址;
    ↓↓↓
  • 系统通过cpu运算生成一个16进制数字,我们把这个16进制数字称为内存地址,cpu再次大发神威把8个还没被内存地址绑定的电容物理地址与这个内存地址进行绑定,他们之间的绑定关系我们也可以叫做 映射,就是把物理地址映射到这个内存地址,他们之间的绑定关系会缓存到寄存器中,然后系统把这个 内存地址(如 0xc00000a0a0)返回给我们的程序;
    ↓↓↓
  • 程序拿到内存地址后会与 a 变量进行绑定,a → 地址(如 0xc00000a0a0) → 内存条电容位置,就是变量 a 指向 内存地址 指向 电容地址;
    ↓↓↓
  • = 1 把数值1转换成一个 8bit 的二进制数 0000 0001,程序告诉系统把 0000 0001 这个数值存储到 0xc00000a0a0 这个地址中;
    ↓↓↓
  • 系统通过cpu运算 把 0xc00000a0a0 虚拟内存地址映射的物理电容地址取出来,并给存储0位的电容断电表示0,给存储1位的电容通电表示1;

此时创建变量 a ,然后赋值 1 的步骤就做完了。


1
2
3
4
go复制代码// a → 0xc00000a0a0 → 0000 0001
var a byte = 1
// a → 0xc00000a0a0 → 0000 0010
a = 2

此时我们给 a 变量再赋一个值 a = 2,这个时候会发生些什么呢?

  • 我们的程序 把 0xc00000a0a0 和 0000 0010 传给系统
  • 系统通过命令告诉CPU把 0000 0010 存储到 0xc00000a0a0 这个内存地址中,CPU一顿骚操作把映射的物理地址取出来,通过系统告诉硬件去把对应物理地址的电容分别进行断电和通电,让其分别表示0和1

a 赋值 2 就完成了;到此对数据存储和内存使用是不是有个大概认识了,实际中的计算机内存使用要比上面描述更加精彩,有兴趣的可以去自行了解,特别是我们的CPU、系统、内存、数据线、硬件控制等的原来,非常有趣。我们的内存讲解就到此为止了,准备进入我们的 指针 讲解。

内存的一点小知识(不感兴趣执行跳到下面的指针)
这里额外讲一下我们的string,如:
a := ‘天一子’
a = ‘天一子叮叮咚咚’
a 变量申请了一个 内存地址,并赋值 ‘天一子’,那么当我们再赋值 ‘天一子叮叮咚咚’ 的时候,a 的内存地址会发生改变吗?
答案是 不会,内存地址不变,但物理地址的映射会有编号,我们之前说过,一个中文汉字在UTF-8编码下占用3个字节,所以第一次赋值时,3个字符,cpu为这个内存地址分配了 3个字节 * 8bit * 3个电容地址;当再赋值 ‘天一子叮叮咚咚’ 后,由于 3 * 8 * 3 个电容地址装不下,需要 3 * 8 * 7 ,所以会为内存地址分配更多的电容地址。

那我们所说的 指针 又和这内存有什么关系呢?
当然有关系,我们上面说指南针的指针指向的是南方;而我们 Go 语言所说的指针,指向的就是 内存地址,我们的 指针类型 存储的值就是 内存地址;我们还可以使用 & + 变量名 就可以得到 给变量的内存地址了:

1
2
3
4
5
6
7
8
9
10
go复制代码a := "天一子"
// 输出变量的值
fmt.Println(a)
// 输出变量的内存地址(16进制内存地址)
fmt.Println(&a)
/*
输出结果:
天一子
0xc0000881e0
*/

我们来创建一个 string指针类型,并赋值一个指针值:

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码a := "天一子"
// 创建一个 存放string类型内存地址的指针类型
var ptr *string = a&
fmt.Println(ptr)
/*
输出结果:
0xc00003a1f0
*/

var b int8 = 1
// 报错
ptr = &b

由上面可以看出,指针是有类型区分的,而且指针只能存储相同类型的指针值,如果在 int 类型指针变量中存储 string 类型指针值就会报错,所以同一类型指针只能存储同一类型的指针值。创建指针类型 只需要在数据类型前面使用 * 号即可;

上面我们说过,内存地址是一个16进制数,而我们的指针类型存储的值就是这个内存地址,这就意味着我们的 指针类型 其实就是一个数值类型,对应的就是我们的 int 类型,32位系统时对应int32,64位系统时对应int64,只是以16进制数表现出来,所以当你打印指针类型变量时,打印出来的就是一个16进制数,所以上面代码打印变量 ptr 时,输出的是16进制数 0xc00003a1f0。

我们通过 * + 指针变量名 就可以得到指针内存地址的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码
var a string = "天一子"
var ptr *string = &a
// 创建 b 变量并赋值 *pta(b变量和内存地址绑定,就是b变量指向a变量的内存地址)
b := *ptr

// 输出指针的值
fmt.Println(ptr)
/*
输出结果:
0xc00008a040
*/

// 输出指针所指向内存地址的值(此处输出a变量的值)
fmt.Println(b)
/*
输出结果:
天一子
*/

从上面代码我们得知,其实 变量a 和 变量b 都指向了 0xc00008a040内存地址;而 ptr变 的值存储的就是 0xc00008a040 的二进制值;现在我们对指针类型也理解了吧?那这个指针类型有什么用呢?复用内存地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码var a = "aaa"
var b = &a
var c = a
fmt.Println("变量a的地址:", &a)
fmt.Println("变量b的地址:", b)
fmt.Println("变量c的地址:", &c)
fmt.Println(a)
*b = "bbb"
fmt.Println(a)
/*
输出结果:
变量a的地址: 0xc00003a1f0
变量b的地址: 0xc00003a1f0
变量c的地址: 0xc00003a200
aaa
bbb
*/

打印出来 a变量的内存地址 和 b变量指针值 都是一样的(0xc00003a1f0),而打印出来 c变量内存地址是0xc00003a200,由此我们得知 GO 语言的值传递其实是值复制; var c = a 程序把a变量的值提取出来,并向系统获取内存地址,系统通过cpu一顿骚操作拿到电容物理地址并进行通电断电,再把内存地址返回给程序,程序把新的内存地址和变量c进行映射;

*b = “bbb” 为指针指向的内存地址进行赋值,由于 b 变量指向的内存地址和 a变量 的内存地址都是一样的,所以 *b = “bbb” 更新这个内存地址的值时,a变量的值自然也是就变成了 “bbb”,所以最后变量a打印出来的就是 “bbb”。

指针的实际用途多用于结构体传递、数组切片等的场景使用,以后会讲述到。现在就讲述到这里吧,只要在这先对指针类型有个了解,至于以后的实际使用以后讲述到自然就明白了。我们给指针类型做个小总结:

  • 指针类型只能存储同类型数据的指针(var ptr *string = &string)
  • 符号 * + 数据类型 创建指针类型
  • 符号 & + 变量 获取指针(ptr = &a)
  • 符号 * + 变量 获取指针指向内存地址的值(a = *ptr)
  • 指针类型存储的是一个16进制数的地址内存,所以 ptr = int
  • 指针的零值是nil
  • 最终总结:指针就是一个数值类型,值存储的16进制内存地址。

数组(array)

上一章讲字符串的时候说过,字符放在凹槽容器内;而我们接下来要讲的就是这个容器,我们把这种容器叫 数组。下面我们使用 [ ] 中括号创建一个占用3个byte空间的数组,:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码// 使用中括号[],创建一个 byte 类型的数组,容量大小为3
var bArray [3]byte
// 变量名[下标] 给三个空间赋值
bArray[0] = 1
bArray[1] = 2
bArray[2] = 3
fmt.Println(bArray)
fmt.Println(&bArray[0])
fmt.Println(&bArray[1])
fmt.Println(&bArray[2])
/*
输出结果:
[1 2 3]
0xc0000a2058
0xc0000a2059
0xc0000a205a
*/

我们的 数组 如果是用来装byte的,就要声明数据类型,所以要在 [ ] 中括号后面添加数据类型, 数组创建时还需要明确容器的大小,所以中括号内需要传递一个数量,说明这个数组存放多少个元素; [3]string 表示数组可以存储3个字符串类型数据,[3]byte 表示数组可以存储3个byte类型数据。数组内的空间位置下标是 0 开始的,所以我们要往 bArray 的三个空间中存储值或取值时,只要通过 变量名+[下标] 就可以访问到数组空间。
其实只要我们记住几个点,数组就非常容易理解:

  • 数组使用 [ ] 中括号创建
  • 数组只能存储定义类型的数据类型
  • 数组的空间大小 [数量] 定义后不可变
  • 数组下标从 0 开始
  • 变量名+[下标] 就能访问数组内对应的空间,我们把这个空间也叫做 元素

是不是超级简单?我们的数组还有木有其他的创建方式?如果我们 bArray[4] 下标4去取值会怎么样?

1
2
3
4
5
6
7
8
9
10
11
go复制代码// 创建byte类型数组定义3个空间大小,并分别为每个空间赋值
var bArray [3]byte
bArray[0] = 1
bArray[1] = 2
bArray[2] = 3
fmt.Println(bArray)
// 创建数组时并同时为每个空间赋值
rArray := [2]rune{'钟', '离'}
fmt.Println(rArray)
// 访问 bArray数组下标4(报错:Invalid array index 4 (out of bounds for 3-element array))
fmt.Println(bArray[4])

数组创建常使用的两种方法,一种是先创建一个数组分批好内存空间,然后再给每个空间进行赋值;另一种是在创建数组的同时进行赋值。是不是看上去好像第二种比较好用和简单?这是要看使用场景的哦,如果你提前已经明确要存储的值,那么就直接使用第二种;如果你只知道要存储多少个值,并不确定要存储那些值,这种情况就需要使用第一种。

场景一:创建一个数组并存储四个状态值 生、老、病、死

1
go复制代码var status = [4]rune{'生', '老', '病', '死'}

场景二:创建一个大小为1000的int32类型数组,并且给每个空间都赋值1~1000,如 a[0] = 1 给第一个下标0添加值1,依次添加到下标999值1000。如果这个场景由你来做,你会怎么做?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码// 你不会这样做吧?
var bArray = [1000]int32{1, 2 ,...省略代码...,1000}

// 你不会这样做吧?
var bArray [1000]int32
bArray[0] = 1
bArray[1] = 2
...依次赋值 省略代码...
bArray[999] = 1000

// 我们使用上面学的for循环来完成这个场景,使用len函数获取数组长度1000
for int i = 0; i < len(bArray); i++ {
bArray[i] = i + 1
}

在实际项目中,我们经常会用到 for 循环来操作数组,for 循环还有一种用法经常使用(range 关键字):

1
2
3
4
go复制代码// 把 bArray数组的元素循环赋值到变量a中
for a := range bArray {
fmt.Println(a)
}

使用 range 关键字,循环加载出 bArray 数组元素,for 会根据数组类型创建一个同样类型的变量 a,然后再把第一个元素赋值到该变量的内存地址上,下一次循环继续把元素赋值到这个 变量a的内存地址上;这个变量a只创建一次哦,每次循环只是复用内存地址,是不是很熟悉,你猜跟上面的指针有没有关系?有兴趣自己找答案。

文章写到这里,明白指针的用处木有?我之前讲过 go 语言中的值传递其实就是复制,所以 var a = b 会申请一个新的内存地址,并把b变量的值存储到a变量的内存地址中,如果不使用指针的方式复用内存地址的话,那么 for a := range bArray 就要为 a 变量申请1000个内存地址,但如果使用指针去实现的话,只需要用一个内存地址赋值1000次。这样是不是节省了很多内存空间?

我再举一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码// 创建一个存储着两千字符的字典数组
func main() {
var runeArray = [2000]string{"天","风"...省略1998个字符...}
printDictionaryStrByIndex(runeArray, 0)
fmt.Println(runeArray[1])
fmt.Println(&runeArray[1])
}

// 打印数组指定下标元素
func printDictionaryStrByIndex(strs [2000]string, index int) {
fmt.Println(strs[index])
fmt.Println(&strs[index])
}
/*
输出结果:
风
0xc000098430
风
0xc000098470
*/

我们输出两个数组同一个下标的内容和地址,虽然输出的内容都是 “风” 当内存地址是不一样的;
我们的数组创建后,实则存储的是一组内存地址,如 var bArray [3]byte 实则= [byte内存地址1, byte内存地址2, byte内存地址3];当代码执行到 printDictionaryRuneByIndex函数 的时候,因为值传递是使用的是值复制,所以 strs变量 此时会先申请2000个内存地址,然后把 runeArray变量 中的值赋值到 runes变量中内存地址的值;这就是问题所在,2个数组变量 runeArray 和 strs 各占用了 2000个内存地址。如果我们使用指针,就可以复用数组了,不用再创建一个数组变量并申请2000个数组空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码// 创建一个存储着两千字符的字典数组
func main() {
var runeArray = [2000]string{"天","风"...省略1998个字符...}
printDictionaryStrByIndex(&runeArray, 0)
fmt.Println(runeArray[1])
fmt.Println(&runeArray[1])
}

// 打印数组指定下标元素
func printDictionaryStrByIndex(strs *[2000]string, index int) {
fmt.Println(strs[index])
fmt.Println(&strs[index])
}
/*
输出结果:
风
0xc000004490
风
0xc000004490
*/

我们使用指针之后,不用额外再申请2000个内存地址并分配内存空间,我们只需要传递一个指针过去,这样可以大大的节省我们的内存开销,如果指针类型使用的好,还是在某些场景下可以提升一定的性能。要知道我们的内存是很宝贵的哦。

至此我们的 if判断、for循环、ptr指针、array数组讲述完毕。

本文转载自: 掘金

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

ElasticSearch7——文本分析 一、什么是文本分析

发表于 2021-11-26

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

这是ElasticSearch7:Text analyis文档的译文,根据资料与个人理解梳理而成。


一、什么是文本分析(Text analysis)

ElasticSearch中的文本分析是指将非结构化文本转换为针对搜索进行优化的结构化数据的过程。

Elasticsearch 在索引或搜索text类型字段时,执行文本分析。如果原生JSON数据不包含text类型的字段,则不需要考虑文本搜索的配置与优化。如果需要搜索text类型:

  • 构建搜索引擎
  • 挖掘非结构化数据
  • 优化特定语言的搜索
  • 优化特定专业的搜索

相关概念

  • token:表示分词过程产生的对象,包括词语、词语在文本中的开始、结束位置以及词语类型。在Lucene 2.9后,官方不再建议使用Token,而推荐使用 Attuibutes。
  • term:是一个最小搜索单元,包含文本中的词语以及对应属性名
  • tokenization(分词):用于语句划分;例如:这是一个es的测试语句->【'这是', '一个', 'es','的','测试','语句'】
  • Normalization(tokenization normalization):将意思相同但是形式不同的词转换成同一个词(token)

分词(tokenization)

文本分析通过分词使全文搜索成为可能:将文本分解为更小的块,称为词语。在大多数情况下,这些词语是单独的词。如果不进行分词,假如文本是测试搜索语句,那么测试语句就无法匹配这个文本。

文本正规化(Normalization)

分词支持对单个term的匹配,但是每个token依旧按字面值进行匹配的,这意味着:

  • 搜索Quick时无法匹配quick
  • 虽然fox和foxes共享相同的词根,但无法匹配
  • jumps与leaps是同义词;但搜索时无法匹配

为了解决这些问题,文本分析可以将这些token规范化为标准格式。

自定义文本分析

文本分析由分析器执行,分析器是一组控制整个过程的规则。Elasticsearch 包含一个默认分析器standard analyzer,适用于大多数开箱即用的用例。

自定义分析器可让您控制分析过程的每个步骤,包括:

  • 分词前对文本的更改
  • 文本如何转换为Token
  • 在索引或搜索前,对Token所做的正规化处理

二、相关概念

分析器

无论内置的还是自定义的分析器(analyzer)都由三部分组成,即

  • 字符过滤器(character filters):接收字符流,可以添加、移除或改变字符来转换流
    • 例如:去除字符里的HTML元素
  • 分词器(tokenizers):接收处理后的字符流,分解为单独的 token(通常是单个单词)。
    • 例如:whitespace分词器会在遇到空格时对文本进行拆分;
    • 分词器还负责记录每个term的顺序或位置以及该term所代表的原始单词的开始和结束字符偏移量;
    • 一个分析器有且只有一个分词器。
  • token过滤器(token filters):token过滤器在遇到token流时,可以添加、删除或改变Token
    • 例如,lowercase令牌过滤器将所有令牌转换为小写;stop令牌过滤器从令牌流中删除常用词(停用词)the;synonym令牌过滤器将同义词引入令牌流。
    • Token过滤器不允许更改每个Token的位置或字符偏移量
    • 分析器可能有零个或多个 token过滤器,按序生效

内置分析器将这些构建块预先打包成适用于不同语言和文本类型的分析器。Elasticsearch 还公开了各个构建块,以便将它们组合起来定义新的自定义分析器。

分析器.png

索引与搜索

在使用时会触发文本分析两次:

  • 索引时间:对文档编制索引时,会分析任何text类型的字段值
  • 搜索时间(查询时间):在字段上运行全文搜索时,会先分析查询字符串(用户正在搜索的文本)

分析器.png

每次使用的分析器或分析规则集分别称为索引分析器或搜索分析器。在大多数情况下,应该在索引和搜索时使用相同的分析器。这样可确保字段的值和查询字符串会转换为相同形式的Token。但是,有时候也有使用指定分析器的需求,例如:

1
2
3
4
5
6
7
8
9
10
11
12
perl复制代码# 指定分析器 
GET my-index-000001/_search
{
"query": {
"match": {
"message": {
"query": "Quick foxes",
"analyzer": "stop"
}
}
}
}

词干

词干提取是将单词简化为其词根形式的过程,确保了搜索时单词能匹配所有的变体。例如,walking和walked可以词干到相同的词根: walk。一旦词干化,出现的任何一个词都会在搜索中与另一个词匹配。

在 Elasticsearch 中,词干提取由词干分词过滤器处理。这些Token过滤器可以根据它们的词干方式进行分类:

  • 算法词干分析器,词干分析基于一组规则
  • 词典词干分析器,通过在词典中查找词干

词干提取是英文语料预处理的一个步骤,但是中文分词并不需要,暂时略过。

Token Graphs

当分词器将字符流转换为token流时,会记录:

  • token的顺序
  • token的开始和结束字符偏移量

使用这些数据,可以生成一个有向无环图。令牌图中,每个位置代表一个节点。每个标记代表一条边或弧,指向下一个位置。

image.png

某些Token过滤器可以向现有token流添加新token,例如同义词。这些同义词通常跨越与现有标记相同的位置。例如:

image.png

一些Token过滤器可以添加跨越多个位置的令牌,可以包括多个词的同义词标记:

  • synonym_graph
  • word_delimiter_graph

image.png

以下令牌过滤器可以添加跨越多个位置的token,但只能添加positionLength=1的token:

  • synonym
  • word_delimiter

image.png

三、文本分析器

默认情况下,Elasticsearch 使用standard分析器进行所有文本分析,提供基于语法的分词器(基于 Unicode 文本分割算法)并且适用于大多数语言。如果标准分析器不符合您的需求,可以尝试 Elasticsearch 的其他内置分析器。

内置分析器

standard Analyzer

standard分析器将文本分为在字边界条件,如通过Unicode文本分割算法定义。它删除了大多数标点符号、小写术语,并支持删除停用词。

分析器.png

例如:

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
bash复制代码POST /_analyze?pretty
{
"analyzer": "standard",
"text": "测试搜索语句"
}

{
"tokens" : [
{
"token" : "测",
"start_offset" : 0,
"end_offset" : 1,
"type" : "<IDEOGRAPHIC>",
"position" : 0
},
{
"token" : "试",
"start_offset" : 1,
"end_offset" : 2,
"type" : "<IDEOGRAPHIC>",
"position" : 1
},
{
"token" : "搜",
"start_offset" : 2,
"end_offset" : 3,
"type" : "<IDEOGRAPHIC>",
"position" : 2
},
{
"token" : "索",
"start_offset" : 3,
"end_offset" : 4,
"type" : "<IDEOGRAPHIC>",
"position" : 3
},
{
"token" : "语",
"start_offset" : 4,
"end_offset" : 5,
"type" : "<IDEOGRAPHIC>",
"position" : 4
},
{
"token" : "句",
"start_offset" : 5,
"end_offset" : 6,
"type" : "<IDEOGRAPHIC>",
"position" : 5
}
]
}

停用词是指在信息检索中,为节省存储空间和提高搜索效率,在处理自然语言数据(或文本)之前或之后会自动过滤掉某些字或词,这些字或词即被称为Stop Words(停用词)。

Simple Analyzer

simple分析仪在任何非字母字符Token,如数字,空格,连字符和撇号,丢弃非字母字符,并将大写为小写。

image.png

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bash复制代码# 测试简单分析器

POST /_analyze?pretty
{
"analyzer": "simple",
"text": "测试搜索语句AAA"
}

{
"tokens" : [
{
"token" : "测试搜索语句aaa",
"start_offset" : 0,
"end_offset" : 9,
"type" : "word",
"position" : 0
}
]
}

Whitespace Analyzer

Whitespace分析器在遇到空白字符时,进行分词。

image.png

例如:

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
bash复制代码# 测试空白分析器

POST /_analyze?pretty
{
"analyzer": "whitespace",
"text": "This is a text"
}

{
"tokens" : [
{
"token" : "This",
"start_offset" : 0,
"end_offset" : 4,
"type" : "word",
"position" : 0
},
{
"token" : "is",
"start_offset" : 5,
"end_offset" : 7,
"type" : "word",
"position" : 1
},
{
"token" : "a",
"start_offset" : 8,
"end_offset" : 9,
"type" : "word",
"position" : 2
},
{
"token" : "text",
"start_offset" : 10,
"end_offset" : 14,
"type" : "word",
"position" : 3
}
]
}

Stop Analyzer

Stop 分析器类似于Whitespace分析器,还可以去除停止词。

image.png

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码# 测试stop分析器
POST /_analyze?pretty
{
"analyzer": "stop",
"text": "This is a TEXT"
}

{
"tokens" : [
{
"token" : "text",
"start_offset" : 10,
"end_offset" : 14,
"type" : "word",
"position" : 3
}
]
}

Keyword Analyzer

keyword分析器是一个“空操作”分析仪,它返回整个输入串作为一个单一Token。该keyword分析仪是不可配置。

image.png

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码# 测试Keyword分析器
POST /_analyze?pretty
{
"analyzer": "keyword",
"text": "This is a TEXT"
}

{
"tokens" : [
{
"token" : "This is a TEXT",
"start_offset" : 0,
"end_offset" : 14,
"type" : "word",
"position" : 0
}
]
}

Pattern analyzer

pattern分析器使用正则表达式分割文本,匹配的是标记分隔符 而不是Token本身,默认为\W+(或所有非单词字符)。

写得不好的正则表达式可能会运行得很慢,甚至会抛出 StackOverflowError 并导致它正在运行的节点突然退出。

image.png

pattern分析器接受以下参数:

pattern Java的正则表达式,则默认为\W+。
flags Java 正则表达式标志
lowercase 术语是否应该小写。默认为true
stopwords 预定义的停用词列表,如_english_或包含停用词列表的数组。默认为_none_
stopwords_path 包含停用词的文件的路径
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
json复制代码POST _analyze
{
"analyzer": "pattern",
"text": "This is a TEXT."
}

{
"tokens" : [
{
"token" : "this",
"start_offset" : 0,
"end_offset" : 4,
"type" : "word",
"position" : 0
},
{
"token" : "is",
"start_offset" : 5,
"end_offset" : 7,
"type" : "word",
"position" : 1
},
{
"token" : "a",
"start_offset" : 8,
"end_offset" : 9,
"type" : "word",
"position" : 2
},
{
"token" : "text",
"start_offset" : 10,
"end_offset" : 14,
"type" : "word",
"position" : 3
}
]
}

其它

除此之外,还有:

  • Language analyzers即语言分析器,可以根据特定语言进行分词;
  • Fingerprint Analyzer即指纹分析器,实现了指纹识别算法,输入文本被小写、规范化以移除扩展字符、排序、去重并连接成单个标记。如果配置了停用词列表,停用词也将被删除。

字符过滤器

字符过滤器用于在将字符流传递给Token前对其进行预处理。

Elasticsearch 有许多内置的字符过滤器,可用于构建 自定义分析器,包括

  • HTML Strip 字符过滤器:用于过滤HTML元素,比如<b>以及&amp;
  • Mapping 字符过滤器:可以设置键值对,当遇到与键一致的字符串时,可以替换该值
  • Pattern Replace 字符过滤器:使用正则表达式进行匹配,匹配后替换

分词器

分词器(tokenizers)会接收处理后的字符流,分解为单独的 token(通常是单个单词),还负责记录以下信息:

  • term的顺序或位置(用于短语和单词邻近查询)
  • 原始单词的开始和结束字符的偏移量(用于突出显示搜索片段)
  • token分类,例如<ALPHANUM>、 <HANGUL>、 或<NUM>

面向词的分词器:

  • Standard Tokenizer:该standard分词器将文本分为单词边界条件,由Unicode文本分割算法定义
  • Letter Tokenizer:按字母进行分词
  • Lowercase Tokenizer:遇到非字母的字符时将文本分成词,但也会将所有词小写
  • Whitespace Tokenizer:按照空白字符进行分词
  • UAX URL Email Tokenizer:类似于standard分词器,不同之处在于它将 URL 和电子邮件地址识别为单个标记。
  • Classic Tokenizer:基于语法的英文分词器
  • Thai Tokenizer:用于将泰语文本分割成单词

部分单词分词器(Partial Word Tokenizers)

这些分词器用于将文本或单词分解为小片段,用于部分单词匹配

  • N-Gram Tokenizer:ngram分词器可以分解文本成单词,能根据文本的步长逐步对写入的文本内容进行约束切割。例如:quick → [qu, ui, ic, ck]
  • Edge N-Gram Tokenizer:可以分解文本成单词,只是解析逻辑不同,便于输入即搜索的业务场景。例如:quick → [q, qu, qui, quic,quick]

结构化文本分词器(Structured Text Tokenizersedit)

通常用于结构化文本,如标识符、电子邮件地址、邮政编码和路径

  • Keyword Tokenizer:“noop”分词器,将输入作为一个单独的token
  • Pattern Tokenizer:使用正则表达式,与单词分隔符匹配时将文本拆分为多个token,或将匹配的文本捕获为token
  • Simple Pattern Tokenizer:使用正则表达式,将匹配文本作为term
  • Simple Pattern Split Tokenizer:使用与simple_pattern分词器相同的受限正则表达式子集,但是匹配的部分用来拆分输入,而不是将匹配部分作为结果返回。
  • Char Group Tokenizer:通过字符集进行分词
  • Path Tokenizer:用于分割路径

四、参考文档

Text analyis

本文转载自: 掘金

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

1…170171172…956

开发者博客

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