能力说明:
了解变量作用域、Java类的结构,能够创建带main方法可执行的java应用,从命令行运行java程序;能够使用Java基本数据类型、运算符和控制结构、数组、循环结构书写和运行简单的Java程序。
能力说明:
了解Python语言的基本特性、编程环境的搭建、语法基础、算法基础等,了解Python的基本数据结构,对Python的网络编程与Web开发技术具备初步的知识,了解常用开发框架的基本特性,以及Python爬虫的基础知识。
Hexo+github搭建个人博客并绑定个人域名本篇教程完整讲述了如何利用Hexo+github搭建个人博客并且绑定自己的域名,成为自己的网站!我的博客网站:武师叔 - 做一个有趣而不甘平庸的人!----------(备用wushishu.github.io)教程参考了很多互联上的内容,在美化教程上面可以根据自己的审美,不必全部照搬~在搭建博客的路上有很多bug出现,一定请大家耐心的调试,最终胜利是属于我们的本文作者:武师叔-----------------------------------------------------最近更新时间2022/5/25关注公众号武师叔---------------------回复博客-------------------------即可获得博客PDF文件第一部分视频学习链接:www.bilibili.com/video/BV1mU…视频中的网址失效了,在本文下面给你最新的博客教程安装并配置Node.jsNode.js下载:【它让JavaScript成为与PHP、Python、Perl、Ruby等服务端语言平起平坐的脚本语言。】教程:blog.csdn.net/weixin_5279…(过程详细,还覆盖win11,评论下面还有师叔的足迹)注意一全局安装最常用的 express 模块 进行测试命令如下:npm install express -g 复制代码、报错图片:编辑解决方法:【亲测有效】需要删除 npmrc 文件。**强调:**不是nodejs安装目录npm模块下的那个npmrc文件而是在 C:\Users\(你的用户名)\下的.npmrc文件聪明的你,一定想到了直接用evering搜索,省的还要调用文件管理器在一点一点的找编辑注意二在文章第四歩测试上查看安装结果可能会出现下面照片结果,更改了目录为什么还是C盘目录下,这时候只需要以管理员身份运行命令即可。在下面路径下找到cmd.exe并且管理员身份运行即可。推测:出像这种现象的原因就是执行权限不够,推荐大家在桌面建立一个快捷方式(管理员命令的)cmdC:\Windows\System32\cmd.exe 复制代码编辑创建管理员权限的cmd桌面快捷方式编辑安装并配置Gitgit是一个并源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理Windows系统Git安装教程:www.cnblogs.com/xueweisuoyo…生成SSH Keys生成sshssh-keygen -t rsa -C "你的邮箱地址" 复制代码编辑找到秘钥位置并复制编辑测试ssh是否绑定成功ssh -T git@github.com 复制代码如果问你(yes or no),直接 yes 就可以得到下面这段话编辑本地访问博客1、创建一个名为 Blog 的文件,在里面启用 Git Bash Here编辑2、初始化hexohexo init 复制代码编辑3、生成本地的hexo页面hexo s 复制代码编辑4、访问打开本地服务区http://localhost:4000/ 复制代码编辑长按 Ctrl + c 关闭服务器编辑上传到Github修改-config.yml文件编辑把图片上位置更换成deploy: type: git repository: 你的github地址 branch: main 复制代码编辑安装hexo-deployer-git 自动部署发布工具npm install hexo-deployer-git --save 复制代码编辑生成页面hexo g 复制代码编辑注意一如果报错如下:(无报错,请忽略此条)报错信息是提示hexo的yml配置文件 冒号后面少了空格解决方案:到提示行将对应的空格补上即可编辑本地文件上传到Github上面hexo d 复制代码中间会出现一个登录界面,可以用令牌登录。(令牌及时保存,就看不到了)结束以后就上传 Github 就成功了!!!编辑注意二如果出现如图错误网络报错,再次尝试,多次尝试,直到更换WiFi~~~~编辑访问GitHub博客编辑访问博客,开始的页面是初始化页面,没有做美化和增加内容。https://wushishu.github.io/ 复制代码编辑第二部分 文档学习撰写博客*电脑要必须有Typora!电脑要必须有Typora!电脑要必须有Typora! *(重要的事情说三遍)文本教程:dhndzwxj.vercel.app/3276806131.…hexo标签教程:haiyong.site/post/cda958…我们打开自己的博客根目录,跟着我一个个了解里面的这些文件(夹)都是干什么的:编辑_config.yml:俗称站点配置文件,很多与博客网站的格式、内容相关的设置都需要在里面改。node_modules:存储Hexo插件的文件,可以实现各种扩展功能。一般不需要管。package.json:别问我,我也不知道干嘛的。scaffolds:模板文件夹,里面的post.md文件可以设置每一篇博客的模板。具体用起来就知道能干嘛了。source:非常重要。所有的个人文件都在里面!themes:主题文件夹,可以从Hexo主题官网或者网上大神的Github主页下载各种各样美观的主题,让自己的网站变得逼格高端的关键!接下来重点介绍source文件夹。新建的博客中,source文件夹下默认只有一个子文件夹——_posts。我们写的博客都放在这个子文件夹里面。我们还可以在source里面新建各种子文件夹满足自己的个性化需求,对初学者而言,我们先把精力放在主线任务上,然后再来搞这些细节。hexo官方文档:hexo.io/zh-cn/docs/…编辑写好内容后,在命令行一键三连:'hexo cl'命令用于清除缓存文件(db.json)和已生成的静态文件(public)。例如:在更换主题后,如果发现站点更改不生效,可以运行该命令。hexo cl 复制代码hexo g 复制代码hexo s 复制代码然后随便打开一个浏览器,在网址栏输入localhost:4000/,就能发现自己的网站更新了!不过这只是在本地进行了更新,要想部署到网上(Github上),输入如下代码:hexo d 复制代码然后在浏览器地址栏输入https://yourname.github.io,或者yourname.github.io就能在网上浏览自己的博客了!以上,我们的博客网站1.0版本就搭建完成了,如果没有更多的需求,做到这里基本上就可以了。如果有更多的要求,还需要进一步的精耕细作!精耕细作**海拥\Butterfly 主题美化:**haiyong.site/post/22e1d5…Butterfly参考文档(小白慎入,但是他也是你走向DIY必须迈出的一歩) :butterfly.js.org/posts/dc584…文章中要更改的文件(.yml .bug 等)可以要用viscode打开!!!Butterfly 主题安装git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly 复制代码这里面如果报错,如下图所示(长路漫漫,bug满满)编辑只需要在命令行中执行git config --global --unset http.proxy git config --global --unset https.proxy 复制代码再次安装主题即可成功编辑应用主题theme: butterfly 复制代码编辑安装插件如果你没有 pug 以及 stylus 的渲染器,请下载安装:npm install hexo-renderer-pug hexo-renderer-stylus --save 复制代码编辑Butterfly 主题美化生成文章唯一链接Hexo的默认文章链接格式是年,月,日,标题这种格式来生成的。如果你的标题是中文的话,那你的URL链接就会包含中文,复制后的URL路径就是把中文变成了一大堆字符串编码,如果你在其他地方用这边文章的url链接,偶然你又修改了改文章的标题,那这个URL链接就会失效。为了给每一篇文章来上一个属于自己的链接,写下此教程,利用 hexo-abbrlink 插件,A Hexo plugin to generate static post link based on post titles ,来解决这个问题。 参考github官方: hexo-abbrlink 按照此教程配置完之后如下:1、安装插件,在博客根目录 [Blogroot] 下打开终端,运行以下指令:npm install hexo-abbrlink --save 复制代码编辑2、插件安装成功后,在根目录 [Blogroot] 的配置文件 _config.yml 找到 permalink:编辑发布博客这次了解我上面只有一个HelloWord的时候,为什么不让右键新建,因为需要命令生成啊,铁汁!npm i hexo-deployer-git 复制代码hexo new post "新建博客文章名" 复制代码hexo cl && hexo g && hexo s 复制代码hexo更换背景图片背景图片参考网址:wallhaven.cc/wall.alphacoders.com/bz.zzzmh.cn/index本方法解决的是多次同步到GitHub上背景图片未成功的情况直接更改原文件图片所在目录:hexo/themes/landscape/source/css/images/图片名称:banner.jpg第三部分 绑定自己的域名博客地址:www.likecs.com/show-30474.…绑定之后你就有有一个自己专属的博客了。买一个域名,可以一块钱白嫖,但是续费贵的飞天!!!注意请谨慎绑定,想我就会出现提交一次 (hexo d) ,需要重新绑定域名声明:如果遇到什么不懂的可以先百度,在不懂可以微信我wushibo0820
哈希表文章内有一些词语(网络热梗等)和插图,他是方便大家理解,并对算法产生(浓厚的)兴趣!不要根据一些注释,过分曲意理解作者哦!!!!文章内容来源本作者对该领域内容的归纳总结,文末附有主要文献来源文章画图软件(www.processon.com)一、认识哈希表1.1 哈希表出现缘由要查一个数在数组中的位置,那可是太费劲了,只能从头开始一个个的比较,直到找到相等的才算完事。这个方法,说实话也太笨了,简直不是我这种懒人应该做的事。就不能有种方法直接看到这个数,就直接在数组中查到位置嘛?!编辑诶,你别说,还真有。因为总有比我更懒的,我只是懒是只能躺着,人家大佬的懒是直接动手解决,果然那句”懒是第一生产力“!1.2 哈希表概述这个就是我今天要给家人们带来的哈希表。编辑哈希表,别名儿叫散列表,洋名儿叫 Hash Table。我在上面说,希望有种方法,直接看到数,就知道它在数组中的位置,其实里就用到了哈希思想。哈希思想就是说不用一些无用的比较,直接可以通过关键字 key 就能找到它的存储位置。这里举一个栗子(可不是堂嫂栗子哦),可能更清楚点:智能班有 40 个学生,每个学生的学号由入学年份 + 年级 + 班级 + 编号组成,例如 2020.20.01.32 是 2020 年入学的 20 级 01 班的 32 号同学。(学号怕你看混,给你个"."分割,贴心不~)现在有个需求(烦人的PM):我们需要快速找到某个同学的个人信息。那这个时候我们就要建立一张表,按理来说我要是想要知道某个同学的个人信息,其实就知道学号就好了,但是在这不行,学号的值实在太大了,我们不能把学号当作下标。学号不可以,那什么可以呢?我们定睛一看,咦,编号可以呀,编号是从 1 ~ 40。(我真是一个小聪明啊)编辑那咋取到编号?不就是学号对 2020.20.01.00 取余就 KO了嘛。(你不会没理解把,不就是相当于(上面栗子32这位大帅哥),32/00=32嘛)此时,如果我们想查学号为 2020.20.01.32 的学生个人信息,只要访问下标为 32的数据即可。其实这就可以在时间复杂度为 O(1) 内解决找到。(不要问我什么是时间复杂的,什么是空间复杂度,生产队的LV马上更,不要打拉)秒男实锤了。(这摸快,O(1),比三秒都快哦)编辑用公式表示就是:存储位置 = f(关键字) 。这里的 f 又叫做哈希函数,每个 key 被对应到 0 ~ N-1 的范围内,并且放在合适的位置。在上面的例子中 f(key) = key % 2021210100。编辑存储时,通过同一个哈希函数的计算 key 的哈希地址,并按照此哈希地址存储该 key。最后形成的表就是哈希表,它主要是面向查找的存储结构,简化了比较的过程,提高了效率。编辑1.3 哈希示例上面看明白的话,那再举个大栗子加深点印象。有个 n = 10 的数组,哈希函数 f(key) = key % 10,将 4,10,11,19,29,39 散列到数组中。哈希函数确定后,剩下的就是计算关键字对应的存储位置。4 % 10 = 4,所以将 4 放入下标为 4 的位置。编辑 10 % 10 = 0,所以将 10 放入下标为 0 的位置。编辑11 % 10 = 1,所以将 11 放入下标为 1 的位置。编辑19 % 10 = 9,所以将 19 放入下标为 9 的位置。编辑29 % 10 = 9,所以将 29 放入下标为 9 的位置。但是现在问题来了,下标为 9 的这个坑已经被 19 占了,这 29 计算出来的下标冲突了。(作为工具人的我,呜呜,就让我为你来平定冲突和亲去,昭君我来了)编辑这种情况有个学名,叫:哈希(散列)冲突。1.4 处理哈希冲突对于两个不相等的关键字 key1 和 key2,若 f(key1) = f(key2),这就是哈希冲突。key1 占了坑,那 key2 只能想别的办法,啥办法呢?一般处理哈希冲突有以下几种办法:开放定址法再哈希(散列)函数法链地址法。。。(别想了,就等你来创造一个,作为算法“行业冥灯”,击垮他们!)1.4.1开放定址法开放定址法就是:一旦发生冲突,就选择另外一个可用的位置。开放定址法有 2 个最常用的策略:线性探测法二次探测法线性探测法线性探测法,顾名思义,直来直去的探测。且看它的公式:f(key) = (f(key) + di) % m (di = 1, 2, 3, ... , m-1)。我还是用“哈希示例”中的栗子(栗子都快熟了):n = 10 的数组,哈希函数 f(key) = key % 10,将 4,10,11,19,29,39 散列到数组中。编辑到了 29 的时候,29 % 10 = 9。但此时下标 9 已经有了元素 19,所以此时探测下一个位置 (9 + 1) % 10 = 0。下标为 0 的位置上已经有了元素 10,所以继续探测下一个位置 (9 + 2) % 10 = 1。下标为 1 的位置上也有了元素 11,所以还得继续探测下一个位置 (9 + 3) % 10 = 2。下标为 2 的位置上总算是空的,因此 29 找到了家(我家的猫不会跳舞,但是会爬树):编辑不知道你发现了没,对于 29 这个来说,本来只是和 19 冲突,整着整着和 10,11 也冲突了。这样就使得每次要处理好几次冲突,而且这样会出现大量数字聚集在一个区域的情况,大大降低了插入和查找的效率。后来不知道哪个大佬在线性的基础上做了改进,捣鼓出二次探测法。编辑二次探测法二次探测法就是把之前的 di 整成了平方,公式如下:f(key) = (f(key) + di) % m (di = 1², -1², 2², -2², ..., q², -q², q ≤ m/2)比如对于 29 来说,下标为 9 的位置上呆了19,所以此时探测下一个位置 (9 + 1²) % 10 = 0。下标为 0 的位置上占了元素 10,那下一次就探测上一个位置 (9 - 1²) % 10 = 8。下标为 8 的位置上空着,直接占住:编辑编辑1.4.2 再哈希(散列)函数法再哈希的话,就是不只是一个哈希函数,而是使用一组哈希函数 f1(key)、f2(key)、f3(key)....。当使用第一个哈希函数计算到的存储位置被占了,那就用第二个哈希函数计算,反正总会有一个散列函数能把冲突解决掉。依次类推,直到找到空的位置,然后占住。当然这种方法不会出现大量数字聚集在一个区域的情况,但这种情况明显增加了计算的时间。1.4.3 链地址法是谁说出现冲突后只能找别的坑位的,几个人蹲一个坑它不香嘛。(还记得伦敦的谜语吗)编辑可能真的有巨佬觉得香,然后就整出了链地址法。链地址法呢是将得出同一个结果的 key 放在一个单链表中,哈希表存储每条单链表的头指针。还是用老栗子:n = 10 的数组,哈希函数 f(key) = key % 10,将 4,10,11,19,29,39 散列。最后得到如下图:编辑你看,链地址法就不会出现此坑被占,就得去找别的坑的情况。大家一起蹲,这样就绝不会出现找不到地址的情况,而是直接插到对应的单链表中即可,所以插入的时间复杂度为 O(1) 。当然有得必有失,这样的情况必然造成了查找上的性能损耗,这里的查找的时间复杂度为 O(k),其中 k = n / 单链表条数。编辑1.5 结语和附录好啦,到这里哈希表就讲完辣,是不是看起来还挺简单的。哈希表作为非常高高高高高效的查找数据结构,丢掉了关键字之间反复无意义的比较,直接一步到位查找结果,非常顶(咳咳)。这么看来它处理冲突啥的这点屁事就显得不是那么烦人了,毕竟有得有失才对嘛。
对象 Objectjava 是面向对象的语言:对象包含了状态和行为,用户通过调用对象的方法、改变对象的属性来实现 java 程序的功能。Car myCar = new Car("BMW"); // 创建对象 me.brand = "Benz"; // 修改对象变量 me.go("London"); // 调用对象方法Copy to clipboardErrorCopied 复制代码在 java 程序中我们通过类和接口来定义对象的性质:每个 java 文件都是一个定义好的 public 类 / 接口,且类名 / 接口名与文件名相同。java 文件可以含有多个类 / 接口,但只能有一个 public 类 / 接口供外部访问。类 Class对象的类型:定义对象含有的变量和方法。public class Car { // 变量 String brand; String description = "this is a car"; // static 变量 static int number_of_car; // 构造方法 public car(String brand){ this.brand = brand; } // 方法 public void go(String loc){ System.out.print("go to" + loc); } // static 方法 void static showNum(){ System.out.print(number_of_car); } // 初始化块 { number_of_car; } // static 初始化块 static{ number_of_car = 0; } // 内部类 public class Warranty{ public void repair(){ System.out.print("repair"); } } }Copy to clipboardErrorCopied 复制代码变量对象中存储的数据。方法调用时执行的代码。初始化块创建对象前自动执行的代码。内部类定义在类中的类。构造方法在创建对象时自动执行,不返回任何参数(先执行初始化块,再执行构造方法)。未定义任何构造方法时,系统会自动添加无参构造方法。终态声明final 常量: 只能赋值一次,不可更改。final 类: 不可被继承。final 方法:(弃用)不可被继承。现在所有的 private 方法都隐式地指定为 final。对于 final 常量,如果编译时就可以确定值,编译器会在编译时直接把这个变量替换成它的值。静态声明static 变量:该变量由该类的所有对象共享,不需要创建对象也可使用。static 方法:允许直接访问,不需要创建对象也可被调用。如 main 方法。static 初始化块:在创建类的第一个对象前自动执行(先执行静态初始化块,再执行初始化块)。static 内部类:外部类对象共享,只能访问外部类的静态成员。权限声明public: 允许所有访问。protected: 只允许本类、同包和子类访问。[default] : 允许本类和同包访问。private: 只允许本类访问。接口 Interface类的规范:只规定应含有哪些方法,而不负责具体实现。public interface Move{ // abstract 方法 public void go(String loc); // default 方法 default void stop() { System.out.print("stop"); }; }Copy to clipboardErrorCopied 复制代码声明接口:必须且默认为 static final,通常为 public 。只允许声明静态常量:必须且默认为 public static final 。声明抽象方法:必须且默认为 abstract ,可以为 static。JDK 1.8 以前,接口中抽象方法必须且默认为 public,不允许实现任何方法。 JDK 1.8 以后,接口中抽象方法可以且默认为 default,且允许实现 static 和 default 方法。 JDK 1.9 以后,接口中抽象方法可以是 private。*抽象声明abstract 方法:只有声明,而没有方法的具体实现。abstract 类:类的模板,不能实例化对象。必须由其他类继承才能使用。public abstract class Vehicle { // 声明变量 String brand; // 声明并实现方法 public void go(String loc){ System.out.print("go to" + loc); } }Copy to clipboardErrorCopied 复制代码接口和抽象类的区别接口不能实现普通方法,抽象类可以实现具体的方法、也可以不实现。接口只能定义静态常量,抽象类可以定义非静态变量。一个实体类可以实现多个接口,但只能继承一个抽象类。更新声明default 方法:更新接口时添加的新方法,允许旧类实现接口而不实现该方法。可以直接在接口内实现,供没有定义的旧类直接使用。若类中实现了该方法则覆盖。如果类实现了多个接口且拥有同名 default 方法:两个接口若存在继承关系,调用时优先使用子类方法。否则,必须重写子类 default 方法,通过 super 关键字明确实现哪个接口:class Plane implements Move, Fly{ ... void go(){ Fly.super.go(); // 实现选定 default 方法 } }Copy to clipboardErrorCopied 复制代码包 Package命名空间,表示 java 文件的存储路径。其路径记录在每个 java 文件首。package com.company.project.module; // 声明存储路径Copy to clipboardErrorCopied 复制代码导入 import在 java 文件中,如果要调用其他 java 文件中定义的类 / 接口,就需要进行导入:同一存储路径(包)下的 java 文件不需要导入,可以直接调用。已默认导入 java.lang 路径下所有 java 文件,包含 System、String、Object、Math 等常用类。如果没有导入对应 java 文件,或者导入了多个同名 java 文件,在调用类 / 接口时需要标明路径。package com.company.project.module; import java.util.Scanner; // 导入 java 文件,但不包括内部 static 变量和方法 import java.net.*; // 导入路径下所有 java 文件,但不包括下属文件夹 import static java.lang.Math.PI; // 导入 java 文件中的 static 变量或方法 public class Test{ public void main(String[] args){ java.io.InputStream in = new java.io.InputStream(System.in); // 未导入类,调用时需要标明路径 Scanner sc = new Scanner(in); // 已导入类,可直接调用 Integer n = sc.nextInt(); // 默认导入类,可直接调用 sc.close(); } }
数值比较和排序的常用方法等值判断Object 类实现了 equals 方法 ,用于比较两个数据元素是否相等。浮点类型由于精度丢失问题,进行等值判断常出现错误。如果有需求推荐使用 [BigDecimal 类]int a = 20 - 10; int b = 10; System.out.println(a.equals(b)); // true double a = 20.0 - 10.0; double b = 10.0; System.out.println(a.equals(b)); // falseCopy to clipboardErrorCopied 复制代码和 == 的区别对于基本类型,两者等价:判断数据是否相等。对于对象(如 String 类):==:比较两个元素内存地址是否相等,即是否是同一个元素。equals 方法:比较两个元素内容是否一致。System.out.println(s1 == s2); // 判断两个引用指向的内存地址是否相等 System.out.println(s1.equals(s2)); // 判断两个引用指向的内存地址是否相等(s1 为空抛出空指针异常) System.out.println(Objects.equals(s1,s2)); // 判断两个引用指向的元素是否一致(推荐)Copy to clipboardErrorCopied 复制代码重写 equals 方法对于用户自定义类,正常使用 equals 方法需要进行重写。重写 equals 方法必须重写 hashcode 方法:以保证相同对象拥有相同的哈希地址。这样才能正常地把该类对象放入 HashSet/HashMap 等集合框架中查找。Object 类的 hashcode 方法是本地方法(底层用 c/c++ 实现),直接返回对象的内存地址。public class User{ int ID; String name; ...... @Override public boolean equals(Object obj) { if(this == obj) return true; if(obj == null) return false; if(obj instanceof User){ User other = (User) obj; if(equals(this.ID, other.ID) && equals(this.name, other.name)){ return true; } } return false; } @Override public int hashCode() { int result = 17; result = 31 * result + (ID == null ? 0 : ID.hashCode()); result = 31 * result + (name == null ? 0 : name.hashCode()); return result; } } Copy to clipboardErrorCopied 复制代码数值比较Comparator 接口和 Comparable 接口都用于比较两个元素的大小:Comparable 接口位于 java.lang 包内,定义在要比较的实体类内部:包含 compareTo 方法。Comparator 接口位于 java.util 包内,实现在类的外部:包含 compare 方法和 equals 方法。Comparator 接口的 equals 方法和 Object 类的 equals 方法不同, Object 类的 equals 方法实现在实体类的内部。compareTo 方法Java 自带数据类型均已实现 Comparable 接口并重写 compareTo 方法,默认情况下如果 s1 等于 s2,则返回 0;如果 s1 小于 s2,则返回小于 0 的值;如果 s1 大于 s2,则返回大于 0 的值。Integer s1 = 100; Integer s2 = 90; System.out.println(s1.compareTo(s2)); Copy to clipboardErrorCopied 复制代码compare 方法Arrays/Collections 类定义了 sort 方法对数组或者集合元素进行排列,数值的比较通过调用 Comparator 接口的 compare 方法实现。执行 sort 方法时如果没有重写 compare 方法,默认调用的 compare 方法将会直接调用数据类型的 compareTo 方法,使数据从小到大排列。如果是自定义数据类型且未实现 compareTo 方法,则必须重写 compare 方法。Arrays.sort(students); // 对数组排序 Collections.sort(students); // 对集合元素排序Copy to clipboardErrorCopied 复制代码开发者可以通过重写 compare 方法,实现自定义排列顺序。但要注意,如果数组中保存的是基础类型数据则无法自定义排序。Arrays.sort(students, new Comparator<Student>() { @Override public int compare(Student s1, Student s2) { return s1.getID() - s2.getID(); } }); Collections.sort(students, (s1, s2) -> s1.getID() - s2.getID()); // 使用 Lamdba 表达式简写Copy to clipboardErrorCopied 复制代码数据排序Arrays/Collections 类定义了 sort 方法对数组或者集合元素进行排列,数值的比较通过调用 Comparator 接口的 compare 方法实现。
正则匹配基本使用java.util.regex 包主要包括以下三个类:Pattern 类正则表达式的编译表示。没有公共构造方法,必须首先调用其公共静态编译方法获得 Pattern 对象。Matcher 类对输入字符串进行解释和匹配操作的引擎。没有公共构造方法,需要调用 Pattern 对象的 matcher 方法获得 Matcher 对象。PatternSyntaxException 类非强制异常类,表示正则表达式模式中的语法错误。import java.util.regex.Matcher; import java.util.regex.Pattern; public class RegexMatches { public static void main( String args[] ) { Pattern p = Pattern.compile("abc"); // 编译正则表达式 Matcher matcher = p.matcher("abcdefg"); // 放入字符串中匹配 System.out.println(matcher.lookingAt()); // 是否存在子串匹配 true System.out.println(matcher.matches()); // 是否完全匹配 false } }Copy to clipboardErrorCopied 复制代码正则表达式我们可以通过使用特殊符号,让一个正则表达式能够匹配多种符合要求的字符串。表意符号. 表示任意字符在 Java 中,正则表达式编译需要再经过一次转义。因此 \ 才表示插入一个正则表达式的反斜线!\d 表示一位数字\\ 表示一个反斜杠字符集x|y 匹配 x 或 y[abc] 匹配括号中任意单个字符[^abc] 匹配除括号中的任意单个字符[a-zA-Z] 匹配任意单个字母[a-z&&[^def]] 除 def 外的任意单个字母字符串匹配通过 ?、*、+ 符号,我们可以对指定类型的字符串进行匹配。贪婪模式饥饿模式独占模式结果X?X??X?+匹配0或1次X*X*?X*+匹配0次或多次X+X+?X++匹配1次或多次X{n}X{n}?X{n}+匹配n次X{m,n}X{m,n}?X{m,n}+匹配m-n次在匹配字符串时,同一个正则表达式可能会在在字符串中匹配到多种结果。Java 提供了以下三种方式供开发者选择:贪婪模式 (默认)尽可能匹配长字符串。饥饿模式 (?)尽可能匹配短字符串。独占模式 (+)尽可能匹配长字符串,不成功会结束匹配而不回溯。捕获组普通捕获组我们可以在正则表达式中同时捕获多个结果,最终以 group 的形式呈现。matcher.group(0) 完全匹配整个正则表达式。matcher.group(1-n) 从左到右分别记录正则表达式中 n 个括号内的结果。public class RegexMatches { public static void main( String args[] ) { String regex = "(\d{4})-((\d{2})-(\d{2}))" Pattern p = Pattern.compile(regex); // 编译正则表达式 Matcher matcher = p.matcher("2020-10-25"); // 放入字符串 matcher.find(); // 执行匹配 System.out.printf(matcher.group(0)); // 2020-10-25 System.out.printf(matcher.group(1)); // 2020 System.out.printf(matcher.group(2)); // 10-25 System.out.printf(matcher.group(3)); // 10 System.out.printf(matcher.group(4)); // 25 } }Copy to clipboardErrorCopied 复制代码命名捕获组我们可以通过 (?<Name>Expression) 对括号内容就行命名,并通过名称获取括号内的匹配结果。public class RegexMatches { public static void main( String args[] ) { String regex = "(?<year>\d{4})-(?<md>(?<month>\d{2})-(?<date>\d{2}))"; Pattern p = Pattern.compile(regex); // 编译正则表达式 Matcher matcher = p.matcher("2020-10-25"); // 放入字符串中匹配 matcher.find(); // 执行匹配 System.out.printf(matcher.group("year")); // 2020 System.out.printf(matcher.group("md")); // 10-25 System.out.printf(matcher.group("month")); // 10 System.out.printf(matcher.group("date")); // 25 } }Copy to clipboardErrorCopied 复制代码非捕获组我们可以通过 (?:Expression) 对组不进行捕获。(?=pattern)例如,'Windows (?=95|98|NT|2000)' 匹配"Windows 2000"中的"Windows",但不匹配"Windows 3.1"中的"Windows"。预测先行不占用字符,即发生匹配后,下一匹配的搜索紧随上一匹配之后,而不是在组成预测先行的字符后。(?!pattern)如 'Windows (?!95|98|NT|2000)' 匹配"Windows 3.1"中的 "Windows",但不匹配"Windows 2000"中的"Windows"。预测先行不占用字符,即发生匹配后,下一匹配的搜索紧随上一匹配之后,而不是在组成预测先行的字符后。
应用层应用层功能在应用进程间传输报文,实现特定类型的数据交互,去执行特定的功能。HTTP 协议(超文本传输协议)负责发布和接收 HTML 页面和其他 Web 资源,即 Web 通信。底层通信通过 TCP 协议实现,默认端口号 80 。资源定位HTTP 协议通过 URL 来访问资源。URL 即 WEB 内容访问标识符,是一种具体的 URI。不仅唯一标识资源,而且还提供了定位该资源的信息。通用格式为:协议://IP地址或域名:端口号/文件路径?参数 ,如 http://localhost:443/student?id=10&name=mrjoker连接方式HTTP/1.0默认使用短连接:访问页面时,客户端和服务器之间每次 HTTP 操作都会单独使用一次 TCP 连接,传输完毕后自动关闭。客户端和服务器之间都会建立多个 HTTP 会话,开销较大。HTTP/1.1默认使用长连接:访问页面时,客户端和服务器之间用于传输 HTTP 数据的 TCP 连接不会关闭,直到 HTML 页面调用的所有 web 资源传输完毕。长连接会在 HTTP 响应头标注:Connection:keep-alive但 HTTP/1.1 是串行化处理所有 web 资源请求。一旦有请求无法正常加载,后续请求就会被阻塞。HTTP/2增加了多路复用功能:让所有数据流共用同一个连接。接收到客户端 HTTP 请求后,服务器会一次性把所需要的 web 资源打包发送。即使有请求无法正常加载,也不会影响处理其它 web 资源请求。TCP 连接在刚开始发送数据时会限制连接的最大速度,集中发送能更有效地利用 TCP 连接。请求类型HTTP 请求有以下四种常用类型:GET:请求服务器数据,请求参数直接附在 URL 上(不够安全,仅适用于简单公开数据的请求和提交)POST:向服务器提交数据,数据以表单形式提交(内容长度无上限,且不会被浏览器记录)PUT:修改服务器数据(慎用)DELETE:删除服务器数据(慎用)除此之外 HTTP 协议在正式通信前还可能发出 OPTION 请求:先询问服务器当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。主机识别HTTP 协议是无状态协议,本身不保存请求和响应之间的通信状态。也就是说通信过后服务器将无法相互识别客户端,因此我们必须引入其他方式让服务器记录客户端的信息。Session & Cookie负责记录对方主机信息,在 HTTP 通信中相互识别。cookie 保存在客户端,session 保存在服务器端。客户端初次访问服务器时,服务端会自动创建 session 用来标识用户。然后在响应头 Set-Cookie 项向客户端返回 session ID。客户端再次访问服务器时,会在请求头 Cookie 项向服务器发送 session ID,服务器根据 ID 查询 Session 就可以识别用户。用户和通信的详细信息通常记录在 session 中,cookie 中只保存 session ID ,以避免信息泄露。如果客户端 cookie 被禁用,就需要利用 URL 重写把 session ID 直接附加在 URL 路径上。Tokentoken 和 session ID 功能相同,都用来在 HTTP 通信中识别用户,过期刷新:session ID 由服务器随机生成,保存在服务器 session 中。再次访问时只需要直接比对,就可以确认客户端身份。token 由服务器根据用户 ID 和时间戳经过特定算法生成,服务器不保存。再次访问时服务器需要重新计算并比对,才可以确认客户端身份。cookie 本身不安全,浏览器中 token 除放在 cookie 外,还可以放到 localStorage 中存储。浏览器发送请求时会自动携带 session ID,但发送 token 需要手动在代码中设置。由于浏览器加载 image 标签中的地址也会发送 session ID,因此使用 token 可以有效防止 CSRF 攻击。localStorage和sessionStorage的区别; localStorage生命周期是永久,除非用户手动清除,否则这些信息将永远存在。 sessionStorage生命周期为当前窗口或标签页,一旦窗口或标签页被永久关闭了,数据也就被清空了。HTTPS 协议由于 HTTP 协议使用明文在互联网传送数据,易被不法分子监听和截获。我们又引入了 HTTPS 协议取而代之,默认端口 443。HTTPS 协议在应用层下方增加了 SSL 层,使用 TLS 协议来加密和解密数据包,这样在互联网上传送的数据将经过加密。加密方式建立连接后的数据交互采用对称加密:加密密钥和解密密钥相同。建立连接时协商密钥采用非对称加密:加密密钥和解密密钥不同,两个密钥互相能解密对方的加密内容。服务器会公开一个非对称加密密钥(公钥),并保留一个非对称加密码密钥(私钥)。证书认证服务器公钥由数字认证机构 CA 统一认证。CA 会用自己的私钥加密服务器公钥和相应信息,生成数字证书。在客户端向 CA 查询时将证书发送给客户端核对。CA 根证书(包含公钥)存储在用户的浏览器中,访问网址时会自动比对服务器公钥。客户端向服务器发送信息:请求连接,说明自己支持的加密算法,并给出随机数 A。服务器向客户端发送信息:同意连接请求,确认合适的加密算法,并给出数字证书和随机数 B。客户端向 CA 核对数字证书,确认有效后得到服务器公钥。建立连接客户端向服务器发送公钥加密信息:给出随机数 C。服务器通过私钥解密信息,对信息 Hash 得到数字签名;然后向客户端发送私钥加密信息:返回数字签名。客户端通过公钥解密信息,核对数字签名,确认服务器收到随机数 C。之后双方就可以按照约定的对称加密方法,使用三个随机数生成的密钥进行数据交互。
线程池线程池基本概念线程池线程池本质上是一种对象池,用于管理线程资源。在任务执行前,需要从线程池中拿出线程来执行。在任务执行完成之后,把线程放回线程池。实际开发中,线程资源一般通过线程池提供,比如处理数据库连接、接收网络请求。线程的创建更加规范,可以合理控制开辟线程的数量。不必频繁地创建和销毁线程,优化了资源的开销。核心线程池(corePool) 通常状况下,线程池最多能创建的线程数。当有新任务等待处理时,线程池会首先判断核心线程池是否已满,如果没满则创建线程执行任务。即使有其他核心线程空闲也会创建新的核心线程来执行。任务队列(BlockQueue) 线程池中等待被线程执行的任务队列。如果核心线程池已满,线程池会判断队列是否已满。如果队列没满,就会将任务放在队列中等待执行。ArrayBlockingQueue // 基于数组实现的阻塞队列,有界。LinkedBlockingQueue // 基于链表实现的阻塞队列,可以无界。SynchronousQueue // 不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作。PriorityBlockingQueue // 带优先级的阻塞队列,无界。最大线程池(maximumPool) 任务量很大时,线程池最多能创建的线程数。如果队列已满,说明当前任务量已经非常大,仅靠核心线程池内的线程数量已无法处理。线程池会判断最大线程池是否已满,如果没满则创建更多线程,从等待队列首部取得任务并执行。拒绝策略(RejectedExecutionHandler) 线程池拒绝过量任务的方式。如果最大线程池已满,表示当前服务器已无法处理这么多任务。任务会按照既定的拒绝策略被处理。CallerRunsPolicy // 在调用者线程执行。AbortPolicy // 直接抛出 RejectedExecutionException 异常。DiscardPolicy // (常用)任务直接丢弃,不做任何处理。DiscardOldestPolicy // 丢弃队列里最旧的那个任务,再尝试执行当前任务。ThreadPoolExecutor 类实现了 ExecutorService 接口,是 java 开发常用的线程池类。位于 java.util.concurrent 包内,使用时需要进行导入。创建线程池ThreadPoolExecutor 类在创建线程池时需要输入以下参数:int corePoolSize = 2; // 核心线程池大小 int maximumPoolSize = 4; // 最大线程池大小 long keepAliveTime = 10; // 空闲线程多久被销毁,0 表示永远不会 TimeUnit unit = TimeUnit.SECONDS; // keepAliveTime 的单位 BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2); // 任务队列 ThreadFactory threadFactory = new NameTreadFactory(); // 线程工厂接口,一般默认。 RejectedExecutionHandler handler = new MyIgnorePolicy(); // 拒绝策略,一般默认。 ThreadPoolExecutor service = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);Copy to clipboardErrorCopied 复制代码ThreadPoolExecutor 类还可以重写以下方法(默认为空实现):ExecutorService service = new ThreadPoolExecutor(1, 1, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1)) { // 任务执行前被调用 @Override protected void beforeExecute(Thread t, Runnable r) { System.out.println("beforeExecute is called"); } // 任务执行后被调用 @Override protected void afterExecute(Runnable r, Throwable t) { System.out.println("afterExecute is called"); } // 线程池结束后被调用 @Override protected void terminated() { System.out.println("terminated is called"); } };Copy to clipboardErrorCopied 复制代码获取线程池信息service.getTaskCount(); // 获取已经执行或正在执行的任务数 service.getCompletedTaskCount(); // 获取已经执行的任务数 service.getLargestPoolSize(); // 获取线程池曾经创建过的最大线程数 service.getPoolSize(); // 获取线程池线程数 service.getActiveCount(); // 获取活跃线程数(正在执行任务的线程数)Copy to clipboardErrorCopied 复制代码提交任务可以向线程池提交的任务有两种:Runnable 接口和 Callable 接口。Runnable 接口内部定义了 run 方法,没有返回值,不允许抛出异常。通过 execute 方法向线程池提交。service.execute(new Runnable(){ System.out.println("new thread"); });Copy to clipboardErrorCopied 复制代码Callable 接口内部定义了 call 方法,允许有返回值,允许抛出异常。通过 submit 方法向线程池提交,返回一个 Future 对象。可以通过调用 Future 对象的 get 方法获得数据,在返回结果前 get 方法会阻塞。Future<Integer> f = service.submit(new Callable(){ System.out.println("new thread"); return 1; }); System.out.println(f.get());Copy to clipboardErrorCopied 复制代码关闭线程池service.shutdown(); // 线程池不再接受新的任务,线程池中已有任务执行完成后终止。 service.shutdownNow(); // 线程池不再接受新的任务并对所有线程执行 interrupt 操作,清空队列并终止。 boolean b = service.isShutdown(); // 返回线程池是否关闭:不再接受新任务。 boolean b = service.isTerminated(); // 返回线程池是否终止Copy to clipboardErrorCopied 复制代码ThreadPoolExecutor 类示例public class ThreadPool { private static ExecutorService pool; public static void main( String[] args ) { //自定义线程工厂 pool = new ThreadPoolExecutor(2, 4, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5), new ThreadFactory() { public Thread newThread(Runnable r) { System.out.println("线程"+r.hashCode()+"创建"); //线程命名 Thread th = new Thread(r,"threadPool"+r.hashCode()); return th; } }, new ThreadPoolExecutor.CallerRunsPolicy()); for(int i=0;i<10;i++) { pool.execute(new ThreadTask()); } } } public class ThreadTask implements Runnable{ public void run() { //输出执行线程的名称 System.out.println("ThreadName:"+Thread.currentThread().getName()); } }Copy to clipboardErrorCopied 复制代码Executors 类(不常用)继承 ThreadPoolExecutor 类的线程池工厂类:提供 4 种工厂方法创建线程池。但该方法既不灵活也不安全,实际开发中很少使用。// 单个线程的线程池 ExecutorService service = Executors.newSingleThreadExecutor(); // 指定数量的线程池 ExecutorService service = Executors.newFixedThreadExecutor(10); // 大小不限的线程池,60s 不使用会自动回收空闲线程。 ExecutorService service = Executors.newCacheThreadExecutor(); // 大小不限的线程池,可定时执行任务。 ExecutorService service = Executors.newScheduleThreadExecutor();Copy to clipboardErrorCopied 复制代码Executors 类示例public class ThreadPoolTest { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(5); for (int i = 0; i < 10; i++) { executor.submit(() -> { System.out.println("thread id is: " + Thread.currentThread().getId()); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } }); } } }
指令重排序指令概念指令是指示计算机执行某种操作的命令,如:数据传送指令、算术运算指令、位运算指令、程序流程控制指令、串操作指令、处理器控制指令。指令不同于我们所写的代码,一行代码按照操作的逻辑可以分成多条指令。举个例子:int a = 1; 这段代码大致可以分为两条指令:1.加载常量1;2.将常量1赋值给变量a。指令重排序只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。指令重排序的意义:使指令更加符合 CPU 的执行特性,最大限度的发挥机器的性能,提高程序的执行效率。指令重排序分类指令重排序主要分为三种,在这里主要讨论 JVM 中的指令重排序。编译器重排序:JVM 中完成指令级并行重排序处理器重排序:CPU 中完成指令重排序原则如果程序中操作A在操作B之前,那么线程中操作A将在操作B之前执行。(只对指令内部重排序,不在指令间重排序)As-If-Serial语义不管怎么进行指令重排序,单线程内程序的执行结果不能被改变。编译器和处理器对存在依赖关系的操作都不会对其进行重排序。只有不存在依赖关系的操作有可能进行重排序。Happens-Before原则保证正确同步的多线程程序的执行结果不被改变。对于被同步的操作,如果操作 A 先于操作 B,那么 A 操作的执行结果将对 B 操作可见,而且 A 操作的执行顺序排在 B 操作之前。管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序)对同一个锁的lock操作。 (如果线程1解锁了monitor a,接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见(线程1和线程2可以是同一个线程))防止指令重排序volatile关键字通过“内存屏障”来防止指令被重排序。为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。Java内存模型采取保守策略(见缝就插)在每个volatile写操作的前面插入一个StoreStore屏障。 在每个volatile写操作的后面插入一个StoreLoad屏障。 在每个volatile读操作的后面插入一个LoadLoad屏障。 在每个volatile读操作的后面插入一个LoadStore屏障。Synchronized 把多线程执行环境改变为单线程执行环境,无需关心指令重排序(单线程执行结果不会改变)。
基本概念SpringSpring 是用于开发 Java 应用程序的开源框架,为解决企业应用开发的复杂性而创建。Spring 的基本设计思想是利用 IOC(依赖注入)和 AOP (面向切面)解耦应用组件,降低应用程序各组件之间的耦合度。在这两者的基础上,Spring 逐渐衍生出了其他的高级功能:如 Security,JPA 等。Spring MVCSpring MVC 是 Spring 的子功能模块,专用于 Web 开发。Spring MVC 基于 Servlet 实现,将 Web 应用中的数据业务、显示逻辑和控制逻辑进行分层设计。开发者可以直接调用 Spring MVC 框架中 Spring 解耦的组件,快速构建 Web 应用。Spring BootSpring Boot 是用于简化创建 Spring 项目配置流程,快速构建 Spring 应用程序的辅助工具。Spring Boot 本身并不提供 Spring 框架的核心特性以及扩展功能。但 在创建 Spring 项目时,Spring Boot 可以:自动添加 Maven 依赖,不需要在 pom.xml 中手动添加配置依赖。不需要配置 XML 文件,将全部配置浓缩在一个 appliaction.yml 配置文件中。自动创建启动类,代表着本工程项目和服务器的启动加载。内嵌 Tomcat 、Jetty 等容器,无需手动部署 war 文件。Spring Boot 配置依赖在Spring Boot中,引入的所有包都是 starter 形式:spring-boot-starter-web-services,针对 SOAP Web Services spring-boot-starter-web,针对 Web 应用与网络接口 spring-boot-starter-jdbc,针对 JDBC spring-boot-starter-data-jpa,基于 Hibernate 的持久层框架 spring-boot-starter-cache,针对缓存支持默认映射路径classpath:/META-INF/resources/classpath:/resources/classpath:/static/classpath:/public/优先级顺序:META-INF/resources > resources > static > public全局配置位于 resources 文件夹下,支持以下两种格式。由 Spring Boot 自动加载。application.propertiesapplication.yml#端口号 server.port=8080 #访问前缀 server.servlet.context-path=/demo #数据库驱动 jdbc.driver=com.mysql.jc.jdbc.Driver #数据库链接 jdbc.url=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC #数据库用户名 jdbc.username=root #数据库密码 jdbc.password=wdh19970506 #Mybatis #配置文件路径 mybatis_config_file=mybatis-config.xml #SQL语句配置路径 mapper_path=/mapper/**.xml #实体类所在包 type_alias_package=com.example.demo.entityCopy to clipboardErrorCopied 复制代码JDBC 连接 Mysql5 驱动: com.mysql.jdbc.DriverJDBC 连接 Mysql6 驱动: com.mysql.cj.jdbc.Driver , URL 必须要指定时区 serverTimezone !多重配置在 Spring Boot 中,我们往往需要配置多个不同的配置文件去适应不同的环境:application-dev.properties 开发环境application-test.properties 测试环境application-prod.properties 生产环境只需要在程序默认配置文件 application.properties 中设置环境,就可以使用指定的配置。spring.profiles.active=devCopy to clipboardErrorCopied 复制代码启动类@SpringBootApplication 类:作为程序入口,在创建 Spring Boot 项目时自动创建。等同于 @Configuration + @EnableAutoConfiguration + @ComponentScan ,会自动完成配置并扫描路径下所有包。@SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }Copy to clipboardErrorCopied 复制代码Spring 需要定义调度程序 servlet ,映射和其他支持配置。我们可以使用 web.xml 文件或 Initializer 类来完成此操作:public class MyWebAppInitializer implements WebApplicationInitializer { @Override public void onStartup(ServletContext container) { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); context.setConfigLocation("com.pingfangushi"); container.addListener(new ContextLoaderListener(context)); ServletRegistration.Dynamic dispatcher = container .addServlet("dispatcher", new DispatcherServlet(context)); dispatcher.setLoadOnStartup(1); dispatcher.addMapping("/"); } }Copy to clipboardErrorCopied 复制代码还需要将 @EnableWebMvc 注释添加到 @Configuration 类,并定义一个视图解析器来解析从控制器返回的视图:@EnableWebMvc @Configuration public class ClientWebConfig implements WebMvcConfigurer { @Bean public ViewResolver viewResolver() { InternalResourceViewResolver bean = new InternalResourceViewResolver(); bean.setViewClass(JstlView.class); bean.setPrefix("/WEB-INF/view/"); bean.setSuffix(".jsp"); return bean; } }
服务器控制响应架构Spring Boot 内集成了 Tomcat 服务器,也可以外接 Tomcat 服务器。通过控制层接收浏览器的 URL 请求进行操作并返回数据。底层和浏览器的信息交互仍旧由 servlet 完成,服务器整体架构如下:Server: Tomcat 最顶层容器,代表整个服务器。Service:服务,对应不同的任务。Connector:有多个,用来处理连接相关的事情,并提供 Socket 到 Request 和 Response 相关转化。Container:只有一个,用于封装和管理 Servlet ,以及处理具体的 Request 请求。启动过程main 方法: 实例化 SpringApplication ,执行run方法run方法:配置属性、获取监听器,初始化输入参数、配置环境,输出banner 创建上下文、预处理上下文、刷新上下文、再刷新上下文:contextrefreshApplicationContext方法:通过ServletWebServerFactory接口定义了getwebServer方法,通过其创建webServer并返回(创建时做了两件重要的事情:把Connector对象添加到tomcat中,配置引擎)【TomcatServletWebServerFactory是接口其中一个实现类】TomcatwebServer类中,规定了Tomcat服务器的启动和关闭方法。而tomcat的启动主要是实例化两个组件:Connector、ContainerController 实现Controller 类需要使用 @RestController 或 @Controller 注解标注。@Controller:类中所有方法以 String 形式返回 classpath 路径下同名 html 页面。适用于 JSP/thymeleaf 等动态加载页面。@RestController:类中所有方法以 Map/List 等形式返回 JSON 数据。适用于前后端分离开发。P.S. @Controller 类中标注 @ResponseBody 的方法,可以起到和 @RestController 类相同的效果。请求映射Controller 类中的方法使用 @RequestMapping 注解标注,就可以将指定 URL 请求映射到方法上处理。@RequestMapping(value = "/hello", method = RequestMethod.GET) // 参数为 URL 路径和请求方式 @RequestMapping("/hello") // 默认接收所有请求方式 @GetMapping("/hello") // 简写形式的 GET 请求 @PostMapping("/hello") // 简写形式的 POST 请求 // 灵活映射 @RequestMapping("/?/hello") // ? 匹配单字符 @RequestMapping("/*/hello")`: // * 匹配任意数量字符 @RequestMapping("/**/hello"): // ** 匹配任意数量目录 @RequestMapping("/{ID}/hello")` // {} 自动读取 URL 路径动态参数Copy to clipboardErrorCopied 复制代码Controller 类也可以通过 @RequestMapping 注解标注,表示路径下的 URL 请求在该类中寻找方法。@Controller @RequestMapping("/speak") public class SpeakController{ @GetMapping("/hello") public String hello(){ return "hello"; } }Copy to clipboardErrorCopied 复制代码GET 请求参数GET 请求参数直接附着在 URL 中。对于请求 /test?username=mrjoker&password=123456 ,Controller 方法有以下几种方式接收:直接获取参数@RequestMapping("/test") public String test(String username, String password){ return username + password; }Copy to clipboardErrorCopied 复制代码通过 HttpServletRequest 类来获取参数@RequestMapping("/test") public String test(HttpServletRequest request){ String username = request.getParameter("username"); String password = request.getParameter("password"); return username + password; }Copy to clipboardErrorCopied 复制代码通过自定义对象来获取参数@RequestMapping("/test") public String test(User user){ String username = user.getUsername(); String password = user.getPassword(); return username + password; }Copy to clipboardErrorCopied 复制代码通过 RequestParam 注解来获取参数,实参值赋给形参。@RequestMapping("/test") public String test(@RequestParam(value="username",required = false, defaultValue ="mrjoker") String s1, @RequestParam("password") String s2){ return s1 + s2; }Copy to clipboardErrorCopied 复制代码通过 PathVariable 注解来动态获取参数,参数直接附着在 URL 中。@RequestMapping("/test/{username}/{password}") public String test(@PathVariable("username") String s1, @PathVariable("password") String s2){ return s1 + s2; }Copy to clipboardErrorCopied 复制代码通过 ModelAttribute 注解来获取其他方法返回值作为参数,被注释方法会在此 controller 中每个方法执行前被执行。@Controller public class HelloWorldController { @ModelAttribute public void populateModel(@RequestParam String abc, Model model) { model.addAttribute("attributeName", abc); } @RequestMapping(value = "/helloWorld") public String helloWorld() { return "helloWorld"; } }Copy to clipboardErrorCopied 复制代码POST 请求参数POST 请求请求参数放置在请求体中,有以下两种格式:Form Data 格式请求的 Content-Type 为 application/x-www-form-urlencoded示例:username=mrjoker&password=123456Request Payload 格式请求的 Content-Type 为 application/json 或者 multipart/form-data示例:{"username":"mrjoker", "password":"123456"}AJAX 提交 POST 请求默认使用 Form Data 格式,Spring MVC 会自动解析到对应的 bean 中并获取参数。// 逐个参数接收 @RequestMapping(value="/test", method=RequestMethod.POST) private String test(@RequestParam("username") String username, @RequestParam("password") String password){ return username + password; } // 解析为整体接收 @RequestMapping(value="/test", method=RequestMethod.POST) private String test(User user){ return user.getUsername() + user.getPassword(); }Copy to clipboardErrorCopied 复制代码Vue 提交 POST 请求默认使用 Request Payload 格式,Spring MVC 接收时必须进行处理:前端解决方案: axios 库可以使用 qs 库将 json 对象转化为 Form Data 格式。后端解决方案: Spring Boot 在请求参数上加 @RequestBody 注解,将请求正文解析到对应的 bean 中获取参数。@RequestBody 可以直接以 String 接收前端传过来的 json 数据,也可以用对象自动解析前端传过来的 json 数据。对象里定义 List 属性,可用来接收多条 json 数据。// String 形式接收 @RequestMapping(value = "/test", method = RequestMethod.POST) public String test(@RequestBody String user) { JSONObject userJson = JSON.parseObject(user); String username = userJson.getString("username"); String password = userJson.getString("password"); return username + password; } // 解析为对象接收 @RequestMapping(value = "/test", method = RequestMethod.POST) public String updateClusterIdByClientAndQueue(@RequestBody User user) { return user.getUsername() + user.getPassword(); }Copy to clipboardErrorCopied 复制代码一个请求可以有多个 @RequestParam,但只能有一个 @RequestBody。 URL 内含有参数时,两者可以同时使用。请求转发和重定向请求转发(forward)客户端(浏览器)向服务器 A 发送一个 URL 请求,服务器 A 会向另一台服务器 B 获取资源并将此资源响应给浏览器。浏览器的 URL 地址仍然是 A 。重定向(Redirect)客户端(浏览器)向服务器 A 发送一个 URL 请求,服务器 A 告知浏览器资源在服务器 B,浏览器会重新发送请求到服务器 B。浏览器的 URL 地址切换为 B。// 请求转发 @RequestMapping("/test1") public String test1(){ String type = 'forward'; return "forward:/test2?type=" + type; } // 重定向 @RequestMapping("/test2") public String test2(){ String type = 'redirect'; return "redirect:/test2?type=" + type; }Copy to clipboardErrorCopied 复制代码在拦截器中,常通过修改 HttpSevletRequest 对象实现请求转发。request.getRequestDispatcher("login").forward(request,response);
MySQL 概念MySQL 数据库MySQL 是一种关系型数据库。开源免费,并且方便扩展。在 Java 开发中常用于保存和管理数据。默认端口号 3306。MySQL 数据库主要分为 Server 和存储引擎两部分,现在最常用的存储引擎是 InnoDB。指令执行过程MySQL 数据库接收到用户指令后,首先由 Server 负责对数据操作的分析、处理和优化,再交给存储引擎执行数据存取操作。连接器连接器负责用户登录数据库时的身份认证,校验账户密码。校验通过后连接器会连接到权限表,并读取该用户的所有权限。如果连接未断开,即使该用户权限被管理员修改也不受影响。查询缓存缓存 SELECT 语句以及返回的结果。收到查询语句会首先和缓存比对,如果相同就直接从查询缓存里返回数据。更新表后,这个表上的所有的查询缓存都会被清空。这导致实际使用场景中查询缓存的作用非常少,在 MySQL 8.0 版本后移除。分析器如果查询语句未命中缓存,或者是更新语句,那么将由分析器负责分析 SQL 语句的用途。词法分析:提取关键字,提取 SQL 语句的关键元素,明确 SQL 语句的功能。语法分析:判断 SQL 语句是否正确,是否符合 MySQL 的语法。如果不符合语法则返回错误信息。优化器明确 SQL 语句功能后,由优化器负责选择尽可能最优的执行方案。比如多个索引的时候选择索引,多表查询的时候选择关联顺序。执行器确定执行方案后,由执行器负责校验该用户有没有权限,并交由存储引擎执行语句,然后从存储引擎返回数据。
权限管理用户权限分为非常多种,包括全局权限、库权限、表权限、列权限等。-- 赋予权限(GRANT) mysql> GRANT SELECT,INSERT ON *.* -- 赋予用户选择插入权限(所有库的所有表) -> TO 'boy'@'localhost' -- 不存在将新建用户 -> IDENTIFIED BY '123456' -> WITH GRANT OPTION; -- (可选)允许用户转授权限 -- 撤消权限(REVOKE) mysql> REVOKE INSERT ON *.* -> FROM 'boy'@'localhost'; -- 查看权限 mysql> SELECT Host,User,Select_priv,Grant_priv -> FROM mysql.user -> WHERE User='testUser';Copy to clipboardErrorCopied 复制代码数据库管理MySQL 内划分为多个互相独立的数据存储区域,调用数据库指令时必须提前声明要使用的数据库。数据库选项信息属性含义备注CHARACTER SET编码方式默认为 utf8mb4COLLATE校对规则默认为 utf8mb4_general_ci-- 查看所有数据库 mysql> SHOW DATABASES; -- 进入/切换数据库 mysql> USE mydb; -- 查看当前数据库 mysql> SELECT DATABASE(); -- 创建数据库 mysql> CREATE DATABASE [IF NOT EXISTS] mydb; mysql> CREATE DATABASE [IF NOT EXISTS] mydb CHARACTER SET utf8mb4; -- 删除数据库 mysql> DROP DATABASE [IF EXISTS] mydb; -- 查看数据库选项信息 mysql> SHOW CREATE DATABASE mydb; -- 修改数据库选项信息 mysql> ALTER DATABASE mydb CHARACTER SET utf8;Copy to clipboardErrorCopied 复制代码表管理表属性属性含义备注CHARSET字符集默认使用数据库字符集ENGINE存储引擎默认为 InnoDBDATA DIRECTORY数据文件目录INDEX DIRECTORY索引文件目录COMMENT表注释如果表标记为 TEMPORARY 则为临时表,在连接断开时表会消失。列属性属性含义备注PRIMARY KEY主键标识记录的字段。可以为字段组合,不能为空且不能重复。INDEX普通索引可以为字段组合,建立普通索引。UNIQUE唯一索引可以为字段组合,不能重复,建立唯一索引。NOT NULL非空(推荐)不允许字段值为空。DEFAULT默认值设置当前字段的默认值。AUTO_INCREMENt自动增长字段无需赋值,从指定值(默认 1)开始自动增长。表内只能存在一个且必须为索引。COMMENT注释字段备注信息。FOREIGN KEY外键该字段关联到其他表的主键。默认建立普通索引。表操作-- 查看所有表 mysql> SHOW TABLES; -- 创建表 mysql> CREATE [TEMPORARY] TABLE [IF NOT EXISTS] student ( id INT(8) PRIMARY KEY AUTO_INCREMENT=20190001, name VARCHAR(50) NOT NULL, sex INT COMMENT 'Male 1,Female 0', access_time DATE DEFAULT GETDATE(), major_id INT FOREIGN KEY REFERENCES major(id) )ENGINE=InnoDB; mysql> CREATE TABLE grade ( student_id INT, course_id INT, grade INT, PRIMARY KEY (student_id,course_id), CONSTRAINT fk_grade_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_grade_course FOREIGN KEY (course_id) REFERENCES course(id) ); -- 删除表 mysql> DROP TABLE [IF EXISTS] student; -- 清空表数据(直接删除表,再重新创建) mysql> TRUNCATE [TABLE] student; -- 查看表结构 mysql> SHOW CREATE TABLE student; mysql> DESC student; -- 修改表属性 mysql> ALTER TABLE student ENGINE=MYISAM; -- 重命名表 mysql> RENAME TABLE student TO new_student; mysql> RENAME TABLE student TO mydb.new_student; -- 复制表 mysql> CREATE TABLE new_student LIKE student; -- 复制表结构 mysql> CREATE TABLE new_student [AS] SELECT * FROM student; -- 复制表结构和数据 -- 检查表是否有错误 mysql> CHECK TABLE tbl_name [, tbl_name] ... [option] ... -- 优化表 mysql> OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ... -- 修复表 mysql> REPAIR [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ... [QUICK] [EXTENDED] [USE_FRM] -- 分析表 mysql> ANALYZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ...Copy to clipboardErrorCopied 复制代码列操作-- 添加字段 mysql> ALTER TABLE student ADD [COLUMN] age INT; -- 默认添加在最后一行 mysql> ALTER TABLE student ADD [COLUMN] age INT AFTER sex; -- 添加在指定字段后 mysql> ALTER TABLE student ADD [COLUMN] age INT FIRST; -- 添加在第一行 --修改字段 mysql> ALTER TABLE student MODIFY [COLUMN] id SMALLINT; -- 修改字段属性 mysql> ALTER TABLE student CHANGE [COLUMN] id new_id INT; -- 修改字段名 -- 删除字段 mysql> ALTER TABLE student DROP [COLUMN] age; -- 编辑主键 mysql> ALTER TABLE student ADD PRIMARY KEY(id,age); mysql> ALTER TABLE student DROP PRIMARY KEY; -- 编辑外键 mysql> ALTER TABLE student ADD CONSTRAINT fk_student_class FOREIGN KEY(cid) REFERENCES class(id); mysql> ALTER TABLE student DROP FOREIGN KEY fk_student_class;
RedisRedis 介绍NoSQL 技术在实际项目开发中,我们往往需要面对海量用户和高并发的数据请求。MySQL 等传统关系型数据库面临着两大问题:磁盘 IO 速度缓慢,单机读写速度不超过 10000 QPS,当数据库无法及时响应高并发的用户请求,请求积压进而导致数据库瘫痪。数据关系复杂,扩展性差。不适合大规模集群。因此我们必须引入 NoSQL 技术去解决以上两个问题,以作为关系型数据库的补充。Redis 数据库Redis 是一种基于内存的数据库技术。底层采用 C 语言开发,默认端口号 6379。Redis 数据库作为数据缓存,将业务数据直接存储在内存中进行读写,单机读/写速度可达 110000/84000 QPS,可以满足高速响应的需求。Redis 数据库只负责存储数据,数据之间不具有任何关联,易于扩容和伸缩。Redis 应用场景受限于内存的高昂成本,一般我们只使用 Redis 存储高频读写的关键数据。比如:热点数据:如热点商品信息。任务队列:如秒杀队列、抢购队列。实时更新信息:如商品排行榜、公交到站信息。时效性信息:如手机验证码、session 、 心跳(heartbeat)。Redis 主要适用于内部系统的高频数据。在线上环境负载极大的情况下,使用 Redis 也不足以满足对数据读写的速度要求。Redis 基本使用安装指令在控制台输入以下指令安装和使用 Redis:$ sudo apt-get install redis-server # 安装 Redis 数据库(仅限 Ubuntu 可用) $ redis-server # 启动 Redis 数据库 $ redis-server --port 6380 # 启动 Redis 数据库,在指定端口 $ redis-server redis-6379.conf # 启动 Redis 数据库,使用指定配置文件 $ redis-cli # 进入 Redis 控制台,在默认端口 $ redis-cli -p 6380 # 进入 Redis 控制台,在指定端口Copy to clipboardErrorCopied 复制代码基础配置在 Redis 安装目录下的 redis.conf 文件是 Redis 默认配置文件,启动 Redis 数据库时默认加载。daemonize no # 守护线程,打开后启动 Redis 控制台不提示 bind 127.0.0.1 # 绑定 IP 地址,绑定后只能通过该地址访问 Redis port 6379 # 端口号 databases 16 # 存储区域数量Copy to clipboardErrorCopied 复制代码日志配置Redis 总共支持四个日志级别:debug / verbose / notice / warning ,从前往后日志记录信息逐渐减少。通常情况下开发环境设为 verbose ,生产环境设为 notice 。loglevel verbose # 日志级别 logfile 6379.log # 日志文件名Copy to clipboardErrorCopied 复制代码持久化配置默认使用 RDB 方式持久化数据,相关配置如下:save 900 1 # 自动同步数据条件,900s 内变更 1 个 key 值则持久化 save 300 10 # 自动同步数据条件,300s 内变更 10 个 key 值则持久化 rdbcompression yes # 是否压缩数据,压缩后节省空间但读取较慢 rdbchecksum yes # 是否格式校验(默认开启),校验降低文件损坏风险但读取较慢 dbfilename dump.rdb # 保存文件名 dir ./ # 保存文件位置Copy to clipboardErrorCopied 复制代码可以在配置文件中改用 AOF 方式持久化数据,刷新文件条件有三种类型: always / everysec / no 。appendonly yes # 选用 AOF 方式持久化 appendsync everysec # 刷新文件条件,每秒更新一次操作日志Copy to clipboardErrorCopied 复制代码容量配置对 Redis 数据库占用空间和客户链接做出限制。maxclients 100 # 客户连接数上限,超出后拒绝客户访问,为 0 表示不限制 timeout 300 # 客户闲置时长,超出后关闭连接,为 0 表示不关闭 maxmemory 50 # Redis 最大占用内存比例,为 0 表示全部可用 maxmemory-samples # Redis 随机选取数据数量 maxmemery-policy volatile-lru # Redis 逐出策略Copy to clipboardErrorCopied 复制代码多机配置如果我们要设置集群,则需要进行以下配置:cluster enabled yes # 开启集群 cluster-config-file nodes.conf # 集群配置文件Copy to clipboardErrorCopied 复制代码如果我们要设置主从服务器,则需要进行以下配置:# 主服务器 requirepass 123456 # 主服务器设置密码(可选) repl-backlog-size 1mb # 缓冲区大小 # 从服务器 slaveof 127.0.0.1 6379 # 主服务器套接字,设置后自动连接 masterauth 123456 # 主服务器密码 slave-serve-stale-data no # 同步数据时是否允许读数据Copy to clipboardErrorCopied
Redis 基础在 Redis 中单个指令都是原子性操作,通过指令操作 Redis 数据时无需担心线程安全问题。Redis 以 key-value 的形式保存数据:key 值一定为 string 类型,而 value 值支持以下五种基础类型:数据类型存储形式string字符串hash哈希表list链表set哈希集sorted_set二叉树集存储区域Redis 将数据存储分为多个相互独立的区域,将 Redis 操作局限在自己的存储区域内。通常划分为 16 个(编号 0-15),默认使用编号 0 。> select 1 # 改用 1 号存储区域 > dbsize # 返回当前区域 key 数量 > move key 2 # 将当前 key 迁移到 2 号存储区域 > flushdb # 清空当前存储区域 > flushall # 清空全部存储区域Copy to clipboardErrorCopied 复制代码key 操作基本操作> del key # 删除 key > exists key # 判断是否存在 key > type key # 返回 key 对应的 value 类型 > rename key newkey # 重命名 > renamenx key newkey # 重命名(返回 1),新名称已存在则失败(返回 0) > sort # 对 key 排序Copy to clipboardErrorCopied 复制代码时效性控制Redis 中可以为 key 设置有效期,key 过期后会由 Redis 执行删除策略回收内存空间。> expire key 10 # key 10s 内有效 > expireat key 1355292000 # key 截至时间戳有效 > persist key # key 永久有效 > ttl key # 返回 key 剩余有效时间,若不存在返回 -2 ,永久返回 -1Copy to clipboardErrorCopied 复制代码查询操作Redis 支持查询存储区域内含有的 key,且允许使用以下通配符:* 表示任意数量字符? 表示任意一个字符[] 表示一个指定字符> keys * # 查询所有 key > keys user:* # 查询所有 user 的 key > keys id:75?? # 查询 ID 为 7500-7599 的 key > keys id:7[2345]55 # 查询 ID 为 7255/7355/7455/7555 的 keyCopy to clipboardErrorCopied 复制代码基础类型string 类型Redis 的 string 类型中,key 值对应的存储空间内将保存一个字符串数据,key 值标准命名格式为 表名:主键名:主键值:字段名,如 user:id:15942348:name - "王东浩"。基本操作> set key 10 # 设置键值对 > get key # 获取键值,不存在则返回 nil > del key # 删除键值对 > strlen key # 获取价值的字符串长度 > append key 0 # 在键值尾部追加 > mset key1 10 key2 100 # 设置多个数据 > mget key1 key2 # 获取多个数据 > setex key 10 1 # 设置键值对,10s 后自动删除 > psetex key 10 1 # 设置键值对,10ms 后自动删除Copy to clipboardErrorCopied 复制代码数据操作如果字符串为合法数字,可以当作数字处理。但数值不能超过 shell 中的 long 类型。> incr key # 键值加一 > decr key # 键值减一 > incrby key 10 # 键值加十 > decrby key 10 # 键值减十 > incrbyfloat key -1.5 # 键值加 -1.5Copy to clipboardErrorCopied 复制代码hash 类型hash 类型中,key 值对应的存储空间内可以保存多个键值对(field-value):field 和 value 都必须是字符串类型。当键值对较少时存储空间内采用数组存储,当键值对较多时采用哈希存储。十分适合存储对象,每个键值对记录对象的一个属性。基本操作> hset key field 10 # 设置/更新键值对 > hsetnx key field 10 # 如果键值不存在则设置键值对 > hget key field # 获取键值 > hgetall key # 获取全部键值 > hdel key field # 删除键值对 > hlen key # 获取键值对数量 > hexists key field # 判断是否存在字段(返回 1 或 0) > hmset key field1 1 field2 2 # 设置/修改多个键值对 > hmget key field1 field2 # 获取多个键值对Copy to clipboardErrorCopied 复制代码扩展操作> hkeys key # 返回 key 对应的所有 field > hvals key # 返回 key 对应的所有 value > hincrby key field 1 # 键值加一 > hdecrby key field 1 # 键值减一Copy to clipboardErrorCopied 复制代码list 类型list 类型中,key 值对应的存储空间内可以保存多个字符串数据,采用双向链表实现。具有索引的概念,但还是更适合从链表两侧操作。字符串总容量不能超过 2 的 32 次方。十分适合存储有序信息,比如粉丝列表。基本操作lpush list 1 # 链表左侧插入数据,返回下标 rpush list 2 # 链表右侧插入数据,返回下标 lpop list # 获取并删除最左侧数据 rpop list # 获取并删除最右侧数据 blpop list 10 # 获取并删除最左侧数据,不存在则至多等待 10 s lrem list 3 x # 从左侧开始,删除三个为 x 的数据 lrange list 0 2 # 返回左侧前3个数据 lrange list 0 -1 # 返回全部数据(常用) lindex list 0 # 返回指定位置数据 llen list # 返回字符串个数Copy to clipboardErrorCopied 复制代码set 类型set 类型中,key 值对应的存储空间内可以保存多个字符串数据,采用哈希存储实现。随机查询效率比 list 类型更高。字符串总容量不能超过 2 的 32 次方。十分适合存储集合类信息,比如用户感兴趣的话题、用户权限。基本操作sadd set member # 添加数据(可以是多个) srem set member # 删除数据(可以是多个) smembers set # 展示全部数据 scard set # 返回数据个数 sismember set # 判断是否含有数据 srandmember set 5 # 随机从集合中选取 5 个数据 spop set # 返回并删除一个随机数据Copy to clipboardErrorCopied 复制代码扩展操作sinter set1 set2 # 交 sunion set1 set2 # 并 sdiff set1 set2 # 差 sinterstore newset set1 set2 # 交且存入新集合 sunionstore newset set1 set2 # 并且存入新集合 sdiffstore newset set1 set2 # 差且存入新集合 smove oldset newset 5 # 数据从旧集合迁移到新集合Copy to clipboardErrorCopied 复制代码sorted_set 类型如果我们需要数据查询效率较高且有序,则可以使用 sorted_set 类型。底层和 set 结构相同采用哈希存储(value 值仍不可重复),但在 key-value 存储结构后添加 score 属性为数据排序,默认从小到大。score 是数字且可以使用小数,但如果使用小数浮点类型可能会出现精度丢失。可以用来存储排行榜等有序数据集合,还可以用于存储时效性或者带有权重的任务队列,用当前时间或者权重作为 score 。基本操作zadd set score1 member # 添加数据且标记序号(可以是多个)Copy to clipboardErrorCopied 复制代码高级类型此外,Redis 还提供了 Bitmaps、 HyberLogLog、GEO 三种高级数据类型,用来适配特定的应用场景。Bitmaps 类型Bitmaps 类型中用作存储布尔值:每个 key 对应若干字节数据(字节数 = 最大编号 / 8),每字节可以存储 8 个 boolean 值。如果 Redis 要存储大量 boolean 值,使用 Bitmaps 类型可以显著节省内存空间。setbit bits 0 1 # 将 0 位置为 1(true) getbit bits 0 # 取 0 位的值Copy to clipboardErrorCopied 复制代码HyperLogLog 类型HyperLogLog 类型用作数据统计,只记录数量不保存数据,且当数据量巨大时存在误差!使用 HyperLogLog 类型可以显著节省内存空间,每个 key 仅占用 12k 内存标记基数。setbit bits 0 1 # 将 0 位置为 1(true) getbit bits 0 # 取 0 位的值Copy to clipboardErrorCopied 复制代码GEO 类型GEO 类型用作地理位置计算,根据经纬度。
Redis 高级持久化Redis 使用内存存储,一旦断电可能会导致数据丢失。因此需要将数据保存到永久性存储介质中,防止数据意外丢失。如果 Redis 负责为数据库高热度数据访问加速或者一些其他业务(数据库中有重复数据),那么没必要为 Redis 数据持久化。Redis 持久化有以下两种方式:数据快照 RDB定时将全部数据存入文件。存储速度慢但是恢复数据的速度很快,如果保存不及时仍会丢失少量数据。数据以二进制形式默认存储在 安装目录/data/dump.rgb 文件。如果 Redis 数据库被关闭,下次重启时会从该文件读取数据。手动存储save # 数据存入文件(会阻塞 Redis 数据库,导致其他指令无法执行) bgsave # 数据存入文件(Redis 数据库调创建单独进程完成指令) debug reload # 重启 Redis,且关闭时将数据存入文件 shutrown save # 关闭 Redis,且关闭时将数据存入文件Copy to clipboardErrorCopied 复制代码修改配置在 安装目录/conf/redis-6379.conf 配置文件内可以修改默认配置:如果操作系统内安装了多个 Redis 数据库(使用不同的端口),必须通过修改存储文件名加以区分。dir data2 # 修改存储路径(默认 data) dbfilename dump-6379.rgb # 修改存储文件名(默认 dump.rgb) rdbcompression no # 关闭数据压缩(默认开启),读取文件加快但文件会变大 rdbchecksum no # 关闭格式校验(默认开启),读取文件加快但存在文件损坏风险 stop-writes-on-bgsave-error no # 后台存储出现错误不停止(默认停止)Copy to clipboardErrorCopied 复制代码通过修改配置文件,可以让 Redis 数据库可以自动调用 bgsave 指令更新 RDB 文件。save 100 10 # 自动存储(100s 内发生 10 个 key 数据变化时触发)Copy to clipboardErrorCopied 复制代码日志记录 AOF将对数据的操作过程存入文件。这种方式刷新更频繁因此丢失数据概率更低,但恢复数据的速度比 RDB 方式更慢,占用存储空间也更大。数据以二进制形式默认存储在 安装目录/data/appendonly.aof 文件。如果 Redis 数据库被关闭,下次重启时会根据该文件恢复数据。文件重写随着命令不断写入 AOF ,AOF 文件会越来越大,占用内存增多、恢复数据也会变慢。因此 Redis 需要对 AOF 文件进行重写,合并指令记录。rewriteaof # 重写 AOF 文件(会阻塞 Redis 数据库,导致其他指令无法执行) bgrewriteaof # 重写 AOF 文件(Redis 数据库调创建单独进程完成指令) Copy to clipboardErrorCopied 复制代码修改配置AOF 不是默认持久化方式,需要在 安装目录/conf/redis-6379.conf 配置文件内修改默认配置:必须通过配置文件开启并配置 AOF 存储。appendonly yes # 选用 AOF 方式持久化 appendsync always # 每次操作刷新文件:非常频繁,损耗性能 appendsync everysec # 每秒刷新文件(默认) appendsync no # 手动刷新文件Copy to clipboardErrorCopied 复制代码修改路径和文件名的操作和 RDB 方法类似。dir data2 # 修改存储路径(默认 data) dbfilename appendonly-6379.aof # 修改存储文件名(默认 appendonly.aof)Copy to clipboardErrorCopied 复制代码通过修改配置文件,可以让 Redis 数据库自动调用 bgrewriteaof 指令重写 AOF 文件。略,之后补充Copy to clipboardErrorCopied 复制代码事务假如我们通过多个操作执行一次购物,如果在这个过程中还执行了其他操作,可能导致我们的购物过程出现意想不到的错误。因此我们引入事务的概念,将多个操作看作一个不可分割的整体,统一执行而不会被其他操作打断。multi # 开启事务,之后的命令不再立刻执行、而是进入任务队列 # 输入事务内的命令 exec # 执行事务,执行任务队列里的命令 discard # 取消事务,清空任务队列Copy to clipboardErrorCopied 复制代码如果事务中包含语法错误(不能识别的命令),所有的命令都不会执行。如果事务中包含无法执行的命令,仅有出错的命令将不会被执行,其他被执行的命令需要开发者自行回滚。锁在事务准备的过程中,如果执行的其他操作导致触发事务的条件发生了变化,这个时候就不应该继续执行事务。我们引入了锁的概念来监视特定 key,在执行事务前如果其 value 发生了变化则终止事务执行。watch key1 key2 # 监视 key,书写在 multi 命令前 unwatch # 取消监视 key,书写在 multi 命令前 # 在之后执行事务Copy to clipboardErrorCopied 复制代码分布式锁如果 key 值变化极为频繁,那么使用普通锁会导致事务一直被终止。我们引入了分布式锁的概念,在加锁期间不允许其他进程对该值修改。setnx lock-num 1 # 对 key(num) 加公共锁,其他线程不能对其进行操作。成功则返回 1,若已有锁导致失败返回 0 # 输入命令或者事务 del lock-num # 对 key(num) 解公共锁Copy to clipboardErrorCopied 复制代码分布式锁如果长期不被释放,就会出现死锁,导致其他操作无法继续执行。我们可以对分布式锁计时。计时分布式锁常用于多部署平台统一竞争锁。expire lock-num 10 # 对 key(num) 加公共锁,10s 后自动释放 pexpire lock-num 10 # 对 key(num) 加公共锁,10ms 后自动释放Copy to clipboardErrorCopied 复制代码删除策略Redis 中每个存储区域除了存储 key-value 值,还会开辟额外的存储空间 expires 记录每个 key-value 的存储地址以及过期时间。如果 key 过期或被删除指令删除,那么 Redis 要执行删除策略清理内存空间。Redis 删除策略有以下三种方式,主要使用惰性删除和定期删除两种方式。定时删除key 过期后,存储 key-value 的内存地址立即被清空。节省内存资源,但可能抢占处在繁忙状态的 CPU。惰性删除key 过期后不做任何处理。访问 key 时才检查是否过期,如果过期存储该 key-value 的内存地址才被清空。节省 CPU 资源,但过期键值对可能大量占用内存。定期删除对于 16 个存储区域的 expires 进行轮询,对选中的 expires 随机选择 W 个 key 进行检查,如果 key 过期就进行删除。如果过期 key 超过 25%,那么重复检查该 expires 存储区域。如果过期 key 少于 25%,那么按顺序检查下一个 expires 存储区域。逐出策略如果 Redis 使用内存空间前会检查内存容量。如果已被占满,那么 Redis 要执行逐出策略删除部分数据,以清理内存空间执行指令。在选取删除数据时 Redis 并不会扫描全库数据,而是随机选取部分数据检测并从中删除:以节省 CPU 性能。响应配置如下:maxmemory 50 # Redis 最大占用内存比例,默认为 0(全部可用) maxmemory-samples # Redis 随机选取数据数量 maxmemery-policy volatile-lru # Redis 逐出策略Copy to clipboardErrorCopied 复制代码Redis 逐出策略有以下三种方式,在配置文件中配置即可。检查会过期数据volatile-lru :(推荐)挑选最久未使用的数据淘汰。volatile-lfu :挑选最近一段时间使用频率最低的数据淘汰。volatile-ttl :挑选将要过期的数据淘汰。volatile-random :随机挑选数据淘汰。检查全部数据allkeys-lru :挑选最久未使用的数据淘汰。allkeys-lfu :挑选最近一段时间使用频率最低的数据淘汰。allkeys-random :随机挑选数据淘汰。不逐出数据no-enviction :(默认)抛出错误 Out Of Memery。
Redis 潜在问题缓存故障Redis 缓存技术常用于高并发情况下,有效减轻服务器和数据库负载。如果 Redis 出现问题导致无法均衡负载,就可能导致服务崩溃。缓存预热当系统刚启动时,由于 Redis 尚未保存数据导致无法命中,数据库被频繁请求数据,由于过载导致数据库崩溃。数据库崩溃后, Redis 和应用服务器无法获取数据,请求积压会进一步导致 Redis 和服务器崩溃。缓存雪崩当流量激增时,如果 Redis 大量 key 过期导致无法命中,数据库被频繁请求数据,由于过载导致数据库崩溃。数据库崩溃后, Redis 和应用服务器无法获取数据,请求积压会进一步导致 Redis 和服务器崩溃。缓存击穿当流量激增时,如果 Redis 某个极高热度的 key 过期导致无法命中,数据库被频繁请求数据,由于过载导致数据库崩溃。数据库崩溃后, Redis 和应用服务器无法获取数据,请求积压会进一步导致 Redis 和服务器崩溃。缓存穿透当流量激增时,如果 Redis 收到大量非法访问导致无法命中,数据库被频繁请求数据,由于过载导致数据库崩溃。数据库崩溃后, Redis 和应用服务器无法获取数据,请求积压会进一步导致 Redis 和服务器崩溃。一致性问题如果在缓存中存储数据库数据备份,以提高查询效率,就一定会出现一致性问题,导致脏读。比如数据库中数据从 1 更新到 10 ,但缓存还未更新时读取,就会读取到 1。这个问题难以避免。缓存就是缓存,必须要设过期时间。实时性要求比较高的(比如充值),直接读数据库。数据库并发高需要分库分表。Redis 客户端我们在实际使用 Redis 时往往要通过 Redis 客户端,以便在程序中直接操作 Redis 。常使用的 Redis 客户端有 Jedis、 以及功能更为高级的 Redisson、Lettuce 等。RedisTemplate 类Spring Boot 提供了 RedisTemplate 工具类直接对 Redis 进行操作,也提供了 StringRedisTemplate 类继承 RedisTemplate 类,两者方法完全一致。RedisTemplate 类:存储数据时序列化成字节数组保存,在 Redis 中数据为字节码。读取数据时自动转化为对象。StringRedisTemplate 类:存储数据直接以字符串形式保存,在 Redis 中数据直接可读。只适用于字符串类型的数据。由于两种序列化方法不同导致的数据存储形式差异,两个类之间不能对另一方存储的 Redis 数据进行操作。常用方法/* 直接对 key 操作 */ redisTemplate.delete("key"); // 删除 key redisTemplate.delete(collection); // 批量删除 key redisTemplate.expire("key",10,TimeUnit.MINUTES); // 设置 key 失效时间 Long expire = redisTemplate.getExpire("key"); // 获取 key 失效时间 boolean flag = redisTemplate.hasKey("key"); // 判断 key 是否存在 /* 操作字符串 */ redisTemplate.opsForValue().set("key", "value"); // 设置键值对 String str = (String)redisTemplate.opsForValue().get("key"); // 获取键值 /* 操作 hash */ redisTemplate.opsForHash().put("HashKey", "SmallKey", "HashValue"); // 设置键值对 redisTemplate.boundHashOps("HashKey").putAll(hashMap); // 批量设置键值对 String value = (String) redisTemplate.opsForHash().get("HashKey", "SmallKey"); // 获取键值 Map entries = redisTemplate.opsForHash().entries("HashKey"); // 获取全部键值对 redisTemplate.boundHashOps("HashKey").delete("SmallKey"); // 删除键值对 Boolean isEmpty = redisTemplate.boundHashOps("HashKey").hasKey("SmallKey"); // 是否含有键值对 redisTemplate.opsForList(); // 操作 list redisTemplate.opsForSet(); // 操作 set redisTemplate.opsForZSet(); // 操作有序 set
Jedis 客户端Jedis 基于 Java 实现,是 shell 程序连接 Redis 数据库最常使用的工具。提供了比较全面的 Redis 命令的支持。Jedis 使用阻塞 I/O,且其方法调用都是同步的,程序流需要等到 sockets 处理完 I/O 才能执行。Jedis 采取直连模式,在多个线程间共享一个 Jedis 实例线程不安全,多线程操作 Redis 必须要使用多个 Jedis 实例。导入依赖Spring Boot 2.x 版本 Redis 默认导入了 lettuce,需要排除才能使用 Redis .<!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <!-- Jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>Copy to clipboardErrorCopied 复制代码基本使用使用引入的 Jedis 类即可连接 Redis 数据库并进行操作。操作名取自 Redis 指令,如果出现问题则会抛出 JedisDataException。import redis.clients.jedis.Jedis; public class JedisTest{ @Test public void jedisTest (){ // 连接 Redis Jedis jedis = new Jedis("127.0.0.1", 6379); // 对 Redis 操作(直接使用 Redis 指令) try { jedis.set("name", "MrJoker"); System.out.print(jedis.get("name")); } catch(JedisDataException e) { System.out.print("error"); } finally { // 关闭 Redis 连接 jedis.close(); } } }Copy to clipboardErrorCopied 复制代码在实际开发中,创建多个 Redis 连接会非常复杂且难以管理,Jedis 提供了 JedisPool 类作为 Redis 连接池来管理 Redis 连接。import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class JedisTest{ @Test public void jedisTest (){ // 配置连接池 JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxIdle(50); // 最大空闲数 poolConfig.setMaxTotal(100); // 最大连接数 poolConfig.setMaxWaitMillis(20000); // 最大等待毫秒数 // 创建连接池 JedisPool pool = new JedisPool(poolConfig, "localhost"); // 从连接池中获取单个连接 Jedis jedis = pool.getResource(); // 如果需要密码 //jedis.auth("password"); } }Copy to clipboardErrorCopied 复制代码Spring Boot 集成Spring Boot 中,我们无需自行创建 Redis 连接,只需要在配置文件中配置好参数。# REDIS配置 # Redis数据库索引(默认为0) spring.redis.database=0 # Redis服务器地址 spring.redis.host=localhost # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password= # 连接池最大连接数(使用负值表示没有限制) spring.redis.pool.max-active=8 # 连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.pool.max-wait=-1 # 连接池中的最大空闲连接 spring.redis.pool.max-idle=8 # 连接池中的最小空闲连接 spring.redis.pool.min-idle=0 # 连接超时时间(毫秒) spring.redis.timeout=0Copy to clipboardErrorCopied 复制代码Spring Boot 提供默认的 RedisTemplate 工具类根据配置文件自动连接 Redis,自动加载后可以直接调用其中的方法去操作。@RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest() public class ApplicationTests { @Autowired private RedisTemplate redisTemplate; @Test public void test() throws Exception { User user = new User(); user.setName("我没有三颗心脏"); user.setAge(21); // 调用工具类方法 redisTemplate.opsForValue().set("user_1", user); User user1 = (User) redisTemplate.opsForValue().get("user_1"); System.out.println(user1.getName()); } }Copy to clipboardErrorCopied 复制代码RedisTemplate 类常用操作redisTemplate.delete(key); // 删除 key redisTemplate.delete(keys); // 批量删除 key redisTemplate.expire(key,time,TimeUnit.MINUTES); // 设置 key 失效时间 Long expire = redisTemplate.getExpire(key); // 获取 key 失效时间Copy to clipboardErrorCopied 复制代码Lettuce 客户端更加高级的 Redis 客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。基于 Netty 框架的事件驱动的通信层,其方法调用是异步的。不用浪费线程等待网络或磁盘 I/O。Lettuce 的 API 是线程安全的,所以可以操作单个 Lettuce 连接来完成各种操作。导入依赖在 spring boot 2.x 版本,为 Redis 默认导入了 Lettuce 。<!-- Redis 默认导入 Lettuce --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>Copy to clipboardErrorCopied 复制代码如果 Spring Boot 版本过低,也可以自行导入 Lettuce. Redis 版本至少需要 2.6 .<!-- 单独导入 Lettuce --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> <version>5.1.8.RELEASE</version> </dependency>Copy to clipboardErrorCopied 复制代码基本使用public class LettuceTest { @Test public void testSetGet() throws Exception { // 注册连接信息 RedisURI redisUri = RedisURI.builder() .withHost("localhost") .withPort(6379) .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); // 创建 Redis 客户端 RedisClient redisClient = RedisClient.create(redisUri); // 创建连接 StatefulRedisConnection<String, String> connection = redisClient.connect(); // 创建同步命令 RedisCommands<String, String> redisCommands = connection.sync(); SetArgs setArgs = SetArgs.Builder.nx().ex(5); String result = redisCommands.set("name", "throwable", setArgs); Assertions.assertThat(result).isEqualToIgnoringCase("OK"); result = redisCommands.get("name"); Assertions.assertThat(result).isEqualTo("throwable"); /******************** 其他操作 **********************/ connection.close(); // 关闭连接 redisClient.shutdown(); // 关闭客户端 } }Copy to clipboardErrorCopied 复制代码Lettuce 主要提供三种API:同步(sync)RedisCommands、异步(async)RedisAsyncCommands、反应式(reactive)RedisReactiveCommands。Spring Boot 集成同样在配置文件中配置好参数。spring.redis.host=localhost spring.redis.port=6379 spring.redis.password=root # 连接池最大连接数(使用负值表示没有限制) 默认为8 spring.redis.lettuce.pool.max-active=8 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认为-1 spring.redis.lettuce.pool.max-wait=-1ms # 连接池中的最大空闲连接 默认为8 spring.redis.lettuce.pool.max-idle=8 # 连接池中的最小空闲连接 默认为 0 spring.redis.lettuce.pool.min-idle=0Copy to clipboardErrorCopied 复制代码我们同样可以使用 Spring Boot 提供默认的 RedisTemplate 工具类根据配置文件自动连接 Redis。但默认情况下的模板只支持 RedisTemplate<String,String> 存入字符串,因此我们往往需要自定义 RedisTemplate 设置序列化器,以方便操作实例对象。@Configuration public class RedisConfig { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>(); // key 采用 String 的序列化方式 redisTemplate.setKeySerializer(new StringRedisSerializer()); // value 采用 jackson 的序列化方式 redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // hash 采用 String/jackson 的序列化方式 redisTemplate.setHashKeySerializer(stringRedisSerializer); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); redisTemplate.setConnectionFactory(connectionFactory); return redisTemplate; } }Copy to clipboardErrorCopied 复制代码完成后即可用自定义的 RedisTemplate 工具类对 Redis 进行操作。@RunWith(SpringRunner.class) @SpringBootTest public class RedisTest { @Autowired private RedisTemplate<String, Serializable> redisTemplate; @Test public void test() { String key = "user:1"; redisTemplate.opsForValue().set(key, new User(1,"pjmike",20)); User user = (User) redisTemplate.opsForValue().get(key); } }作者:武师叔链接:https://juejin.cn/post/7132418128625008647来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
HTML定义页面元素。浏览器加载 html 文件后,会将其解析为树形结构 DOM :每个 html 标签和属性都作为一个 DOM 树节点。在全部代码(包含引入文件)解析完成后,将 DOM 树绘制并渲染为用户可见的页面。html 页面根标签<!DOCTYPE html> <html> <!--your code--> </html>Copy to clipboardErrorCopied 复制代码head 页面头部title 页面标题<title>页面标题</title>Copy to clipboardErrorCopied 复制代码meta 页面元数据页面作者 / 页面描述<meta name="author" content="MrJoker"> <meta name="description" content="这是你的页面。">Copy to clipboardErrorCopied 复制代码页面类型 / 页面编码<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">Copy to clipboardErrorCopied 复制代码http-equiv:元数据在 response 头部返回浏览器link 外部资源链接引入 网页图标 / CSS<link href="/favicon.ico" rel="icon" type="image/x-icon" /> <link href="asserts/css/bootstrap.min.css" rel="stylesheet" type="text/css" />Copy to clipboardErrorCopied 复制代码style 页面样式<style type="text/css"> body {background-color:yellow} p {color:blue} </style>Copy to clipboardErrorCopied 复制代码script 执行脚本引入外部脚本文件<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js">Copy to clipboardErrorCopied 复制代码自定义脚本<script type="text/javascript"> document.getElementById("demo").innerHTML = "My first JavaScript"; </script> 复制代码页面内容 bodydiv / span 区域<div> 为块状元素:独占一行<span> 为内联元素:和其他内联元素共享一行h1 - h6 标题<h1>Chapter 1</h1>p 文本<p>This is my first paragraph.</p>a 超链接<p href="www.baidu.com">This is my second paragraph.</p>href:跳转网址img 图片<img src="boat.gif" alt="Big Boat" />src:图片地址alt:图片不能加载时的替代文本table 表格每个单元项表示为<tr>每个单元格表示为<td> ,标题单元格表示为<th><table> <tr> <th>学号</th> <th>姓名</th> </tr> <tr> <td>0021</td> <td>陈柏言</td> </tr> <tr> <td>0022</td> <td>邓怀瑾</td> </tr> </table>Copy to clipboardErrorCopied 复制代码form 表单<form action="action_page.php" method="GET"> Name: <input type="text" name="name" value="Input your nane"> <br/> Sex: <input type="radio" name="sex" value="male" checked>Male <input type="radio" name="sex" value="female">Female <br/> <input type="submit" value="Submit"> </form> Copy to clipboardErrorCopied 复制代码action:请求发送地址(URL)method:请求类型提交按钮<input type="submit" value="Submit">输入类型输入框Username: <input type="text" name="name" value="MrJoker"> <br/> Password: <input type="password" name="password"> Copy to clipboardErrorCopied 复制代码选择框Sex: <input type="radio" name="sex" value="male" checked>Male <input type="radio" name="sex" value="female">Female <br/> Vehicle: <input type="checkbox" name="vehicle" value="Bike">I have a bike <input type="checkbox" name="vehicle" value="Car">I have a carCopy to clipboardErrorCopied 复制代码下拉菜单Your Car: <select name="cars"> <option value="volvo">Volvo</option> <option value="saab">Saab</option> <option value="fiat">Fiat</option> <option value="audi">Audi</option> </select>Copy to clipboardErrorCopied 复制代码文本域<textarea name="message" rows="10" cols="30">Descript your car here.</textarea>Copy to clipboardErrorCopied作者:武师叔链接:https://juejin.cn/post/7132786641998970911来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
CSS定义页面样式。浏览器在构造完成页面的 DOM 树后,会解析 css 代码以及引入的 css 文件,将定义的样式表规则添加到对应的 DOM 树节点上。在绘制并渲染为用户可见的页面时使用。选择器选择器确定 CSS 样式的作用范围。<div> <h1>title</h1> <p class="px" id="p1">first paragraph</p> <p class="px" id="p2">second paragraph</p> <a type="button" href="./next.html">next</a> </div> <style> div { letter-spacing:0.5em; } /* 简单选择器 */ .px { font-size:20px; } /* 类选择器 */ #p1 { color:brown; } /* ID 选择器 */ [type="button"] { display:block; } /* 属性选择器 */ </style>Copy to clipboardErrorCopied 复制代码复杂类选择器<p class="important welcome">Hello</p> <div class="warning"> <h1>NO</h1> <p class="text">STOP</p> </div> <style> /* 多重条件触发 */ .important.welcome { background:silver; } /* 嵌套条件触发*/ div h1 { font-weight:bold; } .warning .text { background:silver; } </style>Copy to clipboardErrorCopied 复制代码定位方式静态定位(static):默认情况,元素从上到下依次放入 html 页面。相对定位(relative):根据其正常位置偏移。绝对定位(absolute):根据首个非静态父元素位置偏移。固定定位(fixed):相对于浏览器窗口偏移,即使滚动窗口也不发生改变。position:relative; 设置元素为相对定位position:absolute; 设置元素为绝对定位position:fixed; 设置元素为固定定位/* 绝对定位到父元素左上角 */ .box { position: absolute; left: 10px; /* 同时设置 left 和 right 会强置元素宽度 */ top: 10px; /* 同时设置 top 和 bottom 会强置元素高度 */ } Copy to clipboardErrorCopied 复制代码子元素设置为 absolute 定位后,一般要把对应的父元素定位切换到 relative .元素框元素从外到内依次为:外边距(margin) > 边框(border) > 内边距(padding) > 内容(content)外边距默认透明。上下两元素外边距取最大值:外边距重叠。可以取负值:相邻两元素重叠。.block { margin: 0 30px; border: 2px solid #88b7e0; padding: 0 5px 0 10px; width: 100%; height: 180px; }Copy to clipboardErrorCopied 复制代码当元素大小随父元素浮动时,可以为元素最大或最小尺寸。.block { min-height: 70px; max-height: 280px; }Copy to clipboardErrorCopied 复制代码计算元素尺寸时,可以使用四则运算式。.block { min-height: calc(100% - 80px); max-height: 100%; }Copy to clipboardErrorCopied 复制代码元素放置类型块级元素独占一行,可任意规定宽高和内外边距大小。宽度默认是容器的100%,以容纳内联元素和其他块元素。常用的块状元素有:<div>、<p>、<h1>...<h6>、<ol>、<ul>、<dl>、<table>、<address>、<blockquote> 、<form>内联元素和其他内联元素分享一行,宽高随内容自动变化(无法设置),不可设置上下边距大小,但可以设置左右边距。行内元素内不能容纳块元素。常用的行内元素有:<a>、<span>、、<i>、<em>、<strong>、<label>、<q>、<var>、<cite>、<code>行内块状元素同时具备行内元素、块状元素的特点,和其他元素在同一行,高、宽和边距可以设置。常用元素 <img>、<input>display:block 设置为块级元素display:inline 设置为内联元素display:inline-block 设置为行内块级元素display:none 不摆放该元素居中在父级父容器中让行内元素居中对齐:水平居中对于块状元素,只需要规定 margin: 0 auto ;对于内联元素(如文本和链接),只需要在块级父容器中添加 text-align: center 。垂直居中对于已知高度的块状元素,通过.parent { position: relative; } .child { position: absolute; top: 50%; height: 100px; margin-top: -50px; }Copy to clipboardErrorCopied 复制代码对于未知高度的块状元素,通过(该方法也适用于水平居中).parent { position: relative; } .child { position: absolute; top: 50%; transform: translateY(-50%); /* left:50%; transform: translate(-50%,-50%); */ }Copy to clipboardErrorCopied 复制代码对于内联元素,只需为它们添加等值的 padding-top 和 padding-bottom 就可以实现垂直居中。可见隐藏元素共有三种方式,效果不同:display:none 元素不摆放,等同于没有visibility:hidden 元素隐藏,不可用但仍占用布局空间opacity:0 元素透明,可用但不可见(不透明度0-1)当多个元素堆叠在同一个位置时,可以指定摆放层次:z-index: 5 元素摆放在第 5 层,同位置元素数值较大者用户可见文本样式基本h2 { color: white; /* 字体颜色 */ font-size: 15px; /* 字体大小 */ font-family: sans-serif; /* 字体样式 */ }Copy to clipboardErrorCopied 复制代码间距letter-spacing:0.5em; 字符间距word-spacing:0.5em; 单词间距(只对英文起作用)text-indent: 2em; 首行缩进,设置为2字符溢出overflow-x:hidden; 水平方向:文本超出内容框后隐藏overflow-y:auto; 垂直方向:视情况提供滚动条参数含义visible文本超出后会显示在内容框之外。hidden文本超出部分被裁剪。scroll提供滚动条。auto文本超出后提供滚动条。no-display如果内容不适合内容框,则删除整个框。no-content如果内容不适合内容框,则隐藏整个内容。图片样式背景background: url("../assets/x.jpg") no-repeat left; 背景图表格样式边框table 有外边距(margin),但有无内边(padding)距视情况而定:【默认情况】单元格(td)分散table { border-collapse: separate; /* 单元格分散,内边距有效 */ border-space: 0; /* 单元格间距,设为 0 起到紧贴效果 */ padding: 1px; /* 单元格和表格外框间距 */ }Copy to clipboardErrorCopied 复制代码【常用情况】单元格(td)紧挨table { border-collapse: collapse; /* 单元格紧贴,内边距无效 */ }Copy to clipboardErrorCopied 复制代码tr 和 td/th 作为内部元素无外边距(margin)。筛选tr:对指定行采用不同样式/* 奇数行元素 */ table tr:nth-child(odd){ background: #eeeff2; } /* 偶数行元素 */ table tr:nth-child(even){ background: #dfe1e7; } Copy to clipboardErrorCopied 复制代码td:对指定列采取不同样式/* 第3列以前的全部元素 */ tr td:nth-child(-n+3){ text-align:center; } /* 第4列以后的全部元素 */ tr td:nth-child(n+4){ text-align:center; }作者:武师叔链接:https://juejin.cn/post/7133031913237381133来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
这有可能是你见过最离谱的标题这有可能是你没见过的技术文章模式我不知道我的选择是否正确,但是我的想法就是:不再让技术冷冰冰,让所有人学习中获得快乐!序言这是一个奇妙的世界,本来人们在这个世界上相处融洽,大家互相包容,没有任何制度......人们在这个世界上感受到了一丝丝的温馨幸福,但是反而到来的是制度的缺失导致的无畏纷争和内心世界极大的空虚~~但是突然的一天,这一切都她被改变了,她,来自天界,一个身穿天青色长裙的仙女,拥有一种神秘的功法能让本来复杂性的工作大大缩短时间;而且之前常用的大型机器设备,也通过她的改造竟然用一台小而精妙的工具就能实现,大大减少了空间;有些人偷看女子施法,竟然发现这种功法就是在一个只有26个按键机器上行云流水的反复敲打,这是因为如此惊人的工作效率,有些志气风发的少年决定向她学习这种功法,但是当仙女见到这些少年,都是摇摇头,说着:不具备这种天赋。当到最后一个少年,仙女还是摇了摇头说:“不行!”最后的他竟然没有离去,冒着“大不敬”出口就说:“难道你说不行就不行,我们的人生由我们自己努力,不是你在上面摇摇头就能否定我们一生的价值与意义,我的人生有我们自己定义,而不是你!”仙女大为震惊,世间竟然能有如此气魄的少年,他也是古今第一人了。于是仙女收他为徒,得知这种功法名为算法,在传授算法技巧,不出几年这位少年就成为大修行者但是距离突破天境还差一步,因为还没有得到算法心法的传授根据天界规则不许将心法传授给凡人,但是少年不希望这种功法一直被天界人掌控,希望他能被每一个人掌握,人们生来平等都有受教育的权利。于是他趁着仙女去天界拜寿以奴婢的身份把心法偷出了天界还没等到世间,就被天界的人围追堵截,情急之下,把心法用尽全身的力气抛到人间这本心法刚好砸到了正在晒太阳的你,你看到这本书还不知是什么于是就开始翻阅~~~第一章 | 初识算法之复杂度学习数据结构与算法的第一歩,永远都是复杂度分析复杂度分析毫不夸张的说,这就是数据结构与算法学习的内核所在。你学会了它你就登堂入室,封侯拜相,你学不会它,你就永远是孤苦伶仃的卖炭翁!(会背的小伙伴可以吟诵起来啊)那你就会问我,为什么复杂度分析会这么重要?我们平常工作摸鱼的时候,总是想着当当咸鱼划划水老板的钱就能钻进我的卡里,更好就是能泡个澡按个脚睡个觉,每天就有工资领~~(嘻嘻嘻,年轻人不要幻想了,小心被洗脚水淹到哦)数据结构与算法虽然没有我们有这么大的梦想,但是它的出现也是想着花更少的时间和更少的存储来解决问题。一句话————花少钱办大事那如何去考量“更少的时间和更少的存储”,用来衡量时间和储存的量度复杂度分析为此而生。事后统计法概括那肯定有同学出来反驳说:现在有很多工具啊包啊,代码随便跑一下,就能轻轻松松知道运行了多少时间占了多少内存啊。算法的效率不就轻松比对出来了么?这种方法就是我们常说的事后统计法,那边都已经写完代码了,你之后统计知道了,那我要你跑出效率图像还个屁用,类似于前段时间B站崩溃事件,我知道那块JS代码部分忽略了字符串"0"的注入导致时间复杂度剧增,导致服务器瘫痪了,写代码的时候就测出来了,那样还会耽误我看舞蹈区,额呸,知识区来学习吗简单来说,就是你需要提前写好算法代码和编好测试数据,然后在计算机上跑,通过最后得出的运行时间判断算法时效的高低,这里的运行时间就是我们日常的时间。缺点首先,事后统计法太依赖计算机的软件和硬件等性能。再者,事后统计法太依赖于测试数据集的规模。结果可以看出,我们需要一个不依赖于性能和规模等外力影响就可以估算算法效率、判断算法优劣的度量指标,而复杂度分析天生就是干这个的,这也是为什么我们要学习并必须学会复杂度分析!时间复杂度时间复杂度,也就是指算法的运行时间。对于某一问题的不同解决算法,运行时间越短算法效率越高,相反,运行时间越长,算法效率越低。那么如何估算时间复杂度呢?当用运行时间去描述一个算法快慢的时候,算法中执行的总步数显得尤为重要。因为只是估算,我们可以假设每一行代码的运行时间都为一个 Btime,那么算法的总的运行时间 = 运行的总代码行数。下面我们来看这么一段简单的代码。def dogeggs_sum (n): sum = 0 for dogegg in range(n): sum += dogegg return sum这段求累加和的代码总的运行时间是多少呢?第 2 行代码需要 1 Btime 的运行时间,第 4 和 第 5行分别运行了 n 次,所以每个需要 n * Btime 的运行时间,所以总的运行时间就是 (1 + 2n) * Btime。我们一般用 T 函数来表示赋值语句的总运行时间,所以上面总的运行时间就可以表达成 T(n) = (1 + 2n) * Btime,翻译一下就是“数据集大小为 n,总步数为 (1 + 2n) 的算法的执行时间为 T(n)”。通过公式,我们可以看出 T(n) 和总步数是成正比关系,这个规律的发现其实是很重要的,因为这个告诉了我们一种趋势,数据集规模和运行时间之间有趋势!大 O 表示法大 O 表示法,表示的是算法有多快。这个多快,不是说算法代码真正的运行时间,而是代表着一种趋势,一种随着数据集的规模增大,算法代码运行时间变化的一种趋势。一般记作 O(f(n))。那么之前的公式就变成 T(n) = O(f(n)),其中 f(n) 是算法代码执行的总步数,也叫操作数。n 作为数据集大小,它可以取 1,10,100,1000 或者更大更大更大的数,到这你就会发现一件很有意思的事儿,那就是当数据集越来越大时,你会发现代码中的某些部分“失效”了。还是以之前的代码为例。当 n = 1000 时,1 + 2n = 2001,当 n = 10000 时,1 + 2n = 20001,当这个 n 持续增大时,常数 1 和系数 2 对于最后的结果越来越没存在感,即对趋势的变化影响不大。所以对于我们这段代码样例,最终的 T(n) = O(n)。如果 T(n) = O(n) + O(n²) + O(n³),按照我们取“主导”部分,显然前面两个小弟都应该直接 pass,最终 T(n) = O(n³)。对于时间复杂度分析来说,只要找起“主导”作用的部分代码即可,这个主导就是最高的那个复杂度,也就是执行次数最多的那部分 n 的量级。常见时间复杂度算法学习过程中,我们会遇到各种各样的时间复杂度,但纵使你代码七十二变,几乎也逃不过下面这几种常见的时间复杂度。时间复杂度阶f(n) 举例常数复杂度O(1)1对数复杂度O(logn)logn + 1线性复杂度O(n)n + 1线性对数复杂度O(nlogn)nlogn + 1k 次复杂度O(n²)、O(n³)、....n² + n +1指数复杂度O(2n)2n + 1阶乘复杂度O(n!)n! + 1空间复杂度空间复杂度和时间复杂度一样,反映的也是一种趋势,只不过这种趋势是代码运行过程中临时变量占用的内存空间。代码在计算机中的运行所占用的存储空间呐,主要分为 3 部分:代码本身所占用的输入数据所占用的临时变量所占用的前面两个部分是本身就要占这些空间,与代码的性能无关,所以我们在衡量代码的空间复杂度的时候,只关心运行过程中临时占用的内存空间。空间复杂度记作 S(n),表示形式与时间复杂度一致。在这同样 n 作为数据集大小,f(n) 指的是规模 n 所占存储空间的函数。空间复杂度分析下面我们用一段简单代码来学习下如何分析空间复杂度。def dogeggs_sum(lst): sum = 0 for i in range(len(lst)): sum += lst[i] return sum上述代码是求列表 lst 的所有元素之和,根据之前说的,只计算临时变量所占用的内存空间。形参 lst 所占的空间不计,那么剩下的临时变量只有 sum 和 i,sum 是存储和,是常数阶,i 是存储元素位置,也是常数阶,它俩所分配的空间和规模 n 都没有关系,所以整段代码的空间复杂度 S(n) = O(1)。第二章 | 初探数据结构—数组-至纯之物含义数组是一种基础的线性数据结构,它是用连续的一段内存空间,来存储相同数据类型数据的集合。这里面有两个重点:线性数据结构连续内存存储相同数据类型线性结构线性数据结构有限的,它是某类元素的集合并且记录着元素之间的一组顺序关系,除了首尾之外,其余元素之间有且只有一个前驱和后继。除了数组以外,像单链表、栈、队列等也是典型的线性数据结构。连续内存存储的相同数据类型以一个长度为 6 的 int 类型的数组为例,理解一下“连续内存存储相同数据类型”数组的样砸。上图中的计算机给数组 a 分配了一块连续的内存空间 1000 - 1005,首地址 address[0] = 1000。因为连续,且对于数组来说下标从 0 开始的,所有对于数组中的每个元素来说,它的内存地址就很好计算: address[i] = address[0] + i 数组的操作从上面可以看出,连续内存存储使得数组有一个天然的优势,这个优势就是可以根据下标,快速的随意访问任意一个位置的数组元素。查找因为数组可以保证不管你访问哪个元素,只要给出下标,只需进行一次操作,就可以定位到元素的位置,从而拿出这个位置上的值。这步操作的时间复杂度就是 O(1)。连续的内存使得在插入或者删除的元素的时候,其它元素也要相应的移动。插入比如我们在下标为 2 的位置上插入一个元素 29:为了保持连续内存存储,在下标为 2 的位置上插入 29,原先 2 - 5 下标位置上元素都要向后一步走,可以看出这一步操作的时间复杂度为 O(n)。一般我都是按照最坏情况时间复杂度来算。对于插入来说,最好的情况肯定是插在末尾,这样所有的元素都不用动,时间复杂度为 O(1),那最坏的情况就是在数组的开头插入,这样所有的元素都要动,时间复杂度就成了 O(n),请大家一定要注意。删除对删除来说,和插入操作也差不多,比如我们删除下标为 2 位置上的元素。删除了下标 2 位置上的数字 a[2], 原来下标 3 - 5 位置上的元素统统向前一步走。同样对于删除来说,最好情况是删除末尾的元素,时间复杂度为 O(1),最坏的情况是删除首位的元素,时间复杂度是 O(n)。特别注意在做数组类问题的时候要注意数组越界的问题。数组越界,简单来说就是对于一个大小为 n 的数组,它的下标应是 0 到 n-1,对这 n 个位置的元素之外的访问,就是非法的,这就叫做“越界”。第三章 | 初探数据结构—链表-长而简之出现缘由第二章 | 初探数据结构—数组的文章中说过,连续的内存使得数组在进行插入和删除时需要移动大量元素,这就意味着要耗费时间。相比于数组,链表操作是可以缩稍时间,但是也是复杂一些的数据结构。定义线性表的链式存储结构生成的表,叫做链表。那么什么是链式存储结构咧?链式存储结构是指用一组任意的存储单元存储线性表的数据元素,通过指针连接串联起来。这里的“任意”指的就是,存储单元可以连续也可以不连续,这就意味着它们可以是内存中任何未被占用的地方。链表中的存储单元叫做节点。它和数组中只存数据信息不同,每个节点分为两部分:数据域和指针域。数据域存储的数据,指针域存储着同一个表里下一个节点的位置。因为链表家族里的兄弟姐妹太多,在这里咧我只讲常见的几种链表结构:单链表、双向链表。单链表单链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。链接的入口节点称为链表的头结点也就是head。如图所示: 双链表单链表中的指针域只能指向节点的下一个节点。双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。双链表 既可以向前查询也可以向后查询。如图所示: 循环链表循环链表,顾名思义,就是链表首尾相连。循环链表可以用来解决约瑟夫环问题。链表操作删除节点删除D节点,如图所示:只要将C节点的next指针 指向E节点就可以了。那有同学说了,D节点不是依然存留在内存里么?只不过是没有在这个链表里而已。是这样的,所以在C++里最好是再手动释放这个D节点,释放这块内存。其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。添加节点如图所示:可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。性能分析再把链表的特性和数组的特性进行一个对比,如图所示:数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。第四章 | 初探数据结构—哈希表-杂而不乱出现缘由要查一个数在数组中的位置,那可是太费劲了,只能从头开始一个个的比较,直到找到相等的才算完事。这个方法,说实话也太笨了,简直不是我这种懒人应该做的事。就不能有种方法直接看到这个数,就直接在数组中查到位置嘛?!诶,你别说,还真有。因为总有比我更懒的,我只是懒是只能躺着,人家大佬的懒是直接动手解决,果然那句”懒是第一生产力“!定义这个就是我今天要给家人们带来的哈希表。哈希表,别名儿叫散列表(Hash Table)。我在上面说,希望有种方法,直接看到数,就知道它在数组中的位置,其实里就用到了哈希思想。哈希思想就是说不用一些无用的比较,直接可以通过关键字 key 就能找到它的存储位置。这里举一个栗子(可不是堂嫂栗子哦),可能更清楚点:智能班有 40 个学生,每个学生的学号由入学年份 + 年级 + 班级 + 编号组成,例如 2020.20.01.32 是 2020 年入学的 20 级 01 班的 32 号同学。(学号怕你看混,给你个"."分割,贴心不~)现在有个需求(烦人的 PM):我们需要快速找到某个同学的个人信息。那这个时候我们就要建立一张表,按理来说我要是想要知道某个同学的个人信息,其实就知道学号就好了,但是在这不行,学号的值实在太大了,我们不能把学号当作下标。学号不可以,那什么可以呢?我们定睛一看,咦,编号可以呀,编号是从 1 ~ 40。(我真是一个小聪明啊)那咋取到编号?不就是学号对 2020.20.01.00 取余就 KO 了嘛。(你不会没理解把,不就是相当于(上面栗子 32 这位大帅哥),32/00=32 嘛)此时,如果我们想查学号为 2020.20.01.32 的学生个人信息,只要访问下标为 32 的数据即可。其实这就可以在时间复杂度为 O(1) 内解决找到。(不要问我什么是时间复杂的,什么是空间复杂度,生产队的 LV 马上更,不要打拉)秒男实锤了。(这摸快,O(1),比三秒都快哦)用公式表示就是:存储位置 = f(关键字)。这里的 f 又叫做哈希函数,每个 key 被对应到 0 ~ N-1 的范围内,并且放在合适的位置。在上面的例子中 f(key) = key % 2021210100。存储时,通过同一个哈希函数的计算 key 的哈希地址,并按照此哈希地址存储该 key。最后形成的表就是哈希表,它主要是面向查找的存储结构,简化了比较的过程,提高了效率。示例上面看明白的话,那再举个大栗子加深点印象。有个 n = 10 的数组,哈希函数 f(key) = key % 10,将 4,10,11,19,29,39 散列到数组中。哈希函数确定后,剩下的就是计算关键字对应的存储位置。4 % 10 = 4,所以将 4 放入下标为 4 的位置。 10 % 10 = 0,所以将 10 放入下标为 0 的位置。11 % 10 = 1,所以将 11 放入下标为 1 的位置。19 % 10 = 9,所以将 19 放入下标为 9 的位置。29 % 10 = 9,所以将 29 放入下标为 9 的位置。但是现在问题来了,下标为 9 的这个坑已经被 19 占了,这 29 计算出来的下标冲突了。(作为工具人的我,呜呜,就让我为你来平定冲突和亲去,昭君我来了)这种情况有个学名,叫:哈希(散列)冲突。处理冲突对于两个不相等的关键字 key1 和 key2,若 f(key1) = f(key2),这就是哈希冲突。key1 占了坑,那 key2 只能想别的办法,啥办法呢?一般处理哈希冲突有以下几种办法:开放定址法再哈希(散列)函数法链地址法。。。(别想了,就等你来创造一个,作为算法“行业冥灯”,击垮他们!)开放定址法一旦发生冲突,就选择另外一个可用的位置。开放定址法有 2 个最常用的策略:线性探测法二次探测法线性探测法线性探测法,顾名思义,直来直去的探测。且看它的公式:f(key) = (f(key) + di) % m (di = 1, 2, 3, ... , m-1)。我还是用“哈希示例”中的栗子(栗子都快熟了):n = 10 的数组,哈希函数 f(key) = key % 10,将 4,10,11,19,29,39 散列到数组中。到了 29 的时候,29 % 10 = 9。但此时下标 9 已经有了元素 19,所以此时探测下一个位置 (9 + 1) % 10 = 0。下标为 0 的位置上已经有了元素 10,所以继续探测下一个位置 (9 + 2) % 10 = 1。下标为 1 的位置上也有了元素 11,所以还得继续探测下一个位置 (9 + 3) % 10 = 2。下标为 2 的位置上总算是空的,因此 29 找到了家(我家的猫不会跳舞,但是会爬树):不知道你发现了没,对于 29 这个来说,本来只是和 19 冲突,整着整着和 10,11 也冲突了。这样就使得每次要处理好几次冲突,而且这样会出现大量数字聚集在一个区域的情况,大大降低了插入和查找的效率。后来不知道哪个大佬在线性的基础上做了改进,捣鼓出二次探测法。二次探测法二次探测法就是把之前的 di 整成了平方,公式如下:f(key) = (f(key) + di) % m (di = 1², -1², 2², -2², ..., q², -q², q ≤ m/2)比如对于 29 来说,下标为 9 的位置上呆了 19,所以此时探测下一个位置 (9 + 1²) % 10 = 0。下标为 0 的位置上占了元素 10,那下一次就探测上一个位置 (9 - 1²) % 10 = 8。下标为 8 的位置上空着,直接占住:再哈希函数法再哈希的话,就是不只是一个哈希函数,而是使用一组哈希函数 f1(key)、f2(key)、f3(key)....。当使用第一个哈希函数计算到的存储位置被占了,那就用第二个哈希函数计算,反正总会有一个散列函数能把冲突解决掉。依次类推,直到找到空的位置,然后占住。当然这种方法不会出现大量数字聚集在一个区域的情况,但这种情况明显增加了计算的时间。链地址法是谁说出现冲突后只能找别的坑位的,几个人蹲一个坑它不香嘛。(还记得伦敦的谜语吗)可能真的有巨佬觉得香,然后就整出了链地址法。链地址法呢是将得出同一个结果的 key 放在一个单链表中,哈希表存储每条单链表的头指针。还是用老栗子:n = 10 的数组,哈希函数 f(key) = key % 10,将 4,10,11,19,29,39 散列。最后得到如下图:你看,链地址法就不会出现此坑被占,就得去找别的坑的情况。大家一起蹲,这样就绝不会出现找不到地址的情况,而是直接插到对应的单链表中即可,所以插入的时间复杂度为 O(1)。当然有得必有失,这样的情况必然造成了查找上的性能损耗,这里的查找的时间复杂度为 O(k),其中 k = n / 单链表条数。第五章 | 初探数据结构—字符串-敬请期待~~~写在结尾[彩蛋大放送]首先非常感谢InfoQ 携手阿里云打造写作培训课程,助力第三季InfoQ 百位优质创作者签约计划在这里看到了很多优秀创作者大佬分享自己的独特经验,我竟然和自己的偶像[B站UP]三太子敖丙一起连麦聊了一会,同时也解答了我对敖丙开发转运营一直以来的困惑~~很高兴大家能看到这里,其实文章写到你可能感觉这篇文章好混乱,有我命由我不由天的大格局,有幽默的荤段子,有工作中的小调侃,有小小的自嘲,有历史重现......诸多元素的在技术文章的奇妙碰撞,也独有一番风味;突然写到结尾就有了一些感慨,这让我想到了前段时间infoQ送给优秀开发者的T恤——极客青年还请忽略师叔的小肚子,其实之前我一直以为这几个字比较呆版,但是他点醒了我,极客是我们的工作,但是重点我们还是青年,我们可能技术不是大佬,但是我们都有一个共同点热爱分享技术!正式因为我们 share and love (分享热爱)通过我们的每一篇文章在某一刻在启发正在疑惑的年轻人也许我很微小,文章很浅薄,但是我还是青年而且我还有你们,同为社区开发者的道友就让你我用奇思妙想来分享属于技术让技术变得有湿度让技术变得有爱最后让“极客青年”的我们,通过社区的方式,共同努力帮助更多的伙伴们!
第一章 开发环境部署(Python的安装与配置、虚拟环境、PyCharm安装与使用)略第二章 Flask快速入手(Web基础知识、第一个Flask Web 程序、URL传递参数,UPL反转、页面跳转和重定向)2.1 Web基础知识Web(World wide Web)即全球广域网,也成为万维网——一种基于超文本和HTTP协议的、全球的、动态交互的、跨平台的分布式图形信息系系统。万维网的工作原理:1、当用户打开浏览器,并在浏览器中输入网址时,浏览器会分析出网页文件URL(统一资源定位符)。2、浏览器向DNS(域名系统)发出请求,要求把域名转化为IP地址。3、域名解析服务器进行查询后,向浏览器发出解析后的IP地址。4、HTTP协议工作开始,浏览器向该IP地的80端口发送建立一条TCP连接的请求。5、浏览器与与服务器连接建立成功后,浏览器会向服务器发出一条请求传输网页的HTTP命令。6、服务器收到请求后,向浏览器发送相应网页文件。7、文件发送完成后,服务器主动关闭TCP连接。连接释放,HTTP的工作过程结束。8、浏览器显示收到的网页文件。2.2 第一个Flask Web 程序2.2.1 安装Flask框架在pycharm中安装Flaskpycharm汉化教程——Python以及Pycharm安装、汉化详细教程_tianhai12的博客-CSDN博客_python汉化教程1.安装——设置——项目:Flask——Python解释器——+2,。在搜索框上打出(flask)——安装2.2.2 在Flask中输出 Hello Flaskfrom flask import Flask#从flask框架引入Flask对象app = Flask(__name__)#创建flask的应用对象,传入__name__这个变量来初始化Flask对象@app.route('/')#使用route()装饰器注明通过什么样的URL可以访问函数def index(): """定义视图函数""" return 'Hello Flask!'if __name__ == '__main__': app.run()运行结果:2.3 URL传递参数注意 "接收到的名字为:%s"%name这段语句必须紧密相连#encoding:utf-8from flask import Flaskapp = Flask(__name__)@app.route('/')def hello_world(): return '这是url传参演示!'@app.route('/user/<name>')def list_name(name): return "接收到的名字为:%s"%name@app.route('/news/<int:id>')def list_news(id): return "接收到的id为:%s"%idif __name__ == '__main__': app.run(debug=True)运行结果:int类型(第二章图片为1.1,不是int类型报错) 2.4 UPL反转(没有运行出来)# encoding: utf-8from flask import Flask,url_forapp = Flask(__name__)@app.route("/")def index(): url1=(url_for('news',id='10086')) return "URL反转内容为:%s"%url1if __name__ == '__main__': app.run(debug=True)2.5 页面跳转和重定向#endoding:utf-8from flask import Flask,url_for,redirectapp = Flask(__name__)@app.route('/')def hello_world(): print("首先访问了index()这个视图函数!") url1=url_for('user_login') return redirect(url1)@app.route('/user_login')def user_login(): return "这是用户登录界面,请您登录,才能访问首页!"if __name__=="__main__": app.run()
一个双非计算机学生的长远规划(考研篇)作者目前大二,此篇文章只是自己对考研的一些见解。一、计算机研究生(专硕)1.满足名校梦一次考试的失利成就现在的自己,深思以后,发现没有那么多抱怨,反而有点解脱,也许这是属于我人生最好的安排,虽然学校不好,但是有幸被安排到了一个自己喜欢的专业人工智能(计算机方向)。虽然有幸但是不甘心,“十年饮冰,难凉人心”这是一个今年考哈工程学长(赵越)的座右铭,在和他相处这段时间,真正被那种义无反顾所震撼,自己体验一次全力以赴,在他说来就是难忘而美好!(目前自己也认为一个明确目标奋斗是非常美好的,这种经历难忘刻苦的)。父母方面,感觉有些愧对父母,发现当其他人问起我的学校,总有一点迟疑,但是那一刻的沉默往往是自己最难受的时候,心里百般滋味~2.更多的选择现在研究生学历的人越来越多,从而本科生的身份越来越不值钱。也导致现在研究生考试越来越卷,计算机专业更是上升到了新的维度。比如自己向往的大学岗位,大学生是思想最活跃的群体,无论自己工作到什么时候,都不会落伍。喜欢大学开放的思维和管理模式,更加充裕的时间,来做自己有兴趣的领域,工作环境的向往。辅导员(行政路线),大学老师(科研路线,助教-讲师-教授),研究生才是迈进大学最低门槛。政府岗位,青年也有为政一方,造福百姓的政治抱负。选调生,因为自己资格问题,可能不能报考市直选调,在这里说明一下,我可以报考乡镇的选调生,但是因为家庭等方面的因素,还是有大城市的报复的。相反市直比乡镇就多了一点稳定和上升空间,也希望借助考研这个平台来实现自己报复。市直最低要求,世界一流大学的本科生和一流建设学科高校辽宁紧缺的专业的研究生(包括计算机方向)。面对互联网大厂,学校出身可能真的是影响比较大。虽然程序员技术能力占比比较大,但是职位上升肯定会受到一些影响,比如程序员的中年危机,如果不能顺利的转管理岗,面临着失业危机,这是时候基本身后都有一个家庭,所以压力山大,这时候一个不错学校的研究生学历能让你多一成保障,也可能让自己更加从容的面对。现在还年轻试错成本还比较低,真想要在这个年纪折腾折腾,这莫一想,面对大厂的加班风波也更加表示理解(在身体健康的条件下,当然也非常佩服腾讯实习生直接diss主管的魄力)。喜欢大厂还是有一点人生信仰的,从小比较爱看雷军的发布会(米粉一枚),对雷军还是有点崇拜,在50岁还要继续奋斗,投入互联网公司造车的热潮!功成名就,但还要在一个零基础的产业进行再次创业,用雷总自己的话说:人生最后一站为小米汽车而战!感觉这种来自企业家的拼搏精神真是用他写的那本书《一往无前》完美诠释。这也是值得我追随的!3.人生伴侣和为下一代提供更好的平台当然小我考虑好了就应该考虑大我了,其实也不算大我,就是考虑家庭层面,伴侣的选择,未来孩子的培养和发展(现在连女朋友,考虑是不是有点早~)!张雪峰老师的一段话:我的孩子不用考研,因为我已经为挣了一辈子的钱。™的,有点想骂人了,但是细想,的确是这莫一回事,有很多啃老族还有什么富二代啊~的确有点羡慕,但是还是要注重当下,本人农村家庭,也就是正常家庭,也许因为家庭的原因更加向往大城市(一个人没有什么,就渴望得到什么,虽然有点狭隘,但是在这件事上成立啊),所以我还是有点报复的,年轻气盛嘛!但是现在网上有些描述,青年的理想报复终将被社会和现实打败。我呢,不否定这个观点,毕竟学校和社会肯定有点不一样,但是年轻人总要活出不一样的风采。哈哈,唠远了!要是研究生学历找对象基本就是研究生学历,最次是本科生。农村里的奇怪观念,但是一想也对!大概率事件还是值得一做的,毕竟性价比高嘛(严谨点)!所以吧,就希望我和我的伴侣(假设的)能在一个更好的平台上,为下一代生活能过的快乐充实点。
学生管理系统分为两个模块:一个负责编辑增删改查的函数命名(student.py),另一个负责调用类来实现功能(demo.py)。类似于前后端(假想的)首先先建立数据库(注意数据类型)CREATE DATABASE stuman; USE stuman; CREATE TABLE student( sno INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT, sname VARCHAR(20) NOT NULL, gender VARCHAR(1) NOT NULL, birthday DATE, address VARCHAR(50) NOT NULL ) ENGINE=INNODB DEFAULT CHARSET=utf8;在MySQL8.0以上版本出现的问题如果目标计算机积极拒绝链接,则需要配置环境变量如果出现,1045, &quot;Access denied for user 'mysql80'@'localhost' (using password: YES)则类同于SQLyog 报错2058所以连接 mysql 8.0.11 解决方法解决方法:windows 下cmd 登录 mysql -u root -p 登录你的 mysql 数据库,然后 执行这条SQL: ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';上面的password是你的密码(方便好记1234)student.pyimport pymysql connect = pymysql.connect(host='localhost', port=3306, user='root', password='1234', database='stuman', charset='utf8') cur = connect.cursor() class Student(): def __init__(self,sno,name,gender,birthday,address): self.sno= sno self.name= name self.gender= gender self.birthday= birthday self.address = address def inputstudent(self):#增加 sno=int(input("请输入你要添加的学号:")) name=input("请输入你要添加的姓名:") gender = input("请输入性别(1.男 2.女):") birthday= input("请输入出身年月日:") address=input("请输入IP地址:") sql="insert into student values(%s,%s,%s,%s,%s)" date=(sno,name,gender,birthday,address) cur.execute(sql,date)#数据提交 connect.commit()#数据更新 def deletestudent(self):#删除 sno=input("请输入你要删除学生的学号:") sql="delete from student where sno=%s" cur.execute(sql,(sno,)) print("执行删除命令,剩余如下") sql2 = "select * from student" cur.execute(sql2) data = cur.fetchall() for item in data: print(item) connect.commit() def updatestudent(self):#修改 no=input("请输入你要修改的学生学号:") sql1="select * from student where sno=%s" cur.execute(sql1,(no,)) date = cur.fetchall() print(f"学号为{no}的学生信息如下:\n",date) print("请输入新的信息!") sname=input("请输入你要添加的姓名:") sgender = input("请输入性别(1.男 2.女):") sbirthday= input("请输入出身年月日:") saddress=input("请输入IP地址:") sql2="update student set name=%s, gender=%s, birthday=%s ,address=%s,where sno=%s" sdate=(sname,sgender,sbirthday,saddress,no) cur.execute(sql2,sdate) sql3="select * from student where sno=%s" cur.execute(sql3,(no,)) newdate=cur.fetchone() print("修改之后该学生信息如下:\n",newdate) connect.commit() def searchstudent(self):#查询 sno = input("请输入您要查询学生的学号:") sql1="select * from student where sno=%s" cur.execute(sql1,(sno,)) date=cur.fetchone() print(f"学号为{sno}的学生信息如下:\n",date) connect.commit() def allinstudent(self):#显示所有学生 print("所有学生信息如下:") sql1="select * from student" cur.execute(sql1) date=cur.fetchall() print(date)demo.pyimport pymysql from student import Student as a #定义功能函数界面 def info_print(): print("请选择功能--------------------") print("1、添加学员") print("2、删除学员") print("3、修改学员") print("4、查询学员") print("5、显示所有学员") print("6、退出系统") print('-'*20) #用户变量循环使用,直到用户输入6,才退出系统1 def main(): with pymysql.connect(host='localhost', port=3306, user='root', password='1234', database='stuman', charset='utf8') as conn: cur = conn.cursor() while True: info_print() #1.显示功能界面 #2.用户输入功能序号 user_num = int(input("请输入功能序号:")) #3.按照用户输入的功能序号,执行不同的功能(函数) #如果用户输入1,执行添加;如果用户输入2,执行删除----多重判断 if user_num == 1: print("添加") a.inputstudent(cur) elif user_num == 2: print("删除") a.deletestudent(cur) elif user_num == 3: print("修改") a.updatestudent(cur) elif user_num == 4: print("查询") a.searchstudent(cur) elif user_num == 5: print("显示所有成员信息") a.allinstudent(cur) elif user_num == 6: print("退出系统") exit_flag = input("确定要退出吗?yes or no ") if exit_flag == 'yes': break else: print("输入有误!") main()希望我的文章对你有所帮助!
知识点: Python 元组 tuple() 函数将列表转换为元组。 Python strip() 方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列 Python split() 通过指定分隔符对字符串进行切片,如果参数 num 有指定值,则分隔 num+1 个子字符串 tuple(i.strip().split(","))首先创建一个数据库demo,在建立一个有no(自增),name,birthday,department的表student:CREATE DATABASE demo; USE demo; CREATE TABLE student( NO INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT, NAME VARCHAR(20) NOT NULL, birthday DATE, department VARCHAR(50) NOT NULL ) ENGINE=INNODB DEFAULT CHARSET=utf8demo.py代码如下(附有注释,请放心使用):import pymysql '''#把TXT文件变成csv with open("aa.txt","r") as a: data = a.readlines() with open("aa.csv","w") as b: for i in data: b.write(i)''' #读取csv with open("aa.csv","r") as c: data= c.readlines() data_list = [] for i in data: tp=tuple(i.strip().split(",")) data_list.append(tp) insert_list=data_list[1:] print(insert_list) with pymysql.connect(host='localhost', port=3306, user='root', password='1234', database='demo', charset='utf8') as conn: sql = "INSERT INTO student values(NULL,%s,%s,%s)" cur=conn.cursor() cur.executemany(sql,insert_list) conn.commit()返回mysql查看结果:
2022年08月