-
Notifications
You must be signed in to change notification settings - Fork 0
/
content.json
1 lines (1 loc) · 95.4 KB
/
content.json
1
{"meta":{"title":"yangccBlog","subtitle":"","description":"yangcc 博客","author":"杨超超","url":"https://www.yangcc.top","root":"/"},"pages":[{"title":"categories","date":"2024-04-29T15:17:49.215Z","updated":"2024-04-29T15:17:49.215Z","comments":true,"path":"categories/index.html","permalink":"https://www.yangcc.top/categories/index.html","excerpt":"","text":""},{"title":"about","date":"2021-09-07T11:58:36.000Z","updated":"2021-09-07T11:58:36.000Z","comments":true,"path":"about/index.html","permalink":"https://www.yangcc.top/about/index.html","excerpt":"","text":"作品 基于Electron的本地音乐播放器 兴趣爱好 MineCraft 星露谷物语 编程,主要从事服务端Java开发,会点安卓,JS Linux,喜欢ArchLinux Kde桌面 骑行新手 周杰伦"},{"title":"友链","date":"2021-05-13T06:16:07.000Z","updated":"2021-05-13T06:16:07.000Z","comments":true,"path":"links/index.html","permalink":"https://www.yangcc.top/links/index.html","excerpt":"","text":""},{"title":"tags","date":"2024-04-29T15:17:49.216Z","updated":"2024-04-29T15:17:49.216Z","comments":true,"path":"tags/index.html","permalink":"https://www.yangcc.top/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"Java版本管理","slug":"Java版本管理","date":"2023-07-14T16:00:00.000Z","updated":"2023-07-14T16:00:00.000Z","comments":true,"path":"2427860074/","link":"","permalink":"https://www.yangcc.top/2427860074/","excerpt":"","text":"在接触前端的时候发现了nvm这样的工具,可以一行命令切换使用的node版本,非常方便,作为一个Java程序员,java是否有版本管理工具呢? jenv 官网的解释: jEnv is a command line tool to help you forget how to set the JAVA_HOME environment variable 使用一条命令可以轻松配置JAVA_HOME到环境变量。 用法是: 123456789101112jenv <command> [<args>]# 命令参考:commands 列出所有可用的 jenv 命令local 设置或显示本地应用程序特定的 Java 版本global 设置或显示全局 Java 版本shell 设置或显示 shell 特定的 Java 版本rehash 刷新 jenv shims(安装可执行文件后运行此命令)version 显示当前Java版本及其来源versions 列出 jenv 可用的所有 Java 版本which 显示可执行文件的完整路径whence 列出包含给定可执行文件的所有 Java 版本add 添加 将 JDK 添加到 jenv 中 archlinuxjdk在 archlinux 中提供了命令:archlinuxjdk用来管理jdk版本,参考 archwiki 12345678archlinux-java <COMMAND>COMMAND:status 列出已安装并启用的 Java 环境get 返回设置为默认的 Java 环境的短名称set <JAVA_ENV> 强制 <JAVA_ENV> 设置为默认值unset 取消当前默认 Java 环境fix 修复无效/损坏的默认 Java 环境配置","categories":[{"name":"工具","slug":"工具","permalink":"https://www.yangcc.top/categories/%E5%B7%A5%E5%85%B7/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://www.yangcc.top/tags/Java/"}]},{"title":"设计模式-发布订阅","slug":"设计模式-发布订阅","date":"2023-07-13T16:00:00.000Z","updated":"2023-07-13T16:00:00.000Z","comments":true,"path":"1698904213/","link":"","permalink":"https://www.yangcc.top/1698904213/","excerpt":"","text":"发布订阅观察者模式(Observer Pattern),或者叫发布订阅,是一种行为型设计模式,它定义了一种一对多的依赖关系,使得多个观察者对象可以同时监听和被通知被观察者对象的状态变化。 观察者模式的主要特点如下: 主题和观察者的解耦:观察者模式通过定义抽象主题(Subject)和抽象观察者(Observer)来实现主题和观察者的解耦。主题对象并不直接依赖于具体的观察者,而是依赖于观察者的抽象。这样可以使主题对象和具体观察者对象相互独立,彼此之间的耦合度降低。 一对多的依赖关系:观察者模式中,一个主题对象可以有多个观察者对象订阅并监听它的状态变化。当主题对象的状态发生变化时,所有依赖于它的观察者对象都会收到相应的通知并进行相应的处理。这种一对多的依赖关系使得我们可以方便地增加或删除观察者对象,而不需要修改主题对象的代码。 松散耦合的设计:观察者模式通过松散耦合的设计,使得主题对象和观察者对象之间的依赖关系变得松散。主题对象只需要知道观察者对象实现了特定的观察者接口,而不需要了解具体的观察者对象。这样可以使得系统更加灵活,易于扩展和维护。 发布-订阅机制:观察者模式可以看作是一种发布-订阅(Publish-Subscribe)机制的实现。主题对象充当发布者,观察者对象充当订阅者。主题对象维护着一组观察者对象,并在状态变化时主动通知观察者对象,从而实现发布-订阅的通信机制。 观察者模式在实际应用中非常常见,例如事件驱动编程、GUI开发、消息队列等。它可以帮助我们实现对象之间的松散耦合,提高系统的可维护性、扩展性和灵活性。 概念发布订阅核心就是一个Map或者数组之类的容器,存储所有注册的事件,以及对应的回调。而具体每件事情完成后,告诉事件中心,我要触发某个事件的回调,事件中心从容器中找到对应的事件,然后执行函数。可以发现整体是解耦的状态,事件触发者,和事件的函数是分离的,通过中间者eventManager来沟通。 框架的使用上述概念有没有很熟悉,这个和后端常用中间件MQ,桌面框架electron中的一些地方很像?其实这些地方都用到了这个设计模式。 在我们使用消息队列,无论是rabbitMq还是RocketMq的时候,都是一样的,我们往指定的topic中发送消息,然后监听这个topic的消息,然后处理,和上述图的结构一样,本质就是一个发布订阅的设计模式。 electron框架中通过ipcRenderer来管理事件,send来触发事件,on来注册遇到这个事件要做什么事情 1234//触发事件electron.ipcRenderer.send('eventName', data);//注册事件的处理动作electron.ipcRenderer.send('eventName', func); 日常开发如何使用1 在前端开发中使用在前端开发中,我们会发现组件传值在某些情况异常复杂,甚至无法做到,我们会发现,常见的就是父子传值,如果是两个没有嵌套关系的组件传值呢?貌似很复杂,常常可能就会用一个中介来做这件事,比如:redux,vuex之类的状态管理,这边存,那边取。其实可以直接运用这个设计模式来做到。 代码参考:发布订阅 2 在后端开发中使用在后端开发中,我们常见的一个业务场景就是,做了一件事之后,要做一些事情,比如: 要发mq 要发通知 要存es 要刷新缓存 这些事情常常是异步的,和这件事情没有关系,开发中常常会开启一个线程池,或者发一个mq异步里去处理。这样当然是可以的但是有这些缺点: 整体不够抽象化,没有把这类事情做一个抽象,代码不够规范化。 有些时候要做的事情,比如要发mq,要存es等,没有前后顺序,也就是这些事件是互不干扰的,可以在不同的线程处理,而mq通常创建一个监听后,顺序的去做这些事情,只发挥了一个线程的作用 代码参考:发布订阅 像上述代码示例的内容,在下面Add 事件发布的时候,EventManager就会依次开启线程来调用监听。SendMqEventListener,SendSlsEventListener会分两个线程来执行 12345678public class BusinessCode { public static void main(String[] args) { //省略业务代码 //做完事情之后,发送通知 AddEntity entity = new AddEntity("name","code","desc"); EventManager.publish(EventEnum.ADD,entity); }}","categories":[{"name":"技术","slug":"技术","permalink":"https://www.yangcc.top/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://www.yangcc.top/tags/Java/"},{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.yangcc.top/tags/JavaScript/"},{"name":"设计模式","slug":"设计模式","permalink":"https://www.yangcc.top/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}]},{"title":"规则引擎(一)","slug":"规则引擎(一)","date":"2023-07-08T16:00:00.000Z","updated":"2023-07-08T16:00:00.000Z","comments":true,"path":"4078686809/","link":"","permalink":"https://www.yangcc.top/4078686809/","excerpt":"","text":"因为自己接触的项目做的是一个风控系统,涉猎到了规则,流程等知识,转眼一年多了,也没咋回顾这块知识点,今天就总结下规则引起这块的知识点,本文重点讲解用到的表达式相关的算法的逻辑。 一 表达式和操作符我们日常计算,或者看到的数学运算表达式,都是中缀表达式 ,看起来是这样的:(1 + 2) * 3 + 4,其中,加减乘除这些符号就叫操作符,不过,这样的表达式,不过这样的表达式对于计算机并不好识别,我们可以回顾下,算法中的三种表达式的特点: 1 表达式前缀、中缀和后缀表达式都是用来表示数学表达式的方式,它们的主要区别在于运算符的位置。 前缀表达式(也称为波兰表达式):运算符位于操作数之前。例如,表达式 “2 + 3” 的前缀表示为 “+ 2 3”。 中缀表达式:运算符位于操作数之间。这是我们通常使用的方式,例如,表达式 “2 + 3” 就是中缀表达式。 后缀表达式(也称为逆波兰表达式):运算符位于操作数之后。例如,表达式 “2 + 3” 的后缀表示为 “2 3 +”。 为了更好地理解这些表达式之间的区别,让我们以一个简单的表达式为例:”(4 + 5) * 6”。 中缀表达式:(4 + 5) * 6 前缀表达式: + 4 5 6 后缀表达式:4 5 + 6 * 可以看到,中缀表达式需要使用括号来表示运算符的优先级,而前缀和后缀表达式通过运算符的位置来明确表达式的结构和运算顺序。 对于计算机来说,后缀表达式在求值时更容易处理,因为它不需要括号和优先级的考虑。因此,在编写计算机程序或使用栈数据结构进行表达式求值时,通常会将中缀表达式转换为后缀表达式来简化计算过程。 2 操作符操作符又分为两种:逻辑操作符和算术操作符,逻辑操作符用于执行逻辑运算,主要用于布尔逻辑(真和假的逻辑值)的计算。以下是常见的逻辑操作符: 逻辑与(AND):表示为 “&&” 或 “and”,用于判断多个条件是否同时成立。如果两个条件都为真,则结果为真,否则为假。 逻辑或(OR):表示为 “||” 或 “or”,用于判断多个条件中至少有一个是否成立。如果任何一个条件为真,则结果为真,只有当所有条件都为假时结果才为假。 逻辑非(NOT):表示为 “!” 或 “not”,用于对一个条件的逻辑值进行取反。如果条件为真,则结果为假,如果条件为假,则结果为真。 算术操作符用于执行数学运算,用于处理数值和执行各种数学计算。以下是常见的算术操作符: 加法(Addition):表示为 “+”,用于将两个数值相加。 减法(Subtraction):表示为 “-“,用于从一个数值中减去另一个数值。 乘法(Multiplication):表示为 “*”,用于将两个数值相乘。 除法(Division):表示为 “/“,用于将一个数值除以另一个数值。 取模(Modulus):表示为 “%”,用于计算两个数值相除后的余数。 幂运算(Exponentiation):表示为 “^”,用于将一个数值提升到指定的幂次方。 二 中缀表达式转为后缀表达式工作中会常见到这样的需求:页面配置一个复杂的规则,比如:!A||B&&(C||D) ,存储起来,在使用的时候填充不同的值之后进行运算,这个时候就需要将上述的中缀表达式做转换,变为后缀表达式进行后续运算操作。这个算法的核心如下:创建一个栈和一个存储结果的字符串,栈用来存放操作符,结果用于拼接数字和操作符。然后从左到右遍历表达式,算法有这样的规则: 遇到左括弧( 入栈 遇到右括弧) 循环出栈,直到遇到相匹配的左括弧,将出栈符号拼接到结果字符串 遇到符号,循环出栈,将优先级大于等于当前符号的操作符出栈,遇到小于当前符号的操作符停止循环,将出栈符号拼接到结果字符串,最后将当前符号入栈 遇到字符,则拼接到结果的字符串 当表达式循环结束时,将站内的全部符号依次取出并且拼接到结果的字符串 java实现代码,参考:github代码 过程示例图如下: 三 后缀表达式如何使用上面的算法,我们已经得到了一个后缀表达式,这个表达式的运算逻辑是怎样的呢?我们一起来看下: 我们需要一个栈来存放所有的操作数,从左到右遍历符号,具体运算规则如下: 遇到操作数,则入栈 遇到操作符,则出栈两个操作数进行计算,将计算的结果继续入栈 下面以算术表达式为例,一个整体的运算流程如下: 这样很方便的就实现了一个复杂算术表达式的运算,而逻辑表达式也是一样运算方式,比如||,&& 唯一区别就是每次去出两个字符。 具体在规则引擎中如何使用,请听下次讲解。","categories":[{"name":"技术","slug":"技术","permalink":"https://www.yangcc.top/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://www.yangcc.top/tags/Java/"},{"name":"设计模式","slug":"设计模式","permalink":"https://www.yangcc.top/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"},{"name":"规则引擎","slug":"规则引擎","permalink":"https://www.yangcc.top/tags/%E8%A7%84%E5%88%99%E5%BC%95%E6%93%8E/"},{"name":"算法","slug":"算法","permalink":"https://www.yangcc.top/tags/%E7%AE%97%E6%B3%95/"}]},{"title":"Electron(一)","slug":"Electron(一)","date":"2023-06-21T16:00:00.000Z","updated":"2023-06-21T16:00:00.000Z","comments":true,"path":"2402782093/","link":"","permalink":"https://www.yangcc.top/2402782093/","excerpt":"","text":"Electron(一)Electron是一个js桌面端框架,让html,js 打包为桌面应用成为可能,已经有非常多的应用使用了这门技术,如:vscode,notion,figma,思源笔记等等 一 整体交互形式桌面应用最重要的是什么,或者说和浏览器页面有什么区别?大的来说就是可以和操作系统交互,那么什么叫可以和操作系统交互?让我们回顾下浏览器,浏览器就像一个盒子,里面的所有页面的权利是有限的,就比如, 我想在浏览器自己写的一个html页面通过js函数,在文档这个文件夹下创建一个文件,或者修改一个文件 调用系统通知api,使用系统通知 修改系统音量? 这些都是做不到的,这些都涉及了操作系统的api,那electron是如何做到的呢,我们看下这个图: Electron就相当于一个中间层,为js提供了一个曲线访问系统api的能力,做任何和系统有关的操作,都要通过electron 一个简单的例子,我想通过,文件选择器,拿到一个文件的路径,该如何实现?你可能会想到,浏览器也可以呀,通过选择器,选择一个文件,还要Electron干嘛? 是没问题,可以选到一个文件,但是文件的路径是获取不到的,简单思考下,如果一个普通的网页,点击就能获取到你本机文件的路径,甚至别的操作系统信息,你做为浏览器的开发者会允许这样吗?这样对浏览器使用者来讲显然是非常危险的,点开个网页信息泄密了,甚至电脑上文件丢了。 因此,我们要使用的文件选择器是操作系统的文件选择器,这个显然是js办不到的,因此有了Electron,它提供了这样的api:文档详见:https://www.electronjs.org/zh/docs/latest/api/dialog 12345678910import {dialog} from "electron";function open() { let files = dialog.showOpenDialogSync({ title: '选择文件路径', properties: ['openDirectory', 'multiSelections'] }) console.log(files) return files} 文件选择器是有了,我们该如何通知Electron呢?换句话说就是,上面的js函数是显示层调用不到的,因此Electron提供了一种方式,让我们可以与它交互。这种方式类似js的发布订阅模型,发布一个事件,消费者监听事件。 文档详见:https://www.electronjs.org/zh/docs/latest/api/ipc-renderer js端通过ipcRenderer.send发送一个事件通过Electron 123const electron = window.electronelectron.ipcRenderer.send('事件名称',data) electron端通过ipcMain.on监听js端发送过来的事件,那当这件事情处理完成了,想要告诉js端怎么办?通过event.reply发送一个事件,携带数据给js,和上面的ipcRenderer.send是类似的 123456789import {ipcMain} from "electron" ipcMain.on('事件名称',(event, data)) => { //处理 //回调 event.reply('新的事件名称', 要返回的数据) }) js端该如何处理呢?以React为例:useEffect中,通过ipcRenderer.on,监听这个事件,然后处理,一定要在[]这个useEffect,代表页面创建就运行这个函数,监听这个事件,在return中删除这个事件,不然每次每次页面加载都会创建一个监听,这样会越建越多 12345678910useEffect(() => { //处理Electron端发送的事件 electron.ipcRenderer.on('事件名称', (event, 数据) => { //处理 }); return () => { electron.ipcRenderer.removeAllListeners('事件名称'); }; }, []); 到这里就完成了一个交互,是不是很简单。 二 打包打包全靠配置,这个是我的配置: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596/** * @see https://www.electron.build/configuration/configuration */{ appId: "electron-music", productName: "electron-music", copyright: "Copyright © 2023 ${author}", asar: true, directories: { output: "release", buildResources: "public" }, files: [ "dist" ], win: { icon: "public/icons/music256x256.png", target: [ { target: "dir", arch: [ "x64" ] }, { target: "nsis", arch: [ "x64" ] } ] }, nsis: { "oneClick": false, // 创建一键安装程序还是辅助安装程序(默认是一键安装) "allowElevation": true, // 是否允许请求提升,如果为false,则用户必须使用提升的权限重新启动安装程序 (仅作用于辅助安装程序) "allowToChangeInstallationDirectory": true, // 是否允许修改安装目录 (仅作用于辅助安装程序) "installerIcon": "public/icons/music256x256.png", // 安装程序图标的路径 "uninstallerIcon": "public/icons/music256x256.png", // 卸载程序图标的路径 "installerHeader": "public/icons/music256x256.png", // 安装时头部图片路径(仅作用于辅助安装程序) "installerHeaderIcon": "public/icons/music256x256.png", // 安装时标题图标(进度条上方)的路径(仅作用于一键安装程序) "installerSidebar": "public/icons/music256x256.png", // 安装完毕界面图片的路径,(仅作用于辅助安装程序) "uninstallerSidebar": "public/icons/music256x256.png", // 开始卸载界面图片的路径(仅作用于辅助安装程序) "uninstallDisplayName": "electron-music-${version}", // 控制面板中的卸载程序显示名称 "createDesktopShortcut": true, // 是否创建桌面快捷方式 "createStartMenuShortcut": true, // 是否创建开始菜单快捷方式 "include": "script/installer.nsi", // NSIS包含定制安装程序脚本的路径,安装过程中自行调用 (可用于写入注册表 开机自启动等操作) "script": "script/installer.nsi", // 用于自定义安装程序的NSIS脚本的路径 "deleteAppDataOnUninstall": true, // 是否在卸载时删除应用程序数据(仅作用于一键安装程序) "runAfterFinish": false, // 完成后是否运行已安装的应用程序(对于辅助安装程序,应删除相应的复选框) "menuCategory": false, // 是否为开始菜单快捷方式和程序文件目录创建子菜单,如果为true,则使用公司名称 "perMachine": true, //给机器上所有用户安装 "language": "2052" //安装语言(中文) }, mac: { icon: 'public/icons/music256x256.png', category: 'Productivity', target: [ { target: 'default', arch: [ 'arm64', 'x64' ] } ] }, linux: { icon: "public/icons/music256x256.png", target: [ "AppImage", "tar.gz" ], "category": "Audio", artifactName: "${productName}-Linux-${version}.${ext}" }} 参考: 我的音乐播放器项目 三 注意事项1 在linux下窗口关闭前做一件事看环境,比如kde下是做不到比如隐藏窗口的,在kde下,点击关闭按钮,window对象对直接就销毁了。(曲线方式,我们可以不用系统自带的顶栏,自己实现一个,这样就不会有这样的问题了) 12345app.on('closed', () => { if (process.platform !== 'darwin') { app.quit() }}) 2 linux 下,英伟达显卡涉及到动画的页面报错(暂未解决)报错内容 1libva error: vaGetDriverNameByIndex() failed with unknown libva error, driver_name = (null) ","categories":[{"name":"技术","slug":"技术","permalink":"https://www.yangcc.top/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.yangcc.top/tags/JavaScript/"},{"name":"Electron","slug":"Electron","permalink":"https://www.yangcc.top/tags/Electron/"}]},{"title":"Rust学习(三):结构体枚举和模式匹配","slug":"Rust学习(三)","date":"2022-05-14T16:00:00.000Z","updated":"2022-05-14T16:00:00.000Z","comments":true,"path":"379577363/","link":"","permalink":"https://www.yangcc.top/379577363/","excerpt":"","text":"1 Struct结构体实例化的顺序可以不与定义时候一样,但是所有属性都得实例化。结构体实例声明为可变的时候其所有属性都是可变的。 基本的结构体声明一个结构体 12345678910111213141516struct User{ name : String, email : String, sign_in_count : u64, archive : bool,}fn main(){ let mut user = User{ email : String::from("[email protected]"), name : String::from("ycc"), sign_in_count : 1, archive : false, }; println!("{}",user.email);} struct作为函数的返回值: 12345678fn fun()-> User{ User{ email : String::from("[email protected]"), name : String::from("ycc"), sign_in_count : 1, archive : false, };} 字段初始化简写的方式,省略赋值的操作 12345678910fn build_user(name:String,email:String) ->User{ User{ // email : email, // name : name, email, name, sign_in_count : 1, archive : false, };} 当想基于一个struct来创建一个新的struct的时候,可以使用下面的语法糖: 123456789101112fn main(){ let user1 = User{ email : String::from("[email protected]"), name : String::from("ycc"), sign_in_count : 1, archive : false, }; let user2 = User{ email : String::from("[email protected]"), ..user1 };} Tuple Struct元组结构体,适用于给tuple分类型,让这个元组不同于其他的元组,比如可以声明一个元组结构体来描述,颜色RGB,正方形,圆。。。等等 先复习下元组:元组内可以放入不同类型的值 12345fn main(){ let trple = (1,"a",3); // 元组 let (a,b,c) = trple; // 解构 println!("{}",a)} 元组结构体: 123456fn main(){ // 元组结构体 let white = Color(0,0,0); let orange = Color(2,5,1);}struct Color(i32,i32,i32); 没有任何字段的Struct可以定义一个没有任何内容的struct,叫做 Unit-like struct,可以实现抽象的描述,但是又不需要存储元素内容的场景可以适用。 struct的所有权Struct里面可以放数据,也可以放引用(生命周期),生命周期保证只要struct实例是有效的,那么里面的引用就是有效的,如果使用引用却不使用生命周期就会报错 小例子实现计算矩形的面积: 123456789101112131415struct Rectangle { length: u32, width: u32,}fn get_area(r : &Rectangle) -> u32 { r.length * r.width}fn main() { let r = Rectangle{ length:10, width:10, }; let area = get_area(&r); println!("{}",area);} 注意这里传入的是结构体的引用,因此,在 方法执行结束,main方法依然有 Rectangle的实例r的所有权,因此还可以打印: 1println!("{}",r) 但如果直接这么打印编译会报错:提示没有实现这个接口,或则使用第二种方法 = help: the trait std::fmt::Display is not implemented for Rectangle= note: in format strings you may be able to use {:?} (or {:#?} for pretty-print) instead 而当你使用第二种方式打印的话还会报错: 12println!("{:?}",r)println!("{:#?}",r) //美化打印 提示你没有时间Debug接口,或者使用这个注解,放在结构体上面, = help: the trait Debug is not implemented for Rectangle= note: add #[derive(Debug)] to Rectangle or manually impl Debug for Rectangle 方法方法表示与这个结构体有关的函数,使用关键字 impl来表示,可以有很多个代码快,除了特别的方法,一般的方法第一个参数总是自己,用 &self表示,方法调用可以省略自身的引用入参 self 可以是 借用:&self,也可以是:safe表示获取所有权,也可以是可变的,也就是说和普通的参数没啥区别 1234567891011121314impl Rectangle { fn get_area(&self) -> u32 { self.length * self.width }}fn main() { let r = Rectangle{ length:10, width:10, }; let area = r.get_area(); println!("{}",area);} 方法调用的运算符 在c/c++中方法调用,是这样的:object->someting() 和 (*object).someting()这两者等价 object是个指针,星号表示解指针,变为对象 Rust提供了自动的引用,或者解引用,在调用方法的时候,rust会自动的补上,&,& mut 等,来匹配上方法的入参 关联函数: 123456789101112131415impl Rectangle { fn square(size:u32) -> Rectangle { Rectangle{ length :size, width :size, } }}fn main() { // 关联函数的调用 let squ = Rectangle::square(10); println!("{:#?}",squ)} 2 枚举定义一个枚举,不过看了半天,没有发现和java类似的那种常量形式的枚举定义方式 1234enum IP_ADDRESS{ V4(String,u32), V6,} 标准库的Option <T>枚举包含在预导入模块中的,可以直接使用,其结构为: 1234pub enum Option<T> { None, Some( T),} 当想使用包裹着值的,y变量时候,提示需要转换才行,不能直接使用,这样就保证了空安全的情况 12345678910fn main() { let x : i8 = 5; let y : Option<i8> = Some(5); //let z = y.expect("i8"); let sum = x + z; println!("{}",sum);} 3 模式匹配模式匹配必须穷举所有的可能,否则编译不予通过 1234567891011fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => { println!("Lucky penny!"); 1 } Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, }} 附带值的模式匹配: 12345678910111213141516171819202122232425#[derive(Debug)] // 这样可以立刻看到州的名称enum UsState { Alabama, Alaska, // --snip--}enum Coin { Penny, Nickel, Dime, Quarter(UsState),}fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!("State quarter from {:?}!", state); 25 } }} 可以使用占位符 -来表示其他情况 12345match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => (),} 针对一种情况,可以使用if letif let,代码更为简洁 123456789101112fn main(){ let v = Some(1); match v { Some(3)=> println!("3"), _ => (), } //针对一种情况,可以使用if let if let Some(3) = v { println!("3") }}","categories":[{"name":"技术","slug":"技术","permalink":"https://www.yangcc.top/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"Rust","slug":"Rust","permalink":"https://www.yangcc.top/tags/Rust/"}]},{"title":"Rust学习(二):所有权","slug":"Rust学习(二)","date":"2022-05-13T16:00:00.000Z","updated":"2022-05-13T16:00:00.000Z","comments":true,"path":"2861842578/","link":"","permalink":"https://www.yangcc.top/2861842578/","excerpt":"","text":"1 栈和堆 现代处理器在内存中跳转越少就越快 栈内存(所有存放在栈内存的数据必须有已知的固定的大小) 因为放入栈的数据大小都是固定的,因此入栈的时候很快,因为挨着放就行了 堆内存(heap) 编译时大小未知,或者程序运行时会变化的数据存放在 堆 堆对于内存的组织性差一些,因为大小不固定,或者会改变,因此,需要分配空间,而这就需要操作系统去找到一块足够大的空间 访问数据的时候也更慢,因为有个寻址的过程 函数调用 在调用函数的时候,会将函数的入参传入函数,包括入参的指针信息,然后,函数的这些参数信息会压栈,等执行完成会出栈 2 所有权的规则 所有权存在的原因就是因为上面的堆内存,因为这块是不确定的,有的是需要回收的。 所有权(系统)是 Rust 最为与众不同的特性,对语言的其他部分有着深刻含义。它让 Rust 无需垃圾回收(garbage collector)即可保障内存安全,因此理解 Rust 中所有权如何工作是十分重要的。本章,我们将讲到所有权以及相关功能:借用(borrowing)、slice 以及 Rust 如何在内存中布局数据 每个值都有一个变量,这个变量就是该值的所有者 每个值,同时只能有一个所有者 当所有者超出作用域的时候,这个值会被删除 定义一个字符串hello,其内存结构是这样的 而当将携带此字符串值的变量s1赋值给s2的时候,s1在栈中就不可用,失效了,不同于浅拷贝,这种方式被称为移动(Move) 而如果需要实现深拷贝的功能的话,则需要实现 clone方法,类似java 而针对栈上面的数据比如基本类型:整数,布尔,浮点数,字符等是放在栈上的,不需要拷贝这些东西,因此引出了 复制的概念: 123let x = 5let y = x// 到这里其实x和y都是有效的,因为是放在栈上的数据,对这些数据的复制操作是默认执行的 rust提供了 Copy trait(复制 接口) 如果一个类型实现了这个接口,那么旧的变量在值茶u年底后还是有效的,类似上面的代码 如果一个类型,或者这个类型的一部分实现了 Drop trait(释放 接口),那么这个类型就不允许去实现 Copy trait(复制 接口)了 实现了复制接口的有: 整数类型,布尔,字符,浮点,Tuple(这个元组内的所有类型都是实现了复制接口) 2. 1 所有权和函数将值赋给函数或者变量,在语义是其实是一样的: 12345678910111213141516171819fun main(){ let s = String::from("hello") take1(s)// s 在这里就失效了,因为传递给方法了 let x = 5 take2(x)// 在这里x依然是有效的,因为传递给方法的仅仅是副本}fun take1(str:String){ println!("{}",str)}//方法执行结束,会释放str所在的堆内存fun take2(num:i32){ println!("{}",num)}//什么也不会发生 一个变量的所有权总是符合这样的模式: 把一个值赋给变量的时候就会发生移动(Move) 包含这个堆数据的变量离开作用域,值就会被drap函数清理,除非数据的所有权移动到另一个变量了 如何让,一个函数使用一个值,但是不拥有所有权呢? 简单但麻烦的方式,就是返回一个元组,将入参原封不动的返回 使用Rust的特性 引用(Reference) 2.2 引用使用 &符号,表示使用这个值,但是不获得其所有权 12345678910fn main(){ let s1 = String::from("Hello"); let len = get_lenth(&s1); println!("{} length is {}",s1,len);}fn get_lenth(s: &String) -> usize { s.len()} 这段代码在内存中的形式是这样的: s表示一个指针,这个指针指向另一个指针s1,s1指向堆内存 而引用的内容,是否可以修改呢?,我们尝试在上面的方法内修改变量,会报如下的错: s.push_str(“, World”);| ^^^^^^^^^^^^^^^^^^^^^ s is a & reference, so the data it refers to cannot be borrowed as mutable (S是引用,所以它引用的数据不能作为可变数据来借用) 因为引用,也是存放在栈上的,和变量一样是不可变的,而变量可以通过 mut来表示可变变量,那引用呢?答案是可以的: 12345678910111213fn main(){//首先这个变量是可变的 let mut s1 = String::from("Hello");//其次,这个引用也得是可变的 let len = get_lenth(&mut s1); println!("{} length is {}",s1,len);}// 方法的入参也得是可变的fn get_lenth(s: &mut String) -> usize { s.push_str(", World"); s.len()} 1 可变引用可变引用的限制: 在一个作用域内,对于某块数据,只能有一个可变引用 这样做的好处有就是在编译的时候防止 数据竞争,以下三种情况都满足的情况会发生数据竞争 多个指针同时访问一个数据 至少有一个指针用于写入数据 没有任何机制来同步对数据的访问 但是我们可以通过创建作用域的方式,来允许非同时创建多个可变引用 12345678fn main(){ let mut s = String::from("Hello"); { let m1 = &mut s; } let m2 = &mut s;} 还有一个限制: 不可以同时拥有一个可变引用和不可变引用 2 悬空指针悬空指针(Dangling Pointer),或者叫野指针,指向内存一个地址,但是这块内存已经被回收了,甚至已经分配给别人了,而Rust永远不会出现这个问题 Rust保证了,在引用一个数据的时候,编译器会保证,在引用离开作用域之前,数据不会离开作用域 12345678fn main(){ let s = dangle();}//这里会报错fn dangle() -> &String { let str = String::from("hello"); &str} 上面的代码编译会报错,因为str在方法结束就销毁了,而str的引用却返回了,也就是出现了野指针,这样在rust编译会不通过的 3 切片Rust的一种 不持有所有权的数据类型,英文slice,可以用于解决下面的问题 小例子返回第一个空格的下标,这里其实是有问题的,因为,这个index是不合字符串s绑定的,表示的只是之前算过的值,而不是当前的值,可以看到仅仅是返回一个空格下标这么多代码,还有bug 1234567891011121314151617181920fn main(){ let s = String::from("Hello World"); let index = first_world(&s); s.clear(); println!("{}",index);}fn first_world(str : &String) ->usize{ let bytes = str.as_bytes(); // iter会返回一个迭代器 // enumerate会将迭代器包装为一个元组,1 索引, 2 元素的引用 for (i,&item) in bytes.iter().enumerate() { if item == b' ' { return i; } } str.len()} 字符串切片左闭右开的语法:以及部分语法糖 12345678910fn main(){ let s = String::from("Hello World"); let h1 = &s[0..5]; let w1 = &s[6..11]; //语法糖 let h2 = &s[..5]; let w2 = &s[6..]; let all = &s[..];} 字符串切片在内存中的结构,指向堆内存数据的一部分 改造之前的小例子,使其返回一个字符串切片,字符串切片的类型标识为:&str 1234567891011121314151617fn main(){ let s = String::from("Hello World"); let str = first_world(&s); println!("{}",str);}fn first_world(s: &String) ->&str{ let bytes = s.as_bytes(); // iter会返回一个迭代器 // enumerate会将迭代器包装为一个元组,1 索引, 2 元素的引用 for (i,&item) in bytes.iter().enumerate() { if item == b' ' { return &s[..i]; } } &s[..]} 字符串字面值字符串字面值,是什么类型的呢,没错就是切片 将切片作为参数之前的例子都是字符串引用作为入参: 12fn first_world(s: &String) ->&str{} 其实,可以使用切片作为入参,这样切片可以作为入参,二字符串可以传入完整的切片,入参更为多样了。因为切片还是一种引用嘛,一样的","categories":[{"name":"技术","slug":"技术","permalink":"https://www.yangcc.top/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"Rust","slug":"Rust","permalink":"https://www.yangcc.top/tags/Rust/"}]},{"title":"Rust学习(一):猜数字游戏","slug":"Rust学习(一)","date":"2022-03-13T16:00:00.000Z","updated":"2022-03-13T16:00:00.000Z","comments":true,"path":"2690763544/","link":"","permalink":"https://www.yangcc.top/2690763544/","excerpt":"","text":"一 Rust安装Rust安装极为简单,先下载rustup 之后使用命令下载: 1curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 在vscode下载插件 rust-analyzer即可快速开发 二 cargocargo就是rust的包管理工具,与npm,maven等是一样的,一般rustup来安装rust会自带cargo,常见命令如下 12345678# 创建一个catgo项目cargo new 项目# 编译和执行结果,可以用来检验是否编写错误cargo run # 发布的构建,编译会优化,代码更快,但是编译时间更长cargo build --release# 普通的构建cargo build 三 猜数字游戏12345678910111213141516171819202122232425262728293031323334353637use rand::Rng;use std::cmp::Ordering; //Ordering是一个枚举类型use std::io; //Rng是一个trait 可以认为是接口 //rust默认会倒入prelude这个模块fn main() { println!("猜一个数字"); //i32 u32 i64 都是整形 let number = rand::thread_rng().gen_range(1..101); // loop表示无限循环 loop { let mut guess = String::new(); // io函数读取输 使用expect方法,表示如果result是err则执行 io::stdin().read_line(&mut guess).expect("无法读取行"); //将字符串转为整形 parse方法返回中为Result // 隐藏shadow 隐藏重名的旧变量,从18行开始,变量guess类型就变了 let guess: u32 = match guess.trim().parse() { Ok(num) => num, //_:此通配符表示不关心这个值 Err(_) => continue, }; println!("你猜测的数是:{}", guess); //match表达式:可以让我们根据 枚举值选择不同的操作,就相当于if else //cmp就是compere,比较的意思 match guess.cmp(&number) { //三个枚举分别表示小大等 Ordering::Less => println!("Too Small!"), Ordering::Greater => println!("Too Big!"), Ordering::Equal => { println!("You Win!"); break; } } }} 参考:","categories":[{"name":"技术","slug":"技术","permalink":"https://www.yangcc.top/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"Rust","slug":"Rust","permalink":"https://www.yangcc.top/tags/Rust/"}]},{"title":"在Manjaro上配置conky","slug":"在Manjaro上配置conky","date":"2022-03-05T16:00:00.000Z","updated":"2022-03-05T16:00:00.000Z","comments":true,"path":"503294022/","link":"","permalink":"https://www.yangcc.top/503294022/","excerpt":"","text":"一 下载conky使用pacman,或者yay,或者使用gnome的商店都可以下载: 12sudo pacman -S conkyyay -S conky 在家目录新建一个.conkyrc的文件,写上配置文件即可,效果如下: 二 设置开机自启比如在gnome中工具中设置开机自启,是没有效果的,因为conky需要在系统启动桌面加载好后才启动,因此需要一个延时,因此,在目录 ~/.config/autostart/目录中可以看到很多的开机自启的 desktop文件,因此只需要新建一个 sh设置休眠后启动即可: 在家目录新建一个 conky.sh 12#!/bin/bashsleep 5 && conky 在 ~/.config/autostart/新建一个 dekstop文件 1234[Desktop Entry]Type=ApplicationName=conkyExec=/home/${你的用户名}/conky.sh 这样conky就能正常的启动了 三 相关文件:配置文件: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071alignment top_rightbackground noborder_width 0cpu_avg_samples 16default_color greendefault_outline_color whitedefault_shade_color 000000draw_borders nodraw_graph_borders yesdraw_outline nodraw_shades nouse_xft yesfont Noto Sans Regular:size=8xftfont Noto Sans Regular:size=8override_utf8_locale yesminimum_size 280 5maximum_width 350net_avg_samples 2no_buffers yesout_to_console noout_to_stderr noextra_newline noown_window yesown_window_class Conkyown_window_type desktopown_window_hints undecorated,below,sticky,skip_taskbar,skip_pagerdouble_buffer yesown_window_colour 000000own_window_argb_visual yesown_window_argb_value 0stippled_borders 0update_interval 1.0uppercase nouse_spacer noneshow_graph_scale noshow_graph_range nogap_x 10gap_y 50default_color ffffffdefault_shade_color 000000default_outline_color 000000TEXT${voffset 3}${color EAEAEA}${font Noto Sans Regular:pixelsize=100}${time %H:%M}${voffset -26}${color EAEAEA}${font Noto Sans Regular:pixelsize=40} ${color #7fef94}${time %B} ${font}${voffset -3}${offset 2}${color #fdc92d}${font Noto Sans Regular:pixelsize=30}${time %d号} ${font}${voffset 25}${font Noto Sans Regular:pixelsize=30}${offset -55}${time %A}${color}${hr 1}${font Noto Sans Regular:pixelsize=25}${color #fdc92d}主机名称:${alignr}${color } Linux $kernel${color #fdc92d}主机名称:${alignr}${color } $nodename${color #fdc92d}内核版本:${alignr}${color }$kernel${color #fdc92d}运行时间:${alignr}${color }$uptime####系统####${color lightblue}${font :bold:size=12}${color lightblue}系统${alignr 180}${color}${hr 1}${font Noto Sans Regular:pixelsize=25}${color #fdc92d}CPU:${alignr} $color${cpu}% ${color #78af78}${cpubar 10,60}${color #fdc92d}内存: $color$mem / $memmax ${color}${alignr}$memperc% ${color #78af78}${membar 10,60}${color #fdc92d}根分区: ${color}${fs_free /} / ${fs_size /}${alignr}${color #78af78}${fs_bar 10,60 /}${color #fdc92d}用户分区: $color${fs_free /home} / ${fs_size /home} ${color #78af78}${alignr}${fs_bar 10,60 /home}####网络####${color lightblue}${font :bold:size=12}${color lightblue}网络${alignr 180}${color}${hr 1}${font Noto Sans Regular:pixelsize=25}${color #fdc92d}无线WIFI:${alignr}${color}${font :pixelsize=25} IP:${addr wlp1s0}${color #7fef94}${font Noto Sans Regular:pixelsize=25}下载: ${downspeed wlp1s0} KiB/s ${alignr}上传: ${upspeedf wlp1s0} KiB/s${color #C9C9C9}${downspeedgraph wlp1s0 20,100} ${alignr}${upspeedgraph wlp1s0 20,100}$color总计: ${totaldown wlp1s0} ${alignr}总计: ${totalup wlp1s0}${font Noto Sans Regular:pixelsize=25}${color #fdc92d}本地连接:${alignr} ${color}${font :pixelsize=25} IP:${addr enp2s0}${color #7fef94}${font Noto Sans Regular:pixelsize=25}下载: ${downspeedf enp2s0} KiB/s ${alignr} 上传: ${upspeedf enp2s0} KiB/s${color #C9C9C9}${downspeedgraph enp2s0 20,100} ${alignr}${upspeedgraph enp2s0 20,100}$color总计: ${totaldown enp2s0} ${alignr}总计: ${totalup enp2s0}#随便写点啥#$alignc ${color #056107}${font :bold:size=5}","categories":[{"name":"工具","slug":"工具","permalink":"https://www.yangcc.top/categories/%E5%B7%A5%E5%85%B7/"}],"tags":[{"name":"Linux","slug":"Linux","permalink":"https://www.yangcc.top/tags/Linux/"}]},{"title":"Manjaro上安装mysql","slug":"Manjaro上安装mysql","date":"2022-03-04T16:00:00.000Z","updated":"2022-03-04T16:00:00.000Z","comments":true,"path":"2520100274/","link":"","permalink":"https://www.yangcc.top/2520100274/","excerpt":"","text":"一 下载mysql无论是使用自带商店,还是pacman,yay都是可以的 二 配置 打开终端初始化mysql:复制日志中打印的随机密码 1sudo mysqld --initialize --user=mysql --basedir=/usr --datadir=/var/lib/mysql 然后启动mysql 123456781.首先启动MySQLsudo systemctl start mysqld2.登录mysql -u root -p3.修改密码ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'root';4. 开机自启sudo systemctl enable mysqld.service 添加环境变量 123451.编辑profile文件,配置环境变量export MYSQL_HOME=/usr/local/mysqlexport PATH=$PATH:$MYSQL_HOME/bin2.立即生效source /etc/profile 重启服务 12sudo systemctl restart mysqldmysql -u root -p","categories":[{"name":"工具","slug":"工具","permalink":"https://www.yangcc.top/categories/%E5%B7%A5%E5%85%B7/"}],"tags":[{"name":"Linux","slug":"Linux","permalink":"https://www.yangcc.top/tags/Linux/"}]},{"title":"控制缩放以及多倍图","slug":"系统缩放和多倍图","date":"2022-02-07T16:00:00.000Z","updated":"2022-02-07T16:00:00.000Z","comments":true,"path":"1839521991/","link":"","permalink":"https://www.yangcc.top/1839521991/","excerpt":"","text":"一 系统缩放Javafx针对系统缩放是有优化的,比如在我的小新pro13上默认缩放比例为200%,因此在默认情况下如下的代码: 12primaryStage.setWidth(500);primaryStage.setHeight(500); 展示的宽高是1000x1000,而我们是否可以自己来进行控制呢?也就是很多桌面程序常见的功能:是否禁止屏幕DPI适配 在Javafx中为我们提供了两个虚拟机参数: 1234# 禁止屏幕缩放-Dprism.allowhidpi = false# 默认起始缩放比例:-Dglass.win.minHiDPI = 1 因此只需要在虚拟机启动的时候加上这个参数即可,而Java为我们提供了两种方式,分别是键值对,以及传入一个Properties对象,因此就可以将参数写入Properties文件中,就可以保存用户的设置。 二 多倍图多倍图还是对系统缩放的优化,因为如果图片是固定大小的,如果高分屏缩放很可能是150%,200%,那这样图片就会出现模糊的效果,因此Javafx对此进行了优化只要图片名称后加入 @2x或者 @3x,注意只能是整数倍,就可以实现自动切换,比如下面的代码,加载的其实是 [email protected]这张图片。 123primaryStage.getIcons().add( new Image(getClass().getResource("/img/java.png").toExternalForm())); 注意加载图片的方式要使用非流的方式,因为如果是流就固定的使用那张图片了。 三 参考感谢B站LeeWyatt的讲解,这是视频地址 https://www.bilibili.com/video/BV113411k7Zr","categories":[{"name":"技术","slug":"技术","permalink":"https://www.yangcc.top/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"Javafx","slug":"Javafx","permalink":"https://www.yangcc.top/tags/Javafx/"}]},{"title":"类型擦除","slug":"类型擦除","date":"2021-10-01T16:00:00.000Z","updated":"2021-10-01T16:00:00.000Z","comments":true,"path":"2260828129/","link":"","permalink":"https://www.yangcc.top/2260828129/","excerpt":"","text":"一 泛型 在JAVA中的泛型,在编译的时候,所有的泛型信息都会被抹去。这个过程成为 类型擦除。这样做的原因是为了兼容老版本。 JAVA中泛型的引入主要是为了解决两个方面的问题: 减少类型转换 解决的时重复代码的编写,能够复用算法,泛化。 以类Dad为例,有一成员是泛型,并且有对应的get,set方法。 1234567891011public class Dad<T> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; }} 而在反编译后,代码如下,可以看到类型T全部被Object替换。如果定义的类泛型指定为 <T extend Comparable>,则编译后所有的T替换为Comparable 12345678910111213public class Dad{ private Object value; public Object getValue() { return value; } public void setValue(Object value) { this.value = value; }} 二 类型擦除产生的特性1 编译检查根据引用确定: 既然编译后类型变为Object,因此赋值的时候传递不同类型的值是否可以呢?答案是不可以,因为编译器在编译前会先检查代码中泛型的类型,不符合条件编译不通过。 1234//正确方式List<String> list = new ArrayList<String>();list.add("aaa");list.add(1);//编译报错 而如下这种方式虽然可以通过编译但是会有编译警告,但我们这么用就没意义了,因为存储的类型实际上是Object,如果要强行转换为String是会出现 类型转换异常的。 1234567List list = new ArrayList<String>();list.add("aaa"); //编译警告,未检查的参数list.add(1);for (Object o : list) { System.out.println((String) o); //到第二个参数的时候抛出 类型转换异常} 从上面的例子可以看出,编译器在检查泛型是否合格的时候确实是根据 引用检查的 2 不允许引用传递时改变类型: 既然前面我们知道了编译器检查泛型是否合格是根据 引用来决定是否通过的,那么我们引用传递下换成不同的类型是否是可以的呢? 答案是不可以的,泛型的出现就是为了尽量减少类型转换,这样写代码也就失去了意义。 12List<Object> list1 = new ArrayList<>();List<String> list2 = list1; //编译报错 3 集合会自动类型转换 因为在编译的时候都变为了Object,而我们获取后为何还是我们制定的类型呢?以ArrayList为例,在获取元素前已经做了类型转换,我们不再需要进行类型转换了。 123E elementData(int index) { return (E) elementData[index]; //转换为指定的类型} 4 桥方法解决与多态的冲突 有一个泛型父类如下: 1234567891011public class Dad<T> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; }} 有子类继承它,并且指定泛型类型,我们会发现这样写 @Override注解是通过的,也就是满足重写。但是父类编译后是Object,而我们子类的两个方法是String类型,因为方法重写的规则是 方法的参数是必须是相同类型的,因此这里的set方法其实并不满足重写规则的, 1234567891011121314public class Sub extends Dad<String>{ public Sub() { } @Override public String getValue() { return "hello world"; } @Override public void setValue(String value) { System.out.println("设置value"); }} 将子类反编译后会发现,多出来两个对应的方法。可以看到这两个方法调用了我们重写的方法,实际上这两个方法才满足 @Override,这就是 桥方法,编译后通过这个方法解决了泛型类重写的问题。 123456789101112131415161718192021public class Sub extends Dad { public String getValue() //重写的get方法 { return "hello world"; } public volatile Object getValue() //生成的桥方法 { return getValue(); //调用重写的方法 } public void setValue(String value) //重写的set方法 { System.out.println("设置value"); } public volatile void setValue(Object obj) //生产的桥方法 { setValue((String)obj); //调用重写的set方法 }} 5 泛型类型变量不能是基本数据类型 因为类型擦除后所有的泛型关键字都是要替换成Object或者其子类,反正一定要是引用类型。如果要使用基本类型存储数据可以使用相应的包装类。 6 集合的instanceof编译不通过 因为编译后类型被擦除,无论是 List<String>还是 List<Integer>都变成了 List,因此下面的语句在编译时候是不通过的: 12ArrayList<String> arrayList = new ArrayList<String>();if( arrayList instanceof ArrayList<String>) //编译不通过 7 静态成员或者方法无法声明为泛型 因为泛型类型是创建对象的时候才确定是什么类型的,而静态属性或者方法不需要使用对象调用,无法确定泛型是什么类型的。 而下面这种情况例外:因为方法show的返回值类型是由方法参数决定的,返回值类型就是参数类型。 123456public class Demo<T> { public static <T> T show(T t){ //编译正确 return null; } } 三 如何保存泛型信息 既然泛型信息擦除了,那么反射应该是获取不到类型信息的吧,但是还是能获取到 有一个泛型父类,在构造方法中通过反射获取类型信息 12345678910111213141516public class EntityHandler<T> { public EntityHandler() { //获取class对象 Class<?> clazz = this.getClass(); //获取类型 Type genericSuperclass = clazz.getGenericSuperclass(); System.out.println(genericSuperclass); //EntityHandler<User> //获取泛型信息 ParameterizedType t = (ParameterizedType)clazz.getGenericSuperclass(); Type[] ts = t.getActualTypeArguments(); for (Type type : ts) { System.out.println(type); //class User } }} 子类赋值为了User类型 12345public class UserHandler extends EntityHandler<User>{ public static void main(String[] args) { new UserHandler(); }} 控制台打印: 12test.base.generic.EntityHandler<test.base.generic.User>class test.base.generic.User 答案就是在编译为字节码文件的时候,泛型信息通过 Signature保存了下来 12345678910111213141516{ public test.base.generic.UserHandler(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method EntityHandler."<init>":()V 4: return LineNumberTable: line 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Ltest/base/generic/UserHandler;}Signature: #12 // /EntityHandler<Ltest/User; 1234List<String> l1 = new ArrayList<>();List<Integer> l2 = new ArrayList<>();System.out.println(l1.getClass() == l2.getClass());","categories":[{"name":"技术","slug":"技术","permalink":"https://www.yangcc.top/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://www.yangcc.top/tags/Java/"}]},{"title":"Elasticsearch","slug":"ElasticSearch","date":"2021-09-30T16:00:00.000Z","updated":"2021-09-30T16:00:00.000Z","comments":true,"path":"3542956005/","link":"","permalink":"https://www.yangcc.top/3542956005/","excerpt":"","text":"Elasticsearch一、现存问题1.1 现存问题 海量数据存储 海量数据中完成全文检索 如何实现关键字的高亮显示 海量数据中完成统计操作 1.2 ES的介绍(搜索引擎) Elasticsearch天生分布式,支持海量数据的存储,什么在大数据领域也有应用。 Elasticsearch采用倒排索引的方式去全文检索数据,亿级数据中检索时间在毫秒级别。 Elasticsearch提供了highlight高亮查询方式 Elasticsearch提供了及其丰富和聚合函数 Elasticsearch基于Java实现,搜索功能是基于先进最流行的Lucene实现…… Elasticsearch本身是一套技术栈,ELK中的一个组件,ELK是收集日志的一套技术栈…… 中文官方地址:https://www.elastic.co/cn/elasticsearch/ 二、安装ES&Kibana&IK分词器 采用docker运行Elasticsearch容器和Kibana容器 2.1 安装ES&Kibanadocker-compose.yml 123456789101112131415161718192021222324252627version: "3.1"services: elasticsearch: image: 10.9.12.200:60001/elasticsearch:7.8.0 # restart: always # 只要docker启动,当前容器自动启动 container_name: elasticsearch ports: - 9200:9200 environment: - "discovery.type=single-node" - "ES_JAVA_OPTS=-Xms256m -Xmx256m" volumes: - ./data:/usr/share/elasticsearch/data - ./config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml # 数据卷如果映射文件,需要提前在宿主机中创建好这个文件 kibana: image: 10.9.12.200:60001/kibana:7.8.0 # restart: always container_name: kibana ports: - 5601:5601 environment: - elasticsearch_url=http://192.168.41.98:9200 depends_on: - elasticsearch volumes: - ./config/kibana.yml:/usr/share/kibana/config/kibana.yml 数据卷映射文件的内容 elasticsearch.yml 12cluster.name: "docker-cluster"network.host: 0.0.0.0 kibana.yml 1234server.name: kibanaserver.host: "0"elasticsearch.hosts: [ "http://elasticsearch:9200" ]xpack.monitoring.ui.container.elasticsearch.enabled: true 准备好上述内容后,docker-compose up -d 检测启动是否成功: 2.2 安装IK分词器在kibana中基于RESTful的形式和ES交互 需要在kibana上发送HTTP请求并携带JSON参数与ES交互 在kibana的dev tools上测试分词器 安装IK分词器 下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.8.0/elasticsearch-analysis-ik-7.8.0.zip 需要将zip压缩包解压的内容放到elasticsearch容器内部的 /usr/share/elasticsearch/plugins/ik_analyzer/ 修改docker-compose.yml文件,给elasticsearch添加了一个数据卷,映射到 /usr/local/docker/es_docker/plugins 将zip压缩包扔到Linux中,并且放到数据库内的ik_analyzer目录里,通过unzip解压,重启elasticsearch容器 再次通过IK分词器进行分词 三、ES的存储结构 在ES服务中创建索引,并指定索引的主分片个数,以及备份分片个数。 给索引设置存储数据的结构 上述搞定后,可以向索引中添加文件…… 四、索引操作4.1 创建索引(不指定存储结构)12345678# 创建索引PUT /book{ "settings": { "number_of_shards": 5, "number_of_replicas": 1 }} 4.2 Elasticsearch数据类型 字符串类型: text:文本类型,一般用于搜索…… keyword:关键字,当前值不允许分词…… boolean类型: boolean:没啥说的………… 二进制类型: binary:只支持encoding为Base64格式…… 数值类型: long A signed 64-bit integer with a minimum value of -263 and a maximum value of 263-1. integer A signed 32-bit integer with a minimum value of -231 and a maximum value of 231-1. short A signed 16-bit integer with a minimum value of -32,768 and a maximum value of 32,767. byte A signed 8-bit integer with a minimum value of -128 and a maximum value of 127. double A double-precision 64-bit IEEE 754 floating point number, restricted to finite values. float A single-precision 32-bit IEEE 754 floating point number, restricted to finite values. half_float A half-precision 16-bit IEEE 754 floating point number, restricted to finite values. scaled_float A floating point number that is backed by a long, scaled by a fixed double scaling factor. unsigned_long An unsigned 64-bit integer with a minimum value of 0 and a maximum value of 264-1. 时间类型: date:代表时间类型,根据format设置格式化方式 1"format": "yyyy-MM-dd || yyyy-MM-dd HH:mm:ss || strict_date_optional_time || epoch_millis" ip类型: ip:支持ipv4和ipv6类型 geo类型: geo_point:支持经纬度存储…… 4.3 创建索引(结构化存储)1234567891011121314151617181920212223242526# 创建索引并设置存储结构PUT /novel{ "settings": { "number_of_shards": 5, "number_of_replicas": 1 }, "mappings": { "properties": { "name": { "type": "text", # 这样会使用默认的standard分词器,使用IK需要额外添加信息 "analyzer": "ik_max_word" # 添加这个才会使用IK分词器 }, "author": { "type": "keyword" }, "count": { "type": "long" }, "onsale": { "type": "date", "format": "yyyy-MM-dd || yyyy-MM-dd HH:mm:ss || strict_date_optional_time || epoch_millis" } } }} 创建成功,查看效果 索引构建成功后 主分片个数不允许修改…… 备份分片个数可以随意修改…… 索引中的属性类型是不允许修改的,但是可以追加属性…… 12345678PUT /索引/_mapping{ "properties": { "field": { "type": "datatype" } }} 4.4 查看索引 可以通过图形化界面查看 REST查看 1GET /索引 4.5 删除索引 可以通过图形化界面删除 REST删除 1DELETE /索引 五、文档操作5.1 添加文档12345678910111213141516# 添加文档,指定id添加,自动生成idPOST /novel/_doc/ # 自动生成id{ "name": "斗破苍穹", "author": "天残土豆", "count": 99999, "onsale": "2000-01-01"}POST /novel/_create/1 # 手动设置id{ "name": "斗罗大陆", "author": "唐家三少", "count": 999999, "onsale": "2010-01-01"} 5.2 修改文档1234567# 修改文档POST /novel/_update/1{ "doc": { "name": "倒斗大陆" }} 5.3 删除文档12# 删除文档DELETE /novel/_doc/sXVVyHsB4k1tdHs36v4c 5.4 根据id查询文档12# 根据_id查询文档GET /novel/_doc/1 六、Java操作ES Elasticsearch官方推出了两种Java操作的客户端,采用Rest-High-Level-Client。 还有另一种与Elasticsearch交互客户端,可以写类似SQL的语句,与Elasticsearch交互。 还有另一种与Elasticsearch交互客户端,SpringBoot整合的data-elasticsearch。 6.1 Java连接Elasticsearch服务6.1.1 导入依赖1234567891011121314151617<dependencies> <dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>7.8.0</version> </dependency> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.8.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency></dependencies> 6.1.2 编写配置类1234567891011121314151617public class ESClientUtil { /** * 获取与ES交互的client对象 * @return */ public static RestHighLevelClient getClient(){ HttpHost httpHost = new HttpHost("192.168.41.98",9200); RestClientBuilder restClientBuilder = RestClient.builder(httpHost); RestHighLevelClient restHighLevelClient = new RestHighLevelClient(restClientBuilder); return restHighLevelClient; }} 6.2 添加文档12345678910111213141516171819202122232425262728private final String CREATED = "created";@Testpublic void addDoc() throws IOException { //1. 创建指定的request对象 IndexRequest indexRequest = new IndexRequest(); //2. 设置索引信息,文档id indexRequest.index("novel"); indexRequest.id("3"); //3. 准备具体文档数据 Map source = new HashMap<>(8); source.put("name","明朝"); source.put("author","朱元璋"); source.put("count",88888); // 时间推荐使用年月日时分秒格式,虽然kibana显示有问题,但是查询结果没问题 source.put("onsale",new Date()); indexRequest.source(source); //4. 将request用RestHighLevelClient发送出去,接收ES服务的响应 IndexResponse resp = ESClientUtil.getClient().index(indexRequest, RequestOptions.DEFAULT); //5. 基于响应结果,判断添加是否成功 if (CREATED.equals(resp.getResult().getLowercase())) { System.out.println("文档添加成功!!!"); }} 6.3 修改文档12345678910111213141516171819202122@Testpublic void updateById() throws IOException { //1. 创建指定的request对象 UpdateRequest request = new UpdateRequest(); //2. 设置索引信息,文档id request.index("novel"); request.id("3"); //3. 准备具体文档数据 Map<String, Object> source = new HashMap<>(4); source.put("name","明朝那么多事!"); request.doc(source); //4. 将request用RestHighLevelClient发送出去,接收ES服务的响应 UpdateResponse resp = ESClientUtil.getClient().update(request, RequestOptions.DEFAULT); //5. 基于响应结果,判断操作是否成功 if (UPDATED.equals(resp.getResult().getLowercase())) { System.out.println("文档修改成功!"); }} 6.5 批量操作6.6 根据id查询文档123456789101112131415@Testpublic void get() throws IOException { //1. 创建指定的request对象 GetRequest request = new GetRequest(); //2. 设置索引信息,文档id request.index("novel"); request.id("3"); //3. 将request用RestHighLevelClient发送出去,接收ES服务的响应 GetResponse resp = ESClientUtil.getClient().get(request, RequestOptions.DEFAULT); //4. 基于响应结果,判断操作是否成功 System.out.println(resp.getSourceAsMap());} 七、ES的基本查询7.1 倒排/反向索引7.2 term查询 term是ES最基本的查询,基本上所有的检索方式的底层都是term查询…… 7.2.1 term查询 term查询对比MySQL的话,相当于:where column = ? term就是将用户输入的关键字与ES中的某一个属性做等值比较(当前属性的分词库)。 term不会将用户输入的关键字进行分词,直接拿用户的完整关键字匹配分词库。 term更适合查询keyword类型的属性 1234567891011# term查询POST /sms_logs_index/_search{ "query": { "term": { "smsContent": { "value": "滴滴单车平台" } } }} 7.2.2 terms查询 term查询对比MySQL的话,相当于:where column in (?,?,?) 和terms一致,让用户输入多个关键字去匹配一个属性 123456789101112[[terms查询]]POST /sms_logs_index/_search{ "query": { "terms": { "smsContent": [ "滴滴打车", "平台" ] } }} 7.3 match查询 match查询是使用频率最高的查询方式,match查询的底层还是term查询 match查询会根据查询的field的属性,决定是否将用户输入的关键字进行分词 如果field是keyword类型,match查询不会将用户输入的关键字进行分词 如果field是text类型,match查询会将用户输入的关键字进行分词 7.3.1 match查询 用户输入一个关键字去匹配一个Field 123456789# match查询POST /sms_logs_index/_search{ "query": { "match": { "smsContent": "【招商银行】尊贵的王五先生" } }} 7.3.2 match_all查询 查询全部数据 12345678910111213141516# match_all 查询POST /sms_logs_index/_search{ "query": { "match_all": {} }, "from": 10, # limit的第一个参数 "size": 10, # limit的第二个参数 "sort": [ # 指定根据哪个field做排序,不根据es的分数进行排序…… { "fee": { "order": "asc" } } ]} 7.3.3 multi_match查询 一个值匹配多个Field 提升ES的查询命中率…… 12345678910# multi_matchPOST /sms_logs_index/_search{ "query": { "multi_match": { "query": "银行", "fields": ["corpName","smsContent"] } }} 7.4 range查询 range查询可以实现范围检索 针对数值,时间和IP地址做范围查询 12345678910111213141516171819202122232425262728293031323334353637383940# range 查询# 数值范围POST /sms_logs_index/_search{ "query": { "range": { "fee": { "gte": 1, "lt": 5 } } }}# 时间范围, 时间格式规定好,推荐都用时分秒,skr~~POST /sms_logs_index/_search{ "query": { "range": { "sendDate": { "gte": "2021-09-09 02:11:11", "lte": "2021-09-09 05:59:11", "format": "yyyy-MM-dd HH:mm:ss" } } }}# ip范围POST /sms_logs_index/_search{ "query": { "range": { "ipAddr": { "gt": "9.126.2.8", "lte": "11.126.2.255" } } }} 7.5 Java与ES交互实现查询操作123456789101112131415161718192021222324252627282930313233public class Demo2Test { /** * range范围查询,基于fee,查询1分到5分之间的 */ @Test public void rangeQuery() throws IOException { //1. req SearchRequest request = new SearchRequest(); //2. index request.indices("sms_logs_index"); //3. body SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); sourceBuilder.query(QueryBuilders.rangeQuery("fee").gte(1).lte(5)); sourceBuilder.from(0); sourceBuilder.size(2); sourceBuilder.sort("fee", SortOrder.ASC); request.source(sourceBuilder); //4. execute SearchResponse resp = ESClientUtil.getClient().search(request, RequestOptions.DEFAULT); //5. source for (SearchHit hit : resp.getHits().getHits()) { System.out.println(hit.getSourceAsMap()); } }} 八、ES的其他查询8.1 ids查询 ids是根据多个_id的值,直接拉取分片上的数据…… 123456789# ids查询POST /sms_logs_index/_search{ "query": { "ids": { "values": ["1","2","3"] } }} 8.2 prefix查询 将用户输入的关键字去分词库中匹配term的前缀…… 1234567891011# prefix查询POST /sms_logs_index/_search{ "query": { "prefix": { "corpName": { "value": "养车" } } }} 8.3 fuzzy查询 真正的模块查询,允许用户输入的关键字有错别字(错别字尽量出现在后面……) 1234567891011# fuzzy查询POST /sms_logs_index/_search{ "query": { "fuzzy": { "corpName": { "value": "盒马生鲜" } } }} 8.4 wildcard查询 和MySQL中的like查询一样的通配、占位查询, MySQL%代表通配,_代表占位 wildcard中*代表通配,?代表占位 1234567891011# wildcard查询POST /sms_logs_index/_search{ "query": { "wildcard": { "corpName": { "value": "中国????????" } } }} 8.5 regexp查询 基于正则表达式匹配分词库中的term 123456789# regexp查询POST /sms_logs_index/_search{ "query": { "regexp": { "mobile": "1390[0-9]{7}" } }} 九、ES的复合查询 基于ES的query查询数据时,暂时只能一个条件一个条件的使用,没有办法将多个条件以一定的逻辑方式组合在一起。 ES也支持查询方式,允许多个条件封装到一起,这种查询叫bool查询 只要公司项目用ES做全文检索,100%用bool查询 bool查询提供了四种组合方式: must:等于MySQL的and should:等于MySQL的or must_not:等于MySQL的! filter:在query的筛选基础上,再次做筛选,这次筛选不会计算分数 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859# bool查询# smsContent中包含先生 并且 fee大于等于5分# 省份要么是北京,要么是上海# 公司名称不是 滴滴打车# 将上述结果再次筛选出手机号为 13900000000的POST /sms_logs_index/_search{ "query": { "bool": { "must": [ { "match": { "smsContent": "先生" } }, { "range": { "fee": { "gte": 5 } } } ], "should": [ { "term": { "province": { "value": "北京" } } }, { "term": { "province": { "value": "西安" } } } ], "must_not": [ { "term": { "corpName": { "value": "滴滴打车" } } } ], "filter": [ { "term": { "mobile": "13900000000" } } ], "minimum_should_match": 0 } }} 十、ES的高亮查询 将用户输入的关键字匹配项,以高亮的形式返回。 1234567891011121314151617# 高亮查询POST /sms_logs_index/_search{ "query": { "term": { "corpName": "滴滴打车" } }, "highlight": { "pre_tags": "<span style='color:red;'>", "post_tags": "</span>", "fragment_size": 20, "fields": [ {"corpName": {}} ] }} Java代码 1234567891011121314151617181920212223242526272829303132333435363738394041public class Demo2Test { @Test public void matchQuery() throws IOException { //1. req SearchRequest request = new SearchRequest(); //2. index request.indices("sms_logs_index"); //3. body SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); sourceBuilder.query(QueryBuilders.matchQuery("smsContent","先生")); sourceBuilder.from(0); sourceBuilder.size(2); sourceBuilder.sort("fee", SortOrder.ASC); HighlightBuilder highligter = new HighlightBuilder(); highligter.fragmentSize(20); highligter.field("smsContent"); highligter.preTags("<span style='color:red;'>"); highligter.postTags("</span>"); sourceBuilder.highlighter(highligter); request.source(sourceBuilder); //4. execute SearchResponse resp = ESClientUtil.getClient().search(request, RequestOptions.DEFAULT); //5. source for (SearchHit hit : resp.getHits().getHits()) { System.out.println(hit.getSourceAsMap()); Map<String, HighlightField> highlightFields = hit.getHighlightFields(); HighlightField smsContent = highlightFields.get("smsContent"); if(smsContent != null){ String highlight = smsContent.getFragments()[0].toString(); System.out.println("highlight:" + highlight); } } }} 十一、ES的聚合函数ES提供了丰富的聚合函数在海量数据中做统计 想MySQL提供的5种聚合函数,ES都支持…… ES提供的聚合函数特别多,挑几个常用的…… 11.1 Extended_stats 查询出指定属性的count,min,max,sum,avg,平方和,方差,标准偏差………… 1234567891011# 聚合函数-Extended statsPOST /sms_logs_index/_search{ "aggs": { "heiheihei": { "extended_stats": { "field": "fee" } } }} 11.2 Cardinality 针对非text类型的属性做 去重计数 1234567891011# 聚合函数-CardinalityPOST /sms_logs_index/_search{ "aggs": { "xixixi": { "cardinality": { "field": "createDate" } } }} 11.3 Range统计 ES中针对Range范围统计提供了Range,DateRange,IPRange 可以统计范围内出现数据的数量。 12345678910111213141516171819202122232425262728293031323334# 聚合函数-RangePOST /sms_logs_index/_search{ "aggs": { "hahaha": { "range": { "field": "fee", "ranges": [ {"to": 5}, {"from": 5,"to": 8}, {"from": 8} ] } } }}# 聚合函数-DateRangePOST /sms_logs_index/_search{ "aggs": { "hehehe": { "date_range": { "field": "createDate", "ranges": [ {"to": "2021-09-09 04:00:00"}, {"from": "2021-09-09 04:00:00","to": "2021-09-09 12:00:00"}, {"from": "2021-09-09 12:00:00"} ], "format": "yyyy-MM-dd HH:mm:ss" } } }} 11.4 histogram 根据指定的属性和间隔interval做范围统计 1234567891011121314151617181920212223242526# 聚合函数-histogramPOST /sms_logs_index/_search{ "aggs": { "eee": { "histogram": { "field": "fee", "interval": 1 } } }}# 聚合函数-date_histogramPOST /sms_logs_index/_search{ "aggs": { "yesyesyes": { "date_histogram": { "field": "createDate", "interval": "minute" } } }} 11.5 terms 统计某个属性不同值出现的次数,并且可以基于order做排序,基于size做筛选条数 123456789101112131415# 聚合函数-termsPOST /sms_logs_index/_search{ "aggs": { "yiku": { "terms": { "field": "fee", "size": 2, "order": { "_count": "asc" } } } }} 11.6 Java代码实现 向下转型问题! 12345678910111213141516171819202122232425262728293031323334/** * 聚合函数Java实现 * @author zjw */public class Demo3Test { @Test public void terms() throws IOException { // request SearchRequest request = new SearchRequest(); // index request.indices("sms_logs_index"); // body SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); sourceBuilder.aggregation(AggregationBuilders.terms("agg").field("fee").size(10).order(BucketOrder.count(false))); request.source(sourceBuilder); // send,resp SearchResponse resp = ESClientUtil.getClient().search(request, RequestOptions.DEFAULT); // getData Aggregations aggregations = resp.getAggregations(); // 根据聚合函数的查询条件向下转型 Terms agg = aggregations.get("agg"); for (Terms.Bucket bucket : agg.getBuckets()) { System.out.println(bucket.getKey() +"," + bucket.getDocCount()); } }} 十二、ES的GEO查询 GEO查询就是基于经纬度做筛选。 一般经纬度筛选无法计算分数,会将基于GEO的查询统统滴放到filter中。 创建一个索引,指定一个属性的存储类型是geo_point 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647# 玩GEO,创建索引PUT /map { "settings": { "number_of_shards": 3, "number_of_replicas": 1 }, "mappings": { "properties": { "name": { "type": "keyword" }, "location": { "type": "geo_point" } } }}# 插入三条数据 北科,生命科学园地铁,巩华城地铁POST /map/_create/1{ "name": "北科", "location": { "lat": 40.125318, "lon": 116.258312 }}POST /map/_create/2{ "name": "生命科学园地铁", "location": { "lat": 40.101109, "lon": 116.300721 }}POST /map/_create/3{ "name": "巩华城地铁", "location": { "lat": 40.13731, "lon": 116.300344 }} GEO在ES中提供了三种查询方式 12345678910111213141516171819# 一个点,一个距离(半径)查询圆内数据POST /map/_search{ "query": { "bool": { "filter": [ { "geo_distance": { "distance": 4700, "location": { "lat": 40.125318, "lon": 116.258312 } } } ] } }} 123456789101112131415161718192021222324# 两个点确定矩形POST /map/_search{ "query": { "bool": { "filter": [ { "geo_bounding_box": { "location": { "top_left": { "lat": 40.109973, "lon": 116.278192 }, "bottom_right": { "lat": 40.093416, "lon": 116.32131 } } } } ] } }} 123456789101112131415161718192021222324252627282930# 多个点确定多边形POST /map/_search{ "query": { "bool": { "filter": [ { "geo_polygon": { "location": { "points": [ { "lat": 40.143736, "lon": 116.29084 }, { "lat": 40.127132, "lon": 116.306003 }, { "lat": 40.148479, "lon": 116.313549 } ] } } } ] } }}","categories":[{"name":"技术","slug":"技术","permalink":"https://www.yangcc.top/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://www.yangcc.top/tags/Java/"}]},{"title":"Volatile关键字","slug":"volatile关键字","date":"2021-08-11T16:00:00.000Z","updated":"2021-08-11T16:00:00.000Z","comments":true,"path":"1069363226/","link":"","permalink":"https://www.yangcc.top/1069363226/","excerpt":"","text":"1 内存模型缓存不一致问题计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。 也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。 比如有一段代码: 12// 理想情况是两个线程都执行一次,最后的结果为2i = i +1; 此代码在单线程执行没有任何问题,而在多线程运行会被覆盖。 1234线程1:读取i=0放到高速缓冲区 ——> i+1 ——> 刷线数据到主存,i=1线程2:读取i=0放到高速缓冲区 ——> i+1 ——> 刷线数据到主存,i=1 因为在多核心cpu中每个线程可能运行于不同的cpu,因此有可能每个线程的高速缓存不是同一个。 而单核心cpu,是线程调度的,要么分时执行,要么抢占执行,但结果和多核心都是一样的。 解决方式为了解决缓存不一致性问题,通常来说有以下2种解决方法:这2种方式都是 硬件层面上提供的方式。 通过在总线加LOCK锁的方式 在早期的CPU当中,是通过在总线上加LOCK锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个 CPU能使用这个变量的内存。 通过缓存一致性协议 上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致 效率低下。所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。 2 并发编程原子性与数据库那个原子性一个概念,就是两个操作是一体的,要么都执行,要么都不执行 可见性当多个线程访问同一个变量的时候,一个线程修改了值,另外的线程能够立刻看到修改的值 有序性程序是按照代码的顺序执行的,但JVM在真正执行这段代码的时候不一定能保证是按照顺序执行的,因为可能发生 指令重排序(Instruction Reorder) 处理器为了提高程序运行效率,可能会对输入代码进行 优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。 而处理器在进行重排序时是会考虑指令之间的数据依赖性,如果指定2必须要用到指令1,则指令1一定会在指令2之前运行的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。 想要让程序正常的执行,必须要保证满足这三个条件 3 java内存模型 在Java虚拟机规范中试图定义一种 Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。 总结来说JVM中每个线程都有自己独立的虚拟机栈来执行自己的方法,这样如同操作系统的高速缓冲一样,存在可见性问题。 4 如何保证三个特性原子性1234x =8;y = x; // 包含两步,读取x值,赋值给yx++; // 包含三步,读取x值,x+1,赋值给xx = x+1; 上面四个操作只有操作1是原子操作,java中只有 赋值,和 读取值是原子操作,如果要保证多个操作整体具有原子性,需要同步或者锁来实现(synchronized和Lock) 可见性对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。 另外,还可通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。 有序性在Java内存模型中,允许编译器和处理器对指令进行重排序,单线程程序没有问题,多线程会出现有序性问题。 Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。 下面就来具体介绍下happens-before原则(先行发生原则): 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作 volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C 二 JUC1 关键字volatile一个变量被此关键字修饰后就具备了如下特点: 保证了不同线程对这个变量进行操作时的 可见性,当一个变量被volatile修饰时,那么对它的修改会立刻刷新到主存,当其它线程需要读取该变量时,会去内存中读取新值。 禁止进行指令重排序。 使用volatile关键字并不能保证原子性,如下这段代码,输出的值很随机,正常应该是10000才对。 因为 a++这个代码不是原子性的,包含几个步骤:读取,加1,写入内存,三步。因此,此段代码就有可能这样执行: 某个时刻 a = 10 线程1 ——> 读取a=10 a自增 线程2 ——> 读取到a=10 然后因为此时有别的线程正在进行修改,因此进入阻塞状态 线程3 ——> 此时恰好修改a值那个线程结束了,因此读到是内存中的a = 11,然和进行自增操作为 a=12 线程2 ——> 接着对a进行自增操作,a=11 这样三个线程自增结果却只+1,原因就在于自增操作不是原子的 123456789101112131415161718192021222324public class Test0 { public volatile int a =0; // 自增操作 public void setA(){ a++; } // 10个线程每个线程让a+1,1000次 public static void main(String[] args) { Test0 test = new Test0(); for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { for (int j = 0; j <1000; j++) { test.setA(); } } }).start(); } Thread.sleep(2000); // 保证上面执行完成 System.out.println(test.a); // 数值随机 }} 有三种方式来解决原子性问题: 12345678910111213141516// 1 同步public synchronized void setA(){ a++;}// 2 锁private Lock lock = new ReentrantLock();public void setA(){ lock.lock(); a++; lock.unlock();}// 3 原子整数public AtomicInteger a = new AtomicInteger();public void setA(){ a.getAndIncrement();} 原理和实现机制 下面这段话摘自《深入理解Java虚拟机》: “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令” lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能: 1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成; 2)它会强制将对缓存的修改操作立即写入主存; 3)如果是写操作,它会导致其他CPU中对应的缓存行无效。 使用场景 单例模式 12345678910111213141516// 双重检查锁定模式 (Double check locked)public class Test4 { private volatile static Test4 test4 ; private Test4(){}; public static Test4 getTest4() { if (test4==null){ synchronized (Test4.class){ if (test4==null){ test4 = new Test4(); } } } return test4; }} 刚开始是有疑问的,感觉volatile关键字根本没必要呀,其实是为了禁止指令重排,保证 test4 = new Test4()顺序执行,创建对象并赋值有如下的虚拟机指令: 12340: new // 创建一个对象,在堆内开辟空间,并将地址放入虚拟机栈顶 3: dup // 复制操作数栈顶值,并将其压入栈顶,也就是说此时操作数栈上有连续相同的两个对象地址;4: invokespecial #3 //执行构造方法 7: astore_1 // 将地址赋值 创建对象分配堆空间 执行构造方法 将对象地址赋值给变量 上面这三个步骤,2,3是依赖一的,所以2,3一定会在1之后执行,而2,3可能会因为指令重排而发生 3执行在2之前,因此可能会出现下面的情况: 线程1 线程2 1 判断是否为null(true) 2 获取锁 3 判断是否为null(true) 4 发生指令重排 5 创建对象 6 将对象地址赋值给变量 判断是否为null(false) 7 return 8 执行构造方法 在线程1执行构造方法之前,线程2直接将没有初始化的地址返回了,因此线程2要执行操作的时候会出现问题,比如线程1在构造方法对成员变量进行赋值,而线程2获取时候发现还是默认值。 因此volatile的重要性就体现出来了,禁止指令重排序将保证上述问题不会发生。 参考Java并发编程:volatile关键字解析 - Matrix海子 双重检查锁单例模式为什么要用volatile关键字?","categories":[{"name":"技术","slug":"技术","permalink":"https://www.yangcc.top/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://www.yangcc.top/tags/Java/"}]}],"categories":[{"name":"工具","slug":"工具","permalink":"https://www.yangcc.top/categories/%E5%B7%A5%E5%85%B7/"},{"name":"技术","slug":"技术","permalink":"https://www.yangcc.top/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://www.yangcc.top/tags/Java/"},{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.yangcc.top/tags/JavaScript/"},{"name":"设计模式","slug":"设计模式","permalink":"https://www.yangcc.top/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"},{"name":"规则引擎","slug":"规则引擎","permalink":"https://www.yangcc.top/tags/%E8%A7%84%E5%88%99%E5%BC%95%E6%93%8E/"},{"name":"算法","slug":"算法","permalink":"https://www.yangcc.top/tags/%E7%AE%97%E6%B3%95/"},{"name":"Electron","slug":"Electron","permalink":"https://www.yangcc.top/tags/Electron/"},{"name":"Rust","slug":"Rust","permalink":"https://www.yangcc.top/tags/Rust/"},{"name":"Linux","slug":"Linux","permalink":"https://www.yangcc.top/tags/Linux/"},{"name":"Javafx","slug":"Javafx","permalink":"https://www.yangcc.top/tags/Javafx/"}]}